Add conversion statues shown in resolution phase
In Conversion mode, capture a snapshot (pose index + paint) of each hider at conversion time and materialize a frozen, painted statue at the found spot for the resolution phase, independent of the now-hunter avatar. - MatchStatue.cs (new): server-init pose/paint, network-driven pose then freeze, reveal-gate (hidden until paint applied), kills inherited sim. - PlayerPaintSync: _isStatue (sibling auto-detect) suppresses the three auto-behaviours so MatchStatue drives the paint flow deterministically. - MatchManager: statuePrefab; capture in ConvertToHunter; spawn on resolution enter; despawn + record clear on EndMatch/teardown/start. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
0a1e286a34
commit
0f108fce47
4 files changed
+232
-5
No files matched your search
@@ -72,6 +72,12 @@ public class MatchManager : MonoBehaviour
|
||||
"free-cam rig is a separate scene object. Must be a registered network prefab.")]
|
||||
[SerializeField] private GameObject spectatorPrefab;
|
||||
|
||||
[Tooltip("CONVERSION mode only: a frozen, painted 'statue' spawned at the start of Resolution at the exact " +
|
||||
"spot / stance / painting each hider held the moment it was converted into a hunter, so everyone " +
|
||||
"sees where each hider was caught. Purely presentational (no controller / collision / role). Must " +
|
||||
"be a registered network prefab (a stripped Hider variant). Empty = statues disabled.")]
|
||||
[SerializeField] private GameObject statuePrefab;
|
||||
|
||||
[Header("Roles")]
|
||||
[Tooltip("How many randomly chosen players become Hunters at match start; everyone else is a Hider. " +
|
||||
"0 = all Hiders; a value at or above the player count makes everyone a Hunter. Default 1.")]
|
||||
@@ -279,6 +285,13 @@ 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).
|
||||
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<NetworkObject> _statues = new List<NetworkObject>();
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance && Instance != this) { Destroy(this); return; }
|
||||
@@ -424,6 +437,9 @@ public class MatchManager : MonoBehaviour
|
||||
_matchAvatarIds.Clear();
|
||||
_loadTargetIds.Clear();
|
||||
_clientLoadPoll = null;
|
||||
// Teardown unloads the gameplay scenes (destroys the statue objects); just drop our references.
|
||||
_statues.Clear();
|
||||
_conversionRecords.Clear();
|
||||
if (_net) UnhookSessionCallbacks();
|
||||
|
||||
ReturnToMainOnly();
|
||||
@@ -613,6 +629,8 @@ public class MatchManager : MonoBehaviour
|
||||
|
||||
private IEnumerator MatchSequence(HashSet<ulong> hunterIds)
|
||||
{
|
||||
_conversionRecords.Clear(); // fresh match inherits no stale statue captures (EndMatch also clears)
|
||||
|
||||
// Freeze everyone in Loading BEFORE unloading the lobby: the lobby floor vanishes a moment before the
|
||||
// map's is loaded, so the still-bound lobby avatars would otherwise drop into the void during the swap.
|
||||
// Loading gates movement, gravity AND the local own-body mesh on every client (see GameModeManager); the
|
||||
@@ -653,6 +671,9 @@ public class MatchManager : MonoBehaviour
|
||||
yield return RunSeekPhase(seekDuration);
|
||||
|
||||
SetPhaseServer(MatchSubPhase.Resolution, resolveDuration);
|
||||
// Conversion mode: materialize a frozen statue of every converted hider at its capture spot, now that
|
||||
// Resolution (which reveals everyone) has begun. No-op with no records / no statue prefab / in Classic.
|
||||
SpawnConversionStatues();
|
||||
yield return WaitPhase(resolveDuration, null);
|
||||
|
||||
_phaseRoutine = null;
|
||||
@@ -1121,6 +1142,9 @@ 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);
|
||||
current.Despawn(true);
|
||||
|
||||
var go = Instantiate(hunterPrefab, pos, rot);
|
||||
@@ -1131,6 +1155,63 @@ 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)
|
||||
{
|
||||
if (!statuePrefab) return;
|
||||
|
||||
int poseIndex = 0;
|
||||
var pose = hider.GetComponentInChildren<PlayerPoseController>(true);
|
||||
if (pose) poseIndex = pose.CurrentIndex;
|
||||
|
||||
byte[] paint = null;
|
||||
var painter = hider.GetComponentInChildren<MeshPainter>(true);
|
||||
if (painter) painter.TryCaptureSnapshot(out paint);
|
||||
|
||||
_conversionRecords.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()
|
||||
{
|
||||
if (!statuePrefab || _conversionRecords.Count == 0) return;
|
||||
|
||||
foreach (var rec in _conversionRecords)
|
||||
{
|
||||
var go = Instantiate(statuePrefab, rec.pos, rec.rot);
|
||||
var statue = go.GetComponent<MatchStatue>();
|
||||
if (statue) statue.ServerInit(rec.poseIndex, rec.paint);
|
||||
var netObj = go.GetComponent<NetworkObject>();
|
||||
if (netObj)
|
||||
{
|
||||
netObj.Spawn(); // server-owned, non-player: replicates to every client as an observer
|
||||
_statues.Add(netObj);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("[MatchManager] Statue prefab has no NetworkObject - cannot spawn.", this);
|
||||
Destroy(go);
|
||||
}
|
||||
}
|
||||
_conversionRecords.Clear();
|
||||
}
|
||||
|
||||
// Server-only. Despawns every live conversion statue (match ending / back to lobby) and drops the list.
|
||||
private void DespawnConversionStatues()
|
||||
{
|
||||
for (int i = 0; i < _statues.Count; i++)
|
||||
{
|
||||
var s = _statues[i];
|
||||
if (s && s.IsSpawned) s.Despawn(true);
|
||||
}
|
||||
_statues.Clear();
|
||||
}
|
||||
|
||||
// True if the client's current avatar is a Hunter role variant (used to gate shooting server-side).
|
||||
private bool IsHunter(ulong clientId)
|
||||
{
|
||||
@@ -1150,6 +1231,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
|
||||
SetPhaseServer(MatchSubPhase.None, 0f);
|
||||
|
||||
Phase = MatchPhase.Transitioning;
|
||||
|
||||
Reference in new issue
Block a user