Checkpoint on unnecessary Inventory complexity.
authordsc <david.schoonover@gmail.com>
Wed, 9 Feb 2011 23:32:49 +0000 (15:32 -0800)
committerdsc <david.schoonover@gmail.com>
Wed, 9 Feb 2011 23:32:49 +0000 (15:32 -0800)
26 files changed:
lib/jsparse.js [new file with mode: 0755]
src/Y/class.cjs
src/Y/modules/y.event.cjs
src/Y/modules/y.polyevent.cjs [new file with mode: 0644]
src/Y/types/array.cjs
src/Y/types/function.cjs
src/Y/types/string.cjs
src/evt.cjs
src/ezl/layer/index.cjs
src/ezl/layer/layer.cjs
src/ezl/layer/layerable.cjs [new file with mode: 0644]
src/tanks/game.cjs
src/tanks/globals.js
src/tanks/index.js
src/tanks/inventory/bag.cjs
src/tanks/inventory/bagbag.cjs
src/tanks/inventory/belt.cjs
src/tanks/inventory/container.cjs
src/tanks/inventory/index.cjs [new file with mode: 0644]
src/tanks/inventory/inventory.cjs
src/tanks/inventory/requirements/indexer.cjs [new file with mode: 0644]
src/tanks/inventory/requirements/requirements.cjs [new file with mode: 0644]
src/tanks/mixins/inventoried.cjs
src/tanks/thing/item.cjs
src/tanks/thing/player.cjs
src/tanks/ui/inventory/containerui.cjs

diff --git a/lib/jsparse.js b/lib/jsparse.js
new file mode 100755 (executable)
index 0000000..260c3c1
--- /dev/null
@@ -0,0 +1,653 @@
+// Copyright (C) 2007 Chris Double.\r
+// \r
+// Redistribution and use in source and binary forms, with or without\r
+// modification, are permitted provided that the following conditions are met:\r
+// \r
+// 1. Redistributions of source code must retain the above copyright notice,\r
+//    this list of conditions and the following disclaimer.\r
+// \r
+// 2. Redistributions in binary form must reproduce the above copyright notice,\r
+//    this list of conditions and the following disclaimer in the documentation\r
+//    and/or other materials provided with the distribution.\r
+// \r
+// THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,\r
+// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND\r
+// FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE\r
+// DEVELOPERS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\r
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,\r
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;\r
+// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,\r
+// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR\r
+// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF\r
+// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\r
+//\r
+function identity(x) {\r
+    return x;\r
+}\r
+\r
+function foldl(f, initial, seq) {\r
+    for(var i=0; i< seq.length; ++i) \r
+        initial = f(initial, seq[i]);\r
+    return initial;\r
+}\r
+\r
+var memoize = true;\r
+\r
+function ParseState(input, index) {\r
+    this.input = input;\r
+    this.index = index || 0;\r
+    this.length = input.length - this.index;\r
+    this.cache = { };\r
+    return this;\r
+}\r
+\r
+ParseState.prototype.from = function(index) {\r
+    var r = new ParseState(this.input, this.index + index);\r
+    r.cache = this.cache;\r
+    r.length = this.length - index;\r
+    return r;\r
+}\r
+\r
+ParseState.prototype.substring = function(start, end) {\r
+    return this.input.substring(start + this.index, (end || this.length) + this.index);\r
+}\r
+\r
+ParseState.prototype.trimLeft = function() {\r
+    var s = this.substring(0);\r
+    var m = s.match(/^\s+/);\r
+    return m ? this.from(m[0].length) : this;\r
+}\r
+\r
+ParseState.prototype.at = function(index) {\r
+    return this.input.charAt(this.index + index);\r
+}\r
+\r
+ParseState.prototype.toString = function() {\r
+    return 'PS"' + this.substring(0) + '"'; \r
+}\r
+\r
+ParseState.prototype.getCached = function(pid) {\r
+    if(!memoize)\r
+        return false;\r
+\r
+    var p = this.cache[pid];\r
+    if(p) \r
+        return p[this.index];\r
+    else\r
+        return false;\r
+}\r
+\r
+ParseState.prototype.putCached = function(pid, cached) {\r
+    if(!memoize)\r
+        return false;\r
+\r
+    var p = this.cache[pid];\r
+    if(p)\r
+        p[this.index] = cached;\r
+    else {\r
+        p = this.cache[pid] = { };\r
+        p[this.index] = cached;\r
+    }\r
+}\r
+\r
+function ps(str) {\r
+    return new ParseState(str);\r
+}\r
+\r
+// 'r' is the remaining string to be parsed.\r
+// 'matched' is the portion of the string that\r
+// was successfully matched by the parser.\r
+// 'ast' is the AST returned by the successfull parse.\r
+function make_result(r, matched, ast) {\r
+        return { remaining: r, matched: matched, ast: ast };\r
+}\r
+\r
+var parser_id = 0;\r
+                \r
+// 'token' is a parser combinator that given a string, returns a parser\r
+// that parses that string value. The AST contains the string that was parsed.\r
+function token(s) {\r
+    var pid = parser_id++;\r
+    return function(state) {\r
+        var savedState = state;\r
+        var cached = savedState.getCached(pid);\r
+        if(cached)\r
+            return cached;\r
+\r
+        var r = state.length >= s.length && state.substring(0,s.length) == s;\r
+        if(r) \r
+            cached = { remaining: state.from(s.length), matched: s, ast: s };\r
+        else\r
+            cached = false;\r
+        savedState.putCached(pid, cached);\r
+        return cached;\r
+    };\r
+}\r
+\r
+// Like 'token' but for a single character. Returns a parser that given a string\r
+// containing a single character, parses that character value.\r
+function ch(c) {\r
+    var pid = parser_id++;\r
+    return function(state) {\r
+        var savedState = state;\r
+        var cached = savedState.getCached(pid);\r
+        if(cached)\r
+            return cached;\r
+        var r = state.length >= 1 && state.at(0) == c;\r
+        if(r) \r
+            cached = { remaining: state.from(1), matched: c, ast: c };\r
+        else\r
+            cached = false;\r
+        savedState.putCached(pid, cached);\r
+        return cached;\r
+    };\r
+}\r
+\r
+// 'range' is a parser combinator that returns a single character parser\r
+// (similar to 'ch'). It parses single characters that are in the inclusive\r
+// range of the 'lower' and 'upper' bounds ("a" to "z" for example).\r
+function range(lower, upper) {\r
+    var pid = parser_id++;\r
+    return function(state) {\r
+        var savedState = state;\r
+        var cached = savedState.getCached(pid);\r
+        if(cached)\r
+            return cached;\r
+        \r
+        if(state.length < 1) \r
+            cached = false;\r
+        else {\r
+            var ch = state.at(0);\r
+            if(ch >= lower && ch <= upper) \r
+                cached = { remaining: state.from(1), matched: ch, ast: ch };\r
+            else\r
+                cached = false;\r
+        }\r
+        savedState.putCached(pid, cached);\r
+        return cached;\r
+    };\r
+}\r
+\r
+// Helper function to convert string literals to token parsers\r
+// and perform other implicit parser conversions.\r
+function toParser(p) {\r
+    return (typeof(p) == "string") ? token(p) : p;\r
+}\r
+\r
+// Parser combinator that returns a parser that\r
+// skips whitespace before applying parser.\r
+function whitespace(p) {\r
+    var p = toParser(p);\r
+    var pid = parser_id++;\r
+    return function(state) {\r
+        var savedState = state;\r
+        var cached = savedState.getCached(pid);\r
+        if(cached)\r
+            return cached;\r
+\r
+        cached = p(state.trimLeft());\r
+        savedState.putCached(pid, cached);\r
+        return cached;\r
+    };\r
+}\r
+\r
+// Parser combinator that passes the AST generated from the parser 'p' \r
+// to the function 'f'. The result of 'f' is used as the AST in the result.\r
+function action(p, f) {\r
+    var p = toParser(p);\r
+    var pid = parser_id++;\r
+    return function(state) {\r
+        var savedState = state;\r
+        var cached = savedState.getCached(pid);\r
+        if(cached) \r
+            return cached;\r
+\r
+        var x = p(state);\r
+        if(x) {\r
+            x.ast = f(x.ast);\r
+            cached = x;\r
+        }\r
+        else {\r
+            cached = false;\r
+        }\r
+        savedState.putCached(pid, cached);\r
+        return cached;\r
+    };\r
+}\r
+\r
+// Given a parser that produces an array as an ast, returns a\r
+// parser that produces an ast with the array joined by a separator.\r
+function join_action(p, sep) {\r
+    return action(p, function(ast) { return ast.join(sep); });\r
+}\r
+\r
+// Given an ast of the form [ Expression, [ a, b, ...] ], convert to\r
+// [ [ [ Expression [ a ] ] b ] ... ]\r
+// This is used for handling left recursive entries in the grammar. e.g.\r
+// MemberExpression:\r
+//   PrimaryExpression\r
+//   FunctionExpression\r
+//   MemberExpression [ Expression ]\r
+//   MemberExpression . Identifier\r
+//   new MemberExpression Arguments \r
+function left_factor(ast) {\r
+    return foldl(function(v, action) { \r
+                     return [ v, action ]; \r
+                 }, \r
+                 ast[0], \r
+                 ast[1]);\r
+}\r
+\r
+// Return a parser that left factors the ast result of the original\r
+// parser.\r
+function left_factor_action(p) {\r
+    return action(p, left_factor);\r
+}\r
+\r
+// 'negate' will negate a single character parser. So given 'ch("a")' it will successfully\r
+// parse any character except for 'a'. Or 'negate(range("a", "z"))' will successfully parse\r
+// anything except the lowercase characters a-z.\r
+function negate(p) {\r
+    var p = toParser(p);\r
+    var pid = parser_id++;\r
+    return function(state) {\r
+        var savedState = state;\r
+        var cached = savedState.getCached(pid);\r
+        if(cached)\r
+            return cached;\r
+\r
+        if(state.length >= 1) {\r
+            var r = p(state);\r
+            if(!r) \r
+                cached =  make_result(state.from(1), state.at(0), state.at(0));\r
+            else\r
+                cached = false;\r
+        }\r
+        else {\r
+            cached = false;\r
+        }\r
+        savedState.putCached(pid, cached);\r
+        return cached;\r
+    };\r
+}\r
+\r
+// 'end_p' is a parser that is successful if the input string is empty (ie. end of parse).\r
+function end_p(state) {\r
+    if(state.length == 0) \r
+        return make_result(state, undefined, undefined);\r
+    else\r
+        return false;\r
+}\r
+\r
+// 'nothing_p' is a parser that always fails.\r
+function nothing_p(state) {\r
+    return false;\r
+}\r
+\r
+// 'sequence' is a parser combinator that processes a number of parsers in sequence.\r
+// It can take any number of arguments, each one being a parser. The parser that 'sequence'\r
+// returns succeeds if all the parsers in the sequence succeeds. It fails if any of them fail.\r
+function sequence() {\r
+    var parsers = [];\r
+    for(var i = 0; i < arguments.length; ++i) \r
+        parsers.push(toParser(arguments[i]));            \r
+    var pid = parser_id++;\r
+    return function(state) {\r
+        var savedState = state;\r
+        var cached = savedState.getCached(pid);\r
+        if(cached) {\r
+            return cached;\r
+        }\r
+\r
+        var ast = [];\r
+        var matched = "";\r
+        var i;\r
+        for(i=0; i< parsers.length; ++i) {\r
+            var parser = parsers[i];        \r
+            var result = parser(state);\r
+            if(result) {\r
+                state = result.remaining;\r
+                if(result.ast != undefined) {\r
+                    ast.push(result.ast);\r
+                    matched = matched + result.matched;\r
+                }\r
+            }\r
+            else {\r
+                break;\r
+            }\r
+        }\r
+        if(i == parsers.length) {\r
+            cached = make_result(state, matched, ast);\r
+        }\r
+        else \r
+            cached = false;\r
+        savedState.putCached(pid, cached);\r
+        return cached;\r
+    };\r
+}\r
+\r
+// Like sequence, but ignores whitespace between individual parsers.\r
+function wsequence() {\r
+    var parsers = [];\r
+    for(var i=0; i < arguments.length; ++i) {\r
+        parsers.push(whitespace(toParser(arguments[i])));\r
+    }\r
+    return sequence.apply(null, parsers);       \r
+}\r
+\r
+// 'choice' is a parser combinator that provides a choice between other parsers.\r
+// It takes any number of parsers as arguments and returns a parser that will try\r
+// each of the given parsers in order. The first one that succeeds results in a \r
+// successfull parse. It fails if all parsers fail.\r
+function choice() {\r
+    var parsers = [];\r
+    for(var i = 0; i < arguments.length; ++i) \r
+        parsers.push(toParser(arguments[i]));            \r
+    var pid = parser_id++;\r
+    return function(state) {\r
+        var savedState = state;\r
+        var cached = savedState.getCached(pid);\r
+        if(cached) {\r
+            return cached;\r
+        }\r
+        var i;\r
+        for(i=0; i< parsers.length; ++i) {\r
+            var parser=parsers[i];\r
+            var result = parser(state);\r
+            if(result) {\r
+                break;\r
+            }\r
+        }        \r
+        if(i == parsers.length)\r
+            cached = false;\r
+        else\r
+            cached = result;\r
+        savedState.putCached(pid, cached);\r
+        return cached;\r
+    }\r
+}\r
+\r
+// 'butnot' is a parser combinator that takes two parsers, 'p1' and 'p2'. \r
+// It returns a parser that succeeds if 'p1' matches and 'p2' does not, or\r
+// 'p1' matches and the matched text is longer that p2's.\r
+// Useful for things like: butnot(IdentifierName, ReservedWord)\r
+function butnot(p1,p2) {\r
+    var p1 = toParser(p1);\r
+    var p2 = toParser(p2);\r
+    var pid = parser_id++;\r
+    \r
+    // match a but not b. if both match and b's matched text is shorter\r
+    // than a's, a failed match is made\r
+    return function(state) {\r
+        var savedState = state;\r
+        var cached = savedState.getCached(pid);\r
+        if(cached)\r
+            return cached;\r
+        \r
+        var br = p2(state);\r
+        if(!br) {\r
+            cached = p1(state);\r
+        } else {\r
+            var ar = p1(state);\r
+            if(ar.matched.length > br.matched.length)\r
+                cached = ar;\r
+            else\r
+                cached = false;\r
+        }\r
+        savedState.putCached(pid, cached);\r
+        return cached;\r
+    }\r
+}\r
+\r
+// 'difference' is a parser combinator that takes two parsers, 'p1' and 'p2'. \r
+// It returns a parser that succeeds if 'p1' matches and 'p2' does not. If\r
+// both match then if p2's matched text is shorter than p1's it is successfull.\r
+function difference(p1,p2) {\r
+    var p1 = toParser(p1);\r
+    var p2 = toParser(p2);\r
+    var pid = parser_id++;\r
+    \r
+    // match a but not b. if both match and b's matched text is shorter\r
+    // than a's, a successfull match is made\r
+    return function(state) {\r
+        var savedState = sate;\r
+        var cached = savedState.getCached(pid);\r
+        if(cached)\r
+            return cached;\r
+\r
+        var br = p2(state);\r
+        if(!br) {\r
+            cached = p1(state);\r
+        } else {\r
+            var ar = p1(state);\r
+            if(ar.matched.length >= br.matched.length)\r
+                cached = br;\r
+            else\r
+                cached = ar;\r
+        }\r
+        savedState.putCached(pid, cached);\r
+        return cached;\r
+    }\r
+}\r
+\r
+\r
+// 'xor' is a parser combinator that takes two parsers, 'p1' and 'p2'. \r
+// It returns a parser that succeeds if 'p1' or 'p2' match but fails if\r
+// they both match.\r
+function xor(p1, p2) {\r
+    var p1 = toParser(p1);\r
+    var p2 = toParser(p2);\r
+    var pid = parser_id++;\r
+\r
+    // match a or b but not both\r
+    return function(state) {\r
+        var savedState = state;\r
+        var cached = savedState.getCached(pid);\r
+        if(cached)\r
+            return cached;\r
+\r
+        var ar = p1(state);\r
+        var br = p2(state);\r
+        if(ar && br)\r
+            cached = false;\r
+        else\r
+            cached = ar || br;\r
+        savedState.putCached(pid, cached);\r
+        return cached;\r
+    }\r
+}\r
+\r
+// A parser combinator that takes one parser. It returns a parser that\r
+// looks for zero or more matches of the original parser.\r
+function repeat0(p) {\r
+    var p = toParser(p);\r
+    var pid = parser_id++;\r
+    \r
+    return function(state) {\r
+        var savedState = state;\r
+        var cached = savedState.getCached(pid);\r
+        if(cached) {\r
+            return cached;\r
+        }\r
+\r
+        var ast = [];\r
+        var matched = "";\r
+        var result;\r
+        while(result = p(state)) {\r
+            ast.push(result.ast);\r
+            matched = matched + result.matched;\r
+            if(result.remaining.index == state.index)\r
+                break;\r
+            state = result.remaining;                        \r
+        }                \r
+        cached = make_result(state, matched, ast);                \r
+        savedState.putCached(pid, cached);\r
+        return cached;\r
+    }\r
+}\r
+\r
+// A parser combinator that takes one parser. It returns a parser that\r
+// looks for one or more matches of the original parser.\r
+function repeat1(p) {\r
+    var p = toParser(p);\r
+    var pid = parser_id++;\r
+\r
+    return function(state) {\r
+        var savedState = state;\r
+        var cached = savedState.getCached(pid);\r
+        if(cached)\r
+            return cached;\r
+\r
+        var ast = [];\r
+        var matched = "";\r
+        var result= p(state);\r
+        if(!result) \r
+            cached = false;\r
+        else {        \r
+            while(result) {\r
+                ast.push(result.ast);\r
+                matched = matched + result.matched;\r
+                if(result.remaining.index == state.index)\r
+                    break;\r
+                state = result.remaining;                        \r
+                result = p(state);\r
+            }                \r
+            cached = make_result(state, matched, ast);                \r
+        }\r
+        savedState.putCached(pid, cached);\r
+        return cached;\r
+    }\r
+}\r
+\r
+// A parser combinator that takes one parser. It returns a parser that\r
+// matches zero or one matches of the original parser.\r
+function optional(p) {\r
+    var p = toParser(p);\r
+    var pid = parser_id++;\r
+    return function(state) {\r
+        var savedState = state;\r
+        var cached = savedState.getCached(pid);\r
+        if(cached)\r
+            return cached;\r
+        var r = p(state);\r
+        cached = r || make_result(state, "", false);                                \r
+        savedState.putCached(pid, cached);\r
+        return cached;\r
+    }\r
+}\r
+\r
+// A parser combinator that ensures that the given parser succeeds but\r
+// ignores its result. This can be useful for parsing literals that you\r
+// don't want to appear in the ast. eg:\r
+// sequence(expect("("), Number, expect(")")) => ast: Number\r
+function expect(p) {\r
+    return action(p, function(ast) { return undefined; });\r
+}\r
+\r
+function chain(p, s, f) {\r
+    var p = toParser(p);\r
+\r
+    return action(sequence(p, repeat0(action(sequence(s, p), f))),\r
+                  function(ast) { return [ast[0]].concat(ast[1]); });\r
+}\r
+\r
+// A parser combinator to do left chaining and evaluation. Like 'chain', it expects a parser\r
+// for an item and for a seperator. The seperator parser's AST result should be a function\r
+// of the form: function(lhs,rhs) { return x; }\r
+// Where 'x' is the result of applying some operation to the lhs and rhs AST's from the item\r
+// parser.\r
+function chainl(p, s) {\r
+    var p = toParser(p);\r
+    return action(sequence(p, repeat0(sequence(s, p))),\r
+                  function(ast) {\r
+                      return foldl(function(v, action) { return action[0](v, action[1]); }, ast[0], ast[1]);\r
+                  });\r
+}\r
+\r
+// A parser combinator that returns a parser that matches lists of things. The parser to \r
+// match the list item and the parser to match the seperator need to \r
+// be provided. The AST is the array of matched items.\r
+function list(p, s) {\r
+    return chain(p, s, function(ast) { return ast[1]; });\r
+}\r
+\r
+// Like list, but ignores whitespace between individual parsers.\r
+function wlist() {\r
+    var parsers = [];\r
+    for(var i=0; i < arguments.length; ++i) {\r
+        parsers.push(whitespace(arguments[i]));\r
+    }\r
+    return list.apply(null, parsers);       \r
+}\r
+\r
+// A parser that always returns a zero length match\r
+function epsilon_p(state) {\r
+    return make_result(state, "", undefined);\r
+}\r
+\r
+// Allows attaching of a function anywhere in the grammer. If the function returns\r
+// true then parse succeeds otherwise it fails. Can be used for testing if a symbol\r
+// is in the symbol table, etc.\r
+function semantic(f) {\r
+    var pid = parser_id++;\r
+    return function(state) {\r
+        var savedState = state;\r
+        var cached = savedState.getCached(pid);\r
+        if(cached)\r
+            return cached;       \r
+        cached = f() ? make_result(state, "", undefined) : false;\r
+        savedState.putCached(pid, cached);\r
+        return cached;\r
+    }\r
+}\r
+\r
+// The `and` predicate asserts that a certain conditional\r
+// syntax is satisfied before evaluating another production. Eg:\r
+// sequence(and("0"), oct_p)\r
+// (if a leading zero, then parse octal)\r
+// It succeeds if 'p' succeeds and fails if 'p' fails. It never \r
+// consume any input however, and doesn't put anything in the resulting\r
+// AST.\r
+function and(p) {\r
+    var p = toParser(p);\r
+    var pid = parser_id++;\r
+    return function(state) {\r
+        var savedState = state;\r
+        var cached = savedState.getCached(pid);\r
+        if(cached)\r
+            return cached;       \r
+        var r = p(state);\r
+        cached = r ? make_result(state, "", undefined) : false;\r
+        savedState.putCached(pid, cached);\r
+        return cached;\r
+    }\r
+}\r
+                        \r
+// The opposite of 'and'. It fails if 'p' succeeds and succeeds if\r
+// 'p' fails. It never consumes any input. This combined with 'and' can\r
+// be used for 'lookahead' and disambiguation of cases.\r
+//\r
+// Compare:\r
+// sequence("a",choice("+","++"),"b")\r
+//   parses a+b\r
+//   but not a++b because the + matches the first part and peg's don't\r
+//   backtrack to other choice options if they succeed but later things fail.\r
+//\r
+// sequence("a",choice(sequence("+", not("+")),"++"),"b")\r
+//    parses a+b\r
+//    parses a++b\r
+//\r
+function not(p) {\r
+    var p = toParser(p);\r
+    var pid = parser_id++;\r
+    return function(state) {\r
+        var savedState = state;\r
+        var cached = savedState.getCached(pid);\r
+        if(cached)\r
+            return cached;       \r
+        cached = p(state) ? false : make_result(state, "", undefined);\r
+        savedState.putCached(pid, cached);\r
+        return cached;\r
+    }\r
+}\r
+\r
index 65b7d9d..c70ec20 100644 (file)
@@ -109,6 +109,7 @@ function Class(className, Parent, members) {
         var k = classStatics[i];
         NewClass[k] = ClassFactory[k];
     }
+    NewClass.instantiate = instantiate.partial(NewClass);
     
     // Copy parent methods, then add new instance methods
     for (var k in parentMembers)
index 0135273..3cb8030 100644 (file)
@@ -113,23 +113,22 @@ Y.YObject.subclass('Event', {
     },
     
     emit : function emit(evtname, trigger, data){
-        var q = this.queues[evtname]
+        var evt, A = arguments
+        ,   q = this.queues[evtname]
         ,   L = q ? q.length : 0
         ;
-        if ( !L )
-            return this;
-        
-        q = q._o.slice(0);
-        var A = arguments
-        ,   target = this.target
-        ,   evt = new Event(evtname, target, trigger, data)
-        ,   method = (A.length > 3 ? 'apply' : 'call')
-        ,   args   = (A.length > 3 ? [evt].concat(Y(A,3)) : evt)
-        ;
-        for (var i=0, fn=q[i]; i<L; fn=q[++i])
-            fn[method](target, args);
+        if ( L ) {
+            q = q._o.slice(0);
+            var target = this.target
+            ,   evt = new Event(evtname, target, trigger, data)
+            ,   method = (A.length > 3 ? 'apply' : 'call')
+            ,   args   = (A.length > 3 ? [evt].concat(Y(A,3)) : evt)
+            ;
+            for (var i=0, fn=q[i]; i<L; fn=q[++i])
+                fn[method](target, args);
+        }
         
-        if (this.parent && !evt._stopped)
+        if (this.parent && !(evt && evt._stopped) )
             this.parent.emit.apply(this.parent, A);
         
         return evt;
@@ -141,20 +140,6 @@ Y.YObject.subclass('Event', {
     },
     
     /**
-     * Dispatches the given event to all listeners in the appropriate queue.
-     * Listeners are invoked with the event, and with context set to the target.
-     * XXX: does not handle degenerate or recursive event dispatch
-     */
-    dispatchEvent : function dispatchEvent(evt){
-        var queue = this.queues[evt.type];
-        if (queue && queue.length)
-            queue.invoke('call', evt.target, evt);
-        if (this.parent)
-            this.parent.emit(evt.type, evt.trigger, evt.data);
-        return evt;
-    },
-    
-    /**
      * Decorates object with bound methods to act as a delegate of this hub.
      */
     decorate : function decorate(delegate){
@@ -165,6 +150,9 @@ Y.YObject.subclass('Event', {
         return delegate;
     }
 }
+
+
+,   GLOBAL_ID = 0
 ,
 
 Emitter =
@@ -172,6 +160,9 @@ exports['Emitter'] =
 Y.YObject.subclass('Emitter', 
     Y.extend({
         'init' : function(target, parent){
+            if (this.__id__ === undefined)
+                this.__id__ = GLOBAL_ID++;
+            
             this.queues = {};
             
             this.target = target || this;
@@ -187,7 +178,7 @@ methods.on = methods.addListener;
 Emitter.methods = methods;
 
 // Global Event Hub
-Emitter.global = new Emitter()
+Emitter.global = new Emitter();
 Emitter.global.decorate(exports);
 
 Y['event'] = exports;
diff --git a/src/Y/modules/y.polyevent.cjs b/src/Y/modules/y.polyevent.cjs
new file mode 100644 (file)
index 0000000..14cd527
--- /dev/null
@@ -0,0 +1,244 @@
+var Y = require('Y').Y
+,   event = require('Y/modules/y.event')
+,   Event = event.Event
+,   Emitter = event.Emitter
+,
+
+/**
+ * A simple event.
+ * TODO: Detect jQuery event objects.
+ * TODO: If DOM event, wrap with consistent API (use jQuery if present).
+ */
+PolyEvent =
+exports['PolyEvent'] =
+Event.subclass('PolyEvent', {
+    _stopped : false,   // whether propagation is stopped
+    
+    type    : 'event',  // event type
+    target  : null,     // target of the emitter
+    trigger : null,     // object which caused this event
+    data    : null,     // event data
+    
+    
+    
+    init : function init( type, target, trigger, data ){
+        data = data || {};
+        for (var k in data) this[k] = data[k];
+        this.data = this._o = data;
+        
+        this.type = type;
+        this.target = target || trigger;
+        this.trigger = trigger || target;
+    },
+    
+    has : function has(){
+        
+    },
+    
+    hasNoun : function hasNoun(noun){
+        
+    },
+    
+    hasVerb : function hasVerb(verb){
+        
+    },
+    
+    toString: function(){ return "PolyEvent("+this.type+")"; }
+})
+
+/**
+ * A simple multicaster.
+ */
+, methods = {
+    
+    emit : function emit(evtname, trigger, data){
+        var q = this.queues[evtname]
+        ,   L = q ? q.length : 0
+        ;
+        if ( !L )
+            return this;
+        
+        q = q._o.slice(0);
+        var A = arguments
+        ,   target = this.target
+        ,   evt = new PolyEvent(evtname, target, trigger, data)
+        ,   method = (A.length > 3 ? 'apply' : 'call')
+        ,   args   = (A.length > 3 ? [evt].concat(Y(A,3)) : evt)
+        ;
+        for (var i=0, fn=q[i]; i<L; fn=q[++i])
+            fn[method](target, args);
+        
+        if (this.parent && !evt._stopped)
+            this.parent.emit.apply(this.parent, A);
+        
+        return evt;
+    },
+    
+    /**
+     * @param {String} evt Event string to normalize and expand.
+     * @return {PolyEvent...}
+     */
+    getMatchingEvents : function getMatchingEvents(evt){
+        
+    },
+    
+    /**
+     * Decorates object with bound methods to act as a delegate of this hub.
+     */
+    decorate : function decorate(delegate){
+        if (!delegate)
+            return delegate;
+        
+        Emitter.fn.decorate.call(this, delegate);
+        for (var k in methods)
+            delegate[k] = methods[k].bind(this);
+        return delegate;
+    }
+}
+,
+
+PolyEmitter =
+exports['PolyEmitter'] =
+Emitter.subclass('PolyEmitter', 
+    Y.extend({
+        'init' : function(target, parent){
+            Emitter.init.call(this, target, parent);
+        }
+    }, methods) )
+;
+PolyEmitter.methods = methods;
+
+// Global Event Hub
+PolyEmitter.global = new PolyEmitter();
+PolyEmitter.global.decorate(exports);
+
+
+// Helper Functions
+
+var _match_cache = {};
+
+/**
+ * Returns an array of all events which match the given event.
+ * @param event
+ */     
+function getMatchingEvents(event, base_event) {
+    if ( !_match_cache[event] ) {
+        
+        // Segregate event groups in a manner worthy of strict scrutiny
+        var seg = event.split('.').reduce(
+            function(seg, token){
+                if (!base_event)
+                    base_event = token;
+                if (token.indexOf('-') !== -1)
+                    seg.special.push(token);
+                else
+                    seg.plain.push(token);
+                return seg;
+            }, { plain:[], special:[] });
+        
+        // Calculate the correct powerset for each event-group
+        var evtset;
+        if ( !seg.special.length )
+            evtset = powerset( seg.plain ).map(sorter).map(joiner);
+        else {
+            evtset = propertyset( seg.special ).reduce(
+                function(acc, t) {
+                    return acc.concat(
+                        powerset( seg.plain.concat([ t ]) ).map(sorter).map(joiner) );
+                }, new Y.YArray());
+        }
+        // Remove invalid permutations and duplicates
+        _match_cache[event] = evtset
+            .filter(function(v) { 
+                return v.indexOf(base_event) === 0;
+            })
+            .unique()
+            .reverse();
+        
+        // Always notify 'all'
+        _match_cache[event].push('all');
+    }
+    
+    // console.log("getMatchingEvents( event="+event+", base_event="+base_event+" ) --> [ "+_match_cache[event].join(",\n  ")+" ]");
+    return _match_cache[event];
+}
+
+/**
+ * Calculates the set of all unique subsets of S (without regard for order) of size k (ie, combination (S k).
+ * @param {Array} S A collection.
+ * @param {Number} k Length of combinations to calculate.
+ * @return {Array} Array of sub-Arrays of S.
+ */
+function comb(S, k){
+    if ( !S || k <= 0 )
+        return new Y.YArray();
+    
+    if (k === 1)
+        return Y(S).map(mkArray);
+    
+    return S.reduce(function nComb(acc, v, i){
+        var subsets = comb(S.slice(i+1), k-1);
+        return acc.concat( subsets.invoke('concat', [v]) );
+    }, new Y.YArray());
+}
+function mkArray(v){ return new Y.YArray([v]); }
+
+/**
+ * Calculates the powerset of S (set of all subsets, P(S)).
+ * @param {Array} S A collection.
+ * @param {Number} [n] Optional upper-bound on subset size. Defaults to the length of S.
+ * @return {Array} Array of sub-Arrays of S.
+ */
+function powerset(S, n){
+    n = (n < 0 || n === undefined) ? S.length : (!S ? 0 : n);
+    for (var P = new Y.YArray(), i=1; i <= n; ++i)
+        P.extend( comb(S,i) );
+    return P;
+}
+
+/**
+ * @param {Array} T Array of Event-part Arrays.
+ * @return {Array}
+ */
+function propertyset(T){
+    var ps = new Y.YArray();
+    if ( !T || !T.length )
+        return ps;
+    
+    return T.reduce(function(acc, t){ 
+                return acc.extend(t.split('-'), t);
+            }, ps)
+        .concat(T, powerset(T).map(joiner))
+        .unique();
+}
+
+var _prefixes = [ 'state', 'core', 'chrome', 'video', 'share' ]
+,   _suffixes = [ 'user', 'auto' ]
+;
+function cmp( a, b ) {
+    var a_suffix = _suffixes.indexOf(a) !== -1
+    ,   b_suffix = _suffixes.indexOf(b) !== -1
+    ,   a_prefix = _prefixes.indexOf(a) !== -1
+    ,   b_prefix = _prefixes.indexOf(b) !== -1
+    ;
+    if (a_prefix === b_prefix && a_suffix === b_suffix)
+        return 0;
+    else if (a_prefix || b_suffix)
+        return -1;
+    else
+        return 1;
+}
+
+function shove(A, v){ A.push(v); return A; }
+function sorter(A){ A.sort(cmp); return A; }
+function joiner(A){ return A.join('.'); }
+
+function unique(A){
+    return A.reduce(function(acc, v) {
+        return (acc.indexOf(v) === -1) ? shove(acc, v) : acc;
+    }, Y([]));
+}
+
+
+Y.extend( exports, { comb:comb, powerset:powerset, propertyset:propertyset, unique:unique, getMatchingEvents:getMatchingEvents });
+Y.extend( Y['event'], exports );
index 6b3a9eb..011cb36 100644 (file)
@@ -17,7 +17,8 @@ YCollection.subclass('YArray', function(YArray){
     // Add YArray to the things that count as arrays
     type.isArray.types.push(YArray);
     
-    var newYArray = YArray.instantiate.bind(YArray);
+    var newYArray = YArray.instantiate;
+    // var newYArray = YArray.instantiate.bind(YArray);
     
     proxy({ target:YArray, donor:_Array, context:'_o',
         names:'indexOf lastIndexOf shift join'.split(' ') });
@@ -30,14 +31,19 @@ YCollection.subclass('YArray', function(YArray){
     proxy({ target:YArray, donor:_Array, context:'_o', wrap:newYArray,
         names:['slice'] });
     
+    function unwrapY(o){
+        return (o instanceof YArray ? o.end() : o);
+    }
+    
     
     
     this['init'] =
     function initYArray(o){
-        if (o instanceof YArray)
-            this._o = o._o;
-        else
-            this._o = o || [];
+        this._o = (
+            ( o === undefined     ? []   :
+            ( o instanceof Array  ? o    :
+            ( o instanceof YArray ? o._o :
+                [o] )))); // lisp strikes back!
     };
     
     this['get'] =
@@ -105,27 +111,6 @@ YCollection.subclass('YArray', function(YArray){
                 return v;
     };
     
-    /**
-     * Intersects this YArray with another collection, returning a new YArray.
-     * The membership test uses Y(a).has(), so it is possible to intersect collections of different types.
-     * For YArray and YObject, .has() uses strict equality (===) via .indexOf().
-     * 
-     * @param {Array|Object|YCollection} a Comparison collection.
-     * @return {YArray} A new YArray of all elements in {this} found in the supplied collection.
-     * 
-     *      var foo = /foo/;
-     *      var A = [foo, 'A', 1, 2, 3, 'C', /foo/];
-     *      var B = [foo, 'B', 3, 'A', 1, /foo/];
-     *      var I = Y(A).intersect(B);
-     *      I.toString() === "YArray([/foo/,A,1,3])"; // true
-     *      I.get(0) === foo; // true
-     */
-    this['intersect'] =
-    function intersect(a){
-        var A = Y(a);
-        return this.filter(A.has, A);
-    };
-    
     this['clear'] =
     function clear(){
         var A = this._o;
@@ -134,10 +119,6 @@ YCollection.subclass('YArray', function(YArray){
         return this;
     };
     
-    function unwrapY(o){
-        return (o instanceof YArray ? o.end() : o);
-    }
-    
     this['concat'] =
     function concat( donor ){
         var A = this._o;
@@ -234,5 +215,55 @@ YCollection.subclass('YArray', function(YArray){
         }, Y({}), this );
     };
     
+    /**
+     * Set Intersection (A ^ B)
+     * Intersects this YArray with another collection, returning a new YArray.
+     * The membership test uses Y(a).has(), so it is possible to intersect collections of different types.
+     * For YArray and YObject, .has() uses strict equality (===) via .indexOf().
+     * 
+     * @param {Array|Object|YCollection} a Comparison collection.
+     * @return {YArray} A new YArray of all elements in {this} found in the supplied collection.
+     * 
+     *      var foo = /foo/;
+     *      var A = [foo, 'A', 1, 2, 3, 'C', /foo/];
+     *      var B = [foo, 'B', 3, 'A', 1, /foo/];
+     *      var I = Y(A).intersect(B);
+     *      I.toString() === "YArray([/foo/,A,1,3])"; // true
+     *      I.get(0) === foo; // true
+     */
+    this['intersect'] =
+    function intersect(a){
+        var A = Y(a);
+        return this.filter(A.has, A);
+    };
+    
+    /**
+     * Set Union (A v B)
+     * @param {Array|Object|YCollection} a Comparison collection.
+     * @return {YArray} A new YArray of all elements in both collections, but without duplicates.
+     */
+    this['union'] =
+    function union(a){
+        return this.concat(a).unique();
+    };
+    
+    /**
+     * Set Difference (A - B)
+     * @param {Array|Object|YCollection} a Comparison collection.
+     * @return {YArray} A new YArray of only elements in this not in supplied collection.
+     */
+    this['difference'] =
+    function difference(a){
+        var A = Y(a);
+        return this.filter(Y(A.has).compose(op.not), A);
+    };
+    
+    // Symmetric Difference
+    this['xor'] =
+    function xor(a){
+        return this.difference(a).concat( Y(a).difference(this) );
+    };
+    
+    
     return this;
 });
index 22143a7..28e4e93 100644 (file)
@@ -123,11 +123,14 @@ function genericize(fn) {
 
 
 
-function _composer(x,fn){ return fn.call(this, x); }
+
 function compose(f,g){
     var fns = slice.call(arguments).map(_Function.toFunction);
-    return function(){
-        return fns.reduce(_composer, slice.call(arguments), this);
+    return function(x){
+        var self = this;
+        return fns.reduce(function (x, fn){
+            return fn.call(self, x);
+        }, x);
     };
 }
 
index 676a1a9..5f8b808 100644 (file)
@@ -12,7 +12,8 @@ var YCollection = require('Y/types/collection').YCollection
 exports['YString'] =
 YCollection.subclass('YString', function(YString){
     
-    var newYString = YString.instantiate.bind(YString);
+    // var newYString = YString.instantiate.bind(YString);
+    var newYString = YString.instantiate;
     proxy({ target:YString, donor:String, context:'_o', wrap:newYString,
         names:'slice substr substring concat replace toLowerCase toUpperCase'.split(' ') });
     proxy({ target:YString, donor:String, context:'_o',
index 83a0242..d8c1880 100644 (file)
@@ -28,7 +28,7 @@ var Y       = require('Y').Y
 ,   Emitter = require('Y/modules/y.event').Emitter
 ,   unwrap  = require('Y/types/function').unwrap
 
-,   isFunction      = Y.isFunction
+// ,   isFunction      = Y.isFunction
 ,   KNOWN_CLASSES   = Class.KNOWN_CLASSES = exports['KNOWN_CLASSES'] = {}
 
 ,   P        = 'prototype'
@@ -89,7 +89,7 @@ function ConstructorTemplate() {
         }, instance);
         
         var initialise = instance.__initialise__;
-        if ( isFunction(initialise) )
+        if ( typeof initialise == 'function' )
             initialise.apply(instance, args);
         
         cls.emit('created', instance, {
@@ -120,7 +120,7 @@ function createInitialise(cls){
             Y.bindAll(instance, binds);
         
         var init = cls.fn.init;
-        if ( isFunction(init) ) {
+        if ( typeof init == 'function' ) {
             var result = init.apply(instance, arguments);
             if (result) instance = result; // XXX: I think this needs to go away
         }
@@ -165,7 +165,7 @@ function Class(className, Parent, members){
     ,   parentMembers = {}
     ;
     
-    if ( !members && !isFunction(Parent) ) {
+    if ( !members && !(typeof Parent == 'function') ) {
         members = Parent;
         Parent  = null;
     }
@@ -176,7 +176,7 @@ function Class(className, Parent, members){
         Parent = Object;
     
     // Parent is the prototype
-    if ( !isFunction(Parent) ) {
+    if ( !(typeof Parent == 'function') ) {
         SuperClass = getProto(Parent).constructor || Object;
         prototype = Parent;
         
@@ -258,7 +258,7 @@ function Class(className, Parent, members){
         }
     
     // Either invoke body constructor...
-    if ( isFunction(members) ) {
+    if ( typeof members == 'function' ) {
         // body fn is responsible for calling mixin, attaching statics, etc
         members.call(prototype, NewClass);
     
@@ -275,7 +275,7 @@ function Class(className, Parent, members){
     // Notify mixins to let them finish up any customization
     if (mixins.length)
         mixins.forEach(function(mxn){
-            if ( mxn && isFunction(mxn.emit) )
+            if ( mxn && (typeof mxn.emit == 'function') )
                 mxn.emit('mixin', NewClass, { 'mixin':mxn, 'cls':NewClass });
         });
     
@@ -355,18 +355,10 @@ new Class('Mixin', Class, {
         },
         
         /**
+         * @param {String} key
          * @return The merged pairs at key taken from all bases.
          */
-        aggregate : function aggregate(key){
-            return this.__bases__
-                .clone()
-                .unshift(this)
-                .reverse()
-                .reduce(function(acc, base){
-                    var proto = (base instanceof Function ? base.prototype : base);
-                    return Y.extend(acc, proto[key]);
-                }, {});
-        }
+        aggregate : Y(aggregate).methodize()
         
     }
     
@@ -424,17 +416,31 @@ function mixin(cls, _mxn){
             return;
         
         // Register onCreate to fire whenever a new instance is initialized
-        if ( isFunction(cls.on) && hasOwn.call(mproto,'onCreate') && isFunction(onCreate) )
+        if ( (typeof cls.on == 'function') && hasOwn.call(mproto,'onCreate') && (typeof onCreate == 'function') )
             cls.on('create', onCreate);
         
         // Fire the mixin event on this Mixin only if we're done constructing the class
-        if ( isFunction(mxn.emit) && mixin.caller !== Class )
+        if ( (typeof mxn.emit == 'function') && mixin.caller !== Class )
             mxn.emit('mixin', cls, { 'mixin':mxn, 'cls':cls });
     });
     return cls;
 }
 
 
+function aggregate(cls, key){
+    if ( !(cls instanceof Class) && cls.__class__ )
+        cls = cls.__class__;
+    return cls.__bases__
+        .clone()
+        .unshift(cls)
+        .reverse()
+        .reduce(function(acc, base){
+            var proto = (base instanceof Function ? base.prototype : base);
+            return Y.extend(acc, proto[key]);
+        }, {});
+}
+
+
 Mixin.on('subclass',
     function onMixinSubclass(evt){
         var d       = evt.data
@@ -443,13 +449,14 @@ Mixin.on('subclass',
         ,   onMixin = members.onMixin
         ;
         
-        if ( isFunction(onMixin) )
+        if ( typeof onMixin == 'function' )
             mxn.on('mixin', onMixin);
         
         // console.log('Mixin.subclass()', mxn, '<', d.parent, 'onMixin:', onMixin);
     });
 
 
+
 // Expose
 exports['Class'] =
 exports['subclass']    = Class;
@@ -457,3 +464,4 @@ exports['instantiate'] = instantiate;
 exports['fabricate']   = Y.fabricate;
 exports['lookupClass'] = lookupClass;
 exports['mixin']       = mixin;
+exports['aggregate']   = aggregate;
index 55b4392..cfae21f 100644 (file)
@@ -1,9 +1,13 @@
 var Y = require('Y').Y
+,   layerable = require('ezl/layer/layerable')
 ,   layer = require('ezl/layer/layer')
 ,   html  = require('ezl/layer/html')
 ;
 
 Y.core.extend(exports, {
+    'layerable' : layerable,
+    'Layerable' : layerable.Layerable,
+    
     'layer'     : layer,
     'Layer'     : layer.Layer,
     
index bbfc418..7db1b84 100644 (file)
-//#ensure "jquery"
-var Y           = require('Y').Y
-,   op          = require('Y/op')
-
-,   evt         = require('evt')
-
-,   Vec         = require('ezl/math/vec').Vec
-,   Loc         = require('ezl/loc/loc').Loc
-,   BoundingBox = require('ezl/loc/boundingbox').BoundingBox
-,   Animation   = require('ezl/loop/fx').Animation
-, 
-
-CONTEXT_ATTRS  = Y('globalAlpha globalCompositeOperation strokeStyle fillStyle lineWidth lineCap lineJoin miterLimit shadowOffsetX shadowOffsetY shadowBlur shadowColor'.split(' ')),
-FAUX_ACCESSORS = Y('width height position stroke fill origin rotate scale translate title'.split(' '))
-, _X = 0, _Y = 1
+var Y = require('Y')
+,   evt = require('evt')
+,   Layerable = require('ezl/layer/layerable').Layerable
 ,
 
-
-
 Layer =
 exports['Layer'] =
-new evt.Class('Layer', {
-    __bind__ : [ 'tick' ],
-    
-    /// Class Defaults ///
-    
-    // _layer* is applied to this.layer (the DOM Element)
-    _layerHtml  : '<div/>',     // HTML that will become the layer
-    _layerId    : null,         // HTML id attribute
-    _layerAttrs : null,         // HTML attributes
-    _layerClasses : 'ezl layer',// CSS classes
-    
-    originX : 0,
-    originY : 0,
-    
-    hasCanvas          : true,  // Whether to create a canvas
-    useCanvasScaling   : false, // Default to CSS3 scaling
-    alwaysClearDrawing : true,  // Whether to clear the canvas content before redraw
-    alwaysClearAttrs   : false, // Whether to remove all canvas attributes (CONTEXT_ATTRS)
-                                // and transforms (scale, rotate, translate) and reset defaults before redraw
-    
-    
-    
-    /// State ///
-    
-    canvas     : null,
-    parent     : null,
-    children   : null,
-    
-    dirty      : true,
-    ctx        : null,
-    animActive : null,
-    animQueue  : null,
-    _erased    : null,
-    
-    layerWidth  : 0,  canvasWidth  : 0, realWidth  : 0,
-    layerHeight : 0,  canvasHeight : 0, realHeight : 0,
-    
-    x: 0, y: 0, loc : null, // Position relative to parent
-    
-    // Bleeds are marks outside the declared layer-size
-    negBleed : null, // Loc
-    posBleed : null, // Loc
-    
-    // Transforms
-    _origin : null, // rotational origin
-    transform : null, // Object
-    
-    
-    /// Setup ///
+evt.subclass('Layer', {
+    __mixins__ : [ Layerable ],
+    __bind__   : [],
     
-    init : function init(props, attrs, html){
-        if (props !== undefined && props !== null) {
-            switch (typeof props) {
-                case 'boolean' : this.hasCanvas = props;        break;
-                case 'string'  : this._layerHtml = props;       break;
-                case 'object'  : Y.core.extend(this, props);    break;
-            }
-        }
-        
-        this.children   = new Y.YArray();
-        this.animActive = new Y.YArray();
-        this.animQueue  = new Y.YArray();
-        this._erased    = new Y.YArray();
-        
-        this.loc      = new Loc(0,0);
-        this.negBleed = new Loc(0,0);
-        this.posBleed = new Loc(0,0);
-        
-        this.bbox = new BoundingBox(0,0, 0,0, this.originX,this.originY);
-        this._origin = this.bbox.origin;
-        
-        this.transform = {
-            rotate    : 0,
-            scale     : new Loc(1.0,1.0),
-            translate : new Loc(0,0) // translates canvas
-        };
-        
-        this.layer = jQuery(html || this._layerHtml)
-            .addClass(this._layerClasses)
-            .data('layer', this);
-        this.layer[0].layer = this;
-        
-        if (this._layerAttrs)   this.layer.attr(this._layerAttrs);
-        if (this._layerId)      this.layer.attr('id', this._layerId);
-        if (attrs)              this.layer.attr(attrs);
-        
-        if (this.hasCanvas) {
-            this.canvas = jQuery('<canvas />')
-                .appendTo(this.layer);
-            this.ctx = this.canvas[0].getContext('2d');
-            this.canvas[0].layer = this;
-        } else {
-            this.canvas = jQuery();
-        }
-        
-        // this.tick = this.tick.bind(this);
-    },
-    
-    /// Scene Graph Heirarchy ///
-    
-    /** 
-     * @param {Layer} child
-     * @return {this}
-     */
-    append : function append(child){
-        new Y(arguments).invoke('appendTo', this);
-        return this;
-    },
-    
-    /** 
-     * @param {Layer} parent
-     * @return {this}
-     */
-    appendTo : function appendTo(parent){
-        if (!parent) return this;
-        
-        // Always ensure we detach from the DOM and redraw this node
-        this.remove();
-        this.dirty = true;
-        
-        if ( !(parent instanceof Layer) && (parent.shape instanceof Layer) )
-            parent = parent.shape;
-        
-        // Layer? Add self as new child, fix DOM
-        if ( parent instanceof Layer ) {
-            this.parent = parent;
-            parent.children.push(this);
-            parent.layer.append(this.layer);
-            
-        // Otherwise: Attach to a DOM node as a new root layer node, leave parent null
-        } else
-            $(parent).append(this.layer);
-        
-        return this;
-    },
-    
-    /**
-     * @param {Layer} child
-     * @return {this}
-     */
-    prepend : function prepend(child){
-        new Y(arguments).invoke('prependTo', this);
-        return this;
-    },
-    
-    /** 
-     * @param {Layer} parent
-     * @return {this}
-     */
-    prependTo : function prependTo(parent){
-        if (!parent) return this;
-        
-        // Always ensure we detach from the DOM and redraw this node
-        this.remove();
-        this.dirty = true;
-        
-        if ( !(parent instanceof Layer) && (parent.shape instanceof Layer) )
-            parent = parent.shape;
-        
-        // Layer? Add self as new child, fix DOM
-        if ( parent instanceof Layer ) {
-            this.parent = parent;
-            parent.children.unshift(this);
-            parent.layer.prepend(this.layer);
-            
-        // Otherwise: Attach to a DOM node as a new root layer node, leave parent null
-        } else
-            $(parent).prepend(this.layer);
-        
-        return this;
-    },
-    
-    /**
-     * Removes this layer from its parent and the DOM.
-     */
-    remove : function remove(){
-        if (this.parent)
-            this.parent.children.remove(this);
-        this.parent = null;
-        this.layer.remove();
-        return this;
-    },
-    
-    /**
-     * Clears this layer and destroys all children.
-     */
-    empty : function empty(ctx){
-        this.children.invoke('remove');
-        this.clear(false, ctx);
-        return this;
-    },
-    
-    /**
-     * @return The root of the scene graph.
-     */
-    root : function root(){
-        if (this.parent)
-            return this.parent.root();
-        else
-            return this;
-    },
-    
-    
-    
-    
-    
-    /// Attributes ///
-    
-    // set : function set(key, value, def){
-    //     if ( key === undefined || (value === undefined && def === undefined) )
-    //         return this;
-    //     
-    //     value = (value !== undefined ? value : def);
-    //     if ( FAUX_ACCESSORS.has(key) )
-    //         this[key](value);
-    //     else
-    //         this[key] = value;
-    //     
-    //     return this;
-    // },
-    
-    attr : Y.attr.methodize(),
-    
-    size : function size(w,h){
-        if (w === undefined && h === undefined)
-            return new Vec(this.layerWidth,this.layerHeight);
-        
-        if (w === null) w = this.realWidth;
-        if (h === null) h = this.realHeight;
-        
-        this.realWidth  = w;
-        this.realHeight = h;
-        
-        // HTMLElement.{width,height} is a long
-        this.layerWidth  = Math.round(w);
-        this.layerHeight = Math.round(h);
-        
-        var bb = this.bbox.resize(w,h)
-        ,   nb = this.negBleed, pb = this.posBleed
-        
-        ,   cw = this.canvasWidth  = Math.ceil(w + nb.x + pb.x)
-        ,   ch = this.canvasHeight = Math.ceil(h + nb.y + pb.y)
-        ;
-        
-        this.layer.css({
-            'width' : w,            'height' : h,
-            'left'  : bb.x1,        'top' : bb.y1
-        });
-        this.canvas.css({
-            'width' : cw,           'height' : ch,
-            'margin-left' : -nb.x,  'margin-top' : -nb.y
-        });
-        var el = this.canvas[0];
-        if (el) {
-            el.width  = cw;
-            el.height = ch;
-        }
-        
-        return this;
-    },
-    
-    /**
-     * Changes the layer's width and then updates the canvas.
-     */
-    width : function width(w){
-        if (w === undefined)
-            return this.layerWidth;
-        
-        this.layerWidth = w;
-        
-        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.layer.css({
-            'width'       : w,
-            'left'        : bb.x1,
-            // 'margin-left' : -ro.x
-        });
-        
-        this.canvas.css({
-            'width'       : cw,
-            'margin-left' : -nb.x
-        });
-        if (this.canvas.length) this.canvas[0].width = cw;
-        
-        return this;
-    },
-    
-    height : function height(h){
-        if (h === undefined)
-            return this.layerHeight;
-        
-        this.layerHeight = 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
-        
-        this.layer.css({
-            'height'     : h,
-            'top'        : bb.y1,
-            // 'margin-top' : -ro.y
-        });
-        
-        this.canvas.css({
-            'height'     : ch,
-            'margin-top' : -nb.y
-        });
-        if (this.canvas.length) this.canvas[0].height = ch;
-        
-        return this;
-    },
-    
-    /**
-     * position() -> {Loc}
-     * Gets position of the layer relative to the parent.
-     * @return {Loc}
-     */
-    /**
-     * position(x,y) -> {this}
-     * Sets the position of this node, and then returns it.
-     * @param {Number|String|undefined} x
-     * @param {Number|String|undefined} y If omitted, this method must be invoked with `undefined` as the first argument.
-     * @return {this}
-     */
-    /**
-     * position(pos) -> {this}
-     * Sets the position of this node, and then returns it.
-     * @param {Object} pos An object with "x" and/or "y" properties as in `position(x,y)`.
-     * @return {this}
-     */
-    position : function position(x,y){
-        if (x === undefined && y === undefined)
-            return this.layer.position();
-        
-        if ( x instanceof Array ) {
-            y = x[_Y]; x = x[_X];
-        } else if ( Y.isPlainObject(x) ){
-            y = ('top'  in x ? x.top  : x.y);
-            x = ('left' in x ? x.left : x.x);
-        }
-        
-        var bbox = this.bbox.relocate(x,y);
-        this.css({
-            'left' : bbox.x1,
-            'top'  : bbox.y1
-        });
-        return this;
-    },
-    
-    stroke : function stroke(style, width){
-        if (style === undefined && width === undefined)
-            return this.strokeStyle;
-        
-        if (width !== undefined)
-            this.lineWidth = width;
-        
-        this.dirty = true;
-        this.strokeStyle = style;
-        return this;
-    },
     
-    fill : function fill(style){
-        if (style === undefined)
-            return this.fillStyle;
+    init : function initLayer(props, attrs, html){
+        this.setup(props, attrs, html);
         
-        this.dirty = true;
-        this.fillStyle = style;
-        return this;
-    },
-    
-    hide : makeDelegate('hide'),
-    show : makeDelegate('show'),
-    
-    /** CSS properties */
-    css : makeDelegate('css'),
-    
-    /** Position relative to document. */
-    offset : makeDelegate('offset'),
-    
-    
-    
-    /// Transformations ///
-    
-    /**
-     * Gets and sets the origin-location for this shape, used for 
-     * position and rotation. Defaults to the midpoint.
-     */
-    origin : function origin(x,y){
-        var o = this._origin;
-        if (arguments.length === 0)
-            return o.absolute(this.layerWidth, this.layerHeight);
-        
-        if ( x instanceof Array ) {
-            y = x[_Y]; x = x[_X];
-        }
-        o.x = x;
-        o.y = y;
-        return this._applyTransforms();
-    },
-    
-    /**
-     * Rotates this layer by r radians.
-     */
-    rotate : function rotate(r){
-        var t = this.transform;
-        if (r === undefined)
-            return t.rotate;
-        
-        t.rotate = r;
-        return this._applyTransforms();
-    },
-    
-    /**
-     * Scales this layer by (sx,sy), and then applies scaling relatively
-     * to all sublayers (preserving knowledge of their individual scaling).
-     */
-    scale : function scale(sx,sy){
-        var ts = this.transform.scale;
-        if (arguments.length === 0)
-            return ts;
-        
-        if ( sx instanceof Array ) {
-            sy = sx[_Y]; sx = sx[_X];
-        }
-        ts.x = sx;
-        ts.y = sy;
-        this.dirty = true;
-        return this._applyTransforms();
-    },
-    
-    /**
-     * Translates draw calls by (x,y) within this layer only. This allows you to
-     * functionally move the origin of the coordinate system for this layer.
-     */
-    translate : function translate(tx,ty){
-        var tt = this.transform.translate;
-        if (arguments.length === 0)
-            return tt;
-        
-        if ( tx instanceof Array ) {
-            ty = tx[_Y]; tx = tx[_X];
-        }
-        tt.x = tx;
-        tt.y = ty;
-        this.dirty = true;
-        return this;
-    },
-    
-    /**
-     * @private
-     */
-    _applyTransforms : function _applyTransforms(){
-        var t = this.transform, tfns = [];
-        
-        if (t.rotate !== 0)
-            tfns.push('rotate('+t.rotate+'rad)');
-        
-        if (!this.useCanvasScaling && (t.scale.x !== 1 || t.scale.y !== 1))
-            tfns.push('scale('+t.scale.x+','+t.scale.y+')');
-        
-        var trans = (tfns.length ? tfns.join(' ') : 'none')
-        ,   o = this._origin.toUnits('px')
-        ,   origin = o.x+' '+o.y ;
-        this.layer.css(['', '-moz-', '-webkit-']
-            .reduce(
-                function(values, prefix){
-                    values[prefix+'transform'] = trans;
-                    values[prefix+'transform-origin'] = origin;
-                    return values;
-                },
-                {}) );
-        return this;
-    },
-    
-    
-    
-    
-    
-    
-    
-    /// Iterators ///
-    
-    invoke : function invoke(name){
-        var args = Y(arguments,1);
-        // this[name].apply(this, args);
-        // this.children.invoke.apply(this.children, ['invoke', name].concat(args));
-        return this._invoke(name, args);
-    },
-    _invoke : function _invoke(name, args){
-        this[name].apply(this, args);
-        this.children.invoke('_invoke', name, args);
-        return this;
-    },
-    
-    setAll : function setAll(k,v){
-        this[k] = v;
-        this.children.invoke('setAll', k,v);
-        return this;
-    },
-    
-    /**
-     * Reduce "up", across this and parents, inner to outer:
-     *      acc = fn.call(context || node, acc, node)
-     */
-    reduceup : function reduceup(fn, acc, context){
-        acc = fn.call(context || this, acc, this);
-        return ( this.parent ? this.parent.reduceup(fn, acc, context) : acc );
-    },
-    
-    /**
-     * Reduce "down", across this and children, depth-first:
-     *      acc = fn.call(context || node, acc, node)
-     */
-    reduce : function reduce(fn, acc, context){
-        acc = fn.call(context || this, acc, this);
-        return this.children.reduce(function(acc, child){
-            return child.reduce(fn, acc, context);
-        }, acc);
-    },
-    
-    
-    
-    
-    
-    
-    /// Drawing Functions ///
-    
-    /**
-     * @param {CanvasDrawingContext2D} [ctx=this.ctx] Forces context to use rather than the layer's own.
-     * @param {Boolean} [force=false] Forces redraw.
-     */
-    draw : function draw(ctx, force){
-        var _ctx = ctx || this.ctx;
-        
-        if ( this.dirty || force ){
-            this.dirty = false;
-            this._openPath(_ctx);
-            this.render(_ctx);
-            this._closePath(_ctx);
-            if (_ctx) this._erased.forEach(this._erase, this);
-        }
-        
-        this.children.invoke('draw', ctx, force);
-        return this;
-    },
-    
-    _openPath : function _openPath(ctx){
-        if (!ctx) return this;
-        
-        var self = this
-        ,   neg = this.negBleed
-        ,   t = this.transform
-        ,   w = this.canvasWidth, h = this.canvasHeight ;
-        
-        ctx.beginPath();
-        
-        // TODO: only alwaysClearAttrs should reset transforms?
-        // if (this.alwaysClearAttrs)
-            ctx.setTransform(1,0,0,1,0,0);
-        
-        if (this.alwaysClearDrawing)
-            ctx.clearRect(-w,-h, 2*w,2*h);
-        
-        // if (this.alwaysClearAttrs) {
-        if (this.useCanvasScaling && (t.scale.x !== 1 || t.scale.y !== 1))
-            ctx.scale(t.scale.x,t.scale.y);
-        
-        ctx.translate(neg.x, neg.y);
-        ctx.translate(t.translate.x, t.translate.y);
-        // }
-        
-        // Set context attributes
-        CONTEXT_ATTRS.forEach(function(name){
-            if (this[name] !== undefined)
-                ctx[name] = this[name];
-            else if (this.alwaysClearAttrs)
-                delete ctx[name];
-        }, this);
-        
-        return this;
-    },
-    
-    /** To be implemented by subclasses. */
-    render : function render(ctx){ return this; },
-    
-    _closePath : function _closePath(ctx){
-        if (ctx) ctx.closePath();
-        return this;
-    },
-    
-    erase : function erase(x,y, w,h, alsoChildren){
-        this._erased.push({ 'x':x,'y':y, 'w':w,'h':h, 'alsoChildren':alsoChildren });
-        this.dirty = true;
-        if (alsoChildren)
-            this.children.invoke('erase', x,y, w,h, alsoChildren);
-        return this;
-    },
-    
-    _erase : function _erase(args){
-        var x = args.x, y = args.y
-        ,   w = args.w, h = args.h;
-        
-        if (w < 0) w = this.canvas.width()  + w;
-        if (h < 0) h = this.canvas.height() + h;
-        
-        this.ctx.beginPath();
-        this.ctx.clearRect(x,y, w,h);
-        this.ctx.closePath();
-    },
-    
-    /**
-     * Clears this layer and optionally all children.
-     */
-    clear : function clear(alsoChildren, ctx){
-        ctx = ctx || this.ctx;
-        
-        if (ctx) {
-            var w = this.canvas.width()
-            ,   h = this.canvas.height();
-            ctx.beginPath();
-            ctx.setTransform(1,0,0,1,0,0);
-            ctx.clearRect(-w,-h, 2*w,2*h);
-            ctx.closePath();
-        }
-        
-        if (alsoChildren)
-            this.children.invoke('clear');
-        
-        return this;
-    },
-    
-    /**
-     * @param {Number} duration Duration (ms) of animation.
-     * @param {Object} props Object whose keys are the property-names to animate
-     *  paired with the target value. Animated properties can also be relative.
-     *  If a value is supplied with a leading += or -= sequence of characters,
-     *  then the target value is computed by adding or subtracting the given
-     *  number from the current value of the property.
-     * @param {Object} [options] Animation options:
-     *      * {Number} [delay=0] Milliseconds to wait before beginning animation.
-     *      * {Boolean} [queue=true] Indicates whether to place the animation 
-     *          in the effects queue to run when other animations have finished.
-     *          If false, the animation will begin after `delay` milliseconds;
-     *          likewise, if `delay` is non-zero `queue` defaults to `false`
-     *          instead.
-     *      * {String|Function|Object} [easing='linear'] Name of easing function
-     *          used to calculate each step value, or a function, or a map of
-     *          properties to either of the above.
-     *      * {Function} [complete] Function called on animation completion.
-     *      * {Function} [step] Function called after each step of the animation.
-     * @param {Float} [now] Time (ms) to use as the start-time. If omitted, the
-     *  current time is used.
-     */
-    animate : function animate(duration, props, options, now) {
-        var A = new Animation(this, duration, props, options);
-        if ( A.options.queue && this.animActive.length )
-            this.animQueue.push(A);
-        else
-            this.animActive.push(A.start(now || new Date().getTime()));
-        return this;
-    },
-    
-    tick : function tick(elapsed, now){
-        if (elapsed instanceof Event) {
-            var d = evt.data;
-            now      = d.now;
-            elapsed  = d.elapsed;
-        }
-        
-        this.animActive = this.animActive.filter(function(anim){
-            return anim.tick(elapsed, now);
-        });
-        
-        var childrenRunning = this.children.invoke('tick', elapsed, now).some(op.I)
-        ,   running = !!this.animActive.length;
-        if ( !running && this.animQueue.length )
-            this.animActive.push( this.animQueue.shift().start(now) );
-        
-        return childrenRunning || running;
-    },
-    
-    /// Debuggging ///
-    
-    // for debugging
-    point : function point(x,y, color){
-        var ctx = this.ctx;
-        // this._openPath(ctx);
-        
-        var r = 2;
-        ctx.beginPath();
-        ctx.arc(x,y, r, 0, Math.PI*2, false);
-        ctx.fillStyle = color || '#FFFFFF';
-        ctx.fill();
-        ctx.closePath();
-        
-        // this._closePath(ctx);
-        return this;
-    },
-    
-    title : function title(val){
-        if (val === undefined)
-            return this.layer.attr('title');
-        
-        this.layer.attr('title', val);
-        return this;
-    },
-    
-    
-    
-    /// Misc ///
-    toString : function(){
-        var pos = ((this.layer.length && this.layer.parent()[0]) ? this.position() : {top:NaN, left:NaN});
-        return this.className+'['+pos.left+','+pos.top+']( children='+this.children.size()+' )';
     }
-});
-
-function makeDelegate(name, dirties, prop){
-    prop = prop || 'layer';
-    return function delegate(){
-        if (dirties && arguments.length)
-            this.dirty = true;
-        
-        var target = this[prop]
-        ,   result = target[name].apply(target, arguments);
-        
-        return (result !== target ? result : this);
-    };
-}
-
-// Install CSS styles
-$(function(){
-    $('<style />')
-        .text([
-            '.ezl.layer { position:absolute; z-index:1; top:0; left:0; line-height:0; }',
-            // '.ezl.layer { outline:1px solid #000000; }',
-            '.ezl.layer canvas { z-index:0; }'
-        ].join('\n'))
-        .appendTo('head');
-});
+    
+})
+;
diff --git a/src/ezl/layer/layerable.cjs b/src/ezl/layer/layerable.cjs
new file mode 100644 (file)
index 0000000..85141a6
--- /dev/null
@@ -0,0 +1,766 @@
+//#ensure "jquery"
+var Y           = require('Y').Y
+,   op          = require('Y/op')
+
+,   Mixin         = require('evt').Mixin
+
+,   Vec         = require('ezl/math/vec').Vec
+,   Loc         = require('ezl/loc/loc').Loc
+,   BoundingBox = require('ezl/loc/boundingbox').BoundingBox
+,   Animation   = require('ezl/loop/fx').Animation
+, 
+
+CONTEXT_ATTRS  = Y('globalAlpha globalCompositeOperation strokeStyle fillStyle lineWidth lineCap lineJoin miterLimit shadowOffsetX shadowOffsetY shadowBlur shadowColor'.split(' ')),
+FAUX_ACCESSORS = Y('width height position stroke fill origin rotate scale translate title'.split(' '))
+, _X = 0, _Y = 1
+,
+
+
+
+Layerable =
+exports['Layerable'] =
+Mixin.subclass('Layerable', {
+    __bind__ : [ 'tick' ],
+    __layer__ : true,
+    
+    /// Class Defaults ///
+    
+    // _layer* is applied to this.layer (the DOM Element)
+    _layerHtml  : '<div/>',     // HTML that will become the layer
+    _layerId    : null,         // HTML id attribute
+    _layerAttrs : null,         // HTML attributes
+    _layerClasses : 'ezl layer',// CSS classes
+    
+    originX : 0,
+    originY : 0,
+    
+    hasCanvas          : true,  // Whether to create a canvas
+    useCanvasScaling   : false, // Default to CSS3 scaling
+    alwaysClearDrawing : true,  // Whether to clear the canvas content before redraw
+    alwaysClearAttrs   : false, // Whether to remove all canvas attributes (CONTEXT_ATTRS)
+                                // and transforms (scale, rotate, translate) and reset defaults before redraw
+    
+    
+    
+    /// State ///
+    
+    canvas     : null,
+    parent     : null,
+    children   : null,
+    
+    dirty      : true,
+    ctx        : null,
+    animActive : null,
+    animQueue  : null,
+    _erased    : null,
+    
+    layerWidth  : 0,  canvasWidth  : 0, realWidth  : 0,
+    layerHeight : 0,  canvasHeight : 0, realHeight : 0,
+    
+    x: 0, y: 0, loc : null, // Position relative to parent
+    
+    // Bleeds are marks outside the declared layer-size
+    negBleed : null, // Loc
+    posBleed : null, // Loc
+    
+    // Transforms
+    _origin : null, // rotational origin
+    transform : null, // Object
+    
+    
+    /// Setup ///
+    
+    setup : function setupLayer(props, attrs, html){
+        if (props !== undefined && props !== null) {
+            switch (typeof props) {
+                case 'boolean' : this.hasCanvas = props;        break;
+                case 'string'  : this._layerHtml = props;       break;
+                case 'object'  : Y.core.extend(this, props);    break;
+            }
+        }
+        
+        this.children   = new Y.YArray();
+        this.animActive = new Y.YArray();
+        this.animQueue  = new Y.YArray();
+        this._erased    = new Y.YArray();
+        
+        this.loc      = new Loc(0,0);
+        this.negBleed = new Loc(0,0);
+        this.posBleed = new Loc(0,0);
+        
+        this.bbox = new BoundingBox(0,0, 0,0, this.originX,this.originY);
+        this._origin = this.bbox.origin;
+        
+        this.transform = {
+            rotate    : 0,
+            scale     : new Loc(1.0,1.0),
+            translate : new Loc(0,0) // translates canvas
+        };
+        
+        this.layer = jQuery(html || this._layerHtml)
+            .addClass(this._layerClasses)
+            .data('layer', this);
+        this.layer[0].layer = this;
+        
+        if (this._layerAttrs)   this.layer.attr(this._layerAttrs);
+        if (this._layerId)      this.layer.attr('id', this._layerId);
+        if (attrs)              this.layer.attr(attrs);
+        
+        if (this.hasCanvas) {
+            this.canvas = jQuery('<canvas />')
+                .appendTo(this.layer);
+            this.ctx = this.canvas[0].getContext('2d');
+            this.canvas[0].layer = this;
+        } else {
+            this.canvas = jQuery();
+        }
+        
+        // this.tick = this.tick.bind(this);
+    },
+    
+    /// Scene Graph Heirarchy ///
+    
+    /** 
+     * @param {Layerable} child
+     * @return {this}
+     */
+    append : function append(child){
+        new Y(arguments).invoke('appendTo', this);
+        return this;
+    },
+    
+    /** 
+     * @param {Layerable} parent
+     * @return {this}
+     */
+    appendTo : function appendTo(parent){
+        if (!parent) return this;
+        
+        // Always ensure we detach from the DOM and redraw this node
+        this.remove();
+        this.dirty = true;
+        
+        if ( !parent.__layer__ && parent.shape && parent.shape.__layer__ )
+            parent = parent.shape;
+        
+        // Layer? Add self as new child, fix DOM
+        if ( parent.__layer__ ) {
+            this.parent = parent;
+            parent.children.push(this);
+            parent.layer.append(this.layer);
+            
+        // Otherwise: Attach to a DOM node as a new root layer node, leave parent null
+        } else
+            $(parent).append(this.layer);
+        
+        return this;
+    },
+    
+    /**
+     * @param {Layerable} child
+     * @return {this}
+     */
+    prepend : function prepend(child){
+        new Y(arguments).invoke('prependTo', this);
+        return this;
+    },
+    
+    /** 
+     * @param {Layerable} parent
+     * @return {this}
+     */
+    prependTo : function prependTo(parent){
+        if (!parent) return this;
+        
+        // Always ensure we detach from the DOM and redraw this node
+        this.remove();
+        this.dirty = true;
+        
+        if ( !parent.__layer__ && parent.shape && parent.shape.__layer__ )
+            parent = parent.shape;
+        
+        // Layer? Add self as new child, fix DOM
+        if ( parent.__layer__ ) {
+            this.parent = parent;
+            parent.children.unshift(this);
+            parent.layer.prepend(this.layer);
+            
+        // Otherwise: Attach to a DOM node as a new root layer node, leave parent null
+        } else
+            $(parent).prepend(this.layer);
+        
+        return this;
+    },
+    
+    /**
+     * Removes this layer from its parent and the DOM.
+     */
+    remove : function remove(){
+        if (this.parent)
+            this.parent.children.remove(this);
+        this.parent = null;
+        this.layer.remove();
+        return this;
+    },
+    
+    /**
+     * Clears this layer and destroys all children.
+     */
+    empty : function empty(ctx){
+        this.children.invoke('remove');
+        this.clear(false, ctx);
+        return this;
+    },
+    
+    /**
+     * @return The root of the scene graph.
+     */
+    root : function root(){
+        if (this.parent)
+            return this.parent.root();
+        else
+            return this;
+    },
+    
+    
+    
+    
+    
+    /// Attributes ///
+    
+    // set : function set(key, value, def){
+    //     if ( key === undefined || (value === undefined && def === undefined) )
+    //         return this;
+    //     
+    //     value = (value !== undefined ? value : def);
+    //     if ( FAUX_ACCESSORS.has(key) )
+    //         this[key](value);
+    //     else
+    //         this[key] = value;
+    //     
+    //     return this;
+    // },
+    
+    attr : Y.attr.methodize(),
+    
+    size : function size(w,h){
+        if (w === undefined && h === undefined)
+            return new Vec(this.layerWidth,this.layerHeight);
+        
+        if (w === null) w = this.realWidth;
+        if (h === null) h = this.realHeight;
+        
+        this.realWidth  = w;
+        this.realHeight = h;
+        
+        // HTMLElement.{width,height} is a long
+        this.layerWidth  = Math.round(w);
+        this.layerHeight = Math.round(h);
+        
+        var bb = this.bbox.resize(w,h)
+        ,   nb = this.negBleed, pb = this.posBleed
+        
+        ,   cw = this.canvasWidth  = Math.ceil(w + nb.x + pb.x)
+        ,   ch = this.canvasHeight = Math.ceil(h + nb.y + pb.y)
+        ;
+        
+        this.layer.css({
+            'width' : w,            'height' : h,
+            'left'  : bb.x1,        'top' : bb.y1
+        });
+        this.canvas.css({
+            'width' : cw,           'height' : ch,
+            'margin-left' : -nb.x,  'margin-top' : -nb.y
+        });
+        var el = this.canvas[0];
+        if (el) {
+            el.width  = cw;
+            el.height = ch;
+        }
+        
+        return this;
+    },
+    
+    /**
+     * Changes the layer's width and then updates the canvas.
+     */
+    width : function width(w){
+        if (w === undefined)
+            return this.layerWidth;
+        
+        this.layerWidth = w;
+        
+        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.layer.css({
+            'width'       : w,
+            'left'        : bb.x1,
+            // 'margin-left' : -ro.x
+        });
+        
+        this.canvas.css({
+            'width'       : cw,
+            'margin-left' : -nb.x
+        });
+        if (this.canvas.length) this.canvas[0].width = cw;
+        
+        return this;
+    },
+    
+    height : function height(h){
+        if (h === undefined)
+            return this.layerHeight;
+        
+        this.layerHeight = 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
+        
+        this.layer.css({
+            'height'     : h,
+            'top'        : bb.y1,
+            // 'margin-top' : -ro.y
+        });
+        
+        this.canvas.css({
+            'height'     : ch,
+            'margin-top' : -nb.y
+        });
+        if (this.canvas.length) this.canvas[0].height = ch;
+        
+        return this;
+    },
+    
+    /**
+     * position() -> {Loc}
+     * Gets position of the layer relative to the parent.
+     * @return {Loc}
+     */
+    /**
+     * position(x,y) -> {this}
+     * Sets the position of this node, and then returns it.
+     * @param {Number|String|undefined} x
+     * @param {Number|String|undefined} y If omitted, this method must be invoked with `undefined` as the first argument.
+     * @return {this}
+     */
+    /**
+     * position(pos) -> {this}
+     * Sets the position of this node, and then returns it.
+     * @param {Object} pos An object with "x" and/or "y" properties as in `position(x,y)`.
+     * @return {this}
+     */
+    position : function position(x,y){
+        if (x === undefined && y === undefined)
+            return this.layer.position();
+        
+        if ( x instanceof Array ) {
+            y = x[_Y]; x = x[_X];
+        } else if ( Y.isPlainObject(x) ){
+            y = ('top'  in x ? x.top  : x.y);
+            x = ('left' in x ? x.left : x.x);
+        }
+        
+        var bbox = this.bbox.relocate(x,y);
+        this.css({
+            'left' : bbox.x1,
+            'top'  : bbox.y1
+        });
+        return this;
+    },
+    
+    stroke : function stroke(style, width){
+        if (style === undefined && width === undefined)
+            return this.strokeStyle;
+        
+        if (width !== undefined)
+            this.lineWidth = width;
+        
+        this.dirty = true;
+        this.strokeStyle = style;
+        return this;
+    },
+    
+    fill : function fill(style){
+        if (style === undefined)
+            return this.fillStyle;
+        
+        this.dirty = true;
+        this.fillStyle = style;
+        return this;
+    },
+    
+    hide : makeDelegate('hide'),
+    show : makeDelegate('show'),
+    toggle : makeDelegate('toggle'),
+    
+    /** CSS properties */
+    css : makeDelegate('css'),
+    
+    /** Position relative to document. */
+    offset : makeDelegate('offset'),
+    
+    
+    
+    /// Transformations ///
+    
+    /**
+     * Gets and sets the origin-location for this shape, used for 
+     * position and rotation. Defaults to the midpoint.
+     */
+    origin : function origin(x,y){
+        var o = this._origin;
+        if (arguments.length === 0)
+            return o.absolute(this.layerWidth, this.layerHeight);
+        
+        if ( x instanceof Array ) {
+            y = x[_Y]; x = x[_X];
+        }
+        o.x = x;
+        o.y = y;
+        return this._applyTransforms();
+    },
+    
+    /**
+     * Rotates this layer by r radians.
+     */
+    rotate : function rotate(r){
+        var t = this.transform;
+        if (r === undefined)
+            return t.rotate;
+        
+        t.rotate = r;
+        return this._applyTransforms();
+    },
+    
+    /**
+     * Scales this layer by (sx,sy), and then applies scaling relatively
+     * to all sublayers (preserving knowledge of their individual scaling).
+     */
+    scale : function scale(sx,sy){
+        var ts = this.transform.scale;
+        if (arguments.length === 0)
+            return ts;
+        
+        if ( sx instanceof Array ) {
+            sy = sx[_Y]; sx = sx[_X];
+        }
+        ts.x = sx;
+        ts.y = sy;
+        this.dirty = true;
+        return this._applyTransforms();
+    },
+    
+    /**
+     * Translates draw calls by (x,y) within this layer only. This allows you to
+     * functionally move the origin of the coordinate system for this layer.
+     */
+    translate : function translate(tx,ty){
+        var tt = this.transform.translate;
+        if (arguments.length === 0)
+            return tt;
+        
+        if ( tx instanceof Array ) {
+            ty = tx[_Y]; tx = tx[_X];
+        }
+        tt.x = tx;
+        tt.y = ty;
+        this.dirty = true;
+        return this;
+    },
+    
+    /**
+     * @private
+     */
+    _applyTransforms : function _applyTransforms(){
+        var t = this.transform, tfns = [];
+        
+        if (t.rotate !== 0)
+            tfns.push('rotate('+t.rotate+'rad)');
+        
+        if (!this.useCanvasScaling && (t.scale.x !== 1 || t.scale.y !== 1))
+            tfns.push('scale('+t.scale.x+','+t.scale.y+')');
+        
+        var trans = (tfns.length ? tfns.join(' ') : 'none')
+        ,   o = this._origin.toUnits('px')
+        ,   origin = o.x+' '+o.y ;
+        this.layer.css(['', '-moz-', '-webkit-']
+            .reduce(
+                function(values, prefix){
+                    values[prefix+'transform'] = trans;
+                    values[prefix+'transform-origin'] = origin;
+                    return values;
+                },
+                {}) );
+        return this;
+    },
+    
+    
+    
+    
+    
+    
+    
+    /// Iterators ///
+    
+    invoke : function invoke(name){
+        var args = Y(arguments,1);
+        // this[name].apply(this, args);
+        // this.children.invoke.apply(this.children, ['invoke', name].concat(args));
+        return this._invoke(name, args);
+    },
+    _invoke : function _invoke(name, args){
+        this[name].apply(this, args);
+        this.children.invoke('_invoke', name, args);
+        return this;
+    },
+    
+    setAll : function setAll(k,v){
+        this[k] = v;
+        this.children.invoke('setAll', k,v);
+        return this;
+    },
+    
+    /**
+     * Reduce "up", across this and parents, inner to outer:
+     *      acc = fn.call(context || node, acc, node)
+     */
+    reduceup : function reduceup(fn, acc, context){
+        acc = fn.call(context || this, acc, this);
+        return ( this.parent ? this.parent.reduceup(fn, acc, context) : acc );
+    },
+    
+    /**
+     * Reduce "down", across this and children, depth-first:
+     *      acc = fn.call(context || node, acc, node)
+     */
+    reduce : function reduce(fn, acc, context){
+        acc = fn.call(context || this, acc, this);
+        return this.children.reduce(function(acc, child){
+            return child.reduce(fn, acc, context);
+        }, acc);
+    },
+    
+    
+    
+    
+    
+    
+    /// Drawing Functions ///
+    
+    /**
+     * @param {CanvasDrawingContext2D} [ctx=this.ctx] Forces context to use rather than the layer's own.
+     * @param {Boolean} [force=false] Forces redraw.
+     */
+    draw : function draw(ctx, force){
+        var _ctx = ctx || this.ctx;
+        
+        if ( this.dirty || force ){
+            this.dirty = false;
+            this._openPath(_ctx);
+            this.render(_ctx);
+            this._closePath(_ctx);
+            if (_ctx) this._erased.forEach(this._erase, this);
+        }
+        
+        this.children.invoke('draw', ctx, force);
+        return this;
+    },
+    
+    _openPath : function _openPath(ctx){
+        if (!ctx) return this;
+        
+        var self = this
+        ,   neg = this.negBleed
+        ,   t = this.transform
+        ,   w = this.canvasWidth, h = this.canvasHeight ;
+        
+        ctx.beginPath();
+        
+        // TODO: only alwaysClearAttrs should reset transforms?
+        // if (this.alwaysClearAttrs)
+            ctx.setTransform(1,0,0,1,0,0);
+        
+        if (this.alwaysClearDrawing)
+            ctx.clearRect(-w,-h, 2*w,2*h);
+        
+        // if (this.alwaysClearAttrs) {
+        if (this.useCanvasScaling && (t.scale.x !== 1 || t.scale.y !== 1))
+            ctx.scale(t.scale.x,t.scale.y);
+        
+        ctx.translate(neg.x, neg.y);
+        ctx.translate(t.translate.x, t.translate.y);
+        // }
+        
+        // Set context attributes
+        CONTEXT_ATTRS.forEach(function(name){
+            if (this[name] !== undefined)
+                ctx[name] = this[name];
+            else if (this.alwaysClearAttrs)
+                delete ctx[name];
+        }, this);
+        
+        return this;
+    },
+    
+    /** To be implemented by subclasses. */
+    render : function render(ctx){ return this; },
+    
+    _closePath : function _closePath(ctx){
+        if (ctx) ctx.closePath();
+        return this;
+    },
+    
+    erase : function erase(x,y, w,h, alsoChildren){
+        this._erased.push({ 'x':x,'y':y, 'w':w,'h':h, 'alsoChildren':alsoChildren });
+        this.dirty = true;
+        if (alsoChildren)
+            this.children.invoke('erase', x,y, w,h, alsoChildren);
+        return this;
+    },
+    
+    _erase : function _erase(args){
+        var x = args.x, y = args.y
+        ,   w = args.w, h = args.h;
+        
+        if (w < 0) w = this.canvas.width()  + w;
+        if (h < 0) h = this.canvas.height() + h;
+        
+        this.ctx.beginPath();
+        this.ctx.clearRect(x,y, w,h);
+        this.ctx.closePath();
+    },
+    
+    /**
+     * Clears this layer and optionally all children.
+     */
+    clear : function clear(alsoChildren, ctx){
+        ctx = ctx || this.ctx;
+        
+        if (ctx) {
+            var w = this.canvas.width()
+            ,   h = this.canvas.height();
+            ctx.beginPath();
+            ctx.setTransform(1,0,0,1,0,0);
+            ctx.clearRect(-w,-h, 2*w,2*h);
+            ctx.closePath();
+        }
+        
+        if (alsoChildren)
+            this.children.invoke('clear');
+        
+        return this;
+    },
+    
+    /**
+     * @param {Number} duration Duration (ms) of animation.
+     * @param {Object} props Object whose keys are the property-names to animate
+     *  paired with the target value. Animated properties can also be relative.
+     *  If a value is supplied with a leading += or -= sequence of characters,
+     *  then the target value is computed by adding or subtracting the given
+     *  number from the current value of the property.
+     * @param {Object} [options] Animation options:
+     *      * {Number} [delay=0] Milliseconds to wait before beginning animation.
+     *      * {Boolean} [queue=true] Indicates whether to place the animation 
+     *          in the effects queue to run when other animations have finished.
+     *          If false, the animation will begin after `delay` milliseconds;
+     *          likewise, if `delay` is non-zero `queue` defaults to `false`
+     *          instead.
+     *      * {String|Function|Object} [easing='linear'] Name of easing function
+     *          used to calculate each step value, or a function, or a map of
+     *          properties to either of the above.
+     *      * {Function} [complete] Function called on animation completion.
+     *      * {Function} [step] Function called after each step of the animation.
+     * @param {Float} [now] Time (ms) to use as the start-time. If omitted, the
+     *  current time is used.
+     */
+    animate : function animate(duration, props, options, now) {
+        var A = new Animation(this, duration, props, options);
+        if ( A.options.queue && this.animActive.length )
+            this.animQueue.push(A);
+        else
+            this.animActive.push(A.start(now || new Date().getTime()));
+        return this;
+    },
+    
+    tick : function tick(elapsed, now){
+        if (elapsed instanceof Event) {
+            var d = evt.data;
+            now      = d.now;
+            elapsed  = d.elapsed;
+        }
+        
+        this.animActive = this.animActive.filter(function(anim){
+            return anim.tick(elapsed, now);
+        });
+        
+        var childrenRunning = this.children.invoke('tick', elapsed, now).some(op.I)
+        ,   running = !!this.animActive.length;
+        if ( !running && this.animQueue.length )
+            this.animActive.push( this.animQueue.shift().start(now) );
+        
+        return childrenRunning || running;
+    },
+    
+    /// Debuggging ///
+    
+    // for debugging
+    point : function point(x,y, color){
+        var ctx = this.ctx;
+        // this._openPath(ctx);
+        
+        var r = 2;
+        ctx.beginPath();
+        ctx.arc(x,y, r, 0, Math.PI*2, false);
+        ctx.fillStyle = color || '#FFFFFF';
+        ctx.fill();
+        ctx.closePath();
+        
+        // this._closePath(ctx);
+        return this;
+    },
+    
+    title : function title(val){
+        if (val === undefined)
+            return this.layer.attr('title');
+        
+        this.layer.attr('title', val);
+        return this;
+    },
+    
+    
+    
+    /// Misc ///
+    toString : function(){
+        var pos = ((this.layer.length && this.layer.parent()[0]) ? this.position() : {top:NaN, left:NaN});
+        return this.className+'['+pos.left+','+pos.top+']( children='+this.children.size()+' )';
+    }
+});
+
+function makeDelegate(name, dirties, prop){
+    prop = prop || 'layer';
+    return function delegate(){
+        if (dirties && arguments.length)
+            this.dirty = true;
+        
+        var target = this[prop]
+        ,   result = target[name].apply(target, arguments);
+        
+        return (result !== target ? result : this);
+    };
+}
+
+// Install CSS styles
+$(function(){
+    $('<style />')
+        .text([
+            '.ezl.layer { position:absolute; z-index:1; top:0; left:0; line-height:0; }',
+            // '.ezl.layer { outline:1px solid #000000; }',
+            '.ezl.layer canvas { z-index:0; }'
+        ].join('\n'))
+        .appendTo('head');
+});
index 5ca3037..2fe1856 100644 (file)
@@ -5,6 +5,8 @@
 var Y         = require('Y').Y
 ,   Event     = require('Y/modules/y.event').Event
 
+,   evt       = require('evt')
+
 ,   EventLoop = require('ezl/loop').EventLoop
 ,   Layer     = require('ezl/layer/layer').Layer
 ,   Circle    = require('ezl/shape').Circle
@@ -25,7 +27,7 @@ var Y         = require('Y').Y
 
 Game =
 exports['Game'] = 
-Y.subclass('Game', {
+evt.subclass('Game', {
     // Config
     gameoverDelay : null,
     
@@ -36,7 +38,7 @@ Y.subclass('Game', {
     playerAlign : 1,
     
     
-    init : function initGame(levelId, replayFile){
+    init : function initGame(levelId){
         Y.bindAll(this);
         
         if (levelId) this.levelId = levelId;
@@ -72,27 +74,26 @@ Y.subclass('Game', {
         this.viewport.append(this.map.ui);
         
         // automatically keep track of units
+        Thing.on('create',  this.setGame);
         Thing.on('created', this.addUnit);
         Thing.on('destroy', this.killUnit);
-        this.on('tick', this.tick);
+        this.on('tick',     this.tick);
         
         this.level.setup();
-        if (this.player) {
-            this.backpack = new Backpack(this.player)
-                    .appendTo( this.root )
-                    .refresh();
-        }
         
-        if (replayFile) {
-            this.isReplay = true;
-            this.loadLog(replayFile);
-        } else
-            this.emit('ready', this);
+        // if (this.player) {
+        //     this.backpack = new Backpack(this.player)
+        //             .appendTo( this.root )
+        //             .refresh();
+        // }
+        
+        this.emit('ready', this);
     },
     
     destroy : function destroy(){
         this.stop();
-        this.player.destroy();
+        if (this.player) this.player.destroy();
+        Thing.removeListener('create',  this.noteUnit);
         Thing.removeListener('created', this.addUnit);
         Thing.removeListener('destroy', this.killUnit);
         this.root.remove();
@@ -109,7 +110,6 @@ Y.subclass('Game', {
     
     draw : function draw(){
         this.root.draw();
-        // this.backpack.draw();
     },
     
     /**
@@ -180,7 +180,18 @@ Y.subclass('Game', {
         return animation;
     },
     
+    setGame : function setGame(unit){
+        if (unit instanceof Event)
+            unit = unit.data.instance;
+        
+        unit.game = this;
+        return unit;
+    },
+    
     noteUnit : function noteUnit(unit){
+        if (unit instanceof Event)
+            unit = unit.trigger;
+        
         unit.game = this;
         if (unit.dead) return unit;
         
index 9475af0..98aa278 100644 (file)
@@ -15,9 +15,10 @@ var undefined
 ,   FRAME_RATE    = 30
 ,   MS_PER_FRAME  = 1000 / FRAME_RATE
 
-,   NOW           = 0                    // Current tick's timestamp (ms)
-,   ELAPSED       = MS_PER_FRAME         // Time (ms) since previous tick
-,   TICKS         = 0                    // Ticks since start of game
+,   NOW           = 0                   // Current tick's timestamp (ms)
+,   ELAPSED       = MS_PER_FRAME        // Time (ms) since previous tick
+,   TICKS         = 0                   // Ticks since start of game
+,   DROPPED       = 0                   // Frames dropped since last tick
 
 
 /// Common Components of Computation ///
index ff8dc2e..45a6a5d 100644 (file)
@@ -14,6 +14,7 @@ tanks = {
     'effects'   : require('tanks/effects'),
     'fx'        : require('tanks/fx'),
     'mixins'    : require('tanks/mixins'),
+    'inventory' : require('tanks/inventory'),
     'map'       : require('tanks/map'),
     'thing'     : require('tanks/thing'),
     'ui'        : require('tanks/ui'),
index e69de29..bafd675 100644 (file)
@@ -0,0 +1,79 @@
+var Y   = require('Y')
+
+,   evt = require('evt')
+
+,   Item      = require('tanks/thing/item').Item
+,   Container = require('tanks/inventory/container').Container
+,   ContainerUI = require('tanks/ui/inventory/containerui').ContainerUI
+,
+
+Bag =
+exports['Bag'] =
+Item.subclass('Bag', {
+    __mixins__ : [ Container ],
+    __bind__   : [ 'onAcquired', 'onLost' ],
+    
+    /// Setup ///
+    hasUI : true,
+    highlightIconOnOpen : true, // Whether to highlight the inventory icon when bag is open
+    reqs : null,
+    
+    /// Bookkeeping ///
+    bagName : null,
+    ui : null, // ContainerUI
+    
+    
+    init : function initBag(bagName, owner){
+        this.bagName = bagName;
+        this.owner = owner;
+        Item.init.call(this);
+    },
+    
+    
+    /**
+     * @return {this}
+     */
+    showUI : function showUI(){
+        if (!this.hasUI)
+            return this;
+        if (!this.ui)
+            this.ui = new ContainerUI(this.owner, this, { 'name':this.bagName })
+                .appendTo(this.owner.game.root)
+                .refresh();
+        this.ui.show();
+        return this;
+    },
+    
+    hideUI : function hideUI(){
+        if (this.ui)
+            this.ui.hide();
+        return this;
+    },
+    
+    activate : function activate(){
+        if (this.ui && this.owner)
+            this.ui.toggle();
+        return this;
+    },
+    
+    onAcquired : function onAcquired(evt, container){
+        Item.fn.onAcquired.apply(this, arguments);
+        // this.parent = container;
+        this.showUI();
+    },
+    
+    onLost : function onLost(evt){
+        var lostItem = evt.data.item
+        ,   unit = this.owner
+        ;
+        // Bags are Items AND Containers, and are thus sent notifications when
+        // their contents change.
+        if (lostItem !== this)
+            return;
+        
+        this.hideUI();
+        Item.fn.onLost.apply(this, arguments);
+    }
+    
+})
+;
index e69de29..609b163 100644 (file)
@@ -0,0 +1,34 @@
+var Y = require('Y')
+,   evt = require('evt')
+,   Bag = require('tanks/inventory/bag').Bag
+,
+
+/**
+ * Special Bag which holds your backpack (and potentially other bags).
+ */
+BagBag =
+exports['BagBag'] =
+Bag.subclass('BagBag', {
+    __mixins__ : [],
+    __bind__   : [],
+    __static__ : {},
+    
+    /// Setup ///
+    hasUI : true,
+    highlightIconOnOpen : true, // Whether to highlight the inventory icon when bag is open
+    reqs : 'bag',
+    
+    /// Bookkeeping ///
+    bagName : 'bagbag',
+    ui : null, // ContainerUI
+    
+    
+    init : function initBagBag(inventory, player){
+        this.inventory = inventory;
+        Bag.init.call(this, 'bagbag', player);
+        this.showUI();
+        if (this.ui) this.ui.layer.addClass('bagbag bag');
+    }
+    
+})
+;
index e69de29..f9b109a 100644 (file)
@@ -0,0 +1,23 @@
+var Y = require('Y')
+,   evt = require('evt')
+,   Bag = require('tanks/inventory/bag').Bag
+,
+
+/**
+ * A virtual container for item shortcuts.
+ */
+Belt =
+exports['Belt'] =
+Bag.subclass('Belt', {
+    __mixins__ : [],
+    __bind__   : [],
+    __static__ : {},
+    
+    
+    init : function initBelt(){
+        Bag.init.call(this);
+        
+    }
+    
+})
+;
index 9ab726d..775be14 100644 (file)
@@ -2,8 +2,8 @@ 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
+,   evt   = require('evt')
+,   Mixin = evt.Mixin
 
 ,   kNull = op.K(null)
 ,
@@ -12,10 +12,10 @@ Container =
 exports['Container'] =
 Mixin.subclass('Container', {
     __bind__   : [],
-    
+    isItemContainer : true, // Instance-of flag
     
     max   : 1,          // Container capacity
-    reqs  : null,       // Array of tag requirements to be stored here
+    reqs  : null,       // Array of tag requirements to be stored here // XXX: 'not(bag)'
     equipsContents : false, // Whether held items are equipped
     
     owner : null,       // Owning unit
@@ -25,19 +25,28 @@ Mixin.subclass('Container', {
     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);
+    onCreate : function initContainer(evt, self){
+        self.items = {};
+        self.children = {};
+        self.slots = new Array(self.max);
         
+        if ( typeof self.reqs == 'string' )
+            self.reqs = Y([ self.reqs ]);
+        else
+            self.reqs = Y(self.reqs);
+    },
+    
+    setup : function setup(owner, items){
         if ( typeof this.reqs == 'string' )
             this.reqs = Y([ this.reqs ]);
+        else
+            this.reqs = Y(this.reqs);
         
+        if (owner) this.owner = owner;
         if (items) items.forEach(this.moveItem, this);
+        return this;
     },
     
-    
     /**
      * @param {Item} [item] If present, also check whether item matches this container's requirements.
      * @return {Boolean} Whether this container will accept this item.
@@ -45,7 +54,7 @@ Mixin.subclass('Container', {
     canAddItem : function canAddItem(item){
         var reqs = this.reqs;
         return this.size < this.max &&
-            ( !item || !reqs || reqs.intersect(item.tags).length === reqs.length );
+            ( !item || !reqs || this.checkReqs(item) );
     },
     
     checkReqs : function checkReqs(item){
@@ -53,14 +62,11 @@ Mixin.subclass('Container', {
         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;
-        }
+    getItem : function getItem(item){
+        if ( typeof item == 'number' ) {
+            return this.slots[ (item < 0 ? this.max+item : item) ];
+        } else
+            return item;
     },
     
     /**
@@ -68,7 +74,7 @@ Mixin.subclass('Container', {
      * @return {Boolean} Whether the item is in this container.
      */
     hasItem : function hasItem(id){
-        if (id instanceof Item) id = id.__id__;
+        if (id && id.isItem) id = id.__id__;
         return !!this.items[id];
     },
     
@@ -78,8 +84,12 @@ Mixin.subclass('Container', {
      * @return {this}
      */
     addItem : function addItem(item){
-        if ( this.hasItem(item) )
+        if ( !item || this.hasItem(item) )
             return this;
+        if ( this.size >= this.max )
+            throw new Error('Insufficient room to add item!');
+        else if ( !this.checkReqs(item) )
+            throw new Error('Item '+item+' not legal in container '+this+'!');
         else
             return this.moveItem(item, this._getEmptySlot());
     },
@@ -178,7 +188,7 @@ Mixin.subclass('Container', {
      * @return {Item} The removed item or null on failure.
      */
     removeItem : function removeItem(item, options){
-        options = options || {};
+        options = Y.isPlainObject(options) ? options : {};
         item = this.getItem(item);
         if (!item)
             return item;
@@ -193,20 +203,25 @@ Mixin.subclass('Container', {
         return item;
     },
     
+    removeAllItems : function removeAllItems(){
+        Y(this.children).invoke('removeAllItems');
+        
+        
+        return this;
+    },
     
     
     /**
      * @protected
-     * @return {Integer} Index of first empty slot in this container.
+     * @param {Item} [item] If present, we first test to ensure container can accept the item.
+     * @return {Integer} Index of first empty slot in this container, or -1 if
+     *  container is full or if item does not meet requirements.
      */
-    _getEmptySlot : function _getEmptySlot(){
-        var slots = this.slots
-        ,   max = this.max;
-        
-        if (this.size >= max)
+    _getEmptySlot : function _getEmptySlot(item){
+        if ( !this.canAddItem(item) )
             return -1;
         
-        for (var i=0, v=slots[i]; i<max; v=slots[++i])
+        for (var slots = this.slots, i=0, v=slots[i]; i<this.max; v=slots[++i])
             if (v === undefined)
                 return i;
         
@@ -236,6 +251,7 @@ Mixin.subclass('Container', {
      * @protected
      * @param {Item} item
      * @param {Number} [idx] Container position at which to insert item. If missing, first open slot will be used.
+     * @param {Boolean} [didDisplace=false] Whether this put displaced another item, implying the size is unchanged.
      * @return {Boolean}
      */
     _putItem : function _putItem(item, idx, didDisplace){
@@ -250,9 +266,8 @@ Mixin.subclass('Container', {
         if ( 0 <= idx && idx < this.max )
             return false;
         
-        // Already in bag? Remove from current slot
         if ( this.items[id] )
-            delete this.slots[inv.idx];
+            delete this.slots[inv.idx]; // Already in bag? Remove from current slot
         else if (!didDisplace)
             this.size++;                // Otherwise increment size
         
@@ -261,6 +276,9 @@ Mixin.subclass('Container', {
         inv.idx = idx;
         inv.bag = this;
         
+        if (item.isItemContainer)
+            this.children[id] = item;
+        
         return true;
     },
     
@@ -270,7 +288,7 @@ Mixin.subclass('Container', {
      * @param {Item} item
      * @return {Boolean}
      */
-    _removeItem : function removeItem(item, oldIdx){
+    _removeItem : function _removeItem(item, oldIdx){
         if (!item) return false;
         
         var id  = item.__id__
@@ -281,6 +299,7 @@ Mixin.subclass('Container', {
         ;
         
         delete this.items[id];
+        delete this.children[id];
         
         if ( hasItem ) {
             this.size--;
@@ -288,6 +307,10 @@ Mixin.subclass('Container', {
                 delete this.slots[idx];
             if (!itemMoved)
                 inv.idx = inv.bag = null;
+            // TODO: When container is lost, notify unit of lost items,
+            // but do not remove them from the container.
+            // if (item.isItemContainer)
+            //     item.removeAllItems();
             return true;
         }
         
@@ -310,14 +333,36 @@ Mixin.subclass('Container', {
     
     
     
-});
+})
+,
+
+ContainerItem =
+exports['ContainerItem'] =
+evt.subclass('ContainerItem', {
+    __mixins__ : [ Container ],
+    
+    max   : 1,              // Container capacity
+    reqs  : null,           // Array of tag requirements to be stored here // XXX: 'not(bag)'
+    equipsContents : false, // Whether held items are equipped
+    
+    
+    init : function initContainer(owner, items){
+        this.setup(owner, items);
+    }
+    
+})
+,
 
-function InventoryMeta(bag, idx, links){
+ContainerMeta =
+exports['ContainerMeta'] =
+function ContainerMeta(idx, bag, links){
     this.bag = bag;
     this.idx = idx;
     this.links = links || [];
 }
-Y.core.extend(InventoryMeta.prototype, {
+;
+
+Y.core.extend(ContainerMeta.prototype, {
     clear : function clear(){
         this.bag = this.idx = null;
         this.links = [];
@@ -325,11 +370,12 @@ Y.core.extend(InventoryMeta.prototype, {
     },
     
     clone : function clone(){
-        return new InventoryMeta(this.bag, this.idx, this.links); // Don't care about links
+        return new ContainerMeta(this.bag, this.idx, this.links); // Don't care about links
     },
     
     toString : function(){
-        return "BagLoc(idx="+this.idx+", bag="+this.bag+")";
+        return "(idx="+this.idx+", bag="+this.bag+")";
     }
     
-})
+});
+
diff --git a/src/tanks/inventory/index.cjs b/src/tanks/inventory/index.cjs
new file mode 100644 (file)
index 0000000..81942ea
--- /dev/null
@@ -0,0 +1,23 @@
+var Y = require('Y').Y
+,   container = require('tanks/inventory/container')
+,   inventory = require('tanks/inventory/inventory')
+,   bag       = require('tanks/inventory/bag')
+,   bagbag    = require('tanks/inventory/bagbag')
+,   belt      = require('tanks/inventory/belt')
+;
+Y.core.extend(exports, {
+    'inventory' : inventory,
+    'Inventory' : inventory.Inventory,
+    
+    'container' : container,
+    'Container' : container.Container,
+    
+    'bag'       : bag,
+    'Bag'       : bag.Bag,
+    
+    'bagbag'    : bagbag,
+    'BagBag'    : bagbag.BagBag,
+    
+    'belt'      : belt,
+    'Belt'      : belt.Belt
+});
index e69de29..10cd7a1 100644 (file)
@@ -0,0 +1,270 @@
+var Y = require('Y').Y
+,   deepcopy = require('Y/types/object').deepcopy
+
+,   evt = require('evt')
+
+,   container     = require('tanks/inventory/container')
+,   Container     = container.Container
+,   ContainerItem = container.ContainerItem
+,   ContainerMeta = container.ContainerMeta
+,   Item          = require('tanks/thing/item').Item
+,   Bag           = require('tanks/inventory/bag').Bag
+,   BagBag        = require('tanks/inventory/bagbag').BagBag
+,
+
+
+Inventory =
+exports['Inventory'] =
+evt.subclass('Inventory', {
+    __mixins__ : [], // Container
+    __bind__   : [],
+    
+    /// Setup ///
+    
+    defaultBag : 'backpack',    // name of default bag for pickups
+    equipSlots : null,          // name -> Container options; implies equipsContents=true
+    bagSlots : 1,               // Number of bags that can be put on the root toolbar
+    beltSlots : 4,              // Number of items that can be hotkeyed to your belt
+    
+    /// Bookkeeping ///
+    
+    owner : null,               // {Unit}
+    belt : null,                // {Belt}               Size determined by beltSlots
+    bagbag : null,              // {BagBag}             Size determined by bagSlots
+    
+    items : {},                 // id -> {Item}         All items in all bags
+    equipment : {},             // name -> {Item}       All equipped items
+    bags : {},                  // id -> {Container}    All bags in all bags
+    namedBags : {},             // name -> {Container}  All named bags in all bags
+    equipBags : {},             // name -> {EquipSlot}  All EquipSlot containers
+    
+    // Y-wrapped
+    _items : null,
+    _bags : null,
+    
+    
+    
+    init : function initInventory(owner){
+        var S = this.settings = deepcopy(evt.aggregate(owner, 'inventory'));
+        Y.core.extend(this, S);
+        this.owner = owner;
+        
+        this.items     = {};
+        this.equipment = {};
+        this.bags      = {};
+        this.namedBags = {};
+        this.equipBags = {};
+        
+        this._items = Y(this.items);
+        this._bags  = Y(this.bags);
+        
+        this.bagbag = new BagBag(this, owner);
+        
+        // instantiate:
+        // - backpack
+        // - equip slots
+        // - belt
+    },
+    
+    
+    /// Item Management ///
+    
+    /**
+     * Tests whether inventory has room for this item (in a container that would accept it).
+     * @param {Item} item
+     * @return {Boolean}
+     */
+    /**
+     * Tests whether specified container would accept the item.
+     * @param {Item} item
+     * @param {Container} bag Container to which to add.
+     * @return {Boolean}
+     */
+    canAddItem : function canAddItem(item, bag){
+        if (bag === undefined)
+            return this._bags.invoke('canAddItem', item).any();
+        bag = this.getBag(bag);
+        return bag && bag.canAddItem(item);
+    },
+    
+    /**
+     * Adds item to the first empty bag (starting with the default bag) that will accept it.
+     * @param {Item} item The item.
+     * @return {this}
+     */
+    /**
+     * Adds item to the selected bag.
+     * @param {Item} item The item.
+     * @param {Container|String|Number} bag Container to add to; a String is acceptable if the Container is named.
+     * @return {this}
+     */
+    addItem : function addItem(item, bag){
+        bag = this.getBag(bag, true);
+        if ( !bag )
+            throw new Error('Cannot add item: unknown bag '+bag+'!');
+        return bag.addItem(item);
+    },
+    
+    /**
+     * @param {Item} item
+     * @param {String|Number|Container} bag Destination container.
+     * @param {Number} [idx] Destination index. If omitted, first empty index will be used.
+     * @return {this}
+     */
+    moveItem : function moveItem(item, bag, idx){
+        if (!item)
+            throw new Error('Cannot move item: no item supplied!');
+        
+        bag = this.getBag(bag);
+        if ( !bag )
+            throw new Error('Cannot move item: unknown bag '+bag+'!');
+        if ( !bag.checkReqs(item) )
+            throw new Error('Cannot move item '+item+' to container '+bag+'!');
+        
+        idx = (idx !== undefined ? idx : bag._getEmptySlot());
+        return bag.moveItem(item, idx);
+    },
+    
+    /**
+     * Removes item from inventory.
+     * @param {Item|Number} item Item to remove (or index of that item in the bag).
+     * @param {Bag|Number|String} [bag] Bag in which to look up the index. If omitted, all bags will be searched.
+     * @return {Item} The removed item or null on failure.
+     */
+    removeItem : function removeItem(item, bag, options){
+        if ( !this.hasItem(item, bag) )
+            return null;
+        item = this.getItem(item, bag);
+        if ( !item )
+            return null;
+            
+        return item.inv.bag.removeItem(item, options);
+        // TODO: Fire event?
+    },
+    
+    /**
+     * @param {Item|Number} item Item or Id
+     * @param {String|Number|Container} [bag] Container to search. If omitted, all bags will be checked.
+     * @return {Boolean}
+     */
+    hasItem : function hasItem(item, bag){
+        if ( item && item.isItem )
+            item = item.__id__;
+        
+        if ( bag === undefined )
+            return !!this.items[item];
+        
+        bag = this.getBag(bag);
+        return bag && bag.hasItem(item);
+    },
+    
+    /**
+     * @param {Number} idx Index into the default bag from which to retrieve the item.
+     * @return {Item} The item at that index.
+     */
+    /**
+     * @param {Number} idx Index in queried container.
+     * @param {Container} bag Container to query for an item.
+     * @return {Item} The item at that index.
+     */
+    getItem : function getItem(idx, bag){
+        if ( typeof idx == 'number' ) {
+            bag = (bag === undefined ? this.defaultBag : this.getBag(bag));
+            return bag ? bag.getItem(idx) : undefined;
+        } else
+            return idx;
+    },
+    
+    /**
+     * Searches for an available (bag,slot), optionally restricted by an item.
+     * @param {Item} [item] If included, only bags which will accept the item are considered.
+     * @param {Container|String|Number} [bag] Container to search. If omitted, all containers
+     *  will be searched, starting with the default bag.
+     * @return {ContainerMeta} The (bag,idx) information about the empty slot, or null if no matching slot was found.
+     */
+    findEmptySlot : function findEmptySlot(item, bag){
+        var checkSpecificBag = (bag !== undefined)
+        ,   idx = -1
+        ;
+        bag  = this.getBag(bag);
+        item = this.getItem(item, bag);
+        
+        if ( checkSpecificBag ) {
+            idx = bag._getEmptySlot(item);
+        } else {
+            idx = this.defaultBag._getEmptySlot(item);
+            if (idx === -1)
+                for (var id in this.bags) {
+                    bag = this.bags[id];
+                    if ( bag.canAddItem(item) ) {
+                        idx = bag._getEmptySlot();
+                        break;
+                    }
+                }
+        }
+        
+        return (idx === -1) ? null : new ContainerMeta(bag, idx);
+    },
+    
+    
+    
+    /// Bag Management ///
+    
+    canAddBag : function canAddBag(bag){
+        return this.bagbag.canAddItem(bag);
+    },
+    
+    checkReqs : function checkReqs(bag){
+        return this.bagbag.checkReqs(bag);
+    },
+    
+    /**
+     * @param {String|Number|Container} query Queries for a bag:
+     *      - String: Looks up bag by name
+     *      - Number: Looks up bag by index in BagBag
+     *      - Container: Passed through
+     * @param {Boolean} [orDefault=false] If true, an unspecified query will return the default bag.
+     * @return {Container} The queried bag, or the default bag if `orDefault`, otherwise undefined.
+     */
+    getBag : function getBag(query, orDefault){
+        if (query === undefined && orDefault)
+            return this.defaultBag;
+        return ( (typeof query == 'string') ? this.namedBags[query] : this.bagbag.getItem(query) );
+        // return ( (typeof idx == 'string') ? this.namedBags[idx] : this.bagbag.getItem(idx)
+        //             || (orDefault ? this.defaultBag : undefined) );
+    },
+    
+    /**
+     * @param {String} name
+     * @return {Container}
+     */
+    getBagByName : function getBagByName(name){
+        return this.namedBags[name];
+    },
+    
+    hasBag : function hasBag(id){
+        return this.bagbag.hasItem(id);
+    },
+    
+    addBag : function addBag(bag){
+        this.bagbag.addItem(bag);
+        return this;
+    },
+    
+    moveBag : function moveBag(bag, idx){
+        this.bagbag.moveItem(bag, idx);
+        return this;
+    },
+    
+    removeBag : function removeBag(bag, options){
+        this.bagbag.removeItem(bag, options);
+        return this;
+    },
+    
+    removeAllBags : function removeAllBags(){
+        this.bagbag.removeAllItems();
+        return this;
+    }
+    
+})
+;
diff --git a/src/tanks/inventory/requirements/indexer.cjs b/src/tanks/inventory/requirements/indexer.cjs
new file mode 100644 (file)
index 0000000..11a30ee
--- /dev/null
@@ -0,0 +1,154 @@
+var Y = require('Y')
+,   evt = require('evt')
+,
+
+Indexer =
+exports['Indexer'] =
+evt.subclass('Indexer', {
+    __mixins__ : [],
+    __bind__   : [ 'addWord', 'addWords', 'addField', 'addObject', 'matchQuery', '_reduceMatches' ],
+    __static__ : {},
+    
+    className : 'Indexer',
+    _delimiter : ' ',
+    _kvsep     : ':',
+    _words     : null,
+    _fields    : null,
+    
+    
+    
+    init : function initIndexer(this, delimiter, kvsep){
+        this._delimiter = delimiter || ' ';
+        this._kvsep     = kvsep || ':';
+        this._words     = {};
+        this._fields    = {};
+    },
+    
+    
+    addWord: function(object, word){
+        if ( !word ) return this;
+        
+        var w = ''+word,
+            ws = this._words;
+        if ( ws[w] )
+            !ws[w].has(object) && ws[w].push(object);
+        else
+            ws[w] = [ object ];
+        
+        return this;
+    },
+    
+    addWords: function(object, words){
+        if ( !(words && object) ) return this;
+        
+        this._normalizeWords(words)
+            .each(this.addWord.bind(this, object));
+        
+        return this;
+    },
+    
+    addField: function(object, field, words){
+        if ( !(field && object) ) return this;
+        
+        words = words || object[field];
+        if ( !words ) return;
+        
+        this.addWords(object, words);
+        
+        var f = this._fields[field];
+        if ( !f )
+            f = this._fields[field] = new com.clearspring.Indexer( this._delimiter, this._kvsep );
+        
+        f.addWords(object, words);
+        
+        return this;
+    },
+    
+    addObject: function(object, exclude){
+        if ( !exclude )
+            exclude = [ Function ];
+        else if ( !(exclude instanceof Array) )
+            exclude = [ exclude ];
+        
+        basis.each(object, function(words, field){
+            if ( !exclude.has(field, basis.type(words)) )
+                this.addField(object, field);
+        });
+        return this;
+    },
+    
+    _normalizeWords: function(words){
+        if ( !words )
+            return [];
+        
+        if (words instanceof Array)
+            words = words.join(this._delimiter);
+        else
+            words += '';
+        
+        return words
+            // case insensitive
+            .toLowerCase()
+            
+            // normalize whitespace (counting non-WS delimiters)
+            .replace(/([\s-.:]+)/g, this._delimiter)
+            
+            // strip non-word characters
+            .replace(/([^ a-z0-9_-]|^\s+|\s+$)/g, '')
+            
+            // return words
+            .split(this._delimiter);
+    },
+    
+    _reduceMatches: function(results, word){
+        if ( word in this._words )
+            return results.concat(this._words[word]);
+        else
+            return results;
+    },
+    
+    _match: function(field, words){
+        if ( field && !this._fields[field] )
+            return [];
+        
+        var matcher = ( !field ? 
+            this._reduceMatches : this._fields[field]._reduceMatches );
+            
+        return this._normalizeWords(words)
+            .reduce(matcher, [])
+            .unique();
+    },
+    
+    _processAST: function(node){
+        if (!node)
+            return [];
+        
+        if (node instanceof QueryParser.And)
+            return this._processAST(node.lhs)
+                .intersect(this._processAST(node.rhs));
+            
+        else if (node instanceof QueryParser.Or)
+            return this._processAST(node.lhs)
+                .union(this._processAST(node.rhs));
+            
+        else if (node instanceof QueryParser.Not)
+            return Object.values(this._words)
+                .difference(this._processAST(node.expr));
+            
+        else if (node instanceof QueryParser.Field)
+            return this._match(node.field, node.word);
+            
+        else if (node instanceof Array)
+            return node;
+            
+        else
+            return this._match(null, node);
+    },
+    
+    matchQuery: function(query){
+        return this._processAST(QueryParser(query));
+    }
+    
+})
+;
+
diff --git a/src/tanks/inventory/requirements/requirements.cjs b/src/tanks/inventory/requirements/requirements.cjs
new file mode 100644 (file)
index 0000000..5061a89
--- /dev/null
@@ -0,0 +1,141 @@
+//#ensure "jsparse"
+
+/* Item Requirements Grammar
+
+word  := [a-zA-Z0-9_-]+
+field := word ':' word
+group := '(' query ')'
+term  := group | field | word
+
+and   := term 'and' term | term '&&' term | term term
+or    := term 'or' term | term '||' term
+not   := 'not' term | '!' term
+op    := and | or | not
+
+query := op | term
+
+*/
+var Query = function(state) { return Query(state); };
+
+function toNameString(){ 
+    return !this.constructor ? "" : this.constructor.name;
+}
+
+function Symbol(p, Cls){
+    Cls.prototype.toString = toNameString;
+    return action(p, function(lr) {
+        return new Cls(lr[0], lr[1]);
+    });
+}
+
+function Field(f, w){
+    this.field = f;
+    this.word = w;
+}
+
+function And(lhs, rhs) {
+    this.lhs = lhs;
+    this.rhs = rhs;
+}
+
+function Or(lhs, rhs) {
+    this.lhs = lhs;
+    this.rhs = rhs;
+}
+
+function Not(expr) {
+    this.expr = expr;
+}
+
+var word = join_action(
+    repeat1(choice(
+        range('a', 'z'),
+        range('A', 'Z'),
+        range('0', '9'),
+        '_', '-', '$' )), 
+    '');
+// var field = Symbol(sequence(word, expect(':'), word), Field);
+var group = action(wsequence(expect('('), Query, expect(')')), function(g){ return g[0]; });
+
+// var term = choice(group, field, word);
+var term = choice(group, word);
+
+var and = Symbol(choice(
+        wsequence(term, expect(token('and')), Query),
+        wsequence(term, expect(token('&&')), Query),
+        wsequence(term, Query)), And);
+
+var or = Symbol(choice(
+        wsequence(term, expect(token('or')), Query),
+        wsequence(term, expect(token('||')), Query)), Or);
+
+var not = Symbol(choice(
+        wsequence(expect(token('not')), Query),
+        wsequence(expect('!'), Query)), Not);
+
+var op = choice(not, or, and);
+
+var Query = choice(op, term);
+
+function QueryParser(s){
+    return (Query(ps(s)) || {}).ast;
+}
+
+QueryParser.And = And;
+QueryParser.Or = Or;
+QueryParser.Not = Not;
+QueryParser.Field = Field;
+exports['QueryParser'] = QueryParser;
+
+
+var Y = require('Y')
+,   evt = require('evt')
+,
+
+Requirements =
+exports['Requirements'] =
+evt.subclass('Requirements', {
+    __mixins__ : [],
+    __bind__   : [],
+    
+    ast : null,
+    
+    
+    init : function initRequirements(reqs){
+        this.reqs = reqs;
+        this.ast = QueryParser(reqs);
+    },
+    
+    test : function test(tags){
+        return !(this.reqs && this.reqs.length) ||
+            (tags && tags.length && this._test(tags, this.ast));
+    },
+    
+    _test: function _test(tags, node){
+        if (!node)
+            return true;
+        
+        if (node instanceof QueryParser.And)
+            return this._test(tags, node.lhs) && this._test(tags, node.rhs);
+            
+        else if (node instanceof QueryParser.Or)
+            return this._test(tags, node.lhs) || this._test(tags, node.rhs);
+            
+        else if (node instanceof QueryParser.Not)
+            return !this._test(tags, node.expr);
+            
+        else if (node instanceof Array) {
+            console.log('wtf! array node:', node, 'tags:', tags);
+            // throw new Error('wtf! array node! ['+node+']');
+            // return node.every(this._test.bind(this, tags));
+            return node.every(tags.has, tags);
+            
+        } else
+            return tags.has(node);
+    }
+    
+    
+})
+;
+
+
index 0de4a80..e027927 100644 (file)
@@ -6,8 +6,8 @@ var Y        = require('Y').Y
 ,   Item  = require('tanks/thing/item').Item
 
 ,   kNull = op.K(null)
-
 ,
+
 Inventoried =
 exports['Inventoried'] =
 Mixin.subclass('Inventoried', {
index 712a68f..860ee8a 100644 (file)
@@ -8,6 +8,7 @@ var Y         = require('Y').Y
 ,   BoundsType = require('tanks/constants').BoundsType
 ,   Buff      = require('tanks/effects/buff').Buff
 ,   Thing     = require('tanks/thing/thing').Thing
+,   ContainerMeta = require('tanks/inventory/container').ContainerMeta
 
 ,   ITEM_SIZE = REF_SIZE * 0.5
 ,   kNull = op.K(null)
@@ -19,6 +20,7 @@ exports['Item'] =
 Thing.subclass('Item', {
     // __mixins__ : [  ],
     __bind__ : [ 'onCollide', 'onAcquired', 'onLost', 'onEquip', 'onUnequip', 'onBuffDeath' ],
+    isItem : true,
     
     align : 0, // 0 reserved for neutral units
     
@@ -30,9 +32,7 @@ Thing.subclass('Item', {
     stats      : { hp:Infinity, move:0, power:0, speed:0 },
     dealDamage : op.nop,
     
-    // inactive
-    active     : false,
-    
+    active     : false, // inactive in the event loop
     
     /// Instance Properties ///
     name     : 'Dummy',     // {String} Display name
@@ -47,15 +47,9 @@ Thing.subclass('Item', {
     
     owner        : null,    // {Unit} Owner when picked up.
     currentBuffs : null,    // {Buff...} Buffs applied to owner
-    isEquipped   : false,
-    
-    // Inventory Bookkeeping
-    // inv : {
-    //     links : [],         // {Array} Pointers to Belts that link to this item
-    //     idx : null,         // {Integer} Index in bag
-    //     bag : null          // {Container} Containing bag
-    // },
+    inv          : null,    // {ContainerMeta} Inventory Bookkeeping
     
+    isEquipped   : false, // XXX: ?
     
     
     init : function initItem(){
@@ -68,6 +62,7 @@ Thing.subclass('Item', {
         this.currentActive = new Y.YArray();
         this.currentPassive = new Y.YArray();
         
+        this.inv = new ContainerMeta();
         this.activateGauge = new CooldownGauge(this.cooldowns.activate, REF_SIZE+1,REF_SIZE+1);
         
         this.on('collide',      this.onCollide);
@@ -77,25 +72,8 @@ Thing.subclass('Item', {
         this.on('item.unequip', this.onUnequip);
     },
     
-    tick : function tick(elapsed, now, ticks){
-        // Buffs will take care of themselves as part of the unit
-    },
-    
-    addToInventory : function addToInventory(unit){
-        if (!unit.hasInventory)
-            throw new Error('Unit has no inventory! '+unit);
-        unit.addItem(this);
-        return this;
-    },
-    
-    removeFromInventory : function removeFromInventory(){
-        if (this.owner)
-            this.owner.removeItem(this);
-        return this;
-    },
-    
     activate : function activate(){
-        console.log(this+' activated!');
+        // console.log(this+' activated!');
         
         if ( this.owner && this.cooldowns.activate.activate() ) {
             var buffs = this.currentBuffs
@@ -112,6 +90,8 @@ Thing.subclass('Item', {
         return this;
     },
     
+    /// Event Handlers ///
+    
     onBuffDeath : function onBuffDeath(evt, buff){
         this.currentBuffs.remove(buff);
         this.currentActive.remove(buff);
@@ -161,9 +141,15 @@ Thing.subclass('Item', {
     },
     
     onLost : function onLost(evt){
-        var current = this.currentBuffs
+        var lostItem = evt.data.item
+        ,   current = this.currentBuffs
         ,   unit = this.owner
         ;
+        // Bags are Items AND Containers, and are thus sent notifications when
+        // their contents change.
+        if (lostItem !== this)
+            return;
+        
         if ( current.length )
             current.invoke('die', 'item.lost');
         
index a8fc6f5..e4a42e5 100644 (file)
@@ -6,6 +6,7 @@ var Y           = require('Y').Y
 
 ,   config      = require('tanks/config').config
 ,   Tank        = require('tanks/thing/tank').Tank
+,   Inventory   = require('tanks/inventory/inventory').Inventory
 ,   Inventoried = require('tanks/mixins/inventoried').Inventoried
 ,
 
@@ -13,7 +14,8 @@ var Y           = require('Y').Y
 Player =
 exports['Player'] =
 Tank.subclass('Player', {
-    __mixins__ : [ Inventoried ],
+    // __mixins__ : [ Inventoried ],
+    __bind__ : [ 'keydown', 'keyup', 'mousedown', 'mouseup', 'mousemove' ],
     
     enableGameLogging : false,
     
@@ -40,14 +42,13 @@ Tank.subclass('Player', {
         this.gameLog = [];
         this.queue = [];
         this.activeKeys = new Y.YArray();
+        this.inventory = new Inventory(this);
         
-        this.keydown   = this.keydown.bind(this);
-        this.keyup     = this.keyup.bind(this);
-        this.mousedown = this.mousedown.bind(this);
-        this.mouseup   = this.mouseup.bind(this);
-        this.mousemove = this.mousemove.bind(this);
-        
-        this.replayMode = !!replay;
+        // this.keydown   = this.keydown.bind(this);
+        // this.keyup     = this.keyup.bind(this);
+        // this.mousedown = this.mousedown.bind(this);
+        // this.mouseup   = this.mouseup.bind(this);
+        // this.mousemove = this.mousemove.bind(this);
         
         if (this.replayMode) {
             this.replay = replay
index fe20c3a..09cc579 100644 (file)
@@ -6,14 +6,15 @@ var Y = require('Y').Y
 ,
 
 
-ItemContainerUI =
-exports['ItemContainerUI'] =
-HtmlLayer.subclass('ItemContainerUI', {
+ContainerUI =
+exports['ContainerUI'] =
+HtmlLayer.subclass('ContainerUI', {
+    __bind__ : [ 'onItemUpdated', 'onDrop' ],
     _layerClasses : 'item-container hud',
     
     name      : null,
     title     : null,
-    showTitle : true,
+    showTitle : false,
     defaultContainer : false,
     
     max       : 1,
@@ -22,15 +23,13 @@ HtmlLayer.subclass('ItemContainerUI', {
     
     
     
-    init : function initItemContainerUI(name, unit, items, options){
-        Y.bindAll(this, 'onItemUpdated', 'onDrop');
+    init : function initContainerUI(unit, container, options){
         if (options) Y.core.extend(this, options);
         
-        this.name = name;
         this.unit = unit;
-        this.items = items;
+        this.container = container;
         
-        this._layerId = this.name+'_slot';
+        if (!this._layerId && this.name) this._layerId = this.name+'_slot';
         HtmlLayer.init.call(this);
         
         if (this.showTitle)
@@ -46,7 +45,7 @@ HtmlLayer.subclass('ItemContainerUI', {
     },
     
     _makeSlot : function _makeSlot(idx){
-        return new ItemContainerUISlot(this, idx).appendTo(this);
+        return new ContainerUISlot(this, idx).appendTo(this);
     },
     
     refresh : function refresh(){
@@ -91,9 +90,9 @@ HtmlLayer.subclass('ItemContainerUI', {
 ,
 
 
-ItemContainerUISlot =
-exports['ItemContainerUISlot'] =
-Layer.subclass('ItemContainerUISlot', {
+ContainerUISlot =
+exports['ContainerUISlot'] =
+Layer.subclass('ContainerUISlot', {
     _layerClasses : 'item-container slot hud ',
     hasCanvas  : false,
     
@@ -106,11 +105,11 @@ Layer.subclass('ItemContainerUISlot', {
     item     : null,
     
     
-    init : function initItemContainerUISlot(backpack, idx){
+    init : function initContainerUISlot(container, idx){
         Y.bindAll(this, 'onActivate', 'onDragStart');
         Layer.init.call(this);
-        this.backpack = backpack;
-        this.inventory = backpack.inventory;
+        this.container = container;
+        this.inventory = container.inventory;
         this.idx = idx;
         this.layer.addClass('slot'+idx);
         this.inner =
@@ -121,10 +120,10 @@ Layer.subclass('ItemContainerUISlot', {
     
     refresh : function refresh(){
         this.layer.data({
-            'backpack-slot': this,
+            'container-slot': this,
             'idx': this.idx
         });
-        var item = this.inventory.backpack[this.idx];
+        var item = this.container.slots[this.idx];
         if (item)
             this.setItem(item);
         else