spaceship/tests/test_fleet_cargo_limit.gd
Loïc Wisniewski 1f5fecd371 Buy fleet berths with Fleet Offices
#219: fleet capacity is a built, supplied thing rather than a researched
one. The eight fleet-size doctrines are gone; capacity is one berth free
plus one per Fleet Office standing. An office charges every 25s for the
vessel in its berth, priced off that vessel's own build cost so an expensive
hull is expensive to keep, and drawn from its own store the way a gas burner
draws gas. An office that cannot pay goes dark, its vessel is scuttled, and
the player is told through the same queue quest lines use - which reaches
them on a planet surface too.

An empty berth is idle rather than starved: an office is only failing while
there is something in it to fail, and holding it disabled would leave a
berth that could never be crewed and so could never be paid for again.

Ships have no hit points and a shield absorbs a hit wholesale, so the lost
vessel is scuttled rather than damaged - otherwise a shielded hull would
survive its own eviction.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
2026-09-05 17:09:27 +02:00

153 lines
6.3 KiB
GDScript

extends SceneTree
# Headless gameplay test: run with
# godot --headless -s res://tests/test_fleet_cargo_limit.gd
# Forgejo #56: the fleet hold has a finite, composition-driven capacity
# instead of being unbounded.
#
# Covers:
# A. capacity is data-driven: only vessels with the "cargo" capability
# contribute their cargo_capacity stat - combat specialists (shield,
# gas bomber) contribute zero
# B. a lone interceptor cannot carry more than its capacity - a further
# Rock pickup is refused and the rock is not consumed
# C. losing a cargo-capable ship lowers the ceiling immediately without
# deleting cargo already held (clamps on the next deposit instead)
# D. a transfer into a full hold clamps to the remaining space and
# reports the amount actually moved
# E. capacity survives the space -> surface scene swap
var game_state: Node
var scene: Node
var fleet: Node
var stage: int = 0
var elapsed: float = 0.0
var mothership_id: int = 0
var rock: Node
func _initialize() -> void:
game_state = root.get_node("GameState")
game_state.buildings.clear()
game_state.extractors.clear()
game_state.rocks = 500
game_state.fleet_cargo = 0
mothership_id = game_state.add_building(GameState.BUILDING_MOTHERSHIP, Vector2.ZERO)
# #219: fleet capacity is built now - one berth free, one per Fleet
# Office. This test flies more than one vessel, so it needs the offices
# standing before the fleet spawns into them.
load("res://tests/_berths.gd").grant(root.get_node("GameState"), 6)
scene = load("res://world/Main.tscn").instantiate()
root.add_child(scene)
current_scene = scene
func _process(delta: float) -> bool:
elapsed += delta
match stage:
0:
if elapsed < 0.3:
return false
scene.get_node("AsteroidField").set_process(false)
scene.get_node("EnemySpawner").set_process(false)
fleet = scene.get_node("Fleet")
# --- A. capacity is data-driven ---------------------------------
fleet.spawn_ships(1, Vector2.ZERO, PackedStringArray(["interceptor"]))
if game_state.fleet_cargo_total_capacity() != 20:
return _fail("one interceptor: capacity %d, expected 20" % game_state.fleet_cargo_total_capacity())
fleet.spawn_ships(2, Vector2.ZERO, PackedStringArray(["shield", "gas_bomber"]))
if game_state.fleet_cargo_total_capacity() != 0:
return _fail("shield+gas_bomber: capacity %d, expected 0" % game_state.fleet_cargo_total_capacity())
print("capacity is data-driven: interceptor=20, shield+gas_bomber=0")
# --- B. a full hold refuses a pickup, the rock stays ------------
fleet.spawn_ships(1, Vector2.ZERO, PackedStringArray(["interceptor"]))
game_state.fleet_cargo = 20
rock = load("res://surface/Rock.tscn").instantiate()
rock.build_shape(1)
rock.global_position = fleet.ships[0].global_position
scene.add_child(rock)
elapsed = 0.0
stage = 1
return false
1:
if elapsed < 0.3:
return false
if game_state.fleet_cargo != 20:
return _fail("a full hold still absorbed a pickup: hold=%d" % game_state.fleet_cargo)
if not is_instance_valid(rock):
return _fail("the rock was consumed even though the hold was full")
print("full hold (20/20) refused a pickup, the rock remains")
# --- C. losing a cargo ship drops the ceiling, keeps the cargo --
# Mirrors Ship.gd's own death sequence (destroyed.emit() then
# queue_free()) rather than just calling the handler, so no
# untracked ghost ship is left sitting in the scene.
var dying_ship: Node = fleet.ships[0]
fleet._on_ship_destroyed(dying_ship)
dying_ship.queue_free()
if game_state.fleet_cargo_total_capacity() != 0:
return _fail("empty fleet: capacity %d, expected 0" % game_state.fleet_cargo_total_capacity())
if game_state.fleet_cargo != 20:
return _fail("existing cargo was deleted when capacity dropped below it: %d" % game_state.fleet_cargo)
print("losing the ship: capacity=0, existing cargo untouched at %d" % game_state.fleet_cargo)
if game_state.collect_cargo(5) != 0:
return _fail("an over-capacity hold accepted a further deposit")
print("over-capacity hold clamps further deposits to zero, as documented")
# --- D. a transfer into a hold with partial room clamps ---------
fleet.spawn_ships(2, Vector2.ZERO, PackedStringArray(["interceptor", "interceptor"]))
if game_state.fleet_cargo_total_capacity() != 40:
return _fail("two interceptors: capacity %d, expected 40" % game_state.fleet_cargo_total_capacity())
var bank_before: int = game_state.rocks
var moved: int = game_state.transfer_resource_to_fleet(mothership_id, GameState.RESOURCE_ORE, 100)
if moved != 20:
return _fail("transfer into 20/40 room reported moved=%d, expected 20" % moved)
if game_state.fleet_cargo != 40:
return _fail("hold after the clamped transfer is %d, expected 40 (full)" % game_state.fleet_cargo)
if game_state.rocks != bank_before - 20:
return _fail("bank dropped by %d, expected exactly the 20 that moved" % (bank_before - game_state.rocks))
print("transfer into 20/40 room clamped to 20, hold now full, bank debited exactly 20")
# --- E. capacity survives the space -> surface scene swap -------
game_state.surviving_ship_count = 2
game_state.surviving_vessel_types = PackedStringArray(["interceptor", "interceptor"])
game_state.planet_id = "test_fleet_cargo_limit"
game_state.planet_name = "Cargo Limit Test"
scene.queue_free()
elapsed = 0.0
stage = 2
return false
2:
# A tick for queue_free() to actually leave the tree before adding
# the surface scene, matching the shape of every other scene-swap
# test in this suite.
if elapsed < 0.3:
return false
scene = load("res://surface/PlanetSurface.tscn").instantiate()
root.add_child(scene)
current_scene = scene
elapsed = 0.0
stage = 3
return false
3:
if elapsed < 0.3:
return false
if game_state.fleet_cargo_total_capacity() != 40:
return _fail("capacity after the surface restore is %d, expected 40" % game_state.fleet_cargo_total_capacity())
if game_state.fleet_cargo != 40:
return _fail("cargo did not survive the scene swap: %d, expected 40" % game_state.fleet_cargo)
print("capacity (40) and cargo (40) both survived the space -> surface swap")
print("TEST_FLEET_CARGO_LIMIT: PASS")
quit(0)
return true
return false
func _fail(message: String) -> bool:
print("TEST_FLEET_CARGO_LIMIT: FAIL (%s)" % message)
quit(1)
return true