1
0
Fork 0
Code Issues Pull requests Projects Releases Packages Wiki Activity Actions Pages

extract graphics transform and row encoding modules

This commit is contained in:
super-jalawii 2026-06-27 10:47:55 -04:00
parent 9f538b1543
commit 5a9723440c
17 changed files with 693 additions and 577 deletions

View file

@ -1,5 +1,5 @@
import { resolveUnifiedColorSymbol } from "../editorCore"; import { resolveUnifiedColorSymbol } from "../editorCore";
import type { JsonObject } from "../editorCore"; import type { JsonObject } from "../contracts/json";
export const TILE_COLORS: Record<string, string> = { export const TILE_COLORS: Record<string, string> = {
"#": resolveUnifiedColorSymbol("L", "#3d4f6a"), "#": resolveUnifiedColorSymbol("L", "#3d4f6a"),

View file

@ -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: "",
}),
],
});
});
});

View file

@ -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<string, JsonObject>();
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<string>();
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<string, JsonObject>();
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<string>();
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)),
};
}

6
src/contracts/json.ts Normal file
View file

@ -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);
}

View file

@ -1,98 +1,9 @@
import { import {
getSpriteRows,
normalizeImageRecordForSave,
normalizeImagesPayloadForSave,
normalizeTileRecordForSave,
resolveUnifiedColorSymbol, resolveUnifiedColorSymbol,
setUnifiedColorEntries, setUnifiedColorEntries,
} from "./editorCore"; } from "./editorCore";
import type { JsonObject, JsonValue } from "./editorCore";
describe("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", () => { it("updates the unified color lookup from catalog entries", () => {
setUnifiedColorEntries([ setUnifiedColorEntries([
{ key: "A", color: "#123456" }, { key: "A", color: "#123456" },

View file

@ -1,6 +1,24 @@
export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue }; import { getSpriteRows } from "./graphics/rowEncoding";
export type JsonObject = { [key: string]: JsonValue }; 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 = { export type CatalogEntry = {
entryId?: string; entryId?: string;
sourceKey?: string; sourceKey?: string;
@ -314,10 +332,6 @@ export function formatTypeLabel(type: string): string {
return TYPE_LABELS[type] || type.replaceAll("_", " "); 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 { export function buildDefaultRecord(activeType: string, records: JsonObject[]): JsonObject {
if (activeType === "quests") { if (activeType === "quests") {
const maxQuestId = records.reduce((acc, entry) => { const maxQuestId = records.reduce((acc, entry) => {
@ -440,455 +454,6 @@ export function getRecordLabel(record: JsonObject, index: number): string {
return `Record ${index + 1}`; 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<string, JsonObject>();
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<string>();
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<string, JsonObject>();
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<string>();
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<string, string> { export function getSpritePalette(record?: JsonObject): Record<string, string> {
void record; void record;
const palette: Record<string, string> = { const palette: Record<string, string> = {
@ -1021,20 +586,6 @@ export function toFieldLabel(rawKey: string): string {
return withSpaces.charAt(0).toUpperCase() + withSpaces.slice(1); 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 { export function getCatalogEntryIdValue(entry: CatalogEntry | null | undefined, fallback = ""): string {
return String(entry?.key || entry?.sourceKey || entry?.originalName || fallback).trim(); return String(entry?.key || entry?.sourceKey || entry?.originalName || fallback).trim();
} }

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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_INDEX_SCHEMA_VERSION = 1;
export const WORLD_SCHEMA_VERSION = 1; export const WORLD_SCHEMA_VERSION = 1;

View file

@ -1,12 +1,11 @@
import { import {
buildSpritesPayloadFromImagesPayload,
buildTilesPayloadFromImagesPayload,
buildDefaultRecord, buildDefaultRecord,
buildSpritePreviewDataUrl, buildSpritePreviewDataUrl,
fetchJsonOrThrow, fetchJsonOrThrow,
normalizeNpcRecordForLoad, normalizeNpcRecordForLoad,
type JsonObject,
} from "../editorCore"; } from "../editorCore";
import type { JsonObject } from "../contracts/json";
import { buildSpritesPayloadFromImagesPayload, buildTilesPayloadFromImagesPayload } from "../contentTransforms/graphicsPayload";
import type { import type {
HeightLayerPatchPayload, HeightLayerPatchPayload,
NpcOverlay, NpcOverlay,

View file

@ -1,11 +1,10 @@
import { import {
getSpriteRows,
normalizeImageRecordForSave, normalizeImageRecordForSave,
normalizeImagesPayloadForSave, normalizeImagesPayloadForSave,
normalizeTileRecordForSave, normalizeTileRecordForSave,
type JsonObject, } from "../contentTransforms/graphicsPayload";
type JsonValue, import { getSpriteRows } from "../graphics/rowEncoding";
} from "../editorCore"; import type { JsonObject, JsonValue } from "../contracts/json";
export type GraphicRole = "tile" | "sprite" | "other"; export type GraphicRole = "tile" | "sprite" | "other";

View file

@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */ /* eslint-disable @typescript-eslint/ban-ts-comment */
// @ts-nocheck // @ts-nocheck
import { mergeImagesPayloadWithSpritesPayload, mergeImagesPayloadWithTilesPayload } from "../editorCore"; import { mergeImagesPayloadWithSpritesPayload, mergeImagesPayloadWithTilesPayload } from "../contentTransforms/graphicsPayload";
const TILE_SYMBOL_POOL = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!$%&()*+,-/:;<=>?@[]^_{|}~="; const TILE_SYMBOL_POOL = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!$%&()*+,-/:;<=>?@[]^_{|}~=";

View file

@ -6,7 +6,7 @@ import {
buildTilesPayloadFromImagesPayload, buildTilesPayloadFromImagesPayload,
mergeImagesPayloadWithSpritesPayload, mergeImagesPayloadWithSpritesPayload,
mergeImagesPayloadWithTilesPayload, mergeImagesPayloadWithTilesPayload,
} from "../editorCore"; } from "../contentTransforms/graphicsPayload";
import { resizeRows } from "../components/worldshaperShared"; import { resizeRows } from "../components/worldshaperShared";
import { moveItemRelative } from "./reorderableListController"; import { moveItemRelative } from "./reorderableListController";

View file

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */ /* eslint-disable @typescript-eslint/ban-ts-comment */
// @ts-nocheck // @ts-nocheck
import { resolveUnifiedColorSymbol, getSpritePalette } from "../editorCore";
import { getSpritePalette, getSpriteRows, resolveUnifiedColorSymbol } from "../editorCore"; import { getSpriteRows } from "../graphics/rowEncoding";
export function parseHexColor(value, fallback = 0x060A14) { export function parseHexColor(value, fallback = 0x060A14) {
const raw = String(value || "").trim(); const raw = String(value || "").trim();

View file

@ -1,7 +1,6 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */ /* eslint-disable @typescript-eslint/ban-ts-comment */
// @ts-nocheck // @ts-nocheck
import { getSpriteRows } from "../graphics/rowEncoding";
import { getSpriteRows } from "../editorCore";
import { Application, Container, Sprite, Texture } from "pixi.js"; import { Application, Container, Sprite, Texture } from "pixi.js";
import { import {
applyPixelArtTexture, applyPixelArtTexture,

View file

@ -2,14 +2,16 @@
// @ts-nocheck // @ts-nocheck
import { import {
buildSpritePreviewDataUrl, buildSpritePreviewDataUrl,
fetchJsonOrThrow,
} from "../editorCore";
import {
buildSpritesPayloadFromImagesPayload, buildSpritesPayloadFromImagesPayload,
buildTilesPayloadFromImagesPayload, buildTilesPayloadFromImagesPayload,
fetchJsonOrThrow,
mergeImagesPayloadWithSpritesPayload, mergeImagesPayloadWithSpritesPayload,
mergeImagesPayloadWithTilesPayload, mergeImagesPayloadWithTilesPayload,
normalizeImageRecordForSave, normalizeImageRecordForSave,
normalizeTileRecordForSave, normalizeTileRecordForSave,
} from "../editorCore"; } from "../contentTransforms/graphicsPayload";
import { import {
buildSpriteCatalog, buildSpriteCatalog,
buildTileCatalogById, buildTileCatalogById,

View file

@ -3,12 +3,14 @@
import { import {
buildSpritePreviewDataUrl, buildSpritePreviewDataUrl,
getSpritePalette,
} from "../editorCore";
import {
buildSpritesPayloadFromImagesPayload, buildSpritesPayloadFromImagesPayload,
buildTilesPayloadFromImagesPayload, buildTilesPayloadFromImagesPayload,
normalizeImagePlayback, normalizeImagePlayback,
normalizeImageRecordForSave, normalizeImageRecordForSave,
getSpritePalette, } from "../contentTransforms/graphicsPayload";
} from "../editorCore";
import { import {
normalizeEditorTagValue, normalizeEditorTagValue,
normalizeEditorTags, normalizeEditorTags,