The crafter buttons doing nothing was not the crafter: Ship.gd was left mid-edit and failed to parse (undeclared SHIELD_COLOR/SHIELD_RADIUS, and a spawn() that only exists on the concrete VFX subclass). A script that does not compile takes Main.tscn's whole node tree with it, so every button in the game was dead. The project directory is the one being played from, so an unfinished edit there is a broken build - finish or revert before stopping. With that fixed, the real gap underneath: an order the crafter cannot pay for is ACCEPTED and queued (#16), which is indistinguishable from a dead button. Ordering now reports either "building now" or "queued - N more ore must be delivered here", and the transient message is rendered LAST so it is the first line read rather than buried under the standing fleet-capacity note. test_crafter_feedback covers all three presses and asserts the message survives the per-frame refresh, which is the specific way this looked broken before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
121 lines
4.1 KiB
GDScript
121 lines
4.1 KiB
GDScript
extends SceneTree
|
|
|
|
# Headless gameplay test: run with
|
|
# godot --headless -s res://tests/test_crafter_feedback.gd
|
|
# Every press in the ship crafter must say what happened. Reported twice from
|
|
# play-testing as "the buttons do nothing".
|
|
#
|
|
# There is no silent-failure path left to press: storing with an empty hold,
|
|
# ordering something the crafter cannot pay for yet, and storing successfully
|
|
# all report. The queue in particular is invisible by nature - an order that
|
|
# cannot be afforded is accepted and waits (#16), which looks exactly like a
|
|
# dead button unless it says so.
|
|
#
|
|
# Verifies:
|
|
# 1. storing with an empty hold explains where ore comes from
|
|
# 2. ordering against an empty stock says it is queued AND how much more
|
|
# ore the building needs
|
|
# 3. a successful store reports the amount
|
|
# 4. the message survives the per-frame status refresh - it used to be
|
|
# overwritten on the very next frame, which is what made a working
|
|
# button look dead
|
|
# 5. the message is the FIRST line, not buried under the standing state
|
|
|
|
var game_state: Node
|
|
var scene: Node
|
|
var controller: Node
|
|
var crafter_id: int = 0
|
|
var stage: int = 0
|
|
var elapsed: float = 0.0
|
|
|
|
|
|
func _initialize() -> void:
|
|
game_state = root.get_node("GameState")
|
|
game_state.buildings.clear()
|
|
game_state.extractors.clear()
|
|
game_state.transporters.clear()
|
|
game_state.reset_progression()
|
|
game_state.unlocked = ["shipyards", "coordination", "wing"]
|
|
game_state.rocks = 100
|
|
game_state.fleet_cargo = 0
|
|
game_state.ships_owed = 0
|
|
scene = load("res://Main.tscn").instantiate()
|
|
root.add_child(scene)
|
|
controller = scene.get_node("BuildController")
|
|
|
|
|
|
func _first_line() -> String:
|
|
return controller.craft_status.text.split("\n")[0]
|
|
|
|
|
|
func _process(delta: float) -> bool:
|
|
elapsed += delta
|
|
match stage:
|
|
0:
|
|
scene.get_node("AsteroidField").set_process(false)
|
|
scene.get_node("EnemySpawner").set_process(false)
|
|
scene.get_node("BuildingField").place(GameState.BUILDING_SHIP_CRAFTER, Vector2(500.0, 0.0))
|
|
crafter_id = int(game_state.buildings[-1].id)
|
|
elapsed = 0.0
|
|
stage = 1
|
|
return false
|
|
1:
|
|
if elapsed < 0.3:
|
|
return false
|
|
controller.craft_building_id = crafter_id
|
|
controller.craft_menu.visible = true
|
|
controller._refresh_craft_status()
|
|
|
|
# --- 1. store with an empty hold ----------------------------
|
|
controller._on_store_ore()
|
|
var empty_msg: String = _first_line()
|
|
if "carrying nothing" not in empty_msg:
|
|
print("got: %s" % empty_msg)
|
|
print("TEST_CRAFTER_FEEDBACK: FAIL (storing with an empty hold said nothing useful)")
|
|
quit(1)
|
|
return true
|
|
print("empty hold -> %s" % empty_msg)
|
|
|
|
# --- 2. order against an empty stock ------------------------
|
|
controller._on_craft_interceptor()
|
|
var queued_msg: String = _first_line()
|
|
if "queued" not in queued_msg.to_lower() or "more ore" not in queued_msg:
|
|
print("got: %s" % queued_msg)
|
|
print("TEST_CRAFTER_FEEDBACK: FAIL (a queued order did not say it was queued and what it still needs)")
|
|
quit(1)
|
|
return true
|
|
if game_state.pending_orders(crafter_id) != 1:
|
|
print("TEST_CRAFTER_FEEDBACK: FAIL (the order was not actually queued)")
|
|
quit(1)
|
|
return true
|
|
print("unaffordable order -> %s" % queued_msg)
|
|
|
|
# --- 3. a successful store ----------------------------------
|
|
game_state.withdraw_to_fleet(30)
|
|
controller._on_store_ore()
|
|
var stored_msg: String = _first_line()
|
|
if "Stored" not in stored_msg:
|
|
print("got: %s" % stored_msg)
|
|
print("TEST_CRAFTER_FEEDBACK: FAIL (a successful store did not report)")
|
|
quit(1)
|
|
return true
|
|
print("successful store -> %s" % stored_msg)
|
|
elapsed = 0.0
|
|
stage = 2
|
|
return false
|
|
2:
|
|
# --- 4 + 5. it survives the refresh, and stays first --------
|
|
if elapsed < 0.5:
|
|
return false
|
|
controller._refresh_craft_status()
|
|
var still: String = _first_line()
|
|
if "Stored" not in still:
|
|
print("first line half a second later: %s" % still)
|
|
print("TEST_CRAFTER_FEEDBACK: FAIL (the message was overwritten by the per-frame refresh)")
|
|
quit(1)
|
|
return true
|
|
print("half a second later, still first: %s" % still)
|
|
print("TEST_CRAFTER_FEEDBACK: PASS")
|
|
quit(0)
|
|
return true
|
|
return false
|