spaceship/tests/test_payments_use_fleet_hold.gd

306 lines
14 KiB
GDScript3
Raw Normal View History

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>
2026-08-19 12:15:00 +02:00
extends SceneTree
# Headless gameplay test: run with
# godot --headless -s res://tests/test_payments_use_fleet_hold.gd
2026-09-02 13:43:56 +02:00
# A payment draws on the bank and, only at the place being paid, what the
# ships are carrying. Fleet cargo is physical: a building across the map may
# not reach into it (#180).
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>
2026-08-19 12:15:00 +02:00
#
# It was also a deadlock. A new game starts with an empty bank, ore is mined
# into the fleet hold, and the only way to bank it is to deliver it to a
# mothership - so charging construction to the bank alone made the first
# mothership cost ore that could not yet exist.
#
# Covers:
# A. the deadlock premise - a bank-only check rejects what the fleet can
# plainly pay for, so this is guarding something real
# B. paying takes from the bank first and the hold only for the shortfall
# C. an unaffordable purchase takes nothing at all
# D. EVERY payment route is pooled, not just building: the skill tree, ship
# lab upgrades, and multi-resource vessel research
# E. ammo is deliberately NOT pooled - a shot comes out of what the ships
# are physically carrying
# F. end to end: an empty bank and ore in the hold actually gets a
# mothership built through BuildController
2026-09-02 13:43:56 +02:00
# G. no payment position, or a distant one, cannot drain the fleet hold
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>
2026-08-19 12:15:00 +02:00
const HOLD_ORE: int = 40
2026-09-02 13:43:56 +02:00
const PAYMENT_AT := Vector2(120.0, 60.0)
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>
2026-08-19 12:15:00 +02:00
var game_state: Node
var scene: Node
var build_controller: Node
var stage: int = 0
var elapsed: float = 0.0
func _initialize() -> void:
game_state = root.get_node("GameState")
game_state.reset_progression()
game_state.buildings.clear()
game_state.extractors.clear()
2026-09-02 13:43:56 +02:00
game_state.fleet_position = PAYMENT_AT
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>
2026-08-19 12:15:00 +02:00
#138: structures and doctrines cost more than rocks, and more the deeper they are Both halves of the ticket, in the shape vessels have used since gas bombers and laser ships: a definition carries an ore figure and, when it wants more than ore, a dictionary that IS the price. `building_costs()` and `skill_costs()` answer it the way `product_costs()` already did, so there is one question with one answer whatever is being bought, and every payment path now goes through `spend_resources` rather than `spend`. Seven of ten structures and thirteen of seventeen doctrines now want gas, energy or dark matter alongside their rocks, and the totals climb with tree depth - structures 16/31/58/62/96 by depth, doctrines 15/30/73/96/143/258/530. The constraint that shaped the table is REACHABILITY, and it is the reason some prices stayed ore-only. Energy exists only where a gas burner stands, and a gas burner needs Power Systems - so nothing at or before that depth may be priced in energy, or it is a wall with no way through. Dark matter is rarer still and appears once, on the deepest node in the game. The extractor stays rocks-only despite its depth allowing gas, because the quest chain reaches it before it has sent the player for any. test_progressive_costs checks those as properties rather than transcribing the numbers, so rebalancing a price costs nothing while breaking the shape fails: nothing unreachable at its own depth, prices rising with depth, the ore field agreeing with the dictionary (repairs quote the field alone, since a repair drone carries ore and nothing else), and the quest chain never asking for a structure it has not funded. Two things cleaned up on the way: - The per-building cost constants in GameState are gone. Nothing but tests used them, and they were a second copy of a number the definitions already hold - exactly what would drift once a price gained a resource. - research_for_building() returns a display NAME, and the id-keyed half of the tree wants an id. That mix-up is silent - skill_depth() of a name finds no node and answers 0, so a deep structure reads as an ungated one, which is how the first draft of the test let an unreachable price through. research_node_for_building() is the id, and the name is expressed from it. PriceTag no longer rebuilds its labels when re-told the same price; it refreshes the held figures instead. The craft menu re-states prices every frame and the build menu now does too, so that was a dozen Labels being torn down and rebuilt sixty times a second.
2026-08-28 15:30:30 +02:00
var cost: int = int(game_state.building_costs(GameState.BUILDING_MOTHERSHIP)[GameState.RESOURCE_ORE])
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>
2026-08-19 12:15:00 +02:00
# --- A. the deadlock, exactly as a fresh game presents it ---------------
game_state.bank_inventory.set_amount(GameState.RESOURCE_ORE, 0)
# Deliberately more than one starting interceptor can carry (its hold is
# 20, which is exactly MOTHERSHIP_COST), so "the hold paid" shows up as a
# remainder rather than as the hold happening to empty to zero.
game_state.fleet_inventory.total_capacity = HOLD_ORE
game_state.fleet_inventory.set_amount(GameState.RESOURCE_ORE, HOLD_ORE)
if game_state.bank_inventory.amount(GameState.RESOURCE_ORE) >= cost:
_fail("the bank was not actually empty, so nothing below tests the deadlock")
return
2026-09-02 13:43:56 +02:00
if game_state.can_afford(cost):
_fail("a location-free affordability check reached into the fleet hold")
return
if not game_state.can_afford(cost, PAYMENT_AT):
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>
2026-08-19 12:15:00 +02:00
_fail("a fleet holding %d ore could not afford a %d purchase" % [HOLD_ORE, cost])
return
print("bank 0 + hold %d can afford a %d purchase" % [HOLD_ORE, cost])
# --- B. bank first, hold for the shortfall -------------------------------
2026-09-02 13:43:56 +02:00
if not game_state.spend(cost, PAYMENT_AT):
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>
2026-08-19 12:15:00 +02:00
_fail("spend refused a purchase it had just called affordable")
return
if game_state.bank_inventory.amount(GameState.RESOURCE_ORE) != 0 \
or game_state.fleet_inventory.amount(GameState.RESOURCE_ORE) != HOLD_ORE - cost:
_fail("paying from an empty bank should have taken the whole %d from the hold: bank %d, hold %d" \
% [cost, game_state.bank_inventory.amount(GameState.RESOURCE_ORE),
game_state.fleet_inventory.amount(GameState.RESOURCE_ORE)])
return
game_state.bank_inventory.set_amount(GameState.RESOURCE_ORE, 8)
game_state.fleet_inventory.set_amount(GameState.RESOURCE_ORE, 30)
2026-09-02 13:43:56 +02:00
if not game_state.spend(20, PAYMENT_AT):
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>
2026-08-19 12:15:00 +02:00
_fail("a 20 purchase was refused with 8 banked and 30 held")
return
if game_state.bank_inventory.amount(GameState.RESOURCE_ORE) != 0 \
or game_state.fleet_inventory.amount(GameState.RESOURCE_ORE) != 18:
_fail("a split payment should empty the bank first: bank %d, hold %d (expected 0 and 18)" \
% [game_state.bank_inventory.amount(GameState.RESOURCE_ORE),
game_state.fleet_inventory.amount(GameState.RESOURCE_ORE)])
return
print("split payment drained the bank first: bank 8 -> 0, hold 30 -> 18")
# --- C. no partial charge -----------------------------------------------
game_state.bank_inventory.set_amount(GameState.RESOURCE_ORE, 5)
game_state.fleet_inventory.set_amount(GameState.RESOURCE_ORE, 5)
2026-09-02 13:43:56 +02:00
if game_state.spend(50, PAYMENT_AT):
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>
2026-08-19 12:15:00 +02:00
_fail("a purchase costing more than everything owned was allowed")
return
if game_state.bank_inventory.amount(GameState.RESOURCE_ORE) != 5 \
or game_state.fleet_inventory.amount(GameState.RESOURCE_ORE) != 5:
_fail("a rejected purchase still took resources: bank %d, hold %d (expected 5 and 5)" \
% [game_state.bank_inventory.amount(GameState.RESOURCE_ORE),
game_state.fleet_inventory.amount(GameState.RESOURCE_ORE)])
return
print("a purchase nobody can afford takes nothing")
2026-09-02 13:43:56 +02:00
# --- G. cargo does not teleport to a remote purchase ------------------
game_state.bank_inventory.set_amount(GameState.RESOURCE_ORE, 0)
game_state.fleet_inventory.set_amount(GameState.RESOURCE_ORE, cost)
game_state.fleet_position = PAYMENT_AT + Vector2(9000.0, 0.0)
if game_state.spend(cost, PAYMENT_AT):
_fail("a remote purchase took resources from the fleet hold")
return
if game_state.fleet_resource(GameState.RESOURCE_ORE) != cost:
_fail("a refused remote payment still drained the hold")
return
if game_state.spend(cost):
_fail("a purchase with no location reached into the fleet hold")
return
game_state.fleet_position = PAYMENT_AT
print("distant and location-free payments leave fleet cargo untouched")
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>
2026-08-19 12:15:00 +02:00
# The hold has a real capacity (one starting interceptor carries 20), and
# set_amount() is clamped by it - which silently gave the checks below
# less than they asked for and made an affordable price look unaffordable.
# Capacity is not what any of them are about, so take it out of the way;
# the end-to-end stage sets a concrete one again.
game_state.fleet_inventory.total_capacity = -1
if not _check_multi_resource_atomicity():
return
if not _check_skill_tree():
return
if not _check_ship_lab():
return
if not _check_vessel_research():
return
if not _check_ammo_is_not_pooled():
return
# --- F. end to end, through the real build path --------------------------
game_state.bank_inventory.set_amount(GameState.RESOURCE_ORE, 0)
game_state.fleet_inventory.set_amount(GameState.RESOURCE_ORE, HOLD_ORE)
#84: move 215 root files into a folder hierarchy Everything lived in one flat directory, so a diff gave no clue what area it touched and finding the file you wanted meant knowing its exact name. Twelve folders now, grouped by what a thing IS rather than what type it is - a vessel's definition, its component scenes and its upgrade resources sit together, because that is the set you read when you change one of them. buildings core controllers enemies logistics planets resources shaders surface ui vessels vfx world 332 files moved and every res:// reference rewritten - 306 files touched, including the autoload and main-scene paths in project.godot. Nothing renamed: this is purely where files sit, so the diff is a pile of pure moves and no behaviour is buried in it. ui_theme.tres, icon.svg and web_shell.html stay at the root, where Godot and the export preset expect them. Verified three ways, because a suite alone would not have caught all of it: the full 117 pass, the web export builds clean, and the game renders. The export is why that matters. The FIRST export after the move failed with "Attempt to open script 'res://GameState.gd'" - the old path - while project.godot on disk plainly said res://core/GameState.gd. That is this project's documented Android FUSE mtime trap: Godot served a cached copy of a file whose content had changed. A `touch` fixed it. Nothing in the test suite would have found that, because the suite runs from a copy on a native filesystem - the one place the bug cannot happen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 19:44:14 +02:00
scene = load("res://world/Main.tscn").instantiate()
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>
2026-08-19 12:15:00 +02:00
root.add_child(scene)
current_scene = scene
# A price in two resources where only one is coverable must take NEITHER.
func _check_multi_resource_atomicity() -> bool:
game_state.bank_inventory.set_amount(GameState.RESOURCE_ORE, 10)
game_state.fleet_inventory.set_amount(GameState.RESOURCE_ORE, 10)
game_state.bank_inventory.set_amount(GameState.RESOURCE_GAS, 1)
game_state.fleet_inventory.set_amount(GameState.RESOURCE_GAS, 1)
2026-09-02 13:43:56 +02:00
if game_state.spend_resources({GameState.RESOURCE_ORE: 15, GameState.RESOURCE_GAS: 40}, PAYMENT_AT):
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>
2026-08-19 12:15:00 +02:00
return _fail("a two-resource price was charged with only one of them affordable")
if game_state.available_ore() != 20 or game_state.available_resource(GameState.RESOURCE_GAS) != 2:
return _fail("a refused two-resource purchase still took some ore or gas (ore %d, gas %d)" \
% [game_state.available_ore(), game_state.available_resource(GameState.RESOURCE_GAS)])
2026-09-02 13:43:56 +02:00
if not game_state.spend_resources({GameState.RESOURCE_ORE: 15, GameState.RESOURCE_GAS: 2}, PAYMENT_AT):
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>
2026-08-19 12:15:00 +02:00
return _fail("an affordable two-resource price was refused")
if game_state.available_ore() != 5 or game_state.available_resource(GameState.RESOURCE_GAS) != 0:
return _fail("a two-resource purchase charged wrongly (ore %d left, gas %d left)" \
% [game_state.available_ore(), game_state.available_resource(GameState.RESOURCE_GAS)])
print("multi-resource prices are all-or-nothing, and pooled per resource")
return true
func _check_skill_tree() -> bool:
var id: String = "shipyards"
var node: Dictionary = game_state.skill_node(id)
var cost: int = int(node.cost)
game_state.unlocked.clear()
game_state.research_points = 999
game_state.bank_inventory.set_amount(GameState.RESOURCE_ORE, 0)
game_state.fleet_inventory.set_amount(GameState.RESOURCE_ORE, cost)
2026-09-02 13:43:56 +02:00
if not game_state.can_unlock(id, PAYMENT_AT):
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>
2026-08-19 12:15:00 +02:00
return _fail("the skill tree refused '%s' (cost %d) with the whole price in the fleet hold" % [id, cost])
2026-09-02 13:43:56 +02:00
if not game_state.unlock(id, PAYMENT_AT):
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>
2026-08-19 12:15:00 +02:00
return _fail("unlocking '%s' failed despite can_unlock saying yes" % id)
if game_state.fleet_inventory.amount(GameState.RESOURCE_ORE) != 0:
return _fail("the skill unlock did not charge the hold: %d left of %d" \
% [game_state.fleet_inventory.amount(GameState.RESOURCE_ORE), cost])
print("skill tree: unlocked '%s' for %d paid entirely from the hold" % [id, cost])
return true
func _check_ship_lab() -> bool:
# Ship-lab upgrades need a lab standing; the subject is where the money
# comes from, not the prerequisite.
game_state.buildings.clear()
game_state.buildings.append({"id": 1, "type": GameState.BUILDING_SHIP_LAB,
"position": Vector2.ZERO})
if not game_state.has_ship_lab():
return _fail("could not place a ship lab, so the upgrade path cannot be tested")
var id: String = ""
for upgrade in game_state.upgrades_for(GameState.VESSEL_INTERCEPTOR):
if not game_state.upgrade_maxed(String(upgrade.id)):
id = String(upgrade.id)
break
if id == "":
return _fail("no ship-lab upgrade was available to buy")
var cost: int = game_state.upgrade_cost(id)
game_state.research_points = 999
game_state.bank_inventory.set_amount(GameState.RESOURCE_ORE, 0)
game_state.fleet_inventory.set_amount(GameState.RESOURCE_ORE, cost)
2026-09-02 13:43:56 +02:00
if not game_state.can_buy_upgrade(id, PAYMENT_AT):
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>
2026-08-19 12:15:00 +02:00
return _fail("the ship lab refused upgrade '%s' (cost %d) with the whole price in the hold" % [id, cost])
2026-09-02 13:43:56 +02:00
if not game_state.buy_upgrade(id, PAYMENT_AT):
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>
2026-08-19 12:15:00 +02:00
return _fail("buying '%s' failed despite can_buy_upgrade saying yes" % id)
if game_state.fleet_inventory.amount(GameState.RESOURCE_ORE) != 0:
return _fail("the upgrade did not charge the hold: %d left of %d" \
% [game_state.fleet_inventory.amount(GameState.RESOURCE_ORE), cost])
print("ship lab: bought '%s' for %d paid entirely from the hold" % [id, cost])
return true
func _check_vessel_research() -> bool:
# Vessel unlocks are the multi-resource payment route.
var vessel: String = ""
var costs: Dictionary = {}
for definition in game_state.VESSEL_DEFINITIONS.all():
if String(definition.unlock_id) == "" or game_state.vessel_is_unlocked(String(definition.definition_id)):
continue
vessel = String(definition.definition_id)
costs = game_state.research_costs(String(definition.unlock_id))
if not costs.is_empty():
break
if vessel == "" or costs.is_empty():
return _fail("no locked vessel with a resource cost was available to research")
game_state.research_points = 999
for resource_type in costs:
game_state.bank_inventory.set_amount(String(resource_type), 0)
game_state.fleet_inventory.set_amount(String(resource_type), int(costs[resource_type]))
2026-09-02 13:43:56 +02:00
if not game_state.unlock_vessel(vessel, PAYMENT_AT):
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>
2026-08-19 12:15:00 +02:00
return _fail("researching '%s' was refused with its whole price (%s) in the fleet hold" % [vessel, str(costs)])
for resource_type in costs:
if game_state.available_resource(String(resource_type)) != 0:
return _fail("researching '%s' left %d %s unspent" \
% [vessel, game_state.available_resource(String(resource_type)), resource_type])
print("vessel research: unlocked '%s' for %s paid entirely from the hold" % [vessel, str(costs)])
return true
func _check_ammo_is_not_pooled() -> bool:
# Firing is about where the matter physically IS, so it reads the hold
# directly rather than going through the pooled payment path.
game_state.bank_inventory.set_amount(GameState.RESOURCE_GAS, 500)
game_state.fleet_inventory.set_amount(GameState.RESOURCE_GAS, 0)
if game_state.fleet_resource(GameState.RESOURCE_GAS) != 0:
return _fail("banked gas leaked into the fleet hold reading that ammo checks use")
print("ammo still reads the fleet hold alone, ignoring 500 banked gas")
return true
func _process(delta: float) -> bool:
# quit() takes effect on the NEXT frame, so a failure raised during
# _initialize still gets one _process call with nothing set up. Without
# this the test spent five minutes reporting a null scene instead of the
# reason it actually stopped.
if scene == null:
return true
elapsed += delta
match stage:
0:
if elapsed < 0.3:
return false
scene.get_node("AsteroidField").set_process(false)
scene.get_node("EnemySpawner").set_process(false)
build_controller = scene.get_node("BuildController")
game_state.bank_inventory.set_amount(GameState.RESOURCE_ORE, 0)
game_state.fleet_inventory.total_capacity = HOLD_ORE
game_state.fleet_inventory.set_amount(GameState.RESOURCE_ORE, HOLD_ORE)
var before: int = game_state.buildings.size()
#224: refuse to build a structure on top of another BuildingField.place() dropped a structure at the fleet centroid with no overlap check of any kind, so they could be stacked. It reads worse than it looks. BuildingField decides which structure the fleet is standing on by testing the centroid against interact_radius and returning the FIRST match, so two structures inside one another make that a question of list order rather than of where the player flew - you could fly to your workshop and open the crafter. The separation is interact_radius itself (150) rather than a new number, because that is exactly the circle the ambiguity comes from. Buildings draw to a radius of roughly 60, so this leaves a visible gap rather than letting them touch. Gated in three places: the info popup (the button greys out and says which structure is in the way), _confirm_build, and _try_place - the preview goes stale as the fleet keeps moving. Ordering matters in one case. A unique structure replaces itself by demolishing the old one BEFORE placing the new one, so a placement refused afterwards would leave the player with neither. The check runs before the demolition and excludes the structure being replaced. Case D pins it. Two existing tests encoded the old permissive behaviour and now fly clear first; neither is about placement: test_build_info_popup builds every menu option from one position, so each structure blocked the next test_payments_use_fleet_hold builds a mothership on the ship lab that an earlier case injects at the origin Also fixes a bug from 37e2e89: Sfx.warm() called add_child() from _ready(), which Godot refuses outright - it printed an error and never pre-built the pool, defeating the point of warming it. Deferred, like Music.ensure(). 173/173. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
2026-09-09 12:57:28 +02:00
# #224: structures may not be stacked, and _check_ship_lab left a
# lab standing at the origin as a prerequisite for an earlier
# case. Fly clear of it - the subject here is which purse the ore
# comes out of, not where a mothership may stand.
scene.get_node("Fleet").teleport_to(
Vector2(GameState.MIN_BUILDING_SEPARATION * 4.0, 0.0))
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>
2026-08-19 12:15:00 +02:00
build_controller.pending_build_type = GameState.BUILDING_MOTHERSHIP
build_controller._confirm_build()
if game_state.buildings.size() != before + 1:
return _fail("building a mothership from the fleet's own hold did not place one (%d -> %d): %s" \
% [before, game_state.buildings.size(), build_controller.build_menu.status.text])
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>
2026-08-19 12:15:00 +02:00
# Checked as a TOTAL, not as the hold alone: a mothership standing
# where the fleet is starts taking delivery of the hold on the
# very next tick, so ore legitimately moves from hold to bank
# straight after the build. What must be true is that exactly the
# cost left the pool - and that it left, since the bank was empty
# and could not have paid.
var total: int = game_state.available_ore()
#138: structures and doctrines cost more than rocks, and more the deeper they are Both halves of the ticket, in the shape vessels have used since gas bombers and laser ships: a definition carries an ore figure and, when it wants more than ore, a dictionary that IS the price. `building_costs()` and `skill_costs()` answer it the way `product_costs()` already did, so there is one question with one answer whatever is being bought, and every payment path now goes through `spend_resources` rather than `spend`. Seven of ten structures and thirteen of seventeen doctrines now want gas, energy or dark matter alongside their rocks, and the totals climb with tree depth - structures 16/31/58/62/96 by depth, doctrines 15/30/73/96/143/258/530. The constraint that shaped the table is REACHABILITY, and it is the reason some prices stayed ore-only. Energy exists only where a gas burner stands, and a gas burner needs Power Systems - so nothing at or before that depth may be priced in energy, or it is a wall with no way through. Dark matter is rarer still and appears once, on the deepest node in the game. The extractor stays rocks-only despite its depth allowing gas, because the quest chain reaches it before it has sent the player for any. test_progressive_costs checks those as properties rather than transcribing the numbers, so rebalancing a price costs nothing while breaking the shape fails: nothing unreachable at its own depth, prices rising with depth, the ore field agreeing with the dictionary (repairs quote the field alone, since a repair drone carries ore and nothing else), and the quest chain never asking for a structure it has not funded. Two things cleaned up on the way: - The per-building cost constants in GameState are gone. Nothing but tests used them, and they were a second copy of a number the definitions already hold - exactly what would drift once a price gained a resource. - research_for_building() returns a display NAME, and the id-keyed half of the tree wants an id. That mix-up is silent - skill_depth() of a name finds no node and answers 0, so a deep structure reads as an ungated one, which is how the first draft of the test let an unreachable price through. research_node_for_building() is the id, and the name is expressed from it. PriceTag no longer rebuilds its labels when re-told the same price; it refreshes the held figures instead. The craft menu re-states prices every frame and the build menu now does too, so that was a dozen Labels being torn down and rebuilt sixty times a second.
2026-08-28 15:30:30 +02:00
if total != HOLD_ORE - int(game_state.building_costs(GameState.BUILDING_MOTHERSHIP)[GameState.RESOURCE_ORE]):
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>
2026-08-19 12:15:00 +02:00
return _fail("the mothership was placed but %d ore remains, expected %d" \
#138: structures and doctrines cost more than rocks, and more the deeper they are Both halves of the ticket, in the shape vessels have used since gas bombers and laser ships: a definition carries an ore figure and, when it wants more than ore, a dictionary that IS the price. `building_costs()` and `skill_costs()` answer it the way `product_costs()` already did, so there is one question with one answer whatever is being bought, and every payment path now goes through `spend_resources` rather than `spend`. Seven of ten structures and thirteen of seventeen doctrines now want gas, energy or dark matter alongside their rocks, and the totals climb with tree depth - structures 16/31/58/62/96 by depth, doctrines 15/30/73/96/143/258/530. The constraint that shaped the table is REACHABILITY, and it is the reason some prices stayed ore-only. Energy exists only where a gas burner stands, and a gas burner needs Power Systems - so nothing at or before that depth may be priced in energy, or it is a wall with no way through. Dark matter is rarer still and appears once, on the deepest node in the game. The extractor stays rocks-only despite its depth allowing gas, because the quest chain reaches it before it has sent the player for any. test_progressive_costs checks those as properties rather than transcribing the numbers, so rebalancing a price costs nothing while breaking the shape fails: nothing unreachable at its own depth, prices rising with depth, the ore field agreeing with the dictionary (repairs quote the field alone, since a repair drone carries ore and nothing else), and the quest chain never asking for a structure it has not funded. Two things cleaned up on the way: - The per-building cost constants in GameState are gone. Nothing but tests used them, and they were a second copy of a number the definitions already hold - exactly what would drift once a price gained a resource. - research_for_building() returns a display NAME, and the id-keyed half of the tree wants an id. That mix-up is silent - skill_depth() of a name finds no node and answers 0, so a deep structure reads as an ungated one, which is how the first draft of the test let an unreachable price through. research_node_for_building() is the id, and the name is expressed from it. PriceTag no longer rebuilds its labels when re-told the same price; it refreshes the held figures instead. The craft menu re-states prices every frame and the build menu now does too, so that was a dozen Labels being torn down and rebuilt sixty times a second.
2026-08-28 15:30:30 +02:00
% [total, HOLD_ORE - int(game_state.building_costs(GameState.BUILDING_MOTHERSHIP)[GameState.RESOURCE_ORE])])
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>
2026-08-19 12:15:00 +02:00
print("built a mothership with an empty bank, paid out of the fleet hold")
print("TEST_PAYMENTS_USE_FLEET_HOLD: PASS")
quit(0)
return true
return false
func _fail(message: String) -> bool:
print("TEST_PAYMENTS_USE_FLEET_HOLD: FAIL (%s)" % message)
quit(1)
return false