import Rectangle from "./Rectangle.js";
import clone from "clone";
import mitt from "mitt";
import { getProperty, setProperty, deleteProperty } from "dot-prop";
/**
* 2D Grid Wrapper
*/
class Grid {
/**
* @param {number} width
* @param {number} height
* @param {*} [default_element=null] the default element to fill each cell of the grid
*/
constructor(width, height, default_element = null) {
this.w = width;
this.h = height;
this.size = this.w * this.h;
this.def = default_element;
this.grid = new Array(this.size).fill().map(u => this.getDefault());
this.events = mitt();
}
on(event, cb) {
this.events.on(event, cb);
return this;
}
clone() {
let g = new Grid(this.w, this.h, this.def);
g.grid = this.grid;
return g;
}
/**
* internal function used to fill each cell initially, each cell value is a clone, not a reference
* @see https://www.npmjs.com/package/clone
* @return {*}
*/
getDefault() {
return clone(this.def);
}
/**
* get the contents of the cell at x,y
* if the coordinates are outside the grid, the default element is returned
* @param {number} x
* @param {number} y
* @param {string} prop
* @return {*} the cell content
*/
get(x, y, prop = null) {
let i = this.index(x, y);
let g = (i < 0 || i > this.size || this.grid[i] == null) ? this.getDefault() : this.grid[i];
return prop !== null ? getProperty(g, prop) : g;
}
/**
* set the contents of the cell at x,y
* a fourth parameter can be passed to set the value of an object key instead
* @example
* grid.set(4,7,true);
* grid.set(4,7,"key", true);
* @param {number} x
* @param {number} y
* @param {string|*} a value
* @param {*} b
*/
set(x, y, a, b) {
let i = this.index(x, y);
let prop = null;
let val = null;
if (i < 0 || i > this.size) return;
if (typeof b !== "undefined") {
prop = a;
val = b;
setProperty(this.grid[i], a, b);
} else {
val = a;
this.grid[i] = a;
}
this.events.emit("set", { x, y, cell: this.grid[i], prop, val });
}
/**
* deletes the contents of an object key for the cell at x,y
* @example
* grid.delete(4,7,"key");
* @param {number} x
* @param {number} y
* @param {string} prop property key
*/
delete(x, y, prop) {
let i = this.index(x, y);
if (i < 0 || i > this.size || this.grid[i] == null) return;
deleteProperty(this.grid[i], prop);
this.events.emit("delete", { x, y, cell: this.grid[i], prop });
}
/**
* get the one-dimensional index of the cell
* -1 if outside the grid
* @param {number} x
* @param {number} y
* @return {number}
*/
index(x, y) {
if (x < 0 || x > this.w - 1 || y < 0 || y > this.h - 1) return -1;
return this.w * y + x;
}
/**
* get the two-dimensional coordinates of the cell at this index
* @param {number} i
* @return {object} the coordinates
*/
coords(i) {
return {
y: Math.floor(i / this.w),
x: i % this.w
}
}
/**
* Loop callback
* @callback Grid~loopCallback
* @param {number} x
* @param {number} y
* @param {*} cell
*/
/**
* loop over each cell (call cb for each cell)
* @param {function} cb {@link Grid~loopCallback loop callback}
*/
each(cb) {
for (let index = 0; index < this.size; index++) {
let c = this.coords(index);
cb(c.x, c.y, this.grid[index]);
}
}
/**
* loop over each cell (call cb for each cell) and replace it
* the new array is replaced after the callback has been called for each cell
* @param {function} cb {@link Grid~loopCallback loop callback}
*/
map(cb) {
let new_grid = [];
for (let index = 0; index < this.size; index++) {
let c = this.coords(index);
new_grid[index] = cb(c.x, c.y, this.grid[index]);
}
this.grid = new_grid;
this.events.emit("replace", this.grid);
}
/**
* Get a copy of a part of the grid
* @param {Number} x offset on the x-axis
* @param {Number} y offset on the y-axis
* @param {Number} w width of the slice
* @param {Number} h height of the slice
* @return {Grid}
*/
slice(x, y, w, h) {
let ret = new Grid(w, h, this.getDefault());
for (let i = 0; i < w; i++) {
for (let j = 0; j < h; j++) {
ret.set(i, j, this.get(x + i, y + j));
}
}
return ret;
}
/**
* Start a flood fill from the given begin cell
* @param {Object} begin x and y
* @param {function} cellCallback return true if the neighbours of the cell should be checked
* @param {function} neighbourCallback return true if the neighbour should be checked (is the same as cellCallback by default)
* @return {Array} flooded cells
*/
floodfill(begin, cellCallback, neighbourCallback = null) {
let open = [begin];
let closed = new Map();
if (neighbourCallback === null) neighbourCallback = cellCallback;
while (open.length) {
let f = open.pop();
let cell = this.get(f.x, f.y);
closed.set(f.x + "-" + f.y, f);
if (!cellCallback(cell, f.x, f.y)) continue;
for (let x = -1; x <= 1; x++) {
for (let y = -1; y <= 1; y++) {
if (x == 0 && y == 0) continue;
let nx = f.x + x;
let ny = f.y + y;
if (nx < 0 || nx >= this.w || ny < 0 || ny >= this.h) continue;
if (closed.has(nx + "-" + ny) || open.find(o => o.x === nx && o.y === ny)) continue;
if (neighbourCallback(this.get(nx, ny), nx, ny)) {
open.push({ x: nx, y: ny });
}
}
}
}
let ret = Array.from(closed.values());
if (ret.length === 1 && !cellCallback(this.get(begin.x, begin.y), begin.x, begin.y)) {
return [];
}
return ret;
}
/**
* get the neighbours cells of a cell
* @param {number} x
* @param {number} y
* @param {string} [prop=null]
* @return {Array}
*/
neighbours(x, y, prop = null) {
let n = {
...this.directNeighbours(x, y, prop),
nw: this.get(x - 1, y - 1, prop),
ne: this.get(x + 1, y - 1, prop),
sw: this.get(x - 1, y + 1, prop),
se: this.get(x + 1, y + 1, prop)
};
return n;
}
/**
* get the direct neighbours cells of a cell, cells outside the grid are the default
* @param {number} x
* @param {number} y
* @param {string} [prop=null]
* @return {Array}
*/
directNeighbours(x, y, prop = null) {
let n = {
n: this.get(x, y - 1, prop),
e: this.get(x + 1, y, prop),
s: this.get(x, y + 1, prop),
w: this.get(x - 1, y, prop)
};
return n;
}
/**
* get the direct neighbours cells of a cell, ignore those outside the grid
* @param {number} x
* @param {number} y
* @param {string} [prop=null]
* @return {Array}
*/
strictDirectNeighbours(x, y, prop = null) {
let n = {};
if (y > 0) n.n = this.get(x, y - 1, prop);
if (x < this.w - 1) n.e = this.get(x + 1, y, prop);
if (y < this.h - 1) n.s = this.get(x, y + 1, prop);
if (x > 0) n.w = this.get(x - 1, y, prop);
return n;
}
bitmask(x, y, tags, prop = null) {
let m = 0;
let n = this.neighbours(x, y, prop);
for (const key in n) {
if (n[key] === null) continue;
if (n[key].tags) {
n[key] = n[key].hasAnyTag(tags);
} else {
n[key] = tags.includes(n[key]);
}
}
if (n.n) {
m |= 1;
if (n.ne && n.e) m |= 2;
}
if (n.e) {
m |= 4;
if (n.se && n.s) m |= 8;
}
if (n.s) {
m |= 16;
if (n.sw && n.w) m |= 32;
}
if (n.w) {
m |= 64;
if (n.nw && n.n) m |= 128;
}
return m;
}
neighbourCount(x, y, prop, val) {
let n = this.neighbours(x, y, prop);
return Object.values(n).reduce((a, c) => {
return a + (c == val ? 1 : 0);
}, 0);
}
getCollisionBoxes(type, prop = "type") {
let rectangles = [];
for (let x = 0; x < this.w; x++) {
let start_y = null;
let end_y = null;
for (let y = 0; y <= this.h; y++) {
let wall = this.get(x, y, prop) == type;
if (wall && y < this.h) {
if (start_y == null) start_y = y;
end_y = y;
} else if (start_y !== null) {
if (y == this.h - 1) {
end_y = y - 1;
}
let overlaps = [];
for (const r in rectangles) {
let rect = rectangles[r];
if ((rect.end_x == x) && (start_y <= rect.y) && (end_y >= rect.end_y)) {
overlaps.push(rect);
}
}
overlaps.sort((a, b) => a.y - b.y);
for (const o in overlaps) {
let rect = overlaps[o];
if (start_y < rect.y) {
rectangles.push({
x: x,
y: start_y,
end_x: x + 1,
end_y: rect.y - 1
});
start_y = rect.y;
}
if (start_y == rect.y) {
rect.end_x++;
if (end_y == rect.end_y) {
start_y = null;
end_y == null;
} else if (end_y > rect.end_y) {
start_y = rect.end_y + 1;
}
}
}
if (start_y !== null) {
rectangles.push({
x: x,
y: start_y,
end_x: x + 1,
end_y: end_y
});
start_y = null;
end_y = null;
}
}
}
if (start_y !== null) {
rectangles.push({
x: x,
y: start_y,
end_x: x + 1,
end_y: end_y
});
start_y = null;
end_y = null;
}
}
return rectangles.map(box => {
return new Rectangle(box.x, box.y, box.end_x - box.x, box.end_y - box.y + 1);
});
}
/**
* Serialize the grid into a compact object structure
* requires each cell to be an object
* the return value is ready to be send over network or further serialized with JSON.stringify
* @return {object}
*/
serialize() {
let data = {
width: this.w,
height: this.h,
palette: [],
states: []
};
this.each((x, y, t) => {
let s = JSON.stringify(t);
let i = data.palette.indexOf(s);
if (i == -1) {
data.palette.push(s);
i = data.palette.length - 1;
}
data.states.push(i);
});
let last_index = null;
let count = 0;
let indices = [];
data.states.forEach(s => {
if (last_index == null) {
last_index = s;
count = 1;
return;
}
if (last_index !== s) {
indices.push(count == 1 ? last_index : last_index + "-" + count);
last_index = s;
count = 1;
return
}
count++;
});
indices.push(count == 1 ? last_index : last_index + "-" + count);
data.states = indices.join(",");
data.palette = data.palette.map(p => JSON.parse(p));
return data;
}
/**
* unserialize a data structure generated by {@link serialize}
* @param {object} data
*/
unserialize(data) {
let p = data.palette;
let s = data.states.split(",");
let current_index = 0;
s.forEach((i, j) => {
let d = i.split("-");
let type = d[0];
let count = d.length == 1 ? 1 : d[1];
for (let c = 0; c < count; c++) {
this.grid[current_index] = clone(p[type]);
current_index++;
}
});
}
}
export default Grid;