From fcc82292f2a542a4c095923b057d2464767e15e9 Mon Sep 17 00:00:00 2001 From: syntaxbullet Date: Wed, 24 Dec 2025 12:20:42 +0100 Subject: [PATCH] feat: Introduce modular inventory item effect handling and centralize Discord interaction routing. --- src/events/interaction.routes.ts | 37 +++++++++++++ src/events/interactionCreate.ts | 32 +++++------ src/modules/inventory/effects/handlers.ts | 62 +++++++++++++++++++++ src/modules/inventory/effects/registry.ts | 18 +++++++ src/modules/inventory/effects/types.ts | 4 ++ src/modules/inventory/inventory.service.ts | 63 ++++------------------ 6 files changed, 145 insertions(+), 71 deletions(-) create mode 100644 src/events/interaction.routes.ts create mode 100644 src/modules/inventory/effects/handlers.ts create mode 100644 src/modules/inventory/effects/registry.ts create mode 100644 src/modules/inventory/effects/types.ts diff --git a/src/events/interaction.routes.ts b/src/events/interaction.routes.ts new file mode 100644 index 0000000..15e13da --- /dev/null +++ b/src/events/interaction.routes.ts @@ -0,0 +1,37 @@ +import { ButtonInteraction, ModalSubmitInteraction, StringSelectMenuInteraction } from "discord.js"; + +type InteractionHandler = (interaction: any) => Promise; + +interface InteractionRoute { + predicate: (interaction: ButtonInteraction | StringSelectMenuInteraction | ModalSubmitInteraction) => boolean; + handler: () => Promise; + method: string; +} + +export const interactionRoutes: InteractionRoute[] = [ + { + predicate: (i) => i.customId.startsWith("trade_") || i.customId === "amount", + handler: () => import("@/modules/trade/trade.interaction"), + method: 'handleTradeInteraction' + }, + { + predicate: (i) => i.isButton() && i.customId.startsWith("shop_buy_"), + handler: () => import("@/modules/economy/shop.interaction"), + method: 'handleShopInteraction' + }, + { + predicate: (i) => i.isButton() && i.customId.startsWith("lootdrop_"), + handler: () => import("@/modules/economy/lootdrop.interaction"), + method: 'handleLootdropInteraction' + }, + { + predicate: (i) => i.customId.startsWith("createitem_"), + handler: () => import("@/modules/admin/item_wizard"), + method: 'handleItemWizardInteraction' + }, + { + predicate: (i) => i.isButton() && i.customId === "enrollment", + handler: () => import("@/modules/user/enrollment.interaction"), + method: 'handleEnrollmentInteraction' + } +]; diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index fad44fe..1b1ff23 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -9,25 +9,19 @@ const event: Event = { execute: async (interaction) => { // Handle Trade Interactions if (interaction.isButton() || interaction.isStringSelectMenu() || interaction.isModalSubmit()) { - if (interaction.customId.startsWith("trade_") || interaction.customId === "amount") { - await import("@/modules/trade/trade.interaction").then(m => m.handleTradeInteraction(interaction)); - return; - } - if (interaction.customId.startsWith("shop_buy_") && interaction.isButton()) { - await import("@/modules/economy/shop.interaction").then(m => m.handleShopInteraction(interaction)); - return; - } - if (interaction.customId.startsWith("lootdrop_") && interaction.isButton()) { - await import("@/modules/economy/lootdrop.interaction").then(m => m.handleLootdropInteraction(interaction)); - return; - } - if (interaction.customId.startsWith("createitem_")) { - await import("@/modules/admin/item_wizard").then(m => m.handleItemWizardInteraction(interaction)); - return; - } - if (interaction.customId === "enrollment" && interaction.isButton()) { - await import("@/modules/user/enrollment.interaction").then(m => m.handleEnrollmentInteraction(interaction)); - return; + const { interactionRoutes } = await import("./interaction.routes"); + + for (const route of interactionRoutes) { + if (route.predicate(interaction)) { + const module = await route.handler(); + const handlerMethod = module[route.method]; + if (typeof handlerMethod === 'function') { + await handlerMethod(interaction); + return; + } else { + console.error(`Handler method ${route.method} not found in module`); + } + } } } diff --git a/src/modules/inventory/effects/handlers.ts b/src/modules/inventory/effects/handlers.ts new file mode 100644 index 0000000..372a59f --- /dev/null +++ b/src/modules/inventory/effects/handlers.ts @@ -0,0 +1,62 @@ +import { levelingService } from "@/modules/leveling/leveling.service"; +import { economyService } from "@/modules/economy/economy.service"; +import { userTimers } from "@/db/schema"; +import type { EffectHandler } from "./types"; + +// Helper to extract duration in seconds +const getDuration = (effect: any): number => { + if (effect.durationHours) return effect.durationHours * 3600; + if (effect.durationMinutes) return effect.durationMinutes * 60; + return effect.durationSeconds || 60; // Default to 60s if nothing provided +}; + +export const handleAddXp: EffectHandler = async (userId, effect, txFn) => { + await levelingService.addXp(userId, BigInt(effect.amount), txFn); + return `Gained ${effect.amount} XP`; +}; + +export const handleAddBalance: EffectHandler = async (userId, effect, txFn) => { + await economyService.modifyUserBalance(userId, BigInt(effect.amount), 'ITEM_USE', `Used Item`, null, txFn); + return `Gained ${effect.amount} 🪙`; +}; + +export const handleReplyMessage: EffectHandler = async (_userId, effect, _txFn) => { + return effect.message; +}; + +export const handleXpBoost: EffectHandler = async (userId, effect, txFn) => { + const boostDuration = getDuration(effect); + const expiresAt = new Date(Date.now() + boostDuration * 1000); + await txFn.insert(userTimers).values({ + userId: BigInt(userId), + type: 'EFFECT', + key: 'xp_boost', + expiresAt: expiresAt, + metadata: { multiplier: effect.multiplier } + }).onConflictDoUpdate({ + target: [userTimers.userId, userTimers.type, userTimers.key], + set: { expiresAt: expiresAt, metadata: { multiplier: effect.multiplier } } + }); + return `XP Boost (${effect.multiplier}x) active for ${Math.floor(boostDuration / 60)}m`; +}; + +export const handleTempRole: EffectHandler = async (userId, effect, txFn) => { + const roleDuration = getDuration(effect); + const roleExpiresAt = new Date(Date.now() + roleDuration * 1000); + await txFn.insert(userTimers).values({ + userId: BigInt(userId), + type: 'ACCESS', + key: `role_${effect.roleId}`, + expiresAt: roleExpiresAt, + metadata: { roleId: effect.roleId } + }).onConflictDoUpdate({ + target: [userTimers.userId, userTimers.type, userTimers.key], + set: { expiresAt: roleExpiresAt } + }); + // Actual role assignment happens in the Command layer + return `Temporary Role granted for ${Math.floor(roleDuration / 60)}m`; +}; + +export const handleColorRole: EffectHandler = async (_userId, _effect, _txFn) => { + return "Color Role Equipped"; +}; diff --git a/src/modules/inventory/effects/registry.ts b/src/modules/inventory/effects/registry.ts new file mode 100644 index 0000000..fcdde94 --- /dev/null +++ b/src/modules/inventory/effects/registry.ts @@ -0,0 +1,18 @@ +import { + handleAddXp, + handleAddBalance, + handleReplyMessage, + handleXpBoost, + handleTempRole, + handleColorRole +} from "./handlers"; +import type { EffectHandler } from "./types"; + +export const effectHandlers: Record = { + 'ADD_XP': handleAddXp, + 'ADD_BALANCE': handleAddBalance, + 'REPLY_MESSAGE': handleReplyMessage, + 'XP_BOOST': handleXpBoost, + 'TEMP_ROLE': handleTempRole, + 'COLOR_ROLE': handleColorRole +}; diff --git a/src/modules/inventory/effects/types.ts b/src/modules/inventory/effects/types.ts new file mode 100644 index 0000000..478cc80 --- /dev/null +++ b/src/modules/inventory/effects/types.ts @@ -0,0 +1,4 @@ + +import type { Transaction } from "@/lib/types"; + +export type EffectHandler = (userId: string, effect: any, txFn: Transaction) => Promise; diff --git a/src/modules/inventory/inventory.service.ts b/src/modules/inventory/inventory.service.ts index 39d1994..7e0cf07 100644 --- a/src/modules/inventory/inventory.service.ts +++ b/src/modules/inventory/inventory.service.ts @@ -8,12 +8,7 @@ import { UserError } from "@/lib/errors"; import { withTransaction } from "@/lib/db"; import type { Transaction, ItemUsageData } from "@/lib/types"; -// Helper to extract duration in seconds -const getDuration = (effect: any): number => { - if (effect.durationHours) return effect.durationHours * 3600; - if (effect.durationMinutes) return effect.durationMinutes * 60; - return effect.durationSeconds || 60; // Default to 60s if nothing provided -}; + export const inventoryService = { addItem: async (userId: string, itemId: number, quantity: bigint = 1n, tx?: Transaction) => { @@ -165,53 +160,17 @@ export const inventoryService = { const results: string[] = []; // 2. Apply Effects + // 2. Apply Effects + const { effectHandlers } = await import("./effects/registry"); + for (const effect of usageData.effects) { - switch (effect.type) { - case 'ADD_XP': - await levelingService.addXp(userId, BigInt(effect.amount), txFn); - results.push(`Gained ${effect.amount} XP`); - break; - case 'ADD_BALANCE': - await economyService.modifyUserBalance(userId, BigInt(effect.amount), 'ITEM_USE', `Used ${item.name}`, null, txFn); - results.push(`Gained ${effect.amount} 🪙`); - break; - case 'REPLY_MESSAGE': - results.push(effect.message); - break; - case 'XP_BOOST': - const boostDuration = getDuration(effect); - const expiresAt = new Date(Date.now() + boostDuration * 1000); - await txFn.insert(userTimers).values({ - userId: BigInt(userId), - type: 'EFFECT', - key: 'xp_boost', - expiresAt: expiresAt, - metadata: { multiplier: effect.multiplier } - }).onConflictDoUpdate({ - target: [userTimers.userId, userTimers.type, userTimers.key], - set: { expiresAt: expiresAt, metadata: { multiplier: effect.multiplier } } - }); - results.push(`XP Boost (${effect.multiplier}x) active for ${Math.floor(boostDuration / 60)}m`); - break; - case 'TEMP_ROLE': - const roleDuration = getDuration(effect); - const roleExpiresAt = new Date(Date.now() + roleDuration * 1000); - await txFn.insert(userTimers).values({ - userId: BigInt(userId), - type: 'ACCESS', - key: `role_${effect.roleId}`, - expiresAt: roleExpiresAt, - metadata: { roleId: effect.roleId } - }).onConflictDoUpdate({ - target: [userTimers.userId, userTimers.type, userTimers.key], - set: { expiresAt: roleExpiresAt } - }); - // 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; + const handler = effectHandlers[effect.type]; + if (handler) { + const result = await handler(userId, effect, txFn); + results.push(result); + } else { + console.warn(`No handler found for effect type: ${effect.type}`); + results.push(`Effect ${effect.type} applied (no description)`); } }