From ea386b8d34acd114f17f9146e8c681fea5a74399 Mon Sep 17 00:00:00 2001 From: dsc Date: Sun, 9 Jan 2011 02:26:50 -0800 Subject: [PATCH] Adds Fences; minor refactor to map-like stuff. --- data/types/levels.yaml | 10 +- src/tanks/constants.cjs | 32 +++- src/tanks/map/fence.cjs | 31 +++ src/tanks/map/index.cjs | 12 +- src/tanks/map/level.cjs | 4 +- src/tanks/map/map.cjs | 60 ++++-- src/tanks/map/pathing/index.cjs | 6 + src/tanks/map/pathing/pathmap.cjs | 344 +++++++++++++++++++++++++++++++ src/tanks/map/pathing/trajectory.cjs | 178 ++++++++++++++++ src/tanks/map/pathing/traversal.cjs | 169 +++++++++++++++ src/tanks/map/pathmap.cjs | 374 ---------------------------------- src/tanks/map/trajectory.cjs | 172 ---------------- src/tanks/map/traversal.cjs | 157 -------------- src/tanks/map/wall.cjs | 106 ++++++++++ src/tanks/mixins/quantified.cjs | 4 +- src/tanks/thing/bullet.cjs | 15 +- src/tanks/thing/fence.cjs | 80 ------- src/tanks/thing/index.cjs | 2 - src/tanks/thing/item.cjs | 8 +- src/tanks/thing/tank.cjs | 134 +++++++------ src/tanks/thing/thing.cjs | 8 +- src/tanks/thing/wall.cjs | 83 -------- src/tanks/ui/pathmapui.cjs | 6 +- 23 files changed, 1014 insertions(+), 981 deletions(-) create mode 100644 src/tanks/map/fence.cjs create mode 100644 src/tanks/map/pathing/index.cjs create mode 100644 src/tanks/map/pathing/pathmap.cjs create mode 100644 src/tanks/map/pathing/trajectory.cjs create mode 100644 src/tanks/map/pathing/traversal.cjs delete mode 100644 src/tanks/map/pathmap.cjs delete mode 100644 src/tanks/map/trajectory.cjs delete mode 100644 src/tanks/map/traversal.cjs create mode 100644 src/tanks/map/wall.cjs delete mode 100644 src/tanks/thing/fence.cjs delete mode 100644 src/tanks/thing/wall.cjs diff --git a/data/types/levels.yaml b/data/types/levels.yaml index df2c2f5..86b4a1f 100644 --- a/data/types/levels.yaml +++ b/data/types/levels.yaml @@ -30,10 +30,18 @@ types: args: [150,300, 50,50] - type: wall args: [100,100, 50,50] + - type: fence + args: [360,210, 130,30] + - type: fence + args: [10,210, 30,30] + - type: fence + args: [110,210, 30,30] + - type: fence + args: [210,210, 30,30] units: - type: player align: 1 - loc: [275,475] + loc: [325,475] - type: blue align: 1 loc: [175,475] diff --git a/src/tanks/constants.cjs b/src/tanks/constants.cjs index b44aa33..2404b79 100644 --- a/src/tanks/constants.cjs +++ b/src/tanks/constants.cjs @@ -1,11 +1,11 @@ -//#exports PathingType.{PASSABLE,ZONE,BLOCKING,IRREGULAR} +//#exports BoundsType.{PASSABLE,ZONE,BLOCKING,IRREGULAR} +//#exports DensityType.{PASSABLE,BOUNDARY,DENSE,SPARSE,IRREGULAR} //#exports StatInvariant.{NONE,RATIO,FULL} require('Y').Y.extend(exports, { - /// Pathing Constants /// - /** How this object interacts with the world. */ - PathingType : { + /** How to determine the bounds of this object for pathing. */ + BoundsType : { /** Does not obstruct other objects. */ PASSABLE : 0, @@ -21,6 +21,30 @@ require('Y').Y.extend(exports, { }, + /** + * Governs the kinds of objects this object obstructs. + */ + DensityType : { + + /** Not an obstruction. */ + PASSABLE : 0, + + /** Blocks absolutely everything. No exceptions. */ + BOUNDARY : 1, + + /** Blocks everything material (eg, units). */ + DENSE : 2, + + /** Does not block small things (eg, projectiles). */ + SPARSE : 3, + + /** Blocks based on custom rules. */ + IRREGULAR : 4 + + }, + + + /** * Invariant to restore for the current value when modifying the base or max of a Stat. * @see {tanks.effects.stat.Stat} for the default behavior of its methods. diff --git a/src/tanks/map/fence.cjs b/src/tanks/map/fence.cjs new file mode 100644 index 0000000..cb1e79b --- /dev/null +++ b/src/tanks/map/fence.cjs @@ -0,0 +1,31 @@ +var Y = require('Y').Y + +, constants = require('tanks/constants') +, BoundsType = constants.BoundsType +, DensityType = constants.DensityType +, Wall = require('tanks/map/wall').Wall +, + + +Fence = +exports['Fence'] = +Wall.subclass('Fence', { + + isReflective : false, + fillStyle : 'rgba(0,0,0, 0.25)', + strokeStyle : 'transparent', + lineWidth : 0, + + blocking : BoundsType.BLOCKING, + density : DensityType.SPARSE, + isBoundary : false, + + + + init : function initFence(x,y, w,h){ + Wall.init.call(this, x,y, w,h, this.isBoundary); + } + +}); + +Wall.register('fence', Fence); diff --git a/src/tanks/map/index.cjs b/src/tanks/map/index.cjs index d143552..0b2364f 100644 --- a/src/tanks/map/index.cjs +++ b/src/tanks/map/index.cjs @@ -1,9 +1,9 @@ -var Y = require('Y').Y; - -Y.extend(exports, { +require('Y').Y +.extend(exports, { + 'pathing' : require('tanks/map/pathing'), + 'Map' : require('tanks/map/map').Map, - 'PathMap' : require('tanks/map/pathmap').PathMap, 'Level' : require('tanks/map/level').Level, - 'Traversal' : require('tanks/map/traversal').Traversal, - 'Trajectory' : require('tanks/map/trajectory').Trajectory + 'Wall' : require('tanks/map/wall').Wall, + 'Fence' : require('tanks/map/fence').Fence }); diff --git a/src/tanks/map/level.cjs b/src/tanks/map/level.cjs index 328de4a..c31d975 100644 --- a/src/tanks/map/level.cjs +++ b/src/tanks/map/level.cjs @@ -5,12 +5,12 @@ var Y = require('Y').Y , Rect = require('ezl/shape').Rect , Buff = require('tanks/effects/buff').Buff -, PathMap = require('tanks/map/pathmap').PathMap +, PathMap = require('tanks/map/pathing/pathmap').PathMap , Thing = require('tanks/thing/thing').Thing , Tank = require('tanks/thing/tank').Tank , Item = require('tanks/thing/item').Item , Player = require('tanks/thing/player').Player -, Wall = require('tanks/thing/wall').Wall +, Wall = require('tanks/map/wall').Wall , Speciated = require('tanks/mixins/speciated').Speciated , min = Y(Math.min).limit(2) diff --git a/src/tanks/map/map.cjs b/src/tanks/map/map.cjs index feec7a3..92461b4 100644 --- a/src/tanks/map/map.cjs +++ b/src/tanks/map/map.cjs @@ -1,32 +1,45 @@ // -*- mode: JavaScript; tab-width: 4; indent-tabs-mode: nil; -*- var Y = require('Y').Y -, QuadTree = require('ezl/util/tree/quadtree').QuadTree +, QuadTree = require('ezl/util/tree/quadtree').QuadTree - -/** Does not obstruct other objects. */ -, PASSABLE = exports['PASSABLE'] = 0 - -/** Does not obstruct other objects, but still collides with them. */ -, ZONE = exports['ZONE'] = 1 - -/** Obstructs other blockers with its BoundingBox. */ -, BLOCKING = exports['BLOCKING'] = 2 - -/** Potentially obstructs other objects, but requires a special test once a BoundingBox collision has been detected. */ -, IRREGULAR = exports['IRREGULAR'] = 3 +, constants = require('tanks/constants') +, BoundsType = constants.BoundsType +, DensityType = constants.DensityType , Map = exports['Map'] = QuadTree.subclass('Map', { + allWalls : null, // All walls in this map + innerWalls : null, // Non-boundary walls + denseWalls : null, // Non-boundary non-Fences + + + init : function initMap(x1,y1, x2,y2, capacity){ + this.allWalls = Y([]); + this.innerWalls = Y([]); + this.denseWalls = Y([]); + QuadTree.init.call(this, x1,y1, x2,y2, capacity); + }, addBlocker : function addBlocker(obj){ this.removeBlocker(obj); + var pt = obj.blocking , bb = obj.bbox ; if (pt !== undefined && bb) obj.region = this.set(bb.x1,bb.y1, bb.x2,bb.y2, obj); + + if (obj.isWall) { + if ( !this.allWalls.has(obj) ) + this.allWalls.push(obj); + if ( !(obj.isBoundary || this.innerWalls.has(obj)) ) + this.innerWalls.push(obj); + if ( obj.density === DensityType.DENSE || obj.density === DensityType.BOUNDARY ) + this.denseWalls.push(obj); + } + return obj; }, @@ -35,6 +48,13 @@ QuadTree.subclass('Map', { this.remove(obj.region); delete obj.region; } + + if (obj.isWall) { + this.allWalls.remove(obj); + this.innerWalls.remove(obj); + this.denseWalls.remove(obj); + } + return obj; }, @@ -44,13 +64,21 @@ QuadTree.subclass('Map', { y2 = x1.y2; x2 = x1.x2; y1 = x1.y1; x1 = x1.x1; } - var bs = this.get(x1,y1, x2,y2) - .filter('_.blocking === BLOCKING'); + var bs = this.get(x1,y1, x2,y2).filter(this._filterBlockers, this); if (ignore && bs.length) return bs.remove.apply(bs, ignore || []); else return bs; - } + }, + _filterBlockers : function _filterBlockers(v){ + return (v.blocking === BoundsType.BLOCKING || v.blocking === BoundsType.IRREGULAR); + }, + + wallObstructs : function wallObstructs(line){ + return this.innerWalls.some(function(wall){ + return wall.bbox.intersects(line); + }, this); + } }); diff --git a/src/tanks/map/pathing/index.cjs b/src/tanks/map/pathing/index.cjs new file mode 100644 index 0000000..633d120 --- /dev/null +++ b/src/tanks/map/pathing/index.cjs @@ -0,0 +1,6 @@ +require('Y').Y +.extend(exports, { + 'PathMap' : require('tanks/map/pathing/pathmap').PathMap, + 'Traversal' : require('tanks/map/pathing/traversal').Traversal, + 'Trajectory' : require('tanks/map/pathing/trajectory').Trajectory +}); diff --git a/src/tanks/map/pathing/pathmap.cjs b/src/tanks/map/pathing/pathmap.cjs new file mode 100644 index 0000000..6dc6504 --- /dev/null +++ b/src/tanks/map/pathing/pathmap.cjs @@ -0,0 +1,344 @@ +// -*- mode: JavaScript; tab-width: 4; indent-tabs-mode: nil; -*- +var Y = require('Y').Y + +, math = require('ezl/math') +, vec = require('ezl/math/vec') +, QuadTree = require('ezl/util/tree/quadtree').QuadTree +, BinaryHeap = require('ezl/util/tree/binaryheap').BinaryHeap +, Vec = vec.Vec +, Line = math.Line + +, config = require('tanks/config').config +, constants = require('tanks/constants') +, BoundsType = constants.BoundsType +, DensityType = constants.DensityType +, Map = require('tanks/map/map').Map + + +, SQRT_TWO = Math.sqrt(2) +, + +/** + * A QuadTree which aids in pathing for AI. + * - QuadTree methods which took rect coords (x1,y1, x2,y2) will accept + * anything Rect-like (having properties x1,y1, x2,y2). + * - Pathing methods will account for the unit's size when calculating paths, + * intersections, blocking, and similar tasks. + */ +PathMap = +exports['PathMap'] = +Map.subclass('PathMap', { + // Config + gridSquare : null, + gridSquareMid : null, + + // State + _squares : null, // Cache of Square objects + _pathId : -1, // Instance of A* being run, to track when to reset Square instances + + + + init : function init(x1,y1, x2,y2, capacity, buffer_size) { + this.buffer_size = buffer_size; + x1 -= buffer_size; y1 -= buffer_size; + x2 += buffer_size; y2 += buffer_size; + Map.init.call(this, x1,y1, x2,y2, capacity); + this._squares = {}; + }, + + /** + * Takes normal (not gridSquare-relative) coordinates. + */ + _getSquare : function getSquare(x,y){ + if (x instanceof Array) { y=x[1]; x=x[0]; } + var cache = this._squares + , SIZE = this.gridSquare ; + + x = Math.floor(x / SIZE) * SIZE; + y = Math.floor(y / SIZE) * SIZE; + var key = x+'_'+y + , sq = cache[key] ; + + if (sq) + sq.reset(); + else + sq = cache[key] = new Square(this, x,y); + + return sq; + }, + + /** + * Finds the shortest path to a destination for the given agent. + * @param {Thing} agent Agent whose path is being generated. + * @param {Vec} end End destination. + */ + /** + * Finds the shortest path to a destination for the given agent. + * @param {Thing} agent Agent whose path is being generated. + * @param {Integer} x End destination x-coordinate. + * @param {Integer} y End destination y-coordinate. + */ + // * @param {Function} [passable] Function which tests whether a node is passable. + path : function path(agent, x,y){ + ++this._pathId; // Current pathId for Square calculation + this._agent = agent; // Current agent for Square calculation + + var heuristic = vec.manhattan + , start = this._getSquare(agent.loc) + , end = this._getSquare(x,y) + , open = new BinaryHeap('dist') // 'dist' is the scoring key on heap objects + + , iterations = 0 + ; + + open.push(start); + while (open.length) { + iterations++; + + // 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 = Y([]) + , mid = this.gridSquareMid + , node = current; + while( node.prev ) { + path.push( vec.sum(node,mid) ); + node = node.prev; + } + + if (!path.length) + return path; + + path = Y(path.reverse()); + + // Ensure we don't get stuck on a corner on our first step + var first = path.first() + , bb = agent.bbox.relocated( vec.lerp(0.5, agent.loc,first) ) + , blockers = this.getBlockers(bb, [agent]); + + if (blockers.length) + path.unshift( vec.sum(start,mid) ); + + return path; + } + + // Normal case -- move current from open to closed, process each of its neighbors + current.closed = true; + + var neighbors = current.getNeighbors(); + for (var i=0, il = neighbors.length; i < il; i++) { + var n = neighbors[i] + , node = n[0] + , weight = n[1] + ; + + // Move on if we've done this already, or if it's blocker (ignoring start) + if ( node.closed || (node.blocked && node !== start && node !== end) ) + continue; + + // startDist 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. + // --> In fact! Weight is determined by Square.getNeighbors() + var startDist = current.startDist + weight; + + if ( !node.visited || startDist < node.startDist ) { + + // Found an optimal (so far) path to this node. Take score for node to see how good it is. + node.prev = current; + node.endDist = node.endDist || heuristic(node, end); + node.startDist = startDist; + node.dist = startDist + node.endDist; + + // New? Add to the set of nodes to process + if ( !node.visited ) { + node.visited = true; + open.push(node); + + // Seen, but since it has been rescored we need to reorder it in the heap + } else + open.rescore(node); + } + var _node = node; + } + } + + // No result was found -- empty array signifies failure to find path + return Y([]); + }, + + vec2Square : function vec2Square(x,y){ + if (x instanceof Array){ y = x.y; x = x.x; } + var floor = Math.floor, SIZE = this.gridSquare; + return new 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.gridSquare; + return new Vec(floor(x)*SIZE, floor(y)*SIZE); + } + +}) +, + + +// Would be nice if I could make this an inner class (for once) +Square = +exports['Square'] = +Y.subclass('Square', new Vec(0,0), { + pathId : null, // instance of A* being run, to track when to reset + + // resetable: + dist : 0, // estimated distance + startDist : 0, // distance from start + endDist : 0, // estimated distance to end + visited : false, + closed : false, + prev : null, + + + /** + * @param x Left coord of square. + * @param y Top coord of square. + */ + init : function initSquare(pathmap, x,y){ + Vec.init.call(this, x,y); + this.pathmap = pathmap; + this.reset(); + }, + + reset : function reset(){ + var pathId = this.pathmap._pathId; + if (this.pathId === pathId) + return this; + + this.pathId = pathId; + this.agent = this.pathmap._agent; + this.blocked = this._blocked(); + + this.dist = 0; + this.startDist = 0; + this.endDist = 0; + this.visited = false; + this.closed = false; + this.prev = null; + + return this; + }, + + /** + * @private Calculates this.blocked value. Should only be called by reset() to cache the result. + */ + _blocked : function blocked(){ + var pm = this.pathmap + // , bb = this.agent.bbox + + // , origin = bb.relOrigin + // , left = origin.x, right = bb.width - left + // , top = origin.y, bottom = bb.height - top + + , SIZE = pm.gridSquare + , x = Math.floor(this.x) + , y = Math.floor(this.y) + // , x1 = x - left, x2 = x + SIZE + right + // , y1 = y - top, y2 = y + SIZE + bottom + , x1 = x, x2 = x + SIZE + , y1 = y, y2 = y + SIZE + + , blockers = pm.get(x1,y1, x2,y2) + .remove(this.agent) + .filter(this._filterBlocked, this) + ; + + return !!blockers.length; + }, + + /** + * @private + */ + _filterBlocked : function filterBlocked(v, r){ + return (v.blocking === BoundsType.BLOCKING && !v.isBoundary) + || (v.blocking === BoundsType.IRREGULAR && v.testCollide(this.agent,this,null) ); // FIXME: hm. calc bbox? + }, + + getNeighbors : function getNeighbors(){ + var neighbors = [] + , abs = Math.abs + , pm = this.pathmap + , agent = pm._agent + , SIZE = pm.gridSquare + + , x = this.x, y = this.y + , sq, cost, ix, iy, x1,y1, x2,y2 + ; + + for (ix=-1; ix<2; ix++) { + for (iy=-1; iy<2; iy++) { + // Skip self + if (ix === 0 && iy === 0) + continue; + + cost = 1; + x1 = x + ix*SIZE; y1 = y + iy*SIZE; + x2 = x1+SIZE; y2 = y1+SIZE; + + // filter squares outside bounds + if ( pm.x1 >= x1 || pm.y1 >= y1 || + pm.x2 <= x2 || pm.y2 <= y2 ) + continue; + + // Diagonal square + if ( abs(ix) === 1 && abs(iy) === 1 ) { + cost = SQRT_TWO; + + // Ensure we don't cut a blocked corner + if ( pm._getSquare(x1,y).blocked || pm._getSquare(x,y1).blocked ) + continue; + } + + sq = pm._getSquare(x1,y1); + neighbors.push([ sq, cost*SIZE ]); + } + } + + return neighbors; + }, + + toString : function(){ + var props = []; + if (this.blocked) props.push('blocked'); + if (this.visited) props.push('dist='+this.dist); + if (this.closed) props.push('closed'); + props = props.join(', '); + if (props) props = '('+props+')'; + return '['+this.x+' '+this.y+']'+props; + } + +}) +; + + + +// Wrap QuadTree Methods to accept Rect-like objects +'overlaps get set removeAll leaves collect' + .split(' ') + .forEach(function(name){ + var method = PathMap.fn[name] || QuadTree.fn[name]; + PathMap.fn[name] = function pmHandleRect(o){ + var args = arguments, xy; + if (o.x1 !== undefined){ + xy = [o.x1,o.y1, o.x2,o.y2]; + args = (args.length === 1) ? xy : xy.concat(Y(args,1)); + } + return method.apply(this, args); + }; + }); + +// Stay sync'd with config +config.updateOnChange( + ['pathing.gridSquare', 'pathing.gridSquareMid'], + PathMap.fn); + diff --git a/src/tanks/map/pathing/trajectory.cjs b/src/tanks/map/pathing/trajectory.cjs new file mode 100644 index 0000000..837c758 --- /dev/null +++ b/src/tanks/map/pathing/trajectory.cjs @@ -0,0 +1,178 @@ +var Y = require('Y').Y +, op = require('Y/op') +, math = require('ezl/math') +, Vec = math.Vec +, Line = math.Line +, Rect = math.Rect +, BoundingBox = require('ezl/loc').BoundingBox + +, Thing = require('tanks/thing/thing').Thing + +, BOUND_SIZE_RATIO = 0.75 +, + + +Trajectory = +exports['Trajectory'] = +Line.subclass('Trajectory', { + tCurrent : 0, + + + init : function initTrajectory(owner, x1,y1, x2,y2, tdist, tCurrent){ + Y.bindAll(this, 'compare', 'closer', 'intersects'); + + this.owner = owner; + this.game = owner.game; + this.pathmap = this.game.pathmap; + + this.reset(x1,y1, x2,y2, tdist, tCurrent); + }, + + reset : function reset(x1,y1, x2,y2, tdist, tCurrent){ + if (x1 instanceof Array && y1 instanceof Array) { + tdist = x2; + y2 = y1[1]; x2 = y1[0]; + y1 = x1[1]; x1 = x1[0]; + } + + // init with raw numbers to do calculations + Line.init.call(this, x1,y1, x2,y2, tdist || this.tdist); + + // Find appropriate edge in direction of line + var pm = this.pathmap + , ex = (x1 > x2 ? -REF_SIZE : REF_SIZE+pm.width ) + , ey = (y1 > y2 ? -REF_SIZE : REF_SIZE+pm.height) + , edge = this.near(ex,ey) + ; + + // Move goal point beyond far wall to avoid rotations post-reflection + x2 = this.x2 = edge.x; + y2 = this.y2 = edge.y; + this.p2 = new Vec(x2,y2); + Vec.init.call(this, x2-x1, y2-y1); + + this.tCurrent = tCurrent || 0; + this.resetBound(); + return this; + }, + + // Determine how much time can pass before we risk teleporting + // We'll need to reset this whenever the bounding box changes size + resetBound : function resetBound(){ + this.tBound = + BOUND_SIZE_RATIO * Math.min( + Math.abs(this.owner.width / this.pa), + Math.abs(this.owner.height / this.pb) ); + return this; + }, + + clone : function clone(){ + return new Trajectory(this.owner, this.x1,this.y1, this.x2,this.y2, this.tdist, this.tCurrent); + }, + + + intersects : function intersects(x,y){ + var o = x; + if (o instanceof Thing) + return o.bbox.intersects(this); + + if (o instanceof Rect) + return o.intersects(this); + + return Line.fn.intersects.call(this, x,y); + }, + + + /** + * Compares how distant in the future two objects are on this trajectory. + * Objects that have been passed are always further away than those in the future, + * but otherwise the comparison is performed by absolute distance. + * @return -1 if a closer b, 1 if a further b, 0 if a same as b + */ + compare : function compare(a, b){ + if (a instanceof Thing) a = a.loc; + if (b instanceof Thing) b = b.loc; + + var abs = Math.abs + , t = this.tCurrent + + , xa = this.calcX(a.y), ya = this.calcY(a.x) + , ta = this.iparametric(xa,ya) - t + + , xb = this.calcX(b.y), yb = this.calcY(b.x) + , tb = this.iparametric(xb,yb) - t + ; + + // If one has passed, return the other + if ( ta < 0 && tb >= 0 ) + return 1; + if ( tb < 0 && ta >= 0 ) + return -1; + + return op.cmp(abs(ta), abs(tb)); + }, + + closer : function closer(o1, o2){ + return this.compare(o1,o2) === 1 ? o2 : o1; + }, + + sortClosest : function sortClosest(o1, o2){ + var things; + if (o1 instanceof Y.YArray) + things = o1; + else if (o1 instanceof Array) + things = Y(o1); + else + things = new Y(arguments); + + return things.sort( this.compare ); + }, + + closest : function closest(o1, o2){ + return this.sortClosest.apply(this, arguments).shift(); + }, + + comesWithin : function comesWithin(pt, w,h){ + if ( !this.owner.loc ) + return false; + + if (pt instanceof Thing) pt = pt.loc; + + if ( w === undefined ){ + w = 0; h = 0; + } else if ( Y.isNumber(w.width) ){ + h = w.height; w = w.width; + } + + var cur = this.owner.loc + , fx = this.calcX(pt.y), fy = this.calcY(pt.x) + , t = this.iparametric(cur.x, cur.y) + , ft = this.iparametric(fx,fy) + , dw = Math.abs(fx - pt.x), dh = Math.abs(fy - pt.y) + ; + return ( t <= ft && (dw <= w || dh <= h) ); + }, + + pathBlocked : function pathBlocked(obj, ignore){ + var walls, blockers; + if (this.owner.isProjectile) + walls = this.pathmap.denseWalls; + else + walls = this.pathmap.innerWalls; // FIXME: won't filter out concave intersections with the bounds + + blockers = walls + .concat( this.game.units ) + .apply('remove', [this.owner].concat(ignore || []) ) + .filter( this.intersects ) + .sort( this.compare ) + + , blocker = blockers.shift() + ; + + return (blocker === obj ? false : blocker); + }, + + toString : function(){ + return 'T['+this.p1+', '+this.p2+', slope='+this.slope.toFixed(3)+']'; + } +}); diff --git a/src/tanks/map/pathing/traversal.cjs b/src/tanks/map/pathing/traversal.cjs new file mode 100644 index 0000000..6f6beda --- /dev/null +++ b/src/tanks/map/pathing/traversal.cjs @@ -0,0 +1,169 @@ +var Y = require('Y').Y +, constants = require('tanks/constants') +, BoundsType = constants.BoundsType +, DensityType = constants.DensityType +, + +Traversal = +exports['Traversal'] = +Y.subclass('Traversal', { + ignore : null, // objects to ignore when determining blockers + + isBlocked : false, + to : null, // furthest point reached + remaining : 0, // time left unconsumed due to blocker + blocker : null, // blocking object + side : null, // collision side of blocker (Line) + + + + init : function initTraversal(thing, trajectory, ignore){ + this.ignore = Y(ignore || []); + + this.thing = thing; + this.game = thing.game; + this.pathmap = thing.game.pathmap; + this.bbox = thing.bbox.clone(); + + this.trajectory = trajectory || thing.trajectory; + }, + + traverse : function traverse(t, tx,ty){ + var tr = this.trajectory; + this.remaining = t; + + do { + this.step(tx,ty); + } while (!this.isBlocked && this.remaining > 0); + + return this.to; + }, + + /** + * Checks for blockers and moves traversal bounds forward as much as possible. + */ + step : function step(tx,ty){ + this.start = this.bbox.clone(); + var tr = this.trajectory + , or = this.thing.loc + , dt = Math.min(tr.tBound, this.remaining) + , to = tr.parametric(tr.tCurrent+dt) // calc how far down the tr we go + ; + + // Don't overshoot the target + if ( tx !== undefined && Math.abs(or.x-to.x) > Math.abs(or.x-tx) ) + to.x = tx; + + if ( ty !== undefined && Math.abs(or.y-to.y) > Math.abs(or.y-ty) ) + to.y = ty; + + this.to = to; + this.bbox.relocate(to); // BoundingBox.relocate() is in-place + + this.blockers = + this.pathmap.get(this.bbox) + .remove(this.thing) + .sort(tr.compare); + + // this.blocker = this.blockers.filter(this.checkBlocker, this).shift(); + this.blockers.filter(this.checkBlocker, this); + + if (this.blocker) { + + // Move bounding box to furthest non-overlapping point + this.to = this.rewind(this.blocker); + this.bbox.relocate(this.to); + + // Calculate how much time we actually used + var consumed = tr.timeToMove(to.x-or.x, to.y-or.y); + this.consumeTime(consumed); + // console.log('stepTo('+dt+') --> wanted '+oto+' but BLOCKED! by '+this.blocker+', so ending at '+to+', consuming '+consumed); + + // Fire events + this.collide(this.blocker); + + } else { + this.consumeTime(dt); + // console.log('stepTo('+t+') --> '+to+' consuming '+t); + } + }, + + /** + * Filters found obstructions to keep only those not ignored and blocking. + */ + checkBlocker : function checkBlocker(blocker){ + var blocking = blocker.blocking + , density = blocker.density; + + // All blockers after the first are ignored + if ( this.isBlocked || this.ignore.has(blocker) + // Skip passable objects + || (blocking === BoundsType.PASSABLE) + + // Ask irregular objects if we hit + || (blocking === BoundsType.IRREGULAR && !blocker.testCollide(this.thing,this.to,this.bbox)) + + // Filter out Sparse objects if bullet + || (density === DensityType.SPARSE && this.thing.isProjectile) ) + { + return false; + } + + // Only fire collision with this zone once per traversal + // XXX: Zone will have to manage enterance/exit and provide a method for testing, hm -- this would obviate the main need for this.ignore + if ( blocking === BoundsType.ZONE ) { + this.ignore.push(blocker); + this.collide(blocker); + return false; + } + + this.isBlocked = true; + this.blocker = blocker; + return true; + }, + + collide : function collide(blocker){ + var thing = this.thing; + thing.fire('collide', blocker, { 'traversal':this, 'unit':blocker }); + blocker.fire('collide', thing, { 'traversal':this, 'unit':thing }); + }, + + rewind : function rewind(blocker){ + var tr = this.trajectory + , bb = this.bbox, st = this.start + , B = blocker.bbox + , to = this.to ; + + // Figure out which boundary of the blocker we crossed and calculate + // the furthest non-overlapping point. + if (st.x2 <= B.x1 && bb.x2 >= B.x1) { + this.side = B.leftSide; + to = tr.pointAtX(B.x1 - 1 - bb.originRight); + + } else if (st.x1 >= B.x2 && bb.x1 <= B.x2) { + this.side = B.rightSide; + to = tr.pointAtX(B.x2 + 1 + bb.originLeft); + + } else if (st.y2 <= B.y1 && bb.y2 >= B.y1) { + this.side = B.topSide; + to = tr.pointAtY(B.y1 - 1 - bb.originTop); + + } else if (st.y1 >= B.y2 && bb.y1 <= B.y2) { + this.side = B.bottomSide; + to = tr.pointAtY(B.y2 + 1 + bb.originBottom ); + + } else { + console.error('Null reflection line!', 'to:', to, 'blocker:', blocker); + // throw new Error('Null reflection line! to:'+to+', blocker:'+blocker); + } + + return to; + }, + + consumeTime : function consumeTime(t){ + this.trajectory.tCurrent += t; + this.remaining = Math.max(0, this.remaining-t); + return this; + } + +}) diff --git a/src/tanks/map/pathmap.cjs b/src/tanks/map/pathmap.cjs deleted file mode 100644 index 3a09ae2..0000000 --- a/src/tanks/map/pathmap.cjs +++ /dev/null @@ -1,374 +0,0 @@ -// -*- mode: JavaScript; tab-width: 4; indent-tabs-mode: nil; -*- -var Y = require('Y').Y - -, math = require('ezl/math') -, vec = require('ezl/math/vec') -, QuadTree = require('ezl/util/tree/quadtree').QuadTree -, BinaryHeap = require('ezl/util/tree/binaryheap').BinaryHeap -, Vec = vec.Vec -, Line = math.Line - -, config = require('tanks/config').config -, PathingType = require('tanks/constants').PathingType -, Map = require('tanks/map/map').Map -, Wall = require('tanks/thing/wall').Wall - - -, SQRT_TWO = Math.sqrt(2) -, - -/** - * A QuadTree which aids in pathing for AI. - * - QuadTree methods which took rect coords (x1,y1, x2,y2) will accept - * anything Rect-like (having properties x1,y1, x2,y2). - * - Pathing methods will account for the unit's size when calculating paths, - * intersections, blocking, and similar tasks. - */ -PathMap = -exports['PathMap'] = -Map.subclass('PathMap', { - // Config - gridSquare : null, - gridSquareMid : null, - - // State - _squares : null, // Cache of Square objects - _pathId : -1, // Instance of A* being run, to track when to reset Square instances - - - - - init : function init(x1,y1, x2,y2, capacity, buffer_size) { - this.buffer_size = buffer_size; - x1 -= buffer_size; y1 -= buffer_size; - x2 += buffer_size; y2 += buffer_size; - QuadTree.init.call(this, x1,y1, x2,y2, capacity); - this._squares = {}; - - this.innerWalls = Y([]); - this.allWalls = Y([]); - - // this.game.addEventListener('ready', this.setup.bind(this, x1,y1, x2,y2)); - }, - - addBlocker : function addBlocker(obj){ - Map.fn.addBlocker.call(this, obj); - if (obj instanceof Wall) { - if ( !this.allWalls.has(obj) ) - this.allWalls.push(obj); - if ( !(obj.isBoundary || this.innerWalls.has(obj)) ) - this.innerWalls.push(obj); - } - return obj; - }, - - removeBlocker : function removeBlocker(obj){ - Map.fn.removeBlocker.call(this, obj); - if (obj instanceof Wall) { - this.innerWalls.remove(obj); - this.allWalls.remove(obj); - } - return obj; - }, - - wallObstructs : function wallObstructs(line){ - return this.innerWalls.some(function(wall){ - return wall.bbox.intersects(line); - }, this); - }, - - /** - * Takes normal (not gridSquare-relative) coordinates. - */ - _getSquare : function getSquare(x,y){ - if (x instanceof Array) { y=x[1]; x=x[0]; } - var cache = this._squares - , SIZE = this.gridSquare ; - - x = Math.floor(x / SIZE) * SIZE; - y = Math.floor(y / SIZE) * SIZE; - var key = x+'_'+y - , sq = cache[key] ; - - if (sq) - sq.reset(); - else - sq = cache[key] = new Square(this, x,y); - - return sq; - }, - - /** - * Finds the shortest path to a destination for the given agent. - * @param {Thing} agent Agent whose path is being generated. - * @param {Vec} end End destination. - */ - /** - * Finds the shortest path to a destination for the given agent. - * @param {Thing} agent Agent whose path is being generated. - * @param {Integer} x End destination x-coordinate. - * @param {Integer} y End destination y-coordinate. - */ - // * @param {Function} [passable] Function which tests whether a node is passable. - path : function path(agent, x,y){ - ++this._pathId; // Current pathId for Square calculation - this._agent = agent; // Current agent for Square calculation - - var heuristic = vec.manhattan - , start = this._getSquare(agent.loc) - , end = this._getSquare(x,y) - , open = new BinaryHeap('dist') // 'dist' is the scoring key on heap objects - - , iterations = 0 - ; - - open.push(start); - while (open.length) { - iterations++; - - // 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 = Y([]) - , mid = this.gridSquareMid - , node = current; - while( node.prev ) { - path.push( vec.sum(node,mid) ); - node = node.prev; - } - - if (!path.length) - return path; - - path = Y(path.reverse()); - - // Ensure we don't get stuck on a corner on our first step - var first = path.first() - , bb = agent.bbox.relocated( vec.lerp(0.5, agent.loc,first) ) - , blockers = this.getBlockers(bb, [agent]); - - if (blockers.length) - path.unshift( vec.sum(start,mid) ); - - return path; - } - - // Normal case -- move current from open to closed, process each of its neighbors - current.closed = true; - - var neighbors = current.getNeighbors(); - for (var i=0, il = neighbors.length; i < il; i++) { - var n = neighbors[i] - , node = n[0] - , weight = n[1] - ; - - // Move on if we've done this already, or if it's blocker (ignoring start) - if ( node.closed || (node.blocked && node !== start && node !== end) ) - continue; - - // startDist 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. - // --> In fact! Weight is determined by Square.getNeighbors() - var startDist = current.startDist + weight; - - if ( !node.visited || startDist < node.startDist ) { - - // Found an optimal (so far) path to this node. Take score for node to see how good it is. - node.prev = current; - node.endDist = node.endDist || heuristic(node, end); - node.startDist = startDist; - node.dist = startDist + node.endDist; - - // New? Add to the set of nodes to process - if ( !node.visited ) { - node.visited = true; - open.push(node); - - // Seen, but since it has been rescored we need to reorder it in the heap - } else - open.rescore(node); - } - var _node = node; - } - } - - // No result was found -- empty array signifies failure to find path - return Y([]); - }, - - vec2Square : function vec2Square(x,y){ - if (x instanceof Array){ y = x.y; x = x.x; } - var floor = Math.floor, SIZE = this.gridSquare; - return new 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.gridSquare; - return new Vec(floor(x)*SIZE, floor(y)*SIZE); - } - -}) -, - - -// Would be nice if I could make this an inner class (for once) -Square = -exports['Square'] = -Y.subclass('Square', new Vec(0,0), { - pathId : null, // instance of A* being run, to track when to reset - - // resetable: - dist : 0, // estimated distance - startDist : 0, // distance from start - endDist : 0, // estimated distance to end - visited : false, - closed : false, - prev : null, - - - /** - * @param x Left coord of square. - * @param y Top coord of square. - */ - init : function initSquare(pathmap, x,y){ - Vec.init.call(this, x,y); - this.pathmap = pathmap; - this.reset(); - }, - - reset : function reset(){ - var pathId = this.pathmap._pathId; - if (this.pathId === pathId) - return this; - - this.pathId = pathId; - this.blocked = this._blocked(); - - this.dist = 0; - this.startDist = 0; - this.endDist = 0; - this.visited = false; - this.closed = false; - this.prev = null; - - return this; - }, - - /** - * @private Calculates this.blocked value. Should only be called by reset() to cache the result. - */ - _blocked : function blocked(){ - var pm = this.pathmap - , agent = pm._agent - // , bb = agent.bbox - - // , origin = bb.relOrigin - // , left = origin.x, right = bb.width - left - // , top = origin.y, bottom = bb.height - top - - , SIZE = pm.gridSquare - , x = Math.floor(this.x) - , y = Math.floor(this.y) - // , x1 = x - left, x2 = x + SIZE + right - // , y1 = y - top, y2 = y + SIZE + bottom - , x1 = x, x2 = x + SIZE - , y1 = y, y2 = y + SIZE - - , blockers = pm.get(x1,y1, x2,y2) - .remove(agent) - .filter(this._filterBlocked, this) - ; - - return !!blockers.length; - }, - - /** - * @private - */ - _filterBlocked : function filterBlocked(v, r){ - return v.blocking === PathingType.BLOCKING && !v.isBoundary; - }, - - getNeighbors : function getNeighbors(){ - var neighbors = [] - , abs = Math.abs - , pm = this.pathmap - , agent = pm._agent - , SIZE = pm.gridSquare - - , x = this.x, y = this.y - , sq, cost, ix, iy, x1,y1, x2,y2 - ; - - for (ix=-1; ix<2; ix++) { - for (iy=-1; iy<2; iy++) { - // Skip self - if (ix === 0 && iy === 0) - continue; - - cost = 1; - x1 = x + ix*SIZE; y1 = y + iy*SIZE; - x2 = x1+SIZE; y2 = y1+SIZE; - - // filter squares outside bounds - if ( pm.x1 >= x1 || pm.y1 >= y1 || - pm.x2 <= x2 || pm.y2 <= y2 ) - continue; - - // Diagonal square - if ( abs(ix) === 1 && abs(iy) === 1 ) { - cost = SQRT_TWO; - - // Ensure we don't cut a blocked corner - if ( pm._getSquare(x1,y).blocked || pm._getSquare(x,y1).blocked ) - continue; - } - - sq = pm._getSquare(x1,y1); - neighbors.push([ sq, cost*SIZE ]); - } - } - - return neighbors; - }, - - toString : function(){ - var props = []; - if (this.blocked) props.push('blocked'); - if (this.visited) props.push('dist='+this.dist); - if (this.closed) props.push('closed'); - props = props.join(', '); - if (props) props = '('+props+')'; - return '['+this.x+' '+this.y+']'+props; - } - -}) -; - - - -// Wrap QuadTree Methods to accept Rect-like objects -'overlaps get set removeAll leaves collect' - .split(' ') - .forEach(function(name){ - var method = PathMap.fn[name] || QuadTree.fn[name]; - PathMap.fn[name] = function pmHandleRect(o){ - var args = arguments, xy; - if (o.x1 !== undefined){ - xy = [o.x1,o.y1, o.x2,o.y2]; - args = (args.length === 1) ? xy : xy.concat(Y(args,1)); - } - return method.apply(this, args); - }; - }); - -// Stay sync'd with config -config.updateOnChange( - ['pathing.gridSquare', 'pathing.gridSquareMid'], - PathMap.fn); - diff --git a/src/tanks/map/trajectory.cjs b/src/tanks/map/trajectory.cjs deleted file mode 100644 index 9653053..0000000 --- a/src/tanks/map/trajectory.cjs +++ /dev/null @@ -1,172 +0,0 @@ -var Y = require('Y').Y -, op = require('Y/op') -, math = require('ezl/math') -, Vec = math.Vec -, Line = math.Line -, Rect = math.Rect -, BoundingBox = require('ezl/loc').BoundingBox -, Thing = require('tanks/thing/thing').Thing - -, BOUND_SIZE_RATIO = 0.75 -, - - -Trajectory = -exports['Trajectory'] = -Line.subclass('Trajectory', { - tCurrent : 0, - - - init : function initTrajectory(owner, x1,y1, x2,y2, tdist, tCurrent){ - Y.bindAll(this, 'compare', 'closer', 'intersects'); - - this.owner = owner; - this.game = owner.game; - this.pathmap = this.game.pathmap; - - this.reset(x1,y1, x2,y2, tdist, tCurrent); - }, - - reset : function reset(x1,y1, x2,y2, tdist, tCurrent){ - if (x1 instanceof Array && y1 instanceof Array) { - tdist = x2; - y2 = y1[1]; x2 = y1[0]; - y1 = x1[1]; x1 = x1[0]; - } - - // init with raw numbers to do calculations - Line.init.call(this, x1,y1, x2,y2, tdist || this.tdist); - - // Find appropriate edge in direction of line - var pm = this.pathmap - , ex = (x1 > x2 ? -REF_SIZE : REF_SIZE+pm.width ) - , ey = (y1 > y2 ? -REF_SIZE : REF_SIZE+pm.height) - , edge = this.near(ex,ey) - ; - - // Move goal point beyond far wall to avoid rotations post-reflection - x2 = this.x2 = edge.x; - y2 = this.y2 = edge.y; - this.p2 = new Vec(x2,y2); - Vec.init.call(this, x2-x1, y2-y1); - - this.tCurrent = tCurrent || 0; - this.resetBound(); - return this; - }, - - // Determine how much time can pass before we risk teleporting - // We'll need to reset this whenever the bounding box changes size - resetBound : function resetBound(){ - this.tBound = - BOUND_SIZE_RATIO * Math.min( - Math.abs(this.owner.width / this.pa), - Math.abs(this.owner.height / this.pb) ); - return this; - }, - - clone : function clone(){ - return new Trajectory(this.owner, this.x1,this.y1, this.x2,this.y2, this.tdist, this.tCurrent); - }, - - - intersects : function intersects(x,y){ - var o = x; - if (o instanceof Thing) - return o.bbox.intersects(this); - - if (o instanceof Rect) - return o.intersects(this); - - return Line.fn.intersects.call(this, x,y); - }, - - - /** - * Compares how distant in the future two objects are on this trajectory. - * Objects that have been passed are always further away than those in the future, - * but otherwise the comparison is performed by absolute distance. - * @return -1 if a closer b, 1 if a further b, 0 if a same as b - */ - compare : function compare(a, b){ - if (a instanceof Thing) a = a.loc; - if (b instanceof Thing) b = b.loc; - - var abs = Math.abs - , t = this.tCurrent - - , xa = this.calcX(a.y), ya = this.calcY(a.x) - , ta = this.iparametric(xa,ya) - t - - , xb = this.calcX(b.y), yb = this.calcY(b.x) - , tb = this.iparametric(xb,yb) - t - ; - - // If one has passed, return the other - if ( ta < 0 && tb >= 0 ) - return 1; - if ( tb < 0 && ta >= 0 ) - return -1; - - return op.cmp(abs(ta), abs(tb)); - }, - - closer : function closer(o1, o2){ - return this.compare(o1,o2) === 1 ? o2 : o1; - }, - - sortClosest : function sortClosest(o1, o2){ - var things; - if (o1 instanceof Y.YArray) - things = o1; - else if (o1 instanceof Array) - things = Y(o1); - else - things = new Y(arguments); - - return things.sort( this.compare ); - }, - - closest : function closest(o1, o2){ - return this.sortClosest.apply(this, arguments).shift(); - }, - - comesWithin : function comesWithin(pt, w,h){ - if ( !this.owner.loc ) - return false; - - if (pt instanceof Thing) pt = pt.loc; - - if ( w === undefined ){ - w = 0; h = 0; - } else if ( Y.isNumber(w.width) ){ - h = w.height; w = w.width; - } - - var cur = this.owner.loc - , fx = this.calcX(pt.y), fy = this.calcY(pt.x) - , t = this.iparametric(cur.x, cur.y) - , ft = this.iparametric(fx,fy) - , dw = Math.abs(fx - pt.x), dh = Math.abs(fy - pt.y) - ; - return ( t <= ft && (dw <= w || dh <= h) ); - }, - - pathBlocked : function pathBlocked(obj, ignore){ - var blockers = - this.pathmap.innerWalls - .concat( this.game.units ) - .apply('remove', [this.owner].concat(ignore || []) ) - .filter( this.intersects ) - .sort( this.compare ) - - , blocker = blockers.shift() - ; - - return (blocker === obj ? false : blocker); - }, - - toString : function(){ - return 'T['+this.p1+', '+this.p2+', slope='+this.slope.toFixed(3)+']'; - } -}); diff --git a/src/tanks/map/traversal.cjs b/src/tanks/map/traversal.cjs deleted file mode 100644 index 9d9237b..0000000 --- a/src/tanks/map/traversal.cjs +++ /dev/null @@ -1,157 +0,0 @@ -var Y = require('Y').Y -, PathingType = require('tanks/constants').PathingType -, - -Traversal = -exports['Traversal'] = -Y.subclass('Traversal', { - ignore : null, // objects to ignore when determining blockers - - isBlocked : false, - to : null, // furthest point reached - remaining : 0, // time left unconsumed due to blocker - blocker : null, // blocking object - side : null, // collision side of blocker (Line) - - - - init : function initTraversal(thing, trajectory, ignore){ - this.ignore = Y(ignore || []); - - this.thing = thing; - this.game = thing.game; - this.pathmap = thing.game.pathmap; - this.bbox = thing.bbox.clone(); - - this.trajectory = trajectory || thing.trajectory; - }, - - traverse : function traverse(t, tx,ty){ - var tr = this.trajectory; - this.remaining = t; - - do { - this.step(tx,ty); - } while (!this.isBlocked && this.remaining > 0); - - return this.to; - }, - - /** - * Checks for blockers and moves traversal bounds forward as much as possible. - */ - step : function step(tx,ty){ - this.start = this.bbox.clone(); - var tr = this.trajectory - , or = this.thing.loc - , dt = Math.min(tr.tBound, this.remaining) - , to = tr.parametric(tr.tCurrent+dt) // calc how far down the tr we go - ; - - // Don't overshoot the target - if ( tx !== undefined && Math.abs(or.x-to.x) > Math.abs(or.x-tx) ) - to.x = tx; - - if ( ty !== undefined && Math.abs(or.y-to.y) > Math.abs(or.y-ty) ) - to.y = ty; - - this.to = to; - this.bbox.relocate(to); // BoundingBox.relocate() is in-place - - this.blockers = - this.pathmap.get(this.bbox) - .remove(this.thing) - .sort(tr.compare); - - // this.blocker = this.blockers.filter(this.checkBlocker, this).shift(); - this.blockers.filter(this.checkBlocker, this); - - if (this.blocker) { - - // Move bounding box to furthest non-overlapping point - this.to = this.rewind(this.blocker); - this.bbox.relocate(this.to); - - // Calculate how much time we actually used - var consumed = tr.timeToMove(to.x-or.x, to.y-or.y); - this.consumeTime(consumed); - // console.log('stepTo('+dt+') --> wanted '+oto+' but BLOCKED! by '+this.blocker+', so ending at '+to+', consuming '+consumed); - - // Fire events - this.collide(this.blocker); - - } else { - this.consumeTime(dt); - // console.log('stepTo('+t+') --> '+to+' consuming '+t); - } - }, - - /** - * Filters found obstructions to keep only those not ignored and blocking. - */ - checkBlocker : function checkBlocker(blocker){ - var blocking = blocker.blocking; - - // All blockers after the first are ignored - if ( this.ignore.has(blocker) || blocking === PathingType.PASSABLE || this.isBlocked ) - return false; - - // Only fire collision with this zone once per traversal - // XXX: Zone will have to manage enterance/exit and provide a method for testing, hm -- this would obviate the main need for this.ignore - if ( blocking === PathingType.ZONE ) { - this.ignore.push(blocker); - this.collide(blocker); - return false; - } - - this.isBlocked = true; - this.blocker = blocker; - return true; - }, - - collide : function collide(blocker){ - var thing = this.thing; - - thing.fire('collide', blocker, { 'traversal':this, 'unit':blocker }); - blocker.fire('collide', thing, { 'traversal':this, 'unit':thing }); - }, - - rewind : function rewind(blocker){ - var tr = this.trajectory - , bb = this.bbox, st = this.start - , B = blocker.bbox - , to = this.to ; - - // Figure out which boundary of the blocker we crossed and calculate - // the furthest non-overlapping point. - if (st.x2 <= B.x1 && bb.x2 >= B.x1) { - this.side = B.leftSide; - to = tr.pointAtX(B.x1 - 1 - bb.originRight); - - } else if (st.x1 >= B.x2 && bb.x1 <= B.x2) { - this.side = B.rightSide; - to = tr.pointAtX(B.x2 + 1 + bb.originLeft); - - } else if (st.y2 <= B.y1 && bb.y2 >= B.y1) { - this.side = B.topSide; - to = tr.pointAtY(B.y1 - 1 - bb.originTop); - - } else if (st.y1 >= B.y2 && bb.y1 <= B.y2) { - this.side = B.bottomSide; - to = tr.pointAtY(B.y2 + 1 + bb.originBottom ); - - } else { - console.error('Null reflection line!', 'to:', to, 'blocker:', blocker); - // throw new Error('Null reflection line! to:'+to+', blocker:'+blocker); - } - - return to; - }, - - consumeTime : function consumeTime(t){ - this.trajectory.tCurrent += t; - this.remaining = Math.max(0, this.remaining-t); - return this; - } - -}) diff --git a/src/tanks/map/wall.cjs b/src/tanks/map/wall.cjs new file mode 100644 index 0000000..a814c75 --- /dev/null +++ b/src/tanks/map/wall.cjs @@ -0,0 +1,106 @@ +var Y = require('Y').Y +, op = require('Y/op') + +, Rect = require('ezl/shape').Rect + +, Thing = require('tanks/thing/thing').Thing +, constants = require('tanks/constants') +, BoundsType = constants.BoundsType +, DensityType = constants.DensityType +, + + +Wall = +exports['Wall'] = +Thing.subclass('Wall', { + align : 0, // 0 reserved for neutral units + + originX : 0, + originY : 0, + + width : 0, + height : 0, + + + blocking : BoundsType.BLOCKING, + density : DensityType.DENSE, + isWall : true, + isReflective : true, + + // rendering + + isRenderable : true, + fillStyle : 'rgba(255,255,255, 0.25)', + strokeStyle : 'transparent', + lineWidth : 0, + + renderWidth : 0, + renderHeight : 0, + renderOffsetX : 0, + renderOffsetY : 0, + + + // inactive + active : false, + createCooldowns : op.nop, + + // indestructable + dealDamage : op.nop, + + stats : { + hp : Infinity, + move : 0, + power : 0, + speed : 0, + shots : 0 + }, + + // Instance + + isBoundary : false, + + + + init : function initWall(x,y, w,h, isBoundary){ + this.width = w; + this.height = h; + this.isBoundary = !!isBoundary; + if (isBoundary) + this.density = DensityType.BOUNDARY; + Thing.init.call(this); + this.position(x,y); + }, + + + + render : function render(parent){ + if (this.isBoundary) + return this; + + if (this.shape) + this.shape.remove(); + + this.shape = + new Rect( this.renderWidth || this.width, this.renderHeight || this.height ) + .position(this.loc.x + this.renderOffsetX, this.loc.y + this.renderOffsetY) + .fill(this.fillStyle) + .stroke(this.strokeStyle, this.lineWidth) + .appendTo( parent ); + + return this; + }, + + toString : function(){ + var bb = this.bbox + , x1,y1, x2,y2; + if (bb){ + x1 = bb.x1; y1 = bb.y1; + x2 = bb.x2; y2 = bb.y2; + } else { + x1=y1=x2=y2=NaN; + } + return this.className+'['+x1+','+y1+', '+x2+','+y2+'](w='+this.width+', h='+this.height+')'; + } +}); + +Wall.register('wall', Wall); diff --git a/src/tanks/mixins/quantified.cjs b/src/tanks/mixins/quantified.cjs index 05f2e4a..0889a07 100644 --- a/src/tanks/mixins/quantified.cjs +++ b/src/tanks/mixins/quantified.cjs @@ -53,14 +53,14 @@ Mixin.subclass('Quantified', { onBuffAcquired : function onBuffAcquired(evt){ var buff = evt.data.buff; this.buffs.push(buff); - console.log('Buff '+buff+' acquired by '+this); + // console.log('Buff '+buff+' acquired by '+this); // TODO: update UI }, onBuffLost : function onBuffLost(evt){ var buff = evt.data.buff; this.buffs.remove(buff); - console.log('Buff '+buff+' lost by '+this); + // console.log('Buff '+buff+' lost by '+this); // TODO: update UI }, diff --git a/src/tanks/thing/bullet.cjs b/src/tanks/thing/bullet.cjs index 41be7dc..1a48969 100644 --- a/src/tanks/thing/bullet.cjs +++ b/src/tanks/thing/bullet.cjs @@ -8,14 +8,12 @@ var Y = require('Y').Y , Circle = shape.Circle , config = require('tanks/config').config -, PathingType = require('tanks/constants').PathingType +, BoundsType = require('tanks/constants').BoundsType , stat = require('tanks/effects/stat') - -, Thing = require('tanks/thing/thing').Thing -, Wall = require('tanks/thing/wall').Wall -, Trajectory = require('tanks/map/trajectory').Trajectory -, Traversal = require('tanks/map/traversal').Traversal , Explosion = require('tanks/fx/explosion').Explosion +, Thing = require('tanks/thing/thing').Thing +, Trajectory = require('tanks/map/pathing/trajectory').Trajectory +, Traversal = require('tanks/map/pathing/traversal').Traversal , @@ -33,8 +31,9 @@ Thing.subclass('Bullet', { }, // Instance - blocking : PathingType.BLOCKING, + blocking : BoundsType.BLOCKING, isRenderable : true, + isProjectile : true, bounces : 0, bounceLimit : 1, @@ -121,7 +120,7 @@ Thing.subclass('Bullet', { ; // Ignore collisions with zones - if (unit.blocking === PathingType.ZONE) + if (unit.blocking === BoundsType.ZONE) return; // Reflection! diff --git a/src/tanks/thing/fence.cjs b/src/tanks/thing/fence.cjs deleted file mode 100644 index b08b7b0..0000000 --- a/src/tanks/thing/fence.cjs +++ /dev/null @@ -1,80 +0,0 @@ -var Y = require('Y').Y -, op = require('Y/op') -, Rect = require('ezl/shape').Rect -, Wall = require('tanks/thing/wall').Wall -, - - -Fence = -exports['Fence'] = -Wall.subclass('Fence', { - align : 0, // 0 reserved for neutral units - - originX : 0, - originY : 0, - - isReflective : true, - isRenderable : true, - - // inactive - active : false, - createCooldowns : op.nop, - - // indestructable - dealDamage : op.nop, - - stats : { - hp : Infinity, - move : 0, - power : 0, - speed : 0, - shots : 0 - }, - - // Instance - - isBoundary : false, - - - - init : function initFence(x,y, w,h){ - this.width = w; - this.height = h; - this.isBoundary = !!isBoundary; - Wall.init.call(this, x,y, w,h); - this.position(x,y); - }, - - - - render : function render(parent){ - if (this.isBoundary) - return this; - - if (this.shape) - this.shape.remove(); - - this.shape = - new Rect(this.width, this.height) - .position(this.loc.x, this.loc.y) - .fill('rgba(0,0,0, 0.25)') - .stroke('transparent', 0) - .appendTo( parent ); - - return this; - }, - - toString : function(){ - var bb = this.bbox - , x1,y1, x2,y2; - if (bb){ - x1 = bb.x1; y1 = bb.y1; - x2 = bb.x2; y2 = bb.y2; - } else { - x1=y1=x2=y2=NaN; - } - return this.className+'['+x1+','+y1+', '+x2+','+y2+'](w='+this.width+', h='+this.height+')'; - } -}); - -Wall.register('fence', Fence); diff --git a/src/tanks/thing/index.cjs b/src/tanks/thing/index.cjs index d36e903..d1a1bbb 100644 --- a/src/tanks/thing/index.cjs +++ b/src/tanks/thing/index.cjs @@ -3,5 +3,3 @@ exports['Bullet'] = require('tanks/thing/bullet').Bullet; exports['Tank'] = require('tanks/thing/tank').Tank; exports['Player'] = require('tanks/thing/player').Player; exports['Item'] = require('tanks/thing/item').Item; -exports['Wall'] = require('tanks/thing/wall').Wall; -exports['Fence'] = require('tanks/thing/fence').Fence; diff --git a/src/tanks/thing/item.cjs b/src/tanks/thing/item.cjs index 76dd7cc..6457a05 100644 --- a/src/tanks/thing/item.cjs +++ b/src/tanks/thing/item.cjs @@ -4,7 +4,7 @@ var Y = require('Y').Y , shape = require('ezl/shape') , Rect = shape.Rect -, PathingType = require('tanks/constants').PathingType +, BoundsType = require('tanks/constants').BoundsType , Buff = require('tanks/effects/buff').Buff , Thing = require('tanks/thing/thing').Thing @@ -20,7 +20,7 @@ Thing.subclass('Item', { align : 0, // 0 reserved for neutral units - blocking : PathingType.ZONE, + blocking : BoundsType.ZONE, width : ITEM_SIZE, height : ITEM_SIZE, @@ -91,7 +91,7 @@ Thing.subclass('Item', { onAcquired : function onAcquired(evt){ this.owner = evt.data.unit; - console.log(this.owner+' acquired '+this+' ('+this.desc+')!'); + // console.log(this.owner+' acquired '+this+' ('+this.desc+')!'); this.currentBuffs = this.itemBuffs.invoke('instantiate', this.owner); this.destroy(); // removes map object }, @@ -101,7 +101,7 @@ Thing.subclass('Item', { if ( this.currentBuffs ) this.currentBuffs.invoke('die', 'item.lost'); var unit = this.owner; - console.log(unit+' lost '+this+'!'); + // console.log(unit+' lost '+this+'!'); this.owner = null; // TODO: game to listen, re-add to level at unit.loc }, diff --git a/src/tanks/thing/tank.cjs b/src/tanks/thing/tank.cjs index 7dabe64..4c318d5 100644 --- a/src/tanks/thing/tank.cjs +++ b/src/tanks/thing/tank.cjs @@ -1,5 +1,5 @@ -var Y = require('Y').Y -, op = require('Y/op') +var Y = require('Y').Y +, op = require('Y/op') , vec = require('ezl/math/vec') , shape = require('ezl/shape') @@ -10,13 +10,16 @@ var Y = require('Y').Y , Rect = shape.Rect , Circle = shape.Circle -, Thing = require('tanks/thing/thing').Thing -, Bullet = require('tanks/thing/bullet').Bullet -, Trajectory = require('tanks/map/trajectory').Trajectory -, Traversal = require('tanks/map/traversal').Traversal +, constants = require('tanks/constants') +, BoundsType = constants.BoundsType +, DensityType = constants.DensityType +, Trajectory = require('tanks/map/pathing/trajectory').Trajectory +, Traversal = require('tanks/map/pathing/traversal').Traversal +, Thing = require('tanks/thing/thing').Thing +, Bullet = require('tanks/thing/bullet').Bullet , isBullet = Y.is(Bullet) -, BULLET_MOVE_PER_FRAME = MS_PER_FRAME * Bullet.prototype.stats.move*REF_SIZE/1000 +, BULLET_MOVE_PER_FRAME = MS_PER_FRAME * Bullet.fn.stats.move*REF_SIZE/1000 , _X = 0, _Y = 1 , @@ -24,7 +27,6 @@ var Y = require('Y').Y Tank = exports['Tank'] = Thing.subclass('Tank', function(Tank){ - // TODO: lookup projectile on Bullet Y.core.descriptors(this, { colors : { @@ -146,6 +148,65 @@ Thing.subclass('Tank', function(Tank){ /** + * Fires this agent's cannon. If a target location is omitted, the shot + * will be fired in the direction of the tank's current barrel rotation. + * + * @param {Number} [x] Target X coordinate. + * @param {Number} [y] Target Y coordinate. + */ + this['shoot'] = + function shoot(x,y){ + if ( this.nShots >= this.stats.shots.val || !this.cooldowns.attack.ready ) + return null; + + var xydef = (x !== undefined && y !== undefined); + if (xydef) + this.rotateBarrel(x,y); + + // Additional space on each side which must be clear around the + // shot to ensure we don't shoot ourself in the foot (literally) + var WIGGLE = 1 + , Projectile = this.projectile + , pw2 = Projectile.fn.width/2, ph2 = Projectile.fn.height/2 + + , tloc = this.getTurretLoc() + , tx = tloc.x, ty = tloc.y + + , x1 = tx - pw2 - WIGGLE, y1 = ty - ph2 - WIGGLE + , x2 = tx + pw2 + WIGGLE, y2 = ty + ph2 + WIGGLE + , blockers = this.game.pathmap.get(x1,y1, x2,y2).filter(filterShoot, this) + ; + + if ( blockers.size() ) + return null; // console.log('squelch!', blockers); + + if (!xydef) { + var theta = this.barrel.transform.rotate + , sin = Math.sin(theta), cos = Math.cos(theta); + x = tx + REF_SIZE*cos; + y = ty + REF_SIZE*sin; + } + + this.cooldowns.attack.activate(this.now); + this.nShots++; + + var p = new Projectile(this, tx,ty, x,y); + p.addEventListener('destroy', this.onBulletDeath); + return p; + }; + + function filterShoot(v){ + return (v !== this) + && (v.density === DensityType.BOUNDARY || (v.isWall && v.density === DensityType.DENSE)); + } + + this['ableToShoot'] = + function ableToShoot(){ + return this.nShots < this.stats.shots.val && this.cooldowns.attack.ready; + }; + + + /** * @return {this} */ this['move'] = @@ -285,7 +346,6 @@ Thing.subclass('Tank', function(Tank){ var tank = this, bb = this.bbox , w = bb.width+wiggle, h = bb.height+wiggle ; - // , w = (bb.width+wiggle)/2, h = (bb.height+wiggle)/2 ; return bullets.filter(function(b){ var trj = b.trajectory; @@ -295,61 +355,6 @@ Thing.subclass('Tank', function(Tank){ }); }; - /** - * Fires this agent's cannon. If a target location is omitted, the shot - * will be fired in the direction of the tank's current barrel rotation. - * - * @param {Number} [x] Target X coordinate. - * @param {Number} [y] Target Y coordinate. - */ - this['shoot'] = - function shoot(x,y){ - if ( this.nShots >= this.stats.shots.val || !this.cooldowns.attack.ready ) - return null; - - var xydef = (x !== undefined && y !== undefined); - if (xydef) - this.rotateBarrel(x,y); - - // Additional space on each side which must be clear around the - // shot to ensure we don't shoot ourself in the foot (literally) - var WIGGLE = 1 - , Projectile = this.projectile - , pw2 = Projectile.fn.width/2, ph2 = Projectile.fn.height/2 - - , tloc = this.getTurretLoc() - , tx = tloc.x, ty = tloc.y - - , x1 = tx - pw2 - WIGGLE, y1 = ty - ph2 - WIGGLE - , x2 = tx + pw2 + WIGGLE, y2 = ty + ph2 + WIGGLE - , blockers = this.game.pathmap.get(x1,y1, x2,y2).remove(this) - ; - - if ( blockers.size() ) - return null; // console.log('squelch!', blockers); - - if (!xydef) { - var theta = this.barrel.transform.rotate - , sin = Math.sin(theta), cos = Math.cos(theta); - x = tx + REF_SIZE*cos; - y = ty + REF_SIZE*sin; - } - - this.cooldowns.attack.activate(this.now); - this.nShots++; - - var p = new Projectile(this, tx,ty, x,y); - p.addEventListener('destroy', this.onBulletDeath); - // this.game.addThing(p).render(this.game.level); - return p; - }; - - - this['ableToShoot'] = - function ableToShoot(){ - return this.nShots < this.stats.shots.val && this.cooldowns.attack.ready; - }; - this['getTurretLoc'] = function getTurretLoc(){ var WIGGLE = 2 @@ -367,7 +372,6 @@ Thing.subclass('Tank', function(Tank){ , x = loc.x + len*cos , y = loc.y + len*sin ; - // console.log('getTurretLoc()', 'loc:', loc, '(x,y):', [x,y]); return new Vec(x,y); }; diff --git a/src/tanks/thing/thing.cjs b/src/tanks/thing/thing.cjs index 9a0ea6f..975e3d7 100644 --- a/src/tanks/thing/thing.cjs +++ b/src/tanks/thing/thing.cjs @@ -9,7 +9,9 @@ var Y = require('Y').Y , Cooldown = require('ezl/loop').Cooldown , config = require('tanks/config').config -, PathingType = require('tanks/constants').PathingType +, constants = require('tanks/constants') +, BoundsType = constants.BoundsType +, DensityType = constants.DensityType , stat = require('tanks/effects/stat') , Quantified = require('tanks/mixins/quantified').Quantified , Speciated = require('tanks/mixins/speciated').Speciated @@ -52,7 +54,9 @@ new evt.Class('Thing', { dirty : true, // Properties - blocking : PathingType.BLOCKING, + blocking : BoundsType.BLOCKING, + density : DensityType.DENSE, + isProjectile : false, isRenderable : false, // Agent will present itself for rendering when ready // FIXME: stupid hack active : true, // Agent takes actions? isReflective : false, // Projectiles bounce off agent rather than explode? diff --git a/src/tanks/thing/wall.cjs b/src/tanks/thing/wall.cjs deleted file mode 100644 index f59831f..0000000 --- a/src/tanks/thing/wall.cjs +++ /dev/null @@ -1,83 +0,0 @@ -var Y = require('Y').Y -, op = require('Y/op') - -, Rect = require('ezl/shape').Rect - -, Thing = require('tanks/thing/thing').Thing -, PathingType = require('tanks/constants').PathingType -, - - -Wall = -exports['Wall'] = -Thing.subclass('Wall', { - align : 0, // 0 reserved for neutral units - - originX : 0, - originY : 0, - - blocking : PathingType.BLOCKING, - isReflective : true, - isRenderable : true, - - // inactive - active : false, - createCooldowns : op.nop, - - // indestructable - dealDamage : op.nop, - - stats : { - hp : Infinity, - move : 0, - power : 0, - speed : 0, - shots : 0 - }, - - // Instance - - isBoundary : false, - - - - init : function initWall(x,y, w,h, isBoundary){ - this.width = w; - this.height = h; - this.isBoundary = !!isBoundary; - Thing.init.call(this); - this.position(x,y); - }, - - - - render : function render(parent){ - if (this.isBoundary) - return this; - - 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(){ - var bb = this.bbox - , x1,y1, x2,y2; - if (bb){ - x1 = bb.x1; y1 = bb.y1; - x2 = bb.x2; y2 = bb.y2; - } else { - x1=y1=x2=y2=NaN; - } - return this.className+'['+x1+','+y1+', '+x2+','+y2+'](w='+this.width+', h='+this.height+')'; - } -}); - -Wall.register('wall', Wall); diff --git a/src/tanks/ui/pathmapui.cjs b/src/tanks/ui/pathmapui.cjs index 9bf2d6d..ff188e2 100644 --- a/src/tanks/ui/pathmapui.cjs +++ b/src/tanks/ui/pathmapui.cjs @@ -5,8 +5,8 @@ var Y = require('Y').Y , vec = require('ezl/math/vec') , config = require('tanks/config').config -, PathingType = require('tanks/constants').PathingType -, PathMap = require('tanks/map/pathmap').PathMap +, BoundsType = require('tanks/constants').BoundsType +, PathMap = require('tanks/map/pathing/pathmap').PathMap , @@ -81,7 +81,7 @@ Rect.subclass('PathMapUI', { return acc; acc[r.id] = r; - if (v.blocking === PathingType.ZONE) { + if (v.blocking === BoundsType.ZONE) { ctx.fillStyle = this.zoneFillStyle; ctx.strokeStyle = this.zoneStrokeStyle; ctx.lineWidth = this.zoneLineWidth; -- 1.7.0.4