Files
maplecontest/docs/superpowers/plans/2026-06-06-maps-monsters-tiles.md
gahusb dd5acafab4 맵10개 개선: 수확한 다양한 몬스터 2종(StS2 우측 배치) + 맵별 타일셋
공식 필드맵 import로 몬스터 변형 9종·타일셋 12종 수확. map01 기존 4종 미사용.
각 맵: 서로 다른 몬스터 2마리(x=3.5/5.5 우측), 맵별 다른 타일셋, 기존 배경 유지.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 14:09:45 +09:00

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.mjsMONSTER_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.mjsMONSTER_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_savemap/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 렌더 검증 (컨트롤러)
  1. maker_refresh_workspace
  2. map02가 활성인지 확인(maker_get_current_map). 아니면 사용자에게 map02 열기 요청.
  3. maker_playmaker_screenshot → Read로 확인: 몬스터 2마리가 수확된(기존과 다른) 외형으로 우측에 보이고, 타일 텍스처가 바뀌었는지.
  4. 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)) 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: 전체 재생성
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_playmaker_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 표본 시각 확인