247 lines
14 KiB
Markdown
247 lines
14 KiB
Markdown
# 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` |
|