Sweep over 48 files (Editor/ excluded): remove comments that restate the code, condense class summaries, fix three stale doc statements (HsvColorPicker alpha bar, PaintHud layoutPrefab, MasterVolume FallbackDefault). Inline two single-use wrappers (IsPointerOverUi, BuildModes), drop an unused using, replace a magic literal with the existing ParallaxBase constant, and convert ~10 UnityEngine.Object null checks to the implicit-bool convention. No serialized fields, signatures, RPC/NetworkVariable names, defaults, or #if blocks touched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
162 lines
8.1 KiB
C#
162 lines
8.1 KiB
C#
using System.Collections.Generic;
|
|
using Unity.Netcode;
|
|
using UnityEngine;
|
|
using Chameleon.Components;
|
|
|
|
/// <summary>
|
|
/// A frozen, painted "statue" of a hider at the exact spot / stance / painting it held the moment it was
|
|
/// CONVERTED into a hunter (Conversion mode only); <see cref="MatchManager"/> captures those on the
|
|
/// conversion and spawns one statue per converted hider at Resolution start. Purely presentational (no
|
|
/// controller / collision / role) and a dedicated prefab, NOT a <see cref="NetworkPlayerSetup"/> 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 <see cref="PlayerPaintSync"/>
|
|
/// 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).
|
|
/// </summary>
|
|
[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.SpawnConversionStatues, 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<int> _poseIndex =
|
|
new NetworkVariable<int>(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
|
|
|
|
/// <summary>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.</summary>
|
|
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<MeshPainter>(true);
|
|
_paintSync = GetComponentInChildren<PlayerPaintSync>(true);
|
|
_pose = GetComponentInChildren<PlayerPoseController>(true);
|
|
_bodyRenderers = GetComponentsInChildren<SkinnedMeshRenderer>(true);
|
|
_furRenderers = GetComponentsInChildren<FurShellRenderer>(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<PlayerCharacterController>();
|
|
if (controller) controller.enabled = false;
|
|
var cc = GetComponent<CharacterController>();
|
|
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<MatchStatue> _active = new List<MatchStatue>();
|
|
|
|
/// <summary>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.</summary>
|
|
public static IReadOnlyList<MatchStatue> 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;
|
|
|
|
/// <summary>This statue's body collider (the paint MeshCollider), or null until it has been created.</summary>
|
|
public MeshCollider BodyCollider
|
|
{
|
|
get
|
|
{
|
|
if (!_bodyCollider) _bodyCollider = GetComponentInChildren<MeshCollider>(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;
|
|
}
|
|
}
|