Resolve pending gameplay tickets

This commit is contained in:
Loïc Wisniewski 2026-09-02 13:43:56 +02:00
commit c2ebbfaa2d
28 changed files with 618 additions and 188 deletions

View file

@ -657,10 +657,10 @@ func skill_costs(id: String) -> Dictionary:
return {RESOURCE_ORE: int(node.cost)} if int(node.cost) > 0 else {}
func can_unlock(id: String) -> bool:
func can_unlock(id: String, payment_position: Variant = null) -> bool:
var node: Dictionary = skill_node(id)
return not is_unlocked(id) and prerequisite_met(id) \
and can_afford_resources(skill_costs(id)) \
and can_afford_resources(skill_costs(id), payment_position) \
and research_points >= skill_research_point_cost(id)
@ -668,11 +668,11 @@ func skill_research_point_cost(id: String) -> int:
return int(skill_node(id).get("research_points", 0))
func unlock(id: String) -> bool:
if not can_unlock(id):
func unlock(id: String, payment_position: Variant = null) -> bool:
if not can_unlock(id, payment_position):
return false
var node: Dictionary = skill_node(id)
if not spend_resources(skill_costs(id)):
if not spend_resources(skill_costs(id), payment_position):
return false
var points_needed: int = skill_research_point_cost(id)
if points_needed > 0:
@ -1314,13 +1314,13 @@ func vessel_for_unlock(unlock_id: String) -> Resource:
return null
func unlock_vessel(vessel: String) -> bool:
func unlock_vessel(vessel: String, payment_position: Variant = null) -> bool:
var definition: Resource = vessel_definition(vessel)
if definition == null or definition.unlock_id == "" or vessel_is_unlocked(vessel) or not has_ship_lab():
return false
var costs: Dictionary = research_costs(definition.unlock_id)
var points_needed: int = int(definition.research_point_cost)
if research_points < points_needed or not spend_resources(costs):
if research_points < points_needed or not spend_resources(costs, payment_position):
return false
if points_needed > 0:
research_points -= points_needed
@ -1370,15 +1370,16 @@ func upgrade_cost(id: String) -> int:
return int(d.cost) * (upgrade_level(id) + 1)
func can_buy_upgrade(id: String) -> bool:
return has_ship_lab() and not upgrade_maxed(id) and can_afford(upgrade_cost(id)) \
func can_buy_upgrade(id: String, payment_position: Variant = null) -> bool:
return has_ship_lab() and not upgrade_maxed(id) \
and can_afford(upgrade_cost(id), payment_position) \
and research_points >= research_point_cost(id)
func buy_upgrade(id: String) -> bool:
if not can_buy_upgrade(id):
func buy_upgrade(id: String, payment_position: Variant = null) -> bool:
if not can_buy_upgrade(id, payment_position):
return false
if not spend(upgrade_cost(id)):
if not spend(upgrade_cost(id), payment_position):
return false
var points_needed: int = research_point_cost(id)
if points_needed > 0:
@ -1601,7 +1602,12 @@ const LOCAL_PAY_RANGE: float = 150.0
func fleet_is_at(building: Dictionary) -> bool:
if building.is_empty():
return false
return fleet_position.distance_to(building.position) <= LOCAL_PAY_RANGE
return fleet_can_pay_at(building.position)
func fleet_can_pay_at(payment_position: Variant) -> bool:
return payment_position is Vector2 \
and fleet_position.distance_to(payment_position as Vector2) <= LOCAL_PAY_RANGE
func local_payment_available(building: Dictionary, inventory: RefCounted,
@ -3332,10 +3338,10 @@ func _advance_quests() -> void:
# --- 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.
# Payments draw on the bank and may also draw on what the ships are carrying
# when the caller supplies the concrete place being paid. The player can move
# resources between the two by hand, so using nearby cargo avoids ceremony;
# #180 makes "nearby" part of the API instead of an assumption in the caller.
#
# 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
@ -3343,9 +3349,8 @@ func _advance_quests() -> void:
# 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.
# Bank first, nearby hold second. A call with no position is deliberately
# bank-only: missing context must fail safe instead of teleporting cargo.
#
# The one deliberate exception is AMMO - see AimedFleetAbilityController,
# which checks fleet_resource() directly. A gas bomb is fired from what the
@ -3357,36 +3362,50 @@ func available_resource(resource_type: String) -> int:
return bank_inventory.amount(resource_type) + fleet_inventory.amount(resource_type)
# What can pay at one concrete place. Banked resources remain globally
# available; cargo in the fleet hold joins them only when the fleet is close
# enough to physically hand it to the building or construction site (#180).
# A null position deliberately means "no local payer", so a forgotten context
# can never reach into the hold from across the map.
func available_resource_at(resource_type: String, payment_position: Variant = null) -> int:
var amount: int = bank_inventory.amount(resource_type)
if fleet_can_pay_at(payment_position):
amount += fleet_inventory.amount(resource_type)
return amount
func available_ore() -> int:
return available_resource(RESOURCE_ORE)
func can_afford(cost: int) -> bool:
return available_ore() >= cost
func can_afford(cost: int, payment_position: Variant = null) -> bool:
return available_resource_at(RESOURCE_ORE, payment_position) >= cost
func can_afford_resources(costs: Dictionary) -> bool:
func can_afford_resources(costs: Dictionary, payment_position: Variant = null) -> bool:
for resource_type in costs:
if available_resource(String(resource_type)) < int(costs[resource_type]):
if available_resource_at(String(resource_type), payment_position) \
< int(costs[resource_type]):
return false
return true
func spend(cost: int) -> bool:
return spend_resources({RESOURCE_ORE: cost})
func spend(cost: int, payment_position: Variant = null) -> bool:
return spend_resources({RESOURCE_ORE: cost}, payment_position)
func spend_resources(costs: Dictionary) -> bool:
func spend_resources(costs: Dictionary, payment_position: Variant = null) -> 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):
if not can_afford_resources(costs, payment_position):
return false
var fleet_can_pay: bool = fleet_can_pay_at(payment_position)
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:
if owed > 0 and fleet_can_pay:
fleet_inventory.withdraw(id, owed)
return true