BurningTimesAi/프로젝트/수상한잡화점/개발/08_전투시스템_SOT_v1.md

18 KiB
Raw Blame History

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:170InGameInfo.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

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)

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

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)

isHit = DSUtil.RandomTrue(hitterstat.Get_TotalStat(eStat.HitRate) / (avoid * 3.69));

(확인 필요) 3.69 상수의 근거.

2단계 — PC 회피율 (Actor.cs:633~647)

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

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.

4.2 구현

MonsterNodeControler.cs:263~290 — 마우스/터치 다운 이벤트

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:39m_PCDefence.Set(...) 으로 UITouchHandler 이벤트가 Play_Defence/Release_Defence 에 바인딩. Update()에서 m_onHold?.Invoke() 지속 호출 (현재 no-op).

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

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 (#37 PD 지시). 기획 초기 가정("50%")은 추적성 보존을 위해 유지(원칙 1), 실측 수치로 전환하여 밸런스 작업은 0.3 기준으로 진행.


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

5.1 배치 흐름

MonsterNodeControler.Set() (cs:42~171)

  1. StageNodeData_Mob.PatternIDtable_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

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

public override bool IsBoss()
    => e_MonsterType == eMonsterType.Boss_Melee
    || e_MonsterType == eMonsterType.Boss_Range;

6.2 배치 규칙

MonsterNodeControler.cs:76~90

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 상호작용)
  • PC 방어 입력 경로 특정 (UI 버튼 vs 터치 윈도우) — 완료 (2026-04-17 #37): UITouchHandler 기반 터치 윈도우, IngameUIManager.cs:39 바인딩
  • Q-P1 응답서 확정 — 터치 방어 구조 최종 확인 — 완료 (2026-04-17 #37 Q-P2 정밀 2차): 응답서 참조
  • Q-P2 응답서 확정 — 패턴별 배치 예시 생성 (본 항목은 몬스터 배치 패턴 예시 별개 작업)
  • Q-P3 응답서 확정 — 보스10002 시뮬 결과 포함
  • 핫패스 성능 프로파일 (Get_Dmg 호출/프레임, ObscuredInt 오버헤드)
  • 위 7.5 버그 냄새 재현 테스트

9. 변경 이력

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