- 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.
74 lines
No EOL
3 KiB
TypeScript
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,
|
|
}; |