From e041c02959748709311caa252ca2d6f74582c033 Mon Sep 17 00:00:00 2001 From: VinPropane Date: Sun, 7 Jun 2026 12:54:57 -0400 Subject: [PATCH] NEO-122: Fix craft recipe panel scroll and row clipping. Stack recipe label above Craft button so rows fit panel width, disable horizontal scroll, reset scroll to top on populate, and bump panel height. --- client/scenes/main.tscn | 5 +- client/scripts/craft_recipe_panel.gd | 43 ++++++++++-- client/test/craft_recipe_panel_test.gd | 81 ++++++++++++++++++++++ client/test/craft_recipe_panel_test.gd.uid | 1 + docs/manual-qa/NEO-122.md | 5 +- 5 files changed, 128 insertions(+), 7 deletions(-) create mode 100644 client/test/craft_recipe_panel_test.gd create mode 100644 client/test/craft_recipe_panel_test.gd.uid diff --git a/client/scenes/main.tscn b/client/scenes/main.tscn index f9e0f44..9f7d52a 100644 --- a/client/scenes/main.tscn +++ b/client/scenes/main.tscn @@ -1357,7 +1357,7 @@ text = "Gig XP: Loading…" [node name="CraftRecipePanel" type="Control" parent="UICanvas/HudRoot/EconomyHudSection/Body" unique_id=9000011] -custom_minimum_size = Vector2(656, 320) +custom_minimum_size = Vector2(656, 360) layout_mode = 2 size_flags_horizontal = 3 script = ExtResource("19_craft_panel") @@ -1369,6 +1369,9 @@ anchor_right = 1.0 anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 +horizontal_scroll_mode = 0 +vertical_scroll_mode = 1 +follow_focus = true [node name="VBoxContainer" type="VBoxContainer" parent="UICanvas/HudRoot/EconomyHudSection/Body/CraftRecipePanel/ScrollContainer" unique_id=9000013] layout_mode = 2 diff --git a/client/scripts/craft_recipe_panel.gd b/client/scripts/craft_recipe_panel.gd index 0d10dab..b186020 100644 --- a/client/scripts/craft_recipe_panel.gd +++ b/client/scripts/craft_recipe_panel.gd @@ -5,13 +5,22 @@ extends Control signal craft_requested(recipe_id: String) const PrototypeHudTheme := preload("res://scripts/prototype_hud_theme.gd") +const HudThemeConstants := preload("res://scripts/prototype_hud_theme_constants.gd") var _item_defs_client: Node = null var _craft_buttons: Array[Button] = [] +@onready var _scroll: ScrollContainer = $ScrollContainer @onready var _vbox: VBoxContainer = $ScrollContainer/VBoxContainer +func _ready() -> void: + if is_instance_valid(_scroll): + _scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED + _scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_AUTO + resized.connect(_sync_vbox_width) + + func setup(item_defs_client: Node) -> void: _item_defs_client = item_defs_client @@ -29,21 +38,26 @@ func populate(recipes: Array) -> void: var recipe_id: String = str(recipe.get("id", "")) if recipe_id.is_empty(): continue - var row := HBoxContainer.new() + var row := VBoxContainer.new() row.size_flags_horizontal = Control.SIZE_EXPAND_FILL + row.add_theme_constant_override("separation", 4) var info := Label.new() info.size_flags_horizontal = Control.SIZE_EXPAND_FILL - info.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART info.text = _format_recipe_line(recipe) - PrototypeHudTheme.apply_to_label(info, false) + _apply_recipe_row_label(info) row.add_child(info) + var btn_row := HBoxContainer.new() + btn_row.size_flags_horizontal = Control.SIZE_EXPAND_FILL + btn_row.alignment = BoxContainer.ALIGNMENT_END var craft_btn := Button.new() craft_btn.text = "Craft" PrototypeHudTheme.apply_to_button(craft_btn, false) craft_btn.pressed.connect(_on_craft_pressed.bind(recipe_id)) - row.add_child(craft_btn) + btn_row.add_child(craft_btn) + row.add_child(btn_row) _vbox.add_child(row) _craft_buttons.append(craft_btn) + call_deferred("_reset_scroll_after_populate") func set_craft_busy(busy: bool) -> void: @@ -85,3 +99,24 @@ func _item_display_name(item_id: String) -> String: func _on_craft_pressed(recipe_id: String) -> void: craft_requested.emit(recipe_id) + + +func _apply_recipe_row_label(label: Label) -> void: + label.add_theme_font_size_override("font_size", HudThemeConstants.FONT_SIZE_SECONDARY) + label.add_theme_constant_override("outline_size", HudThemeConstants.OUTLINE_SIZE) + label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + + +func _sync_vbox_width() -> void: + if not is_instance_valid(_scroll) or not is_instance_valid(_vbox): + return + var width: float = _scroll.size.x + if width <= 0.0: + return + _vbox.custom_minimum_size.x = width + + +func _reset_scroll_after_populate() -> void: + _sync_vbox_width() + if is_instance_valid(_scroll): + _scroll.scroll_vertical = 0 diff --git a/client/test/craft_recipe_panel_test.gd b/client/test/craft_recipe_panel_test.gd new file mode 100644 index 0000000..6469219 --- /dev/null +++ b/client/test/craft_recipe_panel_test.gd @@ -0,0 +1,81 @@ +extends GdUnitTestSuite + +## NEO-122 follow-up: craft recipe panel layout + scroll reset on populate. + +const CraftRecipePanel := preload("res://scripts/craft_recipe_panel.gd") + + +static func _sample_recipes() -> Array: + return [ + { + "id": "refine_scrap_standard", + "displayName": "Refine Scrap (Standard)", + "inputs": [{"itemId": "scrap_metal_bulk", "quantity": 5}], + }, + { + "id": "refine_scrap_efficient", + "displayName": "Refine Scrap (Efficient)", + "inputs": [{"itemId": "scrap_metal_bulk", "quantity": 10}], + }, + ] + + +func _build_panel() -> Control: + var panel: Control = CraftRecipePanel.new() + panel.custom_minimum_size = Vector2(656, 360) + var scroll := ScrollContainer.new() + scroll.name = "ScrollContainer" + scroll.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + var vbox := VBoxContainer.new() + vbox.name = "VBoxContainer" + scroll.add_child(vbox) + panel.add_child(scroll) + auto_free(panel) + add_child(panel) + await get_tree().process_frame + return panel + + +func test_populate_builds_one_row_per_recipe() -> void: + # Arrange + var panel := await _build_panel() + # Act + panel.call("populate", _sample_recipes()) + await get_tree().process_frame + # Assert + var vbox: VBoxContainer = panel.get_node("ScrollContainer/VBoxContainer") + assert_that(vbox.get_child_count()).is_equal(2) + + +func test_populate_resets_scroll_to_top() -> void: + # Arrange + var panel := await _build_panel() + var scroll: ScrollContainer = panel.get_node("ScrollContainer") + panel.call("populate", _sample_recipes()) + await get_tree().process_frame + scroll.scroll_vertical = 999 + # Act + panel.call("populate", _sample_recipes()) + await get_tree().process_frame + # Assert + assert_that(scroll.scroll_vertical).is_equal(0) + + +func test_populate_emits_craft_requested_for_row() -> void: + # Arrange + var panel := await _build_panel() + var captured: Array = [] + panel.connect( + "craft_requested", func(recipe_id: String) -> void: captured.append(recipe_id) + ) + panel.call("populate", _sample_recipes()) + await get_tree().process_frame + var vbox: VBoxContainer = panel.get_node("ScrollContainer/VBoxContainer") + var first_row: VBoxContainer = vbox.get_child(0) as VBoxContainer + var btn_row: HBoxContainer = first_row.get_child(1) as HBoxContainer + var craft_btn: Button = btn_row.get_child(0) as Button + # Act + craft_btn.pressed.emit() + # Assert + assert_that(captured.size()).is_equal(1) + assert_that(str(captured[0])).is_equal("refine_scrap_standard") diff --git a/client/test/craft_recipe_panel_test.gd.uid b/client/test/craft_recipe_panel_test.gd.uid new file mode 100644 index 0000000..f0a76e0 --- /dev/null +++ b/client/test/craft_recipe_panel_test.gd.uid @@ -0,0 +1 @@ +uid://bneo122craftpanel1 diff --git a/docs/manual-qa/NEO-122.md b/docs/manual-qa/NEO-122.md index 0d8965d..0eb29af 100644 --- a/docs/manual-qa/NEO-122.md +++ b/docs/manual-qa/NEO-122.md @@ -34,8 +34,9 @@ 4. Gather scrap until gather quest completes (resource node **R**). Verify counter updates on **`QuestProgressLabel`** after each successful gather (may lag one GET). 5. Press **Shift+Q** before gather is complete — verify **`Quest accept: denied — prerequisite_incomplete`** for refine (or **`no eligible quest`** if refine is not yet eligible). 6. After gather completes, press **Shift+Q**. Verify refine quest becomes **`active`**. -7. Craft **`refine_scrap_standard`** with Economy HUD open. Verify refine quest **`completed`** after successful craft. -8. Press **Q** again on already-completed gather. Verify deny feedback on **`QuestAcceptFeedbackLabel`** without Bruno. +7. Open **Economy HUD** recipe panel — **Refine Scrap (Standard)** is the **first** row (scroll starts at top). Craft when **≥5** scrap in bag. +8. Craft **`refine_scrap_standard`** with Economy HUD open. Verify refine quest **`completed`** after successful craft. +9. Press **Q** again on already-completed gather. Verify deny feedback on **`QuestAcceptFeedbackLabel`** without Bruno. 9. Optional: stop server while Godot running, press **Q** — verify **`Quests: error — …`** or **`Quest accept: failed — …`** visible on HUD. 10. Optional: block **`GET /game/world/quest-definitions`** only while progress GET succeeds — verify **`definitions error — …`** under **`QuestProgressLabel`** with raw quest ids as fallback.