Son aktivite 1 month ago

nuno bu gisti düzenledi 1 month ago. Düzenlemeye git

1 file changed, 285 insertions

conflcit.ts(dosya oluşturuldu)

@@ -0,0 +1,285 @@
1 + import {
2 + Client,
3 + ButtonBuilder,
4 + ButtonStyle,
5 + ActionRowBuilder,
6 + EmbedBuilder,
7 + ButtonInteraction,
8 + TextChannel,
9 + } from "discord.js";
10 +
11 + // Configurable button styles
12 + const RECLAIM_STYLE = ButtonStyle.Secondary; // gray — change to ButtonStyle.Danger for red
13 + const SWITCH_STYLE = ButtonStyle.Secondary;
14 + import { cfg } from "@systems/config";
15 + import { getCharacters, getCharacterByName, setActiveCharacter } from "@systems/characters";
16 + import { clearSessionBorrowForUser, setPersistentPreference, getEffectiveCharacter } from "@systems/borrow";
17 + import { polls, updatePollMessage, createVoteEntry } from "@systems/poll";
18 + import { resolveMessage, nowFormatted } from "@systems/messages";
19 + import { getClassEmoji } from "@systems/emojis";
20 + import { Character } from "@src/types";
21 + import { format } from "@format";
22 +
23 + const AUTO_VOTE_ON_SWITCH = process.env.AUTO_VOTE_ON_CONFLICT_SWITCH !== "false";
24 +
25 + // Stores pending conflict resolutions: buttonId → { ownerUsermapKey, borrowerUsermapKey, charName, ownerId }
26 + const pendingConflicts = new Map<string, {
27 + ownerUsermapKey: string;
28 + borrowerUsermapKey: string;
29 + charName: string;
30 + ownerId: string;
31 + page: number;
32 + }>();
33 +
34 + function formatChar(char: Character): string {
35 + const emoji = getClassEmoji(char.class) || char.class;
36 + return `${emoji} ${char.level} ${char.name}`;
37 + }
38 +
39 + // Parse <:name:id> or unicode emoji string for use with ButtonBuilder.setEmoji()
40 + function parseEmoji(emojiStr: string): { name: string; id: string } | string | null {
41 + if (!emojiStr) return null;
42 + const match = emojiStr.match(/^<:(\w+):(\d+)>$/);
43 + if (match) return { name: match[1], id: match[2] };
44 + return emojiStr; // unicode fallback
45 + }
46 +
47 + // For button labels — emoji via setEmoji(), text only in label
48 + function applyCharToButton(btn: ButtonBuilder, char: Character): ButtonBuilder {
49 + const emojiStr = getClassEmoji(char.class);
50 + const emoji = format.emoji(emojiStr);
51 + btn.setLabel(`${char.level} ${char.name}`);
52 + if (emoji) btn.setEmoji(emoji as any);
53 + return btn;
54 + }
55 +
56 + function buildConflictEmbed(
57 + borrowerKey: string,
58 + char: Character,
59 + ownerKey: string
60 + ): EmbedBuilder {
61 + const charDisplay = formatChar(char);
62 + return new EmbedBuilder()
63 + .setTitle("⚠️ Character Conflict")
64 + .setDescription(
65 + `**${charDisplay}** is currently borrowed by **${borrowerKey}** for tonight's TG.\n\nYou can reclaim your character or switch to another one.`
66 + )
67 + .setColor(0xe8a317);
68 + }
69 +
70 + function buildConflictButtons(
71 + ownerUsermapKey: string,
72 + borrowerUsermapKey: string,
73 + borrowedCharName: string,
74 + ownerId: string,
75 + allChars: Character[],
76 + page: number
77 + ): ActionRowBuilder<ButtonBuilder>[] {
78 + const PAGE_SIZE = 4; // leave 1 slot for reclaim on first row
79 + const otherChars = allChars.filter((c) => c.name !== borrowedCharName);
80 + const pageChars = otherChars.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE);
81 + const hasMore = otherChars.length > (page + 1) * PAGE_SIZE;
82 + const hasPrev = page > 0;
83 +
84 + const rows: ActionRowBuilder<ButtonBuilder>[] = [];
85 +
86 + // Row 1: char switch buttons
87 + const charButtons = pageChars.map((char) => {
88 + const id = `conflict_switch:${ownerUsermapKey}:${borrowerUsermapKey}:${char.name}:${ownerId}`;
89 + pendingConflicts.set(id, { ownerUsermapKey, borrowerUsermapKey, charName: borrowedCharName, ownerId, page });
90 + return applyCharToButton(
91 + new ButtonBuilder().setCustomId(id).setStyle(ButtonStyle.Secondary),
92 + char
93 + );
94 + });
95 +
96 + if (charButtons.length > 0) {
97 + rows.push(new ActionRowBuilder<ButtonBuilder>().addComponents(...charButtons));
98 + }
99 +
100 + // Row 2: reclaim + pagination
101 + const reclaimId = `conflict_reclaim:${ownerUsermapKey}:${borrowerUsermapKey}:${borrowedCharName}:${ownerId}`;
102 + pendingConflicts.set(reclaimId, { ownerUsermapKey, borrowerUsermapKey, charName: borrowedCharName, ownerId, page });
103 +
104 + const borrowed = allChars.find((c) => c.name === borrowedCharName);
105 + const reclaimBtn = borrowed
106 + ? applyCharToButton(
107 + new ButtonBuilder().setCustomId(reclaimId).setStyle(ButtonStyle.Danger),
108 + borrowed
109 + ).setLabel(`${borrowed.level} ${borrowed.name}`)
110 + : new ButtonBuilder().setCustomId(reclaimId).setLabel(`↩️ Reclaim ${borrowedCharName}`).setStyle(RECLAIM_STYLE);
111 +
112 + const navButtons: ButtonBuilder[] = [reclaimBtn];
113 +
114 + if (hasPrev) {
115 + const prevId = `conflict_page:${ownerUsermapKey}:${borrowerUsermapKey}:${borrowedCharName}:${ownerId}:${page - 1}`;
116 + pendingConflicts.set(prevId, { ownerUsermapKey, borrowerUsermapKey, charName: borrowedCharName, ownerId, page: page - 1 });
117 + navButtons.push(new ButtonBuilder().setCustomId(prevId).setLabel("← Prev").setStyle(ButtonStyle.Primary));
118 + }
119 + if (hasMore) {
120 + const nextId = `conflict_page:${ownerUsermapKey}:${borrowerUsermapKey}:${borrowedCharName}:${ownerId}:${page + 1}`;
121 + pendingConflicts.set(nextId, { ownerUsermapKey, borrowerUsermapKey, charName: borrowedCharName, ownerId, page: page + 1 });
122 + navButtons.push(new ButtonBuilder().setCustomId(nextId).setLabel("Next →").setStyle(ButtonStyle.Primary));
123 + }
124 +
125 + rows.push(new ActionRowBuilder<ButtonBuilder>().addComponents(...navButtons));
126 + return rows;
127 + }
128 +
129 + // Show conflict embed to owner
130 + export async function showConflictEmbed(
131 + interaction: ButtonInteraction,
132 + ownerUsermapKey: string,
133 + borrowerUsermapKey: string,
134 + borrowedChar: Character,
135 + allOwnerChars: Character[]
136 + ): Promise<void> {
137 + const embed = buildConflictEmbed(borrowerUsermapKey, borrowedChar, ownerUsermapKey);
138 + const buttons = buildConflictButtons(
139 + ownerUsermapKey, borrowerUsermapKey, borrowedChar.name,
140 + interaction.user.id, allOwnerChars, 0
141 + );
142 + await interaction.followUp({ embeds: [embed], components: buttons, ephemeral: true });
143 + }
144 +
145 + // Handle conflict button interactions
146 + export async function handleConflictButton(interaction: ButtonInteraction): Promise<void> {
147 + const { customId } = interaction;
148 +
149 + if (customId.startsWith("conflict_page:")) {
150 + const parts = customId.split(":");
151 + const ownerKey = parts[1];
152 + const borrowerKey = parts[2];
153 + const charName = parts[3];
154 + const ownerId = parts[4];
155 + const page = parseInt(parts[5]);
156 +
157 + const allChars = getCharacters(ownerKey);
158 + const borrowed = allChars.find((c) => c.name === charName);
159 + if (!borrowed) return void interaction.reply({ content: "❌ Character not found.", ephemeral: true });
160 +
161 + const embed = buildConflictEmbed(borrowerKey, borrowed, ownerKey);
162 + const buttons = buildConflictButtons(ownerKey, borrowerKey, charName, ownerId, allChars, page);
163 + await interaction.update({ embeds: [embed], components: buttons });
164 + return;
165 + }
166 +
167 + if (customId.startsWith("conflict_switch:")) {
168 + const parts = customId.split(":");
169 + const ownerKey = parts[1];
170 + const borrowerKey = parts[2];
171 + const newCharName = parts[3];
172 + const ownerId = parts[4];
173 +
174 + // Switch owner to the selected char
175 + setActiveCharacter(ownerKey, newCharName);
176 + clearSessionBorrowForUser(ownerKey);
177 +
178 + const slot = [...polls.keys()][0];
179 + const state = slot !== undefined ? polls.get(slot) : null;
180 +
181 + if (state && AUTO_VOTE_ON_SWITCH) {
182 + // Auto-vote Yes for owner with new char
183 + const guild = interaction.guild!;
184 + const member = await guild.members.fetch(ownerId);
185 + const { char } = getEffectiveCharacter(ownerKey);
186 + const now = nowFormatted();
187 + const publicMsg = resolveMessage("public", "yes", 1, ownerKey, member.nickname ?? null, member.user.globalName ?? null);
188 +
189 + state.yes.set(ownerId, {
190 + userKey: ownerKey,
191 + displayName: member.nickname ?? member.user.globalName ?? member.user.username,
192 + characterName: char?.name,
193 + characterClass: char?.class,
194 + characterLevel: char?.level,
195 + characterNation: char?.nation,
196 + votedAt: now,
197 + publicMessage: publicMsg ?? undefined,
198 + });
199 +
200 + const channel = await interaction.client.channels.fetch(cfg("pollChannelId")) as TextChannel;
201 + await updatePollMessage(channel, slot!);
202 + }
203 +
204 + await interaction.update({
205 + embeds: [new EmbedBuilder()
206 + .setTitle("✅ Switched")
207 + .setDescription(`Switched to **${newCharName}**${AUTO_VOTE_ON_SWITCH ? " and voted Yes." : "."}`)
208 + .setColor(0x57f287)],
209 + components: [],
210 + });
211 + return;
212 + }
213 +
214 + if (customId.startsWith("conflict_reclaim:")) {
215 + const parts = customId.split(":");
216 + const ownerKey = parts[1];
217 + const borrowerKey = parts[2];
218 + const charName = parts[3];
219 + const ownerId = parts[4];
220 +
221 + const reclaimBehavior = (cfg as any)("conflictReclaimBehavior") ?? "revert"; // "revert" | "remove"
222 + const slot = [...polls.keys()][0];
223 + const state = slot !== undefined ? polls.get(slot) : null;
224 +
225 + if (state) {
226 + // Find borrower's vote entry
227 + for (const [id, entry] of [...state.yes.entries(), ...state.no.entries()]) {
228 + if (entry.userKey === borrowerKey) {
229 + if (reclaimBehavior === "remove") {
230 + state.yes.delete(id);
231 + state.no.delete(id);
232 + } else {
233 + // Revert borrower to their own active char
234 + clearSessionBorrowForUser(borrowerKey);
235 + const { char: ownChar } = getEffectiveCharacter(borrowerKey);
236 + if (ownChar) {
237 + entry.characterName = ownChar.name;
238 + entry.characterClass = ownChar.class;
239 + entry.characterLevel = ownChar.level;
240 + entry.characterNation = ownChar.nation;
241 + entry.borrowedFrom = undefined;
242 + } else {
243 + // No own char — remove from poll
244 + state.yes.delete(id);
245 + state.no.delete(id);
246 + }
247 + }
248 + break;
249 + }
250 + }
251 +
252 + // Owner joins with their character
253 + const guild = interaction.guild!;
254 + const member = await guild.members.fetch(ownerId);
255 + setActiveCharacter(ownerKey, charName);
256 + clearSessionBorrowForUser(ownerKey);
257 + const { char } = getEffectiveCharacter(ownerKey);
258 + const now = nowFormatted();
259 + const publicMsg = resolveMessage("public", "yes", 1, ownerKey, member.nickname ?? null, member.user.globalName ?? null);
260 +
261 + state.yes.set(ownerId, {
262 + userKey: ownerKey,
263 + displayName: member.nickname ?? member.user.globalName ?? member.user.username,
264 + characterName: char?.name,
265 + characterClass: char?.class,
266 + characterLevel: char?.level,
267 + characterNation: char?.nation,
268 + votedAt: now,
269 + publicMessage: publicMsg ?? undefined,
270 + });
271 +
272 + const channel = await interaction.client.channels.fetch(cfg("pollChannelId")) as TextChannel;
273 + await updatePollMessage(channel, slot!);
274 + }
275 +
276 + await interaction.update({
277 + embeds: [new EmbedBuilder()
278 + .setTitle("↩️ Reclaimed")
279 + .setDescription(`**${charName}** has been reclaimed from **${borrowerKey}** and you've been added to the poll.`)
280 + .setColor(0x57f287)],
281 + components: [],
282 + });
283 + return;
284 + }
285 + }
Daha yeni Daha eski