13 KiB
맵 10개 생성 (랜덤 배경 + 몬스터 2마리) Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: map01을 템플릿으로 독립 맵 10개(map02~map11)를 생성하고, 맵마다 다른 공식 배경 + 랜덤 위치 몬스터 2마리를 배치한다.
Architecture: Node 생성기 tools/gen-maps.mjs가 map/map01.map을 JSON으로 읽어 맵마다 deep-clone → 경로/EntryKey/name 치환, 전 엔티티 GUID 재발급(자기참조 보정), Background.TemplateRUID 교체, 몬스터 2마리 배치 → map/mapNN.map(JSON.stringify)로 기록. SectorConfig.config에 등록. 몬스터 다양화(A)는 MONSTER_VARIANTS 데이터로 주입하며, map02 스파이크로 렌더 검증 후 10개로 확대(실패 시 B=기존 몬스터 폴백).
Tech Stack: Node.js(ESM, 표준 라이브러리), MSW .map(JSON 엔티티), msw-mcp 에셋 검색, msw-maker-mcp reload/play/screenshot/execute_script.
File Structure
- Create:
tools/gen-maps.mjs— 맵 생성기 (템플릿 클론·GUID 재발급·배경/몬스터 주입·SectorConfig 갱신). - Create:
map/map02.map~map/map11.map— 생성 결과. - Modify:
Global/SectorConfig.config—entries에 map02~map11 추가.
배경 RUID 풀(공식 라이브러리, 확보 완료, 10개):
79c95db9fdbb4c4796771733d069e3e2, 1d4a335a5416401f8e289d78a03fd0c3, 731a9cd1cce045e19d50fdcdc9a20be9, 695805b1809243fd9376e2bba113ebde, 454804df4c7e4701997ec8a8de088597, 01992685f5d147b3a5c18fabf584807f, c861e9cb2d0b4d91be5d4d6aedf796b1, ee2e13a352d64611906760c1b722df67, 8e89019c54d14aed875e54f13fa14109, fa936edd365f47e4b5622c19b1a80a0c
맵 구조(map01): 엔티티 /maps/map01(Map+Foothold), /Background(BackgroundComponent.TemplateRUID), /MapleMapLayer, /TileMap, /SpawnLocation, 몬스터들(componentNames에 script.Monster 포함: StaticMonsterTemplate/MoveMonsterTemplate/ChaseMonsterTemplate/monster-43). 엔티티 id는 대시 GUID(8-4-4-4-12), 리소스 RUID는 대시 없는 32hex.
Task 1: 라이브러리 몬스터 변형 후보 조사 (컨트롤러/MCP, 타임박스)
목표: 완결된 라이브러리 몬스터 변형(스프라이트 + stand/hit/die 액션 RUID 세트)을 ≥3종 확보 시도. 액션 그룹핑/이름을 얻을 수 없으면 B 폴백(빈 변형)으로 결정.
- Step 1: 라이브러리 몬스터 리소스 조사
MCP asset_search_resources로 cat="animationclip"/"sprite", source="maplestory", query로 몬스터 후보를 찾고, 가능하면 detail=true 및 메타데이터로 action(stand/hit/die) 식별을 시도한다.
- Step 2: 변형 세트 확정 또는 폴백 결정
각 변형을 { sprite, stand, hit, die }(RUID) 형태로 ≥3개 확보하면 → 그 배열을 Task 2의 MONSTER_VARIANTS로 사용.
액션 식별이 불가하거나 불확실하면 → MONSTER_VARIANTS = []로 두고 B 폴백(기존 템플릿 몬스터 그대로 사용)으로 진행한다. 결정 결과를 한 줄로 기록(log 또는 보고).
이 태스크의 산출물은 "MONSTER_VARIANTS 배열(또는 빈 배열) + 결정 사유" 한 가지다. 코드 변경 없음.
Task 2: 생성기 작성
Files: Create tools/gen-maps.mjs
- Step 1: 스크립트 작성
tools/gen-maps.mjs에 아래를 그대로 작성한다. MONSTER_VARIANTS는 Task 1 결과로 채우거나 빈 배열로 둔다(빈 배열 = B 폴백).
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);
}
- Step 2: 구문 확인
cd "C:/Users/jaeoh/Desktop/workspace/slaymaple"
node --check tools/gen-maps.mjs
Expected: 출력 없음(exit 0).
- Step 3: 커밋
git add tools/gen-maps.mjs
git commit -m "맵 생성기 추가 (map01 템플릿 복제·배경/몬스터 주입)"
Task 3: map02 스파이크 — 생성 + Maker 렌더 검증
Files: Create map/map02.map
- Step 1: map02만 생성
cd "C:/Users/jaeoh/Desktop/workspace/slaymaple"
node tools/gen-maps.mjs 2
Expected: Generated: map02
- Step 2: 데이터 검증
cd "C:/Users/jaeoh/Desktop/workspace/slaymaple"
node -e "const j=JSON.parse(require('fs').readFileSync('map/map02.map','utf8'));const E=j.ContentProto.Entities;console.log('EntryKey:',j.EntryKey);const ids=E.map(e=>e.id);console.log('unique ids:', new Set(ids).size===ids.length);console.log('monsters:', E.filter(e=>(e.componentNames||'').includes('script.Monster')).length);const bg=E.find(e=>(e.path||'').endsWith('/Background'));console.log('bg RUID:', bg.jsonString['@components'].find(c=>c['@type']==='MOD.Core.BackgroundComponent').TemplateRUID);console.log('paths ok:', E.every(e=>!(e.path||'').includes('/maps/map01')))"
Expected: EntryKey: map://map02, unique ids: true, monsters: 2, bg RUID: 가 배경 풀의 첫 값(79c95db9...), paths ok: true.
- Step 3: Maker에서 map02 열어 렌더 검증 (컨트롤러)
maker_refresh_workspace(edit)- Maker에서 map02를 활성 맵으로 연다(에디터에서 map02 더블클릭). MCP로 직접 맵 전환이 안 되면, 사용자에게 "Maker에서 map02 열기"를 요청한다.
maker_play→maker_screenshot→ Read로 확인: 배경이 map01과 다른 배경으로 표시되고 몬스터 2마리가 보이는지.maker_execute_script(client)로 몬스터 로드 확인:→local m1 = _EntityService:GetEntityByPath("/maps/map02/Monster1") local m2 = _EntityService:GetEntityByPath("/maps/map02/Monster2") log("M1="..tostring(m1~=nil).." M2="..tostring(m2~=nil))maker_logs(normal)에서M1=true M2=true확인.maker_stop.
-
Step 4: 게이트 판정
-
배경·몬스터 정상 → 그대로 진행(Task 4).
-
배경이 흰/검 박스이거나 몬스터 안 보임:
- 배경 문제: 배경 RUID 풀이 로컬 워크스페이스에서 로드 안 됨 → 사용자와 상의(공식 배경 로드 가능 여부). 우선 다른 배경 RUID로 교체 시도.
- 몬스터 변형(A) 문제(MONSTER_VARIANTS 사용 중일 때만):
MONSTER_VARIANTS = []로 비우고(B 폴백) Step 1부터 재실행.
-
ui 되돌리기 필요 시:
git checkout map/map02.map후 재생성.
Task 4: 나머지 맵 생성 + SectorConfig 등록
Files: Create map/map03.map~map/map11.map, Modify Global/SectorConfig.config
- Step 1: 전체 생성
cd "C:/Users/jaeoh/Desktop/workspace/slaymaple"
node tools/gen-maps.mjs
Expected: Generated: map02, map03, ... map11 와 SectorConfig entries: 11
- Step 2: 전체 데이터 검증
cd "C:/Users/jaeoh/Desktop/workspace/slaymaple"
node -e "const fs=require('fs');let allIds=new Set(),dup=false,bgs=new Set();for(let n=2;n<=11;n++){const t=String(n).padStart(2,'0');const j=JSON.parse(fs.readFileSync('map/map'+t+'.map','utf8'));const E=j.ContentProto.Entities;if(j.EntryKey!=='map://map'+t)throw new Error('EntryKey '+t);if(E.filter(e=>(e.componentNames||'').includes('script.Monster')).length!==2)throw new Error('monsters '+t);for(const e of E){if(allIds.has(e.id))dup=true;allIds.add(e.id);}bgs.add(E.find(e=>(e.path||'').endsWith('/Background')).jsonString['@components'].find(c=>c['@type']==='MOD.Core.BackgroundComponent').TemplateRUID);}const sec=JSON.parse(fs.readFileSync('Global/SectorConfig.config','utf8'));console.log('cross-map id dup:',dup);console.log('distinct backgrounds:',bgs.size);console.log('sector entries:',sec.ContentProto.Json.Sectors[0].entries.length)"
Expected: cross-map id dup: false, distinct backgrounds: 10, sector entries: 11.
- Step 3: Maker 표본 검증 (컨트롤러)
maker_refresh_workspace 후, 표본 맵 2~3개(map05, map08, map11)를 각각 열어 maker_play→maker_screenshot로 배경이 서로 다르고 몬스터 2마리가 보이는지 확인. 맵 전환이 MCP로 안 되면 사용자에게 해당 맵 열기를 요청. 확인 후 maker_stop.
Task 5: 최종 커밋
Files: map/map02.map~map/map11.map, Global/SectorConfig.config
- Step 1: 커밋
cd "C:/Users/jaeoh/Desktop/workspace/slaymaple"
git add map/map02.map map/map03.map map/map04.map map/map05.map map/map06.map map/map07.map map/map08.map map/map09.map map/map10.map map/map11.map Global/SectorConfig.config
git commit -m "맵 10개(map02~map11) 생성: 랜덤 배경 + 몬스터 2마리, sector 등록"
검증 요약
- 생성기
node --check통과 - map02 스파이크: 데이터(고유 id/2몬스터/배경) + Maker 렌더(배경 상이·몬스터 2)로 A/B 게이트 판정
- 전체: cross-map id 중복 없음, 배경 10종 distinct, sector 11개
- Maker 표본 맵 시각 확인