blink moves forward a short number of meters, not along waypoint boundaries

This commit is contained in:
Ian Woods 2026-08-04 21:03:54 -07:00
parent dc9184075b
commit f4a0dc57c3
3 changed files with 52 additions and 22 deletions

View file

@ -15,5 +15,5 @@ MonoBehaviour:
DisplayName: Blink
Description: Enemies occasionally teleport a short distance ahead.
IntervalSeconds: 4
BlinkWaypoints: 2
BlinkDistanceMeters: 4
StartJitterSeconds: 1.5

View file

@ -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<EnemyMovement>()?.ServerBlinkForward(BlinkWaypoints);
instance.GetComponent<EnemyMovement>()?.ServerBlinkForward(BlinkDistanceMeters);
}
}
}

View file

@ -286,39 +286,70 @@ namespace TD.Gameplay
/// <summary>
/// Server-only: teleport this enemy forward along its existing path by up to
/// <paramref name="tiles"/> waypoints. Used by the Blink wave buff.
/// <paramref name="meters"/> world units. Used by the Blink wave buff.
/// </summary>
/// <remarks>
/// <b>Follows the path rather than the straight line to the goal.</b> 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.
///
/// <para>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.</para>
/// <para>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.</para>
///
/// <para>Blinking onto the final waypoint resolves as a normal goal arrival, despawn and
/// life cost included.</para>
/// <para>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.</para>
///
/// <para>Blinking onto (or past) the final waypoint resolves as a normal goal arrival,
/// despawn and life cost included.</para>
/// </remarks>
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 ----------------------------------------------