From e4df9bc41ae7169130542e8a9c3f16c6a276e72c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Wisniewski?= Date: Fri, 14 Aug 2026 13:44:10 +0200 Subject: [PATCH] Add transfer amounts and active gas vents --- GasVentField.gd | 24 ++++++++++++ IconButton.gd | 18 +++++++++ Main.tscn | 2 +- ResourceTransferList.gd | 66 +++++++++++++++++++++++++++++++-- RockField.gd | 12 ++++-- tests/_transfer_menu_preview.gd | 1 + tests/test_gas_system.gd | 17 +++++++++ tests/test_resource_transfer.gd | 19 ++++++++-- 8 files changed, 147 insertions(+), 12 deletions(-) diff --git a/GasVentField.gd b/GasVentField.gd index 7f8e9c7..9cdd338 100644 --- a/GasVentField.gd +++ b/GasVentField.gd @@ -2,12 +2,15 @@ extends Node2D @onready var fleet: Node2D = get_node("../Fleet") var vents: Array[Vector2] = [] var progress: float = 0.0 +var emission_phase: float = 0.0 func _ready() -> void: var rng := RandomNumberGenerator.new() rng.seed = hash(GameState.planet_name) for i in range(6): vents.append(Vector2(rng.randf_range(-800,800), rng.randf_range(-650,650))) queue_redraw() func _process(delta: float) -> void: + emission_phase = fmod(emission_phase + delta * 0.55, 1.0) + queue_redraw() for vent in vents: if fleet.get_centroid().distance_to(vent) < 90.0: progress += delta @@ -19,6 +22,7 @@ func _process(delta: float) -> void: func _draw() -> void: for vent in vents: _draw_gas_source(vent) + _draw_emission(vent) func _draw_gas_source(centre: Vector2) -> void: @@ -35,3 +39,23 @@ func _draw_gas_source(centre: Vector2) -> void: 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) + + +func _draw_emission(centre: Vector2) -> void: + # Small bubbles continuously rise out of every source. This remains + # animated even when the fleet is not collecting, so the vent reads as + # an active gas generator rather than a static decoration. + var core := Color(0.15, 0.03, 0.22) + for i in range(4): + var travel: float = fmod(emission_phase + float(i) * 0.25, 1.0) + var bubble_centre := centre + Vector2( + sin((travel + float(i)) * TAU) * 11.0, + -48.0 - travel * 92.0 + ) + var radius: float = lerpf(5.0, 14.0, travel) + var colour := Color(0.92, 0.68, 1.0, 1.0 - travel * 0.72) + var ring := PackedVector2Array() + for point in range(10): + var angle: float = TAU * float(point) / 10.0 + ring.append(bubble_centre + Vector2(cos(angle), sin(angle)) * radius) + NeonDraw.draw_glowing_polygon(self, ring, colour, 0.75, core, 0.45) diff --git a/IconButton.gd b/IconButton.gd index a0b7ef6..ace39c6 100644 --- a/IconButton.gd +++ b/IconButton.gd @@ -28,6 +28,7 @@ enum Icon { ACTION_MENU, GAS, LEFT_ARROW, RIGHT_ARROW, + MINUS, PLUS, } # Size the glyphs were originally drawn against. Every glyph dimension is @@ -208,6 +209,10 @@ func _draw() -> void: _draw_horizontal_arrow(center, pulse, -1.0, s) Icon.RIGHT_ARROW: _draw_horizontal_arrow(center, pulse, 1.0, s) + Icon.MINUS: + _draw_minus(center, pulse, s) + Icon.PLUS: + _draw_plus(center, pulse, s) func _draw_close(center: Vector2, pulse: float, s: float) -> void: @@ -242,6 +247,19 @@ func _draw_horizontal_arrow(center: Vector2, pulse: float, direction: float, s: 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_minus(center: Vector2, pulse: float, s: float) -> void: + var r: float = 9.0 * s + NeonDraw.draw_glowing_line(self, center + Vector2(-r, 0.0), center + Vector2(r, 0.0), + _active_color(), pulse, 0.55 * s, core_color) + + +func _draw_plus(center: Vector2, pulse: float, s: float) -> void: + _draw_minus(center, pulse, s) + var r: float = 9.0 * s + NeonDraw.draw_glowing_line(self, center + Vector2(0.0, -r), center + Vector2(0.0, r), + _active_color(), pulse, 0.55 * 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 cec2013..b489c24 100644 --- a/Main.tscn +++ b/Main.tscn @@ -1522,7 +1522,7 @@ offset_left = 24 offset_top = 1092 offset_right = -24 offset_bottom = 1150 -text = "Arrows move everything available on that side" +text = "Arrows move the selected amount from that side" horizontal_alignment = 1 theme_override_colors/font_color = Color(0.55, 0.7, 0.78, 1) theme_override_font_sizes/font_size = 20 diff --git a/ResourceTransferList.gd b/ResourceTransferList.gd index 9a43285..c66a8f9 100644 --- a/ResourceTransferList.gd +++ b/ResourceTransferList.gd @@ -5,12 +5,14 @@ extends Control # 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) +signal transfer_requested(resource_type: String, to_building: bool, amount: int) 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) +const ALL_AMOUNT: int = 1 << 30 +const AMOUNT_OPTIONS: Array[int] = [1, 5, 25, ALL_AMOUNT] var building_id: int = 0 var _destination_heading: Label @@ -46,7 +48,7 @@ func _ready() -> void: 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) + var unload_label := _make_label("UNLOAD →", 20, colour) unload_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER add_child(unload_label) @@ -56,15 +58,37 @@ func _ready() -> void: 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) + var load_label := _make_label("← LOAD", 20, colour) load_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER add_child(load_label) + var decrease := IconButton.new() + decrease.name = "%sDecreaseButton" % GameState.resource_name(resource_type) + decrease.icon = IconButton.Icon.MINUS + decrease.glow_color = colour + decrease.pressed.connect(_change_amount.bind(resource_type, -1)) + add_child(decrease) + var amount_label := _make_label("ALL", 24, colour) + amount_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + add_child(amount_label) + var increase := IconButton.new() + increase.name = "%sIncreaseButton" % GameState.resource_name(resource_type) + increase.icon = IconButton.Icon.PLUS + increase.glow_color = colour + increase.pressed.connect(_change_amount.bind(resource_type, 1)) + add_child(increase) + var amount_caption := _make_label("AMOUNT", 16, Color(0.62, 0.78, 0.86)) + amount_caption.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + add_child(amount_caption) + _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, + "decrease": decrease, "amount_label": amount_label, + "increase": increase, "amount_caption": amount_caption, + "amount_index": AMOUNT_OPTIONS.size() - 1, } resized.connect(_layout) @@ -83,7 +107,29 @@ func _make_label(text_value: String, font_size: int, colour: Color) -> Label: func _request_transfer(resource_type: String, to_building: bool) -> void: - transfer_requested.emit(resource_type, to_building) + var row: Dictionary = _rows.get(resource_type, {}) + if row.is_empty(): + return + transfer_requested.emit(resource_type, to_building, + AMOUNT_OPTIONS[int(row.amount_index)]) + + +func _change_amount(resource_type: String, direction: int) -> void: + var row: Dictionary = _rows.get(resource_type, {}) + if row.is_empty(): + return + row.amount_index = clampi(int(row.amount_index) + direction, 0, AMOUNT_OPTIONS.size() - 1) + _refresh_amount_controls(row) + + +func _refresh_amount_controls(row: Dictionary) -> void: + var index: int = int(row.amount_index) + var amount: int = AMOUNT_OPTIONS[index] + row.amount_label.text = "ALL" if amount == ALL_AMOUNT else str(amount) + row.decrease.enabled = index > 0 + row.increase.enabled = index < AMOUNT_OPTIONS.size() - 1 + row.decrease.queue_redraw() + row.increase.queue_redraw() func set_destination(id: int, heading: String) -> void: @@ -106,6 +152,7 @@ func refresh() -> void: row.load.enabled = local_amount > 0 row.unload.queue_redraw() row.load.queue_redraw() + _refresh_amount_controls(row) func _layout() -> void: @@ -147,6 +194,17 @@ func _layout() -> void: 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) + + var selector_size: float = minf(54.0, centre_width * 0.29) + var selector_y: float = y + 126.0 + row.decrease.position = Vector2(side_width + 2.0, selector_y) + row.decrease.size = Vector2(selector_size, selector_size) + row.increase.position = Vector2(right_x - selector_size - 2.0, selector_y) + row.increase.size = Vector2(selector_size, selector_size) + row.amount_label.position = Vector2(side_width + selector_size + 2.0, selector_y) + row.amount_label.size = Vector2(centre_width - selector_size * 2.0 - 4.0, selector_size) + row.amount_caption.position = Vector2(side_width, selector_y + selector_size + 2.0) + row.amount_caption.size = Vector2(centre_width, 30.0) row_index += 1 queue_redraw() diff --git a/RockField.gd b/RockField.gd index 6210722..a82389c 100644 --- a/RockField.gd +++ b/RockField.gd @@ -26,6 +26,12 @@ var rng: RandomNumberGenerator = RandomNumberGenerator.new() func _ready() -> void: + # Gas giants have their own gas vents and must never receive ore pickups. + # This check happens before the initial population pass, unlike merely + # hiding the field later from PlanetSurfaceController. + if GameState.planet_type == "gas_giant": + set_process(false) + return rng.randomize() # Populate immediately rather than waiting out the first interval, so # the surface isn't visibly empty for half a second on arrival. @@ -65,7 +71,7 @@ func _spawn_rock(centre: Vector2) -> void: var rock: Area2D = rock_scene.instantiate() rock.position = centre + Vector2(cos(angle), sin(angle)) * distance rock.build_shape(rng.randi()) - # Deferred: update_rocks() runs from _ready(), and Godot refuses a - # direct add_child() while the tree is still setting up children. - get_parent().add_child.call_deferred(rock) + # Keep pickups under this field so its visibility and lifecycle really + # own them. Deferred is still required because this can run from _ready(). + add_child.call_deferred(rock) rocks.append(rock) diff --git a/tests/_transfer_menu_preview.gd b/tests/_transfer_menu_preview.gd index b903fa6..85d1334 100644 --- a/tests/_transfer_menu_preview.gd +++ b/tests/_transfer_menu_preview.gd @@ -27,4 +27,5 @@ func _ready() -> void: bc.transfer_building_id = crafter_id main.get_node("UI/TransferMenu").visible = true bc._refresh_transfer() + bc.transfer_list._change_amount(GameState.RESOURCE_ORE, -1) bc.transfer_status.text = "Moved 25 ore into this building" diff --git a/tests/test_gas_system.gd b/tests/test_gas_system.gd index 458dbed..898e43d 100644 --- a/tests/test_gas_system.gd +++ b/tests/test_gas_system.gd @@ -33,6 +33,23 @@ func _process(delta: float) -> bool: if nearby_position.length() > WorldGen.CHUNK_SIZE * 2.0: return _fail("nearby gas giant spawned too far away: %s" % nearby_position) + # Gas surfaces have vents only: no deferred ore pickups, and the vent + # emission advances continuously so the source visibly looks active. + game_state.planet_name = "Gas Test" + game_state.planet_type = "gas_giant" + var surface: Node = load("res://PlanetSurface.tscn").instantiate() + root.add_child(surface) + var rock_field: Node = surface.get_node("RockField") + var gas_field: Node = surface.get_node("GasVentField") + if not rock_field.rocks.is_empty() or rock_field.is_processing(): + surface.free() + return _fail("gas surface spawned or continued processing ore rocks") + gas_field._process(0.5) + if gas_field.emission_phase <= 0.0: + surface.free() + return _fail("gas-source emission animation did not advance") + surface.free() + var found_giant: bool = false for x in range(-12, 13): for y in range(-12, 13): diff --git a/tests/test_resource_transfer.gd b/tests/test_resource_transfer.gd index 04fc58f..7b6a00c 100644 --- a/tests/test_resource_transfer.gd +++ b/tests/test_resource_transfer.gd @@ -180,11 +180,22 @@ 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/ResourceList/OreToFleetButton")) + var resource_list: Node = scene.get_node("UI/TransferMenu/ResourceList") + # Default is ALL. One minus selects 25, proving the amount control + # affects a real transfer rather than only changing its caption. + _tap(resource_list.get_node("OreDecreaseButton")) + _tap(resource_list.get_node("OreToFleetButton")) + _expect(game_state.fleet_cargo == mini(GameState.CARGO_LOAD_BATCH, bank_at_tap), + "selected 25 but the real tap loaded %d" % game_state.fleet_cargo) + _expect(game_state.rocks == bank_at_tap - game_state.fleet_cargo, + "the bank did not pay the selected transfer amount") + # Back to ALL and move the remainder, preserving the original + # end-to-end all-transfer coverage as well. + _tap(resource_list.get_node("OreIncreaseButton")) + _tap(resource_list.get_node("OreToFleetButton")) _expect(game_state.fleet_cargo == bank_at_tap, - "a real tap did not load all banked ore") - _expect(game_state.rocks == 0, - "the bank did not pay for the tapped withdrawal") + "switching back to ALL did not load the remainder") + _expect(game_state.rocks == 0, "ALL left ore in the bank") # --- 7. conservation ---------------------------------------- _expect(_total() == total_before,