mirror of
https://github.com/cliffe/BreakEscape.git
synced 2026-02-20 13:50:46 +00:00
- Deleted `phone-messages-minigame.js` and archived related CSS. - Updated `interactions.js` to exclusively use phone-chat with runtime conversion. - Enhanced error handling for phone interactions. - Marked completion of old phone minigame removal in implementation log. - Added detailed cleanup summary documentation.
540 lines
19 KiB
JavaScript
540 lines
19 KiB
JavaScript
/**
|
||
* 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
|
||
*/
|
||
|
||
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';
|
||
|
||
export class PhoneChatMinigame extends MinigameScene {
|
||
/**
|
||
* Create a PhoneChatMinigame instance
|
||
* @param {HTMLElement} container - Container element
|
||
* @param {Object} params - Configuration parameters
|
||
*/
|
||
constructor(container, params) {
|
||
super(container, params);
|
||
|
||
// Debug logging
|
||
console.log('📱 PhoneChatMinigame constructor called with:', { container, params });
|
||
console.log('📱 this.params after super():', this.params);
|
||
|
||
// 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');
|
||
}
|
||
|
||
// 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.');
|
||
}
|
||
|
||
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);
|
||
}
|
||
break;
|
||
|
||
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;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Start the minigame
|
||
*/
|
||
async start() {
|
||
super.start();
|
||
|
||
// Preload intro messages for NPCs without history
|
||
await this.preloadIntroMessages();
|
||
|
||
// 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);
|
||
}
|
||
|
||
console.log('✅ PhoneChatMinigame started');
|
||
}
|
||
|
||
/**
|
||
* 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);
|
||
|
||
// Only preload if no history exists and NPC has a story (path or JSON)
|
||
if (history.length === 0 && (npc.storyPath || npc.storyJSON)) {
|
||
try {
|
||
// Create temporary conversation to get intro message
|
||
const tempConversation = new PhoneChatConversation(npc.id, this.npcManager, this.inkEngine);
|
||
|
||
// Load from storyJSON if available, otherwise from storyPath
|
||
const storySource = npc.storyJSON || npc.storyPath;
|
||
const loaded = await tempConversation.loadStory(storySource);
|
||
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 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;
|
||
}
|
||
|
||
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);
|
||
|
||
// Load conversation history
|
||
const history = this.history.loadHistory();
|
||
const hasHistory = history.length > 0;
|
||
|
||
if (hasHistory) {
|
||
this.ui.addMessages(history);
|
||
// Mark messages as read
|
||
this.history.markAllRead();
|
||
}
|
||
|
||
// Load and start Ink story
|
||
// 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}`);
|
||
this.ui.showNotification('No conversation available', 'error');
|
||
return;
|
||
}
|
||
|
||
const loaded = await this.conversation.loadStory(storySource);
|
||
if (!loaded) {
|
||
this.ui.showNotification('Failed to load conversation', 'error');
|
||
return;
|
||
}
|
||
|
||
// Set conversation as active
|
||
this.isConversationActive = true;
|
||
|
||
// 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');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Continue the Ink story and display new content
|
||
*/
|
||
continueStory() {
|
||
if (!this.conversation || !this.isConversationActive) {
|
||
return;
|
||
}
|
||
|
||
// 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;
|
||
}
|
||
|
||
// Save story state after initial load
|
||
this.saveStoryState();
|
||
}, 500); // Brief delay for typing effect
|
||
}
|
||
|
||
/**
|
||
* Handle player choice selection
|
||
* @param {number} choiceIndex - Index of selected choice
|
||
*/
|
||
handleChoice(choiceIndex) {
|
||
if (!this.conversation || !this.isConversationActive) {
|
||
return;
|
||
}
|
||
|
||
// 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;
|
||
}
|
||
|
||
// Save story state for resuming later
|
||
this.saveStoryState();
|
||
}, 500); // Brief delay for typing effect
|
||
}
|
||
|
||
/**
|
||
* 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);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 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);
|
||
}
|
||
|
||
/**
|
||
* 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
|
||
*/
|
||
cleanup() {
|
||
console.log('🧹 PhoneChatMinigame cleaning up');
|
||
|
||
if (this.ui) {
|
||
this.ui.cleanup();
|
||
}
|
||
|
||
this.isConversationActive = false;
|
||
this.conversation = null;
|
||
this.history = null;
|
||
|
||
// Call parent cleanup
|
||
super.cleanup();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 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'
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
// Export for module usage
|
||
export default PhoneChatMinigame;
|