- 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);
+ }
+ }
});
}