├── 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 | 23 | 24 |
25 | Show invisible elements 26 | Auto reload 27 | 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 | 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 | 24 |
25 |

26 |     

27 |     Default keys (click to change):
28 |
29 | 30 | 37 | 44 |
31 | Left Arrow - Move left
32 |
33 | Space - Jump
34 |
35 | R - Restart
36 |
38 | Right Arrow - Move right
39 |
40 | T - Shoot
41 |
42 | M - Mute sound 43 |
45 |
46 |
47 | 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 | --------------------------------------------------------------------------------