diff --git a/assets/objects/pin-cracker-large.png b/assets/objects/pin-cracker-large.png new file mode 100644 index 0000000..809759b Binary files /dev/null and b/assets/objects/pin-cracker-large.png differ diff --git a/assets/objects/pin-cracker.png b/assets/objects/pin-cracker.png new file mode 100644 index 0000000..6166773 Binary files /dev/null and b/assets/objects/pin-cracker.png differ diff --git a/css/pin.css b/css/pin.css new file mode 100644 index 0000000..a05e8f5 --- /dev/null +++ b/css/pin.css @@ -0,0 +1,444 @@ +/* PIN Minigame Styles */ + +.pin-minigame-container { + background: linear-gradient(135deg, #1a1a2e, #16213e); + border: 2px solid #0f3460; + box-shadow: 0 0 30px rgba(15, 52, 96, 0.3); +} + +.pin-minigame-game-container { + background: linear-gradient(145deg, #0f0f23, #1a1a2e); + border: 1px solid #0f3460; + box-shadow: + 0 0 20px rgba(0, 0, 0, 0.8) inset, + 0 0 10px rgba(15, 52, 96, 0.2); + padding: 20px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 20px; +} + +.pin-minigame-interface { + display: flex; + flex-direction: column; + align-items: center; + gap: 25px; + width: 100%; + max-width: 400px; +} + +/* Digital Display */ +.pin-minigame-display-container { + width: 100%; + display: flex; + justify-content: center; + margin-bottom: 10px; +} + +.pin-minigame-display { + font-family: 'Courier New', monospace; + font-size: 48px; + font-weight: bold; + color: #00ff41; + background: #000; + border: 3px solid #00ff41; + border-radius: 8px; + padding: 15px 25px; + text-align: center; + letter-spacing: 8px; + min-width: 200px; + box-shadow: + 0 0 20px rgba(0, 255, 65, 0.3), + inset 0 0 10px rgba(0, 0, 0, 0.8); + transition: all 0.3s ease; +} + +.pin-minigame-display.has-input { + box-shadow: + 0 0 25px rgba(0, 255, 65, 0.5), + inset 0 0 15px rgba(0, 0, 0, 0.9); +} + +.pin-minigame-display.success { + color: #00ff00; + border-color: #00ff00; + box-shadow: + 0 0 30px rgba(0, 255, 0, 0.7), + inset 0 0 20px rgba(0, 0, 0, 0.9); + animation: successPulse 0.5s ease-in-out; +} + +.pin-minigame-display.locked { + color: #ff4444; + border-color: #ff4444; + box-shadow: + 0 0 30px rgba(255, 68, 68, 0.7), + inset 0 0 20px rgba(0, 0, 0, 0.9); + animation: errorShake 0.5s ease-in-out; +} + +@keyframes successPulse { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.05); } +} + +@keyframes errorShake { + 0%, 100% { transform: translateX(0); } + 25% { transform: translateX(-5px); } + 75% { transform: translateX(5px); } +} + +/* Keypad */ +.pin-minigame-keypad { + display: grid; + grid-template-columns: repeat(3, 1fr); + grid-template-rows: repeat(4, 1fr); + gap: 12px; + width: 100%; + max-width: 300px; + padding: 20px; + background: rgba(0, 0, 0, 0.3); + border-radius: 15px; + border: 1px solid #0f3460; +} + +/* Special positioning for zero button (centered in bottom row) */ +.pin-minigame-key:nth-child(10) { + grid-column: 2; + grid-row: 4; +} + +/* Position backspace button in bottom left */ +.pin-minigame-backspace { + grid-column: 1; + grid-row: 4; +} + +/* Position enter button in bottom right */ +.pin-minigame-enter { + grid-column: 3; + grid-row: 4; +} + +.pin-minigame-key { + background: linear-gradient(145deg, #2c3e50, #34495e); + color: #ecf0f1; + border: 2px solid #0f3460; + border-radius: 8px; + padding: 15px; + font-family: 'Press Start 2P', monospace; + font-size: 18px; + font-weight: bold; + cursor: pointer; + transition: all 0.2s ease; + min-height: 50px; + display: flex; + align-items: center; + justify-content: center; + box-shadow: + 0 4px 8px rgba(0, 0, 0, 0.3), + inset 0 1px 0 rgba(255, 255, 255, 0.1); +} + +.pin-minigame-key:hover { + background: linear-gradient(145deg, #34495e, #2c3e50); + border-color: #00ff41; + box-shadow: + 0 6px 12px rgba(0, 0, 0, 0.4), + 0 0 15px rgba(0, 255, 65, 0.3), + inset 0 1px 0 rgba(255, 255, 255, 0.2); + transform: translateY(-2px); +} + +.pin-minigame-key:active { + transform: translateY(0); + box-shadow: + 0 2px 4px rgba(0, 0, 0, 0.3), + inset 0 1px 0 rgba(255, 255, 255, 0.1); +} + +.pin-minigame-key:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none; +} + +.pin-minigame-key:disabled:hover { + background: linear-gradient(145deg, #2c3e50, #34495e); + border-color: #0f3460; + box-shadow: + 0 4px 8px rgba(0, 0, 0, 0.3), + inset 0 1px 0 rgba(255, 255, 255, 0.1); +} + +/* Special key styles */ +.pin-minigame-backspace { + background: linear-gradient(145deg, #e74c3c, #c0392b); + border-color: #c0392b; + font-size: 16px; +} + +.pin-minigame-backspace:hover { + background: linear-gradient(145deg, #c0392b, #a93226); + border-color: #ff4444; + box-shadow: + 0 6px 12px rgba(0, 0, 0, 0.4), + 0 0 15px rgba(255, 68, 68, 0.3), + inset 0 1px 0 rgba(255, 255, 255, 0.2); +} + +.pin-minigame-enter { + background: linear-gradient(145deg, #27ae60, #2ecc71); + border-color: #27ae60; + font-size: 12px; +} + +.pin-minigame-enter:hover { + background: linear-gradient(145deg, #2ecc71, #27ae60); + border-color: #00ff41; + box-shadow: + 0 6px 12px rgba(0, 0, 0, 0.4), + 0 0 15px rgba(0, 255, 65, 0.3), + inset 0 1px 0 rgba(255, 255, 255, 0.2); +} + +/* Attempts Log */ +.pin-minigame-attempts-container { + width: 100%; + max-width: 350px; + background: rgba(0, 0, 0, 0.4); + border: 1px solid #0f3460; + border-radius: 10px; + padding: 15px; +} + +.pin-minigame-attempts-title { + font-family: 'Press Start 2P', monospace; + font-size: 12px; + color: #00ff41; + margin-bottom: 10px; + text-align: center; +} + +.pin-minigame-attempts-log { + max-height: 150px; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 8px; +} + +.pin-minigame-attempt { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 12px; + border-radius: 6px; + font-family: 'Courier New', monospace; + font-size: 14px; + transition: all 0.3s ease; +} + +.pin-minigame-attempt.correct { + background: rgba(0, 255, 0, 0.1); + border: 1px solid rgba(0, 255, 0, 0.3); + color: #00ff00; +} + +.pin-minigame-attempt.incorrect { + background: rgba(255, 68, 68, 0.1); + border: 1px solid rgba(255, 68, 68, 0.3); + color: #ff4444; +} + +.pin-minigame-attempt-number { + font-weight: bold; + min-width: 25px; +} + +.pin-minigame-attempt-input { + font-weight: bold; + letter-spacing: 2px; + flex: 1; +} + +.pin-minigame-attempt-feedback { + font-size: 12px; + opacity: 0.8; + font-style: italic; +} + +.pin-minigame-attempt-empty { + text-align: center; + color: #666; + font-style: italic; + padding: 20px; +} + +/* Pin-Cracker Info Leak Mode Toggle */ +.pin-minigame-toggle-container { + display: flex; + align-items: center; + gap: 10px; + padding: 10px; + background: rgba(0, 0, 0, 0.2); + border-radius: 8px; + border: 1px solid #0f3460; +} + +.pin-minigame-toggle-label { + font-family: 'Press Start 2P', monospace; + font-size: 10px; + color: #00ff41; + cursor: pointer; + display: flex; + align-items: center; + gap: 8px; +} + +.pin-minigame-cracker-icon { + width: 64px; + height: 64px; + margin-right: 8px; + filter: drop-shadow(0 0 5px rgba(0, 255, 65, 0.5)); + transition: all 0.3s ease; + image-rendering: pixelated; + image-rendering: -moz-crisp-edges; + image-rendering: crisp-edges; +} + +.pin-minigame-cracker-icon:hover { + filter: drop-shadow(0 0 10px rgba(0, 255, 65, 0.8)); + transform: scale(1.1); +} + +.pin-minigame-toggle { + width: 20px; + height: 20px; + cursor: pointer; + accent-color: #00ff41; +} + +/* Visual Feedback Lights */ +.pin-minigame-feedback-lights { + display: flex; + gap: 4px; + margin-left: 10px; + align-items: center; +} + +.pin-minigame-light { + width: 12px; + height: 12px; + border-radius: 50%; + border: 1px solid rgba(255, 255, 255, 0.3); + box-shadow: 0 0 5px rgba(0, 0, 0, 0.5); +} + +.pin-minigame-light-green { + background: #00ff00; + box-shadow: + 0 0 5px rgba(0, 0, 0, 0.5), + 0 0 10px rgba(0, 255, 0, 0.6); + animation: lightPulse 2s ease-in-out infinite; +} + +.pin-minigame-light-amber { + background: #ffaa00; + box-shadow: + 0 0 5px rgba(0, 0, 0, 0.5), + 0 0 10px rgba(255, 170, 0, 0.6); + animation: lightPulse 2s ease-in-out infinite 0.5s; +} + +@keyframes lightPulse { + 0%, 100% { + opacity: 0.7; + transform: scale(1); + } + 50% { + opacity: 1; + transform: scale(1.1); + } +} + +/* Responsive Design */ +@media (max-width: 600px) { + .pin-minigame-display { + font-size: 36px; + letter-spacing: 6px; + padding: 12px 20px; + min-width: 160px; + } + + .pin-minigame-key { + font-size: 16px; + padding: 12px; + min-height: 45px; + } + + .pin-minigame-keypad { + gap: 10px; + padding: 15px; + } + + .pin-minigame-attempts-container { + padding: 12px; + } + + .pin-minigame-attempt { + font-size: 12px; + padding: 6px 10px; + } + + .pin-minigame-cracker-icon { + width: 48px; + height: 48px; + } +} + +@media (max-width: 400px) { + .pin-minigame-display { + font-size: 28px; + letter-spacing: 4px; + padding: 10px 15px; + min-width: 140px; + } + + .pin-minigame-key { + font-size: 14px; + padding: 10px; + min-height: 40px; + } + + .pin-minigame-keypad { + gap: 8px; + padding: 12px; + } + + .pin-minigame-cracker-icon { + width: 40px; + height: 40px; + } +} + +/* Scrollbar styling for attempts log */ +.pin-minigame-attempts-log::-webkit-scrollbar { + width: 6px; +} + +.pin-minigame-attempts-log::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.2); + border-radius: 3px; +} + +.pin-minigame-attempts-log::-webkit-scrollbar-thumb { + background: #0f3460; + border-radius: 3px; +} + +.pin-minigame-attempts-log::-webkit-scrollbar-thumb:hover { + background: #00ff41; +} diff --git a/index.html b/index.html index f1f25eb..1db810c 100644 --- a/index.html +++ b/index.html @@ -40,6 +40,7 @@ + diff --git a/js/core/game.js b/js/core/game.js index 25f7c23..f2f6936 100644 --- a/js/core/game.js +++ b/js/core/game.js @@ -67,6 +67,7 @@ export function preload() { // Load new object sprites from Tiled map tileset // These are the key objects that appear in the new room_reception2.json this.load.image('fingerprint_kit', 'assets/objects/fingerprint_kit.png'); + this.load.image('pin-cracker', 'assets/objects/pin-cracker.png'); this.load.image('bin11', 'assets/objects/bin11.png'); this.load.image('bin10', 'assets/objects/bin10.png'); this.load.image('bin9', 'assets/objects/bin9.png'); diff --git a/js/minigames/index.js b/js/minigames/index.js index 57004da..1cc84ba 100644 --- a/js/minigames/index.js +++ b/js/minigames/index.js @@ -11,6 +11,7 @@ export { BiometricsMinigame, startBiometricsMinigame } from './biometrics/biomet export { LockpickSetMinigame, startLockpickSetMinigame } from './lockpick/lockpick-set-minigame.js'; export { ContainerMinigame, startContainerMinigame, returnToContainerAfterNotes } from './container/container-minigame.js'; export { PhoneMessagesMinigame, returnToPhoneAfterNotes } from './phone/phone-messages-minigame.js'; +export { PinMinigame, startPinMinigame } from './pin/pin-minigame.js'; // Initialize the global minigame framework for backward compatibility import { MinigameFramework } from './framework/minigame-manager.js'; @@ -57,6 +58,9 @@ import { ContainerMinigame, startContainerMinigame, returnToContainerAfterNotes // Import the phone messages minigame import { PhoneMessagesMinigame, returnToPhoneAfterNotes } from './phone/phone-messages-minigame.js'; +// Import the PIN minigame +import { PinMinigame, startPinMinigame } from './pin/pin-minigame.js'; + // Register minigames MinigameFramework.registerScene('lockpicking', LockpickingMinigamePhaser); // Use Phaser version as default MinigameFramework.registerScene('lockpicking-phaser', LockpickingMinigamePhaser); // Keep explicit phaser name @@ -67,6 +71,7 @@ MinigameFramework.registerScene('biometrics', BiometricsMinigame); MinigameFramework.registerScene('lockpick-set', LockpickSetMinigame); MinigameFramework.registerScene('container', ContainerMinigame); MinigameFramework.registerScene('phone-messages', PhoneMessagesMinigame); +MinigameFramework.registerScene('pin', PinMinigame); // Make minigame functions available globally window.startNotesMinigame = startNotesMinigame; @@ -76,4 +81,5 @@ window.startBiometricsMinigame = startBiometricsMinigame; window.startLockpickSetMinigame = startLockpickSetMinigame; window.startContainerMinigame = startContainerMinigame; window.returnToContainerAfterNotes = returnToContainerAfterNotes; -window.returnToPhoneAfterNotes = returnToPhoneAfterNotes; \ No newline at end of file +window.returnToPhoneAfterNotes = returnToPhoneAfterNotes; +window.startPinMinigame = startPinMinigame; \ No newline at end of file diff --git a/js/minigames/lockpicking/lockpicking-game-phaser.js b/js/minigames/lockpicking/lockpicking-game-phaser.js index 079d9df..54709cf 100644 --- a/js/minigames/lockpicking/lockpicking-game-phaser.js +++ b/js/minigames/lockpicking/lockpicking-game-phaser.js @@ -453,7 +453,9 @@ export class LockpickingMinigamePhaser extends MinigameScene { // Play tension sound if (this.sounds.tension) { this.sounds.tension.play(); - navigator.vibrate([50]); + if (typeof navigator !== 'undefined' && navigator.vibrate) { + navigator.vibrate([50]); + } } if (this.lockState.tensionApplied) { @@ -1805,7 +1807,9 @@ export class LockpickingMinigamePhaser extends MinigameScene { // Play success sound if (this.sounds.success) { this.sounds.success.play(); - navigator.vibrate(500); + if (typeof navigator !== 'undefined' && navigator.vibrate) { + navigator.vibrate(500); + } } this.updateFeedback("Key inserted successfully! Lock turning..."); @@ -3067,7 +3071,9 @@ export class LockpickingMinigamePhaser extends MinigameScene { // Play click sound if (this.sounds.click) { this.sounds.click.play(); - navigator.vibrate(50); + if (typeof navigator !== 'undefined' && navigator.vibrate) { + navigator.vibrate(50); + } } // Hide labels on first pin click @@ -3188,7 +3194,9 @@ export class LockpickingMinigamePhaser extends MinigameScene { // Play click sound if (this.sounds.click) { this.sounds.click.play(); - navigator.vibrate(50); + if (typeof navigator !== 'undefined' && navigator.vibrate) { + navigator.vibrate(50); + } } // Hide labels on first pin click @@ -3238,7 +3246,9 @@ export class LockpickingMinigamePhaser extends MinigameScene { // Play tension sound if (this.sounds.tension) { this.sounds.tension.play(); - navigator.vibrate([200]); + if (typeof navigator !== 'undefined' && navigator.vibrate) { + navigator.vibrate([200]); + } } if (this.lockState.tensionApplied) { @@ -3435,7 +3445,9 @@ export class LockpickingMinigamePhaser extends MinigameScene { // Play overpicking sound if (this.sounds.overtension) { this.sounds.overtension.play(); - navigator.vibrate(500); + if (typeof navigator !== 'undefined' && navigator.vibrate) { + navigator.vibrate(500); + } } @@ -3536,7 +3548,9 @@ export class LockpickingMinigamePhaser extends MinigameScene { // Play overpicking sound if (this.sounds.overtension) { this.sounds.overtension.play(); - navigator.vibrate(500); + if (typeof navigator !== 'undefined' && navigator.vibrate) { + navigator.vibrate(500); + } } if (pin.isSet) { @@ -3654,7 +3668,9 @@ export class LockpickingMinigamePhaser extends MinigameScene { this.sounds.click.play(); } if (typeof navigator !== 'undefined' && navigator.vibrate) { - navigator.vibrate(100); + if (typeof navigator !== 'undefined' && navigator.vibrate) { + navigator.vibrate(100); + } } } } else { @@ -3903,8 +3919,10 @@ export class LockpickingMinigamePhaser extends MinigameScene { // Play set sound if (this.sounds.set) { this.sounds.set.play(); + if (typeof navigator !== 'undefined' && navigator.vibrate) { navigator.vibrate(500); } + } this.updateFeedback(`Pin ${pin.index + 1} set! (${this.lockState.pinsSet}/${this.pinCount})`); this.updateBindingPins(); @@ -4132,7 +4150,9 @@ export class LockpickingMinigamePhaser extends MinigameScene { // Play success sound if (this.sounds.success) { this.sounds.success.play(); - navigator.vibrate(500); + if (typeof navigator !== 'undefined' && navigator.vibrate) { + navigator.vibrate(500); + } } this.updateFeedback("Lock picked successfully!"); diff --git a/js/minigames/pin/pin-minigame.js b/js/minigames/pin/pin-minigame.js new file mode 100644 index 0000000..4b9e583 --- /dev/null +++ b/js/minigames/pin/pin-minigame.js @@ -0,0 +1,513 @@ +import { MinigameScene } from '../framework/base-minigame.js'; + +// PIN Minigame Scene implementation +export class PinMinigame extends MinigameScene { + constructor(container, params) { + // Ensure params is defined before calling parent constructor + params = params || {}; + + // Set default title if not provided + if (!params.title) { + params.title = 'PIN Entry'; + } + + // Enable cancel button for PIN minigame + params.showCancel = true; + params.cancelText = 'Cancel'; + + super(container, params); + + // PIN game configuration + this.correctPin = params.correctPin || '1234'; + this.maxAttempts = params.maxAttempts || 3; + this.pinLength = params.pinLength || 4; + this.infoLeakMode = params.infoLeakMode || false; + this.allowBackspace = params.allowBackspace !== false; + this.hasPinCracker = params.hasPinCracker || false; + + // Game state + this.currentInput = ''; + this.attempts = []; + this.attemptCount = 0; + this.isLocked = false; + + // UI elements + this.displayElement = null; + this.keypadElement = null; + this.attemptsLogElement = null; + this.infoLeakToggleElement = null; + this.pinCrackerIconElement = null; + } + + init() { + // Call parent init to set up common components + super.init(); + + console.log("PIN minigame initializing"); + + // Set container dimensions + this.container.className += ' pin-minigame-container'; + + // Clear header content + this.headerElement.innerHTML = ''; + + // Configure game container + this.gameContainer.className += ' pin-minigame-game-container'; + + // Create the PIN interface + this.createPinInterface(); + } + + createPinInterface() { + // Create main interface container + const interfaceContainer = document.createElement('div'); + interfaceContainer.className = 'pin-minigame-interface'; + + // Create digital display + const displayContainer = document.createElement('div'); + displayContainer.className = 'pin-minigame-display-container'; + + this.displayElement = document.createElement('div'); + this.displayElement.className = 'pin-minigame-display'; + this.displayElement.textContent = '____'; + + displayContainer.appendChild(this.displayElement); + interfaceContainer.appendChild(displayContainer); + + // Create keypad + this.keypadElement = document.createElement('div'); + this.keypadElement.className = 'pin-minigame-keypad'; + + // Create number buttons in standard phone keypad layout + // Row 1: 1, 2, 3 + for (let i = 1; i <= 3; i++) { + const button = document.createElement('button'); + button.className = 'pin-minigame-key'; + button.textContent = i.toString(); + button.dataset.number = i.toString(); + button.addEventListener('click', () => this.handleNumberInput(i.toString())); + this.keypadElement.appendChild(button); + } + + // Row 2: 4, 5, 6 + for (let i = 4; i <= 6; i++) { + const button = document.createElement('button'); + button.className = 'pin-minigame-key'; + button.textContent = i.toString(); + button.dataset.number = i.toString(); + button.addEventListener('click', () => this.handleNumberInput(i.toString())); + this.keypadElement.appendChild(button); + } + + // Row 3: 7, 8, 9 + for (let i = 7; i <= 9; i++) { + const button = document.createElement('button'); + button.className = 'pin-minigame-key'; + button.textContent = i.toString(); + button.dataset.number = i.toString(); + button.addEventListener('click', () => this.handleNumberInput(i.toString())); + this.keypadElement.appendChild(button); + } + + // Row 4: 0 (centered) + const zeroButton = document.createElement('button'); + zeroButton.className = 'pin-minigame-key'; + zeroButton.textContent = '0'; + zeroButton.dataset.number = '0'; + zeroButton.addEventListener('click', () => this.handleNumberInput('0')); + this.keypadElement.appendChild(zeroButton); + + // Create backspace button if allowed + if (this.allowBackspace) { + const backspaceButton = document.createElement('button'); + backspaceButton.className = 'pin-minigame-key pin-minigame-backspace'; + backspaceButton.textContent = '⌫'; + backspaceButton.addEventListener('click', () => this.handleBackspace()); + this.keypadElement.appendChild(backspaceButton); + } + + // Create enter/confirm button + const enterButton = document.createElement('button'); + enterButton.className = 'pin-minigame-key pin-minigame-enter'; + enterButton.textContent = 'ENTER'; + enterButton.addEventListener('click', () => this.handleEnter()); + this.keypadElement.appendChild(enterButton); + + interfaceContainer.appendChild(this.keypadElement); + + // Create attempts log + const attemptsContainer = document.createElement('div'); + attemptsContainer.className = 'pin-minigame-attempts-container'; + + const attemptsTitle = document.createElement('div'); + attemptsTitle.className = 'pin-minigame-attempts-title'; + attemptsTitle.textContent = 'Attempts Log:'; + attemptsContainer.appendChild(attemptsTitle); + + this.attemptsLogElement = document.createElement('div'); + this.attemptsLogElement.className = 'pin-minigame-attempts-log'; + attemptsContainer.appendChild(this.attemptsLogElement); + + interfaceContainer.appendChild(attemptsContainer); + + // Create pin-cracker info leak mode toggle (if pin-cracker is available) + if (this.hasPinCracker) { + const toggleContainer = document.createElement('div'); + toggleContainer.className = 'pin-minigame-toggle-container'; + + const toggleLabel = document.createElement('label'); + toggleLabel.className = 'pin-minigame-toggle-label'; + + // Add pin-cracker icon + this.pinCrackerIconElement = document.createElement('img'); + this.pinCrackerIconElement.src = 'assets/objects/pin-cracker.png'; + this.pinCrackerIconElement.alt = 'Pin Cracker'; + this.pinCrackerIconElement.className = 'pin-minigame-cracker-icon'; + this.pinCrackerIconElement.style.display = 'inline-block'; // Show by default when pin-cracker is available + + const toggleText = document.createElement('span'); + toggleText.textContent = 'Pin-Cracker Info Leak:'; + + this.infoLeakToggleElement = document.createElement('input'); + this.infoLeakToggleElement.type = 'checkbox'; + this.infoLeakToggleElement.className = 'pin-minigame-toggle'; + this.infoLeakToggleElement.checked = true; // Start enabled when pin-cracker is available + this.infoLeakToggleElement.addEventListener('change', () => { + this.updateAttemptsDisplay(); + this.updatePinCrackerIcon(); + }); + + toggleLabel.appendChild(this.pinCrackerIconElement); + toggleLabel.appendChild(toggleText); + toggleLabel.appendChild(this.infoLeakToggleElement); + toggleContainer.appendChild(toggleLabel); + interfaceContainer.appendChild(toggleContainer); + } + + // Add interface to game container + this.gameContainer.appendChild(interfaceContainer); + + // Add keyboard support + this.setupKeyboardSupport(); + } + + setupKeyboardSupport() { + const keyHandler = (e) => { + if (!this.gameState.isActive || this.isLocked) return; + + const key = e.key; + + // Handle number keys + if (key >= '0' && key <= '9') { + e.preventDefault(); + this.handleNumberInput(key); + } + // Handle backspace + else if (key === 'Backspace' && this.allowBackspace) { + e.preventDefault(); + this.handleBackspace(); + } + // Handle enter + else if (key === 'Enter') { + e.preventDefault(); + this.handleEnter(); + } + }; + + this.addEventListener(document, 'keydown', keyHandler); + } + + handleNumberInput(number) { + if (this.isLocked || this.currentInput.length >= this.pinLength) { + return; + } + + this.currentInput += number; + this.updateDisplay(); + + // Auto-submit if PIN length is reached + if (this.currentInput.length === this.pinLength) { + setTimeout(() => this.handleEnter(), 300); + } + } + + handleBackspace() { + if (this.isLocked || this.currentInput.length === 0) { + return; + } + + this.currentInput = this.currentInput.slice(0, -1); + this.updateDisplay(); + } + + handleEnter() { + if (this.isLocked || this.currentInput.length !== this.pinLength) { + return; + } + + this.attemptCount++; + const isCorrect = this.currentInput === this.correctPin; + + // Record attempt + const attempt = { + input: this.currentInput, + isCorrect: isCorrect, + timestamp: new Date(), + feedback: (this.infoLeakToggleElement?.checked || this.infoLeakMode) ? this.calculateFeedback(this.currentInput) : null + }; + + this.attempts.push(attempt); + this.updateAttemptsDisplay(); + + if (isCorrect) { + this.handleSuccess(); + } else { + this.handleFailure(); + } + } + + calculateFeedback(input) { + // Mastermind-style feedback: right number in right place, right number in wrong place + // This handles duplicate digits correctly by matching each digit in the secret at most once + + const inputArray = input.split(''); + const correctArray = this.correctPin.split(''); + const usedInput = new Array(inputArray.length).fill(false); + const usedCorrect = new Array(correctArray.length).fill(false); + + let rightPlace = 0; + let rightNumber = 0; + + // First pass: count exact position matches (right place) + for (let i = 0; i < inputArray.length; i++) { + if (inputArray[i] === correctArray[i]) { + rightPlace++; + usedInput[i] = true; + usedCorrect[i] = true; + } + } + + // Second pass: count correct numbers in wrong positions + // Only consider unmatched digits from the input + for (let i = 0; i < inputArray.length; i++) { + if (!usedInput[i]) { + // Look for this digit in unused positions of the correct PIN + for (let j = 0; j < correctArray.length; j++) { + if (!usedCorrect[j] && inputArray[i] === correctArray[j]) { + rightNumber++; + usedCorrect[j] = true; // Mark this position as used + break; // Move to next input digit + } + } + } + } + + return { rightPlace, rightNumber }; + } + + updateDisplay() { + if (!this.displayElement) return; + + let displayText = this.currentInput; + while (displayText.length < this.pinLength) { + displayText += '_'; + } + + this.displayElement.textContent = displayText; + + // Add visual feedback for current input + if (this.currentInput.length > 0) { + this.displayElement.classList.add('has-input'); + } else { + this.displayElement.classList.remove('has-input'); + } + } + + updateAttemptsDisplay() { + if (!this.attemptsLogElement) return; + + this.attemptsLogElement.innerHTML = ''; + + if (this.attempts.length === 0) { + const emptyMessage = document.createElement('div'); + emptyMessage.className = 'pin-minigame-attempt-empty'; + emptyMessage.textContent = 'No attempts yet'; + this.attemptsLogElement.appendChild(emptyMessage); + return; + } + + this.attempts.forEach((attempt, index) => { + const attemptElement = document.createElement('div'); + attemptElement.className = `pin-minigame-attempt ${attempt.isCorrect ? 'correct' : 'incorrect'}`; + + const attemptNumber = document.createElement('span'); + attemptNumber.className = 'pin-minigame-attempt-number'; + attemptNumber.textContent = `${index + 1}.`; + + const attemptInput = document.createElement('span'); + attemptInput.className = 'pin-minigame-attempt-input'; + attemptInput.textContent = attempt.input; + + attemptElement.appendChild(attemptNumber); + attemptElement.appendChild(attemptInput); + + // Add visual feedback lights if pin-cracker is enabled and feedback is available + // OR if mastermind mode is enabled via parameter + if ((this.hasPinCracker && this.infoLeakToggleElement?.checked && attempt.feedback) || + (this.infoLeakMode && attempt.feedback)) { + const feedbackContainer = document.createElement('div'); + feedbackContainer.className = 'pin-minigame-feedback-lights'; + + // Add green lights for right place + for (let i = 0; i < attempt.feedback.rightPlace; i++) { + const greenLight = document.createElement('div'); + greenLight.className = 'pin-minigame-light pin-minigame-light-green'; + greenLight.title = 'Correct digit in correct position'; + feedbackContainer.appendChild(greenLight); + } + + // Add amber lights for wrong place + for (let i = 0; i < attempt.feedback.rightNumber; i++) { + const amberLight = document.createElement('div'); + amberLight.className = 'pin-minigame-light pin-minigame-light-amber'; + amberLight.title = 'Correct digit in wrong position'; + feedbackContainer.appendChild(amberLight); + } + + attemptElement.appendChild(feedbackContainer); + } + + this.attemptsLogElement.appendChild(attemptElement); + }); + } + + updatePinCrackerIcon() { + if (this.pinCrackerIconElement) { + this.pinCrackerIconElement.style.display = this.infoLeakToggleElement?.checked ? 'inline-block' : 'none'; + } + } + + handleSuccess() { + this.isLocked = true; + this.displayElement.classList.add('success'); + this.displayElement.textContent = this.currentInput; + + this.showSuccess('PIN Correct! Access Granted.', true, 2000); + + // Set game result + this.gameResult = { + success: true, + pin: this.currentInput, + attempts: this.attemptCount, + timeToComplete: Date.now() - this.startTime + }; + } + + handleFailure() { + this.currentInput = ''; + this.updateDisplay(); + + if (this.attemptCount >= this.maxAttempts) { + this.isLocked = true; + this.displayElement.classList.add('locked'); + this.displayElement.textContent = 'LOCKED'; + + this.showFailure('Maximum attempts reached. System locked.', true, 3000); + + // Set game result + this.gameResult = { + success: false, + attempts: this.attemptCount, + maxAttemptsReached: true + }; + } else { + // Show temporary failure message + const remainingAttempts = this.maxAttempts - this.attemptCount; + this.showFailure(`Incorrect PIN. ${remainingAttempts} attempt${remainingAttempts > 1 ? 's' : ''} remaining.`, false, 1500); + + // Clear the failure message after delay + setTimeout(() => { + const failureMessage = this.messageContainer.querySelector('.minigame-failure-message'); + if (failureMessage) { + failureMessage.remove(); + } + }, 1500); + } + } + + start() { + super.start(); + console.log("PIN minigame started"); + + this.startTime = Date.now(); + this.updateDisplay(); + this.updateAttemptsDisplay(); + this.updatePinCrackerIcon(); + } + + complete(success) { + // Call parent complete with result + super.complete(success, this.gameResult); + } + + cleanup() { + super.cleanup(); + } +} + +// Export helper function to start the PIN minigame +export function startPinMinigame(correctPin = '1234', options = {}) { + console.log('Starting PIN minigame with:', { correctPin, options }); + + // Check if framework is available + if (!window.MinigameFramework) { + console.error('MinigameFramework not available. Make sure it is properly initialized.'); + return; + } + + // Make sure the minigame is registered + if (!window.MinigameFramework.registeredScenes['pin']) { + window.MinigameFramework.registerScene('pin', PinMinigame); + console.log('PIN minigame registered on demand'); + } + + // Initialize the framework if not already done + if (!window.MinigameFramework.mainGameScene) { + window.MinigameFramework.init(null); + } + + // Start the PIN minigame with proper parameters + const params = { + title: options.title || 'PIN Entry', + correctPin: correctPin, + maxAttempts: options.maxAttempts || 3, + pinLength: options.pinLength || 4, + infoLeakMode: options.infoLeakMode || false, + allowBackspace: options.allowBackspace !== false, + hasPinCracker: options.hasPinCracker || false, + onComplete: (success, result) => { + console.log('PIN minigame completed:', { success, result }); + + if (success) { + if (window.showNotification) { + window.showNotification('PIN entered successfully!', 'success'); + } + } else { + if (window.showNotification) { + window.showNotification('PIN entry failed', 'error'); + } + } + + // Call custom completion callback if provided + if (options.onComplete) { + options.onComplete(success, result); + } + } + }; + + console.log('Starting PIN minigame with params:', params); + window.MinigameFramework.startMinigame('pin', null, params); +} + +// Make the function available globally +window.startPinMinigame = startPinMinigame; diff --git a/js/systems/minigame-starters.js b/js/systems/minigame-starters.js index 30adac1..65b9065 100644 --- a/js/systems/minigame-starters.js +++ b/js/systems/minigame-starters.js @@ -209,7 +209,63 @@ export function startKeySelectionMinigame(lockable, type, playerKeys, requiredKe }, 500); } +export function startPinMinigame(lockable, type, correctPin, callback) { + console.log('Starting PIN minigame for', type, 'with PIN:', correctPin); + + // Initialize the minigame framework if not already done + if (!window.MinigameFramework) { + console.error('MinigameFramework not available'); + // Fallback to simple prompt + const pinInput = prompt(`Enter PIN code:`); + if (pinInput === correctPin) { + console.log('PIN SUCCESS (fallback)'); + window.gameAlert(`Correct PIN! The ${type} is now unlocked.`, 'success', 'PIN Accepted', 4000); + callback(true); + } else if (pinInput !== null) { + console.log('PIN FAIL (fallback)'); + window.gameAlert("Incorrect PIN code.", 'error', 'PIN Rejected', 3000); + callback(false); + } + return; + } + + // Use the advanced minigame framework + if (!window.MinigameFramework.mainGameScene) { + window.MinigameFramework.init(window.game); + } + + // Check if we have a pin-cracker in inventory + const hasPinCracker = window.inventory.items.some(item => + item && item.scenarioData && + item.scenarioData.type === 'pin-cracker' + ); + + console.log('PIN-CRACKER CHECK:', hasPinCracker); + + // Start the PIN minigame + window.MinigameFramework.startMinigame('pin', null, { + title: `Enter PIN for ${type}`, + correctPin: correctPin, + maxAttempts: 3, + pinLength: correctPin.length, + hasPinCracker: hasPinCracker, + allowBackspace: true, + onComplete: (success, result) => { + if (success) { + console.log('PIN MINIGAME SUCCESS'); + window.gameAlert(`Correct PIN! The ${type} is now unlocked.`, 'success', 'PIN Accepted', 4000); + callback(true); + } else { + console.log('PIN MINIGAME FAILED'); + window.gameAlert("Failed to enter correct PIN.", 'error', 'PIN Rejected', 3000); + callback(false); + } + } + }); +} + // Export for global access window.startLockpickingMinigame = startLockpickingMinigame; window.startKeySelectionMinigame = startKeySelectionMinigame; +window.startPinMinigame = startPinMinigame; diff --git a/js/systems/unlock-system.js b/js/systems/unlock-system.js index d6e526b..e3089a3 100644 --- a/js/systems/unlock-system.js +++ b/js/systems/unlock-system.js @@ -10,7 +10,7 @@ import { DOOR_ALIGN_OVERLAP } from '../utils/constants.js'; import { rooms } from '../core/rooms.js'; import { unlockDoor } from './doors.js'; -import { startLockpickingMinigame, startKeySelectionMinigame } from './minigame-starters.js'; +import { startLockpickingMinigame, startKeySelectionMinigame, startPinMinigame } from './minigame-starters.js'; // Helper function to check if two rectangles overlap function boundsOverlap(rect1, rect2) { @@ -97,15 +97,11 @@ export function handleUnlock(lockable, type) { case 'pin': console.log('PIN CODE REQUESTED'); - const pinInput = prompt(`Enter PIN code:`); - if (pinInput === lockRequirements.requires) { - unlockTarget(lockable, type, lockable.layer); - console.log('PIN CODE SUCCESS'); - window.gameAlert(`Correct PIN! The ${type} is now unlocked.`, 'success', 'PIN Accepted', 4000); - } else if (pinInput !== null) { - console.log('PIN CODE FAIL'); - window.gameAlert("Incorrect PIN code.", 'error', 'PIN Rejected', 3000); - } + startPinMinigame(lockable, type, lockRequirements.requires, (success) => { + if (success) { + unlockTarget(lockable, type, lockable.layer); + } + }); break; case 'password': diff --git a/scenarios/ceo_exfil.json b/scenarios/ceo_exfil.json index f1e84ba..22517ca 100644 --- a/scenarios/ceo_exfil.json +++ b/scenarios/ceo_exfil.json @@ -63,6 +63,12 @@ "takeable": true, "key_id": "office1_key:40,35,38,32,10", "observations": "A key to access the office areas" + }, + { + "type": "pin-cracker", + "name": "PIN Cracker", + "takeable": true, + "observations": "A sophisticated device that can analyze PIN entry patterns and provide feedback on attempts" } ] }, diff --git a/test-pin-minigame.html b/test-pin-minigame.html new file mode 100644 index 0000000..c62d1e2 --- /dev/null +++ b/test-pin-minigame.html @@ -0,0 +1,422 @@ + + +
+ + +When you have the pin-cracker item, you can enable visual feedback for each attempt:
+Mastermind mode is enabled via parameter and provides automatic visual feedback without needing the pin-cracker item:
+