tg-bot-ts/src/ui/leaderboard/layouts/stacked-tg-top.ts
Nuno Duque Nunes 9e8877483d feat: Leaderboard & Result systems with aligned columns, call/confirm-no commands, persistent message slots
- TextAlign: column alignment for embeds using real gg sans font metrics
- EmbedHelpers: per-player grid/column layouts immune to 1024-char field limit
- Layout: domain-aware formatting wrapper (wrank, bringer, cockroach, tgCount)
- PersistentMessage: multi-slot support for independently-updatable embeds
- Leaderboard: weekly rankings + highlights embed (most kills/deaths, next Bringer)
- Result: per-TG breakdown with wRankAtSubmission snapshot for historical accuracy
- /tg call, /tg poll confirm-no, /tg-admin score-inject, result/leaderboard post commands
- Fix: CharacterRegistry wasn't hydrating ownerKey, breaking K/D bot-wide
- Fix: Leaderboard.buildEntries used current week instead of passed-in week param
- /tg-admin test-align: permanent calibration tool for embed text alignment

Includes data/emojis/anima-mastery.json for new combat stat icons.
2026-06-20 03:04:52 +01:00

74 lines
No EOL
3 KiB
TypeScript

/**
* Stacked leaderboard layout — Option 1: TG count joins the name line.
*/
import { EmbedBuilder } from "discord.js";
import { Nation, ClassKey } from "@types";
import { WRankWeek } from "@systems/wrank";
import { Emoji } from "@systems/emojis";
import { Config } from "@systems/config";
import { format } from "@format";
import { Layout, NationContext } from "@ui/layout";
import { EmbedHelpers } from "@ui/embed-helpers";
import { LeaderboardLayout, LeaderboardRow } from "../index";
const TEMPLATE = "{wrank} {class} {name}{indicators} · [{tgs}]";
function formatRow(row: LeaderboardRow, context: NationContext): string {
const char = row.character;
const goal = Config.get({ section: "wrank", key: "goal" });
const wrEntry = Layout.wrankEntry(char as any, row.position, row.weeklyPts, row.tgCount);
const classKey = (typeof char.class === "object" ? char.class?.key : char.class) as ClassKey;
const mainTokens: Record<string, string> = {
wrank: Layout.wrank(wrEntry, goal, context),
class: Emoji.class(classKey) || classKey || "?",
name: char.name,
indicators: Layout.indicators(char as any),
tgs: Layout.tgCount(row.tgCount, goal),
};
const scoreEmoji = Emoji.get("score") || "📊";
const scoreLine = `${scoreEmoji} ${format.scoreBold(row.weeklyPts)}`;
const kdLine = (row.totalKills || row.totalDeaths) ? format.kd(row.totalKills, row.totalDeaths) : "";
const mainLine = Layout.formatRow(TEMPLATE, mainTokens);
const lines = [mainLine, `\u3000${scoreLine}`];
if (kdLine) lines.push(`\u3000${kdLine}`);
return lines.join("\n");
}
function buildEmbed(week: WRankWeek, rows: LeaderboardRow[]): EmbedBuilder {
const capellaRows = rows.filter((r) => r.character.nation === Nation.Capella);
const procyonRows = rows.filter((r) => r.character.nation === Nation.Procyon);
const capContext = Layout.nationContext(capellaRows);
const proContext = Layout.nationContext(procyonRows);
const sortByPts = (a: LeaderboardRow, b: LeaderboardRow) => b.weeklyPts - a.weeklyPts;
const capellaEmoji = Emoji.get("capella");
const procyonEmoji = Emoji.get("procyon");
const embed = new EmbedBuilder()
.setTitle(`🏆 Weekly Leaderboard — ${week.weekKey}`)
.setColor(0xe8a317)
.setFooter({ text: `Weekly Leaderboard · ${week.weekKey} · Updated ${format.date(new Date(), "dd/MM/YYYY HH:mm")}` })
.setTimestamp();
const capellaFormatted = [...capellaRows].sort(sortByPts).map((r) => formatRow(r, capContext));
const procyonFormatted = [...procyonRows].sort(sortByPts).map((r) => formatRow(r, proContext));
EmbedHelpers.addPerPlayerGrid(embed, [
{ header: `${capellaEmoji} Capella`, rows: capellaFormatted },
{ header: `${procyonEmoji} Procyon`, rows: procyonFormatted },
]);
return embed;
}
export const stackedTgTopLeaderboardLayout: LeaderboardLayout = {
name: "stacked-tg-top",
description: "TG count joins the name line, score and K/D each on their own line",
buildEmbed,
formatRow,
};