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