249 lines
9.4 KiB
TypeScript
249 lines
9.4 KiB
TypeScript
import { type Interaction } from "discord.js";
|
|
import { items } from "@db/schema";
|
|
import { DrizzleClient } from "@shared/db/DrizzleClient";
|
|
import type { ItemUsageData, ItemEffect } from "@shared/lib/types";
|
|
import { getItemWizardEmbed, getItemTypeSelection, getEffectTypeSelection, getDetailsModal, getEconomyModal, getVisualsModal, getEffectConfigModal } from "./item_wizard.view";
|
|
import type { DraftItem } from "./item_wizard.types";
|
|
import { ItemType, EffectType } from "@shared/lib/constants";
|
|
|
|
// --- Types ---
|
|
|
|
|
|
// --- State ---
|
|
const draftSession = new Map<string, DraftItem>();
|
|
|
|
|
|
|
|
// --- Render ---
|
|
export const renderWizard = (userId: string, isDraft = true) => {
|
|
let draft = draftSession.get(userId);
|
|
|
|
// Initialize if new
|
|
if (!draft) {
|
|
draft = {
|
|
name: "New Item",
|
|
description: "No description",
|
|
rarity: "Common",
|
|
type: ItemType.MATERIAL,
|
|
price: null,
|
|
iconUrl: "",
|
|
imageUrl: "",
|
|
usageData: { consume: true, effects: [] } // Default Consume to true for now
|
|
};
|
|
draftSession.set(userId, draft);
|
|
}
|
|
|
|
const { embeds, components } = getItemWizardEmbed(draft);
|
|
return { embeds, components };
|
|
};
|
|
|
|
// --- Handler ---
|
|
export const handleItemWizardInteraction = async (interaction: Interaction) => {
|
|
// Only handle createitem interactions
|
|
if (!interaction.isButton() && !interaction.isStringSelectMenu() && !interaction.isModalSubmit()) return;
|
|
if (!interaction.customId.startsWith("createitem_")) return;
|
|
|
|
const userId = interaction.user.id;
|
|
let draft = draftSession.get(userId);
|
|
|
|
// Special case for Cancel - doesn't need draft checks usually, but we want to clear it
|
|
if (interaction.customId === "createitem_cancel") {
|
|
draftSession.delete(userId);
|
|
if (interaction.isMessageComponent()) {
|
|
await interaction.update({ content: "❌ Item creation cancelled.", embeds: [], components: [] });
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Initialize draft if missing for other actions (edge case: bot restart)
|
|
if (!draft) {
|
|
if (interaction.isMessageComponent()) {
|
|
// Create one implicitly to prevent crashes, or warn user
|
|
if (interaction.customId === "createitem_start") {
|
|
// Allow start
|
|
} else {
|
|
await interaction.reply({ content: "⚠️ Session expired. Please run `/createitem` again.", ephemeral: true });
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
// Re-get draft (guaranteed now if we handled the start/restart)
|
|
// Actually renderWizard initializes it, so if we call that we are safe.
|
|
// But for Modals we need it.
|
|
|
|
if (!draft) {
|
|
// Just init it
|
|
renderWizard(userId);
|
|
draft = draftSession.get(userId)!;
|
|
}
|
|
|
|
|
|
// --- Routing ---
|
|
|
|
// 1. Details Modal
|
|
if (interaction.customId === "createitem_details") {
|
|
if (!interaction.isButton()) return;
|
|
const modal = getDetailsModal(draft);
|
|
await interaction.showModal(modal);
|
|
return;
|
|
}
|
|
|
|
// 2. Economy Modal
|
|
if (interaction.customId === "createitem_economy") {
|
|
if (!interaction.isButton()) return;
|
|
const modal = getEconomyModal(draft);
|
|
await interaction.showModal(modal);
|
|
return;
|
|
}
|
|
|
|
// 3. Visuals Modal
|
|
if (interaction.customId === "createitem_visuals") {
|
|
if (!interaction.isButton()) return;
|
|
const modal = getVisualsModal(draft);
|
|
await interaction.showModal(modal);
|
|
return;
|
|
}
|
|
|
|
// 4. Type Toggle (Start Select Menu)
|
|
if (interaction.customId === "createitem_type_toggle") {
|
|
if (!interaction.isButton()) return;
|
|
const { components } = getItemTypeSelection();
|
|
await interaction.update({ components }); // Temporary view
|
|
return;
|
|
}
|
|
|
|
if (interaction.customId === "createitem_select_type") {
|
|
if (!interaction.isStringSelectMenu()) return;
|
|
const selected = interaction.values[0];
|
|
if (selected) {
|
|
draft.type = selected;
|
|
}
|
|
// Re-render
|
|
const payload = renderWizard(userId);
|
|
await interaction.update(payload);
|
|
return;
|
|
}
|
|
|
|
// 5. Add Effect Flow
|
|
if (interaction.customId === "createitem_addeffect_start") {
|
|
if (!interaction.isButton()) return;
|
|
const { components } = getEffectTypeSelection();
|
|
await interaction.update({ components });
|
|
return;
|
|
}
|
|
|
|
if (interaction.customId === "createitem_select_effect_type") {
|
|
if (!interaction.isStringSelectMenu()) return;
|
|
const effectType = interaction.values[0];
|
|
if (!effectType) return;
|
|
draft.pendingEffectType = effectType;
|
|
|
|
// Immediately show modal for data collection
|
|
// Note: You can't showModal from an update? You CAN showModal from a component interaction (SelectMenu).
|
|
// But we shouldn't update the message AND show modal. We must pick one.
|
|
// We will show modal. The message remains in "Select Effect" state until modal submit re-renders it.
|
|
|
|
const modal = getEffectConfigModal(effectType);
|
|
await interaction.showModal(modal);
|
|
return;
|
|
}
|
|
|
|
// Toggle Consume
|
|
if (interaction.customId === "createitem_toggle_consume") {
|
|
if (!interaction.isButton()) return;
|
|
draft.usageData.consume = !draft.usageData.consume;
|
|
const payload = renderWizard(userId);
|
|
await interaction.update(payload);
|
|
return;
|
|
}
|
|
|
|
// 6. Handle Modal Submits
|
|
if (interaction.isModalSubmit()) {
|
|
if (interaction.customId === "createitem_modal_details") {
|
|
draft.name = interaction.fields.getTextInputValue("name");
|
|
draft.description = interaction.fields.getTextInputValue("desc");
|
|
draft.rarity = interaction.fields.getTextInputValue("rarity");
|
|
}
|
|
else if (interaction.customId === "createitem_modal_economy") {
|
|
const price = parseInt(interaction.fields.getTextInputValue("price"));
|
|
draft.price = isNaN(price) || price === 0 ? null : price;
|
|
}
|
|
else if (interaction.customId === "createitem_modal_visuals") {
|
|
draft.iconUrl = interaction.fields.getTextInputValue("icon");
|
|
draft.imageUrl = interaction.fields.getTextInputValue("image");
|
|
}
|
|
else if (interaction.customId === "createitem_modal_effect") {
|
|
const type = draft.pendingEffectType;
|
|
if (type) {
|
|
let effect: ItemEffect | null = null;
|
|
|
|
if (type === EffectType.ADD_XP || type === EffectType.ADD_BALANCE) {
|
|
const amount = parseInt(interaction.fields.getTextInputValue("amount"));
|
|
if (!isNaN(amount)) effect = { type: type as any, amount };
|
|
}
|
|
else if (type === EffectType.REPLY_MESSAGE) {
|
|
effect = { type: EffectType.REPLY_MESSAGE, message: interaction.fields.getTextInputValue("message") };
|
|
}
|
|
else if (type === EffectType.XP_BOOST) {
|
|
const multiplier = parseFloat(interaction.fields.getTextInputValue("multiplier"));
|
|
const duration = parseInt(interaction.fields.getTextInputValue("duration"));
|
|
if (!isNaN(multiplier) && !isNaN(duration)) effect = { type: EffectType.XP_BOOST, multiplier, durationSeconds: duration };
|
|
}
|
|
else if (type === EffectType.TEMP_ROLE) {
|
|
const roleId = interaction.fields.getTextInputValue("role_id");
|
|
const duration = parseInt(interaction.fields.getTextInputValue("duration"));
|
|
if (roleId && !isNaN(duration)) effect = { type: EffectType.TEMP_ROLE, roleId: roleId, durationSeconds: duration };
|
|
}
|
|
else if (type === EffectType.COLOR_ROLE) {
|
|
const roleId = interaction.fields.getTextInputValue("role_id");
|
|
if (roleId) effect = { type: EffectType.COLOR_ROLE, roleId: roleId };
|
|
}
|
|
|
|
if (effect) {
|
|
draft.usageData.effects.push(effect);
|
|
}
|
|
draft.pendingEffectType = undefined;
|
|
}
|
|
}
|
|
|
|
// Re-render
|
|
const payload = renderWizard(userId);
|
|
await interaction.deferUpdate();
|
|
await interaction.editReply(payload);
|
|
return;
|
|
}
|
|
|
|
// 7. Save
|
|
if (interaction.customId === "createitem_save") {
|
|
if (!interaction.isButton()) return;
|
|
|
|
await interaction.deferUpdate(); // Prepare to save
|
|
|
|
try {
|
|
await DrizzleClient.insert(items).values({
|
|
name: draft.name,
|
|
description: draft.description,
|
|
type: draft.type,
|
|
rarity: draft.rarity,
|
|
price: draft.price ? BigInt(draft.price) : null,
|
|
iconUrl: draft.iconUrl,
|
|
imageUrl: draft.imageUrl,
|
|
usageData: draft.usageData
|
|
});
|
|
|
|
draftSession.delete(userId);
|
|
await interaction.editReply({ content: `✅ **${draft.name}** has been created successfully!`, embeds: [], components: [] });
|
|
} catch (error: any) {
|
|
console.error("Failed to create item:", error);
|
|
// Restore state
|
|
await interaction.followUp({ content: `❌ Failed to save item: ${error.message}`, ephemeral: true });
|
|
}
|
|
}
|
|
|
|
};
|
|
|
|
export const clearDraftSessions = () => {
|
|
draftSession.clear();
|
|
console.log("[ItemWizard] All draft item creation sessions cleared.");
|
|
};
|