feat: Add color role item effect with role swapping and implement item consumption toggle.

This commit is contained in:
syntaxbullet
2025-12-23 21:12:36 +01:00
parent 67d6298793
commit a3099b80c5
5 changed files with 37 additions and 3 deletions

View File

@@ -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);

View File

@@ -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()
});

View File

@@ -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;

View File

@@ -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<MessageActionRowComponentBuilder>()
.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<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"))
);
} else if (effectType === "COLOR_ROLE") {
modal.addComponents(
new ActionRowBuilder<TextInputBuilder>().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);

View File

@@ -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;
}
}