// Assets/_Project/Scripts/Gameplay/RaceRegistry.cs using System.Collections.Generic; using UnityEngine; using TD.Core; namespace TD.Gameplay { /// /// Persistent (DontDestroyOnLoad) singleton that holds every /// available in the current build and lets /// any code look one up by . /// /// /// Inspector setup. Place ONE RaceRegistry GameObject in /// the MainMenu scene only. Drag every RaceDefinition asset /// into the Definitions array. The registry marks itself /// DontDestroyOnLoad on Awake, so it survives the scene transitions /// MainMenu → Lobby → Match → back to Lobby and is available to all of /// them through . /// /// Why not also in Lobby/Match scenes? Maintaining the /// Definitions array in multiple places is a designer trap — update /// one, forget the other, runtime mismatch. Single source of truth in /// MainMenu eliminates that class of bug. Duplicate instances in other /// scenes are detected in and self-destruct, so an /// accidental copy doesn't break anything but does log a warning. /// /// Editor-only standalone testing. If you open the Lobby or /// Match scene directly from the editor (without going through MainMenu /// first), no RaceRegistry will exist and race-dependent code falls back /// gracefully ( returns null; UI shows "Coming Soon" or /// the default builder is used). For standalone-scene testing, you can /// temporarily add a registry to whatever scene you're testing — but don't /// commit it as part of normal play flow. /// /// Slot model. The lobby grid shows 16 slots (one per /// value 1-16) regardless of how many are filled. /// returns null for unregistered slots, which the UI /// renders as a "Coming Soon" placeholder. /// /// Plain MonoBehaviour. Not a NetworkBehaviour — the registry /// is identical on every peer (same ScriptableObject assets), so nothing /// to sync. Network state tracks only the chosen on /// PlayerMatchState; the rest is local lookup. /// public class RaceRegistry : MonoBehaviour { // ----- Singleton ------------------------------------------------- public static RaceRegistry Instance { get; private set; } // ----- Inspector -------------------------------------------------- [Tooltip("All RaceDefinition assets available in this build. Drag each asset " + "into the array. Duplicate Ids are rejected with a warning; null entries " + "are skipped.")] [SerializeField] private RaceDefinition[] definitions; // ----- Internal lookup ------------------------------------------- private readonly Dictionary byId = new Dictionary(); // ----- Lifecycle -------------------------------------------------- private void Awake() { // Persistent-singleton pattern: the FIRST instance to wake up wins // and survives scene loads. Subsequent instances (e.g. a stale // copy left over in the Lobby or Match scene) are self-destroyed, // not just ignored — we want the scene to "self-heal" if someone // accidentally drops a second copy in. if (Instance != null && Instance != this) { Debug.LogWarning( $"[RaceRegistry] Persistent instance already exists. " + $"Destroying duplicate in scene '{gameObject.scene.name}'. " + $"Keep RaceRegistry in the MainMenu scene only."); Destroy(gameObject); return; } Instance = this; DontDestroyOnLoad(gameObject); BuildLookup(); } private void OnDestroy() { if (Instance == this) Instance = null; } // ----- Public API ------------------------------------------------- /// /// Returns the for the given id, or null /// if no asset is registered for that id (e.g. "Coming Soon" slots). /// public RaceDefinition Get(RaceId id) { byId.TryGetValue(id, out var def); return def; } /// /// Iterates the canonical 16 lobby slots (Race1..Race16). For each /// slot returns either the registered or /// null. UI consumers use this to render a stable 16-cell grid where /// unfilled slots show a placeholder. /// public IEnumerable<(RaceId id, RaceDefinition def)> AllSlots() { for (int i = (int)RaceId.Race1; i <= (int)RaceId.Race16; i++) { var id = (RaceId)i; yield return (id, Get(id)); } } // ----- Private ---------------------------------------------------- private void BuildLookup() { byId.Clear(); if (definitions == null || definitions.Length == 0) { Debug.LogWarning("[RaceRegistry] No RaceDefinition assets assigned. " + "Drag assets into the Definitions array."); return; } foreach (var def in definitions) { if (def == null) continue; if (def.Id == RaceId.None) { Debug.LogWarning($"[RaceRegistry] '{def.name}' has Id=None — set it to " + "an unused Race1..Race16 value."); continue; } if (byId.ContainsKey(def.Id)) { Debug.LogWarning($"[RaceRegistry] Duplicate Id '{def.Id}' detected on " + $"'{def.name}'. Earlier registration kept; rename one."); continue; } byId[def.Id] = def; } Debug.Log($"[RaceRegistry] Registered {byId.Count} race(s)."); } } }