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
This commit is contained in:
parent
1f5fecd371
commit
265e121cb0
31 changed files with 1180 additions and 141 deletions
29
tests/_staff.gd
Normal file
29
tests/_staff.gd
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
extends RefCounted
|
||||
|
||||
# #216 made hub and workshop crews built things: ordered at a ship crafter,
|
||||
# flown out from the mothership hangar, and worn out by the work they do.
|
||||
#
|
||||
# A test that is about what a crew DOES should not have to build and fly one
|
||||
# in first, so this puts them straight at their post. The assignment is still
|
||||
# made, so the structure counts them as its own and reports them the way it
|
||||
# would if they had arrived on their own - only the journey is skipped.
|
||||
#
|
||||
# test_transporter_supply.gd owns the journey itself, and does not use this.
|
||||
|
||||
|
||||
# Tops the structure UP to `count`, rather than adding that many: a hub asked
|
||||
# for three when it already holds two needs one more, and the order is clamped
|
||||
# to what it can hold anyway.
|
||||
static func staff(game_state: Node, building_id: int, count: int) -> void:
|
||||
game_state.set_station_desired(building_id, count)
|
||||
while game_state.station_crew(building_id) < game_state.station_desired(building_id):
|
||||
game_state._station_arrived(building_id)
|
||||
|
||||
|
||||
# Every hub and workshop already standing, filled to what it can hold. For
|
||||
# tests that just need logistics to work at all.
|
||||
static func staff_all(game_state: Node) -> void:
|
||||
for building in game_state.buildings:
|
||||
if game_state.station_vessel_type(building) == "":
|
||||
continue
|
||||
staff(game_state, int(building.id), game_state.station_capacity(building))
|
||||
|
|
@ -169,6 +169,10 @@ func _process(delta: float) -> bool:
|
|||
game_state.unlocked.append(node.id)
|
||||
game_state.add_building(GameState.BUILDING_MOTHERSHIP, MOTHERSHIP_AT)
|
||||
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)
|
||||
game_state.add_extractor("Event Zero", BLACK_HOLE_AT, GameState.RESOURCE_DARK_MATTER, "test_black_hole_extractor")
|
||||
extractor_index = game_state.extractor_index_at("test_black_hole_extractor")
|
||||
if extractor_index < 0:
|
||||
|
|
|
|||
|
|
@ -22,8 +22,14 @@ func _initialize() -> void:
|
|||
return
|
||||
var provider: Node = building.visual.get_node_or_null("ActionProvider")
|
||||
if "transport_hub" in definition.capabilities:
|
||||
if provider != null or not building.available_actions().is_empty():
|
||||
_fail("transport hub composed a manual action")
|
||||
# #216: a hub has exactly one thing to press - how many
|
||||
# transporters to keep here. Its routes remain automatic, so a
|
||||
# manual transfer would still be wrong.
|
||||
if provider == null:
|
||||
_fail("transport hub has no way to set its crew")
|
||||
elif building.available_actions() != PackedStringArray(["staff"]):
|
||||
_fail("transport hub composed %s, expected only the crew menu" \
|
||||
% [building.available_actions()])
|
||||
return
|
||||
elif "planet_scanner" in definition.capabilities:
|
||||
if provider == null or not provider.actions.has("scanner") \
|
||||
|
|
|
|||
|
|
@ -146,6 +146,10 @@ func _run_state_cases() -> bool:
|
|||
|
||||
# --- E. a hub takes its transporters with it ------------------------
|
||||
var hub_id: int = game_state.add_building(GameState.BUILDING_TRANSPORT_HUB, Vector2(800.0, 0.0))
|
||||
# #216: crews are built and flown in now, not provisioned with the
|
||||
# structure. This test is about what a crew DOES, so it is put
|
||||
# straight at its post - test_transporter_supply.gd owns the journey.
|
||||
load("res://tests/_staff.gd").staff(game_state, hub_id, game_state.hub_capacity())
|
||||
var hub_transporters: int = 0
|
||||
for transporter in game_state.transporters:
|
||||
if int(transporter.get("hub_id", 0)) == hub_id:
|
||||
|
|
|
|||
|
|
@ -46,6 +46,10 @@ func _process(delta: float) -> bool:
|
|||
game_state.extractor_stock(extractor_index), game_state.rocks])
|
||||
game_state.add_building(GameState.BUILDING_MOTHERSHIP, MOTHERSHIP_AT)
|
||||
game_state.add_building(GameState.BUILDING_TRANSPORT_HUB, HUB_AT)
|
||||
# #216: crews are built and flown in now, not provisioned with the
|
||||
# structure. This test is about what a crew DOES, so it is put
|
||||
# straight at its post - test_transporter_supply.gd owns the journey.
|
||||
load("res://tests/_staff.gd").staff_all(game_state)
|
||||
if game_state.transporters.size() != game_state.hub_capacity():
|
||||
_fail("hub did not provision its transporters")
|
||||
return true
|
||||
|
|
|
|||
|
|
@ -125,6 +125,10 @@ func _process(delta: float) -> bool:
|
|||
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")
|
||||
|
|
|
|||
|
|
@ -90,6 +90,10 @@ func _process(delta: float) -> bool:
|
|||
|
||||
game_state.add_building(GameState.BUILDING_MOTHERSHIP, Vector2.ZERO)
|
||||
game_state.add_building(GameState.BUILDING_TRANSPORT_HUB, Vector2(100, 0))
|
||||
# #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)
|
||||
game_state.add_extractor("Aurelia", Vector2(400, 0), "gas")
|
||||
game_state.extractors[0].stock = game_state.transporter_load_limit()
|
||||
var transporter: Dictionary = game_state.transporters[0]
|
||||
|
|
|
|||
|
|
@ -1,8 +1,13 @@
|
|||
extends SceneTree
|
||||
|
||||
# A transport hub owns its logistics vessels directly. Building a hub
|
||||
# provisions every researched slot immediately; Freight Yards provisions its
|
||||
# extra slot on hubs that already exist.
|
||||
# A transport hub holds a crew of logistics vessels, and Freight Yards widens
|
||||
# how many it can hold.
|
||||
#
|
||||
# Forgejo #216 changed what the doctrine buys. A hub used to PROVISION every
|
||||
# researched slot the moment it was built, free and forever; the slots are
|
||||
# empty now, and the player fills them with transporters they have built. So
|
||||
# what is checked here is the ROOM, the clamp on it, and that a hub taken down
|
||||
# takes its crew with it rather than orphaning them.
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
|
|
@ -17,28 +22,50 @@ func _initialize() -> void:
|
|||
state.research_points = state.skill_research_point_cost("freight_yards")
|
||||
state.unlocked = ["shipyards", "logistics", "freight"]
|
||||
|
||||
var staff: Script = load("res://tests/_staff.gd")
|
||||
var hub_a: int = state.add_building(state.BUILDING_TRANSPORT_HUB, Vector2(-500, 0))
|
||||
var per_hub: int = state.hub_capacity()
|
||||
if state.transporters.size() != per_hub or _at_hub(state, hub_a) != per_hub:
|
||||
return _fail("first hub did not immediately provision %d transporters" % per_hub)
|
||||
print("first hub provisioned %d transporters" % per_hub)
|
||||
if not state.transporters.is_empty():
|
||||
return _fail("a new hub arrived with %d transporters of its own" % state.transporters.size())
|
||||
if state.station_capacity(state.find_building(hub_a)) != per_hub:
|
||||
return _fail("a new hub reports room for %d, expected %d" % [
|
||||
state.station_capacity(state.find_building(hub_a)), per_hub])
|
||||
print("a new hub: room for %d, and nobody in it" % per_hub)
|
||||
|
||||
# The order is clamped to the room, so a hub cannot be asked for a crew it
|
||||
# has nowhere to put.
|
||||
if state.set_station_desired(hub_a, per_hub + 3) != per_hub:
|
||||
return _fail("a hub accepted an order past its own capacity")
|
||||
staff.staff(state, hub_a, per_hub)
|
||||
if _at_hub(state, hub_a) != per_hub:
|
||||
return _fail("the hub holds %d of its %d" % [_at_hub(state, hub_a), per_hub])
|
||||
|
||||
var hub_b: int = state.add_building(state.BUILDING_TRANSPORT_HUB, Vector2(-500, 600))
|
||||
if state.transporters.size() != per_hub * 2 or _at_hub(state, hub_b) != per_hub:
|
||||
return _fail("second hub did not bring its own transporters")
|
||||
print("second hub raised logistics fleet to %d" % state.transporters.size())
|
||||
if _at_hub(state, hub_b) != 0:
|
||||
return _fail("a second hub brought its own transporters")
|
||||
staff.staff(state, hub_b, per_hub)
|
||||
if state.transporters.size() != per_hub * 2:
|
||||
return _fail("two crewed hubs hold %d transporters, expected %d" % [
|
||||
state.transporters.size(), per_hub * 2])
|
||||
print("two hubs, each crewed to %d" % per_hub)
|
||||
|
||||
# --- the doctrine buys ROOM, not vessels ---------------------------
|
||||
var before: int = state.transporters.size()
|
||||
if not state.unlock("freight_yards"):
|
||||
return _fail("could not research Freight Yards")
|
||||
var expected: int = state.hub_capacity() * state.hub_count()
|
||||
if state.transporters.size() != expected or state.transporters.size() != before + 2:
|
||||
return _fail("Freight Yards did not provision one transporter per existing hub")
|
||||
if _at_hub(state, hub_a) != state.hub_capacity() \
|
||||
or _at_hub(state, hub_b) != state.hub_capacity():
|
||||
return _fail("researched transporters were assigned to the wrong hubs")
|
||||
if state.hub_capacity() != per_hub + 1:
|
||||
return _fail("Freight Yards moved capacity to %d, expected %d" % [
|
||||
state.hub_capacity(), per_hub + 1])
|
||||
if state.transporters.size() != before:
|
||||
return _fail("Freight Yards conjured %d transporters" % (state.transporters.size() - before))
|
||||
print("Freight Yards widened each hub to %d without crewing anything" % state.hub_capacity())
|
||||
|
||||
# And the new room is usable.
|
||||
staff.staff(state, hub_a, state.hub_capacity())
|
||||
if _at_hub(state, hub_a) != state.hub_capacity():
|
||||
return _fail("the widened hub holds %d of its %d" % [
|
||||
_at_hub(state, hub_a), state.hub_capacity()])
|
||||
|
||||
print("Freight Yards provisioned existing hubs: %d -> %d" % [before, expected])
|
||||
if state.remove_buildings_of_type(state.BUILDING_TRANSPORT_HUB) != 2 \
|
||||
or not state.transporters.is_empty():
|
||||
return _fail("removing hubs left orphaned logistics transporters")
|
||||
|
|
|
|||
|
|
@ -16,6 +16,10 @@ func _initialize() -> void:
|
|||
state.build_time_scale = 0.02
|
||||
state.add_building(state.BUILDING_MOTHERSHIP, Vector2.ZERO)
|
||||
state.add_building(state.BUILDING_TRANSPORT_HUB, Vector2(50, 0))
|
||||
# #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(state)
|
||||
lab_id = state.add_building(state.BUILDING_SHIP_LAB, Vector2(100, 0))
|
||||
if not state.queue_lab_research(lab_id, "int_damage"):
|
||||
return _fail("could not queue lab research")
|
||||
|
|
|
|||
|
|
@ -1,5 +1,14 @@
|
|||
extends SceneTree
|
||||
|
||||
# Logistics travels at a readable pace rather than teleporting: the ceiling
|
||||
# below is the rule, and each leg has to actually advance at the speed its own
|
||||
# stat claims.
|
||||
#
|
||||
# The expected progress is DERIVED from that stat rather than written out,
|
||||
# because the numbers are tuned: #217 slowed transporters from 150 to 120 and
|
||||
# a hardcoded window failed for a change it was not about. What must not drift
|
||||
# is the ceiling, and the agreement between the stat and the movement.
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var state: Node = root.get_node("GameState")
|
||||
|
|
@ -16,16 +25,20 @@ func _initialize() -> void:
|
|||
"to": Vector2(900, 0), "progress": 0.0})
|
||||
state._advance_vessel_deliveries(1.0)
|
||||
var delivery_progress: float = state.vessel_deliveries[0].progress
|
||||
if delivery_progress < 0.18 or delivery_progress > 0.22:
|
||||
return _fail("900-unit delivery advanced %.3f in one second" % delivery_progress)
|
||||
var delivery_expected: float = delivery_speed / 900.0
|
||||
if absf(delivery_progress - delivery_expected) > 0.01:
|
||||
return _fail("900-unit delivery advanced %.3f in one second at %.0f u/s, expected %.3f" \
|
||||
% [delivery_progress, delivery_speed, delivery_expected])
|
||||
|
||||
state.transporters.append({"hub_id": 0, "state": state.TRANSPORTER_RETURNING,
|
||||
"target_id": 0, "carrying": 0, "progress": 0.0,
|
||||
"from": Vector2.ZERO, "to": Vector2(600, 0)})
|
||||
state._advance_transporters(1.0)
|
||||
var transport_progress: float = state.transporters[0].progress
|
||||
if transport_progress < 0.23 or transport_progress > 0.27:
|
||||
return _fail("600-unit transport leg advanced %.3f in one second" % transport_progress)
|
||||
var transport_expected: float = transport_speed / 600.0
|
||||
if absf(transport_progress - transport_expected) > 0.01:
|
||||
return _fail("600-unit transport leg advanced %.3f in one second at %.0f u/s, expected %.3f" \
|
||||
% [transport_progress, transport_speed, transport_expected])
|
||||
|
||||
print("delivery %.0f u/s, transporter %.0f u/s" % [delivery_speed, transport_speed])
|
||||
print("TEST_LOGISTICS_TRAVEL_SPEED: PASS")
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ func _process(delta: float) -> bool:
|
|||
# exact match rather than by hunting for resource words in it: a
|
||||
# Gas Bomber is named after a resource and always will be, and
|
||||
# what #111 removed was the PRICE spelled out underneath.
|
||||
var fleet_defs: Array = VesselDefinitions.fleet()
|
||||
var fleet_defs: Array = VesselDefinitions.craftable()
|
||||
for i in range(mini(controller.craft_menu.vessel_labels.size(), fleet_defs.size())):
|
||||
var caption: String = String(controller.craft_menu.vessel_labels[i].text)
|
||||
var expected: String = String(fleet_defs[i].display_name)
|
||||
|
|
@ -98,7 +98,9 @@ func _process(delta: float) -> bool:
|
|||
var priced: int = 0
|
||||
var stacked: int = 0
|
||||
for i in range(controller.craft_menu.price_tags.size()):
|
||||
var definition: Resource = VesselDefinitions.fleet()[i]
|
||||
# #216 added the two support craft to the crafter, so the rows
|
||||
# are craftable() now - fleet hulls first, then those.
|
||||
var definition: Resource = VesselDefinitions.craftable()[i]
|
||||
var costs: Dictionary = game_state.product_costs(definition.definition_id)
|
||||
var strip: Control = controller.craft_menu.price_tags[i]
|
||||
if costs.is_empty():
|
||||
|
|
|
|||
|
|
@ -15,6 +15,10 @@ func _initialize() -> void:
|
|||
|
||||
state.add_building(GameState.BUILDING_MOTHERSHIP, Vector2.ZERO)
|
||||
state.add_building(GameState.BUILDING_TRANSPORT_HUB, Vector2.ZERO)
|
||||
# #216: crews are built and flown in now, not provisioned with the
|
||||
# structure. This test is about what a crew DOES, so it is put
|
||||
# straight at its post - test_transporter_supply.gd owns the journey.
|
||||
load("res://tests/_staff.gd").staff_all(state)
|
||||
var lab_id: int = state.add_building(GameState.BUILDING_SHIP_LAB, Vector2(120, 0))
|
||||
if not state.queue_lab_research(lab_id, "unlock_gas_bomber"):
|
||||
_fail("could not queue gas research")
|
||||
|
|
|
|||
|
|
@ -89,6 +89,10 @@ func _process(_delta: float) -> bool:
|
|||
|
||||
game_state.add_building(GameState.BUILDING_MOTHERSHIP, Vector2.ZERO)
|
||||
workshop_id = game_state.add_building(GameState.BUILDING_WORKSHOP, Vector2(600.0, 0.0))
|
||||
# #216: crews are built and flown in now, not provisioned with the
|
||||
# structure. This test is about what a crew DOES, so it is put
|
||||
# straight at its post - test_transporter_supply.gd owns the journey.
|
||||
load("res://tests/_staff.gd").staff(game_state, workshop_id, game_state.workshop_drone_capacity())
|
||||
if _drones_of(workshop_id).size() != GameState.BASE_WORKSHOP_DRONES:
|
||||
return _fail("a new workshop stationed %d drones, expected %d" \
|
||||
% [_drones_of(workshop_id).size(), GameState.BASE_WORKSHOP_DRONES])
|
||||
|
|
@ -187,6 +191,7 @@ func _process(_delta: float) -> bool:
|
|||
if bonus_id == "":
|
||||
return _fail("no skill node raises the drone count")
|
||||
var before_count: int = _drones_of(workshop_id).size()
|
||||
var before_room: int = game_state.station_capacity(game_state.find_building(workshop_id))
|
||||
game_state.rocks = 5000
|
||||
# #138: the deep doctrines want gas and energy too.
|
||||
game_state.bank_inventory.set_amount(GameState.RESOURCE_GAS, 5000)
|
||||
|
|
@ -194,10 +199,21 @@ func _process(_delta: float) -> bool:
|
|||
game_state.research_points = 20
|
||||
if not game_state.unlock(bonus_id):
|
||||
return _fail("could not research \"%s\"" % bonus_id)
|
||||
# #216: the doctrine buys ROOM, not drones. It used to provision one into
|
||||
# every standing workshop for nothing; the extra berth is now something
|
||||
# the player fills with a drone they have built.
|
||||
if _drones_of(workshop_id).size() != before_count:
|
||||
return _fail("researching \"%s\" conjured %d drones" \
|
||||
% [bonus_id, _drones_of(workshop_id).size() - before_count])
|
||||
var room: int = game_state.station_capacity(game_state.find_building(workshop_id))
|
||||
if room != before_room + 1:
|
||||
return _fail("the doctrine moved the workshop's room from %d to %d, expected %d" \
|
||||
% [before_room, room, before_room + 1])
|
||||
load("res://tests/_staff.gd").staff(game_state, workshop_id, room)
|
||||
if _drones_of(workshop_id).size() != before_count + 1:
|
||||
return _fail("a workshop built before the upgrade still has %d drones" \
|
||||
return _fail("the berth the doctrine bought could not be crewed (%d drones)" \
|
||||
% _drones_of(workshop_id).size())
|
||||
print("\"%s\" raises an already-standing workshop from %d to %d drones" \
|
||||
print("\"%s\" widens a standing workshop from %d to %d berths, crewed by hand" \
|
||||
% [bonus_id, before_count, _drones_of(workshop_id).size()])
|
||||
|
||||
print("TEST_SPACIAL_WORKSHOP: PASS")
|
||||
|
|
|
|||
|
|
@ -109,8 +109,13 @@ func _process(delta: float) -> bool:
|
|||
print("TEST_TRANSPORT_HUB_BUILDING: FAIL (never spawned)")
|
||||
quit(1)
|
||||
return true
|
||||
if not hub_building.available_actions().is_empty():
|
||||
print("TEST_TRANSPORT_HUB_BUILDING: FAIL (offers a manual resource transfer)")
|
||||
# #216 gave the hub one action, and only one: saying how many
|
||||
# transporters to keep here. Its ROUTES are still automatic -
|
||||
# there is nothing to hand-carry - so a transfer action would put
|
||||
# back exactly what this check was written to keep out.
|
||||
if hub_building.available_actions() != PackedStringArray(["staff"]):
|
||||
print("TEST_TRANSPORT_HUB_BUILDING: FAIL (offers %s, expected only the crew menu)" \
|
||||
% [hub_building.available_actions()])
|
||||
quit(1)
|
||||
return true
|
||||
for ship in scene.get_node("Fleet").ships:
|
||||
|
|
@ -152,9 +157,14 @@ func _process(delta: float) -> bool:
|
|||
# --- 4. animates without ever redrawing (#10) ----------------
|
||||
if elapsed < 2.0:
|
||||
return false
|
||||
# #216 gave the hub one button - how many transporters to keep
|
||||
# here. The TRANSFER button stays absent: its routes are still
|
||||
# automatic, and hand-carrying to a hub is what this check was
|
||||
# written to keep out.
|
||||
if controller.current_building != hub_building \
|
||||
or controller.action_button.visible or controller.transfer_button.visible:
|
||||
print("TEST_TRANSPORT_HUB_BUILDING: FAIL (actionless hub context current=%s expected=%s action=%s transfer=%s)" % [
|
||||
or not controller.action_button.visible \
|
||||
or controller.transfer_button.visible:
|
||||
print("TEST_TRANSPORT_HUB_BUILDING: FAIL (hub context current=%s expected=%s action=%s transfer=%s)" % [
|
||||
controller.current_building, hub_building, controller.action_button.visible,
|
||||
controller.transfer_button.visible,
|
||||
])
|
||||
|
|
|
|||
127
tests/test_transporter_burn.gd
Normal file
127
tests/test_transporter_burn.gd
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
extends SceneTree
|
||||
|
||||
# Forgejo #217: transporters crawl by default, and a Burn Drive lets them
|
||||
# carry energy and spend it to go far faster. The energy is theirs to find -
|
||||
# they fetch it from the nearest store that has any - and it runs out, so the
|
||||
# speed is something the player's energy supply pays for continuously rather
|
||||
# than a one-off purchase.
|
||||
#
|
||||
# Covers:
|
||||
# A. unresearched, a transporter never detours for fuel however much energy
|
||||
# is lying about, and makes the base speed
|
||||
# B. researched and empty, an idle one goes for the NEAREST store with
|
||||
# energy in it, and takes what is there
|
||||
# C. a fuelled transporter is measurably faster than an unfuelled one
|
||||
# D. a leg burns a unit, and an empty tank drops it back to the crawl
|
||||
# E. the leg it flies to collect fuel is not itself charged
|
||||
|
||||
const HUB_AT: Vector2 = Vector2(0.0, 0.0)
|
||||
const NEAR_ENERGY_AT: Vector2 = Vector2(400.0, 0.0)
|
||||
const FAR_ENERGY_AT: Vector2 = Vector2(-3000.0, 0.0)
|
||||
|
||||
var state: Node
|
||||
var hub_id: int
|
||||
var near_id: int
|
||||
var far_id: int
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
state = root.get_node("GameState")
|
||||
state.buildings.clear()
|
||||
state.extractors.clear()
|
||||
state.transporters.clear()
|
||||
state.stored_vessels.clear()
|
||||
state.vessel_deliveries.clear()
|
||||
state.reset_progression()
|
||||
state.rocks = 500
|
||||
state.unlocked = ["shipyards", "logistics", "freight"]
|
||||
state.add_building(state.BUILDING_MOTHERSHIP, Vector2(0.0, 4000.0))
|
||||
hub_id = state.add_building(state.BUILDING_TRANSPORT_HUB, HUB_AT)
|
||||
near_id = state.add_building(state.BUILDING_SHIP_CRAFTER, NEAR_ENERGY_AT)
|
||||
far_id = state.add_building(state.BUILDING_SHIP_LAB, FAR_ENERGY_AT)
|
||||
load("res://tests/_staff.gd").staff(state, hub_id, 1)
|
||||
|
||||
|
||||
func _initialize_done() -> void:
|
||||
pass
|
||||
|
||||
|
||||
func _process(_delta: float) -> bool:
|
||||
# The bank is deliberately empty of energy, so the only sources are the
|
||||
# two structures below and "nearest" is a real choice.
|
||||
state.bank_inventory.set_amount(GameState.RESOURCE_ENERGY, 0)
|
||||
state.deposit_resource_to_building(near_id, GameState.RESOURCE_ENERGY, 4)
|
||||
state.deposit_resource_to_building(far_id, GameState.RESOURCE_ENERGY, 50)
|
||||
|
||||
var t: Dictionary = state.transporters[0]
|
||||
|
||||
# --- A. unresearched: no tank, no detour, base speed ----------------
|
||||
if state.transporter_burns_energy():
|
||||
return _fail("an unresearched transporter already burns energy")
|
||||
var crawl: float = state.transporter_leg_speed(t)
|
||||
if not is_equal_approx(crawl, state.transporter_speed()):
|
||||
return _fail("an unfuelled transporter makes %.1f, expected the base %.1f" \
|
||||
% [crawl, state.transporter_speed()])
|
||||
state._refuel_idle_transporters()
|
||||
if String(t.state) != state.TRANSPORTER_IDLE:
|
||||
return _fail("an unresearched transporter went looking for fuel")
|
||||
print("unresearched: no detour, %.0f base speed" % crawl)
|
||||
|
||||
# --- B. researched and empty: to the NEAREST store ------------------
|
||||
state.vessel_levels["trn_burn"] = 1
|
||||
if not state.transporter_burns_energy():
|
||||
return _fail("a researched Burn Drive did not enable energy burning")
|
||||
state._refuel_idle_transporters()
|
||||
if String(t.state) != state.TRANSPORTER_TO_FUEL:
|
||||
return _fail("an empty transporter did not go for fuel (state %s)" % t.state)
|
||||
if not Vector2(t.to).is_equal_approx(NEAR_ENERGY_AT):
|
||||
return _fail("it flew to %s, not the nearer store at %s" % [t.to, NEAR_ENERGY_AT])
|
||||
print("researched and empty: flies to the nearest store, not the fullest")
|
||||
|
||||
# --- E. and the trip to collect is not itself charged ---------------
|
||||
var before_fuel: int = int(t.get("fuel", 0))
|
||||
state._advance_transporters(600.0)
|
||||
if int(t.get("fuel", 0)) <= before_fuel:
|
||||
return _fail("the transporter came back from the fuel store with nothing")
|
||||
# The near store held 4, which is under the tank, so it took all of it.
|
||||
if int(t.fuel) != 4:
|
||||
return _fail("it drew %d energy, expected the 4 that were there" % int(t.fuel))
|
||||
if state.building_resource(near_id, GameState.RESOURCE_ENERGY) != 0:
|
||||
return _fail("the store it drew from still holds energy")
|
||||
print("collected 4 - all the near store had - and was not charged for the trip")
|
||||
|
||||
# --- C. fuelled is faster -------------------------------------------
|
||||
var boosted: float = state.transporter_leg_speed(t)
|
||||
if boosted <= crawl:
|
||||
return _fail("a fuelled transporter makes %.1f, no better than the crawl %.1f" \
|
||||
% [boosted, crawl])
|
||||
if not is_equal_approx(boosted, crawl * state.transporter_fuel_boost()):
|
||||
return _fail("a fuelled transporter makes %.1f, expected %.1f" \
|
||||
% [boosted, crawl * state.transporter_fuel_boost()])
|
||||
print("fuelled: %.0f against %.0f, a %.1fx difference" \
|
||||
% [boosted, crawl, state.transporter_fuel_boost()])
|
||||
|
||||
# --- D. and it burns down to the crawl ------------------------------
|
||||
var tank: int = int(t.fuel)
|
||||
for leg in range(tank):
|
||||
t.state = state.TRANSPORTER_RETURNING
|
||||
t.progress = 0.0
|
||||
t.from = HUB_AT
|
||||
t.to = NEAR_ENERGY_AT
|
||||
state._advance_transporters(600.0)
|
||||
if int(t.get("fuel", 0)) != 0:
|
||||
return _fail("%d legs left %d energy aboard, expected an empty tank" \
|
||||
% [tank, int(t.fuel)])
|
||||
if not is_equal_approx(state.transporter_leg_speed(t), crawl):
|
||||
return _fail("an emptied transporter did not drop back to the crawl")
|
||||
print("%d legs emptied the tank and it is back to %.0f" % [tank, crawl])
|
||||
|
||||
print("TEST_TRANSPORTER_BURN: PASS")
|
||||
quit(0)
|
||||
return true
|
||||
|
||||
|
||||
func _fail(message: String) -> bool:
|
||||
print("TEST_TRANSPORTER_BURN: FAIL (%s)" % message)
|
||||
quit(1)
|
||||
return true
|
||||
|
|
@ -1,14 +1,26 @@
|
|||
extends SceneTree
|
||||
|
||||
# Hub-owned transporters supply an unfunded combat-vessel order without ever
|
||||
# being crafted, stored at the mothership, or added to the player fleet.
|
||||
# Forgejo #216: a transport hub used to conjure its own transporters, free and
|
||||
# permanent. They are built at a ship crafter now, flown out from the
|
||||
# mothership hangar to whichever hub asked for them, and worn out by the runs
|
||||
# they make.
|
||||
#
|
||||
# Covers:
|
||||
# A. a new hub stands empty - nothing is provisioned with the structure
|
||||
# B. the crafter accepts a transporter order, which it used to refuse
|
||||
# C. one assigned and one in the hangar puts it at the hub, and a hub that
|
||||
# was asked for nothing stays empty however many are sitting there
|
||||
# D. the transporter then does the job the old free ones did: it supplies a
|
||||
# queued order, without ore being created or destroyed
|
||||
# E. it wears out, and the spent vessel is gone rather than returned
|
||||
|
||||
var state: Node
|
||||
var crafter_id: int
|
||||
var initial_transporters: int
|
||||
var hub_id: int
|
||||
var total_at_start: int
|
||||
var consumed: int = 0
|
||||
var elapsed: float = 0.0
|
||||
var stage: int = 0
|
||||
var conservation_broken: bool = false
|
||||
|
||||
|
||||
|
|
@ -20,56 +32,129 @@ func _initialize() -> void:
|
|||
state.stored_vessels.clear()
|
||||
state.vessel_deliveries.clear()
|
||||
state.reset_progression()
|
||||
state.rocks = 200
|
||||
state.rocks = 400
|
||||
state.build_time_scale = 0.02
|
||||
state.unlocked = ["shipyards", "logistics", "freight"]
|
||||
state.add_building(state.BUILDING_MOTHERSHIP, Vector2.ZERO)
|
||||
crafter_id = state.add_building(state.BUILDING_SHIP_CRAFTER, Vector2(900, -300))
|
||||
|
||||
if state.queue_order(crafter_id, state.VESSEL_TRANSPORTER):
|
||||
return _fail("the crafter accepted a logistics transporter order")
|
||||
var hub_id: int = state.add_building(state.BUILDING_TRANSPORT_HUB, Vector2(600, 200))
|
||||
initial_transporters = state.hub_capacity()
|
||||
if state.transporters.size() != initial_transporters:
|
||||
return _fail("hub provisioned %d transporters, expected %d" % [
|
||||
state.transporters.size(), initial_transporters])
|
||||
for transporter in state.transporters:
|
||||
if int(transporter.hub_id) != hub_id or transporter.state != state.TRANSPORTER_IDLE:
|
||||
return _fail("a provisioned transporter was not idle at its hub")
|
||||
|
||||
var bank_before: int = state.rocks
|
||||
if not state.queue_order(crafter_id, state.PRODUCT_SHIP):
|
||||
return _fail("crafter refused the combat-vessel order")
|
||||
if state.rocks != bank_before:
|
||||
return _fail("placing the order changed the bank before a transporter loaded")
|
||||
state.order_completed.connect(_on_order_completed)
|
||||
total_at_start = _total()
|
||||
hub_id = state.add_building(state.BUILDING_TRANSPORT_HUB, Vector2(600, 200))
|
||||
|
||||
|
||||
func _process(delta: float) -> bool:
|
||||
elapsed += delta
|
||||
if _total() + consumed != total_at_start:
|
||||
conservation_broken = true
|
||||
if state.pending_orders(crafter_id) > 0:
|
||||
if elapsed > 25.0:
|
||||
return _fail("hub transporters never supplied the queued order")
|
||||
return false
|
||||
if conservation_broken:
|
||||
return _fail("ore was created or destroyed on the automatic route")
|
||||
if state.transporters.size() != initial_transporters:
|
||||
return _fail("transporters accumulated during delivery")
|
||||
if state.stored_vessels.has(state.VESSEL_TRANSPORTER):
|
||||
return _fail("a transporter appeared in the mothership hangar")
|
||||
print("%d hub-owned transporters supplied the crafter automatically" % initial_transporters)
|
||||
print("TEST_TRANSPORTER_SUPPLY: PASS")
|
||||
quit(0)
|
||||
return true
|
||||
match stage:
|
||||
0:
|
||||
# --- A. nothing comes with the structure ---------------------
|
||||
if not state.transporters.is_empty():
|
||||
return _fail("a new hub arrived with %d transporters" % state.transporters.size())
|
||||
if state.station_desired(hub_id) != 0:
|
||||
return _fail("a new hub already had a crew ordered")
|
||||
print("a new hub stands empty")
|
||||
|
||||
# --- B. the crafter takes the order --------------------------
|
||||
if not state.queue_order(crafter_id, state.VESSEL_TRANSPORTER):
|
||||
return _fail("the crafter refused a transporter order")
|
||||
state.find_building(crafter_id).orders.clear()
|
||||
|
||||
# --- C. assigned, and only then sent -------------------------
|
||||
# One in the hangar and nothing asked for: it must stay there.
|
||||
state.stored_vessels.append(state.VESSEL_TRANSPORTER)
|
||||
state._advance_station_dispatch()
|
||||
if not state.transporters.is_empty():
|
||||
return _fail("a hub that asked for nothing took a transporter anyway")
|
||||
if state.set_station_desired(hub_id, 1) != 1:
|
||||
return _fail("the hub refused an order for one transporter")
|
||||
state._advance_station_dispatch()
|
||||
if state.stored_vessels.has(state.VESSEL_TRANSPORTER):
|
||||
return _fail("the assigned transporter never left the hangar")
|
||||
if state.vessel_deliveries.is_empty():
|
||||
return _fail("nothing was sent to the hub")
|
||||
elapsed = 0.0
|
||||
stage = 1
|
||||
return false
|
||||
1:
|
||||
if state.station_crew(hub_id) < 1:
|
||||
if elapsed > 25.0:
|
||||
return _fail("the transporter never reached the hub")
|
||||
return false
|
||||
var condition: Array = state.station_condition(hub_id)
|
||||
if condition.size() != 1 or int(condition[0]) != GameState.TRANSPORTER_TRIPS:
|
||||
return _fail("the arrived transporter reports %s trips, expected [%d]" \
|
||||
% [condition, GameState.TRANSPORTER_TRIPS])
|
||||
print("ordered, built and flown out: 1 at the hub with %d runs in it" \
|
||||
% GameState.TRANSPORTER_TRIPS)
|
||||
|
||||
# --- D. and it does the work ---------------------------------
|
||||
if not state.queue_order(crafter_id, state.PRODUCT_SHIP):
|
||||
return _fail("crafter refused the combat-vessel order")
|
||||
state.order_completed.connect(_on_order_completed)
|
||||
total_at_start = _total()
|
||||
elapsed = 0.0
|
||||
stage = 2
|
||||
return false
|
||||
2:
|
||||
if _total() + consumed != total_at_start:
|
||||
conservation_broken = true
|
||||
if state.pending_orders(crafter_id) > 0:
|
||||
if elapsed > 25.0:
|
||||
return _fail("the hub's transporter never supplied the queued order")
|
||||
return false
|
||||
if conservation_broken:
|
||||
return _fail("ore was created or destroyed on the automatic route")
|
||||
print("the hub's own transporter supplied the crafter")
|
||||
|
||||
elapsed = 0.0
|
||||
stage = 3
|
||||
return false
|
||||
3:
|
||||
# --- E. and it is used up by doing so ------------------------
|
||||
# The run is spent when it gets HOME, not when the cargo lands, so
|
||||
# this waits for the transporter to be idle at its hub again -
|
||||
# which is also the rule that stops a vessel retiring with its
|
||||
# cargo still in the air.
|
||||
if state.transporters.is_empty():
|
||||
return _fail("the transporter vanished before completing its run")
|
||||
if String(state.transporters[0].state) != state.TRANSPORTER_IDLE:
|
||||
if elapsed > 25.0:
|
||||
return _fail("the transporter never got home")
|
||||
return false
|
||||
var worn: Array = state.station_condition(hub_id)
|
||||
if worn.is_empty() or int(worn[0]) >= GameState.TRANSPORTER_TRIPS:
|
||||
return _fail("a transporter that ran a supply route is still unworn (%s)" % [worn])
|
||||
print("a completed run wore it to %d" % int(worn[0]))
|
||||
# Wound down to its last run, so the next arrival home retires it.
|
||||
state.transporters[0].trips_left = 1
|
||||
elapsed = 0.0
|
||||
stage = 4
|
||||
return false
|
||||
4:
|
||||
if not state.transporters.is_empty():
|
||||
if elapsed > 30.0:
|
||||
return _fail("a transporter on its last run was never retired")
|
||||
# Keep it working: an idle one at its hub never completes
|
||||
# another leg, and it is the arrival home that spends the run.
|
||||
state.deposit_resource_to_building(crafter_id, GameState.RESOURCE_ORE, 0)
|
||||
state.queue_order(crafter_id, state.PRODUCT_SHIP)
|
||||
return false
|
||||
if state.stored_vessels.has(state.VESSEL_TRANSPORTER):
|
||||
return _fail("a spent transporter went back to the hangar instead of being used up")
|
||||
if state.station_crew(hub_id) != 0:
|
||||
return _fail("the hub still counts a retired transporter")
|
||||
print("spent after its last run: gone, and the hub is short again")
|
||||
print("TEST_TRANSPORTER_SUPPLY: PASS")
|
||||
quit(0)
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _on_order_completed(_building_id: int, product: String) -> void:
|
||||
consumed += state.product_cost(product)
|
||||
|
||||
|
||||
# Every place a rock can be while the route runs: the bank (the mothership's
|
||||
# own store IS state.rocks, so counting the building too would double it), a
|
||||
# building's local store, an order that has started and taken its cost, and a
|
||||
# transporter's hold in mid-flight.
|
||||
func _total() -> int:
|
||||
var sum: int = state.rocks + state.fleet_cargo
|
||||
for building in state.buildings:
|
||||
|
|
|
|||
139
tests/test_workshop_staffing.gd
Normal file
139
tests/test_workshop_staffing.gd
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
extends SceneTree
|
||||
|
||||
# Forgejo #216, the workshop half. A spacial workshop used to conjure its own
|
||||
# repair drones, free and permanent. They are built at a ship crafter now,
|
||||
# flown out from the mothership hangar to whichever workshop asked for them,
|
||||
# and spent by the repairs they make.
|
||||
#
|
||||
# test_transporter_supply.gd covers the same rules on the hub side. The two
|
||||
# share a model but not their wear paths - a transporter spends a run getting
|
||||
# home, a drone spends a repair getting home - so the drone's own path is
|
||||
# exercised here rather than assumed from the transporter's.
|
||||
#
|
||||
# Covers:
|
||||
# A. a new workshop stands empty, and takes nothing until it is asked to
|
||||
# B. the crafter accepts a repair drone order, which it used to refuse
|
||||
# C. an assigned workshop is sent one from the hangar
|
||||
# D. it repairs a damaged structure and is worn by doing so
|
||||
# E. its last repair uses it up, and the workshop is short again
|
||||
|
||||
var state: Node
|
||||
var workshop_id: int
|
||||
var damaged_id: int
|
||||
var elapsed: float = 0.0
|
||||
var stage: int = 0
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
state = root.get_node("GameState")
|
||||
state.buildings.clear()
|
||||
state.extractors.clear()
|
||||
state.transporters.clear()
|
||||
state.repair_drones.clear()
|
||||
state.stored_vessels.clear()
|
||||
state.vessel_deliveries.clear()
|
||||
state.reset_progression()
|
||||
state.rocks = 600
|
||||
state.unlocked = ["shipyards", "logistics"]
|
||||
state.add_building(state.BUILDING_MOTHERSHIP, Vector2.ZERO)
|
||||
workshop_id = state.add_building(GameState.BUILDING_WORKSHOP, Vector2(700.0, 0.0))
|
||||
damaged_id = state.add_building(state.BUILDING_SHIP_CRAFTER, Vector2(900.0, 200.0))
|
||||
state.deposit_resource_to_building(workshop_id, GameState.RESOURCE_ORE, 400)
|
||||
|
||||
|
||||
func _drones() -> int:
|
||||
return state.station_crew(workshop_id)
|
||||
|
||||
|
||||
func _process(delta: float) -> bool:
|
||||
elapsed += delta
|
||||
match stage:
|
||||
0:
|
||||
# --- A. nothing comes with the structure ---------------------
|
||||
if not state.repair_drones.is_empty():
|
||||
return _fail("a new workshop arrived with %d drones" % state.repair_drones.size())
|
||||
state.stored_vessels.append(GameState.REPAIR_DRONE)
|
||||
state._advance_station_dispatch()
|
||||
if _drones() != 0:
|
||||
return _fail("a workshop that asked for nothing took a drone anyway")
|
||||
print("a new workshop stands empty, and takes nothing unasked")
|
||||
|
||||
# --- B. the crafter takes the order --------------------------
|
||||
var crafter_id: int = state.add_building(state.BUILDING_SHIP_CRAFTER, Vector2(-900.0, 0.0))
|
||||
if not state.queue_order(crafter_id, GameState.REPAIR_DRONE):
|
||||
return _fail("the crafter refused a repair drone order")
|
||||
state.find_building(crafter_id).orders.clear()
|
||||
|
||||
# --- C. assigned, then sent ----------------------------------
|
||||
if state.set_station_desired(workshop_id, 1) != 1:
|
||||
return _fail("the workshop refused an order for one drone")
|
||||
state._advance_station_dispatch()
|
||||
if state.vessel_deliveries.is_empty():
|
||||
return _fail("nothing was sent to the workshop")
|
||||
elapsed = 0.0
|
||||
stage = 1
|
||||
return false
|
||||
1:
|
||||
if _drones() < 1:
|
||||
if elapsed > 25.0:
|
||||
return _fail("the drone never reached the workshop")
|
||||
return false
|
||||
var fresh: Array = state.station_condition(workshop_id)
|
||||
if fresh.size() != 1 or int(fresh[0]) != GameState.DRONE_REPAIRS:
|
||||
return _fail("the arrived drone reports %s, expected [%d]" \
|
||||
% [fresh, GameState.DRONE_REPAIRS])
|
||||
print("ordered and flown out: 1 drone with %d repairs in it" % GameState.DRONE_REPAIRS)
|
||||
|
||||
# --- D. it does the work, and is worn by it ------------------
|
||||
state.damage_building(damaged_id, state.building_max_hp(state.BUILDING_SHIP_CRAFTER) / 2)
|
||||
elapsed = 0.0
|
||||
stage = 2
|
||||
return false
|
||||
2:
|
||||
if state.building_health_fraction(damaged_id) >= 0.999:
|
||||
print("the workshop's own drone repaired the structure")
|
||||
elapsed = 0.0
|
||||
stage = 3
|
||||
return false
|
||||
if elapsed > 40.0:
|
||||
return _fail("the drone never repaired the damaged structure")
|
||||
return false
|
||||
3:
|
||||
# Spent on getting HOME, so this waits for it to be idle again.
|
||||
if state.repair_drones.is_empty():
|
||||
return _fail("the drone vanished mid-run")
|
||||
if String(state.repair_drones[0].state) != GameState.DRONE_IDLE:
|
||||
if elapsed > 25.0:
|
||||
return _fail("the drone never got home")
|
||||
return false
|
||||
var worn: Array = state.station_condition(workshop_id)
|
||||
if worn.is_empty() or int(worn[0]) >= GameState.DRONE_REPAIRS:
|
||||
return _fail("a drone that ran a repair is still unworn (%s)" % [worn])
|
||||
print("a completed repair wore it to %d" % int(worn[0]))
|
||||
|
||||
# --- E. and its last one uses it up --------------------------
|
||||
state.repair_drones[0].repairs_left = 1
|
||||
state.damage_building(damaged_id, state.building_max_hp(state.BUILDING_SHIP_CRAFTER) / 2)
|
||||
elapsed = 0.0
|
||||
stage = 4
|
||||
return false
|
||||
4:
|
||||
if not state.repair_drones.is_empty():
|
||||
if elapsed > 40.0:
|
||||
return _fail("a drone on its last repair was never retired")
|
||||
return false
|
||||
if state.stored_vessels.has(GameState.REPAIR_DRONE):
|
||||
return _fail("a spent drone went back to the hangar instead of being used up")
|
||||
if _drones() != 0:
|
||||
return _fail("the workshop still counts a retired drone")
|
||||
print("spent after its last repair: gone, and the workshop is short again")
|
||||
print("TEST_WORKSHOP_STAFFING: PASS")
|
||||
quit(0)
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _fail(message: String) -> bool:
|
||||
print("TEST_WORKSHOP_STAFFING: FAIL (%s)" % message)
|
||||
quit(1)
|
||||
return true
|
||||
Loading…
Reference in a new issue