/**
* 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._createMoveCallback( player, this._playerPos, function( pos )
{
_self._playerPos = pos;
} )
);
var sc = this._createStateCallback( player, this._playerPos, function( o )
{
_self._player = o;
} );
action.direction = direction;
action.srcPos = this._playerPos;
player.move( direction, function()
{
action.move();
}, sc );
}
} );