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