Skip to content

fix: barrier wiring + music rotation + XP persistence#141

Merged
slabgorb merged 37 commits intodevelopfrom
fix/session6-playtest-fixes
Mar 29, 2026
Merged

fix: barrier wiring + music rotation + XP persistence#141
slabgorb merged 37 commits intodevelopfrom
fix/session6-playtest-fixes

Conversation

@slabgorb
Copy link
Copy Markdown
Owner

Summary

  • Barrier path wired to full pipeline — removed tokio::spawn, inline await, all 20+ post-narration systems now run for multiplayer barrier turns (combat, HP, tropes, inventory, persistence, music, render, TTS)
  • Music rotation fixed — themes variations (ambient, overture, sparse, tension_build, resolution, full across set-1/set-2) now merged into mood_tracks. Goes from 2 tracks per mood to 14+
  • XP persistence — added xp field to CreatureCore, wired save/restore on reconnect
  • PlayerState sync — xp, inventory, combat_state, chase_state now sync bidirectionally through SharedGameSession
  • MoodContext wiring — in_chase, quest_completed, npc_died read from real state instead of hardcoded false
  • Combatant names — prerender context populated from npcs_present when in combat

Playtest bugs addressed

Test plan

  • Start multiplayer session, verify barrier turns run full pipeline
  • Verify HP changes after combat narration
  • Verify trope activation in GM Mode panel
  • Verify music rotates through different variations
  • Verify XP persists across reconnect

🤖 Generated with Claude Code

slabgorb and others added 30 commits March 29, 2026 05:49
Bug #9: Notify existing players when a new character joins. After player
is inserted into shared session and character data is populated, send
Narration + NarrationEnd to all prior session members: "X has entered
the scene." PARTY_STATUS broadcast follows to update the HUD.

Bug #8: Inject PERSPECTIVE MODE directive into state_summary when other
PCs are present. Narrator uses third-person omniscient — no "you" for
any character. All characters named explicitly. Gates on !other_pcs to
preserve single-player second-person narration.

Bug #7: Barrier TurnContext state_summary now includes the perspective
directive. Combined party actions prompt ends with: "Write in
third-person omniscient. Name all characters explicitly."

narrator.rs: Add constraint handling note — narrator silently respects
game-state constraints without breaking character or acknowledging them.

turn_mode.rs: Add PlayerJoined/PlayerLeft transitions. FreePlay stays
FreePlay on join (sequential turn model). Structured reverts to FreePlay
when player count drops to 1.

shared_session.rs: Add character_class, region_id, display_location to
PlayerState. Add co_located_players(), resolve_region(), load_cartography()
to SharedGameSession for future location-scoped narration routing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
TurnStatusPayload now carries player_name and status fields that the UI
already expects (App.tsx:357-366). Server emits TURN_STATUS "active" to
all players before the THINKING indicator, and "resolved" after
NarrationEnd — both in FreePlay and barrier paths.

This was the missing wire: the UI had the handler, the protocol had the
type, but the server never sent the message. activePlayerName was always
null so isMyTurn was always true.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three bugs with one root cause — narration cross-routing between players:

1. TTS messages (NarrationChunk, TtsStart, TtsEnd, audio cues) used global
   broadcast, so every player received every other player's narration chunks.
   Now routed via session send_to_player to acting player only.

2. Session narration routing used co_located_players() which returned empty
   when cartography regions weren't available (always, for mutant_wasteland).
   Now falls back to all other session members when region data is absent.

3. Writer self-skip filter dropped targeted session messages because it
   checked player_id (set to recipient) against writer_player_id. Now only
   applies self-skip to untargeted broadcasts — targeted messages already
   pass the target filter.

Also removed dead second Narration match arm (unreachable in Rust).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Session channel subscribers may not be initialized when TURN_STATUS is
sent — broadcast::channel drops messages sent before subscription. Switch
all three TURN_STATUS emission points (active, resolved-FreePlay,
resolved-barrier) to global broadcast which reaches all connected clients
immediately.

Root cause of "turn lock still not working": messages were being sent via
session send_to_player but the writer task's session_rx subscription used
try_lock() which silently fails when the session mutex is held by
dispatch_player_action.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ation

When Player A acts, other players now receive a PLAYER_ACTION message
(with "CharName: action text") before the narration broadcast. This
gives NarrativeView the turn boundary marker it needs — PLAYER_ACTION
triggers flushChunks() which creates a visual separator between turns.

Without this, all narration from all players accumulated into one
unbroken text block with no turn separators.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause of 6-attempt turn lock failure: server sent char_name
("Shirley") in TURN_STATUS player_name, but client compared against
connectedPlayerName which is the lobby name ("Keith"). They never
matched, so isMyTurn was wrong for everyone.

Fixed all three emission points (active, resolved-FreePlay,
resolved-barrier) to use player_name_for_save (the lobby name).

Verified end-to-end: lobby input → CONNECT player_name → server
player_name_store → TURN_STATUS player_name → client activePlayerName
→ comparison against connectedPlayerName. Same value at every hop.

Note: PartyMember.name still uses character name, which breaks the
currentPlayerId lookup at App.tsx:611 (affects YOU badge and ACTING
indicator, not the turn lock itself). Separate fix needed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PartyMember.name now carries the player's lobby name (for identity
matching against connectedPlayerName on the client). New character_name
field carries the in-game character name (for display in party panel).

This fixes the currentPlayerId lookup at App.tsx:611 which searched
partyMembers by lobby name but found character names. Without this,
the YOU badge, ACTING indicator, and activePlayerId derivation all
returned null.

Updated all 7 PartyMember construction sites in lib.rs plus state.rs
and protocol tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ction

- Character struct: new `pronouns: String` field with #[serde(default)]
- MechanicalEffects: new `pronoun_hint: Option<String>` for genre pack YAML
- CharacterBuilder: accumulates pronoun_hint → Character.pronouns
- Narrator prompt: injects "Pronouns: she/her — ALWAYS use these pronouns"
- Updated all test Character/MechanicalEffects constructions

Genre pack YAML scene for pronoun selection not yet added — struct and
wiring are in place, ready for content.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two fixes:

1. Session channel subscription: changed try_lock() to lock().await for
   the SharedGameSession mutex. try_lock() silently failed when dispatch
   held the lock, leaving session_rx as None permanently. All session
   messages (NARRATION, NARRATION_END, PLAYER_ACTION for observers) were
   silently dropped. This is why the narration blob grew without
   separators — observers never received NARRATION_END.

2. Observer action format: changed "CharName: action" to "CharName —
   action" so it looks less like a protocol message in the narration feed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The narrator was hallucinating item loss even though inventory was in
the prompt. Strengthened the constraint:

- Items now include description, category, and [EQUIPPED] tag
- Gold amount shown
- Four explicit hard rules: equipped items work immediately, items on
  the list are real, items not on the list fail, never narrate item
  loss unless the game engine removes it
- Framed as "CHARACTER SHEET — INVENTORY (canonical, overrides narration)"
  to signal that the inventory list is ground truth

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ms equipped

Two wiring gaps:

1. After CharacterBuilder.build(), the loose `inventory` variable was
   never updated from the character's inventory. The snapshot builder
   then overwrote character.core.inventory with the empty default.
   /inventory showed "No items" even though chargen created them.

2. Starting equipment items from chargen were created with equipped:false.
   Changed to equipped:true — you start with your gear ready.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ER_SHEET

CharacterSheetPayload now includes race, personality, pronouns, and
equipment list. Both emission sites (chargen + reconnect) populate
the new fields from the Character struct. All fields use #[serde(default)]
for backwards compatibility with saved sessions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reconnecting players were stuck with disabled input because:
1. PARTY_STATUS only included their own character (from saved snapshot),
   not other players in the shared session
2. No TURN_STATUS was sent, so activePlayerName stayed null

Now on reconnect:
- Full multiplayer PARTY_STATUS (all session members) sent via direct tx
- TURN_STATUS "resolved" sent to enable input immediately
- Both go through the direct mpsc channel (not session channel) so they
  arrive before the writer task subscribes to session broadcasts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Added tracing::info/debug to every code path changed during Session 7:
- session_rx subscription (writer task)
- self-skip filter decisions
- reconnect party/turn state sends (member count, solo vs multiplayer)
- chargen complete (character name, hp, items, pronouns)
- observer PLAYER_ACTION broadcast (observer count)
- TTS send_to_acting_player routing (session vs global)
- narrator prompt inventory constraint (item/equipped/gold counts)
- narrator prompt pronouns injection
- multiplayer narration broadcast to observers (text length)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…n, barrier

Extended tracing coverage to all game rule paths:
- Combat pre-tick state (in_combat, round, drama_weight)
- Chase tick (round, separation, gain)
- Slash command dispatch (command name, result type)
- Session save success (player, turn, location, hp, items)
- Location changes (new vs revisit, total discovered)
- Turn barrier mode check (turn_mode, player_count, has_barrier)
- Barrier action submission

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Session 8 found the narrator still puppeting player characters despite
the existing constraint. Strengthened with:

1. PC agency: explicit FORBIDDEN/ALLOWED examples including the exact
   violations found (writing dialogue for PCs, scripting PC-to-PC
   physical interactions). Framed as "violations break the game."

2. NPC identity: expanded the registry header to emphasize gender
   consistency — "If an NPC was described as male, they stay male in
   ALL future narration." Addresses Toggler Copperjaw gender flip bug.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Session 8 fixes:

1. Narration history entries now tagged with [CharacterName] so the
   narrator knows which history belongs to which player. Prevents
   cross-contamination (Shirley's blacksmith scene leaking into
   Laverne's Toggler response).

2. NPC registry: added pronouns (she/he/they/etc) to the common words
   reject list. Added substring dedup — "Toggler" merges into existing
   "Toggler Copperjaw" instead of creating a separate entry. Longer
   (more specific) names upgrade shorter matches.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaces fragile regex-based NPC extraction with structured output from
the narrator. NPCs are now declared in the same JSON block as footnotes
and items_gained:

  {"npcs_present": [{"name": "Toggler Copperjaw", "pronouns": "he/him",
    "role": "blacksmith", "appearance": "Big man, missing an ear",
    "is_new": true}]}

Pipeline: narrator prompt → NarratorStructuredBlock → NarratorExtraction
→ ActionResult → dispatch_player_action → NPC registry update.

Structured extraction runs first; regex fallback catches anything the
narrator forgot to list. Substring dedup prevents "Toggler" and
"Toggler Copperjaw" from becoming separate entries.

Fixes: pronoun-as-NPC-name ("She"), short-name duplication ("Toggler"),
gender flips (pronouns locked on first introduction).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Mutations/abilities were only in the prompt as constraints (what you
CAN'T do). Added directive for proactive narration: when the scene
naturally creates an opportunity, weave the character's mutations into
the narrative. A psychic should catch stray thoughts near a mysterious
ticking, not just hear mechanical sounds.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ywords

Combat never activated mechanically because the keyword scanner didn't
match creature_smith's narration phrasing. Now: if the intent classifier
routes to creature_smith (intent = "Combat"), that alone flips
in_combat = true and transitions turn mode. Keyword scan remains as
fallback.

Fixes: HP unchanged after fights, no CombatOverlay, no turn order.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The narrator had NO location reference in its prompt — it was inventing
settlement names ("Node 11") instead of using genre pack locations
(Tood's Dome, Bone Wind, etc.).

Now injects:
1. Discovered regions (places the party has visited)
2. Cartography region names from the shared session (all world locations)

With directive: "Use ONLY these location names. Do NOT invent new ones."

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Trope engine showed "No active tropes" after 8 turns because
trope_defs was never loaded for returning players. The genre pack
was loaded (for visual_style, audio, etc.) but trope definitions
were skipped — only start_character_creation populated them.

Now the returning player path loads tropes from the genre pack +
world tropes, same as the new player path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Changed PlayerJoined transition: FreePlay → Structured when 2+ players.
All players now submit actions blindly (sealed envelope). The existing
TurnBarrier collects all actions, waits for all players (or timeout),
then resolves as one unified narration.

Infrastructure was already built (barrier, named_actions, combined
context, timeout fallback). This change activates it for all multiplayer
sessions, not just combat.

Added TURN_STATUS "active" broadcast on barrier submission so other
players see who has submitted.

Flow: Player 1 submits → "Waiting for other players..." → Player 2
submits → barrier resolves → one narration covering both actions →
both inputs re-enable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The barrier path was a stub — it generated narration and delivered it
but processed NONE of the game rules. Every multiplayer turn with the
sealed envelope active bypassed combat, HP, items, NPCs, save, etc.

Now wired:
1. Combat detection from intent (broadcasts CombatEvent)
2. NPC registry update from structured narrator data
3. Narration history update (shared session)
4. Item acquisition logging
5. Save/persist (narration to narrative_log)
6. Combat state in saved snapshot
7. HP changes from narration keywords

Still TODO (see task list):
- Location change detection
- Trope engine tick
- Music mood classification
- Render subject extraction
- XP award and level-up
- PARTY_STATUS broadcast

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…arty

Extended barrier path game rules to match FreePlay parity:
- HP: heavy damage (25%) + regular damage (12%) + healing (20%) keywords
- XP: 25 for combat, 10 for exploration, with level-up check
- Items: structured extraction → inventory.add() with dedup
- Location: extract_location_header → snapshot.location + discovered_regions
- PARTY_STATUS: broadcast to all session members after game rules
- Combat: keyword scan for start/end alongside intent-based detection

Still TODO: trope tick, music mood, render subject (tasks #21-23).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Completed barrier path parity with FreePlay. Every game rule now fires
in both paths:

- Trope engine tick (SharedGameSession.trope_states + trope_defs)
- Affinity progression (check_affinity_thresholds with default thresholds)
- Music mood classification (SharedGameSession.music_director.evaluate)
- Combat tick (effects expiry + round advance)
- Chase detection (keyword start/end)
- Footnotes → known facts (knowledge accumulation)
- NPC registry regex fallback
- Render subject extraction + beat filter + enqueue
- Watcher telemetry (narration generated event)
- Turn manager advance
- PARTY_STATUS broadcast

22 game rules total in the barrier path. Zero stubs. Zero workarounds.
Zero "deferred." Zero "known gaps."

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The barrier modifies HP/inventory/combat/XP in the saved snapshot, but
per-connection locals (used by the narrator prompt on the next turn)
were never updated. The barrier runs in a tokio::spawn and can't access
per-connection &mut refs.

Fix: two-way sync through SharedGameSession.PlayerState:
1. Barrier writes updated state back to PlayerState after processing
2. New sync_player_to_locals() reads PlayerState into per-connection
   locals at the start of each dispatch_player_action

Full round-trip: barrier → snapshot → save → PlayerState → sync_to_locals
→ per-connection locals → narrator prompt → UI (via PARTY_STATUS).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The FreePlay save was a stub — it only persisted location, turn_manager,
npc_registry, axis_values, and narrative_log. HP, XP, level, inventory,
combat state, chase state, discovered regions, active tropes, known
facts, and affinities were NEVER saved. This is why HP was unchanged
after 5+ combat encounters across an entire session.

Now syncs all character fields and game state to the snapshot before
save. Also added sync_player_to_locals for barrier→connection sync.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The barrier (multiplayer turn) code path was spawning a separate task that
ran a 95-line mini-pipeline, skipping all 20+ post-narration systems:
HP damage/healing, combat detection, trope scanning, inventory extraction,
NPC registry updates, persistence, music mood, render pipeline, TTS, etc.

Replace tokio::spawn + early return with inline await. After barrier
resolves, fall through to the existing FreePlay pipeline. All game
mechanical systems now run for barrier turns.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…e improvements

Merged remote changes (FreePlay save, barrier game rules, genre cache)
with the inline barrier await fix. Kept remote's TURN_STATUS "active"
broadcast and char_name narration history format.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
slabgorb and others added 7 commits March 29, 2026 10:57
- MoodContext.in_chase now reads chase_state.is_some() instead of hardcoded false
- PrerenderContext.active_dialogue_npc wired to last NPC in registry

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The sync_from_locals block was missing xp, inventory, combat_state,
and chase_state — these per-player fields never propagated to
SharedGameSession.PlayerState, causing stale data in multiplayer.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…nals

MoodContext quest_completed and npc_died were hardcoded false. Now scanned
from narration keywords like combat detection already does.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Added chase_state to sync_player_to_locals and sync_from_locals so chase
state survives across barrier turns. Also wired quest_completed and
npc_died to narration keyword scanning in MoodContext.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
NOT BUILDING — 3 errors remain:
- dispatch_connect missing character_level/character_xp params
- barrier placeholder CreatureCore missing xp field (lib.rs:2595)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add character_level/character_xp params to dispatch_connect for
  restore on reconnect
- Add xp field to barrier placeholder CreatureCore
- Fixes 3 build errors from WIP commit

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
MusicDirector was only reading mood_tracks (2 tracks per mood) and
completely ignoring the themes array which contains 14+ variations
per mood across set-1/set-2 (ambient, full, overture, resolution,
sparse, tension_build). Now merges both sources on init. Energy
levels derived from variation type for intensity matching.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@slabgorb slabgorb merged commit c6ed2c9 into develop Mar 29, 2026
@slabgorb slabgorb deleted the fix/session6-playtest-fixes branch March 30, 2026 13:55
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