From 8231e165ffbefba747cfc550d50792697982ce8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Wisniewski?= Date: Fri, 14 Aug 2026 11:54:31 +0200 Subject: [PATCH] Refactor multi-resource UI and transfers --- BuildController.gd | 88 +++-------- GameState.gd | 149 ++++++++++++------ GasCargoCounter.gd | 10 ++ GasVentField.gd | 18 ++- IconButton.gd | 14 ++ Main.tscn | 249 ++++++++---------------------- PlanetSurface.tscn | 62 +++++++- PlanetSurfaceController.gd | 16 +- ResourceBar.gd | 205 +++++++++++------------- ResourceGlyph.gd | 54 +++++++ ResourceTransferList.gd | 161 +++++++++++++++++++ tests/_cargo_readout_preview.gd | 7 +- tests/_craft_menu_preview.gd | 3 + tests/_gas_surface_preview.gd | 14 ++ tests/_gas_surface_preview.tscn | 6 + tests/_skill_menu_preview.gd | 2 + tests/screenshot.sh | 15 +- tests/test_crafter_feedback.gd | 3 + tests/test_manual_supply_route.gd | 7 +- tests/test_resource_transfer.gd | 2 +- 20 files changed, 649 insertions(+), 436 deletions(-) create mode 100644 GasCargoCounter.gd create mode 100644 ResourceGlyph.gd create mode 100644 ResourceTransferList.gd create mode 100644 tests/_gas_surface_preview.gd create mode 100644 tests/_gas_surface_preview.tscn diff --git a/BuildController.gd b/BuildController.gd index 5acd873..114575f 100644 --- a/BuildController.gd +++ b/BuildController.gd @@ -125,25 +125,14 @@ var lab_building_id: int = 0 @onready var transfer_menu: Control = get_node("../UI/TransferMenu") @onready var transfer_status: Label = get_node("../UI/TransferMenu/StatusLabel") @onready var transfer_close_button: IconButton = get_node("../UI/TransferMenu/CloseButton") -@onready var to_building_label: Label = get_node("../UI/TransferMenu/ToBuildingLabel") -@onready var to_building_batch_button: IconButton = get_node("../UI/TransferMenu/ToBuildingBatchButton") -@onready var to_building_batch_label: Label = get_node("../UI/TransferMenu/ToBuildingBatchLabel") -@onready var to_building_all_label: Label = get_node("../UI/TransferMenu/ToBuildingAllLabel") -@onready var to_building_all_button: IconButton = get_node("../UI/TransferMenu/ToBuildingAllButton") -@onready var to_fleet_label: Label = get_node("../UI/TransferMenu/ToFleetLabel") -@onready var to_fleet_batch_button: IconButton = get_node("../UI/TransferMenu/ToFleetBatchButton") -@onready var to_fleet_batch_label: Label = get_node("../UI/TransferMenu/ToFleetBatchLabel") -@onready var to_fleet_all_label: Label = get_node("../UI/TransferMenu/ToFleetAllLabel") -@onready var to_fleet_all_button: IconButton = get_node("../UI/TransferMenu/ToFleetAllButton") -@onready var gas_to_building_button: IconButton = get_node("../UI/TransferMenu/GasToBuildingButton") -@onready var gas_to_fleet_button: IconButton = get_node("../UI/TransferMenu/GasToFleetButton") +@onready var transfer_list: ResourceTransferList = get_node("../UI/TransferMenu/ResourceList") # Every building menu carries the same header (#24). Held as a list because # the only thing any of them needs is to be told which building it is # reporting on when its panel opens. @onready var resource_bars: Array = [ get_node("../UI/CraftMenu/Bar"), get_node("../UI/SkillMenu/Bar"), - get_node("../UI/LabMenu/Bar"), get_node("../UI/TransferMenu/Bar"), + get_node("../UI/LabMenu/Bar"), ] @onready var skill_menu: Control = get_node("../UI/SkillMenu") @@ -253,12 +242,7 @@ func _ready() -> void: for i in range(queue_slots.size()): queue_slots[i].pressed.connect(_on_queue_slot.bind(i)) transfer_close_button.pressed.connect(func(): transfer_menu.visible = false) - to_building_batch_button.pressed.connect(_on_transfer.bind(true, GameState.CARGO_LOAD_BATCH)) - to_building_all_button.pressed.connect(_on_transfer.bind(true, ALL)) - to_fleet_batch_button.pressed.connect(_on_transfer.bind(false, GameState.CARGO_LOAD_BATCH)) - to_fleet_all_button.pressed.connect(_on_transfer.bind(false, ALL)) - gas_to_building_button.pressed.connect(_on_gas_transfer.bind(true)) - gas_to_fleet_button.pressed.connect(_on_gas_transfer.bind(false)) + transfer_list.transfer_requested.connect(_on_resource_transfer) formation_close_button.pressed.connect(func(): formation_menu.visible = false) formation_reset_button.pressed.connect(_on_reset_formation) @@ -978,40 +962,45 @@ func _on_transfer_pressed() -> void: if current_building == null: return transfer_building_id = current_building.building_id + for bar in resource_bars: + bar.building_id = transfer_building_id transfer_menu.visible = true transfer_status.text = "" _refresh_transfer() func _on_transfer(to_building: bool, amount: int) -> void: + _on_resource_transfer(GameState.RESOURCE_ORE, to_building, amount) + + +func _on_resource_transfer(resource_type: String, to_building: bool, amount: int = ALL) -> void: if transfer_building_id == 0: transfer_status.text = "No building selected - reopen this panel from the building" return - var moved: int = 0 - if to_building: - moved = GameState.transfer_to_building(transfer_building_id, amount) - else: - moved = GameState.transfer_to_fleet(transfer_building_id, amount) + var moved: int = GameState.transfer_resource_to_building( + transfer_building_id, resource_type, amount + ) if to_building else GameState.transfer_resource_to_fleet( + transfer_building_id, resource_type, amount + ) + var resource_name: String = GameState.resource_name(resource_type).to_lower() # Nothing moving is the common case at an empty end, and a silent no-op # is the dead-button problem this panel exists to remove. Say which side # was empty rather than just refusing. if moved <= 0: if to_building: - transfer_status.text = "The fleet is carrying nothing to unload" + transfer_status.text = "The fleet is carrying nothing to unload (no %s)" % resource_name else: - transfer_status.text = "%s has no ore to load" % _transfer_place_name() + transfer_status.text = "%s has no %s to load" % [_transfer_place_name().capitalize(), resource_name] elif to_building: - transfer_status.text = "Moved %d ore into %s" % [moved, _transfer_place_name()] + transfer_status.text = "Moved %d %s into %s" % [moved, resource_name, _transfer_place_name()] else: - transfer_status.text = "Loaded %d ore into the fleet" % moved + transfer_status.text = "Loaded %d %s into the fleet" % [moved, resource_name] _refresh_transfer() func _on_gas_transfer(to_building: bool) -> void: - var moved: int = GameState.transfer_gas_to_building(transfer_building_id, ALL) if to_building else GameState.transfer_gas_to_fleet(transfer_building_id, ALL) - transfer_status.text = "Moved %d gas" % moved if moved > 0 else "No gas available on that side" - _refresh_transfer() + _on_resource_transfer(GameState.RESOURCE_GAS, to_building) func _transfer_place_name() -> String: @@ -1026,38 +1015,11 @@ func _transfer_place_name() -> String: func _refresh_transfer() -> void: - for bar in resource_bars: - bar.building_id = transfer_building_id - - var hold: int = GameState.fleet_cargo - var here: int = GameState.building_holdings(transfer_building_id) - var batch: int = GameState.CARGO_LOAD_BATCH - - to_building_label.text = "ORE" - to_fleet_label.text = "GAS" - to_building_batch_button.visible = false - to_building_batch_label.visible = false - to_fleet_batch_button.visible = false - to_fleet_batch_label.visible = false - to_building_all_label.text = "Unload all" - to_fleet_all_label.text = "Load all" - # The batch buttons say what they would actually move, not the nominal - # batch size: offering "25" when the hold has 4 in it is a promise the - # press cannot keep. - to_building_batch_label.text = "%d" % mini(batch, hold) - to_fleet_batch_label.text = "%d" % mini(batch, here) - - to_building_batch_button.enabled = hold > 0 - to_building_all_button.enabled = hold > 0 - to_fleet_batch_button.enabled = here > 0 - to_fleet_all_button.enabled = here > 0 - for button in [to_building_batch_button, to_building_all_button, - to_fleet_batch_button, to_fleet_all_button]: - button.queue_redraw() - gas_to_building_button.enabled = GameState.fleet_gas_cargo > 0 - gas_to_fleet_button.enabled = GameState.building_gas_holdings(transfer_building_id) > 0 - gas_to_building_button.queue_redraw() - gas_to_fleet_button.queue_redraw() + var building: Dictionary = GameState.find_building(transfer_building_id) + var heading: String = "This building" + if not building.is_empty() and building.type == GameState.BUILDING_MOTHERSHIP: + heading = "Mothership bank" + transfer_list.set_destination(transfer_building_id, heading) func _refresh_queue() -> void: diff --git a/GameState.gd b/GameState.gd index 27f9242..f855f2a 100644 --- a/GameState.gd +++ b/GameState.gd @@ -44,6 +44,22 @@ var gas: int = 0 var fleet_cargo: int = 0 var fleet_gas_cargo: int = 0 +# 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), +} + func collect_cargo(amount: int = 1) -> void: fleet_cargo += amount @@ -51,6 +67,83 @@ func collect_gas_cargo(amount: int = 1) -> void: fleet_gas_cargo += amount +func resource_name(resource_type: String) -> String: + return String(RESOURCE_NAMES.get(resource_type, resource_type.capitalize())) + + +func resource_color(resource_type: String) -> Color: + return RESOURCE_COLORS.get(resource_type, Color.WHITE) + + +func banked_resource(resource_type: String) -> int: + match resource_type: + RESOURCE_ORE: return rocks + RESOURCE_GAS: return gas + _: return 0 + + +func fleet_resource(resource_type: String) -> int: + match resource_type: + RESOURCE_ORE: return fleet_cargo + RESOURCE_GAS: return fleet_gas_cargo + _: return 0 + + +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 + + +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 + + +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 + + # The batch size the transfer menu offers, next to "All". A batch rather # than only all-or-nothing: the hold is the part that can be lost, and # emptying the bank in one tap would strand research, which spends from it. @@ -70,70 +163,29 @@ const CARGO_LOAD_BATCH: int = 25 func building_holdings(id: int) -> int: - var b: Dictionary = find_building(id) - if b.is_empty(): - return 0 - if b.type == BUILDING_MOTHERSHIP: - return rocks - return int(b.get("stock", 0)) + return building_resource(id, RESOURCE_ORE) func building_gas_holdings(id: int) -> int: - var b: Dictionary = find_building(id) - if b.is_empty(): return 0 - return gas if b.type == BUILDING_MOTHERSHIP else int(b.get("gas_stock", 0)) + return building_resource(id, RESOURCE_GAS) func transfer_to_building(id: int, amount: int) -> int: # Hold -> this building. Returns how much actually moved, which is capped # by the hold: a caller asking for "all" passes a number too big on # purpose and reads the result back. - var b: Dictionary = find_building(id) - if b.is_empty(): - return 0 - var moved: int = mini(amount, fleet_cargo) - if moved <= 0: - return 0 - fleet_cargo -= moved - if b.type == BUILDING_MOTHERSHIP: - rocks += moved - else: - b.stock = int(b.get("stock", 0)) + moved - return moved + return transfer_resource_to_building(id, RESOURCE_ORE, amount) func transfer_to_fleet(id: int, amount: int) -> int: # This building -> hold. Capped by what the building actually holds, so # there is no overdraft in either direction. - var b: Dictionary = find_building(id) - if b.is_empty(): - return 0 - var moved: int = mini(amount, building_holdings(id)) - if moved <= 0: - return 0 - if b.type == BUILDING_MOTHERSHIP: - rocks -= moved - else: - b.stock = int(b.get("stock", 0)) - moved - fleet_cargo += moved - return moved + return transfer_resource_to_fleet(id, RESOURCE_ORE, amount) func transfer_gas_to_building(id: int, amount: int) -> int: - var b: Dictionary = find_building(id) - if b.is_empty(): return 0 - var moved := mini(amount, fleet_gas_cargo) - fleet_gas_cargo -= moved - if b.type == BUILDING_MOTHERSHIP: gas += moved - else: b.gas_stock = int(b.get("gas_stock", 0)) + moved - return moved + return transfer_resource_to_building(id, RESOURCE_GAS, amount) func transfer_gas_to_fleet(id: int, amount: int) -> int: - var b: Dictionary = find_building(id) - if b.is_empty(): return 0 - var moved := mini(amount, building_gas_holdings(id)) - if b.type == BUILDING_MOTHERSHIP: gas -= moved - else: b.gas_stock = int(b.get("gas_stock", 0)) - moved - fleet_gas_cargo += moved - return moved + return transfer_resource_to_fleet(id, RESOURCE_GAS, amount) const MOTHERSHIP_COST: int = 20 const SHIP_CRAFTER_COST: int = 12 @@ -348,7 +400,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, "orders": [], + "id": id, "type": type, "position": at_position, "stock": 0, + "gas_stock": 0, "orders": [], }) return id diff --git a/GasCargoCounter.gd b/GasCargoCounter.gd new file mode 100644 index 0000000..ffa8cf4 --- /dev/null +++ b/GasCargoCounter.gd @@ -0,0 +1,10 @@ +extends "res://GasCounter.gd" + +# Fleet-side companion to GasCounter. It keeps the gas bubble glyph instead +# of recolouring ore, and occupies the same row as banked gas in the HUD. + + +func _process(_delta: float) -> void: + visible = GameState.fleet_gas_cargo > 0 + if visible: + set_amount(GameState.fleet_gas_cargo) diff --git a/GasVentField.gd b/GasVentField.gd index 908a664..7f8e9c7 100644 --- a/GasVentField.gd +++ b/GasVentField.gd @@ -18,4 +18,20 @@ func _process(delta: float) -> void: progress = 0.0 func _draw() -> void: for vent in vents: - NeonDraw.draw_glowing_polygon(self, PackedVector2Array([vent+Vector2(0,-44),vent+Vector2(38,-22),vent+Vector2(38,22),vent+Vector2(0,44),vent+Vector2(-38,22),vent+Vector2(-38,-22)]), Color(0.85,0.52,1),0.8,Color(0.15,0.03,0.22),0.5) + _draw_gas_source(vent) + + +func _draw_gas_source(centre: Vector2) -> void: + var colour := Color(0.85, 0.52, 1.0) + var core := Color(0.15, 0.03, 0.22) + var bubbles: Array = [ + [centre + Vector2(-20.0, 8.0), 20.0], + [centre + Vector2(18.0, 14.0), 27.0], + [centre + Vector2(17.0, -22.0), 15.0], + ] + 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, 0.9, core, 0.6) diff --git a/IconButton.gd b/IconButton.gd index 9e61c71..a0b7ef6 100644 --- a/IconButton.gd +++ b/IconButton.gd @@ -27,6 +27,7 @@ enum Icon { UPGRADE_ENGINES, UPGRADE_UNLOCK_SHIELD, ACTION_MENU, GAS, + LEFT_ARROW, RIGHT_ARROW, } # Size the glyphs were originally drawn against. Every glyph dimension is @@ -203,6 +204,10 @@ func _draw() -> void: _draw_action_menu(center, pulse, s) Icon.GAS: _draw_gas(center, pulse, s) + Icon.LEFT_ARROW: + _draw_horizontal_arrow(center, pulse, -1.0, s) + Icon.RIGHT_ARROW: + _draw_horizontal_arrow(center, pulse, 1.0, s) func _draw_close(center: Vector2, pulse: float, s: float) -> void: @@ -228,6 +233,15 @@ func _draw_arrow(center: Vector2, pulse: float, direction: float, s: float) -> v NeonDraw.draw_glowing_line(self, tip, tip + Vector2(r * 0.7, -r * 0.8 * direction), _active_color(), pulse, 0.5 * s, core_color) +func _draw_horizontal_arrow(center: Vector2, pulse: float, direction: float, s: float) -> void: + var r: float = 10.0 * s + var tip: Vector2 = center + Vector2(r * direction, 0.0) + var tail: Vector2 = center + Vector2(-r * direction, 0.0) + NeonDraw.draw_glowing_line(self, tail, tip, _active_color(), pulse, 0.5 * s, core_color) + NeonDraw.draw_glowing_line(self, tip, tip + Vector2(-r * 0.8 * direction, -r * 0.7), _active_color(), pulse, 0.5 * s, core_color) + NeonDraw.draw_glowing_line(self, tip, tip + Vector2(-r * 0.8 * direction, r * 0.7), _active_color(), pulse, 0.5 * s, core_color) + + func _draw_transfer_icon(center: Vector2, pulse: float, s: float) -> void: # Two shafts side by side pointing opposite ways. Drawn as a pair rather # than as one double-headed arrow because the button's whole job is that diff --git a/Main.tscn b/Main.tscn index 5e30cba..cec2013 100644 --- a/Main.tscn +++ b/Main.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=23 format=3] +[gd_scene load_steps=29 format=3] [ext_resource type="Script" path="res://Starfield.gd" id="1"] [ext_resource type="Script" path="res://Fleet.gd" id="2"] @@ -25,6 +25,8 @@ [ext_resource type="Script" path="res://ResourceBar.gd" id="24"] [ext_resource type="Script" path="res://GasCounter.gd" id="25"] [ext_resource type="Script" path="res://VesselDeliveryField.gd" id="26"] +[ext_resource type="Script" path="res://ResourceTransferList.gd" id="27"] +[ext_resource type="Script" path="res://GasCargoCounter.gd" id="28"] [sub_resource type="ShaderMaterial" id="1"] shader = ExtResource("5") @@ -90,48 +92,58 @@ layer = 1 [node name="ResourceCounter" type="Control" parent="UI"] mouse_filter = 2 offset_left = 28 -offset_top = 24 +offset_top = 38 offset_right = 320 -offset_bottom = 92 +offset_bottom = 82 script = ExtResource("16") -digit_height = 60.0 +digit_height = 38.0 [node name="OreCaption" type="Label" parent="UI"] mouse_filter = 2 offset_left = 28 -offset_top = 22 -offset_right = 120 -offset_bottom = 48 +offset_top = 6 +offset_right = 220 +offset_bottom = 34 theme_override_font_sizes/font_size = 18 -text = "ORE" +theme_override_colors/font_color = Color(0.55, 0.78, 0.86, 1) +text = "BANKED" [node name="GasCounter" type="Control" parent="UI"] mouse_filter = 2 -offset_left = 294 -offset_top = 24 -offset_right = 468 -offset_bottom = 92 -script = ExtResource("25") -digit_height = 60.0 - -[node name="GasCaption" type="Label" parent="UI"] -mouse_filter = 2 -offset_left = 294 -offset_top = 22 -offset_right = 380 -offset_bottom = 48 -theme_override_font_sizes/font_size = 18 -theme_override_colors/font_color = Color(0.85, 0.52, 1, 1) -text = "GAS" - -[node name="CargoCounter" type="Control" parent="UI"] -mouse_filter = 2 offset_left = 28 offset_top = 96 offset_right = 320 -offset_bottom = 146 +offset_bottom = 140 +script = ExtResource("25") +digit_height = 38.0 + +[node name="GasCaption" type="Label" parent="UI"] +mouse_filter = 2 +offset_left = 356 +offset_top = 6 +offset_right = 620 +offset_bottom = 34 +theme_override_font_sizes/font_size = 18 +theme_override_colors/font_color = Color(1, 0.78, 0.25, 1) +text = "FLEET HOLD" + +[node name="CargoCounter" type="Control" parent="UI"] +mouse_filter = 2 +offset_left = 356 +offset_top = 38 +offset_right = 696 +offset_bottom = 82 script = ExtResource("22") -digit_height = 42.0 +digit_height = 38.0 + +[node name="GasCargoCounter" type="Control" parent="UI"] +mouse_filter = 2 +offset_left = 356 +offset_top = 96 +offset_right = 696 +offset_bottom = 140 +script = ExtResource("28") +digit_height = 38.0 [node name="PlanetNameLabel" type="Label" parent="UI"] visible = false @@ -947,10 +959,11 @@ horizontal_alignment = 1 [node name="Bar" type="Control" parent="UI/LabMenu"] mouse_filter = 2 +anchor_right = 1.0 offset_left = 24 offset_top = 180 -offset_right = 520 -offset_bottom = 260 +offset_right = -24 +offset_bottom = 264 script = ExtResource("24") [node name="VesselA" type="Control" parent="UI/LabMenu"] @@ -1004,7 +1017,7 @@ anchor_left = 0.5 anchor_right = 0.5 offset_left = -330 offset_right = -130 -offset_top = 376 +offset_top = 400 offset_bottom = 460 theme_override_font_sizes/font_size = 22 horizontal_alignment = 1 @@ -1258,7 +1271,7 @@ mouse_filter = 2 offset_left = 24 offset_top = 180 offset_right = 520 -offset_bottom = 260 +offset_bottom = 264 script = ExtResource("24") [node name="StatusLabel" type="Label" parent="UI/CraftMenu"] @@ -1467,142 +1480,29 @@ offset_left = 24 offset_top = 36 offset_right = -200 offset_bottom = 166 -text = "Move ore" +text = "Transfer cargo" horizontal_alignment = 1 -[node name="Bar" type="Control" parent="UI/TransferMenu"] -mouse_filter = 2 -offset_left = 24 -offset_top = 180 -offset_right = 520 -offset_bottom = 260 -script = ExtResource("24") - [node name="StatusLabel" type="Label" parent="UI/TransferMenu"] mouse_filter = 2 anchor_right = 1.0 offset_left = 24 -offset_top = 274 +offset_top = 184 offset_right = -24 -offset_bottom = 396 -theme_override_font_sizes/font_size = 34 +offset_bottom = 284 +theme_override_font_sizes/font_size = 30 autowrap_mode = 2 text = "" horizontal_alignment = 1 -[node name="ToBuildingLabel" type="Label" parent="UI/TransferMenu"] +[node name="ResourceList" type="Control" parent="UI/TransferMenu"] mouse_filter = 2 anchor_right = 1.0 offset_left = 24 -offset_top = 376 +offset_top = 294 offset_right = -24 -offset_bottom = 426 -theme_override_font_sizes/font_size = 28 -text = "" -horizontal_alignment = 1 - -[node name="ToBuildingBatchButton" type="Control" parent="UI/TransferMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = -184 -offset_right = -36 -offset_top = 438 -offset_bottom = 586 -script = ExtResource("10") -icon = 3 -glow_color = Color(0.45, 0.9, 0.55, 1) - -[node name="ToBuildingBatchLabel" type="Label" parent="UI/TransferMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = -230 -offset_right = 10 -offset_top = 592 -offset_bottom = 638 -theme_override_font_sizes/font_size = 24 -text = "" -horizontal_alignment = 1 - -[node name="ToBuildingAllButton" type="Control" parent="UI/TransferMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = -180 -offset_right = -32 -offset_top = 438 -offset_bottom = 586 -script = ExtResource("10") -icon = 3 -glow_color = Color(0.45, 0.9, 0.55, 1) - -[node name="ToBuildingAllLabel" type="Label" parent="UI/TransferMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = -230 -offset_right = 10 -offset_top = 592 -offset_bottom = 638 -theme_override_font_sizes/font_size = 24 -text = "Unload all" -horizontal_alignment = 1 - -[node name="ToFleetLabel" type="Label" parent="UI/TransferMenu"] -mouse_filter = 2 -anchor_right = 1.0 -offset_left = 24 -offset_top = 680 -offset_right = -24 -offset_bottom = 730 -theme_override_font_sizes/font_size = 28 -text = "" -horizontal_alignment = 1 - -[node name="ToFleetBatchButton" type="Control" parent="UI/TransferMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = -184 -offset_right = -36 -offset_top = 756 -offset_bottom = 904 -script = ExtResource("10") -icon = 4 -glow_color = Color(1, 0.78, 0.25, 1) - -[node name="ToFleetBatchLabel" type="Label" parent="UI/TransferMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = -230 -offset_right = 10 -offset_top = 910 -offset_bottom = 962 -theme_override_font_sizes/font_size = 30 -text = "" -horizontal_alignment = 1 - -[node name="ToFleetAllButton" type="Control" parent="UI/TransferMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = 32 -offset_right = 180 -offset_top = 438 -offset_bottom = 586 -script = ExtResource("10") -icon = 4 -glow_color = Color(1, 0.78, 0.25, 1) - -[node name="ToFleetAllLabel" type="Label" parent="UI/TransferMenu"] -mouse_filter = 2 -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = 10 -offset_right = 230 -offset_top = 592 -offset_bottom = 638 -theme_override_font_sizes/font_size = 24 -text = "Load all" -horizontal_alignment = 1 +offset_bottom = 1080 +script = ExtResource("27") [node name="CloseButton" type="Control" parent="UI/TransferMenu"] anchor_left = 1.0 @@ -1615,37 +1515,17 @@ script = ExtResource("10") icon = 1 glow_color = Color(1, 0.78, 0.25, 1) -[node name="GasToBuildingButton" type="Control" parent="UI/TransferMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = -180 -offset_right = -32 -offset_top = 742 -offset_bottom = 890 -script = ExtResource("10") -icon = 25 -glow_color = Color(0.85, 0.52, 1, 1) - -[node name="GasToFleetButton" type="Control" parent="UI/TransferMenu"] -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = 32 -offset_right = 180 -offset_top = 742 -offset_bottom = 890 -script = ExtResource("10") -icon = 25 -glow_color = Color(0.85, 0.52, 1, 1) - -[node name="GasTransferLabel" type="Label" parent="UI/TransferMenu"] +[node name="HintLabel" type="Label" parent="UI/TransferMenu"] mouse_filter = 2 anchor_right = 1.0 -offset_top = 896 -offset_bottom = 942 -text = "Unload all Load all" +offset_left = 24 +offset_top = 1092 +offset_right = -24 +offset_bottom = 1150 +text = "Arrows move everything available on that side" horizontal_alignment = 1 -theme_override_colors/font_color = Color(0.85, 0.52, 1, 1) -theme_override_font_sizes/font_size = 22 +theme_override_colors/font_color = Color(0.55, 0.7, 0.78, 1) +theme_override_font_sizes/font_size = 20 [node name="FormationMenu" type="Control" parent="UI" groups=["modal_menus"]] visible = false @@ -1755,10 +1635,11 @@ horizontal_alignment = 1 [node name="Bar" type="Control" parent="UI/SkillMenu"] mouse_filter = 2 +anchor_right = 1.0 offset_left = 24.0 offset_top = 180.0 -offset_right = 520.0 -offset_bottom = 260.0 +offset_right = -24.0 +offset_bottom = 264.0 script = ExtResource("24") [node name="Tree" type="Control" parent="UI/SkillMenu"] diff --git a/PlanetSurface.tscn b/PlanetSurface.tscn index f5058a9..507ec2b 100644 --- a/PlanetSurface.tscn +++ b/PlanetSurface.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=12 format=3] +[gd_scene load_steps=15 format=3] [ext_resource type="Script" path="res://GameController.gd" id="1"] [ext_resource type="Script" path="res://LunarBackground.gd" id="2"] @@ -10,6 +10,9 @@ [ext_resource type="Script" path="res://RockField.gd" id="8"] [ext_resource type="Script" path="res://ResourceCounter.gd" id="9"] [ext_resource type="Script" path="res://GasVentField.gd" id="10"] +[ext_resource type="Script" path="res://GasCounter.gd" id="11"] +[ext_resource type="Script" path="res://CargoCounter.gd" id="12"] +[ext_resource type="Script" path="res://GasCargoCounter.gd" id="13"] [sub_resource type="ShaderMaterial" id="1"] shader = ExtResource("3") @@ -62,14 +65,65 @@ layer = 1 [node name="ResourceCounter" type="Control" parent="UI"] mouse_filter = 2 offset_left = 28 -offset_top = 24 +offset_top = 38 offset_right = 320 -offset_bottom = 92 +offset_bottom = 82 script = ExtResource("9") -digit_height = 60.0 +digit_height = 38.0 glow_color = Color(0, 0.42, 0.48, 1) core_color = Color(0, 0.08, 0.1, 1) +[node name="BankCaption" type="Label" parent="UI"] +mouse_filter = 2 +offset_left = 28 +offset_top = 6 +offset_right = 220 +offset_bottom = 34 +theme_override_font_sizes/font_size = 18 +theme_override_colors/font_color = Color(0.08, 0.16, 0.2, 1) +theme_override_colors/font_outline_color = Color(0.9, 0.95, 0.96, 0.8) +theme_override_constants/outline_size = 4 +text = "BANKED" + +[node name="GasCounter" type="Control" parent="UI"] +mouse_filter = 2 +offset_left = 28 +offset_top = 96 +offset_right = 320 +offset_bottom = 140 +script = ExtResource("11") +digit_height = 38.0 + +[node name="FleetCaption" type="Label" parent="UI"] +mouse_filter = 2 +offset_left = 356 +offset_top = 6 +offset_right = 620 +offset_bottom = 34 +theme_override_font_sizes/font_size = 18 +theme_override_colors/font_color = Color(0.24, 0.14, 0.02, 1) +theme_override_colors/font_outline_color = Color(0.96, 0.93, 0.82, 0.85) +theme_override_constants/outline_size = 4 +text = "FLEET HOLD" + +[node name="CargoCounter" type="Control" parent="UI"] +mouse_filter = 2 +offset_left = 356 +offset_top = 38 +offset_right = 696 +offset_bottom = 82 +script = ExtResource("12") +digit_height = 38.0 + +[node name="GasCargoCounter" type="Control" parent="UI"] +mouse_filter = 2 +offset_left = 356 +offset_top = 96 +offset_right = 696 +offset_bottom = 140 +script = ExtResource("13") +digit_height = 38.0 + [node name="PlanetNameLabel" type="Label" parent="UI"] offset_left = 24 offset_top = 156 diff --git a/PlanetSurfaceController.gd b/PlanetSurfaceController.gd index 985e1b4..502d8d4 100644 --- a/PlanetSurfaceController.gd +++ b/PlanetSurfaceController.gd @@ -20,10 +20,22 @@ var transitioning: bool = false func _ready() -> void: if GameState.planet_type == "gas_giant": # A gas giant is not lunar terrain: its surface view uses a deep, - # coloured atmosphere instead of the rocky field. + # coloured atmosphere instead of the rocky field. The background is + # shader-driven, so changing ColorRect.color does not tint it; update + # the actual palette uniforms. get_node("../RockField").visible = false get_node("../GasVentField").visible = true - get_node("../BackgroundLayer/LunarBackground").color = Color(0.16, 0.035, 0.26, 1.0) + var background: ColorRect = get_node("../BackgroundLayer/LunarBackground") + var atmosphere: ShaderMaterial = background.material + atmosphere.set_shader_parameter("base_color", Color(0.13, 0.025, 0.22, 1.0)) + atmosphere.set_shader_parameter("crater_color", Color(0.055, 0.008, 0.11, 1.0)) + atmosphere.set_shader_parameter("highlight_color", Color(0.52, 0.17, 0.7, 1.0)) + # The lunar surface uses dark captions on a bright ground. Flip only + # those captions for the night-dark gas atmosphere. + for caption_path in ["../UI/BankCaption", "../UI/FleetCaption"]: + var caption: Label = get_node(caption_path) + caption.add_theme_color_override("font_color", Color(0.78, 0.88, 0.94)) + caption.add_theme_color_override("font_outline_color", Color(0.02, 0.01, 0.05, 0.9)) fleet.spawn_ships(GameState.surviving_ship_count, Vector2.ZERO) camera.position = Vector2.ZERO camera.zoom = Vector2(GameState.return_camera_zoom, GameState.return_camera_zoom) diff --git a/ResourceBar.gd b/ResourceBar.gd index 830d27d..2bf11b4 100644 --- a/ResourceBar.gd +++ b/ResourceBar.gd @@ -1,146 +1,113 @@ class_name ResourceBar extends Control -# The header every building menu now carries (#24): banked ore, what the -# fleet is holding, and what THIS building has. Before this, each panel -# improvised its own - the crafter spelled the same three figures into a -# run-on StatusLabel string, the research screen printed a bank total inside -# a button caption - so the same numbers looked different in every room. -# -# Three columns rather than a sentence, because the point of the bar is that -# a figure stays in the same place as the player moves between menus. -# -# Reads GameState directly every frame, like ResourceCounter and CargoCounter -# do, so it stays correct across the space <-> planet-surface scene swap with -# nobody having to push updates into it. Only `building_id` is pushed in, -# because "here" is the one thing the bar cannot work out for itself. +# 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. -# Which building the HERE column is reporting. 0 means none, which greys the -# column rather than showing a confident zero. var building_id: int = 0 -# The caption box has to be a full line tall, not just the font size: a Label -# renders its line height, not its point size, and the first version sat the -# digits 26px down from the top straight through the middle of the caption. -const CAPTION_HEIGHT: float = 34.0 -const CAPTION_GAP: float = 4.0 -const CAPTION_FONT_SIZE: int = 24 -const DIGIT_HEIGHT: float = 34.0 - -# Bank keeps NeonNumber's own ore-teal, and the hold keeps the amber -# CargoCounter established for ore in transit. Only HERE needed a new colour; -# it borrows the mothership's green, which already reads as "landed -# somewhere and staying there" everywhere else in the game. -const BANK_GLOW: Color = Color(0.0, 0.62, 0.7) -const BANK_CORE: Color = Color(0.0, 0.12, 0.15) -const HOLD_GLOW: Color = Color(1.0, 0.78, 0.25) -const HOLD_CORE: Color = Color(0.22, 0.12, 0.0) -const HERE_GLOW: Color = Color(0.45, 0.9, 0.55) -const HERE_CORE: Color = Color(0.05, 0.16, 0.08) +const HEADER_HEIGHT: float = 22.0 const RULE_COLOR: Color = Color(0.25, 0.55, 0.7) +const FLEET_COLOR: Color = Color(1.0, 0.78, 0.25) +const LOCAL_COLOR: Color = Color(0.45, 0.9, 0.55) -var _numbers: Array[NeonNumber] = [] -var _captions: Array[Label] = [] -var _gas_caption: Label -var _gas_icon: IconButton -var _summary: Label +var _header_labels: Array[Label] = [] +var _rows: Dictionary = {} func _ready() -> void: mouse_filter = Control.MOUSE_FILTER_IGNORE - var columns: Array = [ - ["BANK", BANK_GLOW, BANK_CORE], - ["HOLD", HOLD_GLOW, HOLD_CORE], - ["HERE", HERE_GLOW, HERE_CORE], - ] - for column in columns: - var caption: Label = Label.new() - caption.text = column[0] - caption.mouse_filter = Control.MOUSE_FILTER_IGNORE - caption.add_theme_font_size_override("font_size", CAPTION_FONT_SIZE) - caption.add_theme_color_override("font_color", column[1]) - add_child(caption) - _captions.append(caption) + for heading in ["", "BANKED", "FLEET", "LOCAL"]: + var label := _make_label(heading, 16, Color(0.62, 0.78, 0.86)) + label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + add_child(label) + _header_labels.append(label) - var number: NeonNumber = NeonNumber.new() - number.mouse_filter = Control.MOUSE_FILTER_IGNORE - number.glow_color = column[1] - number.core_color = column[2] - number.digit_height = DIGIT_HEIGHT - add_child(number) - _numbers.append(number) + for resource_type in GameState.RESOURCE_TYPES: + var glyph := ResourceGlyph.new() + glyph.resource_type = resource_type + add_child(glyph) + var name_label := _make_label(GameState.resource_name(resource_type).to_upper(), 18, + GameState.resource_color(resource_type)) + add_child(name_label) + var bank_label := _make_value_label(GameState.resource_color(resource_type)) + var fleet_label := _make_value_label(FLEET_COLOR) + var local_label := _make_value_label(LOCAL_COLOR) + for label in [bank_label, fleet_label, local_label]: + add_child(label) + _rows[resource_type] = { + "glyph": glyph, "name": name_label, "bank": bank_label, + "fleet": fleet_label, "local": local_label, + } - _layout() resized.connect(_layout) - _gas_caption = Label.new() - _gas_caption.mouse_filter = Control.MOUSE_FILTER_IGNORE - _gas_caption.add_theme_font_size_override("font_size", 20) - _gas_caption.add_theme_color_override("font_color", Color(0.85, 0.52, 1.0)) - add_child(_gas_caption) - _gas_icon = IconButton.new() - _gas_icon.mouse_filter = Control.MOUSE_FILTER_IGNORE - _gas_icon.icon = IconButton.Icon.GAS - _gas_icon.glow_color = Color(0.85, 0.52, 1.0) - add_child(_gas_icon) - _summary = Label.new() - _summary.mouse_filter = Control.MOUSE_FILTER_IGNORE - _summary.add_theme_font_size_override("font_size", 18) - _summary.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER - _summary.size = size - add_child(_summary) + _layout() + queue_redraw() + + +func _make_label(text_value: String, font_size: int, colour: Color) -> Label: + var label := Label.new() + label.text = text_value + label.mouse_filter = Control.MOUSE_FILTER_IGNORE + label.add_theme_font_size_override("font_size", font_size) + label.add_theme_color_override("font_color", colour) + label.add_theme_color_override("font_outline_color", Color(0.01, 0.01, 0.035, 0.95)) + label.add_theme_constant_override("outline_size", 2) + label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + return label + + +func _make_value_label(colour: Color) -> Label: + var label := _make_label("0", 19, colour) + label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + return label func _layout() -> void: - var column_width: float = size.x / float(_numbers.size()) - for i in _numbers.size(): - var x: float = column_width * float(i) - _captions[i].position = Vector2(x, 0.0) - _captions[i].size = Vector2(column_width, CAPTION_HEIGHT) - # The hexagon and digits are drawn from the node's own origin, so the - # number is positioned rather than sized. - _numbers[i].position = Vector2(x, CAPTION_HEIGHT + CAPTION_GAP) - if _gas_caption != null: - _gas_caption.position = Vector2(size.x - 110.0, 2.0) - _gas_caption.size = Vector2(106.0, CAPTION_HEIGHT) - _gas_caption.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT - if _gas_icon != null: - _gas_icon.position = Vector2(size.x - 34.0, 0.0) - _gas_icon.size = Vector2(32.0, 32.0) - _gas_caption.size.x = maxf(0.0, _gas_caption.size.x - 36.0) + if _header_labels.is_empty(): + return + var resource_width: float = size.x * 0.25 + var value_width: float = (size.x - resource_width) / 3.0 + var columns: Array = [0.0, resource_width, resource_width + value_width, + resource_width + value_width * 2.0] + var widths: Array = [resource_width, value_width, value_width, value_width] + for i in range(_header_labels.size()): + _header_labels[i].position = Vector2(columns[i], 0.0) + _header_labels[i].size = Vector2(widths[i], HEADER_HEIGHT) + + 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: + 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) + row.glyph.size = Vector2(row_height - 4.0, row_height - 4.0) + row.name.position = Vector2(row_height, y) + row.name.size = Vector2(resource_width - row_height, row_height) + for i in range(3): + var label: Label = row[["bank", "fleet", "local"][i]] + label.position = Vector2(columns[i + 1], y) + label.size = Vector2(widths[i + 1], row_height) + row_index += 1 + queue_redraw() func _process(_delta: float) -> void: - # Resource rows scale without inventing a new menu per resource. The old - # ore-only columns remain behind this concise, explicit summary. - _summary.text = "ORE Bank %d Fleet %d Local %d\nGAS Bank %d Fleet %d Local %d" % [GameState.rocks, GameState.fleet_cargo, GameState.building_holdings(building_id), GameState.gas, GameState.fleet_gas_cargo, GameState.building_gas_holdings(building_id)] - for number in _numbers: number.visible = false - for caption in _captions: caption.visible = false - _gas_caption.visible = false - _gas_icon.visible = false - _gas_caption.text = "GAS %d / %d / %d" % [GameState.gas, GameState.fleet_gas_cargo, GameState.building_gas_holdings(building_id)] - _numbers[0].set_amount(GameState.rocks) - _numbers[1].set_amount(GameState.fleet_cargo) - - # A building with no id is not a building: showing "0" there would claim - # the store is empty rather than that there is no store to look at. - var known: bool = building_id != 0 - _numbers[2].visible = known - _captions[2].visible = known - if known: - _numbers[2].set_amount(GameState.building_holdings(building_id)) + for resource_type in GameState.RESOURCE_TYPES: + var row: Dictionary = _rows.get(resource_type, {}) + if row.is_empty(): + continue + row.bank.text = str(GameState.banked_resource(resource_type)) + row.fleet.text = str(GameState.fleet_resource(resource_type)) + row.local.text = str(GameState.building_resource(building_id, resource_type)) \ + if building_id != 0 else "—" -# The height the bar needs for a caption line and a row of digits. The scene -# sizes the node, so this is what those offsets have to add up to; anything -# less and the rule is drawn back through the digits. static func natural_height() -> float: - return CAPTION_HEIGHT + CAPTION_GAP + DIGIT_HEIGHT + 8.0 + return 84.0 func _draw() -> void: - # One rule under the whole bar, so the header reads as a band separating - # the standing figures from whatever the panel below them does. - var rule_y: float = maxf(size.y, natural_height()) - NeonDraw.draw_glowing_line( - self, Vector2(0.0, rule_y), Vector2(size.x, rule_y), RULE_COLOR, 0.55, 1.0 - ) + NeonDraw.draw_glowing_line(self, Vector2(0.0, size.y), Vector2(size.x, size.y), + RULE_COLOR, 0.55, 1.0) diff --git a/ResourceGlyph.gd b/ResourceGlyph.gd new file mode 100644 index 0000000..aa80578 --- /dev/null +++ b/ResourceGlyph.gd @@ -0,0 +1,54 @@ +class_name ResourceGlyph +extends Control + +# 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. + +@export var resource_type: String = GameState.RESOURCE_ORE: set = set_resource_type + + +func set_resource_type(value: String) -> void: + if resource_type == value: + return + resource_type = value + queue_redraw() + + +func _ready() -> void: + mouse_filter = Control.MOUSE_FILTER_IGNORE + queue_redraw() + + +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)) diff --git a/ResourceTransferList.gd b/ResourceTransferList.gd new file mode 100644 index 0000000..9a43285 --- /dev/null +++ b/ResourceTransferList.gd @@ -0,0 +1,161 @@ +class_name ResourceTransferList +extends Control + +# Two-sided cargo control. Resources are rows and locations stay fixed: +# fleet on the left, the current building on the right. Horizontal arrows +# make the result of a press visible before the player commits to it. + +signal transfer_requested(resource_type: String, to_building: bool) + +const HEADER_HEIGHT: float = 58.0 +const FLEET_COLOR: Color = Color(1.0, 0.78, 0.25) +const LOCAL_COLOR: Color = Color(0.45, 0.9, 0.55) +const RULE_COLOR: Color = Color(0.18, 0.38, 0.52) + +var building_id: int = 0 +var _destination_heading: Label +var _rows: Dictionary = {} + + +func _ready() -> void: + var fleet_heading := _make_label("FLEET HOLD", 22, FLEET_COLOR) + fleet_heading.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + add_child(fleet_heading) + _destination_heading = _make_label("THIS BUILDING", 22, LOCAL_COLOR) + _destination_heading.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + add_child(_destination_heading) + + for resource_type in GameState.RESOURCE_TYPES: + var colour: Color = GameState.resource_color(resource_type) + var glyph := ResourceGlyph.new() + glyph.resource_type = resource_type + add_child(glyph) + var name_label := _make_label(GameState.resource_name(resource_type).to_upper(), 25, colour) + name_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + add_child(name_label) + var fleet_amount := _make_label("0", 42, FLEET_COLOR) + fleet_amount.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + add_child(fleet_amount) + var local_amount := _make_label("0", 42, LOCAL_COLOR) + local_amount.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + add_child(local_amount) + + var unload := IconButton.new() + unload.name = "%sToBuildingButton" % GameState.resource_name(resource_type) + unload.icon = IconButton.Icon.RIGHT_ARROW + unload.glow_color = colour + unload.pressed.connect(_request_transfer.bind(resource_type, true)) + add_child(unload) + var unload_label := _make_label("UNLOAD ALL →", 20, colour) + unload_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + add_child(unload_label) + + var load := IconButton.new() + load.name = "%sToFleetButton" % GameState.resource_name(resource_type) + load.icon = IconButton.Icon.LEFT_ARROW + load.glow_color = colour + load.pressed.connect(_request_transfer.bind(resource_type, false)) + add_child(load) + var load_label := _make_label("← LOAD ALL", 20, colour) + load_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + add_child(load_label) + + _rows[resource_type] = { + "glyph": glyph, "name": name_label, + "fleet_amount": fleet_amount, "local_amount": local_amount, + "unload": unload, "unload_label": unload_label, + "load": load, "load_label": load_label, + } + + resized.connect(_layout) + _layout() + refresh() + + +func _make_label(text_value: String, font_size: int, colour: Color) -> Label: + var label := Label.new() + label.text = text_value + label.mouse_filter = Control.MOUSE_FILTER_IGNORE + label.add_theme_font_size_override("font_size", font_size) + label.add_theme_color_override("font_color", colour) + label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + return label + + +func _request_transfer(resource_type: String, to_building: bool) -> void: + transfer_requested.emit(resource_type, to_building) + + +func set_destination(id: int, heading: String) -> void: + building_id = id + if _destination_heading != null: + _destination_heading.text = heading.to_upper() + refresh() + + +func refresh() -> void: + for resource_type in GameState.RESOURCE_TYPES: + var row: Dictionary = _rows.get(resource_type, {}) + if row.is_empty(): + continue + var fleet_amount: int = GameState.fleet_resource(resource_type) + var local_amount: int = GameState.building_resource(building_id, resource_type) + row.fleet_amount.text = str(fleet_amount) + row.local_amount.text = str(local_amount) + row.unload.enabled = fleet_amount > 0 + row.load.enabled = local_amount > 0 + row.unload.queue_redraw() + row.load.queue_redraw() + + +func _layout() -> void: + if _destination_heading == null: + return + var side_width: float = size.x * 0.36 + var centre_width: float = size.x - side_width * 2.0 + var right_x: float = side_width + centre_width + var fleet_heading: Label = get_child(0) + fleet_heading.position = Vector2(0.0, 0.0) + fleet_heading.size = Vector2(side_width, HEADER_HEIGHT) + _destination_heading.position = Vector2(right_x, 0.0) + _destination_heading.size = Vector2(side_width, HEADER_HEIGHT) + + 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: + var row: Dictionary = _rows[resource_type] + var y: float = HEADER_HEIGHT + float(row_index) * row_height + var glyph_size: float = minf(58.0, row_height * 0.22) + row.glyph.position = Vector2(size.x * 0.5 - glyph_size * 0.5, y + 12.0) + row.glyph.size = Vector2(glyph_size, glyph_size) + row.name.position = Vector2(side_width, y + 12.0 + glyph_size) + row.name.size = Vector2(centre_width, 38.0) + + row.fleet_amount.position = Vector2(0.0, y + 12.0) + row.fleet_amount.size = Vector2(side_width, 62.0) + row.local_amount.position = Vector2(right_x, y + 12.0) + row.local_amount.size = Vector2(side_width, 62.0) + + var button_size: float = minf(118.0, row_height * 0.42) + var button_y: float = y + 78.0 + row.unload.position = Vector2(side_width * 0.5 - button_size * 0.5, button_y) + row.unload.size = Vector2(button_size, button_size) + row.load.position = Vector2(right_x + side_width * 0.5 - button_size * 0.5, button_y) + row.load.size = Vector2(button_size, button_size) + var caption_y: float = button_y + button_size + 2.0 + row.unload_label.position = Vector2(0.0, caption_y) + row.unload_label.size = Vector2(side_width, 36.0) + row.load_label.position = Vector2(right_x, caption_y) + row.load_label.size = Vector2(side_width, 36.0) + row_index += 1 + queue_redraw() + + +func _draw() -> void: + var row_height: float = (size.y - HEADER_HEIGHT) / maxf(1.0, float(_rows.size())) + NeonDraw.draw_glowing_line(self, Vector2(0.0, HEADER_HEIGHT), Vector2(size.x, HEADER_HEIGHT), + RULE_COLOR, 0.5, 0.7) + for i in range(1, _rows.size()): + var y: float = HEADER_HEIGHT + float(i) * row_height + NeonDraw.draw_glowing_line(self, Vector2(0.0, y), Vector2(size.x, y), + RULE_COLOR, 0.4, 0.6) diff --git a/tests/_cargo_readout_preview.gd b/tests/_cargo_readout_preview.gd index 6e63b47..86361f3 100644 --- a/tests/_cargo_readout_preview.gd +++ b/tests/_cargo_readout_preview.gd @@ -1,8 +1,7 @@ extends Node2D -# Preview, not a test. The resource readouts with the fleet carrying ore, so -# the #15 banked-vs-carried split can actually be looked at - headless draws -# no pixels and cannot say whether two numbers read as two different things. +# Preview, not a test. Both banked resources and both fleet-held resources +# are populated so the complete two-column HUD can be inspected. # # bash tests/screenshot.sh res://tests/_cargo_readout_preview.tscn /tmp/hud.png 20 720x1280 @@ -11,5 +10,7 @@ func _ready() -> void: GameState.buildings.clear() GameState.extractors.clear() GameState.rocks = 128 + GameState.gas = 46 GameState.fleet_cargo = 34 + GameState.fleet_gas_cargo = 17 add_child(load("res://Main.tscn").instantiate()) diff --git a/tests/_craft_menu_preview.gd b/tests/_craft_menu_preview.gd index 760aa1e..317532c 100644 --- a/tests/_craft_menu_preview.gd +++ b/tests/_craft_menu_preview.gd @@ -13,13 +13,16 @@ func _ready() -> void: GameState.extractors.clear() GameState.transporters.clear() GameState.rocks = 180 + GameState.gas = 36 GameState.fleet_cargo = 24 + GameState.fleet_gas_cargo = 12 GameState.unlocked = ["shipyards", "logistics", "freight"] var main: Node = load("res://Main.tscn").instantiate() add_child(main) main.get_node("BuildingField").place(GameState.BUILDING_SHIP_CRAFTER, Vector2(400.0, 0.0)) var crafter_id: int = int(GameState.buildings[-1].id) GameState.deposit_to_building(crafter_id, 42) + GameState.find_building(crafter_id).gas_stock = 18 var bc: Node = main.get_node("BuildController") bc.craft_building_id = crafter_id # The shared header (#24) is the thing worth eyeballing here, so give it diff --git a/tests/_gas_surface_preview.gd b/tests/_gas_surface_preview.gd new file mode 100644 index 0000000..ba68e1c --- /dev/null +++ b/tests/_gas_surface_preview.gd @@ -0,0 +1,14 @@ +extends Node2D + + +func _ready() -> void: + GameState.planet_name = "Violet Tempest" + GameState.planet_type = "gas_giant" + GameState.surviving_ship_count = 3 + GameState.rocks = 128 + GameState.gas = 46 + GameState.fleet_cargo = 34 + GameState.fleet_gas_cargo = 17 + var surface: Node = load("res://PlanetSurface.tscn").instantiate() + surface.get_node("SurfaceController").fade_duration = 0.0 + add_child(surface) diff --git a/tests/_gas_surface_preview.tscn b/tests/_gas_surface_preview.tscn new file mode 100644 index 0000000..a32eeaa --- /dev/null +++ b/tests/_gas_surface_preview.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://tests/_gas_surface_preview.gd" id="1"] + +[node name="GasSurfacePreview" type="Node2D"] +script = ExtResource("1") diff --git a/tests/_skill_menu_preview.gd b/tests/_skill_menu_preview.gd index 907741e..96d4581 100644 --- a/tests/_skill_menu_preview.gd +++ b/tests/_skill_menu_preview.gd @@ -13,7 +13,9 @@ func _ready() -> void: GameState.extractors.clear() GameState.transporters.clear() GameState.rocks = 180 + GameState.gas = 36 GameState.fleet_cargo = 24 + GameState.fleet_gas_cargo = 12 GameState.unlocked = ["shipyards", "logistics", "freight"] var main: Node = load("res://Main.tscn").instantiate() add_child(main) diff --git a/tests/screenshot.sh b/tests/screenshot.sh index 8fa7168..f17bf9c 100644 --- a/tests/screenshot.sh +++ b/tests/screenshot.sh @@ -28,18 +28,15 @@ WAIT_FRAMES="${3:-60}" RESOLUTION="${4:-1280x720}" proot-distro login ubuntu -- bash -c " -# A killed run (or a session that ended mid-screenshot) leaves /tmp/.X99-lock -# behind, and Xvfb then refuses to claim :99 - which surfaces much later and -# far less obviously as Godot's 'X11 Display is not available'. Nothing else -# uses :99 here, so a lock with no live Xvfb behind it is always stale. -if ! pgrep -x Xvfb >/dev/null; then - rm -f /tmp/.X99-lock /tmp/.X11-unix/X99 -fi +# Display :99 is dedicated to this helper. Clear both a live process left by +# an interrupted capture and its lock/socket before claiming it again. +pkill -x Xvfb >/dev/null 2>&1 || true +rm -f /tmp/.X99-lock /tmp/.X11-unix/X99 Xvfb :99 -screen 0 ${RESOLUTION}x24 & XVFB_PID=\$! +trap 'kill \$XVFB_PID >/dev/null 2>&1 || true' EXIT sleep 2 mkdir -p /tmp/xdgrt && chmod 700 /tmp/xdgrt cd '$PROJECT_DIR' -DISPLAY=:99 XDG_RUNTIME_DIR=/tmp/xdgrt GALLIUM_DRIVER=softpipe '$GODOT_BIN' --rendering-driver opengl3 --rendering-method gl_compatibility --resolution '$RESOLUTION' -s res://tests/_screenshot_tool.gd -- '$SCENE_PATH' '$OUTPUT_PATH' '$WAIT_FRAMES' -kill \$XVFB_PID +DISPLAY=:99 XDG_RUNTIME_DIR=/tmp/xdgrt GALLIUM_DRIVER=softpipe '$GODOT_BIN' --audio-driver Dummy --rendering-driver opengl3 --rendering-method gl_compatibility --resolution '$RESOLUTION' -s res://tests/_screenshot_tool.gd -- '$SCENE_PATH' '$OUTPUT_PATH' '$WAIT_FRAMES' " diff --git a/tests/test_crafter_feedback.gd b/tests/test_crafter_feedback.gd index 2cdf4a5..e104c27 100644 --- a/tests/test_crafter_feedback.gd +++ b/tests/test_crafter_feedback.gd @@ -96,7 +96,10 @@ func _process(delta: float) -> bool: print("TEST_CRAFTER_FEEDBACK: FAIL (fleet parked on the crafter never registered)") quit(1) return true + # A crafter now has both Craft and Transfer, so the shared action + # button unfolds those choices before the craft action is selected. _tap(controller.action_button) + _tap(controller.building_action_button) if not controller.craft_menu.visible: print("TEST_CRAFTER_FEEDBACK: FAIL (the action button did not open the crafter panel)") quit(1) diff --git a/tests/test_manual_supply_route.gd b/tests/test_manual_supply_route.gd index 5ae541f..f8479d1 100644 --- a/tests/test_manual_supply_route.gd +++ b/tests/test_manual_supply_route.gd @@ -162,8 +162,11 @@ func _process(delta: float) -> bool: stage = 6 return false 6: - # Long enough for the build itself, which #23 made take time. - if elapsed < game_state.build_time(GameState.PRODUCT_SHIP) * 3.0: + # Long enough for both construction and the physical delivery trip + # from this remote crafter to the mothership hangar. + var delivery_time: float = CRAFTER_AT.distance_to(MOTHERSHIP_AT) \ + / GameState.VESSEL_DELIVERY_SPEED + if elapsed < game_state.build_time(GameState.PRODUCT_SHIP) * 3.0 + delivery_time + 0.5: return false # --- 5. the queued order completed off the fleet's own ore --- if game_state.pending_orders(crafter_id) != 0: diff --git a/tests/test_resource_transfer.gd b/tests/test_resource_transfer.gd index 2fae6a4..04fc58f 100644 --- a/tests/test_resource_transfer.gd +++ b/tests/test_resource_transfer.gd @@ -180,7 +180,7 @@ func _process(delta: float) -> bool: # The handler working while the BUTTON is inert is the exact bug # class this project keeps hitting, so one press goes the long way. var bank_at_tap: int = game_state.rocks - _tap(scene.get_node("UI/TransferMenu/ToFleetAllButton")) + _tap(scene.get_node("UI/TransferMenu/ResourceList/OreToFleetButton")) _expect(game_state.fleet_cargo == bank_at_tap, "a real tap did not load all banked ore") _expect(game_state.rocks == 0,