spaceship/tests/test_gas_burner.gd
Loïc Wisniewski 265e121cb0 Build the logistics crews, and let them burn energy
#216: transport hubs and spacial workshops no longer conjure their crews.
Transporters and repair drones are ordered at a ship crafter, assigned per
building through a new Station menu, flown out from the mothership hangar,
and spent by the work they do - 20 runs, 12 repairs, counted where the run
ENDS so nothing retires with cargo in the air or a building half repaired.
A spent vessel is gone, so the structure is short again and the player has
to build another. The menu shows what each one has left, worst first.

Craftability needed its own flag: "the crafter can make it" and "it can take
a berth in the fleet" stopped being the same question.

#217: transporters crawl at 120 rather than 150, and a Burn Drive lets them
carry energy and spend it to travel far faster. Fetching it is a leg of its
own, to the nearest store that has any - so a refuelling transporter cannot
be dispatched mid-errand - and the leg flown to COLLECT fuel is not charged
for it. Unresearched, nothing detours and nothing changes.

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

174 lines
7.7 KiB
GDScript

extends SceneTree
# Headless gameplay test: run with
# godot --headless -s res://tests/test_gas_burner.gd
# Forgejo #54: gas burner building + the energy resource, stored/moved
# exactly like ore and gas.
#
# Modeled on tests/test_extractor_income.gd's shape: pure GameState state,
# no scene loaded, poking buildings/inventories directly and waiting out
# real production/transporter timing with generous margins rather than
# fixed frame counts (SceneTree physics can run ahead of _process(), trap 5
# in the repo notes).
#
# Covers:
# A. energy is fully generic - in resource_types(), has its own
# ResourceDefinition/glyph/colour - with no per-resource code anywhere
# (ResourceBar/ResourceTransferList already iterate resource_types())
# B. a burner with local gas converts it to energy, into its OWN
# inventory, at the configured rate; with no gas it stalls and never
# touches the bank
# C. the building is gated behind its own research node
# D. a hub transporter both delivers gas to a burner and collects its
# energy, following the existing supply/pickup dispatch paths
var game_state: Node
var stage: int = 0
var elapsed: float = 0.0
var mothership_id: int = 0
var hub_id: int = 0
var burner_id: int = 0
var load_limit: int = 0
var energy_before_stall: int = 0
var conversion_rate: float = 0.0
var conversion_yield: int = 0
const MOTHERSHIP_AT := Vector2.ZERO
const HUB_AT := Vector2(150.0, 0.0)
const BURNER_AT := Vector2(400.0, 0.0)
func _initialize() -> void:
game_state = root.get_node("GameState")
game_state.rocks = 0
game_state.gas = 0
game_state.buildings.clear()
game_state.extractors.clear()
game_state.transporters.clear()
game_state.reset_progression()
mothership_id = game_state.add_building(GameState.BUILDING_MOTHERSHIP, MOTHERSHIP_AT)
burner_id = game_state.add_building(GameState.BUILDING_GAS_BURNER, BURNER_AT)
game_state.deposit_resource_to_building(burner_id, GameState.RESOURCE_GAS, 20)
var logistics: Resource = game_state.building_logistics_profile(game_state.find_building(burner_id))
conversion_rate = logistics.conversion_rate
conversion_yield = logistics.conversion_yield
if not is_equal_approx(conversion_rate, 0.2):
_fail("burner conversion rate is %.2f/s, expected the reduced 0.20/s" % conversion_rate)
func _process(delta: float) -> bool:
elapsed += delta
match stage:
0:
# --- A. energy is fully generic -------------------------------
if not game_state.resource_types().has(GameState.RESOURCE_ENERGY):
return _fail("energy is not in resource_types() - the HUD/transfer list would never show it")
var definition: Resource = game_state.resource_definition(GameState.RESOURCE_ENERGY)
if definition == null or definition.glyph_scene == null:
return _fail("energy has no ResourceDefinition or glyph_scene")
var energy_color: Color = game_state.resource_color(GameState.RESOURCE_ENERGY)
if energy_color == game_state.resource_color(GameState.RESOURCE_ORE) \
or energy_color == game_state.resource_color(GameState.RESOURCE_GAS):
return _fail("energy shares a colour with ore or gas")
print("energy is generic: in resource_types(), own definition, own colour %s" % energy_color)
# --- B. produces energy from local gas at the configured rate -
if elapsed < 5.5:
return false
var gas_left: int = game_state.building_resource(burner_id, GameState.RESOURCE_GAS)
var energy_made: int = game_state.building_resource(burner_id, GameState.RESOURCE_ENERGY)
var gas_burned: int = 20 - gas_left
if gas_burned < 1:
return _fail("burner burned no gas in 5.5s at rate %.2f/s" % conversion_rate)
if energy_made != gas_burned * conversion_yield:
return _fail("burner made %d energy for %d gas burned, expected %dx" % [
energy_made, gas_burned, conversion_yield
])
if game_state.rocks != 0 or game_state.gas != 0:
return _fail("conversion touched the bank (rocks=%d gas=%d) - it must only use local stock" % [
game_state.rocks, game_state.gas
])
print("burner converted %d gas -> %d energy locally, bank untouched" % [gas_burned, energy_made])
# --- stall with no gas -----------------------------------------
var burner_inventory: RefCounted = game_state.building_inventory(burner_id)
burner_inventory.withdraw(GameState.RESOURCE_GAS, burner_inventory.amount(GameState.RESOURCE_GAS))
energy_before_stall = game_state.building_resource(burner_id, GameState.RESOURCE_ENERGY)
elapsed = 0.0
stage = 1
return false
1:
if elapsed < 1.5:
return false
var energy_now: int = game_state.building_resource(burner_id, GameState.RESOURCE_ENERGY)
if energy_now != energy_before_stall:
return _fail("burner made energy with no local gas (energy %d -> %d) - it must stall" % [
energy_before_stall, energy_now
])
print("stalled with no gas: energy held at %d" % energy_now)
# --- C. gated behind research -----------------------------------
if game_state.is_building_unlocked(GameState.BUILDING_GAS_BURNER):
return _fail("gas_burner is buildable with no research at all")
var gate: String = game_state.research_for_building(GameState.BUILDING_GAS_BURNER)
if gate == "":
return _fail("no research node gates gas_burner")
game_state.unlocked = ["shipyards", "logistics", "freight", "power_systems"]
if not game_state.is_building_unlocked(GameState.BUILDING_GAS_BURNER):
return _fail("still locked after researching '%s'" % gate)
print("locked behind research '%s', unlockable" % gate)
# --- D. logistics: reset for a fresh delivery/pickup test -----
game_state.buildings.clear()
game_state.transporters.clear()
game_state.gas = 200
mothership_id = game_state.add_building(GameState.BUILDING_MOTHERSHIP, MOTHERSHIP_AT)
hub_id = game_state.add_building(GameState.BUILDING_TRANSPORT_HUB, HUB_AT)
# #216: a hub arrives empty now - its transporters are built and
# flown in. This test is about the ROUTE, so the crew is put
# straight at its post; test_transporter_supply.gd owns the journey.
load("res://tests/_staff.gd").staff_all(game_state)
burner_id = game_state.add_building(GameState.BUILDING_GAS_BURNER, BURNER_AT)
if game_state.transporters.size() != game_state.hub_capacity():
return _fail("hub did not provision its transporters")
load_limit = game_state.transporter_load_limit()
elapsed = 0.0
stage = 2
return false
2:
# --- a transporter delivers gas to the burner ------------------
if game_state.building_resource(burner_id, GameState.RESOURCE_GAS) > 0:
print("transporter delivered gas to the burner: %d" % [
game_state.building_resource(burner_id, GameState.RESOURCE_GAS)
])
# Force the burner's local energy up to a full load so pickup
# dispatch fires immediately, rather than waiting out the
# genuinely slow conversion rate for a second, separate check.
game_state.building_inventory(burner_id).set_amount(GameState.RESOURCE_ENERGY, load_limit)
elapsed = 0.0
stage = 3
return false
if elapsed > 10.0:
return _fail("no gas was ever delivered to the burner")
return false
3:
# --- a transporter collects the burner's energy to the bank ---
if game_state.banked_resource(GameState.RESOURCE_ENERGY) >= load_limit:
print("transporter collected %d energy to the bank" % game_state.banked_resource(GameState.RESOURCE_ENERGY))
print("TEST_GAS_BURNER: PASS")
quit(0)
return true
if elapsed > 10.0:
return _fail("burner's energy was never collected (bank=%d burner=%d)" % [
game_state.banked_resource(GameState.RESOURCE_ENERGY),
game_state.building_resource(burner_id, GameState.RESOURCE_ENERGY),
])
return false
return false
func _fail(message: String) -> bool:
print("TEST_GAS_BURNER: FAIL (%s)" % message)
quit(1)
return true