#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>
This commit is contained in:
parent
8122e280b7
commit
bd0cc842ff
5 changed files with 444 additions and 6 deletions
120
vessels/VesselGarrisonComponent.gd
Normal file
120
vessels/VesselGarrisonComponent.gd
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
class_name VesselGarrisonComponent
|
||||
extends Node
|
||||
|
||||
# #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
|
||||
|
||||
@export var orbit_radius: float = 170.0
|
||||
@export var orbit_speed: float = 0.85
|
||||
@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 _angle: float = 0.0
|
||||
var _engaging: bool = false
|
||||
|
||||
|
||||
func setup() -> void:
|
||||
# Start the orbit from where the vessel actually IS, not from 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 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:
|
||||
return _engaging
|
||||
|
||||
|
||||
func tick(delta: float) -> void:
|
||||
var ship: Node2D = get_parent()
|
||||
var target: Node2D = _nearest_enemy(ship)
|
||||
_engaging = target != null
|
||||
if target == null:
|
||||
_patrol(ship, delta)
|
||||
_hold_fire(ship)
|
||||
return
|
||||
_engage(ship, target, delta)
|
||||
|
||||
|
||||
func _patrol(ship: Node2D, delta: float) -> void:
|
||||
_angle += orbit_speed * delta
|
||||
ship.move_target = station + Vector2(cos(_angle), sin(_angle)) * orbit_radius
|
||||
|
||||
|
||||
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
|
||||
# The orbit angle keeps advancing while engaged, so breaking off does not
|
||||
# snap the vessel back to whatever bearing it left on.
|
||||
_angle += orbit_speed * delta
|
||||
|
||||
var weapon: Node = ship.get_component("weapon") if ship.has_method("get_component") else null
|
||||
if weapon == null:
|
||||
return
|
||||
if distance <= weapon.fire_range:
|
||||
weapon.set_fire_direction(to_target)
|
||||
else:
|
||||
weapon.set_fire_direction(Vector2.ZERO)
|
||||
|
||||
|
||||
func _hold_fire(ship: Node2D) -> void:
|
||||
var weapon: Node = ship.get_component("weapon") if ship.has_method("get_component") else null
|
||||
if weapon != null:
|
||||
weapon.set_fire_direction(Vector2.ZERO)
|
||||
|
||||
|
||||
func _nearest_enemy(ship: Node2D) -> Node2D:
|
||||
var nearest: Node2D = null
|
||||
var best: float = detection_range
|
||||
for enemy in get_tree().get_nodes_in_group("enemies"):
|
||||
if not is_instance_valid(enemy) or enemy.is_queued_for_deletion():
|
||||
continue
|
||||
var at: Vector2 = (enemy as Node2D).global_position
|
||||
# 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)
|
||||
if d < best:
|
||||
best = d
|
||||
nearest = enemy
|
||||
return nearest
|
||||
Loading…
Reference in a new issue