맵 생성기 추가 (map01 템플릿 복제·배경/몬스터 주입)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
115
tools/gen-maps.mjs
Normal file
115
tools/gen-maps.mjs
Normal file
@@ -0,0 +1,115 @@
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
|
||||
const TEMPLATE = 'map/map01.map';
|
||||
const SECTOR = 'Global/SectorConfig.config';
|
||||
const MAP_NUMBERS = [2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
|
||||
|
||||
// 공식 라이브러리 배경 RUID 풀 (맵마다 1개씩, 서로 다르게)
|
||||
const BACKGROUNDS = [
|
||||
'79c95db9fdbb4c4796771733d069e3e2', '1d4a335a5416401f8e289d78a03fd0c3',
|
||||
'731a9cd1cce045e19d50fdcdc9a20be9', '695805b1809243fd9376e2bba113ebde',
|
||||
'454804df4c7e4701997ec8a8de088597', '01992685f5d147b3a5c18fabf584807f',
|
||||
'c861e9cb2d0b4d91be5d4d6aedf796b1', 'ee2e13a352d64611906760c1b722df67',
|
||||
'8e89019c54d14aed875e54f13fa14109', 'fa936edd365f47e4b5622c19b1a80a0c',
|
||||
];
|
||||
|
||||
// Task 1 결과: B 폴백 (라이브러리 변형 미사용). 비어 있으면 기존 템플릿 몬스터를 그대로 사용.
|
||||
// 각 항목: { sprite, stand, hit, die } (모두 RUID 문자열)
|
||||
const MONSTER_VARIANTS = [];
|
||||
|
||||
// 결정론적 시드 RNG (맵 번호 기반)
|
||||
function rng(seed) {
|
||||
let s = seed >>> 0;
|
||||
return () => { s = (s * 1664525 + 1013904223) >>> 0; return s / 4294967296; };
|
||||
}
|
||||
|
||||
// 결정론적 대시 GUID (맵번호, 인덱스)
|
||||
function mapGuid(nn, idx) {
|
||||
const n = (nn * 1000 + 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) => (e.componentNames || '').includes('script.Monster');
|
||||
const compOf = (e, type) => e.jsonString['@components'].find((c) => c['@type'] === type);
|
||||
|
||||
const template = JSON.parse(readFileSync(TEMPLATE, 'utf8'));
|
||||
const monsterTemplates = template.ContentProto.Entities.filter(isMonster);
|
||||
if (monsterTemplates.length === 0) throw new Error('템플릿에서 몬스터 엔티티를 못 찾음');
|
||||
|
||||
function buildMap(nn) {
|
||||
const tag = String(nn).padStart(2, '0');
|
||||
const rand = rng(nn * 7919);
|
||||
const map = JSON.parse(JSON.stringify(template)); // deep clone
|
||||
map.EntryKey = `map://map${tag}`;
|
||||
|
||||
// 비-몬스터 엔티티만 유지
|
||||
const ents = map.ContentProto.Entities.filter((e) => !isMonster(e));
|
||||
|
||||
// 몬스터 2마리 추가 (템플릿 몬스터 복제)
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const src = monsterTemplates[Math.floor(rand() * monsterTemplates.length)];
|
||||
const m = JSON.parse(JSON.stringify(src));
|
||||
m.jsonString.name = `Monster${i + 1}`;
|
||||
m.path = `/maps/map${tag}/Monster${i + 1}`;
|
||||
m.jsonString.path = m.path;
|
||||
const tr = compOf(m, 'MOD.Core.TransformComponent');
|
||||
if (tr) tr.Position.x = Math.round((rand() * 8 - 4) * 100) / 100; // -4..4 바닥 위
|
||||
if (MONSTER_VARIANTS.length > 0) {
|
||||
const v = MONSTER_VARIANTS[Math.floor(rand() * MONSTER_VARIANTS.length)];
|
||||
const sp = compOf(m, 'MOD.Core.SpriteRendererComponent');
|
||||
if (sp) sp.SpriteRUID = v.sprite;
|
||||
const sa = compOf(m, 'MOD.Core.StateAnimationComponent');
|
||||
if (sa) sa.ActionSheet = { stand: v.stand, hit: v.hit, die: v.die };
|
||||
}
|
||||
ents.push(m);
|
||||
}
|
||||
|
||||
// 경로/이름 치환 + 배경 설정
|
||||
for (const e of ents) {
|
||||
if (typeof e.path === 'string') e.path = e.path.replace('/maps/map01', `/maps/map${tag}`);
|
||||
if (e.jsonString) {
|
||||
if (typeof e.jsonString.path === 'string') e.jsonString.path = e.jsonString.path.replace('/maps/map01', `/maps/map${tag}`);
|
||||
if (e.jsonString.name === 'map01') e.jsonString.name = `map${tag}`;
|
||||
}
|
||||
if ((e.path || '').endsWith('/Background')) {
|
||||
const bg = compOf(e, 'MOD.Core.BackgroundComponent');
|
||||
if (bg) bg.TemplateRUID = BACKGROUNDS[(nn - 2) % BACKGROUNDS.length];
|
||||
}
|
||||
}
|
||||
|
||||
// GUID 재발급 (자기참조 root/sub_entity_id 보정)
|
||||
ents.forEach((e, idx) => {
|
||||
const oldId = e.id;
|
||||
const newId = mapGuid(nn, idx);
|
||||
e.id = newId;
|
||||
const o = e.jsonString && e.jsonString.origin;
|
||||
if (o) {
|
||||
if (o.root_entity_id === oldId) o.root_entity_id = newId;
|
||||
if (o.sub_entity_id === oldId) o.sub_entity_id = newId;
|
||||
}
|
||||
});
|
||||
|
||||
map.ContentProto.Entities = ents;
|
||||
writeFileSync(`map/map${tag}.map`, JSON.stringify(map, null, 2), 'utf8');
|
||||
return `map${tag}`;
|
||||
}
|
||||
|
||||
// 인자: 생성할 맵 번호(미지정 시 전체). 예: node tools/gen-maps.mjs 2
|
||||
const arg = process.argv[2];
|
||||
const targets = arg ? [Number(arg)] : MAP_NUMBERS;
|
||||
const made = targets.map(buildMap);
|
||||
console.log('Generated:', made.join(', '));
|
||||
|
||||
// SectorConfig 등록 (전체 생성 시에만, 중복 방지)
|
||||
if (!arg) {
|
||||
const sector = JSON.parse(readFileSync(SECTOR, 'utf8'));
|
||||
const entries = sector.ContentProto.Json.Sectors[0].entries;
|
||||
for (const nn of MAP_NUMBERS) {
|
||||
const key = `map://map${String(nn).padStart(2, '0')}`;
|
||||
if (!entries.includes(key)) entries.push(key);
|
||||
}
|
||||
writeFileSync(SECTOR, JSON.stringify(sector, null, 2), 'utf8');
|
||||
console.log('SectorConfig entries:', entries.length);
|
||||
}
|
||||
Reference in New Issue
Block a user