/** * Layout — shared domain-aware formatting for all embed types. * Wraps format.ts functions with business logic (Config, Bringer, Leaves). * * Usage: * import { Layout } from "@ui/layout"; * * Layout.wrank(entry, goal, context) * Layout.tgCount(done, goal) * Layout.kd(k, d) * Layout.bringer(char) * Layout.cockroach(char, historyKey) * Layout.indicators(char, { historyKey }) * Layout.formatRow(template, tokens) */ import { Character, Nation } from "@types"; import { WRankEntry, WRankWeek } from "@systems/wrank"; import { Bringer } from "@systems/bringer"; import { Leaves } from "@systems/leaves"; import { Emoji } from "@systems/emojis"; import { format } from "@format"; import { TGKey } from "@systems/tg-key"; // ─── Context ────────────────────────────────────────────────────────────────── export interface NationContext { nationHasRank: boolean; nationHasDelta: boolean; } // ─── Namespace ──────────────────────────────────────────────────────────────── export const Layout = { /** * Format W.Rank prefix — rank + delta with placeholders for alignment. */ wrank(entry: WRankEntry | null, goal: number, context: NationContext): string { return format.wrank.row(entry, goal, context); }, /** * Format TG count as wrank emoji digits. * done/goal — goal always gold, done turns gold when >= goal. */ tgCount(done: number, goal: number): string { const doneEmoji = done >= goal ? (Emoji.get(`wrank_${done}_gold`) || `${done}`) : (Emoji.get(`wrank_${done}`) || `${done}`); const goalEmoji = Emoji.get(`wrank_${goal}_gold`) || `${goal}`; return `${doneEmoji}/${goalEmoji}`; }, /** * Format K/D — returns empty string if both zero. */ kd(k: number, d: number): string { return (k || d) ? format.kd(k, d) : ""; }, /** * Bringer indicator — returns " · {bringer emoji}" if char is Bringer. */ bringer(char: Character, week?: WRankWeek): string { const bringer = Bringer.get({ nation: char.nation, week }); return bringer === char.name ? ` · ${format.bringer(char.nation)}` : ""; }, /** * Cockroach indicator — returns " {cockroach}{count}" if char left that TG. */ cockroach(char: Character, historyKey?: TGKey): string { if (!historyKey) return ""; if (!Leaves.hasLeft({ characterName: char.name, historyKey })) return ""; return ` ${Leaves.formatIndicator({ characterName: char.name })}`; }, /** * All indicators combined — bringer + cockroach. */ indicators(char: Character, opts: { historyKey?: TGKey; week?: WRankWeek } = {}): string { return Layout.bringer(char, opts.week) + Layout.cockroach(char, opts.historyKey); }, /** * Format a row from a template string with token replacement. * Tokens: {wrank} {class} {level} {name} {score} {kd} {tgs} {stats} * Unknown tokens are left as-is. * Trailing empty tokens and extra spaces are cleaned up. */ formatRow(template: string, tokens: Record): string { return template .replace(/\{(\w+)\}/g, (_, key) => tokens[key] ?? `{${key}}`) .replace(/ +/g, " ") // only collapse regular ASCII spaces, not all \s .trim(); }, /** * Build nation context from any rows that have a position. */ nationContext(rows: { position?: { currentRank: number; previousRank?: number } }[]): NationContext { return { nationHasRank: rows.some((r) => r.position && r.position.currentRank !== 0), nationHasDelta: rows.some((r) => r.position?.previousRank !== undefined), }; }, /** * Build WRankEntry shape from a position object for use with format.wrank.row. */ wrankEntry( char: Character, position?: { currentRank: number; previousRank?: number }, weeklyPoints = 0, tgCount = 0 ): WRankEntry | null { if (!position || position.currentRank === 0) return null; return { character: char, weeklyPoints, tgCount, currentRank: position.currentRank, previousRank: position.previousRank, }; }, };