spaceship/vessels/VesselWeaponComponent.gd
Loïc Wisniewski 5f3615026d #89: the default gun bends onto a target near the aimed heading
Aiming with a thumb on a stick is never exact, and at 950 units a couple of
degrees is a long miss. A shot fired roughly toward an enemy now goes at it.

28 degrees of cone, chosen so that pointing at something hits it while the
shot still visibly goes where the player pointed. Past about 45 the gun starts
picking its own targets, which is the behaviour #80 deliberately removed.

Enemies only, never asteroids. Snapping onto a rock while the player is trying
to hit the thing shooting at them would be the assist working against them,
and rocks are large and stationary enough to hit on purpose.

The aim guide is drawn through the SAME helper the gun fires through, so the
dotted line bends with the shot. A guide showing the raw stick heading while
the bolt took an assisted one would be the same class of mismatch #86 removed
between the guide's length and a bolt's actual reach - and this one would have
been harder to notice, because the line would look right most of the time.

Range 620 -> 950, the other half of the ticket.

Also swept the project root again: Godot keeps resurrecting pre-#84 copies of
moved files there from its own cache. None this time.

Suite 120/120.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 03:19:52 +02:00

112 lines
4.7 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.
#
# `fire_direction` is set every frame by FireStickController from the stick's
# own reading, and cleared the moment it is released. Zero 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
var fire_direction: Vector2 = Vector2.ZERO
var projectile_scene: PackedScene = preload("res://vessels/Projectile.tscn")
# 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
# The heading a shot fired from `origin` along `direction` should actually
# take: straight, unless an enemy sits inside the assist cone and within
# reach, in which case it points at the nearest one.
#
# Static, and used by BOTH the gun and the aim guide (see
# FireStickController._draw). If the guide drew the raw stick heading while
# the shot took an assisted one, the line would be telling the player
# something the gun does not do - the same class of mismatch #86 removed
# between the guide's length and a bolt's actual reach.
#
# Enemies only, never asteroids. Snapping onto a rock while the player is
# trying to hit the thing shooting at them would be the assist working
# against them; rocks are large, stationary enough, and easy to hit on
# purpose.
static func assisted_heading(tree: SceneTree, origin: Vector2, direction: Vector2,
reach: float) -> Vector2:
var heading: Vector2 = direction.normalized()
if heading == Vector2.ZERO or tree == null:
return heading
var cone: float = cos(deg_to_rad(ASSIST_CONE_DEGREES))
var nearest: Vector2 = Vector2.ZERO
var nearest_distance: float = INF
for enemy in tree.get_nodes_in_group("enemies"):
if not is_instance_valid(enemy) or not (enemy is Node2D):
continue
var to_enemy: Vector2 = (enemy as Node2D).global_position - origin
var distance: float = to_enemy.length()
if distance > reach or distance < 0.001 or distance >= nearest_distance:
continue
if to_enemy.normalized().dot(heading) < cone:
continue
nearest_distance = distance
nearest = to_enemy
return heading if nearest == Vector2.ZERO else nearest.normalized()
# Called by FireStickController. A zero vector stops the gun.
func set_fire_direction(direction: Vector2) -> void:
fire_direction = direction if direction.length() > 0.001 else Vector2.ZERO
func is_firing() -> bool:
return fire_direction != Vector2.ZERO
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 fire_direction == Vector2.ZERO or fire_timer > 0.0:
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)
# Aimed by DIRECTION, not at a target: launch() takes a displacement, so
# the gun's own range decides how far the shot is sent.
var heading: Vector2 = assisted_heading(ship.get_tree(), ship.global_position,
fire_direction, fire_range)
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)
fire_timer = fire_cooldown