import { readFileSync, writeFileSync } from 'node:fs'; // map02~11에 노드 타입별 몬스터 그룹(combat3/elite2/boss1)을 맵별 테마로 자동 구성. // 기존 몬스터 엔티티를 전부 제거하고 첫 몬스터를 템플릿으로 6마리 재생성(결정론). const MAP_NUMBERS = [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; const COMBAT_POOL = ['orange_mushroom', 'green_mushroom', 'pig', 'blue_mushroom']; const ELITE_POOL = ['mushmom', 'modified_snail']; const BOSS_POOL = ['king_slime', 'slime_boss']; const LAYOUT = [ { group: 'combat', x: 2.3 }, { group: 'combat', x: 3.8 }, { group: 'combat', x: 5.2 }, { group: 'elite', x: 3.0 }, { group: 'elite', x: 5.0 }, { group: 'boss', x: 4.0 }, ]; const MONSTER_VARIANTS = [ { sprite: '96e955c1bf27415e84f96deea200a8f1', stand: '96e955c1bf27415e84f96deea200a8f1', hit: 'aec9504d5dc24aceb5646b79d30abad4', die: '65a2bfb039614f2e9e4ccc354340153d' }, { sprite: 'f86992ba9c41487c8480fcb893fcbda6', stand: 'f86992ba9c41487c8480fcb893fcbda6', hit: 'd305b942b1704c8084548108ff3b7a6b', die: '5a563e5fd98c4132b61057dc6bb8aaf2' }, { sprite: 'a2204a21d88942b281d2cac6053ffbaa', stand: 'a2204a21d88942b281d2cac6053ffbaa', hit: 'afc08936b8a64b26bc3dd8c03ead1f26', die: 'fc1c6d9ba9bc413ab53b6dbfae3ac45b' }, { sprite: 'd8f014043ce8418f96700c2b6c9ebf6c', stand: 'd8f014043ce8418f96700c2b6c9ebf6c', hit: 'c3cf643b618346c7bfa6574187b396f9', die: 'a88d9b3d60f941e4890dc89a6ccaa8ee' }, { sprite: '17b55730c26f4fd6b8fcfa288da388de', stand: '17b55730c26f4fd6b8fcfa288da388de', hit: 'eac48e84a9fc4580a4018de5cf52ddb3', die: '51c2f4b59a2c413db26035aa57002fc8' }, { sprite: '48c10437ae8344a9b2a1d3f36185728f', stand: '48c10437ae8344a9b2a1d3f36185728f', hit: '9044063647854f5e9128efcf80e909be', die: 'f414577d18c94cc387c275df4abdbc3b' }, { sprite: '4ca39dbfa1c6492283ba8bd352d12b0a', stand: '4ca39dbfa1c6492283ba8bd352d12b0a', hit: '7ac78511036e4ebe988b97c35fc275d1', die: '740f3f2b2e7a4b71bec5eac84e8539f9' }, { sprite: 'ed3908e24d694bb786023fc1ed073489', stand: 'ed3908e24d694bb786023fc1ed073489', hit: '4763c9bebc9245998c9c499b6316aa9f', die: 'b168793b92a844a3a3a6f4ce647a14d2' }, { sprite: '3109357701ae41a4bcc7543f52f1f4c3', stand: '3109357701ae41a4bcc7543f52f1f4c3', hit: 'ce0269079e884545b5bb6ea075e2a67f', die: 'a5e65650e00e47878cac1be7a5b999a0' }, ]; function rng(seed) { let s = seed >>> 0; return () => { s = (s * 1664525 + 1013904223) >>> 0; return s / 4294967296; }; } function encGuid(nn, idx) { const n = (nn * 1000 + 500 + idx) >>> 0; const h8 = n.toString(16).padStart(8, '0'); const h12 = n.toString(16).padStart(12, '0'); return `${h8}-0000-4000-8000-${h12}`; } const isMonster = (e) => typeof e.componentNames === 'string' && e.componentNames.includes('script.Monster'); const compOf = (e, t) => e.jsonString['@components'].find((c) => c['@type'] === t); function pick(rand, pool) { return pool[Math.floor(rand() * pool.length)]; } function pickN(rand, pool, n) { const a = pool.slice(); const out = []; for (let i = 0; i < n; i++) { if (a.length === 0) a.push(...pool); out.push(a.splice(Math.floor(rand() * a.length), 1)[0]); } return out; } function patchMap(nn) { const tag = String(nn).padStart(2, '0'); const file = `map/map${tag}.map`; const map = JSON.parse(readFileSync(file, 'utf8')); const ents = map.ContentProto.Entities; const monsters = ents.filter(isMonster); if (monsters.length === 0) throw new Error(`[gen-map-encounters] ${file} 몬스터 템플릿 없음`); const template = monsters[0]; map.ContentProto.Entities = ents.filter((e) => !isMonster(e)); const rand = rng(nn * 7919 + 17); const combatIds = pickN(rand, COMBAT_POOL, 3); const eliteIds = pickN(rand, ELITE_POOL, 2); const bossId = pick(rand, BOSS_POOL); const variants = pickN(rand, MONSTER_VARIANTS, 6); LAYOUT.forEach((slot, idx) => { const m = JSON.parse(JSON.stringify(template)); const enemyId = slot.group === 'combat' ? combatIds[idx] : slot.group === 'elite' ? eliteIds[idx - 3] : bossId; const name = `${slot.group}_${idx + 1}`; m.id = encGuid(nn, idx); m.path = `/maps/map${tag}/${name}`; m.jsonString.path = m.path; m.jsonString.name = name; const o = m.jsonString.origin; if (o) { if (o.root_entity_id) o.root_entity_id = m.id; if (o.sub_entity_id) o.sub_entity_id = m.id; } const tr = compOf(m, 'MOD.Core.TransformComponent'); if (tr && tr.Position) tr.Position.x = slot.x; const v = variants[idx]; const sp = compOf(m, 'MOD.Core.SpriteRendererComponent'); if (sp) sp.SpriteRUID = v.stand; const sa = compOf(m, 'MOD.Core.StateAnimationComponent'); if (sa) sa.ActionSheet = { stand: v.stand, hit: v.hit, die: v.die }; let cm = compOf(m, 'script.CombatMonster'); if (!cm) { cm = { '@type': 'script.CombatMonster', Enable: true }; m.jsonString['@components'].push(cm); const names = (m.componentNames || '').split(',').filter((s) => s && s !== 'script.CombatMonster'); names.push('script.CombatMonster'); m.componentNames = names.join(','); } cm.EnemyId = enemyId; cm.Group = slot.group; map.ContentProto.Entities.push(m); }); writeFileSync(file, JSON.stringify(map, null, 2), 'utf8'); return `map${tag}(${combatIds.join('/')}|${eliteIds.join('/')}|${bossId})`; } const made = MAP_NUMBERS.map(patchMap); console.log('Encounters:', made.join(', '));