spaceship/tests/test_outpost_garrison_menu.gd

206 lines
8.7 KiB
GDScript3
Raw Normal View History

#102/#103 (#97d/#97e): the outpost, and the garrison you compose for it Landed together because an outpost you cannot crew is decoration, and a garrison menu with no building to hang it off is nothing at all. #102, the building. Researched at "Forward Bases", two prerequisites deep and cheaper than any mid-game structure - the ticket asks for early, and the answer to waves attacking your buildings should arrive at about the time the waves can hurt them. It is drawn as a watch mast on a tripod with two open berths, deliberately the least machinery of any structure here: it makes nothing and refines nothing, it holds a position. 120hp, more than a crafter, because being shot at is its job. #103, the garrison. Two separate things live on its record, and conflating them was the first thing to get wrong: the ORDER, what the player asked for, and the CREW, who is actually there. This menu edits the order only; #104 fills the crew. Keeping the order as a wish is what lets an outpost ask for a hull nobody has built yet and wait for it, rather than the request evaporating - and it is why the panel shows "here / ordered" instead of one number. "Only ships that attack can be assigned" is expressed as capabilities rather than a list of types, so a new armed vessel is eligible the day it is added and an unarmed one never is by accident. Today that means the interceptor and the laser vessel; a shield, a hauler and an engine block cannot hold a position however much one might want them to. Two berths by default, and Garrison Bays raises it at outposts ALREADY standing - the same shape as freight_yards for hubs, because a bonus that only reached new buildings would mean demolishing and rebuilding to collect it. The panel builds its own rows from the catalog rather than from nodes placed in the scene, so adding an armed vessel later needs no scene surgery. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 14:02:20 +02:00
extends SceneTree
# Headless test: run with
# godot --headless -s res://tests/test_outpost_garrison_menu.gd
# Forgejo #103 (#97e): "Outposts can be occupied by ships. Only ships that
# attack can be assigned. A dedicated menu on each outpost should let you
# compose how you want the outpost to be filled. There's a default of 2
# vessel per outpost, upgrades can increase this number."
#
# Two things live on an outpost's record and conflating them is the first
# thing to get wrong: the ORDER (what the player asked for) and the CREW
# (who is actually there). This menu edits the order only; #104 fills the
# crew. Keeping the order as a wish is what lets an outpost ask for a hull
# nobody has built yet and simply wait.
#
# Covers:
# A. only armed vessels can be assigned - a shield, a hauler or an engine
# block cannot hold a position however much the player wants it there
# B. two berths by default, and the order cannot exceed them
# C. researching the berth upgrade raises the ceiling at outposts ALREADY
# standing, not only at new ones
# D. the menu opens from the outpost, drives the real order through the
# real buttons, and reads back "here / ordered" rather than one number
# E. an outpost with an order nobody can fill reports it as waiting, and
# the order survives - it is not silently dropped
var game_state: Node
var scene: Node
var controller: Node
var menu: Control
var panel: Control
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.reset_progression()
game_state.reset_waves()
game_state.rocks = 5000
game_state.unlocked.append("forward_bases")
func _fail(message: String) -> bool:
print("TEST_OUTPOST_GARRISON_MENU: FAIL (%s)" % message)
quit(1)
return true
func _state_cases() -> bool:
# --- A. only armed vessels ------------------------------------------
var eligible: PackedStringArray = game_state.garrison_vessel_types()
if eligible.is_empty():
return _fail("no vessel at all may be garrisoned")
if not (GameState.VESSEL_INTERCEPTOR in eligible):
return _fail("the interceptor, which carries the fleet's gun, cannot be garrisoned")
for unarmed in [GameState.VESSEL_SHIELD, GameState.VESSEL_TRANSPORTER,
GameState.VESSEL_CARRIER, GameState.VESSEL_DISTORDER]:
if game_state.vessel_can_garrison(unarmed):
return _fail("%s can be garrisoned - it does not attack" % unarmed)
print("garrisonable: %s" % str(eligible))
var id: int = game_state.add_building(GameState.BUILDING_OUTPOST, Vector2(500.0, 0.0))
outpost_id = id
if not game_state.is_outpost(game_state.find_building(id)):
return _fail("the outpost does not carry the garrison capability")
if game_state.set_garrison_desired(id, GameState.VESSEL_SHIELD, 1) != 0:
return _fail("a shield vessel was accepted into a garrison")
# --- B. two berths, and the ceiling bites ---------------------------
if GameState.GARRISON_BASE_SLOTS != 2:
return _fail("the default is %d berths, not the 2 the ticket asks for" \
% GameState.GARRISON_BASE_SLOTS)
if game_state.outpost_slot_capacity() != 2:
return _fail("an unresearched outpost offers %d berths" % game_state.outpost_slot_capacity())
if game_state.set_garrison_desired(id, GameState.VESSEL_INTERCEPTOR, 5) != 2:
return _fail("an order of 5 was not clamped to the 2 berths available")
if game_state.garrison_desired_total(id) != 2:
return _fail("the outpost holds an order of %d against 2 berths" \
% game_state.garrison_desired_total(id))
print("2 berths by default; an order of 5 clamps to 2")
# --- C. the upgrade reaches existing outposts -----------------------
var upgrade_id: String = ""
for node in GameState.SKILL_TREE:
if int(node.get("outpost_bonus", 0)) > 0:
upgrade_id = String(node.id)
if upgrade_id == "":
return _fail("no skill node raises the berth count")
game_state.unlocked.append(upgrade_id)
if game_state.outpost_slot_capacity() != 3:
return _fail("researching \"%s\" left the capacity at %d" \
% [upgrade_id, game_state.outpost_slot_capacity()])
# The outpost was placed BEFORE the research, which is the case that
# matters: a bonus that only reached new buildings would mean demolishing
# and rebuilding to collect it.
if game_state.set_garrison_desired(id, GameState.VESSEL_INTERCEPTOR, 3) != 3:
return _fail("an outpost built before the upgrade did not gain the berth")
print("\"%s\" raises an already-standing outpost to 3 berths" % upgrade_id)
game_state.set_garrison_desired(id, GameState.VESSEL_INTERCEPTOR, 0)
return false
func _process(delta: float) -> bool:
elapsed += delta
match stage:
0:
if _state_cases():
return true
game_state.buildings.clear()
scene = load("res://world/Main.tscn").instantiate()
root.add_child(scene)
controller = scene.get_node("BuildController")
menu = scene.get_node("UI/GarrisonMenu")
panel = scene.get_node("UI/GarrisonMenu/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_OUTPOST, Vector2(500.0, 0.0))
elapsed = 0.0
stage = 2
return false
2:
if elapsed < 0.2:
return false
# --- D. the real menu, through the real buttons --------------
var outpost: Node2D = null
for b in scene.get_node("BuildingField").spawned:
if is_instance_valid(b) and b.building_type == GameState.BUILDING_OUTPOST:
outpost = b
if outpost == null:
return _fail("no outpost node was placed")
outpost_id = int(outpost.building_id)
if outpost.primary_action() != "garrison":
return _fail("the outpost's primary action is \"%s\", not \"garrison\"" \
% outpost.primary_action())
controller.current_building = outpost
controller._on_action_pressed()
if not menu.visible:
return _fail("the outpost did not open the garrison menu")
if panel.building_id != outpost_id:
return _fail("the panel opened without knowing which outpost it belongs to")
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")
for unarmed in [GameState.VESSEL_SHIELD, GameState.VESSEL_TRANSPORTER]:
for candidate in panel._rows:
if String(candidate.type) == unarmed:
return _fail("the panel offers a row for the unarmed %s" % unarmed)
row.plus.pressed.emit()
row.plus.pressed.emit()
if game_state.garrison_desired_of(outpost_id, GameState.VESSEL_INTERCEPTOR) != 2:
return _fail("two presses of + ordered %d vessels" \
% game_state.garrison_desired_of(outpost_id, GameState.VESSEL_INTERCEPTOR))
if row.count.text != "0/2":
return _fail("the row reads \"%s\" - it should show crew over order" % row.count.text)
row.minus.pressed.emit()
if game_state.garrison_desired_of(outpost_id, GameState.VESSEL_INTERCEPTOR) != 1:
return _fail("- did not take the order back down")
print("the menu drives the real order: + + - leaves 1, shown as \"%s\"" % row.count.text)
# The ceiling is enforced through the buttons too, not only in
# GameState - a + that silently did nothing would be worse than a
# disabled one.
row.plus.pressed.emit()
row.plus.pressed.emit()
row.plus.pressed.emit()
var capacity: int = game_state.outpost_slot_capacity()
if game_state.garrison_desired_total(outpost_id) != capacity:
return _fail("pressing + past the ceiling ordered %d against %d berths" \
% [game_state.garrison_desired_total(outpost_id), capacity])
if row.plus.enabled:
return _fail("+ is still offered with every berth already spoken for")
print("+ stops at %d berths and greys itself out" % capacity)
# --- E. an unfillable order waits ---------------------------
if game_state.garrison_crew(outpost_id).size() != 0:
return _fail("the outpost is crewed already, so waiting proves nothing")
controller._refresh_garrison()
var status: String = scene.get_node("UI/GarrisonMenu/StatusLabel").text
if status.strip_edges() == "":
return _fail("an outpost waiting on vessels says nothing at all")
if game_state.garrison_desired_total(outpost_id) != capacity:
return _fail("the order was dropped because it could not be filled")
print("waiting, and saying so: \"%s\"" % status)
print("TEST_OUTPOST_GARRISON_MENU: PASS")
quit(0)
return true
return false