Last active 4 weeks ago

Revision aef54acffe754e05ed441a6ef334fe8faa02eddd

switch.ts Raw
1import { ChatInputCommandInteraction, TextChannel } from "discord.js";
2import { cfg } from "@systems/config";
3import { resolveUser, hasOfficerRole } from "@systems/users";
4import { setActiveCharacter, getActiveCharacter, getCharacterByName, getCharacters } from "@systems/characters";
5import {
6 getEffectiveCharacter,
7 setSessionBorrow,
8 setPersistentPreference,
9 clearPersistentPreference,
10 clearSessionBorrowForUser,
11} from "@systems/borrow";
12import { polls, updatePollMessage } from "@systems/poll";
13import { getClassEmoji } from "@systems/emojis";
14import { replyAndDelete } from "@src/utils";
15import { format } from "@format";
16import { buildCharSelectButtons } from "@systems/charSelect";
17import fs from "fs";
18import path from "path";
19
20const CHARS_PATH = path.join(__dirname, "../../data/characters.json");
21
22function findSharedChar(userKey: string, charName: string): { ownerKey: string; char: any } | null {
23 try {
24 const chars = JSON.parse(fs.readFileSync(CHARS_PATH, "utf8"));
25 for (const [ownerKey, data] of Object.entries(chars) as [string, any][]) {
26 if (ownerKey === userKey) continue;
27 const char = data.characters?.find(
28 (c: any) => c.name.toLowerCase() === charName.toLowerCase() && c.sharedWith?.includes(userKey)
29 );
30 if (char) return { ownerKey, char };
31 }
32 } catch {}
33 return null;
34}
35
36function findVoteIdInPoll(state: any, userKey: string): string | null {
37 for (const [id, entry] of [...state.yes.entries(), ...state.no.entries()]) {
38 if (entry.userKey === userKey) return id;
39 }
40 return null;
41}
42
43export async function handleSwitch(interaction: ChatInputCommandInteraction): Promise<void> {
44 const member = await interaction.guild!.members.fetch(interaction.user.id);
45 const isOfficer = hasOfficerRole(member, cfg("officerRoles"));
46 const nameArg = interaction.options.getString("name");
47 const charName = interaction.options.getString("char_name", true);
48
49 let userKey: string | null;
50 if (nameArg) {
51 if (!isOfficer) return void replyAndDelete(interaction, "❌ Only officers can switch other players' characters.");
52 userKey = nameArg;
53 } else {
54 const user = await resolveUser(member);
55 userKey = user.userKey;
56 }
57
58 if (!userKey) return void replyAndDelete(interaction, "❌ You are not registered in the system.");
59
60 // Resolve the target character without switching yet
61 let resolvedChar: any = null;
62 let borrowedFrom: string | null = null;
63
64 const ownChar = getCharacterByName(userKey, charName);
65 if (ownChar) {
66 resolvedChar = ownChar;
67 } else {
68 const shared = findSharedChar(userKey, charName);
69 if (shared) {
70 resolvedChar = shared.char;
71 borrowedFrom = shared.ownerKey;
72 }
73 }
74
75 if (!resolvedChar) return void replyAndDelete(interaction, `❌ No character named **${charName}** found.`);
76
77 // If already active — just show current state without switching
78 const current = getEffectiveCharacter(userKey);
79 if (current.char?.name === resolvedChar.name) {
80 const classEmoji = getClassEmoji(resolvedChar.class) || resolvedChar.class;
81 const borrowNote = current.borrowedFrom ? ` *(shared by ${current.borrowedFrom})*` : "";
82 return void replyAndDelete(interaction, `${classEmoji} ${resolvedChar.level} ${resolvedChar.name}${borrowNote}`, true);
83 }
84
85 // Check if target character is already in the active poll by another player
86 const slot = [...polls.keys()][0];
87 if (slot !== undefined) {
88 const state = polls.get(slot)!;
89 for (const [id, entry] of state.yes.entries()) {
90 const isOwnEntry = id === interaction.user.id || id === `impersonated:${userKey}`;
91 if (!isOwnEntry && entry.characterName === resolvedChar.name && entry.userKey !== userKey) {
92 const slotHour = state.slot;
93 const charDisplay = format.char(resolvedChar);
94 const isOwner = getCharacters(userKey).some((c) => c.name === resolvedChar.name);
95 if (isOwner) {
96 await interaction.reply({
97 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:`,
98 components: buildCharSelectButtons(userKey, {
99 customIdPrefix: `switch_after_reclaim:${userKey}`,
100 excludeCharName: resolvedChar.name,
101 appendToCustomId: ":yes",
102 }),
103 ephemeral: true,
104 });
105 return;
106 }
107 const buttons = buildCharSelectButtons(userKey, {
108 customIdPrefix: `switch_after_reclaim:${userKey}`,
109 excludeCharName: resolvedChar.name,
110 appendToCustomId: `:${"yes"}`,
111 });
112 await interaction.reply({
113 content: `${charDisplay} is already scheduled for TG at ${slotHour}:00. Pick a different character.`,
114 components: buttons,
115 ephemeral: true,
116 });
117 return;
118 }
119 }
120 }
121
122 // Now actually switch
123 if (borrowedFrom) {
124 setSessionBorrow(userKey, borrowedFrom, resolvedChar.name);
125 setPersistentPreference(userKey, borrowedFrom, resolvedChar.name);
126 } else {
127 setActiveCharacter(userKey, charName);
128 clearPersistentPreference(userKey);
129 clearSessionBorrowForUser(userKey);
130 resolvedChar = getActiveCharacter(userKey);
131 }
132
133 // Update poll embed if user has already voted
134 if (slot !== undefined) {
135 const state = polls.get(slot)!;
136 const voteId = findVoteIdInPoll(state, userKey);
137
138 if (voteId && (state.yes.has(voteId) || state.no.has(voteId))) {
139 const updateEntry = (map: Map<string, any>) => {
140 const entry = map.get(voteId);
141 if (entry) {
142 entry.characterName = resolvedChar.name;
143 entry.characterClass = resolvedChar.class;
144 entry.characterLevel = resolvedChar.level;
145 entry.characterNation = resolvedChar.nation;
146 entry.borrowedFrom = borrowedFrom ?? undefined;
147 }
148 };
149 updateEntry(state.yes);
150 updateEntry(state.no);
151
152 const channel = await interaction.client.channels.fetch(cfg("pollChannelId")) as TextChannel;
153 await updatePollMessage(channel, slot);
154 }
155 }
156
157 const classEmoji = getClassEmoji(resolvedChar.class) || resolvedChar.class;
158 const borrowNote = borrowedFrom ? ` *(shared by ${borrowedFrom})*` : "";
159 return void replyAndDelete(interaction, `🔄 ${classEmoji} ${resolvedChar.level} ${resolvedChar.name}${borrowNote}`, true);
160}