From d63c828504aec1cda921c432299a9b5ae398711c Mon Sep 17 00:00:00 2001 From: Damian-I Date: Thu, 13 Mar 2025 03:43:33 +0000 Subject: [PATCH] reimplemented lockpicking to be more realistic (WIP) --- index.html | 1510 ++++++++++++++++++++++++---------------------------- 1 file changed, 687 insertions(+), 823 deletions(-) diff --git a/index.html b/index.html index 12104e8..bb6ee55 100644 --- a/index.html +++ b/index.html @@ -4882,849 +4882,713 @@ scene.input.mouse.enabled = false; } - function startLockpickingMinigame(lockable, currentScene, difficulty) { - // Create iframe container - const iframe = document.createElement('div'); - iframe.style.cssText = ` - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 60%; - height: 60%; - background: rgba(0, 0, 0, 0.9); - border: 1px solid #444; - z-index: 1000; - padding: 20px; - border-radius: 5px; - `; + // Minigame Framework + const MinigameFramework = { + activeMinigame: null, + mainGameScene: null, - // Add instructions - const instructions = document.createElement('div'); - instructions.innerHTML = ` -

Lock Picking

-

- Use spacebar or click to toggle tension levels. Each pin requires the right amount of tension.
- 🔵 Blue = Pin moving
- 🟢 Green = Pin set correctly
- 🔴 Red = Over-pushed (reset)
- Set all pins in the correct order with the right tension without resetting. -

- `; - instructions.style.cssText = ` - position: absolute; - top: 10px; - left: 50%; - transform: translateX(-50%); - width: 90%; - `; - - // Create game container - const gameContainer = document.createElement('div'); - gameContainer.style.cssText = ` - width: 100%; - height: calc(100% - 60px); - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - gap: 20px; - margin-top: 40px; - background: #222; - padding: 10px; - `; - - // Add difficulty selection - const difficultySelect = document.createElement('select'); - difficultySelect.style.cssText = ` - margin-bottom: 10px; - padding: 5px; - background: #444; - color: white; - border: 1px solid #666; - border-radius: 3px; - `; - const difficulties = ['Easy - Pins Visible', 'Hard - Audio Only']; - difficulties.forEach(diff => { - const option = document.createElement('option'); - option.value = diff; - option.textContent = diff; - difficultySelect.appendChild(option); - }); - - // Add audio feedback - const lockSounds = { - click: null, // Basic pin movement sound - binding: null, // Sound when a pin is binding (correct tension) - set: null, // Sound when a pin is successfully set - reset: null, // Sound when pins are reset - wrong: null, // Sound when wrong tension or wrong pin - tension: null, // Sound when changing tension - success: null, // Sound when successfully picking the lock - overTension: null // Sound when over-tensioning the lock - }; - - const initAudio = () => { - if (!lockSounds.click) { - // Basic click sound (when pressing a pin) - lockSounds.click = new Audio('assets/sounds/lockpick_click.mp3'); - - // Binding sound (when a pin is binding with correct tension) - lockSounds.binding = new Audio('assets/sounds/lockpick_binding.mp3'); - - // Set sound (when a pin is successfully set) - lockSounds.set = new Audio('assets/sounds/lockpick_set.mp3'); - - // Reset sound (when pins are reset) - lockSounds.reset = new Audio('assets/sounds/lockpick_reset.mp3'); - - // Wrong sound (when using wrong tension or pressing wrong pin) - lockSounds.wrong = new Audio('assets/sounds/lockpick_wrong.mp3'); - - // Tension sound (when changing tension levels) - lockSounds.tension = new Audio('assets/sounds/lockpick_tension.mp3'); - - // Success sound (when successfully picking the lock) - lockSounds.success = new Audio('assets/sounds/lockpick_success.mp3'); - - // Over-tension sound (when applying too much tension) - lockSounds.overTension = new Audio('assets/sounds/lockpick_overtension.mp3'); - } - }; - - // Initialize audio on first interaction - gameContainer.addEventListener('mousedown', initAudio, { once: true }); - - // Add pin binding order and game state - const numPins = getPinCountForDifficulty(difficulty); - const bindingOrder = Array.from({length: numPins}, (_, i) => i) - .sort(() => Math.random() - 0.5); - - const gameState = { - tensionLevel: 1, // Start with light tension (1) - pinStates: Array(numPins).fill(0), // 0 = down, 1 = moving, 2 = set - pinPressTime: Array(numPins).fill(0), // Track how long each pin is pressed - currentBindingIndex: 0, - hardMode: false, - maxPressTime: 1000, // Max time to hold a pin (ms) - failCount: 0, - maxFails: 3, - overTensioned: false, - lastPinSetTime: 0, // Track when the last pin was set - isActivelyPickingPin: false, // Track if we're actively working on a pin - tensionRequirements: Array(numPins).fill(0).map(() => { - // Each pin requires a specific tension level (1, 2, or 3) - const randomValue = Math.random(); - if (randomValue < 0.33) return 1; // Light tension - if (randomValue < 0.66) return 2; // Medium tension - return 3; // Heavy tension - }) - }; - - // Create tension wrench toggle - const tensionWrench = document.createElement('div'); - tensionWrench.style.cssText = ` - width: 100px; - height: 30px; - background: #666; - border: 2px solid #888; - border-radius: 5px; - cursor: pointer; - margin-bottom: 20px; - text-align: center; - line-height: 30px; - color: white; - transform: rotate(2deg); - `; - tensionWrench.textContent = 'Tension: LIGHT'; - - // Function to reset pins - function resetPins(showVisual = true) { - gameState.pinStates.fill(0); - gameState.pinPressTime.fill(0); - gameState.currentBindingIndex = 0; - gameState.failCount++; - - if (showVisual) { - Array.from(pinsContainer.children).forEach(pin => { - pin.style.background = '#555'; - if (!gameState.hardMode) { - pin.style.transition = 'background-color 0.3s'; - pin.style.background = '#f00'; - setTimeout(() => pin.style.background = '#555', 300); - } - }); - } - - if (gameState.failCount >= gameState.maxFails) { - alert("Lock picking failed! The lock is now jammed."); - // Safely remove iframe if it exists in the document - const existingIframe = document.querySelector('div[style*="z-index: 1000"]'); - if (existingIframe && existingIframe.parentNode) { - existingIframe.parentNode.removeChild(existingIframe); - } - if (currentScene && currentScene.input && currentScene.input.mouse) { - currentScene.input.mouse.enabled = true; - } - } - } - - // Create a single function for toggling tension - function toggleTension() { - // Toggle between 3 tension levels (light -> medium -> heavy -> light) - const previousTensionLevel = gameState.tensionLevel; - gameState.tensionLevel = (gameState.tensionLevel % 3) + 1; + // Initialize the framework + init: function(mainScene) { + this.mainGameScene = mainScene; - // Play tension change sound - if (lockSounds.tension) { - lockSounds.tension.currentTime = 0; - lockSounds.tension.play().catch(e => console.log('Audio play failed:', e)); - } - - updateTensionWrench(); - - // Check if we're over-tensioning - but only if we've started interacting with pins - // AND only if we're actively working on a pin (not just changing tension) - const timeSinceLastPinSet = Date.now() - gameState.lastPinSetTime; - const isActivelyPickingLock = gameState.isActivelyPickingPin; - - if (gameState.tensionLevel === 3 && - gameState.currentBindingIndex > 0 && - timeSinceLastPinSet > 1000 && - isActivelyPickingLock) { + // Create container for all minigames if it doesn't exist + if (!document.getElementById('minigame-container')) { + const container = document.createElement('div'); + container.id = 'minigame-container'; + document.body.appendChild(container); - // 30% chance of over-tensioning with heavy pressure - if (Math.random() < 0.3) { - gameState.overTensioned = true; - tensionWrench.style.background = '#ff3333'; - tensionWrench.style.transform = 'rotate(15deg)'; - - // Play over-tension sound - if (lockSounds.overTension) { - lockSounds.overTension.currentTime = 0; - lockSounds.overTension.play().catch(e => console.log('Audio play failed:', e)); - } - - setTimeout(() => { - alert("You applied too much tension and jammed the lock!"); - resetPins(); - }, 500); - } - } - } - - // Add this new function to update tension wrench visuals - function updateTensionWrench() { - // Update tension wrench appearance based on level - switch(gameState.tensionLevel) { - case 1: // Light - tensionWrench.style.background = '#666'; - tensionWrench.style.transform = 'rotate(2deg)'; - tensionWrench.textContent = 'Tension: LIGHT'; - break; - case 2: // Medium - tensionWrench.style.background = '#888'; - tensionWrench.style.transform = 'rotate(5deg)'; - tensionWrench.textContent = 'Tension: MEDIUM'; - break; - case 3: // Heavy - tensionWrench.style.background = '#aaa'; - tensionWrench.style.transform = 'rotate(8deg)'; - tensionWrench.textContent = 'Tension: HEAVY'; - break; - } - } - - // Create pins container - const pinsContainer = document.createElement('div'); - pinsContainer.style.cssText = ` - display: flex; - gap: 10px; - background: #333; - padding: 20px; - border-radius: 10px; - `; - - // Create individual pins - for (let i = 0; i < numPins; i++) { - const pin = document.createElement('div'); - pin.style.cssText = ` - width: 30px; - height: 100px; - background: #555; - border: 2px solid #777; - border-radius: 5px; - cursor: pointer; - position: relative; - transition: transform 0.1s, background-color 0.3s; - `; - - // Add a subtle indicator at the bottom of each pin - const pinIndicator = document.createElement('div'); - pinIndicator.style.cssText = ` - position: absolute; - bottom: 0; - left: 0; - width: 100%; - height: 5px; - background: #555; - transition: background-color 0.3s; - `; - pin.appendChild(pinIndicator); - - const pinNumber = document.createElement('div'); - pinNumber.style.cssText = ` - position: absolute; - top: -20px; - width: 100%; - text-align: center; - color: white; - `; - pinNumber.textContent = (i + 1).toString(); - pin.appendChild(pinNumber); - - // Function to update pin appearance based on its state and binding status - function updatePinAppearance() { - // Reset to default first - pin.style.background = '#555'; - pinIndicator.style.background = '#555'; - pin.style.animation = ''; - pin.style.borderColor = '#777'; - - // If the pin is set, show it as green - if (gameState.pinStates[i] === 2) { - pin.style.background = '#0f0'; - pin.style.cursor = 'default'; - pinIndicator.style.background = '#0f0'; - return; - } - - // Get the current binding pin - const bindingPin = bindingOrder[gameState.currentBindingIndex]; - - // Generate consistent red herrings based on the current binding index - // This ensures the same pins are highlighted as red herrings until the next pin is set - const redHerringSeeds = [ - (bindingPin * 3 + 7) % numPins, - (bindingPin * 5 + 3) % numPins - ]; - - // Filter out the binding pin and already set pins from red herrings - const redHerrings = redHerringSeeds.filter(index => - index !== bindingPin && gameState.pinStates[index] !== 2); - - // If this is the current binding pin, give a subtle hint based on difficulty - if (i === bindingPin) { - // For easy difficulty, make the binding pin more obvious - if (difficulty === 'easy') { - pinIndicator.style.background = '#ff9900'; // Orange indicator for binding pin - - // Also show the required tension level with a color hint - const requiredTension = gameState.tensionRequirements[i]; - if (requiredTension === 1) { - pin.style.borderColor = '#66ccff'; // Light blue for light tension - } else if (requiredTension === 2) { - pin.style.borderColor = '#9966ff'; // Purple for medium tension - } else { - pin.style.borderColor = '#ff6666'; // Red for heavy tension - } - - // Add a subtle animation - pin.style.animation = 'pinWiggle 2s infinite'; - } - // For medium difficulty, just show which pin is binding with less obvious cues - else if (difficulty === 'medium') { - pinIndicator.style.background = '#ff9900'; // Orange indicator for binding pin - pin.style.animation = 'pinWiggle 3s infinite'; - } - // For hard difficulty, very subtle indication - else if (difficulty === 'hard') { - pin.style.animation = 'pinWiggle 4s infinite 0.5s'; - } - } - // If this is a red herring, give misleading feedback - else if (redHerrings.includes(i) && gameState.currentBindingIndex > 0 && difficulty !== 'easy') { - // The amount of misleading feedback depends on difficulty - if (difficulty === 'medium') { - // For medium, make red herrings somewhat convincing - pinIndicator.style.background = '#ff9900'; // Same color as real binding pin - pin.style.animation = 'pinWiggle 3.5s infinite 0.7s'; // Similar wiggle to real pin - - // Randomly assign fake tension indicators to confuse - const fakeTension = Math.floor(Math.random() * 3) + 1; - if (fakeTension === 1) { - pin.style.borderColor = '#66ccff'; - } else if (fakeTension === 2) { - pin.style.borderColor = '#9966ff'; - } else { - pin.style.borderColor = '#ff6666'; - } - } - else if (difficulty === 'hard') { - // For hard, make red herrings very convincing - pin.style.animation = 'pinWiggle 4s infinite 0.3s'; - - // On hard, sometimes make a red herring more convincing than the real pin - if (Math.random() < 0.5) { - pinIndicator.style.background = '#ff9900'; - } - } - } - } - - // Add the wiggle animation to the document - if (!document.getElementById('pinWiggleAnimation')) { + // Add base styles const style = document.createElement('style'); - style.id = 'pinWiggleAnimation'; + style.id = 'minigame-framework-styles'; style.textContent = ` - @keyframes pinWiggle { - 0% { transform: translateY(0); } - 15% { transform: translateY(-2px); } - 30% { transform: translateY(0); } - 45% { transform: translateY(-1px); } - 60% { transform: translateY(0); } - 75% { transform: translateY(-0.5px); } - 100% { transform: translateY(0); } + #minigame-container { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 1000; + display: none; + } + + #minigame-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + z-index: 1001; + } + + .minigame-scene { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: #222; + color: white; + border-radius: 10px; + box-shadow: 0 0 20px rgba(0,0,0,0.5); + z-index: 1002; + display: none; + } + + .minigame-close { + background: #555; + color: white; + border: none; + padding: 10px 20px; + border-radius: 5px; + cursor: pointer; + margin-top: 10px; + align-self: center; } `; document.head.appendChild(style); } - - // Update all pins whenever a pin state changes - function updateAllPins() { - // Get the current binding pin - const bindingPin = bindingOrder[gameState.currentBindingIndex]; - - // Generate consistent red herrings based on the current binding index - const redHerringSeeds = [ - (bindingPin * 3 + 7) % numPins, - (bindingPin * 5 + 3) % numPins - ]; - - // Filter out the binding pin and already set pins from red herrings - const redHerrings = redHerringSeeds.filter(index => - index !== bindingPin && gameState.pinStates[index] !== 2); - - Array.from(pinsContainer.children).forEach((pin, index) => { - // Find the indicator within this pin - const indicator = pin.querySelector('div:first-child'); - - // Reset styles first - pin.style.background = '#555'; - pin.style.animation = ''; - pin.style.borderColor = '#777'; - if (indicator) indicator.style.background = '#555'; - - // Update based on current game state - if (gameState.pinStates[index] === 2) { - pin.style.background = '#0f0'; - pin.style.cursor = 'default'; - if (indicator) indicator.style.background = '#0f0'; - } else { - pin.style.cursor = 'pointer'; - - // Check if this is the binding pin - if (index === bindingPin && !gameState.hardMode) { - if (difficulty === 'easy') { - if (indicator) indicator.style.background = '#ff9900'; - - // Show tension hint - const requiredTension = gameState.tensionRequirements[index]; - if (requiredTension === 1) { - pin.style.borderColor = '#66ccff'; - } else if (requiredTension === 2) { - pin.style.borderColor = '#9966ff'; - } else { - pin.style.borderColor = '#ff6666'; - } - - pin.style.animation = 'pinWiggle 2s infinite'; - } - else if (difficulty === 'medium') { - if (indicator) indicator.style.background = '#ff9900'; - pin.style.animation = 'pinWiggle 3s infinite'; - } - else if (difficulty === 'hard') { - pin.style.animation = 'pinWiggle 4s infinite 0.5s'; - } - } - // Check if this is a red herring, but only for medium and hard difficulties - else if (redHerrings.includes(index) && gameState.currentBindingIndex > 0 && difficulty !== 'easy') { - if (difficulty === 'medium') { - if (indicator) indicator.style.background = '#ff9900'; - pin.style.animation = 'pinWiggle 3.5s infinite 0.7s'; - - const fakeTension = Math.floor(Math.random() * 3) + 1; - if (fakeTension === 1) { - pin.style.borderColor = '#66ccff'; - } else if (fakeTension === 2) { - pin.style.borderColor = '#9966ff'; - } else { - pin.style.borderColor = '#ff6666'; - } - } - else if (difficulty === 'hard') { - pin.style.animation = 'pinWiggle 4s infinite 0.3s'; - if (Math.random() < 0.5 && indicator) { - indicator.style.background = '#ff9900'; - } - } - } - } - }); + }, + + // Launch a minigame + startMinigame: function(minigameId, params = {}) { + // Don't allow multiple minigames at once + if (this.activeMinigame) { + console.warn('A minigame is already running!'); + return false; } - - // Call updatePinAppearance initially and whenever the game state changes - updatePinAppearance(); - - let pressStartTime = 0; - let pressTimer = null; - - function checkPinPress() { - if (pressStartTime === 0) return; - - const pressDuration = Date.now() - pressStartTime; - if (pressDuration > gameState.maxPressTime) { - // Clear the timer first before calling resetPins - clearInterval(pressTimer); - pressTimer = null; - resetPins(); + + // Get the minigame scene constructor + const MinigameScene = this.scenes[minigameId]; + if (!MinigameScene) { + console.error(`Minigame "${minigameId}" not found!`); + return false; + } + + // Pause the main game + if (this.mainGameScene) { + // Pause Phaser scene + if (this.mainGameScene.scene && typeof this.mainGameScene.scene.pause === 'function') { + this.mainGameScene.scene.pause(); } } - - pin.onmousedown = () => { - // First check if this pin is already set - const pinIndex = Array.from(pinsContainer.children).indexOf(pin); - if (gameState.pinStates[pinIndex] === 2) { - // Pin is already set, don't allow interaction - return; + + // Show the minigame container + const container = document.getElementById('minigame-container'); + container.style.display = 'block'; + + // Create overlay + const overlay = document.createElement('div'); + overlay.id = 'minigame-overlay'; + container.appendChild(overlay); + + // Create scene element + const sceneElement = document.createElement('div'); + sceneElement.className = 'minigame-scene'; + sceneElement.id = `minigame-scene-${minigameId}`; + container.appendChild(sceneElement); + + // Instantiate the minigame scene + this.activeMinigame = new MinigameScene(sceneElement, { + ...params, + onComplete: (success, result) => { + this.endMinigame(success, result); + if (params.onComplete) params.onComplete(success, result); } - - // Set the flag to indicate we're actively picking a pin - gameState.isActivelyPickingPin = true; - - // Play basic click sound - if (lockSounds.click) { - lockSounds.click.currentTime = 0; - lockSounds.click.play().catch(e => console.log('Audio play failed:', e)); - } - - pressStartTime = Date.now(); - pressTimer = setInterval(checkPinPress, 100); - - pin.style.transform = 'translateY(-10px)'; - - // Each pin has different tension requirements - const bindingPin = bindingOrder[gameState.currentBindingIndex]; - const requiredTension = gameState.tensionRequirements[pinIndex]; - - // Check if this is the current binding pin - if (pinIndex === bindingPin) { - // This pin needs exactly the right tension level - const correctTension = (gameState.tensionLevel === requiredTension); - - if (correctTension && !gameState.overTensioned) { - // Play binding sound - correct pin with correct tension - if (lockSounds.binding) { - lockSounds.binding.currentTime = 0; - lockSounds.binding.play().catch(e => console.log('Audio play failed:', e)); - } - - if (!gameState.hardMode) { - pin.style.background = '#00f'; - } - - // Start a timer to set the pin - setTimeout(() => { - if (pressStartTime !== 0) { // Still pressing - // Double-check tension is still correct - const stillCorrectTension = (gameState.tensionLevel === requiredTension); - - if (stillCorrectTension && !gameState.overTensioned) { - gameState.pinStates[pinIndex] = 2; - gameState.currentBindingIndex++; - gameState.lastPinSetTime = Date.now(); - - // Play set sound - pin successfully set - if (lockSounds.set) { - lockSounds.set.currentTime = 0; - lockSounds.set.play().catch(e => console.log('Audio play failed:', e)); - } - - if (!gameState.hardMode) { - pin.style.background = '#0f0'; - pinIndicator.style.background = '#0f0'; - } - - // Update all pins to show new binding state - updateAllPins(); - - checkWinCondition(); - } - } - }, 500); - } else if (gameState.tensionLevel > 0) { - // Wrong tension but trying - give feedback - // Play wrong sound - wrong tension on correct pin - if (lockSounds.wrong) { - lockSounds.wrong.currentTime = 0; - lockSounds.wrong.play().catch(e => console.log('Audio play failed:', e)); - } - - if (!gameState.hardMode) { - pin.style.background = '#00f'; - } - - // Start counting towards potential reset - gameState.pinPressTime[pinIndex] = Date.now(); - } - } else if (gameState.tensionLevel > 0 && gameState.pinStates[pinIndex] !== 2) { - // Wrong pin - give feedback - if (lockSounds.wrong) { - lockSounds.wrong.currentTime = 0; - lockSounds.wrong.play().catch(e => console.log('Audio play failed:', e)); - } - - if (!gameState.hardMode) { - pin.style.background = '#00f'; - } - // Start counting towards potential reset - gameState.pinPressTime[pinIndex] = Date.now(); - } - }; - - pin.onmouseup = pin.onmouseleave = () => { - // Clear the flag to indicate we're no longer actively picking a pin - gameState.isActivelyPickingPin = false; - - pressStartTime = 0; - if (pressTimer) { - clearInterval(pressTimer); - pressTimer = null; - } - - pin.style.transform = 'translateY(0)'; - if (gameState.pinStates[i] !== 2) { - pin.style.background = '#555'; - // Update appearance to show binding status - updatePinAppearance(); - } - }; - - pinsContainer.appendChild(pin); - } - - difficultySelect.onchange = () => { - gameState.hardMode = difficultySelect.value.includes('Hard'); - Array.from(pinsContainer.children).forEach(pin => { - pin.style.opacity = gameState.hardMode ? '0.1' : '1'; }); - }; - - // Add components to game container - gameContainer.appendChild(difficultySelect); - gameContainer.appendChild(tensionWrench); - gameContainer.appendChild(pinsContainer); - - // Add close button - const closeButton = document.createElement('button'); - closeButton.textContent = 'X'; - closeButton.style.cssText = ` - position: absolute; - right: 10px; - top: 10px; - background: none; - border: none; - color: white; - font-size: 20px; - cursor: pointer; - `; - closeButton.onclick = () => { - document.body.removeChild(iframe); - if (currentScene && currentScene.input && currentScene.input.mouse) { - currentScene.input.mouse.enabled = true; - } - }; - - // Assemble the interface - iframe.appendChild(closeButton); - iframe.appendChild(instructions); - iframe.appendChild(gameContainer); - document.body.appendChild(iframe); - - // Disable game movement - if (currentScene && currentScene.input && currentScene.input.mouse) { - currentScene.input.mouse.enabled = false; - } - - // Add this function before the pin creation loop - function checkWinCondition() { - if (gameState.currentBindingIndex >= numPins) { - // Play success sound - if (lockSounds.success) { - lockSounds.success.currentTime = 0; - lockSounds.success.play().catch(e => console.log('Audio play failed:', e)); - } - - console.log('Lock picked successfully:', lockable); - - // Create success message - const successMessage = document.createElement('div'); - successMessage.style.cssText = ` - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - background: rgba(0, 0, 0, 0.8); - color: #0f0; - padding: 20px; - border-radius: 10px; - font-size: 24px; - text-align: center; - z-index: 1002; - `; - successMessage.textContent = "Lock successfully picked!"; - iframe.appendChild(successMessage); - - // Disable further interaction - gameContainer.style.pointerEvents = 'none'; - - setTimeout(() => { - // For doors, we need to check properties and handle differently - if (lockable && lockable.properties && lockable.properties.locked) { - console.log('Unlocking door tile:', lockable); - unlockDoor(lockable, lockable.layer); - } - // For containers and other lockable items - else if (lockable && lockable.scenarioData) { - console.log('Unlocking container:', lockable.scenarioData); - lockable.scenarioData.locked = false; - - // Set the flag to indicate the container is unlocked but contents not collected - if (lockable.scenarioData.contents && lockable.scenarioData.contents.length > 0) { - lockable.scenarioData.isUnlockedButNotCollected = true; - debugLog('Container unlocked and ready for collection', lockable.scenarioData, 1); - } - } - - // Remove the minigame - document.body.removeChild(iframe); - if (currentScene && currentScene.input && currentScene.input.mouse) { - currentScene.input.mouse.enabled = true; - } - }, 1500); - - return true; - } - return false; - } - - // Use the toggleTension function for both click and keyboard events - tensionWrench.onclick = toggleTension; - - document.addEventListener('keydown', function(event) { - // Only process if the lockpicking minigame is active - if (!document.querySelector('div[style*="z-index: 1000"]')) return; - if (event.code === 'Space') { - event.preventDefault(); // Prevent page scrolling - toggleTension(); - } - }); - - // Keep only the table debug function - function logTensionDebugInfo() { - // Only show debug info if debug mode is enabled - if (!DEBUG_MODE.enabled) return; - - DEBUG_MODE.log("=== LOCKPICKING DEBUG INFO ==="); - DEBUG_MODE.log("Pin binding order and tension requirements:"); - - const tableData = []; - - for (let orderIndex = 0; orderIndex < numPins; orderIndex++) { - const pinIndex = bindingOrder[orderIndex]; - const requiredTension = gameState.tensionRequirements[pinIndex]; - let tensionNeeded; - - switch(requiredTension) { - case 1: tensionNeeded = 'Light'; break; - case 2: tensionNeeded = 'Medium'; break; - case 3: tensionNeeded = 'Heavy'; break; - default: tensionNeeded = 'Unknown'; - } - - tableData.push({ - 'Binding Order': orderIndex + 1, - 'Pin #': pinIndex + 1, - 'Tension Required': tensionNeeded - }); - } - - console.table(tableData); - } - - // Call this function instead of addTensionDebugDisplay - logTensionDebugInfo(); - } - - // Add this function to get pin count based on difficulty - function getPinCountForDifficulty(difficulty) { - switch(difficulty?.toLowerCase()) { - case 'easy': - return 3; - case 'medium': - return 5; - case 'hard': - return 7; - default: - return 5; // Default to medium difficulty - } - } - - // removes an item from the inventory - function removeFromInventory(sprite) { - if (!sprite || !inventory.items) { - return false; - } - - try { - // Find the index of the sprite in the inventory - const index = inventory.items.indexOf(sprite); - - if (index === -1) { - return false; // Item not found in inventory - } - - // Remove from container - inventory.container.remove(sprite); - - // Remove from items array - inventory.items.splice(index, 1); - - // Destroy the sprite - sprite.destroy(); - - // Rearrange remaining items - rearrangeInventoryItems(); - - // Log the removal - debugLog('INVENTORY ITEM REMOVED', { - name: sprite.name, - totalItems: inventory.items.length - }, 2); + // Initialize and start the minigame + this.activeMinigame.init(); + this.activeMinigame.start(); return true; - } catch (error) { - console.error('Error removing item from inventory:', error); + }, + + // End the active minigame + endMinigame: function(success = false, result = null) { + if (!this.activeMinigame) return; + + // Call the minigame's cleanup method + if (typeof this.activeMinigame.cleanup === 'function') { + this.activeMinigame.cleanup(); + } + + // Remove the minigame elements + const container = document.getElementById('minigame-container'); + container.innerHTML = ''; + container.style.display = 'none'; + + // Unpause the main game + if (this.mainGameScene) { + // Resume Phaser scene + if (this.mainGameScene.scene && typeof this.mainGameScene.scene.resume === 'function') { + this.mainGameScene.scene.resume(); + } + } + + this.activeMinigame = null; + }, + + // Register a new minigame scene + registerScene: function(id, sceneClass) { + if (!this.scenes) this.scenes = {}; + this.scenes[id] = sceneClass; + }, + + // Base class for minigame scenes + MinigameScene: class { + constructor(container, params = {}) { + this.container = container; + this.params = params; + this.isActive = false; + } + + init() { + // Show the scene container + this.container.style.display = 'flex'; + + // Add escape key handler + this.escHandler = (e) => { + if (e.key === 'Escape' && this.isActive) { + this.complete(false); + } + }; + document.addEventListener('keydown', this.escHandler); + } + + start() { + this.isActive = true; + } + + complete(success, result = null) { + this.isActive = false; + document.removeEventListener('keydown', this.escHandler); + if (typeof this.params.onComplete === 'function') { + this.params.onComplete(success, result); + } + } + + cleanup() { + // Cleanup resources + } + } + }; + + // Lockpicking Minigame Scene implementation + class LockpickingMinigame extends MinigameFramework.MinigameScene { + constructor(container, params) { + super(container, params); + + this.lockable = params.lockable; + this.difficulty = params.difficulty || 'medium'; + this.pinCount = this.difficulty === 'easy' ? 3 : this.difficulty === 'medium' ? 4 : 5; + + this.pins = []; + this.gameState = { + tension: 0, + gameActive: false, + pinsSet: 0 + }; + } + + init() { + super.init(); + + // Apply styles + this.container.style.width = '90%'; + this.container.style.maxWidth = '500px'; + this.container.style.padding = '20px'; + this.container.style.display = 'flex'; + this.container.style.flexDirection = 'column'; + this.container.style.gap = '15px'; + + // Add styles if they don't exist + if (!document.getElementById('lockpicking-styles')) { + const style = document.createElement('style'); + style.id = 'lockpicking-styles'; + style.textContent = ` + .lock-visual { + display: flex; + justify-content: center; + align-items: flex-end; + gap: 15px; + height: 120px; + background: #333; + border-radius: 5px; + padding: 15px; + position: relative; + } + + .pin { + width: 40px; + height: 100px; + position: relative; + background: #444; + border-radius: 4px 4px 0 0; + overflow: visible; + cursor: pointer; + transition: transform 0.1s; + } + + .pin:hover { + background: #555; + transform: translateY(-2px); + } + + .pin:active { + transform: translateY(0); + } + + .shear-line { + position: absolute; + width: 100%; + height: 2px; + background: #aaa; + bottom: 40px; + z-index: 5; + } + + .key-pin { + position: absolute; + bottom: 0; + width: 100%; + height: 0px; + background: #dd3333; + transition: height 0.1s; + } + + .driver-pin { + position: absolute; + width: 100%; + height: 40px; + background: #3355dd; + transition: bottom 0.1s; + bottom: 40px; + } + + .spring { + position: absolute; + bottom: 80px; + width: 100%; + height: 20px; + background: linear-gradient(to bottom, + transparent 0%, transparent 20%, + #999 20%, #999 30%, + transparent 30%, transparent 40%, + #999 40%, #999 50%, + transparent 50%, transparent 60%, + #999 60%, #999 70%, + transparent 70%, transparent 80%, + #999 80%, #999 90%, + transparent 90%, transparent 100% + ); + transition: height 0.1s; + } + + .pin.binding { + box-shadow: 0 0 8px 2px #ffcc00; + } + + .pin.set .driver-pin { + bottom: 42px; + } + + .pin.set .key-pin { + height: 39px; + } + + .tension-control { + display: flex; + flex-direction: column; + padding: 10px; + background: #333; + border-radius: 5px; + } + + .tension-control label { + margin-bottom: 5px; + } + + .tension-slider { + margin: 10px 0; + } + + .tension-meter { + width: 100%; + height: 10px; + background: #444; + border-radius: 5px; + overflow: hidden; + } + + .tension-fill { + height: 100%; + width: 0%; + background: linear-gradient(to right, #33cc33, #ffcc00, #cc3333); + transition: width 0.2s; + } + + .lockpick-feedback { + padding: 10px; + background: #333; + border-radius: 5px; + text-align: center; + min-height: 20px; + } + + .lockpick-timer { + padding: 5px 10px; + background: #333; + border-radius: 5px; + text-align: center; + } + + .minigame-scene.success { + border: 2px solid #33cc33; + } + + .minigame-scene.failure { + border: 2px solid #cc3333; + } + `; + document.head.appendChild(style); + } + + // Create header with title and instructions + const header = document.createElement('div'); + header.className = 'lockpick-header'; + header.innerHTML = ` +

Lockpicking

+

Apply tension with the wrench, then click on pins to lift them to the shear line

+ `; + this.container.appendChild(header); + + // Create the lock cylinder visualization + const lockVisual = document.createElement('div'); + lockVisual.className = 'lock-visual'; + this.container.appendChild(lockVisual); + + // Create pins with random binding order + const bindingOrder = this.shuffleArray([...Array(this.pinCount).keys()]); + + for (let i = 0; i < this.pinCount; i++) { + // Create pin container + const pinElement = document.createElement('div'); + pinElement.className = 'pin'; + pinElement.dataset.index = i; + lockVisual.appendChild(pinElement); + + // Create shear line + const shearLine = document.createElement('div'); + shearLine.className = 'shear-line'; + pinElement.appendChild(shearLine); + + // Create key pin (bottom pin) + const keyPin = document.createElement('div'); + keyPin.className = 'key-pin'; + pinElement.appendChild(keyPin); + + // Create driver pin (top pin) + const driverPin = document.createElement('div'); + driverPin.className = 'driver-pin'; + pinElement.appendChild(driverPin); + + // Create spring + const spring = document.createElement('div'); + spring.className = 'spring'; + pinElement.appendChild(spring); + + // Store pin data + const pin = { + index: i, + binding: bindingOrder.indexOf(i), + setPoint: Math.random() * 0.4 + 0.3, // Point between 30-70% where pin sets + currentHeight: 0, + isSet: false, + elements: { + container: pinElement, + keyPin: keyPin, + driverPin: driverPin, + spring: spring + } + }; + + this.pins.push(pin); + + // Add click event to pin + pinElement.addEventListener('click', () => { + if (!this.gameState.gameActive) return; + + // Only allow clicking if we have tension applied + if (this.gameState.tension < 20) { + this.updateFeedback("Apply more tension with the wrench first"); + return; + } + + // Animate the pin moving up + if (!pin.isSet) { + this.animatePinLift(pin); + } + }); + } + + // Add tension wrench control + const tensionControl = document.createElement('div'); + tensionControl.className = 'tension-control'; + tensionControl.innerHTML = ` + + +
+ `; + this.container.appendChild(tensionControl); + + // Feedback area + this.feedback = document.createElement('div'); + this.feedback.className = 'lockpick-feedback'; + this.feedback.textContent = 'Apply tension with the wrench, then click pins to try picking the lock'; + this.container.appendChild(this.feedback); + + // No timer display anymore + + // Tension wrench control event + const tensionSlider = tensionControl.querySelector('.tension-slider'); + const tensionFill = tensionControl.querySelector('.tension-fill'); + + tensionSlider.addEventListener('input', () => { + this.gameState.tension = parseInt(tensionSlider.value); + tensionFill.style.width = `${this.gameState.tension}%`; + + // Update which pins are binding + this.updatePinVisuals(); + + // If tension is suddenly reduced, pins may drop + if (this.gameState.tension < 10 && this.gameState.pinsSet > 0) { + // Drop all set pins + this.pins.forEach(pin => { + if (pin.isSet) { + pin.isSet = false; + pin.currentHeight = 0; + } + }); + this.gameState.pinsSet = 0; + this.updatePinVisuals(); + this.updateFeedback("Tension released - all pins dropped!"); + } + }); + } + + start() { + super.start(); + + // Set game as active + this.gameState.gameActive = true; + + // No timer setup anymore + } + + cleanup() { + // No timer to clear anymore + } + + updatePinVisuals() { + this.pins.forEach(pin => { + // Update key pin and driver pin heights + pin.elements.keyPin.style.height = `${pin.currentHeight * 40}px`; + pin.elements.driverPin.style.bottom = `${pin.currentHeight * 40 + 1}px`; + pin.elements.spring.style.height = `${20 - pin.currentHeight * 5}px`; + + // Show binding state + if (this.shouldPinBind(pin) && !pin.isSet) { + pin.elements.container.classList.add('binding'); + } else { + pin.elements.container.classList.remove('binding'); + } + + // Show set state + if (pin.isSet) { + pin.elements.container.classList.add('set'); + } else { + pin.elements.container.classList.remove('set'); + } + }); + } + + shouldPinBind(pin) { + if (this.gameState.tension < 20) return false; + + // Find the next unset pin in binding order + for (let order = 0; order < this.pinCount; order++) { + const nextPin = this.pins.find(p => p.binding === order && !p.isSet); + if (nextPin) { + return pin.index === nextPin.index; + } + } return false; } + + animatePinLift(pin) { + // Only the binding pin can be set + if (!this.shouldPinBind(pin)) { + this.updateFeedback("This pin isn't binding yet"); + + // Small bounce animation to show it's stuck + let height = 0; + const interval = setInterval(() => { + height += 0.05; + if (height >= 0.2) { + clearInterval(interval); + + // Fall back down + const fallInterval = setInterval(() => { + height -= 0.05; + if (height <= 0) { + height = 0; + clearInterval(fallInterval); + } + pin.currentHeight = height; + this.updatePinVisuals(); + }, 20); + } + pin.currentHeight = height; + this.updatePinVisuals(); + }, 20); + + return; + } + + // Start height at current position + let height = pin.currentHeight; + + // Animate lifting to the potential set point + const liftInterval = setInterval(() => { + height += 0.05; + + // Check if we're at or past the set point + if (Math.abs(height - pin.setPoint) < 0.1) { + // At the correct height - set the pin! + clearInterval(liftInterval); + pin.currentHeight = pin.setPoint; + pin.isSet = true; + this.gameState.pinsSet++; + this.updatePinVisuals(); + this.updateFeedback(`Pin set at the shear line! (${this.gameState.pinsSet}/${this.pinCount})`); + + // Check for win condition + if (this.gameState.pinsSet === this.pinCount) { + this.endGame(true); + } + } + else if (height > pin.setPoint + 0.1) { + // We went too far - overset the pin + clearInterval(liftInterval); + this.updateFeedback("Pin pushed too far - overset!"); + + // Random chance of dropping another pin + if (this.gameState.tension > 60 && Math.random() < 0.3) { + const randomSetPin = this.pins.find(p => p.isSet && Math.random() < 0.5); + if (randomSetPin) { + randomSetPin.isSet = false; + this.gameState.pinsSet--; + this.updateFeedback("A pin dropped! Too much movement"); + this.updatePinVisuals(); + } + } + + // Animate falling back down + const fallInterval = setInterval(() => { + height -= 0.05; + if (height <= 0) { + height = 0; + clearInterval(fallInterval); + } + pin.currentHeight = height; + this.updatePinVisuals(); + }, 20); + } + + // If we reach max height without setting + if (height >= 1) { + clearInterval(liftInterval); + this.updateFeedback("Pin pushed too far!"); + + // Fall back down + const fallInterval = setInterval(() => { + height -= 0.05; + if (height <= 0) { + height = 0; + clearInterval(fallInterval); + } + pin.currentHeight = height; + this.updatePinVisuals(); + }, 20); + } + + pin.currentHeight = height; + this.updatePinVisuals(); + }, 20); + } + + updateFeedback(message) { + this.feedback.textContent = message; + } + + endGame(success) { + this.gameState.gameActive = false; + + if (success) { + this.container.classList.add('success'); + this.updateFeedback("Lock picked successfully!"); + + // Unlock the object in the game + if (typeof unlockTarget === 'function') { + unlockTarget(this.lockable, this.lockable.type || 'object', this.lockable.layer); + } + } else { + this.container.classList.add('failure'); + this.updateFeedback("Failed to pick the lock"); + } + + // Add close button + const closeBtn = document.createElement('button'); + closeBtn.textContent = "Close"; + closeBtn.className = "minigame-close"; + closeBtn.onclick = () => { + this.complete(success, { lockable: this.lockable }); + }; + this.container.appendChild(closeBtn); + } + + shuffleArray(array) { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + return array; + } } - - // Rearrange inventory items after removal - function rearrangeInventoryItems() { - inventory.items.forEach((item, index) => { - item.x = index * 60 + 100; + + // Register the lockpicking minigame with the framework + MinigameFramework.registerScene('lockpicking', LockpickingMinigame); + + // Replacement for the startLockpickingMinigame function + function startLockpickingMinigame(lockable, scene, difficulty = 'medium') { + // Initialize the framework if not already done + if (!MinigameFramework.mainGameScene) { + MinigameFramework.init(scene); + } + + // Start the lockpicking minigame + MinigameFramework.startMinigame('lockpicking', { + lockable: lockable, + difficulty: difficulty, + onComplete: (success, result) => { + if (success) { + debugLog('LOCKPICK SUCCESS', null, 1); + gameAlert(`Successfully picked the lock!`, 'success', 'Lockpicking', 4000); + } else { + debugLog('LOCKPICK FAILED', null, 2); + gameAlert(`Failed to pick the lock.`, 'error', 'Lockpicking', 4000); + } + } }); }