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.
This commit is contained in:
Z. Cliffe Schreuders
2025-11-07 20:33:54 +00:00
parent 6f69ab52c1
commit d36b61f20e
4 changed files with 137 additions and 21 deletions

View File

@@ -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) */

View File

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

View File

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

View File

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