# 맵 개선(다양한 몬스터 + 타일셋 + StS2 배치) 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:** map02~map11에 공식 맵에서 수확한 다양한 몬스터 2종(기존 4종 미사용)을 StS2 우측 배치로, 맵마다 다른 타일셋으로 재생성한다. **Architecture:** 공식 맵 import로 몬스터 변형 `{sprite,stand,hit,die}`과 타일셋 RUID를 수확(배경 수확과 동일 기법) → `tools/gen-maps.mjs`의 `MONSTER_VARIANTS`/`TILESETS`에 반영 → 몬스터 선택을 "서로 다른 2종 + 정적 베이스 + StS2 우측 고정위치"로, TileSetRUID를 맵별로 교체 → map02~map11 재생성. map02 스파이크로 렌더 검증 후 확대. **Tech Stack:** Node.js, MSW `.map` JSON, msw-maker-mcp(import/save/play/screenshot/execute_script), msw-mcp. --- ## File Structure - Modify: `tools/gen-maps.mjs` — `MONSTER_VARIANTS`/`TILESETS` 데이터 + 몬스터 선택/배치 로직 + TileSetRUID 교체. - Modify(재생성): `map/map02.map`~`map11.map`. 기준 사실: - 몬스터 엔티티: `SpriteRendererComponent.SpriteRUID` + `StateAnimationComponent.ActionSheet{stand,hit,die}`. 정적 베이스로 쓸 템플릿은 path에 `Static` 포함(StaticMonsterTemplate, 배회 안 함). - 타일: 맵의 `/TileMap` 엔티티 `TileMapComponent.TileSetRUID.DataId`. map01 기본 `9dfea3808bbd49a5877d8624df21b1c7`. - 배경: 기존 `BACKGROUNDS` 10종 유지. - import는 현재 맵(map02, 재생성 가능)을 교체 → save → 파일에서 추출. --- ### Task 1: 몬스터 변형 + 타일셋 수확 (컨트롤러/MCP, 스파이크 포함) **목표:** `MONSTER_VARIANTS`(≥12종 `{sprite,stand,hit,die}`) + `TILESETS`(10종 RUID) 확정. - [ ] **Step 1: 몬스터 엔티티 구조 스파이크** 몬스터가 있는 공식 **필드맵** 1개를 import(`maker_import_maplestory_map`) → `maker_save` → `map/map02.map`에서 `script.Monster`를 포함하는 엔티티를 찾아 `SpriteRendererComponent.SpriteRUID` + `StateAnimationComponent.ActionSheet`(stand/hit/die)가 존재하는지 확인. - 존재 → 그 형태로 변형 추출. - 부재(구조 다름) → 폴백: `SpriteRUID`만 추출하고 `ActionSheet`는 map01 템플릿 유지(생성기에서 변형에 stand/hit/die가 없으면 ActionSheet 미변경하도록 처리). 필드맵 후보 id는 `maker_list_maplestory_maps`로 탐색(영문/지역명). 몬스터가 있는 사냥/필드맵을 고른다. - [ ] **Step 2: 변형 ≥12종 수확** 필드맵 여러 개를 import→save→추출 반복. 각 맵의 몬스터 엔티티들에서 `{sprite, stand, hit, die}`를 모아 **중복 sprite 제거**해 ≥12종 확보. map01의 4종 sprite(`8ef238e0…`,`6c7130f5…`,`3e76c89a…`,`6d381bea…`,`c96c11f9…`)는 **제외**. - [ ] **Step 3: 타일셋 10종 수확** import한 맵들의 `TileMapComponent.TileSetRUID.DataId`를 수집해 **distinct 10종**(map01의 `9dfea380…` 제외). (배경 수확 때처럼 import 1회로 타일셋+몬스터 동시 수확 가능) - [ ] **Step 4: 결과 정리** `MONSTER_VARIANTS = [{sprite,stand,hit,die}, ...]`(≥12)와 `TILESETS = [ruid, ...]`(10)를 Task 2에 넘길 형태로 기록. (코드 변경 없음; 데이터 산출) --- ### Task 2: 생성기 로직·데이터 갱신 **Files:** Modify `tools/gen-maps.mjs` - [ ] **Step 1: TILESETS 상수 추가** `BACKGROUNDS = [...]` 정의 바로 아래에 추가(값은 Task 1 결과): ```js // 공식 맵에서 수확한 타일셋 RUID 10종 (맵마다 다르게). map01 기본(9dfea380…) 제외. const TILESETS = [ // Task 1에서 수확한 10개 RUID ]; ``` - [ ] **Step 2: MONSTER_VARIANTS 채우기** 기존 `const MONSTER_VARIANTS = [];` 를 Task 1에서 수확한 ≥12종으로 교체: ```js // 공식 맵에서 수확한 몬스터 변형 (기존 map01 4종 미사용). const MONSTER_VARIANTS = [ // { sprite: '...', stand: '...', hit: '...', die: '...' }, ... (≥12종) ]; ``` - [ ] **Step 3: 몬스터 배치 로직 교체 (서로 다른 2종 + StS2 + 정적 베이스)** `buildMap` 안의 몬스터 추가 루프(`const ents = ...` 이후 `for (let i = 0; i < 2; i++) { ... }` 블록 전체)를 다음으로 교체: ```js 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); } ``` (`v.stand`가 없으면 ActionSheet를 유지 → 폴백 호환) - [ ] **Step 4: TileSetRUID 교체 추가** `buildMap`의 경로/배경 설정 루프 `for (const e of ents) { ... }` 안, 배경 설정 블록 다음에 추가: ```js 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] }; } ``` - [ ] **Step 5: 구문 확인 + 커밋** ```bash cd "C:/Users/jaeoh/Desktop/workspace/slaymaple" node --check tools/gen-maps.mjs git add tools/gen-maps.mjs git commit -m "맵 생성기: 수확한 다양한 몬스터 2종(StS2 배치) + 맵별 타일셋 교체" ``` --- ### Task 3: map02 스파이크 — 재생성 + Maker 검증 **Files:** Modify `map/map02.map` - [ ] **Step 1: map02 재생성** 수확 import로 오염된 map02를 깨끗이 재생성: ```bash cd "C:/Users/jaeoh/Desktop/workspace/slaymaple" git checkout map/map01.map # 혹시 모를 보호(템플릿). map01은 변경 대상 아님 node tools/gen-maps.mjs 2 ``` Expected: `Generated: map02` - [ ] **Step 2: 데이터 검증** ```bash 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;const ms=E.filter(e=>(e.componentNames||'').includes('script.Monster'));const old=['8ef238e0d0ca4bb783aca526cff35d11','6c7130f51a654803a1c39cbe30e2f427','3e76c89ae8e7477ca871f5bbcd6f6f29','6d381bea1bcb4504b518a1fbfa0904ac','c96c11f9a3f845a4b6a27d9ca10ab103'];const sprs=ms.map(m=>m.jsonString['@components'].find(c=>c['@type']==='MOD.Core.SpriteRendererComponent').SpriteRUID);const xs=ms.map(m=>m.jsonString['@components'].find(c=>c['@type']==='MOD.Core.TransformComponent').Position.x);const tm=E.find(e=>(e.path||'').endsWith('/TileMap')).jsonString['@components'].find(c=>c['@type']==='MOD.Core.TileMapComponent').TileSetRUID.DataId;console.log('monsters:',ms.length);console.log('sprites:',sprs.join(','));console.log('distinct sprites:',new Set(sprs).size===2);console.log('no old sprite:',sprs.every(s=>!old.includes(s)));console.log('positions x:',xs.join(','));console.log('tileset:',tm,'changed:',tm!=='9dfea3808bbd49a5877d8624df21b1c7')" ``` Expected: `monsters: 2`, 2개 sprite distinct, `no old sprite: true`, positions x = `3.5,5.5`, tileset이 `9dfea380…`이 아님(교체됨). - [ ] **Step 3: Maker 렌더 검증 (컨트롤러)** 1. `maker_refresh_workspace` 2. map02가 활성인지 확인(`maker_get_current_map`). 아니면 사용자에게 map02 열기 요청. 3. `maker_play` → `maker_screenshot` → Read로 확인: 몬스터 2마리가 **수확된(기존과 다른) 외형**으로 **우측에** 보이고, **타일 텍스처가 바뀌었는지**. 4. `maker_execute_script`(client)로 확인: ```lua local m1=_EntityService:GetEntityByPath("/maps/map02/Monster1") local m2=_EntityService:GetEntityByPath("/maps/map02/Monster2") if m1 then log("M1 spr="..tostring(m1.SpriteRendererComponent.SpriteRUID).." x="..tostring(m1.TransformComponent.Position.x)) end if m2 then log("M2 spr="..tostring(m2.SpriteRendererComponent.SpriteRUID).." x="..tostring(m2.TransformComponent.Position.x)) end ``` → `maker_logs(normal)`로 sprite/x 확인. 5. `maker_stop`. - [ ] **Step 4: 게이트 판정** - 몬스터 외형 변경 + 우측 배치 + 타일 변경 정상 → Task 4. - 몬스터가 흰박스/안 보임 → 변형 sprite/animation 로드 문제 → Task 1 폴백(SpriteRUID만, ActionSheet 유지) 적용 후 재생성. - 타일이 깨져 보임 → 해당 타일셋 제외하거나 호환 타일셋으로 교체(`TILESETS` 조정) 후 재생성. --- ### Task 4: 전체 재생성 + 검증 **Files:** Modify `map/map02.map`~`map11.map`, `Global/SectorConfig.config` - [ ] **Step 1: 전체 재생성** ```bash cd "C:/Users/jaeoh/Desktop/workspace/slaymaple" node tools/gen-maps.mjs ``` Expected: `Generated: map02 … map11`, `SectorConfig entries: 11`. - [ ] **Step 2: 전체 데이터 검증** ```bash cd "C:/Users/jaeoh/Desktop/workspace/slaymaple" node -e "const fs=require('fs');const old=['8ef238e0d0ca4bb783aca526cff35d11','6c7130f51a654803a1c39cbe30e2f427','3e76c89ae8e7477ca871f5bbcd6f6f29','6d381bea1bcb4504b518a1fbfa0904ac','c96c11f9a3f845a4b6a27d9ca10ab103'];let ids=new Set(),dup=false,ts=new Set(),bad=false;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;const ms=E.filter(e=>(e.componentNames||'').includes('script.Monster'));if(ms.length!==2)throw new Error('monsters '+t);const sprs=ms.map(m=>m.jsonString['@components'].find(c=>c['@type']==='MOD.Core.SpriteRendererComponent').SpriteRUID);if(new Set(sprs).size!==2)bad=true;if(sprs.some(s=>old.includes(s)))bad=true;ts.add(E.find(e=>(e.path||'').endsWith('/TileMap')).jsonString['@components'].find(c=>c['@type']==='MOD.Core.TileMapComponent').TileSetRUID.DataId);for(const e of E){if(ids.has(e.id))dup=true;ids.add(e.id);}}console.log('cross-map id dup:',dup);console.log('any old/dup-in-map sprite:',bad);console.log('distinct tilesets:',ts.size)" ``` Expected: `cross-map id dup: false`, `any old/dup-in-map sprite: false`, `distinct tilesets: 10`. - [ ] **Step 3: Maker 표본 검증 (컨트롤러)** `maker_refresh_workspace` 후 표본 맵(map05, map09)을 각각 열어(사용자 협조) `maker_play`→`maker_screenshot`로 몬스터 외형·타일이 맵마다 다른지 확인. `maker_stop`. --- ### Task 5: 최종 커밋 - [ ] **Step 1: 커밋** ```bash cd "C:/Users/jaeoh/Desktop/workspace/slaymaple" git add tools/gen-maps.mjs Global/SectorConfig.config 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 git commit -m "맵 10개: 다양한 몬스터 2종(StS2 우측 배치) + 맵별 타일셋 적용" ``` --- ## 검증 요약 - 수확: 몬스터 변형 ≥12 / 타일셋 10 (스파이크로 구조 확인) - map02 스파이크: 데이터(2 distinct sprite·old 미사용·x=3.5/5.5·타일셋 교체) + Maker 렌더 - 전체: cross-map id 무중복, old sprite 미사용, 타일셋 10 distinct - Maker 표본 시각 확인