feat: implement database-backed game settings with a new schema, service, and migration script.
Some checks failed
Deploy to Production / test (push) Failing after 26s
Some checks failed
Deploy to Production / test (push) Failing after 26s
This commit is contained in:
@@ -19,6 +19,8 @@
|
||||
"dev": "bun --watch bot/index.ts",
|
||||
"db:studio": "drizzle-kit studio --port 4983 --host 0.0.0.0",
|
||||
"db:migrate-config": "docker compose run --rm app bun shared/scripts/migrate-config-to-db.ts",
|
||||
"db:migrate-game-config": "docker compose run --rm app bun shared/scripts/migrate-game-settings-to-db.ts",
|
||||
"db:migrate-all": "docker compose run --rm app sh -c 'bun shared/scripts/migrate-config-to-db.ts && bun shared/scripts/migrate-game-settings-to-db.ts'",
|
||||
"remote": "bash shared/scripts/remote.sh",
|
||||
"logs": "bash shared/scripts/logs.sh",
|
||||
"db:backup": "bash shared/scripts/db-backup.sh",
|
||||
|
||||
88
shared/db/schema/game-settings.ts
Normal file
88
shared/db/schema/game-settings.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import {
|
||||
pgTable,
|
||||
text,
|
||||
timestamp,
|
||||
jsonb,
|
||||
} from 'drizzle-orm/pg-core';
|
||||
import { relations, type InferSelectModel, type InferInsertModel } from 'drizzle-orm';
|
||||
|
||||
export type GameSettings = InferSelectModel<typeof gameSettings>;
|
||||
export type GameSettingsInsert = InferInsertModel<typeof gameSettings>;
|
||||
|
||||
export interface LevelingConfig {
|
||||
base: number;
|
||||
exponent: number;
|
||||
chat: {
|
||||
cooldownMs: number;
|
||||
minXp: number;
|
||||
maxXp: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface EconomyConfig {
|
||||
daily: {
|
||||
amount: string;
|
||||
streakBonus: string;
|
||||
weeklyBonus: string;
|
||||
cooldownMs: number;
|
||||
};
|
||||
transfers: {
|
||||
allowSelfTransfer: boolean;
|
||||
minAmount: string;
|
||||
};
|
||||
exam: {
|
||||
multMin: number;
|
||||
multMax: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface InventoryConfig {
|
||||
maxStackSize: string;
|
||||
maxSlots: number;
|
||||
}
|
||||
|
||||
export interface LootdropConfig {
|
||||
activityWindowMs: number;
|
||||
minMessages: number;
|
||||
spawnChance: number;
|
||||
cooldownMs: number;
|
||||
reward: {
|
||||
min: number;
|
||||
max: number;
|
||||
currency: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface TriviaConfig {
|
||||
entryFee: string;
|
||||
rewardMultiplier: number;
|
||||
timeoutSeconds: number;
|
||||
cooldownMs: number;
|
||||
categories: number[];
|
||||
difficulty: 'easy' | 'medium' | 'hard' | 'random';
|
||||
}
|
||||
|
||||
export interface ModerationConfig {
|
||||
prune: {
|
||||
maxAmount: number;
|
||||
confirmThreshold: number;
|
||||
batchSize: number;
|
||||
batchDelayMs: number;
|
||||
};
|
||||
}
|
||||
|
||||
export const gameSettings = pgTable('game_settings', {
|
||||
id: text('id').primaryKey().default('default'),
|
||||
leveling: jsonb('leveling').$type<LevelingConfig>().notNull(),
|
||||
economy: jsonb('economy').$type<EconomyConfig>().notNull(),
|
||||
inventory: jsonb('inventory').$type<InventoryConfig>().notNull(),
|
||||
lootdrop: jsonb('lootdrop').$type<LootdropConfig>().notNull(),
|
||||
trivia: jsonb('trivia').$type<TriviaConfig>().notNull(),
|
||||
moderation: jsonb('moderation').$type<ModerationConfig>().notNull(),
|
||||
commands: jsonb('commands').$type<Record<string, boolean>>().default({}),
|
||||
system: jsonb('system').$type<Record<string, unknown>>().default({}),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
|
||||
export const gameSettingsRelations = relations(gameSettings, () => ({}));
|
||||
@@ -6,3 +6,4 @@ export * from './quests';
|
||||
export * from './moderation';
|
||||
export * from './feature-flags';
|
||||
export * from './guild-settings';
|
||||
export * from './game-settings';
|
||||
|
||||
@@ -66,66 +66,98 @@ export async function getGuildConfig(guildId: string): Promise<GuildConfig> {
|
||||
console.error("Failed to load guild config from database:", error);
|
||||
}
|
||||
|
||||
const fileConfig: GuildConfig = {
|
||||
studentRole: config.studentRole,
|
||||
visitorRole: config.visitorRole,
|
||||
colorRoles: config.colorRoles,
|
||||
welcomeChannelId: config.welcomeChannelId,
|
||||
welcomeMessage: config.welcomeMessage,
|
||||
feedbackChannelId: config.feedbackChannelId,
|
||||
terminal: config.terminal,
|
||||
moderation: config.moderation,
|
||||
return {
|
||||
studentRole: undefined,
|
||||
visitorRole: undefined,
|
||||
colorRoles: [],
|
||||
welcomeChannelId: undefined,
|
||||
welcomeMessage: undefined,
|
||||
feedbackChannelId: undefined,
|
||||
terminal: undefined,
|
||||
moderation: { cases: { dmOnWarn: true } },
|
||||
};
|
||||
|
||||
return fileConfig;
|
||||
}
|
||||
|
||||
export function invalidateGuildConfigCache(guildId: string) {
|
||||
guildConfigCache.delete(guildId);
|
||||
}
|
||||
|
||||
export interface GameConfigType {
|
||||
leveling: {
|
||||
base: number;
|
||||
exponent: number;
|
||||
chat: {
|
||||
cooldownMs: number;
|
||||
minXp: number;
|
||||
maxXp: number;
|
||||
}
|
||||
},
|
||||
economy: {
|
||||
daily: {
|
||||
amount: bigint;
|
||||
streakBonus: bigint;
|
||||
weeklyBonus: bigint;
|
||||
cooldownMs: number;
|
||||
},
|
||||
transfers: {
|
||||
allowSelfTransfer: boolean;
|
||||
minAmount: bigint;
|
||||
},
|
||||
exam: {
|
||||
multMin: number;
|
||||
multMax: number;
|
||||
}
|
||||
},
|
||||
inventory: {
|
||||
maxStackSize: bigint;
|
||||
maxSlots: number;
|
||||
},
|
||||
commands: Record<string, boolean>;
|
||||
lootdrop: {
|
||||
activityWindowMs: number;
|
||||
minMessages: number;
|
||||
spawnChance: number;
|
||||
export interface LevelingConfig {
|
||||
base: number;
|
||||
exponent: number;
|
||||
chat: {
|
||||
cooldownMs: number;
|
||||
reward: {
|
||||
min: number;
|
||||
max: number;
|
||||
currency: string;
|
||||
}
|
||||
minXp: number;
|
||||
maxXp: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface EconomyConfig {
|
||||
daily: {
|
||||
amount: bigint;
|
||||
streakBonus: bigint;
|
||||
weeklyBonus: bigint;
|
||||
cooldownMs: number;
|
||||
};
|
||||
transfers: {
|
||||
allowSelfTransfer: boolean;
|
||||
minAmount: bigint;
|
||||
};
|
||||
exam: {
|
||||
multMin: number;
|
||||
multMax: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface InventoryConfig {
|
||||
maxStackSize: bigint;
|
||||
maxSlots: number;
|
||||
}
|
||||
|
||||
export interface LootdropConfig {
|
||||
activityWindowMs: number;
|
||||
minMessages: number;
|
||||
spawnChance: number;
|
||||
cooldownMs: number;
|
||||
reward: {
|
||||
min: number;
|
||||
max: number;
|
||||
currency: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface TriviaConfig {
|
||||
entryFee: bigint;
|
||||
rewardMultiplier: number;
|
||||
timeoutSeconds: number;
|
||||
cooldownMs: number;
|
||||
categories: number[];
|
||||
difficulty: 'easy' | 'medium' | 'hard' | 'random';
|
||||
}
|
||||
|
||||
export interface ModerationConfig {
|
||||
prune: {
|
||||
maxAmount: number;
|
||||
confirmThreshold: number;
|
||||
batchSize: number;
|
||||
batchDelayMs: number;
|
||||
};
|
||||
cases: {
|
||||
dmOnWarn: boolean;
|
||||
logChannelId?: string;
|
||||
autoTimeoutThreshold?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GameConfigType {
|
||||
leveling: LevelingConfig;
|
||||
economy: EconomyConfig;
|
||||
inventory: InventoryConfig;
|
||||
commands: Record<string, boolean>;
|
||||
lootdrop: LootdropConfig;
|
||||
trivia: TriviaConfig;
|
||||
moderation: ModerationConfig;
|
||||
system: Record<string, unknown>;
|
||||
studentRole: string;
|
||||
visitorRole: string;
|
||||
colorRoles: string[];
|
||||
@@ -136,31 +168,8 @@ export interface GameConfigType {
|
||||
channelId: string;
|
||||
messageId: string;
|
||||
};
|
||||
moderation: {
|
||||
prune: {
|
||||
maxAmount: number;
|
||||
confirmThreshold: number;
|
||||
batchSize: number;
|
||||
batchDelayMs: number;
|
||||
};
|
||||
cases: {
|
||||
dmOnWarn: boolean;
|
||||
logChannelId?: string;
|
||||
autoTimeoutThreshold?: number;
|
||||
};
|
||||
};
|
||||
trivia: {
|
||||
entryFee: bigint;
|
||||
rewardMultiplier: number;
|
||||
timeoutSeconds: number;
|
||||
cooldownMs: number;
|
||||
categories: number[];
|
||||
difficulty: 'easy' | 'medium' | 'hard' | 'random';
|
||||
};
|
||||
system: Record<string, any>;
|
||||
}
|
||||
|
||||
// Initial default config state
|
||||
export const config: GameConfigType = {} as GameConfigType;
|
||||
|
||||
const bigIntSchema = z.union([z.string(), z.number(), z.bigint()])
|
||||
@@ -174,7 +183,7 @@ const bigIntSchema = z.union([z.string(), z.number(), z.bigint()])
|
||||
}, { message: "Must be a valid integer" })
|
||||
.transform((val) => BigInt(val));
|
||||
|
||||
const configSchema = z.object({
|
||||
const fileConfigSchema = z.object({
|
||||
leveling: z.object({
|
||||
base: z.number(),
|
||||
exponent: z.number(),
|
||||
@@ -215,7 +224,6 @@ const configSchema = z.object({
|
||||
max: z.number(),
|
||||
currency: z.string(),
|
||||
})
|
||||
|
||||
}),
|
||||
studentRole: z.string(),
|
||||
visitorRole: z.string(),
|
||||
@@ -268,44 +276,170 @@ const configSchema = z.object({
|
||||
system: z.record(z.string(), z.any()).default({}),
|
||||
});
|
||||
|
||||
export function reloadConfig() {
|
||||
type FileConfig = z.infer<typeof fileConfigSchema>;
|
||||
|
||||
function loadFromFile(): FileConfig | null {
|
||||
if (!existsSync(configPath)) {
|
||||
throw new Error(`Config file not found at ${configPath}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const raw = readFileSync(configPath, 'utf-8');
|
||||
const rawConfig = JSON.parse(raw);
|
||||
|
||||
// Update config object in place
|
||||
// We use Object.assign to keep the reference to the exported 'config' object same
|
||||
const validatedConfig = configSchema.parse(rawConfig);
|
||||
Object.assign(config, validatedConfig);
|
||||
|
||||
console.log("🔄 Config reloaded from disk.");
|
||||
try {
|
||||
const raw = readFileSync(configPath, 'utf-8');
|
||||
const rawConfig = JSON.parse(raw);
|
||||
return fileConfigSchema.parse(rawConfig);
|
||||
} catch (error) {
|
||||
console.error("Failed to load config from file:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Initial load
|
||||
reloadConfig();
|
||||
function applyFileConfig(fileConfig: FileConfig) {
|
||||
Object.assign(config, {
|
||||
leveling: fileConfig.leveling,
|
||||
economy: fileConfig.economy,
|
||||
inventory: fileConfig.inventory,
|
||||
commands: fileConfig.commands,
|
||||
lootdrop: fileConfig.lootdrop,
|
||||
trivia: fileConfig.trivia,
|
||||
moderation: fileConfig.moderation,
|
||||
system: fileConfig.system,
|
||||
studentRole: fileConfig.studentRole,
|
||||
visitorRole: fileConfig.visitorRole,
|
||||
colorRoles: fileConfig.colorRoles,
|
||||
welcomeChannelId: fileConfig.welcomeChannelId,
|
||||
welcomeMessage: fileConfig.welcomeMessage,
|
||||
feedbackChannelId: fileConfig.feedbackChannelId,
|
||||
terminal: fileConfig.terminal,
|
||||
});
|
||||
}
|
||||
|
||||
async function loadFromDatabase(): Promise<boolean> {
|
||||
try {
|
||||
const { gameSettingsService } = await import('@shared/modules/game-settings/game-settings.service');
|
||||
const dbSettings = await gameSettingsService.getSettings();
|
||||
|
||||
if (dbSettings) {
|
||||
Object.assign(config, {
|
||||
leveling: {
|
||||
...dbSettings.leveling,
|
||||
},
|
||||
economy: {
|
||||
daily: {
|
||||
...dbSettings.economy.daily,
|
||||
amount: BigInt(dbSettings.economy.daily.amount),
|
||||
streakBonus: BigInt(dbSettings.economy.daily.streakBonus),
|
||||
weeklyBonus: BigInt(dbSettings.economy.daily.weeklyBonus),
|
||||
},
|
||||
transfers: {
|
||||
...dbSettings.economy.transfers,
|
||||
minAmount: BigInt(dbSettings.economy.transfers.minAmount),
|
||||
},
|
||||
exam: dbSettings.economy.exam,
|
||||
},
|
||||
inventory: {
|
||||
...dbSettings.inventory,
|
||||
maxStackSize: BigInt(dbSettings.inventory.maxStackSize),
|
||||
},
|
||||
commands: dbSettings.commands,
|
||||
lootdrop: dbSettings.lootdrop,
|
||||
trivia: {
|
||||
...dbSettings.trivia,
|
||||
entryFee: BigInt(dbSettings.trivia.entryFee),
|
||||
},
|
||||
moderation: dbSettings.moderation,
|
||||
system: dbSettings.system,
|
||||
});
|
||||
console.log("🎮 Game config loaded from database.");
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load game config from database:", error);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function reloadConfig(): Promise<void> {
|
||||
const dbLoaded = await loadFromDatabase();
|
||||
|
||||
if (!dbLoaded) {
|
||||
const fileConfig = loadFromFile();
|
||||
if (fileConfig) {
|
||||
applyFileConfig(fileConfig);
|
||||
console.log("📄 Game config loaded from file (database not available).");
|
||||
} else {
|
||||
console.warn("⚠️ No game config found in database or file. Using defaults.");
|
||||
const { gameSettingsService } = await import('@shared/modules/game-settings/game-settings.service');
|
||||
const defaults = gameSettingsService.getDefaults();
|
||||
Object.assign(config, {
|
||||
leveling: defaults.leveling,
|
||||
economy: {
|
||||
...defaults.economy,
|
||||
daily: {
|
||||
...defaults.economy.daily,
|
||||
amount: BigInt(defaults.economy.daily.amount),
|
||||
streakBonus: BigInt(defaults.economy.daily.streakBonus),
|
||||
weeklyBonus: BigInt(defaults.economy.daily.weeklyBonus),
|
||||
},
|
||||
transfers: {
|
||||
...defaults.economy.transfers,
|
||||
minAmount: BigInt(defaults.economy.transfers.minAmount),
|
||||
},
|
||||
},
|
||||
inventory: {
|
||||
...defaults.inventory,
|
||||
maxStackSize: BigInt(defaults.inventory.maxStackSize),
|
||||
},
|
||||
commands: defaults.commands,
|
||||
lootdrop: defaults.lootdrop,
|
||||
trivia: {
|
||||
...defaults.trivia,
|
||||
entryFee: BigInt(defaults.trivia.entryFee),
|
||||
},
|
||||
moderation: defaults.moderation,
|
||||
system: defaults.system,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function loadFileSync(): void {
|
||||
const fileConfig = loadFromFile();
|
||||
if (fileConfig) {
|
||||
applyFileConfig(fileConfig);
|
||||
console.log("📄 Game config loaded from file (sync).");
|
||||
}
|
||||
}
|
||||
|
||||
// Backwards compatibility alias
|
||||
export const GameConfig = config;
|
||||
|
||||
export function saveConfig(newConfig: unknown) {
|
||||
// Validate and transform input
|
||||
const validatedConfig = configSchema.parse(newConfig);
|
||||
|
||||
const validatedConfig = fileConfigSchema.parse(newConfig);
|
||||
const jsonString = JSON.stringify(validatedConfig, jsonReplacer, 4);
|
||||
writeFileSync(configPath, jsonString, 'utf-8');
|
||||
reloadConfig();
|
||||
applyFileConfig(validatedConfig);
|
||||
console.log("🔄 Config saved to file.");
|
||||
}
|
||||
|
||||
export function toggleCommand(commandName: string, enabled: boolean) {
|
||||
const fileConfig = loadFromFile();
|
||||
if (!fileConfig) {
|
||||
console.error("Cannot toggle command: no file config available");
|
||||
return;
|
||||
}
|
||||
|
||||
const newConfig = {
|
||||
...config,
|
||||
...fileConfig,
|
||||
commands: {
|
||||
...config.commands,
|
||||
...fileConfig.commands,
|
||||
[commandName]: enabled
|
||||
}
|
||||
};
|
||||
saveConfig(newConfig);
|
||||
}
|
||||
|
||||
export async function initializeConfig(): Promise<void> {
|
||||
loadFileSync();
|
||||
await reloadConfig();
|
||||
}
|
||||
|
||||
loadFileSync();
|
||||
|
||||
192
shared/modules/game-settings/game-settings.service.ts
Normal file
192
shared/modules/game-settings/game-settings.service.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { gameSettings } from "@db/schema";
|
||||
import { DrizzleClient } from "@shared/db/DrizzleClient";
|
||||
import type {
|
||||
LevelingConfig,
|
||||
EconomyConfig,
|
||||
InventoryConfig,
|
||||
LootdropConfig,
|
||||
TriviaConfig,
|
||||
ModerationConfig,
|
||||
} from "@db/schema/game-settings";
|
||||
|
||||
export type GameSettingsData = {
|
||||
leveling: LevelingConfig;
|
||||
economy: EconomyConfig;
|
||||
inventory: InventoryConfig;
|
||||
lootdrop: LootdropConfig;
|
||||
trivia: TriviaConfig;
|
||||
moderation: ModerationConfig;
|
||||
commands: Record<string, boolean>;
|
||||
system: Record<string, unknown>;
|
||||
};
|
||||
|
||||
let cachedSettings: GameSettingsData | null = null;
|
||||
let cacheTimestamp = 0;
|
||||
const CACHE_TTL_MS = 30000;
|
||||
|
||||
export const gameSettingsService = {
|
||||
getSettings: async (useCache = true): Promise<GameSettingsData | null> => {
|
||||
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,
|
||||
commands: settings.commands ?? {},
|
||||
system: settings.system ?? {},
|
||||
};
|
||||
cacheTimestamp = Date.now();
|
||||
|
||||
return cachedSettings;
|
||||
},
|
||||
|
||||
upsertSettings: async (data: Partial<GameSettingsData>) => {
|
||||
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(),
|
||||
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;
|
||||
},
|
||||
|
||||
updateSection: async <K extends keyof GameSettingsData>(
|
||||
section: K,
|
||||
value: GameSettingsData[K]
|
||||
) => {
|
||||
const existing = await gameSettingsService.getSettings(false);
|
||||
|
||||
if (!existing) {
|
||||
throw new Error("Game settings not found. Initialize settings first.");
|
||||
}
|
||||
|
||||
const updates: Partial<GameSettingsData> = { [section]: value };
|
||||
await gameSettingsService.upsertSettings(updates);
|
||||
|
||||
gameSettingsService.invalidateCache();
|
||||
},
|
||||
|
||||
toggleCommand: async (commandName: string, enabled: boolean) => {
|
||||
const settings = await gameSettingsService.getSettings(false);
|
||||
|
||||
if (!settings) {
|
||||
throw new Error("Game settings not found. Initialize settings first.");
|
||||
}
|
||||
|
||||
const commands = {
|
||||
...settings.commands,
|
||||
[commandName]: enabled,
|
||||
};
|
||||
|
||||
await gameSettingsService.updateSection("commands", commands);
|
||||
},
|
||||
|
||||
invalidateCache: () => {
|
||||
cachedSettings = null;
|
||||
cacheTimestamp = 0;
|
||||
},
|
||||
|
||||
getDefaultLeveling: (): LevelingConfig => ({
|
||||
base: 100,
|
||||
exponent: 1.5,
|
||||
chat: {
|
||||
cooldownMs: 60000,
|
||||
minXp: 5,
|
||||
maxXp: 15,
|
||||
},
|
||||
}),
|
||||
|
||||
getDefaultEconomy: (): EconomyConfig => ({
|
||||
daily: {
|
||||
amount: "100",
|
||||
streakBonus: "10",
|
||||
weeklyBonus: "50",
|
||||
cooldownMs: 86400000,
|
||||
},
|
||||
transfers: {
|
||||
allowSelfTransfer: false,
|
||||
minAmount: "1",
|
||||
},
|
||||
exam: {
|
||||
multMin: 1.0,
|
||||
multMax: 2.0,
|
||||
},
|
||||
}),
|
||||
|
||||
getDefaultInventory: (): InventoryConfig => ({
|
||||
maxStackSize: "99",
|
||||
maxSlots: 20,
|
||||
}),
|
||||
|
||||
getDefaultLootdrop: (): LootdropConfig => ({
|
||||
activityWindowMs: 300000,
|
||||
minMessages: 5,
|
||||
spawnChance: 0.1,
|
||||
cooldownMs: 60000,
|
||||
reward: {
|
||||
min: 10,
|
||||
max: 50,
|
||||
currency: "AU",
|
||||
},
|
||||
}),
|
||||
|
||||
getDefaultTrivia: (): TriviaConfig => ({
|
||||
entryFee: "50",
|
||||
rewardMultiplier: 1.8,
|
||||
timeoutSeconds: 30,
|
||||
cooldownMs: 60000,
|
||||
categories: [],
|
||||
difficulty: "random",
|
||||
}),
|
||||
|
||||
getDefaultModeration: (): ModerationConfig => ({
|
||||
prune: {
|
||||
maxAmount: 100,
|
||||
confirmThreshold: 50,
|
||||
batchSize: 100,
|
||||
batchDelayMs: 1000,
|
||||
},
|
||||
}),
|
||||
|
||||
getDefaults: (): GameSettingsData => ({
|
||||
leveling: gameSettingsService.getDefaultLeveling(),
|
||||
economy: gameSettingsService.getDefaultEconomy(),
|
||||
inventory: gameSettingsService.getDefaultInventory(),
|
||||
lootdrop: gameSettingsService.getDefaultLootdrop(),
|
||||
trivia: gameSettingsService.getDefaultTrivia(),
|
||||
moderation: gameSettingsService.getDefaultModeration(),
|
||||
commands: {},
|
||||
system: {},
|
||||
}),
|
||||
};
|
||||
128
shared/scripts/migrate-game-settings-to-db.ts
Normal file
128
shared/scripts/migrate-game-settings-to-db.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { gameSettingsService } from "@shared/modules/game-settings/game-settings.service";
|
||||
import { readFileSync, existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
const configPath = join(import.meta.dir, '..', 'config', 'config.json');
|
||||
|
||||
interface FileConfig {
|
||||
leveling: {
|
||||
base: number;
|
||||
exponent: number;
|
||||
chat: {
|
||||
cooldownMs: number;
|
||||
minXp: number;
|
||||
maxXp: number;
|
||||
};
|
||||
};
|
||||
economy: {
|
||||
daily: {
|
||||
amount: string | number;
|
||||
streakBonus: string | number;
|
||||
weeklyBonus: string | number;
|
||||
cooldownMs: number;
|
||||
};
|
||||
transfers: {
|
||||
allowSelfTransfer: boolean;
|
||||
minAmount: string | number;
|
||||
};
|
||||
exam: {
|
||||
multMin: number;
|
||||
multMax: number;
|
||||
};
|
||||
};
|
||||
inventory: {
|
||||
maxStackSize: string | number;
|
||||
maxSlots: number;
|
||||
};
|
||||
lootdrop: {
|
||||
activityWindowMs: number;
|
||||
minMessages: number;
|
||||
spawnChance: number;
|
||||
cooldownMs: number;
|
||||
reward: {
|
||||
min: number;
|
||||
max: number;
|
||||
currency: string;
|
||||
};
|
||||
};
|
||||
trivia: {
|
||||
entryFee: string | number;
|
||||
rewardMultiplier: number;
|
||||
timeoutSeconds: number;
|
||||
cooldownMs: number;
|
||||
categories: number[];
|
||||
difficulty: 'easy' | 'medium' | 'hard' | 'random';
|
||||
};
|
||||
moderation: {
|
||||
prune: {
|
||||
maxAmount: number;
|
||||
confirmThreshold: number;
|
||||
batchSize: number;
|
||||
batchDelayMs: number;
|
||||
};
|
||||
};
|
||||
commands: Record<string, boolean>;
|
||||
system: Record<string, unknown>;
|
||||
}
|
||||
|
||||
async function migrateGameSettingsToDatabase() {
|
||||
console.log("🎮 Migrating game settings to database...\n");
|
||||
|
||||
const existing = await gameSettingsService.getSettings(false);
|
||||
if (existing) {
|
||||
console.log("Game settings already exist in database:");
|
||||
console.log(JSON.stringify(existing, null, 2));
|
||||
console.log("\nSkipping migration. Delete existing settings first if you want to re-migrate.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!existsSync(configPath)) {
|
||||
console.log("No config.json file found. Creating default settings...");
|
||||
await gameSettingsService.upsertSettings(gameSettingsService.getDefaults());
|
||||
console.log("✅ Default game settings created in database.");
|
||||
return;
|
||||
}
|
||||
|
||||
const raw = readFileSync(configPath, 'utf-8');
|
||||
const fileConfig: FileConfig = JSON.parse(raw);
|
||||
|
||||
await gameSettingsService.upsertSettings({
|
||||
leveling: fileConfig.leveling,
|
||||
economy: {
|
||||
daily: {
|
||||
amount: String(fileConfig.economy.daily.amount),
|
||||
streakBonus: String(fileConfig.economy.daily.streakBonus),
|
||||
weeklyBonus: String(fileConfig.economy.daily.weeklyBonus ?? 50),
|
||||
cooldownMs: fileConfig.economy.daily.cooldownMs,
|
||||
},
|
||||
transfers: {
|
||||
allowSelfTransfer: fileConfig.economy.transfers.allowSelfTransfer,
|
||||
minAmount: String(fileConfig.economy.transfers.minAmount),
|
||||
},
|
||||
exam: fileConfig.economy.exam,
|
||||
},
|
||||
inventory: {
|
||||
maxStackSize: String(fileConfig.inventory.maxStackSize),
|
||||
maxSlots: fileConfig.inventory.maxSlots,
|
||||
},
|
||||
lootdrop: fileConfig.lootdrop,
|
||||
trivia: {
|
||||
...fileConfig.trivia,
|
||||
entryFee: String(fileConfig.trivia.entryFee),
|
||||
},
|
||||
moderation: fileConfig.moderation,
|
||||
commands: fileConfig.commands ?? {},
|
||||
system: fileConfig.system ?? {},
|
||||
});
|
||||
|
||||
console.log("✅ Game settings migrated to database!");
|
||||
console.log("\nGame-wide settings are now stored in the database.");
|
||||
console.log("The config.json file can be safely deleted after verification.");
|
||||
}
|
||||
|
||||
migrateGameSettingsToDatabase()
|
||||
.then(() => process.exit(0))
|
||||
.catch((error) => {
|
||||
console.error("Migration failed:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user