342 lines
9.7 KiB
JavaScript
342 lines
9.7 KiB
JavaScript
/**
|
|
* Represents a classic map (level)
|
|
*
|
|
* 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/>.
|
|
*
|
|
*
|
|
* Each map is concatenated in the file and consists of the following
|
|
* information:
|
|
*
|
|
* - Playfield data (game objects), 16x16 multidimensional char array
|
|
* - Level name, 31 characters
|
|
* - Hint, 256 characters
|
|
* - Author, 31 characters
|
|
* - Difficulty, 16-bit integer, little endian
|
|
*
|
|
* One can easily calculate the position of the level in a file, given its
|
|
* number, by multiplying by the size of the data structure (see TLEVEL, LTANK.H
|
|
* in the original sources).
|
|
*
|
|
* It is worth mentioning how the playfield data is stored. Since the
|
|
* multidimensional array is defined as [x][y], the array is created as an
|
|
* "array of columns", meaning that the data is organized in columns instead of
|
|
* rows. For example, when viewing the data in a HEX editor that displays 16
|
|
* bytes per line (e.g. xxd), the map would appear to be mirrored rotated 90
|
|
* degrees counter-clockwise. To make it easier to visualize, one can create a
|
|
* map with a number of tunnels in a pattern to take advantage of the ASCII
|
|
* display.
|
|
*/
|
|
|
|
|
|
/**
|
|
* Represents a classic map, as they exist in the original game.
|
|
*
|
|
* Classic maps are 16x16 tiles in size (for a total of 256 tiles).
|
|
*/
|
|
ltjs.ClassicMap = Class( 'ClassicMap' )
|
|
.implement( ltjs.Map )
|
|
.extend(
|
|
{
|
|
/**
|
|
* Size of each map in bytes
|
|
* @type {number}
|
|
*/
|
|
'private const _SIZE': 576,
|
|
|
|
/**
|
|
* Game object offset and size
|
|
*
|
|
* The game objects are stores as a 16x16 multi-dimensional signed char
|
|
* array (in the C sources).
|
|
*
|
|
* @type {Array.<number>}
|
|
*/
|
|
'private const _GOSIZE': [ 0, 256 ],
|
|
|
|
/**
|
|
* Offset and length of map name
|
|
* @type {Array.<number>}
|
|
*/
|
|
'private const _NAMESIZE': [ 256, 31 ],
|
|
|
|
/**
|
|
* Offset and length of map hint
|
|
* @type {Array.<number>}
|
|
*/
|
|
'private const _HINTSIZE': [ 287, 256 ],
|
|
|
|
/**
|
|
* Offset and length of author name
|
|
* @type {Array.<number>}
|
|
*/
|
|
'private const _AUTHORSIZE': [ 543, 31 ],
|
|
|
|
/**
|
|
* Offset and length of difficulty level
|
|
*
|
|
* 16-bit integer, little-endian
|
|
*
|
|
* @type {Array.<number>}
|
|
*/
|
|
'private const _DIFFSIZE': [ 574, 2 ],
|
|
|
|
/**
|
|
* Tunnel bitmask
|
|
* @type {number}
|
|
*/
|
|
'private const _TMASK': 0x40,
|
|
|
|
/**
|
|
* Colors used to identify tunnels
|
|
*
|
|
* These colors will be rendered in the background and will bleed through
|
|
* the transparent portions of the tile. We use explicit HEX codes rather
|
|
* than CSS color names because environments may vary the colors used.
|
|
*
|
|
* Taken from ColorList in LTANK2.C in the original sources. Note that there
|
|
* is an endianness difference.
|
|
*
|
|
* @type {Array.<string>}
|
|
*/
|
|
'private const _TCOLORS': [
|
|
'#ff0000', '#00ff00', '#0000ff', '#00ffff', // r g b c
|
|
'#ffff00', '#ff00ff', '#ffffff', '#808080' // y m w b
|
|
],
|
|
|
|
|
|
/**
|
|
* Map set data (binary string)
|
|
* @type {string}
|
|
*/
|
|
'private _data': null,
|
|
|
|
/**
|
|
* Map id (1-indexed)
|
|
* @type {string}
|
|
*/
|
|
'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
|
|
*
|
|
* @param {ltjs.MapSet} set map set data
|
|
* @param {number} id 1-indexed map id
|
|
*/
|
|
__construct: function( data, id )
|
|
{
|
|
this._data = ''+( data );
|
|
this._id = +id;
|
|
|
|
// calculate map offset in LVL data
|
|
this._offset = ( this.__self.$( '_SIZE' ) * ( this._id - 1 ) );
|
|
},
|
|
|
|
|
|
/**
|
|
* Retrieve game objects
|
|
*
|
|
* The game objects are returned in a manner consistent with the original
|
|
* sources - in columns, not rows. The reason for this is that the original
|
|
* 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.<number>} array of game objects
|
|
*/
|
|
'public getObjects': function()
|
|
{
|
|
var tiles = this._getDataSegment( '_GOSIZE', false ).split( '' ),
|
|
i = tiles.length;
|
|
|
|
while ( i-- )
|
|
{
|
|
tiles[ i ] = tiles[ i ].charCodeAt( 0 );
|
|
}
|
|
|
|
return tiles;
|
|
},
|
|
|
|
|
|
/**
|
|
* 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
|
|
*
|
|
* @return {Array.<number>} width and height in tiles
|
|
*/
|
|
'public getDimensions': function()
|
|
{
|
|
return [ 16, 16 ];
|
|
},
|
|
|
|
|
|
/**
|
|
* Retrieve map of object codes to their appropriate tiles
|
|
*
|
|
* @return {Array.<string>}
|
|
*/
|
|
'public getObjectTileMap': function()
|
|
{
|
|
// we return these values here instead of returning, say, a constant,
|
|
// because we would have to clone it to ensure that our inner state
|
|
// would not be altered
|
|
return [
|
|
'dirt', 'tup', 'base', 'water', 'block', 'mblock', 'brick',
|
|
'atup', 'atright', 'atdown', 'atleft', 'mirrorul', 'mirrorur',
|
|
'mirrordr', 'mirrordl', 'owup', 'owright', 'owdown', 'owleft',
|
|
'cblock', 'rmirrorul', 'rmirrorur', 'rmirrordr', 'rmirrordl',
|
|
'ice', 'thinice'
|
|
];
|
|
},
|
|
|
|
|
|
/**
|
|
* Retrieve tunnel color
|
|
*
|
|
* The color will be rendered in the background and will bleed through the
|
|
* transparent portions of the tile. We use explicit HEX codes rather than
|
|
* CSS color names because environments may vary the colors used.
|
|
*
|
|
* @param {number} oid tunnel object id
|
|
*
|
|
* @return {string} tunnel color
|
|
*/
|
|
'public getTunnelColor': function( oid )
|
|
{
|
|
// get the tunnel id by stripping off the tunnel bitmask and then grab
|
|
// the associated color
|
|
var tunnel_id = ( ( +oid ^ this.__self.$( '_TMASK' ) ) / 2 );
|
|
|
|
return this.__self.$( '_TCOLORS' )[ tunnel_id ] || 'black';
|
|
},
|
|
|
|
|
|
/**
|
|
* Determines if the given object is a tunnel
|
|
*
|
|
* @return {boolean} true if tunnel, otherwise false
|
|
*/
|
|
'public isObjectTunnel': function( oid )
|
|
{
|
|
return ( oid & this.__self.$( '_TMASK' ) );
|
|
},
|
|
|
|
|
|
/**
|
|
* 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
|
|
*
|
|
* @return {number} size of map in bytes
|
|
*/
|
|
'public static getMapSize': function()
|
|
{
|
|
return this.$( '_SIZE' );
|
|
}
|
|
} );
|