[WL-816zb] 피격 경직 + 흰색 하이라이트 단축 (#816)
경직 = MobStagger(신규 · HitConfirmed/Killed/Spawned 구독 · 원본 0줄) · NavMeshAgent.speed 0 + velocity 0 + Animator.speed 0
일반 0.18 / 엘리트 0.12 / 보스 0.08 s · 넉백 중에는 종료를 미뤄 넉백 직후부터 다시 셈(상한 1 s) · 스턴·빙결이면 즉시 해제
값 = WLHitFeelSettings(mobStaggerEnabled·초 3개) + 에셋
흰색 = MobHitFlash 점멸 상한 blinkMaxSeconds 0.06 s (연장·티어 시간 배수 2.4 포함해 첫 점멸 시작 기준으로 절단)
세기 blinkColor 2.2→3.0 · blinkEmission 0.6→0.9 (짧게, 대신 세게) · 값 = WLHitBlinkSettings + 에셋
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2799b311b6
commit
e692c53ea0
|
|
@ -37,6 +37,8 @@ namespace WL.Combat.Reaction
|
||||||
public Resolved[] resolved;
|
public Resolved[] resolved;
|
||||||
public bool blinkOn, tint;
|
public bool blinkOn, tint;
|
||||||
public float blinkOffAt, blinkReadyAt, tintHdr, tintRange;
|
public float blinkOffAt, blinkReadyAt, tintHdr, tintRange;
|
||||||
|
// WL #816zb — 이번 점멸이 처음 켜진 시각. 연장(ExtendCount)이 blinkMaxSeconds 를 넘지 못하게 하는 기준점.
|
||||||
|
public float blinkStartAt;
|
||||||
public int activeIndex = -1;
|
public int activeIndex = -1;
|
||||||
// WL #811gh — 임팩트 티어별 점멸 강도. 1 = 기존 동작(티어 시스템 off 이면 항상 1).
|
// WL #811gh — 임팩트 티어별 점멸 강도. 1 = 기존 동작(티어 시스템 off 이면 항상 1).
|
||||||
public float blinkColorMul = 1f, blinkTimeMul = 1f;
|
public float blinkColorMul = 1f, blinkTimeMul = 1f;
|
||||||
|
|
@ -103,13 +105,15 @@ namespace WL.Combat.Reaction
|
||||||
float colorMul, timeMul;
|
float colorMul, timeMul;
|
||||||
ImpactTier.GetBlinkModifiers(victim, out colorMul, out timeMul);
|
ImpactTier.GetBlinkModifiers(victim, out colorMul, out timeMul);
|
||||||
float now = Time.unscaledTime;
|
float now = Time.unscaledTime;
|
||||||
if (s.blinkOn) { s.blinkOffAt = now + st.blinkOnSeconds * timeMul; ExtendCount++; return; } // 같은 프레임 다중 히트 → 연장
|
// WL #816zb — 연장도 '처음 켜진 시각 + blinkMaxSeconds' 를 넘지 못한다(PD: 몹이 계속 하얗게 보이는 현상).
|
||||||
|
if (s.blinkOn) { s.blinkOffAt = CapOffAt(st, s.blinkStartAt, now + st.blinkOnSeconds * timeMul); ExtendCount++; return; }
|
||||||
if (now < s.blinkReadyAt) { ThrottledCount++; return; }
|
if (now < s.blinkReadyAt) { ThrottledCount++; return; }
|
||||||
if (s.tint && !victim.IsDead()) s.tint = false; // 부활·재사용 뒤 남은 엣지 세팅 정리
|
if (s.tint && !victim.IsDead()) s.tint = false; // 부활·재사용 뒤 남은 엣지 세팅 정리
|
||||||
|
|
||||||
WLReactionRunner.Ensure();
|
WLReactionRunner.Ensure();
|
||||||
s.blinkColorMul = colorMul; s.blinkTimeMul = timeMul;
|
s.blinkColorMul = colorMul; s.blinkTimeMul = timeMul;
|
||||||
s.blinkOn = true; s.blinkOffAt = now + st.blinkOnSeconds * timeMul;
|
s.blinkOn = true; s.blinkStartAt = now;
|
||||||
|
s.blinkOffAt = CapOffAt(st, now, now + st.blinkOnSeconds * timeMul);
|
||||||
Apply(s, st);
|
Apply(s, st);
|
||||||
Activate(s);
|
Activate(s);
|
||||||
OnCount++;
|
OnCount++;
|
||||||
|
|
@ -265,6 +269,18 @@ namespace WL.Combat.Reaction
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// WL #816zb — 점멸 종료 시각의 상한. startAt + blinkMaxSeconds 를 넘지 못한다.
|
||||||
|
/// blinkMaxSeconds ≤ 0 이면 상한 없음 = 기존 동작 그대로(C8 롤백 경로).
|
||||||
|
/// </summary>
|
||||||
|
static float CapOffAt(WLHitBlinkSettings st, float startAt, float wantOffAt)
|
||||||
|
{
|
||||||
|
float max = st.blinkMaxSeconds;
|
||||||
|
if (max <= 0f) return wantOffAt;
|
||||||
|
float limit = startAt + max;
|
||||||
|
return wantOffAt < limit ? wantOffAt : limit;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>WL #811gh — RGB 만 배수. 알파는 건드리지 않는다(배수 1 이면 원본 Color 를 그대로 돌려준다 = 오차 0).</summary>
|
/// <summary>WL #811gh — RGB 만 배수. 알파는 건드리지 않는다(배수 1 이면 원본 Color 를 그대로 돌려준다 = 오차 0).</summary>
|
||||||
static Color ScaleRgb(Color c, float m)
|
static Color ScaleRgb(Color c, float m)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,261 @@
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// MobStagger.cs — 피격 몹 짧은 경직(스태거) · 원본 훅 0줄 · 코루틴 0 · GC 0
|
||||||
|
//
|
||||||
|
// PD 지시 #816zb : 「피격 된 적에게 경직이 없는 것 같아. 밀린 상태에서 다시 이동하려고 하기 때문에 어색하고
|
||||||
|
// 액션 게임의 타격감을 느끼기 어려워. 피격 된 적은 짧게 경직 효과을 적용해서 타격감을 살려줘.」
|
||||||
|
//
|
||||||
|
// ■ 무엇을 하나 (HitConfirmed / Killed / Spawned 구독 · Assets/Script 무수정)
|
||||||
|
// HitConfirmed(몹 · 피해 > 0 · 이 타격으로 죽지 않음)
|
||||||
|
// → 그 몹의 NavMeshAgent.speed 를 0 으로 잠그고 velocity 를 지우고 Animator.speed 를 0 으로 세운다.
|
||||||
|
// 등급별 시간(일반 0.18 · 엘리트 0.12 · 보스 0.08 s · WLHitFeelSettings)이 지나면 원래 값으로 되돌린다.
|
||||||
|
// → 다시 맞으면 시간이 갱신된다(연타 중에는 계속 붙잡혀 있다 = 히트 락).
|
||||||
|
// Killed / Spawned(풀 재사용) → 즉시 원복하고 상태를 놓는다.
|
||||||
|
//
|
||||||
|
// ■ 왜 isStopped 가 아니라 speed 인가
|
||||||
|
// MobActor 의 상태 기계가 Chase/Patrol 전이마다 `m_NavMeshAgent.isStopped = false` 를 직접 쓴다(MobActor.cs:304·309·339).
|
||||||
|
// isStopped 로 막으면 같은 프레임에 되돌려져 경직이 먹지 않고, NavMesh 밖/비활성 에이전트에 쓰면 "Resume" 예외가 난다(Q3 D-1).
|
||||||
|
// speed 는 몹 스폰(MobActor.cs:113)·이동 버프(Actor.Add_MoveSpeed) 외에는 매 프레임 덮어쓰이지 않으므로
|
||||||
|
// 매 틱 0 을 다시 써 두면 AI 와 싸우지 않고 확실히 멈춘다. 회전(angularSpeed)은 그대로 둬 바라보기는 유지된다.
|
||||||
|
//
|
||||||
|
// ■ 넉백(원본)과의 관계 — PD 「밀린 상태에서 다시 이동하려고 하기 때문에 어색하고」
|
||||||
|
// 원본 넉백은 transform.Translate 로 미는 코루틴(Actor.Co_KnockBack)이라 speed 0 과 충돌하지 않는다.
|
||||||
|
// 넉백이 진행 중인 동안은 경직 종료 시각을 계속 뒤로 밀어, **넉백이 끝난 시점부터** 경직 시간을 다시 준다
|
||||||
|
// (밀리다 멈추자마자 걸어 나오는 그 어색함이 원인). 무한 연장 방지로 피격 후 HoldMaxSeconds 까지만 민다.
|
||||||
|
// 스턴·빙결 등 게임 자체 CC(IsCC(ignoreknockback:true))가 걸리면 그 쪽이 몹을 소유하므로 우리는 즉시 손을 뗀다
|
||||||
|
// (긴 스턴 동안 애니메이터를 얼려 두는 사고 방지).
|
||||||
|
//
|
||||||
|
// ■ 되돌리기 안전 — 우리가 쓴 0 이 그대로 남아 있을 때만 원래 값으로 복원한다(그 사이 버프·사망·리스폰이 바꿨으면 건드리지 않는다).
|
||||||
|
// WLHitFeelSettings.mobStaggerEnabled = 0(또는 enabled = 0 · RuntimeDisabled)이면 아무 것도 하지 않는다 = 기존 동작 100 %. 롤백 스위치.
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.AI;
|
||||||
|
using WL.Combat.Core;
|
||||||
|
|
||||||
|
namespace WL.Combat.Reaction
|
||||||
|
{
|
||||||
|
public static class MobStagger
|
||||||
|
{
|
||||||
|
/// <summary>넉백이 비정상적으로 길 때를 대비한 연장 상한(초 · 피격 시각 기준). 기본 넉백 f_KnockbackTime 은 이보다 훨씬 짧다.</summary>
|
||||||
|
const float HoldMaxSeconds = 1f;
|
||||||
|
|
||||||
|
sealed class State
|
||||||
|
{
|
||||||
|
public Actor actor;
|
||||||
|
public NavMeshAgent agent;
|
||||||
|
public Animator anim;
|
||||||
|
public float until, hitAt, duration;
|
||||||
|
public float savedSpeed, savedAnimSpeed;
|
||||||
|
public bool speedWritten, animWritten;
|
||||||
|
public int activeIndex = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static readonly Dictionary<Actor, State> s_states = new Dictionary<Actor, State>(64);
|
||||||
|
static State[] s_active = new State[32];
|
||||||
|
static int s_activeCount;
|
||||||
|
|
||||||
|
// ── 진단(프로브가 읽는다)
|
||||||
|
public static bool Subscribed;
|
||||||
|
public static int StaggerCount, RefreshCount, ReleasedByCcCount, HoldCount, SkippedFilter, SkippedNoAgent, EndedCount;
|
||||||
|
public static float LastDuration;
|
||||||
|
public static int ActiveCount { get { return s_activeCount; } }
|
||||||
|
public static int StateCount { get { return s_states.Count; } }
|
||||||
|
|
||||||
|
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterAssembliesLoaded)]
|
||||||
|
static void Register()
|
||||||
|
{
|
||||||
|
CombatEvents.HitConfirmed.Add(OnHitConfirmed);
|
||||||
|
CombatEvents.Killed.Add(OnKilled);
|
||||||
|
CombatEvents.Spawned.Add(OnSpawned);
|
||||||
|
Subscribed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static WLHitFeelSettings St { get { return WL.Feel.WLHitFeel.Settings; } }
|
||||||
|
|
||||||
|
/// <summary>설정 에셋이 없거나 전체 스위치·경직 스위치가 꺼져 있으면 이 시스템은 통째로 비활성.</summary>
|
||||||
|
public static bool Enabled
|
||||||
|
{
|
||||||
|
get { var st = St; return st != null && st.enabled && st.mobStaggerEnabled && !WL.Feel.WLHitFeel.RuntimeDisabled; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ───────────────────────────────────────── 이벤트
|
||||||
|
static void OnHitConfirmed(in HitConfirmedEvent e)
|
||||||
|
{
|
||||||
|
if (!Enabled) return;
|
||||||
|
var victim = e.victim;
|
||||||
|
if (victim == null || !(victim is MobActor)) { SkippedFilter++; return; }
|
||||||
|
if (e.damage <= 0d || e.isKill || victim.IsDead()) { SkippedFilter++; return; } // 무효타·처치타는 경직 없음(사망 모션을 얼리지 않는다)
|
||||||
|
|
||||||
|
var st = St;
|
||||||
|
float dur = victim.IsSubRole(eSubRol.Boss) ? st.mobStaggerSecondsBoss
|
||||||
|
: victim.IsSubRole(eSubRol.Elite) ? st.mobStaggerSecondsElite
|
||||||
|
: st.mobStaggerSeconds;
|
||||||
|
if (dur <= 0.0001f) { SkippedFilter++; return; }
|
||||||
|
|
||||||
|
var s = GetState(victim);
|
||||||
|
if (s == null) { SkippedNoAgent++; return; }
|
||||||
|
|
||||||
|
float now = Time.unscaledTime;
|
||||||
|
if (s.activeIndex >= 0) RefreshCount++; else StaggerCount++;
|
||||||
|
s.hitAt = now;
|
||||||
|
s.duration = dur;
|
||||||
|
s.until = now + dur;
|
||||||
|
LastDuration = dur;
|
||||||
|
|
||||||
|
WLReactionRunner.Ensure();
|
||||||
|
Freeze(s);
|
||||||
|
Activate(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void OnKilled(in KilledEvent e) { Release(e.victim); }
|
||||||
|
|
||||||
|
static void OnSpawned(in SpawnedEvent e) { Release(e.actor); }
|
||||||
|
|
||||||
|
static void Release(Actor actor)
|
||||||
|
{
|
||||||
|
State s;
|
||||||
|
if (actor == null || !s_states.TryGetValue(actor, out s)) return;
|
||||||
|
Restore(s);
|
||||||
|
Deactivate(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ───────────────────────────────────────── 상태
|
||||||
|
static State GetState(Actor actor)
|
||||||
|
{
|
||||||
|
State s;
|
||||||
|
if (s_states.TryGetValue(actor, out s))
|
||||||
|
{
|
||||||
|
if (s.agent == null) s.agent = actor.Get_Agent(); // 풀 재사용 중 에이전트가 붙었을 수 있다
|
||||||
|
if (s.anim == null) s.anim = actor.m_animation;
|
||||||
|
return (s.agent != null || s.anim != null) ? s : null;
|
||||||
|
}
|
||||||
|
var agent = actor.Get_Agent();
|
||||||
|
var anim = actor.m_animation;
|
||||||
|
if (agent == null && anim == null) return null; // 멈출 수단이 없다 — 상태를 만들지 않는다
|
||||||
|
s = new State { actor = actor, agent = agent, anim = anim };
|
||||||
|
s_states.Add(actor, s);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>speed 0 · velocity 0 · Animator.speed 0. 원래 값은 처음 얼릴 때 1회만 기억한다.</summary>
|
||||||
|
static void Freeze(State s)
|
||||||
|
{
|
||||||
|
var a = s.agent;
|
||||||
|
if (a != null)
|
||||||
|
{
|
||||||
|
if (!s.speedWritten) { s.savedSpeed = a.speed; s.speedWritten = true; }
|
||||||
|
a.speed = 0f;
|
||||||
|
// 비활성(넉백_byHitter 가 agent.enabled = false)·NavMesh 밖 에이전트에 velocity 를 쓰면 예외가 난다.
|
||||||
|
if (a.enabled && a.isOnNavMesh) a.velocity = Vector3.zero;
|
||||||
|
}
|
||||||
|
var an = s.anim;
|
||||||
|
if (an != null)
|
||||||
|
{
|
||||||
|
if (!s.animWritten) { s.savedAnimSpeed = an.speed; s.animWritten = true; }
|
||||||
|
an.speed = 0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>우리가 쓴 0 이 그대로 남아 있을 때만 원래 값으로 되돌린다(버프·사망·리스폰이 바꿨으면 손대지 않는다).</summary>
|
||||||
|
static void Restore(State s)
|
||||||
|
{
|
||||||
|
if (s.speedWritten)
|
||||||
|
{
|
||||||
|
var a = s.agent;
|
||||||
|
if (a != null && a.speed == 0f) a.speed = s.savedSpeed;
|
||||||
|
s.speedWritten = false;
|
||||||
|
}
|
||||||
|
if (s.animWritten)
|
||||||
|
{
|
||||||
|
var an = s.anim;
|
||||||
|
if (an != null && an.speed == 0f) an.speed = s.savedAnimSpeed;
|
||||||
|
s.animWritten = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Activate(State s)
|
||||||
|
{
|
||||||
|
if (s.activeIndex >= 0) return;
|
||||||
|
if (s_activeCount == s_active.Length) System.Array.Resize(ref s_active, s_active.Length * 2);
|
||||||
|
s.activeIndex = s_activeCount;
|
||||||
|
s_active[s_activeCount++] = s;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Deactivate(State s)
|
||||||
|
{
|
||||||
|
int i = s.activeIndex;
|
||||||
|
if (i < 0) return;
|
||||||
|
int last = --s_activeCount;
|
||||||
|
s_active[i] = s_active[last];
|
||||||
|
if (s_active[i] != null) s_active[i].activeIndex = i;
|
||||||
|
s_active[last] = null;
|
||||||
|
s.activeIndex = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void Tick(float now)
|
||||||
|
{
|
||||||
|
if (s_activeCount == 0) return;
|
||||||
|
bool on = Enabled;
|
||||||
|
for (int i = s_activeCount - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
var s = s_active[i];
|
||||||
|
var actor = s.actor;
|
||||||
|
|
||||||
|
if (!on || actor == null || !actor.isActiveAndEnabled || actor.IsDead())
|
||||||
|
{
|
||||||
|
Restore(s); Deactivate(s); EndedCount++; continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 스턴·빙결·공중 띄우기 등 게임 자체 CC 가 걸리면 그 쪽이 몹을 소유한다 — 우리는 손을 뗀다.
|
||||||
|
if (actor.IsCC(true))
|
||||||
|
{
|
||||||
|
Restore(s); Deactivate(s); ReleasedByCcCount++; continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 남은 CC = 넉백. 밀리는 동안은 종료 시각을 뒤로 밀어 '넉백이 끝난 시점부터' 경직 시간을 준다.
|
||||||
|
if (actor.IsCC() && now < s.hitAt + HoldMaxSeconds)
|
||||||
|
{
|
||||||
|
s.until = now + s.duration;
|
||||||
|
HoldCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (now >= s.until)
|
||||||
|
{
|
||||||
|
Restore(s); Deactivate(s); EndedCount++; continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Freeze(s); // AI 상태 전이가 speed 를 되돌려도 매 틱 다시 0 으로 잠근다
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>모든 몹의 speed·Animator 를 되돌린다(러너 파괴 · 앱 종료 · 프로브 정리). 상태 캐시는 유지.</summary>
|
||||||
|
public static void RestoreAll()
|
||||||
|
{
|
||||||
|
foreach (var kv in s_states) Restore(kv.Value);
|
||||||
|
for (int i = 0; i < s_activeCount; i++) { s_active[i].activeIndex = -1; s_active[i] = null; }
|
||||||
|
s_activeCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>씬 전환: 되돌리고 상태 캐시(파괴된 Actor 참조)를 버린다.</summary>
|
||||||
|
public static void ClearAll()
|
||||||
|
{
|
||||||
|
RestoreAll();
|
||||||
|
s_states.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>프로브용: 이 몹이 지금 경직 중인가.</summary>
|
||||||
|
public static bool IsStaggered(Actor actor)
|
||||||
|
{
|
||||||
|
State s;
|
||||||
|
return actor != null && s_states.TryGetValue(actor, out s) && s.activeIndex >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>프로브용 카운터 초기화.</summary>
|
||||||
|
public static void ResetDiagnostics()
|
||||||
|
{
|
||||||
|
StaggerCount = RefreshCount = ReleasedByCcCount = HoldCount = SkippedFilter = SkippedNoAgent = EndedCount = 0;
|
||||||
|
LastDuration = 0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5c7d057d17c2430eaa8341432116bebb
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -72,18 +72,24 @@ namespace WL.Combat.Reaction
|
||||||
|
|
||||||
// ─────────────────────────────────────────── 점멸
|
// ─────────────────────────────────────────── 점멸
|
||||||
[Header("점멸 (HitConfirmed → MPB on → blinkOnSeconds 뒤 off)")]
|
[Header("점멸 (HitConfirmed → MPB on → blinkOnSeconds 뒤 off)")]
|
||||||
[Tooltip("점멸 색(HDR · 알베도 색 프로퍼티에 쓴다). 1 보다 크면 텍스처가 밝게 뜬다 = 흰 번쩍임. 기준서 「흰색 0.03~0.06 s」.")]
|
[Tooltip("점멸 색(HDR · 알베도 색 프로퍼티에 쓴다). 1 보다 크면 텍스처가 밝게 뜬다 = 흰 번쩍임. 기준서 「흰색 0.03~0.06 s」.\n" +
|
||||||
[ColorUsage(true, true)] public Color blinkColor = new Color(2.2f, 2.2f, 2.2f, 1f);
|
"PD #816zb — '짧게, 대신 세게'. 2.2 → 3.0.")]
|
||||||
|
[ColorUsage(true, true)] public Color blinkColor = new Color(3f, 3f, 3f, 1f);
|
||||||
|
|
||||||
[Tooltip("에미션 프로퍼티도 함께 올린다(MK Toon 은 키워드 없이 가산 · URP Lit 은 _EMISSION 키워드가 켜진 머티리얼만).")]
|
[Tooltip("에미션 프로퍼티도 함께 올린다(MK Toon 은 키워드 없이 가산 · URP Lit 은 _EMISSION 키워드가 켜진 머티리얼만).")]
|
||||||
public bool useEmission = true;
|
public bool useEmission = true;
|
||||||
|
|
||||||
[Tooltip("점멸 에미션 색(HDR).")]
|
[Tooltip("점멸 에미션 색(HDR). PD #816zb — 0.6 → 0.9 (같은 시간에 더 세게).")]
|
||||||
[ColorUsage(true, true)] public Color blinkEmission = new Color(0.6f, 0.6f, 0.6f, 1f);
|
[ColorUsage(true, true)] public Color blinkEmission = new Color(0.9f, 0.9f, 0.9f, 1f);
|
||||||
|
|
||||||
[Tooltip("켜져 있는 시간(초 · unscaled). Feel 프리셋 Blink On 0.03.")]
|
[Tooltip("켜져 있는 시간(초 · unscaled). Feel 프리셋 Blink On 0.03.")]
|
||||||
public float blinkOnSeconds = 0.03f;
|
public float blinkOnSeconds = 0.03f;
|
||||||
|
|
||||||
|
[Tooltip("한 번 켜진 점멸이 이어질 수 있는 **총 상한**(초 · unscaled · 0 이하면 상한 없음).\n" +
|
||||||
|
"PD #816zb — 몹이 계속 하얗게 보이던 원인은 ① 임팩트 티어 시간 배수(최대 2.4 → 0.072 s)와\n" +
|
||||||
|
"② 점멸 중 추가 타격마다 blinkOffAt 을 다시 미루는 연장 경로였다. 연장까지 포함해 첫 점멸 시작 시각 + 이 값에서 끊는다. 0.06 = 발주 규정값.")]
|
||||||
|
public float blinkMaxSeconds = 0.06f;
|
||||||
|
|
||||||
[Tooltip("꺼진 뒤 다시 켤 수 있기까지의 최소 간격(초 · unscaled). Feel 프리셋 Blink Off 0.03 — 같은 프레임 다중 히트(스윙 1회 5~7타)를 1회 점멸로 묶는다.")]
|
[Tooltip("꺼진 뒤 다시 켤 수 있기까지의 최소 간격(초 · unscaled). Feel 프리셋 Blink Off 0.03 — 같은 프레임 다중 히트(스윙 1회 5~7타)를 1회 점멸로 묶는다.")]
|
||||||
public float blinkOffSeconds = 0.03f;
|
public float blinkOffSeconds = 0.03f;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ namespace WL.Combat.Reaction
|
||||||
KillChain.ClearAll();
|
KillChain.ClearAll();
|
||||||
DamageAggregator.ClearAll();
|
DamageAggregator.ClearAll();
|
||||||
MobHitFlash.ClearAll();
|
MobHitFlash.ClearAll();
|
||||||
|
MobStagger.ClearAll(); // WL #816zb — 씬을 넘기며 speed 0 인 몹이 남지 않게
|
||||||
}
|
}
|
||||||
|
|
||||||
void Update()
|
void Update()
|
||||||
|
|
@ -40,9 +41,10 @@ namespace WL.Combat.Reaction
|
||||||
KillChain.Tick(now);
|
KillChain.Tick(now);
|
||||||
DamageAggregator.Tick(now);
|
DamageAggregator.Tick(now);
|
||||||
MobHitFlash.Tick(now);
|
MobHitFlash.Tick(now);
|
||||||
|
MobStagger.Tick(now); // WL #816zb — 피격 경직 해제
|
||||||
}
|
}
|
||||||
|
|
||||||
void OnDisable() { MobHitFlash.RestoreAll(); KillChain.ReleaseBurst(); }
|
void OnDisable() { MobHitFlash.RestoreAll(); MobStagger.RestoreAll(); KillChain.ReleaseBurst(); }
|
||||||
void OnApplicationQuit() { MobHitFlash.RestoreAll(); KillChain.ReleaseBurst(); }
|
void OnApplicationQuit() { MobHitFlash.RestoreAll(); MobStagger.RestoreAll(); KillChain.ReleaseBurst(); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,11 @@ MonoBehaviour:
|
||||||
targetMobsOnly: 1
|
targetMobsOnly: 1
|
||||||
onlyByMainPC: 0
|
onlyByMainPC: 0
|
||||||
skipWhenDamageZero: 1
|
skipWhenDamageZero: 1
|
||||||
blinkColor: {r: 2.2, g: 2.2, b: 2.2, a: 1}
|
blinkColor: {r: 3, g: 3, b: 3, a: 1}
|
||||||
useEmission: 1
|
useEmission: 1
|
||||||
blinkEmission: {r: 0.6, g: 0.6, b: 0.6, a: 1}
|
blinkEmission: {r: 0.9, g: 0.9, b: 0.9, a: 1}
|
||||||
blinkOnSeconds: 0.03
|
blinkOnSeconds: 0.03
|
||||||
|
blinkMaxSeconds: 0.06
|
||||||
blinkOffSeconds: 0.03
|
blinkOffSeconds: 0.03
|
||||||
colorProperties:
|
colorProperties:
|
||||||
- _AlbedoColor
|
- _AlbedoColor
|
||||||
|
|
|
||||||
|
|
@ -37,4 +37,8 @@ MonoBehaviour:
|
||||||
flashColor: {r: 1, g: 1, b: 1, a: 0.1}
|
flashColor: {r: 1, g: 1, b: 1, a: 0.1}
|
||||||
flashDuration: 0.06
|
flashDuration: 0.06
|
||||||
hapticEnabled: 0
|
hapticEnabled: 0
|
||||||
|
mobStaggerEnabled: 1
|
||||||
|
mobStaggerSeconds: 0.18
|
||||||
|
mobStaggerSecondsElite: 0.12
|
||||||
|
mobStaggerSecondsBoss: 0.08
|
||||||
logHits: 0
|
logHits: 0
|
||||||
|
|
|
||||||
|
|
@ -141,6 +141,24 @@ public class WLHitFeelSettings : ScriptableObject
|
||||||
[Tooltip("NiceVibrations 햅틱. 모바일 배터리 때문에 기본 false.")]
|
[Tooltip("NiceVibrations 햅틱. 모바일 배터리 때문에 기본 false.")]
|
||||||
public bool hapticEnabled = false;
|
public bool hapticEnabled = false;
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────── ⑥ 피격 몹 경직 (스태거)
|
||||||
|
|
||||||
|
[Header("⑥ 피격 몹 경직 — 맞은 몹이 아주 짧게 멈춘다")]
|
||||||
|
[Tooltip("PD #816zb — 맞은 몹이 넉백으로 밀린 직후 바로 걸어 나와 타격감이 죽는 문제.\n" +
|
||||||
|
"직접 타격(HitConfirmed) 마다 NavMeshAgent 속도를 0 으로 잠그고 애니메이터를 멈춘다(WL.Combat.Reaction.MobStagger).\n" +
|
||||||
|
"끄면 그 구독자가 아무 것도 하지 않는다 = 기존 동작 100 %. 롤백 스위치.")]
|
||||||
|
public bool mobStaggerEnabled = true;
|
||||||
|
|
||||||
|
[Tooltip("일반 몹 경직 길이(초 · unscaled). 콤보 간격 0.42~0.47 s 의 절반 이하로 둔다 — 0.18 = 발주 규정값.\n" +
|
||||||
|
"넉백 중에 맞으면 넉백이 끝난 시점부터 이 길이를 1회 다시 준다(밀리다 말고 곧장 걷지 않게).")]
|
||||||
|
public float mobStaggerSeconds = 0.18f;
|
||||||
|
|
||||||
|
[Tooltip("엘리트 경직 길이(초). 일반보다 짧게 — 압박감을 유지한다.")]
|
||||||
|
public float mobStaggerSecondsElite = 0.12f;
|
||||||
|
|
||||||
|
[Tooltip("보스 경직 길이(초). 가장 짧게 — 보스 패턴이 끊기면 안 된다.")]
|
||||||
|
public float mobStaggerSecondsBoss = 0.08f;
|
||||||
|
|
||||||
// ─────────────────────────────────────────── 진단
|
// ─────────────────────────────────────────── 진단
|
||||||
|
|
||||||
[Header("진단 (검증용 · 배포 시 false)")]
|
[Header("진단 (검증용 · 배포 시 false)")]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue