fix: barrier wiring + music rotation + XP persistence#141
Merged
Conversation
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>
- 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Playtest bugs addressed
Test plan
🤖 Generated with Claude Code