160 lines
No EOL
6.3 KiB
TypeScript
160 lines
No EOL
6.3 KiB
TypeScript
import { ChatInputCommandInteraction, TextChannel } from "discord.js";
|
|
import { cfg } from "@systems/config";
|
|
import { resolveUser, hasOfficerRole } from "@systems/users";
|
|
import { setActiveCharacter, getActiveCharacter, getCharacterByName, getCharacters } from "@systems/characters";
|
|
import {
|
|
getEffectiveCharacter,
|
|
setSessionBorrow,
|
|
setPersistentPreference,
|
|
clearPersistentPreference,
|
|
clearSessionBorrowForUser,
|
|
} from "@systems/borrow";
|
|
import { polls, updatePollMessage } from "@systems/poll";
|
|
import { getClassEmoji } from "@systems/emojis";
|
|
import { replyAndDelete } from "@src/utils";
|
|
import { format } from "@format";
|
|
import { buildCharSelectButtons } from "@systems/charSelect";
|
|
import fs from "fs";
|
|
import path from "path";
|
|
|
|
const CHARS_PATH = path.join(__dirname, "../../data/characters.json");
|
|
|
|
function findSharedChar(userKey: string, charName: string): { ownerKey: string; char: any } | null {
|
|
try {
|
|
const chars = JSON.parse(fs.readFileSync(CHARS_PATH, "utf8"));
|
|
for (const [ownerKey, data] of Object.entries(chars) as [string, any][]) {
|
|
if (ownerKey === userKey) continue;
|
|
const char = data.characters?.find(
|
|
(c: any) => c.name.toLowerCase() === charName.toLowerCase() && c.sharedWith?.includes(userKey)
|
|
);
|
|
if (char) return { ownerKey, char };
|
|
}
|
|
} catch {}
|
|
return null;
|
|
}
|
|
|
|
function findVoteIdInPoll(state: any, userKey: string): string | null {
|
|
for (const [id, entry] of [...state.yes.entries(), ...state.no.entries()]) {
|
|
if (entry.userKey === userKey) return id;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export async function handleSwitch(interaction: ChatInputCommandInteraction): Promise<void> {
|
|
const member = await interaction.guild!.members.fetch(interaction.user.id);
|
|
const isOfficer = hasOfficerRole(member, cfg("officerRoles"));
|
|
const nameArg = interaction.options.getString("name");
|
|
const charName = interaction.options.getString("char_name", true);
|
|
|
|
let userKey: string | null;
|
|
if (nameArg) {
|
|
if (!isOfficer) return void replyAndDelete(interaction, "❌ Only officers can switch other players' characters.");
|
|
userKey = nameArg;
|
|
} else {
|
|
const user = await resolveUser(member);
|
|
userKey = user.userKey;
|
|
}
|
|
|
|
if (!userKey) return void replyAndDelete(interaction, "❌ You are not registered in the system.");
|
|
|
|
// Resolve the target character without switching yet
|
|
let resolvedChar: any = null;
|
|
let borrowedFrom: string | null = null;
|
|
|
|
const ownChar = getCharacterByName(userKey, charName);
|
|
if (ownChar) {
|
|
resolvedChar = ownChar;
|
|
} else {
|
|
const shared = findSharedChar(userKey, charName);
|
|
if (shared) {
|
|
resolvedChar = shared.char;
|
|
borrowedFrom = shared.ownerKey;
|
|
}
|
|
}
|
|
|
|
if (!resolvedChar) return void replyAndDelete(interaction, `❌ No character named **${charName}** found.`);
|
|
|
|
// If already active — just show current state without switching
|
|
const current = getEffectiveCharacter(userKey);
|
|
if (current.char?.name === resolvedChar.name) {
|
|
const classEmoji = getClassEmoji(resolvedChar.class) || resolvedChar.class;
|
|
const borrowNote = current.borrowedFrom ? ` *(shared by ${current.borrowedFrom})*` : "";
|
|
return void replyAndDelete(interaction, `${classEmoji} ${resolvedChar.level} ${resolvedChar.name}${borrowNote}`, true);
|
|
}
|
|
|
|
// Check if target character is already in the active poll by another player
|
|
const slot = [...polls.keys()][0];
|
|
if (slot !== undefined) {
|
|
const state = polls.get(slot)!;
|
|
for (const [id, entry] of state.yes.entries()) {
|
|
const isOwnEntry = id === interaction.user.id || id === `impersonated:${userKey}`;
|
|
if (!isOwnEntry && entry.characterName === resolvedChar.name && entry.userKey !== userKey) {
|
|
const slotHour = state.slot;
|
|
const charDisplay = format.char(resolvedChar);
|
|
const isOwner = getCharacters(userKey).some((c) => c.name === resolvedChar.name);
|
|
if (isOwner) {
|
|
await interaction.reply({
|
|
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:`,
|
|
components: buildCharSelectButtons(userKey, {
|
|
customIdPrefix: `switch_after_reclaim:${userKey}`,
|
|
excludeCharName: resolvedChar.name,
|
|
appendToCustomId: ":yes",
|
|
}),
|
|
ephemeral: true,
|
|
});
|
|
return;
|
|
}
|
|
const buttons = buildCharSelectButtons(userKey, {
|
|
customIdPrefix: `switch_after_reclaim:${userKey}`,
|
|
excludeCharName: resolvedChar.name,
|
|
appendToCustomId: `:${"yes"}`,
|
|
});
|
|
await interaction.reply({
|
|
content: `❌ ${charDisplay} is already scheduled for TG at ${slotHour}:00. Pick a different character.`,
|
|
components: buttons,
|
|
ephemeral: true,
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Now actually switch
|
|
if (borrowedFrom) {
|
|
setSessionBorrow(userKey, borrowedFrom, resolvedChar.name);
|
|
setPersistentPreference(userKey, borrowedFrom, resolvedChar.name);
|
|
} else {
|
|
setActiveCharacter(userKey, charName);
|
|
clearPersistentPreference(userKey);
|
|
clearSessionBorrowForUser(userKey);
|
|
resolvedChar = getActiveCharacter(userKey);
|
|
}
|
|
|
|
// Update poll embed if user has already voted
|
|
if (slot !== undefined) {
|
|
const state = polls.get(slot)!;
|
|
const voteId = findVoteIdInPoll(state, userKey);
|
|
|
|
if (voteId && (state.yes.has(voteId) || state.no.has(voteId))) {
|
|
const updateEntry = (map: Map<string, any>) => {
|
|
const entry = map.get(voteId);
|
|
if (entry) {
|
|
entry.characterName = resolvedChar.name;
|
|
entry.characterClass = resolvedChar.class;
|
|
entry.characterLevel = resolvedChar.level;
|
|
entry.characterNation = resolvedChar.nation;
|
|
entry.borrowedFrom = borrowedFrom ?? undefined;
|
|
}
|
|
};
|
|
updateEntry(state.yes);
|
|
updateEntry(state.no);
|
|
|
|
const channel = await interaction.client.channels.fetch(cfg("pollChannelId")) as TextChannel;
|
|
await updatePollMessage(channel, slot);
|
|
}
|
|
}
|
|
|
|
const classEmoji = getClassEmoji(resolvedChar.class) || resolvedChar.class;
|
|
const borrowNote = borrowedFrom ? ` *(shared by ${borrowedFrom})*` : "";
|
|
return void replyAndDelete(interaction, `🔄 ${classEmoji} ${resolvedChar.level} ${resolvedChar.name}${borrowNote}`, true);
|
|
} |