Tune and optimize boss encounter
This commit is contained in:
parent
40f284dcdd
commit
68985316e5
16 changed files with 316 additions and 88 deletions
|
|
@ -4,6 +4,7 @@ extends Node2D
|
|||
@onready var boss: Node2D = $Boss
|
||||
@onready var status_label: Label = $EncounterUI/StatusLabel
|
||||
@onready var hint_label: Label = $EncounterUI/HintLabel
|
||||
@onready var health_bar: ProgressBar = $EncounterUI/HealthBar
|
||||
@onready var game_state: Node = get_node("/root/GameState")
|
||||
|
||||
|
||||
|
|
@ -15,15 +16,20 @@ func _ready() -> void:
|
|||
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
|
||||
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()
|
||||
_refresh_status()
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if not is_instance_valid(boss) or boss.phase == boss.Phase.DEFEATED:
|
||||
return
|
||||
func _refresh_status() -> void:
|
||||
var phase_name: String = "PHASE II · LASER STORM" \
|
||||
if boss.phase == boss.Phase.LASER_STORM else "PHASE I · HATCHLINGS"
|
||||
status_label.text = "THE WARDEN · %s · EYES %d/%d" % [
|
||||
|
|
@ -31,7 +37,19 @@ func _process(_delta: float) -> void:
|
|||
hint_label.text = "DESTROY GOLDEN HATCHLINGS · FOLLOW THEIR LIGHT · BREAK THE OPEN EYES"
|
||||
|
||||
|
||||
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.mark_point_captured(game_state.planet_id, 0)
|
||||
health_bar.value = 0
|
||||
status_label.text = "THE WARDEN IS DEAD · PLANET CONQUERED"
|
||||
hint_label.text = ""
|
||||
|
|
|
|||
|
|
@ -1,8 +1,27 @@
|
|||
[gd_scene load_steps=3 format=3]
|
||||
[gd_scene load_steps=5 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://boss/BossEncounter.gd" id="1"]
|
||||
[ext_resource type="PackedScene" path="res://boss/OctopusBoss.tscn" id="2"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBox_health_background"]
|
||||
bg_color = Color(0.08, 0.005, 0.055, 0.92)
|
||||
border_width_left = 2
|
||||
border_width_top = 2
|
||||
border_width_right = 2
|
||||
border_width_bottom = 2
|
||||
border_color = Color(0.45, 0.1, 0.3, 0.95)
|
||||
corner_radius_top_left = 8
|
||||
corner_radius_top_right = 8
|
||||
corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 8
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBox_health_fill"]
|
||||
bg_color = Color(0.95, 0.12, 0.48, 0.95)
|
||||
corner_radius_top_left = 6
|
||||
corner_radius_top_right = 6
|
||||
corner_radius_bottom_right = 6
|
||||
corner_radius_bottom_left = 6
|
||||
|
||||
[node name="BossEncounter" type="Node2D"]
|
||||
script = ExtResource("1")
|
||||
|
||||
|
|
@ -11,12 +30,41 @@ script = ExtResource("1")
|
|||
[node name="EncounterUI" type="CanvasLayer" parent="."]
|
||||
layer = 2
|
||||
|
||||
[node name="HealthCaption" type="Label" parent="EncounterUI"]
|
||||
mouse_filter = 2
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
offset_left = -520.0
|
||||
offset_top = 280.0
|
||||
offset_right = 520.0
|
||||
offset_bottom = 310.0
|
||||
theme_override_colors/font_color = Color(1, 0.55, 0.76, 1)
|
||||
theme_override_colors/font_outline_color = Color(0.04, 0.005, 0.035, 0.95)
|
||||
theme_override_constants/outline_size = 4
|
||||
theme_override_font_sizes/font_size = 20
|
||||
text = "THE WARDEN"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="HealthBar" type="ProgressBar" parent="EncounterUI"]
|
||||
mouse_filter = 2
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
offset_left = -520.0
|
||||
offset_top = 312.0
|
||||
offset_right = 520.0
|
||||
offset_bottom = 344.0
|
||||
theme_override_styles/background = SubResource("StyleBox_health_background")
|
||||
theme_override_styles/fill = SubResource("StyleBox_health_fill")
|
||||
max_value = 18.0
|
||||
value = 18.0
|
||||
show_percentage = false
|
||||
|
||||
[node name="StatusLabel" type="Label" parent="EncounterUI"]
|
||||
mouse_filter = 2
|
||||
offset_left = 20.0
|
||||
offset_top = 268.0
|
||||
offset_top = 354.0
|
||||
offset_right = 700.0
|
||||
offset_bottom = 310.0
|
||||
offset_bottom = 396.0
|
||||
theme_override_colors/font_color = Color(1, 0.45, 0.72, 1)
|
||||
theme_override_colors/font_outline_color = Color(0.04, 0.005, 0.035, 0.95)
|
||||
theme_override_constants/outline_size = 4
|
||||
|
|
@ -27,9 +75,9 @@ horizontal_alignment = 1
|
|||
[node name="HintLabel" type="Label" parent="EncounterUI"]
|
||||
mouse_filter = 2
|
||||
offset_left = 20.0
|
||||
offset_top = 310.0
|
||||
offset_top = 396.0
|
||||
offset_right = 700.0
|
||||
offset_bottom = 350.0
|
||||
offset_bottom = 436.0
|
||||
theme_override_colors/font_color = Color(0.45, 0.9, 1, 1)
|
||||
theme_override_colors/font_outline_color = Color(0.04, 0.005, 0.035, 0.95)
|
||||
theme_override_constants/outline_size = 3
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ class_name BossEye
|
|||
extends Area2D
|
||||
|
||||
signal destroyed
|
||||
signal health_changed(current: int, maximum: int)
|
||||
|
||||
const COMBAT_TARGET = preload("res://core/CombatTarget.gd")
|
||||
|
||||
|
|
@ -10,6 +11,9 @@ const COMBAT_TARGET = preload("res://core/CombatTarget.gd")
|
|||
var hp: int = 3
|
||||
var opened: bool = false
|
||||
var phase: float = 0.0
|
||||
var redraw_left: float = 0.0
|
||||
|
||||
const REDRAW_INTERVAL: float = 1.0 / 12.0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
|
|
@ -32,6 +36,7 @@ func take_damage(amount: int) -> bool:
|
|||
if not opened or hp <= 0:
|
||||
return false
|
||||
hp -= maxi(amount, 1)
|
||||
health_changed.emit(maxi(hp, 0), max_hp)
|
||||
if hp > 0:
|
||||
queue_redraw()
|
||||
return false
|
||||
|
|
@ -45,7 +50,9 @@ func take_damage(amount: int) -> bool:
|
|||
|
||||
func _process(delta: float) -> void:
|
||||
phase += delta
|
||||
if opened:
|
||||
redraw_left -= delta
|
||||
if opened and redraw_left <= 0.0:
|
||||
redraw_left = REDRAW_INTERVAL
|
||||
queue_redraw()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -6,15 +6,19 @@ signal destroyed
|
|||
const COMBAT_TARGET = preload("res://core/CombatTarget.gd")
|
||||
|
||||
@export var marked: bool = false
|
||||
@export var max_hp: int = 2
|
||||
@export var max_hp: int = 5
|
||||
@export var speed: float = 245.0
|
||||
@export var acceleration: float = 520.0
|
||||
|
||||
var boss: Node2D
|
||||
var hp: int = 2
|
||||
const REDRAW_INTERVAL: float = 1.0 / 12.0
|
||||
|
||||
var hp: int = 5
|
||||
var velocity: Vector2 = Vector2.ZERO
|
||||
var phase: float = 0.0
|
||||
var dead: bool = false
|
||||
var redraw_left: float = 0.0
|
||||
var fleet: Node2D = null
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
|
|
@ -23,12 +27,14 @@ func _ready() -> void:
|
|||
add_to_group("boss_minions")
|
||||
COMBAT_TARGET.register(self, COMBAT_TARGET.DANGER_PRIORITY)
|
||||
phase = randf() * TAU
|
||||
fleet = get_tree().get_first_node_in_group("player_fleet")
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
phase += delta
|
||||
var fleet: Node2D = get_tree().get_first_node_in_group("player_fleet")
|
||||
if not is_instance_valid(fleet):
|
||||
fleet = get_tree().get_first_node_in_group("player_fleet")
|
||||
if fleet != null and not fleet.ships.is_empty():
|
||||
var to_fleet: Vector2 = fleet.get_centroid() - global_position
|
||||
if to_fleet.length() > 1.0:
|
||||
|
|
@ -38,7 +44,10 @@ func _physics_process(delta: float) -> void:
|
|||
if velocity.length() > 4.0:
|
||||
rotation = lerp_angle(rotation, velocity.angle() + PI / 2.0,
|
||||
7.0 * delta)
|
||||
queue_redraw()
|
||||
redraw_left -= delta
|
||||
if redraw_left <= 0.0:
|
||||
redraw_left = REDRAW_INTERVAL
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func take_damage(amount: int) -> bool:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue