Files
feuerwerk/src/core/store/tilemapCells.ts
T

116 lines
4.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* How the cells of a tilemap layer are written to the project file (#134). In memory a layer keeps
* its sparse map from `"x,y"` to a tile id – what painting and reading a neighbour want – but in the
* file every painted cell would then carry its own key and the full tile id, and the server writes
* the file pretty-printed, one entry per line.
*
* So the file holds, per layer:
* - `tiles`: every tile id the layer names, once, in the order they first appear;
* - `origin`: the top-left cell of the smallest rectangle around all painted cells;
* - `rows`: one string per row of that rectangle, as runs `index*count` (a single cell drops the
* count), separated by spaces. `index` counts from 1 into `tiles`, `0` is an empty cell, and
* empty cells at the end of a row are left out.
*
* A row of forty grass cells, three empty ones and a single path cell reads `1*40 0*3 2`. The
* rectangle is taken from the cells, not from the grid, so cells outside the grid – kept so that
* they come back when the grid grows again – are stored as they are.
*/
import { cellKey, parseCellKey } from '../model/tilemap';
export interface StoredCells {
tiles: string[];
origin: { x: number; y: number };
rows: string[];
}
/**
* The longest run a row may state. A file is untrusted: `1*999999999` would otherwise build a
* billion cells. No grid gets near this, so a real map never meets it.
*/
export const MAX_RUN = 65536;
const RUN = /^(\d+)(?:\*(\d+))?$/;
export function encodeCells(cells: Record<string, string>): StoredCells {
const painted: { x: number; y: number; id: string }[] = [];
for (const [key, id] of Object.entries(cells)) {
const at = parseCellKey(key);
if (at && id) painted.push({ ...at, id });
}
if (painted.length === 0) return { tiles: [], origin: { x: 0, y: 0 }, rows: [] };
painted.sort((a, b) => a.y - b.y || a.x - b.x);
const tiles: string[] = [];
const index = new Map<string, number>();
for (const { id } of painted)
if (!index.has(id)) {
tiles.push(id);
index.set(id, tiles.length);
}
const x0 = painted.reduce((min, c) => Math.min(min, c.x), Infinity);
const y0 = painted[0]!.y;
const y1 = painted[painted.length - 1]!.y;
const rows: string[] = [];
let next = 0;
for (let y = y0; y <= y1; y++) {
const runs: string[] = [];
let x = x0;
const push = (value: number, count: number): void => {
runs.push(count === 1 ? String(value) : `${value}*${count}`);
};
while (next < painted.length && painted[next]!.y === y) {
const cell = painted[next]!;
if (cell.x > x) push(0, cell.x - x);
const value = index.get(cell.id)!;
let count = 1;
while (
next + count < painted.length &&
painted[next + count]!.y === y &&
painted[next + count]!.x === cell.x + count &&
painted[next + count]!.id === cell.id
)
count++;
push(value, count);
x = cell.x + count;
next += count;
}
rows.push(runs.join(' '));
}
return { tiles, origin: { x: x0, y: y0 }, rows };
}
/**
* The cells a stored layer describes. Read leniently, as the rest of the file is: a run that does
* not parse ends its row, because every run after it would land in the wrong column; an index
* naming no tile is an empty cell.
*/
export function decodeCells(raw: {
tiles?: unknown;
origin?: unknown;
rows?: unknown;
}): Record<string, string> {
const tiles: unknown[] = Array.isArray(raw.tiles) ? raw.tiles : [];
const origin = raw.origin && typeof raw.origin === 'object' ? (raw.origin as Record<string, unknown>) : {};
const x0 = Number.isSafeInteger(origin.x) ? (origin.x as number) : 0;
const y0 = Number.isSafeInteger(origin.y) ? (origin.y as number) : 0;
const cells: Record<string, string> = {};
const rows = Array.isArray(raw.rows) ? raw.rows : [];
rows.forEach((row, r) => {
if (typeof row !== 'string') return;
let x = x0;
for (const token of row.split(/\s+/)) {
if (token === '') continue;
const match = RUN.exec(token);
const value = match ? Number(match[1]) : NaN;
const count = match ? Number(match[2] ?? 1) : NaN;
if (!Number.isSafeInteger(value) || !(count >= 1 && count <= MAX_RUN)) return;
const id = value >= 1 ? tiles[value - 1] : undefined;
if (typeof id === 'string' && id !== '')
for (let i = 0; i < count; i++) cells[cellKey(x + i, y0 + r)] = id;
x += count;
}
});
return cells;
}