Tune and optimize boss encounter

This commit is contained in:
Loïc Wisniewski 2026-09-03 11:49:51 +02:00
commit 68985316e5
16 changed files with 316 additions and 88 deletions

View file

@ -2,6 +2,7 @@ class_name OctopusBoss
extends Node2D
signal phase_changed(phase: int)
signal health_changed(current: int, maximum: int)
signal defeated
const EYE_SCENE: PackedScene = preload("res://boss/BossEye.tscn")
@ -15,13 +16,14 @@ const EYE_COUNT: int = 6
const PHASE_TWO_EYES: int = 3
const BODY_RADIUS: float = 86.0
const TENTACLE_LENGTH: float = 235.0
const MAX_ACTIVE_MINIONS: int = 8
const REDRAW_INTERVAL: float = 1.0 / 12.0
@export var first_wave_delay: float = 1.5
@export var wave_interval: float = 6.0
@export var minions_per_wave: int = 5
@export var laser_interval: float = 5.5
@export var laser_charge_time: float = 1.6
@export var laser_range: float = 1500.0
@export var laser_interval: float = 3.5
@export var laser_charge_time: float = 0.85
var phase: int = Phase.MINION_WAVES
var wave_timer: float = 0.0
@ -33,6 +35,8 @@ var eyes_destroyed: int = 0
var spawned_minions: Array = []
var return_signals: Array = []
var animation_phase: float = 0.0
var redraw_left: float = 0.0
var total_max_health: int = 0
func _ready() -> void:
@ -49,8 +53,10 @@ func _build_eyes() -> void:
eye.position = eye_position(i)
eye.rotation = eye.position.angle()
add_child(eye)
eye.health_changed.connect(_on_eye_health_changed)
eye.destroyed.connect(_on_eye_destroyed.bind(i))
eyes.append(eye)
total_max_health += int(eye.max_hp)
func eye_position(index: int) -> Vector2:
@ -65,8 +71,6 @@ func _process(delta: float) -> void:
if phase == Phase.DEFEATED:
return
animation_phase += delta
spawned_minions = spawned_minions.filter(func(node): return is_instance_valid(node))
return_signals = return_signals.filter(func(node): return is_instance_valid(node))
wave_timer -= delta
if wave_timer <= 0.0:
@ -78,20 +82,29 @@ func _process(delta: float) -> void:
if laser_timer <= 0.0:
fire_laser_volley()
laser_timer = laser_interval
queue_redraw()
redraw_left -= delta
if redraw_left <= 0.0:
redraw_left = REDRAW_INTERVAL
queue_redraw()
func spawn_wave() -> void:
var fleet: Node2D = get_tree().get_first_node_in_group("player_fleet")
if fleet == null or fleet.ships.is_empty() or get_parent() == null:
return
var count: int = maxi(3, minions_per_wave - (2 if phase == Phase.LASER_STORM else 0))
_prune_invalid(spawned_minions)
var wanted: int = maxi(3,
minions_per_wave - (2 if phase == Phase.LASER_STORM else 0))
var count: int = mini(wanted, MAX_ACTIVE_MINIONS - spawned_minions.size())
if count <= 0:
return
var marked_slot: int = wave_index % count
for i in range(count):
var minion: Area2D = MINION_SCENE.instantiate()
minion.marked = i == marked_slot
minion.boss = self
get_parent().add_child(minion)
minion.tree_exiting.connect(_forget_minion.bind(minion))
var angle: float = TAU * float(i) / float(count) + float(wave_index) * 0.47
# Minions are siblings rather than boss children, so account for the
# encounter scene's imposing boss scale when placing them off its body.
@ -107,6 +120,7 @@ func accept_marked_kill(from: Vector2) -> void:
var returning: Node2D = RETURN_SCENE.instantiate()
returning.target = self
get_parent().add_child(returning)
returning.tree_exiting.connect(_forget_return.bind(returning))
returning.global_position = from
return_signals.append(returning)
@ -124,16 +138,33 @@ func open_next_eye() -> bool:
func _on_eye_destroyed(_index: int) -> void:
eyes_destroyed += 1
health_changed.emit(current_health(), max_health())
if eyes_destroyed >= EYE_COUNT:
_defeat()
return
if phase == Phase.MINION_WAVES and eyes_destroyed >= PHASE_TWO_EYES:
phase = Phase.LASER_STORM
laser_timer = 0.35
laser_timer = 0.2
phase_changed.emit(phase)
queue_redraw()
func _on_eye_health_changed(_current: int, _maximum: int) -> void:
health_changed.emit(current_health(), max_health())
func current_health() -> int:
var total: int = 0
for eye in eyes:
if is_instance_valid(eye):
total += maxi(int(eye.hp), 0)
return total
func max_health() -> int:
return total_max_health
func fire_laser_volley() -> void:
if phase != Phase.LASER_STORM:
return
@ -147,7 +178,7 @@ func fire_laser_volley() -> void:
for spread in [-0.17, 0.0, 0.17]:
var beam: Node2D = LASER_SCENE.instantiate()
beam.charge_time = laser_charge_time
beam.beam_range = laser_range
beam.infinite_range = true
beam.beam_half_width = 24.0
beam.damage = 1
beam.color = Color(1.0, 0.12, 0.58)
@ -156,6 +187,20 @@ func fire_laser_volley() -> void:
beam.launch(global_position, toward.rotated(float(spread)))
func _forget_minion(minion: Node) -> void:
spawned_minions.erase(minion)
func _forget_return(returning: Node) -> void:
return_signals.erase(returning)
func _prune_invalid(nodes: Array) -> void:
for i in range(nodes.size() - 1, -1, -1):
if not is_instance_valid(nodes[i]) or nodes[i].is_queued_for_deletion():
nodes.remove_at(i)
func _defeat() -> void:
if phase == Phase.DEFEATED:
return