# 09. 카드시스템 아키텍처 v1

- 작성일: 2026-04-14
- 작성자: 개발실장 (Explore 에이전트 분석 위임)
- 목적: 기획실이 관리하는 카드(G1~G5 총 311장) 이 **코드상 어떻게 표현·실행·획득되는지** 확정
- 스코프: `Assets/Script/Table/Tables/table_cardlist.cs`, `Assets/Script/InGame/Actor/Actor.cs`, `Assets/Script/My/MyClass.cs`
- 선행 문서: `08_전투시스템_SOT_v1.md`

---

## 1. 카드 데이터 모델

### 1.1 eCardType Enum

`table_cardlist.cs:123~450` 에 `G[1-5]_<효과명>` 형태로 311개 정의.

| 등급 | 개수 | 대표 |
|---|---|---|
| G1 (Common) | 112 | G1_AddPotion ~ G1_ShieldIncreaseAttackPower |
| G2 (Uncommon) | 73 | G2_CriUpAfterCri ~ G2_RangeDodgeUp |
| G3 (Rare) | 51 | G3_CritDmgUp ~ |
| G4 (Hero) | 43 | G4_ZeroShieldCritRate ~ |
| G5 (Legend) | 32 | G5_RemoveNormalSkills ~ G5_RandomLightning_N |
| **합계** | **311** | `eCardType.Max` sentinel |

명명 규칙: `G[1-5]_<CamelCase>`. 기획실 SOT 엑셀 행과 1:1 매핑.

### 1.2 CardListTableData 필드 (`table_cardlist.cs:7~120`)

| 필드 | 타입 | 암호화 | 용도 |
|---|---|---|---|
| `e_CardType` | enum | ✗ | 식별자 |
| `n_ID` | int | **ObscuredInt** | 고유 ID |
| `e_CardGrade` | eGrade | ✗ | 등급 |
| `s_Value1~3` / `f_Value1~3` | string / float | float만 **Obscured** | 수치 (문자열/런타임 이원) |
| `s_LvUpValue1~2` / `f_LvUpValue1~2` | string / float | float만 **Obscured** | 레벨업 가중치 |
| `e_LvUpType` | enum | ✗ | 레벨업 적용 방식 |
| `b_Percent`, `b_LvUpValue*Percent` | bool | ✗ | % 해석 플래그 |
| `s_Projectile` / `e_ProjectileAttackType` / `n_ProjectileCount` / `f_ProjectileDelayTime` | — | Obscured(int/float) | 투사체 |
| `n_StatusOptionSetID` | int | **Obscured** | `table_StatusOptionSet` 조인 |
| `n_UseDmg` | int | **Obscured** | 피해 계산 방식 (0=고정, 1=%비율, 2=배수) **(확인 필요)** |
| `n_Desc`, `n_LvUpDesc` | int | ✗ | 텍스트 테이블 키 |

**핵심 관찰**: 수치는 `ObscuredFloat/Int`로 안티치트 적용. 메타(enum, 플래그)는 평문. **수치 변경은 테이블만 수정하면 코드 무수정** — 기획실 밸런싱 규칙("효과 종류 변경 금지, 수치만 조정")과 구조적으로 일치.

### 1.3 eLvUpType (`table_cardlist.cs:110~122`)

```csharp
Use_LvUpValue1_To_Value1   // f_Value1 누적
Use_LvUpValue1_To_Value2
Use_LvUpValue1_To_Value3
Use_LvUpValue1_To_ProjectileCount
Heal_Multi / Heal_Mul       // 즉시 회복 효과
GetGold                     // 즉시 골드
```

레벨업 수치 적용 (`CardListTableData.cs:96~100`):
```csharp
int Get_CardLv() => Mathf.Max(0, MyValue.sdata.Get_CardLv(n_ID) - 1);
float Get_LvUpValue1(eLvUpType t) =>
    e_LvUpType == t ? f_LvUpValue1 * Get_CardLv() : 0;
```
→ **선형 누적**: `Value_final = Value_base + Level * Coefficient`.

### 1.4 CardSkillData 런타임 인스턴스 (`MyClass.cs:147~250`)

```csharp
public class CardSkillData
{
    public CardListTableData m_Data;
    Actor m_Actor;
    public bool StartBattle;
    public int TargetIdentity;
    public int UseCount;
    public int UseCount_Acc;
    public float m_LifeTime;                   // 주기 쿨타임
    public Dictionary<int, int> dic_identity;  // 대상별 추적
}
```

---

## 2. 카드 효과 실행 구조

### 2.1 OnEvent_* 진입점 맵 (~258개 분기)

`Actor.cs` 전반:

| 메서드 | 라인 | 발동 | 분기 수 |
|---|---|---|---|
| `OnEvent_AddSkillCard()` | 1799 | 카드 획득 직후 1회 | ~80 |
| `OnEvent_RunSkillCard()` | 1655 | 획득/라인변경 반복 | ~50 |
| `OnEvent_Kill_Hitter()` | 2244 | 적 처치 | ~20 |
| `OnEvent_Cri_Hitter()` | 2616 | 크리 발동 | ~15 |
| `OnEvent_Avoid()` | 3001 | 회피 성공 | ~12 |
| `OnEvent_NoMiss_Hitter()` | 2442 | 회피 실패 | ~8 |
| `OnEvent_MyShield_Hit()` | 3131 | 쉴드 피해 | ~10 |
| `OnEvent_MyShield_Break()` | 3145 | 쉴드 소진 | ~5 |
| `OnEvent_GetDmg()` | 3202 | 피해 수신 | ~15 |
| `OnEvent_PlayAttack()` | 2022 | 공격 발동 | ~20 |
| `OnEvent_LvUp()` | 2917 | 레벨업 | ~25 |
| `OnEvent_MeetNode()` | 2746 | 노드 진입 | ~5 |
| `OnEvent_MakeMob()` | 3408 | 몬스터 생성 | ~3 |
| `OnEvent_Hit()` | 3218 | 피해 적용 후 | ~10 |

### 2.2 효과 적용 3가지 패턴

- **A. 조건 분기 (if-switch)** — 카드 1장당 1개 분기 (`Actor.cs:1801~1830` 등). 신규 추가 시 **코드 수정 필요**.
- **B. 데이터 주도 (테이블 참조)** — `CardSkillData.Add(...)` 에서 `Set_Buff(eStat, value)` 로 일반화 (`MyClass.cs:161~180`). 이 경로는 enum 케이스만 추가하면 됨.
- **C. 주기형 (m_LifeTime)** — `G1_MagicMissile` 처럼 쿨타임 기반 (`MyClass.cs:165~166`). `Update()`에서 감소.

### 2.3 트리거 분류표

| 트리거 | 예시 카드 | 위치 |
|---|---|---|
| 즉시 (획득 시) | G1_FireMagicMissiles, G1_InstantFullHeal | OnEvent_AddSkillCard |
| 온 공격 | G1_FirstAttack, G1_FifthAttackBack | OnEvent_PlayAttack |
| 온 피해 | G1_HealOnRangedHit | OnEvent_GetDmg |
| 온 킬 | G1_CastMagicMissilesOnBackKill | OnEvent_Kill_Hitter |
| 온 크리 | G1_ThirdCritDamage | OnEvent_Cri_Hitter |
| 온 회피 | G1_LifeStealOnDodge | OnEvent_Avoid |
| 온 노드시작 | G2_HeroSkillFullHeal | OnEvent_RunSkillCard |
| 영구 스탯 | G1_AddMaxAttack | CardSkillData.Add → Set_Buff |
| 1회성 버프 | G1_DodgeNextNAttacks | OnEvent_AddSkillCard |
| 주기 효과 | G1_MagicMissile | m_LifeTime |

---

## 3. 카드 획득 · 보유 · 제거

### 3.1 획득 경로

```
노드 클리어 → Co_AllKill() → act_EndNode()
 → SelectCardUI.Set_Cards() [BattleCard.cs:37]
 → BattleCard.OnClick()
 → InGameInfo.Ins.Add_Card(CardListTableData)
 → Actor.Add_CardSkill() [Actor.cs:337]
 → dic_Card[eCardType] = CardSkillData
 → OnEvent_AddSkillCard()
```

추가 경로:
- 성소: `Actor.Add_Card_Random()` (`Actor.cs:4474`)
- 장비 특수효과: `Set_Equipment()` → `list_SkillId` (`Actor.cs:3773`)
- 상태이상 연쇄: `GetSkillCard_N` (`Actor.cs:3866~3871`)

### 3.2 보유 구조

- 저장소: `Dictionary<eCardType, CardSkillData> dic_Card` — **카드 타입별 1장**
- 조회: `IsObtainCardSkill(eCardType)` O(1)
- 슬롯 상한: **코드상 무제한** — **(확인 필요)** 기획 UI 상한 존재 여부
- 중복 방지: `if (!dic_Card.ContainsKey(...))` (`Actor.cs:4487`)

### 3.3 제거 경로

**현재 코드상 카드 제거 경로 없음** — `Actor.Set()` (`Actor.cs:148`) 에서 런 리셋 시 `dic_Card.Clear()` 만 존재.

---

## 4. 카드 효과 카탈로그 (대표 샘플)

### 4.1 카테고리별

| 카테고리 | 수량 추정 | 대표 |
|---|---|---|
| 공격력 증강 | ~25 | G1_AddMaxAttack, G3_BacklineDoubleDmg, G5_FlatDamageMinMaxEqual |
| 피해 감소/무효화 | ~18 | G2_ReduceMeeleEnemyDamage, G2_ActivateInvincibleShieldOnKill, G1_ReflectOnRangedDodge |
| 회피/회피율 | ~12 | G1_DodgeNextNAttacks, G2_FirstMeleeAlwaysEvade, G1_MaxDodgeWhenHpBelow |
| 크리 | ~15 | G2_CriUpAfterCri, G1_ThirdCritDamage, G2_NextSevenHitsCritical, G5_EvadeCrit |
| 쉴드 | ~20 | G1_ShieldOnRangedDodge, G1_MaxShieldUpOnLevelUp, G1_SpringShieldToHP |
| 회복/생명력 | ~22 | G1_HealOnRangedHit, G1_AngelFeatherHeal, G1_StopExploreHealHP |
| 상태이상 부여 | ~15 | G1_EnemySpawnStunChance, G1_StunAllMeleeEnemies, G5_EvadeStun_N |
| 특수 투사체 | ~35 | G1_MagicMissile, G1_ThunderOnFifthHit, G4_StartArrowRainRangedImmune, G5_CritTriggerMeteor |
| 스킬 카드/특수 | ~18 | G1_GetRandomRareOrEpicSkill, G1_NextLevelChooseTwoSkillCards, G1_CastReaperOnLevelUp |
| 수집/보상 | ~12 | G1_GainGoldOnSanctuaryFind, G1_XpGainOnCritKill, G1_CampRechargePotion |
| 방어/면역 | ~10 | G2_ActivateInvincibleShieldOnKill, G5_PotionDamageImmunity |

### 4.2 등급별 경향

- **G1 112장 (36%)** — 기초재. 모든 런에서 자주 획득.
- **G2 73장 (23%)** — 조건부/조합 효과.
- **G3 51장 (16%)** — 라인·배수·크리 특수.
- **G4 43장 (14%)** — 실드·동일대상·면역 연계.
- **G5 32장 (10%)** — 투사체·무적·즉사 최상위 시너지.

G1 비중이 높은 것은 기획 의도(풀초기/보편 효과)로 해석 가능.

---

## 5. 카드시스템과 밸런싱 훅

### 5.1 데이터 입력 흐름

```
기획실 .xlsm
  → (익스포트 도구) → JSON/CSV
  → (코드 생성) → table_cardlist.cs (자동)
  → (Awake) → CardListTableData 싱글톤
  → (전투 중) → CardSkillData.m_Data → Get_*Value*()
```

**코드상 수치 하드코딩은 확인되지 않음** — 모든 수치는 테이블 필드. → 밸런싱은 테이블만 수정하면 된다는 구조적 전제 성립.

### 5.2 핫 리로드

- **현재 불가** — 싱글톤 Awake에서 초기화, 런 중 반영 안 됨. 재시작 필요.
- 개선안: ScriptableObject / JSON 원격 동기화 / 기획실 대시보드 연동 — **Phase 1+ 고려**.

### 5.3 기획실 워크플로 접촉점

| 단계 | 파일/도구 | 비고 |
|---|---|---|
| 효과 설계 | 기획 엑셀 | 효과 종류 변경 금지 (규칙) |
| 수치 입력 | `f_Value1~3`, `f_LvUpValue1~2` | |
| 익스포트 | 도구 경로 | **(확인 필요)** 문서화 필요 |
| 코드 생성 | `table_cardlist.cs` 자동 | |
| QA | Unity 런타임 | |
| 시뮬레이터 검증 | 기획실 매크로 | **코드와 공식 일치 여부 (확인 필요)** |

---

## 6. 리스크 · 이슈

### 6.1 조건 분기 폭발 (~258 위치)

- 단일 카드 효과가 여러 `OnEvent_*`에 분산 → 신규 추가 시 수정 누락 위험.
- 개선안: `interface ICardEffect` + `CardEffectRegistry` 디스패처 패턴으로 효과별 객체 분리. **(장기 리팩토링 과제)**

### 6.2 하드코딩 상수 (08 문서와 중복)

- `3.69` (회피 분모) `Actor.cs:623`
- `0.9f` (크리·회피 상한) `MyValue.cs:23`
- `eCardType.Max` sentinel — 선택적 테이블화 대상

### 6.3 구현 누락 후보

- G5 32장 중 일부 미구현/주석 처리 가능성 — **(확인 필요)** TODO/FIXME/주석 grep
- `G1_NullifyDamageEveryNHits` — `Dmg_Immune` 스탯 순서 버그 가능성

### 6.4 시뮬레이터 동기화

| 항목 | 코드 | 시뮬 | 상태 |
|---|---|---|---|
| 피해 15단계 | Actor.Get_Dmg | 엑셀 매크로 | 대조 필요 |
| 크리 확률 | RandomTrue + 강제 | 고정% | 대조 필요 |
| 회피 상한 | PC 0.9f | ? | **(확인 필요)** |
| 카드 효과 트리거 | 분산 | 테이블 | 큰 차이 |

### 6.5 성능

- `ObscuredInt/Float` 복호화·재암호화 비용이 **Get_Damage 호출마다** 발생. 크리 판정/피해 계산에서 프레임당 수십 회. → Profiler 측정 필요.

### 6.6 버그 냄새

1. 중복 획득 방지 — 특수 아이템 `list_SkillId` 경로 **(확인 필요)**
2. OnEvent 순서 의존성 — `AddSkillCard` ↔ `RunSkillCard` 순서
3. G5 카드 스킵 로직 — 이미 획득 상태 체크 누락 가능성

---

## 7. B-2 완료 조건 체크리스트

**필수**
- [ ] 311개 전체 eCardType 하드카피 리스트 (문자열 ID)
- [ ] 각 카드의 대표 분기점 매핑 (OnEvent_*)
- [ ] G5 미구현/주석 목록 확보
- [ ] `table_cardlist` 필드 ↔ 엑셀 컬럼 1:1 매핑 확정
- [ ] eLvUpType 전 케이스 동작 테스트
- [ ] IsObtainCardSkill 258 호출 위치 전수 지도 (누락 확인)

**선택**
- [ ] 기획 엑셀 매크로 방정식 덤프 (코드 수식과 대조)
- [ ] 카드 의존성 그래프
- [ ] ObscuredFloat 프로파일링

---

## 8. 변경 이력

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