131 lines
No EOL
5.3 KiB
C#
131 lines
No EOL
5.3 KiB
C#
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;
|
||
}
|
||
}
|
||
} |