From de2fcdbe7cc1721a397eb84aa965e4002d7f328c Mon Sep 17 00:00:00 2001 From: gahusb Date: Sat, 6 Jun 2026 12:19:11 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A7=B5=20=EC=83=9D=EC=84=B1=EA=B8=B0=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(map01=20=ED=85=9C=ED=94=8C=EB=A6=BF=20?= =?UTF-8?q?=EB=B3=B5=EC=A0=9C=C2=B7=EB=B0=B0=EA=B2=BD/=EB=AA=AC=EC=8A=A4?= =?UTF-8?q?=ED=84=B0=20=EC=A3=BC=EC=9E=85)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- tools/gen-maps.mjs | 115 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 tools/gen-maps.mjs diff --git a/tools/gen-maps.mjs b/tools/gen-maps.mjs new file mode 100644 index 0000000..5129813 --- /dev/null +++ b/tools/gen-maps.mjs @@ -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); +}