Adds alpha replay support.
authordsc <david.schoonover@gmail.com>
Tue, 11 Jan 2011 11:25:59 +0000 (03:25 -0800)
committerdsc <david.schoonover@gmail.com>
Tue, 11 Jan 2011 11:25:59 +0000 (03:25 -0800)
data/config.yaml
lib/strftime.js [new symlink]
lib/strftime.min.js [new file with mode: 0644]
src/ezl/math/vec.cjs
src/tanks/game.cjs
src/tanks/map/level.cjs
src/tanks/map/pathing/traversal.cjs
src/tanks/thing/bullet.cjs
src/tanks/thing/player.cjs
src/tanks/thing/tank.cjs
src/tanks/ui/main.cjs

index 52788cd..e32760c 100644 (file)
@@ -4,6 +4,7 @@ game:
     gameoverDelay       : 1000
 debug:
     showFpsGraph        : false
+    enableGameLogging   : false
     # createGridTable    : false
     # showGridCoords     : false
 map:
diff --git a/lib/strftime.js b/lib/strftime.js
new file mode 120000 (symlink)
index 0000000..996f99b
--- /dev/null
@@ -0,0 +1 @@
+strftime.min.js
\ No newline at end of file
diff --git a/lib/strftime.min.js b/lib/strftime.min.js
new file mode 100644 (file)
index 0000000..50a9444
--- /dev/null
@@ -0,0 +1 @@
+Number.prototype.pad=function(c,b){var a=""+this;b=b||"0";while(a.length<c){a=b+a}return a};Date.prototype.months=["January","February","March","April","May","June","July","August","September","October","November","December"];Date.prototype.weekdays=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];Date.prototype.dpm=[31,28,31,30,31,30,31,31,30,31,30,31];Date.prototype.strftime_f={A:function(a){return a.weekdays[a.getDay()]},a:function(a){return a.weekdays[a.getDay()].substring(0,3)},B:function(a){return a.months[a.getMonth()]},b:function(a){return a.months[a.getMonth()].substring(0,3)},C:function(a){return Math.floor(a.getFullYear()/100)},c:function(a){return a.toString()},D:function(a){return a.strftime_f.m(a)+"/"+a.strftime_f.d(a)+"/"+a.strftime_f.y(a)},d:function(a){return a.getDate().pad(2,"0")},e:function(a){return a.getDate().pad(2," ")},F:function(a){return a.strftime_f.Y(a)+"-"+a.strftime_f.m(a)+"-"+a.strftime_f.d(a)},H:function(a){return a.getHours().pad(2,"0")},I:function(a){return((a.getHours()%12||12).pad(2))},j:function(c){var b=c.getDate();var a=c.getMonth()-1;if(a>1){var e=c.getYear();if(((e%100)==0)&&((e%400)==0)){++b}else{if((e%4)==0){++b}}}while(a>-1){b+=c.dpm[a--]}return b.pad(3,"0")},k:function(a){return a.getHours().pad(2," ")},l:function(a){return((a.getHours()%12||12).pad(2," "))},M:function(a){return a.getMinutes().pad(2,"0")},m:function(a){return(a.getMonth()+1).pad(2,"0")},n:function(a){return"\n"},p:function(a){return(a.getHours()>11)?"PM":"AM"},R:function(a){return a.strftime_f.H(a)+":"+a.strftime_f.M(a)},r:function(a){return a.strftime_f.I(a)+":"+a.strftime_f.M(a)+":"+a.strftime_f.S(a)+" "+a.strftime_f.p(a)},S:function(a){return a.getSeconds().pad(2,"0")},s:function(a){return Math.floor(a.getTime()/1000)},T:function(a){return a.strftime_f.H(a)+":"+a.strftime_f.M(a)+":"+a.strftime_f.S(a)},t:function(a){return"\t"},u:function(a){return(a.getDay()||7)},v:function(a){return a.strftime_f.e(a)+"-"+a.strftime_f.b(a)+"-"+a.strftime_f.Y(a)},w:function(a){return a.getDay()},X:function(a){return a.toTimeString()},x:function(a){return a.toDateString()},Y:function(a){return a.getFullYear()},y:function(a){return(a.getYear()%100).pad(2)},"%":function(a){return"%"}};Date.prototype.strftime_f["+"]=Date.prototype.strftime_f.c;Date.prototype.strftime_f.h=Date.prototype.strftime_f.b;Date.prototype.strftime=function(a){var b="";var e=0;while(e<a.length){var d=a.substring(e,e+1);if(d=="%"){d=a.substring(++e,e+1);b+=(this.strftime_f[d])?this.strftime_f[d](this):d}else{b+=d}++e}return b};
\ No newline at end of file
index 6c9f154..f9733c1 100644 (file)
@@ -97,6 +97,10 @@ new Y.Class('Vec', [], {
         return new this.__class__(this[_X],this[_Y]);
     },
     
+    toObject : function toObject(){
+        return { 'x':this[_X], 'y':this[_Y] };
+    },
+    
     toString : function(){
         var p = 2, x = this[_X], y = this[_Y];
         x = ((x % 1 !== 0) ? x.toFixed(p) : x);
index 3a50a85..f6a1bc7 100644 (file)
@@ -1,5 +1,6 @@
 //#ensure "future"
 //#ensure "jquery"
+//#ensure "strftime"
 
 var Y         = require('Y').Y
 ,   Event     = require('Y/modules/y.event').Event
@@ -25,12 +26,12 @@ Y.subclass('Game', {
     // Config
     gameoverDelay : null,
     
-    // Defaults
+    // Instance
     gameover : false,
+    isReplay : false,
     
     
-    
-    init : function initGame(viewport){
+    init : function initGame(replayFile){
         Y.bindAll(this);
         
         this.byId = {};
@@ -39,7 +40,7 @@ Y.subclass('Game', {
         this.bullets    = new Y.YArray();
         this.animations = new Y.YArray();
         
-        this.viewport = $(viewport || GRID_ELEMENT);
+        this.viewport = $(GRID_ELEMENT);
         this.loop = new EventLoop(FRAME_RATE, this);
         
         this.root =
@@ -60,7 +61,12 @@ Y.subclass('Game', {
         this.addEventListener('tick', this.tick);
         
         this.level.setup();
-        this.fire('ready', this);
+        
+        if (replayFile) {
+            this.isReplay = true;
+            this.loadLog(replayFile);
+        } else
+            this.fire('ready', this);
     },
     
     destroy : function destroy(){
@@ -213,6 +219,42 @@ Y.subclass('Game', {
     },
     
     
+    saveLog : function saveLog(name){
+        var gameLog = this.player.gameLog;
+        if (!gameLog.length)
+            return this;
+        
+        var url = new Date().strftime('/gamelogs/%Y%m%d-%H%M%S'+(name ? '-'+name : '')+'.json');
+        jQuery.ajax({
+            'url'     : url,
+            'type'    : 'PUT',
+            'data'    : JSON.stringify(gameLog),
+            'success' : function(data, textStatus){
+                console.log('success!', data, textStatus);
+            },
+            'error'   : function(xhr, textStatus, errorThrown){
+                console.error('error!', xhr, textStatus, errorThrown);
+            }
+        });
+        console.log('Saving log to //'+(window.location.host)+url+' ...');
+        
+        return this;
+    },
+    
+    loadLog : function loadLog(file){
+        var self = this
+        ,   url = '/gamelogs/'+file;
+        jQuery.get(url, function(replayData){
+            if (!replayData)
+                return console.error('No replay data found!');
+            self.player.setReplay(replayData);
+            console.log('Replay loaded!');
+            self.fire('ready', self);
+        });
+        console.log('Loading replay log from //'+(window.location.host)+url+' ...');
+        return this;
+    },
+    
     
     /**
      * @obsolete
index 393b6eb..1989d25 100644 (file)
@@ -43,7 +43,7 @@ new evt.Class('Level', {
         this.bbox = shape.bbox;
     },
     
-    setup : function setup(){
+    setup : function setup(isReplay){
         var game = this.game;
         
         this.bounds =
@@ -58,6 +58,8 @@ new evt.Class('Level', {
         
         var data = tanks.data.units;
         this.units.map(function(x){
+            // if (isReplay && x.type === 'player')
+            //     return null;
             var obj = data[x.type].instantiate(x.align);
             return game.addThing(obj, x.loc[0], x.loc[1]);
         });
index bc0ca25..9afb802 100644 (file)
@@ -153,8 +153,7 @@ Y.subclass('Traversal', {
             to = tr.pointAtY(B.y2 + 1 + bb.originBottom );
             
         } else {
-            console.error('Null reflection line!', 'to:', to, 'blocker:', blocker);
-            // throw new Error('Null reflection line! to:'+to+', blocker:'+blocker);
+            console.warn('Null reflection line!', 'to:', to, 'blocker:', blocker);
         }
         
         return to;
index e99be37..1401851 100644 (file)
@@ -127,7 +127,7 @@ Thing.subclass('Bullet', {
         // Reflection!
         if ( unit.isReflective && this.bounceLimit >= ++this.bounces ) {
             if (!tvsl.side)
-                return console.error('Null reflection line!', 'to:', to, 'blocker:', unit, 'blockers:', tvsl.blockers._o);
+                return console.warn('Null reflection line!', 'to:', to, 'blocker:', unit, 'blockers:', tvsl.blockers._o);
             
             ng = vec.reflect(traj.p2, tvsl.side);
             traj.reset(to.x,to.y, ng.x,ng.y);
index 78586c0..c12bba2 100644 (file)
@@ -2,6 +2,9 @@
 //#ensure "jquery.hotkeys"
 
 var Y           = require('Y').Y
+,   Vec         = require('ezl/math/vec').Vec
+
+,   config      = require('tanks/config').config
 ,   Tank        = require('tanks/thing/tank').Tank
 ,   Inventoried = require('tanks/mixins/inventoried').Inventoried
 ,
@@ -12,6 +15,8 @@ exports['Player'] =
 Tank.subclass('Player', {
     __mixins__ : [ Inventoried ],
     
+    enableGameLogging : false,
+    
     // Attributes
     stats: {
         hp        : 1,          // health
@@ -25,28 +30,44 @@ Tank.subclass('Player', {
     /// Instance ///
     
     align : null,
+    replayMode : false,
     
     
     
-    init : function init(align){
+    init : function init(align, replay){
         Tank.init.call(this, align);
         
-        this.activeKeys = new Y.YArray();
+        this.gameLog = [];
         this.queue = [];
+        this.activeKeys = new Y.YArray();
+        
+        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);
-        
-        $(document)
-            .bind('keydown',   this.keydown)
-            .bind('keyup',     this.keyup)
-            .bind('mousedown', this.mousedown)
-            .bind('mouseup',   this.mouseup)
-            .bind('mousemove', this.mousemove)
-            ;
+        if (this.replayMode) {
+            this.replay = replay
+            this.nextMove = replay.shift();
+            
+        } else {
+            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);
+            
+            $(document)
+                .bind('keydown',   this.keydown)
+                .bind('keyup',     this.keyup)
+                .bind('mousedown', this.mousedown)
+                .bind('mouseup',   this.mouseup)
+                .bind('mousemove', this.mousemove)
+                ;
+        }
+    },
+    
+    setReplay : function setReplay(replay){
+        this.replayMode = true;
+        this.replay     = replay
+        this.nextMove   = replay.shift();
     },
     
     activeKeys : null,
@@ -55,8 +76,7 @@ Tank.subclass('Player', {
     
     queue : null,
     
-    
-    
+    first : true,
     act : function act(elapsed, now){
         this.elapsed = elapsed;
         this.now = now;
@@ -64,25 +84,61 @@ Tank.subclass('Player', {
         if (this.dead)
             return this;
         
-        var action = this.queue.shift();
-        if (action && action.type === 'fire')
-            this.shoot(action.x, action.y);
+        var action;
+        if (this.replayMode) {
+            action = this.nextMove;
+            
+            if (!action || TICKS < action.ticks)
+                return this;
+            
+            // var drift = TICKS - action.ticks;
+            // console.log('replay drift:', (drift > 0 ? '+' : '')+drift);
+            this.nextMove = this.replay.shift();
+            
+        } else {
+            action = this.queue.shift();
+            
+            if ( !action && this.activeKeys.size() ) {
+                var to = this.getPlayerMove();
+                if (to) action = { 'type':'move', 'data':to };
+            }
+        }
         
-        else if ( this.activeKeys.size() )
-            this.playerMove();
+        if ( !action )
+            return this;
+        
+        var data = action.data;
+        switch (action.type) {
+            case 'shoot':
+                if (!this.replayMode && this.enableGameLogging)
+                    this.gameLog.push({ 'type':'shoot', 'ticks':TICKS, 'data':data.toObject() });
+                this.shoot(data.x, data.y);
+            break;
+            
+            case 'move':
+                this.move(data.x, data.y);
+            break;
+            
+            default:
+                console.error('wtf unknown player action "'+action.type+'"', action);
+            break;
+        }
         
         return this;
     },
     
     attack : function attack(x,y){
-        this.queue.push({
-            type : 'fire',
-            x : x,
-            y : y
-        });
+        if ( x === undefined || y === undefined ) {
+            var theta  = this.barrel.transform.rotate
+            ,   sin = Math.sin(theta),  cos = Math.cos(theta)
+            ,   t = this.getTurretLoc();
+            x = t.x + REF_SIZE*cos;
+            y = t.y + REF_SIZE*sin;
+        }
+        this.queue.push({ type:'shoot', data:new Vec(x,y) });
     },
     
-    playerMove : function playerMove(){
+    getPlayerMove : function getPlayerMove(){
         var dir = this.activeKeys
             .unique()
             .sort()
@@ -91,7 +147,11 @@ Tank.subclass('Player', {
         if (!dir) return;
         
         // @see Tank.move()
-        this.move( this.loc.moveByDir(dir, this.stats.move.val * SQUARETH) );
+        var to = this.loc.moveByDir(dir, this.stats.move.val * SQUARETH);
+        if (!this.replayMode && this.enableGameLogging)
+            this.gameLog.push({ 'type':'move', 'ticks':TICKS, 'data':to.toObject() });
+        
+        return to;
     },
     
     
@@ -104,7 +164,7 @@ Tank.subclass('Player', {
     },
     
     keydown : function keydown(evt){
-        if ( !this.game.loop.running ) return;
+        if ( !this.game.loop.running || this.replayMode ) return;
         
         this.updateMeta(evt);
         
@@ -131,7 +191,7 @@ Tank.subclass('Player', {
     },
     
     mousedown : function mousedown(evt){
-        if ( !this.game.loop.running ) return;
+        if ( !this.game.loop.running || this.replayMode ) return;
         
         switch (evt.which) {
             case 1: evt.leftMouse   = true;
@@ -168,7 +228,7 @@ Tank.subclass('Player', {
     },
     
     mousemove : function mousemove(evt){
-        if ( !this.game.loop.running ) return;
+        if ( !this.game.loop.running || this.replayMode ) return;
         
         this.rotateBarrelRelPage(evt.pageX, evt.pageY);
     }
@@ -200,3 +260,6 @@ exports['Key'] = {
 };
 
 
+config.updateOnChange('debug.enableGameLogging', Player.fn);
+
+
index dfbcbed..17c7c29 100644 (file)
@@ -155,6 +155,7 @@ Thing.subclass('Tank', function(Tank){
         if ( this.nShots >= this.stats.shots.val || !this.cooldowns.attack.ready )
             return null;
         
+        if (x instanceof Array) { y = x[_Y]; x = x[_X]; }
         var xydef = (x !== undefined && y !== undefined);
         if (xydef)
             this.rotateBarrel(x,y);
index 2fdd6d9..b28f224 100644 (file)
@@ -93,11 +93,16 @@ function gameExists(){ return !!tanks.game; }
 function setupGame(){
     if ( gameExists() ) teardownGame();
     
-    game = tanks.game = new Game();
-    
-    game.addEventListener('win',  gameover('You Win!', 'Play Again', ''));
-    game.addEventListener('lose', gameover('You Lose :(', 'Try Again', ''));
-    
+    var replayFile = qkv.replay;
+    game = tanks.game = new Game(replayFile);
+    
+    if (replayFile) {
+        game.addEventListener('win',  gameover('You Won the Replay!', 'Watch Again', ''));
+        game.addEventListener('lose', gameover('You Lost the Replay :(', 'Watch Again', ''));
+    } else {
+        game.addEventListener('win',  gameover('You Win!', 'Play Again', ''));
+        game.addEventListener('lose', gameover('You Lose :(', 'Try Again', ''));
+    }
     ctx = game.level.ctx;
 }