# 08. 전투시스템 SOT (Single Source of Truth) v1

- 작성일: 2026-04-14
- 작성자: 개발실장 (Explore 에이전트 분석 위임)
- 목적: 기획실 밸런싱·시뮬레이터 검증을 지원하기 위해 **코드가 실제로 수행하는 전투 로직**을 SOT로 확정
- 스코프: `D:/NerdNavis/FilGoodBandits/DeckBuilding/Assets/Script/` 전수
- 원칙: 코드가 말하는 것만 기록. 추측 금지. 불확실한 항목은 **(확인 필요)** 태그 명시

---

## 1. 전투 시스템의 파일 맵

### 1.1 핵심 매니저 및 컨트롤러

| 파일경로 | 클래스명 | 역할 |
|---|---|---|
| `Assets/Script/InGame/Stage/MonsterNodeControler.cs` | `MonsterNodeControler` | **전투 루프 제어 중심** — 몬스터 배치, 라인 변경, 노드 종료 |
| `Assets/Script/InGame/Actor/Actor.cs` | `Actor` (기본 클래스) | 피해 계산, 회피, 크리티컬, 상태이상 (약 3,700줄) |
| `Assets/Script/InGame/Actor/PCActor.cs` | `PCActor` | 플레이어 전용 — 방어 입력, HUD 관리 |
| `Assets/Script/InGame/Actor/MobActor.cs` | `MobActor` | 몬스터 전용 — 라인 위치, 드롭 보상 |

### 1.2 핵심 데이터 타입

| 파일:라인 | 타입 | 설명 |
|---|---|---|
| `My/MyClass.cs:339` | `ProjectileData` | 발사체/공격 정보 (데미지, 크리, 특수효과) |
| `My/MyValue.cs:517` | `ActorStatInfo` | 배우 스탯 (HP, 공격력, 회피율 등) |
| `InGame/Stage/IngameStageData.cs:63` | `StageNodeData` | 노드 데이터 (몬스터 구성) |
| `Table/Tables/table_monsterlist.cs:8` | `MonsterListTableData` | 몬스터 테이블 (Normal/Boss 구분) |

### 1.3 전투 씬 진입/종료 경로

- **진입**: `MonsterNodeControler.Set(endnode, stagedata)` (`MonsterNodeControler.cs:42`)
  - 몬스터 생성: `MonsterNodeControler.cs:92~118`
  - 배우 초기화: `MonsterNodeControler.cs:142~168`
  - 전투 시작: `MonsterNodeControler.cs:170` → `InGameInfo.Ins.BattleStart_AfterMakeMob()`
- **종료**:
  - 몬스터 전멸: `Co_AllKill()` (`MonsterNodeControler.cs:334~343`)
  - 라인 변경: `Co_LineChange()` (`MonsterNodeControler.cs:362~450`)

---

## 2. 전투 라이프사이클

### 2.1 흐름

```
1. 노드 진입: MonsterNodeControler.Set(endnode, stagedata)
2. 패턴 선택: table_MonsterPatternList (PatternID 기반)
3. 몬스터 동적 생성: Get_MakeMob_Dynamic()  [cs:198~255]
   - 라인(전열/중열/대기열) × 슬롯(3) = 9슬롯
   - 슬롯별 근접/원거리/고유 확률 추첨
4. 배우 배치: Actor 9개 (0~8: 전열 3, 중열 3, 대기열 3)
5. 전투 루프: Actor.Update()  [Actor.cs:79~111]
   - AttackCoolTime 감소 → 0 도달 시 Play_Attack()
   - 상태이상·카드 효과 업데이트
6. 공격 발동: Actor.Shoot_Projectile()  [Actor.cs:946~1039]
7. 피해 계산: Actor.Get_Dmg(ProjectileData)  [Actor.cs:521~834]
8. 라인 전멸 체크: Check_LineAllDead()  [Actor.cs:480]
   - 라인 변경: Co_LineChange()
   - 전원 처치: Co_AllKill() → act_EndNode()
```

### 2.2 주요 이벤트 / 콜백

| 이벤트 | 함수 | 시점 |
|---|---|---|
| 피해 수신 | `Actor.Get_Dmg(ProjectileData)` `Actor.cs:521` | 발사체 도달 |
| 회피 판정 | `Run_AvoidStatus()` `Actor.cs:1640` | 회피 성공 |
| 크리티컬 결정 | `ProjectileData.Set()` `MyClass.cs:354` | 발사체 생성 |
| 사망 | `Actor.Set_Die(ProjectileData)` `Actor.cs:876` | HP ≤ 0 |
| 부활 | `Co_Resurrection()` `Actor.cs:926` | 부활 카드/성소 |
| 라인 변경 | `Co_LineChange()` `MonsterNodeControler.cs:362` | 라인 전멸 |
| 노드 종료 | `act_EndNode()` `MonsterNodeControler.cs:456` | 전원 처치 |

---

## 3. 공격/피해 계산 수식

### 3.1 기본 공격력

`MyValue.cs:800~804`
```csharp
public int Get_Damage()
{
    var dmg = Random.Range((int)Get_TotalStat(eStat.Attack_Min),
                           (int)Get_TotalStat(eStat.Attack_Max) + 1);
    return Mathf.Max(dmg, 1); // 최소 1 보장
}
```

**Attack_Min 최종값** (`MyValue.cs:585~603`)
```csharp
case eStat.Attack_Min:
{
    rtn += Get_Stat(eStat.Attack) + Get_BuffStat(eStat.Attack) - Get_DebuffStat(eStat.Attack);

    // G5_AttackUpBySkillCardCount: 스킬카드 수만큼 증가
    if (m_Actor.IsObtainCardSkill(cardtype))
        rtn += m_Actor.Get_SkillCardCount();

    // G5_FlatDamageMinMaxEqual: 최소/최대 동일화
    if (m_Actor.IsObtainCardSkill(cardtype))
    {
        var maxdmg = Get_Stat(eStat.Attack_Max) + Get_BuffStat(eStat.Attack_Max)
                   - Get_DebuffStat(eStat.Attack_Max);
        rtn = Mathf.Max((int)rtn, (int)maxdmg);
    }

    var mul = Get_BuffStat(eStat.DmgMul)
            + Get_BuffStat(eStat.Attack_Min_Mul)
            + Get_BuffStat(eStat.PC5_DmgMul);
    return (int)(rtn + (rtn * mul));
}
```

**기여도 순서** (카드 > 장비 > 각성 > 인장 원칙과 매핑):
1. 기본값: `ActorTableDataBase.n_AttackMin / n_AttackMax`
2. 장비: `ApplyMainStat()` / `ApplyStat()` (`MyValue.cs:261~282`)
3. 각성: `eAwakening.Stat_ADD`, `eUniqueAwakeningType.PC1_UniqueAwakening1~5` (`MyValue.cs:156~217`)
4. 인장: `eSpecialEffect.CritChance`, `CritDamage` 등 (`MyValue.cs:145~154`)
5. 런 내 버프/디버프: `Set_Buff()` / `Set_Debuff()` (`MyValue.cs:747~792`)

### 3.2 최종 피해 계산 (15단계)

`Actor.cs:521~834` 순서대로 요약.

1. **기본**: `baseDmg = hitterstat.Get_Damage()`
2. **1회성 절대 증가**: `AddDmg_1Time` + 인장 절대값
3. **1회성 배수 증가**: `AddDmgMul_1Time` + 인장 배수
4. **스턴/라인 보정**: `DmgMul_byStun`, `FrontLine_Dmg_Mul_1Time`, `MiddleLine_Dmg_Mul_1Time`, `BackLine_Dmg_Mul_1Time`
5. **G3_PotionNextAttackBoost**: 물약 스택 소모 시 배수 추가
6. **G5_FirstAttackTripleDamage**: 첫 공격이면 배수를 해당 값으로 **대체**(`Actor.cs:719~724`)
7. **곱연산 합산**: `hitter_dmg = base + 절대 + (hitter_dmg * 배수)` (`Actor.cs:726`)
8. **크리티컬**: `hitter_dmg *= CriDmg` (크리 시) (`Actor.cs:758`)
9. **절대 감소**: `ReduceMeeleDmg` / `ReduceRangeDmg` / `ReduceDmg` + 방어 중이면 `PCDefence` (`Actor.cs:776`)
10. **비율 감소**: `ReduceMeeleDmg_Mul` / `ReduceRangeDmg_Mul` + 방어 중이면 `PCDefence_Mul`, `G2_ReduceRangedDamageWithShield` (`Actor.cs:808`)
11. **최소 피해 1**: `hitter_dmg = max(hitter_dmg, 1)` (`Actor.cs:811`)
12. **G4_ShieldedDamageReduction**: 쉴드 보유 시 비율 추가 감소
13. **FixedDmg**: 고정 피해 스탯이 있으면 피해를 해당 값으로 **대체** (`Actor.cs:817`)
14. **G2_StackDamageOnSameTarget**: 동일 대상 연타 스택 추가 피해 (`Actor.cs:826`)
15. **쉴드 우선 감산** → **HP 감산** (`Actor.cs:435`)

**요약식**
```
기본 = Random[Attack_Min, Attack_Max]
중간 = 기본 + 절대증가 + (기본 × 배수증가)
(크리 시) 중간 × CriDmg
→ 중간 - 절대감소
→ 중간 - (중간 × 감소비율)
→ max(중간, 1)
→ (쉴드 존재) 쉴드 먼저 흡수 → HP 감산
```

### 3.3 크리티컬

`MyClass.cs:354~393`
```csharp
isCri = DSUtil.RandomTrue(crirate);                          // 1차: 확률 판정
if (Hitter.IsObtainCardSkill(G2_NextSevenHitsCritical))      // 2차: N연타 강제
{
    var card = Hitter.Get_CardSkillData_orNull(cardtype);
    if (card.UseCount_Acc < card.m_Data.Get_IntValue1())
    {
        ++card.UseCount_Acc;
        isCri = true;
    }
}
```

**크리 확률 상한**: `MaxCri = 0.9f` (`MyValue.cs:23`).
**크리 배수 적용**: `hitter_dmg *= Get_TotalStat(eStat.CriDmg)` (`Actor.cs:758`).

### 3.4 회피 판정 (3단계)

**1단계 — 명중률 체크 (PC 한정)** (`Actor.cs:623`)
```csharp
isHit = DSUtil.RandomTrue(hitterstat.Get_TotalStat(eStat.HitRate) / (avoid * 3.69));
```
> **(확인 필요)** 3.69 상수의 근거.

**2단계 — PC 회피율** (`Actor.cs:633~647`)
```csharp
if (DSUtil.RandomTrue(Attack가_Melee ? avoid_melee : avoid_range))
    Run_AvoidStatus(...); return;
```

**3단계 — 최초 공격 무조건 회피** (`Actor.cs:649~669`)
- 근접 첫 공격: `G2_FirstMeleeAlwaysEvade`
- 원거리 첫 공격: `G3_DodgeFirstRangedAttack`

**회피율 상한**: `MaxAvoid = 0.9f` (`MyValue.cs:23`) — **PC 한정**. 몬스터 회피는 정수형(비일관성 리스크).

### 3.5 상태이상

`Actor.cs:512~513`
```csharp
OnEvent_GetDmg(pdData);
Set_StatusEffect(pdData.Get_SoSData(pdData.Hitter), this);
```

상태이상 데이터는 `table_StatusOptionSet`에서 조회 (`MyClass.cs:401~438`). 업데이트 불가 CC(Stun/Freezing 등) 은 `Actor.cs:52~53` 상수 참조.

---

## 4. 터치 방어 메커닉 (Q-P1 → Q-P2)

### 4.1 결론

**플레이어 터치 입력으로 방어가 발동된다 — 그러나 현재 확인된 경로는 "타겟팅"이다.** 명시적 방어 입력 윈도우는 ~~(확인 필요)~~ → **✅ 실측 확정 (2026-04-17 #37 Q-P2 정밀 2차)**: `UITouchHandler.cs` `IPointerDownHandler`·`IPointerUpHandler`·`IPointerExitHandler` 인터페이스로 구현. `IngameUIManager.cs:39` `m_PCDefence.Set(m_PCActor.Play_Defence, m_PCActor.Release_Defence, null)` 바인딩. **터치 Down↔Up 구간 동안 지속되는 상태 효과**. 근거: [`공유/소통/완료/2026-04-17_Phase0-C_QP2_정밀2차_응답서.md`](../../../공유/소통/완료/2026-04-17_Phase0-C_QP2_정밀2차_응답서.md).

### 4.2 구현

`MonsterNodeControler.cs:263~290` — 마우스/터치 다운 이벤트
```csharp
void Update()
{
    if (Input.GetMouseButtonDown(0))
    {
        // ... RaycastHit2D로 Actor 탐색
        if (actor != null && !pc.IsDead() && targetlines && !actor.IsDead())
            InGameInfo.Ins.m_PCActor.Set_Target(actor); // 타겟 변경
    }
}
```

### 4.3 방어 상태 관련 흔적

- `PCActor.Play_Defence()` `PCActor.cs:148~154`: 방어 애니메이션 실행
- `PCActor.Release_Defence()` `PCActor.cs:155~161`: 방어 해제
- 실제 호출부 ~~(확인 필요)~~ → **✅ 실측 확정 (#37)**: `IngameUIManager.cs:39` — `m_PCDefence.Set(...)` 으로 `UITouchHandler` 이벤트가 `Play_Defence`/`Release_Defence` 에 바인딩. `Update()`에서 `m_onHold?.Invoke()` 지속 호출 (현재 no-op).

### 4.4 방어 성공 효과 (`Actor.cs:515~519`)

```csharp
if (ActorStatus == eActorStatus.Defecne)
{
    huddmg.Set_Block(dmg, Get_World_Position());
    EffectMgr.Ins.Show_Effect("PCBlock", Get_World_Position(), this);
}
```

방어 중이면 피해 감소:
- 절대: `PCDefence` (`Actor.cs:775`, `table_GlobalValue`)
- 비율: `PCDefence_Mul` (`Actor.cs:783`)

#### ✅ 실측 확정 수치 (2026-04-17 #37 Q-P2 정밀 2차)

| 항목 | 기획 초기 가정 | **실측 확정값** | 근거 |
|------|---------------|---------------|------|
| 피해 감소 비율 | 50% | **30%** (`PCDefence_Mul = 0.3`) | `GlobalValue.json` — `{"s_ID": "PCDefence_Mul", "n_Value": "0.3"}` |
| 고정 절대 감소 | 미정 | **1** (`PCDefence = 1`) | `GlobalValue.json` + `Actor.cs:775` |
| 지속 형태 | 단일 공격 바인딩 가정 | **지속형 상태 효과** (터치 Down↔Up 유지 동안 반복 적용) | `UITouchHandler.cs` + `IngameUIManager.cs:39` |
| 쿨다운 | 미정 | **없음** (즉시 재사용) | `UITouchHandler.cs`·`PCActor.cs:148-162` 전수 확인 |
| 방어 중 공격 | 미정 | **불가** (DPS 0) | `Actor.cs:4500` — 공격 프레임 return |
| 방어 중 피격 애니 | 미정 | **차단** (Play_Hit 조기 return) | `PCActor.cs:133` |
| 적용 공격 종류 | 미정 | **Melee / Ranged 공통** | `Actor.cs:782-783` 단일 분기 |
| Mob 방어 메커닉 | 미정 | **부재** | `MobActor.cs` override 전수 확인 결과 부재 |

**근거 응답서**: [`2026-04-17_Phase0-C_QP2_정밀2차_응답서.md`](../../../공유/소통/완료/2026-04-17_Phase0-C_QP2_정밀2차_응답서.md) (#37 PD 지시). **기획 초기 가정("50%")은 추적성 보존을 위해 유지**(원칙 1), 실측 수치로 전환하여 밸런스 작업은 0.3 기준으로 진행.

---

## 5. 몬스터 배치 · 스폰 패턴 (Q-P2)

### 5.1 배치 흐름

`MonsterNodeControler.Set()` (`cs:42~171`)
1. `StageNodeData_Mob.PatternID` → `table_MonsterPatternList` 조회
2. `PatternID < 1001`이면 `+900` 구데이터 호환 (`cs:70`)
3. `Get_MakeMob_Dynamic()`가 라인 × 슬롯 9개에 대해 근접/원거리/고유 확률 누적 추첨
4. **고유 몬스터는 1개 제한** (`uniqueMakeCount`) — **(확인 필요)** 의도된 캡인지

### 5.2 데이터 테이블 키 매핑

| 테이블 | 필드 | 역할 | 위치 |
|---|---|---|---|
| `table_MonsterPatternList` | `n_PatternId` | 패턴 ID | `cs:11` |
| 동 | `list_MeleeRate_Front/Middle/Back` | 라인별 근접 확률 | `cs:33` |
| 동 | `list_RangeRate_Front/Middle/Back` | 라인별 원거리 확률 | `cs:34` |
| 동 | `list_FixedRate_Front/Middle/Back` | 라인별 고유 확률 | `cs:35` |
| 동 | `n_FixedMonsterId` | 고유 몬스터 ID | `cs:17` |
| `StageNodeData_Mob` | `MobCount` | 배치할 몬스터 수 | `IngameStageData.cs:86` |
| 동 | `list_MobID` | 선택 가능한 몬스터 풀 | `IngameStageData.cs:86` |

확률 문자열 포맷: `^`로 구분된 정수(10000 스케일) → 파싱 시 10000으로 나눠 0~1 정규화 (`table_MonsterPatternList.Init()` `cs:43~115`).

### 5.3 몬스터 ID 결정

`MonsterNodeControler.cs:183~196`
```csharp
int Get_MobID(MakeMobData mob, StageNodeData_Mob mobnode, int makecount, int bossid)
{
    if (mob.MobID > 0) return mob.MobID;              // 고유 몬스터 즉시 반환
    var lst = mobnode.list_MobID;
    if (bossid > 0) lst = lst.FindAll(f => f != bossid); // 보스 ID 중복 방지
    if (mobnode.MakeRandom) return lst[Random.Range(0, lst.Count)];
    return MyValue.m_MyStageData.Get_MobID_onStage(mob.e_ToolMobType, mobnode.list_MobID);
}
```

---

## 6. 보스 처리 (Q-P3)

### 6.1 판정

`table_monsterlist.cs:36~39`
```csharp
public override bool IsBoss()
    => e_MonsterType == eMonsterType.Boss_Melee
    || e_MonsterType == eMonsterType.Boss_Range;
```

### 6.2 배치 규칙

`MonsterNodeControler.cs:76~90`
```csharp
if (stagedata.m_Type == eStageNodeType.Boss)
{
    ++makeMobCount;
    var bossdata = stagedata.Get_Data<StageNodeData_Boss>();
    bossid = bossdata.BossID;
    dic_mob[eMobBattlePos.Backline][2] = bossid;   // 대기열 중앙 고정
    ...
}
```

**보스는 항상 Backline 슬롯 2에 배치된다.**

### 6.3 특수 패턴

- **Death 특수효과 피해**: 보스는 50% HP 피해, 일반 몬스터는 9999 (즉사) — `Actor.cs:570`
- **미션 등급 가중치**: `MobActor.cs:123~220` — 보스=2, 악마=3, 일반=1

> **(확인 필요)** 보스10002 클리어 가능성(Q-P3)은 전용 AI 패턴이 아닌 **스탯·카드 풀 기반 시뮬레이션**으로 검증해야 한다. 현재 코드상 보스 전용 공격 로직은 발견되지 않음.

---

## 7. 발견된 리스크 · 이슈

### 7.1 기획 문서 vs 코드 불일치 후보

| 항목 | 코드 실태 | 위치 |
|---|---|---|
| 회피 공식 | `HitRate / (Avoid × 3.69)` — 3.69 근거 불명 | `Actor.cs:623` |
| PC 방어 | 터치는 타겟팅만 확인, 명시적 방어 윈도우 미확인 | `PCActor.cs`, `MonsterNodeControler.cs` |
| 크리 상한 | 0.9f 고정 (90%) | `MyValue.cs:23` |
| 회피 상한 | PC만 0.9f, 몬스터는 정수형 | `MyValue.cs:625~639` |

### 7.2 하드코딩 값 (밸런싱 숨김)

| 값 | 용도 | 위치 | 개선 |
|---|---|---|---|
| `3.69` | 회피 상수 | `Actor.cs:623` | 테이블화 |
| `0.01f` | 회피율 % 변환 | `MyValue.cs:629` | eStat에서 처리 |
| `"PCDefence"` | 방어 절대 감소 | `Actor.cs:775` | GlobalValue 참조 OK |
| `"PCDefence_Mul"` | 방어 비율 감소 | `Actor.cs:783` | GlobalValue 참조 OK |
| `list_scale[]` | 9슬롯 스케일 | `MonsterNodeControler.cs:26` | 테이블화 |
| `list_pos[]` | 9슬롯 좌표 | `MonsterNodeControler.cs:27~32` | 테이블화 |

### 7.3 시뮬레이터 이원화

- 단일 경로 확인됨 — `Actor.Get_Dmg()` 외 병행 계산 경로 없음.
- 이원화 리스크는 **낮음**. (기획실 시뮬레이터는 외부 엑셀/도구이므로, SOT를 이 문서로 일원화하면 검증 기준 확정 가능.)

### 7.4 코드 품질

- `Actor.cs` ≈ 3,700줄 · `Get_Dmg` ≈ 300줄 — 분할 권고
- 카드 효과 조건분기 난립 (`eCardType.Max = 200+`) — 효과 테이블화·전략 패턴 리팩토링 후보
- `ObscuredInt/Float` 빈번 사용 — 보안 목적 유지하되 핫패스 프로파일링 필요

### 7.5 버그 냄새

1. **HP 0 허용 가능성** — `Math.Max(1, ...)` 가드가 Attack_Min/Max에만 적용 (`MyValue.cs:709`)
2. **몬스터 회피 비일관성** — PC 상한 0.9 vs 몬스터 정수
3. **라인 변경 중 공격 수신** — `Co_LineChange` 도중 상태 불일치 가능성
4. **보스 ID 풀 중복 처리 의존** — `list_MobID`에 boss가 들어가 있을 때 `FindAll` 예외 안전성 **(확인 필요)**

---

## 8. B-1 완료 조건 체크리스트

SOT를 완전 확정하려면 다음 항목이 추가로 필요.

- [ ] 기획 엑셀 SOT와 회피·크리 상한·3.69 상수 대조
- [ ] `table_GlobalValue` 실제 값 덤프 (PCDefence, PCDefence_Mul, PC_ProjectileSpeed 등)
- [ ] `eCardType.G1_* ~ G5_*` 200+개 효과 전수 맵 (효과 종류별 분류표)
- [ ] 상태이상 중첩 규칙 명문화 (Stun/Freezing 등 CC 상호작용)
- [x] PC 방어 입력 경로 특정 (UI 버튼 vs 터치 윈도우) — **완료 (2026-04-17 #37)**: `UITouchHandler` 기반 터치 윈도우, `IngameUIManager.cs:39` 바인딩
- [x] Q-P1 응답서 확정 — 터치 방어 구조 최종 확인 — **완료 (2026-04-17 #37 Q-P2 정밀 2차)**: [응답서](../../../공유/소통/완료/2026-04-17_Phase0-C_QP2_정밀2차_응답서.md) 참조
- [ ] Q-P2 응답서 확정 — 패턴별 배치 예시 생성 (본 항목은 몬스터 배치 패턴 예시 별개 작업)
- [ ] Q-P3 응답서 확정 — 보스10002 시뮬 결과 포함
- [ ] 핫패스 성능 프로파일 (Get_Dmg 호출/프레임, ObscuredInt 오버헤드)
- [ ] 위 7.5 버그 냄새 재현 테스트

---

## 9. 변경 이력

| 버전 | 일자 | 작성자 | 내용 |
|---|---|---|---|
| v1 | 2026-04-14 | 개발실장 (Explore 위임) | 초안 — Phase 0-B-1 |
