Store resources in generic inventories

This commit is contained in:
Loïc Wisniewski 2026-08-14 20:32:58 +02:00
commit 1721edd123
24 changed files with 385 additions and 172 deletions

View file

@ -1,5 +1,8 @@
extends Node
const INVENTORY = preload("res://Inventory.gd")
const RESOURCE_DEFINITIONS = preload("res://ResourceDefinitions.gd")
# Autoload singleton - the one thing that survives change_scene_to_file()
# scene swaps between space (Main.tscn) and a planet's surface
@ -23,126 +26,101 @@ var explored_chunks: Dictionary = {}
# --- Resources -------------------------------------------------------------
# Ore that has been BANKED at the mothership. This is the only pool anything
# can be bought with.
# Starts stocked for play-testing so buildings can be tried without mining
# first - drop this back to 0 for a real playthrough.
var rocks: int = 500
const RESOURCE_ORE: String = RESOURCE_DEFINITIONS.ORE_ID
const RESOURCE_GAS: String = RESOURCE_DEFINITIONS.GAS_ID
const RESOURCE_TYPES: PackedStringArray = RESOURCE_DEFINITIONS.ORDER
# Gas is a second, banked resource. It is deliberately separate from ore:
# construction still spends ore, while future gas technologies can consume
# gas without making the two materials silently interchangeable.
var gas: int = 0
# All resource-bearing locations use the same container. The old public
# names below are computed compatibility accessors, not separate storage.
var bank_inventory: RefCounted = INVENTORY.new({RESOURCE_ORE: 500})
var fleet_inventory: RefCounted = INVENTORY.new()
# Ore the fleet is physically carrying and has not delivered yet (#14). Mining
# fills this, not `rocks`: ore is cargo until the fleet brings it home, so a
# long mining run is a real trip with something to lose rather than instant
# income from anywhere on the map.
#
# Lives here, like everything else that has to survive Main.tscn being torn
# down and rebuilt on every trip to a planet's surface - and cargo mined ON a
# surface would otherwise be dropped the moment the player took off.
var fleet_cargo: int = 0
var fleet_gas_cargo: int = 0
var rocks: int:
get: return bank_inventory.amount(RESOURCE_ORE)
set(value): bank_inventory.set_amount(RESOURCE_ORE, value)
# Resource-facing UI and logistics iterate this list instead of growing a
# second set of controls and controller branches every time a material is
# introduced. Keep presentation data here with the state it describes so a
# resource has one name and colour everywhere it appears.
const RESOURCE_ORE: String = "ore"
const RESOURCE_GAS: String = "gas"
const RESOURCE_TYPES: PackedStringArray = [RESOURCE_ORE, RESOURCE_GAS]
const RESOURCE_NAMES: Dictionary = {
RESOURCE_ORE: "Ore",
RESOURCE_GAS: "Gas",
}
const RESOURCE_COLORS: Dictionary = {
RESOURCE_ORE: Color(0.0, 0.72, 0.82),
RESOURCE_GAS: Color(0.85, 0.52, 1.0),
}
var gas: int:
get: return bank_inventory.amount(RESOURCE_GAS)
set(value): bank_inventory.set_amount(RESOURCE_GAS, value)
var fleet_cargo: int:
get: return fleet_inventory.amount(RESOURCE_ORE)
set(value): fleet_inventory.set_amount(RESOURCE_ORE, value)
var fleet_gas_cargo: int:
get: return fleet_inventory.amount(RESOURCE_GAS)
set(value): fleet_inventory.set_amount(RESOURCE_GAS, value)
func collect_cargo(amount: int = 1) -> void:
fleet_cargo += amount
fleet_inventory.deposit(RESOURCE_ORE, amount)
func collect_gas_cargo(amount: int = 1) -> void:
fleet_gas_cargo += amount
fleet_inventory.deposit(RESOURCE_GAS, amount)
func resource_types() -> PackedStringArray:
return RESOURCE_DEFINITIONS.ids()
func resource_definition(resource_type: String) -> Resource:
return RESOURCE_DEFINITIONS.get_definition(resource_type)
func resource_name(resource_type: String) -> String:
return String(RESOURCE_NAMES.get(resource_type, resource_type.capitalize()))
var definition: Resource = resource_definition(resource_type)
return String(definition.display_name) if definition != null else resource_type.capitalize()
func resource_color(resource_type: String) -> Color:
return RESOURCE_COLORS.get(resource_type, Color.WHITE)
var definition: Resource = resource_definition(resource_type)
return definition.color if definition != null else Color.WHITE
func banked_resource(resource_type: String) -> int:
match resource_type:
RESOURCE_ORE: return rocks
RESOURCE_GAS: return gas
_: return 0
return bank_inventory.amount(resource_type)
func fleet_resource(resource_type: String) -> int:
match resource_type:
RESOURCE_ORE: return fleet_cargo
RESOURCE_GAS: return fleet_gas_cargo
_: return 0
return fleet_inventory.amount(resource_type)
func _inventory_for_record(record: Dictionary) -> RefCounted:
var inventory: RefCounted = record.get("inventory") as RefCounted
if inventory == null:
inventory = INVENTORY.new()
record.inventory = inventory
# Compatibility migration for state made before #33. Erase each legacy
# field once absorbed so it cannot drift out of step with the inventory.
if record.has("stock"):
inventory.set_amount(RESOURCE_ORE, int(record.stock))
record.erase("stock")
if record.has("gas_stock"):
inventory.set_amount(RESOURCE_GAS, int(record.gas_stock))
record.erase("gas_stock")
return inventory
func building_inventory(id: int) -> RefCounted:
var building: Dictionary = find_building(id)
if building.is_empty():
return null
if building.type == BUILDING_MOTHERSHIP:
return bank_inventory
return _inventory_for_record(building)
func building_resource(id: int, resource_type: String) -> int:
var building: Dictionary = find_building(id)
if building.is_empty():
return 0
if building.type == BUILDING_MOTHERSHIP:
return banked_resource(resource_type)
match resource_type:
RESOURCE_ORE: return int(building.get("stock", 0))
RESOURCE_GAS: return int(building.get("gas_stock", 0))
_: return 0
var inventory: RefCounted = building_inventory(id)
return 0 if inventory == null else inventory.amount(resource_type)
func transfer_resource_to_building(id: int, resource_type: String, amount: int) -> int:
var building: Dictionary = find_building(id)
if building.is_empty():
return 0
var moved: int = mini(maxi(amount, 0), fleet_resource(resource_type))
if moved <= 0:
return 0
match resource_type:
RESOURCE_ORE: fleet_cargo -= moved
RESOURCE_GAS: fleet_gas_cargo -= moved
_: return 0
if building.type == BUILDING_MOTHERSHIP:
match resource_type:
RESOURCE_ORE: rocks += moved
RESOURCE_GAS: gas += moved
else:
match resource_type:
RESOURCE_ORE: building.stock = int(building.get("stock", 0)) + moved
RESOURCE_GAS: building.gas_stock = int(building.get("gas_stock", 0)) + moved
return moved
return fleet_inventory.transfer_to(building_inventory(id), resource_type, amount)
func transfer_resource_to_fleet(id: int, resource_type: String, amount: int) -> int:
var building: Dictionary = find_building(id)
if building.is_empty():
return 0
var moved: int = mini(maxi(amount, 0), building_resource(id, resource_type))
if moved <= 0:
return 0
if building.type == BUILDING_MOTHERSHIP:
match resource_type:
RESOURCE_ORE: rocks -= moved
RESOURCE_GAS: gas -= moved
else:
match resource_type:
RESOURCE_ORE: building.stock = int(building.get("stock", 0)) - moved
RESOURCE_GAS: building.gas_stock = int(building.get("gas_stock", 0)) - moved
match resource_type:
RESOURCE_ORE: fleet_cargo += moved
RESOURCE_GAS: fleet_gas_cargo += moved
return moved
var source: RefCounted = building_inventory(id)
return 0 if source == null else source.transfer_to(fleet_inventory, resource_type, amount)
# The batch size the transfer menu offers, next to "All". A batch rather
@ -401,8 +379,8 @@ func add_building(type: String, at_position: Vector2) -> int:
var id: int = next_building_id
next_building_id += 1
buildings.append({
"id": id, "type": type, "position": at_position, "stock": 0,
"gas_stock": 0, "orders": [],
"id": id, "type": type, "position": at_position,
"inventory": INVENTORY.new(), "orders": [],
})
return id
@ -655,8 +633,7 @@ func can_produce(type: String) -> bool:
func building_stock(id: int) -> int:
var b: Dictionary = find_building(id)
return 0 if b.is_empty() else int(b.get("stock", 0))
return building_resource(id, RESOURCE_ORE)
func pending_orders(id: int) -> int:
@ -703,7 +680,7 @@ func cancel_order(id: int, index: int) -> bool:
return false
var order: Dictionary = orders[index]
if bool(order.started):
b.stock = int(b.get("stock", 0)) + product_cost(String(order.product))
_inventory_for_record(b).deposit(RESOURCE_ORE, product_cost(String(order.product)))
orders.remove_at(index)
return true
@ -718,7 +695,7 @@ func outstanding_cost(id: int) -> int:
# A started build has already paid, so it is not still owed.
if not bool(o.started):
need += product_cost(String(o.product))
return maxi(need - int(b.get("stock", 0)), 0)
return maxi(need - _inventory_for_record(b).amount(RESOURCE_ORE), 0)
func deposit_to_building(id: int, amount: int) -> int:
@ -727,8 +704,7 @@ func deposit_to_building(id: int, amount: int) -> int:
var b: Dictionary = find_building(id)
if b.is_empty():
return 0
b.stock = int(b.get("stock", 0)) + amount
return amount
return _inventory_for_record(b).deposit(RESOURCE_ORE, amount)
func queue_order(id: int, product: String = PRODUCT_SHIP) -> bool:
@ -763,9 +739,10 @@ func _advance_production(delta: float) -> void:
var order: Dictionary = orders[0]
var product: String = String(order.product)
var cost: int = product_cost(product)
var inventory: RefCounted = _inventory_for_record(b)
if not bool(order.started):
if int(b.get("stock", 0)) < cost:
if inventory.amount(RESOURCE_ORE) < cost:
continue
if product == PRODUCT_TRANSPORTER and transporters.size() >= transport_capacity():
# A hub was demolished after this was ordered, so the berth it
@ -773,7 +750,7 @@ func _advance_production(delta: float) -> void:
continue
if product == PRODUCT_TRANSPORTER and nearest_hub_to(b.position).is_empty():
continue
b.stock = int(b.stock) - cost
inventory.withdraw(RESOURCE_ORE, cost)
order.started = true
order.elapsed = float(order.elapsed) + delta
@ -826,10 +803,11 @@ func _advance_lab_research(delta: float) -> void:
var order: Dictionary = orders[0]
var id: String = String(order.id)
var cost: int = UNLOCK_SHIELD_COST if id == UNLOCK_SHIELD_VESSEL else upgrade_cost(id)
var inventory: RefCounted = _inventory_for_record(lab)
if not bool(order.started):
if int(lab.get("stock", 0)) < cost:
if inventory.amount(RESOURCE_ORE) < cost:
continue
lab.stock = int(lab.stock) - cost
inventory.withdraw(RESOURCE_ORE, cost)
order.started = true
order.elapsed = float(order.elapsed) + delta
if float(order.elapsed) < lab_research_time():
@ -918,7 +896,7 @@ func rotate_formation(radians: float) -> void:
# Extractors mine into their OWN local stock. They do not own a magical
# hauler: ore becomes spendable only when a hub-stationed transporter picks
# up a full load and physically delivers it to the mothership. Entries are
# {"position", "planet_name", "resource_type", "stock", "mining_progress"}.
# {"position", "planet_name", "resource_type", "inventory", "mining_progress"}.
const EXTRACTOR_RATE: float = 2.0 # ore per second
var extractors: Array = []
@ -939,7 +917,7 @@ func add_extractor(planet_name: String, at_position: Vector2, resource_type: Str
"position": at_position,
"planet_name": planet_name,
"resource_type": resource_type,
"stock": 0,
"inventory": INVENTORY.new(),
"mining_progress": 0.0,
})
# An extractor is built from SPACE, with the fleet parked on the planet -
@ -952,7 +930,21 @@ func add_extractor(planet_name: String, at_position: Vector2, resource_type: Str
func extractor_stock(index: int) -> int:
if index < 0 or index >= extractors.size():
return 0
return int(extractors[index].get("stock", 0))
var extractor: Dictionary = extractors[index]
return _extractor_inventory(extractor).amount(String(extractor.get("resource_type", RESOURCE_ORE)))
func _extractor_inventory(extractor: Dictionary) -> RefCounted:
var inventory: RefCounted = extractor.get("inventory") as RefCounted
if inventory == null:
inventory = INVENTORY.new()
extractor.inventory = inventory
# Old extractor state had one stock field whose material was described by
# resource_type. Absorb it into that key on first access.
if extractor.has("stock"):
inventory.set_amount(String(extractor.get("resource_type", RESOURCE_ORE)), int(extractor.stock))
extractor.erase("stock")
return inventory
func _process(delta: float) -> void:
@ -965,7 +957,7 @@ func _process(delta: float) -> void:
e.mining_progress = float(e.get("mining_progress", 0.0)) + delta * EXTRACTOR_RATE
var whole: int = int(floor(float(e.mining_progress)))
if whole > 0:
e.stock = int(e.get("stock", 0)) + whole
_extractor_inventory(e).deposit(String(e.get("resource_type", RESOURCE_ORE)), whole)
e.mining_progress = float(e.mining_progress) - whole
_advance_transporters(delta)
@ -1127,7 +1119,8 @@ func _dispatch_transporters() -> void:
var load: int = transporter_load_limit()
for i in range(extractors.size()):
var extractor: Dictionary = extractors[i]
if int(extractor.get("stock", 0)) < load or _transporter_inbound_to_extractor(i):
var resource_id: String = String(extractor.get("resource_type", RESOURCE_ORE))
if _extractor_inventory(extractor).amount(resource_id) < load or _transporter_inbound_to_extractor(i):
continue
var chosen_extractor: Dictionary = {}
var best_extractor_distance: float = INF
@ -1141,7 +1134,7 @@ func _dispatch_transporters() -> void:
if chosen_extractor.is_empty():
return
chosen_extractor.extractor_index = i
chosen_extractor.resource_type = String(extractor.get("resource_type", "ore"))
chosen_extractor.resource_type = resource_id
chosen_extractor.target_id = 0
_begin_leg(chosen_extractor, transporter_position(chosen_extractor), extractor.position,
TRANSPORTER_TO_EXTRACTOR)
@ -1192,13 +1185,13 @@ func _arrive(t: Dictionary) -> void:
TRANSPORTER_TO_MOTHERSHIP:
var target: Dictionary = find_building(int(t.target_id))
var want: int = outstanding_cost(int(t.target_id))
var take: int = mini(mini(want, rocks), transporter_load_limit())
var take: int = mini(mini(want, bank_inventory.amount(RESOURCE_ORE)), transporter_load_limit())
if take <= 0 or target.is_empty():
# Nothing to carry, or the building is gone. Go home empty.
t.target_id = 0
_begin_leg(t, here, hub_pos if hub_pos != null else here, TRANSPORTER_RETURNING)
return
rocks -= take
bank_inventory.withdraw(RESOURCE_ORE, take)
t.carrying = take
_begin_leg(t, here, target.position, TRANSPORTER_TO_BUILDING)
TRANSPORTER_TO_BUILDING:
@ -1206,7 +1199,7 @@ func _arrive(t: Dictionary) -> void:
if deposit_to_building(int(t.target_id), int(t.carrying)) <= 0:
# Building demolished in transit - put the ore back rather
# than quietly destroying it.
rocks += int(t.carrying)
bank_inventory.deposit(RESOURCE_ORE, int(t.carrying))
t.carrying = 0
t.target_id = 0
_begin_leg(t, here, hub_pos if hub_pos != null else here, TRANSPORTER_RETURNING)
@ -1217,20 +1210,19 @@ func _arrive(t: Dictionary) -> void:
return
var extractor: Dictionary = extractors[extractor_index]
var load: int = transporter_load_limit()
if int(extractor.get("stock", 0)) < load:
var resource_id: String = String(extractor.get("resource_type", RESOURCE_ORE))
var extractor_inventory: RefCounted = _extractor_inventory(extractor)
if extractor_inventory.amount(resource_id) < load:
t.extractor_index = -1
_begin_leg(t, here, hub_pos if hub_pos != null else here, TRANSPORTER_RETURNING)
return
extractor.stock = int(extractor.stock) - load
extractor_inventory.withdraw(resource_id, load)
t.carrying = load
t.resource_type = String(extractor.get("resource_type", "ore"))
t.resource_type = resource_id
_begin_leg(t, here, get_mothership_position(), TRANSPORTER_TO_MOTHERSHIP_FROM_EXTRACTOR)
TRANSPORTER_TO_MOTHERSHIP_FROM_EXTRACTOR:
# Bank only on arrival; cargo retains the extractor's resource type.
if String(t.get("resource_type", "ore")) == "gas":
gas += int(t.carrying)
else:
rocks += int(t.carrying)
bank_inventory.deposit(String(t.get("resource_type", RESOURCE_ORE)), int(t.carrying))
t.carrying = 0
t.extractor_index = -1
_begin_leg(t, here, hub_pos if hub_pos != null else here, TRANSPORTER_RETURNING)
@ -1240,11 +1232,11 @@ func _arrive(t: Dictionary) -> void:
func can_afford(cost: int) -> bool:
return rocks >= cost
return bank_inventory.amount(RESOURCE_ORE) >= cost
func spend(cost: int) -> bool:
if not can_afford(cost):
return false
rocks -= cost
bank_inventory.withdraw(RESOURCE_ORE, cost)
return true