- tools/{player,monster,camera,map,deck,balance}/ 로 8개 스크립트 분류 (git mv 이력 보존)
- gen-camera의 플레이어 입력 차단·시선 고정을 tools/player/gen-player-lock.mjs(PlayerLock 코드블록)로 분리
- MapCamera 코드블록은 카메라 속성 전용으로 정리, 11개 맵 루트에 script.PlayerLock 부착
- README 및 스크립트 주석의 도구 경로 갱신
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
153 lines
7.5 KiB
JavaScript
153 lines
7.5 KiB
JavaScript
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
|
|
];
|
|
|
|
// 공식 맵에서 수확한 타일셋 RUID (맵마다 다르게). map01 기본(9dfea380…) 제외.
|
|
const TILESETS = [
|
|
'46701ff2021b4d1fb21fbf5790b1ab14',
|
|
'7b6bd117bd0446a5bacec8ea6831c997',
|
|
'9bf18287398c44699c20fc5123d1a1ae',
|
|
'd6a94bc26c8f43e2a7abfabfae0c4fc4',
|
|
'e80a4b6e22d34348837d2ecf30e7cf74',
|
|
'23e80224ef624ea5af497cc50aa0e752',
|
|
'2667829326dd46de80ef26f6bb7f26ae',
|
|
'48afa7d90aa24fadae9c52f30977342e',
|
|
'901f885ef94f4a32961bf6cc64e3ec86',
|
|
'3ca52bc385574e56aaffa15eea5c23aa',
|
|
'df1a1fee05874794a624c2bccbb1574d',
|
|
'e1703f6cb2f84969bc54afcd12006b4e',
|
|
];
|
|
|
|
// 공식 맵에서 수확한 몬스터 변형 (기존 map01 4종 미사용). 항목: { sprite, stand, hit, die }
|
|
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' },
|
|
];
|
|
|
|
// 결정론적 시드 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));
|
|
// 정적 베이스(StS2 위치 고정 — 배회 방지). 변형이 sprite/animation을 덮어쓰므로 외형은 베이스와 무관.
|
|
const base = monsterTemplates.find((e) => (e.path || '').includes('Static')) || monsterTemplates[0];
|
|
// 서로 다른 변형 2종 선택 (맵 내 중복 금지)
|
|
const vi = Math.floor(rand() * MONSTER_VARIANTS.length);
|
|
const vj = (vi + 1 + Math.floor(rand() * (MONSTER_VARIANTS.length - 1))) % MONSTER_VARIANTS.length;
|
|
const chosen = [MONSTER_VARIANTS[vi], MONSTER_VARIANTS[vj]];
|
|
const STS2_X = [3.5, 5.5]; // 화면 우측 전투 포메이션
|
|
for (let i = 0; i < 2; i++) {
|
|
const m = JSON.parse(JSON.stringify(base));
|
|
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 = STS2_X[i];
|
|
const v = chosen[i];
|
|
const sp = compOf(m, 'MOD.Core.SpriteRendererComponent');
|
|
if (sp) sp.SpriteRUID = v.sprite;
|
|
const sa = compOf(m, 'MOD.Core.StateAnimationComponent');
|
|
if (sa && v.stand) 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];
|
|
}
|
|
if ((e.path || '').endsWith('/TileMap')) {
|
|
const tm = compOf(e, 'MOD.Core.TileMapComponent');
|
|
if (tm && TILESETS.length > 0) tm.TileSetRUID = { DataId: TILESETS[(nn - 2) % TILESETS.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/map/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);
|
|
}
|