You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
198 lines
6.0 KiB
JavaScript
198 lines
6.0 KiB
JavaScript
/**
|
|
* Regenerate enemy.<slug>.south sprites via PixelLab API v2 (POST /map-objects).
|
|
* Uses local archetype PNG as init_image for style continuity.
|
|
*
|
|
* Requires: PIXELLAB_API_TOKEN in the environment (https://api.pixellab.ai/mcp).
|
|
*
|
|
* Usage:
|
|
* node scripts/pixellab-enemy-south-v2.mjs --limit=5
|
|
* node scripts/pixellab-enemy-south-v2.mjs --slug=boar_l5_5_canyon
|
|
*/
|
|
|
|
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 BASE = 'https://api.pixellab.ai/v2';
|
|
|
|
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',
|
|
};
|
|
|
|
const BIOME_HINT = {
|
|
meadow: 'sunlit meadow greens and wildflowers',
|
|
forest: 'deep forest shadows, moss and bark tones',
|
|
ruins: 'grey stone dust, cracked masonry palette',
|
|
canyon: 'red rock, dust, harsh sun',
|
|
swamp: 'murky teal, bog mist, rotting wood',
|
|
volcanic: 'ember glow, black rock, lava highlights',
|
|
astral: 'cosmic violet-blue, star specks, ethereal glow',
|
|
};
|
|
|
|
function parseArgs() {
|
|
const a = process.argv.slice(2);
|
|
let limit = Infinity;
|
|
let slug = null;
|
|
for (const x of a) {
|
|
if (x.startsWith('--limit=')) limit = parseInt(x.slice(8), 10);
|
|
if (x.startsWith('--slug=')) slug = x.slice(7);
|
|
}
|
|
return { limit, slug };
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
function buildPrompt(row) {
|
|
const b = BIOME_HINT[row.biome] ?? row.biome;
|
|
return (
|
|
`Pixel RPG combat sprite: ${row.name}. ` +
|
|
`${row.archetype.replace(/_/g, ' ')} monster, ${b}. ` +
|
|
`Low top-down view, facing south toward camera, full body, transparent background, ` +
|
|
`game-ready, single black outline, medium shading — match init image proportions and style.`
|
|
);
|
|
}
|
|
|
|
async function pollObject(token, objectId) {
|
|
const url = `${BASE}/objects/${objectId}`;
|
|
for (let i = 0; i < 120; i++) {
|
|
const res = await fetch(url, {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
});
|
|
const j = await res.json().catch(() => ({}));
|
|
const data = j.data ?? j;
|
|
const status = data?.status ?? data?.generation_status;
|
|
if (status === 'completed' || data?.image_url || data?.download_url) {
|
|
return data;
|
|
}
|
|
if (status === 'failed' || data?.error) {
|
|
throw new Error(JSON.stringify(data?.error ?? data));
|
|
}
|
|
await new Promise((r) => setTimeout(r, 3000));
|
|
}
|
|
throw new Error('Timeout waiting for object');
|
|
}
|
|
|
|
async function downloadMcpObject(objectId, destPath) {
|
|
const url = `https://api.pixellab.ai/mcp/map-objects/${objectId}/download`;
|
|
const res = await fetch(url);
|
|
if (!res.ok) throw new Error(`download ${res.status}`);
|
|
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
fs.writeFileSync(destPath, Buffer.from(await res.arrayBuffer()));
|
|
}
|
|
|
|
async function main() {
|
|
const token = process.env.PIXELLAB_API_TOKEN;
|
|
if (!token) {
|
|
console.error('Set PIXELLAB_API_TOKEN');
|
|
process.exit(1);
|
|
}
|
|
|
|
const { limit, slug } = parseArgs();
|
|
const sql = fs.readFileSync(SQL, 'utf8');
|
|
let rows = parseEnemyRows(sql);
|
|
if (slug) rows = rows.filter((r) => r.type === slug);
|
|
rows = rows.slice(0, limit);
|
|
|
|
const manifest = JSON.parse(fs.readFileSync(MANIFEST, 'utf8'));
|
|
const textures = manifest.textures;
|
|
let updated = 0;
|
|
|
|
for (const row of rows) {
|
|
const refKey = ARCHETYPE_REF[row.archetype];
|
|
const refFile = textures[refKey].file;
|
|
const refPath = path.join(ROOT, 'frontend/assets', refFile);
|
|
const b64 = fs.readFileSync(refPath).toString('base64');
|
|
|
|
const body = {
|
|
description: buildPrompt(row),
|
|
image_size: { width: 96, height: 96 },
|
|
view: 'low top-down',
|
|
outline: 'single color outline',
|
|
shading: 'medium shading',
|
|
detail: 'medium detail',
|
|
init_image: { type: 'base64', base64: b64 },
|
|
init_image_strength: 420,
|
|
text_guidance_scale: 9,
|
|
};
|
|
|
|
const cr = await fetch(`${BASE}/map-objects`, {
|
|
method: 'POST',
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(body),
|
|
});
|
|
const cj = await cr.json().catch(() => ({}));
|
|
if (!cr.ok) {
|
|
console.error('Create failed', row.type, cj);
|
|
continue;
|
|
}
|
|
const objectId = cj.data?.object_id ?? cj.object_id ?? cj.data?.id;
|
|
if (!objectId) {
|
|
console.error('No object_id', row.type, cj);
|
|
continue;
|
|
}
|
|
|
|
await pollObject(token, objectId);
|
|
const southFile = `enemies/enemy.${row.type}.south.png`;
|
|
const southAbs = path.join(ROOT, 'frontend/assets', southFile);
|
|
await downloadMcpObject(objectId, southAbs);
|
|
|
|
const texKey = `enemy.${row.type}.south`;
|
|
textures[texKey] = {
|
|
file: southFile,
|
|
kind: 'map_object',
|
|
rotation: 'south',
|
|
pixellabObjectId: objectId,
|
|
};
|
|
updated++;
|
|
console.log('OK', row.type, objectId);
|
|
}
|
|
|
|
if (updated > 0) {
|
|
manifest.version = (manifest.version ?? 0) + 1;
|
|
fs.writeFileSync(MANIFEST, JSON.stringify(manifest, null, 2) + '\n', 'utf8');
|
|
}
|
|
}
|
|
|
|
main().catch((e) => {
|
|
console.error(e);
|
|
process.exit(1);
|
|
});
|