feat(catalog): inspect Nitro scales and preview original sprites
This commit is contained in:
1 parent
2619bec165
commit
08b0b5021e
6 files changed
+544
-3
No files matched your search
@@ -0,0 +1,189 @@
|
||||
export interface ScaleIssue {
|
||||
asset?: string;
|
||||
message: string;
|
||||
}
|
||||
export interface ScaleReport {
|
||||
size: 32 | 64;
|
||||
state: "missing" | "issues" | "present";
|
||||
assets: string[];
|
||||
animations: string[];
|
||||
directions: string[];
|
||||
issues: ScaleIssue[];
|
||||
}
|
||||
export interface NitroQuality {
|
||||
scales: ScaleReport[];
|
||||
}
|
||||
export const object = (value: unknown): Record<string, unknown> =>
|
||||
value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: {};
|
||||
export function resolveNitroFrame(
|
||||
metadata: Record<string, unknown>,
|
||||
name: string,
|
||||
): {
|
||||
name: string;
|
||||
asset: Record<string, unknown>;
|
||||
frame: Record<string, unknown>;
|
||||
flipH: boolean;
|
||||
flipV: boolean;
|
||||
} {
|
||||
const assets = object(metadata.assets),
|
||||
frames = object(object(metadata.spritesheet).frames),
|
||||
seen = new Set<string>();
|
||||
let current = name,
|
||||
flipH = false,
|
||||
flipV = false;
|
||||
for (let depth = 0; depth < 100; depth++) {
|
||||
if (seen.has(current)) throw Error("Cyclic sprite reference");
|
||||
seen.add(current);
|
||||
const asset = object(assets[current]);
|
||||
flipH = flipH !== Boolean(asset.flipH);
|
||||
flipV = flipV !== Boolean(asset.flipV);
|
||||
if (typeof asset.source === "string" && asset.source !== current) {
|
||||
current = asset.source;
|
||||
continue;
|
||||
}
|
||||
const frame = object(frames[current]);
|
||||
if (Object.keys(frame).length)
|
||||
return {
|
||||
name: current,
|
||||
asset: object(assets[name]),
|
||||
frame,
|
||||
flipH,
|
||||
flipV,
|
||||
};
|
||||
throw Error(`Missing texture frame: ${current}`);
|
||||
}
|
||||
throw Error("Sprite reference chain is too long");
|
||||
}
|
||||
export function inspectNitroScales(
|
||||
metadata: Record<string, unknown>,
|
||||
width: number,
|
||||
height: number,
|
||||
): NitroQuality {
|
||||
const assets = object(metadata.assets),
|
||||
visualizations = Array.isArray(metadata.visualizations)
|
||||
? metadata.visualizations
|
||||
: [];
|
||||
return {
|
||||
scales: ([32, 64] as const).map((size) => {
|
||||
const names = Object.keys(assets)
|
||||
.filter((name) => name.includes(`_${size}_`))
|
||||
.sort();
|
||||
const viz = object(visualizations.find((v) => object(v).size === size)),
|
||||
issues: ScaleIssue[] = [];
|
||||
if (names.length && !Object.keys(viz).length)
|
||||
issues.push({
|
||||
message: "Sprites exist but the scale visualization is missing",
|
||||
});
|
||||
if (!names.length && Object.keys(viz).length)
|
||||
issues.push({
|
||||
message:
|
||||
"Visualization exists but no sprites were found for this scale",
|
||||
});
|
||||
for (const name of names) {
|
||||
const asset = object(assets[name]);
|
||||
if (
|
||||
["x", "y"].some(
|
||||
(key) =>
|
||||
asset[key] !== undefined &&
|
||||
(typeof asset[key] !== "number" || !Number.isFinite(asset[key])),
|
||||
)
|
||||
)
|
||||
issues.push({ asset: name, message: "Invalid sprite offset" });
|
||||
try {
|
||||
const resolved = resolveNitroFrame(metadata, name),
|
||||
rect = object(resolved.frame.frame);
|
||||
if (resolved.name.includes(`_${size === 32 ? 64 : 32}_`))
|
||||
issues.push({
|
||||
asset: name,
|
||||
message:
|
||||
"Sprite references the other scale; this is not native artwork at this scale",
|
||||
});
|
||||
const { x, y, w, h } = rect;
|
||||
if (
|
||||
![x, y, w, h].every(
|
||||
(n) => typeof n === "number" && Number.isInteger(n),
|
||||
) ||
|
||||
Number(x) < 0 ||
|
||||
Number(y) < 0 ||
|
||||
Number(w) <= 0 ||
|
||||
Number(h) <= 0 ||
|
||||
Number(x) + Number(w) > width ||
|
||||
Number(y) + Number(h) > height
|
||||
)
|
||||
issues.push({
|
||||
asset: name,
|
||||
message: "Texture frame is outside the image bounds",
|
||||
});
|
||||
} catch (error) {
|
||||
issues.push({ asset: name, message: (error as Error).message });
|
||||
}
|
||||
}
|
||||
const animations = Object.keys(object(viz.animations));
|
||||
for (const [id, animation] of Object.entries(object(viz.animations))) {
|
||||
const anim = object(animation);
|
||||
for (const key of ["transitionTo", "transitionFrom"])
|
||||
if (
|
||||
anim[key] !== undefined &&
|
||||
Number(anim[key]) >= 0 &&
|
||||
!animations.includes(String(anim[key]))
|
||||
)
|
||||
issues.push({
|
||||
message: `Animation ${id}: missing transition ${anim[key]}`,
|
||||
});
|
||||
}
|
||||
for (const [animationId, animation] of Object.entries(
|
||||
object(viz.animations),
|
||||
)) {
|
||||
for (const [layerId, layer] of Object.entries(
|
||||
object(object(animation).layers),
|
||||
)) {
|
||||
const layerName = /^\d+$/.test(layerId)
|
||||
? String.fromCharCode(97 + Number(layerId))
|
||||
: layerId;
|
||||
const frameIds = new Set<number>();
|
||||
for (const sequence of Object.values(
|
||||
object(object(layer).frameSequences),
|
||||
)) {
|
||||
for (const frame of Object.values(
|
||||
object(object(sequence).frames),
|
||||
)) {
|
||||
const id = object(frame).id;
|
||||
if (typeof id === "number" && Number.isInteger(id) && id >= 0)
|
||||
frameIds.add(id);
|
||||
}
|
||||
}
|
||||
for (const frameId of frameIds)
|
||||
if (
|
||||
!names.some(
|
||||
(name) =>
|
||||
name.includes(`_${size}_${layerName}_`) &&
|
||||
name.endsWith(`_${frameId}`),
|
||||
)
|
||||
)
|
||||
issues.push({
|
||||
message: `Animation ${animationId}: no sprite found for layer ${layerId}, frame ${frameId}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
const directions = [
|
||||
...new Set(
|
||||
names
|
||||
.map(
|
||||
(name) => name.match(new RegExp(`_${size}_[^_]+_([0-7])_`))?.[1],
|
||||
)
|
||||
.filter((v): v is string => v !== undefined),
|
||||
),
|
||||
].sort();
|
||||
return {
|
||||
size,
|
||||
state: issues.length ? "issues" : names.length ? "present" : "missing",
|
||||
assets: names,
|
||||
animations,
|
||||
directions,
|
||||
issues,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user