#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
153 lines
6.3 KiB
GDScript
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
|