diff --git a/Assets/New Terrain.asset b/Assets/New Terrain.asset
new file mode 100644
index 0000000..e41b51b
Binary files /dev/null and b/Assets/New Terrain.asset differ
diff --git a/Assets/New Terrain.asset.meta b/Assets/New Terrain.asset.meta
new file mode 100644
index 0000000..830694d
--- /dev/null
+++ b/Assets/New Terrain.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: e75cb6747dd151148b0f1bf06123e9cf
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 15600000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Levels.meta b/Assets/_Project/Levels.meta
new file mode 100644
index 0000000..08206bb
--- /dev/null
+++ b/Assets/_Project/Levels.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: ae52c3dd5edc3ae4da7d4129059d3637
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Levels/GoalVolume.cs b/Assets/_Project/Levels/GoalVolume.cs
new file mode 100644
index 0000000..086f066
--- /dev/null
+++ b/Assets/_Project/Levels/GoalVolume.cs
@@ -0,0 +1,66 @@
+using UnityEngine;
+using TD.Core;
+
+#if UNITY_EDITOR
+using UnityEditor;
+#endif
+
+namespace TD.Levels
+{
+ ///
+ /// Authoring volume marking the win-condition target. Enemies that reach a goal volume reduce
+ /// the shared player life pool. Goal tiles are in the
+ /// baked grid but remain walkable so enemies can enter them.
+ ///
+ ///
+ /// Goals have no ownership — they are shared across all players. Multiple goal volumes are
+ /// allowed; the designer specifies the expected count via
+ /// and the bake validates the count matches.
+ ///
+ /// Final defenders are identified by their player zone being 4-adjacent to a goal volume's
+ /// tile coverage; they do not have a LeakExitVolume.
+ ///
+ public class GoalVolume : VolumeBase
+ {
+ [Tooltip("Whether tiles in this volume are buildable. Defaults to Invalid.")]
+ public PlacementValidity placementValidity = PlacementValidity.Invalid;
+
+ // Goals draw above player zones to be visually anchoring landmarks. Higher alpha than
+ // other volume types (60% per gizmo design).
+ private const float FillYLevel = 0.04f;
+
+ protected override bool GetAlwaysShowToggle(LevelAuthoring authoring)
+ {
+ return authoring.alwaysShowGoals;
+ }
+
+ private void OnDrawGizmosSelected()
+ {
+ DrawGizmosCore();
+ }
+
+ private void OnDrawGizmos()
+ {
+ if (ShouldDrawAlwaysOn())
+ {
+ DrawGizmosCore();
+ }
+ }
+
+ private void DrawGizmosCore()
+ {
+ Color goalColor = PlayerColors.Goal;
+ DrawTileCoverageFill(goalColor, alpha: 0.60f, yLevel: FillYLevel);
+ DrawRectangularOutline(goalColor, yLevel: FillYLevel);
+
+#if UNITY_EDITOR
+ var col = Collider;
+ if (col != null)
+ {
+ Vector3 labelPos = new Vector3(col.bounds.center.x, FillYLevel + 0.1f, col.bounds.center.z);
+ Handles.Label(labelPos, "GOAL");
+ }
+#endif
+ }
+ }
+}
diff --git a/Assets/_Project/Levels/GoalVolume.cs.meta b/Assets/_Project/Levels/GoalVolume.cs.meta
new file mode 100644
index 0000000..98dcd13
--- /dev/null
+++ b/Assets/_Project/Levels/GoalVolume.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 755948f0244afea4da6ed2e4e3d27579
\ No newline at end of file
diff --git a/Assets/_Project/Levels/LeakExitVolume.cs b/Assets/_Project/Levels/LeakExitVolume.cs
new file mode 100644
index 0000000..93732e0
--- /dev/null
+++ b/Assets/_Project/Levels/LeakExitVolume.cs
@@ -0,0 +1,131 @@
+using UnityEngine;
+using TD.Core;
+
+#if UNITY_EDITOR
+using UnityEditor;
+#endif
+
+namespace TD.Levels
+{
+ ///
+ /// Authoring volume marking the boundary where enemies leak from one player's zone into
+ /// another's. Leak exit tiles are in the baked grid
+ /// but remain walkable.
+ ///
+ ///
+ /// Leak topology is recorded as metadata in the baked for lobby UI
+ /// and future wave-balancing systems, but the runtime gameplay loop does not depend on it —
+ /// enemies pathfind on the unified walkability grid and naturally cross zone boundaries.
+ ///
+ /// Final defenders (the player whose zone is goal-adjacent) do NOT have a LeakExitVolume.
+ /// They are identified by zone-to-goal adjacency at bake time.
+ ///
+ /// Multiple leak exits with the same source zone may exist (e.g., Player 5 in the 9-player
+ /// map splits leaks 50/50 between Player 4 and Player 6). The field
+ /// controls the split. The bake normalizes weights to sum to 1.0 across a source zone's
+ /// leak exits.
+ ///
+ public class LeakExitVolume : VolumeBase
+ {
+ [Tooltip("Which player's zone this leak exits FROM.")]
+ public PlayerSlot sourceZone = PlayerSlot.Player1;
+
+ [Tooltip("Which player's zone this leak feeds INTO.")]
+ public PlayerSlot target = PlayerSlot.Player2;
+
+ [Tooltip("Relative weight for split exits. Bake normalizes weights to sum to 1.0 across all " +
+ "leak exits sharing the same source zone. Set both leaks' weights to 1.0 for a 50/50 split.")]
+ public float weight = 1.0f;
+
+ [Tooltip("Whether tiles in this volume are buildable. Defaults to Invalid.")]
+ public PlacementValidity placementValidity = PlacementValidity.Invalid;
+
+ // Leak exits draw above player zones but below spawners.
+ private const float FillYLevel = 0.04f;
+ private const float ArrowYLevel = 0.05f;
+ private const float ArrowLength = 1.5f;
+
+ protected override bool GetAlwaysShowToggle(LevelAuthoring authoring)
+ {
+ return authoring.alwaysShowLeakExits;
+ }
+
+ private void OnDrawGizmosSelected()
+ {
+ DrawGizmosCore();
+ }
+
+ private void OnDrawGizmos()
+ {
+ if (ShouldDrawAlwaysOn())
+ {
+ DrawGizmosCore();
+ }
+ }
+
+ private void DrawGizmosCore()
+ {
+ // Leak exit fill uses the SOURCE zone's color (visually attaches the leak to the zone
+ // it leaves). The target is conveyed by the arrow direction and label.
+ Color baseColor = PlayerColors.Get(sourceZone);
+ DrawTileCoverageFill(baseColor, alpha: 0.40f, yLevel: FillYLevel);
+ DrawRectangularOutline(baseColor, yLevel: FillYLevel);
+
+ // Arrow points from this leak exit's center toward the target zone's center.
+ var col = Collider;
+ if (col != null)
+ {
+ Vector3 origin = new Vector3(col.bounds.center.x, ArrowYLevel, col.bounds.center.z);
+ Vector3 targetCenter = FindTargetZoneCenter();
+ if (targetCenter.sqrMagnitude > 0f) // sentinel: zero means "not found"
+ {
+ Vector3 directionXZ = new Vector3(targetCenter.x - origin.x, 0f, targetCenter.z - origin.z);
+ DrawArrow(origin, directionXZ, ArrowLength, baseColor);
+ }
+ }
+
+#if UNITY_EDITOR
+ if (col != null)
+ {
+ Vector3 labelPos = new Vector3(col.bounds.center.x, ArrowYLevel + 0.1f, col.bounds.center.z);
+ Handles.Label(labelPos, $"→ {FormatPlayerName(target)}");
+ }
+#endif
+ }
+
+ ///
+ /// Computes the world-space center of the target zone by averaging the bounds-centers
+ /// of all PlayerZoneVolumes whose matches
+ /// . Returns Vector3.zero if no matching zone is found —
+ /// the caller treats zero as a "not found" sentinel.
+ ///
+ ///
+ /// Performs a scene-wide query each gizmo draw. This is fine for our worst case
+ /// (9-player map: 8 leak exits × ~10 zone volumes = ~80 considerations per frame, only
+ /// when gizmos are drawing). If this ever shows up as an editor-perf issue, switch to a
+ /// cached lookup invalidated on hierarchy change.
+ ///
+ private Vector3 FindTargetZoneCenter()
+ {
+ var zones = Object.FindObjectsByType(FindObjectsInactive.Exclude);
+ if (zones == null || zones.Length == 0) return Vector3.zero;
+
+ Vector3 accumulated = Vector3.zero;
+ int count = 0;
+ for (int i = 0; i < zones.Length; i++)
+ {
+ if (zones[i] == null) continue;
+ if (zones[i].owner != target) continue;
+
+ var c = zones[i].GetComponent();
+ if (c == null) continue;
+
+ accumulated += c.bounds.center;
+ count++;
+ }
+
+ if (count == 0) return Vector3.zero;
+ return accumulated / count;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Assets/_Project/Levels/LeakExitVolume.cs.meta b/Assets/_Project/Levels/LeakExitVolume.cs.meta
new file mode 100644
index 0000000..2621ab7
--- /dev/null
+++ b/Assets/_Project/Levels/LeakExitVolume.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: d4da5d6673a12f546ac47563abc49d5a
\ No newline at end of file
diff --git a/Assets/_Project/Levels/LevelAuthoring.cs b/Assets/_Project/Levels/LevelAuthoring.cs
new file mode 100644
index 0000000..60991c4
--- /dev/null
+++ b/Assets/_Project/Levels/LevelAuthoring.cs
@@ -0,0 +1,258 @@
+using System.Collections.Generic;
+using UnityEngine;
+using TD.Core;
+
+#if UNITY_EDITOR
+using UnityEditor;
+#endif
+
+namespace TD.Levels
+{
+ ///
+ /// Scene-level coordinator for level authoring. One per scene, attached to an empty
+ /// GameObject named _LevelAuthoring whose subtree contains all the volumes for the map.
+ ///
+ ///
+ /// LevelAuthoring is the entry point for the bake operation: the custom inspector exposes a
+ /// "Bake LevelData" button that triggers the seven-phase algorithm, writing into the
+ /// ScriptableObject.
+ ///
+ /// This class also owns the per-volume-type "always show gizmos" toggles. Each volume walks
+ /// its parent hierarchy to find this LevelAuthoring and reads the relevant toggle.
+ ///
+ /// In the current session the bake methods are stubs — only the field schema and gizmo
+ /// rendering are implemented. The full bake script is the next session's work.
+ ///
+ public class LevelAuthoring : MonoBehaviour
+ {
+ // -------------------------------------------------------------------
+ // Bake target and lobby-facing metadata.
+ // -------------------------------------------------------------------
+
+ [Header("Bake Target")]
+ [Tooltip("The ScriptableObject this scene bakes into. Must be set before baking.")]
+ public LevelData targetAsset;
+
+ [Header("Map Metadata")]
+ [Tooltip("Display name for the lobby UI.")]
+ public string mapName = "";
+
+ [Tooltip("Number of players this map is designed for. Used by the lobby to filter maps " +
+ "and by bake validation. Allowed values: 1, 2, 3, 4, 5, 9.")]
+ public int playerCount = 1;
+
+ [Tooltip("Expected number of GoalVolume instances. Bake validates the count matches.")]
+ public int expectedGoalCount = 1;
+
+ [Tooltip("Optional description for the lobby UI. Empty triggers a soft warning at bake time.")]
+ [TextArea(2, 5)]
+ public string mapDescription = "";
+
+ [Tooltip("Optional author/credit text. Empty triggers a soft warning at bake time.")]
+ public string author = "";
+
+ // -------------------------------------------------------------------
+ // Gizmo visibility toggles. Each volume type reads its own toggle to decide whether to
+ // draw always-on or only-when-selected.
+ // -------------------------------------------------------------------
+
+ [Header("Gizmo Visibility (Always-Show Toggles)")]
+ [Tooltip("If true, PlayerZoneVolume gizmos draw whether or not the volume is selected.")]
+ public bool alwaysShowPlayerZones = false;
+
+ [Tooltip("If true, SpawnerVolume gizmos draw whether or not the volume is selected.")]
+ public bool alwaysShowSpawners = false;
+
+ [Tooltip("If true, LeakExitVolume gizmos draw whether or not the volume is selected.")]
+ public bool alwaysShowLeakExits = false;
+
+ [Tooltip("If true, GoalVolume gizmos draw whether or not the volume is selected.")]
+ public bool alwaysShowGoals = false;
+
+ // -------------------------------------------------------------------
+ // Bake API (stubs this session — full implementation comes next session).
+ // -------------------------------------------------------------------
+
+ ///
+ /// Runs the seven-phase bake algorithm and writes the result into .
+ ///
+ /// True if the bake succeeded (with or without warnings); false if validation
+ /// failed or any other hard error occurred. On failure, the existing targetAsset on disk
+ /// is left untouched.
+ ///
+ /// STUB — full implementation is the next session's work. Currently logs a not-implemented
+ /// message and returns false.
+ ///
+ public bool BakeLevelData()
+ {
+ Debug.LogWarning("[LevelAuthoring] BakeLevelData is not yet implemented. " +
+ "The seven-phase bake algorithm will be added in the next session.");
+ return false;
+ }
+
+ ///
+ /// Re-renders just the lobby thumbnail without doing a full bake. Useful when only visual
+ /// scene content (terrain, decorations) has changed.
+ ///
+ ///
+ /// STUB — full implementation is part of the bake script work. Currently logs a
+ /// not-implemented message.
+ ///
+ public void RefreshThumbnail()
+ {
+ Debug.LogWarning("[LevelAuthoring] RefreshThumbnail is not yet implemented. " +
+ "Thumbnail rendering will be added with the bake script.");
+ }
+
+ // -------------------------------------------------------------------
+ // Map-level gizmos: origin marker, map bounding rect, combined player zone outlines.
+ // Always draw when LevelAuthoring is in the scene (no per-toggle gating for these).
+ // -------------------------------------------------------------------
+
+ private const float OriginMarkerY = 0.005f;
+ private const float MapBoundsY = 0.01f;
+ private const float CombinedZoneOutlineY = 0.03f;
+ private const float OriginMarkerSize = 0.4f;
+
+ private void OnDrawGizmos()
+ {
+ DrawOriginMarker();
+ DrawMapBoundingRect();
+ DrawCombinedPlayerZoneOutlines();
+ }
+
+ private void DrawOriginMarker()
+ {
+ Color prev = Gizmos.color;
+ Gizmos.color = Color.white;
+
+ // A small "+" cross at world (0,0) plus a sphere for visibility.
+ Vector3 origin = new Vector3(0f, OriginMarkerY, 0f);
+ Gizmos.DrawSphere(origin, 0.1f);
+ Gizmos.DrawLine(origin + new Vector3(-OriginMarkerSize, 0f, 0f),
+ origin + new Vector3( OriginMarkerSize, 0f, 0f));
+ Gizmos.DrawLine(origin + new Vector3(0f, 0f, -OriginMarkerSize),
+ origin + new Vector3(0f, 0f, OriginMarkerSize));
+
+ Gizmos.color = prev;
+
+#if UNITY_EDITOR
+ Handles.Label(origin + new Vector3(0.15f, 0.05f, 0.15f), "Tile (0,0)");
+#endif
+ }
+
+ private void DrawMapBoundingRect()
+ {
+ // Find every VolumeBase in this LevelAuthoring's subtree (matches the bake's scoped scan)
+ // and union their tile bounding rectangles.
+ var volumes = GetComponentsInChildren(includeInactive: false);
+ if (volumes == null || volumes.Length == 0) return;
+
+ bool initialized = false;
+ Vector2Int minTile = Vector2Int.zero;
+ Vector2Int maxTile = Vector2Int.zero;
+
+ for (int i = 0; i < volumes.Length; i++)
+ {
+ var col = volumes[i] != null ? volumes[i].GetComponent() : null;
+ if (col == null) continue;
+
+ if (!VolumeBase.TryGetTightTileRect(col.bounds, out Vector2Int vMin, out Vector2Int vMax))
+ {
+ continue; // volume covers no tiles (e.g., bounds don't intersect Y=0)
+ }
+
+ if (!initialized)
+ {
+ minTile = vMin;
+ maxTile = vMax;
+ initialized = true;
+ }
+ else
+ {
+ if (vMin.x < minTile.x) minTile.x = vMin.x;
+ if (vMin.y < minTile.y) minTile.y = vMin.y;
+ if (vMax.x > maxTile.x) maxTile.x = vMax.x;
+ if (vMax.y > maxTile.y) maxTile.y = vMax.y;
+ }
+ }
+
+ if (!initialized) return;
+
+ // Convert tile-corner indices to world-space corners. Tiles are center-based with
+ // TILE_SIZE = 1, so a tile at (x,y) spans world XZ from (x-0.5, y-0.5) to (x+0.5, y+0.5).
+ float halfTile = GridCoordinates.TILE_SIZE * 0.5f;
+ Vector3 sw = new Vector3(minTile.x - halfTile, MapBoundsY, minTile.y - halfTile);
+ Vector3 se = new Vector3(maxTile.x + halfTile, MapBoundsY, minTile.y - halfTile);
+ Vector3 ne = new Vector3(maxTile.x + halfTile, MapBoundsY, maxTile.y + halfTile);
+ Vector3 nw = new Vector3(minTile.x - halfTile, MapBoundsY, maxTile.y + halfTile);
+
+ Color prev = Gizmos.color;
+ Gizmos.color = new Color(1f, 1f, 1f, 0.6f); // muted white
+
+ Gizmos.DrawLine(sw, se);
+ Gizmos.DrawLine(se, ne);
+ Gizmos.DrawLine(ne, nw);
+ Gizmos.DrawLine(nw, sw);
+
+ Gizmos.color = prev;
+ }
+
+ private void DrawCombinedPlayerZoneOutlines()
+ {
+ // Group all PlayerZoneVolumes in this subtree by owner, then for each owner compute
+ // the union of their tile coverage and draw the perimeter using the general
+ // (non-rectangular) algorithm. This visualizes L-shaped multi-volume zones as a
+ // single unified outline.
+ var zones = GetComponentsInChildren(includeInactive: false);
+ if (zones == null || zones.Length == 0) return;
+
+ // Build per-owner tile sets.
+ var perOwner = new Dictionary>();
+ for (int i = 0; i < zones.Length; i++)
+ {
+ var z = zones[i];
+ if (z == null) continue;
+
+ var col = z.GetComponent();
+ if (col == null) continue;
+
+ if (!perOwner.TryGetValue(z.owner, out var set))
+ {
+ set = new HashSet();
+ perOwner[z.owner] = set;
+ }
+
+ // Use the shared rasterizer so this stays in lock-step with what the bake will see.
+ VolumeBase.RasterizeBoundsToTiles(col.bounds, t => set.Add(t));
+ }
+
+ // For each owner, draw perimeter using the general algorithm.
+ float halfTile = GridCoordinates.TILE_SIZE * 0.5f;
+
+ Color prev = Gizmos.color;
+ foreach (var kv in perOwner)
+ {
+ Color outlineColor = PlayerColors.Get(kv.Key);
+ Gizmos.color = outlineColor;
+
+ foreach (var tile in kv.Value)
+ {
+ // World-space corners of this tile.
+ Vector3 sw = new Vector3(tile.x - halfTile, CombinedZoneOutlineY, tile.y - halfTile);
+ Vector3 se = new Vector3(tile.x + halfTile, CombinedZoneOutlineY, tile.y - halfTile);
+ Vector3 ne = new Vector3(tile.x + halfTile, CombinedZoneOutlineY, tile.y + halfTile);
+ Vector3 nw = new Vector3(tile.x - halfTile, CombinedZoneOutlineY, tile.y + halfTile);
+
+ // For each of the four edges, draw it ONLY if the neighbor across that edge
+ // is not in the covered set (i.e., the edge is on the perimeter).
+ if (!kv.Value.Contains(new Vector2Int(tile.x, tile.y - 1))) Gizmos.DrawLine(sw, se); // south edge
+ if (!kv.Value.Contains(new Vector2Int(tile.x + 1, tile.y))) Gizmos.DrawLine(se, ne); // east edge
+ if (!kv.Value.Contains(new Vector2Int(tile.x, tile.y + 1))) Gizmos.DrawLine(ne, nw); // north edge
+ if (!kv.Value.Contains(new Vector2Int(tile.x - 1, tile.y))) Gizmos.DrawLine(nw, sw); // west edge
+ }
+ }
+ Gizmos.color = prev;
+ }
+ }
+}
diff --git a/Assets/_Project/Levels/LevelAuthoring.cs.meta b/Assets/_Project/Levels/LevelAuthoring.cs.meta
new file mode 100644
index 0000000..6ed5287
--- /dev/null
+++ b/Assets/_Project/Levels/LevelAuthoring.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 37de128911d7bbc4299ea87cdb520ed7
\ No newline at end of file
diff --git a/Assets/_Project/Levels/LevelData.cs b/Assets/_Project/Levels/LevelData.cs
new file mode 100644
index 0000000..df02d83
--- /dev/null
+++ b/Assets/_Project/Levels/LevelData.cs
@@ -0,0 +1,144 @@
+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;
+
+ // -------------------------------------------------------------------
+ // 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;
+
+ // -------------------------------------------------------------------
+ // 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;
+ }
+}
diff --git a/Assets/_Project/Levels/LevelData.cs.meta b/Assets/_Project/Levels/LevelData.cs.meta
new file mode 100644
index 0000000..46f2565
--- /dev/null
+++ b/Assets/_Project/Levels/LevelData.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 6d4e9c37b9205f3408a8225823f7a4da
\ No newline at end of file
diff --git a/Assets/_Project/Levels/PlayerZoneVolume.cs b/Assets/_Project/Levels/PlayerZoneVolume.cs
new file mode 100644
index 0000000..d3ff359
--- /dev/null
+++ b/Assets/_Project/Levels/PlayerZoneVolume.cs
@@ -0,0 +1,68 @@
+using UnityEngine;
+using TD.Core;
+
+#if UNITY_EDITOR
+using UnityEditor;
+#endif
+
+namespace TD.Levels
+{
+ ///
+ /// Authoring volume that defines a player's owned territory. Tiles covered by a
+ /// PlayerZoneVolume are in the baked grid (unless
+ /// covered by an Invalid-validity volume — Invalid wins).
+ ///
+ ///
+ /// A single player's zone may consist of multiple PlayerZoneVolume instances with the same
+ /// ; the bake unions their tile coverage. This supports L-shapes and other
+ /// non-rectangular zone footprints.
+ ///
+ public class PlayerZoneVolume : VolumeBase
+ {
+ [Tooltip("Which player owns this zone.")]
+ public PlayerSlot owner = PlayerSlot.Player1;
+
+ [Tooltip("Whether tiles in this volume are buildable. Defaults to Allowed.")]
+ public PlacementValidity placementValidity = PlacementValidity.Allowed;
+
+ // Per-volume gizmo Y level (see gizmo design summary). Player zones draw lowest so
+ // overlapping spawners/leak exits/goals stack on top of them.
+ private const float FillYLevel = 0.02f;
+
+ protected override bool GetAlwaysShowToggle(LevelAuthoring authoring)
+ {
+ return authoring.alwaysShowPlayerZones;
+ }
+
+ // Selection-only by default. Always-on path runs from OnDrawGizmos when the toggle is set.
+ private void OnDrawGizmosSelected()
+ {
+ DrawGizmosCore();
+ }
+
+ private void OnDrawGizmos()
+ {
+ if (ShouldDrawAlwaysOn())
+ {
+ DrawGizmosCore();
+ }
+ }
+
+ private void DrawGizmosCore()
+ {
+ Color baseColor = PlayerColors.Get(owner);
+ DrawTileCoverageFill(baseColor, alpha: 0.35f, yLevel: FillYLevel);
+ DrawRectangularOutline(baseColor, yLevel: FillYLevel);
+
+#if UNITY_EDITOR
+ // Label centered on the volume's bounds, identifying which player owns this build area.
+ var col = Collider;
+ if (col != null)
+ {
+ Vector3 labelPos = new Vector3(col.bounds.center.x, FillYLevel + 0.1f, col.bounds.center.z);
+ Handles.Label(labelPos, $"{FormatPlayerName(owner)} Build Area");
+ }
+#endif
+ }
+ }
+}
\ No newline at end of file
diff --git a/Assets/_Project/Levels/PlayerZoneVolume.cs.meta b/Assets/_Project/Levels/PlayerZoneVolume.cs.meta
new file mode 100644
index 0000000..2a3ff35
--- /dev/null
+++ b/Assets/_Project/Levels/PlayerZoneVolume.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 5e8ab74b9038bd64782043bfc21f4cd7
\ No newline at end of file
diff --git a/Assets/_Project/Levels/SpawnerVolume.cs b/Assets/_Project/Levels/SpawnerVolume.cs
new file mode 100644
index 0000000..6eb6e62
--- /dev/null
+++ b/Assets/_Project/Levels/SpawnerVolume.cs
@@ -0,0 +1,118 @@
+using UnityEngine;
+using TD.Core;
+
+#if UNITY_EDITOR
+using UnityEditor;
+#endif
+
+namespace TD.Levels
+{
+ ///
+ /// Authoring volume marking where enemies spawn into a player's zone. Spawner tiles are
+ /// in the baked grid (no tower placement allowed)
+ /// but remain walkable so enemies can leave the spawner.
+ ///
+ ///
+ /// A zone may have multiple spawners; disambiguates them.
+ /// Player 5 in the 9-player Wintermaul map is the canonical multi-spawner zone.
+ ///
+ /// The spawner declares an owning player via rather than relying on
+ /// spatial containment within a PlayerZoneVolume. The bake validates (with a soft warning)
+ /// that the spawner's tiles fall inside its declared owner's zone.
+ ///
+ public class SpawnerVolume : VolumeBase
+ {
+ [Tooltip("Which player's zone owns this spawner. Explicit — not inferred from spatial overlap.")]
+ public PlayerSlot owner = PlayerSlot.Player1;
+
+ [Tooltip("Disambiguator for zones with multiple spawners. Should be 0 for single-spawner zones, " +
+ "and contiguous starting from 0 for multi-spawner zones (e.g., 0 and 1).")]
+ public int spawnerIdInZone = 0;
+
+ [Tooltip("Direction enemies face when they spawn. Used for visual orientation and may bias " +
+ "initial enemy movement direction.")]
+ public Direction spawnFacing = Direction.South;
+
+ [Tooltip("Whether tiles in this volume are buildable. Defaults to Invalid (no placement on spawners).")]
+ public PlacementValidity placementValidity = PlacementValidity.Invalid;
+
+ // Spawners draw above player zones so the player zone color reads as background.
+ private const float FillYLevel = 0.06f;
+ private const float ArrowYLevel = 0.07f;
+ private const float ArrowLength = 1.5f; // 1.5 tiles per gizmo design
+
+ protected override bool GetAlwaysShowToggle(LevelAuthoring authoring)
+ {
+ return authoring.alwaysShowSpawners;
+ }
+
+ private void OnDrawGizmosSelected()
+ {
+ DrawGizmosCore();
+ }
+
+ private void OnDrawGizmos()
+ {
+ if (ShouldDrawAlwaysOn())
+ {
+ DrawGizmosCore();
+ }
+ }
+
+ private void DrawGizmosCore()
+ {
+ Color baseColor = PlayerColors.Get(owner);
+ DrawTileCoverageFill(baseColor, alpha: 0.40f, yLevel: FillYLevel);
+ DrawRectangularOutline(baseColor, yLevel: FillYLevel);
+
+ // Direction arrow originates from the volume's center and extends 1.5 tiles in the
+ // declared facing direction, rendered in opaque owner color.
+ var col = Collider;
+ if (col != null)
+ {
+ Vector3 arrowOrigin = new Vector3(col.bounds.center.x, ArrowYLevel, col.bounds.center.z);
+ Vector3 arrowDir = DirectionToWorld(spawnFacing);
+ DrawArrow(arrowOrigin, arrowDir, ArrowLength, baseColor);
+ }
+
+#if UNITY_EDITOR
+ // Label conditional on multi-spawner status:
+ // - Single-spawner zone: "Player N Spawn"
+ // - Multi-spawner zone: "Player N Spawn 0", "Player N Spawn 1", etc.
+ // Multi-spawner detection scans other SpawnerVolumes in the scene with the same owner.
+ // Same posture as the leak-exit target lookup: cheap on realistic maps, can be cached
+ // if it ever shows up as an editor-perf issue.
+ if (col != null)
+ {
+ Vector3 labelPos = new Vector3(col.bounds.center.x, ArrowYLevel + 0.1f, col.bounds.center.z);
+ string labelText = ZoneHasMultipleSpawners()
+ ? $"{FormatPlayerName(owner)} Spawn {spawnerIdInZone}"
+ : $"{FormatPlayerName(owner)} Spawn";
+ Handles.Label(labelPos, labelText);
+ }
+#endif
+ }
+
+ ///
+ /// Returns true if more than one active SpawnerVolume in the scene shares this spawner's
+ /// . Used to decide whether to suffix the gizmo label with the spawner ID.
+ ///
+ private bool ZoneHasMultipleSpawners()
+ {
+ var spawners = Object.FindObjectsByType(FindObjectsInactive.Exclude);
+ if (spawners == null) return false;
+
+ int count = 0;
+ for (int i = 0; i < spawners.Length; i++)
+ {
+ if (spawners[i] == null) continue;
+ if (spawners[i].owner == owner)
+ {
+ count++;
+ if (count > 1) return true;
+ }
+ }
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Assets/_Project/Levels/SpawnerVolume.cs.meta b/Assets/_Project/Levels/SpawnerVolume.cs.meta
new file mode 100644
index 0000000..3b12c00
--- /dev/null
+++ b/Assets/_Project/Levels/SpawnerVolume.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: a756762f899008f45b2986bdcb11c819
\ No newline at end of file
diff --git a/Assets/_Project/Levels/VolumeBase.cs b/Assets/_Project/Levels/VolumeBase.cs
new file mode 100644
index 0000000..cea9b97
--- /dev/null
+++ b/Assets/_Project/Levels/VolumeBase.cs
@@ -0,0 +1,319 @@
+using UnityEngine;
+using TD.Core;
+
+namespace TD.Levels
+{
+ ///
+ /// Abstract base class for all level authoring volumes (player zones, spawners, leak exits, goals).
+ ///
+ ///
+ /// Every concrete volume requires a . The collider's bounds (in world
+ /// space) define the volume's spatial extent, which the bake script rasterizes to tiles via
+ /// and bounds.Contains(tileCenter).
+ ///
+ /// Volumes are authoring-time artifacts only — they are NOT part of the runtime gameplay scene.
+ /// At runtime the bake's output () is the sole source of truth.
+ ///
+ /// Visibility model: gizmos draw via OnDrawGizmosSelected by default (selection-only).
+ /// The owning exposes per-type toggles to make a category always-on.
+ /// Volumes not parented under a LevelAuthoring (orphans) always behave as selection-only;
+ /// they will also produce warnings during bake.
+ ///
+ [RequireComponent(typeof(BoxCollider))]
+ [DisallowMultipleComponent]
+ public abstract class VolumeBase : MonoBehaviour
+ {
+ // Cached collider reference. Looked up lazily and refreshed if it goes null
+ // (e.g., the user manually removed the component).
+ private BoxCollider _cachedCollider;
+
+ // Cached LevelAuthoring lookup. We walk parents to find it once, then keep the reference.
+ // If the cached reference becomes null (parent moved, scene changed), the next access re-resolves.
+ private LevelAuthoring _cachedAuthoring;
+
+ ///
+ /// Gets the volume's , looking it up lazily. Returns null if the
+ /// component has been removed (which violates [RequireComponent] but can happen if
+ /// a user manually removes it).
+ ///
+ protected BoxCollider Collider
+ {
+ get
+ {
+ if (_cachedCollider == null)
+ {
+ _cachedCollider = GetComponent();
+ }
+ return _cachedCollider;
+ }
+ }
+
+ ///
+ /// Walks the parent transform hierarchy to find the owning
+ /// component. Returns null if this volume is orphaned (not parented under a LevelAuthoring).
+ /// Result is cached; the cache invalidates automatically if the reference goes null.
+ ///
+ protected LevelAuthoring FindOwningAuthoring()
+ {
+ if (_cachedAuthoring != null)
+ {
+ return _cachedAuthoring;
+ }
+
+ // GetComponentInParent walks up the hierarchy. Includes inactive parents to handle the
+ // case where _LevelAuthoring is temporarily disabled during scene work.
+ _cachedAuthoring = GetComponentInParent(includeInactive: true);
+ return _cachedAuthoring;
+ }
+
+ ///
+ /// When implemented by a subclass, returns whether the corresponding "always show" toggle
+ /// is enabled on the supplied . Each volume type maps to a
+ /// different toggle field.
+ ///
+ /// The owning LevelAuthoring (guaranteed non-null).
+ protected abstract bool GetAlwaysShowToggle(LevelAuthoring authoring);
+
+ ///
+ /// Decides whether the "always show" path should run for this volume. Returns true if a
+ /// LevelAuthoring is found in parents AND its corresponding toggle is enabled. Orphans
+ /// always return false (always-show requires an authoring to read the toggle from).
+ ///
+ protected bool ShouldDrawAlwaysOn()
+ {
+ var authoring = FindOwningAuthoring();
+ if (authoring == null)
+ {
+ return false;
+ }
+ return GetAlwaysShowToggle(authoring);
+ }
+
+ // -------------------------------------------------------------------
+ // Static rasterization helper. Single source of truth for converting a Bounds to the
+ // set of tiles its rasterization covers. The bake will use the same primitive (a tile
+ // is "covered" iff bounds.Contains(tileCenter)) so gizmos and bake stay in lock-step.
+ // -------------------------------------------------------------------
+
+ ///
+ /// Iterates every tile whose center lies inside and invokes
+ /// for each one. The candidate range is computed via
+ /// with a one-tile padding on each side to guard
+ /// against rounding surprises (WorldToGrid uses round-to-nearest, which can over- or
+ /// under-shoot by one when bounds align with tile edges); the per-tile
+ /// bounds.Contains(tileCenter) test inside the loop guarantees correctness.
+ ///
+ public static void RasterizeBoundsToTiles(Bounds bounds, System.Action onTile)
+ {
+ if (onTile == null) return;
+
+ Vector2Int candidateMin = GridCoordinates.WorldToGrid(new Vector2(bounds.min.x, bounds.min.z));
+ Vector2Int candidateMax = GridCoordinates.WorldToGrid(new Vector2(bounds.max.x, bounds.max.z));
+
+ int xLo = candidateMin.x - 1;
+ int xHi = candidateMax.x + 1;
+ int yLo = candidateMin.y - 1;
+ int yHi = candidateMax.y + 1;
+
+ for (int x = xLo; x <= xHi; x++)
+ {
+ for (int y = yLo; y <= yHi; y++)
+ {
+ Vector3 tileCenter = GridCoordinates.GridToWorld(new Vector2Int(x, y));
+ Vector3 testPoint = new Vector3(tileCenter.x, bounds.center.y, tileCenter.z);
+ if (bounds.Contains(testPoint))
+ {
+ onTile(new Vector2Int(x, y));
+ }
+ }
+ }
+ }
+
+ ///
+ /// Computes the tight tile rectangle for a — the rectangle that
+ /// exactly contains every tile whose center is inside the bounds. Returns false if no
+ /// tile centers fall inside the bounds.
+ ///
+ public static bool TryGetTightTileRect(Bounds bounds, out Vector2Int minTile, out Vector2Int maxTile)
+ {
+ bool any = false;
+ int minX = 0, maxX = 0, minY = 0, maxY = 0;
+
+ RasterizeBoundsToTiles(bounds, t =>
+ {
+ if (!any)
+ {
+ minX = maxX = t.x;
+ minY = maxY = t.y;
+ any = true;
+ }
+ else
+ {
+ if (t.x < minX) minX = t.x;
+ if (t.x > maxX) maxX = t.x;
+ if (t.y < minY) minY = t.y;
+ if (t.y > maxY) maxY = t.y;
+ }
+ });
+
+ if (!any)
+ {
+ minTile = Vector2Int.zero;
+ maxTile = Vector2Int.zero;
+ return false;
+ }
+
+ minTile = new Vector2Int(minX, minY);
+ maxTile = new Vector2Int(maxX, maxY);
+ return true;
+ }
+
+ ///
+ /// Instance-side convenience: computes the tight tile rectangle for THIS volume's collider.
+ ///
+ protected bool TryGetTightTileRect(out Vector2Int minTile, out Vector2Int maxTile)
+ {
+ minTile = Vector2Int.zero;
+ maxTile = Vector2Int.zero;
+ var col = Collider;
+ if (col == null) return false;
+ return TryGetTightTileRect(col.bounds, out minTile, out maxTile);
+ }
+
+ ///
+ /// Draws a translucent fill over the volume's tile coverage at the specified world Y.
+ /// Uses per-tile flat cube gizmos (Option A from the gizmo design discussion).
+ ///
+ /// Fully-opaque color; alpha will be applied via .
+ /// Alpha 0..1 applied to the fill.
+ /// World Y at which to draw the fill. Stagger by volume type to avoid z-fighting.
+ protected void DrawTileCoverageFill(Color fillColor, float alpha, float yLevel)
+ {
+ if (!TryGetTightTileRect(out Vector2Int minTile, out Vector2Int maxTile)) return;
+
+ // For a single BoxCollider, every tile in the tight rectangle is covered (the rect was
+ // built from actually-covered tiles, and the covered set is always rectangular for a
+ // BoxCollider's bounds). So we can iterate the rect directly without re-checking
+ // bounds.Contains per tile.
+ Color prev = Gizmos.color;
+ Gizmos.color = PlayerColors.WithAlpha(fillColor, alpha);
+
+ Vector3 tileSize = new Vector3(GridCoordinates.TILE_SIZE, 0.01f, GridCoordinates.TILE_SIZE);
+
+ for (int x = minTile.x; x <= maxTile.x; x++)
+ {
+ for (int y = minTile.y; y <= maxTile.y; y++)
+ {
+ Vector3 center = GridCoordinates.GridToWorld(new Vector2Int(x, y));
+ Vector3 drawCenter = new Vector3(center.x, yLevel, center.z);
+ Gizmos.DrawCube(drawCenter, tileSize);
+ }
+ }
+
+ Gizmos.color = prev;
+ }
+
+ ///
+ /// Draws an opaque rectangular perimeter outline around the volume's tile coverage at the
+ /// specified world Y. For single-volume rasterization the coverage is always rectangular
+ /// (a BoxCollider's tile rasterization can only produce a tile rectangle), so we draw the
+ /// four edges of the tight tile rect.
+ ///
+ /// Outline color (alpha applied as supplied).
+ /// World Y at which to draw the outline.
+ protected void DrawRectangularOutline(Color outlineColor, float yLevel)
+ {
+ if (!TryGetTightTileRect(out Vector2Int minTile, out Vector2Int maxTile)) return;
+
+ // Convert tile-corner indices to world-space corners. A tile at (x, y) spans world XZ
+ // from (x - 0.5, y - 0.5) to (x + 0.5, y + 0.5) (TILE_SIZE = 1, center-based).
+ float halfTile = GridCoordinates.TILE_SIZE * 0.5f;
+ Vector3 swCorner = new Vector3(minTile.x - halfTile, yLevel, minTile.y - halfTile);
+ Vector3 seCorner = new Vector3(maxTile.x + halfTile, yLevel, minTile.y - halfTile);
+ Vector3 neCorner = new Vector3(maxTile.x + halfTile, yLevel, maxTile.y + halfTile);
+ Vector3 nwCorner = new Vector3(minTile.x - halfTile, yLevel, maxTile.y + halfTile);
+
+ Color prev = Gizmos.color;
+ Gizmos.color = outlineColor;
+
+ Gizmos.DrawLine(swCorner, seCorner);
+ Gizmos.DrawLine(seCorner, neCorner);
+ Gizmos.DrawLine(neCorner, nwCorner);
+ Gizmos.DrawLine(nwCorner, swCorner);
+
+ Gizmos.color = prev;
+ }
+
+ ///
+ /// Draws a line+cone arrow from in the given world-space
+ /// direction. The arrow shaft is drawn with and the
+ /// arrowhead is built from four lines forming a small cone.
+ ///
+ /// Arrow start point (world space).
+ /// Unit-length direction the arrow points in.
+ /// Total arrow length in world units.
+ /// Arrow color (alpha applied as supplied).
+ protected void DrawArrow(Vector3 origin, Vector3 direction, float length, Color color)
+ {
+ if (direction.sqrMagnitude < 0.0001f) return;
+ direction = direction.normalized;
+
+ Vector3 tip = origin + direction * length;
+
+ // Pick an "up" axis perpendicular to the arrow direction in the XZ plane. Since our
+ // arrows are always horizontal (Y is fixed), we want the arrowhead to flare in the
+ // horizontal plane. The right-vector relative to the direction does this:
+ // right = cross(direction, world up). If direction is parallel to world up
+ // (vertical arrow, which we don't expect), fall back to world forward.
+ Vector3 right = Vector3.Cross(direction, Vector3.up);
+ if (right.sqrMagnitude < 0.0001f)
+ {
+ right = Vector3.Cross(direction, Vector3.forward);
+ }
+ right.Normalize();
+
+ // Arrowhead size proportional to arrow length.
+ float headLength = length * 0.25f;
+ float headWidth = length * 0.15f;
+ Vector3 headBase = tip - direction * headLength;
+ Vector3 leftWing = headBase - right * headWidth;
+ Vector3 rightWing = headBase + right * headWidth;
+
+ Color prev = Gizmos.color;
+ Gizmos.color = color;
+
+ Gizmos.DrawLine(origin, tip);
+ Gizmos.DrawLine(tip, leftWing);
+ Gizmos.DrawLine(tip, rightWing);
+ Gizmos.DrawLine(leftWing, rightWing); // close the head for visibility
+
+ Gizmos.color = prev;
+ }
+
+ ///
+ /// Converts a enum to a unit world-space vector in the XZ plane.
+ /// North = +Z, South = -Z, East = +X, West = -X (matches grid-y mapped to world-z).
+ ///
+ protected static Vector3 DirectionToWorld(Direction direction)
+ {
+ switch (direction)
+ {
+ case Direction.North: return new Vector3(0f, 0f, 1f);
+ case Direction.South: return new Vector3(0f, 0f, -1f);
+ case Direction.East: return new Vector3(1f, 0f, 0f);
+ case Direction.West: return new Vector3(-1f, 0f, 0f);
+ default: return Vector3.zero;
+ }
+ }
+
+ ///
+ /// Returns a human-readable label for a player slot, used by gizmo labels for consistency
+ /// across volume types. Currently formatted as "Player N" (or "Player ?" for None / unknown).
+ ///
+ public static string FormatPlayerName(PlayerSlot slot)
+ {
+ if (slot == PlayerSlot.None) return "Player ?";
+ return "Player " + ((byte)slot).ToString();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Assets/_Project/Levels/VolumeBase.cs.meta b/Assets/_Project/Levels/VolumeBase.cs.meta
new file mode 100644
index 0000000..894d2e9
--- /dev/null
+++ b/Assets/_Project/Levels/VolumeBase.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: c95a277ca66b4a34e8de9e5ee420f145
\ No newline at end of file
diff --git a/Assets/_Project/Scenes/Levels/Main.unity b/Assets/_Project/Scenes/Levels/Main.unity
index 97c598a..9733e97 100644
--- a/Assets/_Project/Scenes/Levels/Main.unity
+++ b/Assets/_Project/Scenes/Levels/Main.unity
@@ -119,6 +119,74 @@ NavMeshSettings:
debug:
m_Flags: 0
m_NavMeshData: {fileID: 0}
+--- !u!1 &154690529
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 154690530}
+ - component: {fileID: 154690532}
+ - component: {fileID: 154690531}
+ m_Layer: 0
+ m_Name: Player1Zone
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &154690530
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 154690529}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: -12, y: 0, z: 13}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 441239881}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!114 &154690531
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 154690529}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 5e8ab74b9038bd64782043bfc21f4cd7, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Assembly-CSharp::TD.Levels.PlayerZoneVolume
+ owner: 1
+ placementValidity: 1
+--- !u!65 &154690532
+BoxCollider:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 154690529}
+ m_Material: {fileID: 0}
+ m_IncludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_ExcludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_LayerOverridePriority: 0
+ m_IsTrigger: 0
+ m_ProvidesContacts: 0
+ m_Enabled: 1
+ serializedVersion: 3
+ m_Size: {x: 28, y: 1, z: 7}
+ m_Center: {x: 0, y: 0, z: 0}
--- !u!1 &239104687
GameObject:
m_ObjectHideFlags: 0
@@ -191,6 +259,76 @@ Transform:
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &304575571
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 304575572}
+ - component: {fileID: 304575574}
+ - component: {fileID: 304575573}
+ m_Layer: 0
+ m_Name: Player1Spawn
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &304575572
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 304575571}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: -29, y: 0, z: 13}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 441239881}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!114 &304575573
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 304575571}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: a756762f899008f45b2986bdcb11c819, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Assembly-CSharp::TD.Levels.SpawnerVolume
+ owner: 1
+ spawnerIdInZone: 0
+ spawnFacing: 2
+ placementValidity: 0
+--- !u!65 &304575574
+BoxCollider:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 304575571}
+ m_Material: {fileID: 0}
+ m_IncludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_ExcludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_LayerOverridePriority: 0
+ m_IsTrigger: 0
+ m_ProvidesContacts: 0
+ m_Enabled: 1
+ serializedVersion: 3
+ m_Size: {x: 7, y: 1, z: 7}
+ m_Center: {x: 0, y: 0, z: 0}
--- !u!1 &330585543
GameObject:
m_ObjectHideFlags: 0
@@ -455,6 +593,66 @@ MonoBehaviour:
m_ShadowLayerMask: 1
m_RenderingLayers: 1
m_ShadowRenderingLayers: 1
+--- !u!1 &441239879
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 441239881}
+ - component: {fileID: 441239880}
+ m_Layer: 0
+ m_Name: _LevelAuthoring
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!114 &441239880
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 441239879}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 37de128911d7bbc4299ea87cdb520ed7, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Assembly-CSharp::TD.Levels.LevelAuthoring
+ targetAsset: {fileID: 11400000, guid: 9cc56fbc3ae460a4b862f8510fdf5f09, type: 2}
+ mapName:
+ playerCount: 1
+ expectedGoalCount: 1
+ mapDescription:
+ author:
+ alwaysShowPlayerZones: 1
+ alwaysShowSpawners: 1
+ alwaysShowLeakExits: 1
+ alwaysShowGoals: 1
+--- !u!4 &441239881
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 441239879}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: -35.58358, y: 0, z: -6.35952}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children:
+ - {fileID: 154690530}
+ - {fileID: 1975687920}
+ - {fileID: 304575572}
+ - {fileID: 1078485324}
+ - {fileID: 1064792476}
+ - {fileID: 1360337263}
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &832575517
GameObject:
m_ObjectHideFlags: 0
@@ -504,6 +702,326 @@ Transform:
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &1064792475
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 1064792476}
+ - component: {fileID: 1064792478}
+ - component: {fileID: 1064792477}
+ m_Layer: 0
+ m_Name: Player1Leak
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &1064792476
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1064792475}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 3, y: 0, z: 13}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 441239881}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!114 &1064792477
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1064792475}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: d4da5d6673a12f546ac47563abc49d5a, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Assembly-CSharp::TD.Levels.LeakExitVolume
+ sourceZone: 1
+ target: 2
+ weight: 1
+ placementValidity: 0
+--- !u!65 &1064792478
+BoxCollider:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1064792475}
+ m_Material: {fileID: 0}
+ m_IncludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_ExcludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_LayerOverridePriority: 0
+ m_IsTrigger: 0
+ m_ProvidesContacts: 0
+ m_Enabled: 1
+ serializedVersion: 3
+ m_Size: {x: 2, y: 1, z: 7}
+ m_Center: {x: 0, y: 0, z: 0}
+--- !u!1 &1078485323
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 1078485324}
+ - component: {fileID: 1078485326}
+ - component: {fileID: 1078485325}
+ m_Layer: 0
+ m_Name: Player2Spawn
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &1078485324
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1078485323}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 8, y: 0, z: 22}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 441239881}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!114 &1078485325
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1078485323}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: a756762f899008f45b2986bdcb11c819, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Assembly-CSharp::TD.Levels.SpawnerVolume
+ owner: 2
+ spawnerIdInZone: 0
+ spawnFacing: 1
+ placementValidity: 0
+--- !u!65 &1078485326
+BoxCollider:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1078485323}
+ m_Material: {fileID: 0}
+ m_IncludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_ExcludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_LayerOverridePriority: 0
+ m_IsTrigger: 0
+ m_ProvidesContacts: 0
+ m_Enabled: 1
+ serializedVersion: 3
+ m_Size: {x: 7, y: 1, z: 10}
+ m_Center: {x: 0, y: 0, z: 0}
+--- !u!1 &1360337262
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 1360337263}
+ - component: {fileID: 1360337265}
+ - component: {fileID: 1360337264}
+ m_Layer: 0
+ m_Name: Goal
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &1360337263
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1360337262}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 33.5, y: 0, z: 13.5}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 441239881}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!114 &1360337264
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1360337262}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 755948f0244afea4da6ed2e4e3d27579, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Assembly-CSharp::TD.Levels.GoalVolume
+ placementValidity: 0
+--- !u!65 &1360337265
+BoxCollider:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1360337262}
+ m_Material: {fileID: 0}
+ m_IncludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_ExcludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_LayerOverridePriority: 0
+ m_IsTrigger: 0
+ m_ProvidesContacts: 0
+ m_Enabled: 1
+ serializedVersion: 3
+ m_Size: {x: 3, y: 1, z: 7}
+ m_Center: {x: 0, y: 0, z: 0}
+--- !u!1 &1464027360
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 1464027364}
+ - component: {fileID: 1464027363}
+ - component: {fileID: 1464027362}
+ - component: {fileID: 1464027361}
+ m_Layer: 0
+ m_Name: Plane
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!64 &1464027361
+MeshCollider:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1464027360}
+ m_Material: {fileID: 0}
+ m_IncludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_ExcludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_LayerOverridePriority: 0
+ m_IsTrigger: 0
+ m_ProvidesContacts: 0
+ m_Enabled: 1
+ serializedVersion: 5
+ m_Convex: 0
+ m_CookingOptions: 30
+ m_Mesh: {fileID: 10209, guid: 0000000000000000e000000000000000, type: 0}
+--- !u!23 &1464027362
+MeshRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1464027360}
+ m_Enabled: 1
+ m_CastShadows: 1
+ m_ReceiveShadows: 1
+ m_DynamicOccludee: 1
+ m_StaticShadowCaster: 0
+ m_MotionVectors: 1
+ m_LightProbeUsage: 1
+ m_ReflectionProbeUsage: 1
+ m_RayTracingMode: 2
+ m_RayTraceProcedural: 0
+ m_RayTracingAccelStructBuildFlagsOverride: 0
+ m_RayTracingAccelStructBuildFlags: 1
+ m_SmallMeshCulling: 1
+ m_ForceMeshLod: -1
+ m_MeshLodSelectionBias: 0
+ m_RenderingLayerMask: 1
+ m_RendererPriority: 0
+ m_Materials:
+ - {fileID: 2100000, guid: 31321ba15b8f8eb4c954353edc038b1d, type: 2}
+ m_StaticBatchInfo:
+ firstSubMesh: 0
+ subMeshCount: 0
+ m_StaticBatchRoot: {fileID: 0}
+ m_ProbeAnchor: {fileID: 0}
+ m_LightProbeVolumeOverride: {fileID: 0}
+ m_ScaleInLightmap: 1
+ m_ReceiveGI: 1
+ m_PreserveUVs: 0
+ m_IgnoreNormalsForChartDetection: 0
+ m_ImportantGI: 0
+ m_StitchLightmapSeams: 1
+ m_SelectedEditorRenderState: 3
+ m_MinimumChartSize: 4
+ m_AutoUVMaxDistance: 0.5
+ m_AutoUVMaxAngle: 89
+ m_LightmapParameters: {fileID: 0}
+ m_GlobalIlluminationMeshLod: 0
+ m_SortingLayerID: 0
+ m_SortingLayer: 0
+ m_SortingOrder: 0
+ m_MaskInteraction: 0
+ m_AdditionalVertexStreams: {fileID: 0}
+--- !u!33 &1464027363
+MeshFilter:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1464027360}
+ m_Mesh: {fileID: 10209, guid: 0000000000000000e000000000000000, type: 0}
+--- !u!4 &1464027364
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1464027360}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: -35, y: 0, z: 15}
+ m_LocalScale: {x: 7, y: 1, z: 3}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &1682341399
GameObject:
m_ObjectHideFlags: 0
@@ -610,6 +1128,74 @@ Transform:
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &1975687919
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 1975687920}
+ - component: {fileID: 1975687922}
+ - component: {fileID: 1975687921}
+ m_Layer: 0
+ m_Name: Player2Zone
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &1975687920
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1975687919}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 18, y: 0, z: 13}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 441239881}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!114 &1975687921
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1975687919}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 5e8ab74b9038bd64782043bfc21f4cd7, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Assembly-CSharp::TD.Levels.PlayerZoneVolume
+ owner: 2
+ placementValidity: 1
+--- !u!65 &1975687922
+BoxCollider:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1975687919}
+ m_Material: {fileID: 0}
+ m_IncludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_ExcludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_LayerOverridePriority: 0
+ m_IsTrigger: 0
+ m_ProvidesContacts: 0
+ m_Enabled: 1
+ serializedVersion: 3
+ m_Size: {x: 28, y: 1, z: 7}
+ m_Center: {x: 0, y: 0, z: 0}
--- !u!1660057539 &9223372036854775807
SceneRoots:
m_ObjectHideFlags: 0
@@ -619,3 +1205,5 @@ SceneRoots:
- {fileID: 832575519}
- {fileID: 1682341402}
- {fileID: 239104690}
+ - {fileID: 441239881}
+ - {fileID: 1464027364}
diff --git a/Assets/_Project/Scenes/Levels/TestLevel.asset b/Assets/_Project/Scenes/Levels/TestLevel.asset
new file mode 100644
index 0000000..b21d1f1
--- /dev/null
+++ b/Assets/_Project/Scenes/Levels/TestLevel.asset
@@ -0,0 +1,31 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 6d4e9c37b9205f3408a8225823f7a4da, type: 3}
+ m_Name: TestLevel
+ m_EditorClassIdentifier: Assembly-CSharp::TD.Levels.LevelData
+ MapName:
+ PlayerCount: 0
+ MapDescription:
+ Author:
+ MapThumbnail: {fileID: 0}
+ ScenePath:
+ AuthoringHash:
+ LastBakeTimestamp:
+ LastBakeOutcome: 0
+ LastBakeWarningCount: 0
+ GridOriginTile: {x: 0, y: 0}
+ GridSize: {x: 0, y: 0}
+ PlacementGrid:
+ WalkabilityGrid:
+ OwnerGrid:
+ PlayerZones: []
+ Goals: []
diff --git a/Assets/_Project/Scenes/Levels/TestLevel.asset.meta b/Assets/_Project/Scenes/Levels/TestLevel.asset.meta
new file mode 100644
index 0000000..740cb4f
--- /dev/null
+++ b/Assets/_Project/Scenes/Levels/TestLevel.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 9cc56fbc3ae460a4b862f8510fdf5f09
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 11400000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Scripts/Core/Enums.cs b/Assets/_Project/Scripts/Core/Enums.cs
new file mode 100644
index 0000000..5f28068
--- /dev/null
+++ b/Assets/_Project/Scripts/Core/Enums.cs
@@ -0,0 +1,88 @@
+namespace TD.Core
+{
+ ///
+ /// Identifies a player slot in a match. Backed by byte to keep grid arrays compact.
+ ///
+ ///
+ /// None is a sentinel value used in OwnerGrid to mark tiles not owned by any player zone.
+ /// Player1..Player9 cover the maximum supported player count. Maps using fewer players use a
+ /// contiguous prefix (e.g., a 3-player map uses Player1, Player2, Player3 only).
+ ///
+ public enum PlayerSlot : byte
+ {
+ None = 0,
+ Player1 = 1,
+ Player2 = 2,
+ Player3 = 3,
+ Player4 = 4,
+ Player5 = 5,
+ Player6 = 6,
+ Player7 = 7,
+ Player8 = 8,
+ Player9 = 9,
+ }
+
+ ///
+ /// Whether a volume permits tower placement on the tiles it covers.
+ ///
+ ///
+ /// Defaults: Allowed for , Invalid for
+ /// , , and
+ /// .
+ /// Composition rule when volumes overlap: "Invalid wins" — any tile covered by an Invalid volume
+ /// becomes regardless of other volumes covering it.
+ ///
+ public enum PlacementValidity
+ {
+ Invalid = 0,
+ Allowed = 1,
+ }
+
+ ///
+ /// Cardinal direction for spawner facing. Used by gizmos (direction arrow) and may be used
+ /// at runtime to bias initial enemy movement direction out of a spawner.
+ ///
+ public enum Direction
+ {
+ North,
+ South,
+ East,
+ West,
+ }
+
+ ///
+ /// Per-tile placement state in the baked PlacementGrid.
+ ///
+ ///
+ /// Backed by byte so default-initialized arrays (all zero) start as Outside, which is
+ /// the correct default for any tile not covered by an authoring volume.
+ ///
+ public enum PlacementState : byte
+ {
+ /// Tile is not covered by any authoring volume. Towers cannot be placed.
+ Outside = 0,
+
+ /// Tile is inside a player zone and not covered by any Invalid-validity volume.
+ /// Towers can be placed here (subject to runtime checks: ownership, footprint, gold, path).
+ Buildable = 1,
+
+ /// Tile is covered by at least one Invalid-validity volume (spawner, leak exit, goal).
+ /// Towers cannot be placed here. Note: this affects placement only; pathfinding still treats
+ /// the tile as walkable.
+ Restricted = 2,
+ }
+
+ ///
+ /// Outcome of a bake operation, recorded on the baked asset.
+ ///
+ ///
+ /// Failure is not represented here because failed bakes do not persist any state to the asset —
+ /// the previous successful bake (if any) remains on disk untouched. Failure state lives in
+ /// editor-only memory and is not part of the data schema.
+ ///
+ public enum BakeOutcome
+ {
+ Success,
+ SuccessWithWarnings,
+ }
+}
diff --git a/Assets/_Project/Scripts/Core/Enums.cs.meta b/Assets/_Project/Scripts/Core/Enums.cs.meta
new file mode 100644
index 0000000..6bb5af3
--- /dev/null
+++ b/Assets/_Project/Scripts/Core/Enums.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 7dd18da945084fc4fb2b403cf1557515
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Core/PlayerColors.cs b/Assets/_Project/Scripts/Core/PlayerColors.cs
new file mode 100644
index 0000000..d220f7f
--- /dev/null
+++ b/Assets/_Project/Scripts/Core/PlayerColors.cs
@@ -0,0 +1,82 @@
+using UnityEngine;
+
+namespace TD.Core
+{
+ ///
+ /// Canonical color palette for player slots, used by editor gizmos to color volumes by owner.
+ ///
+ ///
+ /// Color names follow the 9-player map design doc (red, green, blue, purple, yellow, gray,
+ /// teal, olive, dark gray). The exact RGB values have been tuned for readability when drawn
+ /// as translucent gizmos against Unity's default scene-view background. Tweak the constants
+ /// below if any color reads poorly in practice.
+ ///
+ /// The "ErrorPink" color is reserved for diagnostic use: if a volume's owner is
+ /// (which should never happen on a valid volume), the gizmo
+ /// renders in error pink to make the bug obvious.
+ ///
+ public static class PlayerColors
+ {
+ // Player colors. Hex values are RGB; alpha is set per-gizmo at draw time.
+ // Values are tuned to be saturated enough to read against Unity's default scene background.
+ private static readonly Color Player1Red = HexRGB(0xE0, 0x3A, 0x3A); // red
+ private static readonly Color Player2Green = HexRGB(0x3A, 0xC0, 0x4A); // green
+ private static readonly Color Player3Blue = HexRGB(0x3A, 0x7A, 0xE0); // blue
+ private static readonly Color Player4Purple = HexRGB(0xA0, 0x4A, 0xC0); // purple
+ private static readonly Color Player5Yellow = HexRGB(0xE0, 0xC8, 0x3A); // yellow
+ private static readonly Color Player6Gray = HexRGB(0xB0, 0xB0, 0xB8); // gray (slightly cool)
+ private static readonly Color Player7Teal = HexRGB(0x3A, 0xC0, 0xB8); // teal
+ private static readonly Color Player8Olive = HexRGB(0x9A, 0x9A, 0x3A); // olive
+ private static readonly Color Player9DarkGray = HexRGB(0x60, 0x60, 0x68); // dark gray (slightly cool)
+
+ // Non-player colors.
+ private static readonly Color GoalGold = HexRGB(0xE0, 0xB0, 0x20); // gold
+ private static readonly Color ErrorPink = HexRGB(0xFF, 0x4A, 0xC8); // diagnostic
+
+ ///
+ /// Returns the canonical color for a player slot. Returns the diagnostic error pink if
+ /// is or out of range.
+ ///
+ /// The player slot to look up.
+ public static Color Get(PlayerSlot slot)
+ {
+ switch (slot)
+ {
+ case PlayerSlot.Player1: return Player1Red;
+ case PlayerSlot.Player2: return Player2Green;
+ case PlayerSlot.Player3: return Player3Blue;
+ case PlayerSlot.Player4: return Player4Purple;
+ case PlayerSlot.Player5: return Player5Yellow;
+ case PlayerSlot.Player6: return Player6Gray;
+ case PlayerSlot.Player7: return Player7Teal;
+ case PlayerSlot.Player8: return Player8Olive;
+ case PlayerSlot.Player9: return Player9DarkGray;
+ case PlayerSlot.None:
+ default:
+ return ErrorPink;
+ }
+ }
+
+ /// The canonical color used for goal volumes. Not tied to any player.
+ public static Color Goal => GoalGold;
+
+ /// Diagnostic color used when a volume has an invalid/missing owner.
+ public static Color Error => ErrorPink;
+
+ ///
+ /// Returns a copy of with its alpha channel replaced by
+ /// . Convenience for translucent gizmo fills.
+ ///
+ public static Color WithAlpha(Color color, float alpha)
+ {
+ color.a = alpha;
+ return color;
+ }
+
+ // Helper: build a Color from 0-255 RGB byte channels (alpha defaults to 1.0).
+ private static Color HexRGB(byte r, byte g, byte b)
+ {
+ return new Color(r / 255f, g / 255f, b / 255f, 1f);
+ }
+ }
+}
diff --git a/Assets/_Project/Scripts/Core/PlayerColors.cs.meta b/Assets/_Project/Scripts/Core/PlayerColors.cs.meta
new file mode 100644
index 0000000..9d60a92
--- /dev/null
+++ b/Assets/_Project/Scripts/Core/PlayerColors.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 7d03e9552345e7c4098b141ac5f587e2
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Editor.meta b/Assets/_Project/Scripts/Editor.meta
new file mode 100644
index 0000000..1a93ffc
--- /dev/null
+++ b/Assets/_Project/Scripts/Editor.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: a007a87be458dd34da6bc808ea5a322c
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Scripts/Editor/Levels.meta b/Assets/_Project/Scripts/Editor/Levels.meta
new file mode 100644
index 0000000..acd5c11
--- /dev/null
+++ b/Assets/_Project/Scripts/Editor/Levels.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 5495ed4dd0ad5e1479790b6199648b5f
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant: