From 5f4efd372f315a12238713724db7e0739662f6bf Mon Sep 17 00:00:00 2001 From: syntaxbullet Date: Sat, 13 Dec 2025 12:20:30 +0100 Subject: [PATCH] feat: Introduce `GameConfig` to centralize constants for leveling, economy, and inventory, adding new transfer and inventory limits. --- src/commands/economy/pay.ts | 6 ++++ src/config/game.ts | 29 +++++++++++++++++ src/modules/economy/economy.service.ts | 11 +++---- src/modules/inventory/inventory.service.ts | 37 +++++++++++++++------- src/modules/leveling/leveling.service.ts | 14 +++----- 5 files changed, 68 insertions(+), 29 deletions(-) create mode 100644 src/config/game.ts diff --git a/src/commands/economy/pay.ts b/src/commands/economy/pay.ts index d98e61a..77aebdf 100644 --- a/src/commands/economy/pay.ts +++ b/src/commands/economy/pay.ts @@ -2,6 +2,7 @@ import { createCommand } from "@/lib/utils"; import { SlashCommandBuilder, EmbedBuilder } from "discord.js"; import { economyService } from "@/modules/economy/economy.service"; import { userService } from "@/modules/user/user.service"; +import { GameConfig } from "@/config/game"; export const pay = createCommand({ data: new SlashCommandBuilder() @@ -26,6 +27,11 @@ export const pay = createCommand({ const senderId = interaction.user.id; const receiverId = targetUser.id; + if (amount < GameConfig.economy.transfers.minAmount) { + await interaction.editReply({ content: `❌ Amount must be at least ${GameConfig.economy.transfers.minAmount}.` }); + return; + } + if (senderId === receiverId) { await interaction.editReply({ content: "❌ You cannot pay yourself." }); return; diff --git a/src/config/game.ts b/src/config/game.ts new file mode 100644 index 0000000..516b723 --- /dev/null +++ b/src/config/game.ts @@ -0,0 +1,29 @@ +export const GameConfig = { + leveling: { + // Curve: Base * (Level ^ Exponent) + base: 100, + exponent: 2.5, + + chat: { + cooldownMs: 60000, // 1 minute + minXp: 15, + maxXp: 25, + } + }, + economy: { + daily: { + amount: 100n, + streakBonus: 10n, + cooldownMs: 24 * 60 * 60 * 1000, // 24 hours + }, + transfers: { + // Future use + allowSelfTransfer: false, + minAmount: 1n, + } + }, + inventory: { + maxStackSize: 999n, + maxSlots: 50, + } +} as const; diff --git a/src/modules/economy/economy.service.ts b/src/modules/economy/economy.service.ts index 03ef866..65eabe4 100644 --- a/src/modules/economy/economy.service.ts +++ b/src/modules/economy/economy.service.ts @@ -1,10 +1,7 @@ import { users, transactions, cooldowns } from "@/db/schema"; import { eq, sql, and } from "drizzle-orm"; import { DrizzleClient } from "@/lib/DrizzleClient"; - -const DAILY_REWARD_AMOUNT = 100n; -const STREAK_BONUS = 10n; -const DAILY_COOLDOWN = 24 * 60 * 60 * 1000; // 24 hours in ms +import { GameConfig } from "@/config/game"; export const economyService = { transfer: async (fromUserId: string, toUserId: string, amount: bigint, tx?: any) => { @@ -112,9 +109,9 @@ export const economyService = { streak = 1; } - const bonus = (BigInt(streak) - 1n) * STREAK_BONUS; + const bonus = (BigInt(streak) - 1n) * GameConfig.economy.daily.streakBonus; - const totalReward = DAILY_REWARD_AMOUNT + bonus; + const totalReward = GameConfig.economy.daily.amount + bonus; // Update User w/ Economy Service (reuse modifyUserBalance if we split it out, but here manual is fine for atomic combined streak update) // Actually, we can just update directly here as we are already refining specific fields like streak. @@ -127,7 +124,7 @@ export const economyService = { .where(eq(users.id, BigInt(userId))); // Set new cooldown (now + 24h) - const nextReadyAt = new Date(now.getTime() + DAILY_COOLDOWN); + const nextReadyAt = new Date(now.getTime() + GameConfig.economy.daily.cooldownMs); await txFn.insert(cooldowns) .values({ diff --git a/src/modules/inventory/inventory.service.ts b/src/modules/inventory/inventory.service.ts index f8a1f8b..2dffe96 100644 --- a/src/modules/inventory/inventory.service.ts +++ b/src/modules/inventory/inventory.service.ts @@ -1,8 +1,9 @@ import { inventory, items, users } from "@/db/schema"; -import { eq, and, sql } from "drizzle-orm"; +import { eq, and, sql, count } from "drizzle-orm"; import { DrizzleClient } from "@/lib/DrizzleClient"; import { economyService } from "@/modules/economy/economy.service"; +import { GameConfig } from "@/config/game"; export const inventoryService = { addItem: async (userId: string, itemId: number, quantity: bigint = 1n, tx?: any) => { @@ -16,9 +17,14 @@ export const inventoryService = { }); if (existing) { + const newQuantity = (existing.quantity ?? 0n) + quantity; + if (newQuantity > GameConfig.inventory.maxStackSize) { + throw new Error(`Cannot exceed max stack size of ${GameConfig.inventory.maxStackSize}`); + } + const [entry] = await txFn.update(inventory) .set({ - quantity: sql`${inventory.quantity} + ${quantity}`, + quantity: newQuantity, }) .where(and( eq(inventory.userId, BigInt(userId)), @@ -27,6 +33,20 @@ export const inventoryService = { .returning(); return entry; } else { + // Check Slot Limit + const [inventoryCount] = await txFn + .select({ count: count() }) + .from(inventory) + .where(eq(inventory.userId, BigInt(userId))); + + if (inventoryCount.count >= GameConfig.inventory.maxSlots) { + throw new Error(`Inventory full (Max ${GameConfig.inventory.maxSlots} slots)`); + } + + if (quantity > GameConfig.inventory.maxStackSize) { + throw new Error(`Cannot exceed max stack size of ${GameConfig.inventory.maxStackSize}`); + } + const [entry] = await txFn.insert(inventory) .values({ userId: BigInt(userId), @@ -105,16 +125,9 @@ export const inventoryService = { // Since we are modifying buyItem, we can just inline the item addition or call addItem if we update it. // Let's assume we update addItem next. For now, inline the add logic but cleaner. - const existingInv = await txFn.query.inventory.findFirst({ - where: and(eq(inventory.userId, BigInt(userId)), eq(inventory.itemId, itemId)), - }); - - if (existingInv) { - await txFn.update(inventory).set({ quantity: sql`${inventory.quantity} + ${quantity}` }) - .where(and(eq(inventory.userId, BigInt(userId)), eq(inventory.itemId, itemId))); - } else { - await txFn.insert(inventory).values({ userId: BigInt(userId), itemId, quantity }); - } + // Add Item using inner logic or self-call if we refactor properly. + // Calling addItem directly within the same transaction scope: + await inventoryService.addItem(userId, itemId, quantity, txFn); return { success: true, item, totalPrice }; }; diff --git a/src/modules/leveling/leveling.service.ts b/src/modules/leveling/leveling.service.ts index ecd4d07..ca33a6b 100644 --- a/src/modules/leveling/leveling.service.ts +++ b/src/modules/leveling/leveling.service.ts @@ -1,18 +1,12 @@ import { users, cooldowns } from "@/db/schema"; import { eq, sql, and } from "drizzle-orm"; import { DrizzleClient } from "@/lib/DrizzleClient"; - -// Simple configurable curve: Base * (Level ^ Exponent) -const XP_BASE = 100; -const XP_EXPONENT = 2.5; -const CHAT_XP_COOLDOWN_MS = 60000; // 1 minute -const MIN_CHAT_XP = 15; -const MAX_CHAT_XP = 25; +import { GameConfig } from "@/config/game"; export const levelingService = { // Calculate XP required for a specific level getXpForLevel: (level: number) => { - return Math.floor(XP_BASE * Math.pow(level, XP_EXPONENT)); + return Math.floor(GameConfig.leveling.base * Math.pow(level, GameConfig.leveling.exponent)); }, // Pure XP addition - No cooldown checks @@ -77,13 +71,13 @@ export const levelingService = { } // Calculate random XP - const amount = BigInt(Math.floor(Math.random() * (MAX_CHAT_XP - MIN_CHAT_XP + 1)) + MIN_CHAT_XP); + const amount = BigInt(Math.floor(Math.random() * (GameConfig.leveling.chat.maxXp - GameConfig.leveling.chat.minXp + 1)) + GameConfig.leveling.chat.minXp); // Add XP const result = await levelingService.addXp(id, amount, txFn); // Update/Set Cooldown - const nextReadyAt = new Date(now.getTime() + CHAT_XP_COOLDOWN_MS); + const nextReadyAt = new Date(now.getTime() + GameConfig.leveling.chat.cooldownMs); await txFn.insert(cooldowns) .values({