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:
parent
f5a16a7b95
commit
821da05c9f
5 changed files with 341 additions and 215 deletions
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
92
GameState.gd
92
GameState.gd
|
|
@ -738,10 +738,8 @@ func unlock_vessel(vessel: String) -> bool:
|
|||
return false
|
||||
var costs: Dictionary = research_costs(definition.unlock_id)
|
||||
var points_needed: int = int(definition.research_point_cost)
|
||||
if not can_afford_resources(costs) or research_points < points_needed:
|
||||
if research_points < points_needed or not spend_resources(costs):
|
||||
return false
|
||||
for resource_type in costs:
|
||||
bank_inventory.withdraw(String(resource_type), int(costs[resource_type]))
|
||||
if points_needed > 0:
|
||||
research_points -= points_needed
|
||||
research_points_changed.emit(research_points)
|
||||
|
|
@ -1800,55 +1798,63 @@ func _arrive(t: Dictionary) -> void:
|
|||
t.progress = 1.0
|
||||
|
||||
|
||||
# --- paying for things ------------------------------------------------------
|
||||
#
|
||||
# EVERY payment draws on the bank AND on what the ships are carrying. The
|
||||
# player can already move resources between the two by hand at any building
|
||||
# that takes them, so refusing a purchase that a couple of taps would have
|
||||
# made affordable was pure ceremony.
|
||||
#
|
||||
# In one case it was worse than ceremony. A fresh 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. Mine forever,
|
||||
# build nothing.
|
||||
#
|
||||
# Bank first, hold second. The hold is also the fleet's mining capacity, so
|
||||
# leaving it as full as possible means the next trip out can still carry
|
||||
# something back.
|
||||
#
|
||||
# The one deliberate exception is AMMO - see AimedFleetAbilityController,
|
||||
# which checks fleet_resource() directly. A gas bomb is fired from what the
|
||||
# ships are physically carrying; that is a fact about where the matter is,
|
||||
# not a convenience about where the money is.
|
||||
|
||||
|
||||
func available_resource(resource_type: String) -> int:
|
||||
return bank_inventory.amount(resource_type) + fleet_inventory.amount(resource_type)
|
||||
|
||||
|
||||
func available_ore() -> int:
|
||||
return available_resource(RESOURCE_ORE)
|
||||
|
||||
|
||||
func can_afford(cost: int) -> bool:
|
||||
return bank_inventory.amount(RESOURCE_ORE) >= cost
|
||||
return available_ore() >= cost
|
||||
|
||||
|
||||
func can_afford_resources(costs: Dictionary) -> bool:
|
||||
for resource_type in costs:
|
||||
if bank_inventory.amount(String(resource_type)) < int(costs[resource_type]):
|
||||
if available_resource(String(resource_type)) < int(costs[resource_type]):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func spend(cost: int) -> bool:
|
||||
if not can_afford(cost):
|
||||
return spend_resources({RESOURCE_ORE: cost})
|
||||
|
||||
|
||||
func spend_resources(costs: Dictionary) -> bool:
|
||||
# Affordability is settled in full BEFORE anything is taken. Charging for
|
||||
# part of a purchase that then fails is worse than refusing it, and with
|
||||
# several resource types in one price that is a real possibility rather
|
||||
# than a theoretical one.
|
||||
if not can_afford_resources(costs):
|
||||
return false
|
||||
bank_inventory.withdraw(RESOURCE_ORE, cost)
|
||||
return true
|
||||
|
||||
|
||||
# --- construction: the bank AND what the ships are carrying -----------------
|
||||
#
|
||||
# A build is put up by the fleet, on the spot, so it can use ore the fleet is
|
||||
# holding as well as ore already banked. That is not a convenience: a fresh
|
||||
# game starts with an empty bank, and the ONLY way to bank ore is to deliver
|
||||
# it to a mothership. Charging construction to the bank alone therefore made
|
||||
# the first mothership cost ore that could not exist yet - the fleet could
|
||||
# mine all it liked and the game was unwinnable.
|
||||
#
|
||||
# Research still spends from the bank alone (can_afford/spend above). That
|
||||
# stays deliberate: hauling ore back to a base to fund science is the loop,
|
||||
# and unlike construction it was never deadlocked, because by the time there
|
||||
# is anything to research there is a mothership to bank into.
|
||||
|
||||
|
||||
func construction_ore() -> int:
|
||||
return bank_inventory.amount(RESOURCE_ORE) + fleet_inventory.amount(RESOURCE_ORE)
|
||||
|
||||
|
||||
func can_afford_construction(cost: int) -> bool:
|
||||
return construction_ore() >= cost
|
||||
|
||||
|
||||
func spend_construction(cost: int) -> bool:
|
||||
# Bank first, hold second. The hold is also the fleet's mining capacity,
|
||||
# so leaving it as full as possible means the next trip out can still
|
||||
# carry something back.
|
||||
if not can_afford_construction(cost):
|
||||
return false
|
||||
var from_bank: int = bank_inventory.withdraw(RESOURCE_ORE, cost)
|
||||
var remaining: int = cost - from_bank
|
||||
if remaining > 0:
|
||||
fleet_inventory.withdraw(RESOURCE_ORE, remaining)
|
||||
for resource_type in costs:
|
||||
var id: String = String(resource_type)
|
||||
var owed: int = int(costs[resource_type])
|
||||
owed -= bank_inventory.withdraw(id, owed)
|
||||
if owed > 0:
|
||||
fleet_inventory.withdraw(id, owed)
|
||||
return true
|
||||
|
|
|
|||
|
|
@ -1,163 +0,0 @@
|
|||
extends SceneTree
|
||||
|
||||
# Headless gameplay test: run with
|
||||
# godot --headless -s res://tests/test_build_uses_fleet_hold.gd
|
||||
# A new game starts with an empty bank. Ore is mined into the FLEET HOLD, and
|
||||
# the only way to move it into the bank is to deliver it to a mothership - so
|
||||
# charging construction to the bank alone made the first mothership cost ore
|
||||
# that could not exist yet. Mine forever, build nothing: the game was
|
||||
# unwinnable from its own opening position.
|
||||
#
|
||||
# Construction now draws on the bank AND the hold. Covers:
|
||||
# A. the deadlock premise still holds - a bank-only test rejects a build
|
||||
# the fleet can plainly pay for, so this is guarding something real
|
||||
# B. the pooled test accepts it, and paying takes from the bank first and
|
||||
# the hold only for the shortfall
|
||||
# C. an unaffordable build takes nothing at all (no partial charge)
|
||||
# D. end to end: a fleet holding ore, with an empty bank, actually gets a
|
||||
# mothership built through BuildController
|
||||
# E. research is deliberately NOT pooled - it still spends from the bank
|
||||
|
||||
# Deliberately more than one starting interceptor can carry (its hold is 20,
|
||||
# which is exactly MOTHERSHIP_COST). The capacity is widened where this is
|
||||
# used, so "the hold paid for it" is visible as a remainder rather than as
|
||||
# the hold happening to empty to zero.
|
||||
const HOLD_ORE: int = 40
|
||||
|
||||
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()
|
||||
|
||||
var cost: int = GameState.MOTHERSHIP_COST
|
||||
|
||||
# --- A. the deadlock, exactly as a fresh game presents it ---------------
|
||||
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)
|
||||
if game_state.can_afford(cost):
|
||||
_fail("the bank-only test passed with an empty bank, so it can no longer show the deadlock this fixes")
|
||||
return
|
||||
print("bank 0 + hold %d: bank-only affordability says no (the original deadlock)" % HOLD_ORE)
|
||||
|
||||
# --- B. pooled, and paid bank-first -------------------------------------
|
||||
if not game_state.can_afford_construction(cost):
|
||||
_fail("a fleet holding %d ore still could not afford a %d build" % [HOLD_ORE, cost])
|
||||
return
|
||||
if not game_state.spend_construction(cost):
|
||||
_fail("spend_construction refused a build it had just called affordable")
|
||||
return
|
||||
if game_state.bank_inventory.amount(GameState.RESOURCE_ORE) != 0:
|
||||
_fail("paying from an empty bank somehow changed it")
|
||||
return
|
||||
if game_state.fleet_inventory.amount(GameState.RESOURCE_ORE) != HOLD_ORE - cost:
|
||||
_fail("the hold should have paid the whole %d, but holds %d of %d" \
|
||||
% [cost, game_state.fleet_inventory.amount(GameState.RESOURCE_ORE), HOLD_ORE])
|
||||
return
|
||||
print("paid %d entirely from the hold: hold %d -> %d" % [cost, HOLD_ORE, HOLD_ORE - cost])
|
||||
|
||||
# Bank first when there is a bank to draw on: the hold is also the fleet's
|
||||
# mining capacity, so it is spent last.
|
||||
game_state.bank_inventory.set_amount(GameState.RESOURCE_ORE, 8)
|
||||
game_state.fleet_inventory.set_amount(GameState.RESOURCE_ORE, 30)
|
||||
if not game_state.spend_construction(20):
|
||||
_fail("a 20 build 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)
|
||||
if game_state.spend_construction(50):
|
||||
_fail("a build that cost 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 build still took ore: 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 build nobody can afford takes nothing")
|
||||
|
||||
# --- E. research stays banked -------------------------------------------
|
||||
game_state.bank_inventory.set_amount(GameState.RESOURCE_ORE, 0)
|
||||
game_state.fleet_inventory.set_amount(GameState.RESOURCE_ORE, 500)
|
||||
if game_state.can_afford(1):
|
||||
_fail("research affordability started counting the fleet hold - it is meant to stay banked")
|
||||
return
|
||||
print("research still spends from the bank alone")
|
||||
|
||||
# --- D. 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)
|
||||
scene = load("res://Main.tscn").instantiate()
|
||||
root.add_child(scene)
|
||||
current_scene = scene
|
||||
|
||||
|
||||
func _process(delta: float) -> bool:
|
||||
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")
|
||||
|
||||
# Whatever else the scene did while starting up, the point of the
|
||||
# test is a fleet with ore and an empty bank.
|
||||
game_state.bank_inventory.set_amount(GameState.RESOURCE_ORE, 0)
|
||||
# Widened past what a starting interceptor can carry. Its real
|
||||
# hold is 20 and a mothership costs exactly 20, so at the stock
|
||||
# capacity a successful build empties the hold to zero - which is
|
||||
# indistinguishable from the hold having been emptied by anything
|
||||
# else. With room to spare, the remainder is the evidence.
|
||||
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()
|
||||
|
||||
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_status.text])
|
||||
# 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.construction_ore()
|
||||
if total != HOLD_ORE - GameState.MOTHERSHIP_COST:
|
||||
return _fail("the mothership was placed but %d ore remains, expected %d" \
|
||||
% [total, HOLD_ORE - GameState.MOTHERSHIP_COST])
|
||||
print("built a mothership with an empty bank, paid out of the fleet hold (%d ore left, bank %d + hold %d)" \
|
||||
% [total, game_state.bank_inventory.amount(GameState.RESOURCE_ORE),
|
||||
game_state.fleet_inventory.amount(GameState.RESOURCE_ORE)])
|
||||
|
||||
print("TEST_BUILD_USES_FLEET_HOLD: PASS")
|
||||
quit(0)
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _fail(message: String) -> bool:
|
||||
print("TEST_BUILD_USES_FLEET_HOLD: FAIL (%s)" % message)
|
||||
quit(1)
|
||||
return true
|
||||
278
tests/test_payments_use_fleet_hold.gd
Normal file
278
tests/test_payments_use_fleet_hold.gd
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
extends SceneTree
|
||||
|
||||
# Headless gameplay test: run with
|
||||
# godot --headless -s res://tests/test_payments_use_fleet_hold.gd
|
||||
# Every payment draws on the bank AND on what the ships are carrying. The
|
||||
# player can already shuttle resources between the two by hand, so refusing a
|
||||
# purchase that a couple of taps would have made affordable was ceremony.
|
||||
#
|
||||
# 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
|
||||
|
||||
const HOLD_ORE: int = 40
|
||||
|
||||
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()
|
||||
|
||||
var cost: int = GameState.MOTHERSHIP_COST
|
||||
|
||||
# --- 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
|
||||
if not game_state.can_afford(cost):
|
||||
_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 -------------------------------
|
||||
if not game_state.spend(cost):
|
||||
_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)
|
||||
if not game_state.spend(20):
|
||||
_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)
|
||||
if game_state.spend(50):
|
||||
_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")
|
||||
|
||||
# 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)
|
||||
scene = load("res://Main.tscn").instantiate()
|
||||
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)
|
||||
if game_state.spend_resources({GameState.RESOURCE_ORE: 15, GameState.RESOURCE_GAS: 40}):
|
||||
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)])
|
||||
if not game_state.spend_resources({GameState.RESOURCE_ORE: 15, GameState.RESOURCE_GAS: 2}):
|
||||
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)
|
||||
if not game_state.can_unlock(id):
|
||||
return _fail("the skill tree refused '%s' (cost %d) with the whole price in the fleet hold" % [id, cost])
|
||||
if not game_state.unlock(id):
|
||||
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)
|
||||
if not game_state.can_buy_upgrade(id):
|
||||
return _fail("the ship lab refused upgrade '%s' (cost %d) with the whole price in the hold" % [id, cost])
|
||||
if not game_state.buy_upgrade(id):
|
||||
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]))
|
||||
if not game_state.unlock_vessel(vessel):
|
||||
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()
|
||||
|
||||
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_status.text])
|
||||
# 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()
|
||||
if total != HOLD_ORE - GameState.MOTHERSHIP_COST:
|
||||
return _fail("the mothership was placed but %d ore remains, expected %d" \
|
||||
% [total, HOLD_ORE - GameState.MOTHERSHIP_COST])
|
||||
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
|
||||
Loading…
Reference in a new issue