async function handleSwitchAfterReclaim(btn: ButtonInteraction): Promise { const prefix = btn.customId.startsWith("companion_switch:") ? "companion_switch:" : "switch_after_reclaim:"; const withoutPrefix = btn.customId.slice(prefix.length); const firstColon = withoutPrefix.indexOf(":"); const userKey = withoutPrefix.slice(0, firstColon); const rest = withoutPrefix.slice(firstColon + 1); const lastColon = rest.lastIndexOf(":"); const charName = rest.slice(0, lastColon); const prevVoteType = (rest.slice(lastColon + 1) || "yes") as "yes" | "no"; const chars = JSON.parse( fs.readFileSync(path.join(__dirname, "../../data/characters.json"), "utf8") ); let resolvedChar: any = null; let borrowedFrom: string | null = null; // Try own char first const ownEntry = chars[userKey]?.characters?.find((c: any) => c.name === charName); if (ownEntry) { setActiveCharacter(userKey, charName); clearSessionBorrowForUser(userKey); clearPersistentPreference(userKey); resolvedChar = ownEntry; } else { // Try shared char for (const [ownerKey, data] of Object.entries(chars) as [string, any][]) { const char = data.characters?.find( (c: any) => c.name === charName && c.sharedWith?.includes(userKey) ); if (char) { setPersistentPreference(userKey, ownerKey, charName); clearSessionBorrowForUser(userKey); resolvedChar = char; borrowedFrom = ownerKey; break; } } } if (!resolvedChar) { await btn.reply({ content: `❌ Could not switch to **${charName}**.`, ephemeral: true }); return; } // Re-add to poll with previous vote type const slot = [...polls.keys()][0]; const state = slot !== undefined ? polls.get(slot) : null; let existingVoteId: string | null = null; if (state) { for (const [id, entry] of [...state.yes.entries(), ...state.no.entries()]) { if (entry.userKey === userKey) { existingVoteId = id; break; } } } // Conflict check if (state && resolvedChar) { for (const [id, entry] of state.yes.entries()) { if (id !== existingVoteId && id !== btn.user.id && entry.characterName === resolvedChar.name && entry.userKey !== userKey) { await btn.reply({ content: `❌ ${format.char(resolvedChar)} is already in the poll by another player.`, ephemeral: true, }); return; } } } if (state && !state.locked && state.confirmed === null) { const { char } = getEffectiveCharacter(userKey); const now = nowFormatted(); const publicMsg = resolveMessage("public", prevVoteType, 1, userKey, null, null); const voteEntry = { userKey, displayName: charName, characterName: char?.name ?? charName, characterClass: char?.class ?? resolvedChar.class, characterLevel: char?.level ?? resolvedChar.level, characterNation: char?.nation ?? resolvedChar.nation, borrowedFrom: borrowedFrom ?? undefined, discordId: btn.user.id, votedAt: now, publicMessage: publicMsg ?? undefined, }; // Find and reuse existing vote ID — avoids duplicate entries let existingVoteId: string | null = null; for (const [id, entry] of [...state.yes.entries(), ...state.no.entries()]) { if (entry.userKey === userKey) { if (!existingVoteId) existingVoteId = id; state.yes.delete(id); state.no.delete(id); } } const voteId = existingVoteId ?? btn.user.id; if (prevVoteType === "yes") { state.yes.set(voteId, voteEntry); } else { state.no.set(voteId, voteEntry); } console.log(`[switch_reclaim] cleaning up for userKey=${userKey}`); console.log(`[switch_reclaim] yes keys:`, [...state.yes.entries()].map(([id, e]) => `${id}:${e.userKey}`)); console.log(`[switch_reclaim] no keys:`, [...state.no.entries()].map(([id, e]) => `${id}:${e.userKey}`)); const channel = await btn.client.channels.fetch(cfg("pollChannelId")) as TextChannel; await updatePollMessage(channel, slot!); } const charDisplay = resolvedChar ? format.char(resolvedChar) : charName; const borrowNote = borrowedFrom ? ` *(shared by ${borrowedFrom})*` : ""; await btn.reply({ content: `🔄 ${charDisplay}${borrowNote}${state ? ` — re-added to poll as **${prevVoteType}**.` : ""}`, ephemeral: true, }); }