import AABB from '../AABB.js';
import CollisionShape from '../CollisionShape.js';
import Data from '../Data.js';
import {arrayCache} from '../utils/array.js';
import createComponentClass from '../factory.js';
export default (function () {
const
maskJumpThrough = 0x10000000,
maskRotation = 0x20000000,
maskXFlip = 0x80000000,
maskYFlip = 0x40000000,
maskIndex = 0x0fffffff,
getDefaultType = function () {
return this.collisionType;
},
getCollisionType = function (index) {
return this.collisionTypeMap[index & maskIndex] || this.collisionType;
},
flipDiagonal = function (num) {
if (num === 0) {
return num;
} else {
return num ^ maskYFlip ^ maskRotation;
}
},
flipDiagonalInverse = function (num) {
if (num === 0) {
return num;
} else {
return num ^ maskXFlip ^ maskRotation;
}
},
flipX = function (num) {
if (num === 0) {
return num;
} else {
return num ^ maskXFlip;
}
},
flipY = function (num) {
if (num === 0) {
return num;
} else {
return num ^ maskYFlip;
}
},
rotate90 = function (num) {
if (num === 0) {
return num;
} else if (maskRotation & num) {
return num ^ maskYFlip ^ maskRotation;
} else {
return num ^ maskXFlip ^ maskRotation;
}
},
rotate180 = function (num) {
if (num === 0) {
return num;
} else {
return num ^ maskXFlip ^ maskYFlip;
}
},
rotate270 = function (num) {
if (num === 0) {
return num;
} else if (maskRotation & num) {
return num ^ maskXFlip ^ maskRotation;
} else {
return num ^ maskYFlip ^ maskRotation;
}
},
copySection = function (array, originX, originY, width, height) {
const
arr = arrayCache.setUp();
for (let y = 0; y < height; y++) {
arr[y] = arrayCache.setUp();
for (let x = 0; x < width; x++) {
arr[y][x] = array[originX + x][originY + y];
}
}
return arr;
},
cutSection = function (array, originX, originY, width, height) {
const
arr = arrayCache.setUp();
for (let y = 0; y < height; y++) {
arr[y] = arrayCache.setUp();
for (let x = 0; x < width; x++) {
arr[y][x] = array[originX + x][originY + y];
array[originX + x][originY + y] = -1;
}
}
return arr;
},
pasteSection = function (destinationArray, sourceArray, originX, originY, width, height) {
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
destinationArray[originX + x][originY + y] = sourceArray[y][x];
}
}
return destinationArray;
},
transforms = {
"diagonal": function (array, originX, originY, width, height) {
const
arr = copySection(array, originX, originY, width, height),
fD = flipDiagonal;
for (let x = 0; x < width; x++) {
for (let y = 0; y < height; y++) {
array[originX + x][originY + y] = fD(arr[x][y]);
}
}
arrayCache.recycle(arr, 2);
return array;
},
"diagonal-inverse": function (array, originX, originY, width, height) {
const
arr = copySection(array, originX, originY, width, height),
fDI = flipDiagonalInverse;
for (let x = 0; x < width; x++) {
for (let y = 0; y < height; y++) {
array[originX + width - x - 1][originY + height - y - 1] = fDI(arr[x][y]);
}
}
arrayCache.recycle(arr, 2);
return array;
},
"horizontal": function (array, originX, originY, width, height) {
const
arr = copySection(array, originX, originY, width, height),
fX = flipX;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
array[originX + width - x - 1][originY + y] = fX(arr[y][x]);
}
}
arrayCache.recycle(arr, 2);
return array;
},
"vertical": function (array, originX, originY, width, height) {
const
arr = copySection(array, originX, originY, width, height),
fY = flipY;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
array[originX + x][originY + height - y - 1] = fY(arr[y][x]);
}
}
arrayCache.recycle(arr, 2);
return array;
},
"rotate-90": function (array, originX, originY, width, height) {
const
arr = copySection(array, originX, originY, width, height),
r90 = rotate90;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
array[originX + height - y - 1][originY + x] = r90(arr[y][x]);
}
}
arrayCache.recycle(arr, 2);
return array;
},
"rotate-180": function (array, originX, originY, width, height) {
const
arr = copySection(array, originX, originY, width, height),
r180 = rotate180;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
array[originX + width - x - 1][originY + height - y - 1] = r180(arr[y][x]);
}
}
arrayCache.recycle(arr, 2);
return array;
},
"rotate-270": function (array, originX, originY, width, height) {
const
arr = copySection(array, originX, originY, width, height),
r270 = rotate270;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
array[originX + y][originY + width - x - 1] = r270(arr[y][x]);
}
}
arrayCache.recycle(arr, 2);
return array;
},
"translate": function (array, originX, originY, width, height, dx, dy) {
const
arr = cutSection(array, originX, originY, width, height);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
array[originX + x + dx][originY + y + dy] = arr[y][x];
}
}
arrayCache.recycle(arr, 2);
return array;
}
};
return createComponentClass(/** @lends platypus.components.CollisionTiles.prototype */{
id: 'CollisionTiles',
properties: {
/**
* Maps tile indexes to particular collision types. This defaults to a "tiles" collision type for all non-zero values if a particular collision map is not provided.
*
* @property collisionTypeMap
* @type Object
* @default null
*/
collisionTypeMap: null,
/**
* Sets the default collision type for non-zero map tiles.
*
* @property collisionType
* @type String
* @default "tiles"
*/
collisionType: 'tiles',
/**
* The map's top offset.
*
* @property top
* @type Number
* @default 0
*/
top: 0,
/**
* The map's left offset.
*
* @property left
* @type Number
* @default 0
*/
left: 0
},
publicProperties: {
/**
* A 3D array describing the layers of the tile-map with off (0) and on (!0) states. The indexes match Tiled map data indexes with an additional bit setting (0x2000000) for jumpthrough tiles. Example: `[[[0, 0, 0], [1, 0, 0], [1, 1, 1]], [[0, 1, 0], [1, 0, 0], [1, 0, 0]]]`. Available on the entity as `entity.collisionMap`.
*
* @property collisionMap
* @type Array
* @default []
*/
collisionMap: [],
/**
* The width of tiles in world coordinates. Available on the entity as `entity.tileWidth`.
*
* @property tileWidth
* @type number
* @default 10
*/
tileWidth: 10,
/**
* The height of tiles in world coordinates. Available on the entity as `entity.tileHeight`.
*
* @property tileWidth
* @type number
* @default 10
*/
tileHeight: 10
},
/**
* This component causes the tile-map to collide with other entities. It must be part of a collision group and will cause "hit-by-tile" messages to fire on colliding entities.
*
* @memberof platypus.components
* @uses platypus.Component
* @constructs
* @listens platypus.Entity#transform
* @listens platypus.Entity#translate
*/
initialize: function () {
this.tileOffsetLeft = this.tileWidth / 2 + this.left;
this.tileOffsetTop = this.tileHeight / 2 + this.top;
this.columns = this.collisionMap[0].length;
this.rows = this.collisionMap[0][0].length;
this.shapeDefinition = Data.setUp(
"x", 0,
"y", 0,
"type", 'rectangle',
"width", this.tileWidth,
"height", this.tileHeight
);
this.storedTiles = arrayCache.setUp();
this.serveTiles = arrayCache.setUp();
this.storedTileIndex = 0;
this.aabb = AABB.setUp();
this.aabb.setBounds(this.left, this.top, this.tileWidth * this.columns + this.left, this.tileHeight * this.rows + this.top);
if (this.collisionTypeMap) {
this.getType = getCollisionType;
} else {
this.getType = getDefaultType;
}
},
events: {
"transform": function (transform) {
this.transform(transform);
},
"translate": function (translate) {
this.translate(translate);
},
/**
* This event adds a layer of tiles to the collision map.
*
* @event platypus.Entity#add-collision-tiles
* @param message.imageMap {Array} This is a 2D mapping of tile indexes to be added to the collision map.
*/
"add-collision-tiles": function (definition) {
const map = definition.collisionMap[0],
typeMap = definition.collisionTypeMap;
let tileIndex = 0;
if (map) {
this.collisionMap.push(map);
}
if (typeMap) {
for (tileIndex in typeMap) {
if (this.collisionTypeMap[tileIndex]) {
console.warn('Overwriting existing typeMap.');
}
this.collisionTypeMap[tileIndex] = typeMap[tileIndex];
}
}
}
},
methods: {
getShape: function (x, y, type) {
const
{storedTileIndex: i, storedTiles} = this;
let shape = null;
if (i === storedTiles.length) {
shape = CollisionShape.setUp(null, this.shapeDefinition, type);
storedTiles.push(shape);
} else {
shape = storedTiles[i];
shape.collisionType = type;
}
shape.update(x * this.tileWidth + this.tileOffsetLeft, y * this.tileHeight + this.tileOffsetTop);
this.storedTileIndex += 1;
return shape;
},
addShape: function (shapes, prevAABB, layer, x, y, collisionType) {
const
xy = this.collisionMap[layer][x][y],
index = xy & maskIndex;
if (xy && (this.getType(index) === collisionType)) {
const
jumpThrough = maskJumpThrough & xy;
if (jumpThrough) {
const
rotation = maskRotation & xy,
xFlip = maskXFlip & xy,
yFlip = maskYFlip & xy;
if (rotation && xFlip) { // Right
if (prevAABB.left >= (x + 1) * this.tileWidth + this.left) {
shapes.push(this.getShape(x, y, collisionType));
}
} else if (rotation) { // Left
if (prevAABB.right <= x * this.tileWidth + this.left) {
shapes.push(this.getShape(x, y, collisionType));
}
} else if (yFlip) { // Bottom
if (prevAABB.top >= (y + 1) * this.tileHeight + this.top) {
shapes.push(this.getShape(x, y, collisionType));
}
} else if (prevAABB.bottom <= y * this.tileHeight + this.top) { // Top
shapes.push(this.getShape(x, y, collisionType));
}
} else {
shapes.push(this.getShape(x, y, collisionType));
}
}
return shapes;
},
destroy: function () {
const
store = this.storedTiles;
let i = store.length;
this.shapeDefinition.recycle();
this.shapeDefinition = null;
while (i--) {
store[i].recycle();
}
arrayCache.recycle(store);
this.storedTiles = null;
arrayCache.recycle(this.serveTiles);
this.serveTiles = null;
this.aabb.recycle();
this.aabb = null;
}
},
publicMethods: {
/**
* Returns the axis-aligned bounding box of the entire map.
*
* @method platypus.components.CollisionTiles#getAABB
* @return aabb {platypus.AABB} The returned object provides the top, left, width, and height of the collision map.
*/
getAABB: function () {
return this.aabb;
},
/**
* Confirms whether a particular map grid coordinate contains a tile.
*
* @method platypus.components.CollisionTiles#isTile
* @param x {number} Integer specifying the column of tiles in the collision map to check.
* @param y {number} Integer specifying the row of tiles in the collision map to check.
* @return {boolean} Returns `true` if the coordinate contains a collision tile, `false` if it does not.
*/
isTile: function (x, y) {
let z = 0;
if ((x < 0) || (y < 0) || (x >= this.columns) || (y >= this.rows)) {
return false;
}
for (z = 0; z < this.collisionMap.length; z++) {
if (this.collisionMap[z][x][y] !== -1) {
return true;
}
}
return false;
},
/**
* Returns all the collision tiles within the provided axis-aligned bounding box as an array of shapes.
*
* @method platypus.components.CollisionTiles#getTileShapes
* @param aabb {platypus.AABB} The axis-aligned bounding box for which tiles should be returned.
* @param prevAABB {platypus.AABB} The axis-aligned bounding box for a previous location to test for jump-through tiles.
* @param [collisionType] {String} The type of collision to check for. If not specified, "tiles" is used. (Since 0.8.3)
* @return {Array} Each returned object provides the [CollisionShape](CollisionShape.html) of a tile.
*/
getTileShapes: function (aabb, prevAABB, collisionType) {
const
colType = collisionType || 'tiles',
l = this.left,
t = this.top,
th = this.tileHeight,
tw = this.tileWidth,
left = Math.max(Math.floor((aabb.left - l) / tw), 0),
top = Math.max(Math.floor((aabb.top - t) / th), 0),
right = Math.min(Math.ceil((aabb.right - l) / tw), this.columns),
bottom = Math.min(Math.ceil((aabb.bottom - t) / th), this.rows),
shapes = this.serveTiles;
shapes.length = 0;
this.storedTileIndex = 0;
for (let z = 0; z < this.collisionMap.length; z++) {
for (let x = left; x < right; x++) {
for (let y = top; y < bottom; y++) {
this.addShape(shapes, prevAABB, z, x, y, colType);
}
}
}
return shapes;
},
/**
* Performs a transform of a subset of the collision tile grid.
*
* @method platypus.components.CollisionTiles#transform
* @param [transform] {Object} A list of key/value pairs describing the transform.
* @param [transform.layers] {Array.<number>} The layer indexes on which to apply the transform, if not provided transforms all layers. Defaults to the 0th index.
* @param [transform.type="horizontal"] {String} The type of transform; one of the following: "horizontal", "vertical", "diagonal", "diagonal-inverse", "rotate-90", "rotate-180", "rotate-270". Height and width should match for diagonal flips and 90 degree rotations.
* @param [transform.left=0] {number} Grid coordinate for the left side of the bounding box.
* @param [transform.top=0] {number} Grid coordinate for the top of the bounding box.
* @param [transform.width=grid.width] {number} Cell width of the bounding box.
* @param [transform.height=grid.height] {number} Cell height of the bounding box.
*/
transform: function (transform = {}) {
const {
layers = [0],
left: x = 0,
top: y = 0,
width = this.rows,
height = this.columns,
type = "horizontal"
} = transform;
let k = 0;
if (transforms[type]) {
for (k = 0; k < this.collisionMap.length; k++) {
if (layers.includes(k)) {
transforms[type](this.collisionMap[k], x, y, width, height);
}
}
return this.collisionMap;
} else {
return null;
}
},
/**
* Performs a translation of a subset of the collision tile grid.
*
* @method platypus.components.CollisionTiles#translate
* @param [translate] {Object} A list of key/value pairs describing the translation.
* @param [translate.layers] {Array.<number>} The layer indexes on which to apply the translate, if not provided translates all layers. Defaults to the 0th index.
* @param [translate.dx=0] {number} Movement in columns.
* @param [translate.dy=0] {number} Movement in rows.
* @param [translate.left=0] {number} Grid coordinate for the left side of the bounding box.
* @param [translate.top=0] {number} Grid coordinate for the top of the bounding box.
* @param [translate.width=grid.width] {number} Cell width of the bounding box.
* @param [translate.height=grid.height] {number} Cell height of the bounding box.
*/
translate: function (translate = {}) {
const {
layers = [0],
left: x = 0,
top: y = 0,
width = this.rows,
height = this.columns,
dx = 0,
dy = 0
} = translate;
let k = 0;
for (k = 0; k < this.collisionMap.length; k++) {
if (!layers || layers.includes(k)) {
transforms.translate(this.collisionMap[k], x, y, width, height, dx, dy);
}
}
return this.collisionMap;
},
/**
* Gets a subset of the collision tile grid as a 2D array.
*
* @method platypus.components.CollisionTiles#getCollisionMatrix
* @param layer {number} The collision map index from which to copy.
* @param originX {number} Grid coordinate for the left side of the bounding box.
* @param originY {number} Grid coordinate for the top of the bounding box.
* @param width {number} Cell width of the bounding box.
* @param height {number} Cell height of the bounding box.
* @return {Array}
*/
getCollisionMatrix: function (layer, originX, originY, width, height) {
return copySection(this.collisionMap[layer], originX, originY, width, height);
},
/**
* Sets a subset of the collision tile grid.
*
* @method platypus.components.CollisionTiles#setCollisionMatrix
* @param sourceArray {Array} A 2D array describing the collision tiles to insert into the collision tile grid.
* @param layer {number} The collision map index into which to paste the collision data.
* @param originX {number} Grid coordinate for the left side of the bounding box.
* @param originY {number} Grid coordinate for the top of the bounding box.
* @param width {number} Cell width of the bounding box.
* @param height {number} Cell height of the bounding box.
*/
setCollisionMatrix: function (sourceArray, layer, originX, originY, width, height) {
return pasteSection(this.collisionMap[layer], sourceArray, originX, originY, width, height);
}
}
});
}());