155 lines
No EOL
7.9 KiB
TypeScript
155 lines
No EOL
7.9 KiB
TypeScript
/**
|
|
* Sequential-extra-stats leaderboard layout — based directly on the
|
|
* working "sequential" layout (full name/score/K-D/TG-count alignment
|
|
* via TextAlign), with an additional indented second line showing
|
|
* weekly atk/def/heal stats when present.
|
|
*/
|
|
|
|
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 { TextAlign } from "@ui/text-align";
|
|
import { Logger } from "@systems/logger";
|
|
import { LeaderboardLayout, LeaderboardRow } from "../index";
|
|
|
|
const log = Logger.for("sequential-extra-stats");
|
|
|
|
const TEMPLATE = "{rank} {class} {name}{indicators} {score} {kd} {tgs}";
|
|
|
|
// Adjustable extra spacing between columns — tune these to close any
|
|
// residual sub-filler drift between primary row and secondary stats line.
|
|
const KD_GAP = 4;
|
|
const TGS_GAP = 0;
|
|
const HEAL_GAP = TGS_GAP + 5; // separate from TGS_GAP since heal lacks the [ ] brackets tgCount has
|
|
|
|
function formatRankOnly(currentRank: number, goalMet: boolean): string {
|
|
if (!currentRank || currentRank === 0) return "—";
|
|
const suffix = goalMet ? "_gold" : "";
|
|
return Emoji.get(`wrank_${currentRank}${suffix}`) || `${currentRank}`;
|
|
}
|
|
|
|
function formatRow(
|
|
row: LeaderboardRow,
|
|
context: NationContext,
|
|
allNameBlocks: string[],
|
|
allScores: string[],
|
|
allKds: string[],
|
|
allTgs: string[],
|
|
allAtks: string[],
|
|
allDefs: string[],
|
|
allHeals: string[],
|
|
week: WRankWeek
|
|
): string {
|
|
const char = row.character;
|
|
const goal = Config.get({ section: "wrank", key: "goal" });
|
|
const classKey = (typeof char.class === "object" ? char.class?.key : char.class) as ClassKey;
|
|
const goalMet = row.tgCount >= goal;
|
|
const scoreEmoji = Emoji.get("score") || "📊";
|
|
|
|
const scoreText = format.scoreBold(row.weeklyPts);
|
|
const kdText = (row.totalKills || row.totalDeaths) ? format.kd(row.totalKills, row.totalDeaths) : "—";
|
|
const tgText = `[${Layout.tgCount(row.tgCount, goal)}]`;
|
|
|
|
// Each column's target width = widest of (primary stat, secondary stat
|
|
// that will appear below it) — so the primary row makes room for the
|
|
// secondary stats line beneath it, keeping both column-locked together.
|
|
const scoreColumn = [...allScores, ...allAtks];
|
|
const kdColumn = [...allKds, ...allDefs];
|
|
const tgsColumn = [...allTgs, ...allHeals];
|
|
|
|
const bringerTag = Layout.bringer(char as any, week);
|
|
const nameBlock = bringerTag ? `${char.name}${TextAlign.gap(1)}${bringerTag}` : char.name;
|
|
const paddedBlock = TextAlign.padToMax(nameBlock, allNameBlocks);
|
|
const cockroach = Layout.cockroach(char as any);
|
|
|
|
const tokens: Record<string, string> = {
|
|
rank: row.position ? formatRankOnly(row.position.currentRank, goalMet) : "—",
|
|
class: Emoji.class(classKey) || classKey || "?",
|
|
name: paddedBlock,
|
|
indicators: cockroach,
|
|
score: `${scoreEmoji} ${TextAlign.padToMax(scoreText, scoreColumn)}`,
|
|
kd: TextAlign.gap(KD_GAP) + TextAlign.padToMax(kdText, kdColumn),
|
|
tgs: TextAlign.gap(TGS_GAP) + TextAlign.padLeftToMax(tgText, tgsColumn),
|
|
};
|
|
|
|
const mainLine = Layout.formatRow(TEMPLATE, tokens);
|
|
|
|
if (!row.stats || (!row.stats.atk && !row.stats.def && !row.stats.heal)) return mainLine;
|
|
|
|
const prefixText = `${tokens.rank} ${tokens.class} ${tokens.name}${tokens.indicators}`;
|
|
const prefixGap = TextAlign.padLeft("", TextAlign.estimateWidth(prefixText));
|
|
const atkText = format.statText("anima_atk", row.stats.atk, "⚔️");
|
|
const defText = format.statText("anima_def", row.stats.def, "🛡️");
|
|
const healText = format.statText("circle_massheal_purple", row.stats.heal, "💚");
|
|
|
|
const statsLine = `${prefixGap} ${TextAlign.gap(4)}${TextAlign.padToMax(atkText, scoreColumn)} ${TextAlign.gap(KD_GAP)}${TextAlign.padToMax(defText, kdColumn)} ${TextAlign.gap(HEAL_GAP)}${TextAlign.padLeftToMax(healText, tgsColumn)}`;
|
|
|
|
return `${mainLine}\n${statsLine}`;
|
|
}
|
|
|
|
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 sortedCapella = [...capellaRows].sort(sortByPts);
|
|
const sortedProcyon = [...procyonRows].sort(sortByPts);
|
|
|
|
const capellaNameBlocks = sortedCapella.map((r) => {
|
|
const tag = Layout.bringer(r.character as any, week);
|
|
return tag ? `${r.character.name}${TextAlign.gap(1)}${tag}` : r.character.name;
|
|
});
|
|
const procyonNameBlocks = sortedProcyon.map((r) => {
|
|
const tag = Layout.bringer(r.character as any, week);
|
|
return tag ? `${r.character.name}${TextAlign.gap(1)}${tag}` : r.character.name;
|
|
});
|
|
const capellaScores = sortedCapella.map((r) => format.scoreBold(r.weeklyPts));
|
|
const procyonScores = sortedProcyon.map((r) => format.scoreBold(r.weeklyPts));
|
|
const capellaKds = sortedCapella.map((r) => (r.totalKills || r.totalDeaths) ? format.kd(r.totalKills, r.totalDeaths) : "—");
|
|
const procyonKds = sortedProcyon.map((r) => (r.totalKills || r.totalDeaths) ? format.kd(r.totalKills, r.totalDeaths) : "—");
|
|
const goal = Config.get({ section: "wrank", key: "goal" });
|
|
const capellaTgs = sortedCapella.map((r) => `[${Layout.tgCount(r.tgCount, goal)}]`);
|
|
const procyonTgs = sortedProcyon.map((r) => `[${Layout.tgCount(r.tgCount, goal)}]`);
|
|
|
|
const atkEmoji = Emoji.get("atk") || "⚔️";
|
|
const defEmoji = Emoji.get("def") || "🛡️";
|
|
const healEmoji = Emoji.get("heal") || "💚";
|
|
const capellaAtks = sortedCapella.map((r) => r.stats?.atk ? `${atkEmoji} ${format.number.abbrev(r.stats.atk)}` : "");
|
|
const procyonAtks = sortedProcyon.map((r) => r.stats?.atk ? `${atkEmoji} ${format.number.abbrev(r.stats.atk)}` : "");
|
|
const capellaDefs = sortedCapella.map((r) => r.stats?.def ? `${defEmoji} ${format.number.abbrev(r.stats.def)}` : "");
|
|
const procyonDefs = sortedProcyon.map((r) => r.stats?.def ? `${defEmoji} ${format.number.abbrev(r.stats.def)}` : "");
|
|
const capellaHeals = sortedCapella.map((r) => r.stats?.heal ? `${healEmoji} ${format.number.abbrev(r.stats.heal)}` : "");
|
|
const procyonHeals = sortedProcyon.map((r) => r.stats?.heal ? `${healEmoji} ${format.number.abbrev(r.stats.heal)}` : "");
|
|
|
|
const capellaEmoji = Emoji.get("capella");
|
|
const procyonEmoji = Emoji.get("procyon");
|
|
|
|
const capellaFormatted = sortedCapella.map((r) => formatRow(r, capContext, capellaNameBlocks, capellaScores, capellaKds, capellaTgs, capellaAtks, capellaDefs, capellaHeals, week));
|
|
const procyonFormatted = sortedProcyon.map((r) => formatRow(r, proContext, procyonNameBlocks, procyonScores, procyonKds, procyonTgs, procyonAtks, procyonDefs, procyonHeals, week));
|
|
|
|
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();
|
|
|
|
EmbedHelpers.addPerPlayerColumn(embed, `${capellaEmoji} Capella`, capellaFormatted);
|
|
embed.addFields({ name: "\u200b", value: "\u200b", inline: false });
|
|
EmbedHelpers.addPerPlayerColumn(embed, `${procyonEmoji} Procyon`, procyonFormatted);
|
|
|
|
return embed;
|
|
}
|
|
|
|
export const sequentialExtraStatsLeaderboardLayout: LeaderboardLayout = {
|
|
name: "sequential-extra-stats",
|
|
description: "Same as sequential, plus weekly atk/def/heal on indented second line",
|
|
buildEmbed,
|
|
formatRow: formatRow as any,
|
|
}; |