#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
|
|
|
class_name VesselGarrisonComponent
|
|
|
|
|
extends Node
|
|
|
|
|
|
2026-09-03 00:12:08 +02:00
|
|
|
const COMBAT_TARGET = preload("res://core/CombatTarget.gd")
|
|
|
|
|
|
#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
|
|
|
# #105 (#97g): "When a vessel is assigned to an outpost, it navigates around
|
|
|
|
|
# the outpost by itself, attacking nearby ennemies if any are detected."
|
|
|
|
|
#
|
|
|
|
|
# Attached to a Ship at spawn by GarrisonField rather than declared on the
|
|
|
|
|
# vessel definition: garrisoning is a ROLE a hull is put into, not a property
|
|
|
|
|
# of the hull. The same interceptor is a fleet vessel one minute and a
|
|
|
|
|
# garrison the next, and nothing about it changes but who is steering.
|
|
|
|
|
#
|
|
|
|
|
# It steers by writing move_target and lets Ship's own steering do the
|
|
|
|
|
# flying, so a garrison vessel accelerates, banks and brakes exactly like one
|
|
|
|
|
# in the formation. It is not in Fleet.ships, so 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.
|
|
|
|
|
|
|
|
|
|
var capability_id: String = "garrison"
|
|
|
|
|
|
|
|
|
|
# Which outpost this belongs to, and where that outpost is. The position is
|
|
|
|
|
# copied rather than looked up every tick: the record is the truth, and if
|
|
|
|
|
# the outpost is levelled the vessel should hold the ground it was left on
|
|
|
|
|
# rather than snap to the origin.
|
|
|
|
|
var outpost_id: int = 0
|
|
|
|
|
var station: Vector2 = Vector2.ZERO
|
|
|
|
|
|
#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
|
|
|
# #109: it used to walk a circle, one small step per frame. That looked
|
|
|
|
|
# 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
|
#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
|
|
|
@export var detection_range: float = 950.0
|
|
|
|
|
# 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
|
|
|
|
|
# was meant to stop - and the outpost would be left open behind it.
|
|
|
|
|
@export var leash_range: float = 620.0
|
|
|
|
|
# Held off the target rather than ramming it: these are gunships, and a
|
|
|
|
|
# vessel that closed to zero would trade itself for a chaser every time.
|
|
|
|
|
@export var engage_distance: float = 300.0
|
|
|
|
|
|
|
|
|
|
var _engaging: bool = false
|
#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 _wander_to: Vector2 = Vector2.ZERO
|
|
|
|
|
var _leg_time: float = 0.0
|
|
|
|
|
var _pause_left: float = 0.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
|
|
|
|
|
|
|
|
|
|
|
|
|
func setup() -> void:
|
#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
|
|
|
_pick_leg()
|
#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
|
|
|
|
|
|
|
|
|
|
|
|
|
func is_engaging() -> bool:
|
|
|
|
|
return _engaging
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func tick(delta: float) -> void:
|
|
|
|
|
var ship: Node2D = get_parent()
|
2026-09-03 00:12:08 +02:00
|
|
|
var target: Node2D = _nearest_target(ship)
|
#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
|
|
|
_engaging = target != null
|
|
|
|
|
if target == null:
|
|
|
|
|
_patrol(ship, delta)
|
|
|
|
|
_hold_fire(ship)
|
|
|
|
|
return
|
|
|
|
|
_engage(ship, target, delta)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _patrol(ship: Node2D, delta: float) -> void:
|
#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 _pause_left > 0.0:
|
|
|
|
|
# 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
|
#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
|
|
|
|
|
|
|
|
|
|
|
|
|
func _engage(ship: Node2D, target: Node2D, delta: float) -> void:
|
|
|
|
|
# Keep station off the target, but never further from the post than the
|
|
|
|
|
# leash allows - the approach is clamped to a circle around the outpost
|
|
|
|
|
# rather than to a straight line, so it still manoeuvres while pinned.
|
|
|
|
|
var to_target: Vector2 = target.global_position - ship.global_position
|
|
|
|
|
var distance: float = to_target.length()
|
|
|
|
|
var wanted: Vector2 = target.global_position
|
|
|
|
|
if distance > 0.001:
|
|
|
|
|
wanted = target.global_position - to_target.normalized() * engage_distance
|
|
|
|
|
var from_station: Vector2 = wanted - station
|
|
|
|
|
if from_station.length() > leash_range:
|
|
|
|
|
wanted = station + from_station.normalized() * leash_range
|
|
|
|
|
ship.move_target = wanted
|
#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
|
|
|
# Breaking off starts a fresh leg from wherever the fight ended, rather
|
|
|
|
|
# than flying back to a waypoint chosen before it began.
|
|
|
|
|
_wander_to = wanted
|
|
|
|
|
_leg_time = 0.0
|
|
|
|
|
_pause_left = 0.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
|
|
|
|
Garrison laser vessels and gas bombers, on a shared ability seam
#105 shipped with interceptors as the only garrisonable vessel, and the
reason was a design fault rather than a decision: firing the laser and the
gas bomb lived in the player's fleet controllers, so a stationed laser would
have held its position and never shot.
The shot is now a thing in the world rather than something a controller
draws. LaserBeamEffect and GasBombEffect are self-contained, self-animating
and self-freeing, added to the tree root - the same shape EnemyBigLaserBeam
and ArtilleryShell already had on the enemy side - and they outlive the
vessel that fired them.
Above them sits one seam, on VesselCooldownFillComponent: ability_range(),
ability_ammo_resource(), ability_ammo_cost() and fire_ability(). A beam and a
bomb are now the same shape of thing to whoever pulls the trigger, so the
garrison AI never names an ability - the test asserts that literally, by
reading the AI's own source. A third armed vessel needs to implement the
seam and nothing else.
Ammunition is the CALLER's business, deliberately: the fleet spends from its
hold, a garrison from its outpost's store, and neither concern belongs in
the component. A garrison with an empty magazine holds fire rather than
falling back on the fleet's hold.
Outpost storage: 50 gas per stationed bomber and 50 energy per stationed
laser, off the VESSEL definitions, so an outpost never learns which vessel
needs what. It is a capacity, not a stock - hub transporters fill it exactly
as they fill a gas burner's buffer, because building_logistics_deficit now
takes the larger of the definition's static buffer and the garrison's
magazine. Every other resource is capped at zero: an outpost is not a depot,
and ore parked at one has nothing to do there and no transporter coming for
it.
Also restores two properties the Godot editor dropped from
GasBurnerBuildingDefinition.tres when it re-saved the file:
collected_outputs and capabilities. Without the first, nothing ever collects
the burner's energy - test_gas_burner caught it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 19:23:35 +02:00
|
|
|
_shoot(ship, target, distance, to_target)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Two kinds of armament, one rule: point the gun, or fire the ability. Which
|
|
|
|
|
# one this vessel has is a question about its components, not about its type,
|
|
|
|
|
# so a third armed vessel needs nothing here.
|
|
|
|
|
func _shoot(ship: Node2D, target: Node2D, distance: float, to_target: Vector2) -> void:
|
|
|
|
|
var weapon: Node = _component(ship, "weapon")
|
|
|
|
|
if weapon != null:
|
|
|
|
|
# The gun is a held trigger: it is aimed every tick and stops the
|
|
|
|
|
# moment the direction is cleared.
|
|
|
|
|
weapon.set_fire_direction(to_target if distance <= weapon.fire_range else Vector2.ZERO)
|
|
|
|
|
for component in ship.get_children():
|
|
|
|
|
if not component.has_method("fire_ability") or not component.has_ability():
|
|
|
|
|
continue
|
|
|
|
|
if not component.is_ready() or distance > component.ability_range():
|
|
|
|
|
continue
|
|
|
|
|
# The magazine is the OUTPOST's, not the fleet's hold. A garrison
|
|
|
|
|
# spends what was flown out to it, which is the whole point of the
|
|
|
|
|
# outpost holding a store at all - and if the store is empty the
|
|
|
|
|
# vessel holds its fire rather than firing free shots.
|
|
|
|
|
var resource_type: String = component.ability_ammo_resource()
|
|
|
|
|
var cost: int = component.ability_ammo_cost()
|
|
|
|
|
if not GameState.spend_garrison_ammo(outpost_id, resource_type, cost):
|
|
|
|
|
continue
|
|
|
|
|
if not component.fire_ability(get_tree().root, ship.global_position,
|
|
|
|
|
target.global_position):
|
|
|
|
|
# It refused after being paid - put the ammunition back rather
|
|
|
|
|
# than burning it on a shot that never happened.
|
|
|
|
|
GameState.building_inventory(outpost_id).deposit(resource_type, cost)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _component(ship: Node2D, capability: String) -> Node:
|
|
|
|
|
return ship.get_component(capability) if ship.has_method("get_component") else null
|
#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
|
|
|
|
|
|
|
|
|
|
|
|
|
func _hold_fire(ship: Node2D) -> void:
|
Garrison laser vessels and gas bombers, on a shared ability seam
#105 shipped with interceptors as the only garrisonable vessel, and the
reason was a design fault rather than a decision: firing the laser and the
gas bomb lived in the player's fleet controllers, so a stationed laser would
have held its position and never shot.
The shot is now a thing in the world rather than something a controller
draws. LaserBeamEffect and GasBombEffect are self-contained, self-animating
and self-freeing, added to the tree root - the same shape EnemyBigLaserBeam
and ArtilleryShell already had on the enemy side - and they outlive the
vessel that fired them.
Above them sits one seam, on VesselCooldownFillComponent: ability_range(),
ability_ammo_resource(), ability_ammo_cost() and fire_ability(). A beam and a
bomb are now the same shape of thing to whoever pulls the trigger, so the
garrison AI never names an ability - the test asserts that literally, by
reading the AI's own source. A third armed vessel needs to implement the
seam and nothing else.
Ammunition is the CALLER's business, deliberately: the fleet spends from its
hold, a garrison from its outpost's store, and neither concern belongs in
the component. A garrison with an empty magazine holds fire rather than
falling back on the fleet's hold.
Outpost storage: 50 gas per stationed bomber and 50 energy per stationed
laser, off the VESSEL definitions, so an outpost never learns which vessel
needs what. It is a capacity, not a stock - hub transporters fill it exactly
as they fill a gas burner's buffer, because building_logistics_deficit now
takes the larger of the definition's static buffer and the garrison's
magazine. Every other resource is capped at zero: an outpost is not a depot,
and ore parked at one has nothing to do there and no transporter coming for
it.
Also restores two properties the Godot editor dropped from
GasBurnerBuildingDefinition.tres when it re-saved the file:
collected_outputs and capabilities. Without the first, nothing ever collects
the burner's energy - test_gas_burner caught it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 19:23:35 +02:00
|
|
|
var weapon: Node = _component(ship, "weapon")
|
#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
|
|
|
if weapon != null:
|
|
|
|
|
weapon.set_fire_direction(Vector2.ZERO)
|
|
|
|
|
|
|
|
|
|
|
2026-09-03 00:12:08 +02:00
|
|
|
func _nearest_target(ship: Node2D) -> Node2D:
|
#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 nearest: Node2D = null
|
2026-09-03 00:12:08 +02:00
|
|
|
var best_distance: float = INF
|
|
|
|
|
var best_priority: int = -1
|
|
|
|
|
for target_node in get_tree().get_nodes_in_group(COMBAT_TARGET.GROUP):
|
|
|
|
|
if not is_instance_valid(target_node) or not (target_node is Node2D) \
|
|
|
|
|
or target_node.is_queued_for_deletion():
|
#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
|
|
|
continue
|
2026-09-03 00:12:08 +02:00
|
|
|
var target: Node2D = target_node as Node2D
|
|
|
|
|
var at: Vector2 = target.global_position
|
#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
|
|
|
# Judged from the POST, not from the vessel: otherwise a garrison
|
|
|
|
|
# that had drifted out to its leash would see further still, and
|
|
|
|
|
# could be walked away from its outpost one enemy at a time.
|
|
|
|
|
if station.distance_to(at) > leash_range + engage_distance:
|
|
|
|
|
continue
|
|
|
|
|
var d: float = ship.global_position.distance_to(at)
|
2026-09-03 00:12:08 +02:00
|
|
|
if d > detection_range:
|
|
|
|
|
continue
|
|
|
|
|
var priority: int = COMBAT_TARGET.priority(target)
|
|
|
|
|
if priority > best_priority or (priority == best_priority and d < best_distance):
|
|
|
|
|
best_priority = priority
|
|
|
|
|
best_distance = d
|
|
|
|
|
nearest = target
|
#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 nearest
|