fix Bringer showing incorrectly from previous weeks on result post

This commit is contained in:
Nuno Duque Nunes 2026-06-22 15:41:28 +01:00
parent 772477e6e8
commit 7d68530826
4 changed files with 76 additions and 94 deletions

View file

@ -18,19 +18,10 @@
import { format } from "@format";
import { Logger } from "@systems/logger";
import { DiscordClient } from "@discord/client";
import { ResultUI, ResultRow as UIResultRow } from "@ui/result";
import { ResultUI, ResultRow as UIResultRow, ResultRow } from "@ui/result";
const log = Logger.for("result");
// ─── Types ────────────────────────────────────────────────────────────────────
interface ResultRow {
character: Character;
score?: TGScore;
position?: { currentRank: number; previousRank?: number };
leavesCount: number;
}
// ─── Data ─────────────────────────────────────────────────────────────────────
function buildRows(historyKey: TGKey): ResultRow[] {
@ -80,6 +71,7 @@
score,
position,
leavesCount: Leaves.countForChar({ characterName: char.name }),
historyKey
});
}
@ -103,7 +95,10 @@
return;
}
const embed = ResultUI.buildEmbed(historyKey, rows as any);
const { date } = TGKey.parse(historyKey);
const weekKey = WRank.weekKey(new Date(date));
const week = WRank.weekFromKey(weekKey);
const embed = ResultUI.buildEmbed(historyKey, rows, week);
await PersistentMessage.post({
store: "results",

View file

@ -177,19 +177,6 @@ export interface PollState {
calledAt?: string;
}
// export interface PollState {
// messageId: string | null;
// slot: number;
// yes: Map<string, VoteEntry>; // userId → VoteEntry
// no: Map<string, VoteEntry>;
// locked: boolean;
// confirmed: "yes" | "no" | null;
// lockMessage?: string;
// confirmMessage?: string;
// lockedYesKeys?: Set<string>; // snapshot of userKeys in yes at lock time
// }
// ─── Scores ──────────────────────────────────────────────────────────────────
export interface TGScore {

View file

@ -8,9 +8,10 @@
import { Config } from "@systems/config";
import { Logger } from "@systems/logger";
import { Runtime } from "@systems/runtime";
import { TGStats } from "@root/src/types";
import { Character, TGScore, TGStats } from "@root/src/types";
import path from "path";
import fs from "fs";
import { WRankWeek } from "@root/src/systems/wrank";
const log = Logger.for("result-ui");
@ -19,20 +20,8 @@
// ─── Types ────────────────────────────────────────────────────────────────────
export interface ResultRow {
character: {
name: string;
class: any;
level: number;
nation: any;
ownerKey: string;
};
score?: {
pts: number;
k?: number;
d?: number;
stats?: TGStats;
wRankAtSubmission?: { rank: number; delta: number };
};
character: Character;
score?: TGScore;
position?: { currentRank: number; previousRank?: number };
leavesCount: number;
historyKey: TGKey;
@ -41,7 +30,7 @@
export interface ResultLayout {
name: string;
description: string;
buildEmbed(historyKey: TGKey, rows: ResultRow[]): EmbedBuilder;
buildEmbed(historyKey: TGKey, rows: ResultRow[], week?: WRankWeek|null): EmbedBuilder;
formatRow(row: ResultRow, context: any): string;
}
@ -107,8 +96,8 @@
// ─── Dispatcher ───────────────────────────────────────────────────────────────
export const ResultUI = {
buildEmbed(historyKey: TGKey, rows: ResultRow[]): EmbedBuilder {
return activeLayout().buildEmbed(historyKey, rows);
buildEmbed(historyKey: TGKey, rows: ResultRow[], week?: WRankWeek|null): EmbedBuilder {
return activeLayout().buildEmbed(historyKey, rows, week);
},
formatRow(row: ResultRow, context: any): string {

View file

@ -15,6 +15,7 @@
import { EmbedHelpers } from "@ui/embed-helpers";
import { TextAlign } from "@ui/text-align";
import { ResultLayout, ResultRow } from "../index";
import { WRankWeek } from "@root/src/systems/wrank";
const TEMPLATE = "{wrank} {class} {name}{indicators} {score} {kd}";
@ -24,55 +25,59 @@
const HEAL_GAP = 4;
function formatRow(
row: ResultRow,
context: NationContext,
allNames: string[],
allScores: string[],
allKds: string[],
allAtks: string[],
allDefs: string[]
): string {
const char = row.character;
const goal = Config.get({ section: "wrank", key: "goal" });
const wrEntry = Layout.wrankEntry(char as any, row.position, row.score?.pts, 1);
const classKey = (typeof char.class === "object" ? char.class?.key : char.class) as ClassKey;
row: ResultRow,
context: NationContext,
allNameBlocks: string[],
allScores: string[],
allKds: string[],
allAtks: string[],
allDefs: string[],
week: WRankWeek | null
): string {
const char = row.character;
const goal = Config.get({ section: "wrank", key: "goal" });
const wrEntry = Layout.wrankEntry(char as any, row.position, row.score?.pts, 1);
const classKey = (typeof char.class === "object" ? char.class?.key : char.class) as ClassKey;
const scoreEmoji = Emoji.get("score") || "📊";
const scoreText = row.score ? format.scoreBold(row.score.pts) : "—";
const kdText = row.score && (row.score.k || row.score.d) ? format.kd(row.score.k ?? 0, row.score.d ?? 0) : "—";
const scoreColumn = [...allScores, ...allAtks];
const kdColumn = [...allKds, ...allDefs];
const bringerTag = Layout.bringer(char as any, week ?? undefined);
const nameBlock = bringerTag ? `${char.name}${TextAlign.gap(1)}${bringerTag}` : char.name;
const paddedBlock = TextAlign.padToMax(nameBlock, allNameBlocks);
const cockroach = Layout.cockroach(char as any, row.historyKey);
const tokens: Record<string, string> = {
wrank: Layout.wrank(wrEntry, goal, context),
class: Emoji.class(classKey) || classKey || "?",
name: paddedBlock,
indicators: cockroach,
score: `${scoreEmoji} ${TextAlign.padToMax(scoreText, scoreColumn)}`,
kd: TextAlign.gap(KD_GAP) + TextAlign.padToMax(kdText, kdColumn),
};
const mainLine = Layout.formatRow(TEMPLATE, tokens);
const stats = row.score?.stats;
const hasStats = !!stats && !!(stats.atk || stats.def || stats.heal);
if (!hasStats) return mainLine;
const prefixText = `${tokens.wrank} ${tokens.class} ${tokens.name}${tokens.indicators}`;
const prefixGap = TextAlign.padLeft("", TextAlign.estimateWidth(prefixText));
const atkText = format.statText("anima_atk", stats!.atk, "⚔️");
const defText = format.statText("anima_def", stats!.def, "🛡️");
const healText = format.statText("circle_massheal_purple", stats!.heal, "💚");
const statsLine = `${prefixGap} ${TextAlign.gap(SCORE_GAP)}${TextAlign.padToMax(atkText, scoreColumn)} ${TextAlign.gap(DEF_GAP)}${TextAlign.padToMax(defText, kdColumn)} ${TextAlign.gap(HEAL_GAP)}${healText}`;
return `${mainLine}\n${statsLine}`;
}
const scoreEmoji = Emoji.get("score") || "📊";
const scoreText = row.score ? format.scoreBold(row.score.pts) : "—";
const kdText = row.score && (row.score.k || row.score.d) ? format.kd(row.score.k ?? 0, row.score.d ?? 0) : "—";
const scoreColumn = [...allScores, ...allAtks];
const kdColumn = [...allKds, ...allDefs];
const tokens: Record<string, string> = {
wrank: Layout.wrank(wrEntry, goal, context),
class: Emoji.class(classKey) || classKey || "?",
name: TextAlign.padToMax(char.name, allNames),
indicators: Layout.indicators(char as any, { historyKey: row.historyKey }),
score: `${scoreEmoji} ${TextAlign.padToMax(scoreText, scoreColumn)}`,
kd: TextAlign.gap(KD_GAP) + TextAlign.padToMax(kdText, kdColumn),
};
const mainLine = Layout.formatRow(TEMPLATE, tokens);
// Read stats from the NESTED shape (row.score.stats), matching TGScore's
// actual canonical structure.
const stats = row.score?.stats;
const hasStats = !!stats && !!(stats.atk || stats.def || stats.heal);
if (!hasStats) return mainLine;
const prefixText = `${tokens.wrank} ${tokens.class} ${tokens.name}${tokens.indicators}`;
const prefixGap = TextAlign.padLeft("", TextAlign.estimateWidth(prefixText));
const atkText = format.statText("anima_atk", stats!.atk, "⚔️");
const defText = format.statText("anima_def", stats!.def, "🛡️");
const healText = format.statText("circle_massheal_purple", stats!.heal, "💚");
const statsLine = `${prefixGap} ${TextAlign.gap(SCORE_GAP)}${TextAlign.padToMax(atkText, scoreColumn)} ${TextAlign.gap(DEF_GAP)}${TextAlign.padToMax(defText, kdColumn)} ${TextAlign.gap(HEAL_GAP)}${healText}`;
return `${mainLine}\n${statsLine}`;
}
function buildEmbed(historyKey: TGKey, rows: ResultRow[]): EmbedBuilder {
function buildEmbed(historyKey: TGKey, rows: ResultRow[], week: WRankWeek|null): EmbedBuilder {
const { date, slot } = TGKey.parse(historyKey);
const capellaRows = rows.filter((r) => r.character.nation === Nation.Capella);
@ -86,8 +91,14 @@
const sortedCapella = [...capellaRows].sort(sortByScore);
const sortedProcyon = [...procyonRows].sort(sortByScore);
const capellaNames = sortedCapella.map((r) => r.character.name);
const procyonNames = sortedProcyon.map((r) => r.character.name);
const capellaNameBlocks = sortedCapella.map((r) => {
const tag = Layout.bringer(r.character as any, week ?? undefined);
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 ?? undefined);
return tag ? `${r.character.name}${TextAlign.gap(1)}${tag}` : r.character.name;
});
const capellaScores = sortedCapella.map((r) => r.score ? format.scoreBold(r.score.pts) : "—");
const procyonScores = sortedProcyon.map((r) => r.score ? format.scoreBold(r.score.pts) : "—");
const capellaKds = sortedCapella.map((r) => r.score && (r.score.k || r.score.d) ? format.kd(r.score.k ?? 0, r.score.d ?? 0) : "—");
@ -108,8 +119,8 @@
const capellaEmoji = Emoji.get("capella");
const procyonEmoji = Emoji.get("procyon");
const capellaFormatted = sortedCapella.map((r) => formatRow(r, capContext, capellaNames, capellaScores, capellaKds, capellaAtks, capellaDefs));
const procyonFormatted = sortedProcyon.map((r) => formatRow(r, proContext, procyonNames, procyonScores, procyonKds, procyonAtks, procyonDefs));
const capellaFormatted = sortedCapella.map((r) => formatRow(r, capContext, capellaNameBlocks, capellaScores, capellaKds, capellaAtks, capellaDefs, week));
const procyonFormatted = sortedProcyon.map((r) => formatRow(r, proContext, procyonNameBlocks, procyonScores, procyonKds, procyonAtks, procyonDefs, week));
const embed = new EmbedBuilder()
.setTitle(`⚔️ TG Result — ${format.date(new Date(date), "dd/MM/YYYY")} · ${slot}:00`)