mirror of
https://github.com/cliffe/BreakEscape.git
synced 2026-02-20 13:50:46 +00:00
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:
BIN
assets/objects/pin-cracker-large.png
Normal file
BIN
assets/objects/pin-cracker-large.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
BIN
assets/objects/pin-cracker.png
Normal file
BIN
assets/objects/pin-cracker.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 315 B |
444
css/pin.css
Normal file
444
css/pin.css
Normal 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;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
@@ -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!");
|
||||
|
||||
513
js/minigames/pin/pin-minigame.js
Normal file
513
js/minigames/pin/pin-minigame.js
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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
422
test-pin-minigame.html
Normal 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>
|
||||
Reference in New Issue
Block a user