spaceship/tests/test_planning_center.gd
Loïc Wisniewski df4d5e35f9 Find the planet setups in an exported build
The registry scanned res://PlanetSetups for names ending ".tres". An export
does not ship those: text resources are converted to binary and the pack
carries "<name>.tres.remap", which ResourceLoader follows when asked for the
original path. So the scan found nothing in the web build, every planet drew
no setup, and since #211 the setup IS the garrison - every world in the
deployed game was deserted, with nothing logged to say why. Confirmed by
reading index.pck, which holds rocky_default.tres.remap and no .tres.

The suite runs from source, where the bug cannot occur, so the name
resolution is its own function and tested against the exact strings the pack
contains.

Also: the planning centre listed transporters and repair drones but
set_vessel_target() still refused anything not fleet-eligible, so both rows
had buttons that did nothing - the worst shape a refusal can take. Listing
and setting read the same rule now, and a standing order is only offered to
a crafter that can actually take it.

And two quests close the chain: the tentacle world, which the scanner will
not offer until the story hands it over, and the Warden itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
2026-09-07 13:55:41 +02:00

275 lines
12 KiB
GDScript

extends SceneTree
# Headless test: run with
# godot --headless -s res://tests/test_planning_center.gd
# Forgejo #106 and #107 (#98a, #98b): the Planning Center.
#
# "let the player tell the system how many ships of a give type should
# always be available over the whole game (fleet + storage + outposts) ...
# the planning center automatically manage the ship craft building queues
# to ensure that the needed vessels are produced to fullfill the
# constraints."
#
# Covers:
# A. the census counts a vessel exactly once wherever it is - flying,
# stored, stationed at an outpost, or in the air between any two
# B. a shortfall is queued at a crafter, and queued vessels count, so it
# does not re-order the same shortfall every tick
# C. it tops up rather than rewriting: an order placed by hand survives,
# and the planner appends beside it
# D. it stops when the target is met, and starts again when one is lost
# E. nothing happens without a planning centre, but the plan is remembered
# across losing and rebuilding one
# F. the panel drives the real targets through the real buttons
var game_state: Node
var scene: Node
var controller: Node
var menu: Control
var panel: Control
var crafter_id: int = 0
var outpost_id: int = 0
var stage: int = 0
var elapsed: float = 0.0
func _initialize() -> void:
# GameState is an autoload: a bare GameState.<instance member> does not
# compile in a SceneTree script, so the node is fetched by name instead.
game_state = root.get_node("GameState")
game_state.buildings.clear()
game_state.extractors.clear()
game_state.transporters.clear()
game_state.stored_vessels.clear()
game_state.vessel_deliveries.clear()
game_state.vessel_targets.clear()
game_state.fleet_vessel_types = PackedStringArray()
game_state.reset_progression()
game_state.reset_waves()
game_state.unlocked.append_array(["shipyards", "coordination", "forward_bases",
"vessel_science", "logistics_command"])
func _fail(message: String) -> bool:
print("TEST_PLANNING_CENTER: FAIL (%s)" % message)
quit(1)
return true
func _queued_at_crafter() -> int:
return game_state.pending_orders_of(crafter_id, GameState.VESSEL_INTERCEPTOR)
func _state_cases() -> bool:
game_state.add_building(GameState.BUILDING_MOTHERSHIP, Vector2.ZERO)
crafter_id = game_state.add_building(GameState.BUILDING_SHIP_CRAFTER, Vector2(400.0, 0.0))
outpost_id = game_state.add_building(GameState.BUILDING_OUTPOST, Vector2(-900.0, 0.0))
var interceptor: String = GameState.VESSEL_INTERCEPTOR
# --- A. one vessel, one count, wherever it is ------------------------
if game_state.vessel_census(interceptor) != 0:
return _fail("an empty game counts %d vessels" % game_state.vessel_census(interceptor))
# --- #221: support craft are plannable too --------------------------
# #216 made transporters and repair drones ordinary craft that wear out
# and need replacing, which is exactly what a standing order is for -
# and they were the one thing the planning centre could not plan.
var plannable: PackedStringArray = game_state.planned_vessel_types()
for support in [GameState.VESSEL_TRANSPORTER, GameState.REPAIR_DRONE]:
if not plannable.has(support):
return _fail("'%s' cannot be planned, so a standing order cannot replace one" % support)
# And the census has to SEE them where they live, or an order for one
# could never be satisfied and the crafters would build forever.
var hub_id: int = game_state.add_building(GameState.BUILDING_TRANSPORT_HUB,
Vector2(1200.0, 0.0))
load("res://tests/_staff.gd").staff(game_state, hub_id, 1)
if game_state.vessel_census(GameState.VESSEL_TRANSPORTER) != 1:
return _fail("a transporter standing at a hub is counted %d times, expected once" \
% game_state.vessel_census(GameState.VESSEL_TRANSPORTER))
game_state.remove_buildings_of_type(GameState.BUILDING_TRANSPORT_HUB)
# Listed is not enough - the buttons have to move the number. #221 left
# the setter reading the old fleet-eligible rule, so both rows appeared
# with controls that did nothing at all.
for support in [GameState.VESSEL_TRANSPORTER, GameState.REPAIR_DRONE]:
if game_state.adjust_vessel_target(support, 2) != 2:
return _fail("'%s' is listed in the planning centre but its target cannot be raised" \
% support)
if game_state.vessel_target(support) != 2:
return _fail("'%s' target did not stick at 2" % support)
game_state.set_vessel_target(support, 0)
print("support craft are plannable, settable, and counted where they stand")
game_state.fleet_vessel_types = PackedStringArray([interceptor, interceptor])
game_state.stored_vessels.append(interceptor)
game_state.set_garrison_desired(outpost_id, interceptor, 1)
game_state.garrison_crew(outpost_id).append(interceptor)
game_state.vessel_deliveries.append({"type": interceptor, "from": Vector2.ZERO,
"to": Vector2(400.0, 0.0), "progress": 0.0, "to_outpost": 0})
if game_state.vessel_count_in_fleet(interceptor) != 2:
return _fail("the fleet reads %d" % game_state.vessel_count_in_fleet(interceptor))
if game_state.vessel_count_in_hangar(interceptor) != 1:
return _fail("the hangar reads %d" % game_state.vessel_count_in_hangar(interceptor))
if game_state.vessel_count_in_garrisons(interceptor) != 1:
return _fail("the garrisons read %d" % game_state.vessel_count_in_garrisons(interceptor))
if game_state.vessel_count_in_transit(interceptor) != 1:
return _fail("in transit reads %d" % game_state.vessel_count_in_transit(interceptor))
if game_state.vessel_census(interceptor) != 5:
return _fail("2 flying + 1 stored + 1 stationed + 1 moving counted as %d" \
% game_state.vessel_census(interceptor))
print("census: 2 flying, 1 stored, 1 stationed, 1 moving = %d" \
% game_state.vessel_census(interceptor))
# --- E (first half). no centre, no action ---------------------------
game_state.set_vessel_target(interceptor, 8)
for i in range(20):
game_state._advance_planning()
if _queued_at_crafter() != 0:
return _fail("orders were placed with no planning centre built")
print("with no planning centre, a standing order does nothing")
game_state.add_building(GameState.BUILDING_PLANNING_CENTER, Vector2(900.0, 0.0))
if not game_state.has_planning_center():
return _fail("the planning centre was built but is not recognised")
# --- B. the shortfall is queued, once ------------------------------
if game_state.vessel_shortfall(interceptor) != 3:
return _fail("a target of 8 against 5 reads a shortfall of %d" \
% game_state.vessel_shortfall(interceptor))
for i in range(20):
game_state._advance_planning()
if _queued_at_crafter() != 3:
return _fail("20 passes at a shortfall of 3 queued %d orders" % _queued_at_crafter())
if game_state.vessel_shortfall(interceptor) != 0:
return _fail("the shortfall is still %d with 3 on the slipway" \
% game_state.vessel_shortfall(interceptor))
print("a shortfall of 3 queued exactly 3, and 20 more passes added none")
# --- C. hand-placed orders are left alone ---------------------------
game_state.queue_order(crafter_id, GameState.VESSEL_SHIELD)
var by_hand: int = game_state.pending_orders_of(crafter_id, GameState.VESSEL_SHIELD)
game_state.set_vessel_target(interceptor, 10)
for i in range(20):
game_state._advance_planning()
if game_state.pending_orders_of(crafter_id, GameState.VESSEL_SHIELD) != by_hand:
return _fail("the planner cancelled a shield the player ordered by hand")
if _queued_at_crafter() != 5:
return _fail("raising the target to 10 left %d interceptors queued" % _queued_at_crafter())
print("a hand-placed shield survives; the planner appended beside it")
# --- D. it stops, and it reacts to a loss ---------------------------
game_state.set_vessel_target(interceptor, 5)
var queued_before: int = _queued_at_crafter()
for i in range(20):
game_state._advance_planning()
if _queued_at_crafter() != queued_before:
return _fail("lowering the target changed the queue by %d" \
% (_queued_at_crafter() - queued_before))
# Losing one from the fleet is a real shortfall again.
game_state.set_vessel_target(interceptor, 12)
var before_loss: int = _queued_at_crafter()
for i in range(20):
game_state._advance_planning()
var after_top_up: int = _queued_at_crafter()
game_state.fleet_vessel_types = PackedStringArray([interceptor])
for i in range(20):
game_state._advance_planning()
if _queued_at_crafter() <= after_top_up:
return _fail("losing a vessel did not put a replacement on the slipway")
print("target met -> nothing queued; a vessel lost -> %d queued (was %d)" \
% [_queued_at_crafter() - after_top_up, before_loss])
# --- E (second half). the plan outlives the building ----------------
for b in game_state.buildings:
if game_state.building_has_capability(b, "planning"):
game_state.destroy_building(int(b.id))
break
if game_state.has_planning_center():
return _fail("the planning centre survived being destroyed")
if game_state.vessel_target(interceptor) != 12:
return _fail("losing the building erased the plan")
var frozen: int = _queued_at_crafter()
game_state.fleet_vessel_types = PackedStringArray()
for i in range(20):
game_state._advance_planning()
if _queued_at_crafter() != frozen:
return _fail("the planner kept working after its building was destroyed")
print("destroyed: the plan is remembered, and nothing acts on it")
return false
func _process(delta: float) -> bool:
elapsed += delta
match stage:
0:
if _state_cases():
return true
game_state.buildings.clear()
game_state.vessel_targets.clear()
scene = load("res://world/Main.tscn").instantiate()
root.add_child(scene)
controller = scene.get_node("BuildController")
menu = scene.get_node("UI/PlanningMenu")
panel = scene.get_node("UI/PlanningMenu/Panel")
elapsed = 0.0
stage = 1
return false
1:
if elapsed < 0.3:
return false
scene.get_node("AsteroidField").set_process(false)
scene.get_node("EnemySpawner").set_process(false)
scene.get_node("BuildingField").place(GameState.BUILDING_PLANNING_CENTER,
Vector2(900.0, 0.0))
elapsed = 0.0
stage = 2
return false
2:
if elapsed < 0.2:
return false
# --- F. the real panel, through the real buttons -------------
var node: Node2D = null
for b in scene.get_node("BuildingField").spawned:
if is_instance_valid(b) and b.building_type == GameState.BUILDING_PLANNING_CENTER:
node = b
if node == null:
return _fail("no planning centre node was placed")
if node.primary_action() != "planning":
return _fail("its primary action is \"%s\"" % node.primary_action())
controller.current_building = node
controller._on_action_pressed()
if not menu.visible:
return _fail("the planning centre did not open its menu")
var row: Dictionary = {}
for candidate in panel._rows:
if String(candidate.type) == GameState.VESSEL_INTERCEPTOR:
row = candidate
if row.is_empty():
return _fail("the panel has no row for the interceptor")
row.plus.pressed.emit()
row.plus.pressed.emit()
row.plus.pressed.emit()
if game_state.vessel_target(GameState.VESSEL_INTERCEPTOR) != 3:
return _fail("three presses of + set a target of %d" \
% game_state.vessel_target(GameState.VESSEL_INTERCEPTOR))
row.minus.pressed.emit()
if game_state.vessel_target(GameState.VESSEL_INTERCEPTOR) != 2:
return _fail("- did not lower the target")
if not row.count.text.ends_with("/2"):
return _fail("the row reads \"%s\" - it should show have over want" % row.count.text)
# The note is what makes an unmet plan legible: it says where the
# vessels actually are.
if row.note.text.strip_edges() == "" and game_state.vessel_census(
GameState.VESSEL_INTERCEPTOR) > 0:
return _fail("vessels exist but the row says nothing about where they are")
print("the panel drives the real plan: + + + - leaves \"%s\" (%s)" \
% [row.count.text, row.note.text])
var status: String = scene.get_node("UI/PlanningMenu/StatusLabel").text
if status.strip_edges() == "":
return _fail("the planning menu reports nothing at all")
print("status: \"%s\"" % status)
print("TEST_PLANNING_CENTER: PASS")
quit(0)
return true
return false