From a3099b80c506083a8169ae10e97415937ceac026 Mon Sep 17 00:00:00 2001 From: syntaxbullet Date: Tue, 23 Dec 2025 21:12:36 +0100 Subject: [PATCH] feat: Add color role item effect with role swapping and implement item consumption toggle. --- src/commands/inventory/use.ts | 12 ++++++++++-- src/lib/config.ts | 2 ++ src/lib/types.ts | 3 ++- src/modules/admin/item_wizard.ts | 20 ++++++++++++++++++++ src/modules/inventory/inventory.service.ts | 3 +++ 5 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/commands/inventory/use.ts b/src/commands/inventory/use.ts index 155ddcf..8bc918a 100644 --- a/src/commands/inventory/use.ts +++ b/src/commands/inventory/use.ts @@ -8,6 +8,7 @@ import { eq, and, like } from "drizzle-orm"; import { DrizzleClient } from "@/lib/DrizzleClient"; import type { ItemUsageData } from "@/lib/types"; import { UserError } from "@/lib/errors"; +import { config } from "@/lib/config"; export const use = createCommand({ data: new SlashCommandBuilder() @@ -31,11 +32,18 @@ export const use = createCommand({ const usageData = result.usageData; if (usageData) { for (const effect of usageData.effects) { - if (effect.type === 'TEMP_ROLE') { + if (effect.type === 'TEMP_ROLE' || effect.type === 'COLOR_ROLE') { try { const member = await interaction.guild?.members.fetch(user.id); if (member) { - await member.roles.add(effect.roleId); + if (effect.type === 'TEMP_ROLE') { + await member.roles.add(effect.roleId); + } else if (effect.type === 'COLOR_ROLE') { + // Remove existing color roles + const rolesToRemove = config.colorRoles.filter(r => member.roles.cache.has(r)); + if (rolesToRemove.length > 0) await member.roles.remove(rolesToRemove); + await member.roles.add(effect.roleId); + } } } catch (e) { console.error("Failed to assign role in /use command:", e); diff --git a/src/lib/config.ts b/src/lib/config.ts index a56e08a..7d8508e 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -48,6 +48,7 @@ export interface GameConfigType { }; studentRole: string; visitorRole: string; + colorRoles: string[]; welcomeChannelId?: string; welcomeMessage?: string; } @@ -111,6 +112,7 @@ const configSchema = z.object({ }), studentRole: z.string(), visitorRole: z.string(), + colorRoles: z.array(z.string()).default([]), welcomeChannelId: z.string().optional(), welcomeMessage: z.string().optional() }); diff --git a/src/lib/types.ts b/src/lib/types.ts index 97f40b6..b9400f7 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -18,7 +18,8 @@ export type ItemEffect = | { type: 'ADD_BALANCE'; amount: number } | { type: 'XP_BOOST'; multiplier: number; durationSeconds?: number; durationMinutes?: number; durationHours?: number } | { type: 'TEMP_ROLE'; roleId: string; durationSeconds?: number; durationMinutes?: number; durationHours?: number } - | { type: 'REPLY_MESSAGE'; message: string }; + | { type: 'REPLY_MESSAGE'; message: string } + | { type: 'COLOR_ROLE'; roleId: string }; export interface ItemUsageData { consume: boolean; diff --git a/src/modules/admin/item_wizard.ts b/src/modules/admin/item_wizard.ts index fee6b87..aad6730 100644 --- a/src/modules/admin/item_wizard.ts +++ b/src/modules/admin/item_wizard.ts @@ -44,6 +44,7 @@ const getEffectTypeOptions = () => [ { 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 --- @@ -72,6 +73,7 @@ export const renderWizard = (userId: string, isDraft = true) => { { 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 @@ -97,6 +99,7 @@ export const renderWizard = (userId: string, isDraft = true) => { 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("✖️") ); @@ -241,12 +244,25 @@ export const handleItemWizardInteraction = async (interaction: Interaction) => { 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") { @@ -284,6 +300,10 @@ export const handleItemWizardInteraction = async (interaction: Interaction) => { 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); diff --git a/src/modules/inventory/inventory.service.ts b/src/modules/inventory/inventory.service.ts index 29ba381..39d1994 100644 --- a/src/modules/inventory/inventory.service.ts +++ b/src/modules/inventory/inventory.service.ts @@ -209,6 +209,9 @@ export const inventoryService = { // Actual role assignment happens in the Command layer results.push(`Temporary Role granted for ${Math.floor(roleDuration / 60)}m`); break; + case 'COLOR_ROLE': + results.push("Color Role Equipped"); + break; } }