spaceship/tests/test_building_stock_queue.gd

256 lines
9.2 KiB
GDScript3
Raw Permalink Normal View History

extends SceneTree
# Headless gameplay test: run with
# godot --headless -s res://tests/test_building_stock_queue.gd
2026-08-12 22:47:46 +02:00
# Forgejo #16 (part 3 of #12), updated for #23: a building produces out of
# its OWN stock, an order it cannot pay for yet is queued rather than
# refused, and building now TAKES TIME.
#
2026-08-12 22:47:46 +02:00
# What #23 changed here: orders no longer complete the instant the ore
# lands. The head of the queue consumes its ore, builds for build_time, and
# only then finishes - and it finishes into the mothership hangar rather
# than straight into the fleet. So a queue is something the player can watch
# and cancel, and a full fleet no longer stops a crafter working.
#
# Verifies:
# 1. ordering with an empty stock queues instead of failing, and produces
# nothing yet
# 2. it does NOT quietly take the cost out of the bank - the whole point is
# that the building has to be supplied
2026-08-12 22:47:46 +02:00
# 3. delivering enough stock starts the build, and it completes on its own
# 4. orders build ONE AT A TIME in order, not all at once the moment a big
# delivery covers them
# 5. a part-delivery is not enough: stock below the cost leaves the order
# queued and the ore untouched
2026-08-12 22:47:46 +02:00
# 6. stock and queue survive the space scene being torn down
# 7. a build completes with NO fleet in existence, into the hangar, and
# waits there - it is not lost, and not silently crewed either
var game_state: Node
var scene: Node
var fleet: Node
var field: Node
var crafter_id: int = 0
var stage: int = 0
var elapsed: float = 0.0
var ships_at_start: int = 0
var bank_at_start: int = 0
2026-08-12 22:47:46 +02:00
var stored_at_start: int = 0
var ticks: int = 0
const CRAFTER_AT: Vector2 = Vector2(900.0, 0.0)
func _initialize() -> void:
game_state = root.get_node("GameState")
game_state.buildings.clear()
game_state.extractors.clear()
2026-08-12 22:47:46 +02:00
game_state.stored_vessels.clear()
game_state.rocks = 500
game_state.fleet_cargo = 0
2026-08-12 22:47:46 +02:00
# Real build times are 10s+; the timed path is what matters here, not the
# wall clock. See GameState.build_time_scale.
game_state.build_time_scale = 0.02
game_state.unlocked = ["shipyards", "coordination"]
#84: move 215 root files into a folder hierarchy Everything lived in one flat directory, so a diff gave no clue what area it touched and finding the file you wanted meant knowing its exact name. Twelve folders now, grouped by what a thing IS rather than what type it is - a vessel's definition, its component scenes and its upgrade resources sit together, because that is the set you read when you change one of them. buildings core controllers enemies logistics planets resources shaders surface ui vessels vfx world 332 files moved and every res:// reference rewritten - 306 files touched, including the autoload and main-scene paths in project.godot. Nothing renamed: this is purely where files sit, so the diff is a pile of pure moves and no behaviour is buried in it. ui_theme.tres, icon.svg and web_shell.html stay at the root, where Godot and the export preset expect them. Verified three ways, because a suite alone would not have caught all of it: the full 117 pass, the web export builds clean, and the game renders. The export is why that matters. The FIRST export after the move failed with "Attempt to open script 'res://GameState.gd'" - the old path - while project.godot on disk plainly said res://core/GameState.gd. That is this project's documented Android FUSE mtime trap: Godot served a cached copy of a file whose content had changed. A `touch` fixed it. Nothing in the test suite would have found that, because the suite runs from a copy on a native filesystem - the one place the bug cannot happen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 19:44:14 +02:00
scene = load("res://world/Main.tscn").instantiate()
root.add_child(scene)
fleet = scene.get_node("Fleet")
field = scene.get_node("BuildingField")
2026-08-12 22:47:46 +02:00
func _build_time() -> float:
return game_state.build_time(GameState.PRODUCT_SHIP)
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, Vector2.ZERO)
field.place(GameState.BUILDING_SHIP_CRAFTER, CRAFTER_AT)
crafter_id = int(game_state.buildings[-1].id)
ships_at_start = fleet.ships.size()
bank_at_start = game_state.rocks
elapsed = 0.0
stage = 1
return false
1:
if elapsed < 0.2:
return false
# --- 1 + 2. an unaffordable order queues, and costs nothing --
if not game_state.queue_order(crafter_id):
print("TEST_BUILDING_STOCK_QUEUE: FAIL (the crafter refused an order outright)")
quit(1)
return true
elapsed = 0.0
stage = 2
return false
2:
if elapsed < 0.3:
return false
if game_state.pending_orders(crafter_id) != 1:
print("TEST_BUILDING_STOCK_QUEUE: FAIL (order did not stay queued: %d pending)" % game_state.pending_orders(crafter_id))
quit(1)
return true
2026-08-12 22:47:46 +02:00
if not game_state.stored_vessels.is_empty():
print("TEST_BUILDING_STOCK_QUEUE: FAIL (a vessel appeared with an empty stock)")
quit(1)
return true
if game_state.rocks != bank_at_start:
print("TEST_BUILDING_STOCK_QUEUE: FAIL (queueing took %d out of the bank - the building is supposed to be supplied)" % (bank_at_start - game_state.rocks))
quit(1)
return true
2026-08-12 22:47:46 +02:00
print("empty stock -> queued=%d stored=%d bank=%d (untouched)" % [
game_state.pending_orders(crafter_id), game_state.stored_vessels.size(), game_state.rocks
])
# --- 5. a part-delivery is not enough -----------------------
game_state.deposit_to_building(crafter_id, GameState.SHIP_COST - 1)
elapsed = 0.0
stage = 3
return false
3:
if elapsed < 0.3:
return false
2026-08-12 22:47:46 +02:00
if game_state.pending_orders(crafter_id) != 1 or not game_state.stored_vessels.is_empty():
print("TEST_BUILDING_STOCK_QUEUE: FAIL (produced on a part-delivery of %d against a cost of %d)" % [
GameState.SHIP_COST - 1, GameState.SHIP_COST
])
quit(1)
return true
if game_state.building_stock(crafter_id) != GameState.SHIP_COST - 1:
print("TEST_BUILDING_STOCK_QUEUE: FAIL (part-delivered ore was consumed anyway)")
quit(1)
return true
print("part delivery (%d of %d) -> still queued, ore intact" % [
game_state.building_stock(crafter_id), GameState.SHIP_COST
])
2026-08-12 22:47:46 +02:00
# --- 3. topping it up starts the build ----------------------
game_state.deposit_to_building(crafter_id, 1)
elapsed = 0.0
stage = 4
return false
4:
2026-08-12 22:47:46 +02:00
# One tick: the build must have STARTED (ore taken) but not yet
# finished, or "takes time" means nothing.
ticks += 1
if ticks < 2:
return false
var queue: Array = game_state.order_list(crafter_id)
if queue.is_empty() or not bool(queue[0].started):
print("TEST_BUILDING_STOCK_QUEUE: FAIL (a fully funded order never started building)")
quit(1)
return true
if game_state.building_stock(crafter_id) != 0:
print("TEST_BUILDING_STOCK_QUEUE: FAIL (starting the build did not consume the ore)")
quit(1)
return true
if not game_state.stored_vessels.is_empty():
print("TEST_BUILDING_STOCK_QUEUE: FAIL (the vessel appeared instantly - builds are supposed to take time)")
quit(1)
return true
print("funded -> building started, ore consumed, nothing delivered yet")
elapsed = 0.0
stage = 5
return false
5:
if elapsed < _build_time() * 2.0:
return false
if game_state.pending_orders(crafter_id) != 0:
2026-08-12 22:47:46 +02:00
print("TEST_BUILDING_STOCK_QUEUE: FAIL (build never finished)")
quit(1)
return true
2026-08-12 22:47:46 +02:00
if game_state.stored_vessels.size() != 1:
print("TEST_BUILDING_STOCK_QUEUE: FAIL (finished build did not reach the hangar: %d stored)" % game_state.stored_vessels.size())
quit(1)
return true
2026-08-12 22:47:46 +02:00
if fleet.ships.size() != ships_at_start:
print("TEST_BUILDING_STOCK_QUEUE: FAIL (the vessel crewed itself - the player is supposed to pick it up)")
quit(1)
return true
2026-08-12 22:47:46 +02:00
print("built -> 1 vessel waiting in the hangar, fleet untouched at %d" % fleet.ships.size())
2026-08-12 22:47:46 +02:00
# --- 4. one at a time, in order -----------------------------
stored_at_start = game_state.stored_vessels.size()
game_state.queue_order(crafter_id)
game_state.queue_order(crafter_id)
game_state.queue_order(crafter_id)
game_state.deposit_to_building(crafter_id, GameState.SHIP_COST * 3)
2026-08-12 22:47:46 +02:00
elapsed = 0.0
stage = 6
return false
2026-08-12 22:47:46 +02:00
6:
# Enough time for exactly one build, and no more. Funding all
# three at once must NOT finish all three at once - that is the
# behaviour #23 replaced.
if elapsed < _build_time() * 1.4:
return false
2026-08-12 22:47:46 +02:00
var done: int = game_state.stored_vessels.size() - stored_at_start
if done != 1:
print("TEST_BUILDING_STOCK_QUEUE: FAIL (after one build time, %d of 3 orders finished - they are not building one at a time)" % done)
quit(1)
return true
2026-08-12 22:47:46 +02:00
if game_state.pending_orders(crafter_id) != 2:
print("TEST_BUILDING_STOCK_QUEUE: FAIL (%d orders left, expected 2 still queued)" % game_state.pending_orders(crafter_id))
quit(1)
return true
print("one build time -> 1 of 3 done, 2 still queued (sequential)")
elapsed = 0.0
stage = 7
return false
7:
if elapsed < _build_time() * 4.0:
return false
if game_state.stored_vessels.size() - stored_at_start != 3:
print("TEST_BUILDING_STOCK_QUEUE: FAIL (the rest of the queue never drained: %d of 3)" % (game_state.stored_vessels.size() - stored_at_start))
quit(1)
return true
print("queue drained in order -> 3 vessels waiting")
2026-08-12 22:47:46 +02:00
# --- 6 + 7. no scene, no fleet ------------------------------
stored_at_start = game_state.stored_vessels.size()
scene.queue_free()
elapsed = 0.0
2026-08-12 22:47:46 +02:00
stage = 8
return false
2026-08-12 22:47:46 +02:00
8:
if elapsed < 0.3:
return false
if is_instance_valid(fleet):
return false
game_state.queue_order(crafter_id)
game_state.deposit_to_building(crafter_id, GameState.SHIP_COST)
elapsed = 0.0
2026-08-12 22:47:46 +02:00
stage = 9
return false
2026-08-12 22:47:46 +02:00
9:
if elapsed < _build_time() * 3.0:
return false
2026-08-12 22:47:46 +02:00
# The queue, the stock and the hangar live in the autoload, so
# tearing the space scene down must not touch them.
if game_state.pending_orders(crafter_id) != 0:
2026-08-12 22:47:46 +02:00
print("TEST_BUILDING_STOCK_QUEUE: FAIL (build stalled once the space scene was gone)")
quit(1)
return true
2026-08-12 22:47:46 +02:00
if game_state.stored_vessels.size() != stored_at_start + 1:
print("TEST_BUILDING_STOCK_QUEUE: FAIL (a vessel built with no fleet present was lost)")
quit(1)
return true
2026-08-12 22:47:46 +02:00
print("no scene at all -> build completed into the hangar (%d waiting)" % game_state.stored_vessels.size())
print("TEST_BUILDING_STOCK_QUEUE: PASS")
quit(0)
return true
return false
func _clear_hazards() -> void:
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()