# 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(); 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 |