330 lines
16 KiB
TypeScript
330 lines
16 KiB
TypeScript
import {
|
|
ActionRowBuilder,
|
|
ButtonBuilder,
|
|
ButtonStyle,
|
|
EmbedBuilder,
|
|
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";
|
|
|
|
// --- 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<string, DraftItem>();
|
|
|
|
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" },
|
|
];
|
|
|
|
// --- 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 = new EmbedBuilder()
|
|
.setTitle(`🛠️ Item Creator: ${draft.name}`)
|
|
.setColor("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 },
|
|
);
|
|
|
|
// 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<MessageActionRowComponentBuilder>()
|
|
.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<MessageActionRowComponentBuilder>()
|
|
.addComponents(
|
|
new ButtonBuilder().setCustomId("createitem_addeffect_start").setLabel("Add Effect").setStyle(ButtonStyle.Primary).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<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId("name").setLabel("Name").setValue(draft.name).setStyle(TextInputStyle.Short).setRequired(true)),
|
|
new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId("desc").setLabel("Description").setValue(draft.description).setStyle(TextInputStyle.Paragraph).setRequired(false)),
|
|
new ActionRowBuilder<TextInputBuilder>().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<TextInputBuilder>().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<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId("icon").setLabel("Icon URL (Emoji or Link)").setValue(draft.iconUrl).setStyle(TextInputStyle.Short).setRequired(false)),
|
|
new ActionRowBuilder<TextInputBuilder>().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<StringSelectMenuBuilder>().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<StringSelectMenuBuilder>().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<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId("amount").setLabel("Amount").setStyle(TextInputStyle.Short).setRequired(true).setPlaceholder("100")));
|
|
} else if (effectType === "REPLY_MESSAGE") {
|
|
modal.addComponents(new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId("message").setLabel("Message").setStyle(TextInputStyle.Paragraph).setRequired(true)));
|
|
} else if (effectType === "XP_BOOST") {
|
|
modal.addComponents(
|
|
new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId("multiplier").setLabel("Multiplier (e.g. 1.5)").setStyle(TextInputStyle.Short).setRequired(true)),
|
|
new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId("duration").setLabel("Duration (Seconds)").setStyle(TextInputStyle.Short).setRequired(true).setValue("3600"))
|
|
);
|
|
} else if (effectType === "TEMP_ROLE") {
|
|
modal.addComponents(
|
|
new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId("role_id").setLabel("Role ID").setStyle(TextInputStyle.Short).setRequired(true)),
|
|
new ActionRowBuilder<TextInputBuilder>().addComponents(new TextInputBuilder().setCustomId("duration").setLabel("Duration (Seconds)").setStyle(TextInputStyle.Short).setRequired(true).setValue("3600"))
|
|
);
|
|
}
|
|
|
|
await interaction.showModal(modal);
|
|
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 };
|
|
}
|
|
|
|
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 });
|
|
}
|
|
}
|
|
|
|
};
|