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

218 lines
12 KiB
Markdown

# 맵 개선(다양한 몬스터 + 타일셋 + 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 표본 시각 확인