├── .gitignore ├── AUTHORS ├── README.md ├── config.js ├── gfx ├── player.png ├── terrain.png └── trees.png ├── index.html ├── main.js └── src ├── audio.js ├── engine.js ├── input.js ├── noise.js ├── scene.js ├── ui.js └── utils.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.sw* 2 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Anders Evenrud 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Top-down 2D tile based game engnine with infinite procedural chunk generation using only browser JavaScript. 2 | 3 | **This is just an experiment and not complete in any way** 4 | 5 | ## Installation 6 | Just clone and open _index.html_ in your browser. 7 | 8 | ## Compability 9 | Latest _Chrome_ and _Firefox_ works fine (Chrome has better performance) 10 | 11 | ## Controls 12 | * `W` Forward 13 | * `A` Strafe Left 14 | * `S` Backward 15 | * `D` Strafe Right 16 | * `SHIFT` Hold to run 17 | * `MOUSE` Look around 18 | * `MWHEEL` Change weapon 19 | * `LMB` Shoot/Action 20 | 21 | ### Debugging controls 22 | * `1` Toggle tile overlay 23 | * `2` Toggle chunk overlay 24 | * `3` Toggle data overlay 25 | * `7` Toggle cheat mode (noclip, speedup, disable stats) 26 | * `8` Toggle metadata and bounding overlays 27 | * `9` Toggle UI 28 | 29 | ## Configuration 30 | 31 | Game config is located in `main.js` 32 | 33 | Engine config is located in `config.js` 34 | 35 | You can supply these arguments in the URL: 36 | * `seed` Seed string 37 | * `x` Starting X position 38 | * `y` Starting Y position 39 | 40 | **Example:** `?seed=foo&x=1000` 41 | 42 | ## Features 43 | * Infinite 2D procedural tile generation using Perlin and Simplex noise 44 | * Map Generation using seeds 45 | * Chunk/Region based rendering 46 | * Character controls and weapons 47 | * Collision detection 48 | * Animations 49 | 50 | You can see it in action on my YouTube channel
51 | 52 | ## TODO 53 | Lots of stuff... 54 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * @package Game 3 | * @author Anders Evenrud 4 | */ 5 | "use strict"; 6 | 7 | var Game = window.Game || {}; // Namespace 8 | 9 | (function() { 10 | 11 | Game.Config = { 12 | Player : { 13 | sx : 19.666, 14 | sy : 32, 15 | rot : 90 16 | }, 17 | 18 | Textures : { 19 | terrain : { 20 | src : 'gfx/terrain.png', 21 | sx : 32, 22 | sy : 32, 23 | ids : ['water', 'grass', 'dirt', 'sand', 'gravel', 'stone', 'lava', 'desert'] 24 | }, 25 | 26 | player : { 27 | src : 'gfx/player.png', 28 | sx : 19.666, 29 | sy : 32, 30 | ids : ['idle'], 31 | animations: { 32 | walk : { 33 | frames : [0, 5], 34 | duration : 100 35 | }, 36 | run : { 37 | frames : [0, 5], 38 | duration : 50 39 | } 40 | } 41 | }, 42 | 43 | trees : { 44 | src : 'gfx/trees.png', 45 | sx : 66, 46 | sy : 70, 47 | cx : 33, 48 | cy : 65, 49 | ids : ['palm'] 50 | } 51 | }, 52 | 53 | Weapons : [ 54 | { 55 | name : 'Pistol', 56 | ammo : 1000000, 57 | bullet : { 58 | w : 4, 59 | h : 4, 60 | speed : 50, 61 | life : 2000, 62 | color : "#ff0000" 63 | } 64 | }, 65 | { 66 | name : 'Rifle', 67 | ammo : 1000000, 68 | bullet : { 69 | w : 3, 70 | h : 3, 71 | speed : 80, 72 | life : 3000, 73 | color : "#0000ff" 74 | } 75 | } 76 | ], 77 | 78 | Sounds : { 79 | walkSand : { 80 | src : 'walk_sand', 81 | speed : 1.0 82 | }, 83 | runSand : { 84 | src : 'walk_sand', 85 | speed : 2.0 86 | }, 87 | swimWater: { 88 | src : 'swim_water', 89 | speed : 1.0 90 | }, 91 | shootPistol : { 92 | src : 'shot_pistol', 93 | speed : 1.0 94 | }, 95 | shootRifle : { 96 | src : 'shoot_pistol', 97 | speed : 2.0 98 | } 99 | } 100 | }; 101 | 102 | })(); 103 | 104 | -------------------------------------------------------------------------------- /gfx/player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andersevenrud/TileGame/b32933880ed202d6672a0351f788e130147c1c59/gfx/player.png -------------------------------------------------------------------------------- /gfx/terrain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andersevenrud/TileGame/b32933880ed202d6672a0351f788e130147c1c59/gfx/terrain.png -------------------------------------------------------------------------------- /gfx/trees.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andersevenrud/TileGame/b32933880ed202d6672a0351f788e130147c1c59/gfx/trees.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | Survivor 10 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * @package Game 3 | * @author Anders Evenrud 4 | */ 5 | "use strict"; 6 | 7 | var Game = window.Game || {}; // Namespace 8 | 9 | (function() { 10 | 11 | function getQueryVariable(variable) { 12 | var query = window.location.search.substring(1); 13 | var vars = query.split('&'); 14 | for (var i = 0; i < vars.length; i++) { 15 | var pair = vars[i].split('='); 16 | if (decodeURIComponent(pair[0]) == variable) { 17 | return decodeURIComponent(pair[1]); 18 | } 19 | } 20 | return null; 21 | } 22 | 23 | window.onload = function() { 24 | if ( !Game.Engine ) { 25 | throw "Cannot run: No scripts loaded ?!"; 26 | } 27 | 28 | var x = getQueryVariable('x'); 29 | var y = getQueryVariable('y'); 30 | 31 | Game.Engine.initialize().run({ 32 | world : { 33 | seed : getQueryVariable('seed') || 'default' 34 | }, 35 | 36 | init : { 37 | x : x ? (x >> 0) : null, 38 | y : y ? (y >> 0) : null 39 | } 40 | 41 | }); 42 | }; 43 | 44 | })(); 45 | 46 | -------------------------------------------------------------------------------- /src/audio.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * @package Game 3 | * @author Anders Evenrud 4 | */ 5 | "use strict"; 6 | 7 | var Game = window.Game || {}; // Namespace 8 | 9 | (function() { 10 | 11 | var audio_supported = false;//!!document.createElement('audio').canPlayType ? document.createElement('audio') : null; 12 | var SUPPORT_AUDIO = (!!audio_supported); 13 | var SUPPORT_AUDIO_OGG = (audio_supported && !!audio_supported.canPlayType('audio/ogg; codecs="vorbis')); 14 | var SUPPORT_AUDIO_MP3 = (audio_supported && !!audio_supported.canPlayType('audio/mpeg')); 15 | var SUPPORT_AUDIO_WAV = (audio_supported && !!audio_supported.canPlayType('audio/wav; codecs="1"')); 16 | 17 | var AudioHandler = function() { 18 | var format = null; 19 | if ( SUPPORT_AUDIO ) { 20 | format = SUPPORT_AUDIO_OGG ? 'ogg' : (SUPPORT_AUDIO_MP3 ? 'mp3' : (SUPPORT_AUDIO_WAV ? 'wav' : null)); 21 | } 22 | 23 | this.enabled = format !== null; 24 | this.format = format; 25 | this.sounds = {}; 26 | this.songs = {}; 27 | this.song = null; 28 | this.volume = { 29 | sfx : 1.0, 30 | ambient : 0.4, 31 | music : 0.5 32 | }; 33 | 34 | if ( !this.enabled ) console.warn('Audio not enabled :-('); 35 | }; 36 | 37 | AudioHandler.prototype.SCREEN_MAIN = 0; 38 | AudioHandler.prototype.SCREEN_GAME = 1; 39 | 40 | AudioHandler.prototype.init = function(engine) { 41 | console.group("AudioHandler::init()", "AudioHandler handling initialized"); 42 | console.log("Supported", SUPPORT_AUDIO); 43 | console.log("Enabled", this.enabled); 44 | console.log("Format", this.format); 45 | console.groupEnd(); 46 | }; 47 | 48 | AudioHandler.prototype.load = function(sounds, callback) { 49 | if ( this.enabled ) { 50 | var lib = {}; 51 | 52 | var i, s, a, fn; 53 | for ( i in sounds ) { 54 | if ( sounds.hasOwnProperty(i) ) { 55 | fn = sounds[i].src + '.' + this.format; 56 | console.log('AudioHandler::load()', 'Adding sound', i, fn); 57 | 58 | a = new Audio(fn); 59 | 60 | /*a.addEventListener('canplaythrough', function(ev){ 61 | console.info('---', 'loaded audio async', this); 62 | }, false );*/ 63 | 64 | a.preload = 'auto'; 65 | a.load(); 66 | 67 | lib[i] = { 68 | clip : a, 69 | src : fn, 70 | opts : sounds[i] 71 | }; 72 | } 73 | } 74 | 75 | this.sounds = lib; 76 | } 77 | 78 | callback(true); // Async 79 | }; 80 | 81 | AudioHandler.prototype.destroy = function(engine) { 82 | }; 83 | 84 | AudioHandler.prototype.update = function() { 85 | }; 86 | 87 | AudioHandler.prototype.play = function(snd, ambient) { 88 | if ( !this.sounds[snd] ) return false; 89 | 90 | var a = this.sounds[snd].clip.cloneNode(true); 91 | a.currentTime = 0; 92 | a.volume = ambient ? this.volume.ambient : this.volume.sfx; 93 | a.play(); 94 | 95 | return true; 96 | }; 97 | 98 | AudioHandler.prototype.musicAction = function(act) { 99 | switch ( act ) { 100 | case 'pause' : 101 | if ( this.song ) this.song.stop(); 102 | break; 103 | case 'start' : 104 | if ( this.song ) this.song.play(); 105 | break; 106 | case 'stop' : 107 | if ( this.song ) { this.song.stop(); this.song.currentTime = 0; } 108 | break; 109 | case 'next' : 110 | break; 111 | case 'prev' : 112 | break; 113 | 114 | default : 115 | if ( this.songs[act] ) { 116 | this.song.src = this.songs[act]; 117 | this.song.volume = this.volume.music; 118 | this.song.currentTime = 0; 119 | this.song.play(); 120 | } 121 | break; 122 | } 123 | }; 124 | 125 | AudioHandler.prototype.setVolume = function(m, v) { 126 | this.volume[m] = v; 127 | 128 | if ( this.song ) this.song.volume = this.volume.music; 129 | }; 130 | 131 | Game.Audio = new AudioHandler(); 132 | 133 | })(); 134 | 135 | -------------------------------------------------------------------------------- /src/engine.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * @package Game 3 | * @author Anders Evenrud 4 | */ 5 | "use strict"; 6 | 7 | var Game = window.Game || {}; // Namespace 8 | 9 | (function() { 10 | var requestAnimFrame = (function() { 11 | return window.requestAnimationFrame || 12 | window.webkitRequestAnimationFrame || 13 | window.mozRequestAnimationFrame || 14 | window.oRequestAnimationFrame || 15 | window.msRequestAnimationFrame || 16 | 17 | function (callback, element) { 18 | window.setTimeout(callback, 1000/60); 19 | }; 20 | })(); 21 | 22 | ///////////////////////////////////////////////////////////////////////////// 23 | // VARIABLES 24 | ///////////////////////////////////////////////////////////////////////////// 25 | 26 | var canvasWidth = 1280; // Detected 27 | var canvasHeight = 720; // Detected 28 | var running = false; 29 | var renderCanvas = null; 30 | var renderContext = null; 31 | var onShutdown = null; 32 | var origTitle = ""; 33 | var titleInterval = null; 34 | 35 | var EngineInstance; 36 | var LastFrameTime = 0; 37 | var TimeSinceLastFrame = 0; 38 | var FPS = 0; 39 | 40 | ///////////////////////////////////////////////////////////////////////////// 41 | // ENGINE 42 | ///////////////////////////////////////////////////////////////////////////// 43 | 44 | /** 45 | * @class 46 | */ 47 | var Engine = function() { 48 | if ( !Game.Config ) { 49 | throw ("Cannot run: no Config found!"); 50 | } 51 | if ( !Game.Utils ) { 52 | throw ("Cannot run: no Utils found!"); 53 | } 54 | if ( !Game.Input ) { 55 | throw ("Cannot run: no Input found!"); 56 | } 57 | if ( !Game.Scene ) { 58 | throw ("Cannot run: no Scene found!"); 59 | } 60 | if ( !Game.UI ) { 61 | throw ("Cannot run: no UI found!"); 62 | } 63 | 64 | console.log("Engine::Engine()", "Initializing"); 65 | 66 | origTitle = document.title; 67 | renderCanvas = document.getElementById('canvas'); 68 | renderContext = renderCanvas.getContext('2d'); 69 | 70 | this.canvas = document.createElement('canvas'); 71 | this.context = this.canvas.getContext('2d'); 72 | 73 | if ( !this.canvas ) { 74 | throw ("Cannot run: no DOMElement<#canvas> found!"); 75 | } 76 | if ( !this.context ) { 77 | throw ("Cannot run: no CanvasContext found!"); 78 | } 79 | 80 | var self = this; 81 | window.onresize = function() { 82 | self.resize(window.innerWidth, window.innerHeight); 83 | }; 84 | 85 | window.onresize(); 86 | 87 | window.onunload = function() { 88 | self.destroy(); 89 | }; 90 | 91 | window.oncontextmenu = function(ev) { 92 | ev = ev || window.event; 93 | ev.preventDefault(); 94 | return false; 95 | }; 96 | window.onselectstart = function(ev) { 97 | ev = ev || window.event; 98 | ev.preventDefault(); 99 | return false; 100 | }; 101 | }; 102 | 103 | /** 104 | * Run engine 105 | */ 106 | Engine.prototype.run = function(params) { 107 | if ( running ) return; 108 | running = true; 109 | 110 | console.log("Engine::run()", "Main loop starting"); 111 | 112 | Game.Audio.init(this); 113 | Game.Input.init(this); 114 | Game.UI.init(this); 115 | 116 | titleInterval = setInterval(function() { 117 | document.title = origTitle + " (" + (FPS >> 0) + " fps)"; 118 | }, 1000); 119 | 120 | var self = this; 121 | Game.Scene.create(this, params, function() { 122 | self.draw(); 123 | }); 124 | }; 125 | 126 | /** 127 | * Stop running 128 | */ 129 | Engine.prototype.stop = function() { 130 | if ( !running ) return; 131 | var self = this; 132 | 133 | console.warn('Shutting down gracefully...'); 134 | running = false; 135 | onShutdown = function() { 136 | self.context.fillStyle = "#000"; 137 | self.context.fillRect(0, 0, canvasWidth, canvasHeight); 138 | renderContext.drawImage(self.canvas, 0, 0); 139 | self.destroy(); 140 | }; 141 | }; 142 | 143 | /** 144 | * Destroy (quit) engine 145 | */ 146 | Engine.prototype.destroy = function() { 147 | Game.Audio.destroy(this); 148 | Game.Input.destroy(this); 149 | Game.Scene.destroy(this); 150 | Game.UI.destroy(this); 151 | 152 | window.onresize = null; 153 | window.onload = null; 154 | window.onunload = null; 155 | window.oncontextmenu = null; 156 | window.onselectstart = null; 157 | 158 | if ( titleInterval ) { 159 | clearInterval(titleInterval); 160 | titleInterval = null; 161 | } 162 | 163 | console.warn('Game.Engine', 'Shut down!'); 164 | }; 165 | 166 | /** 167 | * Render a frame 168 | */ 169 | Engine.prototype.draw = function(timeSinceLastFrame) { 170 | timeSinceLastFrame = timeSinceLastFrame || 0; 171 | if ( !running ) { 172 | if ( onShutdown !== null ) onShutdown(); 173 | return; 174 | } 175 | 176 | this.context.clearRect(0, 0, canvasWidth, canvasHeight); 177 | 178 | if ( Game.Scene.render(timeSinceLastFrame, this, Game.Input, FPS.toFixed(1)) ) { 179 | running = false; 180 | } 181 | Game.UI.render(LastFrameTime, this, Game.Input); 182 | 183 | Game.Audio.update(this); 184 | Game.Input.update(this); 185 | 186 | renderContext.drawImage(this.canvas, 0, 0); 187 | 188 | var self = this; 189 | requestAnimFrame(function() { 190 | var now = new Date(); 191 | TimeSinceLastFrame = LastFrameTime ? (now - LastFrameTime) : 0; 192 | EngineInstance.draw(TimeSinceLastFrame); 193 | 194 | LastFrameTime = now; 195 | FPS = 1/(TimeSinceLastFrame / 1000); 196 | }); 197 | }; 198 | 199 | /** 200 | * Resize instance 201 | */ 202 | Engine.prototype.resize = function(w, h) { 203 | if ( w && h ) { 204 | this.canvas.width = w; 205 | this.canvas.height = h; 206 | renderCanvas.width = w; 207 | renderCanvas.height = h; 208 | } 209 | 210 | canvasWidth = parseInt(this.canvas.width, 10); 211 | canvasHeight = parseInt(this.canvas.height, 10); 212 | 213 | if ( running ) { 214 | Game.Scene.resize(this, canvasWidth, canvasHeight); 215 | Game.UI.resize(this, canvasWidth, canvasHeight); 216 | } 217 | }; 218 | 219 | /** 220 | * Get canvas (screen) width 221 | */ 222 | Engine.prototype.getCanvasWidth = function() { 223 | return canvasWidth; 224 | }; 225 | 226 | /** 227 | * Get canvas (screen) height 228 | */ 229 | Engine.prototype.getCanvasHeight = function() { 230 | return canvasHeight; 231 | }; 232 | 233 | ///////////////////////////////////////////////////////////////////////////// 234 | // MAIN 235 | ///////////////////////////////////////////////////////////////////////////// 236 | 237 | // Exports 238 | Game.Engine = { 239 | initialize : function() { 240 | if ( !EngineInstance ) { 241 | EngineInstance = new Engine(); 242 | } 243 | 244 | return EngineInstance; 245 | }, 246 | 247 | get : function() { 248 | return EngineInstance; 249 | } 250 | }; 251 | 252 | })(); 253 | 254 | -------------------------------------------------------------------------------- /src/input.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * @package Game 3 | * @author Anders Evenrud 4 | */ 5 | "use strict"; 6 | 7 | Game = window.Game || {}; 8 | 9 | (function() { 10 | 11 | var Input = function() { 12 | this.mouseButtons = [false, false, false]; 13 | this.mbuttonsPressed = [false, false, false]; 14 | this.mouseWheelValue = 0; 15 | this.keysActive = {}; 16 | this.keysPressed = {}; 17 | this.mousePosX = 0; 18 | this.mousePosY = 0; 19 | }; 20 | 21 | Input.prototype.init = function(engine) { 22 | var self = this; 23 | 24 | window.onkeydown = function(ev) { 25 | var btn = Game.Utils.keyButton(ev); 26 | if ( btn.value ) { 27 | self.keysActive[btn.value] = true; 28 | self.keysPressed[btn.value] = true; 29 | } 30 | }; 31 | 32 | window.onkeyup = function(ev) { 33 | var btn = Game.Utils.keyButton(ev); 34 | if ( self.keysActive[btn.value] ) { 35 | delete self.keysActive[btn.value]; 36 | } 37 | }; 38 | 39 | window.onmousedown = function(ev) { 40 | var btn = Game.Utils.mouseButton(ev) - 1; 41 | if ( btn >= 0 ) { 42 | self.mouseButtons[btn] = true; 43 | self.mbuttonsPressed[btn] = true; 44 | } 45 | }; 46 | 47 | window.onmouseup = function(ev) { 48 | var btn = Game.Utils.mouseButton(ev) - 1; 49 | if ( btn >= 0 ) { 50 | self.mouseButtons[btn] = false; 51 | } 52 | }; 53 | 54 | window.onmousemove = function(ev) { 55 | var pos = Game.Utils.mousePosition(ev); 56 | self.mousePosX = pos.x; 57 | self.mousePosY = pos.y; 58 | }; 59 | 60 | window.onmousewheel = function(ev) { 61 | self.mouseWheelValue = Game.Utils.mouseWheel(ev); 62 | }; 63 | 64 | document.addEventListener('DOMMouseScroll', window.onmousewheel, false); 65 | 66 | console.log("Input::init()", "Input handling initialized"); 67 | }; 68 | 69 | Input.prototype.destroy = function(engine) { 70 | document.removeEventListener('DOMMouseScroll', window.onmousewheel, false); 71 | 72 | window.onkeydown = null; 73 | window.onkeyup = null; 74 | window.onmousedown = null; 75 | window.onmouseup = null; 76 | window.onmousemove = null; 77 | window.onmousewheel = null; 78 | 79 | console.warn('Game.Input', 'Shut down!'); 80 | }; 81 | 82 | Input.prototype.update = function(engine) { 83 | this.mouseWheelValue = 0; 84 | this.keysPressed = {}; 85 | this.mbuttonsPressed = [false, false, false]; 86 | }; 87 | 88 | Input.prototype.keyDown = function(k) { 89 | return this.keysActive[k] ? true : false; 90 | }; 91 | 92 | Input.prototype.keyPressed = function(k) { 93 | return this.keysPressed[k] ? true : false; 94 | }; 95 | 96 | Input.prototype.mouseDown = function(b) { 97 | return this.mouseButtons[b]; 98 | }; 99 | 100 | Input.prototype.mousePressed = function(b) { 101 | return this.mbuttonsPressed[b]; 102 | }; 103 | 104 | Input.prototype.mouseWheel = function() { 105 | return this.mouseWheelValue; 106 | }; 107 | 108 | Input.prototype.mousePosition = function() { 109 | return {x: this.mousePosX, y: this.mousePosY}; 110 | }; 111 | 112 | Game.Input = new Input(); 113 | 114 | })(); 115 | 116 | -------------------------------------------------------------------------------- /src/noise.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * @package Game 3 | * @author Anders Evenrud 4 | */ 5 | "use strict"; 6 | 7 | var Game = window.Game || {}; // Namespace 8 | 9 | (function() { 10 | 11 | /** 12 | * @class 13 | */ 14 | var Simplex = function(seed) { 15 | var list = [151,160,137,91,90,15,131,13,201,95,96,53,194,233,7,225,140,36, 16 | 103,30,69,142,8,99,37,240,21,10,23,190,6,148,247,120,234,75,0, 17 | 26,197,62,94,252,219,203,117,35,11,32,57,177,33,88,237,149,56, 18 | 87,174,20,125,136,171,168,68,175,74,165,71,134,139,48,27,166, 19 | 77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55, 20 | 46,245,40,244,102,143,54,65,25,63,161,1,216,80,73,209,76,132, 21 | 187,208,89,18,169,200,196,135,130,116,188,159,86,164,100,109, 22 | 198,173,186,3,64,52,217,226,250,124,123,5,202,38,147,118,126, 23 | 255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42,223,183, 24 | 170,213,119,248,152,2,44,154,163,70,221,153,101,155,167,43,172, 25 | 9,129,22,39,253,19,98,108,110,79,113,224,232,178,185,112,104, 26 | 218,246,97,228,251,34,242,193,238,210,144,12,191,179,162,241, 27 | 81,51,145,235,249,14,239,107,49,192,214,31,181,199,106,157,184, 28 | 84,204,176,115,121,50,45,127,4,150,254,138,236,205,93,222,114, 29 | 67,29,24,72,243,141,128,195,78,66,215,61,156,180]; 30 | 31 | var _simple = function() { 32 | var i = 0, l = list.length, field; 33 | var p = []; 34 | for(i; i < l; i++) { 35 | field = list[i]; 36 | p[i] = field; 37 | p[256 + i] = field; 38 | } 39 | 40 | return p; 41 | }; 42 | 43 | var _seeded = function() { 44 | var s = new Seeder(seed); 45 | var p = []; 46 | 47 | var lst = list, i, k, l; 48 | 49 | for ( i = 0; i < 256; i++ ) { 50 | lst[i] = i; 51 | } 52 | 53 | for ( i = 0; i < 256; i++ ) { 54 | k = s.randomIntRange(0, 256 - i) + i; 55 | l = lst[i]; 56 | lst[i] = lst[k]; 57 | lst[k] = l; 58 | lst[i + 256] = lst[i]; 59 | } 60 | 61 | for(i = 0; i < 256; i++) { 62 | p[256 + i] = p[i] = lst[i]; 63 | } 64 | 65 | return p; 66 | }; 67 | 68 | 69 | this.p = seed ? _seeded() : _simple(); 70 | }; 71 | 72 | Simplex.prototype.lerp = function(t, a, b) { 73 | return a + t * (b - a); 74 | }; 75 | 76 | Simplex.prototype.fade = function(t) { 77 | return t * t * t * (t * (t * 6 - 15) + 10); 78 | }; 79 | 80 | Simplex.prototype.grad = function(hash,x,y,z) { 81 | var h = hash & 15; 82 | var u = h < 8 ? x : y; 83 | var v = h < 4 ? y : h == 12 || h == 14 ? x : z; 84 | 85 | return ((h & 1) == 0 ? u : -u) + ((h & 2) == 0 ? v : -v); 86 | }; 87 | 88 | Simplex.prototype.snoise = function(x,y,z) { 89 | var floopX = Math.floor(x) & 255; 90 | var floopY = Math.floor(y) & 255; 91 | var floopZ = Math.floor(z) & 255; 92 | x -= Math.floor(x); 93 | y -= Math.floor(y); 94 | z -= Math.floor(z); 95 | 96 | var u = this.fade(x); 97 | var v = this.fade(y); 98 | var w = this.fade(z); 99 | 100 | var a = this.p[floopX] + floopY; 101 | var aa = this.p[a] + floopZ; 102 | var ab = this.p[a + 1] + floopZ; 103 | var b = this.p[floopX + 1] + floopY; 104 | var ba = this.p[b] + floopZ; 105 | var bb = this.p[b + 1] + floopZ; 106 | 107 | var pAA = this.p[aa]; 108 | var pAB = this.p[ab]; 109 | var pBA = this.p[ba]; 110 | var pBB = this.p[bb]; 111 | var pAA1 = this.p[aa + 1]; 112 | var pAB1 = this.p[ab + 1]; 113 | var pBA1 = this.p[ba + 1]; 114 | var pBB1 = this.p[bb + 1]; 115 | 116 | var gradAA = this.grad(pAA,x,y,z); 117 | var gradBA = this.grad(pBA, x-1,y,z); 118 | var gradAB = this.grad(pAB,x,y-1,z); 119 | var gradBB = this.grad(pBB,x-1,y-1,z); 120 | var gradAA1 = this.grad(pAA1,x,y,z-1); 121 | var gradBA1 = this.grad(pBA1,x-1,y,z-1); 122 | var gradAB1 = this.grad(pAB1,x,y-1,z-1); 123 | var gradBB1 = this.grad(pBB1,x-1,y-1,z-1); 124 | 125 | return this.lerp(w, 126 | this.lerp(v, this.lerp(u, gradAA, gradBA), this.lerp(u, gradAB, gradBB)), 127 | this.lerp(v, this.lerp(u, gradAA1, gradBA1), this.lerp(u,gradAB1,gradBB1)) 128 | ); 129 | }; 130 | 131 | 132 | /** 133 | * @class 134 | */ 135 | function Seeder(seed) { 136 | this.seed = seed; 137 | } 138 | 139 | Seeder.prototype.randomInt = function() { 140 | return this.gen(); 141 | }; 142 | 143 | Seeder.prototype.random = function() { 144 | return ( this.gen() / 2147483647 ); 145 | }; 146 | 147 | Seeder.prototype.randomIntRange = function(min, max) { 148 | min -= .4999; 149 | max += .4999; 150 | return Math.round(min + ((max - min) * this.random())); 151 | }; 152 | 153 | Seeder.prototype.randomRange = function(min, max) { 154 | return min + ((max - min) * this.random()); 155 | }; 156 | 157 | Seeder.prototype.gen = function() { 158 | return this.seed = (this.seed * 16807) % 2147483647; 159 | }; 160 | 161 | // 162 | // Exports 163 | // 164 | Game.SimplexNoise = Simplex; 165 | 166 | })(); 167 | 168 | -------------------------------------------------------------------------------- /src/scene.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * @package Game 3 | * @author Anders Evenrud 4 | */ 5 | "use strict"; 6 | 7 | var Game = window.Game || {}; // Namespace 8 | 9 | (function() { 10 | var TILE_SIZE = 32; 11 | var CHUNK_SIZE = 512; 12 | var CHUNK_TILES = CHUNK_SIZE / TILE_SIZE; 13 | var CHUNK_ALIVE = 5000; //(5 * 60) * 1000; 14 | var CHUNK_ALIVE_CHECK = 10000; //(1 * 60) * 1000; 15 | 16 | var PLAYER_STAMINA_TICK = 300; 17 | var PLAYER_HUNGER_TICK = 5000; 18 | 19 | var DEFAULT_GEN_MIN = 0; 20 | var DEFAULT_GEN_MAX = 32; 21 | var DEFAULT_GEN_SEED = 'default';//0.12312313512341; 22 | var DEFAULT_GEN_QUAL = 128.0; 23 | var DEFAULT_PLAYER_X = 0; 24 | var DEFAULT_PLAYER_Y = 0; 25 | 26 | var MOVE_SPEED = 5.0; 27 | var RUN_SPEED = 8.0; 28 | var SWIM_SPEED = 1.0; 29 | 30 | ///////////////////////////////////////////////////////////////////////////// 31 | // VARIABLES 32 | ///////////////////////////////////////////////////////////////////////////// 33 | 34 | var canvasWidth = 0; 35 | var canvasHeight = 0; 36 | 37 | var textureSet = null; 38 | var player = null; 39 | var map = null; 40 | var ui = null; 41 | 42 | var chunkCheckCounter = 0; 43 | var debugTileOverlay = false; 44 | var debugChunkOverlay = false; 45 | var debugOverlay = false; 46 | var debugDetail = false; 47 | var cheatMode = false; 48 | 49 | ///////////////////////////////////////////////////////////////////////////// 50 | // HELPERS 51 | ///////////////////////////////////////////////////////////////////////////// 52 | 53 | function hashPair(x, y) { 54 | return (y << 16) ^ x; //((x + y)*(x + y + 1)/2) + y; 55 | } 56 | 57 | ///////////////////////////////////////////////////////////////////////////// 58 | // CLASSES 59 | ///////////////////////////////////////////////////////////////////////////// 60 | 61 | /** 62 | * @class 63 | */ 64 | var CMap = (function() { 65 | 66 | var cachedChunks = 0; 67 | var chunkCache = {}; 68 | 69 | var C = function(params) { 70 | this.viewRect = {x:0, y:0, w:0, h:0}; 71 | this.xTileMin = -1; 72 | this.xTileMax = -1; 73 | this.yTileMin = -1; 74 | this.yTileMax = -1; 75 | this.xOffset = 0; 76 | this.yOffset = 0; 77 | this.visibleChunks = 0; 78 | this.generation = { 79 | min : DEFAULT_GEN_MIN, 80 | max : DEFAULT_GEN_MAX, 81 | quality : DEFAULT_GEN_QUAL, 82 | seed : DEFAULT_GEN_SEED, 83 | _delta : -1, 84 | _num : 0, 85 | _float : 0.0 86 | }; 87 | 88 | for ( var i in params ) { 89 | if ( this.generation.hasOwnProperty(i) ) { 90 | this.generation[i] = params[i]; 91 | } 92 | } 93 | 94 | this.generation._num = Game.Utils.createSeed(this.generation.seed); 95 | this.generation._float = parseFloat('0.' + Math.abs(this.generation._num)); 96 | this.generation._delta = this.generation.quality + this.generation._float; 97 | 98 | this.update(); 99 | this.tick(); 100 | 101 | console.group('CMap::CMap()', 'Map instance'); 102 | console.log('World', this.generation); 103 | console.groupEnd(); 104 | }; 105 | 106 | C.prototype.destroy = function() { 107 | console.warn('Game.Scene::CMap', 'Shut down!'); 108 | }; 109 | 110 | C.prototype.initialize = function() { 111 | this.noise = new Game.SimplexNoise(this.generation._num); 112 | }; 113 | 114 | /** 115 | * On render 116 | */ 117 | C.prototype.tick = function(timeSinceLastFrame) { 118 | this.viewRect.x = (canvasWidth / 2) + player.getX() + this.xOffset; 119 | this.viewRect.y = (canvasHeight / 2) + player.getY() + this.yOffset; 120 | 121 | this.xTileMin = Math.floor((this.viewRect.x) / CHUNK_SIZE); 122 | this.xTileMax = Math.floor((this.viewRect.x + this.viewRect.w) / CHUNK_SIZE); 123 | this.yTileMin = Math.floor((this.viewRect.y) / CHUNK_SIZE); 124 | this.yTileMax = Math.floor((this.viewRect.y + this.viewRect.h) / CHUNK_SIZE); 125 | }; 126 | 127 | /** 128 | * Update/Refresh 129 | */ 130 | C.prototype.update = function() { 131 | this.viewRect.w = canvasWidth; 132 | this.viewRect.h = canvasHeight; 133 | this.xOffset = -(canvasWidth - (CHUNK_SIZE / 2)); 134 | this.yOffset = -(canvasHeight - (CHUNK_SIZE / 2)); 135 | }; 136 | 137 | /** 138 | * Get noise for position 139 | */ 140 | C.prototype.generate = function(cx, cy, tx, ty, layer) { 141 | layer = (typeof layer === 'undefined') ? 0 : layer; 142 | var d = this.generation._delta; 143 | var s = this.generation._float; 144 | var n = this.noise.snoise( 145 | ((((cx * CHUNK_TILES) + (tx))) / s) / d, 146 | ((((cy * CHUNK_TILES) + (ty))) / s) / d, 147 | layer); 148 | /* 149 | var n = this.noise.snoise( 150 | ((((cx * CHUNK_SIZE) + (tx * TILE_SIZE))) / seed) / delta, 151 | ((((cy * CHUNK_SIZE) + (ty * TILE_SIZE))) / seed) / delta, 152 | layer)*/ 153 | 154 | return n; 155 | //return Game.Utils.clampRange(Math.floor(v), min, max); 156 | }; 157 | 158 | /** 159 | * Draw map tiles or overlays 160 | */ 161 | C.prototype.draw = function(timeSinceLastFrame, context, overlays) { 162 | var x, y, visibleChunks = 0, c; 163 | 164 | for ( y = this.yTileMin; y <= this.yTileMax; y++ ) { 165 | for ( x = this.xTileMin; x <= this.xTileMax; x++ ) { 166 | if ( overlays ) { 167 | c = this.getChunk(x, y); 168 | if ( c ) { 169 | c.drawObjects(context); 170 | } 171 | } else { 172 | c = this.drawChunk(context, x, y); 173 | if ( c !== false ) { 174 | visibleChunks++; 175 | } 176 | } 177 | } 178 | } 179 | 180 | if ( !overlays ) { 181 | this.visibleChunks = visibleChunks; 182 | } 183 | }; 184 | 185 | /** 186 | * Check if we can remove chunks from cache 187 | */ 188 | C.prototype.checkChunks = function() { 189 | var i, l, c, remove = []; 190 | for ( i in chunkCache ) { 191 | c = chunkCache[i]; 192 | if ( c.isTimedOut() && !c.inRange(this.viewRect) ) { 193 | remove.push(i); 194 | } 195 | } 196 | 197 | i = 0; 198 | l = remove.length; 199 | for ( i; i < l; i++ ) { 200 | delete chunkCache[remove[i]]; 201 | cachedChunks--; 202 | } 203 | }; 204 | 205 | /** 206 | * Clear chunks 207 | */ 208 | C.prototype.clearChunks = function() { 209 | chunkCache = {}; 210 | cachedChunks = 0; 211 | }; 212 | 213 | /** 214 | * Remove chunk 215 | */ 216 | C.prototype.deleteChunk = function(px, py) { 217 | var cname = hashPair(px, py); 218 | if ( chunkCache[cname] ) { 219 | delete chunkCache[cname]; 220 | 221 | return true; 222 | } 223 | return false; 224 | }; 225 | 226 | /** 227 | * Create chunk 228 | */ 229 | C.prototype.createChunk = function(px, py) { 230 | var cname = hashPair(px, py); 231 | if ( chunkCache[cname] ) { 232 | return chunkCache[cname]; 233 | } 234 | chunkCache[cname] = new CChunk(px, py); 235 | 236 | cachedChunks++; 237 | 238 | return chunkCache[cname]; 239 | }; 240 | 241 | /** 242 | * Draw chunk on to canvas if in range 243 | */ 244 | C.prototype.drawChunk = function(context, x, y) { 245 | var visible = this.isChunkVisible(x, y); 246 | if ( visible === false ) return false; 247 | 248 | var chunk = this.createChunk(x, y); 249 | context.drawImage(chunk.res, visible.x, visible.y); 250 | 251 | return chunk; 252 | }; 253 | 254 | /** 255 | * Check if chunk x,y is in viewable range 256 | */ 257 | C.prototype.isChunkVisible = function(x, y) { 258 | var pos = this.getChunkPosition(x, y); 259 | if ( !Game.Utils.intersectRect( // AABB 260 | {top:pos.r1.y,left:pos.r1.x,bottom:pos.r1.y+pos.r1.h,right:pos.r2.x+pos.r2.w}, 261 | {top:pos.r2.y,left:pos.r2.x,bottom:pos.r2.y+pos.r2.h,right:pos.r2.x+pos.r2.w} 262 | ) ) { 263 | return false; 264 | } 265 | 266 | return {x: pos.px, y: pos.py}; 267 | }; 268 | 269 | /** 270 | * Check if posX, posY tile is walkable 271 | */ 272 | C.prototype.isTileWalkable = function(destX, destY) { 273 | // TODO: Rect intersection on bounds instead ?! 274 | var destChunk = player.getCurrentChunk(destX, destY); 275 | var chunk = this.getChunk(destChunk.x, destChunk.y); 276 | if ( !chunk ) return false; 277 | 278 | var destTile = player.getCurrentTile(destX, destY); 279 | var tile = chunk.getTile(destTile.x, destTile.y); 280 | if ( !tile ) return false; 281 | 282 | return tile.isAccessable(); 283 | }; 284 | 285 | /** 286 | * Get chunk absolute position 287 | */ 288 | C.prototype.getChunkPosition = function(x, y) { 289 | var r1 = {x:(x * CHUNK_SIZE), y:(y * CHUNK_SIZE), w:CHUNK_SIZE, h:CHUNK_SIZE}; 290 | var r2 = this.viewRect; 291 | var px = r1.x - r2.x; //(x * CHUNK_SIZE) - viewRect.x; 292 | var py = r1.y - r2.y; //(y * CHUNK_SIZE) - viewRect.y; 293 | 294 | return {r1:r1, r2:r2, px:px, py:py}; 295 | }; 296 | 297 | /** 298 | * Get a chunk by position 299 | */ 300 | C.prototype.getChunk = function(x, y, checkVisible) { 301 | if ( checkVisible ) { 302 | var v = this.isChunkVisible(x, y); 303 | if ( this.isChunkVisible(x, y) == false ) return false; 304 | } 305 | var c = chunkCache[hashPair(x, y)]; 306 | return c; 307 | }; 308 | 309 | /** 310 | * Get cached chunk count 311 | */ 312 | C.prototype.getCachedChunks = function() { 313 | return cachedChunks; 314 | }; 315 | 316 | /** 317 | * Get visible chunk count 318 | */ 319 | C.prototype.getVisibleChunks = function() { 320 | return this.visibleChunks; 321 | }; 322 | 323 | return C; 324 | })(); 325 | 326 | /** 327 | * @class 328 | */ 329 | var CWeapon = function(id) { 330 | var w = Game.Config.Weapons[id]; 331 | for ( var i in w ) { 332 | if ( w.hasOwnProperty(i) ) { 333 | this[i] = w[i]; 334 | } 335 | } 336 | console.log('CWeapon::CWeapon()', 'Created', id, w); 337 | }; 338 | 339 | /** 340 | * @class 341 | */ 342 | var CPlayer = (function() { 343 | var bullets = []; 344 | 345 | var _removeBullets = function(remove) { 346 | var i = 0, l = remove.length; 347 | for ( i; i < l; i++ ) { 348 | bullets.splice(remove[i], 1); 349 | } 350 | }; 351 | 352 | var _updateBullets = function(timeSinceLastFrame) { 353 | var remove = []; 354 | var i = 0, l = bullets.length, b, speed; 355 | for ( i; i < l; i++ ) { 356 | b = bullets[i]; 357 | speed = (b.speed * (timeSinceLastFrame / 60)); 358 | 359 | b.x += Math.sin(b.r) * speed; 360 | b.y -= Math.cos(b.r) * speed; 361 | b.t += timeSinceLastFrame; 362 | 363 | if ( b.t >= b.life ) { 364 | remove.push(i); 365 | } 366 | 367 | bullets[i] = b; 368 | } 369 | 370 | _removeBullets(remove); 371 | }; 372 | 373 | var _renderBullets = function(timeSinceLastFrame, context) { 374 | var i = 0, l = bullets.length, b; 375 | for ( i; i < l; i++ ) { 376 | b = bullets[i]; 377 | 378 | context.fillStyle = b.color; 379 | context.fillRect(b.x + (b.w/2), b.y + (b.h/2), b.w, b.h); 380 | } 381 | }; 382 | 383 | var _createBullet = function(src, dst, bargs) { 384 | bargs = bargs || {}; 385 | 386 | // FIXME 387 | src.x = -(src.x + src.cx - map.viewRect.x) - map.xOffset; 388 | src.y = -(src.y + src.cy - map.viewRect.y) - map.yOffset; 389 | 390 | var rx = -(dst.x - (dst.w / 2) - map.viewRect.x); 391 | var ry = -(dst.y - (dst.h / 2) - map.viewRect.y); 392 | var r = Math.atan2((dst.x - src.x), - (dst.y - src.y)); 393 | r %= (Math.PI * 2); 394 | 395 | var target = { 396 | sx : src.x, 397 | sy : src.y, 398 | dx : dst.x, 399 | dy : dst.y, 400 | x : src.x, 401 | y : src.y, 402 | r : r, 403 | t : 0, 404 | 405 | w : bargs.w, 406 | h : bargs.h, 407 | speed : bargs.speed, 408 | life : bargs.life, 409 | color : bargs.color 410 | }; 411 | 412 | bullets.push(target); 413 | }; 414 | 415 | var __index = 0; 416 | 417 | var C = function(params) { 418 | console.group('CPlayer::CPlayer()', 'Created player', this.index); 419 | 420 | this.r = 0.0; 421 | this.x = (typeof params.x === 'undefined' || params.x === null) ? DEFAULT_PLAYER_X : params.x; 422 | this.y = (typeof params.y === 'undefined' || params.y === null) ? DEFAULT_PLAYER_Y : params.y; 423 | this.w = Game.Config.Player.sx; 424 | this.h = Game.Config.Player.sy; 425 | this.srot = Game.Config.Player.rot; 426 | this.moving = false; 427 | this.running = false; 428 | this.swimming = false; 429 | this.index = __index; 430 | this.weapon = 0; 431 | 432 | this.weapons = [ 433 | new CWeapon(0), 434 | new CWeapon(1) 435 | ]; 436 | 437 | this.stats = { 438 | health : 100, 439 | stamina : 100, 440 | hunger : 0, 441 | 442 | _health : 0, 443 | _stamina : 0, 444 | _hunger : 0 445 | }; 446 | 447 | console.log('Inited at position', this.x, this.y); 448 | console.log('Inited with stats', this.stats); 449 | console.groupEnd(); 450 | 451 | __index++; 452 | }; 453 | 454 | C.prototype.destroy = function() { 455 | console.warn('Game.Scene::CPlayer', 'Shut down!'); 456 | }; 457 | 458 | C.prototype.draw = function(timeSinceLastFrame, context) { 459 | var r = this.getRelPosition(); 460 | var b = this.getBoundingBox(true); 461 | 462 | if ( debugDetail ) { 463 | context.strokeStyle = "#ff0000"; 464 | context.strokeRect(r.x, r.y, this.w, this.h); 465 | } 466 | 467 | r.x -= this.getCX(); 468 | r.y -= this.getCY(); 469 | b.x -= this.getCX(); 470 | b.y -= this.getCY(); 471 | 472 | var pv = document.createElement('canvas'); 473 | pv.width = this.w; 474 | pv.height = this.h; 475 | var pc = pv.getContext('2d'); 476 | 477 | if ( player.isMoving() ) { 478 | if ( player.isRunning() ) { 479 | textureSet.tick(timeSinceLastFrame, pc, 'player', 'run', 0, 0, this.w, this.h); 480 | } else { 481 | textureSet.tick(timeSinceLastFrame, pc, 'player', 'walk', 0, 0, this.w, this.h); 482 | } 483 | } else { 484 | textureSet.drawTile(pc, 'player', 'idle', 0, 0, this.w, this.h); 485 | } 486 | Game.Utils.drawImageRot(context, pv, r.x, r.y, this.w, this.h, Game.Utils.convertToDegree(this.r) - this.srot); 487 | 488 | if ( debugDetail ) { 489 | context.strokeStyle = "#0000ff"; 490 | context.strokeRect(b.x, b.y, b.w, b.h); 491 | } 492 | 493 | _renderBullets(timeSinceLastFrame, context); 494 | }; 495 | 496 | C.prototype.tick = function(timeSinceLastFrame) { 497 | if ( !cheatMode ) { 498 | this.stats._stamina += timeSinceLastFrame; 499 | this.stats._health += timeSinceLastFrame; 500 | this.stats._hunger += timeSinceLastFrame; 501 | 502 | if ( this.stats._stamina >= PLAYER_STAMINA_TICK ) { 503 | if ( this.moving ) { 504 | if ( this.stats.stamina > 0 ) { 505 | this.stats.stamina -= (this.swimming || this.running ? 2 : 1); 506 | } 507 | if ( this.stats.stamina < 0 ) this.stats.stamina = 0; 508 | } else { 509 | if ( this.stats.stamina < 100 && this.stats.hunger < 100 ) { 510 | this.stats.stamina += 5; 511 | if ( this.stats.stamina > 100 ) this.stats.stamina = 100; 512 | } 513 | } 514 | 515 | this.stats._stamina = 0; 516 | } 517 | 518 | if ( this.stats._hunger >= PLAYER_HUNGER_TICK ) { 519 | if ( this.stats.hunger >= 100 ) { 520 | this.stats.health--; 521 | } else { 522 | if ( this.stats.hunger < 100 ) { 523 | this.stats.hunger++; 524 | } 525 | } 526 | this.stats._hunger = 0; 527 | } 528 | } 529 | 530 | _updateBullets(timeSinceLastFrame); 531 | 532 | this.moving = false; 533 | }; 534 | 535 | C.prototype.shoot = function(target) { 536 | var w = this.weapon; 537 | if ( w == -1 ) return; 538 | 539 | if ( this.weapons[w].ammo <= 0 ) { 540 | return; 541 | } 542 | this.weapons[w].ammo--; 543 | 544 | var bullet = this.weapons[w].bullet; 545 | 546 | _createBullet({ 547 | x : this.x, 548 | y : this.y, 549 | cx : this.getCX(), 550 | cy : this.getCY(), 551 | r : this.r 552 | }, target, bullet); 553 | }; 554 | 555 | C.prototype.prevWeapon = function() { 556 | if ( !this.weapons.length ) return; 557 | 558 | this.weapon--; 559 | if ( this.weapon < 0 ) this.weapon = (this.weapons.length - 1); 560 | }; 561 | 562 | C.prototype.nextWeapon = function() { 563 | if ( !this.weapons.length ) return; 564 | 565 | this.weapon++; 566 | if ( this.weapon > (this.weapons.length - 1) ) this.weapon = 0; 567 | }; 568 | 569 | C.prototype.move = function(timeSinceLastFrame, pos, running) { 570 | if ( !this.swimming ) { 571 | if ( running && this.stats.stamina<=0 ) running = false; 572 | } 573 | 574 | var spd = this.swimming ? SWIM_SPEED : (running ? RUN_SPEED : MOVE_SPEED); 575 | if ( cheatMode ) spd = 10.0; 576 | 577 | var speed = (spd * (timeSinceLastFrame / 60)); 578 | var destX = this.x; 579 | var destY = this.y; 580 | 581 | if ( pos === 'up' ) { 582 | destY -= Math.cos(this.r) * speed; 583 | destX += Math.sin(this.r) * speed; 584 | this.moving = true; 585 | } else if ( pos === 'left' ) { 586 | destX -= Math.cos(this.r) * speed; 587 | destY -= Math.sin(this.r) * speed; 588 | this.moving = true; 589 | } else if ( pos === 'down' ) { 590 | destY += Math.cos(this.r) * speed; 591 | destX -= Math.sin(this.r) * speed; 592 | this.moving = true; 593 | } else if ( pos === 'right' ) { 594 | destX += Math.cos(this.r) * speed; 595 | destY += Math.sin(this.r) * speed; 596 | this.moving = true; 597 | } 598 | 599 | this.running = false; 600 | this.swimming = false; 601 | 602 | var acc = cheatMode ? 1 : map.isTileWalkable(destX, destY); 603 | if ( acc > 0 ) { 604 | this.x = destX; 605 | this.y = destY; 606 | if ( acc === 2 ) { 607 | this.swimming = true; 608 | } else { 609 | this.running = running; 610 | } 611 | } 612 | }; 613 | 614 | C.prototype.rotate = function(timeSinceLastFrame, mp) { 615 | var r = this.getRelPosition(); 616 | this.r = Math.atan2(mp.x - r.x, - (mp.y - r.y)); 617 | this.r %= (Math.PI * 2); 618 | }; 619 | 620 | C.prototype.getRelPosition = function() { 621 | var rx = -(this.getX() - map.viewRect.x) - map.xOffset; 622 | var ry = -(this.getY() - map.viewRect.y) - map.yOffset; 623 | return {x:rx, y:ry}; 624 | }; 625 | 626 | C.prototype.getPosition = function() { 627 | return {x:this.x.toFixed(1), y:this.y.toFixed(1)}; 628 | }; 629 | 630 | C.prototype.getBoundingBox = function(rel) { 631 | var pos = rel ? this.getRelPosition() : this.getPosition(); 632 | return { 633 | x : pos.x - 16 + this.getCX(), 634 | y : pos.y - 16 + this.getCY(), 635 | w : 32, 636 | h : 32 637 | }; 638 | }; 639 | 640 | C.prototype.getCX = function() { 641 | return this.w / 2; 642 | }; 643 | 644 | C.prototype.getCY = function() { 645 | return this.h / 2; 646 | }; 647 | 648 | C.prototype.getX = function() { 649 | return this.x; 650 | }; 651 | 652 | C.prototype.getY = function(whatY) { 653 | return this.y; 654 | }; 655 | 656 | C.prototype.getWidth = function() { 657 | return this.w; 658 | }; 659 | 660 | C.prototype.getHeight = function() { 661 | return this.h; 662 | }; 663 | 664 | C.prototype.getRot = function() { 665 | return this.r; 666 | }; 667 | 668 | C.prototype.getUIData = function() { 669 | var w = this.weapons[this.weapon]; 670 | if ( !w ) w = {name: 'None', ammo: 0}; 671 | 672 | return { 673 | health : this.stats.health >> 0, 674 | stamina : this.stats.stamina >> 0, 675 | hunger : this.stats.hunger >> 0, 676 | ammo : w.ammo, 677 | weapon : w.name 678 | }; 679 | }; 680 | 681 | C.prototype.getCurrentChunk = function(x, y) { 682 | x = (typeof x === 'undefined') ? this.getX() : x; 683 | y = (typeof y === 'undefined') ? this.getY() : y; 684 | 685 | var px = (x + (CHUNK_SIZE / 2)); 686 | var py = (y + (CHUNK_SIZE / 2)); 687 | return { 688 | x : Math.floor(px / CHUNK_SIZE), 689 | y : Math.floor(py / CHUNK_SIZE) 690 | }; 691 | }; 692 | 693 | C.prototype.getCurrentTile = function(x, y) { 694 | x = (typeof x === 'undefined') ? this.getX() : x; 695 | y = (typeof y === 'undefined') ? this.getY() : y; 696 | 697 | var px = (x + (CHUNK_SIZE / 2)); 698 | var py = (y + (CHUNK_SIZE / 2)); 699 | return { 700 | x: Math.floor(px / TILE_SIZE), 701 | y: Math.floor(py / TILE_SIZE) 702 | } 703 | }; 704 | 705 | C.prototype.isRunning = function() { 706 | return this.running; 707 | }; 708 | 709 | C.prototype.isMoving = function() { 710 | return this.moving; 711 | }; 712 | 713 | return C; 714 | })(); 715 | 716 | /** 717 | * @class 718 | */ 719 | var CTextureSet = (function() { 720 | /** 721 | * Private 722 | */ 723 | var _inQueue = 0; 724 | var _queueFailed = 0; 725 | var _queueDone = null; 726 | 727 | var _imageLoaded = function(ok, id, src, img, callback) { 728 | if ( ok ) { 729 | var canvas = document.createElement('canvas'); 730 | canvas.width = img.width; 731 | canvas.height = img.height; 732 | var context = canvas.getContext('2d'); 733 | context.drawImage(img, 0, 0); 734 | 735 | callback(id, context, src); 736 | } else { 737 | _queueFailed++; 738 | 739 | callback(id, false, src); 740 | } 741 | 742 | _inQueue--; 743 | if ( _inQueue <= 0 ) { 744 | _queueDone(_queueFailed); 745 | } 746 | }; 747 | 748 | var _loadImage = function(id, src, callback) { 749 | _inQueue++; 750 | 751 | var img = document.createElement('img'); 752 | img.onload = function _loadImageOnLoad() { 753 | _imageLoaded(true, id, src, this, callback); 754 | }; 755 | img.onerror = function _loadImageOnError() { 756 | _imageLoaded(false, id, src, this, callback); 757 | }; 758 | 759 | img.src = src; 760 | }; 761 | 762 | /** 763 | * Public 764 | */ 765 | var C = function(opts, sets) { 766 | this.options = {}; 767 | 768 | console.group('CTextureSet::CTextureSet()'); 769 | 770 | var i; 771 | for ( i in opts ) { 772 | if ( this.options.hasOwnProperty(i) ) { 773 | this.options[i] = opts[i]; 774 | } 775 | } 776 | 777 | var a; 778 | for ( i in sets ) { 779 | if ( sets.hasOwnProperty(i) ) { 780 | sets[i]._context = null; 781 | if ( sets[i].animations ) { 782 | for ( a in sets[i].animations ) { 783 | sets[i].animations[a]._lastTime = 0; //new Date(); 784 | sets[i].animations[a]._current = sets[i].animations[a].frames[0] || 0; 785 | 786 | console.log('inited animation for', i, '=>', a); 787 | } 788 | } 789 | } 790 | } 791 | this.sets = sets; 792 | 793 | console.log('Options', this.options); 794 | console.log('Sets', this.sets); 795 | console.groupEnd(); 796 | }; 797 | 798 | C.prototype.destroy = function() { 799 | console.warn('Game.Scene::CTextureSet', 'Shut down!'); 800 | }; 801 | 802 | C.prototype.getTileIndex = function(set, id) { 803 | var idx = -1; 804 | var i = 0, l = set.ids.length; 805 | for ( i; i < l; i++ ) { 806 | if ( set.ids[i] == id ) { 807 | idx = i; 808 | break; 809 | } 810 | } 811 | 812 | return idx; 813 | }; 814 | 815 | C.prototype.drawTile = function(context, set, id, dx, dy, dw, dh) { 816 | var set = this.sets[set]; 817 | if ( !set || !set._context ) return false; 818 | 819 | var idx = this.getTileIndex(set, id); 820 | if ( idx >= 0 ) { 821 | var sx = set.sx * idx; 822 | var sy = 0; 823 | var sw = set.sx; 824 | var sh = set.sy; 825 | 826 | context.drawImage(set._context.canvas, sx, sy, sw, sh, dx, dy, dw, dh); 827 | return true; 828 | } 829 | return false; 830 | }; 831 | 832 | C.prototype.drawObj = function(context, set, id, rx, ry, rw, rh) { 833 | var set = this.sets[set]; 834 | if ( !set || !set._context ) return false; 835 | 836 | 837 | var idx = this.getTileIndex(set, id); 838 | if ( idx >= 0 ) { 839 | var sx = set.sx * idx; 840 | var sy = 0; 841 | var sw = set.sx; 842 | var sh = set.sy; 843 | 844 | var dx = (rx + (rw / 2)) - set.cx; 845 | var dy = (ry + (rh / 2)) - set.cy; 846 | var dw = sw; 847 | var dh = sh; 848 | 849 | context.drawImage(set._context.canvas, sx, sy, sw, sh, dx, dy, dw, dh); 850 | return true; 851 | } 852 | return false; 853 | }; 854 | 855 | C.prototype.getTile = function(set, id) { 856 | var set = this.sets[set]; 857 | if ( !set || !set._context ) return false; 858 | 859 | var idx = this.getTileIndex(set, id); 860 | if ( idx >= 0 ) { 861 | var canvas = document.createElement('canvas'); 862 | 863 | var sx = set.sx * idx; 864 | var sy = 0; 865 | var sw = set.sx; 866 | var sh = set.sy; 867 | 868 | var dx = 0; 869 | var dy = 0; 870 | var dw = sw; 871 | var dh = sh; 872 | 873 | canvas.width = dw; 874 | canvas.height = dh; 875 | 876 | var context = canvas.getContext('2d'); 877 | context.drawImage(set._context, sx, sy, sw, sh, dx, dy, dw, dh); 878 | return context; 879 | } 880 | 881 | return false; 882 | }; 883 | 884 | C.prototype.load = function(callback) { 885 | console.group('CTextureSet::load()', 'Loading tetures'); 886 | 887 | var self = this; 888 | _queueDone = function(failed) { 889 | console.groupEnd(); 890 | console.log('CTextureSet::load()', 'Done', failed, 'failed'); 891 | callback(failed === 0); 892 | }; 893 | 894 | var i; 895 | for ( i in this.sets ) { 896 | if ( this.sets.hasOwnProperty(i) ) { 897 | console.log('CTextureSet::load()', 'adding', i); 898 | 899 | _loadImage(i, this.sets[i].src, function(id, context, src) { 900 | if ( context ) { 901 | console.log('CTextureSet::load()', 'loaded', id, src); 902 | } else { 903 | console.log('CTextureSet::load()', 'failed', id, src); 904 | } 905 | 906 | self.sets[id]._context = context || null; 907 | }) 908 | } 909 | } 910 | }; 911 | 912 | C.prototype.tick = function(timeSinceLastFrame, context, setid, anid, dx, dy, dw, dh) { 913 | var set = this.sets[setid]; 914 | if ( !set || !set._context ) return false; 915 | var now = new Date(); 916 | var anim = set.animations[anid]; 917 | var diff = now - anim._lastTime; 918 | var current = anim._current; 919 | 920 | if ( (diff >= anim.duration) && (diff !== now) ) { 921 | current++; 922 | if ( current > anim.frames[anim.frames.length-1] ) { 923 | current = anim.frames[0] 924 | } 925 | 926 | this.sets[setid].animations[anid]._lastTime = now; 927 | this.sets[setid].animations[anid]._current = current; 928 | } 929 | 930 | if ( diff === now ) this.sets[setid].animations[anid]._lastTime = now; 931 | 932 | var sx = set.sx * current; 933 | var sy = 0; 934 | var sw = set.sx; 935 | var sh = set.sy; 936 | 937 | context.drawImage(set._context.canvas, sx, sy, sw, sh, dx, dy, dw, dh); 938 | }; 939 | 940 | 941 | return C; 942 | })(); 943 | 944 | /** 945 | * @class 946 | */ 947 | var CTileData = function(ax, ay, tx, ty, typeId, type, subType) { 948 | this.aX = ax; 949 | this.aY = ay; 950 | this.x = tx; 951 | this.y = ty; 952 | this.t = type; 953 | this.tid = typeId; 954 | this.sub = subType; 955 | }; 956 | 957 | CTileData.prototype.isWalkable = function() { 958 | if ( this.sub ) return false; 959 | return this.t == 'sand' || this.t == 'grass' || this.t == 'gravel'; 960 | }; 961 | 962 | CTileData.prototype.isSwimmable = function() { 963 | if ( this.sub ) return false; 964 | return this.t == 'water'; 965 | }; 966 | 967 | CTileData.prototype.isAccessable = function() { 968 | if ( this.isWalkable() ) return 1; 969 | if ( this.isSwimmable() ) return 2; 970 | return 0; 971 | }; 972 | 973 | /** 974 | * @class 975 | */ 976 | var CChunk = function(cx, cy) { 977 | this.x = cx; 978 | this.y = cy; 979 | this.res = null; 980 | this.created = new Date(); 981 | this.tiles = {}; 982 | this.objects = []; 983 | 984 | this.initialize(); 985 | }; 986 | 987 | CChunk.prototype.initialize = function() { 988 | var canvas = document.createElement('canvas'); 989 | var context = canvas.getContext('2d'); 990 | canvas.width = CHUNK_SIZE; 991 | canvas.height = CHUNK_SIZE; 992 | 993 | var cx = this.x; 994 | var cy = this.y; 995 | var ttype = ''; 996 | var subtype = ''; 997 | 998 | var x = 0, y = 0, v, vv, z; 999 | var atx = 0, aty = 0; 1000 | 1001 | for ( y = 0; y < CHUNK_TILES; y++ ) { 1002 | for ( x = 0; x < CHUNK_TILES; x++ ) { 1003 | atx = x + ((cx*CHUNK_SIZE/CHUNK_SIZE) * CHUNK_TILES); 1004 | aty = y + ((cy*CHUNK_SIZE/CHUNK_SIZE) * CHUNK_TILES); 1005 | z = hashPair(atx, aty); 1006 | v = map.generate(cx, cy, x, y); 1007 | 1008 | ttype = 'water'; 1009 | subtype = ''; 1010 | if ( v > 0 ) { 1011 | if ( v <= 0.1 ) { 1012 | ttype = 'sand'; 1013 | } else if ( v <= 0.4 ) { 1014 | ttype = 'grass'; 1015 | if ( v >= 0.30 && v <= 0.35 ) { 1016 | subtype = 'tree'; 1017 | } 1018 | } else { 1019 | ttype = 'stone'; 1020 | } 1021 | } 1022 | 1023 | textureSet.drawTile(context, 'terrain', ttype, (TILE_SIZE * x), (TILE_SIZE * y), TILE_SIZE, TILE_SIZE); 1024 | 1025 | if ( debugDetail ) { 1026 | context.fillStyle = "#444"; 1027 | context.fillText(atx, (TILE_SIZE*x)+5, (TILE_SIZE*y)+15); 1028 | context.fillStyle = "#888"; 1029 | context.fillText(aty, (TILE_SIZE*x)+10, (TILE_SIZE*y)+20); 1030 | } 1031 | 1032 | this.tiles[z] = new CTileData(atx, aty, x, y, v, ttype, subtype); 1033 | 1034 | if ( subtype ) { 1035 | this.objects.push(z); 1036 | } 1037 | } 1038 | } 1039 | 1040 | if ( debugTileOverlay ) { 1041 | context.lineWidth = 0.1; 1042 | context.strokeStyle = "#111"; 1043 | 1044 | for ( y = 0; y < CHUNK_TILES; y++ ) { 1045 | for ( x = 0; x < CHUNK_TILES; x++ ) { 1046 | context.strokeRect((TILE_SIZE * x), (TILE_SIZE * y), TILE_SIZE, TILE_SIZE); 1047 | } 1048 | } 1049 | } 1050 | 1051 | if ( debugChunkOverlay ) { 1052 | context.lineWidth = 0.3; 1053 | context.strokeStyle = "#000"; 1054 | context.strokeRect(0, 0, CHUNK_SIZE, CHUNK_SIZE); 1055 | 1056 | context.fillStyle = "#000"; 1057 | context.fillText(cx + "," + cy, 5, 15); 1058 | } 1059 | 1060 | this.res = canvas; 1061 | }; 1062 | 1063 | CChunk.prototype.drawObjects = function(context) { 1064 | var i = 0, l = this.objects.length, t; 1065 | if ( !l ) return; 1066 | 1067 | var r1, r2, px, py, pos; 1068 | for ( i; i < l; i++ ) { 1069 | t = this.tiles[this.objects[i]]; 1070 | if ( t.sub == 'tree' ) { 1071 | pos = map.getChunkPosition(this.x, this.y); // We already checked AABB 1072 | textureSet.drawObj(context, 'trees', 'palm', pos.px + (TILE_SIZE * t.x), pos.py + (TILE_SIZE * t.y), TILE_SIZE, TILE_SIZE); 1073 | } 1074 | } 1075 | }; 1076 | 1077 | CChunk.prototype.inRange = function(rect) { 1078 | var r1 = this.getRect(); 1079 | var r2 = rect; 1080 | var c1 = {top:r1.y,left:r1.x,bottom:r1.y+r1.h,right:r2.x+r2.w}; 1081 | var c2 = {top:r2.y,left:r2.x,bottom:r2.y+r2.h,right:r2.x+r2.w}; 1082 | return Game.Utils.intersectRect(c1, c2); 1083 | }; 1084 | 1085 | CChunk.prototype.isTimedOut = function() { 1086 | return ((new Date()) - this.created) >= CHUNK_ALIVE; 1087 | }; 1088 | 1089 | CChunk.prototype.getRect = function() { 1090 | return {x:(this.x * CHUNK_SIZE), y:(this.y * CHUNK_SIZE), w:CHUNK_SIZE, h:CHUNK_SIZE}; 1091 | }; 1092 | 1093 | CChunk.prototype.getTile = function(x, y) { 1094 | return this.tiles[hashPair(x, y)]; 1095 | }; 1096 | 1097 | ///////////////////////////////////////////////////////////////////////////// 1098 | // SCENE 1099 | ///////////////////////////////////////////////////////////////////////////// 1100 | 1101 | function CreateScene(engine, params, callback) { 1102 | Game.UI.setScreen(Game.UI.SCREEN_GAME); 1103 | 1104 | canvasWidth = engine.getCanvasWidth(); 1105 | canvasHeight = engine.getCanvasHeight(); 1106 | 1107 | engine.context.textBaseline = "top"; 1108 | 1109 | engine.context.font = "bold 20pt Monospace"; 1110 | engine.context.fillStyle = "#fff"; 1111 | var dim = engine.context.measureText('Loading...'); 1112 | engine.context.fillText('Loading...', ((canvasWidth - dim.width) / 2), ((canvasHeight) / 2)); 1113 | engine.context.font = "bold 9pt Monospace"; 1114 | 1115 | player = new CPlayer(params.init || {}); 1116 | map = new CMap(params.world || {}); 1117 | textureSet = new CTextureSet({}, Game.Config.Textures || {}); 1118 | 1119 | console.log('CreateScene()', 'Inited -- loading textures'); 1120 | 1121 | var self = this; 1122 | 1123 | var __onTerrainGenerated = function onTerrainGenerated(callback) { 1124 | console.log('CreateScene()', 'Done initing terrain!'); 1125 | 1126 | setTimeout(function() { 1127 | callback(); 1128 | }, 500); 1129 | }; 1130 | 1131 | var __onTileSetLoaded = function onTileSetLoaded(success) { 1132 | if ( !success ) { 1133 | console.error('CreateScene()', 'Cannot continue -- Error loading textures!'); 1134 | return; 1135 | } 1136 | 1137 | console.log('CreateScene()', 'Done loading textures!'); 1138 | 1139 | map.initialize(); 1140 | __onTerrainGenerated(callback); 1141 | }; 1142 | 1143 | var __onAudioLoaded = function onAudioLoaded(success) { 1144 | if ( !success ) { 1145 | console.error('CreateScene()', 'Cannot continue -- Error loading audio!'); 1146 | return; 1147 | } 1148 | 1149 | console.log('CreateScene()', 'Done initing audio!'); 1150 | textureSet.load(__onTileSetLoaded); 1151 | }; 1152 | 1153 | Game.Audio.load((Game.Config.Sounds || {}), __onAudioLoaded); 1154 | } 1155 | 1156 | function RenderScene(timeSinceLastFrame, engine, input, fps) { 1157 | Update(timeSinceLastFrame, engine, input); 1158 | 1159 | map.draw(timeSinceLastFrame, engine.context); 1160 | player.draw(timeSinceLastFrame, engine.context); 1161 | map.draw(timeSinceLastFrame, engine.context, true); 1162 | 1163 | engine.context.fillStyle = "#000"; 1164 | if ( debugOverlay ) { 1165 | var cpos = player.getCurrentChunk(); 1166 | var tpos = player.getCurrentTile(); 1167 | var cnum = map.getCachedChunks(); 1168 | var vchu = map.getVisibleChunks(); 1169 | var ppos = player.getPosition(); 1170 | var text = [vchu, "visible chunks,", cnum, "cached", "[X:", ppos.x, " Y:", ppos.y, "] [cX:" + cpos.x, " cY:" + cpos.y, "] [tX:", tpos.x, " tY:", tpos.y, "]"]; 1171 | if ( cheatMode ) { 1172 | text.push("*"); 1173 | } 1174 | engine.context.fillText(text.join(" "), 5, 5); 1175 | } 1176 | 1177 | engine.context.fillText("2D Tile Engine (c) Anders Evenrud", (canvasWidth - 190), 5); 1178 | 1179 | if ( debugDetail ) { 1180 | engine.context.beginPath(); 1181 | engine.context.moveTo(canvasWidth/2,0); 1182 | engine.context.lineTo(canvasWidth/2, canvasHeight); 1183 | engine.context.moveTo(0, canvasHeight/2); 1184 | engine.context.lineTo(canvasWidth, canvasHeight/2); 1185 | engine.context.stroke(); 1186 | engine.context.closePath(); 1187 | } 1188 | 1189 | chunkCheckCounter += timeSinceLastFrame; 1190 | if ( chunkCheckCounter >= CHUNK_ALIVE_CHECK ) { 1191 | map.checkChunks(); 1192 | chunkCheckCounter = 0; 1193 | } 1194 | 1195 | //return true; // Render only one frame 1196 | } 1197 | 1198 | function DestroyScene(engine) { 1199 | player.destroy(); 1200 | map.destroy(); 1201 | textureSet.destroy(); 1202 | } 1203 | 1204 | function ResizeScene(engine, width, height) { 1205 | if ( width === canvasWidth && height === canvasHeight ) 1206 | return; 1207 | 1208 | canvasWidth = width; 1209 | canvasHeight = height; 1210 | 1211 | map.update(); 1212 | } 1213 | 1214 | function Update(timeSinceLastFrame, engine, input) { 1215 | player.tick(timeSinceLastFrame); // First 1216 | 1217 | var running = input.keyDown("shift"); 1218 | if ( input.keyDown("W") ) { 1219 | player.move(timeSinceLastFrame, 'up', running); 1220 | } else if ( input.keyDown("A") ) { 1221 | player.move(timeSinceLastFrame, 'left', running); 1222 | } else if ( input.keyDown("S") ) { 1223 | player.move(timeSinceLastFrame, 'down', running); 1224 | } else if ( input.keyDown("D") ) { 1225 | player.move(timeSinceLastFrame, 'right', running); 1226 | } 1227 | 1228 | if ( input.mousePressed(0) ) { 1229 | player.shoot(input.mousePosition()); 1230 | } 1231 | 1232 | if ( input.mouseWheel() > 0.0 ) { 1233 | player.prevWeapon(); 1234 | } else if ( input.mouseWheel() < 0.0 ) { 1235 | player.nextWeapon(); 1236 | } 1237 | 1238 | player.rotate(timeSinceLastFrame, input.mousePosition()); 1239 | 1240 | if ( input.keyPressed('1') ) { 1241 | debugTileOverlay = !debugTileOverlay; 1242 | map.clearChunks(); 1243 | } else if ( input.keyPressed('2') ) { 1244 | debugChunkOverlay = !debugChunkOverlay; 1245 | map.clearChunks(); 1246 | } else if ( input.keyPressed('3') ) { 1247 | debugOverlay = !debugOverlay; 1248 | } else if ( input.keyPressed('7') ) { 1249 | cheatMode = !cheatMode; 1250 | } else if ( input.keyPressed('8') ) { 1251 | debugDetail = !debugDetail; 1252 | map.clearChunks(); 1253 | } else if ( input.keyPressed('9') ) { 1254 | Game.UI.toggle(); 1255 | } 1256 | 1257 | map.tick(timeSinceLastFrame); // Last 1258 | 1259 | Game.UI.update({ 1260 | player: player.getUIData(), 1261 | opts : { 1262 | } 1263 | }); 1264 | } 1265 | 1266 | ///////////////////////////////////////////////////////////////////////////// 1267 | // EXPORTS 1268 | ///////////////////////////////////////////////////////////////////////////// 1269 | 1270 | Game.Scene = { 1271 | create : CreateScene, 1272 | render : RenderScene, 1273 | destroy: DestroyScene, 1274 | resize : ResizeScene 1275 | }; 1276 | 1277 | })(); 1278 | 1279 | -------------------------------------------------------------------------------- /src/ui.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * @package Game 3 | * @author Anders Evenrud 4 | */ 5 | "use strict"; 6 | 7 | var Game = window.Game || {}; // Namespace 8 | 9 | (function() { 10 | 11 | var canvasWidth = 0; 12 | var canvasHeight = 0; 13 | 14 | var UI = function() { 15 | this.visible = false; 16 | this.screen = 0; 17 | this.data = null; 18 | }; 19 | 20 | UI.prototype.SCREEN_MAIN = 0; 21 | UI.prototype.SCREEN_GAME = 1; 22 | 23 | UI.prototype.init = function(engine) { 24 | canvasWidth = engine.getCanvasWidth(); 25 | canvasHeight = engine.getCanvasHeight(); 26 | 27 | console.log("UI::init()", "UI handling initialized"); 28 | }; 29 | 30 | UI.prototype.destroy = function(engine) { 31 | }; 32 | 33 | UI.prototype.render = function(timeSinceLastFrame, engine, input) { 34 | if ( !this.visible ) return; 35 | if ( this.screen === this.SCREEN_MAIN ) return; 36 | if ( !this.data ) return; 37 | 38 | var playerData = this.data.player; 39 | 40 | engine.context.font = "8pt Arial"; 41 | 42 | var px = 5; 43 | var py = canvasHeight - 30; 44 | var boxX = px + 5; 45 | var boxY = py + 5; 46 | var boxW = 295; 47 | var boxH = 20; 48 | var lingrad, boxV; 49 | 50 | 51 | engine.context.strokeStle = "#000"; 52 | engine.context.lineWidth = "1"; 53 | engine.context.globalAlpha = 0.9; 54 | 55 | boxV = (boxW * ((playerData.health >> 0) / 100)); 56 | lingrad = engine.context.createLinearGradient(boxX, boxY, boxX+boxV, boxY+boxH); //(x0,y0) to (x1,y1) 57 | lingrad.addColorStop(0, '#3f1616'); 58 | lingrad.addColorStop(1, '#a73b3b'); 59 | 60 | engine.context.fillStyle = "#FFF"; 61 | engine.context.fillRect(boxX, boxY, boxW, boxH); 62 | engine.context.fillStyle = lingrad; 63 | engine.context.fillRect(boxX, boxY, boxV, boxH); 64 | engine.context.strokeRect(boxX, boxY, boxW, boxH); 65 | engine.context.fillStyle = "#aaa"; 66 | engine.context.fillText("Health", boxX + 7, boxY + 3); 67 | engine.context.fillText(playerData.health + "%", boxX + 40, boxY + 3); 68 | 69 | //boxY += 20; 70 | boxX += boxW + 10; 71 | boxV = (boxW * ((playerData.stamina >> 0) / 100)); 72 | lingrad = engine.context.createLinearGradient(boxX, boxY, boxX+boxV, boxY+boxH); //(x0,y0) to (x1,y1) 73 | lingrad.addColorStop(0, '#172a40'); 74 | lingrad.addColorStop(1, '#3a6da5'); 75 | 76 | engine.context.fillStyle = "#FFF"; 77 | engine.context.fillRect(boxX, boxY, boxW, boxH); 78 | engine.context.fillStyle = lingrad; 79 | engine.context.fillRect(boxX, boxY, boxV, boxH); 80 | engine.context.strokeRect(boxX, boxY, boxW, boxH); 81 | engine.context.fillStyle = "#aaa"; 82 | engine.context.fillText("Stamina", boxX + 7, boxY + 3); 83 | engine.context.fillText(playerData.stamina + "%", boxX + 50, boxY + 3); 84 | 85 | //boxY += 20; 86 | boxX += boxW + 10; 87 | boxV = (boxW * ((playerData.hunger >> 0) / 100)); 88 | lingrad = engine.context.createLinearGradient(boxX, boxY, boxX+boxV, boxY+boxH); //(x0,y0) to (x1,y1) 89 | lingrad.addColorStop(0, '#404017'); 90 | lingrad.addColorStop(1, '#a6a73a'); 91 | 92 | engine.context.fillStyle = "#FFF"; 93 | engine.context.fillRect(boxX, boxY, boxW, boxH); 94 | engine.context.fillStyle = lingrad; 95 | engine.context.fillRect(boxX, boxY, boxV, boxH); 96 | engine.context.strokeRect(boxX, boxY, boxW, boxH); 97 | engine.context.fillStyle = "#aaa"; 98 | engine.context.fillText("Hunger", boxX + 7, boxY + 3); 99 | engine.context.fillText(playerData.hunger + "%", boxX + 48, boxY + 3); 100 | 101 | //boxY += 30; 102 | boxX += boxW + 10; 103 | engine.context.fillStyle = "#FFF"; 104 | engine.context.fillRect(boxX, boxY, (boxW / 2) - 5, 20); 105 | engine.context.strokeRect(boxX, boxY, (boxW / 2) - 5, 20); 106 | engine.context.fillRect(boxX + (boxW / 2) + 5, boxY, (boxW / 2) - 5, 20); 107 | engine.context.strokeRect(boxX + (boxW / 2) + 5, boxY, (boxW / 2) - 5, 20); 108 | 109 | //boxY += 4; 110 | engine.context.font = "9pt Arial"; 111 | engine.context.fillStyle = "#000"; 112 | engine.context.fillText(playerData.weapon + " (" + playerData.ammo + " bullets)", boxX + 4, boxY + 3); 113 | engine.context.fillText('None', boxX + (boxW / 2) + 9, boxY + 3); 114 | 115 | engine.context.globalAlpha = 1.0; 116 | }; 117 | 118 | UI.prototype.update = function(data) { 119 | this.data = data; 120 | }; 121 | 122 | UI.prototype.toggle = function() { 123 | this.visible = !this.visible; 124 | }; 125 | 126 | UI.prototype.resize = function(engine, w, h) { 127 | canvasWidth = w; 128 | canvasHeight = h; 129 | }; 130 | 131 | UI.prototype.setScreen = function(s) { 132 | this.screen = s; 133 | this.visible = true; 134 | }; 135 | 136 | Game.UI = new UI(); 137 | 138 | })(); 139 | 140 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * @package Game 3 | * @author Anders Evenrud 4 | */ 5 | "use strict"; 6 | 7 | var Game = window.Game || {}; // Namespace 8 | 9 | (function() { 10 | 11 | Game.Utils = {}; 12 | 13 | /** 14 | * 2D Vector 15 | */ 16 | Game.Utils.Vector2 = function(x, y) { 17 | this.x = x || 0; 18 | this.y = y || 0; 19 | 20 | this.equals = function(other) { 21 | return other.x === this.x && other.y === this.y; 22 | }; 23 | }; 24 | 25 | /** 26 | * Deg to Rad 27 | */ 28 | Game.Utils.convertToRadians = function(degree) { 29 | return degree*(Math.PI / 180); 30 | }; 31 | 32 | /** 33 | * Rad to Deg 34 | */ 35 | Game.Utils.convertToDegree = function(rad) { 36 | return rad / (Math.PI / 180); 37 | }; 38 | 39 | /** 40 | * Draw a rotated image onto canvas 41 | */ 42 | Game.Utils.drawImageRot = function(ctx, img, x, y, width, height, deg){ 43 | var rad = Game.Utils.convertToRadians(deg); 44 | ctx.translate(x + width / 2, y + height / 2); 45 | ctx.rotate(rad); 46 | ctx.drawImage(img, width / 2 * (-1),height / 2 * (-1),width,height); 47 | ctx.rotate(rad * ( -1 ) ); 48 | ctx.translate((x + width / 2) * (-1), (y + height / 2) * (-1)); 49 | }; 50 | 51 | /** 52 | * Radom number range 53 | */ 54 | Game.Utils.randomRange = function(minVal, maxVal) { 55 | return Math.floor(Math.random() * (maxVal - minVal - 1)) + minVal; 56 | }; 57 | 58 | /** 59 | * Clamp value between range 60 | */ 61 | Game.Utils.clampRange = function(val, min, max) { 62 | return (val + 1) / 2 * (max - min) + min; 63 | }; 64 | 65 | /** 66 | * Rectangle intersection check 67 | */ 68 | Game.Utils.intersectRect = function (r1, r2) { 69 | return !(r2.left > r1.right || 70 | r2.right < r1.left || 71 | r2.top > r1.bottom || 72 | r2.bottom < r1.top); 73 | }; 74 | 75 | /** 76 | * Get clicked mouse button id 77 | * @TODO compability 78 | */ 79 | Game.Utils.mouseButton = function(ev) { 80 | ev = ev || window.event; 81 | return ev.which; 82 | }; 83 | 84 | /** 85 | * Get mouse position 86 | * @TODO compability 87 | */ 88 | Game.Utils.mousePosition = function(ev) { 89 | ev = ev || window.event; 90 | return {x: ev.pageX, y:ev.pageY}; 91 | }; 92 | 93 | /** 94 | * Get mouse wheel 95 | * @TODO compability 96 | */ 97 | Game.Utils.mouseWheel = function(ev) { 98 | ev = ev || window.event; 99 | var delta = Math.max(-1, Math.min(1, (ev.wheelDelta || -ev.detail))); 100 | return delta; 101 | }; 102 | 103 | /** 104 | * Get key information from KeyboardEvent 105 | * @TODO add the rest of keycodes 106 | */ 107 | Game.Utils.keyButton = function(ev) { 108 | ev = ev || window.event; 109 | 110 | var stored = { 111 | 37 : 'left', 112 | 39 : 'right', 113 | 38 : 'up', 114 | 40 : 'down', 115 | 32 : 'space', 116 | 27 : 'esc', 117 | 18 : 'alt', 118 | 17 : 'ctrl', 119 | 16 : 'shift', 120 | 13 : 'enter', 121 | 8 : 'backspace', 122 | 48 : '0', 123 | 49 : '1', 124 | 50 : '2', 125 | 51 : '3', 126 | 52 : '4', 127 | 53 : '5', 128 | 54 : '6', 129 | 55 : '7', 130 | 56 : '8', 131 | 57 : '9' 132 | }; 133 | 134 | var key = ev.keyCode || ev.which; 135 | var keyChar = stored[key] || String.fromCharCode(key); 136 | 137 | return {id: key, value: keyChar}; 138 | }; 139 | 140 | /** 141 | * Seed from string 142 | */ 143 | Game.Utils.createSeed = function(s) { 144 | if ( typeof s !== "string" ) { 145 | return s; 146 | } 147 | 148 | var nums = 0; 149 | var i = 0, l = s.length, c; 150 | for ( i; i < l; i++ ) { 151 | c = s.charCodeAt(i); 152 | nums += (c * (31 ^ (i-1))); 153 | } 154 | 155 | return Math.abs(nums); 156 | }; 157 | 158 | })(); 159 | 160 | --------------------------------------------------------------------------------