196 lines
8.2 KiB
GDScript
196 lines
8.2 KiB
GDScript
class_name VesselWeaponComponent
|
|
extends Node
|
|
|
|
# The fleet's gun. It does NOT pick its own targets: the player aims it with
|
|
# the fire stick, and it shoots along whatever heading that stick reports for
|
|
# as long as it is held.
|
|
#
|
|
# It used to seek the nearest enemy in range and fire on its own, which meant
|
|
# combat happened whether or not the player did anything - there was nothing
|
|
# to aim, and nothing to decide. Firing is now an action like any other, and
|
|
# the right-hand stick is what performs it.
|
|
#
|
|
# #172: it is aimed at a POINT, not along a heading.
|
|
#
|
|
# Every gun in the fleet used to be handed the SAME direction, so a fleet
|
|
# firing at something threw a sheet of parallel bolts past either side of it
|
|
# and only the middle hull could ever hit. Handed a point instead, each hull
|
|
# works out its own heading to that point, and the fleet's fire converges
|
|
# where the player is actually pointing - which is what having several ships
|
|
# should be worth.
|
|
#
|
|
# The point is set every frame by FireStickController, from the mouse's own
|
|
# world position or from where the fire stick is pushed, and cleared the
|
|
# moment the trigger is let go. No point means not firing, which is the
|
|
# resting state.
|
|
|
|
var capability_id: String = "weapon"
|
|
var fire_range: float
|
|
var fire_cooldown: float
|
|
var projectile_damage: int
|
|
var fire_timer: float = 0.0
|
|
# Where this gun is pointed, in WORLD space, and whether it is pointed at all.
|
|
# A point rather than a Vector2.ZERO sentinel: the origin is a perfectly
|
|
# ordinary place to aim at, and a fleet that happened to be near it would
|
|
# stop firing.
|
|
var fire_target: Vector2 = Vector2.ZERO
|
|
var has_fire_target: bool = false
|
|
var projectile_scene: PackedScene = preload("res://vessels/Projectile.tscn")
|
|
const COMBAT_TARGET = preload("res://core/CombatTarget.gd")
|
|
# By path, not by class name - see the note at the top of MuzzleSparkVFX.
|
|
const MUZZLE_SPARK = preload("res://vfx/MuzzleSparkVFX.gd")
|
|
|
|
|
|
func apply_stats() -> void:
|
|
var ship: Node = get_parent()
|
|
var state: Node = ship.get_tree().root.get_node("GameState")
|
|
fire_range = state.vessel_stat(ship.vessel_type, "fire_range")
|
|
fire_cooldown = state.vessel_stat(ship.vessel_type, "fire_cooldown")
|
|
projectile_damage = int(state.vessel_stat(ship.vessel_type, "damage"))
|
|
|
|
|
|
# Forgejo #89: how far off the aimed heading a target may sit and still be
|
|
# picked up. 28 degrees is wide enough that pointing roughly at something hits
|
|
# it, and narrow enough that the shot still goes where the player pointed -
|
|
# past about 45 the gun starts choosing targets on its own, which is the
|
|
# behaviour #80 deliberately took away.
|
|
const ASSIST_CONE_DEGREES: float = 28.0
|
|
# #181: a nearby cursor is a second way to express intent. This is deliberately
|
|
# smaller than the directional cone at ordinary weapon range: it catches a
|
|
# pointer placed beside something without making the whole screen magnetic.
|
|
const ASSIST_CURSOR_RADIUS: float = 180.0
|
|
|
|
|
|
# The concrete point an aim should stick to. A target can qualify in two ways:
|
|
# it is close to the cursor, or it lies inside the established directional
|
|
# cone. Cursor proximity wins, then distance from the shooter breaks the old
|
|
# directional case. This preserves approximate stick aiming while letting a
|
|
# deliberately placed cursor choose a farther target in the same direction.
|
|
#
|
|
# Targets opt in through one group rather than this utility branching on
|
|
# enemies versus asteroids. That keeps future aimable compositions out of the
|
|
# weapon code: joining the contract is all they need to do.
|
|
static func assisted_point(tree: SceneTree, origin: Vector2, point: Vector2,
|
|
reach: float) -> Vector2:
|
|
var to_point: Vector2 = point - origin
|
|
var heading: Vector2 = to_point.normalized()
|
|
if heading == Vector2.ZERO or tree == null or reach <= 0.0:
|
|
return point
|
|
var cone: float = cos(deg_to_rad(ASSIST_CONE_DEGREES))
|
|
var cursor_pick: Node2D = null
|
|
var cursor_distance: float = INF
|
|
var direction_pick: Node2D = null
|
|
var direction_distance: float = INF
|
|
for target_node in tree.get_nodes_in_group(COMBAT_TARGET.GROUP):
|
|
if not is_instance_valid(target_node) or not (target_node is Node2D) \
|
|
or target_node.is_queued_for_deletion():
|
|
continue
|
|
var target: Node2D = target_node as Node2D
|
|
var to_target: Vector2 = target.global_position - origin
|
|
var distance: float = to_target.length()
|
|
if distance > reach or distance < 0.001:
|
|
continue
|
|
var from_cursor: float = target.global_position.distance_to(point)
|
|
if from_cursor <= ASSIST_CURSOR_RADIUS and from_cursor < cursor_distance:
|
|
cursor_distance = from_cursor
|
|
cursor_pick = target
|
|
if to_target.normalized().dot(heading) >= cone and distance < direction_distance:
|
|
direction_distance = distance
|
|
direction_pick = target
|
|
if cursor_pick != null:
|
|
return cursor_pick.global_position
|
|
if direction_pick != null:
|
|
return direction_pick.global_position
|
|
return point
|
|
|
|
|
|
# Compatibility entry point for systems that genuinely have only a heading.
|
|
# Their virtual cursor sits at maximum range; the returned vector still points
|
|
# at the same target selected by assisted_point().
|
|
static func assisted_heading(tree: SceneTree, origin: Vector2, direction: Vector2,
|
|
reach: float) -> Vector2:
|
|
var heading: Vector2 = direction.normalized()
|
|
if heading == Vector2.ZERO:
|
|
return heading
|
|
var point: Vector2 = assisted_point(tree, origin, origin + heading * reach, reach)
|
|
return (point - origin).normalized()
|
|
|
|
|
|
# Where this gun is firing FROM. The hull it is bolted to.
|
|
func muzzle() -> Vector2:
|
|
var ship: Node2D = get_parent() as Node2D
|
|
return ship.global_position if ship != null else Vector2.ZERO
|
|
|
|
|
|
# Called by FireStickController every frame the trigger is held.
|
|
func aim_at(point: Vector2) -> void:
|
|
fire_target = point
|
|
has_fire_target = true
|
|
|
|
|
|
func hold_fire() -> void:
|
|
has_fire_target = false
|
|
|
|
|
|
# For anything that knows a DISPLACEMENT rather than a place: an outpost
|
|
# garrison has already measured the vector to the enemy it picked (see
|
|
# VesselGarrisonComponent._shoot), and a lone stationed vessel has no fleet
|
|
# to converge with anyway. Zero still means stop.
|
|
func set_fire_direction(direction: Vector2) -> void:
|
|
if direction.length() <= 0.001:
|
|
hold_fire()
|
|
return
|
|
aim_at(muzzle() + direction)
|
|
|
|
|
|
func is_firing() -> bool:
|
|
return has_fire_target
|
|
|
|
|
|
# The heading a shot from THIS hull will actually take: from the muzzle to the
|
|
# aimed point, bent onto an enemy sitting inside the assist cone. Zero when the
|
|
# gun is not firing, or when the point is on top of the ship - there is no
|
|
# heading to a place you are already at.
|
|
func firing_heading() -> Vector2:
|
|
if not has_fire_target:
|
|
return Vector2.ZERO
|
|
var origin: Vector2 = muzzle()
|
|
var to_point: Vector2 = fire_target - origin
|
|
if to_point.length() <= 0.001:
|
|
return Vector2.ZERO
|
|
var point: Vector2 = assisted_point(get_tree(), origin, fire_target, fire_range)
|
|
return (point - origin).normalized()
|
|
|
|
|
|
func tick(delta: float) -> void:
|
|
# The cooldown runs down whether or not the trigger is held, so tapping
|
|
# the stick in a new direction is not punished by a timer that only
|
|
# started when the stick moved.
|
|
fire_timer -= delta
|
|
if not has_fire_target or fire_timer > 0.0:
|
|
return
|
|
# #172: worked out per HULL, from this ship to the aimed point, which is
|
|
# what makes the fleet's fire converge rather than run parallel.
|
|
var heading: Vector2 = firing_heading()
|
|
if heading == Vector2.ZERO:
|
|
return
|
|
var ship: Node2D = get_parent()
|
|
var projectile: Node2D = projectile_scene.instantiate()
|
|
projectile.position = ship.position
|
|
projectile.damage = projectile_damage
|
|
# The bolt stops where the gun's range ends, and the aim guide draws that
|
|
# same range - one number, one meaning.
|
|
projectile.set_range(fire_range)
|
|
ship.get_tree().root.add_child(projectile)
|
|
# The bolt is sent the gun's own range along that heading, whether the
|
|
# aimed point is nearer or further: range is a property of the gun, not of
|
|
# where the player pointed. A point beyond reach is fired toward and falls
|
|
# short; a point inside it is flown through.
|
|
projectile.launch(heading * fire_range)
|
|
# #87: sparks off the hull that fired, so a shot is visibly attached to
|
|
# the vessel it came from.
|
|
MUZZLE_SPARK.spawn(ship.get_tree().root, ship.global_position, heading)
|
|
# #146: and heard. Beside the spark rather than inside it, because the
|
|
# spark is about this hull and the sound is about the shot.
|
|
Sfx.play(ship.get_tree(), Sfx.SHOT)
|
|
fire_timer = fire_cooldown
|