Adds tank AI!
authordsc <david.schoonover@gmail.com>
Thu, 18 Nov 2010 23:49:37 +0000 (15:49 -0800)
committerdsc <david.schoonover@gmail.com>
Thu, 18 Nov 2010 23:49:37 +0000 (15:49 -0800)
18 files changed:
css/lttl.css
index.php
notes.md
src/Y/alias.js
src/Y/core.js
src/Y/type.js
src/Y/y-core.js
src/Y/y-function.js
src/Y/y-op.js
src/tanks/game/game.js
src/tanks/main.js
src/tanks/map/trajectory.js
src/tanks/thing/bullet.js
src/tanks/thing/player.js [moved from src/tanks/ui/player.js with 59% similarity]
src/tanks/thing/tank.js
src/tanks/thing/thing.js
src/tanks/util/utils.js [new file with mode: 0644]
tanks.php

index fb3d778..72ee86a 100644 (file)
@@ -25,5 +25,7 @@ ul, ol, li { list-style: none ! important; margin:0; padding:0; }
     #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; }
 
index 9499a94..27ec53b 100644 (file)
--- a/index.php
+++ b/index.php
@@ -6,12 +6,20 @@
 <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>
index 04afe34..9b04c89 100644 (file)
--- a/notes.md
+++ b/notes.md
@@ -1,8 +1,4 @@
 # Bugs
-- collision short-circuiting, collision events
-- bullet collisions should explode both
-- 5-shot limit
-
 
 
 # TODOs
index a36af68..01249a8 100644 (file)
@@ -6,7 +6,6 @@ var globals   = this
 ,   _Number   = globals.Number
 
 ,   slice    = _Array.prototype.slice
-,   isArray  = _Array.isArray
 ,   toString = _Object.prototype.toString
 ,   hasOwn   = _Object.prototype.hasOwnProperty
 ,   getProto = _Object.getPrototypeOf
index 3718818..0ef4ca1 100644 (file)
@@ -68,7 +68,7 @@ function dset(o, key, value, def){
         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) )
index 7735fdf..a4be9df 100644 (file)
@@ -1,23 +1,26 @@
 // 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.
@@ -41,7 +44,37 @@ function isPlainObject( obj ){
     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);
+    }
+}
index a7abd34..2abb48b 100644 (file)
@@ -7,7 +7,8 @@ Y.set     = dset;
 Y.attr    = dattr;
 Y.extend  = extend;
 
-Y.isA           = isA;
+Y.type          = type;
+Y.is            = is;
 Y.isString      = isString;
 Y.isNumber      = isNumber;
 Y.isFunction    = isFunction;
@@ -38,7 +39,7 @@ function Y(o){
     // 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 );
index dde3287..fe1b668 100644 (file)
@@ -293,7 +293,9 @@ Y(Y.filter);
 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);
 });
index ca5064d..ab272b8 100644 (file)
@@ -52,8 +52,8 @@ Y.op = {
     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){
@@ -68,7 +68,7 @@ Y.op = {
 
 // 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;
 }, {});
 
index 5b7acfe..09a6a6a 100644 (file)
@@ -50,6 +50,7 @@ tanks.Game = new Y.Class('Game', {
         SECONDTH = ELAPSED / 1000;
         SQUARETH = REF_SIZE * SECONDTH
         
+        this.active.invoke('updateCooldowns', NOW);
         this.active.invoke('act');
         
         this.draw();
index ae192e0..00c78e9 100644 (file)
@@ -11,14 +11,13 @@ function main(){
     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();
 }
 
 
@@ -26,14 +25,15 @@ function main(){
 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();
@@ -42,18 +42,14 @@ function setupUI(){
     $(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);
 }
@@ -89,8 +85,8 @@ function initConfig(){
     $('#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){
@@ -149,9 +145,9 @@ function updateInfo(){
     ,   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 );
index d5433ae..344fe1b 100644 (file)
@@ -36,9 +36,9 @@ Trajectory = new Y.Class('Trajectory', math.Line, {
     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;
     },
     
index 5a05fc9..4e19d45 100644 (file)
@@ -49,6 +49,7 @@ Bullet = Thing.subclass('Bullet', {
     },
     
     createCooldowns : Y.op.nop,
+    updateCooldowns : Y.op.nop,
     
     setTarget : function setTarget(x,y){
         var loc = this.loc
@@ -100,7 +101,7 @@ Bullet = Thing.subclass('Bullet', {
         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);
         
similarity index 59%
rename from src/tanks/ui/player.js
rename to src/tanks/thing/player.js
index 7bb3b03..c684716 100644 (file)
@@ -1,84 +1,72 @@
-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
         
@@ -87,7 +75,7 @@ Player = new Y.Class('Player', {
         
         ,   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);
@@ -107,17 +95,16 @@ Player = new Y.Class('Player', {
         
         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);
     },
     
     
@@ -131,10 +118,12 @@ Player = new Y.Class('Player', {
     },
     
     keydown : function keydown(evt){
+        if ( !this.game.loop.running ) return;
+        
         this.updateMeta(evt);
         
         if (evt.which === Key.fire) {
-            this.fire();
+            this.attack();
             return;
         }
         
@@ -148,14 +137,18 @@ Player = new Y.Class('Player', {
     },
     
     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;
         }
@@ -163,6 +156,8 @@ Player = new Y.Class('Player', {
     },
     
     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;
@@ -172,16 +167,36 @@ Player = new Y.Class('Player', {
     },
     
     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+''] ];
+    }
+};
+
+
+
+})();
index ea3666a..064ff18 100644 (file)
@@ -3,6 +3,11 @@ Tank = Thing.subclass('Tank', {
     projectile : Bullet,
     blocking : true,
     
+    bodyColor   : '#83BB32',
+    turretColor : '#1C625B',
+    barrelColor : '#D43B24',
+    
+    
     // Bounding box size
     width  : REF_SIZE*0.55,
     height : REF_SIZE*0.55,
@@ -11,11 +16,11 @@ Tank = Thing.subclass('Tank', {
     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
     },
     
@@ -27,20 +32,54 @@ Tank = Thing.subclass('Tank', {
         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
@@ -61,9 +100,9 @@ Tank = Thing.subclass('Tank', {
         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);
@@ -91,23 +130,56 @@ Tank = Thing.subclass('Tank', {
         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);
     }
     
 });
index c8482e0..bbc6218 100644 (file)
@@ -42,6 +42,9 @@ Thing = new Evt.Class('Thing', {
     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;
@@ -93,9 +96,10 @@ Thing = new Evt.Class('Thing', {
     },
     
     createCooldowns : function createCooldowns(){
-        this.cooldowns = Y({
+        var cs = this.cooldowns = {
             attack: new Cooldown(1000 * this.stats.speed)
-        });
+        };
+        this._cooldowns = Y(cs);
     },
     
     /**
@@ -108,6 +112,11 @@ Thing = new Evt.Class('Thing', {
     
     // *** Gameplay Methods *** //
     
+    updateCooldowns : function updateCooldowns(){
+        this._cooldowns.invoke('update', NOW);
+        return this;
+    },
+    
     /**
      * Determines what the creep should do -- move, attack, etc.
      */
@@ -115,9 +124,19 @@ Thing = new Evt.Class('Thing', {
         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;
     },
     
diff --git a/src/tanks/util/utils.js b/src/tanks/util/utils.js
new file mode 100644 (file)
index 0000000..9399beb
--- /dev/null
@@ -0,0 +1,7 @@
+Object.dump = function(o){
+    var out = [];
+    for (var k in o)
+        out.push(k+': '+o[k]);
+    return '{ '+out.join(', ')+' }';
+};
+
index 1ae6e32..ff16226 100644 (file)
--- a/tanks.php
+++ b/tanks.php
@@ -24,6 +24,7 @@ class Tanks {
         "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",
@@ -31,7 +32,6 @@ class Tanks {
         "src/tanks/map/pathmap.js",
         
         "src/tanks/ui/grid.js",
-        "src/tanks/ui/player.js",
         
         "src/tanks/game/game.js"
     );