spaceship/enemies/EnemyTargets.gd
Loïc Wisniewski bd0cc842ff #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

66 lines
2.4 KiB
GDScript

class_name EnemyTargets
extends RefCounted
# #101 (#97c): who a wave enemy is coming for.
#
# Before this, every enemy component asked the same question - "which player
# SHIP is nearest?" - and a structure was scenery they flew straight past.
# #97 makes waves go after what the player has built, so the question
# becomes "which player thing is nearest", asked in one place so pursuit,
# standoff, the lancer's lunge and the ranged weapon cannot drift into
# disagreeing about it.
#
# There is one thumb on the scale. A ship inside SHIP_PRIORITY_RANGE always
# wins, even if a building is nearer: an enemy that ignored the vessels
# shooting it in order to reach a depot two hundred pixels closer would read
# as broken, and would make the fleet unable to defend anything by
# interposing itself.
const SHIP_PRIORITY_RANGE: float = 520.0
# The GROUP, not Fleet.ships. #105 puts vessels at outposts that are not in
# the fleet at all, and an enemy that ignored the guns actually shooting at
# it - because they were not in the player's formation - would let a
# garrison hold a position without ever being fought back.
static func nearest_ship(node: Node, from: Vector2) -> Node2D:
var nearest: Node2D = null
var best: float = INF
for ship in node.get_tree().get_nodes_in_group("player_ships"):
if not is_instance_valid(ship) or ship.is_queued_for_deletion():
continue
var d: float = from.distance_to((ship as Node2D).global_position)
if d < best:
best = d
nearest = ship
return nearest
static func nearest_building(node: Node, from: Vector2) -> Node2D:
var nearest: Node2D = null
var best: float = INF
for building in node.get_tree().get_nodes_in_group("buildings"):
if not is_instance_valid(building) or building.is_queued_for_deletion():
continue
var d: float = from.distance_to(building.global_position)
if d < best:
best = d
nearest = building
return nearest
static func nearest_target(node: Node, from: Vector2) -> Node2D:
var ship: Node2D = nearest_ship(node, from)
if ship != null and from.distance_to(ship.global_position) <= SHIP_PRIORITY_RANGE:
return ship
var building: Node2D = nearest_building(node, from)
if building == null:
return ship
if ship == null:
return building
return ship if from.distance_to(ship.global_position) \
<= from.distance_to(building.global_position) else building
static func is_building(target: Node) -> bool:
return target != null and target.is_in_group("buildings")