Последняя активность 3 weeks ago

nuno ревизий этого фрагмента 3 weeks ago. К ревизии

1 file changed, 163 insertions

autocomplete.ts(файл создан)

@@ -0,0 +1,163 @@
1 + import { AutocompleteInteraction } from "discord.js";
2 + import { resolveUser } from "@systems/users";
3 + import { getCharacters } from "@systems/characters";
4 + import { cfg } from "@systems/config";
5 + import { getNationEmoji } from "@systems/emojis";
6 + import fs from "fs";
7 + import path from "path";
8 + import { Nation } from "@types";
9 + import { Paths } from "@helpers/paths";
10 +
11 + let _charCache: Record<string, any> | null = null;
12 +
13 + function 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 +
24 + export function invalidateCharCache(): void {
25 + _charCache = null;
26 + }
27 +
28 + // ─── Autocomplete subsets ─────────────────────────────────────────────────────
29 +
30 + async 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 +
72 + async 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 +
94 + async 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 +
105 + async 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 +
133 + export 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 + }
Новее Позже