diff --git a/Assets/_Project/Constants/Defense/DefenseConstants.asset b/Assets/_Project/Constants/Defense/DefenseConstants.asset index 7715abf..1f05329 100644 --- a/Assets/_Project/Constants/Defense/DefenseConstants.asset +++ b/Assets/_Project/Constants/Defense/DefenseConstants.asset @@ -40,12 +40,12 @@ MonoBehaviour: - {x: 9, y: 0} - {x: 9, y: 0} BuildPadPositions: - - {x: -3.25, y: 3.25} - - {x: 3.25, y: -3.25} - - {x: -3.25, y: -3.25} - - {x: 3.25, y: 3.25} - - {x: 0, y: 3.25} - - {x: 0, y: -3.25} + - {x: -3.25, y: 4.9} + - {x: 3.25, y: -4.9} + - {x: -3.25, y: -4.9} + - {x: 3.25, y: 4.9} + - {x: 0, y: 4.9} + - {x: 0, y: -4.9} WaypointPositions: - {x: -3, y: 3.25} - {x: 3, y: 3.25} diff --git a/Assets/_Project/Scripts/Constants/DefenseConstants.cs b/Assets/_Project/Scripts/Constants/DefenseConstants.cs index 2494abe..e79b91f 100644 --- a/Assets/_Project/Scripts/Constants/DefenseConstants.cs +++ b/Assets/_Project/Scripts/Constants/DefenseConstants.cs @@ -43,14 +43,15 @@ namespace BumpCombat.Constants new(9f, 0f), new(9f, 0f), }; + [Tooltip("Build-only branches sit outside the enemy travel lanes.")] public Vector2[] BuildPadPositions = { - new(-3.25f, 3.25f), - new(3.25f, -3.25f), - new(-3.25f, -3.25f), - new(3.25f, 3.25f), - new(0f, 3.25f), - new(0f, -3.25f), + new(-3.25f, 4.9f), + new(3.25f, -4.9f), + new(-3.25f, -4.9f), + new(3.25f, 4.9f), + new(0f, 4.9f), + new(0f, -4.9f), }; [Tooltip("Two ordered waypoints per lane, indexed by lane * 2 + waypoint.")] public Vector2[] WaypointPositions = diff --git a/Assets/_Project/Scripts/Defense/DefensePrototypeController.cs b/Assets/_Project/Scripts/Defense/DefensePrototypeController.cs index 6a1ef30..0860878 100644 --- a/Assets/_Project/Scripts/Defense/DefensePrototypeController.cs +++ b/Assets/_Project/Scripts/Defense/DefensePrototypeController.cs @@ -964,6 +964,19 @@ namespace BumpCombat.Defense CreateLaneLine(lane, portal, tuning.GetWaypointPosition(lane, 0)); CreateLaneLine(lane, tuning.GetWaypointPosition(lane, 0), tuning.GetWaypointPosition(lane, 1)); CreateLaneLine(lane, tuning.GetWaypointPosition(lane, 1), basePosition); + + int[] padOrder = lane == 0 + ? new[] { 3, 4, 0 } : new[] { 1, 5, 2 }; + Vector2 branchCorner = new( + basePosition.x, tuning.GetBuildPadPosition(padOrder[0]).y); + CreateLaneLine(lane, basePosition, branchCorner, true, 1); + Vector2 branchPoint = branchCorner; + for (int i = 0; i < padOrder.Length; i++) + { + Vector2 padPosition = tuning.GetBuildPadPosition(padOrder[i]); + CreateLaneLine(lane, branchPoint, padPosition, true, i + 2); + branchPoint = padPosition; + } CreateShape( $"Portal {lane + 1}", portal, @@ -999,7 +1012,8 @@ namespace BumpCombat.Defense 3); CreateWorldLabel( $"Build Pad {pad + 1} Label", - position + Vector2.down * 0.95f, + position + (position.y >= tuning.ArenaCenter.y + ? Vector2.down : Vector2.up) * 0.95f, "PAD " + (pad + 1)); } } @@ -1060,21 +1074,29 @@ namespace BumpCombat.Defense + upgrade; } - private void CreateLaneLine(int lane, Vector2 from, Vector2 to) + private void CreateLaneLine( + int lane, Vector2 from, Vector2 to, + bool padBranch = false, int segment = 0) { - GameObject lineObject = new($"Lane {lane + 1} Segment"); + string name = padBranch + ? $"Pad Branch {lane + 1} Segment {segment}" + : $"Lane {lane + 1} Segment"; + GameObject lineObject = new(name); lineObject.transform.SetParent(worldRoot.transform, false); LineRenderer line = lineObject.AddComponent(); line.useWorldSpace = true; line.positionCount = 2; line.SetPosition(0, from); line.SetPosition(1, to); - line.startWidth = 0.08f; - line.endWidth = 0.08f; - line.startColor = new Color32(108, 151, 177, 210); - line.endColor = new Color32(108, 151, 177, 210); + line.startWidth = padBranch ? 0.12f : 0.08f; + line.endWidth = line.startWidth; + Color32 color = padBranch + ? new Color32(91, 206, 160, 225) + : new Color32(108, 151, 177, 210); + line.startColor = color; + line.endColor = color; line.material = new Material(Shader.Find("Sprites/Default")); - line.sortingOrder = 1; + line.sortingOrder = padBranch ? 2 : 1; } private GameObject CreateShape( diff --git a/Assets/_Project/Tests/PlayMode/DefensePrototypePlayModeTests.cs b/Assets/_Project/Tests/PlayMode/DefensePrototypePlayModeTests.cs index 8f53fc3..9e04104 100644 --- a/Assets/_Project/Tests/PlayMode/DefensePrototypePlayModeTests.cs +++ b/Assets/_Project/Tests/PlayMode/DefensePrototypePlayModeTests.cs @@ -151,6 +151,50 @@ namespace BumpCombat.PlayModeTests } } + [UnityTest] + public IEnumerator DefensePrototype_PadBranchesStaySeparateFromEnemyRoutes() + { + yield return StartDefenseRun(); + DefensePrototypeController controller = DefensePrototypeController.Instance; + DefenseConstants tuning = controller.Tuning; + Vector2 home = tuning.GetBasePosition(0); + Assert.That(tuning.GetBasePosition(1), Is.EqualTo(home)); + for (int pad = 0; pad < 6; pad++) + { + Vector2 position = tuning.GetBuildPadPosition(pad); + float distance = float.MaxValue; + for (int lane = 0; lane < 2; lane++) + { + Vector2 previous = tuning.GetPortalPosition(lane); + for (int waypoint = 0; waypoint < 3; waypoint++) + { + Vector2 next = waypoint < 2 + ? tuning.GetWaypointPosition(lane, waypoint) + : tuning.GetBasePosition(lane); + distance = Mathf.Min(distance, + DistanceToSegment(position, previous, next)); + previous = next; + } + } + Assert.That(distance, Is.GreaterThan(tuning.BuildInteractionRadius), + $"Pad {pad + 1} must not sit on an enemy route."); + } + + for (int lane = 0; lane < 2; lane++) + { + GameObject branch = GameObject.Find( + $"Pad Branch {lane + 1} Segment 1"); + Assert.That(branch, Is.Not.Null, + "Each build branch must be visible separately from enemy routes."); + LineRenderer line = branch.GetComponent(); + Assert.That(line, Is.Not.Null); + Assert.That((Vector2)line.GetPosition(0), Is.EqualTo(home)); + float padY = tuning.GetBuildPadPosition(lane).y; + Assert.That((Vector2)line.GetPosition(1), + Is.EqualTo(new Vector2(home.x, padY))); + } + } + [UnityTest] public IEnumerator DefensePrototype_DefaultBuildsCanDamageTheirPortalLanes() { @@ -637,7 +681,7 @@ namespace BumpCombat.PlayModeTests Assert.That(controller.CanOperateTowers, Is.True); Assert.That(playerHealth.CurrentHealth, Is.EqualTo(playerHealth.MaxHealth)); - MovePlayerNear(player, Vector2.zero); + MovePlayerNear(player, new Vector2(10f, -5f)); SaveDefenseConstants(controller.Tuning); touchedDefenseConstants.PortalSpawnInterval = 0.01f; @@ -653,6 +697,7 @@ namespace BumpCombat.PlayModeTests yield return DriveArrivalsUntilBaseDestroyed(controller); Assert.That(controller.GetBaseHealth(0), Is.EqualTo(0)); Assert.That(RunManager.Instance.IsGameOver, Is.True); + SetTestField(playerHealth, "invulnerableUntil", Time.time); Assert.That(playerHealth.TryTakeDamage( playerHealth.MaxHealth + 1f, Vector2.left, 0f), Is.True); @@ -1986,6 +2031,17 @@ namespace BumpCombat.PlayModeTests Assert.That(runManager.IsSelectionOpen, Is.False); } + private static float DistanceToSegment( + Vector2 point, Vector2 from, Vector2 to) + { + Vector2 segment = to - from; + float fraction = segment.sqrMagnitude > 0f + ? Mathf.Clamp01(Vector2.Dot(point - from, segment) + / segment.sqrMagnitude) + : 0f; + return Vector2.Distance(point, from + segment * fraction); + } + private static DefenseTower BuildTower( DefensePrototypeController controller, PlayerController player, @@ -2159,7 +2215,10 @@ namespace BumpCombat.PlayModeTests } Assert.That(agent.HasArrived, Is.True, - "A public route agent must reach the shared base when driven through its route."); + $"A public route agent must reach the shared base when driven through its route. " + + $"Lane={agent.Lane}, position={agent.transform.position}, " + + $"state={agent.Enemy?.State}, dead={agent.Enemy?.IsDead}, " + + $"stunned={agent.Enemy?.IsStunned}, speed={agent.Enemy?.MovementSpeedMultiplier}."); } private static IEnumerator DriveArrivalsUntilBaseDestroyed( @@ -2175,6 +2234,7 @@ namespace BumpCombat.PlayModeTests { if (agents[i] != null && !agents[i].HasArrived) { + yield return WaitForAgentReady(agents[i]); DriveAgentToArrival(agents[i]); if (controller.GetBaseHealth(0) <= 0) {