#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
|
|
@ -4,7 +4,17 @@ extends Resource
|
|||
@export var definition_id: String
|
||||
@export var display_name: String
|
||||
@export_multiline var description: String
|
||||
# What it costs in ORE, and the whole price when it wants more than ore.
|
||||
#
|
||||
# #138: structures all charged rocks and nothing else, whatever they were.
|
||||
# The dictionary is the same shape VesselDefinition has used since gas
|
||||
# bombers and laser ships (build_cost / build_resource_costs): when it is
|
||||
# empty the price is `cost` in ore, and when it is set it IS the price, ore
|
||||
# included - and its ore entry must agree with `cost`, which the test pins,
|
||||
# because repair costs are quoted from `cost` alone (a repair drone carries
|
||||
# ore and nothing else).
|
||||
@export var cost: int
|
||||
@export var build_resource_costs: Dictionary = {}
|
||||
@export var action_icon: int
|
||||
@export var menu_icon: int
|
||||
@export var visual_scene: PackedScene
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ definition_id = "command_center"
|
|||
display_name = "Command Center"
|
||||
description = "Fly the fleet onto it to drag your fleet into a new formation. Rebuilt fleets keep it."
|
||||
cost = 15
|
||||
build_resource_costs = {"ore": 15, "gas": 6}
|
||||
action_icon = 10
|
||||
menu_icon = 10
|
||||
visual_scene = ExtResource("2")
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@ script = ExtResource("1")
|
|||
definition_id = "gas_burner"
|
||||
display_name = "Gas Burner"
|
||||
description = "Burns gas into energy at its own site. Hub transporters keep it stocked with gas and collect the energy it makes."
|
||||
cost = 30
|
||||
cost = 40
|
||||
build_resource_costs = {"ore": 40, "gas": 22}
|
||||
action_icon = 33
|
||||
menu_icon = 33
|
||||
visual_scene = ExtResource("2")
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ definition_id = "outpost"
|
|||
display_name = "Outpost"
|
||||
description = "A forward position for the fleet to garrison. Vessels stationed here hold station and fight what comes at them, without you flying back."
|
||||
cost = 30
|
||||
build_resource_costs = {"ore": 30, "gas": 16}
|
||||
action_icon = 40
|
||||
menu_icon = 40
|
||||
visual_scene = ExtResource("2")
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ script = ExtResource("1")
|
|||
definition_id = "planning_center"
|
||||
display_name = "Planning Center"
|
||||
description = "Holds standing orders for the whole game: say how many of each vessel should always exist, and the ship crafters are kept working until they do."
|
||||
cost = 70
|
||||
cost = 50
|
||||
build_resource_costs = {"ore": 50, "gas": 30}
|
||||
action_icon = 41
|
||||
menu_icon = 41
|
||||
visual_scene = ExtResource("2")
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ script = ExtResource("1")
|
|||
definition_id = "ship_lab"
|
||||
display_name = "Ship Lab"
|
||||
description = "Fly the fleet onto it to upgrade each vessel type and unlock new vessel designs."
|
||||
cost = 35
|
||||
cost = 30
|
||||
build_resource_costs = {"ore": 30, "gas": 18}
|
||||
action_icon = 13
|
||||
menu_icon = 13
|
||||
visual_scene = ExtResource("2")
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ script = ExtResource("1")
|
|||
definition_id = "transport_hub"
|
||||
display_name = "Transport Hub"
|
||||
description = "Stations transport vessels. They collect extracted resources and supply buildings that are waiting on materials."
|
||||
cost = 30
|
||||
cost = 28
|
||||
build_resource_costs = {"ore": 28, "gas": 20}
|
||||
action_icon = 12
|
||||
menu_icon = 12
|
||||
visual_scene = ExtResource("2")
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ script = ExtResource("1")
|
|||
definition_id = "spacial_workshop"
|
||||
display_name = "Spacial Workshop"
|
||||
description = "Sends repair drones out to damaged structures. Repairs cost ore in proportion to the damage, delivered here by hub transporters."
|
||||
cost = 55
|
||||
cost = 50
|
||||
build_resource_costs = {"ore": 50, "gas": 28, "energy": 18}
|
||||
action_icon = 42
|
||||
menu_icon = 42
|
||||
visual_scene = ExtResource("2")
|
||||
|
|
|
|||
|
|
@ -352,7 +352,7 @@ func _configure_build_menu_entries() -> void:
|
|||
button.icon = definition.menu_icon
|
||||
button.pressed.connect(_show_building_info.bind(String(definition.definition_id)))
|
||||
label.text = definition.display_name
|
||||
cost_tag.show_price({GameState.RESOURCE_ORE: int(definition.cost)})
|
||||
cost_tag.show_price(GameState.building_costs(String(definition.definition_id)))
|
||||
build_menu_entries.append([definition.definition_id, button, label, cost_tag, definition])
|
||||
|
||||
|
||||
|
|
@ -820,6 +820,11 @@ func _refresh_build_status() -> void:
|
|||
func _refresh_build_option_states() -> void:
|
||||
for entry in build_menu_entries:
|
||||
var definition: Resource = entry[4]
|
||||
# #138: a price now names several resources, and what the player
|
||||
# holds of each is on the tag - so it is re-stated here, live, the
|
||||
# way the craft menu's already was. Re-stating the same price is
|
||||
# cheap; PriceTag only rebuilds when the price itself changes.
|
||||
entry[3].show_price(GameState.building_costs(String(definition.definition_id)))
|
||||
var enabled: bool = GameState.is_building_unlocked(definition.definition_id)
|
||||
if definition.placement_mode == "planet":
|
||||
var planet: Node2D = _planet_under_fleet()
|
||||
|
|
@ -893,16 +898,21 @@ func _refresh_planning() -> void:
|
|||
planning_status.text = "Short: %s" % ", ".join(short)
|
||||
|
||||
|
||||
# The ORE half of a price, which is all some callers want (the info popup's
|
||||
# headline, the tests). costs_of() below is the whole of it.
|
||||
func cost_of(type: String) -> int:
|
||||
var definition: Resource = BUILDING_DEFINITIONS.get_definition(type)
|
||||
return int(definition.cost) if definition != null else 0
|
||||
return int(costs_of(type).get(GameState.RESOURCE_ORE, 0))
|
||||
|
||||
|
||||
func costs_of(type: String) -> Dictionary:
|
||||
return GameState.building_costs(type)
|
||||
|
||||
|
||||
func _show_building_info(type: String) -> void:
|
||||
var definition: Resource = BUILDING_DEFINITIONS.get_definition(type)
|
||||
if definition == null:
|
||||
return
|
||||
var cost: int = int(definition.cost)
|
||||
var costs: Dictionary = GameState.building_costs(type)
|
||||
var lines: Array = [definition.description]
|
||||
var can_build: bool = true
|
||||
|
||||
|
|
@ -933,7 +943,7 @@ 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(cost):
|
||||
if can_build and not GameState.can_afford_resources(costs):
|
||||
# #137: no sentence about the shortfall. The price tag below carries
|
||||
# what is held over what is wanted, in red - saying it twice, in two
|
||||
# different shapes, is what the ticket is about.
|
||||
|
|
@ -941,7 +951,7 @@ func _show_building_info(type: String) -> void:
|
|||
|
||||
info_title.text = definition.display_name
|
||||
info_message.text = "\n\n".join(lines)
|
||||
info_cost.show_price({GameState.RESOURCE_ORE: cost})
|
||||
info_cost.show_price(costs)
|
||||
info_build_button.visible = can_build
|
||||
pending_build_type = type if can_build else ""
|
||||
info_popup.visible = true
|
||||
|
|
@ -983,30 +993,44 @@ 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(definition.cost):
|
||||
build_status.text = "Not enough rocks (need %d, have %d)" % [definition.cost, GameState.available_ore()]
|
||||
if not GameState.spend_resources(GameState.building_costs(type)):
|
||||
build_status.text = _cannot_afford(type)
|
||||
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(definition.cost):
|
||||
build_status.text = "Not enough rocks (need %d, have %d)" % [
|
||||
definition.cost, GameState.available_ore()
|
||||
]
|
||||
if not GameState.can_afford_resources(GameState.building_costs(type)):
|
||||
build_status.text = _cannot_afford(type)
|
||||
return
|
||||
# Only now is the old one destroyed - never before the new one is
|
||||
# known to be affordable, or the player would be left with none.
|
||||
GameState.remove_buildings_of_type(type)
|
||||
building_field.rebuild_all()
|
||||
|
||||
_try_place(type, cost_of(type))
|
||||
_try_place(type)
|
||||
|
||||
|
||||
func _try_place(type: String, cost: int) -> void:
|
||||
if not GameState.spend(cost):
|
||||
build_status.text = "Not enough rocks (need %d, have %d)" % [cost, GameState.available_ore()]
|
||||
# #138: a structure can want more than rocks, so the refusal has to name
|
||||
# which of them is short rather than always saying "rocks". Built from the
|
||||
# price itself, so a cost gaining a resource needs nothing here.
|
||||
func _cannot_afford(type: String) -> String:
|
||||
var missing: Array = []
|
||||
for resource_type in GameState.building_costs(type):
|
||||
var id: String = String(resource_type)
|
||||
var want: int = int(GameState.building_costs(type)[resource_type])
|
||||
var have: int = GameState.available_resource(id)
|
||||
if have < want:
|
||||
missing.append("%s (need %d, have %d)" % [GameState.resource_name(id), want, have])
|
||||
if missing.is_empty():
|
||||
return ""
|
||||
return "Not enough %s" % " and ".join(missing)
|
||||
|
||||
|
||||
func _try_place(type: String) -> void:
|
||||
if not GameState.spend_resources(GameState.building_costs(type)):
|
||||
build_status.text = _cannot_afford(type)
|
||||
return
|
||||
building_field.place(type, fleet.get_centroid())
|
||||
build_menu.visible = false
|
||||
|
|
@ -1050,7 +1074,7 @@ func _refresh_skill_menu(id: String = "") -> void:
|
|||
# now entries on the tag, held-over-needed, in red when short. One place,
|
||||
# and it says WHICH one is missing without a sentence per case.
|
||||
skill_detail.text = "%s\n%s" % [node.name, node.desc]
|
||||
skill_cost.show_price({GameState.RESOURCE_ORE: int(node.cost)}, points_needed)
|
||||
skill_cost.show_price(GameState.skill_costs(id), points_needed)
|
||||
|
||||
if GameState.is_unlocked(id):
|
||||
skill_status.text = "Researched"
|
||||
|
|
|
|||
|
|
@ -330,9 +330,12 @@ func transfer_gas_to_building(id: int, amount: int) -> int:
|
|||
func transfer_gas_to_fleet(id: int, amount: int) -> int:
|
||||
return transfer_resource_to_fleet(id, RESOURCE_GAS, amount)
|
||||
|
||||
const MOTHERSHIP_COST: int = 20
|
||||
const SHIP_CRAFTER_COST: int = 12
|
||||
const SHIP_COST: int = 6
|
||||
# #138: the per-building cost constants that used to sit here are gone. A
|
||||
# structure's price lives in its own BuildingDefinition and is read through
|
||||
# building_costs(), which is the only thing that knows about the resources
|
||||
# beyond ore that some of them now want. Two places holding the same number
|
||||
# is how they end up disagreeing.
|
||||
|
||||
# --- Buildings -------------------------------------------------------------
|
||||
|
||||
|
|
@ -361,11 +364,6 @@ const BUILDING_SHIP_LAB: String = "ship_lab"
|
|||
# inventory exactly like every other building (#54).
|
||||
const BUILDING_GAS_BURNER: String = "gas_burner"
|
||||
|
||||
const COMMAND_CENTER_COST: int = 15
|
||||
const EXTRACTOR_COST: int = 25
|
||||
const TRANSPORT_HUB_COST: int = 30
|
||||
const SHIP_LAB_COST: int = 35
|
||||
const GAS_BURNER_COST: int = 30
|
||||
|
||||
var buildings: Array = []
|
||||
|
||||
|
|
@ -411,6 +409,7 @@ const SKILL_TREE: Array = [
|
|||
# attacking your structures is available at about the time the waves
|
||||
# start being able to hurt them.
|
||||
"id": "forward_bases", "name": "Forward Bases", "cost": 30, "requires": "coordination",
|
||||
"costs": {"ore": 30, "gas": 12},
|
||||
"x": 0.66, "unlocks": BUILDING_OUTPOST, "research_points": 1,
|
||||
"desc": "Build outposts: forward positions vessels can be stationed at, to hold ground you are not standing on.",
|
||||
},
|
||||
|
|
@ -419,6 +418,7 @@ const SKILL_TREE: Array = [
|
|||
# applies to outposts already standing, not only to new ones, so
|
||||
# researching it never means demolishing and rebuilding.
|
||||
"id": "garrison_bays", "name": "Garrison Bays", "cost": 110, "requires": "forward_bases",
|
||||
"costs": {"ore": 110, "gas": 35},
|
||||
"x": 0.66, "outpost_bonus": 1, "research_points": 2,
|
||||
"icon": 40,
|
||||
"desc": "Permanent: +1 berth at every outpost, including ones already built.",
|
||||
|
|
@ -429,6 +429,7 @@ const SKILL_TREE: Array = [
|
|||
# hand has become work, and it has shipyards behind it because it
|
||||
# manages what shipyards do.
|
||||
"id": "logistics_command", "name": "Logistics Command", "cost": 140,
|
||||
"costs": {"ore": 140, "gas": 40},
|
||||
"requires": "vessel_science", "x": 0.94,
|
||||
"unlocks": BUILDING_PLANNING_CENTER, "research_points": 3,
|
||||
"desc": "Build a planning center: set how many of each vessel should always exist, and the crafters are kept working until they do.",
|
||||
|
|
@ -438,6 +439,7 @@ const SKILL_TREE: Array = [
|
|||
# is infrastructure that looks after infrastructure, and it should
|
||||
# arrive after the player has some worth looking after.
|
||||
"id": "field_engineering", "name": "Field Engineering", "cost": 95,
|
||||
"costs": {"ore": 95, "gas": 35, "energy": 20},
|
||||
"requires": "power_systems", "x": 0.34,
|
||||
"unlocks": BUILDING_WORKSHOP, "research_points": 2,
|
||||
"desc": "Build spacial workshops. Their drones fly out and repair damaged structures, spending ore delivered to the workshop.",
|
||||
|
|
@ -446,28 +448,33 @@ const SKILL_TREE: Array = [
|
|||
# Same shape as freight_yards and garrison_bays: a permanent bonus
|
||||
# that reaches workshops already standing.
|
||||
"id": "drone_bays", "name": "Drone Bays", "cost": 150,
|
||||
"costs": {"ore": 150, "gas": 50, "energy": 35},
|
||||
"requires": "field_engineering", "x": 0.34,
|
||||
"workshop_bonus": 1, "research_points": 2, "icon": 43,
|
||||
"desc": "Permanent: +1 repair drone at every spacial workshop, including ones already built.",
|
||||
},
|
||||
{
|
||||
"id": "freight", "name": "Freight Handling", "cost": 40, "requires": "logistics",
|
||||
"costs": {"ore": 40, "gas": 15},
|
||||
"x": 0.18, "unlocks": BUILDING_TRANSPORT_HUB, "research_points": 1,
|
||||
"desc": "Build transport hubs. Each hub includes its own automatic transporters.",
|
||||
},
|
||||
{
|
||||
"id": "vessel_science", "name": "Vessel Science", "cost": 35, "requires": "shipyards",
|
||||
"costs": {"ore": 35, "gas": 10},
|
||||
"x": 0.82, "unlocks": BUILDING_SHIP_LAB, "research_points": 1,
|
||||
"desc": "Build ship labs, where each vessel type is upgraded and new ones unlocked.",
|
||||
},
|
||||
{
|
||||
"id": "freight_yards", "name": "Freight Yards", "cost": 80, "requires": "freight",
|
||||
"costs": {"ore": 80, "gas": 30},
|
||||
"x": 0.18, "hub_bonus": 1, "research_points": 2,
|
||||
"icon": 12,
|
||||
"desc": "Permanent: +1 transporter stationed at every hub, including ones already built.",
|
||||
},
|
||||
{
|
||||
"id": "power_systems", "name": "Power Systems", "cost": 45, "requires": "freight",
|
||||
"costs": {"ore": 45, "gas": 35},
|
||||
"x": 0.50, "unlocks": BUILDING_GAS_BURNER, "research_points": 2,
|
||||
"desc": "Build gas burners. Each converts gas into energy at its own site - hub transporters keep it stocked and collect what it makes.",
|
||||
},
|
||||
|
|
@ -490,12 +497,14 @@ const SKILL_TREE: Array = [
|
|||
# someone else's lane.
|
||||
# Also cut (70 ore / 2 points), for the same reason as wing.
|
||||
"id": "squadron", "name": "Squadron Doctrine", "cost": 35, "requires": "wing",
|
||||
"costs": {"ore": 35, "gas": 12},
|
||||
"x": 0.82, "fleet_bonus": 1, "research_points": 1,
|
||||
"icon": 2,
|
||||
"desc": "Permanent: +1 vessel in the fleet.",
|
||||
},
|
||||
{
|
||||
"id": "armada", "name": "Armada Doctrine", "cost": 90, "requires": "squadron",
|
||||
"costs": {"ore": 90, "gas": 30, "energy": 15},
|
||||
"x": 0.82, "fleet_bonus": 1, "research_points": 2,
|
||||
"icon": 2,
|
||||
"desc": "Permanent: +1 vessel in the fleet.",
|
||||
|
|
@ -506,12 +515,14 @@ const SKILL_TREE: Array = [
|
|||
# that only ever does one thing; two larger, costlier steps carry the
|
||||
# ceiling from 6 to 10 while keeping the branch a readable length.
|
||||
"id": "battlegroup", "name": "Battlegroup Doctrine", "cost": 180, "requires": "armada",
|
||||
"costs": {"ore": 180, "gas": 60, "energy": 40},
|
||||
"x": 0.82, "fleet_bonus": 2, "research_points": 4,
|
||||
"icon": 2,
|
||||
"desc": "Permanent: +2 vessels in the fleet.",
|
||||
},
|
||||
{
|
||||
"id": "grand_fleet", "name": "Grand Fleet Doctrine", "cost": 320, "requires": "battlegroup",
|
||||
"costs": {"ore": 320, "gas": 110, "energy": 80, "dark_matter": 20},
|
||||
"x": 0.82, "fleet_bonus": 2, "research_points": 6,
|
||||
"icon": 2,
|
||||
"desc": "Permanent: +2 vessels in the fleet.",
|
||||
|
|
@ -570,9 +581,26 @@ func prerequisite_met(id: String) -> bool:
|
|||
return node.requires == "" or is_unlocked(node.requires)
|
||||
|
||||
|
||||
# What one doctrine costs. Same rule as buildings and vessels (#138): the
|
||||
# node's own dictionary when it has one, its ore figure otherwise.
|
||||
#
|
||||
# `cost` stays the ore figure either way rather than being folded into the
|
||||
# dictionary and deleted, so the tree's own display - and everything that
|
||||
# ever compared two nodes by price - keeps working on a plain number.
|
||||
func skill_costs(id: String) -> Dictionary:
|
||||
var node: Dictionary = skill_node(id)
|
||||
if node.is_empty():
|
||||
return {}
|
||||
var costs: Dictionary = node.get("costs", {})
|
||||
if not costs.is_empty():
|
||||
return costs.duplicate()
|
||||
return {RESOURCE_ORE: int(node.cost)} if int(node.cost) > 0 else {}
|
||||
|
||||
|
||||
func can_unlock(id: String) -> bool:
|
||||
var node: Dictionary = skill_node(id)
|
||||
return not is_unlocked(id) and prerequisite_met(id) and can_afford(int(node.cost)) \
|
||||
return not is_unlocked(id) and prerequisite_met(id) \
|
||||
and can_afford_resources(skill_costs(id)) \
|
||||
and research_points >= skill_research_point_cost(id)
|
||||
|
||||
|
||||
|
|
@ -584,7 +612,7 @@ func unlock(id: String) -> bool:
|
|||
if not can_unlock(id):
|
||||
return false
|
||||
var node: Dictionary = skill_node(id)
|
||||
if not spend(int(node.cost)):
|
||||
if not spend_resources(skill_costs(id)):
|
||||
return false
|
||||
var points_needed: int = skill_research_point_cost(id)
|
||||
if points_needed > 0:
|
||||
|
|
@ -621,13 +649,24 @@ func is_building_unlocked(type: String) -> bool:
|
|||
return true
|
||||
|
||||
|
||||
func research_for_building(type: String) -> String:
|
||||
# The doctrine that unlocks a structure, BY ID - the handle everything else
|
||||
# in the tree is keyed on (skill_depth, skill_costs, is_unlocked).
|
||||
func research_node_for_building(type: String) -> String:
|
||||
for node in SKILL_TREE:
|
||||
if node.get("unlocks", "") == type:
|
||||
return node.name
|
||||
return String(node.id)
|
||||
return ""
|
||||
|
||||
|
||||
# The same thing by NAME, which is what a message shows the player. Kept
|
||||
# apart from the id above because the two are easy to mix up and the mistake
|
||||
# is silent: skill_depth() of a display name finds no node and answers 0, so
|
||||
# a deep structure reads as an ungated one.
|
||||
func research_for_building(type: String) -> String:
|
||||
var id: String = research_node_for_building(type)
|
||||
return "" if id == "" else String(skill_node(id).name)
|
||||
|
||||
|
||||
func max_fleet_size() -> int:
|
||||
if intro_fleet_size > 0:
|
||||
return mini(intro_fleet_size, DEFAULT_FORMATION.size())
|
||||
|
|
@ -770,6 +809,20 @@ func find_building(id: int) -> Dictionary:
|
|||
# node would heal itself the moment the player landed - the same reason the
|
||||
# buildings themselves live here.
|
||||
|
||||
# What one structure costs, in every resource it wants.
|
||||
#
|
||||
# #138: the same shape product_costs() already answers for vessels - the
|
||||
# dictionary when a definition carries one, the ore figure otherwise - so
|
||||
# there is one question with one answer whatever is being bought.
|
||||
func building_costs(type: String) -> Dictionary:
|
||||
var definition: Resource = building_definition(type)
|
||||
if definition == null:
|
||||
return {}
|
||||
if not definition.build_resource_costs.is_empty():
|
||||
return definition.build_resource_costs.duplicate()
|
||||
return {RESOURCE_ORE: int(definition.cost)} if int(definition.cost) > 0 else {}
|
||||
|
||||
|
||||
func building_definition(type: String) -> Resource:
|
||||
return BUILDING_DEFINITIONS.get_definition(type)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -47,6 +47,13 @@ const POINT_MARK: String = "RP"
|
|||
@export var show_available: bool = true
|
||||
|
||||
var _entries: Array = []
|
||||
# The price currently on show. Kept so a caller that re-states the same price
|
||||
# every frame - the craft menu does, and since #138 the build menu does too -
|
||||
# refreshes the HELD figures instead of tearing down and rebuilding a dozen
|
||||
# Labels sixty times a second.
|
||||
var _shown_costs: Dictionary = {}
|
||||
var _shown_points: int = 0
|
||||
var _built: bool = false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
|
|
@ -56,6 +63,13 @@ func _ready() -> void:
|
|||
|
||||
# costs: {resource_type: amount}. points: research points, 0 for none.
|
||||
func show_price(costs: Dictionary, points: int = 0) -> void:
|
||||
if _built and points == _shown_points and costs == _shown_costs:
|
||||
# Same price, possibly a different wallet.
|
||||
_refresh_held()
|
||||
return
|
||||
_shown_costs = costs.duplicate()
|
||||
_shown_points = points
|
||||
_built = true
|
||||
_clear()
|
||||
for resource_type in GameState.resource_types():
|
||||
var amount: int = int(costs.get(resource_type, 0))
|
||||
|
|
@ -75,6 +89,29 @@ func set_costs(costs: Dictionary) -> void:
|
|||
show_price(costs)
|
||||
|
||||
|
||||
# What the player holds, re-read. The price does not move; the wallet does.
|
||||
func _refresh_held() -> void:
|
||||
var relayout: bool = false
|
||||
for entry in _entries:
|
||||
var have: int = GameState.research_points if String(entry.resource) == "" \
|
||||
else GameState.available_resource(String(entry.resource))
|
||||
if have == int(entry.have):
|
||||
continue
|
||||
entry.have = have
|
||||
entry.short = have < int(entry.cost)
|
||||
if entry.have_label == null:
|
||||
continue
|
||||
entry.have_label.text = "%d/" % have
|
||||
entry.have_label.add_theme_color_override("font_color",
|
||||
SHORT_COLOR if entry.short else HAVE_COLOR)
|
||||
var width: float = _text_width(entry.have_label, HAVE_FONT_SIZE)
|
||||
if not is_equal_approx(width, float(entry.have_width)):
|
||||
entry.have_width = width
|
||||
relayout = true
|
||||
if relayout:
|
||||
_layout()
|
||||
|
||||
|
||||
func is_free() -> bool:
|
||||
return _entries.is_empty()
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue