Última atividade 1 month ago

gistfile1.txt Bruto
1import {
2 Client,
3 ButtonBuilder,
4 ButtonStyle,
5 ActionRowBuilder,
6 EmbedBuilder,
7 ButtonInteraction,
8 TextChannel,
9} from "discord.js";
10import { cfg } from "./config";
11import { getCharacters, getCharacterByName, setActiveCharacter } from "./characters";
12import { clearSessionBorrowForUser, setPersistentPreference, getEffectiveCharacter } from "./borrow";
13import { polls, updatePollMessage, createVoteEntry } from "./poll";
14import { resolveMessage, nowFormatted } from "./messages";
15import { getClassEmoji } from "./emojis";
16import { Character } from "../types";
17
18const AUTO_VOTE_ON_SWITCH = process.env.AUTO_VOTE_ON_CONFLICT_SWITCH !== "false";
19
20// Stores pending conflict resolutions: buttonId → { ownerUsermapKey, borrowerUsermapKey, charName, ownerId }
21const pendingConflicts = new Map<string, {
22 ownerUsermapKey: string;
23 borrowerUsermapKey: string;
24 charName: string;
25 ownerId: string;
26 page: number;
27}>();
28
29function formatChar(char: Character): string {
30 const emoji = getClassEmoji(char.class) || char.class;
31 return `${emoji} ${char.level} ${char.name}`;
32}
33
34// Parse <:name:id> or unicode emoji string for use with ButtonBuilder.setEmoji()
35function parseEmoji(emojiStr: string): { name: string; id: string } | string | null {
36 if (!emojiStr) return null;
37 const match = emojiStr.match(/^<:(\w+):(\d+)>$/);
38 if (match) return { name: match[1], id: match[2] };
39 return emojiStr; // unicode fallback
40}
41
42// For button labels — emoji via setEmoji(), text only in label
43function applyCharToButton(btn: ButtonBuilder, char: Character): ButtonBuilder {
44 const emojiStr = getClassEmoji(char.class);
45 const emoji = parseEmoji(emojiStr);
46 btn.setLabel(`${char.level} ${char.name}`);
47 if (emoji) btn.setEmoji(emoji as any);
48 return btn;
49}
50
51function buildConflictEmbed(
52 borrowerKey: string,
53 char: Character,
54 ownerKey: string
55): EmbedBuilder {
56 const charDisplay = formatChar(char);
57 return new EmbedBuilder()
58 .setTitle("⚠️ Character Conflict")
59 .setDescription(
60 `**${charDisplay}** is currently borrowed by **${borrowerKey}** for tonight's TG.\n\nYou can reclaim your character or switch to another one.`
61 )
62 .setColor(0xe8a317);
63}
64
65function buildConflictButtons(
66 ownerUsermapKey: string,
67 borrowerUsermapKey: string,
68 borrowedCharName: string,
69 ownerId: string,
70 allChars: Character[],
71 page: number
72): ActionRowBuilder<ButtonBuilder>[] {
73 const PAGE_SIZE = 4; // leave 1 slot for reclaim on first row
74 const otherChars = allChars.filter((c) => c.name !== borrowedCharName);
75 const pageChars = otherChars.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE);
76 const hasMore = otherChars.length > (page + 1) * PAGE_SIZE;
77 const hasPrev = page > 0;
78
79 const rows: ActionRowBuilder<ButtonBuilder>[] = [];
80
81 // Row 1: char switch buttons
82 const charButtons = pageChars.map((char) => {
83 const id = `conflict_switch:${ownerUsermapKey}:${borrowerUsermapKey}:${char.name}:${ownerId}`;
84 pendingConflicts.set(id, { ownerUsermapKey, borrowerUsermapKey, charName: borrowedCharName, ownerId, page });
85 return applyCharToButton(
86 new ButtonBuilder().setCustomId(id).setStyle(ButtonStyle.Secondary),
87 char
88 );
89 });
90
91 if (charButtons.length > 0) {
92 rows.push(new ActionRowBuilder<ButtonBuilder>().addComponents(...charButtons));
93 }
94
95 // Row 2: reclaim + pagination
96 const reclaimId = `conflict_reclaim:${ownerUsermapKey}:${borrowerUsermapKey}:${borrowedCharName}:${ownerId}`;
97 pendingConflicts.set(reclaimId, { ownerUsermapKey, borrowerUsermapKey, charName: borrowedCharName, ownerId, page });
98
99 const borrowed = allChars.find((c) => c.name === borrowedCharName);
100 const reclaimBtn = borrowed
101 ? applyCharToButton(
102 new ButtonBuilder().setCustomId(reclaimId).setStyle(ButtonStyle.Danger),
103 borrowed
104 ).setLabel(`↩️ ${borrowed.level} ${borrowed.name}`)
105 : new ButtonBuilder().setCustomId(reclaimId).setLabel(`↩️ Reclaim ${borrowedCharName}`).setStyle(ButtonStyle.Danger);
106
107 const navButtons: ButtonBuilder[] = [reclaimBtn];
108
109 if (hasPrev) {
110 const prevId = `conflict_page:${ownerUsermapKey}:${borrowerUsermapKey}:${borrowedCharName}:${ownerId}:${page - 1}`;
111 pendingConflicts.set(prevId, { ownerUsermapKey, borrowerUsermapKey, charName: borrowedCharName, ownerId, page: page - 1 });
112 navButtons.push(new ButtonBuilder().setCustomId(prevId).setLabel("← Prev").setStyle(ButtonStyle.Primary));
113 }
114 if (hasMore) {
115 const nextId = `conflict_page:${ownerUsermapKey}:${borrowerUsermapKey}:${borrowedCharName}:${ownerId}:${page + 1}`;
116 pendingConflicts.set(nextId, { ownerUsermapKey, borrowerUsermapKey, charName: borrowedCharName, ownerId, page: page + 1 });
117 navButtons.push(new ButtonBuilder().setCustomId(nextId).setLabel("Next →").setStyle(ButtonStyle.Primary));
118 }
119
120 rows.push(new ActionRowBuilder<ButtonBuilder>().addComponents(...navButtons));
121 return rows;
122}
123
124// Show conflict embed to owner
125export async function showConflictEmbed(
126 interaction: ButtonInteraction,
127 ownerUsermapKey: string,
128 borrowerUsermapKey: string,
129 borrowedChar: Character,
130 allOwnerChars: Character[]
131): Promise<void> {
132 const embed = buildConflictEmbed(borrowerUsermapKey, borrowedChar, ownerUsermapKey);
133 const buttons = buildConflictButtons(
134 ownerUsermapKey, borrowerUsermapKey, borrowedChar.name,
135 interaction.user.id, allOwnerChars, 0
136 );
137 await interaction.followUp({ embeds: [embed], components: buttons, ephemeral: true });
138}
139
140// Handle conflict button interactions
141export async function handleConflictButton(interaction: ButtonInteraction): Promise<void> {
142 const { customId } = interaction;
143
144 if (customId.startsWith("conflict_page:")) {
145 const parts = customId.split(":");
146 const ownerKey = parts[1];
147 const borrowerKey = parts[2];
148 const charName = parts[3];
149 const ownerId = parts[4];
150 const page = parseInt(parts[5]);
151
152 const allChars = getCharacters(ownerKey);
153 const borrowed = allChars.find((c) => c.name === charName);
154 if (!borrowed) return void interaction.reply({ content: "❌ Character not found.", ephemeral: true });
155
156 const embed = buildConflictEmbed(borrowerKey, borrowed, ownerKey);
157 const buttons = buildConflictButtons(ownerKey, borrowerKey, charName, ownerId, allChars, page);
158 await interaction.update({ embeds: [embed], components: buttons });
159 return;
160 }
161
162 if (customId.startsWith("conflict_switch:")) {
163 const parts = customId.split(":");
164 const ownerKey = parts[1];
165 const borrowerKey = parts[2];
166 const newCharName = parts[3];
167 const ownerId = parts[4];
168
169 // Switch owner to the selected char
170 setActiveCharacter(ownerKey, newCharName);
171 clearSessionBorrowForUser(ownerKey);
172
173 const slot = [...polls.keys()][0];
174 const state = slot !== undefined ? polls.get(slot) : null;
175
176 if (state && AUTO_VOTE_ON_SWITCH) {
177 // Auto-vote Yes for owner with new char
178 const guild = interaction.guild!;
179 const member = await guild.members.fetch(ownerId);
180 const { char } = getEffectiveCharacter(ownerKey);
181 const now = nowFormatted();
182 const publicMsg = resolveMessage("public", "yes", 1, ownerKey, member.nickname ?? null, member.user.globalName ?? null);
183
184 state.yes.set(ownerId, {
185 userKey: ownerKey,
186 displayName: member.nickname ?? member.user.globalName ?? member.user.username,
187 characterName: char?.name,
188 characterClass: char?.class,
189 characterLevel: char?.level,
190 characterNation: char?.nation,
191 votedAt: now,
192 publicMessage: publicMsg ?? undefined,
193 });
194
195 const channel = await interaction.client.channels.fetch(cfg("pollChannelId")) as TextChannel;
196 await updatePollMessage(channel, slot!);
197 }
198
199 await interaction.update({
200 embeds: [new EmbedBuilder()
201 .setTitle("✅ Switched")
202 .setDescription(`Switched to **${newCharName}**${AUTO_VOTE_ON_SWITCH ? " and voted Yes." : "."}`)
203 .setColor(0x57f287)],
204 components: [],
205 });
206 return;
207 }
208
209 if (customId.startsWith("conflict_reclaim:")) {
210 const parts = customId.split(":");
211 const ownerKey = parts[1];
212 const borrowerKey = parts[2];
213 const charName = parts[3];
214 const ownerId = parts[4];
215
216 const reclaimBehavior = (cfg as any)("conflictReclaimBehavior") ?? "revert"; // "revert" | "remove"
217 const slot = [...polls.keys()][0];
218 const state = slot !== undefined ? polls.get(slot) : null;
219
220 if (state) {
221 // Find borrower's vote entry
222 for (const [id, entry] of [...state.yes.entries(), ...state.no.entries()]) {
223 if (entry.userKey === borrowerKey) {
224 if (reclaimBehavior === "remove") {
225 state.yes.delete(id);
226 state.no.delete(id);
227 } else {
228 // Revert borrower to their own active char
229 clearSessionBorrowForUser(borrowerKey);
230 const { char: ownChar } = getEffectiveCharacter(borrowerKey);
231 if (ownChar) {
232 entry.characterName = ownChar.name;
233 entry.characterClass = ownChar.class;
234 entry.characterLevel = ownChar.level;
235 entry.characterNation = ownChar.nation;
236 entry.borrowedFrom = undefined;
237 } else {
238 // No own char — remove from poll
239 state.yes.delete(id);
240 state.no.delete(id);
241 }
242 }
243 break;
244 }
245 }
246
247 // Owner joins with their character
248 const guild = interaction.guild!;
249 const member = await guild.members.fetch(ownerId);
250 setActiveCharacter(ownerKey, charName);
251 clearSessionBorrowForUser(ownerKey);
252 const { char } = getEffectiveCharacter(ownerKey);
253 const now = nowFormatted();
254 const publicMsg = resolveMessage("public", "yes", 1, ownerKey, member.nickname ?? null, member.user.globalName ?? null);
255
256 state.yes.set(ownerId, {
257 userKey: ownerKey,
258 displayName: member.nickname ?? member.user.globalName ?? member.user.username,
259 characterName: char?.name,
260 characterClass: char?.class,
261 characterLevel: char?.level,
262 characterNation: char?.nation,
263 votedAt: now,
264 publicMessage: publicMsg ?? undefined,
265 });
266
267 const channel = await interaction.client.channels.fetch(cfg("pollChannelId")) as TextChannel;
268 await updatePollMessage(channel, slot!);
269 }
270
271 await interaction.update({
272 embeds: [new EmbedBuilder()
273 .setTitle("↩️ Reclaimed")
274 .setDescription(`**${charName}** has been reclaimed from **${borrowerKey}** and you've been added to the poll.`)
275 .setColor(0x57f287)],
276 components: [],
277 });
278 return;
279 }
280}