From 717221cb3ca696fefc7814317cab91168b5471a6 Mon Sep 17 00:00:00 2001 From: "Z. Cliffe Schreuders" Date: Fri, 7 Nov 2025 17:27:35 +0000 Subject: [PATCH] Refactor PersonChatMinigame and update canvas handling in portraits - Removed the outdated PersonChatMinigame implementation to streamline the codebase. - Updated CSS for image handling in the chat minigame to use 'contain' for better aspect ratio maintenance. - Improved canvas size management in PersonChatPortraits to ensure optimal rendering based on container dimensions, enhancing visual fidelity. --- css/person-chat-minigame.css | 2 +- .../person-chat/person-chat-minigame-old.js | 287 ------------------ .../person-chat/person-chat-portraits.js | 61 ++-- 3 files changed, 42 insertions(+), 308 deletions(-) delete mode 100644 js/minigames/person-chat/person-chat-minigame-old.js diff --git a/css/person-chat-minigame.css b/css/person-chat-minigame.css index 39037d1..bc4e75a 100644 --- a/css/person-chat-minigame.css +++ b/css/person-chat-minigame.css @@ -77,7 +77,7 @@ display: block; width: 100%; height: 100%; - object-fit: cover; + object-fit: contain; /* Changed from cover to contain to maintain aspect ratio and match main game scaling */ border: none; background-color: #000; } diff --git a/js/minigames/person-chat/person-chat-minigame-old.js b/js/minigames/person-chat/person-chat-minigame-old.js deleted file mode 100644 index 82ca755..0000000 --- a/js/minigames/person-chat/person-chat-minigame-old.js +++ /dev/null @@ -1,287 +0,0 @@ -/** - * PersonChatMinigame - Main Person-Chat Minigame Controller - * - * Extends MinigameScene to provide cinematic in-person conversation interface. - * Orchestrates: - * - Portrait rendering (NPC and player) - * - Dialogue display - * - Choice selection - * - Ink story progression - * - * @module person-chat-minigame - */ - -import { MinigameScene } from '../framework/base-minigame.js'; -import PersonChatUI from './person-chat-ui.js'; -import PhoneChatConversation from '../phone-chat/phone-chat-conversation.js'; // Reuse phone-chat conversation logic -import InkEngine from '../../systems/ink/ink-engine.js?v=1'; - -export class PersonChatMinigame extends MinigameScene { - /** - * Create a PersonChatMinigame instance - * @param {HTMLElement} container - Container element - * @param {Object} params - Configuration parameters - */ - constructor(container, params) { - super(container, params); - - // Get required globals - if (!window.game || !window.npcManager) { - throw new Error('PersonChatMinigame requires window.game and window.npcManager'); - } - - this.game = window.game; - this.npcManager = window.npcManager; - this.player = window.player; - - // Create InkEngine instance for this conversation - this.inkEngine = new InkEngine(`person-chat-${params.npcId}`); - - // Parameters - this.npcId = params.npcId; - this.title = params.title || 'Conversation'; - - // Verify NPC exists - const npc = this.npcManager.getNPC(this.npcId); - if (!npc) { - throw new Error(`NPC not found: ${this.npcId}`); - } - this.npc = npc; - - // Modules - this.ui = null; - this.conversation = null; - - // State - this.isConversationActive = false; - - console.log(`🎭 PersonChatMinigame created for NPC: ${this.npcId}`); - } - - /** - * Initialize the minigame UI and components - */ - init() { - // Set up basic minigame structure (header, container, etc.) - if (!this.params.cancelText) { - this.params.cancelText = 'End Conversation'; - } - super.init(); - - // Customize header - this.headerElement.innerHTML = ` -

🎭 ${this.title}

-

Speaking with ${this.npc.displayName}

- `; - - // Create UI - this.ui = new PersonChatUI(this.gameContainer, { - game: this.game, - npc: this.npc, - playerSprite: this.player - }, this.npcManager); - - this.ui.render(); - - // Set up event listeners - this.setupEventListeners(); - - console.log('✅ PersonChatMinigame initialized'); - } - - /** - * Set up event listeners for UI interactions - */ - setupEventListeners() { - // Choice button clicks - this.addEventListener(this.ui.elements.choicesContainer, 'click', (e) => { - const choiceButton = e.target.closest('.person-chat-choice-button'); - if (choiceButton) { - const choiceIndex = parseInt(choiceButton.dataset.index); - this.handleChoice(choiceIndex); - } - }); - } - - /** - * Start the minigame - * Initializes conversation flow - */ - start() { - super.start(); - - console.log('🎭 PersonChatMinigame started'); - - // Start conversation with Ink - this.startConversation(); - } - - /** - * Start conversation with NPC - * Loads Ink story and shows initial dialogue - */ - async startConversation() { - try { - // Create conversation manager using PhoneChatConversation (reused logic) - this.conversation = new PhoneChatConversation(this.npcId, this.npcManager, this.inkEngine); - - // Load story from NPC's storyPath or storyJSON - const storySource = this.npc.storyJSON || this.npc.storyPath; - const loaded = await this.conversation.loadStory(storySource); - - if (!loaded) { - console.error('❌ Failed to load conversation story'); - this.showError('Failed to load conversation'); - return; - } - - // Navigate to start knot - const startKnot = this.npc.currentKnot || 'start'; - this.conversation.goToKnot(startKnot); - - this.isConversationActive = true; - - // Show initial dialogue - this.showCurrentDialogue(); - - console.log('✅ Conversation started'); - } catch (error) { - console.error('❌ Error starting conversation:', error); - this.showError('An error occurred during conversation'); - } - } - - /** - * Display current dialogue and choices - */ - showCurrentDialogue() { - if (!this.conversation) return; - - try { - // Continue the story to get next content - const result = this.conversation.continue(); - - // Check if story has ended - if (result.hasEnded) { - this.endConversation(); - return; - } - - // Display dialogue text - if (result.text && result.text.trim()) { - this.ui.showDialogue(result.text, this.npcId); - } - - // Display choices - if (result.choices && result.choices.length > 0) { - this.ui.showChoices(result.choices); - } else if (!result.canContinue) { - // No more content and no choices - conversation ended - this.endConversation(); - } - } catch (error) { - console.error('❌ Error showing dialogue:', error); - this.showError('An error occurred during conversation'); - } - } - - /** - * Handle choice selection - * @param {number} choiceIndex - Index of selected choice - */ - handleChoice(choiceIndex) { - if (!this.conversation) return; - - try { - console.log(`📝 Choice selected: ${choiceIndex}`); - - // Make choice in conversation (this also continues the story) - const result = this.conversation.makeChoice(choiceIndex); - - // Clear old choices - this.ui.hideChoices(); - - // Show new dialogue after a small delay for visual feedback - setTimeout(() => { - // Display the result - if (result.hasEnded) { - this.endConversation(); - } else { - // Display new text and choices - if (result.text && result.text.trim()) { - this.ui.showDialogue(result.text, this.npcId); - } - if (result.choices && result.choices.length > 0) { - this.ui.showChoices(result.choices); - } - } - }, 200); - } catch (error) { - console.error('❌ Error handling choice:', error); - this.showError('Failed to process choice'); - } - } - - /** - * Show error message - * @param {string} message - Error message - */ - showError(message) { - if (this.messageContainer) { - this.messageContainer.innerHTML = `
${message}
`; - } - console.error(`⚠️ Error: ${message}`); - } - - /** - * End conversation and close minigame - */ - endConversation() { - try { - console.log('🎭 Ending conversation'); - - // Cleanup conversation - this.conversation = null; - this.isConversationActive = false; - - // Close minigame - this.complete(true); - - } catch (error) { - console.error('❌ Error ending conversation:', error); - this.complete(false); - } - } - - /** - * Cleanup and destroy minigame - */ - destroy() { - try { - // Stop conversation - if (this.conversation) { - this.conversation.end(); - this.conversation = null; - } - - // Destroy UI - if (this.ui) { - this.ui.destroy(); - this.ui = null; - } - - console.log('✅ PersonChatMinigame destroyed'); - } catch (error) { - console.error('❌ Error destroying minigame:', error); - } - } - - /** - * Complete minigame with success/failure - * @param {boolean} success - Whether minigame succeeded - */ - complete(success) { - this.destroy(); - super.complete(success); - } -} diff --git a/js/minigames/person-chat/person-chat-portraits.js b/js/minigames/person-chat/person-chat-portraits.js index 897f04e..cbf5dba 100644 --- a/js/minigames/person-chat/person-chat-portraits.js +++ b/js/minigames/person-chat/person-chat-portraits.js @@ -55,9 +55,6 @@ export default class PersonChatPortraits { // Create canvas this.canvas = document.createElement('canvas'); - // Set canvas to use full available screen size - this.updateCanvasSize(); - this.canvas.className = 'person-chat-portrait'; this.canvas.id = `portrait-${this.npc.id}`; this.ctx = this.canvas.getContext('2d'); @@ -70,14 +67,22 @@ export default class PersonChatPortraits { this.canvas.style.width = '100%'; this.canvas.style.height = '100%'; - // Add to container + // Add to container first so it has dimensions this.portraitContainer.innerHTML = ''; this.portraitContainer.appendChild(this.canvas); // Get sprite sheet and frame this.setupSpriteInfo(); - // Render portrait + // Set canvas size after it's in the DOM (container now has dimensions) + // Use a small delay to ensure container is fully laid out + setTimeout(() => { + this.updateCanvasSize(); + this.render(); + }, 0); + + // Also set initial size immediately (in case container is already sized) + this.updateCanvasSize(); this.render(); // Handle window resize @@ -92,17 +97,30 @@ export default class PersonChatPortraits { } /** - * Calculate optimal integer scale factor for current browser window + * Calculate optimal integer scale factor for current container * Matches base resolution (640x480) with pixel-perfect scaling + * Uses the same logic as the main game * @returns {number} Optimal integer scale factor */ calculateOptimalScale() { + // Try to get the game-container (same as main game uses) + const gameContainer = document.getElementById('game-container'); + const container = gameContainer || this.portraitContainer; + + if (!container) { + return 2; // Default fallback + } + + const containerWidth = container.clientWidth; + const containerHeight = container.clientHeight; + + // Base resolution (same as main game) const baseWidth = 640; const baseHeight = 480; // Calculate scale factors for both dimensions - const scaleX = window.innerWidth / baseWidth; - const scaleY = window.innerHeight / baseHeight; + const scaleX = containerWidth / baseWidth; + const scaleY = containerHeight / baseHeight; // Use the smaller scale to maintain aspect ratio const maxScale = Math.min(scaleX, scaleY); @@ -116,7 +134,7 @@ export default class PersonChatPortraits { const scaledHeight = baseHeight * scale; // If this scale fits within the container, use it - if (scaledWidth <= window.innerWidth && scaledHeight <= window.innerHeight) { + if (scaledWidth <= containerWidth && scaledHeight <= containerHeight) { bestScale = scale; } else { break; // Stop at the largest scale that fits @@ -127,25 +145,26 @@ export default class PersonChatPortraits { } /** - * Update canvas size to match available screen space with pixel-perfect scaling + * Update canvas size to match available container space with pixel-perfect scaling + * Uses the same approach as the main game */ updateCanvasSize() { if (!this.canvas) return; - // Calculate optimal scale for pixel-perfect rendering + // Calculate optimal scale for pixel-perfect rendering (same as main game) const optimalScale = this.calculateOptimalScale(); const baseWidth = 640; const baseHeight = 480; - // Set canvas internal resolution based on optimal scale + // Set canvas internal resolution to scaled resolution for pixel-perfect rendering + // This matches how the main game uses Phaser's scale system this.canvas.width = baseWidth * optimalScale; this.canvas.height = baseHeight * optimalScale; - // Apply CSS scale to maintain proper viewport size - this.canvas.style.width = '100%'; - this.canvas.style.height = '100%'; + // CSS handles the display sizing (width/height 100% with object-fit: contain) + // The canvas internal resolution is set above for pixel-perfect rendering - console.log(`🎨 Canvas scaled to ${optimalScale}x (${this.canvas.width}x${this.canvas.height}px)`); + console.log(`🎨 Canvas scaled to ${optimalScale}x (${this.canvas.width}x${this.canvas.height}px internal, fits container)`); } /** @@ -239,7 +258,8 @@ export default class PersonChatPortraits { // Get the source image const source = frame.source.image; - // Calculate scaling to fill canvas while maintaining aspect ratio + // Calculate scaling to fit sprite within canvas while maintaining aspect ratio + // Use Math.min to ensure full sprite is visible (contain style, not cover) const spriteWidth = frame.cutWidth; const spriteHeight = frame.cutHeight; const canvasWidth = this.canvas.width; @@ -247,7 +267,7 @@ export default class PersonChatPortraits { let scaleX = canvasWidth / spriteWidth; let scaleY = canvasHeight / spriteHeight; - let scale = Math.max(scaleX, scaleY); // Fit cover style + let scale = Math.min(scaleX, scaleY); // Fit contain style - ensures full sprite visible // Calculate position to center the sprite const scaledWidth = spriteWidth * scale; @@ -337,10 +357,11 @@ export default class PersonChatPortraits { const imgWidth = img.width; const imgHeight = img.height; - // Calculate scaling to fill canvas while maintaining aspect ratio + // Calculate scaling to fit image within canvas while maintaining aspect ratio + // Use Math.min to ensure full sprite is visible (contain style, not cover) let scaleX = canvasWidth / imgWidth; let scaleY = canvasHeight / imgHeight; - let scale = Math.max(scaleX, scaleY); // Fit cover style + let scale = Math.min(scaleX, scaleY); // Fit contain style - ensures full sprite visible // Calculate position to center the image const scaledWidth = imgWidth * scale;