Zuletzt aktiv 3 weeks ago

autocomplete.ts Originalformat
1import { AutocompleteInteraction } from "discord.js";
2import { resolveUser } from "@systems/users";
3import { getCharacters } from "@systems/characters";
4import { cfg } from "@systems/config";
5import { getNationEmoji } from "@systems/emojis";
6import fs from "fs";
7import path from "path";
8import { Nation } from "@types";
9import { Paths } from "@helpers/paths";
10
11let _charCache: Record<string, any> | null = null;
12
13function getCharCache(): Record<string, any> | null {
14 if (!_charCache) {
15 try {
16 _charCache = JSON.parse(fs.readFileSync(Paths.data("characters.json"), "utf8"));
17 } catch {
18 _charCache = {};
19 }
20 }
21 return _charCache;
22}
23
24export function invalidateCharCache(): void {
25 _charCache = null;
26}
27
28// ─── Autocomplete subsets ─────────────────────────────────────────────────────
29
30async function autocompleteCharNames(
31 interaction: AutocompleteInteraction,
32 focused: string
33): Promise<void> {
34 const member = await interaction.guild!.members.fetch(interaction.user.id);
35 const user = await resolveUser(member);
36 if (!user.userKey) return interaction.respond([]);
37
38 const ownChars = getCharacters(user.userKey).map((c) => {
39 const nationEmoji = c.nation ? (getNationEmoji(c.nation) || c.nation) : "";
40 return {
41 name: `${c.class} ${c.level} ${c.name} ${nationEmoji}`.trim(),
42 value: c.name,
43 };
44 });
45
46 const sharedChars: { name: string; value: string }[] = [];
47 try {
48 const chars = JSON.parse(
49 fs.readFileSync(path.join(__dirname, "../../data/characters.json"), "utf8")
50 );
51 for (const [ownerKey, data] of Object.entries(chars) as [string, any][]) {
52 if (ownerKey === user.userKey) continue;
53 for (const char of data.characters ?? []) {
54 if (char.sharedWith?.includes(user.userKey)) {
55 const nationEmoji = char.nation ? (getNationEmoji(char.nation) || char.nation) : "";
56 sharedChars.push({
57 name: `${char.class} ${char.level} ${char.name} 🔗 ${nationEmoji}`.trim(),
58 value: char.name,
59 });
60 }
61 }
62 }
63 } catch {}
64
65 const all = [...ownChars, ...sharedChars]
66 .filter((c) => c.name.toLowerCase().includes(focused.toLowerCase()))
67 .slice(0, 25);
68
69 await interaction.respond(all);
70}
71
72async function autocompleteUserKeys(
73 interaction: AutocompleteInteraction,
74 focused: string
75): Promise<void> {
76 try {
77 const usermap = JSON.parse(
78 fs.readFileSync(path.join(__dirname, "../../data/usermap.json"), "utf8")
79 );
80 const choices = Object.entries(usermap)
81 .map(([, entry]: [string, any]) => {
82 const fileKey = typeof entry === "string" ? entry : entry.file;
83 const alias = typeof entry === "object" ? (entry.aliases?.[0] ?? fileKey) : fileKey;
84 return { name: `${alias} (${fileKey})`, value: fileKey };
85 })
86 .filter((c) => c.name.toLowerCase().includes(focused.toLowerCase()))
87 .slice(0, 25);
88 await interaction.respond(choices);
89 } catch {
90 await interaction.respond([]);
91 }
92}
93
94async function autocompleteSlots(
95 interaction: AutocompleteInteraction,
96 focused: string
97): Promise<void> {
98 const slots = cfg("slots")
99 .filter((s) => s.active)
100 .map((s) => ({ name: `${s.tgHour}:00`, value: String(s.tgHour) }))
101 .filter((s) => s.name.includes(focused));
102 await interaction.respond(slots);
103}
104
105async function autocompleteCharNamesForNation(
106 interaction: AutocompleteInteraction,
107 focused: string,
108 nation: Nation | null
109): Promise<void> {
110 const chars = JSON.parse(
111 fs.readFileSync(Paths.data("characters.json"), "utf8")
112 );
113
114 const results: { name: string; value: string }[] = [];
115
116 for (const data of Object.values(chars) as any[]) {
117 for (const char of data.characters ?? []) {
118 if (nation && char.nation !== nation) continue;
119 if (!char.name.toLowerCase().includes(focused.toLowerCase())) continue;
120 const nationEmoji = char.nation ? (getNationEmoji(char.nation) || char.nation) : "";
121 results.push({
122 name: `${char.class} ${char.level} ${char.name} ${nationEmoji}`.trim(),
123 value: char.name,
124 });
125 }
126 }
127
128 await interaction.respond(results.slice(0, 25));
129}
130
131// ─── Router ───────────────────────────────────────────────────────────────────
132
133export async function handleAutocomplete(interaction: AutocompleteInteraction): Promise<void> {
134 try {
135 const focused = interaction.options.getFocused(true);
136 const optionName = focused.name;
137 const focusedValue = focused.value as string;
138
139 if (optionName === "char_name") return await autocompleteCharNames(interaction, focusedValue);
140 if (optionName === "name") return await autocompleteUserKeys(interaction, focusedValue);
141 if (optionName === "slot") return await autocompleteSlots(interaction, focusedValue);
142 if (optionName === "owner") return await autocompleteUserKeys(interaction, focusedValue);
143
144 if (optionName === "char_name") {
145 const commandName = interaction.commandName;
146 const subGroup = interaction.options.getSubcommandGroup(false);
147 const sub = interaction.options.getSubcommand(false);
148
149 if (sub === "set" && subGroup === "bringer") {
150 // Filter by selected nation
151 const nation = interaction.options.getString("nation") as Nation | null;
152 return await autocompleteCharNamesForNation(interaction, focusedValue, nation);
153 }
154
155 return await autocompleteCharNames(interaction, focusedValue);
156 }
157
158 await interaction.respond([]);
159 } catch (err) {
160 console.error("[autocomplete] error:", err);
161 try { await interaction.respond([]); } catch {}
162 }
163}