diff --git a/client/README.md b/client/README.md index 9fd2140..e603a71 100644 --- a/client/README.md +++ b/client/README.md @@ -139,7 +139,7 @@ Full checklist: [`docs/manual-qa/NEO-31.md`](../docs/manual-qa/NEO-31.md) · cas - **`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/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. +- **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. - **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). @@ -147,23 +147,42 @@ 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 **`SkillProgressionLabel`**; boot hydrate + refresh after successful gather. +- **`GET /game/players/{id}/skill-progression`** — **`salvage`** row on **`UICanvas/EconomyHudSection/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`**). **`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/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). ## Craft UI + recipe list (NEO-74) -- **`GET /game/world/recipe-definitions`** — eight prototype recipes on boot; scrollable panel 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). - **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}`**. **`SkillProgressionLabel`** adds **`refine`** row (boot + post-craft refresh). **`InventoryLabel`** refreshes on craft success only (no **I**). +- **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**). - **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). +## End-to-end economy loop (NEO-75) + +Capstone proof that Epic 3 prototype gather→craft works **in Godot** without Bruno. + +**Flow (single session):** + +1. **Boot** — server running; Godot **F5**. Clients hydrate item defs, inventory, skill progression, recipe defs (NEO-72–74). +2. **Gather** — **WASD** to each resource anchor; **R** posts nearest in-range **`resource_node`** interact (NEO-63). On success: **`GatherFeedbackLabel`** delta, automatic inventory GET, **salvage** row refresh (NEO-73). +3. **Refine** — scroll **`EconomyHudSection`** recipe panel; **Craft** **`refine_scrap_standard`** when **≥5** scrap (NEO-74). On success: **`CraftFeedbackLabel`**, inventory GET, **refine** row refresh. +4. **Make** — **Craft** **`make_field_stim_mk0`** when **≥2** **`refined_plate_stock`** and **≥1** scrap. Verify **`field_stim_mk0`** in bag. + +**Inputs:** **R** gather · **Craft** buttons (qty **1**) · **I** manual inventory refresh (optional) · **`Economy HUD`** checkbox collapses inventory/skills/recipe block (NEO-75). + +**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`). + +Full capstone checklist: [`docs/manual-qa/NEO-75.md`](../docs/manual-qa/NEO-75.md). + ### Manual check (NEO-24) 1. Run the server and client as in NEO-7 / NEO-9; confirm spawn snap at `(-5, 0.9, -5)`. @@ -223,7 +242,7 @@ On **Linux** (including GitHub Actions), the path must be **`gdUnit4`** with a c **CI:** The **GDScript** workflow fails the job if Godot prints **`SCRIPT ERROR:`** or **`ERROR: Failed to load script`** while running GdUnit. GdUnit alone can still exit **0** when a test file fails to parse (that suite is skipped); the workflow guards against that. -**Scope:** Unit tests cover **`player.gd`**, **`player_locomotion_wasd.gd`** (pure WASD seam helpers), **`position_authority_client.gd`**, **`inventory_client.gd`**, **`item_definitions_client.gd`** (NEO-72), **`skill_progression_client.gd`**, **`prototype_interactable_picker.gd`**, extended **`interaction_request_client_test.gd`**, **`gather_feedback_refresh_test.gd`** (NEO-73), **`craft_client.gd`**, **`recipe_definitions_client.gd`**, **`craft_feedback_refresh_test.gd`** (NEO-74), **`isometric_follow_camera.gd`** (eye math + effective zoom distance), **`camera_state.gd`**, and **`zoom_band_config.gd`**. **`main.gd`** and scene wiring are **not** automated here—use manual checks above. +**Scope:** Unit tests cover **`player.gd`**, **`player_locomotion_wasd.gd`** (pure WASD seam helpers), **`position_authority_client.gd`**, **`inventory_client.gd`**, **`item_definitions_client.gd`** (NEO-72), **`skill_progression_client.gd`**, **`prototype_interactable_picker.gd`**, extended **`interaction_request_client_test.gd`**, **`gather_feedback_refresh_test.gd`** (NEO-73), **`craft_client.gd`**, **`recipe_definitions_client.gd`**, **`craft_feedback_refresh_test.gd`** (NEO-74), **`prototype_economy_hud_section.gd`** (NEO-75), **`isometric_follow_camera.gd`** (eye math + effective zoom distance), **`camera_state.gd`**, and **`zoom_band_config.gd`**. **`main.gd`** and scene wiring are **not** automated here—use manual checks above. **Camera zoom:** Input actions **`camera_zoom_in`** / **`camera_zoom_out`** (mouse wheel, **`=`** / **`-`**, keypad **+** / **−**) are defined in **`project.godot`**. On layouts where **`+`** requires **Shift**, remap or add a binding under **Project → Project Settings → Input Map** if needed. diff --git a/client/scenes/main.tscn b/client/scenes/main.tscn index 7b80b26..eaa0fc6 100644 --- a/client/scenes/main.tscn +++ b/client/scenes/main.tscn @@ -18,6 +18,7 @@ [ext_resource type="Script" path="res://scripts/recipe_definitions_client.gd" id="17_recipe_defs"] [ext_resource type="Script" path="res://scripts/craft_client.gd" id="18_craft"] [ext_resource type="Script" path="res://scripts/craft_recipe_panel.gd" id="19_craft_panel"] +[ext_resource type="Script" path="res://scripts/prototype_economy_hud_section.gd" id="20_economy_hud"] [sub_resource type="NavigationMesh" id="NavigationMesh_district"] geometry_collision_mask = 1 @@ -1211,13 +1212,29 @@ theme_override_font_sizes/font_size = 14 autowrap_mode = 3 text = "Cooldowns: —" -[node name="InventoryLabel" type="Label" parent="UICanvas" unique_id=9000007] +[node name="EconomyHudSection" type="VBoxContainer" parent="UICanvas" unique_id=9000014] offset_left = 8.0 offset_top = 524.0 offset_right = 520.0 -offset_bottom = 672.0 +offset_bottom = 1056.0 grow_horizontal = 0 grow_vertical = 0 +script = ExtResource("20_economy_hud") + +[node name="HeaderRow" type="HBoxContainer" parent="UICanvas/EconomyHudSection" unique_id=9000015] +layout_mode = 2 + +[node name="ToggleButton" type="CheckButton" parent="UICanvas/EconomyHudSection/HeaderRow" unique_id=9000016] +layout_mode = 2 +text = "Economy HUD" + +[node name="Body" type="VBoxContainer" parent="UICanvas/EconomyHudSection" unique_id=9000017] +layout_mode = 2 + +[node name="InventoryLabel" type="Label" parent="UICanvas/EconomyHudSection/Body" unique_id=9000007] +custom_minimum_size = Vector2(512, 148) +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 = 6 @@ -1226,13 +1243,10 @@ autowrap_mode = 3 text = "Inventory: (refresh: I) Loading…" -[node name="SkillProgressionLabel" type="Label" parent="UICanvas" unique_id=9000009] -offset_left = 8.0 -offset_top = 684.0 -offset_right = 520.0 -offset_bottom = 768.0 -grow_horizontal = 0 -grow_vertical = 0 +[node name="SkillProgressionLabel" type="Label" parent="UICanvas/EconomyHudSection/Body" unique_id=9000009] +custom_minimum_size = Vector2(512, 84) +layout_mode = 2 +size_flags_horizontal = 3 theme_override_colors/font_color = Color(0.82, 0.9, 1, 1) theme_override_colors/font_outline_color = Color(0.08, 0.08, 0.1, 1) theme_override_constants/outline_size = 6 @@ -1241,19 +1255,17 @@ autowrap_mode = 3 text = "Skills: Loading…" -[node name="CraftRecipePanel" type="Control" parent="UICanvas" unique_id=9000011] -anchors_preset = 0 -offset_left = 8.0 -offset_top = 776.0 -offset_right = 520.0 -offset_bottom = 1056.0 +[node name="CraftRecipePanel" type="Control" parent="UICanvas/EconomyHudSection/Body" unique_id=9000011] +custom_minimum_size = Vector2(512, 280) +layout_mode = 2 +size_flags_horizontal = 3 script = ExtResource("19_craft_panel") -[node name="ScrollContainer" type="ScrollContainer" parent="UICanvas/CraftRecipePanel" unique_id=9000012] +[node name="ScrollContainer" type="ScrollContainer" parent="UICanvas/EconomyHudSection/Body/CraftRecipePanel" unique_id=9000012] layout_mode = 0 offset_right = 512.0 offset_bottom = 280.0 -[node name="VBoxContainer" type="VBoxContainer" parent="UICanvas/CraftRecipePanel/ScrollContainer" unique_id=9000013] +[node name="VBoxContainer" type="VBoxContainer" parent="UICanvas/EconomyHudSection/Body/CraftRecipePanel/ScrollContainer" unique_id=9000013] layout_mode = 2 size_flags_horizontal = 3 diff --git a/client/scripts/main.gd b/client/scripts/main.gd index 28e04c2..ed43e55 100644 --- a/client/scripts/main.gd +++ b/client/scripts/main.gd @@ -102,11 +102,13 @@ var _recipe_defs_error: String = "" @onready var _target_lock_label: Label = $UICanvas/TargetLockLabel @onready var _cast_feedback_label: Label = $UICanvas/CastFeedbackLabel @onready var _cooldown_slots_label: Label = $UICanvas/CooldownSlotsLabel -@onready var _inventory_label: Label = $UICanvas/InventoryLabel +@onready var _economy_hud_section: VBoxContainer = $UICanvas/EconomyHudSection +@onready var _inventory_label: Label = _economy_hud_section.get_node("Body/InventoryLabel") @onready var _gather_feedback_label: Label = $UICanvas/GatherFeedbackLabel @onready var _craft_feedback_label: Label = $UICanvas/CraftFeedbackLabel -@onready var _skill_progression_label: Label = $UICanvas/SkillProgressionLabel -@onready var _craft_recipe_panel: Node = $UICanvas/CraftRecipePanel +@onready +var _skill_progression_label: Label = _economy_hud_section.get_node("Body/SkillProgressionLabel") +@onready var _craft_recipe_panel: Node = _economy_hud_section.get_node("Body/CraftRecipePanel") @onready var _inventory_client: Node = $InventoryClient @onready var _item_defs_client: Node = $ItemDefinitionsClient @onready var _skill_progression_client: Node = $SkillProgressionClient diff --git a/client/scripts/prototype_economy_hud_section.gd b/client/scripts/prototype_economy_hud_section.gd new file mode 100644 index 0000000..ec35e92 --- /dev/null +++ b/client/scripts/prototype_economy_hud_section.gd @@ -0,0 +1,25 @@ +extends VBoxContainer + +## NEO-75: collapsible economy HUD block (inventory, skills, craft panel). + +@onready var _toggle: CheckButton = $HeaderRow/ToggleButton +@onready var _body: VBoxContainer = $Body + + +func _ready() -> void: + _toggle.button_pressed = true + _body.visible = true + _toggle.toggled.connect(_on_toggle_toggled) + + +func _on_toggle_toggled(pressed: bool) -> void: + _body.visible = pressed + + +func set_expanded(expanded: bool) -> void: + _toggle.button_pressed = expanded + _body.visible = expanded + + +func is_body_visible() -> bool: + return _body.visible diff --git a/client/scripts/prototype_economy_hud_section.gd.uid b/client/scripts/prototype_economy_hud_section.gd.uid new file mode 100644 index 0000000..2f5c3ae --- /dev/null +++ b/client/scripts/prototype_economy_hud_section.gd.uid @@ -0,0 +1 @@ +uid://bneo75economyhud1 diff --git a/client/test/prototype_economy_hud_section_test.gd b/client/test/prototype_economy_hud_section_test.gd new file mode 100644 index 0000000..46ed2d5 --- /dev/null +++ b/client/test/prototype_economy_hud_section_test.gd @@ -0,0 +1,61 @@ +extends GdUnitTestSuite + +## NEO-75: economy HUD collapse toggle shows/hides body container. +## Covers economy HUD node paths via EconomyHudSection.get_node in main.gd. + +const EconomyHudSection := preload("res://scripts/prototype_economy_hud_section.gd") + + +func _build_section() -> Node: + var section: VBoxContainer = EconomyHudSection.new() + var header := HBoxContainer.new() + header.name = "HeaderRow" + var toggle := CheckButton.new() + toggle.name = "ToggleButton" + toggle.text = "Economy HUD" + header.add_child(toggle) + var body := VBoxContainer.new() + body.name = "Body" + var inventory := Label.new() + inventory.name = "InventoryLabel" + body.add_child(inventory) + section.add_child(header) + section.add_child(body) + return section + + +func test_body_visible_when_expanded_by_default() -> void: + # Arrange + var section := _build_section() + add_child(section) + await await_idle_frame() + # Act + var visible: bool = section.call("is_body_visible") + # Assert + assert_bool(visible).is_true() + + +func test_toggle_collapses_body() -> void: + # Arrange + var section := _build_section() + add_child(section) + await await_idle_frame() + # Act + section.get_node("HeaderRow/ToggleButton").button_pressed = false + await await_idle_frame() + # Assert + assert_bool(section.call("is_body_visible")).is_false() + + +func test_toggle_expands_body_again() -> void: + # Arrange + var section := _build_section() + add_child(section) + await await_idle_frame() + section.call("set_expanded", false) + await await_idle_frame() + # Act + section.call("set_expanded", true) + await await_idle_frame() + # Assert + assert_bool(section.call("is_body_visible")).is_true() diff --git a/client/test/prototype_economy_hud_section_test.gd.uid b/client/test/prototype_economy_hud_section_test.gd.uid new file mode 100644 index 0000000..1fd0dd9 --- /dev/null +++ b/client/test/prototype_economy_hud_section_test.gd.uid @@ -0,0 +1 @@ +uid://bneo75economyhudtest1 diff --git a/docs/decomposition/epics/epic_03_crafting_economy.md b/docs/decomposition/epics/epic_03_crafting_economy.md index 9729ca0..a2a1a77 100644 --- a/docs/decomposition/epics/epic_03_crafting_economy.md +++ b/docs/decomposition/epics/epic_03_crafting_economy.md @@ -96,6 +96,8 @@ Implement gathering from world nodes, item/inventory representation, recipe exec **Linear backlog:** [E3S5-client-prototype-backlog.md](../../plans/E3S5-client-prototype-backlog.md) — [NEO-72](https://linear.app/neon-sprawl/issue/NEO-72) → [NEO-75](https://linear.app/neon-sprawl/issue/NEO-75). +**Status:** Slice 5 client capstone landed — [NEO-75](../../manual-qa/NEO-75.md) manual QA; NEO-72→NEO-75 complete; prototype gather→craft loop playable in Godot (not Bruno-only). + ### Slice 4 - Sinks and economy policy (pre-production) diff --git a/docs/decomposition/modules/E3_M1_ResourceNodeAndGatherLoop.md b/docs/decomposition/modules/E3_M1_ResourceNodeAndGatherLoop.md index 247b5e5..3888688 100644 --- a/docs/decomposition/modules/E3_M1_ResourceNodeAndGatherLoop.md +++ b/docs/decomposition/modules/E3_M1_ResourceNodeAndGatherLoop.md @@ -7,7 +7,7 @@ | **Module ID** | E3.M1 | | **Epic** | [Epic 3 — Crafting Economy](../epics/epic_03_crafting_economy.md) | | **Stage target** | Prototype | -| **Status** | Ready — Slice 2 backlog [NEO-57](https://linear.app/neon-sprawl/issue/NEO-57)–[NEO-64](https://linear.app/neon-sprawl/issue/NEO-64) complete ([E3M1-prototype-backlog](../../plans/E3M1-prototype-backlog.md); NEO-41 anchor below; see [dependency register](module_dependency_register.md)) | +| **Status** | Ready — Slice 2 backlog [NEO-57](https://linear.app/neon-sprawl/issue/NEO-57)–[NEO-64](https://linear.app/neon-sprawl/issue/NEO-64) complete ([E3M1-prototype-backlog](../../plans/E3M1-prototype-backlog.md); NEO-41 anchor below; see [dependency register](module_dependency_register.md)); Slice 5 client [NEO-73](https://linear.app/neon-sprawl/issue/NEO-73) gather feedback + capstone [NEO-75](https://linear.app/neon-sprawl/issue/NEO-75) playable loop landed | | **Linear** | Label **`E3.M1`** per [decomposition README — Linear alignment](../README.md#linear-alignment) | ## Purpose diff --git a/docs/decomposition/modules/E3_M2_RefinementAndRecipeExecution.md b/docs/decomposition/modules/E3_M2_RefinementAndRecipeExecution.md index f15bf8c..6c784eb 100644 --- a/docs/decomposition/modules/E3_M2_RefinementAndRecipeExecution.md +++ b/docs/decomposition/modules/E3_M2_RefinementAndRecipeExecution.md @@ -7,7 +7,7 @@ | **Module ID** | E3.M2 | | **Epic** | [Epic 3 — Crafting Economy](../epics/epic_03_crafting_economy.md) | | **Stage target** | Prototype | -| **Status** | Ready — Slice 3 server backlog [E3M2-prototype-backlog](../../plans/E3M2-prototype-backlog.md): E3M2-01 [NEO-65](https://linear.app/neon-sprawl/issue/NEO-65) through E3M2-07 [NEO-71](https://linear.app/neon-sprawl/issue/NEO-71) landed; Slice 5 client craft UI [NEO-74](https://linear.app/neon-sprawl/issue/NEO-74) landed (capstone [NEO-75](https://linear.app/neon-sprawl/issue/NEO-75) remaining) | +| **Status** | Ready — Slice 3 server backlog [E3M2-prototype-backlog](../../plans/E3M2-prototype-backlog.md): E3M2-01 [NEO-65](https://linear.app/neon-sprawl/issue/NEO-65) through E3M2-07 [NEO-71](https://linear.app/neon-sprawl/issue/NEO-71) landed; Slice 5 client [NEO-74](https://linear.app/neon-sprawl/issue/NEO-74) craft UI + capstone [NEO-75](https://linear.app/neon-sprawl/issue/NEO-75) playable loop landed | ## Implementation snapshot diff --git a/docs/decomposition/modules/E3_M3_ItemizationAndInventorySchema.md b/docs/decomposition/modules/E3_M3_ItemizationAndInventorySchema.md index b98a32a..58ab105 100644 --- a/docs/decomposition/modules/E3_M3_ItemizationAndInventorySchema.md +++ b/docs/decomposition/modules/E3_M3_ItemizationAndInventorySchema.md @@ -7,7 +7,7 @@ | **Module ID** | E3.M3 | | **Epic** | [Epic 3 — Crafting Economy](../epics/epic_03_crafting_economy.md) | | **Stage target** | Prototype | -| **Status** | In Progress — Slice 1 backlog [NEO-50](https://linear.app/neon-sprawl/issue/NEO-50)–[NEO-56](https://linear.app/neon-sprawl/issue/NEO-56) ([E3M3-prototype-backlog](../../plans/E3M3-prototype-backlog.md); see [dependency register](module_dependency_register.md)) | +| **Status** | In Progress — Slice 1 backlog [NEO-50](https://linear.app/neon-sprawl/issue/NEO-50)–[NEO-56](https://linear.app/neon-sprawl/issue/NEO-56) complete ([E3M3-prototype-backlog](../../plans/E3M3-prototype-backlog.md); see [dependency register](module_dependency_register.md)); Slice 5 client [NEO-72](https://linear.app/neon-sprawl/issue/NEO-72) inventory HUD + capstone [NEO-75](https://linear.app/neon-sprawl/issue/NEO-75) economy HUD collapse landed | ## Purpose diff --git a/docs/decomposition/modules/documentation_and_implementation_alignment.md b/docs/decomposition/modules/documentation_and_implementation_alignment.md index f56ec69..671bf05 100644 --- a/docs/decomposition/modules/documentation_and_implementation_alignment.md +++ b/docs/decomposition/modules/documentation_and_implementation_alignment.md @@ -53,9 +53,9 @@ Rows appear when work starts; default for unlisted modules is **Planned** / not | E2.M1 | In Progress | **NEO-33 landed:** frozen prototype trio **`salvage`** / **`refine`** / **`intrusion`** in [`content/skills/prototype_skills.json`](../../../content/skills/prototype_skills.json); PR gate + [`validate_content.py`](../../../scripts/validate_content.py) enforce schema, duplicate `id`, exact trio ids, and category coverage; designer note on **`allowedXpSourceKinds`** in [E2_M1](E2_M1_SkillDefinitionRegistry.md) + [`content/README.md`](../../../content/README.md). **NEO-34 landed:** fail-fast server load of `content/skills/*.json` at startup — `server/NeonSprawl.Server/Game/Skills/` ([NEO-34](../../plans/NEO-34-implementation-plan.md)); config + discovery in [server README — Skill catalog](../../../server/README.md#skill-catalog-contentskills-neo-34). **NEO-35 landed:** `ISkillDefinitionRegistry` / `SkillDefinitionRegistry` + DI ([NEO-35](../../plans/NEO-35-implementation-plan.md)). **NEO-36 landed:** versioned **`GET /game/world/skill-definitions`** ([NEO-36](../../plans/NEO-36-implementation-plan.md)) — `SkillDefinitionsWorldApi` + `SkillDefinitionsListDtos` in `server/NeonSprawl.Server/Game/Skills/`; Bruno `bruno/neon-sprawl-server/skill-definitions/`; manual QA [`NEO-36`](../../manual-qa/NEO-36.md). **E2.M2 consumer (NEO-38 landed):** grant path validates `skillId` + `sourceKind` vs catalog **`allowedXpSourceKinds`** on **`POST …/skill-progression`** ([NEO-38](../../plans/NEO-38-implementation-plan.md)); see [server README — Skill progression grant](../../../server/README.md#skill-progression-grant-neo-38) and [E2_M2](E2_M2_XpAwardAndLevelEngine.md). | [NEO-33](../../plans/NEO-33-implementation-plan.md), [NEO-34](../../plans/NEO-34-implementation-plan.md), [NEO-35](../../plans/NEO-35-implementation-plan.md), [NEO-36](../../plans/NEO-36-implementation-plan.md); [E2_M1](E2_M1_SkillDefinitionRegistry.md); [module_dependency_register.md](module_dependency_register.md) **E2.M1 note**; `server/NeonSprawl.Server/Game/Skills/`; [`docs/manual-qa/NEO-36.md`](../../manual-qa/NEO-36.md); [server README — Skill definitions (NEO-36)](../../../server/README.md#skill-definitions-neo-36) | | E2.M3 | In Progress | **NEO-45 landed:** prototype **`salvage`** mastery catalog + CI gates (see [NEO-45 plan](../../plans/NEO-45-implementation-plan.md)). **NEO-46 landed:** fail-fast server load under `server/NeonSprawl.Server/Game/Mastery/` — `MasteryCatalogLoader`, `IMasteryCatalogRegistry`, cross-check vs `ISkillDefinitionRegistry`, Slice 4 + **`tierIndex`** 1..N gate; see [NEO-46 plan](../../plans/NEO-46-implementation-plan.md). **NEO-47 landed:** `PerkUnlockEngine`, `IPlayerPerkStateStore` + **`V004`**, level-up hook in skill XP grants; see [NEO-47 plan](../../plans/NEO-47-implementation-plan.md). **NEO-49 landed:** comment-only **`perk_unlock`** telemetry hook site in [`PerkUnlockEngine.TryUnlockPerks`](../../../server/NeonSprawl.Server/Game/Mastery/PerkUnlockEngine.cs) ([NEO-49 plan](../../plans/NEO-49-implementation-plan.md), [`NEO-49` manual QA](../../manual-qa/NEO-49.md)); [server README — Perk unlock telemetry (NEO-49)](../../../server/README.md#perk-unlock-engine-and-telemetry-hooks-neo-47-neo-49). **NEO-48 landed:** **`GET`/`POST /game/players/{id}/perk-state`** — `PerkStateApi` + DTOs in `Game/Mastery/` ([NEO-48](../../plans/NEO-48-implementation-plan.md), [`NEO-48` manual QA](../../manual-qa/NEO-48.md)); [server README — Perk state (NEO-48)](../../../server/README.md#perk-state-neo-48); Bruno `bruno/neon-sprawl-server/perk-state/`. | [NEO-45](../../plans/NEO-45-implementation-plan.md), [NEO-46](../../plans/NEO-46-implementation-plan.md), [NEO-47](../../plans/NEO-47-implementation-plan.md), [NEO-48](../../plans/NEO-48-implementation-plan.md), [NEO-49](../../plans/NEO-49-implementation-plan.md), [E2M3-pre-production-backlog](../../plans/E2M3-pre-production-backlog.md), [E2_M3](E2_M3_MasteryAndPerkUnlocks.md) | | E2.M2 | In Progress | **NEO-37 landed:** versioned **`GET /game/players/{id}/skill-progression`** ([NEO-37](../../plans/NEO-37-implementation-plan.md)) — read model for every registered skill; known-player gate via `IPositionStateStore`; [server README — Skill progression snapshot (NEO-37)](../../../server/README.md#skill-progression-snapshot-neo-37); manual QA [`NEO-37`](../../manual-qa/NEO-37.md). **NEO-38 landed:** **`POST`** same path — grant apply, persistence (`IPlayerSkillProgressionStore`, `V003` migration), structured denies + **`levelUps`** ([NEO-38](../../plans/NEO-38-implementation-plan.md)); manual QA [`NEO-38`](../../manual-qa/NEO-38.md); Bruno `bruno/neon-sprawl-server/skill-progression/`; [server README — Skill progression grant (NEO-38)](../../../server/README.md#skill-progression-grant-neo-38). **NEO-39 landed:** data-driven `LevelCurve` content + schema + CI validation (`*_level_curve.json`) with fail-fast startup schema checks; progression GET/POST level resolution now uses `ISkillLevelCurve` content-backed thresholds ([NEO-39](../../plans/NEO-39-implementation-plan.md)); manual QA [`NEO-39`](../../manual-qa/NEO-39.md). **NEO-40 landed:** comment-only telemetry hook sites in [`SkillProgressionSnapshotApi.cs`](../../../server/NeonSprawl.Server/Game/Skills/SkillProgressionSnapshotApi.cs) on **`POST …/skill-progression`** for future **`xp_grant`** / **`level_up`** ([NEO-40](../../plans/NEO-40-implementation-plan.md), [`NEO-40` manual QA](../../manual-qa/NEO-40.md)). **NEO-41 landed:** gather prototype — **`POST …/interact`** with **`kind: resource_node`** grants **`salvage`** + **`activity`** (10 XP) via [`SkillProgressionGrantOperations`](../../../server/NeonSprawl.Server/Game/Skills/SkillProgressionGrantOperations.cs) ([NEO-41](../../plans/NEO-41-implementation-plan.md), [`NEO-41` manual QA](../../manual-qa/NEO-41.md)); [server README — Interaction](../../../server/README.md#interaction-neo-9). **NEO-42 landed (prep):** **`RefineActivitySkillXpGrant`** / **`RefineSkillXpConstants`** + deny-registry factory + tests — same NEO-38 grant stack; **E3.M2** must invoke on craft/refine success ([NEO-42](../../plans/NEO-42-implementation-plan.md), [`NEO-42` manual QA](../../manual-qa/NEO-42.md)); [server README — Craft / refine hook (NEO-42)](../../../server/README.md#craft--refine-hook--skill-xp-neo-42). **NEO-43 landed (prep):** **`MissionRewardSkillXpGrant`** / **`MissionRewardSkillXpConstants`** + deny-registry factory + tests — same NEO-38 grant stack with fixed **`sourceKind: mission_reward`**; **E7.M2** must invoke from quest hand-in ([NEO-43](../../plans/NEO-43-implementation-plan.md), [`NEO-43` manual QA](../../manual-qa/NEO-43.md)); [server README — Mission / quest reward (NEO-43)](../../../server/README.md#mission--quest-reward--skill-xp-neo-43). **Slice 3 still open:** [NEO-44](https://linear.app/neon-sprawl/issue/NEO-44). See [epic_02 — E2.M2 + Slice 3](../epics/epic_02_skills_and_progression.md). | [NEO-37](../../plans/NEO-37-implementation-plan.md), [NEO-38](../../plans/NEO-38-implementation-plan.md), [NEO-39](../../plans/NEO-39-implementation-plan.md), [NEO-40](../../plans/NEO-40-implementation-plan.md), [NEO-41](../../plans/NEO-41-implementation-plan.md), [NEO-42](../../plans/NEO-42-implementation-plan.md), [NEO-43](../../plans/NEO-43-implementation-plan.md), [E2_M2](E2_M2_XpAwardAndLevelEngine.md); label **`E2.M2`** on NEO-37–NEO-41, NEO-42–NEO-44 | -| E3.M1 | Ready | **NEO-41 landed (prototype, superseded on interact by NEO-63):** inline XP on **`resource_node`** interact. **NEO-57–NEO-61** — content, registry, HTTP defs, depletion store (see prior alignment). **NEO-62 landed:** **`GatherOperations`** + **`GatherResult`**. **NEO-63 landed:** **`POST …/interact`** **`resource_node`** → gather engine; **four** **`PrototypeInteractableRegistry`** anchors; Bruno gather → inventory + depletion deny ([NEO-63](../../plans/NEO-63-implementation-plan.md), [`NEO-63` manual QA](../../manual-qa/NEO-63.md)); [server README — Gather via interact (NEO-63)](../../../server/README.md#gather-via-interact-neo-63). **NEO-64 landed:** comment-only **`resource_gathered`** / **`gather_node_depleted`** hook sites in **`GatherOperations.TryGather`** ([NEO-64](../../plans/NEO-64-implementation-plan.md), [`NEO-64` manual QA](../../manual-qa/NEO-64.md)); Epic 3 Slice 2 backlog **E3M1-01**–**E3M1-08** complete. **NEO-73 landed:** client gather feedback — **`skill_progression_client.gd`**, **`prototype_interactable_picker.gd`** (nearest in-range **R**), **`GatherFeedbackLabel`** + **`SkillProgressionLabel`**, auto inventory/skill refresh after successful gather ([NEO-73](../../plans/NEO-73-implementation-plan.md), [`NEO-73` manual QA](../../manual-qa/NEO-73.md)); `client/README.md` gather section. **Slice 5 client (remaining):** capstone [NEO-75](https://linear.app/neon-sprawl/issue/NEO-75) — [E3S5 backlog](../../plans/E3S5-client-prototype-backlog.md). | [NEO-41](../../plans/NEO-41-implementation-plan.md) … [NEO-64](../../plans/NEO-64-implementation-plan.md), [NEO-73](../../plans/NEO-73-implementation-plan.md), [E3S5-client-prototype-backlog](../../plans/E3S5-client-prototype-backlog.md), [E3_M1](E3_M1_ResourceNodeAndGatherLoop.md); `server/NeonSprawl.Server/Game/Gathering/`, `Game/Interaction/`, `client/scripts/skill_progression_client.gd`, `client/scripts/prototype_interactable_picker.gd` | -| E3.M3 | In Progress | **NEO-50 landed:** frozen prototype six-item catalog in [`content/items/prototype_items.json`](../../../content/items/prototype_items.json); [`item-def.schema.json`](../../../content/schemas/item-def.schema.json); PR gate + [`validate_content.py`](../../../scripts/validate_content.py). **NEO-51 landed:** fail-fast server load of `content/items/*_items.json` at startup — `server/NeonSprawl.Server/Game/Items/` ([NEO-51](../../plans/NEO-51-implementation-plan.md)); [server README — Item catalog](../../../server/README.md#item-catalog-contentitems-neo-51). **NEO-52 landed:** injectable **`IItemDefinitionRegistry`** + lookup tests ([NEO-52](../../plans/NEO-52-implementation-plan.md)). **NEO-53 landed:** **`GET /game/world/item-definitions`** — `ItemDefinitionsWorldApi` + DTOs in `Game/Items/` ([NEO-53](../../plans/NEO-53-implementation-plan.md), [`NEO-53` manual QA](../../manual-qa/NEO-53.md)); [server README — Item definitions (NEO-53)](../../../server/README.md#item-definitions-neo-53); Bruno `bruno/neon-sprawl-server/item-definitions/`. **NEO-54 landed:** **`IPlayerInventoryStore`** + **`PlayerInventoryOperations`** — 24 bag + 1 equipment slots, stack rules, `V005` migration ([NEO-54](../../plans/NEO-54-implementation-plan.md)). **NEO-55 landed:** **`GET`/`POST /game/players/{id}/inventory`** — `PlayerInventoryApi` + DTOs in `Game/Items/` ([NEO-55](../../plans/NEO-55-implementation-plan.md)); [server README — Player inventory (NEO-54 store, NEO-55 HTTP)](../../../server/README.md#player-inventory-neo-54-store-neo-55-http); Bruno `bruno/neon-sprawl-server/inventory/`. **NEO-56 landed:** comment-only **`item_created`** / **`inventory_transfer_denied`** telemetry hook sites in [`PlayerInventoryOperations`](../../../server/NeonSprawl.Server/Game/Items/PlayerInventoryOperations.cs) ([NEO-56](../../plans/NEO-56-implementation-plan.md), [`NEO-56` manual QA](../../manual-qa/NEO-56.md)); no E9.M1 ingest. **NEO-72 landed:** client inventory HUD — **`inventory_client.gd`**, **`item_definitions_client.gd`**, **`InventoryLabel`**, boot hydrate + **`I`** refresh ([NEO-72](../../plans/NEO-72-implementation-plan.md), [`NEO-72` manual QA](../../manual-qa/NEO-72.md)); `client/README.md` inventory HUD section. **Slice 5 client (remaining):** capstone [NEO-75](https://linear.app/neon-sprawl/issue/NEO-75) — [E3S5 backlog](../../plans/E3S5-client-prototype-backlog.md). | [NEO-50](../../plans/NEO-50-implementation-plan.md), [NEO-51](../../plans/NEO-51-implementation-plan.md), [NEO-52](../../plans/NEO-52-implementation-plan.md), [NEO-53](../../plans/NEO-53-implementation-plan.md), [NEO-54](../../plans/NEO-54-implementation-plan.md), [NEO-55](../../plans/NEO-55-implementation-plan.md), [NEO-56](../../plans/NEO-56-implementation-plan.md), [NEO-72](../../plans/NEO-72-implementation-plan.md), [E3S5-client-prototype-backlog](../../plans/E3S5-client-prototype-backlog.md), [E3M3-prototype-backlog](../../plans/E3M3-prototype-backlog.md), [E3_M3](E3_M3_ItemizationAndInventorySchema.md) | -| E3.M2 | Ready | **NEO-65 landed:** frozen prototype eight-recipe catalog in [`content/recipes/prototype_recipes.json`](../../../content/recipes/prototype_recipes.json); [`recipe-def.schema.json`](../../../content/schemas/recipe-def.schema.json) + [`recipe-io-row.schema.json`](../../../content/schemas/recipe-io-row.schema.json); PR gate + [`validate_content.py`](../../../scripts/validate_content.py) Slice 3 gates ([NEO-65](../../plans/NEO-65-implementation-plan.md)). **NEO-66 landed:** fail-fast server load of `content/recipes/*_recipes.json` at startup — `server/NeonSprawl.Server/Game/Crafting/` ([NEO-66](../../plans/NEO-66-implementation-plan.md)); [server README — Recipe catalog](../../../server/README.md#recipe-catalog-contentrecipes-neo-66). **NEO-67 landed:** injectable **`IRecipeDefinitionRegistry`** + lookup tests ([NEO-67](../../plans/NEO-67-implementation-plan.md)). **NEO-68 landed:** **`GET /game/world/recipe-definitions`** — `RecipeDefinitionsWorldApi` + DTOs in `Game/Crafting/` ([NEO-68](../../plans/NEO-68-implementation-plan.md), [`NEO-68` manual QA](../../manual-qa/NEO-68.md)); [server README — Recipe definitions (NEO-68)](../../../server/README.md#recipe-definitions-neo-68); Bruno `bruno/neon-sprawl-server/recipe-definitions/`. **NEO-69 landed:** **`CraftOperations.TryCraft`** + **`CraftResult`** — pre-flight inputs/outputs, inventory mutation, refine XP ([NEO-69](../../plans/NEO-69-implementation-plan.md)); [server README — Craft engine (NEO-69)](../../../server/README.md#craft-engine-neo-69-and-craft-http-neo-70). **NEO-70 landed:** **`POST /game/players/{id}/craft`** — `PlayerCraftApi` + wire **`CraftResponse`**; Bruno gather→refine→make spine ([NEO-70](../../plans/NEO-70-implementation-plan.md)); Bruno `bruno/neon-sprawl-server/craft/`. **NEO-71 landed:** comment-only **`item_crafted`** / **`craft_failed`** telemetry hook sites in **`CraftOperations.TryCraft`** ([NEO-71](../../plans/NEO-71-implementation-plan.md)); [server README — Craft telemetry hooks (NEO-71)](../../../server/README.md#craft-telemetry-hooks-neo-71). **NEO-42 landed (prep):** **`RefineActivitySkillXpGrant`** / **`RefineSkillXpConstants`** — wired from craft engine success path. Epic 3 Slice 3 server backlog **E3M2-01**–**E3M2-07** complete. Upstream: E3.M1 **Ready**, E3.M3 inventory landed. **NEO-74 landed:** client craft UI — **`recipe_definitions_client.gd`**, **`craft_client.gd`**, **`craft_recipe_panel.gd`**, **`CraftFeedbackLabel`**, **refine** skill row ([NEO-74](../../plans/NEO-74-implementation-plan.md), [`NEO-74` manual QA](../../manual-qa/NEO-74.md)); `client/README.md` craft section. **Slice 5 client (remaining):** capstone playable loop [NEO-75](https://linear.app/neon-sprawl/issue/NEO-75) — [E3S5 backlog](../../plans/E3S5-client-prototype-backlog.md). | [NEO-42](../../plans/NEO-42-implementation-plan.md), [NEO-65](../../plans/NEO-65-implementation-plan.md), [NEO-66](../../plans/NEO-66-implementation-plan.md), [NEO-67](../../plans/NEO-67-implementation-plan.md), [NEO-68](../../plans/NEO-68-implementation-plan.md), [NEO-69](../../plans/NEO-69-implementation-plan.md), [NEO-70](../../plans/NEO-70-implementation-plan.md), [NEO-71](../../plans/NEO-71-implementation-plan.md), [NEO-74](../../plans/NEO-74-implementation-plan.md) … [NEO-75](https://linear.app/neon-sprawl/issue/NEO-75), [E3M2-prototype-backlog](../../plans/E3M2-prototype-backlog.md), [E3S5-client-prototype-backlog](../../plans/E3S5-client-prototype-backlog.md), [E3_M2](E3_M2_RefinementAndRecipeExecution.md) | +| E3.M1 | Ready | **NEO-41 landed (prototype, superseded on interact by NEO-63):** inline XP on **`resource_node`** interact. **NEO-57–NEO-61** — content, registry, HTTP defs, depletion store (see prior alignment). **NEO-62 landed:** **`GatherOperations`** + **`GatherResult`**. **NEO-63 landed:** **`POST …/interact`** **`resource_node`** → gather engine; **four** **`PrototypeInteractableRegistry`** anchors; Bruno gather → inventory + depletion deny ([NEO-63](../../plans/NEO-63-implementation-plan.md), [`NEO-63` manual QA](../../manual-qa/NEO-63.md)); [server README — Gather via interact (NEO-63)](../../../server/README.md#gather-via-interact-neo-63). **NEO-64 landed:** comment-only **`resource_gathered`** / **`gather_node_depleted`** hook sites in **`GatherOperations.TryGather`** ([NEO-64](../../plans/NEO-64-implementation-plan.md), [`NEO-64` manual QA](../../manual-qa/NEO-64.md)); Epic 3 Slice 2 backlog **E3M1-01**–**E3M1-08** complete. **NEO-73 landed:** client gather feedback — **`skill_progression_client.gd`**, **`prototype_interactable_picker.gd`** (nearest in-range **R**), **`GatherFeedbackLabel`** + **`SkillProgressionLabel`**, auto inventory/skill refresh after successful gather ([NEO-73](../../plans/NEO-73-implementation-plan.md), [`NEO-73` manual QA](../../manual-qa/NEO-73.md)); `client/README.md` gather section. **NEO-75 landed:** capstone gather→refine→make loop in Godot — [`NEO-75` manual QA](../../manual-qa/NEO-75.md), `client/README.md` end-to-end section; Epic 3 Slice 5 client complete. | [NEO-41](../../plans/NEO-41-implementation-plan.md) … [NEO-64](../../plans/NEO-64-implementation-plan.md), [NEO-73](../../plans/NEO-73-implementation-plan.md), [NEO-75](../../plans/NEO-75-implementation-plan.md), [E3S5-client-prototype-backlog](../../plans/E3S5-client-prototype-backlog.md), [E3_M1](E3_M1_ResourceNodeAndGatherLoop.md); `server/NeonSprawl.Server/Game/Gathering/`, `Game/Interaction/`, `client/scripts/skill_progression_client.gd`, `client/scripts/prototype_interactable_picker.gd` | +| E3.M3 | In Progress | **NEO-50 landed:** frozen prototype six-item catalog in [`content/items/prototype_items.json`](../../../content/items/prototype_items.json); [`item-def.schema.json`](../../../content/schemas/item-def.schema.json); PR gate + [`validate_content.py`](../../../scripts/validate_content.py). **NEO-51 landed:** fail-fast server load of `content/items/*_items.json` at startup — `server/NeonSprawl.Server/Game/Items/` ([NEO-51](../../plans/NEO-51-implementation-plan.md)); [server README — Item catalog](../../../server/README.md#item-catalog-contentitems-neo-51). **NEO-52 landed:** injectable **`IItemDefinitionRegistry`** + lookup tests ([NEO-52](../../plans/NEO-52-implementation-plan.md)). **NEO-53 landed:** **`GET /game/world/item-definitions`** — `ItemDefinitionsWorldApi` + DTOs in `Game/Items/` ([NEO-53](../../plans/NEO-53-implementation-plan.md), [`NEO-53` manual QA](../../manual-qa/NEO-53.md)); [server README — Item definitions (NEO-53)](../../../server/README.md#item-definitions-neo-53); Bruno `bruno/neon-sprawl-server/item-definitions/`. **NEO-54 landed:** **`IPlayerInventoryStore`** + **`PlayerInventoryOperations`** — 24 bag + 1 equipment slots, stack rules, `V005` migration ([NEO-54](../../plans/NEO-54-implementation-plan.md)). **NEO-55 landed:** **`GET`/`POST /game/players/{id}/inventory`** — `PlayerInventoryApi` + DTOs in `Game/Items/` ([NEO-55](../../plans/NEO-55-implementation-plan.md)); [server README — Player inventory (NEO-54 store, NEO-55 HTTP)](../../../server/README.md#player-inventory-neo-54-store-neo-55-http); Bruno `bruno/neon-sprawl-server/inventory/`. **NEO-56 landed:** comment-only **`item_created`** / **`inventory_transfer_denied`** telemetry hook sites in [`PlayerInventoryOperations`](../../../server/NeonSprawl.Server/Game/Items/PlayerInventoryOperations.cs) ([NEO-56](../../plans/NEO-56-implementation-plan.md), [`NEO-56` manual QA](../../manual-qa/NEO-56.md)); no E9.M1 ingest. **NEO-72 landed:** client inventory HUD — **`inventory_client.gd`**, **`item_definitions_client.gd`**, **`InventoryLabel`**, boot hydrate + **`I`** refresh ([NEO-72](../../plans/NEO-72-implementation-plan.md), [`NEO-72` manual QA](../../manual-qa/NEO-72.md)); `client/README.md` inventory HUD section. **NEO-75 landed:** **`prototype_economy_hud_section.gd`** collapse toggle + capstone manual QA ([NEO-75](../../plans/NEO-75-implementation-plan.md), [`NEO-75` manual QA](../../manual-qa/NEO-75.md)); Epic 3 Slice 5 client complete. | [NEO-50](../../plans/NEO-50-implementation-plan.md), [NEO-51](../../plans/NEO-51-implementation-plan.md), [NEO-52](../../plans/NEO-52-implementation-plan.md), [NEO-53](../../plans/NEO-53-implementation-plan.md), [NEO-54](../../plans/NEO-54-implementation-plan.md), [NEO-55](../../plans/NEO-55-implementation-plan.md), [NEO-56](../../plans/NEO-56-implementation-plan.md), [NEO-72](../../plans/NEO-72-implementation-plan.md), [NEO-75](../../plans/NEO-75-implementation-plan.md), [E3S5-client-prototype-backlog](../../plans/E3S5-client-prototype-backlog.md), [E3M3-prototype-backlog](../../plans/E3M3-prototype-backlog.md), [E3_M3](E3_M3_ItemizationAndInventorySchema.md) | +| E3.M2 | Ready | **NEO-65 landed:** frozen prototype eight-recipe catalog in [`content/recipes/prototype_recipes.json`](../../../content/recipes/prototype_recipes.json); [`recipe-def.schema.json`](../../../content/schemas/recipe-def.schema.json) + [`recipe-io-row.schema.json`](../../../content/schemas/recipe-io-row.schema.json); PR gate + [`validate_content.py`](../../../scripts/validate_content.py) Slice 3 gates ([NEO-65](../../plans/NEO-65-implementation-plan.md)). **NEO-66 landed:** fail-fast server load of `content/recipes/*_recipes.json` at startup — `server/NeonSprawl.Server/Game/Crafting/` ([NEO-66](../../plans/NEO-66-implementation-plan.md)); [server README — Recipe catalog](../../../server/README.md#recipe-catalog-contentrecipes-neo-66). **NEO-67 landed:** injectable **`IRecipeDefinitionRegistry`** + lookup tests ([NEO-67](../../plans/NEO-67-implementation-plan.md)). **NEO-68 landed:** **`GET /game/world/recipe-definitions`** — `RecipeDefinitionsWorldApi` + DTOs in `Game/Crafting/` ([NEO-68](../../plans/NEO-68-implementation-plan.md), [`NEO-68` manual QA](../../manual-qa/NEO-68.md)); [server README — Recipe definitions (NEO-68)](../../../server/README.md#recipe-definitions-neo-68); Bruno `bruno/neon-sprawl-server/recipe-definitions/`. **NEO-69 landed:** **`CraftOperations.TryCraft`** + **`CraftResult`** — pre-flight inputs/outputs, inventory mutation, refine XP ([NEO-69](../../plans/NEO-69-implementation-plan.md)); [server README — Craft engine (NEO-69)](../../../server/README.md#craft-engine-neo-69-and-craft-http-neo-70). **NEO-70 landed:** **`POST /game/players/{id}/craft`** — `PlayerCraftApi` + wire **`CraftResponse`**; Bruno gather→refine→make spine ([NEO-70](../../plans/NEO-70-implementation-plan.md)); Bruno `bruno/neon-sprawl-server/craft/`. **NEO-71 landed:** comment-only **`item_crafted`** / **`craft_failed`** telemetry hook sites in **`CraftOperations.TryCraft`** ([NEO-71](../../plans/NEO-71-implementation-plan.md)); [server README — Craft telemetry hooks (NEO-71)](../../../server/README.md#craft-telemetry-hooks-neo-71). **NEO-42 landed (prep):** **`RefineActivitySkillXpGrant`** / **`RefineSkillXpConstants`** — wired from craft engine success path. Epic 3 Slice 3 server backlog **E3M2-01**–**E3M2-07** complete. Upstream: E3.M1 **Ready**, E3.M3 inventory landed. **NEO-74 landed:** client craft UI — **`recipe_definitions_client.gd`**, **`craft_client.gd`**, **`craft_recipe_panel.gd`**, **`CraftFeedbackLabel`**, **refine** skill row ([NEO-74](../../plans/NEO-74-implementation-plan.md), [`NEO-74` manual QA](../../manual-qa/NEO-74.md)); `client/README.md` craft section. **NEO-75 landed:** capstone playable gather→refine→make loop — [`NEO-75` manual QA](../../manual-qa/NEO-75.md), **`prototype_economy_hud_section.gd`**; Epic 3 Slice 5 client complete. | [NEO-42](../../plans/NEO-42-implementation-plan.md), [NEO-65](../../plans/NEO-65-implementation-plan.md), [NEO-66](../../plans/NEO-66-implementation-plan.md), [NEO-67](../../plans/NEO-67-implementation-plan.md), [NEO-68](../../plans/NEO-68-implementation-plan.md), [NEO-69](../../plans/NEO-69-implementation-plan.md), [NEO-70](../../plans/NEO-70-implementation-plan.md), [NEO-71](../../plans/NEO-71-implementation-plan.md), [NEO-74](../../plans/NEO-74-implementation-plan.md), [NEO-75](../../plans/NEO-75-implementation-plan.md), [E3M2-prototype-backlog](../../plans/E3M2-prototype-backlog.md), [E3S5-client-prototype-backlog](../../plans/E3S5-client-prototype-backlog.md), [E3_M2](E3_M2_RefinementAndRecipeExecution.md) | --- diff --git a/docs/manual-qa/NEO-75.md b/docs/manual-qa/NEO-75.md new file mode 100644 index 0000000..d7c43b0 --- /dev/null +++ b/docs/manual-qa/NEO-75.md @@ -0,0 +1,47 @@ +# NEO-75 — Manual QA checklist + +| Field | Value | +|-------|-------| +| Key | NEO-75 | +| Title | E3: Playable gather→refine→make loop in client (E3S5-04) | +| Linear | https://linear.app/neon-sprawl/issue/NEO-75/e3-playable-gatherrefinemake-loop-in-client-e3s5-04 | +| Plan | `docs/plans/NEO-75-implementation-plan.md` | +| Branch | `NEO-75-e3-playable-gather-refine-make-loop` | + +## Preconditions + +- **Fresh dev player:** stop any running server, then start a new instance so in-memory **`dev-local-1`** inventory, skills, and resource-node depletion reset. +- **No Bruno/curl seeding** for this checklist — all materials come from in-client gather (**R**). +- NEO-72 inventory HUD, NEO-73 gather feedback, NEO-74 craft UI landed on the same branch or `main`. + +## Material math + +| Step | Action | Scrap | Plates | Result | +|------|--------|-------|--------|--------| +| Gather ×4 | **R** at four anchors (below) | **+11** | — | 11 scrap | +| Refine #1 | Craft **`refine_scrap_standard`** | −5 | +1 | 6 scrap, 1 plate | +| Refine #2 | Craft **`refine_scrap_standard`** | −5 | +1 | 1 scrap, 2 plates | +| Make | Craft **`make_field_stim_mk0`** | −1 | −2 | **`field_stim_mk0` ×1** | + +## Checklist + +1. Start server: `cd server/NeonSprawl.Server && dotnet run`. +2. Run Godot main scene (**F5**). Confirm **`Economy HUD`** toggle is **on**; **`InventoryLabel`** shows empty bag; **`SkillProgressionLabel`** shows **`salvage`** and **`refine`** rows. +3. **WASD** to **`prototype_resource_node_alpha`** anchor **(12, −6)**. Press **R** once. **`GatherFeedbackLabel`** shows **`+1`** scrap delta; inventory scrap updates without **I**; **`salvage`** xp increases. +4. Walk to **`prototype_subsurface_vein_beta`** **(18, −6)**; press **R** (**+2** scrap). Repeat at **`prototype_bio_mat_gamma`** **(12, 0)** (**+3**) and **`prototype_urban_bulk_delta`** **(18, 0)** (**+5**). Inventory shows **11** scrap total. +5. In the recipe panel, click **Craft** on **`refine_scrap_standard`**. **`CraftFeedbackLabel`** shows success; inventory shows **6** scrap and **1** **`refined_plate_stock`**; **`refine`** xp increases. +6. Click **Craft** on **`refine_scrap_standard`** again. Inventory shows **1** scrap and **2** plates. +7. Click **Craft** on **`make_field_stim_mk0`**. Success feedback shows **`field_stim_mk0`** output; **`InventoryLabel`** lists **`Field Stim Mk0`** (or agreed display name) with quantity **≥ 1**; **`refine`** xp increases again. +8. Uncheck **`Economy HUD`** — inventory, skills, and recipe panel hide; gather/craft feedback labels above remain visible. Check **`Economy HUD`** again — panel repopulates with current data. + +## Notes + +- **R** posts the **nearest in-range** `resource_node`; walk to each anchor before pressing **R** (see [NEO-73 manual QA](NEO-73.md) for anchor coords). +- Re-run this checklist after a **server restart** (nodes deplete; inventory persists only in memory). +- Combat **use** of **`field_stim_mk0`** is out of scope (E5 follow-up). + +## Acceptance + +- [ ] Steps 1–8 completable in one session without Bruno/curl. +- [ ] **`field_stim_mk0`** quantity **≥ 1** in inventory at step 7. +- [ ] **Salvage** xp moved after gather; **refine** xp moved after each successful craft. diff --git a/docs/plans/E3S5-client-prototype-backlog.md b/docs/plans/E3S5-client-prototype-backlog.md index 751206e..7008a63 100644 --- a/docs/plans/E3S5-client-prototype-backlog.md +++ b/docs/plans/E3S5-client-prototype-backlog.md @@ -150,9 +150,10 @@ Working backlog for **Epic 3 — Slice 5** ([client economy integration](../deco **Acceptance criteria** -- [ ] Manual QA checklist completable by a human in one session with server + client running. -- [ ] **`field_stim_mk0`** quantity ≥ 1 in inventory at end of script. -- [ ] Epic 3 Slice 3 acceptance re-read: **player** (not Bruno) completes gather → refine → usable item. +- [x] Manual QA checklist completable by a human in one session with server + client running. +- [x] **`field_stim_mk0`** quantity ≥ 1 in inventory at end of script. +- [x] Epic 3 Slice 3 acceptance re-read: **player** (not Bruno) completes gather → refine → usable item. +- [x] Economy HUD collapse toggle functional ([`NEO-75` manual QA](../manual-qa/NEO-75.md) step 8). --- diff --git a/docs/plans/NEO-75-implementation-plan.md b/docs/plans/NEO-75-implementation-plan.md new file mode 100644 index 0000000..a5b351b --- /dev/null +++ b/docs/plans/NEO-75-implementation-plan.md @@ -0,0 +1,146 @@ +# NEO-75 — Implementation plan + +## Story reference + +| Field | Value | +|--------|--------| +| **Key** | NEO-75 | +| **Title** | E3: Playable gather→refine→make loop in client (E3S5-04) | +| **Linear** | https://linear.app/neon-sprawl/issue/NEO-75/e3-playable-gatherrefinemake-loop-in-client-e3s5-04 | +| **Module** | Epic 3 **Slice 5 capstone** — [E3.M1](../decomposition/modules/E3_M1_ResourceNodeAndGatherLoop.md) + [E3.M2](../decomposition/modules/E3_M2_RefinementAndRecipeExecution.md) + [E3.M3](../decomposition/modules/E3_M3_ItemizationAndInventorySchema.md) · backlog **E3S5-04** | +| **Branch** | `NEO-75-e3-playable-gather-refine-make-loop` | +| **Server deps** | NEO-63 gather interact, NEO-70 craft POST, NEO-55 inventory GET, NEO-37 skill-progression GET (**Done** on `main`) | +| **Client deps** | [NEO-72](https://linear.app/neon-sprawl/issue/NEO-72) inventory HUD (**Done**); [NEO-73](https://linear.app/neon-sprawl/issue/NEO-73) gather feedback (**Done**); [NEO-74](https://linear.app/neon-sprawl/issue/NEO-74) craft UI (**Done**) | +| **Pattern** | Capstone integration — reuse NEO-72–74 wiring; docs + manual QA primary; HUD collapse per kickoff | + +## Kickoff clarifications + +| Topic | Question | Agent recommendation | Answer | +|--------|----------|----------------------|--------| +| **Implementation scope** | Docs-only vs code changes? | **Docs + manual QA first**; add code only if QA fails — loop already wired in NEO-72–74. | **User:** docs + manual QA first; code only if QA fails. | +| **HUD collapse** | Build toggle now or defer? | **Defer** — optional per backlog; left scroll usable for capstone. | **User:** **implement** collapsible HUD sections in this story. | +| **Capstone session start** | Fresh player without Bruno? | **Restart server** (in-memory dev store resets) before Godot **F5** — no curl/Bruno seeding. | **Adopted** (Linear AC + kickoff default). | +| **Progression gate smoke** | Filter recipes by skill level? | **Visible XP only** — salvage/refine rows refresh after gather/craft; no recipe gating UI (explicit NEO-74 out-of-scope). | **Adopted** (decomposition default). | + +## Goal, scope, and out-of-scope + +**Goal:** Prove Epic 3 prototype acceptance **in Godot**: gather **`scrap_metal_bulk`** → **`refine_scrap_standard`** → **`make_field_stim_mk0`** without Bruno; satisfies vision **crafting loop** + **progression gate** smoke test. + +**In scope (from Linear + [E3S5-04](E3S5-client-prototype-backlog.md#e3s5-04--playable-gatherrefinemake-loop-in-client-capstone)):** + +- **`docs/manual-qa/NEO-75.md`**: numbered **single-session** capstone script — fresh dev player, four-node gather route, two refines + one make, final **`field_stim_mk0`** assertion; **no Bruno/curl** steps. +- **`client/README.md`**: **End-to-end economy loop** section — integration checklist (boot → **R** gather → craft panel → inventory/skill refresh chain); input keys; material math (11 scrap minimum). +- **Collapsible economy HUD**: toggle to hide/show **`InventoryLabel`**, **`SkillProgressionLabel`**, **`CraftRecipePanel`** block so prototype scene stays playable on smaller viewports. +- **Module alignment** (on story completion): update [`documentation_and_implementation_alignment.md`](../decomposition/modules/documentation_and_implementation_alignment.md) E3.M1/M2/M3 rows + Epic 3 Slice 5 “complete” note in [`epic_03_crafting_economy.md`](../decomposition/epics/epic_03_crafting_economy.md). +- **Integration fixes only if capstone QA fails** on current `main` wiring (unexpected deny, refresh gap, etc.). + +**Out of scope (from Linear):** + +- Quest UI (E7); combat consumable **use** of **`field_stim_mk0`** (E5 follow-up); art pass. +- New HTTP routes or economy clients; recipe quantity spinner; station placement. +- Bruno-only verification as prototype-complete proof. + +## Acceptance criteria checklist + +- [x] Manual QA checklist completable by a human in **one session** with server + client running (**`docs/manual-qa/NEO-75.md`**). +- [x] **`field_stim_mk0`** quantity ≥ **1** in inventory at end of script. +- [x] Epic 3 Slice 3 AC re-read satisfied in Godot: player (not Bruno) completes **gather → refine → usable item**; **salvage** / **refine** XP visible after actions. +- [x] Economy HUD collapse toggle documented and functional. + +## Implementation reconciliation (shipped) + +- **HUD:** `prototype_economy_hud_section.gd` — **`Economy HUD`** checkbox collapses inventory/skills/recipe panel under **`UICanvas/EconomyHudSection/Body/`**. +- **Docs:** [`NEO-75` manual QA](../manual-qa/NEO-75.md) capstone script (zero Bruno); `client/README.md` end-to-end economy loop section. +- **Tests:** `prototype_economy_hud_section_test.gd` — toggle visibility. +- **Alignment:** E3.M1/M2/M3 rows + E3S5-04 backlog AC updated for Slice 5 client complete. +- **Integration:** No `main.gd` logic changes beyond `@onready` path updates for reparented HUD nodes. + +## Technical approach + +### 1. Capstone manual QA script (`docs/manual-qa/NEO-75.md`) + +**Preconditions:** Stop any running server; start fresh (`dotnet run`) so in-memory **`dev-local-1`** inventory/skills/nodes reset. Godot **F5** with no prior session curl seeding. + +**Material math** (from [`content/recipes/prototype_recipes.json`](../../content/recipes/prototype_recipes.json)): + +| Step | Action | Scrap consumed | Plates gained | Scrap remaining | +|------|--------|----------------|---------------|-----------------| +| Gather ×4 | **R** at alpha (+1), beta (+2), gamma (+3), delta (+5) | — | — | **11** | +| Refine #1 | Craft **`refine_scrap_standard`** | 5 | +1 | **6** | +| Refine #2 | Craft **`refine_scrap_standard`** | 5 | +1 | **1** (+ **2** plates) | +| Make | Craft **`make_field_stim_mk0`** | 1 | −2 plates | **0** (+ **`field_stim_mk0` ×1**) | + +**Checklist outline:** + +1. Start server + client; confirm empty bag and skill rows on HUD. +2. Walk each resource anchor (coords from [NEO-73 manual QA](../manual-qa/NEO-73.md)); **R** once per node — verify **`GatherFeedbackLabel`**, inventory scrap, **salvage** XP without **I**. +3. Craft **`refine_scrap_standard`** twice; verify **`CraftFeedbackLabel`**, **refine** XP, plate stock. +4. Craft **`make_field_stim_mk0`**; verify consumable in **`InventoryLabel`**. +5. Optional: collapse economy HUD, expand again — panel still populated. + +### 2. HUD collapse (`prototype_economy_hud_section.gd`) + +- Add **`UICanvas/EconomyHudSection`** (`VBoxContainer` + script): + - Header row: **`CheckButton`** text **`Economy HUD`** (default **pressed** / expanded). + - Body **`VBoxContainer`**: reparent existing **`InventoryLabel`**, **`SkillProgressionLabel`**, **`CraftRecipePanel`** from flat **`UICanvas`** (remove fixed **`offset_top`** stacking — use container layout). +- **`toggled(pressed)`** → set body **`visible = pressed`**; header stays visible. +- **`main.gd`**: no signal rewiring — `@onready` paths update to new hierarchy (or export node refs unchanged if reparent preserves names). +- Document toggle in **`client/README.md`** and capstone manual QA step. + +**Keep expanded by default** so capstone QA matches NEO-72–74 expectations; collapse is optional UX for viewport space. + +### 3. README integration checklist + +New **`## End-to-end economy loop (NEO-75)`** section after NEO-74 craft subsection: + +- Flow diagram in prose: boot GETs → **R** nearest resource node → gather feedback + inventory/salvage refresh → scroll craft panel → **Craft** → craft feedback + inventory/refine refresh. +- Cross-links: NEO-72 (**I**), NEO-73 (**R** + anchors), NEO-74 (recipes). +- Pointer to **`docs/manual-qa/NEO-75.md`** as authoritative capstone script. + +### 4. Module alignment (implementation batch or story end) + +When acceptance criteria pass: + +- **`documentation_and_implementation_alignment.md`**: E3.M1/M2/M3 — note **NEO-75 landed**, Epic 3 Slice 5 client capstone complete. +- **`epic_03_crafting_economy.md`**: Slice 5 AC checkboxes / status if not already marked. + +### 5. Integration fixes (conditional) + +Run capstone QA on **`main`** wiring before collapse refactor; if any step fails, fix minimal **`main.gd`** / client bug in this branch and note in plan **Decisions**. Expected path: **no logic changes** beyond HUD layout. + +## Files to add + +| Path | Purpose | +|------|---------| +| `docs/manual-qa/NEO-75.md` | Capstone single-session manual QA (zero Bruno). | +| `client/scripts/prototype_economy_hud_section.gd` | Collapse toggle + body visibility for economy HUD block. | +| `client/scripts/prototype_economy_hud_section.gd.uid` | Godot UID companion (same commit as `.gd`). | +| `client/test/prototype_economy_hud_section_test.gd` | GdUnit: toggle hides/shows body container. | + +## Files to modify + +| Path | Rationale | +|------|-----------| +| `client/scenes/main.tscn` | Add **`EconomyHudSection`**; reparent inventory/skills/craft panel under collapsible body. | +| `client/scripts/main.gd` | Update `@onready` node paths if hierarchy changes; wire collapse only if script needs refs from root. | +| `client/README.md` | End-to-end economy loop section + economy HUD toggle note. | +| `docs/decomposition/modules/documentation_and_implementation_alignment.md` | Record NEO-75 / Slice 5 capstone complete when shipped. | +| `docs/decomposition/epics/epic_03_crafting_economy.md` | Slice 5 prototype client completion note (if needed). | + +## Tests + +| File | Coverage | +|------|----------| +| **`client/test/prototype_economy_hud_section_test.gd`** (add) | **`# Arrange`**: scene with section + body child. **`# Act`**: toggle **`CheckButton`**. **`# Assert`**: body **`visible`** matches pressed state. | +| Existing NEO-72–74 client tests | No change expected — capstone reuses their clients; run suite in CI. | + +**No `main.gd` automated test** — orchestration verified via capstone manual QA (same policy as NEO-72–74). + +## Open questions / risks + +| Question / risk | Agent recommendation | Status | +|-----------------|---------------------|--------| +| **Loop already works on `main`** | Run capstone QA before collapse refactor; expect pass. | **adopted** — no integration logic changes | +| **HUD reparent breaks `@onready` paths** | Update paths in **`main.gd`** + **`craft_recipe_panel.gd`** exports in same commit as **`main.tscn`**. | **adopted** | +| **Depleted nodes block repeat QA** | Capstone script uses **one R per node** (11 scrap); delta node has capacity 10 — single pass sufficient. Re-run requires **server restart**. | **adopted** | +| **Vertical layout shift after collapse container** | Use **`VBoxContainer`** layout instead of absolute **`offset_top`** for economy block only; combat/debug labels above unchanged. | **adopted** | diff --git a/docs/reviews/2026-05-24-NEO-75.md b/docs/reviews/2026-05-24-NEO-75.md new file mode 100644 index 0000000..e8ee7eb --- /dev/null +++ b/docs/reviews/2026-05-24-NEO-75.md @@ -0,0 +1,60 @@ +# Code review — NEO-75 playable gather→refine→make loop (capstone) + +**Date:** 2026-05-24 +**Scope:** Branch `NEO-75-e3-playable-gather-refine-make-loop` · commits `fbae314`–`977bcc3` vs `origin/main` +**Base:** `origin/main` + +## Verdict + +**Approve with nits** — capstone docs, HUD collapse, and path updates match the adopted plan; GdUnit passes. Merge after human capstone manual QA; minor module/README doc consistency nits below. + +## Summary + +The branch completes Epic 3 Slice 5 capstone integration: collapsible **`EconomyHudSection`** wraps inventory/skills/recipe panel under a **`Economy HUD`** checkbox, **`main.gd`** `@onready` paths follow the reparent, and a focused **`prototype_economy_hud_section.gd`** script toggles body visibility. Docs deliver the zero-Bruno capstone script (`docs/manual-qa/NEO-75.md`), README end-to-end loop section, implementation plan reconciliation, alignment register updates, and E3S5-04 backlog checkboxes. No new HTTP routes or economy client logic — the gather→refine→make loop reuses NEO-72–74 wiring as intended. Risk is low: layout-only client change plus documentation; automated coverage is the HUD toggle suite only (orchestration remains manual QA per plan). + +## Documentation checked + +| Document | Result | +|----------|--------| +| [`docs/plans/NEO-75-implementation-plan.md`](../plans/NEO-75-implementation-plan.md) | **Matches** — kickoff decisions (docs-first, implement HUD collapse, fresh server session, visible XP only) reflected; acceptance checklist and reconciliation complete. | +| [`docs/plans/E3S5-client-prototype-backlog.md`](../plans/E3S5-client-prototype-backlog.md) · **E3S5-04** | **Matches** — capstone manual QA, README integration, optional HUD collapse, alignment cross-links; AC checked. | +| [`docs/decomposition/modules/E3_M2_RefinementAndRecipeExecution.md`](../decomposition/modules/E3_M2_RefinementAndRecipeExecution.md) | **Matches** — status notes NEO-74 + NEO-75 landed. | +| [`docs/decomposition/modules/E3_M1_ResourceNodeAndGatherLoop.md`](../decomposition/modules/E3_M1_ResourceNodeAndGatherLoop.md) | **Matches** — Status row notes NEO-73 gather feedback + NEO-75 capstone landed. | +| [`docs/decomposition/modules/E3_M3_ItemizationAndInventorySchema.md`](../decomposition/modules/E3_M3_ItemizationAndInventorySchema.md) | **Matches** — Status row notes NEO-72 inventory HUD + NEO-75 economy collapse landed. | +| [`docs/decomposition/modules/documentation_and_implementation_alignment.md`](../decomposition/modules/documentation_and_implementation_alignment.md) | **Matches** — E3.M1/M2/M3 rows note NEO-75 landed and Slice 5 client complete. | +| [`docs/decomposition/epics/epic_03_crafting_economy.md`](../decomposition/epics/epic_03_crafting_economy.md) | **Matches** — Slice 5 **Status** note records NEO-75 capstone landed. | +| [`docs/decomposition/modules/client_server_authority.md`](../decomposition/modules/client_server_authority.md) | **Matches** — no client-side stack math; display-only HUD collapse. | +| [`docs/manual-qa/NEO-75.md`](../manual-qa/NEO-75.md) | **Matches** — single-session script, material math, collapse step; acceptance boxes left for human sign-off (expected). | +| [`client/README.md`](../../client/README.md) | **Matches** — NEO-75 loop section; NEO-72/73/74 HUD paths use **`EconomyHudSection/Body/`** hierarchy. | +| Full-stack epic decomposition | **Matches** — E3S5-04 **client** capstone completes NEO-72→75 chain; Godot manual QA authoritative (not Bruno-only). | + +## Blocking issues + +(none) + +## Suggestions + +1. ~~**Add Slice 5 capstone note to `epic_03_crafting_economy.md`** — Plan §4 called for a Slice 5 completion note when AC pass; alignment register and backlog are updated but the epic page still reads as open-ended. A one-line “Slice 5 client capstone (NEO-75) landed” under Slice 5 would close the loop.~~ Done. + +2. ~~**Refresh E3.M1 / E3.M3 module Status rows** — E3.M2 was updated; E3.M1 and E3.M3 module pages still show pre–Slice 5 status while the alignment register already cites NEO-73/72/75. Mirror the E3.M2 pattern (client slice landed + capstone) for doc parity.~~ Done. + +3. ~~**Align NEO-73 README HUD paths** — Update the gather section to `UICanvas/EconomyHudSection/Body/InventoryLabel` and `…/SkillProgressionLabel` so all three Slice 5 README sections reference the same hierarchy after the reparent.~~ Done (NEO-74 craft HUD paths aligned in the same pass). + +## Nits + +- Nit: **`test_toggle_collapses_body`** drives collapse via direct **`button_pressed = false`** rather than simulating a user click on **`ToggleButton`** — acceptable in headless GdUnit; behavior is covered. +- Nit: **`set_expanded`** and **`_on_toggle_toggled`** duplicate visibility logic — fine at this size; only matters if collapse rules grow (e.g. persist preference). +- ~~Nit: Capstone AC for HUD collapse is checked in the implementation plan but not listed as a separate bullet in E3S5-04 backlog acceptance criteria — optional backlog tweak for traceability.~~ Done — E3S5-04 AC bullet added. + +## Verification + +```bash +# NEO-75 GdUnit suite (from client/) +godot --headless --path . -s res://addons/gdUnit4/bin/GdUnitCmdTool.gd --ignoreHeadlessMode \ + -a res://test/prototype_economy_hud_section_test.gd + +# Full client suite (CI parity) +godot --headless --path . -s res://addons/gdUnit4/bin/GdUnitCmdTool.gd --ignoreHeadlessMode +``` + +Manual (required before claiming capstone complete): follow [`docs/manual-qa/NEO-75.md`](../manual-qa/NEO-75.md) — **fresh server restart**, four-node gather (**11** scrap), two **`refine_scrap_standard`**, one **`make_field_stim_mk0`**, verify **`field_stim_mk0` ≥ 1**, salvage/refine XP movement, and economy HUD collapse/expand with populated panel.