#info .sep { opacity:0.1; background-color:#999; margin:5px 0; height:1px; }
#info .fps-sparkline { width:100%; height:1.5em; margin-top:0.5em; }
-#howto { position:fixed; top:3em; right:1em; color:#BFBFBF; }
+#notes { position:fixed; top:4em; right:1em; color:#BFBFBF; }
+ #notes ul, #notes ol, #notes li { list-style:circle ! important; }
+ #notes li { margin-left:1em; }
<link rel="stylesheet" href="css/lttl.css" type="text/css" media="screen">
</head>
<body class="lttl tanks">
- <h1>The Littlest Battletank</h1>
+<h1>The Littlest Battletank</h1>
+<ul id="notes" class="box">
+ <li>Press <code>enter</code> to start! (And to pause.)<br><br></li>
+
+ <li>Move around with <code>wasd</code> or the arrow keys.</li>
+ <li>Use the mouse to aim; left click or press <code>spacebar</code> to shoot.<br><br></li>
+
+ <li>For now, refresh the page to play again. :)</li>
+</ul>
<div id="config" class="box">
<h3>config</h3>
- <div><label for="bullets">bullets</label> <input id="bullets" name="bullets" value="10" type="text"></div>
+ <!--<div><label for="bullets">bullets</label> <input id="bullets" name="bullets" value="10" type="text"></div>-->
<div><label for="pathmap">overlay pathmap</label> <input id="pathmap" name="pathmap" value="1" type="checkbox"></div>
<div><label for="trajectories">trace trajectories</label> <input id="trajectories" name="trajectories" value="1" type="checkbox"></div>
</div>
# Bugs
-- collision short-circuiting, collision events
-- bullet collisions should explode both
-- 5-shot limit
-
# TODOs
, _Number = globals.Number
, slice = _Array.prototype.slice
-, isArray = _Array.isArray
, toString = _Object.prototype.toString
, hasOwn = _Object.prototype.hasOwnProperty
, getProto = _Object.getPrototypeOf
return set(o, key, value, def);
}
-function attr(o,key,value,def){
+function attr(o, key, value, def){
if ( !o || key === undefined ) return o;
if ( Y.isPlainObject(key) )
// Type Utilities //
// Much borrowed from jQuery
-var class2type = "Boolean Number String Function Array Date RegExp Object"
- .split(" ")
- .reduce(function(class2type, name) {
- class2type[ "[object "+name+"]" ] = name.toLowerCase();
- return class2type;
- }, {});
+var class2name =
+ "Boolean Number String Function Array Date RegExp Object"
+ .split(" ")
+ .reduce(function(class2name, name) {
+ class2name[ "[object "+name+"]" ] = name.toLowerCase();
+ return class2name;
+ }, {})
+;
function type_of(obj){
return obj == null ?
String( obj ) :
- class2type[ toString.call(obj) ] || "object";
+ class2name[ toString.call(obj) ] || "object";
}
+function isArray(obj) { return type_of(obj) === "array" || obj instanceof Y.YArray; }
function isFunction(obj) { return type_of(obj) === "function"; }
function isString(obj) { return type_of(obj) === "string"; }
function isNumber(obj) { return type_of(obj) === "number"; }
-function isWindow( obj ) { return obj && typeof obj === "object" && "setInterval" in obj; }
+function isWindow(obj) { return obj && typeof obj === "object" && "setInterval" in obj; }
function isPlainObject( obj ){
// Must be an Object.
return key === undefined || hasOwn.call( obj, key );
}
-function isA(a, b){
- return (a instanceof b) || (type_of(a) === b);
+function type( o ) {
+ switch ( typeof(o) ) {
+ case "undefined" : return undefined;
+ case "string" : return String;
+ case "number" : return Number; // Note: NaN and Infinity are Number literals
+ case "boolean" : return Boolean;
+
+ case "function" :
+ // If the function has a user-specified prototype, we can probably assume
+ // it's meant to be a class constructor (and therefore, a type)
+ if ( o.prototype && o.prototype !== Function.prototype )
+ return o;
+ else
+ return Function;
+
+ case "object" :
+ default :
+ // Null is an object, obv
+ if ( o === null )
+ return null;
+
+ return KNOWN_CLASSES[o.className] || o.__class__
+ || (o.constructor && o.constructor !== Object) ? o.constructor : Object;
+ }
}
+function is( A, B ){
+ if ( isArray(B) )
+ return B.map( Y.is(A) ).any();
+ else {
+ var AT = type(A), BT = type(B);
+ return (A instanceof BT || B instanceof AT || AT === BT);
+ }
+}
Y.attr = dattr;
Y.extend = extend;
-Y.isA = isA;
+Y.type = type;
+Y.is = is;
Y.isString = isString;
Y.isNumber = isNumber;
Y.isFunction = isFunction;
// Cast `arguments` object to a real Array, optionally slicing at specified delimiters
if ( o.prototype === undefined
&& isNumber(o.length)
- && !isArray(o)
+ && !_Array.isArray(o)
&& o.constructor === _Object )
{
r = slice.call( o, A[1]||0, A[2]||o.length );
Y(Y.set);
Y(Y.attr);
Y(Y.extend);
-Y.isA = Y(Y.isA).curry();
+Y(Y.type);
+Y.is = Y(Y.is).curry();
+
Y.reduce(YFunction.prototype, function(_,fn,name){
YFunction(fn);
});
has: function(o,k){ return k in o; },
get: function(o,k){ return o[k] },
getdef: function(o,k,def){ return (k in o ? o[k] : def); },
- set: set,
- attr: attr,
+ set: set, // set(o, key, value, def)
+ attr: attr, // attr(o, key, value, def)
method: function(name){
var args = Y(arguments,1);
return function(obj){
// Curry all operators
Y.op = Y.reduce(Y.op, function(op, fn, k){
- op[k] = Y(fn).curry();
+ op[k] = Y( Y(fn).curry() );
return op;
}, {});
SECONDTH = ELAPSED / 1000;
SQUARETH = REF_SIZE * SECONDTH
+ this.active.invoke('updateCooldowns', NOW);
this.active.invoke('act');
this.draw();
LBT = new tanks.Game();
ctx = LBT.level.ctx;
- T = LBT.addUnit(new Tank(1), 1,2);
- new Player(LBT, T);
+ P = LBT.addUnit(new PlayerTank(1), 1,2);
+ E = LBT.addUnit(new Tank(2), 5,6);
setupUI();
- // barrel = T.barrel;
- // B = bullets.attr(0);
- // R = B.trajectory;
+ // toggleGame();
+ updateInfo();
}
function setupUI(){
LBT.loop.spark = new FpsSparkline(LBT.loop, '.fps-sparkline', 0,0);
- btank = new Tank(1);
- btank.act = function(){ return this; };
- btank.stats.shots = Infinity;
- LBT.addUnit(btank, 0,0);
- LBT.pathmap.removeBlocker(btank);
- btank.shape.hide();
+ // btank = new Tank(1);
+ // btank.act = function(){ return this; };
+ // btank.stats.shots = Infinity;
+ // LBT.addUnit(btank, 0,0);
+ // LBT.pathmap.removeBlocker(btank);
+ // btank.shape.hide();
initConfig();
+ $('#config').bind('mousedown', Y.op.K(false));
$('#config input').bind('change', updateConfig);
LBT.root.draw();
$(document).bind('keydown', 'return', toggleGame);
$(document).bind('keydown', 'ctrl+o', toggleOverlay);
- $('#bullets').bind('blur', function(evt){
- var n = parseInt($('#bullets').val() || 0);
- updateBullets(n);
- });
+ // $('#bullets').bind('blur', function(evt){
+ // var n = parseInt($('#bullets').val() || 0);
+ // updateBullets(n);
+ // });
LBT.root.draw();
setInterval(updateInfo, 1000);
- // Start the simulation!
- toggleGame();
- updateInfo();
-
// Fix grid-size on resize
// $(window).bind('resize', resizeGame);
}
$('#config [name=pathmap]').attr('checked', p.overlayPathmap);
$('#config [name=trajectories]').attr('checked', p.traceTrajectories);
- $('#config [name=bullets]').val(c.debug.projectiles);
- updateBullets( c.debug.projectiles );
+ // $('#config [name=bullets]').val(c.debug.projectiles);
+ // updateBullets( c.debug.projectiles );
}
function updateConfig(evt){
, n_projs = LBT.bullets.size()
;
+ $('#info #state').text( loop.running ? 'Running!' : ('Paused (tick '+TICKS+')') );
$('#info [name=fps]').val( fps.toFixed(2) + " / " + loop.framerate );
$('#info [name=frame]').val( loop.frametime().toFixed(3)+" ms" );
- $('#info #state').text( loop.running ? 'Running!' : ('Paused (tick '+TICKS+')') );
$('#info [name=active]').val( n_active );
$('#info [name=units]').val( n_units );
resetBound : function resetBound(){
var BOUND_SIZE_RATIO = 0.75
, abs = Math.abs
- , bb = this.owner.boundingBox;
- this.tBound = Math.min( abs(bb.width / this.pa) * BOUND_SIZE_RATIO,
- abs(bb.height / this.pb) * BOUND_SIZE_RATIO );
+ , w = this.owner.width, h = this.owner.height;
+ this.tBound = Math.min( abs(w / this.pa) * BOUND_SIZE_RATIO,
+ abs(h / this.pb) * BOUND_SIZE_RATIO );
return this;
},
},
createCooldowns : Y.op.nop,
+ updateCooldowns : Y.op.nop,
setTarget : function setTarget(x,y){
var loc = this.loc
var loc = this.loc;
this.shape = new Circle(3)
.position(loc.x, loc.y)
- .fill('#EC5B38')
+ .fill('#FFF6AE')
.appendTo( parent );
this.shape.layer.attr('title', ''+loc);
-Key = {
- fire : 32,
-
- _dirFromKey : {
- 37: "left", 38: "up", 39: "right", 40: "down",
- 65: "left", 87: "up", 68: "right", 83: "down"
- },
-
- _inverse : {
- left : "right", right : "left",
- up : "down", down : "up"
- },
-
- getDir : function getDir(key){
- return Key._dirFromKey[key+''];
- },
-
- getInverseDir : function getInverseDir(key){
- return Key._inverse[ Key._dirFromKey[key+''] ];
- }
-};
-
+(function(){
-
-Player = new Y.Class('Player', {
- activeKeys : null,
- shift : false, ctrl : false, meta : false, alt : false,
- leftMouse : false, middleMouse : false, rightMouse : false,
+PlayerTank = Tank.subclass('PlayerTank', {
+ bodyColor : '#E73075',
+ turretColor : '#A72F5B',
+ barrelColor : '#2E62C9',
- queue : null,
+ // Attributes
+ stats: {
+ hp : 1, // health
+
+ move : 1.0, // move speed (squares/sec)
+ rotate : HALF_PI, // rotation speed (radians/sec)
+
+ power : 1, // attack power
+ speed : 0.5, // attack cool (sec)
+ shots : 5 // max projectiles in the air at once
+ },
- init : function init(game, tank){
- Y.bindAll(this);
+ init : function init(align){
+ Tank.init.call(this, align);
this.activeKeys = new Y.YArray();
this.queue = [];
- this.tank = tank;
- this.game = game;
- this.pathmap = game.pathmap;
- tank.act = this.act; // Override tank actions with player control
- tank.move = this.move;
+ // tank.act = this.act; // Override tank actions with player control
+ // tank.move = this.move;
$(window)
- .bind('keydown', this.keydown)
- .bind('keyup', this.keyup)
- .bind('mousedown', this.mousedown)
- .bind('mouseup', this.mouseup)
- .bind('mousemove', this.mousemove)
+ .bind('keydown', this.keydown.bind(this))
+ .bind('keyup', this.keyup.bind(this))
+ .bind('mousedown', this.mousedown.bind(this))
+ .bind('mouseup', this.mouseup.bind(this))
+ .bind('mousemove', this.mousemove.bind(this))
;
},
+ activeKeys : null,
+ shift : false, ctrl : false, meta : false, alt : false,
+ leftMouse : false, middleMouse : false, rightMouse : false,
+
+ queue : null,
+
+
act : function act(){
- if (this.tank.dead)
- return this.tank;
-
- this.tank.cooldowns.invoke('update', NOW);
+ if (this.dead)
+ return this;
var action = this.queue.shift();
- if (action && action.type === 'fire') {
- this.tank.shoot(action.x, action.y);
- } else if ( this.activeKeys.size() )
+ if (action && action.type === 'fire')
+ this.shoot(action.x, action.y);
+
+ else if ( this.activeKeys.size() )
this.move();
- return this.tank;
+ return this;
},
- fire : function fire(){
- var WIGGLE = 4
- , tank = this.tank
- , cooldown = tank.cooldowns.attr('attack')
- ;
+ attack : function attack(){
+ var WIGGLE = 4;
- if ( !cooldown.activate(NOW) )
+ if ( !this.cooldowns.attack.ready )
return;
- var barrel = tank.barrel
- , bb = tank.boundingBox
+ var barrel = this.barrel
+ , bb = this.boundingBox
, w2 = bb.width/2, h2 = bb.height/2
, x0 = bb.x1+w2, y0 = bb.y1+h2
, x1 = x0 + w2*cos, y1 = y0 + h2*sin
, sz = (barrel.boundingBox.width - w2)/2 + WIGGLE
- , blockers = this.pathmap.get(x1-sz,y1-sz, x1+sz,y1+sz).remove(tank)
+ , blockers = this.game.pathmap.get(x1-sz,y1-sz, x1+sz,y1+sz).remove(this)
;
if ( blockers.size() ) return; // console.log('squelch!', blockers);
if (!dir) return;
- var tank = this.tank
- , toLoc = tank.loc.moveByDir(dir, (tank.stats.move * SQUARETH))
+ var toLoc = this.loc.moveByDir(dir, (this.stats.move * SQUARETH))
, x = toLoc.x, y = toLoc.y
- , bb = tank.boundingBox.add(x,y)
+ , bb = this.boundingBox.add(x,y)
- , blockers = this.pathmap.get(bb.x1,bb.y1, bb.x2,bb.y2).remove(tank)
+ , blockers = this.game.pathmap.get(bb.x1,bb.y1, bb.x2,bb.y2).remove(this)
;
if ( !blockers.size() )
- this.game.moveAgentTo(tank, x,y);
+ this.game.moveAgentTo(this, x,y);
},
},
keydown : function keydown(evt){
+ if ( !this.game.loop.running ) return;
+
this.updateMeta(evt);
if (evt.which === Key.fire) {
- this.fire();
+ this.attack();
return;
}
},
keyup : function keyup(evt){
+ // if ( !this.game.loop.running ) return;
+
var dir = Key.getDir(evt.which);
if (dir) this.activeKeys.remove(dir);
this.updateMeta(evt);
},
mousedown : function mousedown(evt){
+ if ( !this.game.loop.running ) return;
+
switch (evt.which) {
- case 1: evt.leftMouse = true; this.fire(); break;
+ case 1: evt.leftMouse = true; this.attack(); break;
case 2: evt.rightMouse = true; break;
case 3: evt.middleMouse = true; break;
}
},
mouseup : function mouseup(evt){
+ // if ( !this.game.loop.running ) return;
+
switch (evt.which) {
case 1: evt.leftMouse = false; break;
case 2: evt.rightMouse = false; break;
},
mousemove : function mousemove(evt){
- var shape = this.tank.shape
- , cannon = this.tank.cannon
- , barrel = this.tank.barrel
- , off = shape.offset()
- , w = shape.width(), h = shape.height()
- , x = off.left + w/2 - evt.pageX
- , y = off.top + h/2 - evt.pageY
- , theta = Math.atan2(-y,-x);
-
- barrel.rotate(theta);
+ if ( !this.game.loop.running ) return;
+
+ this.rotateBarrelRelPage(evt.pageX, evt.pageY);
}
-});
\ No newline at end of file
+});
+
+
+var Key = {
+ fire : 32,
+
+ _dirFromKey : {
+ 37: "left", 38: "up", 39: "right", 40: "down",
+ 65: "left", 87: "up", 68: "right", 83: "down"
+ },
+
+ _inverse : {
+ left : "right", right : "left",
+ up : "down", down : "up"
+ },
+
+ getDir : function getDir(key){
+ return Key._dirFromKey[key+''];
+ },
+
+ getInverseDir : function getInverseDir(key){
+ return Key._inverse[ Key._dirFromKey[key+''] ];
+ }
+};
+
+
+
+})();
projectile : Bullet,
blocking : true,
+ bodyColor : '#83BB32',
+ turretColor : '#1C625B',
+ barrelColor : '#D43B24',
+
+
// Bounding box size
width : REF_SIZE*0.55,
height : REF_SIZE*0.55,
stats: {
hp : 1, // health
- move : 1.0, // move speed (squares/sec)
+ move : 0.75, // move speed (squares/sec)
rotate : HALF_PI, // rotation speed (radians/sec)
power : 1, // attack power
- speed : 0.5, // attack cool (sec)
+ speed : 0.75, // attack cool (sec)
shots : 5 // max projectiles in the air at once
},
this.onBulletDeath = this.onBulletDeath.bind(this);
},
- attack : function attack(unit){
- var atk_cool = this.cooldowns.attr('attack');
- if ( atk_cool.activate(NOW) )
- this.doAttack(unit);
- return this;
+ cannonReady : function cannonReady(){
+ return this.nShots < this.stats.shots && this.cooldowns.attack.ready;
},
- doAttack : function doAttack(x,y){
- this.shoot(x,y);
- return this;
+ act : function act(){
+ // Are we ready to fire?
+ if ( this.cannonReady() ) {
+ // Try to blow up nearby tanks
+ var t = this.nearLike(66, 'Y.is(Tank, _) && _.align !== '+this.align).shift();
+ if (t) {
+ // console.log('I gotcha!', t);
+ this.shoot(t.loc.x, t.loc.y);
+ return this;
+ }
+
+ // Try to shoot down nearby bullets
+ var b = this.nearLike(15, Y.is(Bullet)).shift();
+ if (b) {
+ // console.log('Incoming! Shoot it down!', b);
+ this.shoot(b.loc.x, b.loc.y);
+ return this;
+ }
+
+ }
+
+ // Nothing to shoot at? Move toward something
+ var t = this.nearLike(10000, 'Y.is(Tank, _) && _.align !== '+this.align).shift();
+ if (t) {
+ this.move(t.loc.x, t.loc.y);
+ return this;
+ }
+ },
+
+ nearLike : function nearLike(ticks, fn){
+ fn = fn.toFunction();
+ var bMovePerTick = MS_PER_FRAME * Bullet.prototype.stats.move*REF_SIZE/1000
+ , within = bMovePerTick*ticks
+ , bb = this.boundingBox
+ , x1 = bb.x1 - within, y1 = bb.y1 - within
+ , x2 = bb.x2 + within, y2 = bb.y2 + within
+ ;
+ return this.game.pathmap.get(x1,y1, x2,y2).filter(fn || Y.op.K(true));
},
shoot : function shoot(x,y){
- if (this.nShots >= this.stats.shots)
+ this.rotateBarrel(x,y);
+
+ if ( this.nShots >= this.stats.shots || !this.cooldowns.attack.activate(NOW) )
return null;
var ProjectileType = this.projectile
var loc = this.loc
, barrel = this.barrel, bb = barrel.boundingBox
, theta = barrel.transform.rotate, sin = Math.sin(theta), cos = Math.cos(theta)
- , x0 = bb.x2-bb.x1, y0 = (bb.y2-bb.y1)/2
- , x = loc.x + bb.x1 + x0*cos - y0*sin + 3
- , y = loc.y + bb.y1 + x0*sin + y0*cos + 3
+ , x0 = 3+bb.x2-bb.x1, y0 = (bb.y2-bb.y1)/2
+ , x = loc.x + bb.x1 + x0*cos - y0*sin
+ , y = loc.y + bb.y1 + x0*sin + y0*cos
;
// console.log('getTurretLoc()', 'loc:', loc, 'bb.(x2,y2):', [bb.x2,bb.y2], '(x,y):', [x,y]);
return new math.Vec(x,y);
this.shape =
new Rect(w,h)
.position(this.loc.x, this.loc.y)
- .fill('#E73075')
+ .fill(this.bodyColor)
.appendTo( parent );
- this.cannon = new Circle(r, true)
+ this.turret = new Circle(r, true)
.position(w2-r, h2-r)
- .fill('#A72F5B')
+ .fill(this.turretColor)
.appendTo( this.shape )
;
this.barrel = new Rect(cw,ch)
.position(w2-2, h2-ch/2)
.origin(2, ch/2)
- .fill('#2E62C9')
+ .fill(this.barrelColor)
.appendTo( this.shape )
;
return this;
+ },
+
+ colors : function colors(bodyColor, turretColor, barrelColor){
+ var names = Y('bodyColor', 'turretColor', 'barrelColor');
+
+ if (arguments.length === 0)
+ return names.generate( Y.op.get(this) );
+
+ if (bodyColor) this.bodyColor = bodyColor;
+ if (turretColor) this.turretColor = turretColor;
+ if (barrelColor) this.barrelColor = barrelColor;
+ return this;
+ },
+
+ rotateBarrel : function rotateBarrel(x,y){
+ var bb = this.boundingBox
+ , w = this.width, h = this.height
+ , x0 = x - bb.x1 - w/2
+ , y0 = y - bb.y1 - h/2
+ , theta = Math.atan2(y0,x0)
+ ;
+ this.barrel.rotate(theta);
+ },
+
+ rotateBarrelRelPage : function rotateBarrelRelPage(pageX, pageY){
+ var shape = this.shape
+ , off = shape.offset()
+ , w = this.width, h = this.height
+ , x = off.left + w/2 - pageX
+ , y = off.top + h/2 - pageY
+ , theta = Math.atan2(-y,-x)
+ ;
+ this.barrel.rotate(theta);
}
});
width : REF_SIZE*0.7,
height : REF_SIZE*0.6,
+ set : Y.op.set.methodize(),
+ attr : Y.op.attr.methodize(),
+
dealDamage : function dealDamage(d, source){
this.stats.hp -= d;
},
createCooldowns : function createCooldowns(){
- this.cooldowns = Y({
+ var cs = this.cooldowns = {
attack: new Cooldown(1000 * this.stats.speed)
- });
+ };
+ this._cooldowns = Y(cs);
},
/**
// *** Gameplay Methods *** //
+ updateCooldowns : function updateCooldowns(){
+ this._cooldowns.invoke('update', NOW);
+ return this;
+ },
+
/**
* Determines what the creep should do -- move, attack, etc.
*/
return this;
},
- move : function move(){
- var to = this.loc.moveByAngle(this.rotation, this.stats.move * SQUARETH);
- this.game.moveAgentTo(this, to.x, to.y);
+ move : function move(x,y){
+ return this.moveByAngle(Math.atan2(-y,-x));
+ },
+
+ moveByAngle : function moveByAngle(theta){
+ var to = this.loc.moveByAngle(theta, this.stats.move * SQUARETH)
+ , bb = this.boundingBox.add(to.x,to.y)
+ , blockers = this.game.pathmap.get(bb.x1,bb.y1, bb.x2,bb.y2).remove(this)
+ ;
+
+ if ( !blockers.size() )
+ this.game.moveAgentTo(this, to.x, to.y);
+
return this;
},
--- /dev/null
+Object.dump = function(o){
+ var out = [];
+ for (var k in o)
+ out.push(k+': '+o[k]);
+ return '{ '+out.join(', ')+' }';
+};
+
"src/tanks/thing/thing.js",
"src/tanks/thing/bullet.js",
"src/tanks/thing/tank.js",
+ "src/tanks/thing/player.js",
"src/tanks/map/loc.js",
"src/tanks/map/trajectory.js",
"src/tanks/map/pathmap.js",
"src/tanks/ui/grid.js",
- "src/tanks/ui/player.js",
"src/tanks/game/game.js"
);