18 KiB
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
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));
}
기여도 순서 (카드 > 장비 > 각성 > 인장 원칙과 매핑):
- 기본값:
ActorTableDataBase.n_AttackMin / n_AttackMax - 장비:
ApplyMainStat()/ApplyStat()(MyValue.cs:261~282) - 각성:
eAwakening.Stat_ADD,eUniqueAwakeningType.PC1_UniqueAwakening1~5(MyValue.cs:156~217) - 인장:
eSpecialEffect.CritChance,CritDamage등 (MyValue.cs:145~154) - 런 내 버프/디버프:
Set_Buff()/Set_Debuff()(MyValue.cs:747~792)
3.2 최종 피해 계산 (15단계)
Actor.cs:521~834 순서대로 요약.
- 기본:
baseDmg = hitterstat.Get_Damage() - 1회성 절대 증가:
AddDmg_1Time+ 인장 절대값 - 1회성 배수 증가:
AddDmgMul_1Time+ 인장 배수 - 스턴/라인 보정:
DmgMul_byStun,FrontLine_Dmg_Mul_1Time,MiddleLine_Dmg_Mul_1Time,BackLine_Dmg_Mul_1Time - G3_PotionNextAttackBoost: 물약 스택 소모 시 배수 추가
- G5_FirstAttackTripleDamage: 첫 공격이면 배수를 해당 값으로 대체(
Actor.cs:719~724) - 곱연산 합산:
hitter_dmg = base + 절대 + (hitter_dmg * 배수)(Actor.cs:726) - 크리티컬:
hitter_dmg *= CriDmg(크리 시) (Actor.cs:758) - 절대 감소:
ReduceMeeleDmg/ReduceRangeDmg/ReduceDmg+ 방어 중이면PCDefence(Actor.cs:776) - 비율 감소:
ReduceMeeleDmg_Mul/ReduceRangeDmg_Mul+ 방어 중이면PCDefence_Mul,G2_ReduceRangedDamageWithShield(Actor.cs:808) - 최소 피해 1:
hitter_dmg = max(hitter_dmg, 1)(Actor.cs:811) - G4_ShieldedDamageReduction: 쉴드 보유 시 비율 추가 감소
- FixedDmg: 고정 피해 스탯이 있으면 피해를 해당 값으로 대체 (
Actor.cs:817) - G2_StackDamageOnSameTarget: 동일 대상 연타 스택 추가 피해 (
Actor.cs:826) - 쉴드 우선 감산 → 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:39—m_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)
StageNodeData_Mob.PatternID→table_MonsterPatternList조회PatternID < 1001이면+900구데이터 호환 (cs:70)Get_MakeMob_Dynamic()가 라인 × 슬롯 9개에 대해 근접/원거리/고유 확률 누적 추첨- 고유 몬스터는 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 버그 냄새
- HP 0 허용 가능성 —
Math.Max(1, ...)가드가 Attack_Min/Max에만 적용 (MyValue.cs:709) - 몬스터 회피 비일관성 — PC 상한 0.9 vs 몬스터 정수
- 라인 변경 중 공격 수신 —
Co_LineChange도중 상태 불일치 가능성 - 보스 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 |