'instance' : instance,
'cls' : cls,
'args' : Y(args)
- });
+ }, instance);
var initialise = instance.__initialise__;
if ( isFunction(initialise) )
'instance' : instance,
'cls' : cls,
'args' : Y(arguments)
- });
+ }, instance);
return instance;
}
// Record for metaprogramming
KNOWN_CLASSES[className] = NewClass;
+ NewClass.__bind__ = prototype.__bind__;
var mixins = NewClass.__mixins__ = Y([]).concat(members.__mixins__||[], SuperClass.__mixins__||[]).unique()
, statics = NewClass.__static__ = {}
, pstatics = SuperClass.__static__
}
NewClass.instantiate = instantiate.partial(NewClass);
- // prototype.__bind__ = Y([]).concat(prototype.__bind__||[], SuperClass.fn.__bind__||[]).unique();
+ NewClass.__bind__ = prototype.__bind__ = Y([]).concat(prototype.__bind__||[], SuperClass.__bind__||[]).unique();
// Notify mixins to let them finish up any customization
if (mixins.length)
'child' : NewClass,
'members' : members,
'prototype' : prototype
- });
+ }, NewClass);
return NewClass;
}
Class.fn = Class.prototype;
Class.fn.__class__ = Class.fn.constructor = Class;
Class.fn.__bases__ = Class.__bases__ = Y([ Object ]);
+Class.fn.__bind__ = Class.__bind__ = Y([]);
Class.className = Class.fn.className = "Class";
/* Class Methods */
*/
function mixin(cls, _mxn){
var proto = cls.fn
- , bases = cls.__bases__
- , cbinds = proto.__bind__
+ , bases = Y(cls.__bases__ || [])
+ , cbinds = Y(cls.__bind__ || [])
, cstatic = cls.__static__
, mxns = (Y.isArray(_mxn) ? _mxn.slice(0) : Y(arguments, 1))
;
var mproto = (typeof mxn === "function") ? mxn.fn : mxn
, statics = mxn.__static__
+ , binds = mxn.__bind__
, onCreate = mproto.onCreate
, firstMix = (bases && bases.indexOf(mxn) === -1)
;
}
}
+ // Add/ensure the mixin is at the front of bases
+ bases.remove(mxn).unshift(mxn);
+
+ // Collate binds from mixin
+ if (binds)
+ cls.__bind__ = proto.__bind__ = cbinds.concat(binds).unique();
+
// Only perform these actions the first time we mix into this class
if (!firstMix)
return;
- // Add to bases
- bases.unshift(mxn);
-
// Register onCreate to fire whenever a new instance is initialized
if ( isFunction(cls.on) && hasOwn.call(mproto,'onCreate') && isFunction(onCreate) )
cls.on('create', onCreate);
--- /dev/null
+var Y = require('Y').Y
+, op = require('Y/op')
+, deepcopy = require('Y/types/object').deepcopy
+
+, Mixin = require('evt').Mixin
+, Item = require('tanks/thing/item').Item
+
+, kNull = op.K(null)
+,
+
+Container =
+exports['Container'] =
+Mixin.subclass('Container', {
+ __bind__ : [],
+
+
+ max : 1, // Container capacity
+ reqs : null, // Array of tag requirements to be stored here
+ equipsContents : false, // Whether held items are equipped
+
+ owner : null, // Owning unit
+ size : 0, // Number of items in container
+ items : null, // item id -> Item
+ slots : null, // Array of positions of items in container: container idx -> Item
+ children : null, // Nested containers: container id -> Container
+
+
+ onCreate : function initContainer(evt, owner, items){
+ this.owner = owner;
+ this.items = {};
+ this.children = {};
+ this.slots = new Array(this.max);
+
+ if ( typeof this.reqs == 'string' )
+ this.reqs = Y([ this.reqs ]);
+
+ if (items) items.forEach(this.moveItem, this);
+ },
+
+
+ /**
+ * @param {Item} [item] If present, also check whether item matches this container's requirements.
+ * @return {Boolean} Whether this container will accept this item.
+ */
+ canAddItem : function canAddItem(item){
+ var reqs = this.reqs;
+ return this.size < this.max &&
+ ( !item || !reqs || reqs.intersect(item.tags).length === reqs.length );
+ },
+
+ checkReqs : function checkReqs(item){
+ var reqs = this.reqs;
+ return !!item && ( !reqs || reqs.intersect(item.tags).length === reqs.length );
+ },
+
+ getItem : function getItem(idx){
+ if ( typeof idx == 'number' ) {
+ return this.slots[ (idx < 0 ? this.max+idx : idx) ];
+ } else {
+ if (idx && !idx.inv)
+ idx.inv = new InventoryMeta();
+ return idx;
+ }
+ },
+
+ /**
+ * @param {Number|Item} id The Item or Item ID for which to test.
+ * @return {Boolean} Whether the item is in this container.
+ */
+ hasItem : function hasItem(id){
+ if (id instanceof Item) id = id.__id__;
+ return !!this.items[id];
+ },
+
+ /**
+ * Adds an item to this container.
+ * @param {Item} item
+ * @return {this}
+ */
+ addItem : function addItem(item){
+ if ( this.hasItem(item) )
+ return this;
+ else
+ return this.moveItem(item, this._getEmptySlot());
+ },
+
+ /**
+ * Changes the position of the item in this container.
+ * @param {Item} item Item to move.
+ * @param {Number} idx Index in the container to which to move item.
+ * @return {this}
+ */
+ moveItem : function moveItem(item, idx){
+ item = this.getItem(item);
+ if (!item || typeof idx != "number")
+ return this;
+
+ idx = this._getIdx(idx);
+
+ // Note old slot info before we mutate anything
+ var inv = item.inv.clone()
+ , oldIdx = inv.idx
+ , oldBag = inv.bag
+
+ // Note item we're displacing, if any
+ , dispItem = this.slots[idx]
+ , didDisplace = dispItem && dispItem !== item
+ , sameOwner = this.owner && item.owner && this.owner === item.owner
+ , newPickup = !(oldBag && sameOwner)
+ , inventoryMove = sameOwner && !this.hasItem(item)
+ , localMove = sameOwner && !inventoryMove
+
+ , hasRoom = !(didDisplace && newPickup && this.size >= this.max)
+ ;
+
+ if ( !hasRoom )
+ return this;
+
+ // Check requirements and then attempt transaction
+ // TODO: if displaced item fails req check, we should try to find it a new home rather than giving up
+ if ( !( this.checkReqs(item) && ( !didDisplace || oldBag.checkReqs(dispItem) )
+ && this._putItem(item, idx, didDisplace)) )
+ return this;
+
+ // Updates meta + size
+ if ( inventoryMove )
+ oldBag._removeItem(item, oldIdx);
+ else if ( newPickup && oldBag )
+ oldBag.removeItem(item, { 'oldIdx':oldIdx });
+
+ if ( didDisplace ) {
+ this._removeItem(dispItem, idx);
+ if ( oldBag && sameOwner )
+ oldBag._putItem(dispItem, oldIdx);
+ else
+ this._putItem(dispItem, this._getEmptySlot());
+ }
+
+ var eq = this.equipsContents
+ , oldEq = oldBag.equipsContents
+ , isEquipped = sameOwner && oldEq
+ , evt = 'item'+(newPickup ? '.acquire' : '.move')+(eq ? (!isEquipped ? '.equip' : '') : (isEquipped ? '.unequip' : ''))
+ , dispEvt = 'item.move'+(!eq ? (oldEq ? '.equip' : '') : (!oldEq ? '.unequip' : ''))
+ ;
+
+ // metadata clone preserves original values in case of loss (displacement by a new pickup)
+ this._emitItemChange(evt, item, { 'idx':idx, 'oldBag':inv.bag, 'oldIdx':inv.idx });
+
+ if ( didDisplace ) {
+ oldBag._putItem(dispItem, oldIdx);
+ oldBag._emitItemChange('item.move', dispItem, { 'idx':oldIdx, 'oldBag':this, 'oldIdx':idx });
+ }
+
+ if ( inventoryMove ) {
+ if ( didDisplace ) {
+ oldBag._putItem(dispItem, oldIdx);
+ oldBag._emitItemChange('item.move', dispItem, { 'idx':oldIdx, 'oldBag':this, 'oldIdx':idx });
+ }
+ this._emitItemChange('item.move', item, { 'idx':idx, 'oldBag':inv.bag, 'oldIdx':inv.idx });
+ } else {
+ evt =
+ dispEvt = 'item.'+(oldBag.equipsContents ? '.equip' : '');
+ this._emitItemChange(evt, item, { 'idx':idx, 'oldBag':inv.bag, 'oldIdx':inv.idx });
+ }
+
+ return this;
+ },
+
+ /**
+ * Removes the item from this container and fires the appropriate events.
+ * @param {Item|Number} item Item or container index to remove.
+ * @param {Object} [options] Options are:
+ * * @option {Number} [oldIdx] In cases where `item.inv` has already been modified, the
+ * old index of the item is required to be manually supplied to clean up correctly.
+ * * @option {Boolean} [keepEquipped=false] If true, does not fire item.unequip event.
+ * * @option {Boolean} [drop=false] If true, Item is dropped and not just removed from
+ * this container. If Container `equipsContents`, this overrides `options.keepEquipped`.
+ * @return {Item} The removed item or null on failure.
+ */
+ removeItem : function removeItem(item, options){
+ options = options || {};
+ item = this.getItem(item);
+ if (!item)
+ return item;
+
+ var idx = (options.oldIdx !== undefined ? options.oldIdx : item.inv.idx);
+ if ( !this._removeItem(item, options.oldIdx) )
+ return null;
+
+ var unequip = this.equipsContents && (options.drop || !options.keepEquipped);
+ this._emitItemChange('item.lose'+(unequip ? '.unequip' : '')+(options.drop ? '.drop' : ''), item, { 'idx':idx });
+
+ return item;
+ },
+
+
+
+ /**
+ * @protected
+ * @return {Integer} Index of first empty slot in this container.
+ */
+ _getEmptySlot : function _getEmptySlot(){
+ var slots = this.slots
+ , max = this.max;
+
+ if (this.size >= max)
+ return -1;
+
+ for (var i=0, v=slots[i]; i<max; v=slots[++i])
+ if (v === undefined)
+ return i;
+
+ return -1;
+ },
+
+ /**
+ * @protected
+ * @param {Any} idx Index to test.
+ * @return {Number} The index, or the next empty index if non-number or -1.
+ */
+ _getIdx : function _getIdx(idx){
+ return (typeof idx == "number" && idx !== -1) ? idx : this._getEmptySlot();
+ },
+
+ /**
+ * @protected
+ * @param {Number} [idx] Slot index to test.
+ * @return {Boolean} Ok if index is a number in the container bounds.
+ */
+ _idxOk : function _idxOk(idx){
+ return (typeof idx == "number" && 0 <= idx && idx < this.max);
+ },
+
+ /**
+ * Inserts item into container at index, updating metadata.
+ * @protected
+ * @param {Item} item
+ * @param {Number} [idx] Container position at which to insert item. If missing, first open slot will be used.
+ * @return {Boolean}
+ */
+ _putItem : function _putItem(item, idx, didDisplace){
+ if (!item) return false;
+
+ var idx = this._getIdx(idx)
+ , id = item.__id__
+ , inv = item.inv
+ ;
+
+ // Bag bounds check
+ if ( 0 <= idx && idx < this.max )
+ return false;
+
+ // Already in bag? Remove from current slot
+ if ( this.items[id] )
+ delete this.slots[inv.idx];
+ else if (!didDisplace)
+ this.size++; // Otherwise increment size
+
+ this.items[id] = item;
+ this.slots[idx] = item;
+ inv.idx = idx;
+ inv.bag = this;
+
+ return true;
+ },
+
+ /**
+ * Removes item from container, updating metadata.
+ * @protected
+ * @param {Item} item
+ * @return {Boolean}
+ */
+ _removeItem : function removeItem(item, oldIdx){
+ if (!item) return false;
+
+ var id = item.__id__
+ , hasItem = !!this.items[id]
+ , itemMoved = oldIdx !== undefined
+ , inv = item.inv
+ , idx = itemMoved ? oldIdx : inv.idx
+ ;
+
+ delete this.items[id];
+
+ if ( hasItem ) {
+ this.size--;
+ if (this.slots[idx] === item)
+ delete this.slots[idx];
+ if (!itemMoved)
+ inv.idx = inv.bag = null;
+ return true;
+ }
+
+ return false;
+ },
+
+ /**
+ * @param {String} evt Event name.
+ * @param {Item} item Triggering item.
+ * @param {Object} [data] Additional data to add to the event.
+ * @return {this}
+ */
+ _emitItemChange : function _emitItemChange(evt, item, data){
+ data = Y.extend({ 'item':item, 'bag':this, 'unit':this.owner }, data || {});
+ this.emit(evt, item, data, item);
+ item.emit(evt, this, data, this);
+ // unit.emit(evt, item, data); // inventory should take care of this
+ return this;
+ }
+
+
+
+});
+
+function InventoryMeta(bag, idx, links){
+ this.bag = bag;
+ this.idx = idx;
+ this.links = links || [];
+}
+Y.core.extend(InventoryMeta.prototype, {
+ clear : function clear(){
+ this.bag = this.idx = null;
+ this.links = [];
+ return this;
+ },
+
+ clone : function clone(){
+ return new InventoryMeta(this.bag, this.idx, this.links); // Don't care about links
+ },
+
+ toString : function(){
+ return "BagLoc(idx="+this.idx+", bag="+this.bag+")";
+ }
+
+})
+++ /dev/null
-var Y = require('Y').Y
-, op = require('Y/op')
-, deepcopy = require('Y/types/object').deepcopy
-
-, Mixin = require('evt').Mixin
-, Item = require('tanks/thing/item').Item
-
-, kNull = op.K(null)
-,
-
-Container =
-exports['Container'] =
-Mixin.subclass('Container', {
- __bind__ : [],
-
- name : null, // Name of this container
- max : 1, // Container capacity
- reqs : null, // Array of tag requirements to be stored here
- equipContents : false, // Whether held items are equipped
-
- size : 0, // Number of items in container
- items : null, // item id -> Item
- slots : null, // Array of positions of items in container: container idx -> Item
-
-
- init : function initContainer(name, unit, items, options){
- this.name = name;
- this.unit = unit;
- Y.core.extend(this, options);
-
- this.items = {};
- this.slots = new Array(inv.max);
-
- if ( typeof this.reqs == 'string' )
- this.reqs = Y([ this.reqs ]);
-
- if (items) items.forEach(this.moveItem, this);
- },
-
-
- /**
- * @param {Item} [item] If present, also check whether item matches this container's requirements.
- * @return {Boolean} Whether this container will accept this item.
- */
- canAddItem : function canAddItem(item){
- var reqs = this.reqs;
- return this.size < this.max &&
- ( !item || !reqs || reqs.intersect(item.tags).length === reqs.length );
- },
-
- getItem : function getItem(idx){
- if (idx === undefined || idx === null || idx instanceof Item)
- return idx;
- else
- return this.slots[idx];
- },
-
- /**
- * @param {Number|Item} id The Item or Item ID for which to test.
- * @return {Boolean} Whether the item is in this container.
- */
- hasItem : function hasItem(id){
- if (id instanceof Item) id = id.__id__;
- return !!this.items[id];
- },
-
- /**
- * Adds an item to this container.
- * @param {Item} item
- * @return {this}
- */
- addItem : function addItem(item){
- if ( this.hasItem(item) )
- return this;
- else
- return this.moveItem(item, this._getEmptySlot());
- },
-
-
- /**
- * Changes the position of the item in this container.
- * @param {Item} item Item to move.
- * @param {Number} idx Index in the container to which to move item.
- * @return {this}
- */
- moveItem : function moveItem(item, idx){
- item = this.getItem(item);
- if (!item || typeof idx != "number")
- return this;
-
- // Make note of item we're displacing, if any
- var displacedItem = this.slots[idx]
- , oldIdx = item.ui.bpIdx
- , newItem = !this.hasItem(item)
- , evtName = 'item.move'
- ;
-
- if ( !this._putItem(item, idx) )
- return this;
-
- if (newItem)
- if (this.equipContents)
- evtName = 'item.equip'
- else
- evtName = 'item.move'
-
- idx = item.ui.bpIdx;
- this._emitItemChange(evtName, item, { 'idx':idx, 'oldIdx':oldIdx });
-
- if ( displacedItem && displacedItem !== item ) {
- displacedItem.ui.bpIdx = null;
- this._putItem(displacedItem, oldIdx);
- this._emitItemChange('item.move', displacedItem, { 'idx':displacedItem.ui.bpIdx, 'oldIdx':idx });
- }
-
- return this;
- },
-
- /**
- * Removes the item from this container.
- * @param {Item|Number} idx Item or container index to remove.
- * @param {Boolean} [drop=false] If true, Item is dropped and not just removed from this container.
- * @return {Item} The removed item.
- */
- removeItem : function removeItem(idx, drop){
- var item = this.getItem(idx);
- idx = item ? item.ui.bpIdx : -1; // ensures we have the index if given an Item
-
- // TODO: UI feedback on failure
- if ( !this._removeItem(item) )
- return null;
-
- this._emitItemChange('item.lose'+(drop ? '.drop' : ''), item, { 'idx':idx });
- return item;
- },
-
- equipItem : function equipItem(slot, item){
- item = this.getItem(item);
-
- var inv = this.inventory
- , eqs = inv.equipment;
-
- if ( !(slot in eqs) )
- throw new Error('Unit '+this+' does not have an equipment slot '+slot+'!');
-
- if ( !(item instanceof Item) )
- throw new Error('Unit '+this+' cannot equip item '+item+'!');
-
- // TODO: Ensure item has right tags for slot
- var oldIdx = item.backpack;
- this._removeItem(item);
-
- // TODO: UI feedback
- if ( eqs[slot] )
- this.unequipSlot(slot, oldIdx);
-
- this.items[item.__id__] = item;
- eqs[slot] = item;
- item.slot = slot;
-
- return this._emitItemChange('item.equip', item, { 'slot':slot });
- },
-
- unequipSlot : function unequipSlot(slot, idx){
- var inv = this.inventory
- , eqs = inv.equipment
- , item = eqs[slot]
- ;
- if (item) {
- // TODO: UI feedback on failure
- if ( !this._putItem(item, idx) )
- return this;
-
- eqs[slot] = null;
- item.slot = null;
- this._emitItemChange('item.unequip', item, { 'slot':slot, 'idx':idx });
- }
- return this;
- },
-
-
-
- /**
- * @return {Integer} Index of first empty slot in this unit's backpack.
- */
- _getEmptySlot : function _getEmptySlot(){
- var slots = this.slots
- , max = this.max;
-
- if (this.size >= max)
- return -1;
-
- for (var i=0, v=slots[i]; i<max; v=slots[++i])
- if (v === undefined)
- return i;
-
- return -1;
- },
-
-
- /**
- * Inserts item into backpack at index, updating metadata.
- * @protected
- * @param {Item} item
- * @param {Number} [idx] Backpack position at which to insert item. If missing, first open slot will be used.
- * @return {Boolean}
- */
- _putItem : function putItem(item, idx){
- if (!item) return false;
- &nb