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 projectiles: Array = [] var explosions: Array = [] var last_hit_count := 0 var projectile_active: bool: get: return not projectiles.is_empty() var explosion_time: float: get: var longest := 0.0 for explosion in explosions: longest = maxf(longest, float(explosion.time)) return longest func _ready() -> void: button.visible = false queue_redraw() func _process(delta: float) -> void: var had_visuals := aiming or not projectiles.is_empty() or not explosions.is_empty() button.visible = fleet.has_capability("gas_bomber") button.enabled = _next_ready_bomber() != null 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 for i in range(projectiles.size() - 1, -1, -1): var projectile: Dictionary = projectiles[i] var distance: float = (projectile.position as Vector2).distance_to(projectile.target) if distance <= PROJECTILE_SPEED * delta: projectile.position = projectile.target _explode(projectile.target) projectiles.remove_at(i) else: projectile.position = (projectile.position as Vector2).move_toward(projectile.target, PROJECTILE_SPEED * delta) var trail: PackedVector2Array = projectile.trail trail.append(projectile.position) while trail.size() > 12: trail.remove_at(0) projectile.trail = trail for i in range(explosions.size() - 1, -1, -1): var explosion: Dictionary = explosions[i] explosion.time = maxf(float(explosion.time) - delta, 0.0) if float(explosion.time) <= 0.0: explosions.remove_at(i) if aiming or not projectiles.is_empty() or not explosions.is_empty() or had_visuals: queue_redraw() func can_begin_aim() -> bool: return button.visible and button.enabled and not button._blocked_by_modal() \ and not aiming 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 = _next_ready_bomber() if bomber == null: _cancel_aim() return var component: Node = bomber.get_component("gas_bomber") if not component.consume_shot(COOLDOWN): _cancel_aim() return projectiles.append({ "position": bomber.global_position, "target": aim_target, "trail": PackedVector2Array([bomber.global_position]), }) _finish_aim() button.enabled = _next_ready_bomber() != null func _next_ready_bomber() -> Node2D: for ship in fleet.ships: if not is_instance_valid(ship): continue var component: Node = ship.get_component("gas_bomber") if component != null and component.is_ready(): return ship return null func _explode(at: Vector2) -> void: explosions.append({"position": at, "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) for projectile in projectiles: var trail: PackedVector2Array = projectile.trail if trail.size() >= 2: for i in range(1, trail.size()): var alpha := float(i) / trail.size() draw_line(trail[i - 1], trail[i], Color(BOMB_COLOR, alpha * 0.65), 4.0 + alpha * 4.0, true) draw_circle(projectile.position, 12.0, Color(BOMB_COLOR, 0.35)) NeonDraw.draw_glowing_circle(self, projectile.position, 7.0, BOMB_COLOR, 1.0) for explosion in explosions: var progress := 1.0 - float(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: Vector2 = 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)