#136 - the gas burner drawn on top of the transport hub. The build menu's rows were a written-down list of tops read with `tops[mini(row, size - 1)]`, and with every structure researched the menu wanted a fourth row where the list had three, so the last entry was placed on the third row's centre slot. A list that silently stacks whatever will not fit in it is not a layout. Rows are computed now, from a first top and a pitch that tightens to fit and is floored at the row's own height - so two rows cannot occupy the same space however far the catalog grows - and the column count widens rather than adding a row the band cannot hold. Ten structures now sit in four columns of three rows. The test unlocks the whole tree and checks every pair of entries for overlap, which is the shape of the bug rather than a coordinate that happens to be right today. #137 - the game had three answers to "what does this cost". The build menu drew a NeonNumber under each option, the craft menu drew glyph strips, and the ship lab spelled prices out inside the caption. PriceTag is that answer once: the resource's glyph, then what you HOLD over what it COSTS. It replaces all three, plus the info popup and the skill panel, and VesselPriceStrip - which was the same idea for one menu - goes with them. The build menu also gains the resource bar the other two already had, in the same place and at the same size, and the craft menu's - the only one not running the full width of its panel - now matches. The bar drops its LOCAL column where there is no building to report on, for the same reason #95 dropped it where it only repeated BANKED. #134 - the skill panel's explanation and its price were both centred in the same band, so a long description landed on the number. The text is the left of the band now and the tag is the right. The shortfall it used to spell out in a sentence - one per case, ore or research points - is the red on the tag instead, and research points ride along as an entry of their own, so the tag says WHICH thing is missing without a line of prose per possibility. One trap worth recording: PriceTag and ResourceBar read the GameState autoload, and an `extends SceneTree` test script that so much as names such a class - or preloads the file - fails to compile, because GameState is not an identifier in that context. GDScript then caches the failure, so the scene the test instantiates gets a Control with no script where its resource bar should be. The tests compare script PATHS, as strings.
132 lines
4.5 KiB
GDScript
132 lines
4.5 KiB
GDScript
extends SceneTree
|
|
|
|
# Headless gameplay test: run with
|
|
# godot --headless -s res://tests/test_build_info_popup.gd
|
|
# Every build option explains itself before anything is spent. Picking a
|
|
# structure costs ore and, for a mothership, can destroy the one you already
|
|
# have, so no option may build straight off the menu press.
|
|
#
|
|
# Verifies, for EVERY option in the build menu:
|
|
# 1. pressing it opens the popup with that building's own title and a
|
|
# non-empty description, and shows its price
|
|
# 2. nothing is built and no ore is spent until the confirm is pressed
|
|
# 3. confirming builds exactly that structure and charges exactly its cost
|
|
# 4. closing the popup instead drops the pending build entirely - a later
|
|
# confirm press does nothing
|
|
|
|
var scene: Node
|
|
var game_state: Node
|
|
var controller: Node
|
|
var popup: Control
|
|
var title: Label
|
|
var message: Label
|
|
# Control, not PriceTag: naming a class whose body reads the GameState
|
|
# autoload does not compile inside an `extends SceneTree` script.
|
|
var cost_number: Control
|
|
var build_button: IconButton
|
|
var stage: int = 0
|
|
var elapsed: float = 0.0
|
|
|
|
# Extractor is excluded: it needs a planet underneath, which
|
|
# test_extractor_building covers in full.
|
|
const OPTIONS: Array = [
|
|
{"type": "mothership", "title": "Mothership"},
|
|
{"type": "ship_crafter", "title": "Ship Crafter"},
|
|
{"type": "command_center", "title": "Command Center"},
|
|
]
|
|
|
|
|
|
func _initialize() -> void:
|
|
game_state = root.get_node("GameState")
|
|
game_state.rocks = 500
|
|
game_state.buildings.clear()
|
|
game_state.extractors.clear()
|
|
# Structures are gated behind the mothership's research tree. That gate
|
|
# is test_skill_tree's subject, not this one's - here every option needs
|
|
# to be available so the popup itself can be exercised.
|
|
game_state.reset_progression()
|
|
for node in GameState.SKILL_TREE:
|
|
game_state.unlocked.append(node.id)
|
|
|
|
scene = load("res://world/Main.tscn").instantiate()
|
|
root.add_child(scene)
|
|
controller = scene.get_node("BuildController")
|
|
popup = scene.get_node("UI/BuildMenu/InfoPopup")
|
|
title = popup.get_node("Panel/TitleLabel")
|
|
message = popup.get_node("Panel/MessageLabel")
|
|
cost_number = popup.get_node("Panel/CostNumber")
|
|
build_button = popup.get_node("Panel/BuildButton")
|
|
|
|
|
|
func _count(type: String) -> int:
|
|
var n: int = 0
|
|
for b in game_state.buildings:
|
|
if b.type == type:
|
|
n += 1
|
|
return n
|
|
|
|
|
|
func _process(delta: float) -> bool:
|
|
elapsed += delta
|
|
match stage:
|
|
0:
|
|
scene.get_node("AsteroidField").set_process(false)
|
|
scene.get_node("EnemySpawner").set_process(false)
|
|
elapsed = 0.0
|
|
stage = 1
|
|
return false
|
|
1:
|
|
if elapsed < 0.2:
|
|
return false
|
|
|
|
for option in OPTIONS:
|
|
var type: String = option.type
|
|
var expected_cost: int = controller.cost_of(type)
|
|
var rocks_before: int = game_state.rocks
|
|
var built_before: int = _count(type)
|
|
|
|
controller._on_build_pressed()
|
|
controller._show_building_info(type)
|
|
|
|
var informed: bool = popup.visible \
|
|
and title.text == option.title \
|
|
and message.text.length() > 40 \
|
|
and cost_number.priced(GameState.RESOURCE_ORE) == expected_cost \
|
|
and build_button.visible
|
|
var untouched: bool = game_state.rocks == rocks_before and _count(type) == built_before
|
|
print("%s: popup=%s title='%s' cost=%d offered=%s nothing_spent_yet=%s" % [
|
|
type, popup.visible, title.text,
|
|
cost_number.priced(GameState.RESOURCE_ORE),
|
|
build_button.visible, untouched
|
|
])
|
|
if not (informed and untouched):
|
|
print("TEST_BUILD_INFO_POPUP: FAIL (%s did not explain itself before building)" % type)
|
|
quit(1)
|
|
return true
|
|
|
|
# --- closing drops it, it must not build later ------------
|
|
controller._close_info()
|
|
controller._confirm_build()
|
|
if _count(type) != built_before or game_state.rocks != rocks_before:
|
|
print("TEST_BUILD_INFO_POPUP: FAIL (%s built after the popup was dismissed)" % type)
|
|
quit(1)
|
|
return true
|
|
|
|
# --- reopening and confirming builds it ------------------
|
|
controller._on_build_pressed()
|
|
controller._show_building_info(type)
|
|
controller._confirm_build()
|
|
var built: bool = _count(type) == built_before + 1
|
|
var charged: bool = game_state.rocks == rocks_before - expected_cost
|
|
print("%s: confirmed -> built=%s rocks %d->%d charged=%s" % [
|
|
type, built, rocks_before, game_state.rocks, charged
|
|
])
|
|
if not (built and charged):
|
|
print("TEST_BUILD_INFO_POPUP: FAIL (%s did not build on confirmation)" % type)
|
|
quit(1)
|
|
return true
|
|
|
|
print("TEST_BUILD_INFO_POPUP: PASS")
|
|
quit(0)
|
|
return true
|
|
return false
|