187 lines
7.5 KiB
GDScript
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
|