/** * 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 = { 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, };