import { ModalSubmitInteraction, ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder, } from "discord.js"; import { resolveUser } from "@systems/users"; import { getEffectiveCharacter } from "@systems/borrow"; import { score } from "@subcommands/score/submitCore"; import { format } from "@format"; // ─── 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 modal = new ModalBuilder() .setCustomId(`score_submit:${slot}`) .setTitle(`Score for ${charLabel} — ${String(slot).padStart(2, "0")}:00`); const ptsInput = new TextInputBuilder() .setCustomId("pts") .setLabel("Points") .setStyle(TextInputStyle.Short) .setPlaceholder("e.g. 3000") .setRequired(true); const kdInput = new TextInputBuilder() .setCustomId("kd") .setLabel("Kills / Deaths (e.g. 5/2)") .setStyle(TextInputStyle.Short) .setPlaceholder("5/2") .setRequired(false); const atkDefInput = new TextInputBuilder() .setCustomId("atkdef") .setLabel("ATK / DEF (e.g. 120/80)") .setStyle(TextInputStyle.Short) .setPlaceholder("120/80") .setRequired(false); const healInput = new TextInputBuilder() .setCustomId("heal") .setLabel("Heal") .setStyle(TextInputStyle.Short) .setPlaceholder("e.g. 500") .setRequired(false); modal.addComponents( new ActionRowBuilder().addComponents(ptsInput), new ActionRowBuilder().addComponents(kdInput), new ActionRowBuilder().addComponents(atkDefInput), new ActionRowBuilder().addComponents(healInput), ); return modal; } // ─── Parser helpers ──────────────────────────────────────────────────────── function parseSlashPair(raw: string | null): [number, number] | null { if (!raw) return null; const parts = raw.trim().split("/"); if (parts.length !== 2) return null; const a = parseInt(parts[0], 10); const b = parseInt(parts[1], 10); if (isNaN(a) || isNaN(b)) return null; return [a, b]; } function parseOptionalInt(raw: string | null): number | undefined { if (!raw) return undefined; const n = parseInt(raw.trim(), 10); return isNaN(n) ? undefined : n; } // ─── Handler ─────────────────────────────────────────────────────────────── export async function handleModal(interaction: ModalSubmitInteraction): Promise { if (interaction.customId.startsWith("score_submit:")) { await handleScoreSubmit(interaction); return; } // Future modals routed here by customId prefix } async function handleScoreSubmit(interaction: ModalSubmitInteraction): Promise { await interaction.deferReply({ ephemeral: true }); const slotStr = interaction.customId.split(":")[1]; const slot = parseInt(slotStr, 10); if (isNaN(slot)) { await interaction.editReply("❌ Invalid slot in modal."); return; } 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; } // 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 pts = parseInt(ptsRaw.trim(), 10); if (isNaN(pts)) { await interaction.editReply("❌ Points must be a number."); return; } const kd = parseSlashPair(kdRaw); const atkDef = parseSlashPair(atkDefRaw); 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, pts, slot, k: kd?.[0], d: kd?.[1], atk: atkDef?.[0], def: atkDef?.[1], heal, }); await interaction.editReply(result.message); } }