spaceship/tests/test_building_stock_queue.gd
Loïc Wisniewski 2f161621c3 #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

256 lines
9.2 KiB
GDScript

extends SceneTree
# Headless gameplay test: run with
# godot --headless -s res://tests/test_building_stock_queue.gd
# 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.
#
# 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
# 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
# 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
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()
game_state.stored_vessels.clear()
game_state.rocks = 500
game_state.fleet_cargo = 0
# 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", "wing", "squadron", "armada"]
scene = load("res://world/Main.tscn").instantiate()
root.add_child(scene)
fleet = scene.get_node("Fleet")
field = scene.get_node("BuildingField")
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
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
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
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
])
# --- 3. topping it up starts the build ----------------------
game_state.deposit_to_building(crafter_id, 1)
elapsed = 0.0
stage = 4
return false
4:
# 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:
print("TEST_BUILDING_STOCK_QUEUE: FAIL (build never finished)")
quit(1)
return true
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
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
print("built -> 1 vessel waiting in the hangar, fleet untouched at %d" % fleet.ships.size())
# --- 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)
elapsed = 0.0
stage = 6
return false
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
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
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")
# --- 6 + 7. no scene, no fleet ------------------------------
stored_at_start = game_state.stored_vessels.size()
scene.queue_free()
elapsed = 0.0
stage = 8
return false
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
stage = 9
return false
9:
if elapsed < _build_time() * 3.0:
return false
# 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:
print("TEST_BUILDING_STOCK_QUEUE: FAIL (build stalled once the space scene was gone)")
quit(1)
return true
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
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()