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.
This commit is contained in:
Z. Cliffe Schreuders
2025-11-07 17:27:35 +00:00
parent 9a8ef9b9f5
commit 717221cb3c
3 changed files with 42 additions and 308 deletions

View File

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

View File

@@ -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 = `
<h3>🎭 ${this.title}</h3>
<p>Speaking with ${this.npc.displayName}</p>
`;
// 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 = `<div class="minigame-error">${message}</div>`;
}
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);
}
}

View File

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