/** * Gamemaster API Server * Provides endpoints for accessing processed gamemaster data * Allows other apps to fetch unmodified and modified pokemon data */ import express from 'express'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const router = express.Router(); const DATA_DIR = path.join(__dirname, 'data', 'gamemaster'); /** * Ensure data directory exists */ function ensureDataDir() { if (!fs.existsSync(DATA_DIR)) { fs.mkdirSync(DATA_DIR, { recursive: true }); } } /** * Get path to a gamemaster file * @param {string} filename * @returns {string} Full file path */ function getFilePath(filename) { return path.join(DATA_DIR, filename); } /** * Check if a file exists * @param {string} filename * @returns {boolean} */ function fileExists(filename) { return fs.existsSync(getFilePath(filename)); } /** * Get file metadata (size, modified time) * @param {string} filename * @returns {Object|null} File info or null if doesn't exist */ function getFileInfo(filename) { const filepath = getFilePath(filename); if (!fs.existsSync(filepath)) return null; const stats = fs.statSync(filepath); return { filename, size: stats.size, sizeKb: (stats.size / 1024).toFixed(2), modified: stats.mtime.toISOString() }; } /** * Save gamemaster data to file * @param {string} filename * @param {Object|Array} data */ function saveFile(filename, data) { ensureDataDir(); const filepath = getFilePath(filename); fs.writeFileSync(filepath, JSON.stringify(data, null, 2)); return getFileInfo(filename); } /** * Load gamemaster data from file * @param {string} filename * @returns {Object|Array|null} */ function loadFile(filename) { const filepath = getFilePath(filename); if (!fs.existsSync(filepath)) return null; const content = fs.readFileSync(filepath, 'utf-8'); return JSON.parse(content); } // ============================================================================ // ROUTES // ============================================================================ /** * GET /api/gamemaster/status * Get status of available gamemaster files */ router.get('/status', (req, res) => { ensureDataDir(); const files = { pokemon: getFileInfo('pokemon.json'), pokemonAllForms: getFileInfo('pokemon-allFormsCostumes.json'), moves: getFileInfo('pokemon-moves.json'), raw: getFileInfo('latest-raw.json') }; res.json({ available: Object.values(files).filter(f => f !== null), lastUpdate: files.pokemon?.modified || 'Never', totalFiles: Object.values(files).filter(f => f !== null).length }); }); /** * GET /api/gamemaster/pokemon * Get filtered pokemon data (base forms + regional variants) */ router.get('/pokemon', (req, res) => { const data = loadFile('pokemon.json'); if (!data) { return res.status(404).json({ error: 'Pokemon data not available. Generate from GamemasterManager.' }); } res.json(data); }); /** * GET /api/gamemaster/pokemon/allForms * Get all pokemon forms including costumes */ router.get('/pokemon/allForms', (req, res) => { const data = loadFile('pokemon-allFormsCostumes.json'); if (!data) { return res.status(404).json({ error: 'All forms data not available. Generate from GamemasterManager.' }); } res.json(data); }); /** * GET /api/gamemaster/moves * Get all pokemon moves */ router.get('/moves', (req, res) => { const data = loadFile('pokemon-moves.json'); if (!data) { return res.status(404).json({ error: 'Moves data not available. Generate from GamemasterManager.' }); } res.json(data); }); /** * GET /api/gamemaster/raw * Get raw unmodified gamemaster data */ router.get('/raw', (req, res) => { const data = loadFile('latest-raw.json'); if (!data) { return res.status(404).json({ error: 'Raw gamemaster data not available. Fetch from GamemasterManager.' }); } res.json(data); }); /** * POST /api/gamemaster/process * Fetch and process gamemaster data on the server * This avoids sending large payloads from the frontend */ router.post('/process', async (req, res) => { try { const POKEMINERS_URL = 'https://raw.githubusercontent.com/PokeMiners/game_masters/master/latest/latest.json'; console.log('🔄 Fetching gamemaster from PokeMiners...'); const response = await fetch(POKEMINERS_URL); if (!response.ok) { throw new Error(`Failed to fetch: ${response.statusText}`); } const raw = await response.json(); console.log(`✅ Fetched ${raw.length} items from PokeMiners`); // Break up the data server-side console.log('⚙️ Processing gamemaster data...'); const processed = breakUpGamemaster(raw); // Save all files console.log('💾 Saving files to disk...'); const results = { pokemon: saveFile('pokemon.json', processed.pokemon), pokemonAllForms: saveFile( 'pokemon-allFormsCostumes.json', processed.pokemonAllForms ), moves: saveFile('pokemon-moves.json', processed.moves), raw: saveFile('latest-raw.json', raw) }; console.log('✅ All files saved successfully'); res.json({ message: 'Gamemaster processed and saved successfully', files: results, stats: { pokemon: processed.pokemon.length, pokemonAllForms: processed.pokemonAllForms.length, moves: processed.moves.length, raw: raw.length }, timestamp: new Date().toISOString() }); } catch (error) { console.error('Error processing gamemaster:', error); res.status(500).json({ error: 'Failed to process gamemaster', details: error.message }); } }); /** * POST /api/gamemaster/save * Save processed gamemaster data (called by GamemasterManager) * Body: {pokemon, pokemonAllForms, moves, raw} */ router.post('/save', express.json({ limit: '50mb' }), (req, res) => { try { const { pokemon, pokemonAllForms, moves, raw } = req.body; if (!pokemon || !pokemonAllForms || !moves) { return res.status(400).json({ error: 'Missing required data: pokemon, pokemonAllForms, moves' }); } const results = {}; if (pokemon) { results.pokemon = saveFile('pokemon.json', pokemon); } if (pokemonAllForms) { results.pokemonAllForms = saveFile( 'pokemon-allFormsCostumes.json', pokemonAllForms ); } if (moves) { results.moves = saveFile('pokemon-moves.json', moves); } if (raw) { results.raw = saveFile('latest-raw.json', raw); } res.json({ message: 'Files saved successfully', files: results, timestamp: new Date().toISOString() }); } catch (error) { console.error('Error saving gamemaster data:', error); res.status(500).json({ error: 'Failed to save files', details: error.message }); } }); /** * GET /api/gamemaster/download/:filename * Download a specific gamemaster file */ router.get('/download/:filename', (req, res) => { const filename = req.params.filename; // Validate filename to prevent path traversal const allowedFiles = [ 'pokemon.json', 'pokemon-allFormsCostumes.json', 'pokemon-moves.json', 'latest-raw.json' ]; if (!allowedFiles.includes(filename)) { return res.status(400).json({ error: 'Invalid filename' }); } const filepath = getFilePath(filename); if (!fs.existsSync(filepath)) { return res.status(404).json({ error: 'File not found' }); } res.setHeader('Content-Disposition', `attachment; filename="${filename}"`); res.setHeader('Content-Type', 'application/json'); res.sendFile(filepath); }); // ============================================================================ // INITIALIZATION // ============================================================================ ensureDataDir(); export default router;