feat(person-chat): Implement click-through mode for dialogue advancement and enhance UI with continue button

This commit is contained in:
Z. Cliffe Schreuders
2025-11-05 09:18:58 +00:00
parent 4daaa87534
commit 78cc3beab4
7 changed files with 125 additions and 42 deletions

View File

@@ -107,7 +107,7 @@
<div class="popup-overlay"></div>
<!-- Main Game JavaScript Module -->
<script type="module" src="js/main.js?v=40"></script>
<script type="module" src="js/main.js?v=41"></script>
<!-- Mobile touch handling -->
<script>

View File

@@ -2,7 +2,7 @@ import { initializeRooms, calculateWorldBounds, calculateRoomPositions, createRo
import { createPlayer, updatePlayerMovement, movePlayerToPoint, player } from './player.js?v=7';
import { initializePathfinder } from './pathfinding.js?v=7';
import { initializeInventory, processInitialInventoryItems } from '../systems/inventory.js?v=8';
import { checkObjectInteractions, setGameInstance } from '../systems/interactions.js?v=25';
import { checkObjectInteractions, setGameInstance } from '../systems/interactions.js?v=26';
import { introduceScenario } from '../utils/helpers.js?v=19';
import '../minigames/index.js?v=2';
import SoundManager from '../systems/sound-manager.js?v=1';
@@ -602,29 +602,15 @@ export function create() {
if (npcAtPosition) {
// Try to interact with the NPC
if (window.tryInteractWithNPC) {
const player = window.player;
if (player) {
window.preventPlayerMovement = true;
const previousX = player.x;
const previousY = player.y;
// Try the interaction
window.tryInteractWithNPC(npcAtPosition);
// If the interaction didn't move the player (NPC was out of range),
// treat this as a movement request to that NPC instead
if (player.x === previousX && player.y === previousY) {
// Reset the flag and move toward the NPC
window.preventPlayerMovement = false;
movePlayerToPoint(npcAtPosition.x, npcAtPosition.y);
return;
}
// Interaction was successful
setTimeout(() => {
window.preventPlayerMovement = false;
}, 100);
const interactionSuccessful = window.tryInteractWithNPC(npcAtPosition);
if (interactionSuccessful) {
// Interaction was successful (NPC was in range)
return; // Exit early after handling the interaction
} else {
// NPC was out of range - treat click as a movement request
movePlayerToPoint(npcAtPosition.x, npcAtPosition.y);
return;
}
}
}

View File

@@ -1,5 +1,5 @@
import { GAME_CONFIG } from './utils/constants.js?v=8';
import { preload, create, update } from './core/game.js?v=39';
import { preload, create, update } from './core/game.js?v=40';
import { initializeNotifications } from './systems/notifications.js?v=7';
// Bluetooth scanner is now handled as a minigame
// Biometrics is now handled as a minigame

View File

@@ -10,7 +10,7 @@ export { BluetoothScannerMinigame, startBluetoothScannerMinigame } from './bluet
export { BiometricsMinigame, startBiometricsMinigame } from './biometrics/biometrics-minigame.js';
export { ContainerMinigame, startContainerMinigame, returnToContainerAfterNotes } from './container/container-minigame.js';
export { PhoneChatMinigame, returnToPhoneAfterNotes } from './phone-chat/phone-chat-minigame.js';
export { PersonChatMinigame } from './person-chat/person-chat-minigame.js?v=7';
export { PersonChatMinigame } from './person-chat/person-chat-minigame.js?v=8';
export { PinMinigame, startPinMinigame } from './pin/pin-minigame.js';
export { PasswordMinigame } from './password/password-minigame.js';
export { TextFileMinigame, returnToTextFileAfterNotes } from './text-file/text-file-minigame.js';
@@ -58,7 +58,7 @@ import { ContainerMinigame, startContainerMinigame, returnToContainerAfterNotes
import { PhoneChatMinigame, returnToPhoneAfterNotes } from './phone-chat/phone-chat-minigame.js';
// Import the person chat minigame (In-person NPC conversations)
import { PersonChatMinigame } from './person-chat/person-chat-minigame.js?v=2';
import { PersonChatMinigame } from './person-chat/person-chat-minigame.js?v=8';
// Import the PIN minigame
import { PinMinigame, startPinMinigame } from './pin/pin-minigame.js';

View File

@@ -72,6 +72,8 @@ 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.pendingContinueCallback = null; // Callback waiting for player click in click-through mode
console.log(`🎭 PersonChatMinigame created for NPC: ${this.npcId}`);
}
@@ -167,6 +169,58 @@ export class PersonChatMinigame extends MinigameScene {
this.handleChoice(choiceIndex);
}
});
// Continue button to toggle click-through mode
if (this.ui.elements.continueButton) {
this.addEventListener(this.ui.elements.continueButton, 'click', (e) => {
e.preventDefault();
e.stopPropagation();
this.toggleClickThroughMode();
});
}
}
/**
* Toggle between automatic timing and click-through mode
*/
toggleClickThroughMode() {
this.isClickThroughMode = !this.isClickThroughMode;
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;
}
} else {
console.log('📋 Switched to AUTOMATIC mode');
this.ui.elements.continueButton.textContent = '▼ Continue';
}
}
/**
* Schedule the next dialogue advancement, respecting click-through mode
* @param {Function} callback - Function to call to advance dialogue
* @param {number} delay - Delay in milliseconds (ignored in click-through mode)
*/
scheduleDialogueAdvance(callback, delay = 2000) {
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();
}
});
} else {
// In automatic mode, use the delay
this.ui.hideContinueButton();
this.pendingContinueCallback = setTimeout(callback, delay);
}
}
/**
@@ -277,13 +331,12 @@ export class PersonChatMinigame extends MinigameScene {
this.ui.showDialogue(result.text, speaker);
if (result.canContinue) {
// Can continue - auto-advance after delay
console.log('⏳ Auto-continuing in 2 seconds...');
setTimeout(() => this.showCurrentDialogue(), 2000);
// Can continue - schedule next advance
this.scheduleDialogueAdvance(() => this.showCurrentDialogue(), 2000);
} else {
// Can't continue but have text - story will end
console.log('✓ Waiting for story to end...');
setTimeout(() => this.endConversation(), 1000);
this.scheduleDialogueAdvance(() => this.endConversation(), 1000);
}
} else {
// No text and no choices - story has ended
@@ -369,7 +422,7 @@ export class PersonChatMinigame extends MinigameScene {
}
// Then display the result (dialogue blocks) after a small delay
setTimeout(() => {
this.scheduleDialogueAdvance(() => {
// Process accumulated dialogue by splitting into individual speaker blocks
this.displayAccumulatedDialogue(result);
}, 1500);
@@ -479,11 +532,11 @@ export class PersonChatMinigame extends MinigameScene {
if (blockIndex >= blocks.length) {
// All blocks displayed, check if story has ended
if (originalResult.hasEnded) {
setTimeout(() => this.endConversation(), 1000);
this.scheduleDialogueAdvance(() => this.endConversation(), 1000);
} else {
// Try to continue for more dialogue
console.log('⏸️ Blocks finished, checking for more dialogue...');
setTimeout(() => {
this.scheduleDialogueAdvance(() => {
const nextLine = this.conversation.continue();
// Store for choice handling
@@ -510,7 +563,7 @@ export class PersonChatMinigame extends MinigameScene {
this.ui.showDialogue(block.text, block.speaker);
// Display next block after delay
setTimeout(() => {
this.scheduleDialogueAdvance(() => {
this.displayDialogueBlocksSequentially(blocks, originalResult, blockIndex + 1);
}, 2000);
}
@@ -556,12 +609,12 @@ export class PersonChatMinigame extends MinigameScene {
} else if (result.canContinue) {
// No choices but can continue - auto-advance after delay
console.log('⏳ Auto-continuing in 2 seconds...');
setTimeout(() => this.showCurrentDialogue(), 2000);
this.scheduleDialogueAdvance(() => this.showCurrentDialogue(), 2000);
} else {
// No choices and can't continue - check if there's more content
// Try to continue anyway (for linear scripted conversations)
console.log('⏸️ No more choices, attempting to continue for next line...');
setTimeout(() => {
this.scheduleDialogueAdvance(() => {
const nextLine = this.conversation.continue();
if (nextLine.text && nextLine.text.trim()) {
// There's more dialogue to show
@@ -572,7 +625,7 @@ export class PersonChatMinigame extends MinigameScene {
} else {
// No text but story isn't ended - wait a bit and end
console.log('✓ No more dialogue - ending conversation');
setTimeout(() => this.endConversation(), 1000);
this.scheduleDialogueAdvance(() => this.endConversation(), 1000);
}
}, 2000);
}
@@ -604,7 +657,7 @@ export class PersonChatMinigame extends MinigameScene {
this.ui.reset();
// Close minigame after a delay
setTimeout(() => {
this.scheduleDialogueAdvance(() => {
this.complete(true);
}, 1000);
}

View File

@@ -119,6 +119,15 @@ export default class PersonChatUI {
dialogueBox.appendChild(dialogueText);
// Continue button - placed next to dialogue
const continueButton = document.createElement('button');
continueButton.className = 'person-chat-continue-button';
continueButton.textContent = '▼ Continue';
continueButton.id = 'continue-button';
continueButton.style.display = 'none'; // Hidden by default, shown when there's more dialogue
dialogueBox.appendChild(continueButton);
// Choices container (in caption area, below dialogue)
const choicesContainer = document.createElement('div');
choicesContainer.className = 'person-chat-choices-container';
@@ -142,6 +151,7 @@ export default class PersonChatUI {
this.elements.speakerName = speakerName;
this.elements.dialogueBox = dialogueBox;
this.elements.dialogueText = dialogueText;
this.elements.continueButton = continueButton;
this.elements.choicesContainer = choicesContainer;
this.elements.root.appendChild(mainContent);
@@ -304,6 +314,37 @@ export default class PersonChatUI {
/**
* Clear dialogue and reset UI
*/
/**
* Show the continue button to indicate player can advance
* @param {Function} onContinueClick - Callback when continue button is clicked
*/
showContinueButton(onContinueClick) {
if (!this.elements.continueButton) {
return;
}
this.elements.continueButton.style.display = 'inline-block';
// Remove any existing listeners
const newButton = this.elements.continueButton.cloneNode(true);
this.elements.continueButton.parentNode.replaceChild(newButton, this.elements.continueButton);
this.elements.continueButton = newButton;
// Add click listener
if (onContinueClick) {
this.elements.continueButton.addEventListener('click', onContinueClick);
}
}
/**
* Hide the continue button
*/
hideContinueButton() {
if (this.elements.continueButton) {
this.elements.continueButton.style.display = 'none';
}
}
reset() {
this.currentSpeaker = null;
this.hasContinued = false;

View File

@@ -962,12 +962,12 @@ export function tryInteractWithNearest() {
// Handle NPC interaction by sprite reference
export function tryInteractWithNPC(npcSprite) {
if (!npcSprite || !npcSprite._isNPC) {
return;
return false;
}
const player = window.player;
if (!player) {
return;
return false;
}
// Check if NPC is within interaction range of the player
@@ -977,8 +977,11 @@ export function tryInteractWithNPC(npcSprite) {
// Only interact if within range
if (distance <= INTERACTION_RANGE) {
handleObjectInteraction(npcSprite);
return true; // Interaction successful
}
// If out of range, the click handler will treat it as a movement request instead
// Out of range - caller should handle movement
return false;
}
// Export for global access