├── LICENSE ├── README.md ├── index.html ├── lib ├── game │ └── main.js ├── impact │ ├── animation.js │ ├── background-map.js │ ├── collision-map.js │ ├── debug │ │ ├── debug.css │ │ ├── debug.js │ │ ├── entities-panel.js │ │ ├── graph-panel.js │ │ ├── maps-panel.js │ │ └── menu.js │ ├── entity-pool.js │ ├── entity.js │ ├── font.js │ ├── game.js │ ├── image.js │ ├── impact.js │ ├── input.js │ ├── loader.js │ ├── map.js │ ├── sound.js │ ├── system.js │ └── timer.js └── weltmeister │ ├── api │ ├── browse.php │ ├── config.php │ ├── glob.php │ └── save.php │ ├── arrow.png │ ├── collisiontiles-64.png │ ├── config.js │ ├── edit-entities.js │ ├── edit-map.js │ ├── entities.js │ ├── evented-input.js │ ├── jquery-1.7.1.min.js │ ├── jquery-ui-1.8.1.custom.min.js │ ├── modal-dialogs.js │ ├── select-file-dropdown.js │ ├── tile-select.js │ ├── undo.js │ ├── weltmeister.css │ └── weltmeister.js ├── media └── 04b03.font.png ├── tools ├── bake.bat ├── bake.php ├── bake.sh └── jsmin.php └── weltmeister.html /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2018 Dominic Szablewski 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Impact 2 | 3 | Impact is an HTML5 Game Engine. More info & documentation: http://impactjs.com/ 4 | 5 | Various example games to get you started are available on http://impactjs.com/download 6 | 7 | Impact is published under the [MIT Open Source License](http://opensource.org/licenses/mit-license.php). Note that Weltmeister (Impact's level editor) uses jQuery which comes with its own license. 8 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Impact Game 5 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /lib/game/main.js: -------------------------------------------------------------------------------- 1 | ig.module( 2 | 'game.main' 3 | ) 4 | .requires( 5 | 'impact.game', 6 | 'impact.font' 7 | ) 8 | .defines(function(){ 9 | 10 | MyGame = ig.Game.extend({ 11 | 12 | // Load a font 13 | font: new ig.Font( 'media/04b03.font.png' ), 14 | 15 | 16 | init: function() { 17 | // Initialize your game here; bind keys etc. 18 | }, 19 | 20 | update: function() { 21 | // Update all entities and backgroundMaps 22 | this.parent(); 23 | 24 | // Add your own, additional update code here 25 | }, 26 | 27 | draw: function() { 28 | // Draw all entities and backgroundMaps 29 | this.parent(); 30 | 31 | 32 | // Add your own drawing code here 33 | var x = ig.system.width/2, 34 | y = ig.system.height/2; 35 | 36 | this.font.draw( 'It Works!', x, y, ig.Font.ALIGN.CENTER ); 37 | } 38 | }); 39 | 40 | 41 | // Start the Game with 60fps, a resolution of 320x240, scaled 42 | // up by a factor of 2 43 | ig.main( '#canvas', MyGame, 60, 320, 240, 2 ); 44 | 45 | }); 46 | -------------------------------------------------------------------------------- /lib/impact/animation.js: -------------------------------------------------------------------------------- 1 | ig.module( 2 | 'impact.animation' 3 | ) 4 | .requires( 5 | 'impact.timer', 6 | 'impact.image' 7 | ) 8 | .defines(function(){ "use strict"; 9 | 10 | ig.AnimationSheet = ig.Class.extend({ 11 | width: 8, 12 | height: 8, 13 | image: null, 14 | 15 | init: function( path, width, height ) { 16 | this.width = width; 17 | this.height = height; 18 | 19 | this.image = new ig.Image( path ); 20 | } 21 | }); 22 | 23 | 24 | 25 | ig.Animation = ig.Class.extend({ 26 | sheet: null, 27 | timer: null, 28 | 29 | sequence: [], 30 | flip: {x: false, y: false}, 31 | pivot: {x: 0, y: 0}, 32 | 33 | frameTime: 0, 34 | frame: 0, 35 | tile: 0, 36 | stop: false, 37 | loopCount: 0, 38 | alpha: 1, 39 | angle: 0, 40 | 41 | 42 | init: function( sheet, frameTime, sequence, stop ) { 43 | this.sheet = sheet; 44 | this.pivot = {x: sheet.width/2, y: sheet.height/2 }; 45 | this.timer = new ig.Timer(); 46 | 47 | this.frameTime = frameTime; 48 | this.sequence = sequence; 49 | this.stop = !!stop; 50 | this.tile = this.sequence[0]; 51 | }, 52 | 53 | 54 | rewind: function() { 55 | this.timer.set(); 56 | this.loopCount = 0; 57 | this.frame = 0; 58 | this.tile = this.sequence[0]; 59 | return this; 60 | }, 61 | 62 | 63 | gotoFrame: function( f ) { 64 | // Offset the timer by one tenth of a millisecond to make sure we 65 | // jump to the correct frame and circumvent rounding errors 66 | this.timer.set( this.frameTime * -f - 0.0001 ); 67 | this.update(); 68 | }, 69 | 70 | 71 | gotoRandomFrame: function() { 72 | this.gotoFrame( Math.floor(Math.random() * this.sequence.length) ); 73 | }, 74 | 75 | 76 | update: function() { 77 | var frameTotal = Math.floor(this.timer.delta() / this.frameTime); 78 | this.loopCount = Math.floor(frameTotal / this.sequence.length); 79 | if( this.stop && this.loopCount > 0 ) { 80 | this.frame = this.sequence.length - 1; 81 | } 82 | else { 83 | this.frame = frameTotal % this.sequence.length; 84 | } 85 | this.tile = this.sequence[ this.frame ]; 86 | }, 87 | 88 | 89 | draw: function( targetX, targetY ) { 90 | var bbsize = Math.max(this.sheet.width, this.sheet.height); 91 | 92 | // On screen? 93 | if( 94 | targetX > ig.system.width || targetY > ig.system.height || 95 | targetX + bbsize < 0 || targetY + bbsize < 0 96 | ) { 97 | return; 98 | } 99 | 100 | if( this.alpha != 1) { 101 | ig.system.context.globalAlpha = this.alpha; 102 | } 103 | 104 | if( this.angle == 0 ) { 105 | this.sheet.image.drawTile( 106 | targetX, targetY, 107 | this.tile, this.sheet.width, this.sheet.height, 108 | this.flip.x, this.flip.y 109 | ); 110 | } 111 | else { 112 | ig.system.context.save(); 113 | ig.system.context.translate( 114 | ig.system.getDrawPos(targetX + this.pivot.x), 115 | ig.system.getDrawPos(targetY + this.pivot.y) 116 | ); 117 | ig.system.context.rotate( this.angle ); 118 | this.sheet.image.drawTile( 119 | -this.pivot.x, -this.pivot.y, 120 | this.tile, this.sheet.width, this.sheet.height, 121 | this.flip.x, this.flip.y 122 | ); 123 | ig.system.context.restore(); 124 | } 125 | 126 | if( this.alpha != 1) { 127 | ig.system.context.globalAlpha = 1; 128 | } 129 | } 130 | }); 131 | 132 | }); 133 | -------------------------------------------------------------------------------- /lib/impact/background-map.js: -------------------------------------------------------------------------------- 1 | ig.module( 2 | 'impact.background-map' 3 | ) 4 | .requires( 5 | 'impact.map', 6 | 'impact.image' 7 | ) 8 | .defines(function(){ "use strict"; 9 | 10 | ig.BackgroundMap = ig.Map.extend({ 11 | tiles: null, 12 | scroll: {x: 0, y:0}, 13 | distance: 1, 14 | repeat: false, 15 | tilesetName: '', 16 | foreground: false, 17 | enabled: true, 18 | 19 | preRender: false, 20 | preRenderedChunks: null, 21 | chunkSize: 512, 22 | debugChunks: false, 23 | 24 | 25 | anims: {}, 26 | 27 | 28 | init: function( tilesize, data, tileset ) { 29 | this.parent( tilesize, data ); 30 | this.setTileset( tileset ); 31 | }, 32 | 33 | 34 | setTileset: function( tileset ) { 35 | this.tilesetName = tileset instanceof ig.Image ? tileset.path : tileset; 36 | this.tiles = new ig.Image( this.tilesetName ); 37 | this.preRenderedChunks = null; 38 | }, 39 | 40 | 41 | setScreenPos: function( x, y ) { 42 | this.scroll.x = x / this.distance; 43 | this.scroll.y = y / this.distance; 44 | }, 45 | 46 | 47 | preRenderMapToChunks: function() { 48 | var totalWidth = this.width * this.tilesize * ig.system.scale, 49 | totalHeight = this.height * this.tilesize * ig.system.scale; 50 | 51 | // If this layer is smaller than the chunkSize, adjust the chunkSize 52 | // accordingly, so we don't have as much overdraw 53 | this.chunkSize = Math.min( Math.max(totalWidth, totalHeight), this.chunkSize ); 54 | 55 | var chunkCols = Math.ceil(totalWidth / this.chunkSize), 56 | chunkRows = Math.ceil(totalHeight / this.chunkSize); 57 | 58 | this.preRenderedChunks = []; 59 | for( var y = 0; y < chunkRows; y++ ) { 60 | this.preRenderedChunks[y] = []; 61 | 62 | for( var x = 0; x < chunkCols; x++ ) { 63 | 64 | 65 | var chunkWidth = (x == chunkCols-1) 66 | ? totalWidth - x * this.chunkSize 67 | : this.chunkSize; 68 | 69 | var chunkHeight = (y == chunkRows-1) 70 | ? totalHeight - y * this.chunkSize 71 | : this.chunkSize; 72 | 73 | this.preRenderedChunks[y][x] = this.preRenderChunk( x, y, chunkWidth, chunkHeight ); 74 | } 75 | } 76 | }, 77 | 78 | 79 | preRenderChunk: function( cx, cy, w, h ) { 80 | var tw = w / this.tilesize / ig.system.scale + 1, 81 | th = h / this.tilesize / ig.system.scale + 1; 82 | 83 | var nx = (cx * this.chunkSize / ig.system.scale) % this.tilesize, 84 | ny = (cy * this.chunkSize / ig.system.scale) % this.tilesize; 85 | 86 | var tx = Math.floor(cx * this.chunkSize / this.tilesize / ig.system.scale), 87 | ty = Math.floor(cy * this.chunkSize / this.tilesize / ig.system.scale); 88 | 89 | 90 | var chunk = ig.$new('canvas'); 91 | chunk.width = w; 92 | chunk.height = h; 93 | chunk.retinaResolutionEnabled = false; // Opt out for Ejecta 94 | 95 | var chunkContext = chunk.getContext('2d'); 96 | ig.System.scaleMode(chunk, chunkContext); 97 | 98 | var screenContext = ig.system.context; 99 | ig.system.context = chunkContext; 100 | 101 | for( var x = 0; x < tw; x++ ) { 102 | for( var y = 0; y < th; y++ ) { 103 | if( x + tx < this.width && y + ty < this.height ) { 104 | var tile = this.data[y+ty][x+tx]; 105 | if( tile ) { 106 | this.tiles.drawTile( 107 | x * this.tilesize - nx, y * this.tilesize - ny, 108 | tile - 1, this.tilesize 109 | ); 110 | } 111 | } 112 | } 113 | } 114 | ig.system.context = screenContext; 115 | 116 | // Workaround for Chrome 49 bug - handling many offscreen canvases 117 | // seems to slow down the browser significantly. So we convert the 118 | // canvas to an image. 119 | var image = new Image(); 120 | image.src = chunk.toDataURL(); 121 | image.width = chunk.width; 122 | image.height = chunk.height; 123 | 124 | return image; 125 | }, 126 | 127 | 128 | draw: function() { 129 | if( !this.tiles.loaded || !this.enabled ) { 130 | return; 131 | } 132 | 133 | if( this.preRender ) { 134 | this.drawPreRendered(); 135 | } 136 | else { 137 | this.drawTiled(); 138 | } 139 | }, 140 | 141 | 142 | drawPreRendered: function() { 143 | if( !this.preRenderedChunks ) { 144 | this.preRenderMapToChunks(); 145 | } 146 | 147 | var dx = ig.system.getDrawPos(this.scroll.x), 148 | dy = ig.system.getDrawPos(this.scroll.y); 149 | 150 | 151 | if( this.repeat ) { 152 | var w = this.width * this.tilesize * ig.system.scale; 153 | dx = (dx%w + w) % w; 154 | 155 | var h = this.height * this.tilesize * ig.system.scale; 156 | dy = (dy%h + h) % h; 157 | } 158 | 159 | var minChunkX = Math.max( Math.floor(dx / this.chunkSize), 0 ), 160 | minChunkY = Math.max( Math.floor(dy / this.chunkSize), 0 ), 161 | maxChunkX = Math.ceil((dx+ig.system.realWidth) / this.chunkSize), 162 | maxChunkY = Math.ceil((dy+ig.system.realHeight) / this.chunkSize), 163 | maxRealChunkX = this.preRenderedChunks[0].length, 164 | maxRealChunkY = this.preRenderedChunks.length; 165 | 166 | 167 | if( !this.repeat ) { 168 | maxChunkX = Math.min( maxChunkX, maxRealChunkX ); 169 | maxChunkY = Math.min( maxChunkY, maxRealChunkY ); 170 | } 171 | 172 | 173 | var nudgeY = 0; 174 | for( var cy = minChunkY; cy < maxChunkY; cy++ ) { 175 | 176 | var nudgeX = 0; 177 | for( var cx = minChunkX; cx < maxChunkX; cx++ ) { 178 | var chunk = this.preRenderedChunks[cy % maxRealChunkY][cx % maxRealChunkX]; 179 | 180 | var x = -dx + cx * this.chunkSize - nudgeX; 181 | var y = -dy + cy * this.chunkSize - nudgeY; 182 | ig.system.context.drawImage( chunk, x, y); 183 | ig.Image.drawCount++; 184 | 185 | if( this.debugChunks ) { 186 | ig.system.context.strokeStyle = '#f0f'; 187 | ig.system.context.strokeRect( x, y, this.chunkSize, this.chunkSize ); 188 | } 189 | 190 | // If we repeat in X and this chunk's width wasn't the full chunk size 191 | // and the screen is not already filled, we need to draw another chunk 192 | // AND nudge it to be flush with the last chunk 193 | if( this.repeat && chunk.width < this.chunkSize && x + chunk.width < ig.system.realWidth ) { 194 | nudgeX += this.chunkSize - chunk.width; 195 | 196 | // Only re-calculate maxChunkX during initial row to avoid 197 | // unnecessary off-screen draws on subsequent rows. 198 | if( cy == minChunkY ) { 199 | maxChunkX++; 200 | } 201 | } 202 | } 203 | 204 | // Same as above, but for Y 205 | if( this.repeat && chunk.height < this.chunkSize && y + chunk.height < ig.system.realHeight ) { 206 | nudgeY += this.chunkSize - chunk.height; 207 | maxChunkY++; 208 | } 209 | } 210 | }, 211 | 212 | 213 | drawTiled: function() { 214 | var tile = 0, 215 | anim = null, 216 | tileOffsetX = (this.scroll.x / this.tilesize).toInt(), 217 | tileOffsetY = (this.scroll.y / this.tilesize).toInt(), 218 | pxOffsetX = this.scroll.x % this.tilesize, 219 | pxOffsetY = this.scroll.y % this.tilesize, 220 | pxMinX = -pxOffsetX - this.tilesize, 221 | pxMinY = -pxOffsetY - this.tilesize, 222 | pxMaxX = ig.system.width + this.tilesize - pxOffsetX, 223 | pxMaxY = ig.system.height + this.tilesize - pxOffsetY; 224 | 225 | 226 | // FIXME: could be sped up for non-repeated maps: restrict the for loops 227 | // to the map size instead of to the screen size and skip the 'repeat' 228 | // checks inside the loop. 229 | 230 | for( var mapY = -1, pxY = pxMinY; pxY < pxMaxY; mapY++, pxY += this.tilesize) { 231 | var tileY = mapY + tileOffsetY; 232 | 233 | // Repeat Y? 234 | if( tileY >= this.height || tileY < 0 ) { 235 | if( !this.repeat ) { continue; } 236 | tileY = (tileY%this.height + this.height) % this.height; 237 | } 238 | 239 | for( var mapX = -1, pxX = pxMinX; pxX < pxMaxX; mapX++, pxX += this.tilesize ) { 240 | var tileX = mapX + tileOffsetX; 241 | 242 | // Repeat X? 243 | if( tileX >= this.width || tileX < 0 ) { 244 | if( !this.repeat ) { continue; } 245 | tileX = (tileX%this.width + this.width) % this.width; 246 | } 247 | 248 | // Draw! 249 | if( (tile = this.data[tileY][tileX]) ) { 250 | if( (anim = this.anims[tile-1]) ) { 251 | anim.draw( pxX, pxY ); 252 | } 253 | else { 254 | this.tiles.drawTile( pxX, pxY, tile-1, this.tilesize ); 255 | } 256 | } 257 | } // end for x 258 | } // end for y 259 | } 260 | }); 261 | 262 | }); 263 | -------------------------------------------------------------------------------- /lib/impact/collision-map.js: -------------------------------------------------------------------------------- 1 | ig.module( 2 | 'impact.collision-map' 3 | ) 4 | .requires( 5 | 'impact.map' 6 | ) 7 | .defines(function(){ "use strict"; 8 | 9 | ig.CollisionMap = ig.Map.extend({ 10 | 11 | lastSlope: 1, 12 | tiledef: null, 13 | 14 | init: function( tilesize, data, tiledef ) { 15 | this.parent( tilesize, data ); 16 | this.tiledef = tiledef || ig.CollisionMap.defaultTileDef; 17 | 18 | for( var t in this.tiledef ) { 19 | if( t|0 > this.lastSlope ) { 20 | this.lastSlope = t|0; 21 | } 22 | } 23 | }, 24 | 25 | 26 | trace: function( x, y, vx, vy, objectWidth, objectHeight ) { 27 | // Set up the trace-result 28 | var res = { 29 | collision: {x: false, y: false, slope: false}, 30 | pos: {x: x, y: y}, 31 | tile: {x: 0, y: 0} 32 | }; 33 | 34 | // Break the trace down into smaller steps if necessary. 35 | // We add a little extra movement (0.1 px) when calculating the number of steps required, 36 | // to force an additional trace step whenever vx or vy is a factor of tilesize. This 37 | // prevents the trace step from skipping through the very first tile. 38 | var steps = Math.ceil((Math.max(Math.abs(vx), Math.abs(vy))+0.1) / this.tilesize); 39 | if( steps > 1 ) { 40 | var sx = vx / steps; 41 | var sy = vy / steps; 42 | 43 | for( var i = 0; i < steps && (sx || sy); i++ ) { 44 | this._traceStep( res, x, y, sx, sy, objectWidth, objectHeight, vx, vy, i ); 45 | 46 | x = res.pos.x; 47 | y = res.pos.y; 48 | if( res.collision.x ) { sx = 0; vx = 0; } 49 | if( res.collision.y ) { sy = 0; vy = 0; } 50 | if( res.collision.slope ) { break; } 51 | } 52 | } 53 | 54 | // Just one step 55 | else { 56 | this._traceStep( res, x, y, vx, vy, objectWidth, objectHeight, vx, vy, 0 ); 57 | } 58 | 59 | return res; 60 | }, 61 | 62 | 63 | _traceStep: function( res, x, y, vx, vy, width, height, rvx, rvy, step ) { 64 | 65 | res.pos.x += vx; 66 | res.pos.y += vy; 67 | 68 | var t = 0; 69 | 70 | // Horizontal collision (walls) 71 | if( vx ) { 72 | var pxOffsetX = (vx > 0 ? width : 0); 73 | var tileOffsetX = (vx < 0 ? this.tilesize : 0); 74 | 75 | var firstTileY = Math.max( Math.floor(y / this.tilesize), 0 ); 76 | var lastTileY = Math.min( Math.ceil((y + height) / this.tilesize), this.height ); 77 | var tileX = Math.floor( (res.pos.x + pxOffsetX) / this.tilesize ); 78 | 79 | // We need to test the new tile position as well as the current one, as we 80 | // could still collide with the current tile if it's a line def. 81 | // We can skip this test if this is not the first step or the new tile position 82 | // is the same as the current one. 83 | var prevTileX = Math.floor( (x + pxOffsetX) / this.tilesize ); 84 | if( step > 0 || tileX == prevTileX || prevTileX < 0 || prevTileX >= this.width ) { 85 | prevTileX = -1; 86 | } 87 | 88 | // Still inside this collision map? 89 | if( tileX >= 0 && tileX < this.width ) { 90 | for( var tileY = firstTileY; tileY < lastTileY; tileY++ ) { 91 | if( prevTileX != -1 ) { 92 | t = this.data[tileY][prevTileX]; 93 | if( 94 | t > 1 && t <= this.lastSlope && 95 | this._checkTileDef(res, t, x, y, rvx, rvy, width, height, prevTileX, tileY) 96 | ) { 97 | break; 98 | } 99 | } 100 | 101 | t = this.data[tileY][tileX]; 102 | if( 103 | t == 1 || t > this.lastSlope || // fully solid tile? 104 | (t > 1 && this._checkTileDef(res, t, x, y, rvx, rvy, width, height, tileX, tileY)) // slope? 105 | ) { 106 | if( t > 1 && t <= this.lastSlope && res.collision.slope ) { 107 | break; 108 | } 109 | 110 | // full tile collision! 111 | res.collision.x = true; 112 | res.tile.x = t; 113 | x = res.pos.x = tileX * this.tilesize - pxOffsetX + tileOffsetX; 114 | rvx = 0; 115 | break; 116 | } 117 | } 118 | } 119 | } 120 | 121 | // Vertical collision (floor, ceiling) 122 | if( vy ) { 123 | var pxOffsetY = (vy > 0 ? height : 0); 124 | var tileOffsetY = (vy < 0 ? this.tilesize : 0); 125 | 126 | var firstTileX = Math.max( Math.floor(res.pos.x / this.tilesize), 0 ); 127 | var lastTileX = Math.min( Math.ceil((res.pos.x + width) / this.tilesize), this.width ); 128 | var tileY = Math.floor( (res.pos.y + pxOffsetY) / this.tilesize ); 129 | 130 | var prevTileY = Math.floor( (y + pxOffsetY) / this.tilesize ); 131 | if( step > 0 || tileY == prevTileY || prevTileY < 0 || prevTileY >= this.height ) { 132 | prevTileY = -1; 133 | } 134 | 135 | // Still inside this collision map? 136 | if( tileY >= 0 && tileY < this.height ) { 137 | for( var tileX = firstTileX; tileX < lastTileX; tileX++ ) { 138 | if( prevTileY != -1 ) { 139 | t = this.data[prevTileY][tileX]; 140 | if( 141 | t > 1 && t <= this.lastSlope && 142 | this._checkTileDef(res, t, x, y, rvx, rvy, width, height, tileX, prevTileY) ) { 143 | break; 144 | } 145 | } 146 | 147 | t = this.data[tileY][tileX]; 148 | if( 149 | t == 1 || t > this.lastSlope || // fully solid tile? 150 | (t > 1 && this._checkTileDef(res, t, x, y, rvx, rvy, width, height, tileX, tileY)) // slope? 151 | ) { 152 | if( t > 1 && t <= this.lastSlope && res.collision.slope ) { 153 | break; 154 | } 155 | 156 | // full tile collision! 157 | res.collision.y = true; 158 | res.tile.y = t; 159 | res.pos.y = tileY * this.tilesize - pxOffsetY + tileOffsetY; 160 | break; 161 | } 162 | } 163 | } 164 | } 165 | 166 | // res is changed in place, nothing to return 167 | }, 168 | 169 | 170 | _checkTileDef: function( res, t, x, y, vx, vy, width, height, tileX, tileY ) { 171 | var def = this.tiledef[t]; 172 | if( !def ) { return false; } 173 | 174 | var lx = (tileX + def[0]) * this.tilesize, 175 | ly = (tileY + def[1]) * this.tilesize, 176 | lvx = (def[2] - def[0]) * this.tilesize, 177 | lvy = (def[3] - def[1]) * this.tilesize, 178 | solid = def[4]; 179 | 180 | // Find the box corner to test, relative to the line 181 | var tx = x + vx + (lvy < 0 ? width : 0) - lx, 182 | ty = y + vy + (lvx > 0 ? height : 0) - ly; 183 | 184 | // Is the box corner behind the line? 185 | if( lvx * ty - lvy * tx > 0 ) { 186 | 187 | // Lines are only solid from one side - find the dot product of 188 | // line normal and movement vector and dismiss if wrong side 189 | if( vx * -lvy + vy * lvx < 0 ) { 190 | return solid; 191 | } 192 | 193 | // Find the line normal 194 | var length = Math.sqrt(lvx * lvx + lvy * lvy); 195 | var nx = lvy/length, 196 | ny = -lvx/length; 197 | 198 | // Project out of the line 199 | var proj = tx * nx + ty * ny; 200 | var px = nx * proj, 201 | py = ny * proj; 202 | 203 | // If we project further out than we moved in, then this is a full 204 | // tile collision for solid tiles. 205 | // For non-solid tiles, make sure we were in front of the line. 206 | if( px*px+py*py >= vx*vx+vy*vy ) { 207 | return solid || (lvx * (ty-vy) - lvy * (tx-vx) < 0.5); 208 | } 209 | 210 | res.pos.x = x + vx - px; 211 | res.pos.y = y + vy - py; 212 | res.collision.slope = {x: lvx, y: lvy, nx: nx, ny: ny}; 213 | return true; 214 | } 215 | 216 | return false; 217 | } 218 | }); 219 | 220 | 221 | // Default Slope Tile definition. Each tile is defined by an array of 5 vars: 222 | // - 4 for the line in tile coordinates (0 -- 1) 223 | // - 1 specifing whether the tile is 'filled' behind the line or not 224 | // [ x1, y1, x2, y2, solid ] 225 | 226 | // Defining 'half', 'one third' and 'two thirds' as vars makes it a bit 227 | // easier to read... I hope. 228 | var H = 1/2, 229 | N = 1/3, 230 | M = 2/3, 231 | SOLID = true, 232 | NON_SOLID = false; 233 | 234 | ig.CollisionMap.defaultTileDef = { 235 | /* 15 NE */ 5: [0,1, 1,M, SOLID], 6: [0,M, 1,N, SOLID], 7: [0,N, 1,0, SOLID], 236 | /* 22 NE */ 3: [0,1, 1,H, SOLID], 4: [0,H, 1,0, SOLID], 237 | /* 45 NE */ 2: [0,1, 1,0, SOLID], 238 | /* 67 NE */ 10: [H,1, 1,0, SOLID], 21: [0,1, H,0, SOLID], 239 | /* 75 NE */ 32: [M,1, 1,0, SOLID], 43: [N,1, M,0, SOLID], 54: [0,1, N,0, SOLID], 240 | 241 | /* 15 SE */ 27: [0,0, 1,N, SOLID], 28: [0,N, 1,M, SOLID], 29: [0,M, 1,1, SOLID], 242 | /* 22 SE */ 25: [0,0, 1,H, SOLID], 26: [0,H, 1,1, SOLID], 243 | /* 45 SE */ 24: [0,0, 1,1, SOLID], 244 | /* 67 SE */ 11: [0,0, H,1, SOLID], 22: [H,0, 1,1, SOLID], 245 | /* 75 SE */ 33: [0,0, N,1, SOLID], 44: [N,0, M,1, SOLID], 55: [M,0, 1,1, SOLID], 246 | 247 | /* 15 NW */ 16: [1,N, 0,0, SOLID], 17: [1,M, 0,N, SOLID], 18: [1,1, 0,M, SOLID], 248 | /* 22 NW */ 14: [1,H, 0,0, SOLID], 15: [1,1, 0,H, SOLID], 249 | /* 45 NW */ 13: [1,1, 0,0, SOLID], 250 | /* 67 NW */ 8: [H,1, 0,0, SOLID], 19: [1,1, H,0, SOLID], 251 | /* 75 NW */ 30: [N,1, 0,0, SOLID], 41: [M,1, N,0, SOLID], 52: [1,1, M,0, SOLID], 252 | 253 | /* 15 SW */ 38: [1,M, 0,1, SOLID], 39: [1,N, 0,M, SOLID], 40: [1,0, 0,N, SOLID], 254 | /* 22 SW */ 36: [1,H, 0,1, SOLID], 37: [1,0, 0,H, SOLID], 255 | /* 45 SW */ 35: [1,0, 0,1, SOLID], 256 | /* 67 SW */ 9: [1,0, H,1, SOLID], 20: [H,0, 0,1, SOLID], 257 | /* 75 SW */ 31: [1,0, M,1, SOLID], 42: [M,0, N,1, SOLID], 53: [N,0, 0,1, SOLID], 258 | 259 | /* Go N */ 12: [0,0, 1,0, NON_SOLID], 260 | /* Go S */ 23: [1,1, 0,1, NON_SOLID], 261 | /* Go E */ 34: [1,0, 1,1, NON_SOLID], 262 | /* Go W */ 45: [0,1, 0,0, NON_SOLID] 263 | 264 | // Now that was fun! 265 | }; 266 | 267 | 268 | // Static Dummy CollisionMap; never collides 269 | ig.CollisionMap.staticNoCollision = { trace: function( x, y, vx, vy ) { 270 | return { 271 | collision: {x: false, y: false, slope: false}, 272 | pos: {x: x+vx, y: y+vy}, 273 | tile: {x: 0, y: 0} 274 | }; 275 | }}; 276 | 277 | }); -------------------------------------------------------------------------------- /lib/impact/debug/debug.css: -------------------------------------------------------------------------------- 1 | .ig_debug { 2 | position: fixed; 3 | left: 0; 4 | bottom: 0; 5 | width: 100%; 6 | background-color: #000; 7 | border-top: 2px solid #f57401; 8 | font-size: 12px; 9 | color: #fff; 10 | z-index: 1000; 11 | -webkit-user-select: none; 12 | } 13 | 14 | .ig_debug_panel_menu { 15 | height: 28px; 16 | background: #222; 17 | background: -webkit-gradient(linear, left bottom, left top, color-stop(0,#000000), color-stop(1,#333)); 18 | background: -moz-linear-gradient(center bottom, #000000 0%, #333 100%); 19 | background: -o-linear-gradient(#333, #000000); 20 | } 21 | 22 | .ig_debug_panel_menu div { 23 | float: left; 24 | height: 22px; 25 | padding: 6px 8px 0 8px; 26 | border-right: 1px solid #333; 27 | } 28 | 29 | .ig_debug_panel_menu .ig_debug_head { 30 | font-weight: bold; 31 | color: #888; 32 | } 33 | 34 | .ig_debug_menu_item:hover { 35 | cursor: pointer; 36 | background-color: #fff; 37 | color: #000; 38 | } 39 | 40 | .ig_debug_menu_item.active, .ig_debug_menu_item.active:hover { 41 | background-color: #000; 42 | color: #fff; 43 | } 44 | 45 | .ig_debug_stats { 46 | position: absolute; 47 | right: 0; 48 | top: 0; 49 | float: right; 50 | color: #888; 51 | border-left: 1px solid #333; 52 | text-align: right; 53 | } 54 | 55 | .ig_debug_stats span { 56 | width: 3em; 57 | display:inline-block; 58 | color: #fff !important; 59 | margin-right: 0.2em; 60 | margin-left: 0.3em; 61 | font-family: bitstream vera sans mono, courier new; 62 | white-space: nowrap; 63 | } 64 | 65 | .ig_debug_panel { 66 | height: 152px; 67 | overflow: auto; 68 | position: relative; 69 | } 70 | 71 | .ig_debug_panel canvas { 72 | border-bottom: 1px solid #444; 73 | } 74 | 75 | .ig_debug_panel .ig_debug_panel { 76 | padding: 8px; 77 | height: auto; 78 | float: left; 79 | background-color: #000; 80 | border-right: 2px solid #222; 81 | } 82 | 83 | .ig_debug_option { 84 | padding: 2px 0 2px 8px; 85 | cursor: pointer; 86 | } 87 | 88 | .ig_debug_option:first-child { 89 | margin-top: 8px; 90 | } 91 | 92 | .ig_debug_option:hover { 93 | background-color: #111; 94 | } 95 | 96 | .ig_debug_graph_mark { 97 | position: absolute; 98 | color: #888; 99 | left: 4px; 100 | font-size: 10px; 101 | margin-top: -12px; 102 | } 103 | 104 | .ig_debug_legend { 105 | color: #ccc; 106 | } 107 | 108 | .ig_debug_label_mark { 109 | display: inline-block; 110 | width: 10px; 111 | height: 10px; 112 | margin-right: 4px; 113 | -webkit-transition: 0.1s linear; 114 | -moz-transition: 0.1s linear; 115 | } 116 | 117 | .ig_debug_legend_color { 118 | display: inline-block; 119 | width: 6px; 120 | height: 10px; 121 | margin-right: 4px; 122 | margin-left: 16px; 123 | } 124 | 125 | .ig_debug_legend_number { 126 | width: 3em; 127 | display: inline-block; 128 | text-align: right; 129 | font-family: bitstream vera sans mono, courier new; 130 | color: #fff; 131 | margin-right: 0.2em; 132 | } 133 | 134 | 135 | .ig_debug_map_container { 136 | position: relative; 137 | overflow: hidden; 138 | border: 1px solid #888; 139 | } 140 | 141 | .ig_debug_map_container canvas { 142 | position: absolute; 143 | } 144 | 145 | .ig_debug_map_screen { 146 | position: absolute; 147 | border: 1px solid #f0f; 148 | } 149 | -------------------------------------------------------------------------------- /lib/impact/debug/debug.js: -------------------------------------------------------------------------------- 1 | ig.module( 2 | 'impact.debug.debug' 3 | ) 4 | .requires( 5 | 'impact.debug.entities-panel', 6 | 'impact.debug.maps-panel', 7 | 'impact.debug.graph-panel' 8 | ) 9 | .defines(function(){ "use strict"; 10 | 11 | /* Empty module to require all debug panels */ 12 | 13 | }); -------------------------------------------------------------------------------- /lib/impact/debug/entities-panel.js: -------------------------------------------------------------------------------- 1 | ig.module( 2 | 'impact.debug.entities-panel' 3 | ) 4 | .requires( 5 | 'impact.debug.menu', 6 | 'impact.entity' 7 | ) 8 | .defines(function(){ "use strict"; 9 | 10 | 11 | ig.Entity.inject({ 12 | colors: { 13 | names: '#fff', 14 | velocities: '#0f0', 15 | boxes: '#f00' 16 | }, 17 | 18 | draw: function() { 19 | this.parent(); 20 | 21 | // Collision Boxes 22 | if( ig.Entity._debugShowBoxes ) { 23 | ig.system.context.strokeStyle = this.colors.boxes; 24 | ig.system.context.lineWidth = 1.0; 25 | ig.system.context.strokeRect( 26 | ig.system.getDrawPos(this.pos.x.round() - ig.game.screen.x) - 0.5, 27 | ig.system.getDrawPos(this.pos.y.round() - ig.game.screen.y) - 0.5, 28 | this.size.x * ig.system.scale, 29 | this.size.y * ig.system.scale 30 | ); 31 | } 32 | 33 | // Velocities 34 | if( ig.Entity._debugShowVelocities ) { 35 | var x = this.pos.x + this.size.x/2; 36 | var y = this.pos.y + this.size.y/2; 37 | 38 | this._debugDrawLine( this.colors.velocities, x, y, x + this.vel.x, y + this.vel.y ); 39 | } 40 | 41 | // Names & Targets 42 | if( ig.Entity._debugShowNames ) { 43 | if( this.name ) { 44 | ig.system.context.fillStyle = this.colors.names; 45 | ig.system.context.fillText( 46 | this.name, 47 | ig.system.getDrawPos(this.pos.x - ig.game.screen.x), 48 | ig.system.getDrawPos(this.pos.y - ig.game.screen.y) 49 | ); 50 | } 51 | 52 | if( typeof(this.target) == 'object' ) { 53 | for( var t in this.target ) { 54 | var ent = ig.game.getEntityByName( this.target[t] ); 55 | if( ent ) { 56 | this._debugDrawLine( this.colors.names, 57 | this.pos.x + this.size.x/2, this.pos.y + this.size.y/2, 58 | ent.pos.x + ent.size.x/2, ent.pos.y + ent.size.y/2 59 | ); 60 | } 61 | } 62 | } 63 | } 64 | }, 65 | 66 | 67 | _debugDrawLine: function( color, sx, sy, dx, dy ) { 68 | ig.system.context.strokeStyle = color; 69 | ig.system.context.lineWidth = 1.0; 70 | 71 | ig.system.context.beginPath(); 72 | ig.system.context.moveTo( 73 | ig.system.getDrawPos(sx - ig.game.screen.x), 74 | ig.system.getDrawPos(sy - ig.game.screen.y) 75 | ); 76 | ig.system.context.lineTo( 77 | ig.system.getDrawPos(dx - ig.game.screen.x), 78 | ig.system.getDrawPos(dy - ig.game.screen.y) 79 | ); 80 | ig.system.context.stroke(); 81 | ig.system.context.closePath(); 82 | } 83 | }); 84 | 85 | 86 | ig.Entity._debugEnableChecks = true; 87 | ig.Entity._debugShowBoxes = false; 88 | ig.Entity._debugShowVelocities = false; 89 | ig.Entity._debugShowNames = false; 90 | 91 | ig.Entity.oldCheckPair = ig.Entity.checkPair; 92 | ig.Entity.checkPair = function( a, b ) { 93 | if( !ig.Entity._debugEnableChecks ) { 94 | return; 95 | } 96 | ig.Entity.oldCheckPair( a, b ); 97 | }; 98 | 99 | 100 | ig.debug.addPanel({ 101 | type: ig.DebugPanel, 102 | name: 'entities', 103 | label: 'Entities', 104 | options: [ 105 | { 106 | name: 'Checks & Collisions', 107 | object: ig.Entity, 108 | property: '_debugEnableChecks' 109 | }, 110 | { 111 | name: 'Show Collision Boxes', 112 | object: ig.Entity, 113 | property: '_debugShowBoxes' 114 | }, 115 | { 116 | name: 'Show Velocities', 117 | object: ig.Entity, 118 | property: '_debugShowVelocities' 119 | }, 120 | { 121 | name: 'Show Names & Targets', 122 | object: ig.Entity, 123 | property: '_debugShowNames' 124 | } 125 | ] 126 | }); 127 | 128 | 129 | }); -------------------------------------------------------------------------------- /lib/impact/debug/graph-panel.js: -------------------------------------------------------------------------------- 1 | ig.module( 2 | 'impact.debug.graph-panel' 3 | ) 4 | .requires( 5 | 'impact.debug.menu', 6 | 'impact.system', 7 | 'impact.game', 8 | 'impact.image' 9 | ) 10 | .defines(function(){ "use strict"; 11 | 12 | 13 | ig.Game.inject({ 14 | draw: function() { 15 | ig.graph.beginClock('draw'); 16 | this.parent(); 17 | ig.graph.endClock('draw'); 18 | }, 19 | 20 | 21 | update: function() { 22 | ig.graph.beginClock('update'); 23 | this.parent(); 24 | ig.graph.endClock('update'); 25 | }, 26 | 27 | 28 | checkEntities: function() { 29 | ig.graph.beginClock('checks'); 30 | this.parent(); 31 | ig.graph.endClock('checks'); 32 | } 33 | }); 34 | 35 | 36 | 37 | ig.DebugGraphPanel = ig.DebugPanel.extend({ 38 | clocks: {}, 39 | marks: [], 40 | textY: 0, 41 | height: 128, 42 | ms: 64, 43 | timeBeforeRun: 0, 44 | 45 | 46 | init: function( name, label ) { 47 | this.parent( name, label ); 48 | 49 | this.mark16ms = (this.height - (this.height/this.ms) * 16).round(); 50 | this.mark33ms = (this.height - (this.height/this.ms) * 33).round(); 51 | this.msHeight = this.height/this.ms; 52 | 53 | this.graph = ig.$new('canvas'); 54 | this.graph.width = window.innerWidth; 55 | this.graph.height = this.height; 56 | this.container.appendChild( this.graph ); 57 | this.ctx = this.graph.getContext('2d'); 58 | 59 | this.ctx.fillStyle = '#444'; 60 | this.ctx.fillRect( 0, this.mark16ms, this.graph.width, 1 ); 61 | this.ctx.fillRect( 0, this.mark33ms, this.graph.width, 1 ); 62 | 63 | this.addGraphMark( '16ms', this.mark16ms ); 64 | this.addGraphMark( '33ms', this.mark33ms ); 65 | 66 | this.addClock( 'draw', 'Draw', '#13baff' ); 67 | this.addClock( 'update', 'Entity Update', '#bb0fff' ); 68 | this.addClock( 'checks', 'Entity Checks & Collisions', '#a2e908' ); 69 | this.addClock( 'lag', 'System Lag', '#f26900' ); 70 | 71 | ig.mark = this.mark.bind(this); 72 | ig.graph = this; 73 | }, 74 | 75 | 76 | addGraphMark: function( name, height ) { 77 | var span = ig.$new('span'); 78 | span.className = 'ig_debug_graph_mark'; 79 | span.textContent = name; 80 | span.style.top = height.round() + 'px'; 81 | this.container.appendChild( span ); 82 | }, 83 | 84 | 85 | addClock: function( name, description, color ) { 86 | var mark = ig.$new('span'); 87 | mark.className = 'ig_debug_legend_color'; 88 | mark.style.backgroundColor = color; 89 | 90 | var number = ig.$new('span'); 91 | number.className = 'ig_debug_legend_number'; 92 | number.appendChild( document.createTextNode('0') ); 93 | 94 | var legend = ig.$new('span'); 95 | legend.className = 'ig_debug_legend'; 96 | legend.appendChild( mark ); 97 | legend.appendChild( document.createTextNode(description +' (') ); 98 | legend.appendChild( number ); 99 | legend.appendChild( document.createTextNode('ms)') ); 100 | 101 | this.container.appendChild( legend ); 102 | 103 | this.clocks[name] = { 104 | description: description, 105 | color: color, 106 | current: 0, 107 | start: Date.now(), 108 | avg: 0, 109 | html: number 110 | }; 111 | }, 112 | 113 | 114 | beginClock: function( name, offset ) { 115 | this.clocks[name].start = Date.now() + (offset || 0); 116 | }, 117 | 118 | 119 | endClock: function( name ) { 120 | var c = this.clocks[name]; 121 | c.current = Math.round(Date.now() - c.start); 122 | c.avg = c.avg * 0.8 + c.current * 0.2; 123 | }, 124 | 125 | 126 | mark: function( msg, color ) { 127 | if( this.active ) { 128 | this.marks.push( {msg:msg, color:(color||'#fff')} ); 129 | } 130 | }, 131 | 132 | 133 | beforeRun: function() { 134 | this.endClock('lag'); 135 | this.timeBeforeRun = Date.now(); 136 | }, 137 | 138 | 139 | afterRun: function() { 140 | var frameTime = Date.now() - this.timeBeforeRun; 141 | var nextFrameDue = (1000/ig.system.fps) - frameTime; 142 | this.beginClock('lag', Math.max(nextFrameDue, 0)); 143 | 144 | 145 | var x = this.graph.width-1; 146 | var y = this.height; 147 | 148 | this.ctx.drawImage( this.graph, -1, 0 ); 149 | 150 | this.ctx.fillStyle = '#000'; 151 | this.ctx.fillRect( x, 0, 1, this.height ); 152 | 153 | this.ctx.fillStyle = '#444'; 154 | this.ctx.fillRect( x, this.mark16ms, 1, 1 ); 155 | 156 | this.ctx.fillStyle = '#444'; 157 | this.ctx.fillRect( x, this.mark33ms, 1, 1 ); 158 | 159 | for( var ci in this.clocks ) { 160 | var c = this.clocks[ci]; 161 | c.html.textContent = c.avg.toFixed(2); 162 | 163 | if( c.color && c.current > 0 ) { 164 | this.ctx.fillStyle = c.color; 165 | var h = c.current * this.msHeight; 166 | y -= h; 167 | this.ctx.fillRect( x, y, 1, h ); 168 | c.current = 0; 169 | } 170 | } 171 | 172 | this.ctx.textAlign = 'right'; 173 | this.ctx.textBaseline = 'top'; 174 | this.ctx.globalAlpha = 0.5; 175 | 176 | for( var i = 0; i < this.marks.length; i++ ) { 177 | var m = this.marks[i]; 178 | this.ctx.fillStyle = m.color; 179 | this.ctx.fillRect( x, 0, 1, this.height ); 180 | if( m.msg ) { 181 | this.ctx.fillText( m.msg, x-1, this.textY ); 182 | this.textY = (this.textY+8)%32; 183 | } 184 | } 185 | this.ctx.globalAlpha = 1; 186 | this.marks = []; 187 | } 188 | }); 189 | 190 | 191 | ig.debug.addPanel({ 192 | type: ig.DebugGraphPanel, 193 | name: 'graph', 194 | label: 'Performance' 195 | }); 196 | 197 | 198 | }); -------------------------------------------------------------------------------- /lib/impact/debug/maps-panel.js: -------------------------------------------------------------------------------- 1 | ig.module( 2 | 'impact.debug.maps-panel' 3 | ) 4 | .requires( 5 | 'impact.debug.menu', 6 | 'impact.game', 7 | 'impact.background-map' 8 | ) 9 | .defines(function(){ "use strict"; 10 | 11 | 12 | ig.Game.inject({ 13 | loadLevel: function( data ) { 14 | this.parent(data); 15 | ig.debug.panels.maps.load(this); 16 | } 17 | }); 18 | 19 | 20 | ig.DebugMapsPanel = ig.DebugPanel.extend({ 21 | maps: [], 22 | mapScreens: [], 23 | 24 | 25 | init: function( name, label ) { 26 | this.parent( name, label ); 27 | this.load(); 28 | }, 29 | 30 | 31 | load: function( game ) { 32 | this.options = []; 33 | this.panels = []; 34 | 35 | if( !game || !game.backgroundMaps.length ) { 36 | this.container.innerHTML = 'No Maps Loaded'; 37 | return; 38 | } 39 | 40 | this.maps = game.backgroundMaps; 41 | this.mapScreens = []; 42 | this.container.innerHTML = ''; 43 | 44 | for( var m = 0; m < this.maps.length; m++ ) { 45 | var map = this.maps[m]; 46 | 47 | var subPanel = new ig.DebugPanel( m, 'Layer '+m ); 48 | 49 | var head = ig.$new('strong'); 50 | head.textContent = m +': ' + map.tiles.path; 51 | subPanel.container.appendChild( head ); 52 | 53 | subPanel.addOption( new ig.DebugOption('Enabled', map, 'enabled') ); 54 | subPanel.addOption( new ig.DebugOption('Pre Rendered', map, 'preRender') ); 55 | subPanel.addOption( new ig.DebugOption('Show Chunks', map, 'debugChunks') ); 56 | 57 | this.generateMiniMap( subPanel, map, m ); 58 | this.addPanel( subPanel ); 59 | } 60 | }, 61 | 62 | 63 | generateMiniMap: function( panel, map, id ) { 64 | var s = ig.system.scale; // we'll need this a lot 65 | 66 | // resize the tileset, so that one tile is 's' pixels wide and high 67 | var ts = ig.$new('canvas'); 68 | var tsctx = ts.getContext('2d'); 69 | 70 | var w = map.tiles.width * s; 71 | var h = map.tiles.height * s; 72 | var ws = w / map.tilesize; 73 | var hs = h / map.tilesize; 74 | ts.width = ws; 75 | ts.height = hs; 76 | tsctx.drawImage( map.tiles.data, 0, 0, w, h, 0, 0, ws, hs ); 77 | 78 | // create the minimap canvas 79 | var mapCanvas = ig.$new('canvas'); 80 | mapCanvas.width = map.width * s; 81 | mapCanvas.height = map.height * s; 82 | var ctx = mapCanvas.getContext('2d'); 83 | 84 | if( ig.game.clearColor ) { 85 | ctx.fillStyle = ig.game.clearColor; 86 | ctx.fillRect(0, 0, mapCanvas.width, mapCanvas.height); 87 | } 88 | 89 | // draw the map 90 | var tile = 0; 91 | for( var x = 0; x < map.width; x++ ) { 92 | for( var y = 0; y < map.height; y++ ) { 93 | if( (tile = map.data[y][x]) ) { 94 | ctx.drawImage( 95 | ts, 96 | Math.floor(((tile-1) * s) % ws), 97 | Math.floor((tile-1) * s / ws) * s, 98 | s, s, 99 | x * s, y * s, 100 | s, s 101 | ); 102 | } 103 | } 104 | } 105 | 106 | var mapContainer = ig.$new('div'); 107 | mapContainer.className = 'ig_debug_map_container'; 108 | mapContainer.style.width = map.width * s + 'px'; 109 | mapContainer.style.height = map.height * s + 'px'; 110 | 111 | var mapScreen = ig.$new('div'); 112 | mapScreen.className = 'ig_debug_map_screen'; 113 | mapScreen.style.width = ((ig.system.width / map.tilesize) * s - 2) + 'px'; 114 | mapScreen.style.height = ((ig.system.height / map.tilesize) * s - 2) + 'px'; 115 | this.mapScreens[id] = mapScreen; 116 | 117 | mapContainer.appendChild( mapCanvas ); 118 | mapContainer.appendChild( mapScreen ); 119 | panel.container.appendChild( mapContainer ); 120 | }, 121 | 122 | 123 | afterRun: function() { 124 | // Update the screen position DIV for each mini-map 125 | var s = ig.system.scale; 126 | for( var m = 0; m < this.maps.length; m++ ) { 127 | var map = this.maps[m]; 128 | var screen = this.mapScreens[m]; 129 | 130 | if( !map || !screen ) { // Quick sanity check 131 | continue; 132 | } 133 | 134 | var x = map.scroll.x / map.tilesize; 135 | var y = map.scroll.y / map.tilesize; 136 | 137 | if( map.repeat ) { 138 | x %= map.width; 139 | y %= map.height; 140 | } 141 | 142 | screen.style.left = (x * s) + 'px'; 143 | screen.style.top = (y * s) + 'px'; 144 | } 145 | } 146 | }); 147 | 148 | 149 | ig.debug.addPanel({ 150 | type: ig.DebugMapsPanel, 151 | name: 'maps', 152 | label: 'Background Maps' 153 | }); 154 | 155 | 156 | }); 157 | -------------------------------------------------------------------------------- /lib/impact/debug/menu.js: -------------------------------------------------------------------------------- 1 | ig.module( 2 | 'impact.debug.menu' 3 | ) 4 | .requires( 5 | 'dom.ready', 6 | 'impact.system' 7 | ) 8 | .defines(function(){ "use strict"; 9 | 10 | 11 | ig.System.inject({ 12 | run: function() { 13 | ig.debug.beforeRun(); 14 | this.parent(); 15 | ig.debug.afterRun(); 16 | }, 17 | 18 | setGameNow: function( gameClass ) { 19 | this.parent( gameClass ); 20 | ig.debug.ready(); 21 | } 22 | }); 23 | 24 | 25 | ig.Debug = ig.Class.extend({ 26 | options: {}, 27 | panels: {}, 28 | numbers: {}, 29 | container: null, 30 | panelMenu: null, 31 | numberContainer: null, 32 | activePanel: null, 33 | 34 | debugTime: 0, 35 | debugTickAvg: 0.016, 36 | debugRealTime: Date.now(), 37 | 38 | init: function() { 39 | // Inject the Stylesheet 40 | var style = ig.$new('link'); 41 | style.rel = 'stylesheet'; 42 | style.type = 'text/css'; 43 | style.href = ig.prefix + 'lib/impact/debug/debug.css'; 44 | ig.$('body')[0].appendChild( style ); 45 | 46 | // Create the Debug Container 47 | this.container = ig.$new('div'); 48 | this.container.className ='ig_debug'; 49 | ig.$('body')[0].appendChild( this.container ); 50 | 51 | // Create and add the Menu Container 52 | this.panelMenu = ig.$new('div'); 53 | this.panelMenu.innerHTML = '
Impact.Debug:
'; 54 | this.panelMenu.className ='ig_debug_panel_menu'; 55 | 56 | this.container.appendChild( this.panelMenu ); 57 | 58 | // Create and add the Stats Container 59 | this.numberContainer = ig.$new('div'); 60 | this.numberContainer.className ='ig_debug_stats'; 61 | this.panelMenu.appendChild( this.numberContainer ); 62 | 63 | // Set ig.log(), ig.assert() and ig.show() 64 | if( window.console && window.console.log && window.console.assert ) { 65 | // Can't use .bind() on native functions in IE9 :/ 66 | ig.log = console.log.bind ? console.log.bind(console) : console.log; 67 | ig.assert = console.assert.bind ? console.assert.bind(console) : console.assert; 68 | } 69 | ig.show = this.showNumber.bind(this); 70 | }, 71 | 72 | 73 | addNumber: function( name ) { 74 | var number = ig.$new('span'); 75 | this.numberContainer.appendChild( number ); 76 | this.numberContainer.appendChild( document.createTextNode(name) ); 77 | 78 | this.numbers[name] = number; 79 | }, 80 | 81 | 82 | showNumber: function( name, number ) { 83 | if( !this.numbers[name] ) { 84 | this.addNumber( name ); 85 | } 86 | this.numbers[name].textContent = number; 87 | }, 88 | 89 | 90 | addPanel: function( panelDef ) { 91 | // Create the panel and options 92 | var panel = new (panelDef.type)( panelDef.name, panelDef.label ); 93 | if( panelDef.options ) { 94 | for( var i = 0; i < panelDef.options.length; i++ ) { 95 | var opt = panelDef.options[i]; 96 | panel.addOption( new ig.DebugOption(opt.name, opt.object, opt.property) ); 97 | } 98 | } 99 | 100 | this.panels[ panel.name ] = panel; 101 | panel.container.style.display = 'none'; 102 | this.container.appendChild( panel.container ); 103 | 104 | 105 | // Create the menu item 106 | var menuItem = ig.$new('div'); 107 | menuItem.className = 'ig_debug_menu_item'; 108 | menuItem.textContent = panel.label; 109 | menuItem.addEventListener( 110 | 'click', 111 | (function(ev){ this.togglePanel(panel); }).bind(this), 112 | false 113 | ); 114 | panel.menuItem = menuItem; 115 | 116 | // Insert menu item in alphabetical order into the menu 117 | var inserted = false; 118 | for( var i = 1; i < this.panelMenu.childNodes.length; i++ ) { 119 | var cn = this.panelMenu.childNodes[i]; 120 | if( cn.textContent > panel.label ) { 121 | this.panelMenu.insertBefore( menuItem, cn ); 122 | inserted = true; 123 | break; 124 | } 125 | } 126 | if( !inserted ) { 127 | // Not inserted? Append at the end! 128 | this.panelMenu.appendChild( menuItem ); 129 | } 130 | }, 131 | 132 | 133 | showPanel: function( name ) { 134 | this.togglePanel( this.panels[name] ); 135 | }, 136 | 137 | 138 | togglePanel: function( panel ) { 139 | if( panel != this.activePanel && this.activePanel ) { 140 | this.activePanel.toggle( false ); 141 | this.activePanel.menuItem.className = 'ig_debug_menu_item'; 142 | this.activePanel = null; 143 | } 144 | 145 | var dsp = panel.container.style.display; 146 | var active = (dsp != 'block'); 147 | panel.toggle( active ); 148 | panel.menuItem.className = 'ig_debug_menu_item' + (active ? ' active' : ''); 149 | 150 | if( active ) { 151 | this.activePanel = panel; 152 | } 153 | }, 154 | 155 | 156 | ready: function() { 157 | for( var p in this.panels ) { 158 | this.panels[p].ready(); 159 | } 160 | }, 161 | 162 | 163 | beforeRun: function() { 164 | var timeBeforeRun = Date.now(); 165 | this.debugTickAvg = this.debugTickAvg * 0.8 + (timeBeforeRun - this.debugRealTime) * 0.2; 166 | this.debugRealTime = timeBeforeRun; 167 | 168 | if( this.activePanel ) { 169 | this.activePanel.beforeRun(); 170 | } 171 | }, 172 | 173 | 174 | afterRun: function() { 175 | var frameTime = Date.now() - this.debugRealTime; 176 | var nextFrameDue = (1000/ig.system.fps) - frameTime; 177 | 178 | this.debugTime = this.debugTime * 0.8 + frameTime * 0.2; 179 | 180 | 181 | if( this.activePanel ) { 182 | this.activePanel.afterRun(); 183 | } 184 | 185 | this.showNumber( 'ms', this.debugTime.toFixed(2) ); 186 | this.showNumber( 'fps', Math.round(1000/this.debugTickAvg) ); 187 | this.showNumber( 'draws', ig.Image.drawCount ); 188 | if( ig.game && ig.game.entities ) { 189 | this.showNumber( 'entities', ig.game.entities.length ); 190 | } 191 | ig.Image.drawCount = 0; 192 | } 193 | }); 194 | 195 | 196 | 197 | ig.DebugPanel = ig.Class.extend({ 198 | active: false, 199 | container: null, 200 | options: [], 201 | panels: [], 202 | label: '', 203 | name: '', 204 | 205 | 206 | init: function( name, label ) { 207 | this.name = name; 208 | this.label = label; 209 | this.container = ig.$new('div'); 210 | this.container.className = 'ig_debug_panel ' + this.name; 211 | }, 212 | 213 | 214 | toggle: function( active ) { 215 | this.active = active; 216 | this.container.style.display = active ? 'block' : 'none'; 217 | }, 218 | 219 | 220 | addPanel: function( panel ) { 221 | this.panels.push( panel ); 222 | this.container.appendChild( panel.container ); 223 | }, 224 | 225 | 226 | addOption: function( option ) { 227 | this.options.push( option ); 228 | this.container.appendChild( option.container ); 229 | }, 230 | 231 | 232 | ready: function(){}, 233 | beforeRun: function(){}, 234 | afterRun: function(){} 235 | }); 236 | 237 | 238 | 239 | ig.DebugOption = ig.Class.extend({ 240 | name: '', 241 | labelName: '', 242 | className: 'ig_debug_option', 243 | label: null, 244 | mark: null, 245 | container: null, 246 | active: false, 247 | 248 | colors: { 249 | enabled: '#fff', 250 | disabled: '#444' 251 | }, 252 | 253 | 254 | init: function( name, object, property ) { 255 | this.name = name; 256 | this.object = object; 257 | this.property = property; 258 | 259 | this.active = this.object[this.property]; 260 | 261 | this.container = ig.$new('div'); 262 | this.container.className = 'ig_debug_option'; 263 | 264 | this.label = ig.$new('span'); 265 | this.label.className = 'ig_debug_label'; 266 | this.label.textContent = this.name; 267 | 268 | this.mark = ig.$new('span'); 269 | this.mark.className = 'ig_debug_label_mark'; 270 | 271 | this.container.appendChild( this.mark ); 272 | this.container.appendChild( this.label ); 273 | this.container.addEventListener( 'click', this.click.bind(this), false ); 274 | 275 | this.setLabel(); 276 | }, 277 | 278 | 279 | setLabel: function() { 280 | this.mark.style.backgroundColor = this.active ? this.colors.enabled : this.colors.disabled; 281 | }, 282 | 283 | 284 | click: function( ev ) { 285 | this.active = !this.active; 286 | this.object[this.property] = this.active; 287 | this.setLabel(); 288 | 289 | ev.stopPropagation(); 290 | ev.preventDefault(); 291 | return false; 292 | } 293 | }); 294 | 295 | 296 | 297 | // Create the debug instance! 298 | ig.debug = new ig.Debug(); 299 | 300 | }); -------------------------------------------------------------------------------- /lib/impact/entity-pool.js: -------------------------------------------------------------------------------- 1 | ig.module( 2 | 'impact.entity-pool' 3 | ) 4 | .requires( 5 | 'impact.game' 6 | ) 7 | .defines(function(){ "use strict"; 8 | 9 | ig.EntityPool = { 10 | pools: {}, 11 | 12 | mixin: { 13 | staticInstantiate: function( x, y, settings ) { 14 | return ig.EntityPool.getFromPool( this.classId, x, y, settings ); 15 | }, 16 | 17 | erase: function() { 18 | ig.EntityPool.putInPool( this ); 19 | } 20 | }, 21 | 22 | enableFor: function( Class ) { 23 | Class.inject(this.mixin); 24 | }, 25 | 26 | getFromPool: function( classId, x, y, settings ) { 27 | var pool = this.pools[classId]; 28 | if( !pool || !pool.length ) { return null; } 29 | 30 | var instance = pool.pop(); 31 | instance.reset(x, y, settings); 32 | return instance; 33 | }, 34 | 35 | putInPool: function( instance ) { 36 | if( !this.pools[instance.classId] ) { 37 | this.pools[instance.classId] = [instance]; 38 | } 39 | else { 40 | this.pools[instance.classId].push(instance); 41 | } 42 | }, 43 | 44 | drainPool: function( classId ) { 45 | delete this.pools[classId]; 46 | }, 47 | 48 | drainAllPools: function() { 49 | this.pools = {}; 50 | } 51 | }; 52 | 53 | ig.Game.inject({ 54 | loadLevel: function( data ) { 55 | ig.EntityPool.drainAllPools(); 56 | this.parent(data); 57 | } 58 | }); 59 | 60 | }); 61 | -------------------------------------------------------------------------------- /lib/impact/entity.js: -------------------------------------------------------------------------------- 1 | ig.module( 2 | 'impact.entity' 3 | ) 4 | .requires( 5 | 'impact.animation', 6 | 'impact.impact' 7 | ) 8 | .defines(function(){ "use strict"; 9 | 10 | ig.Entity = ig.Class.extend({ 11 | id: 0, 12 | settings: {}, 13 | 14 | size: {x: 16, y:16}, 15 | offset: {x: 0, y: 0}, 16 | 17 | pos: {x: 0, y:0}, 18 | last: {x: 0, y:0}, 19 | vel: {x: 0, y: 0}, 20 | accel: {x: 0, y: 0}, 21 | friction: {x: 0, y: 0}, 22 | maxVel: {x: 100, y: 100}, 23 | zIndex: 0, 24 | gravityFactor: 1, 25 | standing: false, 26 | bounciness: 0, 27 | minBounceVelocity: 40, 28 | 29 | anims: {}, 30 | animSheet: null, 31 | currentAnim: null, 32 | health: 10, 33 | 34 | type: 0, // TYPE.NONE 35 | checkAgainst: 0, // TYPE.NONE 36 | collides: 0, // COLLIDES.NEVER 37 | 38 | _killed: false, 39 | 40 | slopeStanding: {min: (44).toRad(), max: (136).toRad() }, 41 | 42 | init: function( x, y, settings ) { 43 | this.id = ++ig.Entity._lastId; 44 | this.pos.x = this.last.x = x; 45 | this.pos.y = this.last.y = y; 46 | 47 | ig.merge( this, settings ); 48 | }, 49 | 50 | reset: function( x, y, settings ) { 51 | var proto = this.constructor.prototype; 52 | this.pos.x = x; 53 | this.pos.y = y; 54 | this.last.x = x; 55 | this.last.y = y; 56 | this.vel.x = proto.vel.x; 57 | this.vel.y = proto.vel.y; 58 | this.accel.x = proto.accel.x; 59 | this.accel.y = proto.accel.y; 60 | this.health = proto.health; 61 | this._killed = proto._killed; 62 | this.standing = proto.standing; 63 | 64 | this.type = proto.type; 65 | this.checkAgainst = proto.checkAgainst; 66 | this.collides = proto.collides; 67 | 68 | ig.merge( this, settings ); 69 | }, 70 | 71 | addAnim: function( name, frameTime, sequence, stop ) { 72 | if( !this.animSheet ) { 73 | throw( 'No animSheet to add the animation '+name+' to.' ); 74 | } 75 | var a = new ig.Animation( this.animSheet, frameTime, sequence, stop ); 76 | this.anims[name] = a; 77 | if( !this.currentAnim ) { 78 | this.currentAnim = a; 79 | } 80 | 81 | return a; 82 | }, 83 | 84 | update: function() { 85 | this.last.x = this.pos.x; 86 | this.last.y = this.pos.y; 87 | this.vel.y += ig.game.gravity * ig.system.tick * this.gravityFactor; 88 | 89 | this.vel.x = this.getNewVelocity( this.vel.x, this.accel.x, this.friction.x, this.maxVel.x ); 90 | this.vel.y = this.getNewVelocity( this.vel.y, this.accel.y, this.friction.y, this.maxVel.y ); 91 | 92 | // movement & collision 93 | var mx = this.vel.x * ig.system.tick; 94 | var my = this.vel.y * ig.system.tick; 95 | var res = ig.game.collisionMap.trace( 96 | this.pos.x, this.pos.y, mx, my, this.size.x, this.size.y 97 | ); 98 | this.handleMovementTrace( res ); 99 | 100 | if( this.currentAnim ) { 101 | this.currentAnim.update(); 102 | } 103 | }, 104 | 105 | 106 | getNewVelocity: function( vel, accel, friction, max ) { 107 | if( accel ) { 108 | return ( vel + accel * ig.system.tick ).limit( -max, max ); 109 | } 110 | else if( friction ) { 111 | var delta = friction * ig.system.tick; 112 | 113 | if( vel - delta > 0) { 114 | return vel - delta; 115 | } 116 | else if( vel + delta < 0 ) { 117 | return vel + delta; 118 | } 119 | else { 120 | return 0; 121 | } 122 | } 123 | return vel.limit( -max, max ); 124 | }, 125 | 126 | 127 | handleMovementTrace: function( res ) { 128 | this.standing = false; 129 | 130 | if( res.collision.y ) { 131 | if( this.bounciness > 0 && Math.abs(this.vel.y) > this.minBounceVelocity ) { 132 | this.vel.y *= -this.bounciness; 133 | } 134 | else { 135 | if( this.vel.y > 0 ) { 136 | this.standing = true; 137 | } 138 | this.vel.y = 0; 139 | } 140 | } 141 | if( res.collision.x ) { 142 | if( this.bounciness > 0 && Math.abs(this.vel.x) > this.minBounceVelocity ) { 143 | this.vel.x *= -this.bounciness; 144 | } 145 | else { 146 | this.vel.x = 0; 147 | } 148 | } 149 | if( res.collision.slope ) { 150 | var s = res.collision.slope; 151 | 152 | if( this.bounciness > 0 ) { 153 | var proj = this.vel.x * s.nx + this.vel.y * s.ny; 154 | 155 | this.vel.x = (this.vel.x - s.nx * proj * 2) * this.bounciness; 156 | this.vel.y = (this.vel.y - s.ny * proj * 2) * this.bounciness; 157 | } 158 | else { 159 | var lengthSquared = s.x * s.x + s.y * s.y; 160 | var dot = (this.vel.x * s.x + this.vel.y * s.y)/lengthSquared; 161 | 162 | this.vel.x = s.x * dot; 163 | this.vel.y = s.y * dot; 164 | 165 | var angle = Math.atan2( s.x, s.y ); 166 | if( angle > this.slopeStanding.min && angle < this.slopeStanding.max ) { 167 | this.standing = true; 168 | } 169 | } 170 | } 171 | 172 | this.pos = res.pos; 173 | }, 174 | 175 | 176 | draw: function() { 177 | if( this.currentAnim ) { 178 | this.currentAnim.draw( 179 | this.pos.x - this.offset.x - ig.game._rscreen.x, 180 | this.pos.y - this.offset.y - ig.game._rscreen.y 181 | ); 182 | } 183 | }, 184 | 185 | 186 | kill: function() { 187 | ig.game.removeEntity( this ); 188 | }, 189 | 190 | 191 | receiveDamage: function( amount, from ) { 192 | this.health -= amount; 193 | if( this.health <= 0 ) { 194 | this.kill(); 195 | } 196 | }, 197 | 198 | 199 | touches: function( other ) { 200 | return !( 201 | this.pos.x >= other.pos.x + other.size.x || 202 | this.pos.x + this.size.x <= other.pos.x || 203 | this.pos.y >= other.pos.y + other.size.y || 204 | this.pos.y + this.size.y <= other.pos.y 205 | ); 206 | }, 207 | 208 | 209 | distanceTo: function( other ) { 210 | var xd = (this.pos.x + this.size.x/2) - (other.pos.x + other.size.x/2); 211 | var yd = (this.pos.y + this.size.y/2) - (other.pos.y + other.size.y/2); 212 | return Math.sqrt( xd*xd + yd*yd ); 213 | }, 214 | 215 | 216 | angleTo: function( other ) { 217 | return Math.atan2( 218 | (other.pos.y + other.size.y/2) - (this.pos.y + this.size.y/2), 219 | (other.pos.x + other.size.x/2) - (this.pos.x + this.size.x/2) 220 | ); 221 | }, 222 | 223 | 224 | check: function( other ) {}, 225 | collideWith: function( other, axis ) {}, 226 | ready: function() {}, 227 | erase: function() {} 228 | }); 229 | 230 | 231 | // Last used entity id; incremented with each spawned entity 232 | 233 | ig.Entity._lastId = 0; 234 | 235 | 236 | // Collision Types - Determine if and how entities collide with each other 237 | 238 | // In ACTIVE vs. LITE or FIXED vs. ANY collisions, only the "weak" entity moves, 239 | // while the other one stays fixed. In ACTIVE vs. ACTIVE and ACTIVE vs. PASSIVE 240 | // collisions, both entities are moved. LITE or PASSIVE entities don't collide 241 | // with other LITE or PASSIVE entities at all. The behaiviour for FIXED vs. 242 | // FIXED collisions is undefined. 243 | 244 | ig.Entity.COLLIDES = { 245 | NEVER: 0, 246 | LITE: 1, 247 | PASSIVE: 2, 248 | ACTIVE: 4, 249 | FIXED: 8 250 | }; 251 | 252 | 253 | // Entity Types - used for checks 254 | 255 | ig.Entity.TYPE = { 256 | NONE: 0, 257 | A: 1, 258 | B: 2, 259 | BOTH: 3 260 | }; 261 | 262 | 263 | 264 | ig.Entity.checkPair = function( a, b ) { 265 | 266 | // Do these entities want checks? 267 | if( a.checkAgainst & b.type ) { 268 | a.check( b ); 269 | } 270 | 271 | if( b.checkAgainst & a.type ) { 272 | b.check( a ); 273 | } 274 | 275 | // If this pair allows collision, solve it! At least one entity must 276 | // collide ACTIVE or FIXED, while the other one must not collide NEVER. 277 | if( 278 | a.collides && b.collides && 279 | a.collides + b.collides > ig.Entity.COLLIDES.ACTIVE 280 | ) { 281 | ig.Entity.solveCollision( a, b ); 282 | } 283 | }; 284 | 285 | 286 | ig.Entity.solveCollision = function( a, b ) { 287 | 288 | // If one entity is FIXED, or the other entity is LITE, the weak 289 | // (FIXED/NON-LITE) entity won't move in collision response 290 | var weak = null; 291 | if( 292 | a.collides == ig.Entity.COLLIDES.LITE || 293 | b.collides == ig.Entity.COLLIDES.FIXED 294 | ) { 295 | weak = a; 296 | } 297 | else if( 298 | b.collides == ig.Entity.COLLIDES.LITE || 299 | a.collides == ig.Entity.COLLIDES.FIXED 300 | ) { 301 | weak = b; 302 | } 303 | 304 | 305 | // Did they already overlap on the X-axis in the last frame? If so, 306 | // this must be a vertical collision! 307 | if( 308 | a.last.x + a.size.x > b.last.x && 309 | a.last.x < b.last.x + b.size.x 310 | ) { 311 | // Which one is on top? 312 | if( a.last.y < b.last.y ) { 313 | ig.Entity.seperateOnYAxis( a, b, weak ); 314 | } 315 | else { 316 | ig.Entity.seperateOnYAxis( b, a, weak ); 317 | } 318 | a.collideWith( b, 'y' ); 319 | b.collideWith( a, 'y' ); 320 | } 321 | 322 | // Horizontal collision 323 | else if( 324 | a.last.y + a.size.y > b.last.y && 325 | a.last.y < b.last.y + b.size.y 326 | ){ 327 | // Which one is on the left? 328 | if( a.last.x < b.last.x ) { 329 | ig.Entity.seperateOnXAxis( a, b, weak ); 330 | } 331 | else { 332 | ig.Entity.seperateOnXAxis( b, a, weak ); 333 | } 334 | a.collideWith( b, 'x' ); 335 | b.collideWith( a, 'x' ); 336 | } 337 | }; 338 | 339 | 340 | // FIXME: This is a mess. Instead of doing all the movements here, the entities 341 | // should get notified of the collision (with all details) and resolve it 342 | // themselfs. 343 | 344 | ig.Entity.seperateOnXAxis = function( left, right, weak ) { 345 | var nudge = (left.pos.x + left.size.x - right.pos.x); 346 | 347 | // We have a weak entity, so just move this one 348 | if( weak ) { 349 | var strong = left === weak ? right : left; 350 | weak.vel.x = -weak.vel.x * weak.bounciness + strong.vel.x; 351 | 352 | var resWeak = ig.game.collisionMap.trace( 353 | weak.pos.x, weak.pos.y, weak == left ? -nudge : nudge, 0, weak.size.x, weak.size.y 354 | ); 355 | weak.pos.x = resWeak.pos.x; 356 | } 357 | 358 | // Normal collision - both move 359 | else { 360 | var v2 = (left.vel.x - right.vel.x)/2; 361 | left.vel.x = -v2; 362 | right.vel.x = v2; 363 | 364 | var resLeft = ig.game.collisionMap.trace( 365 | left.pos.x, left.pos.y, -nudge/2, 0, left.size.x, left.size.y 366 | ); 367 | left.pos.x = Math.floor(resLeft.pos.x); 368 | 369 | var resRight = ig.game.collisionMap.trace( 370 | right.pos.x, right.pos.y, nudge/2, 0, right.size.x, right.size.y 371 | ); 372 | right.pos.x = Math.ceil(resRight.pos.x); 373 | } 374 | }; 375 | 376 | 377 | ig.Entity.seperateOnYAxis = function( top, bottom, weak ) { 378 | var nudge = (top.pos.y + top.size.y - bottom.pos.y); 379 | 380 | // We have a weak entity, so just move this one 381 | if( weak ) { 382 | var strong = top === weak ? bottom : top; 383 | weak.vel.y = -weak.vel.y * weak.bounciness + strong.vel.y; 384 | 385 | // Riding on a platform? 386 | var nudgeX = 0; 387 | if( weak == top && Math.abs(weak.vel.y - strong.vel.y) < weak.minBounceVelocity ) { 388 | weak.standing = true; 389 | nudgeX = strong.vel.x * ig.system.tick; 390 | } 391 | 392 | var resWeak = ig.game.collisionMap.trace( 393 | weak.pos.x, weak.pos.y, nudgeX, weak == top ? -nudge : nudge, weak.size.x, weak.size.y 394 | ); 395 | weak.pos.y = resWeak.pos.y; 396 | weak.pos.x = resWeak.pos.x; 397 | } 398 | 399 | // Bottom entity is standing - just bounce the top one 400 | else if( ig.game.gravity && (bottom.standing || top.vel.y > 0) ) { 401 | var resTop = ig.game.collisionMap.trace( 402 | top.pos.x, top.pos.y, 0, -(top.pos.y + top.size.y - bottom.pos.y), top.size.x, top.size.y 403 | ); 404 | top.pos.y = resTop.pos.y; 405 | 406 | if( top.bounciness > 0 && top.vel.y > top.minBounceVelocity ) { 407 | top.vel.y *= -top.bounciness; 408 | } 409 | else { 410 | top.standing = true; 411 | top.vel.y = 0; 412 | } 413 | } 414 | 415 | // Normal collision - both move 416 | else { 417 | var v2 = (top.vel.y - bottom.vel.y)/2; 418 | top.vel.y = -v2; 419 | bottom.vel.y = v2; 420 | 421 | var nudgeX = bottom.vel.x * ig.system.tick; 422 | var resTop = ig.game.collisionMap.trace( 423 | top.pos.x, top.pos.y, nudgeX, -nudge/2, top.size.x, top.size.y 424 | ); 425 | top.pos.y = resTop.pos.y; 426 | 427 | var resBottom = ig.game.collisionMap.trace( 428 | bottom.pos.x, bottom.pos.y, 0, nudge/2, bottom.size.x, bottom.size.y 429 | ); 430 | bottom.pos.y = resBottom.pos.y; 431 | } 432 | }; 433 | 434 | }); -------------------------------------------------------------------------------- /lib/impact/font.js: -------------------------------------------------------------------------------- 1 | ig.module( 2 | 'impact.font' 3 | ) 4 | .requires( 5 | 'impact.image' 6 | ) 7 | .defines(function(){ "use strict"; 8 | 9 | 10 | ig.Font = ig.Image.extend({ 11 | widthMap: [], 12 | indices: [], 13 | firstChar: 32, 14 | alpha: 1, 15 | letterSpacing: 1, 16 | lineSpacing: 0, 17 | 18 | 19 | onload: function( ev ) { 20 | this._loadMetrics( this.data ); 21 | this.parent( ev ); 22 | this.height -= 2; // last 2 lines contain no visual data 23 | }, 24 | 25 | 26 | widthForString: function( text ) { 27 | // Multiline? 28 | if( text.indexOf('\n') !== -1 ) { 29 | var lines = text.split( '\n' ); 30 | var width = 0; 31 | for( var i = 0; i < lines.length; i++ ) { 32 | width = Math.max( width, this._widthForLine(lines[i]) ); 33 | } 34 | return width; 35 | } 36 | else { 37 | return this._widthForLine( text ); 38 | } 39 | }, 40 | 41 | 42 | _widthForLine: function( text ) { 43 | var width = 0; 44 | for( var i = 0; i < text.length; i++ ) { 45 | width += this.widthMap[text.charCodeAt(i) - this.firstChar]; 46 | } 47 | if( text.length > 0 ) { 48 | width += this.letterSpacing * (text.length - 1); 49 | } 50 | return width; 51 | }, 52 | 53 | 54 | heightForString: function( text ) { 55 | return text.split('\n').length * (this.height + this.lineSpacing); 56 | }, 57 | 58 | 59 | draw: function( text, x, y, align ) { 60 | if( typeof(text) != 'string' ) { 61 | text = text.toString(); 62 | } 63 | 64 | // Multiline? 65 | if( text.indexOf('\n') !== -1 ) { 66 | var lines = text.split( '\n' ); 67 | var lineHeight = this.height + this.lineSpacing; 68 | for( var i = 0; i < lines.length; i++ ) { 69 | this.draw( lines[i], x, y + i * lineHeight, align ); 70 | } 71 | return; 72 | } 73 | 74 | if( align == ig.Font.ALIGN.RIGHT || align == ig.Font.ALIGN.CENTER ) { 75 | var width = this._widthForLine( text ); 76 | x -= align == ig.Font.ALIGN.CENTER ? width/2 : width; 77 | } 78 | 79 | 80 | if( this.alpha !== 1 ) { 81 | ig.system.context.globalAlpha = this.alpha; 82 | } 83 | 84 | for( var i = 0; i < text.length; i++ ) { 85 | var c = text.charCodeAt(i); 86 | x += this._drawChar( c - this.firstChar, x, y ); 87 | } 88 | 89 | if( this.alpha !== 1 ) { 90 | ig.system.context.globalAlpha = 1; 91 | } 92 | ig.Image.drawCount += text.length; 93 | }, 94 | 95 | 96 | _drawChar: function( c, targetX, targetY ) { 97 | if( !this.loaded || c < 0 || c >= this.indices.length ) { return 0; } 98 | 99 | var scale = ig.system.scale; 100 | 101 | 102 | var charX = this.indices[c] * scale; 103 | var charY = 0; 104 | var charWidth = this.widthMap[c] * scale; 105 | var charHeight = this.height * scale; 106 | 107 | ig.system.context.drawImage( 108 | this.data, 109 | charX, charY, 110 | charWidth, charHeight, 111 | ig.system.getDrawPos(targetX), ig.system.getDrawPos(targetY), 112 | charWidth, charHeight 113 | ); 114 | 115 | return this.widthMap[c] + this.letterSpacing; 116 | }, 117 | 118 | 119 | _loadMetrics: function( image ) { 120 | // Draw the bottommost line of this font image into an offscreen canvas 121 | // and analyze it pixel by pixel. 122 | // A run of non-transparent pixels represents a character and its width 123 | 124 | this.widthMap = []; 125 | this.indices = []; 126 | 127 | var px = ig.getImagePixels( image, 0, image.height-1, image.width, 1 ); 128 | 129 | var currentWidth = 0; 130 | for( var x = 0; x < image.width; x++ ) { 131 | var index = x * 4 + 3; // alpha component of this pixel 132 | if( px.data[index] > 127 ) { 133 | currentWidth++; 134 | } 135 | else if( px.data[index] < 128 && currentWidth ) { 136 | this.widthMap.push( currentWidth ); 137 | this.indices.push( x-currentWidth ); 138 | currentWidth = 0; 139 | } 140 | } 141 | this.widthMap.push( currentWidth ); 142 | this.indices.push( x-currentWidth ); 143 | } 144 | }); 145 | 146 | 147 | ig.Font.ALIGN = { 148 | LEFT: 0, 149 | RIGHT: 1, 150 | CENTER: 2 151 | }; 152 | 153 | }); 154 | -------------------------------------------------------------------------------- /lib/impact/game.js: -------------------------------------------------------------------------------- 1 | ig.module( 2 | 'impact.game' 3 | ) 4 | .requires( 5 | 'impact.impact', 6 | 'impact.entity', 7 | 'impact.collision-map', 8 | 'impact.background-map' 9 | ) 10 | .defines(function(){ "use strict"; 11 | 12 | ig.Game = ig.Class.extend({ 13 | 14 | clearColor: '#000000', 15 | gravity: 0, 16 | screen: {x: 0, y: 0}, 17 | _rscreen: {x: 0, y: 0}, 18 | 19 | entities: [], 20 | 21 | namedEntities: {}, 22 | collisionMap: ig.CollisionMap.staticNoCollision, 23 | backgroundMaps: [], 24 | backgroundAnims: {}, 25 | 26 | autoSort: false, 27 | sortBy: null, 28 | 29 | cellSize: 64, 30 | 31 | _deferredKill: [], 32 | _levelToLoad: null, 33 | _doSortEntities: false, 34 | 35 | 36 | staticInstantiate: function() { 37 | this.sortBy = this.sortBy || ig.Game.SORT.Z_INDEX; 38 | ig.game = this; 39 | return null; 40 | }, 41 | 42 | 43 | loadLevel: function( data ) { 44 | this.screen = {x: 0, y: 0}; 45 | 46 | // Entities 47 | this.entities = []; 48 | this.namedEntities = {}; 49 | for( var i = 0; i < data.entities.length; i++ ) { 50 | var ent = data.entities[i]; 51 | this.spawnEntity( ent.type, ent.x, ent.y, ent.settings ); 52 | } 53 | this.sortEntities(); 54 | 55 | // Map Layer 56 | this.collisionMap = ig.CollisionMap.staticNoCollision; 57 | this.backgroundMaps = []; 58 | for( var i = 0; i < data.layer.length; i++ ) { 59 | var ld = data.layer[i]; 60 | if( ld.name == 'collision' ) { 61 | this.collisionMap = new ig.CollisionMap(ld.tilesize, ld.data ); 62 | } 63 | else { 64 | var newMap = new ig.BackgroundMap(ld.tilesize, ld.data, ld.tilesetName); 65 | newMap.anims = this.backgroundAnims[ld.tilesetName] || {}; 66 | newMap.repeat = ld.repeat; 67 | newMap.distance = ld.distance; 68 | newMap.foreground = !!ld.foreground; 69 | newMap.preRender = !!ld.preRender; 70 | newMap.name = ld.name; 71 | this.backgroundMaps.push( newMap ); 72 | } 73 | } 74 | 75 | // Call post-init ready function on all entities 76 | for( var i = 0; i < this.entities.length; i++ ) { 77 | this.entities[i].ready(); 78 | } 79 | }, 80 | 81 | 82 | loadLevelDeferred: function( data ) { 83 | this._levelToLoad = data; 84 | }, 85 | 86 | 87 | getMapByName: function( name ) { 88 | if( name == 'collision' ) { 89 | return this.collisionMap; 90 | } 91 | 92 | for( var i = 0; i < this.backgroundMaps.length; i++ ) { 93 | if( this.backgroundMaps[i].name == name ) { 94 | return this.backgroundMaps[i]; 95 | } 96 | } 97 | 98 | return null; 99 | }, 100 | 101 | 102 | getEntityByName: function( name ) { 103 | return this.namedEntities[name]; 104 | }, 105 | 106 | 107 | getEntitiesByType: function( type ) { 108 | var entityClass = typeof(type) === 'string' 109 | ? ig.global[type] 110 | : type; 111 | 112 | var a = []; 113 | for( var i = 0; i < this.entities.length; i++ ) { 114 | var ent = this.entities[i]; 115 | if( ent instanceof entityClass && !ent._killed ) { 116 | a.push( ent ); 117 | } 118 | } 119 | return a; 120 | }, 121 | 122 | 123 | spawnEntity: function( type, x, y, settings ) { 124 | var entityClass = typeof(type) === 'string' 125 | ? ig.global[type] 126 | : type; 127 | 128 | if( !entityClass ) { 129 | throw("Can't spawn entity of type " + type); 130 | } 131 | var ent = new (entityClass)( x, y, settings || {} ); 132 | this.entities.push( ent ); 133 | if( ent.name ) { 134 | this.namedEntities[ent.name] = ent; 135 | } 136 | return ent; 137 | }, 138 | 139 | 140 | sortEntities: function() { 141 | this.entities.sort( this.sortBy ); 142 | }, 143 | 144 | 145 | sortEntitiesDeferred: function() { 146 | this._doSortEntities = true; 147 | }, 148 | 149 | 150 | removeEntity: function( ent ) { 151 | // Remove this entity from the named entities 152 | if( ent.name ) { 153 | delete this.namedEntities[ent.name]; 154 | } 155 | 156 | // We can not remove the entity from the entities[] array in the midst 157 | // of an update cycle, so remember all killed entities and remove 158 | // them later. 159 | // Also make sure this entity doesn't collide anymore and won't get 160 | // updated or checked 161 | ent._killed = true; 162 | ent.type = ig.Entity.TYPE.NONE; 163 | ent.checkAgainst = ig.Entity.TYPE.NONE; 164 | ent.collides = ig.Entity.COLLIDES.NEVER; 165 | this._deferredKill.push( ent ); 166 | }, 167 | 168 | 169 | run: function() { 170 | this.update(); 171 | this.draw(); 172 | }, 173 | 174 | 175 | update: function(){ 176 | // load new level? 177 | if( this._levelToLoad ) { 178 | this.loadLevel( this._levelToLoad ); 179 | this._levelToLoad = null; 180 | } 181 | 182 | // update entities 183 | this.updateEntities(); 184 | this.checkEntities(); 185 | 186 | // remove all killed entities 187 | for( var i = 0; i < this._deferredKill.length; i++ ) { 188 | this._deferredKill[i].erase(); 189 | this.entities.erase( this._deferredKill[i] ); 190 | } 191 | this._deferredKill = []; 192 | 193 | // sort entities? 194 | if( this._doSortEntities || this.autoSort ) { 195 | this.sortEntities(); 196 | this._doSortEntities = false; 197 | } 198 | 199 | // update background animations 200 | for( var tileset in this.backgroundAnims ) { 201 | var anims = this.backgroundAnims[tileset]; 202 | for( var a in anims ) { 203 | anims[a].update(); 204 | } 205 | } 206 | }, 207 | 208 | 209 | updateEntities: function() { 210 | for( var i = 0; i < this.entities.length; i++ ) { 211 | var ent = this.entities[i]; 212 | if( !ent._killed ) { 213 | ent.update(); 214 | } 215 | } 216 | }, 217 | 218 | 219 | draw: function(){ 220 | if( this.clearColor ) { 221 | ig.system.clear( this.clearColor ); 222 | } 223 | 224 | // This is a bit of a circle jerk. Entities reference game._rscreen 225 | // instead of game.screen when drawing themselfs in order to be 226 | // "synchronized" to the rounded(?) screen position 227 | this._rscreen.x = ig.system.getDrawPos(this.screen.x)/ig.system.scale; 228 | this._rscreen.y = ig.system.getDrawPos(this.screen.y)/ig.system.scale; 229 | 230 | 231 | var mapIndex; 232 | for( mapIndex = 0; mapIndex < this.backgroundMaps.length; mapIndex++ ) { 233 | var map = this.backgroundMaps[mapIndex]; 234 | if( map.foreground ) { 235 | // All foreground layers are drawn after the entities 236 | break; 237 | } 238 | map.setScreenPos( this.screen.x, this.screen.y ); 239 | map.draw(); 240 | } 241 | 242 | 243 | this.drawEntities(); 244 | 245 | 246 | for( mapIndex; mapIndex < this.backgroundMaps.length; mapIndex++ ) { 247 | var map = this.backgroundMaps[mapIndex]; 248 | map.setScreenPos( this.screen.x, this.screen.y ); 249 | map.draw(); 250 | } 251 | }, 252 | 253 | 254 | drawEntities: function() { 255 | for( var i = 0; i < this.entities.length; i++ ) { 256 | this.entities[i].draw(); 257 | } 258 | }, 259 | 260 | 261 | checkEntities: function() { 262 | // Insert all entities into a spatial hash and check them against any 263 | // other entity that already resides in the same cell. Entities that are 264 | // bigger than a single cell, are inserted into each one they intersect 265 | // with. 266 | 267 | // A list of entities, which the current one was already checked with, 268 | // is maintained for each entity. 269 | 270 | var hash = {}; 271 | for( var e = 0; e < this.entities.length; e++ ) { 272 | var entity = this.entities[e]; 273 | 274 | // Skip entities that don't check, don't get checked and don't collide 275 | if( 276 | entity.type == ig.Entity.TYPE.NONE && 277 | entity.checkAgainst == ig.Entity.TYPE.NONE && 278 | entity.collides == ig.Entity.COLLIDES.NEVER 279 | ) { 280 | continue; 281 | } 282 | 283 | var checked = {}, 284 | xmin = Math.floor( entity.pos.x/this.cellSize ), 285 | ymin = Math.floor( entity.pos.y/this.cellSize ), 286 | xmax = Math.floor( (entity.pos.x+entity.size.x)/this.cellSize ) + 1, 287 | ymax = Math.floor( (entity.pos.y+entity.size.y)/this.cellSize ) + 1; 288 | 289 | for( var x = xmin; x < xmax; x++ ) { 290 | for( var y = ymin; y < ymax; y++ ) { 291 | 292 | // Current cell is empty - create it and insert! 293 | if( !hash[x] ) { 294 | hash[x] = {}; 295 | hash[x][y] = [entity]; 296 | } 297 | else if( !hash[x][y] ) { 298 | hash[x][y] = [entity]; 299 | } 300 | 301 | // Check against each entity in this cell, then insert 302 | else { 303 | var cell = hash[x][y]; 304 | for( var c = 0; c < cell.length; c++ ) { 305 | 306 | // Intersects and wasn't already checkd? 307 | if( entity.touches(cell[c]) && !checked[cell[c].id] ) { 308 | checked[cell[c].id] = true; 309 | ig.Entity.checkPair( entity, cell[c] ); 310 | } 311 | } 312 | cell.push(entity); 313 | } 314 | } // end for y size 315 | } // end for x size 316 | } // end for entities 317 | } 318 | }); 319 | 320 | ig.Game.SORT = { 321 | Z_INDEX: function( a, b ){ return a.zIndex - b.zIndex; }, 322 | POS_X: function( a, b ){ return (a.pos.x+a.size.x) - (b.pos.x+b.size.x); }, 323 | POS_Y: function( a, b ){ return (a.pos.y+a.size.y) - (b.pos.y+b.size.y); } 324 | }; 325 | 326 | }); -------------------------------------------------------------------------------- /lib/impact/image.js: -------------------------------------------------------------------------------- 1 | ig.module( 2 | 'impact.image' 3 | ) 4 | .defines(function(){ "use strict"; 5 | 6 | ig.Image = ig.Class.extend({ 7 | data: null, 8 | width: 0, 9 | height: 0, 10 | loaded: false, 11 | failed: false, 12 | loadCallback: null, 13 | path: '', 14 | 15 | 16 | staticInstantiate: function( path ) { 17 | return ig.Image.cache[path] || null; 18 | }, 19 | 20 | 21 | init: function( path ) { 22 | this.path = path; 23 | this.load(); 24 | }, 25 | 26 | 27 | load: function( loadCallback ) { 28 | if( this.loaded ) { 29 | if( loadCallback ) { 30 | loadCallback( this.path, true ); 31 | } 32 | return; 33 | } 34 | else if( !this.loaded && ig.ready ) { 35 | this.loadCallback = loadCallback || null; 36 | 37 | this.data = new Image(); 38 | this.data.onload = this.onload.bind(this); 39 | this.data.onerror = this.onerror.bind(this); 40 | this.data.src = ig.prefix + this.path + ig.nocache; 41 | } 42 | else { 43 | ig.addResource( this ); 44 | } 45 | 46 | ig.Image.cache[this.path] = this; 47 | }, 48 | 49 | 50 | reload: function() { 51 | this.loaded = false; 52 | this.data = new Image(); 53 | this.data.onload = this.onload.bind(this); 54 | this.data.src = this.path + '?' + Date.now(); 55 | }, 56 | 57 | 58 | onload: function( event ) { 59 | this.width = this.data.width; 60 | this.height = this.data.height; 61 | this.loaded = true; 62 | 63 | if( ig.system.scale != 1 ) { 64 | this.resize( ig.system.scale ); 65 | } 66 | 67 | if( this.loadCallback ) { 68 | this.loadCallback( this.path, true ); 69 | } 70 | }, 71 | 72 | 73 | onerror: function( event ) { 74 | this.failed = true; 75 | 76 | if( this.loadCallback ) { 77 | this.loadCallback( this.path, false ); 78 | } 79 | }, 80 | 81 | 82 | resize: function( scale ) { 83 | // Nearest-Neighbor scaling 84 | 85 | // The original image is drawn into an offscreen canvas of the same size 86 | // and copied into another offscreen canvas with the new size. 87 | // The scaled offscreen canvas becomes the image (data) of this object. 88 | 89 | var origPixels = ig.getImagePixels( this.data, 0, 0, this.width, this.height ); 90 | 91 | var widthScaled = this.width * scale; 92 | var heightScaled = this.height * scale; 93 | 94 | var scaled = ig.$new('canvas'); 95 | scaled.width = widthScaled; 96 | scaled.height = heightScaled; 97 | var scaledCtx = scaled.getContext('2d'); 98 | var scaledPixels = scaledCtx.getImageData( 0, 0, widthScaled, heightScaled ); 99 | 100 | for( var y = 0; y < heightScaled; y++ ) { 101 | for( var x = 0; x < widthScaled; x++ ) { 102 | var index = (Math.floor(y / scale) * this.width + Math.floor(x / scale)) * 4; 103 | var indexScaled = (y * widthScaled + x) * 4; 104 | scaledPixels.data[ indexScaled ] = origPixels.data[ index ]; 105 | scaledPixels.data[ indexScaled+1 ] = origPixels.data[ index+1 ]; 106 | scaledPixels.data[ indexScaled+2 ] = origPixels.data[ index+2 ]; 107 | scaledPixels.data[ indexScaled+3 ] = origPixels.data[ index+3 ]; 108 | } 109 | } 110 | scaledCtx.putImageData( scaledPixels, 0, 0 ); 111 | this.data = scaled; 112 | }, 113 | 114 | 115 | draw: function( targetX, targetY, sourceX, sourceY, width, height ) { 116 | if( !this.loaded ) { return; } 117 | 118 | var scale = ig.system.scale; 119 | sourceX = sourceX ? sourceX * scale : 0; 120 | sourceY = sourceY ? sourceY * scale : 0; 121 | width = (width ? width : this.width) * scale; 122 | height = (height ? height : this.height) * scale; 123 | 124 | ig.system.context.drawImage( 125 | this.data, sourceX, sourceY, width, height, 126 | ig.system.getDrawPos(targetX), 127 | ig.system.getDrawPos(targetY), 128 | width, height 129 | ); 130 | 131 | ig.Image.drawCount++; 132 | }, 133 | 134 | 135 | drawTile: function( targetX, targetY, tile, tileWidth, tileHeight, flipX, flipY ) { 136 | tileHeight = tileHeight ? tileHeight : tileWidth; 137 | 138 | if( !this.loaded || tileWidth > this.width || tileHeight > this.height ) { return; } 139 | 140 | var scale = ig.system.scale; 141 | var tileWidthScaled = Math.floor(tileWidth * scale); 142 | var tileHeightScaled = Math.floor(tileHeight * scale); 143 | 144 | var scaleX = flipX ? -1 : 1; 145 | var scaleY = flipY ? -1 : 1; 146 | 147 | if( flipX || flipY ) { 148 | ig.system.context.save(); 149 | ig.system.context.scale( scaleX, scaleY ); 150 | } 151 | ig.system.context.drawImage( 152 | this.data, 153 | ( Math.floor(tile * tileWidth) % this.width ) * scale, 154 | ( Math.floor(tile * tileWidth / this.width) * tileHeight ) * scale, 155 | tileWidthScaled, 156 | tileHeightScaled, 157 | ig.system.getDrawPos(targetX) * scaleX - (flipX ? tileWidthScaled : 0), 158 | ig.system.getDrawPos(targetY) * scaleY - (flipY ? tileHeightScaled : 0), 159 | tileWidthScaled, 160 | tileHeightScaled 161 | ); 162 | if( flipX || flipY ) { 163 | ig.system.context.restore(); 164 | } 165 | 166 | ig.Image.drawCount++; 167 | } 168 | }); 169 | 170 | ig.Image.drawCount = 0; 171 | ig.Image.cache = {}; 172 | ig.Image.reloadCache = function() { 173 | for( var path in ig.Image.cache ) { 174 | ig.Image.cache[path].reload(); 175 | } 176 | }; 177 | 178 | }); -------------------------------------------------------------------------------- /lib/impact/impact.js: -------------------------------------------------------------------------------- 1 | 2 | // ----------------------------------------------------------------------------- 3 | // Impact Game Engine 1.24 4 | // http://impactjs.com/ 5 | // ----------------------------------------------------------------------------- 6 | 7 | 8 | (function(window){ "use strict"; 9 | 10 | // ----------------------------------------------------------------------------- 11 | // Native Object extensions 12 | 13 | Number.prototype.map = function(istart, istop, ostart, ostop) { 14 | return ostart + (ostop - ostart) * ((this - istart) / (istop - istart)); 15 | }; 16 | 17 | Number.prototype.limit = function(min, max) { 18 | return Math.min(max, Math.max(min, this)); 19 | }; 20 | 21 | Number.prototype.round = function(precision) { 22 | precision = Math.pow(10, precision || 0); 23 | return Math.round(this * precision) / precision; 24 | }; 25 | 26 | Number.prototype.floor = function() { 27 | return Math.floor(this); 28 | }; 29 | 30 | Number.prototype.ceil = function() { 31 | return Math.ceil(this); 32 | }; 33 | 34 | Number.prototype.toInt = function() { 35 | return (this | 0); 36 | }; 37 | 38 | Number.prototype.toRad = function() { 39 | return (this / 180) * Math.PI; 40 | }; 41 | 42 | Number.prototype.toDeg = function() { 43 | return (this * 180) / Math.PI; 44 | }; 45 | 46 | Object.defineProperty(Array.prototype, 'erase', {value: function(item) { 47 | for( var i = this.length; i--; ) { 48 | if( this[i] === item ) { 49 | this.splice(i, 1); 50 | } 51 | } 52 | return this; 53 | }}); 54 | 55 | Object.defineProperty(Array.prototype, 'random', {value: function(item) { 56 | return this[ Math.floor(Math.random() * this.length) ]; 57 | }}); 58 | 59 | Function.prototype.bind = Function.prototype.bind || function (oThis) { 60 | if( typeof this !== "function" ) { 61 | throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable"); 62 | } 63 | 64 | var aArgs = Array.prototype.slice.call(arguments, 1), 65 | fToBind = this, 66 | fNOP = function () {}, 67 | fBound = function () { 68 | return fToBind.apply( 69 | (this instanceof fNOP && oThis ? this : oThis), 70 | aArgs.concat(Array.prototype.slice.call(arguments)) 71 | ); 72 | }; 73 | 74 | fNOP.prototype = this.prototype; 75 | fBound.prototype = new fNOP(); 76 | 77 | return fBound; 78 | }; 79 | 80 | 81 | // ----------------------------------------------------------------------------- 82 | // ig Namespace 83 | 84 | window.ig = { 85 | game: null, 86 | debug: null, 87 | version: '1.24', 88 | global: window, 89 | modules: {}, 90 | resources: [], 91 | ready: false, 92 | baked: false, 93 | nocache: '', 94 | ua: {}, 95 | prefix: (window.ImpactPrefix || ''), 96 | lib: 'lib/', 97 | 98 | _current: null, 99 | _loadQueue: [], 100 | _waitForOnload: 0, 101 | 102 | 103 | $: function( selector ) { 104 | return selector.charAt(0) == '#' 105 | ? document.getElementById( selector.substr(1) ) 106 | : document.getElementsByTagName( selector ); 107 | }, 108 | 109 | 110 | $new: function( name ) { 111 | return document.createElement( name ); 112 | }, 113 | 114 | 115 | copy: function( object ) { 116 | if( 117 | !object || typeof(object) != 'object' || 118 | object instanceof HTMLElement || 119 | object instanceof ig.Class 120 | ) { 121 | return object; 122 | } 123 | else if( object instanceof Array ) { 124 | var c = []; 125 | for( var i = 0, l = object.length; i < l; i++) { 126 | c[i] = ig.copy(object[i]); 127 | } 128 | return c; 129 | } 130 | else { 131 | var c = {}; 132 | for( var i in object ) { 133 | c[i] = ig.copy(object[i]); 134 | } 135 | return c; 136 | } 137 | }, 138 | 139 | 140 | merge: function( original, extended ) { 141 | for( var key in extended ) { 142 | var ext = extended[key]; 143 | if( 144 | typeof(ext) != 'object' || 145 | ext instanceof HTMLElement || 146 | ext instanceof ig.Class || 147 | ext === null 148 | ) { 149 | original[key] = ext; 150 | } 151 | else { 152 | if( !original[key] || typeof(original[key]) != 'object' ) { 153 | original[key] = (ext instanceof Array) ? [] : {}; 154 | } 155 | ig.merge( original[key], ext ); 156 | } 157 | } 158 | return original; 159 | }, 160 | 161 | 162 | ksort: function( obj ) { 163 | if( !obj || typeof(obj) != 'object' ) { 164 | return []; 165 | } 166 | 167 | var keys = [], values = []; 168 | for( var i in obj ) { 169 | keys.push(i); 170 | } 171 | 172 | keys.sort(); 173 | for( var i = 0; i < keys.length; i++ ) { 174 | values.push( obj[keys[i]] ); 175 | } 176 | 177 | return values; 178 | }, 179 | 180 | // Ah, yes. I love vendor prefixes. So much fun! 181 | setVendorAttribute: function( el, attr, val ) { 182 | var uc = attr.charAt(0).toUpperCase() + attr.substr(1); 183 | el[attr] = el['ms'+uc] = el['moz'+uc] = el['webkit'+uc] = el['o'+uc] = val; 184 | }, 185 | 186 | 187 | getVendorAttribute: function( el, attr ) { 188 | var uc = attr.charAt(0).toUpperCase() + attr.substr(1); 189 | return el[attr] || el['ms'+uc] || el['moz'+uc] || el['webkit'+uc] || el['o'+uc]; 190 | }, 191 | 192 | 193 | normalizeVendorAttribute: function( el, attr ) { 194 | var prefixedVal = ig.getVendorAttribute( el, attr ); 195 | if( !el[attr] && prefixedVal ) { 196 | el[attr] = prefixedVal; 197 | } 198 | }, 199 | 200 | 201 | // This function normalizes getImageData to extract the real, actual 202 | // pixels from an image. The naive method recently failed on retina 203 | // devices with a backgingStoreRatio != 1 204 | getImagePixels: function( image, x, y, width, height ) { 205 | var canvas = ig.$new('canvas'); 206 | canvas.width = image.width; 207 | canvas.height = image.height; 208 | var ctx = canvas.getContext('2d'); 209 | 210 | // Try to draw pixels as accurately as possible 211 | ig.System.SCALE.CRISP(canvas, ctx); 212 | 213 | var ratio = ig.getVendorAttribute( ctx, 'backingStorePixelRatio' ) || 1; 214 | ig.normalizeVendorAttribute( ctx, 'getImageDataHD' ); 215 | 216 | var realWidth = image.width / ratio, 217 | realHeight = image.height / ratio; 218 | 219 | canvas.width = Math.ceil( realWidth ); 220 | canvas.height = Math.ceil( realHeight ); 221 | 222 | ctx.drawImage( image, 0, 0, realWidth, realHeight ); 223 | 224 | return (ratio === 1) 225 | ? ctx.getImageData( x, y, width, height ) 226 | : ctx.getImageDataHD( x, y, width, height ); 227 | }, 228 | 229 | 230 | module: function( name ) { 231 | if( ig._current ) { 232 | throw( "Module '"+ig._current.name+"' defines nothing" ); 233 | } 234 | if( ig.modules[name] && ig.modules[name].body ) { 235 | throw( "Module '"+name+"' is already defined" ); 236 | } 237 | 238 | ig._current = {name: name, requires: [], loaded: false, body: null}; 239 | ig.modules[name] = ig._current; 240 | ig._loadQueue.push(ig._current); 241 | return ig; 242 | }, 243 | 244 | 245 | requires: function() { 246 | ig._current.requires = Array.prototype.slice.call(arguments); 247 | return ig; 248 | }, 249 | 250 | 251 | defines: function( body ) { 252 | ig._current.body = body; 253 | ig._current = null; 254 | ig._initDOMReady(); 255 | }, 256 | 257 | 258 | addResource: function( resource ) { 259 | ig.resources.push( resource ); 260 | }, 261 | 262 | 263 | setNocache: function( set ) { 264 | ig.nocache = set 265 | ? '?' + Date.now() 266 | : ''; 267 | }, 268 | 269 | 270 | // Stubs for ig.Debug 271 | log: function() {}, 272 | assert: function( condition, msg ) {}, 273 | show: function( name, number ) {}, 274 | mark: function( msg, color ) {}, 275 | 276 | 277 | _loadScript: function( name, requiredFrom ) { 278 | ig.modules[name] = {name: name, requires:[], loaded: false, body: null}; 279 | ig._waitForOnload++; 280 | 281 | var path = ig.prefix + ig.lib + name.replace(/\./g, '/') + '.js' + ig.nocache; 282 | var script = ig.$new('script'); 283 | script.type = 'text/javascript'; 284 | script.src = path; 285 | script.onload = function() { 286 | ig._waitForOnload--; 287 | ig._execModules(); 288 | }; 289 | script.onerror = function() { 290 | throw( 291 | 'Failed to load module '+name+' at ' + path + ' ' + 292 | 'required from ' + requiredFrom 293 | ); 294 | }; 295 | ig.$('head')[0].appendChild(script); 296 | }, 297 | 298 | 299 | _execModules: function() { 300 | var modulesLoaded = false; 301 | for( var i = 0; i < ig._loadQueue.length; i++ ) { 302 | var m = ig._loadQueue[i]; 303 | var dependenciesLoaded = true; 304 | 305 | for( var j = 0; j < m.requires.length; j++ ) { 306 | var name = m.requires[j]; 307 | if( !ig.modules[name] ) { 308 | dependenciesLoaded = false; 309 | ig._loadScript( name, m.name ); 310 | } 311 | else if( !ig.modules[name].loaded ) { 312 | dependenciesLoaded = false; 313 | } 314 | } 315 | 316 | if( dependenciesLoaded && m.body ) { 317 | ig._loadQueue.splice(i, 1); 318 | m.loaded = true; 319 | m.body(); 320 | modulesLoaded = true; 321 | i--; 322 | } 323 | } 324 | 325 | if( modulesLoaded ) { 326 | ig._execModules(); 327 | } 328 | 329 | // No modules executed, no more files to load but loadQueue not empty? 330 | // Must be some unresolved dependencies! 331 | else if( !ig.baked && ig._waitForOnload == 0 && ig._loadQueue.length != 0 ) { 332 | var unresolved = []; 333 | for( var i = 0; i < ig._loadQueue.length; i++ ) { 334 | 335 | // Which dependencies aren't loaded? 336 | var unloaded = []; 337 | var requires = ig._loadQueue[i].requires; 338 | for( var j = 0; j < requires.length; j++ ) { 339 | var m = ig.modules[ requires[j] ]; 340 | if( !m || !m.loaded ) { 341 | unloaded.push( requires[j] ); 342 | } 343 | } 344 | unresolved.push( ig._loadQueue[i].name + ' (requires: ' + unloaded.join(', ') + ')'); 345 | } 346 | 347 | throw( 348 | "Unresolved (or circular?) dependencies. " + 349 | "Most likely there's a name/path mismatch for one of the listed modules " + 350 | "or a previous syntax error prevents a module from loading:\n" + 351 | unresolved.join('\n') 352 | ); 353 | } 354 | }, 355 | 356 | 357 | _DOMReady: function() { 358 | if( !ig.modules['dom.ready'].loaded ) { 359 | if ( !document.body ) { 360 | return setTimeout( ig._DOMReady, 13 ); 361 | } 362 | ig.modules['dom.ready'].loaded = true; 363 | ig._waitForOnload--; 364 | ig._execModules(); 365 | } 366 | return 0; 367 | }, 368 | 369 | 370 | _boot: function() { 371 | if( document.location.href.match(/\?nocache/) ) { 372 | ig.setNocache( true ); 373 | } 374 | 375 | // Probe user agent string 376 | ig.ua.pixelRatio = window.devicePixelRatio || 1; 377 | ig.ua.viewport = { 378 | width: window.innerWidth, 379 | height: window.innerHeight 380 | }; 381 | ig.ua.screen = { 382 | width: window.screen.availWidth * ig.ua.pixelRatio, 383 | height: window.screen.availHeight * ig.ua.pixelRatio 384 | }; 385 | 386 | ig.ua.iPhone = /iPhone|iPod/i.test(navigator.userAgent); 387 | ig.ua.iPhone4 = (ig.ua.iPhone && ig.ua.pixelRatio == 2); 388 | ig.ua.iPad = /iPad/i.test(navigator.userAgent); 389 | ig.ua.android = /android/i.test(navigator.userAgent); 390 | ig.ua.winPhone = /Windows Phone/i.test(navigator.userAgent); 391 | ig.ua.iOS = ig.ua.iPhone || ig.ua.iPad; 392 | ig.ua.mobile = ig.ua.iOS || ig.ua.android || ig.ua.winPhone || /mobile/i.test(navigator.userAgent); 393 | ig.ua.touchDevice = (('ontouchstart' in window) || (window.navigator.msMaxTouchPoints)); 394 | }, 395 | 396 | 397 | _initDOMReady: function() { 398 | if( ig.modules['dom.ready'] ) { 399 | ig._execModules(); 400 | return; 401 | } 402 | 403 | ig._boot(); 404 | 405 | 406 | ig.modules['dom.ready'] = { requires: [], loaded: false, body: null }; 407 | ig._waitForOnload++; 408 | if ( document.readyState === 'complete' ) { 409 | ig._DOMReady(); 410 | } 411 | else { 412 | document.addEventListener( 'DOMContentLoaded', ig._DOMReady, false ); 413 | window.addEventListener( 'load', ig._DOMReady, false ); 414 | } 415 | } 416 | }; 417 | 418 | 419 | // ----------------------------------------------------------------------------- 420 | // Provide ig.setAnimation and ig.clearAnimation as a compatible way to use 421 | // requestAnimationFrame if available or setInterval otherwise 422 | 423 | // Use requestAnimationFrame if available 424 | ig.normalizeVendorAttribute( window, 'requestAnimationFrame' ); 425 | if( window.requestAnimationFrame ) { 426 | var next = 1, 427 | anims = {}; 428 | 429 | window.ig.setAnimation = function( callback ) { 430 | var current = next++; 431 | anims[current] = true; 432 | 433 | var animate = function() { 434 | if( !anims[current] ) { return; } // deleted? 435 | window.requestAnimationFrame( animate ); 436 | callback(); 437 | }; 438 | window.requestAnimationFrame( animate ); 439 | return current; 440 | }; 441 | 442 | window.ig.clearAnimation = function( id ) { 443 | delete anims[id]; 444 | }; 445 | } 446 | 447 | // [set/clear]Interval fallback 448 | else { 449 | window.ig.setAnimation = function( callback ) { 450 | return window.setInterval( callback, 1000/60 ); 451 | }; 452 | window.ig.clearAnimation = function( id ) { 453 | window.clearInterval( id ); 454 | }; 455 | } 456 | 457 | 458 | // ----------------------------------------------------------------------------- 459 | // Class object based on John Resigs code; inspired by base2 and Prototype 460 | // http://ejohn.org/blog/simple-javascript-inheritance/ 461 | 462 | var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\bparent\b/ : /.*/; 463 | var lastClassId = 0; 464 | 465 | window.ig.Class = function(){}; 466 | var inject = function(prop) { 467 | var proto = this.prototype; 468 | var parent = {}; 469 | for( var name in prop ) { 470 | if( 471 | typeof(prop[name]) == "function" && 472 | typeof(proto[name]) == "function" && 473 | fnTest.test(prop[name]) 474 | ) { 475 | parent[name] = proto[name]; // save original function 476 | proto[name] = (function(name, fn){ 477 | return function() { 478 | var tmp = this.parent; 479 | this.parent = parent[name]; 480 | var ret = fn.apply(this, arguments); 481 | this.parent = tmp; 482 | return ret; 483 | }; 484 | })( name, prop[name] ); 485 | } 486 | else { 487 | proto[name] = prop[name]; 488 | } 489 | } 490 | }; 491 | 492 | window.ig.Class.extend = function(prop) { 493 | var parent = this.prototype; 494 | 495 | initializing = true; 496 | var prototype = new this(); 497 | initializing = false; 498 | 499 | for( var name in prop ) { 500 | if( 501 | typeof(prop[name]) == "function" && 502 | typeof(parent[name]) == "function" && 503 | fnTest.test(prop[name]) 504 | ) { 505 | prototype[name] = (function(name, fn){ 506 | return function() { 507 | var tmp = this.parent; 508 | this.parent = parent[name]; 509 | var ret = fn.apply(this, arguments); 510 | this.parent = tmp; 511 | return ret; 512 | }; 513 | })( name, prop[name] ); 514 | } 515 | else { 516 | prototype[name] = prop[name]; 517 | } 518 | } 519 | 520 | function Class() { 521 | if( !initializing ) { 522 | 523 | // If this class has a staticInstantiate method, invoke it 524 | // and check if we got something back. If not, the normal 525 | // constructor (init) is called. 526 | if( this.staticInstantiate ) { 527 | var obj = this.staticInstantiate.apply(this, arguments); 528 | if( obj ) { 529 | return obj; 530 | } 531 | } 532 | for( var p in this ) { 533 | if( typeof(this[p]) == 'object' ) { 534 | this[p] = ig.copy(this[p]); // deep copy! 535 | } 536 | } 537 | if( this.init ) { 538 | this.init.apply(this, arguments); 539 | } 540 | } 541 | return this; 542 | } 543 | 544 | Class.prototype = prototype; 545 | Class.prototype.constructor = Class; 546 | Class.extend = window.ig.Class.extend; 547 | Class.inject = inject; 548 | Class.classId = prototype.classId = ++lastClassId; 549 | 550 | return Class; 551 | }; 552 | 553 | // Merge the ImpactMixin - if present - into the 'ig' namespace. This gives other 554 | // code the chance to modify 'ig' before it's doing any work. 555 | if( window.ImpactMixin ) { 556 | ig.merge(ig, window.ImpactMixin); 557 | } 558 | 559 | })(window); 560 | 561 | 562 | 563 | // ----------------------------------------------------------------------------- 564 | // The main() function creates the system, input, sound and game objects, 565 | // creates a preloader and starts the run loop 566 | 567 | ig.module( 568 | 'impact.impact' 569 | ) 570 | .requires( 571 | 'dom.ready', 572 | 'impact.loader', 573 | 'impact.system', 574 | 'impact.input', 575 | 'impact.sound' 576 | ) 577 | .defines(function(){ "use strict"; 578 | 579 | ig.main = function( canvasId, gameClass, fps, width, height, scale, loaderClass ) { 580 | ig.system = new ig.System( canvasId, fps, width, height, scale || 1 ); 581 | ig.input = new ig.Input(); 582 | ig.soundManager = new ig.SoundManager(); 583 | ig.music = new ig.Music(); 584 | ig.ready = true; 585 | 586 | var loader = new (loaderClass || ig.Loader)( gameClass, ig.resources ); 587 | loader.load(); 588 | }; 589 | 590 | }); 591 | -------------------------------------------------------------------------------- /lib/impact/input.js: -------------------------------------------------------------------------------- 1 | ig.module( 2 | 'impact.input' 3 | ) 4 | .defines(function(){ "use strict"; 5 | 6 | ig.KEY = { 7 | 'MOUSE1': -1, 8 | 'MOUSE2': -3, 9 | 'MWHEEL_UP': -4, 10 | 'MWHEEL_DOWN': -5, 11 | 12 | 'BACKSPACE': 8, 13 | 'TAB': 9, 14 | 'ENTER': 13, 15 | 'PAUSE': 19, 16 | 'CAPS': 20, 17 | 'ESC': 27, 18 | 'SPACE': 32, 19 | 'PAGE_UP': 33, 20 | 'PAGE_DOWN': 34, 21 | 'END': 35, 22 | 'HOME': 36, 23 | 'LEFT_ARROW': 37, 24 | 'UP_ARROW': 38, 25 | 'RIGHT_ARROW': 39, 26 | 'DOWN_ARROW': 40, 27 | 'INSERT': 45, 28 | 'DELETE': 46, 29 | '_0': 48, 30 | '_1': 49, 31 | '_2': 50, 32 | '_3': 51, 33 | '_4': 52, 34 | '_5': 53, 35 | '_6': 54, 36 | '_7': 55, 37 | '_8': 56, 38 | '_9': 57, 39 | 'A': 65, 40 | 'B': 66, 41 | 'C': 67, 42 | 'D': 68, 43 | 'E': 69, 44 | 'F': 70, 45 | 'G': 71, 46 | 'H': 72, 47 | 'I': 73, 48 | 'J': 74, 49 | 'K': 75, 50 | 'L': 76, 51 | 'M': 77, 52 | 'N': 78, 53 | 'O': 79, 54 | 'P': 80, 55 | 'Q': 81, 56 | 'R': 82, 57 | 'S': 83, 58 | 'T': 84, 59 | 'U': 85, 60 | 'V': 86, 61 | 'W': 87, 62 | 'X': 88, 63 | 'Y': 89, 64 | 'Z': 90, 65 | 'NUMPAD_0': 96, 66 | 'NUMPAD_1': 97, 67 | 'NUMPAD_2': 98, 68 | 'NUMPAD_3': 99, 69 | 'NUMPAD_4': 100, 70 | 'NUMPAD_5': 101, 71 | 'NUMPAD_6': 102, 72 | 'NUMPAD_7': 103, 73 | 'NUMPAD_8': 104, 74 | 'NUMPAD_9': 105, 75 | 'MULTIPLY': 106, 76 | 'ADD': 107, 77 | 'SUBSTRACT': 109, 78 | 'DECIMAL': 110, 79 | 'DIVIDE': 111, 80 | 'F1': 112, 81 | 'F2': 113, 82 | 'F3': 114, 83 | 'F4': 115, 84 | 'F5': 116, 85 | 'F6': 117, 86 | 'F7': 118, 87 | 'F8': 119, 88 | 'F9': 120, 89 | 'F10': 121, 90 | 'F11': 122, 91 | 'F12': 123, 92 | 'SHIFT': 16, 93 | 'CTRL': 17, 94 | 'ALT': 18, 95 | 'PLUS': 187, 96 | 'COMMA': 188, 97 | 'MINUS': 189, 98 | 'PERIOD': 190 99 | }; 100 | 101 | 102 | ig.Input = ig.Class.extend({ 103 | bindings: {}, 104 | actions: {}, 105 | presses: {}, 106 | locks: {}, 107 | delayedKeyup: {}, 108 | 109 | isUsingMouse: false, 110 | isUsingKeyboard: false, 111 | isUsingAccelerometer: false, 112 | mouse: {x: 0, y: 0}, 113 | accel: {x: 0, y: 0, z: 0}, 114 | 115 | 116 | initMouse: function() { 117 | if( this.isUsingMouse ) { return; } 118 | this.isUsingMouse = true; 119 | ig.system.canvas.addEventListener('wheel', this.mousewheel.bind(this), false ); 120 | 121 | ig.system.canvas.addEventListener('contextmenu', this.contextmenu.bind(this), false ); 122 | ig.system.canvas.addEventListener('mousedown', this.keydown.bind(this), false ); 123 | ig.system.canvas.addEventListener('mouseup', this.keyup.bind(this), false ); 124 | ig.system.canvas.addEventListener('mousemove', this.mousemove.bind(this), false ); 125 | 126 | if( ig.ua.touchDevice ) { 127 | // Standard 128 | ig.system.canvas.addEventListener('touchstart', this.keydown.bind(this), false ); 129 | ig.system.canvas.addEventListener('touchend', this.keyup.bind(this), false ); 130 | ig.system.canvas.addEventListener('touchcancel', this.keyup.bind(this), false ); 131 | ig.system.canvas.addEventListener('touchmove', this.mousemove.bind(this), false ); 132 | 133 | // MS 134 | ig.system.canvas.addEventListener('MSPointerDown', this.keydown.bind(this), false ); 135 | ig.system.canvas.addEventListener('MSPointerUp', this.keyup.bind(this), false ); 136 | ig.system.canvas.addEventListener('MSPointerMove', this.mousemove.bind(this), false ); 137 | ig.system.canvas.style.msTouchAction = 'none'; 138 | } 139 | }, 140 | 141 | 142 | initKeyboard: function() { 143 | if( this.isUsingKeyboard ) { return; } 144 | this.isUsingKeyboard = true; 145 | window.addEventListener('keydown', this.keydown.bind(this), false ); 146 | window.addEventListener('keyup', this.keyup.bind(this), false ); 147 | }, 148 | 149 | 150 | initAccelerometer: function() { 151 | if( this.isUsingAccelerometer ) { return; } 152 | this.isUsingAccelerometer = true; 153 | window.addEventListener('devicemotion', this.devicemotion.bind(this), false ); 154 | }, 155 | 156 | 157 | mousewheel: function( event ) { 158 | var code = event.deltaY < 0 ? ig.KEY.MWHEEL_UP : ig.KEY.MWHEEL_DOWN; 159 | var action = this.bindings[code]; 160 | if( action ) { 161 | this.actions[action] = true; 162 | this.presses[action] = true; 163 | this.delayedKeyup[action] = true; 164 | event.stopPropagation(); 165 | event.preventDefault(); 166 | } 167 | }, 168 | 169 | 170 | mousemove: function( event ) { 171 | var internalWidth = ig.system.canvas.offsetWidth || ig.system.realWidth; 172 | var scale = ig.system.scale * (internalWidth / ig.system.realWidth); 173 | 174 | var pos = {left: 0, top: 0}; 175 | if( ig.system.canvas.getBoundingClientRect ) { 176 | pos = ig.system.canvas.getBoundingClientRect(); 177 | } 178 | 179 | var ev = event.touches ? event.touches[0] : event; 180 | this.mouse.x = (ev.clientX - pos.left) / scale; 181 | this.mouse.y = (ev.clientY - pos.top) / scale; 182 | }, 183 | 184 | 185 | contextmenu: function( event ) { 186 | if( this.bindings[ig.KEY.MOUSE2] ) { 187 | event.stopPropagation(); 188 | event.preventDefault(); 189 | } 190 | }, 191 | 192 | 193 | keydown: function( event ) { 194 | var tag = event.target.tagName; 195 | if( tag == 'INPUT' || tag == 'TEXTAREA' ) { return; } 196 | 197 | var code = event.type == 'keydown' 198 | ? event.keyCode 199 | : (event.button == 2 ? ig.KEY.MOUSE2 : ig.KEY.MOUSE1); 200 | 201 | // Focus window element for mouse clicks. Prevents issues when 202 | // running the game in an iframe. 203 | if( code < 0 && !ig.ua.mobile ) { 204 | window.focus(); 205 | } 206 | 207 | if( event.type == 'touchstart' || event.type == 'mousedown' ) { 208 | this.mousemove( event ); 209 | } 210 | 211 | var action = this.bindings[code]; 212 | if( action ) { 213 | this.actions[action] = true; 214 | if( !this.locks[action] ) { 215 | this.presses[action] = true; 216 | this.locks[action] = true; 217 | } 218 | event.preventDefault(); 219 | } 220 | }, 221 | 222 | 223 | keyup: function( event ) { 224 | var tag = event.target.tagName; 225 | if( tag == 'INPUT' || tag == 'TEXTAREA' ) { return; } 226 | 227 | var code = event.type == 'keyup' 228 | ? event.keyCode 229 | : (event.button == 2 ? ig.KEY.MOUSE2 : ig.KEY.MOUSE1); 230 | 231 | var action = this.bindings[code]; 232 | if( action ) { 233 | this.delayedKeyup[action] = true; 234 | event.preventDefault(); 235 | } 236 | }, 237 | 238 | 239 | devicemotion: function( event ) { 240 | this.accel = event.accelerationIncludingGravity; 241 | }, 242 | 243 | 244 | bind: function( key, action ) { 245 | if( key < 0 ) { this.initMouse(); } 246 | else if( key > 0 ) { this.initKeyboard(); } 247 | this.bindings[key] = action; 248 | }, 249 | 250 | 251 | bindTouch: function( selector, action ) { 252 | var element = ig.$( selector ); 253 | 254 | var that = this; 255 | element.addEventListener('touchstart', function(ev) {that.touchStart( ev, action );}, false); 256 | element.addEventListener('touchend', function(ev) {that.touchEnd( ev, action );}, false); 257 | element.addEventListener('MSPointerDown', function(ev) {that.touchStart( ev, action );}, false); 258 | element.addEventListener('MSPointerUp', function(ev) {that.touchEnd( ev, action );}, false); 259 | }, 260 | 261 | 262 | unbind: function( key ) { 263 | var action = this.bindings[key]; 264 | this.delayedKeyup[action] = true; 265 | 266 | this.bindings[key] = null; 267 | }, 268 | 269 | 270 | unbindAll: function() { 271 | this.bindings = {}; 272 | this.actions = {}; 273 | this.presses = {}; 274 | this.locks = {}; 275 | this.delayedKeyup = {}; 276 | }, 277 | 278 | 279 | state: function( action ) { 280 | return this.actions[action]; 281 | }, 282 | 283 | 284 | pressed: function( action ) { 285 | return this.presses[action]; 286 | }, 287 | 288 | released: function( action ) { 289 | return !!this.delayedKeyup[action]; 290 | }, 291 | 292 | clearPressed: function() { 293 | for( var action in this.delayedKeyup ) { 294 | this.actions[action] = false; 295 | this.locks[action] = false; 296 | } 297 | this.delayedKeyup = {}; 298 | this.presses = {}; 299 | }, 300 | 301 | touchStart: function( event, action ) { 302 | this.actions[action] = true; 303 | this.presses[action] = true; 304 | 305 | event.stopPropagation(); 306 | event.preventDefault(); 307 | return false; 308 | }, 309 | 310 | 311 | touchEnd: function( event, action ) { 312 | this.delayedKeyup[action] = true; 313 | event.stopPropagation(); 314 | event.preventDefault(); 315 | return false; 316 | } 317 | }); 318 | 319 | }); 320 | -------------------------------------------------------------------------------- /lib/impact/loader.js: -------------------------------------------------------------------------------- 1 | ig.module( 2 | 'impact.loader' 3 | ) 4 | .requires( 5 | 'impact.image', 6 | 'impact.font', 7 | 'impact.sound' 8 | ) 9 | .defines(function(){ "use strict"; 10 | 11 | ig.Loader = ig.Class.extend({ 12 | resources: [], 13 | 14 | gameClass: null, 15 | status: 0, 16 | done: false, 17 | 18 | _unloaded: [], 19 | _drawStatus: 0, 20 | _intervalId: 0, 21 | _loadCallbackBound: null, 22 | 23 | 24 | init: function( gameClass, resources ) { 25 | this.gameClass = gameClass; 26 | this.resources = resources; 27 | this._loadCallbackBound = this._loadCallback.bind(this); 28 | 29 | for( var i = 0; i < this.resources.length; i++ ) { 30 | this._unloaded.push( this.resources[i].path ); 31 | } 32 | }, 33 | 34 | 35 | load: function() { 36 | ig.system.clear( '#000' ); 37 | 38 | if( !this.resources.length ) { 39 | this.end(); 40 | return; 41 | } 42 | 43 | for( var i = 0; i < this.resources.length; i++ ) { 44 | this.loadResource( this.resources[i] ); 45 | } 46 | this._intervalId = setInterval( this.draw.bind(this), 16 ); 47 | }, 48 | 49 | 50 | loadResource: function( res ) { 51 | res.load( this._loadCallbackBound ); 52 | }, 53 | 54 | 55 | end: function() { 56 | if( this.done ) { return; } 57 | 58 | this.done = true; 59 | clearInterval( this._intervalId ); 60 | ig.system.setGame( this.gameClass ); 61 | }, 62 | 63 | 64 | draw: function() { 65 | this._drawStatus += (this.status - this._drawStatus)/5; 66 | var s = ig.system.scale; 67 | var w = (ig.system.width * 0.6).floor(); 68 | var h = (ig.system.height * 0.1).floor(); 69 | var x = (ig.system.width * 0.5-w/2).floor(); 70 | var y = (ig.system.height * 0.5-h/2).floor(); 71 | 72 | ig.system.context.fillStyle = '#000'; 73 | ig.system.context.fillRect( 0, 0, ig.system.width, ig.system.height ); 74 | 75 | ig.system.context.fillStyle = '#fff'; 76 | ig.system.context.fillRect( x*s, y*s, w*s, h*s ); 77 | 78 | ig.system.context.fillStyle = '#000'; 79 | ig.system.context.fillRect( x*s+s, y*s+s, w*s-s-s, h*s-s-s ); 80 | 81 | ig.system.context.fillStyle = '#fff'; 82 | ig.system.context.fillRect( x*s, y*s, w*s*this._drawStatus, h*s ); 83 | }, 84 | 85 | 86 | _loadCallback: function( path, status ) { 87 | if( status ) { 88 | this._unloaded.erase( path ); 89 | } 90 | else { 91 | throw( 'Failed to load resource: ' + path ); 92 | } 93 | 94 | this.status = 1 - (this._unloaded.length / this.resources.length); 95 | if( this._unloaded.length == 0 ) { // all done? 96 | setTimeout( this.end.bind(this), 250 ); 97 | } 98 | } 99 | }); 100 | 101 | }); 102 | -------------------------------------------------------------------------------- /lib/impact/map.js: -------------------------------------------------------------------------------- 1 | ig.module( 2 | 'impact.map' 3 | ) 4 | .defines(function(){ "use strict"; 5 | 6 | ig.Map = ig.Class.extend({ 7 | tilesize: 8, 8 | width: 1, 9 | height: 1, 10 | pxWidth: 1, 11 | pxHeight: 1, 12 | data: [[]], 13 | name: null, 14 | 15 | 16 | init: function( tilesize, data ) { 17 | this.tilesize = tilesize; 18 | this.data = data; 19 | this.height = data.length; 20 | this.width = data[0].length; 21 | 22 | this.pxWidth = this.width * this.tilesize; 23 | this.pxHeight = this.height * this.tilesize; 24 | }, 25 | 26 | 27 | getTile: function( x, y ) { 28 | var tx = Math.floor( x / this.tilesize ); 29 | var ty = Math.floor( y / this.tilesize ); 30 | if( 31 | (tx >= 0 && tx < this.width) && 32 | (ty >= 0 && ty < this.height) 33 | ) { 34 | return this.data[ty][tx]; 35 | } 36 | else { 37 | return 0; 38 | } 39 | }, 40 | 41 | 42 | setTile: function( x, y, tile ) { 43 | var tx = Math.floor( x / this.tilesize ); 44 | var ty = Math.floor( y / this.tilesize ); 45 | if( 46 | (tx >= 0 && tx < this.width) && 47 | (ty >= 0 && ty < this.height) 48 | ) { 49 | this.data[ty][tx] = tile; 50 | } 51 | } 52 | }); 53 | 54 | }); -------------------------------------------------------------------------------- /lib/impact/sound.js: -------------------------------------------------------------------------------- 1 | ig.module( 2 | 'impact.sound' 3 | ) 4 | .defines(function(){ "use strict"; 5 | 6 | ig.SoundManager = ig.Class.extend({ 7 | clips: {}, 8 | volume: 1, 9 | format: null, 10 | 11 | init: function() { 12 | // Quick sanity check if the Browser supports the Audio tag 13 | if( !ig.Sound.enabled || !window.Audio ) { 14 | ig.Sound.enabled = false; 15 | return; 16 | } 17 | 18 | // Probe sound formats and determine the file extension to load 19 | var probe = new Audio(); 20 | for( var i = 0; i < ig.Sound.use.length; i++ ) { 21 | var format = ig.Sound.use[i]; 22 | if( probe.canPlayType(format.mime) ) { 23 | this.format = format; 24 | break; 25 | } 26 | } 27 | 28 | // No compatible format found? -> Disable sound 29 | if( !this.format ) { 30 | ig.Sound.enabled = false; 31 | } 32 | 33 | // Create WebAudio Context 34 | if( ig.Sound.enabled && ig.Sound.useWebAudio ) { 35 | this.audioContext = new AudioContext(); 36 | this.boundWebAudioUnlock = this.unlockWebAudio.bind(this); 37 | ig.system.canvas.addEventListener('touchstart', this.boundWebAudioUnlock, false); 38 | ig.system.canvas.addEventListener('mousedown', this.boundWebAudioUnlock, false); 39 | } 40 | }, 41 | 42 | unlockWebAudio: function() { 43 | ig.system.canvas.removeEventListener('touchstart', this.boundWebAudioUnlock, false); 44 | ig.system.canvas.removeEventListener('mousedown', this.boundWebAudioUnlock, false); 45 | 46 | // create empty buffer 47 | var buffer = this.audioContext.createBuffer(1, 1, 22050); 48 | var source = this.audioContext.createBufferSource(); 49 | source.buffer = buffer; 50 | 51 | source.connect(this.audioContext.destination); 52 | source.start(0); 53 | }, 54 | 55 | load: function( path, multiChannel, loadCallback ) { 56 | if( multiChannel && ig.Sound.useWebAudio ) { 57 | // Requested as Multichannel and we're using WebAudio? 58 | return this.loadWebAudio( path, multiChannel, loadCallback ); 59 | } 60 | else { 61 | // Oldschool HTML5 Audio - always used for Music 62 | return this.loadHTML5Audio( path, multiChannel, loadCallback ); 63 | } 64 | }, 65 | 66 | loadWebAudio: function( path, multiChannel, loadCallback ) { 67 | // Path to the soundfile with the right extension (.ogg or .mp3) 68 | var realPath = ig.prefix + path.replace(/[^\.]+$/, this.format.ext) + ig.nocache; 69 | 70 | if( this.clips[path] ) { 71 | return this.clips[path]; 72 | } 73 | 74 | var audioSource = new ig.Sound.WebAudioSource(); 75 | this.clips[path] = audioSource; 76 | 77 | var request = new XMLHttpRequest(); 78 | request.open('GET', realPath, true); 79 | request.responseType = 'arraybuffer'; 80 | 81 | 82 | var that = this; 83 | request.onload = function(ev) { 84 | that.audioContext.decodeAudioData(request.response, 85 | function(buffer) { 86 | audioSource.buffer = buffer; 87 | if( loadCallback ) { 88 | loadCallback( path, true, ev ); 89 | } 90 | }, 91 | function(ev) { 92 | if( loadCallback ) { 93 | loadCallback( path, false, ev ); 94 | } 95 | } 96 | ); 97 | }; 98 | request.onerror = function(ev) { 99 | if( loadCallback ) { 100 | loadCallback( path, false, ev ); 101 | } 102 | }; 103 | request.send(); 104 | 105 | return audioSource; 106 | }, 107 | 108 | loadHTML5Audio: function( path, multiChannel, loadCallback ) { 109 | 110 | // Path to the soundfile with the right extension (.ogg or .mp3) 111 | var realPath = ig.prefix + path.replace(/[^\.]+$/, this.format.ext) + ig.nocache; 112 | 113 | // Sound file already loaded? 114 | if( this.clips[path] ) { 115 | // Loaded as WebAudio, but now requested as HTML5 Audio? Probably Music? 116 | if( this.clips[path] instanceof ig.Sound.WebAudioSource ) { 117 | return this.clips[path]; 118 | } 119 | 120 | // Only loaded as single channel and now requested as multichannel? 121 | if( multiChannel && this.clips[path].length < ig.Sound.channels ) { 122 | for( var i = this.clips[path].length; i < ig.Sound.channels; i++ ) { 123 | var a = new Audio( realPath ); 124 | a.load(); 125 | this.clips[path].push( a ); 126 | } 127 | } 128 | return this.clips[path][0]; 129 | } 130 | 131 | var clip = new Audio( realPath ); 132 | if( loadCallback ) { 133 | 134 | // The canplaythrough event is dispatched when the browser determines 135 | // that the sound can be played without interuption, provided the 136 | // download rate doesn't change. 137 | // Mobile browsers stubbornly refuse to preload HTML5, so we simply 138 | // ignore the canplaythrough event and immediately "fake" a successful 139 | // load callback 140 | if( ig.ua.mobile ) { 141 | setTimeout(function(){ 142 | loadCallback( path, true, null ); 143 | }, 0); 144 | } 145 | else { 146 | clip.addEventListener( 'canplaythrough', function cb(ev){ 147 | clip.removeEventListener('canplaythrough', cb, false); 148 | loadCallback( path, true, ev ); 149 | }, false ); 150 | 151 | clip.addEventListener( 'error', function(ev){ 152 | loadCallback( path, false, ev ); 153 | }, false); 154 | } 155 | } 156 | clip.preload = 'auto'; 157 | clip.load(); 158 | 159 | 160 | this.clips[path] = [clip]; 161 | if( multiChannel ) { 162 | for( var i = 1; i < ig.Sound.channels; i++ ) { 163 | var a = new Audio(realPath); 164 | a.load(); 165 | this.clips[path].push( a ); 166 | } 167 | } 168 | 169 | return clip; 170 | }, 171 | 172 | 173 | get: function( path ) { 174 | // Find and return a channel that is not currently playing 175 | var channels = this.clips[path]; 176 | 177 | // Is this a WebAudio source? We only ever have one for each Sound 178 | if( channels && channels instanceof ig.Sound.WebAudioSource ) { 179 | return channels; 180 | } 181 | 182 | // Oldschool HTML5 Audio - find a channel that's not currently 183 | // playing or, if all are playing, rewind one 184 | for( var i = 0, clip; clip = channels[i++]; ) { 185 | if( clip.paused || clip.ended ) { 186 | if( clip.ended ) { 187 | clip.currentTime = 0; 188 | } 189 | return clip; 190 | } 191 | } 192 | 193 | // Still here? Pause and rewind the first channel 194 | channels[0].pause(); 195 | channels[0].currentTime = 0; 196 | return channels[0]; 197 | } 198 | }); 199 | 200 | 201 | 202 | ig.Music = ig.Class.extend({ 203 | tracks: [], 204 | namedTracks: {}, 205 | currentTrack: null, 206 | currentIndex: 0, 207 | random: false, 208 | 209 | _volume: 1, 210 | _loop: false, 211 | _fadeInterval: 0, 212 | _fadeTimer: null, 213 | _endedCallbackBound: null, 214 | 215 | 216 | init: function() { 217 | this._endedCallbackBound = this._endedCallback.bind(this); 218 | 219 | Object.defineProperty(this,"volume", { 220 | get: this.getVolume.bind(this), 221 | set: this.setVolume.bind(this) 222 | }); 223 | 224 | Object.defineProperty(this,"loop", { 225 | get: this.getLooping.bind(this), 226 | set: this.setLooping.bind(this) 227 | }); 228 | }, 229 | 230 | 231 | add: function( music, name ) { 232 | if( !ig.Sound.enabled ) { 233 | return; 234 | } 235 | 236 | var path = music instanceof ig.Sound ? music.path : music; 237 | 238 | var track = ig.soundManager.load(path, false); 239 | 240 | // Did we get a WebAudio Source? This is suboptimal; Music should be loaded 241 | // as HTML5 Audio so it can be streamed 242 | if( track instanceof ig.Sound.WebAudioSource ) { 243 | // Since this error will likely occur at game start, we stop the game 244 | // to not produce any more errors. 245 | ig.system.stopRunLoop(); 246 | throw( 247 | "Sound '"+path+"' loaded as Multichannel but used for Music. " + 248 | "Set the multiChannel param to false when loading, e.g.: new ig.Sound(path, false)" 249 | ); 250 | } 251 | 252 | track.loop = this._loop; 253 | track.volume = this._volume; 254 | track.addEventListener( 'ended', this._endedCallbackBound, false ); 255 | this.tracks.push( track ); 256 | 257 | if( name ) { 258 | this.namedTracks[name] = track; 259 | } 260 | 261 | if( !this.currentTrack ) { 262 | this.currentTrack = track; 263 | } 264 | }, 265 | 266 | 267 | next: function() { 268 | if( !this.tracks.length ) { return; } 269 | 270 | this.stop(); 271 | this.currentIndex = this.random 272 | ? Math.floor(Math.random() * this.tracks.length) 273 | : (this.currentIndex + 1) % this.tracks.length; 274 | this.currentTrack = this.tracks[this.currentIndex]; 275 | this.play(); 276 | }, 277 | 278 | 279 | pause: function() { 280 | if( !this.currentTrack ) { return; } 281 | this.currentTrack.pause(); 282 | }, 283 | 284 | 285 | stop: function() { 286 | if( !this.currentTrack ) { return; } 287 | this.currentTrack.pause(); 288 | this.currentTrack.currentTime = 0; 289 | }, 290 | 291 | 292 | play: function( name ) { 293 | // If a name was provided, stop playing the current track (if any) 294 | // and play the named track 295 | if( name && this.namedTracks[name] ) { 296 | var newTrack = this.namedTracks[name]; 297 | if( newTrack != this.currentTrack ) { 298 | this.stop(); 299 | this.currentTrack = newTrack; 300 | } 301 | } 302 | else if( !this.currentTrack ) { 303 | return; 304 | } 305 | this.currentTrack.play(); 306 | }, 307 | 308 | 309 | getLooping: function() { 310 | return this._loop; 311 | }, 312 | 313 | 314 | setLooping: function( l ) { 315 | this._loop = l; 316 | for( var i in this.tracks ) { 317 | this.tracks[i].loop = l; 318 | } 319 | }, 320 | 321 | 322 | getVolume: function() { 323 | return this._volume; 324 | }, 325 | 326 | 327 | setVolume: function( v ) { 328 | this._volume = v.limit(0,1); 329 | for( var i in this.tracks ) { 330 | this.tracks[i].volume = this._volume; 331 | } 332 | }, 333 | 334 | 335 | fadeOut: function( time ) { 336 | if( !this.currentTrack ) { return; } 337 | 338 | clearInterval( this._fadeInterval ); 339 | this._fadeTimer = new ig.Timer( time ); 340 | this._fadeInterval = setInterval( this._fadeStep.bind(this), 50 ); 341 | }, 342 | 343 | 344 | _fadeStep: function() { 345 | var v = this._fadeTimer.delta() 346 | .map(-this._fadeTimer.target, 0, 1, 0) 347 | .limit( 0, 1 ) 348 | * this._volume; 349 | 350 | if( v <= 0.01 ) { 351 | this.stop(); 352 | this.currentTrack.volume = this._volume; 353 | clearInterval( this._fadeInterval ); 354 | } 355 | else { 356 | this.currentTrack.volume = v; 357 | } 358 | }, 359 | 360 | _endedCallback: function() { 361 | if( this._loop ) { 362 | this.play(); 363 | } 364 | else { 365 | this.next(); 366 | } 367 | } 368 | }); 369 | 370 | 371 | 372 | ig.Sound = ig.Class.extend({ 373 | path: '', 374 | volume: 1, 375 | currentClip: null, 376 | multiChannel: true, 377 | _loop: false, 378 | 379 | 380 | init: function( path, multiChannel ) { 381 | this.path = path; 382 | this.multiChannel = (multiChannel !== false); 383 | 384 | Object.defineProperty(this,"loop", { 385 | get: this.getLooping.bind(this), 386 | set: this.setLooping.bind(this) 387 | }); 388 | 389 | this.load(); 390 | }, 391 | 392 | getLooping: function() { 393 | return this._loop; 394 | }, 395 | 396 | setLooping: function( loop ) { 397 | this._loop = loop; 398 | 399 | if( this.currentClip ) { 400 | this.currentClip.loop = loop; 401 | } 402 | }, 403 | 404 | load: function( loadCallback ) { 405 | if( !ig.Sound.enabled ) { 406 | if( loadCallback ) { 407 | loadCallback( this.path, true ); 408 | } 409 | return; 410 | } 411 | 412 | if( ig.ready ) { 413 | ig.soundManager.load( this.path, this.multiChannel, loadCallback ); 414 | } 415 | else { 416 | ig.addResource( this ); 417 | } 418 | }, 419 | 420 | 421 | play: function() { 422 | if( !ig.Sound.enabled ) { 423 | return; 424 | } 425 | 426 | this.currentClip = ig.soundManager.get( this.path ); 427 | this.currentClip.loop = this._loop; 428 | this.currentClip.volume = ig.soundManager.volume * this.volume; 429 | this.currentClip.play(); 430 | }, 431 | 432 | 433 | stop: function() { 434 | if( this.currentClip ) { 435 | this.currentClip.pause(); 436 | this.currentClip.currentTime = 0; 437 | } 438 | } 439 | }); 440 | 441 | 442 | ig.Sound.WebAudioSource = ig.Class.extend({ 443 | sources: [], 444 | gain: null, 445 | buffer: null, 446 | _loop: false, 447 | 448 | init: function() { 449 | this.gain = ig.soundManager.audioContext.createGain(); 450 | this.gain.connect(ig.soundManager.audioContext.destination); 451 | 452 | Object.defineProperty(this,"loop", { 453 | get: this.getLooping.bind(this), 454 | set: this.setLooping.bind(this) 455 | }); 456 | 457 | Object.defineProperty(this,"volume", { 458 | get: this.getVolume.bind(this), 459 | set: this.setVolume.bind(this) 460 | }); 461 | }, 462 | 463 | play: function() { 464 | if( !this.buffer ) { return; } 465 | var source = ig.soundManager.audioContext.createBufferSource(); 466 | source.buffer = this.buffer; 467 | source.connect(this.gain); 468 | source.loop = this._loop; 469 | 470 | // Add this new source to our sources array and remove it again 471 | // later when it has finished playing. 472 | var that = this; 473 | this.sources.push(source); 474 | source.onended = function(){ that.sources.erase(source); }; 475 | 476 | source.start(0); 477 | }, 478 | 479 | pause: function() { 480 | for( var i = 0; i < this.sources.length; i++ ) { 481 | try{ 482 | this.sources[i].stop(); 483 | } catch(err){} 484 | } 485 | }, 486 | 487 | getLooping: function() { 488 | return this._loop; 489 | }, 490 | 491 | setLooping: function( loop ) { 492 | this._loop = loop; 493 | 494 | for( var i = 0; i < this.sources.length; i++ ) { 495 | this.sources[i].loop = loop; 496 | } 497 | }, 498 | 499 | getVolume: function() { 500 | return this.gain.gain.value; 501 | }, 502 | 503 | setVolume: function( volume ) { 504 | this.gain.gain.value = volume; 505 | } 506 | }); 507 | 508 | 509 | ig.Sound.FORMAT = { 510 | MP3: {ext: 'mp3', mime: 'audio/mpeg'}, 511 | M4A: {ext: 'm4a', mime: 'audio/mp4; codecs=mp4a.40.2'}, 512 | OGG: {ext: 'ogg', mime: 'audio/ogg; codecs=vorbis'}, 513 | WEBM: {ext: 'webm', mime: 'audio/webm; codecs=vorbis'}, 514 | CAF: {ext: 'caf', mime: 'audio/x-caf'} 515 | }; 516 | ig.Sound.use = [ig.Sound.FORMAT.OGG, ig.Sound.FORMAT.MP3]; 517 | ig.Sound.channels = 4; 518 | ig.Sound.enabled = true; 519 | 520 | ig.normalizeVendorAttribute(window, 'AudioContext'); 521 | ig.Sound.useWebAudio = !!window.AudioContext; 522 | 523 | }); 524 | -------------------------------------------------------------------------------- /lib/impact/system.js: -------------------------------------------------------------------------------- 1 | ig.module( 2 | 'impact.system' 3 | ) 4 | .requires( 5 | 'impact.timer', 6 | 'impact.image' 7 | ) 8 | .defines(function(){ "use strict"; 9 | 10 | ig.System = ig.Class.extend({ 11 | fps: 30, 12 | width: 320, 13 | height: 240, 14 | realWidth: 320, 15 | realHeight: 240, 16 | scale: 1, 17 | 18 | tick: 0, 19 | animationId: 0, 20 | newGameClass: null, 21 | running: false, 22 | 23 | delegate: null, 24 | clock: null, 25 | canvas: null, 26 | context: null, 27 | 28 | init: function( canvasId, fps, width, height, scale ) { 29 | this.fps = fps; 30 | 31 | this.clock = new ig.Timer(); 32 | this.canvas = ig.$(canvasId); 33 | this.resize( width, height, scale ); 34 | this.context = this.canvas.getContext('2d'); 35 | 36 | this.getDrawPos = ig.System.drawMode; 37 | 38 | // Automatically switch to crisp scaling when using a scale 39 | // other than 1 40 | if( this.scale != 1 ) { 41 | ig.System.scaleMode = ig.System.SCALE.CRISP; 42 | } 43 | ig.System.scaleMode( this.canvas, this.context ); 44 | }, 45 | 46 | 47 | resize: function( width, height, scale ) { 48 | this.width = width; 49 | this.height = height; 50 | this.scale = scale || this.scale; 51 | 52 | this.realWidth = this.width * this.scale; 53 | this.realHeight = this.height * this.scale; 54 | this.canvas.width = this.realWidth; 55 | this.canvas.height = this.realHeight; 56 | }, 57 | 58 | 59 | setGame: function( gameClass ) { 60 | if( this.running ) { 61 | this.newGameClass = gameClass; 62 | } 63 | else { 64 | this.setGameNow( gameClass ); 65 | } 66 | }, 67 | 68 | 69 | setGameNow: function( gameClass ) { 70 | ig.game = new (gameClass)(); 71 | ig.system.setDelegate( ig.game ); 72 | }, 73 | 74 | 75 | setDelegate: function( object ) { 76 | if( typeof(object.run) == 'function' ) { 77 | this.delegate = object; 78 | this.startRunLoop(); 79 | } else { 80 | throw( 'System.setDelegate: No run() function in object' ); 81 | } 82 | }, 83 | 84 | 85 | stopRunLoop: function() { 86 | ig.clearAnimation( this.animationId ); 87 | this.running = false; 88 | }, 89 | 90 | 91 | startRunLoop: function() { 92 | this.stopRunLoop(); 93 | this.animationId = ig.setAnimation( this.run.bind(this) ); 94 | this.running = true; 95 | }, 96 | 97 | 98 | clear: function( color ) { 99 | this.context.fillStyle = color; 100 | this.context.fillRect( 0, 0, this.realWidth, this.realHeight ); 101 | }, 102 | 103 | 104 | run: function() { 105 | ig.Timer.step(); 106 | this.tick = this.clock.tick(); 107 | 108 | this.delegate.run(); 109 | ig.input.clearPressed(); 110 | 111 | if( this.newGameClass ) { 112 | this.setGameNow( this.newGameClass ); 113 | this.newGameClass = null; 114 | } 115 | }, 116 | 117 | 118 | getDrawPos: null // Set through constructor 119 | }); 120 | 121 | ig.System.DRAW = { 122 | AUTHENTIC: function( p ) { return Math.round(p) * this.scale; }, 123 | SMOOTH: function( p ) { return Math.round(p * this.scale); }, 124 | SUBPIXEL: function( p ) { return p * this.scale; } 125 | }; 126 | ig.System.drawMode = ig.System.DRAW.SMOOTH; 127 | 128 | ig.System.SCALE = { 129 | CRISP: function( canvas, context ) { 130 | ig.setVendorAttribute( context, 'imageSmoothingEnabled', false ); 131 | canvas.style.imageRendering = '-moz-crisp-edges'; 132 | canvas.style.imageRendering = '-o-crisp-edges'; 133 | canvas.style.imageRendering = '-webkit-optimize-contrast'; 134 | canvas.style.imageRendering = 'crisp-edges'; 135 | canvas.style.msInterpolationMode = 'nearest-neighbor'; // No effect on Canvas :/ 136 | }, 137 | SMOOTH: function( canvas, context ) { 138 | ig.setVendorAttribute( context, 'imageSmoothingEnabled', true ); 139 | canvas.style.imageRendering = ''; 140 | canvas.style.msInterpolationMode = ''; 141 | } 142 | }; 143 | ig.System.scaleMode = ig.System.SCALE.SMOOTH; 144 | 145 | }); 146 | -------------------------------------------------------------------------------- /lib/impact/timer.js: -------------------------------------------------------------------------------- 1 | ig.module( 2 | 'impact.timer' 3 | ) 4 | .defines(function(){ "use strict"; 5 | 6 | ig.Timer = ig.Class.extend({ 7 | target: 0, 8 | base: 0, 9 | last: 0, 10 | pausedAt: 0, 11 | 12 | init: function( seconds ) { 13 | this.base = ig.Timer.time; 14 | this.last = ig.Timer.time; 15 | 16 | this.target = seconds || 0; 17 | }, 18 | 19 | 20 | set: function( seconds ) { 21 | this.target = seconds || 0; 22 | this.base = ig.Timer.time; 23 | this.pausedAt = 0; 24 | }, 25 | 26 | 27 | reset: function() { 28 | this.base = ig.Timer.time; 29 | this.pausedAt = 0; 30 | }, 31 | 32 | 33 | tick: function() { 34 | var delta = ig.Timer.time - this.last; 35 | this.last = ig.Timer.time; 36 | return (this.pausedAt ? 0 : delta); 37 | }, 38 | 39 | 40 | delta: function() { 41 | return (this.pausedAt || ig.Timer.time) - this.base - this.target; 42 | }, 43 | 44 | 45 | pause: function() { 46 | if( !this.pausedAt ) { 47 | this.pausedAt = ig.Timer.time; 48 | } 49 | }, 50 | 51 | 52 | unpause: function() { 53 | if( this.pausedAt ) { 54 | this.base += ig.Timer.time - this.pausedAt; 55 | this.pausedAt = 0; 56 | } 57 | } 58 | }); 59 | 60 | ig.Timer._last = 0; 61 | ig.Timer.time = Number.MIN_VALUE; 62 | ig.Timer.timeScale = 1; 63 | ig.Timer.maxStep = 0.05; 64 | 65 | ig.Timer.step = function() { 66 | var current = Date.now(); 67 | var delta = (current - ig.Timer._last) / 1000; 68 | ig.Timer.time += Math.min(delta, ig.Timer.maxStep) * ig.Timer.timeScale; 69 | ig.Timer._last = current; 70 | }; 71 | 72 | }); -------------------------------------------------------------------------------- /lib/weltmeister/api/browse.php: -------------------------------------------------------------------------------- 1 | $f ) { 27 | $files[$i] = substr( $f, $fileRootLength ); 28 | } 29 | foreach( $dirs as $i => $d ) { 30 | $dirs[$i] = substr( $d, $fileRootLength ); 31 | } 32 | 33 | $parent = substr($_GET['dir'], 0, strrpos($_GET['dir'], '/')); 34 | echo json_encode( array( 35 | 'parent' => (empty($_GET['dir']) ? false : $parent), 36 | 'dirs' => $dirs, 37 | 'files' => $files 38 | )); 39 | 40 | ?> 41 | -------------------------------------------------------------------------------- /lib/weltmeister/api/config.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/weltmeister/api/glob.php: -------------------------------------------------------------------------------- 1 | $f ) { 16 | $files[$i] = substr( $f, $fileRootLength ); 17 | } 18 | 19 | echo json_encode( $files ); 20 | 21 | ?> -------------------------------------------------------------------------------- /lib/weltmeister/api/save.php: -------------------------------------------------------------------------------- 1 | 0); 5 | 6 | if( !empty($_POST['path']) && !empty($_POST['data']) ) { 7 | $path = WM_Config::$fileRoot . str_replace( '..', '', $_POST['path'] ); 8 | 9 | if( preg_match('/\.js$/', $path) ) { 10 | $success = @file_put_contents( $path, $_POST['data'] ); 11 | if( $success === false ) { 12 | $result = array( 13 | 'error' => '2', 14 | 'msg' => "Couldn't write to file: $path" 15 | ); 16 | } 17 | } 18 | else { 19 | $result = array( 20 | 'error' => '3', 21 | 'msg' => "File must have a .js suffix" 22 | ); 23 | } 24 | } 25 | else { 26 | $result = array( 27 | 'error' => '1', 28 | 'msg' => "No Data or Path specified" 29 | ); 30 | } 31 | 32 | echo json_encode($result); 33 | 34 | ?> -------------------------------------------------------------------------------- /lib/weltmeister/arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoboslab/Impact/7768fd29c70ca924a78673d93081baab5a72fbe6/lib/weltmeister/arrow.png -------------------------------------------------------------------------------- /lib/weltmeister/collisiontiles-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoboslab/Impact/7768fd29c70ca924a78673d93081baab5a72fbe6/lib/weltmeister/collisiontiles-64.png -------------------------------------------------------------------------------- /lib/weltmeister/config.js: -------------------------------------------------------------------------------- 1 | ig.module( 2 | 'weltmeister.config' 3 | ) 4 | .defines(function(){ "use strict"; 5 | 6 | wm.config = { 7 | 8 | project: { 9 | // The prefix path of your game's source code. You only have to change 10 | // this if you use the 'ImpactPrefix' in your dev environment. 11 | 'modulePath': 'lib/', 12 | 13 | // This "glob" tells Weltmeister where to load the entity files 14 | // from. If you want to load entities from several directories, 15 | // you can specify an array here. E.g.: 16 | // 'entityFiles': ['lib/game/powerups/*.js', 'lib/game/entities/*.js'] 17 | 'entityFiles': 'lib/game/entities/*.js', 18 | 19 | // The default path for the level file selection box 20 | 'levelPath': 'lib/game/levels/', 21 | 22 | // Whether to save levels as plain JSON or wrapped in a module. If 23 | // you want to load levels asynchronously via AJAX, saving as plain 24 | // JSON can be helpful. 25 | 'outputFormat': 'module', // 'module' or 'json' 26 | 27 | // Whether to pretty print the JSON data in level files. If you have 28 | // any issues with your levels, it's usually a good idea to turn this 29 | // on and look at the saved level files with a text editor. 30 | 'prettyPrint': true 31 | }, 32 | 33 | 34 | // Plugins for Weltmeister: an array of module names to load 35 | plugins: [], 36 | 37 | 38 | // Default settings when creating new layers in Weltmeister. Change these 39 | // as you like 40 | 'layerDefaults': { 41 | 'width': 30, 42 | 'height': 20, 43 | 'tilesize': 8 44 | }, 45 | 46 | // Whether to ask before closing Weltmeister when there are unsaved changes 47 | 'askBeforeClose': true, 48 | 49 | // Whether to attempt to load the last opened level on startup 50 | 'loadLastLevel': true, 51 | 52 | // Size of the "snap" grid when moving entities 53 | 'entityGrid': 4, 54 | 55 | // Number of undo levels. You may want to increase this if you use 'undo' 56 | // frequently. 57 | 'undoLevels': 50, 58 | 59 | // Mouse and Key bindings in Weltmeister. Some function are bound to 60 | // several keys. See the documentation of ig.Input for a list of available 61 | // key names. 62 | 'binds': { 63 | 'MOUSE1': 'draw', 64 | 'MOUSE2': 'drag', 65 | 'SHIFT': 'select', 66 | 'CTRL': 'drag', 67 | 'SPACE': 'menu', 68 | 'DELETE': 'delete', 69 | 'BACKSPACE': 'delete', 70 | 'G': 'grid', 71 | 'C': 'clone', 72 | 'Z': 'undo', 73 | 'Y': 'redo', 74 | 'MWHEEL_UP': 'zoomin', 75 | 'PLUS': 'zoomin', 76 | 'MWHEEL_DOWN': 'zoomout', 77 | 'MINUS': 'zoomout' 78 | }, 79 | 80 | // Whether to enable unidirectional scrolling for touchpads; this 81 | // automatically unbinds the MWHEEL_UP and MWHEEL_DOWN actions. 82 | 'touchScroll': false, 83 | 84 | // View settings. You can change the default Zoom level and whether 85 | // to show the grid on startup here. 86 | 'view': { 87 | 'zoom': 1, 88 | 'zoomMax': 4, 89 | 'zoomMin': 0.125, 90 | 'grid': false 91 | }, 92 | 93 | // Font face and size for entity labels and the grid coordinates 94 | 'labels': { 95 | 'draw': true, 96 | 'step': 32, 97 | 'font': '10px Bitstream Vera Sans Mono, Monaco, sans-serif' 98 | }, 99 | 100 | // Colors to use for the background, selection boxes, text and the grid 101 | 'colors': { 102 | 'clear': '#000000', // Background Color 103 | 'highlight': '#ceff36', // Currently selected tile or entity 104 | 'primary': '#ffffff', // Labels and layer bounds 105 | 'secondary': '#555555', // Grid and tile selection bounds 106 | 'selection': '#ff9933' // Selection cursor box on tile maps 107 | }, 108 | 109 | // Settings for the Collision tiles. You shouldn't need to change these. 110 | // The tilesize only specifies the size in the image - resizing to final 111 | // size for each layer happens in Weltmeister. 112 | 'collisionTiles': { 113 | 'path': 'lib/weltmeister/collisiontiles-64.png', 114 | 'tilesize': 64 115 | }, 116 | 117 | // API paths for saving levels and browsing directories. If you use a 118 | // different backend (i.e. not the official PHP backend), you may have 119 | // to change these. 120 | 'api': { 121 | 'save': 'lib/weltmeister/api/save.php', 122 | 'browse': 'lib/weltmeister/api/browse.php', 123 | 'glob': 'lib/weltmeister/api/glob.php' 124 | } 125 | }; 126 | 127 | }); -------------------------------------------------------------------------------- /lib/weltmeister/edit-entities.js: -------------------------------------------------------------------------------- 1 | ig.module( 2 | 'weltmeister.edit-entities' 3 | ) 4 | .requires( 5 | 'impact.game', 6 | 'impact.background-map', 7 | 'weltmeister.config', 8 | 'weltmeister.tile-select', 9 | 'weltmeister.entities' 10 | ) 11 | .defines(function(){ "use strict"; 12 | 13 | wm.EditEntities = ig.Class.extend({ 14 | visible: true, 15 | active: true, 16 | 17 | div: null, 18 | hotkey: -1, 19 | ignoreLastClick: false, 20 | name: 'entities', 21 | 22 | entities: [], 23 | namedEntities: {}, 24 | selectedEntity: null, 25 | entityClasses: {}, 26 | menu: null, 27 | selector: {size:{x:2, y:2}, pos:{x:0,y:0}, offset:{x:0,y:0}}, 28 | wasSelectedOnScaleBorder: false, 29 | gridSize: wm.config.entityGrid, 30 | entityDefinitions: null, 31 | 32 | 33 | 34 | init: function( div ) { 35 | this.div = div; 36 | div.bind( 'mouseup', this.click.bind(this) ); 37 | this.div.children('.visible').bind( 'mousedown', this.toggleVisibilityClick.bind(this) ); 38 | 39 | this.menu = $('#entityMenu'); 40 | this.importEntityClass( wm.entityModules ); 41 | this.entityDefinitions = $('#entityDefinitions'); 42 | 43 | $('#entityKey').bind( 'keydown', function(ev){ 44 | if( ev.which == 13 ){ 45 | $('#entityValue').focus(); 46 | return false; 47 | } 48 | return true; 49 | }); 50 | $('#entityValue').bind( 'keydown', this.setEntitySetting.bind(this) ); 51 | }, 52 | 53 | 54 | clear: function() { 55 | this.entities = []; 56 | this.selectEntity( null ); 57 | }, 58 | 59 | 60 | sort: function() { 61 | this.entities.sort( ig.Game.SORT.Z_INDEX ); 62 | }, 63 | 64 | 65 | 66 | 67 | // ------------------------------------------------------------------------- 68 | // Loading, Saving 69 | 70 | 71 | fileNameToClassName: function( name ) { 72 | var typeName = '-' + name.replace(/^.*\/|\.js/g,''); 73 | typeName = typeName.replace(/-(\w)/g, function( m, a ) { 74 | return a.toUpperCase(); 75 | }); 76 | return 'Entity' + typeName; 77 | }, 78 | 79 | 80 | importEntityClass: function( modules ) { 81 | var unloadedClasses = []; 82 | for( var m in modules ) { 83 | var className = this.fileNameToClassName(modules[m]); 84 | var entityName = className.replace(/^Entity/, ''); 85 | 86 | // ig.global[className] should be the actual class object 87 | if( className && ig.global[className] ) { 88 | 89 | // Ignore entities that have the _wmIgnore flag 90 | if( !ig.global[className].prototype._wmIgnore ) { 91 | var a = $( '
', { 92 | 'id': className, 93 | 'href': '#', 94 | 'html': entityName, 95 | 'mouseup': this.newEntityClick.bind(this) 96 | }); 97 | this.menu.append( a ); 98 | this.entityClasses[className] = m; 99 | } 100 | } 101 | else { 102 | unloadedClasses.push( modules[m] + ' (expected name: ' + className + ')' ); 103 | } 104 | } 105 | 106 | if( unloadedClasses.length > 0 ) { 107 | var warning = 'The following entity classes were not loaded due to\n' 108 | + 'file and class name mismatches: \n\n' 109 | + unloadedClasses.join( '\n' ); 110 | alert( warning ); 111 | } 112 | }, 113 | 114 | 115 | getEntityByName: function( name ) { 116 | return this.namedEntities[name]; 117 | }, 118 | 119 | 120 | getSaveData: function() { 121 | var ents = []; 122 | for( var i = 0; i < this.entities.length; i++ ) { 123 | var ent = this.entities[i]; 124 | var type = ent._wmClassName; 125 | var data = {type:type,x:ent.pos.x,y:ent.pos.y}; 126 | 127 | var hasSettings = false; 128 | for( var p in ent._wmSettings ) { 129 | hasSettings = true; 130 | } 131 | if( hasSettings ) { 132 | data.settings = ent._wmSettings; 133 | } 134 | 135 | ents.push( data ); 136 | } 137 | return ents; 138 | }, 139 | 140 | 141 | 142 | 143 | // ------------------------------------------------------------------------- 144 | // Selecting 145 | 146 | 147 | selectEntityAt: function( x, y ) { 148 | this.selector.pos = { x: x, y: y }; 149 | 150 | // Find all possible selections 151 | var possibleSelections = []; 152 | for( var i = 0; i < this.entities.length; i++ ) { 153 | if( this.entities[i].touches(this.selector) ) { 154 | possibleSelections.push( this.entities[i] ); 155 | } 156 | } 157 | 158 | // Nothing found? Early out. 159 | if( !possibleSelections.length ) { 160 | this.selectEntity( null ); 161 | return false; 162 | } 163 | 164 | // Find the 'next' selection 165 | var selectedIndex = possibleSelections.indexOf(this.selectedEntity); 166 | var nextSelection = (selectedIndex + 1) % possibleSelections.length; 167 | var ent = possibleSelections[nextSelection]; 168 | 169 | // Select it! 170 | this.selector.offset = { 171 | x: (x - ent.pos.x + ent.offset.x), 172 | y: (y - ent.pos.y + ent.offset.y) 173 | }; 174 | this.selectEntity( ent ); 175 | this.wasSelectedOnScaleBorder = this.isOnScaleBorder( ent, this.selector ); 176 | return ent; 177 | }, 178 | 179 | 180 | selectEntity: function( entity ) { 181 | if( entity && entity != this.selectedEntity ) { 182 | this.selectedEntity = entity; 183 | $('#entitySettings').fadeOut(100,(function(){ 184 | this.loadEntitySettings(); 185 | $('#entitySettings').fadeIn(100); 186 | }).bind(this)); 187 | } 188 | else if( !entity ) { 189 | $('#entitySettings').fadeOut(100); 190 | $('#entityKey').blur(); 191 | $('#entityValue').blur(); 192 | } 193 | 194 | this.selectedEntity = entity; 195 | $('#entityKey').val(''); 196 | $('#entityValue').val(''); 197 | }, 198 | 199 | 200 | 201 | 202 | // ------------------------------------------------------------------------- 203 | // Creating, Deleting, Moving 204 | 205 | 206 | deleteSelectedEntity: function() { 207 | if( !this.selectedEntity ) { 208 | return false; 209 | } 210 | 211 | ig.game.undo.commitEntityDelete( this.selectedEntity ); 212 | 213 | this.removeEntity( this.selectedEntity ); 214 | this.selectEntity( null ); 215 | return true; 216 | }, 217 | 218 | 219 | removeEntity: function( ent ) { 220 | if( ent.name ) { 221 | delete this.namedEntities[ent.name]; 222 | } 223 | this.entities.erase( ent ); 224 | }, 225 | 226 | 227 | cloneSelectedEntity: function() { 228 | if( !this.selectedEntity ) { 229 | return false; 230 | } 231 | 232 | var className = this.selectedEntity._wmClassName; 233 | var settings = ig.copy(this.selectedEntity._wmSettings); 234 | if( settings.name ) { 235 | settings.name = settings.name + '_clone'; 236 | } 237 | var x = this.selectedEntity.pos.x + this.gridSize; 238 | var y = this.selectedEntity.pos.y; 239 | var newEntity = this.spawnEntity( className, x, y, settings ); 240 | newEntity._wmSettings = settings; 241 | this.selectEntity( newEntity ); 242 | 243 | ig.game.undo.commitEntityCreate( newEntity ); 244 | 245 | return true; 246 | }, 247 | 248 | 249 | dragOnSelectedEntity: function( x, y ) { 250 | if( !this.selectedEntity ) { 251 | return false; 252 | } 253 | 254 | 255 | // scale or move? 256 | if( this.selectedEntity._wmScalable && this.wasSelectedOnScaleBorder ) { 257 | this.scaleSelectedEntity( x, y ); 258 | } 259 | else { 260 | this.moveSelectedEntity( x, y ) 261 | } 262 | 263 | ig.game.undo.pushEntityEdit( this.selectedEntity ); 264 | return true; 265 | }, 266 | 267 | 268 | moveSelectedEntity: function( x, y ) { 269 | x = 270 | Math.round( (x - this.selector.offset.x ) / this.gridSize ) 271 | * this.gridSize + this.selectedEntity.offset.x; 272 | y = 273 | Math.round( (y - this.selector.offset.y ) / this.gridSize ) 274 | * this.gridSize + this.selectedEntity.offset.y; 275 | 276 | // new position? 277 | if( this.selectedEntity.pos.x != x || this.selectedEntity.pos.y != y ) { 278 | $('#entityDefinitionPosX').text( x ); 279 | $('#entityDefinitionPosY').text( y ); 280 | 281 | this.selectedEntity.pos.x = x; 282 | this.selectedEntity.pos.y = y; 283 | } 284 | }, 285 | 286 | 287 | scaleSelectedEntity: function( x, y ) { 288 | var scale = this.wasSelectedOnScaleBorder; 289 | 290 | var w = Math.round( x / this.gridSize ) * this.gridSize - this.selectedEntity.pos.x; 291 | 292 | if( !this.selectedEntity._wmSettings.size ) { 293 | this.selectedEntity._wmSettings.size = {}; 294 | } 295 | 296 | if( scale == 'n' ) { 297 | var h = this.selectedEntity.pos.y - Math.round( y / this.gridSize ) * this.gridSize; 298 | if( this.selectedEntity.size.y + h <= this.gridSize ) { 299 | h = (this.selectedEntity.size.y - this.gridSize) * -1; 300 | } 301 | this.selectedEntity.size.y += h; 302 | this.selectedEntity.pos.y -= h; 303 | } 304 | else if( scale == 's' ) { 305 | var h = Math.round( y / this.gridSize ) * this.gridSize - this.selectedEntity.pos.y; 306 | this.selectedEntity.size.y = Math.max( this.gridSize, h ); 307 | } 308 | else if( scale == 'e' ) { 309 | var w = Math.round( x / this.gridSize ) * this.gridSize - this.selectedEntity.pos.x; 310 | this.selectedEntity.size.x = Math.max( this.gridSize, w ); 311 | } 312 | else if( scale == 'w' ) { 313 | var w = this.selectedEntity.pos.x - Math.round( x / this.gridSize ) * this.gridSize; 314 | if( this.selectedEntity.size.x + w <= this.gridSize ) { 315 | w = (this.selectedEntity.size.x - this.gridSize) * -1; 316 | } 317 | this.selectedEntity.size.x += w; 318 | this.selectedEntity.pos.x -= w; 319 | } 320 | this.selectedEntity._wmSettings.size.x = this.selectedEntity.size.x; 321 | this.selectedEntity._wmSettings.size.y = this.selectedEntity.size.y; 322 | 323 | this.loadEntitySettings(); 324 | }, 325 | 326 | 327 | newEntityClick: function( ev ) { 328 | this.hideMenu(); 329 | var newEntity = this.spawnEntity( ev.target.id, 0, 0, {} ); 330 | this.selectEntity( newEntity ); 331 | this.selector.offset.x = this.selector.offset.y = 0; 332 | this.moveSelectedEntity( this.selector.pos.x, this.selector.pos.y ); 333 | ig.editor.setModified(); 334 | 335 | ig.game.undo.commitEntityCreate( newEntity ); 336 | }, 337 | 338 | 339 | spawnEntity: function( className, x, y, settings ) { 340 | settings = settings || {}; 341 | var entityClass = ig.global[ className ]; 342 | if( entityClass ) { 343 | var newEntity = new (entityClass)( x, y, settings ); 344 | newEntity._wmInEditor = true; 345 | newEntity._wmClassName = className; 346 | newEntity._wmSettings = {}; 347 | for( var s in settings ) { 348 | newEntity._wmSettings[s] = settings[s]; 349 | } 350 | this.entities.push( newEntity ); 351 | if( settings.name ) { 352 | this.namedEntities[settings.name] = newEntity; 353 | } 354 | this.sort(); 355 | return newEntity; 356 | } 357 | return null; 358 | }, 359 | 360 | 361 | isOnScaleBorder: function( entity, selector ) { 362 | var border = 2; 363 | var w = selector.pos.x - entity.pos.x; 364 | var h = selector.pos.y - entity.pos.y; 365 | 366 | if( w < border ) return 'w'; 367 | if( w > entity.size.x - border ) return 'e'; 368 | 369 | if( h < border ) return 'n'; 370 | if( h > entity.size.y - border ) return 's'; 371 | 372 | return false; 373 | }, 374 | 375 | 376 | 377 | 378 | // ------------------------------------------------------------------------- 379 | // Settings 380 | 381 | 382 | loadEntitySettings: function() { 383 | 384 | if( !this.selectedEntity ) { 385 | return; 386 | } 387 | var html = 388 | '
x:'+this.selectedEntity.pos.x+'
' 389 | + '
y:'+this.selectedEntity.pos.y+'
'; 390 | 391 | html += this.loadEntitySettingsRecursive( this.selectedEntity._wmSettings ); 392 | this.entityDefinitions.html( html ); 393 | 394 | var className = this.selectedEntity._wmClassName.replace(/^Entity/, ''); 395 | $('#entityClass').text( className ); 396 | 397 | $('.entityDefinition').bind( 'mouseup', this.selectEntitySetting ); 398 | }, 399 | 400 | 401 | loadEntitySettingsRecursive: function( settings, path ) { 402 | path = path || ""; 403 | var html = ""; 404 | for( var key in settings ) { 405 | var value = settings[key]; 406 | if( typeof(value) == 'object' ) { 407 | html += this.loadEntitySettingsRecursive( value, path + key + "." ); 408 | } 409 | else { 410 | html += '
'+path+key+':'+value+'
'; 411 | } 412 | } 413 | 414 | return html; 415 | }, 416 | 417 | 418 | setEntitySetting: function( ev ) { 419 | if( ev.which != 13 ) { 420 | return true; 421 | } 422 | var key = $('#entityKey').val(); 423 | var value = $('#entityValue').val(); 424 | var floatVal = parseFloat(value); 425 | if( value == floatVal ) { 426 | value = floatVal; 427 | } 428 | 429 | if( key == 'name' ) { 430 | if( this.selectedEntity.name ) { 431 | delete this.namedEntities[this.selectedEntity.name]; 432 | } 433 | this.namedEntities[ value ] = this.selectedEntity; 434 | } 435 | 436 | if( key == 'x' ) { 437 | this.selectedEntity.pos.x = Math.round(value); 438 | } 439 | else if( key == 'y' ) { 440 | this.selectedEntity.pos.y = Math.round(value); 441 | } 442 | else { 443 | this.writeSettingAtPath( this.selectedEntity._wmSettings, key, value ); 444 | ig.merge( this.selectedEntity, this.selectedEntity._wmSettings ); 445 | } 446 | 447 | this.sort(); 448 | 449 | ig.game.setModified(); 450 | ig.game.draw(); 451 | 452 | $('#entityKey').val(''); 453 | $('#entityValue').val(''); 454 | $('#entityValue').blur(); 455 | this.loadEntitySettings(); 456 | 457 | $('#entityKey').focus(); 458 | return false; 459 | }, 460 | 461 | 462 | writeSettingAtPath: function( root, path, value ) { 463 | path = path.split('.'); 464 | var cur = root; 465 | for( var i = 0; i < path.length; i++ ) { 466 | var n = path[i]; 467 | if( i < path.length-1 && typeof(cur[n]) != 'object' ) { 468 | cur[n] = {}; 469 | } 470 | 471 | if( i == path.length-1 ) { 472 | cur[n] = value; 473 | } 474 | cur = cur[n]; 475 | } 476 | 477 | this.trimObject( root ); 478 | }, 479 | 480 | 481 | trimObject: function( obj ) { 482 | var isEmpty = true; 483 | for( var i in obj ) { 484 | if( 485 | (obj[i] === "") || 486 | (typeof(obj[i]) == 'object' && this.trimObject(obj[i])) 487 | ) { 488 | delete obj[i]; 489 | } 490 | 491 | if( typeof(obj[i]) != 'undefined' ) { 492 | isEmpty = false; 493 | } 494 | } 495 | 496 | return isEmpty; 497 | }, 498 | 499 | 500 | selectEntitySetting: function( ev ) { 501 | $('#entityKey').val( $(this).children('.key').text() ); 502 | $('#entityValue').val( $(this).children('.value').text() ); 503 | $('#entityValue').select(); 504 | }, 505 | 506 | 507 | 508 | 509 | 510 | 511 | // ------------------------------------------------------------------------- 512 | // UI 513 | 514 | setHotkey: function( hotkey ) { 515 | this.hotkey = hotkey; 516 | this.div.attr('title', 'Select Layer ('+this.hotkey+')' ); 517 | }, 518 | 519 | 520 | showMenu: function( x, y ) { 521 | this.selector.pos = { 522 | x: Math.round( (x + ig.editor.screen.x) / this.gridSize ) * this.gridSize, 523 | y: Math.round( (y + ig.editor.screen.y) / this.gridSize ) * this.gridSize 524 | }; 525 | this.menu.css({top: (y * ig.system.scale + 2), left: (x * ig.system.scale + 2) }); 526 | this.menu.show(); 527 | }, 528 | 529 | 530 | hideMenu: function() { 531 | ig.editor.mode = ig.editor.MODE.DEFAULT; 532 | this.menu.hide(); 533 | }, 534 | 535 | 536 | setActive: function( active ) { 537 | this.active = active; 538 | if( active ) { 539 | this.div.addClass( 'layerActive' ); 540 | } else { 541 | this.div.removeClass( 'layerActive' ); 542 | } 543 | }, 544 | 545 | 546 | toggleVisibility: function() { 547 | this.visible ^= 1; 548 | if( this.visible ) { 549 | this.div.children('.visible').addClass('checkedVis'); 550 | } else { 551 | this.div.children('.visible').removeClass('checkedVis'); 552 | } 553 | ig.game.draw(); 554 | }, 555 | 556 | 557 | toggleVisibilityClick: function( ev ) { 558 | if( !this.active ) { 559 | this.ignoreLastClick = true; 560 | } 561 | this.toggleVisibility() 562 | }, 563 | 564 | 565 | click: function() { 566 | if( this.ignoreLastClick ) { 567 | this.ignoreLastClick = false; 568 | return; 569 | } 570 | ig.editor.setActiveLayer( 'entities' ); 571 | }, 572 | 573 | 574 | mousemove: function( x, y ) { 575 | this.selector.pos = { x: x, y: y }; 576 | 577 | if( this.selectedEntity ) { 578 | if( this.selectedEntity._wmScalable && this.selectedEntity.touches(this.selector) ) { 579 | var scale = this.isOnScaleBorder( this.selectedEntity, this.selector ); 580 | if( scale == 'n' || scale == 's' ) { 581 | $('body').css('cursor', 'ns-resize'); 582 | return; 583 | } 584 | else if( scale == 'e' || scale == 'w' ) { 585 | $('body').css('cursor', 'ew-resize'); 586 | return; 587 | } 588 | } 589 | } 590 | 591 | $('body').css('cursor', 'default'); 592 | }, 593 | 594 | 595 | 596 | 597 | 598 | 599 | // ------------------------------------------------------------------------- 600 | // Drawing 601 | 602 | 603 | draw: function() { 604 | if( this.visible ) { 605 | for( var i = 0; i < this.entities.length; i++ ) { 606 | this.drawEntity( this.entities[i] ); 607 | } 608 | } 609 | }, 610 | 611 | 612 | drawEntity: function( ent ) { 613 | 614 | // entity itself 615 | ent.draw(); 616 | 617 | // box 618 | if( ent._wmDrawBox ) { 619 | ig.system.context.fillStyle = ent._wmBoxColor || 'rgba(128, 128, 128, 0.9)'; 620 | ig.system.context.fillRect( 621 | ig.system.getDrawPos(ent.pos.x - ig.game.screen.x), 622 | ig.system.getDrawPos(ent.pos.y - ig.game.screen.y), 623 | ent.size.x * ig.system.scale, 624 | ent.size.y * ig.system.scale 625 | ); 626 | } 627 | 628 | 629 | if( wm.config.labels.draw ) { 630 | // description 631 | var className = ent._wmClassName.replace(/^Entity/, ''); 632 | var description = className + (ent.name ? ': ' + ent.name : '' ); 633 | 634 | // text-shadow 635 | ig.system.context.fillStyle = 'rgba(0,0,0,0.4)'; 636 | ig.system.context.fillText( 637 | description, 638 | ig.system.getDrawPos(ent.pos.x - ig.game.screen.x), 639 | ig.system.getDrawPos(ent.pos.y - ig.game.screen.y + 0.5) 640 | ); 641 | 642 | // text 643 | ig.system.context.fillStyle = wm.config.colors.primary; 644 | ig.system.context.fillText( 645 | description, 646 | ig.system.getDrawPos(ent.pos.x - ig.game.screen.x), 647 | ig.system.getDrawPos(ent.pos.y - ig.game.screen.y) 648 | ); 649 | } 650 | 651 | 652 | // line to targets 653 | if( typeof(ent.target) == 'object' ) { 654 | for( var t in ent.target ) { 655 | this.drawLineToTarget( ent, ent.target[t] ); 656 | } 657 | } 658 | }, 659 | 660 | 661 | drawLineToTarget: function( ent, target ) { 662 | target = ig.game.getEntityByName( target ); 663 | if( !target ) { 664 | return; 665 | } 666 | 667 | ig.system.context.strokeStyle = '#fff'; 668 | ig.system.context.lineWidth = 1; 669 | 670 | ig.system.context.beginPath(); 671 | ig.system.context.moveTo( 672 | ig.system.getDrawPos(ent.pos.x + ent.size.x/2 - ig.game.screen.x), 673 | ig.system.getDrawPos(ent.pos.y + ent.size.y/2 - ig.game.screen.y) 674 | ); 675 | ig.system.context.lineTo( 676 | ig.system.getDrawPos(target.pos.x + target.size.x/2 - ig.game.screen.x), 677 | ig.system.getDrawPos(target.pos.y + target.size.y/2 - ig.game.screen.y) 678 | ); 679 | ig.system.context.stroke(); 680 | ig.system.context.closePath(); 681 | }, 682 | 683 | 684 | drawCursor: function( x, y ) { 685 | if( this.selectedEntity ) { 686 | ig.system.context.lineWidth = 1; 687 | ig.system.context.strokeStyle = wm.config.colors.highlight; 688 | ig.system.context.strokeRect( 689 | ig.system.getDrawPos(this.selectedEntity.pos.x - ig.editor.screen.x) - 0.5, 690 | ig.system.getDrawPos(this.selectedEntity.pos.y - ig.editor.screen.y) - 0.5, 691 | this.selectedEntity.size.x * ig.system.scale + 1, 692 | this.selectedEntity.size.y * ig.system.scale + 1 693 | ); 694 | } 695 | } 696 | }); 697 | 698 | }); 699 | -------------------------------------------------------------------------------- /lib/weltmeister/edit-map.js: -------------------------------------------------------------------------------- 1 | ig.module( 2 | 'weltmeister.edit-map' 3 | ) 4 | .requires( 5 | 'impact.background-map', 6 | 'weltmeister.tile-select' 7 | ) 8 | .defines(function(){ "use strict"; 9 | 10 | wm.EditMap = ig.BackgroundMap.extend({ 11 | name: '', 12 | visible: true, 13 | active: true, 14 | linkWithCollision: false, 15 | 16 | div: null, 17 | brush: [[0]], 18 | oldData: null, 19 | hotkey: -1, 20 | ignoreLastClick: false, 21 | tileSelect: null, 22 | 23 | isSelecting: false, 24 | selectionBegin: null, 25 | 26 | init: function( name, tilesize, tileset, foreground ) { 27 | this.name = name; 28 | this.parent( tilesize, [[0]], tileset || '' ); 29 | this.foreground = foreground; 30 | 31 | this.div = $( '
', { 32 | 'class': 'layer layerActive', 33 | 'id': ('layer_' + name), 34 | 'mouseup': this.click.bind(this) 35 | }); 36 | this.setName( name ); 37 | if( this.foreground ) { 38 | $('#layers').prepend( this.div ); 39 | } 40 | else { 41 | $('#layerEntities').after( this.div ); 42 | } 43 | 44 | this.tileSelect = new wm.TileSelect( this ); 45 | }, 46 | 47 | 48 | getSaveData: function() { 49 | return { 50 | name: this.name, 51 | width: this.width, 52 | height: this.height, 53 | linkWithCollision: this.linkWithCollision, 54 | visible: this.visible, 55 | tilesetName: this.tilesetName, 56 | repeat: this.repeat, 57 | preRender: this.preRender, 58 | distance: this.distance, 59 | tilesize: this.tilesize, 60 | foreground: this.foreground, 61 | data: this.data 62 | }; 63 | }, 64 | 65 | 66 | resize: function( newWidth, newHeight ) { 67 | var newData = new Array( newHeight ); 68 | for( var y = 0; y < newHeight; y++ ) { 69 | newData[y] = new Array( newWidth ); 70 | for( var x = 0; x < newWidth; x++ ) { 71 | newData[y][x] = (x < this.width && y < this.height) ? this.data[y][x] : 0; 72 | } 73 | } 74 | this.data = newData; 75 | this.width = newWidth; 76 | this.height = newHeight; 77 | 78 | this.resetDiv(); 79 | }, 80 | 81 | beginEditing: function() { 82 | this.oldData = ig.copy(this.data); 83 | }, 84 | 85 | getOldTile: function( x, y ) { 86 | var tx = Math.floor( x / this.tilesize ); 87 | var ty = Math.floor( y / this.tilesize ); 88 | if( 89 | (tx >= 0 && tx < this.width) && 90 | (ty >= 0 && ty < this.height) 91 | ) { 92 | return this.oldData[ty][tx]; 93 | } 94 | else { 95 | return 0; 96 | } 97 | }, 98 | 99 | setTileset: function( tileset ) { 100 | if( this.name == 'collision' ) { 101 | this.setCollisionTileset(); 102 | } 103 | else { 104 | this.parent( tileset ); 105 | } 106 | }, 107 | 108 | 109 | setCollisionTileset: function() { 110 | var path = wm.config.collisionTiles.path; 111 | var scale = this.tilesize / wm.config.collisionTiles.tilesize; 112 | this.tiles = new ig.AutoResizedImage( path, scale ); 113 | }, 114 | 115 | 116 | 117 | 118 | 119 | // ------------------------------------------------------------------------- 120 | // UI 121 | 122 | setHotkey: function( hotkey ) { 123 | this.hotkey = hotkey; 124 | this.setName( this.name ); 125 | }, 126 | 127 | 128 | setName: function( name ) { 129 | this.name = name.replace(/[^0-9a-zA-Z]/g, '_'); 130 | this.resetDiv(); 131 | }, 132 | 133 | 134 | resetDiv: function() { 135 | var visClass = this.visible ? ' checkedVis' : ''; 136 | this.div.html( 137 | '' + 138 | '' + this.name + '' + 139 | ' (' + this.width + 'x' + this.height + ')' 140 | ); 141 | this.div.attr('title', 'Select Layer ('+this.hotkey+')' ); 142 | this.div.children('.visible').bind('mousedown', this.toggleVisibilityClick.bind(this) ); 143 | }, 144 | 145 | 146 | setActive: function( active ) { 147 | this.active = active; 148 | if( active ) { 149 | this.div.addClass( 'layerActive' ); 150 | } else { 151 | this.div.removeClass( 'layerActive' ); 152 | } 153 | }, 154 | 155 | 156 | toggleVisibility: function() { 157 | this.visible = !this.visible; 158 | this.resetDiv(); 159 | if( this.visible ) { 160 | this.div.children('.visible').addClass('checkedVis'); 161 | } else { 162 | this.div.children('.visible').removeClass('checkedVis'); 163 | } 164 | ig.game.draw(); 165 | }, 166 | 167 | 168 | toggleVisibilityClick: function( event ) { 169 | if( !this.active ) { 170 | this.ignoreLastClick = true; 171 | } 172 | this.toggleVisibility() 173 | }, 174 | 175 | 176 | click: function() { 177 | if( this.ignoreLastClick ) { 178 | this.ignoreLastClick = false; 179 | return; 180 | } 181 | ig.editor.setActiveLayer( this.name ); 182 | }, 183 | 184 | 185 | destroy: function() { 186 | this.div.remove(); 187 | }, 188 | 189 | 190 | 191 | // ------------------------------------------------------------------------- 192 | // Selecting 193 | 194 | beginSelecting: function( x, y ) { 195 | this.isSelecting = true; 196 | this.selectionBegin = {x:x, y:y}; 197 | }, 198 | 199 | 200 | endSelecting: function( x, y ) { 201 | var r = this.getSelectionRect( x, y); 202 | 203 | var brush = []; 204 | for( var ty = r.y; ty < r.y+r.h; ty++ ) { 205 | var row = []; 206 | for( var tx = r.x; tx < r.x+r.w; tx++ ) { 207 | if( tx < 0 || ty < 0 || tx >= this.width || ty >= this.height ) { 208 | row.push( 0 ); 209 | } 210 | else { 211 | row.push( this.data[ty][tx] ); 212 | } 213 | } 214 | brush.push( row ); 215 | } 216 | this.isSelecting = false; 217 | this.selectionBegin = null; 218 | return brush; 219 | }, 220 | 221 | 222 | getSelectionRect: function( x, y ) { 223 | var sx = this.selectionBegin ? this.selectionBegin.x : x, 224 | sy = this.selectionBegin ? this.selectionBegin.y : y; 225 | 226 | var 227 | txb = Math.floor( (sx + this.scroll.x) / this.tilesize ), 228 | tyb = Math.floor( (sy + this.scroll.y) / this.tilesize ), 229 | txe = Math.floor( (x + this.scroll.x) / this.tilesize ), 230 | tye = Math.floor( (y + this.scroll.y) / this.tilesize ); 231 | 232 | return { 233 | x: Math.min( txb, txe ), 234 | y: Math.min( tyb, tye ), 235 | w: Math.abs( txb - txe) + 1, 236 | h: Math.abs( tyb - tye) + 1 237 | } 238 | }, 239 | 240 | 241 | 242 | 243 | // ------------------------------------------------------------------------- 244 | // Drawing 245 | 246 | draw: function() { 247 | // For performance reasons, repeated background maps are not drawn 248 | // when zoomed out 249 | if( this.visible && !(wm.config.view.zoom < 1 && this.repeat) ) { 250 | this.drawTiled(); 251 | } 252 | 253 | // Grid 254 | if( this.active && wm.config.view.grid ) { 255 | 256 | var x = -ig.system.getDrawPos(this.scroll.x % this.tilesize) - 0.5; 257 | var y = -ig.system.getDrawPos(this.scroll.y % this.tilesize) - 0.5; 258 | var step = this.tilesize * ig.system.scale; 259 | 260 | ig.system.context.beginPath(); 261 | for( x; x < ig.system.realWidth; x += step ) { 262 | ig.system.context.moveTo( x, 0 ); 263 | ig.system.context.lineTo( x, ig.system.realHeight ); 264 | } 265 | for( y; y < ig.system.realHeight; y += step ) { 266 | ig.system.context.moveTo( 0, y ); 267 | ig.system.context.lineTo( ig.system.realWidth, y ); 268 | } 269 | ig.system.context.strokeStyle = wm.config.colors.secondary; 270 | ig.system.context.stroke(); 271 | ig.system.context.closePath(); 272 | 273 | // Not calling beginPath() again has some weird performance issues 274 | // in Firefox 5. closePath has no effect. So to make it happy: 275 | ig.system.context.beginPath(); 276 | } 277 | 278 | // Bounds 279 | if( this.active ) { 280 | ig.system.context.lineWidth = 1; 281 | ig.system.context.strokeStyle = wm.config.colors.primary; 282 | ig.system.context.strokeRect( 283 | -ig.system.getDrawPos(this.scroll.x) - 0.5, 284 | -ig.system.getDrawPos(this.scroll.y) - 0.5, 285 | this.width * this.tilesize * ig.system.scale + 1, 286 | this.height * this.tilesize * ig.system.scale + 1 287 | ); 288 | } 289 | }, 290 | 291 | getCursorOffset: function() { 292 | var w = this.brush[0].length; 293 | var h = this.brush.length; 294 | 295 | //return {x:0, y:0}; 296 | return { 297 | x: (w/2-0.5).toInt() * this.tilesize, 298 | y: (h/2-0.5).toInt() * this.tilesize 299 | } 300 | }, 301 | 302 | drawCursor: function( x, y ) { 303 | if( this.isSelecting ) { 304 | var r = this.getSelectionRect( x, y); 305 | 306 | ig.system.context.lineWidth = 1; 307 | ig.system.context.strokeStyle = wm.config.colors.selection; 308 | ig.system.context.strokeRect( 309 | (r.x * this.tilesize - this.scroll.x) * ig.system.scale - 0.5, 310 | (r.y * this.tilesize - this.scroll.y) * ig.system.scale - 0.5, 311 | r.w * this.tilesize * ig.system.scale + 1, 312 | r.h * this.tilesize * ig.system.scale + 1 313 | ); 314 | } 315 | else { 316 | var w = this.brush[0].length; 317 | var h = this.brush.length; 318 | 319 | var co = this.getCursorOffset(); 320 | 321 | var cx = Math.floor( (x+this.scroll.x) / this.tilesize ) * this.tilesize - this.scroll.x - co.x; 322 | var cy = Math.floor( (y+this.scroll.y) / this.tilesize ) * this.tilesize - this.scroll.y - co.y; 323 | 324 | ig.system.context.lineWidth = 1; 325 | ig.system.context.strokeStyle = wm.config.colors.primary; 326 | ig.system.context.strokeRect( 327 | ig.system.getDrawPos(cx)-0.5, 328 | ig.system.getDrawPos(cy)-0.5, 329 | w * this.tilesize * ig.system.scale + 1, 330 | h * this.tilesize * ig.system.scale + 1 331 | ); 332 | 333 | ig.system.context.globalAlpha = 0.5; 334 | for( var ty = 0; ty < h; ty++ ) { 335 | for( var tx = 0; tx < w; tx++ ) { 336 | var t = this.brush[ty][tx]; 337 | if( t ) { 338 | var px = cx + tx * this.tilesize; 339 | var py = cy + ty * this.tilesize; 340 | this.tiles.drawTile( px, py, t-1, this.tilesize ); 341 | } 342 | } 343 | } 344 | ig.system.context.globalAlpha = 1; 345 | } 346 | } 347 | }); 348 | 349 | 350 | ig.AutoResizedImage = ig.Image.extend({ 351 | internalScale: 1, 352 | 353 | staticInstantiate: function() { 354 | return null; // Never cache! 355 | }, 356 | 357 | init: function( path, internalScale ) { 358 | this.internalScale = internalScale; 359 | this.parent( path ); 360 | }, 361 | 362 | onload: function( event ) { 363 | this.width = Math.ceil(this.data.width * this.internalScale); 364 | this.height = Math.ceil(this.data.height * this.internalScale); 365 | 366 | if( this.internalScale != 1 ) { 367 | var scaled = ig.$new('canvas'); 368 | scaled.width = this.width; 369 | scaled.height = this.height; 370 | var scaledCtx = scaled.getContext('2d'); 371 | 372 | scaledCtx.drawImage( this.data, 0, 0, this.data.width, this.data.height, 0, 0, this.width , this.height ); 373 | this.data = scaled; 374 | } 375 | 376 | this.loaded = true; 377 | if( ig.system.scale != 1 ) { 378 | this.resize( ig.system.scale ); 379 | } 380 | 381 | if( this.loadCallback ) { 382 | this.loadCallback( this.path, true ); 383 | } 384 | } 385 | }); 386 | 387 | 388 | }); -------------------------------------------------------------------------------- /lib/weltmeister/entities.js: -------------------------------------------------------------------------------- 1 | ig.module( 2 | 'weltmeister.entityLoader' 3 | ) 4 | .requires( 5 | 'weltmeister.config' 6 | ) 7 | .defines(function(){ "use strict"; 8 | 9 | // Load the list of entity files via AJAX PHP glob 10 | var path = wm.config.api.glob + '?', 11 | globs = typeof wm.config.project.entityFiles == 'string' ? 12 | [wm.config.project.entityFiles] : 13 | wm.config.project.entityFiles; 14 | 15 | for (var i = 0; i < globs.length; i++) { 16 | path += 'glob[]=' + encodeURIComponent(globs[i]) + '&'; 17 | } 18 | 19 | path += 'nocache=' + Math.random(); 20 | 21 | var req = $.ajax({ 22 | url: path, 23 | method: 'get', 24 | dataType: 'json', 25 | 26 | // MUST load synchronous, as the engine would otherwise determine that it 27 | // can't resolve dependencies to weltmeister.entities when there are 28 | // no more files to load and weltmeister.entities is still not defined 29 | // because the ajax request hasn't finished yet. 30 | // FIXME FFS! 31 | async: false, 32 | success: function(files) { 33 | 34 | // File names to Module names 35 | var moduleNames = []; 36 | var modules = {}; 37 | for( var i = 0; i < files.length; i++ ) { 38 | var name = files[i] 39 | .replace(new RegExp("^"+ig.lib+"|\\.js$", "g"), '') 40 | .replace(/\//g, '.'); 41 | moduleNames.push( name ); 42 | modules[name] = files[i]; 43 | } 44 | 45 | // Define a Module that requires all entity Modules 46 | ig.module('weltmeister.entities') 47 | .requires.apply(ig, moduleNames) 48 | .defines(function(){ wm.entityModules = modules; }); 49 | }, 50 | error: function( xhr, status, error ){ 51 | throw( 52 | "Failed to load entity list via glob.php: " + error + "\n" + 53 | xhr.responseText 54 | ); 55 | } 56 | }); 57 | 58 | }); -------------------------------------------------------------------------------- /lib/weltmeister/evented-input.js: -------------------------------------------------------------------------------- 1 | ig.module( 2 | 'weltmeister.evented-input' 3 | ) 4 | .requires( 5 | 'impact.input' 6 | ) 7 | .defines(function(){ "use strict"; 8 | 9 | wm.EventedInput = ig.Input.extend({ 10 | mousemoveCallback: null, 11 | keyupCallback: null, 12 | keydownCallback: null, 13 | 14 | delayedKeyup: {push:function(){},length: 0}, 15 | 16 | 17 | keydown: function( event ) { 18 | var tag = event.target.tagName; 19 | if( tag == 'INPUT' || tag == 'TEXTAREA' ) { return; } 20 | 21 | var code = event.type == 'keydown' 22 | ? event.keyCode 23 | : (event.button == 2 ? ig.KEY.MOUSE2 : ig.KEY.MOUSE1); 24 | var action = this.bindings[code]; 25 | if( action ) { 26 | if( !this.actions[action] ) { 27 | this.actions[action] = true; 28 | if( this.keydownCallback ) { 29 | this.keydownCallback( action ); 30 | } 31 | } 32 | event.stopPropagation(); 33 | event.preventDefault(); 34 | } 35 | }, 36 | 37 | 38 | keyup: function( event ) { 39 | var tag = event.target.tagName; 40 | if( tag == 'INPUT' || tag == 'TEXTAREA' ) { return; } 41 | 42 | var code = event.type == 'keyup' 43 | ? event.keyCode 44 | : (event.button == 2 ? ig.KEY.MOUSE2 : ig.KEY.MOUSE1); 45 | var action = this.bindings[code]; 46 | if( action ) { 47 | this.actions[action] = false; 48 | if( this.keyupCallback ) { 49 | this.keyupCallback( action ); 50 | } 51 | event.stopPropagation(); 52 | event.preventDefault(); 53 | } 54 | }, 55 | 56 | 57 | mousewheel: function( event ) { 58 | var delta = event.wheelDelta ? event.wheelDelta : (event.detail * -1); 59 | var code = delta > 0 ? ig.KEY.MWHEEL_UP : ig.KEY.MWHEEL_DOWN; 60 | var action = this.bindings[code]; 61 | if( action ) { 62 | if( this.keyupCallback ) { 63 | this.keyupCallback( action ); 64 | } 65 | event.stopPropagation(); 66 | event.preventDefault(); 67 | } 68 | }, 69 | 70 | 71 | mousemove: function( event ) { 72 | this.parent( event ); 73 | if( this.mousemoveCallback ) { 74 | this.mousemoveCallback(); 75 | } 76 | } 77 | }); 78 | 79 | }); -------------------------------------------------------------------------------- /lib/weltmeister/modal-dialogs.js: -------------------------------------------------------------------------------- 1 | ig.module( 2 | 'weltmeister.modal-dialogs' 3 | ) 4 | .requires( 5 | 'weltmeister.select-file-dropdown' 6 | ) 7 | .defines(function(){ "use strict"; 8 | 9 | wm.ModalDialog = ig.Class.extend({ 10 | onOk: null, 11 | onCancel: null, 12 | 13 | text: '', 14 | okText: '', 15 | cancelText: '', 16 | 17 | background: null, 18 | dialogBox: null, 19 | buttonDiv: null, 20 | 21 | init: function( text, okText, cancelText ) { 22 | this.text = text; 23 | this.okText = okText || 'OK'; 24 | this.cancelText = cancelText || 'Cancel'; 25 | 26 | this.background = $('
', {'class':'modalDialogBackground'}); 27 | this.dialogBox = $('
', {'class':'modalDialogBox'}); 28 | this.background.append( this.dialogBox ); 29 | $('body').append( this.background ); 30 | 31 | this.initDialog(); 32 | }, 33 | 34 | 35 | initDialog: function() { 36 | this.buttonDiv = $('
', {'class': 'modalDialogButtons'} ); 37 | var okButton = $('', {'type': 'button', 'class':'button', 'value': this.okText}); 38 | var cancelButton = $('', {'type': 'button', 'class':'button', 'value': this.cancelText}); 39 | 40 | okButton.bind( 'click', this.clickOk.bind(this) ); 41 | cancelButton.bind( 'click', this.clickCancel.bind(this) ); 42 | 43 | this.buttonDiv.append( okButton ).append( cancelButton ); 44 | 45 | this.dialogBox.html('
' + this.text + '
' ); 46 | this.dialogBox.append( this.buttonDiv ); 47 | }, 48 | 49 | 50 | clickOk: function() { 51 | if( this.onOk ) { this.onOk(this); } 52 | this.close(); 53 | }, 54 | 55 | 56 | clickCancel: function() { 57 | if( this.onCancel ) { this.onCancel(this); } 58 | this.close(); 59 | }, 60 | 61 | 62 | open: function() { 63 | this.background.fadeIn(100); 64 | }, 65 | 66 | 67 | close: function() { 68 | this.background.fadeOut(100); 69 | } 70 | }); 71 | 72 | 73 | 74 | wm.ModalDialogPathSelect = wm.ModalDialog.extend({ 75 | pathDropdown: null, 76 | pathInput: null, 77 | fileType: '', 78 | 79 | init: function( text, okText, type ) { 80 | this.fileType = type || ''; 81 | this.parent( text, (okText || 'Select') ); 82 | }, 83 | 84 | 85 | setPath: function( path ) { 86 | var dir = path.replace(/\/[^\/]*$/, ''); 87 | this.pathInput.val( path ); 88 | this.pathDropdown.loadDir( dir ); 89 | }, 90 | 91 | 92 | initDialog: function() { 93 | this.parent(); 94 | this.pathInput = $('', {'type': 'text', 'class': 'modalDialogPath'} ); 95 | this.buttonDiv.before( this.pathInput ); 96 | this.pathDropdown = new wm.SelectFileDropdown( this.pathInput, wm.config.api.browse, this.fileType ); 97 | }, 98 | 99 | 100 | clickOk: function() { 101 | if( this.onOk ) { 102 | this.onOk(this, this.pathInput.val()); 103 | } 104 | this.close(); 105 | } 106 | }); 107 | 108 | }); -------------------------------------------------------------------------------- /lib/weltmeister/select-file-dropdown.js: -------------------------------------------------------------------------------- 1 | ig.module( 2 | 'weltmeister.select-file-dropdown' 3 | ) 4 | .defines(function(){ "use strict"; 5 | 6 | wm.SelectFileDropdown = ig.Class.extend({ 7 | input: null, 8 | boundShow: null, 9 | boundHide: null, 10 | div: null, 11 | filelistPHP: '', 12 | filetype: '', 13 | 14 | init: function( elementId, filelistPHP, filetype ) { 15 | this.filetype = filetype || ''; 16 | this.filelistPHP = filelistPHP; 17 | this.input = $(elementId); 18 | this.boundHide = this.hide.bind(this); 19 | this.input.bind('focus', this.show.bind(this) ); 20 | 21 | this.div = $('
', {'class':'selectFileDialog'}); 22 | this.input.after( this.div ); 23 | this.div.bind('mousedown', this.noHide.bind(this) ); 24 | 25 | this.loadDir( '' ); 26 | }, 27 | 28 | 29 | loadDir: function( dir ) { 30 | var path = this.filelistPHP + '?dir=' + encodeURIComponent( dir || '' ) + '&type=' + this.filetype; 31 | var req = $.ajax({ 32 | url:path, 33 | dataType: 'json', 34 | async: false, 35 | success:this.showFiles.bind(this) 36 | }); 37 | }, 38 | 39 | 40 | selectDir: function( event ) { 41 | this.loadDir( $(event.target).attr('href') ); 42 | return false; 43 | }, 44 | 45 | 46 | selectFile: function( event ) { 47 | this.input.val( $(event.target).attr('href') ); 48 | this.input.blur(); 49 | this.hide(); 50 | return false; 51 | }, 52 | 53 | 54 | showFiles: function( data ) { 55 | this.div.empty(); 56 | if( data.parent !== false ) { 57 | var parentDir = $('', {'class':'dir', href:data.parent, html: '…parent directory'}); 58 | parentDir.bind( 'click', this.selectDir.bind(this) ); 59 | this.div.append( parentDir ); 60 | } 61 | for( var i = 0; i < data.dirs.length; i++ ) { 62 | var name = data.dirs[i].match(/[^\/]*$/)[0] + '/'; 63 | var dir = $('', {'class':'dir', href:data.dirs[i], html: name, title: name}); 64 | dir.bind( 'click', this.selectDir.bind(this) ); 65 | this.div.append( dir ); 66 | } 67 | for( var i = 0; i < data.files.length; i++ ) { 68 | var name = data.files[i].match(/[^\/]*$/)[0]; 69 | var file = $('', {'class':'file', href:data.files[i], html: name, title: name}); 70 | file.bind( 'click', this.selectFile.bind(this) ); 71 | this.div.append( file ); 72 | } 73 | }, 74 | 75 | 76 | noHide: function(event) { 77 | event.stopPropagation(); 78 | }, 79 | 80 | 81 | show: function( event ) { 82 | var inputPos = this.input.position();//this.input.getPosition(this.input.getOffsetParent()); 83 | var inputHeight = parseInt(this.input.innerHeight()) + parseInt(this.input.css('margin-top')); 84 | var inputWidth = this.input.innerWidth(); 85 | $(document).bind( 'mousedown', this.boundHide ); 86 | this.div.css({ 87 | 'top': inputPos.top + inputHeight + 1, 88 | 'left': inputPos.left, 89 | 'width': inputWidth 90 | }).slideDown(100); 91 | }, 92 | 93 | 94 | hide: function() { 95 | $(document).unbind( 'mousedown', this.boundHide ); 96 | this.div.slideUp(100); 97 | } 98 | }); 99 | 100 | }); -------------------------------------------------------------------------------- /lib/weltmeister/tile-select.js: -------------------------------------------------------------------------------- 1 | ig.module( 2 | 'weltmeister.tile-select' 3 | ) 4 | .defines(function(){ "use strict"; 5 | 6 | wm.TileSelect = ig.Class.extend({ 7 | 8 | pos: {x:0, y:0}, 9 | 10 | layer: null, 11 | selectionBegin: null, 12 | 13 | init: function( layer ) { 14 | this.layer = layer; 15 | }, 16 | 17 | 18 | getCurrentTile: function() { 19 | var b = this.layer.brush; 20 | if( b.length == 1 && b[0].length == 1 ) { 21 | return b[0][0] - 1; 22 | } 23 | else { 24 | return -1; 25 | } 26 | }, 27 | 28 | 29 | setPosition: function( x, y ) { 30 | this.selectionBegin = null; 31 | var tile = this.getCurrentTile(); 32 | this.pos.x = 33 | Math.floor( x / this.layer.tilesize ) * this.layer.tilesize 34 | - Math.floor( tile * this.layer.tilesize ) % this.layer.tiles.width; 35 | 36 | this.pos.y = 37 | Math.floor( y / this.layer.tilesize ) * this.layer.tilesize 38 | - Math.floor( tile * this.layer.tilesize / this.layer.tiles.width ) * this.layer.tilesize 39 | - (tile == -1 ? this.layer.tilesize : 0); 40 | 41 | this.pos.x = this.pos.x.limit( 0, ig.system.width - this.layer.tiles.width - (ig.system.width % this.layer.tilesize) ); 42 | this.pos.y = this.pos.y.limit( 0, ig.system.height - this.layer.tiles.height - (ig.system.height % this.layer.tilesize) ); 43 | }, 44 | 45 | 46 | beginSelecting: function( x, y ) { 47 | this.selectionBegin = {x:x, y:y}; 48 | }, 49 | 50 | 51 | endSelecting: function( x, y ) { 52 | var r = this.getSelectionRect( x, y); 53 | 54 | var mw = Math.floor( this.layer.tiles.width / this.layer.tilesize ); 55 | var mh = Math.floor( this.layer.tiles.height / this.layer.tilesize ); 56 | 57 | var brush = []; 58 | for( var ty = r.y; ty < r.y+r.h; ty++ ) { 59 | var row = []; 60 | for( var tx = r.x; tx < r.x+r.w; tx++ ) { 61 | if( tx < 0 || ty < 0 || tx >= mw || ty >= mh) { 62 | row.push( 0 ); 63 | } 64 | else { 65 | row.push( ty * Math.floor(this.layer.tiles.width / this.layer.tilesize) + tx + 1 ); 66 | } 67 | } 68 | brush.push( row ); 69 | } 70 | this.selectionBegin = null; 71 | return brush; 72 | }, 73 | 74 | 75 | getSelectionRect: function( x, y ) { 76 | var sx = this.selectionBegin ? this.selectionBegin.x : x, 77 | sy = this.selectionBegin ? this.selectionBegin.y : y; 78 | 79 | var 80 | txb = Math.floor( (sx - this.pos.x) / this.layer.tilesize ), 81 | tyb = Math.floor( (sy - this.pos.y) / this.layer.tilesize ), 82 | txe = Math.floor( (x - this.pos.x) / this.layer.tilesize ), 83 | tye = Math.floor( (y - this.pos.y) / this.layer.tilesize ); 84 | 85 | return { 86 | x: Math.min( txb, txe ), 87 | y: Math.min( tyb, tye ), 88 | w: Math.abs( txb - txe) + 1, 89 | h: Math.abs( tyb - tye) + 1 90 | } 91 | }, 92 | 93 | 94 | draw: function() { 95 | ig.system.clear( "rgba(0,0,0,0.8)" ); 96 | if( !this.layer.tiles.loaded ) { 97 | return; 98 | } 99 | 100 | // Tileset 101 | ig.system.context.lineWidth = 1; 102 | ig.system.context.strokeStyle = wm.config.colors.secondary; 103 | ig.system.context.fillStyle = wm.config.colors.clear; 104 | ig.system.context.fillRect( 105 | this.pos.x * ig.system.scale, 106 | this.pos.y * ig.system.scale, 107 | this.layer.tiles.width * ig.system.scale, 108 | this.layer.tiles.height * ig.system.scale 109 | ); 110 | ig.system.context.strokeRect( 111 | this.pos.x * ig.system.scale - 0.5, 112 | this.pos.y * ig.system.scale - 0.5, 113 | this.layer.tiles.width * ig.system.scale + 1, 114 | this.layer.tiles.height * ig.system.scale + 1 115 | ); 116 | 117 | this.layer.tiles.draw( this.pos.x, this.pos.y ); 118 | 119 | // Selected Tile 120 | var tile = this.getCurrentTile(); 121 | var tx = Math.floor( tile * this.layer.tilesize ) % this.layer.tiles.width + this.pos.x; 122 | var ty = 123 | Math.floor( tile * this.layer.tilesize / this.layer.tiles.width ) 124 | * this.layer.tilesize + this.pos.y 125 | + (tile == -1 ? this.layer.tilesize : 0); 126 | 127 | ig.system.context.lineWidth = 1; 128 | ig.system.context.strokeStyle = wm.config.colors.highlight; 129 | ig.system.context.strokeRect( 130 | tx * ig.system.scale - 0.5, 131 | ty * ig.system.scale - 0.5, 132 | this.layer.tilesize * ig.system.scale + 1, 133 | this.layer.tilesize * ig.system.scale + 1 134 | ); 135 | }, 136 | 137 | 138 | drawCursor: function( x, y ) { 139 | var r = this.getSelectionRect( x, y); 140 | 141 | ig.system.context.lineWidth = 1; 142 | ig.system.context.strokeStyle = wm.config.colors.selection; 143 | ig.system.context.strokeRect( 144 | (r.x * this.layer.tilesize + this.pos.x) * ig.system.scale - 0.5, 145 | (r.y * this.layer.tilesize + this.pos.y) * ig.system.scale - 0.5, 146 | r.w * this.layer.tilesize * ig.system.scale + 1, 147 | r.h * this.layer.tilesize * ig.system.scale + 1 148 | ); 149 | } 150 | }); 151 | 152 | }); -------------------------------------------------------------------------------- /lib/weltmeister/undo.js: -------------------------------------------------------------------------------- 1 | ig.module( 2 | 'weltmeister.undo' 3 | ) 4 | .requires( 5 | 'weltmeister.config' 6 | ) 7 | .defines(function(){ "use strict"; 8 | 9 | 10 | wm.Undo = ig.Class.extend({ 11 | levels: null, 12 | chain: [], 13 | rpos: 0, 14 | currentAction: null, 15 | 16 | init: function( levels ) { 17 | this.levels = levels || 10; 18 | }, 19 | 20 | 21 | clear: function() { 22 | this.chain = []; 23 | this.currentAction = null; 24 | }, 25 | 26 | 27 | commit: function( action ) { 28 | if( this.rpos ) { 29 | this.chain.splice( this.chain.length - this.rpos, this.rpos ); 30 | this.rpos = 0; 31 | } 32 | action.activeLayer = ig.game.activeLayer ? ig.game.activeLayer.name : ''; 33 | this.chain.push( action ); 34 | if( this.chain.length > this.levels ) { 35 | this.chain.shift(); 36 | } 37 | }, 38 | 39 | 40 | undo: function() { 41 | var action = this.chain[ this.chain.length - this.rpos - 1 ]; 42 | if( !action ) { 43 | return; 44 | } 45 | this.rpos++; 46 | 47 | 48 | ig.game.setActiveLayer( action.activeLayer ); 49 | 50 | if( action.type == wm.Undo.MAP_DRAW ) { 51 | for( var i = 0; i < action.changes.length; i++ ) { 52 | var change = action.changes[i]; 53 | change.layer.setTile( change.x, change.y, change.old ); 54 | } 55 | } 56 | else if( action.type == wm.Undo.ENTITY_EDIT ) { 57 | action.entity.pos.x = action.old.x; 58 | action.entity.pos.y = action.old.y; 59 | action.entity.size.x = action.old.w; 60 | action.entity.size.y = action.old.h; 61 | ig.game.entities.selectEntity( action.entity ); 62 | ig.game.entities.loadEntitySettings(); 63 | } 64 | else if( action.type == wm.Undo.ENTITY_CREATE ) { 65 | ig.game.entities.removeEntity( action.entity ); 66 | ig.game.entities.selectEntity( null ); 67 | } 68 | else if( action.type == wm.Undo.ENTITY_DELETE ) { 69 | ig.game.entities.entities.push( action.entity ); 70 | if( action.entity.name ) { 71 | ig.game.entities.namedEntities[action.entity.name] = action.entity; 72 | } 73 | ig.game.entities.selectEntity( action.entity ); 74 | } 75 | 76 | ig.game.setModified(); 77 | }, 78 | 79 | 80 | redo: function() { 81 | if( !this.rpos ) { 82 | return; 83 | } 84 | 85 | var action = this.chain[ this.chain.length - this.rpos ]; 86 | if( !action ) { 87 | return; 88 | } 89 | this.rpos--; 90 | 91 | 92 | ig.game.setActiveLayer( action.activeLayer ); 93 | 94 | if( action.type == wm.Undo.MAP_DRAW ) { 95 | for( var i = 0; i < action.changes.length; i++ ) { 96 | var change = action.changes[i]; 97 | change.layer.setTile( change.x, change.y, change.current ); 98 | } 99 | } 100 | else if( action.type == wm.Undo.ENTITY_EDIT ) { 101 | action.entity.pos.x = action.current.x; 102 | action.entity.pos.y = action.current.y; 103 | action.entity.size.x = action.current.w; 104 | action.entity.size.y = action.current.h; 105 | ig.game.entities.selectEntity( action.entity ); 106 | ig.game.entities.loadEntitySettings(); 107 | } 108 | else if( action.type == wm.Undo.ENTITY_CREATE ) { 109 | ig.game.entities.entities.push( action.entity ); 110 | if( action.entity.name ) { 111 | ig.game.entities.namedEntities[action.entity.name] = action.entity; 112 | } 113 | ig.game.entities.selectEntity( action.entity ); 114 | } 115 | else if( action.type == wm.Undo.ENTITY_DELETE ) { 116 | ig.game.entities.removeEntity( action.entity ); 117 | ig.game.entities.selectEntity( null ); 118 | } 119 | 120 | ig.game.setModified(); 121 | }, 122 | 123 | 124 | // ------------------------------------------------------------------------- 125 | // Map changes 126 | 127 | beginMapDraw: function() { 128 | this.currentAction = { 129 | type: wm.Undo.MAP_DRAW, 130 | time: Date.now(), 131 | changes: [] 132 | }; 133 | }, 134 | 135 | pushMapDraw: function( layer, x, y, oldTile, currentTile ) { 136 | if( !this.currentAction ) { 137 | return; 138 | } 139 | 140 | this.currentAction.changes.push({ 141 | layer: layer, 142 | x: x, 143 | y: y, 144 | old: oldTile, 145 | current: currentTile 146 | }); 147 | }, 148 | 149 | endMapDraw: function() { 150 | if( !this.currentAction || !this.currentAction.changes.length ) { 151 | return; 152 | } 153 | 154 | this.commit( this.currentAction ); 155 | this.currentAction = null; 156 | }, 157 | 158 | 159 | // ------------------------------------------------------------------------- 160 | // Entity changes 161 | 162 | beginEntityEdit: function( entity ) { 163 | this.currentAction = { 164 | type: wm.Undo.ENTITY_EDIT, 165 | time: Date.now(), 166 | entity: entity, 167 | old: { 168 | x: entity.pos.x, 169 | y: entity.pos.y, 170 | w: entity.size.x, 171 | h: entity.size.y 172 | }, 173 | current: { 174 | x: entity.pos.x, 175 | y: entity.pos.y, 176 | w: entity.size.x, 177 | h: entity.size.y 178 | } 179 | }; 180 | }, 181 | 182 | pushEntityEdit: function( entity ) { 183 | if( !this.currentAction ) { 184 | return; 185 | } 186 | 187 | this.currentAction.current = { 188 | x: entity.pos.x, 189 | y: entity.pos.y, 190 | w: entity.size.x, 191 | h: entity.size.y 192 | }; 193 | }, 194 | 195 | 196 | endEntityEdit: function() { 197 | var a = this.currentAction; 198 | 199 | if( !a || ( 200 | a.old.x == a.current.x && a.old.y == a.current.y && 201 | a.old.w == a.current.w && a.old.h == a.current.h 202 | )) { 203 | return; 204 | } 205 | 206 | this.commit( this.currentAction ); 207 | this.currentAction = null; 208 | }, 209 | 210 | 211 | commitEntityCreate: function( entity ) { 212 | this.commit({ 213 | type: wm.Undo.ENTITY_CREATE, 214 | time: Date.now(), 215 | entity: entity 216 | }); 217 | }, 218 | 219 | 220 | commitEntityDelete: function( entity ) { 221 | this.commit({ 222 | type: wm.Undo.ENTITY_DELETE, 223 | time: Date.now(), 224 | entity: entity 225 | }); 226 | } 227 | }); 228 | 229 | wm.Undo.MAP_DRAW = 1; 230 | wm.Undo.ENTITY_EDIT = 2; 231 | wm.Undo.ENTITY_CREATE = 3; 232 | wm.Undo.ENTITY_DELETE = 4; 233 | 234 | }); -------------------------------------------------------------------------------- /lib/weltmeister/weltmeister.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #000; 3 | color: #fff; 4 | font-family: sans-serif; 5 | font-size: 10pt; 6 | margin: 0px; 7 | overflow: hidden; 8 | text-shadow: 0px 1px 1px rgba(0,0,0,0.5); 9 | -webkit-font-smoothing: antialiased; 10 | -webkit-user-select: none; 11 | } 12 | 13 | h2 { 14 | margin: 0 0 4px 0; 15 | padding: 4px 0 4px 6px; 16 | background-color: #000; 17 | font-size: 100%; 18 | color: #555; 19 | xtext-transform: uppercase; 20 | xborder-bottom: 1px solid #555; 21 | } 22 | 23 | h3 { 24 | margin: 0; 25 | font-size: 100%; 26 | display: block; 27 | } 28 | 29 | dt { 30 | margin: 0; 31 | padding: 4px 0 0 6px; 32 | display:inline; 33 | float:left; 34 | margin-right:5px; 35 | } 36 | 37 | dd { 38 | margin: 0; 39 | padding: 2px 0 8px 6px; 40 | } 41 | 42 | dl { 43 | margin:0; 44 | } 45 | 46 | div.clear { 47 | clear: both; 48 | } 49 | 50 | label { 51 | cursor: pointer; 52 | } 53 | 54 | /* --- Input ------------------------------------------------------------------ */ 55 | 56 | input { 57 | background-color: rgba(0,0,0,0.5); 58 | border: 1px solid rgb(50,50,50); 59 | color: #fff; 60 | margin: 0; 61 | font-family: sans-serif; 62 | -webkit-font-smoothing: antialiased; 63 | font-size: 10pt; 64 | outline: none; 65 | text-shadow: 0px 1px 1px rgba(0,0,0,0.5); 66 | 67 | } 68 | 69 | input:focus{ 70 | border: 1px solid rgb(200,200,200); 71 | } 72 | 73 | input.text { 74 | padding: 1px; 75 | margin: 0; 76 | } 77 | 78 | input.number { 79 | width: 30px; 80 | text-align: right; 81 | } 82 | 83 | input.button { 84 | font-size: 90%; 85 | padding-left: 13px; 86 | padding-right: 13px; 87 | padding-top: 5px; 88 | padding-bottom: 5px; 89 | color: #fff; 90 | font-weight: bold; 91 | background-color: rgba(255,255,255,0.1); 92 | border:none; 93 | border-top: 1px solid rgba(255,255,255,0.1); 94 | border-bottom: 1px solid rgba(0,0,0,0.1); 95 | cursor: pointer; 96 | -webkit-transition: 0.1s linear; 97 | } 98 | 99 | input.button:hover { 100 | background-color: rgba(255,255,255,0.2); 101 | } 102 | 103 | input.button:active { 104 | background-color: rgba(255,255,255,0.3); 105 | } 106 | 107 | input.text#layerName { 108 | width:140px; 109 | } 110 | 111 | input.text#layerTileset { 112 | width:138px; 113 | } 114 | 115 | input#levelSaveAs { margin-right: 10px; } 116 | input#levelLoad { margin-right: 10px; } 117 | input#reloadImages {margin-right: 10px;} 118 | 119 | input:disabled { 120 | background-color: #555; 121 | color: #888; 122 | } 123 | 124 | /* --- Layout ------------------------------------------------------------------ */ 125 | 126 | #editor { 127 | margin: 0px; 128 | position: relative; 129 | } 130 | 131 | #canvas { 132 | image-rendering: optimizeSpeed; 133 | -webkit-interpolation-mode: nearest-neighbor; 134 | } 135 | 136 | #menu { 137 | width: 200px; 138 | float: right; 139 | position:absolute; 140 | top:0px; 141 | right:0px; 142 | } 143 | 144 | /* --- Layers ------------------------------------------------------------------ */ 145 | 146 | #layerContainer { 147 | background-color: rgba(0,0,0,0.95); 148 | padding-right:2px; 149 | } 150 | 151 | #layers { 152 | max-height: 200px; 153 | overflow: auto; 154 | } 155 | 156 | #layerButtons div.button#buttonAddLayer { 157 | position:absolute; 158 | right: 0px; 159 | top:-5px; 160 | cursor: pointer; 161 | padding: 10px; 162 | color:rgba(255,255,255,0.5); 163 | font-weight: bold; 164 | font-size:110%; 165 | margin-left:1px; 166 | -webkit-transition: 0.1s linear; 167 | -webkit-font-smoothing: none; 168 | z-index:10; 169 | } 170 | #layerButtons div.button#buttonAddLayer:hover { color:rgba(255,255,255,1);} 171 | #layerButtons div.button#buttonAddLayer:active { color:rgba(255,255,255,1); text-shadow:none;} 172 | 173 | .layer { 174 | padding: 6px 4px; 175 | cursor: pointer; 176 | -webkit-transition: background-color 0.1s linear; 177 | border-top: 1px solid rgba(255,255,255,0); 178 | border-bottom: 1px solid rgba(255,255,255,0); 179 | } 180 | 181 | .layer:hover { 182 | background-color: rgba(255,255,255,0.1); 183 | border-top: 1px solid rgba(255,255,255,0.1); 184 | border-bottom: 1px solid rgba(255,255,255,0.1); 185 | } 186 | 187 | .layer:active { 188 | background-color: rgba(255,255,255,0.2); 189 | border-top: 1px solid rgba(255,255,255,0.2); 190 | border-bottom: 1px solid rgba(255,255,255,0.2); 191 | } 192 | 193 | .layerActive { 194 | background-color: rgba(255,255,255,0.1); 195 | border-top: 1px solid rgba(255,255,255,0.2); 196 | border-bottom: 1px solid rgba(255,255,255,0.2) !important; 197 | } 198 | 199 | 200 | #layerEntities { border-bottom: 1px solid #000; } 201 | 202 | .layer .visible { 203 | background-color: rgba(255,255,255,0.2); 204 | text-indent: -99999px; 205 | display: inline-block; 206 | width: 10px; 207 | height: 10px; 208 | margin-right: 7px; 209 | margin-left: 4px; 210 | -webkit-transition: 0.1s linear; 211 | } 212 | 213 | .layer .visible.specialVis{ 214 | margin-right: 2px; 215 | } 216 | 217 | .layer .checkedVis{ background-color: rgba(255,255,255,1); } 218 | .layer span.size { font-size: 75%; color: rgba(255,255,255,0.7); } 219 | 220 | #layerSettings { 221 | background-color: rgba(0,0,0,0.95); 222 | padding-top: 5px; 223 | margin-top: 1px; 224 | display: none; 225 | } 226 | 227 | /* --- Entities ------------------------------------------------------------------ */ 228 | h3#entityClass { 229 | border-bottom: 1px solid rgba(255,255,255,0.2); 230 | padding: 5px; 231 | padding-left: 10px; 232 | } 233 | 234 | #entitySettings { 235 | background-color: rgba(0,0,0,0.95); 236 | margin-top: 1px; 237 | display: none; 238 | } 239 | 240 | #entityDefinitions { 241 | max-height: 220px; 242 | overflow: auto; 243 | } 244 | 245 | div.entityDefinition { 246 | color: #aaa; 247 | padding: 2px 0; 248 | border-bottom: 1px solid rgba(255,255,255,0.2); 249 | cursor: pointer; 250 | } 251 | 252 | div.entityDefinition:hover { 253 | background-color: rgba(255,255,255,0.1); 254 | } 255 | 256 | div.entityDefinition .key { 257 | width: 50%; 258 | display: block; 259 | float: left; 260 | margin: 0 0px 0 0; 261 | padding: 0; 262 | text-align: right; 263 | color: #fff; 264 | overflow: hidden; 265 | } 266 | 267 | div.entityDefinition .value { 268 | padding: 0 0 0 2px; 269 | color: #fff; 270 | } 271 | 272 | dl#entityDefinitionInput { 273 | padding: 8px 0; 274 | } 275 | 276 | dl#entityDefinitionInput dt { 277 | width: 40px; 278 | display: block; 279 | float: left; 280 | margin: 0 4px 0 0; 281 | padding: 4px 0 0 0; 282 | text-align: right; 283 | } 284 | 285 | dl#entityDefinitionInput dd { 286 | display: block; 287 | margin: 0; 288 | padding: 2px 0; 289 | } 290 | 291 | #entityKey, #entityValue { 292 | } 293 | 294 | #entityMenu { 295 | background-color: rgba(0,0,0,0.9); 296 | display: none; 297 | position: absolute; 298 | min-width: 100px; 299 | max-height:300px; 300 | overflow-y: scroll; 301 | z-index: 1000; 302 | } 303 | 304 | #entityMenu div { 305 | padding: 3px; 306 | padding-left: 8px; 307 | color: #fff; 308 | cursor: pointer; 309 | border-top: 1px solid rgba(255,255,255,0); 310 | border-bottom: 1px solid rgba(255,255,255,0); 311 | -webkit-transition: 0.1s linear; 312 | } 313 | 314 | #entityMenu div:hover { 315 | background-color: rgba(255,255,255,0.2); 316 | border-top: 1px solid rgba(255,255,255,0.2); 317 | border-bottom: 1px solid rgba(255,255,255,0.2); 318 | } 319 | 320 | /* --- Dialogs ------------------------------------------------------------------ */ 321 | 322 | .selectFileDialog { 323 | background-color: rgba(0,0,0,0.9); 324 | border: 1px solid white; 325 | border-top: 1px solid rgba(255,255,255,0.4); 326 | display: none; 327 | position: absolute; 328 | overflow: hidden; 329 | -webkit-box-shadow: 0px 0px 10px rgba(0,0,0,1); 330 | 331 | max-height: 300px; 332 | overflow-y: scroll; 333 | } 334 | 335 | .selectFileDialog a { 336 | padding: 4px; 337 | color: #fff; 338 | display: block; 339 | text-decoration: none; 340 | border-top: 1px solid rgba(255,255,255,0); 341 | border-bottom: 1px solid rgba(255,255,255,0); 342 | } 343 | 344 | .selectFileDialog a:hover { 345 | background-color: rgba(255,255,255,0.2); 346 | border-top: 1px solid rgba(255,255,255,0.2); 347 | border-bottom: 1px solid rgba(255,255,255,0.2); 348 | } 349 | 350 | div.modalDialogBackground { 351 | background-color: rgba(0,0,0,0.7); 352 | width: 100%; 353 | height: 100%; 354 | position: fixed; 355 | top: 0; 356 | left: 0; 357 | display: none; 358 | z-index: 100; 359 | } 360 | 361 | div.modalDialogBox { 362 | width: 300px; 363 | margin-left: -170px; 364 | background-color: rgba(0,0,0,0.9); 365 | border: 1px solid rgb(100,100,100); 366 | -webkit-box-shadow: 0px 0px 10px rgba(0,0,0,1); 367 | position: absolute; 368 | top: 20%; 369 | left: 50%; 370 | padding: 20px; 371 | } 372 | 373 | div.modalDialogText { 374 | font-size: 180%; 375 | font-weight: bold; 376 | } 377 | 378 | div.modalDialogButtons { 379 | margin-top: 20px; 380 | text-align: right; 381 | } 382 | 383 | div.modalDialogButtons input.button { 384 | min-width: 100px; 385 | text-align: center; 386 | margin-left: 10px; 387 | } 388 | 389 | input.modalDialogPath { 390 | margin-top: 20px; 391 | width: 100%; 392 | outline: none; 393 | } 394 | 395 | input.modalDialogPath:focus { 396 | outline: none; 397 | border: 1px solid rgb(100,100,100); 398 | } 399 | 400 | #headerMenu { 401 | position:relative; 402 | z-index:10; 403 | height:47px; 404 | width:100%; 405 | background: #131314; 406 | background: -webkit-gradient(linear, left bottom, left top, color-stop(0,#000000), color-stop(1,#2e3033)); 407 | background: -moz-linear-gradient(center bottom, #000000 0%, #2e3033 100%); 408 | background: -o-linear-gradient(#2e3033, #000000); 409 | } 410 | 411 | #headerMenu span.headerTitle { 412 | display:inline-block; 413 | font-weight:bold; 414 | font-size:200%; 415 | padding-left:20px; 416 | padding-top:7px; 417 | color:rgba(0,0,0,0.1); 418 | text-shadow:0px -1px 0px rgba(255,255,255,0.4); 419 | } 420 | 421 | #headerMenu span.unsavedTitle { 422 | display:inline-block; 423 | font-weight:bold; 424 | font-size:240%; 425 | padding-left:0px; 426 | color:#cc0000; 427 | text-shadow:0px 1px 1px rgba(0,0,0,0.1); 428 | } 429 | 430 | #headerMenu span.headerFloat { 431 | float: right; 432 | } 433 | 434 | div#zoomIndicator { 435 | font-weight: bold; 436 | font-size: 300%; 437 | position: absolute; 438 | left: 50px; 439 | top: 30px; 440 | color: #fff; 441 | display: none; 442 | } 443 | 444 | input#toggleSidebar { 445 | width: 200px; 446 | height: 47px; 447 | text-indent: -99999px; 448 | background: url(arrow.png) 50% -37px no-repeat; 449 | -webkit-transition: 0s linear; 450 | opacity: 0.25; 451 | padding: 0px; 452 | } 453 | 454 | input#toggleSidebar.active{ 455 | background-position: 50% 10px; 456 | } 457 | 458 | input[type="checkbox"] { 459 | position: relative; 460 | margin: 0; 461 | border: 0; 462 | width: 10px; 463 | height: 10px; 464 | display: inline-block; 465 | -webkit-appearance: none; 466 | -webkit-transition: 0.1s linear; 467 | } 468 | input[type="checkbox"] { 469 | background-color:rgba(255,255,255,0.2); 470 | } 471 | input[type="checkbox"]:checked { 472 | background-color: rgba(255,255,255,1); 473 | } 474 | input[type="checkbox"]:hover { 475 | cursor: pointer; 476 | } 477 | input[type="checkbox"]:disabled { 478 | background-color: rgba(255,255,255,0.1); 479 | } 480 | 481 | ::-webkit-scrollbar { width: 2px; } 482 | ::-webkit-scrollbar-button:start:decrement, 483 | ::-webkit-scrollbar-button:end:increment { display: block; height: 2px; } 484 | ::-webkit-scrollbar-button:vertical:increment { background-color: transparent; } 485 | ::-webkit-scrollbar-track-piece { background-color: rgba(0,0,0,0); } 486 | ::-webkit-scrollbar-thumb:vertical { background-color: rgba(255,255,255,1); } 487 | -------------------------------------------------------------------------------- /media/04b03.font.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoboslab/Impact/7768fd29c70ca924a78673d93081baab5a72fbe6/media/04b03.font.png -------------------------------------------------------------------------------- /tools/bake.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | :: Path to impact.js and your game's main .js 4 | SET IMPACT_LIBRARY=lib/impact/impact.js 5 | SET GAME=lib/game/main.js 6 | 7 | :: Output file 8 | SET OUTPUT_FILE=game.min.js 9 | 10 | 11 | :: Change CWD to Impact's base dir 12 | cd ../ 13 | 14 | 15 | :: Bake! 16 | php tools/bake.php %IMPACT_LIBRARY% %GAME% %OUTPUT_FILE% 17 | 18 | :: If you dont have the php.exe in your PATH uncomment the 19 | :: following line and point it to your php.exe 20 | 21 | ::c:/php/php.exe tools/bake.php %IMPACT_LIBRARY% %GAME% %OUTPUT_FILE% 22 | 23 | pause -------------------------------------------------------------------------------- /tools/bake.php: -------------------------------------------------------------------------------- 1 | \n"; 4 | echo "e.g. bake.php lib/impact/impact.js lib/game/game.js mygame-baked.js\n"; 5 | die; 6 | } 7 | 8 | $inFiles = array_slice( $argv, 1, -1 ); 9 | $outFile = $argv[ count($argv)-1 ]; 10 | 11 | $baker = new Baker( Baker::MINIFIED ); 12 | $baker->bake( $inFiles, $outFile ); 13 | 14 | 15 | class Baker { 16 | const PLAIN = 0; 17 | const MINIFIED = 1; 18 | const GZIPPED = 2; 19 | 20 | protected $base = 'lib/'; 21 | protected $format = 0; 22 | protected $loaded = array(); 23 | protected $currentInput = 'Command Line'; 24 | protected $fileCount = 0, $bytesIn = 0, $bytesOut = 0; 25 | 26 | public function __construct( $format = 0 ) { 27 | $this->format = $format; 28 | if( $this->format & self::MINIFIED ) { 29 | require_once( 'jsmin.php' ); 30 | } 31 | } 32 | 33 | 34 | public function bake( $inFiles, $outFile ) { 35 | $this->fileCount = 0; 36 | $this->bytesIn = 0; 37 | $out = "/*! Built with IMPACT - impactjs.com */\n\n"; 38 | 39 | foreach( $inFiles as $f ) { 40 | $out .= $this->load( $f ); 41 | } 42 | 43 | $bytesOut = strlen($out); 44 | $bytesOutZipped = 0; 45 | 46 | echo "writing $outFile\n"; 47 | @file_put_contents( $outFile, $out ) or 48 | die("ERROR: Couldn't write to $outFile\n"); 49 | 50 | if( $this->format & self::GZIPPED ) { 51 | $gzFile = "$outFile.gz"; 52 | echo "writing $gzFile\n"; 53 | $fh = gzopen( $gzFile, 'w9' ) or 54 | die("ERROR: Couldn't write to $gzFile\n"); 55 | 56 | gzwrite( $fh, $out ); 57 | gzclose( $fh ); 58 | $bytesOutZipped = filesize( $gzFile ); 59 | } 60 | 61 | 62 | echo 63 | "\nbaked {$this->fileCount} files: ". 64 | round($this->bytesIn/1024,1)."kb -> ".round($bytesOut/1024,1)."kb" . 65 | ( $this->format & self::GZIPPED 66 | ? " (".round($bytesOutZipped/1024,1)."kb gzipped)\n" 67 | : "\n" 68 | ); 69 | } 70 | 71 | 72 | protected function load( $path ) { 73 | if( isset($this->loaded[$path]) ) { 74 | return ''; 75 | } 76 | 77 | if( !file_exists($path) ) { 78 | die("ERROR: Couldn't load $path required from {$this->currentInput}\n"); 79 | } 80 | 81 | echo "loading $path \n"; 82 | $this->loaded[$path] = true; 83 | $this->currentInput = $path; 84 | 85 | $code = file_get_contents( $path ); 86 | $this->bytesIn += strlen($code); 87 | $this->fileCount++; 88 | if( $this->format & self::MINIFIED ) { 89 | $code = trim(JSMin::minify($code)); 90 | } 91 | 92 | 93 | // Naively probe the file for 'ig.module().requires().defines()' code; 94 | // the 'requries()' part will be handled by the regexp callback 95 | $this->definesModule = false; 96 | $code = preg_replace_callback( 97 | '/ig\s* 98 | \.\s*module\s*\((.*?)\)\s* 99 | (\.\s*requires\s*\((.*?)\)\s*)? 100 | \.\s*defines\s*\( 101 | /smx', 102 | array($this,'loadCallback'), 103 | $code 104 | ); 105 | 106 | // All files should define a module; maybe we just missed it? Print a 107 | // friendly reminder :) 108 | if( !$this->definesModule ) { 109 | echo "WARNING: file $path seems to define no module!\n"; 110 | } 111 | 112 | return $code; 113 | } 114 | 115 | 116 | protected function loadCallback( $matches ) { 117 | $currentInput = $this->currentInput; 118 | $this->definesModule = true; 119 | 120 | $moduleName = $matches[1]; 121 | $requiredFiles = isset($matches[3]) ? $matches[3] : ''; 122 | $requiredCode = ''; 123 | 124 | if( $requiredFiles ) { 125 | // Explode the module names and map them to file names. Ignore the 126 | // dom.ready module if present 127 | $moduleFiles = array_diff( 128 | explode( 129 | ',', 130 | preg_replace( 131 | '/[\s\'"]|\/\/.*|\/\*.*\*\//', // strip quotes and spaces 132 | '', 133 | str_replace('.', '/', $requiredFiles ) // . to / 134 | ) 135 | ), 136 | array('dom/ready') 137 | ); 138 | 139 | foreach( $moduleFiles as $f ) { 140 | $requiredCode .= $this->load( $this->base . $f.'.js' ); 141 | } 142 | } 143 | 144 | return 145 | $requiredCode . 146 | "\n\n// $currentInput\n" . 147 | 'ig.baked=true;' . 148 | 'ig.module('.$moduleName.')' . 149 | ( $requiredFiles 150 | ? '.requires('.$requiredFiles.')' 151 | : '' 152 | ) . 153 | '.defines('; 154 | } 155 | } 156 | 157 | ?> -------------------------------------------------------------------------------- /tools/bake.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Path to impact.js and your game's main .js 4 | IMPACT_LIBRARY=lib/impact/impact.js 5 | GAME=lib/game/main.js 6 | 7 | # Output file 8 | OUTPUT_FILE=game.min.js 9 | 10 | 11 | # Change CWD to Impact's base dir and bake! 12 | cd .. 13 | php tools/bake.php $IMPACT_LIBRARY $GAME $OUTPUT_FILE -------------------------------------------------------------------------------- /tools/jsmin.php: -------------------------------------------------------------------------------- 1 | 41 | * @copyright 2002 Douglas Crockford (jsmin.c) 42 | * @copyright 2008 Ryan Grove (PHP port) 43 | * @license http://opensource.org/licenses/mit-license.php MIT License 44 | * @version 1.1.1 (2008-03-02) 45 | * @link http://code.google.com/p/jsmin-php/ 46 | */ 47 | 48 | class JSMin { 49 | const ORD_LF = 10; 50 | const ORD_SPACE = 32; 51 | 52 | protected $a = ''; 53 | protected $b = ''; 54 | protected $input = ''; 55 | protected $inputIndex = 0; 56 | protected $inputLength = 0; 57 | protected $lookAhead = null; 58 | protected $output = ''; 59 | 60 | // -- Public Static Methods -------------------------------------------------- 61 | 62 | public static function minify($js) { 63 | $jsmin = new JSMin($js); 64 | return $jsmin->min(); 65 | } 66 | 67 | // -- Public Instance Methods ------------------------------------------------ 68 | 69 | public function __construct($input) { 70 | $this->input = str_replace("\r\n", "\n", $input); 71 | $this->inputLength = strlen($this->input); 72 | } 73 | 74 | // -- Protected Instance Methods --------------------------------------------- 75 | 76 | protected function action($d) { 77 | switch($d) { 78 | case 1: 79 | $this->output .= $this->a; 80 | 81 | case 2: 82 | $this->a = $this->b; 83 | 84 | if ($this->a === "'" || $this->a === '"') { 85 | for (;;) { 86 | $this->output .= $this->a; 87 | $this->a = $this->get(); 88 | 89 | if ($this->a === $this->b) { 90 | break; 91 | } 92 | 93 | if (ord($this->a) <= self::ORD_LF) { 94 | throw new JSMinException('Unterminated string literal.'); 95 | } 96 | 97 | if ($this->a === '\\') { 98 | $this->output .= $this->a; 99 | $this->a = $this->get(); 100 | } 101 | } 102 | } 103 | 104 | case 3: 105 | $this->b = $this->next(); 106 | 107 | if ($this->b === '/' && ( 108 | $this->a === '(' || $this->a === ',' || $this->a === '=' || 109 | $this->a === ':' || $this->a === '[' || $this->a === '!' || 110 | $this->a === '&' || $this->a === '|' || $this->a === '?')) { 111 | 112 | $this->output .= $this->a . $this->b; 113 | 114 | for (;;) { 115 | $this->a = $this->get(); 116 | 117 | if ($this->a === '/') { 118 | break; 119 | } elseif ($this->a === '\\') { 120 | $this->output .= $this->a; 121 | $this->a = $this->get(); 122 | } elseif (ord($this->a) <= self::ORD_LF) { 123 | throw new JSMinException('Unterminated regular expression '. 124 | 'literal.'); 125 | } 126 | 127 | $this->output .= $this->a; 128 | } 129 | 130 | $this->b = $this->next(); 131 | } 132 | } 133 | } 134 | 135 | protected function get() { 136 | $c = $this->lookAhead; 137 | $this->lookAhead = null; 138 | 139 | if ($c === null) { 140 | if ($this->inputIndex < $this->inputLength) { 141 | $c = $this->input[$this->inputIndex]; 142 | $this->inputIndex += 1; 143 | } else { 144 | $c = null; 145 | } 146 | } 147 | 148 | if ($c === "\r") { 149 | return "\n"; 150 | } 151 | 152 | if ($c === null || $c === "\n" || ord($c) >= self::ORD_SPACE) { 153 | return $c; 154 | } 155 | 156 | return ' '; 157 | } 158 | 159 | protected function isAlphaNum($c) { 160 | return ord($c) > 126 || $c === '\\' || preg_match('/^[\w\$]$/', $c) === 1; 161 | } 162 | 163 | protected function min() { 164 | $this->a = "\n"; 165 | $this->action(3); 166 | 167 | while ($this->a !== null) { 168 | switch ($this->a) { 169 | case ' ': 170 | if ($this->isAlphaNum($this->b)) { 171 | $this->action(1); 172 | } else { 173 | $this->action(2); 174 | } 175 | break; 176 | 177 | case "\n": 178 | switch ($this->b) { 179 | case '{': 180 | case '[': 181 | case '(': 182 | case '+': 183 | case '-': 184 | $this->action(1); 185 | break; 186 | 187 | case ' ': 188 | $this->action(3); 189 | break; 190 | 191 | default: 192 | if ($this->isAlphaNum($this->b)) { 193 | $this->action(1); 194 | } 195 | else { 196 | $this->action(2); 197 | } 198 | } 199 | break; 200 | 201 | default: 202 | switch ($this->b) { 203 | case ' ': 204 | if ($this->isAlphaNum($this->a)) { 205 | $this->action(1); 206 | break; 207 | } 208 | 209 | $this->action(3); 210 | break; 211 | 212 | case "\n": 213 | switch ($this->a) { 214 | case '}': 215 | case ']': 216 | case ')': 217 | case '+': 218 | case '-': 219 | case '"': 220 | case "'": 221 | $this->action(1); 222 | break; 223 | 224 | default: 225 | if ($this->isAlphaNum($this->a)) { 226 | $this->action(1); 227 | } 228 | else { 229 | $this->action(3); 230 | } 231 | } 232 | break; 233 | 234 | default: 235 | $this->action(1); 236 | break; 237 | } 238 | } 239 | } 240 | 241 | return $this->output; 242 | } 243 | 244 | protected function next() { 245 | $c = $this->get(); 246 | 247 | if ($c === '/') { 248 | switch($this->peek()) { 249 | case '/': 250 | for (;;) { 251 | $c = $this->get(); 252 | 253 | if (ord($c) <= self::ORD_LF) { 254 | return $c; 255 | } 256 | } 257 | 258 | case '*': 259 | $this->get(); 260 | 261 | for (;;) { 262 | switch($this->get()) { 263 | case '*': 264 | if ($this->peek() === '/') { 265 | $this->get(); 266 | return ' '; 267 | } 268 | break; 269 | 270 | case null: 271 | throw new JSMinException('Unterminated comment.'); 272 | } 273 | } 274 | 275 | default: 276 | return $c; 277 | } 278 | } 279 | 280 | return $c; 281 | } 282 | 283 | protected function peek() { 284 | $this->lookAhead = $this->get(); 285 | return $this->lookAhead; 286 | } 287 | } 288 | 289 | // -- Exceptions --------------------------------------------------------------- 290 | class JSMinException extends Exception {} 291 | ?> -------------------------------------------------------------------------------- /weltmeister.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Weltmeister 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 |
27 |
28 | 29 | 30 | 31 | 93 | 94 |
1x
95 | 96 |
97 | 98 | 99 | --------------------------------------------------------------------------------