Files
Project-M/Assets/_Project/Scripts/Client/Presentation/RumbleUtil.cs
T
kronic c3b53cef28 Combat feel pass: enemy hit-flash, melee connect cue, kill pop, gamepad rumble, footsteps, damage-number tiering
Implements the "what's missing" feel backlog from the combat overhaul investigation (wf_c6c87dc5-9c3). All
client-only, observe-only (PresentationSystemGroup), no sim/netcode change, rollback-safe; new FeelConfig knobs
default-stamped on play-enter.

- ENEMY HIT-FLASH (#1 missing): on the enemy Health-decrease edge, a bright body-scaled particle puff in the
  previously-unused FeelConfig.HitFlashColor (via a new EmitColored helper using per-emit startColor) -- the staple
  "I connected" read. (A true material body-flash needs an AnimatedLitShader emission property + Entities Graphics
  MaterialProperty; the puff is the asset-free version.)
- MELEE CONNECT-vs-WHIFF: on the local swing, a client-side MeleeConeMath.InCone overlap over the cached enemy
  snapshot -> immediate connect read (brightens the slash arc, hit-spark at the nearest enemy, a meaty low "thunk"
  SFX, extra FOV punch) before the authoritative server spark arrives. Whiffs stay dim.
- KILL POP: a colored flash burst + rumble on enemy death (on top of the existing burst/shake/FOV).
- GAMEPAD RUMBLE: new RumbleUtil (auto-stopping pulse, gamepad-only, stops on focus-loss, reset on play-enter)
  pulsed on local hit-taken / hit-dealt / kill, gated on AimPresentation.Scheme==Gamepad.
- FOOTSTEPS: edge-detect local locomotion from the position delta -> soft step SFX at a cadence while moving.
- DAMAGE-NUMBER TIERING: SpawnNumber scales font + life by hit magnitude (vs HitStopRefDamage) so heavy hits read.

390/390 EditMode, clean compile + Play (no exceptions; per-frame footstep/rumble-tick paths verified). On-screen
feel is the operator's eyes. Deferred (focused follow-ups): true material body hit-flash (ShaderGraph), co-op
remote-swing rendering, near-impact telegraph beep (clip reserved).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 00:09:07 -07:00

55 lines
2.1 KiB
C#

using UnityEngine;
using UnityEngine.InputSystem;
namespace ProjectM.Client
{
/// <summary>
/// Gamepad rumble for combat feel — a static bridge (mirrors <see cref="FeelConfig"/> / <see cref="AimPresentation"/>).
/// <see cref="Pulse"/> sets the motors and stamps a stop time; <see cref="Tick"/> (called once per frame from
/// <c>CombatFeedbackSystem</c>) stops them when the pulse elapses OR the app loses focus, so a rumble never sticks.
/// A no-op when no pad is connected; the CALLER gates to the Gamepad scheme. Statics survive fast-enter-playmode
/// reloads, so <see cref="ResetState"/> re-arms clean on play-enter and stops any leaked motor (the AimPresentation
/// reset idiom). Presentation-only, main-thread, never touches the simulation.
/// </summary>
public static class RumbleUtil
{
static float s_StopTime;
static bool s_Active;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
static void ResetState()
{
s_Active = false;
s_StopTime = 0f;
Stop();
}
/// <summary>Pulse both motors at the given low/high strengths for durSec, then auto-stop. No-op without a pad.</summary>
public static void Pulse(float low, float high, float durSec)
{
var pad = Gamepad.current;
if (pad == null) return;
pad.SetMotorSpeeds(Mathf.Clamp01(low), Mathf.Clamp01(high));
s_StopTime = Time.unscaledTime + Mathf.Max(0.02f, durSec);
s_Active = true;
}
/// <summary>Call once per frame: stops the motors when the pulse elapses or focus is lost.</summary>
public static void Tick()
{
if (!s_Active) return;
if (!Application.isFocused || Time.unscaledTime >= s_StopTime)
{
Stop();
s_Active = false;
}
}
static void Stop()
{
var pad = Gamepad.current;
if (pad != null) pad.ResetHaptics();
}
}
}