162 lines
10 KiB
C#
162 lines
10 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
||
// 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);
|
||
}
|
||
}
|
||
}
|