14 KiB
NEO-142 — E7M3-10: Client faction standing + gate feedback HUD (Godot)
Linear: NEO-142
Branch: NEO-142-client-faction-standing-gate-hud
Backlog: E7M3-pre-production-backlog.md — E7M3-10
Module: E7_M3_FactionReputationLedger.md
Pattern: NEO-122 — quest HUD + accept feedback; NEO-131 — completion reward label + transition refresh; NEO-73 / NEO-139 — snapshot GET client
Precursors: NEO-139 Done — GET /game/players/{id}/faction-standing; NEO-140 Done — world factionGateRules + progress completionRewardSummary.reputationGrants
Blocks: NEO-143 — Slice 3 Godot capstone manual QA
Server counterpart: NEO-139 + NEO-140 — authoritative GET projections; Bruno is not prototype-complete per full-stack epic decomposition
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):
faction_standing_client.gd— GET/game/players/{id}/faction-standing; parse v1;faction_row(faction_id, snapshot); signalsfaction_standing_received/faction_standing_sync_failed;request_sync_from_server()with pending coalesce.FactionStandingLabelinHudRootafterQuestRewardDeliveryLabel— both prototype factions with readable names + standing values.- Parse + render refresh after quest completion transitions (wire through
quest_hud_controller.gd). - Surface
faction_gate_blockedonQuestAcceptFeedbackLabelwith readable gate copy (not raw code alone). - Extend
quest_hud_controller.gdreward formatter forreputationGrantslines incompletionRewardSummary. - 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).
docs/manual-qa/NEO-142.md— component checklist.client/README.mdfaction standing + gate feedback subsection.
Out of scope (from Linear + backlog):
- Final journal UI; faction art pass.
GET /game/world/faction-definitionsclient (no backlog item).- Server route/DTO changes.
- Full Slice 3 capstone script (NEO-143).
- Periodic faction-standing poll timer.
Server counterpart: NEO-139, NEO-140.
Acceptance criteria checklist
- Standing visible after operator-chain completion (+15 Grid Operators).
- 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_forhelper.quest_hud_controller.gd:FactionStandingLabelrender; boot + completion refresh; readablefaction_gate_blockeddeny via staticFactionStandingClient.display_name_for;reputationGrantsreward lines.main.gd/main.tscn:FactionStandingClient+FactionStandingLabelwired 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.gdrep 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):
{
"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):
{
"id": "prototype_quest_grid_contract",
"factionGateRules": [
{ "factionId": "prototype_faction_grid_operators", "minStanding": 15 }
]
}
Completion reward summary (operator chain rep line):
{
"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):
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
FactionStandingClientnode sibling toQuestProgressClient. - Add
FactionStandingLabelafterQuestRewardDeliveryLabelinHudRoot(user-adopted placement). - Boot: apply authority HTTP config; connect client signals;
request_sync_from_server()on setup. - Extend
QuestHudController.setup(...)withfaction_standing_client+faction_standing_label(optional params to avoid breaking test harnesses — passnullwhen 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:
setup→ faction client boot GET (via controller ormain— prefer controller callingrequest_sync_from_server()after connect).quest_completion_reward_transition→ faction GET (standing changes on quest completion rep delivery).- 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":
- Read
faction_gate_rules_for(quest_id)from defs client (first rule sufficient for prototype grid contract). - Read current standing from last faction snapshot via
standing_for(factionId, snapshot). - Render e.g.
Quest accept: denied — Grid Operators standing 15 required (have 0). - 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:
# 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
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 |