$(->
logCount = 0
log = ->
logCount++
if logCount == 100
console.log('Ignoring further log messages')
else if logCount < 100
console.log.apply(console, arguments)
gla = new Gladder
canvas: 'canvas'
debug: true
errorCallback: console.error
window.gla = gla
gla.enable gla.Capability.BLEND
gla.blendFunc gla.BlendFactor.SRC_ALPHA, gla.BlendFactor.ONE_MINUS_SRC_ALPHA
PIXEL = 3 # pixel size
T = 16 # tile size
SS = 512 # sprite sheet size
ISS = 1 / SS # sprite sheet size
roundPixel = (x) -> Math.round(x / PIXEL) * PIXEL
solid = (x, y) ->
return true if y >= layers.collision.height
return true if x < 12 || x >= layers.collision.width - 12
return false if y < 0
layers.collision[x + layers.collision.width * y] != 0
textures = {}
musics = {}
playingMusic = null
sounds = {}
levels = []
currentLevel = 0
NUM_LEVELS = 7
LOADING = 0
UP = 1
DOWN = 2
DEAD = 3
WON = 4
INTRO = 5
FINISHED = 6
prevGameState = null
gameState = LOADING
paused = false
layers = []
absSprites = []
player = null
alarmClock = null
playMusic = (key) ->
if playingMusic && musicEnabled
playingMusic.stop()
playingMusic = musics[key]
if playingMusic && musicEnabled
playingMusic.play()
setGameState = (s) ->
switch gameState
when LOADING
$('#loading').fadeOut(200, -> $('#loading').remove())
when INTRO
$('#intro').fadeOut()
when FINISHED
$('#finished').fadeOut()
throw 'err' if gameState == FINISHED && s == FINISHED
prevGameState = gameState
gameState = s
log "Switched state to #{s}"
switch gameState
when INTRO
$('#intro').fadeIn()
when FINISHED
$('#finished').fadeIn(2000)
playMusic 'music'
when UP
playMusic 'music'
when DOWN
playMusic 'music2'
alarmClock = new AlarmClock()
absSprites.push(alarmClock)
absSprites.push(child) for child in alarmClock.children
else
playMusic null
tileProg = new gla.Program
vertexShader: 'vertex-shader'
fragmentShader: 'fragment-shader'
uniforms: {transform: 'mat4', sampler: 'sampler2D'}
attributes: {position: 'vec2', texCoord: 'vec2'}
spriteData = new Float32Array(4*6*128)
spriteBuffer = new gla.Buffer
data: spriteData
usage: gla.Buffer.Usage.DYNAMIC_DRAW
views:
position: {size: 2, stride: 2*2*4}
texCoord: {size: 2, stride: 2*2*4, offset: 2*4}
spriteBuffer.next = 0
tmpPos = vec2.create()
tmpTC = vec4.create()
spriteBuffer.drawSprite = (sprite) ->
return if !sprite.visible
@flush() if @next >= @size / 4
i = @next
vert = (x, y, tcx, tcy) ->
spriteData[i++] = x
spriteData[i++] = y
spriteData[i++] = tcx
spriteData[i++] = tcy
pos = tmpPos
sprite.getPos(pos)
tc = tmpTC
sprite.getTC(tc)
l = ISS * tc[0]
r = ISS * (tc[0] + tc[2])
t = ISS * tc[1]
b = ISS * (tc[1] + tc[3])
if sprite.flip
[l, r] = [r, l]
vert pos[0], pos[1], l, t
vert pos[0] + tc[2], pos[1], r, t
vert pos[0] + tc[2], pos[1] + tc[3], r, b
vert pos[0] + tc[2], pos[1] + tc[3], r, b
vert pos[0], pos[1] + tc[3], l, b
vert pos[0], pos[1], l, t
@next = i
spriteBuffer.flush = ->
return if @next == 0
@set {data: spriteData}
gla.draw
program: tileProg
uniforms: {transform: @transform, sampler: textures.sprites}
attributes: {position: @views.position, texCoord: @views.texCoord}
count: @next / 4
@next = 0
viewMatrix = mat4.create()
projectionMatrix = mat4.create()
mat4.ortho(projectionMatrix, 0, gla.canvas.width / PIXEL, gla.canvas.height / PIXEL, 0, -1, 1)
transform = mat4.create()
GRAVITY = 0.002
collide = (a, b) ->
a.pos[0] <= b.pos[0] + b.size[0] && b.pos[0] <= a.pos[0] + a.size[0] &&
a.pos[1] <= b.pos[1] + b.size[1] && b.pos[1] <= a.pos[1] + a.size[1]
class Sprite
constructor: (data) ->
@pos = vec2.fromValues(data.x, data.y)
@size = vec2.fromValues(data.width, data.height)
@vel = vec2.fromValues(0, 0)
@flip = false
@dead = false
@gone = false
@visible = true
@speed = 0.2
@time = 0
@offset = vec2.create()
getPos: (out) ->
vec2.add(out, @pos, @offset)
setOffset: (x, y) ->
vec2.set(@offset, x, y)
setTC: (tcx, tcy, w, h, frameCount, frameInterval) ->
@tc = vec4.fromValues(tcx, tcy, w, h)
@frameCount = frameCount
@frameInterval = frameInterval
getTC: (out) ->
if @frameCount? && @frameInterval?
frame = Math.floor(@time / @frameInterval) % @frameCount
vec4.set(out, @tc[0] + frame * @tc[2], @tc[1], @tc[2], @tc[3])
else
vec4.copy(out, @tc)
collideWith: (other) ->
update: (dt) ->
@time += dt
die: -> @dead = true
destroy: -> @gone = true
hover: (dt) ->
@time += dt
vec2.set(@pos, @basePos[0], Math.round(@basePos[1] + 2.7 * Math.sin(3 * @time / 1000)))
physics: (dt) ->
x = @pos[0]
y = @pos[1]
vx = @vel[0]
vy = @vel[1]
sx = @size[0]
sy = @size[1]
dir = (if @goingLeft then -1 else 0) + (if @goingRight then 1 else 0)
dir = 0 if @dead
@dir = dir
if dir != 0
vx += 0.001 * dt * dir
vx = Math.min(@speed, Math.max(-@speed, vx))
else
vx *= Math.exp(-0.02 * dt)
vy += GRAVITY * dt
if @standing && @jumping && !@dead
sounds.jump.play()
vy = -Math.sqrt(2 * GRAVITY * @jumpHeight)
# update x
dx = Math.round(dt * vx)
ymin = Math.floor(y / T)
ymax = Math.floor((y + sy - 1) / T)
if dx > 0
xCurr = Math.floor((x + sx - 1) / T)
xNext = Math.floor((x + dx + sx - 1) / T)
coll = false
for tx in [xCurr..xNext]
for ty in [ymin..ymax]
if solid(tx, ty)
x = tx * T - sx
vx = 0
coll = true
break
break if coll
x += dx if not coll
else if dx < 0
xCurr = Math.floor(x / T)
xNext = Math.floor((x + dx) / T)
coll = false
for tx in [xCurr..xNext]
for ty in [ymin..ymax]
if solid(tx, ty)
x = (tx + 1) * T
vx = 0
coll = true
break
break if coll
x += dx if not coll
# update y
dy = Math.round(dt * vy)
xmin = Math.floor(x / T)
xmax = Math.floor((x + sx - 1) / T)
oldStanding = @standing
@standing = false
@dropSpeed = null
if dy > 0
yCurr = Math.floor((y + sy - 1) / T)
yNext = Math.floor((y + dy + sy - 1) / T)
coll = false
for ty in [yCurr..yNext]
for tx in [xmin..xmax]
if solid(tx, ty)
@dropSpeed = vy
@standing = true
y = ty * T - sy
vy = 0
coll = true
break
break if coll
y += dy if not coll
else if dy < 0
yCurr = Math.floor(y / T)
yNext = Math.floor((y + dy) / T)
coll = false
for ty in [yCurr..yNext]
for tx in [xmin..xmax]
if solid(tx, ty)
y = (ty + 1) * T
vy = 0
coll = true
sounds.hit_head.play()
break
break if coll
y += dy if not coll
if oldStanding? && !oldStanding && @standing
sounds.land.play()
@pos[0] = x
@pos[1] = y
@vel[0] = vx
@vel[1] = vy
class Player extends Sprite
constructor: (data) ->
super(data)
@setTC(0, 480, 16, 32)
@setOffset(-4, -8)
@jumpHeight = 6.5 * T
@numSnoozers = parseInt(data.properties?.snoozers || '0')
@snoozers = for i in [0...@numSnoozers]
snoozer = new Sprite({x: 4 + 36 * i, y: (gla.canvas.height / PIXEL) - 20, width: 32, height: 16})
snoozer.setTC(0, 432, 32, 16)
absSprites.push snoozer
snoozer
update: (dt) ->
super(dt)
@physics(dt)
if !@dead
@flip = @dir < 0 if @dir != 0
tcy = if @goal then 448 else 480
if @standing
if @dir != 0
@setTC(32, tcy, 16, 32, 4, 100)
else
@setTC(0, tcy, 16, 32)
else
@setTC(96, tcy, 16, 32)
if gameState == DOWN
while @numSnoozers > 0
--@numSnoozers
@snoozers[@numSnoozers].visible = false
if @dropSpeed? && @dropSpeed > Math.sqrt(2 * GRAVITY * 10.5 * T)
@die()
sounds.crush.play()
clearMessages()
showMessage('Crushed!
You cannot fall very far while carrying treasure.' + restartMsg, 60000)
setGameState(DEAD)
player.goal.drop() if player.goal?
@setTC(16, 480, 16, 32)
else if gameState == UP
if @droppingSnoozer
@droppingSnoozer = false
if @numSnoozers > 0
--@numSnoozers
sounds.drop_snoozer.play()
@snoozers[@numSnoozers].visible = false
snoozer = new Snoozer({x: @pos[0] - 8, y: @pos[1] - 2, width: 32, height: 16})
snoozer.vel[1] = -0.15
layers.action.push(snoozer)
class Goal extends Sprite
constructor: (data) ->
super(data)
@setTC(32 + data.properties?.type * 32, 416, 32, 32)
@basePos = vec2.clone(@pos)
@time = 0
@pickedUp = false
drop: -> @dropped = true
update: (dt) ->
if @pickedUp
if @dropped
@physics(dt)
else
@align()
else
@hover(dt)
align: ->
vec2.copy(@pos, player.pos)
@pos[0] -= 12
@pos[1] -= 24
vec2.copy(@vel, player.vel)
@flip = player.flip
collideWith: (other) ->
if gameState == UP && other instanceof Player
@pickedUp = true
sounds.pickup.play()
player.goal = this
@align()
player.speed = 0.15
player.jumpHeight = 3.5 * T
setGameState(DOWN)
class End extends Sprite
constructor: (data) ->
super(data)
@visible = false
update: (dt) ->
if @winCounter?
@winCounter -= dt
if @winCounter < 0 && gameState == DOWN
setGameState(WON)
if player.goal?
player.goal.drop()
vec2.set(player.goal.vel, 0.5, -0.1)
clearMessages()
showMessage('Made it!
(Press space to continue.)', 60000)
sounds.win.play()
player.die()
collideWith: (other) ->
if !@winCounter? && gameState == DOWN && other instanceof Player
@winCounter = 500
clearMessages = -> $('#messages').empty()
showMessage = (text, timeout) ->
timeout ||= 5000
D = 500
element = $('
').html(text)
$('#messages').append(element)
element
.animate({opacity: 1}, D)
.animate({opacity: 1}, timeout - 2 * D)
#.animate({opacity: 0}, D)
.slideUp(D, -> element.remove())
class Trigger extends Sprite
constructor: (data) ->
super(data)
@visible = false
@text = data.properties?.text
@textTimeout = data.text || 5000
collideWith: (other) ->
if other instanceof Player && !player.dead
if @text?
showMessage(@text, @textTimeout)
sounds.message.play()
@destroy()
class Snoozer extends Sprite
constructor: (data) ->
super(data)
@setTC(0, 432, 32, 16)
@basePos = vec2.clone(@pos)
update: (dt) ->
@physics(dt)
collideWith: (other) ->
if gameState == DOWN && other instanceof Player
alarmClock.timeLeft = 10999
sounds.snooze.play()
@destroy()
class AlarmClock extends Sprite
constructor: ->
w = 96
h = 32
super({x: (gla.canvas.width / PIXEL - w) / 2, y: 8, width: w, height: h})
@setTC(416, 480, w, h)
@timeLeft = 10999
@children = for i in [0...4]
child = new Sprite({x: @pos[0] + w - 24 - 16 * i - 16 * Math.floor(i/2), y: @pos[1], width: 16, height: 32}, 256, 480)
child.setTC(256, 480, 16, 32, 10, 1)
child.update = ->
child
colon = new Sprite({x: @pos[0] + w - 24 - 16*2, y: @pos[1], width: 16, height: 32})
colon.setTC(240, 480, 16, 32)
@children.push(colon)
update: (dt) ->
prevSeconds = Math.max(0, Math.floor(@timeLeft / 1000))
if gameState == DOWN
@timeLeft -= dt
seconds = Math.max(0, Math.floor(@timeLeft / 1000))
if seconds != prevSeconds && seconds < 3
sounds.tick.play()
@children[0].time = seconds % 10
seconds = Math.floor(seconds / 10)
@children[1].time = seconds % 6
seconds = Math.floor(seconds / 6)
@children[2].time = seconds % 10
seconds = Math.floor(seconds / 10)
@children[3].time = seconds % 6
@children[4].visible = ((@timeLeft % 500) + 500) % 500 < 250
if !@timedOut && @timeLeft < 0 && gameState == DOWN
@timedOut = true
sounds.feefifofum.play()
setGameState(DEAD)
player.die()
layers.action.push(new GiantHand())
if gameState != DOWN
@destroy()
child.destroy() for child in @children
return
restartMsg = '
(Press space to restart.)'
class GiantHand extends Sprite
constructor: ->
w = 32
h = 144
super({x: player.pos[0], y: player.pos[1] - h, width: w, height: h})
@setOffset(-13, -130)
@basePos = vec2.clone(player.pos)
@setTC(480, 272, w, h)
@time = -500
update: (dt) ->
@time += dt
@pos[1] = @basePos[1] - 0.00002 * @time * @time
if !@pickedUp && @time >= 0
@pickedUp = true
clearMessages()
showMessage('Fee-fi-fo-fum!
Eaten by the giant!' + restartMsg, 60000)
player.update = ->
if player.goal?
player.goal.drop()
vec2.set(player.goal.vel, 0.5, -0.1)
if @time >= 0
vec2.copy(player.pos, @pos)
class Clouds extends Sprite
constructor: (x, y) ->
super({x: x, y: y, width: 256, height: 256})
@setTC(256, 0, 256, 256)
update: (dt) ->
#vec2.set(@pos, camera[0] / 2, camera[1] / 2)
clouds = []
for y in [1...5]
for x in [-5...5]
cloud = new Clouds(x * 256, y * 256)
clouds.push(cloud)
objects =
Player: Player
Goal: Goal
End: End
Trigger: Trigger
Snoozer: Snoozer
camera = vec2.create()
cleanUp = (layer) ->
r = 0
w = 0
goneCount = 0
while r < layer.length
if layer[r].gone
r++
goneCount++
else
layer[w++] = layer[r++]
for i in [0...goneCount]
layer.pop()
mainLoop = (dt) ->
return unless player?
return if paused
dt = 100 if dt > 100
for layer in layers
continue if layer.tileLayer
for sprite in layer
sprite.update(dt)
actionSprites = layers.action
for i in [0...actionSprites.length]
for j in [i+1...actionSprites.length]
a = actionSprites[i]
b = actionSprites[j]
if collide(a, b)
a.collideWith(b)
b.collideWith(a)
for layer in layers
continue if layer.tileLayer
cleanUp(layer)
for sprite in absSprites
sprite.update(dt) if sprite.update?
cleanUp(absSprites)
gla.clear {color: [0.4, 0.7, 1.0, 1]}
if player && !player.dead
camera[0] = player.pos[0] + player.size[0] / 2
camera[1] = player.pos[1] + player.size[0] / 2
mat4.identity(viewMatrix)
mat4.translate(viewMatrix, viewMatrix, vec3.fromValues(
Math.round(-camera[0] / 2) + gla.canvas.width / PIXEL / 2,
Math.round(-camera[1] / 2) + gla.canvas.height / PIXEL / 2,
0))
mat4.multiply(transform, projectionMatrix, viewMatrix)
spriteBuffer.transform = transform
for cloud in clouds
spriteBuffer.drawSprite(cloud)
spriteBuffer.flush()
mat4.identity(viewMatrix)
mat4.translate(viewMatrix, viewMatrix, vec3.fromValues(
-camera[0] + gla.canvas.width / PIXEL / 2,
-camera[1] + gla.canvas.height / PIXEL / 2,
0))
mat4.multiply(transform, projectionMatrix, viewMatrix)
for layer in layers
if layer.tileLayer && !layer.hidden
gla.draw
program: tileProg
uniforms: {transform: transform, sampler: textures.sprites}
attributes: {position: layer.buffer.views.position, texCoord: layer.buffer.views.texCoord}
count: layer.buffer.views.position.numItems()
else
for sprite in layer
spriteBuffer.drawSprite(sprite)
spriteBuffer.flush()
spriteBuffer.transform = projectionMatrix
for sprite in absSprites
spriteBuffer.drawSprite(sprite)
spriteBuffer.flush()
wantKey = (keyCode) -> keyCode in [32, 37, 38, 39, 40]
keydown = (e) ->
switch gameState
when INTRO
if e.which == 32
startLevel(1)
when UP, DOWN
switch e.which
when 32 then player.jumping = true
when 37 then player.goingLeft = true
when 38 then player.jumping = true
when 39 then player.goingRight = true
when 40 then player.droppingSnoozer = true
when DEAD
if e.which == 32
startLevel(currentLevel)
when WON
if e.which == 32
startLevel(currentLevel + 1)
e.preventDefault() if wantKey(e.which)
keyup = (e) ->
if gameState == UP || gameState == DOWN
switch e.which
when 32 then player.jumping = false
when 37 then player.goingLeft = false
when 38 then player.jumping = false
when 39 then player.goingRight = false
when 40 then
e.preventDefault() if wantKey(e.which)
$(document).keydown keydown
$(document).keyup keyup
$(window).blur -> paused = true
$(window).focus -> paused = false
gla.mainLoop mainLoop, 33
loadCount = 0
loading = (file) ->
loadCount++
console.log("Loading #{file}")
loaded = (file) ->
console.log("Loaded #{file}")
loadCount--
if loadCount == 0
startLevel(currentLevel)
loadError = (file) ->
console.error("#{file} failed to load")
startLevel = (index) ->
currentLevel = index
window.location.hash = "level#{currentLevel}"
log "Starting level #{index}"
if index <= 0
setGameState(INTRO)
return
if index > NUM_LEVELS
setGameState(FINISHED)
return
level = levels[index]
width = level.width
height = level.height
layers = []
absSprites = []
player = null
alarmClock = null
$('#messages').empty()
for l in level.layers
if l.type == 'tilelayer'
layer = l.data
verts = new Float32Array(4 * 6 * width * height)
i = 0
addVert = (x, y, dx, dy, tile) ->
verts[i++] = T * (x + dx)
verts[i++] = T * (y + dy)
verts[i++] = ((tile % 32) + dx) / 32
verts[i++] = (Math.floor(tile / 32) + dy) / 32
j = 0
for y in [0...height]
for x in [0...width]
tile = layer[j++]
continue if tile == 0
tile--
addVert(x, y, 0, 0, tile)
addVert(x, y, 1, 0, tile)
addVert(x, y, 1, 1, tile)
addVert(x, y, 1, 1, tile)
addVert(x, y, 0, 1, tile)
addVert(x, y, 0, 0, tile)
layer.buffer = new gla.Buffer
data: verts
views:
position: {size: 2, stride: 2*2*4}
texCoord: {size: 2, stride: 2*2*4, offset: 2*4}
layer.hidden = l.properties?.hidden?
layer.tileLayer = true
else # l.type == 'objectgroup'
layer = []
for o in l.objects
sprite = null
type = objects[o.type]
if type?
sprite = new type(o)
player = sprite if o.type == 'Player'
layer.push(sprite)
layer.tileLayer = false
layer.width = l.width
layer.height = l.height
layers.push(layer)
layers[l.name] = layer
setGameState(UP)
return
loadTexture = (url, key) ->
loading(url)
img = new Image()
img.onload = ->
tex = new gla.Texture
image: img
minFilter: gla.Texture.Filter.NEAREST
magFilter: gla.Texture.Filter.NEAREST
textures[key] = tex
loaded(url)
img.onerror = -> loadError(url)
img.src = url
loadTexture 'sprites.png', 'sprites'
loadMusic = (basename, volume) ->
loading(basename)
mus = new Howl
urls: ["#{basename}.mp3", "#{basename}.ogg"]
loop: true
volume: volume
onload: -> loaded(basename)
onloaderror: -> loadError(basename)
musics[basename] = mus
loadMusic 'music'
loadMusic 'music2'
loadSound = (basename, volume) ->
loading(basename)
snd = new Howl
urls: ["#{basename}.mp3", "#{basename}.ogg"]
volume: volume
onload: -> loaded(basename)
onloaderror: -> loadError(basename)
sounds[basename] = snd
loadSound 'crush'
loadSound 'drop_snoozer'
loadSound 'feefifofum'
loadSound 'hit_head', 0.3
loadSound 'jump'
loadSound 'land', 0.3
loadSound 'message'
loadSound 'pickup'
loadSound 'snooze'
loadSound 'tick'
loadSound 'win'
window.localStorage = {} unless window.localStorage?
musicEnabled = !localStorage.music? || localStorage.music == 'true'
$('#music')
.prop('checked', musicEnabled)
.click ->
musicEnabled = $(this).is(':checked')
window.localStorage.music = musicEnabled
if playingMusic
if musicEnabled then playingMusic.play() else playingMusic.pause()
loadLevel = (url, index) ->
loading(url)
$.ajax
url: url
dataType: 'json'
success: (level) -> levels[index] = level; loaded(url)
error: (level) -> loadError(url)
for i in [1..NUM_LEVELS]
loadLevel("level#{i}.json", i)
parseHash = ->
currentLevel = 0
m = /^#level(\d+)$/i.exec(window.location.hash)
if m
currentLevel = parseInt(m[1])
parseHash()
$(window).on('hashchange', ->
old = currentLevel
parseHash()
startLevel(currentLevel) if currentLevel != old
)
)