--- /dev/null
+var Y = require('Y').Y
+, op = require('Y/op')
+
+, vec = require('ezl/math/vec')
+, Vec = vec.Vec
+, Cooldown = require('ezl/loop').Cooldown
+, CooldownGauge = require('ezl/widget').CooldownGauge
+, shape = require('ezl/shape')
+, Circle = shape.Circle
+
+, constants = require('tanks/constants')
+, BoundsType = constants.BoundsType
+, DensityType = constants.DensityType
+, LinearTrajectory = require('tanks/map/pathing/linear-trajectory').LinearTrajectory
+, Traversal = require('tanks/map/pathing/traversal').Traversal
+, Thing = require('tanks/thing/thing').Thing
+, Bullet = require('tanks/thing/bullet').Bullet
+, Lootable = require('tanks/mixins/lootable').Lootable
+, Barrel = require('tanks/fx/barrel').Barrel
+
+, _X = 0, _Y = 1
+,
+
+
+Tower =
+exports['Tower'] =
+Thing.subclass('Tower', function(Tower){
+
+ Y.core.descriptors(this, {
+ isCombatant : true,
+ lootTable : '',
+
+ colors : {
+ body : '#980011',
+ barrel : '#244792'
+ },
+
+ barrel: {
+ width : 'unit.width * 0.75',
+ height : 'unit.height / 6',
+ originX : 2,
+ originY : '50%',
+ x : '50%',
+ y : '50%'
+ },
+
+ // Bounding box
+ width : REF_SIZE*0.55,
+ height : REF_SIZE*0.55,
+
+ // Attributes
+ stats : {},
+
+ // AI "Cooldowns" (max frequency of each action per sec)
+ ai : {},
+
+ projectile : 'bullet',
+ defaultProjectile : null,
+
+ /// Instance ///
+
+ align : null,
+ buffs : null,
+
+ nShots : 0
+ });
+
+ this['init'] =
+ function initTower(align){
+ Thing.init.call(this, align);
+ this.projectile = this.defaultProjectile = Bullet.lookup(this.projectile);
+ this.colors = Y.extend({}, this.colors);
+ this.atkGauge = new CooldownGauge(this.cooldowns.attack, this.width+1,this.height+1);
+ this.onBulletDeath = this.onBulletDeath.bind(this);
+ };
+
+ Lootable.mixInto(Tower);
+
+ this['onBulletDeath'] =
+ function onBulletDeath(evt){ this.nShots--; };
+
+
+
+ this['act'] =
+ function act(elapsed, now){
+ var ai = this.ai, map = this.game.map;
+ this.elapsed = elapsed;
+ this.now = now;
+
+ // Try to shoot down nearby bullets
+ if (ai.shootIncoming.ready && this.ableToShoot()) {
+ var bs = map.willCollide(this, map.findNearBullets(this, 25) );
+ // console.log('['+TICKS+':'+this.id, this, '] Shoot down bullets?', bs.size() && bs);
+ if ( bs.size() ) {
+ ai.shootIncoming.activate(now);
+ var b = map.closestOf(this, bs);
+ // console.log(' --> Incoming! Shoot it down!', b);
+ this.shoot(b.loc.x, b.loc.y);
+ return this;
+ }
+ }
+
+ // Try to blow up nearby tanks
+ if (ai.shootEnemy.ready && (this.stats.shots.val - this.nShots > 1)) {
+ var t = map.findNearEnemiesInSight(this).shift();
+ // console.log('['+TICKS+':'+this.id, this, '] Shoot at enemies?', t);
+ if (t) {
+ ai.shootEnemy.activate(now);
+ // console.log(' --> I gotcha!', t);
+ this.shoot(t.loc.x, t.loc.y);
+ return this;
+ }
+ }
+
+ this.components.invoke('act', elapsed, now);
+ return this;
+ };
+
+
+ /**
+ * 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;
+
+ if (x instanceof Array) { y = x[_Y]; x = x[_X]; }
+ 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 = 2
+ , 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.map.get(x1,y1, x2,y2).filter(filterShoot, this)
+ ;
+
+ if ( blockers.size() )
+ return null; // console.log('squelch!', blockers);
+
+ if (!xydef) {
+ var theta = this.barrelShape.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.on('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;
+ };
+
+
+
+ this['getTurretLoc'] =
+ function getTurretLoc(){
+ var WIGGLE = 2
+ , loc = this.loc
+ , barrel = this.barrelShape
+
+ , theta = barrel.transform.rotate
+ , sin = Math.sin(theta), cos = Math.cos(theta)
+
+ // sqrt(2)/2 * (P.width + WIGGLE)
+ // is max diagonal to ensure we don't overlap with the firing unit
+ , pw = this.projectile.fn.width
+ , len = barrel.bbox.width + 0.707*(pw+WIGGLE)
+
+ , x = loc.x + len*cos
+ , y = loc.y + len*sin
+ ;
+ return new Vec(x,y);
+ };
+
+
+
+
+ /// Rendering Methods ///
+
+ /**
+ * Sets up unit appearance for minimal updates. Called once at start,
+ * or when the world needs to be redrawn from scratch.
+ */
+ this['render'] =
+ function render(parent){
+ if (this.shape) this.shape.remove();
+
+ var colors = this.colors
+ , w = this.width, w2 = w/2
+ , h = this.height, h2 = h/2
+ , r = w / 2
+ ;
+
+ this.shape =
+ new Circle(r)
+ .appendTo( parent )
+ .position(this.loc.x, this.loc.y)
+ .fill(colors.body)
+ ;
+
+ // TODO: Bleeds outside of circle
+ // if (this.showAttackCooldown)
+ // this.shape.append(this.atkGauge);
+
+ var b = Y.map(this.barrel, calcValue, this);
+
+ this.barrelShape =
+ new Barrel(b.width,b.height, b.originX,b.originY)
+ .appendTo( this.shape ) // have to append early to avoid problems with relative positioning
+ .position(b.x, b.y)
+ // .position(w2-2, h2-bh/2)
+ // .origin(2, bh/2)
+ .fill( colors.barrel ) ;
+
+ return this;
+ };
+
+
+ this['rotateBarrel'] =
+ function rotateBarrel(x,y){
+ this.barrelShape.rotate(this.angleTo(x,y));
+ return this;
+ };
+
+ this['rotateBarrelRelPage'] =
+ function rotateBarrelRelPage(pageX, pageY){
+ var shape = this.shape
+ , off = shape.offset()
+ , w = this.width, h = this.height
+ , x = off.left + w/2 - pageX
+ , y = off.top + h/2 - pageY
+ , theta = Math.atan2(-y,-x)
+ ;
+ this.barrelShape.rotate(theta);
+ return this;
+ };
+
+});
+
+var NUM_OR_PERCENT = /^\s*[\d\.]+\s*%?\s*$/;
+function calcValue(v){
+ var unit = this;
+ return ( NUM_OR_PERCENT.test(v) ? v : eval(v) );
+}
+