c3b53cef28
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>
55 lines
2.1 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|