diff --git a/lib/ClassicMap.js b/lib/ClassicMap.js index 8f12f7d..641bb2b 100644 --- a/lib/ClassicMap.js +++ b/lib/ClassicMap.js @@ -57,14 +57,41 @@ ltjs.ClassicMap = Class( 'ClassicMap' ) 'private const _SIZE': 576, /** - * Game object size + * Game object offset and size * * The game objects are stores as a 16x16 multi-dimensional signed char * array (in the C sources). * - * @type {number} + * @type {Array.} */ - 'private const _GOSIZE': 256, + 'private const _GOSIZE': [ 0, 256 ], + + /** + * Offset and length of map name + * @type {Array.} + */ + 'private const _NAMESIZE': [ 256, 31 ], + + /** + * Offset and length of map hint + * @type {Array.} + */ + 'private const _HINTSIZE': [ 287, 256 ], + + /** + * Offset and length of author name + * @type {Array.} + */ + 'private const _AUTHORSIZE': [ 543, 31 ], + + /** + * Offset and length of difficulty level + * + * 16-bit integer, little-endian + * + * @type {Array.} + */ + 'private const _DIFFSIZE': [ 574, 2 ], /** * Tunnel bitmask @@ -102,6 +129,12 @@ ltjs.ClassicMap = Class( 'ClassicMap' ) */ 'private _id': 0, + /** + * Offset of beginning of map data in bytes + * @type {number} + */ + 'private _offset': 0, + /** * Initialize map with map data and the given id @@ -113,6 +146,9 @@ ltjs.ClassicMap = Class( 'ClassicMap' ) { this._data = ''+( data ); this._id = +id; + + // calculate map offset in LVL data + this._offset = ( this.__self.$( '_SIZE' ) * ( this._id - 1 ) ); }, @@ -124,32 +160,15 @@ ltjs.ClassicMap = Class( 'ClassicMap' ) * game uses a multi-dimensional array [x][y], which creates an array of * columns (TPLAYFIELD, LTANK.H). * + * The object data at the requested position will be loaded and converted to + * integers (from a binary string). + * * @return {Array.} array of game objects */ 'public getObjects': function() { - // calculate offset in LVL file - var offset = ( this.__self.$( '_SIZE' ) * ( this._id - 1 ) ); - return this._getMapData( offset ); - }, - - - /** - * Retrieve the map data (game objects only) at the requested offset - * - * The object data at the requested position will be loaded and converted to - * integers (from a binary string). - * - * @param {number} offset offset of the requested map data - * - * @return {Array.} converted map data - */ - 'private _getMapData': function( offset ) - { - var tiles = this._data.substr( offset, this.__self.$( '_GOSIZE' ) ) - .split( '' ), - - i = tiles.length; + var tiles = this._getDataSegment( '_GOSIZE', false ).split( '' ), + i = tiles.length; while ( i-- ) { @@ -160,6 +179,25 @@ ltjs.ClassicMap = Class( 'ClassicMap' ) }, + /** + * Retrieve segment of data + * + * @param {string} name name of constant containing segment dfn + * @param {=boolean} stripnull whether to strip null bytes (default true) + */ + 'private _getDataSegment': function( name, stripnull ) + { + stripnull = ( arguments.length < 2 ) ? true : !!stripnull; + + var s = this.__self.$( name ), + data = this._data.substr( ( this._offset + s[ 0 ] ), s[ 1 ] ); + + return ( stripnull ) + ? data.split( '\0' )[ 0 ] + : data; + }, + + /** * Retrieve map dimensions * @@ -223,6 +261,74 @@ ltjs.ClassicMap = Class( 'ClassicMap' ) }, + /** + * Retrieve map name + * + * @return {string} map name + */ + 'public getMapName': function() + { + return this._getDataSegment( '_NAMESIZE' ); + }, + + + /** + * Retrieve map author name + * + * @return {string} map author name + */ + 'public getMapAuthor': function() + { + return this._getDataSegment( '_AUTHORSIZE' ); + }, + + + /** + * Retrieve map hint + * + * @return {string} map hint + */ + 'public getMapHint': function() + { + return this._getDataSegment( '_HINTSIZE' ); + }, + + + /** + * Retrieve map difficulty + * + * The map difficulty will be returned as a 0-indexed value between 0 and 4, + * with 0 representing "kids" and 4 representing "deadly". + * + * The original game uses a bitmask for this value (thus the 16-bit + * integer), which is really of no particular use. For simplicity's sake, we + * will convert it. + * + * @return {number} 0-indexed difficulty level + */ + 'public getMapDifficulty': function() + { + var val = this._getDataSegment( '_DIFFSIZE', false ), + i = val.length, + n = 0; + + // first, convert the value to an integer (from little-endian) + while ( i-- ) n += ( val.charCodeAt( i ) << ( 8 * i ) ); + + // Finally, convert to a simple 0-4 value to represent difficulty by + // taking the binary logarithm of the value (lg(n); original game uses + // bitmasks). For those who do not understand logarithms, the concept + // here is simple: if we are given a difficulty of "hard" (value of 8), + // that has a binary representation of: 1000. We are interested in the + // position of the 1-bit. Since each bit position is an exponent of two, + // we can reverse that calculation with a binary logarithm. So, log2(8), + // also written as lg(8), is equal to 3, since 2^3 = 8. Similarly, + // "deadly" = 16 = 0001 0000 => lg(16) = 4, and "kids" = 1 = 0001 => + // lg(1) = 0. This gives us a 0-indexed difficulty value. + return ( Math.log( n ) / Math.log( 2 ) ); + }, + + /** * Retrieve size of map in bytes * diff --git a/lib/Map.js b/lib/Map.js index 385b3e0..193b66a 100644 --- a/lib/Map.js +++ b/lib/Map.js @@ -103,6 +103,41 @@ ltjs.Map = Interface( 'Map', 'public isObjectTunnel': [ 'object_id' ], + /** + * Retrieve map name + * + * @return {string} map name + */ + 'public getMapName': [], + + + /** + * Retrieve map author name + * + * @return {string} map author name + */ + 'public getMapAuthor': [], + + + /** + * Retrieve map hint + * + * @return {string} map hint + */ + 'public getMapHint': [], + + + /** + * Retrieve map difficulty + * + * The map difficulty should be returned as a 0-indexed value between 0 and + * 4, with 0 representing "kids" and 4 representing "deadly". + * + * @return {number} 0-indexed difficulty level + */ + 'public getMapDifficulty': [], + + /** * Retrieve size of map in bytes *