From 5a752d0aca74b0897e822970c4a5a69a758adfe3 Mon Sep 17 00:00:00 2001 From: "Z. Cliffe Schreuders" Date: Thu, 6 Nov 2025 10:43:43 +0000 Subject: [PATCH] feat(ui): Refactor dialogue and controls layout for improved interaction and visibility --- css/person-chat-minigame.css | 50 ++++++++---- .../person-chat/person-chat-minigame.js | 78 +++++++++++++------ js/minigames/person-chat/person-chat-ui.js | 53 +++++++++---- 3 files changed, 132 insertions(+), 49 deletions(-) diff --git a/css/person-chat-minigame.css b/css/person-chat-minigame.css index 093f7f3..0fdf3f5 100644 --- a/css/person-chat-minigame.css +++ b/css/person-chat-minigame.css @@ -92,22 +92,41 @@ width: 100%; background: linear-gradient(to bottom, rgba(0,0,0,0), rgba(0,0,0,0.95)); display: flex; - flex-direction: column; - justify-content: flex-end; - align-items: stretch; + flex-direction: row; + justify-content: flex-start; + align-items: flex-end; padding: 20px; - gap: 15px; + gap: 20px; z-index: 10; box-sizing: border-box; } +/* Talk right area - speaker name and dialogue (left side, takes more space) */ +.person-chat-talk-right { + display: flex; + flex-direction: column; + gap: 10px; + flex: 1 1 auto; + min-width: 0; +} + +/* Header row: speaker name on left, controls on right */ +.person-chat-header-row { + display: flex; + justify-content: space-between; + align-items: center; + gap: 20px; + width: 100%; + padding-bottom: 8px; + border-bottom: 2px solid #333; +} + /* Speaker name */ .person-chat-speaker-name { font-size: 20px; font-weight: bold; - padding-bottom: 8px; - border-bottom: 2px solid #333; min-height: 20px; + flex: 0 0 auto; } .person-chat-speaker-name.npc-speaker { @@ -126,9 +145,8 @@ min-height: auto; max-height: none; overflow: visible; - display: flex; - align-items: flex-start; - flex: 0 0 auto; + display: block; + width: 100%; } .person-chat-dialogue-text { @@ -141,15 +159,17 @@ text-shadow: 2px 2px 4px rgba(0,0,0,0.8); } -/* Choices and continue button area */ +/* Choices and continue button area (right side, fixed width) */ .person-chat-controls-area { display: flex; flex-direction: column; - gap: 10px; - flex: 0 0 280px; + gap: 8px; + flex: 0 0 auto; + align-items: stretch; + min-width: 200px; } -/* Choices container - displayed below dialogue in caption area */ +/* Choices container - displayed in controls area */ .person-chat-choices-container { display: flex; flex-direction: column; @@ -168,6 +188,10 @@ cursor: pointer; text-align: left; transition: all 0.1s ease; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + min-width: 180px; } .person-chat-choice-button:hover { diff --git a/js/minigames/person-chat/person-chat-minigame.js b/js/minigames/person-chat/person-chat-minigame.js index da2d5d7..10c4763 100644 --- a/js/minigames/person-chat/person-chat-minigame.js +++ b/js/minigames/person-chat/person-chat-minigame.js @@ -72,7 +72,7 @@ export class PersonChatMinigame extends MinigameScene { this.isConversationActive = false; this.currentSpeaker = null; // Track current speaker ID ('player' or NPC id) this.lastResult = null; // Store last continue() result for choice handling - this.isClickThroughMode = false; // If true, player must click to advance between dialogue lines + this.isClickThroughMode = false; // If true, player must click to advance between dialogue lines (starts in AUTO mode) this.pendingContinueCallback = null; // Callback waiting for player click in click-through mode console.log(`🎭 PersonChatMinigame created for NPC: ${this.npcId}`); @@ -134,6 +134,9 @@ export class PersonChatMinigame extends MinigameScene { } super.init(); + // Initialize timer for auto-advance + this.autoAdvanceTimer = null; + // Customize header this.headerElement.innerHTML = `

🎭 ${this.title}

@@ -170,17 +173,37 @@ export class PersonChatMinigame extends MinigameScene { } }); - // Continue button to toggle click-through mode + // Continue button click handler if (this.ui.elements.continueButton) { this.addEventListener(this.ui.elements.continueButton, 'click', (e) => { e.preventDefault(); e.stopPropagation(); - this.toggleClickThroughMode(); + this.handleContinueButtonClick(); }); } } /** + * Handle continue button click - behavior depends on mode + */ + handleContinueButtonClick() { + console.log(`🖱️ Button clicked! isClickThroughMode: ${this.isClickThroughMode}, pendingContinueCallback exists: ${!!this.pendingContinueCallback}`); + if (this.isClickThroughMode) { + // In click-through mode: execute the pending advancement callback + if (this.pendingContinueCallback && typeof this.pendingContinueCallback === 'function') { + console.log(`🖱️ Executing pending callback`); + const callback = this.pendingContinueCallback; + this.pendingContinueCallback = null; + callback(); + } else { + console.log(`🖱️ No pending callback to execute!`); + } + } else { + // In auto mode: toggle to click-through mode + console.log(`🖱️ In auto mode, toggling to click-through`); + this.toggleClickThroughMode(); + } + } /** * Toggle between automatic timing and click-through mode */ toggleClickThroughMode() { @@ -188,15 +211,17 @@ export class PersonChatMinigame extends MinigameScene { if (this.isClickThroughMode) { console.log('📋 Switched to CLICK-THROUGH mode'); - this.ui.elements.continueButton.textContent = '■ Auto'; - // Cancel any pending automatic advances - if (this.pendingContinueCallback) { - clearTimeout(this.pendingContinueCallback); - this.pendingContinueCallback = null; + this.ui.elements.continueButton.textContent = 'Continue'; + // Cancel any pending automatic advances (timer only, keep the callback!) + if (this.autoAdvanceTimer) { + clearTimeout(this.autoAdvanceTimer); + this.autoAdvanceTimer = null; } } else { console.log('📋 Switched to AUTOMATIC mode'); - this.ui.elements.continueButton.textContent = '▼ Continue'; + this.ui.elements.continueButton.textContent = 'Auto'; + // Resume automatic advancement + this.showCurrentDialogue(); } } @@ -206,20 +231,25 @@ export class PersonChatMinigame extends MinigameScene { * @param {number} delay - Delay in milliseconds (ignored in click-through mode) */ scheduleDialogueAdvance(callback, delay = 2000) { + // Always store the callback function itself + this.pendingContinueCallback = callback; + if (this.isClickThroughMode) { - // In click-through mode, show continue button and wait for user click - this.pendingContinueCallback = callback; - this.ui.showContinueButton(() => { - if (this.pendingContinueCallback) { - const pending = this.pendingContinueCallback; - this.pendingContinueCallback = null; - pending(); - } - }); + // In click-through mode, wait for button click to execute callback + console.log(`⏱️ scheduleDialogueAdvance: Stored callback for click-through mode`); } else { - // In automatic mode, use the delay - this.ui.hideContinueButton(); - this.pendingContinueCallback = setTimeout(callback, delay); + // In automatic mode, schedule execution after delay + console.log(`⏱️ scheduleDialogueAdvance: Will auto-advance after ${delay}ms`); + // Clear any existing timeout + if (this.autoAdvanceTimer) { + clearTimeout(this.autoAdvanceTimer); + } + // Set new timeout that will call handleContinueButtonClick + this.autoAdvanceTimer = setTimeout(() => { + if (this.pendingContinueCallback && typeof this.pendingContinueCallback === 'function') { + this.pendingContinueCallback(); + } + }, delay); } } @@ -325,11 +355,12 @@ export class PersonChatMinigame extends MinigameScene { // At a choice point - display choices this.ui.showChoices(result.choices); console.log(`📋 ${result.choices.length} choices available`); + console.log(`📋 pendingContinueCallback NOT set - waiting for choice selection`); // Also display any accompanying text if present if (result.text && result.text.trim()) { console.log(`🗣️ Calling showDialogue with speaker: ${speaker}`); - this.ui.showDialogue(result.text, speaker); + this.ui.showDialogue(result.text, speaker, true); // preserveChoices=true } } else if (result.text && result.text.trim()) { // Have text but no choices - display and continue @@ -338,6 +369,7 @@ 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); } else { // Can't continue but have text - story will end @@ -463,8 +495,10 @@ export class PersonChatMinigame extends MinigameScene { } // Normal dialogue flow: display the result (dialogue blocks) after a small delay + console.log(`🎯 After choice: scheduling displayAccumulatedDialogue with result.text length: ${result?.text?.length || 0}`); this.scheduleDialogueAdvance(() => { // Process accumulated dialogue by splitting into individual speaker blocks + console.log(`🎯 Inside scheduled callback: calling displayAccumulatedDialogue`); this.displayAccumulatedDialogue(result); }, 1500); diff --git a/js/minigames/person-chat/person-chat-ui.js b/js/minigames/person-chat/person-chat-ui.js index 3d7f3ac..89da3bf 100644 --- a/js/minigames/person-chat/person-chat-ui.js +++ b/js/minigames/person-chat/person-chat-ui.js @@ -107,9 +107,14 @@ export default class PersonChatUI { const captionArea = document.createElement('div'); captionArea.className = 'person-chat-caption-area'; + // Talk right area - speaker name + dialogue + const talkRightArea = document.createElement('div'); + talkRightArea.className = 'person-chat-talk-right'; + const speakerName = document.createElement('div'); speakerName.className = 'person-chat-speaker-name'; + // Dialogue box (spans full width below header) const dialogueBox = document.createElement('div'); dialogueBox.className = 'person-chat-dialogue-box'; @@ -119,25 +124,34 @@ export default class PersonChatUI { dialogueBox.appendChild(dialogueText); - // Continue button - placed next to dialogue + // Assemble talk-right area + talkRightArea.appendChild(speakerName); + talkRightArea.appendChild(dialogueBox); + + // Controls area - continue button + choices + const controlsArea = document.createElement('div'); + controlsArea.className = 'person-chat-controls-area'; + + // Continue button const continueButton = document.createElement('button'); continueButton.className = 'person-chat-continue-button'; - continueButton.textContent = '▼ Continue'; + continueButton.textContent = 'Auto'; continueButton.id = 'continue-button'; - continueButton.style.display = 'none'; // Hidden by default, shown when there's more dialogue + continueButton.style.display = 'inline-block'; // Always visible (hidden only when choices shown) - dialogueBox.appendChild(continueButton); + controlsArea.appendChild(continueButton); - // Choices container (in caption area, below dialogue) + // Choices container (in controls area, below continue button) const choicesContainer = document.createElement('div'); choicesContainer.className = 'person-chat-choices-container'; choicesContainer.id = 'choices-container'; choicesContainer.style.display = 'none'; - // Assemble caption area: speaker name, dialogue, choices - captionArea.appendChild(speakerName); - captionArea.appendChild(dialogueBox); - captionArea.appendChild(choicesContainer); + controlsArea.appendChild(choicesContainer); + + // Assemble caption area: talk-right, controls + captionArea.appendChild(talkRightArea); + captionArea.appendChild(controlsArea); // Assemble main content mainContent.appendChild(portraitSection); @@ -148,9 +162,11 @@ export default class PersonChatUI { this.elements.portraitContainer = portraitContainer; this.elements.portraitLabel = portraitLabel; this.elements.captionArea = captionArea; + this.elements.talkRightArea = talkRightArea; this.elements.speakerName = speakerName; this.elements.dialogueBox = dialogueBox; this.elements.dialogueText = dialogueText; + this.elements.controlsArea = controlsArea; this.elements.continueButton = continueButton; this.elements.choicesContainer = choicesContainer; @@ -185,8 +201,9 @@ export default class PersonChatUI { * Display dialogue text with speaker * @param {string} text - Dialogue text to display * @param {string} characterId - Character ID ('player', 'npc', or specific NPC ID) + * @param {boolean} preserveChoices - If true, don't hide existing choices */ - showDialogue(text, characterId = 'npc') { + showDialogue(text, characterId = 'npc', preserveChoices = false) { this.currentSpeaker = characterId; console.log(`📝 showDialogue called with character: ${characterId}, text length: ${text?.length || 0}`); @@ -226,6 +243,11 @@ export default class PersonChatUI { // Reset portrait for new speaker this.updatePortraitForSpeaker(characterId, character); + // Hide choices only if not preserving them (i.e., when transitioning from choices back to text) + if (!preserveChoices) { + this.hideChoices(); + } + // Reset continue button state this.hasContinued = false; } @@ -268,7 +290,7 @@ export default class PersonChatUI { * @param {Array} choices - Array of choice objects {text, index} */ showChoices(choices) { - if (!this.elements.choicesContainer) { + if (!this.elements.choicesContainer || !this.elements.continueButton) { return; } @@ -277,10 +299,12 @@ export default class PersonChatUI { if (!choices || choices.length === 0) { this.elements.choicesContainer.style.display = 'none'; + this.elements.continueButton.style.display = 'inline-block'; return; } - // Show choices container + // Hide continue button and show choices + this.elements.continueButton.style.display = 'none'; this.elements.choicesContainer.style.display = 'flex'; // Create button for each choice @@ -297,12 +321,13 @@ export default class PersonChatUI { } /** - * Hide choices + * Hide choices and restore continue button */ hideChoices() { - if (this.elements.choicesContainer) { + if (this.elements.choicesContainer && this.elements.continueButton) { this.elements.choicesContainer.innerHTML = ''; this.elements.choicesContainer.style.display = 'none'; + this.elements.continueButton.style.display = 'inline-block'; } }