- 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;
-
- // 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) {
-
- // 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); }
+ /* Framework base styles */
+ #minigame-container {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 1000;
+ display: none;
+ pointer-events: all;
+ }
+
+ #minigame-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.5);
+ z-index: 1001;
+ pointer-events: all;
+ }
+
+ .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;
+ pointer-events: all;
+ }
+
+ /* Common minigame UI elements */
+ .minigame-close {
+ background: #555;
+ color: white;
+ border: none;
+ padding: 10px 20px;
+ border-radius: 5px;
+ cursor: pointer;
+ margin-top: 10px;
+ align-self: center;
+ }
+
+ .minigame-header {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ background: rgba(30, 30, 30, 0.9);
+ padding: 10px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ z-index: 5;
+ }
+
+ .minigame-header h3 {
+ margin: 0 0 8px 0;
+ color: #fff;
+ text-align: center;
+ }
+
+ .minigame-header p {
+ margin: 5px 0;
+ color: #ccc;
+ text-align: center;
+ font-size: 12px;
+ }
+
+ .minigame-success-message {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ background: rgba(0, 0, 0, 0.9);
+ padding: 20px;
+ border-radius: 5px;
+ color: #0f0;
+ font-size: 20px;
+ text-align: center;
+ z-index: 1003;
+ box-shadow: 0 0 20px rgba(0, 255, 0, 0.3);
+ }
+
+ .minigame-failure-message {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ background: rgba(0, 0, 0, 0.9);
+ padding: 20px;
+ border-radius: 5px;
+ color: #f00;
+ font-size: 20px;
+ text-align: center;
+ z-index: 1003;
+ box-shadow: 0 0 20px rgba(255, 0, 0, 0.3);
+ }
+
+ .minigame-tool-button {
+ background-color: #444;
+ color: white;
+ border: none;
+ border-radius: 3px;
+ padding: 5px 10px;
+ cursor: pointer;
+ opacity: 0.7;
+ transition: opacity 0.2s, background-color 0.2s;
+ }
+
+ .minigame-tool-button.active {
+ opacity: 1;
+ }
+
+ .minigame-progress-container {
+ height: 6px;
+ width: 100%;
+ background: #333;
+ border-radius: 3px;
+ overflow: hidden;
+ }
+
+ .minigame-progress-bar {
+ height: 100%;
+ width: 0%;
+ background: #2ecc71;
+ transition: width 0.3s;
+ }
+
+ .minigame-scene.success {
+ border: 2px solid #33cc33;
+ }
+
+ .minigame-scene.failure {
+ border: 2px solid #cc3333;
}
`;
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';
- }
- }
- }
- }
- });
- }
-
- // 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();
- }
- }
-
- 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;
- }
-
- // 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));
+ // 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;
+ }
+
+ // 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();
}
+ }
+
+ // 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);
+ }
+ });
+
+ // Initialize and start the minigame
+ this.activeMinigame.init();
+ this.activeMinigame.start();
+
+ return true;
+ },
+
+ // 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;
- console.log('Lock picked successfully:', lockable);
+ // Common game state management
+ this.gameState = {
+ isActive: false,
+ isDragging: false,
+ mousePosition: { x: 0, y: 0 },
+ mouseDown: false,
+ mouseButtonsPressed: [false, false, false],
+ keyState: {},
+ success: false,
+ gameComplete: false,
+ startTime: 0,
+ elapsedTime: 0
+ };
- // Create success message
- const successMessage = document.createElement('div');
- successMessage.style.cssText = `
+ // Bind event handlers to the instance
+ this._handleMouseDown = this._handleMouseDown.bind(this);
+ this._handleMouseUp = this._handleMouseUp.bind(this);
+ this._handleMouseMove = this._handleMouseMove.bind(this);
+ this._handleMouseLeave = this._handleMouseLeave.bind(this);
+ this._handleKeyDown = this._handleKeyDown.bind(this);
+ this._handleKeyUp = this._handleKeyUp.bind(this);
+
+ // Store event listeners for cleanup
+ this._eventListeners = [];
+ }
+
+ init() {
+ console.log(`Initializing minigame scene`);
+
+ // Ensure the scene container is visible
+ this.container.style.display = 'flex';
+
+ // Create standard layout elements
+ this.createLayout();
+
+ // Add escape key handler
+ this.escHandler = (e) => {
+ if (e.key === 'Escape' && this.gameState.isActive) {
+ this.complete(false);
+ }
+ };
+ document.addEventListener('keydown', this.escHandler);
+
+ // Setup common event handling
+ this.setupEventHandling();
+
+ // Record start time
+ this.gameState.startTime = Date.now();
+ }
+
+ // Create standard layout for minigames
+ createLayout() {
+ // Create and add the header
+ this.headerElement = document.createElement('div');
+ this.headerElement.className = 'minigame-header';
+ this.container.appendChild(this.headerElement);
+
+ // Create the main game container
+ this.gameContainer = document.createElement('div');
+ this.gameContainer.className = 'minigame-game-container';
+ this.gameContainer.style.cssText = `
+ width: 80%;
+ height: 80%;
+ margin: 70px auto 20px auto;
+ background: #1a1a1a;
+ border-radius: 5px;
+ box-shadow: 0 0 15px rgba(0, 0, 0, 0.5) inset;
+ position: relative;
+ overflow: hidden;
+ `;
+ this.container.appendChild(this.gameContainer);
+
+ // Create message container for notifications, success/failure
+ this.messageContainer = document.createElement('div');
+ this.messageContainer.className = 'minigame-message-container';
+ this.messageContainer.style.cssText = `
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+ z-index: 1000;
+ `;
+ this.container.appendChild(this.messageContainer);
+
+ // Create progress display
+ this.progressContainer = document.createElement('div');
+ this.progressContainer.style.cssText = `
+ position: absolute;
+ bottom: 15px;
+ left: 50%;
+ transform: translateX(-50%);
+ color: white;
+ text-align: center;
+ font-size: 16px;
+ background: rgba(0, 0, 0, 0.6);
+ padding: 5px 15px;
+ border-radius: 15px;
+ z-index: 10;
+ width: 80%;
+ max-width: 500px;
+ `;
+ this.container.appendChild(this.progressContainer);
+
+ // Create standard close button
+ const closeButton = document.createElement('button');
+ closeButton.textContent = '✕';
+ closeButton.style.cssText = `
+ position: absolute;
+ right: 10px;
+ top: 10px;
+ background: none;
+ border: none;
+ color: white;
+ font-size: 20px;
+ cursor: pointer;
+ z-index: 100;
+ `;
+ closeButton.onclick = () => this.complete(false);
+ this.container.appendChild(closeButton);
+ }
+
+ // Set up common event handling for mouse, touch, keyboard
+ setupEventHandling() {
+ // Mouse events
+ this.addEventListenerWithCleanup(this.gameContainer, 'mousedown', this._handleMouseDown);
+ this.addEventListenerWithCleanup(document, 'mouseup', this._handleMouseUp);
+ this.addEventListenerWithCleanup(this.gameContainer, 'mousemove', this._handleMouseMove);
+ this.addEventListenerWithCleanup(this.gameContainer, 'mouseleave', this._handleMouseLeave);
+
+ // Keyboard events
+ this.addEventListenerWithCleanup(document, 'keydown', this._handleKeyDown);
+ this.addEventListenerWithCleanup(document, 'keyup', this._handleKeyUp);
+
+ // Prevent context menu
+ this.addEventListenerWithCleanup(this.gameContainer, 'contextmenu', (e) => e.preventDefault());
+ }
+
+ // Utility to add event listener and track it for cleanup
+ addEventListenerWithCleanup(element, eventType, handler, options) {
+ element.addEventListener(eventType, handler, options);
+ this._eventListeners.push({ element, eventType, handler });
+ }
+
+ // Event handlers
+ _handleMouseDown(e) {
+ console.log("Mouse down in framework handler");
+ this.gameState.mouseDown = true;
+ this.gameState.mouseButtonsPressed[e.button] = true;
+ this.gameState.isDragging = true;
+
+ // Call subclass handler if it exists
+ if (typeof this.handleMouseDown === 'function') {
+ this.handleMouseDown(e);
+ }
+ }
+
+ _handleMouseUp(e) {
+ console.log("Mouse up in framework handler");
+ this.gameState.mouseDown = false;
+ this.gameState.mouseButtonsPressed[e.button] = false;
+ this.gameState.isDragging = false;
+
+ // Call subclass handler if it exists
+ if (typeof this.handleMouseUp === 'function') {
+ this.handleMouseUp(e);
+ }
+ }
+
+ _handleMouseMove(e) {
+ // Update mouse position
+ const rect = this.gameContainer.getBoundingClientRect();
+ this.gameState.mousePosition = {
+ x: e.clientX - rect.left,
+ y: e.clientY - rect.top,
+ clientX: e.clientX,
+ clientY: e.clientY,
+ screenX: e.screenX,
+ screenY: e.screenY
+ };
+
+ // Call subclass handler if it exists
+ if (typeof this.handleMouseMove === 'function') {
+ this.handleMouseMove(e);
+ }
+ }
+
+ _handleMouseLeave(e) {
+ this.gameState.isDragging = false;
+
+ // Call subclass handler if it exists
+ if (typeof this.handleMouseLeave === 'function') {
+ this.handleMouseLeave(e);
+ }
+ }
+
+ _handleKeyDown(e) {
+ this.gameState.keyState[e.code] = true;
+
+ // Call subclass handler if it exists
+ if (typeof this.handleKeyDown === 'function') {
+ this.handleKeyDown(e);
+ }
+ }
+
+ _handleKeyUp(e) {
+ this.gameState.keyState[e.code] = false;
+
+ // Call subclass handler if it exists
+ if (typeof this.handleKeyUp === 'function') {
+ this.handleKeyUp(e);
+ }
+ }
+
+ // Message display system
+ showMessage(message, type = 'info', duration = 0) {
+ // Create a message element
+ const messageElement = document.createElement('div');
+ messageElement.className = `minigame-message minigame-message-${type}`;
+ messageElement.innerHTML = message;
+
+ // Style based on type
+ let styles = `
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;
+ border-radius: 5px;
text-align: center;
- z-index: 1002;
+ z-index: 1001;
+ pointer-events: all;
+ max-width: 90%;
+ transition: opacity 0.3s;
`;
- 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';
+ switch(type) {
+ case 'success':
+ messageElement.className = 'minigame-success-message';
+ break;
+ case 'failure':
+ case 'error':
+ messageElement.className = 'minigame-failure-message';
+ break;
+ default:
+ styles += `
+ background: rgba(0, 0, 0, 0.8);
+ color: white;
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
+ `;
}
- tableData.push({
- 'Binding Order': orderIndex + 1,
- 'Pin #': pinIndex + 1,
- 'Tension Required': tensionNeeded
- });
+ // Apply styles
+ messageElement.style.cssText = styles;
+
+ // Add to the message container
+ this.messageContainer.appendChild(messageElement);
+
+ // Auto-remove after duration, if specified
+ if (duration > 0) {
+ setTimeout(() => {
+ messageElement.style.opacity = '0';
+ setTimeout(() => {
+ if (messageElement.parentNode) {
+ messageElement.parentNode.removeChild(messageElement);
+ }
+ }, 300);
+ }, duration);
+ }
+
+ return messageElement;
}
- console.table(tableData);
+ // Success/failure methods
+ showSuccess(message, autoComplete = true, delay = 2000) {
+ this.gameState.success = true;
+ this.gameState.gameComplete = true;
+ this.container.classList.add('success');
+
+ // Create success message
+ const successMessage = this.showMessage(message, 'success');
+
+ // Auto-complete after delay
+ if (autoComplete) {
+ setTimeout(() => {
+ this.complete(true);
+ }, delay);
+ }
+
+ return successMessage;
+ }
+
+ showFailure(message, autoComplete = true, delay = 2000) {
+ this.gameState.success = false;
+ this.gameState.gameComplete = true;
+ this.container.classList.add('failure');
+
+ // Create failure message
+ const failureMessage = this.showMessage(message, 'failure');
+
+ // Auto-complete after delay
+ if (autoComplete) {
+ setTimeout(() => {
+ this.complete(false);
+ }, delay);
+ }
+
+ return failureMessage;
+ }
+
+ // Progress updates
+ updateProgress(current, total, label = '') {
+ const percentage = Math.min(100, Math.max(0, (current / total) * 100));
+
+ this.progressContainer.innerHTML = `
+ ${label ? `
${label}
` : ''}
+
+
+
+ `;
+ }
+
+ start() {
+ this.gameState.isActive = true;
+ console.log("Minigame started");
+ }
+
+ update(deltaTime) {
+ // Update elapsed time
+ this.gameState.elapsedTime = Date.now() - this.gameState.startTime;
+
+ // Override in subclass for game-specific updates
+ }
+
+ complete(success, result = null) {
+ console.log(`Minigame complete, success: ${success}`);
+ this.gameState.isActive = false;
+ this.gameState.success = success;
+
+ // Remove event listeners
+ document.removeEventListener('keydown', this.escHandler);
+ this.cleanup();
+
+ if (typeof this.params.onComplete === 'function') {
+ this.params.onComplete(success, result);
+ }
+ }
+
+ cleanup() {
+ console.log("Cleaning up minigame");
+
+ // Remove all tracked event listeners
+ this._eventListeners.forEach(({ element, eventType, handler }) => {
+ element.removeEventListener(eventType, handler);
+ });
+ this._eventListeners = [];
+ }
}
+ };
- // 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
+ // 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 = [];
+
+ // Use gameState from the framework but extend it with lockpicking-specific properties
+ this.lockState = {
+ tensionApplied: false,
+ pinsSet: 0,
+ currentPin: null
+ };
}
- }
-
- // removes an item from the inventory
- function removeFromInventory(sprite) {
- if (!sprite || !inventory.items) {
+
+ init() {
+ // Call parent init to set up common components
+ super.init();
+
+ console.log("Lockpicking minigame initializing");
+
+ // Configure container size and layout
+ this.container.style.width = '90%';
+ this.container.style.maxWidth = '500px';
+ this.container.style.padding = '20px';
+ this.container.style.gap = '15px';
+
+ // Set up header content with proper spacing
+ this.headerElement.innerHTML = `
+
Lockpicking
+
Apply tension and hold click on pins to lift them to the shear line
+ `;
+ this.headerElement.style.marginBottom = '30px'; // Add more space below header
+
+ // Add custom styles for the lockpicking minigame if they don't exist
+ if (!document.getElementById('lockpicking-styles')) {
+ const style = document.createElement('style');
+ style.id = 'lockpicking-styles';
+ style.textContent = `
+ /* Game container styles */
+ .minigame-container {
+ padding-top: 80px !important; /* Add padding at top to prevent header overlap */
+ position: relative;
+ }
+
+ .minigame-header {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ z-index: 10;
+ background: rgba(34, 34, 34, 0.95);
+ border-bottom: 1px solid #444;
+ padding: 10px 20px;
+ margin-bottom: 20px;
+ }
+
+ .lock-visual {
+ display: flex;
+ justify-content: space-evenly;
+ align-items: center;
+ gap: 20px;
+ height: 200px;
+ background: #f0e6a6; /* Light yellow/beige background */
+ border-radius: 5px;
+ padding: 25px;
+ position: relative;
+ margin-top: 20px; /* Add top margin for better spacing from header */
+ margin-bottom: 20px;
+ border: 2px solid #887722;
+ z-index: 1; /* Ensure pins are below header */
+ }
+
+ /* Rest of existing CSS */
+ .pin {
+ width: 40px;
+ height: 150px;
+ position: relative;
+ background: transparent;
+ border-radius: 4px 4px 0 0;
+ overflow: visible;
+ cursor: pointer;
+ transition: transform 0.1s;
+ margin: 0 15px;
+ }
+
+ .pin:hover {
+ opacity: 0.9;
+ }
+
+ .shear-line {
+ position: absolute;
+ width: 100%;
+ height: 2px;
+ background: #aa8833;
+ bottom: 70px;
+ z-index: 5;
+ }
+
+ .pin-assembly {
+ position: absolute;
+ bottom: 0;
+ width: 100%;
+ height: 140px;
+ transition: transform 0.05s;
+ }
+
+ .key-pin {
+ position: absolute;
+ bottom: 0;
+ width: 100%;
+ height: 50px; /* Fixed height for all pins */
+ background: #dd3333; /* Red for key pins */
+ border-radius: 0 0 0 0;
+ clip-path: polygon(0 0, 100% 0, 100% 70%, 50% 100%, 0 70%); /* Pointed bottom */
+ transition: transform 0.3s;
+ }
+
+ .driver-pin {
+ position: absolute;
+ width: 100%;
+ height: 70px;
+ background: #3388dd; /* Blue for driver pins */
+ bottom: 50px; /* Position right above key pin */
+ border-radius: 0 0 0 0;
+ transition: transform 0.3s, background-color 0.3s;
+ }
+
+ .spring {
+ position: absolute;
+ bottom: 120px;
+ width: 100%;
+ height: 40px;
+ background: linear-gradient(to bottom,
+ #cccccc 0%, #cccccc 20%,
+ #999999 20%, #999999 25%,
+ #cccccc 25%, #cccccc 40%,
+ #999999 40%, #999999 45%,
+ #cccccc 45%, #cccccc 60%,
+ #999999 60%, #999999 65%,
+ #cccccc 65%, #cccccc 80%,
+ #999999 80%, #999999 85%,
+ #cccccc 85%, #cccccc 100%
+ );
+ transition: transform 0.3s;
+ }
+
+ .pin.binding {
+ box-shadow: 0 0 8px 2px #ffcc00;
+ }
+
+ /* Remove the pin-assembly transform for set pins */
+ .pin.set .pin-assembly {
+ transform: none; /* Reset transform so we can control individual pieces */
+ }
+
+ /* Keep driver pin (blue) above the shear line when set */
+ .pin.set .driver-pin {
+ background: #22aa22; /* Green to indicate set */
+ }
+
+ /* Reset key pin (red) to the bottom when set */
+ .pin.set .key-pin {
+ transform: translateY(0); /* Keep at bottom */
+ }
+
+ .cylinder {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ height: 30px;
+ background: #ddbb77;
+ border-radius: 5px;
+ margin-top: 5px;
+ position: relative;
+ z-index: 0;
+ border: 2px solid #887722;
+ }
+
+ .cylinder-inner {
+ width: 80%;
+ height: 20px;
+ background: #ccaa66;
+ border-radius: 3px;
+ transform-origin: center;
+ transition: transform 0.3s;
+ }
+
+ .cylinder.rotated .cylinder-inner {
+ transform: rotate(15deg);
+ }
+
+ .lockpick-feedback {
+ padding: 15px;
+ background: #333;
+ border-radius: 5px;
+ text-align: center;
+ min-height: 30px;
+ margin-top: 20px;
+ font-size: 16px;
+ }
+
+ .tension-control {
+ display: grid;
+ grid-template-columns: auto 1fr;
+ gap: 20px;
+ align-items: center;
+ background: #333;
+ padding: 20px;
+ border-radius: 5px;
+ margin-top: 20px;
+ }
+
+ .tension-wrench-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 10px;
+ position: relative;
+ width: 150px;
+ height: 60px;
+ }
+
+ .tension-track {
+ width: 100%;
+ height: 10px;
+ background: #444;
+ border-radius: 5px;
+ position: relative;
+ overflow: hidden;
+ }
+
+ .tension-progress {
+ position: absolute;
+ height: 100%;
+ width: 0%;
+ background: linear-gradient(to right, #666, #2196F3);
+ transition: width 0.3s;
+ }
+
+ .tension-status {
+ font-size: 16px;
+ text-align: left;
+ padding-left: 10px;
+ }
+
+ .tension-wrench {
+ width: 60px;
+ height: 40px;
+ background: #666;
+ border-radius: 4px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: transform 0.3s, background-color 0.3s;
+ position: absolute;
+ left: 0;
+ top: 20px;
+ z-index: 2;
+ box-shadow: 0 2px 5px rgba(0,0,0,0.3);
+ }
+
+ .tension-wrench:hover {
+ background: #777;
+ }
+
+ .tension-wrench.active {
+ background: #2196F3;
+ }
+
+ .wrench-handle {
+ width: 60%;
+ height: 10px;
+ background: #999;
+ position: absolute;
+ }
+
+ .wrench-tip {
+ width: 20px;
+ height: 30px;
+ background: #999;
+ position: absolute;
+ left: 5px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+
+ // Add a class to the container for positioning
+ this.container.classList.add('minigame-container');
+ this.headerElement.classList.add('minigame-header');
+
+ // Replace the game container with custom lockpicking interface
+ this.setupLockpickingInterface();
+
+ // Create pins with random binding order
+ this.createPins();
+
+ // Update the progress display with lockpicking instructions
+ this.updateFeedback("Apply tension first, then click and hold on pins to lift them");
+ }
+
+ setupLockpickingInterface() {
+ // Remove default game container (we'll use custom layout)
+ if (this.gameContainer.parentNode) {
+ this.gameContainer.parentNode.removeChild(this.gameContainer);
+ }
+
+ // Create the content wrapper with padding to avoid header overlap
+ const contentWrapper = document.createElement('div');
+ contentWrapper.style.paddingTop = '10px';
+ this.container.appendChild(contentWrapper);
+ this.contentWrapper = contentWrapper;
+
+ // Create instructions
+ const instructions = document.createElement('div');
+ instructions.className = 'instructions';
+ instructions.textContent = 'Apply tension first, then click and hold on pins to lift them to the shear line';
+ contentWrapper.appendChild(instructions);
+
+ // Create the lock visual container
+ const lockVisual = document.createElement('div');
+ lockVisual.className = 'lock-visual';
+ contentWrapper.appendChild(lockVisual);
+ this.lockVisual = lockVisual;
+
+ // Remove cylinder creation - it's no longer needed
+
+ // Add tension toggle control with horizontal movement
+ const tensionControl = document.createElement('div');
+ tensionControl.className = 'tension-control';
+
+ const wrenchContainer = document.createElement('div');
+ wrenchContainer.className = 'tension-wrench-container';
+
+ const wrenchLabel = document.createElement('div');
+ wrenchLabel.textContent = 'Tension Wrench';
+ wrenchLabel.style.fontSize = '14px';
+ wrenchContainer.appendChild(wrenchLabel);
+
+ // Add tension track and progress
+ const tensionTrack = document.createElement('div');
+ tensionTrack.className = 'tension-track';
+
+ const tensionProgress = document.createElement('div');
+ tensionProgress.className = 'tension-progress';
+ tensionTrack.appendChild(tensionProgress);
+
+ wrenchContainer.appendChild(tensionTrack);
+
+ const tensionWrench = document.createElement('div');
+ tensionWrench.className = 'tension-wrench';
+ tensionWrench.innerHTML = `
+
+
+ `;
+ wrenchContainer.appendChild(tensionWrench);
+
+ tensionControl.appendChild(wrenchContainer);
+
+ const tensionStatus = document.createElement('div');
+ tensionStatus.className = 'tension-status';
+ tensionStatus.textContent = 'Click wrench to apply tension';
+ tensionControl.appendChild(tensionStatus);
+
+ this.contentWrapper.appendChild(tensionControl);
+
+ // Feedback area
+ const feedback = document.createElement('div');
+ feedback.className = 'lockpick-feedback';
+ this.contentWrapper.appendChild(feedback);
+ this.feedback = feedback;
+
+ // Set up tension wrench interaction with horizontal movement
+ tensionWrench.addEventListener('click', () => {
+ this.lockState.tensionApplied = !this.lockState.tensionApplied;
+ tensionWrench.classList.toggle('active', this.lockState.tensionApplied);
+
+ // Move wrench horizontally instead of rotating
+ if (this.lockState.tensionApplied) {
+ // Move to initial right position (25%)
+ this.updateTensionPosition(25);
+ } else {
+ // Return to left position (0%)
+ this.updateTensionPosition(0);
+
+ // Reset progress fill
+ tensionProgress.style.width = '0%';
+ }
+
+ // Update status text
+ tensionStatus.textContent = this.lockState.tensionApplied ?
+ 'Tension applied - now lift pins' : 'Click wrench to apply tension';
+
+ // Update which pins are binding
+ this.updateBindingPins();
+
+ // If tension is toggled off, reset any unset pins
+ if (!this.lockState.tensionApplied) {
+ this.pins.forEach(pin => {
+ if (!pin.isSet) {
+ pin.currentHeight = 0;
+ this.updatePinVisual(pin);
+ }
+ });
+ this.updateFeedback("Tension released - apply tension before lifting pins");
+ } else {
+ this.updateFeedback("Tension applied - click and hold on pins to lift them");
+ }
+ });
+
+ // Store references
+ this.tensionWrench = tensionWrench;
+ this.tensionStatus = tensionStatus;
+ this.tensionProgress = tensionProgress;
+ }
+
+ // New method to update the tension wrench position
+ updateTensionPosition(percentage) {
+ if (percentage < 0) percentage = 0;
+ if (percentage > 100) percentage = 100;
+
+ // Calculate position based on container width
+ const containerWidth = this.tensionWrench.parentElement.offsetWidth;
+ const wrenchWidth = this.tensionWrench.offsetWidth;
+ const maxOffset = containerWidth - wrenchWidth;
+ const position = (maxOffset * percentage) / 100;
+
+ // Update wrench position
+ this.tensionWrench.style.transform = `translateX(${position}px)`;
+
+ // Update progress bar fill
+ if (this.tensionProgress) {
+ this.tensionProgress.style.width = `${percentage}%`;
+ }
+ }
+
+ createPins() {
+ // Generate 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;
+ this.lockVisual.appendChild(pinElement);
+
+ // Create shear line
+ const shearLine = document.createElement('div');
+ shearLine.className = 'shear-line';
+ pinElement.appendChild(shearLine);
+
+ // Create pin assembly container
+ const pinAssembly = document.createElement('div');
+ pinAssembly.className = 'pin-assembly';
+ pinElement.appendChild(pinAssembly);
+
+ // Create key pin (bottom pin) with varying height
+ const keyPin = document.createElement('div');
+ keyPin.className = 'key-pin';
+ pinAssembly.appendChild(keyPin);
+
+ // Generate random key pin height (30px to 60px)
+ const keyPinHeight = Math.floor(Math.random() * 31) + 30;
+ keyPin.style.height = `${keyPinHeight}px`;
+
+ // Create driver pin (top pin) with consistent height
+ const driverPin = document.createElement('div');
+ driverPin.className = 'driver-pin';
+ pinAssembly.appendChild(driverPin);
+ // Position driver pin right above the key pin
+ driverPin.style.bottom = `${keyPinHeight}px`;
+
+ // Create spring
+ const spring = document.createElement('div');
+ spring.className = 'spring';
+ pinAssembly.appendChild(spring);
+ // Position spring above driver pin
+ spring.style.bottom = `${keyPinHeight + 70}px`; // 70px is driver pin height
+
+ // Calculate the distance from the bottom of the pin to the shear line (70px from bottom)
+ const distanceToShearLine = 70 - keyPinHeight;
+
+ // Store pin data
+ const pin = {
+ index: i,
+ binding: bindingOrder.indexOf(i),
+ keyPinHeight: keyPinHeight,
+ distanceToShearLine: distanceToShearLine,
+ currentHeight: 0, // How high the pin is currently lifted (0-1 scale)
+ isSet: false,
+ resistance: Math.random() * 0.02 + 0.01,
+ elements: {
+ container: pinElement,
+ assembly: pinAssembly,
+ keyPin: keyPin,
+ driverPin: driverPin,
+ spring: spring
+ }
+ };
+
+ this.pins.push(pin);
+
+ // Fix: Use an arrow function to preserve 'this' context
+ // and define the handler inline instead of referencing this.handlePinMouseDown
+ const self = this; // Store reference to 'this'
+ this.addEventListenerWithCleanup(pinElement, 'mousedown', function(e) {
+ // Skip if game is not active or pin is already set
+ if (!self.gameState.isActive || pin.isSet) return;
+
+ // Only proceed if tension is applied
+ if (!self.lockState.tensionApplied) {
+ self.updateFeedback("Apply tension first by toggling the wrench");
+ return;
+ }
+
+ // Play a sound effect when interacting with pins
+ if (typeof self.playSound === 'function') {
+ self.playSound('pin_click');
+ }
+
+ // Start lifting the pin
+ self.lockState.currentPin = pin;
+ self.gameState.mouseDown = true;
+ self.liftPin();
+
+ // Add mouse up listener to document
+ const mouseUpHandler = function() {
+ self.gameState.mouseDown = false;
+ self.checkPinSet(self.lockState.currentPin);
+ self.lockState.currentPin = null;
+ document.removeEventListener('mouseup', mouseUpHandler);
+ };
+
+ document.addEventListener('mouseup', mouseUpHandler);
+
+ // Prevent text selection
+ e.preventDefault();
+ });
+ }
+ }
+
+ // Check if a pin should be set or dropped
+ checkPinSet(pin) {
+ if (!this.lockState.tensionApplied || !this.shouldPinBind(pin)) {
+ // Define dropPin function inline since it's not being found
+ this.animatePinDrop(pin);
+ return;
+ }
+
+ // Calculate current pin height in pixels
+ const currentLiftInPixels = pin.currentHeight * pin.distanceToShearLine;
+
+ // Check if the top of the key pin (or bottom of driver pin) is exactly at the shear line
+ // Allow a small tolerance of 2 pixels
+ const tolerance = 2;
+ const isAtShearLine = Math.abs(currentLiftInPixels - pin.distanceToShearLine) <= tolerance;
+
+ if (isAtShearLine) {
+ // Pin set successfully!
+ pin.isSet = true;
+ this.lockState.pinsSet++;
+
+ // Play a satisfying click sound when pin sets
+ if (typeof this.playSound === 'function') {
+ this.playSound('pin_set');
+ }
+
+ // First reset the assembly position
+ pin.elements.assembly.style.transform = 'none';
+
+ // Calculate exact position for the pin junction to be at the shear line
+ const exactLift = pin.distanceToShearLine;
+ pin.elements.assembly.style.transform = `translateY(-${exactLift}px)`;
+
+ // Mark the pin as set
+ pin.elements.container.classList.add('set');
+
+ // Change color of the driver pin to green
+ pin.elements.driverPin.style.backgroundColor = '#22aa22';
+
+ this.updateFeedback(`Pin set at the shear line! (${this.lockState.pinsSet}/${this.pinCount})`);
+
+ // Move the tension wrench further right based on progress
+ const progressPercentage = 25 + (this.lockState.pinsSet / this.pinCount * 75);
+ this.updateTensionPosition(progressPercentage);
+
+ // Update progress
+ this.updateProgress(this.lockState.pinsSet, this.pinCount);
+
+ // Check if all pins are set
+ if (this.lockState.pinsSet === this.pinCount) {
+ this.lockPickingSuccess();
+ return;
+ }
+
+ // Update which pin is binding next
+ this.updateBindingPins();
+ } else {
+ // Pin not at the correct height, drops back down
+ this.animatePinDrop(pin);
+
+ if (currentLiftInPixels > pin.distanceToShearLine) {
+ this.updateFeedback("Pin was pushed too far past the shear line");
+ } else {
+ this.updateFeedback("Pin wasn't lifted high enough to reach the shear line");
+ }
+ }
+ }
+
+ // Define the animatePinDrop method to replace the missing dropPin method
+ animatePinDrop(pin) {
+ // Don't drop pins that are already set
+ if (pin.isSet) return;
+
+ // Calculate drop speed based on how high the pin is
+ const dropSpeed = 0.05 + (pin.currentHeight * 0.1);
+
+ const dropInterval = setInterval(() => {
+ pin.currentHeight -= dropSpeed;
+
+ if (pin.currentHeight <= 0) {
+ pin.currentHeight = 0;
+ clearInterval(dropInterval);
+ }
+
+ this.updatePinVisual(pin);
+ }, 10);
+ }
+
+ // Update pin visual based on current height
+ updatePinVisual(pin) {
+ // Skip visualization update if the pin is set
+ if (pin.isSet) return;
+
+ // Calculate the lift in pixels based on the current progress (0-1) times the distance to the shear line
+ const translateY = pin.currentHeight * pin.distanceToShearLine * -1; // Negative because we're moving up
+
+ // Move the entire pin assembly up
+ pin.elements.assembly.style.transform = `translateY(${translateY}px)`;
+ }
+
+ // Pin-lifting logic with realistic physics
+ liftPin() {
+ if (!this.lockState.currentPin || !this.gameState.isActive ||
+ !this.lockState.tensionApplied || !this.gameState.mouseDown) {
+ return;
+ }
+
+ const pin = this.lockState.currentPin;
+
+ // Add realistic resistance based on binding state
+ let liftAmount = 0;
+
+ // Only binding pins can be lifted effectively
+ if (!this.shouldPinBind(pin)) {
+ // Non-binding pins can be lifted, but with resistance and limited height
+ liftAmount = 0.01;
+ if (pin.currentHeight > 0.3) {
+ liftAmount = 0.005; // Increased resistance at higher positions
+ }
+ } else {
+ // Binding pins lift more smoothly but still have some resistance
+ liftAmount = 0.03 - (pin.resistance * pin.currentHeight);
+
+ // Add slight random variation to simulate realistic feel
+ liftAmount += (Math.random() * 0.01 - 0.005);
+ }
+
+ // Update pin height
+ pin.currentHeight += liftAmount;
+
+ // Cap at maximum height
+ if (pin.currentHeight > 1.2) { // Allow overshooting the shear line a bit
+ pin.currentHeight = 1.2;
+ }
+
+ // Update visual
+ this.updatePinVisual(pin);
+
+ // Add subtle feedback when pin is near the shear line
+ const currentLiftInPixels = pin.currentHeight * pin.distanceToShearLine;
+ const distanceToShearLine = Math.abs(currentLiftInPixels - pin.distanceToShearLine);
+
+ if (distanceToShearLine < 5) {
+ // Pin is close to the shear line
+ pin.elements.container.style.boxShadow = "0 0 5px #ffffff";
+ } else {
+ pin.elements.container.style.boxShadow = "";
+ }
+
+ // Continue lifting while mouse is down
+ if (this.gameState.mouseDown) {
+ requestAnimationFrame(() => this.liftPin());
+ }
+ }
+
+ // Check if a pin should bind based on binding order
+ shouldPinBind(pin) {
+ if (!this.lockState.tensionApplied) 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;
}
- try {
- // Find the index of the sprite in the inventory
- const index = inventory.items.indexOf(sprite);
+ // Update feedback text
+ updateFeedback(message) {
+ this.feedback.textContent = message;
+ }
+
+ // Handle successful lockpicking
+ lockPickingSuccess() {
+ // Disable game interaction
+ this.gameState.isActive = false;
- if (index === -1) {
- return false; // Item not found in inventory
+ // Update UI
+ this.updateFeedback("Lock picked successfully!");
+
+ // Unlock the object in the game
+ if (this.lockable) {
+ // Set locked to false
+ this.lockable.locked = false;
+
+ // If it's a scenarioData object, also update that property
+ if (this.lockable.scenarioData) {
+ this.lockable.scenarioData.locked = false;
+ }
+
+ // Log successful unlock
+ if (typeof debugLog === 'function') {
+ debugLog('LOCKPICK UNLOCK', {
+ object: this.lockable,
+ success: true
+ }, 1);
+ }
}
- // Remove from container
- inventory.container.remove(sprite);
+ // Show success message
+ const successHTML = `
+
+ `;
- // Remove from items array
- inventory.items.splice(index, 1);
+ // Use the framework's success message system
+ this.showSuccess(successHTML, true, 2000);
- // Destroy the sprite
- sprite.destroy();
+ // Store lockable for the result
+ this.gameResult = { lockable: this.lockable };
+ }
+
+ lockPickingFailure() {
+ // Show failure message
+ const failureHTML = `
+
Failed to pick the lock
+
Try again with more careful pin manipulation
+ `;
- // Rearrange remaining items
- rearrangeInventoryItems();
+ // Use the framework's failure message system
+ this.showFailure(failureHTML, true, 2000);
+ }
+
+ start() {
+ super.start();
+ console.log("Lockpicking minigame started");
- // Log the removal
- debugLog('INVENTORY ITEM REMOVED', {
- name: sprite.name,
- totalItems: inventory.items.length
- }, 2);
+ // Initialize game state
+ this.gameState.isActive = true;
+ this.lockState.tensionApplied = false;
+ this.lockState.pinsSet = 0;
- return true;
- } catch (error) {
- console.error('Error removing item from inventory:', error);
- return false;
+ // Initialize progress
+ this.updateProgress(0, this.pinCount);
+ }
+
+ complete(success) {
+ // Call parent complete with result
+ super.complete(success, this.gameResult);
+ }
+
+ // Utility function to shuffle an array
+ 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;
+ }
+
+ // Add new method for updating binding pins
+ updateBindingPins() {
+ if (!this.lockState.tensionApplied) {
+ // No binding if no tension
+ this.pins.forEach(pin => {
+ pin.elements.container.classList.remove('binding');
+ });
+ return;
+ }
+
+ // Find the next unset pin in binding order
+ let bindingPinFound = false;
+
+ for (let order = 0; order < this.pinCount; order++) {
+ const nextPin = this.pins.find(p => p.binding === order && !p.isSet);
+ if (nextPin) {
+ // Mark this pin as binding
+ this.pins.forEach(pin => {
+ pin.elements.container.classList.toggle('binding', pin.index === nextPin.index);
+ });
+ bindingPinFound = true;
+ break;
+ }
+ }
+
+ // If no binding pin was found (all pins set), remove binding class from all
+ if (!bindingPinFound) {
+ this.pins.forEach(pin => {
+ pin.elements.container.classList.remove('binding');
+ });
+ }
}
}
-
- // 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', callback) {
+ // 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);
+ callback();
+ } else {
+ debugLog('LOCKPICK FAILED', null, 2);
+ gameAlert(`Failed to pick the lock.`, 'error', 'Lockpicking', 4000);
+ }
+ }
});
}
@@ -5676,34 +6049,7 @@
deviceContent += ``;
deviceContent += `
MAC: ${device.mac}\n${device.details}
`;
- // Add pairing button only if device is nearby and player has a Bluetooth spoofer
- if (device.nearby) {
- // Check if player has a Bluetooth spoofer in inventory
- const spoofer = inventory.items.find(item =>
- item.scenarioData?.type === "bluetooth_spoofer"
- );
-
- if (spoofer) {
- // Check if this device is already paired with (MAC address programmed to spoofer)
- const isPaired = spoofer.scenarioData?.macPaired === device.mac;
- const buttonClass = isPaired ? 'bluetooth-pair-button paired' : 'bluetooth-pair-button';
- const buttonText = isPaired ? 'MAC Address Paired' : 'Pair MAC Address';
-
- deviceContent += ``;
-
- // If device is paired, add a hint about using it to unlock
- if (isPaired) {
- deviceContent += `
- You can now use this MAC address to unlock matching Bluetooth locks
-
`;
-
- // If this is an inventory item, add an unlock button
- if (device.inInventory) {
- deviceContent += ``;
- }
- }
- }
- }
+
deviceContent += `
Last seen: ${formattedDate} ${formattedTime}
`;
@@ -5711,11 +6057,6 @@
// Toggle expanded state when clicked
deviceElement.addEventListener('click', (event) => {
- // Don't toggle if clicking on the pair button
- if (event.target.classList.contains('bluetooth-pair-button') ||
- event.target.closest('.bluetooth-pair-button')) {
- return;
- }
deviceElement.classList.toggle('expanded');
@@ -5775,49 +6116,6 @@
updateBluetoothPanel();
});
});
-
- // Set up global event delegation for the pairing buttons
- document.addEventListener('click', function(event) {
- if (event.target.classList.contains('bluetooth-pair-button')) {
- const mac = event.target.dataset.mac;
- console.log('Attempting to pair with device MAC:', mac);
-
- // Find the device in our list
- const device = bluetoothDevices.find(device => device.mac === mac);
- console.log('Found device:', device);
-
- if (device) {
- attemptPairingWithDevice(mac);
- } else {
- gameAlert("Device not found in Bluetooth devices list.", 'error', 'Pairing Failed', 3000);
- }
-
- event.stopPropagation(); // Prevent device expanding/collapsing when clicking the button
- }
-
- // Handle unlock button clicks
- if (event.target.classList.contains('bluetooth-unlock-button')) {
- const mac = event.target.dataset.mac;
- console.log('Attempting to unlock device MAC:', mac);
-
- // Find the inventory item with this MAC address
- const item = inventory.items.find(item =>
- item.scenarioData?.mac === mac &&
- item.scenarioData?.lockType === "bluetooth" &&
- item.scenarioData?.locked
- );
- console.log('Found inventory item:', item);
-
- if (item) {
- unlockInventoryDeviceByMac(mac);
- } else {
- gameAlert("Device not found in inventory.", 'error', 'Unlock Failed', 3000);
- }
-
- event.stopPropagation(); // Prevent device expanding/collapsing when clicking the button
- }
- });
-
// Initialize Bluetooth count
updateBluetoothCount();
}
@@ -5843,323 +6141,6 @@
}
console.log('Found inventory item to unlock:', item);
-
- // Attempt to spoof the device
- const success = spoofBluetoothDevice(item);
-
- // If successful, remove the device from the bluetoothDevices array
- if (success) {
- // Find the device in the bluetoothDevices array
- const deviceIndex = bluetoothDevices.findIndex(device =>
- device.mac.toLowerCase() === normalizedMac && device.inInventory
- );
-
- // Remove it if found
- if (deviceIndex !== -1) {
- console.log('Removing unlocked device from bluetoothDevices array');
- bluetoothDevices.splice(deviceIndex, 1);
-
- // Update the Bluetooth panel to reflect the changes
- updateBluetoothPanel();
- }
- }
- }
-
- // Function to handle pairing attempts with Bluetooth devices
- function attemptPairingWithDevice(mac) {
- console.log('Attempting to pair with MAC:', mac);
- console.log('All Bluetooth devices:', bluetoothDevices);
-
- // Find the device in our list (case-insensitive comparison)
- const normalizedMac = mac.toLowerCase();
- const device = bluetoothDevices.find(device => device.mac.toLowerCase() === normalizedMac);
-
- if (!device) {
- console.error('Device not found with MAC:', mac);
- gameAlert("Device not found.", 'error', 'Pairing Failed', 3000);
- return;
- }
-
- console.log('Found device for pairing:', device);
-
- // Find spoofer in inventory
- const spoofer = inventory.items.find(item =>
- item.scenarioData?.type === "bluetooth_spoofer"
- );
-
- if (!spoofer) {
- gameAlert("You need a Bluetooth spoofer to pair with this device.", 'warning', 'Spoofer Required', 3000);
- return;
- }
-
- // Check if player is close enough to the device (using the proximity from real-time scanning)
- // Skip proximity check for inventory items
- if (!device.nearby && !device.inInventory) {
- gameAlert("You need to be closer to the device to pair with it.", 'warning', 'Too Far', 3000);
- return;
- }
-
- // Check if this device is already paired with
- if (spoofer.scenarioData?.macPaired && spoofer.scenarioData.macPaired.toLowerCase() === normalizedMac) {
- gameAlert(`This device's MAC address (${mac}) is already programmed into your spoofer.`, 'info', 'Already Paired', 3000);
- return;
- }
-
- // Launch the MAC address pairing minigame
- startBluetoothPairingMinigame(device);
- }
-
- // Bluetooth MAC Address Pairing Minigame
- function startBluetoothPairingMinigame(device) {
- // Create minigame container
- const minigameContainer = document.createElement('div');
- minigameContainer.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;
- display: flex;
- flex-direction: column;
- align-items: center;
- `;
-
- // Add instructions
- const instructions = document.createElement('div');
- instructions.innerHTML = `
-
MAC Address Spoofing
-
- Align the signal frequencies to match the target device's MAC address pattern.
- Drag the sliders to adjust each frequency band until all segments turn green.
- When all segments are aligned, the MAC address will be successfully paired.
-