- Config-driven unit-types (name, stats, properties; pointers to behavior scripts, assets)
- Game scoring
- Support touch events (for iPad?)
-- Migrate A* code into PathMap
# Code Notes
});
+var
+
+/**
+ * Everybody's favourite party metaclass!
+ */
+Mixin =
+exports['Mixin'] =
+Y.subclass('Mixin', {
+ __mixin_skip__ : [ 'mixIntoClass' ],
+
+
+
+ /**
+ * Mixes this Mixin into another Class.
+ */
+ mixIntoClass : function mixIntoClass(cls){
+
+ },
+
+})
+,
+
+
/**
* Root-class for all Y-objects.
*/
-var YBase = new Class("YBase", {
+YBase = new Class("YBase", {
__y__ : true
});
, del = require('Y/delegate')
, type = require('Y/type')
, op = require('Y/op')
-, mixin = require('Y/utils').mixin
+, mixInto = require('Y/utils').mixInto
, YCollection = require('Y/types/collection').YCollection
, _Array = Array
var newYArray = YArray.instantiate.bind(YArray);
- mixin({ target:YArray, donor:_Array, context:'_o',
+ mixInto({ target:YArray, donor:_Array, context:'_o',
names:'indexOf lastIndexOf shift pop join'.split(' ') });
- mixin({ target:YArray, donor:_Array, context:'_o', chain:true,
+ mixInto({ target:YArray, donor:_Array, context:'_o', chain:true,
names:'push unshift sort splice reverse'.split(' ') });
- mixin({ target:YArray, donor:_Array, context:'_o', fnFirst:true, wrap:newYArray,
+ mixInto({ target:YArray, donor:_Array, context:'_o', fnFirst:true, wrap:newYArray,
names:'map forEach filter'.split(' ') });
- mixin({ target:YArray, donor:_Array, context:'_o', fnFirst:true,
+ mixInto({ target:YArray, donor:_Array, context:'_o', fnFirst:true,
names:['reduce', 'some', 'every'] });
- mixin({ target:YArray, donor:_Array, context:'_o', wrap:newYArray,
+ mixInto({ target:YArray, donor:_Array, context:'_o', wrap:newYArray,
names:['slice'] });
var YCollection = require('Y/types/collection').YCollection
-, mixin = require('Y/utils').mixin
+, mixInto = require('Y/utils').mixInto
, op = require('Y/op')
, core = require('Y/core')
, del = require('Y/delegate')
YCollection.subclass('YString', function(YString){
var newYString = YString.instantiate.bind(YString);
- mixin({ target:YString, donor:String, context:'_o', wrap:newYString,
+ mixInto({ target:YString, donor:String, context:'_o', wrap:newYString,
names:'slice substr substring concat replace toLowerCase toUpperCase'.split(' ') });
- mixin({ target:YString, donor:String, context:'_o',
+ mixInto({ target:YString, donor:String, context:'_o',
names:'split indexOf lastIndexOf charAt charCodeAt'.split(' ') });
function trim(val){
var
-This = mixin['This'] = {},
-Target = mixin['Target'] = {},
+This = mixInto['This'] = {},
+Target = mixInto['Target'] = {},
defaults = {
/**
* {Object|Function} target Required. Target object onto which methods will be added. If this a Function, it's prototype will be used.
'names' : null,
/**
- * {mixin.This|mixin.Target|String} [context=mixin.This]
+ * {mixInto.This|mixInto.Target|String} [context=mixInto.This]
* Sets the call context for the function.
- * - {mixin.This}: (Default) Use runtime `this` as context.
- * - {mixin.Target}: Use `target` as context.
+ * - {mixInto.This}: (Default) Use runtime `this` as context.
+ * - {mixInto.Target}: Use `target` as context.
* - {String}: Names a property on `this` to use. If specified and property is false-y, `this` is used.
*/
'context' : This,
*/
'fnFirst' : false
};
-function mixin(options){
+function mixInto(options){
var o = core.extend({}, defaults, options)
, target = o.target
, donor = o.donor
}
exports['bindAll'] = bindAll;
-exports['mixin'] = mixin;
+exports['mixInto'] = mixInto;
this.negBleed = new Loc(0,0);
this.posBleed = new Loc(0,0);
- this.boundingBox = new BoundingBox(0,0, 0,0, this.originX,this.originY);
- this._origin = this.boundingBox.origin;
+ this.bbox = new BoundingBox(0,0, 0,0, this.originX,this.originY);
+ this._origin = this.bbox.origin;
this.transform = {
rotate : 0,
this.layerWidth = w;
this.layerHeight = h;
- var bb = this.boundingBox.resize(w,h)
+ var bb = this.bbox.resize(w,h)
, nb = this.negBleed, pb = this.posBleed
// HTMLCanvas.{width,height} is a long
this.layerWidth = w;
- var bb = this.boundingBox.resize(w, this.layerHeight)
+ var bb = this.bbox.resize(w, this.layerHeight)
// , ro = bb.relOrigin
, nb = this.negBleed
, cw = this.canvasWidth = Math.ceil(w + nb.x + this.posBleed.x); // HTMLCanvas.width is a long
this.layerHeight = h;
- var bb = this.boundingBox.resize(this.layerWidth, h)
+ var bb = this.bbox.resize(this.layerWidth, h)
// , ro = bb.relOrigin
, nb = this.negBleed
, ch = this.canvasHeight = Math.ceil(h + nb.y + this.posBleed.y); // HTMLCanvas.height is a long
x = ('left' in x ? x.left : x.x);
}
- var bbox = this.boundingBox.relocate(x,y);
+ var bbox = this.bbox.relocate(x,y);
this.css({
'left' : bbox.x1,
'top' : bbox.y1
--- /dev/null
+//#exports PathingType StatInvariant
+require('Y').Y.extend(exports, {
+
+ /// Pathing Constants ///
+ /** How this object interacts with the world. */
+ PathingType : {
+
+ /** Does not obstruct other objects. */
+ PASSABLE : 0,
+
+ /** Does not obstruct other objects, but still collides with them. */
+ ZONE : 1,
+
+ /** Obstructs other blockers with its BoundingBox. */
+ BLOCKING : 2,
+
+ /** Potentially obstructs other objects, but requires a special test once a BoundingBox collision has been detected. */
+ IRREGULAR : 3
+
+ },
+
+ /**
+ * 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.
+ */
+ StatInvariant : {
+
+ /** Do not change current value. */
+ NONE : 1,
+
+ /** Keep current value at the same ratio it was before the change. */
+ RATIO : 2,
+
+ /** Apply the full change to the current value. */
+ FULL : 3
+
+ }
+
+});
\ No newline at end of file
+var Y = require('Y').Y
+,
+
+
+// Events:
+// - live.{affect,stack}
+// - die.{expire,dismiss,dispel}
+Buff =
+exports['Buff'] =
+Y.subclass('Buff', {
+ // __mixins__: [ Informative, Speciated, Meronomic, Usable ], // Configurable?
+
+
+ /// Configurable ///
+ priority : 0, // Order of stat and effect application (lower is earlier)
+ stats : {
+ timeout : -1,
+ stacked : 1,
+ threat : 1
+ },
+ stat_mods : {}, // {stat : Number|Function} Modifications to stats
+ // triggers : {}, // {event : Effect} // maybe
+ // effects : [], // {Function...} Effects to trigger on affect // not sure why i need this
+
+ /// Instance ///
+ owner : null, // Agent which originated the buff
+ target : null, // Agent to which the buff applies
+
+
+
+ init : function initBuff(owner, target){
+ this.owner = owner;
+ this.game = owner.game;
+ this.target = target;
+ this.created = this.game.NOW;
+ // XXX: Speciated mixin must clone objects
+ },
+
+ applyStatMods : function applyStatMods(modifier){
+ modifier = modifier || 1;
+ Y(this.stat_mods).forEach(function(mod, name){
+ var keyparts = name.split('_')
+ , key = parts[0]
+ , part = parts[1] || 'val'
+ ;
+ this.target.stats[key].modifyPart(part, modifier * mod);
+ }, this);
+ return this;
+ },
+
+ removeStatMods : function removeStatMods(){
+ return this.applyStatMods(-1);
+ }
+
+ // XXX: toString implemented by Informative?
+
+})
+
+
+
--- /dev/null
+var Y = require('Y').Y
+
+, StatInvariant = require('tanks/constants').StatInvariant
+
+, NONE = StatInvariant.NONE
+, RATIO = StatInvariant.RATIO
+, FULL = StatInvariant.FULL
+,
+
+
+
+Stat =
+exports['Stat'] =
+Y.subclass('Stat', {
+ // __mixins__: [ Informative, Speciated, Meronomic, Usable ], // Configurable?
+
+ integer : true, // if true, current value is rounded after a change
+
+ base : 0,
+ val : 0,
+ max : 0,
+
+
+ init : function initStat(base, val, max, isFloat){
+ this.base = base;
+ this.val = val !== undefined ? val : base;
+ this.max = max !== undefined ? max : base;
+ this.integer = !isFloat;
+ },
+
+ clone : function clone(){
+ return new Stat(this.base, this.val, this.max, !this.integer);
+ },
+
+ /**
+ * Resets max value and current value to base value.
+ * @return {this}
+ */
+ reset : function reset(){
+ this.val = this.max = this.base;
+ return this;
+ },
+
+ /**
+ * @param {Number} dv Amount to add to current value.
+ * @return {this}
+ */
+ modify : function modify(dv){
+ var v = Math.min(this.max, this.val+dv);
+ this.val = (this.integer ? Math.round(v) : v);
+ return this;
+ },
+
+ /**
+ * @param {Number} dv Amount to add to base and max value.
+ * @param {StatInvariant} [inv=FULL] Invariant to restore for current value.
+ * @return {this}
+ */
+ modifyBase : function modifyBase(dv, inv){
+ this.base += dv;
+ return this.modifyMax(dv, inv || FULL);
+ },
+
+ /**
+ * @param {Number} dv Amount to add to current value.
+ * @param {StatInvariant} [inv=FULL] Invariant to restore for current value.
+ * @return {this}
+ */
+ modifyMax : function modifyMax(dv, inv){
+ var v = this.val
+ , oldmax = this.max;
+ this.max += dv;
+
+ switch ( inv ) {
+ case NONE: break;
+
+ case RATIO:
+ v = (v / oldmax) * this.max;
+ break;
+
+ case FULL:
+ default:
+ v += dv;
+ break;
+ }
+ this.val = (this.integer ? Math.round(v) : v);
+
+ return this;
+ },
+
+ modifyPart : function modifyPart(part, dv, inv){
+ var method =
+ (part === 'max' ? 'modifyMax' :
+ (part === 'base' ? 'modifyBase' : 'modify'));
+ return this[method](dv, inv);
+ },
+
+ toString : function(){
+ return this.val+'/'+this.max+(this.max !== this.base ? ' ['+this.base+']' : '');
+ }
+
+})
+;
+
+Stat.fn.modifyVal = Stat.fn.modify;
+
+
+Stat['from'] =
+function fromValue(v){
+ if ( v instanceof Stat )
+ return v.clone();
+ else if ( Y.isArray(v) )
+ return new Stat(v[0], v[1], v[2], v[3]);
+ else
+ return new Stat(v);
+};
+
+/**
+ * Accepts any number of objects which are combined to make the new map of names to stats.
+ */
+exports['createStats'] =
+function createStats(o){
+ var stats = Y({});
+ return stats
+ .extend.apply(stats, Y(arguments))
+ .map(Stat.from);
+};
var d = evt.data
, self = this;
- NOW = d.now;
- ELAPSED = d.elapsed;
- TICKS = d.ticks;
- SECONDTH = ELAPSED / 1000;
- SQUARETH = REF_SIZE * SECONDTH
+ NOW = this.NOW = d.now;
+ ELAPSED = this.ELAPSED = d.elapsed;
+ TICKS = this.TICKS = d.ticks;
+ SECONDTH = this.SECONDTH = ELAPSED / 1000;
+ SQUARETH = this.SQUARETH = REF_SIZE * SECONDTH
this.active.invoke('updateCooldowns', ELAPSED, NOW);
this.active.invoke('act', ELAPSED, NOW);
if ( this.player.dead )
this.gameover = 'lose';
- else if ( !this.units.filter('_.align !== 1 && !_.dead'.lambda()).size() )
+ else if ( !this.units.filter('_.align !== 1 && !_.dead').size() )
this.gameover = 'win';
if ( this.gameover )
return running;
},
+
+
+ // *** Agent Management *** //
+
addAnimation : function addAnimation(animation){
this.animations.push(animation);
this.level.append(animation);
return animation;
},
-
-
- // *** Agent Management *** //
-
addThing : function addThing(unit, col,row){
if (unit instanceof Event)
unit = unit.trigger;
, ELAPSED = MS_PER_FRAME // Time (ms) since previous tick
, TICKS = 0 // Ticks since start of game
+/// Pathing Constants (from tanks/map/map) ///
+
+/** Does not obstruct other objects. */
+, PASSABLE = 0
+
+/** Does not obstruct other objects, but still collides with them. */
+, ZONE = 1
+
+/** Obstructs other blockers with its BoundingBox. */
+, BLOCKING = 2
+
+/** Potentially obstructs other objects, but requires a special test once a BoundingBox collision has been detected. */
+, IRREGULAR = 3
+
/// Common Components of Computation ///
addBlocker : function addBlocker(obj){
this.removeBlocker(obj);
- var bb = obj.boundingBox;
- if (obj.blocking && bb)
+ var pt = obj.blocking
+ , bb = obj.bbox ;
+ if (pt !== undefined && bb)
obj.region = this.set(bb.x1,bb.y1, bb.x2,bb.y2, obj);
return obj;
},
delete obj.region;
}
return obj;
+ },
+
+ getBlockers : function getBlockers(x1,y1, x2,y2, ignore){
+ if (x1 && x1.x1 !== undefined) {
+ ignore = y1;
+ y2 = x1.y2; x2 = x1.x2;
+ y1 = x1.y1; x1 = x1.x1;
+ }
+ var bs = this.get(x1,y1, x2,y2)
+ .filter('_.blocking === BLOCKING');
+ if (ignore && bs.length)
+ return bs.remove.apply(bs, ignore || []);
+ else
+ return bs;
}
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._squares = {};
this.level = level;
this.game = level.game;
this.walls = level.walls;
this.allWalls = level.allWalls;
- this._squares = {};
-
this.game.addEventListener('ready', this.setup.bind(this, x1,y1, x2,y2));
},
this.boundaryWalls = Y.map(BWs, 'this.level.addWall.apply(this.level, _)', this);
},
- getBlockers : function getBlockers(x1,y1, x2,y2, ignore){
- if (x1 && x1.x1 !== undefined) {
- ignore = y1;
- y2 = x1.y2; x2 = x1.x2;
- y1 = x1.y1; x1 = x1.x1;
- }
- var bs = this.get(x1,y1, x2,y2)
- .filter('_.blocking === 2'); // map.BLOCKING
- if (ignore && bs.length)
- return bs.remove.apply(bs, ignore || []);
- else
- return bs;
- },
-
wallObstructs : function wallObstructs(line){
return this.walls.some(function(wall){
- return wall.boundingBox.intersects(line);
+ return wall.bbox.intersects(line);
}, this);
},
// Ensure we don't get stuck on a corner on our first step
var first = path.first()
- , bb = agent.boundingBox.relocated( vec.lerp(0.5, agent.loc,first) )
+ , bb = agent.bbox.relocated( vec.lerp(0.5, agent.loc,first) )
, blockers = this.getBlockers(bb, [agent]);
if (blockers.length)
_blocked : function blocked(){
var pm = this.pathmap
, agent = pm._agent
- // , bb = agent.boundingBox
+ // , bb = agent.bbox
// , origin = bb.relOrigin
// , left = origin.x, right = bb.width - left
intersects : function intersects(x,y){
var o = x;
if (o instanceof Thing)
- return o.boundingBox.intersects(this);
+ return o.bbox.intersects(this);
if (o instanceof Rect)
return o.intersects(this);
this.thing = thing;
this.game = thing.game;
this.pathmap = thing.game.pathmap;
- this.bbox = thing.boundingBox.clone();
+ this.bbox = thing.bbox.clone();
this.trajectory = trajectory || thing.trajectory;
},
rewind : function rewind(blocker){
var tr = this.trajectory
, bb = this.bbox, st = this.start
- , B = blocker.boundingBox
+ , B = blocker.bbox
, to = this.to ;
// Figure out which boundary of the blocker we crossed and calculate
, Circle = shape.Circle
, config = require('tanks/config').config
-, thing = require('tanks/thing/thing')
, map = require('tanks/map/map')
+, 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
-, fillStats = thing.fillStats
-, Thing = thing.Thing
,
},
createStats : function createStats(){
- this.stats = Y({}, fillStats(this.owner.stats), fillStats(this.stats) );
+ this.stats = stat.createStats(this.owner.stats, this.stats);
},
remove : function remove(){
, wall = this.closestOf(this.game.pathmap.allWalls)
, wmid = wall.midpoint
- , lvl = this.game.level.boundingBox, w = lvl.width, h = lvl.height
+ , lvl = this.game.level.bbox, w = lvl.width, h = lvl.height
, x = ((mid.x - wmid.x) > 0 ? w : 0)
, y = ((mid.y - wmid.y) > 0 ? h : 0)
, to = this.currentMove = trj.near(x,y);
fn = (fn || kTrue).toFunction();
var within = BULLET_MOVE_PER_FRAME*ticks
- , bb = this.boundingBox
+ , bb = this.bbox
, x1 = bb.x1 - within, y1 = bb.y1 - within
, x2 = bb.x2 + within, y2 = bb.y2 + within
;
bullets = ( Y.isArray(bullets) ? bullets : [bullets] );
wiggle = wiggle || 0;
- var tank = this, bb = this.boundingBox
+ var tank = this, bb = this.bbox
, w = bb.width+wiggle, h = bb.height+wiggle ;
// , w = (bb.width+wiggle)/2, h = (bb.height+wiggle)/2 ;
// var barrel = this.barrel
// , loc = this.loc
- // , bb = this.boundingBox
+ // , bb = this.bbox
// , w2 = bb.width/2, h2 = bb.height/2
// , x0 = bb.x1+w2, y0 = bb.y1+h2
//
// , sin = Math.sin(theta), cos = Math.cos(theta)
//
// , x1 = x0 + w2*cos, y1 = y0 + h2*sin
- // , sz = (barrel.boundingBox.width - w2)/2 + WIGGLE
+ // , sz = (barrel.bbox.width - w2)/2 + WIGGLE
// , blockers = this.game.pathmap.get(x1-sz,y1-sz, x1+sz,y1+sz).remove(this)
;
, theta = barrel.transform.rotate
, sin = Math.sin(theta), cos = Math.cos(theta)
- , len = barrel.boundingBox.width + WIGGLE
+ , len = barrel.bbox.width + WIGGLE
, x = loc.x + len*cos
, y = loc.y + len*sin
, config = require('tanks/config').config
, map = require('tanks/map/map')
+, stat = require('tanks/effects/stat')
, THING_ID = 0
,
exports['fillStats'] =
function fillStats(stats){
return Y.reduce(stats, function(_stats, v, k){
+ _stats[k] = v;
+
k = Y(k);
- var k_ = k.rtrim('_max')
- , k_max = k+'_max'
+ var k_ = k.rtrim('_max')
+ , k_max = k_+'_max'
+ , k_base = k_+'_base'
;
- if ( k.endsWith('_max') ) {
- if ( _stats[k_] === undefined )
- _stats[k_] = v;
+ if ( _stats[k_] === undefined )
+ _stats[k_] = v;
- } else if ( _stats[k_max] === undefined )
+ if ( _stats[k_max] === undefined )
_stats[k_max] = v;
+ if ( _stats[k_base] === undefined )
+ _stats[k_base] = _stats[k_max];
+
return _stats;
- }, Y.extend({}, stats));
+ }, {});
}
,
// Location
loc : null,
- boundingBox : null,
+ bbox : null,
// Rotation (rads)
rotation : 0,
createBoundingBox : function createBoundingBox(){
- this.boundingBox = new BoundingBox(0,0, this.width,this.height, this.originX,this.originY);
+ this.bbox = new BoundingBox(0,0, this.width,this.height, this.originX,this.originY);
},
createStats : function createStats(){
- this.stats = fillStats(this.stats);
+ this.stats = stat.createStats(this.stats);
+ },
+
+ recalculateStats : function recalculateStats(){
+ this.stats =
+ Y(this.buffs).reduce(this._applyBuffStats, Y(this.stats).map(this._resetStat, this), this);
},
+ _resetStat : function _resetStat(v, k, stats){
+ k = Y(k);
+ if ( k.endsWith('_max') )
+
+ },
+
+ _applyBuffStats : function _applyBuffStats(buff){
+ Y(buff.stat_mods).forEach(this._applyStatMod, this);
+ },
+
+ _applyStatMod : function _applyStatMod(v, k){
+ this.target.modifyStat(k, v);
+ }
+
+
createCooldowns : function createCooldowns(){
this.cooldowns = {
'attack': new Cooldown(1000 * this.stats.speed)
if (x === undefined && y === undefined)
return this.loc;
- var bb = this.boundingBox.relocate(x,y) // in-place
+ var bb = this.bbox.relocate(x,y) // in-place
, loc = this.loc = bb.absOrigin
, mid = this.midpoint = bb.midpoint
;
},
toString : function(){
- var bb = this.boundingBox
+ var bb = this.bbox
, x1,y1, x2,y2;
if (bb){
x1 = bb.x1; y1 = bb.y1;
<html>
<head>
<title>The Littlest Battletank</title>
+<link rel="shortcut icon" href="/favicon.ico">
<link rel="stylesheet" href="css/reset.css" type="text/css" media="screen">
<link rel="stylesheet" href="css/lttl.css" type="text/css" media="screen">
</head>
.fromPoints(100,100, 150,150)
.fill('#E73075')
.appendTo(plot);
- R.title(R.boundingBox+'');
+ R.title(R.bbox+'');
C = new shape.Circle(25)
.position(150,150)
.fill('#2E62C9')
.appendTo(plot);
- C.title(C.boundingBox+'');
+ C.title(C.bbox+'');
grid.draw();