From ff034870fc35d77182a9295282c5616124ebd793 Mon Sep 17 00:00:00 2001 From: MelekTaus Moribundus Date: Wed, 1 Jul 2026 21:52:46 +0200 Subject: [PATCH] Freeze hiders (pose + transform) during the resolution phase While a match is in the resolution sub-phase, hiders that have not been found hold their current pose and position: the pose graph is frozen (mixer speed 0, current frame re-asserted) and owner input is disabled while gravity/grounding keep them planted. Co-Authored-By: Claude Opus 4.8 --- Assets/Scripts/NetworkPlayerSetup.cs | 36 ++++++++++++++++++++++++++ Assets/Scripts/PlayerPoseController.cs | 23 ++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/Assets/Scripts/NetworkPlayerSetup.cs b/Assets/Scripts/NetworkPlayerSetup.cs index f9f497b..d4067ea 100644 --- a/Assets/Scripts/NetworkPlayerSetup.cs +++ b/Assets/Scripts/NetworkPlayerSetup.cs @@ -115,6 +115,8 @@ public class NetworkPlayerSetup : NetworkBehaviour private ConcealmentMeter _concealment; // owner-only embedded-percent source (disabled on remotes) private bool _xrayApplied; private bool _lastXRay; + private bool _frozenApplied; + private bool _lastFrozen; public override void OnNetworkSpawn() { @@ -238,6 +240,40 @@ public class NetworkPlayerSetup : NetworkBehaviour UpdateVisibility(); UpdateEmbeddedReveal(); UpdateXRayReveal(); + UpdateResolutionFreeze(); + } + + // Per-client. During the resolution phase every still-in-play hider is frozen in place on every machine: + // its pose graph holds the current frame (PlayerPoseController.Frozen) and, on the owning client, its + // movement input is cut (ControlEnabled) so the owner-authoritative transform stops and replicates as + // frozen. Mirrors the resolution X-ray reveal predicate. The pose flag is change-guarded (toggled only on + // a transition); the owner's ControlEnabled is re-asserted every frozen frame so a stray mode toggle can't + // quietly re-enable movement during the reveal. Nothing is restored — EndMatch despawns this avatar. + private void UpdateResolutionFreeze() + { + bool freeze = ComputeResolutionFreeze(); + + if (freeze && IsOwner) + { + var controller = GetComponent(); + if (controller) controller.ControlEnabled = false; + } + + if (_frozenApplied && freeze == _lastFrozen) return; + _lastFrozen = freeze; + _frozenApplied = true; + + var pose = GetComponent(); + if (pose) pose.Frozen = freeze; + } + + // True while THIS avatar is a still-in-play hider during the local resolution phase (the reveal window). + private bool ComputeResolutionFreeze() + { + var match = MatchManager.Instance; + return match && match.MatchActive && + match.LocalSubPhase == MatchManager.MatchSubPhase.Resolution && + role == PlayerRole.Hider && !_isFound.Value; } // Per-client visibility: each machine decides whether to render THIS avatar's body from the LOCAL diff --git a/Assets/Scripts/PlayerPoseController.cs b/Assets/Scripts/PlayerPoseController.cs index 77772cf..fc1e7eb 100644 --- a/Assets/Scripts/PlayerPoseController.cs +++ b/Assets/Scripts/PlayerPoseController.cs @@ -156,6 +156,12 @@ public class PlayerPoseController : MonoBehaviour /// Eased per-pose y-offset for the first-person eye, applied on top of the camera target by the FirstPersonController. public float FirstPersonCameraYOffset => _firstPersonCameraOffset; + /// Holds the avatar on its current pose frame: the Playable graph stops advancing (its bones stay + /// exactly where they are) and all per-frame locomotion / rest-reset / capsule / camera work is skipped. + /// Set per-client for every still-in-play hider during the resolution reveal so nobody can wiggle in place. + /// Clearing it resumes normal playback. Independent of network drive — works on the owner and on remotes. + public bool Frozen { get; set; } + void Awake() { _player = GetComponent(); @@ -169,6 +175,23 @@ public class PlayerPoseController : MonoBehaviour void Update() { + // Resolution freeze: hold the currently-evaluated pose. Keep the graph playing (so it re-asserts the + // frozen frame every render, even under Animator culling) but zero the mixer's speed so clip time + // stops advancing, and skip everything below so no locomotion/rest-reset moves a bone. The per-clip + // speeds PlayPose writes onto the mixer INPUTS are unaffected; only the mixer's own multiplier is + // zeroed here and restored on the first un-frozen frame just below. + if (Frozen) + { + if (_graph.IsValid()) + { + if (!_graph.IsPlaying()) _graph.Play(); + if (_mixer.IsValid()) PlayableExtensions.SetSpeed(_mixer, 0d); + } + return; + } + if (_mixer.IsValid() && PlayableExtensions.GetSpeed(_mixer) == 0d) + PlayableExtensions.SetSpeed(_mixer, 1d); + // Local control: the cycle key and the sprint reset choose the pose. A network-driven (remote) // avatar skips this — its pose index is pushed in from the owner via SetNetworkState. if (!_networkControlled)