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