feat: Introduce GameConfig to centralize constants for leveling, economy, and inventory, adding new transfer and inventory limits.

This commit is contained in:
syntaxbullet
2025-12-13 12:20:30 +01:00
parent 8818d6bb15
commit 5f4efd372f
5 changed files with 68 additions and 29 deletions

View File

@@ -2,6 +2,7 @@ import { createCommand } from "@/lib/utils";
import { SlashCommandBuilder, EmbedBuilder } from "discord.js"; import { SlashCommandBuilder, EmbedBuilder } from "discord.js";
import { economyService } from "@/modules/economy/economy.service"; import { economyService } from "@/modules/economy/economy.service";
import { userService } from "@/modules/user/user.service"; import { userService } from "@/modules/user/user.service";
import { GameConfig } from "@/config/game";
export const pay = createCommand({ export const pay = createCommand({
data: new SlashCommandBuilder() data: new SlashCommandBuilder()
@@ -26,6 +27,11 @@ export const pay = createCommand({
const senderId = interaction.user.id; const senderId = interaction.user.id;
const receiverId = targetUser.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) { if (senderId === receiverId) {
await interaction.editReply({ content: "❌ You cannot pay yourself." }); await interaction.editReply({ content: "❌ You cannot pay yourself." });
return; return;

29
src/config/game.ts Normal file
View File

@@ -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;

View File

@@ -1,10 +1,7 @@
import { users, transactions, cooldowns } from "@/db/schema"; import { users, transactions, cooldowns } from "@/db/schema";
import { eq, sql, and } from "drizzle-orm"; import { eq, sql, and } from "drizzle-orm";
import { DrizzleClient } from "@/lib/DrizzleClient"; import { DrizzleClient } from "@/lib/DrizzleClient";
import { GameConfig } from "@/config/game";
const DAILY_REWARD_AMOUNT = 100n;
const STREAK_BONUS = 10n;
const DAILY_COOLDOWN = 24 * 60 * 60 * 1000; // 24 hours in ms
export const economyService = { export const economyService = {
transfer: async (fromUserId: string, toUserId: string, amount: bigint, tx?: any) => { transfer: async (fromUserId: string, toUserId: string, amount: bigint, tx?: any) => {
@@ -112,9 +109,9 @@ export const economyService = {
streak = 1; 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) // 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. // 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))); .where(eq(users.id, BigInt(userId)));
// Set new cooldown (now + 24h) // 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) await txFn.insert(cooldowns)
.values({ .values({

View File

@@ -1,8 +1,9 @@
import { inventory, items, users } from "@/db/schema"; 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 { DrizzleClient } from "@/lib/DrizzleClient";
import { economyService } from "@/modules/economy/economy.service"; import { economyService } from "@/modules/economy/economy.service";
import { GameConfig } from "@/config/game";
export const inventoryService = { export const inventoryService = {
addItem: async (userId: string, itemId: number, quantity: bigint = 1n, tx?: any) => { addItem: async (userId: string, itemId: number, quantity: bigint = 1n, tx?: any) => {
@@ -16,9 +17,14 @@ export const inventoryService = {
}); });
if (existing) { 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) const [entry] = await txFn.update(inventory)
.set({ .set({
quantity: sql`${inventory.quantity} + ${quantity}`, quantity: newQuantity,
}) })
.where(and( .where(and(
eq(inventory.userId, BigInt(userId)), eq(inventory.userId, BigInt(userId)),
@@ -27,6 +33,20 @@ export const inventoryService = {
.returning(); .returning();
return entry; return entry;
} else { } 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) const [entry] = await txFn.insert(inventory)
.values({ .values({
userId: BigInt(userId), 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. // 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. // Let's assume we update addItem next. For now, inline the add logic but cleaner.
const existingInv = await txFn.query.inventory.findFirst({ // Add Item using inner logic or self-call if we refactor properly.
where: and(eq(inventory.userId, BigInt(userId)), eq(inventory.itemId, itemId)), // Calling addItem directly within the same transaction scope:
}); await inventoryService.addItem(userId, itemId, quantity, txFn);
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 });
}
return { success: true, item, totalPrice }; return { success: true, item, totalPrice };
}; };

View File

@@ -1,18 +1,12 @@
import { users, cooldowns } from "@/db/schema"; import { users, cooldowns } from "@/db/schema";
import { eq, sql, and } from "drizzle-orm"; import { eq, sql, and } from "drizzle-orm";
import { DrizzleClient } from "@/lib/DrizzleClient"; import { DrizzleClient } from "@/lib/DrizzleClient";
import { GameConfig } from "@/config/game";
// 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;
export const levelingService = { export const levelingService = {
// Calculate XP required for a specific level // Calculate XP required for a specific level
getXpForLevel: (level: number) => { 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 // Pure XP addition - No cooldown checks
@@ -77,13 +71,13 @@ export const levelingService = {
} }
// Calculate random XP // 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 // Add XP
const result = await levelingService.addXp(id, amount, txFn); const result = await levelingService.addXp(id, amount, txFn);
// Update/Set Cooldown // 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) await txFn.insert(cooldowns)
.values({ .values({