tg-bot-ts/src/ui/layout.ts
2026-06-22 05:57:04 +01:00

126 lines
No EOL
4.4 KiB
TypeScript

/**
* 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, string>): 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,
};
},
};