feat(act-maps): map02~11 인카운터 자동 구성 (combat3/elite2/boss1·맵별 테마)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
95
tools/map/gen-map-encounters.mjs
Normal file
95
tools/map/gen-map-encounters.mjs
Normal file
@@ -0,0 +1,95 @@
|
||||
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(', '));
|
||||
Reference in New Issue
Block a user