# NEO-142 — E7M3-10: Client faction standing + gate feedback HUD (Godot) **Linear:** [NEO-142](https://linear.app/neon-sprawl/issue/NEO-142) **Branch:** `NEO-142-client-faction-standing-gate-hud` **Backlog:** [E7M3-pre-production-backlog.md](E7M3-pre-production-backlog.md) — **E7M3-10** **Module:** [E7_M3_FactionReputationLedger.md](../decomposition/modules/E7_M3_FactionReputationLedger.md) **Pattern:** [NEO-122](NEO-122-implementation-plan.md) — quest HUD + accept feedback; [NEO-131](NEO-131-implementation-plan.md) — completion reward label + transition refresh; [NEO-73](NEO-73-implementation-plan.md) / [NEO-139](NEO-139-implementation-plan.md) — snapshot GET client **Precursors:** [NEO-139](https://linear.app/neon-sprawl/issue/NEO-139) **`Done`** — `GET /game/players/{id}/faction-standing`; [NEO-140](https://linear.app/neon-sprawl/issue/NEO-140) **`Done`** — world **`factionGateRules`** + progress **`completionRewardSummary.reputationGrants`** **Blocks:** [NEO-143](https://linear.app/neon-sprawl/issue/NEO-143) — Slice 3 Godot capstone manual QA **Server counterpart:** [NEO-139](https://linear.app/neon-sprawl/issue/NEO-139) + [NEO-140](https://linear.app/neon-sprawl/issue/NEO-140) — authoritative GET projections; Bruno is **not** prototype-complete per [full-stack epic decomposition](../../.cursor/rules/full-stack-epic-decomposition.md) ## Goal Player sees faction standing and accept gate denials in Godot without Bruno. ## Kickoff clarifications | Topic | Question | Agent recommendation | Answer | |-------|----------|----------------------|--------| | **Standing HUD placement** | HudRoot quest stack vs EconomyHudSection? | **HudRoot quest stack** — new **`FactionStandingLabel`** after **`QuestRewardDeliveryLabel`**; keeps quest/rep loop visible with NEO-122/131 labels; economy panel stays inventory/skills/gig. | **Adopted** — HudRoot quest stack | | **Faction display names** | How to label standing/rep lines without `GET /game/world/faction-definitions`? | **Static prototype map** in client matching `content/factions/prototype_factions.json` freeze; fallback title-case **`id`** token. | **Adopted** (no kickoff question — NEO-139 server README: “join from client content”) | | **`faction_gate_blocked` copy** | Raw `reasonCode` vs readable gate detail? | **Readable gate detail** — join cached world quest **`factionGateRules`** for denied quest + current standing from faction snapshot (e.g. “Grid Operators standing 15 required (have 0)”); other reason codes keep NEO-122 `denied — {reasonCode}`. | **Adopted** (meets Linear AC; NEO-140 gate projection landed) | | **Standing refresh** | When to GET faction-standing? | **Boot hydrate** + on **`quest_completion_reward_transition`** (rep grants apply on completion, not accept); coalesce via client pending-sync like skill progression. | **Adopted** (backlog: reuse quest HUD completion signals) | | **Reward label rep lines** | Where to surface **`reputationGrants`**? | **Extend `_format_reward_summary_lines`** in **`quest_hud_controller.gd`** — same transition-only reward label as NEO-131 item/skill lines. | **Adopted** (backlog explicit) | ## Scope and out-of-scope **In scope (from Linear + [E7M3-10](E7M3-pre-production-backlog.md#e7m3-10--client-faction-standing--gate-feedback-hud-godot)):** - **`faction_standing_client.gd`** — GET **`/game/players/{id}/faction-standing`**; parse v1; **`faction_row(faction_id, snapshot)`**; signals **`faction_standing_received`** / **`faction_standing_sync_failed`**; **`request_sync_from_server()`** with pending coalesce. - **`FactionStandingLabel`** in **`HudRoot`** after **`QuestRewardDeliveryLabel`** — both prototype factions with readable names + standing values. - Parse + render refresh after quest completion transitions (wire through **`quest_hud_controller.gd`**). - Surface **`faction_gate_blocked`** on **`QuestAcceptFeedbackLabel`** with readable gate copy (not raw code alone). - Extend **`quest_hud_controller.gd`** reward formatter for **`reputationGrants`** lines in **`completionRewardSummary`**. - Optional helper on **`quest_definitions_client.gd`**: **`faction_gate_rules_for(quest_id)`** reading cached world rows (NEO-140 field passes through existing parse). - GdUnit parse + HUD tests ([testing-expectations.md](../../.cursor/rules/testing-expectations.md)). - **`docs/manual-qa/NEO-142.md`** — component checklist. - **`client/README.md`** faction standing + gate feedback subsection. **Out of scope (from Linear + backlog):** - Final journal UI; faction art pass. - **`GET /game/world/faction-definitions`** client (no backlog item). - Server route/DTO changes. - Full Slice 3 capstone script (**[NEO-143](https://linear.app/neon-sprawl/issue/NEO-143)**). - Periodic faction-standing poll timer. **Server counterpart:** [NEO-139](https://linear.app/neon-sprawl/issue/NEO-139), [NEO-140](https://linear.app/neon-sprawl/issue/NEO-140). ## Acceptance criteria checklist - [x] Standing visible after operator-chain completion (+15 Grid Operators). - [x] Accepting grid contract below rep shows readable deny copy on HUD. ## Implementation reconciliation (shipped) - **`faction_standing_client.gd`:** GET parse v1, prototype display names, `faction_row` / `standing_for`, pending sync. - **`quest_definitions_client.gd`:** `faction_gate_rules_for` helper. - **`quest_hud_controller.gd`:** `FactionStandingLabel` render; boot + completion refresh; readable `faction_gate_blocked` deny via static `FactionStandingClient.display_name_for`; `reputationGrants` reward lines. - **`main.gd` / `main.tscn`:** `FactionStandingClient` + `FactionStandingLabel` wired into quest HUD setup. - **Tests:** `faction_standing_client_test.gd`, `faction_standing_hud_test.gd`, `quest_definitions_client_test.gd` (`faction_gate_rules_for`); `quest_reward_hud_test.gd` rep line assertion. **24** targeted GdUnit tests pass. - **Docs:** `client/README.md`; `docs/manual-qa/NEO-142.md`; E7.M3 module + alignment register + E7M3 backlog updated. - **Manual QA:** `docs/manual-qa/NEO-142.md` — standing +15, rep reward line, gate deny (GdUnit + optional dev fixture for live deny). ### PR notes (bundled branch) This branch includes **NEO-142** client HUD work, **NEO-141** story-end manual QA removal (`docs/manual-qa/NEO-141.md`), and **`chore:`** `RewardRouterOperations` collection-expression cleanup — call out all three in the PR description. ## Technical approach ### 1. Server contracts (landed — NEO-139 / NEO-140) **Faction standing GET** (`schemaVersion` 1): ```json { "schemaVersion": 1, "playerId": "dev-local-1", "factions": [ { "id": "prototype_faction_grid_operators", "standing": 15 }, { "id": "prototype_faction_rust_collective", "standing": 0 } ] } ``` Consumers key rows by **`id`** — array order is not contractual. **World quest definition** (grid contract gate row): ```json { "id": "prototype_quest_grid_contract", "factionGateRules": [ { "factionId": "prototype_faction_grid_operators", "minStanding": 15 } ] } ``` **Completion reward summary** (operator chain rep line): ```json { "reputationGrants": [ { "factionId": "prototype_faction_grid_operators", "amount": 15 } ] } ``` ### 2. `faction_standing_client.gd` Mirror **`skill_progression_client.gd`**: | Concern | Implementation | |---------|----------------| | URL | `{base}/game/players/{dev_player_id}/faction-standing` | | Parse | Require `schemaVersion == 1`, non-empty `playerId`, `factions` array of `{ id, standing }` | | Helpers | **`faction_row(faction_id, snapshot)`**; **`standing_for(faction_id, snapshot) -> int`** default **0** when row missing | | Display names | **`display_name_for(faction_id)`** — static map for two prototype ids; title-case fallback | | Errors | 404 → `"HTTP 404 (player unknown)"`; schema mismatch → `"non-JSON body or schemaVersion mismatch"`; emit **`faction_standing_sync_failed`** | Static display-name map (matches [prototype_factions.json](../../content/factions/prototype_factions.json)): | `id` | Label | |------|-------| | `prototype_faction_grid_operators` | Grid Operators | | `prototype_faction_rust_collective` | Rust Collective | Render order: Grid Operators first, Rust Collective second (catalog freeze order). ### 3. HUD wiring (`main.gd` / `main.tscn`) - Add **`FactionStandingClient`** node sibling to **`QuestProgressClient`**. - Add **`FactionStandingLabel`** after **`QuestRewardDeliveryLabel`** in **`HudRoot`** (user-adopted placement). - Boot: apply authority HTTP config; connect client signals; **`request_sync_from_server()`** on setup. - Extend **`QuestHudController.setup(...)`** with **`faction_standing_client`** + **`faction_standing_label`** (optional params to avoid breaking test harnesses — pass `null` when absent). ### 4. Standing label render (`quest_hud_controller.gd`) Default idle: ``` Faction standing: Grid Operators: 0 Rust Collective: 0 ``` On sync error, append **`error — {reason}`** under header (NEO-122 pattern). On successful snapshot, paint both factions from GET (not local math). **Refresh triggers:** 1. **`setup`** → faction client boot GET (via controller or `main` — prefer controller calling **`request_sync_from_server()`** after connect). 2. **`quest_completion_reward_transition`** → faction GET (standing changes on quest completion rep delivery). 3. Do **not** refresh on accept deny alone (standing unchanged on failed accept). ### 5. Accept gate feedback (`faction_gate_blocked`) In **`_on_accept_result_received`**, when **`accepted == false`** and **`reasonCode == "faction_gate_blocked"`**: 1. Read **`faction_gate_rules_for(quest_id)`** from defs client (first rule sufficient for prototype grid contract). 2. Read current standing from last faction snapshot via **`standing_for(factionId, snapshot)`**. 3. Render e.g. **`Quest accept: denied — Grid Operators standing 15 required (have 0)`**. 4. If gate rules or standing snapshot unavailable, fallback **`Quest accept: denied — faction standing too low (faction_gate_blocked)`**. Other deny codes unchanged: **`Quest accept: denied — {reasonCode}`**. ### 6. Reward label — `reputationGrants` Extend **`_format_reward_summary_lines`** after skill XP block: ```gdscript # reputationGrants: [{ factionId, amount }] lines.append(" Grid Operators +15 rep") ``` Use **`display_name_for(factionId)`** from faction standing client when wired; else title-case **`factionId`**. Update **`quest_reward_hud_test.gd`** (or new suite) for operator-chain transition including rep line. ### 7. Flow ```mermaid sequenceDiagram participant Main participant QuestHud as QuestHudController participant FactionClient as FactionStandingClient participant QuestClient as QuestProgressClient participant Server Main->>QuestHud: setup (quest + faction clients) QuestHud->>FactionClient: request_sync_from_server (boot) FactionClient->>Server: GET faction-standing Server-->>FactionClient: factions[] FactionClient-->>QuestHud: faction_standing_received QuestHud->>QuestHud: render FactionStandingLabel QuestClient->>Server: GET quest-progress (completion) QuestClient-->>QuestHud: quest_progress_received QuestHud->>QuestHud: detect completed transition QuestHud->>FactionClient: request_sync_from_server QuestHud->>QuestHud: render reward (+ reputationGrants) QuestClient->>Server: POST accept grid contract (deny) QuestClient-->>QuestHud: reasonCode faction_gate_blocked QuestHud->>QuestHud: readable gate deny on accept label ``` ## Files to add | Path | Rationale | |------|-----------| | `client/scripts/faction_standing_client.gd` | HTTP GET client for NEO-139 faction-standing snapshot | | `client/scripts/faction_standing_client.gd.uid` | Godot uid companion (tracked with script) | | `client/test/faction_standing_client_test.gd` | GdUnit parse, 404, `faction_row` / `display_name_for`, pending sync | | `client/test/faction_standing_hud_test.gd` | GdUnit standing label render, gate deny copy, completion refresh hook | | `docs/manual-qa/NEO-142.md` | Godot component checklist (standing +15, grid-contract gate deny) | ## Files to modify | Path | Rationale | |------|-----------| | `client/scripts/quest_hud_controller.gd` | Wire faction client/label; standing render; gate deny copy; `reputationGrants` reward lines; refresh faction on completion transition | | `client/scripts/quest_definitions_client.gd` | Add **`faction_gate_rules_for(quest_id)`** helper over cached world rows | | `client/scripts/main.gd` | `@onready` faction client + label; pass into quest HUD **`setup`**; boot HTTP config | | `client/scenes/main.tscn` | **`FactionStandingClient`** node + **`FactionStandingLabel`** after reward label | | `client/test/quest_reward_hud_test.gd` | Assert **`reputationGrants`** line on operator-chain completion transition | | `client/README.md` | Document faction standing GET, label placement, gate feedback, refresh triggers | ## Tests | File | Coverage | |------|----------| | `client/test/faction_standing_client_test.gd` | **Add:** parse valid snapshot; reject bad schema; 404 sync failed; `faction_row` / `standing_for`; prototype display names; pending sync coalesce | | `client/test/faction_standing_hud_test.gd` | **Add:** standing label after snapshot (+15 Grid Operators); sync error line; `faction_gate_blocked` readable deny using mock defs + standing; fallback when rules missing | | `client/test/quest_reward_hud_test.gd` | **Change:** operator-chain completion summary includes **`Grid Operators +15 rep`** line | Manual verification: **`docs/manual-qa/NEO-142.md`** — server + Godot; complete operator chain → standing **15**; accept grid contract before rep → readable deny. ## Open questions / risks | Question / risk | Agent recommendation | Status | |-----------------|----------------------|--------| | Standing label shows **0** before any rep (not hidden until +15) | **Show both factions from boot** — AC scenario is “visible after completion” with **15**, not hidden-until-earned | `adopted` | | Gate deny before operator-chain prerequisite (`prerequisite_incomplete`) | **Keep NEO-122 raw reasonCode** — AC targets **`faction_gate_blocked`** after chain complete | `adopted` | | Faction GET lags quest completion refresh | **Accept brief stale standing** on same frame; completion handler fires faction GET immediately after transition | `adopted` | | `main.tscn` ext_resource churn | Add script ext_resource for faction client; match existing client node pattern | `adopted` | | NEO-143 capstone overlap | NEO-142 = component HUD; NEO-143 = full end-to-end manual QA script | `adopted` |