Ostatnio aktywny 1 month ago

Rewizja 1d94fabb98daef8ffa3ea2360f52c7c44f967fca

switch.ts Surowy
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 { getEffectiveCharacter } from "@systems/borrow";
6import { setSessionBorrow, getSessionBorrow, setPersistentPreference, clearPersistentPreference } from "@systems/borrow";
7import { polls, updatePollMessage } from "@systems/poll";
8import { getClassEmoji } from "@systems/emojis";
9import { replyAndDelete } from "@src/utils";
10import { format } from "@format";
11
12import fs from "fs";
13import path from "path";
14
15const CHARS_PATH = path.join(__dirname, "../../data/characters.json");
16
17function findSharedChar(userKey: string, charName: string): { ownerKey: string; char: any } | null {
18 try {
19 const chars = JSON.parse(fs.readFileSync(CHARS_PATH, "utf8"));
20 for (const [ownerKey, data] of Object.entries(chars) as [string, any][]) {
21 if (ownerKey === userKey) continue;
22 const char = data.characters?.find(
23 (c: any) => c.name.toLowerCase() === charName.toLowerCase() && c.sharedWith?.includes(userKey)
24 );
25 if (char) return { ownerKey, char };
26 }
27 } catch {}
28 return null;
29}
30
31// Reverse-lookup: find Discord userId for a userKey from current poll voters
32function findUserIdInPoll(state: any, userKey: string): string | null {
33 for (const [id, entry] of [...state.yes.entries(), ...state.no.entries()]) {
34 if (entry.userKey === userKey) return id;
35 }
36 return null;
37}
38
39export async function handleSwitch(interaction: ChatInputCommandInteraction): Promise<void> {
40 const member = await interaction.guild!.members.fetch(interaction.user.id);
41 const isOfficer = hasOfficerRole(member, cfg("officerRoles"));
42 const nameArg = interaction.options.getString("name");
43 const charName = interaction.options.getString("char_name", true);
44
45 let userKey: string | null;
46 if (nameArg) {
47 if (!isOfficer) return void replyAndDelete(interaction, "❌ Only officers can switch other players' characters.");
48 userKey = nameArg;
49 } else {
50 const user = await resolveUser(member);
51 userKey = user.userKey;
52 }
53
54 if (!userKey) return void replyAndDelete(interaction, "❌ You are not registered in the system.");
55
56 let resolvedChar: any = null;
57 let borrowedFrom: string | null = null;
58
59 // Try own characters first
60 const set = setActiveCharacter(userKey, charName);
61 if (set) {
62 clearPersistentPreference(userKey);
63 resolvedChar = getActiveCharacter(userKey);
64 } else {
65 // Fall back to shared characters
66 const shared = findSharedChar(userKey, charName);
67 if (shared) {
68 setSessionBorrow(userKey, shared.ownerKey, shared.char.name);
69 setPersistentPreference(userKey, shared.ownerKey, shared.char.name);
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 const currentActive = getEffectiveCharacter(userKey);
78 if (currentActive.char?.name === resolvedChar.name) {
79 // Already active — just show current state
80 const classEmoji = getClassEmoji(resolvedChar.class) || resolvedChar.class;
81 const borrowNote = currentActive.borrowedFrom ? ` *(shared by ${currentActive.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 === (nameArg ? `injected:${userKey}` : interaction.user.id) ||
91 id === `impersonated:${userKey}`;
92 if (!isOwnEntry && entry.characterName === resolvedChar.name && entry.userKey !== userKey) {
93 // Character is taken — check if we're the owner
94 const isOwner = getCharacters(userKey).some((c) => c.name === resolvedChar!.name);
95 if (isOwner) {
96 // Show conflict embed — need a button interaction for this, so just error with instructions
97 const slotHour = slot !== undefined ? polls.get(slot!)?.slot : cfg("slots")[0]?.tgHour ?? 20;
98 const charDisplay = format.char(resolvedChar);
99 return void replyAndDelete(interaction,
100 `⚠️ ${charDisplay} is already scheduled for TG at ${slotHour}:00. Use the poll buttons to reclaim it.`,
101 true
102 );
103 }
104 const slotHour = slot !== undefined ? polls.get(slot)?.slot : cfg("slots")[0]?.tgHour ?? 20;
105 return void replyAndDelete(interaction,
106 `${format.char(resolvedChar)} is already scheduled for TG at ${slotHour}:00. Pick a different character.`,
107 true
108 );
109 }
110 }
111 }
112
113 // Update poll embed if user has voted
114 if (slot !== undefined) {
115 const state = polls.get(slot)!;
116 const userId = nameArg
117 ? findUserIdInPoll(state, userKey)
118 : interaction.user.id;
119
120 if (userId && (state.yes.has(userId) || state.no.has(userId))) {
121 const updateEntry = (map: Map<string, any>) => {
122 const entry = map.get(userId);
123 if (entry) {
124 entry.characterName = resolvedChar.name;
125 entry.characterClass = resolvedChar.class;
126 entry.characterLevel = resolvedChar.level;
127 entry.characterNation = resolvedChar.nation;
128 entry.borrowedFrom = borrowedFrom ?? undefined;
129 }
130 };
131 updateEntry(state.yes);
132 updateEntry(state.no);
133
134 const channel = await interaction.client.channels.fetch(cfg("pollChannelId")) as TextChannel;
135 await updatePollMessage(channel, slot);
136 }
137 }
138
139 const classEmoji = getClassEmoji(resolvedChar.class) || resolvedChar.class;
140 const borrowNote = borrowedFrom ? ` *(shared by ${borrowedFrom})*` : "";
141 return void replyAndDelete(interaction, `🔄 ${classEmoji} ${resolvedChar.level} ${resolvedChar.name}${borrowNote}`, true);
142}