BurningTimesAi/프로젝트/EerieVillage/개발/spec/스킬_시스템_설계_v1.md

78 KiB
Raw Blame History

type: 설계_문서 scope: 스킬_시스템 author: 개발팀장 date: 2026-04-24 version: v1.0 (BT12-Dev Phase 1 개발팀장 Opus 설계) project: EerieVillage (기묘한 고을 : 조선퇴마뎐 / EerieVillage: Joseon Exorcist) phase: BT12-Dev Phase 1 — 개발팀장 설계 data_source: 프로젝트/EerieVillage/기획/content/02_스킬_효과_컨셉.md v0.2 · 프로젝트/EerieVillage/기획/content/02_스킬_효과_컨셉_v0.3.csv · 프로젝트/EerieVillage/기획/system/01_카드_시스템.md v0.2 · 프로젝트/EerieVillage/기획/balance/01_전투_수치.md v0.2 · 프로젝트/EerieVillage/기획/04_전투_기본_스펙.md v0.2 · 프로젝트/EerieVillage/기획/content/01_카드_풀.md v0.2 · 프로젝트/EerieVillage/개발/06_BT7-Plan_VS순수형_재구조.md v1.0 status: Phase 1 설계 완료 — Phase 2 클라이언트팀 구현 위임 대기 · Phase 3 개발팀장 검증 대기

스킬 시스템 설계 v1

§1. 아키텍처 개요

1-1. 설계 목표 (C11 개발 관점 3축)

  1. 자원 효율성: 60종 스킬을 런타임에 동시 처리해도 GC 알로케이션·파티클 상한·물리 연산 부담이 모바일 60fps 허용 범위 내 유지
  2. 코드 구조 직관성: 스킬 데이터·런타임 인스턴스·효과 발동 3계층이 명확히 분리되어 다음 개발자가 "이 스킬은 어디서 정의되고 어떻게 발동되는가"를 5분 내 파악 가능
  3. 범용성: 카테고리·패턴 설계로 60종 중 49종이 공통 런타임 클래스 3~4종으로 커버되며, 신규 카드 추가 시 ScriptableObject 인스턴스 생성만으로 확장 가능

1-2. 재미 축 정합 (기획서 §1 인용)

  • 축 1 — 육성 롤러코스터의 카타르시스: 각성 발동 3조건 동시 충족 구조가 "쌓고 폭발" 감정 곡선 형성
  • 축 2 — 자동 발동 감상 (VS 순수형): PlayerAttackTicker 패턴 확장으로 모든 액티브가 주기·이벤트 트리거 자동 발동
  • 축 3 — 영속 성장의 위안 (보조): 패시브 스탯 누적·최대 하트 증가가 런 내 성장 감각 제공

1-3. 4계층 아키텍처

┌─────────────────────────────────────────────────┐
│ 계층 4: 효과 발동 (기존 BT7-Dev 자산 재활용)          │
│   - AttackHitbox (근접·범위)                     │
│   - 신설: ProjectileSpawner (투사체)             │
│   - 신설: AuraZone (설치·지속)                    │
│   - 신설: MinionSpawner (소환)                   │
│   - 신설: DebuffApplier (상태이상)                │
│   - Health.Decrement(int damage) 호출           │
└─────────────────────────────────────────────────┘
                        ▲ 호출
┌─────────────────────────────────────────────────┐
│ 계층 3: 스킬 런타임 (SkillRuntime 컴포넌트)           │
│   - ActiveSkillRuntime (Cooldown·OnHit·OnKill) │
│   - PassiveSkillRuntime (상시·조건부)             │
│   - AwakeningSkillRuntime (진화 후 재활용)         │
│   - 각 카드당 1 인스턴스. PlayerSkillInventory 관리 │
└─────────────────────────────────────────────────┘
                        ▲ 참조
┌─────────────────────────────────────────────────┐
│ 계층 2: 스킬 데이터 (SkillDataAsset · ScriptableObj)│
│   - ActiveSkillData : 카테고리·주기·판정·대미지     │
│   - PassiveSkillData : 카테고리·스탯 보정·조건       │
│   - AwakeningSkillData : 원_액티브·필요_패시브[]·변형│
│   - 60종 .asset 파일 (CSV → 변환 파이프라인)        │
└─────────────────────────────────────────────────┘
                        ▲ 변환 입력
┌─────────────────────────────────────────────────┐
│ 계층 1: 정적 SOT (CSV v0.3 60종)                 │
│   - 02_스킬_효과_컨셉_v0.3.csv                    │
│   - 기획팀 편집 → 에디터 툴로 SkillDataAsset 재생성  │
└─────────────────────────────────────────────────┘

1-4. 기존 BT7-Dev 자산 활용 (C39 시스템 반영 실측)

본 설계는 BT7-Dev Phase 1 산출물(06_BT7-Plan_VS순수형_재구조.md v1.0)을 완전 계승한다. 중복 구현 금지 (C10 중복 작업 방지).

BT7-Dev 자산 본 설계 활용 방식
Platformer.Gameplay.PlayerAttack : Simulation.Event 그대로 사용. 기본 무기 발동은 본 이벤트 유지. 스킬별 효과는 별도 이벤트 확장
Platformer.Gameplay.PlayerAttackTicker 기본 무기 주기 타이머로 유지. 본 설계의 ActiveSkillRuntime은 독립 Cooldown 관리 (슬롯별 개별 주기 필요)
Platformer.Mechanics.AttackHitbox (근접 판정) 카테고리 B(근접·범위) 스킬의 효과 발동기로 재활용. AttackHitbox.Fire(direction) 호출만으로 판정 생성 가능
Platformer.Mechanics.Health: maxHearts·QuartersPerHeart·Decrement(int damage)·Heal(int quarters)·IncreaseMaxHearts(int delta)·CurrentHP·IsAlive 적 피해 전달 API. P-C 생존 강화형(P12·P13)은 IncreaseMaxHearts(1) 호출. P-D 회복형(P16·P17)은 Heal(1) 호출. 계약 변경 없음
Platformer.Mechanics.PlayerController.Facing (public Vector2) 투사체 발사 방향·근접 판정 방향 기준값
Platformer.Core.Simulation.Schedule<T>() 스킬 효과 발동 시 이벤트 Dispatch 패턴 유지

§2. 인터페이스 정의 (C# 코드 블록)

2-1. 공통 런타임 인터페이스

namespace EerieVillage.Skills
{
    /// <summary>
    /// 모든 스킬 런타임의 공통 계약.
    /// PlayerSkillInventory가 보유 스킬을 이 인터페이스 배열로 관리.
    /// </summary>
    public interface ISkillRuntime
    {
        /// <summary>참조하는 정적 데이터 (ScriptableObject)</summary>
        SkillDataAsset Data { get; }

        /// <summary>현재 스택 레벨 (1~5). 5 도달 시 각성 조건 1 충족</summary>
        int StackLevel { get; }

        /// <summary>스킬 장착·Lv 업 시 1회 호출 (런타임 초기화)</summary>
        void OnEquip(PlayerSkillInventory inventory);

        /// <summary>스킬 해제 시 1회 호출 (각성 대체 등)</summary>
        void OnUnequip();

        /// <summary>동일 카드 재픽으로 Lv N→N+1 업그레이드</summary>
        void Upgrade();
    }

    /// <summary>
    /// 액티브 스킬 런타임 계약. 주기·이벤트 트리거로 효과 발동.
    /// </summary>
    public interface IActiveSkill : ISkillRuntime
    {
        /// <summary>기본 쿨다운(초). balance/01 v0.2 `BaseCooldown` 1.5s 기반</summary>
        float BaseCooldown { get; }

        /// <summary>패시브 보정 후 실제 쿨다운 (연사술 P06 적용 결과)</summary>
        float EffectiveCooldown { get; }

        /// <summary>현재 Cooldown 타이머(초). MonoBehaviour Update가 감산</summary>
        float CooldownRemaining { get; }

        /// <summary>쿨다운 경과 또는 트리거 이벤트 시 호출. 효과 발동 Dispatch</summary>
        void Fire();

        /// <summary>트리거 이벤트 구독 — OnHit·OnKill·OnTime 중 해당</summary>
        ActiveTrigger Trigger { get; }
    }

    /// <summary>
    /// 패시브 스킬 런타임 계약. 장착 즉시 상시 적용 + 일부는 조건부 트리거.
    /// </summary>
    public interface IPassiveSkill : ISkillRuntime
    {
        /// <summary>상시 적용 여부 (true = 장착 즉시 효과 · false = 조건부)</summary>
        bool IsAlwaysOn { get; }

        /// <summary>
        /// 장착·해제·Lv 업 시 인벤토리의 PlayerStats에 보정값 반영.
        /// 스탯 상승형(P-A)·주기 단축(P-B)·생존 강화(P-C)·자원 확장(P-E)은 모두 이 경로.
        /// </summary>
        void ApplyModifier(PlayerStats stats);

        /// <summary>해제 시 보정값 원복</summary>
        void RemoveModifier(PlayerStats stats);

        /// <summary>
        /// 조건부 패시브(P11 호신부·P16 단전수련 등) 전용.
        /// OnPlayerDamaged·OnEnemyKilled·OnTimer 이벤트 구독 후 조건 충족 시 효과 발동.
        /// </summary>
        void OnTrigger(PassiveTriggerContext ctx);
    }

    /// <summary>
    /// 각성 스킬 런타임 계약. 원 액티브를 대체(또는 강화)하여 진화.
    /// </summary>
    public interface IAwakeningSkill : ISkillRuntime
    {
        /// <summary>진화 대상 원 액티브 데이터</summary>
        ActiveSkillData OriginalActive { get; }

        /// <summary>필요 패시브 후보 (1개 이상 보유로 조건 충족)</summary>
        PassiveSkillData[] RequiredPassives { get; }

        /// <summary>
        /// 각성 발동 시 1회 호출. 원 액티브 슬롯 점유 유지하며 진화 형태로 대체.
        /// 진화 패턴 4종 중 하나의 효과를 적용.
        /// </summary>
        void Awaken(PlayerSkillInventory inventory);

        /// <summary>진화 패턴 (1 스케일업 · 2 새효과 · 3 다중 발동 · 4 광역 확산)</summary>
        AwakeningPattern Pattern { get; }
    }

    /// <summary>액티브 발동 트리거 분류 — 카테고리·카드에 따라 결정</summary>
    public enum ActiveTrigger
    {
        OnTime,    // 주기 타이머 (카테고리 A·B·C·D·E 기본)
        OnHit,     // 플레이어 피격 시 (카테고리 F·P11 호신부 등)
        OnKill     // 적 처치 시 (P16 단전수련 등 패시브도 동일 enum 재사용)
    }

    /// <summary>각성 진화 패턴 (기획서 §4-2 4종)</summary>
    public enum AwakeningPattern
    {
        ScaleUp,         // 1. 대미지·범위·속도 대폭 증가
        AddEffect,       // 2. 새 효과 추가 (기존 유지 + 부가)
        MultiFire,       // 3. 발동 수 2배+
        GlobalSpread     // 4. 화면 전체 확산
    }

    /// <summary>패시브 조건부 트리거 컨텍스트</summary>
    public struct PassiveTriggerContext
    {
        public PassiveTriggerKind Kind;
        public float DamageTaken;      // OnPlayerDamaged 전용
        public int KillCount;          // OnEnemyKilled 전용 (누적)
        public float TimeElapsed;      // OnTimer 전용
    }

    public enum PassiveTriggerKind
    {
        OnPlayerDamaged,
        OnEnemyKilled,
        OnTimer
    }
}

2-2. ScriptableObject 데이터 계약

namespace EerieVillage.Skills
{
    /// <summary>모든 스킬 데이터 asset의 공통 base</summary>
    public abstract class SkillDataAsset : ScriptableObject
    {
        [Header("공통")]
        public string CardId;            // "A01"·"P12"·"AW19" (CSV v0.3 ID 컬럼)
        public string DisplayName;       // "진언부(眞言符)"·"Magic Arrow"
        public string EnglishName;       // 영문명 (CSV 2번째 컬럼)
        public Sprite Icon;              // UI용 아이콘
        [TextArea(2, 4)] public string Description;  // 플레이어 표시 툴팁
        public AttributeTag[] AttributeTags;  // [물리]·[화염] 등 (Flags enum)
        public TypeTag[] TypeTags;            // [근접]·[원거리] 등 (Flags enum)
    }

    [CreateAssetMenu(fileName = "Active_", menuName = "EerieVillage/Skills/Active")]
    public class ActiveSkillData : SkillDataAsset
    {
        [Header("액티브 전용")]
        public ActiveCategory Category;  // A·B·C·D·E·F
        public ActiveTrigger Trigger;    // OnTime·OnHit·OnKill
        [Tooltip("Lv.1 기본 쿨다운 (초). balance/01 v0.2 `BaseCooldown` 1.5s 참조")]
        public float BaseCooldown = 1.5f;
        [Tooltip("Lv.1 기본 대미지 (쿼터 단위). balance/01 v0.2 `BaseDamage` 10 참조")]
        public int BaseDamage = 10;
        [Tooltip("판정 박스 기본 크기 (A·B 카테고리). balance/01 v0.2 `AttackBoxSize` 1.5x1.0 참조")]
        public Vector2 HitboxSize = new Vector2(1.5f, 1.0f);
        [Tooltip("투사체 전용 (A 카테고리). 궤적 타입 — Line·Homing·Arc 중 1")]
        public ProjectileTrajectory Trajectory = ProjectileTrajectory.Line;
        [Tooltip("소환 전용 (D 카테고리). 소환물 프리팹 참조")]
        public GameObject MinionPrefab;
        [Tooltip("상태이상 전용 (E 카테고리). 스택 한도")]
        public int DebuffStackLimit = 3;
        [Tooltip("확률 판정 전용 (F 카테고리). 발동 확률 0~1")]
        [Range(0f, 1f)] public float FireProbability = 0.2f;
    }

    [CreateAssetMenu(fileName = "Passive_", menuName = "EerieVillage/Skills/Passive")]
    public class PassiveSkillData : SkillDataAsset
    {
        [Header("패시브 전용")]
        public PassiveCategory Category;  // P-A·P-B·P-C·P-D·P-E
        public bool IsAlwaysOn = true;
        [Tooltip("대상 스탯 (P-A·P-C 전용)")]
        public StatType TargetStat;
        [Tooltip("Lv.1 기본 보정값 (대미지 배율·하트 수·확률 등)")]
        public float BaseModifierValue;
        [Tooltip("조건부 패시브 (P11·P16·P17 등) 트리거 종류")]
        public PassiveTriggerKind TriggerKind;
    }

    [CreateAssetMenu(fileName = "Awakening_", menuName = "EerieVillage/Skills/Awakening")]
    public class AwakeningSkillData : SkillDataAsset
    {
        [Header("각성 전용")]
        public AwakeningPattern Pattern;
        public ActiveSkillData OriginalActive;       // 진화 대상 액티브
        public PassiveSkillData[] RequiredPassives;  // 필요 패시브 후보 (1+ 보유)
        [Tooltip("각성 후 기본 대미지 (원 액티브 Lv.5 대비 2~3배 권장, balance/01 v0.2 §3 참조)")]
        public int AwakeningBaseDamage = 25;
        [Tooltip("각성 연출 프리팹 (풀스크린)")]
        public GameObject AwakeningEffectPrefab;
    }

    public enum ActiveCategory { Projectile, MeleeArea, PlacementPersistent, Minion, Debuff, SpecialJudge }
    public enum PassiveCategory { StatUp, CycleAmplify, Survival, Recovery, ResourceExpand }
    public enum ProjectileTrajectory { Line, Homing, Arc }
    public enum StatType { Damage, AttackSpeed, MoveSpeed, MaxHearts, CritChance, CritDamage, DamageReduction, Evasion, IFrameExtend, JumpHeight, XPGain, TreasureFind }

    [System.Flags]
    public enum AttributeTag { None = 0, Physical = 1 << 0, Fire = 1 << 1, Frost = 1 << 2, Lightning = 1 << 3, Dark = 1 << 4 }

    [System.Flags]
    public enum TypeTag { None = 0, Melee = 1 << 0, Ranged = 1 << 1, Area = 1 << 2, Persistent = 1 << 3, Recovery = 1 << 4, Defense = 1 << 5 }
}

2-3. PlayerSkillInventory — 장착 스킬 관리 중앙 컴포넌트

namespace EerieVillage.Skills
{
    /// <summary>
    /// Player GameObject에 부착. 장착 스킬 슬롯·Lv·각성 상태 관리.
    /// [RequireComponent(typeof(PlayerController))]
    /// </summary>
    public class PlayerSkillInventory : MonoBehaviour
    {
        [Header("슬롯 상한 (balance-designer 재확인 — VS 원작 6/6 참고)")]
        public int ActiveSlotMax = 6;
        public int PassiveSlotMax = 6;

        // 장착 슬롯 (인덱스 = 슬롯 번호)
        private readonly List<IActiveSkill> _activeSkills = new();
        private readonly List<IPassiveSkill> _passiveSkills = new();
        private readonly List<IAwakeningSkill> _awakenedSkills = new();

        // 각 카드 ID → 런타임 인스턴스 매핑 (재픽 시 Lv 업용)
        private readonly Dictionary<string, ISkillRuntime> _cardIdToRuntime = new();

        // 통합 PlayerStats — 패시브 보정이 여기에 누적 적용
        public PlayerStats Stats { get; private set; }

        // 적 처치·피격 이벤트 구독자 리스트 (OnKill·OnHit 트리거용)
        public event System.Action<EnemyController> OnEnemyKilled;
        public event System.Action<float> OnPlayerDamaged;

        // 보물상자 획득 시 각성 조건 판정 진입
        public void OnTreasureChestOpened() { /* §5 각성 조건 매니저 호출 */ }

        // 픽 UI 선택 결과 반영
        public void AddSkillByCardId(string cardId) { /* §3 데이터 흐름 참조 */ }

        // Update에서 Active 런타임들의 Cooldown 감산 + OnTime Fire 호출
        void Update() { /* §4 자동 발동 사이클 참조 */ }
    }

    /// <summary>
    /// 통합 플레이어 스탯. 패시브 보정이 모두 이 객체에 누적.
    /// ActiveSkillRuntime의 EffectiveCooldown·EffectiveDamage 산출 입력.
    /// </summary>
    public class PlayerStats
    {
        public float DamageMultiplier = 1.0f;      // P01·P02~P05
        public float CooldownMultiplier = 1.0f;    // P06 연사술 (0.8 = 20% 단축)
        public float AreaMultiplier = 1.0f;        // P07 광역확장
        public int ExtraProjectiles = 0;           // P08 투사체증폭
        public float CritChance = 0f;              // P09
        public float CritDamage = 1.5f;            // P10
        public int ExtraMaxHearts = 0;             // P12·P13
        public float DamageReduction = 0f;         // P14 부적방패
        public float EvasionChance = 0f;           // P15 회피술
        public float IFrameExtend = 0f;            // P15 i-frame 연장분
        public float MoveSpeedMultiplier = 1.0f;   // P18 질풍보
        public float XPMultiplier = 1.0f;          // P19 선견지명
        public float TreasureFindBonus = 0f;       // P20 재물복
        // 속성별 대미지 배율
        public Dictionary<AttributeTag, float> AttributeMultiplier = new();
    }
}

§3. 데이터 흐름 (CSV → ScriptableObject → Runtime → Health.Decrement)

3-1. 전체 파이프라인

[정적 SOT]                    [에디터 툴]                [자산]
02_스킬_효과_컨셉_v0.3.csv  → SkillCsvImporter      → Assets/Data/Skills/*.asset (60종)
(기획팀 편집)                (EditorWindow)            (SkillDataAsset 자식)
                                                          ↓ Resources.Load
                              [런타임 생성]
                              SkillRuntimeFactory  → new ActiveSkillRuntime(data)
                                                    new PassiveSkillRuntime(data)
                                                    new AwakeningSkillRuntime(data)
                                                          ↓ 장착
                              PlayerSkillInventory → ISkillRuntime 슬롯 편입
                                                          ↓ Update
                              ActiveSkillRuntime → .Fire() 호출
                                                          ↓ Schedule<T>
                              Simulation.Event    → 카테고리별 Dispatcher
                                                          ↓
                              효과 발동기 (AttackHitbox·ProjectileSpawner 등)
                                                          ↓ OverlapBox·OnTrigger
                              Health.Decrement(damage)

3-2. CSV → ScriptableObject 변환 파이프라인 (개발팀 에디터 툴)

목적: 기획팀이 CSV v0.3를 편집하면 에디터 툴 원클릭으로 60종 .asset 파일을 재생성.

구현 포인트:

namespace EerieVillage.Skills.Editor
{
    /// <summary>CSV v0.3 → SkillDataAsset 60종 일괄 변환 에디터 툴</summary>
    public class SkillCsvImporter : EditorWindow
    {
        private const string CsvPath = "프로젝트/EerieVillage/기획/content/02_스킬_효과_컨셉_v0.3.csv";
        private const string OutputDir = "Assets/Data/Skills/";

        [MenuItem("EerieVillage/Skills/Import CSV → ScriptableObject")]
        static void Import()
        {
            // 1. UTF-8 BOM 처리 (CSV v0.3 필수 인코딩)
            var text = System.IO.File.ReadAllText(CsvPath, System.Text.Encoding.UTF8);
            // 2. CSV 파싱 (분류 컬럼: 액티브·패시브·각성)
            // 3. ID prefix로 Active/Passive/Awakening 분기
            //    - A## → ActiveSkillData
            //    - P## → PassiveSkillData
            //    - AW## → AwakeningSkillData
            // 4. 기존 asset 발견 시 필드 업데이트 (GUID·참조 보존)
            // 5. 신규는 AssetDatabase.CreateAsset()
            // 6. 카테고리 컬럼("A. 투사체"·"P-A. 스탯" 등)을 enum ActiveCategory·PassiveCategory로 매핑
            // 7. 태그·수치는 Phase 2 balance-designer 확정 후 별도 컬럼 추가 시 반영
        }
    }
}

카테고리 문자열 매핑 테이블:

CSV 값 Enum
"A. 투사체" ActiveCategory.Projectile
"B. 근접·범위" ActiveCategory.MeleeArea
"C. 설치·지속" ActiveCategory.PlacementPersistent
"D. 소환" ActiveCategory.Minion
"E. 상태이상" ActiveCategory.Debuff
"F. 특수 판정" ActiveCategory.SpecialJudge
"P-A. 스탯" PassiveCategory.StatUp
"P-B. 주기" PassiveCategory.CycleAmplify
"P-C. 생존" PassiveCategory.Survival
"P-D. 회복" PassiveCategory.Recovery
"P-E. 자원" PassiveCategory.ResourceExpand
"1. 스케일업" AwakeningPattern.ScaleUp
"2. 새 효과" AwakeningPattern.AddEffect
"3. 다중 발동" AwakeningPattern.MultiFire
"4. 광역 확산" AwakeningPattern.GlobalSpread

3-3. 런타임 인스턴스 생성 (Factory 패턴)

namespace EerieVillage.Skills
{
    public static class SkillRuntimeFactory
    {
        public static ISkillRuntime Create(SkillDataAsset data)
        {
            return data switch
            {
                ActiveSkillData a => new ActiveSkillRuntime(a),
                PassiveSkillData p => new PassiveSkillRuntime(p),
                AwakeningSkillData aw => new AwakeningSkillRuntime(aw),
                _ => throw new System.ArgumentException($"Unknown SkillDataAsset type: {data.GetType()}")
            };
        }
    }
}

3-4. 피해 전달 경로 (적 Health.Decrement까지)

ActiveSkillRuntime.Fire()
  ↓ Schedule<SkillFireEvent>
Simulation.Event<SkillFireEvent>.Execute()
  ↓ 카테고리 분기
┌─── Category.Projectile ─────────────────────────┐
│ ProjectileSpawner.Spawn(data, playerFacing)     │
│   ↓ Projectile.OnTriggerEnter2D                 │
│ Health.Decrement(effectiveDamage)               │
└─────────────────────────────────────────────────┘
┌─── Category.MeleeArea ──────────────────────────┐
│ AttackHitbox.Fire(direction) [BT7-Dev 재활용]     │
│   ↓ OverlapBoxAll → 각 적에 Decrement(damage)    │
└─────────────────────────────────────────────────┘
┌─── Category.PlacementPersistent ────────────────┐
│ AuraZone.Instantiate(position, data)            │
│   ↓ 영역 내 적 주기적 Decrement                    │
└─────────────────────────────────────────────────┘
┌─── Category.Minion ─────────────────────────────┐
│ MinionSpawner.Spawn(data.MinionPrefab)          │
│   ↓ MinionAI.Update → 적 공격 → Decrement         │
└─────────────────────────────────────────────────┘
┌─── Category.Debuff ─────────────────────────────┐
│ DebuffApplier.Apply(target, stackLimit)         │
│   ↓ N스택 시 폭발 이벤트 → Decrement                │
└─────────────────────────────────────────────────┘
┌─── Category.SpecialJudge ───────────────────────┐
│ Random.value < FireProbability → Fire           │
│   ↓ 단발 회심 또는 i-frame 연장                    │
│ Health.Decrement(critDamage)                    │
└─────────────────────────────────────────────────┘

effectiveDamage 산출 공식 (balance/01 v0.2 §3 대미지 공식 준수):

int effectiveDamage = Mathf.RoundToInt(
    data.BaseDamage *                              // ActiveSkillData 기본값
    stats.DamageMultiplier *                       // P01
    StackLevelFactor(runtime.StackLevel) *         // Lv.1=1.0 ~ Lv.5=2.0
    stats.AttributeMultiplier[data.PrimaryAttr]    // P02~P05
);
// 각성 발동 시 BaseDamage 대신 AwakeningBaseDamage 사용

StackLevelFactor 정의 (balance/01 v0.2 §3 명시):

static float StackLevelFactor(int lv) => lv switch
{
    1 => 1.0f, 2 => 1.2f, 3 => 1.4f, 4 => 1.6f, 5 => 2.0f,
    _ => throw new System.ArgumentOutOfRangeException(nameof(lv))
};

§4. 자동 발동 사이클 (Cooldown·OnHit·OnKill·OnTime)

4-1. 기본 원칙 — VS 순수형 정합 (기획 04 §5 준수)

  • 플레이어 수동 공격 입력 없음 (BT7-Dev 확증)
  • 모든 액티브는 ActiveTrigger enum에 따라 자동 발동
  • 기본 무기(퇴마사 고유 1종)는 PlayerAttackTicker 유지 (BT7-Dev 재활용)
  • 카드 시스템 장착 액티브는 각자 독립 Cooldown 관리

4-2. ActiveSkillRuntime Update 로직 (핵심)

namespace EerieVillage.Skills
{
    public class ActiveSkillRuntime : IActiveSkill
    {
        public ActiveSkillData Data { get; }
        public int StackLevel { get; private set; } = 1;
        public ActiveTrigger Trigger => Data.Trigger;
        public float BaseCooldown => Data.BaseCooldown;

        public float EffectiveCooldown =>
            BaseCooldown * _inventory.Stats.CooldownMultiplier / StackLevelFactor(StackLevel);
            // 예: BaseCooldown 1.5s · P06 연사술 0.8 · Lv.5 팩터 2.0 = 0.94s 실제 쿨다운
            // 하드캡 0.5s (balance/01 v0.2 §9 리스크 1 "이펙트 중첩 혼잡 방지")

        public float CooldownRemaining { get; private set; } = 0f;

        private PlayerSkillInventory _inventory;

        public ActiveSkillRuntime(ActiveSkillData data) { Data = data; }

        public void OnEquip(PlayerSkillInventory inventory)
        {
            _inventory = inventory;
            CooldownRemaining = Data.BaseCooldown;  // 첫 발동은 주기 후
        }

        public void OnUnequip() { /* 이벤트 구독 해제 */ }

        public void Upgrade()
        {
            if (StackLevel < 5) StackLevel++;
            // Lv.5 도달 시 각성 조건 1 충족 — AwakeningManager.MarkActiveMaxed(this)
            if (StackLevel == 5) _inventory.AwakeningManager.MarkActiveMaxed(Data);
        }

        /// <summary>PlayerSkillInventory.Update가 매 프레임 호출</summary>
        public void Tick(float deltaTime)
        {
            if (Trigger != ActiveTrigger.OnTime) return;  // OnHit·OnKill은 이벤트 구독
            CooldownRemaining -= deltaTime;
            if (CooldownRemaining <= 0f)
            {
                Fire();
                CooldownRemaining += EffectiveCooldown;  // 누적 오버플로 방지 (BT7-Dev Ticker와 동일 패턴)
                CooldownRemaining = Mathf.Max(CooldownRemaining, 0.5f);  // 하드캡
            }
        }

        public void Fire()
        {
            // 카테고리별 Schedule Dispatch (F 카테고리는 확률 판정 선행)
            if (Data.Category == ActiveCategory.SpecialJudge)
            {
                if (UnityEngine.Random.value > Data.FireProbability) return;
            }
            var ev = Simulation.Schedule<SkillFireEvent>();
            ev.Runtime = this;
            ev.Inventory = _inventory;
        }
    }
}

4-3. PlayerSkillInventory.Update 통합 제어

void Update()
{
    // 액티브 Cooldown 감산·발동
    foreach (var active in _activeSkills)
    {
        active.Tick(Time.deltaTime);
    }

    // 조건부 패시브 타이머 감산 (P17 감로수 등 OnTimer 계열)
    foreach (var passive in _passiveSkills)
    {
        if (!passive.IsAlwaysOn)
        {
            passive.OnTrigger(new PassiveTriggerContext
            {
                Kind = PassiveTriggerKind.OnTimer,
                TimeElapsed = Time.deltaTime
            });
        }
    }
}

4-4. 이벤트 트리거 구독 (OnHit·OnKill)

// PlayerSkillInventory.OnEnable
Health playerHealth = GetComponent<Health>();
playerHealth.OnPlayerDamagedEvent += OnPlayerDamagedHandler;  // Health에 이벤트 추가 필요 — BT7-Dev 확장 포인트
// EnemyDeath 이벤트는 Simulation Schedule에서 발생 → Simulation 커스텀 훅으로 구독

void OnPlayerDamagedHandler(float damageAmount)
{
    OnPlayerDamaged?.Invoke(damageAmount);
    // P11 호신부 등 OnPlayerDamaged 트리거 패시브·액티브 발동
    foreach (var active in _activeSkills.Where(a => a.Trigger == ActiveTrigger.OnHit))
        active.Fire();
    foreach (var passive in _passiveSkills.Where(p => !p.IsAlwaysOn))
        passive.OnTrigger(new PassiveTriggerContext
        {
            Kind = PassiveTriggerKind.OnPlayerDamaged,
            DamageTaken = damageAmount
        });
}

void OnEnemyKilledHandler(EnemyController enemy)
{
    OnEnemyKilled?.Invoke(enemy);
    // P16 단전수련 (처치 시 회복) 등 OnEnemyKilled 트리거
    foreach (var active in _activeSkills.Where(a => a.Trigger == ActiveTrigger.OnKill))
        active.Fire();
    foreach (var passive in _passiveSkills.Where(p => !p.IsAlwaysOn))
        passive.OnTrigger(new PassiveTriggerContext
        {
            Kind = PassiveTriggerKind.OnEnemyKilled,
            KillCount = _totalKillCount  // 인벤토리 누적
        });
}

BT7-Dev Health.cs 확장 필요 사항 (C39 작업 전 실측 기반 근본 개선안):

  • Healthpublic event Action<int> OnDamagedEvent 신설 — Decrement(int damage) 호출 시 발화
  • 기존 HealthIsZero 이벤트는 유지 (BT7-Dev 계승)
  • 본 변경은 Phase 2 클라이언트팀 구현 위임 시 포함 (후속 commit에서 Health.cs 확장)

§5. 각성 발동 조건 처리 (3 조건 동시 충족 매니저)

5-1. AwakeningManager 책임

namespace EerieVillage.Skills
{
    /// <summary>
    /// 각성 발동 3 조건 동시 충족 감지 + 발동 UI 진입 + 액티브 교체.
    /// PlayerSkillInventory 하위 서브시스템.
    /// </summary>
    public class AwakeningManager
    {
        // 조건 1 충족 상태: Lv.5 도달 액티브 ID 집합
        private readonly HashSet<string> _maxedActiveIds = new();

        // 조건 2 충족 상태: 현재 보유 패시브 ID 집합 (참조)
        // - PlayerSkillInventory가 실시간 유지

        // 이미 각성된 액티브 ID (중복 발동 차단)
        private readonly HashSet<string> _alreadyAwakenedIds = new();

        private readonly PlayerSkillInventory _inventory;
        private readonly List<AwakeningSkillData> _allAwakeningData;  // Resources 전체 로드

        public AwakeningManager(PlayerSkillInventory inventory)
        {
            _inventory = inventory;
            _allAwakeningData = Resources.LoadAll<AwakeningSkillData>("Data/Skills/Awakening").ToList();
        }

        /// <summary>액티브 Lv.5 도달 시 ActiveSkillRuntime.Upgrade에서 호출</summary>
        public void MarkActiveMaxed(ActiveSkillData data)
        {
            _maxedActiveIds.Add(data.CardId);
        }

        /// <summary>보물상자 개봉 시 PlayerSkillInventory.OnTreasureChestOpened가 호출</summary>
        public void CheckAwakeningEligibility()
        {
            var eligibleAwakenings = new List<AwakeningSkillData>();

            foreach (var awData in _allAwakeningData)
            {
                // 이미 각성됨 → 스킵
                if (_alreadyAwakenedIds.Contains(awData.CardId)) continue;

                // 조건 1: 원 액티브 Lv.5?
                if (!_maxedActiveIds.Contains(awData.OriginalActive.CardId)) continue;

                // 조건 2: 필요 패시브 1+ 보유?
                bool hasRequiredPassive = awData.RequiredPassives.Any(p =>
                    _inventory.HasPassive(p.CardId));
                if (!hasRequiredPassive) continue;

                eligibleAwakenings.Add(awData);
            }

            // 조건 3: 보물상자는 이미 개봉된 상태에서 호출됨 (진입 시점 보장)

            if (eligibleAwakenings.Count == 0)
            {
                // 대체 보상 처리 (balance-designer 결정 — 재화·회복·경험치)
                _inventory.GrantTreasureFallbackReward();
                return;
            }

            if (eligibleAwakenings.Count == 1)
            {
                // 단일 각성 즉시 발동
                TriggerAwakening(eligibleAwakenings[0]);
            }
            else
            {
                // 다중 각성 조건 동시 충족 — 플레이어 1개 선택 UI
                // UXDesigner 연계: ux/02_HUD_설계.md 각성 UI 섹션 참조
                _inventory.ShowAwakeningChoiceUI(eligibleAwakenings, TriggerAwakening);
            }
        }

        private void TriggerAwakening(AwakeningSkillData awData)
        {
            // 풀스크린 연출 Schedule (BT7-Dev Simulation 패턴)
            var ev = Simulation.Schedule<AwakeningEffectEvent>();
            ev.Data = awData;

            // 원 액티브 런타임 교체
            var runtime = new AwakeningSkillRuntime(awData);
            _inventory.ReplaceActiveWithAwakening(awData.OriginalActive.CardId, runtime);

            _alreadyAwakenedIds.Add(awData.CardId);
        }
    }
}

5-2. 3 조건 동시 충족 판정 테이블 (실행 시점)

조건 판정 시점 상태 저장 위치
1. 원 액티브 Lv.5 ActiveSkillRuntime.Upgrade에서 StackLevel == 5 도달 시 AwakeningManager._maxedActiveIds HashSet
2. 필요 패시브 1+ 보유 보물상자 개봉 시 _inventory.HasPassive(id) 실시간 조회 PlayerSkillInventory._passiveSkills
3. 보물상자 획득 스테이지 내 TreasureChest 오브젝트 OnTrigger 시 → OnTreasureChestOpened() 이벤트 기반 (별도 저장 불요)

조건 3 트리거가 마스터 — 보물상자 개봉 시점에만 1·2 조건을 판정. Lv.5 도달 시점이나 패시브 장착 시점에 즉시 판정하지 않음 (보물상자가 "게이팅 조건").

5-3. AwakeningSkillRuntime 동작 (원 액티브 대체)

public class AwakeningSkillRuntime : IAwakeningSkill, IActiveSkill
{
    public AwakeningSkillData Data { get; }
    public ActiveSkillData OriginalActive => Data.OriginalActive;
    public PassiveSkillData[] RequiredPassives => Data.RequiredPassives;
    public AwakeningPattern Pattern => Data.Pattern;
    public int StackLevel => 5;  // 각성은 Lv.5 고정

    // IActiveSkill 위임 — 기본적으로 원 액티브 동작 + 패턴별 변형
    public ActiveTrigger Trigger => Data.OriginalActive.Trigger;
    public float BaseCooldown => CalculateAwakeningCooldown();
    public float EffectiveCooldown => /* §4-2 동일 공식 */;
    public float CooldownRemaining { get; private set; }

    private float CalculateAwakeningCooldown() => Pattern switch
    {
        AwakeningPattern.ScaleUp => Data.OriginalActive.BaseCooldown * 0.5f,   // 빈도 2배
        AwakeningPattern.MultiFire => Data.OriginalActive.BaseCooldown,         // 주기 동일, 수량 2배
        AwakeningPattern.AddEffect => Data.OriginalActive.BaseCooldown,         // 주기 동일
        AwakeningPattern.GlobalSpread => Data.OriginalActive.BaseCooldown * 2f, // 화면 전체라 빈도 저감
        _ => Data.OriginalActive.BaseCooldown
    };

    public void Fire()
    {
        var ev = Simulation.Schedule<AwakeningFireEvent>();
        ev.Runtime = this;
        ev.Pattern = Pattern;
        // Pattern별 Dispatcher가 효과 발동 처리 — §5-4 참조
    }

    public void Awaken(PlayerSkillInventory inventory) { /* 초기 발동 연출 */ }
    public void OnEquip(PlayerSkillInventory inv) { /* 불요 — Awaken이 대체 */ }
    public void OnUnequip() { }
    public void Upgrade() { throw new System.InvalidOperationException("각성은 Lv 업 없음"); }

    public void Tick(float deltaTime) { /* §4-2 동일 로직 */ }
}

5-4. 각성 패턴별 Dispatcher

public class AwakeningFireEvent : Simulation.Event<AwakeningFireEvent>
{
    public AwakeningSkillRuntime Runtime;
    public AwakeningPattern Pattern;

    public override void Execute()
    {
        switch (Pattern)
        {
            case AwakeningPattern.ScaleUp:
                // 원 액티브 발동 + 판정 범위·대미지 배율 증폭
                FireOriginalWithMultiplier(3.0f);
                break;
            case AwakeningPattern.AddEffect:
                // 원 액티브 발동 + 추가 효과 (예: DoT·속성 부여)
                FireOriginal();
                FireAdditionalEffect(Runtime.Data);
                break;
            case AwakeningPattern.MultiFire:
                // 원 액티브 N회 동시 발동 (투사체 2배·소환물 3~4기)
                for (int i = 0; i < Runtime.Data.MultiFireCount; i++)
                    FireOriginal();
                break;
            case AwakeningPattern.GlobalSpread:
                // 화면 전체 AoE 1회 발동
                FireGlobalAoE(Runtime.Data);
                break;
        }
    }
}

MultiFireCount 필드 추가 필요AwakeningSkillData[SerializeField] public int MultiFireCount = 3; (각성별 Inspector 튠). balance-designer 이관.


§6. 카테고리·패턴 매핑 (6+5+4)

6-1. 액티브 6 카테고리 → 효과 발동기 매핑

카테고리 효과 발동기 (Unity 컴포넌트) BT7-Dev 재활용 신설 필요
A. 투사체 ProjectileSpawner + Projectile (BT.Framework 오브젝트 풀링 활용)
B. 근접·범위 AttackHitbox BT7-Dev 재활용 -
C. 설치·지속 AuraZone
D. 소환 MinionSpawner + MinionAI
E. 상태이상 DebuffApplier + DebuffStack
F. 특수 판정 분기 — AttackHitbox(A16) + IFrameExtender(A18) 부분 재활용 (A16 = Hitbox 확률 판정) (A18 i-frame 연장)

6-2. 패시브 5 카테고리 → PlayerStats 필드 매핑

카테고리 대상 카드 PlayerStats 필드 (§2-3)
P-A. 스탯 상승 P01·P02~P05·P09·P10 DamageMultiplier·AttributeMultiplier·CritChance·CritDamage
P-B. 주기·증폭 P06·P07·P08 CooldownMultiplier·AreaMultiplier·ExtraProjectiles
P-C. 생존 강화 P11·P12·P13·P14·P15 조건부(OnTrigger) + ExtraMaxHearts·DamageReduction·EvasionChance·IFrameExtend
P-D. 회복 P16·P17 조건부(OnTrigger OnEnemyKilled/OnTimer) → Health.Heal(1) 호출
P-E. 자원 확장 P18·P19·P20 MoveSpeedMultiplier·XPMultiplier·TreasureFindBonus

특수 처리 — 최대 하트 수 증가 (P12·P13):

// PassiveSkillRuntime.ApplyModifier (P-C 대상 TargetStat == MaxHearts)
public void ApplyModifier(PlayerStats stats)
{
    if (Data.TargetStat == StatType.MaxHearts)
    {
        stats.ExtraMaxHearts += Mathf.RoundToInt(Data.BaseModifierValue * StackLevelFactor(StackLevel));
        // Health.IncreaseMaxHearts 호출 — BT7-Dev API 재활용
        var playerHealth = _inventory.GetComponent<Health>();
        playerHealth.IncreaseMaxHearts(Mathf.RoundToInt(Data.BaseModifierValue));
    }
}

6-3. 각성 4 패턴 → AwakeningFireEvent 분기

§5-4 Dispatcher에 정의됨. 패턴별 Unity 구현 복잡도:

패턴 구현 복잡도 주요 리스크
1. 스케일업 낮음 (기존 발동기에 배율만 적용) 밸런스 치우침
2. 새효과 추가 중간 (원 효과 + 추가 효과 병렬 실행) 이벤트 순서 관리
3. 다중 발동 중간 (동시 N회 발동 + 풀링 용량) 파티클 상한 초과 (기획 §8 개발팀 확인 1)
4. 광역 확산 높음 (화면 전체 AoE + 연쇄 전파) 성능 이슈 (기획 §8 개발팀 확인 1 — 파티클·충돌 연산 배치 최적화 필수)

§7. 1단계 산출물 (개발팀장 설계) — 본 Phase 완결

7-1. Phase 1 완결 확증 체크리스트

  • §1 아키텍처 4계층 정의 완료
  • §2 인터페이스 4종(ISkillRuntime·IActiveSkill·IPassiveSkill·IAwakeningSkill) + ScriptableObject 3종(ActiveSkillData·PassiveSkillData·AwakeningSkillData) + PlayerSkillInventory·PlayerStats 정의 완료
  • §3 CSV → ScriptableObject → Runtime → Health.Decrement 데이터 흐름 완비 (카테고리 매핑 테이블 포함)
  • §4 자동 발동 사이클 3 트리거(OnTime·OnHit·OnKill) + ActiveSkillRuntime.Tick 구현 설계
  • §5 각성 3 조건 동시 충족 매니저 + 패턴별 Dispatcher 설계
  • §6 카테고리·패턴 매핑 완비 (6+5+4 전수)
  • §10 BT7-Dev 통합 영역 명시

7-2. Phase 2 위임 작업 단위 분해 (클라이언트팀 Sonnet Task)

후속 PM 차원 위임 권고 (본 Task 범위 초과 — C48·C50 준수 판단):

Phase 2는 PM이 별도 Task로 클라이언트팀 Sonnet에 위임하는 것이 C48 3자문 통과 기준에 정합. 본 Phase 1 Task에서 추가 Sonnet 호출 시 토큰 과도 + 단일 Task 범위 과대. 구현 작업 단위 분해는 다음과 같이 명시:

Phase 2-A. 인터페이스·ScriptableObject 코드 생성 (Unity 외부 레포 편집):

  • Assets/Scripts/Skills/{ISkillRuntime.cs, IActiveSkill.cs, IPassiveSkill.cs, IAwakeningSkill.cs} 4개 신설
  • Assets/Scripts/Skills/Data/{SkillDataAsset.cs, ActiveSkillData.cs, PassiveSkillData.cs, AwakeningSkillData.cs} 4개 신설
  • Assets/Scripts/Skills/Runtime/{ActiveSkillRuntime.cs, PassiveSkillRuntime.cs, AwakeningSkillRuntime.cs, PlayerSkillInventory.cs, PlayerStats.cs, AwakeningManager.cs, SkillRuntimeFactory.cs} 7개 신설
  • Assets/Scripts/Skills/Events/{SkillFireEvent.cs, AwakeningFireEvent.cs, AwakeningEffectEvent.cs} 3개 신설

Phase 2-B. 효과 발동기 신설:

  • Assets/Scripts/Skills/Effectors/{ProjectileSpawner.cs, Projectile.cs, AuraZone.cs, MinionSpawner.cs, MinionAI.cs, DebuffApplier.cs, DebuffStack.cs} 7개 신설
  • 기존 AttackHitbox.cs는 B 카테고리 효과 발동기로 재활용 (코드 변경 불요)

Phase 2-C. BT7-Dev 자산 확장:

  • Assets/Scripts/Mechanics/Health.csevent Action<int> OnDamagedEvent 신설 (Decrement 호출 시 발화)
  • Assets/Scripts/Mechanics/PlayerController.csPlayerSkillInventory 참조 필드 추가 ([RequireComponent])

Phase 2-D. 에디터 툴 구현:

  • Assets/Editor/Skills/SkillCsvImporter.cs 신설 (CSV v0.3 → 60종 .asset 변환)
  • Assets/Data/Skills/{Active,Passive,Awakening}/*.asset 60종 생성 (에디터 툴 1회 실행)

Phase 2-E. EditMode 테스트 확장:

  • 기존 BT7-Dev PlayerAttackTests.cs 13건 유지
  • 신설 Assets/Tests/Editor/SkillSystemTests.cs — 인터페이스 계약·Factory·각성 조건 판정·CSV 파싱 검증 (최소 10건)

Phase 2 예상 작업량: 스크립트 약 25개 신설 + 테스트 10건 + asset 60개. 단일 Task 범위. 클라이언트팀 Sonnet 적정.


§8. 2단계 산출물 (클라이언트팀 구현) — 본 Phase 위임 대기

본 섹션은 Phase 2 클라이언트팀 구현 결과 수령 후 갱신 예정. 현재 미이행.


§9. 3단계 산출물 (개발팀장 검증) — 본 Phase 위임 대기

본 섹션은 Phase 2 완료 후 개발팀장 Opus가 검증 결과 기입 예정. 현재 미이행.

검증 항목 (사전 명시):

  1. C11 개발 관점 3축 정합 — 자원 효율(파티클·GC)·코드 직관성(4계층 구조 명확)·범용성(ISkillRuntime 인터페이스 일관)
  2. 기획서 SOT 정합 — 60종 카테고리·패턴 전수 처리 가능 확증
  3. BT7-Dev 충돌 없음Health·PlayerAttack·PlayerAttackTicker·AttackHitbox·PlayerController 계약 변경 최소
  4. BT.Framework 재활용 적정성 — 오브젝트 풀링(Projectile·Minion) 활용 여부
  5. EditMode 테스트 10건 + 기존 13건 전부 green
  6. balance-designer 이관 항목 명시 — 수치·확률·쿨다운 하드캡 등

§10. BT7-Dev 통합 영역

10-1. 완전 보존 자산 (코드 변경 없음)

BT7-Dev 자산 본 설계 활용
Health.cs: maxHearts·QuartersPerHeart·Decrement(int)·Heal(int)·IncreaseMaxHearts(int)·CurrentHP·IsAlive·Die()·Increment() P-C·P-D 패시브 효과의 적용 API
Health.cs: i-frame 0.6s 보호 로직 본 설계 전제 (연속 피격 방지). 확률 회피(P15)는 Decrement 호출 전 분기
PlayerAttack : Simulation.Event 기본 무기 발동 이벤트로 유지 (스킬 시스템 이벤트는 별도)
PlayerAttackTicker 기본 무기 주기 타이머. 스킬 슬롯 액티브는 독립 ActiveSkillRuntime.Tick
AttackHitbox: Fire(Vector2)·size·offsetDistance·activeDuration·damage·targetLayers B 카테고리 스킬 효과 발동기 재활용
PlayerController: Facing·controlEnabled·animator 투사체 방향·발동 제어·애니메이션 연동
InputSystem_Actions Attack 액션 제거 상태 VS 순수형 정합 — 공격 입력 채널 없음

10-2. 확장 필요 자산 (Phase 2 클라이언트팀 구현 범위)

BT7-Dev 자산 확장 내용 위험도
Health.cs public event Action<int> OnDamagedEvent 신설 (Decrement 시 발화) 낮음 (기존 호출자 영향 없음)
PlayerController.cs PlayerSkillInventory 참조 필드 추가·[RequireComponent] 추가 낮음 (필드 추가만)
Player.prefab PlayerSkillInventory 컴포넌트 부착 (PD 수동 작업 or 에디터 툴) 중간 (Inspector 작업 필요)

10-3. BT.Framework 재활용 영역 (P29 준수)

  • 오브젝트 풀링ProjectileSpawner·MinionSpawner·AuraZone 3종의 인스턴스 관리에 BT.Framework Tier 1 오브젝트 풀 활용
  • 로깅·검증ValidationEx·Log Tier 1 유틸리티로 런타임 오류 감지
  • SafeAreaBorder — 풀스크린 각성 연출 시 Safe Area 참조

§11. 기각안

11-1. "ISkillRuntime 없이 ActiveSkillRuntime·PassiveSkillRuntime·AwakeningSkillRuntime을 독립 추상 클래스로 분리" — 기각

기각 근거 (C11 코드 구조 직관성):

  • PlayerSkillInventory가 3개의 독립 리스트를 별도 관리 시 "모든 스킬에 공통 작업" (Upgrade·OnEquip·저장/로드) 구현 시 3 경로 중복
  • 단일 ISkillRuntime 인터페이스로 공통 계약 + 각 파생 인터페이스로 특화 계약 분리 → SOLID ISP·LSP 동시 만족
  • 채택안: ISkillRuntime base + IActiveSkill·IPassiveSkill·IAwakeningSkill 확장

11-2. "PlayerStats를 MonoBehaviour로 구현 (Inspector 가시화)" — 기각

기각 근거 (C11 자원 효율):

  • MonoBehaviour는 GC 오버헤드·Unity Lifecycle 부담
  • PlayerStats는 런타임 동안 패시브 적용·조회만 빈번하게 발생 → 순수 POCO 클래스가 적정
  • 디버깅 시 Inspector 가시화 필요 시 PlayerSkillInventory[SerializeField]로 최신값 표시
  • 채택안: 순수 POCO + PlayerSkillInventory.Stats public 프로퍼티

11-3. "스킬 효과를 Simulation.Event 기반 Schedule 대신 직접 MonoBehaviour 발동으로 구현" — 기각

기각 근거 (BT7-Dev 패턴 일관성 + C11 코드 구조 직관성):

  • BT7-Dev가 Schedule<PlayerAttack>·Schedule<EnemyDeath> 패턴을 일관 적용 — 본 설계가 다른 발동 체계 도입 시 코드 읽기 부담
  • 이벤트 체계의 장점: 실행 순서 관리·일시 정지·재연 디버깅
  • 채택안: 모든 효과 발동을 Simulation.Schedule<SkillFireEvent> 등 Event 패턴으로 통일

11-4. "CSV 파싱을 런타임에 실행 (StreamingAssets)" — 기각

기각 근거 (C11 자원 효율 + 기획팀 피드백 루프):

  • 런타임 CSV 파싱은 모바일 IO 부담 + 파싱 에러의 늦은 감지
  • ScriptableObject는 Unity 시리얼라이즈 최적화 + AssetBundle 적용 가능
  • 기획팀 편집 → 에디터 툴 1회 실행 → 버전 관리되는 asset → PD Play 검증 워크플로우가 이미 확립됨 (BT7-Dev PlayerTestGirl 패턴)
  • 채택안: 에디터 툴 SkillCsvImporter로 CSV → 60종 .asset 변환, 런타임은 ScriptableObject만 참조

11-5. "각성 조건 3종 중 보물상자 대신 스테이지 클리어로 트리거" — 기각

기각 근거 (기획서 §4-1 VS 원작 동일 준수 + 기획 SOT 위배 금지):

  • 기획 system/01_카드_시스템.md v0.2 규칙 5에서 "보물상자 획득"을 조건 3으로 명시 확정
  • 스테이지 클리어 트리거 변경은 기획팀 방향 변경 필요 (PD 결정 영역 — C36-2 c)
  • 개발팀 자의로 변경 시 기획 SOT 위배 (C39 시스템 반영 실측 위반)
  • 채택안: 기획 SOT 그대로 — 보물상자 개봉 시점에 AwakeningManager.CheckAwakeningEligibility() 호출

§12. 변경 이력

일시 변경 사유 기안
2026-04-24 v1.0 Phase 1 설계 — 아키텍처 4계층·인터페이스 4+4종·데이터 흐름·자동 발동 사이클·각성 조건 매니저·카테고리 매핑 6+5+4·BT7-Dev 통합 영역 BT12-Dev PD 직접 지시 "스킬 시스템 설계 진행" + C49 1단계 시범 적용 개발팀장
2026-05-15 §A10 분신 스킬 상세 설계 신설 — PD 명세 5항목 (위치·반투명·동일 스킬·50% 반감·0.5초 딜레이) + 구조 결정 (CloneInstance 단일 + 0.5초 지연 큐) + 신규/수정 파일 13종 분해 + 기각안 5건 + EditMode 7건 + 3단계 검증 항목 7종 BT12-Dev-Clone PD 직접 지시 + C49 1단계 시범 (개발팀장 Opus 설계 → 클라이언트팀 Sonnet 구현 → 개발팀장 검증) 개발팀장

§13. 후속 안건 (PM 보고용)

PM 결정·위임 필요

  1. Phase 2 클라이언트팀 Sonnet 위임 Task 생성 — 본 설계 §7-2 Phase 2-A~E 작업 단위 기반. C48 3자문 통과 (영역 전문성·구현 코드 작성 = Sonnet 적정). C50 토큰 추정 클라이언트팀 Task 단일 건 기준 50~80K (대용량) → PD 사전 안내 검토 필요
  2. Phase 3 개발팀장 검증 Task 생성 — Phase 2 완료 후 개발팀장 Opus 검증. §9 검증 항목 6종 기반
  3. balance-designer 이관 — 60종 수치(BaseDamage·BaseCooldown·FireProbability·DebuffStackLimit·MultiFireCount·AwakeningBaseDamage) 확정 Task 기획팀장 경유 위임
  4. narrative-designer 이관 대기 — 카드명 세계관 재매핑(조선 퇴마 톤)은 PD 후속 검토 대기 (기획 CSV v0.3에 영문명만 있음)

차단 블로커

  • Unity 프로젝트 경로 paths.local.json 미설정 (UNITY_PROJECT_ROOT: __SET_PER_PC__). Phase 2 클라이언트팀 구현 위임 시 PD에게 PC 경로 설정 요청 필요 또는 BT7-Dev 참조 경로(D:/NerdNavis/EerieVillage 대화로그 인용) 재확인

개발팀 C11 확인 완료 사항

  • §1-4 BT7-Dev 자산 활용 확증 (Health·PlayerAttack·PlayerAttackTicker·AttackHitbox·PlayerController 전수)
  • §6-3 광역 확산 패턴(AW01·AW03·AW06·AW13·AW19) 성능 최적화 필수 확인 (기획 §8 개발팀 확인 1)
  • §6-3 다중 발동 패턴(AW09·AW10·AW15·AW20) 동시 오브젝트 상한·풀링 용량 확인 (기획 §8 개발팀 확인 2)
  • §10-3 BT.Framework Tier 1 오브젝트 풀 재활용 방향 확정 (P29 준수)

§A10. 분신 스킬 상세 설계 (BT12-Dev-Clone 2026-05-15)

PD 직접 지시 (2026-05-15) 원문:

"1. 이번에는 분신 스킬을 구현해줘. 2. 이 스킬을 사용하면 플레이어의 x좌표 1 뒤쪽에 반투명한 형태의 플레이어의 분신이 생성되어서 플레이와 동일한 스킬을 사용해야 해. (단, 분신의 공격은 플레이어의 공격력의 50% 반감되어야하고, 플레이어보다 0.5초 뒤 사용해야 해. (공격 시작 딜레이))"

C49 표준 프로세스 시범 — 1단계 (개발팀장 Opus 설계).

A10-1. PD 명세 5항목 확정

# 항목 PD 명세 (원문 인용)
1 위치 "플레이어의 x좌표 1 뒤쪽" — facing 반대 방향 1유닛 (예: facing=오른쪽 → 분신 x = player.x - 1)
2 외형 "반투명한 형태의 플레이어의 분신" — Player sprite 복제 + alpha 0.5 (개발팀장 재량)
3 동작 "플레이와 동일한 스킬을 사용" — Player 장착 액티브 발동 시 분신도 동일 카드 발동
4 공격력 "분신의 공격은 플레이어의 공격력의 50% 반감" — damage * 0.5
5 타이밍 "플레이어보다 0.5초 뒤 사용 (공격 시작 딜레이)" — Player Fire 시점 + 0.5초 지연 큐

기획 SOT v0.4 CSV A10 행 정합 (인용):

A10,액티브,분신,D. 소환,긴 주기로 분신 1기를 생성한다. 플레이어 공격 패턴을 동일하게 모방해 자동 공격 (분신은 무적이나 공격력은 플레이어보다 비율 감소),추가 공격 기회

PD 본 발화에 없는 v0.4 CSV 명세 합리적 기본값 (PM 1차 안 채택):

  • 분신 lifetime: 12초 자동 소멸 + Singleton 1기 유지 (PD 2026-05-15 직접 결정) — spawn 시점 unscaledTime + 12초 후 자동 destroy. 12초 내 재발동 시 기존 분신 destroy + 새 분신 spawn (Singleton 패턴·BaseCooldown 25초 정합)
  • BaseCooldown: 25초 (PD 2026-05-15 직접 결정 — PM 1차 30초 → PD 조정 25초)
  • 분신 무적: 유지 — v0.4 명시 + PD 2026-05-15 직접 결정 "무적으로 진행 (Collider 미부착)". 적 투사체·벽·player·enemy 모두 통과
  • collision: wall·player·enemy 모두 무충돌 (반투명·무적 정합) — 시각 GameObject 전용
  • Lv 업 메커니즘 (PD 2026-05-15 직접 결정): 분신 수 증가 X (1기 고정). 추후 지속시간 ↑ + 플레이어 참조 데미지 비율(%) ↑ (PassiveSkill Lv 업 시 분신 lifetime·damage multiplier 증가 명세 — balance-designer 후속 수치 확정)

A10-2. 구조 결정 — 3개 옵션 검토

옵션 설명 장단점
(가) CloneController 별도 GameObject + 자체 PlayerSkillInventory mirror (장) 분신 = 독립 actor·테스트 격리 (단) 코드 중복 — 인벤토리·Lv·각성·이벤트 구독 2중 동기화 부담
(나) CloneInstance 단일 MonoBehaviour + Player Inventory hook — Player Fire 시 0.5초 지연 큐 enqueue → 분신 위치 anchor로 동일 Effector 재호출 + damage 50% 반감 (장) Effector 코드 재활용·인벤토리 단일 SOT 유지·동기화 부담 0 (단) Effector가 inventory 컨텍스트 분기 필요 — PlayerStats 50% 반감 처리
(다) Effector 한 번 호출 시 Player+Clone 2회 발동 (분신 = sprite만) (장) 최소 구현 (단) "0.5초 뒤" PD 명세 위반 — 즉시 2회 발동은 spec 어긋남

채택 = (나). 근거:

  1. PD 명세 5번 (0.5초 딜레이) 충족: (다) 즉시 발동은 spec 위반
  2. PD 명세 3번 (동일 스킬): Effector 재활용으로 13종 카드 무차별 지원
  3. C11 자원 효율: 별도 Inventory mirror 부재 — 메모리·GC 부담 최소
  4. C11 코드 직관성: 분신 = "Player Fire 0.5초 뒤 분신 위치에서 동일 Effector 호출 + 50% damage" 단 1줄 멘탈 모델

A10-3. 신규 컴포넌트 — CloneInstance MonoBehaviour

파일: Assets/Scripts/Skills/Effectors/CloneInstance.cs (신규)

계약:

namespace EerieVillage.Skills.Effectors
{
    /// <summary>
    /// A10 분신 인스턴스. Player 자식 부착 X (독립 GameObject — Player 이동 시 위치 미동조·고정 위치 1기).
    /// PD 명세 (2026-05-15):
    ///   - Player x좌표 1 뒤쪽 spawn (facing 반대 방향 1유닛)
    ///   - 반투명 sprite (alpha 0.5)
    ///   - Player 발동 시 0.5초 뒤 동일 Effector 발동
    ///   - damage 50% 반감
    ///   - 무적·무충돌 (collider 미부착)
    /// </summary>
    public class CloneInstance : MonoBehaviour
    {
        // 정적 Singleton — 분신 1기 유지 (재발동 시 기존 destroy)
        private static CloneInstance _current;

        private PlayerSkillInventory _playerInventory;
        private float _spawnFacingX;  // spawn 시점 Player facing.x (분신 facing 고정)

        // 지연 큐: (발동 시각, Runtime, Inventory 컨텍스트)
        private readonly Queue<PendingFire> _pendingQueue = new Queue<PendingFire>();

        private struct PendingFire
        {
            public float TriggerTime;        // unscaledTime + 0.5초
            public ActiveSkillRuntime Runtime;
        }

        public static void SpawnOrReplace(PlayerSkillInventory playerInventory, ActiveSkillData cloneData) { /* §A10-4 */ }

        public void EnqueuePlayerFire(ActiveSkillRuntime runtime) { /* §A10-5 */ }

        void Update() { /* §A10-5 — 큐 dequeue + Effector 재호출 */ }

        void OnDestroy() { /* Player Inventory unsubscribe + _current 정리 */ }
    }
}

A10-4. 분신 spawn 로직 (CloneEffector)

파일: Assets/Scripts/Skills/Effectors/CloneEffector.cs (신규)

namespace EerieVillage.Skills.Effectors
{
    /// <summary>
    /// A10 분신 Effector — Category D (Minion) 카테고리에서 CardId == "A10" 분기.
    /// SkillFireEvent.Execute 영역 Minion case 분기:
    ///   if (data.CardId == "A10") effector = new CloneEffector();
    ///   else effector = new SpiritFireSpawner();  (기존 A11)
    /// </summary>
    public class CloneEffector : IEffector
    {
        public void Trigger(ActiveSkillRuntime runtime, PlayerSkillInventory inventory)
        {
            // 분신 spawn 또는 교체 (Singleton)
            CloneInstance.SpawnOrReplace(inventory, runtime.ActiveData);
            // 분신 자체는 "발동"이 아닌 "생성" — 발동은 Player Fire 시 분신 인스턴스가 hook
        }
    }
}

CloneInstance.SpawnOrReplace 동작:

public static void SpawnOrReplace(PlayerSkillInventory playerInventory, ActiveSkillData cloneData)
{
    // 1. 기존 분신 destroy (1기 유지)
    if (_current != null) { Destroy(_current.gameObject); _current = null; }

    // 2. Player 위치·facing 취득
    var pc = playerInventory.GetComponent<PlayerController>();
    Vector2 facing = pc != null ? pc.Facing : Vector2.right;
    float signX = facing.x < 0f ? -1f : 1f;
    Vector2 playerPos = playerInventory.transform.position;

    // 3. PD 명세 — facing 반대 1유닛 (예: facing=오른쪽 → -1유닛)
    Vector2 spawnPos = playerPos + new Vector2(-signX * 1f, 0f);

    // 4. Player sprite clone — SpriteRenderer 복제 + alpha 0.5
    var go = new GameObject("Clone_A10");
    go.hideFlags = HideFlags.DontSave;
    go.transform.position = spawnPos;

    var playerSr = playerInventory.GetComponentInChildren<SpriteRenderer>();
    if (playerSr != null)
    {
        var sr = go.AddComponent<SpriteRenderer>();
        sr.sprite = playerSr.sprite;
        sr.flipX = playerSr.flipX;  // Player와 동일 방향 시각 (PD 명세 "분신" 외형)
        sr.sortingOrder = playerSr.sortingOrder - 1;  // Player 뒤쪽
        Color c = playerSr.color;
        c.a = 0.5f;  // 반투명
        sr.color = c;
    }

    // 5. CloneInstance 컴포넌트 부착 + Player Inventory hook 구독
    var instance = go.AddComponent<CloneInstance>();
    instance._playerInventory = playerInventory;
    instance._spawnFacingX = signX;
    _current = instance;

    // 6. Player Fire 이벤트 구독 — 새 hook 신설 필요 (§A10-6)
    playerInventory.OnPlayerSkillFired += instance.EnqueuePlayerFire;
}

A10-5. Player Fire hook + 0.5초 지연 큐

PlayerSkillInventory 신규 이벤트 (§A10-6 확장):

// PlayerSkillInventory.cs 신설 영역
public event System.Action<ActiveSkillRuntime> OnPlayerSkillFired;

// ActiveSkillRuntime.Fire() 영역에 hook 추가:
//   - Fire() 호출 시 Simulation.Schedule<SkillFireEvent> 발동 직후
//   - inventory.OnPlayerSkillFired?.Invoke(this) 호출
// 이렇게 함으로써 분신이 자체적으로 Player Fire 시점을 감지 → 0.5초 지연 큐 enqueue

CloneInstance Update + enqueue 로직:

public void EnqueuePlayerFire(ActiveSkillRuntime runtime)
{
    // A10 분신 자체 발동 = 무한 재귀 방지 (분신은 분신을 발동하지 않음)
    if (runtime.ActiveData.CardId == "A10") return;

    _pendingQueue.Enqueue(new PendingFire {
        TriggerTime = Time.unscaledTime + 0.5f,  // PD 명세 0.5초 딜레이
        Runtime = runtime
    });
}

void Update()
{
    while (_pendingQueue.Count > 0 && _pendingQueue.Peek().TriggerTime <= Time.unscaledTime)
    {
        var pending = _pendingQueue.Dequeue();
        FireAtClonePosition(pending.Runtime);
    }
}

private void FireAtClonePosition(ActiveSkillRuntime runtime)
{
    var data = runtime.ActiveData;
    if (data == null) return;

    // 분신 위치 anchor로 Effector 재호출
    // 핵심 패턴: PlayerStats.CloneDamageMultiplier (또는 _isCloneFire 플래그) 분기로 damage 50% 반감
    // 옵션 A — _playerInventory.Stats 임시 백업·multiplier 0.5 적용·Effector 호출·복구
    // 옵션 B — CloneInventoryWrapper 별도 객체 (Stats 사본·DamageMultiplier *= 0.5) — 채택

    var cloneCtx = CloneFireContext.Create(_playerInventory, transform.position, _spawnFacingX);
    IEffector effector = ResolveEffector(data);
    if (effector != null) effector.Trigger(runtime, cloneCtx.WrappedInventory);
}

CloneFireContext (신규 헬퍼 — 별도 inventory 어댑터):

internal static class CloneFireContext
{
    // PlayerSkillInventory 직접 수정 없이 분신 발동 컨텍스트 분리
    // 옵션 1 — PlayerSkillInventory.IsCloneFireContext 플래그 (간단)
    // 옵션 2 — 별도 wrapper (코드 직관성 ↑)
    // 채택 = 1 — Effector 변경 최소 (PlayerSkillInventory 1 필드 추가만)
}

최종 채택 = 옵션 1 (PlayerSkillInventory 플래그):

// PlayerSkillInventory.cs 신설 필드
internal bool IsCloneFireActive = false;
internal Vector2 CloneFireOrigin = Vector2.zero;
internal float CloneFireFacingX = 1f;
internal const float CLONE_DAMAGE_MULTIPLIER = 0.5f;

각 Effector에서 inventory.IsCloneFireActive 분기:

  • spawn 위치 = inventory.CloneFireOrigin (분신 위치) 사용
  • facing = Vector2(inventory.CloneFireFacingX, 0) 사용
  • damage = 기존 계산 결과 * CLONE_DAMAGE_MULTIPLIER (0.5)

ActiveSkillRuntime.CalculateEffectiveDamage() 영역에 단 1줄 추가:

public int CalculateEffectiveDamage()
{
    // ... 기존 ...
    float effective = ActiveData.BaseDamage * stats.DamageMultiplier * StackLevelFactor(StackLevel) * attrMult;
    if (_inventory != null && _inventory.IsCloneFireActive) effective *= PlayerSkillInventory.CLONE_DAMAGE_MULTIPLIER;
    return Mathf.RoundToInt(effective);
}

위치·facing 분기는 Effector 진입점 (ProjectileSpawner·MeleeAreaSpawner·LightningStrikeSpawner·LaserSpawner·PoisonSwampSpawner·SpiritFireSpawner) 영역 inventory.transform.position 대신:

Vector2 anchorPos = inventory.IsCloneFireActive ? inventory.CloneFireOrigin : (Vector2)inventory.transform.position;
Vector2 facing = ...; if (inventory.IsCloneFireActive) facing = new Vector2(inventory.CloneFireFacingX, 0);

CloneInstance.FireAtClonePosition:

private void FireAtClonePosition(ActiveSkillRuntime runtime)
{
    var inv = _playerInventory;
    inv.IsCloneFireActive = true;
    inv.CloneFireOrigin = transform.position;
    inv.CloneFireFacingX = _spawnFacingX;
    try
    {
        // SkillFireEvent.Execute 영역 그대로 재호출
        var ev = Simulation.Schedule<SkillFireEvent>();
        ev.Runtime = runtime;
        ev.Inventory = inv;
    }
    finally
    {
        // Schedule 즉시 처리 X — 1 frame 후 reset (또는 SkillFireEvent.Execute 마지막에 reset)
        // 채택: SkillFireEvent.Cleanup 영역 IsCloneFireActive=false 일관 reset
    }
}

reset 시점 결정 안건 (3단계 검증 영역):

  • (가) SkillFireEvent.Cleanup 영역 reset — Simulation 일관 시점·자동
  • (나) 다음 frame Coroutine — race 회피 (다른 Player Fire 영역 영향 차단)
  • 채택 = (가) — Simulation.Event Cleanup 영역 일관 reset

A10-6. ActiveSkillRuntime.Fire() 영역 OnPlayerSkillFired 발화

기존 코드:

public void Fire()
{
    if (ActiveData.Category == ActiveCategory.SpecialJudge) { /* 확률 판정 */ }
    var ev = Simulation.Schedule<SkillFireEvent>();
    ev.Runtime = this;
    ev.Inventory = _inventory;
}

확장 1줄 추가:

public void Fire()
{
    if (ActiveData.Category == ActiveCategory.SpecialJudge) { /* 확률 판정 */ }
    var ev = Simulation.Schedule<SkillFireEvent>();
    ev.Runtime = this;
    ev.Inventory = _inventory;

    // BT12-Dev-Clone 2026-05-15 — A10 분신 hook
    if (_inventory != null && !_inventory.IsCloneFireActive)
        _inventory.OnPlayerSkillFired?.Invoke(this);
}

IsCloneFireActive 분기가 중요 — 분신 발동도 OnPlayerSkillFired 호출 시 무한 재귀. Clone 컨텍스트에서는 발화하지 않음.

A10-7. SkillFireEvent.Execute 영역 Minion case + CardId 분기

기존:

case ActiveCategory.Minion:
    effector = new SpiritFireSpawner();
    break;

수정:

case ActiveCategory.Minion:
    if (data.CardId == "A10") effector = new CloneEffector();
    else effector = new SpiritFireSpawner();
    break;

Cleanup 영역 추가 (분신 발동 reset):

internal override void Cleanup()
{
    if (Inventory != null) Inventory.IsCloneFireActive = false;
    Runtime = null;
    Inventory = null;
}

A10-8. A10 ActiveSkillData .asset 핵심 필드

파일: Assets/Resources/Skills/Active/A10_bunsin.asset (신규)

A11_jeongnyeongbul.asset 동등 패턴 + A10 고유 필드:

필드 근거
CardId "A10" v0.4 CSV
DisplayName "분신" v0.4 CSV
EnglishName "Clone" 일관성
Description "긴 주기로 분신 1기를 생성한다. 플레이어 공격 패턴을 동일하게 모방해 자동 공격..." v0.4 CSV 인용
AttributeTags 0 (None) 분신 자체 속성 X — Effector 발동 시 카드별 속성
TypeTags 0 (None) 동일
maxLevel 5 표준
Category 3 (Minion) v0.4 CSV "D. 소환"
Trigger 0 (OnTime) 표준
BaseCooldown 25 PD 결정 2026-05-15 (PM 1차 30 → PD 조정 25). balance-designer 후속 Lv별 증가량 확정
BaseDamage 0 분신 자체 damage X — Effector 재호출 시 Player damage * 0.5
HitboxSize (0,0) 분신 자체 판정 X
OffsetDistance (0,0) 분신 자체 spawn offset = facing 반대 1유닛 (코드 하드코딩 — PD 명세 정합)
MinionLifetime 12 PD 결정 2026-05-15 — 12초 자동 소멸 + Singleton 1기 (BaseCooldown 25 < lifetime 12 영역 활성 중 재발동 시 25초마다 갱신). Lv 업 시 추후 증가 (balance-designer 후속)
OnHitFxPrefab null 분신 sprite는 코드 spawn (Player sprite 복제)
기타 기본값 -

PD 결정 완료 (2026-05-15): BaseCooldown 25·MinionLifetime 12·facing 고정·무적 Collider 미부착·Lv 업 시 분신 수 X (지속시간+데미지 비율(%)↑·balance-designer 후속 수치).

A10-9. SkillRuntimeFactory.AvailableCardIds 추가

static readonly HashSet<string> AvailableCardIds = new HashSet<string>
{
    "A02", "A13", "A04", "A05", "A_Laser",
    "A08", "A12",
    "A06", "A11",
    "A10"  // BT12-Dev-Clone 2026-05-15 신규
};

A10-10. 무적·무충돌 처리

PD 명세 "분신은 무적" + 본 PM 합리적 기본값 "wall·player·enemy 모두 무충돌":

요소 처리
Collider2D 미부착 — 무충돌 자동 (Trigger·Wall·Enemy 판정 모두 비활성)
Rigidbody2D 미부착 — 물리 영향 X (중력·관성 모두 무관)
Health 컴포넌트 미부착 — 적 투사체 Decrement 호출 X — Health 검색 대상 외
SpriteRenderer 부착 — 시각 전용 (sortingOrder Player 뒤쪽)

PD 검증 권고: Play 시 분신 위에 적이 올라타거나 적 투사체 통과 — 무영향 검증.

A10-11. 분신 위치 동작 — 고정 vs Player 동조

옵션 A — 고정 위치 (Player 이동 시 분신 그대로):

  • spawn 시점 Player 위치 - 1유닛 = 분신 고정 위치
  • Player가 이동해도 분신은 그 자리 유지 → 분신 발동도 분신 위치에서 발동
  • VS류 게임 분신 표준 동작 (Vampire Survivors Pummarola 등)

옵션 B — Player 동조 (Player 자식 부착):

  • 분신이 항상 Player x-1 위치 추종
  • "분신" 의미와 정합도 ↑ ("분신"은 분신·복제·따라다님)

채택 = B (Player 자식 부착) — 근거:

  1. PD 명세 "플레이어의 x좌표 1 뒤쪽" — 현재형 표현 = Player 이동 시 분신도 동조 자연
  2. v0.4 CSV "플레이어 공격 패턴을 동일하게 모방" — 분신이 Player 따라다니며 모방 = 자연
  3. 분신 1기 + Player 영구 동조 = 시각적 일관성

구현: go.transform.SetParent(playerInventory.transform, true); + spawn 시 localPosition = (-signX * 1f, 0, 0)

facing 변경 시: Player facing 변경 시 분신의 "x 1 뒤쪽" 의미가 바뀜 (오른쪽→왼쪽 변경 시 분신은 원래 -1 → 새로운 -1 = Player 오른쪽 +1). PD 명세 모호 영역 — 3단계 검증 시 PD 결정 안건.

PM 1차 채택: spawn 시점 facing 고정. Player facing 변경되어도 분신 위치 (spawn 시점 기준 -1유닛) 유지. 분신의 facing은 Player flipX 동조 (시각 일관성).

A10-12. 변경 영향 — 기존 코드 수정 영역

파일 변경
Assets/Scripts/Skills/Effectors/CloneInstance.cs 신규
Assets/Scripts/Skills/Effectors/CloneEffector.cs 신규
Assets/Scripts/Skills/Runtime/PlayerSkillInventory.cs IsCloneFireActive·CloneFireOrigin·CloneFireFacingX·CLONE_DAMAGE_MULTIPLIER 4필드 + OnPlayerSkillFired 이벤트 신설
Assets/Scripts/Skills/Runtime/ActiveSkillRuntime.cs Fire() 영역 OnPlayerSkillFired 발화 1줄 + CalculateEffectiveDamage() 영역 50% 반감 1줄
Assets/Scripts/Skills/Events/SkillFireEvent.cs Minion case 영역 CardId 분기 + Cleanup 영역 IsCloneFireActive reset
Assets/Scripts/Skills/Runtime/SkillRuntimeFactory.cs AvailableCardIds 영역 "A10" 추가
Assets/Scripts/Skills/Effectors/{ProjectileSpawner,MeleeAreaSpawner,LightningStrikeSpawner,LaserSpawner,PoisonSwampSpawner,SpiritFireSpawner}.cs 선택IsCloneFireActive 분기로 spawn 위치·facing 분신 origin 사용
Assets/Resources/Skills/Active/A10_bunsin.asset 신규

Effector 변경 최소화 옵션: 모든 Effector에 if (inventory.IsCloneFireActive) playerPos = inventory.CloneFireOrigin 일관 적용. 6개 Effector × 약 3줄 = 18줄.

대안 = 매우 최소화 (옵션 ε): 분신 발동 시 PlayerSkillInventory.transform.position을 1 frame 동안 분신 위치로 swap. Effector 변경 0줄. 그러나 부작용 가능성 (다른 컴포넌트 transform 참조). 기각 — 부작용 위험.

채택: 6개 Effector 일관 분기 — 명시성·안정성 우선.

A10-13. 기각안

A10-13-1. 분신 별도 GameObject + 자체 PlayerSkillInventory mirror — 기각

기각 근거 (§A10-2 옵션 (가)):

  • 코드 중복 (인벤토리·Lv·각성·이벤트 구독 2중 동기화)
  • C11 자원 효율 위반 — MonoBehaviour 2중 부담
  • 채택안: (나) — CloneInstance 단일 + Player Inventory hook + 0.5초 지연 큐

A10-13-2. Effector 한 번 호출 시 Player+Clone 2회 발동 (분신 = sprite만) — 기각

기각 근거 (§A10-2 옵션 (다)):

  • PD 명세 5번 "플레이어보다 0.5초 뒤" 직접 위반 — 즉시 2회 발동은 spec 어긋남
  • 채택안: (나) — 0.5초 지연 큐 + 분신 위치 재호출

A10-13-3. PlayerSkillInventory.transform.position swap — 기각

기각 근거 (§A10-12 옵션 ε):

  • 1 frame 부작용 가능성 — Camera·HUD·다른 컴포넌트 transform 참조 영향
  • 채택안: Effector 6개 일관 IsCloneFireActive 분기 (18줄)

A10-13-4. 분신 lifetime 8초 (A11 동등) — 기각

기각 근거 (§A10-1 합리적 기본값):

  • v0.4 CSV "분신 1기" + "긴 주기" — 잠시 spawn 후 사라지면 "긴 주기로 1기 생성" 의미가 무너짐
  • PD 결정 채택안 (2026-05-15): 12초 자동 소멸 + Singleton 1기 + BaseCooldown 25초. 12초 내 재발동 시 기존 destroy + 새 spawn (Singleton). BaseCooldown 25 < lifetime 12 영역 활성 중 재발동 시 25초마다 갱신
  • Lv 업 메커니즘 (PD 결정 2026-05-15): 분신 수 증가 X·추후 지속시간 ↑ + 플레이어 참조 데미지 비율(%) ↑ (balance-designer 후속 Lv별 수치 확정)

A10-13-5. 분신 facing이 Player와 독립 (분신 발동 시 적 방향) — 기각

기각 근거 (§A10-1 PD 명세):

  • "플레이어 공격 패턴을 동일하게 모방" v0.4 CSV — Player와 동일 facing이 자연
  • 분신이 적 방향 자체 추적 시 분신 = 독립 actor 의미 → §A10-2 (가) 옵션 회귀
  • 채택안: Player facing 동조 (분신은 Player flipX와 동조)

A10-14. EditMode 테스트 신설 (3단계 검증 영역)

Assets/Tests/Editor/CloneSkillTests.cs 신설:

  1. CloneSpawnPosition_FacingRight_X_Minus1 — Player facing=오른쪽 시 분신 x = player.x - 1 검증
  2. CloneSpawnPosition_FacingLeft_X_Plus1 — Player facing=왼쪽 시 분신 x = player.x + 1 검증
  3. CloneSpriteAlpha_Is_0_5 — 분신 SpriteRenderer.color.a == 0.5f
  4. CloneDamageMultiplier_50_Percent — Player damage 10 → Clone damage 5 검증
  5. CloneFireDelay_0_5_Seconds — Player Fire 시 분신 발동 시각 = Player Fire 시각 + 0.5초 (unscaledTime 기반)
  6. CloneSingleton_RespawnReplaces — A10 재발동 시 기존 분신 destroy + 새 분신 spawn (단 1기)
  7. CloneNoRecursion_A10_FireSkipped — 분신 자체가 A10 발동 시도 X (무한 재귀 차단)

A10-15. 2단계 클라이언트팀 위임 작업 단위 분해

Sonnet Task 단일 위임 (C48 3자문 통과·영역 전문성·Unity C# 구현 = Sonnet 적정):

# 작업 파일
1 CloneInstance.cs 신규 — MonoBehaviour + 0.5초 지연 큐 + SpawnOrReplace 정적 메서드 + Update dequeue + OnDestroy unsubscribe 신규
2 CloneEffector.cs 신규 — IEffector 구현 + CloneInstance.SpawnOrReplace 호출 신규
3 PlayerSkillInventory.cs 확장 — IsCloneFireActive·CloneFireOrigin·CloneFireFacingX·CLONE_DAMAGE_MULTIPLIER 4필드 + OnPlayerSkillFired 이벤트 신설 수정
4 ActiveSkillRuntime.cs 확장 — Fire() 영역 OnPlayerSkillFired 발화 + CalculateEffectiveDamage() 영역 50% 반감 분기 수정
5 SkillFireEvent.cs 수정 — Minion case 영역 CardId 분기 + Cleanup 영역 IsCloneFireActive reset 수정
6 SkillRuntimeFactory.cs 수정 — AvailableCardIds 영역 "A10" 추가 수정
7 6개 Effector 영역 IsCloneFireActive 분기 일관 추가 (spawn 위치·facing) 수정
8 A10_bunsin.asset 신규 — A11 동등 패턴 + A10 고유 필드 신규
9 EditMode 테스트 7건 신설 신규

예상 작업량: 신규 4 파일 + 수정 9 파일 + .asset 1 + 테스트 1. 단일 Sonnet Task 범위.

A10-16. 3단계 개발팀장 검증 항목 (사전 명시)

  1. PD 명세 5항목 전수 정합 — 위치 (facing 반대 1유닛)·외형 (alpha 0.5)·동작 (동일 스킬)·공격력 (50% 반감)·타이밍 (0.5초 딜레이)
  2. v0.4 CSV A10 행 정합 — 분신 1기·영구 유지·동일 패턴 모방·공격력 비율 감소·무적
  3. 기존 시스템 충돌 없음 — Player Fire 정상 (분신 hook이 Player 발동 영향 X)·다른 카드 정상·BT12-Dev-Vis 13 스킬 정상
  4. C11 정합 — 자원 효율 (CloneInstance 1기·MonoBehaviour 부담 최소)·코드 직관성 (3계층 hook 명시)·범용성 (Effector 6종 무차별 지원)
  5. EditMode 7건 + 기존 테스트 전부 green
  6. SOT 갱신 — 스킬 이펙트 확정 SOT v1 §3 A10 추가 + §4 변경 이력 추가
  7. PD 결정 정합 검증 (2026-05-15) — BaseCooldown 25·MinionLifetime 12·facing 고정·무적 Collider 미부착·Lv 업 메커니즘 (분신 수 X·지속시간+데미지 비율↑) 전수 코드 정합 확증

§14. 참조 문서

  • 기획 SOT:
    • 프로젝트/EerieVillage/기획/content/02_스킬_효과_컨셉.md v0.2
    • 프로젝트/EerieVillage/기획/content/02_스킬_효과_컨셉_v0.3.csv (60종 캐주얼 명명)
    • 프로젝트/EerieVillage/기획/system/01_카드_시스템.md v0.2
    • 프로젝트/EerieVillage/기획/balance/01_전투_수치.md v0.2
    • 프로젝트/EerieVillage/기획/04_전투_기본_스펙.md v0.2
    • 프로젝트/EerieVillage/기획/content/01_카드_풀.md v0.2
  • BT7-Dev 선행: 프로젝트/EerieVillage/개발/06_BT7-Plan_VS순수형_재구조.md v1.0
  • 조직 자산: 프로젝트/코어프레임워크/01·03·04_*.md (BT.Framework Tier 1 재활용 근거)
  • PD 지시 로그: 공유/PD_지시_트래킹/개발팀_PD_지시_로그.md BT12-Dev 항목
  • 대화로그: 공유/대화로그/EerieVillage/2026-04-24.md BT12-Dev 엔트리 (후속 작성)