#130, #135: a vessel's death takes a moment, and leaves an interceptor

#130 asked for the hull to break away from the fleet, drift a few metres,
and go up further off with something more explosive than a light animation.

The wreck is a separate node rather than the Ship staying alive to animate,
and that is the load-bearing part. A dying Ship still in the tree is still
an Area2D in the player_ships group with live weapon components - it would
keep firing, keep being fired at, keep counting as a target, and keep
colliding on the way out. So the ship's own lifecycle is untouched: it
leaves the fleet, emits destroyed, and is freed on exactly the frame it
always was. What is left behind is scenery, and the test pins that.

The explosion at the end was also the quieter of the two the game has.
Killing an enemy was made deliberately loud by #87 while losing one of your
own kept the base class's twelve-particle default - the reward louder than
the loss. It is now the bigger of the two.

#135 is the same scene from the other end: the prologue's convoy flies its
escorts on the corners of the box, which is the exposed part of it, and the
debris belt can take all four. With no interceptor left, the only hulls the
vortex could spare were holds, and the player flew out of the prologue in a
carrier. Two guards now: one interceptor is put back into the convoy at the
top of the vortex, where a vessel appearing among ten being thrown around a
black hole is not something anyone will catch; and the handover checks the
TYPES it hands over rather than only the count - one surviving carrier is
also a fleet of one, and it is not the fleet a new game starts with.
This commit is contained in:
Loïc Wisniewski 2026-08-28 15:04:26 +02:00
commit 710dcbf230
8 changed files with 419 additions and 6 deletions

View file

@ -3,7 +3,20 @@ extends LightBurstVFX
func _init() -> void:
# #130: "a rather more explosive explosion than just a light animation".
# This used to take the base class's defaults - 12 particles, half a
# second, no shockwave - which made losing one of your own vessels a
# quieter event on screen than killing an enemy, whose explosion #87
# had already been told to be the loudest thing in the frame. Now it is
# the louder of the two, which is the right way round: the enemy's is a
# reward, this one is a loss.
color = Color(0.25, 0.85, 1.0)
particle_count = 30
lifetime = 0.8
min_speed = 150.0
max_speed = 540.0
shockwave_radius = 120.0
shockwave_color = Color(0.6, 0.95, 1.0)
static func spawn(parent: Node, world_position: Vector2) -> void:

113
vfx/ShipWreckVFX.gd Normal file
View file

@ -0,0 +1,113 @@
class_name ShipWreckVFX
extends Node2D
# Forgejo #130: a vessel's death, as a short sequence rather than a moment.
#
# A ship used to vanish on the frame it was hit and leave a twelve-particle
# puff behind - lighter than the one an ENEMY gets (see EnemyExplosionVFX,
# which #87 made deliberately loud). Losing a hull is the more expensive
# event of the two and it read as the cheaper one.
#
# So the hull comes off the formation and keeps going: it breaks away from
# the fleet, tumbling and venting, for the length of DRIFT_SECONDS, and only
# then goes up - out where it has some room, rather than inside the wedge it
# was flying in.
#
# It is a separate node rather than the Ship staying alive a little longer,
# and that is the important part. A dying Ship still in the tree is still an
# Area2D in the player_ships group with live weapon components: it would keep
# shooting, keep being shot at, keep counting as a target, and keep colliding
# on its way out. Everything about the ship's own lifecycle - the `destroyed`
# signal, leaving Fleet.ships, freeing the node - happens exactly when it did
# before. What is left behind is scenery.
const DRIFT_SECONDS: float = 0.62
# How hard it is pushed out of the formation. Enough to clear the wedge in
# the time above, not so much that it reads as being thrown.
const BREAKAWAY_SPEED: float = 300.0
const DRAG: float = 0.72
const SPIN_MIN: float = 2.4
const SPIN_MAX: float = 7.0
const VENT_COUNT: int = 4
const VENT_SPEED: float = 90.0
# The hull darkens as it goes, so the drift reads as dying rather than as
# flying away.
const FADE_TO: float = 0.35
var hull: PackedVector2Array = PackedVector2Array()
var glow_color: Color = Color(0.25, 0.85, 1.0)
var core_color: Color = Color.WHITE
var velocity: Vector2 = Vector2.ZERO
var spin: float = 0.0
var age: float = 0.0
var vents: Array = []
var _blown: bool = false
# `away` is the direction out of the formation, so the wreck leaves the fleet
# rather than drifting through the middle of it.
static func spawn(parent: Node, ship: Node2D, away: Vector2) -> ShipWreckVFX:
var wreck := ShipWreckVFX.new()
wreck.position = ship.position
wreck.rotation = ship.rotation
wreck.glow_color = ship.glow_color
wreck.core_color = ship.core_color
if ship.definition != null:
wreck.hull = ship.definition.hull_points
var drift: Vector2 = away if away.length() > 0.01 else Vector2.UP.rotated(ship.rotation)
# Its own momentum plus the push: a ship hit at speed carries on the way
# it was already going.
wreck.velocity = ship.velocity + drift.normalized() * BREAKAWAY_SPEED
wreck.spin = randf_range(SPIN_MIN, SPIN_MAX) * (1.0 if randf() < 0.5 else -1.0)
parent.add_child(wreck)
return wreck
func _ready() -> void:
if hull.is_empty():
hull = PackedVector2Array([Vector2(0, -22), Vector2(-14, 16), Vector2(14, 16)])
for i in range(VENT_COUNT):
var angle: float = TAU * float(i) / float(VENT_COUNT) + randf() * 0.6
vents.append({
"pos": Vector2.ZERO,
"vel": Vector2(cos(angle), sin(angle)) * randf_range(VENT_SPEED * 0.4, VENT_SPEED),
"len": randf_range(9.0, 18.0),
})
func _process(delta: float) -> void:
age += delta
if age >= DRIFT_SECONDS:
_blow_up()
return
position += velocity * delta
velocity = velocity.lerp(Vector2.ZERO, clampf(DRAG * delta, 0.0, 1.0))
rotation += spin * delta
for vent in vents:
vent.pos += vent.vel * delta
vent.vel *= 0.94
queue_redraw()
func _blow_up() -> void:
if _blown:
return
_blown = true
# Where it ended up, not where it was hit - the point of the drift.
if is_inside_tree():
ShipExplosionVFX.spawn(get_tree().root, global_position)
queue_free()
func _draw() -> void:
var t: float = clampf(age / DRIFT_SECONDS, 0.0, 1.0)
var fade: float = lerpf(1.0, FADE_TO, t)
NeonDraw.draw_glowing_polygon(self, hull, Color(glow_color, fade), 1.0,
Color(core_color, fade))
# What is coming out of it. Drawn in the wreck's own local space, so the
# vents tumble with the hull the way a breach in it would.
var vent_colour: Color = Color(1.0, 0.72, 0.35, (1.0 - t) * 0.9)
for vent in vents:
var dir: Vector2 = Vector2(vent.vel).normalized()
NeonDraw.draw_glowing_line(self, vent.pos, vent.pos + dir * float(vent.len),
vent_colour, 0.8, 0.4)

1
vfx/ShipWreckVFX.gd.uid Normal file
View file

@ -0,0 +1 @@
uid://c30rnnoa6fkcd