#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:
parent
209169206b
commit
457ae7023c
2 changed files with 107 additions and 21 deletions
|
|
@ -10,7 +10,11 @@ extends SceneTree
|
||||||
# A. a crewed outpost puts a real vessel in the world, and that vessel is
|
# 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
|
# 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
|
# 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
|
# 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
|
# 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
|
# 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 elapsed: float = 0.0
|
||||||
var orbit_min: float = INF
|
var orbit_min: float = INF
|
||||||
var orbit_max: float = 0.0
|
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 max_from_post: float = 0.0
|
||||||
var dummy_hp_before: int = 0
|
var dummy_hp_before: int = 0
|
||||||
|
|
||||||
|
|
@ -117,20 +125,50 @@ func _process(delta: float) -> bool:
|
||||||
stage = 2
|
stage = 2
|
||||||
return false
|
return false
|
||||||
2:
|
2:
|
||||||
# --- B. it circles the post ---------------------------------
|
# --- B. it wanders its post ---------------------------------
|
||||||
var from_post: float = guard.global_position.length()
|
var from_post: float = guard.global_position.length()
|
||||||
orbit_min = minf(orbit_min, from_post)
|
orbit_min = minf(orbit_min, from_post)
|
||||||
orbit_max = maxf(orbit_max, 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
|
return false
|
||||||
if component.is_engaging():
|
if component.is_engaging():
|
||||||
return _fail("the garrison thinks it is fighting with no enemy on the map")
|
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:
|
if orbit_max > component.leash_range:
|
||||||
return _fail("patrolling took it %.0fpx from its post, past the %.0fpx leash" \
|
return _fail("patrolling took it %.0fpx from its post, past the %.0fpx leash" \
|
||||||
% [orbit_max, component.leash_range])
|
% [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 ----
|
# --- D (first half). an enemy beyond the leash is ignored ----
|
||||||
enemy = _dummy(Vector2(component.leash_range + component.engage_distance + 900.0, 0.0))
|
enemy = _dummy(Vector2(component.leash_range + component.engage_distance + 900.0, 0.0))
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,23 @@ var capability_id: String = "garrison"
|
||||||
var outpost_id: int = 0
|
var outpost_id: int = 0
|
||||||
var station: Vector2 = Vector2.ZERO
|
var station: Vector2 = Vector2.ZERO
|
||||||
|
|
||||||
@export var orbit_radius: float = 170.0
|
# #109: it used to walk a circle, one small step per frame. That looked
|
||||||
@export var orbit_speed: float = 0.85
|
# wrong for two reasons and the second was the real one. A ring is not what
|
||||||
|
# a picket does - but worse, each step was shorter than Ship's arrive_radius,
|
||||||
|
# so every frame the vessel SNAPPED onto its target and set velocity to zero,
|
||||||
|
# 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, goes there, pauses, and picks another.
|
||||||
|
@export var wander_radius: float = 340.0
|
||||||
|
# A new leg is never shorter than this, or the snap-and-stop problem comes
|
||||||
|
# straight back.
|
||||||
|
@export var wander_min_leg: float = 150.0
|
||||||
|
@export var wander_arrive: float = 60.0
|
||||||
|
@export var wander_pause: float = 0.8
|
||||||
|
# A leg that somehow cannot be reached must not strand the picket.
|
||||||
|
@export var wander_leg_timeout: float = 12.0
|
||||||
@export var detection_range: float = 950.0
|
@export var detection_range: float = 950.0
|
||||||
# How far from its post it will go. Without a leash a garrison would be
|
# How far from its post it will go. Without a leash a garrison would be
|
||||||
# drawn off by anything that flew past, which is exactly what stationing it
|
# drawn off by anything that flew past, which is exactly what stationing it
|
||||||
|
|
@ -35,18 +50,14 @@ var station: Vector2 = Vector2.ZERO
|
||||||
# vessel that closed to zero would trade itself for a chaser every time.
|
# vessel that closed to zero would trade itself for a chaser every time.
|
||||||
@export var engage_distance: float = 300.0
|
@export var engage_distance: float = 300.0
|
||||||
|
|
||||||
var _angle: float = 0.0
|
|
||||||
var _engaging: bool = false
|
var _engaging: bool = false
|
||||||
|
var _wander_to: Vector2 = Vector2.ZERO
|
||||||
|
var _leg_time: float = 0.0
|
||||||
|
var _pause_left: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
func setup() -> void:
|
func setup() -> void:
|
||||||
# Start the orbit from where the vessel actually IS, not from a random
|
_pick_leg()
|
||||||
# bearing. A random one makes the first move_target a point on the far
|
|
||||||
# side of the circle, and the vessel flies straight across the middle to
|
|
||||||
# reach it - through the outpost it is supposed to be guarding.
|
|
||||||
var ship: Node2D = get_parent()
|
|
||||||
var offset: Vector2 = ship.global_position - station
|
|
||||||
_angle = offset.angle() if offset.length() > 1.0 else randf() * TAU
|
|
||||||
|
|
||||||
|
|
||||||
func is_engaging() -> bool:
|
func is_engaging() -> bool:
|
||||||
|
|
@ -65,8 +76,43 @@ func tick(delta: float) -> void:
|
||||||
|
|
||||||
|
|
||||||
func _patrol(ship: Node2D, delta: float) -> void:
|
func _patrol(ship: Node2D, delta: float) -> void:
|
||||||
_angle += orbit_speed * delta
|
if _pause_left > 0.0:
|
||||||
ship.move_target = station + Vector2(cos(_angle), sin(_angle)) * orbit_radius
|
# Sitting still between legs, which is most of what makes it read as
|
||||||
|
# a picket holding a position rather than a thing on a rail.
|
||||||
|
_pause_left -= delta
|
||||||
|
return
|
||||||
|
_leg_time += delta
|
||||||
|
if ship.global_position.distance_to(_wander_to) <= wander_arrive \
|
||||||
|
or _leg_time >= wander_leg_timeout:
|
||||||
|
_pause_left = randf_range(wander_pause * 0.5, wander_pause * 1.5)
|
||||||
|
_pick_leg()
|
||||||
|
return
|
||||||
|
ship.move_target = _wander_to
|
||||||
|
|
||||||
|
|
||||||
|
func _pick_leg() -> void:
|
||||||
|
var ship: Node2D = get_parent()
|
||||||
|
var from: Vector2 = ship.global_position if ship != null else station
|
||||||
|
# Rejection-sampled rather than solved: a handful of tries always finds a
|
||||||
|
# point that is both inside the patrol area and far enough to be worth
|
||||||
|
# flying to, and the alternative is arithmetic nobody will read.
|
||||||
|
for attempt in range(8):
|
||||||
|
var angle: float = randf() * TAU
|
||||||
|
var radius: float = sqrt(randf()) * wander_radius
|
||||||
|
var candidate: Vector2 = station + Vector2(cos(angle), sin(angle)) * radius
|
||||||
|
if from.distance_to(candidate) >= wander_min_leg:
|
||||||
|
_wander_to = candidate
|
||||||
|
_leg_time = 0.0
|
||||||
|
return
|
||||||
|
# Nothing far enough came up - head straight out to the edge instead.
|
||||||
|
var away: Vector2 = (from - station)
|
||||||
|
away = away.normalized() if away.length() > 1.0 else Vector2.RIGHT
|
||||||
|
_wander_to = station - away * wander_radius
|
||||||
|
_leg_time = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
func patrol_destination() -> Vector2:
|
||||||
|
return _wander_to
|
||||||
|
|
||||||
|
|
||||||
func _engage(ship: Node2D, target: Node2D, delta: float) -> void:
|
func _engage(ship: Node2D, target: Node2D, delta: float) -> void:
|
||||||
|
|
@ -82,9 +128,11 @@ func _engage(ship: Node2D, target: Node2D, delta: float) -> void:
|
||||||
if from_station.length() > leash_range:
|
if from_station.length() > leash_range:
|
||||||
wanted = station + from_station.normalized() * leash_range
|
wanted = station + from_station.normalized() * leash_range
|
||||||
ship.move_target = wanted
|
ship.move_target = wanted
|
||||||
# The orbit angle keeps advancing while engaged, so breaking off does not
|
# Breaking off starts a fresh leg from wherever the fight ended, rather
|
||||||
# snap the vessel back to whatever bearing it left on.
|
# than flying back to a waypoint chosen before it began.
|
||||||
_angle += orbit_speed * delta
|
_wander_to = wanted
|
||||||
|
_leg_time = 0.0
|
||||||
|
_pause_left = 0.0
|
||||||
|
|
||||||
_shoot(ship, target, distance, to_target)
|
_shoot(ship, target, distance, to_target)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue