From 1721edd1236dd6d2777eda70f3c66826cbf175bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Wisniewski?= Date: Fri, 14 Aug 2026 20:32:58 +0200 Subject: [PATCH] Store resources in generic inventories --- GameState.gd | 236 +++++++++++++------------- GasResourceDefinition.tres | 11 ++ GasResourceGlyph.gd | 19 +++ GasResourceGlyph.gd.uid | 1 + GasResourceGlyph.tscn | 13 ++ Inventory.gd | 79 +++++++++ Inventory.gd.uid | 1 + OreResourceDefinition.tres | 11 ++ OreResourceGlyph.gd | 14 ++ OreResourceGlyph.gd.uid | 1 + OreResourceGlyph.tscn | 13 ++ ResourceBar.gd | 8 +- ResourceDefinition.gd | 7 + ResourceDefinition.gd.uid | 1 + ResourceDefinitions.gd | 18 ++ ResourceDefinitions.gd.uid | 1 + ResourceGlyph.gd | 48 ++---- ResourceTransferList.gd | 6 +- tests/test_generic_inventory.gd | 48 ++++++ tests/test_generic_inventory.gd.uid | 1 + tests/test_manual_supply_route.gd | 3 +- tests/test_resource_transfer.gd | 2 +- tests/test_resources_and_buildings.gd | 2 +- tests/test_transporter_supply.gd | 3 +- 24 files changed, 380 insertions(+), 167 deletions(-) create mode 100644 GasResourceDefinition.tres create mode 100644 GasResourceGlyph.gd create mode 100644 GasResourceGlyph.gd.uid create mode 100644 GasResourceGlyph.tscn create mode 100644 Inventory.gd create mode 100644 Inventory.gd.uid create mode 100644 OreResourceDefinition.tres create mode 100644 OreResourceGlyph.gd create mode 100644 OreResourceGlyph.gd.uid create mode 100644 OreResourceGlyph.tscn create mode 100644 ResourceDefinition.gd create mode 100644 ResourceDefinition.gd.uid create mode 100644 ResourceDefinitions.gd create mode 100644 ResourceDefinitions.gd.uid create mode 100644 tests/test_generic_inventory.gd create mode 100644 tests/test_generic_inventory.gd.uid diff --git a/GameState.gd b/GameState.gd index 11a4b7a..500ec5d 100644 --- a/GameState.gd +++ b/GameState.gd @@ -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 diff --git a/GasResourceDefinition.tres b/GasResourceDefinition.tres new file mode 100644 index 0000000..a36af07 --- /dev/null +++ b/GasResourceDefinition.tres @@ -0,0 +1,11 @@ +[gd_resource type="Resource" script_class="ResourceDefinition" load_steps=3 format=3] + +[ext_resource type="Script" path="res://ResourceDefinition.gd" id="1"] +[ext_resource type="PackedScene" path="res://GasResourceGlyph.tscn" id="2"] + +[resource] +script = ExtResource("1") +resource_id = "gas" +display_name = "Gas" +color = Color(0.85, 0.52, 1, 1) +glyph_scene = ExtResource("2") diff --git a/GasResourceGlyph.gd b/GasResourceGlyph.gd new file mode 100644 index 0000000..8bd9965 --- /dev/null +++ b/GasResourceGlyph.gd @@ -0,0 +1,19 @@ +extends Control + +@export var colour: Color = Color(0.85, 0.52, 1.0) + + +func _draw() -> void: + var centre: Vector2 = size * 0.5 + var radius: float = minf(size.x, size.y) * 0.34 + var bubbles: Array = [ + [centre + Vector2(-radius * 0.38, radius * 0.12), radius * 0.46], + [centre + Vector2(radius * 0.34, radius * 0.26), radius * 0.58], + [centre + Vector2(radius * 0.38, -radius * 0.48), radius * 0.32], + ] + for bubble in bubbles: + var ring := PackedVector2Array() + for i in range(12): + var angle: float = TAU * float(i) / 12.0 + ring.append(bubble[0] + Vector2(cos(angle), sin(angle)) * float(bubble[1])) + NeonDraw.draw_glowing_polygon(self, ring, colour, 1.0, colour.darkened(0.78), maxf(0.5, radius * 0.035)) diff --git a/GasResourceGlyph.gd.uid b/GasResourceGlyph.gd.uid new file mode 100644 index 0000000..1dfbb0c --- /dev/null +++ b/GasResourceGlyph.gd.uid @@ -0,0 +1 @@ +uid://d1ko6yhtjjavx diff --git a/GasResourceGlyph.tscn b/GasResourceGlyph.tscn new file mode 100644 index 0000000..ee02276 --- /dev/null +++ b/GasResourceGlyph.tscn @@ -0,0 +1,13 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://GasResourceGlyph.gd" id="1"] + +[node name="GlyphVisual" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 +script = ExtResource("1") diff --git a/Inventory.gd b/Inventory.gd new file mode 100644 index 0000000..aab25fd --- /dev/null +++ b/Inventory.gd @@ -0,0 +1,79 @@ +class_name Inventory +extends RefCounted + +var total_capacity: int = -1 +var resource_capacities: Dictionary = {} +var _amounts: Dictionary = {} + + +func _init(initial: Dictionary = {}, capacity: int = -1) -> void: + total_capacity = capacity + for resource_id in initial: + set_amount(String(resource_id), int(initial[resource_id])) + + +func amount(resource_id: String) -> int: + return int(_amounts.get(resource_id, 0)) + + +func total() -> int: + var result: int = 0 + for value in _amounts.values(): + result += int(value) + return result + + +func capacity_for(resource_id: String) -> int: + return int(resource_capacities.get(resource_id, -1)) + + +func space_for(resource_id: String) -> int: + var space: int = 1 << 30 + if total_capacity >= 0: + space = maxi(total_capacity - total(), 0) + var resource_capacity: int = capacity_for(resource_id) + if resource_capacity >= 0: + space = mini(space, maxi(resource_capacity - amount(resource_id), 0)) + return space + + +func set_amount(resource_id: String, value: int) -> int: + _amounts.erase(resource_id) + var accepted: int = mini(maxi(value, 0), space_for(resource_id)) + if accepted > 0: + _amounts[resource_id] = accepted + return amount(resource_id) + + +func deposit(resource_id: String, requested: int) -> int: + var moved: int = mini(maxi(requested, 0), space_for(resource_id)) + if moved > 0: + _amounts[resource_id] = amount(resource_id) + moved + return moved + + +func withdraw(resource_id: String, requested: int) -> int: + var moved: int = mini(maxi(requested, 0), amount(resource_id)) + if moved <= 0: + return 0 + var remaining: int = amount(resource_id) - moved + if remaining > 0: + _amounts[resource_id] = remaining + else: + _amounts.erase(resource_id) + return moved + + +func transfer_to(destination: Inventory, resource_id: String, requested: int) -> int: + if destination == null: + return 0 + var moved: int = mini(mini(maxi(requested, 0), amount(resource_id)), destination.space_for(resource_id)) + if moved <= 0: + return 0 + withdraw(resource_id, moved) + destination.deposit(resource_id, moved) + return moved + + +func snapshot() -> Dictionary: + return _amounts.duplicate() diff --git a/Inventory.gd.uid b/Inventory.gd.uid new file mode 100644 index 0000000..59722a2 --- /dev/null +++ b/Inventory.gd.uid @@ -0,0 +1 @@ +uid://celx1yj6cp2rf diff --git a/OreResourceDefinition.tres b/OreResourceDefinition.tres new file mode 100644 index 0000000..6da27dc --- /dev/null +++ b/OreResourceDefinition.tres @@ -0,0 +1,11 @@ +[gd_resource type="Resource" script_class="ResourceDefinition" load_steps=3 format=3] + +[ext_resource type="Script" path="res://ResourceDefinition.gd" id="1"] +[ext_resource type="PackedScene" path="res://OreResourceGlyph.tscn" id="2"] + +[resource] +script = ExtResource("1") +resource_id = "ore" +display_name = "Ore" +color = Color(0, 0.72, 0.82, 1) +glyph_scene = ExtResource("2") diff --git a/OreResourceGlyph.gd b/OreResourceGlyph.gd new file mode 100644 index 0000000..e28f7a3 --- /dev/null +++ b/OreResourceGlyph.gd @@ -0,0 +1,14 @@ +extends Control + +@export var colour: Color = Color(0.0, 0.72, 0.82) + + +func _draw() -> void: + var centre: Vector2 = size * 0.5 + var radius: float = minf(size.x, size.y) * 0.34 + var points := PackedVector2Array() + for i in range(6): + var angle: float = TAU * float(i) / 6.0 - PI * 0.5 + points.append(centre + Vector2(cos(angle), sin(angle)) * radius) + NeonDraw.draw_glowing_polygon(self, points, colour, 1.0, colour.darkened(0.78), maxf(0.5, radius * 0.035)) + NeonDraw.draw_glowing_line(self, points[5], points[2], colour, 0.55, maxf(0.4, radius * 0.025), colour.darkened(0.78)) diff --git a/OreResourceGlyph.gd.uid b/OreResourceGlyph.gd.uid new file mode 100644 index 0000000..2ba6598 --- /dev/null +++ b/OreResourceGlyph.gd.uid @@ -0,0 +1 @@ +uid://cwgkhqo4ev5ao diff --git a/OreResourceGlyph.tscn b/OreResourceGlyph.tscn new file mode 100644 index 0000000..384a20e --- /dev/null +++ b/OreResourceGlyph.tscn @@ -0,0 +1,13 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://OreResourceGlyph.gd" id="1"] + +[node name="GlyphVisual" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 +script = ExtResource("1") diff --git a/ResourceBar.gd b/ResourceBar.gd index 2bf11b4..6594fc1 100644 --- a/ResourceBar.gd +++ b/ResourceBar.gd @@ -3,7 +3,7 @@ extends Control # Compact resource table shared by crafting, research and the ship lab. # Rows are resources; columns are locations. Adding another material to -# GameState.RESOURCE_TYPES automatically gives it a row here. +# The resource-definition catalog automatically gives each material a row. var building_id: int = 0 @@ -24,7 +24,7 @@ func _ready() -> void: add_child(label) _header_labels.append(label) - for resource_type in GameState.RESOURCE_TYPES: + for resource_type in GameState.resource_types(): var glyph := ResourceGlyph.new() glyph.resource_type = resource_type add_child(glyph) @@ -78,7 +78,7 @@ func _layout() -> void: var row_height: float = (size.y - HEADER_HEIGHT - 4.0) / maxf(1.0, float(_rows.size())) var row_index: int = 0 - for resource_type in GameState.RESOURCE_TYPES: + for resource_type in GameState.resource_types(): var row: Dictionary = _rows[resource_type] var y: float = HEADER_HEIGHT + float(row_index) * row_height row.glyph.position = Vector2(2.0, y + 2.0) @@ -94,7 +94,7 @@ func _layout() -> void: func _process(_delta: float) -> void: - for resource_type in GameState.RESOURCE_TYPES: + for resource_type in GameState.resource_types(): var row: Dictionary = _rows.get(resource_type, {}) if row.is_empty(): continue diff --git a/ResourceDefinition.gd b/ResourceDefinition.gd new file mode 100644 index 0000000..acb5acc --- /dev/null +++ b/ResourceDefinition.gd @@ -0,0 +1,7 @@ +class_name ResourceDefinition +extends Resource + +@export var resource_id: String = "ore" +@export var display_name: String = "Ore" +@export var color: Color = Color.WHITE +@export var glyph_scene: PackedScene diff --git a/ResourceDefinition.gd.uid b/ResourceDefinition.gd.uid new file mode 100644 index 0000000..440e5e4 --- /dev/null +++ b/ResourceDefinition.gd.uid @@ -0,0 +1 @@ +uid://kvy02iu7pbyu diff --git a/ResourceDefinitions.gd b/ResourceDefinitions.gd new file mode 100644 index 0000000..7ddf4c0 --- /dev/null +++ b/ResourceDefinitions.gd @@ -0,0 +1,18 @@ +class_name ResourceDefinitions +extends RefCounted + +const ORE_ID: String = "ore" +const GAS_ID: String = "gas" +const ORDER: PackedStringArray = [ORE_ID, GAS_ID] +const DEFINITIONS: Dictionary = { + ORE_ID: preload("res://OreResourceDefinition.tres"), + GAS_ID: preload("res://GasResourceDefinition.tres"), +} + + +static func get_definition(resource_id: String) -> Resource: + return DEFINITIONS.get(resource_id) + + +static func ids() -> PackedStringArray: + return ORDER diff --git a/ResourceDefinitions.gd.uid b/ResourceDefinitions.gd.uid new file mode 100644 index 0000000..9ef7d79 --- /dev/null +++ b/ResourceDefinitions.gd.uid @@ -0,0 +1 @@ +uid://ci7yyyxyd3o67 diff --git a/ResourceGlyph.gd b/ResourceGlyph.gd index aa80578..14a90d0 100644 --- a/ResourceGlyph.gd +++ b/ResourceGlyph.gd @@ -1,6 +1,8 @@ class_name ResourceGlyph extends Control +const RESOURCE_DEFINITIONS = preload("res://ResourceDefinitions.gd") + # A small, non-interactive resource mark shared by tables and transfer rows. # Resource buttons used to borrow IconButton, which made decorative icons # look tappable and encouraged each screen to invent its own version. @@ -12,43 +14,21 @@ func set_resource_type(value: String) -> void: if resource_type == value: return resource_type = value - queue_redraw() + if is_node_ready(): + _rebuild_visual() func _ready() -> void: mouse_filter = Control.MOUSE_FILTER_IGNORE - queue_redraw() + _rebuild_visual() -func _draw() -> void: - var colour: Color = GameState.resource_color(resource_type) - var centre: Vector2 = size * 0.5 - var radius: float = minf(size.x, size.y) * 0.34 - match resource_type: - GameState.RESOURCE_GAS: - _draw_gas(centre, radius, colour) - _: - _draw_ore(centre, radius, colour) - - -func _draw_ore(centre: Vector2, radius: float, colour: Color) -> void: - var points := PackedVector2Array() - for i in range(6): - var angle: float = TAU * float(i) / 6.0 - PI * 0.5 - points.append(centre + Vector2(cos(angle), sin(angle)) * radius) - NeonDraw.draw_glowing_polygon(self, points, colour, 1.0, colour.darkened(0.78), maxf(0.5, radius * 0.035)) - NeonDraw.draw_glowing_line(self, points[5], points[2], colour, 0.55, maxf(0.4, radius * 0.025), colour.darkened(0.78)) - - -func _draw_gas(centre: Vector2, radius: float, colour: Color) -> void: - var bubbles: Array = [ - [centre + Vector2(-radius * 0.38, radius * 0.12), radius * 0.46], - [centre + Vector2(radius * 0.34, radius * 0.26), radius * 0.58], - [centre + Vector2(radius * 0.38, -radius * 0.48), radius * 0.32], - ] - for bubble in bubbles: - var ring := PackedVector2Array() - for i in range(12): - var angle: float = TAU * float(i) / 12.0 - ring.append(bubble[0] + Vector2(cos(angle), sin(angle)) * float(bubble[1])) - NeonDraw.draw_glowing_polygon(self, ring, colour, 1.0, colour.darkened(0.78), maxf(0.5, radius * 0.035)) +func _rebuild_visual() -> void: + for child in get_children(): + child.queue_free() + var definition: Resource = RESOURCE_DEFINITIONS.get_definition(resource_type) + if definition == null or definition.glyph_scene == null: + return + var visual: Control = definition.glyph_scene.instantiate() + visual.colour = definition.color + add_child(visual) diff --git a/ResourceTransferList.gd b/ResourceTransferList.gd index 1883119..859f0f1 100644 --- a/ResourceTransferList.gd +++ b/ResourceTransferList.gd @@ -27,7 +27,7 @@ func _ready() -> void: _destination_heading.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER add_child(_destination_heading) - for resource_type in GameState.RESOURCE_TYPES: + for resource_type in GameState.resource_types(): var colour: Color = GameState.resource_color(resource_type) var glyph := ResourceGlyph.new() glyph.resource_type = resource_type @@ -140,7 +140,7 @@ func set_destination(id: int, heading: String) -> void: func refresh() -> void: - for resource_type in GameState.RESOURCE_TYPES: + for resource_type in GameState.resource_types(): var row: Dictionary = _rows.get(resource_type, {}) if row.is_empty(): continue @@ -169,7 +169,7 @@ func _layout() -> void: var row_height: float = (size.y - HEADER_HEIGHT) / maxf(1.0, float(_rows.size())) var row_index: int = 0 - for resource_type in GameState.RESOURCE_TYPES: + for resource_type in GameState.resource_types(): var row: Dictionary = _rows[resource_type] var y: float = HEADER_HEIGHT + float(row_index) * row_height var glyph_size: float = minf(66.0, row_height * 0.23) diff --git a/tests/test_generic_inventory.gd b/tests/test_generic_inventory.gd new file mode 100644 index 0000000..9e9418c --- /dev/null +++ b/tests/test_generic_inventory.gd @@ -0,0 +1,48 @@ +extends SceneTree + +const INVENTORY = preload("res://Inventory.gd") +const TEST_RESOURCE: String = "crystal" + + +func _initialize() -> void: + var game_state: Node = root.get_node("GameState") + game_state.buildings.clear() + game_state.bank_inventory.set_amount(TEST_RESOURCE, 40) + game_state.fleet_inventory.set_amount(TEST_RESOURCE, 0) + + var mothership_id: int = game_state.add_building(GameState.BUILDING_MOTHERSHIP, Vector2.ZERO) + var crafter_id: int = game_state.add_building(GameState.BUILDING_SHIP_CRAFTER, Vector2(100, 0)) + if game_state.transfer_resource_to_fleet(mothership_id, TEST_RESOURCE, 15) != 15: + return _fail("third resource could not move from bank to fleet") + if game_state.transfer_resource_to_building(crafter_id, TEST_RESOURCE, 9) != 9: + return _fail("third resource could not move from fleet to building") + if game_state.transfer_resource_to_fleet(crafter_id, TEST_RESOURCE, 4) != 4: + return _fail("third resource could not move back out of building") + if game_state.banked_resource(TEST_RESOURCE) != 25 \ + or game_state.fleet_resource(TEST_RESOURCE) != 10 \ + or game_state.building_resource(crafter_id, TEST_RESOURCE) != 5: + return _fail("third-resource amounts were not conserved") + + var limited: RefCounted = INVENTORY.new({}, 10) + limited.resource_capacities[TEST_RESOURCE] = 6 + if limited.deposit(TEST_RESOURCE, 99) != 6 or limited.deposit("water", 99) != 4: + return _fail("total or per-resource capacity was not enforced") + + var legacy_id: int = game_state.next_building_id + game_state.next_building_id += 1 + game_state.buildings.append({"id": legacy_id, "type": GameState.BUILDING_SHIP_CRAFTER, + "position": Vector2.ZERO, "stock": 7, "gas_stock": 3, "orders": []}) + if game_state.building_resource(legacy_id, GameState.RESOURCE_ORE) != 7 \ + or game_state.building_resource(legacy_id, GameState.RESOURCE_GAS) != 3: + return _fail("legacy building stock was not migrated") + var migrated: Dictionary = game_state.find_building(legacy_id) + if migrated.has("stock") or migrated.has("gas_stock") or not migrated.has("inventory"): + return _fail("legacy fields remained after migration") + + print("TEST_GENERIC_INVENTORY: PASS") + quit(0) + + +func _fail(message: String) -> void: + print("TEST_GENERIC_INVENTORY: FAIL (%s)" % message) + quit(1) diff --git a/tests/test_generic_inventory.gd.uid b/tests/test_generic_inventory.gd.uid new file mode 100644 index 0000000..d8972c6 --- /dev/null +++ b/tests/test_generic_inventory.gd.uid @@ -0,0 +1 @@ +uid://53pexhmp3ns0 diff --git a/tests/test_manual_supply_route.gd b/tests/test_manual_supply_route.gd index f8479d1..bfb1f86 100644 --- a/tests/test_manual_supply_route.gd +++ b/tests/test_manual_supply_route.gd @@ -42,7 +42,8 @@ const CRAFTER_AT: Vector2 = Vector2(3000.0, 0.0) func _total() -> int: var sum: int = game_state.rocks + game_state.fleet_cargo for b in game_state.buildings: - sum += int(b.get("stock", 0)) + if b.type != GameState.BUILDING_MOTHERSHIP: + sum += game_state.building_resource(int(b.id), GameState.RESOURCE_ORE) return sum diff --git a/tests/test_resource_transfer.gd b/tests/test_resource_transfer.gd index 7b6a00c..f5035bc 100644 --- a/tests/test_resource_transfer.gd +++ b/tests/test_resource_transfer.gd @@ -39,7 +39,7 @@ func _total() -> int: var sum: int = game_state.rocks + game_state.fleet_cargo for b in game_state.buildings: if b.type != GameState.BUILDING_MOTHERSHIP: - sum += int(b.get("stock", 0)) + sum += game_state.building_resource(int(b.id), GameState.RESOURCE_ORE) return sum diff --git a/tests/test_resources_and_buildings.gd b/tests/test_resources_and_buildings.gd index 6069a1b..17c6780 100644 --- a/tests/test_resources_and_buildings.gd +++ b/tests/test_resources_and_buildings.gd @@ -38,7 +38,7 @@ func _process(delta: float) -> bool: match stage: 0: # --- gather a rock ------------------------------------------- - scene.get_node("RockField").set_process(false) + scene.get_node("SurfaceFeatures/ResourceProvider").set_process(false) rock = load("res://Rock.tscn").instantiate() rock.build_shape(1) # Placed OUTSIDE the pickup shape but inside attract_radius, so diff --git a/tests/test_transporter_supply.gd b/tests/test_transporter_supply.gd index 8130e16..ca9dcbb 100644 --- a/tests/test_transporter_supply.gd +++ b/tests/test_transporter_supply.gd @@ -43,7 +43,8 @@ const CRAFTER_AT: Vector2 = Vector2(900.0, -300.0) func _total() -> int: var sum: int = game_state.rocks + game_state.fleet_cargo for b in game_state.buildings: - sum += int(b.get("stock", 0)) + if b.type != GameState.BUILDING_MOTHERSHIP: + sum += game_state.building_resource(int(b.id), GameState.RESOURCE_ORE) for t in game_state.transporters: sum += int(t.get("carrying", 0)) # Ore inside a build that has STARTED but not finished (#23). It has left