Spawn hider statues in resolution phase for classic mode too

Classic hit path now captures a statue record like conversion does;
rename the statue infrastructure (CaptureStatue/SpawnStatues/
DespawnStatues/_statueRecords) since it is no longer conversion-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Fable 5 committed 2026-07-10 22:37:11 +02:00
1 parent 14015d7134
commit a2a0e85831
2 files changed
+34 -30

No files matched your search

+31 -27
View File
@@ -321,11 +321,11 @@ public class MatchManager : MonoBehaviour
private int _loadGenerationClient;
private Coroutine _clientLoadPoll;
// Server-only. Conversion statues: a snapshot of each converted hider (spot / stance / painting) captured
// on the conversion, materialized as frozen statue objects only at the start of Resolution and despawned
// when the match ends. Only populated in Conversion mode (ConvertToHunter is the sole capture site).
// Server-only. Statues: a snapshot of each hit hider (spot / stance / painting) captured on the hit
// (Conversion convert / Classic found), materialized as frozen statue objects only at the start of
// Resolution and despawned when the match ends.
private class StatueRecord { public Vector3 pos; public Quaternion rot; public int poseIndex; public byte[] paint; }
private readonly List<StatueRecord> _conversionRecords = new List<StatueRecord>();
private readonly List<StatueRecord> _statueRecords = new List<StatueRecord>();
private readonly List<NetworkObject> _statues = new List<NetworkObject>();
// Server-only. Per-participant outcome records for the running match, keyed by clientId. Created on match
@@ -496,7 +496,7 @@ public class MatchManager : MonoBehaviour
_clientLoadPoll = null;
// Teardown unloads the gameplay scenes (destroys the statue objects); just drop our references.
_statues.Clear();
_conversionRecords.Clear();
_statueRecords.Clear();
_matchRecords.Clear();
if (_net) UnhookSessionCallbacks();
@@ -717,7 +717,7 @@ public class MatchManager : MonoBehaviour
private IEnumerator MatchSequence(HashSet<ulong> hunterIds)
{
_conversionRecords.Clear(); // fresh match inherits no stale statue captures (EndMatch also clears)
_statueRecords.Clear(); // fresh match inherits no stale statue captures (EndMatch also clears)
// One outcome record per participant. Survived is the pending-hider default (flips on a hit /
// disconnect); names are resolved later - the freshly respawned avatars' name NetworkVariables
@@ -774,14 +774,14 @@ public class MatchManager : MonoBehaviour
// (which was already live during the match).
FinalizeMatchResults();
// Conversion mode: materialize a frozen statue of every converted hider at its capture spot. The last
// hider is often still converting (its statue's painting hasn't replicated to every client yet), so
// Materialize a frozen statue of every hit hider (converted / found) at its capture spot. The last
// hider is often still resolving (its statue's painting hasn't replicated to every client yet), so
// hold the Resolution countdown - frozen at 0:00, exactly like the pre-match loading barrier - until
// every client has each statue spawned & painted. Then release the timer. No statue prefab / no
// conversions / Classic -> no hold, the countdown starts immediately (unchanged behaviour).
bool holdForStatues = statuePrefab && _conversionRecords.Count > 0;
// hits -> no hold, the countdown starts immediately (unchanged behaviour).
bool holdForStatues = statuePrefab && _statueRecords.Count > 0;
SetPhaseServer(MatchSubPhase.Resolution, holdForStatues ? 0f : resolveDuration);
SpawnConversionStatues();
SpawnStatues();
if (holdForStatues)
{
yield return RunStatueBarrier();
@@ -1308,6 +1308,10 @@ public class MatchManager : MonoBehaviour
ConvertToHunter(hiderClientId, c.PlayerObject);
else
{
// Classic: snapshot the spot/stance/painting too, so Resolution shows a statue of every
// found hider (the found avatar itself stays hidden - its owner is free-cam spectating).
CaptureStatue(c.PlayerObject.transform.position, c.PlayerObject.transform.rotation,
c.PlayerObject);
var setup = c.PlayerObject.GetComponent<NetworkPlayerSetup>();
if (setup) setup.ServerMarkFound();
}
@@ -1331,8 +1335,8 @@ public class MatchManager : MonoBehaviour
Vector3 pos = current.transform.position;
Quaternion rot = current.transform.rotation;
// Snapshot the hider's spot / stance / painting NOW, before it despawns - a frozen statue is
// materialized from this at the start of Resolution (see SpawnConversionStatues).
CaptureConversionStatue(pos, rot, current);
// materialized from this at the start of Resolution (see SpawnStatues).
CaptureStatue(pos, rot, current);
current.Despawn(true);
var go = Instantiate(hunterPrefab, pos, rot);
@@ -1343,11 +1347,11 @@ public class MatchManager : MonoBehaviour
go.GetComponent<NetworkObject>().SpawnAsPlayerObject(clientId);
}
// Server-only. Captures a converting hider's spot / stance / painting so a frozen statue can be
// materialized from it at Resolution. Skipped entirely when no statue prefab is assigned. The painting
// is captured from the SERVER's own replica (the host holds the full paint via the live stream); a
// capture miss stores a null snapshot -> the statue simply stays on the blank seed look.
private void CaptureConversionStatue(Vector3 pos, Quaternion rot, NetworkObject hider)
// Server-only. Captures a hit hider's spot / stance / painting (Conversion convert AND Classic found) so
// a frozen statue can be materialized from it at Resolution. Skipped entirely when no statue prefab is
// assigned. The painting is captured from the SERVER's own replica (the host holds the full paint via the
// live stream); a capture miss stores a null snapshot -> the statue simply stays on the blank seed look.
private void CaptureStatue(Vector3 pos, Quaternion rot, NetworkObject hider)
{
if (!statuePrefab) return;
@@ -1359,17 +1363,17 @@ public class MatchManager : MonoBehaviour
var painter = hider.GetComponentInChildren<MeshPainter>(true);
if (painter) painter.TryCaptureSnapshot(out paint);
_conversionRecords.Add(new StatueRecord { pos = pos, rot = rot, poseIndex = poseIndex, paint = paint });
_statueRecords.Add(new StatueRecord { pos = pos, rot = rot, poseIndex = poseIndex, paint = paint });
}
// Server-only. Spawns one frozen statue per captured conversion at its recorded pos/rot, handing each the
// stance + painting snapshot before it spawns (server-owned, non-player). Called once at Resolution start;
// the records are consumed here. No-op with no prefab / no records (Classic never records any).
private void SpawnConversionStatues()
// the records are consumed here. No-op with no prefab / no records (no hider was hit).
private void SpawnStatues()
{
if (!statuePrefab || _conversionRecords.Count == 0) return;
if (!statuePrefab || _statueRecords.Count == 0) return;
foreach (var rec in _conversionRecords)
foreach (var rec in _statueRecords)
{
var go = Instantiate(statuePrefab, rec.pos, rec.rot);
var statue = go.GetComponent<MatchStatue>();
@@ -1386,10 +1390,10 @@ public class MatchManager : MonoBehaviour
Destroy(go);
}
}
_conversionRecords.Clear();
_statueRecords.Clear();
}
private void DespawnConversionStatues()
private void DespawnStatues()
{
for (int i = 0; i < _statues.Count; i++)
{
@@ -1575,8 +1579,8 @@ public class MatchManager : MonoBehaviour
// the replicated sub-phase so every client drops its match HUD/visibility before the scene swaps.
if (_phaseRoutine != null) { StopCoroutine(_phaseRoutine); _phaseRoutine = null; }
_pendingHiders.Clear();
DespawnConversionStatues(); // drop any Resolution statues before the map unloads
_conversionRecords.Clear(); // and any captures from a match ended before Resolution
DespawnStatues(); // drop any Resolution statues before the map unloads
_statueRecords.Clear(); // and any captures from a match ended before Resolution
_matchRecords.Clear(); // and the outcome records (the None broadcast clears every client's mirror)
SetPhaseServer(MatchSubPhase.None, 0f);
+3 -3
View File
@@ -5,8 +5,8 @@ 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
/// hit (Conversion: converted into a hunter; Classic: marked found); <see cref="MatchManager"/> 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 <see cref="NetworkPlayerSetup"/> avatar -
/// so it never pollutes the scoreboard, per-client visibility matrix or resolution X-ray reveal.
///
@@ -24,7 +24,7 @@ public class MatchStatue : NetworkBehaviour
[Min(0f)]
[SerializeField] private float poseSettle = 0.35f;
// Server-only handoff from MatchManager.SpawnConversionStatues, consumed in OnNetworkSpawn / the first
// 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;