diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..7f6b15b --- /dev/null +++ b/.jshintrc @@ -0,0 +1,20 @@ +{ + "eqeqeq": true, + "esnext": true, + "forin": true, + "freeze": true, + "futurehostile": true, + "latedef": true, + "laxbreak": true, + "maxcomplexity": 100, + "maxdepth": 3, + "maxparams": 5, + "noarg": true, + "nocomma": true, + "node": true, + "nonbsp": true, + "nonew": true, + "undef": true, + "unused": true, + "varstmt": true +} diff --git a/Makefile.am b/Makefile.am index 2fc3379..27abb63 100644 --- a/Makefile.am +++ b/Makefile.am @@ -31,10 +31,16 @@ modindex: $(nsindex) standalone: lasertank.js lasertank.js: modindex - ./node_modules/.bin/browserify -s lasertank --debug src/index.js > "$@" + ./node_modules/.bin/browserify \ + -t strictify \ + -s lasertank \ + --debug \ + src/index.js \ + > "$@" test: check check: + jshint src/ scripts/ @PATH="$(PATH):$(CURDIR)/node_modules/mocha/bin" \ mocha --recursive $(TESTARGS) diff --git a/package.json.in b/package.json.in index bbb64e9..cc41e4b 100644 --- a/package.json.in +++ b/package.json.in @@ -20,7 +20,8 @@ "devDependencies": { "chai": ">=1.9.1", "mocha": ">=1.18.2", - "browserify": "~12" + "browserify": "~12", + "strictify": "~0.2" }, "licenses": [ diff --git a/scripts/game.js b/scripts/game.js index d81eef3..ca3eba6 100644 --- a/scripts/game.js +++ b/scripts/game.js @@ -17,21 +17,30 @@ * along with this program. If not, see . */ +"use strict"; + ( function() { +/* jshint browser:true */ +/* global lasertank */ -var load_ltg = lasertank.FileLoader( document.getElementById( 'ltg' ) ), - load_lvl = lasertank.FileLoader( document.getElementById( 'lvl' ) ), +const ltg_input = document.getElementById( 'ltg' ), + lvl_input = document.getElementById( 'lvl' ), - menu_bar = lasertank.ui.MenuBar( document.getElementById( 'menubar' ) ), + load_ltg = lasertank.FileLoader( ltg_input, new window.FileReader() ), + load_lvl = lasertank.FileLoader( lvl_input, new window.FileReader() ), - ele_game = document.getElementById( 'game' ), - ctx = document.getElementById( 'render' ).getContext( '2d' ), + ele_game = document.getElementById( 'game' ), + ctx = document.getElementById( 'render' ).getContext( '2d' ); - ltg_data = '', +let ltg_data = '', lvl_data = ''; +// XXX: relies on side-effects of ctor +lasertank.ui.MenuBar( document.getElementById( 'menubar' ) ); + + load_ltg.onLoad( function( e, data ) { if ( e ) throw e; @@ -40,6 +49,7 @@ load_ltg.onLoad( function( e, data ) gamechk(); } ); + load_lvl.onLoad( function( e, data ) { if ( e ) throw e; @@ -48,6 +58,7 @@ load_lvl.onLoad( function( e, data ) gamechk(); } ); + function gamechk() { if ( !( ltg_data && lvl_data ) ) return; @@ -55,13 +66,14 @@ function gamechk() // temporary if ( ele_game.className.search( 'opening' ) > -1 ) return; - lasertank.ClassicGame( ltg_data, lvl_data ) + lasertank.ClassicGame( document, ltg_data, lvl_data ) .on( 'ready', function() { - this.renderTo( ctx ); + this.renderTo( ctx, window ); } ); } + // temporary document.getElementById( 'new' ).onclick = function() { diff --git a/scripts/main.js b/scripts/main.js index 75877ef..a132606 100644 --- a/scripts/main.js +++ b/scripts/main.js @@ -17,5 +17,7 @@ * along with this program. If not, see . */ +/* jshint browser:true */ + // alert the user on all uncaught errors -window.onerror = alert; +window.onerror = window.alert; diff --git a/src/ClassicGame.js b/src/ClassicGame.js index ce6f116..ac6adb6 100644 --- a/src/ClassicGame.js +++ b/src/ClassicGame.js @@ -18,17 +18,18 @@ */ -var Class = require( 'easejs' ).Class, - Game = require( './Game' ), - ClassicGameObjectFactory = require( './ClassicGameObjectFactory' ), - ClassicMap = require( './ClassicMap' ), - ClassicTileDfn = require( './ClassicTileDfn' ), - LtgLoader = require( './LtgLoader' ), - MapBounds = require( './MapBounds' ), - MapRender = require( './MapRender' ), - MapSet = require( './MapSet' ), - MapState = require( './MapState' ), - TileMasker = require( './TileMasker' ); +const Class = require( 'easejs' ).Class, + Game = require( './Game' ), + ClassicGameObjectFactory = require( './ClassicGameObjectFactory' ), + ClassicTileDfn = require( './ClassicTileDfn' ), + LtgLoader = require( './LtgLoader' ), + ClassicLevel = require( './level/ClassicLevel' ), + LevelBounds = require( './level/LevelBounds' ), + LevelRender = require( './level/LevelRender' ), + LevelSet = require( './level/LevelSet' ), + LevelState = require( './level/LevelState' ), + TileMasker = require( './TileMasker' ); + /** * Facade for the classic (original) game of LaserTank @@ -56,10 +57,10 @@ module.exports = Class( 'ClassicGame' ) 'private _tileSet': null, /** - * Set of available maps - * @type {MapSet} + * Set of available levels + * @type {LevelSet} */ - 'private _mapSet': null, + 'private _levelSet': null, /** * Event handlers @@ -68,8 +69,8 @@ module.exports = Class( 'ClassicGame' ) 'private _callback': {}, /** - * Performs map rendering - * @type {MapRender} + * Performs level rendering + * @type {LevelRender} */ 'private _render': null, @@ -83,26 +84,31 @@ module.exports = Class( 'ClassicGame' ) /** * Initialize game with LTG and LVL data * - * The LTG and LVL data can be changed at any time, but are required in the - * constructor because they are needed in order for the game to be + * The LTG and LVL data can be changed at any time, but are required in + * the constructor because they are needed in order for the game to be * functional. * + * DOCUMENT is used internally for creating elements; the DOM will not + * be manipulated. + * + * @param {HTMLDocument} document DOM document + * * @param {string} ltg_data binary string containing LTG file data * @param {string} lvl_data binary string containing LVL file data */ - __construct: function( ltg_data, lvl_data ) + __construct: function( document, ltg_data, lvl_data ) { - var _self = this; + const _self = this; this._ltgLoader = LtgLoader(); - this._masker = TileMasker( ClassicTileDfn() ); + this._masker = TileMasker( ClassicTileDfn(), document ); this._gameObjFactory = ClassicGameObjectFactory(); - // load initial tile and map data from the LTG and LVL data + // load initial tile and level data from the LTG and LVL data this.setTileData( ltg_data, function() { - this.setMapData( lvl_data, function() + this.setLevelData( lvl_data, function() { _self._trigger( 'ready' ); } ); @@ -113,32 +119,35 @@ module.exports = Class( 'ClassicGame' ) /** * Render to the given 2d canvas context * - * @param {CanvasRenderingContext2d} ctx 2d canvas context + * EVENT_TARGET will be monitored for keypresses. + * + * @param {CanvasRenderingContext2d} ctx 2d canvas context + * @param {*} event_target keyboard event target * * @return {ClassicGame} self */ - 'public renderTo': function( ctx ) + 'public renderTo': function( ctx, event_target ) { - // if there is a previous renderer, free its canvas before continuing - // (to both clean up and to free any locks, allowing for tile set and - // map changes) + // if there is a previous renderer, free its canvas before + // continuing (to both clean up and to free any locks, allowing for + // tile set and level changes) if ( this._render ) { this._render.clearCanvas(); } - var map = this._mapSet.getMapByNumber( 1 ), - map_state = MapState( map, this._gameObjFactory ), - bounds = MapBounds( map ); + const level = this._levelSet.getLevelByNumber( 1 ), + level_state = LevelState( level, this._gameObjFactory ), + bounds = LevelBounds( level ); - // render the first map (hardcoded for now) - this._render = MapRender( ctx, this._tileSet ) - .render( map, map_state ); + // render the first level (hardcoded for now) + this._render = LevelRender( ctx, this._tileSet ) + .render( level, level_state ); // POC - window.onkeydown = function( event ) + event_target.onkeydown = function( event ) { - var dir; + let dir; switch ( event.keyCode ) { @@ -153,7 +162,8 @@ module.exports = Class( 'ClassicGame' ) return; } - map_state.movePlayer( dir, bounds ); + event.preventDefault(); + level_state.movePlayer( dir, bounds ); }; return this; @@ -171,8 +181,8 @@ module.exports = Class( 'ClassicGame' ) 'public setTileData': function( data, callback ) { // get tile metadata - var _self = this, - meta = this._ltgLoader.fromString( data ); + const _self = this, + meta = this._ltgLoader.fromString( data ); this._masker.getMaskedTiles( meta.tiles, meta.mask, function( tdata ) { @@ -185,16 +195,16 @@ module.exports = Class( 'ClassicGame' ) /** - * Set LVL data for maps + * Set LVL data for levels * * @param {string} data binary string containing LVL data * @param {function()} callback function to call when complete * * @return {ClassicGame} self */ - 'public setMapData': function( data, callback ) + 'public setLevelData': function( data, callback ) { - this._mapSet = MapSet( data, ClassicMap ); + this._levelSet = LevelSet( data, ClassicLevel ); callback.call( this.__inst ); return this; diff --git a/src/ClassicGameObjectFactory.js b/src/ClassicGameObjectFactory.js index 4c36d5d..f20cb9a 100644 --- a/src/ClassicGameObjectFactory.js +++ b/src/ClassicGameObjectFactory.js @@ -17,10 +17,10 @@ * along with this program. If not, see . */ -var Class = require( 'easejs' ).Class, - GameObjectFactory = require( './GameObjectFactory' ), - GameObject = require( './gameobjs/GameObject' ), - Tank = require( './gameobjs/Tank' ); +const Class = require( 'easejs' ).Class, + GameObjectFactory = require( './GameObjectFactory' ), + GameObject = require( './gameobjs/GameObject' ), + Tank = require( './gameobjs/Tank' ); module.exports = Class( 'ClassicGameObjectFactory' ) diff --git a/src/ClassicTileDfn.js b/src/ClassicTileDfn.js index 0fef088..d506850 100644 --- a/src/ClassicTileDfn.js +++ b/src/ClassicTileDfn.js @@ -17,8 +17,8 @@ * along with this program. If not, see . */ -var Class = require( 'easejs' ).Class, - TileDfn = require( './TileDfn' ); +const Class = require( 'easejs' ).Class, + TileDfn = require( './TileDfn' ); /** diff --git a/src/FileLoader.js b/src/FileLoader.js index 28ab62d..ba63145 100644 --- a/src/FileLoader.js +++ b/src/FileLoader.js @@ -21,7 +21,7 @@ * specs, as it depends on FileReader. */ -var Class = require( 'easejs' ).Class; +const Class = require( 'easejs' ).Class; /** @@ -49,8 +49,9 @@ module.exports = Class( 'FileLoader', * Initialize file loader, monitoring the given file element * * @param {HtmlInputElement} element file element to monitor + * @param {FileReader} reader file reader */ - __construct: function( element ) + __construct: function( element, reader ) { if ( element.type !== 'file' ) { @@ -58,6 +59,7 @@ module.exports = Class( 'FileLoader', } this._element = element; + this._reader = reader; }, @@ -100,23 +102,23 @@ module.exports = Class( 'FileLoader', */ 'private _loadFile': function( event ) { - var _self = this, - files = event.target.files; + const _self = this, + files = event.target.files; if ( files.length === 0 ) return; - var reader = new FileReader(); - reader.onload = function( revent ) + this._reader.onload = function( revent ) { _self._callback.call( this.__inst, null, revent.target.result ); }; - reader.onerror = function( e ) + + this._reader.onerror = function( e ) { _self._callback.call( this.__inst, e ); }; // load file - reader.readAsBinaryString( files[ 0 ] ); + this._reader.readAsBinaryString( files[ 0 ] ); } } ); diff --git a/src/Game.js b/src/Game.js index 30a4cf2..e2dad64 100644 --- a/src/Game.js +++ b/src/Game.js @@ -17,7 +17,7 @@ * along with this program. If not, see . */ -var Interface = require( 'easejs' ).Interface; +const Interface = require( 'easejs' ).Interface; /** @@ -50,14 +50,14 @@ module.exports = Interface( 'Game', /** - * Set LVL data for maps + * Set LVL data for levels * * @param {string} data binary string containing LVL data * @param {function()} callback function to call when complete * * @return {Game} self */ - 'public setMapData': [ 'data', 'callback' ], + 'public setLevelData': [ 'data', 'callback' ], /** diff --git a/src/GameObjectFactory.js b/src/GameObjectFactory.js index a9bec24..eff3e8c 100644 --- a/src/GameObjectFactory.js +++ b/src/GameObjectFactory.js @@ -17,7 +17,7 @@ * along with this program. If not, see . */ -var Interface = require( 'easejs' ).Interface; +const Interface = require( 'easejs' ).Interface; module.exports = Interface( 'GameObjectFactory', diff --git a/src/LtgLoader.js b/src/LtgLoader.js index 5c41bf0..0f8dd9c 100644 --- a/src/LtgLoader.js +++ b/src/LtgLoader.js @@ -42,7 +42,10 @@ * identify the file as a bitmap image.) */ -var Class = require( 'easejs' ).Class; +/* XXX: remove me! */ +/* globals btoa */ + +const Class = require( 'easejs' ).Class; /** @@ -73,7 +76,7 @@ module.exports = Class( 'LtgLoader', */ 'public fromString': function( ltg_data ) { - var mask_offset = this._getMaskOffsetFromData( ltg_data ); + const mask_offset = this._getMaskOffsetFromData( ltg_data ); return { name: this._getNameFromData( ltg_data ), @@ -112,7 +115,7 @@ module.exports = Class( 'LtgLoader', sgmt = this.__self.$( sgmt ); } - var data = String.prototype.substr.apply( ltg_data, sgmt ); + const data = String.prototype.substr.apply( ltg_data, sgmt ); return ( stripnull ) ? data.split( '\x00' )[ 0 ] @@ -187,8 +190,9 @@ module.exports = Class( 'LtgLoader', // grab the data and don't bother stripping off the null bytes (it would // function the same with them stripped, but let's avoid the confusion // since we are supposed to be working with a 32-bit value) - var data = this._getDataSegment( ltg_data, '_POS_MOFF', false ), - i = data.length, + const data = this._getDataSegment( ltg_data, '_POS_MOFF', false ); + + let i = data.length, offset = 0; // convert the DWORD entry (little-endian format, 32-bit) into an @@ -234,7 +238,7 @@ module.exports = Class( 'LtgLoader', */ 'private _getGameBitmap': function( ltg_data, mask_offset ) { - var bmp_offset = this.__self.$( '_OFFSET_HEADER_END' ); + const bmp_offset = this.__self.$( '_OFFSET_HEADER_END' ); // return the bitmap data up until the mask offset return ltg_data.substr( bmp_offset, ( mask_offset - bmp_offset ) ); diff --git a/src/TileDfn.js b/src/TileDfn.js index 80a0988..031ff34 100644 --- a/src/TileDfn.js +++ b/src/TileDfn.js @@ -75,7 +75,7 @@ * thinice - thin ice */ - var Interface = require( 'easejs' ).Interface; + const Interface = require( 'easejs' ).Interface; /** diff --git a/src/TileMasker.js b/src/TileMasker.js index 72f0087..3e09076 100644 --- a/src/TileMasker.js +++ b/src/TileMasker.js @@ -63,8 +63,8 @@ * rendering. */ -var Class = require( 'easejs' ).Class, - TileDfn = require( './TileDfn' ); +const Class = require( 'easejs' ).Class, + TileDfn = require( './TileDfn' ); /** * Slices tiles and applies masks @@ -77,6 +77,12 @@ module.exports = Class( 'TileMasker', */ 'private _context': null, + /** + * DOM document + * @type {HTMLDocument} + */ + 'private _document': null, + /** * Tile definition to use for all operations * @type {Array.>} @@ -121,9 +127,13 @@ module.exports = Class( 'TileMasker', * allows us to support *any* type of tile set -- not just those that are * defined by the original game. * - * @param {TileDfn} tile_dfn tile definition object + * DOCUMENT is used internally for creating elements; the DOM will not + * be manipulated. + * + * @param {TileDfn} tile_dfn tile definition object + * @param {HTMLDocument} document DOM document */ - __construct: function( tile_dfn ) + __construct: function( tile_dfn, document ) { if ( !( Class.isA( TileDfn, tile_dfn ) ) ) { @@ -137,13 +147,14 @@ module.exports = Class( 'TileMasker', // rather than accepting a context, we will create our own canvas in // memory to perform our operations (it will not be added to the DOM, so // these operations will not be visible to the user) - var context = document.createElement( 'canvas' ).getContext( '2d' ); + let context = document.createElement( 'canvas' ).getContext( '2d' ); // size the canvas so that it can fit the entire tileset context.canvas.width = this._setWidth; context.canvas.height = this._setHeight; - this._context = context; + this._context = context; + this._document = document; }, @@ -161,8 +172,8 @@ module.exports = Class( 'TileMasker', 'private _calcSetDimensions': function( tile_dfn ) { // these vars are for clarity - var sizes = tile_dfn.getTileDimensions(), - n = this._tileDfn.length; + const sizes = tile_dfn.getTileDimensions(), + n = this._tileDfn.length; // store values so that we do not have to make additional calls to our // TileDfn instance @@ -201,7 +212,7 @@ module.exports = Class( 'TileMasker', */ 'public getMaskedTiles': function( bmp_game, bmp_mask, callback ) { - var _self = this; + const _self = this; this._getImageData( bmp_mask, function( data_mask ) { @@ -248,26 +259,27 @@ module.exports = Class( 'TileMasker', */ 'virtual protected getMaskedTileSet': function( data_mask, callback ) { - var tdata = this._tileDfn, - tiles = {}, - i = -1, - len = tdata.length, + const tdata = this._tileDfn, + len = tdata.length, - // shorten the names - tw = this._tileWidth, - th = this._tileHeight, - xn = this._tilesPerRow; + // shorten the names + tw = this._tileWidth, + th = this._tileHeight, + xn = this._tilesPerRow; + + let tiles = {}, + i = -1; // create each tile (preserving order, thus no decrementing) while ( ++i < len ) { - var id = tdata[ i ][ 0 ], - mask = tdata[ i ][ 1 ], + const id = tdata[ i ][ 0 ], + mask = tdata[ i ][ 1 ]; - // calculate the X and Y position of this tile based on the tile - // and bitmap dimensions - x = ( ( i % xn ) * th ), - y = ( ( Math.floor( i / xn ) ) * tw ); + // calculate the X and Y position of this tile based on the tile + // and bitmap dimensions + const x = ( ( i % xn ) * th ), + y = ( ( Math.floor( i / xn ) ) * tw ); // the third index indicates whether or not a mask should be applied // to the tile @@ -297,7 +309,7 @@ module.exports = Class( 'TileMasker', */ 'protected appendTileFrame': function( set, id, mask, data ) { - var prev = set[ id ]; + const prev = set[ id ]; set[ id ] = { data: data, @@ -314,7 +326,7 @@ module.exports = Class( 'TileMasker', // if there was a previous entry, set its 'next' entry to our new frame, // expanding the linked list - prev && ( prev.next = set[ id ] ) + if ( prev ) prev.next = set[ id ]; }, @@ -347,18 +359,19 @@ module.exports = Class( 'TileMasker', */ 'virtual protected getMaskedTileData': function( data_mask, x, y ) { - var raw = this.getTileData( x, y ), - w = raw.width, - h = raw.height, - mw = data_mask.width, - yi = h; + const raw = this.getTileData( x, y ), + w = raw.width, + h = raw.height, + mw = data_mask.width; + + let yi = h; // apply the mask to the raw tile data (simple and easy-to-understand // algorithm; we can refine it later if need be), looping through each // pixel while ( yi-- ) { - xi = w; + let xi = w; while ( xi-- ) { @@ -366,8 +379,8 @@ module.exports = Class( 'TileMasker', // (remember that, although we are dealing with applying the // mask to a single tile, the mask image contains all tiles, so // we must calculate its position accordingly) - var mi = ( ( ( yi + y ) * ( mw * 4 ) ) + ( ( xi + x ) * 4 ) ), - mr = data_mask.data[ mi ]; + const mi = ( ( ( yi + y ) * ( mw * 4 ) ) + ( ( xi + x ) * 4 ) ), + mr = data_mask.data[ mi ]; // manipulate the alpha channel of our tile; if the R value for // the mask is not 0, then this pixel in our tile should be @@ -414,8 +427,8 @@ module.exports = Class( 'TileMasker', */ 'private _renderImage': function( bmp, callback ) { - var _self = this, - img = new Image(); + const _self = this, + img = this._document.createElement( 'img' ); img.onload = function() { @@ -437,7 +450,7 @@ module.exports = Class( 'TileMasker', */ 'private _getImageData': function( bmp, callback ) { - var _self = this; + const _self = this; this._renderImage( bmp, function() { diff --git a/src/gameobjs/GameObject.js b/src/gameobjs/GameObject.js index 206e80d..33917ce 100644 --- a/src/gameobjs/GameObject.js +++ b/src/gameobjs/GameObject.js @@ -17,7 +17,7 @@ * along with this program. If not, see . */ -var Class = require( 'easejs' ).Class; +const Class = require( 'easejs' ).Class; module.exports = Class( 'GameObject', @@ -44,6 +44,7 @@ module.exports = Class( 'GameObject', }, + /* jshint -W098 */ 'virtual public move': function( dir, c, sc ) { // move in the appropriate direction (action has been pre-configured @@ -52,4 +53,5 @@ module.exports = Class( 'GameObject', return this; } + /* jshint +W098 */ } ); diff --git a/src/gameobjs/Tank.js b/src/gameobjs/Tank.js index 3d239fd..073aeae 100644 --- a/src/gameobjs/Tank.js +++ b/src/gameobjs/Tank.js @@ -17,8 +17,8 @@ * along with this program. If not, see . */ -var Class = require( 'easejs' ).Class, - GameObject = require( './GameObject' ); +const Class = require( 'easejs' ).Class, + GameObject = require( './GameObject' ); module.exports = Class( 'Tank' ) @@ -26,12 +26,12 @@ module.exports = Class( 'Tank' ) { 'override public move': function( direction, c, sc ) { - var state = [ 'tleft', 'tup', 'tright', 'tdown' ][ direction ]; + const state = [ 'tleft', 'tup', 'tright', 'tdown' ][ direction ]; if ( state !== this.getTid() ) { sc( state ); - return; + return this; } // let parent handle the movement diff --git a/src/gameobjs/classic/TankDown.js b/src/gameobjs/classic/TankDown.js index f2bc0dc..efbcf4e 100644 --- a/src/gameobjs/classic/TankDown.js +++ b/src/gameobjs/classic/TankDown.js @@ -17,8 +17,8 @@ * along with this program. If not, see . */ -var Class = require( 'easejs' ).Class, - Tank = require( '../Tank' ); +const Class = require( 'easejs' ).Class, + Tank = require( '../Tank' ); module.exports = Class( 'TankDown' ) diff --git a/src/ClassicMap.js b/src/level/ClassicLevel.js similarity index 62% rename from src/ClassicMap.js rename to src/level/ClassicLevel.js index e436fb7..e44dd95 100644 --- a/src/ClassicMap.js +++ b/src/level/ClassicLevel.js @@ -1,5 +1,5 @@ /** - * Represents a classic map (level) + * Represents a classic level * * Copyright (C) 2012, 2015 Mike Gerwitz * @@ -17,7 +17,7 @@ * along with this program. If not, see . * * - * Each map is concatenated in the file and consists of the following + * Each level is concatenated in the file and consists of the following * information: * * - Playfield data (game objects), 16x16 multidimensional char array @@ -27,34 +27,34 @@ * - 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). + * 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. + * "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 level would appear to be mirrored + * rotated 90 degrees counter-clockwise. To make it easier to visualize, one + * can create a level with a number of tunnels in a pattern to take + * advantage of the ASCII display. */ -var Class = require( 'easejs' ).Class, - Map = require( './Map' ); +const Class = require( 'easejs' ).Class, + Level = require( './Level' ); /** - * Represents a classic map, as they exist in the original game. + * Represents a classic level, as they exist in the original game. * - * Classic maps are 16x16 tiles in size (for a total of 256 tiles). + * Classic levels are 16x16 tiles in size (for a total of 256 tiles). */ -module.exports = Class( 'ClassicMap' ) - .implement( Map ) +module.exports = Class( 'ClassicLevel' ) + .implement( Level ) .extend( { /** - * Size of each map in bytes + * Size of each level in bytes * @type {number} */ 'private const _SIZE': 576, @@ -70,13 +70,13 @@ module.exports = Class( 'ClassicMap' ) 'private const _GOSIZE': [ 0, 256 ], /** - * Offset and length of map name + * Offset and length of level name * @type {Array.} */ 'private const _NAMESIZE': [ 256, 31 ], /** - * Offset and length of map hint + * Offset and length of level hint * @type {Array.} */ 'private const _HINTSIZE': [ 287, 256 ], @@ -105,12 +105,13 @@ module.exports = Class( 'ClassicMap' ) /** * 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. + * 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. + * Taken from ColorList in LTANK2.C in the original sources. Note that + * there is an endianness difference. * * @type {Array.} */ @@ -121,36 +122,36 @@ module.exports = Class( 'ClassicMap' ) /** - * Map set data (binary string) + * Level set data (binary string) * @type {string} */ 'private _data': null, /** - * Map id (1-indexed) + * Level id (1-indexed) * @type {string} */ 'private _id': 0, /** - * Offset of beginning of map data in bytes + * Offset of beginning of level data in bytes * @type {number} */ 'private _offset': 0, /** - * Initialize map with map data and the given id + * Initialize level with level data and the given id * - * @param {MapSet} set map set data - * @param {number} id 1-indexed map id + * @param {LevelSet} set level set data + * @param {number} id 1-indexed level id */ __construct: function( data, id ) { this._data = ''+( data ); this._id = +id; - // calculate map offset in LVL data + // calculate level offset in LVL data this._offset = ( this.__self.$( '_SIZE' ) * ( this._id - 1 ) ); }, @@ -158,20 +159,20 @@ module.exports = Class( 'ClassicMap' ) /** * 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 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). + * 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; + const tiles = this._getDataSegment( '_GOSIZE', false ).split( '' ); + let i = tiles.length; while ( i-- ) { @@ -192,8 +193,8 @@ module.exports = Class( 'ClassicMap' ) { stripnull = ( arguments.length < 2 ) ? true : !!stripnull; - var s = this.__self.$( name ), - data = this._data.substr( ( this._offset + s[ 0 ] ), s[ 1 ] ); + const s = this.__self.$( name ), + data = this._data.substr( ( this._offset + s[ 0 ] ), s[ 1 ] ); return ( stripnull ) ? data.split( '\0' )[ 0 ] @@ -202,7 +203,7 @@ module.exports = Class( 'ClassicMap' ) /** - * Retrieve map dimensions + * Retrieve level dimensions * * @return {Array.} width and height in tiles */ @@ -213,11 +214,11 @@ module.exports = Class( 'ClassicMap' ) /** - * Retrieve map of object codes to their appropriate tiles + * Retrieve level of object codes to their appropriate tiles * * @return {Array.} */ - 'public getObjectTileMap': function() + 'public getObjectTileLevel': 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 @@ -235,9 +236,10 @@ module.exports = Class( 'ClassicMap' ) /** * 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. + * 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 * @@ -245,9 +247,9 @@ module.exports = Class( 'ClassicMap' ) */ '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 ); + // get the tunnel id by stripping off the tunnel bitmask and then + // grab the associated color + const tunnel_id = ( ( +oid ^ this.__self.$( '_TMASK' ) ) / 2 ); return this.__self.$( '_TCOLORS' )[ tunnel_id ] || 'black'; }, @@ -265,79 +267,81 @@ module.exports = Class( 'ClassicMap' ) /** - * Retrieve map name + * Retrieve level name * - * @return {string} map name + * @return {string} level name */ - 'public getMapName': function() + 'public getLevelName': function() { return this._getDataSegment( '_NAMESIZE' ); }, /** - * Retrieve map author name + * Retrieve level author name * - * @return {string} map author name + * @return {string} level author name */ - 'public getMapAuthor': function() + 'public getLevelAuthor': function() { return this._getDataSegment( '_AUTHORSIZE' ); }, /** - * Retrieve map hint + * Retrieve level hint * - * @return {string} map hint + * @return {string} level hint */ - 'public getMapHint': function() + 'public getLevelHint': function() { return this._getDataSegment( '_HINTSIZE' ); }, /** - * Retrieve map difficulty + * Retrieve level 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 level 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. + * 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() + 'public getLevelDifficulty': function() { - var val = this._getDataSegment( '_DIFFSIZE', false ), - i = val.length, - n = 0; + const val = this._getDataSegment( '_DIFFSIZE', false ); + + let 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. + // 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 + * Retrieve size of level in bytes * - * @return {number} size of map in bytes + * @return {number} size of level in bytes */ - 'public static getMapSize': function() + 'public static getLevelSize': function() { return this.$( '_SIZE' ); } diff --git a/src/Map.js b/src/level/Level.js similarity index 69% rename from src/Map.js rename to src/level/Level.js index 7e7b46e..13a646d 100644 --- a/src/Map.js +++ b/src/level/Level.js @@ -17,7 +17,7 @@ * along with this program. If not, see . * * - * The details on exactly how the map data is stored is left to specific + * The details on exactly how the level data is stored is left to specific * implementations. However, the following is common to each file format: * * - All game objects for the playfield should be returned in columns rather @@ -29,26 +29,26 @@ * tunnel identified by index 0 is 0x40, index 1 is 0x42, and so on. */ -var Interface = require( 'easejs' ).Interface; +const Interface = require( 'easejs' ).Interface; /** - * Represents a map (level) + * Represents a game level * - * Maps simply act as basic wrappers around a set of maps, returning only the - * data associated with the requested map. This allows the data to be lazily - * sliced out of the map file. + * Levels simply act as basic wrappers around a set of maps, returning only the + * data associated with the requested level. This allows the data to be lazily + * sliced out of the level file. * * Note that this interface specifies a constructor definition; this allows it * to be used in place of a separate Factory class. */ -module.exports = Interface( 'Map', +module.exports = Interface( 'Level', { /** - * Initialize map with map data and the given id + * Initialize level with level data and the given id * - * @param {MapSet} set map set data - * @param {number} id 1-indexed map id + * @param {LevelSet} set level set data + * @param {number} id 1-indexed level id */ __construct: [ 'set', 'id' ], @@ -67,7 +67,7 @@ module.exports = Interface( 'Map', /** - * Retrieve map dimensions + * Retrieve level dimensions * * @return {Array.} width and height in tiles */ @@ -75,11 +75,11 @@ module.exports = Interface( 'Map', /** - * Retrieve map of object codes to their appropriate tiles + * Retrieve level of object codes to their appropriate tiles * * @return {Array.} */ - 'public getObjectTileMap': [], + 'public getObjectTileLevel': [], /** @@ -106,44 +106,44 @@ module.exports = Interface( 'Map', /** - * Retrieve map name + * Retrieve level name * - * @return {string} map name + * @return {string} level name */ - 'public getMapName': [], + 'public getLevelName': [], /** - * Retrieve map author name + * Retrieve level author name * - * @return {string} map author name + * @return {string} level author name */ - 'public getMapAuthor': [], + 'public getLevelAuthor': [], /** - * Retrieve map hint + * Retrieve level hint * - * @return {string} map hint + * @return {string} level hint */ - 'public getMapHint': [], + 'public getLevelHint': [], /** - * Retrieve map difficulty + * Retrieve level difficulty * - * The map difficulty should be returned as a 0-indexed value between 0 and - * 4, with 0 representing "kids" and 4 representing "deadly". + * The level 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': [], + 'public getLevelDifficulty': [], /** - * Retrieve size of map in bytes + * Retrieve size of level in bytes * - * @return {number} size of map in bytes + * @return {number} size of level in bytes */ - 'public static getMapSize': [] + 'public static getLevelSize': [] } ); diff --git a/src/MapAction.js b/src/level/LevelAction.js similarity index 88% rename from src/MapAction.js rename to src/level/LevelAction.js index ed21069..c3bf1a0 100644 --- a/src/MapAction.js +++ b/src/level/LevelAction.js @@ -17,11 +17,11 @@ * along with this program. If not, see . */ -var Class = require( 'easejs' ).Class, - MapBounds = require( './MapBounds' ); +const Class = require( 'easejs' ).Class, + LevelBounds = require( './LevelBounds' ); -module.exports = Class( 'MapAction', +module.exports = Class( 'LevelAction', { // arranged by keycode 'const D__MIN': 0, @@ -43,9 +43,9 @@ module.exports = Class( 'MapAction', __construct: function( bounds, move_callback ) { - if ( !( Class.isA( MapBounds, bounds ) ) ) + if ( !( Class.isA( LevelBounds, bounds ) ) ) { - throw TypeError( 'Invalid MapBounds provided' ); + throw TypeError( 'Invalid LevelBounds provided' ); } this._dir = this.__self.$( 'D_UP' ); @@ -57,7 +57,7 @@ module.exports = Class( 'MapAction', 'public move': function() { - var method = [ + const method = [ 'getLeftPos', 'getUpperPos', 'getRightPos', diff --git a/src/MapBounds.js b/src/level/LevelBounds.js similarity index 79% rename from src/MapBounds.js rename to src/level/LevelBounds.js index ed4884e..004f937 100644 --- a/src/MapBounds.js +++ b/src/level/LevelBounds.js @@ -1,5 +1,5 @@ /** - * Handles map boundaries for collision detection and movement + * Handles level boundaries for collision detection and movement * * Copyright (C) 2012, 2015 Mike Gerwitz * @@ -17,45 +17,46 @@ * along with this program. If not, see . */ -var Class = require( 'easejs' ).Class, - Map = require( './Map' ); +const Class = require( 'easejs' ).Class, + Level = require( './Level' ); /** - * Calculates map bounding box + * Calculates level bounding box * - * This simply encapsulates the process of determining whether a given position - * is against an edge of the map. + * This simply encapsulates the process of determining whether a given + * position is against an edge of the level. */ -module.exports = Class( 'MapBounds', +module.exports = Class( 'LevelBounds', { /** - * Map width (number of tiles) + * Level width (number of tiles) * @type {number} */ 'private _mw': 0, /** - * Map height (number of tiles) + * Level height (number of tiles) * @type {number} */ 'private _mh': 0, /** - * Initialize bounding box for a given map + * Initialize bounding box for a given level * - * @param {Map} map map for which bounds should be calculated + * @param {Level} level level for which bounds should be calculated */ - __construct: function( map ) + __construct: function( level ) { - if ( !( Class.isA( Map, map ) ) ) + if ( !( Class.isA( Level, level ) ) ) { - throw TypeError( 'Invalid Map provided' ); + throw TypeError( 'Invalid Level provided' ); } - // we are only interested in the dimensions of the map - var dimen = map.getDimensions(); + // we are only interested in the dimensions of the level + const dimen = level.getDimensions(); + this._mw = dimen[ 0 ]; this._mh = dimen[ 1 ]; }, @@ -64,7 +65,7 @@ module.exports = Class( 'MapBounds', /** * Retrieve the tile position above the given position * - * If the given tile position is at the top of the map, then the given + * If the given tile position is at the top of the level, then the given * position will be returned. * * @param {number} pos original tile position @@ -82,8 +83,8 @@ module.exports = Class( 'MapBounds', /** * Retrieve the tile position below the given position * - * If the given tile position is at the bottom of the map, then the given - * position will be returned. + * If the given tile position is at the bottom of the level, then the + * given position will be returned. * * @param {number} pos original tile position * @@ -100,8 +101,8 @@ module.exports = Class( 'MapBounds', /** * Retrieve the tile position to the left of the given position * - * If the given tile position is at the leftmost column of the map, then the - * given position will be returned. + * If the given tile position is at the leftmost column of the level, + * then the given position will be returned. * * @param {number} pos original tile position * @@ -119,8 +120,8 @@ module.exports = Class( 'MapBounds', /** * Retrieve the tile position to the right of the given position * - * If the given tile position is at the rightmost column of the map, then - * the given position will be returned. + * If the given tile position is at the rightmost column of the level, + * then the given position will be returned. * * @param {number} pos original tile position * @@ -136,7 +137,7 @@ module.exports = Class( 'MapBounds', /** - * Determines if the given position is in the topmost row of the map + * Determines if the given position is in the topmost row of the level * * @param {number} pos tile position * @@ -144,14 +145,14 @@ module.exports = Class( 'MapBounds', */ 'public isAtTop': function( pos ) { - // since tile positions are zero-indexed, we know that we're at the top - // if the map height divides the position + // since tile positions are zero-indexed, we know that we're at the + // top if the level height divides the position return ( pos % this._mh === 0 ); }, /** - * Determines if the given position is in the bottom row of the map + * Determines if the given position is in the bottom row of the level * * @param {number} pos tile position * @@ -165,7 +166,8 @@ module.exports = Class( 'MapBounds', /** - * Determines if the given position is in the leftmost column of the map + * Determines if the given position is in the leftmost column of the + * level * * @param {number} pos tile position * @@ -179,7 +181,8 @@ module.exports = Class( 'MapBounds', /** - * Determines if the given position is in the rightmost column of the map + * Determines if the given position is in the rightmost column of the + * level * * @param {number} pos tile position * diff --git a/src/MapRender.js b/src/level/LevelRender.js similarity index 62% rename from src/MapRender.js rename to src/level/LevelRender.js index 8820d3f..62b66f6 100644 --- a/src/MapRender.js +++ b/src/level/LevelRender.js @@ -1,5 +1,5 @@ /** - * Renders a given map + * Renders a given level * * Copyright (C) 2012, 2015 Mike Gerwitz * @@ -17,20 +17,20 @@ * along with this program. If not, see . */ -var Class = require( 'easejs' ).Class, - MapState = require( './MapState' ); +const Class = require( 'easejs' ).Class, + LevelState = require( './LevelState' ); /** - * Renders a map to a canvas + * Renders a level to a canvas */ -module.exports = Class( 'MapRender', +module.exports = Class( 'LevelRender', { /** * Property to hold lock bit on canvas element * @type {string} */ - 'private const _LOCK': '__$$MapRenderLock$$', + 'private const _LOCK': '__$$LevelRenderLock$$', /** * Animation interval in milliseconds @@ -40,7 +40,7 @@ module.exports = Class( 'MapRender', /** - * 2d context to which map should be drawn + * 2d context to which level should be drawn * @type {CanvasRenderingContext2d} */ 'private _ctx': null, @@ -67,8 +67,8 @@ module.exports = Class( 'MapRender', /** * Initialize renderer with a canvas context and a tile set * - * An additional canvas of equal dimensions will be created and laid atop of - * the provided canvas to render masked game objects. + * An additional canvas of equal dimensions will be created and laid + * atop of the provided canvas to render masked game objects. * * @param {CanvasRenderingContext2d} ctx canvas 2d context * @param {Object} tiles tile set to render @@ -79,7 +79,7 @@ module.exports = Class( 'MapRender', this._tiles = tiles; // ensure that we are exclusively rendering to this canvas (no other - // MapRenders) + // LevelRenders) this._lockCanvas(); this._ctxObj = this._getObjCanvas(); @@ -87,22 +87,24 @@ module.exports = Class( 'MapRender', /** - * Lock the canvas to prevent other MapRender instances from rendering to it + * Lock the canvas to prevent other LevelRender instances from rendering + * to it * - * The purpose of this is to provide feedback to the user/developer in the - * event that multiple MapRender instances are attempting to render to the - * same canvas, which would certainly cause display issues and confusion. + * The purpose of this is to provide feedback to the user/developer in + * the event that multiple LevelRender instances are attempting to + * render to the same canvas, which would certainly cause display issues + * and confusion. * * @return {undefined} */ 'private _lockCanvas': function() { - var o = this._ctx, - l = this.__self.$( '_LOCK' ); + const o = this._ctx, + l = this.__self.$( '_LOCK' ); - // simple one-line check to both set the lock and fail if the lock is - // already set (implying that something else is already rendering to the - // canvas) + // simple one-line check to both set the lock and fail if the lock + // is already set (implying that something else is already rendering + // to the canvas) if ( ( o[ l ] ^= 1 ) !== 1 ) { // reset the lock @@ -110,24 +112,24 @@ module.exports = Class( 'MapRender', throw Error( 'Could not set exclusive lock on canvas (in use by another ' + - 'MapRender instance)' + 'LevelRender instance)' ); } }, /** - * Remove exclusive lock on canvas to permit other MapRender instances to - * render to it + * Remove exclusive lock on canvas to permit other LevelRender instances + * to render to it * - * This will also destroy the overlay canvas that the masked objects were - * rendered to. The remaining canvas will not be cleared. + * This will also destroy the overlay canvas that the masked objects + * were rendered to. The remaining canvas will not be cleared. * - * @return {MapRender} self + * @return {LevelRender} self */ 'public freeCanvas': function() { - var c = this._ctxObj.canvas; + const c = this._ctxObj.canvas; // clear any running animations this._clearAnimation(); @@ -148,8 +150,9 @@ module.exports = Class( 'MapRender', */ 'private _getObjCanvas': function() { - var canvas = this._ctx.canvas, - canvas_obj = document.createElement( 'canvas' ); + const canvas = this._ctx.canvas, + document = this._getDocument( canvas ), + canvas_obj = document.createElement( 'canvas' ); // mimic the dimensions and positions of the original canvas canvas_obj.style.position = 'absolute'; @@ -166,46 +169,63 @@ module.exports = Class( 'MapRender', /** - * Render the provided map + * Get HTMLDocument node from element ELEMENT * - * @param {Map} map map to render + * ELEMENT must be on a DOM. This allows us to always reference the + * proper document node without being coupled with the browser's + * window.document, which may not be what we're interested in. * - * @return {MapRender} self + * @param {HTMLElement} element reference element + * + * @return {HTMLDocument} document node */ - 'public render': function( map, map_state ) + 'private _getDocument': function( element ) { - if ( !( Class.isA( MapState, map_state ) ) ) + return ( element.parentElement === null ) + ? element.parentNode + : this._getDocument( element.parentElement ); + }, + + + /** + * Render the provided level + * + * @param {Level} level level to render + * + * @return {LevelRender} self + */ + 'public render': function( level, level_state ) + { + if ( !( Class.isA( LevelState, level_state ) ) ) { - throw TypeError( 'Invalid MapState provided' ); + throw TypeError( 'Invalid LevelState provided' ); } - var objs = map.getObjects(), - size = map.getDimensions(), - omap = map.getObjectTileMap(), - sizex = size[ 0 ], - sizey = size[ 1 ], - i = objs.length, + const objs = level.getObjects(), + size = level.getDimensions(), + sizex = size[ 0 ], + sizey = size[ 1 ], - // tiles to animate - anim = [], + // tiles to animate + anim = [], - // get the width and height from one of the tiles - t = this._tiles.dirt.data, - w = t.width, - h = t.height; + // get the width and height from one of the tiles + t = this._tiles.dirt.data, + w = t.width, + h = t.height; this._clearCanvases(); - var _self = this; - map_state.onChange( function( obj, pos ) + const _self = this; + level_state.onChange( function( obj, pos ) { - var oid = objs[ pos ], - tid = ( obj ) ? obj.getTid() : 'dirt', - tile = ( _self._tiles[ tid ] || {} ).first, + const oid = objs[ pos ], + tid = ( obj ) ? obj.getTid() : 'dirt', + tile = ( _self._tiles[ tid ] || {} ).first, - v = _self._getTileVector( pos, sizex, sizey, w, h ), - x = v[ 0 ], - y = v[ 1 ]; + v = _self._getTileVector( pos, sizex, sizey, w, h ), + x = v[ 0 ], + y = v[ 1 ]; if ( obj === null ) { @@ -214,9 +234,9 @@ module.exports = Class( 'MapRender', } // tunnels are handled a bit differently than other objects - if ( map.isObjectTunnel( oid ) ) + if ( level.isObjectTunnel( oid ) ) { - _self._renderTunnel( x, y, map.getTunnelColor( oid ) ); + _self._renderTunnel( x, y, level.getTunnelColor( oid ) ); return; } @@ -229,10 +249,10 @@ module.exports = Class( 'MapRender', _self._drawTile( tile, x, y ); } ); - map_state.flush(); + level_state.flush(); - // render each object (remember, we're dealing with columns, not rows; - // see Map.getObjects()) + // render each object (remember, we're dealing with columns, not + // rows; see Level.getObjects()) this._beginAnimation( anim ); @@ -241,12 +261,12 @@ module.exports = Class( 'MapRender', /** - * Retrieve a vector representing the x and y position coordinates of a tile - * position + * Retrieve a vector representing the x and y position coordinates of a + * tile position * * @param {number} pos tile position - * @param {number} sizex number of horizontal tiles in map - * @param {number} sizey number of vertical tiles in map + * @param {number} sizex number of horizontal tiles in level + * @param {number} sizey number of vertical tiles in level * @param {number} w tile width * @param {number} h tile height * @@ -264,26 +284,27 @@ module.exports = Class( 'MapRender', /** * Clears overlay canvas * - * This should be used before first rendering a map to ensure that any - * artifacts from previous map renderings will be erased. + * This should be used before first rendering a level to ensure that any + * artifacts from previous level renderings will be erased. * * We need only clear the overlay canvas, because the lower canvas will - * always be overwritten with tiles in every location. Because none of the - * tiles written to the lower canvas are masked, nothing from the previous - * render would ever peek through (of course, putImageData() would overwrite - * it even if that were the case). As such, clearing the lower canvas would - * simply be a waste of time and only serve to degrade performance - * (especially if this is being used with maps larger than the classic - * 16x16). + * always be overwritten with tiles in every location. Because none of + * the tiles written to the lower canvas are masked, nothing from the + * previous render would ever peek through (of course, putImageData() + * would overwrite it even if that were the case). As such, clearing the + * lower canvas would simply be a waste of time and only serve to + * degrade performance (especially if this is being used with levels + * larger than the classic 16x16). * * @return {undefined} */ 'private _clearCanvases': function() { - var ctx = this._ctxObj, - c = ctx.canvas; + const ctx = this._ctxObj, + c = ctx.canvas; - // we need only clear the overlay (to which masked tiles are rendered) + // we need only clear the overlay (to which masked tiles are + // rendered) ctx.clearRect( 0, 0, c.width, c.height ); }, @@ -297,7 +318,7 @@ module.exports = Class( 'MapRender', */ 'private _canAnimate': function( tid ) { - var tdata = this._tiles[ tid ]; + const tdata = this._tiles[ tid ]; return ( tdata.next !== tdata ); }, @@ -305,9 +326,10 @@ module.exports = Class( 'MapRender', /** * Draw the tile identified by the given id * - * The tile will be drawn to the appropriate canvas depending on whether or - * not it has been masked. If it does have a mask, it will be drawn to the - * overlaying canvas and the dirt tile will be drawn underneath it. + * The tile will be drawn to the appropriate canvas depending on whether + * or not it has been masked. If it does have a mask, it will be drawn + * to the overlaying canvas and the dirt tile will be drawn underneath + * it. * * @param {string} tid tile id * @param {number} x left position @@ -317,7 +339,7 @@ module.exports = Class( 'MapRender', */ 'private _drawTile': function( tile, x, y ) { - var ctx = ( tile.masked ) ? this._ctxObj : this._ctx; + const ctx = ( tile.masked ) ? this._ctxObj : this._ctx; ctx.putImageData( tile.data, x, y ); @@ -338,8 +360,8 @@ module.exports = Class( 'MapRender', * Render the given tunnel * * The tunnel background color (which will peek through the mask) is - * rendered to the base canvas, whereas the tunnel tile itself is rendered - * on the overlaying canvas. + * rendered to the base canvas, whereas the tunnel tile itself is + * rendered on the overlaying canvas. * * @param {number} x left position * @param {number} y top position @@ -349,7 +371,7 @@ module.exports = Class( 'MapRender', */ 'private _renderTunnel': function( x, y, color ) { - var tdata = this._tiles.tunnel.data; + const tdata = this._tiles.tunnel.data; // fill tile with the appropriate background color for this tile this._ctx.fillStyle = color; @@ -363,8 +385,8 @@ module.exports = Class( 'MapRender', /** * Begin basic tile animation * - * At each animation interval, each tile will be advanced a single frame and - * rendered atop of the previous. + * At each animation interval, each tile will be advanced a single frame + * and rendered atop of the previous. * * @param {Array.>} anim array of tiles to * animate; tdata,x,y @@ -373,32 +395,32 @@ module.exports = Class( 'MapRender', */ 'private _beginAnimation': function( anim ) { - var _self = this; + const _self = this; // clear any existing rendering animations this._clearAnimation(); - return this._animTimer = setInterval( function() + return ( this._animTimer = setInterval( function() { - var i = anim.length; + let i = anim.length; while ( i-- ) { - var cur = anim[ i ]; + const cur = anim[ i ]; // draw next frame cur[ 0 ] = cur[ 0 ].next; _self._drawTile.apply( _self, anim[ i ] ); } - }, this.__self.$( '_ANIM_INTERVAL' ) ); + }, this.__self.$( '_ANIM_INTERVAL' ) ) ); }, /** * Clear any running animation timers * - * It is important that this be done when a MapRender instance is done being - * used, or it will remain in memory indefinitely! + * It is important that this be done when a LevelRender instance is done + * being used, or it will remain in memory indefinitely! * * @return {undefined} */ diff --git a/src/MapSet.js b/src/level/LevelSet.js similarity index 53% rename from src/MapSet.js rename to src/level/LevelSet.js index 8f3a2c5..3967b88 100644 --- a/src/MapSet.js +++ b/src/level/LevelSet.js @@ -17,22 +17,20 @@ * along with this program. If not, see . * * - * Maps (the term "level" is used in the game) are stored in a fixed-width LVL - * file. + * Levels are stored in a fixed-width LVL file. */ -var Class = require( 'easejs' ).Class; - +const Class = require( 'easejs' ).Class; /** * Handles delegation of LVL data * - * This acts as a Map factory, leaving all processing responsibilities to the - * Map instance. Consequently, any custom map format would be supported, - * provided that the appropriate Map is handling it. + * This acts as a Level factory, leaving all processing responsibilities to + * the Level instance. Consequently, any custom level format would be + * supported, provided that the appropriate Level is handling it. */ -module.exports = Class( 'MapSet', +module.exports = Class( 'LevelSet', { /** * Raw level set data (binary) @@ -41,61 +39,62 @@ module.exports = Class( 'MapSet', 'private _data': '', /** - * Constructor used to create new map instances + * Constructor used to create new level instances * @type {Function} */ - 'private _mapCtor': null, + 'private _levelCtor': null, /** - * Number of maps in the given LVL data + * Number of levels in the given LVL data * @type {number} */ - 'private _mapCount': 0, + 'private _levelCount': 0, /** - * Initialize map set with LVL data and a Map constructor + * Initialize level set with LVL data and a Level constructor * - * The Map constructor is used in place of a separate Map factory. + * The Level constructor is used in place of a separate Level factory. * * @param {string} data binary LVL data - * @param {Map} map_ctor Map constructor + * @param {Level} level_ctor Level constructor */ - __construct: function( data, map_ctor ) + __construct: function( data, level_ctor ) { this._data = ''+( data ); - this._mapCtor = map_ctor; + this._levelCtor = level_ctor; // perform a simple integrity check on the provided data - this._mapDataCheck(); + this._levelDataCheck(); }, /** - * Perform a simple map data integrity check + * Perform a simple level data integrity check * * This is intended to throw an error if we believe the LVL data to be - * invalid, or if the LVL data is invalid for the given Map constructor. - * The check will simply ensure that the map size (in bytes) divides into - * the total LVL size (in bytes) without any remainder. + * invalid, or if the LVL data is invalid for the given Level + * constructor. The check will simply ensure that the level size (in + * bytes) divides into the total LVL size (in bytes) without any + * remainder. * * This is by no means fool-proof, but it should catch most. * * @return {undefined} */ - 'private _mapDataCheck': function() + 'private _levelDataCheck': function() { - var n = ( this._data.length / this._mapCtor.getMapSize() ); + const n = ( this._data.length / this._levelCtor.getLevelSize() ); - // if the result is not an integer, then it is either not an LVL, the - // file is corrupt, or we were given the wrong Map constructor + // if the result is not an integer, then it is either not an LVL, + // the file is corrupt, or we were given the wrong Level constructor if ( n % 1 ) { throw Error( 'Invalid or corrupt LVL data' ); } // we already calculated it, so we may as well store it - this._mapCount = n; + this._levelCount = n; }, @@ -104,19 +103,19 @@ module.exports = Class( 'MapSet', * * @param {number} id number of level to load, 1-indexed */ - 'public getMapByNumber': function( id ) + 'public getLevelByNumber': function( id ) { - return this._mapCtor( this._data, id ); + return this._levelCtor( this._data, id ); }, /** - * Retrieve the number of maps in the LVL file + * Retrieve the number of levels in the LVL file * - * @return {number} number of maps + * @return {number} number of levels */ - 'public getMapCount': function() + 'public getLevelCount': function() { - return this._mapCount; + return this._levelCount; }, } ); diff --git a/src/MapState.js b/src/level/LevelState.js similarity index 69% rename from src/MapState.js rename to src/level/LevelState.js index bd4f97a..3216254 100644 --- a/src/MapState.js +++ b/src/level/LevelState.js @@ -1,5 +1,5 @@ /** - * Represents the current state of a map + * Represents the current state of a level * * Copyright (C) 2012, 2015 Mike Gerwitz * @@ -17,16 +17,16 @@ * along with this program. If not, see . */ -var Class = require( 'easejs' ).Class, - MapAction = require( './MapAction' ), - GameObjectFactory = require( './GameObjectFactory' ), - GameObject = require( './gameobjs/GameObject' ); +const Class = require( 'easejs' ).Class, + LevelAction = require( './LevelAction' ), + GameObjectFactory = require( '../GameObjectFactory' ), + GameObject = require( '../gameobjs/GameObject' ); /** - * Represents the current state of a map + * Represents the current state of a level */ -module.exports = Class( 'MapState', +module.exports = Class( 'LevelState', { /** * Game object factory @@ -47,7 +47,7 @@ module.exports = Class( 'MapState', 'private _player': null, /** - * Game objects representing every object on the map + * Game objects representing every object on the level * @type {GameObject} */ 'private _objs': [], @@ -60,39 +60,41 @@ module.exports = Class( 'MapState', /** - * Initializes map state with a given map and factory with which to create - * game objects + * Initializes level state with a given level 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. + * Game objects influence level state rules, therefore fundamental game + * logic may be altered simply by passing in a custom GameObjectFactory + * instance. * - * @param {Map} map game map + * @param {Level} level game level * @param {GameObjectFactory} obj_factory game object factory */ - __construct: function( map, obj_factory ) + __construct: function( level, obj_factory ) { if ( !( Class.isA( GameObjectFactory, obj_factory ) ) ) { throw TypeError( 'Invalid GameObjectFactory provided' ); } - // initialize game objects based on the initial map state + // initialize game objects based on the initial level state this._objFactory = obj_factory; - this._initObjects( map.getObjects(), map.getObjectTileMap() ); + this._initObjects( level.getObjects(), level.getObjectTileLevel() ); }, /** - * Flush map state, triggering the change event for each game object + * Flush level 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). + * This may be used to perform an initial rendering or re-draw of the + * entire level. Note that this should not be used to re-render the + * level after movements or state changes, as that would not be + * performant (especially since the level size could be arbitrarily + * large). */ 'public flush': function() { - var _self = this; + const _self = this; // emit the change event for each game object this._forEachObj( function( obj, pos ) @@ -105,20 +107,20 @@ module.exports = Class( 'MapState', /** * 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). + * 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. + * 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 + * @return {LevelState} self */ 'public onChange': function( callback ) { @@ -130,11 +132,11 @@ module.exports = Class( 'MapState', /** * 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. + * 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 @@ -143,8 +145,8 @@ module.exports = Class( 'MapState', */ 'private _emitChange': function( obj, pos ) { - var i = -1, - l = this._stateCallbacks.length; + const l = this._stateCallbacks.length; + let i = -1; while ( ++i < l ) { @@ -153,7 +155,7 @@ module.exports = Class( 'MapState', if ( obj === null ) { - var _self = this; + const _self = this; this._objs[ pos ].forEach( function( o ) { if ( o === null ) return; @@ -165,25 +167,25 @@ module.exports = Class( 'MapState', /** - * Initialize game objects for the map's original (default) state + * Initialize game objects for the level'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. + * All necessary game objects will be created to represent all objects + * on the level at its default state. Effectively, this creates the + * default level state. * * @param {Array.} objs game object data (object ids) - * @param {Array.} objmap map from object ids to their tile ids + * @param {Array.} objlevel level from object ids to their tile ids * * @return {undefined} */ - 'private _initObjects': function( objs, objmap ) + 'private _initObjects': function( objs, objlevel ) { - var i = objs.length; + let i = objs.length; while ( i-- ) { - var val = objs[ i ], - obj = this._createObj( objmap[ val ] ); + const val = objs[ i ], + obj = this._createObj( objlevel[ val ] ); this._objs[ i ] = []; this._addObj( obj, i ); @@ -198,14 +200,14 @@ module.exports = Class( 'MapState', /** - * Creates a game object from a given tile id and (optionally) a previous - * object + * 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. + * 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 @@ -214,9 +216,10 @@ module.exports = Class( 'MapState', */ 'private _createObj': function( tid, from ) { - var obj = this._objFactory.createObject( tid ); + const obj = this._objFactory.createObject( tid ); - // if a previous object was provided, copy over its mutable attributes + // if a previous object was provided, copy over its mutable + // attributes if ( from ) { from.cloneTo( obj ); @@ -227,7 +230,7 @@ module.exports = Class( 'MapState', /** - * Invokes a continuation for each game object on the map + * Invokes a continuation for each game object on the level * * The continuation will be called with the game object and its tile * position. @@ -251,7 +254,8 @@ module.exports = Class( 'MapState', /** * Add a game object at the given tile position * - * It is an error to provide an invalid tile position or non-game object. + * 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 @@ -273,22 +277,22 @@ module.exports = Class( 'MapState', * 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 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). + * 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. + * 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 @@ -301,8 +305,7 @@ module.exports = Class( 'MapState', */ 'private _replaceObj': function( cur, newobj, pos ) { - var o = this._objs[ pos ], - i = o.length; + const o = this._objs[ pos ]; // type checks if ( !( Array.isArray( o ) ) ) @@ -316,29 +319,27 @@ module.exports = Class( 'MapState', throw TypeError( "Invalid GameObject or null provided: " + newobj ); } - var free = null; + let i = o.length, + free = null; - ( function() + while ( i-- ) { - while ( i-- ) + if ( o[ i ] === cur ) { - if ( o[ i ] === cur ) - { - o[ i ] = newobj; - return; - } - else if ( o[ i ] === null ) - { - // record this as a free position for additions - free = i; - } + 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 ); - } )(); + // 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 ); @@ -369,8 +370,8 @@ module.exports = Class( 'MapState', /** * 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. + * 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. @@ -395,8 +396,8 @@ module.exports = Class( 'MapState', * 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". + * 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 @@ -411,11 +412,12 @@ module.exports = Class( 'MapState', /** * 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. + * 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 @@ -435,7 +437,7 @@ module.exports = Class( 'MapState', } // replace game object with a new one - var newobj = this._createObj( state, cur ); + const newobj = this._createObj( state, cur ); this._replaceObj( cur, newobj, pos ); return newobj; @@ -448,8 +450,9 @@ module.exports = Class( 'MapState', * 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. + * 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 @@ -460,11 +463,11 @@ module.exports = Class( 'MapState', */ 'private _createStateCallback': function( cur, pos, c ) { - var _self = this; + const _self = this; return function( state ) { - var newobj = _self._changeState( cur, state, pos ); + const newobj = _self._changeState( cur, state, pos ); if ( typeof c === 'function' ) { @@ -478,11 +481,11 @@ module.exports = Class( 'MapState', * 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). + * given game object. The desired direction of movement should be passed + * to the continuation (see LevelAction 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. + * 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 @@ -493,7 +496,7 @@ module.exports = Class( 'MapState', */ 'private _createMoveCallback': function( obj, pos, c ) { - var _self = this; + const _self = this; return function( dest ) { @@ -511,23 +514,23 @@ module.exports = Class( 'MapState', /** * Move a player in the given direction * - * The directions, as defined in MapAction, are: 0:left, 1:up, 2:right, - * 3:down. + * The directions, as defined in LevelAction, 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 + * @param {LevelBounds} bounds level boundaries * * @return {undefined} */ 'public movePlayer': function( direction, bounds ) { - var _self = this, - player = this._player; + const _self = this, + player = this._player; // XXX: tightly coupled - var action = MapAction( + const action = LevelAction( bounds, this._createMoveCallback( player, this._playerPos, function( pos ) { @@ -535,10 +538,14 @@ module.exports = Class( 'MapState', } ) ); - var sc = this._createStateCallback( player, this._playerPos, function( o ) - { - _self._player = o; - } ); + const sc = this._createStateCallback( + player, + this._playerPos, + function( o ) + { + _self._player = o; + } + ); action.direction = direction; action.srcPos = this._playerPos; diff --git a/src/ui/MenuBar.js b/src/ui/MenuBar.js index d7988e2..dd44473 100644 --- a/src/ui/MenuBar.js +++ b/src/ui/MenuBar.js @@ -17,7 +17,7 @@ * along with this program. If not, see . */ -var Class = require( 'easejs' ).Class; +const Class = require( 'easejs' ).Class; /** @@ -68,16 +68,15 @@ module.exports = Class( 'MenuBar', */ 'private _initMenuActivation': function() { - var _self = this, - id = this._bar.id, - menus = this._bar.parentNode.querySelectorAll( '#'+id+' > li > a' ), - i = menus.length, + const id = this._bar.id, + menus = this._bar.parentNode.querySelectorAll( '#'+id+' > li > a' ), + click = function( event ) + { + event.target.parentNode.parentNode.className += ' focus'; + return false; + }; - click = function( event ) - { - event.target.parentNode.parentNode.className += ' focus'; - return false; - }; + let i = menus.length; // on menu click, apply focus class (this allows the menu to be opened // properly on click rather than a simple CSS hover menu) @@ -103,8 +102,8 @@ module.exports = Class( 'MenuBar', */ 'private _hookMenuMouseOut': function() { - var _self = this, - bar = this._bar; + const _self = this, + bar = this._bar; this._bar.addEventListener( 'mouseout', function( event ) { diff --git a/src/version.js.in b/src/version.js.in index 94df6af..db60588 100644 --- a/src/version.js.in +++ b/src/version.js.in @@ -19,7 +19,7 @@ * this program. If not, see . */ -var major = @MAJOR@, +let major = @MAJOR@, minor = @MINOR@, rev = @REV@, suffix = '@SUFFIX@', diff --git a/test/ltgloader-demo.html b/test/ltgloader-demo.html index a964cb3..0808e71 100644 --- a/test/ltgloader-demo.html +++ b/test/ltgloader-demo.html @@ -73,14 +73,14 @@ Your browser does not support the canvas element. - +