Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Squash and stretch
--# Main
-- Sandbox
supportedOrientations(LANDSCAPE_ANY)
displayMode(FULLSCREEN_NO_BUTTONS)
--displayMode(OVERLAY)
--displayMode(FULLSCREEN)
function setup()
screen=vec2(WIDTH,HEIGHT)
loadAllLevels()
level=readLocalData("level",1)
stars=readLocalData("stars",1)
loadUserData()
-- resetUserData()
profiler.init(true)
strokeWidth(10)
lineCapMode(ROUND)
smooth()
stroke(0, 243, 255, 130)
-- font("Vegur-Bold")
font("Noteworthy-Bold")
textAlign(CENTER)
centre=screen*0.5
assets()
walls=Walls{}
objects={}
splash=Splash() --scene manager uses 2 variables, game and splash. game is either Game(), Tutorial(), Editor(), ie main game engine. splash is anything else, which might be running over the top of the game. The scene variable points to either game or splash.
-- game=Tutorial()
-- game=Editor()
cam={scale=1, fov=45}
-- cameraOrient()
end
function draw()
background(40, 40, 50)
scene:draw() --all draw, collide, touched events routed to scene variable
profiler.draw()
end
function collide(contact)
scene:collide(contact)
end
function touched(touch)
tpos=vec2(touch.x, touch.y)
scene:touched(touch)
end
--# LevelIO
-- Level IO
function resetLevel() --soft reset
physics.pause()
for i,v in pairs(objects) do
v:respawn()
end
cameraOrient(true)
end
function clearLevel()
for i,v in pairs(objects) do
v:destroy()
end
moving=false
poly={}
objects={}
ball, goal = nil, nil
end
function clearShapes()
for i,v in ipairs(poly) do
v:destroy()
end
poly={}
end
function newGame(editor)
if editor then game=Editor() else
checkUserData()
if userData[level].tutorial then
game=Tutorial()
else
game=Game()
end
end
end
function saveLevel()
levels[level]={} --clear what was previously saved here
local n=1
local u=levels[level]
u.NAME=levelName
u.items={}
for i,v in pairs(objects) do
if v.id==id.platform or v.id==id.poly then
local points={} --json cannot encode vec2s
for a,b in ipairs(v.body.points) do
points[a]={x=math.floor(b.x),y=math.floor(b.y)} --so convert to x,y tables
end
u.items[n]={id=v.id, args={x=math.floor(v.startX), y=math.floor(v.startY)}, vectors=points}
else
u.items[n]={id=v.id, args={x=math.floor(v.startX), y=math.floor(v.startY)}}
end
n = n + 1
end
print((n-1).." items saved")
local str=json.encode(levels)
saveProjectTab("Levels", "--"..str.."--") --leading dashes make json appear as comment block. nb unneccesary if you were saving as local data (which you would have to if you were exposing level editor to end user, save tab wouldnt work, as tabs become read-only)
end
function loadAllLevels()
local str=readProjectTab("Levels")
levels=json.decode(string.sub(str,3,-3))
end
function loadLevel(lev)
clearLevel()
moving=true
objects={}
levelName=levels[lev].NAME
saveLocalData("level", lev) --remember what level player is on
local funcs={Ball, Crate, Shelf, Goal, Platform, Shape} --must match id array in Body tab
for i,v in ipairs(levels[lev].items) do
if v.id==id.platform or v.id==id.poly then --platform
local points={} --json cannot encode vec2s
for a,b in ipairs(v.vectors) do
points[a]=vec2(b.x,b.y) --so convert back to vec2
end
funcs[v.id]({x=v.args.x, y=v.args.y}, points)
else
funcs[v.id]({x=v.args.x, y=v.args.y})
end
end
end
function resetUserData()
userData={}
for i=1,#levels do
userData[i]={score=0, stars=0}
if i<4 then
userData[i].tutorial=true
end
end
userData[1].unlock=true
local str=json.encode(userData)
saveLocalData("userData", str)
end
function loadUserData()
local str=readLocalData("userData", false)
if str then
userData=json.decode(str)
else
resetUserData()
end
checkUserData()
end
function checkUserData()
local diff=#levels-#userData
for i=1,diff do
userData[#userData+1]={score=0, stars=0, unlock=true}
end
end
function saveUserData()
local str=json.encode(userData)
saveLocalData("userData", str)
end
--# SCENE
Scene = class() --a superclass for the scene manager. nb to aid navigation all superclasses have a tab name ALL IN CAPS
function Scene:init()
self.pos=vec2(centre.x, centre.y)
self.buttons={}
scene=self --scene variable points to the top-most active scene (nb possible for 2 scenes to run at once, ie ingamemenu on top of game etc)
end
function Scene:draw()
sprite("Cargo Bot:Background Fade", centre.x, centre.y, WIDTH, HEIGHT)
if self.mesh then
pushMatrix()
translate(self.pos.x, self.pos.y)
self.mesh:draw()
popMatrix()
for i,v in ipairs(self.buttons) do
v:draw()
end
else
self:setup()
end
end
function Scene:touched(touch)
if touch.state==ENDED then
for i,v in ipairs(self.buttons) do
v:test(tpos)
end
end
end
function Scene:collide(contact)
--dummy. No collisions in editor, ingamemenu, levelwin
end
function Text(t) --creates text/image meshes. usage: mymesh=Text(), mymesh:draw(). parameters: note,x,y,col,back, size, mode, w, h, tint
pushMatrix()
pushStyle()
local size=t.size or 40
fontSize(size)
local str=t.note or ""
local w,h
if t.w then
w=t.w
h=t.h or w
else
w,h=textSize(str)
w = math.max(w * 1.3,180)
h = h * 1.3
end
local img=image(w,h)
setContext(img)
local mode=t.mode or CENTER
textMode(mode)
local cx,cy=0,0
if mode==CENTER then
cx=w*0.5
cy=h*0.5
end
local backCol=t.tint or color(255)
if t.back then tint(backCol) sprite(t.back, cx, cy, w, h) noTint() end
if t.stars then
local x=w*0.25
for i=1,t.stars do
sprite("Cargo Bot:Star Filled", x*i, h*0.5, x*0.9)
end
local empty=clamp(t.stars+1,1,4)
for i=empty, 3 do
sprite("Cargo Bot:Star Empty", x*i, h*0.5, x*0.9)
end
end
fill(0, 0, 0, 12)
text(str, cx-1, cy-1)
text(str, cx, cy-1)
text(str, cx+1, cy-1)
text(str, cx+2, cy-1)
text(str, cx, cy-2)
text(str, cx+1, cy-2)
local col=t.col or color(255)
fill(col)
text(str, cx, cy)
setContext()
local m=mesh()
m.texture=img
local x=t.x or 0 -- WIDTH * 0.5
local y=t.y or 0 --HEIGHT * 0.5
m:addRect(x,y,w,h)
-- m:setRectTex(1,0,0,1,1)
popStyle()
popMatrix()
return m, w, h
end
--# Splash
Splash = class(Scene) --title page
function Splash:init()
local title="Squash & Stretch"
-- local letter={}
self.meshes={}
self.seeds={}
local len=string.len(title)
for i=1,len do
local r=math.random(128, 196)
self.meshes[i]=Text({note=string.sub(title, i, -len+(i-1)), size=math.random(350,400), col=color(0,90, 160)}) --math.random(250,400)
self.seeds[i]=math.random(5000)
end
Scene.init(self)
end
--readImage("Cargo Bot:Level Select BG")
function Splash:draw()
Scene.draw(self)
--
-- sprite("Cargo Bot:Opening Background", centre.x, centre.y, WIDTH)
pushMatrix()
--blendMode(SRC_ALPHA, ONE_MINUS_SRC_ALPHA)
-- blendMode(MULTIPLY)
blendMode(ADDITIVE)
translate(WIDTH*0.2,HEIGHT*0.7)
local mag=0.09
for i=1,#self.meshes do
-- pushMatrix()
local n1=mag+(noise(ElapsedTime+i, self.seeds[i])*mag)
local n2=mag+(noise(self.seeds[i], ElapsedTime+i)*mag)
-- local n1=RotationRate.x*mag
-- local n2=RotationRate.y*mag
-- translate(WIDTH*n1*((i-1)%7),-HEIGHT* 0.3 * (i//9)+1)
-- translate(WIDTH*((i-1)%9)*n1, -HEIGHT* 0.3 * (i//9)+1)
translate(WIDTH*n1,0)
pushMatrix()
translate(0,HEIGHT*n2)
scale(0.4+n1*5,0.4+n2*5)
self.meshes[i]:draw()
popMatrix()
if i==7 then translate(-WIDTH*0.8, -250) end
end
popMatrix()
blendMode(NORMAL)
end
function Splash:setup()
local m=mesh()
m.texture="Cargo Bot:Opening Background"
m:addRect(0,0,WIDTH,HEIGHT)
self.mesh=m
local y=HEIGHT*0.15
self.buttons={
TextButton({no=1,of=4, y=y,note="START\nGAME", tint=color(55,255,64),
callback=function()
splash=LevelSelector()
end}),
TextButton({no=2,of=4, y=y,note="Reset\nprogress",
callback=function()
resetUserData()
splash.buttons[2]:toggle()
end}),
TextButton({no=3,of=4, y=y,note="Level\nEditor",
callback=function()
splash=LevelSelector(true, "Choose a level to edit")
end}),
TextButton({no=4,of=4, y=y,note="EXIT\n",
callback=function()
close()
end})
}
-- font("Vegur-Bold")
end
--# LevelSelector
LevelSelector = class(Scene)
function LevelSelector:init(editor, note)
ortho()
viewMatrix(matrix())
clearLevel()
local note=note or "Choose a level"
note=note.."\n\n\n\n\n\n\n\n\n\n\n"
self.mesh=Text({note=note, w=WIDTH, h=HEIGHT, col=color(255), back="Cargo Bot:Opening Background"})
self.pos=vec2(0,0)
self.buttons={TextButton({no=4, of=4, y=HEIGHT*0.95, note="Back to menu",
callback=function() splash=Splash()
end
})}
local h=4
self.w=math.ceil(#levels/h)
self.step=screen/(h+0.5)
local size=self.step *1.05
for i=1,#levels do
self.buttons[i+1]=LevelButton(i, self.w, h, self.step, size,
function()
tween.delay(0.05,
function()
level=self.buttons[i+1].level
newGame(editor)
end)
end)
end
scene=self
end
function LevelSelector:draw()
pushMatrix()
translate(centre.x,centre.y)
self.mesh:draw()
popMatrix()
self.buttons[1]:draw()
translate(self.pos.x, self.pos.y)
for i=2,#self.buttons do
self.buttons[i]:draw()
end
end
function LevelSelector:touched(touch)
if touch.state==MOVING then --scroll the screen if there are lots of levels
self.pos.x=clamp(self.pos.x+touch.deltaX, (-self.step.x*(self.w+1))+WIDTH, 0)
-- self.pos.y = self.pos.y + touch.deltaY
end
tpos=tpos-self.pos --adjust touch point for screen scrolling
Scene.touched(self, touch)
end
--# Game
Game = class(Scene)
newObj={}
local step=15 --gap between dots for shapes drawn
local touchAction, bod1, bod2, anchor, anchor2, orient, oldTime, grav
local sensitivity =0.25 --how much to scale down users tilting
local maxTilt = 0.2 --how much the world can be tilted in pi radians (ie 1=180 degrees)
function Game:init()
self.id="game"
parameter.clear()
profiler.init(true)
parameter.action("Level Editor", function() clearLevel() game=Editor() end)
-- parameter.watch("gravAngle")
-- walls=Walls()
loadLevel(level)
poly={}
self:buttonSetup()
scene=self
moving=true
physics.pause()
touchAg=vec2(0,0)
self.mode=1
print("level"..level)
oldTime=ElapsedTime-1 --force an update to scoreline mesh
self.aims={polys=0, timer=ElapsedTime} --goals by which stars are awarded. nb only reset on hard reset
-- LevelWin=false
cameraOrient(true)
end
function Game:draw()
self:gravity()
cameraMove()
perspective(cam.fov)
camera(cam.eye.x, cam.eye.y, cam.eye.z, cam.ori.x, cam.ori.y, cam.ori.z, -grav.x,-grav.y,0) --camera up also affected by device gravity, for visual feedback
walls:draw()
for i,v in pairs(objects) do
if v.kill then
sound(SOUND_HIT, 22054)
v:destroy()
objects[i]=nil
else
v:draw()
end
end
ortho()
-- Restore the view matrix to the identity
viewMatrix(matrix())
for i=1,#newObj do
local v,w =newObj[i]
if i<#newObj then w=newObj[i+1] else w=tpos end
line(v.x,v.y,w.x,w.y)
end
for i,v in pairs(self.buttons) do
v:draw()
end
self:overlays()
end
function Game:gravity()
local gravAngle=(math.atan2(Gravity.y,Gravity.x)+math.pi*0.5)-orient --make down=0 radians
gravAngle=clamp(gravAngle*sensitivity, -math.pi*maxTilt, math.pi*maxTilt) --reduce sensitivity of tilt, and clamp it
grav=vec2(0,-1):rotate(gravAngle)
physics.gravity(grav*313)
end
function Game:overlays()
if not moving then
self.aims.timer = self.aims.timer + DeltaTime --"freeze" timers
oldTime = oldTime + DeltaTime
elseif ElapsedTime-oldTime>=1 then
oldTime=ElapsedTime
self:scoreLine()
end
self.timeMesh:draw()
end
function Game:scoreLine()
local time=math.floor(oldTime-self.aims.timer)
local col=color(0, 167, 255, 255)
self.timeMesh=Text({note=string.format("%d:%.2d", time//60, time%60), col=col, x=centre.x, y=HEIGHT-30, mode=CORNER})
end
function Game:touched(touch)
-- tpos=vec2(touch.x,touch.y)
local actions={Game.drawShape, Game.addJoint}
actions[self.mode](self, touch)
end
function Game:addJoint(touch)
if touch.state==BEGAN then
bod1= pointBody(tpos)
if bod1 then newObj[1]=tpos
else
self:cue({key="jointStart", focus=tpos, priority=1})
end
elseif touch.state==MOVING and bod1 then
newObj[2]=tpos
elseif touch.state==ENDED then
if bod1 then
bod2= pointBody(tpos)
if bod2 then
local delta=(tpos-newObj[1])*0.25
if bod1.body.type~=STATIC then
bod1.body.position = bod1.body.position + delta
bod1.body.density=1
end
if bod2.body.type~=STATIC then
bod2.body.position = bod2.body.position - delta
bod2.body.density=1
end
bod2.posZ = bod2.posZ + 50
bod2.joint=Joint(bod1.body, bod2.body, tpos-(delta*2)) --physics.joint(REVOLUTE, bod1.body, bod2.body, tpos-(delta*2)) --newObj[1],
self:cue({key="joint4", focus=self.buttons.menu, priority=1})
else
self:cue({key="jointEnd", focus=tpos, priority=1})
end
end
bod1, bod2= nil, nil
newObj={}
self.buttons.joint:action() --exit joint add mode
self.buttons.joint:toggle()
end
end
function Game:drawShape(touch)
if touch.state==BEGAN then
if not pointBody(tpos) then --check point is not inside another object
newObj={tpos}
touchAg=tpos
touchAction=BEGAN
end
elseif touch.state==MOVING then
if #newObj==0 then --in case touch event started before beginning of game
if touchAction==MOVING then -- still holding old object
anchor.linearVelocity=vec2(touch.deltaX, touch.deltaY)*10
-- anchor.position=tpos --too much control
elseif not pointBody(tpos) then --create new object
newObj={tpos}
touchAg=tpos
end
elseif tpos:dist(newObj[#newObj])>step then
local last=newObj[#newObj]
--local blocked=physics.raycast(last, tpos) --check to see if line intersects other objects
--if not blocked then
if #newObj<4 then --need at least 4 points to be potentially self-intersecting
newObj[#newObj+1]=tpos --add touch point to manifest
touchAg = touchAg + tpos
else --check to see shape is not self-intersecting
-- local intersection=false
for i=1, #newObj-1 do --step
local u, v =newObj[i], newObj[i+1]
if crossing(u,v,last,tpos) then --user has crossed line, closing off shape
-- intersection=true
for a=1,i do --remove all the points prior to the intersection
table.remove(newObj, 1)
end
if self:addPoly(touch) then
anchor=physics.body(CIRCLE, 30)
anchor.type=STATIC -- KINEMATIC
anchor.fixedRotation=true
anchor.position=tpos --newObj[1]
anchor2=physics.joint(REVOLUTE, poly[#poly].body, anchor, tpos)
touchAction=MOVING --actually "holding"
end
newObj={}
touchAg=vec2(0,0)
return
end
end
-- if not intersection then
local delta=vec2(touch.deltaX, touch.deltaY) --tpos-last --dont add extra points if user is drawing straight line
local deltaLast=last-newObj[#newObj-1]
if delta:normalize():dist(deltaLast:normalize())<0.4 then return end
newObj[#newObj+1]=tpos --add touch point
touchAg = touchAg + tpos
-- end
end
-- else
-- self:cue({key="blocked", priority=1})
--end
end
elseif touch.state==ENDED then
if #newObj>3 then
self:addPoly(touch)
elseif touch.tapCount==2 then
for i,v in ipairs(poly) do
if v.body:testPoint(tpos)==true then
v.kill=true
sound(SOUND_EXPLODE, 45852)
table.remove(poly, i)
end
end
elseif touch.tapCount==1 then
self:testButtons()
elseif touchAction==MOVING then
anchor2:destroy()
anchor:destroy()
anchor, anchor2 = nil, nil
end
newObj={}
touchAg=vec2(0,0)
touchAction=ENDED
end
end
function Game:testButtons()
for i,v in pairs(self.buttons) do
if v:test(tpos) then return i end
end
-- return false
end
function Game:addPoly(touch)
touchAg = touchAg / #newObj --average of all the vectors is the centre of the new object
for i=1,#newObj do --centre geometry around origin
newObj[i] = newObj[i] - touchAg
end
local newBod=Poly({position=touchAg}, newObj, vec2(touch.deltaX, touch.deltaY), tpos )
if newBod.body.mass>1 then
poly[#poly+1]=newBod --add poly
if self.id=="game" then self.aims.polys = self.aims.polys + 1 end
else
newBod:destroy() --too small, destroy
return false
end
return true
end
function pointBody(p)
for i,v in pairs(objects) do
if v.body and v.body:testPoint(p) then return v end
end
return nil
end
function Game:collide(contact)
local bodA,bodB = contact.bodyA, contact.bodyB
if bodA.type==DYNAMIC and objects[bodA] then objects[bodA]:hit(bodB, contact) end
if bodB.type==DYNAMIC and objects[bodB] then objects[bodB]:hit(bodA, contact) end
end
function Game:cue()
--dummy routine for when not in tutorial mode
end
function Game:buttonSetup()
local size, step = 60,20
self.buttons={
menu=GameButton({no=1, tex="Cargo Bot:Menu Button", trimL=0.68, on=0, size=size, step=step,
callback=function()
splash=InGameMenu()
end}),
reset=GameButton({no=2, tex="Cargo Bot:Replay Button", trimL=0.7, on=0, size=size, step=step,
callback=function()
resetLevel()
tween.delay(0.2, function() self.buttons.reset:toggle() end)
end}),
record=GameButton({no=3, tex="Cargo Bot:Record Solution Icon", on=0, size=size, step=step,
callback=function()
if isRecording() then
stopRecording()
self.buttons.record:toggle()
else
startRecording()
end
end})
}
if level>=2 then --dont introduce joint button until level 3
self.buttons.joint=GameButton({no=4, tex="Cargo Bot:Icon", on=0, size=size, step=step,
callback=function()
game.mode=3-game.mode
self:cue({key="joint3"})
end})
end
end
function cameraOrient(pan)
orient=math.atan2(Gravity.y,Gravity.x)+math.pi*0.5 --device orientation. nb once per level
grav=vec2(0,-1)
cam.dist=(centre.y*math.sin(math.pi*0.375))/math.sin(math.pi*0.125)/cam.scale --formula for working out correct camera distance for 3D projection to lineup with 2D physics in the Z=0 plane. assumes a normal field of view of 45 degrees. sides of tri add up to pi, 90 degree angle is pi*0.5, half of 45 degree fov is pi*0.125, so third angle is pi*0.375
if pan then
cam.fov=65
cam.ori=vec3(ball.pos.x, ball.pos.y, ball.PosZ) --vec3(centre.x,centre.y,0)
cam.eye=cam.ori+vec3(0,cam.dist*0.7,10) --cam.ori+vec3(0,0,cam.dist)
cam.eye.y=clamp(cam.eye.y, 0, HEIGHT*1.5)
-- track=true
cam.tween=tween(2.5, cam, {ori=vec3(centre.x,centre.y,0), eye=vec3(centre.x,centre.y,cam.dist), fov=45}, tween.easing.sineInOut, function() cam.tween=false physics.resume() end) --function() track=false end
else
cam.ori=vec3(centre.x,centre.y,0)
cam.eye=cam.ori+vec3(0,0,cam.dist)
end
end
function cameraMove()
if not cam.tween then
cam.eye.x = clamp(cam.eye.x - (RotationRate.y*3), screen.x*0.45, screen.x*0.55)
cam.eye.y = clamp(cam.eye.y + (RotationRate.x*3), screen.y*0.45, screen.y*0.55)
cam.ori.x = clamp(cam.ori.x - (RotationRate.y*0.5), screen.x*0.45, screen.x*0.55)
cam.ori.y = clamp(cam.ori.y + (RotationRate.x*0.5), screen.y*0.45, screen.y*0.55)
-- cam.ori=vec3(ball.pos.x,ball.pos.y, ball.posZ)
end
end
--# Tutorial
Tutorial = class(Game)
local timer, touchTimer
--local next = {}
local notes={
ball="Uh-oh\nThe ball is out of its box",
ball2="See if you can hit the ball with a shape",
box="You need to get the ball\nback in this box",
shape="Draw a shape \n with your finger",
shape2="You can close off a shape\nby lifting your finger off the screen",
shape3="But the shape will drop\nas soon as you lift your finger",
shape4="So a better way to close off a shape\nis to cross the line you were drawing",
shape5="Keep your finger on the screen\nafter you close the shape\nand you can hold on to it",
-- aim="See if you can make a shape \n that will knock the ball \n into the toy box",
destroy="Double-tap on a shape you've drawn \n to destroy it",
reset="The reset button \n puts all your creations \n back to their start positions",
menu="This is the menu button",
record="Hit this button to toggle screen recording",
ballDie="Don't let the ball fall into \n the bottomless chasm!",
goalDie="Don't let the toy box \n fall into the pit!",
joint1="Ok, let's start joining shapes together",
joint2="This is the JOINT button.\nPress it now to toggle joint mode",
joint3="Draw a line connecting the arm of the catapult\nto the platform it's sitting on",
joint4="Great! If the joint is in the wrong place\nyou can completely reset the level\nfrom the menu",
jointEnd="Your end point wasn't quite on the shape.\nTap the joint icon and try again",
jointStart="Your start point wasn't quite on the shape.\nTap the joint icon and try again",
-- blocked="You can't draw a shape \n that goes through \n anothehr object",
}
function Tutorial:init()
self.next={}
Game.init(self)
if level<3 then
if level==2 then
self:cue({key="shape2"})
self:cue({key="shape3"})
self:cue({key="shape4"}) --cue up non-context aware notifications
self:cue({key="shape5"})
end
self:cue({key="ball"}) --delays are cumulative
self:cue({key="shape"})
self:cue({key="ball2", focus=ball})
self:cue({key="box", focus=goal})
self:cue({key="destroy", delay=3})
self:cue({key="reset", delay=5, focus=self.buttons.reset})
self:cue({key="menu", focus=self.buttons.menu})
self:cue({key="record", focus=self.buttons.record})
elseif level==3 then
self:cue({key="joint1"})
self:cue({key="joint2", focus=self.buttons.joint})
end
print("tutorial")
touchTimer=math.huge
end
function Tutorial:draw()
Game.draw(self)
if moving then --do timers
if ElapsedTime>timer then self:trigger() end
if ElapsedTime>touchTimer+2 then self:resume() end --dont wait indefinitely to resume
else --"freeze" timers
timer = timer + DeltaTime
touchTimer = touchTimer + DeltaTime
end
if self.mesh then
pushMatrix()
translate(centre.x,screen.y*0.8)
self.mesh:draw()
popMatrix()
if self.mesh2 then
pushMatrix()
translate(self.pos.x, self.pos.y)
self.mesh2:draw()
popMatrix()
end
end
end
function Tutorial:cue(t) --key, delay, priority, focus
if notes[t.key] then --a given note can only be triggered once
local delay=t.delay or 2
local focus=t.focus or nil
local priority=t.priority or #self.next+1 --ie set pos to 1 for top priority note
table.insert(self.next, priority, {key=t.key, delay=delay, focus=focus})--add a key to the stack of whats coming up next
if priority==1 then timer=ElapsedTime+delay end --only set timer if this is first note up
-- print("next"..#self.next)
end
end
function Tutorial:trigger()
if #self.next>0 then --make sure a note exists
local v=self.next[1]
local col=color(0, 167, 255, 255)
self.mesh=Text({note=notes[v.key], col=col}) --display the oldest note that was added
if v.focus then
local p=v.focus.pos
self.pos=p-vec2(180,0)
self.tween=tween(0.5, self.pos, {x=p.x-60}, {easing=tween.easing.sineInOut, loop=tween.loop.pingpong})
self.mesh2=arrowMesh
end
notes[v.key]=nil --notes cannot appear more than once per session
table.remove(self.next, 1) --remove the oldest key from stack
-- physics.pause() --freeze action
-- tween.pauseAll()
touchTimer=ElapsedTime+0.5 --v slight delay to prevent accidental dismissal
end
timer=math.huge --prevent trigger function being repeatedly called
end
function Tutorial:resume()
self.mesh=nil
self.mesh2=nil
if self.tween then tween.stop(self.tween) self.tween=nil end
-- physics.resume() --resume action
-- tween.resumeAll()
if #self.next>0 then --cue up next note if there is one
timer=ElapsedTime+self.next[1].delay
end
touchTimer=math.huge
end
--# LevelWin
LevelWin = class(Scene)
function LevelWin:init(t)
if scene.id=="editor" then return end --cannot trigger a LevelWin state whilst in editor mode
local egoMassage={"Nice one!", "Pretty good", "You did it!", "Alright!", "Now we're talking", "Fantastic", "Get in!", "BOOM!", "Back of the net!"}
self.note=t.note or egoMassage[math.random(#egoMassage)]
local stars=1 --1 star for level completion
if game.aims.polys<4 then stars=2 end --2 for creating less than 4 objects
if ElapsedTime-game.aims.timer<60 then stars = stars + 1 end --a third star for completing in under a minute
userData[level].stars=math.max(stars, userData[level].stars) --save star rating if it is higher than users existing rating
if level<#levels then userData[level+1].unlock=true end
saveUserData() --save stars
Scene.init(self)
if game.mesh then game.mesh:clear() end --stop any cued notifications appearing underneath
if game.mesh2 then game.mesh2:clear() end
if game.next then game.next={} end
end
function LevelWin:draw()
game:draw()
-- sprite("Cargo Bot:Background Fade", centre.x, centre.y, WIDTH, HEIGHT)
Scene.draw(self)
end
function LevelWin:setup()
self.mesh=Text({note=math.floor(level)..". "..levels[level].NAME.."\n"..self.note.."\n\n\n\n", w=WIDTH*0.6, h=HEIGHT*0.75, col=color(255), back="Cargo Bot:Dialogue Box", stars=userData[level].stars})
self.buttons={
TextButton({no=1,of=3,note="Back to\nmain menu",
callback=function()
clearLevel()
splash=Splash() --LevelSelector()
end}),
TextButton({no=2,of=3,note="Replay\nLevel",
callback=function()
resetLevel()
tween.delay(0.05, function() scene=game end)
end}),
TextButton({no=3,of=3,note="NEXT\nLEVEL", tint=color(55,255,64),
callback=function()
clearLevel()
level = clamp(level + 1, 1, #levels)
newGame()
end})
}
end
--# InGameMenu
InGameMenu = class(Scene)
function InGameMenu:init()
physics.pause()
self.pos=vec2(centre.x, HEIGHT*0.4)
moving=false
scene=self
end
function InGameMenu:draw()
game:draw()
Scene.draw(self)
end
function InGameMenu:setup()
local col=color(31, 35, 96, 255)
self.mesh=Text({note=math.floor(level)..". "..levels[level].NAME.."\n\n\n", w=WIDTH*0.8, h=HEIGHT*0.4, col=col, back="Cargo Bot:Dialogue Box"}) --"Cargo Bot:Goal Area"
local total=3
self.buttons={
TextButton({no=1,of=total,note="QUIT to\nmain menu", callback=function()
clearLevel()
splash=Splash()
end}),
TextButton({no=2,of=total,note="Trash shapes\nand reset", callback=function()
--[[
clearShapes()
menuButton:toggle()
physics.resume()
moving=true
scene=game
]]
game=Game() --hard reset
end}),
TextButton({no=3,of=total,note="CONTINUE\nwith game", tint=color(55,255,64), callback=function()
game.buttons.menu:toggle()
physics.resume()
moving=true
scene=game
end
}),
}
end
--# Editor
Editor = class(Game)
local grid = 32
local actions={"select object", "Add scenery (line)", "Draw shape", "Add crate", "Place ball", "Place goal" } --"Add scenery (freehand)",
local snapPos, oldSnapPos, buttonPress, editModeSelect
function Editor:init()
self.id="editor"
editModeSelect=1
-- displayMode(OVERLAY)
--[[
parameter.clear()
-- parameter.watch("#newObj")
parameter.integer("editModeSelect", 1,#actions,1)
parameter.watch("editMode")
parameter.action("DELETE selected object", Editor.deleteObject)
parameter.integer("selectLevel", 1, #levels, level)
parameter.text("levelName", "name me")
parameter.action("Load selected level", function() level = selectLevel loadLevel(level) end)
parameter.action("Save Level", saveLevel)
parameter.action("New Level", function() level = #levels+1 clearLevel() moving=true objects={} end)
parameter.action("SAVE and QUIT", function() saveLevel() clearLevel() newGame() end)
]]
loadLevel(level)
self:buttonSetup()
scene=self
moving=true
touchAg=vec2(0,0)
cameraOrient()
end
function Editor:draw()
Game.draw(self)
pushStyle()
stroke(128,128)
noSmooth()
strokeWidth(0.5)
for y=0, HEIGHT, grid do
line(0,y,WIDTH,y)
end
for x=0, WIDTH, grid do
line(x,0,x,HEIGHT)
end
popStyle()
if selected then
if selected.body.shapeType==CIRCLE then
-- rectMode(CENTER)
noFill()
ellipse(selected.pos.x, selected.pos.y, selected.width*2, selected.height*2)
else
pushMatrix()
translate(selected.pos.x, selected.pos.y)
local p, u=selected.body.points
for i,v in ipairs(p) do
if i==1 then u=p[#p] else u=p[i-1] end
line(u.x, u.y, v.x, v.y)
end
popMatrix()
end
end
-- editMode=actions[editModeSelect]
end
function Editor:overlays()
end
function Editor:gravity()
end
function Editor:touched(touch)
if touch.state==BEGAN then buttonPress=self:testButtons() end
if not buttonPress then
snapPos=vec2(math.round(touch.x/grid)*grid, math.round(touch.y/grid)*grid)
local actionFuncs={Editor.select, Editor.addLine, Editor.drawShape, Editor.addCrate, Editor.addBall, Editor.addGoal, }
actionFuncs[editModeSelect](self,touch)
end
end
function Editor:addLine(touch)
if touch.state==ENDED and touch.tapCount==1 then
if #newObj>2 and snapPos==newObj[1] then
touchAg = touchAg /#newObj
for i=1,#newObj do --centre geometry around origin
newObj[i] = newObj[i] - touchAg
end
selected=Platform({x=touchAg.x, y=touchAg.y}, newObj)
self:selectObject()
touchAg=vec2(0,0)
newObj={}
else
newObj[#newObj+1]=snapPos
touchAg = touchAg + snapPos
end
end
end
function Editor:addBall(touch)
if touch.state==ENDED and touch.tapCount==1 then
if ball then
ball.body.position=snapPos
ball.startX, ball.startY = snapPos.x, snapPos.y
else
Ball({position=snapPos})
end
selected=ball
self:selectObject()
end
end
function Editor:addGoal(touch)
if touch.state==ENDED and touch.tapCount==1 then
if goal then
goal.body.position=snapPos
goal.startX, goal.startY = snapPos.x, snapPos.y
else
Goal({position=snapPos})
end
selected=goal
self:selectObject()
end
end
function Editor:addCrate(touch)
if touch.state==ENDED and touch.tapCount==1 then
selected=Crate({position=snapPos})
self:selectObject()
end
end
function Editor:select(touch)
if touch.state==BEGAN then --select an object
selected=nil
for i,v in pairs(objects) do
if v.body:testPoint(tpos) then
-- newObj=v.body.points
selected=v
end
end
elseif touch.state==MOVING and selected then --move selected object
selected.body.position = selected.body.position + (snapPos-oldSnapPos)
elseif touch.state==ENDED and selected then
selected.startX, selected.startY = selected.body.x, selected.body.y --save new start pos
end
oldSnapPos=snapPos
end
function Editor:selectObject() --ie when not in select mode, at then end of an action, switch into select mode
self.buttons[editModeSelect+1]:toggle()
editModeSelect=1
self.buttons[2]:toggle()
end
function Editor:deleteObject()
if not selected or not objects[selected.body] then sound(SOUND_RANDOM, 39052) return end
selected:destroy()
objects[selected.body]=nil
selected=nil
end
function Editor:buttonSetup()
self.showMenu=1
self.buttons={
GameButton({no=1, note="\u{2263}", help="Toggle menu", --โ‰ฃ
callback=function()
self:toggleMenu()
end
}),
GameButton({no=2, note="\u{1F449}", help="Select object", --"๐Ÿ‘‰"
callback=function()
self.buttons[editModeSelect+1]:toggle()
editModeSelect=1
end}),
GameButton({no=3, note="\u{1F4D0}", on=0, help="Add platform", --๐Ÿ“
callback=function()
self.buttons[editModeSelect+1]:toggle()
editModeSelect=2
end}),
GameButton({no=4, note="\u{270F}", on=0, help="Draw shape", --โœ
callback=function()
self.buttons[editModeSelect+1]:toggle()
editModeSelect=3
end}),
GameButton({no=5, note="\u{1F4E6}", on=0, help="Add crate", --๐Ÿ“ฆ
callback=function()
self.buttons[editModeSelect+1]:toggle()
editModeSelect=4
end}),
GameButton({no=6, note="\u{1F3C0}", on=0, help="Add ball", --๐Ÿ€
callback=function()
self.buttons[editModeSelect+1]:toggle()
editModeSelect=5
end}),
GameButton({no=7, note="\u{1F6A9}", on=0, help="Add goal", --๐Ÿšฉ
callback=function()
self.buttons[editModeSelect+1]:toggle()
editModeSelect=6
end}),
GameButton({no=8.5, note="\u{274C}", help="Delete selected object", --โŒ
callback=function()
tween.delay(0.1, function() self.buttons[8]:toggle() end)
self:deleteObject()
end
}),
GameButton({no=9.5, note="\u{1F195}", help="New level", --๐Ÿ†•
callback=function()
tween.delay(0.1, function() self.buttons[9]:toggle() end)
level = #levels+1
clearLevel()
moving=true
objects={}
end}),
GameButton({no=10.5, note="\u{1F4DD}", help="Name level", --๐Ÿ“
callback=function()
splash=KeyInput("Enter the level name",
function(input)
self.buttons[10]:toggle()
levelName=input
end,
levelName)
end}),
GameButton({no=11.5, note="\u{1F4BE}", help="Save level", --๐Ÿ’พ
callback=function()
tween.delay(0.1, function() self.buttons[11]:toggle() end)
saveLevel()
end}),
GameButton({no=12.5, note="\u{1F4C2}", help="Load level", --๐Ÿ“‚
callback=function()
splash=LevelSelector(true, "Choose a level to edit")
end}),
GameButton({no=13.5, note="\u{1F3AE}", help="Save and play level", --๐ŸŽฎ
callback=function()
saveLevel()
newGame()
end}),
-- GameButton({no=14.5, note="โ“", help="Toggle help text",
-- callback=function() end}),
}
end
function Editor:toggleMenu()
self.showMenu = 1 - self.showMenu
local shade=nil
if self.showMenu==0 then shade=greyScale end
for i=2,#self.buttons do
self.buttons[i].mesh.shader=shade
end
end
--# KeyInput
KeyInput = class(Scene) --ok, so writing keyboard io is dull. but for some reason i enjoyed writing this
function KeyInput:init(note, callback, initial)
local col=color(31, 42, 110, 255)
self.setup=function()
self.mesh=Text({note=note.."\n\n\n", w=WIDTH*0.8, h=HEIGHT*0.4, col=col, back="Cargo Bot:Dialogue Box"})
end
Scene.init(self)
self.callback=callback
self.input=initial or ""
showKeyboard()
end
function KeyInput:draw()
game:draw()
Scene.draw(self)
pushStyle()
textAlign(LEFT)
textMode(CORNER)
fill(color(255))
local cursor=string.rep("โ– ", (ElapsedTime%2)//1)
text(self.input..cursor, WIDTH*0.3, centre.y)
popStyle()
if not isKeyboardShowing() then
scene=game --trigger a return event if the user hides the keyboard
self.callback(self.input)
end
end
function KeyInput:keyboard(key)
if key==RETURN then
hideKeyboard()
scene=game
self.callback(self.input)
elseif key==BACKSPACE then
self.input=string.sub(self.input, 1, -2)
else
self.input=self.input..key
end
end
function keyboard(key)
scene:keyboard(key)
end
--# BUTTON
Button = class() --super classs for 3 types of button in game
function Button:init(on, w, h)
self.on=1-on -- just used for animation. 0 = off, 1=on, (toggles straightaway, hence inversion of on value)
local h=h or w
self.bounds=vec2(w,h)*0.5 --for creating AABB detection box
self.active=true
self:toggle()
end
function Button:toggle()
self.on = 1 - self.on --toggle value
-- sound("A Hero's Quest:Bottle Break 1")
self.mesh:setColors(color(255, (self.on+1)*127)) --button is translucent in off state
end
function Button:draw()
pushMatrix()
translate(self.pos.x,self.pos.y)
self.mesh:draw()
popMatrix()
end
function Button:test(pos)
if self.active and not outOfBounds(pos, self.pos+self.bounds, self.pos-self.bounds) then --AABB test
self:toggle()
self:action()
return true
end
-- return false
end
TextButton = class(Button) --class for dialog buttons
function TextButton:init(t) --params: no, of, note, callback, [y]
local start=centre.x-(WIDTH*0.125*t.of) --dialog buttons arranged horizontally in a row
local off=WIDTH*0.25*(t.no-0.5)
local y=t.y or HEIGHT*0.35
self.pos=vec2(start+off, y)
local note=t.note
self.mesh, w, h =Text({note=t.note, col=color(255), back="Cargo Bot:Dialogue Button", size=30, tint=t.tint})
self.action=function() tween.delay(0.05, t.callback) end --callback nested in split second delay so that button press animation can register.
Button.init(self, 1, w, h)
end
LevelButton = class(Button) --buttons for the level selector class
local icon={"Cargo Bot:Pack Tutorial",
"Cargo Bot:Pack Easy",
"Cargo Bot:Pack Medium",
"Cargo Bot:Pack Hard",
"Cargo Bot:Pack Crazy",
}
function LevelButton:init(lev, gridw, gridh, step, size, callback)
local x=(lev-1)//gridh+1
local y=3.5-(lev-1)%gridh
local i=#icon/#levels
self.pos=vec2(x*step.x,y*step.y)
self.level=lev
local back=icon[math.ceil(lev*i)]
self.mesh=Text({note="\n"..lev..". "..levels[lev].NAME, col=color(255), w=size.x, h=size.y, size=30, back=back , stars=userData[lev].stars})
Button.init(self, 1, size.x, size.y)
if not userData[lev].unlock then
self.mesh.shader=greyScale
self.active=false
end
self.action=callback
end
GameButton = class(Button) --buttons that appear in game, meant to be unobtrusive. just an icon, no text. arranged vertically at right of screen
function GameButton:init(t) --no, tex, trimL, trimR
local s=t.size or HEIGHT/16
local step=t.step or 2
self.pos=vec2(WIDTH*0.95,HEIGHT-(t.no*(s+step)))
if t.tex then
local m=mesh()
m.texture=t.tex
m:addRect(0,0,s,s)
local trimL=t.trimL or 0
local trimR=t.trimR or 1
m:setRectTex(1,trimL,0,trimR-trimL,1) --trim off the word "replay"
self.mesh=m
else
self.mesh=Text({note=t.note, back="Cargo Bot:Dialogue Button", w=s})
end
self.action=t.callback
local on=t.on or 1
Button.init(self, on, s)
end
--# MESH
Mesh = class() --master class for all 3D meshes drawn
function Mesh:init(points, args)
if not self.mesh then
local m,w,h=solidify(points, args)
self.mesh=m
self.length= math.max(w,h)
self.width=w
self.height=h
end
self:light({})
self.rotate=args.rotate or vec3(0,0,1)
self.bounce=vec3(1,1,1)
self.posZ=args.posZ or 0
-- self.angleXY=args.angleXY or vec2(0,0)
end
function Mesh:draw()
pushMatrix()
translate(self.pos.x, self.pos.y,self.posZ)
scale(self.bounce.x,self.bounce.y,self.bounce.z)
rotate(self.angle, self.rotate.x, self.rotate.y, self.rotate.z) --,0.5,0,1)
if moving and self.debug then self:debugDraw() end
-- self.modelM=modelMatrix()
-- rotate(self.angleXY.y,0,1,0)
-- rotate(self.angleXY.x,1,0,0)
self:shade()
self.mesh:draw()
popMatrix()
end
function Mesh:debugDraw()
local p,i,u,v=self.body.points
for i=1,#p do
v=p[i]
if i==#p then u=p[1] else u=p[i+1] end
line(v.x,v.y,u.x,u.y)
end
end
function Mesh:shade()
self.mesh.shader.modelMatrix=modelMatrix()
self.mesh.shader.eye=vec4(cam.eye.x,cam.eye.y,cam.eye.z,1)
end
function Mesh:light(t)
local m=t.me or self.mesh
local d=vec3(100,85,70) --t.light or vec3(-100,-50,-50)
d=d:normalize()
m.shader.light=vec4(d.x,d.y,d.z,0)
m.shader.ambient=t.ambient or 0.2
m.shader.specularPower=t.specularPower or 32
m.shader.shine=t.shine or 1.5
m.shader.lightColor=t.lightColor or color(255, 250, 230, 255)
end
function solidify(points, args)
local verts, tCoords, norms, w, h= extrude(points, args)
local m=mesh()
m.vertices=verts
--[[
if args.hiPoly then
m.normals=calculateAverageNormals(verts)
else
m.normals=calculateNormals(verts)
end
]]
m.normals=norms
local col=args.col or color(255)
m:setColors(col)
m.texture=args.tex
m.texCoords=tCoords
m.shader=DiffuseTex
return m,w,h
end
function extrude(points, args) --relevant args are depth(required). optional flags noFront and bevel. can also pass a table of face coords(if front or back face is different from side walls, like with the goal box)
local verts={} --tables that will be output
local tCoord={}
local norms={}
local ring={} --table of scaled inner points
local w,h=getDimensions(points)
local v,u,n11,n22,n33
local s = 1 --scale
local fdep = args.depth --front face z
local rdep = -args.depth --rear face z
if args.bevel then s=0.8 rdep=0 end --shrink front face and set rear depth to 0 if bevel flag is set. Issue: bevel only works properly with convex shapes....
--sides
local n = #points
if args.noFloor then
ring[n]=points[n]*s
n = n - 1
end
for i=1,n do --,v in ipairs(points) do
v=points[i]
if i<#points then u=points[i+1] else u=points[1] end
ring[i]=v*s
local a,b,c,d = vec3(v.x*s,v.y*s,fdep), vec3(v.x,v.y,rdep), vec3(u.x,u.y,rdep), vec3(u.x*s,u.y*s,fdep)
table.insertMany(verts, a,b,c, a,c,d) --2 triangles for side
--normals
local n1 = ((b - a):cross(c - a)):normalize()
local n2 = ((c - a):cross(d - a)):normalize()
if i>1 then --normals are a face behind
local n3=(n11+n22+n1+n2)*0.25 --average the normal
table.insertMany(norms, n33,n33,n3, n33,n3,n3)
if i==n then --final face
local n4 = (n1 + n2) * 0.5
table.insertMany(norms, n3,n3,n4, n3,n4,n4)
end
n33=n3 --old averaged normal
else
n33=(n1+n2)*0.5
end
n11,n22=n1,n2 --remember previous normals
--texCoords for side faces is more complex
local tux,tvx
if math.abs(u.x-v.x)>math.abs(u.y-v.y) then --work out whether to map x or y onto tex x coord
tvx=(v.x/w)+0.5
tux=(u.x/w)+0.5
else
tvx=(v.y/h)+0.5
tux=(u.y/h)+0.5
end
a,b,c,d = vec2(tvx, 0), vec2(tvx, 1), vec2(tux, 1), vec2(tux,0) --tex y coord is z for the sides
table.insertMany(tCoord, a,b,c, a,c,d)
end
--front or back faces
local vOff = #verts
local face=triangulate(ring)
if args.noFront then fdep=rdep end
for i,v in ipairs(face) do
verts[vOff+i]=vec3(v.x,v.y,fdep)
tCoord[vOff+i]=vec2((v.x/w)+0.5, (v.y/h)+0.5)
norms[vOff+i]=vec3(0,0,1)
end
if args.backFace then
vOff = #verts
face=triangulate(args.backFace)
for i,v in ipairs(face) do
verts[vOff+i]=vec3(v.x,v.y,rdep)
tCoord[vOff+i]=vec2((v.x/w)+0.5, (v.y/h)+0.5)
norms[vOff+i]=vec3(0,0,-1)
end
end
return verts, tCoord, norms, w, h
end
function getDimensions(p)
local x1, y1 =-10000, -10000
local x2, y2 =10000, 10000
for i,v in ipairs(p) do
if v.x>x1 then x1=v.x end
if v.x<x2 then x2=v.x end
if v.y>y1 then y1=v.y end
if v.y<y2 then y2=v.y end
end
local w,h= x1-x2, y1-y2
local d=math.min(w,h)
return w,h,d
end
--# Walls
Walls = class(Mesh) --although the walls are physics bodies, as they are multiple bodies they don't use the Body class
local bx, by =0, 0 --WIDTH*0.05, HEIGHT *0.05
local points={vec2(bx,-HEIGHT*1.5), vec2(bx,HEIGHT*1.5), vec2(WIDTH-bx,HEIGHT*1.5), vec2(WIDTH-bx,-HEIGHT*1.5)}
function Walls:init()
self.bodies={
physics.body(EDGE, points[1], points[2]),
physics.body(EDGE, points[2], points[3]),
physics.body(EDGE, points[3], points[4]),
physics.body(EDGE, points[4], points[1]) --floor
}
self.pos=vec2(0,0)
self.angle=0
self.rotate=vec3(0,0,1)
self.bounce=vec3(1,1,1)
Mesh.init(self, points, {depth=1400, noFront=true, tex="Cargo Bot:Game Area", col=color(146, 207, 255, 255)}) --"Cargo Bot:Game Area","Cargo Bot:Opening Background" "Cargo Bot:Goal Area" "Cargo Bot:Dialogue Box"
--self.debug=true
end
function Walls:debugDraw()
for i=1,#points-1 do
line(points[i].x, points[i].y, points[i+1].x, points[i+1].y)
end
end
--# BODY
Body = class(Mesh) --master class for all physics bodies
id={ball=1, crate=2, shelf=3, goal=4, platform=5, poly=6} --ids allow the level loader to know which class to invoke
function Body:init(bod, bodArgs, meshArgs)
local body=physics.body(unpack(bod))
bodArgs.interpolate=true
for k,v in pairs(bodArgs) do
body[k]=v
end
-- if body.shapeType==POLYGON then
Mesh.init(self, body.points, meshArgs)
-- end
self.startX=body.x --remember start position
self.startY=body.y
self.body=body
self.pos = self.body.position --save position and angle
self.angle = self.body.angle
objects[body]=self
end
function Body:draw()
if moving then self:move() end
Mesh.draw(self)
end
function Body:hit()
end
function Body:move()
self.pos = self.body.position --save position and angle
self.angle = self.body.angle
if self.pos.y<-self.length then self:outOfBounds() end
end
function Body:outOfBounds()
if scene.id=="editor" then self:destroy() end
end
function Body:destroy()
self.body:destroy()
objects[self.body]=nil
end
function Body:respawn()
self.body.active=false
self.body.linearVelocity=vec2(0,0)
self.body.angularVelocity=0
self.body.x, self.body.y = self.startX, self.startY --regenerate
self.body.angle=0
self.body.active=true
self:move()
-- self.kill=nil
end
--# Joint
Joint = class(Body) --joints added by player
local w=15
function Joint.assets()
Joint.mesh=solidify({vec2(-w,0), vec2(0,w), vec2(w,0), vec2(0,-w)}, {depth=100, tex="Cargo Bot:Crate Yellow 2"}) --this can be called with self.mesh
end
function Joint:init(bod1, bod2, pos)
self.joint=physics.joint(REVOLUTE, bod1, bod2, pos)
self.localPoint=bod2:getLocalPoint(pos)
self.master=bod2
self:move()
Mesh.init(self, {}, {posZ=50}) --skips Body init because its a joint not a body
objects[self.joint]=self --add to objects for drawing
end
function Joint:move()
self.pos=self.master:getWorldPoint(self.localPoint)
self.angle=self.master.angle
end
function Joint:destroy()
objects[self.joint]=nil
self.joint:destroy()
end
function Joint:respawn()
--unecessary, as it is attached to another body
end
--# Ball
Ball = class(Body)
local radius=30
function Ball.assets()
local m=mesh()
local colors={
color(255, 0, 0, 255),
color(255, 0, 206, 255),
color(127, 0, 255, 255),
color(0, 19, 255, 255),
color(0, 240, 255, 255),
color(0, 255, 42, 255),
color(253, 255, 0, 255),
color(255, 131, 0, 255),
}
local verts,cols=UVsphere(radius*1.2,32,colors)
m.vertices=verts
m.normals=verts
m.colors=cols
m.shader=BallShader
Ball.mesh=m
end
function Ball:init(t)
self.id=id.ball
t.restitution=0.5
-- t.angularVelocity=-90
Body.init(self,
{CIRCLE, radius},
t,
{rotate=vec3(0.5,0,1)}
)
self:light({specularPower=32, shine=1})
self.width, self.height, self.length = radius*2, radius*2, radius*2
ball=self --shortcut
end
function Ball:hit(bod, contact)
if contact.state==BEGAN then
-- sound(SOUND_JUMP, 6383)
local mag=contact.normalImpulse/35
local norm=contact.normal:rotate90()
local normAbs=vec2(math.abs(norm.x)*mag, math.abs(norm.y)*mag)
local bounce=vec2(clamp(1+normAbs.x-normAbs.y, 0.5,1.8),clamp(1+normAbs.y-normAbs.x, 0.5,1.8))
self.bounce=vec3(bounce.x,bounce.y,math.max(bounce.x, bounce.y))
local time=clamp(mag*0.5, 0.01, 10)
self.bounceTween=tween(time, self, {bounce=vec3(1,1,1)}, tween.easing.bounceOut)
end
if bod.sensor then
splash=LevelWin({})
end
end
function Ball:outOfBounds()
--[[
self.body:destroy()
ball=nil
splash=LevelWin({restart=true})
self:respawn()
]]
if scene.id~="editor" and not cam.tween then
-- track=vec3(centre.x, -HEIGHT*1.5, 0)
cam.tween=tween(2, cam, {ori=vec3(centre.x, -HEIGHT*1.5, 0), eye=vec3(centre.x, centre.y, cam.dist*0.5), fov=65}, tween.easing.sineInOut, resetLevel)
-- tween.delay(1.5, resetLevel)
game:cue({key="ballDie", priority=1})
end
-- LevelWin=true
end
function Ball:destroy()
Body.destroy(self)
ball=nil
end
--# Crate
Crate = class(Body)
local w,h=25,25
local cratePoints={vec2(-w,-h), vec2(-w,h), vec2(w,h), vec2(w,-h)}
function Crate.assets()
Crate.mesh = solidify(cratePoints, {depth=25, tex="Cargo Bot:Crate Yellow 2"})
--nb this can be called with self.mesh
end
function Crate:init(t)
self.id=id.crate
self.width=w*2
self.height=h*2
self.length=w*2
t.friction=0.4
t.density=0.5
Body.init(self,
{POLYGON, unpack(cratePoints)},
t,
{}) --rotate=vec3(0,1,1)
end
--# Shelf
Shelf = class(Body) --not used
function Shelf:init(t)
self.id=id.shelf
local w,h=70,10
t.type=STATIC
Body.init(self,
{POLYGON, vec2(-w,-h), vec2(-w,h), vec2(w,h), vec2(w,-h)},
t,
{depth=100, tex=readImage("Cargo Bot:Game Area")})
end
--# Goal
Goal = class(Body)
function Goal:init(t)
self.id=id.goal
t.angle=35
local w,h=45,45
local th=0.75 --thickness of box
local face={vec2(-w,-h), vec2(-w,h),vec2(w,h), vec2(w,-h)}
Body.init(self,
{POLYGON, vec2(-w,-h), vec2(-w,h), vec2(-w*th,h), vec2(-w*th,-h*th), vec2(w*th,-h*th), vec2(w*th,h),vec2(w,h), vec2(w,-h)},
t,
{depth=w, tex=readImage("SpaceCute:Beetle Ship"), backFace=face} --angleXY=vec2(0,-5) noFront=true,
)
self.sensor=physics.body(POLYGON, vec2(-w*th,0), vec2(-w*th,-h*th), vec2(w*th,-h*th), vec2(w*th,0))
self.sensor.sensor=true
self.sensor.type=STATIC
--self.debug=true
goal=self --shortcut
end
function Goal:move()
self.sensor.position=self.body.position
self.sensor.angle=self.body.angle
Body.move(self)
end
function Goal:outOfBounds()
if scene.id~="editor" then resetLevel() end
game:cue({key="goalDie", priority=1})
--[[
self.body:destroy()
self.sensor:destroy()
goal=nil
splash=LevelWin({restart=true})
]]
-- LevelWin=true
end
function Goal:destroy()
self.body:destroy()
self.sensor:destroy()
end
--# Poly
Poly = class(Body) --shapes drawn by player
function Poly:init(t, vectors, delta, lastTouch)
self.id=id.poly
-- sound(SOUND_RANDOM, 18065)
t.restitution=0.1
t.density=2
local args={depth=60, tex="Cargo Bot:Starry Background"} --bevel=true
-- if n>6 then args.hiPoly=true end
Body.init(self,
{POLYGON, unpack(vectors)},
t,
args)
if delta then self.body:applyForce(delta * 100, lastTouch) end --your final touch influences body
-- print ("mass"..self.body.mass)
-- if self.body.mass<1 then self:destroy() end
--self.debug=true
end
function Poly:destroy()
self.body:destroy()
if self.joint then self.joint:destroy() end
objects[self.body]=nil
end
--# Platform
Platform = class(Body)
function Platform:init(t, vectors) --x, y, vectors
self.id=id.platform
-- local pos=vec2(t.x,t.y)
t.type=STATIC
local args={depth=100, tex="Cargo Bot:Game Area"}
if #vectors>6 then args.hiPoly=true end
Body.init(self,
{POLYGON, unpack(vectors)},
t,
args)
-- self.debug=true
end
--# Shape
Shape = class(Body) --shapes that are part of the level, not user created
function Shape:init(t, vectors)
self.id=id.poly
t.restitution=0.1
t.density=2
local col=color(205, 126, 125, 255)
local args={depth=60, tex="Cargo Bot:Game Area", col=col} --bevel=true "Cargo Bot:Starry Background"
Body.init(self,
{POLYGON, unpack(vectors)},
t,
args)
shape=self --hack to get the tutorial to indicate a shape
end
--# Assets
--assets
function assets()
shaders()
Crate.assets()
Ball.assets()
Joint.assets()
local m=mesh()
m.texture=readImage("Cargo Bot:Next Button")
m:addRect(0,0,60,60)
m:setRectTex(1,0.7,0,0.3,1) --trim off the word "next"
m:setColors(color(0, 167, 255, 255))
arrowMesh=m
end
function UVsphere(r, level, colors) --adapted from someone's UV sphere code... let me know if youd like credit!create a uv sphere with latitudinal stripes. level should be a multiple of the number of colours for best results. returns table of verts and cols
local tab={} -- table of points
local r=r or 50 -- radius of sphere
local M=level or 20
local N=level or 20
for n=0,N do
tab[n]={}
for m=0,M do
-- calculate the x,y,z point position
x=r * math.sin(math.pi * m/M) * math.cos(2*math.pi * n/N)
y=r * math.sin(math.pi * m/M) * math.sin(2*math.pi * n/N)
z=r * math.cos(math.pi * m/M)
-- 2 dimension table of sphere points
tab[n][m]=vec3(x,y,z)
end
end
local sph={}
local cols={}
local spNorm={}
local stripe=N/(#colors)
N = N - 1
M = M - 1
--print ("N"..N.."#colors"..#colors.."stripe"..stripe)
for n=0,N do
for m=0,M do
-- loop thru sphere table to create a rectangle
-- create 2 triangles from 4 points of a rectangle
-- create 1st triangle of the rectangle
table.insert(sph,tab[n][m])
table.insert(sph,tab[n][m+1])
table.insert(sph,tab[n+1][m+1])
-- create 2nd triangle of a rectangle
table.insert(sph,tab[n+1][m+1])
table.insert(sph,tab[n+1][m])
table.insert(sph,tab[n][m])
col=colors[math.ceil((n+1)/stripe)] --color(29, 34, 65, 255)
for i=1,6 do
table.insert(cols,col)
end
end
end
print ("sphere vertices="..#sph)
return sph,cols
end
--lighting shaders adapted from Ignatz's tutorials
function shaders()
--HiPoly, therefor specular lighting effects happen in vertex shader
BallShader = shader(
[[
uniform mat4 modelViewProjection;
uniform mat4 modelMatrix;
uniform mat4 viewMatrix;
uniform mat4 projectionMatrix;
uniform float ambient; // --strength of ambient light 0-1
uniform vec4 eye; // -- position of camera (x,y,z,1)
uniform vec4 light; //--directional light direction (x,y,z,0)
uniform vec4 lightColor;
uniform float specularPower; //higher number = smaller highlight
uniform float shine; // higher number, reflects more
attribute vec4 position;
attribute vec4 color;
// attribute vec2 texCoord;
attribute vec3 normal;
varying lowp vec4 vAmbient;
varying lowp vec4 vColor;
// varying highp vec2 vTexCoord;
varying vec4 vDirectDiffuse;
varying vec4 vSpecular;
void main()
{
vec4 norm = normalize(modelMatrix * vec4( normal, 0.0 ));
vDirectDiffuse = lightColor * max( 0.0, dot( norm, light )); // direct color
vec4 vPosition = modelMatrix * position;
//specular blinn-phong
vec4 cameraDirection = normalize( eye - vPosition );
vec4 halfAngle = normalize( cameraDirection + light );
float spec = pow( max( 0.0, dot( norm, halfAngle)), specularPower );
vSpecular = lightColor * spec * shine;
vAmbient = color * ambient;
// vAmbient.a = color.a * 0.5;
vColor = color;
gl_Position = modelViewProjection * position;
}
]],
[[
precision highp float;
uniform lowp sampler2D texture;
varying lowp vec4 vNormal;
varying lowp vec4 vColor;
varying lowp vec4 vAmbient;
varying vec4 vDirectDiffuse;
varying vec4 vSpecular;
void main()
{
//lowp vec4 pixel;
//lowp vec4 ambient;
// pixel= texture2D( texture, vTexCoord );
//ambient = pixel * vColor;
lowp vec4 diffuse = vColor * vDirectDiffuse;
// diffuse.a = vColor.a * 0.5; //alpha twice, in diffuse and ambient
vec4 totalColor = clamp(vAmbient + diffuse + vSpecular,0.,1.);
totalColor.a=1.;
gl_FragColor=totalColor;
}
]])
DiffuseNoTex=shader(
[[
uniform mat4 modelViewProjection;
uniform mat4 modelMatrix;
uniform vec4 light; //directDirection; //--directional light direction
uniform vec4 lightColor;
uniform float ambient;
attribute vec4 position;
attribute vec4 color;
attribute vec3 normal;
varying lowp vec4 vColor;
varying lowp vec4 vAmbient;
varying vec4 vDirectDiffuse;
void main()
{
vec4 norm = normalize(modelMatrix * vec4(normal,0.0));
vDirectDiffuse = vec4(1.,1.,1.,1.) * max( 0.0, dot( norm,light )); //directColor
vColor = color;
vAmbient = color * ambient;
gl_Position = modelViewProjection * position;
}
]],
[[
precision highp float;
varying lowp vec4 vColor;
varying lowp vec4 vAmbient;
varying vec4 vDirectDiffuse;
void main()
{
lowp vec4 directional;
directional = vColor * vDirectDiffuse;
vec4 totalColor = clamp(vAmbient + directional,0.,1.);
totalColor.a=1.; //vColor.a;transparency not affected by lighting
gl_FragColor=totalColor;
}
]]
)
DiffuseTex = shader(
[[
uniform mat4 modelViewProjection;
uniform mat4 modelMatrix;
uniform vec4 eye; // -- position of camera (x,y,z,1)
uniform vec4 light; //--directional light direction (x,y,z,0)
//uniform float fogRadius;
uniform vec4 lightColor; //--directional light colour
// uniform lowp vec4 aerial;
attribute vec4 position;
attribute vec4 color;
attribute vec2 texCoord;
attribute vec3 normal;
varying lowp vec4 vColor;
// varying float dist;
varying highp vec2 vTexCoord;
varying vec4 vDirectDiffuse;
// varying vec4 vSpecular;
void main()
{
vec4 norm = normalize(modelMatrix * vec4( normal, 0.0 ));
vDirectDiffuse = lightColor * max( 0.0, dot( norm, light )); // brightness of diffuse light
vec4 vPosition = modelMatrix * position;
//dist = clamp(1.0-distance(vPosition.xyz, eye.xyz)/fogRadius+0.1, 0.0, 1.1); //(vPosition.y-eye.y)
//vec4 totalColor = clamp(ambLight + directional,0.,1.) * dist + aerial * (1.0-dist);
//vFog = dist + aerial * (1.0-dist);
//specular blinn-phong
// vec4 cameraDirection = normalize( eye - vPosition );
// vec4 halfAngle = normalize( cameraDirection + light );
// float spec = pow( max( 0.0, dot( norm, halfAngle)), 50. );//last number is specularPower, higher number = smaller highlight
// vSpecular = lightColor * spec * 2.; // add optional shininess at end here
vColor = color;
vTexCoord = texCoord;
gl_Position = modelViewProjection * position;
}
]],
[[
precision highp float;
uniform lowp sampler2D texture;
uniform float ambient; // --strength of ambient light 0-1
// uniform lowp vec4 aerial;
varying lowp vec4 vNormal;
varying lowp vec4 vColor;
varying highp vec2 vTexCoord;
// varying float dist;
varying vec4 vDirectDiffuse;
// varying vec4 vSpecular;
void main()
{
lowp vec4 pixel;
lowp vec4 ambientLight;
pixel= texture2D( texture, vTexCoord ) * vColor;
ambientLight = pixel * ambient; //aerial;
lowp vec4 diffuse = pixel * vDirectDiffuse;
vec4 totalColor = clamp(ambientLight + diffuse ,0.,1.); // * dist + aerial * (1.0-dist); //+ vSpecular
totalColor.a=1.;
// if (vColor.b==1.) totalColor.b=pixel.b; //let through blue
gl_FragColor=totalColor;
}
]])
greyScale=shader(
[[
uniform mat4 modelViewProjection;
attribute vec4 position;
attribute vec4 color;
attribute vec2 texCoord;
varying lowp vec4 vColor;
varying highp vec2 vTexCoord;
void main()
{
vColor = color;
vTexCoord = texCoord;
gl_Position = modelViewProjection * position;
}
]],
[[
precision highp float;
uniform lowp sampler2D texture;
varying lowp vec4 vColor;
varying highp vec2 vTexCoord;
void main()
{
vec4 col = texture2D( texture, vTexCoord ) * vColor;
col.r = (col.r + col.g + col.b)/3.;
col.gb = col.rr;
col.a = col.a * 0.5;
gl_FragColor = col;
}
]]
)
end
--# Helpers
--helpers
function vecMat(vec, mat) --rotate vector by current transform.
return vec2(mat[1]*vec.x + mat[5]*vec.y, mat[2]*vec.x + mat[6]*vec.y)
end
function clamp(v,low,high)
return math.min(math.max(v, low), high)
end
function table.insertMany(tab, ...)
local args={...}
for i,v in ipairs(args) do
tab[#tab+1]=v
end
end
function math.round(number, places) --use -ve places to round to tens, hundreds etc
local mult = 10^(places or 0)
return (number * mult + 0.5) // mult
end
tween.oldUpdate, tween.noOp = tween.update, function() end
tween.pauseAll = function()
tween.update = tween.noOp
end
tween.resumeAll = function()
tween.update = tween.oldUpdate
end
function outOfBounds(pos, bb, aa) --returns true if outofbounds
local aa=aa or vec2(0,0)
if pos.x<aa.x then return true
elseif pos.x>bb.x then return true
end
if pos.y<aa.y then return true
elseif pos.y>bb.y then return true
end
return false
end
--LoopSpace's line crossing algorithm
-- this is the tolerance at the end points when checking crossings
local epsilon = 0.01
function crossing(a,b,c,d)
-- rebase at a
b = b - a
c = c - a
d = d - a
if b:cross(c) * b:cross(d) > 0 then
-- both c and d lie on the same side of b so no intersection
return false
end
-- if there is an intersection point, this will be it
a = (b:cross(d) * c - b:cross(c) * d)/(b:cross(d) - b:cross(c))
-- does the potential intersection point lie on the line
-- segment?
local l = a:dot(b)
if l > epsilon and l < b:dot(b) - epsilon then
return a
end
return false
end
profiler={}
function profiler.init(monitor)
profiler.del=0
profiler.c=0
profiler.fps=0
profiler.mem=0
if monitor then
parameter.watch("profiler.fps")
parameter.watch("profiler.mem")
end
end
function profiler.draw()
profiler.del = profiler.del + DeltaTime
profiler.c = profiler.c + 1
if profiler.c==10 then
profiler.fps=profiler.c/profiler.del
profiler.del=0
profiler.c=0
profiler.mem=collectgarbage("count", 2)
end
end
--# ToDo
--[[
O restart button (that remembers polys player has created)
O restart can be glitchy
O mechanic: device gravity effects world gravity (to an extent?)
- make notifications available at all times (not just in tutorial levels)
- need to make touch rotate with screen...
O look down on restart?
O trash button (in game menu)
O ability to add joints?
O joint mesh
O notification/tutorial system
O calculate average normals for sides of extruded polys while shape is being created, for speed
- use normals for beveling?
O for repeated shapes (crates, ball, goal?), just create mesh once
O tidy up Ball class (make it call mesh init, or maybe have mesh init handle uv sphere creation?)
O add free drawn objects to levels?
O splash screen
O levels
O level editor?
O level select screen
O allow level naming/reordering?
O stop user from creating meshes with overlapping lines/ faces (or that overlap with scenery)
O allow player to hold on to object after it has been closed off?
- have material use mechanic
O auto-destruct tiny shapes
- need meter to measure successive size of shapes
O 3 star level completion system for
- amount of material used
O number of objects made
O time to complete
]]
--# Levels
--[{"items":[{"args":{"y":384,"x":192},"id":2},{"args":{"y":448,"x":192},"id":2},{"args":{"y":352,"x":192},"id":2},{"args":{"y":288,"x":192},"id":2},{"args":{"y":128,"x":504},"id":5,"vectors":[{"y":32,"x":-504},{"y":32,"x":-184},{"y":-64,"x":168},{"y":-64,"x":520},{"y":-32,"x":520},{"y":-32,"x":168},{"y":64,"x":-184},{"y":64,"x":-504}]},{"args":{"y":224,"x":832},"id":4},{"args":{"y":512,"x":192},"id":1},{"args":{"y":96,"x":864},"id":2},{"args":{"y":224,"x":192},"id":2}],"NAME":"Teeing Off"},{"items":[{"args":{"y":288,"x":512},"id":2},{"args":{"y":544,"x":512},"id":2},{"args":{"y":128,"x":928},"id":4},{"args":{"y":608,"x":512},"id":1},{"args":{"y":384,"x":512},"id":2},{"args":{"y":416,"x":512},"id":2},{"args":{"y":181,"x":245},"id":5,"vectors":[{"y":10,"x":-246},{"y":-54,"x":-86},{"y":-54,"x":330},{"y":10,"x":330},{"y":10,"x":-86},{"y":74,"x":-246}]},{"args":{"y":48,"x":912},"id":5,"vectors":[{"y":48,"x":-112},{"y":48,"x":112},{"y":-48,"x":112},{"y":-48,"x":-112}]},{"args":{"y":480,"x":512},"id":2},{"args":{"y":224,"x":512},"id":2},{"args":{"y":320,"x":512},"id":2}],"NAME":"Stacks"},{"items":[{"args":{"y":168,"x":928},"id":5,"vectors":[{"y":152,"x":-64},{"y":120,"x":96},{"y":-136,"x":96},{"y":-136,"x":-128}]},{"args":{"y":373,"x":903},"id":1},{"args":{"y":208,"x":695},"id":5,"vectors":[{"y":-144,"x":-88},{"y":176,"x":-24},{"y":112,"x":72},{"y":-144,"x":40}]},{"args":{"y":234,"x":213},"id":5,"vectors":[{"y":277,"x":74},{"y":277,"x":138},{"y":-171,"x":138},{"y":-171,"x":-214},{"y":-107,"x":-214},{"y":-107,"x":74}]},{"args":{"y":224,"x":128},"id":4},{"args":{"y":367,"x":796},"id":6,"vectors":[{"y":26,"x":-407},{"y":26,"x":-394},{"y":27,"x":-383},{"y":27,"x":-372},{"y":28,"x":-335},{"y":29,"x":-324},{"y":31,"x":-311},{"y":32,"x":-301},{"y":33,"x":-289},{"y":34,"x":-278},{"y":34,"x":-257},{"y":36,"x":-227},{"y":37,"x":-214},{"y":37,"x":-201},{"y":31,"x":55},{"y":21,"x":56},{"y":10,"x":57},{"y":0,"x":60},{"y":-9,"x":64},{"y":-17,"x":72},{"y":-23,"x":82},{"y":-29,"x":91},{"y":-32,"x":101},{"y":-33,"x":113},{"y":-33,"x":124},{"y":-33,"x":142},{"y":-28,"x":151},{"y":-20,"x":160},{"y":-11,"x":167},{"y":-2,"x":171},{"y":9,"x":174},{"y":20,"x":177},{"y":27,"x":185},{"y":20,"x":193},{"y":9,"x":195},{"y":-2,"x":195},{"y":-14,"x":194},{"y":-25,"x":189},{"y":-34,"x":182},{"y":-43,"x":174},{"y":-50,"x":167},{"y":-55,"x":157},{"y":-58,"x":145},{"y":-59,"x":135},{"y":-60,"x":122},{"y":-60,"x":109},{"y":-59,"x":89},{"y":-57,"x":77},{"y":-53,"x":66},{"y":-45,"x":55},{"y":-38,"x":46},{"y":-30,"x":38},{"y":-20,"x":31},{"y":-11,"x":24},{"y":-2,"x":19},{"y":8,"x":15},{"y":9,"x":3},{"y":10,"x":-9},{"y":10,"x":-23},{"y":9,"x":-91},{"y":11,"x":-414},{"y":23,"x":-409}]}],"NAME":"Trebuchet"},{"items":[{"args":{"y":512,"x":560},"id":5,"vectors":[{"y":-32,"x":-176},{"y":32,"x":-176},{"y":32,"x":176},{"y":-32,"x":176}]},{"args":{"y":92,"x":629},"id":5,"vectors":[{"y":35,"x":-470},{"y":99,"x":-470},{"y":99,"x":106},{"y":35,"x":42},{"y":-61,"x":42},{"y":-61,"x":394},{"y":-93,"x":394},{"y":-93,"x":-22},{"y":35,"x":-22}]},{"args":{"y":632,"x":547},"id":6,"vectors":[{"y":-52,"x":-215},{"y":-52,"x":-204},{"y":-51,"x":-194},{"y":-45,"x":-136},{"y":-43,"x":-121},{"y":-39,"x":-85},{"y":-39,"x":-70},{"y":-47,"x":86},{"y":-49,"x":96},{"y":-50,"x":109},{"y":-49,"x":224},{"y":-38,"x":223},{"y":-10,"x":221},{"y":5,"x":221},{"y":12,"x":211},{"y":13,"x":197},{"y":6,"x":-225},{"y":-4,"x":-225},{"y":-20,"x":-224},{"y":-37,"x":-222}]},{"args":{"y":256,"x":352},"id":1},{"args":{"y":224,"x":672},"id":2},{"args":{"y":448,"x":672},"id":2},{"args":{"y":288,"x":672},"id":2},{"args":{"y":416,"x":672},"id":2},{"args":{"y":352,"x":672},"id":2},{"args":{"y":96,"x":768},"id":4}],"NAME":"Swing low"},{"items":[{"args":{"y":208,"x":800},"id":5,"vectors":[{"y":16,"x":-96},{"y":16,"x":96},{"y":-16,"x":96},{"y":-16,"x":-96}]},{"args":{"y":320,"x":832},"id":4},{"args":{"y":672,"x":160},"id":1},{"args":{"y":560,"x":192},"id":5,"vectors":[{"y":-16,"x":-96},{"y":16,"x":-96},{"y":16,"x":96},{"y":-16,"x":96}]}],"NAME":"Freefall"},{"items":[{"args":{"y":192,"x":288},"id":2},{"args":{"y":736,"x":800},"id":1},{"args":{"y":288,"x":288},"id":2},{"args":{"y":192,"x":224},"id":2},{"args":{"y":256,"x":160},"id":2},{"args":{"y":608,"x":928},"id":2},{"args":{"y":672,"x":928},"id":2},{"args":{"y":672,"x":736},"id":2},{"args":{"y":672,"x":800},"id":2},{"args":{"y":288,"x":96},"id":2},{"args":{"y":384,"x":192},"id":4},{"args":{"y":288,"x":224},"id":2},{"args":{"y":544,"x":800},"id":5,"vectors":[{"y":-32,"x":-128},{"y":32,"x":-128},{"y":32,"x":128},{"y":-32,"x":128}]},{"args":{"y":256,"x":96},"id":2},{"args":{"y":608,"x":864},"id":2},{"args":{"y":192,"x":160},"id":2},{"args":{"y":608,"x":736},"id":2},{"args":{"y":256,"x":288},"id":2},{"args":{"y":256,"x":224},"id":2},{"args":{"y":128,"x":208},"id":5,"vectors":[{"y":-32,"x":-112},{"y":32,"x":-112},{"y":32,"x":112},{"y":-32,"x":112}]},{"args":{"y":672,"x":864},"id":2},{"args":{"y":608,"x":800},"id":2},{"args":{"y":192,"x":96},"id":2},{"args":{"y":288,"x":160},"id":2}],"NAME":"Crash landing"},{"items":[{"args":{"y":205,"x":442},"id":5,"vectors":[{"y":-142,"x":581},{"y":-142,"x":-91},{"y":82,"x":-91},{"y":82,"x":-27},{"y":114,"x":-27},{"y":114,"x":-91},{"y":178,"x":-91},{"y":210,"x":-155},{"y":-46,"x":-155},{"y":-46,"x":-283},{"y":-206,"x":-155},{"y":-206,"x":581}]},{"args":{"y":288,"x":224},"id":4},{"args":{"y":384,"x":384},"id":1}],"NAME":"Catapult"},{"items":[{"args":{"y":256,"x":576},"id":1},{"args":{"y":172,"x":160},"id":5,"vectors":[{"y":-77,"x":-160},{"y":-13,"x":-64},{"y":-13,"x":192},{"y":51,"x":192},{"y":51,"x":-160}]},{"args":{"y":108,"x":793},"id":5,"vectors":[{"y":51,"x":-282},{"y":-13,"x":-282},{"y":-13,"x":102},{"y":-77,"x":230},{"y":51,"x":230}]},{"args":{"y":288,"x":96},"id":4},{"args":{"y":192,"x":576},"id":2}],"NAME":"Uphill"},{"items":[{"args":{"y":384,"x":192},"id":2},{"args":{"y":320,"x":224},"id":2},{"args":{"y":608,"x":288},"id":1},{"args":{"y":320,"x":288},"id":2},{"args":{"y":320,"x":416},"id":2},{"args":{"y":480,"x":320},"id":2},{"args":{"y":320,"x":352},"id":2},{"args":{"y":416,"x":352},"id":2},{"args":{"y":128,"x":960},"id":4},{"args":{"y":416,"x":224},"id":2},{"args":{"y":384,"x":384},"id":2},{"args":{"y":384,"x":256},"id":2},{"args":{"y":352,"x":192},"id":2},{"args":{"y":512,"x":288},"id":2},{"args":{"y":384,"x":320},"id":2},{"args":{"y":480,"x":256},"id":2},{"args":{"y":416,"x":288},"id":2},{"args":{"y":149,"x":693},"id":5,"vectors":[{"y":42,"x":330},{"y":-150,"x":330},{"y":-150,"x":106},{"y":-22,"x":106},{"y":74,"x":-214},{"y":74,"x":-694},{"y":138,"x":-694},{"y":138,"x":-214},{"y":10,"x":170},{"y":-86,"x":170},{"y":-86,"x":298},{"y":10,"x":298}]}],"NAME":"Pile up"},{"items":[{"args":{"y":218,"x":229},"id":5,"vectors":[{"y":37,"x":250},{"y":69,"x":250},{"y":69,"x":90},{"y":-91,"x":90},{"y":-91,"x":-102},{"y":69,"x":-102},{"y":69,"x":-230},{"y":37,"x":-230},{"y":37,"x":-134},{"y":-123,"x":-134},{"y":-123,"x":122},{"y":37,"x":122}]},{"args":{"y":400,"x":864},"id":5,"vectors":[{"y":-16,"x":-160},{"y":16,"x":-160},{"y":16,"x":160},{"y":-16,"x":160}]},{"args":{"y":544,"x":864},"id":1},{"args":{"y":559,"x":852},"id":6,"vectors":[{"y":118,"x":65},{"y":113,"x":74},{"y":111,"x":86},{"y":107,"x":95},{"y":100,"x":106},{"y":94,"x":115},{"y":87,"x":123},{"y":77,"x":131},{"y":66,"x":137},{"y":54,"x":142},{"y":44,"x":145},{"y":30,"x":149},{"y":16,"x":150},{"y":5,"x":150},{"y":-33,"x":149},{"y":-43,"x":145},{"y":-55,"x":138},{"y":-72,"x":127},{"y":-81,"x":118},{"y":-90,"x":106},{"y":-95,"x":97},{"y":-100,"x":87},{"y":-105,"x":77},{"y":-109,"x":66},{"y":-112,"x":55},{"y":-116,"x":40},{"y":-117,"x":30},{"y":-120,"x":-20},{"y":-120,"x":-31},{"y":-119,"x":-58},{"y":-115,"x":-70},{"y":-111,"x":-83},{"y":-105,"x":-96},{"y":-99,"x":-105},{"y":-91,"x":-116},{"y":-81,"x":-126},{"y":-72,"x":-132},{"y":-59,"x":-139},{"y":-46,"x":-143},{"y":-33,"x":-145},{"y":-20,"x":-146},{"y":-6,"x":-146},{"y":21,"x":-145},{"y":32,"x":-142},{"y":64,"x":-132},{"y":74,"x":-128},{"y":84,"x":-121},{"y":91,"x":-112},{"y":99,"x":-101},{"y":110,"x":-82},{"y":114,"x":-70},{"y":117,"x":-59},{"y":119,"x":-47},{"y":120,"x":-35},{"y":120,"x":-25},{"y":111,"x":-17},{"y":100,"x":-17},{"y":89,"x":-17},{"y":82,"x":-25},{"y":79,"x":-36},{"y":76,"x":-48},{"y":72,"x":-57},{"y":64,"x":-71},{"y":57,"x":-80},{"y":48,"x":-87},{"y":36,"x":-91},{"y":25,"x":-93},{"y":14,"x":-93},{"y":-7,"x":-94},{"y":-18,"x":-92},{"y":-29,"x":-87},{"y":-39,"x":-81},{"y":-47,"x":-73},{"y":-55,"x":-65},{"y":-62,"x":-55},{"y":-68,"x":-43},{"y":-72,"x":-32},{"y":-76,"x":-20},{"y":-76,"x":-7},{"y":-76,"x":6},{"y":-74,"x":16},{"y":-71,"x":28},{"y":-65,"x":43},{"y":-59,"x":55},{"y":-52,"x":66},{"y":-44,"x":76},{"y":-35,"x":85},{"y":-24,"x":92},{"y":-15,"x":97},{"y":-5,"x":98},{"y":7,"x":98},{"y":35,"x":96},{"y":44,"x":88},{"y":50,"x":78},{"y":53,"x":67},{"y":56,"x":56},{"y":63,"x":48}]},{"args":{"y":224,"x":224},"id":4}],"NAME":"Zorb!"},{"items":[{"args":{"y":544,"x":384},"id":1},{"args":{"y":16,"x":512},"vectors":[{"y":16,"x":-320},{"y":16,"x":320},{"y":-16,"x":320},{"y":-16,"x":-320}],"id":5},{"args":{"y":432,"x":672},"vectors":[{"y":-16,"x":-160},{"y":16,"x":-160},{"y":16,"x":160},{"y":-16,"x":160}],"id":5},{"args":{"y":594,"x":485},"vectors":[{"y":-115,"x":-176},{"y":-115,"x":-155},{"y":-115,"x":-139},{"y":-103,"x":367},{"y":-87,"x":369},{"y":-71,"x":368},{"y":-56,"x":365},{"y":-41,"x":359},{"y":-29,"x":349},{"y":-19,"x":337},{"y":-8,"x":321},{"y":0,"x":306},{"y":7,"x":290},{"y":12,"x":275},{"y":16,"x":258},{"y":21,"x":236},{"y":23,"x":219},{"y":23,"x":203},{"y":22,"x":168},{"y":20,"x":150},{"y":16,"x":133},{"y":12,"x":115},{"y":8,"x":99},{"y":4,"x":79},{"y":4,"x":63},{"y":9,"x":49},{"y":24,"x":44},{"y":41,"x":44},{"y":57,"x":44},{"y":72,"x":38},{"y":82,"x":25},{"y":85,"x":7},{"y":86,"x":-10},{"y":86,"x":-28},{"y":84,"x":-46},{"y":78,"x":-62},{"y":65,"x":-71},{"y":59,"x":-55},{"y":59,"x":-38},{"y":58,"x":5},{"y":47,"x":16},{"y":31,"x":19},{"y":15,"x":22},{"y":-2,"x":23},{"y":-18,"x":23},{"y":-36,"x":22},{"y":-51,"x":18},{"y":-62,"x":8},{"y":-70,"x":-7},{"y":-77,"x":-21},{"y":-78,"x":-39},{"y":-78,"x":-58},{"y":-79,"x":-76},{"y":-81,"x":-91},{"y":-82,"x":-106},{"y":-85,"x":-122},{"y":-85,"x":-138},{"y":-83,"x":-159},{"y":-68,"x":-169},{"y":-51,"x":-172},{"y":-33,"x":-173},{"y":-16,"x":-176},{"y":0,"x":-177},{"y":16,"x":-180},{"y":31,"x":-183},{"y":48,"x":-182},{"y":55,"x":-168},{"y":56,"x":-152},{"y":63,"x":-139},{"y":76,"x":-148},{"y":73,"x":-166},{"y":63,"x":-180},{"y":54,"x":-193},{"y":38,"x":-201},{"y":22,"x":-204},{"y":3,"x":-206},{"y":-12,"x":-208},{"y":-34,"x":-209},{"y":-52,"x":-209},{"y":-68,"x":-205},{"y":-84,"x":-198},{"y":-99,"x":-191},{"y":-110,"x":-181}],"id":6},{"args":{"y":128,"x":288},"id":4}],"NAME":"Swing Low 2"},{"NAME":"Raiders","items":[{"args":{"y":1016,"x":238},"id":6,"vectors":[{"y":89,"x":-23},{"y":85,"x":-39},{"y":80,"x":-55},{"y":73,"x":-70},{"y":54,"x":-93},{"y":37,"x":-103},{"y":-3,"x":-115},{"y":-50,"x":-116},{"y":-65,"x":-110},{"y":-80,"x":-99},{"y":-97,"x":-75},{"y":-114,"x":-22},{"y":-116,"x":26},{"y":-106,"x":69},{"y":-94,"x":85},{"y":-68,"x":105},{"y":-48,"x":113},{"y":2,"x":116},{"y":20,"x":111},{"y":54,"x":94},{"y":68,"x":80},{"y":76,"x":66},{"y":95,"x":26},{"y":97,"x":11}]},{"args":{"y":448,"x":288},"id":1},{"args":{"y":16,"x":928},"id":5,"vectors":[{"y":-16,"x":-96},{"y":16,"x":-96},{"y":16,"x":96},{"y":-16,"x":96}]},{"args":{"y":128,"x":928},"id":4},{"args":{"y":278,"x":438},"id":5,"vectors":[{"y":297,"x":-503},{"y":105,"x":-183},{"y":105,"x":-119},{"y":-55,"x":73},{"y":-87,"x":169},{"y":-87,"x":233},{"y":-55,"x":297},{"y":-119,"x":329},{"y":-151,"x":265},{"y":-151,"x":169},{"y":-119,"x":73},{"y":41,"x":-119},{"y":41,"x":-183},{"y":233,"x":-503}]}]}]--
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You canโ€™t perform that action at this time.