This is the actual cause of "the build a ship menu buttons do nothing", reported four times. Opening the crafter panel stopped recording WHICH crafter: when the auto-unload was replaced with an explicit store button, the replaced block reached one line further up than intended and took `craft_building_id = current_building.building_id` with it. Every handler in the panel then bailed out at its `if craft_building_id == 0` guard, while the panel itself still opened and Close still worked - because Close is the only button that does not need the binding. The silent `return` on that guard is what let it pass unnoticed, so it now says so instead. The test was complicit: it set craft_building_id by hand, which skipped the exact step that was broken. It now parks the fleet on the crafter, waits for the action button to debounce in, and presses it - the way the game does. Verified against the broken code: it fails with "opening the panel did not record which crafter". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
201 lines
7.7 KiB
GDScript
201 lines
7.7 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
|
|
# 6. the panel is opened the way the GAME opens it - park the fleet on the
|
|
# crafter and press the action button - and every button then works
|
|
#
|
|
# Point 6 exists because this test used to set craft_building_id by hand.
|
|
# That skipped the one step that was actually broken: opening the panel
|
|
# stopped recording WHICH crafter, so every handler bailed out at its `== 0`
|
|
# guard while the panel still opened. Reported four times as "the buttons do
|
|
# nothing", and invisible to every test that set the id itself.
|
|
|
|
var game_state: Node
|
|
var scene: Node
|
|
var controller: Node
|
|
var crafter_id: int = 0
|
|
var fleet: Node
|
|
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.vessels_owed.clear()
|
|
scene = load("res://Main.tscn").instantiate()
|
|
root.add_child(scene)
|
|
controller = scene.get_node("BuildController")
|
|
fleet = scene.get_node("Fleet")
|
|
|
|
|
|
func _tap(button: Control) -> void:
|
|
for pressed in [true, false]:
|
|
var ev: InputEventScreenTouch = InputEventScreenTouch.new()
|
|
ev.pressed = pressed
|
|
ev.position = button.size / 2.0
|
|
button._gui_input(ev)
|
|
|
|
|
|
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)
|
|
for a in get_nodes_in_group("asteroids"):
|
|
if is_instance_valid(a):
|
|
a.queue_free()
|
|
scene.get_node("BuildingField").place(GameState.BUILDING_SHIP_CRAFTER, Vector2(500.0, 0.0))
|
|
crafter_id = int(game_state.buildings[-1].id)
|
|
# Park the fleet ON it and let the action button debounce in.
|
|
for ship in fleet.ships:
|
|
ship.position = Vector2(500.0, 0.0)
|
|
fleet.move_to(Vector2(500.0, 0.0))
|
|
elapsed = 0.0
|
|
stage = 1
|
|
return false
|
|
1:
|
|
if elapsed < 1.2:
|
|
return false
|
|
# --- 6. open it the way the game does ------------------------
|
|
if controller.current_building == null:
|
|
print("TEST_CRAFTER_FEEDBACK: FAIL (fleet parked on the crafter never registered)")
|
|
quit(1)
|
|
return true
|
|
_tap(controller.action_button)
|
|
if not controller.craft_menu.visible:
|
|
print("TEST_CRAFTER_FEEDBACK: FAIL (the action button did not open the crafter panel)")
|
|
quit(1)
|
|
return true
|
|
if controller.craft_building_id != crafter_id:
|
|
print("craft_building_id=%d, crafter is %d" % [controller.craft_building_id, crafter_id])
|
|
print("TEST_CRAFTER_FEEDBACK: FAIL (opening the panel did not record which crafter - every button will bail out)")
|
|
quit(1)
|
|
return true
|
|
print("panel opened from the building, bound to crafter %d" % controller.craft_building_id)
|
|
|
|
# --- 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)
|
|
|
|
# --- 6. a REAL tap, through the input path ------------------
|
|
# Everything above calls the handlers directly, which is how a
|
|
# whole class of bug kept getting missed: the crafter logic was
|
|
# fine and the BUTTON was inert, blocked by a modal guard. This
|
|
# taps the control the way a touch does.
|
|
game_state.withdraw_to_fleet(20)
|
|
var stock_before: int = game_state.building_stock(crafter_id)
|
|
var store_button: Control = scene.get_node("UI/CraftMenu/StoreOreButton")
|
|
for pressed in [true, false]:
|
|
var ev: InputEventScreenTouch = InputEventScreenTouch.new()
|
|
ev.pressed = pressed
|
|
ev.position = store_button.size / 2.0
|
|
store_button._gui_input(ev)
|
|
if game_state.building_stock(crafter_id) <= stock_before:
|
|
print("stock %d -> %d after tapping Store ore" % [
|
|
stock_before, game_state.building_stock(crafter_id)
|
|
])
|
|
print("TEST_CRAFTER_FEEDBACK: FAIL (tapping the store button did nothing)")
|
|
quit(1)
|
|
return true
|
|
print("real tap on Store ore moved %d ore into the crafter" % [
|
|
game_state.building_stock(crafter_id) - stock_before
|
|
])
|
|
|
|
# And the same tap must still work after the build menu has been
|
|
# opened and closed, which used to block every button forever.
|
|
controller._on_build_pressed()
|
|
controller._show_building_info(GameState.BUILDING_SHIP_CRAFTER)
|
|
scene.get_node("UI/BuildMenu").visible = false
|
|
game_state.withdraw_to_fleet(10)
|
|
var stock_mid: int = game_state.building_stock(crafter_id)
|
|
for pressed2 in [true, false]:
|
|
var ev2: InputEventScreenTouch = InputEventScreenTouch.new()
|
|
ev2.pressed = pressed2
|
|
ev2.position = store_button.size / 2.0
|
|
store_button._gui_input(ev2)
|
|
if game_state.building_stock(crafter_id) <= stock_mid:
|
|
print("TEST_CRAFTER_FEEDBACK: FAIL (store button dead after the build menu was opened and closed)")
|
|
quit(1)
|
|
return true
|
|
print("still works after opening and closing the build menu")
|
|
print("TEST_CRAFTER_FEEDBACK: PASS")
|
|
quit(0)
|
|
return true
|
|
return false
|