import { ChatInputCommandInteraction, TextChannel } from "discord.js"; import { cfg } from "@systems/config"; import { resolveUser, hasOfficerRole } from "@systems/users"; import { setActiveCharacter, getActiveCharacter, getCharacterByName, getCharacters } from "@systems/characters"; import { getEffectiveCharacter, setSessionBorrow, setPersistentPreference, clearPersistentPreference, clearSessionBorrowForUser, } from "@systems/borrow"; import { polls, updatePollMessage } from "@systems/poll"; import { getClassEmoji } from "@systems/emojis"; import { replyAndDelete } from "@src/utils"; import { format } from "@format"; import { buildCharSelectButtons } from "@systems/charSelect"; import fs from "fs"; import path from "path"; const CHARS_PATH = path.join(__dirname, "../../data/characters.json"); function findSharedChar(userKey: string, charName: string): { ownerKey: string; char: any } | null { try { const chars = JSON.parse(fs.readFileSync(CHARS_PATH, "utf8")); for (const [ownerKey, data] of Object.entries(chars) as [string, any][]) { if (ownerKey === userKey) continue; const char = data.characters?.find( (c: any) => c.name.toLowerCase() === charName.toLowerCase() && c.sharedWith?.includes(userKey) ); if (char) return { ownerKey, char }; } } catch {} return null; } function findVoteIdInPoll(state: any, userKey: string): string | null { for (const [id, entry] of [...state.yes.entries(), ...state.no.entries()]) { if (entry.userKey === userKey) return id; } return null; } export async function handleSwitch(interaction: ChatInputCommandInteraction): Promise { const member = await interaction.guild!.members.fetch(interaction.user.id); const isOfficer = hasOfficerRole(member, cfg("officerRoles")); const nameArg = interaction.options.getString("name"); const charName = interaction.options.getString("char_name", true); let userKey: string | null; if (nameArg) { if (!isOfficer) return void replyAndDelete(interaction, "❌ Only officers can switch other players' characters."); userKey = nameArg; } else { const user = await resolveUser(member); userKey = user.userKey; } if (!userKey) return void replyAndDelete(interaction, "❌ You are not registered in the system."); // Resolve the target character without switching yet let resolvedChar: any = null; let borrowedFrom: string | null = null; const ownChar = getCharacterByName(userKey, charName); if (ownChar) { resolvedChar = ownChar; } else { const shared = findSharedChar(userKey, charName); if (shared) { resolvedChar = shared.char; borrowedFrom = shared.ownerKey; } } if (!resolvedChar) return void replyAndDelete(interaction, `❌ No character named **${charName}** found.`); // If already active — just show current state without switching const current = getEffectiveCharacter(userKey); if (current.char?.name === resolvedChar.name) { const classEmoji = getClassEmoji(resolvedChar.class) || resolvedChar.class; const borrowNote = current.borrowedFrom ? ` *(shared by ${current.borrowedFrom})*` : ""; return void replyAndDelete(interaction, `${classEmoji} ${resolvedChar.level} ${resolvedChar.name}${borrowNote}`, true); } // Check if target character is already in the active poll by another player const slot = [...polls.keys()][0]; if (slot !== undefined) { const state = polls.get(slot)!; for (const [id, entry] of state.yes.entries()) { const isOwnEntry = id === interaction.user.id || id === `impersonated:${userKey}`; if (!isOwnEntry && entry.characterName === resolvedChar.name && entry.userKey !== userKey) { const slotHour = state.slot; const charDisplay = format.char(resolvedChar); const isOwner = getCharacters(userKey).some((c) => c.name === resolvedChar.name); if (isOwner) { await interaction.reply({ content: `⚠️ ${charDisplay} is already scheduled for TG at ${slotHour}:00. Vote with your character to trigger the reclaim option, or switch to a different one:`, components: buildCharSelectButtons(userKey, { customIdPrefix: `switch_after_reclaim:${userKey}`, excludeCharName: resolvedChar.name, appendToCustomId: ":yes", }), ephemeral: true, }); return; } const buttons = buildCharSelectButtons(userKey, { customIdPrefix: `switch_after_reclaim:${userKey}`, excludeCharName: resolvedChar.name, appendToCustomId: `:${"yes"}`, }); await interaction.reply({ content: `❌ ${charDisplay} is already scheduled for TG at ${slotHour}:00. Pick a different character.`, components: buttons, ephemeral: true, }); return; } } } // Now actually switch if (borrowedFrom) { setSessionBorrow(userKey, borrowedFrom, resolvedChar.name); setPersistentPreference(userKey, borrowedFrom, resolvedChar.name); } else { setActiveCharacter(userKey, charName); clearPersistentPreference(userKey); clearSessionBorrowForUser(userKey); resolvedChar = getActiveCharacter(userKey); } // Update poll embed if user has already voted if (slot !== undefined) { const state = polls.get(slot)!; const voteId = findVoteIdInPoll(state, userKey); if (voteId && (state.yes.has(voteId) || state.no.has(voteId))) { const updateEntry = (map: Map) => { const entry = map.get(voteId); if (entry) { entry.characterName = resolvedChar.name; entry.characterClass = resolvedChar.class; entry.characterLevel = resolvedChar.level; entry.characterNation = resolvedChar.nation; entry.borrowedFrom = borrowedFrom ?? undefined; } }; updateEntry(state.yes); updateEntry(state.no); const channel = await interaction.client.channels.fetch(cfg("pollChannelId")) as TextChannel; await updatePollMessage(channel, slot); } } const classEmoji = getClassEmoji(resolvedChar.class) || resolvedChar.class; const borrowNote = borrowedFrom ? ` *(shared by ${borrowedFrom})*` : ""; return void replyAndDelete(interaction, `🔄 ${classEmoji} ${resolvedChar.level} ${resolvedChar.name}${borrowNote}`, true); }