Config UI scaffold.
authordsc <david.schoonover@gmail.com>
Fri, 17 Dec 2010 12:00:33 +0000 (04:00 -0800)
committerdsc <david.schoonover@gmail.com>
Fri, 17 Dec 2010 12:00:33 +0000 (04:00 -0800)
24 files changed:
src/Y/modules/y.config.cjs [copied from src/tanks/util/config.cjs with 59% similarity]
src/Y/modules/y.scaffold.cjs [new file with mode: 0644]
src/Y/op.cjs
src/Y/type.cjs
src/Y/types/object.cjs
src/ezl/loop/eventloop.cjs
src/ezl/util/configuration.cjs [moved from src/tanks/util/config.cjs with 84% similarity]
src/tanks/config.cjs
src/tanks/game.cjs
src/tanks/map/level.cjs
src/tanks/map/pathmap.cjs
src/tanks/thing/bullet.cjs
src/tanks/thing/thing.cjs
src/tanks/ui/config.cjs [deleted file]
src/tanks/ui/configui.cjs [new file with mode: 0644]
src/tanks/ui/grid.cjs
src/tanks/ui/index.cjs
src/tanks/ui/main.cjs
www/css/lttl.css
www/debug.html
www/deps.html
www/favicon.ico
www/panes.html [new file with mode: 0644]
www/welcome.html

similarity index 59%
copy from src/tanks/util/config.cjs
copy to src/Y/modules/y.config.cjs
index 2b90bba..ff41a55 100644 (file)
@@ -1,23 +1,31 @@
 var Y = require('Y').Y
-,   Emitter = require('Y/modules/y.event').Emitter
+,   getNested = require('Y/types/object').getNested
+,   Emitter   = require('Y/modules/y.event').Emitter
 ,
 
 
 Config =
-exports.Config =
-Y.YObject.subclass('Config', function(){
+exports['Config'] =
+Y.YObject.subclass('Config', function(Config){
     
     Y.core.extend(this, {
         
         init : function initConfig(defaults){
-            this._defaults = defaults;
-            this._o = Y({}, defaults);
+            this._defaults = defaults || {};
+            this._o = Y({}, this._defaults);
             this.__emitter__ = new Emitter(this);
         },
         
+        clone : function clone(){
+            var c = new Config();
+            c._defaults = this._defaults;
+            c._o = c._o.clone();
+            return c;
+        },
+        
         set : function set(key, value, def){
             if ( key !== undefined ){
-                var meta = this.ensure(key).getNested(key, def, true)
+                var meta = this.ensure(key).getNestedMeta(key, def)
                 ,   old = meta.value ;
                 def   = (def   === undefined ? old : def);
                 value = (value === undefined ? def : value);
@@ -31,7 +39,7 @@ Y.YObject.subclass('Config', function(){
         remove : function remove(key){
             if ( key !== undefined ){
                 var sentinel = {}
-                ,   meta = this.getNested(key, sentinel, true)
+                ,   meta = this.getNestedMeta(key, sentinel)
                 ,   old = (meta.value === sentinel ? undefined : old);
                 
                 if ( meta.obj ) {
@@ -67,6 +75,7 @@ Y.YObject.subclass('Config', function(){
             context = context || this;
             
             var ret = Y.reduce(this._o, this._reducer, {
+                'fn'   : fn,
                 'acc'  : acc,
                 'path' : new Y.YArray(),
                 'cxt'  : context
@@ -89,12 +98,46 @@ Y.YObject.subclass('Config', function(){
                 
             // Normal value -- invoke iterator
             else
-                state.acc = fn.call(state.cxt, state.acc, v, chain, this);
+                state.acc = state.fn.call(state.cxt, state.acc, v, chain, this);
             
             state.path.pop();
             return state;
+        },
+        
+        /**
+         * Iterates over both items and groups of the config object.
+         */
+        parts : function parts(groupFn, itemFn, acc, context){
+            context = context || this;
+            
+            var path = new Y.YArray();
+            return Y.reduce(this._o,
+                function _parts(acc, v, k, o){
+                    var chain = path.push(k).join('.');
+                    
+                    // Nested object -- recurse
+                    if ( Y.isPlainObject(v) ){
+                        acc = groupFn.call(context, acc, v, chain, this);
+                        acc = Y.reduce(v, _parts, acc, this);
+                        
+                    // Normal value -- invoke iterator
+                    } else
+                        acc = itemFn.call(context, acc, v, chain, this);
+                    
+                    path.pop();
+                    return acc;
+                },
+                acc, this);
+        },
+        
+        getDefault : function getDefault(k, def){
+            return getNested(this._defaults, k, def);
         }
         
     });
     
+    this['get'] = this.getNested;
+    
 });
+
+Y['config'] = exports;
diff --git a/src/Y/modules/y.scaffold.cjs b/src/Y/modules/y.scaffold.cjs
new file mode 100644 (file)
index 0000000..45a9d5c
--- /dev/null
@@ -0,0 +1,151 @@
+//#ensure "jquery"
+var Y  = require('Y').Y
+,   Emitter   = require('Y/modules/y.event').Emitter
+,   op = Y.op
+
+
+,   upperPat = /^[A-Z]+$/
+,   symbolPat = /^[^a-zA-Z]+$/
+,
+camelToSpaces =
+exports['camelToSpaces'] =
+function camelToSpaces(s){
+    return Y(s).reduce(
+            function(acc, ch, i){
+                return acc + (
+                    symbolPat.test(ch) ? '' : 
+                    (upperPat.test(ch) ? ' '+ch : ch) );
+            }, '');
+}
+,
+
+type2parser = {
+    'Boolean'  : op.parseBool,
+    'Number'   : parseFloat,
+    'String'   : String
+}
+,
+
+type2el = {
+    'Boolean'  : 'checkbox',
+    'Number'   : 'text',
+    'String'   : 'text'
+    // 'Array'    : 'select',
+    // 'Date'     : 'datepicker',
+    // 'RegExp'   : 'text'
+}
+,
+
+
+
+Field =
+exports['Field'] =
+Y.subclass('Field', {
+    
+    init : function initField(chain, def, val, options){
+        options = options || {};
+        this.__emitter__ = new Emitter(this);
+        
+        this.id    = chain;
+        this.def   = def;
+        this.val   = this.old = val === undefined ? def : val;
+        this.key   = chain.split('.').pop();
+        this.label = camelToSpaces(this.key);
+        
+        var T = Y(Y.type(val)).getName();
+        this.cast = options.cast || type2parser[T];
+        this.type = options.type || type2el[T];
+        
+        if (!this.cast)
+            throw new Error('No parser defined for type "'+T+'"');
+        if (!this.type)
+            throw new Error('No field element defined for type "'+T+'"');
+        
+        this.build()
+            .update(this.val);
+        
+        this.elField.bind('change', this.onChange.bind(this));
+    },
+    
+    build : function build(){
+        var el =
+        this.el =
+            jQuery('<div/>')
+                .addClass('field');
+        this.elLabel =
+            jQuery('<label/>')
+                .attr('for', this.id)
+                .text(this.label)
+                .appendTo(el);
+        this.elField =
+            jQuery('<input/>')
+                .attr({
+                    'id'   : this.id,
+                    'type' : this.type,
+                    'name' : this.key
+                })
+                .val(this.val)
+                .appendTo(el);
+        return this;
+    },
+    
+    update : function update(val){
+        var el = this.elField;
+        if (val !== this.val) {
+            this.old = this.val;
+            this.val = val;
+            this.fire('change', this, {
+                'key'    : this.id,
+                'oldval' : this.old,
+                'newval' : this.val,
+                'el'     : this.elField
+            });
+            el.val(val);
+        }
+        if (this.type === 'checkbox')
+            el.attr('checked', !!this.val);
+        return this;
+    },
+    
+    onChange : function onChange(evt){
+        this.update( this.cast(this.elField.val()) );
+    }
+    
+})
+,
+
+
+create =
+exports['create'] =
+function create(config, el){
+    config.parts(
+        function createGroup(oldGroup, value, chain){
+            return jQuery('<fieldset/>')
+                        .attr('id', chain)
+                        .addClass('group')
+                        .append( jQuery('<legend/>').text(chain.split('.').pop()) )
+                        .appendTo(el);
+        },
+        function createField(group, value, chain){
+            var def = config.getDefault(chain)
+            ,   field = new Field(chain, def, value);
+            
+            group.append(field.el);
+            config.addEventListener('set:'+chain, function onConfigSet(evt){
+                field.update(evt.data.newval);
+            });
+            field.addEventListener('change', function onFieldChange(evt){
+                config.set(evt.data.key, evt.data.newval);
+            });
+            
+            return group;
+        },
+        el);
+    return el;
+}
+;
+
+Y.YString.fn['camelToSpaces'] = Y(camelToSpaces).methodize()
+
+
+Y['scaffold'] = exports;
index 1acfcbe..408e640 100644 (file)
@@ -70,7 +70,12 @@ var core = require('Y/core')
         };
     },
     
-    end : function end(o){ return ((o && o.__y__) ? o.end() : o); }
+    // misc
+    end : function end(o){ return ((o && o.__y__) ? o.end() : o); },
+    parseBool : function(s){
+        var i = parseInt(s);
+        return isNaN(i) ? (s && s.toLowerCase() !== 'false') : i;
+    }
     
 };
 
index 1b04091..18c7cb8 100644 (file)
@@ -8,6 +8,7 @@ var undefined
 ,   _Array    = globals.Array
 ,   _String   = globals.String
 ,   _Number   = globals.Number
+,   _Boolean  = globals.Boolean
 
 ,   FN = "constructor"
 ,   PT = "prototype"
index 42a2a60..e3a57ba 100644 (file)
@@ -96,10 +96,10 @@ YCollection.subclass('YObject', function setupYObject(YObject){
 
 
 core.forEach({
-    'ensure'     : ensure,
-    'metaGetter' : metaGetter,
-    'getNested'  : getNested,
-    'setNested'  : setNested
+    'ensure'        : ensure,
+    'getNestedMeta' : getNestedMeta,
+    'getNested'     : getNested,
+    'setNested'     : setNested
 }, function(fn, name){
         fn = exports[name] = YFunction(fn);
         YObject.fn[name] = fn.methodize();
index aaccdd5..d744531 100644 (file)
@@ -7,18 +7,23 @@ var Y = require('Y').Y
 ,   NUM_SAMPLES = 33
 ,
 
-methods = {
-    // framerate : 0, // Target framerate
-    // frametime : 0, // 1000 / framerate
-    // samples   : NUM_SAMPLES, // Number of frames to track for effective fps
-    // 
-    // now       : 0, // Last tick time (ms)
-    // fps       : 0, // Effective framerate
-    // ticks     : 0, // Number of ticks since start
-    // 
-    // timer     : null,
-    // running   : false,
-    // times     : null, // Last `samples` frame durations
+
+EventLoop =
+exports['EventLoop'] =
+Emitter.subclass('EventLoop', {
+    samples   : NUM_SAMPLES, // Number of frames to track for effective fps
+    dilation  : 1.0,
+    
+    framerate : 0, // Target framerate
+    frametime : 0, // 1000 / framerate
+    
+    now       : 0, // Last tick time (ms)
+    fps       : 0, // Effective framerate
+    ticks     : 0, // Number of ticks since start
+    
+    timer     : null,
+    running   : false,
+    times     : null, // Last `samples` frame durations
     
     
     /**
@@ -37,8 +42,11 @@ methods = {
         
         this.framerate  = framerate;
         this.targetTime = 1000 / framerate;
-        this.samples    = samples || NUM_SAMPLES;
-        this.dilation   = dilation || 1.0;
+        
+        if (samples !== undefined)
+            this.samples = samples;
+        if (dilation !== undefined)
+            this.dilation = dilation;
         
         this.reset();
     },
@@ -118,19 +126,16 @@ methods = {
         return (this.realtimes.reduce(Y.op.add,0) / this.realtimes.length);
     }
     
-}, 
-
-EventLoop =
-exports['EventLoop'] =
-Emitter.subclass('EventLoop', methods)
+})
 ;
 
+
 function decorate(delegate){
     if (!delegate) return;
-    Emitter.prototype.decorate.call(this, delegate);
+    Emitter.fn.decorate.call(this, delegate);
     ['start', 'stop', 'reset']
         .forEach(function(k){
-            delegate[k] = methods[k].bind(this);
+            delegate[k] = EventLoop.fn[k].bind(this);
         }, this);
     return delegate;
 }
similarity index 84%
rename from src/tanks/util/config.cjs
rename to src/ezl/util/configuration.cjs
index 2b90bba..539bd0a 100644 (file)
@@ -1,20 +1,28 @@
 var Y = require('Y').Y
+,   getNested = require('Y/types/object').getNested
 ,   Emitter = require('Y/modules/y.event').Emitter
 ,
 
 
 Config =
-exports.Config =
-Y.YObject.subclass('Config', function(){
+exports['Config'] =
+Y.YObject.subclass('Config', function(Config){
     
     Y.core.extend(this, {
         
         init : function initConfig(defaults){
-            this._defaults = defaults;
-            this._o = Y({}, defaults);
+            this._defaults = defaults || {};
+            this._o = Y({}, this._defaults);
             this.__emitter__ = new Emitter(this);
         },
         
+        clone : function clone(){
+            var c = new Config();
+            c._defaults = this._defaults;
+            c._o = c._o.clone();
+            return c;
+        },
+        
         set : function set(key, value, def){
             if ( key !== undefined ){
                 var meta = this.ensure(key).getNested(key, def, true)
@@ -93,8 +101,14 @@ Y.YObject.subclass('Config', function(){
             
             state.path.pop();
             return state;
+        },
+        
+        getDefault : function getDefault(k, def){
+            return getNested(this._defaults, k, def);
         }
         
     });
     
+    this['get'] = this.getNested;
+    
 });
index aa180cf..2a1250b 100644 (file)
@@ -1,24 +1,26 @@
 //  -*- mode: JavaScript; tab-width: 4; indent-tabs-mode: nil; -*-
 var Y = require('Y').Y
+,   Config = require('y/modules/y.config').Config
+,
 
-,   defaults =
+defaults =
 exports['defaults'] = {
     game : {
         timeDilation       : 1.0,
         gameoverDelay      : 1000
     },
     ui : {
-        createGridCanvas   : 1,
-        createGridTable    : 0,
-        showGridCoords     : 0,
-        showAttackCooldown : 0,
+        createGridCanvas   : true,
+        createGridTable    : false,
+        showGridCoords     : false,
+        showAttackCooldown : true,
         showCountdown      : (document.location.host.toString() !== 'tanks.woo')
     },
     pathing : { 
-        overlayAIPaths     : 0,
-        overlayPathmap     : 0,
-        traceTrajectories  : 0
+        overlayAiPaths     : false,
+        overlayPathmap     : false,
+        traceTrajectories  : false
     }
 };
 
-exports['values'] = Y(defaults).clone().end();
+exports['values'] = new Config(defaults);
index 4d0787d..0926019 100644 (file)
@@ -19,10 +19,9 @@ var Y         = require('Y').Y
 Game =
 exports['Game'] = 
 Y.subclass('Game', {
-    overlayPathmap : config.pathing.overlayPathmap,
-    overlayAIPaths : config.pathing.overlayAIPaths,
-    timeDilation   : config.game.timeDilation,
-    gameoverDelay  : config.game.gameoverDelay,
+    overlayPathmap : config.get('pathing.overlayPathmap'),
+    overlayAiPaths : config.get('pathing.overlayAiPaths'),
+    gameoverDelay  : config.get('game.gameoverDelay'),
     
     gameover      : false,
     
@@ -37,7 +36,7 @@ Y.subclass('Game', {
         this.animations = new Y.YArray();
         
         this.viewport = $(viewport || GRID_ELEMENT);
-        this.loop = new EventLoop(FRAME_RATE, this, this.timeDilation);
+        this.loop = new EventLoop(FRAME_RATE, this);
         
         this.root = 
         this.grid = 
@@ -92,7 +91,7 @@ Y.subclass('Game', {
         SECONDTH = ELAPSED / 1000;
         SQUARETH = REF_SIZE * SECONDTH
         
-        if (!this.overlayAIPaths)
+        if (!this.overlayAiPaths)
             this.pathmap.hidePaths();
         
         this.active.invoke('updateCooldowns', ELAPSED, NOW);
index db6d81f..ac86997 100644 (file)
@@ -38,12 +38,12 @@ Rect.subclass('Level', {
         
         P = 
         game.player = game.addUnit(new PlayerTank(1), 5,9);
-        game.addUnit(new Tank(1).colors('#4596FF', '#182B53', '#F25522'), 3,9);
+        // game.addUnit(new Tank(1).colors('#4596FF', '#182B53', '#F25522'), 3,9);
         
         E = 
         game.addUnit(new Tank(2), 0,1);
         game.addUnit(new Tank(2), 1,0);
-        game.addUnit(new Tank(2), 8,1);
+        // game.addUnit(new Tank(2), 8,1);
     },
     
     addWall : function addWall(x,y, w,h, isBoundary){
index c107629..5e29503 100644 (file)
@@ -1,16 +1,20 @@
 //  -*- mode: JavaScript; tab-width: 4; indent-tabs-mode: nil; -*-
 var Y        = require('Y').Y
 ,   math     = require('ezl/math')
-,   Vec      = math.Vec
-,   Line     = math.Line
 ,   QuadTree = require('ezl/util/tree/quadtree').QuadTree
-,   Bullet   = require('tanks/thing/bullet').Bullet
 ,   astar    = require('ezl/util/astar')
+,   Bullet   = require('tanks/thing/bullet').Bullet
+,   config   = require('tanks/config').values
+,   Vec      = math.Vec
+,   Line     = math.Line
 ,
 
 PathMap =
 exports['PathMap'] =
 QuadTree.subclass('PathMap', {
+    overlayAiPaths : config.get('pathing.overlayAiPaths'),
+    overlayPathmap : config.get('pathing.overlayPathmap'),
+    
     gridSquareSize  : REF_SIZE,
     gridSquareMidPt : new Vec(REF_SIZE/2, REF_SIZE/2),
     
@@ -220,7 +224,7 @@ QuadTree.subclass('PathMap', {
         ,   path = Y(astar.search(grid, startN, endN))
         ;
         
-        if (tanks.config.values.pathing.overlayAIPaths)
+        if (this.overlayAiPaths)
             this.drawPath(id, startN, path);
         
         return path
index 2f2b155..cf341d6 100644 (file)
@@ -6,6 +6,7 @@ var Y          = require('Y').Y
 ,   Thing      = require('tanks/thing/thing').Thing
 ,   Trajectory = require('tanks/map/trajectory').Trajectory
 ,   Explosion  = require('tanks/fx/explosion').Explosion
+,   config     = require('tanks/config').values
 ,   Line       = shape.Line
 ,   Circle     = shape.Circle
 ,
@@ -131,7 +132,7 @@ Thing.subclass('Bullet', {
     render : function render(parent){
         this.remove();
         
-        if (tanks.config.values.pathing.traceTrajectories) {
+        if (config.get('pathing.traceTrajectories')) {
             var t = this.trajectory;
             this.tline = Line.fromPoints(t.x1,t.y1, t.x2,t.y2)
                 .attr('drawDefinitionPoints', true)
index 24cf897..e0e634a 100644 (file)
@@ -14,7 +14,7 @@ var Y           = require('Y').Y
 Thing =
 exports['Thing'] =
 new evt.Class('Thing', {
-    showAttackCooldown : config.ui.showAttackCooldown,
+    showAttackCooldown : config.get('ui.showAttackCooldown'),
     
     // Attributes
     stats: {
diff --git a/src/tanks/ui/config.cjs b/src/tanks/ui/config.cjs
deleted file mode 100644 (file)
index 0f544e3..0000000
+++ /dev/null
@@ -1,88 +0,0 @@
-//#ensure "jquery"
-
-
-// TODO
-// ====
-// - store at tanks.config.values, but clone at start to t.c.defaults
-// - need Config class to run triggers on changes
-// - button to clear saved config
-// 
-// init
-// - For each item in tanks.config (recursive):
-// - Create a UI box based on type, with a name based on its dotted path
-// - Check cookies -- fill it with the value from cookies or default
-// 
-// update
-// - For each config element:
-// - Extract value, typecast based on default's type
-// - If different from default, set cookie
-// - Update tanks.config.values
-
-var Y = require('Y').Y
-,   config = require('tanks/config')
-,   c = config.values
-,   p = c.pathing
-;
-
-
-
-
-exports['init'] = function initConfigUi(){
-    $('#config [name=pathmap]').attr('checked', p.overlayPathmap);
-    $('#config [name=aipaths]').attr('checked', p.overlayAIPaths);
-    $('#config [name=trajectories]').attr('checked', p.traceTrajectories);
-    
-    $('#config [name=gridCoords]').attr('checked', c.ui.showGridCoords);
-    $('#viewport .layer.grid')[(c.ui.showGridCoords ? 'add' : 'remove')+'Class']('showGridCoords');
-};
-
-exports['update'] = function updateConfigUi(evt){
-    p.overlayPathmap    = $('#config [name=pathmap]').attr('checked');
-    p.overlayAIPaths    = $('#config [name=aipaths]').attr('checked');
-    p.traceTrajectories = $('#config [name=trajectories]').attr('checked');
-    
-    c.ui.showGridCoords = $('#config [name=gridCoords]').attr('checked');
-    $('#viewport .layer.grid')[(c.ui.showGridCoords ? 'add' : 'remove')+'Class']('showGridCoords');
-};
-
-// var templates = {
-//     'label'    : { html:'<label for="{id}">{label}</label>' },
-//     'input'    : { html:'<input id="{id}" type="{type}" name="{key}" value="{value}">' },
-//     'text'     : { include:'input', type:'text' },
-//     'checkbox' : { include:'input', type:'checkbox', props:{ 'checked':false } },
-//     'radio'    : { include:'input', type:'radio' }
-//     // 'select'   : '',
-//     // 'textarea' : '',
-//     // 'hidden'   : '',
-//     // 'button'   : ''
-// };
-// 
-// var type2el = {
-//     'Boolean'
-//     'Number' : 
-//     'String' :