MONSTER_VARIANTS 랜덤 외형 제거(외형=enemies.json appearance로 정체성 고정). buildMonsterInstance로 종별 모델(monster-<id>) 인스턴스 배치, 준비도 가드로 appearance 미보유 로스터 맵은 보존(Task 2 RUID 수확 후 재생성). gen-combat-monster는 codeblock 생성만(맵 부착은 encounters 생성기로 흡수). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011xhLoQbJvQYL65kBtDNDTy
56 lines
2.7 KiB
JavaScript
56 lines
2.7 KiB
JavaScript
import { readFileSync, writeFileSync } from 'node:fs';
|
|
import { buildMonsterInstance } from '../monster/lib/monster-model.mjs';
|
|
|
|
// map01~05에 data/encounters.json 로스터대로 종별 모델 인스턴스를 배치(결정론).
|
|
// 기존 몬스터 엔티티 전부 제거 후 로스터 전체를 그룹별 x 균등 분포로 재생성.
|
|
// 준비도 가드: 로스터에 appearance 미보유 적이 있는 맵은 재생성을 건너뛴다(기존 맵 보존).
|
|
const enemies = JSON.parse(readFileSync('data/enemies.json', 'utf8')).enemies;
|
|
const encounters = JSON.parse(readFileSync('data/encounters.json', 'utf8'));
|
|
const X_RANGE = { combat: [2.3, 6.6], elite: [3.0, 5.6], boss: [4.6, 4.6] };
|
|
|
|
const isMonster = (e) => typeof e.componentNames === 'string' && e.componentNames.includes('script.Monster');
|
|
function encGuid(nn, idx) {
|
|
const n = (nn * 1000 + 500 + idx) >>> 0;
|
|
return `${n.toString(16).padStart(8, '0')}-0000-4000-8000-${n.toString(16).padStart(12, '0')}`;
|
|
}
|
|
function slotX(group, i, count) {
|
|
const [lo, hi] = X_RANGE[group];
|
|
return count <= 1 ? (lo + hi) / 2 : lo + (i * (hi - lo)) / (count - 1);
|
|
}
|
|
|
|
function patchMap(nn) {
|
|
const tag = String(nn).padStart(2, '0');
|
|
const file = `map/map${tag}.map`;
|
|
const roster = encounters[`map${tag}`];
|
|
if (!roster) throw new Error(`[gen-map-encounters] encounters.json에 map${tag} 없음`);
|
|
const rosterIds = ['combat', 'elite', 'boss'].flatMap((g) => roster[g] || []);
|
|
for (const id of rosterIds) {
|
|
if (!enemies[id]) throw new Error(`[gen-map-encounters] map${tag} 로스터에 없는 적: ${id}`);
|
|
}
|
|
// 준비도 가드: appearance 미보유 적이 하나라도 있으면 이 맵은 보존(스킵)
|
|
const missing = rosterIds.filter((id) => !enemies[id].appearance);
|
|
if (missing.length) return `map${tag}(SKIP: appearance 없음 ${[...new Set(missing)].join('/')})`;
|
|
|
|
const map = JSON.parse(readFileSync(file, 'utf8'));
|
|
map.ContentProto.Entities = map.ContentProto.Entities.filter((e) => !isMonster(e));
|
|
const nameCount = {};
|
|
let idx = 0;
|
|
for (const group of ['combat', 'elite', 'boss']) {
|
|
const ids = roster[group] || [];
|
|
ids.forEach((enemyId, i) => {
|
|
nameCount[enemyId] = (nameCount[enemyId] || 0) + 1;
|
|
const name = nameCount[enemyId] > 1 ? `${enemyId}_${nameCount[enemyId]}` : enemyId;
|
|
map.ContentProto.Entities.push(buildMonsterInstance({
|
|
enemyId, enemy: enemies[enemyId], name, guid: encGuid(nn, idx), mapTag: tag, x: slotX(group, i, ids.length), group,
|
|
}));
|
|
idx += 1;
|
|
});
|
|
}
|
|
writeFileSync(file, JSON.stringify(map, null, 2), 'utf8');
|
|
const counts = ['combat', 'elite', 'boss'].map((g) => `${g}${(roster[g] || []).length}`).join('/');
|
|
return `map${tag}(${counts})`;
|
|
}
|
|
|
|
const made = [1, 2, 3, 4, 5].map(patchMap);
|
|
console.log('Encounters:', made.join(', '));
|