2026-09-03 10:33:05 +02:00
|
|
|
class_name BossMinion
|
|
|
|
|
extends Area2D
|
|
|
|
|
|
|
|
|
|
signal destroyed
|
|
|
|
|
|
|
|
|
|
const COMBAT_TARGET = preload("res://core/CombatTarget.gd")
|
|
|
|
|
|
|
|
|
|
@export var marked: bool = false
|
2026-09-03 11:49:51 +02:00
|
|
|
@export var max_hp: int = 5
|
2026-09-03 10:33:05 +02:00
|
|
|
@export var speed: float = 245.0
|
|
|
|
|
@export var acceleration: float = 520.0
|
2026-09-03 22:00:26 +02:00
|
|
|
@export var expulsion_speed: float = 520.0
|
|
|
|
|
@export var dash_speed: float = 760.0
|
2026-09-03 10:33:05 +02:00
|
|
|
|
|
|
|
|
var boss: Node2D
|
2026-09-03 11:49:51 +02:00
|
|
|
const REDRAW_INTERVAL: float = 1.0 / 12.0
|
2026-09-03 22:00:26 +02:00
|
|
|
const EXPULSION_SECONDS: float = 0.85
|
|
|
|
|
const BRAKE_SECONDS: float = 0.42
|
|
|
|
|
const DASH_SECONDS: float = 0.48
|
|
|
|
|
|
|
|
|
|
enum Motion { EXPELLED, CHASING, BRAKING, DASHING }
|
2026-09-03 11:49:51 +02:00
|
|
|
|
|
|
|
|
var hp: int = 5
|
2026-09-03 10:33:05 +02:00
|
|
|
var velocity: Vector2 = Vector2.ZERO
|
|
|
|
|
var phase: float = 0.0
|
|
|
|
|
var dead: bool = false
|
2026-09-03 11:49:51 +02:00
|
|
|
var redraw_left: float = 0.0
|
|
|
|
|
var fleet: Node2D = null
|
2026-09-03 22:00:26 +02:00
|
|
|
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
|
2026-09-03 10:33:05 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
2026-09-03 11:49:51 +02:00
|
|
|
fleet = get_tree().get_first_node_in_group("player_fleet")
|
2026-09-03 10:33:05 +02:00
|
|
|
queue_redraw()
|
|
|
|
|
|
|
|
|
|
|
2026-09-03 22:00:26 +02:00
|
|
|
# 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)
|
|
|
|
|
|
|
|
|
|
|
2026-09-03 10:33:05 +02:00
|
|
|
func _physics_process(delta: float) -> void:
|
|
|
|
|
phase += delta
|
2026-09-03 11:49:51 +02:00
|
|
|
if not is_instance_valid(fleet):
|
|
|
|
|
fleet = get_tree().get_first_node_in_group("player_fleet")
|
2026-09-03 22:00:26 +02:00
|
|
|
_update_motion(delta)
|
|
|
|
|
global_position += velocity * delta
|
|
|
|
|
if velocity.length() > 4.0:
|
|
|
|
|
rotation = lerp_angle(rotation, velocity.angle() + PI / 2.0,
|
|
|
|
|
7.0 * delta)
|
2026-09-03 11:49:51 +02:00
|
|
|
redraw_left -= delta
|
|
|
|
|
if redraw_left <= 0.0:
|
|
|
|
|
redraw_left = REDRAW_INTERVAL
|
|
|
|
|
queue_redraw()
|
2026-09-03 10:33:05 +02:00
|
|
|
|
|
|
|
|
|
2026-09-03 22:00:26 +02:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
2026-09-03 10:33:05 +02:00
|
|
|
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)
|