Files
BreakEscape/js/minigames/phone-chat/phone-chat-minigame.js
Z. Cliffe Schreuders 01010e7e20 refactor: Remove old phone-messages minigame and transition to phone-chat
- 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.
2025-10-30 10:16:31 +00:00

540 lines
19 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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;