#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:
parent
1a2a27b6e8
commit
0c48b5e739
4 changed files with 109 additions and 31 deletions
|
|
@ -8,12 +8,12 @@ script = ExtResource("1")
|
||||||
enemy_id = "artillery"
|
enemy_id = "artillery"
|
||||||
display_name = "Artillery"
|
display_name = "Artillery"
|
||||||
max_hp = 8
|
max_hp = 8
|
||||||
speed = 90.0
|
speed = 215.0
|
||||||
acceleration = 180.0
|
acceleration = 390.0
|
||||||
rotation_speed = 3.0
|
rotation_speed = 4.2
|
||||||
hull_points = PackedVector2Array(0, -30, 14, -22, 16, -6, 26, 2, 22, 18, 10, 26, -10, 26, -22, 18, -26, 2, -16, -6, -14, -22)
|
hull_points = PackedVector2Array(0, -34, 7, -18, 34, -4, 40, 8, 20, 6, 12, 20, 4, 28, -4, 28, -12, 20, -20, 6, -40, 8, -34, -4, -7, -18)
|
||||||
glow_color = Color(1.0, 0.5, 0.2, 1)
|
glow_color = Color(1.0, 0.5, 0.2, 1)
|
||||||
detail_lines = PackedVector2Array(-14, -20, 14, -20, -12, 6, -20, 16, 12, 6, 20, 16, 0, -22, 0, 6)
|
detail_lines = PackedVector2Array(0, -30, 0, 22, -20, 4, 20, 4, -30, 0, -30, 6, 30, 0, 30, 6, -9, 12, 9, 12)
|
||||||
detail_rings = PackedVector2Array(0, -20, 8, 0)
|
detail_rings = PackedVector2Array(0, 14, 9, 0, 0, -16, 5, 0)
|
||||||
component_scenes = Array[PackedScene]([ExtResource("2")])
|
component_scenes = Array[PackedScene]([ExtResource("2")])
|
||||||
contact_damage = 6
|
contact_damage = 6
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ signal finished
|
||||||
# SEE coming and the cross marking where it will land. There used to be a
|
# 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
|
# 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.
|
# 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 aoe_radius: float = 220.0
|
||||||
@export var damage: int = 3
|
@export var damage: int = 3
|
||||||
@export var color: Color = Color(1.0, 0.5, 0.2)
|
@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
|
# already says that something is coming. A full blast-radius ring here read
|
||||||
# as an attack that had already happened.
|
# as an attack that had already happened.
|
||||||
const IMPACT_CROSS_RADIUS: float = 20.0
|
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 phase: int = Phase.TRAVELING
|
||||||
var target: Vector2 = Vector2.ZERO
|
var target: Vector2 = Vector2.ZERO
|
||||||
|
|
@ -78,9 +85,16 @@ func _explode() -> void:
|
||||||
phase = Phase.EXPLODING
|
phase = Phase.EXPLODING
|
||||||
explosion_time = EXPLOSION_DURATION
|
explosion_time = EXPLOSION_DURATION
|
||||||
position = target
|
position = target
|
||||||
|
BLAST_VFX.spawn(get_tree().root, target, color)
|
||||||
for ship in get_tree().get_nodes_in_group("player_ships"):
|
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:
|
if is_instance_valid(ship) and ship.global_position.distance_to(target) <= aoe_radius:
|
||||||
ship.take_damage(damage)
|
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:
|
func _draw() -> void:
|
||||||
|
|
@ -91,14 +105,34 @@ func _draw() -> void:
|
||||||
_draw_impact_cross(target - shell_pos)
|
_draw_impact_cross(target - shell_pos)
|
||||||
for i in range(1, trail.size()):
|
for i in range(1, trail.size()):
|
||||||
var alpha: float = float(i) / 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))
|
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)
|
NeonDraw.draw_glowing_circle(self, Vector2.ZERO, 8.0, color, 1.0)
|
||||||
Phase.EXPLODING:
|
Phase.EXPLODING:
|
||||||
var eprogress: float = 1.0 - explosion_time / EXPLOSION_DURATION
|
var eprogress: float = 1.0 - explosion_time / EXPLOSION_DURATION
|
||||||
var eradius: float = lerpf(30.0, aoe_radius, minf(eprogress * 1.5, 1.0))
|
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_circle(Vector2.ZERO, eradius, Color(color, (1.0 - eprogress) * 0.18))
|
||||||
_draw_ring(eradius, 1.0 - eprogress)
|
_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.
|
# A small cross where the shell will land, plus a dot at the exact point.
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,32 @@
|
||||||
class_name EnemyArtilleryComponent
|
class_name EnemyArtilleryComponent
|
||||||
extends Node
|
extends Node
|
||||||
|
|
||||||
# Stationary siege piece: drifts slowly toward the fleet (deliberately no
|
# The bomber: it moves to where its bombs will land and drops them there.
|
||||||
# pursuit component - closing distance is incidental, not its purpose) and
|
#
|
||||||
# fires a slow, clearly visible AOE shell on a long cooldown via a
|
# #114 turned it from a siege piece into an aircraft. It used to creep at
|
||||||
# standalone ArtilleryShell, so an already-launched shot survives this
|
# 20px/s and lob a shell that took five seconds to arrive, which read as a
|
||||||
# enemy dying mid-flight, same as EnemyProjectile/Projectile already do.
|
# 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 flies at its own speed and its bombs travel at 460.
|
||||||
|
#
|
||||||
|
# The shell is still a standalone ArtilleryShell added to the root, so an
|
||||||
|
# already-dropped bomb survives this enemy dying mid-flight, same as
|
||||||
|
# EnemyProjectile/Projectile already do.
|
||||||
|
|
||||||
var capability_id: String = "artillery"
|
var capability_id: String = "artillery"
|
||||||
|
|
||||||
@export var fire_cooldown: float = 6.0
|
@export var fire_cooldown: float = 4.0
|
||||||
@export var fire_range: float = 900.0
|
@export var fire_range: float = 900.0
|
||||||
@export var shell_speed: float = 180.0
|
# #114: fast enough that the bomb arrives while the target is still roughly
|
||||||
|
# where it was aimed, and slow enough that the impact cross is a warning
|
||||||
|
# rather than a formality.
|
||||||
|
@export var shell_speed: float = 460.0
|
||||||
@export var aoe_radius: float = 220.0
|
@export var aoe_radius: float = 220.0
|
||||||
@export var damage: int = 3
|
@export var damage: int = 3
|
||||||
@export var drift_speed: float = 20.0
|
# It flies now rather than creeping. Kept as its own number rather than the
|
||||||
|
# definition's `speed` so a run-in can be deliberately slower than a chaser's
|
||||||
|
# top speed without making the hull slow.
|
||||||
|
@export var drift_speed: float = 150.0
|
||||||
@export var color: Color = Color(1.0, 0.5, 0.2)
|
@export var color: Color = Color(1.0, 0.5, 0.2)
|
||||||
|
|
||||||
var cooldown_left: float = 0.0
|
var cooldown_left: float = 0.0
|
||||||
|
|
@ -29,10 +41,12 @@ func tick(delta: float) -> void:
|
||||||
if cooldown_left > 0.0:
|
if cooldown_left > 0.0:
|
||||||
return
|
return
|
||||||
|
|
||||||
var fleet: Node2D = get_tree().get_first_node_in_group("player_fleet")
|
# #101/#113: it bombs what the player has BUILT as readily as what they
|
||||||
if fleet == null or fleet.ships.is_empty():
|
# are flying, and asks the shared targeting rather than the fleet alone.
|
||||||
|
var target: Node2D = EnemyTargets.nearest_target(self, enemy.global_position)
|
||||||
|
if target == null:
|
||||||
return
|
return
|
||||||
var centroid: Vector2 = fleet.get_centroid()
|
var centroid: Vector2 = target.global_position
|
||||||
if enemy.global_position.distance_to(centroid) > fire_range:
|
if enemy.global_position.distance_to(centroid) > fire_range:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -47,18 +61,22 @@ func tick(delta: float) -> void:
|
||||||
|
|
||||||
|
|
||||||
func _drift(enemy: Node2D, delta: float) -> void:
|
func _drift(enemy: Node2D, delta: float) -> void:
|
||||||
# Local position throughout, same convention as EnemyPursuitComponent/
|
var target: Node2D = EnemyTargets.nearest_target(self, enemy.global_position)
|
||||||
# EnemyStandoffComponent: valid as long as the enemy's container and
|
if target == null:
|
||||||
# Fleet share a common origin, which every scene in this project does.
|
|
||||||
var fleet: Node2D = get_tree().get_first_node_in_group("player_fleet")
|
|
||||||
if fleet == null or fleet.ships.is_empty():
|
|
||||||
return
|
return
|
||||||
var to_fleet: Vector2 = fleet.get_centroid() - enemy.position
|
var to_target: Vector2 = target.global_position - enemy.global_position
|
||||||
# #69: gated by the same fire_range it already shells within, rather
|
var distance: float = to_target.length()
|
||||||
# than a second tunable that could drift out of sync - outside its own
|
if distance < 1.0:
|
||||||
# firing envelope it has nothing to be drifting toward yet.
|
|
||||||
if to_fleet.length() > fire_range:
|
|
||||||
return
|
return
|
||||||
if to_fleet.length() < 1.0:
|
# #69: gated by the same fire_range it already bombs within, rather than
|
||||||
|
# a second tunable that could drift out of sync - outside its own firing
|
||||||
|
# envelope it has nothing to be flying toward yet. #113: unless it came
|
||||||
|
# in with a wave, in which case it has somewhere to be.
|
||||||
|
if distance > fire_range and not EnemyTargets.ignores_range(enemy):
|
||||||
return
|
return
|
||||||
enemy.position += to_fleet.normalized() * drift_speed * delta
|
var heading: Vector2 = to_target / distance
|
||||||
|
enemy.global_position += heading * drift_speed * delta
|
||||||
|
# #114: it faces where it is going. A bomber that flew sideways was most
|
||||||
|
# of why the old one read as scenery being nudged along.
|
||||||
|
enemy.rotation = lerp_angle(enemy.rotation, heading.angle() + PI / 2.0,
|
||||||
|
enemy.rotation_speed * delta)
|
||||||
|
|
|
||||||
26
vfx/BombBlastVFX.gd
Normal file
26
vfx/BombBlastVFX.gd
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
class_name BombBlastVFX
|
||||||
|
extends LightBurstVFX
|
||||||
|
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
# #114: the bomber's blast. Between a hull going up and a structure
|
||||||
|
# coming down - it is the biggest thing an enemy can do to the player
|
||||||
|
# without destroying anything outright, and it should land like it.
|
||||||
|
color = Color(1.0, 0.5, 0.2)
|
||||||
|
particle_count = 26
|
||||||
|
lifetime = 0.8
|
||||||
|
min_speed = 140.0
|
||||||
|
max_speed = 520.0
|
||||||
|
shockwave_radius = 150.0
|
||||||
|
shockwave_color = Color(1.0, 0.78, 0.4)
|
||||||
|
|
||||||
|
|
||||||
|
# Takes the colour from the shell that threw it, so a bomber recoloured in
|
||||||
|
# its definition does not explode in the default orange.
|
||||||
|
static func spawn(parent: Node, world_position: Vector2,
|
||||||
|
blast_color: Color = Color(1.0, 0.5, 0.2)) -> void:
|
||||||
|
var vfx: BombBlastVFX = BombBlastVFX.new()
|
||||||
|
vfx.position = world_position
|
||||||
|
vfx.color = blast_color
|
||||||
|
vfx.shockwave_color = blast_color.lerp(Color(1.0, 0.95, 0.8), 0.45)
|
||||||
|
parent.add_child(vfx)
|
||||||
Loading…
Reference in a new issue