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.

This commit is contained in:
Z. Cliffe Schreuders
2025-10-13 23:45:53 +01:00
parent 051b90aaa8
commit 6c06aeafe7
12 changed files with 1485 additions and 20 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 B

444
css/pin.css Normal file
View File

@@ -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;
}

View File

@@ -40,6 +40,7 @@
<link rel="stylesheet" href="css/lockpick-set-minigame.css">
<link rel="stylesheet" href="css/container-minigame.css">
<link rel="stylesheet" href="css/phone.css">
<link rel="stylesheet" href="css/pin.css">
<!-- External JavaScript libraries -->
<script src="https://cdn.jsdelivr.net/npm/phaser@3.60.0/dist/phaser.min.js"></script>

View File

@@ -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');

View File

@@ -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;
window.returnToPhoneAfterNotes = returnToPhoneAfterNotes;
window.startPinMinigame = startPinMinigame;

View File

@@ -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!");

View File

@@ -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;

View File

@@ -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;

View File

@@ -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':

View File

@@ -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"
}
]
},

422
test-pin-minigame.html Normal file
View File

@@ -0,0 +1,422 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PIN Minigame Test</title>
<!-- Load fonts -->
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Courier+New&display=swap" rel="stylesheet">
<!-- Load CSS -->
<link rel="stylesheet" href="css/minigames.css">
<link rel="stylesheet" href="css/pin.css">
<style>
body {
margin: 0;
padding: 20px;
background: linear-gradient(135deg, #0f0f23, #1a1a2e);
color: white;
font-family: 'Press Start 2P', monospace;
min-height: 100vh;
}
.test-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.test-header {
text-align: center;
margin-bottom: 30px;
}
.test-header h1 {
color: #00ff41;
font-size: 24px;
margin-bottom: 10px;
}
.test-description {
color: #ccc;
font-size: 12px;
line-height: 1.6;
margin-bottom: 20px;
}
.test-controls {
display: flex;
flex-wrap: wrap;
gap: 15px;
justify-content: center;
margin-bottom: 30px;
}
.test-button {
background: linear-gradient(145deg, #2c3e50, #34495e);
color: #ecf0f1;
border: 2px solid #0f3460;
border-radius: 8px;
padding: 12px 20px;
font-family: 'Press Start 2P', monospace;
font-size: 10px;
cursor: pointer;
transition: all 0.2s ease;
min-width: 150px;
}
.test-button:hover {
background: linear-gradient(145deg, #34495e, #2c3e50);
border-color: #00ff41;
box-shadow: 0 0 15px rgba(0, 255, 65, 0.3);
}
.test-button:active {
transform: translateY(1px);
}
.test-info {
background: rgba(0, 0, 0, 0.3);
border: 1px solid #0f3460;
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
}
.test-info h3 {
color: #00ff41;
font-size: 14px;
margin-bottom: 15px;
}
.test-info p {
color: #ccc;
font-size: 10px;
line-height: 1.6;
margin-bottom: 10px;
}
.test-info ul {
color: #ccc;
font-size: 10px;
line-height: 1.6;
margin-left: 20px;
}
.test-info li {
margin-bottom: 5px;
}
.pin-examples {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-top: 20px;
}
.pin-example {
background: rgba(0, 0, 0, 0.2);
border: 1px solid #0f3460;
border-radius: 8px;
padding: 15px;
text-align: center;
}
.pin-example h4 {
color: #00ff41;
font-size: 12px;
margin-bottom: 10px;
}
.pin-example .pin-code {
font-family: 'Courier New', monospace;
font-size: 16px;
color: #fff;
background: #000;
padding: 8px;
border-radius: 4px;
margin-bottom: 10px;
letter-spacing: 2px;
}
.pin-example .pin-description {
color: #999;
font-size: 8px;
line-height: 1.4;
}
</style>
</head>
<body>
<div class="test-container">
<div class="test-header">
<h1>PIN Minigame Test</h1>
<div class="test-description">
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.
</div>
</div>
<div class="test-controls">
<button class="test-button" onclick="testBasicPin()">Basic PIN (1234)</button>
<button class="test-button" onclick="testCustomPin()">Custom PIN (5678)</button>
<button class="test-button" onclick="testInfoLeakMode()">With Info Leak Mode</button>
<button class="test-button" onclick="testLongPin()">6-Digit PIN</button>
<button class="test-button" onclick="testLimitedAttempts()">Limited Attempts</button>
<button class="test-button" onclick="testNoBackspace()">No Backspace</button>
<button class="test-button" onclick="testDuplicateDigits()">Duplicate Digits</button>
<button class="test-button" onclick="testWithPinCracker()">With Pin-Cracker</button>
<button class="test-button" onclick="testMastermindMode()">Mastermind Mode</button>
</div>
<div class="test-info">
<h3>Features</h3>
<ul>
<li><strong>Digital Display:</strong> Shows current input with visual feedback</li>
<li><strong>Number Pad:</strong> 0-9 keys with hover effects and animations</li>
<li><strong>Backspace:</strong> Remove last entered digit (can be disabled)</li>
<li><strong>Auto-submit:</strong> Automatically submits when PIN length is reached</li>
<li><strong>Attempt Logging:</strong> Shows all previous attempts with timestamps</li>
<li><strong>Pin-Cracker Item:</strong> Toggleable visual feedback with green/amber lights</li>
<li><strong>Mastermind Mode:</strong> Automatic visual feedback enabled via parameter</li>
<li><strong>Visual Feedback:</strong> Green lights for correct position, amber for correct digit</li>
<li><strong>Keyboard Support:</strong> Use number keys, backspace, and enter</li>
<li><strong>Lockout Protection:</strong> Locks after maximum attempts</li>
<li><strong>Visual Feedback:</strong> Success/error animations and colors</li>
</ul>
</div>
<div class="test-info">
<h3>Pin-Cracker Info Leak Mode</h3>
<p>When you have the pin-cracker item, you can enable visual feedback for each attempt:</p>
<ul>
<li><strong>🟢 Green Light:</strong> Correct digit in correct position</li>
<li><strong>🟡 Amber Light:</strong> Correct digit in wrong position</li>
<li><strong>Example 1:</strong> PIN "1234", guess "1356" → 2 green lights, 0 amber lights</li>
<li><strong>Example 2:</strong> PIN "1123", guess "1111" → 2 green lights, 0 amber lights (duplicates handled correctly)</li>
<li><strong>Example 3:</strong> PIN "1234", guess "1123" → 1 green light, 1 amber light</li>
</ul>
</div>
<div class="test-info">
<h3>Mastermind Mode</h3>
<p>Mastermind mode is enabled via parameter and provides automatic visual feedback without needing the pin-cracker item:</p>
<ul>
<li><strong>Automatic Feedback:</strong> Visual lights appear automatically for each attempt</li>
<li><strong>No Toggle Required:</strong> Feedback is always enabled when mastermind mode is active</li>
<li><strong>Same Visual System:</strong> Uses the same green/amber light system as pin-cracker</li>
<li><strong>Perfect for Testing:</strong> Ideal for scenarios where you want guaranteed feedback</li>
</ul>
</div>
<div class="pin-examples">
<div class="pin-example">
<h4>Basic Test</h4>
<div class="pin-code">1234</div>
<div class="pin-description">Standard 4-digit PIN with 3 attempts</div>
</div>
<div class="pin-example">
<h4>Custom PIN</h4>
<div class="pin-code">5678</div>
<div class="pin-description">Different PIN to test various scenarios</div>
</div>
<div class="pin-example">
<h4>Long PIN</h4>
<div class="pin-code">123456</div>
<div class="pin-description">6-digit PIN for extended testing</div>
</div>
<div class="pin-example">
<h4>Info Leak</h4>
<div class="pin-code">9876</div>
<div class="pin-description">PIN with Mastermind feedback enabled</div>
</div>
<div class="pin-example">
<h4>Duplicate Digits</h4>
<div class="pin-code">1123</div>
<div class="pin-description">Tests proper handling of duplicate digits</div>
</div>
<div class="pin-example">
<h4>Pin-Cracker</h4>
<div class="pin-code">9876</div>
<div class="pin-description">Visual feedback with green/amber lights</div>
</div>
<div class="pin-example">
<h4>Mastermind Mode</h4>
<div class="pin-code">2468</div>
<div class="pin-description">Automatic visual feedback enabled</div>
</div>
</div>
</div>
<!-- Load the minigame framework -->
<script type="module">
import { MinigameFramework } from './js/minigames/framework/minigame-manager.js';
import { PinMinigame, startPinMinigame } from './js/minigames/pin/pin-minigame.js';
// Initialize the framework
MinigameFramework.init(null);
// Register the PIN minigame
MinigameFramework.registerScene('pin', PinMinigame);
// Make the framework available globally
window.MinigameFramework = MinigameFramework;
// Make functions available globally for testing
window.startPinMinigame = startPinMinigame;
// Test functions
window.testBasicPin = function() {
console.log('Testing basic PIN minigame');
if (!window.MinigameFramework) {
console.error('MinigameFramework not available');
return;
}
startPinMinigame('1234', {
title: 'Basic PIN Test',
maxAttempts: 3,
pinLength: 4,
infoLeakMode: true,
allowBackspace: true
});
};
window.testCustomPin = function() {
console.log('Testing custom PIN minigame');
if (!window.MinigameFramework) {
console.error('MinigameFramework not available');
return;
}
startPinMinigame('5678', {
title: 'Custom PIN Test',
maxAttempts: 3,
pinLength: 4,
infoLeakMode: true,
allowBackspace: true
});
};
window.testInfoLeakMode = function() {
console.log('Testing PIN minigame with info leak mode');
if (!window.MinigameFramework) {
console.error('MinigameFramework not available');
return;
}
startPinMinigame('9876', {
title: 'Info Leak Mode Test',
maxAttempts: 5,
pinLength: 4,
infoLeakMode: true,
allowBackspace: true
});
};
window.testLongPin = function() {
console.log('Testing 6-digit PIN minigame');
if (!window.MinigameFramework) {
console.error('MinigameFramework not available');
return;
}
startPinMinigame('123456', {
title: '6-Digit PIN Test',
maxAttempts: 4,
pinLength: 6,
infoLeakMode: true,
allowBackspace: true
});
};
window.testLimitedAttempts = function() {
console.log('Testing PIN minigame with limited attempts');
if (!window.MinigameFramework) {
console.error('MinigameFramework not available');
return;
}
startPinMinigame('2468', {
title: 'Limited Attempts Test',
maxAttempts: 2,
pinLength: 4,
infoLeakMode: true,
allowBackspace: true
});
};
window.testNoBackspace = function() {
console.log('Testing PIN minigame without backspace');
if (!window.MinigameFramework) {
console.error('MinigameFramework not available');
return;
}
startPinMinigame('1357', {
title: 'No Backspace Test',
maxAttempts: 3,
pinLength: 4,
infoLeakMode: true,
allowBackspace: false
});
};
window.testDuplicateDigits = function() {
console.log('Testing PIN minigame with duplicate digits');
if (!window.MinigameFramework) {
console.error('MinigameFramework not available');
return;
}
startPinMinigame('1123', {
title: 'Duplicate Digits Test',
maxAttempts: 5,
pinLength: 4,
infoLeakMode: true,
allowBackspace: true
});
};
window.testWithPinCracker = function() {
console.log('Testing PIN minigame with pin-cracker');
if (!window.MinigameFramework) {
console.error('MinigameFramework not available');
return;
}
startPinMinigame('9876', {
title: 'Pin-Cracker Test',
maxAttempts: 5,
pinLength: 4,
hasPinCracker: true,
allowBackspace: true
});
};
window.testMastermindMode = function() {
console.log('Testing PIN minigame with mastermind mode');
if (!window.MinigameFramework) {
console.error('MinigameFramework not available');
return;
}
startPinMinigame('2468', {
title: 'Mastermind Mode Test',
maxAttempts: 5,
pinLength: 4,
infoLeakMode: true,
allowBackspace: true
});
};
console.log('PIN minigame test page loaded');
// Ensure framework is ready
setTimeout(() => {
console.log('MinigameFramework ready:', window.MinigameFramework);
}, 100);
</script>
</body>
</html>