import { eq } from "drizzle-orm"; import { gameSettings } from "@db/schema"; import { DrizzleClient } from "@shared/db/DrizzleClient"; import { SystemError } from "@shared/lib/errors"; import type { LevelingConfig, EconomyConfig, InventoryConfig, LootdropConfig, TriviaConfig, ModerationConfig, QuestConfig, } from "@db/schema/game-settings"; export type GameSettingsData = { leveling: LevelingConfig; economy: EconomyConfig; inventory: InventoryConfig; lootdrop: LootdropConfig; trivia: TriviaConfig; moderation: ModerationConfig; quest: QuestConfig; commands: Record; system: Record; }; let cachedSettings: GameSettingsData | null = null; let cacheTimestamp = 0; const CACHE_TTL_MS = 30000; export const gameSettingsService = { /** Retrieve game settings, using a 30-second TTL cache by default. */ getSettings: async (useCache = true): Promise => { if (useCache && cachedSettings && Date.now() - cacheTimestamp < CACHE_TTL_MS) { return cachedSettings; } const settings = await DrizzleClient.query.gameSettings.findFirst({ where: eq(gameSettings.id, "default"), }); if (!settings) return null; cachedSettings = { leveling: settings.leveling, economy: settings.economy, inventory: settings.inventory, lootdrop: settings.lootdrop, trivia: settings.trivia, moderation: settings.moderation, quest: settings.quest, commands: settings.commands ?? {}, system: settings.system ?? {}, }; cacheTimestamp = Date.now(); return cachedSettings; }, /** Create or update game settings, merging with existing values and invalidating cache. */ upsertSettings: async (data: Partial) => { const existing = await gameSettingsService.getSettings(false); const values: typeof gameSettings.$inferInsert = { id: "default", leveling: data.leveling ?? existing?.leveling ?? gameSettingsService.getDefaultLeveling(), economy: data.economy ?? existing?.economy ?? gameSettingsService.getDefaultEconomy(), inventory: data.inventory ?? existing?.inventory ?? gameSettingsService.getDefaultInventory(), lootdrop: data.lootdrop ?? existing?.lootdrop ?? gameSettingsService.getDefaultLootdrop(), trivia: data.trivia ?? existing?.trivia ?? gameSettingsService.getDefaultTrivia(), moderation: data.moderation ?? existing?.moderation ?? gameSettingsService.getDefaultModeration(), quest: data.quest ?? existing?.quest ?? gameSettingsService.getDefaultQuest(), commands: data.commands ?? existing?.commands ?? {}, system: data.system ?? existing?.system ?? {}, updatedAt: new Date(), }; const [result] = await DrizzleClient.insert(gameSettings) .values(values) .onConflictDoUpdate({ target: gameSettings.id, set: values, }) .returning(); gameSettingsService.invalidateCache(); return result; }, /** Update a single configuration section (e.g., "leveling", "economy") and invalidate cache. */ updateSection: async ( section: K, value: GameSettingsData[K] ) => { const existing = await gameSettingsService.getSettings(false); if (!existing) { throw new SystemError("Game settings not found. Initialize settings first."); } const updates: Partial = { [section]: value }; await gameSettingsService.upsertSettings(updates); gameSettingsService.invalidateCache(); }, /** Enable or disable a specific command in the game settings. */ toggleCommand: async (commandName: string, enabled: boolean) => { const settings = await gameSettingsService.getSettings(false); if (!settings) { throw new SystemError("Game settings not found. Initialize settings first."); } const commands = { ...settings.commands, [commandName]: enabled, }; await gameSettingsService.updateSection("commands", commands); }, /** Invalidate the in-memory settings cache, forcing a fresh DB read on next access. */ invalidateCache: () => { cachedSettings = null; cacheTimestamp = 0; }, /** Return default leveling configuration values. */ getDefaultLeveling: (): LevelingConfig => ({ base: 100, exponent: 1.5, chat: { cooldownMs: 60000, minXp: 5, maxXp: 15, }, }), /** Return default economy configuration values. */ getDefaultEconomy: (): EconomyConfig => ({ daily: { amount: "100", streakBonus: "10", weeklyBonus: "50", cooldownMs: 86400000, }, transfers: { allowSelfTransfer: false, minAmount: "1", }, exam: { multMin: 1.0, multMax: 2.0, }, }), /** Return default inventory configuration values. */ getDefaultInventory: (): InventoryConfig => ({ maxStackSize: "99", maxSlots: 20, }), /** Return default lootdrop configuration values. */ getDefaultLootdrop: (): LootdropConfig => ({ activityWindowMs: 300000, minMessages: 5, spawnChance: 0.1, cooldownMs: 60000, reward: { min: 10, max: 50, currency: "AU", }, }), /** Return default trivia configuration values. */ getDefaultTrivia: (): TriviaConfig => ({ entryFee: "50", rewardMultiplier: 1.8, timeoutSeconds: 30, cooldownMs: 60000, categories: [], difficulty: "random", }), /** Return default moderation configuration values. */ getDefaultModeration: (): ModerationConfig => ({ prune: { maxAmount: 100, confirmThreshold: 50, batchSize: 100, batchDelayMs: 1000, }, }), /** Return default quest configuration values. */ getDefaultQuest: (): QuestConfig => ({ maxActiveQuests: 3, }), /** Return the complete set of default game settings across all sections. */ getDefaults: (): GameSettingsData => ({ leveling: gameSettingsService.getDefaultLeveling(), economy: gameSettingsService.getDefaultEconomy(), inventory: gameSettingsService.getDefaultInventory(), lootdrop: gameSettingsService.getDefaultLootdrop(), trivia: gameSettingsService.getDefaultTrivia(), moderation: gameSettingsService.getDefaultModeration(), quest: gameSettingsService.getDefaultQuest(), commands: {}, system: {}, }), };