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 boss: Node2D = $Boss
|
||||||
@onready var status_label: Label = $EncounterUI/StatusLabel
|
@onready var status_label: Label = $EncounterUI/StatusLabel
|
||||||
@onready var hint_label: Label = $EncounterUI/HintLabel
|
@onready var hint_label: Label = $EncounterUI/HintLabel
|
||||||
|
@onready var health_bar: ProgressBar = $EncounterUI/HealthBar
|
||||||
@onready var game_state: Node = get_node("/root/GameState")
|
@onready var game_state: Node = get_node("/root/GameState")
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -15,15 +16,20 @@ func _ready() -> void:
|
||||||
game_state.planet_definition_id)
|
game_state.planet_definition_id)
|
||||||
if game_state.is_planet_conquered(game_state.planet_id):
|
if game_state.is_planet_conquered(game_state.planet_id):
|
||||||
boss.suppress_as_defeated()
|
boss.suppress_as_defeated()
|
||||||
|
health_bar.max_value = boss.max_health()
|
||||||
|
health_bar.value = 0
|
||||||
status_label.text = "THE WARDEN IS DEAD"
|
status_label.text = "THE WARDEN IS DEAD"
|
||||||
hint_label.text = ""
|
hint_label.text = ""
|
||||||
return
|
return
|
||||||
boss.defeated.connect(_on_boss_defeated)
|
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:
|
func _refresh_status() -> void:
|
||||||
if not is_instance_valid(boss) or boss.phase == boss.Phase.DEFEATED:
|
|
||||||
return
|
|
||||||
var phase_name: String = "PHASE II · LASER STORM" \
|
var phase_name: String = "PHASE II · LASER STORM" \
|
||||||
if boss.phase == boss.Phase.LASER_STORM else "PHASE I · HATCHLINGS"
|
if boss.phase == boss.Phase.LASER_STORM else "PHASE I · HATCHLINGS"
|
||||||
status_label.text = "THE WARDEN · %s · EYES %d/%d" % [
|
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"
|
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:
|
func _on_boss_defeated() -> void:
|
||||||
game_state.mark_point_captured(game_state.planet_id, 0)
|
game_state.mark_point_captured(game_state.planet_id, 0)
|
||||||
|
health_bar.value = 0
|
||||||
status_label.text = "THE WARDEN IS DEAD · PLANET CONQUERED"
|
status_label.text = "THE WARDEN IS DEAD · PLANET CONQUERED"
|
||||||
hint_label.text = ""
|
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="Script" path="res://boss/BossEncounter.gd" id="1"]
|
||||||
[ext_resource type="PackedScene" path="res://boss/OctopusBoss.tscn" id="2"]
|
[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"]
|
[node name="BossEncounter" type="Node2D"]
|
||||||
script = ExtResource("1")
|
script = ExtResource("1")
|
||||||
|
|
||||||
|
|
@ -11,12 +30,41 @@ script = ExtResource("1")
|
||||||
[node name="EncounterUI" type="CanvasLayer" parent="."]
|
[node name="EncounterUI" type="CanvasLayer" parent="."]
|
||||||
layer = 2
|
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"]
|
[node name="StatusLabel" type="Label" parent="EncounterUI"]
|
||||||
mouse_filter = 2
|
mouse_filter = 2
|
||||||
offset_left = 20.0
|
offset_left = 20.0
|
||||||
offset_top = 268.0
|
offset_top = 354.0
|
||||||
offset_right = 700.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_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_colors/font_outline_color = Color(0.04, 0.005, 0.035, 0.95)
|
||||||
theme_override_constants/outline_size = 4
|
theme_override_constants/outline_size = 4
|
||||||
|
|
@ -27,9 +75,9 @@ horizontal_alignment = 1
|
||||||
[node name="HintLabel" type="Label" parent="EncounterUI"]
|
[node name="HintLabel" type="Label" parent="EncounterUI"]
|
||||||
mouse_filter = 2
|
mouse_filter = 2
|
||||||
offset_left = 20.0
|
offset_left = 20.0
|
||||||
offset_top = 310.0
|
offset_top = 396.0
|
||||||
offset_right = 700.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_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_colors/font_outline_color = Color(0.04, 0.005, 0.035, 0.95)
|
||||||
theme_override_constants/outline_size = 3
|
theme_override_constants/outline_size = 3
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ class_name BossEye
|
||||||
extends Area2D
|
extends Area2D
|
||||||
|
|
||||||
signal destroyed
|
signal destroyed
|
||||||
|
signal health_changed(current: int, maximum: int)
|
||||||
|
|
||||||
const COMBAT_TARGET = preload("res://core/CombatTarget.gd")
|
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 hp: int = 3
|
||||||
var opened: bool = false
|
var opened: bool = false
|
||||||
var phase: float = 0.0
|
var phase: float = 0.0
|
||||||
|
var redraw_left: float = 0.0
|
||||||
|
|
||||||
|
const REDRAW_INTERVAL: float = 1.0 / 12.0
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
|
@ -32,6 +36,7 @@ func take_damage(amount: int) -> bool:
|
||||||
if not opened or hp <= 0:
|
if not opened or hp <= 0:
|
||||||
return false
|
return false
|
||||||
hp -= maxi(amount, 1)
|
hp -= maxi(amount, 1)
|
||||||
|
health_changed.emit(maxi(hp, 0), max_hp)
|
||||||
if hp > 0:
|
if hp > 0:
|
||||||
queue_redraw()
|
queue_redraw()
|
||||||
return false
|
return false
|
||||||
|
|
@ -45,7 +50,9 @@ func take_damage(amount: int) -> bool:
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
phase += delta
|
phase += delta
|
||||||
if opened:
|
redraw_left -= delta
|
||||||
|
if opened and redraw_left <= 0.0:
|
||||||
|
redraw_left = REDRAW_INTERVAL
|
||||||
queue_redraw()
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,15 +6,19 @@ signal destroyed
|
||||||
const COMBAT_TARGET = preload("res://core/CombatTarget.gd")
|
const COMBAT_TARGET = preload("res://core/CombatTarget.gd")
|
||||||
|
|
||||||
@export var marked: bool = false
|
@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 speed: float = 245.0
|
||||||
@export var acceleration: float = 520.0
|
@export var acceleration: float = 520.0
|
||||||
|
|
||||||
var boss: Node2D
|
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 velocity: Vector2 = Vector2.ZERO
|
||||||
var phase: float = 0.0
|
var phase: float = 0.0
|
||||||
var dead: bool = false
|
var dead: bool = false
|
||||||
|
var redraw_left: float = 0.0
|
||||||
|
var fleet: Node2D = null
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
|
@ -23,12 +27,14 @@ func _ready() -> void:
|
||||||
add_to_group("boss_minions")
|
add_to_group("boss_minions")
|
||||||
COMBAT_TARGET.register(self, COMBAT_TARGET.DANGER_PRIORITY)
|
COMBAT_TARGET.register(self, COMBAT_TARGET.DANGER_PRIORITY)
|
||||||
phase = randf() * TAU
|
phase = randf() * TAU
|
||||||
|
fleet = get_tree().get_first_node_in_group("player_fleet")
|
||||||
queue_redraw()
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
func _physics_process(delta: float) -> void:
|
func _physics_process(delta: float) -> void:
|
||||||
phase += delta
|
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():
|
if fleet != null and not fleet.ships.is_empty():
|
||||||
var to_fleet: Vector2 = fleet.get_centroid() - global_position
|
var to_fleet: Vector2 = fleet.get_centroid() - global_position
|
||||||
if to_fleet.length() > 1.0:
|
if to_fleet.length() > 1.0:
|
||||||
|
|
@ -38,7 +44,10 @@ func _physics_process(delta: float) -> void:
|
||||||
if velocity.length() > 4.0:
|
if velocity.length() > 4.0:
|
||||||
rotation = lerp_angle(rotation, velocity.angle() + PI / 2.0,
|
rotation = lerp_angle(rotation, velocity.angle() + PI / 2.0,
|
||||||
7.0 * delta)
|
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:
|
func take_damage(amount: int) -> bool:
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ class_name OctopusBoss
|
||||||
extends Node2D
|
extends Node2D
|
||||||
|
|
||||||
signal phase_changed(phase: int)
|
signal phase_changed(phase: int)
|
||||||
|
signal health_changed(current: int, maximum: int)
|
||||||
signal defeated
|
signal defeated
|
||||||
|
|
||||||
const EYE_SCENE: PackedScene = preload("res://boss/BossEye.tscn")
|
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 PHASE_TWO_EYES: int = 3
|
||||||
const BODY_RADIUS: float = 86.0
|
const BODY_RADIUS: float = 86.0
|
||||||
const TENTACLE_LENGTH: float = 235.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 first_wave_delay: float = 1.5
|
||||||
@export var wave_interval: float = 6.0
|
@export var wave_interval: float = 6.0
|
||||||
@export var minions_per_wave: int = 5
|
@export var minions_per_wave: int = 5
|
||||||
@export var laser_interval: float = 5.5
|
@export var laser_interval: float = 3.5
|
||||||
@export var laser_charge_time: float = 1.6
|
@export var laser_charge_time: float = 0.85
|
||||||
@export var laser_range: float = 1500.0
|
|
||||||
|
|
||||||
var phase: int = Phase.MINION_WAVES
|
var phase: int = Phase.MINION_WAVES
|
||||||
var wave_timer: float = 0.0
|
var wave_timer: float = 0.0
|
||||||
|
|
@ -33,6 +35,8 @@ var eyes_destroyed: int = 0
|
||||||
var spawned_minions: Array = []
|
var spawned_minions: Array = []
|
||||||
var return_signals: Array = []
|
var return_signals: Array = []
|
||||||
var animation_phase: float = 0.0
|
var animation_phase: float = 0.0
|
||||||
|
var redraw_left: float = 0.0
|
||||||
|
var total_max_health: int = 0
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
|
@ -49,8 +53,10 @@ func _build_eyes() -> void:
|
||||||
eye.position = eye_position(i)
|
eye.position = eye_position(i)
|
||||||
eye.rotation = eye.position.angle()
|
eye.rotation = eye.position.angle()
|
||||||
add_child(eye)
|
add_child(eye)
|
||||||
|
eye.health_changed.connect(_on_eye_health_changed)
|
||||||
eye.destroyed.connect(_on_eye_destroyed.bind(i))
|
eye.destroyed.connect(_on_eye_destroyed.bind(i))
|
||||||
eyes.append(eye)
|
eyes.append(eye)
|
||||||
|
total_max_health += int(eye.max_hp)
|
||||||
|
|
||||||
|
|
||||||
func eye_position(index: int) -> Vector2:
|
func eye_position(index: int) -> Vector2:
|
||||||
|
|
@ -65,8 +71,6 @@ func _process(delta: float) -> void:
|
||||||
if phase == Phase.DEFEATED:
|
if phase == Phase.DEFEATED:
|
||||||
return
|
return
|
||||||
animation_phase += delta
|
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
|
wave_timer -= delta
|
||||||
if wave_timer <= 0.0:
|
if wave_timer <= 0.0:
|
||||||
|
|
@ -78,20 +82,29 @@ func _process(delta: float) -> void:
|
||||||
if laser_timer <= 0.0:
|
if laser_timer <= 0.0:
|
||||||
fire_laser_volley()
|
fire_laser_volley()
|
||||||
laser_timer = laser_interval
|
laser_timer = laser_interval
|
||||||
queue_redraw()
|
redraw_left -= delta
|
||||||
|
if redraw_left <= 0.0:
|
||||||
|
redraw_left = REDRAW_INTERVAL
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
func spawn_wave() -> void:
|
func spawn_wave() -> void:
|
||||||
var fleet: Node2D = get_tree().get_first_node_in_group("player_fleet")
|
var fleet: Node2D = get_tree().get_first_node_in_group("player_fleet")
|
||||||
if fleet == null or fleet.ships.is_empty() or get_parent() == null:
|
if fleet == null or fleet.ships.is_empty() or get_parent() == null:
|
||||||
return
|
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
|
var marked_slot: int = wave_index % count
|
||||||
for i in range(count):
|
for i in range(count):
|
||||||
var minion: Area2D = MINION_SCENE.instantiate()
|
var minion: Area2D = MINION_SCENE.instantiate()
|
||||||
minion.marked = i == marked_slot
|
minion.marked = i == marked_slot
|
||||||
minion.boss = self
|
minion.boss = self
|
||||||
get_parent().add_child(minion)
|
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
|
var angle: float = TAU * float(i) / float(count) + float(wave_index) * 0.47
|
||||||
# Minions are siblings rather than boss children, so account for the
|
# Minions are siblings rather than boss children, so account for the
|
||||||
# encounter scene's imposing boss scale when placing them off its body.
|
# 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()
|
var returning: Node2D = RETURN_SCENE.instantiate()
|
||||||
returning.target = self
|
returning.target = self
|
||||||
get_parent().add_child(returning)
|
get_parent().add_child(returning)
|
||||||
|
returning.tree_exiting.connect(_forget_return.bind(returning))
|
||||||
returning.global_position = from
|
returning.global_position = from
|
||||||
return_signals.append(returning)
|
return_signals.append(returning)
|
||||||
|
|
||||||
|
|
@ -124,16 +138,33 @@ func open_next_eye() -> bool:
|
||||||
|
|
||||||
func _on_eye_destroyed(_index: int) -> void:
|
func _on_eye_destroyed(_index: int) -> void:
|
||||||
eyes_destroyed += 1
|
eyes_destroyed += 1
|
||||||
|
health_changed.emit(current_health(), max_health())
|
||||||
if eyes_destroyed >= EYE_COUNT:
|
if eyes_destroyed >= EYE_COUNT:
|
||||||
_defeat()
|
_defeat()
|
||||||
return
|
return
|
||||||
if phase == Phase.MINION_WAVES and eyes_destroyed >= PHASE_TWO_EYES:
|
if phase == Phase.MINION_WAVES and eyes_destroyed >= PHASE_TWO_EYES:
|
||||||
phase = Phase.LASER_STORM
|
phase = Phase.LASER_STORM
|
||||||
laser_timer = 0.35
|
laser_timer = 0.2
|
||||||
phase_changed.emit(phase)
|
phase_changed.emit(phase)
|
||||||
queue_redraw()
|
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:
|
func fire_laser_volley() -> void:
|
||||||
if phase != Phase.LASER_STORM:
|
if phase != Phase.LASER_STORM:
|
||||||
return
|
return
|
||||||
|
|
@ -147,7 +178,7 @@ func fire_laser_volley() -> void:
|
||||||
for spread in [-0.17, 0.0, 0.17]:
|
for spread in [-0.17, 0.0, 0.17]:
|
||||||
var beam: Node2D = LASER_SCENE.instantiate()
|
var beam: Node2D = LASER_SCENE.instantiate()
|
||||||
beam.charge_time = laser_charge_time
|
beam.charge_time = laser_charge_time
|
||||||
beam.beam_range = laser_range
|
beam.infinite_range = true
|
||||||
beam.beam_half_width = 24.0
|
beam.beam_half_width = 24.0
|
||||||
beam.damage = 1
|
beam.damage = 1
|
||||||
beam.color = Color(1.0, 0.12, 0.58)
|
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)))
|
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:
|
func _defeat() -> void:
|
||||||
if phase == Phase.DEFEATED:
|
if phase == Phase.DEFEATED:
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -12,27 +12,29 @@ extends Node2D
|
||||||
# the fleet's position while charging.
|
# the fleet's position while charging.
|
||||||
#
|
#
|
||||||
# Damage: every ship in group "player_ships" whose perpendicular distance
|
# Damage: every ship in group "player_ships" whose perpendicular distance
|
||||||
# to the beam segment is within beam_half_width, AND whose projection along
|
# to the beam is within beam_half_width takes Ship.take_damage(damage). The
|
||||||
# the beam falls inside [0, beam_range], takes Ship.take_damage(damage) -
|
# ordinary mothership beam remains a finite segment; bosses opt into a ray
|
||||||
# same all-or-nothing shield semantics as every other damage source in this
|
# with no upper distance bound. Ship.take_damage handles shields itself.
|
||||||
# game (Ship.take_damage already handles the absorb_hit check itself).
|
|
||||||
|
|
||||||
signal finished
|
signal finished
|
||||||
|
|
||||||
@export var charge_time: float = 2.5
|
@export var charge_time: float = 2.5
|
||||||
@export var beam_range: float = 1200.0
|
@export var beam_range: float = 1200.0
|
||||||
|
@export var infinite_range: bool = false
|
||||||
@export var beam_half_width: float = 35.0
|
@export var beam_half_width: float = 35.0
|
||||||
@export var damage: int = 3
|
@export var damage: int = 3
|
||||||
@export var color: Color = Color(0.55, 0.05, 0.85)
|
@export var color: Color = Color(0.55, 0.05, 0.85)
|
||||||
|
|
||||||
enum Phase { CHARGING, FLASH }
|
enum Phase { CHARGING, FLASH }
|
||||||
const FLASH_DURATION: float = 0.25
|
const FLASH_DURATION: float = 0.25
|
||||||
|
const REDRAW_INTERVAL: float = 1.0 / 30.0
|
||||||
|
|
||||||
var phase: int = Phase.CHARGING
|
var phase: int = Phase.CHARGING
|
||||||
var origin: Vector2 = Vector2.ZERO
|
var origin: Vector2 = Vector2.ZERO
|
||||||
var aim_direction: Vector2 = Vector2.RIGHT
|
var aim_direction: Vector2 = Vector2.RIGHT
|
||||||
var charge_left: float = 0.0
|
var charge_left: float = 0.0
|
||||||
var flash_time: float = 0.0
|
var flash_time: float = 0.0
|
||||||
|
var redraw_left: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
func launch(from: Vector2, toward: Vector2) -> void:
|
func launch(from: Vector2, toward: Vector2) -> void:
|
||||||
|
|
@ -46,15 +48,20 @@ func launch(from: Vector2, toward: Vector2) -> void:
|
||||||
|
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
|
redraw_left -= delta
|
||||||
match phase:
|
match phase:
|
||||||
Phase.CHARGING:
|
Phase.CHARGING:
|
||||||
charge_left -= delta
|
charge_left -= delta
|
||||||
if charge_left <= 0.0:
|
if charge_left <= 0.0:
|
||||||
_fire()
|
_fire()
|
||||||
queue_redraw()
|
if redraw_left <= 0.0:
|
||||||
|
redraw_left = REDRAW_INTERVAL
|
||||||
|
queue_redraw()
|
||||||
Phase.FLASH:
|
Phase.FLASH:
|
||||||
flash_time -= delta
|
flash_time -= delta
|
||||||
queue_redraw()
|
if redraw_left <= 0.0:
|
||||||
|
redraw_left = REDRAW_INTERVAL
|
||||||
|
queue_redraw()
|
||||||
if flash_time <= 0.0:
|
if flash_time <= 0.0:
|
||||||
finished.emit()
|
finished.emit()
|
||||||
queue_free()
|
queue_free()
|
||||||
|
|
@ -66,16 +73,19 @@ func _fire() -> void:
|
||||||
for ship in get_tree().get_nodes_in_group("player_ships"):
|
for ship in get_tree().get_nodes_in_group("player_ships"):
|
||||||
if not is_instance_valid(ship):
|
if not is_instance_valid(ship):
|
||||||
continue
|
continue
|
||||||
var to_ship: Vector2 = ship.global_position - origin
|
if reaches_point(ship.global_position):
|
||||||
var along: float = to_ship.dot(aim_direction)
|
|
||||||
if along < 0.0 or along > beam_range:
|
|
||||||
continue
|
|
||||||
var perpendicular: float = (to_ship - aim_direction * along).length()
|
|
||||||
if perpendicular <= beam_half_width:
|
|
||||||
ship.take_damage(damage)
|
ship.take_damage(damage)
|
||||||
queue_redraw()
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
func reaches_point(world_point: Vector2) -> bool:
|
||||||
|
var offset: Vector2 = world_point - origin
|
||||||
|
var along: float = offset.dot(aim_direction)
|
||||||
|
if along < 0.0 or (not infinite_range and along > beam_range):
|
||||||
|
return false
|
||||||
|
return (offset - aim_direction * along).length() <= beam_half_width
|
||||||
|
|
||||||
|
|
||||||
func _draw() -> void:
|
func _draw() -> void:
|
||||||
# Drawn in local space, rotated in launch() so local +X already points
|
# Drawn in local space, rotated in launch() so local +X already points
|
||||||
# along the locked aim_direction - the same reason the beam segment's
|
# along the locked aim_direction - the same reason the beam segment's
|
||||||
|
|
@ -100,7 +110,21 @@ func _draw_beam_line(width: float, alpha: float) -> void:
|
||||||
# Three-layer glow along local +X - the same soft-halo / core / bright-
|
# Three-layer glow along local +X - the same soft-halo / core / bright-
|
||||||
# hairline structure as ArtilleryShell._draw_ring, adapted from a ring
|
# hairline structure as ArtilleryShell._draw_ring, adapted from a ring
|
||||||
# to a straight line.
|
# to a straight line.
|
||||||
var to: Vector2 = Vector2(beam_range, 0.0)
|
var length: float = _visible_ray_length() if infinite_range else beam_range
|
||||||
|
var to: Vector2 = Vector2(length, 0.0)
|
||||||
draw_line(Vector2.ZERO, to, Color(color, 0.16 * alpha), width * 2.6, true)
|
draw_line(Vector2.ZERO, to, Color(color, 0.16 * alpha), width * 2.6, true)
|
||||||
draw_line(Vector2.ZERO, to, Color(color, 0.5 * alpha), width, true)
|
draw_line(Vector2.ZERO, to, Color(color, 0.5 * alpha), width, true)
|
||||||
draw_line(Vector2.ZERO, to, Color(1.0, 0.92, 0.98, 0.9 * alpha), width * 0.35, true)
|
draw_line(Vector2.ZERO, to, Color(1.0, 0.92, 0.98, 0.9 * alpha), width * 0.35, true)
|
||||||
|
|
||||||
|
|
||||||
|
func _visible_ray_length() -> float:
|
||||||
|
# Collision is mathematically unbounded, but rasterizing a 20,000-unit
|
||||||
|
# primitive wastes fill work outside the screen. Reaching beyond the
|
||||||
|
# furthest viewport corner looks identical and follows a moving camera.
|
||||||
|
var viewport_world_size: Vector2 = get_viewport_rect().size
|
||||||
|
var camera: Camera2D = get_viewport().get_camera_2d()
|
||||||
|
if camera == null:
|
||||||
|
return viewport_world_size.length() + 256.0
|
||||||
|
viewport_world_size /= camera.zoom
|
||||||
|
return origin.distance_to(camera.global_position) \
|
||||||
|
+ viewport_world_size.length() * 0.5 + 256.0
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,9 @@ const VEIN_COLOR := Color(0.25, 0.9, 1.0)
|
||||||
const CORE_COLOR := Color(0.11, 0.015, 0.08)
|
const CORE_COLOR := Color(0.11, 0.015, 0.08)
|
||||||
|
|
||||||
var phase: float = 0.0
|
var phase: float = 0.0
|
||||||
|
var redraw_left: float = 0.0
|
||||||
|
|
||||||
|
const REDRAW_INTERVAL: float = 1.0 / 12.0
|
||||||
|
|
||||||
|
|
||||||
func configure(shape_seed: int) -> void:
|
func configure(shape_seed: int) -> void:
|
||||||
|
|
@ -21,7 +24,10 @@ func extractor_anchor() -> Vector2:
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
phase += delta
|
phase += delta
|
||||||
queue_redraw()
|
redraw_left -= delta
|
||||||
|
if redraw_left <= 0.0:
|
||||||
|
redraw_left = REDRAW_INTERVAL
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
func _draw() -> void:
|
func _draw() -> void:
|
||||||
|
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
shader_type canvas_item;
|
|
||||||
|
|
||||||
// An organic membrane rather than terrain: broad folds move below branching
|
|
||||||
// cyan veins. It follows SurfaceBackground's shared camera-uniform contract.
|
|
||||||
uniform vec2 camera_cell = vec2(0.0, 0.0);
|
|
||||||
uniform vec2 camera_frac = vec2(0.0, 0.0);
|
|
||||||
uniform float camera_zoom = 1.0;
|
|
||||||
uniform vec2 viewport_size = vec2(720.0, 1280.0);
|
|
||||||
uniform float cell_size = 220.0;
|
|
||||||
uniform float tile_cells = 32.0;
|
|
||||||
uniform vec4 base_color : source_color = vec4(0.055, 0.008, 0.05, 1.0);
|
|
||||||
uniform vec4 crater_color : source_color = vec4(0.16, 0.018, 0.10, 1.0);
|
|
||||||
uniform vec4 highlight_color : source_color = vec4(0.16, 0.66, 0.72, 1.0);
|
|
||||||
|
|
||||||
float hash21(vec2 p) {
|
|
||||||
vec3 p3 = fract(vec3(p.xyx) * 0.1031);
|
|
||||||
p3 += dot(p3, p3.yzx + 33.33);
|
|
||||||
return fract((p3.x + p3.y) * p3.z);
|
|
||||||
}
|
|
||||||
|
|
||||||
void fragment() {
|
|
||||||
vec2 screen_px = UV * viewport_size;
|
|
||||||
vec2 centered = screen_px - viewport_size * 0.5;
|
|
||||||
vec2 rel = camera_frac + centered * (camera_zoom / cell_size);
|
|
||||||
vec2 cell_rel = floor(rel);
|
|
||||||
vec2 local = rel - cell_rel;
|
|
||||||
vec2 wrapped = mod(camera_cell + cell_rel, tile_cells);
|
|
||||||
vec2 p = (wrapped + local) / tile_cells * 6.28318530718;
|
|
||||||
|
|
||||||
float fold_a = sin(p.x * 3.0 + sin(p.y * 2.0 - TIME * 0.16) * 1.1);
|
|
||||||
// Integer frequencies on both axes keep the 32-cell wrapped field
|
|
||||||
// seamless where p crosses from 2*PI back to zero.
|
|
||||||
float fold_b = sin(p.y * 4.0 - p.x * 2.0 + TIME * 0.11);
|
|
||||||
float tissue = 0.5 + 0.25 * fold_a + 0.25 * fold_b;
|
|
||||||
vec3 color = mix(base_color.rgb, crater_color.rgb, smoothstep(0.18, 0.85, tissue));
|
|
||||||
|
|
||||||
float vein_a = abs(sin(p.x * 5.0 + sin(p.y * 3.0 + TIME * 0.22)));
|
|
||||||
float vein_b = abs(sin(p.y * 6.0 - p.x * 2.0 - TIME * 0.18));
|
|
||||||
// Veins are environmental texture, not attack telegraphs. Keep them thin
|
|
||||||
// and subdued so the boss's bright pink laser lanes remain unambiguous.
|
|
||||||
float vein = 1.0 - smoothstep(0.0, 0.028, min(vein_a, vein_b));
|
|
||||||
float pulse = 0.55 + 0.45 * sin(TIME * 1.8 + p.x + p.y);
|
|
||||||
color = mix(color, highlight_color.rgb, vein * (0.10 + pulse * 0.20));
|
|
||||||
|
|
||||||
float dither = (hash21(screen_px + vec2(7.0, 13.0)) - 0.5) / 128.0;
|
|
||||||
COLOR = vec4(color + vec3(dither), 1.0);
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
uid://csf5umhh3dqft
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
[gd_resource type="Resource" script_class="PlanetSurfaceProfile" load_steps=4 format=3]
|
[gd_resource type="Resource" script_class="PlanetSurfaceProfile" load_steps=5 format=3]
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://surface/PlanetSurfaceProfile.gd" id="1"]
|
[ext_resource type="Script" path="res://surface/PlanetSurfaceProfile.gd" id="1"]
|
||||||
[ext_resource type="Shader" path="res://shaders/boss_surface.gdshader" id="2"]
|
[ext_resource type="Shader" path="res://shaders/lunar_tiled.gdshader" id="2"]
|
||||||
[ext_resource type="PackedScene" path="res://boss/BossEncounter.tscn" id="3"]
|
[ext_resource type="PackedScene" path="res://boss/BossEncounter.tscn" id="3"]
|
||||||
|
[ext_resource type="Texture2D" path="res://surface/boss_membrane.svg" id="4"]
|
||||||
|
|
||||||
[resource]
|
[resource]
|
||||||
script = ExtResource("1")
|
script = ExtResource("1")
|
||||||
|
|
@ -11,6 +12,7 @@ encounter_scene = ExtResource("3")
|
||||||
editor_visible = false
|
editor_visible = false
|
||||||
show_capture_status = false
|
show_capture_status = false
|
||||||
background_shader = ExtResource("2")
|
background_shader = ExtResource("2")
|
||||||
|
background_field_texture = ExtResource("4")
|
||||||
base_color = Color(0.055, 0.008, 0.05, 1)
|
base_color = Color(0.055, 0.008, 0.05, 1)
|
||||||
crater_color = Color(0.16, 0.018, 0.10, 1)
|
crater_color = Color(0.16, 0.018, 0.10, 1)
|
||||||
highlight_color = Color(0.16, 0.66, 0.72, 1)
|
highlight_color = Color(0.16, 0.66, 0.72, 1)
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,10 @@ extends Resource
|
||||||
# the game and the setup editor drift apart, since each scene "already had" a
|
# the game and the setup editor drift apart, since each scene "already had" a
|
||||||
# different shader. SurfaceBackground.apply_profile() is the only consumer.
|
# different shader. SurfaceBackground.apply_profile() is the only consumer.
|
||||||
@export var background_shader: Shader = null
|
@export var background_shader: Shader = null
|
||||||
|
# Optional ready-made field for a tiled background shader. Procedural rocky
|
||||||
|
# terrain still bakes once; authored surfaces can avoid both that load spike
|
||||||
|
# and all per-frame procedural fragment work by shipping their tile directly.
|
||||||
|
@export var background_field_texture: Texture2D = null
|
||||||
@export var base_color: Color = Color(0.75, 0.75, 0.76, 1.0)
|
@export var base_color: Color = Color(0.75, 0.75, 0.76, 1.0)
|
||||||
@export var crater_color: Color = Color(0.44, 0.44, 0.45, 1.0)
|
@export var crater_color: Color = Color(0.44, 0.44, 0.45, 1.0)
|
||||||
@export var highlight_color: Color = Color(0.89, 0.89, 0.9, 1.0)
|
@export var highlight_color: Color = Color(0.89, 0.89, 0.9, 1.0)
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ var camera: Camera2D = null
|
||||||
# differing only in colour, so keying on the shader alone would show void's
|
# differing only in colour, so keying on the shader alone would show void's
|
||||||
# palette with rocky's crater texture.
|
# palette with rocky's crater texture.
|
||||||
var _baked_key: Array = []
|
var _baked_key: Array = []
|
||||||
|
var _provided_field_texture: Texture2D = null
|
||||||
|
|
||||||
|
|
||||||
# Applies a surface profile to this backdrop: which shader draws it, and the
|
# Applies a surface profile to this backdrop: which shader draws it, and the
|
||||||
|
|
@ -68,9 +69,14 @@ func apply_profile(profile: Resource) -> void:
|
||||||
highlight_color = profile.highlight_color
|
highlight_color = profile.highlight_color
|
||||||
if profile.background_shader != null:
|
if profile.background_shader != null:
|
||||||
material.shader = profile.background_shader
|
material.shader = profile.background_shader
|
||||||
|
_provided_field_texture = profile.background_field_texture
|
||||||
material.set_shader_parameter("base_color", base_color)
|
material.set_shader_parameter("base_color", base_color)
|
||||||
material.set_shader_parameter("crater_color", crater_color)
|
material.set_shader_parameter("crater_color", crater_color)
|
||||||
material.set_shader_parameter("highlight_color", highlight_color)
|
material.set_shader_parameter("highlight_color", highlight_color)
|
||||||
|
if _provided_field_texture != null and _material_wants_baked_field():
|
||||||
|
material.set_shader_parameter("field_texture", _provided_field_texture)
|
||||||
|
_baked_key = [material.shader, _provided_field_texture]
|
||||||
|
return
|
||||||
_bake_field()
|
_bake_field()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
22
surface/boss_membrane.svg
Normal file
22
surface/boss_membrane.svg
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
|
||||||
|
<rect width="512" height="512" fill="#21061c"/>
|
||||||
|
<g fill="#451139" opacity="0.5">
|
||||||
|
<ellipse cx="112" cy="116" rx="104" ry="78"/>
|
||||||
|
<ellipse cx="374" cy="350" rx="126" ry="96"/>
|
||||||
|
<ellipse cx="246" cy="246" rx="72" ry="118" opacity="0.42"/>
|
||||||
|
</g>
|
||||||
|
<g fill="none" stroke-linecap="round">
|
||||||
|
<path d="M0 68 C120 18 164 128 268 76 S420 20 512 68" stroke="#2b6972" stroke-width="7" opacity="0.42"/>
|
||||||
|
<path d="M0 260 C102 214 176 324 282 270 S420 214 512 260" stroke="#245d68" stroke-width="6" opacity="0.35"/>
|
||||||
|
<path d="M0 452 C118 402 170 506 276 460 S418 404 512 452" stroke="#2b6972" stroke-width="6" opacity="0.38"/>
|
||||||
|
<path d="M94 0 C46 116 152 178 102 286 S46 420 94 512" stroke="#326f76" stroke-width="6" opacity="0.34"/>
|
||||||
|
<path d="M354 0 C300 102 410 180 356 276 S304 416 354 512" stroke="#285f69" stroke-width="7" opacity="0.4"/>
|
||||||
|
</g>
|
||||||
|
<g fill="#55a9ae" opacity="0.3">
|
||||||
|
<circle cx="268" cy="76" r="8"/>
|
||||||
|
<circle cx="282" cy="270" r="7"/>
|
||||||
|
<circle cx="276" cy="460" r="8"/>
|
||||||
|
<circle cx="102" cy="286" r="7"/>
|
||||||
|
<circle cx="356" cy="276" r="9"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
43
surface/boss_membrane.svg.import
Normal file
43
surface/boss_membrane.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://bajkflkc0r3cp"
|
||||||
|
path="res://.godot/imported/boss_membrane.svg-6482a210571f1472430bfb4ecba98f3c.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://surface/boss_membrane.svg"
|
||||||
|
dest_files=["res://.godot/imported/boss_membrane.svg-6482a210571f1472430bfb4ecba98f3c.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=true
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
|
svg/scale=1.0
|
||||||
|
editor/scale_with_editor_scale=false
|
||||||
|
editor/convert_colors_with_editor_theme=false
|
||||||
|
|
@ -35,15 +35,18 @@ func _process(_delta: float) -> void:
|
||||||
var boss: Node2D = encounter.get_node("Boss")
|
var boss: Node2D = encounter.get_node("Boss")
|
||||||
boss.wave_timer = 999.0
|
boss.wave_timer = 999.0
|
||||||
boss.laser_timer = 999.0
|
boss.laser_timer = 999.0
|
||||||
boss.phase = boss.Phase.LASER_STORM
|
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
boss.eyes[i].open_eye()
|
boss.eyes[i].open_eye()
|
||||||
boss.eyes_opened += 1
|
boss.eyes_opened += 1
|
||||||
|
boss.eyes[i].take_damage(999)
|
||||||
|
# Show one of the remaining weak points in its attackable state while the
|
||||||
|
# aggregate bar truthfully sits at half health.
|
||||||
|
boss.open_next_eye()
|
||||||
boss.spawn_wave()
|
boss.spawn_wave()
|
||||||
for minion in get_tree().get_nodes_in_group("boss_minions"):
|
for minion in get_tree().get_nodes_in_group("boss_minions"):
|
||||||
minion.speed = 0.0
|
minion.speed = 0.0
|
||||||
minion.acceleration = 0.0
|
minion.acceleration = 0.0
|
||||||
boss.laser_charge_time = 999.0
|
boss.laser_charge_time = 999.0
|
||||||
boss.fire_laser_volley()
|
boss.fire_laser_volley()
|
||||||
encounter._process(0.0)
|
encounter._refresh_status()
|
||||||
get_tree().paused = true
|
get_tree().paused = true
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,15 @@ func _process(delta: float) -> bool:
|
||||||
var profile: Resource = surface_profiles.get_profile("boss")
|
var profile: Resource = surface_profiles.get_profile("boss")
|
||||||
if profile == null or profile.encounter_scene == null:
|
if profile == null or profile.encounter_scene == null:
|
||||||
return _fail("boss surface has no scripted encounter")
|
return _fail("boss surface has no scripted encounter")
|
||||||
|
if profile.background_field_texture == null \
|
||||||
|
or not String(profile.background_shader.resource_path).ends_with(
|
||||||
|
"lunar_tiled.gdshader"):
|
||||||
|
return _fail("boss backdrop still performs procedural rendering during combat")
|
||||||
|
var composed_background: ShaderMaterial = surface.get_node(
|
||||||
|
"BackgroundLayer/Background").material
|
||||||
|
if composed_background.get_shader_parameter("field_texture") \
|
||||||
|
!= profile.background_field_texture:
|
||||||
|
return _fail("boss backdrop did not bind its ready-made field texture")
|
||||||
if profile.editor_visible or "boss" in surface_profiles.editor_profile_ids():
|
if profile.editor_visible or "boss" in surface_profiles.editor_profile_ids():
|
||||||
return _fail("boss surface is configurable in the map editor")
|
return _fail("boss surface is configurable in the map editor")
|
||||||
state.dev_mode = true
|
state.dev_mode = true
|
||||||
|
|
@ -103,6 +112,10 @@ func _process(delta: float) -> bool:
|
||||||
boss = encounter.get_node("Boss")
|
boss = encounter.get_node("Boss")
|
||||||
boss.wave_timer = 999.0
|
boss.wave_timer = 999.0
|
||||||
boss.laser_timer = 999.0
|
boss.laser_timer = 999.0
|
||||||
|
var health_bar: ProgressBar = encounter.get_node("EncounterUI/HealthBar")
|
||||||
|
if health_bar.max_value != boss.max_health() \
|
||||||
|
or health_bar.value != boss.current_health():
|
||||||
|
return _fail("aggregate boss health bar did not start full")
|
||||||
if boss.eyes.size() != boss.EYE_COUNT:
|
if boss.eyes.size() != boss.EYE_COUNT:
|
||||||
return _fail("boss has %d tentacle eyes, expected %d" \
|
return _fail("boss has %d tentacle eyes, expected %d" \
|
||||||
% [boss.eyes.size(), boss.EYE_COUNT])
|
% [boss.eyes.size(), boss.EYE_COUNT])
|
||||||
|
|
@ -126,6 +139,11 @@ func _process(delta: float) -> bool:
|
||||||
minion.take_damage(999)
|
minion.take_damage(999)
|
||||||
if marked == null:
|
if marked == null:
|
||||||
return _fail("phase-one wave has no marked hatchling")
|
return _fail("phase-one wave has no marked hatchling")
|
||||||
|
if marked.max_hp < 5:
|
||||||
|
return _fail("boss hatchlings still have only %d health" % marked.max_hp)
|
||||||
|
marked.take_damage(1)
|
||||||
|
if not is_instance_valid(marked) or marked.hp != marked.max_hp - 1:
|
||||||
|
return _fail("a hatchling did not survive a normal weapon hit")
|
||||||
if not get_nodes_in_group("boss_returns").is_empty():
|
if not get_nodes_in_group("boss_returns").is_empty():
|
||||||
return _fail("an ordinary hatchling returned energy to the boss")
|
return _fail("an ordinary hatchling returned energy to the boss")
|
||||||
marked.take_damage(999)
|
marked.take_damage(999)
|
||||||
|
|
@ -156,6 +174,9 @@ func _process(delta: float) -> bool:
|
||||||
eye.take_damage(1)
|
eye.take_damage(1)
|
||||||
if eye.hp != hp_before - 1:
|
if eye.hp != hp_before - 1:
|
||||||
return _fail("an open eye ignored weapon damage")
|
return _fail("an open eye ignored weapon damage")
|
||||||
|
if encounter.health_bar.value != boss.current_health() \
|
||||||
|
or encounter.health_bar.value >= encounter.health_bar.max_value:
|
||||||
|
return _fail("total boss health bar ignored eye damage")
|
||||||
eye.take_damage(999)
|
eye.take_damage(999)
|
||||||
if boss.eyes_destroyed != 1:
|
if boss.eyes_destroyed != 1:
|
||||||
return _fail("destroying the first eye did not damage the boss")
|
return _fail("destroying the first eye did not damage the boss")
|
||||||
|
|
@ -188,12 +209,19 @@ func _process(delta: float) -> bool:
|
||||||
var beams: Array = get_nodes_in_group("boss_lasers")
|
var beams: Array = get_nodes_in_group("boss_lasers")
|
||||||
if beams.size() != 3:
|
if beams.size() != 3:
|
||||||
return _fail("laser phase launched %d beams, expected three lanes" % beams.size())
|
return _fail("laser phase launched %d beams, expected three lanes" % beams.size())
|
||||||
|
if boss.laser_interval >= 5.5 or boss.laser_charge_time >= 1.6:
|
||||||
|
return _fail("boss lasers retained their old slow cadence")
|
||||||
if beams[0].aim_direction.is_equal_approx(beams[1].aim_direction) \
|
if beams[0].aim_direction.is_equal_approx(beams[1].aim_direction) \
|
||||||
or beams[1].aim_direction.is_equal_approx(beams[2].aim_direction):
|
or beams[1].aim_direction.is_equal_approx(beams[2].aim_direction):
|
||||||
return _fail("laser volley lanes all aim in the same direction")
|
return _fail("laser volley lanes all aim in the same direction")
|
||||||
for beam in beams:
|
for beam in beams:
|
||||||
if beam.phase != 0 or beam.charge_left <= 0.0:
|
if beam.phase != 0 or beam.charge_left <= 0.0:
|
||||||
return _fail("boss laser was not telegraphed before firing")
|
return _fail("boss laser was not telegraphed before firing")
|
||||||
|
if not beam.infinite_range or not beam.reaches_point(
|
||||||
|
beam.origin + beam.aim_direction * 1000000.0):
|
||||||
|
return _fail("boss laser ray stops at a finite range")
|
||||||
|
if beam.reaches_point(beam.origin - beam.aim_direction * 10.0):
|
||||||
|
return _fail("boss laser extends backwards through its source")
|
||||||
beam.queue_free()
|
beam.queue_free()
|
||||||
boss.spawn_wave()
|
boss.spawn_wave()
|
||||||
var phase_two_minions: Array = get_nodes_in_group("boss_minions")
|
var phase_two_minions: Array = get_nodes_in_group("boss_minions")
|
||||||
|
|
@ -204,6 +232,14 @@ func _process(delta: float) -> bool:
|
||||||
minion.retire()
|
minion.retire()
|
||||||
if phase_two_minions.size() != 3 or phase_two_marked != 1:
|
if phase_two_minions.size() != 3 or phase_two_marked != 1:
|
||||||
return _fail("laser phase no longer supplies one marked target to open remaining eyes")
|
return _fail("laser phase no longer supplies one marked target to open remaining eyes")
|
||||||
|
boss.spawn_wave()
|
||||||
|
boss.spawn_wave()
|
||||||
|
boss.spawn_wave()
|
||||||
|
if boss.spawned_minions.size() > boss.MAX_ACTIVE_MINIONS:
|
||||||
|
return _fail("boss waves can accumulate without a rendering/gameplay cap")
|
||||||
|
for minion in boss.spawned_minions.duplicate():
|
||||||
|
if is_instance_valid(minion):
|
||||||
|
minion.retire()
|
||||||
print("three broken eyes started a telegraphed three-lane laser phase")
|
print("three broken eyes started a telegraphed three-lane laser phase")
|
||||||
|
|
||||||
# --- F. all eyes -> persistent victory ------------------------
|
# --- F. all eyes -> persistent victory ------------------------
|
||||||
|
|
@ -214,6 +250,9 @@ func _process(delta: float) -> bool:
|
||||||
return _fail("breaking every eye did not defeat and hide the boss")
|
return _fail("breaking every eye did not defeat and hide the boss")
|
||||||
if not state.is_planet_conquered(state.planet_id):
|
if not state.is_planet_conquered(state.planet_id):
|
||||||
return _fail("boss victory did not persist through planet conquest state")
|
return _fail("boss victory did not persist through planet conquest state")
|
||||||
|
if encounter.health_bar.value != 0 \
|
||||||
|
or encounter.health_bar.max_value != boss.max_health():
|
||||||
|
return _fail("boss health bar did not end empty with a stable total")
|
||||||
print("all eyes broken: boss defeated and planet conquest persisted")
|
print("all eyes broken: boss defeated and planet conquest persisted")
|
||||||
|
|
||||||
surface.free()
|
surface.free()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue