From 9044ac7ae43a02fbd8a217c5d64513dc0515403d Mon Sep 17 00:00:00 2001 From: "Z. Cliffe Schreuders" Date: Fri, 14 Nov 2025 10:15:44 +0000 Subject: [PATCH] Implement minigame canvas visibility management and conversation controls - Added functionality to hide the main game canvas and inventory during minigames based on scenario settings. - Enhanced the MinigameFramework to track and manage visibility states for the game canvas and inventory. - Updated PersonChatMinigame to conditionally hide close and cancel buttons based on the new `canEscConversation` parameter. - Modified NPCManager to pass new parameters for minigame initialization, including visibility and conversation settings. - Updated scenario configuration to support hiding the game during minigames and disabling escape key functionality. --- docs/CUTSCENE_IMPROVEMENTS.md | 214 ++++++++++++++++++ js/core/game.js | 15 ++ js/minigames/framework/minigame-manager.js | 34 +++ .../person-chat/person-chat-minigame.js | 27 +++ js/systems/npc-manager.js | 11 +- scenarios/npc-sprite-test2.json | 4 +- 6 files changed, 301 insertions(+), 4 deletions(-) create mode 100644 docs/CUTSCENE_IMPROVEMENTS.md diff --git a/docs/CUTSCENE_IMPROVEMENTS.md b/docs/CUTSCENE_IMPROVEMENTS.md new file mode 100644 index 0000000..4689c0c --- /dev/null +++ b/docs/CUTSCENE_IMPROVEMENTS.md @@ -0,0 +1,214 @@ +# Cut-Scene Improvements: hideGameDuringMinigame and canEscConversation + +## Overview + +Two new features have been added to BreakEscape to improve cut-scene presentation and player experience: + +1. **hideGameDuringMinigame** - Hide the main game canvas while minigames (like person-chat conversations) are running +2. **canEscConversation** - Control whether players can press Esc to exit NPC conversations + +These features are particularly useful for creating immersive opening cut-scenes that trigger at game start (delay: 0ms). + +## Feature 1: hideGameDuringMinigame + +### Problem +When a timed conversation is triggered with `delay: 0`, it starts immediately after the NPC is loaded. However, the main game is briefly visible before the person-chat minigame launches, breaking the immersion of a cut-scene. + +### Solution +Set `hideGameDuringMinigame: true` in the scenario JSON to hide the main game canvas during minigames and show it again when the minigame exits. + +### Usage + +**Scenario-level (applies to all minigames):** +```json +{ + "scenario_brief": "My scenario", + "startRoom": "intro_room", + "hideGameDuringMinigame": true +} +``` + +**Minigame-level (overrides scenario setting for specific minigame):** +```javascript +window.MinigameFramework.startMinigame('person-chat', null, { + npcId: 'director', + hideGameDuringMinigame: true +}); +``` + +### How It Works +1. **At startup:** If `hideGameDuringMinigame: true` is set on the scenario: + - Canvas is hidden (`display: none`) in `game.js` create() BEFORE first room displays + - Inventory container is also hidden (prevents UI from appearing during cut-scene) +2. **During minigames:** Both canvas and inventory container are hidden before minigame starts +3. **On exit:** When the minigame exits: + - Canvas is shown again (`display: block`) + - Inventory container is shown again +4. **Game state:** Game input remains disabled during the minigame to prevent player interaction; the game continues updating in the background + +### Timing Details +The canvas AND inventory are hidden VERY early in game initialization: +- After scenario is loaded ✅ +- After scenario validation ✅ +- **BEFORE first room is created** ✅ +- **BEFORE inventory renders** ✅ +- **BEFORE any visuals render** ✅ + +This ensures zero flash of the main game or UI elements - players see a completely blank page until the minigame launches and fills the screen. + +## Feature 2: canEscConversation + +### Problem +For critical cut-scenes or story moments, you may want to prevent players from casually pressing Esc to exit the conversation. Some scenes should be mandatory viewing. + +### Solution +Set `canEscConversation: false` on the NPC to disable the Escape key during that NPC's conversations AND hide the close button (×). + +### Usage + +**NPC-level setting:** +```json +{ + "id": "director", + "displayName": "Mission Director", + "npcType": "person", + "canEscConversation": false, + "storyPath": "scenarios/ink/director.json", + "timedConversation": { + "delay": 0, + "targetKnot": "mission_briefing" + } +} +``` + +**Default behavior:** +If not specified, `canEscConversation` defaults to `true` (players can press Esc and see the close button). + +### How It Works +1. PersonChatMinigame checks the `canEscConversation` setting in `init()` +2. If `false`, the minigame is configured with `showCancel: false` to hide the close button (×) +3. The fallback Escape handler from the base MinigameScene is removed in `start()` +4. Players cannot press Esc to close +5. Players cannot click a close button (it's not shown) +6. Conversation can only be exited by completing the dialogue naturally +7. This creates a truly "locked" cut-scene that must be viewed to completion + +## Complete Example: Opening Cut-Scene + +```json +{ + "scenario_brief": "Corporate Espionage Mission", + "startRoom": "safe_house", + "hideGameDuringMinigame": true, + + "rooms": { + "safe_house": { + "type": "room_office", + "npcs": [ + { + "id": "handler", + "displayName": "Handler", + "npcType": "person", + "position": { "x": 5, "y": 5 }, + "spriteSheet": "hacker", + "storyPath": "scenarios/ink/handler.json", + "canEscConversation": false, + "currentKnot": "start", + "timedConversation": { + "delay": 0, + "targetKnot": "mission_briefing", + "background": "assets/backgrounds/briefing_room.png" + } + } + ] + } + } +} +``` + +In this setup: +1. ✅ Game scenario loads +2. ✅ Main game canvas is hidden BEFORE first room displays (hideGameDuringMinigame: true) +3. ✅ Handler NPC is loaded +4. ✅ Timed conversation triggers immediately (delay: 0) +5. ✅ Person-chat minigame shows mission briefing at "mission_briefing" knot +6. ✅ Player cannot press Esc to skip (canEscConversation: false) +7. ✅ Close button (×) is hidden (canEscConversation: false) +8. ✅ Conversation must be completed naturally - no escape routes +9. ✅ Once conversation ends, canvas is shown again and game is playable + +## Combining with Other Minigames + +These features work with any minigame type: +- `person-chat` (NPC conversations) +- `notes` (mission briefs, readable documents) +- `container` (equipment/inventory management) +- Custom minigames that extend MinigameScene + +Example with mission brief: +```json +{ + "id": "briefing_doc", + "type": "notes", + "name": "Mission Brief", + "hideGameDuringMinigame": true, + "readable": true, + "text": "Your mission objectives are..." +} +``` + +## Technical Details + +### Implementation Details +- Modified `js/core/game.js` - Hides canvas at startup if `hideGameDuringMinigame: true` +- Modified `js/minigames/framework/minigame-manager.js` - Hides/shows canvas during minigames +- Updated `js/minigames/person-chat/person-chat-minigame.js` - Hides buttons and Esc key when `canEscConversation: false` +- Modified `js/systems/npc-manager.js` - Passes flags during timed conversation startup + +### Canvas & Inventory Manipulation +- **Early hiding:** In `game.js` create() function, both canvas and inventory container are hidden BEFORE room creation +- **Runtime hiding:** Canvas via `this.mainGameScene.game.canvas`, inventory via `document.getElementById('inventory-container')` +- **Visibility control:** Inline CSS: `element.style.display = 'none' | 'block'` +- **Game state:** Preserves game state; the Phaser game continues updating in the background +- **Clean UI:** No game elements visible while minigame is active + +### Escape Key & Button Handling +- **Escape key:** Base MinigameScene sets fallback handler in `start()` method +- **Esc disabling:** PersonChatMinigame removes handler in its `start()` when `canEscConversation: false` +- **Close button (×):** Hidden via `closeBtn.style.display = 'none'` in PersonChatMinigame `init()` +- **Cancel button:** Hidden via `showCancel: false` parameter passed to base class +- **Result:** Creates completely "locked" cut-scene with no escape routes + +## Testing + +To test these features: + +1. Load the test scenario: `scenarios/npc-sprite-test2.json` +2. Observe that: + - Game canvas is hidden when person-chat minigame opens + - Esc key does not work for the "Back NPC" (test_npc_back) + - Close button (×) still works + - Canvas reappears when conversation ends + +## Browser Compatibility + +These features use standard DOM APIs and CSS: +- `HTMLElement.style.display` - Widely supported +- `EventTarget.removeEventListener()` - Widely supported +- No polyfills required for modern browsers + +## Performance Considerations + +- Hiding the canvas (`display: none`) keeps the game running in the background +- The Phaser game continues to update, which maintains game state +- No memory overhead - just DOM style manipulation +- Ideal for scenarios with multiple cut-scenes + +## Future Enhancements + +Possible extensions to these features: +- `pauseGameDuringMinigame` - Pause Phaser update loop during minigames +- `hideUIElementsDuringMinigame` - Hide HUD elements like inventory +- `canClickExitDuringMinigame` - Control close button visibility +- `minigameOpacity` - Add fade-in/fade-out effects + diff --git a/js/core/game.js b/js/core/game.js index fdfb870..8675a4f 100644 --- a/js/core/game.js +++ b/js/core/game.js @@ -458,6 +458,21 @@ export async function create() { return; } + // Check if we need to hide the game canvas for cut-scenes/minigames + // This must be done BEFORE the first room is displayed + if (gameScenario.hideGameDuringMinigame) { + if (this.game && this.game.canvas) { + this.game.canvas.style.display = 'none'; + console.log('🎮 Hidden main game canvas at startup (hideGameDuringMinigame: true)'); + } + // Also hide the inventory container + const inventoryContainer = document.getElementById('inventory-container'); + if (inventoryContainer) { + inventoryContainer.style.display = 'none'; + console.log('🎮 Hidden inventory container at startup'); + } + } + // Initialize global narrative variables from scenario if (gameScenario.globalVariables) { window.gameState.globalVariables = { ...gameScenario.globalVariables }; diff --git a/js/minigames/framework/minigame-manager.js b/js/minigames/framework/minigame-manager.js index 0bf4280..034603e 100644 --- a/js/minigames/framework/minigame-manager.js +++ b/js/minigames/framework/minigame-manager.js @@ -6,6 +6,7 @@ export const MinigameFramework = { currentMinigame: null, registeredScenes: {}, MinigameScene: MinigameScene, // Export the base class + gameHiddenDuringMinigame: false, // Track if game was hidden init(gameScene) { this.mainGameScene = gameScene; @@ -49,6 +50,23 @@ export const MinigameFramework = { } } + // Check if main game should be hidden during this minigame + const hideGameDuringMinigame = params?.hideGameDuringMinigame || window.gameScenario?.hideGameDuringMinigame || false; + if (hideGameDuringMinigame && this.mainGameScene && this.mainGameScene.game) { + const canvas = this.mainGameScene.game.canvas; + if (canvas) { + canvas.style.display = 'none'; + this.gameHiddenDuringMinigame = true; + console.log('🎮 Hidden main game canvas during minigame'); + } + // Also hide the inventory container + const inventoryContainer = document.getElementById('inventory-container'); + if (inventoryContainer) { + inventoryContainer.style.display = 'none'; + console.log('🎮 Hidden inventory container during minigame'); + } + } + // Disable main game input if we have a main game scene // (unless the minigame explicitly allows game input via disableGameInput: false) if (this.mainGameScene && this.mainGameScene.input) { @@ -122,6 +140,22 @@ export const MinigameFramework = { }); } + // Show main game canvas again if it was hidden + if (this.gameHiddenDuringMinigame && this.mainGameScene && this.mainGameScene.game) { + const canvas = this.mainGameScene.game.canvas; + if (canvas) { + canvas.style.display = 'block'; + this.gameHiddenDuringMinigame = false; + console.log('🎮 Showed main game canvas again after minigame'); + } + // Also show the inventory container again + const inventoryContainer = document.getElementById('inventory-container'); + if (inventoryContainer) { + inventoryContainer.style.display = 'block'; + console.log('🎮 Showed inventory container again after minigame'); + } + } + // Re-enable main game input if we have a main game scene and we disabled it if (this.mainGameScene && this.mainGameScene.input && this.gameInputDisabled) { this.mainGameScene.input.mouse.enabled = true; diff --git a/js/minigames/person-chat/person-chat-minigame.js b/js/minigames/person-chat/person-chat-minigame.js index b2695cc..6ba672f 100644 --- a/js/minigames/person-chat/person-chat-minigame.js +++ b/js/minigames/person-chat/person-chat-minigame.js @@ -51,6 +51,7 @@ export class PersonChatMinigame extends MinigameScene { this.npcId = params.npcId; this.title = params.title || 'Conversation'; this.background = params.background; // Optional background image path from timedConversation + this.canEscConversation = params.canEscConversation !== false; // Allow Esc by default, can be disabled // Verify NPC exists const npc = this.npcManager.getNPC(this.npcId); @@ -150,8 +151,24 @@ export class PersonChatMinigame extends MinigameScene { if (!this.params.cancelText) { this.params.cancelText = 'End Conversation'; } + + // If canEscConversation is false, hide the close button and cancel button + if (!this.canEscConversation) { + this.params.showCancel = false; + console.log('🎭 Close/cancel buttons hidden because canEscConversation is false'); + } + super.init(); + // If canEscConversation is false, also hide the close (×) button in header + if (!this.canEscConversation) { + const closeBtn = this.container.querySelector('.minigame-close-button'); + if (closeBtn) { + closeBtn.style.display = 'none'; + console.log('🎭 Hidden minigame close button (×)'); + } + } + // Initialize timer for auto-advance this.autoAdvanceTimer = null; @@ -281,6 +298,16 @@ export class PersonChatMinigame extends MinigameScene { console.log('🎭 PersonChatMinigame started'); + // Handle canEscConversation setting + if (!this.canEscConversation) { + // Remove the fallback Escape handler set by base class if Esc is not allowed + if (this._fallbackCloseHandler) { + document.removeEventListener('keydown', this._fallbackCloseHandler); + this._fallbackCloseHandler = null; + console.log('🎭 Disabled Escape key for conversation (canEscConversation: false)'); + } + } + // Track NPC context for tag processing and minigame return flow window.currentConversationNPCId = this.npcId; window.currentConversationMinigameType = 'person-chat'; diff --git a/js/systems/npc-manager.js b/js/systems/npc-manager.js index d15fb44..4a0fee7 100644 --- a/js/systems/npc-manager.js +++ b/js/systems/npc-manager.js @@ -704,11 +704,16 @@ export default class NPCManager { if (window.MinigameFramework && typeof window.MinigameFramework.startMinigame === 'function') { console.log(`🎭 Starting timed conversation for ${conversation.npcId} at knot: ${conversation.targetKnot}`); - window.MinigameFramework.startMinigame('person-chat', null, { + // Build minigame params with optional NPC-specific settings + const minigameParams = { npcId: conversation.npcId, title: npc.displayName || conversation.npcId, - background: conversation.background // Optional background image path - }); + background: conversation.background, // Optional background image path + canEscConversation: npc.canEscConversation !== false, // Allow by default, disable if set to false + hideGameDuringMinigame: npc.hideGameDuringMinigame !== undefined ? npc.hideGameDuringMinigame : window.gameScenario?.hideGameDuringMinigame + }; + + window.MinigameFramework.startMinigame('person-chat', null, minigameParams); } else { console.warn(`[NPCManager] MinigameFramework not available to start person-chat for timed conversation`); } diff --git a/scenarios/npc-sprite-test2.json b/scenarios/npc-sprite-test2.json index 6df2f87..6c75e5a 100644 --- a/scenarios/npc-sprite-test2.json +++ b/scenarios/npc-sprite-test2.json @@ -4,6 +4,7 @@ "player_joined_organization": false }, "startRoom": "test_room", + "hideGameDuringMinigame": true, "startItemsInInventory": [ { @@ -96,9 +97,10 @@ "idleFrameEnd": 23 }, "storyPath": "scenarios/ink/test2.json", + "canEscConversation": false, "currentKnot": "hub", "timedConversation": { - "delay": 100, + "delay": 0, "targetKnot": "group_meeting", "background": "assets/backgrounds/hq1.png" }