using UnityEngine; using Platformer.Core; namespace EerieVillage.Skills { /// /// 액티브 스킬 런타임 구현체. ActiveSkillData.Category 기준 단일 클래스. /// BT12-Dev v1 §4-2 정합. /// - OnTime: Tick()에서 쿨다운 감산 → Fire() /// - OnHit·OnKill: PlayerSkillInventory 이벤트 핸들러에서 Fire() 직접 호출 /// - F(SpecialJudge): Fire() 내 확률 판정 선행 /// Phase 2-B에서 SkillFireEvent.Execute에 카테고리별 실 발동기 연결 예정. /// public class ActiveSkillRuntime : IActiveSkill { // --- ISkillRuntime --- public SkillDataAsset Data => ActiveData; public int StackLevel { get; private set; } = 1; // --- IActiveSkill --- public float BaseCooldown => ActiveData.BaseCooldown; /// /// 실제 쿨다운 = BaseCooldown * CooldownMultiplier(P06) / StackLevelFactor. /// 하드캡 0.5s (balance/01 v0.2 §9). /// public float EffectiveCooldown => Mathf.Max( BaseCooldown * _inventory.Stats.CooldownMultiplier / StackLevelFactor(StackLevel), 0.5f ); public float CooldownRemaining { get; private set; } = 0f; public ActiveTrigger Trigger => ActiveData.Trigger; // --- 내부 --- public ActiveSkillData ActiveData { get; private set; } private PlayerSkillInventory _inventory; public ActiveSkillRuntime(ActiveSkillData data) { ActiveData = data; } public void OnEquip(PlayerSkillInventory inventory) { _inventory = inventory; CooldownRemaining = ActiveData.BaseCooldown; // 첫 발동은 주기 후 } public void OnUnequip() { // OnHit·OnKill 이벤트 구독 해제 — Phase 2-D 인벤토리 통합 시 구현 } public void Upgrade() { if (StackLevel < ActiveData.maxLevel && StackLevel < 5) StackLevel++; // Lv.5 도달 시 각성 조건 1 충족 — Phase 2-D AwakeningManager 연결 예정 } /// /// PlayerSkillInventory.Update가 매 프레임 호출. /// OnTime 트리거만 처리 (OnHit·OnKill은 인벤토리 이벤트 핸들러에서 Fire() 직접 호출). /// public void Tick(float deltaTime) { if (Trigger != ActiveTrigger.OnTime) return; CooldownRemaining -= deltaTime; if (CooldownRemaining <= 0f) { Fire(); // 누적 오버플로 방지 (BT7-Dev PlayerAttackTicker와 동일 패턴) CooldownRemaining += EffectiveCooldown; CooldownRemaining = Mathf.Max(CooldownRemaining, 0f); } } /// /// 스킬 효과 Dispatch. SkillFireEvent를 Simulation에 Schedule. /// F(SpecialJudge) 카테고리는 확률 판정 선행. /// Phase 2-B에서 SkillFireEvent.Execute에 실 발동기 연결. /// public void Fire() { // F 카테고리 확률 판정 if (ActiveData.Category == ActiveCategory.SpecialJudge) { if (Random.value > ActiveData.FireProbability) return; } var ev = Simulation.Schedule(); ev.Runtime = this; ev.Inventory = _inventory; } /// /// StackLevel에 따른 대미지 팩터 (balance/01 v0.2 §3). /// Lv.1=1.0 · Lv.2=1.2 · Lv.3=1.4 · Lv.4=1.6 · Lv.5=2.0 /// public static float StackLevelFactor(int lv) { return lv switch { 1 => 1.0f, 2 => 1.2f, 3 => 1.4f, 4 => 1.6f, 5 => 2.0f, _ => 1.0f }; } /// /// 유효 대미지 산출 (balance/01 v0.2 §3 대미지 공식). /// effectiveDamage = BaseDamage * DamageMultiplier * StackLevelFactor * AttributeMultiplier /// public int CalculateEffectiveDamage() { if (_inventory == null) return ActiveData.BaseDamage; var stats = _inventory.Stats; float attrMult = 1.0f; if (ActiveData.AttributeTags != AttributeTag.None && stats.AttributeMultiplier.TryGetValue(ActiveData.AttributeTags, out float v)) { attrMult = v; } return Mathf.RoundToInt( ActiveData.BaseDamage * stats.DamageMultiplier * StackLevelFactor(StackLevel) * attrMult ); } } }