class_name EnemyBigLaserBeam extends Node2D # Self-contained charge -> fire -> flash sequence for the mothership's big # laser (see EnemyBigLaserComponent). Lives independently of the enemy that # fired it once launched - added to the tree root, exactly like # ArtilleryShell/EnemyProjectile - so the mothership dying mid-charge # doesn't cancel a shot already telegraphed to the player. # # The aim direction is locked once, in launch(), and never touched again: # that is what makes the telegraph dodgeable. Nothing in _process() re-reads # the fleet's position while charging. # # Damage: every ship in group "player_ships" whose perpendicular distance # to the beam segment is within beam_half_width, AND whose projection along # the beam falls inside [0, beam_range], takes Ship.take_damage(damage) - # same all-or-nothing shield semantics as every other damage source in this # game (Ship.take_damage already handles the absorb_hit check itself). signal finished @export var charge_time: float = 2.5 @export var beam_range: float = 1200.0 @export var beam_half_width: float = 35.0 @export var damage: int = 3 @export var color: Color = Color(0.55, 0.05, 0.85) enum Phase { CHARGING, FLASH } const FLASH_DURATION: float = 0.25 var phase: int = Phase.CHARGING var origin: Vector2 = Vector2.ZERO var aim_direction: Vector2 = Vector2.RIGHT var charge_left: float = 0.0 var flash_time: float = 0.0 func launch(from: Vector2, toward: Vector2) -> void: origin = from aim_direction = toward.normalized() if toward.length() > 0.001 else Vector2.RIGHT position = origin rotation = aim_direction.angle() charge_left = charge_time phase = Phase.CHARGING queue_redraw() func _process(delta: float) -> void: match phase: Phase.CHARGING: charge_left -= delta if charge_left <= 0.0: _fire() queue_redraw() Phase.FLASH: flash_time -= delta queue_redraw() if flash_time <= 0.0: finished.emit() queue_free() func _fire() -> void: phase = Phase.FLASH flash_time = FLASH_DURATION for ship in get_tree().get_nodes_in_group("player_ships"): if not is_instance_valid(ship): continue var to_ship: Vector2 = ship.global_position - origin var along: float = to_ship.dot(aim_direction) if along < 0.0 or along > beam_range: continue var perpendicular: float = (to_ship - aim_direction * along).length() if perpendicular <= beam_half_width: ship.take_damage(damage) queue_redraw() func _draw() -> void: # Drawn in local space, rotated in launch() so local +X already points # along the locked aim_direction - the same reason the beam segment's # own math is done in world space up in _fire() (global_position/origin) # while _draw() only ever needs the length along that local +X axis. match phase: Phase.CHARGING: var progress: float = clampf(1.0 - charge_left / maxf(charge_time, 0.001), 0.0, 1.0) var width: float = lerpf(4.0, beam_half_width * 2.0, progress) _draw_beam_line(width, 0.65) # Countdown arc at the origin, the same idiom as # ArtilleryShell's growing-ring-plus-arc telegraph. draw_arc(Vector2.ZERO, 40.0, -PI / 2.0, -PI / 2.0 + TAU * progress, 32, Color(color, 0.85), 8.0, true) Phase.FLASH: var fprogress: float = 1.0 - flash_time / FLASH_DURATION var width: float = lerpf(beam_half_width * 2.2, 6.0, fprogress) _draw_beam_line(width, 1.0 - fprogress * 0.3) func _draw_beam_line(width: float, alpha: float) -> void: # Three-layer glow along local +X - the same soft-halo / core / bright- # hairline structure as ArtilleryShell._draw_ring, adapted from a ring # to a straight line. var to: Vector2 = Vector2(beam_range, 0.0) draw_line(Vector2.ZERO, to, Color(color, 0.16 * alpha), width * 2.6, true) draw_line(Vector2.ZERO, to, Color(color, 0.5 * alpha), width, true) draw_line(Vector2.ZERO, to, Color(1.0, 0.92, 0.98, 0.9 * alpha), width * 0.35, true)