style: format code biome
Local Build and Deploy / deploy (push) Failing after 46s

This commit is contained in:
openhands committed 2026-07-13 21:57:41 +02:00
1 parent 8efd032cc6
commit df38dccbf1
735 files changed
+128321 -120870

No files matched your search

+298 -254
View File
@@ -5,38 +5,44 @@ import { encodePng, parseNitroBundle } from "@/lib/services/swf/nitro-builder";
// Pet .nitro can live in the CMS import target or the bundled asset set.
const PET_DIRS = [
path.join(/*turbopackIgnore: true*/ process.cwd(), "public/nitro-assets/bundled/pet"),
path.join(/*turbopackIgnore: true*/ process.cwd(), "public/Gamedata/bundled/pet"),
path.join(
/*turbopackIgnore: true*/ process.cwd(),
"public/nitro-assets/bundled/pet",
),
path.join(
/*turbopackIgnore: true*/ process.cwd(),
"public/Gamedata/bundled/pet",
),
];
interface Rect {
x: number;
y: number;
w: number;
h: number;
x: number;
y: number;
w: number;
h: number;
}
interface Asset {
x?: number;
y?: number;
source?: string;
x?: number;
y?: number;
source?: string;
}
interface AnimLayer {
frameSequences?: Record<string, { frames?: Record<string, { id?: number }> }>;
frameSequences?: Record<string, { frames?: Record<string, { id?: number }> }>;
}
interface Visualization {
size?: number;
layerCount?: number;
animations?: Record<string, { layers?: Record<string, AnimLayer> }>;
size?: number;
layerCount?: number;
animations?: Record<string, { layers?: Record<string, AnimLayer> }>;
}
interface NitroJson {
name?: string;
assets?: Record<string, Asset>;
spritesheet?: { frames?: Record<string, { frame: Rect; rotated?: boolean }> };
visualizations?: Visualization[];
name?: string;
assets?: Record<string, Asset>;
spritesheet?: { frames?: Record<string, { frame: Rect; rotated?: boolean }> };
visualizations?: Visualization[];
}
interface LayerSpec {
letter: string;
frameId: number;
letter: string;
frameId: number;
}
// A pet is rendered as several stacked layers (a,b,c,d = body parts; sd = shadow).
@@ -47,11 +53,11 @@ const DIR_PREFERENCE = [2, 0, 4, 3, 1, 7, 6, 5];
const iconCache = new Map<string, Buffer | null>();
function findNitro(lib: string): string | null {
for (const dir of PET_DIRS) {
const p = path.join(/*turbopackIgnore: true*/ dir, `${lib}.nitro`);
if (existsSync(/*turbopackIgnore: true*/ p)) return p;
}
return null;
for (const dir of PET_DIRS) {
const p = path.join(/*turbopackIgnore: true*/ dir, `${lib}.nitro`);
if (existsSync(/*turbopackIgnore: true*/ p)) return p;
}
return null;
}
/**
@@ -62,67 +68,84 @@ function findNitro(lib: string): string | null {
* into a junk pile. Falls back to layers a–d at frame 0 when no visualization.
*/
function idleLayerSpecs(json: NitroJson): LayerSpec[] {
const viz = json.visualizations?.find((v) => v.size === 64);
if (!viz || !viz.layerCount) {
return ["a", "b", "c", "d"].map((letter) => ({ letter, frameId: 0 }));
}
const anim0 = viz.animations?.["0"];
const raw: { letter: string; alPresent: boolean; hasSeq: boolean; frameId: number }[] = [];
let anyWithSeq = false;
for (let i = 0; i < viz.layerCount && i < 4; i++) {
const letter = String.fromCharCode(97 + i); // a, b, c, d
const al = anim0?.layers?.[String(i)];
const seqKeys = al ? Object.keys(al.frameSequences ?? {}) : [];
const hasSeq = seqKeys.length > 0;
if (hasSeq) anyWithSeq = true;
const frameId = hasSeq ? Number(al?.frameSequences?.[seqKeys[0]]?.frames?.["0"]?.id ?? 0) || 0 : 0;
raw.push({ letter, alPresent: !!al, hasSeq, frameId });
}
// No idle animation defined anywhere → render every layer's static frame 0.
if (!anyWithSeq) return raw.map((r) => ({ letter: r.letter, frameId: 0 }));
// Idle animation exists → include only layers it drives (or layers with no
// entry); skip layers explicitly muted in idle (empty frameSequences).
return raw.filter((r) => r.hasSeq || !r.alPresent).map((r) => ({ letter: r.letter, frameId: r.frameId }));
const viz = json.visualizations?.find((v) => v.size === 64);
if (!viz?.layerCount) {
return ["a", "b", "c", "d"].map((letter) => ({ letter, frameId: 0 }));
}
const anim0 = viz.animations?.["0"];
const raw: {
letter: string;
alPresent: boolean;
hasSeq: boolean;
frameId: number;
}[] = [];
let anyWithSeq = false;
for (let i = 0; i < viz.layerCount && i < 4; i++) {
const letter = String.fromCharCode(97 + i); // a, b, c, d
const al = anim0?.layers?.[String(i)];
const seqKeys = al ? Object.keys(al.frameSequences ?? {}) : [];
const hasSeq = seqKeys.length > 0;
if (hasSeq) anyWithSeq = true;
const frameId = hasSeq
? Number(al?.frameSequences?.[seqKeys[0]]?.frames?.["0"]?.id ?? 0) || 0
: 0;
raw.push({ letter, alPresent: !!al, hasSeq, frameId });
}
// No idle animation defined anywhere → render every layer's static frame 0.
if (!anyWithSeq) return raw.map((r) => ({ letter: r.letter, frameId: 0 }));
// Idle animation exists → include only layers it drives (or layers with no
// entry); skip layers explicitly muted in idle (empty frameSequences).
return raw
.filter((r) => r.hasSeq || !r.alPresent)
.map((r) => ({ letter: r.letter, frameId: r.frameId }));
}
/** Resolve a layer asset to its spritesheet rect + placement offset. */
function resolveSprite(
json: NitroJson,
lib: string,
layer: string,
dir: number,
frameId: number,
json: NitroJson,
lib: string,
layer: string,
dir: number,
frameId: number,
): { rect: Rect; ox: number; oy: number } | null {
const assetName = `${lib}_64_${layer}_${dir}_${frameId}`;
const asset = json.assets?.[assetName];
if (!asset) return null;
// `source` aliases another asset's pixels; offset stays on this asset.
const pixelAsset = asset.source ?? assetName;
const frameKey = `${lib}_${pixelAsset}`;
const f = json.spritesheet?.frames?.[frameKey];
if (!f || f.rotated || f.frame.w <= 0 || f.frame.h <= 0) return null;
return { rect: f.frame, ox: asset.x ?? 0, oy: asset.y ?? 0 };
const assetName = `${lib}_64_${layer}_${dir}_${frameId}`;
const asset = json.assets?.[assetName];
if (!asset) return null;
// `source` aliases another asset's pixels; offset stays on this asset.
const pixelAsset = asset.source ?? assetName;
const frameKey = `${lib}_${pixelAsset}`;
const f = json.spritesheet?.frames?.[frameKey];
if (!f || f.rotated || f.frame.w <= 0 || f.frame.h <= 0) return null;
return { rect: f.frame, ox: asset.x ?? 0, oy: asset.y ?? 0 };
}
/** Alpha-over `src` (w×h RGBA) onto `dst` at (dx,dy) within a dstW-wide canvas. */
function blit(dst: Buffer, dstW: number, src: Buffer, w: number, h: number, dx: number, dy: number): void {
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
const s = (y * w + x) * 4;
const sa = src[s + 3];
if (sa === 0) continue;
const d = ((dy + y) * dstW + (dx + x)) * 4;
if (sa === 255) {
src.copy(dst, d, s, s + 4);
continue;
}
const ia = 255 - sa;
dst[d] = (src[s] * sa + dst[d] * ia) / 255;
dst[d + 1] = (src[s + 1] * sa + dst[d + 1] * ia) / 255;
dst[d + 2] = (src[s + 2] * sa + dst[d + 2] * ia) / 255;
dst[d + 3] = sa + (dst[d + 3] * ia) / 255;
}
}
function blit(
dst: Buffer,
dstW: number,
src: Buffer,
w: number,
h: number,
dx: number,
dy: number,
): void {
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
const s = (y * w + x) * 4;
const sa = src[s + 3];
if (sa === 0) continue;
const d = ((dy + y) * dstW + (dx + x)) * 4;
if (sa === 255) {
src.copy(dst, d, s, s + 4);
continue;
}
const ia = 255 - sa;
dst[d] = (src[s] * sa + dst[d] * ia) / 255;
dst[d + 1] = (src[s + 1] * sa + dst[d + 1] * ia) / 255;
dst[d + 2] = (src[s + 2] * sa + dst[d + 2] * ia) / 255;
dst[d + 3] = sa + (dst[d + 3] * ia) / 255;
}
}
}
// Real per-breed pet colour is applied at runtime (the .nitro stores white
@@ -131,60 +154,63 @@ function blit(dst: Buffer, dstW: number, src: Buffer, w: number, h: number, dx:
// per breed (an estimate, not the exact in-game value). Naturally-coloured
// pixels (white dog, brown horse, green plant) are left untouched.
const DEFAULT_PET_COLOR: Record<string, [number, number, number]> = {
lion: [216, 166, 72],
haloompa: [216, 166, 72],
pig: [240, 168, 178],
pigletbaby: [240, 168, 178],
bunnylove: [240, 168, 190],
dog: [196, 160, 120],
terrier: [196, 160, 120],
terrierbaby: [196, 160, 120],
puppybaby: [196, 160, 120],
bunnydepressed: [196, 160, 120],
gnome: [196, 160, 120],
fools: [196, 160, 120],
cat: [156, 152, 158],
kittenbaby: [156, 152, 158],
monkey: [150, 108, 74],
demonmonkey: [150, 108, 74],
bear: [150, 112, 80],
bearbaby: [150, 112, 80],
horse: [162, 122, 86],
rhino: [150, 150, 158],
elephants: [150, 150, 158],
spider: [96, 84, 96],
bunnyevil: [120, 120, 128],
pigeonevil: [120, 120, 130],
pigeongood: [176, 176, 182],
bunnyeaster: [220, 214, 214],
cow: [206, 186, 162],
chicken: [238, 226, 176],
frog: [120, 184, 92],
dragon: [120, 172, 110],
croco: [128, 176, 108],
turtle: [130, 172, 120],
monster: [128, 176, 100],
monsterplant: [120, 170, 96],
pterosaur: [128, 176, 108],
velociraptor: [128, 176, 108],
lion: [216, 166, 72],
haloompa: [216, 166, 72],
pig: [240, 168, 178],
pigletbaby: [240, 168, 178],
bunnylove: [240, 168, 190],
dog: [196, 160, 120],
terrier: [196, 160, 120],
terrierbaby: [196, 160, 120],
puppybaby: [196, 160, 120],
bunnydepressed: [196, 160, 120],
gnome: [196, 160, 120],
fools: [196, 160, 120],
cat: [156, 152, 158],
kittenbaby: [156, 152, 158],
monkey: [150, 108, 74],
demonmonkey: [150, 108, 74],
bear: [150, 112, 80],
bearbaby: [150, 112, 80],
horse: [162, 122, 86],
rhino: [150, 150, 158],
elephants: [150, 150, 158],
spider: [96, 84, 96],
bunnyevil: [120, 120, 128],
pigeonevil: [120, 120, 130],
pigeongood: [176, 176, 182],
bunnyeaster: [220, 214, 214],
cow: [206, 186, 162],
chicken: [238, 226, 176],
frog: [120, 184, 92],
dragon: [120, 172, 110],
croco: [128, 176, 108],
turtle: [130, 172, 120],
monster: [128, 176, 100],
monsterplant: [120, 170, 96],
pterosaur: [128, 176, 108],
velociraptor: [128, 176, 108],
};
const FALLBACK_PET_COLOR: [number, number, number] = [190, 158, 120];
function recolorMask(rgba: Buffer, [tr, tg, tb]: [number, number, number]): void {
for (let i = 0; i < rgba.length; i += 4) {
if (rgba[i + 3] === 0) continue;
const r = rgba[i];
const g = rgba[i + 1];
const b = rgba[i + 2];
// Placeholder mask: highly saturated toward magenta/red (green far below
// red/blue). Tint to the breed colour, preserving the mask's shading.
if (Math.max(r, b) - g > 60) {
const l = Math.max(r, g, b) / 255;
rgba[i] = Math.round(tr * l);
rgba[i + 1] = Math.round(tg * l);
rgba[i + 2] = Math.round(tb * l);
}
}
function recolorMask(
rgba: Buffer,
[tr, tg, tb]: [number, number, number],
): void {
for (let i = 0; i < rgba.length; i += 4) {
if (rgba[i + 3] === 0) continue;
const r = rgba[i];
const g = rgba[i + 1];
const b = rgba[i + 2];
// Placeholder mask: highly saturated toward magenta/red (green far below
// red/blue). Tint to the breed colour, preserving the mask's shading.
if (Math.max(r, b) - g > 60) {
const l = Math.max(r, g, b) / 255;
rgba[i] = Math.round(tr * l);
rgba[i + 1] = Math.round(tg * l);
rgba[i + 2] = Math.round(tb * l);
}
}
}
/**
@@ -193,122 +219,138 @@ function recolorMask(rgba: Buffer, [tr, tg, tb]: [number, number, number]): void
* that don't align at this frame), then tight-crop to it. Turns "body + floating
* junk" into a clean centered icon.
*/
function keepLargestBlob(rgba: Buffer, w: number, h: number): { rgba: Buffer; w: number; h: number } {
const n = w * h;
const label = new Int32Array(n).fill(-1);
const opaque = (i: number) => rgba[i * 4 + 3] > 30;
const comps: { id: number; size: number }[] = [];
for (let start = 0; start < n; start++) {
if (label[start] !== -1 || !opaque(start)) continue;
const id = comps.length;
let size = 0;
const stack = [start];
label[start] = id;
while (stack.length) {
const p = stack.pop() as number;
size++;
const x = p % w;
const y = (p / w) | 0;
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
const nx = x + dx;
const ny = y + dy;
if (nx < 0 || ny < 0 || nx >= w || ny >= h) continue;
const np = ny * w + nx;
if (label[np] === -1 && opaque(np)) {
label[np] = id;
stack.push(np);
}
}
}
}
comps.push({ id, size });
}
if (comps.length === 0) return { rgba, w, h };
let main = comps[0];
for (const c of comps) if (c.size > main.size) main = c;
function keepLargestBlob(
rgba: Buffer,
w: number,
h: number,
): { rgba: Buffer; w: number; h: number } {
const n = w * h;
const label = new Int32Array(n).fill(-1);
const opaque = (i: number) => rgba[i * 4 + 3] > 30;
const comps: { id: number; size: number }[] = [];
for (let start = 0; start < n; start++) {
if (label[start] !== -1 || !opaque(start)) continue;
const id = comps.length;
let size = 0;
const stack = [start];
label[start] = id;
while (stack.length) {
const p = stack.pop() as number;
size++;
const x = p % w;
const y = (p / w) | 0;
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
const nx = x + dx;
const ny = y + dy;
if (nx < 0 || ny < 0 || nx >= w || ny >= h) continue;
const np = ny * w + nx;
if (label[np] === -1 && opaque(np)) {
label[np] = id;
stack.push(np);
}
}
}
}
comps.push({ id, size });
}
if (comps.length === 0) return { rgba, w, h };
let main = comps[0];
for (const c of comps) if (c.size > main.size) main = c;
// Tight-crop bbox of the main blob.
let minX = w;
let minY = h;
let maxX = -1;
let maxY = -1;
for (let p = 0; p < n; p++) {
if (label[p] !== main.id) continue;
const x = p % w;
const y = (p / w) | 0;
if (x < minX) minX = x;
if (x > maxX) maxX = x;
if (y < minY) minY = y;
if (y > maxY) maxY = y;
}
const nw = maxX - minX + 1;
const nh = maxY - minY + 1;
const out = Buffer.alloc(nw * nh * 4);
for (let y = 0; y < nh; y++) {
for (let x = 0; x < nw; x++) {
const sp = (minY + y) * w + (minX + x);
if (label[sp] === main.id) {
const o = (y * nw + x) * 4;
const si = sp * 4;
rgba.copy(out, o, si, si + 4);
}
}
}
return { rgba: out, w: nw, h: nh };
// Tight-crop bbox of the main blob.
let minX = w;
let minY = h;
let maxX = -1;
let maxY = -1;
for (let p = 0; p < n; p++) {
if (label[p] !== main.id) continue;
const x = p % w;
const y = (p / w) | 0;
if (x < minX) minX = x;
if (x > maxX) maxX = x;
if (y < minY) minY = y;
if (y > maxY) maxY = y;
}
const nw = maxX - minX + 1;
const nh = maxY - minY + 1;
const out = Buffer.alloc(nw * nh * 4);
for (let y = 0; y < nh; y++) {
for (let x = 0; x < nw; x++) {
const sp = (minY + y) * w + (minX + x);
if (label[sp] === main.id) {
const o = (y * nw + x) * 4;
const si = sp * 4;
rgba.copy(out, o, si, si + 4);
}
}
}
return { rgba: out, w: nw, h: nh };
}
/** Composite all non-shadow body layers of one direction's base pose into a PNG. */
function composite(json: NitroJson, sheet: { width: number; rgba: Buffer }): Buffer | null {
const lib = (json.name ?? "").toString();
if (!lib) return null;
function composite(
json: NitroJson,
sheet: { width: number; rgba: Buffer },
): Buffer | null {
const lib = (json.name ?? "").toString();
if (!lib) return null;
type Sprite = { rect: Rect; ox: number; oy: number };
const bestDir = (specs: LayerSpec[]): Sprite[] => {
let best: Sprite[] = [];
for (const dir of DIR_PREFERENCE) {
const resolved = specs
.map((s) => resolveSprite(json, lib, s.letter, dir, s.frameId))
.filter((s): s is Sprite => s !== null);
if (resolved.length > best.length) {
best = resolved;
if (dir === DIR_PREFERENCE[0]) break; // front pose is good enough
}
}
return best;
};
type Sprite = { rect: Rect; ox: number; oy: number };
const bestDir = (specs: LayerSpec[]): Sprite[] => {
let best: Sprite[] = [];
for (const dir of DIR_PREFERENCE) {
const resolved = specs
.map((s) => resolveSprite(json, lib, s.letter, dir, s.frameId))
.filter((s): s is Sprite => s !== null);
if (resolved.length > best.length) {
best = resolved;
if (dir === DIR_PREFERENCE[0]) break; // front pose is good enough
}
}
return best;
};
// Prefer the idle-pose layers; if nothing resolves, fall back to every
// layer's static frame 0 (covers pets whose frame ids don't line up).
let sprites = bestDir(idleLayerSpecs(json));
if (sprites.length === 0) {
sprites = bestDir(["a", "b", "c", "d"].map((letter) => ({ letter, frameId: 0 })));
}
if (sprites.length === 0) return null;
// Prefer the idle-pose layers; if nothing resolves, fall back to every
// layer's static frame 0 (covers pets whose frame ids don't line up).
let sprites = bestDir(idleLayerSpecs(json));
if (sprites.length === 0) {
sprites = bestDir(
["a", "b", "c", "d"].map((letter) => ({ letter, frameId: 0 })),
);
}
if (sprites.length === 0) return null;
// Union bounding box in pet-local coords.
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
for (const s of sprites) {
minX = Math.min(minX, s.ox);
minY = Math.min(minY, s.oy);
maxX = Math.max(maxX, s.ox + s.rect.w);
maxY = Math.max(maxY, s.oy + s.rect.h);
}
const cw = maxX - minX;
const ch = maxY - minY;
if (cw <= 0 || ch <= 0 || cw > 256 || ch > 256) return null;
// Union bounding box in pet-local coords.
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
for (const s of sprites) {
minX = Math.min(minX, s.ox);
minY = Math.min(minY, s.oy);
maxX = Math.max(maxX, s.ox + s.rect.w);
maxY = Math.max(maxY, s.oy + s.rect.h);
}
const cw = maxX - minX;
const ch = maxY - minY;
if (cw <= 0 || ch <= 0 || cw > 256 || ch > 256) return null;
const canvas = Buffer.alloc(cw * ch * 4); // transparent
for (const s of sprites) {
const px = cropRgba(sheet.rgba, sheet.width, s.rect.x, s.rect.y, s.rect.w, s.rect.h);
blit(canvas, cw, px, s.rect.w, s.rect.h, s.ox - minX, s.oy - minY);
}
recolorMask(canvas, DEFAULT_PET_COLOR[lib] ?? FALLBACK_PET_COLOR);
const blob = keepLargestBlob(canvas, cw, ch);
return encodePng(blob.w, blob.h, blob.rgba);
const canvas = Buffer.alloc(cw * ch * 4); // transparent
for (const s of sprites) {
const px = cropRgba(
sheet.rgba,
sheet.width,
s.rect.x,
s.rect.y,
s.rect.w,
s.rect.h,
);
blit(canvas, cw, px, s.rect.w, s.rect.h, s.ox - minX, s.oy - minY);
}
recolorMask(canvas, DEFAULT_PET_COLOR[lib] ?? FALLBACK_PET_COLOR);
const blob = keepLargestBlob(canvas, cw, ch);
return encodePng(blob.w, blob.h, blob.rgba);
}
/**
@@ -316,23 +358,25 @@ function composite(json: NitroJson, sheet: { width: number; rgba: Buffer }): Buf
* if the pet has no bundle / no usable frames. Cached in-process.
*/
export function getPetIconPng(lib: string): Buffer | null {
if (!/^[A-Za-z0-9_]+$/.test(lib)) return null;
const cached = iconCache.get(lib);
if (cached !== undefined) return cached;
if (!/^[A-Za-z0-9_]+$/.test(lib)) return null;
const cached = iconCache.get(lib);
if (cached !== undefined) return cached;
const nitroPath = findNitro(lib);
if (!nitroPath) {
iconCache.set(lib, null);
return null;
}
try {
const { json, png } = parseNitroBundle(readFileSync(/*turbopackIgnore: true*/ nitroPath));
const sheet = decodePng(png);
const icon = composite(json as NitroJson, sheet);
iconCache.set(lib, icon);
return icon;
} catch {
iconCache.set(lib, null);
return null;
}
const nitroPath = findNitro(lib);
if (!nitroPath) {
iconCache.set(lib, null);
return null;
}
try {
const { json, png } = parseNitroBundle(
readFileSync(/*turbopackIgnore: true*/ nitroPath),
);
const sheet = decodePng(png);
const icon = composite(json as NitroJson, sheet);
iconCache.set(lib, icon);
return icon;
} catch {
iconCache.set(lib, null);
return null;
}
}