Files
discord-rpg-concept/src/modules/inventory/inventory.service.ts

186 lines
7.0 KiB
TypeScript

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 { UserError } from "@/lib/errors";
import { withTransaction } from "@/lib/db";
import type { Transaction, ItemUsageData } from "@/lib/types";
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, '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
// 2. Apply Effects
const { effectHandlers } = await import("./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 };
}, tx);
}
};