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]; // 공식 맵에서 수확한 Background-타입 RUID 풀 (맵마다 1개씩, 서로 다르게). // 공식 MapleStory 맵을 import해 각 맵의 BackgroundComponent.TemplateRUID를 수집함. // (라이브러리 검색 cat=background는 AnimationClip을 반환해 Background 타입이 아니었음 → 사용 불가) const BACKGROUNDS = [ '794ad8421e2543d8a6d2c70307637450', // 기본 템플릿(문서 예시) '65c4167ea7484196b890022354e5a4a4', // Henesys '23801e25ab854f3189454a4fa9d01761', // Edelstein 'f91f8b5b2b1e47208fb18f2cbd2a5e8d', // Fox Point Village 'd84241f17de344a097f5b96ac914f1d2', // Lith Harbor '60741c3333334297b9f211939e02a4fc', // Ellinia '0c398bbb2cf6400992532465b9d53024', // Perion 'f9e546932b014c0e867365a796c8dc91', // Kerning City '2ee96aa776e8480b849c1fb9a4d4c05c', // Orbis 'b7c47cfae79e40e9b1352469a78af0bd', // Ludibrium ]; // 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); }