/** * Represents the current state of a map * * Copyright (C) 2012 Mike Gerwitz * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ /** * Represents the current state of a map */ ltjs.MapState = Class( 'MapState', { /** * Game object factory * @type {ltjs.GameObjectFactory} */ 'private _objFactory': null, /** * Player game object tile position * @type {number} */ 'private _playerPos': 0, /** * Reference to player object * @type {GameObject} */ 'private _player': null, /** * Game objects representing every object on the map * @type {GameObject} */ 'private _objs': [], /** * Continuations to invoke when object state changes * @type {Array.} */ 'private _stateCallbacks': [], /** * Initializes map state with a given map and factory with which to create * game objects * * Game objects influence map state rules, therefore fundamental game logic * may be altered simply by passing in a custom GameObjectFactory instance. * * @param {Map} map game map * @param {GameObjectFactory} obj_factory game object factory */ __construct: function( map, obj_factory ) { if ( !( Class.isA( ltjs.GameObjectFactory, obj_factory ) ) ) { throw TypeError( 'Invalid GameObjectFactory provided' ); } // initialize game objects based on the initial map state this._objFactory = obj_factory; this._initObjects( map.getObjects(), map.getObjectTileMap() ); }, /** * Flush map state, triggering the change event for each game object * * This may be used to perform an initial rendering or re-draw of the entire * map. Note that this should not be used to re-render the map after * movements or state changes, as that would not be performant (especially * since the map size could be arbitrarily large). */ 'public flush': function() { var _self = this; // emit the change event for each game object this._forEachObj( function( obj, pos ) { _self._emitChange( obj, pos ); } ); }, /** * Register an object change callback * * Registers a continuation to be called when a game object changes state * (a state change may represent an object transforming into another, a * movement, or anything else that requires re-rendering). * * TODO: use event base * * The continuation will be passed the game object in its new state its tile * position. If the game object is null, then it is to be assumed that the * object no longer exists (e.g. has moved to another location) and should * be cleared. * * @param {function(GameObject,number)} callback continuation * * @return {MapState} self */ 'public onChange': function( callback ) { this._stateCallbacks.push( callback ); return this; }, /** * Emit a change event for the given object * * States that an object's state has changed; see the onChange() method for * additional information. A change may not necessarily imply that the * object itself has changed---the change may simply represent a new * position or---in the case of a null object---that the position has been * cleared of an object. * * @param {GameObject} obj the game object that has updated, or null * @param {number} pos tile position of the game object * * @return {undefined} */ 'private _emitChange': function( obj, pos ) { var i = -1, l = this._stateCallbacks.length; while ( ++i < l ) { this._stateCallbacks[ i ].call( this.__inst, obj, pos ); } if ( obj === null ) { var _self = this; this._objs[ pos ].forEach( function( o ) { if ( o === null ) return; _self._emitChange( o, pos ); } ); } }, /** * Initialize game objects for the map's original (default) state * * All necessary game objects will be created to represent all objects on * the map at its default state. Effectively, this creates the default map * state. * * @param {Array.} objs game object data (object ids) * @param {Array.} objmap map from object ids to their tile ids * * @return {undefined} */ 'private _initObjects': function( objs, objmap ) { var i = objs.length; while ( i-- ) { var val = objs[ i ], obj = this._createObj( objmap[ val ] ); this._objs[ i ] = []; this._addObj( obj, i ); // XXX: temporary if ( val === 1 ) { this._initPlayer( obj, i ); } } }, /** * Creates a game object from a given tile id and (optionally) a previous * object * * A previous game object may be provided if state is to be transferred * between objects---that is, the transformation of one game object into * another may require a certain transfer of information. If no such game * object is provided, then the game object will be created with its default * state. * * @param {string} tid tile id * @param {GameObject?} from optional game object for state change data * * @return {GameObject} new game object */ 'private _createObj': function( tid, from ) { var obj = this._objFactory.createObject( tid ); // if a previous object was provided, copy over its mutable attributes if ( from ) { from.cloneTo( obj ); } return obj; }, /** * Invokes a continuation for each game object on the map * * The continuation will be called with the game object and its tile * position. * * @param {function(GameObject, number)} c continuation * * @return {undefined} */ 'private _forEachObj': function( c ) { this._objs.forEach( function( objs, pos ) { objs.forEach( function( obj ) { c( obj, pos ); } ); } ); }, /** * Add a game object at the given tile position * * It is an error to provide an invalid tile position or non-game object. * * @param {GameObject} obj game object to add * @param {number} pos tile position * * @return {undefined} * * @throws {Error} if an invalid tile position * @throws {TypeError} if an invalid replacement game object */ 'private _addObj': function( obj, pos ) { // replace no object with the given object (that is, replace nothing // with something---add) this._replaceObj( null, obj, pos ); }, /** * Replaces the given game object at the given tile position with a new game * object * * If the given game object can be found at the given tile position, then it * will be replaced with the new given game object. If it cannot be found, * then it will be added at the given tile position without any replacement * being made (effectively an append), unless the given replacement object * is null. * * If appending, the object will be placed in any open space (represented by * a null); ``open'' space is created when an object is replaced with null, * which has the effect of removing an object entirely (with no * replacement). * * The change will result in the notification of any observers that have * registered continuations for game object state changes. * * It is an error to provide an unknown tile position or a replacement that * is not a game object. * * @param {GameObject} cur game object to replace (or null) * @param {GameObject} newobj replacement game object * @param {number} pos tile position * * @return {undefined} * * @throws {Error} if an invalid tile position * @throws {TypeError} if an invalid replacement game object */ 'private _replaceObj': function( cur, newobj, pos ) { var o = this._objs[ pos ], i = o.length; // type checks if ( !( Array.isArray( o ) ) ) { throw Error( "Invalid tile position: " + pos ); } if ( !( Class.isA( ltjs.gameobjs.GameObject, newobj ) || ( newobj === null ) ) ) { throw TypeError( "Invalid GameObject or null provided: " + newobj ); } var free = null; ( function() { while ( i-- ) { if ( o[ i ] === cur ) { o[ i ] = newobj; return; } else if ( o[ i ] === null ) { // record this as a free position for additions free = i; } } // not found; add if ( newobj === null ) return; else if ( free ) o[ i ] = newobj; else o.push( newobj ); } )(); // notify observers of the change this._emitChange( newobj, pos ); }, /** * Remove a game object at the given tile position * * If the game object is not found at the given tile position, then no * action will be taken. * * It is an error to provide an invalid tile position. * * @param {GameObject} obj game object to remove * @param {number} pos tile position * * @return {undefined} * * @throws {Error} if an invalid tile position */ 'private _removeObj': function( obj, pos ) { this._replaceObj( obj, null, pos ); }, /** * Move a game object from one tile position to another * * This has the direct effect of (a) removing the given game object from its * original position and (b) adding it to its new position. * * It is an error to specify an object that is not a game object, or to * specify tile positions that do not exist. * * @param {GameObject} obj game object to move * @param {number} from original tile position * @param {number} to destination tile position * * @return {undefined} * * @throws {TypeError} if an invalid game object * @throws {Error} if an invalid tile position */ 'private _moveObj': function( obj, from, to ) { this._removeObj( obj, from ); this._addObj( obj, to ); }, /** * Initializes player game object and tile position references * * These references exist purely for performance, preventing the need to * scan for game objects that may represent the player. This also allows for * any arbitrary game object to represent the "player". * * @param {GameObject} obj game object representing the player * @param {number} pos player tile position */ 'private _initPlayer': function( obj, pos ) { this._player = obj; this._playerPos = pos; }, /** * Changes the state of a game object at a given tile position * * The "state" of a game object is represented by the object's type. If the * state is unchanged, then no action will be taken. Otherwise, the game * object at the given tile position, if available, will be replaced with a * new game object representing the given state. If a game object cannot be * found at the given tile position, then it will be added. * * @param {GameObject} cur current game object * @param {string} state new object state * @param {number} pos tile position * * @return {GameObject} new game object * * @throws {TypeError} if state results in an invalid game object * @throws {Error} if an invalid tile position */ 'private _changeState': function( cur, state, pos ) { // if the state has not changed, then do nothing if ( state === cur.getTid() ) { return; } // replace game object with a new one var newobj = this._createObj( state, cur ); this._replaceObj( cur, newobj, pos ); return newobj; }, /** * Creates a callback to alter the state of an object * * Produces a continuation that will perform a state change on the given * game object. The desired state should be passed to the continuation. * * If an additional continuation c is provided, it will be invoked after the * state change and may be used to process the resulting game object. * * @param {GameObject} cur game object to alter * @param {number} pos game object tile position * * @param {function(GameObject)=} c additional continuation * * @return {function(string)} callback to perform state change */ 'private _createStateCallback': function( cur, pos, c ) { var _self = this; return function( state ) { var newobj = _self._changeState( cur, state, pos ); if ( typeof c === 'function' ) { c( newobj ); } }; }, /** * Creates a callback to move a game object to a new tile position * * Produces a continuation that will perform a tile position move on the * given game object. The desired direction of movement should be passed to * the continuation (see MapAction for direction codes). * * If an additional continuation c is provided, it will be invoked after the * movement and may be used to process the new position. * * @param {GameObject} cur game object to alter * @param {number} pos game object tile position * * @param {function(GameObject)=} c additional continuation * * @return {function(string)} callback to perform state change */ 'private _createMoveCallback': function( obj, pos, c ) { var _self = this; return function( dest ) { _self._moveObj( obj, pos, dest ); // call continuation with new position, if requested if ( typeof c === 'function' ) { c( dest ); } }; }, /** * Move a player in the given direction * * The directions, as defined in MapAction, are: 0:left, 1:up, 2:right, * 3:down. * * XXX: the bounds argument is temporary * * @param {number} direction direction code * @param {MapBounds} bounds map boundaries * * @return {undefined} */ 'public movePlayer': function( direction, bounds ) { var _self = this, player = this._player; // XXX: tightly coupled var action = ltjs.MapAction( bounds, this._createStateCallback( player, this._playerPos, function( o ) { _self._player = o; } ), this._createMoveCallback( player, this._playerPos, function( pos ) { _self._playerPos = pos; } ) ); action.direction = direction; action.srcPos = this._playerPos; player.move( action ); } } );