// Assets/_Project/Scripts/Gameplay/EnemyUpgrades/EnemyUpgradePool.cs using System.Collections.Generic; using UnityEngine; namespace TD.Gameplay.EnemyUpgrades { /// /// Scene singleton holding every that can appear anywhere in /// this match. The array index is the option's EnemyUpgradeOptionId — the stable /// identifier used to replicate vote offers, ballots, and the upgrades recorded on wave slots. /// /// /// Why a flat pool and per-phase groups. The pool supplies network identity: one /// array, one index per card, identical on every peer. PhaseDefinition.EnemyUpgradeGroups /// supplies gating: which of those cards a given phase may offer. Folding gating into the pool /// would make ids phase-relative and therefore unstable across a phase change, which is /// exactly when replicated slot upgrades are being cleared and rewritten. /// /// Plain MonoBehaviour — identical assets on every peer, so there is nothing to sync. /// The server reads it to generate and resolve votes; clients read it to render vote cards /// from replicated ids. /// public class EnemyUpgradePool : MonoBehaviour { public static EnemyUpgradePool Instance { get; private set; } [Tooltip("Every EnemyUpgradeOption asset that can appear this match. Array index is the " + "id used over the network — reordering mid-development is fine, but not between " + "a host and its clients.")] [SerializeField] private EnemyUpgradeOption[] options; private void Awake() { if (Instance != null && Instance != this) { Debug.LogError("[EnemyUpgradePool] Multiple instances detected. Only one per scene."); return; } Instance = this; } private void OnDestroy() { if (Instance == this) Instance = null; } /// Number of options in the pool. public int Count => options?.Length ?? 0; /// Returns the option at , or null if out of range. public EnemyUpgradeOption Get(int id) => (options != null && id >= 0 && id < options.Length) ? options[id] : null; /// Resolves an option asset back to its pool id, or -1 if it isn't in the pool. public int GetId(EnemyUpgradeOption option) { if (options == null || option == null) return -1; for (int i = 0; i < options.Length; i++) if (options[i] == option) return i; return -1; } /// /// Collects the pool ids of every card the given phase groups allow. Cards missing from /// the pool are reported once and skipped — an authored group referencing an unpooled /// card is a wiring mistake that would otherwise silently shrink the vote. /// public void CollectAllowedIds(EnemyUpgradeGroup[] phaseGroups, List entryScratch, List intoIds, List intoWeights) { intoIds.Clear(); intoWeights.Clear(); EnemyUpgradeGroup.CollectCandidates(phaseGroups, entryScratch); foreach (var entry in entryScratch) { int id = GetId(entry.Option); if (id < 0) { Debug.LogWarning($"[EnemyUpgradePool] '{entry.Option.name}' is referenced by a " + $"phase's upgrade groups but is not in the pool — it can " + $"never be offered. Add it to the pool's options array."); continue; } intoIds.Add(id); intoWeights.Add(entry.Weight); } } } }