const fs = require('fs'); function load(path) { return JSON.parse(fs.readFileSync(path, 'utf8')); } function dist(a, b) { return Math.hypot(a.world.x - b.world.x, a.world.y - b.world.y); } function qlo(item) { return item.quality & 0xff; } function isSpawner(item) { return item.shapeDefId === 'shape:1232'; } for (const [label, path] of [ ['map1', 'k:/ghidra/crusader_map_viewer/map_renderer/.cache/scene-cache/remorse/map-1/9ccaa5dabe08947e/scene.json'], ['map248', 'k:/ghidra/crusader_map_viewer/map_renderer/.cache/scene-cache/remorse/map-248/b27ea0d8d2a1a391/scene.json'] ]) { const scene = load(path); const items = scene.items.filter(isSpawner); const interesting = items.filter((item) => item.npcPreview?.name === 'Observer' || item.npcPreview?.name === 'RoamingSusan'); console.log(`\n### ${label} interesting spawners ${interesting.length}`); for (const item of interesting) { const pairs = items.filter((candidate) => candidate.id !== item.id && candidate.frame !== item.frame && qlo(candidate) === qlo(item) && dist(candidate, item) <= 128); const pairText = pairs.map((candidate) => `${candidate.id} src=${candidate.mapSourceIndex} f=${candidate.frame} npc=${candidate.npcNum} ${candidate.npcPreview?.name || '?'} qlo=${qlo(candidate)} d=${dist(candidate, item).toFixed(1)} map=${candidate.mapNum}`).join(' || '); console.log(`${item.id} src=${item.mapSourceIndex} f=${item.frame} npc=${item.npcNum} ${item.npcPreview?.name || '?'} qlo=${qlo(item)} map=${item.mapNum} world=${item.world.x},${item.world.y},${item.world.z}`); if (pairText) { console.log(` pairs: ${pairText}`); } } }