├── 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 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
93 |
94 |
1x
95 |
96 |
97 |
98 |
99 |
--------------------------------------------------------------------------------