Adds A* pathing for AI tanks.
authordsc <david.schoonover@gmail.com>
Fri, 19 Nov 2010 11:45:51 +0000 (03:45 -0800)
committerdsc <david.schoonover@gmail.com>
Fri, 19 Nov 2010 11:45:51 +0000 (03:45 -0800)
14 files changed:
index.php
src/portal/math/vec.js
src/portal/util/astar.js [new file with mode: 0644]
src/portal/util/binaryheap.js [new file with mode: 0644]
src/portal/util/graph.js [new file with mode: 0644]
src/portal/util/tree/quadtree.js
src/tanks/config.js
src/tanks/game/game.js
src/tanks/main.js
src/tanks/map/pathmap.js
src/tanks/thing/player.js
src/tanks/thing/tank.js
src/tanks/thing/thing.js
tanks.php

index 27ec53b..54d1905 100644 (file)
--- a/index.php
+++ b/index.php
@@ -21,6 +21,7 @@
     <h3>config</h3>
     <!--<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="aipaths">overlay ai paths</label> <input id="aipaths" name="aipaths" value="1" type="checkbox"></div>
     <div><label for="trajectories">trace trajectories</label> <input id="trajectories" name="trajectories" value="1" type="checkbox"></div>
 </div>
 
index 7debadf..4154495 100644 (file)
@@ -26,12 +26,18 @@ math.Vec = new Y.Class('Vec', [], {
         return this;
     },
     
-    add : function add(b){
-        return this.setXY(this.x+b.x, this.y+b.y);
+    add : function add(x,y){
+        if ( x instanceof Array ) {
+            y = x[1]; x = x[0];
+        }
+        return this.setXY(this.x+x, this.y+y);
     },
     
-    subtract : function subtract(b){
-        return this.setXY(this.x-b.x, this.y-b.y);
+    subtract : function subtract(x,y){
+        if ( x instanceof Array ) {
+            y = x[1]; x = x[0];
+        }
+        return this.setXY(this.x-x, this.y-y);
     },
     
     scale : function scale(s){
diff --git a/src/portal/util/astar.js b/src/portal/util/astar.js
new file mode 100644 (file)
index 0000000..78f89ed
--- /dev/null
@@ -0,0 +1,140 @@
+/*  astar.js http://github.com/bgrins/javascript-astar
+    MIT License
+    
+    Implements the astar search algorithm in javascript using a binary heap
+    **Requires graph.js**
+
+    Example Usage:
+        var graph = new Graph([
+            [0,0,0,0],
+            [1,0,0,1],
+            [1,1,0,0]
+        ]);
+        var start = graph.nodes[0][0];
+        var end = graph.nodes[1][2];
+        astar.search(graph.nodes, start, end);
+*/
+
+var astar = {
+    
+    init: function initAStar(grid) {
+        // var copy = [];
+        for(var x = 0, xl = grid.length; x < xl; x++) {
+            var yl = grid[x].length;
+            // copy[x] = new Array(yl);
+            for(var y = 0; y < yl; y++) {
+                // var node = grid[x][y].clone();
+                var node = grid[x][y];
+                node.f = 0;
+                node.g = 0;
+                node.h = 0;
+                node.visited = false;
+                node.closed = false;
+                node.parent = null;
+                // copy[x][y] = node;
+            }
+        }
+        // return copy;
+    },
+    
+    search: function search(grid, start, end, heuristic, ignore) {
+        astar.init(grid);
+        heuristic = heuristic || astar.manhattan;
+        
+        ignore = ignore || [];
+        ignore.push(start.value);
+        ignore.push(end.value);
+        
+        var open = new BinaryHeap('f');
+        open.push(start);
+        
+        while( open.length > 0 ) {
+            
+            // Grab the lowest f(x) to process next.  Heap keeps this sorted for us.
+            var current = open.pop();
+            
+            // End case -- result has been found, return the traced path
+            if(current === end) {
+                var path = []
+                ,   node = current;
+                while( node.parent ) {
+                    path.push( node.clone() );
+                    node = node.parent;
+                }
+                return path.reverse();
+            }
+            
+            // Normal case -- move current from open to closed, process each of its neighbors
+            current.closed = true;
+            
+            var neighbors = astar.neighbors(grid, current);
+            for(var i=0, il = neighbors.length; i < il; i++) {
+                var neighbor = neighbors[i];
+                
+                // blocker
+                if( neighbor.closed || (neighbor.blocked && ignore.indexOf(neighbor.value) === -1) )
+                    continue;
+                
+                // g score is the shortest distance from start to current node, we need to check if
+                //   the path we have arrived at this neighbor is the shortest one we have seen yet.
+                // We define the distance from a node to it's neighbor as 1, but this could be variable for weighted paths.
+                var g = current.g + 1;
+                
+                if( !neighbor.visited || g < neighbor.g ) {
+                    
+                    // Found an optimal (so far) path to this node.  Take score for node to see how good it is.
+                    neighbor.parent = current;
+                    neighbor.h = neighbor.h || heuristic(neighbor, end);
+                    neighbor.g = g;
+                    neighbor.f = g + neighbor.h;
+                    
+                    // New? Add to the set of nodes to process
+                    if ( !neighbor.visited ) {
+                        neighbor.visited = true;
+                        open.push(neighbor);
+                    
+                    // Seen, but since it has been rescored we need to reorder it in the heap
+                    } else
+                        open.rescore(neighbor);
+                }
+            }
+        }
+        
+        // No result was found -- empty array signifies failure to find path
+        return [];
+    },
+    
+    manhattan: function manhattan(p1, p2) {
+        // See list of heuristics: http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html
+        var d1 = Math.abs(p2.x - p1.x)
+        ,   d2 = Math.abs(p2.y - p1.y) ;
+        return d1 + d2;
+    },
+    
+    manhattan4: function manhattan(x1,y1, x2,y2) {
+        // See list of heuristics: http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html
+        var d1 = Math.abs(x2 - x1)
+        ,   d2 = Math.abs(y2 - y1) ;
+        return d1 + d2;
+    },
+    
+    neighbors: function neighbors(grid, node) {
+        var ret = [];
+        var x = node.x;
+        var y = node.y;
+        
+        if(grid[x-1] && grid[x-1][y]) {
+            ret.push(grid[x-1][y]);
+        }
+        if(grid[x+1] && grid[x+1][y]) {
+            ret.push(grid[x+1][y]);
+        }
+        if(grid[x] && grid[x][y-1]) {
+            ret.push(grid[x][y-1]);
+        }
+        if(grid[x] && grid[x][y+1]) {
+            ret.push(grid[x][y+1]);
+        }
+        return ret;
+    }
+};
diff --git a/src/portal/util/binaryheap.js b/src/portal/util/binaryheap.js
new file mode 100644 (file)
index 0000000..cfecb3a
--- /dev/null
@@ -0,0 +1,146 @@
+(function(){
+
+function BinaryHeap(score){
+    if (typeof score === 'string')
+        this.key = score;
+    else
+        this.score = score;
+}
+
+var AP = Array.prototype
+,   proto = BinaryHeap.prototype = []
+,   methods = {
+    
+    _push : AP.push,
+    _pop: AP.pop,
+    
+    // Replaced by score function if supplied
+    score : function score(el){
+        return el[this.key];
+    },
+    
+    push: function(el) {
+        // Add the new element to the end of the array.
+        this._push(el);
+        
+        // Allow it to sink down.
+        this.heapDown(this.length-1);
+        
+        return this;
+    },
+    
+    pop: function() {
+        // Store the first element so we can return it later.
+        var result = this[0]
+        
+        // Get the element at the end of the array.
+        ,   end = this._pop();
+        
+        // If there are any elements left, put the end element at the
+        // start, and let it bubble up.
+        if ( this.length > 0 ) {
+            this[0] = end;
+            this.heapUp(0);
+        }
+        return result;
+    },
+    
+    remove: function(node) {
+        var key = this.key
+        ,   i = this.indexOf(node)
+        
+        // When it is found, the process seen in 'pop' is repeated
+        // to fill up the hole.
+        ,   end = this._pop();
+        
+        if ( i !== this.length-1 ) {
+            this[i] = end;
+            var endScore  = (key ? end[key] : this.score(end))
+            ,   nodeScore = (key ? node[key] : this.score(node)) ;
+            if ( endScore < nodeScore )
+                this.heapDown(i);
+            else
+                this.heapUp(i);
+        }
+        return this;
+    },
+    
+    rescore: function(node) {
+        this.heapDown(this.indexOf(node));
+        return this;
+    },
+    
+    heapDown: function(n) {
+        var key = this.key
+        ,   el = this[n]
+        ,   elScore = (key ? el[key] : this.score(el))
+        ;
+        
+        // When at 0, an element can not sink any further.
+        while ( n > 0 ) {
+            
+            // Compute the parent element's index and score
+            var parentN = ((n + 1) >> 1) - 1
+            ,   parent = this[parentN]
+            ,   parentScore = (key ? parent[key] : this.score(parent))
+            ;
+            
+            // Swap the elements if the parent is greater
+            if ( elScore < parentScore ) {
+                this[parentN] = el;
+                this[n] = parent;
+                
+                // Update 'n' to continue at the new position.
+                n = parentN;
+            
+            // Found a parent that is less, no need to sink any further.
+            } else break;
+        }
+    },
+    
+    heapUp: function(n) {
+        var key     = this.key
+        ,   len     = this.length
+        ,   el      = this[n]
+        ,   elScore = this.score(el)
+        ;
+        
+        while( true ) {
+            var child2N = (n+1) << 1    // Compute children
+            ,   child1N = child2N - 1
+            ,   swap    = null          // Swap index
+            ;
+            
+            if ( child1N < len ) {
+                var child1 = this[child1N]
+                ,   child1Score = (key ? child1[key] : this.score(child1))
+                ;
+                if (child1Score < elScore)
+                    swap = child1N;
+            }
+            
+            if ( child2N < len ) {
+                var child2 = this[child2N]
+                ,   child2Score = (key ? child2[key] : this.score(child2))
+                ;
+                if (child2Score < (swap === null ? elScore : child1Score))
+                    swap = child2N;
+            }
+            
+            // Perform swap if necessary
+            if ( swap !== null ) {
+                this[n] = this[swap];
+                this[swap] = el;
+                n = swap;
+                
+            } else break;
+        }
+    }
+};
+
+// Add methods to prototype
+for (var k in methods) proto[k] = methods[k];
+
+this.BinaryHeap = BinaryHeap;
+
+})();
\ No newline at end of file
diff --git a/src/portal/util/graph.js b/src/portal/util/graph.js
new file mode 100644 (file)
index 0000000..e85dc73
--- /dev/null
@@ -0,0 +1,16 @@
+function Graph(grid) {
+    this.grid = grid;
+    this.nodes = [];
+    
+    for (var x = 0; x < grid.length; x++) {
+        var row = grid[x];
+        this.nodes[x] = [];
+        for (var y = 0; y < row.length; y++) {
+            var n = new math.Vec(x,y);
+            n.blocked = !!row[y];
+            this.nodes[x].push(n);
+        }
+    }
+}
+
+
index d1ac1c8..b7a5afb 100644 (file)
@@ -34,8 +34,8 @@ Region = new Y.Class('Region', {
     
     // Expects caller will have ordered x1 < x2, y1 < y2
     overlaps : function overlaps(x1,y1, x2,y2){
-        return !( x1 > this.x2 || y1 > this.y2
-               || x2 < this.x1 || y2 < this.y1 );
+        return !( x1 >  this.x2 || y1 >  this.y2
+               || x2 <= this.x1 || y2 <= this.y1 );
     },
     
     toString : function toString(){
@@ -64,8 +64,8 @@ QuadTree = new Y.Class('QuadTree', {
     
     // Expects caller will have ordered x1 < x2, y1 < y2
     overlaps : function overlaps(x1,y1, x2,y2){
-        return !( x1 > this.x2 || y1 > this.y2
-               || x2 < this.x1 || y2 < this.y1 );
+        return !( x1 >  this.x2 || y1 >  this.y2
+               || x2 <= this.x1 || y2 <= this.y1 );
     },
     
     
index 0588ebb..46503eb 100644 (file)
@@ -2,6 +2,7 @@
 tanks.config = {
     pathing : { 
         overlayPathmap    : false,
+        overlayAIPaths    : true,
         traceTrajectories : false
     },
     debug : {
index 09a6a6a..ba148a9 100644 (file)
@@ -33,6 +33,7 @@ tanks.Game = new Y.Class('Game', {
     
     draw : function draw(){
         this.root.draw();
+        
         this.pathmap.removeOverlay(this.viewport);
         if (tanks.config.pathing.overlayPathmap)
             this.pathmap.overlay(this.viewport);
@@ -50,6 +51,9 @@ tanks.Game = new Y.Class('Game', {
         SECONDTH = ELAPSED / 1000;
         SQUARETH = REF_SIZE * SECONDTH
         
+        if (!tanks.config.pathing.overlayAIPaths)
+            this.pathmap.clearPath();
+        
         this.active.invoke('updateCooldowns', NOW);
         this.active.invoke('act');
         
index 00c78e9..233dc98 100644 (file)
@@ -11,13 +11,15 @@ function main(){
     LBT = new tanks.Game();
     ctx = LBT.level.ctx;
     
-    P = LBT.addUnit(new PlayerTank(1), 1,2);
-    E = LBT.addUnit(new Tank(2), 5,6);
+    P = LBT.addUnit(new PlayerTank(1), 8,9);
+    E = LBT.addUnit(new Tank(2), 1,2);
     
     setupUI();
     
     // toggleGame();
     updateInfo();
+    
+    pm = LBT.pathmap;
 }
 
 
@@ -83,6 +85,7 @@ function initConfig(){
     var c = tanks.config, p = c.pathing;
     
     $('#config [name=pathmap]').attr('checked', p.overlayPathmap);
+    $('#config [name=aipaths]').attr('checked', p.overlayAIPaths);
     $('#config [name=trajectories]').attr('checked', p.traceTrajectories);
     
     // $('#config [name=bullets]').val(c.debug.projectiles);
@@ -91,7 +94,8 @@ function initConfig(){
 
 function updateConfig(evt){
     var p = tanks.config.pathing;
-    p.overlayPathmap = $('#config [name=pathmap]').attr('checked');
+    p.overlayPathmap    = $('#config [name=pathmap]').attr('checked');
+    p.overlayAIPaths    = $('#config [name=aipaths]').attr('checked');
     p.traceTrajectories = $('#config [name=trajectories]').attr('checked');
 }
 
index bce6aca..e113de3 100644 (file)
@@ -1,25 +1,29 @@
 //  -*- mode: JavaScript; tab-width: 4; indent-tabs-mode: nil; -*-
+(function(){
+
 PathMap = new Y.Class('PathMap', QuadTree, {
+    gridSquareSize  : REF_SIZE,
+    gridSquareMidPt : new math.Vec(REF_SIZE/2, REF_SIZE/2),
+    
+    
     
-    init : function init(game, x1,y1, x2,y2, capacity) {
+    init : function init(level, x1,y1, x2,y2, capacity) {
+        x1 -= 1; y1 -= 1; x2 += 1; y2 += 1;
         QuadTree.init.call(this, x1,y1, x2,y2, capacity);
-        this.game = game;
+        this.level = level;
+        this.game = level.game;
         
         var w = this.width, h = this.height;
-        // this.walls = {
-        //     top    : new math.Line(0,0, w,0),
-        //     bottom : new math.Line(0,h, w,h),
-        //     left   : new math.Line(0,0, 0,h),
-        //     right  : new math.Line(w,0, w,h)
-        // };
-        
         this.walls = {
-            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)
+            top    : new Level.Wall(x1,y1,   w,1),
+            bottom : new Level.Wall(x1,y2-1, w,1),
+            left   : new Level.Wall(x1,y1,   1,h),
+            right  : new Level.Wall(x2-1,y1, 1,h)
         };
-        Y(this.walls).forEach(this.addBlocker, this);
+        Y(this.walls).forEach(function(wall){
+            wall.isBoundary = true;
+            this.addBlocker(wall);
+        }, this);
     },
     
     addBlocker : function addBlocker(obj){
@@ -38,6 +42,8 @@ PathMap = new Y.Class('PathMap', QuadTree, {
         return obj;
     },
     
+    
+    
     moveBlocked : function moveBlocked(agent, trj, to, bb){
         bb = bb || agent.boundingBox;
         var blockers, blocker, msg
@@ -96,10 +102,193 @@ PathMap = new Y.Class('PathMap', QuadTree, {
     
     
     
+    grid : function grid(){
+        if ( !this._grid ) {
+            var size = this.gridSquareSize
+            ,   floor = Math.floor, ceil = Math.ceil
+            ,   cols = ceil((this.width-2) /size)
+            ,   rows = ceil((this.height-2)/size)
+            ,   grid = new Array(cols);
+            ;
+            
+            for (var x=0; x<cols; x++) {
+                var col = grid[x] = new Array(rows);
+                for (var y=0; y<rows; y++) {
+                    var node = col[y] = new math.Vec(x,y);
+                    
+                    node.clone = function cloneNode(){
+                        var n = math.Vec.prototype.clone.call(this);
+                        n.blocked = this.blocked;
+                        n.value   = this.value;
+                        return n;
+                    };
+                }
+            }
+            
+            this._grid = this.reduce(function(grid, v, r){
+                
+                // Ignore bullets and boundary-walls in pathing
+                if (v.isBoundary || v instanceof Bullet)
+                    return grid;
+                
+                var rowMin = floor( r.y1/size )
+                ,   colMin = floor( r.x1/size )
+                ,   rowMax = ceil(  r.y2/size )
+                ,   colMax = ceil(  r.x2/size )
+                ;
+                
+                for (var x=colMin; x<colMax; x++) {
+                    var col = grid[x];
+                    for (var y=rowMin; y<rowMax; y++) {
+                        var node = col[y];
+                        node.blocked = true;
+                        node.value = v;
+                    }
+                }
+                
+                return grid;
+            }, grid);
+        }
+        return this._grid;
+    },
+    
+    vec2Square : function vec2Square(x,y){
+        if (x instanceof Array){
+            y = x.y;
+            x = x.x;
+        }
+        var floor = Math.floor, size = this.gridSquareSize;
+        return new math.Vec(floor(x/size), floor(y/size));
+    },
+    
+    square2Vec : function square2Vec(x,y){
+        if (x instanceof Array){
+            y = x.y;
+            x = x.x;
+        }
+        var floor = Math.floor, size = this.gridSquareSize;
+        return new math.Vec(floor(x)*size, floor(y)*size);
+    },
+    
+    manhattan: function manhattan(p1, p2) {
+        var d1 = Math.abs(p2.x - p1.x)
+        ,   d2 = Math.abs(p2.y - p1.y) ;
+        return d1 + d2;
+    },
+    
+    manhattan4: function manhattan4(x1,y1, x2,y2) {
+        var d1 = Math.abs(x2 - x1)
+        ,   d2 = Math.abs(y2 - y1) ;
+        return d1 + d2;
+    },
+    
+    path : function path(start, end){
+        var size = this.gridSquareSize, floor = Math.floor
+        ,   grid = this.grid()
+        
+        ,   startX = floor(start.x/size)
+        ,   startY = floor(start.y/size)
+        ,   startN = grid[startX][startY]
+        
+        ,   endX   = floor(end.x/size)
+        ,   endY   = floor(end.y/size)
+        ,   endN   = grid[endX][endY]
+        
+        ,   path = Y(astar.search(grid, startN, endN))
+        ;
+        
+        if (tanks.config.pathing.overlayAIPaths)
+            this.drawPath(startN, path);
+        
+        return path
+                .invoke('scale', size)
+                .invoke('add', this.gridSquareMidPt)
+                .end();
+    },
+    
+    drawPath : function drawPath(start, path){
+        var size   = this.gridSquareSize, off = size*0.15
+        ,   w      = this.width-2,  h = this.height-2
+        ,   el     = this.game.viewport
+        ,   grid   = this.grid()
+        ,   canvas = $('.path', el)[0]
+        ;
+        
+        if (!canvas) {
+            canvas = $('<canvas class="path" style="position:absolute"/>').appendTo(el)[0];
+            $(canvas).width(w).height(h);
+            canvas.width  = w;
+            canvas.height = h;
+        }
+        
+        var ctx = canvas.getContext('2d');
+        ctx.lineWidth = 0;
+        
+        // Clear the canvas
+        ctx.beginPath();
+        ctx.clearRect(this.x,this.y, w,h);
+        ctx.closePath();
+        
+        
+        // Draw blockers
+        // var s = size/2;
+        // ctx.fillStyle = 'rgba(255,0,0,0.2)';
+        // 
+        // Y(grid).invoke('forEach', function(node){
+        //     if ( !node.blocked ) return;
+        //     
+        //     var x = node.x*size + s
+        //     ,   y = node.y*size + s ;
+        //     
+        //     ctx.beginPath();
+        //     ctx.moveTo( x,   y-s );
+        //     ctx.lineTo( x+s, y   );
+        //     ctx.lineTo( x,   y+s );
+        //     ctx.lineTo( x-s, y   );
+        //     ctx.fill();
+        //     ctx.closePath();
+        // });
+        
+        
+        // Draw path
+        var r = size/2 - off;
+        
+        function drawStep(p, i){
+            var x = p.x*size + off + r
+            ,   y = p.y*size + off + r ;
+            
+            ctx.beginPath();
+            ctx.arc(x,y, r, 0, Math.PI*2, false);
+            ctx.fill();
+            ctx.closePath();
+        }
+        this.drawStep = drawStep;
+        
+        ctx.fillStyle = 'rgba(0,0,0,0.2)';
+        path.forEach(drawStep);
+        
+        
+        // Draw start
+        ctx.fillStyle = 'rgba(0,255,0,0.2)';
+        drawStep(start, 0);
+        
+        // Draw finish
+        ctx.fillStyle = 'rgba(0,0,255,0.2)';
+        drawStep( path.last() );
+        
+        $(canvas).show();
+    },
+    
+    clearPath : function clearPath(){
+        $('.path', this.game.viewport).hide();
+    },
+    
+    
+    
     _overlayBG : $('<img src="img/pathmap-bg.png" />')[0],
     overlay : function overlay(gridEl){
-        var w = this.width  *SCALE
-        ,   h = this.height *SCALE
+        var w = this.width-2
+        ,   h = this.height-2
         ,   canvas = $('.overlay', gridEl)[0];
         
         if (!canvas) {
@@ -110,7 +299,7 @@ PathMap = new Y.Class('PathMap', QuadTree, {
         }
         
         var ctx = canvas.getContext('2d');
-        ctx.scale(SCALE, SCALE);
+        // ctx.scale(SCALE, SCALE);
         
         // Clear the canvas
         ctx.beginPath();
@@ -124,8 +313,9 @@ PathMap = new Y.Class('PathMap', QuadTree, {
         
         
         // Draw regions
-        this.reduce(function(acc, value, r, tree){
-            if ( acc[r.id] ) return acc;
+        this.reduce(function(acc, v, r, tree){
+            if ( acc[r.id] || v.isBoundary )
+                return acc;
             
             acc[r.id] = r;
             
@@ -150,3 +340,16 @@ PathMap = new Y.Class('PathMap', QuadTree, {
     
 });
 
+var QT = QuadTree.prototype;
+
+'set remove removeAll clear'
+    .split(' ')
+    .forEach(function(name){
+        PathMap.prototype[name] = function(){
+            delete this._grid;
+            return QT[name].apply(this, arguments);
+        };
+    });
+
+
+})();
index c684716..b4a2af0 100644 (file)
@@ -59,31 +59,11 @@ PlayerTank = Tank.subclass('PlayerTank', {
         return this;
     },
     
-    attack : function attack(){
-        var WIGGLE = 4;
-        
-        if ( !this.cooldowns.attack.ready )
-            return;
-        
-        var barrel = this.barrel
-        ,   bb = this.boundingBox
-        ,   w2 = bb.width/2,    h2 = bb.height/2
-        ,   x0 = bb.x1+w2,      y0 = bb.y1+h2
-        
-        ,   theta  = barrel.transform.rotate
-        ,   sin = Math.sin(theta),  cos = Math.cos(theta)
-        
-        ,   x1 = x0 + w2*cos, y1 = y0 + h2*sin
-        ,   sz = (barrel.boundingBox.width - w2)/2 + WIGGLE
-        ,   blockers = this.game.pathmap.get(x1-sz,y1-sz, x1+sz,y1+sz).remove(this)
-        ;
-        
-        if ( blockers.size() ) return; // console.log('squelch!', blockers);
-        
+    attack : function attack(x,y){
         this.queue.push({
             type : 'fire',
-            x : x0 + REF_SIZE*cos,
-            y : y0 + REF_SIZE*sin
+            x : x,
+            y : y
         });
     },
     
index 0add167..3b9974c 100644 (file)
@@ -30,6 +30,11 @@ Tank = Thing.subclass('Tank', {
     init : function init(align){
         Thing.init.call(this, align);
         this.onBulletDeath = this.onBulletDeath.bind(this);
+        
+        var self = this;
+        this.addEventListener('destroy', function(){
+            this.game.pathmap.clearPath();
+        });
     },
     
     cannonReady : function cannonReady(){
@@ -54,18 +59,66 @@ Tank = Thing.subclass('Tank', {
                 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)
-                    .remove(this)
-                    .shift();
-        if (t) {
-            // console.log(this, 'moving toward', t);
-            this.move(t.loc.x, t.loc.y);
-            return this;
-        }
+        this.continueMove();
+        
+        return this;
+    },
+    
+    continueMove : function continueMove(){
+        if (!this.currentMove)
+            this.recalculatePath();
+        
+        var to = this.currentMove;
+        if (!to) return;
+        
+        this.move(to.x, to.y);
+        if ( this.boundingBox.midpoint().equals(to) )
+            this.currentMove = null;
+    },
+    
+    recalculatePath : function recalculatePath(){
+        var t = this.nearLike(10000, 'Y.is(Tank, _) && _.align !== '+this.align).shift()
+        ,   pm = this.game.pathmap
+        ;
+        
+        if (!t) return;
+        
+        pm.clearPath();
+        
+        // console.log(this, 'moving toward', t);
+        var end = t.boundingBox.midpoint()
+        ,   bb = this.boundingBox
+        ,   mid = bb.midpoint()
+        ,   start = mid
+        
+        ,   path  = this.lastPath = pm.path(start, end)
+        ,   to = this.currentMove = path[0]
+        ;
+        
+        // VVV We only really need this code if we're going to recalculate before we reach the currentMove
+        
+        // we may need to move closer to start if we occupy multiple grid-squares
+        // var tosq = pm.vec2Square(to), manhattan = pm.manhattan
+        // ,   tl = pm.vec2Square(bb.x1,bb.y1), tr = pm.vec2Square(bb.x2,bb.y1)
+        // ,   bl = pm.vec2Square(bb.x1,bb.y2), br = pm.vec2Square(bb.x2,bb.y2)
+        // ,   straddles = [tl, tr, bl, br]
+        // ;
+        // 
+        // 
+        // if ( !tl.equals(br) ) {
+        //     tl.to = pm.vec2Square(bb.x1,bb.y1); tr.to = pm.vec2Square(mid.x,bb.y1);
+        //     bl.to = pm.vec2Square(bb.x1,mid.y); br.to = pm.vec2Square(mid.x,mid.y);
+        //     straddles.sort(function(a,b){
+        //         return Y.op.cmp(manhattan(a,tosq), manhattan(b,tosq));
+        //     });
+        //     to = pm.square2Vec(straddles[0].to).add(pm.gridSquareMidPt);
+        //     path.unshift(to);
+        // }
+        
+        // console.log('start:', start, 'straddles:', straddles.map(pm.square2Vec.bind(pm)), 'end:', end, 'next:', to);
     },
     
     nearLike : function nearLike(ticks, fn){
@@ -80,11 +133,35 @@ Tank = Thing.subclass('Tank', {
     },
     
     shoot : function shoot(x,y){
-        this.rotateBarrel(x,y);
+        var WIGGLE = 4
+        ,   xydef = (x !== undefined && y !== undefined)
+        ;
+        
+        if (xydef) this.rotateBarrel(x,y);
         
         if ( this.nShots >= this.stats.shots || !this.cooldowns.attack.activate(NOW) )
             return null;
         
+        var barrel = this.barrel
+        ,   bb = this.boundingBox
+        ,   w2 = bb.width/2,    h2 = bb.height/2
+        ,   x0 = bb.x1+w2,      y0 = bb.y1+h2
+        
+        ,   theta  = barrel.transform.rotate
+        ,   sin = Math.sin(theta),  cos = Math.cos(theta)
+        
+        ,   x1 = x0 + w2*cos, y1 = y0 + h2*sin
+        ,   sz = (barrel.boundingBox.width - w2)/2 + WIGGLE
+        ,   blockers = this.game.pathmap.get(x1-sz,y1-sz, x1+sz,y1+sz).remove(this)
+        ;
+        
+        if ( blockers.size() ) return; // console.log('squelch!', blockers);
+        
+        if (!xydef) {
+            x = x0 + REF_SIZE*cos;
+            y = y0 + REF_SIZE*sin;
+        }
+        
         var ProjectileType = this.projectile
         ,   p = new ProjectileType(this, x,y);
         
@@ -111,6 +188,38 @@ Tank = Thing.subclass('Tank', {
         return new math.Vec(x,y);
     },
     
+    move : function move(x,y){
+        var bb = this.boundingBox
+        ,   w2 = bb.width/2, h2 = bb.height/2
+        ;
+        return this.moveByAngle( this.angleTo(x,y), x-w2,y-h2 );
+    },
+    
+    moveByAngle : function moveByAngle(theta, targetX,targetY){
+        var abs = Math.abs
+        ,   bb = this.boundingBox, w2 = bb.width/2, h2 = bb.height/2
+        ,   loc = this.loc, mid = bb.midpoint()
+        ,   to  = loc.moveByAngle(theta, this.stats.move * SQUARETH)
+        ;
+        
+        // Don't overshoot the target
+        if ( targetX !== undefined && abs(loc.x-to.x) > abs(loc.x-targetX) )
+            to.setXY(targetX, to.y);
+        
+        if ( targetY !== undefined && abs(loc.y-to.y) > abs(loc.y-targetY) )
+            to.setXY(to.x, targetY);
+        
+        var nbb = bb.add(to.x,to.y)
+        ,   blockers = this.game.pathmap.get(nbb.x1,nbb.y1, nbb.x2,nbb.y2).remove(this)
+        ;
+        
+        if ( !blockers.size() )
+            this.game.moveAgentTo(this, to.x, to.y);
+        
+        return this;
+    },
+    
+    
     
     
     /// Rendering Methods ///
index 0177d53..66bbd78 100644 (file)
@@ -132,22 +132,6 @@ Thing = new Evt.Class('Thing', {
         return this;
     },
     
-    move : function move(x,y){
-        return this.moveByAngle( this.angleTo(x,y) );
-    },
-    
-    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;
-    },
-    
     
     
     
index ff16226..82bba0a 100644 (file)
--- a/tanks.php
+++ b/tanks.php
@@ -64,6 +64,10 @@ class Tanks {
         "src/portal/shape/line.js",
         "src/portal/shape/polygon.js",
         
+        "src/portal/util/binaryheap.js",
+        "src/portal/util/graph.js",
+        "src/portal/util/astar.js",
+        
         "src/portal/util/tree/quadtree.js",
         
         "src/portal/loop/eventloop.js",