$(-> 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 ) )