291 lines
16 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|