diff --git a/Assets/_Project/Definitions/RunDefinitions/EnemyAbilities/BlinkAbility.asset b/Assets/_Project/Definitions/RunDefinitions/EnemyAbilities/BlinkAbility.asset index 6cc7cf7..cba6d55 100644 --- a/Assets/_Project/Definitions/RunDefinitions/EnemyAbilities/BlinkAbility.asset +++ b/Assets/_Project/Definitions/RunDefinitions/EnemyAbilities/BlinkAbility.asset @@ -15,5 +15,5 @@ MonoBehaviour: DisplayName: Blink Description: Enemies occasionally teleport a short distance ahead. IntervalSeconds: 4 - BlinkWaypoints: 2 + BlinkDistanceMeters: 4 StartJitterSeconds: 1.5 diff --git a/Assets/_Project/Scripts/Gameplay/EnemyAbilities/BlinkAbilityDefinition.cs b/Assets/_Project/Scripts/Gameplay/EnemyAbilities/BlinkAbilityDefinition.cs index 236c8b7..14f76f6 100644 --- a/Assets/_Project/Scripts/Gameplay/EnemyAbilities/BlinkAbilityDefinition.cs +++ b/Assets/_Project/Scripts/Gameplay/EnemyAbilities/BlinkAbilityDefinition.cs @@ -27,10 +27,9 @@ namespace TD.Gameplay.EnemyAbilities [Min(0.1f)] public float IntervalSeconds = 4f; - [Tooltip("How many path waypoints to skip per blink. Note that path smoothing can make " + - "one waypoint span several tiles, so this is a coarser dial than it looks.")] - [Min(1)] - public int BlinkWaypoints = 2; + [Tooltip("World units to advance along the path per blink.")] + [Min(0.1f)] + public float BlinkDistanceMeters = 4f; [Tooltip("Random spread (seconds) added to each enemy's first blink so a wave doesn't " + "blink in one synchronized block. Applied once, at spawn.")] @@ -54,7 +53,7 @@ namespace TD.Gameplay.EnemyAbilities if (timer < IntervalSeconds) return; timer = 0f; - instance.GetComponent()?.ServerBlinkForward(BlinkWaypoints); + instance.GetComponent()?.ServerBlinkForward(BlinkDistanceMeters); } } } diff --git a/Assets/_Project/Scripts/Gameplay/EnemyMovement.cs b/Assets/_Project/Scripts/Gameplay/EnemyMovement.cs index 147eaf9..6815ff2 100644 --- a/Assets/_Project/Scripts/Gameplay/EnemyMovement.cs +++ b/Assets/_Project/Scripts/Gameplay/EnemyMovement.cs @@ -286,39 +286,70 @@ namespace TD.Gameplay /// /// Server-only: teleport this enemy forward along its existing path by up to - /// waypoints. Used by the Blink wave buff. + /// world units. Used by the Blink wave buff. /// /// /// Follows the path rather than the straight line to the goal. Jumping toward the /// goal as the crow flies would let a grounded enemy blink through walls, which is the - /// flying buff's job, not this one. Skipping waypoints keeps the enemy inside the maze — + /// flying buff's job, not this one. Walking the path keeps the enemy inside the maze — /// it cuts corners, it doesn't ignore them. /// - /// Zone tracking still runs on the destination tile, so an enemy that blinks out of - /// its origin zone still credits its owner a leak. Skipping that would make the buff a way - /// to launder leaks past the no-leak bonus. + /// Distance is measured along the path (waypoint to waypoint), consuming the + /// budget one segment at a time, so the landing spot can fall mid-segment rather than + /// snapping to a waypoint. This keeps the blink distance predictable regardless of how + /// far apart the path's waypoints happen to be — unlike skipping a fixed waypoint count, + /// where one blink could span a few tiles or the length of the whole map depending on + /// path smoothing. /// - /// Blinking onto the final waypoint resolves as a normal goal arrival, despawn and - /// life cost included. + /// Zone tracking runs once, on the final landing tile, so an enemy that blinks out + /// of its origin zone still credits its owner a leak. Skipping that would make the buff a + /// way to launder leaks past the no-leak bonus. + /// + /// Blinking onto (or past) the final waypoint resolves as a normal goal arrival, + /// despawn and life cost included. /// - public void ServerBlinkForward(int tiles) + public void ServerBlinkForward(float meters) { - if (!IsServer || tiles <= 0) return; + if (!IsServer || meters <= 0f) return; if (hasReachedGoal || remainingPath.Count == 0) return; if (health != null && health.IsHeld) return; - int skip = Mathf.Min(tiles, remainingPath.Count); - Vector2Int destination = remainingPath[skip - 1]; - remainingPath.RemoveRange(0, skip); + float remaining = meters; + Vector3 position = transform.position; + + while (remaining > 0f && remainingPath.Count > 0) + { + Vector3 targetWorld = GridCoordinates.GridToWorld(remainingPath[0]); + Vector3 toTarget = targetWorld - position; + toTarget.y = 0f; + float distance = toTarget.magnitude; + + if (distance <= remaining) + { + // Reach this waypoint fully and keep walking the leftover budget + // into the next segment. + remaining -= distance; + position = new Vector3(targetWorld.x, position.y, targetWorld.z); + remainingPath.RemoveAt(0); + } + else + { + // Budget runs out partway along this segment. + position += toTarget.normalized * remaining; + remaining = 0f; + } + } // Preserve Y so flyers stay at altitude and grounded pivots don't sink. - Vector3 world = GridCoordinates.GridToWorld(destination); - transform.position = new Vector3(world.x, transform.position.y, world.z); - - CheckZoneTransition(destination); + transform.position = position; if (remainingPath.Count == 0) + { HandleGoalReached(); + return; + } + + CheckZoneTransition(GridCoordinates.WorldToGrid(position)); } // ----- Path invalidation ----------------------------------------------