├── .gitignore ├── .gitmodules ├── Makefile ├── README.md ├── build.js ├── concepts └── concept.xcf ├── config ├── constants.json ├── js.json └── mangle.json ├── media ├── feedback │ ├── screen01.png │ ├── screen02.png │ ├── screen03.png │ ├── screen04.png │ └── screen05.png └── js13k │ ├── icon-160x160.png │ └── screenshot-400x250.png ├── src ├── index.html ├── js │ ├── character │ │ ├── character.js │ │ ├── enemy.js │ │ ├── jumping-enemy.js │ │ ├── player.js │ │ └── walking-enemy.js │ ├── controls.js │ ├── game.js │ ├── glitches │ │ ├── noise.js │ │ └── slice.js │ ├── graphics │ │ ├── code-pattern.js │ │ ├── font.js │ │ ├── halo.js │ │ ├── mobile-controls.js │ │ ├── noise-pattern.js │ │ ├── particle.js │ │ ├── show-tiles-animation.js │ │ └── spawn-animation.js │ ├── item │ │ ├── grenade-item.js │ │ ├── health-item.js │ │ └── item.js │ ├── main.js │ ├── menu │ │ ├── game-over.js │ │ ├── main.js │ │ ├── menu.js │ │ └── mode.js │ ├── sound │ │ ├── jsfxr.js │ │ └── sounds.js │ ├── tutorial-level.js │ ├── util │ │ ├── between.js │ │ ├── cache.js │ │ ├── dist.js │ │ ├── expose-math.js │ │ ├── flatten.js │ │ ├── format-time.js │ │ ├── globals.js │ │ ├── interp.js │ │ ├── pad.js │ │ ├── pick.js │ │ ├── progress-string.js │ │ ├── proto.js │ │ ├── rand.js │ │ ├── remove.js │ │ ├── resize.js │ │ └── shape.js │ └── world │ │ ├── camera.js │ │ ├── generate-world.js │ │ ├── grenade.js │ │ ├── masks.js │ │ ├── mirror-mask.js │ │ ├── tile.js │ │ └── world.js └── style.css └── tools ├── level-creator.html ├── level-creator.js ├── mask-creator.html ├── mask-creator.js ├── sound.html └── tile-render-map.js /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | concepts 3 | todo.txt 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "compiler"] 2 | path = js13k-compiler 3 | url = https://github.com/remvst/js13k-compiler 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build 2 | 3 | build: 4 | node build.js 5 | 6 | update: 7 | cd js13k-compiler && git checkout master && git pull 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Glitch Buster 2 | 3 | My entry for [JS13K 2016](http://2016.js13kgames.com/). 4 | 5 | Uses my [compiler](http://github.com/remvst/js13k-compiler) for better compression. 6 | 7 | ## Screenshots 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const compiler = require('./js13k-compiler/src/compiler'); 4 | 5 | const JS_FILES = require('./config/js'); 6 | const CONSTANTS = require('./config/constants'); 7 | const MANGLE_SETTINGS = require('./config/mangle'); 8 | 9 | function copy(obj){ 10 | return JSON.parse(JSON.stringify(obj)); 11 | } 12 | 13 | compiler.run((tasks) => { 14 | function buildJS(mangle, uglify){ 15 | // Manually injecting the DEBUG constant 16 | const constants = copy(CONSTANTS); 17 | constants.DEBUG = !uglify; 18 | 19 | const sequence = [ 20 | tasks.label('Building JS'), 21 | tasks.loadFiles(JS_FILES), 22 | tasks.concat(), 23 | tasks.constants(constants), 24 | tasks.macro('matrix'), 25 | tasks.macro('evaluate'), 26 | tasks.macro('nomangle') 27 | ]; 28 | 29 | if(mangle){ 30 | sequence.push(tasks.mangle(MANGLE_SETTINGS)); 31 | } 32 | 33 | if(uglify){ 34 | sequence.push(tasks.uglifyJS()); 35 | } 36 | 37 | return tasks.sequence(sequence); 38 | } 39 | 40 | function buildCSS(uglify){ 41 | const sequence = [ 42 | tasks.label('Building CSS'), 43 | tasks.loadFiles([__dirname + "/src/style.css"]), 44 | tasks.concat() 45 | ]; 46 | 47 | if(uglify){ 48 | sequence.push(tasks.uglifyCSS()); 49 | } 50 | 51 | return tasks.sequence(sequence); 52 | } 53 | 54 | function buildHTML(uglify){ 55 | const sequence = [ 56 | tasks.label('Building HTML'), 57 | tasks.loadFiles([__dirname + "/src/index.html"]), 58 | tasks.concat() 59 | ]; 60 | 61 | if(uglify){ 62 | sequence.push(tasks.uglifyHTML()); 63 | } 64 | 65 | return tasks.sequence(sequence); 66 | } 67 | 68 | function buildMain(){ 69 | return tasks.sequence([ 70 | tasks.block('Building main files'), 71 | tasks.parallel({ 72 | 'js': buildJS(true, true), 73 | 'css': buildCSS(true), 74 | 'html': buildHTML(true) 75 | }), 76 | tasks.combine(), 77 | tasks.output(__dirname + '/build/game.html'), 78 | tasks.label('Building ZIP'), 79 | tasks.zip('index.html'), 80 | tasks.output(__dirname + '/build/game.zip'), 81 | tasks.checkSize(__dirname + '/build/game.zip') 82 | ]); 83 | } 84 | 85 | function buildDebug(mangle, suffix){ 86 | return tasks.sequence([ 87 | tasks.block('Building debug files'), 88 | tasks.parallel({ 89 | // Debug JS in a separate file 90 | 'debug_js': tasks.sequence([ 91 | buildJS(mangle, false), 92 | tasks.output(__dirname + '/build/debug' + suffix + '.js') 93 | ]), 94 | 95 | // Injecting the debug file 96 | 'js': tasks.inject(['debug.js']), 97 | 98 | 'css': buildCSS(false), 99 | 'html': buildHTML(false) 100 | }), 101 | tasks.combine(), 102 | tasks.output(__dirname + '/build/debug' + suffix + '.html') 103 | ]); 104 | } 105 | 106 | function main(){ 107 | return tasks.sequence([ 108 | buildMain(), 109 | buildDebug(false, ''), 110 | buildDebug(true, '_mangled') 111 | ]); 112 | } 113 | 114 | return main(); 115 | }); 116 | -------------------------------------------------------------------------------- /concepts/concept.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remvst/glitchbuster/e2fb14f6ecede60a319f821137dbf80cdb0baf26/concepts/concept.xcf -------------------------------------------------------------------------------- /config/constants.json: -------------------------------------------------------------------------------- 1 | { 2 | "true": 1, 3 | "false": 0, 4 | "null": 0, 5 | 6 | "CANVAS_WIDTH_": 640, 7 | "CANVAS_HEIGHT_": 920, 8 | "TILE_SIZE": 80, 9 | "CHARACTER_WIDTH": 40, 10 | "CHARACTER_HEIGHT": 52, 11 | "WORLD_PADDING": 5, 12 | "MASK_ROWS": 10, 13 | "MASK_COLS": 10, 14 | 15 | "VOID_ID": 0, 16 | "TILE_ID": 1, 17 | "UNBREAKABLE_TILE_ID": 2, 18 | "PROBABLE_TILE_ID": 3, 19 | "SPAWN_ID": 4, 20 | "EXIT_ID": 5, 21 | "CEILING_SPIKE_ID": 6, 22 | "FLOOR_SPIKE_ID": 7, 23 | 24 | "RENDERABLE" : 1, 25 | "CYCLABLE": 2, 26 | "KILLABLE": 4, 27 | 28 | "HEALTH": 1, 29 | "GRENADE": 2, 30 | 31 | "PROBABLE_TILE_PROBABILITY": 0.5, 32 | "SPIKE_DENSITY": 0.05, 33 | 34 | "UP": 1, 35 | "DOWN": 2, 36 | "LEFT": 4, 37 | "RIGHT": 8, 38 | 39 | "GRAVITY": 7500, 40 | "PLAYER_SPEED": 560, 41 | "PLAYER_JUMP_ACCELERATION": -1700, 42 | "PLAYER_INITIAL_HEALTH": 5, 43 | "PLAYER_MAX_HEALTH": 8, 44 | 45 | "GRENADE_RADIUS": 8, 46 | "GRENADE_RADIUS_2": 16, 47 | "GRENADE_BOUNCE_FACTOR": 0.5, 48 | 49 | "WALKING_ENEMY_SPEED": 120, 50 | "JUMPING_ENEMY_SPEED": 480, 51 | "ENEMY_PATH_MIN_LENGTH": 2, 52 | "ENEMY_DENSITY": 0.2, 53 | 54 | "NOISE_PATTERN_SIZE": 400, 55 | "NOISE_PIXEL_SIZE": 4, 56 | 57 | "HALO_SIZE": 160, 58 | "HALO_SIZE_HALF": 80, 59 | "DARK_HALO_SIZE": 1000, 60 | "DARK_HALO_SIZE_HALF": 500, 61 | 62 | "FILLSTYLE": 1, 63 | "FILLRECT": 2, 64 | "DRAWIMAGE": 4, 65 | 66 | "SPIKE_HEIGHT": 24, 67 | "SPIKES_PER_TILE": 4, 68 | 69 | "ARROW_SIZE": 20, 70 | "ARROW_Y_OFFSET": -10, 71 | "ITEM_ARROW_Y_OFFSET": -40, 72 | "ITEM_PICKUP_RADIUS": 40, 73 | "ITEM_DENSITY": 0.05, 74 | "ENEMY_DROP_PROBABILITY": 0.5, 75 | 76 | "TIME_PER_LEVEL": 105, 77 | 78 | "MOBILE_BUTTON_SIZE": 80, 79 | 80 | "GAME_OVER_DEATH": 0, 81 | "GAME_OVER_TIME": 1, 82 | "GAME_OVER_SUCCESS": 2 83 | } 84 | -------------------------------------------------------------------------------- /config/js.json: -------------------------------------------------------------------------------- 1 | [ 2 | "src/js/sound/jsfxr.js", 3 | 4 | "src/js/util/expose-math.js", 5 | "src/js/util/cache.js", 6 | "src/js/util/pad.js", 7 | "src/js/util/flatten.js", 8 | "src/js/util/proto.js", 9 | 10 | "src/js/util/globals.js", 11 | "src/js/sound/sounds.js", 12 | 13 | "src/js/graphics/particle.js", 14 | "src/js/graphics/noise-pattern.js", 15 | "src/js/graphics/halo.js", 16 | "src/js/graphics/mobile-controls.js", 17 | "src/js/graphics/font.js", 18 | "src/js/graphics/spawn-animation.js", 19 | "src/js/graphics/show-tiles-animation.js", 20 | "src/js/graphics/code-pattern.js", 21 | 22 | "src/js/world/masks.js", 23 | "src/js/tutorial-level.js", 24 | 25 | "src/js/util/rand.js", 26 | "src/js/util/dist.js", 27 | "src/js/util/resize.js", 28 | "src/js/util/pick.js", 29 | "src/js/util/between.js", 30 | "src/js/util/remove.js", 31 | "src/js/util/interp.js", 32 | "src/js/util/format-time.js", 33 | 34 | "src/js/menu/menu.js", 35 | "src/js/menu/game-over.js", 36 | "src/js/menu/main.js", 37 | "src/js/menu/mode.js", 38 | 39 | "src/js/item/item.js", 40 | "src/js/item/grenade-item.js", 41 | "src/js/item/health-item.js", 42 | 43 | "src/js/world/mirror-mask.js", 44 | "src/js/world/generate-world.js", 45 | "src/js/world/grenade.js", 46 | "src/js/world/world.js", 47 | "src/js/world/camera.js", 48 | "src/js/world/tile.js", 49 | 50 | "src/js/character/character.js", 51 | "src/js/character/enemy.js", 52 | "src/js/character/walking-enemy.js", 53 | "src/js/character/jumping-enemy.js", 54 | "src/js/character/player.js", 55 | 56 | "src/js/glitches/slice.js", 57 | "src/js/glitches/noise.js", 58 | 59 | "src/js/game.js", 60 | "src/js/controls.js", 61 | "src/js/main.js" 62 | ] 63 | -------------------------------------------------------------------------------- /config/mangle.json: -------------------------------------------------------------------------------- 1 | { 2 | "skip": [ 3 | "arguments", 4 | "callee", 5 | "left", 6 | "px" 7 | ], 8 | "force": [ 9 | "alpha", 10 | "button", 11 | "mask", 12 | "matrix", 13 | "contains", 14 | "remove", 15 | "speed", 16 | "direction", 17 | "visible", 18 | "pad", 19 | "type", 20 | "rotation", 21 | "rows", 22 | "cols", 23 | "item", 24 | "center", 25 | "reason", 26 | "buttons", 27 | "rect", 28 | "data", 29 | "a", 30 | "b", 31 | "c", 32 | "d", 33 | "e", 34 | "f", 35 | "g", 36 | "h", 37 | "i", 38 | "j", 39 | "k", 40 | "l", 41 | "m", 42 | "n", 43 | "o", 44 | "p", 45 | "q", 46 | "r", 47 | "s", 48 | "t", 49 | "u", 50 | "v", 51 | "w", 52 | "x", 53 | "y", 54 | "z" 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /media/feedback/screen01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remvst/glitchbuster/e2fb14f6ecede60a319f821137dbf80cdb0baf26/media/feedback/screen01.png -------------------------------------------------------------------------------- /media/feedback/screen02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remvst/glitchbuster/e2fb14f6ecede60a319f821137dbf80cdb0baf26/media/feedback/screen02.png -------------------------------------------------------------------------------- /media/feedback/screen03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remvst/glitchbuster/e2fb14f6ecede60a319f821137dbf80cdb0baf26/media/feedback/screen03.png -------------------------------------------------------------------------------- /media/feedback/screen04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remvst/glitchbuster/e2fb14f6ecede60a319f821137dbf80cdb0baf26/media/feedback/screen04.png -------------------------------------------------------------------------------- /media/feedback/screen05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remvst/glitchbuster/e2fb14f6ecede60a319f821137dbf80cdb0baf26/media/feedback/screen05.png -------------------------------------------------------------------------------- /media/js13k/icon-160x160.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remvst/glitchbuster/e2fb14f6ecede60a319f821137dbf80cdb0baf26/media/js13k/icon-160x160.png -------------------------------------------------------------------------------- /media/js13k/screenshot-400x250.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remvst/glitchbuster/e2fb14f6ecede60a319f821137dbf80cdb0baf26/media/js13k/screenshot-400x250.png -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Glitch Buster 5 | 6 | 9 | 10 | 11 | 12 |
13 |
14 |
15 | 16 |
17 |
18 |
19 | 20 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/js/character/character.js: -------------------------------------------------------------------------------- 1 | function Character(){ 2 | this.x = this.y = 0; 3 | this.direction = 0; 4 | this.facing = 1; 5 | 6 | this.visible = true; 7 | 8 | this.offsetY = 0; 9 | this.bodyOffsetY = 0; 10 | this.bubbleTailLength = 0; 11 | this.saying = []; 12 | this.sayingTimeleft = 0; 13 | 14 | this.scaleFactorX = 1; 15 | this.scaleFactorY = 1; 16 | this.recoveryTime = 0; 17 | this.frictionFactor = 4; 18 | 19 | this.vX = 0; 20 | this.vY = 0; 21 | 22 | this.lastAdjustment = 0; 23 | 24 | var jumpCount = 0, 25 | previousFloorY; 26 | 27 | this.render = function(){ 28 | if(this.recoveryTime > 0 && ~~((this.recoveryTime * 2 * 4) % 2) && !this.dead || 29 | !this.visible || 30 | !V.contains(this.x, this.y, CHARACTER_WIDTH / 2)){ 31 | return; 32 | } 33 | 34 | save(); 35 | translate(~~this.x, ~~this.y + this.offsetY); 36 | 37 | // Halo 38 | if(!shittyMode && !this.dead){ 39 | drawImage(this.halo, -HALO_SIZE_HALF, -HALO_SIZE_HALF); 40 | } 41 | 42 | // Dialog 43 | if(this.sayingTimeleft > 0 && this.saying.length){ 44 | R.font = '16pt Arial'; 45 | 46 | var t = this.saying[0], 47 | w = measureText(t).width + 8; 48 | R.fillStyle = '#000'; 49 | R.globalAlpha = 0.5; 50 | fillRect(-w / 2, -68 - this.bubbleTailLength, w, 24); 51 | R.globalAlpha = 1; 52 | 53 | R.fillStyle = this.bodyColor; 54 | fillRect(-2, -40, 4, -this.bubbleTailLength); 55 | 56 | fillText(t, 0, -56 - this.bubbleTailLength); 57 | } 58 | 59 | // Facing left or right 60 | scale(this.facing * this.scaleFactorX, this.scaleFactorY); 61 | 62 | // Legs 63 | if(!this.dead){ 64 | save(); 65 | translate(evaluate(-CHARACTER_WIDTH / 2 + 2), evaluate(-CHARACTER_HEIGHT / 2)); 66 | 67 | var legAmplitude = 7, 68 | legPeriod = 0.3, 69 | legLength = (sin((G.t * PI * 2) / legPeriod) / 2) * legAmplitude + legAmplitude / 2; 70 | 71 | var leftLegLength = this.direction || jumpCount > 0 ? legLength : legAmplitude; 72 | var rightLegLength = this.direction || jumpCount > 0 ? legAmplitude - legLength : legAmplitude; 73 | 74 | R.fillStyle = this.legColor; 75 | fillRect(0, 45, 6, leftLegLength); 76 | fillRect(30, 45, 6, rightLegLength); 77 | restore(); 78 | } 79 | 80 | // Let's bob a little 81 | var bodyRotationMaxAngle = PI / 16, 82 | bodyRotationPeriod = 0.5, 83 | bodyRotation = (sin((G.t * PI * 2) / bodyRotationPeriod) / 2) * bodyRotationMaxAngle; 84 | 85 | if(this.bodyRotation){ 86 | bodyRotation = this.bodyRotation; 87 | }else if(!this.direction && !this.fixing){ 88 | bodyRotation = 0; 89 | } 90 | 91 | translate(0, this.bodyOffsetY); 92 | rotate(bodyRotation); 93 | 94 | save(); 95 | translate(evaluate(-CHARACTER_WIDTH / 2 - 3), evaluate(-CHARACTER_HEIGHT / 2)); 96 | 97 | // Body 98 | R.fillStyle = this.bodyColor; 99 | fillRect(0, 0, 46, 45); 100 | 101 | // Eyes 102 | var p = 4, // blink interval 103 | bt = 0.3, // blink time 104 | mt = G.t % p, // modulo-ed time 105 | mi = p - bt / 2, // middle of the blink 106 | s = min(1, max(-mt + mi, mt - mi) / (bt / 2)), // scale of the eyes 107 | h = s * 4; 108 | 109 | if(this.dead){ 110 | h = 1; 111 | } 112 | 113 | var eyesY = this.lookingDown ? 24 : 10; 114 | 115 | if(!this.fixing){ 116 | R.fillStyle = '#000'; 117 | var offset = this.talking ? -10 : 0; 118 | fillRect(27 + offset, eyesY, 4, h); 119 | fillRect(37 + offset, eyesY, 4, h); 120 | } 121 | restore(); 122 | 123 | restore(); 124 | }; 125 | 126 | this.cycle = function(e){ 127 | var before = { 128 | x: this.x, 129 | y: this.y 130 | }; 131 | 132 | this.recoveryTime -= e; 133 | 134 | if((this.sayingTimeleft -= e) <= 0){ 135 | this.say(this.saying.slice(1)); 136 | } 137 | 138 | if(this.dead){ 139 | this.direction = 0; 140 | } 141 | 142 | // Movement 143 | 144 | // Friction 145 | var frictionFactor = this.frictionFactor * this.speed, 146 | targetSpeed = this.direction * this.speed, 147 | diff = targetSpeed - this.vX, 148 | appliedDiff = between(-frictionFactor * e, diff, frictionFactor * e); 149 | 150 | this.vX = between(-this.speed, this.vX + appliedDiff, this.speed); 151 | 152 | this.x += this.vX * e; 153 | 154 | if(this.direction == -this.facing){ 155 | interp(this, 'scaleFactorX', -1, 1, 0.1); 156 | } 157 | 158 | this.facing = this.direction || this.facing; 159 | 160 | // Vertical movement 161 | this.vY += e * GRAVITY; 162 | this.y += this.vY * e; 163 | 164 | // Collisions 165 | this.lastAdjustment = this.readjust(before); 166 | 167 | // If there has been no adjustment for up or down, it means we're in the air 168 | if(!(this.lastAdjustment & DOWN) && !(this.lastAdjustment & UP)){ 169 | jumpCount = max(1, jumpCount); 170 | } 171 | }; 172 | 173 | this.jump = function(p, f){ 174 | if(f){ 175 | jumpCount = 0; 176 | } 177 | 178 | if(jumpCount++ <= 1){ 179 | this.vY = p * PLAYER_JUMP_ACCELERATION; 180 | previousFloorY = -1; 181 | 182 | var y = this.y + evaluate(CHARACTER_HEIGHT / 2); 183 | for(var i = 0 ; i < 5 ; i++){ 184 | var x = rand(this.x - evaluate(CHARACTER_WIDTH / 2), this.x + evaluate(CHARACTER_WIDTH / 2)); 185 | particle(3, '#888', [ 186 | ['x', x, x, 0.3], 187 | ['y', y, y - rand(40, 80), 0.3], 188 | ['s', 12, 0, 0.3] 189 | ]); 190 | } 191 | 192 | return true; 193 | } 194 | }; 195 | 196 | this.throwAway = function(angle, force){ 197 | this.vX = cos(angle) * force; 198 | this.vY = sin(angle) * force; 199 | this.facing = this.vX < 0 ? -1 : 1; 200 | }; 201 | 202 | this.hurt = function(source, power){ 203 | var facing = this.facing; 204 | if(this.recoveryTime <= 0 && !this.dead && !this.fixing){ 205 | hitSound.play(); 206 | 207 | this.throwAway(atan2( 208 | this.y - source.y, 209 | this.x - source.x 210 | ), 1500); 211 | 212 | this.recoveryTime = 2; 213 | 214 | if((this.health -= power || 1) <= 0){ 215 | this.die(); 216 | this.facing = facing; 217 | }else{ 218 | this.say(pick([ 219 | nomangle('Ouch!'), 220 | nomangle('health--') 221 | ])); 222 | } 223 | } 224 | }; 225 | 226 | this.landOn = function(tiles){ 227 | this.vY = 0; 228 | jumpCount = 0; 229 | 230 | // Find the tile that is the closest 231 | var tile = tiles.sort(function(a, b){ 232 | return abs(a.center.x - P.x) - abs(b.center.x - P.x); 233 | })[0]; 234 | 235 | tile.landed(this); 236 | 237 | if(tile.y === previousFloorY){ 238 | return; 239 | } 240 | 241 | if(!this.dead){ 242 | interp(this, 'bodyOffsetY', 0, 8, 0.1); 243 | interp(this, 'bodyOffsetY', 8, 0, 0.1, 0.1); 244 | 245 | for(var i = 0 ; i < 5 ; i++){ 246 | var x = rand(this.x - evaluate(CHARACTER_WIDTH / 2), this.x + evaluate(CHARACTER_WIDTH / 2)); 247 | particle(3, '#888', [ 248 | ['x', x, x, 0.3], 249 | ['y', tile.y, tile.y - rand(40, 80), 0.3], 250 | ['s', 12, 0, 0.3] 251 | ]); 252 | } 253 | } 254 | 255 | previousFloorY = tile.y; 256 | 257 | return true; 258 | }; 259 | 260 | this.tapOn = function(tiles){ 261 | this.vY = 0; // prevent from pushing that tile 262 | 263 | // Find the tile that was the least dangerous 264 | // We assume types are sorted from non lethal to most lethal 265 | var tile = tiles.sort(function(a, b){ 266 | return abs(a.center.x - P.x) - abs(b.center.x - P.x); 267 | })[0]; 268 | 269 | tile.tapped(this); 270 | }; 271 | 272 | this.readjust = function(before){ 273 | var leftX = this.x - evaluate(CHARACTER_WIDTH / 2), 274 | rightX = this.x + evaluate(CHARACTER_WIDTH / 2), 275 | topY = this.y - evaluate(CHARACTER_HEIGHT / 2), 276 | bottomY = this.y + evaluate(CHARACTER_HEIGHT / 2); 277 | 278 | var topLeft = W.tileAt(leftX, topY), 279 | topRight = W.tileAt(rightX, topY), 280 | bottomLeft = W.tileAt(leftX, bottomY), 281 | bottomRight = W.tileAt(rightX, bottomY); 282 | 283 | var t = 0; 284 | 285 | if(topRight && bottomLeft && !bottomRight && !topLeft){ 286 | t |= topRight.pushAway(this); 287 | t |= bottomLeft.pushAway(this); 288 | } 289 | 290 | else if(topLeft && bottomRight && !topRight && !bottomLeft){ 291 | t |= topLeft.pushAway(this); 292 | t |= bottomRight.pushAway(this); 293 | } 294 | 295 | else if(topLeft && topRight){ 296 | this.y = ceil(topY / TILE_SIZE) * TILE_SIZE + evaluate(CHARACTER_HEIGHT / 2); 297 | t |= DOWN; 298 | 299 | if(bottomLeft){ 300 | this.x = ceil(leftX / TILE_SIZE) * TILE_SIZE + evaluate(CHARACTER_WIDTH / 2); 301 | t |= RIGHT; 302 | }else if(bottomRight){ 303 | this.x = floor(rightX / TILE_SIZE) * TILE_SIZE - evaluate(CHARACTER_WIDTH / 2); 304 | t |= LEFT; 305 | } 306 | 307 | //this.tapOn([topLeft, topRight]); 308 | } 309 | 310 | else if(bottomLeft && bottomRight){ 311 | this.y = floor(bottomY / TILE_SIZE) * TILE_SIZE - evaluate(CHARACTER_HEIGHT / 2); 312 | t |= UP; 313 | 314 | if(topLeft){ 315 | this.x = ceil(leftX / TILE_SIZE) * TILE_SIZE + evaluate(CHARACTER_WIDTH / 2); 316 | t |= RIGHT; 317 | }else if(topRight){ 318 | this.x = floor(rightX / TILE_SIZE) * TILE_SIZE - evaluate(CHARACTER_WIDTH / 2); 319 | t |= LEFT; 320 | } 321 | 322 | //this.landOn([bottomLeft, bottomRight]); 323 | } 324 | 325 | // Collision against a wall 326 | else if(topLeft && bottomLeft){ 327 | this.x = ceil(leftX / TILE_SIZE) * TILE_SIZE + evaluate(CHARACTER_WIDTH / 2); 328 | t |= RIGHT; 329 | } 330 | 331 | else if(topRight && bottomRight){ 332 | this.x = floor(rightX / TILE_SIZE) * TILE_SIZE - evaluate(CHARACTER_WIDTH / 2); 333 | t |= LEFT; 334 | } 335 | 336 | // 1 intersection 337 | else if(bottomLeft){ 338 | t |= bottomLeft.pushAway(this); 339 | } 340 | 341 | else if(bottomRight){ 342 | t |= bottomRight.pushAway(this); 343 | } 344 | 345 | else if(topLeft){ 346 | t |= topLeft.pushAway(this); 347 | } 348 | 349 | else if(topRight){ 350 | t |= topRight.pushAway(this); 351 | } 352 | 353 | // Based on the adjustment, fire some tile events 354 | if(t & UP){ 355 | this.landOn([bottomLeft, bottomRight].filter(Boolean)); 356 | }else if(t & DOWN){ 357 | this.tapOn([topLeft, topRight].filter(Boolean)); 358 | } 359 | 360 | return t; 361 | }; 362 | 363 | this.die = function(){ 364 | // Can't die twice, avoid deaths while fixing bugs 365 | if(this.dead || this.fixing){ 366 | return; 367 | } 368 | 369 | this.controllable = false; 370 | this.dead = true; 371 | this.health = 0; 372 | 373 | for(var i = 0 ; i < 40 ; i++){ 374 | var x = rand(this.x - evaluate(CHARACTER_WIDTH / 2), this.x + evaluate(CHARACTER_WIDTH / 2)), 375 | y = rand(this.y - evaluate(CHARACTER_HEIGHT / 2), this.y + evaluate(CHARACTER_HEIGHT / 2)), 376 | yUnder = W.firstYUnder(x, this.y), 377 | d = rand(0.5, 1); 378 | particle(3, '#900', [ 379 | ['x', x, x, 0.5], 380 | ['y', y, y - rand(40, 80), 0.5], 381 | ['s', 12, 0, 0.5] 382 | ]); 383 | particle(3, '#900', [ 384 | ['x', x, x, d], 385 | ['y', y, yUnder, d, 0, easeOutBounce], 386 | ['s', 12, 0, d] 387 | ]); 388 | } 389 | 390 | this.bodyOffsetY = 8; 391 | 392 | interp(this, 'bodyRotation', 0, -PI / 2, 0.3); 393 | 394 | this.say(pick([ 395 | nomangle('...'), 396 | nomangle('exit(1)'), 397 | nomangle('NULL'), 398 | nomangle('Fatal error') 399 | ])); 400 | }; 401 | 402 | this.say = function(s){ 403 | this.saying = s.push ? s : [s]; 404 | this.sayingTimeleft = this.saying.length ? 3 : 0; 405 | if(this.saying.length){ 406 | interp(this, 'bubbleTailLength', 0, 56, 0.3, 0, easeOutBack); 407 | } 408 | }; 409 | 410 | return proto(this); 411 | } 412 | -------------------------------------------------------------------------------- /src/js/character/enemy.js: -------------------------------------------------------------------------------- 1 | function Enemy(x, y){ 2 | var sup = Character.call(this); 3 | 4 | this.x = x; 5 | this.y = y; 6 | 7 | this.bodyColor = '#f00'; 8 | this.legColor = '#b22'; 9 | this.halo = redHalo; 10 | this.health = 1; 11 | this.speed = 0; 12 | 13 | this.cycle = function(e){ 14 | // Skipping cycles for far enemies 15 | if(V.contains(this.x, this.y, evaluate(CHARACTER_WIDTH / 2))){ 16 | sup.cycle(e); 17 | 18 | if(!this.dead){ 19 | var dX = abs(P.x - this.x), 20 | dY = abs(P.y - this.y); 21 | if(dX < CHARACTER_WIDTH && dY < CHARACTER_HEIGHT){ 22 | // Okay there's a collision, but is he landing on me or is he colliding with me? 23 | if(dX < dY && P.y < this.y && P.vY > 0){ 24 | P.jump(0.8, true); 25 | this.hurt(P); 26 | }else{ 27 | P.hurt(this); 28 | this.direction = this.x > P.x ? 1 : -1; 29 | } 30 | } 31 | 32 | // Say random shit 33 | if(this.sayingTimeleft <= 0){ 34 | this.say('0x' + (~~rand(0x100000, 0xffffff)).toString(16)); 35 | } 36 | } 37 | } 38 | }; 39 | 40 | this.die = function(){ 41 | if(!this.dead){ 42 | sup.die(); 43 | 44 | var s = this; 45 | 46 | delayed(function(){ 47 | s.say([]); 48 | 49 | // Fly away animation 50 | interp(s, 'scaleFactorX', 1, 0, 0.4); 51 | interp(s, 'scaleFactorY', 1, 5, 0.3, 0.1); 52 | interp(s, 'offsetY', 0, -400, 0.3, 0.1, null, function(){ 53 | delayed(function(){ 54 | G.remove(s); 55 | }, 0); 56 | }); 57 | 58 | // Item drop 59 | G.droppable(s.x, s.y, ENEMY_DROP_PROBABILITY, true); 60 | }, 500); 61 | } 62 | }; 63 | 64 | return proto(this); 65 | } 66 | -------------------------------------------------------------------------------- /src/js/character/jumping-enemy.js: -------------------------------------------------------------------------------- 1 | function JumpingEnemy(x, y){ 2 | var sup = Enemy.call(this, x, y); 3 | 4 | this.nextJump = 4; 5 | this.frictionFactor = 0; 6 | 7 | this.speed = JUMPING_ENEMY_SPEED; 8 | 9 | this.cycle = function(e){ 10 | sup.cycle(e); 11 | 12 | if((this.nextJump -= e) <= 0 && !this.dead){ 13 | this.vX = (this.direction = this.facing = pick([-1, 1])) * this.speed; 14 | 15 | this.jump(0.8); 16 | this.nextJump = rand(1.5, 2.5); 17 | } 18 | }; 19 | 20 | this.landOn = function(t){ 21 | sup.landOn(t); 22 | this.vX = 0; 23 | this.direction = 0; 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/js/character/player.js: -------------------------------------------------------------------------------- 1 | function Player(){ 2 | var sup = Character.call(this); 3 | 4 | this.controllable = true; 5 | 6 | this.grenades = 0; 7 | this.health = PLAYER_INITIAL_HEALTH; 8 | 9 | this.bodyColor = '#fff'; 10 | this.legColor = '#aaa'; 11 | this.halo = whiteHalo; 12 | 13 | this.speed = PLAYER_SPEED; 14 | 15 | this.preparingGrenade = false; 16 | this.grenadePreparation = 0; 17 | 18 | this.cycle = function(e){ 19 | if(!this.controllable){ 20 | this.direction = 0; 21 | }else{ 22 | if(this.direction){ 23 | V.targetted = null; 24 | } 25 | 26 | var d = dist(this, W.exit.center); 27 | if(d < evaluate(TILE_SIZE / 2)){ 28 | this.controllable = false; 29 | this.fixing = true; 30 | 31 | this.say([ 32 | nomangle('Let\'s fix this...'), 33 | nomangle('Done!') 34 | ]); 35 | 36 | interp(this, 'x', this.x, W.exit.center.x, 1); 37 | interp(W.exit, 'alpha', 1, 0, 3); 38 | 39 | delayed(function(){ 40 | fixedSound.play(); 41 | G.bugFixed(); 42 | }, 3500); 43 | }else if(d < (CANVAS_WIDTH / 2) && !this.found){ 44 | this.found = true; 45 | this.say(nomangle('You found the bug!')); // TODO more strings 46 | } 47 | } 48 | 49 | this.grenadePreparation = (this.grenadePreparation + e / 4) % 1; 50 | 51 | sup.cycle(e); 52 | }; 53 | 54 | this.die = function(){ 55 | sup.die(); 56 | G.playerDied(); 57 | }; 58 | 59 | this.jump = function(p, f){ 60 | if(this.controllable && sup.jump(p, f)){ 61 | jumpSound.play(); 62 | } 63 | }; 64 | 65 | this.prepareGrenade = function(){ 66 | if(this.grenades){ 67 | this.preparingGrenade = true; 68 | this.grenadePreparation = 0; 69 | }else{ 70 | P.say(pick([ 71 | nomangle('You don\'t have any breakpoints'), 72 | nomangle('breakpoints.count == 0'), 73 | nomangle('NoBreakpointException') 74 | ])); 75 | } 76 | }; 77 | 78 | this.grenadePower = function(){ 79 | return 500 + (1 - abs((this.grenadePreparation - 0.5) * 2)) * 1500; 80 | }; 81 | 82 | this.throwGrenade = function(){ 83 | if(this.preparingGrenade && !this.dead){ 84 | var g = new Grenade( 85 | this.x, 86 | this.y, 87 | -PI / 2 + this.facing * PI / 4, 88 | this.grenadePower() 89 | ); 90 | G.add(g, evaluate(RENDERABLE | CYCLABLE)); 91 | 92 | V.targetted = g; // make the camera target the grenade 93 | 94 | this.preparingGrenade = false; 95 | this.grenades = max(0, this.grenades - 1); 96 | } 97 | }; 98 | 99 | this.say = function(a){ 100 | sup.say(a); 101 | if(a && a.length){ 102 | saySound.play(); 103 | } 104 | }; 105 | 106 | this.landOn = function(t){ 107 | if(sup.landOn(t)){ 108 | landSound.play(); 109 | } 110 | }; 111 | 112 | this.render = function(e){ 113 | sup.render(e); 114 | 115 | if(this.preparingGrenade){ 116 | var g = new Grenade( 117 | this.x, 118 | this.y, 119 | -PI / 2 + this.facing * PI / 4, 120 | this.grenadePower(), 121 | true 122 | ); 123 | 124 | R.fillStyle = '#fff'; 125 | for(var i = 0 ; i < 40 && !g.stuck ; i++){ 126 | g.cycle(1 / 60); 127 | 128 | if(!(i % 2)){ 129 | fillRect(~~g.x - 2, ~~g.y - 2, 4, 4); 130 | } 131 | } 132 | } 133 | }; 134 | } 135 | -------------------------------------------------------------------------------- /src/js/character/walking-enemy.js: -------------------------------------------------------------------------------- 1 | function WalkingEnemy(x, y){ 2 | var sup = Enemy.call(this, x, y); 3 | 4 | this.speed = WALKING_ENEMY_SPEED; 5 | 6 | this.direction = pick([-1, 1]); 7 | 8 | this.cycle = function(e){ 9 | sup.cycle(e); 10 | 11 | if(!this.dead){ 12 | var leftX = this.x - CHARACTER_WIDTH, 13 | rightX = this.x + CHARACTER_WIDTH, 14 | bottomY = this.y + CHARACTER_HEIGHT / 2, 15 | 16 | bottomLeft = W.tileAt(leftX, bottomY), 17 | bottomRight = W.tileAt(rightX, bottomY); 18 | 19 | if(this.lastAdjustment & LEFT || !bottomRight || bottomRight.type > CEILING_SPIKE_ID){ 20 | this.direction = -1; 21 | } 22 | if(this.lastAdjustment & RIGHT || !bottomLeft || bottomLeft.type > CEILING_SPIKE_ID){ 23 | this.direction = 1; 24 | } 25 | } 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /src/js/controls.js: -------------------------------------------------------------------------------- 1 | var touchButtons = {}, 2 | downKeys = {}; 3 | 4 | function reevalControls(e){ 5 | P.direction = 0; 6 | if(downKeys[37] || downKeys[65]){ 7 | P.direction = -1; 8 | } 9 | if(downKeys[39] || downKeys[68]){ 10 | P.direction = 1; 11 | } 12 | P.lookingDown = downKeys[40] || downKeys[83]; 13 | } 14 | 15 | onkeydown = function(e){ 16 | if(!downKeys[38] && e.keyCode == 38 || !downKeys[87] && e.keyCode == 87){ 17 | P.jump(1); 18 | } 19 | 20 | if(!downKeys[32] && e.keyCode == 32){ 21 | P.prepareGrenade(); 22 | } 23 | 24 | if(DEBUG && e.keyCode === 68){ 25 | P.die(); 26 | } 27 | 28 | downKeys[e.keyCode] = true; 29 | reevalControls(e); 30 | }; 31 | 32 | onkeyup = function(e){ 33 | if(e.keyCode == 32){ 34 | P.throwGrenade(); 35 | } 36 | 37 | downKeys[e.keyCode] = false; 38 | reevalControls(e); 39 | }; 40 | 41 | onclick = function(e){ 42 | var rect = C.getBoundingClientRect(); 43 | if(G.menu){ 44 | var x = CANVAS_WIDTH * (e.pageX - rect.left) / rect.width, 45 | y = CANVAS_HEIGHT * (e.pageY - rect.top) / rect.height; 46 | 47 | G.menu.click(x, y); 48 | } 49 | }; 50 | 51 | var touch = function(e){ 52 | e.preventDefault(); 53 | 54 | P.direction = 0; 55 | G.touch = true; 56 | 57 | touchButtons = {}; 58 | 59 | var rect = C.getBoundingClientRect(); 60 | for(var i = 0 ; i < e.touches.length ; i++){ 61 | var x = CANVAS_WIDTH * (e.touches[i].pageX - rect.left) / rect.width, 62 | col = ~~(x / (CANVAS_WIDTH / 4)); 63 | 64 | if(!G.menu){ 65 | if(!col){ 66 | P.direction = -1; 67 | }else if(col == 1){ 68 | P.direction = 1; 69 | }else if(col == 2){ 70 | P.prepareGrenade(); 71 | }else if(col == 3){ 72 | P.jump(1); 73 | } 74 | 75 | touchButtons[col] = true; 76 | } 77 | } 78 | 79 | if(P.preparingGrenade && !touchButtons[2]){ 80 | P.throwGrenade(); 81 | } 82 | }; 83 | 84 | addEventListener('touchstart', function(e){ 85 | onclick(e.touches[0]); 86 | }); 87 | addEventListener('touchstart', touch); 88 | addEventListener('touchmove', touch); 89 | addEventListener('touchend', touch); 90 | -------------------------------------------------------------------------------- /src/js/game.js: -------------------------------------------------------------------------------- 1 | function Game(){ 2 | G = this; 3 | 4 | var glitchEnd, 5 | nextGlitch = 0, 6 | glitchTimeleft = 0; 7 | 8 | G.currentLevel = 0; 9 | G.resolution = 1; 10 | 11 | G.t = 0; 12 | //G.frameCount = 0; 13 | //G.frameCountStart = Date.now(); 14 | 15 | V = new Camera(); 16 | P = new Player(); 17 | P.controllable = false; 18 | 19 | G.tutorial = function(){ 20 | G.newGame(true); 21 | }; 22 | 23 | G.newGame = function(tutorial){ 24 | P = new Player(); 25 | 26 | G.currentLevel = tutorial ? -1 : 0; 27 | G.totalTime = 0; 28 | G.startNewWorld(); 29 | interp(G.menu, 'alpha', 1, 0, 0.5, 0, 0, function(){ 30 | G.menu = null; 31 | }); 32 | 33 | G.add(new SpawnAnimation(P.x, P.y), RENDERABLE); 34 | }; 35 | 36 | G.startNewWorld = function(dummy){ 37 | G.cyclables = []; 38 | G.killables = []; 39 | G.renderables = []; 40 | G.timeLeft = TIME_PER_LEVEL; 41 | 42 | G.applyGlitch(0, 0.5); 43 | 44 | if(dummy){ 45 | return; 46 | } 47 | 48 | // World 49 | W = new World(generateWorld(++G.currentLevel)); 50 | 51 | // Keeping track of the items we can spawn 52 | W.itemsAllowed = { 53 | HEALTH: PLAYER_MAX_HEALTH - P.health, // max 6 health 54 | GRENADE: 10 - P.grenades // max 5 nades 55 | }; 56 | 57 | G.hideTiles = false; 58 | 59 | // Player 60 | P.x = W.spawn.x + TILE_SIZE / 2; 61 | P.y = W.spawn.y + TILE_SIZE - CHARACTER_WIDTH / 2; 62 | P.controllable = true; 63 | P.fixing = false; 64 | 65 | G.add(V, CYCLABLE); 66 | G.add(P, evaluate(CYCLABLE | RENDERABLE | KILLABLE)); 67 | 68 | // Prevent camera from lagging behind 69 | V.forceCenter(); 70 | 71 | // Enemies 72 | if(!G.currentLevel){ 73 | // Put the enemies at the right spots 74 | var e1; 75 | 76 | G.add(e1 = new WalkingEnemy(4500, 800), evaluate(CYCLABLE | RENDERABLE | KILLABLE)); 77 | G.add(new JumpingEnemy(5700, 800), evaluate(CYCLABLE | RENDERABLE | KILLABLE)); 78 | 79 | var metEnemy; 80 | G.add({ 81 | cycle: function(){ 82 | if(!metEnemy && abs(P.x - e1.x) < CANVAS_WIDTH){ 83 | metEnemy = true; 84 | 85 | P.say([ 86 | nomangle('Watch out for the pointers!'), 87 | nomangle('They\'re super dangerous!'), 88 | nomangle('Either avoid them or kill them') 89 | ]); 90 | } 91 | } 92 | }, CYCLABLE); 93 | }else{ 94 | delayed(function(){ 95 | P.say(pick([ 96 | nomangle('There\'s more?!'), 97 | nomangle('Yay more bugs'), 98 | nomangle('Okay one more bug...') 99 | ])); 100 | }, 500); 101 | 102 | // Add enemies 103 | W.detectPaths(ENEMY_PATH_MIN_LENGTH).forEach(function(path){ 104 | var enemy = new (pick([WalkingEnemy, JumpingEnemy]))( 105 | TILE_SIZE * rand(path.colLeft, path.colRight), 106 | TILE_SIZE * (path.row + 1) - CHARACTER_HEIGHT / 2 107 | ); 108 | if(rand() < ENEMY_DENSITY && dist(enemy, P) > CANVAS_WIDTH / 2){ 109 | G.add(enemy, evaluate(CYCLABLE | RENDERABLE | KILLABLE)); 110 | } 111 | }); 112 | 113 | // Add items for pickup 114 | var itemPaths = W.detectPaths(1); 115 | pick(itemPaths, itemPaths.length).forEach(function(path){ 116 | // Create the item and place it on the path 117 | G.droppable( 118 | (~~rand(path.colLeft, path.colRight) + 0.5) * TILE_SIZE, 119 | (path.row + 0.5) * TILE_SIZE, 120 | ITEM_DENSITY 121 | ); 122 | }); 123 | } 124 | }; 125 | 126 | // Game loop 127 | G.cycle = function(e){ 128 | G.t += e; 129 | 130 | /*// 100th frame, checking if we are in a bad situation, and if yes, enable shitty mode 131 | if(++G.frameCount == 100 && (G.frameCount / ((Date.now() - G.frameCountStart) / 1000) < 30)){ 132 | G.setResolution(G.resolution * 0.5); 133 | shittyMode = true; 134 | }*/ 135 | 136 | glitchTimeleft -= e; 137 | if(glitchTimeleft <= 0){ 138 | glitchEnd = null; 139 | 140 | nextGlitch -= e; 141 | if(nextGlitch <= 0){ 142 | G.applyGlitch(); 143 | } 144 | } 145 | 146 | var maxDelta = 1 / 120, // TODO adjust 147 | deltas = ~~(e / maxDelta); 148 | while(e > 0){ 149 | G.doCycle(min(e, maxDelta)); 150 | e -= maxDelta; 151 | } 152 | 153 | // Rendering 154 | save(); 155 | scale(G.resolution, G.resolution); 156 | 157 | // Font settings are common across the game 158 | R.textAlign = nomangle('center'); 159 | R.textBaseline = nomangle('middle'); 160 | 161 | if(W){ 162 | W.render(); 163 | } 164 | 165 | if(G.menu){ 166 | G.menu.render(); 167 | }else{ 168 | // HUD 169 | 170 | // Health string 171 | var healthString = ''; 172 | for(i = 0 ; i < P.health ; i++){ 173 | healthString += '!'; 174 | } 175 | 176 | // Timer string 177 | var timerString = formatTime(G.timeLeft, true), 178 | progressString = nomangle('progress: ') + G.currentLevel + '/13', 179 | grenadesString = nomangle('breakpoints: ') + P.grenades; 180 | 181 | drawText(R, timerString, (CANVAS_WIDTH - requiredCells(timerString) * 10) / 2, mobile ? 50 : 10, 10, G.timeLeft > 30 ? '#fff' : '#f00'); 182 | drawCachedText(R, healthString, (CANVAS_WIDTH - requiredCells(healthString) * 5) / 2, mobile ? 120 : 80, 5, P.health < 3 || P.recoveryTime > 1.8 ? '#f00' : '#fff'); 183 | 184 | drawCachedText(R, progressString, (CANVAS_WIDTH - requiredCells(progressString) * 4) - 10, 10, 4, '#fff'); 185 | drawCachedText(R, grenadesString, 10, 10, 4, '#fff'); 186 | 187 | if(G.touch){ 188 | // Mobile controls 189 | [leftArrow, rightArrow, grenadeButton, jumpArrow].forEach(function(b, i){ 190 | R.globalAlpha = touchButtons[i] ? 1 : 0.5; 191 | drawImage(b, (i + 0.5) * CANVAS_WIDTH / 4 - MOBILE_BUTTON_SIZE / 2, CANVAS_HEIGHT - 100); 192 | }); 193 | 194 | R.globalAlpha = 1; 195 | } 196 | } 197 | 198 | if(DEBUG){ 199 | save(); 200 | 201 | R.fillStyle = '#000'; 202 | fillRect(CANVAS_WIDTH * 0.6, 0, CANVAS_WIDTH * 0.4, 120); 203 | 204 | R.fillStyle = 'white'; 205 | R.textAlign = 'left'; 206 | R.font = '18pt Courier New'; 207 | fillText('FPS: ' + G.fps, CANVAS_WIDTH * 0.6, 20); 208 | fillText('Cyclables: ' + G.cyclables.length, CANVAS_WIDTH * 0.6, 40); 209 | fillText('Renderables: ' + G.renderables.length, CANVAS_WIDTH * 0.6, 60); 210 | fillText('Killables: ' + G.killables.length, CANVAS_WIDTH * 0.6, 80); 211 | fillText('Resolution: ' + G.resolution, CANVAS_WIDTH * 0.6, 100); 212 | 213 | restore(); 214 | } 215 | 216 | restore(); 217 | 218 | if(glitchEnd){ 219 | glitchEnd(); 220 | } 221 | }; 222 | 223 | G.doCycle = function(e){ 224 | // Cycles 225 | for(var i = G.cyclables.length ; --i >= 0 ;){ 226 | G.cyclables[i].cycle(e); 227 | } 228 | 229 | if(!G.menu && P.controllable){ 230 | if((G.timeLeft -= e) <= 0){ 231 | G.timeLeft = 0; 232 | G.menu = new GameOverMenu(GAME_OVER_TIME); 233 | interp(G.menu, 'alpha', 0, 1, 0.5); 234 | } 235 | 236 | if(G.currentLevel){ 237 | // Not counting the tutorial time because it's skippable anyway 238 | G.totalTime += e; 239 | } 240 | } 241 | }; 242 | 243 | G.applyGlitch = function(id, t){ 244 | var l = [function(){ 245 | glitchEnd = noiseGlitch; 246 | }]; 247 | 248 | if(!G.menu && !shittyMode){ 249 | l.push(function(){ 250 | glitchEnd = sliceGlitch; 251 | }); 252 | } 253 | 254 | if(isNaN(id)){ 255 | pick(l)(); 256 | }else{ 257 | l[id](); 258 | } 259 | 260 | glitchTimeleft = t || rand(0.1, 0.3); 261 | nextGlitch = G.currentLevel ? rand(4, 8) : 99; 262 | }; 263 | 264 | G.playerDied = function(){ 265 | delayed(function(){ 266 | G.menu = new GameOverMenu(GAME_OVER_DEATH); 267 | interp(G.menu, 'alpha', 0, 1, 0.5); 268 | }, 2000); 269 | }; 270 | 271 | G.bugFixed = function(){ 272 | if(G.currentLevel == 13){ 273 | G.menu = new GameOverMenu(GAME_OVER_SUCCESS); 274 | interp(G.menu, 'alpha', 0, 1, 0.5); 275 | }else{ 276 | G.applyGlitch(0, 0.5); 277 | hideTilesAnimation(); 278 | delayed(function(){ 279 | G.startNewWorld(); 280 | G.hideTiles = true; 281 | delayed(showTilesAnimation, 500); 282 | }, 500); 283 | } 284 | }; 285 | 286 | G.mainMenu = function(){ 287 | G.menu = new MainMenu(); 288 | }; 289 | 290 | G.setResolution = function(r){ 291 | G.resolution = r; 292 | C.width = CANVAS_WIDTH * r; 293 | C.height = CANVAS_HEIGHT * r; 294 | }; 295 | 296 | G.add = function(e, type){ 297 | if(type & RENDERABLE){ 298 | G.renderables.push(e); 299 | } 300 | if(type & CYCLABLE){ 301 | G.cyclables.push(e); 302 | } 303 | if(type & KILLABLE){ 304 | G.killables.push(e); 305 | } 306 | }; 307 | 308 | G.remove = function(e){ 309 | remove(G.cyclables, e); 310 | remove(G.killables, e); 311 | remove(G.renderables, e); 312 | }; 313 | 314 | G.droppable = function(x, y, probability, particles){ 315 | if(rand() < probability){ 316 | var item = new (pick([GrenadeItem, HealthItem]))(x, y); 317 | if(--W.itemsAllowed[item.type] > 0){ 318 | G.add(item, evaluate(CYCLABLE | RENDERABLE)); 319 | if(particles){ 320 | item.particles(); 321 | } 322 | } 323 | } 324 | }; 325 | 326 | /*var displayablePixels = w.innerWidth * w.innerHeight * w.devicePixelRatio, 327 | gamePixels = CANVAS_WIDTH / CANVAS_HEIGHT, 328 | ratio = displayablePixels / gamePixels; 329 | if(ratio < 0.5){ 330 | G.setResolution(ratio * 2); 331 | }*/ 332 | 333 | G.startNewWorld(true); 334 | 335 | G.menu = new (mobile ? ModeMenu : MainMenu)(); 336 | if(!mobile){ 337 | shittyMode = false; 338 | } 339 | 340 | glitchTimeleft = 0; 341 | nextGlitch = 1; 342 | 343 | var lf = Date.now(); 344 | (function(){ 345 | var n = Date.now(), 346 | e = (n - lf) / 1000; 347 | 348 | if(DEBUG){ 349 | G.fps = ~~(1 / e); 350 | } 351 | 352 | lf = n; 353 | 354 | G.cycle(e); 355 | 356 | (requestAnimationFrame || webkitRequestAnimationFrame || mozRequestAnimationFrame)(arguments.callee); 357 | })(); 358 | } 359 | -------------------------------------------------------------------------------- /src/js/glitches/noise.js: -------------------------------------------------------------------------------- 1 | function noiseGlitch(){ 2 | R.fillStyle = noisePattern; 3 | 4 | var x = ~~rand(-NOISE_PATTERN_SIZE, NOISE_PATTERN_SIZE), 5 | y = ~~rand(-NOISE_PATTERN_SIZE, NOISE_PATTERN_SIZE); 6 | 7 | save(); 8 | translate(x, y); 9 | R.globalAlpha = rand(0.5); 10 | fillRect(-x, -y, CANVAS_WIDTH, CANVAS_HEIGHT); 11 | restore(); 12 | } 13 | -------------------------------------------------------------------------------- /src/js/glitches/slice.js: -------------------------------------------------------------------------------- 1 | function sliceGlitch(){ 2 | var sh = CANVAS_HEIGHT / 10; 3 | 4 | drawImage(cache(CANVAS_WIDTH, CANVAS_HEIGHT, function(r){ 5 | for(var y = 0 ; y < CANVAS_HEIGHT ; y += sh){ 6 | r.drawImage( 7 | C, 8 | 0, y, CANVAS_WIDTH, sh, 9 | rand(-100, 100), y, CANVAS_WIDTH, sh 10 | ); 11 | } 12 | }), 0, 0); 13 | } 14 | -------------------------------------------------------------------------------- /src/js/graphics/code-pattern.js: -------------------------------------------------------------------------------- 1 | var codePattern = cachePattern(400, 400, function(r){ 2 | var lines = Character.toString().split(';').slice(0, 20), 3 | step = 400 / lines.length, 4 | y = step / 2; 5 | 6 | with(r){ 7 | fillStyle = '#000'; 8 | fillRect(0, 0, 400, 400); 9 | 10 | fillStyle = '#fff'; 11 | globalAlpha = 0.1; 12 | font = '14pt Courier New'; 13 | 14 | lines.forEach(function(l, i){ 15 | fillText(l, 0, y); 16 | 17 | y += step; 18 | }); 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /src/js/graphics/font.js: -------------------------------------------------------------------------------- 1 | var defs = { 2 | nomangle(a): matrix([ 3 | [1,1,1], 4 | [1,0,1], 5 | [1,1,1], 6 | [1,0,1], 7 | [1,0,1] 8 | ]), 9 | nomangle(b): matrix([ 10 | [1,1,1], 11 | [1,0,1], 12 | [1,1,0], 13 | [1,0,1], 14 | [1,1,1] 15 | ]), 16 | nomangle(c): matrix([ 17 | [1,1,1], 18 | [1,0,0], 19 | [1,0,0], 20 | [1,0,0], 21 | [1,1,1] 22 | ]), 23 | nomangle(d): matrix([ 24 | [1,1,0], 25 | [1,0,1], 26 | [1,0,1], 27 | [1,0,1], 28 | [1,1,1] 29 | ]), 30 | nomangle(e): matrix([ 31 | [1,1,1], 32 | [1,0,0], 33 | [1,1,0], 34 | [1,0,0], 35 | [1,1,1] 36 | ]), 37 | nomangle(f): matrix([ 38 | [1,1,1], 39 | [1,0,0], 40 | [1,1,0], 41 | [1,0,0], 42 | [1,0,0] 43 | ]), 44 | nomangle(g): matrix([ 45 | [1,1,1], 46 | [1,0,0], 47 | [1,0,0], 48 | [1,0,1], 49 | [1,1,1] 50 | ]), 51 | nomangle(h): matrix([ 52 | [1,0,1], 53 | [1,0,1], 54 | [1,1,1], 55 | [1,0,1], 56 | [1,0,1] 57 | ]), 58 | nomangle(i): matrix([ 59 | [1,1,1], 60 | [0,1,0], 61 | [0,1,0], 62 | [0,1,0], 63 | [1,1,1] 64 | ]), 65 | /*_j: [ 66 | [0,0,1], 67 | [0,0,1], 68 | [0,0,1], 69 | [1,0,1], 70 | [1,1,1] 71 | ],*/ 72 | nomangle(k): matrix([ 73 | [1,0,1], 74 | [1,0,1], 75 | [1,1,0], 76 | [1,0,1], 77 | [1,0,1] 78 | ]), 79 | nomangle(l): matrix([ 80 | [1,0,0], 81 | [1,0,0], 82 | [1,0,0], 83 | [1,0,0], 84 | [1,1,1] 85 | ]), 86 | nomangle(m): matrix([ 87 | [1,0,1], 88 | [1,1,1], 89 | [1,0,1], 90 | [1,0,1], 91 | [1,0,1] 92 | ]), 93 | nomangle(n): matrix([ 94 | [1,1,1], 95 | [1,0,1], 96 | [1,0,1], 97 | [1,0,1], 98 | [1,0,1] 99 | ]), 100 | nomangle(o): matrix([ 101 | [1,1,1], 102 | [1,0,1], 103 | [1,0,1], 104 | [1,0,1], 105 | [1,1,1] 106 | ]), 107 | nomangle(p): matrix([ 108 | [1,1,1], 109 | [1,0,1], 110 | [1,1,1], 111 | [1,0,0], 112 | [1,0,0] 113 | ]), 114 | nomangle(q): matrix([ 115 | [1,1,1], 116 | [1,0,1], 117 | [1,0,1], 118 | [1,1,1], 119 | [0,0,1] 120 | ]), 121 | nomangle(r): matrix([ 122 | [1,1,1], 123 | [1,0,1], 124 | [1,1,0], 125 | [1,0,1], 126 | [1,0,1] 127 | ]), 128 | nomangle(s): matrix([ 129 | [1,1,1], 130 | [1,0,0], 131 | [1,1,1], 132 | [0,0,1], 133 | [1,1,1] 134 | ]), 135 | nomangle(t): matrix([ 136 | [1,1,1], 137 | [0,1,0], 138 | [0,1,0], 139 | [0,1,0], 140 | [0,1,0] 141 | ]), 142 | nomangle(u): matrix([ 143 | [1,0,1], 144 | [1,0,1], 145 | [1,0,1], 146 | [1,0,1], 147 | [1,1,1] 148 | ]), 149 | nomangle(v): matrix([ 150 | [1,0,1], 151 | [1,0,1], 152 | [1,0,1], 153 | [1,0,1], 154 | [0,1,0] 155 | ]), 156 | nomangle(w): matrix([ 157 | [1,0,1,0,1], 158 | [1,0,1,0,1], 159 | [1,0,1,0,1], 160 | [1,0,1,0,1], 161 | [0,1,0,1,0] 162 | ]), 163 | nomangle(x): matrix([ 164 | [1,0,1], 165 | [1,0,1], 166 | [0,1,0], 167 | [1,0,1], 168 | [1,0,1] 169 | ]), 170 | nomangle(y): matrix([ 171 | [1,0,1], 172 | [1,0,1], 173 | [1,1,1], 174 | [0,1,0], 175 | [0,1,0] 176 | ]), 177 | /*'\'': matrix([ 178 | [1] 179 | ]),*/ 180 | '.': matrix([ 181 | [0], 182 | [0], 183 | [0], 184 | [0], 185 | [1] 186 | ]), 187 | ' ': matrix([ 188 | [0,0], 189 | [0,0], 190 | [0,0], 191 | [0,0], 192 | [0,0] 193 | ]), 194 | '-': [ 195 | [0,0], 196 | [0,0], 197 | [1,1], 198 | [0,0], 199 | [0,0] 200 | ], 201 | ':': matrix([ 202 | [0], 203 | [1], 204 | [ ], 205 | [1], 206 | [ ] 207 | ]), 208 | '?': matrix([ 209 | [1,1,1], 210 | [1,1,1], 211 | [1,1,1], 212 | [1,1,1], 213 | [1,1,1] 214 | ]), 215 | '!': matrix([ 216 | [0,1,0,1,0], 217 | [1,1,1,1,1], 218 | [1,1,1,1,1], 219 | [0,1,1,1,0], 220 | [0,0,1,0,0] 221 | ]), 222 | '/': matrix([ 223 | [0,0,1], 224 | [0,0,1], 225 | [0,1,0], 226 | [1,0,0], 227 | [1,0,0] 228 | ]), 229 | '1': matrix([ 230 | [1,1,0], 231 | [0,1,0], 232 | [0,1,0], 233 | [0,1,0], 234 | [1,1,1] 235 | ]), 236 | '2': matrix([ 237 | [1,1,1], 238 | [0,0,1], 239 | [1,1,1], 240 | [1,0,0], 241 | [1,1,1] 242 | ]), 243 | '3': matrix([ 244 | [1,1,1], 245 | [0,0,1], 246 | [0,1,1], 247 | [0,0,1], 248 | [1,1,1] 249 | ]), 250 | '4': matrix([ 251 | [1,0,0], 252 | [1,0,0], 253 | [1,0,1], 254 | [1,1,1], 255 | [0,0,1] 256 | ]), 257 | '5': matrix([ 258 | [1,1,1], 259 | [1,0,0], 260 | [1,1,0], 261 | [0,0,1], 262 | [1,1,0] 263 | ]), 264 | '6': matrix([ 265 | [1,1,1], 266 | [1,0,0], 267 | [1,1,1], 268 | [1,0,1], 269 | [1,1,1] 270 | ]), 271 | '7': matrix([ 272 | [1,1,1], 273 | [0,0,1], 274 | [0,1,0], 275 | [0,1,0], 276 | [0,1,0] 277 | ]), 278 | '8': matrix([ 279 | [1,1,1], 280 | [1,0,1], 281 | [1,1,1], 282 | [1,0,1], 283 | [1,1,1] 284 | ]), 285 | '9': matrix([ 286 | [1,1,1], 287 | [1,0,1], 288 | [1,1,1], 289 | [0,0,1], 290 | [1,1,1] 291 | ]), 292 | '0': matrix([ 293 | [1,1,1], 294 | [1,0,1], 295 | [1,0,1], 296 | [1,0,1], 297 | [1,1,1] 298 | ]), 299 | '(': matrix([ 300 | [0,1], 301 | [1], 302 | [1], 303 | [1], 304 | [0,1] 305 | ]), 306 | ')': matrix([ 307 | [1, 0], 308 | [0, 1], 309 | [0, 1], 310 | [0, 1], 311 | [1] 312 | ]) 313 | }; 314 | 315 | if(DEBUG){ 316 | (function(){ 317 | used = {}; 318 | for(var i in defs){ 319 | used[i] = false; 320 | } 321 | 322 | window.checkUsed = function(){ 323 | var unused = []; 324 | for(var i in used){ 325 | if(!used[i]){ 326 | unused.push(i); 327 | } 328 | } 329 | return unused.sort(); 330 | }; 331 | })(); 332 | } 333 | 334 | function drawText(r, t, x, y, s, c){ 335 | for(var i = 0 ; i < t.length ; i++){ 336 | if(DEBUG){ 337 | used[t.charAt(i)] = true; 338 | } 339 | 340 | var cached = cachedCharacter(t.charAt(i), s, c); 341 | 342 | r.drawImage(cached, x, y); 343 | 344 | x += cached.width + s; 345 | } 346 | } 347 | 348 | var cachedTexts = {}; 349 | function drawCachedText(r, t, x, y, s, c){ 350 | var key = t + s + c; 351 | if(!cachedTexts[key]){ 352 | cachedTexts[key] = cache(s * requiredCells(t, s), s * 5, function(r){ 353 | drawText(r, t, 0, 0, s, c); 354 | }); 355 | } 356 | r.drawImage(cachedTexts[key], x, y); 357 | } 358 | 359 | function requiredCells(t, s){ 360 | var r = 0; 361 | for(var i = 0 ; i < t.length ; i++){ 362 | r += defs[t.charAt(i)][0].length + 1; 363 | } 364 | return r - 1; 365 | } 366 | 367 | var cachedChars = {}; 368 | function cachedCharacter(t, s, c){ 369 | var key = t + s + c; 370 | if(!cachedChars[key]){ 371 | var def = defs[t]; 372 | cachedChars[key] = cache(def[0].length * s, def.length * s, function(r){ 373 | r.fillStyle = c; 374 | for(var row = 0 ; row < def.length ; row++){ 375 | for(var col = 0 ; col < def[row].length ; col++){ 376 | if(def[row][col]){ 377 | r.fillRect(col * s, row * s, s, s); 378 | } 379 | } 380 | } 381 | }); 382 | } 383 | return cachedChars[key]; 384 | } 385 | 386 | function button(t, w){ 387 | w = w || 440; 388 | return cache(w, 100, function(r){ 389 | with(r){ 390 | fillStyle = '#444'; 391 | fillRect(0, 90, w, 10); 392 | 393 | fillStyle = '#fff'; 394 | fillRect(0, 0, w, 90); 395 | 396 | drawText(r, '::' + t + '()', 100, 20, 10, '#000'); 397 | 398 | fillStyle = '#000'; 399 | beginPath(); 400 | moveTo(40, 20); 401 | lineTo(80, 45); 402 | lineTo(40, 70); 403 | fill(); 404 | } 405 | }); 406 | } 407 | -------------------------------------------------------------------------------- /src/js/graphics/halo.js: -------------------------------------------------------------------------------- 1 | function halo(s, c1, c2){ 2 | return cache(s, s, function(r){ 3 | with(r){ 4 | var g = createRadialGradient( 5 | s / 2, s / 2, 0, 6 | s / 2, s / 2, s / 2 7 | ); 8 | 9 | g.addColorStop(0, c1); 10 | g.addColorStop(1, c2); 11 | 12 | fillStyle = g; 13 | fillRect(0, 0, s, s); 14 | } 15 | }); 16 | } 17 | 18 | var whiteHalo = halo(HALO_SIZE, 'rgba(255,255,255,.25)', 'rgba(255,255,255,0)'), 19 | redHalo = halo(HALO_SIZE, 'rgba(255,0,0,.25)', 'rgba(255,0,0,0)'), 20 | darkHalo = halo(DARK_HALO_SIZE, 'rgba(0,0,0,0)', 'rgba(0,0,0,1)'); 21 | -------------------------------------------------------------------------------- /src/js/graphics/mobile-controls.js: -------------------------------------------------------------------------------- 1 | var 2 | rightArrow = cache(MOBILE_BUTTON_SIZE, MOBILE_BUTTON_SIZE, function(r){ 3 | with(r){ 4 | fillStyle = '#fff'; 5 | beginPath(); 6 | moveTo(0, 0); 7 | lineTo(MOBILE_BUTTON_SIZE, MOBILE_BUTTON_SIZE / 2); 8 | lineTo(0, MOBILE_BUTTON_SIZE); 9 | fill(); 10 | } 11 | }), 12 | leftArrow = cache(MOBILE_BUTTON_SIZE, MOBILE_BUTTON_SIZE, function(r){ 13 | with(r){ 14 | translate(MOBILE_BUTTON_SIZE, 0); 15 | scale(-1, 1); 16 | drawImage(rightArrow, 0, 0); 17 | } 18 | }), 19 | jumpArrow = cache(MOBILE_BUTTON_SIZE, MOBILE_BUTTON_SIZE, function(r){ 20 | with(r){ 21 | translate(0, MOBILE_BUTTON_SIZE); 22 | rotate(-PI / 2); 23 | 24 | drawImage(rightArrow, 0, 0); 25 | } 26 | }), 27 | grenadeButton = cache(MOBILE_BUTTON_SIZE, MOBILE_BUTTON_SIZE, function(r){ 28 | with(r){ 29 | fillStyle = '#fff'; 30 | beginPath(); 31 | arc(MOBILE_BUTTON_SIZE / 2, MOBILE_BUTTON_SIZE / 2, MOBILE_BUTTON_SIZE / 2, 0, PI * 2, true); 32 | fill(); 33 | } 34 | }) 35 | ; 36 | -------------------------------------------------------------------------------- /src/js/graphics/noise-pattern.js: -------------------------------------------------------------------------------- 1 | var noisePattern = cachePattern(NOISE_PATTERN_SIZE, NOISE_PATTERN_SIZE, function(r){ 2 | with(r){ 3 | fillStyle = '#000'; 4 | fillRect(0, 0, NOISE_PATTERN_SIZE, NOISE_PATTERN_SIZE); 5 | 6 | fillStyle = '#fff'; 7 | 8 | for(var x = 0 ; x < NOISE_PATTERN_SIZE ; x += NOISE_PIXEL_SIZE){ 9 | for(var y = 0 ; y < NOISE_PATTERN_SIZE ; y += NOISE_PIXEL_SIZE){ 10 | globalAlpha = rand(); 11 | fillRect(x, y, NOISE_PIXEL_SIZE, NOISE_PIXEL_SIZE); 12 | } 13 | } 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /src/js/graphics/particle.js: -------------------------------------------------------------------------------- 1 | function particle(s, c, as, numeric){ 2 | var p, n = pick([0, 1]); 3 | 4 | // Add to the list of particles 5 | G.add(p = { 6 | s: s, 7 | c: c, 8 | render: function(){ 9 | if(!V.contains(this.x, this.y, this.s)){ 10 | return; 11 | } 12 | 13 | R.fillStyle = p.c; 14 | if(numeric){ 15 | fillText(n.toString(), p.x, p.y); 16 | }else{ 17 | fillRect(p.x - p.s / 2, p.y - p.s / 2, p.s, p.s); 18 | } 19 | } 20 | }, RENDERABLE); 21 | 22 | // Interpolations 23 | as.forEach(function(a, id){ 24 | var args = [p].concat(a); 25 | 26 | // Add the remove callback 27 | if(!id){ 28 | args[7] = function(){ 29 | G.remove(p); 30 | }; 31 | } 32 | 33 | // Apply the interpolation 34 | interp.apply(0, args); 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /src/js/graphics/show-tiles-animation.js: -------------------------------------------------------------------------------- 1 | function surroudingTiles(f){ 2 | var cameraRightX = V.x + CANVAS_WIDTH, 3 | cameraBottomY = V.y + CANVAS_HEIGHT; 4 | 5 | for(var row = ~~(V.y / TILE_SIZE) ; row < ~~(cameraBottomY / TILE_SIZE) + 1 ; row++){ 6 | for(var col = ~~(V.x / TILE_SIZE) ; col < ~~(cameraRightX / TILE_SIZE) + 1 ; col++){ 7 | if(W.tiles[row] && W.tiles[row][col]){ 8 | f(W.tiles[row][col]); 9 | } 10 | } 11 | } 12 | } 13 | 14 | function showTilesAnimation(){ 15 | G.hideTiles = false; 16 | 17 | surroudingTiles(function(t){ 18 | var r = dist(t.center, P); 19 | t.sizeScale = 0.5; 20 | interp(t, 'sizeScale', 0, 1, r / CANVAS_WIDTH, 0, easeOutBounce); 21 | }); 22 | } 23 | 24 | function hideTilesAnimation(){ 25 | G.hideTiles = false; 26 | 27 | surroudingTiles(function(t){ 28 | var r = dist(t.center, P); 29 | t.sizeScale = 0.5; 30 | interp(t, 'sizeScale', 1, 0, r / CANVAS_WIDTH, 0, easeOutBounce); 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /src/js/graphics/spawn-animation.js: -------------------------------------------------------------------------------- 1 | function SpawnAnimation(){ 2 | this.alpha = 1; 3 | this.radius = 400; 4 | 5 | this.render = function(){ 6 | R.globalAlpha = this.alpha; 7 | R.fillStyle = '#fff'; 8 | beginPath(); 9 | arc(P.x, P.y, this.radius, 0, PI * 2, true); 10 | fill(); 11 | R.globalAlpha = 1; 12 | }; 13 | 14 | var a = this; 15 | 16 | interp(this, 'radius', 320, 0, 0.4, 1); 17 | interp(this, 'alpha', 0, 1, 0.4, 1, null, function(){ 18 | P.visible = true; 19 | 20 | for(var i = 0 ; i < 50 ; i++){ 21 | var t = rand(0.5, 1.5), 22 | a = rand(-PI, PI), 23 | l = rand(8, 80), 24 | x = cos(a) * l + P.x, 25 | y = sin(a) * l + P.y - 40; 26 | 27 | particle(4, '#fff', [ 28 | ['x', x, x, t, 0, oscillate], 29 | ['y', y, y + rand(80, 240), t, 0], 30 | ['s', rand(8, 16), 0, t] 31 | ], true); 32 | } 33 | }); 34 | 35 | P.visible = P.controllable = false; 36 | P.talking = true; 37 | G.hideTiles = true; 38 | 39 | var tUnlock = 500; 40 | if(!G.currentLevel){ 41 | delayed(function(){ 42 | P.say([ 43 | nomangle('Hello there!'), 44 | nomangle('This code is falling apart!'), 45 | nomangle('Let\'s fix the glitches before it\'s too late!') 46 | ]); 47 | }, 2000); 48 | tUnlock = 9000; 49 | } 50 | 51 | delayed(function(){ 52 | spawnSound.play(); 53 | }, 500); 54 | 55 | delayed(function(){ 56 | P.talking = false; 57 | P.controllable = true; 58 | showTilesAnimation(); 59 | }, tUnlock); 60 | } 61 | -------------------------------------------------------------------------------- /src/js/item/grenade-item.js: -------------------------------------------------------------------------------- 1 | function GrenadeItem(x, y){ 2 | Item.call(this, x, y, GRENADE); 3 | 4 | this.renderItem = function(){ 5 | R.fillStyle = 'red'; 6 | rotate(PI / 4); 7 | fillRect(-GRENADE_RADIUS, -GRENADE_RADIUS, GRENADE_RADIUS_2, GRENADE_RADIUS_2); 8 | }; 9 | 10 | this.pickup = function(){ 11 | P.grenades++; 12 | 13 | P.say([pick([ 14 | nomangle('Here\'s a breakpoint!'), 15 | nomangle('You found a breakpoint!'), 16 | nomangle('That\'s a breakpoint!') 17 | ]), G.touch ? nomangle('Hold the circle button to throw it') : nomangle('Press SPACE to throw it')]); 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /src/js/item/health-item.js: -------------------------------------------------------------------------------- 1 | function HealthItem(x, y){ 2 | Item.call(this, x, y, HEALTH); 3 | 4 | this.renderItem = function(){ 5 | var o = -requiredCells('!', 5) * 5 / 2; 6 | drawText(R, '!', o, o, 5, '#f00'); 7 | }; 8 | 9 | this.pickup = function(){ 10 | P.health++; 11 | P.say(nomangle('health++')); // TODO more strings 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /src/js/item/item.js: -------------------------------------------------------------------------------- 1 | function Item(x, y, type){ 2 | this.x = x; 3 | this.y = y; 4 | this.type = type; 5 | 6 | this.render = function(){ 7 | if(!V.contains(this.x, this.y, TILE_SIZE)){ 8 | return; 9 | } 10 | 11 | save(); 12 | translate(x, y); 13 | 14 | if(!shittyMode){ 15 | drawImage(whiteHalo, -HALO_SIZE_HALF, -HALO_SIZE_HALF); 16 | } 17 | 18 | var arrowOffsetY = sin(G.t * PI * 2 * 0.5) * 10 + ITEM_ARROW_Y_OFFSET; 19 | 20 | // Arrow 21 | R.fillStyle = '#fff'; 22 | beginPath(); 23 | moveTo(-ARROW_SIZE / 2, -ARROW_SIZE / 2 + arrowOffsetY); 24 | lineTo(ARROW_SIZE / 2, -ARROW_SIZE / 2 + arrowOffsetY); 25 | lineTo(0, arrowOffsetY); 26 | fill(); 27 | 28 | this.renderItem(); // defined in subclasses 29 | 30 | restore(); 31 | }; 32 | 33 | this.cycle = function(){ 34 | if(dist(this, P) < ITEM_PICKUP_RADIUS && !this.pickedUp){ 35 | G.remove(this); 36 | 37 | this.particles(); 38 | 39 | this.pickedUp = true; 40 | pickupSound.play(); 41 | 42 | this.pickup(); // defined in subclasses 43 | } 44 | }; 45 | 46 | this.particles = function(){ 47 | for(var i = 0 ; i < 10 ; i++){ 48 | var x = rand(this.x - TILE_SIZE / 4, this.x + TILE_SIZE / 4), 49 | y = rand(this.y - TILE_SIZE / 4, this.y + TILE_SIZE / 4), 50 | d = rand(0.2, 0.5); 51 | particle(3, '#fff', [ 52 | ['x', x, x, 0.5], 53 | ['y', y, y - rand(40, 80), 0.5], 54 | ['s', 12, 0, 0.5] 55 | ]); 56 | } 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /src/js/main.js: -------------------------------------------------------------------------------- 1 | onload = function(){ 2 | C = D.querySelector('canvas'); 3 | C.width = CANVAS_WIDTH; 4 | C.height = CANVAS_HEIGHT; 5 | 6 | R = C.getContext('2d'); 7 | 8 | // Shortcut for all canvas methods 9 | var p = CanvasRenderingContext2D.prototype; 10 | Object.getOwnPropertyNames(p).forEach(function(n){ 11 | if(R[n] && R[n].call){ 12 | w[n] = p[n].bind(R); 13 | } 14 | }); 15 | 16 | onresize(); 17 | 18 | new Game(); 19 | }; 20 | -------------------------------------------------------------------------------- /src/js/menu/game-over.js: -------------------------------------------------------------------------------- 1 | function GameOverMenu(reason){ 2 | Menu.call(this); 3 | 4 | var ss = [ 5 | [nomangle('critical'), nomangle('mental health')], 6 | [nomangle('time'), nomangle('expired')], 7 | [nomangle('code fixed'), '!!!'] 8 | ][reason]; 9 | 10 | var t = formatTime(G.totalTime); 11 | 12 | this.button(button(nomangle('retry')), 0, 420, G.newGame); 13 | this.button(button(nomangle('back')), 0, 560, G.mainMenu); 14 | this.button(button(nomangle('share')), 0, 700, function(){ 15 | open(nomangle('//twitter.com/intent/tweet?') + 16 | nomangle('hashtags=js13k') + 17 | nomangle('&url=') + encodeURIComponent(nomangle('http://js13kgames.com/entries/glitchbuster')) + 18 | nomangle('&text=') + encodeURIComponent( 19 | (reason == GAME_OVER_SUCCESS ? nomangle('I fixed all glitches in ') + t : nomangle('I fixed ') + (G.currentLevel - 1) + nomangle('/13 glitches')) + nomangle(' on Glitchbuster!') 20 | ) 21 | ); 22 | }); 23 | 24 | /*var b; 25 | this.button(button(nomangleing('foo')), 0, 700, function(){ 26 | this.d = button((b = !b) ? 'bar' : 'foo'); 27 | });*/ 28 | 29 | this.animateButtons(); 30 | 31 | ss.push(reason == GAME_OVER_SUCCESS ? nomangle('time: ') + t : nomangle('fixed ') + (G.currentLevel - 1) + '/13'); 32 | 33 | var s1 = ss[0], 34 | t1 = 10, 35 | w1 = requiredCells(s1) * t1, 36 | s2 = ss[1], 37 | t2 = 10, 38 | w2 = requiredCells(s2) * t2, 39 | s3 = ss[2], 40 | t3 = 5, 41 | w3 = requiredCells(s3) * t3; 42 | 43 | this.button(cache(w1, t1 * 5 + 5, function(r){ 44 | drawText(r, s1, 0, 5, t1, '#444'); 45 | drawText(r, s1, 0, 0, t1, '#fff'); 46 | }), (CANVAS_WIDTH - w1) / 2, 120); 47 | 48 | this.button(cache(w2, t2 * 5 + 5, function(r){ 49 | drawText(r, s2, 0, 5, t2, '#444'); 50 | drawText(r, s2, 0, 0, t2, '#fff'); 51 | }), (CANVAS_WIDTH - w2) / 2, 200); 52 | 53 | this.button(cache(w3, t3 * 5 + 5, function(r){ 54 | drawText(r, s3, 0, 5, t3, '#444'); 55 | drawText(r, s3, 0, 0, t3, '#fff'); 56 | }), (CANVAS_WIDTH - w3) / 2, 280); 57 | } 58 | -------------------------------------------------------------------------------- /src/js/menu/main.js: -------------------------------------------------------------------------------- 1 | function MainMenu(){ 2 | Menu.call(this); 3 | 4 | this.button(button(nomangle('learn')), 0, 420, G.tutorial); 5 | this.button(button(nomangle('start')), 0, 560, G.newGame); 6 | this.button(button(nomangle('whois')), 0, 700, function(){ 7 | open(nomangle('//goo.gl/QRxjGP')); 8 | }); 9 | 10 | this.animateButtons(); 11 | 12 | var titleX = (CANVAS_WIDTH - 460) / 2; 13 | this.button(cache(460, 230, function(r){ 14 | drawText(r, 'glitch', 0, 10, 20, '#444'); 15 | drawText(r, 'glitch', 0, 0, 20, '#fff'); 16 | 17 | drawText(r, 'buster', 0, 130, 20, '#444'); 18 | drawText(r, 'buster', 0, 120, 20, '#fff'); 19 | }), titleX, 90); 20 | 21 | interp(this.buttons[this.buttons.length - 1], 'o', 0, 1, 0.25, 0.5); 22 | } 23 | -------------------------------------------------------------------------------- /src/js/menu/menu.js: -------------------------------------------------------------------------------- 1 | function Menu(){ 2 | this.buttons = []; 3 | 4 | this.alpha = 1; 5 | 6 | this.button = function(d, x, y, a){ 7 | this.buttons.push({ 8 | d: d, // drawable 9 | x: x, 10 | y: y, 11 | a: a, // action 12 | o: 1 // opacity 13 | }); 14 | }; 15 | 16 | this.click = function(x, y){ 17 | if(this.alpha == 1){ 18 | this.buttons.forEach(function(b){ 19 | if(x > b.x && y > b.y && x < b.x + b.d.width && y < b.y + b.d.height){ 20 | menuSound.play(); 21 | b.a.call(b); 22 | } 23 | }); 24 | } 25 | }; 26 | 27 | this.render = function(){ 28 | R.globalAlpha = this.alpha; 29 | 30 | R.fillStyle = codePattern; 31 | fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); 32 | 33 | var a = this.alpha; 34 | this.buttons.forEach(function(b){ 35 | R.globalAlpha = a * b.o; 36 | drawImage(b.d, b.x, b.y); 37 | }); 38 | 39 | R.globalAlpha = 1; 40 | }; 41 | 42 | this.animateButtons = function(){ 43 | this.buttons.forEach(function(b, i){ 44 | interp(b, 'x', -b.d.width, 0, 0.25, i * 0.25 + 0.5); 45 | }); 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /src/js/menu/mode.js: -------------------------------------------------------------------------------- 1 | function ModeMenu(){ 2 | Menu.call(this); 3 | 4 | this.button(button(nomangle('high'), 500), 0, 420, function(){ 5 | shittyMode = false; // need to switch from undefined 6 | G.mainMenu(); 7 | }); 8 | this.button(button(nomangle('low'), 500), 0, 560, function(){ 9 | G.setResolution(0.5); 10 | shittyMode = true; 11 | G.mainMenu(); 12 | }); 13 | 14 | this.animateButtons(); 15 | 16 | var titleX = (CANVAS_WIDTH - 270) / 2; 17 | this.button(cache(270, 55, function(r){ 18 | drawText(r, nomangle('quality'), 0, 5, 10, '#444'); 19 | drawText(r, nomangle('quality'), 0, 0, 10, '#fff'); 20 | }), titleX, titleX); 21 | } 22 | -------------------------------------------------------------------------------- /src/js/sound/jsfxr.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SfxrParams 3 | * 4 | * Copyright 2010 Thomas Vian 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | * @author Thomas Vian 19 | */ 20 | /** @constructor */ 21 | function SfxrParams() { 22 | //-------------------------------------------------------------------------- 23 | // 24 | // Settings String Methods 25 | // 26 | //-------------------------------------------------------------------------- 27 | 28 | /** 29 | * Parses a settings array into the parameters 30 | * @param array Array of the settings values, where elements 0 - 23 are 31 | * a: waveType 32 | * b: attackTime 33 | * c: sustainTime 34 | * d: sustainPunch 35 | * e: decayTime 36 | * f: startFrequency 37 | * g: minFrequency 38 | * h: slide 39 | * i: deltaSlide 40 | * j: vibratoDepth 41 | * k: vibratoSpeed 42 | * l: changeAmount 43 | * m: changeSpeed 44 | * n: squareDuty 45 | * o: dutySweep 46 | * p: repeatSpeed 47 | * q: phaserOffset 48 | * r: phaserSweep 49 | * s: lpFilterCutoff 50 | * t: lpFilterCutoffSweep 51 | * u: lpFilterResonance 52 | * v: hpFilterCutoff 53 | * w: hpFilterCutoffSweep 54 | * x: masterVolume 55 | * @return If the string successfully parsed 56 | */ 57 | this.setSettings = function(values){ 58 | for(var i = 0 ; i < 24 ; i++){ 59 | this[String.fromCharCode(97 + i)] = values[i] || 0; 60 | } 61 | 62 | // I moved this here from the reset(true) function 63 | if (this.c < 0.01) { 64 | this.c = 0.01; 65 | } 66 | 67 | var totalTime = this.b + this.c + this.e; 68 | if (totalTime < 0.18) { 69 | var multiplier = 0.18 / totalTime; 70 | this.b *= multiplier; 71 | this.c *= multiplier; 72 | this.e *= multiplier; 73 | } 74 | }; 75 | } 76 | 77 | /** 78 | * SfxrSynth 79 | * 80 | * Copyright 2010 Thomas Vian 81 | * 82 | * Licensed under the Apache License, Version 2.0 (the "License"); 83 | * you may not use this file except in compliance with the License. 84 | * You may obtain a copy of the License at 85 | * 86 | * http://www.apache.org/licenses/LICENSE-2.0 87 | * 88 | * Unless required by applicable law or agreed to in writing, software 89 | * distributed under the License is distributed on an "AS IS" BASIS, 90 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 91 | * See the License for the specific language governing permissions and 92 | * limitations under the License. 93 | * 94 | * @author Thomas Vian 95 | */ 96 | /** @constructor */ 97 | function SfxrSynth() { 98 | // All variables are kept alive through function closures 99 | 100 | //-------------------------------------------------------------------------- 101 | // 102 | // Sound Parameters 103 | // 104 | //-------------------------------------------------------------------------- 105 | 106 | this._params = new SfxrParams(); // Params instance 107 | 108 | //-------------------------------------------------------------------------- 109 | // 110 | // Synth Variables 111 | // 112 | //-------------------------------------------------------------------------- 113 | 114 | var _envelopeLength0, // Length of the attack stage 115 | _envelopeLength1, // Length of the sustain stage 116 | _envelopeLength2, // Length of the decay stage 117 | 118 | _period, // Period of the wave 119 | _maxPeriod, // Maximum period before sound stops (from minFrequency) 120 | 121 | _slide, // Note slide 122 | _deltaSlide, // Change in slide 123 | 124 | _changeAmount, // Amount to change the note by 125 | _changeTime, // Counter for the note change 126 | _changeLimit, // Once the time reaches this limit, the note changes 127 | 128 | _squareDuty, // Offset of center switching point in the square wave 129 | _dutySweep; // Amount to change the duty by 130 | 131 | //-------------------------------------------------------------------------- 132 | // 133 | // Synth Methods 134 | // 135 | //-------------------------------------------------------------------------- 136 | 137 | /** 138 | * Resets the runing variables from the params 139 | * Used once at the start (total reset) and for the repeat effect (partial reset) 140 | */ 141 | this.resetManglable = function() { 142 | // Shorter reference 143 | var p = this._params; 144 | 145 | _period = 100 / (p.f * p.f + 0.001); 146 | _maxPeriod = 100 / (p.g * p.g + 0.001); 147 | 148 | _slide = 1 - p.h * p.h * p.h * 0.01; 149 | _deltaSlide = -p.i * p.i * p.i * 0.000001; 150 | 151 | if(!p.a){ 152 | _squareDuty = 0.5 - p.n / 2; 153 | _dutySweep = -p.o * 0.00005; 154 | } 155 | 156 | _changeAmount = 1 + p.l * p.l * (p.l > 0 ? -0.9 : 10); 157 | _changeTime = 0; 158 | _changeLimit = p.m == 1 ? 0 : (1 - p.m) * (1 - p.m) * 20000 + 32; 159 | }; 160 | 161 | // I split the reset() function into two functions for better readability 162 | this.totalReset = function() { 163 | this.resetManglable(); 164 | 165 | // Shorter reference 166 | var p = this._params; 167 | 168 | // Calculating the length is all that remained here, everything else moved somewhere 169 | _envelopeLength0 = p.b * p.b * 100000; 170 | _envelopeLength1 = p.c * p.c * 100000; 171 | _envelopeLength2 = p.e * p.e * 100000 + 12; 172 | // Full length of the volume envelop (and therefore sound) 173 | // Make sure the length can be divided by 3 so we will not need the padding "==" after base64 encode 174 | return ((_envelopeLength0 + _envelopeLength1 + _envelopeLength2) / 3 | 0) * 3; 175 | }; 176 | 177 | /** 178 | * Writes the wave to the supplied buffer ByteArray 179 | * @param buffer A ByteArray to write the wave to 180 | * @return If the wave is finished 181 | */ 182 | this.synthWave = function(buffer, length) { 183 | // Shorter reference 184 | var p = this._params; 185 | 186 | // If the filters are active 187 | var _filters = p.s != 1 || p.v, 188 | // Cutoff multiplier which adjusts the amount the wave position can move 189 | _hpFilterCutoff = p.v * p.v * 0.1, 190 | 191 | // Speed of the high-pass cutoff multiplier 192 | _hpFilterDeltaCutoff = 1 + p.w * 0.0003, 193 | 194 | // Cutoff multiplier which adjusts the amount the wave position can move 195 | _lpFilterCutoff = p.s * p.s * p.s * 0.1, 196 | 197 | // Speed of the low-pass cutoff multiplier 198 | _lpFilterDeltaCutoff = 1 + p.t * 0.0001, 199 | 200 | // If the low pass filter is active 201 | _lpFilterOn = p.s != 1, 202 | 203 | // masterVolume * masterVolume (for quick calculations) 204 | _masterVolume = p.x * p.x, 205 | 206 | // Minimum frequency before stopping 207 | _minFreqency = p.g, 208 | 209 | // If the phaser is active 210 | _phaser = p.q || p.r, 211 | 212 | // Change in phase offset 213 | _phaserDeltaOffset = p.r * p.r * p.r * 0.2, 214 | 215 | // Phase offset for phaser effect 216 | _phaserOffset = p.q * p.q * (p.q < 0 ? -1020 : 1020), 217 | 218 | // Once the time reaches this limit, some of the iables are reset 219 | _repeatLimit = p.p ? ((1 - p.p) * (1 - p.p) * 20000 | 0) + 32 : 0, 220 | 221 | // The punch factor (louder at begining of sustain) 222 | _sustainPunch = p.d, 223 | 224 | // Amount to change the period of the wave by at the peak of the vibrato wave 225 | _vibratoAmplitude = p.j / 2, 226 | 227 | // Speed at which the vibrato phase moves 228 | _vibratoSpeed = p.k * p.k * 0.01, 229 | 230 | // The type of wave to generate 231 | _waveType = p.a; 232 | 233 | var _envelopeLength = _envelopeLength0, // Length of the current envelope stage 234 | _envelopeOverLength0 = 1 / _envelopeLength0, // (for quick calculations) 235 | _envelopeOverLength1 = 1 / _envelopeLength1, // (for quick calculations) 236 | _envelopeOverLength2 = 1 / _envelopeLength2; // (for quick calculations) 237 | 238 | // Damping muliplier which restricts how fast the wave position can move 239 | var _lpFilterDamping = 5 / (1 + p.u * p.u * 20) * (0.01 + _lpFilterCutoff); 240 | if (_lpFilterDamping > 0.8) { 241 | _lpFilterDamping = 0.8; 242 | } 243 | _lpFilterDamping = 1 - _lpFilterDamping; 244 | 245 | var _finished = false, // If the sound has finished 246 | _envelopeStage = 0, // Current stage of the envelope (attack, sustain, decay, end) 247 | _envelopeTime = 0, // Current time through current enelope stage 248 | _envelopeVolume = 0, // Current volume of the envelope 249 | _hpFilterPos = 0, // Adjusted wave position after high-pass filter 250 | _lpFilterDeltaPos = 0, // Change in low-pass wave position, as allowed by the cutoff and damping 251 | _lpFilterOldPos, // Previous low-pass wave position 252 | _lpFilterPos = 0, // Adjusted wave position after low-pass filter 253 | _periodTemp, // Period modified by vibrato 254 | _phase = 0, // Phase through the wave 255 | _phaserInt, // Integer phaser offset, for bit maths 256 | _phaserPos = 0, // Position through the phaser buffer 257 | _pos, // Phase expresed as a Number from 0-1, used for fast sin approx 258 | _repeatTime = 0, // Counter for the repeats 259 | _sample, // Sub-sample calculated 8 times per actual sample, averaged out to get the super sample 260 | _superSample, // Actual sample writen to the wave 261 | _vibratoPhase = 0; // Phase through the vibrato sine wave 262 | 263 | // Buffer of wave values used to create the out of phase second wave 264 | var _phaserBuffer = new Array(1024), 265 | 266 | // Buffer of random values used to generate noise 267 | _noiseBuffer = new Array(32); 268 | 269 | for (var i = _phaserBuffer.length; i--; ) { 270 | _phaserBuffer[i] = 0; 271 | } 272 | for (i = _noiseBuffer.length; i--; ) { 273 | _noiseBuffer[i] = rand(-1, 1); 274 | } 275 | 276 | for (i = 0; i < length; i++) { 277 | if (_finished) { 278 | return i; 279 | } 280 | 281 | // Repeats every _repeatLimit times, partially resetting the sound parameters 282 | if (_repeatLimit) { 283 | if (++_repeatTime >= _repeatLimit) { 284 | _repeatTime = 0; 285 | this.resetManglable(); 286 | } 287 | } 288 | 289 | // If _changeLimit is reached, shifts the pitch 290 | if (_changeLimit) { 291 | if (++_changeTime >= _changeLimit) { 292 | _changeLimit = 0; 293 | _period *= _changeAmount; 294 | } 295 | } 296 | 297 | // Acccelerate and apply slide 298 | _slide += _deltaSlide; 299 | _period *= _slide; 300 | 301 | // Checks for frequency getting too low, and stops the sound if a minFrequency was set 302 | if (_period > _maxPeriod) { 303 | _period = _maxPeriod; 304 | if (_minFreqency > 0) { 305 | _finished = true; 306 | } 307 | } 308 | 309 | _periodTemp = _period; 310 | 311 | // Applies the vibrato effect 312 | if (_vibratoAmplitude > 0) { 313 | _vibratoPhase += _vibratoSpeed; 314 | _periodTemp *= 1 + sin(_vibratoPhase) * _vibratoAmplitude; 315 | } 316 | 317 | _periodTemp |= 0; 318 | if (_periodTemp < 8) { 319 | _periodTemp = 8; 320 | } 321 | 322 | // Sweeps the square duty 323 | if (!_waveType) { 324 | _squareDuty += _dutySweep; 325 | if (_squareDuty < 0) { 326 | _squareDuty = 0; 327 | } else if (_squareDuty > 0.5) { 328 | _squareDuty = 0.5; 329 | } 330 | } 331 | 332 | // Moves through the different stages of the volume envelope 333 | if (++_envelopeTime > _envelopeLength) { 334 | _envelopeTime = 0; 335 | 336 | switch (++_envelopeStage) { 337 | case 1: 338 | _envelopeLength = _envelopeLength1; 339 | break; 340 | case 2: 341 | _envelopeLength = _envelopeLength2; 342 | } 343 | } 344 | 345 | // Sets the volume based on the position in the envelope 346 | switch (_envelopeStage) { 347 | case 0: 348 | _envelopeVolume = _envelopeTime * _envelopeOverLength0; 349 | break; 350 | case 1: 351 | _envelopeVolume = 1 + (1 - _envelopeTime * _envelopeOverLength1) * 2 * _sustainPunch; 352 | break; 353 | case 2: 354 | _envelopeVolume = 1 - _envelopeTime * _envelopeOverLength2; 355 | break; 356 | case 3: 357 | _envelopeVolume = 0; 358 | _finished = true; 359 | } 360 | 361 | // Moves the phaser offset 362 | if (_phaser) { 363 | _phaserOffset += _phaserDeltaOffset; 364 | _phaserInt = _phaserOffset | 0; 365 | if (_phaserInt < 0) { 366 | _phaserInt = -_phaserInt; 367 | } else if (_phaserInt > 1023) { 368 | _phaserInt = 1023; 369 | } 370 | } 371 | 372 | // Moves the high-pass filter cutoff 373 | if (_filters && _hpFilterDeltaCutoff) { 374 | _hpFilterCutoff *= _hpFilterDeltaCutoff; 375 | if (_hpFilterCutoff < 0.00001) { 376 | _hpFilterCutoff = 0.00001; 377 | } else if (_hpFilterCutoff > 0.1) { 378 | _hpFilterCutoff = 0.1; 379 | } 380 | } 381 | 382 | _superSample = 0; 383 | for (var j = 8; j--; ) { 384 | // Cycles through the period 385 | _phase++; 386 | if (_phase >= _periodTemp) { 387 | _phase %= _periodTemp; 388 | 389 | // Generates new random noise for this period 390 | if (_waveType == 3) { 391 | for (var n = _noiseBuffer.length; n--; ) { 392 | _noiseBuffer[n] = rand(-1, 1); 393 | } 394 | } 395 | } 396 | 397 | // Gets the sample from the oscillator 398 | switch (_waveType) { 399 | case 0: // Square wave 400 | _sample = ((_phase / _periodTemp) < _squareDuty) ? 0.5 : -0.5; 401 | break; 402 | case 1: // Saw wave 403 | _sample = 1 - _phase / _periodTemp * 2; 404 | break; 405 | case 2: // Sine wave (fast and accurate approx) 406 | _pos = _phase / _periodTemp; 407 | _pos = (_pos > 0.5 ? _pos - 1 : _pos) * 6.28318531; 408 | _sample = 1.27323954 * _pos + 0.405284735 * _pos * _pos * (_pos < 0 ? 1 : -1); 409 | _sample = 0.225 * ((_sample < 0 ? -1 : 1) * _sample * _sample - _sample) + _sample; 410 | break; 411 | case 3: // Noise 412 | _sample = _noiseBuffer[abs(_phase * 32 / _periodTemp | 0)]; 413 | } 414 | 415 | // Applies the low and high pass filters 416 | if (_filters) { 417 | _lpFilterOldPos = _lpFilterPos; 418 | _lpFilterCutoff *= _lpFilterDeltaCutoff; 419 | if (_lpFilterCutoff < 0) { 420 | _lpFilterCutoff = 0; 421 | } else if (_lpFilterCutoff > 0.1) { 422 | _lpFilterCutoff = 0.1; 423 | } 424 | 425 | if (_lpFilterOn) { 426 | _lpFilterDeltaPos += (_sample - _lpFilterPos) * _lpFilterCutoff; 427 | _lpFilterDeltaPos *= _lpFilterDamping; 428 | } else { 429 | _lpFilterPos = _sample; 430 | _lpFilterDeltaPos = 0; 431 | } 432 | 433 | _lpFilterPos += _lpFilterDeltaPos; 434 | 435 | _hpFilterPos += _lpFilterPos - _lpFilterOldPos; 436 | _hpFilterPos *= 1 - _hpFilterCutoff; 437 | _sample = _hpFilterPos; 438 | } 439 | 440 | // Applies the phaser effect 441 | if (_phaser) { 442 | _phaserBuffer[_phaserPos % 1024] = _sample; 443 | _sample += _phaserBuffer[(_phaserPos - _phaserInt + 1024) % 1024]; 444 | _phaserPos++; 445 | } 446 | 447 | _superSample += _sample; 448 | } 449 | 450 | // Averages out the super samples and applies volumes 451 | _superSample *= 0.125 * _envelopeVolume * _masterVolume; 452 | 453 | // Clipping if too loud 454 | buffer[i] = _superSample >= 1 ? 32767 : _superSample <= -1 ? -32768 : _superSample * 32767 | 0; 455 | } 456 | 457 | return length; 458 | }; 459 | } 460 | 461 | // Adapted from http://codebase.es/riffwave/ 462 | var synth = new SfxrSynth(); 463 | 464 | // Export for the Closure Compiler 465 | var jsfxr = function(settings) { 466 | // Initialize SfxrParams 467 | synth._params.setSettings(settings); 468 | 469 | // Synthesize Wave 470 | var envelopeFullLength = synth.totalReset(); 471 | var data = new Uint8Array(((envelopeFullLength + 1) / 2 | 0) * 4 + 44); 472 | var used = synth.synthWave(new Uint16Array(data.buffer, 44), envelopeFullLength) * 2; 473 | var dv = new Uint32Array(data.buffer, 0, 44); 474 | 475 | // Initialize header 476 | dv[0] = 0x46464952; // "RIFF" 477 | dv[1] = used + 36; // put total size here 478 | dv[2] = 0x45564157; // "WAVE" 479 | dv[3] = 0x20746D66; // "fmt " 480 | dv[4] = 0x00000010; // size of the following 481 | dv[5] = 0x00010001; // Mono: 1 channel, PCM format 482 | dv[6] = 0x0000AC44; // 44,100 samples per second 483 | dv[7] = 0x00015888; // byte rate: two bytes per sample 484 | dv[8] = 0x00100002; // 16 bits per sample, aligned on every two bytes 485 | dv[9] = 0x61746164; // "data" 486 | dv[10] = used; // put number of samples here 487 | 488 | // Base64 encoding written by me, @maettig 489 | used += 44; 490 | var i = 0, 491 | base64Characters = nomangle('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'), 492 | output = nomangle('data:audio/wav;base64,'); 493 | for (; i < used; i += 3){ 494 | var a = data[i] << 16 | data[i + 1] << 8 | data[i + 2]; 495 | output += base64Characters[a >> 18] + base64Characters[a >> 12 & 63] + base64Characters[a >> 6 & 63] + base64Characters[a & 63]; 496 | } 497 | 498 | var audio = new Audio(); 499 | audio.src = output; 500 | return audio; 501 | }; 502 | -------------------------------------------------------------------------------- /src/js/sound/sounds.js: -------------------------------------------------------------------------------- 1 | var jumpSound = jsfxr([0,,0.1434,,0.1212,0.4471,,0.2511,,,,,,0.0426,,,,,0.8862,,,,,0.5]), 2 | hitSound = jsfxr([1,,0.0713,,0.1467,0.5483,,-0.4465,,,,,,,,,,,1,,,0.0639,,0.5]), 3 | pickupSound = jsfxr([0,,0.0224,0.441,0.1886,0.6932,,,,,,,,,,,,,1,,,,,0.5]), 4 | spawnSound = jsfxr([2,0.28,0.45,,0.56,0.35,,0.4088,,,,,0.03,0.1557,,0.5565,-0.02,-0.02,1,,,,,0.5]), 5 | explosionSound = jsfxr([3,,0.244,0.6411,0.2242,0.7416,,-0.2717,,,,0.0171,0.0346,,,,-0.0305,0.0244,1,,,0.0275,-0.0076,0.5]), 6 | menuSound = jsfxr([0,,0.1394,,0.0864,0.48,,,,,,,,0.5326,,,,,1,,,0.1,,0.5]), 7 | saySound = jsfxr([2,0.03,0.1,0.14,0.25,0.54,0.3167,-0.02,0.3999,,0.05,,,0.1021,0.0684,,0.1287,-0.1816,1,,,,,0.46]), 8 | landSound = jsfxr([3,,0.0118,0.03,0.1681,0.565,,-0.2343,,,,0.26,0.6855,,,,,,1,,,,,0.2]), 9 | fixedSound = jsfxr([0,,0.2098,,0.4725,0.3665,,0.1895,,,,,,0.0067,,0.5437,,,1,,,,,0.45]); 10 | -------------------------------------------------------------------------------- /src/js/tutorial-level.js: -------------------------------------------------------------------------------- 1 | var tutorialLevel = matrix([ 2 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 3 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 4 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 5 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], 6 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], 7 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 6, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 6, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], 8 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], 9 | [1, 0, 4, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 5, 0, 0, 0, 1], 10 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 7, 7, 1, 1, 7, 7, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] 11 | ]); 12 | -------------------------------------------------------------------------------- /src/js/util/between.js: -------------------------------------------------------------------------------- 1 | function between(a, b, c){ 2 | if(b < a) return a; 3 | if(b > c ) return c; 4 | return b; 5 | } 6 | -------------------------------------------------------------------------------- /src/js/util/cache.js: -------------------------------------------------------------------------------- 1 | function cache(w, h, f){ 2 | var c = D.createElement('canvas'); 3 | c.width = w; 4 | c.height = h; 5 | 6 | f(c.getContext('2d'), c); 7 | 8 | return c; 9 | } 10 | 11 | function cachePattern(w, h, f){ 12 | var c = cache(w, h, f); 13 | return c.getContext('2d').createPattern(c, 'repeat'); 14 | } 15 | -------------------------------------------------------------------------------- /src/js/util/dist.js: -------------------------------------------------------------------------------- 1 | // Actual distance 2 | function dist(a, b){ 3 | return sqrt(pow(a.x - b.x, 2) + pow(a.y - b.y, 2)); 4 | } 5 | -------------------------------------------------------------------------------- /src/js/util/expose-math.js: -------------------------------------------------------------------------------- 1 | // Exposing all math functions to the global scope 2 | Object.getOwnPropertyNames(Math).forEach(function(n){ 3 | if(Math[n].call){ 4 | this[n] = Math[n]; 5 | } 6 | }); 7 | -------------------------------------------------------------------------------- /src/js/util/flatten.js: -------------------------------------------------------------------------------- 1 | function flatten(m){ 2 | var flattened = []; 3 | m.forEach(function(row){ 4 | flattened = flattened.concat(row); 5 | }); 6 | return flattened; 7 | } 8 | -------------------------------------------------------------------------------- /src/js/util/format-time.js: -------------------------------------------------------------------------------- 1 | function addZeros(n, l){ 2 | n = '' + n; 3 | while(n.length < l){ 4 | n = '0' + n; 5 | } 6 | return n; 7 | } 8 | 9 | function formatTime(t, ms){ 10 | var m = ~~(t / 60), 11 | s = ~~(t % 60); 12 | 13 | return addZeros(m, 2) + ':' + addZeros(s, 2) + (ms ? '.' + addZeros(~~(t % 1 * 100), 2) : ''); 14 | } 15 | -------------------------------------------------------------------------------- /src/js/util/globals.js: -------------------------------------------------------------------------------- 1 | var D = document, 2 | w = window, 3 | delayed = setTimeout, 4 | shittyMode, // undefined by default 5 | C, // canvas 6 | R, // canvas context 7 | W, // world 8 | P, // player 9 | V, // camera 10 | PI = Math.PI, 11 | mobile = navigator.userAgent.match(nomangle(/andro|ipho|ipa|ipo|windows ph/i)), 12 | CANVAS_WIDTH = mobile ? 640 : 920, 13 | CANVAS_HEIGHT = 920; 14 | -------------------------------------------------------------------------------- /src/js/util/interp.js: -------------------------------------------------------------------------------- 1 | function linear(t, b, c, d){ 2 | return (t / d) * c + b; 3 | } 4 | 5 | function easeOutBack(t, b, c, d) { 6 | s = 1.70158; 7 | return c*((t=t/d-1)*t*((s+1)*t + s) + 1) + b; 8 | } 9 | 10 | function oscillate(t, b, c, d) { 11 | return sin((t / d) * PI * 4) * c + b; 12 | } 13 | 14 | function easeOutBounce(t, b, c, d) { 15 | if ((t /= d) < (1/2.75)) { 16 | return c * (7.5625 * t * t) + b; 17 | } 18 | if (t < (2/2.75)) { 19 | return c * (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75) + b; 20 | } 21 | if (t < (2.5/2.75)) { 22 | return c * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375) + b; 23 | } 24 | return c * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375) + b; 25 | } 26 | 27 | function interp(o, p, a, b, d, l, f, e){ 28 | var i = { 29 | o: o, // object 30 | p: p, // property 31 | a: a, // from 32 | b: b, // to 33 | d: d, // duration 34 | l: l || 0, // delay 35 | f: f || linear, // easing function 36 | e: e, // end callback 37 | t: 0, 38 | cycle: function(e){ 39 | if(i.l > 0){ 40 | i.l -= e; 41 | i.o[i.p] = i.a; 42 | }else{ 43 | i.t = min(i.d, i.t + e); 44 | i.o[i.p] = i.f(i.t, i.a, i.b - i.a, i.d); 45 | if(i.t == i.d){ 46 | if(i.e){ 47 | i.e(); 48 | } 49 | remove(G.cyclables, i); 50 | } 51 | } 52 | } 53 | }; 54 | G.add(i, CYCLABLE); 55 | } 56 | -------------------------------------------------------------------------------- /src/js/util/pad.js: -------------------------------------------------------------------------------- 1 | function pad(m, n){ 2 | var r = []; 3 | for(var row = 0 ; row < m.length + n * 2 ; row++){ 4 | r.push([]); 5 | for(var col = 0 ; col < m[0].length + n * 2 ; col++){ 6 | if(row < n || row >= m.length + n || col < n || col >= m[0].length + n){ 7 | r[row][col] = UNBREAKABLE_TILE_ID; 8 | }else{ 9 | r[row][col] = m[row - n][col - n]; 10 | } 11 | } 12 | } 13 | return r; 14 | } 15 | -------------------------------------------------------------------------------- /src/js/util/pick.js: -------------------------------------------------------------------------------- 1 | function pick(choices, results, forceArray){ 2 | choices = choices.slice(0); 3 | results = results || 1; 4 | 5 | var res = []; 6 | 7 | while(res.length < results){ 8 | res = res.concat( 9 | choices.splice(~~(random() * choices.length), 1) // returns the array of deleted elements 10 | ); 11 | } 12 | 13 | return results === 1 && !forceArray ? res[0] : res; 14 | } 15 | -------------------------------------------------------------------------------- /src/js/util/progress-string.js: -------------------------------------------------------------------------------- 1 | function progressString(c, p, n){ 2 | var s = ''; 3 | for(var i = 0 ; i < n ; i++){ 4 | s += i < p ? c : '-'; 5 | } 6 | return s; 7 | } 8 | -------------------------------------------------------------------------------- /src/js/util/proto.js: -------------------------------------------------------------------------------- 1 | function proto(o){ 2 | var r = {}; 3 | for(var i in o){ 4 | if(o[i].call){ 5 | r[i] = o[i].bind(o); 6 | } 7 | } 8 | return r; 9 | } 10 | -------------------------------------------------------------------------------- /src/js/util/rand.js: -------------------------------------------------------------------------------- 1 | function rand(a, b){ 2 | // ~~b -> 0 3 | return random() * ((a || 1) - ~~b) + ~~b; 4 | } 5 | -------------------------------------------------------------------------------- /src/js/util/remove.js: -------------------------------------------------------------------------------- 1 | // Remove an element from an array 2 | function remove(l, e){ 3 | var i = l.indexOf(e); 4 | if(i >= 0) l.splice(i, 1); 5 | } 6 | -------------------------------------------------------------------------------- /src/js/util/resize.js: -------------------------------------------------------------------------------- 1 | onresize = function(){ 2 | var mw = innerWidth, 3 | mh = innerHeight, 4 | 5 | ar = mw / mh, // available ratio 6 | br = CANVAS_WIDTH / CANVAS_HEIGHT, // base ratio 7 | w, 8 | h, 9 | s = D.querySelector('#cc').style; 10 | 11 | if(ar <= br){ 12 | w = mw; 13 | h = w / br; 14 | }else{ 15 | h = mh; 16 | w = h * br; 17 | } 18 | 19 | s.width = w + 'px'; 20 | s.height = h + 'px'; 21 | }; 22 | -------------------------------------------------------------------------------- /src/js/util/shape.js: -------------------------------------------------------------------------------- 1 | function shape(c, x){ 2 | for(var i in x){ 3 | var p = x[i].slice(1); 4 | switch(x[i][0]){ 5 | case FILLSTYLE: 6 | c.fillStyle = x[i][1]; 7 | break; 8 | case FILLRECT: 9 | c.fillRect.apply(c, p); 10 | break; 11 | case DRAWIMAGE: 12 | c.drawImage.apply(c, p); 13 | break; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/js/world/camera.js: -------------------------------------------------------------------------------- 1 | function Camera(){ 2 | // Lazy init 3 | this.realX = this.realY = this.x = this.y = 0; 4 | 5 | // Position at which the camera would ideally be 6 | this.target = function(facing){ 7 | var x, y; 8 | if(!this.targetted){ 9 | x = P.x + (P.controllable && facing ? P.facing * 50 : 0); 10 | y = P.y + (P.controllable && P.lookingDown && facing ? 400 : 0); 11 | }else{ 12 | x = this.targetted.x; 13 | y = this.targetted.y; 14 | } 15 | return { 16 | x: ~~(x - (CANVAS_WIDTH / 2)), 17 | y: ~~(y - (CANVAS_HEIGHT / 2)) 18 | }; 19 | }; 20 | 21 | // Instantly moves the camera to the position where it's supposed to be 22 | this.forceCenter = function(e){ 23 | var t = this.target(); 24 | this.realX = this.x = t.x; 25 | this.realY = this.y = t.y; 26 | }; 27 | 28 | this.contains = function(x, y, d){ 29 | return x + d > this.x && 30 | y + d > this.y && 31 | x - d < this.x + CANVAS_WIDTH && 32 | y - d < this.y + CANVAS_HEIGHT; 33 | }; 34 | 35 | this.cycle = function(e){ 36 | var target = this.target(true), 37 | d = dist(target, this), 38 | speed = max(1, d / 0.2), 39 | angle = atan2(target.y - this.realY, target.x - this.realX), 40 | appliedDist = min(speed * e, d); 41 | 42 | var px = 1 / G.resolution; 43 | 44 | if(d > px){ 45 | this.realX += cos(angle) * appliedDist; 46 | this.realY += sin(angle) * appliedDist; 47 | } 48 | 49 | this.x = ~~(this.realX / px) * px; 50 | this.y = ~~(this.realY / px) * px; 51 | }; 52 | } 53 | -------------------------------------------------------------------------------- /src/js/world/generate-world.js: -------------------------------------------------------------------------------- 1 | function pickMask(masks, requirements){ 2 | return pick(masks.filter(function(m){ 3 | return m.exits == requirements; 4 | })); 5 | } 6 | 7 | 8 | function generateWorld(id){ 9 | if(!id){ 10 | return pad(tutorialLevel, WORLD_PADDING); 11 | } 12 | 13 | // Mirror all the masks to have more possibilities 14 | var usedMasks = masks.concat(masks.map(mirrorMask)); 15 | 16 | var maskMapRows = id < 0 ? 4 : round((id - 1) * 0.4 + 2), 17 | maskMapCols = id < 0 ? 5 : round((id - 1) * 0.2 + 3), 18 | maskMap = [], 19 | col, 20 | row, 21 | downCols = [], 22 | cols = []; 23 | 24 | for(col = 0 ; col < maskMapCols ; col++){ 25 | cols.push(col); 26 | } 27 | 28 | for(row = 0 ; row < maskMapRows ; row++){ 29 | maskMap.push([]); 30 | 31 | for(col = 0 ; col < maskMapCols ; col++){ 32 | maskMap[row][col] = 0; 33 | 34 | // The tile above was going down, need to ensure there's a to this one 35 | if(downCols.indexOf(col) >= 0){ 36 | maskMap[row][col] |= UP; 37 | } 38 | 39 | // Need to connect left if we're not on the far left 40 | if(col > 0){ 41 | maskMap[row][col] |= LEFT; 42 | } 43 | 44 | // Need to connect right if we're not on the far right 45 | if(col < maskMapCols - 1){ 46 | maskMap[row][col] |= RIGHT; 47 | } 48 | } 49 | 50 | // Generate the link to the lower row 51 | if(row < maskMapRows - 1){ 52 | downCols = pick(cols, pick([1, 2, 3]), true); 53 | downCols.forEach(function(col){ 54 | maskMap[row][col] |= DOWN; 55 | }); 56 | } 57 | } 58 | 59 | var matrix = []; 60 | for(row = 0 ; row < maskMapRows * MASK_ROWS ; row++){ 61 | matrix[row] = []; 62 | } 63 | 64 | function applyMask(matrix, mask, rowStart, colStart){ 65 | for(var row = 0 ; row < MASK_ROWS ; row++){ 66 | for(var col = 0 ; col < MASK_COLS ; col++){ 67 | matrix[row + rowStart][col + colStart] = mask[row][col]; 68 | } 69 | } 70 | } 71 | 72 | for(row = 0 ; row < maskMapRows ; row++){ 73 | for(col = 0 ; col < maskMapCols ; col++){ 74 | 75 | var mask = pickMask(usedMasks, maskMap[row][col]).mask; 76 | 77 | // Apply mask 78 | applyMask(matrix, mask, row * MASK_ROWS, col * MASK_COLS); 79 | } 80 | } 81 | 82 | var finalMatrix = [], 83 | floors = [], 84 | ceilings = [], 85 | floorsMap = []; 86 | 87 | for(row = 0 ; row < matrix.length ; row++){ 88 | finalMatrix.push([]); 89 | floorsMap.push([]); 90 | 91 | matrix[row][col] = parseInt(matrix[row][col]); 92 | 93 | for(col = 0 ; col < matrix[row].length ; col++){ 94 | finalMatrix[row].push(matrix[row][col]); 95 | 96 | // Probabilistic wall, let's decide now 97 | if(matrix[row][col] == PROBABLE_TILE_ID){ 98 | finalMatrix[row][col] = rand() < PROBABLE_TILE_PROBABILITY ? TILE_ID : VOID_ID; 99 | } 100 | 101 | // Detect floors and ceilings to add spikes, spawn and exit 102 | if(row > 0){ 103 | if(finalMatrix[row][col] == TILE_ID && finalMatrix[row - 1][col] == VOID_ID){ 104 | var f = [row, col]; 105 | floors.push(f); 106 | floorsMap[row].push(f); 107 | } 108 | 109 | if(finalMatrix[row][col] == VOID_ID && finalMatrix[row - 1][col] == TILE_ID){ 110 | ceilings.push([row - 1, col]); 111 | } 112 | } 113 | } 114 | } 115 | 116 | // Add a random spawn and a random exit 117 | var spawn = pick(flatten(floorsMap.slice(0, MASK_ROWS))), 118 | exit = pick(flatten(floorsMap.slice(finalMatrix.length - MASK_ROWS * 0.6))); 119 | 120 | finalMatrix[spawn[0] - 1][spawn[1]] = SPAWN_ID; 121 | finalMatrix[exit[0] - 1][exit[1]] = EXIT_ID; 122 | finalMatrix[exit[0]][exit[1]] = UNBREAKABLE_TILE_ID; 123 | 124 | // Add random spikes 125 | floors.forEach(function(f){ 126 | if(f != exit && f != spawn && rand() < SPIKE_DENSITY){ 127 | finalMatrix[f[0]][f[1]] = FLOOR_SPIKE_ID; 128 | } 129 | }); 130 | 131 | ceilings.forEach(function(c){ 132 | if(c != exit && c != spawn && rand() < SPIKE_DENSITY){ 133 | finalMatrix[c[0]][c[1]] = CEILING_SPIKE_ID; 134 | } 135 | }); 136 | 137 | return pad(finalMatrix, WORLD_PADDING); 138 | } 139 | -------------------------------------------------------------------------------- /src/js/world/grenade.js: -------------------------------------------------------------------------------- 1 | function Grenade(x, y, angle, force, simulated){ 2 | this.x = x; 3 | this.y = y; 4 | this.timer = 2; 5 | this.rotation = 0; 6 | 7 | this.vX = cos(angle) * force; 8 | this.vY = sin(angle) * force; 9 | 10 | this.cycle = function(e){ 11 | var before = { 12 | x: this.x, 13 | y: this.y 14 | }; 15 | 16 | if(!this.stuck || this.stuck.destroyed){ 17 | this.stuck = null; 18 | 19 | this.vY += e * GRAVITY * 0.5; 20 | 21 | this.x += this.vX * e; 22 | this.y += this.vY * e; 23 | 24 | this.rotation += PI * 4 * e; 25 | 26 | var after = { 27 | x: this.x, 28 | y: this.y 29 | }; 30 | 31 | // Trail 32 | if(!shittyMode && !simulated){ 33 | var t = { 34 | alpha: 1, 35 | render: function(){ 36 | R.strokeStyle = 'rgba(255, 0, 0, ' + this.alpha + ')'; 37 | R.lineWidth = 8; 38 | beginPath(); 39 | moveTo(before.x, before.y); 40 | lineTo(after.x, after.y); 41 | stroke(); 42 | } 43 | }; 44 | G.add(t, RENDERABLE); 45 | 46 | interp(t, 'alpha', 1, 0, 0.3, 0, null, function(){ 47 | G.remove(t); 48 | }); 49 | } 50 | } 51 | 52 | // Explosion 53 | if(!simulated){ 54 | this.timer -= e; 55 | if(this.timer <= 0){ 56 | this.explode(); 57 | }else{ 58 | for(var i in G.killables){ 59 | if(G.killables[i] != P && dist(G.killables[i], this) < CHARACTER_WIDTH / 2){ 60 | return this.explode(); // no need to do the rest 61 | } 62 | } 63 | } 64 | } 65 | 66 | var tile = W.tileAt(this.x, this.y); 67 | if(tile && !this.stuck){ 68 | this.vX *= GRENADE_BOUNCE_FACTOR; 69 | this.vY *= GRENADE_BOUNCE_FACTOR; 70 | 71 | var iterations = 0, 72 | adjustments; 73 | do{ 74 | adjustments = tile.pushAway(this, GRENADE_RADIUS_2, GRENADE_RADIUS_2); 75 | 76 | if(simulated){ 77 | this.stuck |= adjustments; 78 | } 79 | 80 | if(adjustments & UP){ 81 | this.vY = -abs(this.vY); 82 | } 83 | if(adjustments & DOWN){ 84 | this.vY = abs(this.vY); 85 | } 86 | if(adjustments & LEFT){ 87 | this.vX = -abs(this.vX); 88 | } 89 | if(adjustments & RIGHT){ 90 | this.vX = abs(this.vX); 91 | } 92 | 93 | if(max(abs(this.vX), abs(this.vY)) < 150){ 94 | this.stuck = tile; 95 | this.vX = this.vY = 0; 96 | }else{ 97 | // Particle when bouncing 98 | if(adjustments && !shittyMode && !simulated){ 99 | for(var i = 0 ; i < 2 ; i++){ 100 | var x = this.x + rand(-8, 8), 101 | y = this.y + rand(-8, 8), 102 | d = rand(0.2, 0.5); 103 | particle(3, '#fff', [ 104 | ['x', x, x, d], 105 | ['y', y, y - rand(40, 80), d], 106 | ['s', 12, 0, d] 107 | ]); 108 | } 109 | } 110 | } 111 | }while(adjustments && iterations++ < 5); 112 | } 113 | }; 114 | 115 | this.explode = function(){ 116 | if(this.exploded){ 117 | return; 118 | } 119 | 120 | this.exploded = true; 121 | 122 | [ 123 | [this.x - TILE_SIZE, this.y + TILE_SIZE], 124 | [this.x, this.y + TILE_SIZE], 125 | [this.x + TILE_SIZE, this.y + TILE_SIZE], 126 | [this.x - TILE_SIZE, this.y], 127 | [this.x, this.y], 128 | [this.x + TILE_SIZE, this.y], 129 | [this.x - TILE_SIZE, this.y - TILE_SIZE], 130 | [this.x, this.y - TILE_SIZE], 131 | [this.x + TILE_SIZE, this.y - TILE_SIZE] 132 | ].forEach(function(p){ 133 | W.destroyTileAt(p[0], p[1]); 134 | }); 135 | 136 | for(var i = 0 ; i < 40 ; i++){ 137 | var d = rand(0.5, 1.5), 138 | x = rand(-TILE_SIZE, TILE_SIZE) + this.x, 139 | y = rand(-TILE_SIZE, TILE_SIZE) + this.y; 140 | 141 | particle(3, pick([ 142 | '#f00', 143 | '#f80', 144 | '#ff0' 145 | ]), [ 146 | ['x', x, x + 8, d, 0, oscillate], 147 | ['y', y, y - rand(80, 240), d, 0], 148 | ['s', rand(24, 40), 0, d] 149 | ]); 150 | } 151 | 152 | for(i = G.killables.length ; --i >= 0 ;){ 153 | if(dist(this, G.killables[i]) < TILE_SIZE * 2){ 154 | G.killables[i].hurt(this, 3); 155 | } 156 | } 157 | 158 | G.remove(this); 159 | 160 | var m = this; 161 | delayed(function(){ 162 | if(V.targetted == m){ 163 | V.targetted = null; 164 | } 165 | }, 1000); 166 | 167 | explosionSound.play(); 168 | }; 169 | 170 | this.render = function(){ 171 | save(); 172 | translate(this.x, this.y); 173 | rotate(this.rotation); 174 | R.fillStyle = 'red'; 175 | fillRect(-GRENADE_RADIUS, -GRENADE_RADIUS, GRENADE_RADIUS_2, GRENADE_RADIUS_2); 176 | restore(); 177 | }; 178 | } 179 | -------------------------------------------------------------------------------- /src/js/world/masks.js: -------------------------------------------------------------------------------- 1 | var masks = [{ 2 | "mask": matrix([ 3 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 4 | [1, 1, 0, 3, 1, 1, 3, 0, 1, 1], 5 | [1, 1, 0, 0, 0, 0, 0, 0, 1, 1], 6 | [1, 1, 0, 0, 0, 0, 0, 0, 1, 1], 7 | [1, 1, 1, 0, 0, 0, 0, 1, 1, 1], 8 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 9 | [0, 0, 3, 0, 0, 0, 0, 3, 0, 0], 10 | [1, 1, 1, 0, 1, 1, 0, 1, 1, 1], 11 | [1, 1, 1, 0, 0, 0, 0, 1, 1, 1], 12 | [1, 1, 1, 0, 0, 0, 0, 1, 1, 1] 13 | ]), 14 | "exits": evaluate(DOWN | LEFT | RIGHT | UP) 15 | }, { 16 | "mask": matrix([ 17 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 18 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 19 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 20 | [0, 3, 3, 0, 0, 0, 0, 3, 3, 0], 21 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 22 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 23 | [0, 3, 3, 0, 0, 0, 0, 3, 3, 0], 24 | [1, 1, 1, 3, 0, 0, 3, 1, 1, 1], 25 | [1, 1, 1, 0, 0, 0, 0, 1, 1, 1], 26 | [1, 1, 1, 0, 0, 0, 0, 1, 1, 1] 27 | ]), 28 | "exits": evaluate(DOWN | LEFT | RIGHT) 29 | }, { 30 | "mask": matrix([ 31 | [1, 0, 0, 1, 1, 1, 1, 0, 0, 1], 32 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], 33 | [1, 1, 1, 1, 0, 0, 1, 1, 1, 1], 34 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], 35 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], 36 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 0], 37 | [1, 0, 0, 1, 1, 1, 1, 0, 0, 0], 38 | [1, 0, 0, 1, 1, 1, 1, 0, 0, 1], 39 | [1, 0, 0, 1, 1, 1, 1, 0, 0, 1], 40 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] 41 | ]), 42 | "exits": evaluate(RIGHT | UP) 43 | }, { 44 | "mask": matrix([ 45 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 46 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 0], 47 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 0], 48 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 0], 49 | [1, 0, 0, 1, 1, 0, 0, 3, 0, 0], 50 | [1, 0, 0, 1, 1, 0, 0, 1, 1, 0], 51 | [1, 0, 0, 1, 1, 0, 0, 1, 1, 0], 52 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 53 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 54 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] 55 | ]), 56 | "exits": evaluate(RIGHT) 57 | }, { 58 | "mask": matrix([ 59 | [1, 1, 1, 0, 0, 0, 0, 0, 0, 1], 60 | [1, 1, 1, 1, 0, 0, 0, 0, 1, 1], 61 | [0, 0, 1, 1, 0, 0, 0, 0, 1, 1], 62 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 1], 63 | [0, 0, 0, 3, 0, 0, 0, 0, 0, 1], 64 | [0, 0, 1, 1, 0, 0, 0, 0, 1, 1], 65 | [0, 0, 1, 1, 0, 0, 0, 0, 1, 1], 66 | [1, 1, 1, 1, 3, 0, 1, 1, 1, 1], 67 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], 68 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 1] 69 | ]), 70 | "exits": evaluate(DOWN | LEFT | UP) 71 | }, { 72 | "mask": matrix([ 73 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 74 | [0, 0, 3, 0, 0, 0, 0, 3, 0, 0], 75 | [0, 1, 1, 1, 3, 3, 1, 1, 1, 0], 76 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 77 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 78 | [0, 0, 3, 0, 0, 0, 0, 3, 0, 0], 79 | [0, 1, 1, 1, 3, 3, 1, 1, 1, 0], 80 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 81 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 82 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] 83 | ]), 84 | "exits": evaluate(DOWN | LEFT | RIGHT | UP) 85 | }, { 86 | "mask": matrix([ 87 | [1, 1, 0, 0, 0, 0, 0, 0, 1, 1], 88 | [1, 1, 0, 0, 0, 0, 0, 0, 1, 1], 89 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 90 | [0, 0, 0, 0, 3, 3, 0, 0, 0, 0], 91 | [0, 0, 0, 0, 1, 1, 0, 0, 0, 0], 92 | [0, 0, 0, 0, 1, 1, 0, 0, 0, 0], 93 | [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], 94 | [0, 0, 0, 0, 3, 3, 0, 0, 0, 0], 95 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 96 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] 97 | ]), 98 | "exits": evaluate(DOWN | LEFT | RIGHT | UP) 99 | }, { 100 | "mask": matrix([ 101 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], 102 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], 103 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], 104 | [1, 1, 1, 1, 0, 0, 1, 1, 1, 1], 105 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 106 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 107 | [1, 1, 1, 0, 1, 1, 0, 1, 1, 1], 108 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], 109 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], 110 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] 111 | ]), 112 | "exits": evaluate(LEFT | RIGHT | UP) 113 | }, { 114 | "mask": matrix([ 115 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 116 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 117 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 118 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 119 | [0, 0, 0, 0, 3, 3, 0, 0, 0, 0], 120 | [0, 1, 1, 0, 1, 1, 0, 1, 1, 0], 121 | [0, 1, 1, 0, 0, 0, 0, 1, 1, 0], 122 | [0, 1, 1, 0, 0, 0, 0, 1, 1, 0], 123 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 124 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] 125 | ]), 126 | "exits": evaluate(LEFT | RIGHT | UP) 127 | }, { 128 | "mask": matrix([ 129 | [1, 3, 0, 0, 0, 0, 0, 0, 1, 1], 130 | [1, 3, 0, 0, 0, 0, 0, 0, 1, 1], 131 | [1, 0, 0, 0, 0, 0, 0, 0, 1, 1], 132 | [1, 0, 0, 0, 0, 0, 0, 0, 1, 1], 133 | [1, 1, 0, 0, 0, 0, 0, 0, 1, 1], 134 | [1, 1, 0, 0, 0, 0, 0, 0, 3, 0], 135 | [1, 1, 0, 0, 1, 1, 0, 0, 0, 0], 136 | [1, 1, 0, 0, 1, 1, 0, 0, 3, 0], 137 | [1, 1, 0, 0, 1, 1, 0, 0, 1, 1], 138 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] 139 | ]), 140 | "exits": evaluate(RIGHT | UP) 141 | }, { 142 | "mask": matrix([ 143 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 144 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 145 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 146 | [0, 0, 0, 0, 1, 1, 0, 0, 0, 0], 147 | [0, 0, 0, 0, 1, 1, 0, 0, 0, 0], 148 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 149 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 150 | [1, 1, 1, 0, 0, 0, 0, 1, 1, 1], 151 | [1, 1, 1, 0, 3, 3, 0, 1, 1, 1], 152 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] 153 | ]), 154 | "exits": evaluate(LEFT | RIGHT) 155 | }, { 156 | "mask": matrix([ 157 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 158 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 1], 159 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 1], 160 | [0, 0, 0, 0, 0, 0, 0, 0, 1, 1], 161 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 1], 162 | [0, 0, 3, 3, 0, 0, 0, 0, 0, 1], 163 | [1, 1, 1, 1, 3, 0, 3, 1, 1, 1], 164 | [1, 1, 1, 1, 0, 0, 0, 0, 0, 1], 165 | [1, 1, 1, 1, 0, 0, 0, 0, 0, 1], 166 | [1, 1, 1, 1, 0, 0, 0, 0, 0, 1] 167 | ]), 168 | "exits": evaluate(DOWN | LEFT) 169 | }, { 170 | "mask": matrix([ 171 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 1], 172 | [0, 0, 0, 0, 3, 1, 1, 3, 0, 1], 173 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 1], 174 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 1], 175 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 1], 176 | [1, 1, 1, 0, 0, 0, 0, 3, 0, 1], 177 | [1, 1, 1, 0, 0, 0, 0, 1, 1, 1], 178 | [1, 1, 1, 0, 0, 0, 0, 0, 0, 1], 179 | [1, 1, 1, 3, 3, 0, 0, 0, 0, 1], 180 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] 181 | ]), 182 | "exits": evaluate(LEFT | UP) 183 | }, { 184 | "mask": matrix([ 185 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 186 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 187 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 188 | [0, 0, 0, 0, 1, 1, 0, 0, 0, 0], 189 | [0, 0, 0, 0, 1, 1, 0, 0, 0, 0], 190 | [0, 0, 0, 1, 1, 1, 1, 0, 0, 0], 191 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 192 | [0, 0, 3, 0, 0, 0, 0, 3, 0, 0], 193 | [0, 1, 1, 0, 0, 0, 0, 1, 1, 0], 194 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] 195 | ]), 196 | "exits": evaluate(LEFT | RIGHT) 197 | }, { 198 | "mask": matrix([ 199 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 200 | [1, 1, 1, 0, 0, 0, 0, 1, 1, 1], 201 | [1, 1, 1, 0, 0, 0, 0, 1, 1, 1], 202 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 203 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 204 | [0, 0, 0, 0, 1, 1, 0, 0, 0, 0], 205 | [0, 3, 0, 0, 1, 1, 0, 0, 3, 0], 206 | [1, 1, 0, 0, 1, 1, 0, 0, 1, 1], 207 | [1, 1, 0, 0, 1, 1, 0, 0, 1, 1], 208 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] 209 | ]), 210 | "exits": evaluate(LEFT | RIGHT) 211 | }, { 212 | "mask": matrix([ 213 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 214 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 215 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 216 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 217 | [0, 0, 0, 0, 1, 1, 0, 0, 0, 0], 218 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 219 | [0, 1, 1, 0, 0, 0, 0, 1, 1, 0], 220 | [0, 1, 1, 0, 0, 0, 0, 1, 1, 0], 221 | [1, 1, 1, 0, 0, 0, 0, 1, 1, 1], 222 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] 223 | ]), 224 | "exits": evaluate(LEFT | RIGHT) 225 | }, { 226 | "mask": matrix([ 227 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 228 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 229 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 230 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 231 | [0, 0, 0, 0, 0, 0, 0, 1, 1, 0], 232 | [0, 0, 0, 0, 0, 0, 0, 1, 1, 0], 233 | [0, 0, 0, 1, 1, 0, 0, 1, 1, 3], 234 | [0, 0, 0, 1, 1, 0, 0, 1, 1, 1], 235 | [1, 0, 0, 1, 1, 0, 0, 1, 1, 1], 236 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] 237 | ]), 238 | "exits": evaluate(LEFT | RIGHT) 239 | }, { 240 | "mask": matrix([ 241 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 242 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 243 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 244 | [1, 1, 0, 3, 1, 1, 3, 0, 1, 1], 245 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 246 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 247 | [1, 1, 0, 3, 1, 1, 3, 0, 1, 1], 248 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 249 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 250 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] 251 | ]), 252 | "exits": evaluate(LEFT | RIGHT) 253 | }, { 254 | "mask": matrix([ 255 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], 256 | [1, 1, 1, 3, 1, 1, 1, 0, 0, 1], 257 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], 258 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], 259 | [1, 0, 0, 1, 1, 1, 1, 1, 1, 1], 260 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 0], 261 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 0], 262 | [1, 1, 1, 1, 3, 1, 1, 0, 0, 1], 263 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], 264 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 1] 265 | ]), 266 | "exits": evaluate(DOWN | RIGHT | UP) 267 | }, { 268 | "mask": matrix([ 269 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 270 | [1, 0, 0, 1, 1, 1, 1, 1, 1, 1], 271 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 0], 272 | [1, 0, 0, 1, 0, 0, 0, 3, 0, 0], 273 | [1, 1, 1, 1, 0, 0, 1, 1, 1, 0], 274 | [1, 0, 0, 0, 0, 0, 1, 1, 1, 0], 275 | [1, 0, 0, 0, 0, 0, 1, 1, 1, 1], 276 | [1, 0, 0, 1, 1, 1, 1, 1, 1, 1], 277 | [1, 0, 0, 1, 1, 1, 1, 1, 1, 1], 278 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] 279 | ]), 280 | "exits": evaluate(RIGHT) 281 | }, { 282 | "mask": matrix([ 283 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 284 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], 285 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], 286 | [1, 1, 1, 3, 3, 0, 0, 1, 1, 1], 287 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 0], 288 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 0], 289 | [1, 1, 1, 0, 0, 3, 3, 1, 1, 1], 290 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], 291 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], 292 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] 293 | ]), 294 | "exits": evaluate(RIGHT) 295 | }, { 296 | "mask": matrix([ 297 | [1, 1, 1, 0, 0, 0, 0, 1, 1, 1], 298 | [1, 1, 1, 0, 0, 0, 0, 1, 1, 1], 299 | [1, 1, 1, 0, 0, 0, 0, 1, 1, 1], 300 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 301 | [0, 0, 3, 0, 0, 0, 0, 3, 0, 0], 302 | [0, 1, 1, 0, 0, 0, 0, 1, 1, 0], 303 | [0, 1, 1, 0, 1, 1, 0, 1, 1, 0], 304 | [1, 1, 1, 0, 0, 0, 0, 1, 1, 1], 305 | [1, 1, 1, 0, 0, 0, 0, 1, 1, 1], 306 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] 307 | ]), 308 | "exits": evaluate(LEFT | RIGHT | UP) 309 | }, { 310 | "mask": matrix([ 311 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 312 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 1], 313 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 1], 314 | [0, 1, 1, 1, 1, 1, 1, 0, 0, 1], 315 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1], 316 | [1, 1, 0, 0, 0, 0, 0, 0, 0, 1], 317 | [1, 1, 0, 0, 1, 1, 3, 1, 1, 1], 318 | [1, 1, 0, 0, 0, 0, 0, 0, 0, 1], 319 | [1, 1, 0, 0, 0, 0, 0, 0, 0, 1], 320 | [1, 1, 1, 1, 1, 1, 1, 0, 0, 1] 321 | ]), 322 | "exits": evaluate(DOWN | LEFT) 323 | }, { 324 | "mask": matrix([ 325 | [1, 1, 0, 0, 0, 0, 0, 0, 1, 1], 326 | [0, 0, 0, 0, 1, 1, 0, 0, 0, 0], 327 | [0, 0, 0, 0, 1, 1, 0, 0, 0, 0], 328 | [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], 329 | [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], 330 | [0, 0, 0, 0, 1, 1, 0, 0, 0, 0], 331 | [3, 3, 0, 0, 1, 1, 0, 0, 3, 3], 332 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 333 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 334 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] 335 | ]), 336 | "exits": evaluate(LEFT | RIGHT | UP) 337 | }]; 338 | -------------------------------------------------------------------------------- /src/js/world/mirror-mask.js: -------------------------------------------------------------------------------- 1 | function mirrorMask(mask){ 2 | var exits = mask.exits; 3 | if(mask.exits & RIGHT){ 4 | exits |= LEFT; 5 | }else{ 6 | exits ^= LEFT; 7 | } 8 | if(mask.exits & LEFT){ 9 | exits |= RIGHT; 10 | }else{ 11 | exits ^= RIGHT; 12 | } 13 | 14 | return { 15 | 'mask': mask.mask.map(function(r){ 16 | return r.slice(0).reverse(); // reverse() modifies the array so we need to make a copy of it 17 | }), 18 | 'exits': exits 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /src/js/world/tile.js: -------------------------------------------------------------------------------- 1 | function Tile(row, col, type){ 2 | this.x = (this.col = col) * TILE_SIZE; 3 | this.y = (this.row = row) * TILE_SIZE; 4 | this.solid = [SPAWN_ID, EXIT_ID].indexOf(type) < 0; 5 | this.type = type; 6 | 7 | this.alpha = 1; 8 | this.sizeScale = 1; 9 | 10 | this.center = { 11 | x: this.x + TILE_SIZE / 2, 12 | y: this.y + TILE_SIZE / 2 13 | }; 14 | 15 | this.pushAway = function(character, w, h){ 16 | var adjustments = [{ 17 | x: this.x - (w || CHARACTER_WIDTH) / 2, 18 | y: character.y, 19 | type: LEFT 20 | }, { 21 | x: this.x + TILE_SIZE + (w || CHARACTER_WIDTH) / 2, 22 | y: character.y, 23 | type: RIGHT 24 | }, { 25 | x: character.x, 26 | y: this.y - (h || CHARACTER_HEIGHT) / 2, 27 | type: UP 28 | }, { 29 | x: character.x, 30 | y: this.y + TILE_SIZE + (h || CHARACTER_HEIGHT) / 2, 31 | type: DOWN 32 | }]; 33 | 34 | var closest, 35 | closestDist; 36 | 37 | adjustments.forEach(function(adj){ 38 | var d = sqrt( 39 | pow(adj.x - character.x, 2) + 40 | pow(adj.y - character.y, 2) 41 | ); 42 | if(!closest || d < closestDist){ 43 | closest = adj; 44 | closestDist = d; 45 | } 46 | }); 47 | 48 | character.x = closest.x; 49 | character.y = closest.y; 50 | 51 | return closest.type; 52 | }; 53 | 54 | this.render = function(){ 55 | if(!G.hideTiles && !this.hidden){ 56 | R.fillStyle = '#fff'; 57 | 58 | if(shittyMode){ 59 | var colorChar = ~~(between(0, 1 - dist(this.center, P) / 800, 1) * 0xf); 60 | R.fillStyle = '#' + colorChar.toString(16) + colorChar.toString(16) + colorChar.toString(16); 61 | } 62 | 63 | save(); 64 | translate(this.center.x, this.center.y); 65 | scale(this.sizeScale, this.sizeScale); 66 | translate(evaluate(-TILE_SIZE / 2), evaluate(-TILE_SIZE / 2)); 67 | 68 | if(type == TILE_ID || type == UNBREAKABLE_TILE_ID){ 69 | fillRect(0, 0, TILE_SIZE, TILE_SIZE); 70 | } 71 | 72 | if(type == FLOOR_SPIKE_ID || type == CEILING_SPIKE_ID){ 73 | if(type == CEILING_SPIKE_ID){ 74 | translate(0, TILE_SIZE); 75 | scale(1, -1); 76 | } 77 | 78 | fillRect(0, SPIKE_HEIGHT, TILE_SIZE, evaluate(TILE_SIZE - SPIKE_HEIGHT)); 79 | 80 | beginPath(); 81 | moveTo(0, SPIKE_HEIGHT); 82 | 83 | var step = evaluate(TILE_SIZE / SPIKES_PER_TILE); 84 | for(var x = step / 2 ; x < TILE_SIZE ; x += step){ 85 | lineTo(x, 0); 86 | lineTo(x + step / 2, SPIKE_HEIGHT); 87 | } 88 | lineTo(TILE_SIZE, SPIKE_HEIGHT); 89 | fill(); 90 | } 91 | 92 | if(type == EXIT_ID){ 93 | // Halo 94 | if(!shittyMode){ 95 | drawImage(whiteHalo, evaluate(TILE_SIZE / 2 - HALO_SIZE_HALF), evaluate(TILE_SIZE / 2 - HALO_SIZE_HALF)); 96 | } 97 | 98 | if(this.alpha == 1){ 99 | // Bug ID 100 | R.font = '14pt Courier New'; 101 | 102 | fillText( 103 | 'Bug #' + G.currentLevel, 104 | evaluate(TILE_SIZE / 2), 105 | evaluate(-ARROW_SIZE + ARROW_Y_OFFSET - 10) 106 | ); 107 | 108 | // Arrow 109 | beginPath(); 110 | moveTo(evaluate(TILE_SIZE / 2 - ARROW_SIZE / 2), evaluate(-ARROW_SIZE / 2 + ARROW_Y_OFFSET)); 111 | lineTo(evaluate(TILE_SIZE / 2 + ARROW_SIZE / 2), evaluate(-ARROW_SIZE / 2 + ARROW_Y_OFFSET)); 112 | lineTo(evaluate(TILE_SIZE / 2), evaluate(ARROW_Y_OFFSET)); 113 | fill(); 114 | } 115 | 116 | R.globalAlpha = this.alpha; 117 | 118 | R.fillStyle = noisePattern; 119 | 120 | var x = rand(NOISE_PATTERN_SIZE), 121 | y = rand(NOISE_PATTERN_SIZE); 122 | 123 | translate(x, y); 124 | fillRect(-x, -y, TILE_SIZE, TILE_SIZE); 125 | } 126 | 127 | restore(); 128 | } 129 | }; 130 | 131 | this.landed = function(c){ 132 | if(type === FLOOR_SPIKE_ID){ 133 | c.hurt(this.center); 134 | } 135 | }; 136 | 137 | this.tapped = function(c){ 138 | if(type == CEILING_SPIKE_ID){ 139 | c.hurt(this.center); 140 | } 141 | }; 142 | } 143 | -------------------------------------------------------------------------------- /src/js/world/world.js: -------------------------------------------------------------------------------- 1 | function World(matrix){ 2 | this.tiles = []; 3 | this.matrix = matrix; 4 | 5 | this.rows = matrix.length; 6 | this.cols = matrix[0].length; 7 | 8 | for(var row = 0 ; row < matrix.length ; row++){ 9 | this.tiles.push([]); 10 | for(var col = 0 ; col < matrix[row].length ; col++){ 11 | this.tiles[row][col] = null; 12 | if(matrix[row][col] > 0){ 13 | this.tiles[row][col] = new Tile(row, col, matrix[row][col]); 14 | 15 | if(matrix[row][col] == SPAWN_ID){ 16 | this.spawn = this.tiles[row][col]; 17 | }else if(matrix[row][col] == EXIT_ID){ 18 | this.exit = this.tiles[row][col]; 19 | } 20 | } 21 | } 22 | } 23 | 24 | this.tileAt = function(x, y){ 25 | var row = ~~(y / TILE_SIZE); 26 | var t = this.tiles[row] && this.tiles[row][~~(x / TILE_SIZE)]; 27 | return t && t.solid && t; 28 | }; 29 | 30 | this.destroyTile = function(tile){ 31 | if(tile && tile.type != UNBREAKABLE_TILE_ID){ 32 | for(var i = 0 ; i < 50 ; i++){ 33 | var d = rand(0.5, 2), 34 | x = tile.x + rand(TILE_SIZE); 35 | 36 | particle(4, '#fff', [ 37 | ['x', x, x, d], 38 | ['y', tile.y + rand(TILE_SIZE), this.firstYUnder(x, tile.center.y), d, 0, easeOutBounce], 39 | ['s', 12, 0, d] 40 | ]); 41 | } 42 | 43 | tile.destroyed = true; 44 | this.tiles[tile.row][tile.col] = null; 45 | } 46 | }; 47 | 48 | this.destroyTileAt = function(x, y){ 49 | this.destroyTile(this.tileAt(x, y)); 50 | }; 51 | 52 | this.detectPaths = function(l){ 53 | var colCount = 0, 54 | paths = []; 55 | for(var row = 0 ; row < this.rows - 1 ; row++){ // skip the last row 56 | colCount = 0; 57 | for(var col = 0 ; col < this.cols ; col++){ 58 | var current = this.matrix[row][col] != VOID_ID; 59 | var below = this.matrix[row + 1][col] == TILE_ID || this.matrix[row + 1][col] == UNBREAKABLE_TILE_ID; 60 | 61 | if(!below || current){ 62 | if(colCount >= l){ 63 | paths.push({ 64 | row: row, 65 | colLeft: col - colCount, 66 | colRight: col - 1 67 | }); 68 | } 69 | colCount = 0; 70 | }else{ 71 | colCount++; 72 | } 73 | } 74 | } 75 | return paths; 76 | }; 77 | 78 | this.firstYUnder = function(x, y){ 79 | do{ 80 | y += TILE_SIZE; 81 | }while(y < this.rows * TILE_SIZE && !this.tileAt(x, y)); 82 | 83 | return ~~(y / TILE_SIZE) * TILE_SIZE; 84 | }; 85 | 86 | this.render = function(){ 87 | R.fillStyle = G.hideTiles || shittyMode ? '#000' : '#fff'; 88 | fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); 89 | 90 | save(); 91 | 92 | /*if(G.invert){ 93 | translate(0, CANVAS_HEIGHT); 94 | scale(1, -1); 95 | }*/ 96 | 97 | translate(-V.x, -V.y); 98 | 99 | R.fillStyle = shittyMode ? '#000' : codePattern; 100 | fillRect(0, 0, this.cols * TILE_SIZE, this.rows * TILE_SIZE); 101 | 102 | var cameraRightX = V.x + CANVAS_WIDTH, 103 | cameraBottomY = V.y + CANVAS_HEIGHT; 104 | 105 | for(var row = ~~(V.y / TILE_SIZE) ; row < ~~(cameraBottomY / TILE_SIZE) + 1 ; row++){ 106 | for(var col = ~~(V.x / TILE_SIZE) ; col < ~~(cameraRightX / TILE_SIZE) + 1 ; col++){ 107 | if(this.tiles[row] && this.tiles[row][col]){ 108 | this.tiles[row][col].render(); 109 | } 110 | } 111 | } 112 | 113 | P.render(); 114 | 115 | for(var i in G.renderables){ 116 | G.renderables[i].render(); 117 | } 118 | 119 | if(!shittyMode){ 120 | var px = P.x, 121 | py = P.y + (P.lookingDown ? 200 : 0); 122 | 123 | px = V.x + CANVAS_WIDTH / 2; 124 | py = V.y + CANVAS_HEIGHT / 2; 125 | var haloX = ~~px - DARK_HALO_SIZE_HALF, 126 | haloY = ~~py - DARK_HALO_SIZE_HALF, 127 | haloX2 = haloX + DARK_HALO_SIZE, 128 | haloY2 = haloY + DARK_HALO_SIZE; 129 | 130 | R.fillStyle = '#000'; 131 | if(haloX > V.x){ 132 | fillRect(V.x, haloY, haloX - V.x, DARK_HALO_SIZE); 133 | } 134 | if(haloX2 < cameraRightX){ 135 | fillRect(haloX2, haloY, cameraRightX - haloX2, DARK_HALO_SIZE); 136 | } 137 | if(haloY > V.y){ 138 | fillRect(V.x, V.y, CANVAS_WIDTH, haloY - V.y); 139 | } 140 | if(haloY2 < cameraBottomY){ 141 | fillRect(V.x, haloY2, CANVAS_WIDTH, cameraBottomY - haloY2); 142 | } 143 | 144 | drawImage(darkHalo, haloX, haloY); 145 | } 146 | 147 | restore(); 148 | }; 149 | } 150 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | *{ 2 | margin: 0; 3 | } 4 | 5 | body, html{ 6 | height: 100%; 7 | } 8 | 9 | body{ 10 | background-color: #000; 11 | width: 100%; 12 | } 13 | 14 | #t{ 15 | display: table; 16 | } 17 | 18 | #c{ 19 | display: table-cell; 20 | vertical-align: middle 21 | } 22 | 23 | #cc{ 24 | margin: auto; 25 | outline: 1px solid white; 26 | } 27 | 28 | #t, canvas{ 29 | width: 100%; 30 | height: 100%; 31 | } 32 | 33 | canvas{ 34 | display: block; 35 | } 36 | -------------------------------------------------------------------------------- /tools/level-creator.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Level creator 5 | 6 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 |
31 | 32 |
33 | 34 |
35 | 36 | 37 | -------------------------------------------------------------------------------- /tools/level-creator.js: -------------------------------------------------------------------------------- 1 | var P = { 2 | width: 800, 3 | height: 800, 4 | gridRows: 80, 5 | gridCols: 80, 6 | cellSize: 10 7 | }; 8 | 9 | window.addEventListener('load', function(){ 10 | var textArea = document.querySelector('textarea'); 11 | 12 | var can = document.querySelector('canvas'); 13 | can.width = P.width; 14 | can.height = P.height; 15 | 16 | var ctx = can.getContext('2d'); 17 | 18 | var level = tutorialLevel; 19 | 20 | function extendLevel(rows, cols){ 21 | for(var row = level.length ; row < rows ; row++){ 22 | level.push([]); 23 | for(var col = level[row].length ; col < cols ; col++){ 24 | level[row].push(VOID_ID); 25 | } 26 | } 27 | } 28 | 29 | function renderGrid(grid, ctx, s){ 30 | ctx.fillStyle = '#000'; 31 | ctx.fillRect(0, 0, grid[0].length * s, grid.length * s); 32 | 33 | // Render the tiles 34 | for(var row = 0 ; row < grid.length ; row++){ 35 | for(var col = 0 ; col < grid[0].length ; col++){ 36 | tileRenderMap[grid[row][col]](ctx, row, col, s); 37 | } 38 | } 39 | 40 | // Render a grid 41 | ctx.fillStyle = '#f00'; 42 | for(var col = 0 ; col < grid[0].length ; col++){ 43 | ctx.fillRect(col * s, 0, 1, grid.length * s); 44 | } 45 | for(var row = 0 ; row < grid.length ; row++){ 46 | ctx.fillRect(0, row * s, grid[0].length * s, 1); 47 | } 48 | } 49 | 50 | function render(){ 51 | renderGrid(level, ctx, P.cellSize); 52 | } 53 | 54 | function updateUI(){ 55 | textArea.value = '[\n' + minifiedLevel(level).map(function(xs){ 56 | return ' [' + xs.join(', ') + ']'; 57 | }).join(',\n') + '\n]'; 58 | } 59 | 60 | function mouseEvent(e){ 61 | e.preventDefault(); 62 | 63 | var rect = can.getBoundingClientRect(); 64 | 65 | var canX = e.pageX - rect.left; 66 | var canY = e.pageY - rect.top; 67 | 68 | var col = ~~(canX / P.cellSize); 69 | var row = ~~(canY / P.cellSize); 70 | 71 | var diff = e.which === 1 ? 1 : -1; 72 | 73 | level[row][col] = (level[row][col] + diff + 8) % 8; 74 | 75 | render(); 76 | updateUI(); 77 | } 78 | 79 | can.addEventListener('mousedown', mouseEvent, false); 80 | can.addEventListener('contextmenu', function(e){ 81 | e.preventDefault(); 82 | }, false); 83 | 84 | textArea.addEventListener('keyup', function(){ 85 | try{ 86 | level = eval(this.value); 87 | }catch(e){ 88 | console.error(e); 89 | } 90 | 91 | render(); 92 | }); 93 | 94 | function minifiedLevel(){ 95 | var copy = JSON.parse(JSON.stringify(level)); 96 | 97 | var minRow = null; 98 | var maxRow = 0; 99 | for(var row = 0 ; row < level.length ; row++){ 100 | var used = level[row].filter(function(x){ 101 | return x > 0; 102 | }).length > 0; 103 | 104 | if(used){ 105 | if(minRow === null){ 106 | minRow = row; 107 | } 108 | maxRow = row; 109 | } 110 | } 111 | 112 | copy = copy.slice(minRow, maxRow + 1); 113 | 114 | return copy; 115 | } 116 | 117 | extendLevel(P.gridRows, P.gridCols); 118 | render(); 119 | updateUI(); 120 | }, false); 121 | -------------------------------------------------------------------------------- /tools/mask-creator.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Mask creator 5 | 6 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 |
53 | 54 |
55 | 56 | 57 | 58 | 61 | 62 | 65 | 66 | 69 | 70 | 73 | 74 | 75 | 76 | 77 | 78 |
79 | 80 |
81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /tools/mask-creator.js: -------------------------------------------------------------------------------- 1 | var P = { 2 | width: 400, 3 | height: 400, 4 | gridRows: 10, 5 | gridCols: 10, 6 | cellSize: 40, 7 | simulationCellSize: 10 8 | }; 9 | 10 | function maskMap(){ 11 | var map = {}; 12 | 13 | var allMasks = masks.concat(masks.map(mirrorMask)); 14 | allMasks.forEach(function(mask){ 15 | map[mask.exits] = map[mask.exits] || 0; 16 | map[mask.exits]++; 17 | }); 18 | return map; 19 | } 20 | 21 | console.log(maskMap()); 22 | 23 | window.addEventListener('load', function(){ 24 | var textArea = document.querySelector('textarea'); 25 | 26 | var can = document.querySelector('canvas'); 27 | can.width = P.width; 28 | can.height = P.height; 29 | 30 | var select = document.querySelector('select'); 31 | document.querySelector('#add').onclick = function(){ 32 | masks.push({ 33 | 'mask': [ 34 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 35 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 36 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 37 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 38 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 39 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 40 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 41 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 42 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 43 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] 44 | ], 45 | 'exits': [] 46 | }); 47 | 48 | updateUI(); 49 | }; 50 | 51 | document.querySelector('#delete').onclick = function(){ 52 | masks.splice(currentMaskId, 1); 53 | 54 | currentMaskId--; 55 | updateUI(); 56 | render(); 57 | }; 58 | 59 | document.querySelector('#simulate').onclick = function(){ 60 | var w = generateWorld(12); 61 | 62 | var can = document.querySelector('#simulation-canvas'); 63 | can.width = w[0].length * P.simulationCellSize; 64 | can.height = w.length * P.simulationCellSize; 65 | 66 | var c = can.getContext('2d'); 67 | 68 | renderGrid(w, c, P.simulationCellSize); 69 | }; 70 | 71 | var leftCB = document.querySelector('#left'); 72 | var rightCB = document.querySelector('#right'); 73 | var upCB = document.querySelector('#up'); 74 | var downCB = document.querySelector('#down'); 75 | 76 | downCB.onchange = rightCB.onchange = upCB.onchange = leftCB.onchange = function(){ 77 | var mask = masks[currentMaskId]; 78 | mask.exits = 0; 79 | 80 | if(leftCB.checked) mask.exits |= LEFT; 81 | if(rightCB.checked) mask.exits |= RIGHT; 82 | if(upCB.checked) mask.exits |= UP; 83 | if(downCB.checked) mask.exits |= DOWN; 84 | 85 | updateUI(); 86 | }; 87 | 88 | var ctx = can.getContext('2d'); 89 | 90 | var currentMaskId = 0; 91 | 92 | var tileRenderMap = { 93 | '0': function(){ 94 | 95 | }, 96 | '1': function(ctx, row, col, s){ 97 | // Tile 98 | ctx.fillStyle = '#fff'; 99 | ctx.fillRect(col * s, row * s, s, s); 100 | }, 101 | '2': function(ctx, row, col, s){ 102 | // Unbreakable tile 103 | ctx.fillStyle = '#f00'; 104 | ctx.fillRect(col * s, row * s, s, s); 105 | }, 106 | '3': function(ctx, row, col, s){ 107 | // Probable tile 108 | ctx.globalAlpha = 0.5; 109 | ctx.fillStyle = '#fff'; 110 | ctx.fillRect(col * s, row * s, s, s); 111 | ctx.globalAlpha = 1; 112 | }, 113 | '4': function(ctx, row, col, s){ 114 | // Spawn 115 | ctx.fillStyle = 'blue'; 116 | ctx.fillRect(col * s, row * s, s, s); 117 | }, 118 | '5': function(ctx, row, col, s){ 119 | // Exit 120 | ctx.fillStyle = 'blue'; 121 | ctx.fillRect(col * s, row * s, s, s); 122 | }, 123 | '6': function(ctx, row, col, s){ 124 | // Floor spikes 125 | ctx.fillStyle = '#fff'; 126 | ctx.fillRect(col * s, row * s, s, s); 127 | 128 | ctx.fillStyle = '#f00'; 129 | ctx.fillRect(col * s, row * s, s, s * 0.25); 130 | }, 131 | '7': function(ctx, row, col, s){ 132 | // Ceiling spikes 133 | ctx.fillStyle = '#fff'; 134 | ctx.fillRect(col * s, row * s, s, s); 135 | 136 | ctx.fillStyle = '#f00'; 137 | ctx.fillRect(col * s, (row + 0.75) * s, s, s * 0.25); 138 | } 139 | }; 140 | 141 | function renderGrid(grid, ctx, s){ 142 | ctx.fillStyle = '#000'; 143 | ctx.fillRect(0, 0, grid[0].length * s, grid.length * s); 144 | 145 | // Render the tiles 146 | for(var row = 0 ; row < grid.length ; row++){ 147 | for(var col = 0 ; col < grid[0].length ; col++){ 148 | tileRenderMap[grid[row][col]](ctx, row, col, s); 149 | } 150 | } 151 | 152 | // Render a grid 153 | ctx.fillStyle = '#f00'; 154 | for(var col = 0 ; col < grid[0].length ; col++){ 155 | ctx.fillRect(col * s, 0, 1, grid.length * s); 156 | } 157 | for(var row = 0 ; row < grid.length ; row++){ 158 | ctx.fillRect(0, row * s, grid[0].length * s, 1); 159 | } 160 | } 161 | 162 | function render(){ 163 | var mask = masks[currentMaskId].mask; 164 | renderGrid(mask, ctx, P.cellSize); 165 | } 166 | 167 | function updateUI(){ 168 | textArea.value = JSON.stringify(masks, null, 4); 169 | 170 | textArea.value = '[' + masks.map(function(m){ 171 | var exits = []; 172 | if(m.exits & RIGHT){ 173 | exits.push('RIGHT'); 174 | } 175 | if(m.exits & LEFT){ 176 | exits.push('LEFT'); 177 | } 178 | if(m.exits & DOWN){ 179 | exits.push('DOWN'); 180 | } 181 | if(m.exits & UP){ 182 | exits.push('UP'); 183 | } 184 | 185 | return '{\n' + 186 | ' "mask": matrix([\n' + 187 | m.mask.map(function(row){ 188 | return ' [' + row.join(', ') + ']'; 189 | }).join(',\n') + '\n' + 190 | ' ]),\n' + 191 | ' "exits": evaluate(' + exits.sort().join(' | ') + ')\n' + 192 | '}'; 193 | }).join(', ') + ']'; 194 | 195 | select.innerHTML = ''; 196 | for(var i = 0 ; i < masks.length ; i++){ 197 | var option = document.createElement('option'); 198 | option.setAttribute('data-mask-id', i); 199 | option.innerHTML = 'Mask #' + i; 200 | option.value = i; 201 | option.selected = (i == currentMaskId ? 'selected' : ''); 202 | select.appendChild(option); 203 | } 204 | 205 | select.value = currentMaskId; 206 | 207 | var mask = masks[currentMaskId]; 208 | 209 | rightCB.checked = mask.exits & RIGHT; 210 | leftCB.checked = mask.exits & LEFT; 211 | upCB.checked = mask.exits & UP; 212 | downCB.checked = mask.exits & DOWN; 213 | 214 | updatingUI = false; 215 | } 216 | 217 | select.onchange = function(){ 218 | currentMaskId = parseInt(this.value); 219 | updateUI(); 220 | render(); 221 | }; 222 | 223 | render(); 224 | 225 | function mouseEvent(e){ 226 | e.preventDefault(); 227 | 228 | var rect = can.getBoundingClientRect(); 229 | 230 | var canX = e.pageX - rect.left; 231 | var canY = e.pageY - rect.top; 232 | 233 | var col = ~~(canX / P.cellSize); 234 | var row = ~~(canY / P.cellSize); 235 | 236 | var diff = e.which === 1 ? 1 : -1; 237 | 238 | var mask = masks[currentMaskId].mask; 239 | mask[row][col] = (mask[row][col] + diff + 4) % 4; 240 | 241 | render(); 242 | updateUI(); 243 | } 244 | 245 | can.addEventListener('mousedown', mouseEvent, false); 246 | can.addEventListener('contextmenu', function(e){ 247 | e.preventDefault(); 248 | }, false); 249 | 250 | textArea.addEventListener('keyup', function(){ 251 | try{ 252 | masks = eval(this.value); 253 | }catch(e){ 254 | console.error(e); 255 | } 256 | 257 | render(); 258 | }); 259 | 260 | updateUI(); 261 | }, false); 262 | -------------------------------------------------------------------------------- /tools/sound.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /tools/tile-render-map.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | var tileRenderMap = { 4 | '0': function(){ 5 | 6 | }, 7 | '1': function(ctx, row, col, s){ 8 | // Tile 9 | ctx.fillStyle = '#fff'; 10 | ctx.fillRect(col * s, row * s, s, s); 11 | }, 12 | '2': function(ctx, row, col, s){ 13 | // Unbreakable tile 14 | ctx.fillStyle = '#f00'; 15 | ctx.fillRect(col * s, row * s, s, s); 16 | }, 17 | '3': function(ctx, row, col, s){ 18 | // Probable tile 19 | ctx.globalAlpha = 0.5; 20 | ctx.fillStyle = '#fff'; 21 | ctx.fillRect(col * s, row * s, s, s); 22 | ctx.globalAlpha = 1; 23 | }, 24 | '4': function(ctx, row, col, s){ 25 | // Spawn 26 | ctx.fillStyle = 'blue'; 27 | ctx.fillRect(col * s, row * s, s, s); 28 | }, 29 | '5': function(ctx, row, col, s){ 30 | // Exit 31 | ctx.fillStyle = 'blue'; 32 | ctx.fillRect(col * s, row * s, s, s); 33 | }, 34 | '6': function(ctx, row, col, s){ 35 | // Ceiling spikes 36 | ctx.fillStyle = '#fff'; 37 | ctx.fillRect(col * s, row * s, s, s); 38 | 39 | ctx.fillStyle = '#f00'; 40 | ctx.fillRect(col * s, (row + 0.75) * s, s, s * 0.25); 41 | }, 42 | '7': function(ctx, row, col, s){ 43 | // Floor spikes 44 | ctx.fillStyle = '#fff'; 45 | ctx.fillRect(col * s, row * s, s, s); 46 | 47 | ctx.fillStyle = '#f00'; 48 | ctx.fillRect(col * s, row * s, s, s * 0.25); 49 | } 50 | }; 51 | --------------------------------------------------------------------------------