forked from syntaxbullet/AuroraBot-discord
feat: Introduce modular inventory item effect handling and centralize Discord interaction routing.
This commit is contained in:
37
src/events/interaction.routes.ts
Normal file
37
src/events/interaction.routes.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { ButtonInteraction, ModalSubmitInteraction, StringSelectMenuInteraction } from "discord.js";
|
||||||
|
|
||||||
|
type InteractionHandler = (interaction: any) => Promise<void>;
|
||||||
|
|
||||||
|
interface InteractionRoute {
|
||||||
|
predicate: (interaction: ButtonInteraction | StringSelectMenuInteraction | ModalSubmitInteraction) => boolean;
|
||||||
|
handler: () => Promise<any>;
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
];
|
||||||
@@ -9,25 +9,19 @@ const event: Event<Events.InteractionCreate> = {
|
|||||||
execute: async (interaction) => {
|
execute: async (interaction) => {
|
||||||
// Handle Trade Interactions
|
// Handle Trade Interactions
|
||||||
if (interaction.isButton() || interaction.isStringSelectMenu() || interaction.isModalSubmit()) {
|
if (interaction.isButton() || interaction.isStringSelectMenu() || interaction.isModalSubmit()) {
|
||||||
if (interaction.customId.startsWith("trade_") || interaction.customId === "amount") {
|
const { interactionRoutes } = await import("./interaction.routes");
|
||||||
await import("@/modules/trade/trade.interaction").then(m => m.handleTradeInteraction(interaction));
|
|
||||||
|
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;
|
return;
|
||||||
|
} else {
|
||||||
|
console.error(`Handler method ${route.method} not found in module`);
|
||||||
}
|
}
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
62
src/modules/inventory/effects/handlers.ts
Normal file
62
src/modules/inventory/effects/handlers.ts
Normal file
@@ -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";
|
||||||
|
};
|
||||||
18
src/modules/inventory/effects/registry.ts
Normal file
18
src/modules/inventory/effects/registry.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import {
|
||||||
|
handleAddXp,
|
||||||
|
handleAddBalance,
|
||||||
|
handleReplyMessage,
|
||||||
|
handleXpBoost,
|
||||||
|
handleTempRole,
|
||||||
|
handleColorRole
|
||||||
|
} from "./handlers";
|
||||||
|
import type { EffectHandler } from "./types";
|
||||||
|
|
||||||
|
export const effectHandlers: Record<string, EffectHandler> = {
|
||||||
|
'ADD_XP': handleAddXp,
|
||||||
|
'ADD_BALANCE': handleAddBalance,
|
||||||
|
'REPLY_MESSAGE': handleReplyMessage,
|
||||||
|
'XP_BOOST': handleXpBoost,
|
||||||
|
'TEMP_ROLE': handleTempRole,
|
||||||
|
'COLOR_ROLE': handleColorRole
|
||||||
|
};
|
||||||
4
src/modules/inventory/effects/types.ts
Normal file
4
src/modules/inventory/effects/types.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
import type { Transaction } from "@/lib/types";
|
||||||
|
|
||||||
|
export type EffectHandler = (userId: string, effect: any, txFn: Transaction) => Promise<string>;
|
||||||
@@ -8,12 +8,7 @@ import { UserError } from "@/lib/errors";
|
|||||||
import { withTransaction } from "@/lib/db";
|
import { withTransaction } from "@/lib/db";
|
||||||
import type { Transaction, ItemUsageData } from "@/lib/types";
|
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 = {
|
export const inventoryService = {
|
||||||
addItem: async (userId: string, itemId: number, quantity: bigint = 1n, tx?: Transaction) => {
|
addItem: async (userId: string, itemId: number, quantity: bigint = 1n, tx?: Transaction) => {
|
||||||
@@ -165,53 +160,17 @@ export const inventoryService = {
|
|||||||
const results: string[] = [];
|
const results: string[] = [];
|
||||||
|
|
||||||
// 2. Apply Effects
|
// 2. Apply Effects
|
||||||
|
// 2. Apply Effects
|
||||||
|
const { effectHandlers } = await import("./effects/registry");
|
||||||
|
|
||||||
for (const effect of usageData.effects) {
|
for (const effect of usageData.effects) {
|
||||||
switch (effect.type) {
|
const handler = effectHandlers[effect.type];
|
||||||
case 'ADD_XP':
|
if (handler) {
|
||||||
await levelingService.addXp(userId, BigInt(effect.amount), txFn);
|
const result = await handler(userId, effect, txFn);
|
||||||
results.push(`Gained ${effect.amount} XP`);
|
results.push(result);
|
||||||
break;
|
} else {
|
||||||
case 'ADD_BALANCE':
|
console.warn(`No handler found for effect type: ${effect.type}`);
|
||||||
await economyService.modifyUserBalance(userId, BigInt(effect.amount), 'ITEM_USE', `Used ${item.name}`, null, txFn);
|
results.push(`Effect ${effect.type} applied (no description)`);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user