├── externs.js
├── schnosm.js
├── README.md
├── cc.sh
├── LICENSE
├── style.css
├── storage.js
├── editor.html
├── game_objects.js
├── index.html
├── editor.js
├── audio.js
├── keyboard.js
├── renderer.js
├── bitmap.js
├── util.js
├── levels
├── 2up.js
└── level1.js
└── engine.js
/externs.js:
--------------------------------------------------------------------------------
1 | var Audio = {};
2 | var console = {};
3 | var LevelEditor = {};
4 | var localStorage = window.localStorage;
5 |
--------------------------------------------------------------------------------
/schnosm.js:
--------------------------------------------------------------------------------
1 | +function() {
2 | var code = "";
3 | window.addEventListener("keyup", function(e)
4 | {
5 | code = (code + e.which).substr(-14);
6 |
7 | if(code == 0x4c1aa27a8229)
8 | {
9 | localStorage.last_level = '"2up.js"';
10 | localStorage.removeItem("state");
11 | location.reload();
12 | }
13 | }, false);
14 | }();
15 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | I Wanna Be Thy Copy
2 | -
3 |
4 | **Thy Copy : Thy Game**
5 |
6 |
7 | I Wanna Be Thy Copy is yet another I Wanna Be The Guy fangame, written in
8 | JavaScript.
9 |
10 | You can play it online at http://copy.sh/iw/.
11 |
12 |
13 |
14 | The original resource folder is not in this repository yet. It's too big at this
15 | point, I'll add it later.
16 |
17 |
18 |
19 | vim: set tw=80:
20 |
--------------------------------------------------------------------------------
/cc.sh:
--------------------------------------------------------------------------------
1 | IN="storage.js clone.js engine.js audio.js bitmap.js game_objects.js keyboard.js renderer.js util.js schnosm.js"
2 | OUT="iwbtc-all.js"
3 |
4 | ls -lh $OUT
5 |
6 | #--compilation_level ADVANCED_OPTIMIZATIONS\
7 |
8 | java -jar ~/closure-compiler.jar \
9 | --js_output_file $OUT\
10 | --warning_level VERBOSE\
11 | --externs externs.js\
12 | --define=DEBUG=false\
13 | --language_in ECMASCRIPT5_STRICT\
14 | --js $IN
15 |
16 |
17 | #echo $FILENAME
18 |
19 | ls -lh $OUT
20 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2013 Fabian Hemmer
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | The Software shall be used for Good, not Evil.
14 |
--------------------------------------------------------------------------------
/style.css:
--------------------------------------------------------------------------------
1 | canvas {
2 | border: 1px solid #000000;
3 | }
4 | #wrapper {
5 | display: table;
6 | margin: 0 auto;
7 | }
8 | body {
9 | font-family: sans-serif;
10 | background-color: #eee;
11 | }
12 | #editor {
13 | width: 420px;
14 | height: 596px;
15 | margin-left: 3px;
16 | padding: 3px;
17 | }
18 | @media screen and (min-width: 1900px) {
19 | #editor {
20 | width: 700px;
21 | }
22 | }
23 |
24 | #save {
25 | float: right;
26 | }
27 |
28 | .link_like {
29 | cursor: pointer;
30 | font-size: small;
31 | color: darkred;
32 | }
33 |
34 | .link_like:hover {
35 | color: green;
36 | }
37 |
38 | #keys td {
39 | padding: 3px 15px;
40 | }
41 |
42 | #keys {
43 | margin-left: 50px;
44 | margin-top: 10px;
45 | }
46 |
47 | #press_key_msg {
48 | height: 40px;
49 | margin-left: 50px;
50 | }
51 |
52 |
--------------------------------------------------------------------------------
/storage.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 |
4 | /** @constructor */
5 | function GameStorage(gameVersion)
6 | {
7 | this.works = true;
8 | this.tempStorage = {};
9 |
10 | try {
11 | if(!localStorage.getItem("version"))
12 | {
13 | localStorage.setItem("version", gameVersion);
14 |
15 | if(!localStorage.getItem("version"))
16 | {
17 | throw "grenade";
18 | }
19 | }
20 | }
21 | catch(e)
22 | {
23 | this.works = false;
24 | }
25 |
26 | if(this.works)
27 | {
28 | if(Number(localStorage.getItem("version")) !== gameVersion)
29 | {
30 | localStorage.clear();
31 | console.log("Storage cleared because of game update to version " + gameVersion);
32 | }
33 | }
34 | else
35 | {
36 | this.setItem("version", gameVersion);
37 | }
38 | }
39 |
40 | GameStorage.prototype.setItem = function(key, value)
41 | {
42 | var storageValue = JSON.stringify(value);
43 |
44 | if(this.works)
45 | {
46 | localStorage.setItem(key, storageValue);
47 | }
48 | else
49 | {
50 | this.tempStorage[key] = storageValue;
51 | }
52 | };
53 |
54 | GameStorage.prototype.removeItem = function(key)
55 | {
56 | if(this.works)
57 | {
58 | localStorage.removeItem(key);
59 | }
60 | else
61 | {
62 | delete this.tempStorage[key];
63 | }
64 | };
65 |
66 | GameStorage.prototype.getItem = function(key)
67 | {
68 | if(this.works)
69 | {
70 | // returns null if key is not defined
71 | return JSON.parse(localStorage.getItem(key));
72 | }
73 | else
74 | {
75 | if(this.tempStorage[key])
76 | {
77 | return JSON.parse(this.tempStorage[key]);
78 | }
79 | else
80 | {
81 | return null;
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/editor.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
I Wanna Be Thy Copy
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | Your browser doesn't support canvas.
21 |
22 |
Your browser needs JavaScript to run this game.
23 |
24 |
25 |
Show invisible elements
26 |
Auto reload
27 |
save
28 |
29 |
30 |
31 |
32 |
33 | Left Arrow - Move left
34 | Right Arrow - Move right
35 | Space - Jump
36 | T - Shoot
37 | R - Restart
38 | M - Mute sound
39 |
40 |
41 |
mute (key: M)
42 |
43 | Press key to change, or escape to abort.
44 |
45 |
Click here to reset keys
46 |
47 |
Start again from the first level
48 |
49 |
--------------------------------------------------------------------------------
/game_objects.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | /** @constructor */
4 | function Line(x1, y1, x2, y2)
5 | {
6 | this.p1 = { x: x1, y: y1 };
7 | this.p2 = { x: x2, y: y2 };
8 |
9 | // it has to include the last point, so + 1
10 | this.width = Math.abs(this.p1.x - this.p2.x) + 1,
11 | this.height = Math.abs(this.p1.y - this.p2.y) + 1;
12 |
13 | this.bitmap = null;
14 |
15 | //console.log(this.getPixels());
16 | }
17 |
18 | Line.prototype.getBitmap = function()
19 | {
20 | if(this.bitmap)
21 | {
22 | return this.bitmap;
23 | }
24 |
25 | var
26 |
27 | // may be infinity, be careful with it
28 | m = (this.height - 1) / (this.width - 1);
29 |
30 | this.bitmap = new Bitmap(this.width, this.height);
31 |
32 | // current strategy:
33 | // if slope is > 1, walk from y1 to y2,
34 | // otherwise, walk from x1 to x2
35 |
36 | if(m > 1)
37 | {
38 | for(var i = 0; i < this.height; i++)
39 | {
40 | this.bitmap.set(
41 | Math.round(i / m),
42 | i,
43 | 1
44 | );
45 | }
46 | }
47 | else
48 | {
49 | for(var i = 0; i < this.width; i++)
50 | {
51 | this.bitmap.set(
52 | i,
53 | Math.round(i * m),
54 | 1
55 | );
56 | }
57 | }
58 |
59 | return this.bitmap;
60 | };
61 |
62 | /** @constructor */
63 | function Rectangle(width, height)
64 | {
65 | this.width = width;
66 | this.height = height;
67 |
68 | this.bitmap = null;
69 | }
70 |
71 |
72 | Rectangle.prototype.getBitmap = function()
73 | {
74 | if(this.bitmap)
75 | {
76 | return this.bitmap;
77 | }
78 |
79 | this.bitmap = new Bitmap(this.width, this.height);
80 |
81 | for(var i = 0; i < this.bitmap.count; i++)
82 | {
83 | this.bitmap.data[i] = 1;
84 | }
85 |
86 | return this.bitmap;
87 | };
88 |
89 |
90 | /** @constructor */
91 | function AutoShape(image)
92 | {
93 | this.width = image.width;
94 | this.height = image.height;
95 |
96 | this.image = image;
97 | this.bitmap = null;
98 | }
99 |
100 | AutoShape.prototype.getBitmap = function()
101 | {
102 | if(!this.bitmap)
103 | {
104 | this.bitmap = Bitmap.fromImage(this.image);
105 | }
106 |
107 | return this.bitmap;
108 | };
109 |
110 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | I Wanna Be Thy Copy
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | I Wanna Be Thy Copy
12 | Thy Copy : Thy Game
13 |
14 | Back to level 1 & delete save
15 |
16 |
17 | mute (key: M)
18 |
19 |
20 | Your browser doesn't support canvas.
21 |
22 |
23 |
Your browser needs JavaScript to run this game.
24 |
25 |
26 |
27 | Default keys (click to change):
28 |
29 |
30 |
31 | Left Arrow - Move left
32 |
33 | Space - Jump
34 |
35 | R - Restart
36 |
37 |
38 | Right Arrow - Move right
39 |
40 | T - Shoot
41 |
42 | M - Mute sound
43 |
44 |
45 |
46 |
47 |
48 | Press key to change, or escape to abort.
49 |
50 |
Click here to reset keys
51 |
52 |
53 |
54 |
55 | IWBTG was originally created by
Kayin , who shall
56 | be worshipped for creating
the best game ever .
57 | Also props to Ellipsis and sam136 from the IWBTG forum for ripping
58 | the music and images from the original game.
59 |
60 |
61 | Source code available on
github .
62 |
63 |
64 |
--------------------------------------------------------------------------------
/editor.js:
--------------------------------------------------------------------------------
1 | function LevelEditor(game)
2 | {
3 | this.saveButton = document.getElementById("save");
4 | this.editor = document.getElementById("editor");
5 |
6 | this.showInvisibles = document.getElementById("show_invisibles");
7 |
8 | this.editor.value = "Loading ...";
9 | this.editor.disabled = true;
10 |
11 | this.showInvisibleElements = false;
12 |
13 | http_get(LEVEL_DIR + game.levelFile, this.loaded.bind(this));
14 |
15 | this.game = game;
16 |
17 | Function.hook(game.renderer, "redraw", this.redrawHook.bind(this));
18 | // TODO:
19 | //game.addDrawHook(this.redrawHook.bind(this));
20 |
21 | game.renderer.canvas.addEventListener("mousemove",
22 | this.updateCoords.bind(this), false);
23 |
24 | this.showInvisibles.addEventListener("change",
25 | this.setShowInvisibles.bind(this), false);
26 |
27 | var autoreload_box = document.getElementById("autoreload");
28 |
29 | if(location.search.indexOf("reload=1") >= 0)
30 | {
31 | autoreload_box.checked = true;
32 | autoreload_box.onclick = function()
33 | {
34 | location.search = location.search.replace("reload=1", "");
35 | }
36 |
37 | timeout = setTimeout(function()
38 | {
39 | location.reload();
40 | }, 2000);
41 | }
42 | else
43 | {
44 | autoreload_box.onclick = function()
45 | {
46 | location.search += "&reload=1";
47 | }
48 | }
49 |
50 | // debugging help
51 | window.ge = game;
52 | }
53 |
54 | LevelEditor.prototype.redrawHook = function()
55 | {
56 | var game = this.game,
57 | renderer = game.renderer,
58 | ctx = renderer.context,
59 | level = game.level;
60 |
61 | if(this.showInvisibleElements)
62 | {
63 | ctx.fillStyle = "rgba(0, 0, 0, .5)";
64 |
65 | game.objects.forEach(function(obj)
66 | {
67 | if(!obj.visible || !obj.image)
68 | {
69 | if(obj.image)
70 | {
71 | ctx.drawImage(
72 | obj.image,
73 | Math.round(obj.x - game.viewportX),
74 | Math.round(obj.y - game.viewportY),
75 | obj.width,
76 | obj.height
77 | );
78 | }
79 | else if(obj.bitmap)
80 | {
81 | obj.bitmap.withOtherRect(level.width, level.height, -obj.x, -obj.y, putOnImage);
82 | }
83 |
84 | }
85 | });
86 |
87 | function putOnImage(x, y, bit)
88 | {
89 | if(bit)
90 | {
91 | ctx.fillRect(x - game.viewportX, y - game.viewportY, 1, 1);
92 | }
93 | }
94 | }
95 |
96 | };
97 |
98 | LevelEditor.prototype.updateCoords = function(e)
99 | {
100 | var x = e.offsetX + this.game.viewportX,
101 | y = e.offsetY + this.game.viewportY;
102 |
103 | document.getElementById("coords").textContent = x + " " + y;
104 | };
105 |
106 | LevelEditor.prototype.setShowInvisibles = function(e)
107 | {
108 | this.showInvisibleElements = this.showInvisibles.checked;
109 | };
110 |
111 | LevelEditor.prototype.updateCoords = function(e)
112 | {
113 | var x = e.offsetX + this.game.viewportX,
114 | y = e.offsetY + this.game.viewportY;
115 |
116 | document.getElementById("coords").textContent = x + " " + y;
117 | };
118 |
119 | LevelEditor.prototype.loaded = function(text)
120 | {
121 | this.editor.value = text;
122 | this.editor.disabled = false;
123 |
124 | this.saveButton.addEventListener("click", this.save.bind(this), false);
125 | };
126 |
127 | LevelEditor.prototype.save = function()
128 | {
129 | try
130 | {
131 | var level = eval(this.editor.value + "; level;");
132 | }
133 | catch(e)
134 | {
135 | console.log("syntax error");
136 | return;
137 | }
138 |
139 | if(!level)
140 | {
141 | console.log("no level");
142 | return;
143 | }
144 |
145 | this.game.initLevel(level);
146 | };
147 |
--------------------------------------------------------------------------------
/audio.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | /**
4 | * @constructor
5 | */
6 | function AudioManager(enabled)
7 | {
8 | this.muted = !enabled;
9 | this.works = true;
10 |
11 | if(!window.Audio)
12 | {
13 | this.works = false;
14 | }
15 | // save the planet, use ogg/vorbis
16 | else if(!new Audio().canPlayType("audio/ogg; codecs=vorbis"))
17 | {
18 | this.works = false;
19 | }
20 | //this.works = false;
21 |
22 | this.playing = [];
23 |
24 | // files that are enqueued when the game is muted, so
25 | // they can be played as soon as someone unmutes
26 | this.playQueue = [];
27 | }
28 |
29 | /**
30 | * @param {boolean=} loop
31 | * @param {boolean=} once
32 | * @param {number=} volume
33 | * @param {number=} startTime
34 | */
35 | AudioManager.prototype.play = function(file, loop, once, volume, startTime)
36 | {
37 | if(!this.works)
38 | {
39 | return;
40 | }
41 |
42 | if(this.muted)
43 | {
44 | this.playQueue.push([new Date, Array.toArray(arguments)]);
45 | return;
46 | }
47 |
48 | var existing = this.playing.filter(Function.byIndex("file", file)),
49 | audioObject = existing.find(playable),
50 | audio;
51 |
52 | function playable(obj)
53 | {
54 | return (obj.audio.ended || obj.audio.paused) != !!once;
55 | }
56 |
57 | if(audioObject)
58 | {
59 | // if the audio ended and can be player more than once
60 | // OR it's running and shouldn't be played more than once,
61 | // restart it
62 | audio = audioObject.audio;
63 | }
64 | else
65 | {
66 | audio = new Audio(this.path + file);
67 |
68 | this.playing.push({
69 | audio: audio,
70 | file: file,
71 | });
72 | }
73 |
74 | var dontPlay = false;
75 |
76 | if(startTime !== undefined)
77 | {
78 | // audio.duration is NaN if it's not known yet, so this
79 | // check should be safe
80 | if(audio.duration && (audio.duration < startTime))
81 | {
82 | // we're too late
83 | dontPlay = true;
84 | }
85 | }
86 | else
87 | {
88 | startTime = 0;
89 | }
90 |
91 | if(!dontPlay)
92 | {
93 | if(volume !== undefined)
94 | {
95 | audio.volume = volume;
96 | }
97 |
98 | if(audio.readyState < 4)
99 | {
100 | audio.addEventListener("loadedmetadata", function()
101 | {
102 | audio.currentTime = startTime;
103 | });
104 | }
105 | else
106 | {
107 | audio.currentTime = startTime;
108 | }
109 |
110 | audio.loop = loop;
111 | audio.play();
112 | }
113 | };
114 |
115 | /**
116 | * @param {function()=} onload
117 | * @param {function()=} onerror
118 | */
119 | AudioManager.prototype.preload = function(file, onload, onerror)
120 | {
121 | // no check is made if the game is muted
122 |
123 | if(this.works && !this.muted)
124 | {
125 | var existing = this.playing.find(Function.byIndex("file", file));
126 |
127 | if(existing)
128 | {
129 | if(onload)
130 | {
131 | setTimeout(onload, 0);
132 | }
133 | return;
134 | }
135 |
136 | var audio = new Audio(this.path + file);
137 |
138 | audio.muted = this.muted;
139 |
140 | if(onload)
141 | {
142 | audio.addEventListener("canplaythrough", onload);
143 |
144 | if(onerror)
145 | {
146 | audio.addEventListener("error", onerror);
147 | }
148 | }
149 |
150 | audio.load();
151 |
152 | this.playing.push({
153 | audio: audio,
154 | file: file,
155 | });
156 |
157 | return audio;
158 | }
159 | else
160 | {
161 | setTimeout(onload, 0);
162 | }
163 | };
164 |
165 | AudioManager.prototype.stop = function(file)
166 | {
167 | if(this.works)
168 | {
169 | var audioObjs = this.playing.filter(Function.byIndex("file", file));
170 |
171 | audioObjs.forEach(stop);
172 | }
173 |
174 | function stop(obj)
175 | {
176 | obj.audio.pause();
177 | //obj.currentTime = 0;
178 | }
179 | };
180 |
181 | AudioManager.prototype.toggleMute = function()
182 | {
183 | if(this.works)
184 | {
185 | var muted = this.muted = !this.muted;
186 |
187 | this.playing.forEach(setMute);
188 | }
189 |
190 | if(!this.muted)
191 | {
192 | var self = this;
193 |
194 | this.playQueue.forEach(function(args)
195 | {
196 | args[1][4] = (Date.now() - args[0]) / 1000;
197 | self.play.apply(self, args[1]);
198 | });
199 |
200 | this.playQueue = [];
201 | }
202 |
203 | function setMute(audioObj)
204 | {
205 | audioObj.audio.muted = muted;
206 | }
207 | };
208 |
209 |
210 |
--------------------------------------------------------------------------------
/keyboard.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var KEY_UP = 38,
4 | KEY_DOWN = 40,
5 | KEY_LEFT = 37,
6 | KEY_RIGHT = 39,
7 | KEY_JUMP = 32,
8 | KEY_RESTART = 82,
9 | KEY_MUTE = 77,
10 | KEY_SHOOT = 84,
11 | KEY_SUICIDE = 81;
12 |
13 |
14 | var KBD_STORAGE_KEY = "keyboard_settings";
15 |
16 | /** @constructor */
17 | function KeyboardManager(game)
18 | {
19 | this.keysPressed = {};
20 | this.keyWasPressed = {};
21 | this.game = game;
22 |
23 | var fromSettings = game.storage.getItem(KBD_STORAGE_KEY);
24 |
25 | if(fromSettings)
26 | {
27 | this.gameKeys = fromSettings;
28 | }
29 | else
30 | {
31 | this.resetKeys();
32 | }
33 |
34 | window.addEventListener("keydown", this.onkeydown.bind(this), false);
35 | window.addEventListener("keyup", this.onkeyup.bind(this), false);
36 |
37 | window.addEventListener("blur", this.onblur.bind(this), false);
38 |
39 | var self = this;
40 |
41 | var click_count = 0;
42 |
43 | document.getElementById("reset_keys").addEventListener("click",
44 | function()
45 | {
46 | this.textContent = [
47 | "Done.",
48 | "Keys resetted.",
49 | "You keep failing at this, huh?",
50 | "undefined",
51 | "Just kidding.",
52 | "Bored?",
53 | "Okay ...",
54 | "There once was a girl from Kentucky ..."
55 | ][click_count++];
56 |
57 | if(click_count === 8)
58 | {
59 | location.href = "https://www.youtube.com/watch?v=oHg5SJYRHA0";
60 | }
61 |
62 | self.resetKeys();
63 | self.saveSettings();
64 | }, false);
65 |
66 | [
67 | ["left", KEY_LEFT],
68 | ["right", KEY_RIGHT],
69 | ["shoot", KEY_SHOOT],
70 | ["jump", KEY_JUMP],
71 | ["mute", KEY_MUTE],
72 | ["restart", KEY_RESTART],
73 | ].forEach(addKeyChanger);
74 |
75 | function addKeyChanger(key)
76 | {
77 | var name = key[0],
78 | keyNumber = key[1],
79 | id = "change_" + name,
80 | element = document.getElementById(id),
81 | keyField = document.getElementById("keys"),
82 | msgField = document.getElementById("press_key_msg");
83 |
84 | element.addEventListener("click", function()
85 | {
86 | keyField.style.display = "none";
87 | msgField.style.display = "block";
88 |
89 | window.addEventListener("keydown", function changeKey(e)
90 | {
91 | if(e.which === 27)
92 | {
93 | // escape
94 | }
95 | else
96 | {
97 | Object.deleteByValue(self.gameKeys, keyNumber);
98 |
99 | self.gameKeys[e.which] = keyNumber;
100 | self.saveSettings();
101 | }
102 |
103 | window.removeEventListener("keydown", changeKey, false);
104 | keyField.style.display = "block";
105 | msgField.style.display = "none";
106 |
107 | e.preventDefault();
108 |
109 | }, false);
110 | }, false);
111 | }
112 | };
113 |
114 | KeyboardManager.prototype.resetKeys = function()
115 | {
116 | this.gameKeys = {
117 | 38: KEY_UP,
118 | 40: KEY_DOWN,
119 | 37: KEY_LEFT,
120 | 39: KEY_RIGHT,
121 | 32: KEY_JUMP,
122 | 82: KEY_RESTART,
123 | 77: KEY_MUTE,
124 | 84: KEY_SHOOT,
125 | 81: KEY_SUICIDE,
126 | };
127 | };
128 |
129 | KeyboardManager.prototype.saveSettings = function()
130 | {
131 | this.game.storage.setItem(KBD_STORAGE_KEY, this.gameKeys);
132 | };
133 |
134 | KeyboardManager.prototype.isValid = function(e)
135 | {
136 | return !(
137 | e.ctrlKey || e.altKey || e.metaKey ||
138 | e.target instanceof HTMLTextAreaElement ||
139 | e.target instanceof HTMLInputElement ||
140 | !this.gameKeys[e.which]
141 | );
142 | };
143 |
144 | KeyboardManager.prototype.onkeydown = function(e)
145 | {
146 | if(this.keysPressed[e.which])
147 | {
148 | e.preventDefault();
149 | }
150 | else if(this.isValid(e))
151 | {
152 | this.keysPressed[e.which] = true;
153 | this.handleKey(false, e.which);
154 | e.preventDefault();
155 | }
156 | }
157 |
158 | KeyboardManager.prototype.onkeyup = function(e)
159 | {
160 | if(this.isValid(e))
161 | {
162 | this.keysPressed[e.which] = false;
163 | this.handleKey(true, e.which);
164 | e.preventDefault();
165 | }
166 | }
167 |
168 | KeyboardManager.prototype.onblur = function()
169 | {
170 | var keys = Object.keys(this.keysPressed);
171 |
172 | for(var i = 0; i < keys.length; i++)
173 | {
174 | var key = keys[i];
175 |
176 | if(this.keysPressed[key])
177 | {
178 | this.handleKey(true, Number(key));
179 | }
180 | }
181 |
182 | this.keysPressed = {};
183 | }
184 |
185 | KeyboardManager.prototype.handleKey = function(is_up, keyCode)
186 | {
187 | var code = this.gameKeys[keyCode];
188 |
189 | if(code === KEY_MUTE)
190 | {
191 | if(!is_up)
192 | {
193 | this.game.toggleMute();
194 | }
195 | }
196 | else if(code === KEY_RESTART)
197 | {
198 | if(!is_up)
199 | {
200 | this.game.restart();
201 | }
202 | }
203 | if(code === KEY_SUICIDE)
204 | {
205 | if(!is_up)
206 | {
207 | this.game.die();
208 | }
209 | }
210 | else
211 | {
212 | this.keyWasPressed[code] = !is_up;
213 | }
214 | };
215 |
216 |
--------------------------------------------------------------------------------
/renderer.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | if(!window.requestAnimationFrame)
4 | {
5 | window.requestAnimationFrame =
6 | window.mozRequestAnimationFrame ||
7 | window.webkitRequestAnimationFrame ||
8 | window.msRequestAnimationFrame;
9 | }
10 |
11 |
12 | /** @constructor */
13 | function GameRenderer(game)
14 | {
15 | var canvas = document.getElementById("canvas");
16 | this.context = canvas.getContext("2d");
17 |
18 | canvas.width = game.width;
19 | canvas.height = game.height;
20 |
21 | this.canvas = canvas;
22 |
23 | // a canvas with the whole background of the level
24 | // can be much bigger than the actual canvas
25 | this.backgroundCanvas = null;
26 |
27 | this.animationTick = 0;
28 |
29 | this.animations = {};
30 |
31 | this.game = game;
32 | }
33 |
34 |
35 | // draws all the static images on one huge canvas
36 | GameRenderer.prototype.loadBackground = function(images)
37 | {
38 | this.backgroundCanvas = this.imagesToCanvas(
39 | images,
40 | this.game.level.width,
41 | this.game.level.height,
42 | this.game.level.backgroundColor
43 | );
44 | };
45 |
46 | GameRenderer.prototype.loadForeground = function(images)
47 | {
48 | this.foregroundCanvas = this.imagesToCanvas(
49 | images,
50 | this.game.level.width,
51 | this.game.level.height
52 | );
53 | };
54 |
55 |
56 | /** @param {string=} background */
57 | GameRenderer.prototype.imagesToCanvas = function(images, width, height, background)
58 | {
59 | var canvas = document.createElement("canvas"),
60 | context = canvas.getContext("2d"),
61 | level = this.game.level;
62 |
63 | canvas.width = width;
64 | canvas.height = height;
65 |
66 |
67 | if(background)
68 | {
69 | context.fillStyle = background;
70 | context.fillRect(0, 0, width, height);
71 | }
72 |
73 | images.forEach(drawOnBackground);
74 |
75 | function drawOnBackground(obj)
76 | {
77 | context.drawImage(obj.image, obj.x, obj.y, obj.width, obj.height);
78 | }
79 |
80 |
81 | return canvas;
82 | };
83 |
84 |
85 | GameRenderer.prototype.drawAnimation = function(id, x, y)
86 | {
87 | var animation = this.game.level.animations[id],
88 | index = this.animationTick / animation.time | 0,
89 | imageId = animation.images[index % animation.images.length];
90 |
91 | this.context.drawImage(this.game.images[imageId], x, y);
92 | };
93 |
94 | GameRenderer.prototype.drawImageOrAnimation = function(id, x, y)
95 | {
96 | if(this.game.level.animations[id])
97 | {
98 | this.drawAnimation(id, x, y);
99 | }
100 | else
101 | {
102 | this.context.drawImage(this.game.images[id], x, y);
103 | }
104 | };
105 |
106 |
107 | GameRenderer.prototype.redraw = function()
108 | {
109 | var ctx = this.context,
110 | game = this.game,
111 | imgs = game.images;
112 |
113 | this.animationTick++;
114 |
115 | drawCanvas(this.backgroundCanvas);
116 |
117 | if(!game.dead)
118 | {
119 | var charImage = "char";
120 |
121 | if(game.fallingState === NOT_FALLING)
122 | {
123 | if(game.isMoving)
124 | {
125 | charImage += "Moving";
126 | }
127 | }
128 | else
129 | {
130 | if(game.vspeed > 0)
131 | {
132 | charImage += "Falling";
133 | }
134 | else
135 | {
136 | charImage += "Jumping";
137 | }
138 | }
139 |
140 | if(game.direction == LEFT)
141 | {
142 | charImage += "Left";
143 | }
144 | else
145 | {
146 | charImage += "Right";
147 | }
148 |
149 | this.drawImageOrAnimation(
150 | charImage,
151 | game.posX - game.viewportX,
152 | game.posY - game.viewportY
153 | );
154 | }
155 |
156 |
157 | //drawCanvas(this.foregroundCanvas);
158 |
159 |
160 | game.drawableObjects.forEach(drawObject);
161 |
162 | if(game.dead)
163 | {
164 | ctx.drawImage(imgs.gameOver, 0, 150);
165 | }
166 |
167 | function drawObject(obj)
168 | {
169 | if(
170 | obj.visible &&
171 | obj.x + obj.width >= game.viewportX &&
172 | obj.x < game.viewportX + game.width &&
173 | obj.y + obj.height >= game.viewportY &&
174 | obj.y < game.viewportY + game.height
175 | )
176 | {
177 | ctx.drawImage(
178 | obj.image,
179 | Math.round(obj.x - game.viewportX),
180 | Math.round(obj.y - game.viewportY),
181 | obj.width,
182 | obj.height
183 | );
184 | }
185 | }
186 |
187 | function drawCanvas(canvas)
188 | {
189 | ctx.drawImage(
190 | canvas,
191 | // from viewport:
192 | game.viewportX, game.viewportY, game.width, game.height,
193 | // to this:
194 | 0, 0, game.width, game.height
195 | );
196 | }
197 |
198 |
199 | //document.getElementById("fps").textContent = Math.round(1000 / delta) + "fps";
200 | };
201 |
202 | GameRenderer.prototype.drawLoadingScreen = function(filesLoaded, fileCount)
203 | {
204 | this.context.fillStyle = "#000";
205 | this.context.fillRect(0, 0, this.game.width, this.game.height);
206 |
207 | this.context.fillStyle = "#fff";
208 | this.context.font = "20px monospace";
209 | this.context.textAlign = "center";
210 | this.context.fillText("Loading resources. Please wait ...", this.game.width >> 1, 100);
211 | this.context.fillText(filesLoaded + " out of " + fileCount, this.game.width >> 1, 140);
212 |
213 | };
214 |
--------------------------------------------------------------------------------
/bitmap.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | /**
4 | * a black/white 2d bitmap for collision detecting
5 | * @constructor
6 | */
7 | function Bitmap(width, height)
8 | {
9 | this.width = width;
10 | this.height = height;
11 | this.count = width * height;
12 |
13 | this.data = new Uint8Array(this.count);
14 | }
15 |
16 | // given an image, return a bitmap of pixels
17 | // where 0 indicates a transparent pixel and 1 non-transparent one
18 | Bitmap.fromImage = function(image)
19 | {
20 | var
21 | width = image.width,
22 | height = image.height,
23 | bitmap = new Bitmap(width, height),
24 | canvas = document.createElement("canvas"),
25 | context = canvas.getContext("2d"),
26 | data;
27 |
28 | canvas.width = width;
29 | canvas.height = height;
30 |
31 | // make sure the canvas is transparent and not white
32 | context.clearRect(0, 0, width, height);
33 |
34 | context.drawImage(image, 0, 0);
35 |
36 | data = context.getImageData(0, 0, width, height).data;
37 |
38 | for(var i = 0; i < bitmap.count; i++)
39 | {
40 | bitmap.data[i] = data[i * 4 + 3] > 0 | 0;
41 | }
42 |
43 | return bitmap;
44 | };
45 |
46 |
47 | Bitmap.prototype.withOtherRect = function(width, height, dx, dy, f)
48 | {
49 | var intersection = this.getIntersection(width, height, dx, dy),
50 | srcPointer = intersection.otherY * width + intersection.otherX,
51 | destPointer = intersection.thisY * this.width + intersection.thisX;
52 |
53 | for(var y = 0; y < intersection.height; y++)
54 | {
55 | for(var x = 0; x < intersection.width; x++)
56 | {
57 | f(intersection.otherX + x, intersection.otherY + y, this.data[destPointer]);
58 |
59 | srcPointer++;
60 | destPointer++;
61 | }
62 |
63 | srcPointer += width - intersection.width;
64 | destPointer += this.width - intersection.width;
65 | }
66 | };
67 |
68 |
69 | Bitmap.prototype.stringify = function()
70 | {
71 | var field = "Bitmap width=" + this.width + " height=" + this.height + "\n";
72 |
73 | for(var y = 0; y < this.height; y++)
74 | {
75 | for(var x = 0; x < this.width; x++)
76 | {
77 | field += String(this.data[y * this.width + x]);
78 | }
79 |
80 | field += "\n";
81 | }
82 |
83 | return field;
84 | };
85 |
86 |
87 | Bitmap.prototype.set = function(x, y, value)
88 | {
89 | if(x >= 0 && y >= 0 && x < this.width && y < this.height)
90 | {
91 | this.data[y * this.width + x] = value;
92 | }
93 | };
94 |
95 | Bitmap.prototype.copy = function()
96 | {
97 | // hacky, but whatever
98 | return {
99 | __proto__: Bitmap.prototype,
100 |
101 | width : this.width,
102 | height : this.height,
103 | count : this.count,
104 |
105 | data : new Uint8Array(this.data),
106 | };
107 | };
108 |
109 |
110 | /**
111 | * Calculate the intersection of this bitmap and an area with given size
112 | * and position
113 | */
114 | Bitmap.prototype.getIntersection = function(width, height, dx, dy)
115 | {
116 | dx = dx || 0;
117 | dy = dy || 0;
118 |
119 | var thisX = Math.max(0, dx),
120 | thisY = Math.max(0, dy),
121 | otherX = Math.max(0, -dx),
122 | otherY = Math.max(0, -dy);
123 |
124 | return {
125 | thisX : thisX,
126 | thisY : thisY,
127 |
128 | otherX : otherX,
129 | otherY : otherY,
130 |
131 | width : Math.max(0, Math.min(this.width - thisX, width - otherX)),
132 | height : Math.max(0, Math.min(this.height - thisY, height - otherY)),
133 | };
134 | };
135 |
136 |
137 | /*
138 | * return a different bitmap that is a slice in the given area
139 | */
140 | Bitmap.prototype.slice = function(width, height, dx, dy)
141 | {
142 | var intersection = this.getIntersection(width, height, dx, dy),
143 | other = new Bitmap(width, height),
144 | srcPointer = intersection.thisY * this.width + intersection.thisX,
145 | destPointer = intersection.otherY * width + intersection.otherX;
146 |
147 | for(var y = 0; y < intersection.height; y++)
148 | {
149 | for(var x = 0; x < intersection.width; x++)
150 | {
151 | other.data[destPointer] = this.data[srcPointer];
152 | srcPointer++;
153 | destPointer++;
154 | }
155 | srcPointer += this.width - intersection.width;
156 | destPointer += width - intersection.width;
157 | }
158 |
159 | return other;
160 | };
161 |
162 | Bitmap.prototype.isZero = function()
163 | {
164 | for(var i = 0; i < this.count; i++)
165 | {
166 | if(this.data[i])
167 | {
168 | return false;
169 | }
170 | }
171 |
172 | return true;
173 | };
174 |
175 |
176 | /**
177 | * @param {Bitmap} other
178 | * @param {number=} dx an offset, by which the parameter bitmap is moved
179 | * @param {number=} dy an offset, by which the parameter bitmap is moved
180 | */
181 | Bitmap.prototype.or = function(other, dx, dy)
182 | {
183 | var intersection = this.getIntersection(other.width, other.height, dx, dy),
184 | srcPointer = intersection.otherY * other.width + intersection.otherX,
185 | destPointer = intersection.thisY * this.width + intersection.thisX;
186 |
187 | for(var y = 0; y < intersection.height; y++)
188 | {
189 | for(var x = 0; x < intersection.width; x++)
190 | {
191 | this.data[destPointer] |= other.data[srcPointer];
192 | srcPointer++;
193 | destPointer++;
194 | }
195 | srcPointer += other.width - intersection.width;
196 | destPointer += this.width - intersection.width;
197 | }
198 | };
199 |
200 |
201 | // compare this bitmap with a given bitmap, moved by dx and dy,
202 | // returning true if they have a bit in common
203 | Bitmap.prototype.compare = function(other, dx, dy)
204 | {
205 | var intersection = this.getIntersection(other.width, other.height, dx, dy),
206 | srcPointer = intersection.otherY * other.width + intersection.otherX,
207 | destPointer = intersection.thisY * this.width + intersection.thisX;
208 |
209 | for(var y = 0; y < intersection.height; y++)
210 | {
211 | for(var x = 0; x < intersection.width; x++)
212 | {
213 | if(this.data[destPointer] && other.data[srcPointer])
214 | {
215 | return true;
216 | }
217 |
218 | srcPointer++;
219 | destPointer++;
220 | }
221 |
222 | srcPointer += other.width - intersection.width;
223 | destPointer += this.width - intersection.width;
224 | }
225 |
226 | return false;
227 | };
228 |
229 | /**
230 | * Compare this bitmap to a list of bitmaps with x and y value
231 | * sx and sy indicate, how this bitmap is moved
232 | */
233 | Bitmap.prototype.compareMany = function(bitmaps, sx, sy)
234 | {
235 | var self = this;
236 |
237 | return bitmaps.some(function(obj)
238 | {
239 | if(obj.x > sx + self.width ||
240 | obj.y > sy + self.height ||
241 | obj.x + obj.bitmap.width < sx ||
242 | obj.y + obj.bitmap.height < sy)
243 | {
244 | // safes us some computations,
245 | // since this is often the case
246 | return false;
247 | }
248 |
249 | return self.compare(obj.bitmap, obj.x - sx, obj.y - sy);
250 | });
251 | };
252 |
253 |
254 |
--------------------------------------------------------------------------------
/util.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | Math.sign = function(x)
4 | {
5 | return (x > 0) - (x < 0);
6 | };
7 |
8 | // round away from zero, opposite of truncation
9 | Math.roundInfinity = function(x)
10 | {
11 | return x > 0 ? Math.ceil(x) : Math.floor(x);
12 | };
13 |
14 | Math.floatToRandom = function(x)
15 | {
16 | var floor = Math.floor(x)
17 |
18 | return floor + (Math.random() > x - floor);
19 | }
20 |
21 | /**
22 | * Return the triangle wave function between -1 and 1 with the given frequency a
23 | */
24 | Math.triangle = function(a)
25 | {
26 | return function(t)
27 | {
28 | var r = t / a;
29 | return -1 + 4 * Math.abs(r + .25 - Math.floor(r + .75));
30 | }
31 | };
32 |
33 | /**
34 | * Return a rectangle wave function between -1 and 1 (can also yield 0) with the
35 | * given frequency a
36 | */
37 | Math.rectangle = function(a)
38 | {
39 | var triangle = Math.triangle(a);
40 |
41 | return function(t)
42 | {
43 | return Math.sign(triangle(t));
44 | };
45 | };
46 |
47 | // return 1 if x > y, -1 if y > x, 0 otherwise
48 | Math.compare = function(x, y)
49 | {
50 | return Math.sign(x - y);
51 | };
52 |
53 | /**
54 | * index, n.:
55 | * Alphabetical list of words of no possible interest where an
56 | * alphabetical list of subjects with references ought to be.
57 | */
58 | Array.prototype.findIndex = function(f)
59 | {
60 | var result;
61 |
62 | this.some(function(value, index)
63 | {
64 | if(f(value))
65 | {
66 | result = index;
67 | return true;
68 | }
69 | });
70 |
71 | return result;
72 | };
73 |
74 | Array.prototype.find = function(f)
75 | {
76 | var index = this.findIndex(f);
77 |
78 | if(index === undefined)
79 | {
80 | return undefined;
81 | }
82 | else
83 | {
84 | return this[index];
85 | }
86 | };
87 |
88 | // delete one element by value
89 | Array.prototype.delete = function(v)
90 | {
91 | var index = this.indexOf(v);
92 |
93 | if(index !== -1)
94 | {
95 | return this.slice(0, index).concat(this.slice(index + 1));
96 | }
97 |
98 | return this;
99 | };
100 |
101 | // concatMap :: (a -> [b]) -> [a] -> [b]
102 | // Map a function over a list and concatenate the results.
103 | Array.prototype.concatMap = function(f)
104 | {
105 | var result = [];
106 |
107 | this.forEach(function(x)
108 | {
109 | result = result.concat(f(x));
110 | });
111 |
112 | return result;
113 | };
114 |
115 | Array.prototype.deleteList = function(xs)
116 | {
117 | return this.filter(function(x)
118 | {
119 | return xs.indexOf(x) === -1;
120 | });
121 | };
122 |
123 | Array.replicate = function(n, x)
124 | {
125 | var xs = [];
126 |
127 | for(var i = 0; i < n; i++)
128 | {
129 | xs[i] = x;
130 | }
131 |
132 | return xs;
133 | }
134 |
135 |
136 | // partition :: (a -> Bool) -> [a] -> ([a], [a])
137 | // The partition function takes a predicate a list and returns the pair of lists
138 | // of elements which do and do not satisfy the predicate, respectively
139 | Array.prototype.partition = function(f)
140 | {
141 | return this.reduce(function(result, x)
142 | {
143 | if(f(x))
144 | {
145 | result[0].push(x);
146 | }
147 | else
148 | {
149 | result[1].push(x);
150 | }
151 |
152 | return result;
153 | }, [[], []]);
154 | };
155 |
156 |
157 | Array.toArray = function(xs)
158 | {
159 | return [].slice.call(xs);
160 | };
161 |
162 |
163 | Function.byIndex = function(index, value)
164 | {
165 | return function(obj)
166 | {
167 | return obj[index] === value;
168 | };
169 | }
170 |
171 | // not well tested, works for everything that we need
172 | Object.deepcopy = function clone(obj)
173 | {
174 | if(typeof obj === "object")
175 | {
176 | if(obj instanceof Array)
177 | {
178 | return obj.map(clone);
179 | }
180 | else if(obj.__proto__)
181 | {
182 | var result = {
183 | __proto__: obj.__proto__
184 | },
185 | keys = Object.keys(obj);
186 |
187 | for(var i = 0; i < keys.length; i++)
188 | {
189 | result[keys[i]] = obj[keys[i]];
190 | }
191 | }
192 | else
193 | {
194 | var result = {};
195 |
196 | for(var k in obj)
197 | {
198 | result[k] = clone(obj[k]);
199 | }
200 | }
201 |
202 | return result;
203 | }
204 | else
205 | {
206 | return obj;
207 | }
208 | };
209 |
210 | Object.isArray = function(x)
211 | {
212 | return x instanceof Array;
213 | }
214 |
215 | Function.not = function(f)
216 | {
217 | return function(x) { return !f(x); };
218 | }
219 |
220 | function range(min, max, step)
221 | {
222 | var result = [];
223 |
224 | step = step || 1;
225 | dbg_assert(step > 0);
226 |
227 | if(max === undefined)
228 | {
229 | max = min;
230 | min = 0;
231 | }
232 |
233 | for(var i = min; i < max; i += step)
234 | {
235 | //yield i; // fuck you
236 | result.push(i);
237 | }
238 |
239 | return result;
240 | }
241 |
242 |
243 | /**
244 | * Hook obj[name], so when it gets called, func will get called too
245 | */
246 | Function.hook = function(obj, name, func)
247 | {
248 | var old = obj[name];
249 |
250 | obj[name] = function()
251 | {
252 | old.apply(this, arguments);
253 | func.apply(this, arguments);
254 | };
255 | }
256 |
257 |
258 | Object.extend = function(o1, o2)
259 | {
260 | var keys = Object.keys(o2),
261 | key;
262 |
263 | for(var i = 0; i < keys.length; i++)
264 | {
265 | key = keys[i];
266 | o1[key] = o2[key];
267 | }
268 |
269 | return o1;
270 | };
271 |
272 | Object.deleteByValue = function(obj, value)
273 | {
274 | var keys = Object.keys(obj),
275 | key
276 |
277 | for(var i = 0; i < keys.length; i++)
278 | {
279 | key = keys[i];
280 |
281 | if(obj[keys[i]] === value)
282 | {
283 | delete obj[keys[i]];
284 | }
285 | }
286 | }
287 |
288 | Object.merge = function(o1, o2)
289 | {
290 | return Object.extend(Object.extend({}, o1), o2);
291 | };
292 |
293 |
294 | Object.values = function(obj)
295 | {
296 | var keys = Object.keys(obj),
297 | result = [];
298 |
299 | for(var i = 0; i < keys.length; i++)
300 | {
301 | result.push(obj[keys[i]]);
302 | }
303 |
304 | return result;
305 | };
306 |
307 |
308 | function dbg_log(str)
309 | {
310 | document.getElementById("debug").textContent = str;
311 | }
312 |
313 | function dbg_warn(str)
314 | {
315 | document.getElementById("warn").textContent += str + "\n";
316 | }
317 |
318 | /**
319 | * @param {string=} msg
320 | */
321 | function dbg_assert(cond, msg)
322 | {
323 | if(!cond)
324 | {
325 | //console.log("assert failed");
326 | //console.log(msg || "");
327 | console.trace();
328 | throw "Assert failed: " + msg;
329 | }
330 | }
331 |
332 | /**
333 | * @param {function(string,number)=} onerror
334 | */
335 | function http_get(url, onready, onerror)
336 | {
337 | var http = new XMLHttpRequest();
338 |
339 | http.onreadystatechange = function()
340 | {
341 | if(http.readyState === 4)
342 | {
343 | if(http.status === 200)
344 | {
345 | onready(http.responseText, url);
346 | }
347 | else
348 | {
349 | if(onerror)
350 | {
351 | onerror(http.responseText, http.status);
352 | }
353 | }
354 | }
355 | };
356 |
357 | http.open("get", url, true);
358 | http.send("");
359 |
360 | return {
361 | cancel : function()
362 | {
363 | http.abort();
364 | }
365 | };
366 | }
367 |
368 | // Given a list of objects and a list of keys that should exist on the objects,
369 | // get the cartesian product of the values and return objects with the result of
370 | // each product
371 | function cartesianProductOnObjects(list, keys)
372 | {
373 | if(!Object.isArray(list))
374 | {
375 | list = [list];
376 | }
377 |
378 | return keys.reduce(singleProduct, list);
379 |
380 | function singleProduct(list, key)
381 | {
382 | return list.concatMap(function(obj)
383 | {
384 | var values = obj[key];
385 |
386 | if(!Object.isArray(values))
387 | {
388 | return [obj];
389 | }
390 | else
391 | {
392 | return values.map(function(val)
393 | {
394 | var x = Object.deepcopy(obj);
395 |
396 | x[key] = val;
397 |
398 | return x;
399 | });
400 | }
401 | });
402 | }
403 | }
404 |
405 |
--------------------------------------------------------------------------------
/levels/2up.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | (function()
4 | {
5 | function loadState(game, state)
6 | {
7 | if(state === 0xbaffbeff)
8 | {
9 | //throw "todo";
10 | game.posX = 350;
11 | game.posY = 577;
12 | game.removeObjectById("saveState1");
13 | }
14 | else
15 | {
16 | console.log("invalid state: " + state);
17 | }
18 | }
19 |
20 | function saveState1(game)
21 | {
22 | game.saveState(0xbaffbeff);
23 | game.removeObject(this);
24 | }
25 |
26 |
27 | function saveState2(game)
28 | {
29 | game.saveState(0xf0f0b33f);
30 | game.removeObject(this);
31 | }
32 |
33 | function movePlatform(game)
34 | {
35 | if(this.backward)
36 | {
37 | if(this.y <= 300)
38 | {
39 | this.backward = false;
40 | this.forward = true;
41 | }
42 | else
43 | {
44 | game.moveObjectDown(this, -1);
45 | }
46 | }
47 |
48 | if(this.forward)
49 | {
50 | game.moveObjectDown(this, 4);
51 |
52 | if(this.y === 460)
53 | {
54 | this.backward = false;
55 | this.forward = false;
56 | }
57 | else if(this.y > 588)
58 | {
59 | this.backward = true;
60 | this.forward = false;
61 | }
62 | }
63 | }
64 |
65 | function moveSpike(length, speed)
66 | {
67 | return function(game)
68 | {
69 | if(this.start === undefined)
70 | {
71 | this.start = this.x;
72 | }
73 |
74 | if(this.backward)
75 | {
76 | if(this.x === this.start)
77 | {
78 | this.backward = false;
79 | this.forward = false;
80 | }
81 | else
82 | {
83 | this.x -= speed;
84 | }
85 | }
86 |
87 | if(this.forward)
88 | {
89 | if(Math.abs(this.x - this.start) >= length)
90 | {
91 | this.backward = true;
92 | this.forward = false;
93 | }
94 | else
95 | {
96 | this.x += speed;
97 | }
98 | }
99 | }
100 | }
101 |
102 | function moveSpikeLaby1(game)
103 | {
104 | if(this.forward)
105 | {
106 | if(this.x > 395)
107 | {
108 | this.x -= 2;
109 | }
110 | }
111 | }
112 |
113 | function moveSpikeLaby2(game)
114 | {
115 | this.x -= Math.rectangle(150)(game.tickCount);
116 | }
117 |
118 | function startObject(id)
119 | {
120 | return function(game)
121 | {
122 | if(!game.dead)
123 | {
124 | var obj = game.objectMap[id] || id;
125 |
126 | obj.forward = true;
127 | obj.backward = false;
128 | }
129 | };
130 | }
131 |
132 | function tickFunction(game)
133 | {
134 | game.viewportX = Math.min(350, Math.max(0, game.posX - 200) * .4 | 0);
135 | }
136 |
137 | // the spikes in the first part of the game
138 | function MovingSpike(posX, posY, triggerY, moveLength, speed)
139 | {
140 | this.position = { x: posX, y: posY };
141 | this.image = speed < 0 ? "spikesLeft" : "spikesRight";
142 | this.killing = true;
143 | this.tickFunction = moveSpike(moveLength, speed);
144 | this.dynamic = true;
145 |
146 | this.init = function(game)
147 | {
148 | game.addObject({
149 | x: 0,
150 | y: triggerY,
151 | shape: new Rectangle(300, 100),
152 | trigger: startObject(this),
153 | });
154 | };
155 | }
156 |
157 |
158 | return {
159 |
160 | resourceDir: "res/original/",
161 | musicDir : "res/music/",
162 |
163 | startPosition: { x: 3, y: 575 },
164 | startViewport: { x: 0, y: 0 },
165 |
166 | width: 1200,
167 | height: 600,
168 |
169 | characterWidth : 25,
170 | characterHeight: 21,
171 |
172 | backgroundMusic : "Fire_Man_Stage.ogg",
173 | deathMusic : "28.ogg",
174 |
175 | jumpMusic1 : "jump1.ogg",
176 | jumpMusic2 : "jump2.ogg",
177 |
178 | loadState: loadState,
179 |
180 | init : function(game)
181 | {
182 | var canvas = document.createElement("canvas"),
183 | context;
184 |
185 | canvas.width = game.width;
186 | canvas.height = game.height;
187 | context = canvas.getContext("2d");
188 |
189 | game.addDrawHook(function(game)
190 | {
191 | return;
192 | var renderer = game.renderer,
193 | x = game.posX + game.level.characterWidth / 2 - game.viewportX | 0,
194 | y = game.posY + game.level.characterHeight / 2 - game.viewportY | 0;
195 |
196 | context.clearRect(0, 0, game.width, game.height);
197 |
198 | var g = context.createRadialGradient(x, y, 0, x, y, 200);
199 | g.addColorStop(1, 'rgba(0,0,0,.95)');
200 | g.addColorStop(0, 'rgba(0,0,0,0)');
201 |
202 | context.fillStyle = g;
203 | context.fillRect(0, 0, game.width, game.height);
204 |
205 | renderer.context.drawImage(canvas, 0, 0);
206 | });
207 |
208 | },
209 |
210 |
211 | physics : {
212 | jumpInitialSpeed : -5,
213 | jumpGravity : .15,
214 | jumpTicks : 100,
215 |
216 | fallSpeedCap : 4.5,
217 | fallGravity : 0.3,
218 |
219 | moveSpeed : 2,
220 |
221 | timePerTick : 12,
222 | },
223 |
224 |
225 | backgroundColor : "#211",
226 |
227 | images : {
228 | "gameOver" : "309.png",
229 |
230 | "platform" : "black_platform.png",
231 | "wall": "1686.png",
232 |
233 | "spikesLeft": "spikes_left.png",
234 | "spikesRight": "spikes_right.png",
235 | "spikesUp": "spikes_up.png",
236 | "spikesDown": "spikes_down.png",
237 |
238 | "yellowGradientLeft": "left_gradient_yellow.png",
239 | "yellowGradientRight": "right_gradient_yellow.png",
240 | "yellowGradientTop": "top_gradient_yellow.png",
241 |
242 | "charR1": "1.png",
243 | "charR2": "2.png",
244 | "charR3": "3.png",
245 | "charR4": "4.png",
246 | "charL1": "18.png",
247 | "charL2": "19.png",
248 | "charL3": "20.png",
249 | "charL4": "21.png",
250 |
251 | "charMR1": "250.png",
252 | "charMR2": "247.png",
253 | "charMR3": "251.png",
254 | "charMR4": "249.png",
255 | "charMR5": "252.png",
256 | "charMR6": "250.png",
257 |
258 | "charML1": "255.png",
259 | "charML2": "248.png",
260 | "charML3": "256.png",
261 | "charML4": "248.png",
262 | "charML5": "257.png",
263 | "charML6": "255.png",
264 |
265 | "charFR1": "14.png",
266 | "charFR2": "15.png",
267 | "charFL1": "17.png",
268 | "charFL2": "16.png",
269 |
270 | "charJumpingLeft" : "7.png",
271 | "charJumpingRight" : "12.png",
272 |
273 | "charHitmap": "char_hitmap.png",
274 |
275 | "spikeUp": "small_spike_up.png",
276 | "spikeLeft": "small_spike_left.png",
277 | "spikeRight": "small_spike_right.png",
278 | "spikeDown": "small_spike_down.png",
279 |
280 |
281 | "jumpOrb": "844.png",
282 | "redOrb": "red_orb.png",
283 | "blueOrb": "blue_orb.png",
284 | },
285 |
286 | animations : {
287 |
288 | "charFallingLeft" : {
289 | time : 2,
290 | images : ["charFL1", "charFL2"],
291 | },
292 | "charFallingRight" : {
293 | time : 2,
294 | images : ["charFR1", "charFR2"],
295 | },
296 |
297 | "charRight" : {
298 | time : 6,
299 | images : ["charR1", "charR2", "charR3", "charR4"],
300 | },
301 | "charLeft" : {
302 | time : 6,
303 | images : ["charL1", "charL2", "charL3"],
304 | },
305 |
306 | "charMovingRight" : {
307 | time : 2,
308 | images : ["charMR1", "charMR2", "charMR3", "charMR4", "charMR5", "charMR6"],
309 | },
310 | "charMovingLeft" : {
311 | time : 2,
312 | images : ["charML1", "charML2", "charML3", "charML4", "charML5", "charML6"],
313 | },
314 | },
315 |
316 | tickFunction : tickFunction,
317 |
318 | objects : [
319 |
320 | {
321 | blocking: true,
322 | image: "wall",
323 | position: [
324 | { x: 0, y: range(184, 568, 32) },
325 | { x: 280, y: range(184, 600, 32) },
326 | { x: 352, y: range(184, 568, 32) },
327 |
328 | { x: 704, y: range(184, 600, 32) },
329 |
330 | { x: range(448, 608, 32), y: 536 },
331 | { x: 640, y: 536 },
332 | { x: range(384, 660, 32), y: 504 },
333 |
334 | { x: range(416, 480, 32), y: 440 },
335 | { x: range(608, 692, 32), y: 440 },
336 |
337 | { x: range(416, 692, 32), y: 408 },
338 |
339 | { x: range(384, 672, 32), y: 314 },
340 | { x: range(384, 576, 32), y: 314 },
341 | { x: range(384, 448, 32), y: 346 },
342 | { x: range(576, 672, 32), y: 346 },
343 |
344 | { x: range(416, 692, 32), y: 250 },
345 | { x: range(384, 660, 32), y: 184 },
346 |
347 | { x: 768, y: range(184, 280, 32) },
348 |
349 | { x: 768, y: 568 },
350 |
351 | { x: range(800, 1200, 32), y: 248 },
352 |
353 | { x: range(768, 1088, 32), y: [344, 376] },
354 | { x: range(768, 1000, 32), y: [472, 504] },
355 |
356 | { x: 1024, y: range(472, 600, 32) },
357 | { x: 1088, y: range(344, 600, 32) },
358 |
359 | ]
360 | },
361 |
362 |
363 | {
364 | image: "yellowGradientRight",
365 | position: { x: 0, y: 568 },
366 | },
367 |
368 | {
369 | image: "yellowGradientTop",
370 | position: { x: 1120, y: 568 },
371 | },
372 |
373 | {
374 | shape: new Line(0, 0, 0, 350),
375 | position: { x: 1150, y: 250 },
376 | blocking: true,
377 | },
378 |
379 | {
380 | image: "spikesUp",
381 | position: [
382 | { x: 736, y: 587 },
383 | { x: 704, y: 172 },
384 | { x: 768, y: 172 },
385 |
386 | { x: 501, y: 492 },
387 | { x: 522, y: 492 },
388 |
389 | { x: range(1024, 1300, 32), y: 236 },
390 |
391 | { x: range(800, 1000, 32), y: 588 },
392 |
393 | { x: 1056, y: 588 },
394 |
395 | ],
396 | killing: true,
397 | },
398 |
399 | {
400 | image: "spikesUp",
401 | position: [
402 | { x: 554, y: 492 },
403 | ],
404 | id: "movingSpikesLaby1",
405 | tickFunction: moveSpikeLaby1,
406 | killing: true,
407 | },
408 | {
409 | position: { x: 460, y: 490 },
410 | shape: new Line(0, 0, 1, 0),
411 | trigger: startObject("movingSpikesLaby1"),
412 | },
413 |
414 | {
415 | image: "spikesUp",
416 | position: [
417 | { x: 524, y: 396 },
418 | ],
419 | id: "movingSpikesLaby2-1",
420 | tickFunction: moveSpikeLaby2,
421 | killing: true,
422 | },
423 |
424 | {
425 | image: "spikeUp",
426 | position: [
427 | { x: 621, y: 591 },
428 | ],
429 | killing: true,
430 | },
431 | {
432 | image: "spikeDown",
433 | position: [
434 | { x: 393, y: 215 },
435 | ],
436 | killing: true,
437 | },
438 | {
439 | image: "spikeLeft",
440 | position: [
441 | { x: 406, y: 264 },
442 | ],
443 | killing: true,
444 | },
445 |
446 | {
447 | image: "spikeUp",
448 | position: [
449 | { x: 619, y: 238 },
450 | ],
451 | blocking: true,
452 | },
453 |
454 |
455 | {
456 | position: [
457 | { x: range(418, 672, 32), y: 279 },
458 | ],
459 | image: "spikesDown",
460 | killing: true,
461 | },
462 |
463 | {
464 | position: [
465 | { x: 692, y: range(474, 596, 32) },
466 | { x: 692, y: range(282, 404, 32) },
467 | ],
468 | image: "spikesLeft",
469 | killing: true,
470 | },
471 |
472 | {
473 | position: [
474 | { x: 384, y: range(378, 500, 32) },
475 | ],
476 | image: "spikesRight",
477 | killing: true,
478 | },
479 |
480 | {
481 | position: { x: -200, y: 600 },
482 | shape: new Line(0, 0, 1400, 0),
483 | blocking: true,
484 | },
485 |
486 | {
487 | position: { x: -150, y: 400 },
488 | shape: new Line(0, 0, 0, 400),
489 | killing: true,
490 | },
491 |
492 |
493 | {
494 | id: "saveState1",
495 | position: { x: 406, y: 550 },
496 | image: "blueOrb",
497 | trigger: saveState1,
498 | },
499 |
500 |
501 | {
502 | id: "saveState2",
503 | position: { x: 406, y: 550 },
504 | image: "blueOrb",
505 | trigger: saveState1,
506 | },
507 |
508 |
509 | // platform
510 | {
511 | id: "movingPlatform",
512 | position: { x: 140, y: 430 },
513 | image: "platform",
514 | tickFunction: movePlatform,
515 | blocking: true,
516 | },
517 | {
518 | position: [
519 | { x: 100, y: 599 },
520 | { x: 150, y: 599 },
521 | ],
522 | shape: new Line(0, 0, 1, 0),
523 | trigger: startObject("movingPlatform"),
524 | },
525 |
526 |
527 | // spikes on the left
528 | {
529 | position: [
530 | { x: 270, y: 184 },
531 | { x: 270, y: 216 },
532 | { x: 270, y: 568 },
533 | { x: 270, y: 536 },
534 | ],
535 | image: "spikesLeft",
536 | killing: true,
537 | },
538 |
539 |
540 | new MovingSpike(270, 248, 210, 110, -1),
541 | new MovingSpike(270, 280, 230, 110, -2),
542 | new MovingSpike(270, 312, 240, 110, -3),
543 | new MovingSpike(270, 344, 270, 70, -1),
544 | new MovingSpike(270, 376, 270, 130, -3),
545 | new MovingSpike(270, 408, 380, 130, -3),
546 | new MovingSpike(270, 440, 370, 130, -3),
547 | new MovingSpike(270, 472, 390, 130, -2),
548 | new MovingSpike(270, 504, 450, 130, -3),
549 |
550 |
551 | // spikes on the right
552 | {
553 | position: [
554 | { x: 32, y: 184 },
555 | { x: 32, y: 216 },
556 | { x: 32, y: 536 },
557 | { x: 32, y: 504 },
558 | { x: 32, y: 440 },
559 | ],
560 | image: "spikesRight",
561 | killing: true,
562 | },
563 |
564 |
565 | new MovingSpike(32, 248, 310, 90, 3),
566 | new MovingSpike(32, 280, 330, 140, 2),
567 | new MovingSpike(32, 312, 330, 170, 1),
568 | new MovingSpike(32, 344, 370, 70, 2),
569 | //new MovingSpike(32, 376, 340, 130, 2),
570 | //new MovingSpike(32, 408, 400, 90, 5),
571 | //new MovingSpike(32, 472, 450, 130, 3),
572 |
573 | // tunnel
574 | {
575 | position: { x: 312, y: range(184, 600, 32) },
576 | image: "spikesRight",
577 | killing: true,
578 | },
579 | {
580 | position: { x: 342, y: range(184, 568, 32) },
581 | image: "spikesLeft",
582 | killing: true,
583 | },
584 |
585 |
586 | {
587 | position: { x: range(352, 660, 32), y: 171 },
588 | image: "spikesUp",
589 | killing: true,
590 | },
591 | {
592 | position: { x: 402, y: 588 },
593 | image: "spikesUp",
594 | killing: true,
595 | },
596 |
597 |
598 | ],
599 | };
600 |
601 | })();
602 |
603 |
--------------------------------------------------------------------------------
/levels/level1.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | (function()
4 | {
5 | function spawnSpike(game)
6 | {
7 | if(this.spike && this.spike.timer)
8 | {
9 | // there is already a spike that is moving
10 | return;
11 | }
12 |
13 | this.spike = game.addObject({
14 | image: "spikeUp",
15 | killing: true,
16 | x: this.x,
17 | y: this.y,
18 | tickFunction: moveHiddenSpike,
19 | zIndex: -1,
20 | });
21 |
22 | this.spike.timer = 64;
23 | }
24 |
25 | function moveHiddenSpike(game)
26 | {
27 | if(this.timer)
28 | {
29 | this.y += Math.rectangle(64)(this.timer);
30 | this.timer--;
31 | }
32 | else
33 | {
34 | game.removeObject(this);
35 | }
36 | }
37 |
38 | function startObject(id)
39 | {
40 | return function(game)
41 | {
42 | var obj = game.objectMap[id];
43 |
44 | obj.active = true;
45 | obj.timer = 0;
46 |
47 | // remove the trigger
48 | game.removeObject(this);
49 | }
50 | }
51 |
52 | function movePlatform(game)
53 | {
54 | if(this.active)
55 | {
56 | if(this.timer % 2 === 0)
57 | {
58 | game.moveObjectDown(this, -1);
59 | }
60 |
61 | if(this.y > 400)
62 | {
63 | game.moveObjectRight(this, -Math.rectangle(260)(this.timer + 65));
64 | }
65 | else
66 | {
67 | game.moveObjectRight(this, 1);
68 | }
69 |
70 | if(this.x < -40)
71 | {
72 | game.removeObject(this);
73 | }
74 |
75 | this.timer++;
76 | }
77 | }
78 |
79 | function movePlatform2(game)
80 | {
81 | if(this.active)
82 | {
83 | if(this.timer < 95)
84 | {
85 | game.moveObjectRight(this, -2);
86 | }
87 | else if(this.timer < 185)
88 | {
89 | //game.moveObjectDown(this,
90 | // 2 * Math.rectangle(85)(this.timer - 95));
91 |
92 | game.moveObjectRight(this, 3);
93 | }
94 | else if(this.timer < 325)
95 | {
96 | if(this.timer % 2)
97 | {
98 | game.moveObjectDown(this, -1);
99 | game.moveObjectRight(this, -1);
100 | }
101 | }
102 | else if(this.timer < 480)
103 | {
104 | game.moveObjectRight(this, -2);
105 | }
106 | else if(this.timer < 500)
107 | {
108 | game.moveObjectDown(this, 6);
109 | }
110 | else if(this.timer < 600)
111 | {
112 | game.moveObjectDown(this, -1);
113 | game.moveObjectRight(this,
114 | Math.round(2 * Math.triangle(80)(this.timer)));
115 | }
116 | else if(this.timer < 750)
117 | {
118 | game.moveObjectRight(this, -1);
119 | }
120 | else if(this.timer < 790)
121 | {
122 | game.moveObjectDown(this, -1);
123 | }
124 | else if(this.timer < 850)
125 | {
126 |
127 | }
128 | else if(this.timer < 950)
129 | {
130 | game.moveObjectRight(this, 3);
131 | }
132 | else if(this.timer < 1000)
133 | {
134 | game.moveObjectDown(this, -1);
135 | }
136 | else if(this.timer < 1022)
137 | {
138 | game.moveObjectRight(this, 4);
139 | }
140 | else if(this.timer < 1050)
141 | {
142 |
143 | }
144 | else if(this.timer < 1150)
145 | {
146 | game.moveObjectDown(this, -1);
147 | }
148 | else if(this.timer < 1230)
149 | {
150 | game.moveObjectRight(this, 1);
151 | }
152 | else if(this.timer < 1300)
153 | {
154 |
155 | }
156 | else
157 | {
158 | game.moveObjectRight(this, -Math.ceil((this.timer - 1300) / 18));
159 | }
160 |
161 |
162 | this.timer++;
163 | }
164 | }
165 |
166 | function badPlatform(game)
167 | {
168 | game.removeObject(this);
169 |
170 | }
171 |
172 | function transitionUp(game)
173 | {
174 | if(game.viewportY === 600)
175 | {
176 | game.viewportY = 0;
177 | }
178 | }
179 |
180 | function transitionDown(game)
181 | {
182 | if(game.viewportY === 0)
183 | {
184 | game.viewportY = 600;
185 | }
186 | }
187 |
188 | function additionalJump(game)
189 | {
190 | game.canJump = true;
191 | game.removeObject(this);
192 |
193 | game.audio.play("Mega_Man_Blast_Sound.ogg");
194 | }
195 |
196 |
197 | function moveApple(game)
198 | {
199 | this.y += Math.rectangle(100)(game.tickCount);
200 | }
201 |
202 | function redOrb(game)
203 | {
204 | var f = game.addObject({
205 | tickFunction: shake,
206 | });
207 |
208 | f.timer = 0;
209 |
210 | game.removeObject(this);
211 | }
212 |
213 | function shake(game)
214 | {
215 | if(this.timer % 4 === 2)
216 | {
217 | game.viewportY -= 2;
218 | }
219 | else if(this.timer % 4 === 0)
220 | {
221 | game.viewportY += 2;
222 | }
223 |
224 | if(this.timer % 10 === 0)
225 | {
226 | game.objectMap["exitBlock"].y--;
227 | game.objectMap["exitSpike"].y--;
228 | }
229 |
230 | this.timer++;
231 |
232 | if(this.timer > 1500)
233 | {
234 | game.removeObject(this);
235 | }
236 | }
237 |
238 |
239 | function tickFunction(game)
240 | {
241 | }
242 |
243 | function nextLevel(game)
244 | {
245 | game.nextLevel("2up.js");
246 |
247 | game.removeObject(this);
248 | }
249 |
250 | function loadState(game, state)
251 | {
252 | if(state === 0xbeeeef)
253 | {
254 | game.posY = 224;
255 | game.posX = 37;
256 | game.viewportY = 0;
257 |
258 | game.removeObjectById("saveState1");
259 | }
260 | else
261 | {
262 | console.log("invalid state: " + state);
263 | }
264 | }
265 |
266 | function saveState1(game)
267 | {
268 | game.saveState(0xbeeeef);
269 | game.removeObject(this);
270 |
271 | game.audio.play("Mega_Man_Beam_Sound.ogg", false, false);
272 | }
273 |
274 | return {
275 |
276 | resourceDir: "res/original/",
277 | musicDir : "res/music/",
278 |
279 | startPosition: { x: 37, y: 900 },
280 | startViewport: { x: 0, y: 600 },
281 |
282 | width: 900,
283 | height: 1360,
284 |
285 | characterWidth : 25,
286 | characterHeight: 21,
287 |
288 | backgroundMusic : "Fire_Man_Stage.ogg",
289 | deathMusic : "28.ogg",
290 |
291 | jumpMusic1 : "jump1.ogg",
292 | jumpMusic2 : "jump2.ogg",
293 |
294 | loadState: loadState,
295 |
296 | init: function(game) {},
297 |
298 | //physics : {
299 | // jumpInitialSpeed : -1.5,
300 | // jumpGravity : .015,
301 | // jumpTicks : 400,
302 |
303 | // fallSpeedCap : 3,
304 | // fallGravity : .045,
305 |
306 | // moveSpeed : 1,
307 |
308 | // // in ms
309 | // timePerTick : 5,
310 | //},
311 |
312 | physics : {
313 | jumpInitialSpeed : -5,
314 | jumpGravity : .15,
315 | jumpTicks : 100,
316 |
317 | fallSpeedCap : 4.5,
318 | fallGravity : 0.3,
319 |
320 | moveSpeed : 2,
321 |
322 | timePerTick : 12,
323 | },
324 |
325 |
326 | backgroundColor : "#ddf",
327 |
328 | images : {
329 | "gameOver" : "309.png",
330 | 1: "338.png",
331 | 3: "5.png",
332 | "apple": "269.png",
333 |
334 | "gradient": "down_gradient_black.png",
335 |
336 |
337 | "charR1": "1.png",
338 | "charR2": "2.png",
339 | "charR3": "3.png",
340 | "charR4": "4.png",
341 | "charL1": "18.png",
342 | "charL2": "19.png",
343 | "charL3": "20.png",
344 | "charL4": "21.png",
345 |
346 | "charMR1": "250.png",
347 | "charMR2": "247.png",
348 | "charMR3": "251.png",
349 | "charMR4": "249.png",
350 | "charMR5": "252.png",
351 | "charMR6": "250.png",
352 |
353 | "charML1": "255.png",
354 | "charML2": "248.png",
355 | "charML3": "256.png",
356 | "charML4": "248.png",
357 | "charML5": "257.png",
358 | "charML6": "255.png",
359 |
360 | "charFR1": "14.png",
361 | "charFR2": "15.png",
362 | "charFL1": "17.png",
363 | "charFL2": "16.png",
364 |
365 | "charJumpingLeft" : "7.png",
366 | "charJumpingRight" : "12.png",
367 |
368 | "charHitmap": "char_hitmap.png",
369 |
370 | "spikeUp": "161.png",
371 | "spikeLeft": "163.png",
372 | "spikeRight": "162.png",
373 | "spikeDown": "164.png",
374 |
375 | "platform": "259.png",
376 | "platform2": "341.png",
377 |
378 | "jumpOrb": "844.png",
379 | "redOrb": "red_orb.png",
380 | "blueOrb": "blue_orb.png",
381 | },
382 |
383 | animations : {
384 |
385 | "charFallingLeft" : {
386 | time : 2,
387 | images : ["charFL1", "charFL2"],
388 | },
389 | "charFallingRight" : {
390 | time : 2,
391 | images : ["charFR1", "charFR2"],
392 | },
393 |
394 | "charRight" : {
395 | time : 6,
396 | images : ["charR1", "charR2", "charR3", "charR4"],
397 | },
398 | "charLeft" : {
399 | time : 6,
400 | images : ["charL1", "charL2", "charL3"],
401 | },
402 |
403 | "charMovingRight" : {
404 | time : 2,
405 | images : ["charMR1", "charMR2", "charMR3", "charMR4", "charMR5", "charMR6"],
406 | },
407 | "charMovingLeft" : {
408 | time : 2,
409 | images : ["charML1", "charML2", "charML3", "charML4", "charML5", "charML6"],
410 | },
411 | },
412 |
413 |
414 | tickFunction : tickFunction,
415 |
416 |
417 | // The list of all objects in the game. Basically speaking, everything is an
418 | // object. Use this if you want to create the ground, draw an image
419 | // somewhere and create enemies. Every object is a JS object and can have
420 | // the following properties:
421 | //
422 | // - image:
423 | // Optional. An image from the list of images defined above. If left out,
424 | // the object is invisible
425 | //
426 | // - blocking:
427 | // Optional, default: false. If set to true, the object blocks the player
428 | // character, he can stand on it but cannot walk through
429 | //
430 | // - killing:
431 | // Optional, default: false. If set to true, the object kills the player
432 | //
433 | // - trigger:
434 | // Optional. A function that is called, when the player enters this
435 | // object. Note: The function will be called on every tick as long as the
436 | // player is inside of the object, you have to take care of this for
437 | // yourself. If not set, nothing happens when the player is inside of the
438 | // object
439 | //
440 | // - retrigger:
441 | // Boolean, optional, default false. If set to true, the trigger
442 | // function above will be called on every tick, as long as the
443 | // character is standing inside of the trigger. Otherwise, it will
444 | // only be called once every time the character enters the trigger.
445 | //
446 | // - shape:
447 | // Optional if the object has an image. Determines where the object blocks
448 | // the player or triggers functions. If left out, the shape is determined
449 | // by the non-transparent pixels of the image.
450 | // Type: An object with a .getBitmap() method, that returns the shape of
451 | // the object as a bitmap.
452 | //
453 | // - position:
454 | // The position of the object(s). A list of objects or a single object
455 | // with x and y property. Both coordinates can be a list or a number. If
456 | // both are lists, the cartesian product is calculated to determine all
457 | // points. Example:
458 | // [{x: 3, y: 4}, {x: 9, y: [3,4,5]]
459 | // -> [{x: 3, y: 4}, {x: 9, y: 3}, {x: 9, y: 4}, {x: 9, y: 5},
460 | // [{x: [1,2], y:[1,2]}
461 | // -> [{x: 1, y: 1}, {x: 2, y: 1}, {x: 1, y: 2}, {x: 2, y: 2},
462 | //
463 | // - dynamic:
464 | // Optional, default false. If set to true, the object can be changed or
465 | // removed later. It will appear in game.objects. Dynamic objects take
466 | // more performance.
467 | //
468 | // - id:
469 | // Optional. Implies dynamic, you don't have to set dynamic if you give an
470 | // id. The object will be exported to level.objectMap[id]
471 | //
472 | // - zIndex:
473 | // Optional, default 0. Cannot be used on non-static objects. Determines
474 | // how objects overlay each other. Objects are drawn in this order:
475 | // 1. Every dynamic object with a negative zIndex, ordered by zIndex
476 | // 2. Every non-dynamic object in the order they appear
477 | // 3. The character
478 | // 4. Every dynamic object with a positive zIndex, ordered by zIndex
479 | //
480 | // - tickFunction:
481 | // Optional. A function that gets called on every tick of the game with
482 | // this set as the object and the game as first parameter.
483 | //
484 | // - init:
485 | // Optional. A function that gets called once the game is (re-)started.
486 |
487 | objects : [
488 |
489 |
490 | {
491 | image: 3,
492 | blocking: true,
493 | position: [
494 | { x: 32, y: 984 },
495 | { x: range(0, 800, 32), y: 1168 },
496 | { x: 0, y: range(0, 1168, 32) },
497 | { x: 768, y: range(0, 1104, 32) },
498 | { x: 260, y: [984, 1016] },
499 | { x: 32, y: 850 },
500 | { x: range(128, 800, 32), y: 384 },
501 | { x: [292, 324], y: 1016 },
502 | { x: range(292, 420, 32), y: 1016 },
503 | { x: 324, y: range(920, 1016, 32) },
504 | { x: 206, y: 550 },
505 | { x: 32, y: 500 },
506 | { x: 32, y: 384 },
507 | { x: range(32, 320, 32), y: 0 },
508 | { x: range(352, 800, 32), y: 0 },
509 |
510 | { x: 309, y: 102 },
511 |
512 | { x: 100, y: 1030 },
513 | { x: 544, y: 964 },
514 | { x: 132, y: 1030 },
515 | { x: 192, y: 1030 },
516 | { x: 512, y: 964 },
517 |
518 | ],
519 | },
520 |
521 | {
522 | image: "gradient",
523 | position: { x: 320, y: 0 },
524 | },
525 |
526 | {
527 | image: "spikeLeft",
528 | killing: true,
529 | position: [
530 | { x: 736, y: range(416, 1104, 32) },
531 | { x: 96, y: 384 },
532 | { x: 736, y: range(32, 356, 32) },
533 | { x: 256, y: 250 },
534 | { x: 416, y: 108 },
535 | { x: 480, y: 224 },
536 | { x: 640, y: 160 },
537 | ],
538 | },
539 | {
540 | image: "spikeRight",
541 | killing: true,
542 | position: [
543 | { x: 288, y: 250 },
544 | { x: 448, y: 108 },
545 | { x: 512, y: 224 },
546 | { x: 672, y: 160 },
547 | ],
548 | },
549 | {
550 | image: "spikeDown",
551 | killing: true,
552 | position: [
553 | { x: range(128, 734, 32), y: 416 },
554 |
555 | { x: range(32, 320, 32), y: 32 },
556 | { x: range(352, 736, 32), y: 32 },
557 | ]
558 | },
559 | {
560 | image: "spikeUp",
561 | killing: true,
562 | position: [
563 | { x: range(32, 734, 32), y: 1136 },
564 | { x: range(128, 736, 32), y: 352 },
565 | { x: [292, 356], y: 984 },
566 | { x: 324, y: 890 },
567 | ],
568 | },
569 |
570 | {
571 | trigger: spawnSpike,
572 | shape: new Line(0, 0, 32, 0),
573 | position: [
574 | { x: 132, y: 1029 },
575 | { x: 100, y: 1029 },
576 | { x: 192, y: 1029 },
577 | { x: 528, y: 963 },
578 | ],
579 | },
580 |
581 | {
582 | image: 3,
583 | trigger: badPlatform,
584 | dynamic: true,
585 |
586 | position: [
587 | { x: 170, y: 168 },
588 | { x: 435, y: 70 },
589 | ]
590 | },
591 |
592 | {
593 | dynamic: true,
594 | trigger: redOrb,
595 | image: "redOrb",
596 | position: { x: 317, y: 76 },
597 | },
598 |
599 | {
600 | trigger: startObject("platform1"),
601 | shape: new Line(0, 0, 32, 0),
602 | position: { x: 690, y: 845 },
603 | },
604 |
605 |
606 | {
607 | trigger: transitionUp,
608 | shape: new Line(0, 0, 800, 0),
609 | position: { x: 0, y: 588 }
610 | },
611 | {
612 | trigger: transitionDown,
613 | shape: new Line(0, 0, 800, 0),
614 | position: { x: 0, y: 615 }
615 | },
616 |
617 |
618 | {
619 | id: "platform1",
620 | image: "platform2",
621 | blocking: true,
622 | position: { x: 700, y: 850 },
623 | tickFunction: movePlatform,
624 | },
625 |
626 | {
627 | id: "platform2",
628 | image: "platform2",
629 | blocking: true,
630 | position: { x: 650, y: 330 },
631 | tickFunction: movePlatform2,
632 | },
633 |
634 | {
635 | trigger: startObject("platform2"),
636 | position: { x: 300, y: 288 },
637 | shape: new Line(0, 0, 0, 100),
638 | },
639 |
640 | //{
641 | // image: 3,
642 | // dynamic: true,
643 | // blocking: true,
644 | // position: [
645 | // { x: 100, y: 1030 },
646 | // { x: 544, y: 964 },
647 | // { x: 132, y: 1030 },
648 | // { x: 192, y: 1030 },
649 | // { x: 512, y: 964 },
650 | // ],
651 | //},
652 |
653 | {
654 | id: "bottomApple",
655 | image: "apple",
656 | killing: true,
657 | position: { x: 775, y: 1130 }
658 | },
659 |
660 | {
661 | dynamic: true,
662 | trigger: additionalJump,
663 | image: "jumpOrb",
664 | position: [
665 | { x: [565, 485, 405, 325], y: 500 },
666 | { x: [230, 300], y: 310 },
667 | { x: 340, y: 275 }, // hard mode: y: 260
668 | ],
669 | },
670 |
671 | {
672 | dynamic: true,
673 | killing: true,
674 | image: "apple",
675 | tickFunction: moveApple,
676 | position: { x: 466, y: 880 },
677 | },
678 |
679 | {
680 | id: "saveState1",
681 | trigger: saveState1,
682 | position: { x: 40, y: 360 },
683 | image: "blueOrb",
684 | },
685 |
686 | {
687 | id: "exitBlock",
688 | position: { x: 320, y: 0 },
689 | image: 3,
690 | },
691 |
692 | {
693 | id: "exitSpike",
694 | position: { x: 320, y: 32 },
695 | image: "spikeDown",
696 | killing: true,
697 | },
698 |
699 | {
700 | position: { x: 319, y: -300 },
701 | blocking: true,
702 | shape: new Line(0, 0, 0, 300),
703 | },
704 |
705 | {
706 | position: { x: 500, y: -300 },
707 | shape: new Line(0, 0, 0, 300),
708 | trigger: nextLevel,
709 | },
710 |
711 |
712 | ],
713 | };
714 |
715 | })();
716 |
717 |
--------------------------------------------------------------------------------
/engine.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | /** @define */
4 | var DEBUG = true;
5 |
6 | /** @const */
7 | var RIGHT = 0,
8 | LEFT = 1;
9 |
10 | /** @const */
11 | var NOT_FALLING = 0,
12 | IN_JUMP = 1,
13 | FALLING = 2;
14 |
15 |
16 | /** @const */
17 | var GAME_WIDTH = 800,
18 | GAME_HEIGHT = 600;
19 |
20 | /** @const */
21 | var FIRST_LEVEL = "level1.js",
22 | LEVEL_DIR = "levels/";
23 |
24 |
25 | /** @const */
26 | var STORAGE_NO_AUDIO = "no_audio",
27 | STORAGE_LEVEL = "last_level",
28 | STORAGE_STATE = "state";
29 |
30 |
31 | window.addEventListener("load", function()
32 | {
33 | var ge = new GameEngine;
34 |
35 | if(DEBUG && location.host === "localhost" && window.LevelEditor)
36 | {
37 | new LevelEditor(ge);
38 | }
39 |
40 | }, false);
41 |
42 |
43 | /** @constructor */
44 | function GameEngine()
45 | {
46 | this.version = .02;
47 |
48 | this.width = GAME_WIDTH;
49 | this.height = GAME_HEIGHT;
50 |
51 | this.storage = new GameStorage(this.version);
52 |
53 | if(!this.storage.works)
54 | {
55 | dbg_warn("localStorage not supported. Your game will not be saved.");
56 | }
57 |
58 | this.keyboard = new KeyboardManager(this);
59 | this.renderer = new GameRenderer(this);
60 |
61 | this.audio = new AudioManager(!this.storage.getItem(STORAGE_NO_AUDIO));
62 |
63 | if(!this.audio.works)
64 | {
65 | dbg_warn("Your browser does not support HTML5 audio or ogg/vorbis");
66 | }
67 |
68 | this.viewportX = null;
69 | this.viewportY = null;
70 |
71 | this.posX = null;
72 | this.posY = null;
73 |
74 | this.lastTick = Date.now();
75 | this.totalDelta = 0;
76 |
77 | this.tickCount = null;
78 |
79 | this.fallingState = null;
80 | this.canJump = null;
81 | this.direction = null;
82 |
83 | // fall speed (vertical)
84 | this.vspeed = null;
85 |
86 | // bitmap of where the character can be hit
87 | this.charBitmap = null;
88 |
89 |
90 | this.objects = null;
91 | this.objectMap = null;
92 | this.triggeringObjects = null;
93 | this.blockingObjects = null;
94 | this.drawableObjects = null;
95 |
96 | this.running = null;
97 | this.tickFunctionStopped = true;
98 |
99 | // is the character moving, used for animating
100 | this.isMoving = null;
101 |
102 | // loaded images
103 | this.images = {};
104 |
105 | // information populated by the level scripter
106 | this.gameData = null;
107 |
108 | this.drawHooks = null;
109 |
110 | this.levelFile = this.storage.getItem(STORAGE_LEVEL);
111 |
112 | var self = this;
113 |
114 | document.getElementById("mute_button").addEventListener("click",
115 | function()
116 | {
117 | self.toggleMute();
118 | }, false);
119 |
120 | document.getElementById("reset_save").addEventListener("click",
121 | function()
122 | {
123 | self.nextLevel(FIRST_LEVEL);
124 | }, false);
125 |
126 |
127 | if(!this.levelFile)
128 | {
129 | this.levelFile = FIRST_LEVEL;
130 | }
131 |
132 | this.loadLevel(this.levelFile);
133 |
134 | }
135 |
136 | // called by level scripter
137 | GameEngine.prototype.nextLevel = function(file)
138 | {
139 | // autosave, there is no way back
140 | this.levelFile = file;
141 | this.storage.setItem(STORAGE_LEVEL, file);
142 | this.storage.removeItem(STORAGE_STATE);
143 |
144 | this.loadLevel(file);
145 | };
146 |
147 | // Step 1: Download the level file,
148 | // call loadResources when done
149 | GameEngine.prototype.loadLevel = function(file)
150 | {
151 | var self = this;
152 |
153 | this.running = false;
154 |
155 | http_get(LEVEL_DIR + file + "?" + Math.random(), function(result)
156 | {
157 | var level = eval(result);
158 |
159 | if(!level)
160 | {
161 | throw "level not found";
162 | }
163 |
164 | self.level = level;
165 |
166 | self.loadResources(level);
167 | });
168 | };
169 |
170 | // Step 2: Download images and audio files of the level
171 | // Call start when done
172 | GameEngine.prototype.loadResources = function(level)
173 | {
174 | var base = level.resourceDir,
175 | imageIds = Object.keys(level.images),
176 | fileCount = 0,
177 | filesLoaded = 0,
178 | self = this;
179 |
180 | this.audio.path = level.musicDir;
181 |
182 | this.level = level;
183 |
184 |
185 | this.audio.preload(this.level.jumpMusic1, fileLoaded(), loadFailed);
186 | this.audio.preload(this.level.jumpMusic2, fileLoaded(), loadFailed);
187 |
188 | imageIds.forEach(function(id)
189 | {
190 | var filename = level.images[id],
191 | image = new Image();
192 |
193 | self.images[id] = image;
194 |
195 | image.onload = fileLoaded();
196 | image.onerror = loadFailed;
197 | image.src = base + filename;
198 | });
199 |
200 | this.renderer.drawLoadingScreen(0, fileCount);
201 |
202 | function fileLoaded()
203 | {
204 | fileCount++;
205 |
206 | return function()
207 | {
208 | filesLoaded++;
209 | self.renderer.drawLoadingScreen(filesLoaded, fileCount);
210 |
211 | if(fileCount === filesLoaded)
212 | {
213 | self.start();
214 | }
215 | };
216 | }
217 |
218 | function loadFailed()
219 | {
220 | throw "loading a resource failed. Will not start";
221 | }
222 | };
223 |
224 |
225 | // Step 3: Start the game
226 | GameEngine.prototype.start = function()
227 | {
228 | this.charBitmap = Bitmap.fromImage(this.images["charHitmap"]);
229 |
230 | this.dead = false;
231 |
232 | this.audio.play(this.level.backgroundMusic, true, true);
233 |
234 | // everything else is initialized there:
235 | this.restart();
236 |
237 | this.running = true;
238 |
239 |
240 | if(this.tickFunctionStopped)
241 | {
242 | this.doTick(this);
243 | }
244 | };
245 |
246 | GameEngine.prototype.restart = function()
247 | {
248 | this.viewportX = this.level.startViewport.x;
249 | this.viewportY = this.level.startViewport.y;
250 |
251 | this.posX = this.level.startPosition.x;
252 | this.posY = this.level.startPosition.y;
253 |
254 | this.fallingState = FALLING;
255 | this.direction = RIGHT;
256 | this.dead = false;
257 | this.isMoving = false;
258 | this.canJump = false;
259 |
260 | this.vspeed = 0;
261 |
262 | //this.audio.setMuteSingle(this.level.backgroundMusic, false);
263 | this.audio.stop(this.level.deathMusic);
264 |
265 | this.tickCount = 0;
266 |
267 | this.gameData = {};
268 |
269 | this.drawHooks = [];
270 |
271 | this.loadObjects();
272 |
273 | var gameState = this.storage.getItem(STORAGE_STATE);
274 | if(gameState)
275 | {
276 | this.level.loadState(this, gameState);
277 | }
278 |
279 | this.level.init(this);
280 | };
281 |
282 | GameEngine.prototype.saveState = function(state)
283 | {
284 | this.storage.setItem(STORAGE_STATE, state);
285 | }
286 |
287 |
288 | GameEngine.prototype.loadObjects = function()
289 | {
290 | var staticImages = [],
291 | self = this,
292 | level = this.level,
293 | objects = Object.deepcopy(level.objects);
294 |
295 | this.objects = [];
296 | this.objectMap = {};
297 | this.triggeringObjects = [];
298 | this.blockingObjects = [];
299 | this.drawableObjects = [];
300 |
301 | objects = objects.concatMap(function(obj)
302 | {
303 | var coords = cartesianProductOnObjects(obj.position, ["x", "y"]);
304 |
305 | return coords.map(function(pos)
306 | {
307 | var o = Object.deepcopy(obj);
308 |
309 | o.x = pos.x;
310 | o.y = pos.y;
311 |
312 | return o;
313 | });
314 | });
315 |
316 |
317 | // temporary cache for the image to bitmap part
318 | var cache = {};
319 |
320 | objects.forEach(function(object)
321 | {
322 | var obj = self.addObject(object, cache);
323 |
324 | if(obj.init)
325 | {
326 | obj.init(self);
327 | }
328 | });
329 |
330 |
331 | var objectsByType = this.drawableObjects.partition(
332 | function(obj)
333 | {
334 | return obj.dynamic;
335 | });
336 |
337 |
338 | //console.log(objects.find(function(x)
339 | // {
340 | // return x.
341 | //console.log(objectsByType)
342 |
343 | this.drawableObjects = objectsByType[0];
344 | //self.objects.sort(function(x, y) { return compare(x.zIndex, y.zIndex); });
345 |
346 | this.renderer.loadBackground(objectsByType[1]);
347 | this.renderer.loadForeground([]);
348 | };
349 |
350 | GameEngine.prototype.addObject = function(object, cache)
351 | {
352 | var
353 | isDynamic = object.dynamic || !!object.id,
354 | shape = object.shape,
355 | trigger = object.trigger,
356 | image = undefined,
357 | width = undefined,
358 | height = undefined,
359 | bitmap = undefined,
360 | knownProperties = [
361 | "x", "y", "dynamic", "trigger", "image",
362 | "blocking", "killing", "id", "tickFunction",
363 | "zIndex", "position", "shape", "retrigger",
364 | "init",
365 | ];
366 |
367 | cache = cache || {};
368 |
369 | if(Object.keys(object).deleteList(knownProperties).length)
370 | {
371 | console.log(Object.keys(object).deleteList(knownProperties));
372 | dbg_assert(false, "Unkown properties");
373 | }
374 |
375 | if(object.image)
376 | {
377 | image = this.images[object.image];
378 | dbg_assert(image, "invalid image id");
379 |
380 | width = image.width;
381 | height = image.height;
382 | }
383 |
384 | dbg_assert(!object.blocking || !trigger,
385 | "an object cannot block and have a trigger at the same time");
386 |
387 | if(object.killing)
388 | {
389 | dbg_assert(!trigger, "an object cannot kill and have a trigger at the same time");
390 | dbg_assert(!object.blocking, "an object cannot kill and block at the same time");
391 | trigger = this.die.bind(this);
392 | }
393 |
394 | if(shape === undefined && image && (trigger || object.blocking || isDynamic))
395 | {
396 | // if the object has an image, but no specific shape and
397 | // might need a shape, generate it now from the image
398 | if(cache[object.image])
399 | {
400 | shape = cache[object.image];
401 | }
402 | else
403 | {
404 | cache[object.image] = shape = new AutoShape(image);
405 | }
406 | }
407 |
408 | if(shape)
409 | {
410 | width = shape.width;
411 | height = shape.height;
412 | bitmap = shape.getBitmap();
413 | }
414 |
415 | var newObject = {
416 | x: object.x,
417 | y: object.y,
418 | width: width,
419 | height: height,
420 | dynamic: isDynamic,
421 | visible: true,
422 | image: image, // may be undefined
423 | bitmap: bitmap, // may be undefined
424 | trigger: trigger, // may be undefined
425 | zIndex: object.zIndex || 0,
426 | retrigger: !!object.retrigger,
427 | tickFunction: object.tickFunction, // may be undefined
428 | init: object.init, // may be undefined
429 | };
430 |
431 |
432 | if(trigger)
433 | {
434 | dbg_assert(trigger instanceof Function, "trigger has to be a function");
435 | dbg_assert(shape, "objects that kill or have a trigger require a shape");
436 |
437 | this.triggeringObjects.push(newObject);
438 | }
439 |
440 | if(object.image)
441 | {
442 | this.drawableObjects.push(newObject);
443 | }
444 |
445 | if(object.blocking)
446 | {
447 | dbg_assert(shape, "objects that block require a shape");
448 | this.blockingObjects.push(newObject);
449 | }
450 |
451 | this.objects.push(newObject);
452 |
453 | if(object.id)
454 | {
455 | dbg_assert(!this.objectMap[object.id], "id used twice");
456 | this.objectMap[object.id] = newObject;
457 | }
458 |
459 | return newObject;
460 | };
461 |
462 | GameEngine.prototype.removeObject = function(obj)
463 | {
464 | this.objects = this.objects.delete(obj);
465 | this.blockingObjects = this.blockingObjects.delete(obj);
466 | this.drawableObjects = this.drawableObjects.delete(obj);
467 | this.triggeringObjects = this.triggeringObjects.delete(obj);
468 |
469 | if(obj.id)
470 | {
471 | delete this.objectMap[obj.id];
472 | }
473 | };
474 |
475 | GameEngine.prototype.removeObjectById = function(id)
476 | {
477 | var obj = this.objectMap[id];
478 |
479 | if(obj)
480 | {
481 | this.removeObject(obj);
482 | }
483 | }
484 |
485 | GameEngine.prototype.toggleMute = function()
486 | {
487 | this.audio.toggleMute();
488 |
489 | if(this.audio.muted)
490 | {
491 | this.storage.setItem(STORAGE_NO_AUDIO, 1);
492 | }
493 | else
494 | {
495 | this.storage.removeItem(STORAGE_NO_AUDIO);
496 | }
497 | };
498 |
499 |
500 | GameEngine.prototype.die = function()
501 | {
502 | if(!this.dead)
503 | {
504 | // Should the music be muted or paused when the death sound is running?
505 | //this.audio.setMuteSingle(this.level.backgroundMusic, true);
506 | this.audio.play(this.level.deathMusic, false, true, .3);
507 | this.dead = true;
508 | }
509 | };
510 |
511 |
512 | GameEngine.prototype.crush = function()
513 | {
514 | // the character is inside of a block
515 | console.log("death by crushing");
516 | this.die();
517 | };
518 |
519 |
520 |
521 | GameEngine.prototype.doTick = function doTick(self)
522 | {
523 | self.tickFunctionStopped = false;
524 |
525 | if(!self.running)
526 | {
527 | self.tickFunctionStopped = true;
528 | return;
529 | }
530 |
531 | var level = self.level,
532 | now = Date.now(),
533 | delta = now - self.lastTick;
534 |
535 | //console.time(1);
536 |
537 | self.totalDelta += delta;
538 | self.lastTick = now;
539 |
540 | if(self.totalDelta >= 500)
541 | {
542 | // caused by going to another tab, screensavers, etc.
543 | // just skip game logic
544 | //console.log("logic skip");
545 | self.totalDelta = 0;
546 | }
547 |
548 | //var t = Date.now();
549 | while(self.totalDelta >= level.physics.timePerTick)
550 | {
551 | self.tick(self);
552 | self.totalDelta -= level.physics.timePerTick;
553 | }
554 | //if(Date.now() - t > 5) console.log(Date.now() - t);
555 | //console.timeEnd(1);
556 |
557 |
558 | //var t = Date.now();
559 | self.renderer.redraw();
560 |
561 | self.drawHooks.forEach(function(f)
562 | {
563 | f.call(self.level, self);
564 | });
565 | //if(Date.now() - t > 10) console.log(Date.now() - t);
566 |
567 | requestAnimationFrame(function() { doTick(self); });
568 | };
569 |
570 | GameEngine.prototype.tick = function(self)
571 | {
572 | var physics = self.level.physics,
573 | keysPressed = self.keyboard.keyWasPressed;
574 |
575 |
576 | // TODO: tickFunction.call(something, self);
577 | self.level.tickFunction(self);
578 |
579 | self.objects.forEach(doTick);
580 |
581 | function doTick(obj)
582 | {
583 | if(obj.tickFunction)
584 | {
585 | obj.tickFunction.call(obj, self);
586 | }
587 |
588 | if(obj.bitmap && obj.trigger)
589 | {
590 | var hit = self.characterCollision(obj.bitmap, obj.x, obj.y);
591 |
592 | if(hit && (obj.retrigger || !obj.triggered))
593 | {
594 | obj.trigger.call(obj, self);
595 | }
596 |
597 | obj.triggered = hit;
598 | }
599 | }
600 |
601 | self.tickCount++;
602 |
603 | if(self.dead)
604 | {
605 | return;
606 | }
607 |
608 | if(!keysPressed[KEY_LEFT] !== !keysPressed[KEY_RIGHT])
609 | {
610 | var didMove;
611 |
612 | self.isMoving = true;
613 |
614 | if(keysPressed[KEY_LEFT])
615 | {
616 | didMove = !self.moveCharacterRight(-physics.moveSpeed);
617 | self.direction = LEFT;
618 | }
619 | else if(keysPressed[KEY_RIGHT])
620 | {
621 | didMove = !self.moveCharacterRight(physics.moveSpeed);
622 | self.direction = RIGHT;
623 | }
624 | }
625 | else
626 | {
627 | self.isMoving = false;
628 | }
629 |
630 | // current jump physics:
631 | // - Jump causes an instant speed upwards
632 | // - While the character is in the air, this speed decreases (aka gravity)
633 | // - While the character is in the air and space is still pressed,
634 | // gravity is reduced (for a limited amount of time)
635 |
636 | // Note: On top of that, fall speed is capped
637 |
638 | if(self.fallingState === NOT_FALLING)
639 | {
640 | if(keysPressed[KEY_JUMP])
641 | {
642 | self.audio.play(self.level.jumpMusic1, false, false, .6);
643 |
644 | self.fallingState = IN_JUMP;
645 | self.canJump = true;
646 | self.vspeed = physics.jumpInitialSpeed;
647 | self.jumpTicks = physics.jumpTicks;
648 | }
649 |
650 | // see if character fell off a platform:
651 | // try to move the character down. If it works, he fell
652 | // off a platform. Otherwise this does nothing
653 | if(!self.moveCharacterDown(1))
654 | {
655 | // player is falling (not jumping), but can jump once more
656 | self.fallingState = FALLING;
657 | self.canJump = true;
658 | }
659 | }
660 | else
661 | {
662 | if(self.fallingState === IN_JUMP)
663 | {
664 | // reduced gravity while space is pressed
665 | self.vspeed += physics.jumpGravity;
666 | self.jumpTicks--;
667 |
668 | if(!keysPressed[KEY_JUMP] || self.jumpTicks === 0)
669 | {
670 | self.fallingState = FALLING;
671 | }
672 | }
673 | else // FALLING
674 | {
675 | if(self.canJump && keysPressed[KEY_JUMP])
676 | {
677 | self.audio.play(self.level.jumpMusic2, false, false, .6);
678 |
679 | self.fallingState = IN_JUMP;
680 | self.canJump = false;
681 | self.vspeed = physics.jumpInitialSpeed / 1.5;
682 | self.jumpTicks = physics.jumpTicks;
683 | }
684 | else
685 | {
686 | //self.keysPressed[KEY_JUMP] = false;
687 | }
688 |
689 | if(self.vspeed < physics.fallSpeedCap)
690 | {
691 | // "normal" gravity
692 | self.vspeed += physics.fallGravity;
693 | }
694 | else
695 | {
696 | self.vspeed = physics.fallSpeedCap;
697 | }
698 | }
699 |
700 |
701 | // IN_JUMP or FALLING
702 | var blocked = self.moveCharacterDown(Math.roundInfinity(self.vspeed));
703 |
704 | if(blocked)
705 | {
706 | if(self.vspeed > 0)
707 | {
708 | // landed on the ground
709 | self.fallingState = NOT_FALLING;
710 | self.vspeed = 0;
711 |
712 | // don't jump again if space is still pressed
713 | keysPressed[KEY_JUMP] = false;
714 | }
715 | else
716 | {
717 | // character hit his head under a platform
718 | self.vspeed = 0;
719 | }
720 | }
721 | }
722 |
723 |
724 | // debug
725 | //dbg_log(["NOT_FALLING", "IN_JUMP", "FALLING"][self.fallingState]);
726 | };
727 |
728 | GameEngine.prototype.addDrawHook = function(f)
729 | {
730 | this.drawHooks.push(f);
731 | };
732 |
733 |
734 | // move the character n pixels, detecting collisions
735 | // returns true if character has been blocked by something
736 | GameEngine.prototype.moveCharacterRight = function(x)
737 | {
738 | // Note: This will fail for large movements
739 | // (|x| > size of the char), but makes it much faster.
740 | // Large movements could be considered teleportation anyways
741 | if(!this.charBitmap.compareMany(this.blockingObjects, this.posX + x, this.posY))
742 | {
743 | this.posX += x;
744 | return false;
745 | }
746 |
747 | // The character collided, find the collision point
748 | // using a safe approach
749 | var dx = Math.sign(x);
750 |
751 | while(x)
752 | {
753 | // We could safe us one comparison here, because it
754 | // has already been done above
755 | this.posX += dx;
756 | x -= dx;
757 |
758 | if(this.charBitmap.compareMany(this.blockingObjects, this.posX, this.posY))
759 | {
760 | // undo last movement if character is "inside" of something
761 | this.posX -= dx;
762 |
763 | return true;
764 | }
765 | }
766 |
767 | return false;
768 | };
769 |
770 | // same as the above function
771 | GameEngine.prototype.moveCharacterDown = function(y)
772 | {
773 | if(!this.charBitmap.compareMany(this.blockingObjects, this.posX, this.posY + y))
774 | {
775 | this.posY += y;
776 | return false;
777 | }
778 |
779 | var dy = Math.sign(y);
780 |
781 | while(y)
782 | {
783 | this.posY += dy;
784 | y -= dy;
785 |
786 | if(this.charBitmap.compareMany(this.blockingObjects, this.posX, this.posY))
787 | {
788 | this.posY -= dy;
789 |
790 | return true;
791 | }
792 | }
793 |
794 | return false;
795 | };
796 |
797 | GameEngine.prototype.characterCollision = function(bitmap, bx, by)
798 | {
799 | if(bx > this.posX + this.level.characterWidth ||
800 | by > this.posY + this.level.characterHeight ||
801 | bx + bitmap.width < this.posX ||
802 | by + bitmap.height < this.posY)
803 | {
804 | // not necessary, but avoids a bunch of calculations
805 | return false;
806 | }
807 |
808 | return this.charBitmap.compare(bitmap, Math.round(bx - this.posX), Math.round(by - this.posY));
809 | };
810 |
811 |
812 |
813 | // Move a blocking object vertically
814 | // Things that can happen:
815 | // - The character is next to the object -> Push the character
816 | // - The character is standing on the object -> Move him vertically with the platform
817 | GameEngine.prototype.moveObjectRight = function(object, x)
818 | {
819 | var dx = Math.sign(x),
820 | characterIsOntop = false;
821 |
822 | // How to determine if the character is standing on an object:
823 | // Move the character down (or the object up), by 1px.
824 | // If they collide, he's standing on it
825 | if(this.characterCollision(object.bitmap, object.x, object.y - 1))
826 | {
827 | characterIsOntop = true;
828 | }
829 |
830 | while(x)
831 | {
832 | x -= dx;
833 | object.x += dx;
834 |
835 | if(characterIsOntop)
836 | {
837 | this.moveCharacterRight(dx);
838 | }
839 | else if(this.characterCollision(object.bitmap, object.x, object.y))
840 | {
841 | // push the character right or left
842 | if(this.moveCharacterRight(dx))
843 | {
844 | // the character has been blocked by an object on the other side
845 | // this should kill him
846 | console.log("crushed");
847 | this.die();
848 | }
849 | }
850 | }
851 | };
852 |
853 | // Move a blocking object down
854 | // Things that can happen:
855 | // - The player is standing on the object -> move the character up with it
856 | GameEngine.prototype.moveObjectDown = function(object, y)
857 | {
858 | if(y >= 0)
859 | {
860 | var characterIsOntop = false;
861 |
862 | // How to determine if the character is standing on an object:
863 | // Move the character down (or the object up), by 1px.
864 | // If they collide, he's standing on it
865 | if(this.characterCollision(object.bitmap, object.x, object.y - 1))
866 | {
867 | characterIsOntop = true;
868 | }
869 |
870 | while(y)
871 | {
872 | object.y++;
873 | y--;
874 |
875 | if(characterIsOntop)
876 | {
877 | if(this.moveCharacterDown(1))
878 | {
879 | // the object that the character is standing on
880 | // went through another object. Nothing has to
881 | // be done
882 | }
883 | }
884 | else if(this.characterCollision(object.bitmap, object.x, object.y))
885 | {
886 | if(this.moveCharacterDown(1))
887 | {
888 | console.log("crushed");
889 | this.die();
890 | }
891 | }
892 | }
893 | }
894 | else
895 | {
896 | while(y)
897 | {
898 | y++;
899 | object.y--;
900 |
901 | if(this.characterCollision(object.bitmap, object.x, object.y))
902 | {
903 | // push the character up
904 | if(this.moveCharacterDown(-1))
905 | {
906 | // the character has been blocked by an object on the other side
907 | // this should kill him
908 | console.log("crushed");
909 | this.die();
910 | }
911 | }
912 | }
913 | }
914 | };
915 |
916 |
--------------------------------------------------------------------------------