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