From 8996cc8fca79771369acc71239bbad349308c06e Mon Sep 17 00:00:00 2001 From: dsc Date: Tue, 23 Nov 2010 19:20:16 -0800 Subject: [PATCH] AH HA. Finally found the random nested YCollection bug, which in turn fixed line-of-sight targeting (well, along wiht a lot of code). --- css/lttl.css | 2 +- src/Y/y-array.js | 15 ++- src/Y/y-class.js | 18 +++- src/Y/y-collection.js | 46 ++++--- src/Y/y-function.js | 28 ++-- src/portal/math/vec.js | 6 +- src/tanks/config.js | 5 +- src/tanks/game.js | 183 ++++++++++++++++++++++++++++ src/tanks/game/game.js | 176 --------------------------- src/tanks/main.js | 268 ----------------------------------------- src/tanks/map/level.js | 7 +- src/tanks/map/loc.js | 14 +- src/tanks/map/pathmap.js | 13 ++- src/tanks/map/trajectory.js | 108 ++++++++++++++--- src/tanks/thing/bullet.js | 2 +- src/tanks/thing/player.js | 2 +- src/tanks/thing/tank.js | 18 ++-- src/tanks/thing/thing.js | 16 +-- src/tanks/ui/main.js | 279 +++++++++++++++++++++++++++++++++++++++++++ tanks.php | 9 +- 20 files changed, 663 insertions(+), 552 deletions(-) create mode 100644 src/tanks/game.js delete mode 100644 src/tanks/game/game.js delete mode 100644 src/tanks/main.js create mode 100644 src/tanks/ui/main.js diff --git a/css/lttl.css b/css/lttl.css index 051d12c..23b3e57 100644 --- a/css/lttl.css +++ b/css/lttl.css @@ -21,7 +21,7 @@ table.grid, table.grid td { /* outline:1px solid rgba(255,255,255,0.1); */ color:transparent; font: 18pt monospace; margin:0; padding:0; white-space:nowrap; overflow:hidden; } -.gridShowCoords table.grid td:hover { color:rgba(255,255,255,0.1); } +.showGridCoords table.grid td:hover { color:rgba(255,255,255,0.1); } .bigblue { position:fixed; width:100%; top:50%; margin-top:-200px; z-index:101; } .bigblue .box { width:400px; margin:0 auto; padding:1em; color:#000; background-color:#2992C5; diff --git a/src/Y/y-array.js b/src/Y/y-array.js index 7871abe..8b72596 100644 --- a/src/Y/y-array.js +++ b/src/Y/y-array.js @@ -3,7 +3,8 @@ YArray = Y.YArray = YCollection.subclass('YArray', function(YArray){ this.init = function(o){ - YCollection.init.call(this, o || []); + // YCollection.init.call(this, o || []); + this._o = o || []; }; this.merge = @@ -11,16 +12,20 @@ YCollection.subclass('YArray', function(YArray){ function concat( donor ){ var A = this._o; new Y(arguments).forEach(function( donor ){ - A = A.concat(donor); + A = A.concat(donor instanceof Y.YArray ? donor.end() : donor); }); return Y(A); }; this.remove = function remove(v){ - var idx = this.indexOf(v); - if ( idx != -1 ) - this.splice(idx, 1); + if (arguments.length > 1) + Y(arguments).forEach(this.remove, this); + else { + var idx = this.indexOf(v); + if ( idx != -1 ) + this.splice(idx, 1); + } return this; }; diff --git a/src/Y/y-class.js b/src/Y/y-class.js index 2922230..5c7aea0 100644 --- a/src/Y/y-class.js +++ b/src/Y/y-class.js @@ -176,10 +176,22 @@ YBase = Y.YBase = new Class("YBase", { /// Other Class Utilities /// +function bindName(v, k){ + if ( isFunction(v) ) + this[k] = Y(this[k]).bind(this); +} +// bindName = Y(bindName).curry(); + Y.bindAll = bindAll; -function bindAll(o){ - for (var k in o) - if (isFunction(o[k])) o[k] = o[k].bind(o); +function bindAll(o, names){ + var names = new Y(arguments, 1); + Y(names.size() ? names.generate(Y.op.get(o)) : o).forEach( bindName, o ); + + // if ( names.size() ){ + // names.forEach(binder); + // } else + // for (var k in o) binder(k); + return o; } diff --git a/src/Y/y-collection.js b/src/Y/y-collection.js index 9e7ee7b..f027877 100644 --- a/src/Y/y-collection.js +++ b/src/Y/y-collection.js @@ -17,7 +17,7 @@ YBase.subclass('YCollection', { this._o = o; }, - 'attr' : function(k, v, def){ + 'attr' : function attr(k, v, def){ var r = Y.attr(this._o, k, v, def); if (r === this._o) return this; @@ -28,9 +28,9 @@ YBase.subclass('YCollection', { 'merge' : extendY, 'concat' : extendY, - 'end' : function(){ return this._o; }, + 'end' : function end(){ return this._o; }, - 'reduce' : function( fn, acc, context ){ + 'reduce' : function reduce( fn, acc, context ){ var o = this._o || this, acc = acc || new o.constructor(); for ( var name in o ) @@ -38,20 +38,20 @@ YBase.subclass('YCollection', { return acc; }, - 'map' : function( fn, context ){ + 'map' : function map( fn, context ){ var o = this._o, acc = new o.constructor(); for ( var name in o ) acc[name] = fn.call( context || this, o[name], name, o ); return acc; }, - 'forEach' : function( fn, context ){ + 'forEach' : function forEach( fn, context ){ var o = this._o; for ( var name in o ) fn.call( context || this, o[name], name, o ); }, - 'filter' : function( fn, context ){ + 'filter' : function filter( fn, context ){ var o = this._o, acc = new o.constructor(); for ( var name in o ) if ( fn.call( context || this, o[name], name, o ) ) @@ -59,7 +59,7 @@ YBase.subclass('YCollection', { return acc; }, - 'indexOf' : function( value ){ + 'indexOf' : function indexOf( value ){ var o = this._o; for ( var name in o ) if ( o[name] === value ) @@ -67,57 +67,65 @@ YBase.subclass('YCollection', { return -1; }, - 'has' : function( value ){ + 'has' : function has( value ){ return ( this.indexOf(value) !== -1 ); }, - 'clone' : function(){ + 'clone' : function clone(){ return Y({}).extend(this); }, - 'remove' : function(v){ - var o = this._o; + 'remove' : function remove(v){ + var o = this._o + , toRemove = new Y(arguments); + for (var k in o) { - if (o[k] !== v) continue; - delete o[k]; - break; + var v = o[k]; + if ( toRemove.has(v) ) { + delete o[k]; + toRemove.remove(v); + } } return this; }, - 'every' : function( fn ){ + 'every' : function every( fn ){ var self = this, fn = fn || bool; return this.reduce(function(acc, v, k, o){ return acc && fn.call(self, v, k, o); }, true); }, - 'any' : function( fn ){ + 'any' : function any( fn ){ var self = this, fn = fn || bool; return this.reduce(function(acc, v, k, o){ return acc || fn.call(self, v, k, o); }, false); }, - 'zip' : function(){ + 'zip' : function zip(){ var sequences = new Y(arguments).map( Y.limit(1) ).unshift(this); return this.map(function(_, k){ return sequences.invoke('attr', k); }).invoke('end'); }, - 'pluck' : function(key){ + 'pluck' : function pluck(key){ return this.map(function(v){ return v && (isFunction(v.attr) ? v.attr(key) : v[key]); }); }, - 'invoke' : function(name){ + 'invoke' : function invoke(name){ var args = Y(arguments), name = args.shift(); return this.map(function(o){ return o && o[name].apply(o, args); }); + }, + + 'apply' : function apply(name, args){ + return this[name].apply(this, args); } }); diff --git a/src/Y/y-function.js b/src/Y/y-function.js index fe1b668..6f1e873 100644 --- a/src/Y/y-function.js +++ b/src/Y/y-function.js @@ -166,24 +166,24 @@ function splat(fn, x){ var _bind = _Function.prototype.bind; YFunction.prototype.bind = -function bind(context, args){ - var bound = _bind.apply(this, arguments); - bound[WRAPS] = this; - return Y(bound); -}; + function bind(context, args){ + var bound = _bind.apply(this, arguments); + bound[WRAPS] = this; + return Y(bound); + }; // Remembers arguments but obeys current context YFunction.prototype.partial = -function partial(){ - var fn = this - , args = Y(arguments) - , partially = function(){ - return fn.apply( this, args.concat(Y(arguments)) ); - }; - partially[WRAPS] = fn; - return Y(partially); -}; + function partial(){ + var fn = this + , args = Y(arguments) + , partially = function(){ + return fn.apply( this, args.concat(Y(arguments)) ); + }; + partially[WRAPS] = fn; + return Y(partially); + }; // Only works for arguments whose toString is unique and stateless (for example, primitives, but not closures). diff --git a/src/portal/math/vec.js b/src/portal/math/vec.js index 7d5a2e0..d0035a0 100644 --- a/src/portal/math/vec.js +++ b/src/portal/math/vec.js @@ -120,9 +120,9 @@ Y.extend(math.Vec, { // }, manhattan: function manhattan(x1,y1, x2,y2) { - if (x1 instanceof math.Vec && y1 instanceof math.Vec) { - y2 = y1.y; x2 = y1.x; - y1 = x1.y; x1 = x1.x; + if (x1 instanceof Array && y1 instanceof Array) { + y2 = y1[1]; x2 = y1[0]; + y1 = x1[1]; x1 = x1[0]; } var d1 = Math.abs(x2 - x1) , d2 = Math.abs(y2 - y1) ; diff --git a/src/tanks/config.js b/src/tanks/config.js index 39b1897..e8987c9 100644 --- a/src/tanks/config.js +++ b/src/tanks/config.js @@ -1,7 +1,8 @@ // -*- mode: JavaScript; tab-width: 4; indent-tabs-mode: nil; -*- tanks.config = { - debug : { - gridShowCoords : false + ui : { + showGridCoords : false, + showCountdown : false }, pathing : { overlayPathmap : false, diff --git a/src/tanks/game.js b/src/tanks/game.js new file mode 100644 index 0000000..b3cf024 --- /dev/null +++ b/src/tanks/game.js @@ -0,0 +1,183 @@ +tanks.Game = new Y.Class('Game', { + overlayPathmap : false, + + + init : function init(viewport){ + Y.bindAll(this); + tanks.resetGlobals(); + + this.byId = {}; + this.active = new Y.YArray(); + this.units = new Y.YArray(); + this.bullets = new Y.YArray(); + this.viewport = $(viewport || GRID_ELEMENT); + + this.loop = new EventLoop(this, FRAME_RATE); + + this.root = + this.grid = + new Grid(COLUMNS,ROWS, REF_SIZE) + .appendTo(this.viewport); + + this.level = + new Level(this, COLUMNS*REF_SIZE, ROWS*REF_SIZE) + .appendTo(this.root); + + this.pathmap = this.level.pathmap; + + + Thing.addEventListener('create', this.addUnit); + Thing.addEventListener('destroy', this.killUnit); + + this.addEventListener('tick', this.tick); + + this.fire('ready', this); + }, + + destroy : function destroy(){ + this.stop(); + tanks.resetGlobals(); + }, + + draw : function draw(){ + this.root.draw(); + + this.pathmap.removeOverlay(this.viewport); + if (tanks.config.pathing.overlayPathmap) + this.pathmap.overlay(this.viewport); + }, + + /** + * Main Event Loop. + */ + tick : function tick(evt){ + var d = evt.data; + + NOW = d.now; + ELAPSED = d.elapsed; + TICKS = d.ticks; + SECONDTH = ELAPSED / 1000; + SQUARETH = REF_SIZE * SECONDTH + + if (!tanks.config.pathing.overlayAIPaths) + this.pathmap.hidePaths(); + + this.active.invoke('updateCooldowns', NOW); + this.active.invoke('act'); + + this.draw(); + + if (this.player.dead) { + this.fire('lose', this.player); + this.stop(); + + } else if ( !this.units.filter('!_.dead'.lambda()).remove(this.player).size() ) { + this.fire('win', this.player); + this.stop(); + } + + }, + + + + + // *** Agent Management *** // + + addUnit : function addUnit(unit, col,row){ + if (unit instanceof Y.event.YEvent) + unit = unit.instance; + + unit.game = this; + + if (col !== undefined) { + // Center unit in square + var sqX = (col || 0) * REF_SIZE + , sqY = (row || 0) * REF_SIZE + , x = sqX + (REF_SIZE-unit.width) /2 + , y = sqY + (REF_SIZE-unit.height)/2 ; + + unit.setLocation(x,y); + unit.render( this.level ); + } + + this.pathmap.addBlocker(unit); + + if ( unit.active && !this.byId[unit.id] ) { + this.byId[unit.id] = unit; + this.active.push(unit); + + if (unit instanceof Bullet) + this.bullets.push(unit); + else + this.units.push(unit); + } + + return unit; + }, + + killUnit : function killUnit(unit){ + if (unit instanceof Y.event.YEvent) + unit = unit.instance; + + // console.log('killUnit(', unit, ')'); + + delete this.byId[unit.id]; + this.active.remove(unit); + this.pathmap.removeBlocker(unit); + + if (unit instanceof Bullet) + this.bullets.remove(unit); + else + this.units.remove(unit); + + return unit; + }, + + moveUnitTo : function moveUnitTo(agent, x,y){ + this.pathmap.removeBlocker(agent); + agent.setLocation(x,y); + this.pathmap.addBlocker(agent); + return agent; + }, + + + + /** + * @obsolete + */ + resize : function resize(){ + var ratio = COLUMNS / ROWS + , el = this.el + , p = el.parent() + , pw = p.width(), ph = p.height() + , pRatio = pw / ph + ; + + if ( ratio > pRatio ) + CELL_SIZE = Math.floor((pw-GRID_OFFSET*2) / COLUMNS); + else + CELL_SIZE = Math.floor((ph-GRID_OFFSET*2) / ROWS); + + SCALE = CELL_SIZE/REF_SIZE; + + var w = COLUMNS*CELL_SIZE + , h = ROWS*CELL_SIZE + , canvas = this.canvas[0]; + + this.el.width(w).height(h); + this.canvas.width(w).height(h); + canvas.width = w; + canvas.height = h; + + this.el.offset({ + top : (ph - h) / 2, + left : (pw - w) / 2 + }); + + this.ctx.scale(SCALE,SCALE); + } + + +}); + + diff --git a/src/tanks/game/game.js b/src/tanks/game/game.js deleted file mode 100644 index a8a9950..0000000 --- a/src/tanks/game/game.js +++ /dev/null @@ -1,176 +0,0 @@ -tanks.Game = new Y.Class('Game', { - - init : function init(viewport){ - Y.bindAll(this); - - tanks.resetGlobals(); - - this.byId = {}; - this.active = new Y.YArray(); - this.units = new Y.YArray(); - this.bullets = new Y.YArray(); - this.viewport = $(viewport || GRID_ELEMENT); - - this.loop = new EventLoop(this, FRAME_RATE); - - this.root = - this.grid = - new Grid(COLUMNS,ROWS, REF_SIZE) - .appendTo(this.viewport); - - this.level = - new Level(this, COLUMNS*REF_SIZE, ROWS*REF_SIZE) - .appendTo(this.root); - - this.pathmap = this.level.pathmap; - - - Thing.addEventListener('create', this.addUnit); - Thing.addEventListener('destroy', this.killUnit); - - this.addEventListener('tick', this.tick); - - this.fire('ready', this); - }, - - - - draw : function draw(){ - this.root.draw(); - - this.pathmap.removeOverlay(this.viewport); - if (tanks.config.pathing.overlayPathmap) - this.pathmap.overlay(this.viewport); - }, - - /** - * Main Event Loop. - */ - tick : function tick(evt){ - var d = evt.data; - - NOW = d.now; - ELAPSED = d.elapsed; - TICKS = d.ticks; - SECONDTH = ELAPSED / 1000; - SQUARETH = REF_SIZE * SECONDTH - - if (!tanks.config.pathing.overlayAIPaths) - this.pathmap.hidePaths(); - - this.active.invoke('updateCooldowns', NOW); - this.active.invoke('act'); - - this.draw(); - - if (this.player.dead) { - this.fire('lose', this.player); - this.stop(); - - } else if ( !this.units.filter('!_.dead'.lambda()).remove(this.player).size() ) { - this.fire('win', this.player); - this.stop(); - } - - }, - - - - - // *** Agent Management *** // - - addUnit : function addUnit(unit, col,row){ - if (unit instanceof Y.event.YEvent) - unit = unit.instance; - - unit.game = this; - - if (col !== undefined) { - // Center unit in square - var sqX = (col || 0) * REF_SIZE - , sqY = (row || 0) * REF_SIZE - , x = sqX + (REF_SIZE-unit.width) /2 - , y = sqY + (REF_SIZE-unit.height)/2 ; - - unit.setLocation(x,y); - unit.render( this.level ); - } - - this.pathmap.addBlocker(unit); - - if ( unit.active && !this.byId[unit.id] ) { - this.byId[unit.id] = unit; - this.active.push(unit); - - if (unit instanceof Bullet) - this.bullets.push(unit); - else - this.units.push(unit); - } - - return unit; - }, - - killUnit : function killUnit(unit){ - if (unit instanceof Y.event.YEvent) - unit = unit.instance; - - // console.log('killUnit(', unit, ')'); - - delete this.byId[unit.id]; - this.active.remove(unit); - this.pathmap.removeBlocker(unit); - - if (unit instanceof Bullet) - this.bullets.remove(unit); - else - this.units.remove(unit); - - return unit; - }, - - moveAgentTo : function moveAgentTo(agent, x,y){ - this.pathmap.removeBlocker(agent); - agent.setLocation(x,y); - this.pathmap.addBlocker(agent); - return agent; - }, - - - - resize : function resize(){ - var ratio = COLUMNS / ROWS - , el = this.el - , p = el.parent() - , pw = p.width(), ph = p.height() - , pRatio = pw / ph - ; - - if ( ratio > pRatio ) - CELL_SIZE = Math.floor((pw-GRID_OFFSET*2) / COLUMNS); - else - CELL_SIZE = Math.floor((ph-GRID_OFFSET*2) / ROWS); - - SCALE = CELL_SIZE/REF_SIZE; - - var w = COLUMNS*CELL_SIZE - , h = ROWS*CELL_SIZE - , canvas = this.canvas[0]; - - this.el.width(w).height(h); - this.canvas.width(w).height(h); - canvas.width = w; - canvas.height = h; - - this.el.offset({ - top : (ph - h) / 2, - left : (pw - w) / 2 - }); - - this.ctx.scale(SCALE,SCALE); - } - - -}); - - diff --git a/src/tanks/main.js b/src/tanks/main.js deleted file mode 100644 index 6944542..0000000 --- a/src/tanks/main.js +++ /dev/null @@ -1,268 +0,0 @@ -(function(){ -jQuery(main); - -this.main = main; - -// Main method is only executed once, so we'll setup things -// that don't change between games. -function main(){ - qkv = Y(window.location.search.slice(1)).fromKV().end(); - - /// Debug /// - if (qkv.ai) { - $('#welcome').hide(); - $('#ai').toggle(); - $('#ai textarea')[0].focus(); - } - if (qkv.debug) $('#debug').toggle(); - - $('#ai .ready').bind('click', function(evt){ - try { - var script = $('#custom-tank').val(); - Tank.prototype.act = eval('(function(){ with(this){'+script+'} })'); - $('#ai').hide(); - startGame(); - } catch(e) { - alert('AI Error! '+e); - } - }); - - // Show debug - $(document).bind('keydown', 'ctrl+c', function(evt){ $('#debug').toggle(); }); - - // Don't fire on clicks in the debug menu - $('#debug').bind('mousedown', Y.op.K(false)); - - // Update config when changed - $('#debug input').bind('change', updateConfig); - - - // Create #pause box - $('#welcome').clone() - .attr('id', 'pause') - .hide() - .appendTo( $('body') ) - .find('.box').html('

Paused!

'); - - // Create Victory/Defeat box - $('#welcome').clone() - .attr('id', 'gameover') - .hide() - .appendTo( $('body') ) - .find('.box') - .html('

You Win!

Play Again
'); - - - // Bind to all future restart buttons - $('#gameover .restart') - .live('click', function(evt){ - setupGame(); - setupUI(); - - startGame(); - }); - - setupGame(); - setupUI(); -} - -function gameExists(){ return !!tanks.currentGame; } - -function setupGame(){ - if ( gameExists() ) teardownGame(); - - LBT = tanks.currentGame = new tanks.Game(); - - LBT.addEventListener('win', gameover('You Win!', 'Play Again', '')); - LBT.addEventListener('lose', gameover('You Lose :(', 'Try Again', '')); - - ctx = LBT.level.ctx; -} - -function teardownGame(){ - LBT.stop(); - $('#viewport').empty(); -} - - -// Set up UI listeners -function setupUI(){ - if ( gameExists() ) teardownUI(); - - if (!qkv.ai) $('#welcome').show(); - LBT.loop.spark = new FpsSparkline(LBT.loop, '.fps-sparkline', 0,0); - initConfig(); - LBT.root.draw(); - - // Start button (click or return key) - if (!qkv.ai) $(document).bind('click', startGame); - $(document).bind('keydown', 'return', startGame); - - - LBT.root.draw(); - setInterval(updateInfo, 1000); - - updateInfo(); - - // Fix grid-size on resize - // $(window).bind('resize', resizeGame); -} - -function teardownUI(){ - $(document).unbind('click', startGame); - $(document).unbind('keydown', startGame); - $(document).unbind('keydown', pauseGame); - - $('#welcome, #pause, #gameover').hide(); -} - - -function startGame(evt){ - $(document).unbind('click', startGame); - $(document).unbind('keydown', startGame); - - $('#welcome').hide(); - countdownToStart(3, function(){ - $('#overlay').hide(); - $(document).bind('keydown', 'return', pauseGame); - toggleGame(); - }); -} - -function pauseGame(evt){ - $('#overlay').toggle(); - $('#pause').toggle(); - toggleGame(); -} - -function toggleGame(evt){ - if (LBT.loop.running) - LBT.stop(); - else - LBT.start(); - - updateInfo(); -} - -function resizeGame(evt){ - LBT.resize(evt); - - if (!LBT.loop.running) { - LBT.start(); - LBT.stop(); - } -} - -function gameover(headline, button, body, evt){ - $('#overlay').toggle(); - $('#gameover') - .find('h1').text(headline).end() - .find('.pinkbutton').text(button).end() - .find('p').text(body).end() - .show(); -} -gameover = Y(gameover).curry(); - - -function initConfig(){ - var c = tanks.config - , p = c.pathing - ; - - $('#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.debug.gridShowCoords); - $('#viewport .layer.grid')[(c.debug.gridShowCoords ? 'add' : 'remove')+'Class']('gridShowCoords'); -} - -function updateConfig(evt){ - var c = tanks.config - , p = c.pathing - ; - - p.overlayPathmap = $('#config [name=pathmap]').attr('checked'); - p.overlayAIPaths = $('#config [name=aipaths]').attr('checked'); - p.traceTrajectories = $('#config [name=trajectories]').attr('checked'); - - c.debug.gridShowCoords = $('#config [name=gridCoords]').attr('checked'); - $('#viewport .layer.grid')[(c.debug.gridShowCoords ? 'add' : 'remove')+'Class']('gridShowCoords'); -} - -// Update performance info periodically -function updateInfo(){ - var loop = LBT.loop - , fps = loop.fps() - , n_active = LBT.active.size() - , n_units = LBT.units.size() - , n_projs = LBT.bullets.size() - ; - - $('#info #state').text( loop.running ? 'Running!' : ('Paused (tick '+TICKS+')') ); - $('#info [name=fps]').val( fps.toFixed(2) + " / " + loop.framerate ); - $('#info [name=frame]').val( loop.frametime().toFixed(3)+" ms" ); - - $('#info [name=active]').val( n_active ); - $('#info [name=units]').val( n_units ); - $('#info [name=bullets]').val( n_projs ); - - loop.spark.drawTimes(); - - return false; -} - -function countdownToStart(n, fn){ - var el - , showFor = 750 - , pauseFor = 500 - - , body = $('body') - , sizeRatio = 0.6 - , values = new Y(1,n+1).unshift('GO').end() - , colors = [ '#E73075', '#2992C5', '#2992C5', '#2992C5' ] - ; - - function tickDown(){ - var bw = body.width() - , bh = body.height() - , size = bh*sizeRatio - - , el = $('
') - .addClass('countdown') - .width(bh*0.75).height(bh*0.75) - .text( values.pop() ) - .css({ - 'top' : ((bh - size)/2)+'px', - 'left' : (bw/2 - bh*0.375)+'px', - 'font-size' : size+'px', - 'background-color' : colors.pop(), - }) - ; - - ' -moz- -webkit-' - .split(' ') - .forEach(function(prefix){ - // el.css(prefix+'border-radius', (size*0.25)+'px'); - el.css(prefix+'border-radius', (bh/2)+'px'); - }); - - body.append(el); - - setTimeout(function(){ - el.remove(); - - if ( values.length ) { - setTimeout(tickDown, pauseFor); - } else { - fn(); - } - }, showFor); - } - - tickDown(); -} - - -})(); - diff --git a/src/tanks/map/level.js b/src/tanks/map/level.js index 8909eff..17308f9 100644 --- a/src/tanks/map/level.js +++ b/src/tanks/map/level.js @@ -6,7 +6,7 @@ Level = Rect.subclass('Level', { this.canvas.remove(); this.game = game; - this.walls = []; + this.walls = Y([]); this.pathmap = new PathMap(this, 0,0, w, h, CAPACITY); game.addEventListener('ready', this.setup.bind(this)); @@ -34,10 +34,11 @@ Level = Rect.subclass('Level', { addWall : function addWall(x,y, w,h, isBoundary){ var wall = new Level.Wall(x,y, w,h, isBoundary); - this.walls.push(wall); this.pathmap.addBlocker(wall); - this.append( wall.render(this).shape ); + + if (!isBoundary) + this.walls.push(wall); return wall; }, diff --git a/src/tanks/map/loc.js b/src/tanks/map/loc.js index 6024481..3f65d91 100644 --- a/src/tanks/map/loc.js +++ b/src/tanks/map/loc.js @@ -104,14 +104,14 @@ Y(Loc).extend({ Loc.Rect = new Y.Class('Rect', [], { init : function initRect(x1,y1, x2,y2){ - if (x1 instanceof Loc && y1 instanceof Loc) { - y2 = y1.y; x2 = y1.x; - y1 = x1.y; x1 = x1.x; + if (x1 instanceof Array && y1 instanceof Array) { + y2 = y1[1]; x2 = y1[0]; + y1 = x1[1]; x1 = x1[0]; } // init : function initRect(_x1,_y1, _x2,_y2){ - // if (_x1 instanceof Loc && _y1 instanceof Loc) { - // _y2 = _y1.y; _x2 = _y1.x; - // _y1 = _x1.y; _x1 = _x1.x; + // if (_x1 instanceof Array && _y1 instanceof Array) { + // _y2 = _y1[1]; _x2 = _y1[0]; + // _y1 = _x1[1]; _x1 = _x1[0]; // } // var x1 = Math.min(_x1,_x2), x2 = Math.max(_x1,_x2) // , y1 = Math.min(_y1,_y2), y2 = Math.max(_y1,_y2); @@ -159,7 +159,7 @@ Loc.Rect = new Y.Class('Rect', [], { } }); -Loc.BoundingBox = new Y.Class('BoundingBox', Loc.Rect, { +Loc.BoundingBox = Loc.Rect.subclass('BoundingBox', { init : function initBoundingBox(x1,y1, x2,y2){ Loc.Rect.init.call(this, x1,y1, x2,y2); diff --git a/src/tanks/map/pathmap.js b/src/tanks/map/pathmap.js index 665f2df..5ab4c17 100644 --- a/src/tanks/map/pathmap.js +++ b/src/tanks/map/pathmap.js @@ -31,12 +31,15 @@ PathMap = QuadTree.subclass('PathMap', { this.boundaryWalls = Y.map(BWs, 'this.level.addWall.apply(this.level, _)', this); }, - wallObstructs : function wallObstructs(line, w,h){ + wallObstructs : function wallObstructs(line){ + // var walls = this.walls; + // for (var i=0, L=walls.length, w=walls[i]; i= bb.y1 && line.calcY(bb.x2) <= bb.y2 ) - || ( line.calcX(bb.y1) >= bb.x1 && line.calcX(bb.y2) <= bb.x2 ) ; + return wall.boundingBox.intersects(line); }, this); }, diff --git a/src/tanks/map/trajectory.js b/src/tanks/map/trajectory.js index 0521f90..038864c 100644 --- a/src/tanks/map/trajectory.js +++ b/src/tanks/map/trajectory.js @@ -5,6 +5,8 @@ Trajectory = math.Line.subclass('Trajectory', { init : function initTrajectory(owner, x1,y1, x2,y2, tdist){ + Y.bindAll(this, 'cmp', 'closer'); + this.owner = owner; this.game = owner.game; this.pathmap = this.game.pathmap; @@ -13,6 +15,12 @@ Trajectory = math.Line.subclass('Trajectory', { }, reset : function reset(x1,y1, x2,y2, tdist){ + if (x1 instanceof Array && y1 instanceof Array) { + tdist = x2; + y2 = y1[1]; x2 = y1[0]; + y1 = x1[1]; x1 = x1[0]; + } + // init with raw numbers to do calculations math.Line.init.call(this, x1,y1, x2,y2, tdist || this.tdist); @@ -44,6 +52,87 @@ Trajectory = math.Line.subclass('Trajectory', { return this; }, + /** + * Compares how distant in the future two objects are on this trajectory. + * Objects that have been passed are always further away than those in the future, + * but otherwise the comparison is performed by absolute distance. + * @returns -1 if a closer b, 1 if a further b, 0 if a same as b + */ + cmp : function cmp(a, b){ + if (a instanceof Thing) a = a.midpoint; + if (b instanceof Thing) b = b.midpoint; + + var abs = Math.abs + // , cur = this.owner.midpoint + // , t = this.iparametric(cur.x, cur.y) + , t = this.elapsed + + , xa = this.calcX(a.y), ya = this.calcY(a.x) + , ta = this.iparametric(xa,ya) + , da = (ta.x + ta.y)/2 - t + // , da = (ta.x - t.x + ta.y - t.y)/2 + // , dxa = ta.x - t.x, dya = ta.y - t.y + + , xb = this.calcX(b.y), yb = this.calcY(b.x) + , tb = this.iparametric(xb,yb) + , db = (tb.x + tb.y)/2 - t + // , db = (tb.x - t.x + tb.y - t.y)/2 + // , dxb = tb.x - t.x, dyb = tb.y - t.y + ; + + // If one has passed, return the other + if ( da < 0 && db >= 0 ) + return 1; + if ( db < 0 && da >= 0 ) + return -1; + + return Y.op.cmp(abs(da), abs(db)); + }, + + closer : function closer(o1, o2){ + return this.cmp(o1,o2) === 1 ? o2 : o1; + }, + + closest : function closest(o1, o2){ + return new Y(arguments).sort( this.cmp ).shift(); + }, + + comesWithin : function comesWithin(pt, w,h){ + if ( !this.owner.midpoint ) + return false; + + if (pt instanceof Thing) pt = pt.midpoint; + + if ( w === undefined ){ + w = 0; h = 0; + } else if ( Y.isNumber(w.width) ){ + h = w.height; w = w.width; + } + + var cur = this.owner.midpoint + , fx = this.calcX(pt.y), fy = this.calcY(pt.x) + , t = this.iparametric(cur.x, cur.y) + , ft = this.iparametric(fx,fy) + , dw = Math.abs(fx - pt.x), dh = Math.abs(fy - pt.y) + ; + return ( t.x <= ft.x && t.y <= ft.y + && ( dw <= w || dh <= h ) ); + }, + + pathBlocked : function pathBlocked(obj, ignore){ + var blockers = + this.pathmap.walls + .concat( this.game.units ) + .apply('remove', Y(ignore || []).concat([this.owner]).end()) + .sort( this.cmp ) + + , blocker = blockers.shift() + ; + + return (blocker === obj ? false : blocker); + }, + + step : function step(dt){ this.halt = false; @@ -118,25 +207,6 @@ Trajectory = math.Line.subclass('Trajectory', { return to; }, - willComeWithin : function willComeWithin(pt, w,h){ - if ( !this.owner.midpoint ) - return false; - - if ( !Y.isNumber(w) ){ - h = w.height; w = w.width; - } - - var cur = this.owner.midpoint - , fx = this.calcX(pt.y), fy = this.calcY(pt.x) - , t = this.iparametric(cur.x, cur.y) - , ft = this.iparametric(fx,fy) - , dw = Math.abs(fx - pt.x), dh = Math.abs(fy - pt.y) - ; - return ( t.x <= ft.x && t.y <= ft.y - && ( dw <= w || dh <= h ) - && !this.pathmap.wallObstructs(this, w,h) ); - }, - toString : function toString(){ return 'T['+this.p1+', '+this.p2+', slope='+this.slope.toFixed(3)+']'; } diff --git a/src/tanks/thing/bullet.js b/src/tanks/thing/bullet.js index 1d3dc9b..331aa9d 100644 --- a/src/tanks/thing/bullet.js +++ b/src/tanks/thing/bullet.js @@ -69,7 +69,7 @@ Bullet = Thing.subclass('Bullet', { var to = this.trajectory.step( ELAPSED ); if (!this.dead) - this.game.moveAgentTo(this, to.x, to.y); + this.game.moveUnitTo(this, to.x, to.y); return this; }, diff --git a/src/tanks/thing/player.js b/src/tanks/thing/player.js index efb9caf..cd7827c 100644 --- a/src/tanks/thing/player.js +++ b/src/tanks/thing/player.js @@ -87,7 +87,7 @@ PlayerTank = Tank.subclass('PlayerTank', { ; if ( !blockers.size() ) - this.game.moveAgentTo(this, x,y); + this.game.moveUnitTo(this, x,y); }, diff --git a/src/tanks/thing/tank.js b/src/tanks/thing/tank.js index 1429cd2..1e8bcec 100644 --- a/src/tanks/thing/tank.js +++ b/src/tanks/thing/tank.js @@ -125,18 +125,21 @@ Tank = Thing.subclass('Tank', { willCollide : function willCollide(bullets, wiggle){ wiggle = wiggle || 0; - var bb = this.boundingBox, mid = this.midpoint + var tank = this, bb = this.boundingBox , w = (bb.width+wiggle)/2, h = (bb.height+wiggle)/2 ; return bullets.filter(function(b){ - return !b.dead && b.willComeWithin(mid, w,h); + var trj = b.trajectory; + return ( !b.dead + && trj.comesWithin(tank, w,h) + && !trj.pathBlocked(tank) ); }); }, continueMove : function continueMove(){ if ( !this.currentMove || this.currentMoveLimit <= NOW ){ var t = this.findNearEnemies(10000).shift(); - this.calculatePath(t.midpoint); + if (t) this.calculatePath(t.midpoint); } var to = this.currentMove; @@ -213,10 +216,9 @@ Tank = Thing.subclass('Tank', { , pm = this.game.pathmap ; return this.findNearLike(ticks, function(agent){ var am = agent.midpoint; - return agent.align !== this.align - && Y.is(Tank, agent) - && !(wallObs - && pm.wallObstructs(new Trajectory(this, mid.x,mid.y, am.x,am.y), 6,6)); + return ( agent.align !== this.align + && Y.is(Tank, agent) + && !(wallObs && new Trajectory(this,mid,am).pathBlocked(agent)) ); }); }, @@ -307,7 +309,7 @@ Tank = Thing.subclass('Tank', { ; if ( !blockers.size() ) - this.game.moveAgentTo(this, to.x, to.y); + this.game.moveUnitTo(this, to.x, to.y); return this; }, diff --git a/src/tanks/thing/thing.js b/src/tanks/thing/thing.js index 4fcd2ab..82fcfbc 100644 --- a/src/tanks/thing/thing.js +++ b/src/tanks/thing/thing.js @@ -123,22 +123,10 @@ Thing = new Evt.Class('Thing', { return Math.atan2(y0,x0); }, - willComeWithin : function willComeWithin(pt, w,h){ + comesWithin : function comesWithin(pt, w,h){ if ( !(this.trajectory && this.midpoint) ) return false; - - if ( !Y.isNumber(w) ){ - h = w.height; w = w.width; - } - var trj = this.trajectory - , cur = this.midpoint - , fx = trj.calcX(pt.y), fy = trj.calcY(pt.x) - , t = trj.iparametric(cur.x, cur.y) - , ft = trj.iparametric(fx,fy) - , dw = Math.abs(fx - pt.x), dh = Math.abs(fy - pt.y) - ; - return ( t.x <= ft.x && t.y <= ft.y - && ( dw <= w || dh <= h ) ); + return this.trajectory.comesWithin(pt, w,h); }, diff --git a/src/tanks/ui/main.js b/src/tanks/ui/main.js new file mode 100644 index 0000000..d966db4 --- /dev/null +++ b/src/tanks/ui/main.js @@ -0,0 +1,279 @@ +tanks.ui = {}; + +(function(){ + +jQuery(main); + +this.main = main; + +// Main method is only executed once, so we'll setup things +// that don't change between games. +function main(){ + qkv = Y(window.location.search.slice(1)).fromKV().end(); + + /// Debug /// + if (qkv.ai) { + $('#welcome').hide(); + $('#ai').toggle(); + $('#ai textarea')[0].focus(); + } + if (qkv.debug) $('#debug').toggle(); + + $('#ai .ready').bind('click', function(evt){ + try { + var script = $('#custom-tank').val(); + Tank.prototype.act = eval('(function(){ with(this){'+script+'} })'); + $('#ai').hide(); + startGame(); + } catch(e) { + alert('AI Error! '+e); + } + }); + + // Show debug + $(document).bind('keydown', 'ctrl+c', function(evt){ $('#debug').toggle(); }); + + // Don't fire on clicks in the debug menu + $('#debug').bind('mousedown', Y.op.K(false)); + + // Update config when changed + $('#debug input').bind('change', updateConfig); + + + // Create #pause box + $('#welcome').clone() + .attr('id', 'pause') + .hide() + .appendTo( $('body') ) + .find('.box').html('

Paused!

'); + + // Create Victory/Defeat box + $('#welcome').clone() + .attr('id', 'gameover') + .hide() + .appendTo( $('body') ) + .find('.box') + .html('

You Win!

Play Again
'); + + + // Bind to all future restart buttons + $('#gameover .restart') + .live('click', function(evt){ + setupGame(); + setupUI(); + + startGame(); + }); + + setupGame(); + setupUI(); +} + +function gameExists(){ return !!tanks.currentGame; } + +function setupGame(){ + if ( gameExists() ) teardownGame(); + + LBT = tanks.currentGame = new tanks.Game(); + + LBT.addEventListener('win', gameover('You Win!', 'Play Again', '')); + LBT.addEventListener('lose', gameover('You Lose :(', 'Try Again', '')); + + ctx = LBT.level.ctx; +} + +function teardownGame(){ + LBT.destroy(); + $('#viewport').empty(); +} + + +// Set up UI listeners +function setupUI(){ + if ( gameExists() ) teardownUI(); + + if (!qkv.ai) $('#welcome').show(); + LBT.loop.spark = new FpsSparkline(LBT.loop, '.fps-sparkline', 0,0); + initConfig(); + LBT.root.draw(); + + // Start button (click or return key) + if (!qkv.ai) $(document).bind('click', startGame); + $(document).bind('keydown', 'return', startGame); + + + LBT.root.draw(); + setInterval(updateInfo, 1000); + + updateInfo(); + + // Fix grid-size on resize + // $(window).bind('resize', resizeGame); +} + +function teardownUI(){ + $(document).unbind('click', startGame); + $(document).unbind('keydown', startGame); + $(document).unbind('keydown', pauseGame); + + $('#welcome, #pause, #gameover').hide(); +} + + +function startGame(evt){ + $(document).unbind('click', startGame); + $(document).unbind('keydown', startGame); + + $('#welcome').hide(); + + if ( tanks.config.ui.showCountdown ) + countdownToStart(3, readyToStart); + else + readyToStart(); +} + +function pauseGame(evt){ + $('#overlay').toggle(); + $('#pause').toggle(); + toggleGame(); +} + +function toggleGame(evt){ + if (LBT.loop.running) + LBT.stop(); + else + LBT.start(); + + updateInfo(); +} + +function resizeGame(evt){ + LBT.resize(evt); + + if (!LBT.loop.running) { + LBT.start(); + LBT.stop(); + } +} + +function gameover(headline, button, body, evt){ + $('#overlay').toggle(); + $('#gameover') + .find('h1').text(headline).end() + .find('.pinkbutton').text(button).end() + .find('p').text(body).end() + .show(); +} +gameover = Y(gameover).curry(); + + +function initConfig(){ + var c = tanks.config + , p = c.pathing + ; + + $('#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'); +} + +function updateConfig(evt){ + var c = tanks.config + , p = c.pathing + ; + + 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'); +} + +// Update performance info periodically +function updateInfo(){ + var loop = LBT.loop + , fps = loop.fps() + , n_active = LBT.active.size() + , n_units = LBT.units.size() + , n_projs = LBT.bullets.size() + ; + + $('#info #state').text( loop.running ? 'Running!' : ('Paused (tick '+TICKS+')') ); + $('#info [name=fps]').val( fps.toFixed(2) + " / " + loop.framerate ); + $('#info [name=frame]').val( loop.frametime().toFixed(3)+" ms" ); + + $('#info [name=active]').val( n_active ); + $('#info [name=units]').val( n_units ); + $('#info [name=bullets]').val( n_projs ); + + loop.spark.drawTimes(); + + return false; +} + +function countdownToStart(n, fn){ + var el + , showFor = 750 + , pauseFor = 500 + + , body = $('body') + , sizeRatio = 0.6 + , values = new Y(1,n+1).unshift('GO').end() + , colors = [ '#E73075', '#2992C5', '#2992C5', '#2992C5' ] + ; + + function tickDown(){ + var bw = body.width() + , bh = body.height() + , size = bh*sizeRatio + + , el = $('
') + .addClass('countdown') + .width(bh*0.75).height(bh*0.75) + .text( values.pop() ) + .css({ + 'top' : ((bh - size)/2)+'px', + 'left' : (bw/2 - bh*0.375)+'px', + 'font-size' : size+'px', + 'background-color' : colors.pop(), + }) + ; + + ' -moz- -webkit-' + .split(' ') + .forEach(function(prefix){ + // el.css(prefix+'border-radius', (size*0.25)+'px'); + el.css(prefix+'border-radius', (bh/2)+'px'); + }); + + body.append(el); + + setTimeout(function(){ + el.remove(); + + if ( values.length ) { + setTimeout(tickDown, pauseFor); + } else { + fn(); + } + }, showFor); + } + + tickDown(); +} + +function readyToStart(){ + $('#overlay').hide(); + $(document).bind('keydown', 'return', pauseGame); + toggleGame(); +} + + + + +}).call(tanks.ui); + diff --git a/tanks.php b/tanks.php index 6177a46..9a3054d 100644 --- a/tanks.php +++ b/tanks.php @@ -11,10 +11,11 @@ class Tanks { static $mainScripts = array( - "src/tanks/main.js" - // "src/tanks/main-ui.js" + "src/tanks/ui/main.js" ); + + static $srcScripts = array( "src/tanks/tanks.js", "src/tanks/globals.js", @@ -34,9 +35,11 @@ class Tanks { "src/tanks/ui/grid.js", - "src/tanks/game/game.js" + "src/tanks/game.js" ); + + static $libScripts = array( "lib/jquery-1.4.3.js", "lib/jquery.sparkline.min.js", -- 1.7.0.4