class_name ArtilleryShell extends Node2D # Self-contained 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 shell already in the air. # # 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 # The tower fires as soon as it decides to; the warning is the shell you can # 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 aoe_radius: float = 220.0 @export var damage: int = 3 @export var color: Color = Color(1.0, 0.5, 0.2) enum Phase { TRAVELING, EXPLODING } const EXPLOSION_DURATION: float = 0.8 # The impact cross. Small on purpose: it says WHERE, and the shell in flight # 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 var phase: int = Phase.TRAVELING var target: Vector2 = Vector2.ZERO var shell_pos: Vector2 = Vector2.ZERO 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 phase = Phase.TRAVELING trail = PackedVector2Array([shell_pos]) position = shell_pos queue_redraw() func _process(delta: float) -> void: match phase: 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.TRAVELING: # Where it will land, drawn on the ground. Local coordinates, # because this node rides along with the shell while it flies. _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_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) # A small cross where the shell will land, plus a dot at the exact point. # Deliberately not a filled circle the size of the blast: the player needs to # read "move off this spot" at a glance, and a large translucent area was # easy to mistake for the explosion itself. func _draw_impact_cross(at: Vector2) -> void: var r: float = IMPACT_CROSS_RADIUS for offset in [Vector2(r, r), Vector2(r, -r)]: draw_line(at - offset, at + offset, Color(color, 0.35), 7.0, true) draw_line(at - offset, at + offset, Color(1.0, 0.92, 0.86, 0.85), 2.5, true) draw_arc(at, r * 0.32, 0.0, TAU, 20, Color(color, 0.9), 2.5, true) 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)