diff --git a/scripts/game/player_destruction_effect.gd b/scripts/game/player_destruction_effect.gd index 4bb30db..912080c 100644 --- a/scripts/game/player_destruction_effect.gd +++ b/scripts/game/player_destruction_effect.gd @@ -31,7 +31,8 @@ func configure(sprite: Sprite2D, drift: Vector2, camera: Camera2D) -> void: global_position = sprite.global_position global_rotation = sprite.global_rotation _ship_scale = sprite.global_scale - _drift = drift.rotated(-global_rotation) * 0.12 + # The whole effect inherits world velocity; fragment velocities are relative. + _drift = drift _camera = camera if is_instance_valid(_camera): _camera_offset = _camera.offset @@ -39,6 +40,9 @@ func configure(sprite: Sprite2D, drift: Vector2, camera: Camera2D) -> void: func _ready() -> void: z_index = 45 light_mask = 4 + texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR + # Animation advances in render frames, so don't interpolate its transform again. + physics_interpolation_mode = Node.PHYSICS_INTERPOLATION_MODE_OFF _rng.randomize() _build_fragments() _prepare_smoke_texture() @@ -81,14 +85,15 @@ func _build_fragments() -> void: uv.append(vertex / dimensions) var wreck := origin.length() < 12.0 var radial := origin.normalized() if origin.length_squared() > 0.1 else Vector2.RIGHT.rotated(_rng.randf_range(0, TAU)) - _fragments.append({"points": points, "uv": uv, "position": origin, "velocity": radial.rotated(_rng.randf_range(-0.45, 0.45)) * _rng.randf_range(65, 155) + _drift if not wreck else radial * 5.0, "angle": 0.0, "spin": _rng.randf_range(-5, 5) if not wreck else _rng.randf_range(-0.3, 0.3), "wreck": wreck}) + _fragments.append({"points": points, "uv": uv, "position": origin, "velocity": radial.rotated(_rng.randf_range(-0.45, 0.45)) * _rng.randf_range(65, 155) if not wreck else radial * 5.0, "angle": 0.0, "spin": _rng.randf_range(-5, 5) if not wreck else _rng.randf_range(-0.3, 0.3), "wreck": wreck}) func _process(delta: float) -> void: + global_position += _drift * delta if _done and _elapsed >= DURATION + 1.8: - _smoke.clear() - _sparks.clear() - set_process(false) - queue_redraw() + if not _smoke.is_empty() or not _sparks.is_empty(): + _smoke.clear() + _sparks.clear() + queue_redraw() return _elapsed += delta if not _detonated and _elapsed < CHARGE_TIME: @@ -191,13 +196,14 @@ func _emit_smoke() -> void: position = _burns[_rng.randi_range(0, _burns.size() - 1)]["position"] if not _burns.is_empty() else _hull_burn_position() elif not _fragments.is_empty() and _rng.randf() < 0.7: position = _fragments[_rng.randi_range(0, _fragments.size() - 1)]["position"] - _smoke.append({"position": position, "velocity": Vector2(_rng.randf_range(-12, 12), _rng.randf_range(-22, 8)), "age": 0.0, "life": _rng.randf_range(0.9, 1.7), "radius": _rng.randf_range(5, 12)}) + _smoke.append({"position": position, "velocity": Vector2(_rng.randf_range(-12, 12), _rng.randf_range(-22, 8)) - _drift.rotated(-global_rotation) * 0.25, "age": 0.0, "life": _rng.randf_range(0.9, 1.7), "radius": _rng.randf_range(5, 12)}) func _update_camera() -> void: if not is_instance_valid(_camera): return var time := _elapsed - CHARGE_TIME var strength := 0.8 if time < 0.0 else 7.0 * exp(-5.0 * time) + _camera.global_position = global_position _camera.offset = _camera_offset + Vector2(sin(_elapsed * 83), cos(_elapsed * 67)) * strength func _restore_camera() -> void: @@ -238,10 +244,6 @@ func _draw() -> void: for layer in 3: draw_arc(Vector2.ZERO, 18 + time * 230 + layer * 2, 0, TAU, 96, Color(0.85, 0.75, 0.6, fade * 0.08), 2, true) _draw_sparks() - if time > 0.8: - for index in 4: - var flicker := maxf(0, sin(_elapsed * 17 + index * 8)) * clampf((3.3 - time) / 2, 0, 1) - draw_circle(Vector2.RIGHT.rotated(index * 2.3) * (4 + index * 2), 2 + flicker * 2, Color(1, 0.28, 0.04, flicker * 0.75)) func _draw_sparks() -> void: for spark in _sparks: diff --git a/tests/items/player_destruction_test.gd b/tests/items/player_destruction_test.gd index 45e78be..9ba2572 100644 --- a/tests/items/player_destruction_test.gd +++ b/tests/items/player_destruction_test.gd @@ -22,8 +22,11 @@ func _ready() -> void: assert(effect.get("_fragments").size() >= 4) assert(not game.has_node("DefeatOverlay")) assert(not effect.has_node("ExplosionAudio")) + var start_position: Vector2 = effect.global_position + assert(effect.get("_drift") == Vector2(100, -40)) effect.finished.connect(func() -> void: _finished_count += 1) effect._process(0.2) + assert(effect.global_position.is_equal_approx(start_position + Vector2(20, -8))) assert(not effect.get("_detonated") and camera.offset != Vector2(3, 5)) for step in 18: effect._process(0.1) @@ -39,6 +42,7 @@ func _ready() -> void: effect._process(0.3) assert(game.has_node("DefeatOverlay") and _finished_count == 1) assert(camera.offset == Vector2(3, 5)) + assert(effect.global_position.is_equal_approx(start_position + Vector2(100, -40) * 5.35)) effect._process(0.1) ship.call("_destroy_ship") assert(_finished_count == 1) @@ -47,6 +51,11 @@ func _ready() -> void: if child.name == "PlayerDestructionEffect": deaths += 1 assert(deaths == 1) + # A drifting wreck must retain its velocity even after the visual cleanup. + effect._process(2.0) + var coasting_position: Vector2 = effect.global_position + effect._process(0.5) + assert(effect.global_position.is_equal_approx(coasting_position + Vector2(50, -20))) for chassis in preload("res://scripts/game/ship_catalog.gd").SHIPS: var source := Sprite2D.new() source.texture = load(preload("res://scripts/game/ship_catalog.gd").texture_path(chassis))