NEO-122: Pin economy HUD to viewport so recipes stay visible.
Reparent EconomyHudSection out of HudRoot, clamp its height to the viewport, put recipes first in a single BodyScroll, and update tests.pull/161/head
parent
e041c02959
commit
d1fa08b8c4
|
|
@ -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`.
|
**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.
|
**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/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).
|
- **`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**).
|
- **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).
|
- **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).
|
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)
|
## 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.
|
- **`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`.
|
- **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).
|
- **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).
|
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`**).
|
- **`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).
|
- **`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`.
|
- **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.
|
- **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).
|
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`**.
|
**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).
|
Full capstone checklist: [`docs/manual-qa/NEO-75.md`](../docs/manual-qa/NEO-75.md).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1303,24 +1303,61 @@ theme_override_font_sizes/font_size = 20
|
||||||
autowrap_mode = 3
|
autowrap_mode = 3
|
||||||
text = "Cooldowns: —"
|
text = "Cooldowns: —"
|
||||||
|
|
||||||
[node name="EconomyHudSection" type="VBoxContainer" parent="UICanvas/HudRoot" unique_id=9000014]
|
[node name="EconomyHudSection" type="VBoxContainer" parent="UICanvas" unique_id=9000014]
|
||||||
layout_mode = 2
|
layout_mode = 1
|
||||||
size_flags_horizontal = 3
|
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")
|
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
|
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
|
layout_mode = 2
|
||||||
theme_override_font_sizes/font_size = 20
|
theme_override_font_sizes/font_size = 20
|
||||||
text = "Economy HUD"
|
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
|
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
|
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)
|
custom_minimum_size = Vector2(656, 0)
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
size_flags_horizontal = 3
|
size_flags_horizontal = 3
|
||||||
|
|
@ -1332,7 +1369,7 @@ autowrap_mode = 3
|
||||||
text = "Inventory: (refresh: I)
|
text = "Inventory: (refresh: I)
|
||||||
Loading…"
|
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)
|
custom_minimum_size = Vector2(656, 0)
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
size_flags_horizontal = 3
|
size_flags_horizontal = 3
|
||||||
|
|
@ -1344,7 +1381,7 @@ autowrap_mode = 3
|
||||||
text = "Skills:
|
text = "Skills:
|
||||||
Loading…"
|
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)
|
custom_minimum_size = Vector2(656, 0)
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
size_flags_horizontal = 3
|
size_flags_horizontal = 3
|
||||||
|
|
@ -1355,25 +1392,3 @@ theme_override_font_sizes/font_size = 20
|
||||||
autowrap_mode = 3
|
autowrap_mode = 3
|
||||||
text = "Gig XP:
|
text = "Gig XP:
|
||||||
Loading…"
|
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
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
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 _item_defs_client: Node = null
|
||||||
var _craft_buttons: Array[Button] = []
|
var _craft_buttons: Array[Button] = []
|
||||||
|
|
||||||
@onready var _scroll: ScrollContainer = $ScrollContainer
|
@onready var _rows: VBoxContainer = $RecipeRows
|
||||||
@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:
|
||||||
|
|
@ -26,9 +19,9 @@ func setup(item_defs_client: Node) -> void:
|
||||||
|
|
||||||
|
|
||||||
func populate(recipes: Array) -> void:
|
func populate(recipes: Array) -> void:
|
||||||
if not is_instance_valid(_vbox):
|
if not is_instance_valid(_rows):
|
||||||
return
|
return
|
||||||
for child in _vbox.get_children():
|
for child in _rows.get_children():
|
||||||
child.queue_free()
|
child.queue_free()
|
||||||
_craft_buttons.clear()
|
_craft_buttons.clear()
|
||||||
for row_variant in recipes:
|
for row_variant in recipes:
|
||||||
|
|
@ -55,9 +48,9 @@ func populate(recipes: Array) -> void:
|
||||||
craft_btn.pressed.connect(_on_craft_pressed.bind(recipe_id))
|
craft_btn.pressed.connect(_on_craft_pressed.bind(recipe_id))
|
||||||
btn_row.add_child(craft_btn)
|
btn_row.add_child(craft_btn)
|
||||||
row.add_child(btn_row)
|
row.add_child(btn_row)
|
||||||
_vbox.add_child(row)
|
_rows.add_child(row)
|
||||||
_craft_buttons.append(craft_btn)
|
_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:
|
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
|
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
|
||||||
|
|
||||||
func _sync_vbox_width() -> void:
|
func _scroll_economy_body_to_recipes() -> void:
|
||||||
if not is_instance_valid(_scroll) or not is_instance_valid(_vbox):
|
var node: Node = get_parent()
|
||||||
return
|
while node != null:
|
||||||
var width: float = _scroll.size.x
|
if node.has_method("scroll_body_to_recipes"):
|
||||||
if width <= 0.0:
|
node.call("scroll_body_to_recipes")
|
||||||
return
|
return
|
||||||
_vbox.custom_minimum_size.x = width
|
node = node.get_parent()
|
||||||
|
|
||||||
|
|
||||||
func _reset_scroll_after_populate() -> void:
|
|
||||||
_sync_vbox_width()
|
|
||||||
if is_instance_valid(_scroll):
|
|
||||||
_scroll.scroll_vertical = 0
|
|
||||||
|
|
|
||||||
|
|
@ -141,14 +141,14 @@ var _dev_pulse_bootstrap_attempted: bool = false
|
||||||
@onready var _npc_state_label: Label = $UICanvas/HudRoot/NpcStateLabel
|
@onready var _npc_state_label: Label = $UICanvas/HudRoot/NpcStateLabel
|
||||||
@onready var _telegraph_label: Label = $UICanvas/HudRoot/TelegraphLabel
|
@onready var _telegraph_label: Label = $UICanvas/HudRoot/TelegraphLabel
|
||||||
@onready var _cooldown_slots_label: Label = $UICanvas/HudRoot/CooldownSlotsLabel
|
@onready var _cooldown_slots_label: Label = $UICanvas/HudRoot/CooldownSlotsLabel
|
||||||
@onready var _economy_hud_section: VBoxContainer = $UICanvas/HudRoot/EconomyHudSection
|
@onready var _economy_hud_section: VBoxContainer = $UICanvas/EconomyHudSection
|
||||||
@onready var _inventory_label: Label = _economy_hud_section.get_node("Body/InventoryLabel")
|
@onready var _inventory_label: Label = _economy_hud_section.get_node("BodyScroll/Body/InventoryLabel")
|
||||||
@onready var _gather_feedback_label: Label = $UICanvas/HudRoot/GatherFeedbackLabel
|
@onready var _gather_feedback_label: Label = $UICanvas/HudRoot/GatherFeedbackLabel
|
||||||
@onready var _craft_feedback_label: Label = $UICanvas/HudRoot/CraftFeedbackLabel
|
@onready var _craft_feedback_label: Label = $UICanvas/HudRoot/CraftFeedbackLabel
|
||||||
@onready
|
@onready
|
||||||
var _skill_progression_label: Label = _economy_hud_section.get_node("Body/SkillProgressionLabel")
|
var _skill_progression_label: Label = _economy_hud_section.get_node("BodyScroll/Body/SkillProgressionLabel")
|
||||||
@onready var _gig_xp_label: Label = _economy_hud_section.get_node("Body/GigXpLabel")
|
@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("Body/CraftRecipePanel")
|
@onready var _craft_recipe_panel: Node = _economy_hud_section.get_node("BodyScroll/Body/CraftRecipePanel")
|
||||||
@onready var _inventory_client: Node = $InventoryClient
|
@onready var _inventory_client: Node = $InventoryClient
|
||||||
@onready var _item_defs_client: Node = $ItemDefinitionsClient
|
@onready var _item_defs_client: Node = $ItemDefinitionsClient
|
||||||
@onready var _skill_progression_client: Node = $SkillProgressionClient
|
@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)
|
PrototypeHudTheme.apply_to_label(_move_reject_label, true)
|
||||||
if is_instance_valid(_hud_root):
|
if is_instance_valid(_hud_root):
|
||||||
PrototypeHudTheme.apply_to_hud_root(_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:
|
func _on_authoritative_ack_for_hud(world: Vector3) -> void:
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,70 @@
|
||||||
extends VBoxContainer
|
extends VBoxContainer
|
||||||
|
|
||||||
## NEO-75: collapsible economy HUD block (inventory, skills, craft panel).
|
## 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 _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:
|
func _ready() -> void:
|
||||||
_toggle.button_pressed = true
|
_toggle.button_pressed = true
|
||||||
_body.visible = true
|
_body_scroll.visible = true
|
||||||
_toggle.toggled.connect(_on_toggle_toggled)
|
_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:
|
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:
|
func set_expanded(expanded: bool) -> void:
|
||||||
_toggle.button_pressed = expanded
|
_toggle.button_pressed = expanded
|
||||||
_body.visible = expanded
|
_body_scroll.visible = expanded
|
||||||
|
call_deferred("_apply_viewport_layout")
|
||||||
|
|
||||||
|
|
||||||
func is_body_visible() -> bool:
|
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
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,6 @@ static func apply_to_hud_root(root: VBoxContainer) -> void:
|
||||||
if child is Label:
|
if child is Label:
|
||||||
var label: Label = child as Label
|
var label: Label = child as Label
|
||||||
apply_to_label(label, not Constants.SECONDARY_LABEL_NAMES.has(label.name))
|
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:
|
static func apply_to_economy_section(section: Node) -> void:
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
extends GdUnitTestSuite
|
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 CraftRecipePanel := preload("res://scripts/craft_recipe_panel.gd")
|
||||||
|
const EconomyHudSection := preload("res://scripts/prototype_economy_hud_section.gd")
|
||||||
|
|
||||||
|
|
||||||
static func _sample_recipes() -> Array:
|
static func _sample_recipes() -> Array:
|
||||||
|
|
@ -22,20 +23,44 @@ static func _sample_recipes() -> Array:
|
||||||
|
|
||||||
func _build_panel() -> Control:
|
func _build_panel() -> Control:
|
||||||
var panel: Control = CraftRecipePanel.new()
|
var panel: Control = CraftRecipePanel.new()
|
||||||
panel.custom_minimum_size = Vector2(656, 360)
|
var rows := VBoxContainer.new()
|
||||||
var scroll := ScrollContainer.new()
|
rows.name = "RecipeRows"
|
||||||
scroll.name = "ScrollContainer"
|
panel.add_child(rows)
|
||||||
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)
|
auto_free(panel)
|
||||||
add_child(panel)
|
add_child(panel)
|
||||||
await get_tree().process_frame
|
await get_tree().process_frame
|
||||||
return panel
|
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:
|
func test_populate_builds_one_row_per_recipe() -> void:
|
||||||
# Arrange
|
# Arrange
|
||||||
var panel := await _build_panel()
|
var panel := await _build_panel()
|
||||||
|
|
@ -43,22 +68,22 @@ func test_populate_builds_one_row_per_recipe() -> void:
|
||||||
panel.call("populate", _sample_recipes())
|
panel.call("populate", _sample_recipes())
|
||||||
await get_tree().process_frame
|
await get_tree().process_frame
|
||||||
# Assert
|
# Assert
|
||||||
var vbox: VBoxContainer = panel.get_node("ScrollContainer/VBoxContainer")
|
var rows: VBoxContainer = panel.get_node("RecipeRows")
|
||||||
assert_that(vbox.get_child_count()).is_equal(2)
|
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
|
# Arrange
|
||||||
var panel := await _build_panel()
|
var built: Dictionary = await _build_panel_in_economy_section()
|
||||||
var scroll: ScrollContainer = panel.get_node("ScrollContainer")
|
var panel: Control = built["panel"]
|
||||||
panel.call("populate", _sample_recipes())
|
var body_scroll: ScrollContainer = built["body_scroll"]
|
||||||
await get_tree().process_frame
|
body_scroll.scroll_vertical = 999
|
||||||
scroll.scroll_vertical = 999
|
|
||||||
# Act
|
# Act
|
||||||
panel.call("populate", _sample_recipes())
|
panel.call("populate", _sample_recipes())
|
||||||
await get_tree().process_frame
|
await get_tree().process_frame
|
||||||
|
await get_tree().process_frame
|
||||||
# Assert
|
# 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:
|
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())
|
panel.call("populate", _sample_recipes())
|
||||||
await get_tree().process_frame
|
await get_tree().process_frame
|
||||||
var vbox: VBoxContainer = panel.get_node("ScrollContainer/VBoxContainer")
|
var rows: VBoxContainer = panel.get_node("RecipeRows")
|
||||||
var first_row: VBoxContainer = vbox.get_child(0) as VBoxContainer
|
var first_row: VBoxContainer = rows.get_child(0) as VBoxContainer
|
||||||
var btn_row: HBoxContainer = first_row.get_child(1) as HBoxContainer
|
var btn_row: HBoxContainer = first_row.get_child(1) as HBoxContainer
|
||||||
var craft_btn: Button = btn_row.get_child(0) as Button
|
var craft_btn: Button = btn_row.get_child(0) as Button
|
||||||
# Act
|
# Act
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
extends GdUnitTestSuite
|
extends GdUnitTestSuite
|
||||||
|
|
||||||
## NEO-75: economy HUD collapse toggle shows/hides body container.
|
## NEO-75: economy HUD collapse toggle shows/hides body scroll container.
|
||||||
## Covers economy HUD node paths via EconomyHudSection.get_node in main.gd.
|
## NEO-122: BodyScroll/Body paths match main.tscn.
|
||||||
|
|
||||||
const EconomyHudSection := preload("res://scripts/prototype_economy_hud_section.gd")
|
const EconomyHudSection := preload("res://scripts/prototype_economy_hud_section.gd")
|
||||||
|
|
||||||
|
|
@ -14,13 +14,16 @@ func _build_section() -> Node:
|
||||||
toggle.name = "ToggleButton"
|
toggle.name = "ToggleButton"
|
||||||
toggle.text = "Economy HUD"
|
toggle.text = "Economy HUD"
|
||||||
header.add_child(toggle)
|
header.add_child(toggle)
|
||||||
|
var body_scroll := ScrollContainer.new()
|
||||||
|
body_scroll.name = "BodyScroll"
|
||||||
var body := VBoxContainer.new()
|
var body := VBoxContainer.new()
|
||||||
body.name = "Body"
|
body.name = "Body"
|
||||||
var inventory := Label.new()
|
var inventory := Label.new()
|
||||||
inventory.name = "InventoryLabel"
|
inventory.name = "InventoryLabel"
|
||||||
body.add_child(inventory)
|
body.add_child(inventory)
|
||||||
|
body_scroll.add_child(body)
|
||||||
section.add_child(header)
|
section.add_child(header)
|
||||||
section.add_child(body)
|
section.add_child(body_scroll)
|
||||||
return section
|
return section
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue