2026-10-18 15:09:05 +00:00
// ─────────────────────────────────────────────────────────────────────────────
// 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>
2026-10-31 06:53:50 +00:00
/// <summary>#1001 0 이상이면 이번 직접 타격 피해(Damage · NoCriDmg · PiercingDmg)에 곱한다 — 대검 기본 회전 연타(WLStaffEarth spinDamage)가 판정 동안만 켠다(그 밖 = − 1).</summary>
public static float DamageScale = - 1f ;
2026-10-18 15:09:05 +00:00
public static DamageInfo BeforeDamage ( Actor victim , DamageInfo dinfo )
{
2026-10-31 06:53:50 +00:00
if ( DamageScale > = 0f & & dinfo ! = null ) { dinfo . Damage * = DamageScale ; dinfo . NoCriDmg * = DamageScale ; dinfo . PiercingDmg * = DamageScale ; }
2026-10-18 15:09:05 +00:00
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 ) ;
}
}
}