공식 필드맵 import로 몬스터 변형 9종·타일셋 12종 수확. map01 기존 4종 미사용. 각 맵: 서로 다른 몬스터 2마리(x=3.5/5.5 우측), 맵별 다른 타일셋, 기존 배경 유지. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
12 KiB
맵 개선(다양한 몬스터 + 타일셋 + 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. - 배경: 기존
BACKGROUNDS10종 유지. - 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 결과):
// 공식 맵에서 수확한 타일셋 RUID 10종 (맵마다 다르게). map01 기본(9dfea380…) 제외.
const TILESETS = [
// Task 1에서 수확한 10개 RUID
];
- Step 2: MONSTER_VARIANTS 채우기
기존 const MONSTER_VARIANTS = []; 를 Task 1에서 수확한 ≥12종으로 교체:
// 공식 맵에서 수확한 몬스터 변형 (기존 map01 4종 미사용).
const MONSTER_VARIANTS = [
// { sprite: '...', stand: '...', hit: '...', die: '...' }, ... (≥12종)
];
- Step 3: 몬스터 배치 로직 교체 (서로 다른 2종 + StS2 + 정적 베이스)
buildMap 안의 몬스터 추가 루프(const ents = ... 이후 for (let i = 0; i < 2; i++) { ... } 블록 전체)를 다음으로 교체:
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) { ... } 안, 배경 설정 블록 다음에 추가:
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: 구문 확인 + 커밋
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를 깨끗이 재생성:
cd "C:/Users/jaeoh/Desktop/workspace/slaymaple"
git checkout map/map01.map # 혹시 모를 보호(템플릿). map01은 변경 대상 아님
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;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 렌더 검증 (컨트롤러)
maker_refresh_workspace- map02가 활성인지 확인(
maker_get_current_map). 아니면 사용자에게 map02 열기 요청. maker_play→maker_screenshot→ Read로 확인: 몬스터 2마리가 수확된(기존과 다른) 외형으로 우측에 보이고, 타일 텍스처가 바뀌었는지.maker_execute_script(client)로 확인:→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)) endmaker_logs(normal)로 sprite/x 확인.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: 전체 재생성
cd "C:/Users/jaeoh/Desktop/workspace/slaymaple"
node tools/gen-maps.mjs
Expected: Generated: map02 … map11, SectorConfig entries: 11.
- Step 2: 전체 데이터 검증
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: 커밋
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 표본 시각 확인