Pay for everything from the bank and the fleet hold, not just buildings

Construction already pooled the two; research, ship-lab upgrades and vessel
unlocks still charged the bank alone. That split was arbitrary from the
player's side - the resources are a couple of taps away from being banked
either way, so refusing a purchase over where they happen to be sitting was
ceremony. Worse, it was inconsistent: the same ore could buy a mothership and
not a skill node.

So the pooling moved down into can_afford/spend themselves, and the
construction-specific pair is gone. can_afford_resources now pools per
resource type, and a new spend_resources() charges bank first and hold for
the shortfall, settling affordability in full before taking anything - a
multi-resource price must not charge for the half it can cover and then fail.
unlock_vessel goes through it instead of withdrawing from the bank directly.

The skill menu's "you have %d" quoted the bank, which would have told the
player they could not afford something they could.

Ammo stays out of it. AimedFleetAbilityController reads the fleet hold
directly, and should: a gas bomb is fired from what the ships are physically
carrying, which is a fact about where the matter is rather than a convenience
about where the money is.

test_build_uses_fleet_hold became test_payments_use_fleet_hold and now walks
every route - skill tree, ship lab, vessel research, multi-resource
atomicity, and ammo staying unpooled. Two of its own setup steps were being
silently clamped by the fleet hold's capacity, which is what made an
affordable price look unaffordable; capacity is not what those checks are
about, so it is taken out of the way explicitly.

Full suite 106/106.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Loïc Wisniewski 2026-08-19 12:15:00 +02:00
commit 821da05c9f
5 changed files with 341 additions and 215 deletions

View file

@ -705,8 +705,8 @@ func _show_building_info(type: String) -> void:
# The same pooled test the build itself uses. Asking the bank here while
# charging bank-plus-hold below would hide the build button for something
# the player can in fact afford.
if can_build and not GameState.can_afford_construction(cost):
lines.append("Not enough rocks - needs %d, you have %d." % [cost, GameState.construction_ore()])
if can_build and not GameState.can_afford(cost):
lines.append("Not enough rocks - needs %d, you have %d." % [cost, GameState.available_ore()])
can_build = false
info_title.text = definition.display_name
@ -753,17 +753,17 @@ func _confirm_build() -> void:
# glyph/HUD rendering the moment it produced anything.
build_status.text = "%s has nothing to extract" % planet.planet_name
return
if not GameState.spend_construction(definition.cost):
build_status.text = "Not enough rocks (need %d, have %d)" % [definition.cost, GameState.construction_ore()]
if not GameState.spend(definition.cost):
build_status.text = "Not enough rocks (need %d, have %d)" % [definition.cost, GameState.available_ore()]
return
GameState.add_extractor(planet.planet_name, planet.position, planet.resource_type, planet.planet_id)
build_menu.visible = false
return
if definition.unique and GameState.has_building_of_type(type):
if not GameState.can_afford_construction(definition.cost):
if not GameState.can_afford(definition.cost):
build_status.text = "Not enough rocks (need %d, have %d)" % [
definition.cost, GameState.construction_ore()
definition.cost, GameState.available_ore()
]
return
# Only now is the old one destroyed - never before the new one is
@ -775,8 +775,8 @@ func _confirm_build() -> void:
func _try_place(type: String, cost: int) -> void:
if not GameState.spend_construction(cost):
build_status.text = "Not enough rocks (need %d, have %d)" % [cost, GameState.construction_ore()]
if not GameState.spend(cost):
build_status.text = "Not enough rocks (need %d, have %d)" % [cost, GameState.available_ore()]
return
building_field.place(type, fleet.get_centroid())
build_menu.visible = false
@ -817,7 +817,10 @@ func _refresh_skill_menu(id: String = "") -> void:
skill_status.text = "Needs %s first" % GameState.skill_node(node.requires).name
skill_unlock_button.visible = false
elif not GameState.can_afford(int(node.cost)):
skill_status.text = "Not enough rocks - you have %d" % GameState.rocks
# available_ore(), not the bank alone: research is paid for the same
# pooled way as everything else, so quoting the bank here would tell
# the player they cannot afford something they can.
skill_status.text = "Not enough rocks - you have %d" % GameState.available_ore()
skill_unlock_button.visible = false
elif GameState.research_points < points_needed:
# #63: a distinct reason from the ore one above - conflating them
@ -1110,8 +1113,10 @@ func _transfer_place_name() -> String:
if b.is_empty():
return "this building"
if GameState.building_has_capability(b, "bank"):
# Worth naming as the bank rather than as a structure: this is the
# one building where unloading makes the ore spendable everywhere.
# Worth naming as the bank rather than as a structure: it is the
# shared pool every building's transporters draw from, not just
# another store. (It is no longer where ore has to be to be SPENT -
# purchases now draw on the fleet hold too.)
return "the bank"
return "this building"