Last active
August 29, 2015 14:17
-
-
Save oliver-dew/8a364a35f397f26aefad to your computer and use it in GitHub Desktop.
Squash and stretch
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--# 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