diff --git a/src/lib/figure-convert.test.ts b/src/lib/figure-convert.test.ts deleted file mode 100644 index 754666e67a..0000000000 --- a/src/lib/figure-convert.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { - convertFigurePart, - convertFigureString, - getNewFormatFigure, -} from "./figure-convert"; - -describe("convertFigurePart", () => { - it("converts head part", () => { - expect(convertFigurePart("hr-11782-40-40")).toBe("hd-180-1"); - expect(convertFigurePart("hr-11782-41-41")).toBe("hd-180-2"); - }); - - it("converts hair part", () => { - expect(convertFigurePart("hd-180-7-14")).toBe("hd-180-1"); - expect(convertFigurePart("hd-180-8-14")).toBe("hd-180-2"); - }); - - it("converts face part", () => { - expect(convertFigurePart("ch-11592-66")).toBe("ch-255-66"); - }); - - it("converts shirt part", () => { - expect(convertFigurePart("lg-10726-79-1408")).toBe("lg-280-110"); - }); - - it("converts shorts part", () => { - expect(convertFigurePart("sh-11764-1408")).toBe("sh-305-62"); - }); - - it("converts shoes part", () => { - expect(convertFigurePart("ha-11958-70-1408")).toBe("ha-310-70"); - }); - - it("converts waist part", () => { - expect(convertFigurePart("wa-2007-0")).toBe("wa-200-0"); - }); - - it("returns original for unknown parts", () => { - expect(convertFigurePart("unknown-part")).toBe("unknown-part"); - expect(convertFigurePart("hd-999-99")).toBe("hd-999-99"); - }); -}); - -describe("convertFigureString", () => { - it("converts full figure string from old to new format (with defaults: exclude shoes, waist, dedupe head)", () => { - const oldFigure = - "hr-11782-40-40.hd-180-7-14.ch-11592-66.lg-10726-79-1408.sh-11764-1408.ha-11958-70-1408.wa-2007-0"; - const newFigure = "hd-180-1.ch-255-66.lg-280-110.sh-305-62"; - expect(convertFigureString(oldFigure)).toBe(newFigure); - }); - - it("includes all parts when options disabled", () => { - const oldFigure = - "hr-11782-40-40.hd-180-7-14.ch-11592-66.lg-10726-79-1408.sh-11764-1408.ha-11958-70-1408.wa-2007-0"; - const newFigure = - "hd-180-1.hd-180-1.ch-255-66.lg-280-110.sh-305-62.ha-310-70.wa-200-0"; - expect( - convertFigureString(oldFigure, { - excludeShoes: false, - excludeWaist: false, - dedupeHead: false, - }), - ).toBe(newFigure); - }); - - it("handles empty string", () => { - expect(convertFigureString("")).toBe(""); - }); - - it("handles already converted figure", () => { - const newFigure = "hd-180-1.ch-255-66.lg-280-110"; - expect(convertFigureString(newFigure)).toBe(newFigure); - }); -}); - -describe("getNewFormatFigure", () => { - it("converts figure using getNewFormatFigure with defaults", () => { - const oldFigure = "hr-11782-40-40.hd-180-7-14.ch-11592-66"; - expect(getNewFormatFigure(oldFigure)).toBe("hd-180-1.ch-255-66"); - }); - - it("allows custom options", () => { - const oldFigure = "hr-11782-40-40.hd-180-7-14.ch-11592-66.ha-11958-70-1408"; - expect(getNewFormatFigure(oldFigure, { excludeShoes: false })).toBe( - "hd-180-1.ch-255-66.ha-310-70", - ); - }); -}); diff --git a/src/lib/figure-convert.ts b/src/lib/figure-convert.ts deleted file mode 100644 index 3db7899653..0000000000 --- a/src/lib/figure-convert.ts +++ /dev/null @@ -1,155 +0,0 @@ -/** - * Figure string converter: old Habbo format -> epicnabbo.nl format. - * - * Old format uses long IDs (e.g., hr-11782-40-40, hd-180-7-14) - * New format uses short IDs (e.g., hd-180-1, ch-255-66) - */ - -interface FigurePartMap { - [key: string]: string; -} - -const HEAD_MAP: FigurePartMap = { - "hr-11782-40-40": "hd-180-1", - "hr-11782-41-41": "hd-180-2", - "hr-11782-42-42": "hd-180-3", - "hr-11782-43-43": "hd-180-4", - "hr-11782-44-44": "hd-180-5", - "hr-11782-45-45": "hd-180-6", - "hr-11782-46-46": "hd-180-7", - "hr-11782-47-47": "hd-180-8", - "hr-11782-48-48": "hd-180-9", - "hr-11782-49-49": "hd-180-10", -}; - -const HAIR_MAP: FigurePartMap = { - "hd-180-7-14": "hd-180-1", - "hd-180-8-14": "hd-180-2", - "hd-180-9-14": "hd-180-3", - "hd-180-10-14": "hd-180-4", - "hd-180-11-14": "hd-180-5", - "hd-180-12-14": "hd-180-6", - "hd-180-13-14": "hd-180-7", - "hd-180-14-14": "hd-180-8", - "hd-180-15-14": "hd-180-9", - "hd-180-16-14": "hd-180-10", -}; - -const FACE_MAP: FigurePartMap = { - "ch-11592-66": "ch-255-66", - "ch-11593-66": "ch-255-67", - "ch-11594-66": "ch-255-68", - "ch-11595-66": "ch-255-69", -}; - -const SHIRT_MAP: FigurePartMap = { - "lg-10726-79-1408": "lg-280-110", - "lg-10727-79-1408": "lg-280-111", - "lg-10728-79-1408": "lg-280-112", - "lg-10729-79-1408": "lg-280-113", - "lg-10730-79-1408": "lg-280-114", -}; - -const SHORTS_MAP: FigurePartMap = { - "sh-11764-1408": "sh-305-62", - "sh-11765-1408": "sh-305-63", - "sh-11766-1408": "sh-305-64", - "sh-11767-1408": "sh-305-65", -}; - -const SHOES_MAP: FigurePartMap = { - "ha-11958-70-1408": "ha-310-70", - "ha-11959-70-1408": "ha-310-71", - "ha-11960-70-1408": "ha-310-72", -}; - -const WAIST_MAP: FigurePartMap = { - "wa-2007-0": "wa-200-0", -}; - -function getMapForPrefix(prefix: string): FigurePartMap | null { - if (prefix === "hr") return HEAD_MAP; - if (prefix === "hd") return HAIR_MAP; - if (prefix === "ch") return FACE_MAP; - if (prefix === "lg") return SHIRT_MAP; - if (prefix === "sh") return SHORTS_MAP; - if (prefix === "ha") return SHOES_MAP; - if (prefix === "wa") return WAIST_MAP; - return null; -} - -/** - * Convert a single figure part from old format to new format. - * Example: "hr-11782-40-40" -> "hd-180-1" - */ -export function convertFigurePart(part: string): string { - const dashIndex = part.indexOf("-"); - if (dashIndex === -1) return part; - - const prefix = part.slice(0, dashIndex); - const map = getMapForPrefix(prefix); - if (!map) return part; - - return map[part] ?? part; -} - -/** - * Convert a full figure string from old format to new format. - * Example: "hr-11782-40-40.hd-180-7-14.ch-11592-66.lg-10726-79-1408.sh-11764-1408.ha-11958-70-1408.wa-2007-0" - * -> "hd-180-1.hd-180-1.ch-255-66.lg-280-110.sh-305-62.ha-310-70.wa-200-0" - */ -export interface ConvertFigureOptions { - /** Exclude shoes (ha-) */ - excludeShoes?: boolean; - /** Exclude waist (wa-) */ - excludeWaist?: boolean; - /** Remove duplicate head/hair (keep first hd-) */ - dedupeHead?: boolean; -} - -export function convertFigureString( - figure: string, - options: ConvertFigureOptions = {}, -): string { - if (!figure) return figure; - - const { - excludeShoes = true, - excludeWaist = true, - dedupeHead = true, - } = options; - - const parts = figure.split("."); - const converted = parts.map(convertFigurePart); - - let result = converted; - if (excludeShoes) { - result = result.filter((p) => !p.startsWith("ha-")); - } - if (excludeWaist) { - result = result.filter((p) => !p.startsWith("wa-")); - } - if (dedupeHead) { - const seen = new Set(); - result = result.filter((p) => { - if (p.startsWith("hd-")) { - if (seen.has("hd")) return false; - seen.add("hd"); - } - return true; - }); - } - - return result.join("."); -} - -/** - * Get the new format figure string for a user's look. - * Handles both old and new format input. - */ -export function getNewFormatFigure( - look: string, - options?: ConvertFigureOptions, -): string { - return convertFigureString(look, options); -} diff --git a/src/lib/format.test.ts b/src/lib/format.test.ts index 20d924dbf1..6295790d27 100644 --- a/src/lib/format.test.ts +++ b/src/lib/format.test.ts @@ -37,6 +37,15 @@ describe("avatarImageUrl", () => { expect(url.startsWith("https://img.example.com?")).toBe(true); }); + it("preserves the exact user look instead of replacing it with a default figure", () => { + const look = + "hr-11782-40-40.hd-180-7-14.ch-11592-66.lg-10726-79-1408.sh-11764-1408.ha-11958-70-1408.wa-2007-0"; + + const url = new URL(avatarImageUrl(look)); + + expect(url.searchParams.get("figure")).toBe(look); + }); + it("uses & when the base already has a query string", () => { // This test is no longer applicable since avatarImageUrl doesn't accept a base parameter // The base URL is now resolved from env diff --git a/src/lib/format.ts b/src/lib/format.ts index 11b3cef5e1..f611fb87bb 100644 --- a/src/lib/format.ts +++ b/src/lib/format.ts @@ -1,11 +1,9 @@ -import { getNewFormatFigure } from "@/lib/figure-convert"; import { getAvatarUrl } from "@/lib/imager"; import type { AvatarOptions } from "@/types/admin"; /** Build a Habbo avatar-imager URL for a figure string. */ export function avatarImageUrl(look: string, opts: AvatarOptions = {}): string { - const figure = getNewFormatFigure(look); - return getAvatarUrl(figure, opts); + return getAvatarUrl(look, opts); } /** URL-safe slug from a title (lowercase, ascii, hyphenated). */ diff --git a/src/lib/imager.ts b/src/lib/imager.ts index 9e68bc6490..56297d9442 100644 --- a/src/lib/imager.ts +++ b/src/lib/imager.ts @@ -8,7 +8,6 @@ export type { AvatarOptions } from "@/types/admin"; import { env } from "@/env"; -import { getNewFormatFigure } from "@/lib/figure-convert"; import type { AvatarOptions } from "@/types/admin"; const DEFAULT_IMAGER_URL = "https://epicnabbo.nl/imaging/avatarimage"; @@ -28,14 +27,13 @@ export function resolveImagerBase(): string { /** * Build an avatar image URL using the configured imager. * Uses effect=14 and img_format=apng by default for epicnabbo.nl compatibility. - * Automatically converts old Habbo figure format to new epicnabbo.nl format. + * Preserves the user's stored figure so the rendered avatar matches the account. * Omits default parameters (direction=2, head_direction=3, size=m) for cleaner URLs. */ export function getAvatarUrl( figure: string, options: AvatarOptions = {}, ): string { - const convertedFigure = getNewFormatFigure(figure); const { size = "m", direction = 2, @@ -48,7 +46,7 @@ export function getAvatarUrl( } = options; const params = new URLSearchParams({ - figure: convertedFigure, + figure, img_format, effect: String(effect), });