Take planets by killing what defends them
#211 conquest is the fight now: a planet registers the garrison its setup authors, each defender's own death counts against it, and the last one takes the world. The capture points - stand in a ring, wait out a timer, once per ring - are gone, along with the editor palette that placed them. #212 conquest opens a dialog on the surface naming the research points recovered; the award travels with the signal, after it is banked. #209 the prologue's charted sectors go with the breach, so the map the survivors keep is of where they now are - and "chart 30 sectors" is no longer complete before they have flown anywhere. #218 enemy variety is gated on the FLEET, not on wave strength: a starting fleet meets chasers only at any wave number, and each vessel past three opens one more kind. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
This commit is contained in:
parent
78836882af
commit
6e13b6397c
42 changed files with 540 additions and 749 deletions
|
|
@ -1,57 +0,0 @@
|
|||
extends Node2D
|
||||
|
||||
# Preloaded by path rather than an `is CapturePoint` check - this preview
|
||||
# runs through screenshot.sh, which (unlike run_tests.sh) does not run an
|
||||
# import pass first, so the global class_name cache may not be warm yet.
|
||||
const CAPTURE_POINT_SCRIPT = preload("res://surface/CapturePoint.gd")
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
GameState.planet_name = "Kepler's Rest"
|
||||
GameState.planet_id = "chunk:3:7"
|
||||
GameState.planet_definition_id = "rocky"
|
||||
GameState.surviving_ship_count = 3
|
||||
GameState.rocks = 128
|
||||
var surface: Node = load("res://surface/PlanetSurface.tscn").instantiate()
|
||||
surface.get_node("SurfaceController").fade_duration = 0.0
|
||||
add_child(surface)
|
||||
|
||||
# Zoomed out so all three ~220-radius capture rings fit the frame with
|
||||
# margin, regardless of wherever the real setup's points happen to sit.
|
||||
surface.get_node("Camera2D").zoom = Vector2(0.55, 0.55)
|
||||
|
||||
var host: Node = surface.get_node("SurfaceFeatures/PlanetSetupHost")
|
||||
var points: Array = []
|
||||
for child in host.get_children():
|
||||
if child.get_script() == CAPTURE_POINT_SCRIPT:
|
||||
points.append(child)
|
||||
|
||||
# Lay the three points out in a compact, clearly-separated triangle for
|
||||
# the screenshot, rather than the real (far-flung) authored positions.
|
||||
# Point 0 sits at the origin - exactly where the fleet spawns - so the
|
||||
# screenshot also proves the #50 z_index fix: the ring must not occlude
|
||||
# the ships parked on top of it.
|
||||
var layout: Array = [Vector2(0, 0), Vector2(-300, 150), Vector2(300, -200)]
|
||||
for i in range(mini(points.size(), layout.size())):
|
||||
points[i].position = layout[i]
|
||||
|
||||
# Show all three states at once: captured, mid-fill, and blocked. Each
|
||||
# point's own _process() would otherwise immediately recompute state
|
||||
# from the real (empty, this far from any ship) fleet/enemy proximity
|
||||
# and overwrite these on the very next frame, so freeze it after setting
|
||||
# the display state - same technique as freezing enemy.speed for the
|
||||
# gas bomber screenshot.
|
||||
if points.size() >= 1:
|
||||
points[0].mark_captured()
|
||||
points[0].set_process(false)
|
||||
GameState.mark_point_captured(GameState.planet_id, points[0].point_index)
|
||||
if points.size() >= 2:
|
||||
points[1].state = CAPTURE_POINT_SCRIPT.State.CAPTURING
|
||||
points[1].progress = 0.55
|
||||
points[1].queue_redraw()
|
||||
points[1].set_process(false)
|
||||
if points.size() >= 3:
|
||||
points[2].state = CAPTURE_POINT_SCRIPT.State.BLOCKED
|
||||
points[2].progress = 0.3
|
||||
points[2].queue_redraw()
|
||||
points[2].set_process(false)
|
||||
|
|
@ -1 +0,0 @@
|
|||
uid://njieetp3l0xc
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://tests/_capture_points_preview.gd" id="1"]
|
||||
|
||||
[node name="CapturePointsPreview" type="Node2D"]
|
||||
script = ExtResource("1")
|
||||
|
|
@ -5,11 +5,11 @@ func _ready() -> void:
|
|||
var editor: Node = load("res://surface/PlanetSetupEditor.tscn").instantiate()
|
||||
add_child(editor)
|
||||
editor.new_setup("preview_setup", "Preview Setup", "rocky")
|
||||
editor.add_capture_point(Vector2(300, 150))
|
||||
editor.add_capture_point(Vector2(-250, 300))
|
||||
editor.add_enemy("chaser", Vector2(300, 150))
|
||||
editor.add_enemy("chaser", Vector2(-250, 300))
|
||||
editor.add_enemy("chaser", Vector2(200, -300))
|
||||
editor.add_enemy("sniper", Vector2(-300, -150))
|
||||
editor.add_enemy("artillery", Vector2(0, 350))
|
||||
editor.selected_item = {"kind": "capture", "index": 0}
|
||||
editor.selected_item = {"kind": "enemy", "index": 0}
|
||||
editor._rebuild_markers()
|
||||
editor._refresh_selected_label()
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ extends SceneTree
|
|||
# Resolved via load() inside _initialize(), NOT a top-level `const ...
|
||||
# preload(...)` - a top-level preload in a script-mode ENTRY point (-s
|
||||
# this_file.gd) evaluates during this file's own early compilation, which
|
||||
# can happen before autoloads are registered. See test_capture_points.gd
|
||||
# can happen before autoloads are registered. See test_planet_garrison.gd
|
||||
# for the full explanation.
|
||||
var ARTILLERY_SHELL_SCRIPT: Script
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ extends SceneTree
|
|||
# C. landing on a black hole spawns no resource provider and no capture
|
||||
# points at all (void's features_scene is null, and it has no
|
||||
# authored PlanetSetup) - yet the planet is immediately conquered
|
||||
# (register_capture_points treats zero required points as vacuous),
|
||||
# (a garrison of zero is vacuously already beaten),
|
||||
# so an extractor is buildable there right away
|
||||
# D. an extractor on a black hole mines dark matter locally exactly like
|
||||
# any other extractor, and a hub transporter banks it through the
|
||||
|
|
@ -132,7 +132,7 @@ func _process(delta: float) -> bool:
|
|||
# resolved at compile time, before autoloads are registered. Same
|
||||
# hazard the header of test_planet_setups.gd documents.
|
||||
if not game_state.is_planet_conquered(game_state.planet_id) \
|
||||
and int(game_state.total_capture_points(game_state.planet_id)) == 0:
|
||||
and int(game_state.garrison_total(game_state.planet_id)) == 0:
|
||||
return _fail("a black hole with no capture points required is not marked conquered")
|
||||
print("no resource provider; %d authored feature host(s)" % feature_host.get_child_count())
|
||||
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ func _process(delta: float) -> bool:
|
|||
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 \
|
||||
if state.garrison_total(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:
|
||||
|
|
|
|||
|
|
@ -1,214 +0,0 @@
|
|||
extends SceneTree
|
||||
|
||||
# Headless gameplay test: run with
|
||||
# godot --headless -s res://tests/test_capture_points.gd
|
||||
# Verifies:
|
||||
# 1. PlanetSetupHost restores an already-captured point immediately on
|
||||
# (re-)composition, instead of restarting its fill
|
||||
# 2. a ship parked in range with no enemies nearby fills a point to
|
||||
# CAPTURED in about capture_time seconds, and GameState reflects it
|
||||
# (captured_point_count, is_planet_conquered - which is what
|
||||
# BuildController's extractor gate reads, see test_planet_conquest_gate)
|
||||
# 3. an enemy within enemy_clear_radius blocks further progress
|
||||
# (state == BLOCKED) without erasing what was already filled
|
||||
# 4. moving out of range drains progress gradually, not instantly, and
|
||||
# never below what a completed point already holds
|
||||
|
||||
# Resolved via load() inside _initialize(), NOT a top-level `const ...
|
||||
# preload(...)`. A top-level preload in a script-mode ENTRY point (-s
|
||||
# this_file.gd) evaluates during this file's own early compilation, which
|
||||
# can happen before autoloads are registered - and CapturePoint.gd
|
||||
# references GameState in a function body, which GDScript resolves at
|
||||
# compile time. preload()ing it from inside _initialize() defers that
|
||||
# until after the engine has finished booting, same as every plain load()
|
||||
# call elsewhere in this file.
|
||||
var CAPTURE_POINT_SCRIPT: Script
|
||||
var PLANET_SETUP_SCRIPT: Script
|
||||
|
||||
var game_state: Node
|
||||
var main: Node
|
||||
var fleet: Node
|
||||
var point: Node
|
||||
var enemy: Node
|
||||
var planet_id: String = "test_capture_planet"
|
||||
var stage: int = 0
|
||||
var elapsed: float = 0.0
|
||||
var progress_before_block: float = 0.0
|
||||
var progress_before_drain: float = 0.0
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
CAPTURE_POINT_SCRIPT = load("res://surface/CapturePoint.gd")
|
||||
PLANET_SETUP_SCRIPT = load("res://surface/PlanetSetup.gd")
|
||||
game_state = root.get_node("GameState")
|
||||
game_state.planet_capture.clear()
|
||||
|
||||
# --- 1. restoration: an already-captured point stays captured -----
|
||||
var restore_planet_id: String = "test_capture_restore_planet"
|
||||
var fake_setup: Resource = PLANET_SETUP_SCRIPT.new()
|
||||
fake_setup.setup_id = "test_restore"
|
||||
fake_setup.surface_profile_id = "rocky"
|
||||
fake_setup.capture_points = PackedVector2Array([Vector2(0, 0), Vector2(500, 0)])
|
||||
|
||||
game_state.planet_id = restore_planet_id
|
||||
var host1: Node = load("res://surface/PlanetSetupHost.tscn").instantiate()
|
||||
host1.apply_setup(fake_setup)
|
||||
game_state.mark_point_captured(restore_planet_id, 0) # simulates a completed prior visit
|
||||
|
||||
var host2: Node = load("res://surface/PlanetSetupHost.tscn").instantiate()
|
||||
host2.apply_setup(fake_setup)
|
||||
var restored: Node = null
|
||||
var fresh: Node = null
|
||||
for child in host2.get_children():
|
||||
if child.get_script() == CAPTURE_POINT_SCRIPT and child.point_index == 0:
|
||||
restored = child
|
||||
elif child.get_script() == CAPTURE_POINT_SCRIPT and child.point_index == 1:
|
||||
fresh = child
|
||||
if restored == null or restored.state != CAPTURE_POINT_SCRIPT.State.CAPTURED or restored.progress < 0.999:
|
||||
return _fail("re-landing did not restore an already-captured point")
|
||||
if fresh == null or fresh.state == CAPTURE_POINT_SCRIPT.State.CAPTURED:
|
||||
return _fail("an uncaptured point came back already CAPTURED")
|
||||
|
||||
# --- 2-4. capture mechanics, in a live scene with a real fleet -----
|
||||
game_state.planet_capture.clear()
|
||||
game_state.planet_id = planet_id
|
||||
game_state.register_capture_points(planet_id, 1)
|
||||
game_state.buildings.clear()
|
||||
game_state.extractors.clear()
|
||||
game_state.rocks = 200
|
||||
|
||||
main = load("res://world/Main.tscn").instantiate()
|
||||
root.add_child(main)
|
||||
fleet = main.get_node("Fleet")
|
||||
|
||||
|
||||
func _process(delta: float) -> bool:
|
||||
elapsed += delta
|
||||
match stage:
|
||||
0:
|
||||
if elapsed < 0.1:
|
||||
return false
|
||||
main.get_node("EnemySpawner").set_process(false)
|
||||
main.get_node("AsteroidField").set_process(false)
|
||||
for a in get_nodes_in_group("asteroids"):
|
||||
a.queue_free()
|
||||
|
||||
point = load("res://surface/CapturePoint.tscn").instantiate()
|
||||
point.point_index = 0
|
||||
if point.capture_time > 4.0:
|
||||
return _fail("default capture still takes %.1fs" % point.capture_time)
|
||||
point.capture_time = 1.0
|
||||
point.capture_radius = 200.0
|
||||
point.enemy_clear_radius = 300.0
|
||||
point.drain_rate = 0.5
|
||||
point.position = Vector2(4000, 0) # well clear of spawn-time activity
|
||||
main.add_child(point)
|
||||
|
||||
var far_away: Vector2 = point.position + Vector2(2500, 0)
|
||||
for ship in fleet.ships:
|
||||
ship.position = far_away
|
||||
fleet.move_to(far_away)
|
||||
elapsed = 0.0
|
||||
stage = 1
|
||||
return false
|
||||
1:
|
||||
# Out of range: no progress at all.
|
||||
if elapsed < 0.3:
|
||||
return false
|
||||
if point.progress > 0.001 or point.state != CAPTURE_POINT_SCRIPT.State.NEUTRAL:
|
||||
return _fail("progress rose while no ship was in capture_radius")
|
||||
|
||||
for ship in fleet.ships:
|
||||
ship.position = point.position
|
||||
fleet.move_to(point.position)
|
||||
elapsed = 0.0
|
||||
stage = 2
|
||||
return false
|
||||
2:
|
||||
# Partway through capture_time: rising, not yet complete.
|
||||
if elapsed < point.capture_time * 0.4:
|
||||
return false
|
||||
if point.state != CAPTURE_POINT_SCRIPT.State.CAPTURING or point.progress <= 0.05:
|
||||
return _fail("point did not start filling with a ship in range (state=%d progress=%.2f)" % [
|
||||
point.state, point.progress
|
||||
])
|
||||
|
||||
# An enemy shows up within enemy_clear_radius: must block further
|
||||
# fill without erasing what's already there. A bare "enemies"
|
||||
# group member (not a real Enemy.tscn) is deliberate: CapturePoint
|
||||
# only cares that something is in the group and in range, and
|
||||
# this avoids a real chaser/weapon exchange destroying the fleet
|
||||
# ships this test also needs alive in later stages.
|
||||
enemy = Node2D.new()
|
||||
enemy.add_to_group("enemies")
|
||||
enemy.position = point.position + Vector2(50, 0)
|
||||
main.add_child(enemy)
|
||||
progress_before_block = point.progress
|
||||
elapsed = 0.0
|
||||
stage = 3
|
||||
return false
|
||||
3:
|
||||
if elapsed < 0.5:
|
||||
return false
|
||||
if point.state != CAPTURE_POINT_SCRIPT.State.BLOCKED:
|
||||
return _fail("point did not report BLOCKED with an enemy nearby (state=%d)" % point.state)
|
||||
if point.progress > progress_before_block + 0.01:
|
||||
return _fail("progress rose while an enemy was inside enemy_clear_radius")
|
||||
|
||||
# Enemy leaves and the ship backs off out of range: progress
|
||||
# should drain, but gradually - not snap to zero.
|
||||
enemy.queue_free()
|
||||
var far_away: Vector2 = point.position + Vector2(2500, 0)
|
||||
for ship in fleet.ships:
|
||||
ship.position = far_away
|
||||
fleet.move_to(far_away)
|
||||
progress_before_drain = point.progress
|
||||
elapsed = 0.0
|
||||
stage = 4
|
||||
return false
|
||||
4:
|
||||
if elapsed < 0.3:
|
||||
return false
|
||||
if point.progress >= progress_before_drain - 0.001:
|
||||
return _fail("progress did not drain while out of range")
|
||||
if point.progress <= 0.001:
|
||||
return _fail("progress dropped to zero instantly instead of draining gradually")
|
||||
if point.state != CAPTURE_POINT_SCRIPT.State.NEUTRAL:
|
||||
return _fail("point state did not return to NEUTRAL while draining out of range")
|
||||
|
||||
# Move back in and let it finish this time.
|
||||
for ship in fleet.ships:
|
||||
ship.position = point.position
|
||||
fleet.move_to(point.position)
|
||||
elapsed = 0.0
|
||||
stage = 5
|
||||
return false
|
||||
5:
|
||||
# Draining first means completion now takes longer than a bare
|
||||
# capture_time - give it a generous margin.
|
||||
if elapsed < point.capture_time * 2.0 + 0.5:
|
||||
return false
|
||||
if point.state != CAPTURE_POINT_SCRIPT.State.CAPTURED:
|
||||
return _fail("point never reached CAPTURED (state=%d progress=%.2f after %.2fs)" % [
|
||||
point.state, point.progress, elapsed
|
||||
])
|
||||
if game_state.captured_point_count(planet_id) != 1:
|
||||
return _fail("GameState.captured_point_count did not record the capture")
|
||||
# is_planet_conquered is exactly what BuildController's extractor
|
||||
# gate reads (see test_planet_conquest_gate.gd) - not re-tested
|
||||
# through the build UI here to avoid duplicating that coverage.
|
||||
if not game_state.is_planet_conquered(planet_id):
|
||||
return _fail("single-point planet was not marked conquered")
|
||||
if point.z_index >= 0:
|
||||
return _fail("capture point does not draw behind the fleet/enemies (z_index=%d)" % point.z_index)
|
||||
|
||||
print("TEST_CAPTURE_POINTS: PASS")
|
||||
quit(0)
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _fail(message: String) -> bool:
|
||||
print("TEST_CAPTURE_POINTS: FAIL (%s)" % message)
|
||||
quit(1)
|
||||
return true
|
||||
|
|
@ -1 +0,0 @@
|
|||
uid://cmordwxwbr5ca
|
||||
|
|
@ -3,20 +3,22 @@ extends SceneTree
|
|||
# Headless gameplay test: run with
|
||||
# godot --headless -s res://tests/test_conquered_planet_clear.gd
|
||||
# A planet's garrison was destroyed to take it, so it must not be standing
|
||||
# there again the next time the player lands. Capture points already came
|
||||
# back marked captured; the enemies came back alive, which read as the
|
||||
# conquest having quietly been undone.
|
||||
# there again the next time the player lands.
|
||||
#
|
||||
# #211 made the garrison the whole of conquest: killing the last defender
|
||||
# takes the planet, and there are no capture points beside them any more.
|
||||
#
|
||||
# Covers:
|
||||
# A. an unconquered planet lays out its authored enemies, as before
|
||||
# B. once every capture point is taken, re-landing places no enemies - and
|
||||
# still places the capture points, marked captured
|
||||
# C. a setup that authors enemies but NO capture points keeps them. Such a
|
||||
# planet counts as vacuously conquered (nothing to capture), so keying
|
||||
# only off the conquered flag would delete a garrison nobody has beaten
|
||||
# B. killing the last of them conquers it, and re-landing places none
|
||||
# C. a setup with NO enemies counts as vacuously conquered - there is
|
||||
# nothing there to beat
|
||||
# D. a half-cleared planet the player left comes back whole, and its count
|
||||
# with it, rather than falling to a single kill on the next visit
|
||||
|
||||
const PLANET_KEY: String = "test_conquered_planet_clear"
|
||||
const OPEN_PLANET_KEY: String = "test_conquered_planet_clear_open"
|
||||
const LEFT_PLANET_KEY: String = "test_conquered_planet_clear_left"
|
||||
|
||||
var game_state: Node
|
||||
var host_scene: PackedScene
|
||||
|
|
@ -41,42 +43,54 @@ func _process(delta: float) -> bool:
|
|||
# --- A. unconquered: the garrison is there ---------------------------
|
||||
game_state.planet_id = PLANET_KEY
|
||||
game_state.planet_definition_id = "rocky"
|
||||
var setup: Resource = _make_setup(2, 3)
|
||||
var setup: Resource = _make_setup(3)
|
||||
var enemies: int = _apply_and_count_enemies(setup)
|
||||
if enemies != 3:
|
||||
return _fail("an unconquered planet placed %d of its 3 authored enemies" % enemies)
|
||||
if game_state.is_planet_conquered(PLANET_KEY):
|
||||
return _fail("a planet with 2 uncaptured points reported itself conquered")
|
||||
return _fail("a planet with 3 defenders standing reported itself conquered")
|
||||
print("unconquered planet: 3 enemies placed")
|
||||
|
||||
# --- B. conquered: the garrison stays destroyed ----------------------
|
||||
game_state.mark_point_captured(PLANET_KEY, 0)
|
||||
game_state.mark_point_captured(PLANET_KEY, 1)
|
||||
# --- B. the last kill takes it, and it stays taken -------------------
|
||||
game_state.mark_garrison_enemy_destroyed(PLANET_KEY)
|
||||
game_state.mark_garrison_enemy_destroyed(PLANET_KEY)
|
||||
if game_state.is_planet_conquered(PLANET_KEY):
|
||||
return _fail("the planet fell with a defender still standing")
|
||||
game_state.mark_garrison_enemy_destroyed(PLANET_KEY)
|
||||
if not game_state.is_planet_conquered(PLANET_KEY):
|
||||
return _fail("capturing every point did not conquer the planet")
|
||||
return _fail("killing the whole garrison did not conquer the planet")
|
||||
|
||||
var host: Node = host_scene.instantiate()
|
||||
root.add_child(host)
|
||||
host.apply_setup(setup)
|
||||
var remaining: int = _count_enemies(host)
|
||||
var points: int = _count_capture_points(host)
|
||||
host.free()
|
||||
var remaining: int = _apply_and_count_enemies(setup)
|
||||
if remaining != 0:
|
||||
return _fail("re-landing on a conquered planet placed %d enemies" % remaining)
|
||||
if points != 2:
|
||||
return _fail("a conquered planet placed %d of its 2 capture points - conquest should clear the garrison, not the map" % points)
|
||||
print("conquered planet: 0 enemies, both capture points still placed")
|
||||
print("conquered planet: last kill took it, re-landing places nobody")
|
||||
|
||||
# --- C. a setup with nothing to capture keeps its enemies ------------
|
||||
# --- C. nothing to fight is nothing to take --------------------------
|
||||
game_state.planet_id = OPEN_PLANET_KEY
|
||||
var open_setup: Resource = _make_setup(0, 2)
|
||||
var open_setup: Resource = _make_setup(0)
|
||||
var open_enemies: int = _apply_and_count_enemies(open_setup)
|
||||
if not game_state.is_planet_conquered(OPEN_PLANET_KEY):
|
||||
return _fail("a setup with no capture points was expected to register as vacuously conquered, and did not - so this case no longer tests what it claims to")
|
||||
if open_enemies != 2:
|
||||
return _fail("a setup with no capture points lost %d of its 2 enemies to the conquest rule" \
|
||||
% (2 - open_enemies))
|
||||
print("setup with no capture points: both enemies kept, despite counting as conquered")
|
||||
return _fail("a setup with no enemies was expected to be vacuously conquered")
|
||||
if open_enemies != 0:
|
||||
return _fail("a setup authoring no enemies placed %d" % open_enemies)
|
||||
print("setup with no garrison: vacuously conquered, nobody placed")
|
||||
|
||||
# --- D. leaving mid-fight does not bank the kills --------------------
|
||||
# The garrison is re-placed in full on the next landing, so its count
|
||||
# has to start again with it - otherwise a planet left at one-from-
|
||||
# clear would fall to a single kill against three fresh defenders.
|
||||
game_state.planet_id = LEFT_PLANET_KEY
|
||||
var left_setup: Resource = _make_setup(3)
|
||||
_apply_and_count_enemies(left_setup)
|
||||
game_state.mark_garrison_enemy_destroyed(LEFT_PLANET_KEY)
|
||||
game_state.mark_garrison_enemy_destroyed(LEFT_PLANET_KEY)
|
||||
var returned: int = _apply_and_count_enemies(left_setup)
|
||||
if returned != 3:
|
||||
return _fail("returning to a half-cleared planet placed %d of 3 defenders" % returned)
|
||||
game_state.mark_garrison_enemy_destroyed(LEFT_PLANET_KEY)
|
||||
if game_state.is_planet_conquered(LEFT_PLANET_KEY):
|
||||
return _fail("one kill took a planet defended by three - the count did not reset with the garrison")
|
||||
print("returning mid-fight: the garrison and its count both start again")
|
||||
|
||||
print("TEST_CONQUERED_PLANET_CLEAR: PASS")
|
||||
quit(0)
|
||||
|
|
@ -84,14 +98,10 @@ func _process(delta: float) -> bool:
|
|||
return false
|
||||
|
||||
|
||||
func _make_setup(capture_count: int, enemy_count: int) -> Resource:
|
||||
func _make_setup(enemy_count: int) -> Resource:
|
||||
var setup: Resource = load("res://surface/PlanetSetup.gd").new()
|
||||
setup.setup_id = "in_memory"
|
||||
setup.surface_profile_id = "rocky"
|
||||
var points: PackedVector2Array = PackedVector2Array()
|
||||
for i in range(capture_count):
|
||||
points.append(Vector2(200.0 * float(i + 1), 0.0))
|
||||
setup.capture_points = points
|
||||
var spawn_script: Script = load("res://surface/PlanetSetupSpawn.gd")
|
||||
for i in range(enemy_count):
|
||||
var spawn: Resource = spawn_script.new()
|
||||
|
|
@ -121,16 +131,6 @@ func _count_enemies(host: Node) -> int:
|
|||
return count
|
||||
|
||||
|
||||
func _count_capture_points(host: Node) -> int:
|
||||
var count: int = 0
|
||||
for child in host.get_children():
|
||||
if child.is_in_group("enemies"):
|
||||
continue
|
||||
if "point_index" in child:
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
func _fail(message: String) -> bool:
|
||||
print("TEST_CONQUERED_PLANET_CLEAR: FAIL (%s)" % message)
|
||||
quit(1)
|
||||
|
|
|
|||
|
|
@ -51,8 +51,8 @@ func _initialize() -> void:
|
|||
# This test is about the planet-proximity/duplicate rules, not conquest
|
||||
# (see test_planet_conquest_gate), so pre-conquer the target planet.
|
||||
var target_planet_id: String = "chunk:%d:%d" % [target_chunk.x, target_chunk.y]
|
||||
game_state.register_capture_points(target_planet_id, 1)
|
||||
game_state.mark_point_captured(target_planet_id, 0)
|
||||
game_state.register_planet_garrison(target_planet_id, 1)
|
||||
game_state.mark_garrison_enemy_destroyed(target_planet_id)
|
||||
# Extractors are gated behind research (see test_skill_tree). This test
|
||||
# is about the planet-proximity rule, so grant the research up front.
|
||||
game_state.reset_progression()
|
||||
|
|
|
|||
|
|
@ -408,6 +408,27 @@ func _process(delta: float) -> bool:
|
|||
print("the vortex took %d vessels and left one interceptor" % (convoy_size - 1))
|
||||
if not director.whiteout_teleported:
|
||||
return _fail("the screen reached white without teleporting the fleet")
|
||||
# #209: the prologue's charted space belongs to a place 16000
|
||||
# units away. Carried over, it also completed "chart 30 sectors"
|
||||
# before the player had flown anywhere.
|
||||
#
|
||||
# Not "empty": the tracker charts the neighbourhood around the
|
||||
# fleet the moment it arrives, so what is asserted is that
|
||||
# nothing REMOTE survived - every chunk still on the map is one
|
||||
# the survivors can see from where they now are.
|
||||
# Measured from where the fleet actually IS: the tracker charts a
|
||||
# neighbourhood around the formation's centroid, which sits a
|
||||
# little off the spawn point, and the survivor drifts after
|
||||
# arriving. Two sectors of slack over the sensor's own radius
|
||||
# covers both without admitting anything from the convoy's route,
|
||||
# which is thousands of sectors away.
|
||||
var here: Vector2i = WorldGen.chunk_coords(fleet.get_centroid())
|
||||
var reach: int = 1 + 2
|
||||
for chunk in game_state.explored_chunks:
|
||||
var away: int = maxi(absi(chunk.x - here.x), absi(chunk.y - here.y))
|
||||
if away > reach:
|
||||
return _fail("chunk %s, %d sectors from the survivors, survived the breach" \
|
||||
% [chunk, away])
|
||||
if fleet.get_formation_origin().distance_to(director.DEFAULT_SPAWN) > 1.0:
|
||||
return _fail("whiteout revealed the fleet at %s instead of default spawn %s" \
|
||||
% [fleet.get_formation_origin(), director.DEFAULT_SPAWN])
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@ func _process(delta: float) -> bool:
|
|||
setup.setup_id = "reward_probe"
|
||||
setup.display_name = "Reward Probe"
|
||||
setup.surface_profile_id = "rocky"
|
||||
setup.capture_points = PackedVector2Array([Vector2(10, 10), Vector2(-10, -10)])
|
||||
setup.research_point_reward = 5
|
||||
var path: String = "user://test_reward_probe.tres"
|
||||
if ResourceSaver.save(setup, path) != OK:
|
||||
|
|
@ -55,9 +54,9 @@ func _process(delta: float) -> bool:
|
|||
# A research_lab planet's own definition pays a flat 1.
|
||||
var flat_rate: int = int(load("res://planets/ResearchLabPlanetDefinition.tres").conquest_research_points)
|
||||
game_state.research_points = 0
|
||||
game_state.register_capture_points("hard_lab", 2, "research_lab", 6)
|
||||
game_state.mark_point_captured("hard_lab", 0)
|
||||
game_state.mark_point_captured("hard_lab", 1)
|
||||
game_state.register_planet_garrison("hard_lab", 2, "research_lab", 6)
|
||||
game_state.mark_garrison_enemy_destroyed("hard_lab")
|
||||
game_state.mark_garrison_enemy_destroyed("hard_lab")
|
||||
if game_state.research_points != 6:
|
||||
return _fail("a hard lab authored at 6 points awarded %d" % game_state.research_points)
|
||||
if flat_rate >= 6:
|
||||
|
|
@ -66,8 +65,8 @@ func _process(delta: float) -> bool:
|
|||
|
||||
# --- C. silence still means the type's rate ------------------
|
||||
game_state.research_points = 0
|
||||
game_state.register_capture_points("plain_lab", 1, "research_lab", 0)
|
||||
game_state.mark_point_captured("plain_lab", 0)
|
||||
game_state.register_planet_garrison("plain_lab", 1, "research_lab", 0)
|
||||
game_state.mark_garrison_enemy_destroyed("plain_lab")
|
||||
if game_state.research_points != flat_rate:
|
||||
return _fail("a setup with no authored reward paid %d, expected the type's %d" \
|
||||
% [game_state.research_points, flat_rate])
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@ extends SceneTree
|
|||
# Headless gameplay test: run with
|
||||
# godot --headless -s res://tests/test_planet_conquest_gate.gd
|
||||
# Verifies:
|
||||
# 1. GameState starts every planet unconquered, and register_capture_points
|
||||
# is idempotent (a second call with the same total doesn't wipe progress)
|
||||
# 2. mark_point_captured only conquers once every point is captured, and
|
||||
# the planet_conquered signal fires exactly once
|
||||
# 1. GameState starts every planet unconquered, and re-registering a
|
||||
# garrison on a conquered planet cannot un-conquer it
|
||||
# 2. a planet falls only when its LAST defender does, and the
|
||||
# planet_conquered signal fires exactly once, carrying what it paid
|
||||
# 3. over an unconquered planet, the extractor build option is disabled,
|
||||
# the info popup explains why, and confirming spends nothing and builds
|
||||
# nothing
|
||||
|
|
@ -22,6 +22,9 @@ var target_chunk: Vector2i
|
|||
var stage: int = 0
|
||||
var elapsed: float = 0.0
|
||||
var conquered_signal_count: int = 0
|
||||
# #212 needs the award to travel WITH the signal, so whatever announces
|
||||
# conquest reports a number that was actually paid.
|
||||
var conquered_signal_points: int = -1
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
|
|
@ -57,22 +60,47 @@ func _initialize() -> void:
|
|||
# Unit-level checks on GameState itself, before any scene involvement.
|
||||
if game_state.is_planet_conquered(target_planet_id):
|
||||
return _fail("a fresh planet reported conquered")
|
||||
game_state.register_capture_points(target_planet_id, 3)
|
||||
game_state.mark_point_captured(target_planet_id, 0)
|
||||
game_state.register_capture_points(target_planet_id, 3) # idempotent re-registration
|
||||
if game_state.captured_point_count(target_planet_id) != 1:
|
||||
return _fail("re-registering wiped already-captured progress")
|
||||
game_state.planet_conquered.connect(func(_id): conquered_signal_count += 1)
|
||||
game_state.mark_point_captured(target_planet_id, 1)
|
||||
game_state.register_planet_garrison(target_planet_id, 3)
|
||||
if game_state.garrison_remaining(target_planet_id) != 3:
|
||||
return _fail("a garrison of 3 registered as %d" \
|
||||
% game_state.garrison_remaining(target_planet_id))
|
||||
game_state.planet_conquered.connect(func(_id, points):
|
||||
conquered_signal_count += 1
|
||||
conquered_signal_points = points)
|
||||
game_state.mark_garrison_enemy_destroyed(target_planet_id)
|
||||
game_state.mark_garrison_enemy_destroyed(target_planet_id)
|
||||
if game_state.is_planet_conquered(target_planet_id):
|
||||
return _fail("conquered with points still missing")
|
||||
game_state.mark_point_captured(target_planet_id, 2)
|
||||
return _fail("conquered with a defender still standing")
|
||||
game_state.mark_garrison_enemy_destroyed(target_planet_id)
|
||||
if not game_state.is_planet_conquered(target_planet_id):
|
||||
return _fail("all points captured but planet not marked conquered")
|
||||
# Marking an already-captured point again must not double-fire the signal.
|
||||
game_state.mark_point_captured(target_planet_id, 2)
|
||||
return _fail("the whole garrison is down but the planet is not conquered")
|
||||
# A stray kill reported after the planet has fallen must not fire again.
|
||||
game_state.mark_garrison_enemy_destroyed(target_planet_id)
|
||||
# Nor may re-landing hand the planet back: registration is refused
|
||||
# outright once it is taken.
|
||||
game_state.register_planet_garrison(target_planet_id, 3)
|
||||
if not game_state.is_planet_conquered(target_planet_id):
|
||||
return _fail("re-registering a garrison un-conquered a planet already taken")
|
||||
if conquered_signal_count != 1:
|
||||
return _fail("planet_conquered fired %d times, expected exactly 1" % conquered_signal_count)
|
||||
# This planet was registered with no definition and no override, so the
|
||||
# conquest paid nothing - and the signal has to say so rather than leave
|
||||
# the announcement to guess.
|
||||
if conquered_signal_points != 0:
|
||||
return _fail("an unrewarded conquest reported %d research points" \
|
||||
% conquered_signal_points)
|
||||
|
||||
# A rewarded one reports what it paid.
|
||||
var paid_planet_id: String = target_planet_id + ":paid"
|
||||
var points_before: int = game_state.research_points
|
||||
game_state.register_planet_garrison(paid_planet_id, 1, "", 4)
|
||||
conquered_signal_points = -1
|
||||
game_state.mark_garrison_enemy_destroyed(paid_planet_id)
|
||||
if conquered_signal_points != 4:
|
||||
return _fail("a 4-point conquest reported %d" % conquered_signal_points)
|
||||
if game_state.research_points != points_before + 4:
|
||||
return _fail("the reported points were not the ones actually banked")
|
||||
print("conquest reports the research points it paid")
|
||||
|
||||
# Reset conquest for the scene-level gate check below - this run tests
|
||||
# the unconquered path first, then conquers it.
|
||||
|
|
@ -134,9 +162,9 @@ func _process(delta: float) -> bool:
|
|||
popup.visible = false
|
||||
|
||||
# Now conquer it and confirm the same flow succeeds.
|
||||
game_state.register_capture_points(planet.planet_id, 2)
|
||||
game_state.mark_point_captured(planet.planet_id, 0)
|
||||
game_state.mark_point_captured(planet.planet_id, 1)
|
||||
game_state.register_planet_garrison(planet.planet_id, 2)
|
||||
game_state.mark_garrison_enemy_destroyed(planet.planet_id)
|
||||
game_state.mark_garrison_enemy_destroyed(planet.planet_id)
|
||||
|
||||
controller._on_build_pressed()
|
||||
if not button.enabled:
|
||||
|
|
|
|||
143
tests/test_planet_garrison.gd
Normal file
143
tests/test_planet_garrison.gd
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
extends SceneTree
|
||||
|
||||
# Headless gameplay test: run with
|
||||
# godot --headless -s res://tests/test_planet_garrison.gd
|
||||
# Forgejo #211: taking a planet is killing what defends it. The capture
|
||||
# points this replaced - stand in a ring, wait out a timer, once per ring -
|
||||
# are gone, so what has to be proven is that the FIGHT reaches the conquest
|
||||
# state, through the enemies' own deaths rather than through a bookkeeping
|
||||
# call a test makes on their behalf.
|
||||
#
|
||||
# test_conquered_planet_clear.gd covers the counting rules around landing and
|
||||
# re-landing. This one covers the wiring: a real Enemy, destroyed the way
|
||||
# anything destroys one, moves the planet toward being held.
|
||||
#
|
||||
# Covers:
|
||||
# A. an authored garrison registers itself when the surface is composed
|
||||
# B. killing one defender is progress, not conquest
|
||||
# C. killing the last one conquers the planet, exactly once, and the
|
||||
# research award rides along with the signal
|
||||
# D. an enemy that dies after the planet has already fallen changes nothing
|
||||
|
||||
# Resolved via load() inside _initialize(), NOT a top-level `const ...
|
||||
# preload(...)`. A top-level preload in a script-mode ENTRY point (-s
|
||||
# this_file.gd) evaluates during this file's own early compilation, which can
|
||||
# happen before autoloads are registered - and these scripts reference
|
||||
# GameState in function bodies, which GDScript resolves at compile time.
|
||||
var PLANET_SETUP_SCRIPT: Script
|
||||
var SPAWN_SCRIPT: Script
|
||||
|
||||
var game_state: Node
|
||||
var host: Node
|
||||
var planet_id: String = "test_garrison_planet"
|
||||
var conquered_count: int = 0
|
||||
var conquered_points: int = -1
|
||||
var stage: int = 0
|
||||
var elapsed: float = 0.0
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
PLANET_SETUP_SCRIPT = load("res://surface/PlanetSetup.gd")
|
||||
SPAWN_SCRIPT = load("res://surface/PlanetSetupSpawn.gd")
|
||||
game_state = root.get_node("GameState")
|
||||
game_state.planet_capture.clear()
|
||||
game_state.planet_id = planet_id
|
||||
game_state.planet_definition_id = "rocky"
|
||||
game_state.planet_conquered.connect(func(_id, points):
|
||||
conquered_count += 1
|
||||
conquered_points = points)
|
||||
|
||||
var setup: Resource = PLANET_SETUP_SCRIPT.new()
|
||||
setup.setup_id = "test_garrison"
|
||||
setup.surface_profile_id = "rocky"
|
||||
# Worth something, so the award travelling with the signal is checked on a
|
||||
# planet that actually pays.
|
||||
setup.research_point_reward = 3
|
||||
for i in range(3):
|
||||
var spawn: Resource = SPAWN_SCRIPT.new()
|
||||
spawn.enemy_id = "chaser"
|
||||
spawn.position = Vector2(0.0, 300.0 * float(i + 1))
|
||||
setup.enemy_spawns.append(spawn)
|
||||
|
||||
host = load("res://surface/PlanetSetupHost.tscn").instantiate()
|
||||
root.add_child(host)
|
||||
host.apply_setup(setup)
|
||||
|
||||
|
||||
func _defenders() -> Array:
|
||||
var out: Array = []
|
||||
for child in host.get_children():
|
||||
if is_instance_valid(child) and child.is_in_group("enemies") \
|
||||
and not child.is_queued_for_deletion():
|
||||
out.append(child)
|
||||
return out
|
||||
|
||||
|
||||
func _process(delta: float) -> bool:
|
||||
elapsed += delta
|
||||
if elapsed < 0.2:
|
||||
return false
|
||||
match stage:
|
||||
0:
|
||||
# --- A. the garrison registered itself -----------------------
|
||||
var defenders: Array = _defenders()
|
||||
if defenders.size() != 3:
|
||||
return _fail("the surface placed %d defenders for 3 authored" % defenders.size())
|
||||
if game_state.garrison_total(planet_id) != 3 \
|
||||
or game_state.garrison_remaining(planet_id) != 3:
|
||||
return _fail("a 3-strong garrison registered as %d of %d" % [
|
||||
game_state.garrison_remaining(planet_id),
|
||||
game_state.garrison_total(planet_id)])
|
||||
print("3 defenders placed, and the planet knows it")
|
||||
|
||||
# --- B. one down is not conquest -----------------------------
|
||||
var points_before: int = game_state.research_points
|
||||
defenders[0].take_damage(defenders[0].hp)
|
||||
if game_state.garrison_remaining(planet_id) != 2:
|
||||
return _fail("one kill left %d of the garrison standing, expected 2" \
|
||||
% game_state.garrison_remaining(planet_id))
|
||||
if game_state.is_planet_conquered(planet_id):
|
||||
return _fail("the planet fell to the first kill")
|
||||
if game_state.research_points != points_before:
|
||||
return _fail("an unfinished conquest paid out")
|
||||
print("one defender down: 2 left, planet still theirs")
|
||||
|
||||
# --- C. the last one takes it --------------------------------
|
||||
defenders[1].take_damage(defenders[1].hp)
|
||||
defenders[2].take_damage(defenders[2].hp)
|
||||
if not game_state.is_planet_conquered(planet_id):
|
||||
return _fail("the whole garrison is dead and the planet is not ours")
|
||||
if conquered_count != 1:
|
||||
return _fail("planet_conquered fired %d times, expected once" % conquered_count)
|
||||
if conquered_points != 3:
|
||||
return _fail("the conquest reported %d research points, expected the authored 3" \
|
||||
% conquered_points)
|
||||
if game_state.research_points != points_before + 3:
|
||||
return _fail("the reported award was not the one banked")
|
||||
print("last defender down: planet taken, 3 research points paid once")
|
||||
|
||||
elapsed = 0.0
|
||||
stage = 1
|
||||
return false
|
||||
1:
|
||||
# --- D. a straggler after the fact changes nothing -----------
|
||||
# A shot already in flight, or a bomb still blooming, can kill
|
||||
# something a frame after the planet has fallen.
|
||||
var points_after: int = game_state.research_points
|
||||
game_state.mark_garrison_enemy_destroyed(planet_id)
|
||||
if conquered_count != 1:
|
||||
return _fail("a kill after conquest fired planet_conquered again")
|
||||
if game_state.research_points != points_after:
|
||||
return _fail("a kill after conquest paid the reward a second time")
|
||||
print("a late kill is absorbed: no second conquest, no second payment")
|
||||
|
||||
print("TEST_PLANET_GARRISON: PASS")
|
||||
quit(0)
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _fail(message: String) -> bool:
|
||||
print("TEST_PLANET_GARRISON: FAIL (%s)" % message)
|
||||
quit(1)
|
||||
return true
|
||||
|
|
@ -145,7 +145,6 @@ func _plant_pool() -> bool:
|
|||
setup.setup_id = pool_id
|
||||
setup.display_name = pool_id
|
||||
setup.surface_profile_id = "rocky"
|
||||
setup.capture_points = PackedVector2Array([Vector2(100.0, 100.0)])
|
||||
if ResourceSaver.save(setup, "res://PlanetSetups/%s.tres" % pool_id) != OK:
|
||||
_remove_scratch()
|
||||
_fail("could not plant the scratch map pool")
|
||||
|
|
|
|||
|
|
@ -8,18 +8,18 @@ extends SceneTree
|
|||
# 2. dropping a new .tres into res://PlanetSetups/ makes it appear via
|
||||
# PlanetSetups.rescan() with no other code change
|
||||
# 3. landing on a rocky/gas planet composes a PlanetSetupHost carrying the
|
||||
# resolved setup, with one marker per capture point
|
||||
# resolved setup, placing one enemy per authored spawn
|
||||
|
||||
const SCRATCH_SETUP_PATH: String = "res://PlanetSetups/_test_scratch_setup.tres"
|
||||
|
||||
# Resolved via load() inside _initialize(), NOT a top-level `const ...
|
||||
# preload(...)` - a top-level preload in a script-mode ENTRY point (-s
|
||||
# this_file.gd) evaluates during this file's own early compilation, which
|
||||
# can happen before autoloads are registered, and CapturePoint.gd
|
||||
# references GameState in a function body (resolved at compile time). See
|
||||
# test_capture_points.gd for the full explanation.
|
||||
# can happen before autoloads are registered, and the scripts loaded here
|
||||
# reference GameState in function bodies (resolved at compile time). See
|
||||
# test_planet_garrison.gd for the full explanation.
|
||||
var PLANET_SETUP_SCRIPT: Script
|
||||
var CAPTURE_POINT_SCRIPT: Script
|
||||
var ENEMY_SCRIPT: Script
|
||||
|
||||
var game_state: Node
|
||||
var rocky_setup: Resource
|
||||
|
|
@ -30,7 +30,7 @@ var pending_surface: Node
|
|||
|
||||
func _initialize() -> void:
|
||||
PLANET_SETUP_SCRIPT = load("res://surface/PlanetSetup.gd")
|
||||
CAPTURE_POINT_SCRIPT = load("res://surface/CapturePoint.gd")
|
||||
ENEMY_SCRIPT = load("res://enemies/Enemy.gd")
|
||||
# --- 1. determinism and profile filtering ---
|
||||
rocky_setup = PlanetSetups.setup_for_planet("chunk:3:7", "rocky")
|
||||
if rocky_setup == null:
|
||||
|
|
@ -65,7 +65,6 @@ func _initialize() -> void:
|
|||
scratch.setup_id = "_test_scratch_setup"
|
||||
scratch.display_name = "Scratch"
|
||||
scratch.surface_profile_id = "rocky"
|
||||
scratch.capture_points = PackedVector2Array([Vector2(100, 100)])
|
||||
var save_err: int = ResourceSaver.save(scratch, SCRATCH_SETUP_PATH)
|
||||
if save_err != OK:
|
||||
return _fail("could not save the scratch setup for the pickup check (err %d)" % save_err)
|
||||
|
|
@ -108,14 +107,14 @@ func _process(delta: float) -> bool:
|
|||
if host.setup != rocky_setup:
|
||||
pending_surface.free()
|
||||
return _fail("host did not receive the deterministically resolved setup")
|
||||
var point_count: int = 0
|
||||
var enemy_count: int = 0
|
||||
for child in host.get_children():
|
||||
if child.get_script() == CAPTURE_POINT_SCRIPT:
|
||||
point_count += 1
|
||||
if point_count != rocky_setup.capture_points.size():
|
||||
if child.get_script() == ENEMY_SCRIPT:
|
||||
enemy_count += 1
|
||||
if enemy_count != rocky_setup.enemy_spawns.size():
|
||||
pending_surface.free()
|
||||
return _fail("host placed %d capture points for %d authored ones" % [
|
||||
point_count, rocky_setup.capture_points.size()
|
||||
return _fail("host placed %d enemies for %d authored spawns" % [
|
||||
enemy_count, rocky_setup.enemy_spawns.size()
|
||||
])
|
||||
pending_surface.free()
|
||||
|
||||
|
|
|
|||
|
|
@ -44,8 +44,8 @@ func _initialize() -> void:
|
|||
target_planet_id = "chunk:%d:%d" % [target_chunk.x, target_chunk.y]
|
||||
# This test is about the resource-id gate, not conquest itself (see
|
||||
# test_planet_conquest_gate) - pre-conquer the target directly.
|
||||
game_state.register_capture_points(target_planet_id, 1, "research_lab")
|
||||
game_state.mark_point_captured(target_planet_id, 0)
|
||||
game_state.register_planet_garrison(target_planet_id, 1, "research_lab")
|
||||
game_state.mark_garrison_enemy_destroyed(target_planet_id)
|
||||
game_state.reset_progression()
|
||||
for node in GameState.SKILL_TREE:
|
||||
game_state.unlocked.append(node.id)
|
||||
|
|
|
|||
|
|
@ -98,34 +98,34 @@ func _initialize() -> void:
|
|||
if int(lab_definition.conquest_research_points) != 0:
|
||||
return _fail("the research_lab planet type still grants %d point(s) of its own - the reward belongs to the authored map now" \
|
||||
% int(lab_definition.conquest_research_points))
|
||||
game_state.register_capture_points("test_lab_no_reward", 1, "research_lab")
|
||||
game_state.mark_point_captured("test_lab_no_reward", 0)
|
||||
game_state.register_planet_garrison("test_lab_no_reward", 1, "research_lab")
|
||||
game_state.mark_garrison_enemy_destroyed("test_lab_no_reward")
|
||||
if game_state.research_points != 0:
|
||||
return _fail("a lab whose map states no reward still awarded %d" % game_state.research_points)
|
||||
print("the planet type grants nothing by itself; an unpriced map awards nothing")
|
||||
|
||||
# What an authored map is worth is passed through as the override, exactly
|
||||
# as PlanetSetupHost does from the setup's research_point_reward.
|
||||
game_state.register_capture_points("test_lab_planet", 2, "research_lab", 1)
|
||||
game_state.register_planet_garrison("test_lab_planet", 2, "research_lab", 1)
|
||||
if game_state.is_planet_conquered("test_lab_planet"):
|
||||
return _fail("a lab with 2 real capture points started conquered")
|
||||
game_state.mark_point_captured("test_lab_planet", 0)
|
||||
game_state.mark_garrison_enemy_destroyed("test_lab_planet")
|
||||
if game_state.research_points != 0:
|
||||
return _fail("a research point was awarded before every point was captured")
|
||||
game_state.mark_point_captured("test_lab_planet", 1)
|
||||
game_state.mark_garrison_enemy_destroyed("test_lab_planet")
|
||||
if game_state.research_points != 1:
|
||||
return _fail("conquering a research_lab did not award the 1 point its map is worth: %d" % game_state.research_points)
|
||||
if totals_seen != [1]:
|
||||
return _fail("research_points_changed did not fire exactly once with the new total: %s" % [totals_seen])
|
||||
print("conquering a research_lab awarded exactly what its map is worth, once")
|
||||
|
||||
game_state.mark_point_captured("test_lab_planet", 0) # already captured - redundant re-mark
|
||||
game_state.mark_garrison_enemy_destroyed("test_lab_planet") # already beaten - redundant kill
|
||||
if game_state.research_points != 1:
|
||||
return _fail("re-marking an already-captured point re-awarded a research point: %d" % game_state.research_points)
|
||||
print("double-award guarded against")
|
||||
|
||||
game_state.register_capture_points("test_rocky_planet", 1, "rocky")
|
||||
game_state.mark_point_captured("test_rocky_planet", 0)
|
||||
game_state.register_planet_garrison("test_rocky_planet", 1, "rocky")
|
||||
game_state.mark_garrison_enemy_destroyed("test_rocky_planet")
|
||||
if game_state.research_points != 1:
|
||||
return _fail("conquering a non-research_lab planet awarded a research point: %d" % game_state.research_points)
|
||||
print("a non-research_lab planet's conquest never awards a point")
|
||||
|
|
|
|||
|
|
@ -5,14 +5,14 @@ extends SceneTree
|
|||
# Drives PlanetSetupEditor's model directly (place/move/delete/save/reload)
|
||||
# rather than through touch input - see the ticket's own note that the
|
||||
# touch layer can stay out of the headless test. Verifies:
|
||||
# 1. placing capture points and enemies updates current_setup
|
||||
# 1. placing enemies updates current_setup
|
||||
# 2. moving and deleting update it correctly, including index shifting
|
||||
# 3. saving writes a real res://PlanetSetups/<id>.tres and the registry
|
||||
# picks it up
|
||||
# 4. reopening that file in a fresh editor instance round-trips the
|
||||
# exact points/enemies, and they can be moved/deleted again
|
||||
# 5. the palette has one entry per EnemyDefinitions entry plus the
|
||||
# capture point, generated from the registry rather than hardcoded
|
||||
# enemy kind, generated from the registry rather than hardcoded
|
||||
|
||||
const TEST_SETUP_ID: String = "test_editor_setup"
|
||||
const TEST_SETUP_PATH: String = "res://PlanetSetups/test_editor_setup.tres"
|
||||
|
|
@ -37,32 +37,29 @@ func _process(delta: float) -> bool:
|
|||
if elapsed < 0.1:
|
||||
return false
|
||||
|
||||
var expected_palette_size: int = 1 + EnemyDefinitions.all().size()
|
||||
if editor._palette_buttons.size() != expected_palette_size:
|
||||
return _fail("palette has %d entries, expected 1 + %d enemy definitions" % [
|
||||
# #211 took the capture-point tile out: the palette is exactly the
|
||||
# enemy registry now.
|
||||
if editor._palette_buttons.size() != EnemyDefinitions.all().size():
|
||||
return _fail("palette has %d entries, expected %d enemy definitions" % [
|
||||
editor._palette_buttons.size(), EnemyDefinitions.all().size()
|
||||
])
|
||||
|
||||
editor.new_setup(TEST_SETUP_ID, "Test Editor Setup", "rocky")
|
||||
editor.add_capture_point(Vector2(100, 200))
|
||||
editor.add_capture_point(Vector2(-300, 50))
|
||||
editor.add_capture_point(Vector2(0, -400))
|
||||
editor.add_enemy("chaser", Vector2(100, 200))
|
||||
editor.add_enemy("chaser", Vector2(500, 500))
|
||||
editor.add_enemy("sniper", Vector2(-500, -500))
|
||||
|
||||
if editor.current_setup.capture_points.size() != 3:
|
||||
return _fail("placing 3 capture points did not land 3")
|
||||
if editor.current_setup.enemy_spawns.size() != 3:
|
||||
return _fail("placing 3 enemies did not land 3")
|
||||
|
||||
editor.move_enemy(0, Vector2(150, 250))
|
||||
if editor.current_setup.enemy_spawns[0].position != Vector2(150, 250):
|
||||
return _fail("move_enemy did not update the position")
|
||||
|
||||
editor.remove_enemy(1) # removes the second chaser
|
||||
if editor.current_setup.enemy_spawns.size() != 2:
|
||||
return _fail("placing 2 enemies did not land 2")
|
||||
|
||||
editor.move_capture_point(0, Vector2(150, 250))
|
||||
if editor.current_setup.capture_points[0] != Vector2(150, 250):
|
||||
return _fail("move_capture_point did not update the position")
|
||||
|
||||
editor.remove_enemy(0) # removes the chaser spawn
|
||||
if editor.current_setup.enemy_spawns.size() != 1:
|
||||
return _fail("remove_enemy did not shrink enemy_spawns")
|
||||
if editor.current_setup.enemy_spawns[0].enemy_id != "sniper":
|
||||
if editor.current_setup.enemy_spawns[1].enemy_id != "sniper":
|
||||
return _fail("remove_enemy removed the wrong entry (index did not shift)")
|
||||
|
||||
var result: Dictionary = editor.save_setup()
|
||||
|
|
@ -97,20 +94,19 @@ func _process(delta: float) -> bool:
|
|||
return false
|
||||
if not editor.load_setup(TEST_SETUP_ID):
|
||||
return _fail("reopening the saved setup failed")
|
||||
var points: PackedVector2Array = editor.current_setup.capture_points
|
||||
var spawns: Array = editor.current_setup.enemy_spawns
|
||||
if points.size() != 3 or points[0] != Vector2(150, 250) \
|
||||
or points[1] != Vector2(-300, 50) or points[2] != Vector2(0, -400):
|
||||
return _fail("reopened capture points did not match what was saved: %s" % [points])
|
||||
if spawns.size() != 1 or spawns[0].enemy_id != "sniper" or spawns[0].position != Vector2(-500, -500):
|
||||
if spawns.size() != 2 or spawns[0].enemy_id != "chaser" \
|
||||
or spawns[0].position != Vector2(150, 250) \
|
||||
or spawns[1].enemy_id != "sniper" \
|
||||
or spawns[1].position != Vector2(-500, -500):
|
||||
return _fail("reopened enemy spawns did not match what was saved")
|
||||
|
||||
# The reopened items can still be moved and deleted.
|
||||
editor.move_enemy(0, Vector2(-600, -600))
|
||||
if editor.current_setup.enemy_spawns[0].position != Vector2(-600, -600):
|
||||
return _fail("could not move an item after reopening")
|
||||
editor.remove_capture_point(1)
|
||||
if editor.current_setup.capture_points.size() != 2:
|
||||
editor.remove_enemy(1)
|
||||
if editor.current_setup.enemy_spawns.size() != 1:
|
||||
return _fail("could not delete an item after reopening")
|
||||
|
||||
print("TEST_SETUP_EDITOR: PASS")
|
||||
|
|
|
|||
|
|
@ -60,11 +60,11 @@ func _process(delta: float) -> bool:
|
|||
# Two setups, so deleting one still leaves a populated list - a panel that
|
||||
# empties would hide itself and mask a wrong-row deletion.
|
||||
editor.new_setup(KEEP_ID, "Delete Keeper", "rocky")
|
||||
editor.add_capture_point(Vector2(10, 20))
|
||||
editor.add_enemy("chaser", Vector2(10, 20))
|
||||
if not editor.save_setup().ok:
|
||||
return _fail("could not save the keeper setup")
|
||||
editor.new_setup(SCRATCH_ID, "Delete Scratch", "rocky")
|
||||
editor.add_capture_point(Vector2(30, 40))
|
||||
editor.add_enemy("chaser", Vector2(30, 40))
|
||||
if not editor.save_setup().ok:
|
||||
return _fail("could not save the scratch setup")
|
||||
|
||||
|
|
@ -123,8 +123,8 @@ func _process(delta: float) -> bool:
|
|||
return _fail("deleting the open setup failed: %s" % outcome.message)
|
||||
if String(editor.current_setup.setup_id) == KEEP_ID:
|
||||
return _fail("the editor is still holding '%s' after deleting it - the next save would put the file straight back" % KEEP_ID)
|
||||
if not editor.current_setup.capture_points.is_empty():
|
||||
return _fail("the replacement setup carried the deleted one's capture points over")
|
||||
if not editor.current_setup.enemy_spawns.is_empty():
|
||||
return _fail("the replacement setup carried the deleted one's enemies over")
|
||||
print("deleting the open setup left a fresh '%s' being edited" % editor.current_setup.setup_id)
|
||||
|
||||
# Deleting something that is not there is refused, not silently "ok".
|
||||
|
|
|
|||
|
|
@ -74,7 +74,6 @@ func _process(delta: float) -> bool:
|
|||
scratch.setup_id = SCRATCH_ID
|
||||
scratch.display_name = "Identity Scratch"
|
||||
scratch.surface_profile_id = "rocky"
|
||||
scratch.capture_points = PackedVector2Array([Vector2(10.0, 20.0)])
|
||||
scratch.research_point_reward = 1
|
||||
if ResourceSaver.save(scratch, SCRATCH_FILE) != OK:
|
||||
return _fail("could not write the scratch setup")
|
||||
|
|
@ -116,7 +115,7 @@ func _process(delta: float) -> bool:
|
|||
|
||||
# --- E. a NEW setup still lands under its id --------------------------
|
||||
editor.new_setup(NEW_ID, "Identity New", "rocky")
|
||||
editor.add_capture_point(Vector2(20.0, 30.0))
|
||||
editor.add_enemy("chaser", Vector2(20.0, 30.0))
|
||||
if not editor.save_setup().ok:
|
||||
return _fail("saving a brand-new setup failed")
|
||||
if not FileAccess.file_exists(ProjectSettings.globalize_path(NEW_PATH)):
|
||||
|
|
|
|||
|
|
@ -15,8 +15,7 @@ extends SceneTree
|
|||
#
|
||||
# Covers:
|
||||
# A. the gas giant's vent field draws behind the ships (the #91 report)
|
||||
# B. the same for every other drawing feature on every profile, rocks and
|
||||
# capture rings included
|
||||
# B. the same for every other drawing feature on every profile
|
||||
# C. the fleet's own ships are where the comparison says they are, so a
|
||||
# future change that pushed the ships back instead cannot make A and B
|
||||
# pass by moving the goalposts
|
||||
|
|
@ -106,8 +105,14 @@ func _process(delta: float) -> bool:
|
|||
return _fail("ships are at z=%d, not 0 - this test's premise moved" % ship_z)
|
||||
|
||||
var features: Array = _drawing_descendants(surface.get_node("SurfaceFeatures"), [])
|
||||
# A profile is allowed to have no scenery of its own - the research lab
|
||||
# had none but the capture rings, and #211 took those away. The rule is
|
||||
# about features that EXIST, so an empty profile is skipped rather than
|
||||
# failed; the run-wide check below is what stops this test quietly
|
||||
# checking nothing at all.
|
||||
if features.is_empty():
|
||||
return _fail("%s has no drawing surface features to check" % game_state.planet_definition_id)
|
||||
print("%s: no drawing surface features of its own" \
|
||||
% game_state.planet_definition_id)
|
||||
for feature in features:
|
||||
var z: int = _effective_z(feature)
|
||||
if z >= ship_z:
|
||||
|
|
@ -120,6 +125,8 @@ func _process(delta: float) -> bool:
|
|||
checked += features.size()
|
||||
|
||||
if planets.is_empty():
|
||||
if checked == 0:
|
||||
return _fail("no profile had a single drawing feature - this test proved nothing")
|
||||
print("TEST_SURFACE_FEATURE_DEPTH: PASS (%d features checked)" % checked)
|
||||
quit(0)
|
||||
return true
|
||||
|
|
|
|||
|
|
@ -118,6 +118,38 @@ func _state_cases() -> bool:
|
|||
return _fail("a wave of %d exceeds the %d cap" % [large.size(), GameState.WAVE_MAX_ENEMIES])
|
||||
print("fleet 1 -> %d enemies, fleet 9 -> %d enemies of %d kinds" \
|
||||
% [small.size(), large.size(), heavy_kinds.size()])
|
||||
|
||||
# --- #218: a starting fleet meets one kind of enemy, however long it
|
||||
# takes. Strength alone could not promise this - it counts the wave
|
||||
# NUMBER too, so simply surviving used to unlock lancers and snipers
|
||||
# against a fleet that had grown by nothing.
|
||||
var patient: int = game_state.wave_number
|
||||
for wave in [1, 5, 20, 60]:
|
||||
game_state.wave_number = wave
|
||||
for size in range(1, GameState.BASE_FLEET_SIZE + 1):
|
||||
for id in game_state.wave_composition(size):
|
||||
if id != "chaser":
|
||||
game_state.wave_number = patient
|
||||
return _fail("wave %d against a fleet of %d fields a %s" \
|
||||
% [wave, size, id])
|
||||
game_state.wave_number = patient
|
||||
print("a fleet of %d or fewer meets chasers only, at every wave number" \
|
||||
% GameState.BASE_FLEET_SIZE)
|
||||
|
||||
# And each vessel past the starting three opens exactly one more kind, so
|
||||
# the ramp is progressive rather than a cliff.
|
||||
var previous_kinds: int = 1
|
||||
for size in range(GameState.BASE_FLEET_SIZE, GameState.BASE_FLEET_SIZE + 4):
|
||||
var kinds_here: Dictionary = {}
|
||||
for id in game_state.wave_composition(size):
|
||||
kinds_here[id] = true
|
||||
if kinds_here.size() < previous_kinds:
|
||||
return _fail("a fleet of %d meets %d kinds, fewer than the %d before it" \
|
||||
% [size, kinds_here.size(), previous_kinds])
|
||||
previous_kinds = kinds_here.size()
|
||||
if previous_kinds < 3:
|
||||
return _fail("a grown fleet still only meets %d kinds of enemy" % previous_kinds)
|
||||
print("enemy variety climbs with the fleet, reaching %d kinds" % previous_kinds)
|
||||
game_state.reset_waves()
|
||||
return false
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue