Initial commit

This commit is contained in:
IllusionMan1212
2018-09-30 06:17:31 +02:00
commit 96ce7c0dce
75 changed files with 10420 additions and 0 deletions
+130
View File
@@ -0,0 +1,130 @@
ig.module(
'impact.animation'
)
.requires(
'impact.timer',
'impact.image'
)
.defines(function(){ "use strict";
ig.AnimationSheet = ig.Class.extend({
width: 8,
height: 8,
image: null,
init: function( path, width, height ) {
this.width = width;
this.height = height;
this.image = new ig.Image( path );
}
});
ig.Animation = ig.Class.extend({
sheet: null,
timer: null,
sequence: [],
flip: {x: false, y: false},
pivot: {x: 0, y: 0},
frame: 0,
tile: 0,
loopCount: 0,
alpha: 1,
angle: 0,
init: function( sheet, frameTime, sequence, stop ) {
this.sheet = sheet;
this.pivot = {x: sheet.width/2, y: sheet.height/2 };
this.timer = new ig.Timer();
this.frameTime = frameTime;
this.sequence = sequence;
this.stop = !!stop;
this.tile = this.sequence[0];
},
rewind: function() {
this.timer.set();
this.loopCount = 0;
this.frame = 0;
this.tile = this.sequence[0];
return this;
},
gotoFrame: function( f ) {
// Offset the timer by one tenth of a millisecond to make sure we
// jump to the correct frame and circumvent rounding errors
this.timer.set( this.frameTime * -f - 0.0001 );
this.update();
},
gotoRandomFrame: function() {
this.gotoFrame( Math.floor(Math.random() * this.sequence.length) )
},
update: function() {
var frameTotal = Math.floor(this.timer.delta() / this.frameTime);
this.loopCount = Math.floor(frameTotal / this.sequence.length);
if( this.stop && this.loopCount > 0 ) {
this.frame = this.sequence.length - 1;
}
else {
this.frame = frameTotal % this.sequence.length;
}
this.tile = this.sequence[ this.frame ];
},
draw: function( targetX, targetY ) {
var bbsize = Math.max(this.sheet.width, this.sheet.height);
// On screen?
if(
targetX > ig.system.width || targetY > ig.system.height ||
targetX + bbsize < 0 || targetY + bbsize < 0
) {
return;
}
if( this.alpha != 1) {
ig.system.context.globalAlpha = this.alpha;
}
if( this.angle == 0 ) {
this.sheet.image.drawTile(
targetX, targetY,
this.tile, this.sheet.width, this.sheet.height,
this.flip.x, this.flip.y
);
}
else {
ig.system.context.save();
ig.system.context.translate(
ig.system.getDrawPos(targetX + this.pivot.x),
ig.system.getDrawPos(targetY + this.pivot.y)
);
ig.system.context.rotate( this.angle );
this.sheet.image.drawTile(
-this.pivot.x, -this.pivot.y,
this.tile, this.sheet.width, this.sheet.height,
this.flip.x, this.flip.y
);
ig.system.context.restore();
}
if( this.alpha != 1) {
ig.system.context.globalAlpha = 1;
}
}
});
});
+249
View File
@@ -0,0 +1,249 @@
ig.module(
'impact.background-map'
)
.requires(
'impact.map',
'impact.image'
)
.defines(function(){ "use strict";
ig.BackgroundMap = ig.Map.extend({
tiles: null,
scroll: {x: 0, y:0},
distance: 1,
repeat: false,
tilesetName: '',
foreground: false,
enabled: true,
preRender: false,
preRenderedChunks: null,
chunkSize: 512,
debugChunks: false,
anims: {},
init: function( tilesize, data, tileset ) {
this.parent( tilesize, data );
this.setTileset( tileset );
},
setTileset: function( tileset ) {
this.tilesetName = tileset instanceof ig.Image ? tileset.path : tileset;
this.tiles = new ig.Image( this.tilesetName );
this.preRenderedChunks = null;
},
setScreenPos: function( x, y ) {
this.scroll.x = x / this.distance;
this.scroll.y = y / this.distance;
},
preRenderMapToChunks: function() {
var totalWidth = this.width * this.tilesize * ig.system.scale,
totalHeight = this.height * this.tilesize * ig.system.scale;
// If this layer is smaller than the chunkSize, adjust the chunkSize
// accordingly, so we don't have as much overdraw
this.chunkSize = Math.min( Math.max(totalWidth, totalHeight), this.chunkSize );
var chunkCols = Math.ceil(totalWidth / this.chunkSize),
chunkRows = Math.ceil(totalHeight / this.chunkSize);
this.preRenderedChunks = [];
for( var y = 0; y < chunkRows; y++ ) {
this.preRenderedChunks[y] = [];
for( var x = 0; x < chunkCols; x++ ) {
var chunkWidth = (x == chunkCols-1)
? totalWidth - x * this.chunkSize
: this.chunkSize;
var chunkHeight = (y == chunkRows-1)
? totalHeight - y * this.chunkSize
: this.chunkSize;
this.preRenderedChunks[y][x] = this.preRenderChunk( x, y, chunkWidth, chunkHeight );
}
}
},
preRenderChunk: function( cx, cy, w, h ) {
var tw = w / this.tilesize / ig.system.scale + 1,
th = h / this.tilesize / ig.system.scale + 1;
var nx = (cx * this.chunkSize / ig.system.scale) % this.tilesize,
ny = (cy * this.chunkSize / ig.system.scale) % this.tilesize;
var tx = Math.floor(cx * this.chunkSize / this.tilesize / ig.system.scale),
ty = Math.floor(cy * this.chunkSize / this.tilesize / ig.system.scale);
var chunk = ig.$new('canvas');
chunk.width = w;
chunk.height = h;
chunk.retinaResolutionEnabled = false; // Opt out for Ejecta
var chunkContext = chunk.getContext('2d');
ig.System.scaleMode(chunk, chunkContext);
var screenContext = ig.system.context;
ig.system.context = chunkContext;
for( var x = 0; x < tw; x++ ) {
for( var y = 0; y < th; y++ ) {
if( x + tx < this.width && y + ty < this.height ) {
var tile = this.data[y+ty][x+tx];
if( tile ) {
this.tiles.drawTile(
x * this.tilesize - nx, y * this.tilesize - ny,
tile - 1, this.tilesize
);
}
}
}
}
ig.system.context = screenContext;
return chunk;
},
draw: function() {
if( !this.tiles.loaded || !this.enabled ) {
return;
}
if( this.preRender ) {
this.drawPreRendered();
}
else {
this.drawTiled();
}
},
drawPreRendered: function() {
if( !this.preRenderedChunks ) {
this.preRenderMapToChunks();
}
var dx = ig.system.getDrawPos(this.scroll.x),
dy = ig.system.getDrawPos(this.scroll.y);
if( this.repeat ) {
var w = this.width * this.tilesize * ig.system.scale;
dx = (dx%w + w) % w;
var h = this.height * this.tilesize * ig.system.scale;
dy = (dy%h + h) % h;
}
var minChunkX = Math.max( Math.floor(dx / this.chunkSize), 0 ),
minChunkY = Math.max( Math.floor(dy / this.chunkSize), 0 ),
maxChunkX = Math.ceil((dx+ig.system.realWidth) / this.chunkSize),
maxChunkY = Math.ceil((dy+ig.system.realHeight) / this.chunkSize),
maxRealChunkX = this.preRenderedChunks[0].length,
maxRealChunkY = this.preRenderedChunks.length;
if( !this.repeat ) {
maxChunkX = Math.min( maxChunkX, maxRealChunkX );
maxChunkY = Math.min( maxChunkY, maxRealChunkY );
}
var nudgeY = 0;
for( var cy = minChunkY; cy < maxChunkY; cy++ ) {
var nudgeX = 0;
for( var cx = minChunkX; cx < maxChunkX; cx++ ) {
var chunk = this.preRenderedChunks[cy % maxRealChunkY][cx % maxRealChunkX];
var x = -dx + cx * this.chunkSize - nudgeX;
var y = -dy + cy * this.chunkSize - nudgeY;
ig.system.context.drawImage( chunk, x, y);
ig.Image.drawCount++;
if( this.debugChunks ) {
ig.system.context.strokeStyle = '#f0f';
ig.system.context.strokeRect( x, y, this.chunkSize, this.chunkSize );
}
// If we repeat in X and this chunk's width wasn't the full chunk size
// and the screen is not already filled, we need to draw anohter chunk
// AND nudge it to be flush with the last chunk
if( this.repeat && chunk.width < this.chunkSize && x + chunk.width < ig.system.realWidth ) {
nudgeX += this.chunkSize - chunk.width;
maxChunkX++;
}
}
// Same as above, but for Y
if( this.repeat && chunk.height < this.chunkSize && y + chunk.height < ig.system.realHeight ) {
nudgeY += this.chunkSize - chunk.height;
maxChunkY++;
}
}
},
drawTiled: function() {
var tile = 0,
anim = null,
tileOffsetX = (this.scroll.x / this.tilesize).toInt(),
tileOffsetY = (this.scroll.y / this.tilesize).toInt(),
pxOffsetX = this.scroll.x % this.tilesize,
pxOffsetY = this.scroll.y % this.tilesize,
pxMinX = -pxOffsetX - this.tilesize,
pxMinY = -pxOffsetY - this.tilesize,
pxMaxX = ig.system.width + this.tilesize - pxOffsetX,
pxMaxY = ig.system.height + this.tilesize - pxOffsetY;
// FIXME: could be sped up for non-repeated maps: restrict the for loops
// to the map size instead of to the screen size and skip the 'repeat'
// checks inside the loop.
for( var mapY = -1, pxY = pxMinY; pxY < pxMaxY; mapY++, pxY += this.tilesize) {
var tileY = mapY + tileOffsetY;
// Repeat Y?
if( tileY >= this.height || tileY < 0 ) {
if( !this.repeat ) { continue; }
tileY = (tileY%this.height + this.height) % this.height;
}
for( var mapX = -1, pxX = pxMinX; pxX < pxMaxX; mapX++, pxX += this.tilesize ) {
var tileX = mapX + tileOffsetX;
// Repeat X?
if( tileX >= this.width || tileX < 0 ) {
if( !this.repeat ) { continue; }
tileX = (tileX%this.width + this.width) % this.width;
}
// Draw!
if( (tile = this.data[tileY][tileX]) ) {
if( (anim = this.anims[tile-1]) ) {
anim.draw( pxX, pxY );
}
else {
this.tiles.drawTile( pxX, pxY, tile-1, this.tilesize );
}
}
} // end for x
} // end for y
}
});
});
+274
View File
@@ -0,0 +1,274 @@
ig.module(
'impact.collision-map'
)
.requires(
'impact.map'
)
.defines(function(){ "use strict";
ig.CollisionMap = ig.Map.extend({
lastSlope: 1,
tiledef: null,
init: function( tilesize, data, tiledef ) {
this.parent( tilesize, data );
this.tiledef = tiledef || ig.CollisionMap.defaultTileDef;
for( var t in this.tiledef ) {
if( t|0 > this.lastSlope ) {
this.lastSlope = t|0;
}
}
},
trace: function( x, y, vx, vy, objectWidth, objectHeight ) {
// Set up the trace-result
var res = {
collision: {x: false, y: false, slope: false},
pos: {x: x, y: y},
tile: {x: 0, y: 0}
};
// Break the trace down into smaller steps if necessary
var steps = Math.ceil(Math.max(Math.abs(vx), Math.abs(vy)) / this.tilesize);
if( steps > 1 ) {
var sx = vx / steps;
var sy = vy / steps;
for( var i = 0; i < steps && (sx || sy); i++ ) {
this._traceStep( res, x, y, sx, sy, objectWidth, objectHeight, vx, vy, i );
x = res.pos.x;
y = res.pos.y;
if( res.collision.x ) { sx = 0; vx = 0; }
if( res.collision.y ) { sy = 0; vy = 0; }
if( res.collision.slope ) { break; }
}
}
// Just one step
else {
this._traceStep( res, x, y, vx, vy, objectWidth, objectHeight, vx, vy, 0 );
}
return res;
},
_traceStep: function( res, x, y, vx, vy, width, height, rvx, rvy, step ) {
res.pos.x += vx;
res.pos.y += vy;
var t = 0;
// Horizontal collision (walls)
if( vx ) {
var pxOffsetX = (vx > 0 ? width : 0);
var tileOffsetX = (vx < 0 ? this.tilesize : 0);
var firstTileY = Math.max( Math.floor(y / this.tilesize), 0 );
var lastTileY = Math.min( Math.ceil((y + height) / this.tilesize), this.height );
var tileX = Math.floor( (res.pos.x + pxOffsetX) / this.tilesize );
// We need to test the new tile position as well as the current one, as we
// could still collide with the current tile if it's a line def.
// We can skip this test if this is not the first step or the new tile position
// is the same as the current one.
var prevTileX = Math.floor( (x + pxOffsetX) / this.tilesize );
if( step > 0 || tileX == prevTileX || prevTileX < 0 || prevTileX >= this.width ) {
prevTileX = -1;
}
// Still inside this collision map?
if( tileX >= 0 && tileX < this.width ) {
for( var tileY = firstTileY; tileY < lastTileY; tileY++ ) {
if( prevTileX != -1 ) {
t = this.data[tileY][prevTileX];
if(
t > 1 && t <= this.lastSlope &&
this._checkTileDef(res, t, x, y, rvx, rvy, width, height, prevTileX, tileY)
) {
break;
}
}
t = this.data[tileY][tileX];
if(
t == 1 || t > this.lastSlope || // fully solid tile?
(t > 1 && this._checkTileDef(res, t, x, y, rvx, rvy, width, height, tileX, tileY)) // slope?
) {
if( t > 1 && t <= this.lastSlope && res.collision.slope ) {
break;
}
// full tile collision!
res.collision.x = true;
res.tile.x = t;
x = res.pos.x = tileX * this.tilesize - pxOffsetX + tileOffsetX;
rvx = 0;
break;
}
}
}
}
// Vertical collision (floor, ceiling)
if( vy ) {
var pxOffsetY = (vy > 0 ? height : 0);
var tileOffsetY = (vy < 0 ? this.tilesize : 0);
var firstTileX = Math.max( Math.floor(res.pos.x / this.tilesize), 0 );
var lastTileX = Math.min( Math.ceil((res.pos.x + width) / this.tilesize), this.width );
var tileY = Math.floor( (res.pos.y + pxOffsetY) / this.tilesize );
var prevTileY = Math.floor( (y + pxOffsetY) / this.tilesize );
if( step > 0 || tileY == prevTileY || prevTileY < 0 || prevTileY >= this.height ) {
prevTileY = -1;
}
// Still inside this collision map?
if( tileY >= 0 && tileY < this.height ) {
for( var tileX = firstTileX; tileX < lastTileX; tileX++ ) {
if( prevTileY != -1 ) {
t = this.data[prevTileY][tileX];
if(
t > 1 && t <= this.lastSlope &&
this._checkTileDef(res, t, x, y, rvx, rvy, width, height, tileX, prevTileY) ) {
break;
}
}
t = this.data[tileY][tileX];
if(
t == 1 || t > this.lastSlope || // fully solid tile?
(t > 1 && this._checkTileDef(res, t, x, y, rvx, rvy, width, height, tileX, tileY)) // slope?
) {
if( t > 1 && t <= this.lastSlope && res.collision.slope ) {
break;
}
// full tile collision!
res.collision.y = true;
res.tile.y = t;
res.pos.y = tileY * this.tilesize - pxOffsetY + tileOffsetY;
break;
}
}
}
}
// res is changed in place, nothing to return
},
_checkTileDef: function( res, t, x, y, vx, vy, width, height, tileX, tileY ) {
var def = this.tiledef[t];
if( !def ) { return false; }
var lx = (tileX + def[0]) * this.tilesize,
ly = (tileY + def[1]) * this.tilesize,
lvx = (def[2] - def[0]) * this.tilesize,
lvy = (def[3] - def[1]) * this.tilesize,
solid = def[4];
// Find the box corner to test, relative to the line
var tx = x + vx + (lvy < 0 ? width : 0) - lx,
ty = y + vy + (lvx > 0 ? height : 0) - ly;
// Is the box corner behind the line?
if( lvx * ty - lvy * tx > 0 ) {
// Lines are only solid from one side - find the dot product of
// line normal and movement vector and dismiss if wrong side
if( vx * -lvy + vy * lvx < 0 ) {
return solid;
}
// Find the line normal
var length = Math.sqrt(lvx * lvx + lvy * lvy);
var nx = lvy/length,
ny = -lvx/length;
// Project out of the line
var proj = tx * nx + ty * ny;
var px = nx * proj,
py = ny * proj;
// If we project further out than we moved in, then this is a full
// tile collision for solid tiles.
// For non-solid tiles, make sure we were in front of the line.
if( px*px+py*py >= vx*vx+vy*vy ) {
return solid || (lvx * (ty-vy) - lvy * (tx-vx) < 0.5);
}
res.pos.x = x + vx - px;
res.pos.y = y + vy - py;
res.collision.slope = {x: lvx, y: lvy, nx: nx, ny: ny};
return true;
}
return false;
}
});
// Default Slope Tile definition. Each tile is defined by an array of 5 vars:
// - 4 for the line in tile coordinates (0 -- 1)
// - 1 specifing whether the tile is 'filled' behind the line or not
// [ x1, y1, x2, y2, solid ]
// Defining 'half', 'one third' and 'two thirds' as vars makes it a bit
// easier to read... I hope.
var H = 1/2,
N = 1/3,
M = 2/3,
SOLID = true,
NON_SOLID = false;
ig.CollisionMap.defaultTileDef = {
/* 15 NE */ 5: [0,1, 1,M, SOLID], 6: [0,M, 1,N, SOLID], 7: [0,N, 1,0, SOLID],
/* 22 NE */ 3: [0,1, 1,H, SOLID], 4: [0,H, 1,0, SOLID],
/* 45 NE */ 2: [0,1, 1,0, SOLID],
/* 67 NE */ 10: [H,1, 1,0, SOLID], 21: [0,1, H,0, SOLID],
/* 75 NE */ 32: [M,1, 1,0, SOLID], 43: [N,1, M,0, SOLID], 54: [0,1, N,0, SOLID],
/* 15 SE */ 27: [0,0, 1,N, SOLID], 28: [0,N, 1,M, SOLID], 29: [0,M, 1,1, SOLID],
/* 22 SE */ 25: [0,0, 1,H, SOLID], 26: [0,H, 1,1, SOLID],
/* 45 SE */ 24: [0,0, 1,1, SOLID],
/* 67 SE */ 11: [0,0, H,1, SOLID], 22: [H,0, 1,1, SOLID],
/* 75 SE */ 33: [0,0, N,1, SOLID], 44: [N,0, M,1, SOLID], 55: [M,0, 1,1, SOLID],
/* 15 NW */ 16: [1,N, 0,0, SOLID], 17: [1,M, 0,N, SOLID], 18: [1,1, 0,M, SOLID],
/* 22 NW */ 14: [1,H, 0,0, SOLID], 15: [1,1, 0,H, SOLID],
/* 45 NW */ 13: [1,1, 0,0, SOLID],
/* 67 NW */ 8: [H,1, 0,0, SOLID], 19: [1,1, H,0, SOLID],
/* 75 NW */ 30: [N,1, 0,0, SOLID], 41: [M,1, N,0, SOLID], 52: [1,1, M,0, SOLID],
/* 15 SW */ 38: [1,M, 0,1, SOLID], 39: [1,N, 0,M, SOLID], 40: [1,0, 0,N, SOLID],
/* 22 SW */ 36: [1,H, 0,1, SOLID], 37: [1,0, 0,H, SOLID],
/* 45 SW */ 35: [1,0, 0,1, SOLID],
/* 67 SW */ 9: [1,0, H,1, SOLID], 20: [H,0, 0,1, SOLID],
/* 75 SW */ 31: [1,0, M,1, SOLID], 42: [M,0, N,1, SOLID], 53: [N,0, 0,1, SOLID],
/* Go N */ 12: [0,0, 1,0, NON_SOLID],
/* Go S */ 23: [1,1, 0,1, NON_SOLID],
/* Go E */ 34: [1,0, 1,1, NON_SOLID],
/* Go W */ 45: [0,1, 0,0, NON_SOLID]
// Now that was fun!
};
// Static Dummy CollisionMap; never collides
ig.CollisionMap.staticNoCollision = { trace: function( x, y, vx, vy ) {
return {
collision: {x: false, y: false, slope: false},
pos: {x: x+vx, y: y+vy},
tile: {x: 0, y: 0}
};
}};
});
+148
View File
@@ -0,0 +1,148 @@
.ig_debug {
position: fixed;
left: 0;
bottom: 0;
width: 100%;
background-color: #000;
border-top: 2px solid #f57401;
font-size: 12px;
color: #fff;
z-index: 1000;
-webkit-user-select: none;
}
.ig_debug_panel_menu {
height: 28px;
background: #222;
background: -webkit-gradient(linear, left bottom, left top, color-stop(0,#000000), color-stop(1,#333));
background: -moz-linear-gradient(center bottom, #000000 0%, #333 100%);
background: -o-linear-gradient(#333, #000000);
}
.ig_debug_panel_menu div {
float: left;
height: 22px;
padding: 6px 8px 0 8px;
border-right: 1px solid #333;
}
.ig_debug_panel_menu .ig_debug_head {
font-weight: bold;
color: #888;
}
.ig_debug_menu_item:hover {
cursor: pointer;
background-color: #fff;
color: #000;
}
.ig_debug_menu_item.active, .ig_debug_menu_item.active:hover {
background-color: #000;
color: #fff;
}
.ig_debug_stats {
position: absolute;
right: 0;
top: 0;
float: right;
color: #888;
border-left: 1px solid #333;
text-align: right;
}
.ig_debug_stats span {
width: 3em;
display:inline-block;
color: #fff !important;
margin-right: 0.2em;
margin-left: 0.3em;
font-family: bitstream vera sans mono, courier new;
white-space: nowrap;
}
.ig_debug_panel {
height: 152px;
overflow: auto;
position: relative;
}
.ig_debug_panel canvas {
border-bottom: 1px solid #444;
}
.ig_debug_panel .ig_debug_panel {
padding: 8px;
height: auto;
float: left;
background-color: #000;
border-right: 2px solid #222;
}
.ig_debug_option {
padding: 2px 0 2px 8px;
cursor: pointer;
}
.ig_debug_option:first-child {
margin-top: 8px;
}
.ig_debug_option:hover {
background-color: #111;
}
.ig_debug_graph_mark {
position: absolute;
color: #888;
left: 4px;
font-size: 10px;
margin-top: -12px;
}
.ig_debug_legend {
color: #ccc;
}
.ig_debug_label_mark {
display: inline-block;
width: 10px;
height: 10px;
margin-right: 4px;
-webkit-transition: 0.1s linear;
-moz-transition: 0.1s linear;
}
.ig_debug_legend_color {
display: inline-block;
width: 6px;
height: 10px;
margin-right: 4px;
margin-left: 16px;
}
.ig_debug_legend_number {
width: 3em;
display: inline-block;
text-align: right;
font-family: bitstream vera sans mono, courier new;
color: #fff;
margin-right: 0.2em;
}
.ig_debug_map_container {
position: relative;
overflow: hidden;
border: 1px solid #888;
}
.ig_debug_map_container canvas {
position: absolute;
}
.ig_debug_map_screen {
position: absolute;
border: 1px solid #f0f;
}
+13
View File
@@ -0,0 +1,13 @@
ig.module(
'impact.debug.debug'
)
.requires(
'impact.debug.entities-panel',
'impact.debug.maps-panel',
'impact.debug.graph-panel'
)
.defines(function(){ "use strict";
/* Empty module to require all debug panels */
});
+129
View File
@@ -0,0 +1,129 @@
ig.module(
'impact.debug.entities-panel'
)
.requires(
'impact.debug.menu',
'impact.entity'
)
.defines(function(){ "use strict";
ig.Entity.inject({
colors: {
names: '#fff',
velocities: '#0f0',
boxes: '#f00'
},
draw: function() {
this.parent();
// Collision Boxes
if( ig.Entity._debugShowBoxes ) {
ig.system.context.strokeStyle = this.colors.boxes;
ig.system.context.lineWidth = 1.0;
ig.system.context.strokeRect(
ig.system.getDrawPos(this.pos.x.round() - ig.game.screen.x) - 0.5,
ig.system.getDrawPos(this.pos.y.round() - ig.game.screen.y) - 0.5,
this.size.x * ig.system.scale,
this.size.y * ig.system.scale
);
}
// Velocities
if( ig.Entity._debugShowVelocities ) {
var x = this.pos.x + this.size.x/2;
var y = this.pos.y + this.size.y/2;
this._debugDrawLine( this.colors.velocities, x, y, x + this.vel.x, y + this.vel.y );
}
// Names & Targets
if( ig.Entity._debugShowNames ) {
if( this.name ) {
ig.system.context.fillStyle = this.colors.names;
ig.system.context.fillText(
this.name,
ig.system.getDrawPos(this.pos.x - ig.game.screen.x),
ig.system.getDrawPos(this.pos.y - ig.game.screen.y)
);
}
if( typeof(this.target) == 'object' ) {
for( var t in this.target ) {
var ent = ig.game.getEntityByName( this.target[t] );
if( ent ) {
this._debugDrawLine( this.colors.names,
this.pos.x + this.size.x/2, this.pos.y + this.size.y/2,
ent.pos.x + ent.size.x/2, ent.pos.y + ent.size.y/2
);
}
}
}
}
},
_debugDrawLine: function( color, sx, sy, dx, dy ) {
ig.system.context.strokeStyle = color;
ig.system.context.lineWidth = 1.0;
ig.system.context.beginPath();
ig.system.context.moveTo(
ig.system.getDrawPos(sx - ig.game.screen.x),
ig.system.getDrawPos(sy - ig.game.screen.y)
);
ig.system.context.lineTo(
ig.system.getDrawPos(dx - ig.game.screen.x),
ig.system.getDrawPos(dy - ig.game.screen.y)
);
ig.system.context.stroke();
ig.system.context.closePath();
}
});
ig.Entity._debugEnableChecks = true;
ig.Entity._debugShowBoxes = false;
ig.Entity._debugShowVelocities = false;
ig.Entity._debugShowNames = false;
ig.Entity.oldCheckPair = ig.Entity.checkPair;
ig.Entity.checkPair = function( a, b ) {
if( !ig.Entity._debugEnableChecks ) {
return;
}
ig.Entity.oldCheckPair( a, b );
};
ig.debug.addPanel({
type: ig.DebugPanel,
name: 'entities',
label: 'Entities',
options: [
{
name: 'Checks & Collisions',
object: ig.Entity,
property: '_debugEnableChecks'
},
{
name: 'Show Collision Boxes',
object: ig.Entity,
property: '_debugShowBoxes'
},
{
name: 'Show Velocities',
object: ig.Entity,
property: '_debugShowVelocities'
},
{
name: 'Show Names & Targets',
object: ig.Entity,
property: '_debugShowNames'
}
]
});
});
+198
View File
@@ -0,0 +1,198 @@
ig.module(
'impact.debug.graph-panel'
)
.requires(
'impact.debug.menu',
'impact.system',
'impact.game',
'impact.image'
)
.defines(function(){ "use strict";
ig.Game.inject({
draw: function() {
ig.graph.beginClock('draw');
this.parent();
ig.graph.endClock('draw');
},
update: function() {
ig.graph.beginClock('update');
this.parent();
ig.graph.endClock('update');
},
checkEntities: function() {
ig.graph.beginClock('checks');
this.parent();
ig.graph.endClock('checks');
}
});
ig.DebugGraphPanel = ig.DebugPanel.extend({
clocks: {},
marks: [],
textY: 0,
height: 128,
ms: 64,
timeBeforeRun: 0,
init: function( name, label ) {
this.parent( name, label );
this.mark16ms = (this.height - (this.height/this.ms) * 16).round();
this.mark33ms = (this.height - (this.height/this.ms) * 33).round();
this.msHeight = this.height/this.ms;
this.graph = ig.$new('canvas');
this.graph.width = window.innerWidth;
this.graph.height = this.height;
this.container.appendChild( this.graph );
this.ctx = this.graph.getContext('2d');
this.ctx.fillStyle = '#444';
this.ctx.fillRect( 0, this.mark16ms, this.graph.width, 1 );
this.ctx.fillRect( 0, this.mark33ms, this.graph.width, 1 );
this.addGraphMark( '16ms', this.mark16ms );
this.addGraphMark( '33ms', this.mark33ms );
this.addClock( 'draw', 'Draw', '#13baff' );
this.addClock( 'update', 'Entity Update', '#bb0fff' );
this.addClock( 'checks', 'Entity Checks & Collisions', '#a2e908' );
this.addClock( 'lag', 'System Lag', '#f26900' );
ig.mark = this.mark.bind(this);
ig.graph = this;
},
addGraphMark: function( name, height ) {
var span = ig.$new('span');
span.className = 'ig_debug_graph_mark';
span.textContent = name;
span.style.top = height.round() + 'px';
this.container.appendChild( span );
},
addClock: function( name, description, color ) {
var mark = ig.$new('span');
mark.className = 'ig_debug_legend_color';
mark.style.backgroundColor = color;
var number = ig.$new('span');
number.className = 'ig_debug_legend_number';
number.appendChild( document.createTextNode('0') );
var legend = ig.$new('span');
legend.className = 'ig_debug_legend';
legend.appendChild( mark );
legend.appendChild( document.createTextNode(description +' (') );
legend.appendChild( number );
legend.appendChild( document.createTextNode('ms)') );
this.container.appendChild( legend );
this.clocks[name] = {
description: description,
color: color,
current: 0,
start: Date.now(),
avg: 0,
html: number
};
},
beginClock: function( name, offset ) {
this.clocks[name].start = Date.now() + (offset || 0);
},
endClock: function( name ) {
var c = this.clocks[name];
c.current = Math.round(Date.now() - c.start);
c.avg = c.avg * 0.8 + c.current * 0.2;
},
mark: function( msg, color ) {
if( this.active ) {
this.marks.push( {msg:msg, color:(color||'#fff')} );
}
},
beforeRun: function() {
this.endClock('lag');
this.timeBeforeRun = Date.now();
},
afterRun: function() {
var frameTime = Date.now() - this.timeBeforeRun;
var nextFrameDue = (1000/ig.system.fps) - frameTime;
this.beginClock('lag', Math.max(nextFrameDue, 0));
var x = this.graph.width-1;
var y = this.height;
this.ctx.drawImage( this.graph, -1, 0 );
this.ctx.fillStyle = '#000';
this.ctx.fillRect( x, 0, 1, this.height );
this.ctx.fillStyle = '#444';
this.ctx.fillRect( x, this.mark16ms, 1, 1 );
this.ctx.fillStyle = '#444';
this.ctx.fillRect( x, this.mark33ms, 1, 1 );
for( var ci in this.clocks ) {
var c = this.clocks[ci];
c.html.textContent = c.avg.toFixed(2);
if( c.color && c.current > 0 ) {
this.ctx.fillStyle = c.color;
var h = c.current * this.msHeight;
y -= h;
this.ctx.fillRect( x, y, 1, h );
c.current = 0;
}
}
this.ctx.textAlign = 'right';
this.ctx.textBaseline = 'top';
this.ctx.globalAlpha = 0.5;
for( var i = 0; i < this.marks.length; i++ ) {
var m = this.marks[i];
this.ctx.fillStyle = m.color;
this.ctx.fillRect( x, 0, 1, this.height );
if( m.msg ) {
this.ctx.fillText( m.msg, x-1, this.textY );
this.textY = (this.textY+8)%32;
}
}
this.ctx.globalAlpha = 1;
this.marks = [];
}
});
ig.debug.addPanel({
type: ig.DebugGraphPanel,
name: 'graph',
label: 'Performance'
});
});
+156
View File
@@ -0,0 +1,156 @@
ig.module(
'impact.debug.maps-panel'
)
.requires(
'impact.debug.menu',
'impact.game',
'impact.background-map'
)
.defines(function(){ "use strict";
ig.Game.inject({
loadLevel: function( data ) {
this.parent(data);
ig.debug.panels.maps.load(this);
}
});
ig.DebugMapsPanel = ig.DebugPanel.extend({
maps: [],
mapScreens: [],
init: function( name, label ) {
this.parent( name, label );
this.load();
},
load: function( game ) {
this.options = [];
this.panels = [];
if( !game || !game.backgroundMaps.length ) {
this.container.innerHTML = '<em>No Maps Loaded</em>';
return;
}
this.maps = game.backgroundMaps;
this.mapScreens = [];
this.container.innerHTML = '';
for( var m = 0; m < this.maps.length; m++ ) {
var map = this.maps[m];
var subPanel = new ig.DebugPanel( m, 'Layer '+m );
var head = new ig.$new('strong');
head.textContent = m +': ' + map.tiles.path;
subPanel.container.appendChild( head );
subPanel.addOption( new ig.DebugOption('Enabled', map, 'enabled') );
subPanel.addOption( new ig.DebugOption('Pre Rendered', map, 'preRender') );
subPanel.addOption( new ig.DebugOption('Show Chunks', map, 'debugChunks') );
this.generateMiniMap( subPanel, map, m );
this.addPanel( subPanel );
}
},
generateMiniMap: function( panel, map, id ) {
var s = ig.system.scale; // we'll need this a lot
// resize the tileset, so that one tile is 's' pixels wide and high
var ts = ig.$new('canvas');
var tsctx = ts.getContext('2d');
var w = map.tiles.width * s;
var h = map.tiles.height * s;
var ws = w / map.tilesize;
var hs = h / map.tilesize;
ts.width = ws;
ts.height = hs;
tsctx.drawImage( map.tiles.data, 0, 0, w, h, 0, 0, ws, hs );
// create the minimap canvas
var mapCanvas = ig.$new('canvas');
mapCanvas.width = map.width * s;
mapCanvas.height = map.height * s;
var ctx = mapCanvas.getContext('2d');
if( ig.game.clearColor ) {
ctx.fillStyle = ig.game.clearColor;
ctx.fillRect(0, 0, w, h);
}
// draw the map
var tile = 0;
for( var x = 0; x < map.width; x++ ) {
for( var y = 0; y < map.height; y++ ) {
if( (tile = map.data[y][x]) ) {
ctx.drawImage(
ts,
Math.floor(((tile-1) * s) % ws),
Math.floor((tile-1) * s / ws) * s,
s, s,
x * s, y * s,
s, s
);
}
}
}
var mapContainer = ig.$new('div');
mapContainer.className = 'ig_debug_map_container';
mapContainer.style.width = map.width * s + 'px';
mapContainer.style.height = map.height * s + 'px';
var mapScreen = ig.$new('div');
mapScreen.className = 'ig_debug_map_screen';
mapScreen.style.width = ((ig.system.width / map.tilesize) * s - 2) + 'px';
mapScreen.style.height = ((ig.system.height / map.tilesize) * s - 2) + 'px';
this.mapScreens[id] = mapScreen;
mapContainer.appendChild( mapCanvas );
mapContainer.appendChild( mapScreen );
panel.container.appendChild( mapContainer );
},
afterRun: function() {
// Update the screen position DIV for each mini-map
var s = ig.system.scale;
for( var m = 0; m < this.maps.length; m++ ) {
var map = this.maps[m];
var screen = this.mapScreens[m];
if( !map || !screen ) { // Quick sanity check
continue;
}
var x = map.scroll.x / map.tilesize;
var y = map.scroll.y / map.tilesize;
if( map.repeat ) {
x %= map.width;
y %= map.height;
}
screen.style.left = (x * s) + 'px';
screen.style.top = (y * s) + 'px';
}
}
});
ig.debug.addPanel({
type: ig.DebugMapsPanel,
name: 'maps',
label: 'Background Maps'
});
});
+299
View File
@@ -0,0 +1,299 @@
ig.module(
'impact.debug.menu'
)
.requires(
'dom.ready',
'impact.system'
)
.defines(function(){ "use strict";
ig.System.inject({
run: function() {
ig.debug.beforeRun();
this.parent();
ig.debug.afterRun();
},
setGameNow: function( gameClass ) {
this.parent( gameClass );
ig.debug.ready();
}
});
ig.Debug = ig.Class.extend({
options: {},
panels: {},
numbers:{},
container: null,
panelMenu: null,
activePanel: null,
debugTime: 0,
debugTickAvg: 0.016,
debugRealTime: Date.now(),
init: function() {
// Inject the Stylesheet
var style = ig.$new('link');
style.rel = 'stylesheet';
style.type = 'text/css';
style.href = ig.prefix + 'lib/impact/debug/debug.css';
ig.$('body')[0].appendChild( style );
// Create the Debug Container
this.container = ig.$new('div');
this.container.className ='ig_debug';
ig.$('body')[0].appendChild( this.container );
// Create and add the Menu Container
this.panelMenu = ig.$new('div');
this.panelMenu.innerHTML = '<div class="ig_debug_head">Impact.Debug:</div>';
this.panelMenu.className ='ig_debug_panel_menu';
this.container.appendChild( this.panelMenu );
// Create and add the Stats Container
this.numberContainer = ig.$new('div');
this.numberContainer.className ='ig_debug_stats';
this.panelMenu.appendChild( this.numberContainer );
// Set ig.log(), ig.assert() and ig.show()
if( window.console && window.console.log && window.console.assert ) {
// Can't use .bind() on native functions in IE9 :/
ig.log = console.log.bind ? console.log.bind(console) : console.log;
ig.assert = console.assert.bind ? console.assert.bind(console) : console.assert;
}
ig.show = this.showNumber.bind(this);
},
addNumber: function( name, width ) {
var number = ig.$new('span');
this.numberContainer.appendChild( number );
this.numberContainer.appendChild( document.createTextNode(name) );
this.numbers[name] = number;
},
showNumber: function( name, number, width ) {
if( !this.numbers[name] ) {
this.addNumber( name, width );
}
this.numbers[name].textContent = number;
},
addPanel: function( panelDef ) {
// Create the panel and options
var panel = new (panelDef.type)( panelDef.name, panelDef.label );
if( panelDef.options ) {
for( var i = 0; i < panelDef.options.length; i++ ) {
var opt = panelDef.options[i];
panel.addOption( new ig.DebugOption(opt.name, opt.object, opt.property) );
}
}
this.panels[ panel.name ] = panel;
panel.container.style.display = 'none';
this.container.appendChild( panel.container );
// Create the menu item
var menuItem = ig.$new('div');
menuItem.className = 'ig_debug_menu_item';
menuItem.textContent = panel.label;
menuItem.addEventListener(
'click',
(function(ev){ this.togglePanel(panel); }).bind(this),
false
);
panel.menuItem = menuItem;
// Insert menu item in alphabetical order into the menu
var inserted = false;
for( var i = 1; i < this.panelMenu.childNodes.length; i++ ) {
var cn = this.panelMenu.childNodes[i];
if( cn.textContent > panel.label ) {
this.panelMenu.insertBefore( menuItem, cn );
inserted = true;
break;
}
}
if( !inserted ) {
// Not inserted? Append at the end!
this.panelMenu.appendChild( menuItem );
}
},
showPanel: function( name ) {
this.togglePanel( this.panels[name] );
},
togglePanel: function( panel ) {
if( panel != this.activePanel && this.activePanel ) {
this.activePanel.toggle( false );
this.activePanel.menuItem.className = 'ig_debug_menu_item';
this.activePanel = null;
}
var dsp = panel.container.style.display;
var active = (dsp != 'block');
panel.toggle( active );
panel.menuItem.className = 'ig_debug_menu_item' + (active ? ' active' : '');
if( active ) {
this.activePanel = panel;
}
},
ready: function() {
for( var p in this.panels ) {
this.panels[p].ready();
}
},
beforeRun: function() {
var timeBeforeRun = Date.now();
this.debugTickAvg = this.debugTickAvg * 0.8 + (timeBeforeRun - this.debugRealTime) * 0.2;
this.debugRealTime = timeBeforeRun;
if( this.activePanel ) {
this.activePanel.beforeRun();
}
},
afterRun: function() {
var frameTime = Date.now() - this.debugRealTime;
var nextFrameDue = (1000/ig.system.fps) - frameTime;
this.debugTime = this.debugTime * 0.8 + frameTime * 0.2;
if( this.activePanel ) {
this.activePanel.afterRun();
}
this.showNumber( 'ms', this.debugTime.toFixed(2) );
this.showNumber( 'fps', Math.round(1000/this.debugTickAvg) );
this.showNumber( 'draws', ig.Image.drawCount );
if( ig.game && ig.game.entities ) {
this.showNumber( 'entities', ig.game.entities.length );
}
ig.Image.drawCount = 0;
}
});
ig.DebugPanel = ig.Class.extend({
active: false,
container: null,
options: [],
panels: [],
label: '',
name: '',
init: function( name, label ) {
this.name = name;
this.label = label;
this.container = ig.$new('div');
this.container.className = 'ig_debug_panel ' + this.name;
},
toggle: function( active ) {
this.active = active;
this.container.style.display = active ? 'block' : 'none';
},
addPanel: function( panel ) {
this.panels.push( panel );
this.container.appendChild( panel.container );
},
addOption: function( option ) {
this.options.push( option );
this.container.appendChild( option.container );
},
ready: function(){},
beforeRun: function(){},
afterRun: function(){}
});
ig.DebugOption = ig.Class.extend({
name: '',
labelName: '',
className: 'ig_debug_option',
label: null,
mark: null,
container: null,
active: false,
colors: {
enabled: '#fff',
disabled: '#444'
},
init: function( name, object, property ) {
this.name = name;
this.object = object;
this.property = property;
this.active = this.object[this.property];
this.container = ig.$new('div');
this.container.className = 'ig_debug_option';
this.label = ig.$new('span');
this.label.className = 'ig_debug_label';
this.label.textContent = this.name;
this.mark = ig.$new('span');
this.mark.className = 'ig_debug_label_mark';
this.container.appendChild( this.mark );
this.container.appendChild( this.label );
this.container.addEventListener( 'click', this.click.bind(this), false );
this.setLabel();
},
setLabel: function() {
this.mark.style.backgroundColor = this.active ? this.colors.enabled : this.colors.disabled;
},
click: function( ev ) {
this.active = !this.active;
this.object[this.property] = this.active;
this.setLabel();
ev.stopPropagation();
ev.preventDefault();
return false;
}
});
// Create the debug instance!
ig.debug = new ig.Debug();
});
+60
View File
@@ -0,0 +1,60 @@
ig.module(
'impact.entity-pool'
)
.requires(
'impact.game'
)
.defines(function(){ "use strict";
ig.EntityPool = {
pools: {},
mixin: {
staticInstantiate: function( x, y, settings ) {
return ig.EntityPool.getFromPool( this.classId, x, y, settings );
},
erase: function() {
ig.EntityPool.putInPool( this );
}
},
enableFor: function( Class ) {
Class.inject(this.mixin);
},
getFromPool: function( classId, x, y, settings ) {
var pool = this.pools[classId];
if( !pool || !pool.length ) { return null; }
var instance = pool.pop();
instance.reset(x, y, settings);
return instance;
},
putInPool: function( instance ) {
if( !this.pools[instance.classId] ) {
this.pools[instance.classId] = [instance];
}
else {
this.pools[instance.classId].push(instance);
}
},
drainPool: function( classId ) {
delete this.pools[classId];
},
drainAllPools: function() {
this.pools = {};
}
};
ig.Game.inject({
loadLevel: function( data ) {
ig.EntityPool.drainAllPools();
this.parent(data);
}
});
});
+434
View File
@@ -0,0 +1,434 @@
ig.module(
'impact.entity'
)
.requires(
'impact.animation',
'impact.impact'
)
.defines(function(){ "use strict";
ig.Entity = ig.Class.extend({
id: 0,
settings: {},
size: {x: 16, y:16},
offset: {x: 0, y: 0},
pos: {x: 0, y:0},
last: {x: 0, y:0},
vel: {x: 0, y: 0},
accel: {x: 0, y: 0},
friction: {x: 0, y: 0},
maxVel: {x: 100, y: 100},
zIndex: 0,
gravityFactor: 1,
standing: false,
bounciness: 0,
minBounceVelocity: 40,
anims: {},
animSheet: null,
currentAnim: null,
health: 10,
type: 0, // TYPE.NONE
checkAgainst: 0, // TYPE.NONE
collides: 0, // COLLIDES.NEVER
_killed: false,
slopeStanding: {min: (44).toRad(), max: (136).toRad() },
init: function( x, y, settings ) {
this.id = ++ig.Entity._lastId;
this.pos.x = this.last.x = x;
this.pos.y = this.last.y = y;
ig.merge( this, settings );
},
reset: function( x, y, settings ) {
var proto = this.constructor.prototype;
this.pos.x = x;
this.pos.y = y;
this.last.x = x;
this.last.y = y;
this.vel.x = proto.vel.x;
this.vel.y = proto.vel.y;
this.accel.x = proto.accel.x;
this.accel.y = proto.accel.y;
this.health = proto.health;
this._killed = proto._killed;
this.standing = proto.standing;
this.type = proto.type;
this.checkAgainst = proto.checkAgainst;
this.collides = proto.collides;
ig.merge( this, settings );
},
addAnim: function( name, frameTime, sequence, stop ) {
if( !this.animSheet ) {
throw( 'No animSheet to add the animation '+name+' to.' );
}
var a = new ig.Animation( this.animSheet, frameTime, sequence, stop );
this.anims[name] = a;
if( !this.currentAnim ) {
this.currentAnim = a;
}
return a;
},
update: function() {
this.last.x = this.pos.x;
this.last.y = this.pos.y;
this.vel.y += ig.game.gravity * ig.system.tick * this.gravityFactor;
this.vel.x = this.getNewVelocity( this.vel.x, this.accel.x, this.friction.x, this.maxVel.x );
this.vel.y = this.getNewVelocity( this.vel.y, this.accel.y, this.friction.y, this.maxVel.y );
// movement & collision
var mx = this.vel.x * ig.system.tick;
var my = this.vel.y * ig.system.tick;
var res = ig.game.collisionMap.trace(
this.pos.x, this.pos.y, mx, my, this.size.x, this.size.y
);
this.handleMovementTrace( res );
if( this.currentAnim ) {
this.currentAnim.update();
}
},
getNewVelocity: function( vel, accel, friction, max ) {
if( accel ) {
return ( vel + accel * ig.system.tick ).limit( -max, max );
}
else if( friction ) {
var delta = friction * ig.system.tick;
if( vel - delta > 0) {
return vel - delta;
}
else if( vel + delta < 0 ) {
return vel + delta;
}
else {
return 0;
}
}
return vel.limit( -max, max );
},
handleMovementTrace: function( res ) {
this.standing = false;
if( res.collision.y ) {
if( this.bounciness > 0 && Math.abs(this.vel.y) > this.minBounceVelocity ) {
this.vel.y *= -this.bounciness;
}
else {
if( this.vel.y > 0 ) {
this.standing = true;
}
this.vel.y = 0;
}
}
if( res.collision.x ) {
if( this.bounciness > 0 && Math.abs(this.vel.x) > this.minBounceVelocity ) {
this.vel.x *= -this.bounciness;
}
else {
this.vel.x = 0;
}
}
if( res.collision.slope ) {
var s = res.collision.slope;
if( this.bounciness > 0 ) {
var proj = this.vel.x * s.nx + this.vel.y * s.ny;
this.vel.x = (this.vel.x - s.nx * proj * 2) * this.bounciness;
this.vel.y = (this.vel.y - s.ny * proj * 2) * this.bounciness;
}
else {
var lengthSquared = s.x * s.x + s.y * s.y;
var dot = (this.vel.x * s.x + this.vel.y * s.y)/lengthSquared;
this.vel.x = s.x * dot;
this.vel.y = s.y * dot;
var angle = Math.atan2( s.x, s.y );
if( angle > this.slopeStanding.min && angle < this.slopeStanding.max ) {
this.standing = true;
}
}
}
this.pos = res.pos;
},
draw: function() {
if( this.currentAnim ) {
this.currentAnim.draw(
this.pos.x - this.offset.x - ig.game._rscreen.x,
this.pos.y - this.offset.y - ig.game._rscreen.y
);
}
},
kill: function() {
ig.game.removeEntity( this );
},
receiveDamage: function( amount, from ) {
this.health -= amount;
if( this.health <= 0 ) {
this.kill();
}
},
touches: function( other ) {
return !(
this.pos.x >= other.pos.x + other.size.x ||
this.pos.x + this.size.x <= other.pos.x ||
this.pos.y >= other.pos.y + other.size.y ||
this.pos.y + this.size.y <= other.pos.y
);
},
distanceTo: function( other ) {
var xd = (this.pos.x + this.size.x/2) - (other.pos.x + other.size.x/2);
var yd = (this.pos.y + this.size.y/2) - (other.pos.y + other.size.y/2);
return Math.sqrt( xd*xd + yd*yd );
},
angleTo: function( other ) {
return Math.atan2(
(other.pos.y + other.size.y/2) - (this.pos.y + this.size.y/2),
(other.pos.x + other.size.x/2) - (this.pos.x + this.size.x/2)
);
},
check: function( other ) {},
collideWith: function( other, axis ) {},
ready: function() {},
erase: function() {}
});
// Last used entity id; incremented with each spawned entity
ig.Entity._lastId = 0;
// Collision Types - Determine if and how entities collide with each other
// In ACTIVE vs. LITE or FIXED vs. ANY collisions, only the "weak" entity moves,
// while the other one stays fixed. In ACTIVE vs. ACTIVE and ACTIVE vs. PASSIVE
// collisions, both entities are moved. LITE or PASSIVE entities don't collide
// with other LITE or PASSIVE entities at all. The behaiviour for FIXED vs.
// FIXED collisions is undefined.
ig.Entity.COLLIDES = {
NEVER: 0,
LITE: 1,
PASSIVE: 2,
ACTIVE: 4,
FIXED: 8
};
// Entity Types - used for checks
ig.Entity.TYPE = {
NONE: 0,
A: 1,
B: 2,
BOTH: 3
};
ig.Entity.checkPair = function( a, b ) {
// Do these entities want checks?
if( a.checkAgainst & b.type ) {
a.check( b );
}
if( b.checkAgainst & a.type ) {
b.check( a );
}
// If this pair allows collision, solve it! At least one entity must
// collide ACTIVE or FIXED, while the other one must not collide NEVER.
if(
a.collides && b.collides &&
a.collides + b.collides > ig.Entity.COLLIDES.ACTIVE
) {
ig.Entity.solveCollision( a, b );
}
};
ig.Entity.solveCollision = function( a, b ) {
// If one entity is FIXED, or the other entity is LITE, the weak
// (FIXED/NON-LITE) entity won't move in collision response
var weak = null;
if(
a.collides == ig.Entity.COLLIDES.LITE ||
b.collides == ig.Entity.COLLIDES.FIXED
) {
weak = a;
}
else if(
b.collides == ig.Entity.COLLIDES.LITE ||
a.collides == ig.Entity.COLLIDES.FIXED
) {
weak = b;
}
// Did they already overlap on the X-axis in the last frame? If so,
// this must be a vertical collision!
if(
a.last.x + a.size.x > b.last.x &&
a.last.x < b.last.x + b.size.x
) {
// Which one is on top?
if( a.last.y < b.last.y ) {
ig.Entity.seperateOnYAxis( a, b, weak );
}
else {
ig.Entity.seperateOnYAxis( b, a, weak );
}
a.collideWith( b, 'y' );
b.collideWith( a, 'y' );
}
// Horizontal collision
else if(
a.last.y + a.size.y > b.last.y &&
a.last.y < b.last.y + b.size.y
){
// Which one is on the left?
if( a.last.x < b.last.x ) {
ig.Entity.seperateOnXAxis( a, b, weak );
}
else {
ig.Entity.seperateOnXAxis( b, a, weak );
}
a.collideWith( b, 'x' );
b.collideWith( a, 'x' );
}
};
// FIXME: This is a mess. Instead of doing all the movements here, the entities
// should get notified of the collision (with all details) and resolve it
// themselfs.
ig.Entity.seperateOnXAxis = function( left, right, weak ) {
var nudge = (left.pos.x + left.size.x - right.pos.x);
// We have a weak entity, so just move this one
if( weak ) {
var strong = left === weak ? right : left;
weak.vel.x = -weak.vel.x * weak.bounciness + strong.vel.x;
var resWeak = ig.game.collisionMap.trace(
weak.pos.x, weak.pos.y, weak == left ? -nudge : nudge, 0, weak.size.x, weak.size.y
);
weak.pos.x = resWeak.pos.x;
}
// Normal collision - both move
else {
var v2 = (left.vel.x - right.vel.x)/2;
left.vel.x = -v2;
right.vel.x = v2;
var resLeft = ig.game.collisionMap.trace(
left.pos.x, left.pos.y, -nudge/2, 0, left.size.x, left.size.y
);
left.pos.x = Math.floor(resLeft.pos.x);
var resRight = ig.game.collisionMap.trace(
right.pos.x, right.pos.y, nudge/2, 0, right.size.x, right.size.y
);
right.pos.x = Math.ceil(resRight.pos.x);
}
};
ig.Entity.seperateOnYAxis = function( top, bottom, weak ) {
var nudge = (top.pos.y + top.size.y - bottom.pos.y);
// We have a weak entity, so just move this one
if( weak ) {
var strong = top === weak ? bottom : top;
weak.vel.y = -weak.vel.y * weak.bounciness + strong.vel.y;
// Riding on a platform?
var nudgeX = 0;
if( weak == top && Math.abs(weak.vel.y - strong.vel.y) < weak.minBounceVelocity ) {
weak.standing = true;
nudgeX = strong.vel.x * ig.system.tick;
}
var resWeak = ig.game.collisionMap.trace(
weak.pos.x, weak.pos.y, nudgeX, weak == top ? -nudge : nudge, weak.size.x, weak.size.y
);
weak.pos.y = resWeak.pos.y;
weak.pos.x = resWeak.pos.x;
}
// Bottom entity is standing - just bounce the top one
else if( ig.game.gravity && (bottom.standing || top.vel.y > 0) ) {
var resTop = ig.game.collisionMap.trace(
top.pos.x, top.pos.y, 0, -(top.pos.y + top.size.y - bottom.pos.y), top.size.x, top.size.y
);
top.pos.y = resTop.pos.y;
if( top.bounciness > 0 && top.vel.y > top.minBounceVelocity ) {
top.vel.y *= -top.bounciness;
}
else {
top.standing = true;
top.vel.y = 0;
}
}
// Normal collision - both move
else {
var v2 = (top.vel.y - bottom.vel.y)/2;
top.vel.y = -v2;
bottom.vel.y = v2;
var nudgeX = bottom.vel.x * ig.system.tick;
var resTop = ig.game.collisionMap.trace(
top.pos.x, top.pos.y, nudgeX, -nudge/2, top.size.x, top.size.y
);
top.pos.y = resTop.pos.y;
var resBottom = ig.game.collisionMap.trace(
bottom.pos.x, bottom.pos.y, 0, nudge/2, bottom.size.x, bottom.size.y
);
bottom.pos.y = resBottom.pos.y;
}
};
});
+152
View File
@@ -0,0 +1,152 @@
ig.module(
'impact.font'
)
.requires(
'impact.image'
)
.defines(function(){ "use strict";
ig.Font = ig.Image.extend({
widthMap: [],
indices: [],
firstChar: 32,
alpha: 1,
letterSpacing: 1,
lineSpacing: 0,
onload: function( ev ) {
this._loadMetrics( this.data );
this.parent( ev );
},
widthForString: function( text ) {
// Multiline?
if( text.indexOf('\n') !== -1 ) {
var lines = text.split( '\n' );
var width = 0;
for( var i = 0; i < lines.length; i++ ) {
width = Math.max( width, this._widthForLine(lines[i]) );
}
return width;
}
else {
return this._widthForLine( text );
}
},
_widthForLine: function( text ) {
var width = 0;
for( var i = 0; i < text.length; i++ ) {
width += this.widthMap[text.charCodeAt(i) - this.firstChar] + this.letterSpacing;
}
return width;
},
heightForString: function( text ) {
return text.split('\n').length * (this.height + this.lineSpacing);
},
draw: function( text, x, y, align ) {
if( typeof(text) != 'string' ) {
text = text.toString();
}
// Multiline?
if( text.indexOf('\n') !== -1 ) {
var lines = text.split( '\n' );
var lineHeight = this.height + this.lineSpacing;
for( var i = 0; i < lines.length; i++ ) {
this.draw( lines[i], x, y + i * lineHeight, align );
}
return;
}
if( align == ig.Font.ALIGN.RIGHT || align == ig.Font.ALIGN.CENTER ) {
var width = this._widthForLine( text );
x -= align == ig.Font.ALIGN.CENTER ? width/2 : width;
}
if( this.alpha !== 1 ) {
ig.system.context.globalAlpha = this.alpha;
}
for( var i = 0; i < text.length; i++ ) {
var c = text.charCodeAt(i);
x += this._drawChar( c - this.firstChar, x, y );
}
if( this.alpha !== 1 ) {
ig.system.context.globalAlpha = 1;
}
ig.Image.drawCount += text.length;
},
_drawChar: function( c, targetX, targetY ) {
if( !this.loaded || c < 0 || c >= this.indices.length ) { return 0; }
var scale = ig.system.scale;
var charX = this.indices[c] * scale;
var charY = 0;
var charWidth = this.widthMap[c] * scale;
var charHeight = (this.height-2) * scale;
ig.system.context.drawImage(
this.data,
charX, charY,
charWidth, charHeight,
ig.system.getDrawPos(targetX), ig.system.getDrawPos(targetY),
charWidth, charHeight
);
return this.widthMap[c] + this.letterSpacing;
},
_loadMetrics: function( image ) {
// Draw the bottommost line of this font image into an offscreen canvas
// and analyze it pixel by pixel.
// A run of non-transparent pixels represents a character and its width
this.height = image.height-1;
this.widthMap = [];
this.indices = [];
var px = ig.getImagePixels( image, 0, image.height-1, image.width, 1 );
var currentChar = 0;
var currentWidth = 0;
for( var x = 0; x < image.width; x++ ) {
var index = x * 4 + 3; // alpha component of this pixel
if( px.data[index] > 127 ) {
currentWidth++;
}
else if( px.data[index] < 128 && currentWidth ) {
this.widthMap.push( currentWidth );
this.indices.push( x-currentWidth );
currentChar++;
currentWidth = 0;
}
}
this.widthMap.push( currentWidth );
this.indices.push( x-currentWidth );
}
});
ig.Font.ALIGN = {
LEFT: 0,
RIGHT: 1,
CENTER: 2
};
});
+326
View File
@@ -0,0 +1,326 @@
ig.module(
'impact.game'
)
.requires(
'impact.impact',
'impact.entity',
'impact.collision-map',
'impact.background-map'
)
.defines(function(){ "use strict";
ig.Game = ig.Class.extend({
clearColor: '#000000',
gravity: 0,
screen: {x: 0, y: 0},
_rscreen: {x: 0, y: 0},
entities: [],
namedEntities: {},
collisionMap: ig.CollisionMap.staticNoCollision,
backgroundMaps: [],
backgroundAnims: {},
autoSort: false,
sortBy: null,
cellSize: 64,
_deferredKill: [],
_levelToLoad: null,
_doSortEntities: false,
staticInstantiate: function() {
this.sortBy = this.sortBy || ig.Game.SORT.Z_INDEX;
ig.game = this;
return null;
},
loadLevel: function( data ) {
this.screen = {x: 0, y: 0};
// Entities
this.entities = [];
this.namedEntities = {};
for( var i = 0; i < data.entities.length; i++ ) {
var ent = data.entities[i];
this.spawnEntity( ent.type, ent.x, ent.y, ent.settings );
}
this.sortEntities();
// Map Layer
this.collisionMap = ig.CollisionMap.staticNoCollision;
this.backgroundMaps = [];
for( var i = 0; i < data.layer.length; i++ ) {
var ld = data.layer[i];
if( ld.name == 'collision' ) {
this.collisionMap = new ig.CollisionMap(ld.tilesize, ld.data );
}
else {
var newMap = new ig.BackgroundMap(ld.tilesize, ld.data, ld.tilesetName);
newMap.anims = this.backgroundAnims[ld.tilesetName] || {};
newMap.repeat = ld.repeat;
newMap.distance = ld.distance;
newMap.foreground = !!ld.foreground;
newMap.preRender = !!ld.preRender;
newMap.name = ld.name;
this.backgroundMaps.push( newMap );
}
}
// Call post-init ready function on all entities
for( var i = 0; i < this.entities.length; i++ ) {
this.entities[i].ready();
}
},
loadLevelDeferred: function( data ) {
this._levelToLoad = data;
},
getMapByName: function( name ) {
if( name == 'collision' ) {
return this.collisionMap;
}
for( var i = 0; i < this.backgroundMaps.length; i++ ) {
if( this.backgroundMaps[i].name == name ) {
return this.backgroundMaps[i];
}
}
return null;
},
getEntityByName: function( name ) {
return this.namedEntities[name];
},
getEntitiesByType: function( type ) {
var entityClass = typeof(type) === 'string'
? ig.global[type]
: type;
var a = [];
for( var i = 0; i < this.entities.length; i++ ) {
var ent = this.entities[i];
if( ent instanceof entityClass && !ent._killed ) {
a.push( ent );
}
}
return a;
},
spawnEntity: function( type, x, y, settings ) {
var entityClass = typeof(type) === 'string'
? ig.global[type]
: type;
if( !entityClass ) {
throw("Can't spawn entity of type " + type);
}
var ent = new (entityClass)( x, y, settings || {} );
this.entities.push( ent );
if( ent.name ) {
this.namedEntities[ent.name] = ent;
}
return ent;
},
sortEntities: function() {
this.entities.sort( this.sortBy );
},
sortEntitiesDeferred: function() {
this._doSortEntities = true;
},
removeEntity: function( ent ) {
// Remove this entity from the named entities
if( ent.name ) {
delete this.namedEntities[ent.name];
}
// We can not remove the entity from the entities[] array in the midst
// of an update cycle, so remember all killed entities and remove
// them later.
// Also make sure this entity doesn't collide anymore and won't get
// updated or checked
ent._killed = true;
ent.type = ig.Entity.TYPE.NONE;
ent.checkAgainst = ig.Entity.TYPE.NONE;
ent.collides = ig.Entity.COLLIDES.NEVER;
this._deferredKill.push( ent );
},
run: function() {
this.update();
this.draw();
},
update: function(){
// load new level?
if( this._levelToLoad ) {
this.loadLevel( this._levelToLoad );
this._levelToLoad = null;
}
// update entities
this.updateEntities();
this.checkEntities();
// remove all killed entities
for( var i = 0; i < this._deferredKill.length; i++ ) {
this._deferredKill[i].erase();
this.entities.erase( this._deferredKill[i] );
}
this._deferredKill = [];
// sort entities?
if( this._doSortEntities || this.autoSort ) {
this.sortEntities();
this._doSortEntities = false;
}
// update background animations
for( var tileset in this.backgroundAnims ) {
var anims = this.backgroundAnims[tileset];
for( var a in anims ) {
anims[a].update();
}
}
},
updateEntities: function() {
for( var i = 0; i < this.entities.length; i++ ) {
var ent = this.entities[i];
if( !ent._killed ) {
ent.update();
}
}
},
draw: function(){
if( this.clearColor ) {
ig.system.clear( this.clearColor );
}
// This is a bit of a circle jerk. Entities reference game._rscreen
// instead of game.screen when drawing themselfs in order to be
// "synchronized" to the rounded(?) screen position
this._rscreen.x = ig.system.getDrawPos(this.screen.x)/ig.system.scale;
this._rscreen.y = ig.system.getDrawPos(this.screen.y)/ig.system.scale;
var mapIndex;
for( mapIndex = 0; mapIndex < this.backgroundMaps.length; mapIndex++ ) {
var map = this.backgroundMaps[mapIndex];
if( map.foreground ) {
// All foreground layers are drawn after the entities
break;
}
map.setScreenPos( this.screen.x, this.screen.y );
map.draw();
}
this.drawEntities();
for( mapIndex; mapIndex < this.backgroundMaps.length; mapIndex++ ) {
var map = this.backgroundMaps[mapIndex];
map.setScreenPos( this.screen.x, this.screen.y );
map.draw();
}
},
drawEntities: function() {
for( var i = 0; i < this.entities.length; i++ ) {
this.entities[i].draw();
}
},
checkEntities: function() {
// Insert all entities into a spatial hash and check them against any
// other entity that already resides in the same cell. Entities that are
// bigger than a single cell, are inserted into each one they intersect
// with.
// A list of entities, which the current one was already checked with,
// is maintained for each entity.
var hash = {};
for( var e = 0; e < this.entities.length; e++ ) {
var entity = this.entities[e];
// Skip entities that don't check, don't get checked and don't collide
if(
entity.type == ig.Entity.TYPE.NONE &&
entity.checkAgainst == ig.Entity.TYPE.NONE &&
entity.collides == ig.Entity.COLLIDES.NEVER
) {
continue;
}
var checked = {},
xmin = Math.floor( entity.pos.x/this.cellSize ),
ymin = Math.floor( entity.pos.y/this.cellSize ),
xmax = Math.floor( (entity.pos.x+entity.size.x)/this.cellSize ) + 1,
ymax = Math.floor( (entity.pos.y+entity.size.y)/this.cellSize ) + 1;
for( var x = xmin; x < xmax; x++ ) {
for( var y = ymin; y < ymax; y++ ) {
// Current cell is empty - create it and insert!
if( !hash[x] ) {
hash[x] = {};
hash[x][y] = [entity];
}
else if( !hash[x][y] ) {
hash[x][y] = [entity];
}
// Check against each entity in this cell, then insert
else {
var cell = hash[x][y];
for( var c = 0; c < cell.length; c++ ) {
// Intersects and wasn't already checkd?
if( entity.touches(cell[c]) && !checked[cell[c].id] ) {
checked[cell[c].id] = true;
ig.Entity.checkPair( entity, cell[c] );
}
}
cell.push(entity);
}
} // end for y size
} // end for x size
} // end for entities
}
});
ig.Game.SORT = {
Z_INDEX: function( a, b ){ return a.zIndex - b.zIndex; },
POS_X: function( a, b ){ return (a.pos.x+a.size.x) - (b.pos.x+b.size.x); },
POS_Y: function( a, b ){ return (a.pos.y+a.size.y) - (b.pos.y+b.size.y); }
};
});
+178
View File
@@ -0,0 +1,178 @@
ig.module(
'impact.image'
)
.defines(function(){ "use strict";
ig.Image = ig.Class.extend({
data: null,
width: 0,
height: 0,
loaded: false,
failed: false,
loadCallback: null,
path: '',
staticInstantiate: function( path ) {
return ig.Image.cache[path] || null;
},
init: function( path ) {
this.path = path;
this.load();
},
load: function( loadCallback ) {
if( this.loaded ) {
if( loadCallback ) {
loadCallback( this.path, true );
}
return;
}
else if( !this.loaded && ig.ready ) {
this.loadCallback = loadCallback || null;
this.data = new Image();
this.data.onload = this.onload.bind(this);
this.data.onerror = this.onerror.bind(this);
this.data.src = ig.prefix + this.path + ig.nocache;
}
else {
ig.addResource( this );
}
ig.Image.cache[this.path] = this;
},
reload: function() {
this.loaded = false;
this.data = new Image();
this.data.onload = this.onload.bind(this);
this.data.src = this.path + '?' + Date.now();
},
onload: function( event ) {
this.width = this.data.width;
this.height = this.data.height;
this.loaded = true;
if( ig.system.scale != 1 ) {
this.resize( ig.system.scale );
}
if( this.loadCallback ) {
this.loadCallback( this.path, true );
}
},
onerror: function( event ) {
this.failed = true;
if( this.loadCallback ) {
this.loadCallback( this.path, false );
}
},
resize: function( scale ) {
// Nearest-Neighbor scaling
// The original image is drawn into an offscreen canvas of the same size
// and copied into another offscreen canvas with the new size.
// The scaled offscreen canvas becomes the image (data) of this object.
var origPixels = ig.getImagePixels( this.data, 0, 0, this.width, this.height );
var widthScaled = this.width * scale;
var heightScaled = this.height * scale;
var scaled = ig.$new('canvas');
scaled.width = widthScaled;
scaled.height = heightScaled;
var scaledCtx = scaled.getContext('2d');
var scaledPixels = scaledCtx.getImageData( 0, 0, widthScaled, heightScaled );
for( var y = 0; y < heightScaled; y++ ) {
for( var x = 0; x < widthScaled; x++ ) {
var index = (Math.floor(y / scale) * this.width + Math.floor(x / scale)) * 4;
var indexScaled = (y * widthScaled + x) * 4;
scaledPixels.data[ indexScaled ] = origPixels.data[ index ];
scaledPixels.data[ indexScaled+1 ] = origPixels.data[ index+1 ];
scaledPixels.data[ indexScaled+2 ] = origPixels.data[ index+2 ];
scaledPixels.data[ indexScaled+3 ] = origPixels.data[ index+3 ];
}
}
scaledCtx.putImageData( scaledPixels, 0, 0 );
this.data = scaled;
},
draw: function( targetX, targetY, sourceX, sourceY, width, height ) {
if( !this.loaded ) { return; }
var scale = ig.system.scale;
sourceX = sourceX ? sourceX * scale : 0;
sourceY = sourceY ? sourceY * scale : 0;
width = (width ? width : this.width) * scale;
height = (height ? height : this.height) * scale;
ig.system.context.drawImage(
this.data, sourceX, sourceY, width, height,
ig.system.getDrawPos(targetX),
ig.system.getDrawPos(targetY),
width, height
);
ig.Image.drawCount++;
},
drawTile: function( targetX, targetY, tile, tileWidth, tileHeight, flipX, flipY ) {
tileHeight = tileHeight ? tileHeight : tileWidth;
if( !this.loaded || tileWidth > this.width || tileHeight > this.height ) { return; }
var scale = ig.system.scale;
var tileWidthScaled = Math.floor(tileWidth * scale);
var tileHeightScaled = Math.floor(tileHeight * scale);
var scaleX = flipX ? -1 : 1;
var scaleY = flipY ? -1 : 1;
if( flipX || flipY ) {
ig.system.context.save();
ig.system.context.scale( scaleX, scaleY );
}
ig.system.context.drawImage(
this.data,
( Math.floor(tile * tileWidth) % this.width ) * scale,
( Math.floor(tile * tileWidth / this.width) * tileHeight ) * scale,
tileWidthScaled,
tileHeightScaled,
ig.system.getDrawPos(targetX) * scaleX - (flipX ? tileWidthScaled : 0),
ig.system.getDrawPos(targetY) * scaleY - (flipY ? tileHeightScaled : 0),
tileWidthScaled,
tileHeightScaled
);
if( flipX || flipY ) {
ig.system.context.restore();
}
ig.Image.drawCount++;
}
});
ig.Image.drawCount = 0;
ig.Image.cache = {};
ig.Image.reloadCache = function() {
for( var path in ig.Image.cache ) {
ig.Image.cache[path].reload();
}
};
});
+590
View File
@@ -0,0 +1,590 @@
// -----------------------------------------------------------------------------
// Impact Game Engine 1.24
// http://impactjs.com/
// -----------------------------------------------------------------------------
(function(window){ "use strict";
// -----------------------------------------------------------------------------
// Native Object extensions
Number.prototype.map = function(istart, istop, ostart, ostop) {
return ostart + (ostop - ostart) * ((this - istart) / (istop - istart));
};
Number.prototype.limit = function(min, max) {
return Math.min(max, Math.max(min, this));
};
Number.prototype.round = function(precision) {
precision = Math.pow(10, precision || 0);
return Math.round(this * precision) / precision;
};
Number.prototype.floor = function() {
return Math.floor(this);
};
Number.prototype.ceil = function() {
return Math.ceil(this);
};
Number.prototype.toInt = function() {
return (this | 0);
};
Number.prototype.toRad = function() {
return (this / 180) * Math.PI;
};
Number.prototype.toDeg = function() {
return (this * 180) / Math.PI;
};
Object.defineProperty(Array.prototype, 'erase', {value: function(item) {
for( var i = this.length; i--; ) {
if( this[i] === item ) {
this.splice(i, 1);
}
}
return this;
}});
Object.defineProperty(Array.prototype, 'random', {value: function(item) {
return this[ Math.floor(Math.random() * this.length) ];
}});
Function.prototype.bind = Function.prototype.bind || function (oThis) {
if( typeof this !== "function" ) {
throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
}
var aArgs = Array.prototype.slice.call(arguments, 1),
fToBind = this,
fNOP = function () {},
fBound = function () {
return fToBind.apply(
(this instanceof fNOP && oThis ? this : oThis),
aArgs.concat(Array.prototype.slice.call(arguments))
);
};
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
};
// -----------------------------------------------------------------------------
// ig Namespace
window.ig = {
game: null,
debug: null,
version: '1.24',
global: window,
modules: {},
resources: [],
ready: false,
baked: false,
nocache: '',
ua: {},
prefix: (window.ImpactPrefix || ''),
lib: 'lib/',
_current: null,
_loadQueue: [],
_waitForOnload: 0,
$: function( selector ) {
return selector.charAt(0) == '#'
? document.getElementById( selector.substr(1) )
: document.getElementsByTagName( selector );
},
$new: function( name ) {
return document.createElement( name );
},
copy: function( object ) {
if(
!object || typeof(object) != 'object' ||
object instanceof HTMLElement ||
object instanceof ig.Class
) {
return object;
}
else if( object instanceof Array ) {
var c = [];
for( var i = 0, l = object.length; i < l; i++) {
c[i] = ig.copy(object[i]);
}
return c;
}
else {
var c = {};
for( var i in object ) {
c[i] = ig.copy(object[i]);
}
return c;
}
},
merge: function( original, extended ) {
for( var key in extended ) {
var ext = extended[key];
if(
typeof(ext) != 'object' ||
ext instanceof HTMLElement ||
ext instanceof ig.Class ||
ext === null
) {
original[key] = ext;
}
else {
if( !original[key] || typeof(original[key]) != 'object' ) {
original[key] = (ext instanceof Array) ? [] : {};
}
ig.merge( original[key], ext );
}
}
return original;
},
ksort: function( obj ) {
if( !obj || typeof(obj) != 'object' ) {
return [];
}
var keys = [], values = [];
for( var i in obj ) {
keys.push(i);
}
keys.sort();
for( var i = 0; i < keys.length; i++ ) {
values.push( obj[keys[i]] );
}
return values;
},
// Ah, yes. I love vendor prefixes. So much fun!
setVendorAttribute: function( el, attr, val ) {
var uc = attr.charAt(0).toUpperCase() + attr.substr(1);
el[attr] = el['ms'+uc] = el['moz'+uc] = el['webkit'+uc] = el['o'+uc] = val;
},
getVendorAttribute: function( el, attr ) {
var uc = attr.charAt(0).toUpperCase() + attr.substr(1);
return el[attr] || el['ms'+uc] || el['moz'+uc] || el['webkit'+uc] || el['o'+uc];
},
normalizeVendorAttribute: function( el, attr ) {
var prefixedVal = ig.getVendorAttribute( el, attr );
if( !el[attr] && prefixedVal ) {
el[attr] = prefixedVal;
}
},
// This function normalizes getImageData to extract the real, actual
// pixels from an image. The naive method recently failed on retina
// devices with a backgingStoreRatio != 1
getImagePixels: function( image, x, y, width, height ) {
var canvas = ig.$new('canvas');
canvas.width = image.width;
canvas.height = image.height;
var ctx = canvas.getContext('2d');
// Try to draw pixels as accurately as possible
ig.System.SCALE.CRISP(canvas, ctx);
var ratio = ig.getVendorAttribute( ctx, 'backingStorePixelRatio' ) || 1;
ig.normalizeVendorAttribute( ctx, 'getImageDataHD' );
var realWidth = image.width / ratio,
realHeight = image.height / ratio;
canvas.width = Math.ceil( realWidth );
canvas.height = Math.ceil( realHeight );
ctx.drawImage( image, 0, 0, realWidth, realHeight );
return (ratio === 1)
? ctx.getImageData( x, y, width, height )
: ctx.getImageDataHD( x, y, width, height );
},
module: function( name ) {
if( ig._current ) {
throw( "Module '"+ig._current.name+"' defines nothing" );
}
if( ig.modules[name] && ig.modules[name].body ) {
throw( "Module '"+name+"' is already defined" );
}
ig._current = {name: name, requires: [], loaded: false, body: null};
ig.modules[name] = ig._current;
ig._loadQueue.push(ig._current);
return ig;
},
requires: function() {
ig._current.requires = Array.prototype.slice.call(arguments);
return ig;
},
defines: function( body ) {
ig._current.body = body;
ig._current = null;
ig._initDOMReady();
},
addResource: function( resource ) {
ig.resources.push( resource );
},
setNocache: function( set ) {
ig.nocache = set
? '?' + Date.now()
: '';
},
// Stubs for ig.Debug
log: function() {},
assert: function( condition, msg ) {},
show: function( name, number ) {},
mark: function( msg, color ) {},
_loadScript: function( name, requiredFrom ) {
ig.modules[name] = {name: name, requires:[], loaded: false, body: null};
ig._waitForOnload++;
var path = ig.prefix + ig.lib + name.replace(/\./g, '/') + '.js' + ig.nocache;
var script = ig.$new('script');
script.type = 'text/javascript';
script.src = path;
script.onload = function() {
ig._waitForOnload--;
ig._execModules();
};
script.onerror = function() {
throw(
'Failed to load module '+name+' at ' + path + ' ' +
'required from ' + requiredFrom
);
};
ig.$('head')[0].appendChild(script);
},
_execModules: function() {
var modulesLoaded = false;
for( var i = 0; i < ig._loadQueue.length; i++ ) {
var m = ig._loadQueue[i];
var dependenciesLoaded = true;
for( var j = 0; j < m.requires.length; j++ ) {
var name = m.requires[j];
if( !ig.modules[name] ) {
dependenciesLoaded = false;
ig._loadScript( name, m.name );
}
else if( !ig.modules[name].loaded ) {
dependenciesLoaded = false;
}
}
if( dependenciesLoaded && m.body ) {
ig._loadQueue.splice(i, 1);
m.loaded = true;
m.body();
modulesLoaded = true;
i--;
}
}
if( modulesLoaded ) {
ig._execModules();
}
// No modules executed, no more files to load but loadQueue not empty?
// Must be some unresolved dependencies!
else if( !ig.baked && ig._waitForOnload == 0 && ig._loadQueue.length != 0 ) {
var unresolved = [];
for( var i = 0; i < ig._loadQueue.length; i++ ) {
// Which dependencies aren't loaded?
var unloaded = [];
var requires = ig._loadQueue[i].requires;
for( var j = 0; j < requires.length; j++ ) {
var m = ig.modules[ requires[j] ];
if( !m || !m.loaded ) {
unloaded.push( requires[j] );
}
}
unresolved.push( ig._loadQueue[i].name + ' (requires: ' + unloaded.join(', ') + ')');
}
throw(
"Unresolved (or circular?) dependencies. " +
"Most likely there's a name/path mismatch for one of the listed modules " +
"or a previous syntax error prevents a module from loading:\n" +
unresolved.join('\n')
);
}
},
_DOMReady: function() {
if( !ig.modules['dom.ready'].loaded ) {
if ( !document.body ) {
return setTimeout( ig._DOMReady, 13 );
}
ig.modules['dom.ready'].loaded = true;
ig._waitForOnload--;
ig._execModules();
}
return 0;
},
_boot: function() {
if( document.location.href.match(/\?nocache/) ) {
ig.setNocache( true );
}
// Probe user agent string
ig.ua.pixelRatio = window.devicePixelRatio || 1;
ig.ua.viewport = {
width: window.innerWidth,
height: window.innerHeight
};
ig.ua.screen = {
width: window.screen.availWidth * ig.ua.pixelRatio,
height: window.screen.availHeight * ig.ua.pixelRatio
};
ig.ua.iPhone = /iPhone/i.test(navigator.userAgent);
ig.ua.iPhone4 = (ig.ua.iPhone && ig.ua.pixelRatio == 2);
ig.ua.iPad = /iPad/i.test(navigator.userAgent);
ig.ua.android = /android/i.test(navigator.userAgent);
ig.ua.winPhone = /Windows Phone/i.test(navigator.userAgent);
ig.ua.iOS = ig.ua.iPhone || ig.ua.iPad;
ig.ua.mobile = ig.ua.iOS || ig.ua.android || ig.ua.winPhone || /mobile/i.test(navigator.userAgent);
ig.ua.touchDevice = (('ontouchstart' in window) || (window.navigator.msMaxTouchPoints));
},
_initDOMReady: function() {
if( ig.modules['dom.ready'] ) {
ig._execModules();
return;
}
ig._boot();
ig.modules['dom.ready'] = { requires: [], loaded: false, body: null };
ig._waitForOnload++;
if ( document.readyState === 'complete' ) {
ig._DOMReady();
}
else {
document.addEventListener( 'DOMContentLoaded', ig._DOMReady, false );
window.addEventListener( 'load', ig._DOMReady, false );
}
}
};
// -----------------------------------------------------------------------------
// Provide ig.setAnimation and ig.clearAnimation as a compatible way to use
// requestAnimationFrame if available or setInterval otherwise
// Use requestAnimationFrame if available
ig.normalizeVendorAttribute( window, 'requestAnimationFrame' );
if( window.requestAnimationFrame ) {
var next = 1,
anims = {};
window.ig.setAnimation = function( callback, element ) {
var current = next++;
anims[current] = true;
var animate = function() {
if( !anims[current] ) { return; } // deleted?
window.requestAnimationFrame( animate, element );
callback();
};
window.requestAnimationFrame( animate, element );
return current;
};
window.ig.clearAnimation = function( id ) {
delete anims[id];
};
}
// [set/clear]Interval fallback
else {
window.ig.setAnimation = function( callback, element ) {
return window.setInterval( callback, 1000/60 );
};
window.ig.clearAnimation = function( id ) {
window.clearInterval( id );
};
}
// -----------------------------------------------------------------------------
// Class object based on John Resigs code; inspired by base2 and Prototype
// http://ejohn.org/blog/simple-javascript-inheritance/
var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\bparent\b/ : /.*/;
var lastClassId = 0;
window.ig.Class = function(){};
var inject = function(prop) {
var proto = this.prototype;
var parent = {};
for( var name in prop ) {
if(
typeof(prop[name]) == "function" &&
typeof(proto[name]) == "function" &&
fnTest.test(prop[name])
) {
parent[name] = proto[name]; // save original function
proto[name] = (function(name, fn){
return function() {
var tmp = this.parent;
this.parent = parent[name];
var ret = fn.apply(this, arguments);
this.parent = tmp;
return ret;
};
})( name, prop[name] );
}
else {
proto[name] = prop[name];
}
}
};
window.ig.Class.extend = function(prop) {
var parent = this.prototype;
initializing = true;
var prototype = new this();
initializing = false;
for( var name in prop ) {
if(
typeof(prop[name]) == "function" &&
typeof(parent[name]) == "function" &&
fnTest.test(prop[name])
) {
prototype[name] = (function(name, fn){
return function() {
var tmp = this.parent;
this.parent = parent[name];
var ret = fn.apply(this, arguments);
this.parent = tmp;
return ret;
};
})( name, prop[name] );
}
else {
prototype[name] = prop[name];
}
}
function Class() {
if( !initializing ) {
// If this class has a staticInstantiate method, invoke it
// and check if we got something back. If not, the normal
// constructor (init) is called.
if( this.staticInstantiate ) {
var obj = this.staticInstantiate.apply(this, arguments);
if( obj ) {
return obj;
}
}
for( var p in this ) {
if( typeof(this[p]) == 'object' ) {
this[p] = ig.copy(this[p]); // deep copy!
}
}
if( this.init ) {
this.init.apply(this, arguments);
}
}
return this;
}
Class.prototype = prototype;
Class.prototype.constructor = Class;
Class.extend = window.ig.Class.extend;
Class.inject = inject;
Class.classId = prototype.classId = ++lastClassId;
return Class;
};
// Merge the ImpactMixin - if present - into the 'ig' namespace. This gives other
// code the chance to modify 'ig' before it's doing any work.
if( window.ImpactMixin ) {
ig.merge(ig, window.ImpactMixin);
}
})(window);
// -----------------------------------------------------------------------------
// The main() function creates the system, input, sound and game objects,
// creates a preloader and starts the run loop
ig.module(
'impact.impact'
)
.requires(
'dom.ready',
'impact.loader',
'impact.system',
'impact.input',
'impact.sound'
)
.defines(function(){ "use strict";
ig.main = function( canvasId, gameClass, fps, width, height, scale, loaderClass ) {
ig.system = new ig.System( canvasId, fps, width, height, scale || 1 );
ig.input = new ig.Input();
ig.soundManager = new ig.SoundManager();
ig.music = new ig.Music();
ig.ready = true;
var loader = new (loaderClass || ig.Loader)( gameClass, ig.resources );
loader.load();
};
});
+321
View File
@@ -0,0 +1,321 @@
ig.module(
'impact.input'
)
.defines(function(){ "use strict";
ig.KEY = {
'MOUSE1': -1,
'MOUSE2': -3,
'MWHEEL_UP': -4,
'MWHEEL_DOWN': -5,
'BACKSPACE': 8,
'TAB': 9,
'ENTER': 13,
'PAUSE': 19,
'CAPS': 20,
'ESC': 27,
'SPACE': 32,
'PAGE_UP': 33,
'PAGE_DOWN': 34,
'END': 35,
'HOME': 36,
'LEFT_ARROW': 37,
'UP_ARROW': 38,
'RIGHT_ARROW': 39,
'DOWN_ARROW': 40,
'INSERT': 45,
'DELETE': 46,
'_0': 48,
'_1': 49,
'_2': 50,
'_3': 51,
'_4': 52,
'_5': 53,
'_6': 54,
'_7': 55,
'_8': 56,
'_9': 57,
'A': 65,
'B': 66,
'C': 67,
'D': 68,
'E': 69,
'F': 70,
'G': 71,
'H': 72,
'I': 73,
'J': 74,
'K': 75,
'L': 76,
'M': 77,
'N': 78,
'O': 79,
'P': 80,
'Q': 81,
'R': 82,
'S': 83,
'T': 84,
'U': 85,
'V': 86,
'W': 87,
'X': 88,
'Y': 89,
'Z': 90,
'NUMPAD_0': 96,
'NUMPAD_1': 97,
'NUMPAD_2': 98,
'NUMPAD_3': 99,
'NUMPAD_4': 100,
'NUMPAD_5': 101,
'NUMPAD_6': 102,
'NUMPAD_7': 103,
'NUMPAD_8': 104,
'NUMPAD_9': 105,
'MULTIPLY': 106,
'ADD': 107,
'SUBSTRACT': 109,
'DECIMAL': 110,
'DIVIDE': 111,
'F1': 112,
'F2': 113,
'F3': 114,
'F4': 115,
'F5': 116,
'F6': 117,
'F7': 118,
'F8': 119,
'F9': 120,
'F10': 121,
'F11': 122,
'F12': 123,
'SHIFT': 16,
'CTRL': 17,
'ALT': 18,
'PLUS': 187,
'COMMA': 188,
'MINUS': 189,
'PERIOD': 190
};
ig.Input = ig.Class.extend({
bindings: {},
actions: {},
presses: {},
locks: {},
delayedKeyup: {},
isUsingMouse: false,
isUsingKeyboard: false,
isUsingAccelerometer: false,
mouse: {x: 0, y: 0},
accel: {x: 0, y: 0, z: 0},
initMouse: function() {
if( this.isUsingMouse ) { return; }
this.isUsingMouse = true;
var mouseWheelBound = this.mousewheel.bind(this);
ig.system.canvas.addEventListener('mousewheel', mouseWheelBound, false );
ig.system.canvas.addEventListener('DOMMouseScroll', mouseWheelBound, false );
ig.system.canvas.addEventListener('contextmenu', this.contextmenu.bind(this), false );
ig.system.canvas.addEventListener('mousedown', this.keydown.bind(this), false );
ig.system.canvas.addEventListener('mouseup', this.keyup.bind(this), false );
ig.system.canvas.addEventListener('mousemove', this.mousemove.bind(this), false );
if( ig.ua.touchDevice ) {
// Standard
ig.system.canvas.addEventListener('touchstart', this.keydown.bind(this), false );
ig.system.canvas.addEventListener('touchend', this.keyup.bind(this), false );
ig.system.canvas.addEventListener('touchmove', this.mousemove.bind(this), false );
// MS
ig.system.canvas.addEventListener('MSPointerDown', this.keydown.bind(this), false );
ig.system.canvas.addEventListener('MSPointerUp', this.keyup.bind(this), false );
ig.system.canvas.addEventListener('MSPointerMove', this.mousemove.bind(this), false );
ig.system.canvas.style.msTouchAction = 'none';
}
},
initKeyboard: function() {
if( this.isUsingKeyboard ) { return; }
this.isUsingKeyboard = true;
window.addEventListener('keydown', this.keydown.bind(this), false );
window.addEventListener('keyup', this.keyup.bind(this), false );
},
initAccelerometer: function() {
if( this.isUsingAccelerometer ) { return; }
this.isUsingAccelerometer = true;
window.addEventListener('devicemotion', this.devicemotion.bind(this), false );
},
mousewheel: function( event ) {
var delta = event.wheelDelta ? event.wheelDelta : (event.detail * -1);
var code = delta > 0 ? ig.KEY.MWHEEL_UP : ig.KEY.MWHEEL_DOWN;
var action = this.bindings[code];
if( action ) {
this.actions[action] = true;
this.presses[action] = true;
this.delayedKeyup[action] = true;
event.stopPropagation();
event.preventDefault();
}
},
mousemove: function( event ) {
var internalWidth = parseInt(ig.system.canvas.offsetWidth) || ig.system.realWidth;
var scale = ig.system.scale * (internalWidth / ig.system.realWidth);
var pos = {left: 0, top: 0};
if( ig.system.canvas.getBoundingClientRect ) {
pos = ig.system.canvas.getBoundingClientRect();
}
var ev = event.touches ? event.touches[0] : event;
this.mouse.x = (ev.clientX - pos.left) / scale;
this.mouse.y = (ev.clientY - pos.top) / scale;
},
contextmenu: function( event ) {
if( this.bindings[ig.KEY.MOUSE2] ) {
event.stopPropagation();
event.preventDefault();
}
},
keydown: function( event ) {
var tag = event.target.tagName;
if( tag == 'INPUT' || tag == 'TEXTAREA' ) { return; }
var code = event.type == 'keydown'
? event.keyCode
: (event.button == 2 ? ig.KEY.MOUSE2 : ig.KEY.MOUSE1);
// Focus window element for mouse clicks. Prevents issues when
// running the game in an iframe.
if( code < 0 && !ig.ua.mobile ) {
window.focus();
}
if( event.type == 'touchstart' || event.type == 'mousedown' ) {
this.mousemove( event );
}
var action = this.bindings[code];
if( action ) {
this.actions[action] = true;
if( !this.locks[action] ) {
this.presses[action] = true;
this.locks[action] = true;
}
event.preventDefault();
}
},
keyup: function( event ) {
var tag = event.target.tagName;
if( tag == 'INPUT' || tag == 'TEXTAREA' ) { return; }
var code = event.type == 'keyup'
? event.keyCode
: (event.button == 2 ? ig.KEY.MOUSE2 : ig.KEY.MOUSE1);
var action = this.bindings[code];
if( action ) {
this.delayedKeyup[action] = true;
event.preventDefault();
}
},
devicemotion: function( event ) {
this.accel = event.accelerationIncludingGravity;
},
bind: function( key, action ) {
if( key < 0 ) { this.initMouse(); }
else if( key > 0 ) { this.initKeyboard(); }
this.bindings[key] = action;
},
bindTouch: function( selector, action ) {
var element = ig.$( selector );
var that = this;
element.addEventListener('touchstart', function(ev) {that.touchStart( ev, action );}, false);
element.addEventListener('touchend', function(ev) {that.touchEnd( ev, action );}, false);
element.addEventListener('MSPointerDown', function(ev) {that.touchStart( ev, action );}, false);
element.addEventListener('MSPointerUp', function(ev) {that.touchEnd( ev, action );}, false);
},
unbind: function( key ) {
var action = this.bindings[key];
this.delayedKeyup[action] = true;
this.bindings[key] = null;
},
unbindAll: function() {
this.bindings = {};
this.actions = {};
this.presses = {};
this.locks = {};
this.delayedKeyup = {};
},
state: function( action ) {
return this.actions[action];
},
pressed: function( action ) {
return this.presses[action];
},
released: function( action ) {
return !!this.delayedKeyup[action];
},
clearPressed: function() {
for( var action in this.delayedKeyup ) {
this.actions[action] = false;
this.locks[action] = false;
}
this.delayedKeyup = {};
this.presses = {};
},
touchStart: function( event, action ) {
this.actions[action] = true;
this.presses[action] = true;
event.stopPropagation();
event.preventDefault();
return false;
},
touchEnd: function( event, action ) {
this.delayedKeyup[action] = true;
event.stopPropagation();
event.preventDefault();
return false;
}
});
});
+101
View File
@@ -0,0 +1,101 @@
ig.module(
'impact.loader'
)
.requires(
'impact.image',
'impact.font',
'impact.sound'
)
.defines(function(){ "use strict";
ig.Loader = ig.Class.extend({
resources: [],
gameClass: null,
status: 0,
done: false,
_unloaded: [],
_drawStatus: 0,
_intervalId: 0,
_loadCallbackBound: null,
init: function( gameClass, resources ) {
this.gameClass = gameClass;
this.resources = resources;
this._loadCallbackBound = this._loadCallback.bind(this);
for( var i = 0; i < this.resources.length; i++ ) {
this._unloaded.push( this.resources[i].path );
}
},
load: function() {
ig.system.clear( '#000' );
if( !this.resources.length ) {
this.end();
return;
}
for( var i = 0; i < this.resources.length; i++ ) {
this.loadResource( this.resources[i] );
}
this._intervalId = setInterval( this.draw.bind(this), 16 );
},
loadResource: function( res ) {
res.load( this._loadCallbackBound );
},
end: function() {
if( this.done ) { return; }
this.done = true;
clearInterval( this._intervalId );
ig.system.setGame( this.gameClass );
},
draw: function() {
this._drawStatus += (this.status - this._drawStatus)/5;
var s = ig.system.scale;
var w = ig.system.width * 0.6;
var h = ig.system.height * 0.1;
var x = ig.system.width * 0.5-w/2;
var y = ig.system.height * 0.5-h/2;
ig.system.context.fillStyle = '#000';
ig.system.context.fillRect( 0, 0, 480, 320 );
ig.system.context.fillStyle = '#fff';
ig.system.context.fillRect( x*s, y*s, w*s, h*s );
ig.system.context.fillStyle = '#000';
ig.system.context.fillRect( x*s+s, y*s+s, w*s-s-s, h*s-s-s );
ig.system.context.fillStyle = '#fff';
ig.system.context.fillRect( x*s, y*s, w*s*this._drawStatus, h*s );
},
_loadCallback: function( path, status ) {
if( status ) {
this._unloaded.erase( path );
}
else {
throw( 'Failed to load resource: ' + path );
}
this.status = 1 - (this._unloaded.length / this.resources.length);
if( this._unloaded.length == 0 ) { // all done?
setTimeout( this.end.bind(this), 250 );
}
}
});
});
+52
View File
@@ -0,0 +1,52 @@
ig.module(
'impact.map'
)
.defines(function(){ "use strict";
ig.Map = ig.Class.extend({
tilesize: 8,
width: 1,
height: 1,
data: [[]],
name: null,
init: function( tilesize, data ) {
this.tilesize = tilesize;
this.data = data;
this.height = data.length;
this.width = data[0].length;
this.pxWidth = this.width * this.tilesize;
this.pxHeight = this.height * this.tilesize;
},
getTile: function( x, y ) {
var tx = Math.floor( x / this.tilesize );
var ty = Math.floor( y / this.tilesize );
if(
(tx >= 0 && tx < this.width) &&
(ty >= 0 && ty < this.height)
) {
return this.data[ty][tx];
}
else {
return 0;
}
},
setTile: function( x, y, tile ) {
var tx = Math.floor( x / this.tilesize );
var ty = Math.floor( y / this.tilesize );
if(
(tx >= 0 && tx < this.width) &&
(ty >= 0 && ty < this.height)
) {
this.data[ty][tx] = tile;
}
}
});
});
+516
View File
@@ -0,0 +1,516 @@
ig.module(
'impact.sound'
)
.defines(function(){ "use strict";
ig.SoundManager = ig.Class.extend({
clips: {},
volume: 1,
format: null,
init: function() {
// Quick sanity check if the Browser supports the Audio tag
if( !ig.Sound.enabled || !window.Audio ) {
ig.Sound.enabled = false;
return;
}
// Probe sound formats and determine the file extension to load
var probe = new Audio();
for( var i = 0; i < ig.Sound.use.length; i++ ) {
var format = ig.Sound.use[i];
if( probe.canPlayType(format.mime) ) {
this.format = format;
break;
}
}
// No compatible format found? -> Disable sound
if( !this.format ) {
ig.Sound.enabled = false;
}
// Create WebAudio Context
if( ig.Sound.enabled && ig.Sound.useWebAudio ) {
this.audioContext = new AudioContext();
this.boundWebAudioUnlock = this.unlockWebAudio.bind(this);
document.addEventListener('touchstart', this.boundWebAudioUnlock, false);
}
},
unlockWebAudio: function() {
document.removeEventListener('touchstart', this.boundWebAudioUnlock, false);
// create empty buffer
var buffer = this.audioContext.createBuffer(1, 1, 22050);
var source = this.audioContext.createBufferSource();
source.buffer = buffer;
source.connect(this.audioContext.destination);
source.start(0);
},
load: function( path, multiChannel, loadCallback ) {
if( multiChannel && ig.Sound.useWebAudio ) {
// Requested as Multichannel and we're using WebAudio?
return this.loadWebAudio( path, multiChannel, loadCallback );
}
else {
// Oldschool HTML5 Audio - always used for Music
return this.loadHTML5Audio( path, multiChannel, loadCallback );
}
},
loadWebAudio: function( path, multiChannel, loadCallback ) {
// Path to the soundfile with the right extension (.ogg or .mp3)
var realPath = ig.prefix + path.replace(/[^\.]+$/, this.format.ext) + ig.nocache;
if( this.clips[path] ) {
return this.clips[path];
}
var audioSource = new ig.Sound.WebAudioSource()
this.clips[path] = audioSource;
var request = new XMLHttpRequest();
request.open('GET', realPath, true);
request.responseType = 'arraybuffer';
var that = this;
request.onload = function(ev) {
that.audioContext.decodeAudioData(request.response,
function(buffer) {
audioSource.buffer = buffer;
loadCallback( path, true, ev );
},
function(ev) {
loadCallback( path, false, ev );
}
);
};
request.onerror = function(ev) {
loadCallback( path, false, ev );
};
request.send();
return audioSource;
},
loadHTML5Audio: function( path, multiChannel, loadCallback ) {
// Path to the soundfile with the right extension (.ogg or .mp3)
var realPath = ig.prefix + path.replace(/[^\.]+$/, this.format.ext) + ig.nocache;
// Sound file already loaded?
if( this.clips[path] ) {
// Loaded as WebAudio, but now requested as HTML5 Audio? Probably Music?
if( this.clips[path] instanceof ig.Sound.WebAudioSource ) {
return this.clips[path];
}
// Only loaded as single channel and now requested as multichannel?
if( multiChannel && this.clips[path].length < ig.Sound.channels ) {
for( var i = this.clips[path].length; i < ig.Sound.channels; i++ ) {
var a = new Audio( realPath );
a.load();
this.clips[path].push( a );
}
}
return this.clips[path][0];
}
var clip = new Audio( realPath );
if( loadCallback ) {
// The canplaythrough event is dispatched when the browser determines
// that the sound can be played without interuption, provided the
// download rate doesn't change.
// Mobile browsers stubbornly refuse to preload HTML5, so we simply
// ignore the canplaythrough event and immediately "fake" a successful
// load callback
if( ig.ua.mobile ) {
setTimeout(function(){
loadCallback( path, true, null );
}, 0);
}
else {
clip.addEventListener( 'canplaythrough', function cb(ev){
clip.removeEventListener('canplaythrough', cb, false);
loadCallback( path, true, ev );
}, false );
clip.addEventListener( 'error', function(ev){
loadCallback( path, false, ev );
}, false);
}
}
clip.preload = 'auto';
clip.load();
this.clips[path] = [clip];
if( multiChannel ) {
for( var i = 1; i < ig.Sound.channels; i++ ) {
var a = new Audio(realPath);
a.load();
this.clips[path].push( a );
}
}
return clip;
},
get: function( path ) {
// Find and return a channel that is not currently playing
var channels = this.clips[path];
// Is this a WebAudio source? We only ever have one for each Sound
if( channels && channels instanceof ig.Sound.WebAudioSource ) {
return channels;
}
// Oldschool HTML5 Audio - find a channel that's not currently
// playing or, if all are playing, rewind one
for( var i = 0, clip; clip = channels[i++]; ) {
if( clip.paused || clip.ended ) {
if( clip.ended ) {
clip.currentTime = 0;
}
return clip;
}
}
// Still here? Pause and rewind the first channel
channels[0].pause();
channels[0].currentTime = 0;
return channels[0];
}
});
ig.Music = ig.Class.extend({
tracks: [],
namedTracks: {},
currentTrack: null,
currentIndex: 0,
random: false,
_volume: 1,
_loop: false,
_fadeInterval: 0,
_fadeTimer: null,
_endedCallbackBound: null,
init: function() {
this._endedCallbackBound = this._endedCallback.bind(this);
Object.defineProperty(this,"volume", {
get: this.getVolume.bind(this),
set: this.setVolume.bind(this)
});
Object.defineProperty(this,"loop", {
get: this.getLooping.bind(this),
set: this.setLooping.bind(this)
});
},
add: function( music, name ) {
if( !ig.Sound.enabled ) {
return;
}
var path = music instanceof ig.Sound ? music.path : music;
var track = ig.soundManager.load(path, false);
// Did we get a WebAudio Source? This is suboptimal; Music should be loaded
// as HTML5 Audio so it can be streamed
if( track instanceof ig.Sound.WebAudioSource ) {
// Since this error will likely occour at game start, we stop the game
// to not produce any more errors.
ig.system.stopRunLoop();
throw(
"Sound '"+path+"' loaded as Multichannel but used for Music. " +
"Set the multiChannel param to false when loading, e.g.: new ig.Sound(path, false)"
);
}
track.loop = this._loop;
track.volume = this._volume;
track.addEventListener( 'ended', this._endedCallbackBound, false );
this.tracks.push( track );
if( name ) {
this.namedTracks[name] = track;
}
if( !this.currentTrack ) {
this.currentTrack = track;
}
},
next: function() {
if( !this.tracks.length ) { return; }
this.stop();
this.currentIndex = this.random
? Math.floor(Math.random() * this.tracks.length)
: (this.currentIndex + 1) % this.tracks.length;
this.currentTrack = this.tracks[this.currentIndex];
this.play();
},
pause: function() {
if( !this.currentTrack ) { return; }
this.currentTrack.pause();
},
stop: function() {
if( !this.currentTrack ) { return; }
this.currentTrack.pause();
this.currentTrack.currentTime = 0;
},
play: function( name ) {
// If a name was provided, stop playing the current track (if any)
// and play the named track
if( name && this.namedTracks[name] ) {
var newTrack = this.namedTracks[name];
if( newTrack != this.currentTrack ) {
this.stop();
this.currentTrack = newTrack;
}
}
else if( !this.currentTrack ) {
return;
}
this.currentTrack.play();
},
getLooping: function() {
return this._loop;
},
setLooping: function( l ) {
this._loop = l;
for( var i in this.tracks ) {
this.tracks[i].loop = l;
}
},
getVolume: function() {
return this._volume;
},
setVolume: function( v ) {
this._volume = v.limit(0,1);
for( var i in this.tracks ) {
this.tracks[i].volume = this._volume;
}
},
fadeOut: function( time ) {
if( !this.currentTrack ) { return; }
clearInterval( this._fadeInterval );
this.fadeTimer = new ig.Timer( time );
this._fadeInterval = setInterval( this._fadeStep.bind(this), 50 );
},
_fadeStep: function() {
var v = this.fadeTimer.delta()
.map(-this.fadeTimer.target, 0, 1, 0)
.limit( 0, 1 )
* this._volume;
if( v <= 0.01 ) {
this.stop();
this.currentTrack.volume = this._volume;
clearInterval( this._fadeInterval );
}
else {
this.currentTrack.volume = v;
}
},
_endedCallback: function() {
if( this._loop ) {
this.play();
}
else {
this.next();
}
}
});
ig.Sound = ig.Class.extend({
path: '',
volume: 1,
currentClip: null,
multiChannel: true,
_loop: false,
init: function( path, multiChannel ) {
this.path = path;
this.multiChannel = (multiChannel !== false);
Object.defineProperty(this,"loop", {
get: this.getLooping.bind(this),
set: this.setLooping.bind(this)
});
this.load();
},
getLooping: function() {
return this._loop;
},
setLooping: function( loop ) {
this._loop = loop;
if( this.currentClip ) {
this.currentClip.loop = loop;
}
},
load: function( loadCallback ) {
if( !ig.Sound.enabled ) {
if( loadCallback ) {
loadCallback( this.path, true );
}
return;
}
if( ig.ready ) {
ig.soundManager.load( this.path, this.multiChannel, loadCallback );
}
else {
ig.addResource( this );
}
},
play: function() {
if( !ig.Sound.enabled ) {
return;
}
this.currentClip = ig.soundManager.get( this.path );
this.currentClip.loop = this._loop;
this.currentClip.volume = ig.soundManager.volume * this.volume;
this.currentClip.play();
},
stop: function() {
if( this.currentClip ) {
this.currentClip.pause();
this.currentClip.currentTime = 0;
}
}
});
ig.Sound.WebAudioSource = ig.Class.extend({
sources: [],
gain: null,
buffer: null,
_loop: false,
init: function() {
this.gain = ig.soundManager.audioContext.createGain();
this.gain.connect(ig.soundManager.audioContext.destination);
Object.defineProperty(this,"loop", {
get: this.getLooping.bind(this),
set: this.setLooping.bind(this)
});
Object.defineProperty(this,"volume", {
get: this.getVolume.bind(this),
set: this.setVolume.bind(this)
});
},
play: function() {
if( !this.buffer ) { return; }
var source = ig.soundManager.audioContext.createBufferSource();
source.buffer = this.buffer;
source.connect(this.gain);
source.loop = this._loop;
// Add this new source to our sources array and remove it again
// later when it has finished playing.
var that = this;
this.sources.push(source);
source.onended = function(){ that.sources.erase(source); }
source.start(0);
},
pause: function() {
for( var i = 0; i < this.sources.length; i++ ) {
try{
this.sources[i].stop();
} catch(err){}
}
},
getLooping: function() {
return this._loop;
},
setLooping: function( loop ) {
this._loop = loop;
for( var i = 0; i < this.sources.length; i++ ) {
this.sources[i].loop = loop;
}
},
getVolume: function() {
return this.gain.gain.value;
},
setVolume: function( volume ) {
this.gain.gain.value = volume;
}
});
ig.Sound.FORMAT = {
MP3: {ext: 'mp3', mime: 'audio/mpeg'},
M4A: {ext: 'm4a', mime: 'audio/mp4; codecs=mp4a'},
OGG: {ext: 'ogg', mime: 'audio/ogg; codecs=vorbis'},
WEBM: {ext: 'webm', mime: 'audio/webm; codecs=vorbis'},
CAF: {ext: 'caf', mime: 'audio/x-caf'}
};
ig.Sound.use = [ig.Sound.FORMAT.OGG, ig.Sound.FORMAT.MP3];
ig.Sound.channels = 4;
ig.Sound.enabled = true;
ig.normalizeVendorAttribute(window, 'AudioContext');
ig.Sound.useWebAudio = !!window.AudioContext && !window.nwf;
});
+145
View File
@@ -0,0 +1,145 @@
ig.module(
'impact.system'
)
.requires(
'impact.timer',
'impact.image'
)
.defines(function(){ "use strict";
ig.System = ig.Class.extend({
fps: 30,
width: 320,
height: 240,
realWidth: 320,
realHeight: 240,
scale: 1,
tick: 0,
animationId: 0,
newGameClass: null,
running: false,
delegate: null,
clock: null,
canvas: null,
context: null,
init: function( canvasId, fps, width, height, scale ) {
this.fps = fps;
this.clock = new ig.Timer();
this.canvas = ig.$(canvasId);
this.resize( width, height, scale );
this.context = this.canvas.getContext('2d');
this.getDrawPos = ig.System.drawMode;
// Automatically switch to crisp scaling when using a scale
// other than 1
if( this.scale != 1 ) {
ig.System.scaleMode = ig.System.SCALE.CRISP;
}
ig.System.scaleMode( this.canvas, this.context );
},
resize: function( width, height, scale ) {
this.width = width;
this.height = height;
this.scale = scale || this.scale;
this.realWidth = this.width * this.scale;
this.realHeight = this.height * this.scale;
this.canvas.width = this.realWidth;
this.canvas.height = this.realHeight;
},
setGame: function( gameClass ) {
if( this.running ) {
this.newGameClass = gameClass;
}
else {
this.setGameNow( gameClass );
}
},
setGameNow: function( gameClass ) {
ig.game = new (gameClass)();
ig.system.setDelegate( ig.game );
},
setDelegate: function( object ) {
if( typeof(object.run) == 'function' ) {
this.delegate = object;
this.startRunLoop();
} else {
throw( 'System.setDelegate: No run() function in object' );
}
},
stopRunLoop: function() {
ig.clearAnimation( this.animationId );
this.running = false;
},
startRunLoop: function() {
this.stopRunLoop();
this.animationId = ig.setAnimation( this.run.bind(this), this.canvas );
this.running = true;
},
clear: function( color ) {
this.context.fillStyle = color;
this.context.fillRect( 0, 0, this.realWidth, this.realHeight );
},
run: function() {
ig.Timer.step();
this.tick = this.clock.tick();
this.delegate.run();
ig.input.clearPressed();
if( this.newGameClass ) {
this.setGameNow( this.newGameClass );
this.newGameClass = null;
}
},
getDrawPos: null // Set through constructor
});
ig.System.DRAW = {
AUTHENTIC: function( p ) { return Math.round(p) * this.scale; },
SMOOTH: function( p ) { return Math.round(p * this.scale); },
SUBPIXEL: function( p ) { return p * this.scale; }
};
ig.System.drawMode = ig.System.DRAW.SMOOTH;
ig.System.SCALE = {
CRISP: function( canvas, context ) {
ig.setVendorAttribute( context, 'imageSmoothingEnabled', false );
canvas.style.imageRendering = '-moz-crisp-edges';
canvas.style.imageRendering = '-o-crisp-edges';
canvas.style.imageRendering = '-webkit-optimize-contrast';
canvas.style.imageRendering = 'crisp-edges';
canvas.style.msInterpolationMode = 'nearest-neighbor'; // No effect on Canvas :/
},
SMOOTH: function( canvas, context ) {
ig.setVendorAttribute( context, 'imageSmoothingEnabled', true );
canvas.style.imageRendering = '';
canvas.style.msInterpolationMode = '';
}
};
ig.System.scaleMode = ig.System.SCALE.SMOOTH;
});
+72
View File
@@ -0,0 +1,72 @@
ig.module(
'impact.timer'
)
.defines(function(){ "use strict";
ig.Timer = ig.Class.extend({
target: 0,
base: 0,
last: 0,
pausedAt: 0,
init: function( seconds ) {
this.base = ig.Timer.time;
this.last = ig.Timer.time;
this.target = seconds || 0;
},
set: function( seconds ) {
this.target = seconds || 0;
this.base = ig.Timer.time;
this.pausedAt = 0;
},
reset: function() {
this.base = ig.Timer.time;
this.pausedAt = 0;
},
tick: function() {
var delta = ig.Timer.time - this.last;
this.last = ig.Timer.time;
return (this.pausedAt ? 0 : delta);
},
delta: function() {
return (this.pausedAt || ig.Timer.time) - this.base - this.target;
},
pause: function() {
if( !this.pausedAt ) {
this.pausedAt = ig.Timer.time;
}
},
unpause: function() {
if( this.pausedAt ) {
this.base += ig.Timer.time - this.pausedAt;
this.pausedAt = 0;
}
}
});
ig.Timer._last = 0;
ig.Timer.time = Number.MIN_VALUE;
ig.Timer.timeScale = 1;
ig.Timer.maxStep = 0.05;
ig.Timer.step = function() {
var current = Date.now();
var delta = (current - ig.Timer._last) / 1000;
ig.Timer.time += Math.min(delta, ig.Timer.maxStep) * ig.Timer.timeScale;
ig.Timer._last = current;
};
});