603 lines
24 KiB
TypeScript
603 lines
24 KiB
TypeScript
|
|
// @ts-nocheck
|
||
|
|
|
||
|
|
export function createFilledRows(width, height, fillChar) {
|
||
|
|
return Array.from({ length: Math.max(1, Number(height) || 1) }, () => String(fillChar || " ").repeat(Math.max(1, Number(width) || 1)));
|
||
|
|
}
|
||
|
|
|
||
|
|
function writeRowSegment(rows, y, x, segment) {
|
||
|
|
if (!Array.isArray(rows) || !segment) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const targetY = Math.floor(Number(y) || 0);
|
||
|
|
if (targetY < 0 || targetY >= rows.length) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const safeX = Math.max(0, Math.floor(Number(x) || 0));
|
||
|
|
const sourceRow = String(rows[targetY] || "");
|
||
|
|
const paddedRow = sourceRow.length >= safeX
|
||
|
|
? sourceRow
|
||
|
|
: (sourceRow + " ".repeat(Math.max(0, safeX - sourceRow.length)));
|
||
|
|
const before = paddedRow.slice(0, safeX);
|
||
|
|
const afterStart = safeX + segment.length;
|
||
|
|
const after = afterStart < paddedRow.length ? paddedRow.slice(afterStart) : "";
|
||
|
|
rows[targetY] = before + segment + after;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function composeWorldRoomLayers(chunks, chunkWidth, chunkHeight, originChunkX, originChunkY, worldWidth, worldHeight) {
|
||
|
|
const layerMap = new Map();
|
||
|
|
(Array.isArray(chunks) ? chunks : []).forEach((chunk) => {
|
||
|
|
const baseChunkX = Math.floor(Number(chunk?.chunkX) || 0);
|
||
|
|
const baseChunkY = Math.floor(Number(chunk?.chunkY) || 0);
|
||
|
|
const offsetX = (baseChunkX - originChunkX) * chunkWidth;
|
||
|
|
const offsetY = (baseChunkY - originChunkY) * chunkHeight;
|
||
|
|
const rawLayers = Array.isArray(chunk?.roomLayers) ? chunk.roomLayers : [];
|
||
|
|
rawLayers.forEach((rawLayer) => {
|
||
|
|
const layerNumber = Number(rawLayer?.layer) || 0;
|
||
|
|
const fillChar = layerNumber === 0 ? "." : " ";
|
||
|
|
if (!layerMap.has(layerNumber)) {
|
||
|
|
layerMap.set(layerNumber, {
|
||
|
|
layer: layerNumber,
|
||
|
|
name: typeof rawLayer?.name === "string" && String(rawLayer.name).trim() ? String(rawLayer.name).trim() : undefined,
|
||
|
|
rows: createFilledRows(worldWidth, worldHeight, fillChar),
|
||
|
|
instanceIds: [],
|
||
|
|
});
|
||
|
|
}
|
||
|
|
const targetLayer = layerMap.get(layerNumber);
|
||
|
|
const sourceRows = Array.isArray(rawLayer?.rows) ? rawLayer.rows.map((row) => String(row || "")) : [];
|
||
|
|
sourceRows.forEach((row, localY) => {
|
||
|
|
const targetY = offsetY + localY;
|
||
|
|
if (targetY < 0 || targetY >= targetLayer.rows.length) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const maxWidth = Math.max(0, worldWidth - offsetX);
|
||
|
|
writeRowSegment(targetLayer.rows, targetY, offsetX, row.slice(0, maxWidth));
|
||
|
|
});
|
||
|
|
const sourceInstanceIds = Array.isArray(rawLayer?.instanceIds)
|
||
|
|
? rawLayer.instanceIds.map((entry) => String(entry || "").trim()).filter(Boolean)
|
||
|
|
: [];
|
||
|
|
targetLayer.instanceIds = Array.from(new Set([...(targetLayer.instanceIds || []), ...sourceInstanceIds]));
|
||
|
|
});
|
||
|
|
});
|
||
|
|
if (!layerMap.has(0)) {
|
||
|
|
layerMap.set(0, {
|
||
|
|
layer: 0,
|
||
|
|
rows: createFilledRows(worldWidth, worldHeight, "."),
|
||
|
|
instanceIds: [],
|
||
|
|
});
|
||
|
|
}
|
||
|
|
return Array.from(layerMap.values()).sort((a, b) => (Number(a.layer) || 0) - (Number(b.layer) || 0));
|
||
|
|
}
|
||
|
|
|
||
|
|
export function composeWorldHeightLayers(chunks, chunkWidth, chunkHeight, originChunkX, originChunkY) {
|
||
|
|
const patches = [];
|
||
|
|
(Array.isArray(chunks) ? chunks : []).forEach((chunk) => {
|
||
|
|
const baseChunkX = Math.floor(Number(chunk?.chunkX) || 0);
|
||
|
|
const baseChunkY = Math.floor(Number(chunk?.chunkY) || 0);
|
||
|
|
const offsetX = (baseChunkX - originChunkX) * chunkWidth;
|
||
|
|
const offsetY = (baseChunkY - originChunkY) * chunkHeight;
|
||
|
|
const rawHeightLayers = Array.isArray(chunk?.heightLayers) ? chunk.heightLayers : [];
|
||
|
|
rawHeightLayers.forEach((entry, index) => {
|
||
|
|
const fallbackId = `height_${baseChunkX}_${baseChunkY}_${index + 1}`;
|
||
|
|
patches.push({
|
||
|
|
id: String(entry?.id || fallbackId).trim() || fallbackId,
|
||
|
|
name: typeof entry?.name === "string" && String(entry.name).trim() ? String(entry.name).trim() : undefined,
|
||
|
|
z: Math.max(1, Math.floor(Number(entry?.z) || 1)),
|
||
|
|
x: offsetX + Math.max(0, Number(entry?.x) || 0),
|
||
|
|
y: offsetY + Math.max(0, Number(entry?.y) || 0),
|
||
|
|
rows: Array.isArray(entry?.rows) ? entry.rows.map((row) => String(row || "")) : [],
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
return patches.sort((a, b) => {
|
||
|
|
if (a.z !== b.z) {
|
||
|
|
return a.z - b.z;
|
||
|
|
}
|
||
|
|
return String(a.name || a.id).localeCompare(String(b.name || b.id));
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
export function buildWorldLayerMetadata(chunks) {
|
||
|
|
const layerMap = new Map();
|
||
|
|
(Array.isArray(chunks) ? chunks : []).forEach((chunk) => {
|
||
|
|
const rawLayers = Array.isArray(chunk?.roomLayers) ? chunk.roomLayers : [];
|
||
|
|
rawLayers.forEach((rawLayer) => {
|
||
|
|
const layerNumber = Number(rawLayer?.layer) || 0;
|
||
|
|
if (layerMap.has(layerNumber)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
layerMap.set(layerNumber, {
|
||
|
|
layer: layerNumber,
|
||
|
|
name: typeof rawLayer?.name === "string" && String(rawLayer.name).trim() ? String(rawLayer.name).trim() : undefined,
|
||
|
|
rows: [],
|
||
|
|
instanceIds: Array.isArray(rawLayer?.instanceIds) ? rawLayer.instanceIds.map((entry) => String(entry || "").trim()).filter(Boolean) : [],
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
if (!layerMap.has(0)) {
|
||
|
|
layerMap.set(0, {
|
||
|
|
layer: 0,
|
||
|
|
rows: [],
|
||
|
|
instanceIds: [],
|
||
|
|
});
|
||
|
|
}
|
||
|
|
if (!Array.from(layerMap.keys()).some((layerNumber) => layerNumber > 0)) {
|
||
|
|
layerMap.set(1, {
|
||
|
|
layer: 1,
|
||
|
|
rows: [],
|
||
|
|
instanceIds: [],
|
||
|
|
});
|
||
|
|
}
|
||
|
|
return Array.from(layerMap.values()).sort((a, b) => (Number(a.layer) || 0) - (Number(b.layer) || 0));
|
||
|
|
}
|
||
|
|
|
||
|
|
export function sliceNormalizedRows(rows, startX, startY, width, height, fillChar) {
|
||
|
|
return Array.from({ length: Math.max(1, Number(height) || 1) }, (_, rowOffset) => {
|
||
|
|
const sourceRow = String((Array.isArray(rows) ? rows[startY + rowOffset] : "") || "");
|
||
|
|
const paddedRow = sourceRow.length >= startX + width
|
||
|
|
? sourceRow
|
||
|
|
: sourceRow + String(fillChar || " ").repeat(Math.max(0, (startX + width) - sourceRow.length));
|
||
|
|
return paddedRow.slice(startX, startX + width);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
export function buildChunkHeightLayersFromDocument({ mapDocument, cloneHeightLayers, baseTileX, baseTileY, chunkWidth, chunkHeight }) {
|
||
|
|
return (Array.isArray(mapDocument.heightLayers) ? cloneHeightLayers(mapDocument.heightLayers) : [])
|
||
|
|
.map((entry) => {
|
||
|
|
const patchX = Math.max(0, Number(entry?.x) || 0);
|
||
|
|
const patchY = Math.max(0, Number(entry?.y) || 0);
|
||
|
|
const rows = Array.isArray(entry?.rows) ? entry.rows.map((row) => String(row || "")) : [];
|
||
|
|
const patchWidth = rows.reduce((max, row) => Math.max(max, row.length), 0);
|
||
|
|
const patchHeight = rows.length;
|
||
|
|
const patchRight = patchX + patchWidth;
|
||
|
|
const patchBottom = patchY + patchHeight;
|
||
|
|
const chunkRight = baseTileX + chunkWidth;
|
||
|
|
const chunkBottom = baseTileY + chunkHeight;
|
||
|
|
const overlapLeft = Math.max(baseTileX, patchX);
|
||
|
|
const overlapTop = Math.max(baseTileY, patchY);
|
||
|
|
const overlapRight = Math.min(chunkRight, patchRight);
|
||
|
|
const overlapBottom = Math.min(chunkBottom, patchBottom);
|
||
|
|
if (overlapRight <= overlapLeft || overlapBottom <= overlapTop) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
const localRows = [];
|
||
|
|
for (let y = overlapTop; y < overlapBottom; y += 1) {
|
||
|
|
const sourceRow = String(rows[y - patchY] || "");
|
||
|
|
localRows.push(sourceRow.slice(overlapLeft - patchX, overlapRight - patchX).replace(/\s+$/g, ""));
|
||
|
|
}
|
||
|
|
return {
|
||
|
|
id: String(entry?.id || "").trim(),
|
||
|
|
name: typeof entry?.name === "string" && entry.name.trim() ? entry.name.trim() : undefined,
|
||
|
|
z: Math.max(1, Number(entry?.z) || 1),
|
||
|
|
x: overlapLeft - baseTileX,
|
||
|
|
y: overlapTop - baseTileY,
|
||
|
|
rows: localRows,
|
||
|
|
};
|
||
|
|
})
|
||
|
|
.filter((entry) => entry && entry.id);
|
||
|
|
}
|
||
|
|
|
||
|
|
export function buildChunkInstancesFromDocument({ mapDocument, cloneValue, baseTileX, baseTileY, chunkWidth, chunkHeight, tileOffsetX, tileOffsetY }) {
|
||
|
|
const chunkInstances = cloneValue(mapDocument.npcOverlays)
|
||
|
|
.filter((npc) => {
|
||
|
|
const localX = Math.floor(Number(npc?.x));
|
||
|
|
const localY = Math.floor(Number(npc?.y));
|
||
|
|
return Number.isFinite(localX)
|
||
|
|
&& Number.isFinite(localY)
|
||
|
|
&& localX >= baseTileX
|
||
|
|
&& localX < baseTileX + chunkWidth
|
||
|
|
&& localY >= baseTileY
|
||
|
|
&& localY < baseTileY + chunkHeight;
|
||
|
|
})
|
||
|
|
.map((npc) => ({
|
||
|
|
id: String(npc.id || "").trim(),
|
||
|
|
templateId: String(npc?.record?.templateId || "").trim(),
|
||
|
|
layer: Number(npc.layer) || 0,
|
||
|
|
x: Math.floor(Number(npc.x) || 0) - baseTileX,
|
||
|
|
y: Math.floor(Number(npc.y) || 0) - baseTileY,
|
||
|
|
record: {
|
||
|
|
...cloneValue(npc.record || {}),
|
||
|
|
id: String(npc.id || "").trim(),
|
||
|
|
layer: Number(npc.layer) || 0,
|
||
|
|
templateId: String(npc?.record?.templateId || "").trim(),
|
||
|
|
name: String(npc.name || npc?.record?.name || ""),
|
||
|
|
entityType: String(npc?.record?.entityType || npc?.entityType || "friendly"),
|
||
|
|
faction: String(npc.faction || npc?.record?.faction || ""),
|
||
|
|
spriteId: String(npc.spriteId || npc?.record?.spriteId || ""),
|
||
|
|
dialogueId: String(npc.dialogueId || npc?.record?.dialogueId || ""),
|
||
|
|
description: String(npc.description || npc?.record?.description || ""),
|
||
|
|
tags: cloneValue(npc?.record?.tags) || [],
|
||
|
|
enabled: typeof npc?.record?.enabled === "boolean" ? npc.record.enabled : true,
|
||
|
|
position: {
|
||
|
|
x: Math.floor(Number(npc.x) || 0) + tileOffsetX,
|
||
|
|
y: Math.floor(Number(npc.y) || 0) + tileOffsetY,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
}))
|
||
|
|
.filter((entry) => entry.id);
|
||
|
|
const npcIdsByLayer = new Map();
|
||
|
|
chunkInstances.forEach((entry) => {
|
||
|
|
const layerNumber = Number(entry.layer) || 0;
|
||
|
|
if (!npcIdsByLayer.has(layerNumber)) {
|
||
|
|
npcIdsByLayer.set(layerNumber, []);
|
||
|
|
}
|
||
|
|
npcIdsByLayer.get(layerNumber).push(entry.id);
|
||
|
|
});
|
||
|
|
return {
|
||
|
|
chunkInstances,
|
||
|
|
npcIdsByLayer,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
export function normalizeWorldChunkRows(rows, width, height, fillChar) {
|
||
|
|
const safeWidth = Math.max(1, Math.floor(Number(width) || 1));
|
||
|
|
const safeHeight = Math.max(1, Math.floor(Number(height) || 1));
|
||
|
|
return Array.from({ length: safeHeight }, (_entry, rowIndex) => {
|
||
|
|
const sourceRow = String((Array.isArray(rows) ? rows[rowIndex] : "") || "");
|
||
|
|
return sourceRow.length >= safeWidth
|
||
|
|
? sourceRow.slice(0, safeWidth)
|
||
|
|
: (sourceRow + String(fillChar || " ").repeat(Math.max(0, safeWidth - sourceRow.length)));
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
export function cloneWorldChunkHeightLayers(source) {
|
||
|
|
return (Array.isArray(source) ? source : [])
|
||
|
|
.map((entry, index) => ({
|
||
|
|
id: String(entry?.id || `height_patch_${index + 1}`).trim() || `height_patch_${index + 1}`,
|
||
|
|
name: typeof entry?.name === "string" && entry.name.trim() ? entry.name.trim() : undefined,
|
||
|
|
z: Math.max(1, Math.floor(Number(entry?.z) || 1)),
|
||
|
|
x: Math.max(0, Math.floor(Number(entry?.x) || 0)),
|
||
|
|
y: Math.max(0, Math.floor(Number(entry?.y) || 0)),
|
||
|
|
rows: Array.isArray(entry?.rows) ? entry.rows.map((row) => String(row || "")) : [],
|
||
|
|
}))
|
||
|
|
.filter((entry) => entry.id);
|
||
|
|
}
|
||
|
|
|
||
|
|
export function buildWorldChunkLayerInstanceIds(roomLayers, instances, width, height) {
|
||
|
|
const safeWidth = Math.max(1, Math.floor(Number(width) || 1));
|
||
|
|
const safeHeight = Math.max(1, Math.floor(Number(height) || 1));
|
||
|
|
const nextLayers = new Map();
|
||
|
|
(Array.isArray(roomLayers) ? roomLayers : []).forEach((layer) => {
|
||
|
|
const layerNumber = Math.max(0, Math.floor(Number(layer?.layer) || 0));
|
||
|
|
nextLayers.set(layerNumber, {
|
||
|
|
layer: layerNumber,
|
||
|
|
name: typeof layer?.name === "string" && layer.name.trim() ? layer.name.trim() : undefined,
|
||
|
|
rows: normalizeWorldChunkRows(layer?.rows, safeWidth, safeHeight, layerNumber === 0 ? "." : " "),
|
||
|
|
instanceIds: [],
|
||
|
|
});
|
||
|
|
});
|
||
|
|
if (!nextLayers.has(0)) {
|
||
|
|
nextLayers.set(0, {
|
||
|
|
layer: 0,
|
||
|
|
rows: normalizeWorldChunkRows([], safeWidth, safeHeight, "."),
|
||
|
|
instanceIds: [],
|
||
|
|
});
|
||
|
|
}
|
||
|
|
if (!Array.from(nextLayers.keys()).some((layerNumber) => layerNumber > 0)) {
|
||
|
|
nextLayers.set(1, {
|
||
|
|
layer: 1,
|
||
|
|
rows: normalizeWorldChunkRows([], safeWidth, safeHeight, " "),
|
||
|
|
instanceIds: [],
|
||
|
|
});
|
||
|
|
}
|
||
|
|
(Array.isArray(instances) ? instances : []).forEach((entry) => {
|
||
|
|
const layerNumber = Math.max(0, Math.floor(Number(entry?.layer) || 0));
|
||
|
|
const instanceId = String(entry?.id || "").trim();
|
||
|
|
if (!instanceId) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (!nextLayers.has(layerNumber)) {
|
||
|
|
nextLayers.set(layerNumber, {
|
||
|
|
layer: layerNumber,
|
||
|
|
rows: normalizeWorldChunkRows([], safeWidth, safeHeight, layerNumber === 0 ? "." : " "),
|
||
|
|
instanceIds: [],
|
||
|
|
});
|
||
|
|
}
|
||
|
|
nextLayers.get(layerNumber).instanceIds.push(instanceId);
|
||
|
|
});
|
||
|
|
return Array.from(nextLayers.values())
|
||
|
|
.map((entry) => ({
|
||
|
|
...entry,
|
||
|
|
instanceIds: Array.from(new Set((Array.isArray(entry.instanceIds) ? entry.instanceIds : []).map((id) => String(id || "").trim()).filter(Boolean))),
|
||
|
|
}))
|
||
|
|
.sort((left, right) => (Number(left.layer) || 0) - (Number(right.layer) || 0));
|
||
|
|
}
|
||
|
|
|
||
|
|
export function normalizeWorldChunkInstances({ sourceInstances, chunkX, chunkY, width, height, options, cloneValue, runtimeUniqueId }) {
|
||
|
|
const config = options && typeof options === "object" ? options : {};
|
||
|
|
const duplicateIds = config.duplicateIds === true;
|
||
|
|
const safeChunkX = Math.floor(Number(chunkX) || 0);
|
||
|
|
const safeChunkY = Math.floor(Number(chunkY) || 0);
|
||
|
|
const safeWidth = Math.max(1, Math.floor(Number(width) || 1));
|
||
|
|
const safeHeight = Math.max(1, Math.floor(Number(height) || 1));
|
||
|
|
return (Array.isArray(sourceInstances) ? sourceInstances : [])
|
||
|
|
.map((entry) => {
|
||
|
|
const record = entry?.record && typeof entry.record === "object" && !Array.isArray(entry.record)
|
||
|
|
? cloneValue(entry.record)
|
||
|
|
: {};
|
||
|
|
const nextId = duplicateIds
|
||
|
|
? runtimeUniqueId()
|
||
|
|
: (String(entry?.id || record?.id || runtimeUniqueId()).trim() || runtimeUniqueId());
|
||
|
|
const nextLayer = Math.max(0, Math.floor(Number(entry?.layer ?? record?.layer) || 0));
|
||
|
|
const nextX = Math.max(0, Math.min(safeWidth - 1, Math.floor(Number(entry?.x) || 0)));
|
||
|
|
const nextY = Math.max(0, Math.min(safeHeight - 1, Math.floor(Number(entry?.y) || 0)));
|
||
|
|
const nextTemplateId = String(entry?.templateId || record?.templateId || "").trim();
|
||
|
|
record.id = nextId;
|
||
|
|
record.layer = nextLayer;
|
||
|
|
record.templateId = nextTemplateId;
|
||
|
|
record.position = {
|
||
|
|
x: (safeChunkX * safeWidth) + nextX,
|
||
|
|
y: (safeChunkY * safeHeight) + nextY,
|
||
|
|
};
|
||
|
|
return {
|
||
|
|
id: nextId,
|
||
|
|
templateId: nextTemplateId,
|
||
|
|
layer: nextLayer,
|
||
|
|
x: nextX,
|
||
|
|
y: nextY,
|
||
|
|
record,
|
||
|
|
};
|
||
|
|
})
|
||
|
|
.filter((entry) => entry.id);
|
||
|
|
}
|
||
|
|
|
||
|
|
export function createEmptyWorldChunkPayload({ chunkX, chunkY, chunkWidth, chunkHeight, worldId }) {
|
||
|
|
const safeChunkX = Math.floor(Number(chunkX) || 0);
|
||
|
|
const safeChunkY = Math.floor(Number(chunkY) || 0);
|
||
|
|
return {
|
||
|
|
schemaVersion: 1,
|
||
|
|
worldId: String(worldId || "").trim(),
|
||
|
|
chunkX: safeChunkX,
|
||
|
|
chunkY: safeChunkY,
|
||
|
|
width: chunkWidth,
|
||
|
|
height: chunkHeight,
|
||
|
|
backgroundTileId: "",
|
||
|
|
roomLayers: [
|
||
|
|
{
|
||
|
|
layer: 0,
|
||
|
|
rows: Array.from({ length: chunkHeight }, () => ".".repeat(chunkWidth)),
|
||
|
|
instanceIds: [],
|
||
|
|
},
|
||
|
|
{
|
||
|
|
layer: 1,
|
||
|
|
rows: Array.from({ length: chunkHeight }, () => " ".repeat(chunkWidth)),
|
||
|
|
instanceIds: [],
|
||
|
|
},
|
||
|
|
],
|
||
|
|
heightLayers: [],
|
||
|
|
instances: [],
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
export function normalizeCachedWorldChunkPayload({ chunkPayload, chunkX, chunkY, chunkWidth, chunkHeight, worldId, cloneValue, runtimeUniqueId, options }) {
|
||
|
|
const safeChunkX = Math.floor(Number(chunkX ?? chunkPayload?.chunkX) || 0);
|
||
|
|
const safeChunkY = Math.floor(Number(chunkY ?? chunkPayload?.chunkY) || 0);
|
||
|
|
const safeWidth = Math.max(1, Math.floor(Number(chunkPayload?.width) || Number(chunkWidth) || 32));
|
||
|
|
const safeHeight = Math.max(1, Math.floor(Number(chunkPayload?.height) || Number(chunkHeight) || 32));
|
||
|
|
const instances = normalizeWorldChunkInstances({
|
||
|
|
sourceInstances: chunkPayload?.instances,
|
||
|
|
chunkX: safeChunkX,
|
||
|
|
chunkY: safeChunkY,
|
||
|
|
width: safeWidth,
|
||
|
|
height: safeHeight,
|
||
|
|
options,
|
||
|
|
cloneValue,
|
||
|
|
runtimeUniqueId,
|
||
|
|
});
|
||
|
|
const roomLayers = buildWorldChunkLayerInstanceIds(chunkPayload?.roomLayers, instances, safeWidth, safeHeight);
|
||
|
|
return {
|
||
|
|
schemaVersion: Math.max(1, Math.floor(Number(chunkPayload?.schemaVersion) || 1)),
|
||
|
|
worldId: String(chunkPayload?.worldId || worldId || "").trim(),
|
||
|
|
chunkX: safeChunkX,
|
||
|
|
chunkY: safeChunkY,
|
||
|
|
width: safeWidth,
|
||
|
|
height: safeHeight,
|
||
|
|
backgroundTileId: String(chunkPayload?.backgroundTileId || "").trim(),
|
||
|
|
roomLayers,
|
||
|
|
heightLayers: cloneWorldChunkHeightLayers(chunkPayload?.heightLayers),
|
||
|
|
instances,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
export function isChunkFillSymbol(ch, fillChar) {
|
||
|
|
const symbol = String(ch || "").charAt(0);
|
||
|
|
return !symbol || symbol === fillChar || symbol === "." || symbol === " ";
|
||
|
|
}
|
||
|
|
|
||
|
|
export function isWorldChunkPayloadEmpty({ chunkPayload, chunkWidth, chunkHeight, worldId, cloneValue, runtimeUniqueId }) {
|
||
|
|
const normalized = normalizeCachedWorldChunkPayload({
|
||
|
|
chunkPayload,
|
||
|
|
chunkX: chunkPayload?.chunkX,
|
||
|
|
chunkY: chunkPayload?.chunkY,
|
||
|
|
chunkWidth,
|
||
|
|
chunkHeight,
|
||
|
|
worldId,
|
||
|
|
cloneValue,
|
||
|
|
runtimeUniqueId,
|
||
|
|
});
|
||
|
|
if (String(normalized?.backgroundTileId || "").trim()) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
if (Array.isArray(normalized?.instances) && normalized.instances.length > 0) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
if ((Array.isArray(normalized?.heightLayers) ? normalized.heightLayers : []).some((entry) => (
|
||
|
|
Array.isArray(entry?.rows) && entry.rows.some((row) => /[^ .]/.test(String(row || "")))
|
||
|
|
))) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
return !(Array.isArray(normalized?.roomLayers) ? normalized.roomLayers : []).some((layer) => {
|
||
|
|
const fillChar = (Number(layer?.layer) || 0) === 0 ? "." : " ";
|
||
|
|
return (Array.isArray(layer?.rows) ? layer.rows : []).some((row) => {
|
||
|
|
const sourceRow = String(row || "");
|
||
|
|
for (let index = 0; index < sourceRow.length; index += 1) {
|
||
|
|
if (!isChunkFillSymbol(sourceRow.charAt(index), fillChar)) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
export function transformChunkLocalCoord(localX, localY, width, height, operation) {
|
||
|
|
const safeX = Math.floor(Number(localX) || 0);
|
||
|
|
const safeY = Math.floor(Number(localY) || 0);
|
||
|
|
const safeWidth = Math.max(1, Math.floor(Number(width) || 1));
|
||
|
|
const safeHeight = Math.max(1, Math.floor(Number(height) || 1));
|
||
|
|
switch (String(operation || "").trim()) {
|
||
|
|
case "flipHorizontal":
|
||
|
|
return { x: (safeWidth - 1) - safeX, y: safeY };
|
||
|
|
case "flipVertical":
|
||
|
|
return { x: safeX, y: (safeHeight - 1) - safeY };
|
||
|
|
case "rotate180":
|
||
|
|
return { x: (safeWidth - 1) - safeX, y: (safeHeight - 1) - safeY };
|
||
|
|
case "rotate90cw":
|
||
|
|
if (safeWidth !== safeHeight) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
return { x: (safeWidth - 1) - safeY, y: safeX };
|
||
|
|
case "rotate90ccw":
|
||
|
|
if (safeWidth !== safeHeight) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
return { x: safeY, y: (safeHeight - 1) - safeX };
|
||
|
|
default:
|
||
|
|
return { x: safeX, y: safeY };
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export function transformChunkRows(rows, width, height, fillChar, operation) {
|
||
|
|
const safeWidth = Math.max(1, Math.floor(Number(width) || 1));
|
||
|
|
const safeHeight = Math.max(1, Math.floor(Number(height) || 1));
|
||
|
|
const sourceRows = normalizeWorldChunkRows(rows, safeWidth, safeHeight, fillChar);
|
||
|
|
const nextRows = Array.from({ length: safeHeight }, () => Array.from({ length: safeWidth }, () => String(fillChar || " ").charAt(0) || " "));
|
||
|
|
for (let rowIndex = 0; rowIndex < safeHeight; rowIndex += 1) {
|
||
|
|
const sourceRow = sourceRows[rowIndex];
|
||
|
|
for (let columnIndex = 0; columnIndex < safeWidth; columnIndex += 1) {
|
||
|
|
const char = String(sourceRow.charAt(columnIndex) || fillChar).charAt(0) || String(fillChar || " ").charAt(0) || " ";
|
||
|
|
if (isChunkFillSymbol(char, fillChar)) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
const nextCoord = transformChunkLocalCoord(columnIndex, rowIndex, safeWidth, safeHeight, operation);
|
||
|
|
if (!nextCoord) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
nextRows[nextCoord.y][nextCoord.x] = char;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return nextRows.map((row) => row.join(""));
|
||
|
|
}
|
||
|
|
|
||
|
|
export function transformChunkHeightPatch(patch, width, height, operation) {
|
||
|
|
const safeWidth = Math.max(1, Math.floor(Number(width) || 1));
|
||
|
|
const safeHeight = Math.max(1, Math.floor(Number(height) || 1));
|
||
|
|
const sourceRows = Array.isArray(patch?.rows) ? patch.rows.map((row) => String(row || "")) : [];
|
||
|
|
const patchWidth = sourceRows.reduce((max, row) => Math.max(max, row.length), 0);
|
||
|
|
const patchHeight = sourceRows.length;
|
||
|
|
const transformedCells = [];
|
||
|
|
for (let localY = 0; localY < patchHeight; localY += 1) {
|
||
|
|
const row = sourceRows[localY] || "";
|
||
|
|
for (let localX = 0; localX < patchWidth; localX += 1) {
|
||
|
|
const char = String(row.charAt(localX) || " ").charAt(0) || " ";
|
||
|
|
if (char === " " || char === ".") {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
const worldX = Math.max(0, Math.floor(Number(patch?.x) || 0)) + localX;
|
||
|
|
const worldY = Math.max(0, Math.floor(Number(patch?.y) || 0)) + localY;
|
||
|
|
if (worldX < 0 || worldY < 0 || worldX >= safeWidth || worldY >= safeHeight) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
const nextCoord = transformChunkLocalCoord(worldX, worldY, safeWidth, safeHeight, operation);
|
||
|
|
if (!nextCoord) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
transformedCells.push({
|
||
|
|
x: nextCoord.x,
|
||
|
|
y: nextCoord.y,
|
||
|
|
char,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (transformedCells.length <= 0) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
const minX = transformedCells.reduce((min, entry) => Math.min(min, entry.x), transformedCells[0].x);
|
||
|
|
const maxX = transformedCells.reduce((max, entry) => Math.max(max, entry.x), transformedCells[0].x);
|
||
|
|
const minY = transformedCells.reduce((min, entry) => Math.min(min, entry.y), transformedCells[0].y);
|
||
|
|
const maxY = transformedCells.reduce((max, entry) => Math.max(max, entry.y), transformedCells[0].y);
|
||
|
|
const nextRows = Array.from({ length: (maxY - minY) + 1 }, () => Array.from({ length: (maxX - minX) + 1 }, () => " "));
|
||
|
|
transformedCells.forEach((entry) => {
|
||
|
|
nextRows[entry.y - minY][entry.x - minX] = entry.char;
|
||
|
|
});
|
||
|
|
return {
|
||
|
|
id: String(patch?.id || "").trim(),
|
||
|
|
name: typeof patch?.name === "string" && patch.name.trim() ? patch.name.trim() : undefined,
|
||
|
|
z: Math.max(1, Math.floor(Number(patch?.z) || 1)),
|
||
|
|
x: minX,
|
||
|
|
y: minY,
|
||
|
|
rows: nextRows.map((row) => row.join("").replace(/\s+$/g, "")),
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
export function transformWorldChunkPayload({ chunkPayload, operation, chunkWidth, chunkHeight, worldId, cloneValue, runtimeUniqueId, options }) {
|
||
|
|
const config = options && typeof options === "object" ? options : {};
|
||
|
|
const normalized = normalizeCachedWorldChunkPayload({
|
||
|
|
chunkPayload,
|
||
|
|
chunkX: chunkPayload?.chunkX,
|
||
|
|
chunkY: chunkPayload?.chunkY,
|
||
|
|
chunkWidth,
|
||
|
|
chunkHeight,
|
||
|
|
worldId,
|
||
|
|
cloneValue,
|
||
|
|
runtimeUniqueId,
|
||
|
|
options: config,
|
||
|
|
});
|
||
|
|
const safeWidth = Math.max(1, Math.floor(Number(normalized?.width) || 1));
|
||
|
|
const safeHeight = Math.max(1, Math.floor(Number(normalized?.height) || 1));
|
||
|
|
const normalizedOperation = String(operation || "").trim();
|
||
|
|
if ((normalizedOperation === "rotate90cw" || normalizedOperation === "rotate90ccw") && safeWidth !== safeHeight) {
|
||
|
|
throw new Error("Chunk rotation requires square chunks.");
|
||
|
|
}
|
||
|
|
const instances = normalizeWorldChunkInstances({
|
||
|
|
sourceInstances: (Array.isArray(normalized.instances) ? normalized.instances : []).map((entry) => {
|
||
|
|
const nextCoord = transformChunkLocalCoord(entry.x, entry.y, safeWidth, safeHeight, normalizedOperation);
|
||
|
|
return {
|
||
|
|
...cloneValue(entry),
|
||
|
|
x: nextCoord?.x ?? entry.x,
|
||
|
|
y: nextCoord?.y ?? entry.y,
|
||
|
|
};
|
||
|
|
}),
|
||
|
|
chunkX: normalized.chunkX,
|
||
|
|
chunkY: normalized.chunkY,
|
||
|
|
width: safeWidth,
|
||
|
|
height: safeHeight,
|
||
|
|
options: config,
|
||
|
|
cloneValue,
|
||
|
|
runtimeUniqueId,
|
||
|
|
});
|
||
|
|
const roomLayers = buildWorldChunkLayerInstanceIds(
|
||
|
|
(Array.isArray(normalized.roomLayers) ? normalized.roomLayers : []).map((layer) => ({
|
||
|
|
...cloneValue(layer),
|
||
|
|
rows: transformChunkRows(layer?.rows, safeWidth, safeHeight, (Number(layer?.layer) || 0) === 0 ? "." : " ", normalizedOperation),
|
||
|
|
})),
|
||
|
|
instances,
|
||
|
|
safeWidth,
|
||
|
|
safeHeight,
|
||
|
|
);
|
||
|
|
const heightLayers = cloneWorldChunkHeightLayers(normalized.heightLayers)
|
||
|
|
.map((entry) => transformChunkHeightPatch(entry, safeWidth, safeHeight, normalizedOperation))
|
||
|
|
.filter(Boolean)
|
||
|
|
.sort((left, right) => {
|
||
|
|
if ((Number(left?.z) || 0) !== (Number(right?.z) || 0)) {
|
||
|
|
return (Number(left?.z) || 0) - (Number(right?.z) || 0);
|
||
|
|
}
|
||
|
|
return String(left?.name || left?.id || "").localeCompare(String(right?.name || right?.id || ""));
|
||
|
|
});
|
||
|
|
return {
|
||
|
|
...normalized,
|
||
|
|
roomLayers,
|
||
|
|
heightLayers,
|
||
|
|
instances,
|
||
|
|
};
|
||
|
|
}
|