diff --git a/planning_notes/npc/npc_chat_improvements/CHARACTER_REGISTRY_IMPLEMENTATION.md b/planning_notes/npc/npc_chat_improvements/CHARACTER_REGISTRY_IMPLEMENTATION.md new file mode 100644 index 0000000..c4c0de8 --- /dev/null +++ b/planning_notes/npc/npc_chat_improvements/CHARACTER_REGISTRY_IMPLEMENTATION.md @@ -0,0 +1,138 @@ +# Global Character Registry Implementation + +## Overview +Implemented a global character registry system that maintains all available characters (player + NPCs) for the game, enabling reliable speaker resolution in multi-character conversations. + +## Architecture + +### Character Registry System (`js/systems/character-registry.js`) +Global object `window.characterRegistry` with methods: +- **setPlayer(playerData)** - Register player at game initialization +- **registerNPC(npcId, npcData)** - Register NPC when npcManager registers it +- **getCharacter(characterId)** - Get specific character by ID +- **getAllCharacters()** - Get complete dictionary of all characters +- **hasCharacter(characterId)** - Check if character exists +- **clearNPCs()** - Clear all NPCs (for scenario transitions) +- **debug()** - Log registry state + +### Data Flow + +1. **Initialization Phase** (game.js): + ``` + createPlayer() + → window.player = player + → characterRegistry.setPlayer(playerData) + ``` + +2. **NPC Registration Phase** (npc-manager.js): + ``` + npcManager.registerNPC(id, opts) + → this.npcs.set(id, entry) + → characterRegistry.registerNPC(id, entry) ← NEW + ``` + +3. **Person-Chat Minigame Phase** (person-chat-minigame.js): + ``` + buildCharacterIndex() + → if window.characterRegistry exists + → return characterRegistry.getAllCharacters() + → Otherwise fallback to legacy local building + ``` + +## Key Features + +✅ **Automatic Population**: NPCs automatically added when registered via npcManager +✅ **Global Access**: Available to any minigame or system that needs speaker resolution +✅ **Early Availability**: Player and room NPCs available before person-chat minigame starts +✅ **Backward Compatible**: Falls back to legacy local character index building if registry unavailable +✅ **Clean Separation**: Registry only knows about character data, not minigame logic + +## Files Modified + +1. **NEW: `js/systems/character-registry.js`** + - 100+ lines of character registry implementation + - Self-documenting with comprehensive JSDoc comments + +2. **`js/main.js`** + - Added import: `import './systems/character-registry.js';` + - Ensures registry available before any scripts that need it + +3. **`js/core/game.js`** + - After `window.player = player`, added: + ```javascript + if (window.characterRegistry && window.player) { + const playerData = { id, displayName, spriteSheet, ... }; + window.characterRegistry.setPlayer(playerData); + } + ``` + +4. **`js/systems/npc-manager.js`** + - In `registerNPC()` method, after `this.npcs.set(realId, entry)`: + ```javascript + if (window.characterRegistry) { + window.characterRegistry.registerNPC(realId, entry); + } + ``` + +5. **`js/minigames/person-chat/person-chat-minigame.js`** + - Simplified `buildCharacterIndex()` to: + ```javascript + if (window.characterRegistry) { + return characterRegistry.getAllCharacters(); + } + // Fallback to legacy local building... + ``` + +## Test Verification + +To verify the system is working: + +1. **Check Console Logs**: + ``` + ✅ Character Registry system initialized + ✅ Character Registry: Added player (Agent 0x00) + ✅ Character Registry: Added NPC test_npc_front (displayName: Helper NPC) + ✅ Character Registry: Added NPC test_npc_back (displayName: Back NPC) + ``` + +2. **Check Character Resolution**: + ``` + 👥 Using global character registry with 3 characters: ['player', 'test_npc_front', 'test_npc_back'] + ``` + +3. **Check Speaker Display**: + - When `test_npc_back: "Welcome..."` appears in dialogue + - Should resolve to "Back NPC" (from registry displayName) + - Not show raw ID "test_npc_back" + +## Browser Console Debug + +Available in browser console: +```javascript +window.characterRegistry.debug() +// Output: +// 📋 Character Registry: { +// playerCount: 1, +// npcCount: 5, +// totalCharacters: 6, +// characters: ['player', 'test_npc_front', 'test_npc_back', ...] +// } + +window.characterRegistry.getCharacter('test_npc_back') +// Returns: { id, displayName: 'Back NPC', spriteSheet, ... } +``` + +## Benefits + +1. **No More Missing Secondary NPCs**: All room NPCs automatically available before minigame starts +2. **Consistent Speaker Resolution**: Single source of truth for all characters +3. **Extensible**: Easy to add more features (character filtering, abilities, etc.) +4. **Debuggable**: Clear logging and debug methods for troubleshooting +5. **Maintainable**: Centralized character management separate from minigame logic + +## Next Steps + +1. Reload game with hard refresh to clear cache +2. Test line-prefix speaker format with secondary NPCs +3. Verify both `test_npc_front` and `test_npc_back` display correct names +4. Check console for character registry logs diff --git a/planning_notes/npc/npc_chat_improvements/CHARACTER_REGISTRY_QUICK_REF.md b/planning_notes/npc/npc_chat_improvements/CHARACTER_REGISTRY_QUICK_REF.md new file mode 100644 index 0000000..3c44baa --- /dev/null +++ b/planning_notes/npc/npc_chat_improvements/CHARACTER_REGISTRY_QUICK_REF.md @@ -0,0 +1,126 @@ +# Global Character Registry - Quick Reference + +## What It Does +Maintains a single, global registry of all characters (player + NPCs) that's populated automatically as the game loads. Used by person-chat minigame for reliable speaker resolution in conversations. + +## How to Use (Developer) + +### Access the Registry +```javascript +// Get all characters +window.characterRegistry.getAllCharacters() +// Returns: { player: {...}, npc_id_1: {...}, npc_id_2: {...} } + +// Get specific character +window.characterRegistry.getCharacter('test_npc_back') +// Returns: { id, displayName, spriteSheet, ... } + +// Check if character exists +window.characterRegistry.hasCharacter('some_npc') +// Returns: true or false + +// Debug current state +window.characterRegistry.debug() +``` + +### NPCs Added Automatically When: +1. **Game initializes**: Player registered via `game.js` +2. **NPC is registered**: Via `npcManager.registerNPC(id, opts)` in npc-lazy-loader.js +3. **Room loads**: NPCs from scenario json automatically registered by npc-lazy-loader + +### How Person-Chat Uses It +```javascript +// In person-chat-minigame.js buildCharacterIndex(): +buildCharacterIndex() { + if (window.characterRegistry) { + // Use global registry (has all characters already) + return characterRegistry.getAllCharacters(); + } + // Fallback to legacy local building if needed +} +``` + +## Console Debug Commands + +```javascript +// See all registered characters +window.characterRegistry.debug() + +// Check if specific NPC is registered +window.characterRegistry.hasCharacter('test_npc_back') + +// Get NPC display name +window.characterRegistry.getCharacter('test_npc_back').displayName +// Should output: "Back NPC" + +// Get all character IDs +Object.keys(window.characterRegistry.getAllCharacters()) +// Should output: ['player', 'test_npc_front', 'test_npc_back', ...] + +// Clear all NPCs (for scenario transitions) +window.characterRegistry.clearNPCs() +``` + +## Expected Console Output + +When game loads: +``` +✅ Character Registry system initialized +✅ Character Registry: Added player (Agent 0x00) +✅ Character Registry: Added NPC test_npc_front (displayName: Helper NPC) +✅ Character Registry: Added NPC test_npc_back (displayName: Back NPC) +``` + +When person-chat starts: +``` +👥 Using global character registry with 3 characters: ['player', 'test_npc_front', 'test_npc_back'] +``` + +## Files Changed + +| File | Change | +|------|--------| +| `js/systems/character-registry.js` | NEW: 100+ lines | +| `js/main.js` | Added import of character-registry.js | +| `js/core/game.js` | Register player in registry after initialization | +| `js/systems/npc-manager.js` | Register NPCs in registry when they're registered | +| `js/minigames/person-chat/person-chat-minigame.js` | Use global registry instead of building local index | + +## How It Fixes the Bug + +**Before**: Secondary NPCs like `test_npc_back` weren't in the character index when person-chat minigame started, so speaker resolution failed. + +**After**: +1. `test_npc_back` registered in npcManager → immediately added to characterRegistry +2. When person-chat minigame starts, it uses characterRegistry +3. `test_npc_back` is already there with displayName "Back NPC" +4. Line-prefix parsing works: `test_npc_back:` → resolves to "Back NPC" ✅ + +## Scenario Example + +Room with 3 NPCs in `scenario.json`: +```json +"test_room": { + "npcs": [ + { "id": "test_npc_front", "displayName": "Helper NPC", ... }, + { "id": "test_npc_back", "displayName": "Back NPC", ... }, + { "id": "test_npc_influence", "displayName": "Expert", ... } + ] +} +``` + +Character Registry after room loads: +```javascript +{ + player: { id: 'player', displayName: 'Agent 0x00', ... }, + test_npc_front: { id: 'test_npc_front', displayName: 'Helper NPC', ... }, + test_npc_back: { id: 'test_npc_back', displayName: 'Back NPC', ... }, + test_npc_influence: { id: 'test_npc_influence', displayName: 'Expert', ... } +} +``` + +When dialogue line is `test_npc_back: "Welcome..."`: +1. Parser extracts speaker: `test_npc_back` +2. Looks up in characterRegistry: ✅ found +3. Gets displayName: "Back NPC" +4. Shows: "Back NPC: Welcome..." diff --git a/planning_notes/npc_chat_improvements/IMPLEMENTATION_PLAN_REVISED.md b/planning_notes/npc/npc_chat_improvements/IMPLEMENTATION_PLAN_REVISED.md similarity index 100% rename from planning_notes/npc_chat_improvements/IMPLEMENTATION_PLAN_REVISED.md rename to planning_notes/npc/npc_chat_improvements/IMPLEMENTATION_PLAN_REVISED.md diff --git a/planning_notes/npc_chat_improvements/INDEX.md b/planning_notes/npc/npc_chat_improvements/INDEX.md similarity index 100% rename from planning_notes/npc_chat_improvements/INDEX.md rename to planning_notes/npc/npc_chat_improvements/INDEX.md diff --git a/planning_notes/npc_chat_improvements/OVERVIEW_REVISED.md b/planning_notes/npc/npc_chat_improvements/OVERVIEW_REVISED.md similarity index 100% rename from planning_notes/npc_chat_improvements/OVERVIEW_REVISED.md rename to planning_notes/npc/npc_chat_improvements/OVERVIEW_REVISED.md diff --git a/planning_notes/npc_chat_improvements/QUICK_REFERENCE_REVISED.md b/planning_notes/npc/npc_chat_improvements/QUICK_REFERENCE_REVISED.md similarity index 100% rename from planning_notes/npc_chat_improvements/QUICK_REFERENCE_REVISED.md rename to planning_notes/npc/npc_chat_improvements/QUICK_REFERENCE_REVISED.md diff --git a/planning_notes/npc_chat_improvements/review1/DELIVERABLES.md b/planning_notes/npc/npc_chat_improvements/review1/DELIVERABLES.md similarity index 100% rename from planning_notes/npc_chat_improvements/review1/DELIVERABLES.md rename to planning_notes/npc/npc_chat_improvements/review1/DELIVERABLES.md diff --git a/planning_notes/npc_chat_improvements/review1/IMPLEMENTATION_PLAN.md b/planning_notes/npc/npc_chat_improvements/review1/IMPLEMENTATION_PLAN.md similarity index 100% rename from planning_notes/npc_chat_improvements/review1/IMPLEMENTATION_PLAN.md rename to planning_notes/npc/npc_chat_improvements/review1/IMPLEMENTATION_PLAN.md diff --git a/planning_notes/npc_chat_improvements/review1/OVERVIEW.md b/planning_notes/npc/npc_chat_improvements/review1/OVERVIEW.md similarity index 100% rename from planning_notes/npc_chat_improvements/review1/OVERVIEW.md rename to planning_notes/npc/npc_chat_improvements/review1/OVERVIEW.md diff --git a/planning_notes/npc_chat_improvements/review1/QUICK_REFERENCE.md b/planning_notes/npc/npc_chat_improvements/review1/QUICK_REFERENCE.md similarity index 100% rename from planning_notes/npc_chat_improvements/review1/QUICK_REFERENCE.md rename to planning_notes/npc/npc_chat_improvements/review1/QUICK_REFERENCE.md diff --git a/planning_notes/npc_chat_improvements/review1/REVIEW1.md b/planning_notes/npc/npc_chat_improvements/review1/REVIEW1.md similarity index 100% rename from planning_notes/npc_chat_improvements/review1/REVIEW1.md rename to planning_notes/npc/npc_chat_improvements/review1/REVIEW1.md diff --git a/planning_notes/npc_chat_improvements/review1/UPDATES.md b/planning_notes/npc/npc_chat_improvements/review1/UPDATES.md similarity index 100% rename from planning_notes/npc_chat_improvements/review1/UPDATES.md rename to planning_notes/npc/npc_chat_improvements/review1/UPDATES.md diff --git a/planning_notes/npc_chat_improvements/review1/UPDATES_SUMMARY.md b/planning_notes/npc/npc_chat_improvements/review1/UPDATES_SUMMARY.md similarity index 100% rename from planning_notes/npc_chat_improvements/review1/UPDATES_SUMMARY.md rename to planning_notes/npc/npc_chat_improvements/review1/UPDATES_SUMMARY.md diff --git a/planning_notes/npc_chat_improvements/review2/BACKGROUND_CHANGES_FEATURE.md b/planning_notes/npc/npc_chat_improvements/review2/BACKGROUND_CHANGES_FEATURE.md similarity index 100% rename from planning_notes/npc_chat_improvements/review2/BACKGROUND_CHANGES_FEATURE.md rename to planning_notes/npc/npc_chat_improvements/review2/BACKGROUND_CHANGES_FEATURE.md diff --git a/planning_notes/npc_chat_improvements/review2/COMPREHENSIVE_REVIEW.md b/planning_notes/npc/npc_chat_improvements/review2/COMPREHENSIVE_REVIEW.md similarity index 100% rename from planning_notes/npc_chat_improvements/review2/COMPREHENSIVE_REVIEW.md rename to planning_notes/npc/npc_chat_improvements/review2/COMPREHENSIVE_REVIEW.md diff --git a/planning_notes/npc_chat_improvements/review2/README.md b/planning_notes/npc/npc_chat_improvements/review2/README.md similarity index 100% rename from planning_notes/npc_chat_improvements/review2/README.md rename to planning_notes/npc/npc_chat_improvements/review2/README.md diff --git a/public/break_escape/css/person-chat-minigame.css b/public/break_escape/css/person-chat-minigame.css index ccb8f86..71d73c6 100644 --- a/public/break_escape/css/person-chat-minigame.css +++ b/public/break_escape/css/person-chat-minigame.css @@ -149,6 +149,22 @@ color: #ff9a4a; } +/* PHASE 4: Narrator mode styling */ +.person-chat-portrait-section.narrator-mode { + background-color: #000; +} + +.person-chat-speaker-name.narrator-speaker { + display: none; /* Hide speaker name in narrator mode */ +} + +/* Dialogue text styling for narrator */ +.person-chat-portrait-section.narrator-mode + .person-chat-caption-area .person-chat-dialogue-text { + text-align: center; + font-style: italic; + color: #999; +} + /* Dialogue text box */ .person-chat-dialogue-box { background-color: transparent; diff --git a/public/break_escape/js/core/game.js b/public/break_escape/js/core/game.js index e727931..870b065 100644 --- a/public/break_escape/js/core/game.js +++ b/public/break_escape/js/core/game.js @@ -532,6 +532,18 @@ export async function create() { // Store player globally for access from other modules window.player = player; + // Register player in global character registry for speaker resolution + if (window.characterRegistry && window.player) { + const playerData = { + id: 'player', + displayName: window.gameState?.playerName || 'Agent 0x00', + spriteSheet: 'hacker', + spriteTalk: 'assets/characters/hacker-talk.png', + metadata: {} + }; + window.characterRegistry.setPlayer(playerData); + } + // Create door opening animation (for N/S doors) this.anims.create({ key: 'door_open', diff --git a/public/break_escape/js/main.js b/public/break_escape/js/main.js index 4711056..58c8950 100644 --- a/public/break_escape/js/main.js +++ b/public/break_escape/js/main.js @@ -8,6 +8,9 @@ import { initializeDebugSystem } from './systems/debug.js?v=7'; import { initializeUI } from './ui/panels.js?v=9'; import { initializeModals } from './ui/modals.js?v=7'; +// Import character registry system +import './systems/character-registry.js'; + // Import minigame framework import './minigames/index.js'; diff --git a/public/break_escape/js/minigames/person-chat/person-chat-minigame.js b/public/break_escape/js/minigames/person-chat/person-chat-minigame.js index 2c3a384..0eb6dc6 100644 --- a/public/break_escape/js/minigames/person-chat/person-chat-minigame.js +++ b/public/break_escape/js/minigames/person-chat/person-chat-minigame.js @@ -80,15 +80,25 @@ export class PersonChatMinigame extends MinigameScene { this.lastResult = null; // Store last continue() result for choice handling this.isClickThroughMode = false; // If true, player must click to advance between dialogue lines (starts in AUTO mode) this.pendingContinueCallback = null; // Callback waiting for player click in click-through mode + this.isProcessingDialogue = false; // PHASE 0: State locking to prevent race conditions during dialogue advancement console.log(`🎭 PersonChatMinigame created for NPC: ${this.npcId}`); } /** * Build index of all available characters (player + NPCs) + * Uses global character registry populated as NPCs are registered * @returns {Object} Map of character ID to character data */ buildCharacterIndex() { + // Use global character registry if available + if (window.characterRegistry) { + const allCharacters = window.characterRegistry.getAllCharacters(); + console.log(`👥 Using global character registry with ${Object.keys(allCharacters).length} characters:`, Object.keys(allCharacters)); + return allCharacters; + } + + // Fallback to legacy local building if registry not available const characters = {}; // Add player @@ -98,12 +108,13 @@ export class PersonChatMinigame extends MinigameScene { characters[this.npc.id] = this.npc; // Add other NPCs from current room (NPCs are now per-room, not at scenario root) - const currentRoom = window.currentRoom || this.npc.roomId; + // Use npc.roomId first, fallback to window.currentRoom + const currentRoom = this.npc.roomId || window.currentRoom; if (currentRoom && this.scenario.rooms && this.scenario.rooms[currentRoom]) { const roomNPCs = this.scenario.rooms[currentRoom].npcs || []; roomNPCs.forEach(npc => { if (npc.id !== this.npc.id) { - // Look up NPC data from npcManager for complete displayName + // Look up NPC data from npcManager for complete displayName and other properties const npcData = window.npcManager?.getNPC(npc.id) || npc; characters[npc.id] = npcData; } @@ -111,7 +122,7 @@ export class PersonChatMinigame extends MinigameScene { } // Fallback to legacy root-level NPCs for backward compatibility - if (!Object.keys(characters).length > 2 && this.scenario.npcs && Array.isArray(this.scenario.npcs)) { + if (Object.keys(characters).length <= 2 && this.scenario.npcs && Array.isArray(this.scenario.npcs)) { this.scenario.npcs.forEach(npc => { if (npc.id !== this.npc.id && !characters[npc.id]) { characters[npc.id] = npc; @@ -544,6 +555,182 @@ export class PersonChatMinigame extends MinigameScene { return this.npc.id; } + /** + * PHASE 1: Parse a dialogue line for speaker prefix format + * + * Validates that dialogue text is not empty (ignores "Speaker: " lines) + * Case-insensitive speaker IDs ("Player:", "player:", "PLAYER:" all work) + * First colon is delimiter ("Speaker: Text: with: colons" → speaker="Speaker", text="Text: with: colons") + * Rejects prefixes where speaker ID doesn't exist in character index + * Handles Narrator[character_id]: syntax for narrator with character portrait + * + * @param {string} line - Dialogue line to parse + * @returns {Object|null} Object with {speaker, text, isNarrator, narratorCharacter} or null if no prefix + */ + parseDialogueLine(line) { + if (!line || typeof line !== 'string') { + return null; + } + + line = line.trim(); + if (!line) { + return null; + } + + // Check for Narrator[character]: pattern first (highest priority) + const narratorMatch = line.match(/^Narrator\s*\[\s*([^\]]*)\s*\]\s*:\s*(.+)$/i); + if (narratorMatch) { + const characterId = narratorMatch[1].trim(); + const text = narratorMatch[2].trim(); + + // Must have non-empty text + if (!text) { + return null; + } + + // If character ID is provided, validate it exists + if (characterId && !this.characters[characterId]) { + console.warn(`⚠️ parseDialogueLine: Unknown character in Narrator[${characterId}], treating as unprefixed`); + return null; + } + + return { + speaker: 'narrator', + text: text, + isNarrator: true, + narratorCharacter: characterId || null + }; + } + + // Check for basic Narrator: pattern + const basicNarratorMatch = line.match(/^Narrator\s*:\s*(.+)$/i); + if (basicNarratorMatch) { + const text = basicNarratorMatch[1].trim(); + + // Must have non-empty text + if (!text) { + return null; + } + + return { + speaker: 'narrator', + text: text, + isNarrator: true, + narratorCharacter: null + }; + } + + // Check for regular Speaker: pattern + const colonIndex = line.indexOf(':'); + if (colonIndex === -1) { + // No colon - not a prefixed line + return null; + } + + const speakerId = line.substring(0, colonIndex).trim(); + const text = line.substring(colonIndex + 1).trim(); + + // Speaker ID must not be empty + if (!speakerId) { + return null; + } + + // Text must not be empty (reject lines like "Speaker: ") + if (!text) { + return null; + } + + // Validate speaker exists in characters + const normalizedSpeaker = this.normalizeSpeakerId(speakerId); + if (!normalizedSpeaker) { + // Speaker not found - treat as unprefixed line + return null; + } + + return { + speaker: normalizedSpeaker, + text: text, + isNarrator: false, + narratorCharacter: null + }; + } + + /** + * PHASE 1: Normalize speaker ID for consistent lookup + * + * Converts raw speaker ID to canonical form and validates existence + * Returns null if speaker ID doesn't exist in character index + * + * @param {string} speakerId - Raw speaker ID from dialogue line + * @returns {string|null} Normalized speaker ID or null if invalid + */ + normalizeSpeakerId(speakerId) { + if (!speakerId || typeof speakerId !== 'string') { + return null; + } + + const normalized = speakerId.toLowerCase().trim(); + + if (!normalized) { + return null; + } + + // Handle special cases + if (normalized === 'player') { + return this.characters['player'] ? 'player' : null; + } + + if (normalized === 'npc') { + return this.npc.id; + } + + // Look up by ID (case-sensitive after normalization to lowercase) + for (const [id, character] of Object.entries(this.characters)) { + if (id.toLowerCase() === normalized) { + return id; // Return original casing + } + } + + // Not found + return null; + } + + /** + * PHASE 4.5: Parse a background change line + * + * Background syntax: Background[filename]: optional text after (ignored) + * Example: "Background[scary-room.png]: The room transforms..." + * + * @param {string} line - Dialogue line to parse + * @returns {string|null} Background filename if valid, null otherwise + */ + parseBackgroundLine(line) { + if (!line || typeof line !== 'string') { + return null; + } + + line = line.trim(); + if (!line) { + return null; + } + + // Match Background[filename]: optional text pattern + const bgMatch = line.match(/^Background\s*\[\s*([^\]]+)\s*\]\s*(?::\s*(.*))?$/i); + if (!bgMatch) { + return null; + } + + const filename = bgMatch[1].trim(); + + // Background filename must not be empty + if (!filename) { + return null; + } + + console.log(`🎨 PHASE 4.5: Parsed background change to: ${filename}`); + return filename; + } + /** * Handle choice selection * @param {number} choiceIndex - Index of selected choice @@ -631,112 +818,170 @@ export class PersonChatMinigame extends MinigameScene { * @param {Object} result - Result with potentially multiple lines and tags */ displayAccumulatedDialogue(result) { - if (!result.text || !result.tags) { - // No content to display - if (result.hasEnded) { - // Story ended - save state and show message - if (this.inkEngine && this.inkEngine.story) { - npcConversationStateManager.saveNPCState(this.npcId, this.inkEngine.story); - } - this.ui.showDialogue('(Conversation ended - press ESC to close)', 'system'); - console.log('🏁 Story has reached an end point'); - } + // PHASE 0: State locking to prevent race conditions during rapid dialogue advancement + if (this.isProcessingDialogue) { + console.log('⏳ Already processing dialogue, ignoring call'); return; } + this.isProcessingDialogue = true; - // Process any game action tags (give_item, unlock_door, etc.) BEFORE displaying dialogue - if (result.tags && result.tags.length > 0) { - console.log('🏷️ Processing action tags from accumulated dialogue:', result.tags); - processGameActionTags(result.tags, this.ui); - } - - // Split text into lines - const lines = result.text.split('\n').filter(line => line.trim()); - - // We have lines and tags - pair them up - // Each tag corresponds to a line (or group of lines before the next tag) - if (lines.length === 0) { - if (result.hasEnded) { - // Story ended - save state and show message - if (this.inkEngine && this.inkEngine.story) { - npcConversationStateManager.saveNPCState(this.npcId, this.inkEngine.story); + try { + if (!result.text || !result.tags) { + // No content to display + if (result.hasEnded) { + // Story ended - save state and show message + if (this.inkEngine && this.inkEngine.story) { + npcConversationStateManager.saveNPCState(this.npcId, this.inkEngine.story); + } + this.ui.showDialogue('(Conversation ended - press ESC to close)', 'system'); + console.log('🏁 Story has reached an end point'); } - this.ui.showDialogue('(Conversation ended - press ESC to close)', 'system'); - console.log('🏁 Story has reached an end point'); + return; } - return; + + // Process any game action tags (give_item, unlock_door, etc.) BEFORE displaying dialogue + if (result.tags && result.tags.length > 0) { + console.log('🏷️ Processing action tags from accumulated dialogue:', result.tags); + processGameActionTags(result.tags, this.ui); + } + + // Split text into lines + const lines = result.text.split('\n').filter(line => line.trim()); + + // We have lines and tags - pair them up + // Each tag corresponds to a line (or group of lines before the next tag) + if (lines.length === 0) { + if (result.hasEnded) { + // Story ended - save state and show message + if (this.inkEngine && this.inkEngine.story) { + npcConversationStateManager.saveNPCState(this.npcId, this.inkEngine.story); + } + this.ui.showDialogue('(Conversation ended - press ESC to close)', 'system'); + console.log('🏁 Story has reached an end point'); + } + return; + } + + // Create dialogue blocks: each block is one or more consecutive lines with the same speaker + // PHASE 0: Pass result object so createDialogueBlocks can use determineSpeaker() for tag-based fallback + const dialogueBlocks = this.createDialogueBlocks(lines, result.tags, result); + + // Display blocks sequentially with delays + this.displayDialogueBlocksSequentially(dialogueBlocks, result, 0); + } finally { + // PHASE 0: Unlock state on all exit paths (including errors) + this.isProcessingDialogue = false; } - - // Create dialogue blocks: each block is one or more consecutive lines with the same speaker - const dialogueBlocks = this.createDialogueBlocks(lines, result.tags); - - // Display blocks sequentially with delays - this.displayDialogueBlocksSequentially(dialogueBlocks, result, 0); } /** * Create dialogue blocks from lines and speaker tags - * When speaker tags are missing, dialogue defaults to the main NPC being talked to + * + * PHASE 3: Enhanced to support line prefix format + * PHASE 4.5: Enhanced to detect and extract background changes + * - First checks if line is background change (Background[...]) + * - Then tries to parse line prefix format using parseDialogueLine() + * - Falls back to tag-based grouping if no prefix found + * - Handles speaker changes mid-dialogue + * - Groups consecutive lines from same speaker into single block * * @param {Array} lines - Text lines - * @param {Array} tags - Speaker tags - * @returns {Array} Array of {speaker, text} blocks + * @param {Array} tags - Speaker tags (for fallback) + * @param {Object} result - Result object for tag-based fallback + * @returns {Array} Array of {speaker, text, isNarrator, narratorCharacter, backgroundChange} blocks */ - createDialogueBlocks(lines, tags) { + createDialogueBlocks(lines, tags, result) { const blocks = []; - let blockIndex = 0; + let currentSpeaker = null; + let currentText = ''; + let currentIsNarrator = false; + let currentNarratorCharacter = null; + let currentBackgroundChange = null; - // Special case: NO tags at all - all lines belong to main NPC - if (!tags || tags.length === 0) { - if (lines.length > 0) { - const allText = lines.join('\n').trim(); - if (allText) { - blocks.push({ speaker: this.npc.id, text: allText, tag: null }); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + // PHASE 4.5: Check for background change first + const bgChange = this.parseBackgroundLine(line); + if (bgChange) { + // Save current block if we have one + if (currentSpeaker !== null && currentText.trim()) { + blocks.push({ + speaker: currentSpeaker, + text: currentText.trim(), + isNarrator: currentIsNarrator, + narratorCharacter: currentNarratorCharacter, + backgroundChange: currentBackgroundChange + }); } + + // Create background-only block + blocks.push({ + speaker: null, + text: '', + isNarrator: false, + narratorCharacter: null, + backgroundChange: bgChange + }); + + // Reset for next speaker + currentSpeaker = null; + currentText = ''; + currentIsNarrator = false; + currentNarratorCharacter = null; + currentBackgroundChange = null; + continue; + } + + // Try to parse line prefix format + const parsed = this.parseDialogueLine(line); + + if (parsed) { + // This line has a prefix - speaker changed! + // First, save current block if we have one + if (currentSpeaker !== null && currentText.trim()) { + blocks.push({ + speaker: currentSpeaker, + text: currentText.trim(), + isNarrator: currentIsNarrator, + narratorCharacter: currentNarratorCharacter, + backgroundChange: currentBackgroundChange + }); + } + + // Start new block with parsed line + currentSpeaker = parsed.speaker; + currentText = parsed.text; + currentIsNarrator = parsed.isNarrator; + currentNarratorCharacter = parsed.narratorCharacter; + currentBackgroundChange = null; + } else { + // No prefix - continues current speaker + if (currentSpeaker === null) { + // First line without prefix - use tag-based or default speaker + currentSpeaker = this.determineSpeaker(result); + currentIsNarrator = false; + currentNarratorCharacter = null; + currentBackgroundChange = null; + } + + // Add to current text (newline-separated) + currentText += (currentText ? '\n' : '') + line; } - return blocks; } - // Group lines by speaker based on tags - for (let tagIdx = 0; tagIdx < tags.length; tagIdx++) { - const tag = tags[tagIdx]; - - // Determine speaker from tag - support multiple formats - // Default to main NPC if no speaker tag found - let speaker = this.npc.id; - if (tag.includes('speaker:player')) { - speaker = 'player'; - } else if (tag.includes('speaker:npc:')) { - // Extract character ID from speaker:npc:character_id format - const match = tag.match(/speaker:npc:(\S+)/); - if (match && match[1]) { - speaker = match[1]; - } - } else if (tag === 'player' || tag.includes('player')) { - speaker = 'player'; - } - - // Find how many lines belong to this speaker (until next tag or end) - const nextTagIdx = tagIdx + 1; - const startLineIdx = blockIndex; - - // Count lines for this speaker - lines between this tag and the next - let endLineIdx = lines.length; - if (nextTagIdx < tags.length) { - // There's another tag coming, but we need to figure out how many lines - // For now, assume 1 line per tag (common case) - endLineIdx = startLineIdx + 1; - } - - // Collect the text for this speaker - const blockText = lines.slice(startLineIdx, endLineIdx).join('\n').trim(); - if (blockText) { - blocks.push({ speaker, text: blockText, tag }); - } - - blockIndex = endLineIdx; + // Don't forget the last block! + if (currentSpeaker !== null && currentText.trim()) { + blocks.push({ + speaker: currentSpeaker, + text: currentText.trim(), + isNarrator: currentIsNarrator, + narratorCharacter: currentNarratorCharacter, + backgroundChange: currentBackgroundChange + }); } + console.log(`📝 PHASE 3: createDialogueBlocks created ${blocks.length} blocks from ${lines.length} lines`); return blocks; } @@ -796,6 +1041,26 @@ export class PersonChatMinigame extends MinigameScene { // Display current block's lines one at a time with accumulation const block = blocks[blockIndex]; + + // PHASE 4.5: Handle background changes before displaying dialogue + if (block.backgroundChange) { + console.log(`🎨 Background change block detected: ${block.backgroundChange}`); + + // Change background and move to next block + this.changeBackground(block.backgroundChange); + + this.scheduleDialogueAdvance(() => { + this.displayDialogueBlocksSequentially(blocks, originalResult, blockIndex + 1, 0, ''); + }, DIALOGUE_AUTO_ADVANCE_DELAY); + return; + } + + // Skip empty dialogue blocks + if (!block.text || !block.text.trim()) { + this.displayDialogueBlocksSequentially(blocks, originalResult, blockIndex + 1, 0, ''); + return; + } + const lines = block.text.split('\n').filter(line => line.trim()); if (lineIndex >= lines.length) { @@ -810,8 +1075,14 @@ export class PersonChatMinigame extends MinigameScene { console.log(`📋 Displaying line ${lineIndex + 1}/${lines.length} from block ${blockIndex + 1}/${blocks.length}: ${block.speaker}`); - // Show accumulated text (all lines up to and including current line) - this.ui.showDialogue(newAccumulatedText, block.speaker); + // PHASE 4: Show accumulated text with narrator support + this.ui.showDialogue( + newAccumulatedText, + block.speaker, + false, // preserveChoices + block.isNarrator || false, // isNarrator + block.narratorCharacter || null // narratorCharacter + ); // Display next line after delay this.scheduleDialogueAdvance(() => { @@ -1011,6 +1282,32 @@ export class PersonChatMinigame extends MinigameScene { super.cleanup(); } + /** + * PHASE 4.5: Change background image for current portrait + * + * Updates the portrait renderer's background image and re-renders + * + * @param {string} backgroundFilename - Filename of new background image + * @returns {Promise} + */ + async changeBackground(backgroundFilename) { + if (!backgroundFilename || !this.ui || !this.ui.portraitRenderer) { + console.warn(`⚠️ changeBackground: Invalid background or portrait renderer`); + return; + } + + try { + console.log(`🎨 Changing background to: ${backgroundFilename}`); + + // Call setBackground to load and render the new background + this.ui.portraitRenderer.setBackground(backgroundFilename); + + console.log(`✅ Background changed successfully`); + } catch (error) { + console.error(`❌ Error changing background: ${error.message}`); + } + } + /** * Show error message * @param {string} message - Error message to display diff --git a/public/break_escape/js/minigames/person-chat/person-chat-portraits.js b/public/break_escape/js/minigames/person-chat/person-chat-portraits.js index 53b671a..2582e5f 100644 --- a/public/break_escape/js/minigames/person-chat/person-chat-portraits.js +++ b/public/break_escape/js/minigames/person-chat/person-chat-portraits.js @@ -336,6 +336,22 @@ export default class PersonChatPortraits { img.src = bgSrc; } + /** + * PHASE 4.5: Change background to a new image + * @param {string} newBackgroundPath - Path to new background image + */ + setBackground(newBackgroundPath) { + if (!newBackgroundPath) { + console.warn('⚠️ setBackground: No background path provided'); + return; + } + + this.backgroundPath = newBackgroundPath; + this.backgroundImage = null; // Clear old image + console.log(`🎨 Setting new background: ${newBackgroundPath}`); + this.loadBackgroundImage(); + } + /** * Draw background image at same pixel scale as character sprite * Fills the canvas while maintaining sprite's pixel scale (may extend beyond canvas if larger) @@ -685,6 +701,30 @@ export default class PersonChatPortraits { ); } + /** + * PHASE 4: Clear the portrait canvas (for narrator mode without portrait) + */ + clearPortrait() { + if (!this.canvas || !this.ctx) return; + + // Clear canvas to black + this.ctx.fillStyle = '#000'; + this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); + + // Draw placeholder text + this.ctx.fillStyle = '#666'; + this.ctx.font = '16px Arial'; + this.ctx.textAlign = 'center'; + this.ctx.textBaseline = 'middle'; + this.ctx.fillText( + 'Narrator', + this.canvas.width / 2, + this.canvas.height / 2 + ); + + console.log('🖼️ Portrait cleared for narrator mode'); + } + /** * Destroy portrait and cleanup */ diff --git a/public/break_escape/js/minigames/person-chat/person-chat-ui.js b/public/break_escape/js/minigames/person-chat/person-chat-ui.js index 9a77ca0..203954f 100644 --- a/public/break_escape/js/minigames/person-chat/person-chat-ui.js +++ b/public/break_escape/js/minigames/person-chat/person-chat-ui.js @@ -212,47 +212,79 @@ export default class PersonChatUI { * @param {string} text - Dialogue text to display * @param {string} characterId - Character ID ('player', 'npc', or specific NPC ID) * @param {boolean} preserveChoices - If true, don't hide existing choices + * @param {boolean} isNarrator - PHASE 4: If true, display as narrator mode (centered, no speaker name) + * @param {string} narratorCharacter - PHASE 4: For Narrator[character]: format, character to show portrait of */ - showDialogue(text, characterId = 'npc', preserveChoices = false) { + showDialogue(text, characterId = 'npc', preserveChoices = false, isNarrator = false, narratorCharacter = null) { this.currentSpeaker = characterId; - console.log(`📝 showDialogue called with character: ${characterId}, text length: ${text?.length || 0}`); + console.log(`📝 showDialogue called with character: ${characterId}, text length: ${text?.length || 0}, narrator: ${isNarrator}`); - // Get character data - let character = this.characters[characterId]; - if (!character) { - // Fallback for legacy speaker values or main NPC ID - if (characterId === 'player') { - character = this.playerData; - } else if (characterId === 'npc' || !characterId) { - character = this.npc; - } else if (characterId === this.npc?.id) { - // Main NPC passed by ID - use main NPC data - character = this.npc; + // PHASE 4: Handle narrator mode + if (isNarrator) { + // Narrator mode - centered text, no speaker name + this.elements.portraitSection.className = 'person-chat-portrait-section narrator-mode'; + this.elements.speakerName.className = 'person-chat-speaker-name narrator-speaker'; + this.elements.portraitLabel.textContent = ''; // No label in narrator mode + this.elements.speakerName.textContent = 'Narrator'; // Hidden by CSS but available + + // If narratorCharacter is specified, show that character's portrait + if (narratorCharacter) { + const character = this.characters[narratorCharacter]; + if (character) { + this.updatePortraitForSpeaker(narratorCharacter, character); + } + } else { + // No character portrait in pure narrator mode + if (this.portraitRenderer) { + this.portraitRenderer.clearPortrait(); + } } + + console.log(`📝 Narrator mode: character=${narratorCharacter}, portrait shown=${!!narratorCharacter}`); + } else { + // Normal dialogue mode + // Get character data + let character = this.characters[characterId]; + if (!character) { + // Fallback for legacy speaker values or main NPC ID + if (characterId === 'player') { + character = this.playerData; + } else if (characterId === 'npc' || !characterId) { + character = this.npc; + } else if (characterId === this.npc?.id) { + // Main NPC passed by ID - use main NPC data + character = this.npc; + } + } + + // Determine display name + // If no character found, use the character ID itself formatted as display name + const displayName = character?.displayName || (characterId === 'player' ? 'You' : + characterId === 'npc' ? 'NPC' : + // Format character ID: convert snake_case or camelCase to Title Case + characterId.replace(/[_-]/g, ' ').replace(/\b\w/g, l => l.toUpperCase())); + + const speakerType = characterId === 'player' ? 'player' : 'npc'; + + this.elements.portraitLabel.textContent = displayName; + this.elements.speakerName.textContent = displayName; + + console.log(`📝 Set speaker name to: ${displayName}`); + + // Update speaker styling + this.elements.portraitSection.className = `person-chat-portrait-section speaker-${speakerType}`; + this.elements.speakerName.className = `person-chat-speaker-name ${speakerType}-speaker`; + + // Reset portrait for new speaker + this.updatePortraitForSpeaker(characterId, character); } - // Determine display name - const displayName = character?.displayName || (characterId === 'player' ? 'You' : 'NPC'); - const speakerType = characterId === 'player' ? 'player' : 'npc'; - - this.elements.portraitLabel.textContent = displayName; - this.elements.speakerName.textContent = displayName; - - console.log(`📝 Set speaker name to: ${displayName}`); - - // Update speaker styling - this.elements.portraitSection.className = `person-chat-portrait-section speaker-${speakerType}`; - this.elements.speakerName.className = `person-chat-speaker-name ${speakerType}-speaker`; - // Update dialogue text this.elements.dialogueText.textContent = text; console.log(`📝 Set dialogue text, element content: "${this.elements.dialogueText.textContent}"`); - // Reset portrait for new speaker - this.updatePortraitForSpeaker(characterId, character); - // Hide choices only if not preserving them (i.e., when transitioning from choices back to text) if (!preserveChoices) { this.hideChoices(); diff --git a/public/break_escape/js/systems/character-registry.js b/public/break_escape/js/systems/character-registry.js new file mode 100644 index 0000000..aec457d --- /dev/null +++ b/public/break_escape/js/systems/character-registry.js @@ -0,0 +1,99 @@ +/** + * Global Character Registry + * ======================== + * Maintains a registry of all characters (player, NPCs) available in the game. + * This registry is populated as NPCs are registered via npcManager, and as rooms are loaded. + * The person-chat minigame uses this registry for speaker resolution. + * + * When an NPC is registered via npcManager.registerNPC(), it's automatically added here. + * Format: { id: { id, displayName, spriteSheet, spriteTalk, ... }, ... } + */ + +window.characterRegistry = { + // Player character - set when game initializes + player: null, + + // All NPCs registered in the game + npcs: {}, + + /** + * Add player to registry + * @param {Object} playerData - Player object with id, displayName, etc. + */ + setPlayer(playerData) { + this.player = playerData; + console.log(`✅ Character Registry: Added player (${playerData.displayName})`); + }, + + /** + * Register an NPC in the character registry + * Called automatically when npcManager.registerNPC() is invoked + * @param {string} npcId - NPC identifier + * @param {Object} npcData - Full NPC data object + */ + registerNPC(npcId, npcData) { + this.npcs[npcId] = npcData; + console.log(`✅ Character Registry: Added NPC ${npcId} (displayName: ${npcData.displayName})`); + }, + + /** + * Get a character (player or NPC) by ID + * @param {string} characterId - Character identifier + * @returns {Object|null} Character data or null if not found + */ + getCharacter(characterId) { + if (characterId === 'player') { + return this.player; + } + return this.npcs[characterId] || null; + }, + + /** + * Get all available characters for speaker resolution + * Combines player and all registered NPCs + * @returns {Object} Dictionary of all characters + */ + getAllCharacters() { + const all = {}; + if (this.player) { + all['player'] = this.player; + } + Object.assign(all, this.npcs); + return all; + }, + + /** + * Check if a character exists in registry + * @param {string} characterId - Character identifier + * @returns {boolean} True if character exists + */ + hasCharacter(characterId) { + if (characterId === 'player') { + return this.player !== null; + } + return characterId in this.npcs; + }, + + /** + * Clear all registered NPCs (used for scenario transitions) + */ + clearNPCs() { + this.npcs = {}; + console.log(`🗑️ Character Registry: Cleared all NPCs`); + }, + + /** + * Debug: Log current registry state + */ + debug() { + const chars = Object.keys(this.getAllCharacters()); + console.log(`📋 Character Registry:`, { + playerCount: this.player ? 1 : 0, + npcCount: Object.keys(this.npcs).length, + totalCharacters: chars.length, + characters: chars + }); + } +}; + +console.log('✅ Character Registry system initialized'); diff --git a/public/break_escape/js/systems/npc-manager.js b/public/break_escape/js/systems/npc-manager.js index ddbfdef..8411ad6 100644 --- a/public/break_escape/js/systems/npc-manager.js +++ b/public/break_escape/js/systems/npc-manager.js @@ -77,6 +77,11 @@ export default class NPCManager { this.npcs.set(realId, entry); + // Register in global character registry for speaker resolution + if (window.characterRegistry) { + window.characterRegistry.registerNPC(realId, entry); + } + // Initialize conversation history for this NPC if (!this.conversationHistory.has(realId)) { this.conversationHistory.set(realId, []); diff --git a/scenarios/ink/test-line-prefix.ink b/scenarios/ink/test-line-prefix.ink new file mode 100644 index 0000000..d55da19 --- /dev/null +++ b/scenarios/ink/test-line-prefix.ink @@ -0,0 +1,170 @@ +// Test Line Prefix Speaker Format +// Tests for PHASE 1-4.5: Speaker prefixes, narrator mode, background changes +// +// Test Coverage: +// - Line prefix format: "Speaker: Text" +// - Narrator mode: "Narrator: Text" +// - Narrator with character: "Narrator[character]: Text" +// - Background changes: "Background[filename]: optional text" +// - Mixed format with traditional tags +// - Multi-line dialogue with speaker changes + +VAR test_phase = 0 + +=== start === +~ test_phase = 1 +PHASE 1: Line Prefix Speaker Format +Please observe the dialogue displays with speaker names derived from line prefixes. +-> phase_1_basic + +=== phase_1_basic === +// Test basic line prefix format with regular speakers +// Each line should show the speaker name correctly from the prefix + +Player: Hello! I'm here for the security briefing. +test_npc_front: Welcome to the facility. I'll run through the basics for you. +test_npc_front: We use biometric authentication and keycard access throughout the building. +Player: That sounds comprehensive. Any recent incidents? +test_npc_front: Nothing major. System updates last week, everything's stable now. + ++ [Continue to Phase 2] -> phase_2_narrator ++ [Repeat Phase 1] -> phase_1_basic + +=== phase_2_narrator === +~ test_phase = 2 +PHASE 2: Narrator Mode Testing +Narrator: The sun begins to set over the facility grounds. An eerie silence fills the corridors. +Narrator: Your briefing complete, you prepare to move deeper into the building. + ++ [Continue] -> phase_2_narrator_with_character + +=== phase_2_narrator_with_character === +// Narrator with character portrait displayed +Narrator[test_npc_front]: The guard glances nervously at the security monitors. +Narrator[test_npc_front]: Something seems different about the usual patrol rotation today. + ++ [Continue] -> phase_3_background_changes + +=== phase_3_background_changes === +~ test_phase = 3 +PHASE 3: Background Changes Testing +Background[lab-normal.png]: The laboratory appears normal at first glance. +test_npc_back: Welcome to the lab. Everything is operating within normal parameters. +Background[lab-alert.png]: Suddenly, the room plunges into emergency lighting! +test_npc_back: Wait... the alarm system just triggered. Something's wrong with the main servers! +test_npc_back: We need to get to the server room immediately! + ++ [Continue] -> phase_4_mixed_format + +=== phase_4_mixed_format === +~ test_phase = 4 +PHASE 4: Mixed Format Testing +Now we'll test mixing line prefix format with traditional tag-based dialogue. +Test that both systems work together smoothly. + +Player: What's the emergency protocol? +test_npc_front: Protocol Alpha - all personnel to designated zones. +test_npc_front: The facility goes into lockdown automatically. +Narrator: Red emergency lights begin flashing throughout the building. +Player: How long until the lockdown is complete? +test_npc_front: Usually takes about 30 seconds for all doors to secure. +test_npc_back: The server room needs to stay accessible for diagnostics! + ++ [Continue] -> phase_5_speaker_changes + +=== phase_5_speaker_changes === +~ test_phase = 5 +PHASE 5: Multi-Speaker Dialogue Testing +Testing rapid speaker changes using line prefixes. + +Player: Who's in charge here? +test_npc_front: I am, in the security department. +test_npc_back: But I'm responsible for the lab systems. +test_npc_front: Right, so we need both perspectives. +Player: What do you recommend? +test_npc_back: We need lab access first. +test_npc_front: I'll authorize it - follow me. +Narrator: The tension in the room is palpable as both professionals prepare for action. + ++ [Continue] -> phase_6_narrator_variations + +=== phase_6_narrator_variations === +~ test_phase = 6 +PHASE 6: Narrator Variations Testing + +Narrator: Pure narrator - no character portrait shown +Narrator[test_npc_back]: Narrator with test_npc_back portrait +Narrator[test_npc_front]: Narrator with security guard portrait +Background[hallway-dark.png]: The hallway descends into darkness +Narrator: Another narration after background change +Player: Are we ready to proceed? +Narrator[test_npc_front]: The guard nods solemnly + ++ [Continue] -> phase_7_comprehensive + +=== phase_7_comprehensive === +~ test_phase = 7 +PHASE 7: Comprehensive Scene +A scene combining all features. + +Background[facility-entrance.png]: You stand at the facility entrance +Narrator[test_npc_front]: The security guard approaches your position +test_npc_front: Welcome to the facility. I hope you're here for a good reason. +Player: I'm here to investigate the recent incident. +test_npc_front: Incident? There is no incident. +Background[security-office.png]: The guard escorts you to the security office +Narrator: Inside, screens display various security feeds +Narrator[test_npc_back]: From a monitor, a test_npc_back's face appears urgently +test_npc_back: We need to talk. Alone. +test_npc_front: What's this about? +test_npc_back: It's classified. Speaker confidential access only. +Narrator: The guard looks conflicted but nods slowly +test_npc_front: Alright. Go ahead. But I'm logging this. + ++ [Investigate Further] -> phase_8_edge_cases ++ [Conclude Test] -> ending + +=== phase_8_edge_cases === +~ test_phase = 8 +PHASE 8: Edge Cases & Stress Testing + +// Multiple consecutive narrator lines +Narrator: The tension builds. +Narrator: Everything is at stake. +Narrator: Time seems to slow down. + +// Multiple consecutive same speaker +Player: I need answers. +Player: Real answers. +Player: Not more evasions. + +// Rapid background changes +Background[room-a.png]: Location: Room A +Background[room-b.png]: Location: Room B +Background[room-c.png]: Location: Room C + +// Mixed speakers after backgrounds +test_npc_back: The data is clear. +test_npc_front: But the implications are severe. +Player: We need to act fast. +Narrator: The three of you exchange worried glances. + ++ [Conclude] -> ending + +=== ending === +~ test_phase = 9 +All test phases completed! +Total phases run: {test_phase} + +Thank you for testing the line prefix speaker format implementation. +The dialogue system should now support: +✓ Line prefix format (Speaker: Text) +✓ Narrator mode (Narrator: Text) +✓ Narrator with character (Narrator[char]: Text) +✓ Background changes (Background[file]: Text) +✓ Multi-speaker dialogue +✓ Mixed format compatibility +✓ Edge case handling + +Test complete. +-> END diff --git a/scenarios/ink/test-line-prefix.json b/scenarios/ink/test-line-prefix.json new file mode 100644 index 0000000..7ad93b4 --- /dev/null +++ b/scenarios/ink/test-line-prefix.json @@ -0,0 +1 @@ +{"inkVersion":21,"root":[[["done",{"#n":"g-0"}],null],"done",{"start":["ev",1,"/ev",{"VAR=":"test_phase","re":true},"^PHASE 1: Line Prefix Speaker Format","\n","^Please observe the dialogue displays with speaker names derived from line prefixes.","\n",{"->":"phase_1_basic"},null],"phase_1_basic":[["^Player: Hello! I'm here for the security briefing.","\n","^test_npc_front: Welcome to the facility. I'll run through the basics for you.","\n","^test_npc_front: We use biometric authentication and keycard access throughout the building.","\n","^Player: That sounds comprehensive. Any recent incidents?","\n","^test_npc_front: Nothing major. System updates last week, everything's stable now.","\n","ev","str","^Continue to Phase 2","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Repeat Phase 1","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["^ ",{"->":"phase_2_narrator"},"\n",null],"c-1":["^ ",{"->":".^.^.^"},"\n",null]}],null],"phase_2_narrator":[["ev",2,"/ev",{"VAR=":"test_phase","re":true},"^PHASE 2: Narrator Mode Testing","\n","^Narrator: The sun begins to set over the facility grounds. An eerie silence fills the corridors.","\n","^Narrator: Your briefing complete, you prepare to move deeper into the building.","\n","ev","str","^Continue","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["^ ",{"->":"phase_2_narrator_with_character"},"\n",null]}],null],"phase_2_narrator_with_character":[["^Narrator[test_npc_front]: The guard glances nervously at the security monitors.","\n","^Narrator[test_npc_front]: Something seems different about the usual patrol rotation today.","\n","ev","str","^Continue","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["^ ",{"->":"phase_3_background_changes"},"\n",null]}],null],"phase_3_background_changes":[["ev",3,"/ev",{"VAR=":"test_phase","re":true},"^PHASE 3: Background Changes Testing","\n","^Background[lab-normal.png]: The laboratory appears normal at first glance.","\n","^test_npc_back: Welcome to the lab. Everything is operating within normal parameters.","\n","^Background[lab-alert.png]: Suddenly, the room plunges into emergency lighting!","\n","^test_npc_back: Wait... the alarm system just triggered. Something's wrong with the main servers!","\n","^test_npc_back: We need to get to the server room immediately!","\n","ev","str","^Continue","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["^ ",{"->":"phase_4_mixed_format"},"\n",null]}],null],"phase_4_mixed_format":[["ev",4,"/ev",{"VAR=":"test_phase","re":true},"^PHASE 4: Mixed Format Testing","\n","^Now we'll test mixing line prefix format with traditional tag-based dialogue.","\n","^Test that both systems work together smoothly.","\n","^Player: What's the emergency protocol?","\n","^test_npc_front: Protocol Alpha - all personnel to designated zones.","\n","^test_npc_front: The facility goes into lockdown automatically.","\n","^Narrator: Red emergency lights begin flashing throughout the building.","\n","^Player: How long until the lockdown is complete?","\n","^test_npc_front: Usually takes about 30 seconds for all doors to secure.","\n","^test_npc_back: The server room needs to stay accessible for diagnostics!","\n","ev","str","^Continue","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["^ ",{"->":"phase_5_speaker_changes"},"\n",null]}],null],"phase_5_speaker_changes":[["ev",5,"/ev",{"VAR=":"test_phase","re":true},"^PHASE 5: Multi-Speaker Dialogue Testing","\n","^Testing rapid speaker changes using line prefixes.","\n","^Player: Who's in charge here?","\n","^test_npc_front: I am, in the security department.","\n","^test_npc_back: But I'm responsible for the lab systems.","\n","^test_npc_front: Right, so we need both perspectives.","\n","^Player: What do you recommend?","\n","^test_npc_back: We need lab access first.","\n","^test_npc_front: I'll authorize it - follow me.","\n","^Narrator: The tension in the room is palpable as both professionals prepare for action.","\n","ev","str","^Continue","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["^ ",{"->":"phase_6_narrator_variations"},"\n",null]}],null],"phase_6_narrator_variations":[["ev",6,"/ev",{"VAR=":"test_phase","re":true},"^PHASE 6: Narrator Variations Testing","\n","^Narrator: Pure narrator - no character portrait shown","\n","^Narrator[test_npc_back]: Narrator with test_npc_back portrait","\n","^Narrator[test_npc_front]: Narrator with security guard portrait","\n","^Background[hallway-dark.png]: The hallway descends into darkness","\n","^Narrator: Another narration after background change","\n","^Player: Are we ready to proceed?","\n","^Narrator[test_npc_front]: The guard nods solemnly","\n","ev","str","^Continue","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["^ ",{"->":"phase_7_comprehensive"},"\n",null]}],null],"phase_7_comprehensive":[["ev",7,"/ev",{"VAR=":"test_phase","re":true},"^PHASE 7: Comprehensive Scene","\n","^A scene combining all features.","\n","^Background[facility-entrance.png]: You stand at the facility entrance","\n","^Narrator[test_npc_front]: The security guard approaches your position","\n","^test_npc_front: Welcome to the facility. I hope you're here for a good reason.","\n","^Player: I'm here to investigate the recent incident.","\n","^test_npc_front: Incident? There is no incident.","\n","^Background[security-office.png]: The guard escorts you to the security office","\n","^Narrator: Inside, screens display various security feeds","\n","^Narrator[test_npc_back]: From a monitor, a test_npc_back's face appears urgently","\n","^test_npc_back: We need to talk. Alone.","\n","^test_npc_front: What's this about?","\n","^test_npc_back: It's classified. Speaker confidential access only.","\n","^Narrator: The guard looks conflicted but nods slowly","\n","^test_npc_front: Alright. Go ahead. But I'm logging this.","\n","ev","str","^Investigate Further","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Conclude Test","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["^ ",{"->":"phase_8_edge_cases"},"\n",null],"c-1":["^ ",{"->":"ending"},"\n",null]}],null],"phase_8_edge_cases":[["ev",8,"/ev",{"VAR=":"test_phase","re":true},"^PHASE 8: Edge Cases & Stress Testing","\n","^Narrator: The tension builds.","\n","^Narrator: Everything is at stake.","\n","^Narrator: Time seems to slow down.","\n","^Player: I need answers.","\n","^Player: Real answers.","\n","^Player: Not more evasions.","\n","^Background[room-a.png]: Location: Room A","\n","^Background[room-b.png]: Location: Room B","\n","^Background[room-c.png]: Location: Room C","\n","^test_npc_back: The data is clear.","\n","^test_npc_front: But the implications are severe.","\n","^Player: We need to act fast.","\n","^Narrator: The three of you exchange worried glances.","\n","ev","str","^Conclude","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["^ ",{"->":"ending"},"\n",null]}],null],"ending":["ev",9,"/ev",{"VAR=":"test_phase","re":true},"^All test phases completed!","\n","^Total phases run: ","ev",{"VAR?":"test_phase"},"out","/ev","\n","^Thank you for testing the line prefix speaker format implementation.","\n","^The dialogue system should now support:","\n","^✓ Line prefix format (Speaker: Text)","\n","^✓ Narrator mode (Narrator: Text)","\n","^✓ Narrator with character (Narrator[char]: Text)","\n","^✓ Background changes (Background[file]: Text)","\n","^✓ Multi-speaker dialogue","\n","^✓ Mixed format compatibility","\n","^✓ Edge case handling","\n","^Test complete.","\n","end",null],"global decl":["ev",0,{"VAR=":"test_phase"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/scenarios/ink/test-line-prefix2.ink b/scenarios/ink/test-line-prefix2.ink new file mode 100644 index 0000000..7965699 --- /dev/null +++ b/scenarios/ink/test-line-prefix2.ink @@ -0,0 +1,170 @@ +// Test Line Prefix Speaker Format +// Tests for PHASE 1-4.5: Speaker prefixes, narrator mode, background changes +// +// Test Coverage: +// - Line prefix format: "Speaker: Text" +// - Narrator mode: "Narrator: Text" +// - Narrator with character: "Narrator[character]: Text" +// - Background changes: "Background[filename]: optional text" +// - Mixed format with traditional tags +// - Multi-line dialogue with speaker changes + +VAR test_phase = 0 + +=== start === +~ test_phase = 1 +PHASE 1: Line Prefix Speaker Format +Please observe the dialogue displays with speaker names derived from line prefixes. +-> phase_1_basic + +=== phase_1_basic === +// Test basic line prefix format with regular speakers +// Each line should show the speaker name correctly from the prefix + +Player: Hello! I'm here for the security briefing. +security_guard: Welcome to the facility. I'll run through the basics for you. +security_guard: We use biometric authentication and keycard access throughout the building. +Player: That sounds comprehensive. Any recent incidents? +security_guard: Nothing major. System updates last week, everything's stable now. + ++ [Continue to Phase 2] -> phase_2_narrator ++ [Repeat Phase 1] -> phase_1_basic + +=== phase_2_narrator === +~ test_phase = 2 +PHASE 2: Narrator Mode Testing +Narrator: The sun begins to set over the facility grounds. An eerie silence fills the corridors. +Narrator: Your briefing complete, you prepare to move deeper into the building. + ++ [Continue] -> phase_2_narrator_with_character + +=== phase_2_narrator_with_character === +// Narrator with character portrait displayed +Narrator[security_guard]: The guard glances nervously at the security monitors. +Narrator[security_guard]: Something seems different about the usual patrol rotation today. + ++ [Continue] -> phase_3_background_changes + +=== phase_3_background_changes === +~ test_phase = 3 +PHASE 3: Background Changes Testing +Background[lab-normal.png]: The laboratory appears normal at first glance. +scientist: Welcome to the lab. Everything is operating within normal parameters. +Background[lab-alert.png]: Suddenly, the room plunges into emergency lighting! +scientist: Wait... the alarm system just triggered. Something's wrong with the main servers! +scientist: We need to get to the server room immediately! + ++ [Continue] -> phase_4_mixed_format + +=== phase_4_mixed_format === +~ test_phase = 4 +PHASE 4: Mixed Format Testing +Now we'll test mixing line prefix format with traditional tag-based dialogue. +Test that both systems work together smoothly. + +Player: What's the emergency protocol? +security_guard: Protocol Alpha - all personnel to designated zones. +security_guard: The facility goes into lockdown automatically. +Narrator: Red emergency lights begin flashing throughout the building. +Player: How long until the lockdown is complete? +security_guard: Usually takes about 30 seconds for all doors to secure. +scientist: The server room needs to stay accessible for diagnostics! + ++ [Continue] -> phase_5_speaker_changes + +=== phase_5_speaker_changes === +~ test_phase = 5 +PHASE 5: Multi-Speaker Dialogue Testing +Testing rapid speaker changes using line prefixes. + +Player: Who's in charge here? +security_guard: I am, in the security department. +scientist: But I'm responsible for the lab systems. +security_guard: Right, so we need both perspectives. +Player: What do you recommend? +scientist: We need lab access first. +security_guard: I'll authorize it - follow me. +Narrator: The tension in the room is palpable as both professionals prepare for action. + ++ [Continue] -> phase_6_narrator_variations + +=== phase_6_narrator_variations === +~ test_phase = 6 +PHASE 6: Narrator Variations Testing + +Narrator: Pure narrator - no character portrait shown +Narrator[scientist]: Narrator with scientist portrait +Narrator[security_guard]: Narrator with security guard portrait +Background[hallway-dark.png]: The hallway descends into darkness +Narrator: Another narration after background change +Player: Are we ready to proceed? +Narrator[security_guard]: The guard nods solemnly + ++ [Continue] -> phase_7_comprehensive + +=== phase_7_comprehensive === +~ test_phase = 7 +PHASE 7: Comprehensive Scene +A scene combining all features. + +Background[facility-entrance.png]: You stand at the facility entrance +Narrator[security_guard]: The security guard approaches your position +security_guard: Welcome to the facility. I hope you're here for a good reason. +Player: I'm here to investigate the recent incident. +security_guard: Incident? There is no incident. +Background[security-office.png]: The guard escorts you to the security office +Narrator: Inside, screens display various security feeds +Narrator[scientist]: From a monitor, a scientist's face appears urgently +scientist: We need to talk. Alone. +security_guard: What's this about? +scientist: It's classified. Speaker confidential access only. +Narrator: The guard looks conflicted but nods slowly +security_guard: Alright. Go ahead. But I'm logging this. + ++ [Investigate Further] -> phase_8_edge_cases ++ [Conclude Test] -> ending + +=== phase_8_edge_cases === +~ test_phase = 8 +PHASE 8: Edge Cases & Stress Testing + +// Multiple consecutive narrator lines +Narrator: The tension builds. +Narrator: Everything is at stake. +Narrator: Time seems to slow down. + +// Multiple consecutive same speaker +Player: I need answers. +Player: Real answers. +Player: Not more evasions. + +// Rapid background changes +Background[room-a.png]: Location: Room A +Background[room-b.png]: Location: Room B +Background[room-c.png]: Location: Room C + +// Mixed speakers after backgrounds +scientist: The data is clear. +security_guard: But the implications are severe. +Player: We need to act fast. +Narrator: The three of you exchange worried glances. + ++ [Conclude] -> ending + +=== ending === +~ test_phase = 9 +All test phases completed! +Total phases run: {test_phase} + +Thank you for testing the line prefix speaker format implementation. +The dialogue system should now support: +✓ Line prefix format (Speaker: Text) +✓ Narrator mode (Narrator: Text) +✓ Narrator with character (Narrator[char]: Text) +✓ Background changes (Background[file]: Text) +✓ Multi-speaker dialogue +✓ Mixed format compatibility +✓ Edge case handling + +Test complete. +-> END diff --git a/scenarios/ink/test-line-prefix2.json b/scenarios/ink/test-line-prefix2.json new file mode 100644 index 0000000..1f78db2 --- /dev/null +++ b/scenarios/ink/test-line-prefix2.json @@ -0,0 +1 @@ +{"inkVersion":21,"root":[[["done",{"#n":"g-0"}],null],"done",{"start":["ev",1,"/ev",{"VAR=":"test_phase","re":true},"^PHASE 1: Line Prefix Speaker Format","\n","^Please observe the dialogue displays with speaker names derived from line prefixes.","\n",{"->":"phase_1_basic"},null],"phase_1_basic":[["^Player: Hello! I'm here for the security briefing.","\n","^security_guard: Welcome to the facility. I'll run through the basics for you.","\n","^security_guard: We use biometric authentication and keycard access throughout the building.","\n","^Player: That sounds comprehensive. Any recent incidents?","\n","^security_guard: Nothing major. System updates last week, everything's stable now.","\n","ev","str","^Continue to Phase 2","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Repeat Phase 1","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["^ ",{"->":"phase_2_narrator"},"\n",null],"c-1":["^ ",{"->":".^.^.^"},"\n",null]}],null],"phase_2_narrator":[["ev",2,"/ev",{"VAR=":"test_phase","re":true},"^PHASE 2: Narrator Mode Testing","\n","^Narrator: The sun begins to set over the facility grounds. An eerie silence fills the corridors.","\n","^Narrator: Your briefing complete, you prepare to move deeper into the building.","\n","ev","str","^Continue","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["^ ",{"->":"phase_2_narrator_with_character"},"\n",null]}],null],"phase_2_narrator_with_character":[["^Narrator[security_guard]: The guard glances nervously at the security monitors.","\n","^Narrator[security_guard]: Something seems different about the usual patrol rotation today.","\n","ev","str","^Continue","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["^ ",{"->":"phase_3_background_changes"},"\n",null]}],null],"phase_3_background_changes":[["ev",3,"/ev",{"VAR=":"test_phase","re":true},"^PHASE 3: Background Changes Testing","\n","^Background[lab-normal.png]: The laboratory appears normal at first glance.","\n","^scientist: Welcome to the lab. Everything is operating within normal parameters.","\n","^Background[lab-alert.png]: Suddenly, the room plunges into emergency lighting!","\n","^scientist: Wait... the alarm system just triggered. Something's wrong with the main servers!","\n","^scientist: We need to get to the server room immediately!","\n","ev","str","^Continue","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["^ ",{"->":"phase_4_mixed_format"},"\n",null]}],null],"phase_4_mixed_format":[["ev",4,"/ev",{"VAR=":"test_phase","re":true},"^PHASE 4: Mixed Format Testing","\n","^Now we'll test mixing line prefix format with traditional tag-based dialogue.","\n","^Test that both systems work together smoothly.","\n","^Player: What's the emergency protocol?","\n","^security_guard: Protocol Alpha - all personnel to designated zones.","\n","^security_guard: The facility goes into lockdown automatically.","\n","^Narrator: Red emergency lights begin flashing throughout the building.","\n","^Player: How long until the lockdown is complete?","\n","^security_guard: Usually takes about 30 seconds for all doors to secure.","\n","^scientist: The server room needs to stay accessible for diagnostics!","\n","ev","str","^Continue","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["^ ",{"->":"phase_5_speaker_changes"},"\n",null]}],null],"phase_5_speaker_changes":[["ev",5,"/ev",{"VAR=":"test_phase","re":true},"^PHASE 5: Multi-Speaker Dialogue Testing","\n","^Testing rapid speaker changes using line prefixes.","\n","^Player: Who's in charge here?","\n","^security_guard: I am, in the security department.","\n","^scientist: But I'm responsible for the lab systems.","\n","^security_guard: Right, so we need both perspectives.","\n","^Player: What do you recommend?","\n","^scientist: We need lab access first.","\n","^security_guard: I'll authorize it - follow me.","\n","^Narrator: The tension in the room is palpable as both professionals prepare for action.","\n","ev","str","^Continue","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["^ ",{"->":"phase_6_narrator_variations"},"\n",null]}],null],"phase_6_narrator_variations":[["ev",6,"/ev",{"VAR=":"test_phase","re":true},"^PHASE 6: Narrator Variations Testing","\n","^Narrator: Pure narrator - no character portrait shown","\n","^Narrator[scientist]: Narrator with scientist portrait","\n","^Narrator[security_guard]: Narrator with security guard portrait","\n","^Background[hallway-dark.png]: The hallway descends into darkness","\n","^Narrator: Another narration after background change","\n","^Player: Are we ready to proceed?","\n","^Narrator[security_guard]: The guard nods solemnly","\n","ev","str","^Continue","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["^ ",{"->":"phase_7_comprehensive"},"\n",null]}],null],"phase_7_comprehensive":[["ev",7,"/ev",{"VAR=":"test_phase","re":true},"^PHASE 7: Comprehensive Scene","\n","^A scene combining all features.","\n","^Background[facility-entrance.png]: You stand at the facility entrance","\n","^Narrator[security_guard]: The security guard approaches your position","\n","^security_guard: Welcome to the facility. I hope you're here for a good reason.","\n","^Player: I'm here to investigate the recent incident.","\n","^security_guard: Incident? There is no incident.","\n","^Background[security-office.png]: The guard escorts you to the security office","\n","^Narrator: Inside, screens display various security feeds","\n","^Narrator[scientist]: From a monitor, a scientist's face appears urgently","\n","^scientist: We need to talk. Alone.","\n","^security_guard: What's this about?","\n","^scientist: It's classified. Speaker confidential access only.","\n","^Narrator: The guard looks conflicted but nods slowly","\n","^security_guard: Alright. Go ahead. But I'm logging this.","\n","ev","str","^Investigate Further","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Conclude Test","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["^ ",{"->":"phase_8_edge_cases"},"\n",null],"c-1":["^ ",{"->":"ending"},"\n",null]}],null],"phase_8_edge_cases":[["ev",8,"/ev",{"VAR=":"test_phase","re":true},"^PHASE 8: Edge Cases & Stress Testing","\n","^Narrator: The tension builds.","\n","^Narrator: Everything is at stake.","\n","^Narrator: Time seems to slow down.","\n","^Player: I need answers.","\n","^Player: Real answers.","\n","^Player: Not more evasions.","\n","^Background[room-a.png]: Location: Room A","\n","^Background[room-b.png]: Location: Room B","\n","^Background[room-c.png]: Location: Room C","\n","^scientist: The data is clear.","\n","^security_guard: But the implications are severe.","\n","^Player: We need to act fast.","\n","^Narrator: The three of you exchange worried glances.","\n","ev","str","^Conclude","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["^ ",{"->":"ending"},"\n",null]}],null],"ending":["ev",9,"/ev",{"VAR=":"test_phase","re":true},"^All test phases completed!","\n","^Total phases run: ","ev",{"VAR?":"test_phase"},"out","/ev","\n","^Thank you for testing the line prefix speaker format implementation.","\n","^The dialogue system should now support:","\n","^✓ Line prefix format (Speaker: Text)","\n","^✓ Narrator mode (Narrator: Text)","\n","^✓ Narrator with character (Narrator[char]: Text)","\n","^✓ Background changes (Background[file]: Text)","\n","^✓ Multi-speaker dialogue","\n","^✓ Mixed format compatibility","\n","^✓ Edge case handling","\n","^Test complete.","\n","end",null],"global decl":["ev",0,{"VAR=":"test_phase"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/scenarios/npc-sprite-test3/scenario.json.erb b/scenarios/npc-sprite-test3/scenario.json.erb new file mode 100644 index 0000000..77018c9 --- /dev/null +++ b/scenarios/npc-sprite-test3/scenario.json.erb @@ -0,0 +1,186 @@ +{ + "scenario_brief": "Test scenario for NPC sprite functionality", + "globalVariables": { + "player_joined_organization": false + }, + "startRoom": "test_room", + + "startItemsInInventory": [ + { + "type": "phone", + "name": "Your Phone 0", + "takeable": true, + "phoneId": "player_phone", + "npcIds": ["gossip_girl"], + "observations": "Your personal phone with some interesting contacts" + } + ], + + "player": { + "id": "player", + "displayName": "Agent 0x00", + "spriteSheet": "hacker", + "spriteTalk": "assets/characters/hacker-talk.png", + "spriteConfig": { + "idleFrameStart": 20, + "idleFrameEnd": 23 + } + }, + + "rooms": { + "test_room": { + "type": "room_office", + "connections": {}, + "npcs": [ + { + "id": "neye_eve", + "displayName": "Neye Eve", + "storyPath": "scenarios/ink/neye-eve.json", + "avatar": "assets/npc/avatars/npc_adversary.png", + "phoneId": "player_phone", + "currentKnot": "start", + "npcType": "phone" + }, + { + "id": "gossip_girl", + "displayName": "Gossip Girl", + "storyPath": "scenarios/ink/gossip-girl.json", + "avatar": "assets/npc/avatars/npc_neutral.png", + "phoneId": "player_phone", + "currentKnot": "start", + "npcType": "phone", + "timedMessages": [ + { + "delay": 5000, + "message": "Hey! 👋 Got any juicy gossip for me today?", + "type": "text" + } + ] + }, + { + "id": "test_npc_front", + "displayName": "Helper NPC", + "npcType": "person", + "position": { "x": 5, "y": 3 }, + "spriteSheet": "hacker-red", + "spriteTalk": "assets/characters/hacker-red-talk.png", + "spriteConfig": { + "idleFrameStart": 20, + "idleFrameEnd": 23 + }, + "storyPath": "scenarios/ink/test-line-prefix.json", + "currentKnot": "start", + "itemsHeld": [ + { + "type": "workstation", + "name": "Crypto Analysis Station", + "takeable": true, + "observations": "A powerful workstation for cryptographic analysis" + }, + { + "type": "lockpick", + "name": "Lock Pick Kit", + "takeable": true, + "observations": "A professional lock picking kit with various picks and tension wrenches" + } + ] + }, + { + "id": "test_npc_back", + "displayName": "Back NPC", + "npcType": "person", + "position": { "x": 6, "y": 8 }, + "spriteSheet": "hacker", + "spriteConfig": { + "idleFrameStart": 20, + "idleFrameEnd": 23 + }, + "storyPath": "scenarios/ink/test2.json", + "currentKnot": "hub", + "timedConversation": { + "delay": 0, + "targetKnot": "group_meeting", + "background": "assets/backgrounds/hq1.png" + } + }, + { + "id": "test_npc_influence", + "displayName": "Influence NPC", + "npcType": "person", + "position": { "x": 2, "y": 2 }, + "spriteSheet": "hacker", + "spriteConfig": { + "idleFrameStart": 20, + "idleFrameEnd": 23 + }, + "storyPath": "scenarios/ink/influence-demo.json", + "currentKnot": "start" + }, + { + "id": "container_test_npc", + "displayName": "Equipment Officer", + "npcType": "person", + "position": { "x": 8, "y": 5 }, + "spriteSheet": "hacker-red", + "spriteTalk": "assets/characters/hacker-red-talk.png", + "spriteConfig": { + "idleFrameStart": 20, + "idleFrameEnd": 23 + }, + "storyPath": "scenarios/ink/equipment-officer.json", + "currentKnot": "start", + "itemsHeld": [ + { + "type": "phone", + "name": "Your Phone 1", + "takeable": true, + "phoneId": "player_phone", + "npcIds": ["gossip_girl"], + "observations": "Your personal phone with some interesting contacts" + }, + { + "type": "phone", + "name": "Your Phone 2", + "takeable": true, + "phoneId": "player_phone", + "npcIds": ["neye_eve"], + "observations": "Your personal phone with some interesting contacts" + }, + { + "type": "lockpick", + "name": "Basic Lock Pick Kit", + "takeable": true, + "observations": "A basic set of lock picking tools" + }, + { + "type": "lockpick", + "name": "Advanced Lock Pick Kit", + "takeable": true, + "observations": "An advanced set with precision tools" + }, + { + "type": "workstation", + "name": "Analysis Workstation", + "takeable": true, + "observations": "Portable analysis workstation" + }, + { + "type": "keycard", + "name": "Security Keycard", + "takeable": true, + "observations": "Electronic access keycard" + }, + { + "type": "notes", + "name": "Security Log", + "takeable": true, + "readable": true, + "text": "Unusual after-hours access detected:\n- CEO office: 11:30 PM\n- Server room: 2:15 AM\n- CEO office again: 3:45 AM", + "observations": "A concerning security log from last night" + } + ] + } + ] + } + } +}