import { inventory, items, users, userTimers } from "@db/schema"; import { eq, and, sql, count, ilike } from "drizzle-orm"; import { DrizzleClient } from "@shared/db/DrizzleClient"; import { economyService } from "@shared/modules/economy/economy.service"; import { levelingService } from "@shared/modules/leveling/leveling.service"; import { config } from "@shared/lib/config"; import { UserError } from "@shared/lib/errors"; import { withTransaction } from "@/lib/db"; import type { Transaction, ItemUsageData } from "@shared/lib/types"; import { TransactionType } from "@shared/lib/constants"; export const inventoryService = { addItem: async (userId: string, itemId: number, quantity: bigint = 1n, tx?: Transaction) => { return await withTransaction(async (txFn) => { // Check if item exists in inventory const existing = await txFn.query.inventory.findFirst({ where: and( eq(inventory.userId, BigInt(userId)), eq(inventory.itemId, itemId) ), }); if (existing) { const newQuantity = (existing.quantity ?? 0n) + quantity; if (newQuantity > config.inventory.maxStackSize) { throw new UserError(`Cannot exceed max stack size of ${config.inventory.maxStackSize}`); } const [entry] = await txFn.update(inventory) .set({ quantity: newQuantity, }) .where(and( eq(inventory.userId, BigInt(userId)), eq(inventory.itemId, itemId) )) .returning(); return entry; } else { // Check Slot Limit const [inventoryCount] = await txFn .select({ count: count() }) .from(inventory) .where(eq(inventory.userId, BigInt(userId))); if (inventoryCount && inventoryCount.count >= config.inventory.maxSlots) { throw new UserError(`Inventory full (Max ${config.inventory.maxSlots} slots)`); } if (quantity > config.inventory.maxStackSize) { throw new UserError(`Cannot exceed max stack size of ${config.inventory.maxStackSize}`); } const [entry] = await txFn.insert(inventory) .values({ userId: BigInt(userId), itemId: itemId, quantity: quantity, }) .returning(); return entry; } }, tx); }, removeItem: async (userId: string, itemId: number, quantity: bigint = 1n, tx?: Transaction) => { return await withTransaction(async (txFn) => { const existing = await txFn.query.inventory.findFirst({ where: and( eq(inventory.userId, BigInt(userId)), eq(inventory.itemId, itemId) ), }); if (!existing || (existing.quantity ?? 0n) < quantity) { throw new UserError("Insufficient item quantity"); } if ((existing.quantity ?? 0n) === quantity) { // Delete if quantity becomes 0 await txFn.delete(inventory) .where(and( eq(inventory.userId, BigInt(userId)), eq(inventory.itemId, itemId) )); return { itemId, quantity: 0n, userId: BigInt(userId) }; } else { const [entry] = await txFn.update(inventory) .set({ quantity: sql`${inventory.quantity} - ${quantity}`, }) .where(and( eq(inventory.userId, BigInt(userId)), eq(inventory.itemId, itemId) )) .returning(); return entry; } }, tx); }, getInventory: async (userId: string) => { return await DrizzleClient.query.inventory.findMany({ where: eq(inventory.userId, BigInt(userId)), with: { item: true, }, }); }, buyItem: async (userId: string, itemId: number, quantity: bigint = 1n, tx?: Transaction) => { return await withTransaction(async (txFn) => { const item = await txFn.query.items.findFirst({ where: eq(items.id, itemId), }); if (!item) throw new UserError("Item not found"); if (!item.price) throw new UserError("Item is not for sale"); const totalPrice = item.price * quantity; // Deduct Balance using economy service (passing tx ensures atomicity) await economyService.modifyUserBalance(userId, -totalPrice, TransactionType.PURCHASE, `Bought ${quantity}x ${item.name}`, null, txFn); await inventoryService.addItem(userId, itemId, quantity, txFn); return { success: true, item, totalPrice }; }, tx); }, getItem: async (itemId: number) => { return await DrizzleClient.query.items.findFirst({ 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 UserError("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 UserError("This item cannot be used."); } const results: string[] = []; // 2. Apply Effects const { effectHandlers } = await import("@/modules/inventory/effects/registry"); for (const effect of usageData.effects) { 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)`); } } // 3. Consume if (usageData.consume) { await inventoryService.removeItem(userId, itemId, 1n, txFn); } return { success: true, results, usageData, item }; }, tx); }, getAutocompleteItems: async (userId: string, query: string) => { const entries = await DrizzleClient.select({ quantity: inventory.quantity, item: items }) .from(inventory) .innerJoin(items, eq(inventory.itemId, items.id)) .where(and( eq(inventory.userId, BigInt(userId)), ilike(items.name, `%${query}%`) )) .limit(20); const filtered = entries.filter((entry: any) => { const usageData = entry.item.usageData as ItemUsageData | null; return usageData && usageData.effects && usageData.effects.length > 0; }); return filtered.map((entry: any) => ({ name: `${entry.item.name} (${entry.quantity}) [${entry.item.rarity || 'Common'}]`, value: entry.item.id })); } };