/** * 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 . * * * 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.} */ '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 * @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.} */ '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.} 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.} width and height in tiles */ 'public getDimensions': function() { return [ 16, 16 ]; }, /** * Retrieve map of object codes to their appropriate tiles * * @return {Array.} */ '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' ); } } );