# Bugs
-- Turret can fire through walls
+- collision short-circuiting, collision events
+- bullet collisions should explode both
+- 5-shot limit
+
+
# TODOs
- Move game objects into namespace `tanks`
# Notes
-- TODO Replace *2 and /2 with shifts at compile-time
+- Replace *2 and /2 with shifts at compile-time
- Clipping will suck (masking is easy -- overflow:hidden)
-- Should use unit vectors for trajectories?
---> matrix math for reflections?
+
+
# The Tank AI Challenge
return fn && fn !== self && fn.__wraps__ !== self;
}
-function reduce(o, fn, acc, cxt){
+function reduce(o, fn, acc){
if ( !o )
return acc;
- // fn = Function.toFunction(fn);
+ fn = Function.toFunction(fn);
+ var cxt = arguments[3] || o;
if ( notWrapped(o.reduce) )
return o.reduce.apply(o, [fn, acc, cxt]);
- cxt = cxt || o;
for ( var name in o )
acc = fn.call(cxt, acc, o[name], name, o);
return acc;
}
+function map(o, fn){
+ if ( !o )
+ return o;
+
+ fn = Function.toFunction(fn);
+ var acc = {}, cxt = arguments[2] || o;
+ if ( notWrapped(o.map) )
+ return o.map.apply(o, [fn, cxt]);
+ for ( var name in o )
+ acc[name] = fn.call(cxt, o[name], name, o);
+
+ return acc;
+}
+
+function forEach(o, fn){
+ map(o, fn, cxt);
+}
+
+function filter(o, fn){
+ if ( !o )
+ return o;
+
+ fn = Function.toFunction(fn);
+ var acc = {}, cxt = arguments[2] || o;
+ if ( notWrapped(o.filter) )
+ return o.filter.apply(o, [fn, cxt]);
+
+ for ( var name in o )
+ if ( fn.call(cxt, o[name], name, o) )
+ acc[name] = o[name];
+
+ return acc;
+}
+
function set(o, key, value, def){
if ( o && key !== undefined )
o[key] = (value !== undefined ? value : def);
},
addEventListener : function addEventListener(evt, fn){
- this.getQueue(evt).push(fn);
+ this.getQueue(evt).push(fn.toFunction());
return this.target;
},
removeEventListener : function removeEventListener(evt, fn){
- this.getQueue(evt).remove(fn);
+ this.getQueue(evt).remove(fn.toFunction());
},
fire : function fire(evtname, trigger, data, async){
-Y.reduce = reduce;
-Y.set = dset;
-Y.attr = dattr;
-Y.extend = extend;
+Y.reduce = reduce;
+Y.map = map;
+Y.forEach = forEach;
+Y.filter = filter;
+Y.set = dset;
+Y.attr = dattr;
+Y.extend = extend;
Y.isA = isA;
Y.isString = isString;
YFunction(Y);
Y(Y.reduce);
+Y(Y.map);
+Y(Y.forEach);
+Y(Y.filter);
Y(Y.set);
Y(Y.attr);
Y(Y.extend);
drawShape : function drawShape(ctx){
ctx.rect(0,0, this.canvasWidth,this.canvasHeight);
ctx.fill();
+ },
+
+ toString : function(){
+ var loc = this.loc
+ , x1 = loc.x, y1 = loc.y
+ , x2 = x1+this.layerWidth, y2 = y1 + this.layerHeight;
+ return this.className+'['+x1+','+y1+', '+x2+','+y2+'](w='+this.layerWidth+', h='+this.layerHeight+')';
}
});
+++ /dev/null
-
-Y(tanks.Game.prototype).extend({
-
- initMap : function initMap(){
- var self = this;
-
- this.byId = {};
- this.units = new Y.YArray();
- this.bullets = new Y.YArray();
- this.blockers = new Y.YArray();
-
- this.root =
- this.grid =
- new Grid(COLUMNS,ROWS, CELL_SIZE)
- .appendTo(this.viewport);
-
- this.level =
- new Level(this, COLUMNS*REF_SIZE, ROWS*REF_SIZE)
- .appendTo(this.root);
-
- this.pathmap = this.level.pathmap;
-
- // Agent.addEventListener('create', function(evt){
- // self.addAgent(evt.instance);
- // });
- // Agent.addEventListener('destroy', function(evt){
- // self.killAgent(evt.instance);
- // });
- },
-
-
- // *** Agent Management *** //
-
- addUnit : function addUnit(unit, col,row){
- unit.game = this;
-
- if (col !== undefined) {
- // Center unit in square
- var sqX = (col || 0) * REF_SIZE
- , sqY = (row || 0) * REF_SIZE
- , x = sqX + (REF_SIZE-unit.width) /2
- , y = sqY + (REF_SIZE-unit.height)/2 ;
-
- unit.setLocation(x,y);
- unit.render( this.level );
- }
-
- this.pathmap.addBlocker(unit);
-
- if ( !this.byId[unit.id] ) {
- this.byId[unit.id] = unit;
- this.units.push(unit);
- }
-
-
- return unit;
- },
-
- addAgent : function addAgent(agent){
- agent.game = this;
- if (agent.id === undefined) return agent;
-
- this.pathmap.addBlocker(agent);
-
- if ( !this.byId[agent.id] ) {
- this.byId[agent.id] = agent;
- if (agent instanceof Ability)
- this.abilities.push(agent);
- else
- this.units.push(agent);
- }
-
- return agent;
- },
-
- killUnit : function killUnit(unit){
- delete this.byId[unit.id];
- this.units.remove(unit);
- this.pathmap.removeBlocker(unit);
- return unit;
- },
-
- moveAgentTo : function moveAgentTo(agent, x,y){
- this.pathmap.removeBlocker(agent);
- agent.setLocation(x,y);
- this.pathmap.addBlocker(agent);
- return agent;
- },
-
- getUnitAt : function getUnitAt(x,y){
- return this.grid.get(x,y);
- },
-
- getUnitsAt : function getUnitsAt(x1,y1, x2,y2){
- return this.grid.get(x1,y1, x2,y2);
- }
-
-});
-
-Y(tanks.Game.prototype).extend({
-
- resize : function resize(){
- var ratio = COLUMNS / ROWS
- , el = this.el
- , p = el.parent()
- , pw = p.width(), ph = p.height()
- , pRatio = pw / ph
- ;
-
- if ( ratio > pRatio )
- CELL_SIZE = Math.floor((pw-GRID_OFFSET*2) / COLUMNS);
- else
- CELL_SIZE = Math.floor((ph-GRID_OFFSET*2) / ROWS);
-
- SCALE = CELL_SIZE/REF_SIZE;
-
- var w = COLUMNS*CELL_SIZE
- , h = ROWS*CELL_SIZE
- , canvas = this.canvas[0];
-
- this.el.width(w).height(h);
- this.canvas.width(w).height(h);
- canvas.width = w;
- canvas.height = h;
-
- this.el.offset({
- top : (ph - h) / 2,
- left : (pw - w) / 2
- });
-
- this.ctx.scale(SCALE,SCALE);
- }
-
-});
tanks.Game = new Y.Class('Game', {
init : function init(viewport){
+ Y.bindAll(this); // Seal all methods
+
+ this.byId = {};
+ this.active = new Y.YArray();
+ this.units = new Y.YArray();
+ this.bullets = new Y.YArray();
+ this.viewport = $(viewport || GRID_ELEMENT);
+
this.loop = new EventLoop(this, FRAME_RATE);
- // Seal all methods
- // Y.bindAll(this);
- this.resize = this.resize.bind(this);
- this.tick = this.tick.bind(this);
+ // this.resize = this.resize.bind(this);
+ // this.tick = this.tick.bind(this);
- this.viewport = $(viewport || GRID_ELEMENT);
+ this.root =
+ this.grid =
+ new Grid(COLUMNS,ROWS, CELL_SIZE)
+ .appendTo(this.viewport);
+
+ this.level =
+ new Level(this, COLUMNS*REF_SIZE, ROWS*REF_SIZE)
+ .appendTo(this.root);
- this.initMap();
+ this.pathmap = this.level.pathmap;
+
+
+ Thing.addEventListener('create', this.addUnit);
+ Thing.addEventListener('destroy', this.killUnit);
this.addEventListener('tick', this.tick);
},
+
+
draw : function draw(){
this.root.draw();
this.pathmap.removeOverlay(this.viewport);
SECONDTH = ELAPSED / 1000;
SQUARETH = REF_SIZE * SECONDTH
- this.units.invoke('act');
- // XXX: Collect the dead
+ this.active.invoke('act');
this.draw();
+ },
+
+
+
+
+ // *** Agent Management *** //
+
+ addUnit : function addUnit(unit, col,row){
+ if (unit instanceof Y.event.YEvent)
+ unit = unit.instance;
+
+ unit.game = this;
+
+ if (col !== undefined) {
+ // Center unit in square
+ var sqX = (col || 0) * REF_SIZE
+ , sqY = (row || 0) * REF_SIZE
+ , x = sqX + (REF_SIZE-unit.width) /2
+ , y = sqY + (REF_SIZE-unit.height)/2 ;
+
+ unit.setLocation(x,y);
+ unit.render( this.level );
+ }
+
+ this.pathmap.addBlocker(unit);
+
+ if ( unit.active && !this.byId[unit.id] ) {
+ this.byId[unit.id] = unit;
+ this.active.push(unit);
+
+ if (unit instanceof Bullet)
+ this.bullets.push(unit);
+ else
+ this.units.push(unit);
+ }
+
+ return unit;
+ },
+
+ killUnit : function killUnit(unit){
+ if (unit instanceof Y.event.YEvent)
+ unit = unit.instance;
+
+ console.log('killUnit(', unit, ')');
+
+ delete this.byId[unit.id];
+ this.active.remove(unit);
+ this.pathmap.removeBlocker(unit);
+
+ if (unit instanceof Bullet)
+ this.bullets.remove(unit);
+ else
+ this.units.remove(unit);
+
+ return unit;
+ },
+
+ moveAgentTo : function moveAgentTo(agent, x,y){
+ this.pathmap.removeBlocker(agent);
+ agent.setLocation(x,y);
+ this.pathmap.addBlocker(agent);
+ return agent;
+ },
+
+
+
+ resize : function resize(){
+ var ratio = COLUMNS / ROWS
+ , el = this.el
+ , p = el.parent()
+ , pw = p.width(), ph = p.height()
+ , pRatio = pw / ph
+ ;
+
+ if ( ratio > pRatio )
+ CELL_SIZE = Math.floor((pw-GRID_OFFSET*2) / COLUMNS);
+ else
+ CELL_SIZE = Math.floor((ph-GRID_OFFSET*2) / ROWS);
+
+ SCALE = CELL_SIZE/REF_SIZE;
+
+ var w = COLUMNS*CELL_SIZE
+ , h = ROWS*CELL_SIZE
+ , canvas = this.canvas[0];
+
+ this.el.width(w).height(h);
+ this.canvas.width(w).height(h);
+ canvas.width = w;
+ canvas.height = h;
+
+ this.el.offset({
+ top : (ph - h) / 2,
+ left : (pw - w) / 2
+ });
+
+ this.ctx.scale(SCALE,SCALE);
}
+
});
if (!window.console) {
console = {
- log : Y.op.noop,
- info : Y.op.noop,
- warn : Y.op.noop,
- error : Y.op.noop,
- dir : Y.op.noop,
- debug : Y.op.noop
+ log : Y.op.nop,
+ info : Y.op.nop,
+ warn : Y.op.nop,
+ error : Y.op.nop,
+ dir : Y.op.nop,
+ debug : Y.op.nop
};
}
function main(){
- var v = $('#viewport')
- , sq = REF_SIZE
- ;
+ var v = $('#viewport');
LBT = new tanks.Game();
ctx = LBT.level.ctx;
- LBT.level.append(
- new Wall(6*sq,1*sq, 1*sq,4*sq),
- new Wall(6*sq,7*sq, 1*sq,2*sq),
- new Wall(1*sq,7*sq, 2*sq,2*sq),
- new Wall(3*sq,6*sq, 1*sq,1*sq)
- );
-
- T = LBT.addUnit(new Tank(0), 1,2);
+ T = LBT.addUnit(new Tank(1), 1,2);
new Player(LBT, T);
setupUI();
function setupUI(){
LBT.loop.spark = new FpsSparkline(LBT.loop, '.fps-sparkline', 0,0);
- btank = new Tank(0);
+ btank = new Tank(1);
btank.act = function(){ return this; };
LBT.addUnit(btank, 0,0);
LBT.pathmap.removeBlocker(btank);
+++ /dev/null
-Collision = new Y.Class('Collision', {
-
- init : function initCollision(line, to){
-
- },
-
-});
\ No newline at end of file
-Level = new Y.Class('Level', Rect, {
+Level = Rect.subclass('Level', {
init : function init(game, w,h){
Rect.init.call(this, w,h);
- this.game = game;
+ this.game = game;
this.pathmap = new PathMap(this, 0,0, w, h, CAPACITY);
+ this.walls = [];
+
+ this.setup();
+ },
+
+ setup : function setup(){
+ Y.map([ [6,1, 1,4],
+ [6,7, 1,2],
+ [1,7, 2,2],
+ [3,6, 1,1] ],
+ "this.addWall.apply(this, Y.map(_, '*50'))",
+ this );
},
- append : function append(children){
- Y(arguments).forEach(this.pathmap.addBlocker, this.pathmap);
- return Rect.prototype.append.apply(this, arguments);
+ addWall : function addWall(x,y, w,h){
+ var wall = new Level.Wall(x,y, w,h);
+ this.walls.push(wall);
+ this.pathmap.addBlocker(wall);
+
+ return this.append( wall.render(this).shape );
},
drawShape : Y.op.nop
});
-Wall = new Y.Class('Wall', Rect, {
- fillStyle : 'rgba(255,255,255, 0.25)',
+Level.Wall = Thing.subclass('Wall', {
blocking : true,
+ active : false,
- init : function init(x,y, w,h){
- Rect.init.call(this, w,h);
-
- var x1 = x, y1 = y
- , x2 = x+w, y2 = y+h;
- this.position(x,y);
+ stats : {
+ hp : Infinity,
+ move : 0,
+ power : 0,
+ speed : 0,
+ shots : 0
},
- appendTo : function appendTo(parent){
- if (parent instanceof Level)
- parent.pathmap.addBlocker(this);
- return Rect.prototype.appendTo.call(this, parent);
+
+ init : function initWall(x,y, w,h){
+ this.width = w;
+ this.height = h;
+ Thing.init.call(this);
+ this.setLocation(x,y);
+ },
+
+ // inactive
+ createCooldowns : Y.op.nop,
+
+ // indestructable
+ dealDamage : Y.op.nop,
+
+
+ render : function render(parent){
+ if (this.shape) this.shape.remove();
+
+ this.shape =
+ new Rect(this.width, this.height)
+ .position(this.loc.x, this.loc.y)
+ .fill('rgba(255,255,255, 0.25)')
+ .appendTo( parent );
+
+ return this;
},
toString : function(){
- return this.className+'('+this.loc+', w='+this.layerWidth+', h='+this.layerHeight+')';
+ var loc = this.loc
+ , x1 = loc.x, y1 = loc.y
+ , x2 = x1+this.width, y2 = y1 + this.height;
+ return this.className+'['+x1+','+y1+', '+x2+','+y2+'](w='+this.width+', h='+this.height+')';
}
-
});
// };
this.walls = {
- top : new Wall(0,0, w,1),
- bottom : new Wall(0,h-1, w,1),
- left : new Wall(0,0, 1,h),
- right : new Wall(w-1,0, 1,h)
+ top : new Level.Wall(0,0, w,1),
+ bottom : new Level.Wall(0,h-1, w,1),
+ left : new Level.Wall(0,0, 1,h),
+ right : new Level.Wall(w-1,0, 1,h)
};
Y(this.walls).forEach(this.addBlocker, this);
},
Trajectory = new Y.Class('Trajectory', math.Line, {
- depth : 0,
+ halt : false,
elapsed : 0,
bounces : 0,
+
init : function initTrajectory(owner, x1,y1, x2,y2, tdist){
this.owner = owner;
this.game = owner.game;
// Determine how much time can pass before we risk teleporting
resetBound : function resetBound(){
- var abs = Math.abs
+ var BOUND_SIZE_RATIO = 0.75
+ , abs = Math.abs
, bb = this.owner.boundingBox;
- // this.tBound = Math.max( (bb.width - this.x1) / this.pa,
- // (bb.height - this.y1) / this.pb );
- this.tBound = Math.min( abs(bb.width / this.pa),
- abs(bb.height / this.pb) );
+ this.tBound = Math.min( abs(bb.width / this.pa) * BOUND_SIZE_RATIO,
+ abs(bb.height / this.pb) * BOUND_SIZE_RATIO );
return this;
},
step : function step(dt){
- var _dt = dt = (dt === undefined ? ELAPSED : dt);
+ this.halt = false;
- var o = this.owner, offX = o.offsetX, offY = o.offsetY, _bb
- , bb = _bb = o.boundingBox, bw = bb.width, bh = bb.height
- , loc = bb.p1
- ;
+ var _dt = dt = (dt === undefined ? ELAPSED : dt)
+ , _bb = bb = this.owner.boundingBox, bw = bb.width, bh = bb.height
+ , to, t, r;
do {
- var t, to, _to, ng
- , test, blockers, side
- ;
-
t = Math.min(this.tBound, dt);
dt -= t;
this.elapsed += t;
- _to = to = this.parametric(this.elapsed);
- test = this.pathmap.moveBlocked(o, this, to, bb);
+ to = this.stepTo(t, bb);
+ if (this.halt) break;
+
+ bb = new Loc.Rect(to.x,to.y, to.x+bw,to.y+bh);
+
+ } while (dt > 0);
+
+ return to;
+ },
+
+ stepTo : function stepTo(t, bb){
+ var ng, og = this.p2, owner = this.owner
+ , to = this.parametric(this.elapsed), _to = to
+ , test = this.pathmap.moveBlocked(owner, this, to, bb)
+ ;
+
+ // Blocked! Reflect trajectory
+ if ( test ) {
+ to = test.to;
+ this.bounces++;
+
+ var blocker = test.blockers[0];
+ owner.fire('collide', blocker);
+ blocker.fire('collide', owner);
+
+ // Potentially set by responders to collision event
+ if (this.halt) return to;
- // Blocked! Reflect trajectory
- if ( test ) {
- this.bounces++;
- to = test.to;
- side = test.side;
- blockers = test.blockers;
-
- if (!side) {
- console.error('Null reflection line!', 'to:', to, 'blockers:', blockers);
- continue;
- }
-
- if ( blockers.length > 1 ) {
- to = loc;
- ng = this.p1; // XXX: recalculate?
- console.log('corner!', this, 'to:', to, 'ng:', ng);
- } else {