Woo, configuration-driven units, items and buffs.
authordsc <david.schoonover@gmail.com>
Tue, 4 Jan 2011 07:15:03 +0000 (23:15 -0800)
committerdsc <david.schoonover@gmail.com>
Tue, 4 Jan 2011 07:15:03 +0000 (23:15 -0800)
21 files changed:
data/config.yaml [new file with mode: 0644]
data/game.yaml [new file with mode: 0644]
data/hotkeys.yaml [new file with mode: 0644]
data/types/buffs.yaml [new file with mode: 0644]
data/types/items.yaml [new file with mode: 0644]
data/types/levels.yaml [new file with mode: 0644]
data/types/units.yaml [new file with mode: 0644]
pavement.py
src/Y/types/object.cjs
src/evt.cjs
src/ezl/util/data/datafile.cjs [new file with mode: 0644]
src/ezl/util/data/index.cjs [new file with mode: 0644]
src/ezl/util/data/loader.cjs [new file with mode: 0644]
src/tanks/config.cjs
src/tanks/effects/buff.cjs
src/tanks/game.cjs
src/tanks/mixins/speciated.cjs
src/tanks/thing/thing.cjs
src/tanks/ui/main.cjs
src/tanks/ui/pathmapui.cjs
www/deps.html

diff --git a/data/config.yaml b/data/config.yaml
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/data/game.yaml b/data/game.yaml
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/data/hotkeys.yaml b/data/hotkeys.yaml
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/data/types/buffs.yaml b/data/types/buffs.yaml
new file mode 100644 (file)
index 0000000..231d445
--- /dev/null
@@ -0,0 +1,19 @@
+# Buff definitions
+name: buff
+defaults:
+    symbol: tanks/effects/buff.Buff
+    timeout: -1 # the Infinity literal is not valid JSON
+    priority: 0
+    stack_limit: 1
+types:
+    speedup:
+        name: Speed Up
+        desc: Speeds up your tank temporarily.
+        tags: [ 'movement' ]
+        stats:
+            move: 2.0
+        effects: []
+        art:
+            icon: ''
+            inv_icon: ''
+    
diff --git a/data/types/items.yaml b/data/types/items.yaml
new file mode 100644 (file)
index 0000000..4d7a2de
--- /dev/null
@@ -0,0 +1,14 @@
+# Item definitions
+name: item
+defaults:
+    symbol: tanks/thing/item.Item
+types:
+    nitro:
+        name: Nitro
+        desc: Speeds up your tank temporarily.
+        tags: [ 'movement' ]
+        buffs: [ 'speedup' ]
+        art:
+            map_icon: ''
+            inv_icon: ''
+    
diff --git a/data/types/levels.yaml b/data/types/levels.yaml
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/data/types/units.yaml b/data/types/units.yaml
new file mode 100644 (file)
index 0000000..8016198
--- /dev/null
@@ -0,0 +1,53 @@
+# Unit definitions
+name: unit
+defaults:
+    symbol: tanks/thing/thing.Thing
+    level: 1
+    stats:
+        hp            : 1           # health
+        move          : 1.0         # move speed (squares/sec)
+        power         : 1           # attack power
+        speed         : 0.5         # attack cool (sec)
+        accuracy      : 1.0         # chance of shooting where aiming
+        shots         : 5           # max projectiles in the air at once
+        sight         : 5           # distance this unit can see (squares)
+    cooldowns:
+        attack: stats.move.val
+    ai:
+        path          : 1.0         # calculate a path to enemy
+        dodge         : 1.0         # dodge an incoming bullet
+        shootIncoming : 0.5         # shoot down incoming bullet
+        shootEnemy    : 0.75        # shoot at enemy tank if in range
+    art:
+        icon: ''
+        inv_icon: ''
+types:
+    player:
+        name: Player
+        desc: "Don't hate the Player, hate the--Wait. No, that's fine."
+        tags: [ 'tank' ]
+        symbol: tanks/thing/player.PlayerTank
+        stats:
+            hp    : 1
+            move  : 0.90
+            power : 1
+            speed : 0.5
+            shots : 5
+    pink:
+        name: Pink Tank
+        desc: A very pink tank.
+        tags: [ 'tank' ]
+        symbol: tanks/thing/tank.Tank
+        stats:
+            hp    : 1
+            move  : 0.75
+            power : 1
+            speed : 0.5
+            shots : 4
+        ai:
+            path          : 1.0
+            dodge         : 1.0
+            shootIncoming : 0.5
+            shootEnemy    : 0.75
+        
+    
index 9f68f83..d0a9f0a 100755 (executable)
@@ -1,7 +1,10 @@
 #!/usr/bin/env paver
 from paver.easy import *
+import yaml, json, os
 
+BUILD_DIR = path('build')
 SRC_DIRS = [ path('src')/d for d in ('Y', 'ezl', 'tanks') ]
+DATA_DIR = path('data')
 
 
 def commonjs(*args, **kw):
@@ -16,18 +19,33 @@ def commonjs(*args, **kw):
         if v is not True: kwargs.append(str(v))
     return sh('commonjs %s' % ' '.join( SRC_DIRS + kwargs + list(args)), capture=capture)
 
-
-
 @task
-def clean():
-    "Cleans dep cache and build files"
-    commonjs(clean=True)
+def data():
+    "Convert all yaml files to json."
+    for dirpath, dirs, files in os.walk(DATA_DIR):
+        indir  = path(dirpath)
+        outdir = BUILD_DIR/dirpath
+        if not outdir.exists():
+            outdir.makedirs()
+        for f in files:
+            if f.endswith('.yaml'):
+                in_ = indir / f
+                out = outdir / f.replace('.yaml', '.json')
+                with in_.open('rU') as infile, out.open('w') as outfile:
+                    json.dump(yaml.load(infile), outfile, indent=4)
+
 
 @task
+@needs('data')
 def build():
     "Builds the Tanks project"
     tags = commonjs(script_tags=True, capture=True)
     with path('www/deps.html').open('w') as f:
         f.write(tags)
 
-auto = build
+@task
+def clean():
+    "Cleans dep cache and build files"
+    commonjs(clean=True)
+    build()
+
index 860597c..a383701 100644 (file)
@@ -92,6 +92,14 @@ function deepcopy(o){
     if ( Y.isFunction(o.clone) )
         return o.clone();
     
+    if ( Y.isArray(o) )
+        return o.map(deepcopy);
+    
+    // FIXME: Should really be taking appropriate steps to copy
+    // builtin Objects like RegExp, Date & DOM crap
+    if ( !Y.isPlainObject(o) )
+        return o;
+    
     var path = new Y.YArray();
     return core.reduce(o, function _deepcopy(acc, v, k){
         var chain = path.push(k).join('.');
@@ -99,6 +107,10 @@ function deepcopy(o){
         if ( v && Y.isFunction(v.clone) )
             acc.setNested(chain, v.clone());
         
+        // Nested array -- recurse, but restart path stack
+        else if ( Y.isArray(v) )
+            acc.setNested(chain, v.map(deepcopy));
+            
         // Nested object -- recurse
         else if ( Y.isPlainObject(v) )
             core.reduce(v, _deepcopy, acc);
@@ -193,3 +205,4 @@ core.forEach({
         YObject.fn[name] = fn.methodize();
     });
 
+//#exports ensure getNested getNestedMeta setNested
index 07392eb..42532bc 100644 (file)
@@ -44,9 +44,10 @@ var Y       = require('Y').Y
 ,   classStatics = [ 'instantiate', 'fabricate', 'subclass' ]
 ,   classMagic = [
             '__bases__', '__initialise__', '__class__', 'constructor', 'className',
-            '__emitter__', '__static__', '__mixins__'
+            '__emitter__', '__static__', '__mixins__', '__id__'
         ] //.concat(classStatics)
-,   mixinSkip = [ '__mixin_skip__', 'onMixin', 'onCreate', 'init'
+,   mixinSkip = [
+            '__mixin_skip__', 'onMixin', 'onCreate', 'init'
         ].concat(classMagic)
 ,   GLOBAL_ID = 0
 ,
@@ -66,16 +67,17 @@ var Y       = require('Y').Y
  */
 ConstructorTemplate =
 exports['ConstructorTemplate'] =
-function ConstructorTemplate(_args) {
+function ConstructorTemplate() {
     var instance = this
-    ,   cls = arguments.callee
+    ,   A = arguments
+    ,   cls = A.callee
     ,   caller = unwrap(cls.caller)
-    ,   args = (caller === instantiate) ? _args : arguments
+    ,   args = (caller === instantiate) ? A[0] : A
     ;
     
     // Not subclassing
     if ( caller !== Y.fabricate ) {
-        instance.id = GLOBAL_ID++;
+        instance.__id__ = GLOBAL_ID++;
         
         // Perform actions that should only happen once in Constructor
         instance.__emitter__ = new Emitter(instance, cls);
diff --git a/src/ezl/util/data/datafile.cjs b/src/ezl/util/data/datafile.cjs
new file mode 100644 (file)
index 0000000..0623e60
--- /dev/null
@@ -0,0 +1,93 @@
+//#ensure "jquery"
+var Y = require('Y').Y
+,   evt = require('evt')
+,   getNested = require('Y/types/object').getNested
+,   deepcopy = require('Y/types/object').deepcopy
+,
+
+
+DataFile =
+exports['DataFile'] =
+new evt.Class('DataFile', {
+    __bind__ : [ 'onLoad' ],
+    
+    path : '',
+    
+    
+    init : function initDataFile(path){
+        this.path = path;
+    },
+    
+    load : function load(){
+        this.fire('load', this);
+        jQuery.getJSON(this.path, this.onLoad);
+        return this;
+    },
+    
+    resolve : function resolve(spec){
+        var nameParts = spec.split('.')
+        ,   modName = nameParts.shift()
+        ;
+        if (!(modName && modName.length && nameParts.length))
+            this.die('Cannot resolve symbol: '+spec);
+        
+        var module = require(modName)
+        ,   symbol = getNested(module, nameParts)
+        ;
+        
+        if (!symbol)
+            this.die('Unable to locate class specified by symbol (symbol="'+spec+'", module='+module+' --> '+symbol+')!');
+        if (!Y.isFunction(symbol.speciate))
+            this.die('Cannot create types from data (symbol-class '+symbol+' from "'+spec+'" is not Speciated)!');
+        
+        return symbol;
+    },
+    
+    process : function process(data){
+        this.data = data;
+        this.fire('process', data);
+        console.group('DataFile: processing '+this.path);
+        
+        var types = Y(data.types)
+        ,   defaults = data.defaults || {};
+        if (!(types && Y.isFunction(types.forEach)))
+            this.die('Specified types are not iterable! '+data.types);
+        
+        types.forEach(function(kv, id){
+            // TODO: process 'inherits' key -- requires resolving data-type path
+            var props = Y.extend({}, deepcopy(data.defaults), kv)
+            ,   symbol = props.symbol;
+            
+            if (!symbol)
+                this.die('No symbol defined for type "'+id+'" at '+this.path+'!');
+            
+            delete props.symbol;
+            var base = this.resolve(symbol)
+            ,   type = base.speciate(id, props);
+            console.log('Added type '+id+':', type);
+        }, this);
+        
+        console.groupEnd();
+        this.fire('complete', this);
+        return this;
+    },
+    
+    // TODO: repeat other jq events
+    onLoad : function onLoad(data){
+        console.log(this+'.onLoad()');
+        this.process(data);
+    },
+    
+    /**
+     * @private
+     */
+    die : function die(reason){
+        throw new Error(this+' Error: '+reason);
+    },
+    
+    toString : function(){
+        return this.className+'(file="'+this.path+'")';
+    }
+    
+})
+;
diff --git a/src/ezl/util/data/index.cjs b/src/ezl/util/data/index.cjs
new file mode 100644 (file)
index 0000000..8a5fcf4
--- /dev/null
@@ -0,0 +1,6 @@
+//#exports DataFile Loader
+require('Y').Y.core
+.extend(exports, {
+    'DataFile' : require('ezl/util/data/datafile').DataFile,
+    'Loader'   : require('ezl/util/data/loader').Loader
+});
diff --git a/src/ezl/util/data/loader.cjs b/src/ezl/util/data/loader.cjs
new file mode 100644 (file)
index 0000000..c5cd6d3
--- /dev/null
@@ -0,0 +1,116 @@
+//#ensure "jquery"
+var Y = require('Y').Y
+,   evt = require('evt')
+,
+
+/**
+ * Orchestrates the loading of a number of data files in a civilized fashion,
+ * emitting progress and notifying of completion.
+ */
+Loader =
+exports['Loader'] =
+new evt.Class('Loader', {
+    max      : 4,
+    
+    queue    : [],
+    inFlight : [],
+    running  : false,
+    
+    
+    /**
+     * @param {(Function | Job)[]} [jobs] A list of jobs to enqueue.
+     * @param {Number} [max=4] Maximum concurrent jobs in flight.
+     * 
+     * A Job can be a function, or any object with a load() method. In both cases,
+     * it will be called with this Loader as the only argument.
+     * 
+     * The job must notify of its completion. It may do so in one of two ways:
+     *  - If it implements the {EventDispatcher} interface, the Loader will listen
+     *    for the 'complete' event.
+     *  - Otherwise, the job must trigger the 'job.complete' event on this Loader,
+     *    ensuring the 'target' property of the dispatched event points to the
+     *    original job object.
+     * 
+     * TODO: support all that crap. Only supporting dispachers for now.
+     */
+    init : function initLoader(jobs, max){
+        this.queue = Y([]);
+        this.inFlight = Y([]);
+        if (jobs) this.addJobs.apply(this, jobs);
+        if (max) this.max = max;
+    },
+    
+    /**
+     * Accepts any number of jobs (Functions or objects with a load() method) to load.
+     * @return {this}
+     */
+    addJobs : function addJobs(job){
+        // TODO: validate jobs
+        // Y(arguments).reduce(this.queue.push.genericize().limit(2), this.queue);
+        Y(arguments).forEach(function(job){ this.queue.push(job); }, this);
+        this.nextJob();
+        return this;
+    },
+    
+    /**
+     * Starts the load process if stopped.
+     * Has no effect if loading has already commenced.
+     * @return {this}
+     */
+    start : function start(){
+        if (!this.running) {
+            this.running = true;
+            this.fire('start');
+            this.nextJob();
+        }
+        return this;
+    },
+    
+    /**
+     * Stops the load process if running.
+     * Has no effect if loader has not started.
+     * @return {this}
+     */
+    stop : function stop(){
+        if (this.running) {
+            this.fire('stop');
+            this.running = false;
+        }
+        return this;
+    },
+    
+    /**
+     * @protected
+     */
+    nextJob : function nextJob(){
+        var self = this;
+        // console.log(self+'.nextJob()');
+        if ( !self.running
+                || self.inFlight.length >= self.max
+                || !self.queue.length )
+            return;
+        
+        var job = self.queue.shift();
+        job.addEventListener('complete', function onJobComplete(evt){
+            // console.log(self+'.onJobComplete('+job+')');
+            job.removeEventListener('complete', arguments.callee);
+            self.inFlight.remove(job);
+            self.nextJob();
+        });
+        self.inFlight.push(job);
+        job.load(self);
+        
+        // Recurse until we've got enough in-flight, or we're out of queued jobs
+        if (!self.queue.length){
+            this.running = false;
+            self.fire('complete');
+        } else
+            self.nextJob();
+    },
+    
+    toString : function(){
+        return this.className+'(running='+this.running+', inFlight='+this.inFlight.length+', queue='+this.queue.length+')';
+    }
+    
+})
+;
index 06ec09f..65becea 100644 (file)
@@ -1,9 +1,14 @@
 //  -*- mode: JavaScript; tab-width: 4; indent-tabs-mode: nil; -*-
-var Y      = require('Y').Y
-,   Config = require('Y/modules/y.config').Config
-,   Vec    = require('ezl/math').Vec
+var Y        = require('Y').Y
+,   Config   = require('Y/modules/y.config').Config
+
+,   Vec      = require('ezl/math').Vec
+,   DataFile = require('ezl/util/data/datafile').DataFile
+,   Loader   = require('ezl/util/data/loader').Loader
 ,
 
+/// Config (Inserted Here) ///
+
 defaults =
 exports['defaults'] = {
     game : {
@@ -31,8 +36,24 @@ config =
 exports['config'] = new Config(defaults)
 ;
 
+// Updates the midpoint square when square-size changes
 config.addEventListener('set:pathing.gridSquare', function(evt){
     var sq = evt.data.newval;
     config.set('pathing.gridSquareMid', new Vec(sq/2, sq/2));
 });
 
+
+
+/// Load Data Files ///
+
+exports['loadDataFiles'] =
+    function loadDataFiles(){
+        var files = 
+            'types/buffs types/items types/units' // types/level levels game
+                .split(' ')
+                .map(function(type){
+                    return new DataFile('build/data/'+type+'.json');
+                });
+        return new Loader(files).start();
+    };
+
index b29e874..3d0b346 100644 (file)
@@ -18,8 +18,8 @@ new evt.Class('Buff', {
     
     
     /// Configurable ///
-    priority  : 0, // Order of stat and effect application (lower is earlier) // TODO
-    timeout : 5000,
+    priority  : 0, // Order of stat and effect application (lower is earlier) // TODO: doesn't do anything
+    timeout : Infinity,
     threat  : 1, // TODO
     
     // TODO: functions
@@ -31,7 +31,7 @@ new evt.Class('Buff', {
     triggers  : {}, // {event : Effect} // maybe
     
     /// Instance ///
-    name    : 'Turrrrbo',
+    name    : '',
     owner   : null, // Agent which originated the buff
     target  : null, // Agent to which the buff applies
     game    : null,
index b6d99ad..20d5e6e 100644 (file)
@@ -150,8 +150,8 @@ Y.subclass('Game', {
         
         this.pathmap.addBlocker(unit);
         
-        if ( unit.active && !this.byId[unit.id] ) {
-            this.byId[unit.id] = unit;
+        if ( unit.active && !this.byId[unit.__id__] ) {
+            this.byId[unit.__id__] = unit;
             this.active.push(unit);
             
             if (unit instanceof Bullet)
@@ -167,7 +167,7 @@ Y.subclass('Game', {
         if (unit instanceof Event)
             unit = unit.trigger;
         
-        delete this.byId[unit.id];
+        delete this.byId[unit.__id__];
         this.active.remove(unit);
         this.pathmap.removeBlocker(unit);
         
index c7f489a..9a4ec24 100644 (file)
@@ -24,7 +24,7 @@ Mixin.subclass('Speciated', {
             
             props = props || {};
             props.__species__ = id;
-            delete props.id; // reserved for instance id
+            // delete props.id; // reserved for instance id
             
             var cls = this
             ,   speciesName = this.id2name(id)
index d49794d..bc537f6 100644 (file)
@@ -12,6 +12,7 @@ var Y           = require('Y').Y
 ,   map         = require('tanks/map/map')
 ,   stat        = require('tanks/effects/stat')
 ,   Quantified  = require('tanks/mixins/quantified').Quantified
+,   Speciated   = require('tanks/mixins/speciated').Speciated
 ,
 
 
@@ -19,7 +20,7 @@ var Y           = require('Y').Y
 Thing =
 exports['Thing'] =
 new evt.Class('Thing', {
-    __mixins__ : [ Quantified ],
+    __mixins__ : [ Quantified, Speciated ],
     
     // Config
     showAttackCooldown : null,
@@ -177,7 +178,7 @@ new evt.Class('Thing', {
     },
     
     toString : function toString(){
-        return this.className+'(id='+this.id+', loc='+this.loc+')';
+        return this.className+'(id='+this.__id__+', loc='+this.loc+')';
     }
     
 });
index 23aa750..5b8972a 100644 (file)
@@ -4,12 +4,15 @@
 require("Y/modules/y.kv");
 
 var Y            = require('Y').Y
+
+,   FpsSparkline = require('ezl/loop').FpsSparkline
+
+,   cfg          = require('tanks/config')
 ,   configui     = require('tanks/ui/configui')
 ,   Game         = require('tanks/game').Game
 ,   Tank         = require('tanks/thing').Tank
-,   FpsSparkline = require('ezl/loop').FpsSparkline
-,   config       = require('tanks/config').config
 
+,   config       = cfg.config
 ,   updateTimer  = null
 ,   LBT = null
 ;
@@ -20,6 +23,11 @@ qkv = Y(window.location.search.slice(1)).fromKV();
 // Main method is only executed once, so we'll setup things
 // that don't change between games.
 function main(){
+    
+    // TODO: wait until completed to allow start
+    // TODO: loading screen
+    var configLoader = cfg.loadDataFiles();
+    
     $('#welcome').center();
     
     /// Debug ///
index 13e7928..d306c3e 100644 (file)
@@ -1,3 +1,4 @@
+//#ensure "jquery"
 var Y       = require('Y').Y
 
 ,   Rect    = require('ezl/shape').Rect
@@ -103,11 +104,11 @@ Rect.subclass('PathMapUI', {
     },
     
     monitorAgent : function monitorAgent(agent){
-        if ( agent && !this.hasPath(agent.id) )
+        if ( agent && !this.hasPath(agent.__id__) )
             agent.addEventListener('destroy', this.cleanUpAgent);
     },
     
-    cleanUpAgent : function cleanUpAgent(evt){ this.destroyPath(evt.target.id); },
+    cleanUpAgent : function cleanUpAgent(evt){ this.destroyPath(evt.target.__id__); },
     
     getPath     : function getPath(id){     return $('.path.agent_'+id, this.layer); },
     hasPath     : function hasPath(id){     return !!this.getPath(id).length; },
@@ -118,7 +119,7 @@ Rect.subclass('PathMapUI', {
     drawPath : function drawPath(agent, start, path){
         this.monitorAgent(agent);
         
-        var id = agent.id
+        var id = agent.__id__
         ,   w = this.layerWidth, h = this.layerHeight
         ,   canvas = this.getPath(id)
         ;
index 10dba88..9caaa5a 100644 (file)
@@ -56,6 +56,8 @@
 <script src="build/tanks/effects/stat.js" type="text/javascript"></script>
 <script src="build/tanks/map/map.js" type="text/javascript"></script>
 <script src="build/Y/modules/y.cookies.js" type="text/javascript"></script>
+<script src="build/ezl/util/data/datafile.js" type="text/javascript"></script>
+<script src="build/ezl/util/data/loader.js" type="text/javascript"></script>
 <script src="build/tanks/mixins/speciated.js" type="text/javascript"></script>
 <script src="build/tanks/mixins/meronomic.js" type="text/javascript"></script>
 <script src="build/tanks/map/traversal.js" type="text/javascript"></script>
@@ -67,8 +69,8 @@
 <script src="build/tanks/effects/buff.js" type="text/javascript"></script>
 <script src="build/tanks/map/trajectory.js" type="text/javascript"></script>
 <script src="build/tanks/effects.js" type="text/javascript"></script>
-<script src="build/tanks/thing/item.js" type="text/javascript"></script>
 <script src="build/tanks/ui/pathmapui.js" type="text/javascript"></script>
+<script src="build/tanks/thing/item.js" type="text/javascript"></script>
 <script src="build/tanks/thing/wall.js" type="text/javascript"></script>
 <script src="build/tanks/ui/grid.js" type="text/javascript"></script>
 <script src="build/tanks/fx/explosion.js" type="text/javascript"></script>