Adds viewport element to deal with larger levels.
authordsc <david.schoonover@gmail.com>
Mon, 28 Feb 2011 04:36:23 +0000 (20:36 -0800)
committerdsc <david.schoonover@gmail.com>
Mon, 28 Feb 2011 04:36:23 +0000 (20:36 -0800)
12 files changed:
data/types/levels.yaml
src/evt.cjs
src/ezl/layer/layer.cjs
src/ezl/layer/layerable.cjs
src/tanks/game.cjs
src/tanks/map/level.cjs
src/tanks/ui/grid.cjs
src/tanks/ui/index.cjs
src/tanks/ui/pathmapui.cjs
src/tanks/ui/viewport.cjs [new file with mode: 0644]
www/css/lttl.css
www/welcome.html

index af501f8..8fda80c 100644 (file)
@@ -13,8 +13,7 @@ types:
     test:
         name: Da Test
         desc: A test level.
-        width: 500
-        height: 500
+        levelSize: 500 500
         bounds:
           - [-50,-50, 600,50]
           - [-50,0, 50,500]
@@ -76,8 +75,7 @@ types:
     three_test:
         name: Three Test
         desc: A more complex test level.
-        width: 500
-        height: 500
+        levelSize: 500 500
         # bounds: [[-1,-1], [501,-1], [501,351], [351,351], [351,501], [-1,501]]
         # bounds:
         #     top:    [[-50,-50, 600,50]]
@@ -150,8 +148,7 @@ types:
     autotest:
         name: Automated AI Test
         desc: AI test level.
-        width: 500
-        height: 500
+        levelSize: 500 500
         bounds:
           - [-50,-50, 600,50]
           - [-50,0, 50,500]
@@ -217,8 +214,7 @@ types:
     los_test:
         name: Line-of-Sight Test
         desc: LOS test level.
-        width:  450
-        height: 450
+        levelSize: 450 450
         bounds:
           - [-50,-50, 550,50]
           - [-50,0,   50,450]
index d386765..88918b4 100644 (file)
@@ -118,7 +118,6 @@ function createInitialise(cls){
         ,   proto = cls.fn          // use prototype so super() calls pick up the right lists
         ,   binds = proto.__bind__  // list of names to bind
         ,   inits = proto.__inits__ // init functions
-        ,   super = cls.__super__
         ,   args  = Y(arguments)
         ;
         
@@ -127,8 +126,11 @@ function createInitialise(cls){
         
         if ( inits && inits.length )
             inits.invoke('apply', instance, args);
-        else if (super && super.init )
-            super.init.apply(instance, args);
+        else if ( cls.__bases__ ) {
+            var superInits = super(cls, '__inits__');
+            if ( superInits && superInits.length )
+                superInits.invoke('apply', instance, args);
+        }
         
         instance.emit('init', instance, {
             'instance' : instance,
@@ -441,6 +443,30 @@ function mixin(cls, _mxn){
 }
 
 
+/**
+ * Looks up a key on the first prototype in the chain.
+ * @param {Class} cls Class on which to perform lookup.
+ * @param {String} key Key to lookup.
+ * @return {*} First non-undefined value found in the prototypes of the __bases__ chain. (nb. This does not include cls.)
+ */
+function super(cls, key){
+    if ( !key )
+        return;
+    if ( !(cls instanceof Class) && cls.__class__ )
+        return super(cls.__class__, key); // cls = cls.__class__;
+    return cls.__bases__ // don't check cls.fn[key] -- we want super only
+        .reduce(function(v, base){
+            return (v !== undefined ? v : base.prototype[key]);
+        }, undefined);
+}
+
+/**
+ * Aggregate the objects up the prototypes in the __bases__ chain, merging together their properties
+ * such that the deepest subclasses supersceede their parents.
+ * @param {Class} cls Class on which to perform lookup.
+ * @param {String} key Key to lookup.
+ * @return {Object} Combined values from objects at key found in the prototypes of the __bases__ chain. (nb. This does not include cls.)
+ */
 function aggregate(cls, key){
     if ( !(cls instanceof Class) && cls.__class__ )
         cls = cls.__class__;
@@ -454,6 +480,21 @@ function aggregate(cls, key){
         }, {});
 }
 
+/**
+ * Looks up a key on the Classes in the __bases__ chain.
+ * @param {Class} cls Class on which to perform lookup.
+ * @param {String} key Key to lookup.
+ * @return {*} First non-undefined value found in the __bases__ chain. (nb. This does not include cls.)
+ */
+function superStatic(cls, key){
+    if ( !(cls instanceof Class) && cls.__class__ )
+        return superStatic(cls.__class__, key); // cls = cls.__class__;
+    return cls.__bases__ // don't check cls[key] -- we want super only
+        .reduce(function(v, base){
+            return (v !== undefined ? v : base[key]);
+        }, undefined);
+}
+
 
 Mixin.on('subclass',
     function onMixinSubclass(evt){
@@ -479,3 +520,4 @@ exports['fabricate']   = Y.fabricate;
 exports['lookupClass'] = lookupClass;
 exports['mixin']       = mixin;
 exports['aggregate']   = aggregate;
+exports['superStatic'] = superStatic;
index a17d7e6..c7d8ec1 100644 (file)
@@ -6,13 +6,7 @@ var Y = require('Y')
 Layer =
 exports['Layer'] =
 evt.subclass('Layer', {
-    __mixins__ : [ Layerable ],
-    __bind__   : [],
-    
-    
-    init : function initLayer(props, attrs, html){
-        // Layer.init.call(this, props, attrs, html);
-    }
+    __mixins__ : [ Layerable ]
     
 })
 ;
index 64ec7f6..5c46bfc 100644 (file)
@@ -404,6 +404,10 @@ Mixin.subclass('Layerable', {
     /** Position relative to document. */
     offset : makeDelegate('offset'),
     
+    scrollLeft   : makeDelegate('scrollLeft'),
+    scrollTop    : makeDelegate('scrollTop'),
+    scrollWidth  : function scrollWidth(v){  return this.layer.attr('scrollWidth',  v); },
+    scrollHeight : function scrollHeight(v){ return this.layer.attr('scrollHeight', v); },
     
     
     /// Transformations ///
@@ -736,8 +740,14 @@ Mixin.subclass('Layerable', {
     
     /// 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()+' )';
+        var pos  = { 'top':NaN, 'left':NaN }
+        ,   kids = this.children ? this.children.length : 0
+        ,   el   = this.layer
+        ;
+        // Check we're in the DOM, otherwise jQuery vomits
+        if ( el && el.attr('parentNode') )
+            pos = this.position();
+        return this.className+'['+pos.left+','+pos.top+']( children='+kids+' )';
     }
 });
 
index b225073..da01c55 100644 (file)
@@ -9,6 +9,7 @@ var Y         = require('Y').Y
 
 ,   EventLoop = require('ezl/loop').EventLoop
 ,   Layer     = require('ezl/layer/layer').Layer
+,   HtmlLayer = require('ezl/layer/html').HtmlLayer
 ,   Circle    = require('ezl/shape').Circle
 
 ,   config    = require('tanks/config').config
@@ -16,6 +17,7 @@ var Y         = require('Y').Y
 ,   Level     = require('tanks/map/level').Level
 ,   Grid      = require('tanks/ui/grid').Grid
 ,   PathMapUI = require('tanks/ui/pathmapui').PathMapUI
+,   Viewport  = require('tanks/ui/viewport').Viewport
 ,   Backpack  = require('tanks/ui/inventory/backpack').Backpack
 
 ,   Thing  = thing.Thing
@@ -53,14 +55,14 @@ evt.subclass('Game', {
         this.loop = new EventLoop(FRAME_RATE, this);
         
         this.root =
-            new Layer({ hasCanvas:false }, { 'id':'game' })
+            new HtmlLayer({ '_layerId':'game' })
                 .prependTo( $('body') );
         
         this.ui =
-            new Layer({ hasCanvas:false }, { 'id':'ui' })
+            new HtmlLayer({ '_layerId':'ui' })
                 .appendTo( this.root );
         this.viewport = 
-            new Layer({ hasCanvas:false }, { 'id':'viewport' })
+            new Viewport({ '_layerId':'viewport' })
                 .appendTo( this.root );
         this.level =
             Level.create(this.levelId, this, CAPACITY, REF_SIZE)
@@ -68,9 +70,6 @@ evt.subclass('Game', {
         this.grid =
             new Grid(this.level, REF_SIZE)
                 .prependTo( this.viewport );
-        this.viewport
-            .width(this.level.width)
-            .height(this.level.height);
         
         this.map = this.level.map;
         this.map.ui = new PathMapUI(this, this.map);
@@ -83,6 +82,8 @@ evt.subclass('Game', {
         this.on('tick',     this.tick);
         
         this.level.setup(tanks.data);
+        this.viewport.centerOn();
+        
         this.emit('ready', this);
     },
     
index c34779f..435f1ad 100644 (file)
@@ -15,6 +15,7 @@ var Y          = require('Y').Y
 
 ,   min = Y(Math.min).limit(2)
 ,   max = Y(Math.max).limit(2)
+,   toInt = Y(parseInt).limit(1)
 ,
 
 
@@ -26,11 +27,11 @@ Rect.subclass('Level', {
     
     init : function init(game, capacity, buffer_size){
         this.game    = game;
-        this.map = new Map(0,0, this.width, this.height, capacity, buffer_size);
+        this.map = new Map(0,0, this.levelWidth, this.levelHeight, capacity, buffer_size);
         
-        Rect.init.call(this, this.width,this.height);
+        Rect.init.call(this, this.levelWidth,this.levelHeight);
         this.fill('transparent');
-        // var shape = this.shape = new Rect(this.width,this.height).fill('transparent');
+        // var shape = this.shape = new Rect(this.levelWidth,this.levelHeight).fill('transparent');
         // shape.layer.attr('class', this._layerClasses);
         // this.bbox = shape.bbox;
     },
@@ -68,23 +69,30 @@ Rect.subclass('Level', {
     
 });
 
-proxy({ target:Level, donor:Rect, context:'shape', chain:true,
-    names:'append appendTo remove invoke clear'.split(' ') });
+// proxy({ target:Level, donor:Rect, context:'shape', chain:true,
+//     names:'append appendTo remove invoke clear'.split(' ') });
 
 Level.on('speciate',
     function onSpeciate(evt){
         var NewLevel = evt.data.species
         ,   proto = NewLevel.fn
         ,   bounds = Y(proto.bounds)
+        ,   size = proto.levelSize
         ;
         
-        if (!proto.width) {
-            proto.width =
+        if (size) {
+            size = size.split(' ').map(toInt);
+            proto.levelWidth = size[0];
+            proto.levelHeight = size[1];
+        }
+        
+        if (!proto.levelWidth) {
+            proto.levelWidth =
                 bounds.right.pluck(0).reduce(max,0)
                 - bounds.left.pluck(0).reduce(min,Infinity);
         }
-        if (!proto.height) {
-            proto.height =
+        if (!proto.levelHeight) {
+            proto.levelHeight =
                 bounds.bottom.pluck(1).reduce(max,0)
                 - bounds.top.pluck(1).reduce(min,Infinity);
         }
index e1f0cc6..cc82a2c 100644 (file)
@@ -21,8 +21,8 @@ Rect.subclass('Grid', {
     
     init : function init(level, side){
         this.level = level;
-        this.cols = Math.round(level.width/side);
-        this.rows = Math.round(level.height/side);
+        this.cols = Math.round(level.levelWidth/side);
+        this.rows = Math.round(level.levelHeight/side);
         this.side = side;
         Rect.init.call(this, this.cols*side, this.rows*side);
     },
index c70c957..5c41507 100644 (file)
@@ -1,6 +1,10 @@
 var Y = require('Y').Y;
-
-exports['PathMapUI'] = require('tanks/ui/pathmapui').PathMapUI;
-exports['main'] = require('tanks/ui/main');
-exports['config'] = require('tanks/ui/configui');
-exports['inventory'] = require('tanks/ui/inventory');
\ No newline at end of file
+Y.core.extend(exports, {
+    'config'    : require('tanks/ui/configui'),
+    'main'      : require('tanks/ui/main'),
+    'inventory' : require('tanks/ui/inventory'),
+    
+    'Grid'      : require('tanks/ui/grid').Grid,
+    'Viewport'  : require('tanks/ui/viewport').Viewport,
+    'PathMapUI' : require('tanks/ui/pathmapui').PathMapUI
+});
index bb5f017..1f7d5ad 100644 (file)
@@ -41,7 +41,7 @@ Rect.subclass('PathMapUI', {
         
         var level = game.level
         ,   b = this.buffer = map.buffer_size;
-        Rect.init.call(this, level.width, level.height);
+        Rect.init.call(this, level.levelWidth, level.levelHeight);
         // this.posBleed.x = this.posBleed.y = 
         // this.negBleed.x = this.negBleed.y = b;
         
diff --git a/src/tanks/ui/viewport.cjs b/src/tanks/ui/viewport.cjs
new file mode 100644 (file)
index 0000000..e582d55
--- /dev/null
@@ -0,0 +1,115 @@
+var Y = require('Y').Y
+,   Vec       = require('ezl/math/vec').Vec
+,   HtmlLayer = require('ezl/layer/html').HtmlLayer
+
+,   PAD_X = 1024, PAD_Y = 690
+,   min = Y(Math.min).limit(2)
+,   max = Y(Math.max).limit(2)
+,
+
+Viewport =
+exports['Viewport'] =
+HtmlLayer.subclass('Viewport', {
+    _layerId : 'viewport',
+    
+    spacer : null, // inner layer to induce scrolling so we can arbitrarily center the elements
+    
+    
+    
+    init : function initViewport(props, attrs, html){
+        HtmlLayer.init.call(this, props, attrs, html);
+        this.spacer = $('<div class="spacer ezl layer" />').appendTo(this.layer);
+    },
+    
+    get contentWidth(){  return this.children.pluck('layerWidth').reduce(max,0); },
+    get contentHeight(){ return this.children.pluck('layerHeight').reduce(max,0); },
+    
+    /**
+     * Sets the current scroll position of the Viewport. (Unlike the DOM, Viewport has 50% negative scroll-space top and left.)
+     * @param {Number} x Left offset in pixels.
+     * @param {Number} y Top offset in pixels.
+     * @return {this}
+     */
+    /**
+     * Gets the current scroll position of the Viewport.
+     * @return {Vec} Current scroll position (left, top).
+     */
+    scroll : function scroll(x,y){
+        var el = this.layer;
+        if (arguments.length === 0)
+            return new Vec(el.scrollLeft()-PAD_X, el.scrollTop()-PAD_Y);
+        
+        if (x instanceof Array) { y=x[1]; x=x[0]; }
+        
+        if (x !== undefined) el.scrollLeft( x + PAD_X );
+        if (y !== undefined) el.scrollTop(  y + PAD_Y );
+        
+        return this;
+    },
+    
+    
+    /**
+     * Scrolls the viewport to place (x,y) at the center.
+     * Note: Viewport can be centered on values ranging from -100% to +100% of width and height.
+     * @param {Number} [x=undefined] Desired center x-coordinate. If undefined, half contentWidth will be used.
+     * @param {Number} [y=undefined] Desired center y-coordinate. If undefined, half contentHeight will be used.
+     * @return {this}
+     */
+    centerOn : function centerOn(x,y){
+        if (x === undefined) x = this.contentWidth/2;
+        if (y === undefined) y = this.contentHeight/2;
+        var el = this.layer
+        ,   w2 = el.width()/2, h2 = el.height()/2;
+        return this.scroll(x-w2, y-h2);
+    },
+    
+    
+    /// Debug ///
+    
+    createGrid : function createGrid(step, maxX, maxY){
+        step = step || 50;
+        maxX = maxX || _X*3;
+        maxY = maxY || _Y*3;
+        
+        // $('.chrome').hide();
+        $('#pen, #mapblotter').remove();
+        
+        var map = $('#viewport .map');
+        $('<div id="mapblotter" class="ezl layer blotter" />')
+            .css({
+                'background-color': '#3F3F3F', 'z-index':-1,
+                'width':map.width(), 'height':map.height()
+            })
+            .appendTo(this.layer);
+        var pen = $('<div id="pen" />')
+            .css({
+                'position': 'absolute',
+                'left': 0, 'top': 0, 'margin': 0,
+                'z-index':-10
+            })
+            .appendTo(this.layer);
+        
+        Y(0,maxX,step).forEach(function(x, xth){
+            Y(0,maxY,step).forEach(function(y, yth){
+                var isColored = (xth % 2) == (yth % 2);
+                $('<div class="sq">'+x+','+y+'</div>')
+                    .css({
+                        'opacity': 0.05,
+                        'position': 'absolute', 'display':'table-cell',
+                        'left': x, 'top': y,
+                        'width': step, 'height': step,
+                        'margin': 0, 'padding': 0,
+                        'font'  : '8px/'+step+'px Menlo', 'text-align' : 'center',
+                        'color' : (isColored ? '#000' : '#fff'),
+                        'background-color': (isColored ? '#fff' : 'transparent')
+                    })
+                    .appendTo(pen)
+                ;
+            });
+        });
+        
+        return this;
+    }
+    
+})
+;
\ No newline at end of file
index e142e97..d6496cd 100644 (file)
@@ -1,4 +1,4 @@
-html, body { width:100%; height:100%; top:0; left:0; margin:0; padding:0; background-color:#3F3F3F; }
+html, body { width:100%; height:100%; top:0; left:0; margin:0; padding:0; background-color:#333; }
 body { position:absolute; font-family:Geogrotesque,Helvetica; font-size:12pt; color:#fff; }
 h1, h2, h3, h4, h5 { margin:0.75em 0; font-weight:normal;  }
 ul, ol, li { list-style: none ! important; margin:0; padding:0; }
@@ -46,16 +46,25 @@ td { text-align:center; vertical-align:middle; }
     #notes li { margin-left:1em; }
 
 
-#game { position:absolute; width:100%; height:100%; margin:0; padding:0; }
-#viewport { position:relative; margin:1em auto; cursor:crosshair; width:auto; /* width:500px; height:500px;  overflow:hidden; top:50px; left:50px; */ }
-    #viewport .layer.grid { outline:1px solid rgba(255,255,255,0.1); }
+/*** Game and Viewport ***/
+
+#game { position:relative; width:100%; height:100%; margin:0; padding:0; }
+#viewport { position:relative; width:100%; height:100%; max-width:1024px; max-height:690px;
+        overflow:hidden; cursor:crosshair; margin:0 auto; background-color:#3F3F3F; }
+    #viewport > .layer { margin:690px 1024px; }
+    #viewport > .spacer { position:absolute; z-index:-1; width:1024px; height:690px; /* width:100%; height:100%; */ }
+    #viewport .layer.grid { outline:1px solid rgba(255,255,255, 0.1); }
+    #viewport .ezl.layer.map { }
+    #viewport .ezl.layer.pathmap { z-index:50; }
+
+
 
 .item-container { position:absolute; }
     .item-container h3 { color:#fff; margin:0.25em; padding:0.5em 0; font-size:0.8em; }
     .item-container .slot { float:left; position:relative; width:50px; height:50px; margin:0.25em; border:1px solid transparent; }
     .item-container .slot.drophover { border:1px solid #FFF6AE; }
 
-#ui { position:absolute; bottom:0; right:0; left:auto; top:auto; width:auto; }
+#ui { position:absolute; bottom:0; right:0; left:auto; top:auto; width:auto; z-index:500; }
     #ui > .bag { position:relative; float:right; margin:0 0 1em 1em; }
     #ui .equipslot .slot { background-position:center; background-repeat:no-repeat; }
     #ui .equipslot .slot.occupied { background-image:none; }
@@ -97,8 +106,6 @@ table.grid td { /* outline:1px solid rgba(255,255,255,0.1); */
     margin:0; padding:0; white-space:nowrap; overflow:hidden; }
 .showGridCoords table.grid td:hover { color:rgba(255,255,255,0.1); }
 
-.ezl.layer.pathmap { z-index:50; }
-
 /* 
 #debug { position:relative; top:1em; right:1em; z-index:5000; }
     #debug .inner { position:absolute; top:0; right:0; padding:1em; }
index d2c140a..3c7ba77 100644 (file)
@@ -1,4 +1,4 @@
-<div id="loading" class="bigblue">
+<div id="loading" class="bigblue chrome">
     <div class="box hud">
         <h1>The Littlest Battletank</h1>
         
@@ -6,7 +6,7 @@
     </div>
 </div>
 
-<div id="welcome" class="bigblue" style="display:none">
+<div id="welcome" class="bigblue chrome" style="display:none">
     <div class="box hud">
         <h1>The Littlest Battletank</h1>