├── .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 |
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 |
--------------------------------------------------------------------------------