Add artillery enemy with telegraphed AOE (#46)
EnemyArtilleryComponent is a stationary siege piece (slow drift toward the fleet, deliberately no pursuit component) that fires at the fleet centroid on a long cooldown via a standalone ArtilleryShell - telegraph ring (~1.8s, growing radius plus a filling countdown arc) -> slow visible travel -> AOE explosion via Ship.take_damage() on every player_ships node inside, same all-or-nothing shield absorption as every other damage source. The shell is independent of the enemy that fired it (added to the tree root, like EnemyProjectile/Projectile) so the shooter dying mid-flight doesn't cancel an already-telegraphed shot, and the impact point is fixed at fire time - never re-aimed at where the fleet moves to afterward. Part of the #40 conquest-mechanic breakdown.
This commit is contained in:
parent
fc2b5ca74a
commit
def224667d
16 changed files with 455 additions and 1 deletions
112
ArtilleryShell.gd
Normal file
112
ArtilleryShell.gd
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
class_name ArtilleryShell
|
||||
extends Node2D
|
||||
|
||||
# Self-contained telegraph -> travel -> explode sequence for the artillery
|
||||
# enemy's attack (see EnemyArtilleryComponent). Lives independently of the
|
||||
# enemy that fired it once launched - added to the tree root, exactly like
|
||||
# EnemyProjectile/Projectile - so the shooter dying mid-flight doesn't
|
||||
# cancel a shot already telegraphed to the player.
|
||||
#
|
||||
# Shield interaction: a shield charge absorbs this hit entirely, same
|
||||
# all-or-nothing semantics as every other damage source in this game
|
||||
# (VesselShieldComponent.absorb_hit) - _explode() just calls
|
||||
# Ship.take_damage() like everything else and lets that logic decide.
|
||||
|
||||
signal finished
|
||||
|
||||
@export var telegraph_time: float = 1.8
|
||||
@export var shell_speed: float = 180.0
|
||||
@export var aoe_radius: float = 220.0
|
||||
@export var damage: int = 3
|
||||
@export var color: Color = Color(1.0, 0.5, 0.2)
|
||||
|
||||
enum Phase { TELEGRAPHING, TRAVELING, EXPLODING }
|
||||
|
||||
const EXPLOSION_DURATION: float = 0.8
|
||||
|
||||
var phase: int = Phase.TELEGRAPHING
|
||||
var target: Vector2 = Vector2.ZERO
|
||||
var shell_pos: Vector2 = Vector2.ZERO
|
||||
var telegraph_left: float = 0.0
|
||||
var explosion_time: float = 0.0
|
||||
var trail: PackedVector2Array = PackedVector2Array()
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("artillery_shells")
|
||||
|
||||
|
||||
func launch(from: Vector2, at: Vector2) -> void:
|
||||
target = at
|
||||
shell_pos = from
|
||||
telegraph_left = telegraph_time
|
||||
phase = Phase.TELEGRAPHING
|
||||
position = target
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
match phase:
|
||||
Phase.TELEGRAPHING:
|
||||
telegraph_left -= delta
|
||||
if telegraph_left <= 0.0:
|
||||
phase = Phase.TRAVELING
|
||||
trail = PackedVector2Array([shell_pos])
|
||||
queue_redraw()
|
||||
Phase.TRAVELING:
|
||||
var to_target: Vector2 = target - shell_pos
|
||||
var step: float = shell_speed * delta
|
||||
if to_target.length() <= step:
|
||||
shell_pos = target
|
||||
_explode()
|
||||
else:
|
||||
shell_pos += to_target.normalized() * step
|
||||
trail.append(shell_pos)
|
||||
while trail.size() > 14:
|
||||
trail.remove_at(0)
|
||||
position = shell_pos
|
||||
queue_redraw()
|
||||
Phase.EXPLODING:
|
||||
explosion_time -= delta
|
||||
queue_redraw()
|
||||
if explosion_time <= 0.0:
|
||||
finished.emit()
|
||||
queue_free()
|
||||
|
||||
|
||||
func _explode() -> void:
|
||||
phase = Phase.EXPLODING
|
||||
explosion_time = EXPLOSION_DURATION
|
||||
position = target
|
||||
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)
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
match phase:
|
||||
Phase.TELEGRAPHING:
|
||||
var progress: float = clampf(1.0 - telegraph_left / maxf(telegraph_time, 0.001), 0.0, 1.0)
|
||||
# Growing ring plus a filling countdown arc: the warning reads
|
||||
# clearly without needing a number on screen.
|
||||
var radius: float = lerpf(24.0, aoe_radius, progress)
|
||||
_draw_ring(radius, 0.7)
|
||||
draw_arc(Vector2.ZERO, aoe_radius + 14.0, -PI / 2.0, -PI / 2.0 + TAU * progress, 48,
|
||||
Color(color, 0.85), 8.0, true)
|
||||
Phase.TRAVELING:
|
||||
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_circle(Vector2.ZERO, 14.0, Color(color, 0.35))
|
||||
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)
|
||||
|
||||
|
||||
func _draw_ring(radius: float, alpha: float) -> void:
|
||||
draw_arc(Vector2.ZERO, radius, 0.0, TAU, 64, Color(color, 0.16 * alpha), 16.0, true)
|
||||
draw_arc(Vector2.ZERO, radius, 0.0, TAU, 64, Color(color, 0.5 * alpha), 6.0, true)
|
||||
draw_arc(Vector2.ZERO, radius, 0.0, TAU, 64, Color(1.0, 0.92, 0.86, 0.9 * alpha), 2.0, true)
|
||||
Loading…
Reference in a new issue