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.
This commit is contained in:
Z. Cliffe Schreuders
2025-11-07 16:15:52 +00:00
parent 3f06b8fc8c
commit 1d889fe148
5 changed files with 127 additions and 24 deletions

View File

@@ -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

View File

@@ -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) {

View File

@@ -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

View File

@@ -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":{}}
{"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":{}}

View File

@@ -45,7 +45,11 @@
"idleFrameEnd": 23
},
"storyPath": "scenarios/ink/test2.json",
"currentKnot": "hub"
"currentKnot": "hub",
"timedConversation": {
"delay": 3000,
"targetKnot": "group_meeting"
}
}
]
}