Skip to content

Instantly share code, notes, and snippets.

@lardratboy
Last active August 29, 2015 14:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lardratboy/aa0f25e6741929b1b33f to your computer and use it in GitHub Desktop.
Save lardratboy/aa0f25e6741929b1b33f to your computer and use it in GitHub Desktop.
Phaser.io Prefab/Scene Layout Factory - early pass
// Author Brad P. Taylor (bradptaylor+github@gmail.com) license MIT
///<reference path="../../../../bower_components/phaser-official/build/phaser.d.ts"/>
/*
Pefab JSON configuration / layout format details
Every Prefab registered is automatically associated with an [OPTIONAL] .json file, the
members of this .json data blob if present are currently processed as follows.
'config' is used to modify existing properties of the object created,
properties are MODIFIED* not created.
'mixin' uses Phaser.Utils.mixin to add properties to the target (which will create if not present)
'configFrom' is an object of properties to modify, a string referencing a .json key or an array
of objects, arrays or properties to modify. this is handled by the bpt.prefab.configPrefab function.
'contents' is an array, object or string referencing a .json key. This member is
processed by the bpt.prefab.loadContents method, items in the contents are added directly
to the item that is being configured.
A content .json blob is basically a child description system (think scene graph), unlike the config .json blobs,
the content blob creates items, configs modify contents create.
Each property name is looked up in the bpt.prefab.Factory and created from registered items. The constructor
for the is prefab is passed property values which are passed in order defined by the 'named alias ordering
table' associated with each prefab. if the prefab creation member contains a 'config' or 'contents' member
this block is then processed by bpt.prefab.configurePrefab.
'named/alias ordering table' fancy name for an array of arrays, which associate property names to array order.
When a prefab is registered it can be given this array of arrays which describe how to translate
named property values from the content descriptor into constructor arguments in proper order.
example [ ["x"], ["y"], ["key"] ] given {x:123,y:456,opacity,key:"happy"} would produce [123,456,"happy"]
'aliased properties to configuration property source' fancy name for an object which defines target
properties to modify after prefab creation. (basically think of this as shorthand for 'config' .json blobs)
example { visible:["visible"], alpha:["opacity","alpha"], name:["id","name"] }
given { opacity:0.5, id:"bob" } would produce { alpha:0.5, name:"bob" }
How properties are *MODIFIED.
Instead of just simply assigning property values, the config/content system can handle a small amount of
'processing requests' (aka mogrifiers), these can be used for logging prefab creation, for picking a
random value for property initializer or even requesting the drop into the debugger when the prefab
is created. These processing requests are handled by what I call mogrifiers, is really just a fancy name
for a function that takes two value. The initial property value and the data to be used by the mogrifier.
Aspects of this should be abstracted - this could be easily adjusted to just work for PIXI or Phaser.
*/
module bpt.prefab {
export var rnd_:any = undefined;
export var game_:Phaser.Game;
// ------------------------------------------------------------------------
export var configAuditor:any = undefined;
export function _log_(...args:any[]);
export function _log_() { if ('undefined' !== typeof console) console.log.apply( console, arguments ); }
function makeLogger(prefix) {
return ('undefined' !== typeof console) && console.log.bind ? console.log.bind(console,prefix) : _log_;
}
export var logger:any = {
warning : makeLogger('warning'),
trace : makeLogger('trace'),
contents : undefined, //makeLogger('contents')
mogrify : undefined, //makeLogger('mogrify'),
modify : undefined, //makeLogger('modify')
log : undefined //makeLogger('log')
};
// ------------------------------------------------------------------------
export class PropertyMogrifier {
// TODO - this probably should have a type and a registration process
private static transmogrifiers_ = {
".=": (a, b) => (b), // function( a:any, b:any ):any { return b; },
".+=": (a, b) => (a + b), // function( a:any, b:any ):any { return a+b; },
".-=": (a, b) => (a - b), // function( a:any, b:any ):any { return a-b; },
".*=": (a, b) => (a * b), // function( a:any, b:any ):any { return a*b; },
"./=": (a, b) => (a / b), // function( a:any, b:any ):any { return a/b; },
".|=": (a, b) => (a | b), // function( a:any, b:any ):any { return a|b; },
".&=": (a, b) => (a & b), // function( a:any, b:any ):any { return a&b; },
".^=": (a, b) => (a ^ b), // function( a:any, b:any ):any { return a^b; },
".>>=": (a, b) => (a >> (b | 0)), // function( a:any, b:any ):any { return a>>(b|0); },
".<<=": (a, b) => (a << (b | 0)), // function( a:any, b:any ):any { return a<<(b|0); },
".>>>=": (a, b) => (a >>> (b | 0)), // function( a:any, b:any ):any { return a>>>(b|0); },
".text": function (a:any, b:any):any {
if (bpt.prefab.game_ && bpt.prefab.game_.cache.checkTextKey(b)) {
return bpt.prefab.game_.cache.getText(b);
} else if (bpt.prefab.logger.warning) {
bpt.prefab.logger.warning('text transmogrify missing data?', {audit: {key: b, game: bpt.prefab.game_}})
}
return a;
},
".json": function (a:any, b:any):any {
if (bpt.prefab.game_ && bpt.prefab.game_.cache.checkJSONKey(b)) {
return bpt.prefab.game_.cache.getJSON(b);
} else if (bpt.prefab.logger.warning) {
bpt.prefab.logger.warning('json transmogrify missing data?', {audit: {key: b, game: bpt.prefab.game_}})
}
return a;
},
".toString": function (a:any, b:any):string {
if ('number' === typeof b) return a.toString(b);
return a.toString();
},
".parseInt": function (a:any, b:any):any {
return parseInt(a, b);
},
".prefix": function (a:any, b:any):any {
return b.toString() + a.toString();
},
".append": function (a:any, b:any):any {
return a.toString() + b.toString();
},
".rnd": function (a:any, b:any):any {
var rnd = (rnd_ || Math.random)();
if (b instanceof Array) {
if ('number' !== typeof b[0]) {
return b[ (rnd * b.length) | 0 ];
}
return b[0] + rnd * (b[1] - b[0]);
}
if ('number' == typeof b) return rnd * b;
return rnd;
},
".->": function (a:any, b:any):any {
for (var k in b) {
a = PropertyMogrifier.transmogrifyValue_(a, b[k]);
}
return a;
},
".trace": function (a:any, b:any):any {
if (bpt.prefab.logger.trace) bpt.prefab.logger.trace("transmogrify", a, b);
return a;
},
".log": function (a:any, b:any):any {
if (bpt.prefab.logger.log) bpt.prefab.logger.log("transmogrify", a, b);
return a;
},
".debugger": function (a:any, b:any):any {
debugger;
return a;
}
}
static transmogrifyValue_(value:any, mogrifier:any):any {
if (('object' === typeof mogrifier) && (!(mogrifier instanceof Array))) {
for (var key in mogrifier) {
if ('function' === typeof PropertyMogrifier.transmogrifiers_[key]) {
if (bpt.prefab.logger.mogrify) var before = value.toString();
value = PropertyMogrifier.transmogrifiers_[key](value, mogrifier[key]);
if (bpt.prefab.logger.mogrify) {
var after = value.toString();
if (before !== after) {
bpt.prefab.logger.mogrify(before, '->', after, {audit: mogrifier[key]});
}
}
}
}
}
return value;
}
private static splitCache_ = {};
static modifyExisting(target:any, source:any) {
if (source && ('object' === typeof source) &&
('string' === typeof source.json) && target.game.cache.checkJSONKey(source.json)) {
source = target.game.cache.getJSON(source.json);
}
for (var key in source) {
var value = source[key];
if (undefined === value) continue;
var tokens = PropertyMogrifier.splitCache_[key],
outer = target,
nested = target;
if ( undefined === tokens ) {
tokens = key.split('.');
PropertyMogrifier.splitCache_[ key ] = tokens;
}
for (var i = 0; i < tokens.length; ++i) {
outer = nested;
if (undefined === nested) break;
nested = nested[tokens[i]];
}
if (undefined !== nested) {
if (bpt.prefab.logger.modify) var modifierLogHelperBefore = outer[tokens[tokens.length - 1]].toString();
if (('object' !== typeof value) || ((value instanceof Array) && (nested instanceof Array))) {
outer[tokens[tokens.length - 1]] = value;
} else if ('object' !== typeof outer[tokens[tokens.length - 1]]) {
outer[tokens[tokens.length - 1]] = PropertyMogrifier.transmogrifyValue_(outer[tokens[tokens.length - 1]], value);
} else {
PropertyMogrifier.modifyExisting(nested, value);
}
if (bpt.prefab.logger.modify) {
var modifierLogHelperAfter = outer[tokens[tokens.length - 1]].toString();
if (modifierLogHelperBefore !== modifierLogHelperAfter) {
bpt.prefab.logger.modify(key, "modified", modifierLogHelperBefore, "->", modifierLogHelperAfter, {audit: target});
} else {
bpt.prefab.logger.modify(key, "no change modifier?", modifierLogHelperBefore, {audit: target});
}
delete modifierLogHelperBefore;
delete modifierLogHelperAfter;
}
} else if (bpt.prefab.logger.warning) {
bpt.prefab.logger.warning(key, "is not a member of target?", {audit: target});
}
}
}
}
// ------------------------------------------------------------------------
export var helper_transformed_named_args_to_config = {
visible:["visible"],
alpha:["opacity","alpha"],
angle:["angle"],
rotation:["rotation"],
scale:["zoom","scale"],
tint:["tint"],
pivot:["origin","pivot"],
exists:["exists"],
frameName:["frameName"]
};
export var default_constructor_argument_order = [ ["x"], ["y"], ["key"], ["frame"] ];
class FactoryItem {
named_args_to_structure:Object;
named_args_order:any[];
Type:any;
constructor( Type:any, named_args_order:any[], named_args_to_structure?:Object ) {
this.Type = Type;
this.named_args_order = named_args_order;
this.named_args_to_structure = named_args_to_structure;
}
}
export class Factory {
private static registered:any = {};
private static makeSimplePrefabFactory( key:string, base:any, config:any ) {
var prefab:any = (function (_super) {
for (var p in _super) if (_super.hasOwnProperty(p)) prefab[p] = _super[p];
function maker() { this.constructor = prefab; }
maker.prototype = _super.prototype;
prefab.prototype = new maker();
prefab.prototype['$.bpt.prefab'] = {'key':key, 'prefab':prefab, 'base':base, 'config':config };
prefab.prototype.toString = function() { return key.concat( " name is '", this.name, "'" ); }
function prefab() { /* HOW CAN I RENAME THIS to type:string's value???? */
_super.apply(this, arguments);
this.name = this.name || key;
configPrefab(this, config);
}
return prefab;
})(base);
return prefab;
}
static isRegistered( key ):boolean {
return (undefined !== Factory.registered[key]);
}
static getNamedArgsFor_( key ) {
if ( !Factory.isRegistered(key) ) return undefined;
return Factory.registered[key].named_args_order;
}
static getNamedArgsToStructureFor_( key ):Object {
if ( !Factory.isRegistered(key) ) return undefined;
return Factory.registered[key].named_args_to_structure;
}
static isKeyIn_( key, source:any ) {
for ( var k in source )
if ( k === key || ('object' === typeof source[k]) && Factory.isKeyIn_( key, source[k])) return true;
return false;
}
static add( key, Type:any,
named_args_order:any[] = default_constructor_argument_order,
named_args_to_structure?:Object ) {
if ( Factory.isRegistered(key) && bpt.prefab.logger.warning ) {
bpt.prefab.logger.warning( 'prefab key already registered', key );
}
Factory.registered[key] = new FactoryItem( Type, named_args_order, named_args_to_structure );
if ( bpt.prefab.logger.warning ) {
// check key against constructor/named structure factory stuff
// if there is an overlap and it is used for an array inside a contents/config
// it would potentially cause a problem - unlikely but could happen...
for ( var m in Factory.registered ) {
var item:FactoryItem = Factory.registered[m];
if ( Factory.isKeyIn_(key,item.named_args_order) ) {
bpt.prefab.logger.warning( key, "found in", m, item.named_args_order );
}
if ( Factory.isKeyIn_(key,item.named_args_to_structure) ) {
bpt.prefab.logger.warning( key, "found in", m, item.named_args_to_structure );
}
}
}
}
static addAlias( alias, key ) {
if ( undefined !== Factory.registered[key]) {
Factory.registered[alias] = Factory.registered[key];
} else if ( bpt.prefab.logger.warning ) {
bpt.prefab.logger.warning( 'addAlias - unknown original key', key, "for alias", alias );
}
}
static addSimple( key, base:any, named_args_order?:any[],
named_args_to_structure?:Object, config?:any ) {
Factory.add( key, Factory.makeSimplePrefabFactory(
key, base, config || (key + ".json") ), named_args_order, named_args_to_structure );
}
static addTwin( key, base:any, otherKey, config?:any ) {
Factory.addSimple( key, base,
Factory.getNamedArgsFor_( otherKey ),
Factory.getNamedArgsToStructureFor_( otherKey ), config );
}
static create( prefab, ...args:any[] ) :any {
return Factory.create_( prefab, args );
}
static create_( prefab, params:any[] ) :any {
if ( undefined !== Factory.registered[prefab]) {
var instance:any = Object.create(Factory.registered[prefab].Type.prototype);
Factory.registered[prefab].Type.apply( instance, params );
return instance;
} else if ( bpt.prefab.logger.warning ) {
bpt.prefab.logger.warning( 'create_ - unknown prefab', prefab );
}
}
}
// ------------------------------------------------------------------------
var createPrefabParamsArray:any[] = []; // reusable array for multiple calls
function createPrefabFromTransmogrifiedNamedArguments( game, key, values ) : any {
var argument_order:any[] = Factory.getNamedArgsFor_(key) || default_constructor_argument_order;
/* TODO - check cache (argument_order/values) */
createPrefabParamsArray.length = argument_order.length + 1;
var args = createPrefabParamsArray;
args[0] = game;
var bCachable:boolean = true;
for ( var i = 1; i <= argument_order.length; ++i ) {
var value:any = undefined;
var aliases:any[] = argument_order[i-1];
for ( var j = aliases.length; --j >= 0; ) {
value = values[ aliases[j] ];
if ( undefined !== value ) break;
}
args[i] = PropertyMogrifier.transmogrifyValue_( value, value );
bCachable = bCachable && (value === args[i]);
}
if ( bCachable ) {
// TODO - add to cache
// remember to create a new candidate cache
// createPrefabParamsArray = [];
}
return Factory.create_( key, args );
}
// this is here for a shortcut to avoid having to do a manual config block for x/y etc
function getOptionalConfigFromTransmogrifiedNamedArguments( initializer, key, values ): any {
var argument_to_structure:Object = Factory.getNamedArgsToStructureFor_(key);
if ( undefined === argument_to_structure ) return initializer;
var custom = {};
for ( var key in initializer ) custom[key] = initializer[key];
for ( var member in argument_to_structure ) {
var aliases:any[] = argument_to_structure[member];
var value:any = undefined;
for ( var i = aliases.length; --i >= 0; ) {
value = values[ aliases[i] ];
if ( undefined !== value ) break;
}
if ( undefined !== value ) {
custom[member] = PropertyMogrifier.transmogrifyValue_( value, value );
}
}
return custom;
}
// ------------------------------------------------------------------------
function loadContentsFromObject_( target:any, source:any, callDepth:number ) {
++callDepth;
var name_initializer = { name:"" };
for ( var key in source ) {
var values = source[key];
if ( Factory.isRegistered(key) ) {
var bunch:any = (values instanceof Array) ? values : [ values ];
for ( var i = 0, l = bunch.length; l > i; ++i ) {
values = bunch[i];
var name = values.name || values.id || key;
name_initializer.name = PropertyMogrifier.transmogrifyValue_(name,name);
var initializer = name_initializer;
var prefab = createPrefabFromTransmogrifiedNamedArguments( target.game, key, values );
initializer = getOptionalConfigFromTransmogrifiedNamedArguments( initializer, key, values );
if ( prefab ) {
if ( bpt.prefab.logger.contents ) bpt.prefab.logger.contents( prefab, "(", callDepth, ")" );
target.addChild(prefab);
PropertyMogrifier.modifyExisting( prefab, initializer );
if ( values.config ) {
configPrefab( prefab, {config:values.config}, callDepth );
}
if ( values.contents ) {
loadContents( prefab, values.contents, callDepth );
}
for ( var sub in values ) {
if ( Factory.isRegistered(sub) ) {
var wrapper = {};
wrapper[sub] = values[sub];
loadContentsFromObject_( prefab, wrapper, callDepth );
}
}
} else if ( bpt.prefab.logger.warning ) {
bpt.prefab.logger.warning( "trouble creating prefab of type", key );
}
}
} else if ("contents" === key) {
loadContents(target, values, callDepth);
} else if ( bpt.prefab.logger.warning ) {
bpt.prefab.logger.warning( "unexpected prefab key", key, "inside", target.name, {'target':target,'source':source} );
}
}
}
function loadContentsFromJSON_( target:any, key:string, callDepth:number ) {
++callDepth;
if (!key || !target.game.cache.checkJSONKey(key) ) return;
loadContentsFromObject_( target, target.game.cache.getJSON(key), callDepth );
}
export function loadContents( target:any, source:any, callDepth:number = -1 ) {
++callDepth;
if ( 'object' === typeof source ) {
if ( source instanceof Array ) {
for ( var i = 0; i < source.length; ++i ) {
loadContents(target, source[i], callDepth);
}
} else {
loadContentsFromObject_( target, source, callDepth );
}
} else {
loadContentsFromJSON_( target, source, callDepth );
}
}
// ------------------------------------------------------------------------
function configPrefabFromObject_( target:any, settings:any, callDepth:number ) {
++callDepth;
if ( bpt.prefab.configAuditor ) {
bpt.prefab.configAuditor( target, settings );
}
if ( settings.mixin ) {
Phaser.Utils.mixin( settings.mixin, target );
}
if ( settings.configFrom ) {
configPrefab( target, settings.configFrom, callDepth );
}
var bResetTexture = false;
if ( settings.config ) {
var pre_key = target.key, pre_frame = target.frame;
PropertyMogrifier.modifyExisting(target, settings.config);
bResetTexture = (pre_key !== target.key) || (pre_frame !== target.frame);
}
if ( bResetTexture ) {
target.loadTexture( target.key, target.frame );
}
if ( settings.contents ) {
loadContents( target, settings.contents, callDepth );
}
// 'animations' are sort of special and should be part of another
// registry type of system for specific descendant of a class type of thing
if ( settings.animations && (target instanceof Phaser.Sprite) ) {
for ( var akey in settings.animations ) {
var anim:any = settings.animations[akey];
target.animations.add( anim.key, anim.frames, anim.fps, anim.loop );
}
}
// what about basic anchor independent positioning (left,top,right,bottom?)
}
function configPrefabFromJSON_( target:any, key, callDepth:number ) {
if (!key || !target.game.cache.checkJSONKey(key) ) return;
++callDepth;
configPrefabFromObject_( target, target.game.cache.getJSON(key), callDepth );
}
export function configPrefab( target:any, source:any, callDepth:number = -1 ) {
++callDepth;
if ( 'object' == typeof source ) {
if ( source instanceof Array ) {
for ( var i = 0; i < source.length; ++i ) {
configPrefab(target, source[i], callDepth);
}
} else {
configPrefabFromObject_(target, source, callDepth);
}
} else {
configPrefabFromJSON_( target, source, callDepth );
}
}
// ------------------------------------------------------------------------
// Phaser full names
Factory.addSimple( 'Phaser.Image', Phaser.Image, undefined,
helper_transformed_named_args_to_config );
Factory.addSimple( 'Phaser.Sprite', Phaser.Sprite, undefined,
helper_transformed_named_args_to_config );
Factory.addSimple( 'Phaser.Text', Phaser.Text, [ ["x"], ["y"], ["text"], ["style"] ],
helper_transformed_named_args_to_config );
Factory.addSimple( 'Phaser.TileSprite', Phaser.TileSprite,
[ ["x"], ["y"], ["width"], ["height"], ["key"], ["frame"] ] );
Factory.addSimple( 'Phaser.Button', Phaser.Button,
[["x"], ["y"], ["key"], ["callback"], ["callbackContext"],
["overFrame"], ["outFrame"], ["downFrame"], ["upFrame"]] );
Factory.addSimple( 'Phaser.Particles.Arcade.Emitter', Phaser.Particles.Arcade.Emitter,
[["x"], ["y"], ["maxParticles","max"]] );
// prefab Phaser.Group/SpriteBatch override the automatic add to world
export class PrefabGroupShim extends Phaser.Group {
constructor(game) { super( game, null ); }
}
export class PrefabBatchShim extends Phaser.SpriteBatch {
constructor(game) { super( game, null ); }
}
Factory.addSimple( 'Phaser.Group', PrefabGroupShim, [[]], {x:["x"],y:["y"]} );
Factory.addSimple( 'Phaser.SpriteBatch', PrefabBatchShim, [[]], {x:["x"],y:["y"]} );
}
@lardratboy
Copy link
Author

Here are a couple .json blobs to show the general format accepted by the prefab config code

bpt.prefab.loadContents( this.game.world, {
    "contents" : [
        {"image" : { "x":0, "y":0, "key":"background" }},
        {"image" : { "x":15, "y":11, "key":"project_header" }},
        {"image" : { "x":856, "y":13, "key":"hey_teachers_button" }},
        {"image" : { "x":59, "y":699, "key":"other_charts_button" }},
        {"image" : { "x":22, "y":100, "key":"select_type_of_chart_button" }},
        {"image" : { "x":263, "y":107, "key":"adjust_chart_tab_button" }},
        {"image" : { "x":437, "y":92, "key":"add_background_tab_button" }},
        {"image" : { "x":666, "y":89, "key":"add_text_tab_button" }},
        {"image" : { "x":828, "y":90, "key":"add_pictures_tab_button" }},
        {"image" : { "x":13, "y":188, "key":"context_panel" }},
        {"image" : { "x":262, "y":189, "key":"chart_area_panel" }},
        {"Counter_widget_prefab" : { "x":40, "y":275}},
        {"d3_test_prefab" : {} }
    ]
});

or even better

bpt.prefab.loadContents( this.game.world, {
    "contents" : [
        {"image":[
            { "x":0, "y":0, "key":"background" },
            { "x":15, "y":11, "key":"project_header" },
            { "x":856, "y":13, "key":"hey_teachers_button" },
            { "x":59, "y":699, "key":"other_charts_button" },
            { "x":22, "y":100, "key":"select_type_of_chart_button" },
            { "x":263, "y":107, "key":"adjust_chart_tab_button" },
            { "x":437, "y":92, "key":"add_background_tab_button" },
            { "x":666, "y":89, "key":"add_text_tab_button" },
            { "x":828, "y":90, "key":"add_pictures_tab_button" },
            { "x":13, "y":188, "key":"context_panel" },
            { "x":262, "y":189, "key":"chart_area_panel" }
        ]},
        {"Counter_widget_prefab" : { "x":40, "y":275}},
        {"d3_test_prefab" : {} }
    ]
});

here is json blob that uses a sequence mogrify operation to pick a random frame for each of this prefab's instances.

{
    "config": {
        "key": "amys_game_atlas",
        "animationFrame": {
            ".->" : [
                {".rnd":[1,25]},
                {".parseInt":10},
                {".prefix":"original_pieces_"}
            ]
        }
   }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment