From aa53ce53eaba352b82ba1c99e9446f4109502a5b Mon Sep 17 00:00:00 2001 From: "Z. Cliffe Schreuders" Date: Mon, 13 Oct 2025 11:53:07 +0100 Subject: [PATCH] Add Container Minigame: Introduce a new minigame for interacting with container items, including features for visual display, item interaction, and integration with the unlock system. Add CSS styles and test page for functionality. Update index.html and minigame manager to support the new minigame. --- CONTAINER_MINIGAME_USAGE.md | 123 ++++++++ assets/objects/notes.png | Bin 0 -> 411 bytes css/container-minigame.css | 228 ++++++++++++++ index.html | 1 + js/minigames/container/container-minigame.js | 304 +++++++++++++++++++ js/minigames/index.js | 9 +- js/minigames/notes/notes-minigame.js | 9 + js/systems/doors.js | 8 +- js/systems/interactions.js | 38 +++ js/systems/unlock-system.js | 25 ++ test-container-minigame.html | 142 +++++++++ 11 files changed, 882 insertions(+), 5 deletions(-) create mode 100644 CONTAINER_MINIGAME_USAGE.md create mode 100644 assets/objects/notes.png create mode 100644 css/container-minigame.css create mode 100644 js/minigames/container/container-minigame.js create mode 100644 test-container-minigame.html diff --git a/CONTAINER_MINIGAME_USAGE.md b/CONTAINER_MINIGAME_USAGE.md new file mode 100644 index 0000000..42f607f --- /dev/null +++ b/CONTAINER_MINIGAME_USAGE.md @@ -0,0 +1,123 @@ +# Container Minigame Usage + +## Overview + +The Container Minigame allows players to interact with container items (like suitcases, briefcases, etc.) that contain other items. The minigame provides a visual interface similar to the player inventory, showing the container's contents in a grid layout. + +## Features + +- **Visual Container Display**: Shows an image of the container item with its name and observations +- **Contents Grid**: Displays all items within the container in a grid layout similar to the player inventory +- **Item Interaction**: Players can click on individual items to add them to their inventory +- **Notes Handling**: Notes items automatically trigger the notes minigame instead of being added to inventory +- **Container Collection**: If the container itself is takeable, players can add the entire container to their inventory +- **Unlock Integration**: Automatically launches after successfully unlocking a locked container + +## Usage + +### Automatic Launch +The container minigame automatically launches when: +1. A player interacts with an unlocked container that has contents +2. A player successfully unlocks a locked container (after the unlock minigame completes) + +### Manual Launch +You can manually start the container minigame using: +```javascript +window.startContainerMinigame(containerItem, contents, isTakeable); +``` + +### Parameters +- `containerItem`: The sprite object representing the container +- `contents`: Array of items within the container +- `isTakeable`: Boolean indicating if the container itself can be taken + +## Scenario Data Structure + +### Container Item +```json +{ + "type": "suitcase", + "name": "CEO Briefcase", + "takeable": false, + "locked": true, + "lockType": "key", + "requires": "briefcase_key:45,35,25,15", + "difficulty": "medium", + "observations": "An expensive leather briefcase with a sturdy lock", + "contents": [ + { + "type": "notes", + "name": "Private Note", + "takeable": true, + "readable": true, + "text": "Closet keypad code: 7391 - Must move evidence to safe before audit", + "observations": "A hastily written note on expensive paper" + }, + { + "type": "key", + "name": "Safe Key", + "takeable": true, + "key_id": "safe_key:52,29,44,37", + "observations": "A heavy-duty safe key hidden behind server equipment" + } + ] +} +``` + +### Content Items +Each item in the `contents` array should have: +- `type`: The item type (used for image path: `assets/objects/{type}.png`) +- `name`: Display name for the item +- `takeable`: Whether the item can be taken by the player +- Additional properties as needed (observations, text, key_id, etc.) + +## Integration with Unlock System + +The container minigame integrates seamlessly with the existing unlock system: + +1. **Locked Container**: When a player interacts with a locked container, the unlock minigame starts +2. **Successful Unlock**: After successful unlocking, the container minigame automatically launches +3. **Unlock State**: The container's `isUnlockedButNotCollected` flag is set to prevent automatic collection + +## Visual Design + +- **Container Image**: Large image of the container item at the top +- **Container Info**: Name and observations displayed below the image +- **Contents Grid**: Grid layout showing all items within the container +- **Item Tooltips**: Hover tooltips showing item names +- **Action Buttons**: "Take Container" (if takeable) and "Close" buttons + +## Styling + +The minigame uses the following CSS classes: +- `.container-minigame`: Main container +- `.container-image-section`: Container image and info +- `.container-contents-grid`: Grid of container contents +- `.container-content-slot`: Individual item slots +- `.container-content-item`: Item images +- `.container-actions`: Action buttons + +## Testing + +Use the test file `test-container-minigame.html` to test the container minigame functionality with sample data. + +## Example Scenario + +The CEO Briefcase in the `ceo_exfil.json` scenario demonstrates a complete container implementation: +- Locked with a key requirement +- Contains a private note with important information (triggers notes minigame when clicked) +- Contains a safe key for further progression +- Automatically launches the container minigame after unlocking + +### Notes Item Behavior +When a notes item is clicked in the container minigame: +1. The note is immediately removed from the container display +2. A success message shows "Read [Note Name]" +3. The container state is saved for return after reading +4. The container minigame closes +5. The notes minigame opens with the note's text and observations +6. The note is automatically added to the player's notes collection +7. **After closing the notes minigame, the player automatically returns to the container minigame** +8. If the container becomes empty, it shows "This container is empty" + +**Special Exception**: Unlike other minigames that close all other minigames, the notes minigame from containers has a special return flow that brings the player back to the container after reading. diff --git a/assets/objects/notes.png b/assets/objects/notes.png new file mode 100644 index 0000000000000000000000000000000000000000..188210a0e7d2efcb75fd5f93a070e0dc9b1d64ac GIT binary patch literal 411 zcmV;M0c8G(P)Px$RY^oaR5*>rl)p;DP!z_$bQAm&JG5C8=^`S6_yW3j=?geHIQjsk6cJY?lVr%` z3kc$DKoCI)iiFZ7gk(uFq&JD+c%AMs|0-!5{HBvHIsDFdZnzR^=-IgEGsXY_{lT$R zr>9DD8!gk|j_Uycwl+7&7hV%+slhT04ghhIVr_K=#k#27gQMC|`cOcGegiaxN&uj1 zD&N`ek+}$D5pW9t{E%TXwK196m@!7@(nOByLDy7X^{%CBDqmXekl_arHN{DaI7t_R ziJ!?b4gPR#L#kh1-*Iw!CY5%3b$!FXK0|^k z-8TFng3@j0rjL^pGsbcN0NCH_VO^GU-s6+`ldMWrX|q5-h~S5e000OgE`aPH!_l~$ ztPWTxyDD+E#k9lGm>7+(9x;GOTwG@s@@H5PIuGz~`UV + diff --git a/js/minigames/container/container-minigame.js b/js/minigames/container/container-minigame.js new file mode 100644 index 0000000..debacc0 --- /dev/null +++ b/js/minigames/container/container-minigame.js @@ -0,0 +1,304 @@ +// Container Minigame +import { MinigameScene } from '../framework/base-minigame.js'; +import { addToInventory, removeFromInventory } from '../../systems/inventory.js'; + +export class ContainerMinigame extends MinigameScene { + constructor(container, params) { + super(container, params); + this.containerItem = params.containerItem; + this.contents = params.contents || []; + this.isTakeable = params.isTakeable || false; + } + + init() { + // Call parent init first + super.init(); + + // Update header with container name + if (this.headerElement) { + this.headerElement.innerHTML = ` +

${this.containerItem.scenarioData.name}

+

${this.containerItem.scenarioData.observations || ''}

+ `; + } + + // Create the container minigame UI + this.createContainerUI(); + } + + createContainerUI() { + this.gameContainer.innerHTML = ` +
+
+ ${this.containerItem.scenarioData.name} +
+

${this.containerItem.scenarioData.name}

+

${this.containerItem.scenarioData.observations || ''}

+
+
+ +
+

Contents

+
+ +
+
+ +
+ ${this.isTakeable ? '' : ''} + +
+
+ `; + + // Populate contents + this.populateContents(); + + // Set up event listeners + this.setupEventListeners(); + } + + populateContents() { + const contentsGrid = document.getElementById('container-contents-grid'); + if (!contentsGrid) return; + + if (this.contents.length === 0) { + contentsGrid.innerHTML = '

This container is empty.

'; + return; + } + + this.contents.forEach((item, index) => { + const slot = document.createElement('div'); + slot.className = 'container-content-slot'; + + const itemImg = document.createElement('img'); + itemImg.className = 'container-content-item'; + itemImg.src = `assets/objects/${item.type}.png`; + itemImg.alt = item.name; + itemImg.title = item.name; + + // Add item data + itemImg.scenarioData = item; + itemImg.name = item.type; + itemImg.objectId = `container_${index}`; + + // Add click handler for taking items + if (item.takeable) { + itemImg.style.cursor = 'pointer'; + + // Special handling for notes - trigger notes minigame instead of taking + if (item.type === 'notes' && item.readable && item.text) { + itemImg.addEventListener('click', () => this.handleNotesItem(item, itemImg)); + } else { + itemImg.addEventListener('click', () => this.takeItem(item, itemImg)); + } + } + + // Create tooltip + const tooltip = document.createElement('div'); + tooltip.className = 'container-content-tooltip'; + tooltip.textContent = item.name; + + slot.appendChild(itemImg); + slot.appendChild(tooltip); + contentsGrid.appendChild(slot); + }); + } + + setupEventListeners() { + // Take container button + const takeContainerBtn = document.getElementById('take-container-btn'); + if (takeContainerBtn) { + this.addEventListener(takeContainerBtn, 'click', () => this.takeContainer()); + } + + // Close button + const closeBtn = document.getElementById('close-container-btn'); + if (closeBtn) { + this.addEventListener(closeBtn, 'click', () => this.complete(false)); + } + } + + handleNotesItem(item, itemElement) { + console.log('Handling notes item from container:', item); + + // Remove the note from container display + itemElement.parentElement.remove(); + + // Remove from contents array + const itemIndex = this.contents.findIndex(content => content === item); + if (itemIndex !== -1) { + this.contents.splice(itemIndex, 1); + } + + // Show success message + this.showMessage(`Read ${item.name}`, 'success'); + + // If container is now empty, update display + if (this.contents.length === 0) { + const contentsGrid = document.getElementById('container-contents-grid'); + if (contentsGrid) { + contentsGrid.innerHTML = '

This container is empty.

'; + } + } + + // Store container state for return after notes minigame + const containerState = { + containerItem: this.containerItem, + contents: this.contents, + isTakeable: this.isTakeable + }; + + // Store the container state globally so we can return to it + window.pendingContainerReturn = containerState; + + // Close the container minigame first + this.complete(false); + + // Start the notes minigame + if (window.startNotesMinigame) { + // Create a temporary sprite-like object for the notes minigame + const tempSprite = { + scenarioData: item, + name: item.type, + objectId: `temp_${Date.now()}` + }; + + // Start notes minigame with the item's text + window.startNotesMinigame(tempSprite, item.text, item.observations); + } else { + console.error('Notes minigame not available'); + window.gameAlert('Notes minigame not available', 'error', 'Error', 3000); + } + } + + takeItem(item, itemElement) { + console.log('Taking item from container:', item); + + // Create a temporary sprite-like object for the inventory system + const tempSprite = { + scenarioData: item, + name: item.type, + objectId: `temp_${Date.now()}`, + setVisible: function(visible) { + // Mock setVisible method for inventory compatibility + console.log(`Mock setVisible(${visible}) called on temp sprite`); + } + }; + + // Add to inventory + if (addToInventory(tempSprite)) { + // Remove from container display + itemElement.parentElement.remove(); + + // Remove from contents array + const itemIndex = this.contents.findIndex(content => content === item); + if (itemIndex !== -1) { + this.contents.splice(itemIndex, 1); + } + + // Show success message + this.showMessage(`Added ${item.name} to inventory`, 'success'); + + // If container is now empty, update display + if (this.contents.length === 0) { + const contentsGrid = document.getElementById('container-contents-grid'); + if (contentsGrid) { + contentsGrid.innerHTML = '

This container is empty.

'; + } + } + } else { + this.showMessage(`Failed to add ${item.name} to inventory`, 'error'); + } + } + + takeContainer() { + console.log('Taking container:', this.containerItem); + + // Ensure container item has setVisible method if it doesn't already + if (!this.containerItem.setVisible) { + this.containerItem.setVisible = function(visible) { + console.log(`Mock setVisible(${visible}) called on container item`); + }; + } + + // Add container to inventory + if (addToInventory(this.containerItem)) { + this.showMessage(`Added ${this.containerItem.scenarioData.name} to inventory`, 'success'); + + // Close the minigame after a short delay + setTimeout(() => { + this.complete(true); + }, 1500); + } else { + this.showMessage(`Failed to add ${this.containerItem.scenarioData.name} to inventory`, 'error'); + } + } + + showMessage(message, type) { + const messageElement = document.createElement('div'); + messageElement.className = `container-message container-message-${type}`; + messageElement.textContent = message; + + this.messageContainer.appendChild(messageElement); + + // Remove message after 3 seconds + setTimeout(() => { + if (messageElement.parentElement) { + messageElement.parentElement.removeChild(messageElement); + } + }, 3000); + } +} + +// Function to start the container minigame +export function startContainerMinigame(containerItem, contents, isTakeable = false) { + console.log('Starting container minigame', { containerItem, contents, isTakeable }); + + // Initialize the minigame framework if not already done + if (!window.MinigameFramework) { + console.error('MinigameFramework not available'); + return; + } + + if (!window.MinigameFramework.mainGameScene) { + window.MinigameFramework.init(window.game); + } + + // Start the container minigame + window.MinigameFramework.startMinigame('container', null, { + title: containerItem.scenarioData.name, + containerItem: containerItem, + contents: contents, + isTakeable: isTakeable, + cancelText: 'Close', + showCancel: true, + onComplete: (success, result) => { + console.log('Container minigame completed', { success, result }); + } + }); +} + +// Function to return to container after notes minigame +export function returnToContainerAfterNotes() { + console.log('Returning to container after notes minigame'); + + // Check if there's a pending container return + if (window.pendingContainerReturn) { + const containerState = window.pendingContainerReturn; + + // Clear the pending return state + window.pendingContainerReturn = null; + + // Start the container minigame with the stored state + startContainerMinigame( + containerState.containerItem, + containerState.contents, + containerState.isTakeable + ); + } else { + console.log('No pending container return found'); + } +} diff --git a/js/minigames/index.js b/js/minigames/index.js index 667cd11..9de4bfd 100644 --- a/js/minigames/index.js +++ b/js/minigames/index.js @@ -9,6 +9,7 @@ export { NotesMinigame, startNotesMinigame, showMissionBrief } from './notes/not export { BluetoothScannerMinigame, startBluetoothScannerMinigame } from './bluetooth/bluetooth-scanner-minigame.js'; export { BiometricsMinigame, startBiometricsMinigame } from './biometrics/biometrics-minigame.js'; export { LockpickSetMinigame, startLockpickSetMinigame } from './lockpick/lockpick-set-minigame.js'; +export { ContainerMinigame, startContainerMinigame, returnToContainerAfterNotes } from './container/container-minigame.js'; // Initialize the global minigame framework for backward compatibility import { MinigameFramework } from './framework/minigame-manager.js'; @@ -32,6 +33,9 @@ import { BiometricsMinigame, startBiometricsMinigame } from './biometrics/biomet // Import the lockpick set minigame import { LockpickSetMinigame, startLockpickSetMinigame } from './lockpick/lockpick-set-minigame.js'; +// Import the container minigame +import { ContainerMinigame, startContainerMinigame, returnToContainerAfterNotes } from './container/container-minigame.js'; + // Register minigames MinigameFramework.registerScene('lockpicking', LockpickingMinigamePhaser); // Use Phaser version as default MinigameFramework.registerScene('lockpicking-phaser', LockpickingMinigamePhaser); // Keep explicit phaser name @@ -40,10 +44,13 @@ MinigameFramework.registerScene('notes', NotesMinigame); MinigameFramework.registerScene('bluetooth-scanner', BluetoothScannerMinigame); MinigameFramework.registerScene('biometrics', BiometricsMinigame); MinigameFramework.registerScene('lockpick-set', LockpickSetMinigame); +MinigameFramework.registerScene('container', ContainerMinigame); // Make minigame functions available globally window.startNotesMinigame = startNotesMinigame; window.showMissionBrief = showMissionBrief; window.startBluetoothScannerMinigame = startBluetoothScannerMinigame; window.startBiometricsMinigame = startBiometricsMinigame; -window.startLockpickSetMinigame = startLockpickSetMinigame; \ No newline at end of file +window.startLockpickSetMinigame = startLockpickSetMinigame; +window.startContainerMinigame = startContainerMinigame; +window.returnToContainerAfterNotes = returnToContainerAfterNotes; \ No newline at end of file diff --git a/js/minigames/notes/notes-minigame.js b/js/minigames/notes/notes-minigame.js index b43dded..425aaeb 100644 --- a/js/minigames/notes/notes-minigame.js +++ b/js/minigames/notes/notes-minigame.js @@ -730,6 +730,15 @@ export function startNotesMinigame(item, noteContent, observationText, navigateT } else { console.log('NOTES COMPLETED - Not added to inventory'); } + + // Check if we need to return to a container after notes minigame + if (window.pendingContainerReturn && window.returnToContainerAfterNotes) { + console.log('Returning to container after notes minigame'); + // Small delay to ensure notes minigame cleanup completes + setTimeout(() => { + window.returnToContainerAfterNotes(); + }, 100); + } } }; diff --git a/js/systems/doors.js b/js/systems/doors.js index d4b3bf9..f8c355a 100644 --- a/js/systems/doors.js +++ b/js/systems/doors.js @@ -13,23 +13,23 @@ let gameRef = null; let rooms = null; // Global toggle for disabling locks during testing -window.DISABLE_LOCKS = false; // Set to true in console to bypass all lock checks +window.DISABLE_LOCKS = false; // Set to true in console to bypass all lock checks (doors and items) // Console helper functions for testing window.toggleLocks = function() { window.DISABLE_LOCKS = !window.DISABLE_LOCKS; - console.log(`Locks ${window.DISABLE_LOCKS ? 'DISABLED' : 'ENABLED'} for testing`); + console.log(`Locks ${window.DISABLE_LOCKS ? 'DISABLED' : 'ENABLED'} for testing (affects doors and items)`); return window.DISABLE_LOCKS; }; window.disableLocks = function() { window.DISABLE_LOCKS = true; - console.log('Locks DISABLED for testing - all doors will open without minigames'); + console.log('Locks DISABLED for testing - all doors and items will open/unlock without minigames'); }; window.enableLocks = function() { window.DISABLE_LOCKS = false; - console.log('Locks ENABLED - doors will require proper unlocking'); + console.log('Locks ENABLED - doors and items will require proper unlocking'); }; // Door transition cooldown system diff --git a/js/systems/interactions.js b/js/systems/interactions.js index 8b881d4..944bc4a 100644 --- a/js/systems/interactions.js +++ b/js/systems/interactions.js @@ -230,6 +230,23 @@ export function handleObjectInteraction(sprite) { return; } + // Handle container items (suitcase, briefcase, etc.) + if (data.type === 'suitcase' || data.type === 'briefcase' || data.contents) { + console.log('CONTAINER ITEM INTERACTION', data); + + // Check if container was unlocked but not yet collected + if (data.isUnlockedButNotCollected) { + console.log('CONTAINER UNLOCKED - LAUNCHING MINIGAME', data); + handleContainerInteraction(sprite); + return; + } + + // If container is still locked, the unlock system will handle it + // and set isUnlockedButNotCollected flag + console.log('CONTAINER LOCKED - UNLOCK SYSTEM WILL HANDLE', data); + return; + } + let message = `${data.name} `; if (data.observations) { message += `Observations: ${data.observations}\n`; @@ -284,6 +301,27 @@ export function handleObjectInteraction(sprite) { window.gameAlert(message, 'info', data.name, 0); } +// Handle container item interactions +function handleContainerInteraction(sprite) { + const data = sprite.scenarioData; + console.log('Handling container interaction:', data); + + // Check if container has contents + if (!data.contents || data.contents.length === 0) { + window.gameAlert(`${data.name} is empty.`, 'info', 'Empty Container', 3000); + return; + } + + // Start the container minigame + if (window.startContainerMinigame) { + window.startContainerMinigame(sprite, data.contents, data.takeable); + } else { + console.error('Container minigame not available'); + window.gameAlert('Container minigame not available', 'error', 'Error', 3000); + } +} + // Export for global access window.checkObjectInteractions = checkObjectInteractions; window.handleObjectInteraction = handleObjectInteraction; +window.handleContainerInteraction = handleContainerInteraction; diff --git a/js/systems/unlock-system.js b/js/systems/unlock-system.js index 82e6cf5..d6e526b 100644 --- a/js/systems/unlock-system.js +++ b/js/systems/unlock-system.js @@ -23,6 +23,13 @@ function boundsOverlap(rect1, rect2) { export function handleUnlock(lockable, type) { console.log('UNLOCK ATTEMPT'); + // Check if locks are disabled for testing + if (window.DISABLE_LOCKS) { + console.log('LOCKS DISABLED FOR TESTING - Unlocking directly'); + unlockTarget(lockable, type, lockable.layer); + return; + } + // Get lock requirements based on type const lockRequirements = type === 'door' ? getLockRequirementsForDoor(lockable) @@ -327,12 +334,30 @@ export function unlockTarget(lockable, type, layer) { // Set new state for containers with contents if (lockable.scenarioData.contents) { lockable.scenarioData.isUnlockedButNotCollected = true; + + // Automatically launch container minigame after unlocking + setTimeout(() => { + if (window.handleContainerInteraction) { + console.log('Auto-launching container minigame after unlock'); + window.handleContainerInteraction(lockable); + } + }, 500); // Small delay to ensure unlock message is shown + return; // Return early to prevent automatic collection } } else { lockable.locked = false; if (lockable.contents) { lockable.isUnlockedButNotCollected = true; + + // Automatically launch container minigame after unlocking + setTimeout(() => { + if (window.handleContainerInteraction) { + console.log('Auto-launching container minigame after unlock'); + window.handleContainerInteraction(lockable); + } + }, 500); // Small delay to ensure unlock message is shown + return; // Return early to prevent automatic collection } } diff --git a/test-container-minigame.html b/test-container-minigame.html new file mode 100644 index 0000000..6c56cec --- /dev/null +++ b/test-container-minigame.html @@ -0,0 +1,142 @@ + + + + + Container Minigame Test + + + + + + + + + + + + + + +
+

Container Minigame Test

+ +
+

Test Data

+

This test simulates the CEO Briefcase from the ceo_exfil.json scenario:

+
    +
  • Container: CEO Briefcase (suitcase)
  • +
  • Takeable: false
  • +
  • Contents: Private Note + Safe Key
  • +
+
+ + + + +
+ + + + +