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

162 lines
10 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ─────────────────────────────────────────────────────────────────────────────
// WLStatus.cs — WL 상태이상 7종 (#842 · 2026-10-11 · PD 「Master Stylized FX 로 스턴·속도 증가/저하·수면·중독·출혈·능력치 상승 구현」)
//
// ■ 원칙 — 원본(힘민지) CC 체계를 그대로 쓴다. 훅은 ProjectileBase.ProcessCollider 1줄(BeforeDamage · 수면 치명타 규칙)뿐.
// · 행동 불능(스턴·수면) = 원본 Actor.Stun → dic_ccData[Stun] → IsCC() 가 이동·피격 모션·넉백·경직을 막는다(피격받아도 유지 = 원본 규칙).
// · 수면 = 스턴 CC 슬롯 + 이 쪽 플래그(Sleeping). 직접 타격(ProjectileBase)만 깨우고 도트는 깨우지 않는다(PD 결정 2026-10-11).
// 깨우는 타격 = 100 % 치명 + 치명 피해 ×sleepCritDamageMultiplier(기본 2).
// · 중독(고정값/초)·출혈(최대 HP %/초) = 이 쪽 코루틴이 Actor.Cal_Damage 로 넣는다(원본 DotDamage 는 공격자 피해 비율이라 사양과 다름).
// CC 슬롯(Poison·Blooding)도 함께 켜 두어 원본 스킬이 거는 독·출혈과 같은 길로 VFX 가 뜬다.
// · 속도 증가/저하·능력치 상승 = 원본 CC 타이머(ATKSPD_Up … Cri_Up)의 상승 순간을 러너가 감지 → 1회 VFX + 텍스트 1.5 s 페이드. 원본 버프 코드 무수정.
// ■ 트리거 — triggers[] (classIds × 콤보 단계) : HitConfirmed 에서 넉백(ignorePreknockback) 뒤 상태 적용. 첫 항목 = 해머 1타 → 넉백 + 스턴 0.5 s.
// ■ 값·프리팹 이름·문구 = Resources/WL/WLStatus.json (C45 · 코드 상수 0). enabled=false 면 전부 무동작(C8).
// 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것.
// ─────────────────────────────────────────────────────────────────────────────
using System.Collections.Generic;
using UnityEngine;
using WL.Combat.Core;
namespace WL.Combat.Status
{
public enum WLStatusType { Stun, Sleep, Poison, Bleed, SpeedUp, SpeedDown, StatUp }
public sealed class WLStatusVfx { public string prefab; public string loc = "Top"; public bool loop; public float scale = 1f; public float yOffset; }
public sealed class WLStatusTrigger { public int[] classIds; public int stage = 1; public string status = "Stun"; public float duration = 0.5f; public float value; public float knockbackSeconds; public float knockbackSpeed = 4f; public bool stunAfterKnockback = true; public float knockbackMaxWait = 0.6f; }
public sealed class WLStatusSettings
{
public bool enabled = true;
public float vfxScale = 1f;
/// <summary>크기 기준 키(m) — 액터 키(Top−발)÷이 값을 VFX 크기에 곱한다(PD 「몬스터 크기에 맞게」). 0 이면 비례 없음.</summary>
public float referenceHeight = 1f;
public float sizeFactorMin = 0.5f, sizeFactorMax = 3f;
public float oneShotSeconds = 1f;
public float textSeconds = 1.5f;
public float textYOffset = 0.15f;
/// <summary>월드 라벨 글자 크기(TextMeshPro fontSize · 섬 몹 라벨 Lv 는 2.4) · 상승 속도(m/s) · 투명해지기 시작하는 비율.</summary>
public float textWorldSize = 2.2f, textRiseSpeed = 0.35f, textFadeStart = 0.5f;
public string buffTextColor = "", debuffTextColor = "", poisonTextColor = "", bleedTextColor = "";
public float dotIntervalSeconds = 1f;
public float sleepCritDamageMultiplier = 2f;
public Dictionary<string, WLStatusVfx> vfx = new Dictionary<string, WLStatusVfx>();
public Dictionary<string, string> ccTexts = new Dictionary<string, string>();
public List<WLStatusTrigger> triggers = new List<WLStatusTrigger>();
}
public static class WLStatus
{
public const string JsonPath = "WL/WLStatus";
static WLStatusSettings s_settings; static bool s_loaded;
public static WLStatusSettings S { get { if (!s_loaded) Load(); return s_settings; } }
public static bool Enabled { get { var s = S; return s != null && s.enabled; } }
public static void Reload() { s_loaded = false; }
static void Load()
{
s_loaded = true;
var ta = Resources.Load<TextAsset>(JsonPath);
try { s_settings = ta != null ? Newtonsoft.Json.JsonConvert.DeserializeObject<WLStatusSettings>(ta.text) : null; }
catch (System.Exception ex) { Debug.LogException(ex); s_settings = null; }
}
// ── 진단(프로브가 읽는다)
public static int Applies, Texts, DotTicks, SleepWakes, SleepCritHits, TriggerFires, Skipped, LastStage;
public static string LastLog = "", LastSleepHit = "";
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterAssembliesLoaded)]
static void Register()
{
CombatEvents.HitConfirmed.Add(OnHitConfirmed);
CombatEvents.Spawned.Add(OnSpawned);
}
static void OnSpawned(in SpawnedEvent e) { if (Enabled && e.actor != null) WLStatusRunner.Ensure(e.actor); }
static void OnHitConfirmed(in HitConfirmedEvent e)
{
if (!Enabled || e.victim == null || e.attacker == null) return;
WLStatusRunner.Ensure(e.victim); // 스폰 이벤트 전에 있던 몹도 피격 때 러너를 받는다
if (e.isKill) return;
var s = S; if (s.triggers == null || s.triggers.Count == 0) return;
int cls = e.attacker.Get_ID();
// 콤보 단계 = ComboTracker(Animator 상태 클립 이름 Attack{N}) · WL 콤보는 Animator 전이로 이어져 CurAnim 이 1타에 머문다(2026-10-11 실측 · 모든 타에 발동하던 원인). 트래커가 없으면 CurAnim 으로.
var ct = e.attacker.GetComponent<ComboTracker>();
int stage = ct != null && ct.LastStage > 0 ? ct.LastStage : (int)e.attacker.Get_CurAnim() - (int)eAnim.Attack + 1;
LastStage = stage;
for (int i = 0; i < s.triggers.Count; i++)
{
var t = s.triggers[i];
if (t == null || t.classIds == null || t.stage != stage) continue;
bool hit = false; for (int k = 0; k < t.classIds.Length; k++) if (t.classIds[k] == cls) { hit = true; break; }
if (!hit) continue;
Fire(t, e.attacker, e.victim);
}
}
static void Fire(WLStatusTrigger t, Actor attacker, Actor victim)
{
if (victim.IsDead()) return;
if (t.knockbackSeconds > 0f)
{
victim.KnockBack(t.knockbackSeconds, t.knockbackSpeed, null, true); // 원본 넉백(이미 밀리는 중이어도 다시) — 스턴 전에 걸어야 IsCC 에 안 막힌다
var kb = victim.Get_CCData(eCC.KnockBack);
if (kb.CCTime > 0d && kb.CCTime < t.knockbackSeconds) kb.CCTime = t.knockbackSeconds; // 원본의 연속 넉백 감쇠(CCReduceTime · 최소 0.1 s)를 이 트리거엔 적용하지 않는다(해머 1타는 매번 같은 거리). WL 절반 규칙(MobKnockbackScale)은 그대로 받는다.
}
WLStatusType type;
if (!System.Enum.TryParse(t.status, true, out type)) return;
if (t.knockbackSeconds > 0f && t.stunAfterKnockback && (type == WLStatusType.Stun || type == WLStatusType.Sleep))
{ // #843 PD 「스턴 중 이동 금지」 — 밀려나는 동안엔 스턴을 걸지 않고, 넉백이 끝난 자리에서 스턴 시작
var r = WLStatusRunner.Ensure(victim);
if (r != null) r.ApplyAfterKnockback(type, t.duration, t.value, attacker, t.knockbackMaxWait);
}
else Apply(victim, type, t.duration, t.value, attacker);
TriggerFires++;
LastLog = "trigger " + t.status + " " + t.duration + "s kb=" + t.knockbackSeconds + " → " + victim.name + " by " + attacker.name + " cls " + attacker.Get_ID();
}
/// <summary>상태 적용 API. duration 초 · value = 독 고정 피해/초 · 출혈 최대 HP 비율/초 · 속도·능력치 배율. 성공하면 true.</summary>
public static bool Apply(Actor target, WLStatusType type, float duration, float value = 0f, Actor source = null)
{
if (!Enabled || target == null || target.IsDead()) { Skipped++; return false; }
var r = WLStatusRunner.Ensure(target);
if (r == null) { Skipped++; return false; }
r.Apply(type, duration, value, source);
Applies++;
return true;
}
/// <summary>ProjectileBase.ProcessCollider 훅 — 직접 타격이 수면 중인 대상에 닿기 직전. 100 % 치명 + 치명 피해 배율 뒤 즉시 깨운다.</summary>
public static DamageInfo BeforeDamage(Actor victim, DamageInfo dinfo)
{
if (!Enabled || victim == null || dinfo == null) return dinfo;
var r = victim.GetComponent<WLStatusRunner>();
if (r != null && r.Sleeping) r.ApplySleepHit(dinfo);
return dinfo;
}
public static bool IsSleeping(Actor a) { var r = a != null ? a.GetComponent<WLStatusRunner>() : null; return r != null && r.Sleeping; }
}
/// <summary>메인 PC 에 러너를 보장한다(씬 전환·캐릭터 교체 대응 · WLBowChargeBoot 와 같은 0.5 s 폴링).</summary>
public sealed class WLStatusBoot : MonoBehaviour
{
float _next;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
static void Boot()
{
if (FindFirstObjectByType<WLStatusBoot>() != null) return;
var go = new GameObject("WL_StatusBoot");
DontDestroyOnLoad(go);
go.AddComponent<WLStatusBoot>();
}
void Update()
{
if (Time.unscaledTime < _next) return;
_next = Time.unscaledTime + 0.5f;
if (!WLStatus.Enabled) return;
var pc = MyValue.MyPC;
if (pc != null) WLStatusRunner.Ensure(pc);
}
}
}