Files
memory-infrastructure-palace/code/websites/pokedex.online/server/gamemaster-api.js

376 lines
9.5 KiB
JavaScript

/**
* 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';
import fetch from 'node-fetch';
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);
}
/**
* Break up gamemaster into separate categories
* @param {Array} gamemaster - Full gamemaster data
* @returns {Object} Separated data {pokemon, pokemonAllForms, moves}
*/
function breakUpGamemaster(gamemaster) {
const regionCheck = ['alola', 'galarian', 'hisuian', 'paldea'];
const result = gamemaster.reduce(
(acc, item) => {
const templateId = item.templateId;
// POKEMON FILTER
if (
templateId.startsWith('V') &&
templateId.toLowerCase().includes('pokemon')
) {
const pokemonSettings = item.data?.pokemonSettings;
const pokemonId = pokemonSettings?.pokemonId;
acc.pokemonAllForms.push(item);
const form = pokemonSettings?.form;
const isRegionalForm =
form && typeof form === 'string'
? regionCheck.includes(form.split('_')[1]?.toLowerCase())
: false;
if (
!acc.pokemonSeen.has(pokemonId) ||
(acc.pokemonSeen.has(pokemonId) && isRegionalForm)
) {
acc.pokemonSeen.add(pokemonId);
acc.pokemon.push(item);
}
}
// POKEMON MOVE FILTER
if (
templateId.startsWith('V') &&
templateId.toLowerCase().includes('move')
) {
const moveSettings = item.data?.moveSettings;
const moveId = moveSettings?.movementId;
if (!acc.moveSeen.has(moveId)) {
acc.moveSeen.add(moveId);
acc.moves.push(item);
}
}
return acc;
},
{
pokemon: [],
pokemonAllForms: [],
moves: [],
pokemonSeen: new Set(),
moveSeen: new Set()
}
);
delete result.pokemonSeen;
delete result.moveSeen;
return result;
}
// ============================================================================
// 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;