From 1d889fe1485bfca11035e76e867e04c060a00547 Mon Sep 17 00:00:00 2001 From: "Z. Cliffe Schreuders" Date: Fri, 7 Nov 2025 16:15:52 +0000 Subject: [PATCH] Enhance NPCManager to support timed conversations and update dialogue handling in PersonChatMinigame - Added functionality to schedule timed conversations in NPCManager, allowing NPCs to automatically initiate dialogues after a specified delay. - Updated PersonChatMinigame to handle multiple dialogue lines and speakers, improving the display logic for accumulated dialogue. - Modified scenario JSON and Ink files to include timed conversation configurations for NPCs, enhancing narrative flow. --- .../person-chat/person-chat-minigame.js | 25 ++++-- js/systems/npc-manager.js | 90 +++++++++++++++++++ scenarios/ink/test2.ink | 28 +++--- scenarios/ink/test2.json | 2 +- scenarios/npc-sprite-test2.json | 6 +- 5 files changed, 127 insertions(+), 24 deletions(-) diff --git a/js/minigames/person-chat/person-chat-minigame.js b/js/minigames/person-chat/person-chat-minigame.js index 8c9ed4b..8a5e72d 100644 --- a/js/minigames/person-chat/person-chat-minigame.js +++ b/js/minigames/person-chat/person-chat-minigame.js @@ -365,15 +365,28 @@ export class PersonChatMinigame extends MinigameScene { // Display choices if available (check this first, before text) if (result.choices && result.choices.length > 0) { - // At a choice point - display choices - this.ui.showChoices(result.choices); console.log(`📋 ${result.choices.length} choices available`); - console.log(`📋 pendingContinueCallback NOT set - waiting for choice selection`); - // Also display any accompanying text if present + // Check if we have accompanying text if (result.text && result.text.trim()) { - console.log(`🗣️ Calling showDialogue with speaker: ${speaker}`); - this.ui.showDialogue(result.text, speaker, true); // preserveChoices=true + // Check if we have multiple lines/speakers (accumulated dialogue) + const hasMultipleLines = result.text.includes('\n'); + const hasMultipleSpeakers = result.tags && result.tags.filter(t => t.includes('speaker:')).length > 1; + + if (hasMultipleLines || hasMultipleSpeakers) { + // Multiple dialogue lines - display them sequentially, choices shown at end + console.log(`🗣️ Initial dialogue has multiple lines/speakers - using block display`); + this.displayAccumulatedDialogue(result); + } else { + // Single line - display immediately with choices + console.log(`🗣️ Single line dialogue - showing with choices immediately`); + this.ui.showChoices(result.choices); + this.ui.showDialogue(result.text, speaker, true); // preserveChoices=true + } + } else { + // No text, just choices - show them immediately + this.ui.showChoices(result.choices); + console.log(`📋 No text, just showing choices`); } } else if (result.text && result.text.trim()) { // Have text but no choices - display and continue diff --git a/js/systems/npc-manager.js b/js/systems/npc-manager.js index b30c4e0..c0d2ff6 100644 --- a/js/systems/npc-manager.js +++ b/js/systems/npc-manager.js @@ -10,6 +10,7 @@ export default class NPCManager { 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.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 @@ -92,6 +93,16 @@ export default class NPCManager { console.log(`[NPCManager] Scheduled ${entry.timedMessages.length} timed messages for ${realId}`); } + // Schedule timed conversations if any are defined + if (entry.timedConversation) { + this.scheduleTimedConversation({ + npcId: realId, + targetKnot: entry.timedConversation.targetKnot, + delay: entry.timedConversation.delay + }); + console.log(`[NPCManager] Scheduled timed conversation for ${realId} to knot: ${entry.timedConversation.targetKnot}`); + } + return entry; } @@ -410,6 +421,51 @@ export default class NPCManager { console.log(`[NPCManager] Scheduled timed message from ${npcId} at ${actualTriggerTime}ms:`, text); } + // Schedule a timed conversation to start after a delay + // Similar to timedMessages but for person NPCs (opens person-chat minigame) + // + // opts: { npcId, targetKnot, triggerTime (ms from game start) OR delay (ms from now) } + // + // Example: After 3 seconds, automatically open a conversation with test_npc_back at the "group_meeting" knot + // scheduleTimedConversation({ + // npcId: 'test_npc_back', + // targetKnot: 'group_meeting', + // delay: 3000 + // }) + // + // USAGE IN SCENARIO JSON: + // { + // "id": "test_npc_back", + // "displayName": "Back NPC", + // "npcType": "person", + // "storyPath": "scenarios/ink/test2.json", + // "currentKnot": "hub", + // "timedConversation": { + // "delay": 3000, // 3 seconds + // "targetKnot": "group_meeting" + // } + // } + scheduleTimedConversation(opts) { + const { npcId, targetKnot, triggerTime, delay } = opts; + + if (!npcId || !targetKnot) { + console.error('[NPCManager] scheduleTimedConversation requires npcId and targetKnot'); + return; + } + + // Use triggerTime if provided, otherwise use delay (defaults to 0) + const actualTriggerTime = triggerTime !== undefined ? triggerTime : (delay || 0); + + this.timedConversations.push({ + npcId, + targetKnot, + triggerTime: actualTriggerTime, // milliseconds from game start + delivered: false + }); + + console.log(`[NPCManager] Scheduled timed conversation from ${npcId} at ${actualTriggerTime}ms to knot: ${targetKnot}`); + } + // Start checking for timed messages (call this when game starts) startTimedMessages() { if (this.timerInterval) { @@ -445,6 +501,14 @@ export default class NPCManager { message.delivered = true; } } + + // Also check timed conversations + for (const conversation of this.timedConversations) { + if (!conversation.delivered && elapsed >= conversation.triggerTime) { + this._deliverTimedConversation(conversation); + conversation.delivered = true; + } + } } // Deliver a timed message (add to history and show bark) @@ -482,6 +546,32 @@ 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) + _deliverTimedConversation(conversation) { + const npc = this.getNPC(conversation.npcId); + if (!npc) { + console.warn(`[NPCManager] Cannot deliver timed conversation: NPC ${conversation.npcId} not found`); + return; + } + + // Update NPC's current knot to the target knot + npc.currentKnot = conversation.targetKnot; + + // Check if MinigameFramework is available to start the person-chat 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 + }); + } else { + console.warn(`[NPCManager] MinigameFramework not available to start person-chat for timed conversation`); + } + + console.log(`[NPCManager] Delivered timed conversation from ${conversation.npcId} to knot: ${conversation.targetKnot}`); + } + // Load timed messages from scenario data // timedMessages: [ { npcId, text, triggerTime, phoneId } ] loadTimedMessages(timedMessages) { diff --git a/scenarios/ink/test2.ink b/scenarios/ink/test2.ink index af39d86..da46fcd 100644 --- a/scenarios/ink/test2.ink +++ b/scenarios/ink/test2.ink @@ -4,58 +4,54 @@ VAR conversation_started = false === hub === # speaker:npc:test_npc_back -Welcome! This is a group conversation test. Let me introduce you to my colleague. +Woop! Welcome! This is a group conversation test. Let me introduce you to my colleague. + [Listen in on the introduction] -> group_meeting === group_meeting === # speaker:npc:test_npc_back Agent, meet my colleague from the back office. BACK --> colleague_introduction ++ [Continue] -> colleague_introduction === colleague_introduction === # speaker:npc:test_npc_front Nice to meet you! I'm the lead technician here. FRONT. --> player_question ++ [Ask about their work] -> player_question === player_question === # speaker:player What kind of work do you both do here? --> front_npc_explains ++ [Listen] -> front_npc_explains === front_npc_explains === # speaker:npc:test_npc_back Well, I handle the front desk operations and guest interactions. But my colleague here... --> colleague_responds ++ [Continue listening] -> colleague_responds === colleague_responds === # speaker:npc:test_npc_front I manage all the backend systems and security infrastructure. Together, we keep everything running smoothly. --> player_follow_up ++ [Respond] -> player_follow_up === player_follow_up === # speaker:player That sounds like a well-coordinated operation! --> front_npc_agrees ++ [Listen more] -> front_npc_agrees === front_npc_agrees === # speaker:npc:test_npc_back It really is! We've been working together for several years now. Communication is key. --> colleague_adds ++ [Hear more] -> colleague_adds === colleague_adds === # speaker:npc:test_npc_front Exactly. And we're always looking for talented people like you to join our team. --> player_closing ++ [Respond] -> player_closing === player_closing === # speaker:player -I appreciate the opportunity. I'll definitely consider it. --> conversation_end - -=== conversation_end === -# speaker:npc:test_npc_back -Great! Feel free to explore and let us know if you have any questions. --> END ++ [I appreciate the opportunity. I'll definitely consider it.] #exit_conversation +Thank you. +-> hub diff --git a/scenarios/ink/test2.json b/scenarios/ink/test2.json index 3b0081c..f73deb3 100644 --- a/scenarios/ink/test2.json +++ b/scenarios/ink/test2.json @@ -1 +1 @@ -{"inkVersion":21,"root":[[["done",{"#n":"g-0"}],null],"done",{"hub":[["#","^speaker:npc:test_npc_back","/#","^Welcome! This is a group conversation test. Let me introduce you to my colleague.","\n","ev","str","^Listen in on the introduction","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["^ ",{"->":"group_meeting"},"\n",null]}],null],"group_meeting":["#","^speaker:npc:test_npc_back","/#","^Agent, meet my colleague from the back office. BACK","\n",{"->":"colleague_introduction"},null],"colleague_introduction":["#","^speaker:npc:test_npc_front","/#","^Nice to meet you! I'm the lead technician here. FRONT.","\n",{"->":"player_question"},null],"player_question":["#","^speaker:player","/#","^What kind of work do you both do here?","\n",{"->":"front_npc_explains"},null],"front_npc_explains":["#","^speaker:npc:test_npc_back","/#","^Well, I handle the front desk operations and guest interactions. But my colleague here...","\n",{"->":"colleague_responds"},null],"colleague_responds":["#","^speaker:npc:test_npc_front","/#","^I manage all the backend systems and security infrastructure. Together, we keep everything running smoothly.","\n",{"->":"player_follow_up"},null],"player_follow_up":["#","^speaker:player","/#","^That sounds like a well-coordinated operation!","\n",{"->":"front_npc_agrees"},null],"front_npc_agrees":["#","^speaker:npc:test_npc_back","/#","^It really is! We've been working together for several years now. Communication is key.","\n",{"->":"colleague_adds"},null],"colleague_adds":["#","^speaker:npc:test_npc_front","/#","^Exactly. And we're always looking for talented people like you to join our team.","\n",{"->":"player_closing"},null],"player_closing":["#","^speaker:player","/#","^I appreciate the opportunity. I'll definitely consider it.","\n",{"->":"conversation_end"},null],"conversation_end":["#","^speaker:npc:test_npc_back","/#","^Great! Feel free to explore and let us know if you have any questions.","\n","end",null],"global decl":["ev",false,{"VAR=":"conversation_started"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[[["done",{"#n":"g-0"}],null],"done",{"hub":[["#","^speaker:npc:test_npc_back","/#","^Woop! Welcome! This is a group conversation test. Let me introduce you to my colleague.","\n","ev","str","^Listen in on the introduction","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["^ ",{"->":"group_meeting"},"\n",null]}],null],"group_meeting":["#","^speaker:npc:test_npc_back","/#","^Agent, meet my colleague from the back office. BACK","\n",{"->":"colleague_introduction"},null],"colleague_introduction":["#","^speaker:npc:test_npc_front","/#","^Nice to meet you! I'm the lead technician here. FRONT.","\n",{"->":"player_question"},null],"player_question":["#","^speaker:player","/#","^What kind of work do you both do here?","\n",{"->":"front_npc_explains"},null],"front_npc_explains":["#","^speaker:npc:test_npc_back","/#","^Well, I handle the front desk operations and guest interactions. But my colleague here...","\n",{"->":"colleague_responds"},null],"colleague_responds":["#","^speaker:npc:test_npc_front","/#","^I manage all the backend systems and security infrastructure. Together, we keep everything running smoothly.","\n",{"->":"player_follow_up"},null],"player_follow_up":["#","^speaker:player","/#","^That sounds like a well-coordinated operation!","\n",{"->":"front_npc_agrees"},null],"front_npc_agrees":["#","^speaker:npc:test_npc_back","/#","^It really is! We've been working together for several years now. Communication is key.","\n",{"->":"colleague_adds"},null],"colleague_adds":["#","^speaker:npc:test_npc_front","/#","^Exactly. And we're always looking for talented people like you to join our team.","\n",{"->":"player_closing"},null],"player_closing":[["#","^speaker:player","/#","ev","str","^I appreciate the opportunity. I'll definitely consider it.","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["^ ","#","^exit_conversation","/#","\n","^Thank you.","\n",{"->":"hub"},null]}],null],"global decl":["ev",false,{"VAR=":"conversation_started"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/scenarios/npc-sprite-test2.json b/scenarios/npc-sprite-test2.json index f20a289..7635c3f 100644 --- a/scenarios/npc-sprite-test2.json +++ b/scenarios/npc-sprite-test2.json @@ -45,7 +45,11 @@ "idleFrameEnd": 23 }, "storyPath": "scenarios/ink/test2.json", - "currentKnot": "hub" + "currentKnot": "hub", + "timedConversation": { + "delay": 3000, + "targetKnot": "group_meeting" + } } ] }