543 lines
16 KiB
JavaScript
543 lines
16 KiB
JavaScript
/**
|
|
* 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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
|
|
/**
|
|
* 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.<function(GameObject,number)>}
|
|
*/
|
|
'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.<number>} objs game object data (object ids)
|
|
* @param {Array.<string>} 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 );
|
|
}
|
|
} );
|