Adding big batch of structural code provided by Claude to start designing levels
This commit is contained in:
parent
0ed4df8bc9
commit
a4e28bc93f
26 changed files with 1951 additions and 0 deletions
BIN
Assets/New Terrain.asset
Normal file
BIN
Assets/New Terrain.asset
Normal file
Binary file not shown.
8
Assets/New Terrain.asset.meta
Normal file
8
Assets/New Terrain.asset.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e75cb6747dd151148b0f1bf06123e9cf
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 15600000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Assets/_Project/Levels.meta
Normal file
8
Assets/_Project/Levels.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ae52c3dd5edc3ae4da7d4129059d3637
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
66
Assets/_Project/Levels/GoalVolume.cs
Normal file
66
Assets/_Project/Levels/GoalVolume.cs
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
using UnityEngine;
|
||||||
|
using TD.Core;
|
||||||
|
|
||||||
|
#if UNITY_EDITOR
|
||||||
|
using UnityEditor;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace TD.Levels
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Authoring volume marking the win-condition target. Enemies that reach a goal volume reduce
|
||||||
|
/// the shared player life pool. Goal tiles are <see cref="PlacementState.Restricted"/> in the
|
||||||
|
/// baked grid but remain walkable so enemies can enter them.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Goals have no ownership — they are shared across all players. Multiple goal volumes are
|
||||||
|
/// allowed; the designer specifies the expected count via
|
||||||
|
/// <see cref="LevelAuthoring.expectedGoalCount"/> 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.
|
||||||
|
/// </remarks>
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/_Project/Levels/GoalVolume.cs.meta
Normal file
2
Assets/_Project/Levels/GoalVolume.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 755948f0244afea4da6ed2e4e3d27579
|
||||||
131
Assets/_Project/Levels/LeakExitVolume.cs
Normal file
131
Assets/_Project/Levels/LeakExitVolume.cs
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
using UnityEngine;
|
||||||
|
using TD.Core;
|
||||||
|
|
||||||
|
#if UNITY_EDITOR
|
||||||
|
using UnityEditor;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace TD.Levels
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Authoring volume marking the boundary where enemies leak from one player's zone into
|
||||||
|
/// another's. Leak exit tiles are <see cref="PlacementState.Restricted"/> in the baked grid
|
||||||
|
/// but remain walkable.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Leak topology is recorded as metadata in the baked <see cref="LevelData"/> 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 <see cref="weight"/> field
|
||||||
|
/// controls the split. The bake normalizes weights to sum to 1.0 across a source zone's
|
||||||
|
/// leak exits.
|
||||||
|
/// </remarks>
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Computes the world-space center of the target zone by averaging the bounds-centers
|
||||||
|
/// of all PlayerZoneVolumes whose <see cref="PlayerZoneVolume.owner"/> matches
|
||||||
|
/// <see cref="target"/>. Returns <c>Vector3.zero</c> if no matching zone is found —
|
||||||
|
/// the caller treats zero as a "not found" sentinel.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 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.
|
||||||
|
/// </remarks>
|
||||||
|
private Vector3 FindTargetZoneCenter()
|
||||||
|
{
|
||||||
|
var zones = Object.FindObjectsByType<PlayerZoneVolume>(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<BoxCollider>();
|
||||||
|
if (c == null) continue;
|
||||||
|
|
||||||
|
accumulated += c.bounds.center;
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count == 0) return Vector3.zero;
|
||||||
|
return accumulated / count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/_Project/Levels/LeakExitVolume.cs.meta
Normal file
2
Assets/_Project/Levels/LeakExitVolume.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d4da5d6673a12f546ac47563abc49d5a
|
||||||
258
Assets/_Project/Levels/LevelAuthoring.cs
Normal file
258
Assets/_Project/Levels/LevelAuthoring.cs
Normal file
|
|
@ -0,0 +1,258 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
using TD.Core;
|
||||||
|
|
||||||
|
#if UNITY_EDITOR
|
||||||
|
using UnityEditor;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace TD.Levels
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Scene-level coordinator for level authoring. One per scene, attached to an empty
|
||||||
|
/// GameObject named <c>_LevelAuthoring</c> whose subtree contains all the volumes for the map.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 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
|
||||||
|
/// <see cref="targetAsset"/> 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.
|
||||||
|
/// </remarks>
|
||||||
|
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).
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs the seven-phase bake algorithm and writes the result into <see cref="targetAsset"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>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.</returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// STUB — full implementation is the next session's work. Currently logs a not-implemented
|
||||||
|
/// message and returns false.
|
||||||
|
/// </remarks>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Re-renders just the lobby thumbnail without doing a full bake. Useful when only visual
|
||||||
|
/// scene content (terrain, decorations) has changed.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// STUB — full implementation is part of the bake script work. Currently logs a
|
||||||
|
/// not-implemented message.
|
||||||
|
/// </remarks>
|
||||||
|
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<VolumeBase>(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<BoxCollider>() : 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<PlayerZoneVolume>(includeInactive: false);
|
||||||
|
if (zones == null || zones.Length == 0) return;
|
||||||
|
|
||||||
|
// Build per-owner tile sets.
|
||||||
|
var perOwner = new Dictionary<PlayerSlot, HashSet<Vector2Int>>();
|
||||||
|
for (int i = 0; i < zones.Length; i++)
|
||||||
|
{
|
||||||
|
var z = zones[i];
|
||||||
|
if (z == null) continue;
|
||||||
|
|
||||||
|
var col = z.GetComponent<BoxCollider>();
|
||||||
|
if (col == null) continue;
|
||||||
|
|
||||||
|
if (!perOwner.TryGetValue(z.owner, out var set))
|
||||||
|
{
|
||||||
|
set = new HashSet<Vector2Int>();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/_Project/Levels/LevelAuthoring.cs.meta
Normal file
2
Assets/_Project/Levels/LevelAuthoring.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 37de128911d7bbc4299ea87cdb520ed7
|
||||||
144
Assets/_Project/Levels/LevelData.cs
Normal file
144
Assets/_Project/Levels/LevelData.cs
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
using System;
|
||||||
|
using UnityEngine;
|
||||||
|
using TD.Core;
|
||||||
|
|
||||||
|
namespace TD.Levels
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Baked level data. Authored in a Unity scene via volume MonoBehaviours; produced by
|
||||||
|
/// <see cref="LevelAuthoring.BakeLevelData"/>; consumed at runtime as the canonical map
|
||||||
|
/// definition.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// LevelData is the canonical map identity. The lobby browses LevelData assets; loading a
|
||||||
|
/// map = read LevelData → load the referenced scene at <see cref="ScenePath"/>.
|
||||||
|
///
|
||||||
|
/// All flat grid arrays use row-major indexing: <c>grid[y * GridSize.x + x]</c>.
|
||||||
|
/// Origin-relative: <see cref="GridOriginTile"/> is the world-tile coordinate that grid index
|
||||||
|
/// (0,0) corresponds to. Convert world-tile to grid-index via
|
||||||
|
/// <c>worldTile - GridOriginTile</c>.
|
||||||
|
/// </remarks>
|
||||||
|
[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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Per-zone baked data. The runtime can use this to enumerate a player's spawners and leak
|
||||||
|
/// exits without scanning the full grid.
|
||||||
|
/// </summary>
|
||||||
|
[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;
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/_Project/Levels/LevelData.cs.meta
Normal file
2
Assets/_Project/Levels/LevelData.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 6d4e9c37b9205f3408a8225823f7a4da
|
||||||
68
Assets/_Project/Levels/PlayerZoneVolume.cs
Normal file
68
Assets/_Project/Levels/PlayerZoneVolume.cs
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
using UnityEngine;
|
||||||
|
using TD.Core;
|
||||||
|
|
||||||
|
#if UNITY_EDITOR
|
||||||
|
using UnityEditor;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace TD.Levels
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Authoring volume that defines a player's owned territory. Tiles covered by a
|
||||||
|
/// PlayerZoneVolume are <see cref="PlacementState.Buildable"/> in the baked grid (unless
|
||||||
|
/// covered by an Invalid-validity volume — Invalid wins).
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// A single player's zone may consist of multiple PlayerZoneVolume instances with the same
|
||||||
|
/// <see cref="owner"/>; the bake unions their tile coverage. This supports L-shapes and other
|
||||||
|
/// non-rectangular zone footprints.
|
||||||
|
/// </remarks>
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/_Project/Levels/PlayerZoneVolume.cs.meta
Normal file
2
Assets/_Project/Levels/PlayerZoneVolume.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5e8ab74b9038bd64782043bfc21f4cd7
|
||||||
118
Assets/_Project/Levels/SpawnerVolume.cs
Normal file
118
Assets/_Project/Levels/SpawnerVolume.cs
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
using UnityEngine;
|
||||||
|
using TD.Core;
|
||||||
|
|
||||||
|
#if UNITY_EDITOR
|
||||||
|
using UnityEditor;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace TD.Levels
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Authoring volume marking where enemies spawn into a player's zone. Spawner tiles are
|
||||||
|
/// <see cref="PlacementState.Restricted"/> in the baked grid (no tower placement allowed)
|
||||||
|
/// but remain walkable so enemies can leave the spawner.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// A zone may have multiple spawners; <see cref="spawnerIdInZone"/> disambiguates them.
|
||||||
|
/// Player 5 in the 9-player Wintermaul map is the canonical multi-spawner zone.
|
||||||
|
///
|
||||||
|
/// The spawner declares an owning player via <see cref="owner"/> 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.
|
||||||
|
/// </remarks>
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns true if more than one active SpawnerVolume in the scene shares this spawner's
|
||||||
|
/// <see cref="owner"/>. Used to decide whether to suffix the gizmo label with the spawner ID.
|
||||||
|
/// </summary>
|
||||||
|
private bool ZoneHasMultipleSpawners()
|
||||||
|
{
|
||||||
|
var spawners = Object.FindObjectsByType<SpawnerVolume>(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/_Project/Levels/SpawnerVolume.cs.meta
Normal file
2
Assets/_Project/Levels/SpawnerVolume.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: a756762f899008f45b2986bdcb11c819
|
||||||
319
Assets/_Project/Levels/VolumeBase.cs
Normal file
319
Assets/_Project/Levels/VolumeBase.cs
Normal file
|
|
@ -0,0 +1,319 @@
|
||||||
|
using UnityEngine;
|
||||||
|
using TD.Core;
|
||||||
|
|
||||||
|
namespace TD.Levels
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Abstract base class for all level authoring volumes (player zones, spawners, leak exits, goals).
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Every concrete volume requires a <see cref="BoxCollider"/>. The collider's bounds (in world
|
||||||
|
/// space) define the volume's spatial extent, which the bake script rasterizes to tiles via
|
||||||
|
/// <see cref="BoxCollider.bounds"/> and <c>bounds.Contains(tileCenter)</c>.
|
||||||
|
///
|
||||||
|
/// Volumes are authoring-time artifacts only — they are NOT part of the runtime gameplay scene.
|
||||||
|
/// At runtime the bake's output (<see cref="LevelData"/>) is the sole source of truth.
|
||||||
|
///
|
||||||
|
/// Visibility model: gizmos draw via <c>OnDrawGizmosSelected</c> by default (selection-only).
|
||||||
|
/// The owning <see cref="LevelAuthoring"/> exposes per-type toggles to make a category always-on.
|
||||||
|
/// Volumes not parented under a <c>LevelAuthoring</c> (orphans) always behave as selection-only;
|
||||||
|
/// they will also produce warnings during bake.
|
||||||
|
/// </remarks>
|
||||||
|
[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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the volume's <see cref="BoxCollider"/>, looking it up lazily. Returns null if the
|
||||||
|
/// component has been removed (which violates <c>[RequireComponent]</c> but can happen if
|
||||||
|
/// a user manually removes it).
|
||||||
|
/// </summary>
|
||||||
|
protected BoxCollider Collider
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_cachedCollider == null)
|
||||||
|
{
|
||||||
|
_cachedCollider = GetComponent<BoxCollider>();
|
||||||
|
}
|
||||||
|
return _cachedCollider;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Walks the parent transform hierarchy to find the owning <see cref="LevelAuthoring"/>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
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<LevelAuthoring>(includeInactive: true);
|
||||||
|
return _cachedAuthoring;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When implemented by a subclass, returns whether the corresponding "always show" toggle
|
||||||
|
/// is enabled on the supplied <see cref="LevelAuthoring"/>. Each volume type maps to a
|
||||||
|
/// different toggle field.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="authoring">The owning LevelAuthoring (guaranteed non-null).</param>
|
||||||
|
protected abstract bool GetAlwaysShowToggle(LevelAuthoring authoring);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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).
|
||||||
|
/// </summary>
|
||||||
|
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.
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Iterates every tile whose center lies inside <paramref name="bounds"/> and invokes
|
||||||
|
/// <paramref name="onTile"/> for each one. The candidate range is computed via
|
||||||
|
/// <see cref="GridCoordinates.WorldToGrid"/> 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
|
||||||
|
/// <c>bounds.Contains(tileCenter)</c> test inside the loop guarantees correctness.
|
||||||
|
/// </summary>
|
||||||
|
public static void RasterizeBoundsToTiles(Bounds bounds, System.Action<Vector2Int> 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Computes the tight tile rectangle for a <see cref="Bounds"/> — the rectangle that
|
||||||
|
/// exactly contains every tile whose center is inside the bounds. Returns false if no
|
||||||
|
/// tile centers fall inside the bounds.
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Instance-side convenience: computes the tight tile rectangle for THIS volume's collider.
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="fillColor">Fully-opaque color; alpha will be applied via <paramref name="alpha"/>.</param>
|
||||||
|
/// <param name="alpha">Alpha 0..1 applied to the fill.</param>
|
||||||
|
/// <param name="yLevel">World Y at which to draw the fill. Stagger by volume type to avoid z-fighting.</param>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="outlineColor">Outline color (alpha applied as supplied).</param>
|
||||||
|
/// <param name="yLevel">World Y at which to draw the outline.</param>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Draws a line+cone arrow from <paramref name="origin"/> in the given world-space
|
||||||
|
/// direction. The arrow shaft is drawn with <see cref="Gizmos.DrawLine"/> and the
|
||||||
|
/// arrowhead is built from four lines forming a small cone.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="origin">Arrow start point (world space).</param>
|
||||||
|
/// <param name="direction">Unit-length direction the arrow points in.</param>
|
||||||
|
/// <param name="length">Total arrow length in world units.</param>
|
||||||
|
/// <param name="color">Arrow color (alpha applied as supplied).</param>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a <see cref="Direction"/> 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).
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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).
|
||||||
|
/// </summary>
|
||||||
|
public static string FormatPlayerName(PlayerSlot slot)
|
||||||
|
{
|
||||||
|
if (slot == PlayerSlot.None) return "Player ?";
|
||||||
|
return "Player " + ((byte)slot).ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/_Project/Levels/VolumeBase.cs.meta
Normal file
2
Assets/_Project/Levels/VolumeBase.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c95a277ca66b4a34e8de9e5ee420f145
|
||||||
|
|
@ -119,6 +119,74 @@ NavMeshSettings:
|
||||||
debug:
|
debug:
|
||||||
m_Flags: 0
|
m_Flags: 0
|
||||||
m_NavMeshData: {fileID: 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
|
--- !u!1 &239104687
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
@ -191,6 +259,76 @@ Transform:
|
||||||
m_Children: []
|
m_Children: []
|
||||||
m_Father: {fileID: 0}
|
m_Father: {fileID: 0}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 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
|
--- !u!1 &330585543
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
@ -455,6 +593,66 @@ MonoBehaviour:
|
||||||
m_ShadowLayerMask: 1
|
m_ShadowLayerMask: 1
|
||||||
m_RenderingLayers: 1
|
m_RenderingLayers: 1
|
||||||
m_ShadowRenderingLayers: 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
|
--- !u!1 &832575517
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
@ -504,6 +702,326 @@ Transform:
|
||||||
m_Children: []
|
m_Children: []
|
||||||
m_Father: {fileID: 0}
|
m_Father: {fileID: 0}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 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
|
--- !u!1 &1682341399
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
@ -610,6 +1128,74 @@ Transform:
|
||||||
m_Children: []
|
m_Children: []
|
||||||
m_Father: {fileID: 0}
|
m_Father: {fileID: 0}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 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
|
--- !u!1660057539 &9223372036854775807
|
||||||
SceneRoots:
|
SceneRoots:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
@ -619,3 +1205,5 @@ SceneRoots:
|
||||||
- {fileID: 832575519}
|
- {fileID: 832575519}
|
||||||
- {fileID: 1682341402}
|
- {fileID: 1682341402}
|
||||||
- {fileID: 239104690}
|
- {fileID: 239104690}
|
||||||
|
- {fileID: 441239881}
|
||||||
|
- {fileID: 1464027364}
|
||||||
|
|
|
||||||
31
Assets/_Project/Scenes/Levels/TestLevel.asset
Normal file
31
Assets/_Project/Scenes/Levels/TestLevel.asset
Normal file
|
|
@ -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: []
|
||||||
8
Assets/_Project/Scenes/Levels/TestLevel.asset.meta
Normal file
8
Assets/_Project/Scenes/Levels/TestLevel.asset.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 9cc56fbc3ae460a4b862f8510fdf5f09
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
88
Assets/_Project/Scripts/Core/Enums.cs
Normal file
88
Assets/_Project/Scripts/Core/Enums.cs
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
namespace TD.Core
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Identifies a player slot in a match. Backed by byte to keep grid arrays compact.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <c>None</c> is a sentinel value used in <c>OwnerGrid</c> 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).
|
||||||
|
/// </remarks>
|
||||||
|
public enum PlayerSlot : byte
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
Player1 = 1,
|
||||||
|
Player2 = 2,
|
||||||
|
Player3 = 3,
|
||||||
|
Player4 = 4,
|
||||||
|
Player5 = 5,
|
||||||
|
Player6 = 6,
|
||||||
|
Player7 = 7,
|
||||||
|
Player8 = 8,
|
||||||
|
Player9 = 9,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether a volume permits tower placement on the tiles it covers.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Defaults: <c>Allowed</c> for <see cref="TD.Levels.PlayerZoneVolume"/>, <c>Invalid</c> for
|
||||||
|
/// <see cref="TD.Levels.SpawnerVolume"/>, <see cref="TD.Levels.LeakExitVolume"/>, and
|
||||||
|
/// <see cref="TD.Levels.GoalVolume"/>.
|
||||||
|
/// Composition rule when volumes overlap: "Invalid wins" — any tile covered by an Invalid volume
|
||||||
|
/// becomes <see cref="PlacementState.Restricted"/> regardless of other volumes covering it.
|
||||||
|
/// </remarks>
|
||||||
|
public enum PlacementValidity
|
||||||
|
{
|
||||||
|
Invalid = 0,
|
||||||
|
Allowed = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
public enum Direction
|
||||||
|
{
|
||||||
|
North,
|
||||||
|
South,
|
||||||
|
East,
|
||||||
|
West,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Per-tile placement state in the baked <c>PlacementGrid</c>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Backed by byte so default-initialized arrays (all zero) start as <c>Outside</c>, which is
|
||||||
|
/// the correct default for any tile not covered by an authoring volume.
|
||||||
|
/// </remarks>
|
||||||
|
public enum PlacementState : byte
|
||||||
|
{
|
||||||
|
/// <summary>Tile is not covered by any authoring volume. Towers cannot be placed.</summary>
|
||||||
|
Outside = 0,
|
||||||
|
|
||||||
|
/// <summary>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).</summary>
|
||||||
|
Buildable = 1,
|
||||||
|
|
||||||
|
/// <summary>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.</summary>
|
||||||
|
Restricted = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Outcome of a bake operation, recorded on the baked <see cref="TD.Levels.LevelData"/> asset.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 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.
|
||||||
|
/// </remarks>
|
||||||
|
public enum BakeOutcome
|
||||||
|
{
|
||||||
|
Success,
|
||||||
|
SuccessWithWarnings,
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/_Project/Scripts/Core/Enums.cs.meta
Normal file
2
Assets/_Project/Scripts/Core/Enums.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 7dd18da945084fc4fb2b403cf1557515
|
||||||
82
Assets/_Project/Scripts/Core/PlayerColors.cs
Normal file
82
Assets/_Project/Scripts/Core/PlayerColors.cs
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace TD.Core
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Canonical color palette for player slots, used by editor gizmos to color volumes by owner.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 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
|
||||||
|
/// <see cref="PlayerSlot.None"/> (which should never happen on a valid volume), the gizmo
|
||||||
|
/// renders in error pink to make the bug obvious.
|
||||||
|
/// </remarks>
|
||||||
|
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
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the canonical color for a player slot. Returns the diagnostic error pink if
|
||||||
|
/// <paramref name="slot"/> is <see cref="PlayerSlot.None"/> or out of range.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="slot">The player slot to look up.</param>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>The canonical color used for goal volumes. Not tied to any player.</summary>
|
||||||
|
public static Color Goal => GoalGold;
|
||||||
|
|
||||||
|
/// <summary>Diagnostic color used when a volume has an invalid/missing owner.</summary>
|
||||||
|
public static Color Error => ErrorPink;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a copy of <paramref name="color"/> with its alpha channel replaced by
|
||||||
|
/// <paramref name="alpha"/>. Convenience for translucent gizmo fills.
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/_Project/Scripts/Core/PlayerColors.cs.meta
Normal file
2
Assets/_Project/Scripts/Core/PlayerColors.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 7d03e9552345e7c4098b141ac5f587e2
|
||||||
8
Assets/_Project/Scripts/Editor.meta
Normal file
8
Assets/_Project/Scripts/Editor.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: a007a87be458dd34da6bc808ea5a322c
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Assets/_Project/Scripts/Editor/Levels.meta
Normal file
8
Assets/_Project/Scripts/Editor/Levels.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5495ed4dd0ad5e1479790b6199648b5f
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
Loading…
Add table
Add a link
Reference in a new issue