diff --git a/buildings/FleetOfficeBuildingDefinition.tres b/buildings/FleetOfficeBuildingDefinition.tres index 7d32fad..d705430 100644 --- a/buildings/FleetOfficeBuildingDefinition.tres +++ b/buildings/FleetOfficeBuildingDefinition.tres @@ -7,9 +7,9 @@ [sub_resource type="Resource" id="Resource_office_logistics"] script = ExtResource("3") input_buffers = { -"energy": 30, -"gas": 30, -"ore": 60 +"energy": 6, +"gas": 6, +"ore": 10 } collected_outputs = PackedStringArray() conversion_input = "" diff --git a/core/GameState.gd b/core/GameState.gd index 5d86356..baa4d24 100644 --- a/core/GameState.gd +++ b/core/GameState.gd @@ -786,6 +786,12 @@ const FLEET_OFFICE_UPKEEP_DIVISOR: int = 8 # seven and a half. A steeper rate reaches "unaffordable" while the player is # still learning what the fleet is for. const FLEET_UPKEEP_GROWTH: float = 1.12 +# #223: how many charges an office is kept stocked for. Enough that the tax +# is always payable while the bank has anything - a transporter round trip is +# well inside four charge intervals - and no more, so an office does not sit +# on resources the rest of the base needs. Below this it can starve with ore +# in the bank; far above it, offices become a hoard. +const FLEET_OFFICE_STOCK_CHARGES: int = 4 # Offices in the order they were built, which is the order they are matched to @@ -868,6 +874,28 @@ func fleet_upkeep_breakdown() -> Dictionary: # The vessel an office is answerable for, or "" when the fleet is smaller than # the berths built for it - an office with nothing in it costs nothing. +# What this office should hold of one resource: enough charges of whatever is +# in its berth to keep paying while a transporter is on its way, and nothing +# it will never spend. An office keeping an interceptor asks for no gas at +# all, where a flat buffer had it hauling gas it could not use. +# +# An empty berth asks for nothing beyond the small floor its definition +# declares - which is what keeps a newly crewed berth payable before its +# first delivery arrives. +func fleet_office_stock_target(id: int, resource_type: String) -> int: + var offices: Array = fleet_offices() + for i in range(offices.size()): + if int(offices[i].get("id", 0)) != id: + continue + var slot_index: int = i + BASE_FLEET_SIZE + var vessel_type: String = fleet_office_vessel(slot_index) + if vessel_type == "": + return 0 + var upkeep: Dictionary = fleet_office_upkeep(vessel_type, slot_index) + return int(upkeep.get(resource_type, 0)) * FLEET_OFFICE_STOCK_CHARGES + return 0 + + func fleet_office_vessel(slot_index: int) -> String: if slot_index < 0 or slot_index >= fleet_vessel_types.size(): return "" @@ -2842,6 +2870,10 @@ func building_logistics_deficit(id: int, resource_type: String) -> int: # take, which likewise depends on the world rather than on a definition. if resource_type == RESOURCE_ORE: target = maxi(target, workshop_ore_demand(id)) + # #223: a Fleet Office asks for what the vessel in ITS berth is charged, + # which no definition can state - the berth's occupant decides it, and + # since #220 so does how far down the fleet that berth sits. + target = maxi(target, fleet_office_stock_target(id, resource_type)) return maxi(0, target - building_resource(id, resource_type)) diff --git a/tests/test_fleet_office_stock.gd b/tests/test_fleet_office_stock.gd new file mode 100644 index 0000000..40a5d9e --- /dev/null +++ b/tests/test_fleet_office_stock.gd @@ -0,0 +1,103 @@ +extends SceneTree + +# Forgejo #223: a Fleet Office is stocked to a bounded amount - enough that +# its charge is always payable while the bank has anything, and no more. +# +# It used to declare a flat buffer (60 ore, 30 gas, 30 energy) whatever was +# in its berth, so an office keeping an interceptor had transporters haul it +# gas and energy it could never spend, and the amount had no relation to what +# it was actually charged. Since #220 the charge also depends on how far down +# the fleet the berth sits, which no definition can state. +# +# Covers: +# A. an office asks for what its OWN berth is charged, not a flat figure +# B. it asks for nothing of a resource that berth never spends +# C. what it asks for covers at least one charge, so the tax is payable +# D. the request is bounded - a stocked office stops asking +# E. and a deeper berth, which #220 charges more, asks for more + +var state: Node + + +func _initialize() -> void: + state = root.get_node("GameState") + state.buildings.clear() + state.transporters.clear() + state.reset_progression() + state.reset_new_game() + state.intro_active = false + state.fleet_vessel_types = PackedStringArray() + + +func _process(_delta: float) -> bool: + var offices: Array = [] + for i in range(4): + offices.append(state.add_building(GameState.BUILDING_FLEET_OFFICE, + Vector2(600.0 * float(i + 1), 0.0))) + # Berth 2 keeps an interceptor (ore only); berth 5 keeps a laser, which + # is dearer both in kind and in depth. + state.fleet_vessel_types = PackedStringArray([ + GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_INTERCEPTOR, + GameState.VESSEL_LASER, GameState.VESSEL_LASER, GameState.VESSEL_LASER]) + + var first: int = int(offices[0]) + var charge: Dictionary = state.fleet_office_upkeep( + GameState.VESSEL_INTERCEPTOR, GameState.BASE_FLEET_SIZE) + var ore_charge: int = int(charge.get(GameState.RESOURCE_ORE, 0)) + if ore_charge <= 0: + return _fail("an interceptor berth is charged no ore, so this proves nothing") + + # --- A + C. it asks for its own charge, several times over --------- + var ore_wanted: int = state.building_requested_resource(first, GameState.RESOURCE_ORE) + if ore_wanted < ore_charge: + return _fail("an empty office asks for %d ore against a %d charge - it can starve with ore banked" \ + % [ore_wanted, ore_charge]) + if ore_wanted < ore_charge * GameState.FLEET_OFFICE_STOCK_CHARGES: + return _fail("asks for %d ore, under the %d charges it is meant to hold" \ + % [ore_wanted, GameState.FLEET_OFFICE_STOCK_CHARGES]) + # A cheap hull's charges come to less than the small floor the definition + # declares, and the floor wins - that floor is what keeps a berth payable + # before its first delivery lands. The dynamic term is what matters for a + # dear one, which is checked at berth 5 below. + + # --- B. and nothing it cannot spend -------------------------------- + # An interceptor costs ore alone, so the gas request is the definition's + # small floor rather than a buffer's worth of cargo it will never burn. + var gas_wanted: int = state.building_requested_resource(first, GameState.RESOURCE_GAS) + if gas_wanted >= ore_wanted: + return _fail("an ore-only berth asks for %d gas against %d ore" % [gas_wanted, ore_wanted]) + print("berth 2 (interceptor): %d ore wanted for a %d charge, %d gas" \ + % [ore_wanted, ore_charge, gas_wanted]) + + # --- D. bounded: stocked, it stops asking --------------------------- + state.deposit_resource_to_building(first, GameState.RESOURCE_ORE, ore_wanted) + if state.building_requested_resource(first, GameState.RESOURCE_ORE) != 0: + return _fail("a stocked office is still asking for ore - the fill is unbounded") + state.deposit_resource_to_building(first, GameState.RESOURCE_ORE, 500) + if state.building_requested_resource(first, GameState.RESOURCE_ORE) != 0: + return _fail("an over-stocked office asks for still more") + print("stocked, it asks for nothing further") + + # --- E. a deeper berth costs more, and asks for more ---------------- + var deep: int = int(offices[3]) + var deep_ore: int = state.building_requested_resource(deep, GameState.RESOURCE_ORE) + var shallow_laser: Dictionary = state.fleet_office_upkeep( + GameState.VESSEL_LASER, GameState.BASE_FLEET_SIZE) + if deep_ore <= int(shallow_laser.get(GameState.RESOURCE_ORE, 0)) * GameState.FLEET_OFFICE_STOCK_CHARGES - 1: + return _fail("berth 5 asks for %d ore, no more than the same hull would at berth 2" % deep_ore) + # A laser burns energy too, so that berth asks for energy where the + # interceptor's did not. + if state.building_requested_resource(deep, GameState.RESOURCE_ENERGY) \ + <= state.building_requested_resource(first, GameState.RESOURCE_ENERGY): + return _fail("a laser berth asks for no more energy than an interceptor berth") + print("berth 5 (laser, deeper): %d ore and energy on top" % deep_ore) + + print("TEST_FLEET_OFFICE_STOCK: PASS") + quit(0) + return true + + +func _fail(message: String) -> bool: + print("TEST_FLEET_OFFICE_STOCK: FAIL (%s)" % message) + quit(1) + return true