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(); // --- 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."); };