From d36b61f20e70b8f67bacf3d33fb2f8cc7b62f18e Mon Sep 17 00:00:00 2001 From: "Z. Cliffe Schreuders" Date: Fri, 7 Nov 2025 20:33:54 +0000 Subject: [PATCH] Enhance PersonChatMinigame and UI with improved caption area and parallax animation - Updated CSS for the caption area to span the full width of the screen, enhancing visual consistency. - Introduced a new inner container for caption content to maintain a maximum width, improving layout structure. - Added parallax animation functionality in PersonChatPortraits for a more dynamic visual experience during conversations. - Implemented automatic parallax animation reset when the speaker changes, ensuring smooth transitions between dialogues. --- css/person-chat-minigame.css | 24 +++-- .../person-chat/person-chat-minigame.js | 20 ++-- .../person-chat/person-chat-portraits.js | 95 ++++++++++++++++++- js/minigames/person-chat/person-chat-ui.js | 19 +++- 4 files changed, 137 insertions(+), 21 deletions(-) diff --git a/css/person-chat-minigame.css b/css/person-chat-minigame.css index bc4e75a..ccb8f86 100644 --- a/css/person-chat-minigame.css +++ b/css/person-chat-minigame.css @@ -82,26 +82,34 @@ background-color: #000; } -/* Caption area - positioned at bottom 1/3 of screen */ +/* Caption area - positioned at bottom 1/3 of screen, full width background */ .person-chat-caption-area { position: absolute; bottom: 0; - left: 50%; - transform: translateX(-50%); - max-width: 1200px; - width: calc(100% - 40px); + left: 0; + right: 0; + width: 100%; height: 33%; background: linear-gradient(to bottom, rgba(0,0,0,0), rgba(0,0,0,0.95)); + z-index: 10; + box-sizing: border-box; + display: flex; + justify-content: center; + align-items: flex-end; + padding: 20px; +} + +/* Inner container for caption content - constrained to max-width */ +.person-chat-caption-content { + max-width: 1200px; + width: 100%; display: flex; flex-direction: row; flex-wrap: wrap; justify-content: space-between; align-items: flex-end; align-content: flex-end; - padding: 20px; gap: 20px; - z-index: 10; - box-sizing: border-box; } /* Talk right area - speaker name and dialogue (left side, takes more space) */ diff --git a/js/minigames/person-chat/person-chat-minigame.js b/js/minigames/person-chat/person-chat-minigame.js index 250a40a..210e532 100644 --- a/js/minigames/person-chat/person-chat-minigame.js +++ b/js/minigames/person-chat/person-chat-minigame.js @@ -19,6 +19,10 @@ import InkEngine from '../../systems/ink/ink-engine.js?v=1'; import { processGameActionTags, determineSpeaker as determineSpeakerFromTags } from '../helpers/chat-helpers.js'; import npcConversationStateManager from '../../systems/npc-conversation-state.js?v=2'; +// Configuration constants for dialogue auto-advance timing +const DIALOGUE_AUTO_ADVANCE_DELAY = 5000; // Default delay in milliseconds for new dialogue text (5 seconds) +const DIALOGUE_END_DELAY = 1000; // Delay in milliseconds for ending conversations (1 second) + export class PersonChatMinigame extends MinigameScene { /** * Create a PersonChatMinigame instance @@ -245,7 +249,7 @@ export class PersonChatMinigame extends MinigameScene { * @param {Function} callback - Function to call to advance dialogue * @param {number} delay - Delay in milliseconds (ignored in click-through mode) */ - scheduleDialogueAdvance(callback, delay = 2000) { + scheduleDialogueAdvance(callback, delay = DIALOGUE_AUTO_ADVANCE_DELAY) { // Always store the callback function itself this.pendingContinueCallback = callback; @@ -398,11 +402,11 @@ export class PersonChatMinigame extends MinigameScene { if (result.canContinue) { // Can continue - schedule next advance console.log(`📋 Setting pendingContinueCallback - canContinue: true, no choices`); - this.scheduleDialogueAdvance(() => this.showCurrentDialogue(), 2000); + this.scheduleDialogueAdvance(() => this.showCurrentDialogue(), DIALOGUE_AUTO_ADVANCE_DELAY); } else { // Can't continue but have text - story will end console.log('✓ Waiting for story to end...'); - this.scheduleDialogueAdvance(() => this.endConversation(), 1000); + this.scheduleDialogueAdvance(() => this.endConversation(), DIALOGUE_END_DELAY); } } else { // No text and no choices - story has ended @@ -699,7 +703,7 @@ export class PersonChatMinigame extends MinigameScene { this.ui.showDialogue('(Conversation ended - press ESC to close)', 'system'); console.log('🏁 Story has reached an end point'); } - }, 2000); + }, DIALOGUE_AUTO_ADVANCE_DELAY); } return; } @@ -726,7 +730,7 @@ export class PersonChatMinigame extends MinigameScene { // Display next line after delay this.scheduleDialogueAdvance(() => { this.displayDialogueBlocksSequentially(blocks, originalResult, blockIndex, lineIndex + 1, newAccumulatedText); - }, 2000); + }, DIALOGUE_AUTO_ADVANCE_DELAY); } /** @@ -774,8 +778,8 @@ export class PersonChatMinigame extends MinigameScene { console.log(`📋 ${result.choices.length} choices available`); } else if (result.canContinue) { // No choices but can continue - auto-advance after delay - console.log('⏳ Auto-continuing in 2 seconds...'); - this.scheduleDialogueAdvance(() => this.showCurrentDialogue(), 2000); + console.log(`⏳ Auto-continuing in ${DIALOGUE_AUTO_ADVANCE_DELAY / 1000} seconds...`); + this.scheduleDialogueAdvance(() => this.showCurrentDialogue(), DIALOGUE_AUTO_ADVANCE_DELAY); } else { // No choices and can't continue - check if there's more content // Try to continue anyway (for linear scripted conversations) @@ -800,7 +804,7 @@ export class PersonChatMinigame extends MinigameScene { } this.ui.showDialogue('(No more dialogue available - press ESC to close)', 'system'); } - }, 2000); + }, DIALOGUE_AUTO_ADVANCE_DELAY); } } catch (error) { console.error('❌ Error displaying dialogue:', error); diff --git a/js/minigames/person-chat/person-chat-portraits.js b/js/minigames/person-chat/person-chat-portraits.js index 21af9b2..5a1858b 100644 --- a/js/minigames/person-chat/person-chat-portraits.js +++ b/js/minigames/person-chat/person-chat-portraits.js @@ -34,6 +34,8 @@ export default class PersonChatPortraits { // Background image this.backgroundImage = null; // Loaded background image + this.parallaxStartTime = Date.now(); // Track time for parallax animation + this.animationFrameId = null; // Track animation frame for cleanup // Sprite info this.spriteSheet = null; @@ -98,6 +100,8 @@ export default class PersonChatPortraits { // Handle window resize window.addEventListener('resize', () => this.handleResize()); + // Parallax animation will start automatically when background image loads + console.log(`✅ Portrait initialized for ${this.npc.id} (${this.canvas.width}x${this.canvas.height})`); return true; } catch (error) { @@ -193,6 +197,67 @@ export default class PersonChatPortraits { } } + /** + * Start parallax animation loop (stops after movement completes) + */ + startParallaxAnimation() { + if (this.animationFrameId) { + return; // Already animating + } + + const parallaxDuration = 2.0; // Duration of movement in seconds + + const animate = () => { + if (!this.canvas || !this.backgroundImage) { + this.animationFrameId = null; + return; + } + + const elapsed = (Date.now() - this.parallaxStartTime) / 1000; // Time in seconds + + // Re-render to update parallax position + // This will redraw both background (with parallax) and sprite + this.render(); + + // Continue animation until movement is complete + if (elapsed < parallaxDuration) { + this.animationFrameId = requestAnimationFrame(animate); + } else { + // Movement complete, stop animation loop + this.animationFrameId = null; + } + }; + + // Start animation loop + this.animationFrameId = requestAnimationFrame(animate); + } + + /** + * Stop parallax animation loop + */ + stopParallaxAnimation() { + if (this.animationFrameId) { + cancelAnimationFrame(this.animationFrameId); + this.animationFrameId = null; + } + } + + /** + * Reset and restart parallax animation (called when speaker changes) + */ + resetParallaxAnimation() { + // Stop current animation if running + this.stopParallaxAnimation(); + + // Reset start time to begin new animation + this.parallaxStartTime = Date.now(); + + // Restart animation if background is loaded + if (this.backgroundImage) { + this.startParallaxAnimation(); + } + } + /** * Set up sprite sheet and frame information */ @@ -247,6 +312,8 @@ export default class PersonChatPortraits { console.log(`✅ Background image loaded: ${this.backgroundPath}`); // Re-render when background loads this.render(); + // Start parallax animation now that background is loaded + this.startParallaxAnimation(); }; img.onerror = () => { @@ -311,12 +378,34 @@ export default class PersonChatPortraits { y = 0; } + // Calculate subtle parallax effect - move background towards sprite once and stop + const elapsed = (Date.now() - this.parallaxStartTime) / 1000; // Time in seconds + const parallaxDuration = 1.0; // Duration of movement in seconds + const maxParallaxAmount = 10; // Maximum parallax offset in pixels + + // Calculate parallax amount: moves from 0 to maxParallaxAmount over duration, then stops + let parallaxAmount = 0; + if (elapsed < parallaxDuration) { + // Ease-out animation: starts fast, slows down as it approaches target + const progress = elapsed / parallaxDuration; // 0 to 1 + const easedProgress = 1 - Math.pow(1 - progress, 3); // Ease-out cubic + parallaxAmount = easedProgress * maxParallaxAmount; + } else { + // Movement complete, stay at max position + parallaxAmount = maxParallaxAmount; + } + + // Move background towards sprite (towards center) + // NPC on right: move left (negative), Player on left: move right (positive) + const parallaxOffset = this.flipped ? parallaxAmount : -parallaxAmount; + x += parallaxOffset; + // Draw background image at same pixel scale as sprite // Note: Canvas will clip anything outside its bounds, but background may extend beyond this.ctx.imageSmoothingEnabled = false; // Pixel-perfect rendering this.ctx.drawImage( this.backgroundImage, - x, y, // Destination position + x, y, // Destination position (with parallax offset) scaledWidth, scaledHeight // Destination size (scaled to match sprite scale exactly) ); } @@ -577,7 +666,9 @@ export default class PersonChatPortraits { * Destroy portrait and cleanup */ destroy() { - // No timers to clear in this version + // Stop parallax animation + this.stopParallaxAnimation(); + if (this.canvas && this.canvas.parentNode) { this.canvas.parentNode.removeChild(this.canvas); } diff --git a/js/minigames/person-chat/person-chat-ui.js b/js/minigames/person-chat/person-chat-ui.js index ed83854..dea3f26 100644 --- a/js/minigames/person-chat/person-chat-ui.js +++ b/js/minigames/person-chat/person-chat-ui.js @@ -108,6 +108,10 @@ export default class PersonChatUI { const captionArea = document.createElement('div'); captionArea.className = 'person-chat-caption-area'; + // Inner content wrapper - constrained to max-width + const captionContent = document.createElement('div'); + captionContent.className = 'person-chat-caption-content'; + // Talk right area - speaker name + dialogue const talkRightArea = document.createElement('div'); talkRightArea.className = 'person-chat-talk-right'; @@ -150,9 +154,12 @@ export default class PersonChatUI { controlsArea.appendChild(choicesContainer); - // Assemble caption area: talk-right, controls - captionArea.appendChild(talkRightArea); - captionArea.appendChild(controlsArea); + // Assemble caption content: talk-right, controls + captionContent.appendChild(talkRightArea); + captionContent.appendChild(controlsArea); + + // Add content wrapper to caption area + captionArea.appendChild(captionContent); // Assemble main content mainContent.appendChild(portraitSection); @@ -281,6 +288,12 @@ export default class PersonChatUI { } this.portraitRenderer.setupSpriteInfo(); + + // Reset parallax animation for new speaker + if (this.portraitRenderer.backgroundImage) { + this.portraitRenderer.resetParallaxAnimation(); + } + this.portraitRenderer.render(); } catch (error) { console.error('❌ Error updating portrait:', error);