From 91d4670b2a4af38aeb3eabf75992638e53ac0b35 Mon Sep 17 00:00:00 2001 From: "Z. Cliffe Schreuders" Date: Wed, 18 Feb 2026 00:47:37 +0000 Subject: [PATCH] fix: update event mapping structure and validation for NPCs - Changed 'eventMapping' to 'eventMappings' in NPC definitions for consistency. - Updated target knot for closing debrief NPC to 'start' and adjusted story path. - Enhanced validation script to check for correct eventMappings structure and properties. - Added checks for missing properties in eventMappings and timedMessages. - Provided best practice guidance for event-driven cutscenes and closing debrief implementation. --- .vscode/settings.json | 6 +- .../js/minigames/helpers/chat-helpers.js | 55 +++++- .../phone-chat/phone-chat-conversation.js | 13 +- .../phone-chat/phone-chat-minigame.js | 36 ++-- public/break_escape/js/systems/npc-manager.js | 41 +++-- .../ink/m01_closing_debrief.json | 2 +- scenarios/m01_first_contact/scenario.json.erb | 11 +- scripts/validate_scenario.rb | 162 +++++++++++++++++- 8 files changed, 284 insertions(+), 42 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 36349a0..622c6e1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,6 +2,10 @@ "cursor.general.disableHttp2": true, "chat.agent.maxRequests": 100, "chat.tools.terminal.autoApprove": { - "bin/inklecate": true + "bin/inklecate": true, + "/^ruby scripts/validate_scenario\\.rb scenarios/m01_first_contact/scenario\\.json\\.erb 2>&1 \\| grep -A 100 \"Found\\.\\*issue\"$/": { + "approve": true, + "matchCommandLine": true + } } } \ No newline at end of file diff --git a/public/break_escape/js/minigames/helpers/chat-helpers.js b/public/break_escape/js/minigames/helpers/chat-helpers.js index f78b910..784a079 100644 --- a/public/break_escape/js/minigames/helpers/chat-helpers.js +++ b/public/break_escape/js/minigames/helpers/chat-helpers.js @@ -52,7 +52,10 @@ export function processGameActionTags(tags, ui) { if (!trimmedTag) return; // Parse action and parameter (format: "action:param" or "action") - const [action, param] = trimmedTag.split(':').map(s => s.trim()); + // Split only on FIRST colon to preserve colons in parameters (e.g., set_global:var:value) + const colonIndex = trimmedTag.indexOf(':'); + const action = colonIndex === -1 ? trimmedTag : trimmedTag.substring(0, colonIndex).trim(); + const param = colonIndex === -1 ? '' : trimmedTag.substring(colonIndex + 1).trim(); let result = { action, param, success: false, message: '' }; @@ -404,6 +407,56 @@ export function processGameActionTags(tags, ui) { } break; + case 'set_global': + if (param) { + // Format: set_global:variableName:value + const parts = param.split(':'); + const varName = parts[0]?.trim(); + const varValue = parts[1]?.trim(); + + if (!varName) { + result.message = '⚠️ set_global tag missing variable name'; + console.warn(result.message); + break; + } + + // Parse value (support booleans, numbers, strings) + let parsedValue = varValue; + if (varValue === 'true') parsedValue = true; + else if (varValue === 'false') parsedValue = false; + else if (!isNaN(varValue)) parsedValue = Number(varValue); + + // Set the global variable + if (!window.gameState) { + window.gameState = {}; + } + if (!window.gameState.globalVariables) { + window.gameState.globalVariables = {}; + } + + const oldValue = window.gameState.globalVariables[varName]; + window.gameState.globalVariables[varName] = parsedValue; + + console.log(`🌐 Set global variable: ${varName} = ${parsedValue} (was: ${oldValue})`); + + // Emit event for any listeners (including NPCManager event mappings) + if (window.eventDispatcher) { + window.eventDispatcher.emit(`global_variable_changed:${varName}`, { + name: varName, + value: parsedValue, + oldValue: oldValue + }); + console.log(`📡 Emitted event: global_variable_changed:${varName}`); + } + + result.success = true; + result.message = `🌐 Global variable set: ${varName} = ${parsedValue}`; + } else { + result.message = '⚠️ set_global tag missing parameters'; + console.warn(result.message); + } + break; + default: // Unknown tag, log but don't fail console.log(`ℹ️ Unknown game action tag: ${action}`); diff --git a/public/break_escape/js/minigames/phone-chat/phone-chat-conversation.js b/public/break_escape/js/minigames/phone-chat/phone-chat-conversation.js index 56b3311..ee3ac07 100644 --- a/public/break_escape/js/minigames/phone-chat/phone-chat-conversation.js +++ b/public/break_escape/js/minigames/phone-chat/phone-chat-conversation.js @@ -274,26 +274,29 @@ export default class PhoneChatConversation { } /** - * Process Ink tags for game actions + * Process conversation-specific Ink tags (like #exit_conversation) + * Note: Game action tags (#set_global, #unlock_door, etc.) are processed + * later by processGameActionTags in phone-chat-minigame.js * @param {Array} tags - Tags from current line */ processTags(tags) { if (!tags || tags.length === 0) return; tags.forEach(tag => { - console.log(`🏷️ Processing tag: ${tag}`); - // Tag format: "action:param1:param2" const [action, ...params] = tag.split(':'); switch (action.trim().toLowerCase()) { case 'end_conversation': + case 'exit_conversation': + console.log(`🏷️ Processing conversation tag: ${tag}`); this.handleEndConversation(); break; default: - // Unknown tags are okay - they might be processed by the UI layer - console.log(`ℹ️ Unhandled tag: ${action}`); + // Other tags are game action tags - will be processed by minigame layer + // Don't log them here to avoid confusion + break; } }); } diff --git a/public/break_escape/js/minigames/phone-chat/phone-chat-minigame.js b/public/break_escape/js/minigames/phone-chat/phone-chat-minigame.js index 662adf6..6e0efa7 100644 --- a/public/break_escape/js/minigames/phone-chat/phone-chat-minigame.js +++ b/public/break_escape/js/minigames/phone-chat/phone-chat-minigame.js @@ -705,24 +705,8 @@ export class PhoneChatMinigame extends MinigameScene { } }); - // Check if the story output contains the exit_conversation tag - const shouldExit = accumulatedTags.some(tag => tag.includes('exit_conversation')); - - // If this was an exit choice, close the minigame - if (shouldExit) { - console.log('🚪 Exit conversation tag detected - closing minigame'); - - // Save state before closing - this.saveStoryState(); - - // Close minigame after brief delay to show final message - setTimeout(() => { - this.complete(true); - }, 1500); - return; - } - - // Process all accumulated game action tags + // Process all accumulated game action tags FIRST (before exit check) + // This ensures tags like #set_global are processed before conversation closes console.log('🔍 Checking for tags after choice...', { hasTags: accumulatedTags.length > 0, tagsLength: accumulatedTags.length, @@ -736,6 +720,22 @@ export class PhoneChatMinigame extends MinigameScene { console.log('⚠️ No tags to process after choice'); } + // Check if the story output contains the exit_conversation tag + const shouldExit = accumulatedTags.some(tag => tag.includes('exit_conversation')); + + // If this was an exit choice, close the minigame + if (shouldExit) { + console.log('🚪 Exit conversation tag detected - closing minigame'); + + // Save state before closing + this.saveStoryState(); + + // Complete immediately - don't delay, as this might trigger an event-driven cutscene + // that needs to start right after this minigame closes + this.complete(true); + return; + } + // Check if conversation ended AFTER displaying the final text if (lastResult.hasEnded) { console.log('🏁 Conversation ended'); diff --git a/public/break_escape/js/systems/npc-manager.js b/public/break_escape/js/systems/npc-manager.js index 8799f5b..9c4822a 100644 --- a/public/break_escape/js/systems/npc-manager.js +++ b/public/break_escape/js/systems/npc-manager.js @@ -80,6 +80,13 @@ export default class NPCManager { entry.phoneId = 'player_phone'; } + // Normalize eventMapping (singular) to eventMappings (plural) for backward compatibility + if (entry.eventMapping && !entry.eventMappings) { + console.log(`🔧 Normalizing eventMapping → eventMappings for ${realId}`); + entry.eventMappings = entry.eventMapping; + delete entry.eventMapping; // Remove the incorrect property + } + this.npcs.set(realId, entry); // Register in global character registry for speaker resolution @@ -95,6 +102,8 @@ export default class NPCManager { // Set up event listeners for auto-mapping if (entry.eventMappings && this.eventDispatcher) { this._setupEventMappings(realId, entry.eventMappings); + } else if (entry.eventMappings && !this.eventDispatcher) { + console.error(`❌ ${realId} has eventMappings but eventDispatcher is not available!`); } // Schedule timed messages if any are defined @@ -379,13 +388,19 @@ export default class NPCManager { if (config.condition) { let conditionMet = false; + console.log(`🔍 Evaluating condition for ${eventPattern}:`, config.condition); + console.log(` Event data:`, eventData); + if (typeof config.condition === 'function') { conditionMet = config.condition(eventData, npc); } else if (typeof config.condition === 'string') { // Evaluate condition string as JavaScript try { const data = eventData; // Make 'data' available in eval scope + const value = eventData?.value; // Extract value for common pattern + const name = eventData?.name; // Extract name for common pattern conditionMet = eval(config.condition); + console.log(` Condition result: ${conditionMet}`); } catch (error) { console.error(`❌ Error evaluating condition: ${config.condition}`, error); return; @@ -394,6 +409,7 @@ export default class NPCManager { if (!conditionMet) { console.log(`🚫 Event ${eventPattern} condition not met:`, config.condition); + console.log(` Expected: value === true, Got: value =`, eventData?.value); return; } } @@ -503,18 +519,21 @@ export default class NPCManager { console.log(`✅ Closed current minigame`); } - // Start the person-chat minigame + // Start the person-chat minigame after a brief delay to allow previous minigame to fully clean up if (window.MinigameFramework) { - console.log(`✅ Starting person-chat minigame for ${npcId}`); - const knotToUse = config.targetKnot || config.knot || npc.currentKnot; - window.MinigameFramework.startMinigame('person-chat', null, { - npcId: npc.id, - startKnot: knotToUse, - background: config.background || null, - scenario: window.gameScenario - }); - console.log(`[NPCManager] Event '${eventPattern}' triggered for NPC '${npcId}' → person-chat conversation`); - return; // Exit early - person-chat is handling it + console.log(`⏳ Waiting 500ms before starting person-chat cutscene for ${npcId}`); + setTimeout(() => { + console.log(`✅ Starting person-chat minigame for ${npcId}`); + const knotToUse = config.targetKnot || config.knot || npc.currentKnot; + window.MinigameFramework.startMinigame('person-chat', null, { + npcId: npc.id, + startKnot: knotToUse, + background: config.background || null, + scenario: window.gameScenario + }); + console.log(`[NPCManager] Event '${eventPattern}' triggered for NPC '${npcId}' → person-chat conversation`); + }, 500); // 500ms delay for cleanup + return; // Exit early - person-chat will start after delay } else { console.warn(`⚠️ MinigameFramework not available for person-chat`); } diff --git a/scenarios/m01_first_contact/ink/m01_closing_debrief.json b/scenarios/m01_first_contact/ink/m01_closing_debrief.json index cbbc079..368e7f8 100644 --- a/scenarios/m01_first_contact/ink/m01_closing_debrief.json +++ b/scenarios/m01_first_contact/ink/m01_closing_debrief.json @@ -1 +1 @@ -{"inkVersion":21,"root":[[["done",{"#n":"g-0"}],null],"done",{"start":[["#","^speaker:agent_0x99","/#","^Agent 0x99: ","ev",{"VAR?":"player_name"},"out","/ev","^, return to HQ for debrief.","\n","^Agent 0x99: Operation Shatter is neutralized. Let's review what happened.","\n","ev","str","^On my way","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["\n",{"->":"debrief_location"},null]}],null],"debrief_location":[["^[SAFETYNET HQ - Agent 0x99's Office]","\n","#","^speaker:agent_0x99","/#","^Agent 0x99: ","ev",{"VAR?":"player_name"},"out","/ev","^. First, I need you to understand what you accomplished today.","\n","^Agent 0x99: Those casualty projections—42 to 85 people. Diabetics. Elderly. People with anxiety disorders.","\n","^Agent 0x99: They're going to live. Because of you.","\n","ev","str","^That's what matters","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^It was close. Too close.","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n",{"->":"evidence_review"},null],"c-1":["\n",{"->":"close_call"},null]}],null],"close_call":["^Agent 0x99: 72 hours. That's how close we cut it.","\n","^Agent 0x99: If our AI hadn't flagged those data collection patterns, if you hadn't found the documentation...","\n","^Agent 0x99: But you did. And those people will never know how close they came.","\n",{"->":"evidence_review"},null],"evidence_review":["^Agent 0x99: Let's review what you recovered.","\n","ev",{"VAR?":"found_casualty_projections"},{"VAR?":"found_target_database"},"&&","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->":"evidence_complete"},{"->":".^.^.^.8"},null]}],"nop","\n","ev",{"VAR?":"found_casualty_projections"},{"VAR?":"found_target_database"},"!","&&","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->":"evidence_partial_projections"},{"->":".^.^.^.17"},null]}],"nop","\n","ev",{"VAR?":"found_casualty_projections"},"!",{"VAR?":"found_target_database"},"&&","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->":"evidence_partial_database"},{"->":".^.^.^.26"},null]}],"nop","\n","ev",{"VAR?":"found_casualty_projections"},"!",{"VAR?":"found_target_database"},"!","&&","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->":"evidence_minimal"},{"->":".^.^.^.36"},null]}],"nop","\n",null],"evidence_complete":[["^Agent 0x99: You found everything. The casualty projections. The target demographics database. The complete Operation Shatter documentation.","\n","^Agent 0x99: This is exactly what prosecutors need. Derek's signature on the death calculations. The Architect's approval. The targeting methodology.","\n","^Agent 0x99: Thorough work. You didn't rush past the evidence.","\n","ev","str","^I wanted to make sure we had enough to convict","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^The more I found, the worse it got","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n","^Agent 0x99: You do. There's no walking away from this for Derek.","\n",{"->":"npc_interactions"},null],"c-1":["\n","^Agent 0x99: Yeah. Reading those casualty projections... that stays with you.","\n",{"->":"npc_interactions"},null]}],null],"evidence_partial_projections":["^Agent 0x99: You found the casualty projections—the smoking gun. Derek's death calculations, The Architect's approval.","\n","^Agent 0x99: We're missing the full target demographics database, but that's recoverable from their servers now that we have access.","\n","^Agent 0x99: The critical evidence is secured. That's what matters for prosecution.","\n",{"->":"npc_interactions"},null],"evidence_partial_database":["^Agent 0x99: You found the target demographics database—2.3 million people profiled for vulnerability.","\n","^Agent 0x99: We're still missing the casualty projections document, but the database alone proves intent. They were targeting vulnerable populations deliberately.","\n","^Agent 0x99: Our forensics team is recovering the rest from their systems.","\n",{"->":"npc_interactions"},null],"evidence_minimal":["^Agent 0x99: The core Operation Shatter documentation is still being recovered by our forensics team.","\n","^Agent 0x99: The operation is stopped, but we're relying on digital forensics for the prosecution evidence.","\n","^Agent 0x99: Next time, prioritize document recovery. Physical evidence is harder to deny in court.","\n",{"->":"npc_interactions"},null],"npc_interactions":["ev",{"VAR?":"talked_to_kevin"},{"VAR?":"talked_to_maya"},"&&","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->":"worked_with_both"},{"->":".^.^.^.6"},null]}],"nop","\n","ev",{"VAR?":"talked_to_kevin"},{"VAR?":"talked_to_maya"},"!","&&","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->":"worked_with_kevin"},{"->":".^.^.^.15"},null]}],"nop","\n","ev",{"VAR?":"talked_to_kevin"},"!",{"VAR?":"talked_to_maya"},"&&","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->":"worked_with_maya"},{"->":".^.^.^.24"},null]}],"nop","\n","ev",{"VAR?":"talked_to_kevin"},"!",{"VAR?":"talked_to_maya"},"!","&&","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->":"worked_alone"},{"->":".^.^.^.34"},null]}],"nop","\n",null],"worked_with_both":["^Agent 0x99: I noticed you worked with both Kevin and Maya.","\n","^Agent 0x99: Kevin gave you legitimate access—that's the IT contractor cover working as intended.","\n","ev",{"VAR?":"maya_identity_protected"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^Agent 0x99: And Maya... you protected her identity. She's safe. She can continue her journalism without looking over her shoulder.","\n","^Agent 0x99: That matters. She took a risk contacting us.","\n",{"->":".^.^.^.9"},null]}],[{"->":".^.b"},{"b":["\n","^Agent 0x99: Maya's identity was compromised during the operation. We're relocating her for safety.","\n","^Agent 0x99: She'll be okay, but her career at Viral Dynamics is over. Collateral damage.","\n",{"->":".^.^.^.9"},null]}],"nop","\n",{"->":"kevin_frame_discussion"},null],"worked_with_kevin":["^Agent 0x99: Kevin's cooperation was valuable. The IT contractor cover worked perfectly.","\n","^Agent 0x99: You got legitimate access without raising suspicion. That's clean infiltration.","\n",{"->":"kevin_frame_discussion"},null],"worked_with_maya":["^Agent 0x99: Maya was taking a risk talking to you. I hope you appreciated that.","\n","ev",{"VAR?":"maya_identity_protected"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^Agent 0x99: Her identity stayed protected. She can continue investigating on her own terms now.","\n",{"->":".^.^.^.7"},null]}],[{"->":".^.b"},{"b":["\n","^Agent 0x99: Unfortunately, her identity was compromised. We're handling her protection.","\n",{"->":".^.^.^.7"},null]}],"nop","\n",{"->":"kevin_frame_discussion"},null],"worked_alone":["^Agent 0x99: You handled this mostly solo. Independent approach.","\n","^Agent 0x99: Sometimes that's the right call. Fewer people involved means fewer potential leaks.","\n",{"->":"kevin_frame_discussion"},null],"kevin_frame_discussion":["ev",{"VAR?":"kevin_choice"},"str","^","/str","==","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->":"security_audit_review"},{"->":".^.^.^.8"},null]}],"nop","\n","ev",{"VAR?":"kevin_choice"},"str","^warn","/str","==","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->":"kevin_warned"},{"->":".^.^.^.18"},null]}],"nop","\n","ev",{"VAR?":"kevin_choice"},"str","^evidence","/str","==","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->":"kevin_evidence"},{"->":".^.^.^.28"},null]}],"nop","\n","ev",{"VAR?":"kevin_choice"},"str","^ignore","/str","==","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->":"kevin_ignored"},{"->":".^.^.^.38"},null]}],"nop","\n",null],"kevin_warned":[["^Agent 0x99: I saw in your report that you warned Kevin about the frame-up.","\n","^Agent 0x99: That was risky. If he'd panicked, if Derek had noticed...","\n","ev","str","^He deserved to know","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^I couldn't just let Derek destroy him","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n","^Agent 0x99: He did. And now he's lawyered up, documented everything. When the prosecutors came for him, he was ready.","\n","^Agent 0x99: His career is intact. His life isn't ruined. Because you took five minutes to be decent.","\n",{"->":"kevin_outcome_positive"},null],"c-1":["\n","^Agent 0x99: You're right. Kevin didn't ask to be part of this. He helped you because he's a good person.","\n","^Agent 0x99: Derek would have fed him to the wolves. You didn't let that happen.","\n",{"->":"kevin_outcome_positive"},null]}],null],"kevin_evidence":["^Agent 0x99: The contingency files you left for investigators—that was smart.","\n","^Agent 0x99: When the follow-up team found them, they immediately flagged Kevin as a victim, not a suspect.","\n","^Agent 0x99: He never even knew he was in danger. Woke up, went to work, found out his company was a front for terrorists, and went home to his family.","\n","^Agent 0x99: Clean. Professional. And kind.","\n",{"->":"kevin_outcome_positive"},null],"kevin_outcome_positive":["^Agent 0x99: You know what Derek would have said? \"Kevin is acceptable collateral damage.\"","\n","^Agent 0x99: You disagreed. That matters.","\n","^Agent 0x99: Not every agent would have taken the time. Not every agent would have cared.","\n",{"->":"security_audit_review"},null],"kevin_ignored":[["^Agent 0x99: Kevin Park was arrested this morning.","\n","ev","str","^What?","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^The frame-up worked?","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n",{"->":"kevin_arrest_details"},null],"c-1":["\n",{"->":"kevin_arrest_details"},null]}],null],"kevin_arrest_details":[["^Agent 0x99: Derek's contingency plan activated automatically when Viral Dynamics' systems were seized. Fake logs, forged emails.","\n","^Agent 0x99: Kevin spent six hours in interrogation before our team figured out he was being framed.","\n","^Agent 0x99: He's cleared now. But he's traumatized. His neighbors saw him taken away in handcuffs. His kids watched.","\n","ev","str","^I... I saw the files. I knew.","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^The mission had to come first","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n","^Agent 0x99: I know. It's in Derek's computer logs.","\n","^Agent 0x99: You made a choice. Focus on the mission. Let Kevin be collateral damage.","\n","^Agent 0x99: Sometimes that's the right call. Sometimes the mission really does come first.","\n","^Agent 0x99: But Kevin's going to need therapy. His kids are going to need therapy.","\n","^Agent 0x99: Just... remember that. Next time you're weighing priorities.","\n",{"->":"security_audit_review"},null],"c-1":["\n","^Agent 0x99: Did it? You still stopped Operation Shatter. You still caught Derek.","\n","^Agent 0x99: Would five minutes to warn Kevin have changed that?","\n","^Agent 0x99: I'm not judging. Field decisions are hard. But consequences are real.","\n","^Agent 0x99: Kevin's kids watched him get arrested. That happened because of a choice you made.","\n","^Agent 0x99: Live with it. Learn from it.","\n",{"->":"security_audit_review"},null]}],null],"security_audit_review":["ev",{"VAR?":"security_audit_completed"},"/ev",[{"->":".^.b","c":true},{"b":["\n",{"->":"audit_feedback"},{"->":".^.^.^.4"},null]}],"nop","\n","ev",{"VAR?":"security_audit_completed"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->":"no_audit_feedback"},{"->":".^.^.^.11"},null]}],"nop","\n",null],"audit_feedback":["^Agent 0x99: I noticed you gave Kevin a security assessment during your cover operation.","\n","ev",{"VAR?":"audit_correct_answers"},4,">=","/ev",[{"->":".^.b","c":true},{"b":["\n","^Agent 0x99: Your security analysis was excellent. You identified every major vulnerability correctly.","\n","^Agent 0x99: Physical access controls, Derek's suspicious access patterns, predictable passwords, Patricia's firing, and Derek's unjustified network segmentation.","\n","^Agent 0x99: That's professional-grade security consulting. Your cover was completely convincing.","\n","ev","str","^I wanted to maintain my cover properly","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^The vulnerabilities were pretty obvious once I looked","/str","/ev",{"*":".^.c-1","flg":4},{"->":".^.^.^.8"},{"c-0":["\n","^Agent 0x99: And you did. Kevin trusted you completely because you demonstrated real expertise.","\n","^Agent 0x99: That kind of authentic tradecraft makes all the difference in deep cover work.","\n",{"->":"derek_discussion"},null],"c-1":["\n","^Agent 0x99: Maybe to you. But recognizing them under pressure, while maintaining cover, while gathering intelligence on Operation Shatter?","\n","^Agent 0x99: That's good work. Don't undersell it.","\n",{"->":"derek_discussion"},null]}]}],"nop","\n","ev",{"VAR?":"audit_correct_answers"},3,"==","/ev",[{"->":".^.b","c":true},{"b":["\n","^Agent 0x99: Your security analysis was solid. Three out of five correct assessments.","\n","^Agent 0x99: You identified most of the key vulnerabilities—enough to maintain credibility with Kevin.","\n","^Agent 0x99: A few blind spots, but nothing that compromised your cover or the mission.","\n","ev","str","^Which ones did I miss?","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^I was focused on the bigger picture","/str","/ev",{"*":".^.c-1","flg":4},{"->":".^.^.^.16"},{"c-0":["\n","ev",{"VAR?":"audit_wrong_answers"},1,">=","/ev",[{"->":".^.b","c":true},{"b":["\n","^Agent 0x99: You underestimated a couple of the vulnerabilities Kevin had already flagged.","\n","^Agent 0x99: In the field, always trust when an insider is telling you something's wrong. They see the patterns we miss.","\n",{"->":".^.^.^.7"},null]}],"nop","\n",{"->":"derek_discussion"},null],"c-1":["\n","^Agent 0x99: Fair enough. Your primary mission was Operation Shatter, not a comprehensive security audit.","\n","^Agent 0x99: Kevin bought your cover. That's what mattered.","\n",{"->":"derek_discussion"},null]}]}],"nop","\n","ev",{"VAR?":"audit_correct_answers"},2,"<=","/ev",[{"->":".^.b","c":true},{"b":["\n","^Agent 0x99: Your security assessment was... rough. Two or fewer correct answers out of five.","\n","^Agent 0x99: Kevin was asking you about obvious vulnerabilities he'd already identified. You dismissed most of them.","\n","ev","str","^I was trying not to alarm him","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Security assessment wasn't my priority","/str","/ev",{"*":".^.c-1","flg":4},{"->":".^.^.^.24"},{"c-0":["\n","^Agent 0x99: Understandable. But when an insider is showing you red flags, validate their concerns.","\n","^Agent 0x99: You're supposed to be a security expert. Kevin needed you to see what he was seeing.","\n","^Agent 0x99: Fortunately, your other actions kept him cooperative. But that assessment almost blew your cover.","\n",{"->":"derek_discussion"},null],"c-1":["\n","^Agent 0x99: It's part of your cover identity. When you're undercover as an expert, you need to be that expert.","\n","^Agent 0x99: Kevin noticed you were missing things he'd already flagged. That could have raised suspicions.","\n","^Agent 0x99: Mission succeeded anyway, but... work on your tradecraft. Deep cover requires authenticity.","\n",{"->":"derek_discussion"},null]}]}],"nop","\n",null],"no_audit_feedback":["^Agent 0x99: I noticed you didn't provide Kevin with a security assessment during your cover operation.","\n","^Agent 0x99: That's fine—it wasn't required for the mission. But it could have strengthened your cover credibility.","\n","^Agent 0x99: Next time you're undercover with a professional identity, look for opportunities to demonstrate authentic expertise.","\n","^Agent 0x99: It builds trust. And trust gives you access.","\n",{"->":"derek_discussion"},null],"derek_discussion":["^Agent 0x99: Now, about Derek Lawson...","\n","ev",{"VAR?":"final_choice"},"str","^fight","/str","==","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->":"consequence_fight"},{"->":".^.^.^.10"},null]}],"nop","\n","ev",{"VAR?":"final_choice"},"str","^arrest","/str","==","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->":"consequence_arrest"},{"->":".^.^.^.20"},null]}],"nop","\n","ev",{"VAR?":"final_choice"},"str","^recruit","/str","==","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->":"consequence_recruit"},{"->":".^.^.^.30"},null]}],"nop","\n","ev",{"VAR?":"final_choice"},"str","^expose","/str","==","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->":"consequence_expose"},{"->":".^.^.^.40"},null]}],"nop","\n",{"->":"consequence_arrest"},null],"consequence_fight":[["^Agent 0x99: You took Derek down physically. Aggressive approach.","\n","^Agent 0x99: Walk me through your tactical reasoning.","\n","ev","str","^He was planning mass murder. I ended the threat.","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^He calculated those deaths so coldly. I reacted.","/str","/ev",{"*":".^.c-1","flg":4},"ev","str","^He reached for something. Threat assessment.","/str","/ev",{"*":".^.c-2","flg":4},{"c-0":["\n","^Agent 0x99: Direct and effective. Derek's in custody, Operation Shatter is stopped.","\n","^Agent 0x99: His lawyers will make noise about excessive force, but you had full field authorization.","\n",{"->":"fight_outcome"},null],"c-1":["\n","^Agent 0x99: I saw the footage. The way he talked about those casualties like statistics...","\n","^Agent 0x99: Understandable reaction. Derek's narrative now is that SAFETYNET attacked him, but that's lawyer talk.","\n",{"->":"fight_outcome"},null],"c-2":["\n","^Agent 0x99: Field decisions happen fast. I saw the footage—he did move toward his desk.","\n","^Agent 0x99: You neutralized a potential threat. Textbook response.","\n",{"->":"fight_outcome_justified"},null]}],null],"fight_outcome":[["^Agent 0x99: Derek's in custody. Mission accomplished.","\n","^Agent 0x99: His defense team is spinning the excessive force angle, but you have field immunity as a SAFETYNET operative.","\n","^Agent 0x99: The confrontation will be part of his trial narrative. His lawyers will use it. Worth noting for future ops.","\n","ev",{"VAR?":"found_casualty_projections"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^Agent 0x99: The hard evidence you recovered—his casualty projections—that's what convicts him. The confrontation is just noise.","\n",{"->":".^.^.^.11"},null]}],[{"->":".^.b"},{"b":["\n","^Agent 0x99: Forensics is building the evidence case. The physical confrontation adds complexity to prosecution, but he's not walking free.","\n",{"->":".^.^.^.11"},null]}],"nop","\n","^Agent 0x99: Different approach than a quiet arrest, but the result's the same. He's neutralized.","\n","ev","str","^Mission complete. That's what matters.","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^He planned to kill 85 people. No sympathy.","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n","^Agent 0x99: Agreed. Operation Shatter stopped, lives saved.","\n",{"->":"phase_3_discussion"},null],"c-1":["\n","^Agent 0x99: None deserved. Derek's done. ENTROPY lost this round.","\n",{"->":"phase_3_discussion"},null]}],null],"fight_outcome_justified":[["^Agent 0x99: Derek's in custody. You neutralized a potentially armed hostile.","\n","^Agent 0x99: Turned out he was reaching for a phone, not a weapon. But split-second decisions don't have hindsight.","\n","^Agent 0x99: Response was controlled. Minimal injury. Threat neutralized.","\n","ev",{"VAR?":"found_casualty_projections"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^Agent 0x99: The evidence backs up the arrest—his casualty projections with his signature.","\n",{"->":".^.^.^.11"},null]}],[{"->":".^.b"},{"b":["\n","^Agent 0x99: Forensics is pulling evidence from his systems. Prosecution case is solid.","\n",{"->":".^.^.^.11"},null]}],"nop","\n","^Agent 0x99: His lawyers will file complaints, but review board will clear it. Standard hostile engagement protocol.","\n","^Agent 0x99: Clean tactical response to a perceived threat.","\n","ev","str","^Threat assessment was correct.","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^I'd make the same call again.","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n","^Agent 0x99: Agreed. You made the right call in the moment.","\n",{"->":"phase_3_discussion"},null],"c-1":["\n","^Agent 0x99: That's what field agents do. Assess, act, neutralize.","\n",{"->":"phase_3_discussion"},null]}],null],"consequence_arrest":[["^Agent 0x99: You chose arrest. Legal prosecution through proper channels.","\n","^Agent 0x99: He's not cooperating—true believers rarely do. But we have the evidence. His signature on the casualty projections.","\n","^Agent 0x99: He'll spend decades in prison explaining why 85 dead people would have been \"educational.\"","\n","ev","str","^Will the charges stick?","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^He seemed so certain he was right","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n","^Agent 0x99: Conspiracy to commit mass murder. Terrorism. Computer crimes.","\n","ev",{"VAR?":"found_casualty_projections"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^Agent 0x99: With the casualty projections you recovered? He's done.","\n",{"->":".^.^.^.8"},null]}],[{"->":".^.b"},{"b":["\n","^Agent 0x99: We're building the evidence case. It'll take longer, but he's not walking free.","\n",{"->":".^.^.^.8"},null]}],"nop","\n",{"->":"phase_3_discussion"},null],"c-1":["\n","^Agent 0x99: That's what makes true believers dangerous. They've rationalized everything.","\n","^Agent 0x99: Derek doesn't think he's a murderer. He thinks he's an educator.","\n","^Agent 0x99: The jury will disagree.","\n",{"->":"phase_3_discussion"},null]}],null],"consequence_recruit":[["^Agent 0x99: You offered him a chance to cooperate. Turn informant.","\n","^Agent 0x99: I heard his answer. \"I will never betray ENTROPY.\"","\n","^Agent 0x99: True believers don't turn, ","ev",{"VAR?":"player_name"},"out","/ev","^. They'd rather go to prison as martyrs.","\n","ev","str","^I had to try","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^I thought maybe he'd want to reduce his sentence","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n","^Agent 0x99: It was worth asking. His refusal tells us something about ENTROPY's organizational culture.","\n","^Agent 0x99: These aren't mercenaries. They're ideologues. That's useful intelligence.","\n",{"->":"recruit_outcome"},null],"c-1":["\n","^Agent 0x99: A rational person would. Derek isn't rational. He's a believer.","\n","^Agent 0x99: His ideology matters more than his freedom.","\n",{"->":"recruit_outcome"},null]}],null],"recruit_outcome":["^Agent 0x99: He's in custody now. Same outcome as arrest.","\n","^Agent 0x99: But we learned something important: ENTROPY attracts true believers. They won't flip for deals.","\n","^Agent 0x99: We'll need to find other ways to get inside intelligence.","\n",{"->":"phase_3_discussion"},null],"consequence_expose":[["^Agent 0x99: Public disclosure. Full transparency.","\n","^Agent 0x99: The casualty projections are on every news site. Derek's death calculations. The targeting lists.","\n","^Agent 0x99: The world now knows what ENTROPY was willing to do.","\n","ev","str","^People deserve to know","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Let them see who Derek really is","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n","^Agent 0x99: Maybe. But now ENTROPY knows we're onto Operation Shatter methodology.","\n","^Agent 0x99: They'll adapt. Change tactics. We've lost the element of surprise.","\n",{"->":"expose_outcome"},null],"c-1":["\n","^Agent 0x99: They're seeing. \"Acceptable losses.\" \"Educational deaths.\"","\n","^Agent 0x99: The public is horrified. Good. They should be.","\n",{"->":"expose_outcome"},null]}],null],"expose_outcome":["^Agent 0x99: Director Netherton is... not happy. We don't usually expose methods.","\n","^Agent 0x99: But ENTROPY's tactics are now public knowledge. People know to verify. To question.","\n","^Agent 0x99: In a twisted way, you taught the lesson Derek wanted—just without the deaths.","\n","ev",{"VAR?":"maya_identity_protected"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^Agent 0x99: At least Maya's identity stayed protected through all this.","\n",{"->":".^.^.^.11"},null]}],[{"->":".^.b"},{"b":["\n","^Agent 0x99: Maya's identity came out in the disclosure. She's being handled as a public whistleblower now.","\n",{"->":".^.^.^.11"},null]}],"nop","\n",{"->":"phase_3_discussion"},null],"phase_3_discussion":[["^Agent 0x99: ","ev",{"VAR?":"player_name"},"out","/ev","^, I need you to understand what we learned today.","\n","^Agent 0x99: We always thought ENTROPY was sophisticated cybercrime. Data theft. Corporate espionage.","\n","^Agent 0x99: This is different. Derek had casualty projections. He calculated deaths and considered them acceptable.","\n","ev","str","^They're willing to kill for their ideology","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^What does that mean for future missions?","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n",{"->":"true_nature"},null],"c-1":["\n",{"->":"true_nature"},null]}],null],"true_nature":[["^Agent 0x99: It means we're not fighting criminals. We're fighting true believers.","\n","^Agent 0x99: People who think killing people is \"education.\" Who see deaths as \"acceptable losses.\"","\n","^Agent 0x99: And if Social Fabric was willing to do this... what are the other cells planning?","\n","ev","str","^Who is The Architect?","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^How do we stop them?","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n",{"->":"architect_mystery"},null],"c-1":["\n",{"->":"stop_entropy"},null]}],null],"architect_mystery":[["^Agent 0x99: We don't know. ENTROPY's leader, strategist, philosopher.","\n","^Agent 0x99: Derek quoted The Architect. Believed every word. Got approval to kill 85 people.","\n","^Agent 0x99: Whoever they are, they've built an organization of true believers.","\n","ev","str","^We have to find them","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^That sounds terrifying","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n","^Agent 0x99: Every cell we disrupt, every operation we stop, brings us closer.","\n","ev",{"VAR?":"lore_collected"},3,">=","/ev",[{"->":".^.b","c":true},{"b":["\n","^Agent 0x99: The intelligence you collected today gives us new leads. The Architect's communication patterns. Their philosophical fingerprints.","\n",{"->":".^.^.^.9"},null]}],"nop","\n",{"->":"mission_end"},null],"c-1":["\n","^Agent 0x99: It is. But that's why SAFETYNET exists.","\n","^Agent 0x99: Today, you stood between ENTROPY and 85 people they'd sacrifice.","\n",{"->":"mission_end"},null]}],null],"stop_entropy":["^Agent 0x99: Cell by cell. Operation by operation.","\n","^Agent 0x99: Today you stopped Operation Shatter. Tomorrow, we stop the next one.","\n",{"->":"mission_end"},null],"mission_end":["^Agent 0x99: First mission complete. Lives saved. True believer in custody.","\n","ev",{"VAR?":"lore_collected"},3,">=","/ev",[{"->":".^.b","c":true},{"b":["\n","^Agent 0x99: And ","ev",{"VAR?":"lore_collected"},"out","/ev","^ intelligence fragments recovered. That's thorough investigative work.","\n",{"->":".^.^.^.8"},null]}],"nop","\n","ev",{"VAR?":"lore_collected"},0,"==","/ev",[{"->":".^.b","c":true},{"b":["\n","^Agent 0x99: You focused on the primary objectives. Efficient.","\n","^Agent 0x99: But next time, look for additional intelligence. Context helps future operations.","\n",{"->":".^.^.^.16"},null]}],"nop","\n","^Agent 0x99: Get some rest. Next briefing is in 48 hours.","\n","^Agent 0x99: And ","ev",{"VAR?":"player_name"},"out","/ev","^? You did more than complete a mission today.","\n","^Agent 0x99: You saved lives. Real people who will never know your name.","\n","^Agent 0x99: That's what SAFETYNET is for.","\n","^[MISSION COMPLETE: FIRST CONTACT]","\n","ev",{"VAR?":"final_choice"},"str","^fight","/str","==","/ev",[{"->":".^.b","c":true},{"b":["\n","^[OUTCOME: Derek Lawson subdued by force - Hostile engagement neutralized]","\n",{"->":".^.^.^.41"},null]}],"nop","\n","ev",{"VAR?":"final_choice"},"str","^arrest","/str","==","/ev",[{"->":".^.b","c":true},{"b":["\n","^[OUTCOME: Derek Lawson arrested - Prosecution pending]","\n",{"->":".^.^.^.51"},null]}],"nop","\n","ev",{"VAR?":"final_choice"},"str","^recruit","/str","==","/ev",[{"->":".^.b","c":true},{"b":["\n","^[OUTCOME: Derek Lawson arrested - Refused cooperation]","\n",{"->":".^.^.^.61"},null]}],"nop","\n","ev",{"VAR?":"final_choice"},"str","^expose","/str","==","/ev",[{"->":".^.b","c":true},{"b":["\n","^[OUTCOME: Full public disclosure - ENTROPY methods exposed]","\n",{"->":".^.^.^.71"},null]}],"nop","\n","^[OPERATION SHATTER: NEUTRALIZED]","\n","^[LIVES SAVED: 42-85 (estimated)]","\n","ev",{"VAR?":"found_casualty_projections"},{"VAR?":"found_target_database"},"&&","/ev",[{"->":".^.b","c":true},{"b":["\n","^[EVIDENCE: COMPLETE - All critical documents recovered]","\n",{"->":".^.^.^.83"},null]}],"nop","\n","ev",{"VAR?":"found_casualty_projections"},{"VAR?":"found_target_database"},"!","&&","/ev",[{"->":".^.b","c":true},{"b":["\n","^[EVIDENCE: SUBSTANTIAL - Casualty projections secured]","\n",{"->":".^.^.^.92"},null]}],"nop","\n","ev",{"VAR?":"found_casualty_projections"},"!",{"VAR?":"found_target_database"},"&&","/ev",[{"->":".^.b","c":true},{"b":["\n","^[EVIDENCE: SUBSTANTIAL - Target database secured]","\n",{"->":".^.^.^.101"},null]}],"nop","\n","ev",{"VAR?":"found_casualty_projections"},"!",{"VAR?":"found_target_database"},"!","&&","/ev",[{"->":".^.b","c":true},{"b":["\n","^[EVIDENCE: PARTIAL - Forensics team recovering additional files]","\n",{"->":".^.^.^.111"},null]}],"nop","\n","ev",{"VAR?":"maya_identity_protected"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^[MAYA CHEN: Identity protected]","\n",{"->":".^.^.^.118"},null]}],[{"->":".^.b"},{"b":["\n","^[MAYA CHEN: Identity compromised - Under SAFETYNET protection]","\n",{"->":".^.^.^.118"},null]}],"nop","\n","ev",{"VAR?":"kevin_protected"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^[KEVIN PARK: Protected from frame-up - Career intact]","\n",{"->":".^.^.^.124"},null]}],"nop","\n","ev",{"VAR?":"kevin_choice"},"str","^ignore","/str","==","/ev",[{"->":".^.b","c":true},{"b":["\n","^[KEVIN PARK: Arrested, later cleared - Traumatized but free]","\n",{"->":".^.^.^.134"},null]}],"nop","\n","ev",{"VAR?":"kevin_choice"},"str","^","/str","==","/ev",[{"->":".^.b","c":true},{"b":["\n","^[KEVIN PARK: Status unknown]","\n",{"->":".^.^.^.144"},null]}],"nop","\n","^[The Architect remains at large...]","\n","#","^exit_conversation","/#","end",null],"global decl":["ev","str","^Agent 0x00","/str",{"VAR=":"player_name"},"str","^","/str",{"VAR=":"final_choice"},0,{"VAR=":"objectives_completed"},0,{"VAR=":"lore_collected"},false,{"VAR=":"found_casualty_projections"},false,{"VAR=":"found_target_database"},false,{"VAR=":"talked_to_maya"},false,{"VAR=":"talked_to_kevin"},true,{"VAR=":"maya_identity_protected"},"str","^","/str",{"VAR=":"kevin_choice"},false,{"VAR=":"kevin_protected"},false,{"VAR=":"security_audit_completed"},0,{"VAR=":"audit_correct_answers"},0,{"VAR=":"audit_wrong_answers"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[[["done",{"#n":"g-0"}],null],"done",{"start":[["^[SAFETYNET HQ - Agent 0x99's Office]","\n","#","^speaker:agent_0x99","/#","^Agent 0x99: ","ev",{"VAR?":"player_name"},"out","/ev","^. First, I need you to understand what you accomplished today.","\n","^Agent 0x99: Those casualty projections—42 to 85 people. Diabetics. Elderly. People with anxiety disorders.","\n","^Agent 0x99: They're going to live. Because of you.","\n","ev","str","^That's what matters","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^It was close. Too close.","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n",{"->":"evidence_review"},null],"c-1":["\n",{"->":"close_call"},null]}],null],"close_call":["^Agent 0x99: 72 hours. That's how close we cut it.","\n","^Agent 0x99: If our AI hadn't flagged those data collection patterns, if you hadn't found the documentation...","\n","^Agent 0x99: But you did. And those people will never know how close they came.","\n",{"->":"evidence_review"},null],"evidence_review":["^Agent 0x99: Let's review what you recovered.","\n","ev",{"VAR?":"found_casualty_projections"},{"VAR?":"found_target_database"},"&&","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->":"evidence_complete"},{"->":".^.^.^.8"},null]}],"nop","\n","ev",{"VAR?":"found_casualty_projections"},{"VAR?":"found_target_database"},"!","&&","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->":"evidence_partial_projections"},{"->":".^.^.^.17"},null]}],"nop","\n","ev",{"VAR?":"found_casualty_projections"},"!",{"VAR?":"found_target_database"},"&&","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->":"evidence_partial_database"},{"->":".^.^.^.26"},null]}],"nop","\n","ev",{"VAR?":"found_casualty_projections"},"!",{"VAR?":"found_target_database"},"!","&&","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->":"evidence_minimal"},{"->":".^.^.^.36"},null]}],"nop","\n",null],"evidence_complete":[["^Agent 0x99: You found everything. The casualty projections. The target demographics database. The complete Operation Shatter documentation.","\n","^Agent 0x99: This is exactly what prosecutors need. Derek's signature on the death calculations. The Architect's approval. The targeting methodology.","\n","^Agent 0x99: Thorough work. You didn't rush past the evidence.","\n","ev","str","^I wanted to make sure we had enough to convict","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^The more I found, the worse it got","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n","^Agent 0x99: You do. There's no walking away from this for Derek.","\n",{"->":"npc_interactions"},null],"c-1":["\n","^Agent 0x99: Yeah. Reading those casualty projections... that stays with you.","\n",{"->":"npc_interactions"},null]}],null],"evidence_partial_projections":["^Agent 0x99: You found the casualty projections—the smoking gun. Derek's death calculations, The Architect's approval.","\n","^Agent 0x99: We're missing the full target demographics database, but that's recoverable from their servers now that we have access.","\n","^Agent 0x99: The critical evidence is secured. That's what matters for prosecution.","\n",{"->":"npc_interactions"},null],"evidence_partial_database":["^Agent 0x99: You found the target demographics database—2.3 million people profiled for vulnerability.","\n","^Agent 0x99: We're still missing the casualty projections document, but the database alone proves intent. They were targeting vulnerable populations deliberately.","\n","^Agent 0x99: Our forensics team is recovering the rest from their systems.","\n",{"->":"npc_interactions"},null],"evidence_minimal":["^Agent 0x99: The core Operation Shatter documentation is still being recovered by our forensics team.","\n","^Agent 0x99: The operation is stopped, but we're relying on digital forensics for the prosecution evidence.","\n","^Agent 0x99: Next time, prioritize document recovery. Physical evidence is harder to deny in court.","\n",{"->":"npc_interactions"},null],"npc_interactions":["ev",{"VAR?":"talked_to_kevin"},{"VAR?":"talked_to_maya"},"&&","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->":"worked_with_both"},{"->":".^.^.^.6"},null]}],"nop","\n","ev",{"VAR?":"talked_to_kevin"},{"VAR?":"talked_to_maya"},"!","&&","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->":"worked_with_kevin"},{"->":".^.^.^.15"},null]}],"nop","\n","ev",{"VAR?":"talked_to_kevin"},"!",{"VAR?":"talked_to_maya"},"&&","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->":"worked_with_maya"},{"->":".^.^.^.24"},null]}],"nop","\n","ev",{"VAR?":"talked_to_kevin"},"!",{"VAR?":"talked_to_maya"},"!","&&","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->":"worked_alone"},{"->":".^.^.^.34"},null]}],"nop","\n",null],"worked_with_both":["^Agent 0x99: I noticed you worked with both Kevin and Maya.","\n","^Agent 0x99: Kevin gave you legitimate access—that's the IT contractor cover working as intended.","\n","ev",{"VAR?":"maya_identity_protected"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^Agent 0x99: And Maya... you protected her identity. She's safe. She can continue her journalism without looking over her shoulder.","\n","^Agent 0x99: That matters. She took a risk contacting us.","\n",{"->":".^.^.^.9"},null]}],[{"->":".^.b"},{"b":["\n","^Agent 0x99: Maya's identity was compromised during the operation. We're relocating her for safety.","\n","^Agent 0x99: She'll be okay, but her career at Viral Dynamics is over. Collateral damage.","\n",{"->":".^.^.^.9"},null]}],"nop","\n",{"->":"kevin_frame_discussion"},null],"worked_with_kevin":["^Agent 0x99: Kevin's cooperation was valuable. The IT contractor cover worked perfectly.","\n","^Agent 0x99: You got legitimate access without raising suspicion. That's clean infiltration.","\n",{"->":"kevin_frame_discussion"},null],"worked_with_maya":["^Agent 0x99: Maya was taking a risk talking to you. I hope you appreciated that.","\n","ev",{"VAR?":"maya_identity_protected"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^Agent 0x99: Her identity stayed protected. She can continue investigating on her own terms now.","\n",{"->":".^.^.^.7"},null]}],[{"->":".^.b"},{"b":["\n","^Agent 0x99: Unfortunately, her identity was compromised. We're handling her protection.","\n",{"->":".^.^.^.7"},null]}],"nop","\n",{"->":"kevin_frame_discussion"},null],"worked_alone":["^Agent 0x99: You handled this mostly solo. Independent approach.","\n","^Agent 0x99: Sometimes that's the right call. Fewer people involved means fewer potential leaks.","\n",{"->":"kevin_frame_discussion"},null],"kevin_frame_discussion":["ev",{"VAR?":"kevin_choice"},"str","^","/str","==","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->":"security_audit_review"},{"->":".^.^.^.8"},null]}],"nop","\n","ev",{"VAR?":"kevin_choice"},"str","^warn","/str","==","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->":"kevin_warned"},{"->":".^.^.^.18"},null]}],"nop","\n","ev",{"VAR?":"kevin_choice"},"str","^evidence","/str","==","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->":"kevin_evidence"},{"->":".^.^.^.28"},null]}],"nop","\n","ev",{"VAR?":"kevin_choice"},"str","^ignore","/str","==","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->":"kevin_ignored"},{"->":".^.^.^.38"},null]}],"nop","\n",null],"kevin_warned":[["^Agent 0x99: I saw in your report that you warned Kevin about the frame-up.","\n","^Agent 0x99: That was risky. If he'd panicked, if Derek had noticed...","\n","ev","str","^He deserved to know","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^I couldn't just let Derek destroy him","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n","^Agent 0x99: He did. And now he's lawyered up, documented everything. When the prosecutors came for him, he was ready.","\n","^Agent 0x99: His career is intact. His life isn't ruined. Because you took five minutes to be decent.","\n",{"->":"kevin_outcome_positive"},null],"c-1":["\n","^Agent 0x99: You're right. Kevin didn't ask to be part of this. He helped you because he's a good person.","\n","^Agent 0x99: Derek would have fed him to the wolves. You didn't let that happen.","\n",{"->":"kevin_outcome_positive"},null]}],null],"kevin_evidence":["^Agent 0x99: The contingency files you left for investigators—that was smart.","\n","^Agent 0x99: When the follow-up team found them, they immediately flagged Kevin as a victim, not a suspect.","\n","^Agent 0x99: He never even knew he was in danger. Woke up, went to work, found out his company was a front for terrorists, and went home to his family.","\n","^Agent 0x99: Clean. Professional. And kind.","\n",{"->":"kevin_outcome_positive"},null],"kevin_outcome_positive":["^Agent 0x99: You know what Derek would have said? \"Kevin is acceptable collateral damage.\"","\n","^Agent 0x99: You disagreed. That matters.","\n","^Agent 0x99: Not every agent would have taken the time. Not every agent would have cared.","\n",{"->":"security_audit_review"},null],"kevin_ignored":[["^Agent 0x99: Kevin Park was arrested this morning.","\n","ev","str","^What?","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^The frame-up worked?","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n",{"->":"kevin_arrest_details"},null],"c-1":["\n",{"->":"kevin_arrest_details"},null]}],null],"kevin_arrest_details":[["^Agent 0x99: Derek's contingency plan activated automatically when Viral Dynamics' systems were seized. Fake logs, forged emails.","\n","^Agent 0x99: Kevin spent six hours in interrogation before our team figured out he was being framed.","\n","^Agent 0x99: He's cleared now. But he's traumatized. His neighbors saw him taken away in handcuffs. His kids watched.","\n","ev","str","^I... I saw the files. I knew.","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^The mission had to come first","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n","^Agent 0x99: I know. It's in Derek's computer logs.","\n","^Agent 0x99: You made a choice. Focus on the mission. Let Kevin be collateral damage.","\n","^Agent 0x99: Sometimes that's the right call. Sometimes the mission really does come first.","\n","^Agent 0x99: But Kevin's going to need therapy. His kids are going to need therapy.","\n","^Agent 0x99: Just... remember that. Next time you're weighing priorities.","\n",{"->":"security_audit_review"},null],"c-1":["\n","^Agent 0x99: Did it? You still stopped Operation Shatter. You still caught Derek.","\n","^Agent 0x99: Would five minutes to warn Kevin have changed that?","\n","^Agent 0x99: I'm not judging. Field decisions are hard. But consequences are real.","\n","^Agent 0x99: Kevin's kids watched him get arrested. That happened because of a choice you made.","\n","^Agent 0x99: Live with it. Learn from it.","\n",{"->":"security_audit_review"},null]}],null],"security_audit_review":["ev",{"VAR?":"security_audit_completed"},"/ev",[{"->":".^.b","c":true},{"b":["\n",{"->":"audit_feedback"},{"->":".^.^.^.4"},null]}],"nop","\n","ev",{"VAR?":"security_audit_completed"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->":"no_audit_feedback"},{"->":".^.^.^.11"},null]}],"nop","\n",null],"audit_feedback":["^Agent 0x99: I noticed you gave Kevin a security assessment during your cover operation.","\n","ev",{"VAR?":"audit_correct_answers"},4,">=","/ev",[{"->":".^.b","c":true},{"b":["\n","^Agent 0x99: Your security analysis was excellent. You identified every major vulnerability correctly.","\n","^Agent 0x99: Physical access controls, Derek's suspicious access patterns, predictable passwords, Patricia's firing, and Derek's unjustified network segmentation.","\n","^Agent 0x99: That's professional-grade security consulting. Your cover was completely convincing.","\n","ev","str","^I wanted to maintain my cover properly","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^The vulnerabilities were pretty obvious once I looked","/str","/ev",{"*":".^.c-1","flg":4},{"->":".^.^.^.8"},{"c-0":["\n","^Agent 0x99: And you did. Kevin trusted you completely because you demonstrated real expertise.","\n","^Agent 0x99: That kind of authentic tradecraft makes all the difference in deep cover work.","\n",{"->":"derek_discussion"},null],"c-1":["\n","^Agent 0x99: Maybe to you. But recognizing them under pressure, while maintaining cover, while gathering intelligence on Operation Shatter?","\n","^Agent 0x99: That's good work. Don't undersell it.","\n",{"->":"derek_discussion"},null]}]}],"nop","\n","ev",{"VAR?":"audit_correct_answers"},3,"==","/ev",[{"->":".^.b","c":true},{"b":["\n","^Agent 0x99: Your security analysis was solid. Three out of five correct assessments.","\n","^Agent 0x99: You identified most of the key vulnerabilities—enough to maintain credibility with Kevin.","\n","^Agent 0x99: A few blind spots, but nothing that compromised your cover or the mission.","\n","ev","str","^Which ones did I miss?","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^I was focused on the bigger picture","/str","/ev",{"*":".^.c-1","flg":4},{"->":".^.^.^.16"},{"c-0":["\n","ev",{"VAR?":"audit_wrong_answers"},1,">=","/ev",[{"->":".^.b","c":true},{"b":["\n","^Agent 0x99: You underestimated a couple of the vulnerabilities Kevin had already flagged.","\n","^Agent 0x99: In the field, always trust when an insider is telling you something's wrong. They see the patterns we miss.","\n",{"->":".^.^.^.7"},null]}],"nop","\n",{"->":"derek_discussion"},null],"c-1":["\n","^Agent 0x99: Fair enough. Your primary mission was Operation Shatter, not a comprehensive security audit.","\n","^Agent 0x99: Kevin bought your cover. That's what mattered.","\n",{"->":"derek_discussion"},null]}]}],"nop","\n","ev",{"VAR?":"audit_correct_answers"},2,"<=","/ev",[{"->":".^.b","c":true},{"b":["\n","^Agent 0x99: Your security assessment was... rough. Two or fewer correct answers out of five.","\n","^Agent 0x99: Kevin was asking you about obvious vulnerabilities he'd already identified. You dismissed most of them.","\n","ev","str","^I was trying not to alarm him","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Security assessment wasn't my priority","/str","/ev",{"*":".^.c-1","flg":4},{"->":".^.^.^.24"},{"c-0":["\n","^Agent 0x99: Understandable. But when an insider is showing you red flags, validate their concerns.","\n","^Agent 0x99: You're supposed to be a security expert. Kevin needed you to see what he was seeing.","\n","^Agent 0x99: Fortunately, your other actions kept him cooperative. But that assessment almost blew your cover.","\n",{"->":"derek_discussion"},null],"c-1":["\n","^Agent 0x99: It's part of your cover identity. When you're undercover as an expert, you need to be that expert.","\n","^Agent 0x99: Kevin noticed you were missing things he'd already flagged. That could have raised suspicions.","\n","^Agent 0x99: Mission succeeded anyway, but... work on your tradecraft. Deep cover requires authenticity.","\n",{"->":"derek_discussion"},null]}]}],"nop","\n",null],"no_audit_feedback":["^Agent 0x99: I noticed you didn't provide Kevin with a security assessment during your cover operation.","\n","^Agent 0x99: That's fine—it wasn't required for the mission. But it could have strengthened your cover credibility.","\n","^Agent 0x99: Next time you're undercover with a professional identity, look for opportunities to demonstrate authentic expertise.","\n","^Agent 0x99: It builds trust. And trust gives you access.","\n",{"->":"derek_discussion"},null],"derek_discussion":["^Agent 0x99: Now, about Derek Lawson...","\n","ev",{"VAR?":"final_choice"},"str","^fight","/str","==","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->":"consequence_fight"},{"->":".^.^.^.10"},null]}],"nop","\n","ev",{"VAR?":"final_choice"},"str","^arrest","/str","==","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->":"consequence_arrest"},{"->":".^.^.^.20"},null]}],"nop","\n","ev",{"VAR?":"final_choice"},"str","^recruit","/str","==","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->":"consequence_recruit"},{"->":".^.^.^.30"},null]}],"nop","\n","ev",{"VAR?":"final_choice"},"str","^expose","/str","==","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->":"consequence_expose"},{"->":".^.^.^.40"},null]}],"nop","\n",{"->":"consequence_arrest"},null],"consequence_fight":[["^Agent 0x99: You took Derek down physically. Aggressive approach.","\n","^Agent 0x99: Walk me through your tactical reasoning.","\n","ev","str","^He was planning mass murder. I ended the threat.","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^He calculated those deaths so coldly. I reacted.","/str","/ev",{"*":".^.c-1","flg":4},"ev","str","^He reached for something. Threat assessment.","/str","/ev",{"*":".^.c-2","flg":4},{"c-0":["\n","^Agent 0x99: Direct and effective. Derek's in custody, Operation Shatter is stopped.","\n","^Agent 0x99: His lawyers will make noise about excessive force, but you had full field authorization.","\n",{"->":"fight_outcome"},null],"c-1":["\n","^Agent 0x99: I saw the footage. The way he talked about those casualties like statistics...","\n","^Agent 0x99: Understandable reaction. Derek's narrative now is that SAFETYNET attacked him, but that's lawyer talk.","\n",{"->":"fight_outcome"},null],"c-2":["\n","^Agent 0x99: Field decisions happen fast. I saw the footage—he did move toward his desk.","\n","^Agent 0x99: You neutralized a potential threat. Textbook response.","\n",{"->":"fight_outcome_justified"},null]}],null],"fight_outcome":[["^Agent 0x99: Derek's in custody. Mission accomplished.","\n","^Agent 0x99: His defense team is spinning the excessive force angle, but you have field immunity as a SAFETYNET operative.","\n","^Agent 0x99: The confrontation will be part of his trial narrative. His lawyers will use it. Worth noting for future ops.","\n","ev",{"VAR?":"found_casualty_projections"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^Agent 0x99: The hard evidence you recovered—his casualty projections—that's what convicts him. The confrontation is just noise.","\n",{"->":".^.^.^.11"},null]}],[{"->":".^.b"},{"b":["\n","^Agent 0x99: Forensics is building the evidence case. The physical confrontation adds complexity to prosecution, but he's not walking free.","\n",{"->":".^.^.^.11"},null]}],"nop","\n","^Agent 0x99: Different approach than a quiet arrest, but the result's the same. He's neutralized.","\n","ev","str","^Mission complete. That's what matters.","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^He planned to kill 85 people. No sympathy.","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n","^Agent 0x99: Agreed. Operation Shatter stopped, lives saved.","\n",{"->":"phase_3_discussion"},null],"c-1":["\n","^Agent 0x99: None deserved. Derek's done. ENTROPY lost this round.","\n",{"->":"phase_3_discussion"},null]}],null],"fight_outcome_justified":[["^Agent 0x99: Derek's in custody. You neutralized a potentially armed hostile.","\n","^Agent 0x99: Turned out he was reaching for a phone, not a weapon. But split-second decisions don't have hindsight.","\n","^Agent 0x99: Response was controlled. Minimal injury. Threat neutralized.","\n","ev",{"VAR?":"found_casualty_projections"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^Agent 0x99: The evidence backs up the arrest—his casualty projections with his signature.","\n",{"->":".^.^.^.11"},null]}],[{"->":".^.b"},{"b":["\n","^Agent 0x99: Forensics is pulling evidence from his systems. Prosecution case is solid.","\n",{"->":".^.^.^.11"},null]}],"nop","\n","^Agent 0x99: His lawyers will file complaints, but review board will clear it. Standard hostile engagement protocol.","\n","^Agent 0x99: Clean tactical response to a perceived threat.","\n","ev","str","^Threat assessment was correct.","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^I'd make the same call again.","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n","^Agent 0x99: Agreed. You made the right call in the moment.","\n",{"->":"phase_3_discussion"},null],"c-1":["\n","^Agent 0x99: That's what field agents do. Assess, act, neutralize.","\n",{"->":"phase_3_discussion"},null]}],null],"consequence_arrest":[["^Agent 0x99: You chose arrest. Legal prosecution through proper channels.","\n","^Agent 0x99: He's not cooperating—true believers rarely do. But we have the evidence. His signature on the casualty projections.","\n","^Agent 0x99: He'll spend decades in prison explaining why 85 dead people would have been \"educational.\"","\n","ev","str","^Will the charges stick?","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^He seemed so certain he was right","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n","^Agent 0x99: Conspiracy to commit mass murder. Terrorism. Computer crimes.","\n","ev",{"VAR?":"found_casualty_projections"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^Agent 0x99: With the casualty projections you recovered? He's done.","\n",{"->":".^.^.^.8"},null]}],[{"->":".^.b"},{"b":["\n","^Agent 0x99: We're building the evidence case. It'll take longer, but he's not walking free.","\n",{"->":".^.^.^.8"},null]}],"nop","\n",{"->":"phase_3_discussion"},null],"c-1":["\n","^Agent 0x99: That's what makes true believers dangerous. They've rationalized everything.","\n","^Agent 0x99: Derek doesn't think he's a murderer. He thinks he's an educator.","\n","^Agent 0x99: The jury will disagree.","\n",{"->":"phase_3_discussion"},null]}],null],"consequence_recruit":[["^Agent 0x99: You offered him a chance to cooperate. Turn informant.","\n","^Agent 0x99: I heard his answer. \"I will never betray ENTROPY.\"","\n","^Agent 0x99: True believers don't turn, ","ev",{"VAR?":"player_name"},"out","/ev","^. They'd rather go to prison as martyrs.","\n","ev","str","^I had to try","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^I thought maybe he'd want to reduce his sentence","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n","^Agent 0x99: It was worth asking. His refusal tells us something about ENTROPY's organizational culture.","\n","^Agent 0x99: These aren't mercenaries. They're ideologues. That's useful intelligence.","\n",{"->":"recruit_outcome"},null],"c-1":["\n","^Agent 0x99: A rational person would. Derek isn't rational. He's a believer.","\n","^Agent 0x99: His ideology matters more than his freedom.","\n",{"->":"recruit_outcome"},null]}],null],"recruit_outcome":["^Agent 0x99: He's in custody now. Same outcome as arrest.","\n","^Agent 0x99: But we learned something important: ENTROPY attracts true believers. They won't flip for deals.","\n","^Agent 0x99: We'll need to find other ways to get inside intelligence.","\n",{"->":"phase_3_discussion"},null],"consequence_expose":[["^Agent 0x99: Public disclosure. Full transparency.","\n","^Agent 0x99: The casualty projections are on every news site. Derek's death calculations. The targeting lists.","\n","^Agent 0x99: The world now knows what ENTROPY was willing to do.","\n","ev","str","^People deserve to know","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Let them see who Derek really is","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n","^Agent 0x99: Maybe. But now ENTROPY knows we're onto Operation Shatter methodology.","\n","^Agent 0x99: They'll adapt. Change tactics. We've lost the element of surprise.","\n",{"->":"expose_outcome"},null],"c-1":["\n","^Agent 0x99: They're seeing. \"Acceptable losses.\" \"Educational deaths.\"","\n","^Agent 0x99: The public is horrified. Good. They should be.","\n",{"->":"expose_outcome"},null]}],null],"expose_outcome":["^Agent 0x99: Director Netherton is... not happy. We don't usually expose methods.","\n","^Agent 0x99: But ENTROPY's tactics are now public knowledge. People know to verify. To question.","\n","^Agent 0x99: In a twisted way, you taught the lesson Derek wanted—just without the deaths.","\n","ev",{"VAR?":"maya_identity_protected"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^Agent 0x99: At least Maya's identity stayed protected through all this.","\n",{"->":".^.^.^.11"},null]}],[{"->":".^.b"},{"b":["\n","^Agent 0x99: Maya's identity came out in the disclosure. She's being handled as a public whistleblower now.","\n",{"->":".^.^.^.11"},null]}],"nop","\n",{"->":"phase_3_discussion"},null],"phase_3_discussion":[["^Agent 0x99: ","ev",{"VAR?":"player_name"},"out","/ev","^, I need you to understand what we learned today.","\n","^Agent 0x99: We always thought ENTROPY was sophisticated cybercrime. Data theft. Corporate espionage.","\n","^Agent 0x99: This is different. Derek had casualty projections. He calculated deaths and considered them acceptable.","\n","ev","str","^They're willing to kill for their ideology","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^What does that mean for future missions?","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n",{"->":"true_nature"},null],"c-1":["\n",{"->":"true_nature"},null]}],null],"true_nature":[["^Agent 0x99: It means we're not fighting criminals. We're fighting true believers.","\n","^Agent 0x99: People who think killing people is \"education.\" Who see deaths as \"acceptable losses.\"","\n","^Agent 0x99: And if Social Fabric was willing to do this... what are the other cells planning?","\n","ev","str","^Who is The Architect?","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^How do we stop them?","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n",{"->":"architect_mystery"},null],"c-1":["\n",{"->":"stop_entropy"},null]}],null],"architect_mystery":[["^Agent 0x99: We don't know. ENTROPY's leader, strategist, philosopher.","\n","^Agent 0x99: Derek quoted The Architect. Believed every word. Got approval to kill 85 people.","\n","^Agent 0x99: Whoever they are, they've built an organization of true believers.","\n","ev","str","^We have to find them","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^That sounds terrifying","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n","^Agent 0x99: Every cell we disrupt, every operation we stop, brings us closer.","\n","ev",{"VAR?":"lore_collected"},3,">=","/ev",[{"->":".^.b","c":true},{"b":["\n","^Agent 0x99: The intelligence you collected today gives us new leads. The Architect's communication patterns. Their philosophical fingerprints.","\n",{"->":".^.^.^.9"},null]}],"nop","\n",{"->":"mission_end"},null],"c-1":["\n","^Agent 0x99: It is. But that's why SAFETYNET exists.","\n","^Agent 0x99: Today, you stood between ENTROPY and 85 people they'd sacrifice.","\n",{"->":"mission_end"},null]}],null],"stop_entropy":["^Agent 0x99: Cell by cell. Operation by operation.","\n","^Agent 0x99: Today you stopped Operation Shatter. Tomorrow, we stop the next one.","\n",{"->":"mission_end"},null],"mission_end":["^Agent 0x99: First mission complete. Lives saved. True believer in custody.","\n","ev",{"VAR?":"lore_collected"},3,">=","/ev",[{"->":".^.b","c":true},{"b":["\n","^Agent 0x99: And ","ev",{"VAR?":"lore_collected"},"out","/ev","^ intelligence fragments recovered. That's thorough investigative work.","\n",{"->":".^.^.^.8"},null]}],"nop","\n","ev",{"VAR?":"lore_collected"},0,"==","/ev",[{"->":".^.b","c":true},{"b":["\n","^Agent 0x99: You focused on the primary objectives. Efficient.","\n","^Agent 0x99: But next time, look for additional intelligence. Context helps future operations.","\n",{"->":".^.^.^.16"},null]}],"nop","\n","^Agent 0x99: Get some rest. Next briefing is in 48 hours.","\n","^Agent 0x99: And ","ev",{"VAR?":"player_name"},"out","/ev","^? You did more than complete a mission today.","\n","^Agent 0x99: You saved lives. Real people who will never know your name.","\n","^Agent 0x99: That's what SAFETYNET is for.","\n","^[MISSION COMPLETE: FIRST CONTACT]","\n","ev",{"VAR?":"final_choice"},"str","^fight","/str","==","/ev",[{"->":".^.b","c":true},{"b":["\n","^[OUTCOME: Derek Lawson subdued by force - Hostile engagement neutralized]","\n",{"->":".^.^.^.41"},null]}],"nop","\n","ev",{"VAR?":"final_choice"},"str","^arrest","/str","==","/ev",[{"->":".^.b","c":true},{"b":["\n","^[OUTCOME: Derek Lawson arrested - Prosecution pending]","\n",{"->":".^.^.^.51"},null]}],"nop","\n","ev",{"VAR?":"final_choice"},"str","^recruit","/str","==","/ev",[{"->":".^.b","c":true},{"b":["\n","^[OUTCOME: Derek Lawson arrested - Refused cooperation]","\n",{"->":".^.^.^.61"},null]}],"nop","\n","ev",{"VAR?":"final_choice"},"str","^expose","/str","==","/ev",[{"->":".^.b","c":true},{"b":["\n","^[OUTCOME: Full public disclosure - ENTROPY methods exposed]","\n",{"->":".^.^.^.71"},null]}],"nop","\n","^[OPERATION SHATTER: NEUTRALIZED]","\n","^[LIVES SAVED: 42-85 (estimated)]","\n","ev",{"VAR?":"found_casualty_projections"},{"VAR?":"found_target_database"},"&&","/ev",[{"->":".^.b","c":true},{"b":["\n","^[EVIDENCE: COMPLETE - All critical documents recovered]","\n",{"->":".^.^.^.83"},null]}],"nop","\n","ev",{"VAR?":"found_casualty_projections"},{"VAR?":"found_target_database"},"!","&&","/ev",[{"->":".^.b","c":true},{"b":["\n","^[EVIDENCE: SUBSTANTIAL - Casualty projections secured]","\n",{"->":".^.^.^.92"},null]}],"nop","\n","ev",{"VAR?":"found_casualty_projections"},"!",{"VAR?":"found_target_database"},"&&","/ev",[{"->":".^.b","c":true},{"b":["\n","^[EVIDENCE: SUBSTANTIAL - Target database secured]","\n",{"->":".^.^.^.101"},null]}],"nop","\n","ev",{"VAR?":"found_casualty_projections"},"!",{"VAR?":"found_target_database"},"!","&&","/ev",[{"->":".^.b","c":true},{"b":["\n","^[EVIDENCE: PARTIAL - Forensics team recovering additional files]","\n",{"->":".^.^.^.111"},null]}],"nop","\n","ev",{"VAR?":"maya_identity_protected"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^[MAYA CHEN: Identity protected]","\n",{"->":".^.^.^.118"},null]}],[{"->":".^.b"},{"b":["\n","^[MAYA CHEN: Identity compromised - Under SAFETYNET protection]","\n",{"->":".^.^.^.118"},null]}],"nop","\n","ev",{"VAR?":"kevin_protected"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^[KEVIN PARK: Protected from frame-up - Career intact]","\n",{"->":".^.^.^.124"},null]}],"nop","\n","ev",{"VAR?":"kevin_choice"},"str","^ignore","/str","==","/ev",[{"->":".^.b","c":true},{"b":["\n","^[KEVIN PARK: Arrested, later cleared - Traumatized but free]","\n",{"->":".^.^.^.134"},null]}],"nop","\n","ev",{"VAR?":"kevin_choice"},"str","^","/str","==","/ev",[{"->":".^.b","c":true},{"b":["\n","^[KEVIN PARK: Status unknown]","\n",{"->":".^.^.^.144"},null]}],"nop","\n","^[The Architect remains at large...]","\n","#","^exit_conversation","/#","end",null],"global decl":["ev","str","^Agent 0x00","/str",{"VAR=":"player_name"},"str","^","/str",{"VAR=":"final_choice"},0,{"VAR=":"objectives_completed"},0,{"VAR=":"lore_collected"},false,{"VAR=":"found_casualty_projections"},false,{"VAR=":"found_target_database"},false,{"VAR=":"talked_to_maya"},false,{"VAR=":"talked_to_kevin"},true,{"VAR=":"maya_identity_protected"},"str","^","/str",{"VAR=":"kevin_choice"},false,{"VAR=":"kevin_protected"},false,{"VAR=":"security_audit_completed"},0,{"VAR=":"audit_correct_answers"},0,{"VAR=":"audit_wrong_answers"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/scenarios/m01_first_contact/scenario.json.erb b/scenarios/m01_first_contact/scenario.json.erb index 923dd68..85c5521 100644 --- a/scenarios/m01_first_contact/scenario.json.erb +++ b/scenarios/m01_first_contact/scenario.json.erb @@ -380,12 +380,12 @@ password_hints = "Common passwords: Marketing123, Campaign2024, Viral_Dynamics_A "id": "closing_debrief_person", "displayName": "Agent 0x99", "npcType": "person", - "eventMapping": [ + "eventMappings": [ { "eventPattern": "global_variable_changed:start_debrief_cutscene", "condition": "value === true", "conversationMode": "person-chat", - "targetKnot": "debrief_location", + "targetKnot": "start", "background": "assets/backgrounds/hq1.png", "onceOnly": true } @@ -393,12 +393,13 @@ password_hints = "Common passwords: Marketing123, Campaign2024, Viral_Dynamics_A "position": { "x": 500, "y": 500 }, "spriteSheet": "female_spy", "avatar": "assets/characters/female_spy_headshot.png", + "spriteTalk": "assets/characters/female_spy_headshot.png", "spriteConfig": { "idleFrameRate": 6, "walkFrameRate": 10 }, - "storyPath": "scenarios/m01_first_contact/ink/m01_phone_agent0x99.json", - "currentKnot": "debrief_location", + "storyPath": "scenarios/m01_first_contact/ink/m01_closing_debrief.json", + "currentKnot": "start", "behavior": { "initiallyHidden": true } @@ -1023,6 +1024,8 @@ password_hints = "Common passwords: Marketing123, Campaign2024, Viral_Dynamics_A "talked_to_kevin": false, "maya_identity_protected": true, "discussed_operation": false, + + "start_debrief_cutscene": false, "operation_shatter_reported": false, "objectives_completed": 0, diff --git a/scripts/validate_scenario.rb b/scripts/validate_scenario.rb index 6e82836..e2d7e91 100755 --- a/scripts/validate_scenario.rb +++ b/scripts/validate_scenario.rb @@ -331,6 +331,43 @@ def check_common_issues(json_data) end end + # Validate eventMapping vs eventMappings (parameter name mismatch) + if npc['eventMapping'] && !npc['eventMappings'] + issues << "❌ INVALID: '#{path}' uses 'eventMapping' (singular) - should use 'eventMappings' (plural). The NPCManager expects 'eventMappings' and won't register event listeners with 'eventMapping'" + end + + # Validate eventMappings structure + if npc['eventMappings'] + # Check if it's an array + unless npc['eventMappings'].is_a?(Array) + issues << "❌ INVALID: '#{path}' eventMappings is not an array - must be an array of event mapping objects" + else + npc['eventMappings'].each_with_index do |mapping, idx| + mapping_path = "#{path}/eventMappings[#{idx}]" + + # Check for incorrect property name (knot vs targetKnot) + if mapping['knot'] && !mapping['targetKnot'] + issues << "❌ INVALID: '#{mapping_path}' uses 'knot' property - should use 'targetKnot' instead. Change 'knot' to 'targetKnot'" + end + + # Check for missing eventPattern + unless mapping['eventPattern'] + issues << "❌ INVALID: '#{mapping_path}' missing required 'eventPattern' property - must specify the event pattern to listen for (e.g., 'global_variable_changed:varName')" + end + + # Check for missing conversationMode when targetKnot is present + if mapping['targetKnot'] && !mapping['conversationMode'] + issues << "⚠ WARNING: '#{mapping_path}' has targetKnot but no conversationMode - should specify 'phone-chat' or 'person-chat' to indicate which UI to use" + end + + # Check for missing background when conversationMode is person-chat + if mapping['conversationMode'] == 'person-chat' && !mapping['background'] + issues << "⚠ WARNING: '#{mapping_path}' has conversationMode: 'person-chat' but no background - person-chat cutscenes typically need a background image (e.g., 'assets/backgrounds/hq1.png')" + end + end + end + end + # Track phone NPCs (phone contacts) if npc['npcType'] == 'phone' has_phone_contacts = true @@ -355,6 +392,37 @@ def check_common_issues(json_data) issues << "⚠ WARNING: '#{path}' (phone NPC) has 'spriteSheet' field - phone NPCs should NOT have spriteSheet (they're not in-world sprites). Remove the spriteSheet field." end + # Validate timedMessages structure for phone NPCs + if npc['timedMessages'] + unless npc['timedMessages'].is_a?(Array) + issues << "❌ INVALID: '#{path}' timedMessages is not an array - must be an array of timed message objects" + else + npc['timedMessages'].each_with_index do |msg, idx| + msg_path = "#{path}/timedMessages[#{idx}]" + + # Check for missing message field + unless msg['message'] + issues << "❌ INVALID: '#{msg_path}' missing required 'message' field - must specify the text content of the message" + end + + # Check for incorrect property name (text vs message) + if msg['text'] && !msg['message'] + issues << "❌ INVALID: '#{msg_path}' uses 'text' property - should use 'message' instead. The NPCManager reads msg.message, not msg.text" + end + + # Check for missing delay field + unless msg.key?('delay') + issues << "⚠ WARNING: '#{msg_path}' missing 'delay' property - should specify delay in milliseconds (0 for immediate)" + end + + # Check for incorrect property name (knot vs targetKnot) in timed messages + if msg['knot'] && !msg['targetKnot'] + issues << "❌ INVALID: '#{msg_path}' uses 'knot' property - should use 'targetKnot' instead. Change 'knot' to 'targetKnot'" + end + end + end + end + # Track phone NPCs with messages in rooms if npc['timedMessages'] && !npc['timedMessages'].empty? has_phone_npc_with_messages = true @@ -437,6 +505,93 @@ def check_common_issues(json_data) end end + # Check for event-driven cutscene architecture patterns + person_npcs_with_event_cutscenes = [] + global_variables_referenced = Set.new + global_variables_defined = Set.new + + # Collect global variables defined in scenario + if json_data['globalVariables'] + global_variables_defined.merge(json_data['globalVariables'].keys) + end + + # Check all NPCs for event-driven cutscene patterns + json_data['rooms']&.each do |room_id, room| + room['npcs']&.each_with_index do |npc, idx| + path = "rooms/#{room_id}/npcs[#{idx}]" + + # Check for person NPCs with eventMappings (cutscene NPCs) + if npc['npcType'] == 'person' && npc['eventMappings'] + npc['eventMappings'].each_with_index do |mapping, mapping_idx| + mapping_path = "#{path}/eventMappings[#{mapping_idx}]" + + # Check if this is a cutscene trigger (has conversationMode) + if mapping['conversationMode'] == 'person-chat' + person_npcs_with_event_cutscenes << { + npc_id: npc['id'], + path: path, + mapping: mapping + } + + # Extract global variable name from event pattern + if mapping['eventPattern']&.match(/global_variable_changed:(\w+)/) + var_name = $1 + global_variables_referenced << var_name + + # Check if the global variable is defined + unless global_variables_defined.include?(var_name) + issues << "❌ INVALID: '#{mapping_path}' references global variable '#{var_name}' in eventPattern, but it's not defined in scenario.globalVariables. Add '#{var_name}' with an initial value (typically false) to globalVariables" + end + end + + # Check for missing spriteTalk when using non-numeric frame sprites + if !npc['spriteTalk'] && npc['spriteSheet'] + # Sprites with named frames (not numeric indices) need spriteTalk + named_frame_sprites = ['female_spy', 'male_spy', 'female_hacker_hood', 'male_doctor'] + if named_frame_sprites.include?(npc['spriteSheet']) + issues << "⚠ WARNING: '#{path}' uses spriteSheet '#{npc['spriteSheet']}' which has named frames, but no 'spriteTalk' property. Person-chat cutscenes will show frame errors. Add 'spriteTalk' property pointing to a headshot image (e.g., 'assets/characters/#{npc['spriteSheet']}_headshot.png')" + end + end + + # Validate background for person-chat cutscenes + unless mapping['background'] + issues << "⚠ WARNING: '#{mapping_path}' is a person-chat cutscene but has no 'background' property. Person-chat cutscenes should have a background image for better visual presentation (e.g., 'assets/backgrounds/hq1.png')" + end + + # Check for onceOnly to prevent repeated cutscenes + unless mapping['onceOnly'] + issues << "⚠ WARNING: '#{mapping_path}' is a person-chat cutscene without 'onceOnly: true'. Cutscenes typically should only trigger once. Add 'onceOnly: true' unless you want the cutscene to repeat" + end + end + end + end + + # Check for phone NPCs setting global variables in their stories + if npc['npcType'] == 'phone' && npc['storyPath'] + # Note: We can't easily check the Ink story content from Ruby, but we can suggest best practices + if npc['eventMappings'] + # This phone NPC has both a story and event mappings, which suggests it might be setting up a cutscene + cutscene_event_mappings = npc['eventMappings'].select { |m| m['sendTimedMessage'] } + if cutscene_event_mappings.any? + # This looks like a mission-ending phone NPC + issues << "💡 BEST PRACTICE: '#{path}' appears to be a mission-ending phone NPC with sendTimedMessage. Consider using event-driven cutscene architecture instead: 1) Add #set_global:variable_name:true tag in Ink story, 2) Add #exit_conversation tag to close phone, 3) Create separate person NPC with eventMapping listening for global_variable_changed:variable_name. See scenarios/m01_first_contact/scenario.json.erb for reference implementation" + end + end + end + end + end + + # Check for orphaned global variable references + orphaned_vars = global_variables_referenced - global_variables_defined + orphaned_vars.each do |var_name| + issues << "❌ INVALID: Global variable '#{var_name}' is referenced in eventPatterns but not defined in scenario.globalVariables. Add '#{var_name}' to globalVariables with an initial value (typically false for cutscene triggers)" + end + + # Provide best practice guidance for event-driven cutscenes + if person_npcs_with_event_cutscenes.any? + issues << "✅ GOOD PRACTICE: Scenario uses event-driven cutscene architecture with #{person_npcs_with_event_cutscenes.size} person-chat cutscene(s). Ensure corresponding phone NPCs use #set_global tags to trigger these cutscenes" + end + # Feature suggestions unless has_vm_launcher issues << "💡 SUGGESTION: Consider adding VM launcher terminals (type: 'vm-launcher') - see scenarios/secgen_vm_lab/scenario.json.erb for example" @@ -459,7 +614,12 @@ def check_common_issues(json_data) end unless has_closing_debrief - issues << "💡 SUGGESTION: Consider adding closing debrief trigger - phone NPC with eventMapping for global_variable_changed - see scenarios/m01_first_contact/scenario.json.erb for example" + issues << "💡 SUGGESTION: Consider adding event-driven closing debrief cutscene using this architecture:" + issues << " 1. Add global variable to scenario.globalVariables (e.g., 'start_debrief_cutscene': false)" + issues << " 2. In phone NPC's Ink story, add tags: #set_global:start_debrief_cutscene:true and #exit_conversation" + issues << " 3. Create person NPC with eventMappings: [{eventPattern: 'global_variable_changed:start_debrief_cutscene', condition: 'value === true', conversationMode: 'person-chat', targetKnot: 'start', background: 'assets/backgrounds/hq1.png', onceOnly: true}]" + issues << " 4. Add behavior: {initiallyHidden: true} to person NPC so it doesn't appear in-world" + issues << " See scenarios/m01_first_contact/scenario.json.erb for complete reference implementation" end # Check for NPCs without waypoints