using System.Collections.Generic; using Unity.Netcode; using UnityEngine; using Chameleon.Components; /// /// A frozen, painted "statue" of a hider at the exact spot / stance / painting it held the moment it was /// hit (Conversion: converted into a hunter; Classic: marked found); captures /// those on the hit and spawns one statue per hit hider at Resolution start. Purely presentational (no /// controller / collision / role) and a dedicated prefab, NOT a avatar - /// so it never pollutes the scoreboard, per-client visibility matrix or resolution X-ray reveal. /// /// Server-owned; three replicated ingredients rebuild it identically on every client: transform /// (server-authoritative NetworkTransform), painting (rides the existing /// snapshot transfer, pushed explicitly by the server - PlayerPaintSync suppresses its own auto /// pull/publish for a statue receiver so the two never race) and stance (a replicated pose index the /// pose controller reproduces, then freezes). /// [DisallowMultipleComponent] public class MatchStatue : NetworkBehaviour { [Tooltip("Seconds the captured stance is allowed to blend in (as the network-driven idle of that pose) " + "before the rig is frozen on that frame. Keeps the pose from snapping in mid-blend.")] [Min(0f)] [SerializeField] private float poseSettle = 0.35f; // Server-only handoff from MatchManager.SpawnStatues, consumed in OnNetworkSpawn / the first // ready frame. The paint may be null (capture failed) -> the statue stays on the blank seed look. private byte[] _serverPaint; private int _serverPoseIndex; // The captured stance, replicated so every client poses the rig identically (server-written on spawn). private readonly NetworkVariable _poseIndex = new NetworkVariable(0, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server); private MeshPainter _painter; private PlayerPaintSync _paintSync; private PlayerPoseController _pose; private SkinnedMeshRenderer[] _bodyRenderers; private FurShellRenderer[] _furRenderers; private bool _hostPaintDone; // server: the captured snapshot has been loaded + pushed to peers (one-shot) private bool _bodyShown; // reveal-gate change guard private bool _frozen; // pose freeze applied private float _freezeAt; // Time.time deadline that pins the settled pose /// Server-only. Call on a freshly-instantiated statue BEFORE spawning it: the captured painting /// snapshot (may be null) and the stance pose index to reproduce. public void ServerInit(int poseIndex, byte[] paint) { _serverPoseIndex = poseIndex; _serverPaint = paint; } public override void OnNetworkSpawn() { _active.Add(this); // player-collision registry (paired with OnNetworkDespawn) _painter = GetComponentInChildren(true); _paintSync = GetComponentInChildren(true); _pose = GetComponentInChildren(true); _bodyRenderers = GetComponentsInChildren(true); _furRenderers = GetComponentsInChildren(true); // Pure presentation: kill any inherited simulation so nothing moves/falls or fights the held pose. // Done before the disabled controllers' first Update (OnNetworkSpawn runs during the spawn), so no // gravity frame slips through. var controller = GetComponent(); if (controller) controller.enabled = false; var cc = GetComponent(); if (cc) cc.enabled = false; if (IsServer) _poseIndex.Value = _serverPoseIndex; // Hide the body until the painting has actually landed locally, so no client flashes the blank seed // material before its snapshot applies (mirrors the avatar's receiver-side reveal gate). SetBodyVisible(false); // Reproduce the captured stance: network-driven so no local input / capsule / camera work runs; the // idle of that pose plays for a short settle, then Update freezes the rig on that frame. if (_pose) { _pose.EnableNetworkDrive(); _pose.SetNetworkState(_poseIndex.Value, PlayerPoseController.Locomotion.Idle); } _freezeAt = Time.time + Mathf.Max(0f, poseSettle); } public override void OnNetworkDespawn() { _active.Remove(this); } // Statues carry their own runtime paint MeshCollider (MeshPainter bakes it), so the local player's capsule // bumps into them like any body - but they are NOT NetworkPlayerSetup avatars and so aren't in that registry. // Tracked here so GameModeManager's player-collision toggle can let the local player pass through statues // too: a statue is a frozen player representation, so it follows the same "players collide" switch. private static readonly List _active = new List(); /// Read-only view of the statues spawned on this machine. Consumed by the player-collision /// reconciler so the local player passes through statues when player collision is off. public static IReadOnlyList Active => _active; // The runtime body MeshCollider (baked skinned mesh) the local capsule collides with. MeshPainter creates // it in Start - a frame after OnNetworkSpawn - so resolve it lazily and cache once it exists. private MeshCollider _bodyCollider; /// This statue's body collider (the paint MeshCollider), or null until it has been created. public MeshCollider BodyCollider { get { if (!_bodyCollider) _bodyCollider = GetComponentInChildren(true); return _bodyCollider; } } private void Update() { // Server owner: once the painter is set up, load the captured snapshot and push it to every peer over // the existing PlayerPaintSync transfer. Always publish a baseline (even a null capture -> the blank // seed) so a client's receiver-side gate opens and the body can reveal. One-shot. if (IsServer && !_hostPaintDone && _painter && _painter.IsPaintReady && _paintSync) { _hostPaintDone = true; if (_serverPaint != null) _painter.LoadPaintFromSnapshot(_serverPaint); _paintSync.BroadcastLoadedPaint(); _serverPaint = null; } // Reveal once the painting is applied locally: the server right after its load/publish, a client once // its received snapshot has caught up to the published baseline. bool paintReady = IsServer ? _hostPaintDone : (_paintSync && _paintSync.PaintRevealReady); if (paintReady && !_bodyShown) { SetBodyVisible(true); // See-through-walls X-ray glow, exactly like an un-found hider during Resolution. The statue only // ever exists in the resolution phase, so the reveal is steadily on (no just-shot flash). Raised // together with the body so the authored _XRayColor eases in alongside it (per-client, cosmetic). if (_painter) _painter.XRayRevealed = true; } // Re-assert the stance until it has settled (covers a pose index arriving a frame late), then freeze. if (_pose && !_frozen) { _pose.SetNetworkState(_poseIndex.Value, PlayerPoseController.Locomotion.Idle); if (Time.time >= _freezeAt) { _pose.Frozen = true; _frozen = true; } } } // Toggles the skinned body AND the custom-pass fur together (the fur is not a Renderer, so the skinned // toggle misses it) - same atomic reveal the avatar uses, so a statue never shows stale pooled fur. private void SetBodyVisible(bool visible) { _bodyShown = visible; if (_bodyRenderers != null) foreach (var r in _bodyRenderers) if (r) r.enabled = visible; if (_furRenderers != null) foreach (var f in _furRenderers) if (f) f.enabled = visible; } }