spaceship/boss/BossEncounter.gd
Loïc Wisniewski 6e13b6397c Take planets by killing what defends them
#211 conquest is the fight now: a planet registers the garrison its setup
authors, each defender's own death counts against it, and the last one takes
the world. The capture points - stand in a ring, wait out a timer, once per
ring - are gone, along with the editor palette that placed them.
#212 conquest opens a dialog on the surface naming the research points
recovered; the award travels with the signal, after it is banked.
#209 the prologue's charted sectors go with the breach, so the map the
survivors keep is of where they now are - and "chart 30 sectors" is no
longer complete before they have flown anywhere.
#218 enemy variety is gated on the FLEET, not on wave strength: a starting
fleet meets chasers only at any wave number, and each vessel past three
opens one more kind.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
2026-09-05 13:31:25 +02:00

156 lines
5.6 KiB
GDScript

class_name BossEncounter
extends Node2D
@onready var boss: Node2D = $Boss
@onready var fleet: Node2D = get_node("../../Fleet")
@onready var camera: Camera2D = get_node("../../Camera2D")
@onready var health_caption: Label = $EncounterUI/HealthCaption
@onready var status_label: Label = $EncounterUI/StatusLabel
@onready var hint_label: Label = $EncounterUI/HintLabel
@onready var health_bar: ProgressBar = $EncounterUI/HealthBar
@onready var indicator: Control = $EncounterUI/BossIndicator
@onready var indicator_label: Label = $EncounterUI/IndicatorLabel
@onready var game_state: Node = get_node("/root/GameState")
const REVEAL_SECONDS: float = 1.15
const REVEAL_SHAKE: float = 28.0
var reveal_started: bool = false
var reveal_left: float = 0.0
var encounter_active: bool = false
func _ready() -> void:
# A garrison of one lets boss victory flow through the same conquest state
# as an authored map without pretending the boss is an ordinary defender.
game_state.register_planet_garrison(game_state.planet_id, 1,
game_state.planet_definition_id)
if game_state.is_planet_conquered(game_state.planet_id):
boss.suppress_as_defeated()
health_bar.max_value = boss.max_health()
health_bar.value = 0
health_caption.visible = false
health_bar.visible = false
indicator.visible = false
indicator_label.visible = false
status_label.text = "THE WARDEN IS DEAD"
hint_label.text = ""
return
boss.defeated.connect(_on_boss_defeated)
boss.phase_changed.connect(_on_boss_phase_changed)
boss.health_changed.connect(_on_boss_health_changed)
health_bar.max_value = boss.max_health()
health_bar.value = boss.current_health()
boss.set_combat_active(false)
_set_combat_ui_visible(false)
indicator.visible = true
indicator_label.visible = true
# The signal is a destination, not an autopilot order. The player keeps
# control while approaching it; only the short reveal below takes the wheel.
_refresh_status()
func _exit_tree() -> void:
if game_state != null:
game_state.encounter_locks_movement = false
if camera != null:
camera.offset = Vector2.ZERO
func _process(delta: float) -> void:
if boss.phase == boss.Phase.DEFEATED:
return
if not reveal_started:
_update_indicator()
if _boss_is_fully_visible():
begin_reveal()
return
if encounter_active:
return
reveal_left -= delta
var strength: float = REVEAL_SHAKE * clampf(reveal_left / REVEAL_SECONDS, 0.0, 1.0)
camera.offset = Vector2(randf_range(-strength, strength), randf_range(-strength, strength))
if reveal_left <= 0.0:
camera.offset = Vector2.ZERO
encounter_active = true
boss.set_combat_active(true)
game_state.encounter_locks_movement = false
func begin_reveal() -> void:
if reveal_started or boss.phase == boss.Phase.DEFEATED:
return
reveal_started = true
reveal_left = REVEAL_SECONDS
game_state.encounter_locks_movement = true
fleet.stop()
indicator.visible = false
indicator_label.visible = false
_set_combat_ui_visible(true)
status_label.text = "THE WARDEN AWAKENS"
hint_label.text = ""
Sfx.play(get_tree(), Sfx.BOSS_SCREAM)
func _set_combat_ui_visible(shown: bool) -> void:
for control in [health_caption, health_bar, status_label, hint_label]:
control.visible = shown
func _boss_is_fully_visible() -> bool:
# World-space math is immediate. The viewport canvas transform trails a
# Camera2D assignment until the render server updates, which made a fast
# approach able to cross the threshold one frame before the detector saw
# it. The tentacles deliberately reach beyond the framing, so the authored
# body radius is the useful cinematic boundary: the roar must wait until
# the whole central creature is on screen, not fire when a tentacle enters.
var half_view: Vector2 = get_viewport_rect().size * 0.5 / camera.zoom
var extent: float = boss.BODY_RADIUS * maxf(boss.scale.x, boss.scale.y)
var separation: Vector2 = (boss.global_position - camera.global_position).abs()
return separation.x + extent <= half_view.x \
and separation.y + extent <= half_view.y
func _update_indicator() -> void:
var viewport_size: Vector2 = get_viewport_rect().size
var screen_at: Vector2 = get_viewport().get_canvas_transform() * boss.global_position
var from_center: Vector2 = screen_at - viewport_size * 0.5
if from_center == Vector2.ZERO:
from_center = Vector2.UP
var half: Vector2 = indicator.size * 0.5
indicator.position = Vector2(
clampf(screen_at.x - half.x, 24.0, viewport_size.x - indicator.size.x - 24.0),
clampf(screen_at.y - half.y, 170.0, viewport_size.y - indicator.size.y - 24.0))
indicator.rotation = from_center.angle() + PI / 2.0
indicator_label.position.x = clampf(indicator.position.x - 90.0,
24.0, viewport_size.x - indicator_label.size.x - 24.0)
indicator_label.position.y = indicator.position.y + indicator.size.y + 8.0
func _refresh_status() -> void:
var phase_name: String = "PHASE III · EXPOSED CORE" if boss.phase == boss.Phase.CORE_EXPOSED \
else ("PHASE II · LASER STORM" if boss.phase == boss.Phase.LASER_STORM \
else "PHASE I · HATCHLINGS")
status_label.text = "THE WARDEN · %s · EYES %d/%d" % [
phase_name, boss.eyes_destroyed, boss.EYE_COUNT]
hint_label.text = "GOLDEN HATCHLINGS APPEAR EVERY THIRD WAVE · BREAK THE EYES, THEN THE CORE"
func _on_boss_phase_changed(_phase: int) -> void:
if boss.phase != boss.Phase.DEFEATED:
_refresh_status()
func _on_boss_health_changed(current: int, maximum: int) -> void:
health_bar.max_value = maximum
health_bar.value = current
_refresh_status()
func _on_boss_defeated() -> void:
game_state.encounter_locks_movement = false
camera.offset = Vector2.ZERO
game_state.mark_garrison_enemy_destroyed(game_state.planet_id)
health_bar.value = 0
status_label.text = "THE WARDEN IS DEAD · PLANET CONQUERED"
hint_label.text = ""