diff --git a/core/GameState.gd b/core/GameState.gd index 7354e73..adcab6b 100644 --- a/core/GameState.gd +++ b/core/GameState.gd @@ -25,6 +25,10 @@ var surviving_vessel_types: PackedStringArray = PackedStringArray() # whenever the composition changes. GameState cannot see scene nodes, and the # census the Planning Center works from has to include the fleet. var fleet_vessel_types: PackedStringArray = PackedStringArray() +# Where the fleet is, written by Fleet each frame. Only #158 needs it: a +# building may draw on the hold when the hold is actually there, and this +# autoload has no other way to know. +var fleet_position: Vector2 = Vector2.ZERO # #122: the prologue is running. Set by reset_new_game() so it plays once at # the start of a NEW game and never on a resumed one, and cleared by the # director when the prologue hands over. @@ -693,6 +697,7 @@ func reset_new_game() -> void: surviving_ship_count = STARTING_SHIP_COUNT surviving_vessel_types = PackedStringArray() fleet_vessel_types = PackedStringArray() + fleet_position = Vector2.ZERO return_world_position = Vector2.ZERO return_camera_zoom = 1.0 planet_name = "" @@ -854,8 +859,23 @@ func building_health_fraction(id: int) -> float: # savings and their stored vessels with no route back, so it bottoms out at # one hit point instead of dying. Keyed on the capability rather than on the # type: whatever building holds the bank is the one that cannot be lost. -func building_is_indestructible(building: Dictionary) -> bool: - return building_has_capability(building, "bank") +# #159: "a building with no life should explode" - and nothing is exempt any +# more. The mothership used to floor at 1hp, so a battered one sat there +# reading as dead and never came down. +# +# That exemption was there to stop a soft-lock, and the soft-lock turns out +# not to exist. Losing the structure does not lose what is in it: the bank is +# GameState.bank_inventory and the hangar is stored_vessels, both of which +# outlive any building. And rebuilding is always available - the mothership +# is the one structure research never gates, it costs exactly what a single +# interceptor's hold carries, and ore comes from asteroids, which do not run +# out. So the way back is always mine, fly, rebuild. +# +# Kept as a function rather than deleted because the QUESTION is still a real +# one - something may well need to be un-killable later - and a caller asking +# it is clearer than a caller that has forgotten it was ever asked. +func building_is_indestructible(_building: Dictionary) -> bool: + return false # Returns true when this killed the building. @@ -1489,6 +1509,68 @@ func queue_order(id: int, product: String = PRODUCT_SHIP) -> bool: return true +# What a building can actually pay a local cost with (#158). +# +# #12 made production cost resources physically DELIVERED to the building +# rather than money out of the bank, and that stands: the bank is still not +# raided from here. What #158 adds is the fleet's own hold - the one other +# place resources physically are. A hull that flew in carrying gas can hand +# it over, and making the player park, open the transfer menu and move it +# across by hand was ceremony rather than a decision. +# +# But only WHERE THE FLEET IS, and that qualifier is the whole design. +# Without it a crafter on the far side of the map reaches into the hold and +# spends ore the player withdrew to carry somewhere else - which is exactly +# the complaint that got automatic banking at the mothership removed in #24, +# arriving from the other direction. test_manual_supply_route caught it +# doing precisely that. +# +# The range is Building.interact_radius, the same distance that decides +# whether the transfer button is offered at all. So the rule is not a hidden +# one: if you could hand the cargo over by hand, the building can take it. +const LOCAL_PAY_RANGE: float = 150.0 + + +func fleet_is_at(building: Dictionary) -> bool: + if building.is_empty(): + return false + return fleet_position.distance_to(building.position) <= LOCAL_PAY_RANGE + + +func local_payment_available(building: Dictionary, inventory: RefCounted, + resource_type: String) -> int: + var here: int = 0 if inventory == null else inventory.amount(resource_type) + return here + (fleet_inventory.amount(resource_type) if fleet_is_at(building) else 0) + + +func can_pay_locally(building: Dictionary, inventory: RefCounted, + costs: Dictionary) -> bool: + for resource_type in costs: + if local_payment_available(building, inventory, String(resource_type)) \ + < int(costs[resource_type]): + return false + return true + + +# All or nothing, settled in full before anything is taken - a part-charged +# order that then fails is worse than one that never started. The building's +# own stock goes first, so what was hauled in is spent before the hold the +# player is still flying around with. +func pay_locally(building: Dictionary, inventory: RefCounted, + costs: Dictionary) -> bool: + if not can_pay_locally(building, inventory, costs): + return false + var at_hand: bool = fleet_is_at(building) + for resource_type in costs: + var id: String = String(resource_type) + var owed: int = int(costs[resource_type]) + if inventory != null: + owed -= inventory.withdraw(id, owed) + if owed > 0 and at_hand: + fleet_inventory.withdraw(id, owed) + return true + + func _advance_production(delta: float) -> void: for b in buildings: if not can_produce(b.type): @@ -1508,15 +1590,9 @@ func _advance_production(delta: float) -> void: var definition: Resource = vessel_definition(product) if not bool(order.started): - var affordable := true - for resource_type in costs: - if inventory.amount(String(resource_type)) < int(costs[resource_type]): - affordable = false - break - if not affordable: + # #158: the crafter's own stock, or the hold that flew in. + if not pay_locally(b, inventory, costs): continue - for resource_type in costs: - inventory.withdraw(String(resource_type), int(costs[resource_type])) order.started = true order.elapsed = float(order.elapsed) + delta @@ -2038,11 +2114,8 @@ func _advance_lab_research(delta: float) -> void: var costs: Dictionary = research_costs(id) var inventory: RefCounted = _inventory_for_record(lab) if not bool(order.started): - var affordable := true - for resource_type in costs: - if inventory.amount(String(resource_type)) < int(costs[resource_type]): - affordable = false - break + # #158: the lab's own stock, or the hold that flew in. + var affordable: bool = can_pay_locally(lab, inventory, costs) # #63: research points are a fleet-wide currency, not delivered # to the lab like ore/gas - checked and spent here too so a # shortfall stalls the order the same way an undelivered @@ -2052,8 +2125,8 @@ func _advance_lab_research(delta: float) -> void: affordable = false if not affordable: continue - for resource_type in costs: - inventory.withdraw(String(resource_type), int(costs[resource_type])) + if not pay_locally(lab, inventory, costs): + continue if points_needed > 0: research_points -= points_needed research_points_changed.emit(research_points) diff --git a/tests/test_building_health.gd b/tests/test_building_health.gd index 5bb4417..644e48a 100644 --- a/tests/test_building_health.gd +++ b/tests/test_building_health.gd @@ -17,9 +17,12 @@ extends SceneTree # record outright # C. a record written before this existed reads as undamaged, not as # rubble - an existing save must not open on a field of wrecks -# D. the mothership cannot be destroyed. The bank, the hangar and research -# all live there and a wiped fleet rebuilds there, so losing it would -# be a soft-lock; it bottoms out at 1hp instead +# D. #159 - NOTHING is exempt from coming down, the mothership included. +# It used to floor at 1hp to avoid a soft-lock, and a battered one sat +# there reading as dead and never exploded. The soft-lock does not +# exist: what is IN the mothership outlives it, and rebuilding it is +# always reachable - so the way back is checked here rather than the +# exemption that used to stand in for it # E. destroying a transport hub takes its transporters with it, and # nothing else keyed on the id is left pointing at a hole # F. the Building node follows its record out of existence, and shows @@ -96,22 +99,50 @@ func _run_state_cases() -> bool: return _fail("one point of damage levelled a building carried over from an old save") print("a pre-#99 record reads as undamaged (%d hp), not as rubble" % lab_full) - # --- D. the mothership survives anything ---------------------------- + # --- D. #159: the mothership comes down like anything else ---------- var mothership_id: int = game_state.add_building(GameState.BUILDING_MOTHERSHIP, Vector2.ZERO) - if not game_state.building_is_indestructible(game_state.find_building(mothership_id)): - return _fail("the mothership is not marked indestructible") - if game_state.damage_building(mothership_id, 999999): - return _fail("the mothership was destroyed - the bank, hangar and research went with it") - if game_state.building_hp(mothership_id) != 1: - return _fail("an overwhelmed mothership sits at %d hp, expected to floor at 1" \ - % game_state.building_hp(mothership_id)) - if game_state.destroy_building(mothership_id): - return _fail("destroy_building demolished the mothership outright") - if game_state.repair_building(mothership_id, 999999) <= 0: + if game_state.building_is_indestructible(game_state.find_building(mothership_id)): + return _fail("the mothership is still exempt - it would sit at 1hp reading as dead") + # Stock the things that used to be the reason for the exemption, so + # their survival is observed rather than assumed. + game_state.bank_inventory.set_amount(GameState.RESOURCE_ORE, 400) + game_state.stored_vessels.append(GameState.VESSEL_INTERCEPTOR) + if not game_state.damage_building(mothership_id, 999999): + return _fail("enough damage did not destroy the mothership") + if not game_state.find_building(mothership_id).is_empty(): + return _fail("the mothership record outlived its own destruction") + + # ...and the way back. This is what the exemption was protecting, so it + # is what has to be true without it. + if game_state.banked_resource(GameState.RESOURCE_ORE) != 400: + return _fail("the bank died with the building - it holds %d" \ + % game_state.banked_resource(GameState.RESOURCE_ORE)) + if game_state.stored_vessels.is_empty(): + return _fail("the hangar died with the building") + if game_state.research_for_building(GameState.BUILDING_MOTHERSHIP) != "": + return _fail("rebuilding the mothership needs research, which needs a mothership") + var rebuild: int = int(game_state.building_costs(GameState.BUILDING_MOTHERSHIP).get( + GameState.RESOURCE_ORE, 0)) + var one_hold: int = game_state.fleet_cargo_capacity( + PackedStringArray([GameState.VESSEL_INTERCEPTOR])) + if rebuild > one_hold: + return _fail("a rebuild costs %d ore and one surviving hull carries %d - unreachable with the bank gone" \ + % [rebuild, one_hold]) + print("the mothership comes down; bank, hangar and an ungated %d-ore rebuild all survive it" \ + % rebuild) + # Repair, on a fresh one - the last was destroyed on purpose above. + var rebuilt_id: int = game_state.add_building(GameState.BUILDING_MOTHERSHIP, Vector2.ZERO) + var at_full: int = game_state.building_max_hp(GameState.BUILDING_MOTHERSHIP) + if game_state.damage_building(rebuilt_id, at_full - 5): + return _fail("damage short of the last few points still destroyed it") + if game_state.building_hp(rebuilt_id) != 5: + return _fail("a battered mothership sits at %d hp, expected 5" \ + % game_state.building_hp(rebuilt_id)) + if game_state.repair_building(rebuilt_id, 999999) <= 0: return _fail("a battered mothership could not be repaired") - if game_state.building_hp(mothership_id) != game_state.building_max_hp(GameState.BUILDING_MOTHERSHIP): + if game_state.building_hp(rebuilt_id) != at_full: return _fail("repair did not take the mothership back to full") - print("the mothership floors at 1hp and repairs to full") + print("a rebuilt one takes damage down to %d and repairs back to %d" % [5, at_full]) # --- 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)) diff --git a/tests/test_local_costs_use_hold.gd b/tests/test_local_costs_use_hold.gd new file mode 100644 index 0000000..c4ee766 --- /dev/null +++ b/tests/test_local_costs_use_hold.gd @@ -0,0 +1,191 @@ +extends SceneTree + +# Headless test: run with +# godot --headless -s res://tests/test_local_costs_use_hold.gd +# Forgejo #158: "any costs local to a building (creating a ship in the ship +# crafter, researching in the lab...) can also be paid thanks to the ship +# inventory." +# +# #12 made a building's production cost resources physically DELIVERED to it +# rather than money out of the bank, and that principle stands - the bank is +# still not raided from here. What changes is that the fleet's own hold +# counts as well, because it is the one other place resources physically +# are: a hull that flew in carrying gas can hand it over, and making the +# player park, open the transfer menu and move it across by hand was +# ceremony rather than a decision. +# +# Covers: +# A. a crafter with an empty stock builds out of the fleet's hold +# B. the building's OWN stock is spent first - what was hauled in goes +# before the hold the player is still flying around with +# C. a cost split across both is taken from both +# D. the bank is still not touched: banked ore alone does not start an +# order, which is the #12 rule this must not quietly undo +# E. all or nothing - an order that cannot be paid for takes nothing +# F. the same for lab research +# G. and only WHERE THE FLEET IS. A crafter across the map must not reach +# into the hold and spend ore the player withdrew to carry elsewhere - +# which is the same complaint that got automatic banking removed in #24, +# arriving from the other direction. test_manual_supply_route caught the +# first draft of this doing exactly that. + +var game_state: Node +var crafter_id: int = 0 +var lab_id: int = 0 + + +func _fail(message: String) -> bool: + print("TEST_LOCAL_COSTS_USE_HOLD: FAIL (%s)" % message) + quit(1) + return true + + +# Emptied rather than replaced: GameState.INVENTORY is reached through the +# autoload's name, which an `extends SceneTree` script cannot compile. +func _reset() -> void: + for resource_type in game_state.resource_types(): + var id: String = String(resource_type) + game_state.bank_inventory.set_amount(id, 0) + game_state.fleet_inventory.set_amount(id, 0) + game_state.building_inventory(crafter_id).set_amount(id, 0) + game_state.building_inventory(lab_id).set_amount(id, 0) + game_state.fleet_inventory.total_capacity = 500 + for b in game_state.buildings: + b["orders"] = [] + b["research_orders"] = [] + + +func _initialize() -> void: + # GameState is an autoload: a bare GameState. does not + # compile in a SceneTree script, so the node is fetched by name instead. + game_state = root.get_node("GameState") + game_state.reset_new_game() + game_state.intro_active = false + game_state.reset_quests() + game_state.build_time_scale = 0.02 + crafter_id = game_state.add_building(GameState.BUILDING_SHIP_CRAFTER, Vector2.ZERO) + lab_id = game_state.add_building(GameState.BUILDING_SHIP_LAB, Vector2(500.0, 0.0)) + # Parked on the crafter. The hold only pays for what the fleet is + # standing at - see G below, which is the half of #158 that matters. + game_state.fleet_position = Vector2.ZERO + + var cost: int = int(game_state.product_costs(GameState.VESSEL_INTERCEPTOR).get( + GameState.RESOURCE_ORE, 0)) + if cost <= 1: + _fail("an interceptor costs %d ore - nothing here would be measurable" % cost) + return + + # --- A. built out of the hold alone --------------------------------- + _reset() + game_state.fleet_inventory.deposit(GameState.RESOURCE_ORE, cost) + if not game_state.queue_order(crafter_id, GameState.VESSEL_INTERCEPTOR): + _fail("the order would not queue") + return + game_state._advance_production(0.01) + var order: Dictionary = game_state.order_list(crafter_id)[0] + if not bool(order.started): + _fail("a crafter with the fleet parked on it and %d ore aboard did not start" % cost) + return + if game_state.fleet_resource(GameState.RESOURCE_ORE) != 0: + _fail("the hold still holds %d after paying" \ + % game_state.fleet_resource(GameState.RESOURCE_ORE)) + return + print("an empty crafter built out of the fleet's hold (%d ore)" % cost) + + # --- B and C. the building's own stock goes first ------------------- + _reset() + for b in game_state.buildings: + b["orders"] = [] + var in_building: int = cost - 1 + game_state.building_inventory(crafter_id).set_amount(GameState.RESOURCE_ORE, in_building) + game_state.fleet_inventory.deposit(GameState.RESOURCE_ORE, 5) + game_state.queue_order(crafter_id, GameState.VESSEL_INTERCEPTOR) + game_state._advance_production(0.01) + if not bool(game_state.order_list(crafter_id)[0].started): + _fail("a cost split across the crafter and the hold did not start") + return + if game_state.building_resource(crafter_id, GameState.RESOURCE_ORE) != 0: + _fail("the crafter's own %d ore was not spent first - %d left" \ + % [in_building, game_state.building_resource(crafter_id, GameState.RESOURCE_ORE)]) + return + if game_state.fleet_resource(GameState.RESOURCE_ORE) != 4: + _fail("the hold paid %d, expected to cover the last 1" \ + % (5 - game_state.fleet_resource(GameState.RESOURCE_ORE))) + return + print("stock first, then the hold: %d from the crafter and 1 from the fleet" % in_building) + + # --- D. the bank is still not raided -------------------------------- + _reset() + game_state.bank_inventory.set_amount(GameState.RESOURCE_ORE, 5000) + game_state.queue_order(crafter_id, GameState.VESSEL_INTERCEPTOR) + game_state._advance_production(0.01) + if bool(game_state.order_list(crafter_id)[0].started): + _fail("the order started out of the BANK - #12's rule is undone") + return + if game_state.banked_resource(GameState.RESOURCE_ORE) != 5000: + _fail("the bank was charged %d for an order that did not start" \ + % (5000 - game_state.banked_resource(GameState.RESOURCE_ORE))) + return + print("banked ore alone does not start an order, and is not touched") + + # --- E. all or nothing ---------------------------------------------- + _reset() + game_state.fleet_inventory.deposit(GameState.RESOURCE_ORE, cost - 1) + game_state.queue_order(crafter_id, GameState.VESSEL_INTERCEPTOR) + game_state._advance_production(0.01) + if bool(game_state.order_list(crafter_id)[0].started): + _fail("an order started one ore short") + return + if game_state.fleet_resource(GameState.RESOURCE_ORE) != cost - 1: + _fail("an order that could not be paid for still took %d from the hold" \ + % (cost - 1 - game_state.fleet_resource(GameState.RESOURCE_ORE))) + return + print("one short takes nothing at all") + + # --- F. and the lab does the same ----------------------------------- + _reset() + var upgrade: String = "int_damage" + var research: Dictionary = game_state.research_costs(upgrade) + if research.is_empty(): + _fail("the upgrade %s costs nothing - F would prove nothing" % upgrade) + return + game_state.research_points = 99 + game_state.fleet_position = Vector2(500.0, 0.0) + for resource_type in research: + game_state.fleet_inventory.deposit(String(resource_type), int(research[resource_type])) + if not game_state.queue_lab_research(lab_id, upgrade): + _fail("the research would not queue") + return + game_state._advance_lab_research(0.01) + if not bool(game_state.lab_research_order(lab_id).started): + _fail("research did not start from the fleet's hold") + return + for resource_type in research: + if game_state.fleet_resource(String(resource_type)) != 0: + _fail("the hold still holds %s after paying for research" % resource_type) + return + print("a lab researches out of the hold too (%s)" % str(research)) + + # --- G. and not from across the map --------------------------------- + _reset() + game_state.fleet_position = Vector2(9000.0, 9000.0) + game_state.fleet_inventory.deposit(GameState.RESOURCE_ORE, cost * 4) + game_state.queue_order(crafter_id, GameState.VESSEL_INTERCEPTOR) + game_state._advance_production(0.01) + if bool(game_state.order_list(crafter_id)[0].started): + _fail("a crafter on the far side of the map started out of the fleet's hold") + return + if game_state.fleet_resource(GameState.RESOURCE_ORE) != cost * 4: + _fail("a distant crafter took %d from the hold" \ + % (cost * 4 - game_state.fleet_resource(GameState.RESOURCE_ORE))) + return + # ...and flying back is all it takes. + game_state.fleet_position = Vector2.ZERO + game_state._advance_production(0.01) + if not bool(game_state.order_list(crafter_id)[0].started): + _fail("the order did not start once the fleet was parked on the crafter") + return + print("out of range it takes nothing; parked on it, the same order starts") + + print("TEST_LOCAL_COSTS_USE_HOLD: PASS") + quit(0) diff --git a/tests/test_local_costs_use_hold.gd.uid b/tests/test_local_costs_use_hold.gd.uid new file mode 100644 index 0000000..d121b4f --- /dev/null +++ b/tests/test_local_costs_use_hold.gd.uid @@ -0,0 +1 @@ +uid://b0dsxrl5b47a7 diff --git a/tests/test_manual_supply_route.gd b/tests/test_manual_supply_route.gd index d0a3960..3a65e95 100644 --- a/tests/test_manual_supply_route.gd +++ b/tests/test_manual_supply_route.gd @@ -18,7 +18,12 @@ extends SceneTree # triggered on arrival (#14); #24 removed that entirely, because an # automatic one-way move overrides a player who has just loaded the # hold in order to carry ore somewhere else -# 4. a building takes the fleet's whole hold when opened +# 4. a building takes the fleet's whole hold when opened - and since #158 +# it does not need to be asked: a queued order at a building the fleet +# is PARKED ON pays itself out of the hold, so the hand-off happens on +# arrival rather than through the transfer menu. The manual move still +# works and is still what stocks a building ahead of an order, which is +# checked separately here # 5. end to end: a queued order the bank cannot reach is fulfilled using # only the fleet, with no Transport Hub in existence # 6. nothing is created or destroyed anywhere along the route @@ -153,12 +158,31 @@ func _process(delta: float) -> bool: 5: if elapsed < 0.6: return false - var moved: int = game_state.transfer_to_building(crafter_id, game_state.fleet_cargo) - if moved != GameState.SHIP_COST or game_state.fleet_cargo != 0: - print("TEST_MANUAL_SUPPLY_ROUTE: FAIL (unloaded %d, hold left at %d)" % [moved, game_state.fleet_cargo]) + # #158: parked on the crafter with the ore aboard, the queued + # order takes it - the flight IS the delivery now, and the hold + # is empty because it has been spent rather than because nothing + # happened. + if game_state.fleet_cargo != 0: + print("TEST_MANUAL_SUPPLY_ROUTE: FAIL (parked on the crafter with %d still in the hold)" % game_state.fleet_cargo) quit(1) return true - print("unloaded %d into the crafter" % moved) + # Orders run at build_time_scale 0.02 here, so by now it has not + # only started on the hold's ore but finished. + if game_state.pending_orders(crafter_id) != 0: + print("TEST_MANUAL_SUPPLY_ROUTE: FAIL (the hold emptied but the order is still queued)") + quit(1) + return true + print("parked on the crafter: the queued order took the hold's %d itself" % GameState.SHIP_COST) + + # ...and the manual move still works, which is what stocks a + # building AHEAD of an order rather than paying for one. + game_state.collect_cargo(4) + var moved: int = game_state.transfer_to_building(crafter_id, 4) + if moved != 4 or game_state.fleet_cargo != 0: + print("TEST_MANUAL_SUPPLY_ROUTE: FAIL (hand-unloaded %d of 4, hold left at %d)" % [moved, game_state.fleet_cargo]) + quit(1) + return true + print("and a hand transfer still moves %d into the crafter" % moved) elapsed = 0.0 stage = 6 return false @@ -183,7 +207,9 @@ func _process(delta: float) -> bool: print("queued order fulfilled by hand, no transport hub: %d vessel in the hangar" % game_state.stored_vessels.size()) # --- 6. conservation ---------------------------------------- - var expected: int = total_before - GameState.SHIP_COST + # The 4 mined above to exercise the hand transfer are still in + # the crafter's stock, so they are part of the total. + var expected: int = total_before - GameState.SHIP_COST + 4 if _total() != expected: print("bank=%d hold=%d stocks total=%d, expected %d" % [ game_state.rocks, game_state.fleet_cargo, _total(), expected diff --git a/tests/test_quest_chain.gd b/tests/test_quest_chain.gd index 8cf2aa3..f14cd62 100644 --- a/tests/test_quest_chain.gd +++ b/tests/test_quest_chain.gd @@ -31,6 +31,8 @@ extends SceneTree # a quest of its own, so research is taught rather than assumed # R. #145 - every structure quest is actually finishable: building the # thing it asks for, the way the game builds it, completes it +# S. #157 - nothing costing more than a single hold is asked for before +# the chain has grown the fleet or taught banking # I. lines survive having nobody to say them: a quest finished with no # banner alive is read out by the next one that is # J. a planet surface carries the chain too - a quest finished down there @@ -196,6 +198,35 @@ func _catalogue_cases() -> bool: print("%d ore is asked for before the %s that costs %d, and the opening hold carries %d" \ % [gathered, build_type, build_price, hold]) + # --- S. #157: a second hull before anything one hull cannot pay for -- + # A structure costing more than a single hold cannot be bought in one + # trip, so the chain has to have grown the fleet - or taught banking - + # before it asks for one. Both are in front of the extractor today; this + # fails if either is ever moved behind it. + var one_hold: int = game_state.fleet_cargo_capacity( + PackedStringArray([GameState.VESSEL_INTERCEPTOR])) + var grew_fleet: int = -1 + var taught_banking: int = -1 + for i in range(QUESTS.count()): + var goal_s: Dictionary = QUESTS.at(i).goal + if String(goal_s.kind) == QUESTS.GOAL_FLEET_VESSEL and int(goal_s.count) > 1: + grew_fleet = i if grew_fleet < 0 else grew_fleet + if String(goal_s.kind) == QUESTS.GOAL_BANKED: + taught_banking = i if taught_banking < 0 else taught_banking + if String(goal_s.kind) != QUESTS.GOAL_BUILDING: + continue + var price: int = int(game_state.building_costs(String(goal_s.building)).get( + GameState.RESOURCE_ORE, 0)) + if price <= one_hold: + continue + if grew_fleet < 0 and taught_banking < 0: + return _fail("quest %s wants a %s at %d ore, more than the %d one hull carries, and the chain has neither grown the fleet nor taught banking yet" \ + % [QUESTS.at(i).id, goal_s.building, price, one_hold]) + if grew_fleet < 0: + return _fail("no quest ever asks for a second hull") + print("a second hull at %d and banking at %d, both before anything costing more than one %d-unit hold" \ + % [grew_fleet, taught_banking, one_hold]) + # --- Q. #141: research is taught before it is assumed ---------------- # The first structure in the chain that needs a doctrine must have that # doctrine as a quest of its own first. Only the FIRST one: once the diff --git a/vessels/Fleet.gd b/vessels/Fleet.gd index dd45d00..b96d157 100644 --- a/vessels/Fleet.gd +++ b/vessels/Fleet.gd @@ -228,6 +228,11 @@ func move_in_direction(direction: Vector2) -> void: func _process(delta: float) -> void: + # #158: a building may pay a local cost out of the hold when the fleet is + # actually standing at it, and the autoload has no other way to know + # where that is. Written before the early return below - a parked fleet + # is exactly the case that matters. + GameState.fleet_position = get_centroid() if steer_direction == Vector2.ZERO: return # The anchor - the fleet's formation centre - travels at cruise_speed.