diff --git a/client/README.md b/client/README.md index cd16344..0942103 100644 --- a/client/README.md +++ b/client/README.md @@ -218,8 +218,8 @@ Full capstone checklist: [`docs/manual-qa/NEO-86.md`](../docs/manual-qa/NEO-86.m - **`GET /game/players/{id}/encounter-progress`** — server-authoritative per-encounter **`state`**, **`defeatedTargetIds`**, and **`rewardGrantSummary`** when **`completed`** (NEO-108); see [server README — Per-player encounter progress](../server/README.md#per-player-encounter-progress-neo-108). - **Scripts:** `scripts/encounter_progress_client.gd`; wired from `main.gd` in `_setup_encounter_progress_sync()`. - **HUD:** - - **`UICanvas/HudRoot/EncounterProgressLabel`** — frozen pocket **`prototype_combat_pocket`**: **`not started (0/3)`** → **`{n}/3`** while **`active`** → **`completed (3/3)`**. - - **`UICanvas/HudRoot/EncounterCompleteLabel`** — **`Loot: —`** until **`completed`**; then one line per grant (**`displayName`** via **`ItemDefinitionsClient`**, fallback **`itemId`**). + - **`UICanvas/HudRootScroll/HudRoot/EncounterProgressLabel`** — frozen pocket **`prototype_combat_pocket`**: **`not started (0/3)`** → **`{n}/3`** while **`active`** → **`completed (3/3)`**. + - **`UICanvas/HudRootScroll/HudRoot/EncounterCompleteLabel`** — **`Loot: —`** until **`completed`**; then one line per grant (**`displayName`** via **`ItemDefinitionsClient`**, fallback **`itemId`**). - **Refresh:** boot hydrate + **`GET`** after cast accept with **`targetDefeated: true`** (alongside gig/combat refreshes). No periodic poll or manual key. - **Inventory:** on **`completed`** row, **`InventoryLabel`** auto-refreshes (no **I**) so bag shows **`scrap_metal_bulk`** and **`contract_handoff_token`**. @@ -232,8 +232,8 @@ Full checklist: [`docs/manual-qa/NEO-110.md`](../docs/manual-qa/NEO-110.md). - **`GET /game/world/quest-definitions`** — cached on boot for display names and eligible-quest iteration (NEO-115). - **Scripts:** `scripts/quest_progress_client.gd`, `scripts/quest_definitions_client.gd`; wired from `main.gd` in `_setup_quest_progress_sync()`. - **HUD:** - - **`UICanvas/HudRoot/QuestProgressLabel`** — all four prototype quests in catalog order with compact status/step/counter lines. - - **`UICanvas/HudRoot/QuestAcceptFeedbackLabel`** — accept success/deny/failure copy. + - **`UICanvas/HudRootScroll/HudRoot/QuestProgressLabel`** — all four prototype quests in catalog order with compact status/step/counter lines. + - **`UICanvas/HudRootScroll/HudRoot/QuestAcceptFeedbackLabel`** — accept success/deny/failure copy. - **Accept keys:** **Q** = `prototype_quest_gather_intro`; **Shift+Q** = first catalog-order **`not_started`** quest with prerequisites **`completed`** in current snapshot. - **Refresh:** boot hydrate + GET after gather (interaction success), craft success, cast with **`targetDefeated: true`**, and after accept POST. No periodic poll. diff --git a/client/scenes/main.tscn b/client/scenes/main.tscn index 0698508..e102e31 100644 --- a/client/scenes/main.tscn +++ b/client/scenes/main.tscn @@ -1150,13 +1150,20 @@ theme_override_constants/outline_size = 8 theme_override_font_sizes/font_size = 22 autowrap_mode = 3 -[node name="HudRoot" type="VBoxContainer" parent="UICanvas" unique_id=9000022] +[node name="HudRootScroll" type="ScrollContainer" parent="UICanvas" unique_id=9000029] offset_left = 8.0 offset_top = 8.0 offset_right = 664.0 +offset_bottom = 392.0 +horizontal_scroll_mode = 0 +vertical_scroll_mode = 1 + +[node name="HudRoot" type="VBoxContainer" parent="UICanvas/HudRootScroll" unique_id=9000022] +layout_mode = 2 +size_flags_horizontal = 3 theme_override_constants/separation = 12 -[node name="PlayerPositionLabel" type="Label" parent="UICanvas/HudRoot" unique_id=9000003] +[node name="PlayerPositionLabel" type="Label" parent="UICanvas/HudRootScroll/HudRoot" unique_id=9000003] layout_mode = 2 size_flags_horizontal = 3 theme_override_colors/font_color = Color(0.92, 0.95, 0.98, 1) @@ -1169,7 +1176,7 @@ y: — z: — srv: —" -[node name="TargetLockLabel" type="Label" parent="UICanvas/HudRoot" unique_id=9000004] +[node name="TargetLockLabel" type="Label" parent="UICanvas/HudRootScroll/HudRoot" unique_id=9000004] layout_mode = 2 size_flags_horizontal = 3 theme_override_colors/font_color = Color(0.86, 0.94, 1, 1) @@ -1181,7 +1188,7 @@ text = "Target: — Validity: — Seq: —" -[node name="CastFeedbackLabel" type="Label" parent="UICanvas/HudRoot" unique_id=9000005] +[node name="CastFeedbackLabel" type="Label" parent="UICanvas/HudRootScroll/HudRoot" unique_id=9000005] layout_mode = 2 size_flags_horizontal = 3 theme_override_colors/font_color = Color(0.95, 0.88, 0.72, 1) @@ -1191,7 +1198,7 @@ theme_override_font_sizes/font_size = 22 autowrap_mode = 3 text = "Cast: —" -[node name="CombatTargetHpLabel" type="Label" parent="UICanvas/HudRoot" unique_id=9000018] +[node name="CombatTargetHpLabel" type="Label" parent="UICanvas/HudRootScroll/HudRoot" unique_id=9000018] layout_mode = 2 size_flags_horizontal = 3 theme_override_colors/font_color = Color(0.92, 0.78, 0.82, 1) @@ -1201,7 +1208,7 @@ theme_override_font_sizes/font_size = 22 autowrap_mode = 3 text = "Target HP: — (Tab → dummy)" -[node name="PlayerCombatHpLabel" type="Label" parent="UICanvas/HudRoot" unique_id=9000019] +[node name="PlayerCombatHpLabel" type="Label" parent="UICanvas/HudRootScroll/HudRoot" unique_id=9000019] layout_mode = 2 size_flags_horizontal = 3 theme_override_colors/font_color = Color(0.82, 0.92, 0.98, 1) @@ -1211,7 +1218,7 @@ theme_override_font_sizes/font_size = 22 autowrap_mode = 3 text = "Player HP: —" -[node name="EncounterProgressLabel" type="Label" parent="UICanvas/HudRoot" unique_id=9000023] +[node name="EncounterProgressLabel" type="Label" parent="UICanvas/HudRootScroll/HudRoot" unique_id=9000023] layout_mode = 2 size_flags_horizontal = 3 theme_override_colors/font_color = Color(0.9, 0.82, 0.72, 1) @@ -1222,7 +1229,7 @@ autowrap_mode = 3 text = "Encounter: Loading…" -[node name="EncounterCompleteLabel" type="Label" parent="UICanvas/HudRoot" unique_id=9000024] +[node name="EncounterCompleteLabel" type="Label" parent="UICanvas/HudRootScroll/HudRoot" unique_id=9000024] layout_mode = 2 size_flags_horizontal = 3 theme_override_colors/font_color = Color(0.95, 0.9, 0.65, 1) @@ -1232,7 +1239,7 @@ theme_override_font_sizes/font_size = 22 autowrap_mode = 3 text = "Loot: —" -[node name="QuestProgressLabel" type="Label" parent="UICanvas/HudRoot" unique_id=9000025] +[node name="QuestProgressLabel" type="Label" parent="UICanvas/HudRootScroll/HudRoot" unique_id=9000025] layout_mode = 2 size_flags_horizontal = 3 theme_override_colors/font_color = Color(0.82, 0.92, 0.98, 1) @@ -1243,7 +1250,7 @@ autowrap_mode = 3 text = "Quests: Loading…" -[node name="QuestAcceptFeedbackLabel" type="Label" parent="UICanvas/HudRoot" unique_id=9000026] +[node name="QuestAcceptFeedbackLabel" type="Label" parent="UICanvas/HudRootScroll/HudRoot" unique_id=9000026] layout_mode = 2 size_flags_horizontal = 3 theme_override_colors/font_color = Color(0.9, 0.86, 1, 1) @@ -1253,7 +1260,7 @@ theme_override_font_sizes/font_size = 22 autowrap_mode = 3 text = "Quest accept: — (Q gather / Shift+Q next)" -[node name="NpcStateLabel" type="Label" parent="UICanvas/HudRoot" unique_id=9000020] +[node name="NpcStateLabel" type="Label" parent="UICanvas/HudRootScroll/HudRoot" unique_id=9000020] layout_mode = 2 size_flags_horizontal = 3 theme_override_colors/font_color = Color(0.88, 0.82, 0.98, 1) @@ -1263,7 +1270,7 @@ theme_override_font_sizes/font_size = 22 autowrap_mode = 3 text = "NPC state: —" -[node name="TelegraphLabel" type="Label" parent="UICanvas/HudRoot" unique_id=9000021] +[node name="TelegraphLabel" type="Label" parent="UICanvas/HudRootScroll/HudRoot" unique_id=9000021] layout_mode = 2 size_flags_horizontal = 3 theme_override_colors/font_color = Color(1, 0.72, 0.55, 1) @@ -1273,7 +1280,7 @@ theme_override_font_sizes/font_size = 22 autowrap_mode = 3 text = "Telegraph: —" -[node name="GatherFeedbackLabel" type="Label" parent="UICanvas/HudRoot" unique_id=9000008] +[node name="GatherFeedbackLabel" type="Label" parent="UICanvas/HudRootScroll/HudRoot" unique_id=9000008] layout_mode = 2 size_flags_horizontal = 3 theme_override_colors/font_color = Color(0.88, 0.95, 0.72, 1) @@ -1283,7 +1290,7 @@ theme_override_font_sizes/font_size = 20 autowrap_mode = 3 text = "Gather: —" -[node name="CraftFeedbackLabel" type="Label" parent="UICanvas/HudRoot" unique_id=9000010] +[node name="CraftFeedbackLabel" type="Label" parent="UICanvas/HudRootScroll/HudRoot" unique_id=9000010] layout_mode = 2 size_flags_horizontal = 3 theme_override_colors/font_color = Color(0.95, 0.82, 0.72, 1) @@ -1293,7 +1300,7 @@ theme_override_font_sizes/font_size = 20 autowrap_mode = 3 text = "Craft: —" -[node name="CooldownSlotsLabel" type="Label" parent="UICanvas/HudRoot" unique_id=9000006] +[node name="CooldownSlotsLabel" type="Label" parent="UICanvas/HudRootScroll/HudRoot" unique_id=9000006] layout_mode = 2 size_flags_horizontal = 3 theme_override_colors/font_color = Color(0.78, 0.92, 0.86, 1) diff --git a/client/scripts/main.gd b/client/scripts/main.gd index a1fba89..ae2ffd3 100644 --- a/client/scripts/main.gd +++ b/client/scripts/main.gd @@ -17,7 +17,7 @@ extends Node3D ## Steep ramp: **2 m / 1.5 m** rise/run (~53°) so slope exceeds `Player` `floor_max_angle` (~50°) ## and is not walkable; gentle ramp stays shallower than `NavigationMesh.agent_max_climb`. Steep is ## under `World` only (not nav source geometry). -## Prototype HUD: world `CharacterBody3D` position in `UICanvas/HudRoot/PlayerPositionLabel` +## Prototype HUD: world `CharacterBody3D` position in `UICanvas/HudRootScroll/HudRoot/PlayerPositionLabel` ## (updated in `_physics_process` so it matches physics ticks). const MOVE_REJECT_MSG_SECONDS: float = 4.0 @@ -128,23 +128,23 @@ var _dev_pulse_bootstrap_attempted: bool = false @onready var _interactables_root: Node3D = $World/NavigationRegion3D/InteractablesRoot @onready var _radius_preview: Node3D = $World/InteractionMarkers @onready var _move_reject_label: Label = $UICanvas/MoveRejectLabel -@onready var _hud_root: VBoxContainer = $UICanvas/HudRoot -@onready var _player_pos_label: Label = $UICanvas/HudRoot/PlayerPositionLabel -@onready var _target_lock_label: Label = $UICanvas/HudRoot/TargetLockLabel -@onready var _cast_feedback_label: Label = $UICanvas/HudRoot/CastFeedbackLabel -@onready var _combat_target_hp_label: Label = $UICanvas/HudRoot/CombatTargetHpLabel -@onready var _player_combat_hp_label: Label = $UICanvas/HudRoot/PlayerCombatHpLabel -@onready var _encounter_progress_label: Label = $UICanvas/HudRoot/EncounterProgressLabel -@onready var _encounter_complete_label: Label = $UICanvas/HudRoot/EncounterCompleteLabel -@onready var _quest_progress_label: Label = $UICanvas/HudRoot/QuestProgressLabel -@onready var _quest_accept_feedback_label: Label = $UICanvas/HudRoot/QuestAcceptFeedbackLabel -@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 _hud_root: VBoxContainer = $UICanvas/HudRootScroll/HudRoot +@onready var _player_pos_label: Label = $UICanvas/HudRootScroll/HudRoot/PlayerPositionLabel +@onready var _target_lock_label: Label = $UICanvas/HudRootScroll/HudRoot/TargetLockLabel +@onready var _cast_feedback_label: Label = $UICanvas/HudRootScroll/HudRoot/CastFeedbackLabel +@onready var _combat_target_hp_label: Label = $UICanvas/HudRootScroll/HudRoot/CombatTargetHpLabel +@onready var _player_combat_hp_label: Label = $UICanvas/HudRootScroll/HudRoot/PlayerCombatHpLabel +@onready var _encounter_progress_label: Label = $UICanvas/HudRootScroll/HudRoot/EncounterProgressLabel +@onready var _encounter_complete_label: Label = $UICanvas/HudRootScroll/HudRoot/EncounterCompleteLabel +@onready var _quest_progress_label: Label = $UICanvas/HudRootScroll/HudRoot/QuestProgressLabel +@onready var _quest_accept_feedback_label: Label = $UICanvas/HudRootScroll/HudRoot/QuestAcceptFeedbackLabel +@onready var _npc_state_label: Label = $UICanvas/HudRootScroll/HudRoot/NpcStateLabel +@onready var _telegraph_label: Label = $UICanvas/HudRootScroll/HudRoot/TelegraphLabel +@onready var _cooldown_slots_label: Label = $UICanvas/HudRootScroll/HudRoot/CooldownSlotsLabel @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 _gather_feedback_label: Label = $UICanvas/HudRootScroll/HudRoot/GatherFeedbackLabel +@onready var _craft_feedback_label: Label = $UICanvas/HudRootScroll/HudRoot/CraftFeedbackLabel @onready 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") diff --git a/client/scripts/prototype_economy_hud_section.gd b/client/scripts/prototype_economy_hud_section.gd index 45e30aa..a601ab3 100644 --- a/client/scripts/prototype_economy_hud_section.gd +++ b/client/scripts/prototype_economy_hud_section.gd @@ -1,13 +1,17 @@ extends VBoxContainer ## NEO-75: collapsible economy HUD block (inventory, skills, craft panel). -## NEO-122: pinned to viewport bottom-left so recipes stay on-screen. +## NEO-122: pinned to viewport bottom-left; clamps HudRootScroll above this panel. -const MAX_VIEWPORT_HEIGHT_FRACTION := 0.48 -const MIN_BODY_SCROLL_HEIGHT := 180.0 +const MAX_VIEWPORT_HEIGHT_FRACTION := 0.42 +const MIN_BODY_SCROLL_HEIGHT := 160.0 const BOTTOM_MARGIN := 8.0 const SIDE_MARGIN := 8.0 const PANEL_WIDTH := 656.0 +const HUD_TOP_MARGIN := 8.0 +const HUD_ECONOMY_GAP := 12.0 +const HUD_ROOT_SCROLL_NAME := &"HudRootScroll" +const PANEL_FILL := Color(0.06, 0.06, 0.1, 0.94) @onready var _toggle: CheckButton = $HeaderRow/ToggleButton @onready var _body_scroll: ScrollContainer = $BodyScroll @@ -16,6 +20,7 @@ const PANEL_WIDTH := 656.0 func _ready() -> void: z_index = 10 + clip_contents = true _toggle.button_pressed = true _body_scroll.visible = true _toggle.toggled.connect(_on_toggle_toggled) @@ -25,6 +30,10 @@ func _ready() -> void: _apply_viewport_layout() +func _draw() -> void: + draw_rect(Rect2(Vector2.ZERO, size), PANEL_FILL) + + func _on_toggle_toggled(pressed: bool) -> void: _body_scroll.visible = pressed call_deferred("_apply_viewport_layout") @@ -46,6 +55,10 @@ func scroll_body_to_recipes() -> void: _body_scroll.scroll_vertical = int(_recipes_header.position.y) +func panel_top_y() -> float: + return position.y + + func _apply_viewport_layout() -> void: if not is_instance_valid(_body_scroll): return @@ -62,6 +75,17 @@ func _apply_viewport_layout() -> void: panel_height += body_height position = Vector2(SIDE_MARGIN, vp_size.y - panel_height - BOTTOM_MARGIN) size = Vector2(PANEL_WIDTH, panel_height) + _apply_hud_root_scroll_bounds() + queue_redraw() + + +func _apply_hud_root_scroll_bounds() -> void: + var hud_scroll := get_parent().get_node_or_null(HUD_ROOT_SCROLL_NAME) + if not hud_scroll is ScrollContainer: + return + var available_height: float = position.y - HUD_TOP_MARGIN - HUD_ECONOMY_GAP + hud_scroll.position = Vector2(SIDE_MARGIN, HUD_TOP_MARGIN) + hud_scroll.size = Vector2(PANEL_WIDTH, max(80.0, available_height)) func _measure_header_height() -> float: diff --git a/client/test/prototype_economy_hud_section_test.gd b/client/test/prototype_economy_hud_section_test.gd index ebeecb4..47a6085 100644 --- a/client/test/prototype_economy_hud_section_test.gd +++ b/client/test/prototype_economy_hud_section_test.gd @@ -78,3 +78,23 @@ func test_viewport_layout_pins_section_above_bottom_margin() -> void: assert_that(section.position.y).is_greater(vp_height * 0.2) assert_that(section.position.y + section.size.y).is_less_equal(vp_height) + +func test_viewport_layout_clamps_hud_root_scroll() -> void: + # Arrange + var canvas := Control.new() + canvas.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + var hud_scroll := ScrollContainer.new() + hud_scroll.name = "HudRootScroll" + canvas.add_child(hud_scroll) + var section: Control = _build_section() as Control + canvas.add_child(section) + add_child(canvas) + await await_idle_frame() + await get_tree().process_frame + # Act + section.call("_apply_viewport_layout") + await get_tree().process_frame + # Assert + assert_that(hud_scroll.size.y).is_less(section.call("panel_top_y")) + +