import path from 'path'; import { existsSync, mkdirSync, readFileSync } from 'fs'; export type EngineName = 'yaml' | 'ink' | 'zork' | string; export interface GameMetadata { title: string; author?: string; subtitle?: string; version?: string; copyright?: string; language?: string; } export interface GamePaths { mainGameFile: string; inkSource?: string; inkCompiled?: string; promptDir?: string; music?: string; sfx?: string; images?: string; [key: string]: string | undefined; } export interface GameEngineConfig { engine: EngineName; locale: 'en_US' | 'de_DE' | string; paths: GamePaths; metadata: GameMetadata; } const PROJECT_ROOT = path.resolve(__dirname, '../..'); function fallbackConfig(engine: EngineName): GameEngineConfig { return { engine, locale: 'en_US', paths: { mainGameFile: engine === 'ink' ? 'data/ink/story.ink.json' : engine === 'zork' ? 'data/z-code/zork1.bin' : 'data/worlds/example_world.yml', music: 'public/music', sfx: 'public/sounds', images: 'public/images', }, metadata: { title: 'AI Interactive Fiction', author: 'Generative AI', subtitle: 'An open-world text adventure', version: '1.0.0', copyright: '', language: 'en_US', }, }; } export function projectPath(relativeOrAbsolutePath: string): string { return path.isAbsolute(relativeOrAbsolutePath) ? relativeOrAbsolutePath : path.resolve(PROJECT_ROOT, relativeOrAbsolutePath); } export function loadGameConfig(configPath: string, engine: EngineName): GameEngineConfig { const absolutePath = projectPath(configPath); if (!existsSync(absolutePath)) { console.warn(`[config] Missing ${absolutePath}; using ${engine} defaults.`); return fallbackConfig(engine); } const parsed = JSON.parse(readFileSync(absolutePath, 'utf8')) as Partial; const fallback = fallbackConfig(engine); return { engine: parsed.engine ?? fallback.engine, locale: parsed.locale ?? fallback.locale, paths: { ...fallback.paths, ...(parsed.paths ?? {}), }, metadata: { ...fallback.metadata, ...(parsed.metadata ?? {}), language: parsed.metadata?.language ?? parsed.locale ?? fallback.metadata.language, }, }; } export function ensureConfiguredAssetDirectories(config: GameEngineConfig): void { const directories = [ config.paths.music, config.paths.sfx, config.paths.images, config.paths.inkSource ? path.dirname(config.paths.inkSource) : undefined, config.paths.inkCompiled ? path.dirname(config.paths.inkCompiled) : undefined, config.paths.mainGameFile ? path.dirname(config.paths.mainGameFile) : undefined, config.paths.promptDir, ]; for (const directory of directories) { if (!directory) continue; const absolutePath = projectPath(directory); if (!existsSync(absolutePath)) { mkdirSync(absolutePath, { recursive: true }); } } } export function clientGameConfig(config: GameEngineConfig) { return { engine: config.engine, locale: config.locale, metadata: config.metadata, assets: { music: '/music/', sfx: '/sounds/', sounds: '/sounds/', images: '/images/', }, }; }