From 6c06aeafe7281407c2ba6cf0f02a5e9cf11d7a12 Mon Sep 17 00:00:00 2001 From: "Z. Cliffe Schreuders" Date: Mon, 13 Oct 2025 23:45:53 +0100 Subject: [PATCH] Add PIN Minigame: Introduce a new minigame for PIN entry, featuring a digital keypad, attempt logging, and visual feedback for correct and incorrect inputs. Implement CSS styles for the minigame interface and integrate it into the existing framework. Update index.html to include the new CSS file and register the minigame in the minigame manager. Add test page for functionality and ensure compatibility with the pin-cracker item for enhanced gameplay experience. --- assets/objects/pin-cracker-large.png | Bin 0 -> 1370 bytes assets/objects/pin-cracker.png | Bin 0 -> 315 bytes css/pin.css | 444 +++++++++++++++ index.html | 1 + js/core/game.js | 1 + js/minigames/index.js | 8 +- .../lockpicking/lockpicking-game-phaser.js | 38 +- js/minigames/pin/pin-minigame.js | 513 ++++++++++++++++++ js/systems/minigame-starters.js | 56 ++ js/systems/unlock-system.js | 16 +- scenarios/ceo_exfil.json | 6 + test-pin-minigame.html | 422 ++++++++++++++ 12 files changed, 1485 insertions(+), 20 deletions(-) create mode 100644 assets/objects/pin-cracker-large.png create mode 100644 assets/objects/pin-cracker.png create mode 100644 css/pin.css create mode 100644 js/minigames/pin/pin-minigame.js create mode 100644 test-pin-minigame.html diff --git a/assets/objects/pin-cracker-large.png b/assets/objects/pin-cracker-large.png new file mode 100644 index 0000000000000000000000000000000000000000..809759bdc6e9314627d7df528ef6bb0690f9e6ec GIT binary patch literal 1370 zcmV-g1*Q6lP)@@gdkza0@>!0m{XRMgq)J#AxI=- z@uUY2E)9C{peT;oE~q2zii)EMI--J(;xdXe*w*iT{V%FNTTyW6f4zE3RlR!eRn<31 z^YhcI_kxT5u!-15)=kz<8+72{{4RIko1)wI-*;};d$%3@;dz}^q0-gt=x_S8ttuS* zW47s`f^=Dme*fHQRiUr+r)@m>U{Aq;_IV}u@{$2J<@-hM^$m%GpFF>u>nPoHqV#7h zKa|5Biaj!@fUXhHGq2G7&svLOq8%UbyVuU=j30J4kMGVY?4mZJ)<{NVfaW1`%p)Eh zfG5&`DYkCu$w3qCSRS(T)hCPZ|w1(D!4fV^`$xwU*IgNAh0OMn%oc&Zi@;zN;}r zjv8S8rqKmkXW{2BnQ^Fr7yc?cc18|+&s;O5CNL(`oIg2`ku+vW3sIv6s0FN8XX>~A zrbUQ%s#-j({5;F<0X#+yWW!*grM=a89eLtz^k0Cue8-1I8#UC<7aIqhoWwENg;OAbsEkA<&yOj%_%C1M zhbn6ve>fJ8VNoKt^xdLU-r=#0k;Kfy^i>0*8cVoIB|MQMs`R1o0Dj_QBOor^13N!^ z^{(7A88FLC)B5pOCQ&UIQ)^fZ)Yy;$q9j@WtufZM8o;YJmhH7SfLb8$fE=2YZ_1S` zzIApy>2O-0_k$KpW_)dCaf(dfn{W@XQz~B7&uQ>_r>gzoL>e;XQMTk7yGGE0N$<)= zYLkU~fZeidMWAEBdNX)xe9@>Hg}yNJ6MH!cH9OT}*ug{ZhwvC_$BFYm{SFbJVrQ%I`4`Ax&7_zsE})RNM!Q0W9R&zCdDbhLcpu)e$2`E ziNCC~Tdz)8W9*ZRJ`*spA_5f;#YU7#;c*PGkZM5}6sJR}$VP~9?l3AfC=;vYQg_$D|o zuKOp?rauixxwKvLUqG5v`qkSms%v!Ua=IwFCu#_ZE>t}I;DByAgoGX%qiaX{0iqqolf-ta0i{5> zk8N;Dd~hHZN*U8wuR^6rL2Rd+QnGlQWU*7h0V%UII{{$?6d{q)PUpNm)`kxDtT5VB zK4(;mW#=)aZ-N6d^s>62xj zjXL1s;E?rodKSGu#ZHlUQ~Jgn(6{1vL8nNml3XbfWxSkZjynJV0RR8!l0hs0000I_ cL_t&o04yyjsfS-G`~Uy|07*qoM6N<$f=pwO?EnA( literal 0 HcmV?d00001 diff --git a/assets/objects/pin-cracker.png b/assets/objects/pin-cracker.png new file mode 100644 index 0000000000000000000000000000000000000000..6166773cb90f43e502d9bc9abdb3faf0870730c5 GIT binary patch literal 315 zcmV-B0mS}^P)Px#^+`lQR49?1j-hYDP#DC2ZP;kQ)nTU4BqIv~L4sovQGp?V#EpLfg+XA4M<5VL zFeFQ}R8WBcSul_xAf#*!MZGr`TC#A-%a?ce-FK&G$&3#$t?GFD18Tm)vELzDtaBQ5 zQS%i}qb>rWrvGb}^RopCb$bz(UKWo6-1)ekYX>ZUl|7Ff)5}K<5pXWPw`D?lC1P!$OLy`Dz_GeuHIS- zI&@#9`CASEyJ-OMQF;!X(y(((0Di0i0NeQlfJblk=MR9WbUDQf;?FMBUUcOF) + 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 @@ + + + + + + PIN Minigame Test + + + + + + + + + + + + +
+
+

PIN Minigame Test

+
+ Test the PIN minigame with various configurations. The minigame features a digital keypad, + attempt logging, and two types of Mastermind-style feedback: Pin-Cracker item (toggleable) + and Mastermind Mode (automatic via parameter). Both provide visual feedback with green/amber + lights showing correct digits and positions. +
+
+ +
+ + + + + + + + + +
+ +
+

Features

+
    +
  • Digital Display: Shows current input with visual feedback
  • +
  • Number Pad: 0-9 keys with hover effects and animations
  • +
  • Backspace: Remove last entered digit (can be disabled)
  • +
  • Auto-submit: Automatically submits when PIN length is reached
  • +
  • Attempt Logging: Shows all previous attempts with timestamps
  • +
  • Pin-Cracker Item: Toggleable visual feedback with green/amber lights
  • +
  • Mastermind Mode: Automatic visual feedback enabled via parameter
  • +
  • Visual Feedback: Green lights for correct position, amber for correct digit
  • +
  • Keyboard Support: Use number keys, backspace, and enter
  • +
  • Lockout Protection: Locks after maximum attempts
  • +
  • Visual Feedback: Success/error animations and colors
  • +
+
+ +
+

Pin-Cracker Info Leak Mode

+

When you have the pin-cracker item, you can enable visual feedback for each attempt:

+
    +
  • 🟢 Green Light: Correct digit in correct position
  • +
  • 🟡 Amber Light: Correct digit in wrong position
  • +
  • Example 1: PIN "1234", guess "1356" → 2 green lights, 0 amber lights
  • +
  • Example 2: PIN "1123", guess "1111" → 2 green lights, 0 amber lights (duplicates handled correctly)
  • +
  • Example 3: PIN "1234", guess "1123" → 1 green light, 1 amber light
  • +
+
+ +
+

Mastermind Mode

+

Mastermind mode is enabled via parameter and provides automatic visual feedback without needing the pin-cracker item:

+
    +
  • Automatic Feedback: Visual lights appear automatically for each attempt
  • +
  • No Toggle Required: Feedback is always enabled when mastermind mode is active
  • +
  • Same Visual System: Uses the same green/amber light system as pin-cracker
  • +
  • Perfect for Testing: Ideal for scenarios where you want guaranteed feedback
  • +
+
+ +
+
+

Basic Test

+
1234
+
Standard 4-digit PIN with 3 attempts
+
+ +
+

Custom PIN

+
5678
+
Different PIN to test various scenarios
+
+ +
+

Long PIN

+
123456
+
6-digit PIN for extended testing
+
+ +
+

Info Leak

+
9876
+
PIN with Mastermind feedback enabled
+
+ +
+

Duplicate Digits

+
1123
+
Tests proper handling of duplicate digits
+
+ +
+

Pin-Cracker

+
9876
+
Visual feedback with green/amber lights
+
+ +
+

Mastermind Mode

+
2468
+
Automatic visual feedback enabled
+
+
+
+ + + + +