Files
maplecontest/docs/superpowers/specs/2026-06-10-map-monster-combat-design.md
gahusb da5dd03183 docs(map-monster-combat): 맵 몬스터 카드 전투 설계
추상 단일 적 → 맵 실제 몬스터 멀티 전투(클릭 타겟·각자 HP/의도·전체 처치 시 승리).
컨트롤러 단일 소유 + script.CombatMonster(EnemyId) 매핑 + 월드 HP바 슬롯.

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

119 lines
8.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 맵 몬스터 카드 전투 — 설계
- 날짜: 2026-06-10
- 대상: `tools/deck/gen-slaydeck.mjs`(SlayDeckController + UI), `data/enemies.json`, 신규 `tools/monster/gen-combat-monster.mjs`(+`CombatMonster.codeblock`), 맵 몬스터 엔티티, `tools/balance/sim-balance.mjs`
- 상태: 승인됨 (브레인스토밍 → 본 스펙)
## 1. 배경
현재 카드 전투는 `SlayDeckController` 내부의 **추상 단일 적**으로 동작한다.
- 상태: `EnemyHp`/`EnemyMaxHp`/`EnemyBlock`/`EnemyName`/`EnemyIntents`/`EnemyIntentIndex` (단일 적 1체).
- 공격 카드 → `DealDamageToEnemy(damage)``EnemyHp` 차감.
- `CheckCombatEnd`: `EnemyHp<=0` → 승리(골드·보상·노드/막 진행), `PlayerHp<=0` → 패배.
- 적 데이터는 `data/enemies.json`(slime/elite/boss), 노드의 `CurrentEnemyId`가 어떤 적인지 결정, `Floor` 배율 적용.
- CombatHud는 단일 적 패널(EnemyName/EnemyHp/EnemyBlock/EnemyIntent)을 텍스트로 표시.
맵에는 실제 몬스터 엔티티(`script.Monster` + `script.MonsterAttack`, 예: map01의 주황버섯)가 있으나, 이는 물리 액션 전투용이라 **카드 전투와 완전히 분리**되어 있다.
## 2. 목표
카드 공격이 추상 슬라임이 아니라 **맵의 실제 몬스터**에 적용되고, **맵의 모든 몬스터가 쓰러지면 전투 승리**가 되도록 한다. 승리 이후 흐름(보상·노드·상점·휴식·막/보스)은 기존 런 시스템을 그대로 재사용한다.
## 3. 확정 요구사항 (브레인스토밍 결과)
1. **타겟팅**: 몬스터를 클릭하면 그 몬스터가 "현재 타겟"이 되고, 이후 공격 카드가 그 타겟에 적용된다.
2. **전투원 모델**: 각 몬스터가 개별 HP와 의도(공격/방어)를 가지며, 적 턴에 생존 몬스터가 각자 플레이어를 공격한다(멀티 적).
3. **런 연동**: 전투 노드 = 현재 물리 맵의 몬스터들. 전부 처치 시 노드 클리어 → 기존 보상/맵/상점/막 흐름 유지.
4. **스탯 소스**: 각 맵 몬스터가 적 타입 id를 보유하고 HP/의도를 `data/enemies.json`에서 읽는다. 배율은 **막 배율(필수, 기존 `1+(Floor-1)*0.6` 재사용)** + **노드 타입 배율(선택, 기본 1; 엘리트/보스만 가산)**.
5. **상태 표시**: 각 몬스터 머리 위에 월드(화면) HP바 + 의도 표시.
6. **아키텍처**: 컨트롤러 중심. 전투 상태는 `SlayDeckController`가 단일 소유, 몬스터 엔티티는 타겟·시각(애니메이션) 역할만. 사망 연출은 기존 `script.Monster` 자산 재사용.
7. **몬스터↔적ID**: 전용 경량 스크립트 `script.CombatMonster``EnemyId`(string) 속성으로 명시.
## 4. 데이터 모델
### 4.1 `data/enemies.json`
맵 몬스터용 적 타입을 추가한다(기존 slime/elite/boss는 유지). 예:
```json
"orange_mushroom": {
"name": "주황버섯",
"maxHp": 16,
"intents": [ { "kind": "Attack", "value": 5 }, { "kind": "Defend", "value": 4 } ]
}
```
스탯 수치는 placeholder이며 `sim-balance.mjs`로 추후 튜닝한다.
### 4.2 `script.CombatMonster` (신규 코드블록)
맵 몬스터 엔티티에 부착하는 경량 마커. 속성 `EnemyId`(string) 1개. 런타임 로직 없음(컨트롤러가 읽기만). `script.Monster`를 보유한 엔티티에 함께 부착한다.
### 4.3 런타임 전투 상태 (SlayDeckController)
단일 `Enemy*` 속성군을 제거하고 리스트로 교체한다.
- `Monsters`(any 리스트). 원소: `{ path, enemyId, name, hp, maxHp, block, intents, intentIndex, alive }`.
- `TargetIndex`(number). 현재 선택된 타겟의 `Monsters` 인덱스.
- 상수 `MAX_MONSTERS`(예: 4). UI 슬롯 수 = 이 값. 맵 몬스터가 더 많으면 앞에서 `MAX_MONSTERS`마리만 전투에 참여(초과분은 미지원, 로그 경고).
## 5. 전투 흐름 (SlayDeckController 메서드 변경)
- **StartCombat**:
1. 현재 맵에서 `script.CombatMonster`를 가진 몬스터 엔티티를 스캔(맵 루트 하위, 최대 `MAX_MONSTERS`).
2. 각 몬스터의 `EnemyId``enemies.json` 스탯을 읽어 배율(막 배율 필수 + 노드 타입 배율 선택, §3-4)을 적용해 `Monsters` 리스트 구성.
3. 몬스터 부활: 가시성 on, `StateComponent` IDLE, HP 리셋.
4. 각 몬스터 화면 위치에 UI 슬롯(HP바·의도·타겟버튼) 배치·활성화.
5. `TargetIndex` = 첫 생존자. 손패/턴 시작.
- **SetTarget(i)**: `TargetIndex` 갱신 + 타겟 하이라이트 갱신.
- **PlayCard(Attack)**: `Monsters[TargetIndex]`에 방어도→HP 차감. HP≤0 → 해당 몬스터 사망 처리(§6), UI 슬롯 숨김, 자동으로 다음 생존 타겟 선택. 이후 `CheckCombatEnd`.
- **PlayCard(Skill)**: 기존대로 플레이어 방어도 증가(변경 없음).
- **EnemyTurn**: 생존 몬스터 각자 `intentIndex` 진행 — Attack→`DealDamageToPlayer`, Defend→자기 `block` 증가. (각 몬스터는 독립 의도 사이클)
- **CheckCombatEnd**: 생존 몬스터 0 → 승리(기존 보상/노드/막 분기 재사용). `PlayerHp<=0` → 패배.
- **RenderCombat**: 각 생존 몬스터 UI 슬롯의 HP바·의도 갱신, 플레이어 패널 갱신. 기존 단일 적 패널(EnemyName/EnemyHp/EnemyIntent)은 제거 또는 숨김.
## 6. 사망 / 부활 연출
컨트롤러가 직접 관리하여 **노드 간 몬스터 영속**(엔티티 Destroy 안 함):
- 사망: 타겟 몬스터의 `StateComponent`를 DEAD로 전환(die 애니메이션) 후 짧은 지연 뒤 가시성 off. `alive=false`.
- 부활: `StartCombat`에서 가시성 on, IDLE 상태, HP 리셋.
기존 `Monster.codeblock`의 hit/die 애니메이션 자산을 활용하되, Destroy/Respawn 타이머에 의존하지 않고 컨트롤러가 생사 시점을 통제한다.
## 7. UI — 몬스터 슬롯 (DefaultGroup.ui)
카메라가 고정(MapCamera)이라 몬스터의 화면상 위치가 불변 → UI 슬롯을 전투 시작 시 한 번 배치하면 된다.
- `gen-slaydeck.mjs``CombatHud` 아래 **MonsterSlot ×MAX_MONSTERS**를 사전 생성(평소 비활성):
- HP바 스프라이트(배경+채움), 의도 텍스트, 투명 타겟 버튼(클릭→`SetTarget`).
- **위치 결정**: 런타임 world→screen 변환을 우선 시도(카메라 고정이므로 전투 시작 시 1회 계산). 변환 API가 여의치 않으면 **`data`에 슬롯 화면좌표를 명시**(현재 map01 몬스터 배치 기준)하는 폴백을 사용한다. → 구현 단계에서 변환 가용성 검증.
## 8. 변경 파일 요약
| 파일 | 변경 |
|------|------|
| `data/enemies.json` | 맵 몬스터 적 타입 추가(orange_mushroom 등) |
| 신규 `tools/monster/gen-combat-monster.mjs` | `CombatMonster.codeblock` 생성 + 11개 맵 몬스터 엔티티에 `script.CombatMonster`(EnemyId) 부착(idempotent) |
| `RootDesk/MyDesk/CombatMonster.codeblock` | 신규 생성물 |
| `tools/deck/gen-slaydeck.mjs` | 전투 멀티 몬스터화(상태·PlayCard·EnemyTurn·CheckCombatEnd·RenderCombat·StartCombat·타겟 바인딩) + UI 몬스터 슬롯 생성 |
| `tools/balance/sim-balance.mjs` | 멀티 몬스터 규칙으로 동기화 |
| `map/map01.map`~`map11.map` | 몬스터 엔티티에 `script.CombatMonster` 부착(생성기 재실행 산출) |
| `ui/DefaultGroup.ui` | 몬스터 슬롯 추가(생성기 산출) |
## 9. 알려진 한계 (MVP)
- 모든 전투 노드가 같은 물리 맵 몬스터를 재사용한다(막 배율로 난이도 차등). 노드별 다른 적 구성/맵 이동은 후속 과제.
- `MAX_MONSTERS` 초과 몬스터는 전투에 미참여.
- 보스 노드도 동일 맵 몬스터를 사용(테마 불일치)는 후속 콘텐츠 확장에서 해결.
## 10. 리스크
- **world→screen 변환 가용성**: 미지원 시 슬롯 좌표 데이터 폴백으로 대응(§7).
- **외부 엔티티 스크립트 메서드/상태 접근**: 컨트롤러가 몬스터 엔티티의 `StateComponent`·가시성을 제어할 수 있어야 함(구현 단계 검증).
- **생성물 단일 소스 유지**: 전투/HUD 산출물은 `gen-slaydeck.mjs`에서만 생성(직접 편집 금지) 규칙 유지.
## 11. 검증
- `node tools/balance/sim-balance.test.mjs` 통과 + 멀티 몬스터 규칙 반영.
- 생성기 2회 실행 결과 동일(결정적).
- 메이커 플레이: 카드로 특정 몬스터 타겟 공격 → HP 감소·사망 애니 → 전체 처치 시 승리 → 기존 보상/노드 흐름 진입. 적 턴에 생존 몬스터가 플레이어 공격.