forked from syntaxbullet/AuroraBot-discord
refactor: initial moves
This commit is contained in:
209
shared/modules/inventory/inventory.service.ts
Normal file
209
shared/modules/inventory/inventory.service.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
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 "@/lib/config";
|
||||
import { UserError } from "@/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("./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
|
||||
}));
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user