├── Assets ├── Audio │ ├── cat.m4a │ ├── dog.m4a │ ├── harp_60C.m4a │ ├── harp_62D.m4a │ ├── harp_64E.m4a │ ├── harp_65F.m4a │ ├── harp_67G.m4a │ ├── harp_69A.m4a │ ├── harp_71H.m4a │ └── harp_72C.m4a └── Image │ ├── Restriction Arrows │ ├── down.png │ ├── down_left.png │ ├── down_left_right.png │ ├── down_right.png │ ├── left.png │ ├── left_right.png │ ├── no.png │ ├── right.png │ ├── up.png │ ├── up_down.png │ ├── up_down_left.png │ ├── up_down_left_right.png │ ├── up_down_right.png │ ├── up_left.png │ ├── up_left_right.png │ └── up_right.png │ ├── animal.png │ ├── castle.png │ ├── castledoor_1.png │ ├── castletown.png │ ├── character.png │ ├── favicon.ico │ ├── forest.png │ ├── snow_jukebox.png │ ├── snowforest.png │ ├── sunset1.png │ └── wizard.png ├── LICENSE ├── README.md ├── astar.js ├── control.js ├── core.js ├── data.js ├── index.html ├── scriptLoader.js └── w3.css /Assets/Audio/cat.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheNyanta/JS-RPG-Engine/d02303c33abae4e281f7be4df5c12fc27bd119b2/Assets/Audio/cat.m4a -------------------------------------------------------------------------------- /Assets/Audio/dog.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheNyanta/JS-RPG-Engine/d02303c33abae4e281f7be4df5c12fc27bd119b2/Assets/Audio/dog.m4a -------------------------------------------------------------------------------- /Assets/Audio/harp_60C.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheNyanta/JS-RPG-Engine/d02303c33abae4e281f7be4df5c12fc27bd119b2/Assets/Audio/harp_60C.m4a -------------------------------------------------------------------------------- /Assets/Audio/harp_62D.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheNyanta/JS-RPG-Engine/d02303c33abae4e281f7be4df5c12fc27bd119b2/Assets/Audio/harp_62D.m4a -------------------------------------------------------------------------------- /Assets/Audio/harp_64E.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheNyanta/JS-RPG-Engine/d02303c33abae4e281f7be4df5c12fc27bd119b2/Assets/Audio/harp_64E.m4a -------------------------------------------------------------------------------- /Assets/Audio/harp_65F.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheNyanta/JS-RPG-Engine/d02303c33abae4e281f7be4df5c12fc27bd119b2/Assets/Audio/harp_65F.m4a -------------------------------------------------------------------------------- /Assets/Audio/harp_67G.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheNyanta/JS-RPG-Engine/d02303c33abae4e281f7be4df5c12fc27bd119b2/Assets/Audio/harp_67G.m4a -------------------------------------------------------------------------------- /Assets/Audio/harp_69A.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheNyanta/JS-RPG-Engine/d02303c33abae4e281f7be4df5c12fc27bd119b2/Assets/Audio/harp_69A.m4a -------------------------------------------------------------------------------- /Assets/Audio/harp_71H.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheNyanta/JS-RPG-Engine/d02303c33abae4e281f7be4df5c12fc27bd119b2/Assets/Audio/harp_71H.m4a -------------------------------------------------------------------------------- /Assets/Audio/harp_72C.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheNyanta/JS-RPG-Engine/d02303c33abae4e281f7be4df5c12fc27bd119b2/Assets/Audio/harp_72C.m4a -------------------------------------------------------------------------------- /Assets/Image/Restriction Arrows/down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheNyanta/JS-RPG-Engine/d02303c33abae4e281f7be4df5c12fc27bd119b2/Assets/Image/Restriction Arrows/down.png -------------------------------------------------------------------------------- /Assets/Image/Restriction Arrows/down_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheNyanta/JS-RPG-Engine/d02303c33abae4e281f7be4df5c12fc27bd119b2/Assets/Image/Restriction Arrows/down_left.png -------------------------------------------------------------------------------- /Assets/Image/Restriction Arrows/down_left_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheNyanta/JS-RPG-Engine/d02303c33abae4e281f7be4df5c12fc27bd119b2/Assets/Image/Restriction Arrows/down_left_right.png -------------------------------------------------------------------------------- /Assets/Image/Restriction Arrows/down_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheNyanta/JS-RPG-Engine/d02303c33abae4e281f7be4df5c12fc27bd119b2/Assets/Image/Restriction Arrows/down_right.png -------------------------------------------------------------------------------- /Assets/Image/Restriction Arrows/left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheNyanta/JS-RPG-Engine/d02303c33abae4e281f7be4df5c12fc27bd119b2/Assets/Image/Restriction Arrows/left.png -------------------------------------------------------------------------------- /Assets/Image/Restriction Arrows/left_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheNyanta/JS-RPG-Engine/d02303c33abae4e281f7be4df5c12fc27bd119b2/Assets/Image/Restriction Arrows/left_right.png -------------------------------------------------------------------------------- /Assets/Image/Restriction Arrows/no.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheNyanta/JS-RPG-Engine/d02303c33abae4e281f7be4df5c12fc27bd119b2/Assets/Image/Restriction Arrows/no.png -------------------------------------------------------------------------------- /Assets/Image/Restriction Arrows/right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheNyanta/JS-RPG-Engine/d02303c33abae4e281f7be4df5c12fc27bd119b2/Assets/Image/Restriction Arrows/right.png -------------------------------------------------------------------------------- /Assets/Image/Restriction Arrows/up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheNyanta/JS-RPG-Engine/d02303c33abae4e281f7be4df5c12fc27bd119b2/Assets/Image/Restriction Arrows/up.png -------------------------------------------------------------------------------- /Assets/Image/Restriction Arrows/up_down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheNyanta/JS-RPG-Engine/d02303c33abae4e281f7be4df5c12fc27bd119b2/Assets/Image/Restriction Arrows/up_down.png -------------------------------------------------------------------------------- /Assets/Image/Restriction Arrows/up_down_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheNyanta/JS-RPG-Engine/d02303c33abae4e281f7be4df5c12fc27bd119b2/Assets/Image/Restriction Arrows/up_down_left.png -------------------------------------------------------------------------------- /Assets/Image/Restriction Arrows/up_down_left_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheNyanta/JS-RPG-Engine/d02303c33abae4e281f7be4df5c12fc27bd119b2/Assets/Image/Restriction Arrows/up_down_left_right.png -------------------------------------------------------------------------------- /Assets/Image/Restriction Arrows/up_down_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheNyanta/JS-RPG-Engine/d02303c33abae4e281f7be4df5c12fc27bd119b2/Assets/Image/Restriction Arrows/up_down_right.png -------------------------------------------------------------------------------- /Assets/Image/Restriction Arrows/up_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheNyanta/JS-RPG-Engine/d02303c33abae4e281f7be4df5c12fc27bd119b2/Assets/Image/Restriction Arrows/up_left.png -------------------------------------------------------------------------------- /Assets/Image/Restriction Arrows/up_left_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheNyanta/JS-RPG-Engine/d02303c33abae4e281f7be4df5c12fc27bd119b2/Assets/Image/Restriction Arrows/up_left_right.png -------------------------------------------------------------------------------- /Assets/Image/Restriction Arrows/up_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheNyanta/JS-RPG-Engine/d02303c33abae4e281f7be4df5c12fc27bd119b2/Assets/Image/Restriction Arrows/up_right.png -------------------------------------------------------------------------------- /Assets/Image/animal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheNyanta/JS-RPG-Engine/d02303c33abae4e281f7be4df5c12fc27bd119b2/Assets/Image/animal.png -------------------------------------------------------------------------------- /Assets/Image/castle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheNyanta/JS-RPG-Engine/d02303c33abae4e281f7be4df5c12fc27bd119b2/Assets/Image/castle.png -------------------------------------------------------------------------------- /Assets/Image/castledoor_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheNyanta/JS-RPG-Engine/d02303c33abae4e281f7be4df5c12fc27bd119b2/Assets/Image/castledoor_1.png -------------------------------------------------------------------------------- /Assets/Image/castletown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheNyanta/JS-RPG-Engine/d02303c33abae4e281f7be4df5c12fc27bd119b2/Assets/Image/castletown.png -------------------------------------------------------------------------------- /Assets/Image/character.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheNyanta/JS-RPG-Engine/d02303c33abae4e281f7be4df5c12fc27bd119b2/Assets/Image/character.png -------------------------------------------------------------------------------- /Assets/Image/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheNyanta/JS-RPG-Engine/d02303c33abae4e281f7be4df5c12fc27bd119b2/Assets/Image/favicon.ico -------------------------------------------------------------------------------- /Assets/Image/forest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheNyanta/JS-RPG-Engine/d02303c33abae4e281f7be4df5c12fc27bd119b2/Assets/Image/forest.png -------------------------------------------------------------------------------- /Assets/Image/snow_jukebox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheNyanta/JS-RPG-Engine/d02303c33abae4e281f7be4df5c12fc27bd119b2/Assets/Image/snow_jukebox.png -------------------------------------------------------------------------------- /Assets/Image/snowforest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheNyanta/JS-RPG-Engine/d02303c33abae4e281f7be4df5c12fc27bd119b2/Assets/Image/snowforest.png -------------------------------------------------------------------------------- /Assets/Image/sunset1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheNyanta/JS-RPG-Engine/d02303c33abae4e281f7be4df5c12fc27bd119b2/Assets/Image/sunset1.png -------------------------------------------------------------------------------- /Assets/Image/wizard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheNyanta/JS-RPG-Engine/d02303c33abae4e281f7be4df5c12fc27bd119b2/Assets/Image/wizard.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 David Schwarz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HTML-JAVASCRIPT 2 | 3 | An Engine + Editor for a top-down rpg 4 | 5 | ## Implemented 6 | 7 | Maps, spritesheets, objects 8 | 9 | Map collision with direction restrictions 10 | 11 | Component-component-collision 12 | 13 | Click/touch interaction with objects, for "main" charcter enter interaction 14 | 15 | ## TODO: 16 | 17 | ### Engine 18 | 19 | Implement storing of game data for object + events 20 | 21 | Implement (/ refine: astar) click + touch control 22 | 23 | Refine dialog: Selecting choice with click / touch 24 | 25 | Implement saving and loading of the gamedata into localStorage 26 | 27 | Implement saving and loading of the gamestate into localStorage 28 | 29 | Implement proper audio select/play/pause system 30 | 31 | Refine canvas scaling (i.e. when using fullscreen) 32 | 33 | (Implement proper GUI + Gamemenu (i.e. it opens by pressing "Esc" or Right click)) 34 | 35 | ### Editor 36 | 37 | Refine map editing 38 | 39 | Implement adding and editing of objects / events 40 | 41 | ## DEMO 42 | 43 | https://thenyanta.github.io/JS-RPG-Engine/ 44 | -------------------------------------------------------------------------------- /astar.js: -------------------------------------------------------------------------------- 1 | // javascript-astar 0.4.0 2 | // http://github.com/bgrins/javascript-astar 3 | // Freely distributable under the MIT License. 4 | // Implements the astar search algorithm in javascript using a Binary Heap. 5 | // Includes Binary Heap (with modifications) from Marijn Haverbeke. 6 | // http://eloquentjavascript.net/appendix2.html 7 | 8 | (function(definition) { 9 | /* global module, define */ 10 | if(typeof module === 'object' && typeof module.exports === 'object') { 11 | module.exports = definition(); 12 | } else if(typeof define === 'function' && define.amd) { 13 | define([], definition); 14 | } else { 15 | var exports = definition(); 16 | window.astar = exports.astar; 17 | window.Graph = exports.Graph; 18 | } 19 | })(function() { 20 | 21 | function pathTo(node){ 22 | var curr = node, 23 | path = []; 24 | while(curr.parent) { 25 | path.push(curr); 26 | curr = curr.parent; 27 | } 28 | return path.reverse(); 29 | } 30 | 31 | function getHeap() { 32 | return new BinaryHeap(function(node) { 33 | return node.f; 34 | }); 35 | } 36 | 37 | var astar = { 38 | /** 39 | * Perform an A* Search on a graph given a start and end node. 40 | * @param {Graph} graph 41 | * @param {GridNode} start 42 | * @param {GridNode} end 43 | * @param {Object} [options] 44 | * @param {bool} [options.closest] Specifies whether to return the 45 | path to the closest node if the target is unreachable. 46 | * @param {Function} [options.heuristic] Heuristic function (see 47 | * astar.heuristics). 48 | */ 49 | search: function(graph, start, end, options) { 50 | graph.cleanDirty(); 51 | options = options || {}; 52 | var heuristic = options.heuristic || astar.heuristics.manhattan, 53 | closest = options.closest || false; 54 | 55 | var openHeap = getHeap(), 56 | closestNode = start; // set the start node to be the closest if required 57 | 58 | start.h = heuristic(start, end); 59 | 60 | openHeap.push(start); 61 | 62 | while(openHeap.size() > 0) { 63 | 64 | // Grab the lowest f(x) to process next. Heap keeps this sorted for us. 65 | var currentNode = openHeap.pop(); 66 | 67 | // End case -- result has been found, return the traced path. 68 | if(currentNode === end) { 69 | return pathTo(currentNode); 70 | } 71 | 72 | // Normal case -- move currentNode from open to closed, process each of its neighbors. 73 | currentNode.closed = true; 74 | 75 | // Find all neighbors for the current node. 76 | var neighbors = graph.neighbors(currentNode); 77 | 78 | for (var i = 0, il = neighbors.length; i < il; ++i) { 79 | var neighbor = neighbors[i]; 80 | 81 | if (neighbor.closed || neighbor.isWall()) { 82 | // Not a valid node to process, skip to next neighbor. 83 | continue; 84 | } 85 | 86 | // The g score is the shortest distance from start to current node. 87 | // We need to check if the path we have arrived at this neighbor is the shortest one we have seen yet. 88 | var gScore = currentNode.g + neighbor.getCost(currentNode), 89 | beenVisited = neighbor.visited; 90 | 91 | if (!beenVisited || gScore < neighbor.g) { 92 | 93 | // Found an optimal (so far) path to this node. Take score for node to see how good it is. 94 | neighbor.visited = true; 95 | neighbor.parent = currentNode; 96 | neighbor.h = neighbor.h || heuristic(neighbor, end); 97 | neighbor.g = gScore; 98 | neighbor.f = neighbor.g + neighbor.h; 99 | graph.markDirty(neighbor); 100 | if (closest) { 101 | // If the neighbour is closer than the current closestNode or if it's equally close but has 102 | // a cheaper path than the current closest node then it becomes the closest node 103 | if (neighbor.h < closestNode.h || (neighbor.h === closestNode.h && neighbor.g < closestNode.g)) { 104 | closestNode = neighbor; 105 | } 106 | } 107 | 108 | if (!beenVisited) { 109 | // Pushing to heap will put it in proper place based on the 'f' value. 110 | openHeap.push(neighbor); 111 | } 112 | else { 113 | // Already seen the node, but since it has been rescored we need to reorder it in the heap 114 | openHeap.rescoreElement(neighbor); 115 | } 116 | } 117 | } 118 | } 119 | 120 | if (closest) { 121 | return pathTo(closestNode); 122 | } 123 | 124 | // No result was found - empty array signifies failure to find path. 125 | return []; 126 | }, 127 | // See list of heuristics: http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html 128 | heuristics: { 129 | manhattan: function(pos0, pos1) { 130 | var d1 = Math.abs(pos1.x - pos0.x); 131 | var d2 = Math.abs(pos1.y - pos0.y); 132 | return d1 + d2; 133 | }, 134 | diagonal: function(pos0, pos1) { 135 | var D = 1; 136 | var D2 = Math.sqrt(2); 137 | var d1 = Math.abs(pos1.x - pos0.x); 138 | var d2 = Math.abs(pos1.y - pos0.y); 139 | return (D * (d1 + d2)) + ((D2 - (2 * D)) * Math.min(d1, d2)); 140 | } 141 | }, 142 | cleanNode:function(node){ 143 | node.f = 0; 144 | node.g = 0; 145 | node.h = 0; 146 | node.visited = false; 147 | node.closed = false; 148 | node.parent = null; 149 | } 150 | }; 151 | 152 | /** 153 | * A graph memory structure 154 | * @param {Array} gridIn 2D array of input weights 155 | * @param {Object} [options] 156 | * @param {bool} [options.diagonal] Specifies whether diagonal moves are allowed 157 | */ 158 | function Graph(gridIn, options) { 159 | options = options || {}; 160 | this.nodes = []; 161 | this.diagonal = !!options.diagonal; 162 | this.grid = []; 163 | for (var x = 0; x < gridIn.length; x++) { 164 | this.grid[x] = []; 165 | 166 | for (var y = 0, row = gridIn[x]; y < row.length; y++) { 167 | var node = new GridNode(x, y, row[y]); 168 | this.grid[x][y] = node; 169 | this.nodes.push(node); 170 | } 171 | } 172 | this.init(); 173 | } 174 | 175 | Graph.prototype.init = function() { 176 | this.dirtyNodes = []; 177 | for (var i = 0; i < this.nodes.length; i++) { 178 | astar.cleanNode(this.nodes[i]); 179 | } 180 | }; 181 | 182 | Graph.prototype.cleanDirty = function() { 183 | for (var i = 0; i < this.dirtyNodes.length; i++) { 184 | astar.cleanNode(this.dirtyNodes[i]); 185 | } 186 | this.dirtyNodes = []; 187 | }; 188 | 189 | Graph.prototype.markDirty = function(node) { 190 | this.dirtyNodes.push(node); 191 | }; 192 | 193 | Graph.prototype.neighbors = function(node) { 194 | var ret = [], 195 | x = node.x, 196 | y = node.y, 197 | grid = this.grid; 198 | 199 | // West 200 | if(grid[x-1] && grid[x-1][y]) { 201 | ret.push(grid[x-1][y]); 202 | } 203 | 204 | // East 205 | if(grid[x+1] && grid[x+1][y]) { 206 | ret.push(grid[x+1][y]); 207 | } 208 | 209 | // South 210 | if(grid[x] && grid[x][y-1]) { 211 | ret.push(grid[x][y-1]); 212 | } 213 | 214 | // North 215 | if(grid[x] && grid[x][y+1]) { 216 | ret.push(grid[x][y+1]); 217 | } 218 | 219 | if (this.diagonal) { 220 | // Southwest 221 | if(grid[x-1] && grid[x-1][y-1]) { 222 | ret.push(grid[x-1][y-1]); 223 | } 224 | 225 | // Southeast 226 | if(grid[x+1] && grid[x+1][y-1]) { 227 | ret.push(grid[x+1][y-1]); 228 | } 229 | 230 | // Northwest 231 | if(grid[x-1] && grid[x-1][y+1]) { 232 | ret.push(grid[x-1][y+1]); 233 | } 234 | 235 | // Northeast 236 | if(grid[x+1] && grid[x+1][y+1]) { 237 | ret.push(grid[x+1][y+1]); 238 | } 239 | } 240 | 241 | return ret; 242 | }; 243 | 244 | Graph.prototype.toString = function() { 245 | var graphString = [], 246 | nodes = this.grid, // when using grid 247 | rowDebug, row, y, l; 248 | for (var x = 0, len = nodes.length; x < len; x++) { 249 | rowDebug = []; 250 | row = nodes[x]; 251 | for (y = 0, l = row.length; y < l; y++) { 252 | rowDebug.push(row[y].weight); 253 | } 254 | graphString.push(rowDebug.join(" ")); 255 | } 256 | return graphString.join("\n"); 257 | }; 258 | 259 | function GridNode(x, y, weight) { 260 | this.x = x; 261 | this.y = y; 262 | this.weight = weight; 263 | } 264 | 265 | GridNode.prototype.toString = function() { 266 | return "[" + this.x + " " + this.y + "]"; 267 | }; 268 | 269 | GridNode.prototype.getCost = function() { 270 | return this.weight; 271 | }; 272 | 273 | GridNode.prototype.isWall = function() { 274 | return this.weight === 0; 275 | }; 276 | 277 | function BinaryHeap(scoreFunction){ 278 | this.content = []; 279 | this.scoreFunction = scoreFunction; 280 | } 281 | 282 | BinaryHeap.prototype = { 283 | push: function(element) { 284 | // Add the new element to the end of the array. 285 | this.content.push(element); 286 | 287 | // Allow it to sink down. 288 | this.sinkDown(this.content.length - 1); 289 | }, 290 | pop: function() { 291 | // Store the first element so we can return it later. 292 | var result = this.content[0]; 293 | // Get the element at the end of the array. 294 | var end = this.content.pop(); 295 | // If there are any elements left, put the end element at the 296 | // start, and let it bubble up. 297 | if (this.content.length > 0) { 298 | this.content[0] = end; 299 | this.bubbleUp(0); 300 | } 301 | return result; 302 | }, 303 | remove: function(node) { 304 | var i = this.content.indexOf(node); 305 | 306 | // When it is found, the process seen in 'pop' is repeated 307 | // to fill up the hole. 308 | var end = this.content.pop(); 309 | 310 | if (i !== this.content.length - 1) { 311 | this.content[i] = end; 312 | 313 | if (this.scoreFunction(end) < this.scoreFunction(node)) { 314 | this.sinkDown(i); 315 | } 316 | else { 317 | this.bubbleUp(i); 318 | } 319 | } 320 | }, 321 | size: function() { 322 | return this.content.length; 323 | }, 324 | rescoreElement: function(node) { 325 | this.sinkDown(this.content.indexOf(node)); 326 | }, 327 | sinkDown: function(n) { 328 | // Fetch the element that has to be sunk. 329 | var element = this.content[n]; 330 | 331 | // When at 0, an element can not sink any further. 332 | while (n > 0) { 333 | 334 | // Compute the parent element's index, and fetch it. 335 | var parentN = ((n + 1) >> 1) - 1, 336 | parent = this.content[parentN]; 337 | // Swap the elements if the parent is greater. 338 | if (this.scoreFunction(element) < this.scoreFunction(parent)) { 339 | this.content[parentN] = element; 340 | this.content[n] = parent; 341 | // Update 'n' to continue at the new position. 342 | n = parentN; 343 | } 344 | // Found a parent that is less, no need to sink any further. 345 | else { 346 | break; 347 | } 348 | } 349 | }, 350 | bubbleUp: function(n) { 351 | // Look up the target element and its score. 352 | var length = this.content.length, 353 | element = this.content[n], 354 | elemScore = this.scoreFunction(element); 355 | 356 | while(true) { 357 | // Compute the indices of the child elements. 358 | var child2N = (n + 1) << 1, 359 | child1N = child2N - 1; 360 | // This is used to store the new position of the element, if any. 361 | var swap = null, 362 | child1Score; 363 | // If the first child exists (is inside the array)... 364 | if (child1N < length) { 365 | // Look it up and compute its score. 366 | var child1 = this.content[child1N]; 367 | child1Score = this.scoreFunction(child1); 368 | 369 | // If the score is less than our element's, we need to swap. 370 | if (child1Score < elemScore){ 371 | swap = child1N; 372 | } 373 | } 374 | 375 | // Do the same checks for the other child. 376 | if (child2N < length) { 377 | var child2 = this.content[child2N], 378 | child2Score = this.scoreFunction(child2); 379 | if (child2Score < (swap === null ? elemScore : child1Score)) { 380 | swap = child2N; 381 | } 382 | } 383 | 384 | // If the element needs to be moved, swap it, and continue. 385 | if (swap !== null) { 386 | this.content[n] = this.content[swap]; 387 | this.content[swap] = element; 388 | n = swap; 389 | } 390 | // Otherwise, we are done. 391 | else { 392 | break; 393 | } 394 | } 395 | } 396 | }; 397 | 398 | return { 399 | astar: astar, 400 | Graph: Graph 401 | }; 402 | 403 | }); -------------------------------------------------------------------------------- /control.js: -------------------------------------------------------------------------------- 1 | // TODO: Would be nice to make the structure nicer - the way how you add different types of control like keyboard input, mouse-click, npc-follow 2 | 3 | // TODO: Add goto a given position 4 | 5 | // TODO: Maybe fuse into Component.js 6 | 7 | /** 8 | * attach it to a Component to add control: Following, Mouse Control -> goto click 9 | * @param obj 10 | */ 11 | function control(obj) { 12 | this.obj = obj; 13 | 14 | // Follow Properties 15 | this.doFollow = false; 16 | this.route; 17 | this.routeIndex; 18 | 19 | // Mouse/Touch move 20 | this.swipMove = false; 21 | 22 | // Goto Click 23 | this.goto = false; 24 | 25 | this.finished = true; 26 | 27 | this.setTarget = function(target) { 28 | this.target = target; 29 | 30 | return this; 31 | } 32 | 33 | this.update = function() { 34 | if (this.target != undefined && this.doFollow) { 35 | this.destX = Math.floor((this.target.x+this.target.offset_x)/16); 36 | this.destY = Math.floor((this.target.y+this.target.offset_y)/16); 37 | this.follow(); 38 | } 39 | 40 | if (this.swipMove && myGameArea.mousedown) { 41 | this.gestureMove(); 42 | } 43 | 44 | if (this.goto) { 45 | if (myGameArea.clicked) { 46 | // Update new click only if finished 47 | if (this.finished) { 48 | this.destX = Math.floor((myGameArea.clickdownX+myGameArea.gameCamera.x)/16); 49 | this.destY = Math.floor((myGameArea.clickdownY+myGameArea.gameCamera.y)/16); 50 | this.finished = false; 51 | } 52 | // Disable Key Control while going to obj 53 | obj.disableControls = true; 54 | 55 | this.gotoClick(); 56 | } 57 | 58 | } 59 | } 60 | 61 | this.drawRoute = function() { 62 | if (this.rects) 63 | for (i = this.routeIndex; i < this.rects.length; i++) this.rects[i].draw(myGameArea.context); 64 | } 65 | 66 | // Follow a given object 67 | this.follow = function() { 68 | 69 | // No collision: Take direct route 70 | if (!this.obj.collidable) { 71 | this.directPath(); 72 | } 73 | 74 | // Map collision: Take astar route 75 | else { 76 | if (!this.obj.ComponentCollision(this.target)) { 77 | // Create / Update route 78 | this.createRoute(this.destX, this.destY); 79 | 80 | // Follow the route 81 | this.followRoute(); 82 | } 83 | } 84 | } 85 | 86 | // Go to a clicked position 87 | this.gotoClick = function() { 88 | // Create routet 89 | if (this.route == undefined) { 90 | this.createRoute(this.destX, this.destY); 91 | } 92 | 93 | // Follow the route 94 | this.followRoute(); 95 | 96 | if (this.finished) { 97 | myGameArea.clicked = false; 98 | } 99 | } 100 | 101 | this.directPath = function() { 102 | if (Math.floor((this.obj.x+this.obj.offset_x)/16) < Math.floor((this.target.x+this.target.offset_x)/16)) 103 | this.obj.speedX = this.obj.speed; 104 | else if (Math.floor((this.obj.x+this.obj.offset_x)/16) > Math.floor((this.target.x+this.target.offset_x)/16)) 105 | this.obj.speedX = -this.obj.speed; 106 | else if (Math.floor((this.obj.y+this.obj.offset_y)/16) < Math.floor((this.target.y+this.target.offset_y)/16)) 107 | this.obj.speedY = this.obj.speed; 108 | else if (Math.floor((this.obj.y+this.obj.offset_y)/16) > Math.floor((this.target.y+this.target.offset_y)/16)) 109 | this.obj.speedY = -this.obj.speed; 110 | } 111 | 112 | this.createRoute = function(x, y) { 113 | this.route = astarPath(Math.floor((this.obj.x+this.obj.offset_x)/16), Math.floor((this.obj.y+this.obj.offset_y)/16), x, y); 114 | this.routeIndex = 0; 115 | } 116 | 117 | this.followRoute = function() { 118 | if (this.route != undefined) { 119 | if (this.route.length != 0) { 120 | 121 | // else-if to prevent diagonal movement 122 | if (Math.floor((this.obj.x+this.obj.offset_x)/16) < this.route[this.routeIndex].x) 123 | this.obj.speedX = this.obj.speed; 124 | else if (Math.floor((this.obj.x+this.obj.offset_x)/16) > this.route[this.routeIndex].x) 125 | this.obj.speedX -= this.obj.speed; 126 | else if (Math.floor((this.obj.y+this.obj.offset_y)/16) < this.route[this.routeIndex].y) 127 | this.obj.speedY = this.obj.speed; 128 | else if (Math.floor((this.obj.y+this.obj.offset_y)/16) > this.route[this.routeIndex].y) 129 | this.obj.speedY = -this.obj.speed; 130 | 131 | // To show the Path 132 | if (myGameArea.debug) { 133 | this.rects = []; 134 | for (i = 0; i < this.route.length; i++) this.rects[i] = new Component(this.route[i].x * 16, this.route[i].y * 16).rectangle(16, 16, "black", false, "yellow", true); 135 | } 136 | 137 | // Increase routeIndex to follow the given path until the destination is reached 138 | if ((Math.abs(this.route[this.routeIndex].x-((this.obj.x+this.obj.offset_x)/16)) < 1.0) && (Math.abs(this.route[this.routeIndex].y-((this.obj.y+this.obj.offset_y)/16)) < 1.0)) { 139 | // Target not reached 140 | if (this.route.length - 1 > this.routeIndex) { 141 | this.routeIndex++; 142 | } 143 | // Target reached 144 | else { 145 | //console.log("Reached"); 146 | this.route = undefined; 147 | this.rects = undefined; 148 | this.obj.disableControls = false; 149 | this.finished = true; 150 | } 151 | } 152 | } 153 | // Not reachable 154 | else { 155 | //console.log("Not reachable!") 156 | this.route = undefined; 157 | this.rects = undefined; 158 | this.obj.disableControls = false; 159 | this.finished = true; 160 | } 161 | if (this.obj.collided) { 162 | //console.log("Collided"); 163 | this.route = undefined; 164 | this.obj.disableControls = false; 165 | this.rects = undefined; 166 | this.finished = true; 167 | } 168 | } 169 | } 170 | 171 | // ############################################### 172 | // ### Doesn't works so well with tablet touch ### 173 | // ############################################### 174 | 175 | this.gestureMove = function() { 176 | if (Math.abs(myGameArea.x - myGameArea.clickdownX) > Math.abs(myGameArea.y - myGameArea.clickdownY)) { 177 | if (myGameArea.x < myGameArea.clickdownX - 4) 178 | obj.speedX -= obj.speed; 179 | else if (myGameArea.x > myGameArea.clickdownX + 4) 180 | obj.speedX += obj.speed; 181 | } 182 | else { 183 | if (myGameArea.y < myGameArea.clickdownY - 4) 184 | obj.speedY -= obj.speed; 185 | else if (myGameArea.y > myGameArea.clickdownY + 4) 186 | obj.speedY += obj.speed; 187 | } 188 | } 189 | 190 | this.drawSwip = function() { 191 | if (myGameArea.mousedown) { 192 | ctx = myGameArea.context; 193 | ctx.beginPath(); 194 | ctx.arc(myGameArea.clickdownX, myGameArea.clickdownY, 5, 0, 2 * Math.PI, true); 195 | ctx.arc(myGameArea.x, myGameArea.y, 5, 0, 2 * Math.PI, true); 196 | // Fill 197 | ctx.fillStyle = "black"; 198 | ctx.fill(); 199 | // Outline 200 | ctx.strokeStyle = "black"; 201 | ctx.stroke(); 202 | } 203 | } 204 | } -------------------------------------------------------------------------------- /core.js: -------------------------------------------------------------------------------- 1 | // UTILITIES ------------------------------------------------------------------- 2 | // Convert 2dim array coordinates to 1dim array coordinates 3 | function xy2i(x, y, width) { 4 | var index = y * width + x; 5 | return index; 6 | } 7 | 8 | // Convert 1dim array coordinates to 2dim array coordinates 9 | function i2xy(index, width) { 10 | var x = index % width; 11 | var y = Math.floor(index / width); 12 | return [x, y]; 13 | } 14 | 15 | // Rectangle collision 16 | function rectangleOverlap(x1, y1, w1, h1, x2, y2, w2, h2) { 17 | if ((x1 + w1 < x2) || (x2 + w2 < x1) || (y1 + h1 < y2) || (y2 + h2 < y1)) return false; 18 | return true; 19 | } 20 | 21 | // Rectangle mid 22 | function rectangleMid(x, y, w, h) { 23 | return [x + w / 2, y + h / 2]; 24 | } 25 | 26 | // Euclidean distance between to points on a cartesian grid 27 | function distance(xy1, xy2) { 28 | return Math.sqrt(Math.pow(xy1[0] - xy2[0], 2) + Math.pow(xy1[1] - xy2[1], 2)); 29 | } 30 | 31 | /** 32 | * Check if array contains object with the given id 33 | * If it contains it return it else return undefined 34 | */ 35 | function containsID(data, id) { 36 | for (var i = 0, l = data.length; i < l; i++) 37 | if (data[i].id == id) return data[i]; 38 | return undefined; 39 | } 40 | 41 | /** 42 | * Check if an array contains the component with the given id and mapID 43 | * If it contains it return it else return undefined 44 | * @param the array (i.e. maps.data) 45 | * @param the object (i.e. 3) 46 | */ 47 | function containsComponent(data, id, mapID) { 48 | for (var i = 0, l = data.length; i < l; i++) 49 | if (data[i].id == id && data[i].mapID == mapID) return data[i]; 50 | return undefined; 51 | } 52 | 53 | // Not for nested arrays 54 | function arrayEqual(arr1, arr2) { 55 | if (arr1.length != arr2.length) return false; 56 | for (var i = 0, l = arr1.length; i < l; i++) 57 | if (arr1[i] !== arr2[i]) return false 58 | return true; 59 | } 60 | 61 | function teleportComponent(component, map, x, y) { 62 | // Set new position 63 | if (x != null) component.x = x; 64 | if (y != null) component.y = y; 65 | // Remove component from current map and add to new 66 | if (game.currentMap != map) { 67 | for (var i = 0, l = maps.data[game.currentMap].components.data.length; i < l; i++) { 68 | if (maps.data[game.currentMap].components.data[i] == component) 69 | maps.data[map].components.data.push(maps.data[game.currentMap].components.data.splice(i, 1)[0]); 70 | } 71 | } 72 | } 73 | 74 | /** 75 | * For loading src: Check if the file name already exists 76 | */ 77 | function loadImage(img) { 78 | var tmp; 79 | for (var i = 0, l = spritesheets.data.length; i < l; i++) { 80 | if (spritesheets.data[i].img.src.match(/[\w]+\.[A-Za-z]{3}$/)[0] == img.match(/[\w]+\.[A-Za-z]{3}$/)[0]) { 81 | console.log("spritesheet already exists: taking existing one"); 82 | tmp = spritesheets.data[i]; 83 | } 84 | } 85 | if (tmp == undefined) { 86 | console.log("created new spritesheet") 87 | addSprite(img, 60, 32, 8, 8); 88 | tmp = spritesheets.data[spritesheets.data.length - 1]; 89 | } 90 | //maps.data[game.currentMap].image.src = img; //maps background image 91 | maps.data[game.currentMap].tileset = tmp; 92 | maps.data[game.currentMap].switchTileset(); 93 | 94 | setTimeout(function () { 95 | maps.data[game.currentMap].drawCache(); 96 | maps.data[game.currentMap].drawTileset(); 97 | }, 100); 98 | } 99 | 100 | // Harp: audio.data[0-7].play(); 101 | var myHarp = (function () { 102 | var note = 0; 103 | var melody = [0, 4, 6, 7, 6, 4, 1, 2, 5, 4, 0, 3, 2, 1, 0, 2, 3, 1, 0, 7, 6, 4, 3, 2, 5, 7, 6, 4]; 104 | var melodyIndex = 0; 105 | var playing = false; 106 | 107 | function change(mode) { 108 | // Random note 109 | if (mode == 0) note = Math.round(Math.random() * 7); 110 | // Melody note 111 | if (mode == 1) { 112 | melodyIndex++; 113 | if (melodyIndex == melody.length) melodyIndex = 0; 114 | note = melody[melodyIndex]; 115 | } 116 | } 117 | return { 118 | currentNote: function () { 119 | return note; 120 | }, 121 | isPlaying: function () { 122 | return playing; 123 | }, 124 | startPlaying: function () { 125 | playing = true; 126 | }, 127 | stopPlaying: function () { 128 | playing = false; 129 | }, 130 | play: function (mode) { 131 | if (playing) { 132 | if (audio.data[note].ended) change(mode); 133 | audio.data[note].play(); 134 | } 135 | } 136 | }; 137 | })(); 138 | 139 | // Timer: init() sets the start time to now, value() gets the start time, check(seconds) tells if "seconds" have passed 140 | var Timer = function () { 141 | var start; 142 | 143 | function initStart() { 144 | start = new Date(); 145 | } 146 | return { 147 | init: function () { 148 | initStart(); 149 | }, 150 | value: function () { 151 | return start; 152 | }, 153 | check: function (seconds) { 154 | var tmp = new Date(); 155 | if (Math.floor((tmp - start) / 1000 >= seconds)) return true; 156 | else return false; 157 | } 158 | } 159 | }; 160 | 161 | //convert listmap to a grid 162 | function getGrid(maplayer, width, height) { 163 | 164 | arr = []; 165 | k = 0; 166 | for (i = 0; i < height; i++) { 167 | tmp = []; 168 | for (j = 0; j < width; j++) { 169 | tmp[j] = maplayer[k]; 170 | k++; 171 | } 172 | arr[i] = tmp; 173 | } 174 | 175 | return arr; 176 | } 177 | 178 | //convert mapCL to a graph 179 | function getGraph() { 180 | var mapCL = maps.data[game.currentMap].layerC; 181 | var mapWidth = maps.data[game.currentMap].mapWidth; 182 | var mapHeight = maps.data[game.currentMap].mapHeight; 183 | 184 | var arr = Array(); 185 | for (var i = 0; i < mapWidth; i++) { 186 | arr.push(Array()); 187 | } 188 | for (var i = 0; i < mapWidth * mapHeight; i++) { 189 | if (mapCL[i]) 190 | arr[i % mapWidth][Math.floor(i / mapWidth)] = 1; 191 | else 192 | arr[i % mapWidth][Math.floor(i / mapWidth)] = 0; 193 | } 194 | return arr; 195 | } 196 | 197 | // Get shortest path using astar algorithm 198 | function astarPath(startX, startY, endX, endY) { 199 | var graph = new Graph(getGraph()); 200 | // Check if start and end are valid positions (= not outside of the graph grid) 201 | if (startX < 0 || startX >= graph.grid.length || startY < 0 || startY >= graph.grid[0].length) return undefined; 202 | if (endX < 0 || endX >= graph.grid.length || endY < 0 || endY >= graph.grid[0].length) return undefined; 203 | var start = graph.grid[startX][startY]; 204 | var end = graph.grid[endX][endY]; 205 | //console.log("Start = ["+startX+"]["+startY+"]"); 206 | //console.log("End = ["+endX+"]["+endY+"]"); 207 | return astar.search(graph, start, end, { 208 | closest: true 209 | }); 210 | } 211 | 212 | function DisableScrollbar() { 213 | document.documentElement.style.overflow = 'hidden'; 214 | document.body.scoll = "no"; 215 | } 216 | 217 | function EnableScrollbar() { 218 | document.documentElement.style.overflow = 'visible'; 219 | document.body.scroll = "yes"; 220 | } 221 | 222 | function enterFullscreen() { 223 | element = game.canvas; 224 | if (element.requestFullscreen) { 225 | element.requestFullscreen(); 226 | } else if (element.mozRequestFullScreen) { 227 | element.mozRequestFullScreen(); 228 | } else if (element.webkitRequestFullscreen) { 229 | element.webkitRequestFullscreen(); 230 | } else if (element.msRequestFullscreen) { 231 | element.msRequestFullscreen(); 232 | } 233 | } 234 | 235 | /** 236 | * Use localStorage to save game state 237 | * TODO: Change to save all the game state values 238 | */ 239 | function saveGameState(address, value) { 240 | // Check browser support 241 | if (typeof (Storage) !== "undefined") { 242 | // Store 243 | localStorage.setItem(address, value); 244 | console.log("Stored: " + value); 245 | } else { 246 | alert("Sorry, your browser does not support Web Storage..."); 247 | } 248 | } 249 | 250 | /** 251 | * Use localStorage to load game state 252 | * TODO: Change to load all the game state values & apply them 253 | */ 254 | function loadGameState(address) { 255 | // Check browser support 256 | if (typeof (Storage) !== "undefined") { 257 | var value = localStorage.getItem(address); 258 | // Retrieve 259 | console.log("Retrieved: " + value); 260 | } else { 261 | alert("Sorry, your browser does not support Web Storage..."); 262 | } 263 | } 264 | 265 | function toggle(boolean) { 266 | if (boolean) return false; 267 | return true; 268 | } 269 | 270 | function blackTransition() { 271 | ctx = game.context; 272 | ctx.fillStyle = "black"; 273 | ctx.fillRect(0, 0, game.canvas.width, game.canvas.height); 274 | } 275 | 276 | function showTime() { 277 | ctx = game.context; 278 | ctx.font = "bold 20px Serif"; 279 | ctx.fillStyle = "black"; 280 | ctx.fillText("Timer : " + Math.floor(time / 1000), 5, 20); 281 | } 282 | 283 | // Init FPS and time 284 | var start, before, now, time, fps; 285 | start = Date.now(); 286 | before = Date.now(); 287 | fps = 0; 288 | 289 | function everyinterval(n) { 290 | if ((game.frameNo / n) % 1 == 0) return true; 291 | return false; 292 | } 293 | 294 | function updateFPS() { 295 | now = Date.now(); 296 | time = now - start; 297 | if (everyinterval(30)) fps = Math.floor(1000 / (now - before)); 298 | before = now; 299 | } 300 | 301 | function showFPS() { 302 | ctx = game.context; 303 | ctx.font = "bold 20px Serif"; 304 | ctx.fillStyle = "black"; 305 | ctx.fillText("FPS : " + fps, 5, 40); 306 | } 307 | 308 | function showPosition(target) { 309 | ctx = game.context; 310 | ctx.fillStyle = "black"; 311 | ctx.fillText("x:" + (target.x + target.boundingBox.x) + ", y:" + (target.y + target.boundingBox.y), 5, 60); 312 | ctx.fillText("Tile[" + Math.floor((target.x + target.boundingBox.x) / spritesheets.data[maps.data[game.currentMap].spritesheetID].spriteWidth) + ", " + Math.floor((target.y + target.boundingBox.y) / spritesheets.data[maps.data[game.currentMap].spritesheetID].spriteHeight) + "]", 5, 80); 313 | } 314 | 315 | function showDistance() { 316 | /* 317 | ctx = game.context; 318 | ctx.beginPath(); 319 | ctx.lineWidth = 2; //2px 320 | ctx.moveTo(x1, y1); 321 | ctx.lineTo(x2, y2); 322 | ctx.strokeStyle = black; 323 | ctx.stroke(); 324 | */ 325 | } 326 | 327 | // Button functions 328 | 329 | function debugButton() { 330 | game.debug = toggle(game.debug); 331 | maps.data[game.currentMap].drawCache(); 332 | if (game.debug) document.getElementById("debugButton").setAttribute("class", "w3-button w3-green"); 333 | else document.getElementById("debugButton").setAttribute("class", "w3-button w3-red"); 334 | } 335 | 336 | function guiButton() { 337 | game.info = toggle(game.info); 338 | if (game.info) document.getElementById("guiButton").setAttribute("class", "w3-button w3-green"); 339 | else document.getElementById("guiButton").setAttribute("class", "w3-button w3-red"); 340 | } 341 | 342 | // ########## 343 | // Map Editor 344 | // ########## 345 | 346 | // Layer Selection 347 | function openlayerButton() { 348 | if (game.dom.layerSelection.className.indexOf("w3-show") == -1) game.dom.layerSelection.className += " w3-show"; 349 | else game.dom.layerSelection.className = game.dom.layerSelection.className.replace(" w3-show", ""); 350 | } 351 | 352 | function layerButton(i) { 353 | game.currentLayer = i; 354 | game.dom.layer1.setAttribute("class", "w3-bar-item w3-button w3-blue"); 355 | game.dom.layer2.setAttribute("class", "w3-bar-item w3-button w3-blue"); 356 | game.dom.layer3.setAttribute("class", "w3-bar-item w3-button w3-blue"); 357 | game.dom.layerSelection.className = game.dom.layerSelection.className.replace(" w3-show", ""); 358 | game.dom.collisionLayer.setAttribute("class", "w3-bar-item w3-button w3-blue"); 359 | if (i == 0) { 360 | game.dom.layer1.setAttribute("class", "w3-bar-item w3-button w3-green"); 361 | game.dom.currentLayer.innerHTML = "Layer 1"; 362 | } 363 | if (i == 1) { 364 | game.dom.layer2.setAttribute("class", "w3-bar-item w3-button w3-green"); 365 | game.dom.currentLayer.innerHTML = "Layer 2"; 366 | } 367 | if (i == 2) { 368 | game.dom.layer3.setAttribute("class", "w3-bar-item w3-button w3-green"); 369 | game.dom.currentLayer.innerHTML = "Layer 3"; 370 | } 371 | if (i == 3) { 372 | game.dom.collisionLayer.setAttribute("class", "w3-bar-item w3-button w3-green"); 373 | game.dom.currentLayer.innerHTML = "Collision"; 374 | // Enable debug to see collision restriction 375 | if (!game.debug) debugButton(); 376 | // Show restricition selection 377 | game.dom.currentRestriction.className = game.dom.currentRestriction.className.replace("w3-hide", ""); 378 | 379 | } 380 | // Hide restriction selection 381 | else if (game.dom.currentRestriction.className.indexOf("w3-hide") == -1) game.dom.currentRestriction.className += " w3-hide"; 382 | // Enable draw 383 | if (!game.drawingOn) drawButton(); 384 | } 385 | 386 | // Collision Restriction Selection 387 | function openCollisionButton() { 388 | if (game.dom.restrictionSelection.className.indexOf("w3-show") == -1) game.dom.restrictionSelection.className += " w3-show"; 389 | else game.dom.restrictionSelection.className = game.dom.restrictionSelection.className.replace(" w3-show", ""); 390 | } 391 | 392 | function restrictionButton(i, src) { 393 | game.dom.restrictionSelection.className = game.dom.restrictionSelection.className.replace(" w3-show", ""); 394 | game.dom.currentRestriction.src = src; 395 | if (i == 0) game.tileCollisionType = 1; // Full Restriction 396 | if (i == 1) game.tileCollisionType = [1, 2, 3]; // Down, Left, Right Restriction 397 | if (i == 2) game.tileCollisionType = [0, 2, 3]; // Up, Left, Right Restriction 398 | if (i == 3) game.tileCollisionType = [0, 1, 3]; // Up, Down, Right Restriction 399 | if (i == 4) game.tileCollisionType = [0, 1, 2]; // Up, Down, Left Restriction 400 | if (i == 5) game.tileCollisionType = [2, 3]; // Left, Right Restriction 401 | if (i == 6) game.tileCollisionType = [1, 3]; // Down, Right Restriction 402 | if (i == 7) game.tileCollisionType = [1, 2]; // Down, Left Restriction 403 | if (i == 8) game.tileCollisionType = [0, 3]; // Up, Right Restriction 404 | if (i == 9) game.tileCollisionType = [0, 2]; // Up, Left Restriction 405 | if (i == 10) game.tileCollisionType = [0, 1]; // Up, Down Restriction 406 | if (i == 11) game.tileCollisionType = [3]; // Right Restriction 407 | if (i == 12) game.tileCollisionType = [2]; // Left Restriction 408 | if (i == 13) game.tileCollisionType = [1]; // Down Restriction 409 | if (i == 14) game.tileCollisionType = [0]; // Up Restriction 410 | if (i == 15) game.tileCollisionType = 0; // No restriction 411 | } 412 | 413 | // Enable draw button 414 | function drawButton() { 415 | game.drawingOn = toggle(game.drawingOn); 416 | if (game.drawingOn) { 417 | game.control.disableMouse = true; 418 | document.getElementById("drawButton").setAttribute("class", "w3-button w3-green"); 419 | } else { 420 | game.control.disableMouse = false; 421 | document.getElementById("drawButton").setAttribute("class", "w3-button w3-red"); 422 | } 423 | } 424 | 425 | // Game/Engine Start Button 426 | function startButton() { 427 | document.getElementById("startButton").parentNode.removeChild(document.getElementById("startButton")); 428 | // Display Editor Buttons 429 | document.getElementById("editorButtons").className += document.getElementById("editorButtons").className.replace(" w3-hide", ""); 430 | // Initalize the game 431 | game.init(); 432 | // Start after a short timeout 433 | setTimeout(game.start, 200); 434 | } 435 | 436 | // IMAGES ---------------------------------------------------------------------- 437 | /** 438 | * Contains all the images of the game 439 | */ 440 | var images = { 441 | data: [], // Contains all loaded images 442 | freeIDs: [] // FreeIDs 443 | }; 444 | 445 | /** 446 | * For adding a new image 447 | * @param {file} image src 448 | */ 449 | function addImage(image) { 450 | // Add 451 | images.data.push(new Image()); 452 | images.data[images.data.length - 1].src = image; 453 | // ID management 454 | if (images.freeIDs.length > 0) images.data[images.data.length - 1].id = images.freeIDs[0].pop(); 455 | else images.data[images.data.length - 1].id = images.data.length - 1; 456 | } 457 | 458 | /** 459 | * Remove an image 460 | */ 461 | function removeImage(id) { 462 | for (var i = 0, l = images.data.length; i < l; i++) 463 | if (images.data[i].id == id) { 464 | // Remove if ID found 465 | images.data.splice(id, 1); 466 | // ID Management 467 | images.freeIDs.push(id); 468 | break; 469 | } 470 | } 471 | 472 | /** 473 | * Generate the data for loading the images 474 | */ 475 | function generateImageData() { 476 | // Datastring 477 | for (var i = 0, l = images.data.length; i < l; i++) 478 | game.data += "addImage('" + images.data[i].src + "');\n"; 479 | } 480 | 481 | // AUDIO --------------------------------------------------------------------- 482 | 483 | /** 484 | * Contains all the audio of the game 485 | */ 486 | var audio = { 487 | data: [], // The audios 488 | freeIDs: [] // freeIDs 489 | } 490 | 491 | /** 492 | * Add a new audio 493 | * @param {file} src 494 | */ 495 | function addAudio(src) { 496 | // Add 497 | audio.data.push(new Audio(src)); 498 | // ID Management 499 | if (audio.freeIDs.length != 0) audio.data[audio.data.length - 1].id = audio.freeIDs.pop(); 500 | else audio.data[audio.data.length - 1].id = audio.data.length - 1; 501 | // Sets default volume 0.2 for all added audios; maybe include as parameter on creating 502 | audio.data[audio.data.length - 1].volume = 0.2; 503 | } 504 | 505 | /** 506 | * Remove an audio 507 | */ 508 | function removeAudio(id) { 509 | for (var i = 0, l = audio.data.length; i < l; i++) 510 | if (audio.data[i].id == id) { 511 | // Remove if ID found 512 | audio.data.splice(id, 1); 513 | // ID Management 514 | audio.freeIDs.push(id); 515 | break; 516 | } 517 | } 518 | 519 | /** 520 | * Generate the data for the audios 521 | */ 522 | function generateAudioData() { 523 | // Datastring 524 | for (var i = 0, l = audio.data.length; i < l; i++) 525 | game.data += "addAudio('" + audio.data[i].src + "');\n"; 526 | } 527 | 528 | // SPRITESHEETS ---------------------------------------------------------------- 529 | 530 | /** 531 | * Contains all the spritesheets of the game 532 | */ 533 | var spritesheets = { 534 | data: [], // Contains the spritesheets 535 | freeIDs: [] // Free IDs 536 | }; 537 | 538 | /** 539 | * For adding a new spritesheet 540 | */ 541 | function addSprite(src, spritesX, spritesY, spriteWidth, spriteHeight, name) { 542 | // Add 543 | spritesheets.data.push(new Spritesheet(src, spritesX, spritesY, spriteWidth, spriteHeight, name)); 544 | // ID management 545 | if (spritesheets.freeIDs.length > 0) spritesheets.data[spritesheets.data.length - 1].id = spritesheets.freeIDs[0].pop(); 546 | else spritesheets.data[spritesheets.data.length - 1].id = spritesheets.data.length - 1; 547 | } 548 | 549 | /** 550 | * Remove a spritesheet 551 | */ 552 | function removeSprite(id) { 553 | for (var i = 0, l = spritesheets.data.length; i < l; i++) 554 | if (spritesheets.data[i].id == id) { 555 | // Remove if ID found 556 | spritesheets.data.splice(id, 1); 557 | // ID Management 558 | spritesheets.freeIDs.push(id); 559 | break; 560 | } 561 | } 562 | 563 | /** 564 | * Generate the data for the spritesheets 565 | */ 566 | function generateSpriteData() { 567 | // Datastring 568 | for (var i = 0, l = spritesheets.data.length; i < l; i++) 569 | game.data += "addSprite(" + spritesheets.data[i].imageID + ", " + +spritesheets.data[i].spritesX + ", " + +spritesheets.data[i].spritesY + ", " + +spritesheets.data[i].spriteWidth + ", " + +spritesheets.data[i].spriteHeight + ");\n"; 570 | } 571 | 572 | /** 573 | * Spritesheet for map-tiles and objects 574 | */ 575 | function Spritesheet(imageID, spritesX, spritesY, spriteWidth, spriteHeight) { 576 | this.imageID = imageID; 577 | this.width = spritesX * spriteWidth; 578 | this.height = spritesY * spriteHeight; 579 | this.spritesX = spritesX; 580 | this.spritesY = spritesY; 581 | this.spriteWidth = spriteWidth; 582 | this.spriteHeight = spriteHeight; 583 | this.name = images.data[this.imageID].src.match(/[\w]+\.[A-Za-z]{3}$/)[0]; 584 | } 585 | 586 | /** 587 | * Draw a specific sprite of a spritesheet 588 | * @param the context where to draw it 589 | * @param the spritesheet 590 | * @param the specific sprite 591 | * @param x position 592 | * @param y position 593 | */ 594 | function drawSprite(ctx, spritesheet, number, x, y) { 595 | var res = i2xy(number, Math.max(spritesheet.spritesX, spritesheet.spritesY)); 596 | ctx.drawImage(images.data[spritesheet.imageID], res[0] * spritesheet.spriteWidth, res[1] * spritesheet.spriteHeight, spritesheet.spriteWidth, spritesheet.spriteHeight, x, y, spritesheet.spriteWidth, spritesheet.spriteHeight); 597 | } 598 | 599 | // MAPS ---------------------------------------------------------------------- 600 | 601 | /** 602 | * Contains all the maps of the game 603 | */ 604 | var maps = { 605 | data: [], // Contains the maps 606 | freeIDs: [] // Free IDs 607 | }; 608 | 609 | /** 610 | * For adding a new map 611 | * @param imageID of the background panorama 612 | * @param spritesheetID of the tile spritesheet 613 | * @param the maps number of tiles in x direction 614 | * @param the maps number of tiles in y direction 615 | */ 616 | function addMap(imageID, spritesheetID, mapWidth, mapHeight) { 617 | // Add 618 | maps.data.push(new Map(imageID, spritesheetID, mapWidth, mapHeight)); 619 | // ID management 620 | if (maps.freeIDs.length > 0) maps.data[maps.data.length - 1].id = maps.freeIDs[0].pop(); 621 | else maps.data[maps.data.length - 1].id = maps.data.length - 1; 622 | } 623 | 624 | /** 625 | * Remove a map 626 | */ 627 | function removeMap(id) { 628 | for (var i = 0, l = maps.data.length; i < l; i++) 629 | if (maps.data[i].id == id) { 630 | // Remove if ID found 631 | maps.data.splice(i, 1); 632 | // ID Management 633 | maps.freeIDs.push(id); 634 | break; 635 | } 636 | } 637 | 638 | /** 639 | * Generate the data for the maps 640 | * TODO: INCLUDE LAYER DATA and for this add a TILE EVENTS LAYER 641 | */ 642 | function generateMapData() { 643 | // Datastring 644 | for (var i = 0, l = maps.data.length; i < l; i++) 645 | game.data += "addMap(" + maps.data[i].imageID + ", " + maps.data[i].spritesheetID + ", " + maps.data[i].mapWidth + ", " + maps.data[i].mapHeight + ");\n"; 646 | } 647 | 648 | /** 649 | * A Tile 650 | * @param spritesheetID 651 | */ 652 | function Tile(spritesheetID, x, y) { 653 | this.spritesheetID = spritesheetID; 654 | this.width = spritesheets.data[this.spritesheetID].spriteWidth; 655 | this.height = spritesheets.data[this.spritesheetID].spriteHeight; 656 | // Position 657 | this.x = x; 658 | this.y = y; 659 | // Layers: Set which part of the spritesheet should be drawn 660 | this.layer1 = 0; 661 | this.layer2 = 0; 662 | this.layer3 = 0; 663 | // Collision 664 | this.collision = 0; 665 | 666 | /** This draws the tile on the cached canvas' 667 | * @param Cached canvas context for layer 1 & 2 (Background) 668 | * @param Cached canvas context for layer 3 (Foreground) 669 | */ 670 | this.draw = function (ctx1, ctx2) { 671 | if (this.layer1 - 1 >= 0) drawSprite(ctx1, spritesheets.data[this.spritesheetID], this.layer1 - 1, this.x, this.y); 672 | if (this.layer2 - 1 >= 0) drawSprite(ctx1, spritesheets.data[this.spritesheetID], this.layer2 - 1, this.x, this.y); 673 | if (this.layer3 - 1 >= 0) drawSprite(ctx2, spritesheets.data[this.spritesheetID], this.layer3 - 1, this.x, this.y); 674 | // Debug information 675 | if (game.debug) { 676 | //Draw Collision Restriction of the tiles 677 | game.cgx3.globalAlpha = 0.3; 678 | if (this.collision === 0) game.cgx3.fillStyle = "blue"; 679 | else if (this.collision === 1) game.cgx3.fillStyle = "red"; 680 | else game.cgx3.fillStyle = "magenta"; 681 | game.cgx3.fillRect(this.x, this.y, this.width, this.height); 682 | game.cgx3.globalAlpha = 1.0; 683 | } 684 | } 685 | } 686 | 687 | /** 688 | * Define a map 689 | * @param imageID of background panorama 690 | * @param spritesheetID of the tiles spritesheet 691 | * @param the maps number of tiles in x direction 692 | * @param the maps number of tiles in y direction 693 | */ 694 | function Map(imageID, spritesheetID, mapWidth, mapHeight) { 695 | 696 | // Panorama Image 697 | if (imageID != undefined) { 698 | this.imageID = imageID; 699 | // Panorama Properties 700 | this.x = 0; 701 | this.y = 0; 702 | this.speedX = 0; 703 | this.speedY = 0; 704 | } 705 | 706 | // Tileset Spritesheet 707 | this.spritesheetID = spritesheetID; 708 | 709 | // Map Properties 710 | this.mapWidth = mapWidth; 711 | this.mapHeight = mapHeight; 712 | 713 | // Pixel width & height 714 | this.width = this.mapWidth * spritesheets.data[this.spritesheetID].spriteWidth; 715 | this.height = this.mapHeight * spritesheets.data[this.spritesheetID].spriteHeight; 716 | 717 | // The name is the filename 718 | this.name = spritesheets.data[this.spritesheetID].name.match(/[\w]+/)[0]; 719 | 720 | // Contains all the tiles of this map 721 | // A tile is a component which contains the layers and collision / stepOnEventID / interactEventID 722 | this.tiles = []; 723 | for (var i = 0, l = this.mapWidth * this.mapHeight; i < l; i++) { 724 | var res = i2xy(i, this.mapWidth); 725 | this.tiles.push(new Tile(this.spritesheetID, res[0] * spritesheets.data[this.spritesheetID].spriteWidth, res[1] * spritesheets.data[this.spritesheetID].spriteHeight)); 726 | } 727 | 728 | // Contains all components of this map 729 | this.components = { 730 | data: [], // Contains the components 731 | freeIDs: [] // Free IDs 732 | } 733 | 734 | /** 735 | * Load layers + collision & eventIDs into the the map 736 | * @param layer1 (background) 737 | * @param layer2 (background) 738 | * @param layer3 (foreground) 739 | * @param collision 740 | * @param stepOnEventID 741 | * @param interactEventID 742 | */ 743 | this.loadLayers = function (l1, l2, l3, collision) { 744 | for (var i = 0, l = this.mapWidth * this.mapHeight; i < l; i++) { 745 | this.tiles[i].layer1 = l1[i]; 746 | this.tiles[i].layer2 = l2[i]; 747 | this.tiles[i].layer3 = l3[i]; 748 | this.tiles[i].collision = collision[i]; 749 | } 750 | } 751 | 752 | /** 753 | * If the cached images need to be updated 754 | */ 755 | this.drawCache = function () { 756 | // Adjust the cache canvas' size 757 | game.panorama.width = game.canvas.width; 758 | game.panorama.height = game.canvas.height; 759 | 760 | game.background.width = this.width; 761 | game.background.height = this.height; 762 | 763 | game.foreground.width = this.width; 764 | game.foreground.height = this.height; 765 | 766 | // Clear the canvas' ... 767 | game.cgx1.clearRect(0, 0, game.canvas.width, game.canvas.height); 768 | game.cgx2.clearRect(0, 0, game.background.width, game.background.height); 769 | game.cgx3.clearRect(0, 0, game.foreground.width, game.foreground.height); 770 | 771 | // ... and repaint! 772 | if (this.imageID != undefined) game.cgx1.drawImage(images.data[this.imageID], this.x, this.y, game.panorama.width, game.panorama.height); 773 | 774 | for (var i = 0, l = this.mapWidth * this.mapHeight; i < l; i++) this.tiles[i].draw(game.cgx2, game.cgx3); 775 | } 776 | 777 | // #################################### 778 | // Draw functions for the game canvas 779 | // #################################### 780 | 781 | /// TODO: Drawing animated tiles need extra caches one for each animation [IF MANY animated tiles?] 782 | 783 | /** 784 | * Draws the Panorama & the background of the map 785 | */ 786 | this.drawBackground = function () { 787 | ctx = game.context; 788 | ctx.drawImage(game.panorama, 0, 0); 789 | ctx.drawImage(game.background, -game.camera.x, -game.camera.y); 790 | } 791 | 792 | /** 793 | * Draws the foreground of the map 794 | */ 795 | this.drawForeground = function () { 796 | ctx = game.context; 797 | ctx.drawImage(game.foreground, -game.camera.x, -game.camera.y); 798 | } 799 | 800 | // ################ 801 | // ## Map Editor ## 802 | // ################ 803 | 804 | this.initTilesetEditor = function () { 805 | 806 | } 807 | 808 | /** 809 | * Map editing 810 | * Get tile by clicking on the tileset 811 | * Get collision with buttons 812 | * Set tile + collision by clicking on the game 813 | */ 814 | this.clickedTile = function (param_x, param_y) { 815 | // Click on Map 816 | if (game.activeCanvas == 0) { 817 | var x = Math.floor((param_x + game.camera.x) / spritesheets.data[this.spritesheetID].spriteWidth); 818 | var y = Math.floor((param_y + game.camera.y) / spritesheets.data[this.spritesheetID].spriteHeight); 819 | var d = maps.data[game.currentMap].tiles[xy2i(x, y, this.mapWidth)]; 820 | // Clicked Tile console.log(xy2i(x, y, this.mapWidth)); 821 | if (game.drawingOn) { 822 | if (game.currentLayer == 0) d.layer1 = game.tiletype; 823 | if (game.currentLayer == 1) d.layer2 = game.tiletype; 824 | if (game.currentLayer == 2) d.layer3 = game.tiletype; 825 | if (game.currentLayer == 3) d.collision = game.tileCollisionType; 826 | 827 | maps.data[game.currentMap].drawCache(); 828 | } 829 | document.getElementById("clickedXY").innerHTML = "[" + x + " | " + y + "]"; 830 | } 831 | // Click on Tileset 832 | if (game.activeCanvas == 1) { 833 | var x = Math.floor(param_x / spritesheets.data[this.spritesheetID].spriteWidth); 834 | var y = Math.floor(param_y / spritesheets.data[this.spritesheetID].spriteHeight); 835 | game.tiletype = xy2i(x, y, spritesheets.data[this.spritesheetID].spritesX) + 1; 836 | this.drawTileset(); 837 | 838 | document.getElementById("selectedTile").innerHTML = game.tiletype; 839 | document.getElementById("clickedXY").innerHTML = "[" + x + " | " + y + "]"; 840 | } 841 | } 842 | 843 | /** 844 | * Draw the tileset on the tileset canvas 845 | */ 846 | this.drawTileset = function () { 847 | game.tileset.width = spritesheets.data[this.spritesheetID].spriteWidth * spritesheets.data[this.spritesheetID].spritesX; 848 | game.tileset.height = spritesheets.data[this.spritesheetID].spriteHeight * spritesheets.data[this.spritesheetID].spritesY; 849 | game.tilecontext.clearRect(0, 0, spritesheets.data[this.spritesheetID].spriteWidth * spritesheets.data[this.spritesheetID].spritesX, spritesheets.data[this.spritesheetID].spriteHeight * spritesheets.data[this.spritesheetID].spritesY); 850 | 851 | var mapIndex = 0; 852 | var tile_w, tile_h; 853 | for (var h = 0, m = spritesheets.data[this.spritesheetID].spritesY; h < m; h++) { 854 | for (var w = 0, n = spritesheets.data[this.spritesheetID].spritesX; w < n; w++, mapIndex++) { 855 | tile_w = w * spritesheets.data[this.spritesheetID].spriteWidth; 856 | tile_h = h * spritesheets.data[this.spritesheetID].spriteHeight; 857 | //(ctx, spritesheet, number, x, y) 858 | drawSprite(game.tilecontext, spritesheets.data[this.spritesheetID], mapIndex, tile_w, tile_h); 859 | 860 | // Show Tileset Grid 861 | if (false) { 862 | game.tilecontext.strokeStyle = "black"; 863 | game.tilecontext.strokeRect(tile_w, tile_h, 8, 8); 864 | } 865 | 866 | if (game.tiletype - 1 == mapIndex) { 867 | game.tilecontext.strokeStyle = "red"; 868 | game.tilecontext.strokeRect(tile_w, tile_h, 8, 8); 869 | } 870 | } 871 | } 872 | } 873 | 874 | /** 875 | * print layers as string to console 876 | */ 877 | this.printLayers = function () { 878 | var output = ""; 879 | // Layer 1 880 | output += "var " + this.name + "_layer1 = ["; 881 | for (var i = 0, j = this.mapWidth * this.mapHeight; i < j; i++) { 882 | output += this.tiles[i].layer1; 883 | if (i < this.mapWidth * this.mapHeight - 1) 884 | output += ", "; 885 | } 886 | output += "];\n"; 887 | // Layer 2 888 | output += "var " + this.name + "_layer2 = ["; 889 | for (var i = 0, j = this.mapWidth * this.mapHeight; i < j; i++) { 890 | output += this.tiles[i].layer2; 891 | if (i < this.mapWidth * this.mapHeight - 1) 892 | output += ", "; 893 | } 894 | output += "];\n"; 895 | // Layer 3 896 | output += "var " + this.name + "_layer3 = ["; 897 | for (var i = 0, j = this.mapWidth * this.mapHeight; i < j; i++) { 898 | output += this.tiles[i].layer3; 899 | if (i < this.mapWidth * this.mapHeight - 1) 900 | output += ", "; 901 | } 902 | output += "];\n"; 903 | // Collision Layer 904 | output += "var " + this.name + "_layerC = ["; 905 | for (var i = 0, j = this.mapWidth * this.mapHeight; i < j; i++) { 906 | if (this.tiles[i].collision instanceof Array) output += "[" + this.tiles[i].collision + "]"; 907 | else output += this.tiles[i].collision; 908 | if (i < this.mapWidth * this.mapHeight - 1) 909 | output += ", "; 910 | } 911 | output += "];\n"; 912 | //Print 913 | console.log(output); 914 | } 915 | 916 | // After the maps tileset was changed, change the tileset of the tiles 917 | this.switchTileset = function () { 918 | for (var i = 0, j = this.mapWidth * this.mapHeight; i < j; i++) this.tiles[i].spritesheet = this.tileset; 919 | } 920 | } 921 | 922 | // COMPONENTS ---------------------------------------------------------------- 923 | 924 | /** 925 | * Add a component 926 | * @param name 927 | * @param mapID: the original map of creation 928 | * @param spritesheetID 929 | * @param x-position 930 | * @param y-position 931 | * @param {offsetX: , offsetY: , width: , height: , collidable: } 932 | * @param {function} contactEvent 933 | * @param {function} triggerEvent 934 | * @param {function} moveEvent 935 | */ 936 | function addComponent(name, mapID, spritesheetID, x, y, boundingBox, contactEvent, triggerEvent, moveEvent) { 937 | // Check if the mapID is valid 938 | var map = containsComponent(maps.data, mapID); 939 | if (map != undefined) { 940 | // Add 941 | var index = map.components.data.push(new Component(name, mapID, spritesheetID, x, y, boundingBox, contactEvent, triggerEvent, moveEvent)); 942 | // ID management 943 | if (map.components.freeIDs.length > 0) map.components.data[index - 1].id = map.components.freeIDs[0].pop(); 944 | else map.components.data[index - 1].id = index - 1; 945 | } 946 | } 947 | 948 | /** 949 | * Remove a component 950 | */ 951 | function removeComponent(id) { 952 | for (var i = 0, l = components.data.length; i < l; i++) 953 | if (components.data[i].id == id) { 954 | // Remove if ID found 955 | //delete components.data[i]; 956 | components.data.splice(id, 1); 957 | // ID Management 958 | components.freeIDs.push(id); 959 | break; 960 | } 961 | // Remove componentID reference from all maps 962 | for (var i = 0, l = maps.data.length; i < l; i++) 963 | for (var j = 0; j < maps.data[i].components.length; j++) 964 | if (maps.data[i].components[j] == id) 965 | maps.data[i].components.splice(j, 1); 966 | } 967 | 968 | /** 969 | * Generate the data for the components 970 | */ 971 | function generateComponentData() { 972 | // Datastring 973 | for (var i = 0, l = maps.data.length; i < l; i++) 974 | for (var j = 0; j < maps.data[i].components.data.length; j++) { 975 | var c = maps.data[i].components.data; 976 | game.data += "addComponent('" + c[j].name + "', " + c[j].mapID + ", " + c[j].spritesheetID + ", " + c[j].x + ", " + c[j].y + ", {x: " + c[j].boundingBox.x + ", y: " + c[j].boundingBox.y + ", width: " + c[j].boundingBox.width + ", height: " + c[j].boundingBox.height + ", collidable: " + c[j].boundingBox.collidable + "}, " + c[j].contactEvent + ", " + c[j].triggerEvent + ", " + c[j].moveEvent + ");\n"; 977 | } 978 | } 979 | 980 | /** 981 | * Component constructor 982 | * @param name 983 | * @param mapID: the original map of creation 984 | * @param spritesheetID 985 | * @param x-position 986 | * @param y-position 987 | * @param {x: , y: , width: , height: , collidable: } //x, y are offsets to the components x,y which are for the sprite image 988 | * @param {function} contactEvent 989 | * @param {function} triggerEvent 990 | * @param {function} moveEvent 991 | */ 992 | function Component(name, mapID, spritesheetID, x, y, boundingBox, contactEvent, triggerEvent, moveEvent) { 993 | this.name = name; 994 | this.mapID = mapID; 995 | this.spritesheetID = spritesheetID; 996 | 997 | this.x = x; 998 | this.y = y; 999 | 1000 | // Collision Properties 1001 | this.boundingBox = { 1002 | x: 0, 1003 | y: 0, 1004 | width: 0, 1005 | height: 0, 1006 | collidable: false, 1007 | //moveable: false // TODO: Implement 1008 | }; 1009 | this.boundingBox = boundingBox; 1010 | 1011 | // NOOP function if no event 1012 | this.contactEvent = function () {}; 1013 | if (contactEvent instanceof Function) this.contactEvent = contactEvent; 1014 | this.triggerEvent = function () {}; 1015 | if (triggerEvent instanceof Function) this.triggerEvent = triggerEvent; 1016 | this.moveEvent = function () {}; 1017 | if (moveEvent instanceof Function) this.moveEvent = moveEvent; 1018 | 1019 | // Movement Properties 1020 | this.speed = 2; 1021 | this.speedX = 0; 1022 | this.speedY = 0; 1023 | 1024 | // Animation Properties 1025 | this.sequence = 0; 1026 | this.isMoving = false; 1027 | this.faceOnInteraction = true; 1028 | this.animationTime = 0; 1029 | this.animationDelay = 0; 1030 | this.animationIndexCounter = 0; 1031 | this.direction = 64; // Default Direction 1032 | 1033 | // NOOP function if no image 1034 | this.draw = function () {}; 1035 | if (spritesheetID != undefined) { 1036 | this.draw = function (ctx) { 1037 | // Sets animations (based on moving and direction) 1038 | this.updateAnimation(); 1039 | // Animation: Moving / Idle 1040 | if (this.sequence.length != undefined) { 1041 | if (this.animationDelay++ >= this.animationTime) { 1042 | this.animationDelay = 0; 1043 | this.animationIndexCounter++; 1044 | if (this.animationIndexCounter >= this.sequence.length) this.animationIndexCounter = 0; 1045 | } 1046 | drawSprite(ctx, spritesheets.data[this.spritesheetID], this.sequence[this.animationIndexCounter], (this.x - game.camera.x), (this.y - game.camera.y)); 1047 | } 1048 | // No Animation: Just sprite image 1049 | else drawSprite(ctx, spritesheets.data[this.spritesheetID], this.sequence, (this.x - game.camera.x), (this.y - game.camera.y)); 1050 | } 1051 | } 1052 | 1053 | this.idleAnimation = function (idleAnimationTime, idleUp, idleDown, idleLeft, idleRight) { 1054 | this.idleAnimationTime = idleAnimationTime; 1055 | 1056 | this.idleUp = idleUp; 1057 | this.idleDown = idleDown; 1058 | this.idleLeft = idleLeft; 1059 | this.idleRight = idleRight; 1060 | 1061 | return this; 1062 | } 1063 | 1064 | this.moveAnimation = function (moveAnimationTime, moveUp, moveDown, moveLeft, moveRight) { 1065 | this.moveAnimationTime = moveAnimationTime; 1066 | 1067 | this.moveUp = moveUp; 1068 | this.moveDown = moveDown; 1069 | this.moveLeft = moveLeft; 1070 | this.moveRight = moveRight; 1071 | 1072 | return this; 1073 | } 1074 | 1075 | this.specialAnimation = function (specialAnimationTime, special) { 1076 | this.specialAnimationTime = specialAnimationTime; 1077 | this.special = special; 1078 | this.specialOn = false; 1079 | this.specialTimer = new Timer(); 1080 | 1081 | this.startSpecial = function () { 1082 | this.specialOn = true; 1083 | this.animationTime = this.specialAnimationTime; 1084 | this.specialTimer.init(1000); 1085 | } 1086 | 1087 | return this; 1088 | } 1089 | 1090 | /** 1091 | * Update the facing direction based on the speed 1092 | * This is used for drawing the right animation sequence 1093 | * For 4-direction movement (for more than four directions add more cases) 1094 | */ 1095 | this.updateDirection = function () { 1096 | if (this.speedY < 0) this.direction = 4; 1097 | if (this.speedY > 0) this.direction = 64; 1098 | if (this.speedX < 0) this.direction = 16; 1099 | if (this.speedX > 0) this.direction = 1; 1100 | } 1101 | 1102 | /** 1103 | * Updates the shown animation sequence based on the direction te Component is facing 1104 | * For 4-direction movement (for more than four directions add more cases) 1105 | */ 1106 | this.updateAnimation = function () { 1107 | // Special sequence 1108 | if (this.specialOn) { 1109 | this.sequence = this.special; 1110 | if (this.animationIndexCounter == this.special.length - 1) { 1111 | this.specialOn = false; 1112 | this.sequence = this.special[this.special.length - 1]; 1113 | } 1114 | } 1115 | // Moving sequence 1116 | else if (this.isMoving && !game.gameSequence) { 1117 | if (this.moveAnimationTime != undefined) { 1118 | this.animationTime = this.moveAnimationTime; 1119 | if (this.direction == 4) this.sequence = this.moveUp; 1120 | if (this.direction == 64) this.sequence = this.moveDown; 1121 | if (this.direction == 16) this.sequence = this.moveLeft; 1122 | if (this.direction == 1) this.sequence = this.moveRight; 1123 | } 1124 | } 1125 | // Idle sequence 1126 | else { 1127 | if (this.idleAnimationTime != undefined) { 1128 | this.animationTime = this.idleAnimationTime; 1129 | if (this.direction == 4) this.sequence = this.idleUp; 1130 | if (this.direction == 64) this.sequence = this.idleDown; 1131 | if (this.direction == 16) this.sequence = this.idleLeft; 1132 | if (this.direction == 1) this.sequence = this.idleRight; 1133 | } 1134 | } 1135 | 1136 | } 1137 | 1138 | // ######################### 1139 | // ## Collision functions ## 1140 | // ######################### 1141 | 1142 | /** 1143 | * Apply map tile collision to the component 1144 | * First check map borders 1145 | * If component can collide calculates its four corners and applies the tile restrictions of all the tile it is standing on 1146 | * [TODO: Check if there is no collision between the old and the new position 1147 | * (Can happen if either moving really fast or realtime moving )] 1148 | */ 1149 | this.tileCollision = function () { 1150 | 1151 | var d = maps.data[game.currentMap]; 1152 | 1153 | // Check map borders 1154 | if (this.x + this.boundingBox.x + this.speedX < 0) this.speedX = 0; 1155 | else if (this.y + this.boundingBox.y + this.speedY < 0) this.speedY = 0; 1156 | else if (this.x + this.boundingBox.x + this.speedX + this.boundingBox.width > d.width) this.speedX = 0; 1157 | else if (this.y + this.boundingBox.y + this.speedY + this.boundingBox.height > d.height) this.speedY = 0; 1158 | 1159 | if (this.boundingBox.collidable) { 1160 | // Grid coordinates of the components corners 1161 | var x0 = Math.floor((this.x + this.boundingBox.x + this.speedX) / spritesheets.data[d.spritesheetID].spriteWidth); 1162 | var y0 = Math.floor((this.y + this.boundingBox.y + this.speedY) / spritesheets.data[d.spritesheetID].spriteHeight); 1163 | var xn = (this.x + this.boundingBox.x + this.boundingBox.width + this.speedX) / spritesheets.data[d.spritesheetID].spriteWidth; 1164 | var yn = (this.y + this.boundingBox.y + this.boundingBox.height + this.speedY) / spritesheets.data[d.spritesheetID].spriteHeight; 1165 | // Check if only touching right/down tile 1166 | if (Math.floor(xn) - xn == 0) xn = Math.floor(xn) - 1; 1167 | if (Math.floor(yn) - yn == 0) yn = Math.floor(yn) - 1; 1168 | // Check collision for all the tiles the comonent is standing on 1169 | for (var i = x0; i <= xn; i++) 1170 | for (var j = y0; j <= yn; j++) 1171 | this.tileRestriction(d.tiles[xy2i(i, j, d.mapWidth)]); 1172 | } 1173 | } 1174 | 1175 | /** 1176 | * Set movement based on the tiles collision restricting certain directions 1177 | * @param the tile 1178 | */ 1179 | this.tileRestriction = function (tile) { 1180 | // NO RESTRICTION (equal without typecheck: 0 == [0] => true, equal with typecheck:: 0 === [0] => false) 1181 | if (tile.collision === 0) {} 1182 | // FULL RESTRICTION 1183 | else if (tile.collision === 1) { 1184 | this.speedX = 0; 1185 | this.speedY = 0; 1186 | } 1187 | // DIRECTION RESTRICTIONS (assume collision is an array i.e. [0] / [1,2] / [0,2,3] / ... ) 1188 | // 0: UP, 1: DOWN, 2: LEFT, 3: RIGHT 1189 | else { 1190 | for (var i = 0, l = tile.collision.length; i < l; i++) { 1191 | // RESTRICTED: UP 1192 | if (tile.collision[i] == 0) { 1193 | if (this.y + this.boundingBox.y >= tile.y + tile.height) 1194 | if (this.speedY < 0) this.speedY = 0; 1195 | } 1196 | // RESTRICTED: DOWN 1197 | else if (tile.collision[i] == 1) { 1198 | if (this.y + this.boundingBox.y + this.boundingBox.height <= tile.y) 1199 | if (this.speedY > 0) this.speedY = 0; 1200 | } 1201 | // RESTRICTED: LEFT 1202 | else if (tile.collision[i] == 2) { 1203 | if (this.x + this.boundingBox.x >= tile.x + tile.width) 1204 | if (this.speedX < 0) this.speedX = 0; 1205 | } 1206 | // RESTRICTED: RIGHT 1207 | else if (tile.collision[i] == 3) { 1208 | if (this.x + this.boundingBox.x + this.boundingBox.width <= tile.x) 1209 | if (this.speedX > 0) this.speedX = 0; 1210 | } 1211 | } 1212 | } 1213 | } 1214 | 1215 | /** 1216 | * Prevent collision with other Components 1217 | * Has to be called in the main loop for all combinations after the control updates of all Components 1218 | * (TODO: Fix up/down/left/right collision dectection: if up it also detects down ... 1219 | * Pushable Components -> if (pushable && otherobj.noMapCollision) => push) 1220 | */ 1221 | this.componentCollision = function (otherobj) { 1222 | if (!this.boundingBox.collidable || !otherobj.boundingBox.collidable) return false; 1223 | if (this == otherobj) return false; 1224 | 1225 | // Saving y-speed 1226 | var tmp1 = this.speedY; 1227 | var tmp2 = otherobj.speedY; 1228 | 1229 | // Checking X Collision 1230 | this.speedY = 0; 1231 | otherobj.speedY = 0; 1232 | 1233 | if ((this.y + this.boundingBox.y + this.speedY + this.boundingBox.height <= otherobj.y + otherobj.boundingBox.y + otherobj.speedY) || 1234 | (this.y + this.boundingBox.y + this.speedY >= otherobj.y + otherobj.boundingBox.y + otherobj.speedY + otherobj.boundingBox.height) || 1235 | (this.x + this.boundingBox.x + this.speedX + this.boundingBox.width <= otherobj.x + otherobj.boundingBox.x + otherobj.speedX) || 1236 | (this.x + this.boundingBox.x + this.speedX >= otherobj.x + otherobj.boundingBox.x + otherobj.speedX + otherobj.boundingBox.width)) { 1237 | // No X Collision 1238 | this.leftCollision = false; 1239 | this.rightCollision = false; 1240 | } else { 1241 | if ((this.x + this.boundingBox.x + this.speedX + this.boundingBox.width > otherobj.x + otherobj.boundingBox.x + otherobj.speedX)) { 1242 | // X Right Collision 1243 | this.rightCollision = true; 1244 | if (this.speedX <= 0) this.speedX = 0; 1245 | if (otherobj.speedX >= 0) otherobj.speedX = 0; 1246 | } 1247 | if ((this.boundingBox.x + this.speedX < otherobj.boundingBox.x + otherobj.speedX + otherobj.boundingBox.width)) { 1248 | // X Left Collision 1249 | this.leftCollision = true; 1250 | if (this.speedX >= 0) this.speedX = 0; 1251 | if (otherobj.speedX <= 0) otherobj.speedX = 0; 1252 | } 1253 | } 1254 | 1255 | // Checking Y Collision 1256 | this.speedY = tmp1; 1257 | otherobj.speedY = tmp2; 1258 | 1259 | if ((this.y + this.boundingBox.y + this.speedY + this.boundingBox.height <= otherobj.y + otherobj.boundingBox.y + otherobj.speedY) || 1260 | (this.y + this.boundingBox.y + this.speedY >= otherobj.y + otherobj.boundingBox.y + otherobj.speedY + otherobj.boundingBox.height) || 1261 | (this.x + this.boundingBox.x + this.speedX + this.boundingBox.width <= otherobj.x + otherobj.boundingBox.x + otherobj.speedX) || 1262 | (this.x + this.boundingBox.x + this.speedX >= otherobj.x + otherobj.boundingBox.x + otherobj.speedX + otherobj.boundingBox.width)) { 1263 | // NO Y Collision 1264 | this.upCollision = false; 1265 | this.downCollision = false; 1266 | } else { 1267 | if ((this.y + this.boundingBox.y + this.speedY + this.boundingBox.height > otherobj.y + otherobj.boundingBox.y + otherobj.speedY)) { 1268 | // Y Up Collision 1269 | this.upCollision = true; 1270 | if (this.speedY >= 0) this.speedY = 0; 1271 | if (otherobj.speedY <= 0) otherobj.speedY = 0; 1272 | } 1273 | if ((this.y + this.boundingBox.y + this.speedY < otherobj.y + otherobj.boundingBox.y + otherobj.speedY + otherobj.boundingBox.height)) { 1274 | // Y Down Collision 1275 | this.downCollision = true; 1276 | if (this.speedY >= 0) this.speedY = 0; 1277 | if (otherobj.speedY <= 0) otherobj.speedY = 0; 1278 | } 1279 | } 1280 | } 1281 | 1282 | // Get the mid of the 1283 | this.mid = function () { 1284 | return rectangleMid(this.x + this.boundingBox.x, this.y + this.boundingBox.y, this.boundingBox.width, this.boundingBox.height); 1285 | } 1286 | 1287 | // Component interaction 1288 | this.updateInteraction = function (other) { 1289 | // No self interaction 1290 | if (this != other) { 1291 | // In range 1292 | if (this.boundingBoxOverlap(other)) { 1293 | // Fire the contactEvent of the other component with which this component collided 1294 | other.contactEvent(); 1295 | // For interaction the component must look at the other component 1296 | if (this.facing(other)) { 1297 | if (game.enter || other.isClicked()) { 1298 | if (game.eventReady) { 1299 | if (other.faceOnInteraction) this.face(other); 1300 | other.triggerEvent(); 1301 | game.eventReady = false; 1302 | } 1303 | } else game.eventReady = true; 1304 | } 1305 | } 1306 | } 1307 | } 1308 | 1309 | /** 1310 | * Check if the component is clicked / touched 1311 | * If it's not clicked a clickEvent can be fired again 1312 | */ 1313 | this.isClicked = function () { 1314 | // Mouse / Touchdown 1315 | if (game.mousedown || game.touchdown) { 1316 | if (game.mousedown && game.clickdownX != undefined && game.clickdownY != undefined) { 1317 | if ((this.x + this.boundingBox.x > game.clickdownX + game.camera.x) || 1318 | (this.x + this.boundingBox.x + this.boundingBox.width < game.clickdownX + game.camera.x) || 1319 | (this.y + this.boundingBox.y > game.clickdownY + game.camera.y) || 1320 | (this.y + this.boundingBox.y + this.boundingBox.height < game.clickdownY + game.camera.y)) return false; 1321 | // Clicked 1322 | else return true; 1323 | } 1324 | if (game.touchdown && game.touchstartX != undefined && game.touchstartY != undefined) { 1325 | if ((this.x + this.boundingBox.x > game.touchstartX + game.camera.x) || 1326 | (this.x + this.boundingBox.x + this.boundingBox.width < game.touchstartX + game.camera.x) || 1327 | (this.y + this.boundingBox.y > game.touchstartY + game.camera.y) || 1328 | (this.y + this.boundingBox.y + this.boundingBox.height < game.touchstartY + game.camera.y)) return false; 1329 | // Touched 1330 | else return true; 1331 | } 1332 | } 1333 | } 1334 | 1335 | /** 1336 | * 1337 | */ 1338 | this.boundingBoxOverlap = function (other) { 1339 | return rectangleOverlap(this.x + this.boundingBox.x, this.y + this.boundingBox.y, this.boundingBox.width, this.boundingBox.height, other.x + other.boundingBox.x, other.y + other.boundingBox.y, other.boundingBox.width, other.boundingBox.height); 1340 | return false; 1341 | } 1342 | 1343 | /** 1344 | * Turn other object if interaction 1345 | * If you talk to a other person you expect them to turn to face you 1346 | */ 1347 | this.face = function (other) { 1348 | // Stop running animation if Enter is pressed while moving 1349 | this.updateAnimation(); 1350 | // Turn otherobj to face this 1351 | if (this.direction == 4) other.direction = 64; 1352 | if (this.direction == 64) other.direction = 4; 1353 | if (this.direction == 1) other.direction = 16; 1354 | if (this.direction == 16) other.direction = 1; 1355 | other.updateAnimation(); 1356 | } 1357 | 1358 | /** 1359 | * Check if the component is looking at the other object 1360 | */ 1361 | this.facing = function (other) { 1362 | // Left or Right 1363 | if (Math.abs(this.mid()[0] - other.mid()[0]) > Math.abs(this.mid()[1] - other.mid()[1])) { 1364 | if (this.x > other.x) return (this.direction == 16); 1365 | else return (this.direction == 1); 1366 | } 1367 | // Up or Below 1368 | else { 1369 | if (this.y > other.y) return (this.direction == 4); 1370 | else return (this.direction == 64); 1371 | } 1372 | return false; 1373 | } 1374 | 1375 | /** 1376 | * Updating: 1377 | * - First step: How will the Component move only based on it's control-input (speedX/Y)? 1378 | * - Second step: Will it collide with other Components? Resolve collision! If all involved Components not moveable: all speedX/Y = 0. 1379 | * - Third step: Is the resolved collision ok with the map collision? If not speedX/Y of all colliding comps = 0. 1380 | */ 1381 | 1382 | /** 1383 | * Update the components movement (based on speedX/Y values) 1384 | * Tile Collision 1385 | */ 1386 | this.updateMovement = function () { 1387 | // Movement Function 1388 | this.moveEvent(); 1389 | 1390 | // The direction the component is facing can be updated after the speedX/Y is set 1391 | this.updateDirection(); 1392 | 1393 | // Checks for map collision and adjust speed to stop at zero distance 1394 | if (this.speedX != 0) { 1395 | var sign = Math.sign(this.speedX); 1396 | for (var spd = Math.abs(this.speedX); 0 < spd; spd--) { 1397 | this.speedX = sign * spd; 1398 | this.tileCollision(); 1399 | if (this.speedX != 0) break; 1400 | } 1401 | } 1402 | if (this.speedY != 0) { 1403 | var sign = Math.sign(this.speedY); 1404 | for (var spd = Math.abs(this.speedY); 0 < spd; spd--) { 1405 | this.speedY = sign * spd; 1406 | this.tileCollision(); 1407 | if (this.speedY != 0) break; 1408 | } 1409 | } 1410 | 1411 | return this; 1412 | } 1413 | 1414 | /** 1415 | * Update the components position after all collision checks are done 1416 | */ 1417 | this.updatePosition = function () { 1418 | // Check moving for animations 1419 | this.isMoving = (this.speedX != 0 || this.speedY != 0); 1420 | 1421 | // Update Position 1422 | this.x += this.speedX; 1423 | this.y += this.speedY; 1424 | 1425 | // Reset Movement 1426 | this.speedX = 0; 1427 | this.speedY = 0; 1428 | 1429 | return this; 1430 | } 1431 | } 1432 | 1433 | // DIALOGS ------------------------------------------------------------------- 1434 | 1435 | /** 1436 | * Contains all the dialogs of the game 1437 | */ 1438 | var dialogs = { 1439 | data: [], // Contains the dialogs 1440 | freeIDs: [] // FreeIDs 1441 | }; 1442 | 1443 | /** 1444 | * For adding a new dialog 1445 | * @param the text 1446 | * @param fnc 1447 | */ 1448 | function addDialog(input, fnc) { 1449 | // Add 1450 | dialogs.data.push(new Dialog(input, fnc)); 1451 | // ID Management 1452 | if (dialogs.freeIDs.length != 0) dialogs.data[dialogs.data.length - 1].id = dialogs.freeIDs.pop(); 1453 | else dialogs.data[dialogs.data.length - 1].id = dialogs.data.length - 1; 1454 | } 1455 | 1456 | /** 1457 | * Remove a dialog 1458 | */ 1459 | function removeDialog(id) { 1460 | for (var i = 0, l = dialogs.data.length; i < l; i++) 1461 | if (dialogs.data[i].id == id) { 1462 | // Remove if ID found 1463 | dialogs.data.splice(id, 1); 1464 | // ID Management 1465 | dialogs.freeIDs.push(id); 1466 | break; 1467 | } 1468 | } 1469 | 1470 | /** 1471 | * Generate the data for the dialogs 1472 | */ 1473 | function generateDialogData() { 1474 | // Datastring 1475 | for (var i = 0, l = dialogs.data.length; i < l; i++) 1476 | game.data += "addDialog(" + JSON.stringify(dialogs.data[i].text) + ", " + dialogs.data[i].event + ");\n"; 1477 | } 1478 | 1479 | /** 1480 | * Dialog 1481 | * Input Examples 1482 | * 1) Normal text: ["text1", "text2"] 1483 | * 2) Text with option: ["question", ["answer1", "answer2"]] 1484 | * The second parameter is for an eventID 1485 | * TODO: Refine dialog styling 1486 | */ 1487 | function Dialog(input, fnc) { 1488 | this.text = input; 1489 | this.event = function () {}; 1490 | if (fnc instanceof Function) this.event = fnc; 1491 | this.chatCounter = 0; 1492 | this.selectedOption = 0; 1493 | 1494 | this.keyPush = false; 1495 | 1496 | this.x; 1497 | this.y; 1498 | this.width; 1499 | this.height; 1500 | 1501 | // Set the dialog text 1502 | this.setDialog = function (input) { 1503 | this.text = input; 1504 | } 1505 | // Set the dialogs position (& TODO: Style) 1506 | this.setPosition = function (x, y, width, height) { 1507 | this.x = x; 1508 | this.y = y; 1509 | this.height = height; 1510 | this.width = width; 1511 | } 1512 | // Set an event to fire when the dialog finished 1513 | this.setEvent = function (fnc) { 1514 | if (fnc instanceof Function) this.event = fnc; 1515 | } 1516 | 1517 | this.update = function () { 1518 | // Dialog finished 1519 | if (this.chatCounter == this.text.length) { 1520 | // Stop dialog updating in main-loop 1521 | game.currentDialog = undefined; 1522 | // Check for event 1523 | this.event(this.selectedOption); 1524 | // Reset dialog 1525 | this.chatCounter = 0; 1526 | this.selectedOption = 0; 1527 | } 1528 | // Draw Text 1529 | else this.draw(); 1530 | // Increase dialog counter 1531 | this.nextText(); 1532 | } 1533 | 1534 | this.draw = function () { 1535 | // Setup dialog design 1536 | ctx = game.context; 1537 | // Black box 1538 | ctx.globalAlpha = 0.8; 1539 | ctx.fillStyle = "black"; 1540 | ctx.strokeStyle = "black"; 1541 | ctx.fillRect(0, this.y, this.width - 1, 50); 1542 | ctx.strokeRect(0, this.y, this.width - 1, 50); 1543 | ctx.globalAlpha = 1.0; 1544 | // Text Style 1545 | ctx.font = '30px serif'; 1546 | ctx.fillStyle = 'white'; 1547 | // Position (for testing) 1548 | this.y = game.canvas.height - 50; 1549 | this.width = game.canvas.width + 1; 1550 | 1551 | // Options 1552 | if (this.text[this.chatCounter] instanceof Array) { 1553 | this.selectOption(); 1554 | // Highlight the currently selected option 1555 | for (var i = 0, l = this.text[this.chatCounter].length; i < l; i++) { 1556 | if (i == this.selectedOption) 1557 | ctx.fillStyle = 'white'; 1558 | else ctx.fillStyle = 'gray'; 1559 | if (i == 0) ctx.fillText(this.text[this.chatCounter][i], 50, this.y + 22); 1560 | if (i == 1) ctx.fillText(this.text[this.chatCounter][i], 50, this.y + 44); 1561 | if (i == 2) ctx.fillText(this.text[this.chatCounter][i], 200, this.y + 22); 1562 | if (i == 3) ctx.fillText(this.text[this.chatCounter][i], 200, this.y + 44); 1563 | } 1564 | } 1565 | // Text 1566 | else ctx.fillText(this.text[this.chatCounter], 50, this.y + 22); 1567 | } 1568 | 1569 | this.selectOption = function () { 1570 | if (game.keys[87] || game.keys[83] || game.keys[38] || game.keys[40]) { 1571 | if (this.keyPush) { 1572 | if (game.keys[87] || game.keys[38]) this.selectedOption--; 1573 | else if (game.keys[83] || game.keys[40]) this.selectedOption++; 1574 | this.keyPush = false; 1575 | } 1576 | } else this.keyPush = true; 1577 | if (game.activeCanvas == 0) { 1578 | // TODO: Refine select with Mouseover / Touching 1579 | if (game.x > 30 && game.x < 160 && game.y > 405 && game.y < 425) this.selectedOption = 0; 1580 | else if (game.x > 30 && game.x < 160 && game.y > 425 && game.y < 445) this.selectedOption = 1; 1581 | else if (game.x > 190 && game.x < 350 && game.y > 405 && game.y < 425) this.selectedOption = 2; 1582 | else if (game.x > 190 && game.x < 350 && game.y > 425 && game.y < 445) this.selectedOption = 3; 1583 | } 1584 | // Stay in range 1585 | if (this.selectedOption < 0) this.selectedOption = this.selectedOption + this.text[this.chatCounter].length; 1586 | else this.selectedOption = this.selectedOption % this.text[this.chatCounter].length; 1587 | } 1588 | 1589 | this.nextText = function () { 1590 | if (game.enter || game.mousedown || game.touchdown) { 1591 | if (game.eventReady) { 1592 | this.chatCounter++; 1593 | game.eventReady = false; 1594 | } 1595 | } else game.eventReady = true; 1596 | } 1597 | } 1598 | 1599 | // GENERATE GAME DATA -------------------------------------------------------- 1600 | 1601 | /** 1602 | * Generate the all the data of the game 1603 | */ 1604 | function generateGameData() { 1605 | generateImageData(); 1606 | generateAudioData(); 1607 | generateSpriteData(); 1608 | generateMapData(); 1609 | generateComponentData(); 1610 | generateDialogData(); 1611 | } 1612 | 1613 | // GAME ---------------------------------------------------------------------- 1614 | var game = { 1615 | canvas: document.getElementById("game"), 1616 | tileset: document.getElementById("tileset"), 1617 | debug: false, 1618 | info: false, 1619 | // Map Editor Variables 1620 | activeCanvas: undefined, 1621 | tiletype: 1, 1622 | tileCollisionType: 0, 1623 | currentLayer: 0, 1624 | drawingOn: false, 1625 | // Game 1626 | currentMap: 0, // The map which is currently used 1627 | nextMap: 0, // The map that will be switched to in the next iteration of the game loop 1628 | currentDialog: null, // The current dialog 1629 | // The data of the game: When creating new spritesheets/maps/components it will be saved as a string 1630 | // If you start the engine again and feed it this data it will be the same game data as before 1631 | data: "", 1632 | // Init control, maps, .. 1633 | init: function () { 1634 | // Game canvas 1635 | this.context = this.canvas.getContext("2d"); 1636 | 1637 | // Tileset canvas 1638 | this.tilecontext = this.tileset.getContext("2d"); 1639 | 1640 | // Pause game if not selected 1641 | document.active = true; 1642 | window.addEventListener('focus', function (e) { 1643 | document.active = true; 1644 | }); 1645 | window.addEventListener('blur', function (e) { 1646 | document.active = false; 1647 | }); 1648 | 1649 | this.frameNo = 0; 1650 | this.gameSequence = false; 1651 | 1652 | this.keys = []; 1653 | 1654 | // To only fire a single event on enter / mousedown / touchdown 1655 | this.eventReady = false; 1656 | this.enter = false; 1657 | this.mousedown = false; 1658 | this.touchdown = false; 1659 | 1660 | // Draw Tileset 1661 | maps.data[game.currentMap].drawTileset(); 1662 | 1663 | //this.canvas.style.cursor = "none"; //hide the original cursor 1664 | 1665 | // "Cache" Map on an hidden canvas 1666 | this.panorama = document.createElement('canvas'); 1667 | this.cgx1 = this.panorama.getContext("2d"); 1668 | 1669 | this.background = document.createElement('canvas'); 1670 | this.cgx2 = this.background.getContext("2d"); 1671 | 1672 | this.foreground = document.createElement('canvas'); 1673 | this.cgx3 = this.foreground.getContext("2d"); 1674 | 1675 | // For quick accessing of doms objects 1676 | this.dom = { 1677 | // Layer Selection 1678 | layerSelection: document.getElementById("layerSelection"), 1679 | currentLayer: document.getElementById("currentLayer"), 1680 | layer1: document.getElementById("layer1Button"), 1681 | layer2: document.getElementById("layer2Button"), 1682 | layer3: document.getElementById("layer3Button"), 1683 | collisionLayer: document.getElementById("collisionLayerButton"), 1684 | // Collision Restriction Selection 1685 | currentRestriction: document.getElementById("currentRestriction"), 1686 | restrictionSelection: document.getElementById("restrictionSelection"), 1687 | restriction1: document.getElementById("restriction1"), 1688 | restriction2: document.getElementById("restriction2"), 1689 | restriction3: document.getElementById("restriction3"), 1690 | restriction4: document.getElementById("restriction4"), 1691 | restriction5: document.getElementById("restriction5"), 1692 | restriction6: document.getElementById("restriction6"), 1693 | restriction7: document.getElementById("restriction7"), 1694 | restriction8: document.getElementById("restriction8"), 1695 | restriction9: document.getElementById("restriction9"), 1696 | restriction10: document.getElementById("restriction10"), 1697 | restriction11: document.getElementById("restriction11"), 1698 | restriction12: document.getElementById("restriction12"), 1699 | restriction13: document.getElementById("restriction13"), 1700 | restriction14: document.getElementById("restriction14"), 1701 | } 1702 | // Editor images 1703 | this.arrows = { 1704 | no: new Image(), 1705 | up: new Image(), 1706 | down: new Image(), 1707 | left: new Image(), 1708 | right: new Image(), 1709 | up_down: new Image(), 1710 | up_left: new Image(), 1711 | up_right: new Image(), 1712 | down_left: new Image(), 1713 | down_right: new Image(), 1714 | left_right: new Image(), 1715 | up_down_left: new Image(), 1716 | up_down_right: new Image(), 1717 | up_left_right: new Image(), 1718 | down_left_right: new Image(), 1719 | up_down_left_right: new Image() 1720 | } 1721 | 1722 | game.arrows.no.src = "Assets/Image/Restriction Arrows/no.png"; 1723 | game.arrows.up.src = "Assets/Image/Restriction Arrows/up.png"; 1724 | game.arrows.down.src = "Assets/Image/Restriction Arrows/down.png"; 1725 | game.arrows.left.src = "Assets/Image/Restriction Arrows/left.png"; 1726 | game.arrows.right.src = "Assets/Image/Restriction Arrows/right.png"; 1727 | game.arrows.up_down.src = "Assets/Image/Restriction Arrows/up_down.png"; 1728 | game.arrows.up_left.src = "Assets/Image/Restriction Arrows/up_left.png"; 1729 | game.arrows.up_right.src = "Assets/Image/Restriction Arrows/up_right.png"; 1730 | game.arrows.down_left.src = "Assets/Image/Restriction Arrows/down_left.png"; 1731 | game.arrows.down_right.src = "Assets/Image/Restriction Arrows/down_right.png"; 1732 | game.arrows.left_right.src = "Assets/Image/Restriction Arrows/left_right.png"; 1733 | game.arrows.up_down_left.src = "Assets/Image/Restriction Arrows/up_down_left.png"; 1734 | game.arrows.up_down_right.src = "Assets/Image/Restriction Arrows/up_down_right.png"; 1735 | game.arrows.up_left_right.src = "Assets/Image/Restriction Arrows/up_left_right.png"; 1736 | game.arrows.down_left_right.src = "Assets/Image/Restriction Arrows/down_left_right.png"; 1737 | game.arrows.up_down_left_right.src = "Assets/Image/Restriction Arrows/up_down_left_right.png"; 1738 | 1739 | // Camera 1740 | this.camera = new function () { 1741 | this.x = 0; 1742 | this.y = 0; 1743 | 1744 | this.setTarget = function (target) { 1745 | this.target = target; 1746 | } 1747 | 1748 | this.update = function () { 1749 | // Move camera with WASD or Arrow Keys if no target is defined 1750 | if (this.target == undefined) { 1751 | if (game.keys[38] || game.keys[87]) 1752 | this.y--; 1753 | else if (game.keys[40] || game.keys[83]) 1754 | this.y++; 1755 | else if (game.keys[37] || game.keys[65]) 1756 | this.x--; 1757 | else if (game.keys[39] || game.keys[68]) 1758 | this.x++; 1759 | } 1760 | // Camera will follow target 1761 | else { 1762 | this.x = this.target.x - game.canvas.width / 2; 1763 | this.y = this.target.y - game.canvas.height / 2; 1764 | } 1765 | // Keep camera view inside the map 1766 | if (this.x < 0) this.x = 0; 1767 | if (this.y < 0) this.y = 0; 1768 | if (this.x > maps.data[game.currentMap].width - game.canvas.width) this.x = maps.data[game.currentMap].width - game.canvas.width; 1769 | if (this.y > maps.data[game.currentMap].height - game.canvas.height) this.y = maps.data[game.currentMap].height - game.canvas.height; 1770 | } 1771 | } 1772 | 1773 | // Control 1774 | this.control = new function () { 1775 | this.setTarget = function (target) { 1776 | this.target = target; 1777 | } 1778 | 1779 | this.disableControls = false; 1780 | this.disableMouse = false; 1781 | 1782 | this.update = function () { 1783 | if (this.target != undefined) { 1784 | // Check if it key control is allowed 1785 | if (!this.disableControls && !game.gameSequence) { 1786 | // Listen to keys: "Else if" to limit movement in only one direction at the same time (no diagonal moving) 1787 | if (game.keys[38] || game.keys[87]) 1788 | this.target.speedY = -this.target.speed; 1789 | else if (game.keys[40] || game.keys[83]) 1790 | this.target.speedY = this.target.speed; 1791 | else if (game.keys[37] || game.keys[65]) 1792 | this.target.speedX = -this.target.speed; 1793 | else if (game.keys[39] || game.keys[68]) 1794 | this.target.speedX = this.target.speed; 1795 | else if (!this.disableMouse) { 1796 | if (game.mousedown || game.touchdown) { 1797 | if (game.activeCanvas == 0) { 1798 | /* 1799 | // Move direction = Difference between clicked and current mousemove/touch position 1800 | if (game.mousedown) { 1801 | if (Math.abs(game.x - game.clickdownX) > Math.abs(game.y - game.clickdownY)) { 1802 | if (game.x < game.clickdownX - 4) 1803 | this.target.speedX -= this.target.speed; 1804 | else if (game.x > game.clickdownX + 4) 1805 | this.target.speedX += this.target.speed; 1806 | } else { 1807 | if (game.y < game.clickdownY - 4) 1808 | this.target.speedY -= this.target.speed; 1809 | else if (game.y > game.clickdownY + 4) 1810 | this.target.speedY += this.target.speed; 1811 | } 1812 | } 1813 | if (game.touchdown) { 1814 | if (Math.abs(game.x - game.touchstartX) > Math.abs(game.y - game.touchstartY)) { 1815 | if (game.x < game.touchstartX - 4) 1816 | this.target.speedX -= this.target.speed; 1817 | else if (game.x > game.touchstartX + 4) 1818 | this.target.speedX += this.target.speed; 1819 | } else { 1820 | if (game.y < game.touchstartY - 4) 1821 | this.target.speedY -= this.target.speed; 1822 | else if (game.y > game.touchstartY + 4) 1823 | this.target.speedY += this.target.speed; 1824 | } 1825 | }*/ 1826 | } 1827 | } 1828 | } 1829 | } 1830 | } 1831 | } 1832 | } 1833 | 1834 | // Set first component of the current map (start map) as the target of the camera and control 1835 | this.hero = maps.data[0].components.data[0]; 1836 | this.camera.setTarget(game.hero); 1837 | this.control.setTarget(game.hero); 1838 | 1839 | // Disable Mouse Control in Editor Mode 1840 | if (game.editor) game.camera.disableMouse = true; 1841 | 1842 | // Initalize Maps 1843 | for (i = 0, l = maps.data.length; i < l; i++) maps.data[i].loadLayers(layers1[i], layers2[i], layers3[i], layersC[i]); 1844 | 1845 | // Draw the first or current map onto the cached canvas' 1846 | maps.data[game.currentMap].drawCache(); 1847 | 1848 | // Insert tileset canvas after game canvas if editor mode 1849 | if (game.editor) this.canvas.after(this.tileset); 1850 | 1851 | window.requestAnimationFrame = window.requestAnimationFrame || 1852 | window.mozRequestAnimationFrame || 1853 | window.webkitRequestAnimationFrame || 1854 | window.msRequestAnimationFrame || 1855 | function (f) { 1856 | return setTimeout(f, 1000 / 60) 1857 | }; // simulate calling code 60 1858 | 1859 | window.cancelAnimationFrame = window.cancelAnimationFrame || 1860 | window.mozCancelAnimationFrame || 1861 | function (requestID) { 1862 | clearTimeout(requestID) 1863 | }; //fall back 1864 | 1865 | // OnCanvas 1866 | this.onCanvas = function (x, y, canvas) { 1867 | if (x > canvas.getBoundingClientRect().x && 1868 | x < canvas.getBoundingClientRect().x + canvas.width && 1869 | y > canvas.getBoundingClientRect().y && 1870 | y < canvas.getBoundingClientRect().y + canvas.height) return true; 1871 | return false; 1872 | } 1873 | 1874 | // INITIALIZE USER INPUT 1875 | // Customize context menu on right click if canvas 1876 | window.addEventListener('contextmenu', function (e) { 1877 | if (game.onCanvas(e.clientX, e.clientY, game.canvas)) { 1878 | //console.log("Default context menu prevent"); 1879 | e.preventDefault(); 1880 | //toggleMenuOn(); 1881 | //positionMenu(e); 1882 | } else { 1883 | //console.log("Default context menu"); 1884 | //taskItemInContext = null; 1885 | //toggleMenuOff(); 1886 | } 1887 | }) 1888 | // Keydown 1889 | window.addEventListener('keydown', function (e) { 1890 | game.keys = (game.keys || []); 1891 | game.keys[e.keyCode] = (e.type == "keydown"); 1892 | if (game.printkeyCode) console.log(e.keyCode); 1893 | // Enter key 1894 | if (e.keyCode == 13) game.enter = true; 1895 | // no scrolling on arrow keys 1896 | if ([37, 38, 39, 40].indexOf(e.keyCode) > -1) e.preventDefault(); 1897 | }) 1898 | // Keyup 1899 | window.addEventListener('keyup', function (e) { 1900 | game.keys[e.keyCode] = (e.type == "keydown"); 1901 | if (e.keyCode == 13) game.enter = false; 1902 | }) 1903 | // MOUSE 1904 | // Mouse down 1905 | window.addEventListener('mousedown', function (e) { 1906 | game.mousedown = true; 1907 | game.clickdownX = e.clientX; 1908 | game.clickdownY = e.clientY; 1909 | 1910 | if (game.onCanvas(e.clientX, e.clientY, game.canvas)) { 1911 | e.preventDefault(); 1912 | game.clickdownX -= game.canvas.getBoundingClientRect().x; 1913 | game.clickdownY -= game.canvas.getBoundingClientRect().y; 1914 | maps.data[game.currentMap].clickedTile(game.clickdownX, game.clickdownY); 1915 | } else if (game.onCanvas(e.clientX, e.clientY, game.tileset)) { 1916 | e.preventDefault(); 1917 | game.clickdownX -= game.tileset.getBoundingClientRect().x; 1918 | game.clickdownY -= game.tileset.getBoundingClientRect().y; 1919 | maps.data[game.currentMap].clickedTile(game.clickdownX, game.clickdownY); 1920 | } 1921 | 1922 | document.getElementById("clicked/touched").innerHTML = "Mousedown" + "[" + game.clickdownX + "|" + game.clickdownY + "]"; 1923 | }) 1924 | // Mouse up 1925 | window.addEventListener('mouseup', function (e) { 1926 | game.mousedown = false; 1927 | game.clickupX = e.clientX; 1928 | game.clickupY = e.clientY; 1929 | 1930 | if (game.onCanvas(e.clientX, e.clientY, game.canvas)) { 1931 | game.clickupX -= game.canvas.getBoundingClientRect().x; 1932 | game.clickupY -= game.canvas.getBoundingClientRect().y; 1933 | } else if (game.onCanvas(e.clientX, e.clientY, game.tileset)) { 1934 | game.clickupX -= game.tileset.getBoundingClientRect().x; 1935 | game.clickupY -= game.tileset.getBoundingClientRect().y; 1936 | } 1937 | 1938 | document.getElementById("clicked/touched").innerHTML = "-"; 1939 | }) 1940 | // Mouse move 1941 | window.addEventListener('mousemove', function (e) { 1942 | game.x = e.clientX; 1943 | game.y = e.clientY; 1944 | 1945 | if (game.onCanvas(e.clientX, e.clientY, game.canvas)) { 1946 | game.activeCanvas = 0; 1947 | game.x = Math.floor(e.clientX - game.canvas.getBoundingClientRect().x); 1948 | game.y = Math.floor(e.clientY - game.canvas.getBoundingClientRect().y); 1949 | 1950 | document.getElementById("activeCanvas").innerHTML = "Game"; 1951 | document.getElementById("canvasXY").innerHTML = "[" + game.x + " | " + game.y + "]"; 1952 | } else if (game.onCanvas(e.clientX, e.clientY, game.tileset)) { 1953 | game.activeCanvas = 1; 1954 | game.x = Math.floor(e.clientX - game.tileset.getBoundingClientRect().x); 1955 | game.y = Math.floor(e.clientY - game.tileset.getBoundingClientRect().y); 1956 | 1957 | document.getElementById("activeCanvas").innerHTML = "Tileset"; 1958 | document.getElementById("canvasXY").innerHTML = "[" + game.x + " | " + game.y + "]"; 1959 | } else { 1960 | game.activeCanvas = undefined; 1961 | 1962 | document.getElementById("activeCanvas").innerHTML = "Off Canvas"; 1963 | document.getElementById("canvasXY").innerHTML = "[" + e.clientX + " | " + e.clientY + "]"; 1964 | } 1965 | document.getElementById("mtp").innerHTML = "-> [" + game.x + "|" + game.y + "]"; 1966 | }) 1967 | // TOUCH 1968 | // Touch start 1969 | window.addEventListener('touchstart', function (e) { 1970 | game.touchdown = true; 1971 | game.touchstartX = e.touches[0].clientX; 1972 | game.touchstartY = e.touches[0].clientY; 1973 | 1974 | if (game.onCanvas(e.clientX, e.clientY, game.canvas)) { 1975 | game.touchstartX -= game.canvas.getBoundingClientRect().x; 1976 | game.touchstartY -= game.canvas.getBoundingClientRect().y; 1977 | } else if (game.onCanvas(e.clientX, e.clientY, game.tileset)) { 1978 | game.touchstartX -= game.tileset.getBoundingClientRect().x; 1979 | game.touchstartY -= game.tileset.getBoundingClientRect().y; 1980 | } 1981 | 1982 | document.getElementById("clicked/touched").innerHTML = "Touchstart" + "[" + game.touchstartX + "|" + game.touchstartY + "]"; 1983 | }) 1984 | // Touch end 1985 | window.addEventListener('touchend', function (e) { 1986 | game.touchdown = false; 1987 | game.touchendX = e.touches[0].clientX; 1988 | game.touchendY = e.touches[0].clientY; 1989 | 1990 | if (game.onCanvas(e.clientX, e.clientY, game.canvas)) { 1991 | game.touchendX -= game.canvas.getBoundingClientRect().x; 1992 | game.touchendY -= game.canvas.getBoundingClientRect().y; 1993 | } else if (game.onCanvas(e.clientX, e.clientY, game.tileset)) { 1994 | game.touchendX -= game.tileset.getBoundingClientRect().x; 1995 | game.touchendY -= game.tileset.getBoundingClientRect().y; 1996 | } 1997 | 1998 | document.getElementById("clicked/touched").innerHTML = "-"; 1999 | }) 2000 | // Touch move 2001 | window.addEventListener('touchmove', function (e) { 2002 | game.x = e.touches[0].clientX; 2003 | game.y = e.touches[0].clientY; 2004 | 2005 | if (game.onCanvas(e.touches[0].clientX, e.touches[0].clientY, game.canvas)) { 2006 | activeCanvas = 0; 2007 | game.x = Math.floor(e.touches[0].clientX - game.canvas.getBoundingClientRect().x); 2008 | game.y = Math.floor(e.touches[0].clientY - game.canvas.getBoundingClientRect().y); 2009 | 2010 | document.getElementById("activeCanvas").innerHTML = "Game"; 2011 | document.getElementById("canvasXY").innerHTML = "[" + game.x + " | " + game.y + "]"; 2012 | } else if (game.onCanvas(e.touches[0].clientX, e.touches[0].clientY, game.tileset)) { 2013 | activeCanvas = 1; 2014 | game.x = Math.floor(e.touches[0].clientX - game.tileset.getBoundingClientRect().x); 2015 | game.y = Math.floor(e.touches[0].clientY - game.tileset.getBoundingClientRect().y); 2016 | 2017 | document.getElementById("activeCanvas").innerHTML = "Tileset"; 2018 | document.getElementById("canvasXY").innerHTML = "[" + game.x + " | " + game.y + "]"; 2019 | } else { 2020 | game.activeCanvas = undefined; 2021 | 2022 | document.getElementById("activeCanvas").innerHTML = "Off Canvas"; 2023 | document.getElementById("canvasXY").innerHTML = "[" + e.clientX + " | " + e.clientY + "]"; 2024 | } 2025 | document.getElementById("mtp").innerHTML = "-> [" + game.x + "|" + game.y + "]"; 2026 | }) 2027 | }, 2028 | 2029 | start: function () { 2030 | function gameLoop() { 2031 | requestAnimationFrame(gameLoop); 2032 | if (document.active) 2033 | updateGame(); 2034 | } 2035 | gameLoop(); 2036 | }, 2037 | 2038 | stop: function () { 2039 | 2040 | }, 2041 | 2042 | clear: function () { 2043 | this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); 2044 | } 2045 | }; 2046 | 2047 | /** 2048 | * Updates the current map with all it's components 2049 | * TODO: Clean up - structure; for-loop length as var in the loop BUT care it must not change will the loop runs EVENTS that change map / delete objects 2050 | */ 2051 | function update() { 2052 | // While game.gameSequence == true all components will stop moving (i.e. used for menus, dialogs,...) 2053 | if (!game.gameSequence) { 2054 | // Update the movement of all components on the current map (this also resolves tileCollision) 2055 | if (!game.transition) 2056 | for (var i = 0; i < maps.data[game.currentMap].components.data.length; i++) 2057 | maps.data[game.currentMap].components.data[i].updateMovement(); 2058 | // For components that can start an interacted 2059 | for (var i = 0; i < maps.data[game.currentMap].components.data.length; i++) 2060 | // The controlled component can acted with other component 2061 | if (maps.data[game.currentMap].components.data[i] != game.control.target) 2062 | game.control.target.updateInteraction(maps.data[game.currentMap].components.data[i]); 2063 | // Check each combination pair of components on the current map for component-component-collision 2064 | for (var i = 0; i < maps.data[game.currentMap].components.data.length; i++) 2065 | for (var j = 0; j < maps.data[game.currentMap].components.data.length; j++) { 2066 | var c1 = maps.data[game.currentMap].components.data[i]; 2067 | var c2 = maps.data[game.currentMap].components.data[j]; 2068 | c1.componentCollision(c2); 2069 | } 2070 | // Update the position of all components on the current map 2071 | for (var i = 0, l = maps.data[game.currentMap].components.data.length; i < l; i++) maps.data[game.currentMap].components.data[i].updatePosition(); 2072 | } 2073 | } 2074 | 2075 | /** 2076 | * Draws the canvas 2077 | * 1) Clear the canvas 2078 | * 2) Draw the background 2079 | * 3) Draw the objects 2080 | * 4) Draw the foreground 2081 | * 4) Draw the gui 2082 | */ 2083 | function draw() { 2084 | // Draw map 2085 | // Clear the canvas 2086 | game.context.clearRect(0, 0, game.canvas.width, game.canvas.height); 2087 | // Draw Background 2088 | maps.data[game.currentMap].drawBackground(); 2089 | // Sorts the array after it's y value so that components with bigger y are drawn later 2090 | maps.data[game.currentMap].components.data.sort(function (a, b) { 2091 | return (a.y > b.y) ? 1 : ((b.y > a.y) ? -1 : 0); 2092 | }); 2093 | for (var i = 0, l = maps.data[game.currentMap].components.data.length; i < l; i++) maps.data[game.currentMap].components.data[i].draw(game.context); 2094 | // Draw Foreground 2095 | maps.data[game.currentMap].drawForeground(); 2096 | 2097 | // Information for debugging and editing 2098 | if (game.debug) { 2099 | for (var i = 0, l = maps.data[game.currentMap].components.data.length; i < l; i++) { 2100 | var comp = maps.data[game.currentMap].components.data[i]; 2101 | // Draw a rectangle for invisiable components 2102 | if (comp.spritesheetID == undefined) { 2103 | game.context.fillStyle = "cyan"; 2104 | game.context.globalAlpha = 0.8; 2105 | game.context.fillRect(comp.x + comp.boundingBox.x - game.camera.x, comp.y + comp.boundingBox.y - game.camera.y, comp.boundingBox.width, comp.boundingBox.height); 2106 | game.context.globalAlpha = 1.0; 2107 | } 2108 | // Draw Collision Box 2109 | game.context.strokeStyle = "red"; 2110 | game.context.strokeRect(comp.x + comp.boundingBox.x - game.camera.x, comp.y + comp.boundingBox.y - game.camera.y, comp.boundingBox.width, comp.boundingBox.height); 2111 | } 2112 | } 2113 | // Useful information 2114 | if (game.info) { 2115 | game.context.globalAlpha = 0.5; 2116 | game.context.fillStyle = "cyan"; 2117 | game.context.fillRect(0, 0, 120, 90); 2118 | game.context.globalAlpha = 1.0; 2119 | showTime(); 2120 | updateFPS(); 2121 | showFPS(); 2122 | if (game.camera.target != undefined) showPosition(game.camera.target); 2123 | } 2124 | // Mouse / Touch 2125 | /* 2126 | if (game.mousedown || game.touchdown) { 2127 | if (game.activeCanvas == 0) { 2128 | ctx = game.context; 2129 | ctx.beginPath(); 2130 | if (game.mousedown) ctx.arc(game.clickdownX, game.clickdownY, 5, 0, 2 * Math.PI, true); 2131 | if (game.touchdown) ctx.arc(game.touchstartX, game.touchstartY, 5, 0, 2 * Math.PI, true); 2132 | ctx.arc(game.x, game.y, 5, 0, 2 * Math.PI, true); 2133 | // Fill 2134 | ctx.fillStyle = "black"; 2135 | ctx.fill(); 2136 | // Outline 2137 | ctx.strokeStyle = "black"; 2138 | ctx.stroke(); 2139 | } 2140 | } 2141 | */ 2142 | } 2143 | 2144 | /** 2145 | * Updates the canvas 2146 | * This is the core function of the game 2147 | */ 2148 | function updateGame() { 2149 | game.frameNo += 1; 2150 | 2151 | // Redraw caches' on map change + map switch transition 2152 | if (game.currentMap != game.nextMap) { 2153 | game.currentMap = game.nextMap; 2154 | maps.data[game.currentMap].drawCache(); 2155 | if (game.editor != undefined) maps.data[game.currentMap].drawTileset(); 2156 | setTimeout(function () { 2157 | game.transition = false; 2158 | }, 400); 2159 | } 2160 | 2161 | // Draw map transition 2162 | if (game.transition) blackTransition(); 2163 | else { 2164 | // Update camera 2165 | game.camera.update(); 2166 | // Update control 2167 | game.control.update(); 2168 | // Update game 2169 | update(); 2170 | // Draw game 2171 | draw(); 2172 | } 2173 | 2174 | // Draw dialog 2175 | if (game.currentDialog != undefined) { 2176 | game.gameSequence = true; 2177 | game.currentDialog.update(); 2178 | } else game.gameSequence = false; 2179 | } 2180 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | RPG Engine 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 |
Start
22 | 32 |
33 | 34 |
35 |
36 | 37 | 38 | 39 | 40 | 41 |
42 | 43 |
44 | Layer 1 45 | Layer 2 46 | Layer 3 47 | Collision 48 |
49 |
50 | 51 |
52 | 53 |
54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 |
71 |
72 |
73 | Off Canvas 74 | 75 |
76 | Selected Tile 77 | 78 | Clicked Tile 79 | 80 |
81 | False 82 | False 83 |
84 | 88 |
89 |
90 | 91 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /scriptLoader.js: -------------------------------------------------------------------------------- 1 | // All script that need to be loaded 2 | var scriptsToLoad = [ 3 | 'astar.js', 4 | 'core.js', 5 | 'data.js']; 6 | scriptsToLoad.forEach(function (src) { 7 | var script = document.createElement('script'); 8 | script.src = src; 9 | script.async = false; 10 | document.getElementById("gameScripts").appendChild(script); 11 | }); 12 | -------------------------------------------------------------------------------- /w3.css: -------------------------------------------------------------------------------- 1 | /* W3.CSS 4.10 February 2018 by Jan Egil and Borge Refsnes */ 2 | html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit} 3 | /* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */ 4 | html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0} 5 | article,aside,details,figcaption,figure,footer,header,main,menu,nav,section,summary{display:block} 6 | audio,canvas,progress,video{display:inline-block}progress{vertical-align:baseline} 7 | audio:not([controls]){display:none;height:0}[hidden],template{display:none} 8 | a{background-color:transparent;-webkit-text-decoration-skip:objects} 9 | a:active,a:hover{outline-width:0}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted} 10 | dfn{font-style:italic}mark{background:#ff0;color:#000} 11 | small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline} 12 | sub{bottom:-0.25em}sup{top:-0.5em}figure{margin:1em 40px}img{border-style:none}svg:not(:root){overflow:hidden} 13 | code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}hr{box-sizing:content-box;height:0;overflow:visible} 14 | button,input,select,textarea{font:inherit;margin:0}optgroup{font-weight:bold} 15 | button,input{overflow:visible}button,select{text-transform:none} 16 | button,html [type=button],[type=reset],[type=submit]{-webkit-appearance:button} 17 | button::-moz-focus-inner, [type=button]::-moz-focus-inner, [type=reset]::-moz-focus-inner, [type=submit]::-moz-focus-inner{border-style:none;padding:0} 18 | button:-moz-focusring, [type=button]:-moz-focusring, [type=reset]:-moz-focusring, [type=submit]:-moz-focusring{outline:1px dotted ButtonText} 19 | fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em} 20 | legend{color:inherit;display:table;max-width:100%;padding:0;white-space:normal}textarea{overflow:auto} 21 | [type=checkbox],[type=radio]{padding:0} 22 | [type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto} 23 | [type=search]{-webkit-appearance:textfield;outline-offset:-2px} 24 | [type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none} 25 | ::-webkit-input-placeholder{color:inherit;opacity:0.54} 26 | ::-webkit-file-upload-button{-webkit-appearance:button;font:inherit} 27 | /* End extract */ 28 | html,body{font-family:Verdana,sans-serif;font-size:15px;line-height:1.5}html{overflow-x:hidden} 29 | h1{font-size:36px}h2{font-size:30px}h3{font-size:24px}h4{font-size:20px}h5{font-size:18px}h6{font-size:16px}.w3-serif{font-family:serif} 30 | h1,h2,h3,h4,h5,h6{font-family:"Segoe UI",Arial,sans-serif;font-weight:400;margin:10px 0}.w3-wide{letter-spacing:4px} 31 | hr{border:0;border-top:1px solid #eee;margin:20px 0} 32 | .w3-image{max-width:100%;height:auto}img{vertical-align:middle}a{color:inherit} 33 | .w3-table,.w3-table-all{border-collapse:collapse;border-spacing:0;width:100%;display:table}.w3-table-all{border:1px solid #ccc} 34 | .w3-bordered tr,.w3-table-all tr{border-bottom:1px solid #ddd}.w3-striped tbody tr:nth-child(even){background-color:#f1f1f1} 35 | .w3-table-all tr:nth-child(odd){background-color:#fff}.w3-table-all tr:nth-child(even){background-color:#f1f1f1} 36 | .w3-hoverable tbody tr:hover,.w3-ul.w3-hoverable li:hover{background-color:#ccc}.w3-centered tr th,.w3-centered tr td{text-align:center} 37 | .w3-table td,.w3-table th,.w3-table-all td,.w3-table-all th{padding:8px 8px;display:table-cell;text-align:left;vertical-align:top} 38 | .w3-table th:first-child,.w3-table td:first-child,.w3-table-all th:first-child,.w3-table-all td:first-child{padding-left:16px} 39 | .w3-btn,.w3-button{border:none;display:inline-block;padding:8px 16px;vertical-align:middle;overflow:hidden;text-decoration:none;color:inherit;background-color:inherit;text-align:center;cursor:pointer;white-space:nowrap} 40 | .w3-btn:hover{box-shadow:0 8px 16px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)} 41 | .w3-btn,.w3-button{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none} 42 | .w3-disabled,.w3-btn:disabled,.w3-button:disabled{cursor:not-allowed;opacity:0.3}.w3-disabled *,:disabled *{pointer-events:none} 43 | .w3-btn.w3-disabled:hover,.w3-btn:disabled:hover{box-shadow:none} 44 | .w3-badge,.w3-tag{background-color:#000;color:#fff;display:inline-block;padding-left:8px;padding-right:8px;text-align:center}.w3-badge{border-radius:50%} 45 | .w3-ul{list-style-type:none;padding:0;margin:0}.w3-ul li{padding:8px 16px;border-bottom:1px solid #ddd}.w3-ul li:last-child{border-bottom:none} 46 | .w3-tooltip,.w3-display-container{position:relative}.w3-tooltip .w3-text{display:none}.w3-tooltip:hover .w3-text{display:inline-block} 47 | .w3-ripple:active{opacity:0.5}.w3-ripple{transition:opacity 0s} 48 | .w3-input{padding:8px;display:block;border:none;border-bottom:1px solid #ccc;width:100%} 49 | .w3-select{padding:9px 0;width:100%;border:none;border-bottom:1px solid #ccc} 50 | .w3-dropdown-click,.w3-dropdown-hover{position:relative;display:inline-block;cursor:pointer} 51 | .w3-dropdown-hover:hover .w3-dropdown-content{display:block} 52 | .w3-dropdown-hover:first-child,.w3-dropdown-click:hover{background-color:#ccc;color:#000} 53 | .w3-dropdown-hover:hover > .w3-button:first-child,.w3-dropdown-click:hover > .w3-button:first-child{background-color:#ccc;color:#000} 54 | .w3-dropdown-content{cursor:auto;color:#000;background-color:#fff;display:none;position:absolute;min-width:160px;margin:0;padding:0;z-index:1} 55 | .w3-check,.w3-radio{width:24px;height:24px;position:relative;top:6px} 56 | .w3-sidebar{height:100%;width:200px;background-color:#fff;position:fixed!important;z-index:1;overflow:auto} 57 | .w3-bar-block .w3-dropdown-hover,.w3-bar-block .w3-dropdown-click{width:100%} 58 | .w3-bar-block .w3-dropdown-hover .w3-dropdown-content,.w3-bar-block .w3-dropdown-click .w3-dropdown-content{min-width:100%} 59 | .w3-bar-block .w3-dropdown-hover .w3-button,.w3-bar-block .w3-dropdown-click .w3-button{width:100%;text-align:left;padding:8px 16px} 60 | .w3-main,#main{transition:margin-left .4s} 61 | .w3-modal{z-index:3;display:none;padding-top:100px;position:fixed;left:0;top:0;width:100%;height:100%;overflow:auto;background-color:rgb(0,0,0);background-color:rgba(0,0,0,0.4)} 62 | .w3-modal-content{margin:auto;background-color:#fff;position:relative;padding:0;outline:0;width:600px} 63 | .w3-bar{width:100%;overflow:hidden}.w3-center .w3-bar{display:inline-block;width:auto} 64 | .w3-bar .w3-bar-item{padding:8px 16px;float:left;width:auto;border:none;display:block;outline:0} 65 | .w3-bar .w3-dropdown-hover,.w3-bar .w3-dropdown-click{position:static;float:left} 66 | .w3-bar .w3-button{white-space:normal} 67 | .w3-bar-block .w3-bar-item{width:100%;display:block;padding:8px 16px;text-align:left;border:none;white-space:normal;float:none;outline:0} 68 | .w3-bar-block.w3-center .w3-bar-item{text-align:center}.w3-block{display:block;width:100%} 69 | .w3-responsive{display:block;overflow-x:auto} 70 | .w3-container:after,.w3-container:before,.w3-panel:after,.w3-panel:before,.w3-row:after,.w3-row:before,.w3-row-padding:after,.w3-row-padding:before, 71 | .w3-cell-row:before,.w3-cell-row:after,.w3-clear:after,.w3-clear:before,.w3-bar:before,.w3-bar:after{content:"";display:table;clear:both} 72 | .w3-col,.w3-half,.w3-third,.w3-twothird,.w3-threequarter,.w3-quarter{float:left;width:100%} 73 | .w3-col.s1{width:8.33333%}.w3-col.s2{width:16.66666%}.w3-col.s3{width:24.99999%}.w3-col.s4{width:33.33333%} 74 | .w3-col.s5{width:41.66666%}.w3-col.s6{width:49.99999%}.w3-col.s7{width:58.33333%}.w3-col.s8{width:66.66666%} 75 | .w3-col.s9{width:74.99999%}.w3-col.s10{width:83.33333%}.w3-col.s11{width:91.66666%}.w3-col.s12{width:99.99999%} 76 | @media (min-width:601px){.w3-col.m1{width:8.33333%}.w3-col.m2{width:16.66666%}.w3-col.m3,.w3-quarter{width:24.99999%}.w3-col.m4,.w3-third{width:33.33333%} 77 | .w3-col.m5{width:41.66666%}.w3-col.m6,.w3-half{width:49.99999%}.w3-col.m7{width:58.33333%}.w3-col.m8,.w3-twothird{width:66.66666%} 78 | .w3-col.m9,.w3-threequarter{width:74.99999%}.w3-col.m10{width:83.33333%}.w3-col.m11{width:91.66666%}.w3-col.m12{width:99.99999%}} 79 | @media (min-width:993px){.w3-col.l1{width:8.33333%}.w3-col.l2{width:16.66666%}.w3-col.l3{width:24.99999%}.w3-col.l4{width:33.33333%} 80 | .w3-col.l5{width:41.66666%}.w3-col.l6{width:49.99999%}.w3-col.l7{width:58.33333%}.w3-col.l8{width:66.66666%} 81 | .w3-col.l9{width:74.99999%}.w3-col.l10{width:83.33333%}.w3-col.l11{width:91.66666%}.w3-col.l12{width:99.99999%}} 82 | .w3-content{max-width:980px;margin:auto}.w3-rest{overflow:hidden} 83 | .w3-cell-row{display:table;width:100%}.w3-cell{display:table-cell} 84 | .w3-cell-top{vertical-align:top}.w3-cell-middle{vertical-align:middle}.w3-cell-bottom{vertical-align:bottom} 85 | .w3-hide{display:none!important}.w3-show-block,.w3-show{display:block!important}.w3-show-inline-block{display:inline-block!important} 86 | @media (max-width:600px){.w3-modal-content{margin:0 10px;width:auto!important}.w3-modal{padding-top:30px} 87 | .w3-dropdown-hover.w3-mobile .w3-dropdown-content,.w3-dropdown-click.w3-mobile .w3-dropdown-content{position:relative} 88 | .w3-hide-small{display:none!important}.w3-mobile{display:block;width:100%!important}.w3-bar-item.w3-mobile,.w3-dropdown-hover.w3-mobile,.w3-dropdown-click.w3-mobile{text-align:center} 89 | .w3-dropdown-hover.w3-mobile,.w3-dropdown-hover.w3-mobile .w3-btn,.w3-dropdown-hover.w3-mobile .w3-button,.w3-dropdown-click.w3-mobile,.w3-dropdown-click.w3-mobile .w3-btn,.w3-dropdown-click.w3-mobile .w3-button{width:100%}} 90 | @media (max-width:768px){.w3-modal-content{width:500px}.w3-modal{padding-top:50px}} 91 | @media (min-width:993px){.w3-modal-content{width:900px}.w3-hide-large{display:none!important}.w3-sidebar.w3-collapse{display:block!important}} 92 | @media (max-width:992px) and (min-width:601px){.w3-hide-medium{display:none!important}} 93 | @media (max-width:992px){.w3-sidebar.w3-collapse{display:none}.w3-main{margin-left:0!important;margin-right:0!important}} 94 | .w3-top,.w3-bottom{position:fixed;width:100%;z-index:1}.w3-top{top:0}.w3-bottom{bottom:0} 95 | .w3-overlay{position:fixed;display:none;width:100%;height:100%;top:0;left:0;right:0;bottom:0;background-color:rgba(0,0,0,0.5);z-index:2} 96 | .w3-display-topleft{position:absolute;left:0;top:0}.w3-display-topright{position:absolute;right:0;top:0} 97 | .w3-display-bottomleft{position:absolute;left:0;bottom:0}.w3-display-bottomright{position:absolute;right:0;bottom:0} 98 | .w3-display-middle{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%)} 99 | .w3-display-left{position:absolute;top:50%;left:0%;transform:translate(0%,-50%);-ms-transform:translate(-0%,-50%)} 100 | .w3-display-right{position:absolute;top:50%;right:0%;transform:translate(0%,-50%);-ms-transform:translate(0%,-50%)} 101 | .w3-display-topmiddle{position:absolute;left:50%;top:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)} 102 | .w3-display-bottommiddle{position:absolute;left:50%;bottom:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)} 103 | .w3-display-container:hover .w3-display-hover{display:block}.w3-display-container:hover span.w3-display-hover{display:inline-block}.w3-display-hover{display:none} 104 | .w3-display-position{position:absolute} 105 | .w3-circle{border-radius:50%} 106 | .w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px} 107 | .w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px} 108 | .w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px} 109 | .w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px} 110 | .w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word} 111 | .w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%} 112 | .w3-card,.w3-card-2{box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12)} 113 | .w3-card-4,.w3-hover-shadow:hover{box-shadow:0 4px 10px 0 rgba(0,0,0,0.2),0 4px 20px 0 rgba(0,0,0,0.19)} 114 | .w3-spin{animation:w3-spin 2s infinite linear}@keyframes w3-spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}} 115 | .w3-animate-fading{animation:fading 10s infinite}@keyframes fading{0%{opacity:0}50%{opacity:1}100%{opacity:0}} 116 | .w3-animate-opacity{animation:opac 0.8s}@keyframes opac{from{opacity:0} to{opacity:1}} 117 | .w3-animate-top{position:relative;animation:animatetop 0.4s}@keyframes animatetop{from{top:-300px;opacity:0} to{top:0;opacity:1}} 118 | .w3-animate-left{position:relative;animation:animateleft 0.4s}@keyframes animateleft{from{left:-300px;opacity:0} to{left:0;opacity:1}} 119 | .w3-animate-right{position:relative;animation:animateright 0.4s}@keyframes animateright{from{right:-300px;opacity:0} to{right:0;opacity:1}} 120 | .w3-animate-bottom{position:relative;animation:animatebottom 0.4s}@keyframes animatebottom{from{bottom:-300px;opacity:0} to{bottom:0;opacity:1}} 121 | .w3-animate-zoom {animation:animatezoom 0.6s}@keyframes animatezoom{from{transform:scale(0)} to{transform:scale(1)}} 122 | .w3-animate-input{transition:width 0.4s ease-in-out}.w3-animate-input:focus{width:100%!important} 123 | .w3-opacity,.w3-hover-opacity:hover{opacity:0.60}.w3-opacity-off,.w3-hover-opacity-off:hover{opacity:1} 124 | .w3-opacity-max{opacity:0.25}.w3-opacity-min{opacity:0.75} 125 | .w3-greyscale-max,.w3-grayscale-max,.w3-hover-greyscale:hover,.w3-hover-grayscale:hover{filter:grayscale(100%)} 126 | .w3-greyscale,.w3-grayscale{filter:grayscale(75%)}.w3-greyscale-min,.w3-grayscale-min{filter:grayscale(50%)} 127 | .w3-sepia{filter:sepia(75%)}.w3-sepia-max,.w3-hover-sepia:hover{filter:sepia(100%)}.w3-sepia-min{filter:sepia(50%)} 128 | .w3-tiny{font-size:10px!important}.w3-small{font-size:12px!important}.w3-medium{font-size:15px!important}.w3-large{font-size:18px!important} 129 | .w3-xlarge{font-size:24px!important}.w3-xxlarge{font-size:36px!important}.w3-xxxlarge{font-size:48px!important}.w3-jumbo{font-size:64px!important} 130 | .w3-left-align{text-align:left!important}.w3-right-align{text-align:right!important}.w3-justify{text-align:justify!important}.w3-center{text-align:center!important} 131 | .w3-border-0{border:0!important}.w3-border{border:1px solid #ccc!important} 132 | .w3-border-top{border-top:1px solid #ccc!important}.w3-border-bottom{border-bottom:1px solid #ccc!important} 133 | .w3-border-left{border-left:1px solid #ccc!important}.w3-border-right{border-right:1px solid #ccc!important} 134 | .w3-topbar{border-top:6px solid #ccc!important}.w3-bottombar{border-bottom:6px solid #ccc!important} 135 | .w3-leftbar{border-left:6px solid #ccc!important}.w3-rightbar{border-right:6px solid #ccc!important} 136 | .w3-section,.w3-code{margin-top:16px!important;margin-bottom:16px!important} 137 | .w3-margin{margin:16px!important}.w3-margin-top{margin-top:16px!important}.w3-margin-bottom{margin-bottom:16px!important} 138 | .w3-margin-left{margin-left:16px!important}.w3-margin-right{margin-right:16px!important} 139 | .w3-padding-small{padding:4px 8px!important}.w3-padding{padding:8px 16px!important}.w3-padding-large{padding:12px 24px!important} 140 | .w3-padding-16{padding-top:16px!important;padding-bottom:16px!important}.w3-padding-24{padding-top:24px!important;padding-bottom:24px!important} 141 | .w3-padding-32{padding-top:32px!important;padding-bottom:32px!important}.w3-padding-48{padding-top:48px!important;padding-bottom:48px!important} 142 | .w3-padding-64{padding-top:64px!important;padding-bottom:64px!important} 143 | .w3-left{float:left!important}.w3-right{float:right!important} 144 | .w3-button:hover{color:#000!important;background-color:#ccc!important} 145 | .w3-transparent,.w3-hover-none:hover{background-color:transparent!important} 146 | .w3-hover-none:hover{box-shadow:none!important} 147 | /* Colors */ 148 | .w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important} 149 | .w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important} 150 | .w3-blue,.w3-hover-blue:hover{color:#fff!important;background-color:#2196F3!important} 151 | .w3-light-blue,.w3-hover-light-blue:hover{color:#000!important;background-color:#87CEEB!important} 152 | .w3-brown,.w3-hover-brown:hover{color:#fff!important;background-color:#795548!important} 153 | .w3-cyan,.w3-hover-cyan:hover{color:#000!important;background-color:#00bcd4!important} 154 | .w3-blue-grey,.w3-hover-blue-grey:hover,.w3-blue-gray,.w3-hover-blue-gray:hover{color:#fff!important;background-color:#607d8b!important} 155 | .w3-green,.w3-hover-green:hover{color:#fff!important;background-color:#4CAF50!important} 156 | .w3-light-green,.w3-hover-light-green:hover{color:#000!important;background-color:#8bc34a!important} 157 | .w3-indigo,.w3-hover-indigo:hover{color:#fff!important;background-color:#3f51b5!important} 158 | .w3-khaki,.w3-hover-khaki:hover{color:#000!important;background-color:#f0e68c!important} 159 | .w3-lime,.w3-hover-lime:hover{color:#000!important;background-color:#cddc39!important} 160 | .w3-orange,.w3-hover-orange:hover{color:#000!important;background-color:#ff9800!important} 161 | .w3-deep-orange,.w3-hover-deep-orange:hover{color:#fff!important;background-color:#ff5722!important} 162 | .w3-pink,.w3-hover-pink:hover{color:#fff!important;background-color:#e91e63!important} 163 | .w3-purple,.w3-hover-purple:hover{color:#fff!important;background-color:#9c27b0!important} 164 | .w3-deep-purple,.w3-hover-deep-purple:hover{color:#fff!important;background-color:#673ab7!important} 165 | .w3-red,.w3-hover-red:hover{color:#fff!important;background-color:#f44336!important} 166 | .w3-sand,.w3-hover-sand:hover{color:#000!important;background-color:#fdf5e6!important} 167 | .w3-teal,.w3-hover-teal:hover{color:#fff!important;background-color:#009688!important} 168 | .w3-yellow,.w3-hover-yellow:hover{color:#000!important;background-color:#ffeb3b!important} 169 | .w3-white,.w3-hover-white:hover{color:#000!important;background-color:#fff!important} 170 | .w3-black,.w3-hover-black:hover{color:#fff!important;background-color:#000!important} 171 | .w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important} 172 | .w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important} 173 | .w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important} 174 | .w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important} 175 | .w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important} 176 | .w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important} 177 | .w3-pale-blue,.w3-hover-pale-blue:hover{color:#000!important;background-color:#ddffff!important} 178 | .w3-text-amber,.w3-hover-text-amber:hover{color:#ffc107!important} 179 | .w3-text-aqua,.w3-hover-text-aqua:hover{color:#00ffff!important} 180 | .w3-text-blue,.w3-hover-text-blue:hover{color:#2196F3!important} 181 | .w3-text-light-blue,.w3-hover-text-light-blue:hover{color:#87CEEB!important} 182 | .w3-text-brown,.w3-hover-text-brown:hover{color:#795548!important} 183 | .w3-text-cyan,.w3-hover-text-cyan:hover{color:#00bcd4!important} 184 | .w3-text-blue-grey,.w3-hover-text-blue-grey:hover,.w3-text-blue-gray,.w3-hover-text-blue-gray:hover{color:#607d8b!important} 185 | .w3-text-green,.w3-hover-text-green:hover{color:#4CAF50!important} 186 | .w3-text-light-green,.w3-hover-text-light-green:hover{color:#8bc34a!important} 187 | .w3-text-indigo,.w3-hover-text-indigo:hover{color:#3f51b5!important} 188 | .w3-text-khaki,.w3-hover-text-khaki:hover{color:#b4aa50!important} 189 | .w3-text-lime,.w3-hover-text-lime:hover{color:#cddc39!important} 190 | .w3-text-orange,.w3-hover-text-orange:hover{color:#ff9800!important} 191 | .w3-text-deep-orange,.w3-hover-text-deep-orange:hover{color:#ff5722!important} 192 | .w3-text-pink,.w3-hover-text-pink:hover{color:#e91e63!important} 193 | .w3-text-purple,.w3-hover-text-purple:hover{color:#9c27b0!important} 194 | .w3-text-deep-purple,.w3-hover-text-deep-purple:hover{color:#673ab7!important} 195 | .w3-text-red,.w3-hover-text-red:hover{color:#f44336!important} 196 | .w3-text-sand,.w3-hover-text-sand:hover{color:#fdf5e6!important} 197 | .w3-text-teal,.w3-hover-text-teal:hover{color:#009688!important} 198 | .w3-text-yellow,.w3-hover-text-yellow:hover{color:#d2be0e!important} 199 | .w3-text-white,.w3-hover-text-white:hover{color:#fff!important} 200 | .w3-text-black,.w3-hover-text-black:hover{color:#000!important} 201 | .w3-text-grey,.w3-hover-text-grey:hover,.w3-text-gray,.w3-hover-text-gray:hover{color:#757575!important} 202 | .w3-text-light-grey,.w3-hover-text-light-grey:hover,.w3-text-light-gray,.w3-hover-text-light-gray:hover{color:#f1f1f1!important} 203 | .w3-text-dark-grey,.w3-hover-text-dark-grey:hover,.w3-text-dark-gray,.w3-hover-text-dark-gray:hover{color:#3a3a3a!important} 204 | .w3-border-amber,.w3-hover-border-amber:hover{border-color:#ffc107!important} 205 | .w3-border-aqua,.w3-hover-border-aqua:hover{border-color:#00ffff!important} 206 | .w3-border-blue,.w3-hover-border-blue:hover{border-color:#2196F3!important} 207 | .w3-border-light-blue,.w3-hover-border-light-blue:hover{border-color:#87CEEB!important} 208 | .w3-border-brown,.w3-hover-border-brown:hover{border-color:#795548!important} 209 | .w3-border-cyan,.w3-hover-border-cyan:hover{border-color:#00bcd4!important} 210 | .w3-border-blue-grey,.w3-hover-border-blue-grey:hover,.w3-border-blue-gray,.w3-hover-border-blue-gray:hover{border-color:#607d8b!important} 211 | .w3-border-green,.w3-hover-border-green:hover{border-color:#4CAF50!important} 212 | .w3-border-light-green,.w3-hover-border-light-green:hover{border-color:#8bc34a!important} 213 | .w3-border-indigo,.w3-hover-border-indigo:hover{border-color:#3f51b5!important} 214 | .w3-border-khaki,.w3-hover-border-khaki:hover{border-color:#f0e68c!important} 215 | .w3-border-lime,.w3-hover-border-lime:hover{border-color:#cddc39!important} 216 | .w3-border-orange,.w3-hover-border-orange:hover{border-color:#ff9800!important} 217 | .w3-border-deep-orange,.w3-hover-border-deep-orange:hover{border-color:#ff5722!important} 218 | .w3-border-pink,.w3-hover-border-pink:hover{border-color:#e91e63!important} 219 | .w3-border-purple,.w3-hover-border-purple:hover{border-color:#9c27b0!important} 220 | .w3-border-deep-purple,.w3-hover-border-deep-purple:hover{border-color:#673ab7!important} 221 | .w3-border-red,.w3-hover-border-red:hover{border-color:#f44336!important} 222 | .w3-border-sand,.w3-hover-border-sand:hover{border-color:#fdf5e6!important} 223 | .w3-border-teal,.w3-hover-border-teal:hover{border-color:#009688!important} 224 | .w3-border-yellow,.w3-hover-border-yellow:hover{border-color:#ffeb3b!important} 225 | .w3-border-white,.w3-hover-border-white:hover{border-color:#fff!important} 226 | .w3-border-black,.w3-hover-border-black:hover{border-color:#000!important} 227 | .w3-border-grey,.w3-hover-border-grey:hover,.w3-border-gray,.w3-hover-border-gray:hover{border-color:#9e9e9e!important} 228 | .w3-border-light-grey,.w3-hover-border-light-grey:hover,.w3-border-light-gray,.w3-hover-border-light-gray:hover{border-color:#f1f1f1!important} 229 | .w3-border-dark-grey,.w3-hover-border-dark-grey:hover,.w3-border-dark-gray,.w3-hover-border-dark-gray:hover{border-color:#616161!important} 230 | .w3-border-pale-red,.w3-hover-border-pale-red:hover{border-color:#ffe7e7!important}.w3-border-pale-green,.w3-hover-border-pale-green:hover{border-color:#e7ffe7!important} 231 | .w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important} --------------------------------------------------------------------------------