Three events get a sound: a shot leaving a gun, a vessel dying, a structure coming down. The sounds are SYNTHESISED rather than downloaded - audio/make_sfx.py generates all three from scratch, and is committed alongside them. The ticket asked for free sources and the good ones are in its comments (Kenney's CC0 packs, OpenGameArt's CC0 sets, Freesound filtered to CC0), but a generated sound has no licence to track, no attribution to keep, and no binary in the repository whose provenance nobody can check a year from now. It also makes the sounds DATA: too sharp a shot is a number in that file and a re-run, not a hunt through a library. Dropping in a pack file later means replacing a .wav and nothing else. A voice POOL, not a player, because these overlap constantly - a fleet firing is several shots inside one shot's length. Twelve voices, and when they are all busy the one furthest through its own sound is recycled, which is the one with the least left to lose. Parented to the tree root, for the reason that matters: every one of these sounds is started by something that is being freed on the same frame. A dying ship cannot own the sound of its own death. Levels are per sound rather than shared - a shot fires constantly and a death happens once, so equal volumes are not proportionate ones - and shots get a little pitch spread so a stream of them does not flatten into one machine-gun note. The deaths get none: a wobbling explosion sounds broken rather than varied. The test goes at the failures a play-through would hide for weeks: firing three times the pool size must leave exactly the pool's worth sounding and no leaked voices, and the ship-death case is checked from a node that really has been freed on that frame.
115 lines
4.9 KiB
GDScript
115 lines
4.9 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)
|
|
# #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
|