#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.
This commit is contained in:
parent
ea25318201
commit
1ece202731
22 changed files with 435 additions and 46 deletions
|
|
@ -39,6 +39,10 @@ const OPTIONS: Array = [
|
|||
func _initialize() -> void:
|
||||
game_state = root.get_node("GameState")
|
||||
game_state.rocks = 500
|
||||
# #138: some structures want gas as well as rocks, and the popup
|
||||
# only offers a build button for something the player can pay for.
|
||||
game_state.bank_inventory.set_amount(GameState.RESOURCE_GAS, 500)
|
||||
game_state.bank_inventory.set_amount(GameState.RESOURCE_ENERGY, 500)
|
||||
game_state.buildings.clear()
|
||||
game_state.extractors.clear()
|
||||
# Structures are gated behind the mothership's research tree. That gate
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ func _process(delta: float) -> bool:
|
|||
controller._confirm_build()
|
||||
|
||||
var built: bool = game_state.has_extractor_at(planet2.planet_id)
|
||||
var charged: bool = game_state.rocks == rocks_before - GameState.EXTRACTOR_COST
|
||||
var charged: bool = game_state.rocks == rocks_before - int(game_state.building_costs(GameState.BUILDING_EXTRACTOR)[GameState.RESOURCE_ORE])
|
||||
print("over planet '%s': enabled=%s built=%s rocks %d->%d charged=%s" % [
|
||||
planet2.planet_name, button.enabled, built, rocks_before, game_state.rocks, charged
|
||||
])
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ func _initialize() -> void:
|
|||
state.transporters.clear()
|
||||
state.reset_progression()
|
||||
state.rocks = 900
|
||||
# #138: doctrines past the first tier want gas as well as rocks.
|
||||
state.bank_inventory.set_amount(GameState.RESOURCE_GAS, 900)
|
||||
state.research_points = state.skill_research_point_cost("freight_yards")
|
||||
state.unlocked = ["shipyards", "logistics", "freight"]
|
||||
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ func _process(delta: float) -> bool:
|
|||
controller._confirm_build()
|
||||
var only_one: bool = _count_motherships() == 1
|
||||
var moved: bool = game_state.get_mothership_position().distance_to(second_pos) < 60.0
|
||||
var charged: bool = game_state.rocks == rocks_before - GameState.MOTHERSHIP_COST
|
||||
var charged: bool = game_state.rocks == rocks_before - int(game_state.building_costs(GameState.BUILDING_MOTHERSHIP)[GameState.RESOURCE_ORE])
|
||||
print("confirmed: motherships=%d moved=%s rocks %d->%d charged=%s" % [
|
||||
_count_motherships(), moved, rocks_before, game_state.rocks, charged
|
||||
])
|
||||
|
|
@ -121,7 +121,7 @@ func _process(delta: float) -> bool:
|
|||
return true
|
||||
|
||||
# --- unaffordable replacement must not destroy the old one ---
|
||||
game_state.rocks = GameState.MOTHERSHIP_COST - 1
|
||||
game_state.rocks = int(game_state.building_costs(GameState.BUILDING_MOTHERSHIP)[GameState.RESOURCE_ORE]) - 1
|
||||
_place_fleet(Vector2(2500.0, -800.0))
|
||||
controller._on_build_pressed()
|
||||
controller._show_building_info(GameState.BUILDING_MOTHERSHIP)
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ func _initialize() -> void:
|
|||
game_state.buildings.clear()
|
||||
game_state.extractors.clear()
|
||||
|
||||
var cost: int = GameState.MOTHERSHIP_COST
|
||||
var cost: int = int(game_state.building_costs(GameState.BUILDING_MOTHERSHIP)[GameState.RESOURCE_ORE])
|
||||
|
||||
# --- A. the deadlock, exactly as a fresh game presents it ---------------
|
||||
game_state.bank_inventory.set_amount(GameState.RESOURCE_ORE, 0)
|
||||
|
|
@ -261,9 +261,9 @@ func _process(delta: float) -> bool:
|
|||
# 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:
|
||||
if total != HOLD_ORE - int(game_state.building_costs(GameState.BUILDING_MOTHERSHIP)[GameState.RESOURCE_ORE]):
|
||||
return _fail("the mothership was placed but %d ore remains, expected %d" \
|
||||
% [total, HOLD_ORE - GameState.MOTHERSHIP_COST])
|
||||
% [total, HOLD_ORE - int(game_state.building_costs(GameState.BUILDING_MOTHERSHIP)[GameState.RESOURCE_ORE])])
|
||||
print("built a mothership with an empty bank, paid out of the fleet hold")
|
||||
|
||||
print("TEST_PAYMENTS_USE_FLEET_HOLD: PASS")
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ func _process(delta: float) -> bool:
|
|||
return _fail("conquered planet did not offer a build")
|
||||
controller._confirm_build()
|
||||
var built: bool = game_state.has_extractor_at(planet.planet_id)
|
||||
var charged: bool = game_state.rocks == rocks_before - GameState.EXTRACTOR_COST
|
||||
var charged: bool = game_state.rocks == rocks_before - int(game_state.building_costs(GameState.BUILDING_EXTRACTOR)[GameState.RESOURCE_ORE])
|
||||
if not built or not charged:
|
||||
return _fail("conquered planet did not build/charge correctly")
|
||||
|
||||
|
|
|
|||
247
tests/test_progressive_costs.gd
Normal file
247
tests/test_progressive_costs.gd
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
extends SceneTree
|
||||
|
||||
# Headless test: run with
|
||||
# godot --headless -s res://tests/test_progressive_costs.gd
|
||||
# Forgejo #138: "all buildings only cost rocks, you can introduce other
|
||||
# resources for some buildings, make the cost meaningful depending on how far
|
||||
# they are in the skill tree. same for skills."
|
||||
#
|
||||
# "Meaningful depending on how far they are" is a property of the whole
|
||||
# table, not of any one entry, so that is what is checked here - and the
|
||||
# checks are properties rather than transcribed numbers, so rebalancing a
|
||||
# price does not mean editing this file, while breaking the shape does.
|
||||
#
|
||||
# The dangerous half is REACHABILITY. Energy exists only where a gas burner
|
||||
# has been built, and a gas burner needs Power Systems - so anything at or
|
||||
# before that depth which asked for energy would be a wall the player has no
|
||||
# way through. Dark matter is rarer still: black hole worlds. The rule below
|
||||
# is the one that keeps a price from being a dead end, and it is the reason
|
||||
# the extractor stays ore-only even though its depth would allow gas.
|
||||
#
|
||||
# Covers:
|
||||
# A. more than one resource is actually in play - the ticket's whole point
|
||||
# B. no price asks for something unreachable at its own depth
|
||||
# C. prices grow with depth, for buildings and for doctrines alike
|
||||
# D. the ore figure and the price dictionary agree, because repairs are
|
||||
# quoted from the ore figure alone
|
||||
# E. nothing is free, and nothing charges a resource that does not exist
|
||||
# F. the quest chain stays payable: no quest asks for a structure whose
|
||||
# price needs a resource the chain has not reached yet
|
||||
|
||||
# Quests.gd is plain data with no autoload in it, so preloading it here is
|
||||
# safe where preloading PriceTag or ResourceBar would not be.
|
||||
const QUESTS = preload("res://core/Quests.gd")
|
||||
|
||||
var game_state: Node
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
# GameState is an autoload: a bare GameState.<instance member> does not
|
||||
# compile in a SceneTree script, so the node is fetched by name instead.
|
||||
game_state = root.get_node("GameState")
|
||||
game_state.reset_new_game()
|
||||
game_state.intro_active = false
|
||||
game_state.reset_quests()
|
||||
|
||||
|
||||
func _fail(message: String) -> bool:
|
||||
print("TEST_PROGRESSIVE_COSTS: FAIL (%s)" % message)
|
||||
quit(1)
|
||||
return true
|
||||
|
||||
|
||||
# The depth at which each resource first becomes gettable.
|
||||
#
|
||||
# ore from the asteroids the game opens on
|
||||
# gas from a gas giant, which needs no research at all - only flying
|
||||
# energy from a gas burner, unlocked by Power Systems, so it exists only
|
||||
# BELOW that node rather than at it
|
||||
# dark matter from a black hole world: no research either, but the rarest
|
||||
# thing in the sky, so it is reserved for the deepest tier
|
||||
func _first_available_depth(resource_type: String) -> int:
|
||||
match resource_type:
|
||||
GameState.RESOURCE_ORE, GameState.RESOURCE_GAS:
|
||||
return 0
|
||||
GameState.RESOURCE_ENERGY:
|
||||
return game_state.skill_depth("power_systems") + 1
|
||||
GameState.RESOURCE_DARK_MATTER:
|
||||
return 4
|
||||
return 99
|
||||
|
||||
|
||||
# How deep in the tree a structure sits: the depth of the node that unlocks
|
||||
# it, or 0 for one that is never gated.
|
||||
func _building_depth(type: String) -> int:
|
||||
# The ID, not the display name: skill_depth() of a name silently answers
|
||||
# 0, which would let an unreachable price through as "ungated".
|
||||
var gate: String = String(game_state.research_node_for_building(type))
|
||||
return 0 if gate == "" else game_state.skill_depth(gate)
|
||||
|
||||
|
||||
func _total(costs: Dictionary) -> int:
|
||||
var sum: int = 0
|
||||
for resource_type in costs:
|
||||
sum += int(costs[resource_type])
|
||||
return sum
|
||||
|
||||
|
||||
func _check_reachable(what: String, depth: int, costs: Dictionary) -> String:
|
||||
for resource_type in costs:
|
||||
var id: String = String(resource_type)
|
||||
if not (id in game_state.resource_types()):
|
||||
return "%s is priced in \"%s\", which is not a resource" % [what, id]
|
||||
var need_depth: int = _first_available_depth(id)
|
||||
if depth < need_depth:
|
||||
return "%s sits at depth %d and asks for %s, which nothing produces before depth %d" \
|
||||
% [what, depth, game_state.resource_name(id), need_depth]
|
||||
return ""
|
||||
|
||||
|
||||
# Average price per depth, and it must go up. Averaged rather than compared
|
||||
# entry-by-entry: a tier holds several things of deliberately different
|
||||
# weight - a doctrine that adds one vessel beside one that unlocks a whole
|
||||
# branch - and demanding every one of them out-price every shallower one
|
||||
# would be demanding a flat tree.
|
||||
func _rising(label: String, totals: Dictionary) -> String:
|
||||
var depths: Array = totals.keys()
|
||||
depths.sort()
|
||||
var previous: float = -1.0
|
||||
var trace: PackedStringArray = PackedStringArray()
|
||||
for depth in depths:
|
||||
var here: Array = totals[depth]
|
||||
var mean: float = 0.0
|
||||
for value in here:
|
||||
mean += float(value)
|
||||
mean /= float(here.size())
|
||||
trace.append("d%d=%.0f" % [depth, mean])
|
||||
if mean <= previous:
|
||||
return "%s prices do not rise with depth: %s" % [label, " ".join(trace)]
|
||||
previous = mean
|
||||
print(" %s: %s" % [label, " ".join(trace)])
|
||||
return ""
|
||||
|
||||
|
||||
func _process(_delta: float) -> bool:
|
||||
# --- buildings -------------------------------------------------------
|
||||
var building_totals: Dictionary = {}
|
||||
var multi_resource: int = 0
|
||||
var kinds: Dictionary = {}
|
||||
for definition in BuildingDefinitions.all():
|
||||
var type: String = String(definition.definition_id)
|
||||
var costs: Dictionary = game_state.building_costs(type)
|
||||
var depth: int = _building_depth(type)
|
||||
|
||||
# --- E. nothing free ------------------------------------------
|
||||
if costs.is_empty() or _total(costs) <= 0:
|
||||
return _fail("%s costs nothing" % type)
|
||||
# --- D. the ore figure and the dictionary agree ----------------
|
||||
if int(costs.get(GameState.RESOURCE_ORE, 0)) != int(definition.cost):
|
||||
return _fail("%s is priced at %d ore but its cost field says %d - repairs quote the field" \
|
||||
% [type, int(costs.get(GameState.RESOURCE_ORE, 0)), int(definition.cost)])
|
||||
# --- B. reachable ---------------------------------------------
|
||||
var problem: String = _check_reachable(type, depth, costs)
|
||||
if problem != "":
|
||||
return _fail(problem)
|
||||
|
||||
if costs.size() > 1:
|
||||
multi_resource += 1
|
||||
for resource_type in costs:
|
||||
kinds[String(resource_type)] = true
|
||||
if not building_totals.has(depth):
|
||||
building_totals[depth] = []
|
||||
building_totals[depth].append(_total(costs))
|
||||
|
||||
# --- A. the ticket's point ------------------------------------------
|
||||
if multi_resource < 3:
|
||||
return _fail("only %d structures cost anything but rocks" % multi_resource)
|
||||
if kinds.size() < 3:
|
||||
return _fail("structures between them use only %d kinds of resource" % kinds.size())
|
||||
print("%d of %d structures cost more than rocks, across %d resources" \
|
||||
% [multi_resource, BuildingDefinitions.all().size(), kinds.size()])
|
||||
|
||||
# --- C. rising with depth -------------------------------------------
|
||||
var rising: String = _rising("structure", building_totals)
|
||||
if rising != "":
|
||||
return _fail(rising)
|
||||
|
||||
# --- doctrines -------------------------------------------------------
|
||||
var skill_totals: Dictionary = {}
|
||||
var multi_skills: int = 0
|
||||
for node in GameState.SKILL_TREE:
|
||||
var id: String = String(node.id)
|
||||
var costs: Dictionary = game_state.skill_costs(id)
|
||||
var depth: int = game_state.skill_depth(id)
|
||||
if costs.is_empty() or _total(costs) <= 0:
|
||||
return _fail("doctrine %s costs nothing" % id)
|
||||
if int(costs.get(GameState.RESOURCE_ORE, 0)) != int(node.cost):
|
||||
return _fail("doctrine %s is priced at %d ore but its cost field says %d" \
|
||||
% [id, int(costs.get(GameState.RESOURCE_ORE, 0)), int(node.cost)])
|
||||
var problem2: String = _check_reachable("doctrine %s" % id, depth, costs)
|
||||
if problem2 != "":
|
||||
return _fail(problem2)
|
||||
if costs.size() > 1:
|
||||
multi_skills += 1
|
||||
if not skill_totals.has(depth):
|
||||
skill_totals[depth] = []
|
||||
skill_totals[depth].append(_total(costs))
|
||||
if multi_skills < 5:
|
||||
return _fail("only %d doctrines cost anything but rocks" % multi_skills)
|
||||
print("%d of %d doctrines cost more than rocks" % [multi_skills, GameState.SKILL_TREE.size()])
|
||||
var rising2: String = _rising("doctrine", skill_totals)
|
||||
if rising2 != "":
|
||||
return _fail(rising2)
|
||||
|
||||
# --- the money really changes hands ----------------------------------
|
||||
game_state.bank_inventory.set_amount(GameState.RESOURCE_ORE, 500)
|
||||
game_state.bank_inventory.set_amount(GameState.RESOURCE_GAS, 500)
|
||||
game_state.unlocked = ["shipyards", "logistics", "coordination"]
|
||||
game_state.research_points = 9
|
||||
var gas_before: int = game_state.available_resource(GameState.RESOURCE_GAS)
|
||||
if not game_state.unlock("freight"):
|
||||
return _fail("could not research a doctrine with everything in the bank")
|
||||
var gas_spent: int = gas_before - game_state.available_resource(GameState.RESOURCE_GAS)
|
||||
if gas_spent != int(game_state.skill_costs("freight")[GameState.RESOURCE_GAS]):
|
||||
return _fail("researching Freight Handling took %d gas, its price is %d" \
|
||||
% [gas_spent, int(game_state.skill_costs("freight")[GameState.RESOURCE_GAS])])
|
||||
print("researching Freight Handling really took %d gas as well as its ore" % gas_spent)
|
||||
|
||||
# ...and a doctrine the player cannot pay the gas for is refused, even
|
||||
# with rocks to spare. This is the case that would have passed silently
|
||||
# before, when only the ore was ever checked.
|
||||
game_state.bank_inventory.set_amount(GameState.RESOURCE_GAS, 0)
|
||||
game_state.fleet_inventory.set_amount(GameState.RESOURCE_GAS, 0)
|
||||
game_state.bank_inventory.set_amount(GameState.RESOURCE_ORE, 5000)
|
||||
if game_state.can_unlock("power_systems"):
|
||||
return _fail("Power Systems is buyable with no gas at all")
|
||||
var ore_before: int = game_state.available_resource(GameState.RESOURCE_ORE)
|
||||
if game_state.unlock("power_systems"):
|
||||
return _fail("Power Systems was bought with no gas at all")
|
||||
if game_state.available_resource(GameState.RESOURCE_ORE) != ore_before:
|
||||
return _fail("a refused purchase still took %d ore" \
|
||||
% (ore_before - game_state.available_resource(GameState.RESOURCE_ORE)))
|
||||
print("a doctrine short of gas is refused, and nothing is taken for it")
|
||||
|
||||
# --- F. the quest chain stays payable --------------------------------
|
||||
# Quests.gd walks the player through the game in its own order, which is
|
||||
# not the tree's. A quest asking for a structure priced in something the
|
||||
# chain has not sent them for yet is the same dead end as an unreachable
|
||||
# depth, in a different coordinate system.
|
||||
var reached: Dictionary = {GameState.RESOURCE_ORE: true}
|
||||
for quest in QUESTS.CHAIN:
|
||||
var goal: Dictionary = quest.goal
|
||||
if String(goal.kind) == QUESTS.GOAL_BUILDING:
|
||||
for resource_type in game_state.building_costs(String(goal.building)):
|
||||
if not reached.has(String(resource_type)):
|
||||
return _fail("quest \"%s\" asks for a %s, priced in %s, before the chain has sent anyone for any" \
|
||||
% [quest.id, goal.building, game_state.resource_name(String(resource_type))])
|
||||
if String(goal.kind) == QUESTS.GOAL_RESOURCE:
|
||||
reached[String(goal.resource)] = true
|
||||
# Building the gas burner is what puts energy in reach.
|
||||
if String(goal.kind) == QUESTS.GOAL_BUILDING \
|
||||
and String(goal.building) == GameState.BUILDING_GAS_BURNER:
|
||||
reached[GameState.RESOURCE_ENERGY] = true
|
||||
print("the quest chain never asks for a structure it has not funded")
|
||||
|
||||
print("TEST_PROGRESSIVE_COSTS: PASS")
|
||||
quit(0)
|
||||
return true
|
||||
1
tests/test_progressive_costs.gd.uid
Normal file
1
tests/test_progressive_costs.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bq6d3l4hxfmkb
|
||||
|
|
@ -84,11 +84,11 @@ func _process(delta: float) -> bool:
|
|||
ship.position = mothership_pos
|
||||
fleet.move_to(mothership_pos)
|
||||
field.place(GameState.BUILDING_MOTHERSHIP, mothership_pos)
|
||||
game_state.spend(GameState.MOTHERSHIP_COST)
|
||||
game_state.spend(int(game_state.building_costs(GameState.BUILDING_MOTHERSHIP)[GameState.RESOURCE_ORE]))
|
||||
field.place(GameState.BUILDING_SHIP_CRAFTER, Vector2(1400.0, 300.0))
|
||||
game_state.spend(GameState.SHIP_CRAFTER_COST)
|
||||
game_state.spend(int(game_state.building_costs(GameState.BUILDING_SHIP_CRAFTER)[GameState.RESOURCE_ORE]))
|
||||
|
||||
var paid: bool = game_state.rocks == before - GameState.MOTHERSHIP_COST - GameState.SHIP_CRAFTER_COST
|
||||
var paid: bool = game_state.rocks == before - int(game_state.building_costs(GameState.BUILDING_MOTHERSHIP)[GameState.RESOURCE_ORE]) - int(game_state.building_costs(GameState.BUILDING_SHIP_CRAFTER)[GameState.RESOURCE_ORE])
|
||||
var stored: bool = game_state.buildings.size() == 2
|
||||
print("buildings=%d rocks %d->%d paid=%s" % [
|
||||
game_state.buildings.size(), before, game_state.rocks, paid
|
||||
|
|
|
|||
|
|
@ -188,6 +188,9 @@ func _process(_delta: float) -> bool:
|
|||
return _fail("no skill node raises the drone count")
|
||||
var before_count: int = _drones_of(workshop_id).size()
|
||||
game_state.rocks = 5000
|
||||
# #138: the deep doctrines want gas and energy too.
|
||||
game_state.bank_inventory.set_amount(GameState.RESOURCE_GAS, 5000)
|
||||
game_state.bank_inventory.set_amount(GameState.RESOURCE_ENERGY, 5000)
|
||||
game_state.research_points = 20
|
||||
if not game_state.unlock(bonus_id):
|
||||
return _fail("could not research \"%s\"" % bonus_id)
|
||||
|
|
|
|||
|
|
@ -84,16 +84,17 @@ func _process(delta: float) -> bool:
|
|||
print("TEST_TRANSPORT_HUB_BUILDING: FAIL (still locked after researching '%s')" % gate)
|
||||
quit(1)
|
||||
return true
|
||||
var hub_cost: int = int(game_state.building_costs(GameState.BUILDING_TRANSPORT_HUB)[GameState.RESOURCE_ORE])
|
||||
var before: int = game_state.rocks
|
||||
field.place(GameState.BUILDING_TRANSPORT_HUB, HUB_AT)
|
||||
game_state.spend(controller.cost_of(GameState.BUILDING_TRANSPORT_HUB))
|
||||
if game_state.rocks != before - GameState.TRANSPORT_HUB_COST:
|
||||
if game_state.rocks != before - hub_cost:
|
||||
print("TEST_TRANSPORT_HUB_BUILDING: FAIL (cost %d charged, expected %d)" % [
|
||||
before - game_state.rocks, GameState.TRANSPORT_HUB_COST
|
||||
before - game_state.rocks, hub_cost
|
||||
])
|
||||
quit(1)
|
||||
return true
|
||||
print("researched and placed for %d ore" % GameState.TRANSPORT_HUB_COST)
|
||||
print("researched and placed for %d ore" % hub_cost)
|
||||
elapsed = 0.0
|
||||
stage = 1
|
||||
return false
|
||||
|
|
|
|||
Loading…
Reference in a new issue