Add tentacled boss planet encounter
This commit is contained in:
parent
0f7edff15a
commit
40f284dcdd
38 changed files with 1051 additions and 19 deletions
49
tests/_boss_planet_preview.gd
Normal file
49
tests/_boss_planet_preview.gd
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
extends Node2D
|
||||
|
||||
# Stable portrait composition for visually checking #188. The real surface,
|
||||
# encounter, fleet, UI, minions, open eyes and laser telegraph are all used;
|
||||
# only their timers are frozen once the representative second phase is laid
|
||||
# out so a screenshot does not depend on capture timing.
|
||||
|
||||
var surface: Node
|
||||
var laid_out: bool = false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
GameState.reset_new_game()
|
||||
GameState.intro_active = false
|
||||
GameState.reset_quests(false)
|
||||
GameState.planet_name = "Many-Eyed Deep"
|
||||
GameState.planet_id = "preview:boss"
|
||||
GameState.planet_definition_id = "boss"
|
||||
GameState.surviving_ship_count = 3
|
||||
GameState.surviving_vessel_types = PackedStringArray([
|
||||
GameState.VESSEL_INTERCEPTOR,
|
||||
GameState.VESSEL_GAS_BOMBER,
|
||||
GameState.VESSEL_LASER,
|
||||
])
|
||||
surface = load("res://surface/PlanetSurface.tscn").instantiate()
|
||||
add_child(surface)
|
||||
surface.get_node("FadeOverlay/FadeRect").visible = false
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if laid_out:
|
||||
return
|
||||
laid_out = true
|
||||
var encounter: Node = surface.get_node("SurfaceFeatures/BossEncounter")
|
||||
var boss: Node2D = encounter.get_node("Boss")
|
||||
boss.wave_timer = 999.0
|
||||
boss.laser_timer = 999.0
|
||||
boss.phase = boss.Phase.LASER_STORM
|
||||
for i in range(3):
|
||||
boss.eyes[i].open_eye()
|
||||
boss.eyes_opened += 1
|
||||
boss.spawn_wave()
|
||||
for minion in get_tree().get_nodes_in_group("boss_minions"):
|
||||
minion.speed = 0.0
|
||||
minion.acceleration = 0.0
|
||||
boss.laser_charge_time = 999.0
|
||||
boss.fire_laser_volley()
|
||||
encounter._process(0.0)
|
||||
get_tree().paused = true
|
||||
1
tests/_boss_planet_preview.gd.uid
Normal file
1
tests/_boss_planet_preview.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://7n3t677nrm0b
|
||||
6
tests/_boss_planet_preview.tscn
Normal file
6
tests/_boss_planet_preview.tscn
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://tests/_boss_planet_preview.gd" id="1"]
|
||||
|
||||
[node name="BossPlanetPreview" type="Node2D"]
|
||||
script = ExtResource("1")
|
||||
255
tests/test_boss_planet.gd
Normal file
255
tests/test_boss_planet.gd
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
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:
|
||||
# A. the planet/profile are registered, dev mode plants it nearby, and its
|
||||
# scripted profile is excluded from the map editor
|
||||
# B. landing composes BossEncounter instead of a PlanetSetup and does not
|
||||
# mark the world conquered for free
|
||||
# C. phase one spawns an octopus wave with exactly one marked hatchling
|
||||
# D. killing the marked hatchling sends a visible return to the boss and
|
||||
# opens an attackable tentacle eye
|
||||
# E. three destroyed eyes start the three-lane laser phase
|
||||
# F. destroying every eye defeats the boss, persists conquest, and a second
|
||||
# landing does not resurrect it
|
||||
|
||||
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")
|
||||
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
|
||||
var normal_boss_found: bool = false
|
||||
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:
|
||||
normal_boss_found = true
|
||||
break
|
||||
if normal_boss_found:
|
||||
break
|
||||
if not normal_boss_found:
|
||||
return _fail("normal world generation never produces the boss planet")
|
||||
print("boss planet registered, editor-hidden, and generated normally plus in dev mode")
|
||||
|
||||
# --- 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")
|
||||
boss.wave_timer = 999.0
|
||||
boss.laser_timer = 999.0
|
||||
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")
|
||||
|
||||
# --- C. phase-one wave ----------------------------------------
|
||||
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
|
||||
for minion in minions:
|
||||
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")
|
||||
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
|
||||
print("phase one spawned a wave; the marked kill returned toward the boss")
|
||||
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")
|
||||
eye.take_damage(999)
|
||||
if boss.eyes_destroyed != 1:
|
||||
return _fail("destroying the first eye did not damage the boss")
|
||||
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())
|
||||
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")
|
||||
beam.queue_free()
|
||||
boss.spawn_wave()
|
||||
var phase_two_minions: Array = get_nodes_in_group("boss_minions")
|
||||
var phase_two_marked: int = 0
|
||||
for minion in phase_two_minions:
|
||||
if minion.marked:
|
||||
phase_two_marked += 1
|
||||
minion.retire()
|
||||
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")
|
||||
print("three broken eyes started a telegraphed three-lane laser phase")
|
||||
|
||||
# --- F. all eyes -> persistent victory ------------------------
|
||||
for i in range(3, boss.EYE_COUNT):
|
||||
boss.eyes[i].open_eye()
|
||||
boss.eyes[i].take_damage(999)
|
||||
if boss.phase != BOSS_SCRIPT.Phase.DEFEATED or boss.visible:
|
||||
return _fail("breaking every eye did not defeat and hide the boss")
|
||||
if not state.is_planet_conquered(state.planet_id):
|
||||
return _fail("boss victory did not persist through planet conquest state")
|
||||
print("all eyes broken: boss defeated and planet conquest persisted")
|
||||
|
||||
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
|
||||
1
tests/test_boss_planet.gd.uid
Normal file
1
tests/test_boss_planet.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dgdo2pk2ra7ii
|
||||
|
|
@ -105,7 +105,8 @@ func _process(delta: float) -> bool:
|
|||
return _fail("dev mode's %s spawned %.0f units away, too far to be 'near the start'" \
|
||||
% [definition_id, world_pos.length()])
|
||||
for definition_id in [PlanetDefinitions.ROCKY_ID, PlanetDefinitions.GAS_GIANT_ID,
|
||||
PlanetDefinitions.BLACK_HOLE_ID, PlanetDefinitions.RESEARCH_LAB_ID]:
|
||||
PlanetDefinitions.BLACK_HOLE_ID, PlanetDefinitions.RESEARCH_LAB_ID,
|
||||
PlanetDefinitions.BOSS_ID]:
|
||||
if not found.has(definition_id):
|
||||
return _fail("dev mode is missing a %s - it must plant one of EVERY planet type" % definition_id)
|
||||
print("dev mode plants all %d planet types within %.0f units of the spawn" \
|
||||
|
|
|
|||
Loading…
Reference in a new issue