From 2131b5674b206c89ab4d4313f20f40695405a7cd Mon Sep 17 00:00:00 2001 From: Nuno Duque Nunes Date: Thu, 30 Jul 2026 00:18:53 +0100 Subject: [PATCH] update layout, fix reset week --- data/emojis/misc.json | 4 +- data/emojis/wrank-down.json | 3 +- data/emojis/wrank-up.json | 3 +- data/updates/v0.9.2/update.json | 29 +++++ font/gg sans Regular.ttf | Bin 0 -> 82424 bytes messages/emojis.json | 6 +- src/commands/tgAdmin.ts | 7 ++ src/handlers/modals.ts | 123 +++++++++++++------ src/subcommands/admin/reset-week.ts | 19 +++ src/systems/bringer.ts | 24 ++-- src/systems/score.ts | 20 --- src/systems/tg.ts | 30 +---- src/systems/wrank.ts | 17 ++- src/ui/result/layouts/sequential-arrow.ts | 143 ++++++++++++++++++++++ src/ui/result/layouts/sequential.ts | 28 +++-- src/ui/text-align.ts | 2 +- 16 files changed, 340 insertions(+), 118 deletions(-) create mode 100644 data/updates/v0.9.2/update.json create mode 100644 font/gg sans Regular.ttf create mode 100644 src/subcommands/admin/reset-week.ts create mode 100644 src/ui/result/layouts/sequential-arrow.ts diff --git a/data/emojis/misc.json b/data/emojis/misc.json index 3e2b8cd..87c564f 100644 --- a/data/emojis/misc.json +++ b/data/emojis/misc.json @@ -12,5 +12,7 @@ "wrank_up": "<:wrank_up:1512114414474756132>", "wrank_no_rank_delta": "<:wrank_no_rank_delta:1512263603519229982>", "slash": "<:slash:1516648012422844416>", - "tg_flag": "<:tg_flag:1516946073392910336>" + "tg_flag": "<:tg_flag:1516946073392910336>", + "wrank_Q": "<:wrank_Q:1528508962717040740>", + "wrank_Q_gold": "<:wrank_Q_gold:1528508971978195088>" } \ No newline at end of file diff --git a/data/emojis/wrank-down.json b/data/emojis/wrank-down.json index abb6fc3..89f98ad 100644 --- a/data/emojis/wrank-down.json +++ b/data/emojis/wrank-down.json @@ -99,5 +99,6 @@ "wrank_down_97": "<:wrank_down_97:1513360453152280709>", "wrank_down_98": "<:wrank_down_98:1513360459758174328>", "wrank_down_99": "<:wrank_down_99:1513360463558082590>", - "wrank_down_0": "<:wrank_down_0:1516648019297046709>" + "wrank_down_0": "<:wrank_down_0:1516648019297046709>", + "wrank_down_Q": "<:wrank_down_Q:1528508967658197144>" } \ No newline at end of file diff --git a/data/emojis/wrank-up.json b/data/emojis/wrank-up.json index 139554d..f26700e 100644 --- a/data/emojis/wrank-up.json +++ b/data/emojis/wrank-up.json @@ -99,5 +99,6 @@ "wrank_up_97": "<:wrank_up_97:1513360766588424192>", "wrank_up_98": "<:wrank_up_98:1513360770895708303>", "wrank_up_99": "<:wrank_up_99:1513360776071745576>", - "wrank_up_0": "<:wrank_up_0:1516648023084503113>" + "wrank_up_0": "<:wrank_up_0:1516648023084503113>", + "wrank_up_Q": "<:wrank_up_Q:1528508976185213072>" } \ No newline at end of file diff --git a/data/updates/v0.9.2/update.json b/data/updates/v0.9.2/update.json new file mode 100644 index 0000000..dd83355 --- /dev/null +++ b/data/updates/v0.9.2/update.json @@ -0,0 +1,29 @@ +{ + "version": "v0.9.2", + "date": "2026-07-21", + "title": "Score Button Fix & W.Rank Reset Fix", + "layout": "default", + "sections": [ + { + "type": "fix", + "label": "Fixes", + "emoji": "๐Ÿ”ง", + "items": [ + { "text": "Fixed Submit Score button not updating the Leaderboard or auto-posting the TG Result" }, + { "text": "Fixed W.Rank weekly reset writing Bringer data into the wrong week and not creating the new week correctly" }, + { "text": "Fixed Bringer incorrectly being assigned to a player who met the TG count goal but was not W.Rank 1" }, + { "text": "Fixed weekly reset overwriting the previous week's Bringer data" } + ] + }, + { + "type": "improvement", + "label": "Improvements", + "emoji": "โœจ", + "items": [ + { "text": "Submit Score modal now has separate ATK and DEF fields" }, + { "text": "Heal field in the Submit Score modal only appears for healing classes (FA)" } + ] + } + ], + "examples": [] +} \ No newline at end of file diff --git a/font/gg sans Regular.ttf b/font/gg sans Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..745d2ecc72a64974ffb958d9b4bd630122205176 GIT binary patch literal 82424 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 Nj_=9))lKJY`9E#ea;X3S literal 0 HcmV?d00001 diff --git a/messages/emojis.json b/messages/emojis.json index 6af1d2c..98c17b2 100644 --- a/messages/emojis.json +++ b/messages/emojis.json @@ -274,5 +274,9 @@ "anima_atk": "<:anima_atk:1517702182710018179>", "anima_def": "<:anima_def:1517700561468657785>", "anima_massheal": "<:anima_massheal:1517702186434433146>", - "circle_massheal_purple": "<:circle_massheal_purple:1518287829648801904>" + "circle_massheal_purple": "<:circle_massheal_purple:1518287829648801904>", + "wrank_Q": "<:wrank_Q:1528508962717040740>", + "wrank_down_Q": "<:wrank_down_Q:1528508967658197144>", + "wrank_Q_gold": "<:wrank_Q_gold:1528508971978195088>", + "wrank_up_Q": "<:wrank_up_Q:1528508976185213072>" } \ No newline at end of file diff --git a/src/commands/tgAdmin.ts b/src/commands/tgAdmin.ts index 3d76ef3..d10b537 100644 --- a/src/commands/tgAdmin.ts +++ b/src/commands/tgAdmin.ts @@ -9,6 +9,7 @@ import { UpdatesCommands } from "@subcommands/admin/updates"; import { ScoreInjectCommands } from "@subcommands/admin/score-inject"; import { ResultCommands } from "@subcommands/admin/result-post"; import { TestAlignCommands } from "@subcommands/admin/test-align"; +import { ResetWeekCommands } from "../subcommands/admin/reset-week"; import { AnnouncementCommands } from "../subcommands/admin/announcement"; export function buildTgAdminCommand(): SlashCommandBuilder { @@ -167,6 +168,11 @@ export function buildTgAdminCommand(): SlashCommandBuilder { ) ) + cmd.addSubcommand((s) => s + .setName("reset-week") + .setDescription("[Admin] Manually trigger the weekly W.Rank reset โ€” for testing only") + ) + cmd.addSubcommandGroup((g) => g .setName("announcement") .setDescription("Manage announcements") @@ -221,4 +227,5 @@ export async function handleTgAdminCommand(interaction: ChatInputCommandInteract if (group === "announcement" && sub === "list") return AnnouncementCommands.list(interaction); if (group === null && sub === "test-align") return TestAlignCommands.handle(interaction); + if (group === null && sub === "reset-week") return ResetWeekCommands.handle(interaction); } \ No newline at end of file diff --git a/src/handlers/modals.ts b/src/handlers/modals.ts index 495e337..28e2385 100644 --- a/src/handlers/modals.ts +++ b/src/handlers/modals.ts @@ -5,28 +5,38 @@ import { TextInputStyle, ActionRowBuilder, } from "discord.js"; +import { Logger } from "@systems/logger"; +import { Score } from "@systems/score"; +import { Emoji } from "@systems/emojis"; import { resolveUser } from "@systems/users"; import { getEffectiveCharacter } from "@systems/borrow"; -import { score } from "@subcommands/score/submitCore"; import { format } from "@format"; +import { SlotHour, ClassKey } from "@root/src/types"; + +const log = Logger.for("modals"); + +// โ”€โ”€โ”€ Healing classes โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// Only these classes see the Heal field in the score modal. +// Extend this array when new healing classes are added. +const HEALING_CLASSES: ClassKey[] = ["FA"]; + +function isHealingClass(cls: ClassKey): boolean { + return HEALING_CLASSES.includes(cls); +} // โ”€โ”€โ”€ Modal IDs โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ // // score_submit: โ€” score submission modal, slot baked into the customId -// so we don't need to pass state through the modal itself export namespace modals { // โ”€โ”€โ”€ Builder โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - /** - * Build the score submission modal for a given userKey + slot. - * Title shows the active character so the user knows what they're submitting for. - */ export function buildScoreModal(userKey: string, slot: number): ModalBuilder { const { char } = getEffectiveCharacter(userKey); - // const charLabel = char ? format.char(char) : "your character"; const charLabel = char ? format.char(char, { emoji: false }) : "your character"; + const classKey = (typeof char?.class === "object" ? (char.class as any)?.key : char?.class) as ClassKey | undefined; + const showHeal = !!classKey && isHealingClass(classKey); const modal = new ModalBuilder() .setCustomId(`score_submit:${slot}`) @@ -46,25 +56,36 @@ export namespace modals { .setPlaceholder("5/2") .setRequired(false); - const atkDefInput = new TextInputBuilder() - .setCustomId("atkdef") - .setLabel("ATK / DEF (e.g. 120/80)") + const atkInput = new TextInputBuilder() + .setCustomId("atk") + .setLabel("Attack Score") .setStyle(TextInputStyle.Short) - .setPlaceholder("120/80") + .setPlaceholder("e.g. 120") + .setRequired(false); + + const defInput = new TextInputBuilder() + .setCustomId("def") + .setLabel("Defense Score") + .setStyle(TextInputStyle.Short) + .setPlaceholder("e.g. 80") .setRequired(false); const healInput = new TextInputBuilder() .setCustomId("heal") - .setLabel("Heal") + .setLabel("Healing Score") .setStyle(TextInputStyle.Short) .setPlaceholder("e.g. 500") .setRequired(false); + // Base fields always shown: pts, k/d, atk, def (4 fields) + // Heal replaces def when class is a healer โ€” keeps total at 4 or 5 + // With heal: pts, k/d, atk, def, heal = 5 (Discord max) modal.addComponents( new ActionRowBuilder().addComponents(ptsInput), new ActionRowBuilder().addComponents(kdInput), - new ActionRowBuilder().addComponents(atkDefInput), - new ActionRowBuilder().addComponents(healInput), + new ActionRowBuilder().addComponents(atkInput), + new ActionRowBuilder().addComponents(defInput), + ...(showHeal ? [new ActionRowBuilder().addComponents(healInput)] : []), ); return modal; @@ -108,18 +129,35 @@ export namespace modals { return; } - const member = await interaction.guild!.members.fetch(interaction.user.id); - const user = await resolveUser(member); + const member = await interaction.guild!.members.fetch(interaction.user.id); + const user = await resolveUser(member); if (!user.userKey) { await interaction.editReply("โŒ You are not registered in the system."); return; } + const { char, borrowedFrom } = getEffectiveCharacter(user.userKey); + if (!char) { + await interaction.editReply("โŒ No active character found. Use `/tg char set-active` first."); + return; + } + + const classKey = (typeof char.class === "object" ? (char.class as any)?.key : char.class) as ClassKey; + const showHeal = isHealingClass(classKey); + // Parse fields - const ptsRaw = interaction.fields.getTextInputValue("pts"); - const kdRaw = interaction.fields.getTextInputValue("kd") || null; - const atkDefRaw = interaction.fields.getTextInputValue("atkdef") || null; - const healRaw = interaction.fields.getTextInputValue("heal") || null; + const ptsRaw = interaction.fields.getTextInputValue("pts"); + const kdRaw = interaction.fields.getTextInputValue("kd") || null; + const atkRaw = interaction.fields.getTextInputValue("atk") || null; + const defRaw = interaction.fields.getTextInputValue("def") || null; + // Heal field only exists in the modal for healing classes โ€” safe to attempt, + // getTextInputValue returns "" for missing fields on some discord.js versions, + // so we guard with try/catch + let healRaw: string | null = null; + if (showHeal) { + try { healRaw = interaction.fields.getTextInputValue("heal") || null; } + catch { healRaw = null; } + } const pts = parseInt(ptsRaw.trim(), 10); if (isNaN(pts)) { @@ -127,30 +165,45 @@ export namespace modals { return; } - const kd = parseSlashPair(kdRaw); - const atkDef = parseSlashPair(atkDefRaw); - const heal = parseOptionalInt(healRaw); + const kd = parseSlashPair(kdRaw); + const atk = parseOptionalInt(atkRaw); + const def = parseOptionalInt(defRaw); + const heal = parseOptionalInt(healRaw); if (kdRaw && !kd) { await interaction.editReply("โŒ K/D must be in `kills/deaths` format, e.g. `5/2`."); return; } - if (atkDefRaw && !atkDef) { - await interaction.editReply("โŒ ATK/DEF must be in `atk/def` format, e.g. `120/80`."); - return; - } - const result = await score.submitForUser({ - userKey: user.userKey, + log.debug(`Score submit via modal: userKey=${user.userKey} char=${char.name} slot=${slot} showHeal=${showHeal}`); + + // โ”€โ”€ Canonical path โ€” same as /tg score set โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + // Score.submit emits RuntimeEvents โ†’ Leaderboard.update + Result.post + await Score.submit({ + character: char, + playedBy: borrowedFrom ? user.userKey : undefined, pts, - slot, - k: kd?.[0], - d: kd?.[1], - atk: atkDef?.[0], - def: atkDef?.[1], + k: kd?.[0], + d: kd?.[1], + atk, + def, heal, + slot: slot as SlotHour, + submittedByOfficer: false, }); - await interaction.editReply(result.message); + const scoreEmoji = Emoji.get("score") || "๐Ÿ“Š"; + const kdEmoji = Emoji.get("kd") || "โš”๏ธ"; + const borrowNote = borrowedFrom ? ` *(borrowed from ${borrowedFrom})*` : ""; + const kdNote = kd ? `\n${kdEmoji} ${kd[0]}/${kd[1]}` : ""; + const statsNote = [ + atk !== undefined ? `ATK: ${atk}` : null, + def !== undefined ? `DEF: ${def}` : null, + heal !== undefined ? `HEAL: ${heal}` : null, + ].filter(Boolean).join(" ยท "); + + await interaction.editReply( + `โœ… ${scoreEmoji} **${pts}** submitted for **${char.name}**${borrowNote} (${slot}:00 TG)${kdNote}${statsNote ? `\n${statsNote}` : ""}` + ); } } \ No newline at end of file diff --git a/src/subcommands/admin/reset-week.ts b/src/subcommands/admin/reset-week.ts new file mode 100644 index 0000000..38337ca --- /dev/null +++ b/src/subcommands/admin/reset-week.ts @@ -0,0 +1,19 @@ +import { ChatInputCommandInteraction } from "discord.js"; +import { TG } from "@systems/tg"; +import { replyAndDelete } from "@utils"; +import { User } from "@systems/users"; +import { Config } from "@systems/config"; + +export async function handleResetWeek(interaction: ChatInputCommandInteraction): Promise { + const member = await interaction.guild!.members.fetch(interaction.user.id); + if (!User.hasOfficerRole({ member, officerRoles: Config.get({ section: "roles", key: "officer" }) })) { + return void replyAndDelete(interaction, "โŒ Officer only."); + } + + TG.resetWeek(); + return void replyAndDelete(interaction, "โœ… Week reset executed. Check logs for result."); +} + +export const ResetWeekCommands = { + handle: handleResetWeek, +}; \ No newline at end of file diff --git a/src/systems/bringer.ts b/src/systems/bringer.ts index 0fce385..3aae63c 100644 --- a/src/systems/bringer.ts +++ b/src/systems/bringer.ts @@ -52,17 +52,15 @@ * Recompute the earned Bringer from scores. * Called after weekly reset โ€” the top ranked player with >= goal TGs becomes Bringer. */ - update({ week }: { week?: WRankWeek }): void { - const goal = Config.get({ section: "wrank", key: "goal" }); - for (const nation of [Nation.Capella, Nation.Procyon]) { - const _week = week ?? WRank.currentWeek(); - const list = _week.entries[nation]; - const qualified = list - .filter((e) => e.tgCount >= goal) - .sort((a, b) => a.currentRank - b.currentRank); - _week.bringer[nation] = qualified[0]?.characterName ?? null; - } - - WRank.save(); - }, + update({ week }: { week?: WRankWeek }): void { + const goal = Config.get({ section: "wrank", key: "goal" }); + const _week = week ?? WRank.currentWeek(); + + for (const nation of [Nation.Capella, Nation.Procyon]) { + const rank1 = _week.entries[nation].find((e) => e.currentRank === 1); + _week.bringer[nation] = (rank1 && rank1.tgCount >= goal) ? rank1.characterName : null; + } + + WRank.save(); + }, }; \ No newline at end of file diff --git a/src/systems/score.ts b/src/systems/score.ts index d49162b..932f3b5 100644 --- a/src/systems/score.ts +++ b/src/systems/score.ts @@ -16,26 +16,6 @@ import { TGKey } from "@systems/tg-key"; import { RuntimeEvents } from "@systems/runtime"; -// export interface TGScore { -// userKey: UserKey; -// playedBy?: UserKey; // if borrowed -// characterName: string; -// class: string; -// nation: Nation; -// pts: number; -// k?: number; -// d?: number; -// stats?: TGStats; -// submittedAt: string; -// slot: SlotHour; -// date: string; -// submittedByOfficer: boolean; -// wRankAtSubmission?: { -// rank: number; -// delta: number; -// }; -// } - export interface WeeklySummary { userKey: UserKey; character: Character; diff --git a/src/systems/tg.ts b/src/systems/tg.ts index 2510cf7..85b9b6c 100644 --- a/src/systems/tg.ts +++ b/src/systems/tg.ts @@ -32,35 +32,7 @@ * Called every Monday 00:00 by the Scheduler. */ resetWeek(): void { - const now = new Date(); - const newWeekKey = WRank.weekKey(now); - const prevWeekKey = WRank.weekKey(new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)); - - // Access internal data via WRank - const prevWeek = WRank.weekFromKey(prevWeekKey); - const newWeek = WRank.currentWeek(); // ensures new week exists - - if (prevWeek) { - const goal = Config.get({ section: "wrank", key: "goal" }); - for (const nation of [Nation.Capella, Nation.Procyon]) { - const entries = prevWeek.entries[nation]; - const rank1 = entries.find((e) => e.currentRank === 1); - - // Rank 1 with >= goal TGs becomes Bringer โ€” no exceptions - // Officers use Bringer.override() for manual adjustments - if (rank1 && rank1.tgCount >= goal) { - newWeek.bringer[nation] = rank1.characterName; - } else { - newWeek.bringer[nation] = null; - } - - // Overrides do NOT carry forward โ€” each week starts clean - delete (newWeek.bringer as any)[`${nation}Override`]; - } - } - - WRank.save(); - console.log(`[TG] Week reset to ${newWeekKey}. Bringer:`, newWeek.bringer); + return WRank.resetWeek(); }, // โ”€โ”€ Attendance โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ diff --git a/src/systems/wrank.ts b/src/systems/wrank.ts index bbb10eb..348fa7d 100644 --- a/src/systems/wrank.ts +++ b/src/systems/wrank.ts @@ -234,20 +234,25 @@ export const WRank = { // โ”€โ”€ Weekly reset โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ resetWeek(): void { - const prevWeekKey = WRank.weekKey(new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)); + // Fire the weekly reset on Monday 00:00 (WRank reset) (UTC+2 = Sunday 22:00) + const nowUtcNoon = new Date(); + nowUtcNoon.setUTCHours(12, 0, 0, 0); + + const newWeekKey = WRank.weekKey(nowUtcNoon); + const prevWeekKey = WRank.weekKey(new Date(nowUtcNoon.getTime() - 7 * 24 * 60 * 60 * 1000)); const prevWeek = _data[prevWeekKey]; - const newWeek = ensureWeek(WRank.weekKey()); + const newWeek = ensureWeek(newWeekKey); if (prevWeek) { - Bringer.update({ week: prevWeek }); + const goal = Config.get({ section: "wrank", key: "goal" }); for (const nation of [Nation.Capella, Nation.Procyon]) { - const bringer = prevWeek.bringer[nation]; - if (bringer) newWeek.bringer[nation] = bringer; + const rank1 = prevWeek.entries[nation].find((e) => e.currentRank === 1); + newWeek.bringer[nation] = (rank1 && rank1.tgCount >= goal) ? rank1.characterName : null; } } WRank.save(); - log.info(`Reset to ${WRank.weekKey()}.`); + log.info(`Week reset to ${newWeekKey}. Bringer: ${JSON.stringify(newWeek.bringer)}`); }, // โ”€โ”€ Bringer (legacy โ€” use Bringer namespace directly) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ diff --git a/src/ui/result/layouts/sequential-arrow.ts b/src/ui/result/layouts/sequential-arrow.ts new file mode 100644 index 0000000..0cfe887 --- /dev/null +++ b/src/ui/result/layouts/sequential-arrow.ts @@ -0,0 +1,143 @@ +/** + * Sequential result layout with โ†ณ continuation line for secondary stats. + * + * Primary row: wrank ยท class ยท name[bringer] ยท score ยท k/d + * Secondary row (when stats exist): โ†ณ atk ยท def ยท heal + * + * The โ†ณ prefix is a custom emoji if configured, falling back to the + * Unicode arrow character. No column-locking attempt on the secondary + * row โ€” it starts with the arrow and lists stats cleanly left-to-right. + */ + + import { EmbedBuilder } from "discord.js"; + import { Nation, ClassKey } from "@types"; + import { TGKey } from "@systems/tg-key"; + 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 { WRankWeek } from "@systems/wrank"; + import { ResultLayout, ResultRow } from "../index"; + + const TEMPLATE = "{wrank} {class} {name}{indicators}{score} {kd}"; + + const KD_GAP = 4; + const SCORE_GAP = 4; + + function arrowPrefix(): string { + return Emoji.get("arrow_continue") || "โ†ณ"; + } + + function formatRow( + row: ResultRow, + context: NationContext, + allNameBlocks: string[], + allScores: string[], + allKds: 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 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 = { + wrank: Layout.wrank(wrEntry, goal, context), + class: Emoji.class(classKey) || classKey || "?", + name: paddedBlock, + indicators: cockroach, + score: `${TextAlign.gap(SCORE_GAP)}${scoreEmoji} ${TextAlign.padToMax(scoreText, allScores)}`, + kd: TextAlign.gap(KD_GAP) + TextAlign.padToMax(kdText, allKds), + }; + + 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 arrow = arrowPrefix(); + 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 statParts = [atkText, defText, healText].filter(Boolean); + const statsLine = `${TextAlign.gap(3)}${arrow} ${statParts.join(" ")}`; + + return `${mainLine}\n${statsLine}`; + } + + 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); + const procyonRows = rows.filter((r) => r.character.nation === Nation.Procyon); + + const capContext = Layout.nationContext(capellaRows); + const proContext = Layout.nationContext(procyonRows); + + const sortByScore = (a: ResultRow, b: ResultRow) => (b.score?.pts ?? 0) - (a.score?.pts ?? 0); + const sortedCapella = [...capellaRows].sort(sortByScore); + const sortedProcyon = [...procyonRows].sort(sortByScore); + + const BRINGER_GAP = 2; + + const capellaNameBlocks = sortedCapella.map((r) => { + const tag = Layout.bringer(r.character as any, week ?? undefined); + if (!tag) return r.character.name; + return `${r.character.name}${TextAlign.gap(1)}${tag}${TextAlign.gap(BRINGER_GAP)}`; + }); + const procyonNameBlocks = sortedProcyon.map((r) => { + const tag = Layout.bringer(r.character as any, week ?? undefined); + if (!tag) return r.character.name; + return `${r.character.name}${TextAlign.gap(1)}${tag}${TextAlign.gap(BRINGER_GAP)}`; + }); + + 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) : "โ€”"); + const procyonKds = sortedProcyon.map((r) => r.score && (r.score.k || r.score.d) ? format.kd(r.score.k ?? 0, r.score.d ?? 0) : "โ€”"); + + const capK = capellaRows.reduce((s, r) => s + (r.score?.k ?? 0), 0); + const capD = capellaRows.reduce((s, r) => s + (r.score?.d ?? 0), 0); + const proK = procyonRows.reduce((s, r) => s + (r.score?.k ?? 0), 0); + const proD = procyonRows.reduce((s, r) => s + (r.score?.d ?? 0), 0); + + const capellaEmoji = Emoji.get("capella"); + const procyonEmoji = Emoji.get("procyon"); + + const capellaFormatted = sortedCapella.map((r) => formatRow(r, capContext, capellaNameBlocks, capellaScores, capellaKds, week)); + const procyonFormatted = sortedProcyon.map((r) => formatRow(r, proContext, procyonNameBlocks, procyonScores, procyonKds, week)); + + const embed = new EmbedBuilder() + .setTitle(`โš”๏ธ TG Result โ€” ${format.date(new Date(date), "dd/MM/YYYY")} ยท ${slot}:00`) + .setColor(0xe8a317) + .setFooter({ text: `TG Result ยท ${TGKey.toDisplay(historyKey)}` }) + .setTimestamp(); + + EmbedHelpers.addPerPlayerColumn(embed, `${capellaEmoji} Capella${(capK || capD) ? ` โ€” ${format.kd(capK, capD)}` : ""}`, capellaFormatted); + embed.addFields({ name: "\u200b", value: "\u200b", inline: false }); + EmbedHelpers.addPerPlayerColumn(embed, `${procyonEmoji} Procyon${(proK || proD) ? ` โ€” ${format.kd(proK, proD)}` : ""}`, procyonFormatted); + + return embed; + } + + export const sequentialArrowResultLayout: ResultLayout = { + name: "sequential-arrow", + description: "Name/score/K-D aligned, secondary stats on โ†ณ continuation line", + buildEmbed, + formatRow: formatRow as any, + }; \ No newline at end of file diff --git a/src/ui/result/layouts/sequential.ts b/src/ui/result/layouts/sequential.ts index 2d5fa78..99d5a8a 100644 --- a/src/ui/result/layouts/sequential.ts +++ b/src/ui/result/layouts/sequential.ts @@ -15,9 +15,12 @@ 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}"; + import { WRankWeek } from "@root/src/systems/wrank"; + import { Logger } from "@root/src/systems/logger"; + + const log = Logger.for("result-layout/sequential"); + + const TEMPLATE = "{wrank} {class} {name}{indicators}{score} {kd}"; const KD_GAP = 4; const SCORE_GAP = 4; @@ -47,16 +50,18 @@ import { WRankWeek } from "@root/src/systems/wrank"; 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 nameBlock = bringerTag ? `${char.name}${TextAlign.gap(1)}${bringerTag}` : char.name; + const paddedName = TextAlign.padToMax(nameBlock, allNameBlocks); + const cockroach = Layout.cockroach(char as any, row.historyKey); + const paddedBlock = TextAlign.padToMax(nameBlock, allNameBlocks); + const tokens: Record = { wrank: Layout.wrank(wrEntry, goal, context), class: Emoji.class(classKey) || classKey || "?", name: paddedBlock, - indicators: cockroach, - score: `${scoreEmoji} ${TextAlign.padToMax(scoreText, scoreColumn)}`, + indicators: "", // already merged into name + score: `${TextAlign.gap(SCORE_GAP)}${scoreEmoji} ${TextAlign.padToMax(scoreText, scoreColumn)}`, kd: TextAlign.gap(KD_GAP) + TextAlign.padToMax(kdText, kdColumn), }; @@ -71,8 +76,11 @@ import { WRankWeek } from "@root/src/systems/wrank"; 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}`; + log.debug(`prefixText: "${prefixText}" estimatedWidth: ${TextAlign.estimateWidth(prefixText)}`); + log.debug(`prefixGap length: ${prefixGap.length} chars`); + const scoreEmojiWidth = TextAlign.estimateWidth(`${scoreEmoji} `); +const statsLine = `${prefixGap}${TextAlign.gap(SCORE_GAP)}${TextAlign.padLeft("", scoreEmojiWidth)}${TextAlign.padToMax(atkText, scoreColumn)} ${TextAlign.gap(DEF_GAP)}${TextAlign.padToMax(defText, kdColumn)} ${TextAlign.gap(HEAL_GAP)}${healText}`; + // 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}`; } diff --git a/src/ui/text-align.ts b/src/ui/text-align.ts index c4561f1..647ce60 100644 --- a/src/ui/text-align.ts +++ b/src/ui/text-align.ts @@ -123,7 +123,7 @@ const current = TextAlign.estimateWidth(text); const diff = targetWidth - current; const fillerCount = diff > 0 ? Math.round(diff / FILLER_WIDTH) : 0; - log.debug(`"${text}": estimatedWidth=${current.toFixed(3)} target=${targetWidth.toFixed(3)} diff=${diff.toFixed(3)} fillerCount=${fillerCount}`); + // log.debug(`"${text}": estimatedWidth=${current.toFixed(3)} target=${targetWidth.toFixed(3)} diff=${diff.toFixed(3)} fillerCount=${fillerCount}`); if (fillerCount <= 0) return text; return text + FILLER.repeat(fillerCount); },