extends Node2D const BEAM_DURATION := 3.0 const PRIMARY_TICK_DMG := 40 const SPLASH_TICK_DMG := 3 const TICK_INTERVAL := 0.5 const SWEEP_RATE := 22.0 @export var segment_size := 16.0 @export var start_offset := 24.0 @onready var perk_effects = get_node("/root/Game/PerkEffects") var beam_seg := preload("res://scenes/beam.tscn") var target: Node2D = null var all_segs: Array = [] var mid_segs: Array = [] var tick_timer := 0.0 var elapsed := 0.0 var done := false var current_angle := 0.0 var sweep_to := 0.0 var sweeping := false func _ready() -> void: target = get_highest_hp_enemy() if target == null: queue_free() return current_angle = global_position.direction_to(target.global_position).angle() rebuild_segments() func _process(delta: float) -> void: if done: return elapsed += delta tick_timer += delta if sweeping: current_angle = lerp_angle(current_angle, sweep_to, 1.0 - exp(-SWEEP_RATE * delta)) if abs(angle_difference(current_angle, sweep_to)) < 0.005: current_angle = sweep_to sweeping = false rebuild_segments() if tick_timer >= TICK_INTERVAL: tick_timer -= TICK_INTERVAL do_damage_tick() if elapsed >= BEAM_DURATION: done = true cleanup() func rebuild_segments() -> void: var dir := Vector2.from_angle(current_angle) var beam_start := global_position + dir * start_offset var dest_dist := global_position.distance_to(target.global_position) var beam_dist := dest_dist - start_offset var n_mid := int(max(0.0, beam_dist - segment_size) / segment_size) var positions: Array = [beam_start] var types: Array = ["start"] for i in range(n_mid): positions.append(beam_start + dir * (segment_size * float(i + 1))) types.append("middle") positions.append(beam_start + dir * (segment_size * float(n_mid + 1))) types.append("end") while all_segs.size() < positions.size(): var seg = beam_seg.instantiate() get_parent().add_child(seg) all_segs.append(seg) while all_segs.size() > positions.size(): var seg = all_segs.pop_back() if is_instance_valid(seg): seg.queue_free() mid_segs.clear() for i in range(all_segs.size()): var seg = all_segs[i] if not is_instance_valid(seg): continue seg.global_position = positions[i] seg.rotation = current_angle if seg.beam_type != types[i]: seg.beam_type = types[i] seg.shape.disabled = types[i] != "middle" if seg.sprite.sprite_frames.has_animation(types[i]): seg.sprite.play(types[i]) if types[i] == "middle": mid_segs.append(seg) if all_segs.size() > 1 and is_instance_valid(all_segs[0]): var ref_frame: int = all_segs[0].sprite.frame var ref_progress: float = all_segs[0].sprite.frame_progress for i in range(1, all_segs.size()): var seg = all_segs[i] if is_instance_valid(seg): seg.sprite.frame = ref_frame seg.sprite.frame_progress = ref_progress func retarget(new_target: Node2D) -> void: target = new_target sweep_to = global_position.direction_to(new_target.global_position).angle() sweeping = true func do_damage_tick() -> void: if (not is_instance_valid(target) or target.is_dying) and perk_effects.laser_retarget_enabled: var new_target = get_highest_hp_enemy() if new_target != null: retarget(new_target) if is_instance_valid(target) and not target.is_dying: target.take_damage(PRIMARY_TICK_DMG) var hit: Array = [] for seg in mid_segs: if not is_instance_valid(seg): continue for body in seg.get_overlapping_bodies(): if body.is_in_group("enemies") and not body.is_dying \ and body != target and not hit.has(body): body.take_damage(SPLASH_TICK_DMG) hit.append(body) func cleanup() -> void: for seg in all_segs: if is_instance_valid(seg): seg.queue_free() queue_free() func get_highest_hp_enemy() -> Node2D: var best: Node2D = null var best_hp: int = -1 for enemy in get_tree().get_nodes_in_group("enemies"): if not is_instance_valid(enemy) or enemy.is_dying: continue if enemy.hp > best_hp: best_hp = enemy.hp best = enemy return best