236 lines
10 KiB
C#
236 lines
10 KiB
C#
using System.Collections;
|
|
using UnityEngine;
|
|
|
|
namespace WL.Feel
|
|
{
|
|
/// <summary>
|
|
/// PD #797 — "타격 시점에만 살짝" 임팩트 연출의 진입점.
|
|
///
|
|
/// ■ 훅 (힘민지 코어 변경은 이 1 줄뿐)
|
|
/// Actor.cs Get_Damage() 안, Cal_Damage(_dinfo) 직후의 기존 카메라 셰이크 줄을
|
|
/// if (_dinfo.Beater.IsMainPC() && !WL.Feel.WLHitFeel.OnHit(this, _dinfo))
|
|
/// MyValue.m_RealCamera.ShakeCamera(1);
|
|
/// 로 바꾼다. OnHit 이 false 를 반환하면(에셋 없음·enabled=false) 기존 동작이 100% 그대로다.
|
|
///
|
|
/// ■ 왜 Cal_Damage 직후인가
|
|
/// · 그 시점에 _dinfo.Damage 가 방어력·감쇠까지 끝난 최종값이다.
|
|
/// · Cal_Damage 가 _dinfo.IsDeadByThisDamage 를 세팅한 뒤라 킬 여부를 알 수 있다.
|
|
/// · Cal_Damage 자체는 도트(화상·중독)·연쇄 데미지에서도 불리므로(Actor.cs 1114/1400/1451/1534)
|
|
/// 그 안에 훅을 넣으면 '타격 시점' 이 아닌 도트 틱마다 연출이 터진다.
|
|
/// Get_Damage 경로는 직접 타격 전용이라 여기가 맞다.
|
|
///
|
|
/// ■ 스로틀 — 스윙 1회당 1발
|
|
/// 근접 스윙 1 회가 0.02 s 박스 투사체로 군집의 몹 5~7 마리를 **같은 프레임에** 때린다(PM 실측).
|
|
/// throttleSeconds(기본 0.08 s) 안의 후속 타격은 연출을 건너뛴다.
|
|
/// 콤보 간격이 0.42~0.47 s 이므로 3 히트는 각각 정상 발동한다.
|
|
///
|
|
/// ■ 모바일 예산
|
|
/// 드로우 추가 0 (플래시 옵션을 켰을 때만 +1) · 포스트 프로세싱 볼륨 오버라이드 없음 ·
|
|
/// 셰이크는 transform 만 · 매 타격 GC 할당 없음(코루틴 3 개는 재사용하지 않고 짧게 끝난다).
|
|
/// </summary>
|
|
public static class WLHitFeel
|
|
{
|
|
const string SettingsPath = "WL/WLHitFeelSettings";
|
|
|
|
static WLHitFeelSettings s_Settings;
|
|
static bool s_Loaded;
|
|
|
|
/// <summary>설정 에셋. 없으면 null — 그 경우 이 기능은 통째로 비활성이다.</summary>
|
|
public static WLHitFeelSettings Settings
|
|
{
|
|
get
|
|
{
|
|
if (!s_Loaded)
|
|
{
|
|
s_Loaded = true;
|
|
s_Settings = Resources.Load<WLHitFeelSettings>(SettingsPath);
|
|
}
|
|
return s_Settings;
|
|
}
|
|
}
|
|
|
|
/// <summary>에디터/진단에서 에셋을 갈아끼울 때.</summary>
|
|
public static void ReloadSettings() { s_Loaded = false; s_Settings = null; }
|
|
|
|
/// <summary>
|
|
/// 진단·A/B 전용 런타임 스위치. **에셋을 건드리지 않고** 이 기능만 끈다.
|
|
/// (Play 중에 설정 SO 인스턴스를 고치면 디스크로 새는 사고가 있었다 — 그래서 별도 필드로 둔다.)
|
|
/// Play 종료 시 도메인 리로드로 자동 false 로 돌아간다.
|
|
/// </summary>
|
|
public static bool RuntimeDisabled;
|
|
|
|
// ── WL #811b: 코어 이벤트 파이프라인 편입 — HitConfirmed 구독자.
|
|
// CombatEvents.RaiseHitConfirmed 가 LastCoreHandled(= OnHit 반환값)로 기존 false 반환 규약(원본 ShakeCamera 폴백)을 그대로 잇는다.
|
|
// 코어가 비활성이면 RaiseHitConfirmed 가 OnHit 을 직접 부르므로 이 구독은 돌지 않는다(동작 동일).
|
|
public static bool LastCoreHandled;
|
|
public static bool CoreSubscribed;
|
|
|
|
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterAssembliesLoaded)]
|
|
static void RegisterCoreSubscriber()
|
|
{
|
|
WL.Combat.Core.CombatEvents.HitConfirmed.Add(OnHitConfirmedEvent);
|
|
CoreSubscribed = true;
|
|
}
|
|
|
|
static void OnHitConfirmedEvent(in WL.Combat.Core.HitConfirmedEvent e) { LastCoreHandled = OnHit(e.victim, e.dinfo); }
|
|
|
|
// ── 상태
|
|
static float s_LastFiredTime = -999f;
|
|
static WLHitFeelRunner s_Runner;
|
|
static WLHitFeelCameraShake s_Shaker;
|
|
static GameObject s_ShakerHost;
|
|
|
|
// ── 진단 (WL797_Probe 가 읽는다)
|
|
public static int HitCount; // 훅이 불린 총 횟수
|
|
public static int FiredCount; // 실제로 연출이 나간 횟수
|
|
public static int ThrottledCount; // 스로틀로 건너뛴 횟수
|
|
public static float LastHitTime; // unscaled
|
|
public static float LastFiredTime; // unscaled
|
|
public static float LastHitStopMeasured; // 실측된 히트스톱 길이(초)
|
|
public static int LastFiredFrame; // 연출이 나간 프레임 번호
|
|
public static Transform LastVictimTf; // 마지막으로 펀치를 건 몹
|
|
public static float LastVictimBaseScaleX; // 그 몹의 펀치 직전 스케일(비율 계산 기준)
|
|
public static bool LastWasKill;
|
|
|
|
public static void ResetDiagnostics()
|
|
{
|
|
HitCount = FiredCount = ThrottledCount = 0;
|
|
LastHitTime = LastFiredTime = 0f;
|
|
LastHitStopMeasured = 0f;
|
|
s_LastFiredTime = -999f;
|
|
var sh = GetShaker(false);
|
|
if (sh != null) sh.MaxOffsetMagnitude = 0f;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 셰이커는 **RealCamera 가 직접 transform.position 을 쓰는 그 게임오브젝트**에 붙여야 한다.
|
|
/// (카메라가 자식이면 부모가 움직일 때 우리 오프셋을 되돌리지 못해 누적된다)
|
|
/// RealCamera 가 없을 때만 Camera.main 으로 폴백한다.
|
|
/// </summary>
|
|
public static WLHitFeelCameraShake GetShaker(bool create)
|
|
{
|
|
GameObject host = null;
|
|
if (MyValue.m_RealCamera != null) host = MyValue.m_RealCamera.gameObject;
|
|
else if (Camera.main != null) host = Camera.main.gameObject;
|
|
if (host == null) return null;
|
|
|
|
if (s_Shaker != null && s_ShakerHost == host) return s_Shaker;
|
|
|
|
s_ShakerHost = host;
|
|
s_Shaker = host.GetComponent<WLHitFeelCameraShake>();
|
|
if (s_Shaker == null && create) s_Shaker = host.AddComponent<WLHitFeelCameraShake>();
|
|
return s_Shaker;
|
|
}
|
|
|
|
static WLHitFeelRunner Runner
|
|
{
|
|
get
|
|
{
|
|
if (s_Runner == null)
|
|
{
|
|
// hideFlags 를 주지 않는다 — 검증 때 하이어라키에서 보이는 편이 낫고,
|
|
// HideAndDontSave 는 DontDestroyOnLoad 와 겹쳐 경고를 낸다.
|
|
var go = new GameObject("__WLHitFeel");
|
|
Object.DontDestroyOnLoad(go);
|
|
s_Runner = go.AddComponent<WLHitFeelRunner>();
|
|
}
|
|
return s_Runner;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 타격 훅.
|
|
/// </summary>
|
|
/// <returns>
|
|
/// true = WL 이 카메라 연출을 담당했다(또는 의도적으로 억제했다) → 호출측의 기존 셰이크를 부르지 말 것.
|
|
/// false = 이 기능이 비활성 → 호출측이 기존 동작을 그대로 수행할 것.
|
|
/// </returns>
|
|
public static bool OnHit(Actor victim, DamageInfo dinfo)
|
|
{
|
|
var st = Settings;
|
|
if (st == null || !st.enabled || RuntimeDisabled) return false; // ← 롤백 경로: 기존 동작 100 %
|
|
|
|
if (victim == null || dinfo == null) return false;
|
|
|
|
var beater = dinfo.Beater;
|
|
if (beater == null || !beater.IsMainPC()) return false; // 내 PC 가 때린 것만
|
|
if (victim.IsMainPC()) return false; // PC 피격은 이번 범위 밖
|
|
|
|
bool suppressLegacy = st.suppressLegacyShake;
|
|
|
|
if (dinfo.Damage <= 0d) return suppressLegacy; // 데미지 0 = 연출 없음
|
|
|
|
HitCount++;
|
|
LastHitTime = Time.unscaledTime;
|
|
|
|
// ── 스윙 1회당 1발
|
|
if (LastHitTime - s_LastFiredTime < st.throttleSeconds)
|
|
{
|
|
ThrottledCount++;
|
|
if (st.logHits)
|
|
Debug.Log(string.Format("[WLHitFeel] THROTTLED t={0:F3} victim={1} dmg={2}",
|
|
LastHitTime, victim.name, dinfo.Damage));
|
|
return suppressLegacy;
|
|
}
|
|
|
|
s_LastFiredTime = LastHitTime;
|
|
LastFiredTime = LastHitTime;
|
|
LastFiredFrame = Time.frameCount;
|
|
FiredCount++;
|
|
|
|
bool kill = dinfo.IsDeadByThisDamage;
|
|
LastWasKill = kill;
|
|
|
|
if (st.logHits)
|
|
Debug.Log(string.Format("[WLHitFeel] FIRE t={0:F3} frame={1} victim={2} dmg={3} kill={4}",
|
|
LastHitTime, Time.frameCount, victim.name, dinfo.Damage, kill));
|
|
|
|
// ── ① 히트스톱
|
|
if (st.hitStopEnabled)
|
|
{
|
|
float dur = Mathf.Min(kill ? st.hitStopKillDuration : st.hitStopDuration, st.hitStopMax);
|
|
if (dur > 0.0001f) Runner.RunHitStop(st, dur);
|
|
}
|
|
|
|
// ── ② 카메라 셰이크
|
|
if (st.cameraShakeEnabled)
|
|
{
|
|
var shaker = GetShaker(true);
|
|
if (shaker != null)
|
|
{
|
|
float amp = st.shakeAmplitude * (kill ? Mathf.Max(1f, st.shakeKillMultiplier) : 1f);
|
|
shaker.Shake(amp, st.shakeDuration, st.shakeFrequency, st.shakeAxisWeight);
|
|
}
|
|
}
|
|
|
|
// ── ③ 피격 몹 스케일 펀치
|
|
if (st.scalePunchEnabled && !(st.skipPunchOnKill && kill) && !(st.skipPunchOnBoss && victim.IsSubRole(eSubRol.Boss)))
|
|
{
|
|
LastVictimTf = victim.transform;
|
|
LastVictimBaseScaleX = LastVictimTf.localScale.x;
|
|
Runner.RunScalePunch(st, victim);
|
|
}
|
|
|
|
// ── ④ 화면 플래시 (기본 OFF)
|
|
if (st.flashEnabled) Runner.RunFlash(st);
|
|
|
|
// ── ⑤ 진동 (기본 OFF)
|
|
if (st.hapticEnabled) PlayHaptic();
|
|
|
|
return suppressLegacy;
|
|
}
|
|
|
|
static void PlayHaptic()
|
|
{
|
|
#if UNITY_ANDROID || UNITY_IOS
|
|
try
|
|
{
|
|
Lofelt.NiceVibrations.HapticPatterns.PlayPreset(
|
|
Lofelt.NiceVibrations.HapticPatterns.PresetType.LightImpact);
|
|
}
|
|
catch { /* 햅틱 미지원 기기 — 무시 */ }
|
|
#endif
|
|
}
|
|
}
|
|
}
|