2025-10-29 13:48:22 +00:00
|
|
|
|
/**
|
2025-10-29 19:17:51 +00:00
|
|
|
|
* PhoneChatMinigame - Main Controller
|
|
|
|
|
|
*
|
|
|
|
|
|
* Extends MinigameScene to provide Phaser-based phone chat functionality.
|
|
|
|
|
|
* Orchestrates UI, conversation, and history management for NPC interactions.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @module phone-chat-minigame
|
2025-10-29 13:48:22 +00:00
|
|
|
|
*/
|
2025-10-29 19:17:51 +00:00
|
|
|
|
|
|
|
|
|
|
import { MinigameScene } from '../framework/base-minigame.js';
|
|
|
|
|
|
import PhoneChatUI from './phone-chat-ui.js';
|
|
|
|
|
|
import PhoneChatConversation from './phone-chat-conversation.js';
|
|
|
|
|
|
import PhoneChatHistory from './phone-chat-history.js';
|
|
|
|
|
|
import InkEngine from '../../systems/ink/ink-engine.js';
|
|
|
|
|
|
|
2025-10-29 13:48:22 +00:00
|
|
|
|
export class PhoneChatMinigame extends MinigameScene {
|
2025-10-29 19:17:51 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* Create a PhoneChatMinigame instance
|
|
|
|
|
|
* @param {HTMLElement} container - Container element
|
|
|
|
|
|
* @param {Object} params - Configuration parameters
|
|
|
|
|
|
*/
|
2025-10-29 13:48:22 +00:00
|
|
|
|
constructor(container, params) {
|
|
|
|
|
|
super(container, params);
|
|
|
|
|
|
|
2025-10-29 19:17:51 +00:00
|
|
|
|
// Debug logging
|
|
|
|
|
|
console.log('📱 PhoneChatMinigame constructor called with:', { container, params });
|
|
|
|
|
|
console.log('📱 this.params after super():', this.params);
|
2025-10-29 13:48:22 +00:00
|
|
|
|
|
2025-10-29 19:17:51 +00:00
|
|
|
|
// Ensure params exists (use this.params from parent)
|
|
|
|
|
|
const safeParams = this.params || {};
|
|
|
|
|
|
console.log('📱 safeParams:', safeParams);
|
|
|
|
|
|
|
|
|
|
|
|
// Validate required params
|
|
|
|
|
|
if (!safeParams.npcId && !safeParams.phoneId) {
|
|
|
|
|
|
console.error('❌ Missing required params. npcId:', safeParams.npcId, 'phoneId:', safeParams.phoneId);
|
|
|
|
|
|
throw new Error('PhoneChatMinigame requires either npcId or phoneId');
|
2025-10-29 13:48:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-29 19:17:51 +00:00
|
|
|
|
// Get NPC manager from window (set up by main.js)
|
|
|
|
|
|
if (!window.npcManager) {
|
|
|
|
|
|
throw new Error('NPCManager not found. Ensure main.js has initialized it.');
|
|
|
|
|
|
}
|
2025-10-29 13:48:22 +00:00
|
|
|
|
|
2025-10-29 19:17:51 +00:00
|
|
|
|
this.npcManager = window.npcManager;
|
|
|
|
|
|
this.inkEngine = new InkEngine();
|
|
|
|
|
|
|
|
|
|
|
|
// Initialize modules (will be set up in init())
|
|
|
|
|
|
this.ui = null;
|
|
|
|
|
|
this.conversation = null;
|
|
|
|
|
|
this.history = null;
|
|
|
|
|
|
|
|
|
|
|
|
// State
|
|
|
|
|
|
this.currentNPCId = safeParams.npcId || null;
|
|
|
|
|
|
this.phoneId = safeParams.phoneId || 'player_phone';
|
|
|
|
|
|
this.isConversationActive = false;
|
|
|
|
|
|
|
|
|
|
|
|
console.log('📱 PhoneChatMinigame created', {
|
|
|
|
|
|
npcId: this.currentNPCId,
|
|
|
|
|
|
phoneId: this.phoneId
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Initialize the minigame UI and components
|
|
|
|
|
|
*/
|
|
|
|
|
|
init() {
|
|
|
|
|
|
// Call parent init to set up basic structure
|
|
|
|
|
|
super.init();
|
|
|
|
|
|
|
|
|
|
|
|
// Ensure params exists
|
|
|
|
|
|
const safeParams = this.params || {};
|
|
|
|
|
|
|
|
|
|
|
|
// Customize header
|
|
|
|
|
|
this.headerElement.innerHTML = `
|
|
|
|
|
|
<h3>${safeParams.title || 'Phone'}</h3>
|
|
|
|
|
|
<p>Messages and conversations</p>
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
// Initialize UI
|
|
|
|
|
|
this.ui = new PhoneChatUI(this.gameContainer, safeParams, this.npcManager);
|
|
|
|
|
|
this.ui.render();
|
|
|
|
|
|
|
|
|
|
|
|
// Set up event listeners
|
|
|
|
|
|
this.setupEventListeners();
|
|
|
|
|
|
|
|
|
|
|
|
console.log('✅ PhoneChatMinigame initialized');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Set up event listeners for UI interactions
|
|
|
|
|
|
*/
|
|
|
|
|
|
setupEventListeners() {
|
|
|
|
|
|
// Contact list item clicks
|
|
|
|
|
|
this.addEventListener(this.ui.elements.contactList, 'click', (e) => {
|
|
|
|
|
|
const contactItem = e.target.closest('.contact-item');
|
|
|
|
|
|
if (contactItem) {
|
|
|
|
|
|
const npcId = contactItem.dataset.npcId;
|
|
|
|
|
|
this.openConversation(npcId);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Back button (return to contact list)
|
|
|
|
|
|
this.addEventListener(this.ui.elements.backButton, 'click', () => {
|
|
|
|
|
|
this.closeConversation();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Choice button clicks
|
|
|
|
|
|
this.addEventListener(this.ui.elements.choicesContainer, 'click', (e) => {
|
|
|
|
|
|
const choiceButton = e.target.closest('.choice-button');
|
|
|
|
|
|
if (choiceButton) {
|
|
|
|
|
|
const choiceIndex = parseInt(choiceButton.dataset.index);
|
|
|
|
|
|
this.handleChoice(choiceIndex);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Keyboard shortcuts
|
|
|
|
|
|
this.addEventListener(document, 'keydown', (e) => {
|
|
|
|
|
|
this.handleKeyPress(e);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Handle keyboard input
|
|
|
|
|
|
* @param {KeyboardEvent} event - Keyboard event
|
|
|
|
|
|
*/
|
|
|
|
|
|
handleKeyPress(event) {
|
|
|
|
|
|
if (!this.gameState.isActive) return;
|
|
|
|
|
|
|
|
|
|
|
|
switch(event.key) {
|
|
|
|
|
|
case 'Escape':
|
|
|
|
|
|
if (this.ui.getCurrentView() === 'conversation') {
|
|
|
|
|
|
// Go back to contact list
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
this.closeConversation();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Close minigame
|
|
|
|
|
|
this.complete(false);
|
2025-10-29 13:48:22 +00:00
|
|
|
|
}
|
2025-10-29 19:17:51 +00:00
|
|
|
|
break;
|
2025-10-29 13:48:22 +00:00
|
|
|
|
|
2025-10-29 19:17:51 +00:00
|
|
|
|
case '1':
|
|
|
|
|
|
case '2':
|
|
|
|
|
|
case '3':
|
|
|
|
|
|
case '4':
|
|
|
|
|
|
case '5':
|
|
|
|
|
|
// Quick choice selection (1-5)
|
|
|
|
|
|
if (this.ui.getCurrentView() === 'conversation') {
|
|
|
|
|
|
const choiceIndex = parseInt(event.key) - 1;
|
|
|
|
|
|
const choices = this.ui.elements.choicesContainer.querySelectorAll('.choice-button');
|
|
|
|
|
|
if (choices[choiceIndex]) {
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
this.handleChoice(choiceIndex);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
2025-10-29 13:48:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-10-29 19:17:51 +00:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Start the minigame
|
|
|
|
|
|
*/
|
2025-10-30 01:31:37 +00:00
|
|
|
|
async start() {
|
2025-10-29 19:17:51 +00:00
|
|
|
|
super.start();
|
2025-10-29 13:48:22 +00:00
|
|
|
|
|
2025-10-30 01:31:37 +00:00
|
|
|
|
// Preload intro messages for NPCs without history
|
|
|
|
|
|
await this.preloadIntroMessages();
|
|
|
|
|
|
|
2025-10-29 19:17:51 +00:00
|
|
|
|
// If NPC ID provided, open that conversation directly
|
|
|
|
|
|
if (this.currentNPCId) {
|
|
|
|
|
|
this.openConversation(this.currentNPCId);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Show contact list for this phone
|
|
|
|
|
|
this.ui.showContactList(this.phoneId);
|
2025-10-29 13:48:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-29 19:17:51 +00:00
|
|
|
|
console.log('✅ PhoneChatMinigame started');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-30 01:31:37 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* Preload intro messages for NPCs that have no conversation history
|
|
|
|
|
|
* This makes it look like messages exist before opening the conversation
|
|
|
|
|
|
*/
|
|
|
|
|
|
async preloadIntroMessages() {
|
|
|
|
|
|
// Get all NPCs for this phone
|
|
|
|
|
|
const npcs = this.phoneId
|
|
|
|
|
|
? this.npcManager.getNPCsByPhone(this.phoneId)
|
|
|
|
|
|
: Array.from(this.npcManager.npcs.values());
|
|
|
|
|
|
|
|
|
|
|
|
for (const npc of npcs) {
|
|
|
|
|
|
const history = this.npcManager.getConversationHistory(npc.id);
|
|
|
|
|
|
|
2025-10-30 02:45:05 +00:00
|
|
|
|
// Only preload if no history exists and NPC has a story (path or JSON)
|
|
|
|
|
|
if (history.length === 0 && (npc.storyPath || npc.storyJSON)) {
|
2025-10-30 01:31:37 +00:00
|
|
|
|
try {
|
|
|
|
|
|
// Create temporary conversation to get intro message
|
|
|
|
|
|
const tempConversation = new PhoneChatConversation(npc.id, this.npcManager, this.inkEngine);
|
2025-10-30 02:45:05 +00:00
|
|
|
|
|
|
|
|
|
|
// Load from storyJSON if available, otherwise from storyPath
|
|
|
|
|
|
const storySource = npc.storyJSON || npc.storyPath;
|
|
|
|
|
|
const loaded = await tempConversation.loadStory(storySource);
|
2025-10-30 01:31:37 +00:00
|
|
|
|
|
|
|
|
|
|
if (loaded) {
|
|
|
|
|
|
// Navigate to start
|
|
|
|
|
|
const startKnot = npc.currentKnot || 'start';
|
|
|
|
|
|
tempConversation.goToKnot(startKnot);
|
|
|
|
|
|
|
|
|
|
|
|
// Get intro message
|
|
|
|
|
|
const result = tempConversation.continue();
|
|
|
|
|
|
|
|
|
|
|
|
if (result.text && result.text.trim()) {
|
|
|
|
|
|
// Add intro message(s) to history
|
|
|
|
|
|
const messages = result.text.trim().split('\n').filter(line => line.trim());
|
|
|
|
|
|
messages.forEach(message => {
|
|
|
|
|
|
if (message.trim()) {
|
|
|
|
|
|
this.npcManager.addMessage(npc.id, 'npc', message.trim(), {
|
|
|
|
|
|
preloaded: true,
|
|
|
|
|
|
timestamp: Date.now() - 3600000 // 1 hour ago
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Save the story state after preloading
|
|
|
|
|
|
// This prevents the intro from replaying when conversation is opened
|
|
|
|
|
|
npc.storyState = tempConversation.saveState();
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`📝 Preloaded intro message for ${npc.id} and saved state`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.warn(`⚠️ Could not preload intro for ${npc.id}:`, error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-29 19:17:51 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* Open a conversation with an NPC
|
|
|
|
|
|
* @param {string} npcId - NPC identifier
|
|
|
|
|
|
*/
|
|
|
|
|
|
async openConversation(npcId) {
|
|
|
|
|
|
const npc = this.npcManager.getNPC(npcId);
|
|
|
|
|
|
if (!npc) {
|
|
|
|
|
|
console.error(`❌ NPC not found: ${npcId}`);
|
|
|
|
|
|
this.ui.showNotification('Contact not found', 'error');
|
|
|
|
|
|
return;
|
2025-10-29 13:48:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-29 19:17:51 +00:00
|
|
|
|
console.log(`💬 Opening conversation with ${npc.displayName || npcId}`);
|
|
|
|
|
|
|
|
|
|
|
|
// Update current NPC
|
|
|
|
|
|
this.currentNPCId = npcId;
|
|
|
|
|
|
|
|
|
|
|
|
// Initialize conversation modules
|
|
|
|
|
|
this.history = new PhoneChatHistory(npcId, this.npcManager);
|
|
|
|
|
|
this.conversation = new PhoneChatConversation(npcId, this.npcManager, this.inkEngine);
|
|
|
|
|
|
|
|
|
|
|
|
// Show conversation view
|
|
|
|
|
|
this.ui.showConversation(npcId);
|
2025-10-29 13:48:22 +00:00
|
|
|
|
|
2025-10-29 19:17:51 +00:00
|
|
|
|
// Load conversation history
|
|
|
|
|
|
const history = this.history.loadHistory();
|
2025-10-30 01:31:37 +00:00
|
|
|
|
const hasHistory = history.length > 0;
|
|
|
|
|
|
|
|
|
|
|
|
if (hasHistory) {
|
2025-10-29 19:17:51 +00:00
|
|
|
|
this.ui.addMessages(history);
|
|
|
|
|
|
// Mark messages as read
|
|
|
|
|
|
this.history.markAllRead();
|
2025-10-29 13:48:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-29 19:17:51 +00:00
|
|
|
|
// Load and start Ink story
|
2025-10-30 02:45:05 +00:00
|
|
|
|
// Support both storyJSON (inline) and storyPath (file)
|
|
|
|
|
|
const storySource = npc.storyJSON || npc.storyPath || npc.inkStoryPath;
|
|
|
|
|
|
if (!storySource) {
|
|
|
|
|
|
console.error(`❌ No story source found for ${npcId}`);
|
2025-10-29 19:17:51 +00:00
|
|
|
|
this.ui.showNotification('No conversation available', 'error');
|
|
|
|
|
|
return;
|
2025-10-29 13:48:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-30 02:45:05 +00:00
|
|
|
|
const loaded = await this.conversation.loadStory(storySource);
|
2025-10-29 19:17:51 +00:00
|
|
|
|
if (!loaded) {
|
|
|
|
|
|
this.ui.showNotification('Failed to load conversation', 'error');
|
|
|
|
|
|
return;
|
2025-10-29 13:48:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-30 01:31:37 +00:00
|
|
|
|
// Set conversation as active
|
2025-10-29 19:17:51 +00:00
|
|
|
|
this.isConversationActive = true;
|
2025-10-30 01:31:37 +00:00
|
|
|
|
|
|
|
|
|
|
// Check if we have saved story state to restore
|
|
|
|
|
|
if (hasHistory && npc.storyState) {
|
|
|
|
|
|
// Restore previous story state
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
|
|
// First time opening - show intro message and choices
|
|
|
|
|
|
this.continueStory();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Show current choices without continuing story (for reopening conversations)
|
|
|
|
|
|
*/
|
|
|
|
|
|
showCurrentChoices() {
|
|
|
|
|
|
if (!this.conversation || !this.isConversationActive) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Get current state without continuing
|
|
|
|
|
|
const result = this.conversation.getCurrentState();
|
|
|
|
|
|
|
|
|
|
|
|
if (result.choices && result.choices.length > 0) {
|
|
|
|
|
|
this.ui.addChoices(result.choices);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log('ℹ️ No choices available in current state');
|
|
|
|
|
|
}
|
2025-10-29 13:48:22 +00:00
|
|
|
|
}
|
2025-10-29 19:17:51 +00:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Continue the Ink story and display new content
|
|
|
|
|
|
*/
|
|
|
|
|
|
continueStory() {
|
|
|
|
|
|
if (!this.conversation || !this.isConversationActive) {
|
|
|
|
|
|
return;
|
2025-10-29 13:48:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-29 19:17:51 +00:00
|
|
|
|
// Show typing indicator briefly
|
|
|
|
|
|
this.ui.showTypingIndicator();
|
|
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
this.ui.hideTypingIndicator();
|
|
|
|
|
|
|
|
|
|
|
|
// Get next story content
|
|
|
|
|
|
const result = this.conversation.continue();
|
|
|
|
|
|
console.log('📖 Story continue result:', result);
|
|
|
|
|
|
console.log('📖 Choices:', result.choices);
|
|
|
|
|
|
console.log('📖 Choices length:', result.choices?.length);
|
|
|
|
|
|
|
|
|
|
|
|
// If story has ended
|
|
|
|
|
|
if (result.hasEnded) {
|
|
|
|
|
|
console.log('🏁 Conversation ended');
|
|
|
|
|
|
this.ui.showNotification('Conversation ended', 'info');
|
|
|
|
|
|
this.isConversationActive = false;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Display NPC messages
|
|
|
|
|
|
if (result.text && result.text.trim()) {
|
|
|
|
|
|
const npcMessages = result.text.trim().split('\n').filter(line => line.trim());
|
|
|
|
|
|
|
|
|
|
|
|
npcMessages.forEach(message => {
|
|
|
|
|
|
if (message.trim()) {
|
|
|
|
|
|
this.ui.addMessage('npc', message.trim());
|
|
|
|
|
|
this.history.addMessage('npc', message.trim());
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Display choices
|
|
|
|
|
|
if (result.choices && result.choices.length > 0) {
|
|
|
|
|
|
this.ui.addChoices(result.choices);
|
|
|
|
|
|
} else if (!result.canContinue) {
|
|
|
|
|
|
// No more content and no choices - end conversation
|
|
|
|
|
|
console.log('🏁 No more choices available');
|
|
|
|
|
|
this.isConversationActive = false;
|
|
|
|
|
|
}
|
2025-10-30 01:31:37 +00:00
|
|
|
|
|
|
|
|
|
|
// Save story state after initial load
|
|
|
|
|
|
this.saveStoryState();
|
2025-10-29 19:17:51 +00:00
|
|
|
|
}, 500); // Brief delay for typing effect
|
2025-10-29 13:48:22 +00:00
|
|
|
|
}
|
2025-10-29 19:17:51 +00:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Handle player choice selection
|
|
|
|
|
|
* @param {number} choiceIndex - Index of selected choice
|
|
|
|
|
|
*/
|
|
|
|
|
|
handleChoice(choiceIndex) {
|
|
|
|
|
|
if (!this.conversation || !this.isConversationActive) {
|
|
|
|
|
|
return;
|
2025-10-29 13:48:22 +00:00
|
|
|
|
}
|
2025-10-29 19:17:51 +00:00
|
|
|
|
|
|
|
|
|
|
// Get choice text before making choice
|
|
|
|
|
|
const choices = this.ui.elements.choicesContainer.querySelectorAll('.choice-button');
|
|
|
|
|
|
const choiceButton = choices[choiceIndex];
|
|
|
|
|
|
if (!choiceButton) {
|
|
|
|
|
|
console.error(`❌ Invalid choice index: ${choiceIndex}`);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const choiceText = choiceButton.textContent;
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`👆 Player chose: ${choiceText}`);
|
|
|
|
|
|
|
|
|
|
|
|
// Display player's choice as a message
|
|
|
|
|
|
this.ui.addMessage('player', choiceText);
|
|
|
|
|
|
this.history.addMessage('player', choiceText, { choice: choiceIndex });
|
|
|
|
|
|
|
|
|
|
|
|
// Clear choices
|
|
|
|
|
|
this.ui.clearChoices();
|
|
|
|
|
|
|
|
|
|
|
|
// Make choice in Ink story (this also continues and returns the result)
|
|
|
|
|
|
const result = this.conversation.makeChoice(choiceIndex);
|
|
|
|
|
|
|
|
|
|
|
|
// Display the result from makeChoice (don't call continueStory again!)
|
|
|
|
|
|
if (result.hasEnded) {
|
|
|
|
|
|
console.log('🏁 Conversation ended');
|
|
|
|
|
|
this.ui.showNotification('Conversation ended', 'info');
|
|
|
|
|
|
this.isConversationActive = false;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Show typing indicator briefly
|
|
|
|
|
|
this.ui.showTypingIndicator();
|
|
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
this.ui.hideTypingIndicator();
|
|
|
|
|
|
|
|
|
|
|
|
// Display NPC messages from the result
|
|
|
|
|
|
if (result.text && result.text.trim()) {
|
|
|
|
|
|
const npcMessages = result.text.trim().split('\n').filter(line => line.trim());
|
|
|
|
|
|
|
|
|
|
|
|
npcMessages.forEach(message => {
|
|
|
|
|
|
if (message.trim()) {
|
|
|
|
|
|
this.ui.addMessage('npc', message.trim());
|
|
|
|
|
|
this.history.addMessage('npc', message.trim());
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Display choices
|
|
|
|
|
|
if (result.choices && result.choices.length > 0) {
|
|
|
|
|
|
this.ui.addChoices(result.choices);
|
|
|
|
|
|
} else if (!result.canContinue) {
|
|
|
|
|
|
// No more content and no choices - end conversation
|
|
|
|
|
|
console.log('🏁 No more choices available');
|
|
|
|
|
|
this.isConversationActive = false;
|
|
|
|
|
|
}
|
2025-10-30 01:31:37 +00:00
|
|
|
|
|
|
|
|
|
|
// Save story state for resuming later
|
|
|
|
|
|
this.saveStoryState();
|
2025-10-29 19:17:51 +00:00
|
|
|
|
}, 500); // Brief delay for typing effect
|
2025-10-29 13:48:22 +00:00
|
|
|
|
}
|
2025-10-29 19:17:51 +00:00
|
|
|
|
|
2025-10-30 01:31:37 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* Save the current Ink story state to NPC data
|
|
|
|
|
|
*/
|
|
|
|
|
|
saveStoryState() {
|
|
|
|
|
|
if (!this.conversation || !this.currentNPCId) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const npc = this.npcManager.getNPC(this.currentNPCId);
|
|
|
|
|
|
if (npc) {
|
|
|
|
|
|
const state = this.conversation.saveState();
|
|
|
|
|
|
npc.storyState = state;
|
|
|
|
|
|
console.log('💾 Saved story state for', this.currentNPCId);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-29 19:17:51 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* Close the current conversation and return to contact list
|
|
|
|
|
|
*/
|
|
|
|
|
|
closeConversation() {
|
|
|
|
|
|
console.log('🔙 Closing conversation');
|
|
|
|
|
|
|
|
|
|
|
|
this.isConversationActive = false;
|
|
|
|
|
|
this.currentNPCId = null;
|
|
|
|
|
|
this.conversation = null;
|
|
|
|
|
|
this.history = null;
|
|
|
|
|
|
|
|
|
|
|
|
// Show contact list
|
|
|
|
|
|
this.ui.showContactList(this.phoneId);
|
2025-10-29 13:48:22 +00:00
|
|
|
|
}
|
2025-10-29 19:17:51 +00:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Complete the minigame
|
|
|
|
|
|
* @param {boolean} success - Whether minigame was successful
|
|
|
|
|
|
*/
|
|
|
|
|
|
complete(success) {
|
|
|
|
|
|
console.log('📱 PhoneChatMinigame completing', { success });
|
|
|
|
|
|
|
|
|
|
|
|
// Clean up conversation
|
|
|
|
|
|
this.isConversationActive = false;
|
|
|
|
|
|
|
|
|
|
|
|
// Call parent complete
|
|
|
|
|
|
super.complete(success);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Clean up resources
|
|
|
|
|
|
*/
|
2025-10-29 13:48:22 +00:00
|
|
|
|
cleanup() {
|
2025-10-29 19:17:51 +00:00
|
|
|
|
console.log('🧹 PhoneChatMinigame cleaning up');
|
|
|
|
|
|
|
|
|
|
|
|
if (this.ui) {
|
|
|
|
|
|
this.ui.cleanup();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.isConversationActive = false;
|
|
|
|
|
|
this.conversation = null;
|
|
|
|
|
|
this.history = null;
|
|
|
|
|
|
|
|
|
|
|
|
// Call parent cleanup
|
2025-10-29 13:48:22 +00:00
|
|
|
|
super.cleanup();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-10-29 19:17:51 +00:00
|
|
|
|
|
2025-10-30 10:16:31 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* Return to phone-chat after notes minigame
|
|
|
|
|
|
* Called by notes minigame when user closes it and needs to return to phone
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function returnToPhoneAfterNotes() {
|
|
|
|
|
|
console.log('Returning to phone-chat after notes minigame');
|
|
|
|
|
|
|
|
|
|
|
|
// Check if there's a pending phone return
|
|
|
|
|
|
if (window.pendingPhoneReturn) {
|
|
|
|
|
|
const phoneState = window.pendingPhoneReturn;
|
|
|
|
|
|
|
|
|
|
|
|
// Clear the pending return state
|
|
|
|
|
|
window.pendingPhoneReturn = null;
|
|
|
|
|
|
|
|
|
|
|
|
// Restart the phone-chat minigame with the saved state
|
|
|
|
|
|
if (window.MinigameFramework) {
|
|
|
|
|
|
window.MinigameFramework.startMinigame('phone-chat', null, phoneState.params || {
|
|
|
|
|
|
phoneId: phoneState.phoneId || 'default_phone',
|
|
|
|
|
|
title: phoneState.title || 'Phone'
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-29 19:17:51 +00:00
|
|
|
|
// Export for module usage
|
|
|
|
|
|
export default PhoneChatMinigame;
|