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)