diff --git a/Assets/_Project/Scripts/Client/Presentation/FeelConfig.cs b/Assets/_Project/Scripts/Client/Presentation/FeelConfig.cs index 89e15ae0c..0b53b8165 100644 --- a/Assets/_Project/Scripts/Client/Presentation/FeelConfig.cs +++ b/Assets/_Project/Scripts/Client/Presentation/FeelConfig.cs @@ -39,14 +39,10 @@ namespace ProjectM.Client public static float HitStopFovKickMin; /// Max FOV kick (deg) on a heavy player-dealt hit (delta >= HitStopRefDamage). public static float HitStopFovKickMax; - /// Reserved cap for the (deferred) true freeze-frame, in fixed frames. - public static int HitStopMaxFrames; /// Damage delta that saturates the player-dealt punch to HitStopFovKickMax. public static float HitStopRefDamage; /// Tint for the (deferred) enemy material hit-flash — exposed now, wired in the ShaderGraph slice. public static Color HitFlashColor; - /// Master gate for the (deferred) true freeze-frame hit-stop. FALSE for v1 (camera-punch only). - public static bool HitStopFreezeEnabled; // ---- Feature 1/2: death camera punch ---- /// Camera shake on LOCAL player death (loudest event by design). @@ -170,8 +166,6 @@ namespace ProjectM.Client public static float MeleeArcIntensity; /// Bubbles shed along a melee cleave (0 = off). public static int MeleeArcBubbles; - /// Camera hold frames when the melee FINISHER lands (07-20 G5 impact ladder; light hits use HitStopMaxFrames). - public static int FinisherHoldFrames; // ---- 07-21 enemy hit-reacts (G5; the Rukhanka-safe flinch that replaced the cut vibrate) ---- /// Seconds the light flinch pulse holds (0 = hit-reacts OFF). @@ -252,10 +246,8 @@ namespace ProjectM.Client HitStopDurationMs = 90f; HitStopFovKickMin = 0.6f; HitStopFovKickMax = 2.2f; - HitStopMaxFrames = 2; // C4: a 2-frame (~33ms) camera hold reads as crunch, not a lag-y stutter HitStopRefDamage = 30f; HitFlashColor = new Color(1f, 0.85f, 0.55f, 1f); - HitStopFreezeEnabled = true; // C4: enable the finisher hit-stop hold (presentation-only camera freeze, bounded below) // Feature 1/2 death PlayerDeathShake = 0.50f; @@ -318,7 +310,6 @@ namespace ProjectM.Client CombatIdleHoldSec = 4f; // 07-18: Menacing01 combat-idle hold after the last swing MeleeArcIntensity = 0.8f; // 07-21 HEAVY LOCK: the weapon is the read, the arc recedes further MeleeArcBubbles = 6; - FinisherHoldFrames = 7; // 07-21 HEAVY LOCK: a fatter finisher beat (~117ms, still under the 8-frame ladder cap) HitReactSeconds = 0.3f; // 07-21: light flinch pulse (clip plays at 1.4x -> the peak lands inside it) HitStaggerSeconds = 0.55f; // heavy stagger pulse HitReactStaggerDamage = 50f; // finisher (63 seeded) staggers, light (42) flinches -- tracks B2 poise diff --git a/Assets/_Project/Scripts/Client/Presentation/PrototypeCameraRig.cs b/Assets/_Project/Scripts/Client/Presentation/PrototypeCameraRig.cs index 6cc4eccb6..c3070e929 100644 --- a/Assets/_Project/Scripts/Client/Presentation/PrototypeCameraRig.cs +++ b/Assets/_Project/Scripts/Client/Presentation/PrototypeCameraRig.cs @@ -58,10 +58,6 @@ namespace ProjectM.Client s_fovLambda = 3f / durSec; // ~95% decayed after durSec (3 time constants) } - /// C4 hit-stop: hold the follow camera for a couple frames on a heavy hit (the combo finisher) so the - /// impact lands with a beat of crunch. Presentation only — NEVER Time.timeScale (the deterministic sim keeps ticking). - static int s_holdFrames; - public static void Hold(int frames) { if (frames > s_holdFrames) s_holdFrames = frames; } [Header("Angle (degrees)")] @@ -95,6 +91,11 @@ namespace ProjectM.Client Camera _cam; Vector3 _leadOffset; // smoothed look-ahead offset (world units), eased toward the desired lead each frame + // The SMOOTHED follow position, kept free of shake. Reading the shaken transform back into the follow + // filter is what made the camera wander in combat (see LateUpdate). Seeded from the transform on the + // first frame so a scene-authored camera pose is honoured rather than snapped from the origin. + Vector3 _basePos; + bool _baseInit; // 07-16c DEV ZOOM (LoL/SoD-style scroll-to-inspect; may not ship): the wheel moves a smoothed // distance target between ZoomMinDistance and the rig's authored Distance — zoom IN only, scroll @@ -156,15 +157,32 @@ namespace ProjectM.Client // uninitialized -> no change). Multiplies the serialized FollowSharpness so scenes stay untouched. float dragSharp = FollowSharpness * Mathf.Max(0.05f, FeelConfig.CameraDragMult <= 0f ? 1f : FeelConfig.CameraDragMult); float k = FollowSharpness <= 0f ? 1f : 1f - Mathf.Exp(-dragSharp * Time.deltaTime); - Vector3 basePos; - if (s_holdFrames > 0) { s_holdFrames--; basePos = transform.position; } // C4: brief hit-stop hold (freeze the follow a couple frames) - else basePos = Vector3.Lerp(transform.position, desired, k); + // 2026-08-13, TWO fixes here — they are the whole "combat freezes on a kill / doesn't feel smooth" bug. + // + // (1) SHAKE IS A TRANSIENT OFFSET, NOT PART OF THE FOLLOW STATE. The smoothing used to run as + // Lerp(transform.position, desired, k) — and transform.position already contained LAST frame's + // random shake, so the filter saw shake as real positional error and integrated it, correcting + // only ~9 %/frame (FollowSharpness 8 at ~12 ms). Shake was never subtracted, only slowly lerped + // out while new shake was added on top, so in sustained combat the camera random-walked around + // its framing and never settled. Measured on a STATIONARY player (ideal motion = 0): one kill + // left it 0.45 units off-frame and still 0.20 off 24 frames later. The follow now smooths + // _basePos, which shake never touches; shake is added only when writing the transform. + // + // (2) The camera position-HOLD is gone. It froze the follow for a fixed number of RENDER frames on + // every kill/heavy hit, so the freeze length was framerate-dependent (7 frames = 49 ms at + // 144 fps, 233 ms at 30 fps), and it took its base from transform.position, permanently baking + // that frame's shake in — a measured 0.28-unit single-frame jump. A camera that stops while the + // world keeps moving reads as a hitch, not as crunch; impact rides the FOV punch + shake alone. + if (!_baseInit) { _basePos = transform.position; _baseInit = true; } + _basePos = Vector3.Lerp(_basePos, desired, k); + + Vector3 writePos = _basePos; if (s_shake > 0.0001f) { - basePos += UnityEngine.Random.insideUnitSphere * s_shake; + writePos += UnityEngine.Random.insideUnitSphere * s_shake; s_shake = Mathf.Lerp(s_shake, 0f, 1f - Mathf.Exp(-12f * Time.deltaTime)); } - transform.SetPositionAndRotation(basePos, rot); + transform.SetPositionAndRotation(writePos, rot); } } diff --git a/Assets/_Project/Tuning/Profiles/baseline-0720.json b/Assets/_Project/Tuning/Profiles/baseline-0720.json index 41851fba9..085528fce 100644 --- a/Assets/_Project/Tuning/Profiles/baseline-0720.json +++ b/Assets/_Project/Tuning/Profiles/baseline-0720.json @@ -15,7 +15,6 @@ "feel": [ { "k": "MeleeArcIntensity", "v": "0.8" }, { "k": "MeleeArcBubbles", "v": "6" }, - { "k": "FinisherHoldFrames", "v": "7" }, { "k": "MeleeConnectFovKick", "v": "1.1" }, { "k": "CombatIdleHoldSec", "v": "4" } ] diff --git a/Assets/_Project/Tuning/Profiles/heavy-committed.json b/Assets/_Project/Tuning/Profiles/heavy-committed.json index c73d0a69d..4677edc27 100644 --- a/Assets/_Project/Tuning/Profiles/heavy-committed.json +++ b/Assets/_Project/Tuning/Profiles/heavy-committed.json @@ -32,10 +32,6 @@ "k": "MeleeArcIntensity", "v": "0.8" }, - { - "k": "FinisherHoldFrames", - "v": "7" - }, { "k": "MeleeConnectFovKick", "v": "1.1" diff --git a/Assets/_Project/Tuning/Profiles/snappier.json b/Assets/_Project/Tuning/Profiles/snappier.json index 5ebcfdddc..042553eda 100644 --- a/Assets/_Project/Tuning/Profiles/snappier.json +++ b/Assets/_Project/Tuning/Profiles/snappier.json @@ -11,7 +11,6 @@ ], "feel": [ { "k": "MeleeArcIntensity", "v": "1.1" }, - { "k": "FinisherHoldFrames", "v": "4" }, { "k": "MeleeConnectFovKick", "v": "0.7" } ] }