import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ModalBuilder, StringSelectMenuBuilder, TextInputBuilder, TextInputStyle, type Interaction, type MessageActionRowComponentBuilder } from "discord.js"; import { items } from "@/db/schema"; import { DrizzleClient } from "@/lib/DrizzleClient"; import type { ItemUsageData, ItemEffect } from "@/lib/types"; import { createBaseEmbed } from "@lib/embeds"; // --- Types --- export interface DraftItem { name: string; description: string; rarity: string; type: string; price: number | null; iconUrl: string; imageUrl: string; usageData: ItemUsageData; // Temporary state for effect adding flow pendingEffectType?: string; } // --- State --- const draftSession = new Map(); const getItemTypeOptions = () => [ { label: "Material", value: "MATERIAL", description: "Used for crafting or trading" }, { label: "Consumable", value: "CONSUMABLE", description: "Can be used to gain effects" }, { label: "Equipment", value: "EQUIPMENT", description: "Can be equipped (Not yet implemented)" }, { label: "Quest Item", value: "QUEST", description: "Required for quests" }, ]; const getEffectTypeOptions = () => [ { label: "Add XP", value: "ADD_XP", description: "Gives XP to the user" }, { label: "Add Balance", value: "ADD_BALANCE", description: "Gives currency to the user" }, { label: "Reply Message", value: "REPLY_MESSAGE", description: "Bot replies with a message" }, { label: "XP Boost", value: "XP_BOOST", description: "Temporarily boosts XP gain" }, { label: "Temp Role", value: "TEMP_ROLE", description: "Gives a temporary role" }, { label: "Color Role", value: "COLOR_ROLE", description: "Equips a permanent color role (swaps)" }, ]; // --- 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: "MATERIAL", price: null, iconUrl: "", imageUrl: "", usageData: { consume: true, effects: [] } // Default Consume to true for now }; draftSession.set(userId, draft); } const embed = createBaseEmbed(`🛠️ Item Creator: ${draft.name}`, undefined, "Blue") .addFields( { name: "General", value: `**Type:** ${draft.type}\n**Rarity:** ${draft.rarity}\n**Desc:** ${draft.description}`, inline: true }, { name: "Economy", value: `**Price:** ${draft.price ? `${draft.price} 🪙` : "Not for sale"}`, inline: true }, { name: "Visuals", value: `**Icon:** ${draft.iconUrl ? "✅ Set" : "❌"}\n**Image:** ${draft.imageUrl ? "✅ Set" : "❌"}`, inline: true }, { name: "Usage", value: `**Consume:** ${draft.usageData.consume ? "✅ Yes" : "❌ No"}`, inline: true }, ); // Effects Display if (draft.usageData.effects.length > 0) { const effecto = draft.usageData.effects.map((e, i) => `${i + 1}. **${e.type}**: ${JSON.stringify(e)}`).join("\n"); embed.addFields({ name: "Usage Effects", value: effecto.substring(0, 1024) }); } else { embed.addFields({ name: "Usage Effects", value: "None" }); } if (draft.imageUrl) embed.setImage(draft.imageUrl); if (draft.iconUrl) embed.setThumbnail(draft.iconUrl); // Components const row1 = new ActionRowBuilder() .addComponents( new ButtonBuilder().setCustomId("createitem_details").setLabel("Edit Details").setStyle(ButtonStyle.Secondary).setEmoji("📝"), new ButtonBuilder().setCustomId("createitem_economy").setLabel("Edit Economy").setStyle(ButtonStyle.Secondary).setEmoji("💰"), new ButtonBuilder().setCustomId("createitem_visuals").setLabel("Edit Visuals").setStyle(ButtonStyle.Secondary).setEmoji("🖼️"), new ButtonBuilder().setCustomId("createitem_type_toggle").setLabel("Change Type").setStyle(ButtonStyle.Secondary).setEmoji("🔄"), ); const row2 = new ActionRowBuilder() .addComponents( new ButtonBuilder().setCustomId("createitem_addeffect_start").setLabel("Add Effect").setStyle(ButtonStyle.Primary).setEmoji("✨"), new ButtonBuilder().setCustomId("createitem_toggle_consume").setLabel(`Consume: ${draft.usageData.consume ? "ON" : "OFF"}`).setStyle(ButtonStyle.Secondary).setEmoji("🔄"), new ButtonBuilder().setCustomId("createitem_save").setLabel("Save Item").setStyle(ButtonStyle.Success).setEmoji("💾"), new ButtonBuilder().setCustomId("createitem_cancel").setLabel("Cancel").setStyle(ButtonStyle.Danger).setEmoji("✖️") ); return { embeds: [embed], components: [row1, row2] }; }; // --- 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 = new ModalBuilder().setCustomId("createitem_modal_details").setTitle("Edit Details"); modal.addComponents( new ActionRowBuilder().addComponents(new TextInputBuilder().setCustomId("name").setLabel("Name").setValue(draft.name).setStyle(TextInputStyle.Short).setRequired(true)), new ActionRowBuilder().addComponents(new TextInputBuilder().setCustomId("desc").setLabel("Description").setValue(draft.description).setStyle(TextInputStyle.Paragraph).setRequired(false)), new ActionRowBuilder().addComponents(new TextInputBuilder().setCustomId("rarity").setLabel("Rarity").setValue(draft.rarity).setStyle(TextInputStyle.Short).setPlaceholder("Common, Rare, Legendary...").setRequired(true)) ); await interaction.showModal(modal); return; } // 2. Economy Modal if (interaction.customId === "createitem_economy") { if (!interaction.isButton()) return; const modal = new ModalBuilder().setCustomId("createitem_modal_economy").setTitle("Edit Economy"); modal.addComponents( new ActionRowBuilder().addComponents(new TextInputBuilder().setCustomId("price").setLabel("Price (0 for not for sale)").setValue(draft.price?.toString() || "0").setStyle(TextInputStyle.Short).setRequired(true)) ); await interaction.showModal(modal); return; } // 3. Visuals Modal if (interaction.customId === "createitem_visuals") { if (!interaction.isButton()) return; const modal = new ModalBuilder().setCustomId("createitem_modal_visuals").setTitle("Edit Visuals"); modal.addComponents( new ActionRowBuilder().addComponents(new TextInputBuilder().setCustomId("icon").setLabel("Icon URL (Emoji or Link)").setValue(draft.iconUrl).setStyle(TextInputStyle.Short).setRequired(false)), new ActionRowBuilder().addComponents(new TextInputBuilder().setCustomId("image").setLabel("Image URL").setValue(draft.imageUrl).setStyle(TextInputStyle.Short).setRequired(false)) ); await interaction.showModal(modal); return; } // 4. Type Toggle (Start Select Menu) if (interaction.customId === "createitem_type_toggle") { if (!interaction.isButton()) return; const row = new ActionRowBuilder().addComponents( new StringSelectMenuBuilder().setCustomId("createitem_select_type").setPlaceholder("Select Item Type").addOptions(getItemTypeOptions()) ); await interaction.update({ components: [row as any] }); // 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 row = new ActionRowBuilder().addComponents( new StringSelectMenuBuilder().setCustomId("createitem_select_effect_type").setPlaceholder("Select Effect Type").addOptions(getEffectTypeOptions()) ); await interaction.update({ components: [row as any] }); return; } if (interaction.customId === "createitem_select_effect_type") { if (!interaction.isStringSelectMenu()) return; const effectType = interaction.values[0]; 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. let modal = new ModalBuilder().setCustomId("createitem_modal_effect").setTitle(`Config ${effectType}`); if (effectType === "ADD_XP" || effectType === "ADD_BALANCE") { modal.addComponents(new ActionRowBuilder().addComponents(new TextInputBuilder().setCustomId("amount").setLabel("Amount").setStyle(TextInputStyle.Short).setRequired(true).setPlaceholder("100"))); } else if (effectType === "REPLY_MESSAGE") { modal.addComponents(new ActionRowBuilder().addComponents(new TextInputBuilder().setCustomId("message").setLabel("Message").setStyle(TextInputStyle.Paragraph).setRequired(true))); } else if (effectType === "XP_BOOST") { modal.addComponents( new ActionRowBuilder().addComponents(new TextInputBuilder().setCustomId("multiplier").setLabel("Multiplier (e.g. 1.5)").setStyle(TextInputStyle.Short).setRequired(true)), new ActionRowBuilder().addComponents(new TextInputBuilder().setCustomId("duration").setLabel("Duration (Seconds)").setStyle(TextInputStyle.Short).setRequired(true).setValue("3600")) ); } else if (effectType === "TEMP_ROLE") { modal.addComponents( new ActionRowBuilder().addComponents(new TextInputBuilder().setCustomId("role_id").setLabel("Role ID").setStyle(TextInputStyle.Short).setRequired(true)), new ActionRowBuilder().addComponents(new TextInputBuilder().setCustomId("duration").setLabel("Duration (Seconds)").setStyle(TextInputStyle.Short).setRequired(true).setValue("3600")) ); } else if (effectType === "COLOR_ROLE") { modal.addComponents( new ActionRowBuilder().addComponents(new TextInputBuilder().setCustomId("role_id").setLabel("Role ID").setStyle(TextInputStyle.Short).setRequired(true)) ); } 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 === "ADD_XP" || type === "ADD_BALANCE") { const amount = parseInt(interaction.fields.getTextInputValue("amount")); if (!isNaN(amount)) effect = { type: type as any, amount }; } else if (type === "REPLY_MESSAGE") { effect = { type: "REPLY_MESSAGE", message: interaction.fields.getTextInputValue("message") }; } else if (type === "XP_BOOST") { const multiplier = parseFloat(interaction.fields.getTextInputValue("multiplier")); const duration = parseInt(interaction.fields.getTextInputValue("duration")); if (!isNaN(multiplier) && !isNaN(duration)) effect = { type: "XP_BOOST", multiplier, durationSeconds: duration }; } else if (type === "TEMP_ROLE") { const roleId = interaction.fields.getTextInputValue("role_id"); const duration = parseInt(interaction.fields.getTextInputValue("duration")); if (roleId && !isNaN(duration)) effect = { type: "TEMP_ROLE", roleId: roleId, durationSeconds: duration }; } else if (type === "COLOR_ROLE") { const roleId = interaction.fields.getTextInputValue("role_id"); if (roleId) effect = { type: "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 }); } } };