diff --git a/src/components/worldshaperShared.ts b/src/components/worldshaperShared.ts index 29a1a70..b6ce820 100644 --- a/src/components/worldshaperShared.ts +++ b/src/components/worldshaperShared.ts @@ -1,5 +1,5 @@ import { resolveUnifiedColorSymbol } from "../editorCore"; -import type { JsonObject } from "../editorCore"; +import type { JsonObject } from "../contracts/json"; export const TILE_COLORS: Record = { "#": resolveUnifiedColorSymbol("L", "#3d4f6a"), diff --git a/src/contentTransforms/graphicsPayload.test.ts b/src/contentTransforms/graphicsPayload.test.ts new file mode 100644 index 0000000..0ebe701 --- /dev/null +++ b/src/contentTransforms/graphicsPayload.test.ts @@ -0,0 +1,159 @@ +import { + buildSpritesPayloadFromImagesPayload, + buildTilesPayloadFromImagesPayload, + mergeImagesPayloadWithTilesPayload, + normalizeImageRecordForSave, + normalizeImagesPayloadForSave, + normalizeTileRecordForSave, +} from "./graphicsPayload"; +import type { JsonObject, JsonValue } from "../contracts/json"; +import { getSpriteRows } from "../graphics/rowEncoding"; + +describe("graphicsPayload", () => { + it("normalizes tile records with padded rows and compatibility symbols", () => { + const result = normalizeTileRecordForSave({ + id: "tile_grass", + rows: ["A", "BC", "DROP"], + width: 2, + height: 2, + }); + + expect(result).toEqual(expect.objectContaining({ + id: "tile_grass", + symbol: "t", + rows: ["A.", "BC"], + width: 2, + height: 2, + })); + }); + + it("normalizes image records into frame-based storage while preserving tile compatibility", () => { + const result = normalizeImageRecordForSave({ + id: "tile_tree", + roles: ["tile", "sprite", "other", "tile"], + rows: ["A", "BC"], + width: 2, + height: 2, + speed: -10, + playback: "invalid", + tileSymbol: "$", + }); + + expect(result.rows).toBeUndefined(); + expect(result.symbol).toBeUndefined(); + expect(result.graphicRole).toBeUndefined(); + expect(result.roles).toEqual(["tile", "sprite"]); + expect(result.defaultFrame).toBe("frame_0"); + expect(result.speed).toBe(0); + expect(result.playback).toBe("normal"); + expect(result.tileSymbol).toBe("$"); + expect(result.frames).toEqual([ + expect.objectContaining({ + id: "frame_0", + rows: ["A.", "BC"], + }), + ]); + }); + + it("resolves sprite rows from the default enabled frame", () => { + const record: JsonObject = { + defaultFrame: "walk_1", + frames: [ + { id: "idle", rows: ["II"], enabled: true }, + { id: "walk_1", rows: ["WW"], enabled: true }, + { id: "disabled", rows: ["DD"], enabled: false }, + ], + }; + + expect(getSpriteRows(record)).toEqual(["WW"]); + }); + + it("normalizes image payload arrays without changing non-record entries", () => { + const payload: JsonValue = { + schemaVersion: 1, + images: [ + { + id: "sprite_hero", + roles: ["sprite"], + rows: ["X"], + }, + "leave-me-alone", + ], + }; + + expect(normalizeImagesPayloadForSave(payload)).toEqual({ + schemaVersion: 1, + images: [ + expect.objectContaining({ + id: "sprite_hero", + roles: ["sprite"], + defaultFrame: "frame_0", + }), + "leave-me-alone", + ], + }); + }); + + it("projects tiles and sprites from images while preserving current compatibility behavior", () => { + const imagesPayload: JsonValue = { + schemaVersion: 1, + images: [ + { + id: "tile_grass", + roles: ["tile"], + tileSymbol: "G", + width: 2, + height: 2, + frames: [{ id: "frame_0", rows: ["AA", "BB"] }], + }, + { + id: "sprite_hero", + roles: ["sprite"], + width: 1, + height: 1, + frames: [{ id: "frame_0", rows: ["H"] }], + }, + ], + }; + + expect(buildTilesPayloadFromImagesPayload(imagesPayload)).toEqual({ + schemaVersion: 1, + tiles: [expect.objectContaining({ id: "tile_grass", symbol: "G", rows: ["AA", "BB"] })], + }); + expect(buildSpritesPayloadFromImagesPayload(imagesPayload)).toEqual({ + schemaVersion: 1, + sprites: [expect.objectContaining({ id: "sprite_hero", rows: ["H"] })], + }); + }); + + it("merges tile payloads into images and removes tile role when a tile disappears", () => { + const merged = mergeImagesPayloadWithTilesPayload( + { + schemaVersion: 1, + images: [ + { + id: "shared", + roles: ["tile", "sprite"], + tileSymbol: "S", + frames: [{ id: "frame_0", rows: ["A"] }], + }, + ], + }, + { + schemaVersion: 1, + tiles: [], + }, + ); + + expect(merged).toEqual({ + schemaVersion: 1, + images: [ + expect.objectContaining({ + id: "shared", + roles: ["sprite"], + tileSymbol: "", + }), + ], + }); + }); +}); diff --git a/src/contentTransforms/graphicsPayload.ts b/src/contentTransforms/graphicsPayload.ts new file mode 100644 index 0000000..1e5c6ba --- /dev/null +++ b/src/contentTransforms/graphicsPayload.ts @@ -0,0 +1,413 @@ +import { isPlainObject, type JsonObject, type JsonValue } from "../contracts/json"; +import { getDirectSpriteRows, getRawImageFrames, getSpriteRows, normalizeRowsToSize } from "../graphics/rowEncoding"; +import { normalizeStringList } from "../shared/normalization"; + +const IMAGES_ROOT = "images"; +const SPRITES_ROOT = "sprites"; +const TILES_ROOT = "tiles"; + +function createRandomIdFragment(): string { + try { + const bytes = new Uint8Array(5); + crypto.getRandomValues(bytes); + return Array.from(bytes).map((value) => value.toString(16).padStart(2, "0")).join(""); + } catch { + return Math.random().toString(16).slice(2, 12); + } +} + +function normalizeImageRoles(value: JsonValue | undefined): string[] { + if (!Array.isArray(value)) { + return []; + } + return Array.from(new Set( + value + .map((entry) => String(entry || "").trim().toLowerCase()) + .filter((entry) => entry === "tile" || entry === "sprite"), + )); +} + +function normalizeSpriteLikeRecord(record: JsonObject, defaultIdPrefix: string): JsonObject { + const nextRecord: JsonObject = { ...record }; + const id = String(nextRecord.id ?? "").trim(); + nextRecord.id = id || `${defaultIdPrefix}_${createRandomIdFragment()}`; + + if (typeof nextRecord.name !== "string") { + nextRecord.name = ""; + } + + const width = Number(nextRecord.width); + const height = Number(nextRecord.height); + const pixelScale = Number(nextRecord.pixelScale); + const opacity = Number(nextRecord.opacity); + nextRecord.width = Number.isFinite(width) && width > 0 ? Math.floor(width) : 1; + nextRecord.height = Number.isFinite(height) && height > 0 ? Math.floor(height) : 1; + nextRecord.pixelScale = Number.isFinite(pixelScale) && pixelScale > 0 ? Math.floor(pixelScale) : 1; + nextRecord.opacity = Number.isFinite(opacity) ? Math.max(0, Math.min(1, opacity)) : 1; + + delete nextRecord.palette; + + nextRecord.rows = normalizeRowsToSize( + getDirectSpriteRows(nextRecord), + Number(nextRecord.width) || 1, + Number(nextRecord.height) || 1, + ) as unknown as JsonValue; + + return nextRecord; +} + +function normalizeImageFrameRecord( + frame: JsonObject, + fallbackRecord: JsonObject, + index: number, +): JsonObject { + const nextFrameId = String(frame.id || "").trim() || `frame_${index}`; + const normalizedFrame = normalizeSpriteLikeRecord({ + ...frame, + id: nextFrameId, + width: Number(frame.width) || Number(fallbackRecord.width) || 1, + height: Number(frame.height) || Number(fallbackRecord.height) || 1, + pixelScale: Number(frame.pixelScale) || Number(fallbackRecord.pixelScale) || 1, + opacity: Number(frame.opacity ?? fallbackRecord.opacity ?? 1), + rows: Array.isArray(frame.rows) ? frame.rows : getDirectSpriteRows(fallbackRecord), + }, "frame"); + return { + ...normalizedFrame, + id: nextFrameId, + enabled: frame.enabled !== false, + index: Number.isFinite(Number(frame.index)) ? Math.max(0, Math.floor(Number(frame.index))) : index, + }; +} + +export function normalizeSpritePayloadForSave(payload: JsonValue): JsonValue { + if (!isPlainObject(payload)) { + return payload; + } + const records = payload[SPRITES_ROOT]; + if (!Array.isArray(records)) { + return payload; + } + return { + ...payload, + [SPRITES_ROOT]: records.map((entry) => (isPlainObject(entry) ? normalizeSpriteLikeRecord(entry, "sprite") : entry)), + }; +} + +export function normalizeTileRecordForSave(record: JsonObject): JsonObject { + const nextRecord = normalizeSpriteLikeRecord(record, "tile"); + const symbol = String(nextRecord.symbol ?? "").trim().charAt(0); + nextRecord.symbol = symbol || String(nextRecord.id || "T").charAt(0) || "T"; + if (typeof nextRecord.description !== "string") { + nextRecord.description = ""; + } + return nextRecord; +} + +export function normalizeTilesPayloadForSave(payload: JsonValue): JsonValue { + if (!isPlainObject(payload)) { + return payload; + } + const records = payload[TILES_ROOT]; + if (!Array.isArray(records)) { + return payload; + } + return { + ...payload, + [TILES_ROOT]: records.map((entry) => (isPlainObject(entry) ? normalizeTileRecordForSave(entry) : entry)), + }; +} + +export function normalizeImagePlayback(value: JsonValue | undefined): "normal" | "rewind" | "stop" { + const normalized = String(value || "").trim().toLowerCase(); + if (normalized === "rewind" || normalized === "stop") { + return normalized; + } + return "normal"; +} + +export function normalizeImageRecordForSave(record: JsonObject): JsonObject { + const nextRecord = normalizeSpriteLikeRecord(record, "image"); + const roles = normalizeImageRoles(nextRecord.roles); + const inputFrames = getRawImageFrames(record); + const explicitRows = getDirectSpriteRows(record); + nextRecord.description = typeof nextRecord.description === "string" ? nextRecord.description : ""; + nextRecord.tags = normalizeStringList(nextRecord.tags); + nextRecord.roles = roles as unknown as JsonValue; + let normalizedFrames = inputFrames.map((entry, index) => normalizeImageFrameRecord(entry, nextRecord, index)); + if (normalizedFrames.length <= 0) { + normalizedFrames = [normalizeImageFrameRecord({ + id: "frame_0", + rows: explicitRows.length > 0 ? explicitRows : getDirectSpriteRows(nextRecord), + }, nextRecord, 0)]; + } + const requestedDefaultFrameId = String(record.defaultFrame || nextRecord.defaultFrame || "").trim(); + const resolvedDefaultFrameId = String( + normalizedFrames.find((entry) => String(entry.id || "").trim() === requestedDefaultFrameId)?.id + || normalizedFrames[0]?.id + || "frame_0", + ).trim() || "frame_0"; + if (explicitRows.length > 0) { + normalizedFrames = normalizedFrames.map((entry) => ( + String(entry.id || "").trim() !== resolvedDefaultFrameId + ? entry + : normalizeImageFrameRecord({ + ...entry, + id: resolvedDefaultFrameId, + rows: explicitRows, + width: Number(nextRecord.width) || 1, + height: Number(nextRecord.height) || 1, + pixelScale: Number(nextRecord.pixelScale) || 1, + opacity: Number(nextRecord.opacity ?? 1), + }, nextRecord, normalizedFrames.findIndex((candidate) => String(candidate.id || "").trim() === resolvedDefaultFrameId)) + )); + } + nextRecord.defaultFrame = resolvedDefaultFrameId; + nextRecord.speed = Number.isFinite(Number(record.speed)) && Number(record.speed) >= 0 ? Number(record.speed) : 0; + nextRecord.playback = normalizeImagePlayback(record.playback); + nextRecord.frames = normalizedFrames as unknown as JsonValue; + nextRecord.tileSymbol = roles.includes("tile") + ? (String(nextRecord.tileSymbol ?? nextRecord.symbol ?? "").trim().charAt(0) || String(nextRecord.id || "T").charAt(0) || "T") + : ""; + delete nextRecord.rows; + delete nextRecord.symbol; + delete nextRecord.graphicRole; + return nextRecord; +} + +export function normalizeImagesPayloadForSave(payload: JsonValue): JsonValue { + if (!isPlainObject(payload)) { + return payload; + } + const records = payload[IMAGES_ROOT]; + if (!Array.isArray(records)) { + return payload; + } + return { + ...payload, + [IMAGES_ROOT]: records.map((entry) => (isPlainObject(entry) ? normalizeImageRecordForSave(entry) : entry)), + }; +} + +export function buildTilesPayloadFromImagesPayload(payload: JsonValue): JsonObject { + const normalizedPayload = normalizeImagesPayloadForSave(payload); + const records = isPlainObject(normalizedPayload) && Array.isArray(normalizedPayload.images) + ? normalizedPayload.images + : []; + return { + schemaVersion: isPlainObject(normalizedPayload) && typeof normalizedPayload.schemaVersion === "number" ? normalizedPayload.schemaVersion : 1, + tiles: records + .filter((entry): entry is JsonObject => isPlainObject(entry)) + .filter((entry) => normalizeImageRoles(entry.roles).includes("tile")) + .map((entry) => normalizeTileRecordForSave({ + id: String(entry.id || "").trim(), + symbol: String(entry.tileSymbol || entry.symbol || "").trim().charAt(0), + name: String(entry.name || "").trim(), + description: String(entry.description || "").trim(), + width: Number(entry.width) || 16, + height: Number(entry.height) || 16, + pixelScale: Number(entry.pixelScale) || 1, + opacity: Number(entry.opacity ?? 1), + rows: getSpriteRows(entry), + tags: normalizeStringList(entry.tags), + })), + }; +} + +export function buildSpritesPayloadFromImagesPayload(payload: JsonValue): JsonObject { + const normalizedPayload = normalizeImagesPayloadForSave(payload); + const records = isPlainObject(normalizedPayload) && Array.isArray(normalizedPayload.images) + ? normalizedPayload.images + : []; + return { + schemaVersion: isPlainObject(normalizedPayload) && typeof normalizedPayload.schemaVersion === "number" ? normalizedPayload.schemaVersion : 1, + sprites: records + .filter((entry): entry is JsonObject => isPlainObject(entry)) + .filter((entry) => { + const roles = normalizeImageRoles(entry.roles); + return roles.includes("sprite") || roles.length === 0; + }) + .map((entry) => { + const roles = normalizeImageRoles(entry.roles); + return normalizeSpriteLikeRecord({ + id: String(entry.id || "").trim(), + name: String(entry.name || "").trim(), + description: String(entry.description || "").trim(), + width: Number(entry.width) || 16, + height: Number(entry.height) || 16, + pixelScale: Number(entry.pixelScale) || 1, + opacity: Number(entry.opacity ?? 1), + rows: getSpriteRows(entry), + tags: normalizeStringList(entry.tags), + graphicRole: roles.includes("sprite") ? "sprite" : "other", + }, "sprite"); + }), + }; +} + +export function mergeImagesPayloadWithTilesPayload(imagesPayload: JsonValue, tilesPayload: JsonValue): JsonObject { + const normalizedImagesPayload = normalizeImagesPayloadForSave(imagesPayload); + const normalizedTilesPayload = normalizeTilesPayloadForSave(tilesPayload); + const nextImagesById = new Map(); + const nextOrder: string[] = []; + const existingImages = isPlainObject(normalizedImagesPayload) && Array.isArray(normalizedImagesPayload.images) + ? normalizedImagesPayload.images + : []; + existingImages.forEach((entry) => { + if (!isPlainObject(entry)) { + return; + } + const normalizedEntry = normalizeImageRecordForSave(entry); + const id = String(normalizedEntry.id || "").trim(); + if (!id) { + return; + } + nextImagesById.set(id, normalizedEntry); + nextOrder.push(id); + }); + const incomingTiles = isPlainObject(normalizedTilesPayload) && Array.isArray(normalizedTilesPayload.tiles) + ? normalizedTilesPayload.tiles + : []; + const seenTileIds = new Set(); + incomingTiles.forEach((entry) => { + if (!isPlainObject(entry)) { + return; + } + const normalizedTile = normalizeTileRecordForSave(entry); + const id = String(normalizedTile.id || "").trim(); + if (!id) { + return; + } + seenTileIds.add(id); + const existing = nextImagesById.get(id); + const existingRoles = existing ? normalizeImageRoles(existing.roles) : []; + const nextImage = normalizeImageRecordForSave({ + ...(existing || {}), + id, + name: String(normalizedTile.name || existing?.name || "").trim(), + description: String(normalizedTile.description || existing?.description || "").trim(), + width: Number(normalizedTile.width) || Number(existing?.width) || 16, + height: Number(normalizedTile.height) || Number(existing?.height) || 16, + pixelScale: Number(normalizedTile.pixelScale) || Number(existing?.pixelScale) || 1, + opacity: Number(normalizedTile.opacity ?? existing?.opacity ?? 1), + rows: getSpriteRows(normalizedTile), + tags: normalizeStringList(normalizedTile.tags ?? existing?.tags), + roles: Array.from(new Set([...existingRoles, "tile"])), + tileSymbol: String(normalizedTile.symbol || existing?.tileSymbol || "").trim().charAt(0), + }); + if (!nextImagesById.has(id)) { + nextOrder.push(id); + } + nextImagesById.set(id, nextImage); + }); + Array.from(nextImagesById.entries()).forEach(([id, entry]) => { + const roles = normalizeImageRoles(entry.roles); + if (!roles.includes("tile") || seenTileIds.has(id)) { + return; + } + const nextRoles = roles.filter((role) => role !== "tile"); + if (nextRoles.length === 0) { + nextImagesById.delete(id); + return; + } + nextImagesById.set(id, normalizeImageRecordForSave({ + ...entry, + roles: nextRoles, + tileSymbol: "", + })); + }); + return { + schemaVersion: isPlainObject(normalizedImagesPayload) && typeof normalizedImagesPayload.schemaVersion === "number" ? normalizedImagesPayload.schemaVersion : 1, + images: nextOrder + .map((id) => nextImagesById.get(id)) + .filter((entry): entry is JsonObject => isPlainObject(entry)), + }; +} + +export function mergeImagesPayloadWithSpritesPayload(imagesPayload: JsonValue, spritesPayload: JsonValue): JsonObject { + const normalizedImagesPayload = normalizeImagesPayloadForSave(imagesPayload); + const normalizedSpritesPayload = normalizeSpritePayloadForSave(spritesPayload); + const nextImagesById = new Map(); + const nextOrder: string[] = []; + const existingImages = isPlainObject(normalizedImagesPayload) && Array.isArray(normalizedImagesPayload.images) + ? normalizedImagesPayload.images + : []; + existingImages.forEach((entry) => { + if (!isPlainObject(entry)) { + return; + } + const normalizedEntry = normalizeImageRecordForSave(entry); + const id = String(normalizedEntry.id || "").trim(); + if (!id) { + return; + } + nextImagesById.set(id, normalizedEntry); + nextOrder.push(id); + }); + const incomingSprites = isPlainObject(normalizedSpritesPayload) && Array.isArray(normalizedSpritesPayload.sprites) + ? normalizedSpritesPayload.sprites + : []; + const seenSpriteIds = new Set(); + incomingSprites.forEach((entry) => { + if (!isPlainObject(entry)) { + return; + } + const normalizedSprite = normalizeSpriteLikeRecord(entry, "sprite"); + const id = String(normalizedSprite.id || "").trim(); + if (!id) { + return; + } + seenSpriteIds.add(id); + const existing = nextImagesById.get(id); + const existingRoles = existing ? normalizeImageRoles(existing.roles) : []; + const wantsSpriteRole = String(normalizedSprite.graphicRole || "sprite").trim().toLowerCase() !== "other"; + const nextRoles = wantsSpriteRole + ? Array.from(new Set([...existingRoles, "sprite"])) + : existingRoles.filter((role) => role !== "sprite"); + const nextImage = normalizeImageRecordForSave({ + ...(existing || {}), + id, + name: String(normalizedSprite.name || existing?.name || "").trim(), + description: String(normalizedSprite.description || existing?.description || "").trim(), + width: Number(normalizedSprite.width) || Number(existing?.width) || 16, + height: Number(normalizedSprite.height) || Number(existing?.height) || 16, + pixelScale: Number(normalizedSprite.pixelScale) || Number(existing?.pixelScale) || 1, + opacity: Number(normalizedSprite.opacity ?? existing?.opacity ?? 1), + rows: getSpriteRows(normalizedSprite), + tags: normalizeStringList(normalizedSprite.tags ?? existing?.tags), + roles: nextRoles, + tileSymbol: String(existing?.tileSymbol || "").trim().charAt(0), + }); + if (!nextImagesById.has(id)) { + nextOrder.push(id); + } + if (nextRoles.length === 0 && !normalizeImageRoles(existing?.roles).includes("tile")) { + nextImagesById.delete(id); + return; + } + nextImagesById.set(id, nextImage); + }); + Array.from(nextImagesById.entries()).forEach(([id, entry]) => { + if (seenSpriteIds.has(id)) { + return; + } + const roles = normalizeImageRoles(entry.roles); + if (!roles.includes("sprite")) { + return; + } + const nextRoles = roles.filter((role) => role !== "sprite"); + if (nextRoles.length === 0) { + nextImagesById.delete(id); + return; + } + nextImagesById.set(id, normalizeImageRecordForSave({ + ...entry, + roles: nextRoles, + })); + }); + return { + schemaVersion: isPlainObject(normalizedImagesPayload) && typeof normalizedImagesPayload.schemaVersion === "number" ? normalizedImagesPayload.schemaVersion : 1, + images: nextOrder + .map((id) => nextImagesById.get(id)) + .filter((entry): entry is JsonObject => isPlainObject(entry)), + }; +} diff --git a/src/contracts/json.ts b/src/contracts/json.ts new file mode 100644 index 0000000..25b6c74 --- /dev/null +++ b/src/contracts/json.ts @@ -0,0 +1,6 @@ +export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue }; +export type JsonObject = { [key: string]: JsonValue }; + +export function isPlainObject(value: unknown): value is JsonObject { + return Boolean(value) && typeof value === "object" && !Array.isArray(value); +} diff --git a/src/editorCore.test.ts b/src/editorCore.test.ts index 62c645d..6e3f25c 100644 --- a/src/editorCore.test.ts +++ b/src/editorCore.test.ts @@ -1,98 +1,9 @@ import { - getSpriteRows, - normalizeImageRecordForSave, - normalizeImagesPayloadForSave, - normalizeTileRecordForSave, resolveUnifiedColorSymbol, setUnifiedColorEntries, } from "./editorCore"; -import type { JsonObject, JsonValue } from "./editorCore"; describe("editorCore", () => { - it("normalizes tile records with padded rows and compatibility symbols", () => { - const result = normalizeTileRecordForSave({ - id: "tile_grass", - rows: ["A", "BC", "DROP"], - width: 2, - height: 2, - }); - - expect(result).toEqual(expect.objectContaining({ - id: "tile_grass", - symbol: "t", - rows: ["A.", "BC"], - width: 2, - height: 2, - })); - }); - - it("normalizes image records into frame-based storage while preserving tile compatibility", () => { - const result = normalizeImageRecordForSave({ - id: "tile_tree", - roles: ["tile", "sprite", "other", "tile"], - rows: ["A", "BC"], - width: 2, - height: 2, - speed: -10, - playback: "invalid", - tileSymbol: "$", - }); - - expect(result.rows).toBeUndefined(); - expect(result.symbol).toBeUndefined(); - expect(result.graphicRole).toBeUndefined(); - expect(result.roles).toEqual(["tile", "sprite"]); - expect(result.defaultFrame).toBe("frame_0"); - expect(result.speed).toBe(0); - expect(result.playback).toBe("normal"); - expect(result.tileSymbol).toBe("$"); - expect(result.frames).toEqual([ - expect.objectContaining({ - id: "frame_0", - rows: ["A.", "BC"], - }), - ]); - }); - - it("resolves sprite rows from the default enabled frame", () => { - const record: JsonObject = { - defaultFrame: "walk_1", - frames: [ - { id: "idle", rows: ["II"], enabled: true }, - { id: "walk_1", rows: ["WW"], enabled: true }, - { id: "disabled", rows: ["DD"], enabled: false }, - ], - }; - - expect(getSpriteRows(record)).toEqual(["WW"]); - }); - - it("normalizes image payload arrays without changing non-record entries", () => { - const payload: JsonValue = { - schemaVersion: 1, - images: [ - { - id: "sprite_hero", - roles: ["sprite"], - rows: ["X"], - }, - "leave-me-alone", - ], - }; - - expect(normalizeImagesPayloadForSave(payload)).toEqual({ - schemaVersion: 1, - images: [ - expect.objectContaining({ - id: "sprite_hero", - roles: ["sprite"], - defaultFrame: "frame_0", - }), - "leave-me-alone", - ], - }); - }); - it("updates the unified color lookup from catalog entries", () => { setUnifiedColorEntries([ { key: "A", color: "#123456" }, diff --git a/src/editorCore.ts b/src/editorCore.ts index 61c2470..bc7d750 100644 --- a/src/editorCore.ts +++ b/src/editorCore.ts @@ -1,6 +1,24 @@ -export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue }; -export type JsonObject = { [key: string]: JsonValue }; +import { getSpriteRows } from "./graphics/rowEncoding"; +import { isPlainObject, type JsonObject, type JsonValue } from "./contracts/json"; + +export type { JsonObject, JsonValue } from "./contracts/json"; +export { isPlainObject } from "./contracts/json"; +export { normalizeHexColor, normalizeStringList, parseCsv } from "./shared/normalization"; +export { getSpriteRows } from "./graphics/rowEncoding"; +export { + buildSpritesPayloadFromImagesPayload, + buildTilesPayloadFromImagesPayload, + mergeImagesPayloadWithSpritesPayload, + mergeImagesPayloadWithTilesPayload, + normalizeImagePlayback, + normalizeImageRecordForSave, + normalizeImagesPayloadForSave, + normalizeSpritePayloadForSave, + normalizeTileRecordForSave, + normalizeTilesPayloadForSave, +} from "./contentTransforms/graphicsPayload"; + export type CatalogEntry = { entryId?: string; sourceKey?: string; @@ -314,10 +332,6 @@ export function formatTypeLabel(type: string): string { return TYPE_LABELS[type] || type.replaceAll("_", " "); } -export function isPlainObject(value: JsonValue | undefined): value is JsonObject { - return Boolean(value) && typeof value === "object" && !Array.isArray(value); -} - export function buildDefaultRecord(activeType: string, records: JsonObject[]): JsonObject { if (activeType === "quests") { const maxQuestId = records.reduce((acc, entry) => { @@ -440,455 +454,6 @@ export function getRecordLabel(record: JsonObject, index: number): string { return `Record ${index + 1}`; } -export function normalizeHexColor(value: JsonValue | undefined, fallback = "#7aa2ff"): string { - const raw = String(value || "").trim(); - if (/^#[0-9a-fA-F]{6}$/.test(raw)) { - return raw.toLowerCase(); - } - return fallback; -} - -function getDirectSpriteRows(record: JsonObject): string[] { - const rawRows = record.rows; - if (!Array.isArray(rawRows)) { - return []; - } - return rawRows.map((row) => String(row || "")); -} - -function getRawImageFrames(record: JsonObject): JsonObject[] { - if (!Array.isArray(record.frames)) { - return []; - } - return record.frames.filter((entry): entry is JsonObject => isPlainObject(entry)); -} - -function getSortedImageFrames(record: JsonObject): JsonObject[] { - return getRawImageFrames(record) - .map((entry, index) => ({ - entry, - sortIndex: Number.isFinite(Number(entry.index)) ? Number(entry.index) : index, - sourceIndex: index, - })) - .sort((left, right) => ( - left.sortIndex !== right.sortIndex - ? left.sortIndex - right.sortIndex - : left.sourceIndex - right.sourceIndex - )) - .map((entry) => entry.entry); -} - -export function getSpriteRows(record: JsonObject): string[] { - const frames = getSortedImageFrames(record); - if (frames.length > 0) { - const defaultFrameId = String(record.defaultFrame || "").trim(); - const enabledFrames = frames.filter((entry) => entry.enabled !== false); - const renderFrames = enabledFrames.length > 0 ? enabledFrames : frames; - const resolvedFrame = renderFrames.find((entry) => String(entry.id || "").trim() === defaultFrameId) || renderFrames[0]; - return getDirectSpriteRows(resolvedFrame); - } - return getDirectSpriteRows(record); -} - -function normalizeImageFrameRecord( - frame: JsonObject, - fallbackRecord: JsonObject, - index: number, -): JsonObject { - const nextFrameId = String(frame.id || "").trim() || `frame_${index}`; - const normalizedFrame = normalizeSpriteLikeRecord({ - ...frame, - id: nextFrameId, - width: Number(frame.width) || Number(fallbackRecord.width) || 1, - height: Number(frame.height) || Number(fallbackRecord.height) || 1, - pixelScale: Number(frame.pixelScale) || Number(fallbackRecord.pixelScale) || 1, - opacity: Number(frame.opacity ?? fallbackRecord.opacity ?? 1), - rows: Array.isArray(frame.rows) ? frame.rows : getDirectSpriteRows(fallbackRecord), - }, "frame"); - return { - ...normalizedFrame, - id: nextFrameId, - enabled: frame.enabled !== false, - index: Number.isFinite(Number(frame.index)) ? Math.max(0, Math.floor(Number(frame.index))) : index, - }; -} - -function normalizeSpriteLikeRecord(record: JsonObject, defaultIdPrefix: string): JsonObject { - const nextRecord: JsonObject = { ...record }; - const id = String(nextRecord.id ?? "").trim(); - nextRecord.id = id || `${defaultIdPrefix}_${genRandomId()}`; - - if (typeof nextRecord.name !== "string") { - nextRecord.name = ""; - } - - const width = Number(nextRecord.width); - const height = Number(nextRecord.height); - const pixelScale = Number(nextRecord.pixelScale); - const opacity = Number(nextRecord.opacity); - nextRecord.width = Number.isFinite(width) && width > 0 ? Math.floor(width) : 1; - nextRecord.height = Number.isFinite(height) && height > 0 ? Math.floor(height) : 1; - nextRecord.pixelScale = Number.isFinite(pixelScale) && pixelScale > 0 ? Math.floor(pixelScale) : 1; - nextRecord.opacity = Number.isFinite(opacity) ? Math.max(0, Math.min(1, opacity)) : 1; - - delete nextRecord.palette; - - const rows = getDirectSpriteRows(nextRecord); - const normalizedRows = Array.from({ length: Number(nextRecord.height) || 1 }, (_, rowIndex) => { - const base = rows[rowIndex] || ""; - return base.padEnd(Number(nextRecord.width) || 1, ".").slice(0, Number(nextRecord.width) || 1); - }); - nextRecord.rows = normalizedRows as unknown as JsonValue; - - return nextRecord; -} - -export function normalizeSpritePayloadForSave(payload: JsonValue): JsonValue { - if (!isPlainObject(payload)) { - return payload; - } - const root = ROOT_KEY_BY_TYPE.sprites; - const records = payload[root]; - if (!Array.isArray(records)) { - return payload; - } - return { - ...payload, - [root]: records.map((entry) => (isPlainObject(entry) ? normalizeSpriteLikeRecord(entry, "sprite") : entry)), - }; -} - -export function normalizeTileRecordForSave(record: JsonObject): JsonObject { - const nextRecord = normalizeSpriteLikeRecord(record, "tile"); - const symbol = String(nextRecord.symbol ?? "").trim().charAt(0); - nextRecord.symbol = symbol || String(nextRecord.id || "T").charAt(0) || "T"; - if (typeof nextRecord.description !== "string") { - nextRecord.description = ""; - } - return nextRecord; -} - -export function normalizeTilesPayloadForSave(payload: JsonValue): JsonValue { - if (!isPlainObject(payload)) { - return payload; - } - const root = ROOT_KEY_BY_TYPE.tiles; - const records = payload[root]; - if (!Array.isArray(records)) { - return payload; - } - return { - ...payload, - [root]: records.map((entry) => (isPlainObject(entry) ? normalizeTileRecordForSave(entry) : entry)), - }; -} - -function normalizeImageRoles(value: JsonValue | undefined): string[] { - if (!Array.isArray(value)) { - return []; - } - return Array.from(new Set( - value - .map((entry) => String(entry || "").trim().toLowerCase()) - .filter((entry) => entry === "tile" || entry === "sprite"), - )); -} - -export function normalizeImagePlayback(value: JsonValue | undefined): "normal" | "rewind" | "stop" { - const normalized = String(value || "").trim().toLowerCase(); - if (normalized === "rewind" || normalized === "stop") { - return normalized; - } - return "normal"; -} - -export function normalizeImageRecordForSave(record: JsonObject): JsonObject { - const nextRecord = normalizeSpriteLikeRecord(record, "image"); - const roles = normalizeImageRoles(nextRecord.roles); - const inputFrames = getRawImageFrames(record); - const explicitRows = getDirectSpriteRows(record); - nextRecord.description = typeof nextRecord.description === "string" ? nextRecord.description : ""; - nextRecord.tags = normalizeStringList(nextRecord.tags); - nextRecord.roles = roles as unknown as JsonValue; - let normalizedFrames = inputFrames.map((entry, index) => normalizeImageFrameRecord(entry, nextRecord, index)); - if (normalizedFrames.length <= 0) { - normalizedFrames = [normalizeImageFrameRecord({ - id: "frame_0", - rows: explicitRows.length > 0 ? explicitRows : getDirectSpriteRows(nextRecord), - }, nextRecord, 0)]; - } - const requestedDefaultFrameId = String(record.defaultFrame || nextRecord.defaultFrame || "").trim(); - const resolvedDefaultFrameId = String( - normalizedFrames.find((entry) => String(entry.id || "").trim() === requestedDefaultFrameId)?.id - || normalizedFrames[0]?.id - || "frame_0", - ).trim() || "frame_0"; - if (explicitRows.length > 0) { - normalizedFrames = normalizedFrames.map((entry) => ( - String(entry.id || "").trim() !== resolvedDefaultFrameId - ? entry - : normalizeImageFrameRecord({ - ...entry, - id: resolvedDefaultFrameId, - rows: explicitRows, - width: Number(nextRecord.width) || 1, - height: Number(nextRecord.height) || 1, - pixelScale: Number(nextRecord.pixelScale) || 1, - opacity: Number(nextRecord.opacity ?? 1), - }, nextRecord, normalizedFrames.findIndex((candidate) => String(candidate.id || "").trim() === resolvedDefaultFrameId)) - )); - } - nextRecord.defaultFrame = resolvedDefaultFrameId; - nextRecord.speed = Number.isFinite(Number(record.speed)) && Number(record.speed) >= 0 ? Number(record.speed) : 0; - nextRecord.playback = normalizeImagePlayback(record.playback); - nextRecord.frames = normalizedFrames as unknown as JsonValue; - nextRecord.tileSymbol = roles.includes("tile") - ? (String(nextRecord.tileSymbol ?? nextRecord.symbol ?? "").trim().charAt(0) || String(nextRecord.id || "T").charAt(0) || "T") - : ""; - delete nextRecord.rows; - delete nextRecord.symbol; - delete nextRecord.graphicRole; - return nextRecord; -} - -export function normalizeImagesPayloadForSave(payload: JsonValue): JsonValue { - if (!isPlainObject(payload)) { - return payload; - } - const records = payload.images; - if (!Array.isArray(records)) { - return payload; - } - return { - ...payload, - images: records.map((entry) => (isPlainObject(entry) ? normalizeImageRecordForSave(entry) : entry)), - }; -} - -export function buildTilesPayloadFromImagesPayload(payload: JsonValue): JsonObject { - const normalizedPayload = normalizeImagesPayloadForSave(payload); - const records = isPlainObject(normalizedPayload) && Array.isArray(normalizedPayload.images) - ? normalizedPayload.images - : []; - return { - schemaVersion: isPlainObject(normalizedPayload) && typeof normalizedPayload.schemaVersion === "number" ? normalizedPayload.schemaVersion : 1, - tiles: records - .filter((entry): entry is JsonObject => isPlainObject(entry)) - .filter((entry) => normalizeImageRoles(entry.roles).includes("tile")) - .map((entry) => normalizeTileRecordForSave({ - id: String(entry.id || "").trim(), - symbol: String(entry.tileSymbol || entry.symbol || "").trim().charAt(0), - name: String(entry.name || "").trim(), - description: String(entry.description || "").trim(), - width: Number(entry.width) || 16, - height: Number(entry.height) || 16, - pixelScale: Number(entry.pixelScale) || 1, - opacity: Number(entry.opacity ?? 1), - rows: getSpriteRows(entry), - tags: normalizeStringList(entry.tags), - })), - }; -} - -export function buildSpritesPayloadFromImagesPayload(payload: JsonValue): JsonObject { - const normalizedPayload = normalizeImagesPayloadForSave(payload); - const records = isPlainObject(normalizedPayload) && Array.isArray(normalizedPayload.images) - ? normalizedPayload.images - : []; - return { - schemaVersion: isPlainObject(normalizedPayload) && typeof normalizedPayload.schemaVersion === "number" ? normalizedPayload.schemaVersion : 1, - sprites: records - .filter((entry): entry is JsonObject => isPlainObject(entry)) - .filter((entry) => { - const roles = normalizeImageRoles(entry.roles); - return roles.includes("sprite") || roles.length === 0; - }) - .map((entry) => { - const roles = normalizeImageRoles(entry.roles); - return normalizeSpriteLikeRecord({ - id: String(entry.id || "").trim(), - name: String(entry.name || "").trim(), - description: String(entry.description || "").trim(), - width: Number(entry.width) || 16, - height: Number(entry.height) || 16, - pixelScale: Number(entry.pixelScale) || 1, - opacity: Number(entry.opacity ?? 1), - rows: getSpriteRows(entry), - tags: normalizeStringList(entry.tags), - graphicRole: roles.includes("sprite") ? "sprite" : "other", - }, "sprite"); - }), - }; -} - -export function mergeImagesPayloadWithTilesPayload(imagesPayload: JsonValue, tilesPayload: JsonValue): JsonObject { - const normalizedImagesPayload = normalizeImagesPayloadForSave(imagesPayload); - const normalizedTilesPayload = normalizeTilesPayloadForSave(tilesPayload); - const nextImagesById = new Map(); - const nextOrder: string[] = []; - const existingImages = isPlainObject(normalizedImagesPayload) && Array.isArray(normalizedImagesPayload.images) - ? normalizedImagesPayload.images - : []; - existingImages.forEach((entry) => { - if (!isPlainObject(entry)) { - return; - } - const normalizedEntry = normalizeImageRecordForSave(entry); - const id = String(normalizedEntry.id || "").trim(); - if (!id) { - return; - } - nextImagesById.set(id, normalizedEntry); - nextOrder.push(id); - }); - const incomingTiles = isPlainObject(normalizedTilesPayload) && Array.isArray(normalizedTilesPayload.tiles) - ? normalizedTilesPayload.tiles - : []; - const seenTileIds = new Set(); - incomingTiles.forEach((entry) => { - if (!isPlainObject(entry)) { - return; - } - const normalizedTile = normalizeTileRecordForSave(entry); - const id = String(normalizedTile.id || "").trim(); - if (!id) { - return; - } - seenTileIds.add(id); - const existing = nextImagesById.get(id); - const existingRoles = existing ? normalizeImageRoles(existing.roles) : []; - const nextImage = normalizeImageRecordForSave({ - ...(existing || {}), - id, - name: String(normalizedTile.name || existing?.name || "").trim(), - description: String(normalizedTile.description || existing?.description || "").trim(), - width: Number(normalizedTile.width) || Number(existing?.width) || 16, - height: Number(normalizedTile.height) || Number(existing?.height) || 16, - pixelScale: Number(normalizedTile.pixelScale) || Number(existing?.pixelScale) || 1, - opacity: Number(normalizedTile.opacity ?? existing?.opacity ?? 1), - rows: getSpriteRows(normalizedTile), - tags: normalizeStringList(normalizedTile.tags ?? existing?.tags), - roles: Array.from(new Set([...existingRoles, "tile"])), - tileSymbol: String(normalizedTile.symbol || existing?.tileSymbol || "").trim().charAt(0), - }); - if (!nextImagesById.has(id)) { - nextOrder.push(id); - } - nextImagesById.set(id, nextImage); - }); - Array.from(nextImagesById.entries()).forEach(([id, entry]) => { - const roles = normalizeImageRoles(entry.roles); - if (!roles.includes("tile") || seenTileIds.has(id)) { - return; - } - const nextRoles = roles.filter((role) => role !== "tile"); - if (nextRoles.length === 0) { - nextImagesById.delete(id); - return; - } - nextImagesById.set(id, normalizeImageRecordForSave({ - ...entry, - roles: nextRoles, - tileSymbol: "", - })); - }); - return { - schemaVersion: isPlainObject(normalizedImagesPayload) && typeof normalizedImagesPayload.schemaVersion === "number" ? normalizedImagesPayload.schemaVersion : 1, - images: nextOrder - .map((id) => nextImagesById.get(id)) - .filter((entry): entry is JsonObject => isPlainObject(entry)), - }; -} - -export function mergeImagesPayloadWithSpritesPayload(imagesPayload: JsonValue, spritesPayload: JsonValue): JsonObject { - const normalizedImagesPayload = normalizeImagesPayloadForSave(imagesPayload); - const normalizedSpritesPayload = normalizeSpritePayloadForSave(spritesPayload); - const nextImagesById = new Map(); - const nextOrder: string[] = []; - const existingImages = isPlainObject(normalizedImagesPayload) && Array.isArray(normalizedImagesPayload.images) - ? normalizedImagesPayload.images - : []; - existingImages.forEach((entry) => { - if (!isPlainObject(entry)) { - return; - } - const normalizedEntry = normalizeImageRecordForSave(entry); - const id = String(normalizedEntry.id || "").trim(); - if (!id) { - return; - } - nextImagesById.set(id, normalizedEntry); - nextOrder.push(id); - }); - const incomingSprites = isPlainObject(normalizedSpritesPayload) && Array.isArray(normalizedSpritesPayload.sprites) - ? normalizedSpritesPayload.sprites - : []; - const seenSpriteIds = new Set(); - incomingSprites.forEach((entry) => { - if (!isPlainObject(entry)) { - return; - } - const normalizedSprite = normalizeSpriteLikeRecord(entry, "sprite"); - const id = String(normalizedSprite.id || "").trim(); - if (!id) { - return; - } - seenSpriteIds.add(id); - const existing = nextImagesById.get(id); - const existingRoles = existing ? normalizeImageRoles(existing.roles) : []; - const wantsSpriteRole = String(normalizedSprite.graphicRole || "sprite").trim().toLowerCase() !== "other"; - const nextRoles = wantsSpriteRole - ? Array.from(new Set([...existingRoles, "sprite"])) - : existingRoles.filter((role) => role !== "sprite"); - const nextImage = normalizeImageRecordForSave({ - ...(existing || {}), - id, - name: String(normalizedSprite.name || existing?.name || "").trim(), - description: String(normalizedSprite.description || existing?.description || "").trim(), - width: Number(normalizedSprite.width) || Number(existing?.width) || 16, - height: Number(normalizedSprite.height) || Number(existing?.height) || 16, - pixelScale: Number(normalizedSprite.pixelScale) || Number(existing?.pixelScale) || 1, - opacity: Number(normalizedSprite.opacity ?? existing?.opacity ?? 1), - rows: getSpriteRows(normalizedSprite), - tags: normalizeStringList(normalizedSprite.tags ?? existing?.tags), - roles: nextRoles, - tileSymbol: String(existing?.tileSymbol || "").trim().charAt(0), - }); - if (!nextImagesById.has(id)) { - nextOrder.push(id); - } - if (nextRoles.length === 0 && !normalizeImageRoles(existing?.roles).includes("tile")) { - nextImagesById.delete(id); - return; - } - nextImagesById.set(id, nextImage); - }); - Array.from(nextImagesById.entries()).forEach(([id, entry]) => { - if (seenSpriteIds.has(id)) { - return; - } - const roles = normalizeImageRoles(entry.roles); - if (!roles.includes("sprite")) { - return; - } - const nextRoles = roles.filter((role) => role !== "sprite"); - if (nextRoles.length === 0) { - nextImagesById.delete(id); - return; - } - nextImagesById.set(id, normalizeImageRecordForSave({ - ...entry, - roles: nextRoles, - })); - }); - return { - schemaVersion: isPlainObject(normalizedImagesPayload) && typeof normalizedImagesPayload.schemaVersion === "number" ? normalizedImagesPayload.schemaVersion : 1, - images: nextOrder - .map((id) => nextImagesById.get(id)) - .filter((entry): entry is JsonObject => isPlainObject(entry)), - }; -} - export function getSpritePalette(record?: JsonObject): Record { void record; const palette: Record = { @@ -1021,20 +586,6 @@ export function toFieldLabel(rawKey: string): string { return withSpaces.charAt(0).toUpperCase() + withSpaces.slice(1); } -export function normalizeStringList(value: unknown): string[] { - if (!Array.isArray(value)) { - return []; - } - return Array.from(new Set(value.map((entry) => String(entry || "").trim()).filter(Boolean))); -} - -export function parseCsv(value: string): string[] { - return String(value || "") - .split(",") - .map((entry) => entry.trim()) - .filter(Boolean); -} - export function getCatalogEntryIdValue(entry: CatalogEntry | null | undefined, fallback = ""): string { return String(entry?.key || entry?.sourceKey || entry?.originalName || fallback).trim(); } diff --git a/src/graphics/rowEncoding.ts b/src/graphics/rowEncoding.ts new file mode 100644 index 0000000..a3c8739 --- /dev/null +++ b/src/graphics/rowEncoding.ts @@ -0,0 +1,52 @@ +import { isPlainObject, type JsonObject } from "../contracts/json"; + +export function getDirectSpriteRows(record: JsonObject): string[] { + const rawRows = record.rows; + if (!Array.isArray(rawRows)) { + return []; + } + return rawRows.map((row) => String(row || "")); +} + +export function normalizeRowsToSize(rows: string[], width: number, height: number, fillChar = "."): string[] { + 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 }, (_, rowIndex) => { + const base = String(rows[rowIndex] || ""); + return base.padEnd(safeWidth, fillChar).slice(0, safeWidth); + }); +} + +export function getRawImageFrames(record: JsonObject): JsonObject[] { + if (!Array.isArray(record.frames)) { + return []; + } + return record.frames.filter((entry): entry is JsonObject => isPlainObject(entry)); +} + +export function getSortedImageFrames(record: JsonObject): JsonObject[] { + return getRawImageFrames(record) + .map((entry, index) => ({ + entry, + sortIndex: Number.isFinite(Number(entry.index)) ? Number(entry.index) : index, + sourceIndex: index, + })) + .sort((left, right) => ( + left.sortIndex !== right.sortIndex + ? left.sortIndex - right.sortIndex + : left.sourceIndex - right.sourceIndex + )) + .map((entry) => entry.entry); +} + +export function getSpriteRows(record: JsonObject): string[] { + const frames = getSortedImageFrames(record); + if (frames.length > 0) { + const defaultFrameId = String(record.defaultFrame || "").trim(); + const enabledFrames = frames.filter((entry) => entry.enabled !== false); + const renderFrames = enabledFrames.length > 0 ? enabledFrames : frames; + const resolvedFrame = renderFrames.find((entry) => String(entry.id || "").trim() === defaultFrameId) || renderFrames[0]; + return getDirectSpriteRows(resolvedFrame); + } + return getDirectSpriteRows(record); +} diff --git a/src/shared/normalization.ts b/src/shared/normalization.ts new file mode 100644 index 0000000..5d1b26c --- /dev/null +++ b/src/shared/normalization.ts @@ -0,0 +1,23 @@ +import type { JsonValue } from "../contracts/json"; + +export function normalizeHexColor(value: JsonValue | undefined, fallback = "#7aa2ff"): string { + const raw = String(value || "").trim(); + if (/^#[0-9a-fA-F]{6}$/.test(raw)) { + return raw.toLowerCase(); + } + return fallback; +} + +export function normalizeStringList(value: unknown): string[] { + if (!Array.isArray(value)) { + return []; + } + return Array.from(new Set(value.map((entry) => String(entry || "").trim()).filter(Boolean))); +} + +export function parseCsv(value: string): string[] { + return String(value || "") + .split(",") + .map((entry) => entry.trim()) + .filter(Boolean); +} diff --git a/src/worldChunking.ts b/src/worldChunking.ts index f84be93..f413204 100644 --- a/src/worldChunking.ts +++ b/src/worldChunking.ts @@ -1,4 +1,4 @@ -import type { JsonObject } from "./editorCore"; +import type { JsonObject } from "./contracts/json"; export const WORLD_INDEX_SCHEMA_VERSION = 1; export const WORLD_SCHEMA_VERSION = 1; diff --git a/src/worldshaperStudio/bootstrap.ts b/src/worldshaperStudio/bootstrap.ts index e7d2cb9..0a24e66 100644 --- a/src/worldshaperStudio/bootstrap.ts +++ b/src/worldshaperStudio/bootstrap.ts @@ -1,12 +1,11 @@ import { - buildSpritesPayloadFromImagesPayload, - buildTilesPayloadFromImagesPayload, buildDefaultRecord, buildSpritePreviewDataUrl, fetchJsonOrThrow, normalizeNpcRecordForLoad, - type JsonObject, } from "../editorCore"; +import type { JsonObject } from "../contracts/json"; +import { buildSpritesPayloadFromImagesPayload, buildTilesPayloadFromImagesPayload } from "../contentTransforms/graphicsPayload"; import type { HeightLayerPatchPayload, NpcOverlay, diff --git a/src/worldshaperStudio/graphicsDocumentHelpers.ts b/src/worldshaperStudio/graphicsDocumentHelpers.ts index d3e122f..b131daf 100644 --- a/src/worldshaperStudio/graphicsDocumentHelpers.ts +++ b/src/worldshaperStudio/graphicsDocumentHelpers.ts @@ -1,11 +1,10 @@ import { - getSpriteRows, normalizeImageRecordForSave, normalizeImagesPayloadForSave, normalizeTileRecordForSave, - type JsonObject, - type JsonValue, -} from "../editorCore"; +} from "../contentTransforms/graphicsPayload"; +import { getSpriteRows } from "../graphics/rowEncoding"; +import type { JsonObject, JsonValue } from "../contracts/json"; export type GraphicRole = "tile" | "sprite" | "other"; diff --git a/src/worldshaperStudio/importController.ts b/src/worldshaperStudio/importController.ts index 79afe80..8cc5dca 100644 --- a/src/worldshaperStudio/importController.ts +++ b/src/worldshaperStudio/importController.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ // @ts-nocheck -import { mergeImagesPayloadWithSpritesPayload, mergeImagesPayloadWithTilesPayload } from "../editorCore"; +import { mergeImagesPayloadWithSpritesPayload, mergeImagesPayloadWithTilesPayload } from "../contentTransforms/graphicsPayload"; const TILE_SYMBOL_POOL = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!$%&()*+,-/:;<=>?@[]^_{|}~="; diff --git a/src/worldshaperStudio/mapDocumentController.ts b/src/worldshaperStudio/mapDocumentController.ts index abd77ed..f848e31 100644 --- a/src/worldshaperStudio/mapDocumentController.ts +++ b/src/worldshaperStudio/mapDocumentController.ts @@ -6,7 +6,7 @@ import { buildTilesPayloadFromImagesPayload, mergeImagesPayloadWithSpritesPayload, mergeImagesPayloadWithTilesPayload, -} from "../editorCore"; +} from "../contentTransforms/graphicsPayload"; import { resizeRows } from "../components/worldshaperShared"; import { moveItemRelative } from "./reorderableListController"; diff --git a/src/worldshaperStudio/pixiSurfaceHelpers.ts b/src/worldshaperStudio/pixiSurfaceHelpers.ts index 6f1ec19..f8e764c 100644 --- a/src/worldshaperStudio/pixiSurfaceHelpers.ts +++ b/src/worldshaperStudio/pixiSurfaceHelpers.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ // @ts-nocheck - -import { getSpritePalette, getSpriteRows, resolveUnifiedColorSymbol } from "../editorCore"; +import { resolveUnifiedColorSymbol, getSpritePalette } from "../editorCore"; +import { getSpriteRows } from "../graphics/rowEncoding"; export function parseHexColor(value, fallback = 0x060A14) { const raw = String(value || "").trim(); diff --git a/src/worldshaperStudio/pixiTileStageController.ts b/src/worldshaperStudio/pixiTileStageController.ts index 2da8c8f..9073a4d 100644 --- a/src/worldshaperStudio/pixiTileStageController.ts +++ b/src/worldshaperStudio/pixiTileStageController.ts @@ -1,7 +1,6 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ // @ts-nocheck - -import { getSpriteRows } from "../editorCore"; +import { getSpriteRows } from "../graphics/rowEncoding"; import { Application, Container, Sprite, Texture } from "pixi.js"; import { applyPixelArtTexture, diff --git a/src/worldshaperStudio/runtime.ts b/src/worldshaperStudio/runtime.ts index 3c6c8d0..63aadc9 100644 --- a/src/worldshaperStudio/runtime.ts +++ b/src/worldshaperStudio/runtime.ts @@ -2,14 +2,16 @@ // @ts-nocheck import { buildSpritePreviewDataUrl, + fetchJsonOrThrow, +} from "../editorCore"; +import { buildSpritesPayloadFromImagesPayload, buildTilesPayloadFromImagesPayload, - fetchJsonOrThrow, mergeImagesPayloadWithSpritesPayload, mergeImagesPayloadWithTilesPayload, normalizeImageRecordForSave, normalizeTileRecordForSave, -} from "../editorCore"; +} from "../contentTransforms/graphicsPayload"; import { buildSpriteCatalog, buildTileCatalogById, diff --git a/src/worldshaperStudio/tileArtEditorWindowController.ts b/src/worldshaperStudio/tileArtEditorWindowController.ts index 597f0ee..136373d 100644 --- a/src/worldshaperStudio/tileArtEditorWindowController.ts +++ b/src/worldshaperStudio/tileArtEditorWindowController.ts @@ -3,12 +3,14 @@ import { buildSpritePreviewDataUrl, + getSpritePalette, +} from "../editorCore"; +import { buildSpritesPayloadFromImagesPayload, buildTilesPayloadFromImagesPayload, normalizeImagePlayback, normalizeImageRecordForSave, - getSpritePalette, -} from "../editorCore"; +} from "../contentTransforms/graphicsPayload"; import { normalizeEditorTagValue, normalizeEditorTags,