Последняя активность 1 month ago

nuno ревизий этого фрагмента 1 month ago. К ревизии

1 file changed, 156 insertions

modals.ts(файл создан)

@@ -0,0 +1,156 @@
1 + import {
2 + ModalSubmitInteraction,
3 + ModalBuilder,
4 + TextInputBuilder,
5 + TextInputStyle,
6 + ActionRowBuilder,
7 + } from "discord.js";
8 + import { resolveUser } from "@systems/users";
9 + import { getEffectiveCharacter } from "@systems/borrow";
10 + import { score } from "@subcommands/score/submitCore";
11 + import { format } from "@format";
12 +
13 + // ─── Modal IDs ────────────────────────────────────────────────────────────────
14 + //
15 + // score_submit:<slot> — score submission modal, slot baked into the customId
16 + // so we don't need to pass state through the modal itself
17 +
18 + export namespace modals {
19 +
20 + // ─── Builder ───────────────────────────────────────────────────────────────
21 +
22 + /**
23 + * Build the score submission modal for a given userKey + slot.
24 + * Title shows the active character so the user knows what they're submitting for.
25 + */
26 + export function buildScoreModal(userKey: string, slot: number): ModalBuilder {
27 + const { char } = getEffectiveCharacter(userKey);
28 + // const charLabel = char ? format.char(char) : "your character";
29 + const charLabel = char ? format.char(char, { emoji: false }) : "your character";
30 +
31 + const modal = new ModalBuilder()
32 + .setCustomId(`score_submit:${slot}`)
33 + .setTitle(`Score for ${charLabel} — ${String(slot).padStart(2, "0")}:00`);
34 +
35 + const ptsInput = new TextInputBuilder()
36 + .setCustomId("pts")
37 + .setLabel("Points")
38 + .setStyle(TextInputStyle.Short)
39 + .setPlaceholder("e.g. 3000")
40 + .setRequired(true);
41 +
42 + const kdInput = new TextInputBuilder()
43 + .setCustomId("kd")
44 + .setLabel("Kills / Deaths (e.g. 5/2)")
45 + .setStyle(TextInputStyle.Short)
46 + .setPlaceholder("5/2")
47 + .setRequired(false);
48 +
49 + const atkDefInput = new TextInputBuilder()
50 + .setCustomId("atkdef")
51 + .setLabel("ATK / DEF (e.g. 120/80)")
52 + .setStyle(TextInputStyle.Short)
53 + .setPlaceholder("120/80")
54 + .setRequired(false);
55 +
56 + const healInput = new TextInputBuilder()
57 + .setCustomId("heal")
58 + .setLabel("Heal")
59 + .setStyle(TextInputStyle.Short)
60 + .setPlaceholder("e.g. 500")
61 + .setRequired(false);
62 +
63 + modal.addComponents(
64 + new ActionRowBuilder<TextInputBuilder>().addComponents(ptsInput),
65 + new ActionRowBuilder<TextInputBuilder>().addComponents(kdInput),
66 + new ActionRowBuilder<TextInputBuilder>().addComponents(atkDefInput),
67 + new ActionRowBuilder<TextInputBuilder>().addComponents(healInput),
68 + );
69 +
70 + return modal;
71 + }
72 +
73 + // ─── Parser helpers ────────────────────────────────────────────────────────
74 +
75 + function parseSlashPair(raw: string | null): [number, number] | null {
76 + if (!raw) return null;
77 + const parts = raw.trim().split("/");
78 + if (parts.length !== 2) return null;
79 + const a = parseInt(parts[0], 10);
80 + const b = parseInt(parts[1], 10);
81 + if (isNaN(a) || isNaN(b)) return null;
82 + return [a, b];
83 + }
84 +
85 + function parseOptionalInt(raw: string | null): number | undefined {
86 + if (!raw) return undefined;
87 + const n = parseInt(raw.trim(), 10);
88 + return isNaN(n) ? undefined : n;
89 + }
90 +
91 + // ─── Handler ───────────────────────────────────────────────────────────────
92 +
93 + export async function handleModal(interaction: ModalSubmitInteraction): Promise<void> {
94 + if (interaction.customId.startsWith("score_submit:")) {
95 + await handleScoreSubmit(interaction);
96 + return;
97 + }
98 + // Future modals routed here by customId prefix
99 + }
100 +
101 + async function handleScoreSubmit(interaction: ModalSubmitInteraction): Promise<void> {
102 + await interaction.deferReply({ ephemeral: true });
103 +
104 + const slotStr = interaction.customId.split(":")[1];
105 + const slot = parseInt(slotStr, 10);
106 + if (isNaN(slot)) {
107 + await interaction.editReply("❌ Invalid slot in modal.");
108 + return;
109 + }
110 +
111 + const member = await interaction.guild!.members.fetch(interaction.user.id);
112 + const user = await resolveUser(member);
113 + if (!user.userKey) {
114 + await interaction.editReply("❌ You are not registered in the system.");
115 + return;
116 + }
117 +
118 + // Parse fields
119 + const ptsRaw = interaction.fields.getTextInputValue("pts");
120 + const kdRaw = interaction.fields.getTextInputValue("kd") || null;
121 + const atkDefRaw = interaction.fields.getTextInputValue("atkdef") || null;
122 + const healRaw = interaction.fields.getTextInputValue("heal") || null;
123 +
124 + const pts = parseInt(ptsRaw.trim(), 10);
125 + if (isNaN(pts)) {
126 + await interaction.editReply("❌ Points must be a number.");
127 + return;
128 + }
129 +
130 + const kd = parseSlashPair(kdRaw);
131 + const atkDef = parseSlashPair(atkDefRaw);
132 + const heal = parseOptionalInt(healRaw);
133 +
134 + if (kdRaw && !kd) {
135 + await interaction.editReply("❌ K/D must be in `kills/deaths` format, e.g. `5/2`.");
136 + return;
137 + }
138 + if (atkDefRaw && !atkDef) {
139 + await interaction.editReply("❌ ATK/DEF must be in `atk/def` format, e.g. `120/80`.");
140 + return;
141 + }
142 +
143 + const result = await score.submitForUser({
144 + userKey: user.userKey,
145 + pts,
146 + slot,
147 + k: kd?.[0],
148 + d: kd?.[1],
149 + atk: atkDef?.[0],
150 + def: atkDef?.[1],
151 + heal,
152 + });
153 +
154 + await interaction.editReply(result.message);
155 + }
156 + }
Новее Позже