2026-09-03 10:33:05 +02:00
|
|
|
extends SceneTree
|
|
|
|
|
|
|
|
|
|
# Forgejo #188: the authored, non-editor boss planet and its complete weak-
|
|
|
|
|
# point loop. This test drives the real landed surface and encounter nodes.
|
|
|
|
|
#
|
|
|
|
|
# Covers:
|
2026-09-04 15:56:07 +02:00
|
|
|
# A. the planet/profile are registered, dev mode plants it nearby, normal
|
|
|
|
|
# generation owns exactly one inside its octopus biome, and its scripted
|
|
|
|
|
# profile is excluded from the map editor
|
2026-09-03 10:33:05 +02:00
|
|
|
# B. landing composes BossEncounter instead of a PlanetSetup and does not
|
|
|
|
|
# mark the world conquered for free
|
2026-09-03 22:21:11 +02:00
|
|
|
# C. the off-screen approach remains controllable and reveals with its own cue
|
2026-09-03 22:00:26 +02:00
|
|
|
# D. waves are larger, expelled outward, varied, and golden only 1/3 times
|
|
|
|
|
# E. killing the marked hatchling sends a visible return to the boss and
|
2026-09-03 10:33:05 +02:00
|
|
|
# opens an attackable tentacle eye
|
2026-09-03 22:00:26 +02:00
|
|
|
# F. three destroyed eyes start the three-lane laser phase
|
|
|
|
|
# G. eyes expose a separate core; destroying it persists conquest, and a
|
|
|
|
|
# second landing does not resurrect the boss
|
2026-09-03 10:33:05 +02:00
|
|
|
|
|
|
|
|
const COMBAT_TARGET = preload("res://core/CombatTarget.gd")
|
|
|
|
|
const BOSS_SCRIPT = preload("res://boss/OctopusBoss.gd")
|
|
|
|
|
|
|
|
|
|
var state: Node
|
|
|
|
|
var surface: Node
|
|
|
|
|
var encounter: Node
|
|
|
|
|
var boss: Node2D
|
|
|
|
|
var stage: int = 0
|
|
|
|
|
var elapsed: float = 0.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _initialize() -> void:
|
|
|
|
|
state = root.get_node("GameState")
|
|
|
|
|
state.reset_new_game()
|
|
|
|
|
state.intro_active = false
|
|
|
|
|
state.reset_quests(false)
|
|
|
|
|
state.planet_name = "Many-Eyed Deep"
|
|
|
|
|
state.planet_id = "test:boss:planet"
|
|
|
|
|
state.planet_definition_id = PlanetDefinitions.BOSS_ID
|
|
|
|
|
state.surviving_ship_count = 1
|
|
|
|
|
state.surviving_vessel_types = PackedStringArray([GameState.VESSEL_INTERCEPTOR])
|
|
|
|
|
surface = load("res://surface/PlanetSurface.tscn").instantiate()
|
|
|
|
|
root.add_child(surface)
|
|
|
|
|
current_scene = surface
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _process(delta: float) -> bool:
|
|
|
|
|
elapsed += delta
|
|
|
|
|
match stage:
|
|
|
|
|
0:
|
|
|
|
|
if elapsed < 0.2:
|
|
|
|
|
return false
|
|
|
|
|
# --- A. definition, dev placement, editor exclusion ----------
|
|
|
|
|
var definition: Resource = PlanetDefinitions.get_definition(
|
|
|
|
|
PlanetDefinitions.BOSS_ID)
|
|
|
|
|
if definition == null or definition.definition_id != PlanetDefinitions.BOSS_ID \
|
|
|
|
|
or definition.surface_profile_id != "boss" \
|
|
|
|
|
or definition.visual_scene == null:
|
|
|
|
|
return _fail("boss planet definition is not registered")
|
|
|
|
|
# Load at runtime, after the GameState autoload has compiled. Resolving
|
|
|
|
|
# the whole surface registry while that autoload is still compiling
|
|
|
|
|
# also pulls in pickup scripts which legitimately refer back to it.
|
|
|
|
|
var surface_profiles: Script = load("res://surface/PlanetSurfaceProfiles.gd")
|
|
|
|
|
var profile: Resource = surface_profiles.get_profile("boss")
|
|
|
|
|
if profile == null or profile.encounter_scene == null:
|
|
|
|
|
return _fail("boss surface has no scripted encounter")
|
2026-09-03 11:49:51 +02:00
|
|
|
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")
|
2026-09-03 22:31:32 +02:00
|
|
|
if float(composed_background.get_shader_parameter("cell_size")) >= 120.0:
|
|
|
|
|
return _fail("boss backdrop still magnifies too small a part of its authored field")
|
2026-09-03 10:33:05 +02:00
|
|
|
if profile.editor_visible or "boss" in surface_profiles.editor_profile_ids():
|
|
|
|
|
return _fail("boss surface is configurable in the map editor")
|
|
|
|
|
state.dev_mode = true
|
|
|
|
|
var boss_chunk: Variant = null
|
|
|
|
|
for chunk in WorldGen.DEV_PLANET_CHUNKS:
|
|
|
|
|
if String(WorldGen.DEV_PLANET_CHUNKS[chunk]) == PlanetDefinitions.BOSS_ID:
|
|
|
|
|
boss_chunk = chunk
|
|
|
|
|
break
|
|
|
|
|
if boss_chunk == null or not WorldGen.has_planet_at_chunk(boss_chunk) \
|
|
|
|
|
or String(WorldGen.planet_data_at_chunk(boss_chunk).definition_id) \
|
|
|
|
|
!= PlanetDefinitions.BOSS_ID:
|
|
|
|
|
return _fail("dev mode does not plant the boss planet")
|
|
|
|
|
state.dev_mode = false
|
2026-09-04 15:56:07 +02:00
|
|
|
var normal_boss_count: int = 0
|
2026-09-03 10:33:05 +02:00
|
|
|
for x in range(-24, 25):
|
|
|
|
|
for y in range(-24, 25):
|
|
|
|
|
var chunk := Vector2i(x, y)
|
|
|
|
|
if WorldGen.has_planet_at_chunk(chunk) \
|
|
|
|
|
and String(WorldGen.planet_data_at_chunk(chunk).get(
|
|
|
|
|
"definition_id", "")) == PlanetDefinitions.BOSS_ID:
|
2026-09-04 15:56:07 +02:00
|
|
|
normal_boss_count += 1
|
|
|
|
|
if normal_boss_count != 1 \
|
|
|
|
|
or WorldGen.biome_at_chunk(WorldGen.OCTOPUS_BIOME_CENTER) \
|
|
|
|
|
!= WorldGen.Biome.OCTOPUS:
|
|
|
|
|
return _fail("normal world generation has %d bosses, or its only boss is outside the octopus biome" \
|
|
|
|
|
% normal_boss_count)
|
|
|
|
|
print("boss planet registered, editor-hidden, unique in its normal octopus biome, plus nearby in dev mode")
|
2026-09-03 10:33:05 +02:00
|
|
|
|
|
|
|
|
# --- B. generic surface composition ---------------------------
|
|
|
|
|
encounter = surface.get_node_or_null("SurfaceFeatures/BossEncounter")
|
|
|
|
|
if encounter == null:
|
|
|
|
|
return _fail("landing did not compose BossEncounter")
|
|
|
|
|
if surface.get_node_or_null("SurfaceFeatures/PlanetSetupHost") != null:
|
|
|
|
|
return _fail("boss planet was routed through configurable PlanetSetup")
|
|
|
|
|
if state.total_capture_points(state.planet_id) != 1 \
|
|
|
|
|
or state.is_planet_conquered(state.planet_id):
|
|
|
|
|
return _fail("boss planet did not start as one unfinished persistent objective")
|
|
|
|
|
if surface.get_node("UI/CaptureLabel").visible:
|
|
|
|
|
return _fail("ordinary capture-point status is visible during the boss encounter")
|
|
|
|
|
boss = encounter.get_node("Boss")
|
2026-09-03 22:00:26 +02:00
|
|
|
if not (boss is Area2D) or boss.get_node_or_null("CoreCollision") == null:
|
|
|
|
|
return _fail("the boss core has no projectile collision target")
|
|
|
|
|
if boss.position.y > -2400.0 or boss.scale.x < 2.7:
|
|
|
|
|
return _fail("the boss is not far above the landing point and at least twice its old size")
|
|
|
|
|
if encounter.health_bar.visible or encounter.status_label.visible:
|
|
|
|
|
return _fail("boss combat UI is visible before the boss enters view")
|
|
|
|
|
if not encounter.indicator.visible or not encounter.indicator_label.visible:
|
|
|
|
|
return _fail("the off-screen boss has no directional border indicator")
|
2026-09-03 22:21:11 +02:00
|
|
|
if state.encounter_locks_movement or boss.combat_active:
|
|
|
|
|
return _fail("the off-screen approach locked player movement or already started combat")
|
2026-09-04 18:40:58 +02:00
|
|
|
# Seeing a tentacle is not enough to trigger the cinematic or its roar.
|
2026-09-03 22:00:26 +02:00
|
|
|
surface.get_node("Camera2D").global_position = boss.global_position + Vector2(0.0, 1000.0)
|
|
|
|
|
encounter._process(0.0)
|
2026-09-04 18:40:58 +02:00
|
|
|
if encounter.reveal_started:
|
|
|
|
|
return _fail("a partially visible boss triggered its reveal and scream")
|
|
|
|
|
# Carry the fleet close enough to frame the complete central body.
|
|
|
|
|
surface.get_node("Camera2D").global_position = boss.global_position + Vector2(0.0, 250.0)
|
|
|
|
|
encounter._process(0.0)
|
2026-09-03 22:00:26 +02:00
|
|
|
if not encounter.reveal_started:
|
2026-09-04 18:40:58 +02:00
|
|
|
return _fail("fully framing the boss did not trigger its reveal")
|
2026-09-03 22:21:11 +02:00
|
|
|
if not state.encounter_locks_movement:
|
|
|
|
|
return _fail("the reveal did not briefly lock movement for the cinematic")
|
2026-09-03 22:00:26 +02:00
|
|
|
if not encounter.health_bar.visible or encounter.indicator.visible:
|
|
|
|
|
return _fail("the reveal did not exchange the indicator for the health UI")
|
|
|
|
|
var scream_heard: bool = false
|
|
|
|
|
var sfx_pool: Node = root.get_node_or_null(Sfx.NODE_NAME)
|
|
|
|
|
if sfx_pool != null:
|
|
|
|
|
for voice in sfx_pool.get_children():
|
|
|
|
|
if voice.stream != null and voice.stream.resource_path.ends_with("sfx_boss_scream.wav"):
|
|
|
|
|
scream_heard = true
|
|
|
|
|
if not scream_heard:
|
|
|
|
|
return _fail("the on-screen reveal played no boss scream")
|
|
|
|
|
encounter._process(encounter.REVEAL_SECONDS + 0.1)
|
|
|
|
|
if not encounter.encounter_active or not boss.combat_active \
|
|
|
|
|
or state.encounter_locks_movement:
|
|
|
|
|
return _fail("the reveal did not start combat and return fleet control")
|
2026-09-03 22:31:32 +02:00
|
|
|
var boss_position_before_move: Vector2 = boss.global_position
|
|
|
|
|
boss._process(0.5)
|
|
|
|
|
if boss.global_position.distance_to(boss_position_before_move) < 10.0:
|
|
|
|
|
return _fail("the active boss remained stationary")
|
2026-09-03 22:00:26 +02:00
|
|
|
print("off-screen signal led into a roar, shake, health reveal and player control")
|
2026-09-03 10:33:05 +02:00
|
|
|
boss.wave_timer = 999.0
|
|
|
|
|
boss.laser_timer = 999.0
|
2026-09-03 11:49:51 +02:00
|
|
|
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")
|
2026-09-03 10:33:05 +02:00
|
|
|
if boss.eyes.size() != boss.EYE_COUNT:
|
|
|
|
|
return _fail("boss has %d tentacle eyes, expected %d" \
|
|
|
|
|
% [boss.eyes.size(), boss.EYE_COUNT])
|
|
|
|
|
for eye in boss.eyes:
|
|
|
|
|
if eye.opened or eye.is_in_group(COMBAT_TARGET.GROUP):
|
|
|
|
|
return _fail("a tentacle eye starts open and attackable")
|
2026-09-03 22:31:32 +02:00
|
|
|
if boss.ARMOURED_IRIS_COLOR.is_equal_approx(boss.eyes[0].IRIS_COLOR) \
|
|
|
|
|
or boss.ARMOURED_PUPIL_COLOR.is_equal_approx(boss.eyes[0].PUPIL_COLOR):
|
|
|
|
|
return _fail("the central eye still shares the tentacle-eye palette")
|
2026-09-03 10:33:05 +02:00
|
|
|
|
2026-09-03 22:00:26 +02:00
|
|
|
# --- D. larger, varied, one-in-three waves --------------------
|
2026-09-03 10:33:05 +02:00
|
|
|
boss.spawn_wave()
|
|
|
|
|
var minions: Array = get_nodes_in_group("boss_minions")
|
|
|
|
|
if minions.size() != boss.minions_per_wave:
|
|
|
|
|
return _fail("phase one spawned %d hatchlings, expected %d" \
|
|
|
|
|
% [minions.size(), boss.minions_per_wave])
|
|
|
|
|
var marked: Node = null
|
2026-09-03 22:00:26 +02:00
|
|
|
var dashers: int = 0
|
2026-09-03 10:33:05 +02:00
|
|
|
for minion in minions:
|
2026-09-03 22:00:26 +02:00
|
|
|
if minion.uses_dash_pattern:
|
|
|
|
|
dashers += 1
|
2026-09-03 10:33:05 +02:00
|
|
|
if minion.marked:
|
|
|
|
|
if marked != null:
|
|
|
|
|
return _fail("one wave contains more than one marked hatchling")
|
|
|
|
|
marked = minion
|
|
|
|
|
else:
|
|
|
|
|
minion.take_damage(999)
|
|
|
|
|
if marked == null:
|
|
|
|
|
return _fail("phase-one wave has no marked hatchling")
|
2026-09-03 22:00:26 +02:00
|
|
|
if minions.size() < 7 or dashers < 2:
|
|
|
|
|
return _fail("the boss wave is not larger and behaviorally mixed")
|
|
|
|
|
var launch_distance: float = marked.global_position.distance_to(boss.global_position)
|
|
|
|
|
var launch_velocity: Vector2 = marked.velocity
|
|
|
|
|
if launch_distance <= boss.BODY_RADIUS * boss.scale.x \
|
|
|
|
|
or launch_velocity.dot(marked.global_position - boss.global_position) <= 0.0:
|
|
|
|
|
return _fail("a hatchling was not expelled outward from the boss")
|
|
|
|
|
marked._physics_process(0.4)
|
|
|
|
|
if marked.global_position.distance_to(boss.global_position) <= launch_distance + 100.0:
|
|
|
|
|
return _fail("the outward launch did not spread a hatchling before it targeted the fleet")
|
|
|
|
|
marked._physics_process(1.0)
|
|
|
|
|
marked.dash_cooldown = 0.0
|
|
|
|
|
marked._physics_process(0.01)
|
|
|
|
|
if marked.motion != marked.Motion.BRAKING:
|
|
|
|
|
return _fail("a dash-pattern hatchling never stops before lunging")
|
|
|
|
|
marked._physics_process(marked.BRAKE_SECONDS + 0.01)
|
|
|
|
|
marked._physics_process(0.2)
|
|
|
|
|
if marked.motion != marked.Motion.DASHING or marked.velocity.length() <= marked.speed:
|
|
|
|
|
return _fail("a stopped hatchling did not accelerate into its random dash")
|
2026-09-03 11:49:51 +02:00
|
|
|
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")
|
2026-09-03 10:33:05 +02:00
|
|
|
if not get_nodes_in_group("boss_returns").is_empty():
|
|
|
|
|
return _fail("an ordinary hatchling returned energy to the boss")
|
|
|
|
|
marked.take_damage(999)
|
|
|
|
|
var returns: Array = get_nodes_in_group("boss_returns")
|
|
|
|
|
if returns.size() != 1:
|
|
|
|
|
return _fail("marked death launched %d returns, expected one" % returns.size())
|
|
|
|
|
returns[0].speed = 4000.0
|
2026-09-03 22:00:26 +02:00
|
|
|
for expected_marked in [0, 0, 1]:
|
|
|
|
|
boss.spawn_wave()
|
|
|
|
|
var spawned: Array = get_nodes_in_group("boss_minions")
|
|
|
|
|
var marked_count: int = 0
|
|
|
|
|
for minion in spawned:
|
|
|
|
|
if minion.is_queued_for_deletion():
|
|
|
|
|
continue
|
|
|
|
|
if minion.marked:
|
|
|
|
|
marked_count += 1
|
|
|
|
|
minion.retire()
|
|
|
|
|
if marked_count != expected_marked:
|
|
|
|
|
return _fail("golden cadence produced %d marked hatchlings, expected %d" \
|
|
|
|
|
% [marked_count, expected_marked])
|
|
|
|
|
print("larger waves launch outward with dashers; only one of three is golden")
|
2026-09-03 10:33:05 +02:00
|
|
|
stage = 1
|
|
|
|
|
elapsed = 0.0
|
|
|
|
|
return false
|
|
|
|
|
1:
|
|
|
|
|
# --- D. return opens an attackable eye ------------------------
|
|
|
|
|
if boss.eyes_opened < 1:
|
|
|
|
|
if elapsed > 1.0:
|
|
|
|
|
var pending: Array = get_nodes_in_group("boss_returns")
|
|
|
|
|
var detail: String = "no return remains"
|
|
|
|
|
if not pending.is_empty():
|
|
|
|
|
detail = "return at %s, boss at %s, speed %.1f, processing %s" % [
|
|
|
|
|
pending[0].global_position, boss.global_position,
|
|
|
|
|
pending[0].speed, pending[0].is_processing()]
|
|
|
|
|
return _fail("marked energy reached no tentacle eye (%s)" % detail)
|
|
|
|
|
return false
|
|
|
|
|
var eye: Node = boss.eyes[0]
|
|
|
|
|
if not eye.opened or not eye.is_in_group(COMBAT_TARGET.GROUP):
|
|
|
|
|
return _fail("returned energy did not make the eye attackable")
|
|
|
|
|
var hp_before: int = eye.hp
|
|
|
|
|
eye.take_damage(1)
|
|
|
|
|
if eye.hp != hp_before - 1:
|
|
|
|
|
return _fail("an open eye ignored weapon damage")
|
2026-09-03 11:49:51 +02:00
|
|
|
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")
|
2026-09-03 10:33:05 +02:00
|
|
|
eye.take_damage(999)
|
|
|
|
|
if boss.eyes_destroyed != 1:
|
|
|
|
|
return _fail("destroying the first eye did not damage the boss")
|
2026-09-03 22:00:26 +02:00
|
|
|
if boss.max_health() - boss.current_health() >= boss.max_health() / boss.EYE_COUNT:
|
|
|
|
|
return _fail("one eye still removes a full sixth of the total encounter health")
|
2026-09-03 10:33:05 +02:00
|
|
|
print("return opened a tentacle eye; the eye accepted damage and broke")
|
|
|
|
|
_open_return()
|
|
|
|
|
stage = 2
|
|
|
|
|
elapsed = 0.0
|
|
|
|
|
return false
|
|
|
|
|
2:
|
|
|
|
|
if boss.eyes_opened < 2:
|
|
|
|
|
if elapsed > 1.0:
|
|
|
|
|
return _fail("second marked return opened no eye")
|
|
|
|
|
return false
|
|
|
|
|
boss.eyes[1].take_damage(999)
|
|
|
|
|
_open_return()
|
|
|
|
|
stage = 3
|
|
|
|
|
elapsed = 0.0
|
|
|
|
|
return false
|
|
|
|
|
3:
|
|
|
|
|
# --- E. phase two and laser pattern ---------------------------
|
|
|
|
|
if boss.eyes_opened < 3:
|
|
|
|
|
if elapsed > 1.0:
|
|
|
|
|
return _fail("third marked return opened no eye")
|
|
|
|
|
return false
|
|
|
|
|
boss.eyes[2].take_damage(999)
|
|
|
|
|
if boss.phase != BOSS_SCRIPT.Phase.LASER_STORM:
|
|
|
|
|
return _fail("three broken eyes did not start the laser phase")
|
|
|
|
|
boss.laser_timer = 999.0
|
|
|
|
|
boss.fire_laser_volley()
|
|
|
|
|
var beams: Array = get_nodes_in_group("boss_lasers")
|
|
|
|
|
if beams.size() != 3:
|
|
|
|
|
return _fail("laser phase launched %d beams, expected three lanes" % beams.size())
|
2026-09-03 22:31:32 +02:00
|
|
|
if boss.laser_interval > 2.5 or boss.laser_charge_time > 0.75:
|
|
|
|
|
return _fail("boss lasers are not using the faster cadence")
|
2026-09-03 10:33:05 +02:00
|
|
|
if beams[0].aim_direction.is_equal_approx(beams[1].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")
|
|
|
|
|
for beam in beams:
|
|
|
|
|
if beam.phase != 0 or beam.charge_left <= 0.0:
|
|
|
|
|
return _fail("boss laser was not telegraphed before firing")
|
2026-09-03 11:49:51 +02:00
|
|
|
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")
|
2026-09-03 10:33:05 +02:00
|
|
|
beam.queue_free()
|
|
|
|
|
boss.spawn_wave()
|
2026-09-03 11:49:51 +02:00
|
|
|
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()
|
2026-09-03 10:33:05 +02:00
|
|
|
print("three broken eyes started a telegraphed three-lane laser phase")
|
|
|
|
|
|
2026-09-03 22:00:26 +02:00
|
|
|
# --- G. all eyes -> exposed core -> persistent victory --------
|
2026-09-03 10:33:05 +02:00
|
|
|
for i in range(3, boss.EYE_COUNT):
|
|
|
|
|
boss.eyes[i].open_eye()
|
|
|
|
|
boss.eyes[i].take_damage(999)
|
2026-09-03 22:00:26 +02:00
|
|
|
if boss.phase != BOSS_SCRIPT.Phase.CORE_EXPOSED or not boss.visible:
|
|
|
|
|
return _fail("breaking every eye did not expose a still-living core")
|
|
|
|
|
if state.is_planet_conquered(state.planet_id):
|
|
|
|
|
return _fail("breaking the eyes conquered the planet before the core died")
|
|
|
|
|
if not boss.is_in_group(COMBAT_TARGET.GROUP):
|
|
|
|
|
return _fail("the exposed core is not an attackable combat target")
|
|
|
|
|
var core_before: int = boss.core_health
|
|
|
|
|
boss.take_damage(1)
|
|
|
|
|
if boss.core_health != core_before - 1 or boss.phase == BOSS_SCRIPT.Phase.DEFEATED:
|
|
|
|
|
return _fail("the exposed core did not provide a separate final health pool")
|
|
|
|
|
boss.take_damage(999)
|
2026-09-03 10:33:05 +02:00
|
|
|
if boss.phase != BOSS_SCRIPT.Phase.DEFEATED or boss.visible:
|
2026-09-03 22:00:26 +02:00
|
|
|
return _fail("destroying the core did not defeat and hide the boss")
|
2026-09-03 10:33:05 +02:00
|
|
|
if not state.is_planet_conquered(state.planet_id):
|
|
|
|
|
return _fail("boss victory did not persist through planet conquest state")
|
2026-09-03 11:49:51 +02:00
|
|
|
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")
|
2026-09-03 22:00:26 +02:00
|
|
|
print("all eyes exposed a durable core; destroying it persisted planet conquest")
|
2026-09-03 10:33:05 +02:00
|
|
|
|
|
|
|
|
surface.free()
|
|
|
|
|
surface = load("res://surface/PlanetSurface.tscn").instantiate()
|
|
|
|
|
root.add_child(surface)
|
|
|
|
|
current_scene = surface
|
|
|
|
|
stage = 4
|
|
|
|
|
elapsed = 0.0
|
|
|
|
|
return false
|
|
|
|
|
4:
|
|
|
|
|
if elapsed < 0.2:
|
|
|
|
|
return false
|
|
|
|
|
var second_encounter: Node = surface.get_node_or_null(
|
|
|
|
|
"SurfaceFeatures/BossEncounter")
|
|
|
|
|
if second_encounter == null:
|
|
|
|
|
return _fail("second landing lost the boss encounter host")
|
|
|
|
|
var second_boss: Node = second_encounter.get_node("Boss")
|
|
|
|
|
if second_boss.phase != BOSS_SCRIPT.Phase.DEFEATED or second_boss.visible:
|
|
|
|
|
return _fail("defeated boss resurrected on the second landing")
|
|
|
|
|
if not "DEAD" in second_encounter.status_label.text:
|
|
|
|
|
return _fail("second landing does not report the defeated boss")
|
|
|
|
|
print("second landing keeps the boss dead")
|
|
|
|
|
print("TEST_BOSS_PLANET: PASS")
|
|
|
|
|
quit(0)
|
|
|
|
|
return true
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _open_return() -> void:
|
|
|
|
|
boss.accept_marked_kill(boss.global_position + Vector2(12, 0))
|
|
|
|
|
var returns: Array = get_nodes_in_group("boss_returns")
|
|
|
|
|
for returning in returns:
|
|
|
|
|
returning.speed = 4000.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _fail(message: String) -> bool:
|
|
|
|
|
print("TEST_BOSS_PLANET: FAIL (%s)" % message)
|
|
|
|
|
quit(1)
|
|
|
|
|
return true
|