diff --git a/Assets/_Project/Definitions/RunDefinitions/EnemyAbilities/BlinkAbility.asset b/Assets/_Project/Definitions/RunDefinitions/EnemyAbilities/BlinkAbility.asset index ec1b7b5..6cc7cf7 100644 --- a/Assets/_Project/Definitions/RunDefinitions/EnemyAbilities/BlinkAbility.asset +++ b/Assets/_Project/Definitions/RunDefinitions/EnemyAbilities/BlinkAbility.asset @@ -14,5 +14,6 @@ MonoBehaviour: m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.EnemyAbilities.BlinkAbilityDefinition DisplayName: Blink Description: Enemies occasionally teleport a short distance ahead. - IntervalSeconds: 8 - BlinkDistanceMeters: 4 + IntervalSeconds: 4 + BlinkWaypoints: 2 + StartJitterSeconds: 1.5 diff --git a/Assets/_Project/Scripts/Gameplay/EnemyAbilities/BlinkAbilityDefinition.cs b/Assets/_Project/Scripts/Gameplay/EnemyAbilities/BlinkAbilityDefinition.cs index c00904c..236c8b7 100644 --- a/Assets/_Project/Scripts/Gameplay/EnemyAbilities/BlinkAbilityDefinition.cs +++ b/Assets/_Project/Scripts/Gameplay/EnemyAbilities/BlinkAbilityDefinition.cs @@ -25,22 +25,26 @@ namespace TD.Gameplay.EnemyAbilities [Header("Blink")] [Tooltip("Seconds between blinks.")] [Min(0.1f)] - public float IntervalSeconds = 8f; + public float IntervalSeconds = 4f; - [Tooltip("World units to advance along the path per blink.")] - [Min(0.1f)] - public float BlinkDistanceMeters = 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("Random spread (seconds) added to each enemy's first blink so a wave doesn't " + + "blink in one synchronized block. Applied once, at spawn.")] + [Min(0f)] + public float StartJitterSeconds = 1.5f; public override void ServerOnSpawn(EnemyAbility instance, int abilityIndex) { - // Seed each enemy's cooldown with a random point within the full interval, as if it - // spawned already partway through a cooldown. A small jitter window near zero would - // still let a burst of enemies spawned together sync up and blink as a visible cluster - // every cycle; spreading across the whole interval avoids that regardless of how - // IntervalSeconds is tuned. Done once, here, rather than on the first tick — a tick-time + // Seed each enemy's cooldown with a different negative offset so the wave's first + // blink is staggered. Done once, here, rather than on the first tick — a tick-time // check would have to distinguish "never started" from "just blinked", and both // states read as a zero timer. - instance.TimerFor(abilityIndex) = -Random.Range(0f, IntervalSeconds); + if (StartJitterSeconds > 0f) + instance.TimerFor(abilityIndex) = -Random.Range(0f, StartJitterSeconds); } public override void ServerTick(EnemyAbility instance, int abilityIndex, float dt) @@ -50,7 +54,7 @@ namespace TD.Gameplay.EnemyAbilities if (timer < IntervalSeconds) return; timer = 0f; - instance.GetComponent()?.ServerBlinkForward(BlinkDistanceMeters); + instance.GetComponent()?.ServerBlinkForward(BlinkWaypoints); } } } diff --git a/Assets/_Project/Scripts/Gameplay/EnemyMovement.cs b/Assets/_Project/Scripts/Gameplay/EnemyMovement.cs index 6815ff2..147eaf9 100644 --- a/Assets/_Project/Scripts/Gameplay/EnemyMovement.cs +++ b/Assets/_Project/Scripts/Gameplay/EnemyMovement.cs @@ -286,70 +286,39 @@ namespace TD.Gameplay /// /// Server-only: teleport this enemy forward along its existing path by up to - /// world units. Used by the Blink wave buff. + /// waypoints. 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. Walking the path keeps the enemy inside the maze — + /// flying buff's job, not this one. Skipping waypoints keeps the enemy inside the maze — /// it cuts corners, it doesn't ignore them. /// - /// 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. + /// 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. /// - /// 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. + /// Blinking onto the final waypoint resolves as a normal goal arrival, despawn and + /// life cost included. /// - public void ServerBlinkForward(float meters) + public void ServerBlinkForward(int tiles) { - if (!IsServer || meters <= 0f) return; + if (!IsServer || tiles <= 0) return; if (hasReachedGoal || remainingPath.Count == 0) return; if (health != null && health.IsHeld) return; - 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; - } - } + int skip = Mathf.Min(tiles, remainingPath.Count); + Vector2Int destination = remainingPath[skip - 1]; + remainingPath.RemoveRange(0, skip); // Preserve Y so flyers stay at altitude and grounded pivots don't sink. - transform.position = position; + Vector3 world = GridCoordinates.GridToWorld(destination); + transform.position = new Vector3(world.x, transform.position.y, world.z); + + CheckZoneTransition(destination); if (remainingPath.Count == 0) - { HandleGoalReached(); - return; - } - - CheckZoneTransition(GridCoordinates.WorldToGrid(position)); } // ----- Path invalidation ----------------------------------------------