import Component from '@/ecs/component.js'; import {CHUNK_SIZE} from '@/util/constants.js'; import {floodwalk2D, ortho, removeCollinear} from '@/util/math.js'; import vector2d from './helpers/vector-2d'; class LayerProxy { $$chunks = []; $$sourceJson = {}; constructor(instance, Component, index) { this.instance = instance; this.Component = Component; this.index = index; this.$$chunks = Array( Math.ceil(this.area.x / CHUNK_SIZE) * Math.ceil(this.area.y / CHUNK_SIZE) ).fill(0).map(() => ({})); } get area() { return this.layer.area; } clone() { const {$$chunks, $$sourceJson} = this.instance.$$layersProxies[this.index]; const proxy = new LayerProxy(this.instance, this.Component, this.index); proxy.$$chunks = [...$$chunks]; proxy.$$sourceJson = $$sourceJson; return proxy; } compute(index) { const cx = Math.floor((index % this.area.x) / CHUNK_SIZE); const cy = Math.floor(Math.floor(index / this.area.x) / CHUNK_SIZE); const ax = Math.ceil(this.area.x / CHUNK_SIZE); return cy * ax + cx; } get data() { return this.layer.data; } get hulls() { const {data, area, tileSize} = this; const hulls = []; const seen = {}; const n = data.length; const {x: w, y: h} = area; let x = 0; let y = 0; for (let i = 0; i < n; ++i) { if (data[i]) { if (!seen[i]) { const indices = floodwalk2D(new Set([data[i]]), data, {x, y, w, h}); if (indices.size > 0) { const pointHash = Object.create(null); const points = []; const seePoint = ({x, y}) => { if (pointHash[y]?.[x]) { return false; } if (!pointHash[y]) { pointHash[y] = Object.create({}); } return pointHash[y][x] = true; }; for (const index of indices) { seen[index] = true; const op = { x: tileSize.x * (index % area.x), y: tileSize.y * (Math.floor(index / area.x)), }; let p; const tsq = {x: tileSize.x / 4, y: tileSize.y / 4}; p = {x: op.x + tsq.x, y: op.y + tsq.y}; if (seePoint(p)) { points.push(p); } p = {x: op.x + tileSize.x - tsq.x, y: op.y + tsq.y}; if (seePoint(p)) { points.push(p); } p = {x: op.x + tileSize.x - tsq.x, y: op.y + tileSize.y - tsq.y}; if (seePoint(p)) { points.push(p); } p = {x: op.x + tsq.x, y: op.y + tileSize.y - tsq.y}; if (seePoint(p)) { points.push(p); } } hulls.push(removeCollinear(ortho(points, {x: tileSize.x / 2, y: tileSize.y / 2}))); } } } x += 1; if (x === w) { x -= w; y += 1; } } return hulls; } get layer() { return this.instance.layers[this.index]; } async load() { this.$$sourceJson = this.layer.source ? await this.Component.ecs.readJson(this.layer.source) : {}; } get source() { return this.layer.source; } get sourceJson() { return this.$$sourceJson; } stamp(at, data) { const changes = {}; for (const row in data) { const columns = data[row]; for (const column in columns) { const tile = columns[column]; const x = at.x + parseInt(column); const y = at.y + parseInt(row); if (x < 0 || y < 0 || x >= this.layer.area.x || y >= this.layer.area.y) { continue; } const calculated = y * this.layer.area.x + x; this.layer.data[calculated] = tile; changes[calculated] = tile; } } this.Component.markChange(this.instance.entity, 'layerChange', {[this.index]: changes}); } tile({x, y}) { if (x < 0 || y < 0 || x >= this.layer.area.x || y >= this.layer.area.y) { return undefined; } return this.layer.data[y * this.layer.area.x + x]; } get tileSize() { return this.layer.tileSize; } } export default class TileLayers extends Component { async createMany(entities) { for (const [, {layerChange, layers}] of entities) { if (layers) { for (const layer of layers) { const area = layer.area.x * layer.area.y; if (layer.data.length < area) { for (let i = 0; i < area; ++i) { if (!layer.data[i]) { layer.data[i] = 0; } } } } } if (layerChange) { for (const layerIndex in layerChange) { for (const calculated in layerChange[layerIndex]) { const tile = layerChange[layerIndex][calculated]; layers[layerIndex].data[calculated] = tile; } } } } return super.createMany(entities); } async insertMany(entities) { for (const [, {layers}] of entities) { if (layers) { for (const layer of layers) { const area = layer.area.x * layer.area.y; if (layer.data.length < area) { for (let i = 0; i < area; ++i) { if (!layer.data[i]) { layer.data[i] = 0; } } } } } } await super.insertMany(entities); for (const [id, {layerChange}] of entities) { if (layerChange) { const component = this.get(id); const {layers} = component; for (const layerIndex in layerChange) { const proxy = component.$$layersProxies[layerIndex].clone(); const chunksChanged = new Set(); for (const tileIndex in layerChange[layerIndex]) { chunksChanged.add(proxy.compute(tileIndex)); const tile = layerChange[layerIndex][tileIndex]; layers[layerIndex].data[tileIndex] = tile; } for (const chunkChanged of chunksChanged) { proxy.$$chunks[chunkChanged] = {}; } component.$$layersProxies[layerIndex] = proxy; } } } } instanceFromSchema() { return class TileLayersInstance extends super.instanceFromSchema() { $$layersProxies = {}; layer(index) { return this.$$layersProxies[index]; } } } async load(instance) { for (const index in instance.layers) { instance.$$layersProxies[index] = new LayerProxy(instance, this, index); await instance.$$layersProxies[index].load(); } } mergeDiff(original, update) { if (!update.layerChange) { return super.mergeDiff(original, update); } const layerChange = { ...original.layerChange, }; for (const index in update.layerChange) { layerChange[index] = { ...layerChange[index], ...update.layerChange[index], }; } return {layerChange}; } static properties = { layers: { type: 'array', subtype: { type: 'object', properties: { area: vector2d('float32'), data: { type: 'array', subtype: { type: 'uint16', }, }, source: {type: 'string'}, tileSize: vector2d('float32'), }, }, }, }; }