#109: garrison vessels wander their post instead of walking a ring

Two things were wrong and the second was the real one. A ring is not what a
picket does - but each step around it was shorter than Ship's arrive_radius,
so every frame the vessel SNAPPED onto its target and zeroed its velocity,
and Ship only turns to face a heading while it is actually moving. The
result slid around the outpost pointing whichever way it had spawned.

So: real destinations, far enough apart to be flown to. The vessel picks a
point somewhere in its patrol area, flies there, pauses, and picks another.
A leg is never shorter than wander_min_leg, which is what keeps the
snap-and-stop from coming straight back, and a leg that cannot be reached
times out rather than stranding the picket.

Breaking off a fight now starts a fresh leg from where the fight ended,
rather than flying back to a waypoint chosen before it began.

Measured: 485px of wander, 46-271px from its post, up to 690px/s, pointing
where it was going for 88% of its travel. The test measures that facing as a
FRACTION rather than a worst instant - a vessel just given a new heading is
legitimately still swinging onto it at its own rotation_speed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Loïc Wisniewski 2026-08-23 21:38:07 +02:00
commit 457ae7023c
2 changed files with 107 additions and 21 deletions

View file

@ -10,7 +10,11 @@ extends SceneTree
# A. a crewed outpost puts a real vessel in the world, and that vessel is
# NOT in the fleet - it must not follow the formation, must not eat a
# fleet slot, and must not fly off when the player leaves
# B. left alone it circles its post rather than sitting on it
# B. left alone it WANDERS its post: it visits genuinely different places,
# it is actually travelling rather than sliding, and it faces where it
# is going (#109 - it used to walk a circle in steps shorter than
# Ship's arrive_radius, so every frame it snapped and zeroed its
# velocity, and Ship only turns while it is moving)
# C. an enemy inside its detection range is engaged and shot at
# D. it will not be walked away: an enemy beyond the leash is ignored, and
# an engagement never drags it further than the leash from its post
@ -30,6 +34,10 @@ var stage: int = 0
var elapsed: float = 0.0
var orbit_min: float = INF
var orbit_max: float = 0.0
var visited: Array = []
var top_speed: float = 0.0
var moving_frames: int = 0
var facing_frames: int = 0
var max_from_post: float = 0.0
var dummy_hp_before: int = 0
@ -117,20 +125,50 @@ func _process(delta: float) -> bool:
stage = 2
return false
2:
# --- B. it circles the post ---------------------------------
# --- B. it wanders its post ---------------------------------
var from_post: float = guard.global_position.length()
orbit_min = minf(orbit_min, from_post)
orbit_max = maxf(orbit_max, from_post)
if elapsed < 4.0:
visited.append(guard.global_position)
var speed: float = guard.velocity.length()
top_speed = maxf(top_speed, speed)
if speed > 40.0:
# Facing is only meaningful while actually travelling, which
# is the whole of what #109 was about. Measured as a FRACTION
# of the travel rather than as a worst instant: a vessel that
# has just been given a new heading is legitimately still
# swinging onto it at its own rotation_speed, and one frame
# mid-turn says nothing about whether it flies nose-first.
moving_frames += 1
var drift: float = absf(angle_difference(guard.rotation,
guard.velocity.angle() + PI / 2.0))
if drift <= PI / 4.0:
facing_frames += 1
if elapsed < 8.0:
return false
if component.is_engaging():
return _fail("the garrison thinks it is fighting with no enemy on the map")
if orbit_min < 40.0:
return _fail("the garrison sat on top of its outpost (closest %.0fpx)" % orbit_min)
if orbit_max > component.leash_range:
return _fail("patrolling took it %.0fpx from its post, past the %.0fpx leash" \
% [orbit_max, component.leash_range])
print("patrolled between %.0f and %.0fpx of its post" % [orbit_min, orbit_max])
if top_speed < 60.0:
return _fail("the picket never got above %.0fpx/s - it is sliding, not flying" \
% top_speed)
if moving_frames <= 0:
return _fail("it never travelled at all")
var aligned: float = float(facing_frames) / float(moving_frames)
if aligned < 0.7:
return _fail("it was pointing where it was going for only %.0f%% of its travel" \
% (aligned * 100.0))
# Genuinely different places, not one path walked over and over.
var spread: float = 0.0
for a in visited:
for b in visited:
spread = maxf(spread, a.distance_to(b))
if spread < 120.0:
return _fail("everywhere it went was within %.0fpx - that is not wandering" % spread)
print("wandered %.0fpx across, %.0f-%.0fpx from its post, up to %.0fpx/s, nose-first %.0f%% of the way" \
% [spread, orbit_min, orbit_max, top_speed, aligned * 100.0])
# --- D (first half). an enemy beyond the leash is ignored ----
enemy = _dummy(Vector2(component.leash_range + component.engage_distance + 900.0, 0.0))