#114: the enemy bomber flies, and its bombs arrive

It was a siege piece that crept at 20px/s and lobbed a shell taking five
seconds to cross the gap. That read as a thing that was stuck rather than a
thing that was dangerous: by the time a shell landed the fleet had wandered
off, and the tower was still approaching.

It now moves at 150px/s, faces where it is going, and its bombs travel at
460 - fast enough to arrive while the target is roughly where it was aimed,
slow enough that the impact cross is a warning rather than a formality.

The hull is a bomber rather than a lumpy octagon: swept wings, a short
fuselage, a bay under it. The bomb has fins and a hot core down its smoke
trail, and the blast throws real debris - a BombBlastVFX built from the same
LightBurstVFX as every other explosion, plus a second ring chasing the first
outwards and a white flash that is gone almost at once.

It also bombs what the player has BUILT, through the shared targeting rather
than the fleet alone, and its blast damages structures - it had been asking
Fleet directly and hitting only player_ships, which was already out of step
with #101.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Loïc Wisniewski 2026-08-23 21:58:56 +02:00
commit 0c48b5e739
4 changed files with 109 additions and 31 deletions

View file

@ -18,7 +18,7 @@ signal finished
# SEE coming and the cross marking where it will land. There used to be a
# 1.8s charge-up before anything left the barrel, which arrived long before
# the threat was legible and mostly read as the tower being stuck.
@export var shell_speed: float = 180.0
@export var shell_speed: float = 460.0
@export var aoe_radius: float = 220.0
@export var damage: int = 3
@export var color: Color = Color(1.0, 0.5, 0.2)
@ -30,6 +30,13 @@ const EXPLOSION_DURATION: float = 0.8
# already says that something is coming. A full blast-radius ring here read
# as an attack that had already happened.
const IMPACT_CROSS_RADIUS: float = 20.0
# #114: the blast throws real debris now. The ring alone read as a decal
# being stamped on the map; a burst gives the moment some weight, and it is
# the same LightBurstVFX every other explosion in the game is built from.
const BLAST_VFX = preload("res://vfx/BombBlastVFX.gd")
# Fins on the falling bomb, so it reads as an object with a direction rather
# than a travelling dot.
const FIN_LENGTH: float = 13.0
var phase: int = Phase.TRAVELING
var target: Vector2 = Vector2.ZERO
@ -78,9 +85,16 @@ func _explode() -> void:
phase = Phase.EXPLODING
explosion_time = EXPLOSION_DURATION
position = target
BLAST_VFX.spawn(get_tree().root, target, color)
for ship in get_tree().get_nodes_in_group("player_ships"):
if is_instance_valid(ship) and ship.global_position.distance_to(target) <= aoe_radius:
ship.take_damage(damage)
# #101: a bomb does not care whether what it landed on flies. The hitbox
# is a child Area2D of the Building, which is a plain Node2D and would
# otherwise be unhittable.
for hitbox in get_tree().get_nodes_in_group("player_buildings"):
if is_instance_valid(hitbox) and hitbox.global_position.distance_to(target) <= aoe_radius:
hitbox.take_damage(damage)
func _draw() -> void:
@ -91,14 +105,34 @@ func _draw() -> void:
_draw_impact_cross(target - shell_pos)
for i in range(1, trail.size()):
var alpha: float = float(i) / trail.size()
draw_line(trail[i - 1] - shell_pos, trail[i] - shell_pos, Color(color, alpha * 0.6), 5.0, true)
draw_line(trail[i - 1] - shell_pos, trail[i] - shell_pos,
Color(color, alpha * 0.6), 5.0, true)
# A hot core down the middle of the smoke, brightest at the
# bomb itself.
draw_line(trail[i - 1] - shell_pos, trail[i] - shell_pos,
Color(1.0, 0.94, 0.86, alpha * 0.5), 1.6, true)
draw_circle(Vector2.ZERO, 14.0, Color(color, 0.35))
# Fins, laid across the direction of travel.
var heading: Vector2 = (target - shell_pos)
heading = heading.normalized() if heading.length() > 0.001 else Vector2.DOWN
var across: Vector2 = Vector2(-heading.y, heading.x) * FIN_LENGTH * 0.55
var tail: Vector2 = -heading * FIN_LENGTH
NeonDraw.draw_glowing_line(self, tail - across, tail + across, color, 0.9, 0.4)
NeonDraw.draw_glowing_line(self, tail, heading * 6.0, color, 0.9, 0.45)
NeonDraw.draw_glowing_circle(self, Vector2.ZERO, 8.0, color, 1.0)
Phase.EXPLODING:
var eprogress: float = 1.0 - explosion_time / EXPLOSION_DURATION
var eradius: float = lerpf(30.0, aoe_radius, minf(eprogress * 1.5, 1.0))
draw_circle(Vector2.ZERO, eradius, Color(color, (1.0 - eprogress) * 0.18))
_draw_ring(eradius, 1.0 - eprogress)
# A second ring chasing the first outwards, and a white flash
# that is gone almost at once - the two together are what make a
# blast read as a shock rather than a circle growing.
if eprogress < 0.55:
_draw_ring(eradius * 0.62, (0.55 - eprogress) * 1.6)
if eprogress < 0.18:
draw_circle(Vector2.ZERO, eradius * 0.7,
Color(1.0, 0.95, 0.88, (0.18 - eprogress) * 3.0))
# A small cross where the shell will land, plus a dot at the exact point.