neon-sprawl/docs/plans/NEO-142-implementation-plan.md

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.mdE7M3-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 DoneGET /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); 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).
  • 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).
  • 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 (in progress)

  • 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; 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_reward_hud_test.gd rep line assertion.
  • Docs: client/README.md; docs/manual-qa/NEO-142.md.

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 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:

# 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