spaceship/controllers/FireStickController.gd
2026-09-01 23:50:05 +02:00

187 lines
7.5 KiB
GDScript

extends Node2D
# Points every armed vessel's gun wherever the right-hand stick is pushed.
#
# #172: what it points them at is a PLACE, not a heading. The mouse names one
# directly; a pushed stick names a point along it, with the push magnitude
# choosing the distance from the fleet up to the guns' reach. Each hull then
# works out its own heading to that place, so the fleet's fire converges there
# instead of leaving in parallel - see VesselWeaponComponent.
#
# The fleet's guns used to seek their own targets and fire on their own. That
# made combat something that happened TO the player rather than something they
# did, and left the right half of the screen with nothing on it. Aiming is now
# the second stick, and this is the only thing that tells a weapon to fire.
#
# Deliberately a scene node rather than state on the fleet or in GameState:
# it is input plumbing, it belongs to whichever scene is showing the stick,
# and a fleet that moves between scenes should arrive with its guns silent
# until the local stick says otherwise.
# The aim guide: a dotted line from the fleet along the heading being fired,
# out to the guns' own range. Deliberately faint and broken rather than a
# solid beam - it has to answer "where will this go and how far" at a glance
# without competing with the shots themselves for attention.
const GUIDE_DASH: float = 14.0
const GUIDE_GAP: float = 16.0
const GUIDE_WIDTH: float = 2.0
const GUIDE_COLOR: Color = Color(1.0, 0.55, 0.38, 0.28)
# #172: the fleet aims at a PLACE now, so the guide ends in a mark on that
# place rather than trailing off into the dark. Four ticks around a gap, not
# a filled dot - the thing being aimed at has to stay visible inside it.
const RETICLE_RADIUS: float = 22.0
const RETICLE_TICK: float = 11.0
const RETICLE_COLOR: Color = Color(1.0, 0.62, 0.42, 0.55)
# By path rather than by class name, for the load-order reason documented in
# PlanetSetups and elsewhere.
const WEAPON_COMPONENT = preload("res://vessels/VesselWeaponComponent.gd")
@onready var fleet: Node2D = get_node("../Fleet")
# Optional so a scene without a fire stick (a preview, a test harness) still
# runs - the guns simply stay quiet, which is the correct resting state.
@onready var stick: Node = get_node_or_null("../UI/FireStick")
# The guide's heading, measured from the fleet's centre to the aimed point.
# Derived rather than input since #172 - it is what gets DRAWN, not what gets
# fired: each hull works out its own heading to the point (see
# VesselWeaponComponent.firing_heading).
var aim_direction: Vector2 = Vector2.ZERO
# Where the fleet is shooting, in world space, and whether it is shooting at
# all. #172: a place, not a heading, so the fleet's fire converges on it.
var aim_point: Vector2 = Vector2.ZERO
var has_aim: bool = false
# Set by MouseCombatController (#80) when a desktop player holds the left
# button: the world point under the cursor, which is already a place and needs
# no converting. Kept as a second input rather than a second aimer, so there is
# still exactly one thing in the scene that points a gun.
var external_target: Vector2 = Vector2.ZERO
var has_external_target: bool = false
func _process(_delta: float) -> void:
var direction: Vector2 = stick.direction if stick != null else Vector2.ZERO
var was_aiming: bool = has_aim
if direction != Vector2.ZERO:
# The stick wins when both are live: a finger on it is a deliberate
# act, a mouse resting somewhere is not.
aim_at(stick_point(direction))
elif has_external_target:
aim_at(external_target)
else:
hold_fire()
# The guide starts at the fleet, which is usually moving, so this redraws
# while aiming rather than only when the heading changes. Once the stick is
# released it redraws once more to clear the line, then stops.
if has_aim or was_aiming:
queue_redraw()
# #172 made the stick resolve to a point so fleet fire converges. The stick
# already reports both direction and a 0..1 displacement; preserve that second
# value so a phone player can pull the convergence point nearer or push it all
# the way to the guns' reach.
func stick_point(direction: Vector2) -> Vector2:
if fleet == null or direction == Vector2.ZERO:
return Vector2.ZERO
return fleet.get_centroid() + direction.limit_length(1.0) * maxf(guide_range(), 1.0)
# Also the entry point for anything driving the guns without a stick - tests
# aim through this rather than reaching into each weapon component, so what
# they exercise is the same path the stick uses. Takes a HEADING, which is
# what a stick has; aim_at() takes the place it resolves to.
func aim(direction: Vector2) -> void:
if direction == Vector2.ZERO:
hold_fire()
return
aim_at(stick_point(direction))
# Points every armed hull at one place. Each works out its own heading to it,
# which is the whole of #172: two ships either side of a target now both hit
# it instead of both missing it by half the gap between them.
func aim_at(point: Vector2) -> void:
if fleet == null:
return
aim_point = point
has_aim = true
aim_direction = point - fleet.get_centroid()
for ship in fleet.ships:
if not is_instance_valid(ship):
continue
var weapon: Node = ship.get_component("weapon")
if weapon != null:
weapon.aim_at(point)
func hold_fire() -> void:
has_aim = false
aim_direction = Vector2.ZERO
if fleet == null:
return
for ship in fleet.ships:
if not is_instance_valid(ship):
continue
var weapon: Node = ship.get_component("weapon")
if weapon != null:
weapon.hold_fire()
# How far the guide reaches: the actual range of a gun that is actually
# aboard, so the line never promises reach the fleet does not have.
func guide_range() -> float:
for ship in fleet.ships:
if not is_instance_valid(ship):
continue
var weapon: Node = ship.get_component("weapon")
if weapon != null:
return float(weapon.fire_range)
return 0.0
func _draw() -> void:
if not has_aim or aim_direction == Vector2.ZERO or fleet == null \
or fleet.ships.is_empty():
return
var reach: float = guide_range()
if reach <= 0.0:
return
var origin: Vector2 = fleet.get_centroid()
# Drawn along the heading a shot will ACTUALLY take, assist included, so
# the line keeps telling the truth about where fire is going (#89).
var heading: Vector2 = WEAPON_COMPONENT.assisted_heading(get_tree(), origin,
aim_direction, reach)
# #172: the line stops at the aimed PLACE when that is inside the guns'
# reach. Running it on to full range past a point the fleet is converging
# on would be drawing a direction again, which is what the ticket took
# away. Beyond reach it stops at reach, as it always did - the guide never
# promises further than the guns carry (#86).
var length: float = minf(aim_direction.length(), reach)
var travelled: float = GUIDE_GAP
while travelled < length:
var dash_end: float = minf(travelled + GUIDE_DASH, length)
draw_line(origin + heading * travelled, origin + heading * dash_end,
GUIDE_COLOR, GUIDE_WIDTH, true)
travelled = dash_end + GUIDE_GAP
_draw_reticle(origin + heading * length)
# Four ticks around the aimed point, open in the middle so whatever is being
# shot at stays visible inside them.
func _draw_reticle(at: Vector2) -> void:
for step in range(4):
var out: Vector2 = Vector2.RIGHT.rotated(TAU * float(step) / 4.0)
draw_line(at + out * RETICLE_RADIUS,
at + out * (RETICLE_RADIUS + RETICLE_TICK),
RETICLE_COLOR, GUIDE_WIDTH, true)
func is_firing() -> bool:
for ship in fleet.ships:
if not is_instance_valid(ship):
continue
var weapon: Node = ship.get_component("weapon")
if weapon != null and weapon.is_firing():
return true
return false