Project_WL/Assets/WL/Combat/Status/WLStatusRunner.cs

291 lines
16 KiB
C#

// ─────────────────────────────────────────────────────────────────────────────
// WLStatusRunner.cs — 액터 1명의 상태이상 실행기 (#842). WLStatus.Ensure 가 PC·몹에 런타임 부착(프리팹 무수정).
// · 매 프레임 원본 CC 타이머(Get_CCData) 상승/하강 순간만 감지 → VFX 시작/정지 · 버프/디버프 텍스트.
// · 수면 플래그 · 중독/출혈 도트 코루틴 · 출혈 1초 반복 VFX.
// ─────────────────────────────────────────────────────────────────────────────
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace WL.Combat.Status
{
[DisallowMultipleComponent]
public sealed class WLStatusRunner : MonoBehaviour
{
Actor _actor;
public bool Sleeping { get; private set; }
float _sleepUntil;
// 감시 CC — 앞 3개는 루프 VFX(스턴/수면 · 중독 · 출혈), 나머지는 1회 VFX + 텍스트
static readonly eCC[] s_watch =
{
eCC.Stun, eCC.Poison, eCC.Blooding,
eCC.ATKSPD_Up, eCC.MOVSPD_Up, eCC.ATKSPD_Down, eCC.MOVSPD_Down,
eCC.ATK_Up, eCC.STR_Mul_Up, eCC.DEX_Mul_Up, eCC.INT_Mul_Up, eCC.LUK_Mul_Up, eCC.Cri_Up
};
readonly bool[] _was = new bool[s_watch.Length];
WLStatusFX.Handle _stunFx, _poisonFx;
float _nextBleedFx;
Coroutine _poisonCo, _bleedCo;
readonly List<string> _rising = new List<string>(4);
// 도트 피해용 가짜 투사체 데이터 — Actor.Cal_Damage 는 Beater_pd 가 Skill_Ignited 면 피격 모션(Play_Hit)을 건너뛴다(Actor.cs:606).
// 매초 몸이 움찔하지 않게 하려는 것뿐 · CC_AddVal2 = 0 이라 사망 분기(:589)의 버닝 전이는 타지 않는다.
static ProjectileData s_dotPd;
public string Info
{
get
{
if (_actor == null) return "no actor";
return _actor.name + " sleep=" + Sleeping + " stun=" + _actor.Get_CCData(eCC.Stun).CCTime.ToString("F2") + " poison=" + _actor.Get_CCData(eCC.Poison).CCTime.ToString("F2")
+ " blood=" + _actor.Get_CCData(eCC.Blooding).CCTime.ToString("F2") + " aspdUp=" + _actor.Get_CCData(eCC.ATKSPD_Up).CCTime.ToString("F2") + " mspdDown=" + _actor.Get_CCData(eCC.MOVSPD_Down).CCTime.ToString("F2")
+ " atkUp=" + _actor.Get_CCData(eCC.ATK_Up).CCTime.ToString("F2") + " isCC=" + _actor.IsCC() + " fx(stun=" + (_stunFx != null && _stunFx.Active) + " poison=" + (_poisonFx != null && _poisonFx.Active) + ")"
+ " dot(p=" + (_poisonCo != null) + " b=" + (_bleedCo != null) + ") hp=" + _actor.Get_HP().ToString("F0") + "/" + _actor.Get_MaxHP().ToString("F0") + " anim=" + _actor.Get_CurAnim()
+ " height=" + (_actor.Get_Top_postion().y - _actor.Get_position().y).ToString("F2") + " sizeK=" + WLStatusFX.SizeFactor(_actor, WLStatus.S).ToString("F2");
}
}
public static WLStatusRunner Ensure(Actor a)
{
if (a == null) return null;
var r = a.GetComponent<WLStatusRunner>();
if (r == null) { r = a.gameObject.AddComponent<WLStatusRunner>(); r._actor = a; }
return r;
}
void Awake() { if (_actor == null) _actor = GetComponent<Actor>(); }
// 같은 상태가 또 들어오면 남은 시간과 새 시간 중 긴 쪽으로(PD 「시간을 새로 발생된 시간에 맞게 늘린다」 · 짧은 재적용이 긴 스턴을 끊지 않게)
float _stunEndAt, _poisonEndAt, _bleedEndAt;
public void Apply(WLStatusType type, float duration, float value, Actor source)
{
var s = WLStatus.S; if (s == null || _actor == null || _actor.IsDead()) return;
float now = Time.time;
switch (type)
{
case WLStatusType.Stun:
Sleeping = false;
if (_actor.Get_CCData(eCC.Stun).CCTime > 0d && _stunEndAt - now > duration) { RefreshStunLoop(); break; } // 더 긴 행동 불능이 남아 있다 — 유지
_stunEndAt = now + duration;
_actor.Stun(duration, false);
RefreshStunLoop();
break;
case WLStatusType.Sleep:
Sleeping = true;
if (_actor.Get_CCData(eCC.Stun).CCTime > 0d && _stunEndAt - now > duration) { _sleepUntil = Mathf.Max(_sleepUntil, _stunEndAt); RefreshStunLoop(); break; }
_sleepUntil = _stunEndAt = now + duration;
_actor.Stun(duration, false);
RefreshStunLoop();
break;
case WLStatusType.Poison:
_poisonEndAt = Mathf.Max(_poisonEndAt, now + duration);
_actor.Buff_or_Debuff(eCC.Poison, _poisonEndAt - now, value);
StartDot(ref _poisonCo, _poisonEndAt - now, value, source, false);
break;
case WLStatusType.Bleed:
_bleedEndAt = Mathf.Max(_bleedEndAt, now + duration);
_actor.Buff_or_Debuff(eCC.Blooding, _bleedEndAt - now, value);
StartDot(ref _bleedCo, _bleedEndAt - now, value, source, true);
break;
case WLStatusType.SpeedUp:
_actor.Buff_or_Debuff(eCC.ATKSPD_Up, duration, value);
_actor.Buff_or_Debuff(eCC.MOVSPD_Up, duration, value);
break;
case WLStatusType.SpeedDown:
_actor.Buff_or_Debuff(eCC.ATKSPD_Down, duration, value);
_actor.Buff_or_Debuff(eCC.MOVSPD_Down, duration, value);
break;
case WLStatusType.StatUp:
_actor.Buff_or_Debuff(eCC.ATK_Up, duration, value);
break;
}
WLStatus.LastLog = "apply " + type + " " + duration + "s v=" + value + " → " + _actor.name;
}
/// <summary>넉백(원본 KnockBack CC)이 끝난 뒤에 상태를 건다 — 밀리는 동안 스턴이 겹치지 않게(#843). maxWait 초 안에 안 끝나면 그때 건다.</summary>
public void ApplyAfterKnockback(WLStatusType type, float duration, float value, Actor source, float maxWait)
{
if (_afterKb != null) StopCoroutine(_afterKb);
_afterKb = StartCoroutine(CoAfterKnockback(type, duration, value, source, maxWait));
}
Coroutine _afterKb;
IEnumerator CoAfterKnockback(WLStatusType type, float duration, float value, Actor source, float maxWait)
{
float until = Time.time + Mathf.Max(0.05f, maxWait);
yield return null; // KnockBack 코루틴이 CCTime 을 세우는 프레임을 넘긴다
while (Time.time < until && _actor != null && !_actor.IsDead() && _actor.Get_CCData(eCC.KnockBack).CCTime > 0d) yield return null;
_afterKb = null;
if (_actor != null && !_actor.IsDead()) { Apply(type, duration, value, source); WLStatus.Applies++; }
}
/// <summary>수면 중 직접 타격 — 100 % 치명 + 치명 피해 배율, 그리고 즉시 기상.</summary>
public void ApplySleepHit(DamageInfo dinfo)
{
var s = WLStatus.S; var sh = dinfo.Beater;
if (sh != null && sh.Get_StatInfo() != null)
{
var st = sh.Get_StatInfo();
dinfo.Damage = dinfo.NoCriDmg;
dinfo.Cal_Cri(sh.m_AttackType, st, true); // forceCri = 원본 파라미터 — 치명 피해 1배분 가산 + 텍스트 색
double mul = sh.m_AttackType == eAttackType.Magic ? st.Get_Stat(eStat.M_CRI_DMG_Multiplier) : st.Get_Stat(eStat.CRI_DMG_Multiplier);
dinfo.Damage += dinfo.NoCriDmg * mul * Mathf.Max(0f, s.sleepCritDamageMultiplier - 1f); // 나머지 배율분
WLStatus.SleepCritHits++;
WLStatus.LastSleepHit = "sleep-hit " + _actor.name + " cri=" + dinfo.Critical + " nocri=" + dinfo.NoCriDmg.ToString("F0") + " dmg=" + dinfo.Damage.ToString("F0") + " mul=" + mul.ToString("F2");
}
Wake("hit");
}
public void Wake(string why)
{
if (!Sleeping) return;
Sleeping = false;
if (_actor != null && !_actor.IsDead())
{
_actor.Get_CCData(eCC.Stun).CCTime = 0d; // CCTime 0 → IsCC 해제(TimeUpdate 는 건드리지 않는다). 원본 Co_Stun 은 남은 시간 뒤 0 을 다시 쓰고 에이전트를 켠다(무해).
_actor.AnimationPlay(MyValue.Get_AnimName(eAnim.Idle));
}
WLStatusFX.Stop(ref _stunFx);
WLStatus.SleepWakes++;
WLStatus.LastLog = "wake(" + why + ") " + (_actor != null ? _actor.name : "?");
}
void RefreshStunLoop()
{
if (_actor == null || _actor.Get_CCData(eCC.Stun).CCTime <= 0d) return; // 아직 안 켜졌으면 다음 Update 의 상승 에지가 켠다
StartStunLoop();
_was[0] = true;
}
/// <summary>스턴/수면 루프 — 종류가 바뀌면(수면↔스턴) 이전 것을 끄고, 같으면 WLStatusFX 가 중복 없이 이어 간다.</summary>
void StartStunLoop()
{
var want = Sleeping ? WLStatusType.Sleep : WLStatusType.Stun;
if (_stunFx != null && _stunFx.Active && _stunFx.type != want) WLStatusFX.Stop(ref _stunFx);
_stunFx = WLStatusFX.Play(_actor, want, true);
}
// ── 도트 ─────────────────────────────────────────────────────────────
void StartDot(ref Coroutine co, float duration, float value, Actor source, bool percentOfMaxHp)
{
if (co != null) StopCoroutine(co);
co = StartCoroutine(CoDot(duration, value, source, percentOfMaxHp));
}
IEnumerator CoDot(float duration, float value, Actor source, bool pct)
{
var s = WLStatus.S;
float gap = Mathf.Max(0.1f, s.dotIntervalSeconds);
float end = Time.time + duration;
var wait = new WaitForSeconds(gap);
while (Time.time < end)
{
yield return wait;
if (_actor == null || _actor.IsDead()) break;
double dmg = pct ? _actor.Get_MaxHP() * value : value;
if (dmg < 1d) dmg = 1d;
Actor beater = source != null && !source.IsDead() ? source : (MyValue.MyPC != null ? (Actor)MyValue.MyPC : _actor);
if (s_dotPd == null) s_dotPd = new ProjectileData { m_SkillListTableData = new SkillListTableData { e_Skill = eEffect.Skill_Ignited } };
var dinfo = new DamageInfo
{
Beater = beater, Beater_pd = s_dotPd, BaseDmg = dmg, NoCriDmg = dmg, Damage = dmg,
TextColor = pct ? s.bleedTextColor : s.poisonTextColor, Critical = eStat.None
};
_actor.Cal_Damage(dinfo);
WLStatus.DotTicks++;
if (pct) { WLStatusFX.PlayOneShot(_actor, WLStatusType.Bleed); _nextBleedFx = Time.time + gap; }
}
if (pct) _bleedCo = null; else _poisonCo = null;
}
// ── 매 프레임: CC 에지 감지 ───────────────────────────────────────────
void Update()
{
if (_actor == null) return;
var s = WLStatus.S; if (s == null || !s.enabled) return;
bool dead = _actor.IsDead();
_rising.Clear();
bool speedUp = false, speedDown = false, statUp = false;
for (int i = 0; i < s_watch.Length; i++)
{
var cc = s_watch[i];
bool now = !dead && _actor.Get_CCData(cc).CCTime > 0d;
bool was = _was[i]; _was[i] = now;
if (now == was) continue;
if (now) OnRise(cc, ref speedUp, ref speedDown, ref statUp); else OnFall(cc);
}
if (speedUp) WLStatusFX.PlayOneShot(_actor, WLStatusType.SpeedUp);
if (speedDown) WLStatusFX.PlayOneShot(_actor, WLStatusType.SpeedDown);
if (statUp) WLStatusFX.PlayOneShot(_actor, WLStatusType.StatUp);
if (_rising.Count > 0) ShowText(string.Join(" · ", _rising), speedDown && !speedUp && !statUp ? s.debuffTextColor : s.buffTextColor);
// 출혈 = 매초 반복 재생(원본 스킬 Blooding 도 같은 길 · WL 도트는 틱에서 직접 재생)
if (!dead && _bleedCo == null && _actor.Get_CCData(eCC.Blooding).CCTime > 0d && Time.time >= _nextBleedFx)
{ _nextBleedFx = Time.time + Mathf.Max(0.1f, s.dotIntervalSeconds); WLStatusFX.PlayOneShot(_actor, WLStatusType.Bleed); }
if (Sleeping && (dead || Time.time >= _sleepUntil + 0.05f)) Sleeping = false; // 자연 종료(깨움 규칙 없음)
// PD 「피격을 받아도 스턴 유지」 — 원본은 공격 쪽 부가효과(KnockBack_byHitter 끝의 Stun(val2) · 스턴 확률 적중 등)가 Stun() 을 다시 불러
// 남은 시간을 짧은 값으로 덮어쓴다(2026-10-11 실측: 해머 타격 뒤 3 s 스턴이 0.3 s 안에 0). WL 이 건 스턴/수면은 남은 시간이 있으면 되살린다.
if (!dead && _stunEndAt > Time.time + 0.05f && _actor.Get_CCData(eCC.Stun).CCTime <= 0d)
{
_actor.Stun(_stunEndAt - Time.time, false);
Reasserts++;
if (_stunFx == null || !_stunFx.Active) StartStunLoop();
}
}
public static int Reasserts;
void OnRise(eCC cc, ref bool speedUp, ref bool speedDown, ref bool statUp)
{
var s = WLStatus.S;
switch (cc)
{
case eCC.Stun:
StartStunLoop();
break;
case eCC.Poison:
_poisonFx = WLStatusFX.Play(_actor, WLStatusType.Poison, true); // 이미 떠 있으면 WLStatusFX 가 같은 인스턴스를 이어 간다
break;
case eCC.Blooding:
_nextBleedFx = 0f;
break;
case eCC.ATKSPD_Up: case eCC.MOVSPD_Up: speedUp = true; AddText(s, cc); break;
case eCC.ATKSPD_Down: case eCC.MOVSPD_Down: speedDown = true; AddText(s, cc); break;
default: statUp = true; AddText(s, cc); break;
}
}
void OnFall(eCC cc)
{
if (cc == eCC.Stun) { WLStatusFX.Stop(ref _stunFx); Sleeping = false; }
else if (cc == eCC.Poison) WLStatusFX.Stop(ref _poisonFx);
}
void AddText(WLStatusSettings s, eCC cc)
{
string t;
if (s.ccTexts != null && s.ccTexts.TryGetValue(cc.ToString(), out t) && !string.IsNullOrEmpty(t) && !_rising.Contains(t)) _rising.Add(t);
}
void ShowText(string text, string colorTag)
{
var s = WLStatus.S;
WLStatusText.Show(_actor, text, colorTag, s.textSeconds, s.textWorldSize, s.textYOffset, s.textRiseSpeed, s.textFadeStart); // 같은 문구가 떠 있으면 안에서 연장
WLStatus.Texts++;
WLStatus.LastLog = "text \"" + text + "\" → " + _actor.name;
}
void OnDisable()
{
WLStatusFX.Stop(ref _stunFx); WLStatusFX.Stop(ref _poisonFx);
if (_poisonCo != null) { StopCoroutine(_poisonCo); _poisonCo = null; }
if (_bleedCo != null) { StopCoroutine(_bleedCo); _bleedCo = null; }
Sleeping = false;
for (int i = 0; i < _was.Length; i++) _was[i] = false;
}
}
}