Refactor launcher and studio modules
This commit is contained in:
parent
a20d298be2
commit
ec3e0f5138
34 changed files with 10300 additions and 8600 deletions
602
src/worldshaperStudio/worldChunkRuntimeHelpers.ts
Normal file
602
src/worldshaperStudio/worldChunkRuntimeHelpers.ts
Normal file
|
|
@ -0,0 +1,602 @@
|
|||
// @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,
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue