// Assets/_Project/Scripts/Gameplay/Waves/PhaseDefinition.cs using System.Collections.Generic; using UnityEngine; namespace TD.Gameplay.Waves { /// /// One phase of a run: the pool its cycle waves are drawn from, and the pool its boss is /// drawn from. A phase runs the same drawn wave set for every one of its cycles, then ends /// in a boss. /// /// /// Groups, not waves. Both pools are lists of assets rather /// than flat wave lists, so re-balancing a run means dragging a group between phases instead /// of re-authoring individual entries. Listing the same group in two phases is legal — the /// draws are independent. /// /// Draw contract. RunState draws WavesPerCycle waves from /// with distinct enemy types, because enemy upgrades are keyed /// to a wave slot and two slots sharing an enemy type would make "which wave did this buff /// apply to" ambiguous. That makes the real authoring requirement stricter than the entry /// count: a phase needs at least WavesPerCycle distinct enemy types across its groups, /// not just that many waves. exists so the draw can /// fail loudly at match start rather than mid-run. /// [CreateAssetMenu(fileName = "PhaseDefinition", menuName = "TD/Run/Phase Definition", order = 11)] public class PhaseDefinition : ScriptableObject { [Tooltip("Designer-facing name for this phase, shown in validation messages. " + "Falls back to the asset name when empty.")] public string DisplayName; [Tooltip("Groups this phase's cycle waves are drawn from. All groups are pooled " + "together for the draw; group membership is an authoring convenience, not a " + "draw boundary.")] public WaveGroup[] WaveGroups; [Tooltip("Groups this phase's boss is drawn from. A boss is an ordinary WaveDefinition " + "that happens to spawn a single powerful enemy.")] public WaveGroup[] BossGroups; [Tooltip("Enemy-buff cards votable during this phase. Gating is per PHASE, not per cycle " + "— every card listed here can be voted on from this phase's very first wave. " + "Cards must also appear in the scene's EnemyUpgradePool to be offerable.")] public EnemyUpgrades.EnemyUpgradeGroup[] EnemyUpgradeGroups; /// Name used in validation and log messages. Falls back to the asset name. public string Label => string.IsNullOrWhiteSpace(DisplayName) ? name : DisplayName; /// /// Appends every non-null, non-zero-weight entry across into /// . Zero-weight entries are filtered here (not at draw time) so /// "weight 0 means it can never appear" is enforced in exactly one place. /// public static void CollectCandidates(WaveGroup[] groups, List into) { into.Clear(); if (groups == null) return; for (int g = 0; g < groups.Length; g++) { var group = groups[g]; if (group?.Waves == null) continue; for (int i = 0; i < group.Waves.Length; i++) { var entry = group.Waves[i]; if (entry?.Wave == null) continue; if (entry.Weight <= 0f) continue; into.Add(entry); } } } /// Collects this phase's cycle-wave candidates. public void CollectWaveCandidates(List into) => CollectCandidates(WaveGroups, into); /// Collects this phase's boss candidates. public void CollectBossCandidates(List into) => CollectCandidates(BossGroups, into); /// /// How many distinct enemy types are reachable across this phase's wave groups. The /// upper bound on how many wave slots this phase can fill, since the draw requires /// distinct types. /// public int CountDistinctEnemyTypes() { var candidates = new List(); CollectWaveCandidates(candidates); var seen = new HashSet(); foreach (var entry in candidates) { var type = entry.Wave.PrimaryEnemyType; if (type != null) seen.Add(type); } return seen.Count; } } }