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.
pull/161/head
VinPropane 2026-06-07 12:54:57 -04:00
parent b36fe6d863
commit e041c02959
5 changed files with 128 additions and 7 deletions

View File

@ -1357,7 +1357,7 @@ text = "Gig XP:
Loading…" Loading…"
[node name="CraftRecipePanel" type="Control" parent="UICanvas/HudRoot/EconomyHudSection/Body" unique_id=9000011] [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 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
script = ExtResource("19_craft_panel") script = ExtResource("19_craft_panel")
@ -1369,6 +1369,9 @@ anchor_right = 1.0
anchor_bottom = 1.0 anchor_bottom = 1.0
grow_horizontal = 2 grow_horizontal = 2
grow_vertical = 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] [node name="VBoxContainer" type="VBoxContainer" parent="UICanvas/HudRoot/EconomyHudSection/Body/CraftRecipePanel/ScrollContainer" unique_id=9000013]
layout_mode = 2 layout_mode = 2

View File

@ -5,13 +5,22 @@ extends Control
signal craft_requested(recipe_id: String) signal craft_requested(recipe_id: String)
const PrototypeHudTheme := preload("res://scripts/prototype_hud_theme.gd") 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 _item_defs_client: Node = null
var _craft_buttons: Array[Button] = [] var _craft_buttons: Array[Button] = []
@onready var _scroll: ScrollContainer = $ScrollContainer
@onready var _vbox: VBoxContainer = $ScrollContainer/VBoxContainer @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: func setup(item_defs_client: Node) -> void:
_item_defs_client = item_defs_client _item_defs_client = item_defs_client
@ -29,21 +38,26 @@ func populate(recipes: Array) -> void:
var recipe_id: String = str(recipe.get("id", "")) var recipe_id: String = str(recipe.get("id", ""))
if recipe_id.is_empty(): if recipe_id.is_empty():
continue continue
var row := HBoxContainer.new() var row := VBoxContainer.new()
row.size_flags_horizontal = Control.SIZE_EXPAND_FILL row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
row.add_theme_constant_override("separation", 4)
var info := Label.new() var info := Label.new()
info.size_flags_horizontal = Control.SIZE_EXPAND_FILL info.size_flags_horizontal = Control.SIZE_EXPAND_FILL
info.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
info.text = _format_recipe_line(recipe) info.text = _format_recipe_line(recipe)
PrototypeHudTheme.apply_to_label(info, false) _apply_recipe_row_label(info)
row.add_child(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() var craft_btn := Button.new()
craft_btn.text = "Craft" craft_btn.text = "Craft"
PrototypeHudTheme.apply_to_button(craft_btn, false) PrototypeHudTheme.apply_to_button(craft_btn, false)
craft_btn.pressed.connect(_on_craft_pressed.bind(recipe_id)) 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) _vbox.add_child(row)
_craft_buttons.append(craft_btn) _craft_buttons.append(craft_btn)
call_deferred("_reset_scroll_after_populate")
func set_craft_busy(busy: bool) -> void: 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: func _on_craft_pressed(recipe_id: String) -> void:
craft_requested.emit(recipe_id) 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

View File

@ -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")

View File

@ -0,0 +1 @@
uid://bneo122craftpanel1

View File

@ -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). 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). 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`**. 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. 7. Open **Economy HUD** recipe panel — **Refine Scrap (Standard)** is the **first** row (scroll starts at top). Craft when **≥5** scrap in bag.
8. Press **Q** again on already-completed gather. Verify deny feedback on **`QuestAcceptFeedbackLabel`** without Bruno. 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. 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. 10. Optional: block **`GET /game/world/quest-definitions`** only while progress GET succeeds — verify **`definitions error — …`** under **`QuestProgressLabel`** with raw quest ids as fallback.