Adding big batch of structural code provided by Claude to start designing levels

This commit is contained in:
Matt F 2026-04-27 22:55:23 -07:00
parent 0ed4df8bc9
commit a4e28bc93f
26 changed files with 1951 additions and 0 deletions

View 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;
}
}
}