diff --git a/docs/superpowers/specs/2026-06-10-node-type-monster-groups-design.md b/docs/superpowers/specs/2026-06-10-node-type-monster-groups-design.md new file mode 100644 index 0000000..7d552a1 --- /dev/null +++ b/docs/superpowers/specs/2026-06-10-node-type-monster-groups-design.md @@ -0,0 +1,103 @@ +# 노드 타입별 몬스터 그룹 — 설계 + +- 날짜: 2026-06-10 +- 대상: `RootDesk/MyDesk/CombatMonster.codeblock`(+`tools/monster/gen-combat-monster.mjs`), `tools/deck/gen-slaydeck.mjs`(SlayDeckController), `data/monster-slots.json`, map 몬스터 엔티티(메이커 저작) +- 상태: 승인됨 (브레인스토밍 → 본 스펙) + +## 1. 배경 + +맵 몬스터 카드 전투가 구현돼 있다. 현재 `BuildMonsters`는 맵에 등록된 `script.CombatMonster` 몬스터를 **노드 타입과 무관하게 전부** 전투에 투입한다. 그래서 일반/엘리트/보스 노드를 어디로 가든 같은 몬스터와 싸운다. + +흐름: `PickNode(id)` → `self.CurrentNodeId = id`, `self.CurrentEnemyId = node.enemy`, `StartCombat()` → `BuildMonsters()`. 노드 타입은 `self.MapNodes[self.CurrentNodeId].type`(`combat`/`elite`/`boss`/`shop`/`rest`)로 접근 가능. + +## 2. 목표 + +한 맵 안에서 **노드 타입에 따라 다른 몬스터 그룹**이 등장하도록 한다. +- 일반(`combat`) 노드 → 일반 몬스터 그룹 +- 엘리트(`elite`) 노드 → 엘리트(+졸개) 그룹 +- 보스(`boss`) 노드 → 보스(+졸개) 그룹 + +## 3. 확정 요구사항 (브레인스토밍 결과) + +1. **구성**: 노드 타입별 그룹. 엘리트/보스 그룹은 졸개(일반 몬스터)를 포함할 수 있다. 그룹당 몬스터 수는 `MAX_MONSTERS`(4) 이하. +2. **선택 단위**: 노드 **타입**(모든 `combat` 노드는 동일한 일반 그룹, `elite`는 엘리트 그룹…). 노드별 개별 구성은 후속. +3. **배치**: 세 그룹을 맵 내 **서로 다른 위치**에 배치. HP바 슬롯 좌표는 **그룹별**로 둔다. +4. **메커니즘**: 각 몬스터에 `Group` 태그 + `BuildMonsters`에서 현재 노드 타입으로 필터. (MSW Layer는 렌더 z-순서용이라 부적합 — 사용 안 함) +5. **저작**: 각 몬스터의 `Group`/`EnemyId`는 **메이커 인스펙터**에서 직접 설정. 생성기는 컴포넌트 존재만 보장하고 사용자 설정 값을 덮어쓰지 않는다. + +## 4. 데이터·컴포넌트 변경 + +### 4.1 `script.CombatMonster` (codeblock) +- 속성 추가: `Group`(string, 기본 `"combat"`). 기존 `EnemyId`(string)·`RegTries`(number) 유지. +- `OnBeginPlay`: 등록 호출에 Group 추가 → `c.SlayDeckController:RegisterMonster(self.Entity, self.EnemyId, self.Group)`. + +### 4.2 `tools/monster/gen-combat-monster.mjs` (클로버 금지로 변경) +- `script.Monster` 엔티티에 `script.CombatMonster`가 **없을 때만** 부착(기본값 `Group="combat"`, `EnemyId=DEFAULT_ENEMY`). +- 이미 `script.CombatMonster`가 있으면 **그 인스턴스의 Group/EnemyId 값을 보존**(필터-후-재삽입으로 값을 날리지 않음). `Enable`·componentNames만 정합 유지. +- 결과: 신규 배치 몬스터엔 컴포넌트가 자동 생기고, 메이커에서 설정한 값은 유지된다. + +### 4.3 `data/monster-slots.json` (그룹별 좌표) +평면 배열 → 그룹 키 객체: +```json +{ + "combat": [ { "x": 430, "y": 140 }, ... ], + "elite": [ ... ], + "boss": [ ... ] +} +``` +각 배열 길이는 해당 그룹의 몬스터 수(≤ MAX_MONSTERS)를 커버. 좌표는 플레이테스트로 튜닝. + +### 4.4 런타임 상태 (SlayDeckController) +- `Registered` 원소: `{ entity, enemyId, group }` (group 추가). +- `SlotPos`: 그룹별 좌표 테이블(`SlotPos.combat`/`.elite`/`.boss`)로 주입(StartRun). + +## 5. 런타임 흐름 + +- **RegisterMonster(monster, enemyId, group)**: `Registered`에 `{entity, enemyId, group}` append. +- **BuildMonsters**: + 1. `local g = self.MapNodes[self.CurrentNodeId].type` (combat/elite/boss) + 2. 등록된 모든 몬스터 엔티티 **숨김**(`SetVisible(false)`). + 3. `r.group == g`인 항목만 추려 월드 x 정렬 → 최대 `MAX_MONSTERS`. + 4. 각 몬스터: `enemies.json[enemyId]` 스탯 + 막 배율(`1+(Floor-1)*0.6`)로 `Monsters[i]` 구성, `ReviveMonsterEntity`(표시), `PositionMonsterSlot(i)`. + 5. 슬롯 좌표는 `SlotPos[g]` 사용. + 6. `TargetIndex = 1`. +- **PositionMonsterSlot(slot)**: 현재 그룹 좌표(`self.ActiveSlotPos` 또는 `SlotPos[g]`)에서 위치 설정. (BuildMonsters가 현재 그룹 좌표를 임시 보관) +- 나머지(SetTarget·PlayCard·DealDamageToTarget·KillMonster·EnemyTurn·CheckCombatEnd·RenderCombat)는 **변경 없음**. + +> 구현 메모: `PositionMonsterSlot`이 그룹 좌표를 알 수 있도록, BuildMonsters에서 `self.ActiveSlotPos = self.SlotPos[g]`를 설정하고 PositionMonsterSlot은 `self.ActiveSlotPos[slot]`을 참조한다. + +## 6. 저작 워크플로 (메이커) + +1. map01에 일반/엘리트/보스 몬스터를 서로 다른 위치에 배치(엘리트/보스 그룹은 졸개 포함 가능). +2. 각 몬스터의 `CombatMonster`에서 `Group`(combat/elite/boss)·`EnemyId`(enemies.json id) 지정. +3. `data/monster-slots.json`에 그룹별 슬롯 좌표 입력. +4. `node tools/monster/gen-combat-monster.mjs` → `node tools/deck/gen-slaydeck.mjs` → 메이커 reload. + +## 7. 변경 파일 요약 + +| 파일 | 변경 | +|------|------| +| `tools/monster/gen-combat-monster.mjs` | CombatMonster에 `Group` 프로퍼티 추가, 부착을 **없을 때만**(값 보존) 으로 변경, OnBeginPlay 등록에 Group 전달 | +| `RootDesk/MyDesk/CombatMonster.codeblock` | 생성물(Group 프로퍼티·등록 인자) | +| `data/monster-slots.json` | 그룹별 좌표 구조로 변경 | +| `tools/deck/gen-slaydeck.mjs` | `RegisterMonster`(group 인자)·`BuildMonsters`(노드 타입 필터·전체 숨김·그룹 슬롯)·`SlotPos` 주입·`PositionMonsterSlot`(활성 그룹 좌표) | +| 생성물 | `SlayDeckController.codeblock` 재생성. `ui/DefaultGroup.ui`는 **변경 없음**(기존 4개 슬롯을 그룹 간 재사용, 좌표만 런타임 변경) | +| `map/map01.map` | 그룹별 몬스터 배치·CombatMonster 태그(메이커 저작) | + +## 8. 알려진 한계 + +- 노드 타입 단위 구성(노드별 개별 인카운터 아님). +- 모든 그룹 합산이 많아도 UI 슬롯은 `MAX_MONSTERS`(4) 동시 표시 한도. 그룹당 ≤4. +- 전투 외(맵 UI 오버레이) 구간엔 필드에 세 그룹이 모두 보일 수 있음(허용). 원하면 StartRun에서 전체 숨김(후속). + +## 9. 리스크 + +- `gen-combat-monster`의 "값 보존" 로직: 기존 인스턴스 값을 정확히 유지하면서 componentNames 정합을 깨지 않도록 주의. +- 그룹 슬롯 좌표 미설정 시 기본 폴백 필요(좌표 없으면 슬롯 위치 미변경). +- 보스 노드 클리어 시 막 진행 로직(`CheckCombatEnd`)은 기존 그대로 동작해야 함(그룹 변경과 독립). + +## 10. 검증 + +- 생성기 2회 실행 결과 동일(결정적), JSON 유효·중복 id 없음. +- 메이커 플레이테스트: combat 노드 → 일반 그룹만, elite 노드 → 엘리트(+졸개)만, boss 노드 → 보스(+졸개)만 등장. 비활성 그룹은 숨김. 각 그룹 슬롯이 해당 몬스터 위에 표시. +- sim 테스트는 기존대로 통과(규칙 불변).