From f30dd7f279eba207f4e367ebbb66756a5a84fdd6 Mon Sep 17 00:00:00 2001 From: "Z. Cliffe Schreuders" Date: Tue, 17 Feb 2026 16:33:49 +0000 Subject: [PATCH] Update scenario.json.erb: Change NPC sprite and add closing debrief triggers - Updated spriteSheet for Sarah Martinez from "female_office_worker" to "female_blowse". - Added new event triggers for closing debrief upon entering the main office area and confronting Derek. - Modified Agent 0x99 to use a person type NPC with updated event mappings and properties. --- .vscode/settings.json | 5 +- .../break_escape/games_controller.rb | 2 +- .../{woman_blowse.json => female_blowse.json} | 0 .../{woman_blowse.png => female_blowse.png} | Bin ...eadshot.png => female_blowse_headshot.png} | Bin public/break_escape/js/core/game.js | 6 +- .../js/minigames/helpers/chat-helpers.js | 35 +++++++ .../phone-chat/phone-chat-minigame.js | 64 ++++++++++--- .../js/minigames/phone-chat/phone-chat-ui.js | 10 +- public/break_escape/js/systems/npc-manager.js | 87 ++++++++++++++---- .../ink/m01_closing_debrief.ink | 13 --- .../ink/m01_phone_agent0x99.ink | 31 +++++++ .../ink/m01_phone_agent0x99.json | 2 +- scenarios/m01_first_contact/scenario.json.erb | 53 ++++++++--- 14 files changed, 244 insertions(+), 64 deletions(-) rename public/break_escape/assets/characters/{woman_blowse.json => female_blowse.json} (100%) rename public/break_escape/assets/characters/{woman_blowse.png => female_blowse.png} (100%) rename public/break_escape/assets/characters/{woman_blowse_headshot.png => female_blowse_headshot.png} (100%) diff --git a/.vscode/settings.json b/.vscode/settings.json index 9f0eb0b..36349a0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,7 @@ { "cursor.general.disableHttp2": true, - "chat.agent.maxRequests": 100 + "chat.agent.maxRequests": 100, + "chat.tools.terminal.autoApprove": { + "bin/inklecate": true + } } \ No newline at end of file diff --git a/app/controllers/break_escape/games_controller.rb b/app/controllers/break_escape/games_controller.rb index 56c9378..b82f2bd 100644 --- a/app/controllers/break_escape/games_controller.rb +++ b/app/controllers/break_escape/games_controller.rb @@ -1134,7 +1134,7 @@ module BreakEscape stdout, stderr, status = Open3.capture3( inklecate_path.to_s, - '-o', output_path, + '-jo', output_path, ink_path.to_s ) diff --git a/public/break_escape/assets/characters/woman_blowse.json b/public/break_escape/assets/characters/female_blowse.json similarity index 100% rename from public/break_escape/assets/characters/woman_blowse.json rename to public/break_escape/assets/characters/female_blowse.json diff --git a/public/break_escape/assets/characters/woman_blowse.png b/public/break_escape/assets/characters/female_blowse.png similarity index 100% rename from public/break_escape/assets/characters/woman_blowse.png rename to public/break_escape/assets/characters/female_blowse.png diff --git a/public/break_escape/assets/characters/woman_blowse_headshot.png b/public/break_escape/assets/characters/female_blowse_headshot.png similarity index 100% rename from public/break_escape/assets/characters/woman_blowse_headshot.png rename to public/break_escape/assets/characters/female_blowse_headshot.png diff --git a/public/break_escape/js/core/game.js b/public/break_escape/js/core/game.js index 1fdec4b..1ee6bcf 100644 --- a/public/break_escape/js/core/game.js +++ b/public/break_escape/js/core/game.js @@ -440,9 +440,9 @@ export function preload() { this.load.atlas('female_scientist', 'characters/female_scientist.png', 'characters/female_scientist.json'); - this.load.atlas('woman_blowse', - 'characters/woman_blowse.png', - 'characters/woman_blowse.json'); + this.load.atlas('female_blowse', + 'characters/female_blowse.png', + 'characters/female_blowse.json'); // Male characters this.load.atlas('male_hacker_hood', diff --git a/public/break_escape/js/minigames/helpers/chat-helpers.js b/public/break_escape/js/minigames/helpers/chat-helpers.js index 41a52e2..f78b910 100644 --- a/public/break_escape/js/minigames/helpers/chat-helpers.js +++ b/public/break_escape/js/minigames/helpers/chat-helpers.js @@ -246,6 +246,41 @@ export function processGameActionTags(tags, ui) { } } break; + case 'transition_to_person_chat': + { + // Format: transition_to_person_chat:npcId|background|knot + // Example: # transition_to_person_chat:closing_debrief_trigger|assets/backgrounds/hq1.png|start + const [targetNpcId, background, targetKnot] = param ? param.split('|').map(s => s.trim()) : []; + + if (!targetNpcId) { + result.message = '⚠️ transition_to_person_chat requires npcId parameter'; + console.warn(result.message); + break; + } + + console.log('🔄 Transitioning to person-chat:', { targetNpcId, background, targetKnot }); + + // Close current phone-chat minigame + if (window.MinigameFramework && window.MinigameFramework.currentMinigame) { + window.MinigameFramework.currentMinigame.complete(false); + } + + // Small delay before starting person-chat + setTimeout(() => { + if (window.MinigameFramework) { + window.MinigameFramework.startMinigame('person-chat', { + npcId: targetNpcId, + background: background || null, + startKnot: targetKnot || null + }); + } + }, 100); + + result.success = true; + result.message = `🔄 Transitioning to person-chat with ${targetNpcId}`; + } + break; + case 'clone_keycard': // Parameter is the card_id to clone // Look up card data from NPC's rfidCard property 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 baad324..662adf6 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 @@ -365,8 +365,31 @@ export class PhoneChatMinigame extends MinigameScene { // Load conversation history const history = this.history.loadHistory(); - // Filter out bark-only messages to check if there's real conversation history - const conversationHistory = history.filter(msg => !msg.metadata?.isBark); + // Determine target knot (needed before clearing history) + const safeParams = this.params || {}; + const explicitStartKnot = safeParams.startKnot; + const targetKnot = explicitStartKnot || npc.currentKnot || 'start'; + + // If navigating to a new knot explicitly (e.g., from timed message), + // clear the non-timed history to avoid showing old messages from previous visits to this knot + if (explicitStartKnot && history.length > 0) { + console.log(`🧹 Explicit knot navigation detected - clearing old conversation messages (keeping timed/bark notifications)`); + console.log('📝 History before filtering:', history.map(m => ({ text: m.text.substring(0, 40), timed: m.timed, isBark: m.isBark }))); + // Keep only timed messages and barks (notifications), remove old Ink dialogue + // Note: metadata is spread directly onto message object, not nested + const filteredHistory = history.filter(msg => msg.isBark || msg.timed); + console.log('📝 History after filtering:', filteredHistory.map(m => ({ text: m.text.substring(0, 40), timed: m.timed, isBark: m.isBark }))); + + // Update NPCManager's conversation history directly + this.npcManager.conversationHistory.set(this.npcId, filteredHistory); + + // Update what we'll display + history.splice(0, history.length, ...filteredHistory); + } + + // Filter out bark-only and timed messages to check if there's real conversation history + // (timed messages are just notifications, not actual Ink dialogue) + const conversationHistory = history.filter(msg => !msg.isBark && !msg.timed); const hasConversationHistory = conversationHistory.length > 0; // Show all history (including barks) in the UI @@ -377,11 +400,12 @@ export class PhoneChatMinigame extends MinigameScene { } // Load and start Ink story - // Support both storyJSON (inline) and storyPath (file) - let storySource = npc.storyJSON || npc.inkStoryPath; + // Prefer Rails API endpoint if storyPath exists (ensures fresh story after path changes) + console.log(`📱 openConversation - npc.storyJSON exists: ${!!npc.storyJSON}, npc.storyPath: ${npc.storyPath}, npc.inkStoryPath: ${npc.inkStoryPath}`); + let storySource = null; - // If no storyJSON but storyPath exists, use Rails API endpoint - if (!storySource && npc.storyPath) { + // If storyPath exists, use Rails API endpoint (ensures fresh load after story path changes) + if (npc.storyPath) { const gameId = window.breakEscapeConfig?.gameId; if (gameId) { storySource = `/break_escape/games/${gameId}/ink?npc=${npcId}`; @@ -389,6 +413,11 @@ export class PhoneChatMinigame extends MinigameScene { } } + // Fallback to storyJSON or inkStoryPath + if (!storySource) { + storySource = npc.storyJSON || npc.inkStoryPath; + } + if (!storySource) { console.error(`❌ No story source found for ${npcId}`); this.ui.showNotification('No conversation available', 'error'); @@ -405,20 +434,25 @@ export class PhoneChatMinigame extends MinigameScene { this.isConversationActive = true; // Check if we have saved story state to restore - if (hasConversationHistory && npc.storyState) { - // Restore previous story state + // BUT: if startKnot was explicitly provided (e.g., from timed message), + // navigate to that knot instead of restoring old state + if (hasConversationHistory && npc.storyState && !explicitStartKnot) { + // Restore previous story state (only if no explicit knot override) console.log('📚 Restoring story state from previous conversation'); this.conversation.restoreState(npc.storyState); // Show current choices without continuing this.showCurrentChoices(); } else { - // Navigate to starting knot for first time - const safeParams = this.params || {}; - const startKnot = safeParams.startKnot || npc.currentKnot || 'start'; - this.conversation.goToKnot(startKnot); + // Navigate to starting knot (either first time, or explicit navigation request) + if (explicitStartKnot) { + console.log(`📱 Explicit navigation to knot: ${explicitStartKnot} (overriding saved state)`); + } else { + console.log(`📱 Navigating to knot: ${targetKnot}`); + } + this.conversation.goToKnot(targetKnot); - // First time opening - show intro message and choices + // Continue story to get fresh content and choices this.continueStory(); } } @@ -470,6 +504,9 @@ export class PhoneChatMinigame extends MinigameScene { return; } + console.log('🎬 continueStory() called'); + console.trace('Call stack'); // This will show us where continueStory is being called from + // Show typing indicator briefly this.ui.showTypingIndicator(); @@ -530,6 +567,7 @@ export class PhoneChatMinigame extends MinigameScene { console.log('📖 Accumulated messages:', accumulatedMessages.length); console.log('🏷️ Accumulated tags:', accumulatedTags); + console.log('📝 Messages detail:', accumulatedMessages); // If story has ended if (lastResult.hasEnded && accumulatedMessages.length === 0) { diff --git a/public/break_escape/js/minigames/phone-chat/phone-chat-ui.js b/public/break_escape/js/minigames/phone-chat/phone-chat-ui.js index 565a3e5..2916913 100644 --- a/public/break_escape/js/minigames/phone-chat/phone-chat-ui.js +++ b/public/break_escape/js/minigames/phone-chat/phone-chat-ui.js @@ -317,7 +317,15 @@ export default class PhoneChatUI { // Filter to only allowed NPCs if npcIds was specified if (this.allowedNpcIds && this.allowedNpcIds.length > 0) { console.log(`🔍 Filtering contacts: allowed NPCs = ${this.allowedNpcIds.join(', ')}`); - npcs = npcs.filter(npc => this.allowedNpcIds.includes(npc.id)); + npcs = npcs.filter(npc => { + // Include if in allowed list + if (this.allowedNpcIds.includes(npc.id)) { + return true; + } + // Include if has conversation history (i.e., has been activated by events) + const history = this.npcManager.getConversationHistory(npc.id); + return history && history.length > 0; + }); console.log(`✅ Filtered to ${npcs.length} contacts`); } diff --git a/public/break_escape/js/systems/npc-manager.js b/public/break_escape/js/systems/npc-manager.js index bfa2524..8799f5b 100644 --- a/public/break_escape/js/systems/npc-manager.js +++ b/public/break_escape/js/systems/npc-manager.js @@ -11,7 +11,7 @@ export default class NPCManager { this.eventListeners = new Map(); // Track registered listeners for cleanup this.triggeredEvents = new Map(); // Track which events have been triggered per NPC this.conversationHistory = new Map(); // Track conversation history per NPC: { npcId: [ {type, text, timestamp, choiceText} ] } - this.timedMessages = []; // Scheduled messages: { npcId, text, triggerTime, delivered, phoneId } + this.timedMessages = []; // Scheduled messages: { npcId, text, triggerTime, delivered, phoneId, targetKnot } this.timedConversations = []; // Scheduled conversations: { npcId, targetKnot, triggerTime, delivered } this.gameStartTime = Date.now(); // Track when game started for timed messages this.timerInterval = null; // Timer for checking timed messages @@ -315,7 +315,9 @@ export default class NPCManager { cooldown: mapping.cooldown, condition: mapping.condition, maxTriggers: mapping.maxTriggers, // Add max trigger limit - conversationMode: mapping.conversationMode // Add conversation mode (e.g., 'person-chat') + conversationMode: mapping.conversationMode, // Add conversation mode (e.g., 'person-chat') + changeStoryPath: mapping.changeStoryPath, // Change the NPC's story file + sendTimedMessage: mapping.sendTimedMessage // Send a timed message when event triggers }; console.log(` 📌 Registering listener for event: ${eventPattern} → ${config.knot}`); @@ -403,10 +405,41 @@ export default class NPCManager { triggered.lastTime = now; this.triggeredEvents.set(eventKey, triggered); - // Update NPC's current knot if specified - if (config.knot) { - npc.currentKnot = config.knot; - console.log(`📍 Updated ${npcId} current knot to: ${config.knot}`); + // Update NPC's current knot if specified (use targetKnot or knot for backwards compatibility) + const knotToSet = config.targetKnot || config.knot; + if (knotToSet) { + npc.currentKnot = knotToSet; + console.log(`📍 Updated ${npcId} current knot to: ${knotToSet}`); + } + + // Change NPC's story path if specified (switches conversation to different Ink file) + if (config.changeStoryPath) { + console.log(`📖 BEFORE changeStoryPath - npc.storyPath: ${npc.storyPath}, npc.storyJSON exists: ${!!npc.storyJSON}`); + npc.storyPath = config.changeStoryPath; + // Clear cached story state so new story loads fresh + delete npc.storyState; + delete npc.storyJSON; + // Clear cached InkEngine so it reloads with new story + if (this.inkEngineCache.has(npcId)) { + this.inkEngineCache.delete(npcId); + } + // Clear ALL conversation history (new timed message will be added fresh) + this.conversationHistory.set(npcId, []); + console.log(`📖 AFTER changeStoryPath - npc.storyPath: ${npc.storyPath}, npc.storyJSON exists: ${!!npc.storyJSON}`); + console.log(`📖 Changed ${npcId} story path to: ${config.changeStoryPath} (cleared all caches and history)`); + } + + // Send timed message if specified + if (config.sendTimedMessage) { + const msgConfig = config.sendTimedMessage; + this.scheduleTimedMessage({ + npcId: npcId, + text: msgConfig.message, + delay: msgConfig.delay || 0, + phoneId: npc.phoneId, + targetKnot: msgConfig.targetKnot || null + }); + console.log(`📨 Scheduled timed message for ${npcId}: "${msgConfig.message}" (delay: ${msgConfig.delay}ms, targetKnot: ${msgConfig.targetKnot || 'default'})`); } // Debug: Log the full config to see what we're working with @@ -473,9 +506,11 @@ export default class NPCManager { // Start the person-chat minigame 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: config.knot || npc.currentKnot, + startKnot: knotToUse, + background: config.background || null, scenario: window.gameScenario }); console.log(`[NPCManager] Event '${eventPattern}' triggered for NPC '${npcId}' → person-chat conversation`); @@ -602,7 +637,7 @@ export default class NPCManager { // Schedule a timed message to be delivered after a delay // opts: { npcId, text, triggerTime (ms from game start) OR delay (ms from now), phoneId } scheduleTimedMessage(opts) { - const { npcId, text, triggerTime, delay, phoneId } = opts; + const { npcId, text, triggerTime, delay, phoneId, targetKnot } = opts; if (!npcId || !text) { console.error('[NPCManager] scheduleTimedMessage requires npcId and text'); @@ -617,6 +652,7 @@ export default class NPCManager { text, triggerTime: actualTriggerTime, // milliseconds from game start phoneId: phoneId || 'player_phone', + targetKnot: targetKnot || null, delivered: false }); @@ -722,7 +758,7 @@ export default class NPCManager { return; } - // Add message to conversation history + // Add message to conversation history (represents the incoming mobile chat message) this.addMessage(message.npcId, 'npc', message.text, { timed: true, phoneId: message.phoneId @@ -741,7 +777,7 @@ export default class NPCManager { message: message.text, avatar: npc.avatar, inkStoryPath: npc.storyPath, - startKnot: npc.currentKnot, + startKnot: message.targetKnot || npc.currentKnot, phoneId: message.phoneId }); } @@ -749,7 +785,7 @@ export default class NPCManager { console.log(`[NPCManager] Delivered timed message from ${message.npcId}:`, message.text); } - // Deliver a timed conversation (start person-chat minigame at specified knot) + // Deliver a timed conversation (start person-chat or phone-chat minigame at specified knot) _deliverTimedConversation(conversation) { const npc = this.getNPC(conversation.npcId); if (!npc) { @@ -760,17 +796,28 @@ export default class NPCManager { // Update NPC's current knot to the target knot npc.currentKnot = conversation.targetKnot; - // Check if MinigameFramework is available to start the person-chat minigame + // Check if MinigameFramework is available to start the appropriate minigame if (window.MinigameFramework && typeof window.MinigameFramework.startMinigame === 'function') { - console.log(`🎭 Starting timed conversation for ${conversation.npcId} at knot: ${conversation.targetKnot}`); - - window.MinigameFramework.startMinigame('person-chat', null, { - npcId: conversation.npcId, - title: npc.displayName || conversation.npcId, - background: conversation.background // Optional background image path - }); + // Determine which minigame type to start based on NPC type + if (npc.npcType === 'phone') { + console.log(`📱 Starting timed phone conversation for ${conversation.npcId} at knot: ${conversation.targetKnot}`); + + window.MinigameFramework.startMinigame('phone-chat', null, { + npcId: conversation.npcId, + phoneId: npc.phoneId || 'player_phone', + title: 'Phone' + }); + } else { + console.log(`🎭 Starting timed person conversation for ${conversation.npcId} at knot: ${conversation.targetKnot}`); + + window.MinigameFramework.startMinigame('person-chat', null, { + npcId: conversation.npcId, + title: npc.displayName || conversation.npcId, + background: conversation.background // Optional background image path + }); + } } else { - console.warn(`[NPCManager] MinigameFramework not available to start person-chat for timed conversation`); + console.warn(`[NPCManager] MinigameFramework not available to start conversation for timed conversation`); } console.log(`[NPCManager] Delivered timed conversation from ${conversation.npcId} to knot: ${conversation.targetKnot}`); diff --git a/scenarios/m01_first_contact/ink/m01_closing_debrief.ink b/scenarios/m01_first_contact/ink/m01_closing_debrief.ink index 3d718e6..4df4555 100644 --- a/scenarios/m01_first_contact/ink/m01_closing_debrief.ink +++ b/scenarios/m01_first_contact/ink/m01_closing_debrief.ink @@ -30,20 +30,7 @@ VAR audit_wrong_answers = 0 // Number of incorrect assessments // ================================================ === start === -#speaker:agent_0x99 -Agent 0x99: {player_name}, return to HQ for debrief. - -Agent 0x99: Operation Shatter is neutralized. Let's review what happened. - -+ [On my way] - -> debrief_location - -// ================================================ -// DEBRIEF LOCATION -// ================================================ - -=== debrief_location === [SAFETYNET HQ - Agent 0x99's Office] #speaker:agent_0x99 diff --git a/scenarios/m01_first_contact/ink/m01_phone_agent0x99.ink b/scenarios/m01_first_contact/ink/m01_phone_agent0x99.ink index 8e32be0..45d41ed 100644 --- a/scenarios/m01_first_contact/ink/m01_phone_agent0x99.ink +++ b/scenarios/m01_first_contact/ink/m01_phone_agent0x99.ink @@ -15,8 +15,22 @@ VAR operation_shatter_reported = false VAR player_name = "Agent 0x00" VAR current_task = "" VAR talked_to_maya = false +VAR talked_to_kevin = false VAR discussed_operation = false +// Closing debrief variables +VAR final_choice = "" +VAR objectives_completed = 0 +VAR lore_collected = 0 +VAR found_casualty_projections = false +VAR found_target_database = false +VAR maya_identity_protected = true +VAR kevin_choice = "" +VAR kevin_protected = false +VAR security_audit_completed = false +VAR audit_correct_answers = 0 +VAR audit_wrong_answers = 0 + // ================================================ // START: PHONE SUPPORT // ================================================ @@ -485,3 +499,20 @@ Agent 0x99: Confrontation, silent extraction, or public exposure. Each has conse Agent 0x99: Good luck, {player_name}. You've got this. #exit_conversation -> support_hub + +// ================================================ +// CLOSING DEBRIEF - Mission Complete +// ================================================ + +=== closing_debrief === +#speaker:agent_0x99 + +Agent 0x99: Operation Shatter is neutralized. Let's review what happened. + ++ [On my way] + #set_global:start_debrief_cutscene:true + #exit_conversation + -> END + +#exit_conversation +-> END diff --git a/scenarios/m01_first_contact/ink/m01_phone_agent0x99.json b/scenarios/m01_first_contact/ink/m01_phone_agent0x99.json index 6751b1c..ed341c7 100644 --- a/scenarios/m01_first_contact/ink/m01_phone_agent0x99.json +++ b/scenarios/m01_first_contact/ink/m01_phone_agent0x99.json @@ -1 +1 @@ -{"inkVersion":21,"root":[[["done",{"#n":"g-0"}],null],"done",{"start":["ev",{"VAR?":"first_contact"},"/ev",[{"->":".^.b","c":true},{"b":["\n","ev",false,"/ev",{"VAR=":"first_contact","re":true},{"->":"first_call"},{"->":"start.4"},null]}],"nop","\n","ev",{"VAR?":"first_contact"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->":"support_hub"},{"->":"start.11"},null]}],"nop","\n",null],"first_call":[["#","^speaker:agent_0x99","/#","^Agent 0x99: ","ev",{"VAR?":"player_name"},"out","/ev","^, checking in. How's the infiltration going?","\n","^Agent 0x99: If you need guidance on any challenges, I'm here. That's what handlers are for.","\n","ev","str","^Everything's going smoothly so far","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^I could use some tips","/str","/ev",{"*":".^.c-1","flg":4},"ev","str","^I'll call if I need help","/str","/ev",{"*":".^.c-2","flg":4},{"c-0":["\n","^Agent 0x99: Good. Remember, take your time. Rushing creates mistakes.","\n",{"->":"support_hub"},null],"c-1":["\n",{"->":"support_hub"},null],"c-2":["\n","#","^exit_conversation","/#","^Agent 0x99: Roger that. I'm here when you need me.","\n",{"->":"support_hub"},null]}],null],"support_hub":[["#","^speaker:agent_0x99","/#","^Agent 0x99: What do you need help with?","\n","ev","str","^I discovered what ENTROPY is planning - Operation Shatter","/str",{"VAR?":"talked_to_maya"},{"VAR?":"discussed_operation"},"&&",{"VAR?":"operation_shatter_reported"},"!","&&","/ev",{"*":".^.c-0","flg":5},"ev","str","^Lockpicking guidance","/str",{"VAR?":"lockpick_hint_given"},"!","/ev",{"*":".^.c-1","flg":5},"ev","str","^SSH brute force help","/str",{"VAR?":"ssh_hint_given"},"!","/ev",{"*":".^.c-2","flg":5},"ev","str","^Linux navigation tips","/str",{"VAR?":"linux_hint_given"},"!","/ev",{"*":".^.c-3","flg":5},"ev","str","^Privilege escalation guidance","/str",{"VAR?":"sudo_hint_given"},"!","/ev",{"*":".^.c-4","flg":5},"ev","str","^General mission advice","/str","/ev",{"*":".^.c-5","flg":4},"ev","str","^I'm good for now","/str","/ev",{"*":".^.c-6","flg":4},{"c-0":["\n",{"->":"report_operation_shatter"},null],"c-1":["\n",{"->":"lockpick_help"},null],"c-2":["\n",{"->":"ssh_help"},null],"c-3":["\n",{"->":"linux_help"},null],"c-4":["\n",{"->":"sudo_help"},null],"c-5":["\n",{"->":"general_advice"},null],"c-6":["\n","#","^exit_conversation","/#","^Agent 0x99: Copy that. Call anytime.","\n",{"->":".^.^.^"},null]}],null],"lockpick_help":[["ev",true,"/ev",{"VAR=":"lockpick_hint_given","re":true},"^Agent 0x99: Lockpicking is about patience and listening.","\n","^Agent 0x99: Each pin has a sweet spot. Apply tension, test each pin, feel for the feedback.","\n","^Agent 0x99: Start with the storage closet practice safe—low stakes, good for learning.","\n","ev","str","^Any other tips?","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Got it, thanks","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n","^Agent 0x99: Don't force it. If you're stuck, reset and try again. There's no timer.","\n",{"->":"support_hub"},null],"c-1":["\n",{"->":"support_hub"},null]}],null],"ssh_help":[["ev",true,"/ev",{"VAR=":"ssh_hint_given","re":true},"^Agent 0x99: SSH brute force uses Hydra to test password lists against login prompts.","\n","^Agent 0x99: The key is using good password lists. Kevin's hints about \"ViralDynamics2025\" variations are gold.","\n","^Agent 0x99: Command format: hydra -l username -P passwordlist.txt ssh:","\n","ev","str","^What if I don't have a password list?","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Thanks, that helps","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n","^Agent 0x99: Build one from intel. Kevin mentioned patterns, the whiteboard had clues. Social engineering works.","\n",{"->":"support_hub"},null],"c-1":["\n",{"->":"support_hub"},null]}],null],"linux_help":[["ev",true,"/ev",{"VAR=":"linux_hint_given","re":true},"^Agent 0x99: Linux navigation basics: ls lists files, cd changes directory, cat reads files.","\n","^Agent 0x99: Check the home directory first. User files, hidden configs—look for .bashrc, .ssh, personal directories.","\n","^Agent 0x99: Hidden files start with a dot. Use ls -la to see them.","\n","ev","str","^Where should I look for flags?","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Got it","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n","^Agent 0x99: Home directories, user documents, sometimes hidden in config files. Explore methodically.","\n",{"->":"support_hub"},null],"c-1":["\n",{"->":"support_hub"},null]}],null],"sudo_help":[["ev",true,"/ev",{"VAR=":"sudo_hint_given","re":true},"^Agent 0x99: Privilege escalation means gaining access to other accounts or higher permissions.","\n","^Agent 0x99: Try \"sudo -l\" to see what sudo permissions you have. Some accounts allow switching users.","\n","^Agent 0x99: Command: sudo -u otherusername bash gives you a shell as that user.","\n","ev","str","^What if I don't have sudo access?","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Thanks","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n","^Agent 0x99: Check for misconfigured files, world-writable directories, or SUID binaries. But for this mission, sudo works.","\n",{"->":"support_hub"},null],"c-1":["\n",{"->":"support_hub"},null]}],null],"general_advice":[["^Agent 0x99: Remember the mission priorities: gather evidence, identify operatives, minimize innocent casualties.","\n","^Agent 0x99: Most people at Viral Dynamics are legitimate employees. We want ENTROPY, not collateral damage.","\n","ev","str","^How do I know who's ENTROPY?","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^What about Maya?","/str","/ev",{"*":".^.c-1","flg":4},"ev","str","^Understood","/str","/ev",{"*":".^.c-2","flg":4},{"c-0":["\n","^Agent 0x99: Evidence correlation. Look for encrypted communications, connections to known cells, suspicious behavior.","\n","^Agent 0x99: Derek's our primary suspect, but gather proof before confronting.","\n",{"->":"support_hub"},null],"c-1":["\n","^Agent 0x99: Protect her. She's the informant who brought this to us. Don't expose her unless absolutely necessary.","\n",{"->":"support_hub"},null],"c-2":["\n",{"->":"support_hub"},null]}],null],"report_operation_shatter":[["ev",true,"/ev",{"VAR=":"operation_shatter_reported","re":true},"#","^unlock_task:inform_safetynet_operation_shatter","/#","^Agent 0x99: ...Say that again.","\n","ev","str","^Operation Shatter - coordinated disinformation attack","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^They're planning mass casualties","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n",{"->":"shatter_details_1"},null],"c-1":["\n",{"->":"shatter_casualties"},null]}],null],"shatter_details_1":[["^Agent 0x99: Operation Shatter. Christ.","\n","^Agent 0x99: What exactly are they planning?","\n","ev","str","^Fake crisis messages targeting vulnerable populations","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["\n",{"->":"shatter_details_2"},null]}],null],"shatter_details_2":[["^Agent 0x99: Talk to me. What did Maya tell you?","\n","ev","str","^Over two million profiles. Fake hospital closures, bank failures, infrastructure attacks.","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["\n",{"->":"shatter_casualties"},null]}],null],"shatter_casualties":[["^Agent 0x99: ","ev",{"VAR?":"player_name"},"out","/ev","^, this is worse than we thought.","\n","^Agent 0x99: How bad are we talking?","\n","ev","str","^Their own projections: 42 to 85 deaths in the first 24 hours","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^They've calculated acceptable casualties. They're targeting diabetics, elderly, people with anxiety disorders.","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n",{"->":"shatter_reaction"},null],"c-1":["\n",{"->":"shatter_reaction"},null]}],null],"shatter_reaction":[["^Agent 0x99: ...Forty-two to eighty-five people. Calculated. Deliberate.","\n","^Agent 0x99: They're not just terrorists. They're mass murderers with spreadsheets.","\n","^Agent 0x99: ","ev",{"VAR?":"player_name"},"out","/ev","^, listen carefully. Your mission just changed priority.","\n","ev","str","^What do I need to do?","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["\n",{"->":"updated_objectives"},null]}],null],"updated_objectives":[["^Agent 0x99: New priority objective: Stop Operation Shatter before deployment.","\n","^Agent 0x99: Maya said Sunday, 6 AM. That's when the messages go out.","\n","^Agent 0x99: Find the complete documentation—target lists, message templates, deployment systems.","\n","^Agent 0x99: Gather proof of Derek's involvement. And shut down their attack infrastructure before those messages go out.","\n","ev","str","^What about those 85 people?","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^I'll stop it","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n",{"->":"people_at_stake"},null],"c-1":["\n",{"->":"mission_commitment"},null]}],null],"people_at_stake":["^Agent 0x99: They're counting on you, ","ev",{"VAR?":"player_name"},"out","/ev","^. Even if they don't know it.","\n","^Agent 0x99: Diabetics who'll skip insulin. Elderly with heart conditions. People who'll panic and make fatal decisions.","\n","^Agent 0x99: Every piece of evidence you find brings us closer to stopping this.","\n",{"->":"mission_commitment"},null],"mission_commitment":["#","^complete_task:inform_safetynet_operation_shatter","/#","^Agent 0x99: Good work discovering this. Now we know what we're dealing with.","\n","^Agent 0x99: Continue investigating. Find the Operation Shatter files, identify all operatives, and prepare to shut this down.","\n","^Agent 0x99: Call me if you need support. This just became a race against the clock.","\n","#","^exit_conversation","/#",{"->":"support_hub"},null],"event_lockpick_acquired":[["#","^speaker:agent_0x99","/#","^Agent 0x99: I see Kevin gave you lockpicks. Smart social engineering.","\n","^Agent 0x99: Practice on low-risk targets first. Storage closet, unlocked areas.","\n","^Agent 0x99: Remember, you're testing security—officially.","\n","ev","str","^Will do","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Any lockpicking tips?","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n","#","^exit_conversation","/#",{"->":"support_hub"},null],"c-1":["\n",{"->":"lockpick_help"},null]}],null],"event_server_room_entered":[["#","^speaker:agent_0x99","/#","#","^complete_task:access_server_room","/#","#","^unlock_task:access_vm","/#","^Agent 0x99: You're in the server room. Good work getting access.","\n","^Agent 0x99: Look for the compromised systems. VM access will give you deeper intelligence.","\n","ev","str","^What am I looking for?","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^On it","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n","^Agent 0x99: Evidence of ENTROPY's infrastructure. Backdoors, encrypted communications, anything linking Derek to other cells.","\n","#","^exit_conversation","/#",{"->":"support_hub"},null],"c-1":["\n","#","^exit_conversation","/#",{"->":"support_hub"},null]}],null],"event_first_flag":[["#","^speaker:agent_0x99","/#","^Agent 0x99: First flag submitted. Nice work, ","ev",{"VAR?":"player_name"},"out","/ev","^.","\n","^Agent 0x99: Each flag unlocks intelligence. Keep correlating VM findings with physical evidence.","\n","ev","str","^What should I focus on next?","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Thanks","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n","^Agent 0x99: Continue the VM challenges, but don't forget physical investigation. Derek's office, filing cabinets, computer access.","\n","^Agent 0x99: Hybrid approach—digital and physical evidence together.","\n","#","^exit_conversation","/#",{"->":"support_hub"},null],"c-1":["\n","#","^exit_conversation","/#",{"->":"support_hub"},null]}],null],"event_derek_office_entered":[["#","^speaker:agent_0x99","/#","#","^unlock_task:find_campaign_materials","/#","#","^unlock_task:discover_manifesto","/#","#","^unlock_task:decode_communications","/#","^Agent 0x99: You're in Derek's office. Good.","\n","^Agent 0x99: Look for communications, project documents, anything linking him to ENTROPY.","\n","^Agent 0x99: Whiteboard messages, computer files, filing cabinets. Be thorough.","\n","ev","str","^What if Derek catches me?","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^On it","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n","^Agent 0x99: Your cover is solid. You're doing a security audit—accessing offices is expected.","\n","^Agent 0x99: But don't tip your hand too early. Gather evidence before confronting.","\n","#","^exit_conversation","/#",{"->":"support_hub"},null],"c-1":["\n","#","^exit_conversation","/#",{"->":"support_hub"},null]}],null],"event_all_flags":[["#","^speaker:agent_0x99","/#","^Agent 0x99: All VM flags submitted. Excellent work.","\n","^Agent 0x99: Intelligence confirms Derek Lawson as primary operative, coordinating with Zero Day Syndicate.","\n","^Agent 0x99: Now correlate with physical evidence. Then we can move to confrontation.","\n","ev","str","^What's the confrontation plan?","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Roger that","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n","^Agent 0x99: That's your call. Direct, silent extraction, or something creative.","\n","^Agent 0x99: I trust your judgment. You've proven capable.","\n","#","^exit_conversation","/#",{"->":"support_hub"},null],"c-1":["\n","#","^exit_conversation","/#",{"->":"support_hub"},null]}],null],"event_contingency_found":[["#","^speaker:agent_0x99","/#","^Agent 0x99: ","ev",{"VAR?":"player_name"},"out","/ev","^, I just saw what you pulled from Derek's computer.","\n","^Agent 0x99: He's planning to frame Kevin Park for the entire breach. Fake logs, forged emails, the works.","\n","^Agent 0x99: Kevin—the IT guy who gave you access, who trusted you—is going to take the fall for ENTROPY.","\n","ev","str","^That's monstrous","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^What can I do about it?","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n",{"->":"contingency_reaction"},null],"c-1":["\n",{"->":"contingency_options"},null]}],null],"contingency_reaction":["^Agent 0x99: It gets worse. Derek's contingency activates automatically when systems are seized.","\n","^Agent 0x99: If we don't do something, Kevin gets arrested. His kids watch him taken away in handcuffs.","\n","^Agent 0x99: Eventually he'd be cleared, but... that's not something you just walk off.","\n",{"->":"contingency_options"},null],"contingency_options":[["^Agent 0x99: You have options here. None of them are perfect.","\n","^Agent 0x99: What do you want to do?","\n","ev","str","^Warn Kevin directly - tell him what's coming","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Leave evidence clearing Kevin for investigators","/str","/ev",{"*":".^.c-1","flg":4},"ev","str","^Focus on the mission - Kevin's not my responsibility","/str","/ev",{"*":".^.c-2","flg":4},{"c-0":["\n",{"->":"warn_kevin_choice"},null],"c-1":["\n",{"->":"plant_evidence_choice"},null],"c-2":["\n",{"->":"ignore_kevin_choice"},null]}],null],"warn_kevin_choice":[["^Agent 0x99: Direct warning. Risky—if Kevin panics or acts differently, Derek might notice.","\n","^Agent 0x99: But if it works, Kevin has time to lawyer up, document everything. He's protected.","\n","ev","str","^I'll take that risk. He deserves to know.","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Maybe there's a safer option...","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n","#","^set_variable:kevin_choice=warn","/#","#","^set_variable:kevin_protected=true","/#","^Agent 0x99: Understood. Find Kevin, tell him what's coming. Just... be careful how much you reveal.","\n","^Agent 0x99: The more he knows about SAFETYNET, the more complicated this gets.","\n","#","^exit_conversation","/#",{"->":"support_hub"},null],"c-1":["\n",{"->":"contingency_options"},null]}],null],"plant_evidence_choice":[["^Agent 0x99: Anonymous help. Leave the frame-up files where our follow-up team will find them.","\n","^Agent 0x99: Kevin never knows he was in danger. Investigators see Derek's setup immediately.","\n","^Agent 0x99: Clean. Professional. Takes time, but lower risk.","\n","ev","str","^That's the smarter play. Do it that way.","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Maybe there's another option...","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n","#","^set_variable:kevin_choice=evidence","/#","#","^set_variable:kevin_protected=true","/#","^Agent 0x99: Copy the contingency files to a visible location. Investigators will find them during evidence collection.","\n","^Agent 0x99: Kevin walks away clean without ever knowing. That's the professional approach.","\n","#","^exit_conversation","/#",{"->":"support_hub"},null],"c-1":["\n",{"->":"contingency_options"},null]}],null],"ignore_kevin_choice":[["^Agent 0x99: ...You're sure about that?","\n","^Agent 0x99: Kevin helped you. If you ignore this, he gets arrested. His family watches.","\n","^Agent 0x99: He'll be cleared eventually, but that's trauma that doesn't heal.","\n","ev","str","^The mission has to come first. I can't save everyone.","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Wait. Let me reconsider.","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n","#","^set_variable:kevin_choice=ignore","/#","#","^set_variable:kevin_protected=false","/#","^Agent 0x99: ...Understood. That's your call to make.","\n","^Agent 0x99: Just know that choice has consequences. For Kevin. For his family.","\n","^Agent 0x99: And for you, when you think about it later.","\n","#","^exit_conversation","/#",{"->":"support_hub"},null],"c-1":["\n",{"->":"contingency_options"},null]}],null],"event_act2_complete":[["#","^speaker:agent_0x99","/#","^Agent 0x99: You've identified the operatives and gathered the evidence.","\n","^Agent 0x99: Time to decide: How do you want to resolve this?","\n","^Agent 0x99: Confrontation, silent extraction, or public exposure. Each has consequences.","\n","ev","str","^I need to think about this","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^I'm ready to proceed","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n","^Agent 0x99: Take your time. This is the part where your choices matter most.","\n","#","^exit_conversation","/#",{"->":"support_hub"},null],"c-1":["\n","^Agent 0x99: Good luck, ","ev",{"VAR?":"player_name"},"out","/ev","^. You've got this.","\n","#","^exit_conversation","/#",{"->":"support_hub"},null]}],null],"global decl":["ev",false,{"VAR=":"lockpick_hint_given"},false,{"VAR=":"ssh_hint_given"},false,{"VAR=":"linux_hint_given"},false,{"VAR=":"sudo_hint_given"},true,{"VAR=":"first_contact"},false,{"VAR=":"operation_shatter_reported"},"str","^Agent 0x00","/str",{"VAR=":"player_name"},"str","^","/str",{"VAR=":"current_task"},false,{"VAR=":"talked_to_maya"},false,{"VAR=":"discussed_operation"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[[["done",{"#n":"g-0"}],null],"done",{"start":["ev",{"VAR?":"first_contact"},"/ev",[{"->":".^.b","c":true},{"b":["\n","ev",false,"/ev",{"VAR=":"first_contact","re":true},{"->":"first_call"},{"->":"start.4"},null]}],"nop","\n","ev",{"VAR?":"first_contact"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->":"support_hub"},{"->":"start.11"},null]}],"nop","\n",null],"first_call":[["#","^speaker:agent_0x99","/#","^Agent 0x99: ","ev",{"VAR?":"player_name"},"out","/ev","^, checking in. How's the infiltration going?","\n","^Agent 0x99: If you need guidance on any challenges, I'm here. That's what handlers are for.","\n","ev","str","^Everything's going smoothly so far","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^I could use some tips","/str","/ev",{"*":".^.c-1","flg":4},"ev","str","^I'll call if I need help","/str","/ev",{"*":".^.c-2","flg":4},{"c-0":["\n","^Agent 0x99: Good. Remember, take your time. Rushing creates mistakes.","\n",{"->":"support_hub"},null],"c-1":["\n",{"->":"support_hub"},null],"c-2":["\n","#","^exit_conversation","/#","^Agent 0x99: Roger that. I'm here when you need me.","\n",{"->":"support_hub"},null]}],null],"support_hub":[["#","^speaker:agent_0x99","/#","^Agent 0x99: What do you need help with?","\n","ev","str","^I discovered what ENTROPY is planning - Operation Shatter","/str",{"VAR?":"talked_to_maya"},{"VAR?":"discussed_operation"},"&&",{"VAR?":"operation_shatter_reported"},"!","&&","/ev",{"*":".^.c-0","flg":5},"ev","str","^Lockpicking guidance","/str",{"VAR?":"lockpick_hint_given"},"!","/ev",{"*":".^.c-1","flg":5},"ev","str","^SSH brute force help","/str",{"VAR?":"ssh_hint_given"},"!","/ev",{"*":".^.c-2","flg":5},"ev","str","^Linux navigation tips","/str",{"VAR?":"linux_hint_given"},"!","/ev",{"*":".^.c-3","flg":5},"ev","str","^Privilege escalation guidance","/str",{"VAR?":"sudo_hint_given"},"!","/ev",{"*":".^.c-4","flg":5},"ev","str","^General mission advice","/str","/ev",{"*":".^.c-5","flg":4},"ev","str","^I'm good for now","/str","/ev",{"*":".^.c-6","flg":4},{"c-0":["\n",{"->":"report_operation_shatter"},null],"c-1":["\n",{"->":"lockpick_help"},null],"c-2":["\n",{"->":"ssh_help"},null],"c-3":["\n",{"->":"linux_help"},null],"c-4":["\n",{"->":"sudo_help"},null],"c-5":["\n",{"->":"general_advice"},null],"c-6":["\n","#","^exit_conversation","/#","^Agent 0x99: Copy that. Call anytime.","\n",{"->":".^.^.^"},null]}],null],"lockpick_help":[["ev",true,"/ev",{"VAR=":"lockpick_hint_given","re":true},"^Agent 0x99: Lockpicking is about patience and listening.","\n","^Agent 0x99: Each pin has a sweet spot. Apply tension, test each pin, feel for the feedback.","\n","^Agent 0x99: Start with the storage closet practice safe—low stakes, good for learning.","\n","ev","str","^Any other tips?","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Got it, thanks","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n","^Agent 0x99: Don't force it. If you're stuck, reset and try again. There's no timer.","\n",{"->":"support_hub"},null],"c-1":["\n",{"->":"support_hub"},null]}],null],"ssh_help":[["ev",true,"/ev",{"VAR=":"ssh_hint_given","re":true},"^Agent 0x99: SSH brute force uses Hydra to test password lists against login prompts.","\n","^Agent 0x99: The key is using good password lists. Kevin's hints about \"ViralDynamics2025\" variations are gold.","\n","^Agent 0x99: Command format: hydra -l username -P passwordlist.txt ssh:","\n","ev","str","^What if I don't have a password list?","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Thanks, that helps","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n","^Agent 0x99: Build one from intel. Kevin mentioned patterns, the whiteboard had clues. Social engineering works.","\n",{"->":"support_hub"},null],"c-1":["\n",{"->":"support_hub"},null]}],null],"linux_help":[["ev",true,"/ev",{"VAR=":"linux_hint_given","re":true},"^Agent 0x99: Linux navigation basics: ls lists files, cd changes directory, cat reads files.","\n","^Agent 0x99: Check the home directory first. User files, hidden configs—look for .bashrc, .ssh, personal directories.","\n","^Agent 0x99: Hidden files start with a dot. Use ls -la to see them.","\n","ev","str","^Where should I look for flags?","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Got it","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n","^Agent 0x99: Home directories, user documents, sometimes hidden in config files. Explore methodically.","\n",{"->":"support_hub"},null],"c-1":["\n",{"->":"support_hub"},null]}],null],"sudo_help":[["ev",true,"/ev",{"VAR=":"sudo_hint_given","re":true},"^Agent 0x99: Privilege escalation means gaining access to other accounts or higher permissions.","\n","^Agent 0x99: Try \"sudo -l\" to see what sudo permissions you have. Some accounts allow switching users.","\n","^Agent 0x99: Command: sudo -u otherusername bash gives you a shell as that user.","\n","ev","str","^What if I don't have sudo access?","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Thanks","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n","^Agent 0x99: Check for misconfigured files, world-writable directories, or SUID binaries. But for this mission, sudo works.","\n",{"->":"support_hub"},null],"c-1":["\n",{"->":"support_hub"},null]}],null],"general_advice":[["^Agent 0x99: Remember the mission priorities: gather evidence, identify operatives, minimize innocent casualties.","\n","^Agent 0x99: Most people at Viral Dynamics are legitimate employees. We want ENTROPY, not collateral damage.","\n","ev","str","^How do I know who's ENTROPY?","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^What about Maya?","/str","/ev",{"*":".^.c-1","flg":4},"ev","str","^Understood","/str","/ev",{"*":".^.c-2","flg":4},{"c-0":["\n","^Agent 0x99: Evidence correlation. Look for encrypted communications, connections to known cells, suspicious behavior.","\n","^Agent 0x99: Derek's our primary suspect, but gather proof before confronting.","\n",{"->":"support_hub"},null],"c-1":["\n","^Agent 0x99: Protect her. She's the informant who brought this to us. Don't expose her unless absolutely necessary.","\n",{"->":"support_hub"},null],"c-2":["\n",{"->":"support_hub"},null]}],null],"report_operation_shatter":[["ev",true,"/ev",{"VAR=":"operation_shatter_reported","re":true},"#","^unlock_task:inform_safetynet_operation_shatter","/#","^Agent 0x99: ...Say that again.","\n","ev","str","^Operation Shatter - coordinated disinformation attack","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^They're planning mass casualties","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n",{"->":"shatter_details_1"},null],"c-1":["\n",{"->":"shatter_casualties"},null]}],null],"shatter_details_1":[["^Agent 0x99: Operation Shatter. Christ.","\n","^Agent 0x99: What exactly are they planning?","\n","ev","str","^Fake crisis messages targeting vulnerable populations","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["\n",{"->":"shatter_details_2"},null]}],null],"shatter_details_2":[["^Agent 0x99: Talk to me. What did Maya tell you?","\n","ev","str","^Over two million profiles. Fake hospital closures, bank failures, infrastructure attacks.","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["\n",{"->":"shatter_casualties"},null]}],null],"shatter_casualties":[["^Agent 0x99: ","ev",{"VAR?":"player_name"},"out","/ev","^, this is worse than we thought.","\n","^Agent 0x99: How bad are we talking?","\n","ev","str","^Their own projections: 42 to 85 deaths in the first 24 hours","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^They've calculated acceptable casualties. They're targeting diabetics, elderly, people with anxiety disorders.","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n",{"->":"shatter_reaction"},null],"c-1":["\n",{"->":"shatter_reaction"},null]}],null],"shatter_reaction":[["^Agent 0x99: ...Forty-two to eighty-five people. Calculated. Deliberate.","\n","^Agent 0x99: They're not just terrorists. They're mass murderers with spreadsheets.","\n","^Agent 0x99: ","ev",{"VAR?":"player_name"},"out","/ev","^, listen carefully. Your mission just changed priority.","\n","ev","str","^What do I need to do?","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["\n",{"->":"updated_objectives"},null]}],null],"updated_objectives":[["^Agent 0x99: New priority objective: Stop Operation Shatter before deployment.","\n","^Agent 0x99: Maya said Sunday, 6 AM. That's when the messages go out.","\n","^Agent 0x99: Find the complete documentation—target lists, message templates, deployment systems.","\n","^Agent 0x99: Gather proof of Derek's involvement. And shut down their attack infrastructure before those messages go out.","\n","ev","str","^What about those 85 people?","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^I'll stop it","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n",{"->":"people_at_stake"},null],"c-1":["\n",{"->":"mission_commitment"},null]}],null],"people_at_stake":["^Agent 0x99: They're counting on you, ","ev",{"VAR?":"player_name"},"out","/ev","^. Even if they don't know it.","\n","^Agent 0x99: Diabetics who'll skip insulin. Elderly with heart conditions. People who'll panic and make fatal decisions.","\n","^Agent 0x99: Every piece of evidence you find brings us closer to stopping this.","\n",{"->":"mission_commitment"},null],"mission_commitment":["#","^complete_task:inform_safetynet_operation_shatter","/#","^Agent 0x99: Good work discovering this. Now we know what we're dealing with.","\n","^Agent 0x99: Continue investigating. Find the Operation Shatter files, identify all operatives, and prepare to shut this down.","\n","^Agent 0x99: Call me if you need support. This just became a race against the clock.","\n","#","^exit_conversation","/#",{"->":"support_hub"},null],"event_lockpick_acquired":[["#","^speaker:agent_0x99","/#","^Agent 0x99: I see Kevin gave you lockpicks. Smart social engineering.","\n","^Agent 0x99: Practice on low-risk targets first. Storage closet, unlocked areas.","\n","^Agent 0x99: Remember, you're testing security—officially.","\n","ev","str","^Will do","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Any lockpicking tips?","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n","#","^exit_conversation","/#",{"->":"support_hub"},null],"c-1":["\n",{"->":"lockpick_help"},null]}],null],"event_server_room_entered":[["#","^speaker:agent_0x99","/#","#","^complete_task:access_server_room","/#","#","^unlock_task:access_vm","/#","^Agent 0x99: You're in the server room. Good work getting access.","\n","^Agent 0x99: Look for the compromised systems. VM access will give you deeper intelligence.","\n","ev","str","^What am I looking for?","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^On it","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n","^Agent 0x99: Evidence of ENTROPY's infrastructure. Backdoors, encrypted communications, anything linking Derek to other cells.","\n","#","^exit_conversation","/#",{"->":"support_hub"},null],"c-1":["\n","#","^exit_conversation","/#",{"->":"support_hub"},null]}],null],"event_first_flag":[["#","^speaker:agent_0x99","/#","^Agent 0x99: First flag submitted. Nice work, ","ev",{"VAR?":"player_name"},"out","/ev","^.","\n","^Agent 0x99: Each flag unlocks intelligence. Keep correlating VM findings with physical evidence.","\n","ev","str","^What should I focus on next?","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Thanks","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n","^Agent 0x99: Continue the VM challenges, but don't forget physical investigation. Derek's office, filing cabinets, computer access.","\n","^Agent 0x99: Hybrid approach—digital and physical evidence together.","\n","#","^exit_conversation","/#",{"->":"support_hub"},null],"c-1":["\n","#","^exit_conversation","/#",{"->":"support_hub"},null]}],null],"event_derek_office_entered":[["#","^speaker:agent_0x99","/#","#","^unlock_task:find_campaign_materials","/#","#","^unlock_task:discover_manifesto","/#","#","^unlock_task:decode_communications","/#","^Agent 0x99: You're in Derek's office. Good.","\n","^Agent 0x99: Look for communications, project documents, anything linking him to ENTROPY.","\n","^Agent 0x99: Whiteboard messages, computer files, filing cabinets. Be thorough.","\n","ev","str","^What if Derek catches me?","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^On it","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n","^Agent 0x99: Your cover is solid. You're doing a security audit—accessing offices is expected.","\n","^Agent 0x99: But don't tip your hand too early. Gather evidence before confronting.","\n","#","^exit_conversation","/#",{"->":"support_hub"},null],"c-1":["\n","#","^exit_conversation","/#",{"->":"support_hub"},null]}],null],"event_all_flags":[["#","^speaker:agent_0x99","/#","^Agent 0x99: All VM flags submitted. Excellent work.","\n","^Agent 0x99: Intelligence confirms Derek Lawson as primary operative, coordinating with Zero Day Syndicate.","\n","^Agent 0x99: Now correlate with physical evidence. Then we can move to confrontation.","\n","ev","str","^What's the confrontation plan?","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Roger that","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n","^Agent 0x99: That's your call. Direct, silent extraction, or something creative.","\n","^Agent 0x99: I trust your judgment. You've proven capable.","\n","#","^exit_conversation","/#",{"->":"support_hub"},null],"c-1":["\n","#","^exit_conversation","/#",{"->":"support_hub"},null]}],null],"event_contingency_found":[["#","^speaker:agent_0x99","/#","^Agent 0x99: ","ev",{"VAR?":"player_name"},"out","/ev","^, I just saw what you pulled from Derek's computer.","\n","^Agent 0x99: He's planning to frame Kevin Park for the entire breach. Fake logs, forged emails, the works.","\n","^Agent 0x99: Kevin—the IT guy who gave you access, who trusted you—is going to take the fall for ENTROPY.","\n","ev","str","^That's monstrous","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^What can I do about it?","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n",{"->":"contingency_reaction"},null],"c-1":["\n",{"->":"contingency_options"},null]}],null],"contingency_reaction":["^Agent 0x99: It gets worse. Derek's contingency activates automatically when systems are seized.","\n","^Agent 0x99: If we don't do something, Kevin gets arrested. His kids watch him taken away in handcuffs.","\n","^Agent 0x99: Eventually he'd be cleared, but... that's not something you just walk off.","\n",{"->":"contingency_options"},null],"contingency_options":[["^Agent 0x99: You have options here. None of them are perfect.","\n","^Agent 0x99: What do you want to do?","\n","ev","str","^Warn Kevin directly - tell him what's coming","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Leave evidence clearing Kevin for investigators","/str","/ev",{"*":".^.c-1","flg":4},"ev","str","^Focus on the mission - Kevin's not my responsibility","/str","/ev",{"*":".^.c-2","flg":4},{"c-0":["\n",{"->":"warn_kevin_choice"},null],"c-1":["\n",{"->":"plant_evidence_choice"},null],"c-2":["\n",{"->":"ignore_kevin_choice"},null]}],null],"warn_kevin_choice":[["^Agent 0x99: Direct warning. Risky—if Kevin panics or acts differently, Derek might notice.","\n","^Agent 0x99: But if it works, Kevin has time to lawyer up, document everything. He's protected.","\n","ev","str","^I'll take that risk. He deserves to know.","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Maybe there's a safer option...","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n","#","^set_variable:kevin_choice=warn","/#","#","^set_variable:kevin_protected=true","/#","^Agent 0x99: Understood. Find Kevin, tell him what's coming. Just... be careful how much you reveal.","\n","^Agent 0x99: The more he knows about SAFETYNET, the more complicated this gets.","\n","#","^exit_conversation","/#",{"->":"support_hub"},null],"c-1":["\n",{"->":"contingency_options"},null]}],null],"plant_evidence_choice":[["^Agent 0x99: Anonymous help. Leave the frame-up files where our follow-up team will find them.","\n","^Agent 0x99: Kevin never knows he was in danger. Investigators see Derek's setup immediately.","\n","^Agent 0x99: Clean. Professional. Takes time, but lower risk.","\n","ev","str","^That's the smarter play. Do it that way.","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Maybe there's another option...","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n","#","^set_variable:kevin_choice=evidence","/#","#","^set_variable:kevin_protected=true","/#","^Agent 0x99: Copy the contingency files to a visible location. Investigators will find them during evidence collection.","\n","^Agent 0x99: Kevin walks away clean without ever knowing. That's the professional approach.","\n","#","^exit_conversation","/#",{"->":"support_hub"},null],"c-1":["\n",{"->":"contingency_options"},null]}],null],"ignore_kevin_choice":[["^Agent 0x99: ...You're sure about that?","\n","^Agent 0x99: Kevin helped you. If you ignore this, he gets arrested. His family watches.","\n","^Agent 0x99: He'll be cleared eventually, but that's trauma that doesn't heal.","\n","ev","str","^The mission has to come first. I can't save everyone.","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Wait. Let me reconsider.","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n","#","^set_variable:kevin_choice=ignore","/#","#","^set_variable:kevin_protected=false","/#","^Agent 0x99: ...Understood. That's your call to make.","\n","^Agent 0x99: Just know that choice has consequences. For Kevin. For his family.","\n","^Agent 0x99: And for you, when you think about it later.","\n","#","^exit_conversation","/#",{"->":"support_hub"},null],"c-1":["\n",{"->":"contingency_options"},null]}],null],"event_act2_complete":[["#","^speaker:agent_0x99","/#","^Agent 0x99: You've identified the operatives and gathered the evidence.","\n","^Agent 0x99: Time to decide: How do you want to resolve this?","\n","^Agent 0x99: Confrontation, silent extraction, or public exposure. Each has consequences.","\n","ev","str","^I need to think about this","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^I'm ready to proceed","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n","^Agent 0x99: Take your time. This is the part where your choices matter most.","\n","#","^exit_conversation","/#",{"->":"support_hub"},null],"c-1":["\n","^Agent 0x99: Good luck, ","ev",{"VAR?":"player_name"},"out","/ev","^. You've got this.","\n","#","^exit_conversation","/#",{"->":"support_hub"},null]}],null],"closing_debrief":[["#","^speaker:agent_0x99","/#","^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","#","^set_global:start_debrief_cutscene:true","/#","#","^exit_conversation","/#","end","#","^exit_conversation","/#","end",null]}],null],"global decl":["ev",false,{"VAR=":"lockpick_hint_given"},false,{"VAR=":"ssh_hint_given"},false,{"VAR=":"linux_hint_given"},false,{"VAR=":"sudo_hint_given"},true,{"VAR=":"first_contact"},false,{"VAR=":"operation_shatter_reported"},"str","^Agent 0x00","/str",{"VAR=":"player_name"},"str","^","/str",{"VAR=":"current_task"},false,{"VAR=":"talked_to_maya"},false,{"VAR=":"talked_to_kevin"},false,{"VAR=":"discussed_operation"},"str","^","/str",{"VAR=":"final_choice"},0,{"VAR=":"objectives_completed"},0,{"VAR=":"lore_collected"},false,{"VAR=":"found_casualty_projections"},false,{"VAR=":"found_target_database"},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 07ff30d..923dd68 100644 --- a/scenarios/m01_first_contact/scenario.json.erb +++ b/scenarios/m01_first_contact/scenario.json.erb @@ -285,7 +285,7 @@ password_hints = "Common passwords: Marketing123, Campaign2024, Viral_Dynamics_A "displayName": "Sarah Martinez", "npcType": "person", "position": { "x": 4, "y": 1.5 }, - "spriteSheet": "female_office_worker", + "spriteSheet": "female_blowse", "spriteTalk": "assets/characters/hacker-red-talk.png", "spriteConfig": { "idleFrameRate": 2, @@ -352,25 +352,56 @@ password_hints = "Common passwords: Marketing123, Campaign2024, Viral_Dynamics_A "eventPattern": "item_picked_up:contingency_files", "targetKnot": "event_contingency_found", "onceOnly": true + }, + { + "eventPattern": "room_entered:main_office_area", + "targetKnot": "closing_debrief", + "onceOnly": true, + "sendTimedMessage": { + "delay": 1000, + "message": "Mission complete. Return to HQ for debrief.", + "targetKnot": "closing_debrief" + } + }, + { + "eventPattern": "global_variable_changed:derek_confronted", + "targetKnot": "closing_debrief", + "condition": "value === true", + "onceOnly": true, + "sendTimedMessage": { + "delay": 1000, + "message": "Mission complete. Return to HQ for debrief.", + "targetKnot": "closing_debrief" + } } ] }, { - "id": "closing_debrief_trigger", + "id": "closing_debrief_person", "displayName": "Agent 0x99", - "npcType": "phone", - "storyPath": "scenarios/m01_first_contact/ink/m01_closing_debrief.json", - "avatar": "assets/characters/female_spy_headshot.png", - "phoneId": "player_phone", - "currentKnot": "start", - "eventMappings": [ + "npcType": "person", + "eventMapping": [ { - "eventPattern": "global_variable_changed:derek_confronted", - "targetKnot": "start", + "eventPattern": "global_variable_changed:start_debrief_cutscene", "condition": "value === true", + "conversationMode": "person-chat", + "targetKnot": "debrief_location", + "background": "assets/backgrounds/hq1.png", "onceOnly": true } - ] + ], + "position": { "x": 500, "y": 500 }, + "spriteSheet": "female_spy", + "avatar": "assets/characters/female_spy_headshot.png", + "spriteConfig": { + "idleFrameRate": 6, + "walkFrameRate": 10 + }, + "storyPath": "scenarios/m01_first_contact/ink/m01_phone_agent0x99.json", + "currentKnot": "debrief_location", + "behavior": { + "initiallyHidden": true + } } ], "objects": [