class_name BossMinion extends Area2D signal destroyed const COMBAT_TARGET = preload("res://core/CombatTarget.gd") @export var marked: bool = false @export var max_hp: int = 5 @export var speed: float = 245.0 @export var acceleration: float = 520.0 @export var expulsion_speed: float = 520.0 @export var dash_speed: float = 760.0 var boss: Node2D const REDRAW_INTERVAL: float = 1.0 / 12.0 const EXPULSION_SECONDS: float = 0.85 const BRAKE_SECONDS: float = 0.42 const DASH_SECONDS: float = 0.48 enum Motion { EXPELLED, CHASING, BRAKING, DASHING } var hp: int = 5 var velocity: Vector2 = Vector2.ZERO var phase: float = 0.0 var dead: bool = false var redraw_left: float = 0.0 var fleet: Node2D = null var motion: int = Motion.CHASING var motion_left: float = 0.0 var dash_cooldown: float = 0.0 var dash_direction: Vector2 = Vector2.ZERO var uses_dash_pattern: bool = false func _ready() -> void: hp = max_hp add_to_group("enemies") add_to_group("boss_minions") COMBAT_TARGET.register(self, COMBAT_TARGET.DANGER_PRIORITY) phase = randf() * TAU fleet = get_tree().get_first_node_in_group("player_fleet") queue_redraw() # The boss throws hatchlings clear of its now much larger silhouette before # they acquire the fleet. Every third hatchling also gets the stop-and-lunge # rhythm requested in #189; the others remain steady pursuers so a wave is # varied rather than uniformly twitchy. func expel(direction: Vector2, dashes: bool) -> void: var outward: Vector2 = direction.normalized() uses_dash_pattern = dashes motion = Motion.EXPELLED motion_left = EXPULSION_SECONDS * randf_range(0.8, 1.2) velocity = outward * expulsion_speed * randf_range(0.85, 1.15) dash_cooldown = randf_range(1.2, 2.8) func _physics_process(delta: float) -> void: phase += delta if not is_instance_valid(fleet): fleet = get_tree().get_first_node_in_group("player_fleet") _update_motion(delta) global_position += velocity * delta if velocity.length() > 4.0: rotation = lerp_angle(rotation, velocity.angle() + PI / 2.0, 7.0 * delta) redraw_left -= delta if redraw_left <= 0.0: redraw_left = REDRAW_INTERVAL queue_redraw() func _update_motion(delta: float) -> void: if motion == Motion.EXPELLED: motion_left -= delta if motion_left <= 0.0: motion = Motion.CHASING return if fleet == null or fleet.ships.is_empty(): velocity = velocity.move_toward(Vector2.ZERO, acceleration * delta) return var to_fleet: Vector2 = fleet.get_centroid() - global_position if motion == Motion.BRAKING: motion_left -= delta velocity = velocity.move_toward(Vector2.ZERO, acceleration * 2.2 * delta) if motion_left <= 0.0: motion = Motion.DASHING motion_left = DASH_SECONDS dash_direction = to_fleet.normalized().rotated(randf_range(-0.42, 0.42)) return if motion == Motion.DASHING: motion_left -= delta velocity = velocity.move_toward(dash_direction * dash_speed, acceleration * 4.0 * delta) if motion_left <= 0.0: motion = Motion.CHASING dash_cooldown = randf_range(1.8, 3.8) return if uses_dash_pattern: dash_cooldown -= delta if dash_cooldown <= 0.0: motion = Motion.BRAKING motion_left = BRAKE_SECONDS return if to_fleet.length() > 1.0: velocity = velocity.move_toward(to_fleet.normalized() * speed, acceleration * delta) func take_damage(amount: int) -> bool: if dead: return false hp -= maxi(amount, 1) if hp > 0: return false _die(true) return true func collide_with_player_vessel() -> void: _die(false) func retire() -> void: if dead: return dead = true hp = 0 destroyed.emit() queue_free() func _die(killed_by_player: bool) -> void: if dead: return dead = true hp = 0 if killed_by_player and marked and is_instance_valid(boss): boss.accept_marked_kill(global_position) EnemyExplosionVFX.spawn(get_tree().root, global_position) Sfx.play(get_tree(), Sfx.ENEMY_DEATH) destroyed.emit() queue_free() func _draw() -> void: var glow := Color(1.0, 0.78, 0.18) if marked else Color(0.9, 0.16, 0.58) var core := Color(0.13, 0.01, 0.09) # A small bulb with four streaming arms: recognisably related to the boss, # but compact enough to read among an entire wave. draw_set_transform(Vector2.ZERO, 0.0, Vector2(1.0, 0.76)) NeonDraw.draw_glowing_circle(self, Vector2(0, -7), 13.0, glow, 1.0, core) draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE) for i in range(4): var x: float = -12.0 + float(i) * 8.0 var curl: float = sin(phase * 5.0 + float(i)) * 5.0 NeonDraw.draw_glowing_path(self, PackedVector2Array([ Vector2(x, 1), Vector2(x + curl * 0.35, 10), Vector2(x + curl, 21), ]), glow, 0.85, 0.35, core) if marked: NeonDraw.draw_glowing_circle(self, Vector2(0, -8), 4.0, Color.WHITE, 1.0, core)