class_name GasBombController extends Node2D const BOMB_COLOR := Color(0.96, 0.22, 0.72) const TARGET_COLOR := Color(0.78, 0.32, 1.0) const MAX_RANGE := 950.0 const AOE_RADIUS := 180.0 const DRAG_RADIUS := 260.0 const AIM_DEADZONE := 22.0 const PROJECTILE_SPEED := 720.0 const DAMAGE := 3 const COOLDOWN := 5.0 @export var minimum_zoom: float = 0.4 @onready var fleet: Node2D = get_node("../Fleet") @onready var camera: Camera2D = get_node("../Camera2D") @onready var button: IconButton = get_node("../UI/GasBombButton") var aiming := false var aim_touch := -1 var aim_start_screen := Vector2.ZERO var aim_screen := Vector2.ZERO var aim_target := Vector2.ZERO var saved_zoom := 1.0 var restoring_zoom := false var cooldown_left := 0.0 var projectile_active := false var projectile_position := Vector2.ZERO var projectile_target := Vector2.ZERO var projectile_trail := PackedVector2Array() var explosion_time := 0.0 var explosion_position := Vector2.ZERO var last_hit_count := 0 func _ready() -> void: button.visible = false queue_redraw() func _process(delta: float) -> void: var was_exploding := explosion_time > 0.0 button.visible = fleet.has_capability("gas_bomber") cooldown_left = maxf(cooldown_left - delta, 0.0) button.enabled = cooldown_left <= 0.0 if not button.visible and aiming: _cancel_aim() if restoring_zoom and not aiming: var z := move_toward(camera.zoom.x, saved_zoom, maxf(saved_zoom, 0.1) * delta * 5.0) camera.zoom = Vector2(z, z) if is_equal_approx(z, saved_zoom): restoring_zoom = false if projectile_active: var distance := projectile_position.distance_to(projectile_target) if distance <= PROJECTILE_SPEED * delta: projectile_position = projectile_target projectile_active = false _explode(projectile_target) else: projectile_position = projectile_position.move_toward(projectile_target, PROJECTILE_SPEED * delta) projectile_trail.append(projectile_position) while projectile_trail.size() > 12: projectile_trail.remove_at(0) if explosion_time > 0.0: explosion_time = maxf(explosion_time - delta, 0.0) if aiming or projectile_active or explosion_time > 0.0 or was_exploding: queue_redraw() func can_begin_aim() -> bool: return button.visible and button.enabled and not button._blocked_by_modal() \ and not aiming and not projectile_active func handle_touch(event: InputEventScreenTouch) -> bool: if event.pressed: if not can_begin_aim() or not Rect2(button.global_position, button.size).has_point(event.position): return false aiming = true aim_touch = event.index aim_start_screen = event.position aim_screen = event.position saved_zoom = camera.zoom.x restoring_zoom = false button.pressed_down = true button.queue_redraw() _update_aim() return true if event.index != aim_touch: return false var should_fire := aim_start_screen.distance_to(aim_screen) >= AIM_DEADZONE if should_fire: _fire() else: _cancel_aim() return true func handle_drag(event: InputEventScreenDrag) -> bool: if not aiming or event.index != aim_touch: return false aim_screen = event.position _update_aim() return true func _update_aim() -> void: var drag := aim_screen - aim_start_screen var origin: Vector2 = fleet.get_centroid() if drag.length() < 0.001: aim_target = origin else: var distance := minf(drag.length() / DRAG_RADIUS, 1.0) * MAX_RANGE aim_target = origin + drag.normalized() * distance _update_aim_zoom(origin, aim_target) queue_redraw() func _update_aim_zoom(origin: Vector2, target: Vector2) -> void: var displacement := target - origin var half_screen := get_viewport_rect().size * 0.5 - Vector2(54.0, 54.0) var fit_x := saved_zoom if absf(displacement.x) < 1.0 else half_screen.x / (absf(displacement.x) + AOE_RADIUS) var fit_y := saved_zoom if absf(displacement.y) < 1.0 else half_screen.y / (absf(displacement.y) + AOE_RADIUS) var desired := clampf(minf(saved_zoom, minf(fit_x, fit_y)), minimum_zoom, saved_zoom) camera.zoom = Vector2(desired, desired) func _finish_aim() -> void: aiming = false aim_touch = -1 button.pressed_down = false button.queue_redraw() restoring_zoom = true func _cancel_aim() -> void: _finish_aim() queue_redraw() func _fire() -> void: var bomber: Node2D = fleet.first_ship_with_capability("gas_bomber") if bomber == null: _cancel_aim() return projectile_active = true projectile_position = bomber.global_position projectile_target = aim_target projectile_trail = PackedVector2Array([projectile_position]) cooldown_left = COOLDOWN _finish_aim() func _explode(at: Vector2) -> void: explosion_position = at explosion_time = 0.8 last_hit_count = 0 for enemy in get_tree().get_nodes_in_group("enemies"): if is_instance_valid(enemy) and enemy is Node2D \ and enemy.global_position.distance_to(at) <= AOE_RADIUS: enemy.take_damage(DAMAGE) last_hit_count += 1 func _draw() -> void: if aiming: var origin: Vector2 = fleet.get_centroid() NeonDraw.draw_glowing_line(self, origin, aim_target, TARGET_COLOR, 0.7, 0.8) _draw_ring(aim_target, AOE_RADIUS, TARGET_COLOR, 0.8) _draw_ring(aim_target, 18.0, BOMB_COLOR, 1.0) if projectile_trail.size() >= 2 and projectile_active: for i in range(1, projectile_trail.size()): var alpha := float(i) / projectile_trail.size() draw_line(projectile_trail[i - 1], projectile_trail[i], Color(BOMB_COLOR, alpha * 0.65), 4.0 + alpha * 4.0, true) if projectile_active: draw_circle(projectile_position, 12.0, Color(BOMB_COLOR, 0.35)) NeonDraw.draw_glowing_circle(self, projectile_position, 7.0, BOMB_COLOR, 1.0) if explosion_time > 0.0: var progress := 1.0 - explosion_time / 0.8 var radius := lerpf(24.0, AOE_RADIUS, minf(progress * 1.5, 1.0)) draw_circle(explosion_position, radius, Color(BOMB_COLOR, (1.0 - progress) * 0.16)) _draw_ring(explosion_position, radius, BOMB_COLOR, 1.0 - progress) for i in range(7): var angle := TAU * float(i) / 7.0 + progress var cloud_at := explosion_position + Vector2(cos(angle), sin(angle)) * radius * 0.55 draw_circle(cloud_at, 18.0 + 14.0 * progress, Color(BOMB_COLOR, (1.0 - progress) * 0.22)) func _draw_ring(center: Vector2, radius: float, color: Color, alpha: float) -> void: draw_arc(center, radius, 0.0, TAU, 64, Color(color, 0.16 * alpha), 16.0, true) draw_arc(center, radius, 0.0, TAU, 64, Color(color, 0.5 * alpha), 6.0, true) draw_arc(center, radius, 0.0, TAU, 64, Color(1.0, 0.86, 0.96, 0.9 * alpha), 2.0, true)