EerieVillage/Assets/Scripts/Skills/Runtime/ActiveSkillRuntime.cs

141 lines
4.8 KiB
C#
Raw Normal View History

docs(BT12-Dev Phase 2-A): Skills 13 파일 신규 (인터페이스·SO·중앙 컴포넌트) C49 Phase 2 (집행) — Sonnet 위임 결과·Phase 1 dev-team-lead 재분석 보고서 정합. 신설 13 파일 (Assets/Scripts/Skills/): - Interfaces/ (4): ISkillRuntime, IActiveSkill, IPassiveSkill, IAwakeningSkill (+ ActiveTrigger·PassiveTriggerKind·AwakeningPattern enum) - Data/ (4): SkillDataAsset (abstract·AttributeTag·TypeTag), ActiveSkillData (Category 6종·14 신규 필드), PassiveSkillData (StatType·stub), AwakeningSkillData (stub) - Runtime/ (4): PlayerStats (POCO·AttributeTag Dictionary), ActiveSkillRuntime (Tick·Fire·EffectiveCooldown 하드캡 0.5s·StackLevelFactor), PlayerSkillInventory ([RequireComponent(Health)]·OnDamagedEvent 구독·NotifyEnemyKilled), SkillRuntimeFactory (Resolve·Create·stub 2종) - Events/ (1): SkillFireEvent (Simulation.Event<T>·Execute stub·카테고리 분기 6종 주석) 설계서 정합: - §2-1 인터페이스 계약 (ISkillRuntime → IActiveSkill·IPassiveSkill·IAwakeningSkill) - §2-2 ScriptableObject 계약 (ActiveCategory 6종·CreateAssetMenu 3종) - §2-3 PlayerStats POCO·AttributeTag 키 Dictionary - §3-2 CSV 매핑 테이블·§3-3 Resolve+Create 분기 - §4-2 EffectiveCooldown = BaseCooldown × CooldownMultiplier ÷ StackLevelFactor·하드캡 0.5s - §4-4 OnHit·OnKill 이벤트 핸들러 PlayerSkillInventory 구현 설계서 대비 조정 3건 (Sonnet 자체 정합): 1. IPassiveSkill.ApplyTo → ApplyModifier·RemoveModifier (설계서 §2-1 명세 정합) 2. AddSkillByCardId 반환 void → bool (실패 감지) 3. EnemyKillContext struct 신설 (Phase 2-D 정식 통합 전 decoupling) Phase 2-B 준비: - SkillFireEvent.Execute stub 영역 카테고리 분기 6종 주석 - Phase 2-B 투사체 진입 시 ProjectileSpawner·AttackHitbox 연결 지점 명확 기존 파일 영역 변경 X (BT12-MVP-A·BT5-Dev·BT7-Dev 미변경) 회귀 위험 = 매우 낮음 (신규 파일만) C50 분량 (PD 사전 승인 80~120K) — 실제 ~73K (정합) PD 결정 (b 5분할·b-1 카테고리 6분할·우선 투사체) 사전 승인 정합 pm-auditor 사전 감사 = Pass 4 + Minor 1 + Major 1 - Major 1 정정 영역 = git add 명시 path 한정 (Skills 디렉토리만·Screenshots·_Recovery 미포함) ✅ - Minor 1 후속 영역 = PD Editor Refresh 후 read_console 본 PM 직접 실측 untracked 영역 별도 안건: - Assets/Screenshots/ (manage_camera screenshot 영역·.gitignore 검토 영역) - Assets/_Recovery/ (Unity 자동 복구 파일·.gitignore 검토 영역)
2026-05-09 09:31:38 +00:00
using UnityEngine;
using Platformer.Core;
namespace EerieVillage.Skills
{
/// <summary>
/// 액티브 스킬 런타임 구현체. ActiveSkillData.Category 기준 단일 클래스.
/// BT12-Dev v1 §4-2 정합.
/// - OnTime: Tick()에서 쿨다운 감산 → Fire()
/// - OnHit·OnKill: PlayerSkillInventory 이벤트 핸들러에서 Fire() 직접 호출
/// - F(SpecialJudge): Fire() 내 확률 판정 선행
/// Phase 2-B에서 SkillFireEvent.Execute에 카테고리별 실 발동기 연결 예정.
/// </summary>
public class ActiveSkillRuntime : IActiveSkill
{
// --- ISkillRuntime ---
public SkillDataAsset Data => ActiveData;
public int StackLevel { get; private set; } = 1;
// --- IActiveSkill ---
public float BaseCooldown => ActiveData.BaseCooldown;
/// <summary>
/// 실제 쿨다운 = BaseCooldown * CooldownMultiplier(P06) / StackLevelFactor.
/// 하드캡 0.5s (balance/01 v0.2 §9).
/// </summary>
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 연결 예정
}
/// <summary>
/// PlayerSkillInventory.Update가 매 프레임 호출.
/// OnTime 트리거만 처리 (OnHit·OnKill은 인벤토리 이벤트 핸들러에서 Fire() 직접 호출).
/// </summary>
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);
}
}
/// <summary>
/// 스킬 효과 Dispatch. SkillFireEvent를 Simulation에 Schedule.
/// F(SpecialJudge) 카테고리는 확률 판정 선행.
/// Phase 2-B에서 SkillFireEvent.Execute에 실 발동기 연결.
/// </summary>
public void Fire()
{
// F 카테고리 확률 판정
if (ActiveData.Category == ActiveCategory.SpecialJudge)
{
if (Random.value > ActiveData.FireProbability) return;
}
var ev = Simulation.Schedule<SkillFireEvent>();
ev.Runtime = this;
ev.Inventory = _inventory;
}
/// <summary>
/// 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
/// </summary>
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
};
}
/// <summary>
/// 유효 대미지 산출 (balance/01 v0.2 §3 대미지 공식).
/// effectiveDamage = BaseDamage * DamageMultiplier * StackLevelFactor * AttributeMultiplier
/// </summary>
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
);
}
}
}