spaceship/boss/BossEncounter.gd

156 lines
5.5 KiB
GDScript3
Raw Normal View History

2026-09-03 10:33:05 +02:00
class_name BossEncounter
extends Node2D
@onready var boss: Node2D = $Boss
2026-09-03 22:00:26 +02:00
@onready var fleet: Node2D = get_node("../../Fleet")
@onready var camera: Camera2D = get_node("../../Camera2D")
@onready var health_caption: Label = $EncounterUI/HealthCaption
2026-09-03 10:33:05 +02:00
@onready var status_label: Label = $EncounterUI/StatusLabel
@onready var hint_label: Label = $EncounterUI/HintLabel
2026-09-03 11:49:51 +02:00
@onready var health_bar: ProgressBar = $EncounterUI/HealthBar
2026-09-03 22:00:26 +02:00
@onready var indicator: Control = $EncounterUI/BossIndicator
@onready var indicator_label: Label = $EncounterUI/IndicatorLabel
2026-09-03 10:33:05 +02:00
@onready var game_state: Node = get_node("/root/GameState")
2026-09-03 22:00:26 +02:00
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
2026-09-03 10:33:05 +02:00
func _ready() -> void:
# One persistent objective lets boss victory flow through the same conquest
# state as authored capture-point maps without pretending the boss is a
# configurable capture point.
game_state.register_capture_points(game_state.planet_id, 1,
game_state.planet_definition_id)
if game_state.is_planet_conquered(game_state.planet_id):
boss.suppress_as_defeated()
2026-09-03 11:49:51 +02:00
health_bar.max_value = boss.max_health()
health_bar.value = 0
2026-09-03 22:00:26 +02:00
health_caption.visible = false
health_bar.visible = false
indicator.visible = false
indicator_label.visible = false
2026-09-03 10:33:05 +02:00
status_label.text = "THE WARDEN IS DEAD"
hint_label.text = ""
return
boss.defeated.connect(_on_boss_defeated)
2026-09-03 11:49:51 +02:00
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()
2026-09-03 22:00:26 +02:00
boss.set_combat_active(false)
_set_combat_ui_visible(false)
indicator.visible = true
indicator_label.visible = true
2026-09-03 22:21:11 +02:00
# The signal is a destination, not an autopilot order. The player keeps
# control while approaching it; only the short reveal below takes the wheel.
2026-09-03 11:49:51 +02:00
_refresh_status()
2026-09-03 10:33:05 +02:00
2026-09-03 22:00:26 +02:00
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_reaches_view():
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
2026-09-03 22:21:11 +02:00
game_state.encounter_locks_movement = true
2026-09-03 22:00:26 +02:00
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_reaches_view() -> 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. Including the tentacle reach makes the reveal begin when the first
# part of the creature actually touches the view.
var half_view: Vector2 = get_viewport_rect().size * 0.5 / camera.zoom
var extent: float = boss.TENTACLE_LENGTH * boss.scale.x
var separation: Vector2 = (boss.global_position - camera.global_position).abs()
return separation.x <= half_view.x + extent \
and separation.y <= half_view.y + extent
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
2026-09-03 11:49:51 +02:00
func _refresh_status() -> void:
2026-09-03 22:00:26 +02:00
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")
2026-09-03 10:33:05 +02:00
status_label.text = "THE WARDEN · %s · EYES %d/%d" % [
phase_name, boss.eyes_destroyed, boss.EYE_COUNT]
2026-09-03 22:00:26 +02:00
hint_label.text = "GOLDEN HATCHLINGS APPEAR EVERY THIRD WAVE · BREAK THE EYES, THEN THE CORE"
2026-09-03 10:33:05 +02:00
2026-09-03 11:49:51 +02:00
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()
2026-09-03 10:33:05 +02:00
func _on_boss_defeated() -> void:
2026-09-03 22:00:26 +02:00
game_state.encounter_locks_movement = false
camera.offset = Vector2.ZERO
2026-09-03 10:33:05 +02:00
game_state.mark_point_captured(game_state.planet_id, 0)
2026-09-03 11:49:51 +02:00
health_bar.value = 0
2026-09-03 10:33:05 +02:00
status_label.text = "THE WARDEN IS DEAD · PLANET CONQUERED"
hint_label.text = ""