class_name EnemyArtilleryComponent extends Node # The bomber: it moves to where its bombs will land and drops them there. # # #114 turned it from a siege piece into an aircraft. It used to creep at # 20px/s and lob a shell that took five seconds to arrive, which read as a # thing that was stuck rather than a thing that was dangerous - by the time # a shell landed the fleet had wandered off, and the tower was still # approaching. It now flies at its own speed and its bombs travel at 460. # # The shell is still a standalone ArtilleryShell added to the root, so an # already-dropped bomb survives this enemy dying mid-flight, same as # EnemyProjectile/Projectile already do. var capability_id: String = "artillery" @export var fire_cooldown: float = 4.0 @export var fire_range: float = 900.0 # #114: fast enough that the bomb arrives while the target is still roughly # where it was aimed, and slow enough that the impact cross is a warning # rather than a formality. @export var shell_speed: float = 460.0 @export var aoe_radius: float = 220.0 @export var damage: int = 3 # It flies now rather than creeping. Kept as its own number rather than the # definition's `speed` so a run-in can be deliberately slower than a chaser's # top speed without making the hull slow. @export var drift_speed: float = 150.0 @export var color: Color = Color(1.0, 0.5, 0.2) var cooldown_left: float = 0.0 var shell_scene: PackedScene = preload("res://enemies/ArtilleryShell.tscn") func tick(delta: float) -> void: var enemy: Node2D = get_parent() _drift(enemy, delta) cooldown_left -= delta if cooldown_left > 0.0: return # #101/#113: it bombs what the player has BUILT as readily as what they # are flying, and asks the shared targeting rather than the fleet alone. var target: Node2D = EnemyTargets.nearest_target(self, enemy.global_position) if target == null: return var centroid: Vector2 = target.global_position if enemy.global_position.distance_to(centroid) > fire_range: return var shell: Node2D = shell_scene.instantiate() shell.shell_speed = shell_speed shell.aoe_radius = aoe_radius shell.damage = damage shell.color = color enemy.get_tree().root.add_child(shell) shell.launch(enemy.global_position, centroid) cooldown_left = fire_cooldown func _drift(enemy: Node2D, delta: float) -> void: var target: Node2D = EnemyTargets.nearest_target(self, enemy.global_position) if target == null: return var to_target: Vector2 = target.global_position - enemy.global_position var distance: float = to_target.length() if distance < 1.0: return # #69: gated by the same fire_range it already bombs within, rather than # a second tunable that could drift out of sync - outside its own firing # envelope it has nothing to be flying toward yet. #113: unless it came # in with a wave, in which case it has somewhere to be. if distance > fire_range and not EnemyTargets.ignores_range(enemy): return var heading: Vector2 = to_target / distance enemy.global_position += heading * drift_speed * delta # #114: it faces where it is going. A bomber that flew sideways was most # of why the old one read as scenery being nudged along. enemy.rotation = lerp_angle(enemy.rotation, heading.angle() + PI / 2.0, enemy.rotation_speed * delta)