using System; using UnityEngine; using TD.Core; namespace TD.Levels { /// /// Baked level data. Authored in a Unity scene via volume MonoBehaviours; produced by /// ; consumed at runtime as the canonical map /// definition. /// /// /// LevelData is the canonical map identity. The lobby browses LevelData assets; loading a /// map = read LevelData → load the referenced scene at . /// /// All flat grid arrays use row-major indexing: grid[y * GridSize.x + x]. /// Origin-relative: is the world-tile coordinate that grid index /// (0,0) corresponds to. Convert world-tile to grid-index via /// worldTile - GridOriginTile. /// [CreateAssetMenu(fileName = "LevelData", menuName = "TD/Level Data", order = 1)] public class LevelData : ScriptableObject { // ------------------------------------------------------------------- // Lobby-facing metadata. // ------------------------------------------------------------------- [Header("Map Metadata")] public string MapName; public int PlayerCount; public string MapDescription; public string Author; [Tooltip("Auto-generated by bake. Use the Refresh Thumbnail button on LevelAuthoring to " + "re-render without doing a full bake.")] public Sprite MapThumbnail; [Tooltip("Path to the scene this LevelData was baked from. Auto-populated by bake.")] public string ScenePath; /// /// Just the scene name (no path, no extension) — what NetworkManager.SceneManager.LoadScene /// expects. Derived from ; returns empty string if ScenePath is unset. /// public string SceneName => string.IsNullOrEmpty(ScenePath) ? string.Empty : System.IO.Path.GetFileNameWithoutExtension(ScenePath); // ------------------------------------------------------------------- // Bake metadata (used for dirty-detection and diagnostic display). // ------------------------------------------------------------------- [Header("Bake Metadata")] [Tooltip("Hash of the authoring inputs at last bake. Compared at Play-mode entry to detect " + "unbaked changes. Captures volumes + LevelAuthoring metadata; does NOT capture " + "visual scene content.")] public string AuthoringHash; [Tooltip("ISO 8601 UTC timestamp of the last successful bake.")] public string LastBakeTimestamp; public BakeOutcome LastBakeOutcome; public int LastBakeWarningCount; // ------------------------------------------------------------------- // Grid metadata. // ------------------------------------------------------------------- [Header("Grid")] [Tooltip("World-tile coordinate that grid index (0,0) corresponds to. Origin-relative " + "addressing: worldTile - GridOriginTile gives the index into the flat arrays.")] public Vector2Int GridOriginTile; [Tooltip("Width × height of the grid in tiles.")] public Vector2Int GridSize; // ------------------------------------------------------------------- // Per-tile flat arrays. All indexed as grid[y * GridSize.x + x]. // ------------------------------------------------------------------- [Tooltip("Per-tile placement state: Outside / Buildable / Restricted. Length = GridSize.x * GridSize.y.")] public PlacementState[] PlacementGrid; [Tooltip("Per-tile initial walkability (does NOT account for towers — they stamp footprints " + "at runtime). Length = GridSize.x * GridSize.y.")] public bool[] WalkabilityGrid; [Tooltip("Per-tile owning player slot. PlayerSlot.None for tiles not in any player zone. " + "Length = GridSize.x * GridSize.y.")] public PlayerSlot[] OwnerGrid; [Tooltip("Per-tile map-area membership. True if the tile is part of the playable map " + "(inside any MapAreaVolume); false for void tiles outside the map. This is the " + "outermost gating layer — gameplay queries (placement, ownership, walkability) " + "are only meaningful for tiles where MapAreaGrid is true. Length = GridSize.x * " + "GridSize.y.")] public bool[] MapAreaGrid; // ------------------------------------------------------------------- // Per-zone and per-goal structures (populated by bake from volume data). // ------------------------------------------------------------------- [Tooltip("One entry per declared player zone, sorted by Owner.")] public PlayerZoneData[] PlayerZones; [Tooltip("One entry per GoalVolume, sorted by min tile coordinate.")] public GoalData[] Goals; } /// /// Per-zone baked data. The runtime can use this to enumerate a player's spawners and leak /// exits without scanning the full grid. /// [Serializable] public class PlayerZoneData { public PlayerSlot Owner; [Tooltip("Spawners in this zone, sorted by SpawnerIdInZone.")] public SpawnerData[] Spawners; [Tooltip("Leak exits OUT OF this zone, sorted by Target enum value. Empty for the final " + "defender (whose zone is goal-adjacent and has no leak exit).")] public LeakExitData[] LeakExits; } [Serializable] public class SpawnerData { public int SpawnerIdInZone; [Tooltip("Tile coordinate of the spawner's center (its volume's bounds center, projected to grid).")] public Vector2Int TilePosition; [Tooltip("Full tile coverage of the spawner volume.")] public Vector2Int[] TileArea; public Direction Facing; } [Serializable] public class LeakExitData { public PlayerSlot Target; [Tooltip("Full tile coverage of the leak exit volume.")] public Vector2Int[] TileArea; [Tooltip("Weight normalized so all leak exits sharing the same source zone sum to 1.0.")] public float NormalizedWeight; } [Serializable] public class GoalData { [Tooltip("Full tile coverage of the goal volume.")] public Vector2Int[] TileArea; } }