Files
maplecontest/tools/map/gen-map-encounters.mjs
gahusb 6e82d0f128 feat(map): 인카운터를 encounters.json 로스터 기반 모델 인스턴스 배치로 개편
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
2026-07-02 13:55:46 +09:00

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(', '));