mirror of
https://github.com/cliffe/BreakEscape.git
synced 2026-02-21 11:18:08 +00:00
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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user