tg-bot-ts/src/ui/leaderboard/highlights.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

70 lines
No EOL
3.1 KiB
TypeScript

/**
* Leaderboard highlights — secondary embed with weekly standout stats.
* Most kills, most deaths, next bringer per nation, etc.
*/
import { EmbedBuilder } from "discord.js";
import { Nation } from "@types";
import { Emoji } from "@systems/emojis";
import { Config } from "@systems/config";
import { LeaderboardRow } from "./index";
function topByKills(rows: LeaderboardRow[]): LeaderboardRow | null {
return [...rows].sort((a, b) => b.totalKills - a.totalKills)[0] ?? null;
}
function topByDeaths(rows: LeaderboardRow[]): LeaderboardRow | null {
return [...rows].sort((a, b) => b.totalDeaths - a.totalDeaths)[0] ?? null;
}
function nextBringerCandidate(rows: LeaderboardRow[], goal: number): LeaderboardRow | null {
// Rank 1 with goal TGs met is eligible — pick the rank-1 player if they qualify
const rank1 = rows.find((r) => r.position?.currentRank === 1);
if (!rank1) return null;
return rank1.tgCount >= goal ? rank1 : null;
}
export function buildHighlightsEmbed(allRows: LeaderboardRow[], weekKey: string): EmbedBuilder {
const goal = Config.get({ section: "wrank", key: "goal" });
const capellaRows = allRows.filter((r) => r.character.nation === Nation.Capella);
const procyonRows = allRows.filter((r) => r.character.nation === Nation.Procyon);
const topKillsCapella = topByKills(capellaRows);
const topKillsProcyon = topByKills(procyonRows);
const topDeathsAll = topByDeaths(allRows);
// Storm Bringer -> Procyon, Luminous Bringer -> Capella
const nextLuminousBringer = nextBringerCandidate(capellaRows, goal); // Capella
const nextStormBringer = nextBringerCandidate(procyonRows, goal); // Procyon
const capellaEmoji = Emoji.get("capella");
const procyonEmoji = Emoji.get("procyon");
const killEmoji = Emoji.get("wrank_down_1") || "⚔️";
const deathEmoji = Emoji.get("wrank_up_1") || "💀";
const stormEmoji = Emoji.get("storm_bringer") || "⚡";
const luminousEmoji = Emoji.get("luminous_bringer") || "🌟";
const lines: string[] = [];
if (topKillsCapella && topKillsCapella.totalKills > 0) {
lines.push(`${killEmoji} Most Kills (${capellaEmoji}): **${topKillsCapella.character.name}** (${topKillsCapella.totalKills})`);
}
if (topKillsProcyon && topKillsProcyon.totalKills > 0) {
lines.push(`${killEmoji} Most Kills (${procyonEmoji}): **${topKillsProcyon.character.name}** (${topKillsProcyon.totalKills})`);
}
if (topDeathsAll && topDeathsAll.totalDeaths > 0) {
lines.push(`${deathEmoji} Most Deaths: **${topDeathsAll.character.name}** (${topDeathsAll.totalDeaths})`);
}
lines.push(""); // spacer
lines.push(`${luminousEmoji} Next Luminous Bringer (${capellaEmoji}): ${nextLuminousBringer ? `**${nextLuminousBringer.character.name}**` : "—"}`);
lines.push(`${stormEmoji} Next Storm Bringer (${procyonEmoji}): ${nextStormBringer ? `**${nextStormBringer.character.name}**` : "—"}`);
return new EmbedBuilder()
.setTitle("📊 Weekly Highlights")
.setColor(0x5865f2)
.setDescription(lines.join("\n"))
.setFooter({ text: `Highlights · ${weekKey}` });
}