extends Node2D # Points every armed vessel's gun wherever the right-hand stick is pushed. # # 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) # 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") var aim_direction: Vector2 = Vector2.ZERO # Set by MouseCombatController (#80) when a desktop player holds the left # button. 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_direction: Vector2 = Vector2.ZERO func _process(_delta: float) -> void: var direction: Vector2 = stick.direction if stick != null else Vector2.ZERO 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. direction = external_direction aim(direction) # 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. var was_aiming: bool = aim_direction != Vector2.ZERO aim_direction = direction if direction != Vector2.ZERO or was_aiming: queue_redraw() # 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. func aim(direction: Vector2) -> void: 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.set_fire_direction(direction) # 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 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) var travelled: float = GUIDE_GAP while travelled < reach: var dash_end: float = minf(travelled + GUIDE_DASH, reach) draw_line(origin + heading * travelled, origin + heading * dash_end, GUIDE_COLOR, GUIDE_WIDTH, true) travelled = dash_end + GUIDE_GAP 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