import fs from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const LEGACY_MAP_ROOT = path.resolve(__dirname, '..', 'legacy', 'hwe', 'scenario', 'map'); const LEGACY_BASE_FILE = path.resolve(__dirname, '..', 'legacy', 'hwe', 'sammo', 'CityConstBase.php'); const OUTPUT_ROOT = path.resolve(__dirname, '..', 'app', 'game-engine', 'resources', 'map'); const MAP_NAMES = ['che', 'miniche', 'miniche_b', 'miniche_clean', 'cr', 'chess', 'ludo_rathowm', 'pokemon_v1']; const LEVEL_MAP = { 수: 1, 진: 2, 관: 3, 이: 4, 소: 5, 중: 6, 대: 7, 특: 8, }; const LEVEL_LABELS = Object.entries(LEVEL_MAP).reduce((acc, [label, value]) => { acc[value] = label; return acc; }, {}); const DEFAULT_REGION_MAP = { 하북: 1, 중원: 2, 서북: 3, 서촉: 4, 남중: 5, 초: 6, 오월: 7, 동이: 8, }; const BUILD_INIT_COMMON = { trust: 50, trade: 100, }; const BUILD_INIT = { 수: { population: 5000, agriculture: 100, commerce: 100, security: 100, defence: 500, wall: 500, }, 진: { population: 5000, agriculture: 100, commerce: 100, security: 100, defence: 500, wall: 500, }, 관: { population: 10000, agriculture: 100, commerce: 100, security: 100, defence: 1000, wall: 1000, }, 이: { population: 50000, agriculture: 1000, commerce: 1000, security: 1000, defence: 1000, wall: 1000, }, 소: { population: 100000, agriculture: 1000, commerce: 1000, security: 1000, defence: 2000, wall: 2000, }, 중: { population: 100000, agriculture: 1000, commerce: 1000, security: 1000, defence: 3000, wall: 3000, }, 대: { population: 150000, agriculture: 1000, commerce: 1000, security: 1000, defence: 4000, wall: 4000, }, 특: { population: 150000, agriculture: 1000, commerce: 1000, security: 1000, defence: 5000, wall: 5000, }, }; const DEFAULT_SUPPLY_STATE = 1; const DEFAULT_FRONT_STATE = 0; const readFileOrNull = async (filePath) => { try { return await fs.readFile(filePath, 'utf8'); } catch { return null; } }; const extractPhpArray = (source, marker) => { const markerIndex = source.indexOf(marker); if (markerIndex < 0) { return null; } const start = source.indexOf('[', markerIndex); if (start < 0) { return null; } let depth = 0; let inString = null; for (let i = start; i < source.length; i += 1) { const char = source[i]; if (inString) { if (char === '\\') { i += 1; continue; } if (char === inString) { inString = null; } continue; } if (char === '"' || char === "'") { inString = char; continue; } if (char === '[') { depth += 1; continue; } if (char === ']') { depth -= 1; if (depth === 0) { return source.slice(start, i + 1); } } } return null; }; const stripPhpComments = (source) => source .replace(/\/\*[\s\S]*?\*\//g, '') .replace(/\/\/.*$/gm, '') .replace(/#.*$/gm, ''); const normalizePhpArray = (source) => stripPhpComments(source) .replace(/\bNULL\b/gi, 'null') .replace(/'/g, '"') .replace(/,(\s*[\]\}])/g, '$1'); const parseLegacyCityRows = (value) => { if (!Array.isArray(value)) { throw new Error('Legacy map data is not an array.'); } return value.map((row, index) => { if (!Array.isArray(row)) { throw new Error(`Legacy map row ${index} is not an array.`); } const [ id, name, level, population, agriculture, commerce, security, defence, wall, region, positionX, positionY, connections, ] = row; if (typeof id !== 'number' || typeof name !== 'string') { throw new Error(`Legacy map row ${index} has invalid id/name.`); } const connectionNames = Array.isArray(connections) ? connections.filter((value) => typeof value === 'string') : []; return { id, name, level, population, agriculture, commerce, security, defence, wall, region, positionX, positionY, connectionNames, }; }); }; const resolveLevelLabel = (level) => { if (typeof level === 'string') { return level; } const label = LEVEL_LABELS[level]; if (!label) { throw new Error(`Unknown level value: ${level}`); } return label; }; const resolveLevelValue = (level) => { if (typeof level === 'number') { return level; } const value = LEVEL_MAP[level]; if (!value) { throw new Error(`Unknown level label: ${level}`); } return value; }; const resolveRegionValue = (region, regionMap) => { if (typeof region === 'number') { return region; } const value = regionMap[region]; if (!value) { throw new Error(`Unknown region label: ${region}`); } return value; }; const buildCityDefinition = (row, nameToId, regionMap) => { const levelLabel = resolveLevelLabel(row.level); const initial = BUILD_INIT[levelLabel]; if (!initial) { throw new Error(`Missing build init for level ${levelLabel}.`); } const connections = row.connectionNames .map((name) => nameToId.get(name)) .filter((value) => typeof value === 'number'); return { id: row.id, name: row.name, level: resolveLevelValue(row.level), region: resolveRegionValue(row.region, regionMap), position: { x: row.positionX, y: row.positionY, }, connections, max: { population: row.population * 100, agriculture: row.agriculture * 100, commerce: row.commerce * 100, security: row.security * 100, defence: row.defence * 100, wall: row.wall * 100, }, initial, meta: { source: 'legacy', connectionNames: row.connectionNames, }, }; }; const parseRegionMap = (source) => { const raw = extractPhpArray(source, 'public static $regionMap'); if (!raw) { return null; } const normalized = normalizePhpArray(raw); const regex = /"([^"]+)"\s*=>\s*(\d+)/g; const regionMap = {}; let match = regex.exec(normalized); while (match) { const [, key, value] = match; regionMap[key] = Number(value); match = regex.exec(normalized); } return Object.keys(regionMap).length > 0 ? regionMap : null; }; const loadLegacyMapRows = async (mapName) => { const mapFilePath = path.resolve(LEGACY_MAP_ROOT, `${mapName}.php`); const [mapSource, baseSource] = await Promise.all([readFileOrNull(mapFilePath), readFileOrNull(LEGACY_BASE_FILE)]); if (!baseSource) { throw new Error(`Legacy base map file is missing: ${LEGACY_BASE_FILE}`); } const initCitySource = (mapSource ? extractPhpArray(mapSource, 'protected static $initCity') : null) ?? extractPhpArray(baseSource, 'protected static $initCity'); if (!initCitySource) { throw new Error(`Legacy map data not found for ${mapName}.`); } const parsed = JSON.parse(normalizePhpArray(initCitySource)); const rows = parseLegacyCityRows(parsed); const baseRegionMap = parseRegionMap(baseSource) ?? DEFAULT_REGION_MAP; const regionMap = mapSource ? (parseRegionMap(mapSource) ?? baseRegionMap) : baseRegionMap; return { rows, regionMap }; }; const buildMapDefinition = (mapName, rows, regionMap) => { const nameToId = new Map(rows.map((row) => [row.name, row.id])); return { id: mapName, name: mapName, cities: rows.map((row) => buildCityDefinition(row, nameToId, regionMap)), defaults: { trust: BUILD_INIT_COMMON.trust, trade: BUILD_INIT_COMMON.trade, supplyState: DEFAULT_SUPPLY_STATE, frontState: DEFAULT_FRONT_STATE, }, meta: { source: 'legacy', mapName, }, }; }; const ensureOutputRoot = async () => { await fs.mkdir(OUTPUT_ROOT, { recursive: true }); }; const writeMapDefinition = async (mapName, definition) => { const filePath = path.resolve(OUTPUT_ROOT, `map_${mapName}.json`); const payload = `${JSON.stringify(definition, null, 4)}\n`; await fs.writeFile(filePath, payload, 'utf8'); }; const main = async () => { await ensureOutputRoot(); for (const mapName of MAP_NAMES) { const { rows, regionMap } = await loadLegacyMapRows(mapName); const definition = buildMapDefinition(mapName, rows, regionMap); await writeMapDefinition(mapName, definition); console.log(`Generated map_${mapName}.json (${rows.length} cities)`); } }; await main();