/** * Collision map is a 2D tilemap specifically designed for collision. * All the `Entity` instances will trace against this map during update. * * @class */ class CollisionMap { /** * @constructor * @param {Number} tilesize Tile size in pixel * @param {Array} data A 2D array representing the map. */ constructor(tilesize, data) { if (!Number.isFinite(tilesize) || tilesize <= 0) { console.log('Invalid tilesize!'); return; } if (!Array.isArray(data) || (data.length === 0) || !Array.isArray(data[0])) { console.log('Invalid data format!'); return; } /** * Size of tiles in pixel * @type {Number} */ this.tilesize = tilesize; /** * Map data as a 2D array * @type {Array} */ this.data = data; this._width = data[0].length; this._height = data.length; } /** * Width of this map, in tile * @readonly */ get width() { return this._width; } /** * Height of this map, in tile * @readonly */ get height() { return this._height; } /** * Get the tile index with its row and column number * @param {Number} r Row of this tile * @param {Number} q Column of this tile * @return {Number} Tile index */ getTile(r, q) { return (q >= 0 && q < this._width && r >= 0 && r < this._height) ? this.data[r][q] : 0; } /** * Get the tile index at a specific position(in pixel) * @param {Number} x X position * @param {Number} y Y position * @return {Number} Tile index */ getTileAt(x, y) { const q = Math.floor(x / this.tilesize); const r = Math.floor(y / this.tilesize); return (q >= 0 && q < this._width && r >= 0 && r < this._height) ? this.data[r][q] : 0; } /** * Set the tile at the giving row and column * @param {Number} r Row of this tile * @param {Number} q Column of this tile * @param {Number} tile New tile index */ setTile(r, q, tile) { if (q >= 0 && q < this._width && r >= 0 && r < this._height) { this.data[r][q] = tile; } } /** * Set the tile at a specific position * @param {Number} x X position * @param {Number} y Y position * @param {Number} tile New tile index */ setTileAt(x, y, tile) { const q = Math.floor(x / this.tilesize); const r = Math.floor(y / this.tilesize); if (q >= 0 && q < this._width && r >= 0 && r < this._height) { this.data[r][q] = tile; } } /** * Trace a collider against this map. * @param {Collider} coll Collider object. * @param {Number} sx Movement on x-axis. * @param {Number} sy Movement on y-axis. * @param {Object} res Resolved movement result. */ trace(coll, sx, sy, res) { // TODO: fast movement if (sx === 0 && sy === 0) { return; } // Set result as full movement res.x = sx; res.y = sy; res.hitX = res.hitY = false; let posi, leading, dir, start, end, tilespace, tilespaceEnd, done; let edgeVector, edge, tile; let i, j; // Check x-axis posi = sx > 0; leading = posi ? coll.right : coll.left; dir = posi ? 1 : -1; start = Math.floor(coll.top / this.tilesize); end = Math.ceil(coll.bottom / this.tilesize); tilespace = Math.floor(leading / this.tilesize); tilespaceEnd = Math.floor((leading + sx) / this.tilesize) + dir; done = false; for (i = tilespace; !done && i !== tilespaceEnd; i += dir) { // Out of map area if (i < 0 || i >= this._width) { continue; } for (j = start; j !== end; ++j) { // Out of map area if (j < 0 || j >= this._height) { continue; } tile = this.data[j][i]; // Out of map area if (tile === undefined) { continue; } edge = ((dir > 0) ? i : (i + 1)) * this.tilesize; edgeVector = edge - leading; // if (oncollision(axis, tile, coords, dir, edgeVector)) { if (tile === 1) { res.x = edgeVector; res.hitX = true; done = true; break; } } } // Check y-axis posi = sy > 0; leading = posi ? coll.bottom : coll.top; dir = posi ? 1 : -1; start = Math.floor(coll.left / this.tilesize); end = Math.ceil(coll.right / this.tilesize); tilespace = Math.floor(leading / this.tilesize); tilespaceEnd = Math.floor((leading + sy) / this.tilesize) + dir; done = false; for (i = tilespace; !done && i !== tilespaceEnd; i += dir) { // Out of map area if (i < 0 || i >= this._height) { continue; } for (j = start; j !== end; ++j) { // Out of map area if (j < 0 || j >= this._width) { continue; } tile = this.data[i][j]; // Out of map area if (tile === undefined) { continue; } edge = ((dir > 0) ? i : (i + 1)) * this.tilesize; edgeVector = edge - leading; // if (oncollision(axis, tile, coords, dir, edgeVector)) { if (tile === 1) { res.y = edgeVector; res.hitY = true; done = true; break; } } } } } /** * CollisionMap factory * @param {Number} tilesize Tile size in pixel. * @param {Array} data Map data as a 2D array. * @return {CollisionMap} CollisionMap instance. */ module.exports = function(tilesize = 16, data = [[]]) { return new CollisionMap(tilesize, data); };