#105 (#97g): garrison vessels patrol and defend their outpost
A garrisoned vessel circles its post, engages what comes into range, and
goes back to circling. It steers by writing move_target and lets Ship's own
steering fly it, so it accelerates, banks and brakes exactly like a vessel
in the formation.
It is not in Fleet.ships. It does not follow the formation, does not count
against max_fleet_size(), and does not move when the player flies away -
which is the whole point of stationing it. A garrison that ate a fleet slot
would mean stationing a vessel cost the player a place in their own wedge.
The leash is the load-bearing part. Enemies are judged from the POST rather
than from the vessel, so a garrison that has drifted out to its leash cannot
see further still and be walked away from its outpost one enemy at a time,
and an engagement is clamped to a circle around the post rather than to a
straight line, so it still manoeuvres while pinned.
The orbit starts from where the vessel actually is rather than a random
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 - through the
outpost it is guarding.
EnemyTargets.nearest_ship now searches the player_ships GROUP instead of
Fleet.ships: an enemy that ignored the guns shooting at it, because they
were not in the player's formation, would let a garrison hold a position
without ever being fought back.
The laser vessel is deliberately not garrisonable. Firing the laser lives in
LaserController, a fleet ability driven by the laser button, rather than in
VesselLaserComponent - so a garrisoned laser would hold its position and
never shoot, and offering a vessel that cannot fight is worse than a short
list.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 14:13:47 +02:00
|
|
|
extends SceneTree
|
|
|
|
|
|
|
|
|
|
# Headless gameplay test: run with
|
|
|
|
|
# godot --headless -s res://tests/test_outpost_garrison_patrol.gd
|
|
|
|
|
# Forgejo #105 (#97g): "When a vessel is assigned to an outpost, it
|
|
|
|
|
# navigates around the outpost by itself, attacking nearby ennemies if any
|
|
|
|
|
# are detected."
|
|
|
|
|
#
|
|
|
|
|
# Covers:
|
|
|
|
|
# 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
|
#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>
2026-08-23 21:38:07 +02:00
|
|
|
# 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)
|
#105 (#97g): garrison vessels patrol and defend their outpost
A garrisoned vessel circles its post, engages what comes into range, and
goes back to circling. It steers by writing move_target and lets Ship's own
steering fly it, so it accelerates, banks and brakes exactly like a vessel
in the formation.
It is not in Fleet.ships. It does not follow the formation, does not count
against max_fleet_size(), and does not move when the player flies away -
which is the whole point of stationing it. A garrison that ate a fleet slot
would mean stationing a vessel cost the player a place in their own wedge.
The leash is the load-bearing part. Enemies are judged from the POST rather
than from the vessel, so a garrison that has drifted out to its leash cannot
see further still and be walked away from its outpost one enemy at a time,
and an engagement is clamped to a circle around the post rather than to a
straight line, so it still manoeuvres while pinned.
The orbit starts from where the vessel actually is rather than a random
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 - through the
outpost it is guarding.
EnemyTargets.nearest_ship now searches the player_ships GROUP instead of
Fleet.ships: an enemy that ignored the guns shooting at it, because they
were not in the player's formation, would let a garrison hold a position
without ever being fought back.
The laser vessel is deliberately not garrisonable. Firing the laser lives in
LaserController, a fleet ability driven by the laser button, rather than in
VesselLaserComponent - so a garrisoned laser would hold its position and
never shoot, and offering a vessel that cannot fight is worse than a short
list.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 14:13:47 +02:00
|
|
|
# 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
|
|
|
|
|
# E. it goes back to patrolling once the enemy is gone
|
|
|
|
|
# F. losing a garrison vessel frees the berth, so it gets crewed again
|
|
|
|
|
# G. enemies fight back - a garrison vessel is a target like any other
|
|
|
|
|
|
|
|
|
|
var game_state: Node
|
|
|
|
|
var main: Node
|
|
|
|
|
var fleet: Node
|
|
|
|
|
var field: Node
|
|
|
|
|
var outpost_id: int = 0
|
|
|
|
|
var guard: Node2D
|
|
|
|
|
var component: Node
|
|
|
|
|
var enemy: Node2D
|
|
|
|
|
var stage: int = 0
|
|
|
|
|
var elapsed: float = 0.0
|
|
|
|
|
var orbit_min: float = INF
|
|
|
|
|
var orbit_max: float = 0.0
|
#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>
2026-08-23 21:38:07 +02:00
|
|
|
var visited: Array = []
|
|
|
|
|
var top_speed: float = 0.0
|
|
|
|
|
var moving_frames: int = 0
|
|
|
|
|
var facing_frames: int = 0
|
#105 (#97g): garrison vessels patrol and defend their outpost
A garrisoned vessel circles its post, engages what comes into range, and
goes back to circling. It steers by writing move_target and lets Ship's own
steering fly it, so it accelerates, banks and brakes exactly like a vessel
in the formation.
It is not in Fleet.ships. It does not follow the formation, does not count
against max_fleet_size(), and does not move when the player flies away -
which is the whole point of stationing it. A garrison that ate a fleet slot
would mean stationing a vessel cost the player a place in their own wedge.
The leash is the load-bearing part. Enemies are judged from the POST rather
than from the vessel, so a garrison that has drifted out to its leash cannot
see further still and be walked away from its outpost one enemy at a time,
and an engagement is clamped to a circle around the post rather than to a
straight line, so it still manoeuvres while pinned.
The orbit starts from where the vessel actually is rather than a random
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 - through the
outpost it is guarding.
EnemyTargets.nearest_ship now searches the player_ships GROUP instead of
Fleet.ships: an enemy that ignored the guns shooting at it, because they
were not in the player's formation, would let a garrison hold a position
without ever being fought back.
The laser vessel is deliberately not garrisonable. Firing the laser lives in
LaserController, a fleet ability driven by the laser button, rather than in
VesselLaserComponent - so a garrisoned laser would hold its position and
never shoot, and offering a vessel that cannot fight is worse than a short
list.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 14:13:47 +02:00
|
|
|
var max_from_post: float = 0.0
|
|
|
|
|
var dummy_hp_before: int = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _initialize() -> void:
|
|
|
|
|
# GameState is an autoload: a bare GameState.<instance member> does not
|
|
|
|
|
# compile in a SceneTree script, so the node is fetched by name instead.
|
|
|
|
|
game_state = root.get_node("GameState")
|
|
|
|
|
game_state.buildings.clear()
|
|
|
|
|
game_state.extractors.clear()
|
|
|
|
|
game_state.transporters.clear()
|
|
|
|
|
game_state.stored_vessels.clear()
|
|
|
|
|
game_state.vessel_deliveries.clear()
|
|
|
|
|
game_state.reset_progression()
|
|
|
|
|
game_state.reset_waves()
|
|
|
|
|
game_state.unlocked.append("forward_bases")
|
|
|
|
|
main = load("res://world/Main.tscn").instantiate()
|
|
|
|
|
root.add_child(main)
|
|
|
|
|
current_scene = main
|
|
|
|
|
fleet = main.get_node("Fleet")
|
|
|
|
|
field = main.get_node("GarrisonField")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _fail(message: String) -> bool:
|
|
|
|
|
print("TEST_OUTPOST_GARRISON_PATROL: FAIL (%s)" % message)
|
|
|
|
|
quit(1)
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# A pinned target that neither shoots back nor dies. The point of these
|
|
|
|
|
# stages is where the GARRISON goes and whether it fires, and a sniper that
|
|
|
|
|
# killed the guard mid-check, or a chaser that died to it, would end the
|
|
|
|
|
# observation before the thing being measured had happened.
|
|
|
|
|
func _dummy(at: Vector2) -> Node2D:
|
|
|
|
|
var dummy: Node2D = load("res://enemies/Enemy.tscn").instantiate()
|
|
|
|
|
dummy.enemy_id = "chaser"
|
|
|
|
|
dummy.position = at
|
|
|
|
|
main.add_child(dummy)
|
|
|
|
|
dummy.speed = 0.0
|
|
|
|
|
dummy.hp = 100000
|
|
|
|
|
return dummy
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _process(delta: float) -> bool:
|
|
|
|
|
elapsed += delta
|
|
|
|
|
match stage:
|
|
|
|
|
0:
|
|
|
|
|
if elapsed < 0.3:
|
|
|
|
|
return false
|
|
|
|
|
main.get_node("EnemySpawner").set_process(false)
|
|
|
|
|
main.get_node("AsteroidField").set_process(false)
|
|
|
|
|
for hazard in get_nodes_in_group("asteroids") + get_nodes_in_group("enemies"):
|
|
|
|
|
if is_instance_valid(hazard):
|
|
|
|
|
hazard.queue_free()
|
|
|
|
|
# The fleet is parked a long way off, so nothing here is the
|
|
|
|
|
# fleet's doing.
|
|
|
|
|
fleet.spawn_ships(1, Vector2(-9000.0, 0.0),
|
|
|
|
|
PackedStringArray([GameState.VESSEL_INTERCEPTOR]))
|
|
|
|
|
fleet.ships[0].move_target = fleet.ships[0].position
|
|
|
|
|
|
|
|
|
|
main.get_node("BuildingField").place(GameState.BUILDING_OUTPOST, Vector2.ZERO)
|
|
|
|
|
outpost_id = int(game_state.buildings[-1].id)
|
|
|
|
|
game_state.set_garrison_desired(outpost_id, GameState.VESSEL_INTERCEPTOR, 1)
|
|
|
|
|
game_state.garrison_crew(outpost_id).append(GameState.VESSEL_INTERCEPTOR)
|
|
|
|
|
elapsed = 0.0
|
|
|
|
|
stage = 1
|
|
|
|
|
return false
|
|
|
|
|
1:
|
|
|
|
|
if elapsed < 0.3:
|
|
|
|
|
return false
|
|
|
|
|
# --- A. a real vessel, and not a fleet one -------------------
|
|
|
|
|
var here: Array = field.ships_at(outpost_id)
|
|
|
|
|
if here.size() != 1:
|
|
|
|
|
return _fail("a crewed outpost put %d vessels in the world" % here.size())
|
|
|
|
|
guard = here[0]
|
|
|
|
|
component = guard.get_component("garrison")
|
|
|
|
|
if component == null:
|
|
|
|
|
return _fail("the garrison vessel has no garrison component steering it")
|
|
|
|
|
if guard in fleet.ships:
|
|
|
|
|
return _fail("the garrison vessel joined the fleet")
|
|
|
|
|
if fleet.ships.size() != 1:
|
|
|
|
|
return _fail("stationing a vessel changed the fleet to %d ships" % fleet.ships.size())
|
|
|
|
|
print("outpost crewed with 1 vessel, fleet still %d and untouched" % fleet.ships.size())
|
|
|
|
|
elapsed = 0.0
|
|
|
|
|
stage = 2
|
|
|
|
|
return false
|
|
|
|
|
2:
|
#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>
2026-08-23 21:38:07 +02:00
|
|
|
# --- B. it wanders its post ---------------------------------
|
#105 (#97g): garrison vessels patrol and defend their outpost
A garrisoned vessel circles its post, engages what comes into range, and
goes back to circling. It steers by writing move_target and lets Ship's own
steering fly it, so it accelerates, banks and brakes exactly like a vessel
in the formation.
It is not in Fleet.ships. It does not follow the formation, does not count
against max_fleet_size(), and does not move when the player flies away -
which is the whole point of stationing it. A garrison that ate a fleet slot
would mean stationing a vessel cost the player a place in their own wedge.
The leash is the load-bearing part. Enemies are judged from the POST rather
than from the vessel, so a garrison that has drifted out to its leash cannot
see further still and be walked away from its outpost one enemy at a time,
and an engagement is clamped to a circle around the post rather than to a
straight line, so it still manoeuvres while pinned.
The orbit starts from where the vessel actually is rather than a random
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 - through the
outpost it is guarding.
EnemyTargets.nearest_ship now searches the player_ships GROUP instead of
Fleet.ships: an enemy that ignored the guns shooting at it, because they
were not in the player's formation, would let a garrison hold a position
without ever being fought back.
The laser vessel is deliberately not garrisonable. Firing the laser lives in
LaserController, a fleet ability driven by the laser button, rather than in
VesselLaserComponent - so a garrisoned laser would hold its position and
never shoot, and offering a vessel that cannot fight is worse than a short
list.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 14:13:47 +02:00
|
|
|
var from_post: float = guard.global_position.length()
|
|
|
|
|
orbit_min = minf(orbit_min, from_post)
|
|
|
|
|
orbit_max = maxf(orbit_max, from_post)
|
#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>
2026-08-23 21:38:07 +02:00
|
|
|
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:
|
#105 (#97g): garrison vessels patrol and defend their outpost
A garrisoned vessel circles its post, engages what comes into range, and
goes back to circling. It steers by writing move_target and lets Ship's own
steering fly it, so it accelerates, banks and brakes exactly like a vessel
in the formation.
It is not in Fleet.ships. It does not follow the formation, does not count
against max_fleet_size(), and does not move when the player flies away -
which is the whole point of stationing it. A garrison that ate a fleet slot
would mean stationing a vessel cost the player a place in their own wedge.
The leash is the load-bearing part. Enemies are judged from the POST rather
than from the vessel, so a garrison that has drifted out to its leash cannot
see further still and be walked away from its outpost one enemy at a time,
and an engagement is clamped to a circle around the post rather than to a
straight line, so it still manoeuvres while pinned.
The orbit starts from where the vessel actually is rather than a random
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 - through the
outpost it is guarding.
EnemyTargets.nearest_ship now searches the player_ships GROUP instead of
Fleet.ships: an enemy that ignored the guns shooting at it, because they
were not in the player's formation, would let a garrison hold a position
without ever being fought back.
The laser vessel is deliberately not garrisonable. Firing the laser lives in
LaserController, a fleet ability driven by the laser button, rather than in
VesselLaserComponent - so a garrisoned laser would hold its position and
never shoot, and offering a vessel that cannot fight is worse than a short
list.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 14:13:47 +02:00
|
|
|
return false
|
|
|
|
|
if component.is_engaging():
|
|
|
|
|
return _fail("the garrison thinks it is fighting with no enemy on the map")
|
|
|
|
|
if orbit_max > component.leash_range:
|
|
|
|
|
return _fail("patrolling took it %.0fpx from its post, past the %.0fpx leash" \
|
|
|
|
|
% [orbit_max, component.leash_range])
|
#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>
2026-08-23 21:38:07 +02:00
|
|
|
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])
|
#105 (#97g): garrison vessels patrol and defend their outpost
A garrisoned vessel circles its post, engages what comes into range, and
goes back to circling. It steers by writing move_target and lets Ship's own
steering fly it, so it accelerates, banks and brakes exactly like a vessel
in the formation.
It is not in Fleet.ships. It does not follow the formation, does not count
against max_fleet_size(), and does not move when the player flies away -
which is the whole point of stationing it. A garrison that ate a fleet slot
would mean stationing a vessel cost the player a place in their own wedge.
The leash is the load-bearing part. Enemies are judged from the POST rather
than from the vessel, so a garrison that has drifted out to its leash cannot
see further still and be walked away from its outpost one enemy at a time,
and an engagement is clamped to a circle around the post rather than to a
straight line, so it still manoeuvres while pinned.
The orbit starts from where the vessel actually is rather than a random
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 - through the
outpost it is guarding.
EnemyTargets.nearest_ship now searches the player_ships GROUP instead of
Fleet.ships: an enemy that ignored the guns shooting at it, because they
were not in the player's formation, would let a garrison hold a position
without ever being fought back.
The laser vessel is deliberately not garrisonable. Firing the laser lives in
LaserController, a fleet ability driven by the laser button, rather than in
VesselLaserComponent - so a garrisoned laser would hold its position and
never shoot, and offering a vessel that cannot fight is worse than a short
list.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 14:13:47 +02:00
|
|
|
|
|
|
|
|
# --- D (first half). an enemy beyond the leash is ignored ----
|
|
|
|
|
enemy = _dummy(Vector2(component.leash_range + component.engage_distance + 900.0, 0.0))
|
|
|
|
|
elapsed = 0.0
|
|
|
|
|
stage = 3
|
|
|
|
|
return false
|
|
|
|
|
3:
|
|
|
|
|
if elapsed < 1.0:
|
|
|
|
|
return false
|
|
|
|
|
if component.is_engaging():
|
|
|
|
|
return _fail("the garrison went after an enemy well beyond its leash")
|
|
|
|
|
print("an enemy %.0fpx out is left alone" % enemy.global_position.length())
|
|
|
|
|
enemy.queue_free()
|
|
|
|
|
|
|
|
|
|
# --- C. an enemy in reach is engaged and shot at -------------
|
|
|
|
|
enemy = _dummy(Vector2(300.0, 0.0))
|
|
|
|
|
dummy_hp_before = enemy.hp
|
|
|
|
|
max_from_post = 0.0
|
|
|
|
|
elapsed = 0.0
|
|
|
|
|
stage = 4
|
|
|
|
|
return false
|
|
|
|
|
4:
|
|
|
|
|
if not is_instance_valid(guard):
|
|
|
|
|
return _fail("the garrison vessel was destroyed mid-engagement")
|
|
|
|
|
max_from_post = maxf(max_from_post, guard.global_position.length())
|
|
|
|
|
if elapsed < 2.5:
|
|
|
|
|
return false
|
|
|
|
|
if not component.is_engaging():
|
|
|
|
|
return _fail("an enemy 300px from the outpost was not engaged")
|
|
|
|
|
# Damage taken, not projectiles in flight: a bolt that has already
|
|
|
|
|
# connected has freed itself, so counting live ones at an instant
|
|
|
|
|
# reads zero exactly when the gun is working best.
|
|
|
|
|
if enemy.hp >= dummy_hp_before:
|
|
|
|
|
return _fail("the garrison engaged but the enemy took no damage")
|
|
|
|
|
# --- D (second half). the leash holds during a fight ---------
|
|
|
|
|
if max_from_post > component.leash_range + 60.0:
|
|
|
|
|
return _fail("the fight pulled it %.0fpx from its post, past a %.0fpx leash" \
|
|
|
|
|
% [max_from_post, component.leash_range])
|
|
|
|
|
print("engaged, took %d hp off it, and stayed within %.0fpx of its post" \
|
|
|
|
|
% [dummy_hp_before - enemy.hp, max_from_post])
|
|
|
|
|
|
|
|
|
|
# --- G. and it can be fought back ---------------------------
|
|
|
|
|
var targets: Script = load("res://enemies/EnemyTargets.gd")
|
|
|
|
|
var picked: Node2D = targets.nearest_ship(main, enemy.global_position)
|
|
|
|
|
if picked != guard:
|
|
|
|
|
return _fail("an enemy beside the garrison picks %s instead of it - it cannot be fought back" \
|
|
|
|
|
% ("nothing" if picked == null else str(picked.name)))
|
|
|
|
|
print("enemies see the garrison as a target too")
|
|
|
|
|
|
|
|
|
|
if is_instance_valid(enemy):
|
|
|
|
|
enemy.queue_free()
|
|
|
|
|
elapsed = 0.0
|
|
|
|
|
stage = 5
|
|
|
|
|
return false
|
|
|
|
|
5:
|
|
|
|
|
# --- E. back to the patrol ----------------------------------
|
|
|
|
|
if elapsed < 1.5:
|
|
|
|
|
return false
|
|
|
|
|
if component.is_engaging():
|
|
|
|
|
return _fail("the garrison is still fighting an enemy that is gone")
|
|
|
|
|
print("stood down and went back to its patrol")
|
|
|
|
|
|
|
|
|
|
# --- F. a lost vessel frees its berth ------------------------
|
|
|
|
|
if game_state.garrison_crew(outpost_id).size() != 1:
|
|
|
|
|
return _fail("the crew is not 1 before the kill")
|
|
|
|
|
guard.take_damage(1)
|
|
|
|
|
elapsed = 0.0
|
|
|
|
|
stage = 6
|
|
|
|
|
return false
|
|
|
|
|
6:
|
|
|
|
|
if elapsed < 0.3:
|
|
|
|
|
return false
|
|
|
|
|
if not game_state.garrison_crew(outpost_id).is_empty():
|
|
|
|
|
return _fail("a destroyed garrison vessel still holds its berth")
|
|
|
|
|
if game_state.garrison_desired_of(outpost_id, GameState.VESSEL_INTERCEPTOR) != 1:
|
|
|
|
|
return _fail("losing the vessel cancelled the order as well")
|
|
|
|
|
if field.ships_at(outpost_id).size() != 0:
|
|
|
|
|
return _fail("the dead vessel is still stationed")
|
|
|
|
|
print("losing the vessel frees the berth and leaves the order standing")
|
|
|
|
|
|
|
|
|
|
print("TEST_OUTPOST_GARRISON_PATROL: PASS")
|
|
|
|
|
quit(0)
|
|
|
|
|
return true
|
|
|
|
|
return false
|