/** * Ensures one south-facing enemy PNG per DB template (enemies.type). * 1) Downloads archetype reference sprites from PixelLab MCP URLs (manifest pixellabObjectId). * 2) Copies the right archetype PNG to enemies/enemy..south.png for every row in 000006b_enemy_data.sql. * 3) Merges manifest.json entries for all enemy..south keys. * * For per-slug PixelLab generations (init_image + unique prompt), use pixellab-enemy-south-v2.mjs with PIXELLAB_API_TOKEN. */ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ROOT = path.join(__dirname, '..'); const SQL = path.join(ROOT, 'backend/migrations/000006b_enemy_data.sql'); const MANIFEST = path.join(ROOT, 'frontend/public/assets/game/manifest.json'); const ASSETS_ENEMIES = path.join(ROOT, 'frontend/assets/enemies'); const ARCHETYPE_REF = { wolf: 'enemy.wolf', boar: 'enemy.boar', zombie: 'enemy.zombie', spider: 'enemy.spider', orc: 'enemy.orc', skeleton: 'enemy.skeleton_archer', battle_lizard: 'enemy.battle_lizard', element: 'enemy.water_element', demon: 'enemy.fire_demon', skeleton_king: 'enemy.skeleton_king', forest_warden: 'enemy.forest_warden', titan: 'enemy.lightning_titan', bandit: 'enemy.orc', cultist: 'enemy.skeleton_archer', golem: 'enemy.ice_guardian', wraith: 'enemy.zombie', treant: 'enemy.forest_warden', basilisk: 'enemy.battle_lizard', wyvern: 'enemy.battle_lizard', harpy: 'enemy.spider', manticore: 'enemy.battle_lizard', shade: 'enemy.zombie', }; function parseEnemyRows(sqlText) { const re = /VALUES\s*\(\d+,\s*'([^']+)',\s*'([^']+)',\s*'([^']+)',\s*'([^']*)'/g; const rows = []; let m; while ((m = re.exec(sqlText))) { rows.push({ type: m[1], archetype: m[2], biome: m[3], name: m[4] }); } return rows; } async function downloadFile(url, destPath) { const res = await fetch(url, { redirect: 'follow' }); if (!res.ok) throw new Error(`GET ${url} -> ${res.status}`); const buf = Buffer.from(await res.arrayBuffer()); fs.mkdirSync(path.dirname(destPath), { recursive: true }); fs.writeFileSync(destPath, buf); } async function main() { const sql = fs.readFileSync(SQL, 'utf8'); const rows = parseEnemyRows(sql); if (rows.length !== 220) { throw new Error(`Expected 220 enemy rows, got ${rows.length}`); } const manifest = JSON.parse(fs.readFileSync(MANIFEST, 'utf8')); const textures = manifest.textures; const refKeys = [...new Set(Object.values(ARCHETYPE_REF))]; for (const refKey of refKeys) { const entry = textures[refKey]; if (!entry?.pixellabObjectId || !entry?.file) { throw new Error(`Manifest missing reference ${refKey}`); } const dest = path.join(ROOT, 'frontend/assets', entry.file); if (fs.existsSync(dest) && fs.statSync(dest).size > 0) continue; const url = `https://api.pixellab.ai/mcp/map-objects/${entry.pixellabObjectId}/download`; console.log('Download', refKey, '->', entry.file); await downloadFile(url, dest); } fs.mkdirSync(ASSETS_ENEMIES, { recursive: true }); for (const row of rows) { const refManifestKey = ARCHETYPE_REF[row.archetype]; if (!refManifestKey) { throw new Error(`No ARCHETYPE_REF for archetype ${row.archetype}`); } const refEntry = textures[refManifestKey]; const refAbs = path.join(ROOT, 'frontend/assets', refEntry.file); if (!fs.existsSync(refAbs)) throw new Error(`Missing ref file ${refAbs}`); const southFile = `enemies/enemy.${row.type}.south.png`; const southAbs = path.join(ROOT, 'frontend/assets', southFile); const texKey = `enemy.${row.type}.south`; const existing = textures[texKey]; if (existing?.pixellabObjectId) { const url = `https://api.pixellab.ai/mcp/map-objects/${existing.pixellabObjectId}/download`; await downloadFile(url, southAbs); } else { fs.copyFileSync(refAbs, southAbs); } textures[texKey] = { file: southFile, kind: 'map_object', rotation: 'south', ...(existing?.pixellabObjectId ? { pixellabObjectId: existing.pixellabObjectId } : {}), }; } manifest.version = (manifest.version ?? 0) + 1; fs.writeFileSync(MANIFEST, JSON.stringify(manifest, null, 2) + '\n', 'utf8'); console.log('Wrote manifest version', manifest.version, 'with', Object.keys(textures).filter((k) => k.endsWith('.south') && k.startsWith('enemy.')).length, 'enemy south keys'); } main().catch((e) => { console.error(e); process.exit(1); });