diff --git a/client/README.md b/client/README.md index 85731ed..cd16344 100644 --- a/client/README.md +++ b/client/README.md @@ -207,7 +207,7 @@ Epic 5 Slice 1 capstone — tab-target lock, cast, defeat, and gig XP visibility **Scripts:** `ability_cast_client.gd`, `combat_targets_client.gd`, `gig_progression_client.gd` — wired from `main.gd`. -**Economy HUD:** **`GigXpLabel`** lives under **`EconomyHudSection/Body/`** (hidden when **`Economy HUD`** collapsed, NEO-75). Gig GET refreshes on boot and when cast accept carries **`targetDefeated: true`** only — not on every hit. +**Economy HUD:** **`GigXpLabel`** lives under **`EconomyHudSection/BodyScroll/Body/`** (hidden when **`Economy HUD`** collapsed, NEO-75). Gig GET refreshes on boot and when cast accept carries **`targetDefeated: true`** only — not on every hit. **Preconditions:** **Server restart** before capstone run resets in-memory dummy HP and gig XP baseline. @@ -267,7 +267,7 @@ Full capstone checklist: [`docs/manual-qa/NEO-111.md`](../docs/manual-qa/NEO-111 - **`GET /game/players/{id}/inventory`** — server-authoritative bag (**24** slots) + equipment stub (**1** slot); see [server README — Player inventory](../server/README.md#player-inventory-neo-54-store-neo-55-http). - **`GET /game/world/item-definitions`** — cached on boot for **`displayName`** labels (NEO-53). - **Scripts:** `scripts/inventory_client.gd`, `scripts/item_definitions_client.gd`; wired from `main.gd` on boot and on **`inventory_refresh`** (**I**). -- **HUD:** `UICanvas/EconomyHudSection/Body/InventoryLabel` lists **non-empty bag** rows and **equipment slot 0** (shows **`— empty`** when unoccupied). Failed GET paints **`error — …`** on the label and **`push_warning`** in Output. **`Economy HUD`** checkbox (NEO-75) collapses the economy block. +- **HUD:** `UICanvas/EconomyHudSection/BodyScroll/Body/InventoryLabel` lists **non-empty bag** rows and **equipment slot 0** (shows **`— empty`** when unoccupied). Failed GET paints **`error — …`** on the label and **`push_warning`** in Output. **`Economy HUD`** checkbox (NEO-75) collapses the economy block. - **Refresh:** press **I** (or call **`InventoryClient.request_sync_from_server()`** from `main.gd` helpers — entrypoint for NEO-73 gather/craft refresh). Full checklist: [`docs/manual-qa/NEO-72.md`](../docs/manual-qa/NEO-72.md). @@ -275,9 +275,9 @@ Full checklist: [`docs/manual-qa/NEO-72.md`](../docs/manual-qa/NEO-72.md). ## Gather feedback on interact (NEO-73) - **`POST /game/players/{id}/interact`** on **`resource_node`** kinds — server gather engine (NEO-63); no separate gather HTTP on client. -- **`GET /game/players/{id}/skill-progression`** — **`salvage`** row on **`UICanvas/EconomyHudSection/Body/SkillProgressionLabel`**; boot hydrate + refresh after successful gather. +- **`GET /game/players/{id}/skill-progression`** — **`salvage`** row on **`UICanvas/EconomyHudSection/BodyScroll/Body/SkillProgressionLabel`**; boot hydrate + refresh after successful gather. - **Scripts:** `scripts/skill_progression_client.gd`, `scripts/prototype_interactable_picker.gd`; **`interaction_request_client.gd`** emits **`interaction_result_received`**; wired from `main.gd`. -- **HUD:** `UICanvas/GatherFeedbackLabel` — success delta **`+N {displayName}`** or deny **`Gather: denied — {reasonCode}`** (including **`node_depleted`**). **`UICanvas/EconomyHudSection/Body/InventoryLabel`** refreshes automatically on successful gather (no **I**). +- **HUD:** `UICanvas/GatherFeedbackLabel` — success delta **`+N {displayName}`** or deny **`Gather: denied — {reasonCode}`** (including **`node_depleted`**). **`UICanvas/EconomyHudSection/BodyScroll/Body/InventoryLabel`** refreshes automatically on successful gather (no **I**). - **R binding:** nearest in-range **`resource_node`** by horizontal distance to catalog anchors (same radius rule as glow preview). Full checklist: [`docs/manual-qa/NEO-73.md`](../docs/manual-qa/NEO-73.md). @@ -287,7 +287,7 @@ Full checklist: [`docs/manual-qa/NEO-73.md`](../docs/manual-qa/NEO-73.md). - **`GET /game/world/recipe-definitions`** — eight prototype recipes on boot; scrollable panel inside **`EconomyHudSection`** (below **`SkillProgressionLabel`**). - **`POST /game/players/{id}/craft`** — JSON body `schemaVersion`, `recipeId`, `quantity` (default **1**); see [server README — Craft HTTP](../server/README.md). - **Scripts:** `scripts/recipe_definitions_client.gd`, `scripts/craft_client.gd`, `scripts/craft_recipe_panel.gd`; wired from `main.gd`. -- **HUD:** `UICanvas/CraftFeedbackLabel` — success **`Craft: +N {displayName}`** from POST **`outputsGranted`**; deny **`Craft: denied — {reasonCode}`**. **`UICanvas/EconomyHudSection/Body/SkillProgressionLabel`** adds **`refine`** row (boot + post-craft refresh). **`UICanvas/EconomyHudSection/Body/InventoryLabel`** refreshes on craft success only (no **I**). +- **HUD:** `UICanvas/CraftFeedbackLabel` — success **`Craft: +N {displayName}`** from POST **`outputsGranted`**; deny **`Craft: denied — {reasonCode}`**. **`UICanvas/EconomyHudSection/BodyScroll/Body/SkillProgressionLabel`** adds **`refine`** row (boot + post-craft refresh). **`UICanvas/EconomyHudSection/BodyScroll/Body/InventoryLabel`** refreshes on craft success only (no **I**). - **Panel:** per-recipe **Craft** button (qty **1**); input summary uses cached item **`displayName`** when available. Full checklist: [`docs/manual-qa/NEO-74.md`](../docs/manual-qa/NEO-74.md). @@ -307,7 +307,7 @@ Capstone proof that Epic 3 prototype gather→craft works **in Godot** without B **Material minimum:** **11** scrap from one **R** at each of four prototype nodes (+1, +2, +3, +5) → two **`refine_scrap_standard`** → one **`make_field_stim_mk0`**. -**HUD paths:** feedback labels stay on **`UICanvas`**; economy block under **`UICanvas/EconomyHudSection/Body/`** (`InventoryLabel`, `SkillProgressionLabel`, `CraftRecipePanel`). +**HUD paths:** feedback labels stay on **`UICanvas`**; economy block under **`UICanvas/EconomyHudSection/BodyScroll/Body/`** (`InventoryLabel`, `SkillProgressionLabel`, `CraftRecipePanel`). Full capstone checklist: [`docs/manual-qa/NEO-75.md`](../docs/manual-qa/NEO-75.md). diff --git a/client/scenes/main.tscn b/client/scenes/main.tscn index 9f7d52a..ea76b1a 100644 --- a/client/scenes/main.tscn +++ b/client/scenes/main.tscn @@ -1303,24 +1303,61 @@ theme_override_font_sizes/font_size = 20 autowrap_mode = 3 text = "Cooldowns: —" -[node name="EconomyHudSection" type="VBoxContainer" parent="UICanvas/HudRoot" unique_id=9000014] -layout_mode = 2 -size_flags_horizontal = 3 +[node name="EconomyHudSection" type="VBoxContainer" parent="UICanvas" unique_id=9000014] +layout_mode = 1 +anchors_preset = 3 +anchor_top = 1.0 +anchor_bottom = 1.0 +offset_left = 8.0 +offset_top = -440.0 +offset_right = 664.0 +offset_bottom = -8.0 +grow_horizontal = 2 +grow_vertical = 0 script = ExtResource("20_economy_hud") -[node name="HeaderRow" type="HBoxContainer" parent="UICanvas/HudRoot/EconomyHudSection" unique_id=9000015] +[node name="HeaderRow" type="HBoxContainer" parent="UICanvas/EconomyHudSection" unique_id=9000015] layout_mode = 2 -[node name="ToggleButton" type="CheckButton" parent="UICanvas/HudRoot/EconomyHudSection/HeaderRow" unique_id=9000016] +[node name="ToggleButton" type="CheckButton" parent="UICanvas/EconomyHudSection/HeaderRow" unique_id=9000016] layout_mode = 2 theme_override_font_sizes/font_size = 20 text = "Economy HUD" -[node name="Body" type="VBoxContainer" parent="UICanvas/HudRoot/EconomyHudSection" unique_id=9000017] +[node name="BodyScroll" type="ScrollContainer" parent="UICanvas/EconomyHudSection" unique_id=9000027] layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +custom_minimum_size = Vector2(656, 380) +horizontal_scroll_mode = 0 +vertical_scroll_mode = 1 +follow_focus = true + +[node name="Body" type="VBoxContainer" parent="UICanvas/EconomyHudSection/BodyScroll" unique_id=9000017] +layout_mode = 2 +size_flags_horizontal = 3 theme_override_constants/separation = 8 -[node name="InventoryLabel" type="Label" parent="UICanvas/HudRoot/EconomyHudSection/Body" unique_id=9000007] +[node name="RecipesHeaderLabel" type="Label" parent="UICanvas/EconomyHudSection/BodyScroll/Body" unique_id=9000028] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_colors/font_color = Color(0.9, 0.86, 0.98, 1) +theme_override_colors/font_outline_color = Color(0.08, 0.08, 0.1, 1) +theme_override_constants/outline_size = 8 +theme_override_font_sizes/font_size = 20 +text = "Recipes:" + +[node name="CraftRecipePanel" type="VBoxContainer" parent="UICanvas/EconomyHudSection/BodyScroll/Body" unique_id=9000011] +layout_mode = 2 +size_flags_horizontal = 3 +script = ExtResource("19_craft_panel") + +[node name="RecipeRows" type="VBoxContainer" parent="UICanvas/EconomyHudSection/BodyScroll/Body/CraftRecipePanel" unique_id=9000013] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_constants/separation = 8 + +[node name="InventoryLabel" type="Label" parent="UICanvas/EconomyHudSection/BodyScroll/Body" unique_id=9000007] custom_minimum_size = Vector2(656, 0) layout_mode = 2 size_flags_horizontal = 3 @@ -1332,7 +1369,7 @@ autowrap_mode = 3 text = "Inventory: (refresh: I) Loading…" -[node name="SkillProgressionLabel" type="Label" parent="UICanvas/HudRoot/EconomyHudSection/Body" unique_id=9000009] +[node name="SkillProgressionLabel" type="Label" parent="UICanvas/EconomyHudSection/BodyScroll/Body" unique_id=9000009] custom_minimum_size = Vector2(656, 0) layout_mode = 2 size_flags_horizontal = 3 @@ -1344,7 +1381,7 @@ autowrap_mode = 3 text = "Skills: Loading…" -[node name="GigXpLabel" type="Label" parent="UICanvas/HudRoot/EconomyHudSection/Body" unique_id=9000018] +[node name="GigXpLabel" type="Label" parent="UICanvas/EconomyHudSection/BodyScroll/Body" unique_id=9000018] custom_minimum_size = Vector2(656, 0) layout_mode = 2 size_flags_horizontal = 3 @@ -1355,25 +1392,3 @@ theme_override_font_sizes/font_size = 20 autowrap_mode = 3 text = "Gig XP: Loading…" - -[node name="CraftRecipePanel" type="Control" parent="UICanvas/HudRoot/EconomyHudSection/Body" unique_id=9000011] -custom_minimum_size = Vector2(656, 360) -layout_mode = 2 -size_flags_horizontal = 3 -script = ExtResource("19_craft_panel") - -[node name="ScrollContainer" type="ScrollContainer" parent="UICanvas/HudRoot/EconomyHudSection/Body/CraftRecipePanel" unique_id=9000012] -layout_mode = 1 -anchors_preset = 15 -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 -size_flags_horizontal = 3 -theme_override_constants/separation = 8 diff --git a/client/scripts/craft_recipe_panel.gd b/client/scripts/craft_recipe_panel.gd index b186020..3351c60 100644 --- a/client/scripts/craft_recipe_panel.gd +++ b/client/scripts/craft_recipe_panel.gd @@ -1,6 +1,7 @@ -extends Control +extends VBoxContainer -## NEO-74: scrollable prototype recipe list with per-row Craft buttons. +## NEO-74: prototype recipe list with per-row Craft buttons. +## NEO-122: rows live in economy BodyScroll (no nested scroll). signal craft_requested(recipe_id: String) @@ -10,15 +11,7 @@ const HudThemeConstants := preload("res://scripts/prototype_hud_theme_constants. 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) +@onready var _rows: VBoxContainer = $RecipeRows func setup(item_defs_client: Node) -> void: @@ -26,9 +19,9 @@ func setup(item_defs_client: Node) -> void: func populate(recipes: Array) -> void: - if not is_instance_valid(_vbox): + if not is_instance_valid(_rows): return - for child in _vbox.get_children(): + for child in _rows.get_children(): child.queue_free() _craft_buttons.clear() for row_variant in recipes: @@ -55,9 +48,9 @@ func populate(recipes: Array) -> void: craft_btn.pressed.connect(_on_craft_pressed.bind(recipe_id)) btn_row.add_child(craft_btn) row.add_child(btn_row) - _vbox.add_child(row) + _rows.add_child(row) _craft_buttons.append(craft_btn) - call_deferred("_reset_scroll_after_populate") + call_deferred("_scroll_economy_body_to_recipes") func set_craft_busy(busy: bool) -> void: @@ -107,16 +100,10 @@ func _apply_recipe_row_label(label: Label) -> void: 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 +func _scroll_economy_body_to_recipes() -> void: + var node: Node = get_parent() + while node != null: + if node.has_method("scroll_body_to_recipes"): + node.call("scroll_body_to_recipes") + return + node = node.get_parent() diff --git a/client/scripts/main.gd b/client/scripts/main.gd index 645a1b7..a1fba89 100644 --- a/client/scripts/main.gd +++ b/client/scripts/main.gd @@ -141,14 +141,14 @@ var _dev_pulse_bootstrap_attempted: bool = false @onready var _npc_state_label: Label = $UICanvas/HudRoot/NpcStateLabel @onready var _telegraph_label: Label = $UICanvas/HudRoot/TelegraphLabel @onready var _cooldown_slots_label: Label = $UICanvas/HudRoot/CooldownSlotsLabel -@onready var _economy_hud_section: VBoxContainer = $UICanvas/HudRoot/EconomyHudSection -@onready var _inventory_label: Label = _economy_hud_section.get_node("Body/InventoryLabel") +@onready var _economy_hud_section: VBoxContainer = $UICanvas/EconomyHudSection +@onready var _inventory_label: Label = _economy_hud_section.get_node("BodyScroll/Body/InventoryLabel") @onready var _gather_feedback_label: Label = $UICanvas/HudRoot/GatherFeedbackLabel @onready var _craft_feedback_label: Label = $UICanvas/HudRoot/CraftFeedbackLabel @onready -var _skill_progression_label: Label = _economy_hud_section.get_node("Body/SkillProgressionLabel") -@onready var _gig_xp_label: Label = _economy_hud_section.get_node("Body/GigXpLabel") -@onready var _craft_recipe_panel: Node = _economy_hud_section.get_node("Body/CraftRecipePanel") +var _skill_progression_label: Label = _economy_hud_section.get_node("BodyScroll/Body/SkillProgressionLabel") +@onready var _gig_xp_label: Label = _economy_hud_section.get_node("BodyScroll/Body/GigXpLabel") +@onready var _craft_recipe_panel: Node = _economy_hud_section.get_node("BodyScroll/Body/CraftRecipePanel") @onready var _inventory_client: Node = $InventoryClient @onready var _item_defs_client: Node = $ItemDefinitionsClient @onready var _skill_progression_client: Node = $SkillProgressionClient @@ -309,6 +309,8 @@ func _apply_prototype_hud_theme() -> void: PrototypeHudTheme.apply_to_label(_move_reject_label, true) if is_instance_valid(_hud_root): PrototypeHudTheme.apply_to_hud_root(_hud_root) + if is_instance_valid(_economy_hud_section): + PrototypeHudTheme.apply_to_economy_section(_economy_hud_section) func _on_authoritative_ack_for_hud(world: Vector3) -> void: diff --git a/client/scripts/prototype_economy_hud_section.gd b/client/scripts/prototype_economy_hud_section.gd index ec35e92..3e3e5bc 100644 --- a/client/scripts/prototype_economy_hud_section.gd +++ b/client/scripts/prototype_economy_hud_section.gd @@ -1,25 +1,70 @@ extends VBoxContainer ## NEO-75: collapsible economy HUD block (inventory, skills, craft panel). +## NEO-122: pinned to viewport bottom-left so recipes stay on-screen. + +const MAX_VIEWPORT_HEIGHT_FRACTION := 0.48 +const MIN_BODY_SCROLL_HEIGHT := 180.0 +const BOTTOM_MARGIN := 8.0 +const SIDE_MARGIN := 8.0 +const PANEL_WIDTH := 656.0 @onready var _toggle: CheckButton = $HeaderRow/ToggleButton -@onready var _body: VBoxContainer = $Body +@onready var _body_scroll: ScrollContainer = $BodyScroll +@onready var _recipes_header: Label = $BodyScroll/Body/RecipesHeaderLabel func _ready() -> void: _toggle.button_pressed = true - _body.visible = true + _body_scroll.visible = true _toggle.toggled.connect(_on_toggle_toggled) + get_viewport().size_changed.connect(_apply_viewport_layout) + call_deferred("_apply_viewport_layout") func _on_toggle_toggled(pressed: bool) -> void: - _body.visible = pressed + _body_scroll.visible = pressed + call_deferred("_apply_viewport_layout") func set_expanded(expanded: bool) -> void: _toggle.button_pressed = expanded - _body.visible = expanded + _body_scroll.visible = expanded + call_deferred("_apply_viewport_layout") func is_body_visible() -> bool: - return _body.visible + return _body_scroll.visible + + +func scroll_body_to_recipes() -> void: + if not is_instance_valid(_body_scroll) or not is_instance_valid(_recipes_header): + return + _body_scroll.scroll_vertical = int(_recipes_header.position.y) + + +func _apply_viewport_layout() -> void: + if not is_instance_valid(_body_scroll): + return + var vp_height: float = get_viewport().get_visible_rect().size.y + var header_height: float = _measure_header_height() + var body_height: float = max( + MIN_BODY_SCROLL_HEIGHT, vp_height * MAX_VIEWPORT_HEIGHT_FRACTION - header_height + ) + _body_scroll.custom_minimum_size = Vector2(PANEL_WIDTH, body_height) + var panel_height: float = header_height + if _body_scroll.visible: + panel_height += body_height + offset_left = SIDE_MARGIN + offset_right = SIDE_MARGIN + PANEL_WIDTH + offset_bottom = -BOTTOM_MARGIN + offset_top = -panel_height - BOTTOM_MARGIN + + +func _measure_header_height() -> float: + if not is_instance_valid(_toggle): + return 32.0 + var header_row: Node = _toggle.get_parent() + if header_row is Control: + return max(32.0, (header_row as Control).size.y) + return 32.0 diff --git a/client/scripts/prototype_hud_theme.gd b/client/scripts/prototype_hud_theme.gd index c5c3fd3..525e791 100644 --- a/client/scripts/prototype_hud_theme.gd +++ b/client/scripts/prototype_hud_theme.gd @@ -30,8 +30,6 @@ static func apply_to_hud_root(root: VBoxContainer) -> void: if child is Label: var label: Label = child as Label apply_to_label(label, not Constants.SECONDARY_LABEL_NAMES.has(label.name)) - elif child.name == "EconomyHudSection": - apply_to_economy_section(child) static func apply_to_economy_section(section: Node) -> void: diff --git a/client/test/craft_recipe_panel_test.gd b/client/test/craft_recipe_panel_test.gd index 6469219..45f32c0 100644 --- a/client/test/craft_recipe_panel_test.gd +++ b/client/test/craft_recipe_panel_test.gd @@ -1,8 +1,9 @@ extends GdUnitTestSuite -## NEO-122 follow-up: craft recipe panel layout + scroll reset on populate. +## NEO-122 follow-up: craft recipe panel rows + economy body scroll reset. const CraftRecipePanel := preload("res://scripts/craft_recipe_panel.gd") +const EconomyHudSection := preload("res://scripts/prototype_economy_hud_section.gd") static func _sample_recipes() -> Array: @@ -22,20 +23,44 @@ static func _sample_recipes() -> Array: 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) + var rows := VBoxContainer.new() + rows.name = "RecipeRows" + panel.add_child(rows) auto_free(panel) add_child(panel) await get_tree().process_frame return panel +func _build_panel_in_economy_section() -> Dictionary: + var section: VBoxContainer = EconomyHudSection.new() + var header := HBoxContainer.new() + header.name = "HeaderRow" + var toggle := CheckButton.new() + toggle.name = "ToggleButton" + header.add_child(toggle) + var body_scroll := ScrollContainer.new() + body_scroll.name = "BodyScroll" + var body := VBoxContainer.new() + body.name = "Body" + var recipes_header := Label.new() + recipes_header.name = "RecipesHeaderLabel" + body.add_child(recipes_header) + var panel: Control = CraftRecipePanel.new() + panel.name = "CraftRecipePanel" + var rows := VBoxContainer.new() + rows.name = "RecipeRows" + panel.add_child(rows) + body.add_child(panel) + body_scroll.add_child(body) + section.add_child(header) + section.add_child(body_scroll) + auto_free(section) + add_child(section) + await get_tree().process_frame + return {"section": section, "panel": panel, "body_scroll": body_scroll} + + func test_populate_builds_one_row_per_recipe() -> void: # Arrange var panel := await _build_panel() @@ -43,22 +68,22 @@ func test_populate_builds_one_row_per_recipe() -> void: 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) + var rows: VBoxContainer = panel.get_node("RecipeRows") + assert_that(rows.get_child_count()).is_equal(2) -func test_populate_resets_scroll_to_top() -> void: +func test_populate_scrolls_economy_body_to_recipes() -> 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 + var built: Dictionary = await _build_panel_in_economy_section() + var panel: Control = built["panel"] + var body_scroll: ScrollContainer = built["body_scroll"] + body_scroll.scroll_vertical = 999 # Act panel.call("populate", _sample_recipes()) await get_tree().process_frame + await get_tree().process_frame # Assert - assert_that(scroll.scroll_vertical).is_equal(0) + assert_that(body_scroll.scroll_vertical).is_equal(0) func test_populate_emits_craft_requested_for_row() -> void: @@ -70,8 +95,8 @@ func test_populate_emits_craft_requested_for_row() -> void: ) 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 rows: VBoxContainer = panel.get_node("RecipeRows") + var first_row: VBoxContainer = rows.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 diff --git a/client/test/prototype_economy_hud_section_test.gd b/client/test/prototype_economy_hud_section_test.gd index 46ed2d5..9ad55fc 100644 --- a/client/test/prototype_economy_hud_section_test.gd +++ b/client/test/prototype_economy_hud_section_test.gd @@ -1,7 +1,7 @@ extends GdUnitTestSuite -## NEO-75: economy HUD collapse toggle shows/hides body container. -## Covers economy HUD node paths via EconomyHudSection.get_node in main.gd. +## NEO-75: economy HUD collapse toggle shows/hides body scroll container. +## NEO-122: BodyScroll/Body paths match main.tscn. const EconomyHudSection := preload("res://scripts/prototype_economy_hud_section.gd") @@ -14,13 +14,16 @@ func _build_section() -> Node: toggle.name = "ToggleButton" toggle.text = "Economy HUD" header.add_child(toggle) + var body_scroll := ScrollContainer.new() + body_scroll.name = "BodyScroll" var body := VBoxContainer.new() body.name = "Body" var inventory := Label.new() inventory.name = "InventoryLabel" body.add_child(inventory) + body_scroll.add_child(body) section.add_child(header) - section.add_child(body) + section.add_child(body_scroll) return section