Skip to content

fix(viewer): A-dialogue Beta-gap — #751 + #615 - #923

Merged
100yenadmin merged 1 commit into
mainfrom
fix/a-dialogue-beta-gap-751-615
Jun 15, 2026
Merged

fix(viewer): A-dialogue Beta-gap — #751 + #615#923
100yenadmin merged 1 commit into
mainfrom
fix/a-dialogue-beta-gap-751-615

Conversation

@100yenadmin

@100yenadmin 100yenadmin commented Jun 15, 2026

Copy link
Copy Markdown
Member

Dialogue/Parley felt cluster — v1.0.4 Player-Ready-Beta gap

The Parley header named the lead PC ("Speaking with Cassian") instead of the NPC the party is talking TO — a conversation that named the wrong speaker — and gave no live attitude read. This binds the parley surface to the conversation TARGET and surfaces its disposition. Viewer-side, additive, read-only.

#751 — header shows the PLAYER not the NPC; NPC can switch mid-interaction

  • build_parley_surface gains an additive npc_id param (threaded from ?npc=<id> on /parley-surface).
  • New _parley_npc_block resolves the conversation target, in precedence order:
    1. explicit npc_id — the interlocutor the player opened the conversation with (authoritative; fixes the "switch mid-interaction" half by pinning to one id), then
    2. the live Event's anchor_npc_id — the engine already chose WHO a stumble-into is about.
    • Absent/unknown id → no npc key, degrading exactly like a bad event_id. Today's freeform parley payload is byte-identical.
  • screen-dialogue.jsx renders surface.npc.name in the header (pinned to the bound id) instead of surface.actor. The actor's sheet still drives the skill slots — the header names the target, the slots/portrait stay the speaking PC.

#615 — Disposition meter on the Dialogue screen

  • The same npc block carries the canonical disposition bucket (reusing _attitude_disposition, the predicate the Relations screen already uses) + the raw attitude_value (fine even at 0 → "neutral").
  • screen-dialogue.jsx renders a DispositionDot meter — reused from screen-relations.jsx via window.DispositionDot, no fork — so the player sees their standing while talking.

Invariants honored

  • Read-only: pure projection over snapshot state; no engine-state writes/mutation. The engine remains the sole writer.
  • Additive / round-trips: no npc key without a bound target → old snapshots and the unbound parley are wire-identical.
  • Reuse canonical predicates: _attitude_disposition (server) + DispositionDot (relations screen) — not forked.
  • Wire contract: unchanged when no NPC is bound.

The engine half this binds to already landed (generate_parley_options emits an npc block — see servers/engine/server.py generate_parley_options, tests test_parley.py); this PR is the viewer read + presentation side.

TDD (RED → GREEN)

  • 7 surface tests (viewer/tests/test_readmodel_surfaces.py): npc block shape, explicit-id binding + pin, event-anchor fallback, explicit-wins-over-anchor, unknown-id graceful degrade, disposition band/value (incl. attitude_value 0 → "neutral").
  • 7 component tests (viewer/tests/test_dialogue_npc_header.py, new): render the REAL ParleyMenu under the babel-vm Node harness (mirrors test_chronicle_hygiene) — header names the NPC not the player, name is pinned independent of actor, no-npc keeps the actor header, DispositionDot renders when bound (incl. at 0) and not when unbound.

Verification

  • bash qa/fast_gate.shPASS (221 deterministic engine tests).
  • Parley suites green: viewer/tests/test_readmodel_surfaces.py -k parley (14) + test_dialogue_npc_header.py (7) + engine tests/test_parley.py (47).
  • Pre-existing, unrelated failures in this isolated worktree are engine-load only (_load_engine_server() → None in the local viewer env; bestiary/build_options/save_export/director). They fail identically on the clean baseline and pass under CI's installed engine — untouched here.

Refs #751, #615

Summary by CodeRabbit

  • New Features

    • Dialogue headers now display the specific NPC you're speaking with, with a live Disposition meter showing their attitude. Previously defaulted to the player character.
    • Conversations can be pinned to explicit NPCs via query parameter, overriding event anchors when necessary.
  • Tests

    • Added comprehensive test suite validating NPC-bound dialogue headers, disposition meter rendering, and fallback behaviors for free-form conversations.

The Parley header named the lead PC ("Speaking with <PLAYER>") instead of the
NPC the party is talking TO — a conversation that named the wrong speaker — and
carried no live attitude read. Bind the parley surface to the conversation TARGET
and surface its disposition.

#751 (header names the NPC, pinned):
- build_parley_surface gains an additive `npc_id` param (threaded from `?npc=<id>`).
- New `_parley_npc_block` resolves the target: explicit npc_id wins (the
  interlocutor the player opened the conversation with — fixes the "switch
  mid-interaction" half), else the live Event's anchor_npc_id (the engine already
  chose WHO a stumble-into is about). Absent/unknown id -> no `npc` key (degrades
  exactly like a bad event_id; today's freeform parley is byte-identical).
- screen-dialogue.jsx renders `surface.npc.name` in the header (pinned to the
  bound id) instead of `surface.actor`. The actor's sheet still drives the slots.

#615 (disposition meter on the Dialogue screen):
- The same `npc` block carries the canonical disposition bucket (reusing
  `_attitude_disposition`, the predicate the Relations screen already uses) + the
  raw attitude_value (fine at 0 -> "neutral").
- screen-dialogue.jsx renders a DispositionDot meter (reused from
  screen-relations.jsx via window.DispositionDot) so the player sees their standing
  WHILE talking.

Invariants: viewer stays READ-ONLY (pure projection; no engine-state writes); the
new field is additive (old snapshots round-trip — no npc key without a target); the
wire contract is unchanged when npc is unbound; reuses canonical predicates
(_attitude_disposition, DispositionDot) rather than forking.

TDD: RED tests first (7 surface tests in test_readmodel_surfaces.py for the npc
block + binding precedence + graceful degrade + disposition band/value; 7
component tests in new test_dialogue_npc_header.py rendering the REAL ParleyMenu
under the babel-vm Node harness to assert the header names the NPC not the player
and the DispositionDot meter renders), then GREEN.

Refs #751, #615.
@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: aee27c39-f0ed-498c-b2de-40d6e2cb970e

📥 Commits

Reviewing files that changed from the base of the PR and between 8c22f0b and e03cddf.

📒 Files selected for processing (4)
  • viewer/openworlds/screen-dialogue.jsx
  • viewer/server.py
  • viewer/tests/test_dialogue_npc_header.py
  • viewer/tests/test_readmodel_surfaces.py

📝 Walkthrough

Walkthrough

The Parley screen gains NPC-binding support. The backend adds _parley_npc_block to resolve a conversation-target NPC from an explicit ?npc query param or a live event anchor, attaches the enriched npc block (id, name, attitude, disposition) to the build_parley_surface response, and the frontend ParleyMenu uses it to show the NPC name in the header and render a live DispositionDot meter.

Changes

Parley NPC Binding and Disposition Meter

Layer / File(s) Summary
Backend NPC block helper, build_parley_surface extension, and route wiring
viewer/server.py, viewer/tests/test_readmodel_surfaces.py
_parley_npc_block resolves the NPC id (explicit npc_id arg, then live_event.anchor_npc_id), builds a disposition-enriched npc dict (id, name, attitude, attitude_value, met, disposition), and returns None for unknown ids. build_parley_surface gains an npc_id: str = "" parameter and conditionally sets base["npc"]. The /parley-surface HTTP handler parses ?npc=<id> and passes it through. Read-model tests cover explicit binding, live-event fallback, precedence (explicit wins), graceful degradation for unknown ids, disposition band/value correctness, and attitude_value == 0 edge case.
ParleyMenu NPC header and DispositionDot rendering
viewer/openworlds/screen-dialogue.jsx, viewer/tests/test_dialogue_npc_header.py
ParleyMenu derives npc and npcName from surface.npc and reads window.DispositionDot. The breadcrumb header displays npcName || actorName and conditionally renders DispositionDot plus signed npc.attitude_value text when the NPC block and the window component are both present. A new Babel+Node VM test harness in test_dialogue_npc_header.py exercises the real JSX component in a stubbed sandbox, asserting NPC-named header (vs. actor-named), fallback actor header when no NPC block, NPC name independence from actor identity, and DispositionDot presence/absence at normal and zero attitude.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant ParleySurfaceRoute
  participant build_parley_surface
  participant _parley_npc_block
  participant ParleyMenu

  Client->>ParleySurfaceRoute: GET /parley-surface?npc=npc_42
  ParleySurfaceRoute->>build_parley_surface: npc_id="npc_42"
  build_parley_surface->>_parley_npc_block: snapshot, npc_id, live_event
  _parley_npc_block-->>build_parley_surface: {id, name, attitude, attitude_value, met, disposition}
  build_parley_surface-->>ParleySurfaceRoute: {actor, npc: {...}, skills, ...}
  ParleySurfaceRoute-->>Client: JSON with npc block
  Client->>ParleyMenu: surface.npc = {name, disposition, attitude_value}
  ParleyMenu-->>Client: header "Parley · Speaking with Aldric" + DispositionDot
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • electricsheephq/WorldOS#161: Introduced the original /parley-surface read model and ParleyMenu component that this PR directly extends with NPC binding and the disposition meter.
  • electricsheephq/WorldOS#365: Previously modified the same "Parley · Speaking with …" breadcrumb header logic in screen-dialogue.jsx that this PR further changes to use the NPC name.

Suggested labels

enhancement

🐇 A rabbit hopped into the parley hall,
Where once only actors would answer the call.
Now NPCs step forward, their name on the sign,
A disposition dot glowing — hostile or fine.
With attitude_value signed and clear,
The snowy-eared coder declares: "They're here!" 🎯

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(viewer): A-dialogue Beta-gap — #751 + #615' clearly indicates it fixes dialogue issues and references both addressed issues.
Description check ✅ Passed The description provides comprehensive details of changes, testing verification, and design invariants honoring the template requirements.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands and usage tips.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant