extract graphics transform and row encoding modules
This commit is contained in:
parent
9f538b1543
commit
5a9723440c
17 changed files with 693 additions and 577 deletions
|
|
@ -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"),
|
||||||
|
|
|
||||||
159
src/contentTransforms/graphicsPayload.test.ts
Normal file
159
src/contentTransforms/graphicsPayload.test.ts
Normal 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: "",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
413
src/contentTransforms/graphicsPayload.ts
Normal file
413
src/contentTransforms/graphicsPayload.ts
Normal 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
6
src/contracts/json.ts
Normal 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);
|
||||||
|
}
|
||||||
|
|
@ -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" },
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
52
src/graphics/rowEncoding.ts
Normal file
52
src/graphics/rowEncoding.ts
Normal 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);
|
||||||
|
}
|
||||||
23
src/shared/normalization.ts
Normal file
23
src/shared/normalization.ts
Normal 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);
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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!$%&()*+,-/:;<=>?@[]^_{|}~=";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue