feat: add /use command for inventory items with effects, implement XP boosts, and enhance scheduler for temporary role removal.

This commit is contained in:
syntaxbullet
2025-12-15 23:22:51 +01:00
parent 1d4263e178
commit d3ade218ec
6 changed files with 230 additions and 140 deletions

View File

@@ -1,10 +1,11 @@
import { inventory, items, users } from "@/db/schema";
import { inventory, items, users, userTimers } from "@/db/schema";
import { eq, and, sql, count } from "drizzle-orm";
import { DrizzleClient } from "@/lib/DrizzleClient";
import { economyService } from "@/modules/economy/economy.service";
import { levelingService } from "@/modules/leveling/leveling.service";
import { config } from "@/lib/config";
import { withTransaction } from "@/lib/db";
import type { Transaction } from "@/lib/types";
import type { Transaction, ItemUsageData } from "@/lib/types";
export const inventoryService = {
addItem: async (userId: string, itemId: number, quantity: bigint = 1n, tx?: Transaction) => {
@@ -130,4 +131,84 @@ export const inventoryService = {
where: eq(items.id, itemId),
});
},
useItem: async (userId: string, itemId: number, tx?: Transaction) => {
return await withTransaction(async (txFn) => {
// 1. Check Ownership & Quantity
const entry = await txFn.query.inventory.findFirst({
where: and(
eq(inventory.userId, BigInt(userId)),
eq(inventory.itemId, itemId)
),
with: { item: true }
});
if (!entry || (entry.quantity ?? 0n) < 1n) {
throw new Error("You do not own this item.");
}
const item = entry.item;
const usageData = item.usageData as ItemUsageData | null;
if (!usageData || !usageData.effects || usageData.effects.length === 0) {
throw new Error("This item cannot be used.");
}
const results: string[] = [];
// 2. Apply Effects
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 expiresAt = new Date(Date.now() + effect.durationSeconds * 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(effect.durationSeconds / 60)}m`);
break;
case 'TEMP_ROLE':
const roleExpiresAt = new Date(Date.now() + effect.durationSeconds * 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 (or here if we had client, but service shouldn't depend on client ideally)
// We return a flag to let the interaction handler know it needs to assign a role.
results.push(`Temporary Role granted for ${Math.floor(effect.durationSeconds / 60)}m`);
break;
}
}
// 3. Consume
if (usageData.consume) {
await inventoryService.removeItem(userId, itemId, 1n, txFn);
}
return { success: true, results, usageData };
}, tx);
}
};