167 lines
6 KiB
GDScript
167 lines
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
|
|
const APPROACH_STANDOFF: float = 850.0
|
|
|
|
var reveal_started: bool = false
|
|
var reveal_left: float = 0.0
|
|
var encounter_active: bool = false
|
|
|
|
|
|
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()
|
|
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
|
|
game_state.encounter_locks_movement = true
|
|
# The encounter introduces itself in the world: the fleet is drawn toward
|
|
# the signal until the Warden reaches the edge of view, then control is
|
|
# returned after the roar. No dialog or separate scene hides the approach.
|
|
_begin_approach.call_deferred()
|
|
_refresh_status()
|
|
|
|
|
|
func _begin_approach() -> void:
|
|
# PlanetSurfaceController composes this encounter before it spawns the
|
|
# restored fleet. Waiting one call guarantees there are ships for the
|
|
# scripted move to steer.
|
|
if not reveal_started and not fleet.ships.is_empty():
|
|
fleet.move_to(boss.global_position + Vector2(0.0, APPROACH_STANDOFF))
|
|
|
|
|
|
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
|
|
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
|
|
|
|
|
|
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_point_captured(game_state.planet_id, 0)
|
|
health_bar.value = 0
|
|
status_label.text = "THE WARDEN IS DEAD · PLANET CONQUERED"
|
|
hint_label.text = ""
|