Remove the one-way ore API superseded by the transfer menu
The loose end from #24. withdraw_to_fleet, unload_into_building and deliver_cargo were the old one-way moves behind "Load ore" and "Store ore here"; every one of them is a special case of transfer_to_building / transfer_to_fleet, and none had a caller left outside tests. The cargo_delivered signal had no listeners at all. Not just tidying: unload_into_building routed through deposit_to_building, which does NOT special-case the mothership - it writes b.stock, while building_holdings() returns `rocks` for a mothership and ignores stock entirely. So unload_into_building( mothership_id) would have parked ore in a field nothing reads, with no error. Nothing called it that way, but it is a plausibly-named function sitting there to be reached for later. transfer_to_building handles it. CARGO_LOAD_BATCH stays - the transfer menu's batch buttons use it; only its role as withdraw_to_fleet's default argument is gone. The #24 note expected folding these in to be costly for test_cargo_readout, which places no buildings. It is not: the transfer primitives work off GameState records rather than spawned nodes, so a single add_building() in setup is enough and it stays a display test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
fe1c721d64
commit
b6427b080d
27 changed files with 1085 additions and 275 deletions
|
|
@ -1,8 +1,8 @@
|
|||
extends Node2D
|
||||
|
||||
# Preview, not a test. Opens the CraftMenu so its layout can be checked on a
|
||||
# real viewport - the mothership's "Load ore" label was falling off the bottom
|
||||
# of shorter screens, which no headless test can see.
|
||||
# real viewport - label collisions and things falling off the bottom of
|
||||
# shorter screens are what no headless test can see.
|
||||
#
|
||||
# bash tests/screenshot.sh res://tests/_craft_menu_preview.tscn /tmp/m.png 20 720x1280
|
||||
# ... and again at 720x960 to check a shorter screen format.
|
||||
|
|
@ -17,11 +17,14 @@ func _ready() -> void:
|
|||
GameState.unlocked = ["shipyards", "logistics", "freight"]
|
||||
var main: Node = load("res://Main.tscn").instantiate()
|
||||
add_child(main)
|
||||
main.get_node("BuildingField").place(GameState.BUILDING_TRANSPORT_HUB, Vector2(400.0, 0.0))
|
||||
main.get_node("BuildingField").place(GameState.BUILDING_SHIP_CRAFTER, Vector2(400.0, 0.0))
|
||||
var crafter_id: int = int(GameState.buildings[-1].id)
|
||||
GameState.deposit_to_building(crafter_id, 42)
|
||||
var bc: Node = main.get_node("BuildController")
|
||||
bc.craft_building_id = 1
|
||||
bc.craft_building_id = crafter_id
|
||||
# The shared header (#24) is the thing worth eyeballing here, so give it
|
||||
# a building to report on - otherwise its HERE column hides itself.
|
||||
for bar in bc.resource_bars:
|
||||
bar.building_id = crafter_id
|
||||
main.get_node("UI/CraftMenu").visible = true
|
||||
if "Craft" == "Craft":
|
||||
bc._refresh_craft_status()
|
||||
else:
|
||||
bc._refresh_load_ore()
|
||||
bc._refresh_craft_status()
|
||||
|
|
|
|||
|
|
@ -16,10 +16,17 @@ func _ready() -> void:
|
|||
var main: Node = load("res://Main.tscn").instantiate()
|
||||
add_child(main)
|
||||
main.get_node("BuildingField").place(GameState.BUILDING_SHIP_LAB, Vector2(400.0, 0.0))
|
||||
var lab_id: int = int(GameState.buildings[-1].id)
|
||||
GameState.deposit_to_building(lab_id, 18)
|
||||
GameState.fleet_cargo = 12
|
||||
# A level already bought, so the list shows both a researched and an
|
||||
# unresearched line rather than a uniform page of zeroes.
|
||||
GameState.vessel_levels["int_damage"] = 1
|
||||
var bc: Node = main.get_node("BuildController")
|
||||
# The shared header (#24) hides its HERE column when it has no building,
|
||||
# so bind it the way opening the panel from the building does.
|
||||
for bar in bc.resource_bars:
|
||||
bar.building_id = lab_id
|
||||
main.get_node("UI/LabMenu").visible = true
|
||||
bc.lab_vessel = GameState.VESSEL_INTERCEPTOR
|
||||
bc._refresh_lab()
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
extends Node2D
|
||||
|
||||
# Preview, not a test. Opens the SkillMenu so its layout can be checked on a
|
||||
# real viewport - the mothership's "Load ore" label was falling off the bottom
|
||||
# of shorter screens, which no headless test can see.
|
||||
# real viewport - the tree lost vertical room to the shared resource header
|
||||
# (#24), and whether it still fits is a thing only pixels can answer.
|
||||
#
|
||||
# bash tests/screenshot.sh res://tests/_skill_menu_preview.tscn /tmp/m.png 20 720x1280
|
||||
# ... and again at 720x960 to check a shorter screen format.
|
||||
|
|
@ -17,15 +17,13 @@ func _ready() -> void:
|
|||
GameState.unlocked = ["shipyards", "logistics", "freight"]
|
||||
var main: Node = load("res://Main.tscn").instantiate()
|
||||
add_child(main)
|
||||
main.get_node("BuildingField").place(GameState.BUILDING_TRANSPORT_HUB, Vector2(400.0, 0.0))
|
||||
main.get_node("BuildingField").place(GameState.BUILDING_MOTHERSHIP, Vector2(400.0, 0.0))
|
||||
var mothership_id: int = int(GameState.buildings[-1].id)
|
||||
var bc: Node = main.get_node("BuildController")
|
||||
bc.craft_building_id = 1
|
||||
for bar in bc.resource_bars:
|
||||
bar.building_id = mothership_id
|
||||
main.get_node("UI/SkillMenu").visible = true
|
||||
if "Skill" == "Craft":
|
||||
bc._refresh_craft_status()
|
||||
else:
|
||||
bc._refresh_load_ore()
|
||||
# Select a node so the description, price and status all have text -
|
||||
# an empty description cannot collide with anything.
|
||||
bc.skill_tree.selected_id = "freight"
|
||||
bc._refresh_skill_menu("freight")
|
||||
# Select a node so the description, price and status all have text - an
|
||||
# empty description cannot collide with anything.
|
||||
bc.skill_tree.selected_id = "freight"
|
||||
bc._refresh_skill_menu("freight")
|
||||
|
|
|
|||
27
tests/_transfer_menu_preview.gd
Normal file
27
tests/_transfer_menu_preview.gd
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
extends Node2D
|
||||
|
||||
# Preview, not a test. Opens the TransferMenu (#24) so its layout can be
|
||||
# checked on a real viewport - four buttons, two captions and the shared
|
||||
# header have to coexist without collisions, which no headless test can see.
|
||||
#
|
||||
# bash tests/screenshot.sh res://tests/_transfer_menu_preview.tscn /tmp/m.png 20 720x1280
|
||||
# ... and again at 720x960 to check a shorter screen format.
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
GameState.buildings.clear()
|
||||
GameState.extractors.clear()
|
||||
GameState.transporters.clear()
|
||||
GameState.rocks = 180
|
||||
GameState.fleet_cargo = 24
|
||||
GameState.unlocked = ["shipyards", "logistics", "freight"]
|
||||
var main: Node = load("res://Main.tscn").instantiate()
|
||||
add_child(main)
|
||||
main.get_node("BuildingField").place(GameState.BUILDING_SHIP_CRAFTER, Vector2(400.0, 0.0))
|
||||
var crafter_id: int = int(GameState.buildings[-1].id)
|
||||
GameState.deposit_to_building(crafter_id, 42)
|
||||
var bc: Node = main.get_node("BuildController")
|
||||
bc.transfer_building_id = crafter_id
|
||||
main.get_node("UI/TransferMenu").visible = true
|
||||
bc._refresh_transfer()
|
||||
bc.transfer_status.text = "Moved 25 ore into this building"
|
||||
1
tests/_transfer_menu_preview.gd.uid
Normal file
1
tests/_transfer_menu_preview.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://b8omh1o4ysy6p
|
||||
6
tests/_transfer_menu_preview.tscn
Normal file
6
tests/_transfer_menu_preview.tscn
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://tests/_transfer_menu_preview.gd" id="1"]
|
||||
|
||||
[node name="TransferMenuPreview" type="Node2D"]
|
||||
script = ExtResource("1")
|
||||
|
|
@ -27,6 +27,7 @@ var scene: Node
|
|||
var banked: Control
|
||||
var cargo: Control
|
||||
var stage: int = 0
|
||||
var mothership_id: int = 0
|
||||
var elapsed: float = 0.0
|
||||
|
||||
|
||||
|
|
@ -36,6 +37,11 @@ func _initialize() -> void:
|
|||
game_state.extractors.clear()
|
||||
game_state.rocks = 42
|
||||
game_state.fleet_cargo = 0
|
||||
# Banking is a transfer TO the mothership now (#24), and a transfer needs
|
||||
# a building to aim at. Only the record is required - the transfer
|
||||
# primitives work off GameState, not off the spawned node - so this stays
|
||||
# a display test rather than becoming one about the building system.
|
||||
mothership_id = game_state.add_building(GameState.BUILDING_MOTHERSHIP, Vector2.ZERO)
|
||||
scene = load("res://Main.tscn").instantiate()
|
||||
root.add_child(scene)
|
||||
banked = scene.get_node("UI/ResourceCounter")
|
||||
|
|
@ -95,7 +101,7 @@ func _process(delta: float) -> bool:
|
|||
])
|
||||
|
||||
# --- 4. delivery moves it across ----------------------------
|
||||
game_state.deliver_cargo()
|
||||
game_state.transfer_to_building(mothership_id, game_state.fleet_cargo)
|
||||
elapsed = 0.0
|
||||
stage = 2
|
||||
return false
|
||||
|
|
|
|||
|
|
@ -5,29 +5,28 @@ extends SceneTree
|
|||
# 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.
|
||||
# The queue 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
|
||||
# 1. 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
|
||||
# 2. 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
|
||||
# 3. the message is the FIRST line, not buried under the standing state
|
||||
# 4. the panel is opened the way the GAME opens it - park the fleet on the
|
||||
# crafter and press the action button - and its buttons then work
|
||||
#
|
||||
# Point 6 exists because this test used to set craft_building_id by hand.
|
||||
# Point 4 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.
|
||||
#
|
||||
# Storing ore used to be tested here too. It moved out of this panel entirely
|
||||
# in #24 - see test_resource_transfer.gd.
|
||||
|
||||
var game_state: Node
|
||||
var scene: Node
|
||||
|
|
@ -44,7 +43,11 @@ func _initialize() -> void:
|
|||
game_state.extractors.clear()
|
||||
game_state.transporters.clear()
|
||||
game_state.reset_progression()
|
||||
game_state.unlocked = ["shipyards", "coordination", "wing"]
|
||||
# Every capacity node, so the interceptor button stays available for the
|
||||
# real-tap checks below. A full fleet correctly DISABLES it, and a
|
||||
# disabled button not reacting to a tap is not the bug those checks are
|
||||
# looking for.
|
||||
game_state.unlocked = ["shipyards", "coordination", "wing", "squadron", "armada"]
|
||||
game_state.rocks = 100
|
||||
game_state.fleet_cargo = 0
|
||||
game_state.vessels_owed.clear()
|
||||
|
|
@ -104,17 +107,7 @@ func _process(delta: float) -> bool:
|
|||
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 ------------------------
|
||||
# --- 1. 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:
|
||||
|
|
@ -127,71 +120,47 @@ func _process(delta: float) -> bool:
|
|||
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 --------
|
||||
# --- 2 + 3. 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:
|
||||
if "queued" not in still.to_lower():
|
||||
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 ------------------
|
||||
# --- 4. 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)
|
||||
var queued_before: int = game_state.pending_orders(crafter_id)
|
||||
_tap(scene.get_node("UI/CraftMenu/InterceptorButton"))
|
||||
if game_state.pending_orders(crafter_id) <= queued_before:
|
||||
print("orders %d -> %d after tapping Interceptor" % [
|
||||
queued_before, game_state.pending_orders(crafter_id)
|
||||
])
|
||||
print("TEST_CRAFTER_FEEDBACK: FAIL (tapping the store button did nothing)")
|
||||
print("TEST_CRAFTER_FEEDBACK: FAIL (tapping the interceptor 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
|
||||
])
|
||||
print("real tap on Interceptor queued another order")
|
||||
|
||||
# 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)")
|
||||
var queued_mid: int = game_state.pending_orders(crafter_id)
|
||||
_tap(scene.get_node("UI/CraftMenu/InterceptorButton"))
|
||||
if game_state.pending_orders(crafter_id) <= queued_mid:
|
||||
print("TEST_CRAFTER_FEEDBACK: FAIL (craft button dead after the build menu was opened and closed)")
|
||||
quit(1)
|
||||
return true
|
||||
print("still works after opening and closing the build menu")
|
||||
|
|
|
|||
|
|
@ -12,7 +12,10 @@ extends SceneTree
|
|||
#
|
||||
# Verifies:
|
||||
# 1. mining fills the hold and leaves the bank alone
|
||||
# 2. reaching the mothership banks the lot, and empties the hold
|
||||
# 2. unloading at the mothership banks the lot and empties the hold - but
|
||||
# only when the player asks. Arriving used to do it by itself; #24
|
||||
# removed that, so this now checks BOTH halves: parking changes
|
||||
# nothing, and the explicit transfer moves everything
|
||||
# 3. with no mothership there is nowhere to deliver, so cargo just
|
||||
# accumulates - it is never quietly banked anyway
|
||||
# 4. sitting on a DIFFERENT building is not a delivery
|
||||
|
|
@ -27,6 +30,7 @@ var field: Node
|
|||
var stage: int = 0
|
||||
var elapsed: float = 0.0
|
||||
var banked_before: int = 0
|
||||
var mothership_id: int = 0
|
||||
|
||||
const MOTHERSHIP_AT: Vector2 = Vector2(1200.0, 0.0)
|
||||
const CRAFTER_AT: Vector2 = Vector2(-4000.0, 0.0)
|
||||
|
|
@ -117,6 +121,7 @@ func _process(delta: float) -> bool:
|
|||
# --- 4. a different building is not a delivery point --------
|
||||
field.place(GameState.BUILDING_SHIP_CRAFTER, CRAFTER_AT)
|
||||
field.place(GameState.BUILDING_MOTHERSHIP, MOTHERSHIP_AT)
|
||||
mothership_id = int(game_state.buildings[-1].id)
|
||||
_park_fleet_on(CRAFTER_AT)
|
||||
elapsed = 0.0
|
||||
stage = 2
|
||||
|
|
@ -139,6 +144,14 @@ func _process(delta: float) -> bool:
|
|||
if elapsed < 0.4:
|
||||
return false
|
||||
var total_before: int = banked_before + 3
|
||||
# Parking alone must do nothing now (#24).
|
||||
if game_state.fleet_cargo != 3 or game_state.rocks != banked_before:
|
||||
print("TEST_FLEET_CARGO: FAIL (arriving at the mothership banked the hold by itself)")
|
||||
quit(1)
|
||||
return true
|
||||
print("parked on the mothership -> hold still %d (no auto-banking)" % game_state.fleet_cargo)
|
||||
|
||||
game_state.transfer_to_building(mothership_id, 999)
|
||||
print("delivered: hold=%d bank=%d (expected hold=0 bank=%d)" % [
|
||||
game_state.fleet_cargo, game_state.rocks, total_before
|
||||
])
|
||||
|
|
@ -156,10 +169,6 @@ func _process(delta: float) -> bool:
|
|||
# Not simulated by loading PlanetSurface.tscn here: the point is
|
||||
# that cargo lives in the autoload rather than in Main.tscn, so
|
||||
# tearing Main.tscn down must not touch it.
|
||||
# Fly off the mothership first, or this new cargo is (correctly)
|
||||
# banked the moment BuildingField next runs, and the teardown
|
||||
# check below would be measuring a delivery instead.
|
||||
_park_fleet_on(CRAFTER_AT)
|
||||
game_state.collect_cargo(5)
|
||||
scene.queue_free()
|
||||
elapsed = 0.0
|
||||
|
|
|
|||
|
|
@ -14,9 +14,10 @@ extends SceneTree
|
|||
# Verifies:
|
||||
# 1. withdrawing moves ore bank -> hold, and conserves the total
|
||||
# 2. withdrawing is capped by what is actually banked (no overdraft)
|
||||
# 3. arriving at the mothership banks the hold, but doing so is EDGE
|
||||
# triggered - ore withdrawn while parked there is not instantly taken
|
||||
# back, which would make the button look broken
|
||||
# 3. arriving at the mothership does NOT bank the hold. It used to, edge
|
||||
# triggered on arrival (#14); #24 removed that entirely, because an
|
||||
# automatic one-way move overrides a player who has just loaded the
|
||||
# hold in order to carry ore somewhere else
|
||||
# 4. a building takes the fleet's whole hold when opened
|
||||
# 5. end to end: a queued order the bank cannot reach is fulfilled using
|
||||
# only the fleet, with no Transport Hub in existence
|
||||
|
|
@ -28,6 +29,7 @@ var fleet: Node
|
|||
var field: Node
|
||||
var controller: Node
|
||||
var crafter_id: int = 0
|
||||
var mothership_id: int = 0
|
||||
var stage: int = 0
|
||||
var elapsed: float = 0.0
|
||||
var ships_before: int = 0
|
||||
|
|
@ -74,6 +76,7 @@ func _process(delta: float) -> bool:
|
|||
_clear_hazards()
|
||||
fleet.spawn_ships(2, MOTHERSHIP_AT)
|
||||
field.place(GameState.BUILDING_MOTHERSHIP, MOTHERSHIP_AT)
|
||||
mothership_id = int(game_state.buildings[-1].id)
|
||||
field.place(GameState.BUILDING_SHIP_CRAFTER, CRAFTER_AT)
|
||||
crafter_id = int(game_state.buildings[-1].id)
|
||||
total_before = _total()
|
||||
|
|
@ -107,7 +110,7 @@ func _process(delta: float) -> bool:
|
|||
if elapsed < 0.3:
|
||||
return false
|
||||
var bank_before: int = game_state.rocks
|
||||
var took: int = game_state.withdraw_to_fleet(GameState.SHIP_COST)
|
||||
var took: int = game_state.transfer_to_fleet(mothership_id, GameState.SHIP_COST)
|
||||
if took != GameState.SHIP_COST or game_state.fleet_cargo != GameState.SHIP_COST:
|
||||
print("TEST_MANUAL_SUPPLY_ROUTE: FAIL (withdrew %d into a hold of %d)" % [took, game_state.fleet_cargo])
|
||||
quit(1)
|
||||
|
|
@ -123,14 +126,22 @@ func _process(delta: float) -> bool:
|
|||
stage = 4
|
||||
return false
|
||||
4:
|
||||
# --- 3. still parked on the mothership ----------------------
|
||||
# --- 3. parked on the mothership, and arriving again --------
|
||||
if elapsed < 0.5:
|
||||
return false
|
||||
if game_state.fleet_cargo != GameState.SHIP_COST:
|
||||
print("TEST_MANUAL_SUPPLY_ROUTE: FAIL (ore withdrawn while parked on the mothership was instantly banked again - delivery is not edge triggered)")
|
||||
print("TEST_MANUAL_SUPPLY_ROUTE: FAIL (ore withdrawn while parked on the mothership was banked again by itself)")
|
||||
quit(1)
|
||||
return true
|
||||
print("still parked -> hold kept its %d (delivery is edge triggered)" % game_state.fleet_cargo)
|
||||
# Leaving and coming back is the case the old edge-triggered
|
||||
# delivery fired on, so it is the one worth re-entering.
|
||||
_park_fleet_on(CRAFTER_AT)
|
||||
_park_fleet_on(MOTHERSHIP_AT)
|
||||
if game_state.fleet_cargo != GameState.SHIP_COST:
|
||||
print("TEST_MANUAL_SUPPLY_ROUTE: FAIL (arriving at the mothership auto-banked the hold - #24 removed that)")
|
||||
quit(1)
|
||||
return true
|
||||
print("hold kept its %d across an arrival (no auto-banking)" % game_state.fleet_cargo)
|
||||
|
||||
# --- 4. carry it to the crafter and open it -----------------
|
||||
_park_fleet_on(CRAFTER_AT)
|
||||
|
|
@ -140,7 +151,7 @@ func _process(delta: float) -> bool:
|
|||
5:
|
||||
if elapsed < 0.6:
|
||||
return false
|
||||
var moved: int = game_state.unload_into_building(crafter_id)
|
||||
var moved: int = game_state.transfer_to_building(crafter_id, game_state.fleet_cargo)
|
||||
if moved != GameState.SHIP_COST or game_state.fleet_cargo != 0:
|
||||
print("TEST_MANUAL_SUPPLY_ROUTE: FAIL (unloaded %d, hold left at %d)" % [moved, game_state.fleet_cargo])
|
||||
quit(1)
|
||||
|
|
@ -180,7 +191,7 @@ func _process(delta: float) -> bool:
|
|||
|
||||
# --- 2. no overdraft ----------------------------------------
|
||||
game_state.rocks = 3
|
||||
var over: int = game_state.withdraw_to_fleet(999)
|
||||
var over: int = game_state.transfer_to_fleet(mothership_id, 999)
|
||||
if over != 3 or game_state.rocks != 0:
|
||||
print("TEST_MANUAL_SUPPLY_ROUTE: FAIL (overdrew: took %d from a bank of 3, leaving %d)" % [over, game_state.rocks])
|
||||
quit(1)
|
||||
|
|
|
|||
219
tests/test_resource_transfer.gd
Normal file
219
tests/test_resource_transfer.gd
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
extends SceneTree
|
||||
|
||||
# Headless gameplay test: run with
|
||||
# godot --headless -s res://tests/test_resource_transfer.gd
|
||||
# Forgejo #24: moving ore between the fleet and a building is its own
|
||||
# top-level action, not something buried inside the ship crafter or the
|
||||
# research screen.
|
||||
#
|
||||
# Verifies:
|
||||
# 1. the transfer button appears alongside the action button whenever the
|
||||
# fleet is standing on a building, and opens the transfer panel bound to
|
||||
# THAT building - the `== 0` guard failure that made every crafter
|
||||
# button look dead is the same trap here
|
||||
# 2. ore moves both ways at an ordinary building, capped by what is
|
||||
# actually at each end (no overdraft in either direction)
|
||||
# 3. at a mothership the building's holdings ARE the bank, so unloading
|
||||
# there banks the ore and loading there draws on it
|
||||
# 4. a REAL tap through the input path moves ore, not just the handler
|
||||
# 5. pressing against an empty end reports why instead of doing nothing
|
||||
# 6. the shared resource bar reports the building the panel was opened from
|
||||
# 7. ore is conserved across the whole exercise
|
||||
|
||||
var game_state: Node
|
||||
var scene: Node
|
||||
var fleet: Node
|
||||
var controller: Node
|
||||
var crafter_id: int = 0
|
||||
var mothership_id: int = 0
|
||||
var stage: int = 0
|
||||
var elapsed: float = 0.0
|
||||
var total_before: int = 0
|
||||
var failures: PackedStringArray = PackedStringArray()
|
||||
|
||||
const MOTHERSHIP_AT: Vector2 = Vector2(0.0, 0.0)
|
||||
const CRAFTER_AT: Vector2 = Vector2(3000.0, 0.0)
|
||||
|
||||
|
||||
func _total() -> int:
|
||||
var sum: int = game_state.rocks + game_state.fleet_cargo
|
||||
for b in game_state.buildings:
|
||||
if b.type != GameState.BUILDING_MOTHERSHIP:
|
||||
sum += int(b.get("stock", 0))
|
||||
return sum
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
game_state = root.get_node("GameState")
|
||||
game_state.buildings.clear()
|
||||
game_state.extractors.clear()
|
||||
game_state.transporters.clear()
|
||||
game_state.rocks = 100
|
||||
game_state.fleet_cargo = 0
|
||||
game_state.vessels_owed.clear()
|
||||
game_state.unlocked = ["shipyards", "coordination", "wing", "squadron", "armada"]
|
||||
scene = load("res://Main.tscn").instantiate()
|
||||
root.add_child(scene)
|
||||
fleet = scene.get_node("Fleet")
|
||||
controller = scene.get_node("BuildController")
|
||||
|
||||
|
||||
func _park_fleet_on(at: Vector2) -> void:
|
||||
for ship in fleet.ships:
|
||||
ship.position = at
|
||||
fleet.move_to(at)
|
||||
|
||||
|
||||
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 _expect(condition: bool, message: String) -> void:
|
||||
if not condition:
|
||||
failures.append("stage %d: %s" % [stage, message])
|
||||
|
||||
|
||||
func _process(delta: float) -> bool:
|
||||
elapsed += delta
|
||||
match stage:
|
||||
0:
|
||||
scene.get_node("AsteroidField").set_process(false)
|
||||
scene.get_node("EnemySpawner").set_process(false)
|
||||
_clear_hazards()
|
||||
fleet.spawn_ships(2, MOTHERSHIP_AT)
|
||||
var field: Node = scene.get_node("BuildingField")
|
||||
field.place(GameState.BUILDING_MOTHERSHIP, MOTHERSHIP_AT)
|
||||
mothership_id = int(game_state.buildings[-1].id)
|
||||
field.place(GameState.BUILDING_SHIP_CRAFTER, CRAFTER_AT)
|
||||
crafter_id = int(game_state.buildings[-1].id)
|
||||
total_before = _total()
|
||||
_park_fleet_on(CRAFTER_AT)
|
||||
elapsed = 0.0
|
||||
stage = 1
|
||||
return false
|
||||
|
||||
1:
|
||||
# --- 1. the button appears, and binds to this building ------
|
||||
if elapsed < 1.2:
|
||||
return false
|
||||
_expect(controller.current_building != null,
|
||||
"parking on the crafter never registered")
|
||||
_expect(controller.transfer_button.visible,
|
||||
"the transfer button did not appear at a building")
|
||||
_tap(controller.transfer_button)
|
||||
_expect(controller.transfer_menu.visible,
|
||||
"tapping the transfer button did not open the panel")
|
||||
_expect(controller.transfer_building_id == crafter_id,
|
||||
"panel bound to building %d, crafter is %d"
|
||||
% [controller.transfer_building_id, crafter_id])
|
||||
|
||||
# --- 6. the shared bar follows the panel --------------------
|
||||
for bar in controller.resource_bars:
|
||||
_expect(bar.building_id == crafter_id,
|
||||
"a resource bar is reporting building %d, not the crafter %d"
|
||||
% [bar.building_id, crafter_id])
|
||||
|
||||
# --- 5. an empty hold explains itself -----------------------
|
||||
game_state.fleet_cargo = 0
|
||||
controller._on_transfer(true, GameState.CARGO_LOAD_BATCH)
|
||||
_expect("carrying nothing" in controller.transfer_status.text,
|
||||
"unloading an empty hold said: %s" % controller.transfer_status.text)
|
||||
|
||||
# --- 2. both directions at an ordinary building -------------
|
||||
# Baseline taken AFTER the last hand-set figure: the conservation
|
||||
# check at the end is about the transfers, and ore conjured into
|
||||
# the hold to set the scene would otherwise read as a leak.
|
||||
game_state.fleet_cargo = 40
|
||||
total_before = _total()
|
||||
controller._on_transfer(true, GameState.CARGO_LOAD_BATCH)
|
||||
_expect(game_state.building_stock(crafter_id) == GameState.CARGO_LOAD_BATCH,
|
||||
"a batch into the crafter left stock at %d"
|
||||
% game_state.building_stock(crafter_id))
|
||||
_expect(game_state.fleet_cargo == 40 - GameState.CARGO_LOAD_BATCH,
|
||||
"the hold paid %d, not the batch" % (40 - game_state.fleet_cargo))
|
||||
|
||||
controller._on_transfer(false, 999)
|
||||
_expect(game_state.building_stock(crafter_id) == 0,
|
||||
"loading ALL left %d in the crafter" % game_state.building_stock(crafter_id))
|
||||
_expect(game_state.fleet_cargo == 40,
|
||||
"the hold came back to %d, not 40" % game_state.fleet_cargo)
|
||||
|
||||
# Asking for more than exists moves only what is there.
|
||||
var over: int = game_state.transfer_to_fleet(crafter_id, 999)
|
||||
_expect(over == 0, "overdrew %d from an empty crafter" % over)
|
||||
|
||||
_park_fleet_on(MOTHERSHIP_AT)
|
||||
controller.transfer_menu.visible = false
|
||||
elapsed = 0.0
|
||||
stage = 2
|
||||
return false
|
||||
|
||||
2:
|
||||
# --- 3. the mothership's holdings are the bank --------------
|
||||
if elapsed < 1.2:
|
||||
return false
|
||||
_expect(controller.transfer_button.visible,
|
||||
"the transfer button did not appear at the mothership")
|
||||
_tap(controller.transfer_button)
|
||||
_expect(controller.transfer_building_id == mothership_id,
|
||||
"panel bound to %d, mothership is %d"
|
||||
% [controller.transfer_building_id, mothership_id])
|
||||
_expect(game_state.building_holdings(mothership_id) == game_state.rocks,
|
||||
"mothership holdings %d != bank %d"
|
||||
% [game_state.building_holdings(mothership_id), game_state.rocks])
|
||||
|
||||
var bank_before: int = game_state.rocks
|
||||
var hold_before: int = game_state.fleet_cargo
|
||||
controller._on_transfer(true, 999)
|
||||
_expect(game_state.fleet_cargo == 0,
|
||||
"unloading ALL at the mothership left %d in the hold" % game_state.fleet_cargo)
|
||||
_expect(game_state.rocks == bank_before + hold_before,
|
||||
"the bank went to %d, expected %d" % [game_state.rocks, bank_before + hold_before])
|
||||
|
||||
# --- 4. a REAL tap, through the input path ------------------
|
||||
# The handler working while the BUTTON is inert is the exact bug
|
||||
# class this project keeps hitting, so one press goes the long way.
|
||||
var bank_at_tap: int = game_state.rocks
|
||||
_tap(scene.get_node("UI/TransferMenu/ToFleetBatchButton"))
|
||||
_expect(game_state.fleet_cargo == GameState.CARGO_LOAD_BATCH,
|
||||
"a real tap loaded %d, expected a batch of %d"
|
||||
% [game_state.fleet_cargo, GameState.CARGO_LOAD_BATCH])
|
||||
_expect(game_state.rocks == bank_at_tap - GameState.CARGO_LOAD_BATCH,
|
||||
"the bank did not pay for the tapped withdrawal")
|
||||
|
||||
# --- 7. conservation ----------------------------------------
|
||||
_expect(_total() == total_before,
|
||||
"ore created or destroyed: %d -> %d" % [total_before, _total()])
|
||||
|
||||
_finish()
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _finish() -> void:
|
||||
if failures.is_empty():
|
||||
print("TEST_RESOURCE_TRANSFER: PASS")
|
||||
quit(0)
|
||||
else:
|
||||
print("bank=%d hold=%d crafter=%d" % [
|
||||
game_state.rocks, game_state.fleet_cargo, game_state.building_stock(crafter_id)
|
||||
])
|
||||
for f in failures:
|
||||
print("FAIL: %s" % f)
|
||||
print("TEST_RESOURCE_TRANSFER: FAIL")
|
||||
quit(1)
|
||||
|
||||
|
||||
func _clear_hazards() -> void:
|
||||
# Drifting rocks can destroy a parked ship and change the fleet size out
|
||||
# from under a logistics test. Same reason as test_manual_supply_route.
|
||||
for a in get_nodes_in_group("asteroids"):
|
||||
if is_instance_valid(a):
|
||||
a.queue_free()
|
||||
for e in get_nodes_in_group("enemies"):
|
||||
if is_instance_valid(e):
|
||||
e.queue_free()
|
||||
1
tests/test_resource_transfer.gd.uid
Normal file
1
tests/test_resource_transfer.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://j27f3sjneewj
|
||||
Loading…
Reference in a new issue