#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
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue