Refactor minigame structure and styles: Update index_new.html to link to the new minigames-framework.css, add new lockpicking-comparison.html and locksmith-forge.html files for enhanced gameplay, and introduce dusting and lockpicking CSS files for improved styling. Update README_design.md for clarity on main.js functionality. Add new test-phaser-lockpicking.html for testing purposes. Enhance Bluetooth system with new functionality in bluetooth.js and interactions.js. Ensure game state management for notes and Bluetooth devices is consistent across the application.

This commit is contained in:
Z. Cliffe Schreuders
2025-08-08 15:33:44 +01:00
parent c4d8508bcf
commit b864d3e139
22 changed files with 4553 additions and 380 deletions

View File

@@ -48,7 +48,7 @@ BreakEscape/
│ └── utilities.css # Utility classes and helpers
├── js/ # JavaScript source code
│ ├── main.js # Application entry point and initialization
│ ├── main.js # Application entry point, init, and game state variables
│ │
│ ├── core/ # Core game engine components
│ │ ├── game.js # Main game scene (preload, create, update)

182
css/dusting.css Normal file
View File

@@ -0,0 +1,182 @@
/* Dusting Minigame Styles */
.dusting-container {
width: 75% !important;
height: 75% !important;
padding: 20px;
}
.dusting-game-container {
width: 100%;
height: 60%;
margin: 0 auto 20px auto;
background: #1a1a1a;
border-radius: 5px;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.5) inset;
position: relative;
overflow: hidden;
border: 2px solid #333;
}
.dusting-grid-background {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
background-size: 20px 20px;
background-repeat: repeat;
z-index: 1;
}
.dusting-tools-container {
position: absolute;
top: 10px;
right: 10px;
display: flex;
flex-direction: column;
gap: 5px;
z-index: 3;
}
.dusting-tool-button {
padding: 8px 12px;
border: none;
border-radius: 3px;
cursor: pointer;
font-size: 12px;
font-weight: bold;
color: white;
transition: opacity 0.2s, transform 0.1s;
opacity: 0.7;
}
.dusting-tool-button:hover {
opacity: 0.9;
transform: scale(1.05);
}
.dusting-tool-button.active {
opacity: 1;
box-shadow: 0 0 8px rgba(255, 255, 255, 0.3);
}
.dusting-tool-fine {
background-color: #3498db;
}
.dusting-tool-medium {
background-color: #2ecc71;
}
.dusting-tool-wide {
background-color: #e67e22;
}
.dusting-particle-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 2;
}
.dusting-particle {
position: absolute;
width: 3px;
height: 3px;
border-radius: 50%;
pointer-events: none;
z-index: 2;
}
.dusting-progress-container {
position: absolute;
bottom: 10px;
left: 10px;
right: 10px;
background: rgba(0, 0, 0, 0.8);
padding: 10px;
border-radius: 3px;
color: white;
font-family: 'VT323', monospace;
font-size: 14px;
z-index: 3;
}
.dusting-grid-cell {
position: absolute;
background: #000;
border: 1px solid #222;
cursor: crosshair;
}
.dusting-cell-clean {
background: black !important;
box-shadow: none !important;
}
.dusting-cell-light-dust {
background: #444 !important;
box-shadow: inset 0 0 3px rgba(255,255,255,0.2) !important;
}
.dusting-cell-fingerprint {
background: #0f0 !important;
box-shadow: inset 0 0 5px rgba(0,255,0,0.5), 0 0 5px rgba(0,255,0,0.3) !important;
}
.dusting-cell-medium-dust {
background: #888 !important;
box-shadow: inset 0 0 4px rgba(255,255,255,0.3) !important;
}
.dusting-cell-heavy-dust {
background: #ccc !important;
box-shadow: inset 0 0 5px rgba(255,255,255,0.5) !important;
}
.dusting-progress-found {
color: #2ecc71;
}
.dusting-progress-over-dusted {
color: #e74c3c;
}
.dusting-progress-normal {
color: #fff;
}
/* Dusting Game Success/Failure Messages */
.dusting-success-message {
font-weight: bold;
font-size: 24px;
margin-bottom: 10px;
color: #2ecc71;
}
.dusting-success-quality {
font-size: 18px;
margin-bottom: 15px;
color: #fff;
}
.dusting-success-details {
font-size: 14px;
color: #aaa;
}
.dusting-failure-message {
font-weight: bold;
margin-bottom: 10px;
color: #e74c3c;
}
.dusting-failure-subtitle {
font-size: 16px;
margin-top: 5px;
color: #fff;
}

515
css/lockpicking.css Normal file
View File

@@ -0,0 +1,515 @@
/* Lockpicking Minigame Styles */
/* Override header positioning for lockpicking */
.minigame-header {
position: relative !important;
background: rgba(34, 34, 34, 0.95);
padding: 10px 20px;
margin-bottom: 20px;
border-radius: 5px;
}
.minigame-header h3 {
font-family: 'Press Start 2P', monospace;
font-size: 16px;
margin: 0 0 10px 0;
}
.minigame-header p {
font-family: 'VT323', monospace;
font-size: 18px;
margin: 0;
}
.lock-visual {
display: flex;
justify-content: space-evenly;
align-items: center;
gap: 20px;
height: 300px; /* Taller for better visibility */
background: #f0e6a6; /* Light yellow/beige background */
border-radius: 5px;
padding: 25px;
position: relative;
margin: 20px auto; /* Center and add margins */
border: 2px solid #887722;
max-width: 800px; /* Reasonable maximum width */
width: 90%; /* Responsive width */
}
.pin {
width: 40px;
height: 200px; /* Taller to match container */
position: relative;
background: transparent;
border-radius: 4px 4px 0 0;
overflow: visible;
cursor: pointer;
transition: transform 0.1s;
margin: 0 15px;
}
.pin:hover {
opacity: 0.9;
}
.shear-line {
position: absolute;
width: 100%;
height: 2px;
background: #aa8833;
bottom: 60px; /* Match driver pin starting position */
z-index: 5;
}
.key-pin {
position: absolute;
bottom: 0;
width: 100%;
height: 0px; /* Start at 0px, grows dynamically via JavaScript */
background: #dd3333; /* Red for key pins */
border-radius: 0 0 0 0;
clip-path: polygon(0 0, 100% 0, 100% 70%, 50% 100%, 0 70%); /* Pointed bottom */
transition: height 0.1s ease; /* Smooth height animation */
}
.driver-pin {
position: absolute;
width: 100%;
height: 40px; /* Smaller height for better proportion */
background: #3388dd; /* Blue for driver pins */
bottom: 60px; /* Start at shear line level */
border-radius: 4px 4px 0 0;
transition: bottom 0.1s ease, background-color 0.3s;
}
.spring {
position: absolute;
bottom: 100px; /* Positioned above driver pin */
width: 100%;
height: 30px;
background: repeating-linear-gradient(
to bottom,
#cccccc 0px,
#cccccc 2px,
#999999 2px,
#999999 4px
);
transition: transform 0.1s ease;
}
.pin.binding {
box-shadow: 0 0 8px 2px #ffcc00;
}
/* Keep driver pin (blue) above the shear line when set */
.pin.set .driver-pin {
background: #22aa22; /* Green to indicate set */
}
/* Key pin turns green when set */
.pin.set .key-pin {
background: #22aa22; /* Green to indicate set */
}
.cylinder {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 30px;
background: #ddbb77;
border-radius: 5px;
margin-top: 5px;
position: relative;
z-index: 0;
border: 2px solid #887722;
}
.cylinder-inner {
width: 80%;
height: 20px;
background: #ccaa66;
border-radius: 3px;
transform-origin: center;
transition: transform 0.3s;
}
.cylinder.rotated .cylinder-inner {
transform: rotate(15deg);
}
.lockpick-feedback {
padding: 15px;
background: #333;
border-radius: 5px;
text-align: center;
min-height: 30px;
margin-top: 20px;
font-family: 'VT323', monospace;
font-size: 18px;
}
.tension-control {
display: grid;
grid-template-columns: auto 1fr;
gap: 20px;
align-items: center;
background: #333;
padding: 20px;
border-radius: 5px;
margin-top: 20px;
}
.tension-wrench-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
position: relative;
width: 150px;
height: 60px;
}
.tension-track {
width: 100%;
height: 10px;
background: #444;
border-radius: 5px;
position: relative;
overflow: hidden;
}
.tension-progress {
position: absolute;
height: 100%;
width: 0%;
background: linear-gradient(to right, #666, #2196F3);
transition: width 0.3s;
}
.tension-status {
font-size: 16px;
text-align: left;
padding-left: 10px;
}
.tension-wrench {
width: 60px;
height: 40px;
background: #666;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.3s, background-color 0.3s;
position: absolute;
left: 0;
top: 20px;
z-index: 2;
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
}
.tension-wrench:hover {
background: #777;
}
.tension-wrench.active {
background: #2196F3;
}
.wrench-handle {
width: 60%;
height: 10px;
background: #999;
position: absolute;
}
.wrench-tip {
width: 20px;
height: 30px;
background: #999;
position: absolute;
left: 5px;
}
.instructions {
text-align: center;
margin-bottom: 10px;
font-size: 12px;
color: #ccc;
}
/* General success/failure message styles */
.lockpicking-success-message {
font-weight: bold;
font-size: 18px;
margin-bottom: 10px;
color: #2ecc71;
}
.lockpicking-success-subtitle {
font-size: 14px;
margin-bottom: 15px;
color: #fff;
}
.lockpicking-success-details {
font-size: 12px;
color: #aaa;
}
.lockpicking-failure-message {
font-weight: bold;
margin-bottom: 10px;
color: #e74c3c;
}
.lockpicking-failure-subtitle {
font-size: 16px;
margin-top: 5px;
color: #fff;
}
.tension-control {
display: grid;
grid-template-columns: auto 1fr;
gap: 20px;
align-items: center;
background: #333;
padding: 20px;
border-radius: 5px;
margin-top: 20px;
}
.tension-wrench-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
position: relative;
width: 150px;
height: 60px;
}
.tension-track {
width: 100%;
height: 10px;
background: #444;
border-radius: 5px;
position: relative;
overflow: hidden;
}
.tension-progress {
position: absolute;
height: 100%;
width: 0%;
background: linear-gradient(to right, #666, #2196F3);
transition: width 0.3s;
}
.tension-status {
font-size: 16px;
text-align: left;
padding-left: 10px;
}
.tension-wrench {
width: 60px;
height: 40px;
background: #666;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.3s, background-color 0.3s;
position: absolute;
left: 0;
top: 20px;
z-index: 2;
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
}
.tension-wrench:hover {
background: #777;
}
.tension-wrench.active {
background: #2196F3;
}
.wrench-handle {
width: 60%;
height: 10px;
background: #999;
position: absolute;
}
.wrench-tip {
width: 20px;
height: 30px;
background: #999;
position: absolute;
left: 5px;
}
.cylinder {
height: 20px;
margin-top: -5px;
}
.lock-visual {
display: flex;
justify-content: space-evenly;
align-items: center;
gap: 10px;
height: 160px;
background: #f0e6a6; /* Light yellow/beige background */
border-radius: 5px;
padding: 15px;
position: relative;
margin-bottom: 10px;
border: 2px solid #887722;
}
.pin {
width: 30px;
height: 110px;
position: relative;
background: transparent;
border-radius: 4px 4px 0 0;
overflow: visible;
cursor: pointer;
transition: transform 0.1s;
margin: 0 5px;
}
.pin:hover {
opacity: 0.9;
}
.shear-line {
position: absolute;
width: 100%;
height: 2px;
background: #aa8833;
bottom: 50px;
z-index: 5;
}
.key-pin {
position: absolute;
bottom: 0;
width: 100%;
height: 0px;
background: #dd3333; /* Red for key pins */
transition: height 0.05s;
border-radius: 0 0 0 0;
clip-path: polygon(0 0, 100% 0, 100% 70%, 50% 100%, 0 70%); /* Pointed bottom */
}
.driver-pin {
position: absolute;
width: 100%;
height: 50px;
background: #3388dd; /* Blue for driver pins */
transition: bottom 0.05s;
bottom: 50px;
border-radius: 0 0 0 0;
}
.spring {
position: absolute;
bottom: 100px;
width: 100%;
height: 25px;
background: linear-gradient(to bottom,
#cccccc 0%, #cccccc 20%,
#999999 20%, #999999 25%,
#cccccc 25%, #cccccc 40%,
#999999 40%, #999999 45%,
#cccccc 45%, #cccccc 60%,
#999999 60%, #999999 65%,
#cccccc 65%, #cccccc 80%,
#999999 80%, #999999 85%,
#cccccc 85%, #cccccc 100%
);
transition: height 0.05s;
}
.pin.binding {
box-shadow: 0 0 8px 2px #ffcc00;
}
.pin.set .driver-pin {
bottom: 52px; /* Just above shear line */
background: #22aa22; /* Green to indicate set */
}
.pin.set .key-pin {
height: 49px; /* Just below shear line */
background: #22aa22; /* Green to indicate set */
clip-path: polygon(0 0, 100% 0, 100% 70%, 50% 100%, 0 70%);
}
.cylinder {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 30px;
background: #ddbb77;
border-radius: 5px;
margin-top: 5px;
position: relative;
z-index: 0;
border: 2px solid #887722;
}
.cylinder-inner {
width: 80%;
height: 20px;
background: #ccaa66;
border-radius: 3px;
transform-origin: center;
transition: transform 0.3s;
}
.cylinder.rotated .cylinder-inner {
transform: rotate(15deg);
}
.lockpick-feedback {
padding: 15px;
background: #333;
border-radius: 5px;
text-align: center;
min-height: 30px;
margin-top: 20px;
font-family: 'VT323', monospace;
font-size: 18px;
}
/* Phaser-specific styles */
.phaser-game-container {
width: 100%;
height: 400px;
background: #1a1a1a;
border-radius: 5px;
margin: 20px 0;
display: flex;
justify-content: center;
align-items: center;
border: 2px solid #444;
}
.phaser-game-container canvas {
border-radius: 5px;
max-width: 100%;
max-height: 100%;
}

194
css/minigames-framework.css Normal file
View File

@@ -0,0 +1,194 @@
/* Minigame Framework Styles */
.minigame-container {
position: fixed;
top: 2vh;
left: 2vw;
width: 96vw;
height: 96vh;
background: rgba(0, 0, 0, 0.95);
z-index: 2000;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-family: 'Press Start 2P', monospace;
color: white;
border-radius: 10px;
border: 2px solid #444;
box-shadow: 0 0 30px rgba(0, 0, 0, 0.8);
}
.minigame-header {
width: 100%;
text-align: center;
font-size: 18px;
margin-bottom: 20px;
color: #3498db;
}
.minigame-header h3 {
font-family: 'Press Start 2P', monospace;
font-size: 16px;
margin: 0 0 10px 0;
}
.minigame-header p {
font-family: 'VT323', monospace;
font-size: 18px;
margin: 0;
}
.minigame-game-container {
width: 80%;
max-width: 600px;
height: 60%;
margin: 20px auto;
background: #1a1a1a;
border-radius: 5px;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.5) inset;
position: relative;
overflow: hidden;
}
.minigame-message-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1000;
}
.minigame-success-message {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(46, 204, 113, 0.9);
color: white;
padding: 20px;
border-radius: 10px;
text-align: center;
z-index: 10001;
font-size: 14px;
border: 2px solid #27ae60;
box-shadow: 0 0 20px rgba(46, 204, 113, 0.5);
}
.minigame-failure-message {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(231, 76, 60, 0.9);
color: white;
padding: 20px;
border-radius: 10px;
text-align: center;
z-index: 10001;
font-size: 14px;
border: 2px solid #c0392b;
box-shadow: 0 0 20px rgba(231, 76, 60, 0.5);
}
.minigame-controls {
display: flex;
justify-content: center;
gap: 10px;
margin-top: 20px;
}
.minigame-button {
background: #3498db;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-family: 'VT323', monospace;
font-size: 16px;
transition: background 0.3s;
}
.minigame-button:hover {
background: #2980b9;
}
.minigame-button:active {
background: #21618c;
}
.minigame-progress-container {
width: 100%;
height: 20px;
background: #333;
border-radius: 10px;
overflow: hidden;
margin: 10px 0;
}
.minigame-progress-bar {
height: 100%;
background: #2ecc71;
width: 0%;
transition: width 0.3s;
}
/* Minigame disabled state */
.minigame-disabled {
pointer-events: none !important;
}
/* Biometric scanner visual feedback */
.biometric-scanner-success {
border: 2px solid #00ff00 !important;
}
/* Close button for minigames */
.minigame-close-button {
position: absolute;
top: 15px;
right: 15px;
width: 30px;
height: 30px;
background: #e74c3c;
color: white;
border: none;
border-radius: 50%;
cursor: pointer;
font-family: 'VT323', monospace;
font-size: 14px;
font-weight: bold;
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.3s ease;
}
.minigame-close-button:hover {
background: #c0392b;
}
.minigame-close-button:active {
background: #a93226;
}
/* Progress bar styling for minigames */
.minigame-progress-container {
width: 100%;
height: 10px;
background: #333;
border-radius: 5px;
overflow: hidden;
margin-top: 5px;
}
.minigame-progress-bar {
height: 100%;
background: linear-gradient(90deg, #2ecc71, #27ae60);
transition: width 0.3s ease;
border-radius: 5px;
}

View File

@@ -1,133 +1,5 @@
/* Minigames Styles */
/* Lockpicking Game */
.lockpick-container {
width: 350px;
height: 300px;
background: #8A5A3C;
border-radius: 10px;
position: relative;
margin: 20px auto;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
border: 2px solid #887722;
}
.pin {
width: 30px;
height: 110px;
position: relative;
background: transparent;
border-radius: 4px 4px 0 0;
overflow: visible;
cursor: pointer;
transition: transform 0.1s;
margin: 0 5px;
}
.pin:hover {
opacity: 0.9;
}
.shear-line {
position: absolute;
width: 100%;
height: 2px;
background: #aa8833;
bottom: 50px;
z-index: 5;
}
.key-pin {
position: absolute;
bottom: 0;
width: 100%;
height: 0px;
background: #dd3333; /* Red for key pins */
transition: height 0.05s;
border-radius: 0 0 0 0;
clip-path: polygon(0 0, 100% 0, 100% 70%, 50% 100%, 0 70%); /* Pointed bottom */
}
.driver-pin {
position: absolute;
width: 100%;
height: 50px;
background: #3388dd; /* Blue for driver pins */
transition: bottom 0.05s;
bottom: 50px;
border-radius: 0 0 0 0;
}
.spring {
position: absolute;
bottom: 100px;
width: 100%;
height: 25px;
background: linear-gradient(to bottom,
#cccccc 0%, #cccccc 20%,
#999999 20%, #999999 25%,
#cccccc 25%, #cccccc 40%,
#999999 40%, #999999 45%,
#cccccc 45%, #cccccc 60%,
#999999 60%, #999999 65%,
#cccccc 65%, #cccccc 80%,
#999999 80%, #999999 85%,
#cccccc 85%, #cccccc 100%
);
transition: height 0.05s;
}
.pin.binding {
box-shadow: 0 0 8px 2px #ffcc00;
}
.pin.set .driver-pin {
bottom: 52px; /* Just above shear line */
background: #22aa22; /* Green to indicate set */
}
.pin.set .key-pin {
height: 49px; /* Just below shear line */
background: #22aa22; /* Green to indicate set */
clip-path: polygon(0 0, 100% 0, 100% 70%, 50% 100%, 0 70%);
}
.cylinder {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 30px;
background: #ddbb77;
border-radius: 5px;
margin-top: 5px;
position: relative;
z-index: 0;
border: 2px solid #887722;
}
.cylinder-inner {
width: 80%;
height: 20px;
background: #ccaa66;
border-radius: 3px;
transform-origin: center;
transition: transform 0.3s;
}
.cylinder.rotated .cylinder-inner {
transform: rotate(15deg);
}
.lockpick-feedback {
padding: 15px;
background: #333;
border-radius: 5px;
text-align: center;
min-height: 30px;
margin-top: 20px;
font-size: 16px;
}
/* Minigame Framework Styles */
.minigame-container {
@@ -254,118 +126,6 @@
transition: width 0.3s;
}
/* Advanced Lockpicking specific styles */
.lock-visual {
display: flex;
justify-content: space-evenly;
align-items: center;
gap: 15px;
height: 200px;
background: #f0e6a6;
border-radius: 5px;
padding: 20px;
position: relative;
margin: 20px;
border: 2px solid #887722;
}
.pin {
width: 30px;
height: 150px;
position: relative;
background: transparent;
border-radius: 4px 4px 0 0;
overflow: visible;
cursor: pointer;
transition: transform 0.1s;
}
.pin:hover {
transform: scale(1.05);
}
.shear-line {
position: absolute;
width: 100%;
height: 2px;
background: #aa8833;
top: 60px;
z-index: 5;
}
.key-pin {
position: absolute;
bottom: 0;
width: 100%;
height: 0px;
background: #dd3333;
transition: height 0.1s;
border-radius: 0 0 4px 4px;
}
.driver-pin {
position: absolute;
width: 100%;
height: 40px;
background: #3388dd;
transition: bottom 0.1s;
bottom: 60px;
border-radius: 4px 4px 0 0;
}
.spring {
position: absolute;
bottom: 100px;
width: 100%;
height: 20px;
background: repeating-linear-gradient(
to bottom,
#cccccc 0px,
#cccccc 2px,
#999999 2px,
#999999 4px
);
transition: height 0.1s;
}
.pin.binding {
box-shadow: 0 0 10px 2px #ffcc00;
}
.pin.set .driver-pin {
bottom: 62px;
background: #22aa22;
}
.pin.set .key-pin {
height: 59px;
background: #22aa22;
}
.tension-control {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 10px;
}
.tension-wrench {
width: 60px;
height: 20px;
background: #888;
border-radius: 3px;
cursor: pointer;
transition: transform 0.2s;
}
.tension-wrench.active {
transform: rotate(15deg);
background: #ffcc00;
}
.instructions {
text-align: center;
margin-bottom: 10px;
@@ -373,20 +133,6 @@
color: #ccc;
}
.lockpick-feedback {
position: absolute;
bottom: 60px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 10px;
border-radius: 5px;
text-align: center;
font-size: 11px;
min-width: 200px;
}
/* Dusting Minigame */
.dusting-container {
width: 75% !important;
@@ -538,37 +284,6 @@
color: #fff;
}
/* Lockpicking Game Success/Failure Messages */
.lockpicking-success-message {
font-weight: bold;
font-size: 18px;
margin-bottom: 10px;
color: #2ecc71;
}
.lockpicking-success-subtitle {
font-size: 14px;
margin-bottom: 15px;
color: #fff;
}
.lockpicking-success-details {
font-size: 12px;
color: #aaa;
}
.lockpicking-failure-message {
font-weight: bold;
margin-bottom: 10px;
color: #e74c3c;
}
.lockpicking-failure-subtitle {
font-size: 16px;
margin-top: 5px;
color: #fff;
}
/* Dusting Game Success/Failure Messages */
.dusting-success-message {
font-weight: bold;

View File

@@ -244,6 +244,120 @@
background-color: #444;
}
#bluetooth-content {
max-height: 300px;
overflow-y: auto;
padding: 10px;
}
.bluetooth-device {
background-color: #333;
border: 1px solid #444;
border-radius: 5px;
padding: 10px;
margin-bottom: 8px;
cursor: pointer;
transition: all 0.2s;
}
.bluetooth-device:hover {
background-color: #444;
border-color: #9b59b6;
}
.bluetooth-device:last-child {
margin-bottom: 0;
}
.bluetooth-device.expanded {
background-color: #2a2a2a;
}
.bluetooth-device-name {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 14px;
font-weight: bold;
margin-bottom: 5px;
}
.bluetooth-device-icons {
display: flex;
align-items: center;
gap: 5px;
}
.bluetooth-device-icon {
font-size: 12px;
}
.bluetooth-device-details {
display: none;
font-size: 12px;
color: #ccc;
margin-top: 8px;
white-space: pre-line;
}
.bluetooth-device.expanded .bluetooth-device-details {
display: block;
}
.bluetooth-device-timestamp {
font-size: 10px;
color: #888;
margin-top: 5px;
text-align: right;
}
/* Bluetooth Signal Strength Bar */
.bluetooth-signal-bar-container {
display: flex;
align-items: center;
gap: 3px;
}
.bluetooth-signal-bars {
display: flex;
align-items: flex-end;
gap: 1px;
}
.bluetooth-signal-bar {
width: 3px;
background-color: #666;
border-radius: 1px;
transition: all 0.2s;
}
.bluetooth-signal-bar.active {
background-color: currentColor;
}
.bluetooth-signal-bar:nth-child(1) { height: 3px; }
.bluetooth-signal-bar:nth-child(2) { height: 6px; }
.bluetooth-signal-bar:nth-child(3) { height: 9px; }
.bluetooth-signal-bar:nth-child(4) { height: 12px; }
.bluetooth-signal-bar:nth-child(5) { height: 16px; }
.bluetooth-signal-text {
font-size: 10px;
color: #aaa;
}
.bluetooth-device.hover-preserved {
background-color: #444;
border-color: #9b59b6;
}
.bluetooth-device:hover .bluetooth-device-name,
.bluetooth-device:hover .bluetooth-device-details,
.bluetooth-device:hover .bluetooth-device-timestamp,
.bluetooth-device:hover {
color: inherit;
}
/* Biometrics Panel */
#biometrics-panel {
position: fixed;

View File

@@ -30,7 +30,7 @@
<link rel="stylesheet" href="css/notifications.css">
<link rel="stylesheet" href="css/panels.css">
<link rel="stylesheet" href="css/inventory.css">
<link rel="stylesheet" href="css/minigames.css">
<link rel="stylesheet" href="css/minigames-framework.css">
<link rel="stylesheet" href="css/modals.css">
<!-- External JavaScript libraries -->

View File

@@ -173,7 +173,7 @@ export function update() {
// Check for Bluetooth devices
const currentTime = Date.now();
if (currentTime - lastBluetoothScan >= 2000) { // 2 second interval
if (currentTime - lastBluetoothScan >= 200) { // 200ms interval for more responsive updates
if (window.checkBluetoothDevices) {
window.checkBluetoothDevices();
}

View File

@@ -20,8 +20,10 @@ export function initializeRooms(gameInstance) {
gameRef = gameInstance;
console.log('Initializing rooms');
rooms = {};
window.rooms = rooms; // Ensure window.rooms references the same object
currentRoom = '';
currentPlayerRoom = '';
window.currentPlayerRoom = '';
discoveredRooms = new Set();
}
@@ -316,6 +318,9 @@ export function createRoom(roomId, roomData, position) {
objects: {},
position
};
// Ensure window.rooms is updated
window.rooms = rooms;
const layers = rooms[roomId].layers;
const wallsLayers = rooms[roomId].wallsLayers;
@@ -601,8 +606,8 @@ export function updatePlayerRoom() {
// If we're not overlapping any rooms
if (overlappingRooms.length === 0) {
console.log('Player not in any room');
currentPlayerRoom = null;
window.currentPlayerRoom = null;
return null;
}
@@ -610,6 +615,7 @@ export function updatePlayerRoom() {
if (currentPlayerRoom !== overlappingRooms[0]) {
console.log(`Player's main room changed to: ${overlappingRooms[0]}`);
currentPlayerRoom = overlappingRooms[0];
window.currentPlayerRoom = overlappingRooms[0];
}
return currentPlayerRoom;

View File

@@ -36,6 +36,7 @@ window.gameState = {
biometricSamples: [],
biometricUnlocks: [],
bluetoothDevices: [],
notes: [],
startTime: null
};
window.lastBluetoothScan = 0;

View File

@@ -1,5 +1,14 @@
import { MinigameScene } from '../framework/base-minigame.js';
// Load dusting-specific CSS
const dustingCSS = document.createElement('link');
dustingCSS.rel = 'stylesheet';
dustingCSS.href = 'css/dusting.css';
dustingCSS.id = 'dusting-css';
if (!document.getElementById('dusting-css')) {
document.head.appendChild(dustingCSS);
}
// Dusting Minigame Scene implementation
export class DustingMinigame extends MinigameScene {
constructor(container, params) {

View File

@@ -12,22 +12,24 @@ export const MinigameFramework = {
console.log("MinigameFramework initialized");
},
startMinigame(sceneType, params) {
startMinigame(sceneType, container, params) {
if (!this.registeredScenes[sceneType]) {
console.error(`Minigame scene '${sceneType}' not registered`);
return;
return null;
}
// Disable main game input
if (this.mainGameScene) {
// Disable main game input if we have a main game scene
if (this.mainGameScene && this.mainGameScene.input) {
this.mainGameScene.input.mouse.enabled = false;
this.mainGameScene.input.keyboard.enabled = false;
}
// Create minigame container
const container = document.createElement('div');
container.className = 'minigame-container';
document.body.appendChild(container);
// Use provided container or create one
if (!container) {
container = document.createElement('div');
container.className = 'minigame-container';
document.body.appendChild(container);
}
// Create and start the minigame
const MinigameClass = this.registeredScenes[sceneType];
@@ -36,26 +38,27 @@ export const MinigameFramework = {
this.currentMinigame.start();
console.log(`Started minigame: ${sceneType}`);
return this.currentMinigame;
},
endMinigame(success, result) {
if (this.currentMinigame) {
this.currentMinigame.cleanup();
// Remove minigame container
// Remove minigame container only if it was auto-created
const container = document.querySelector('.minigame-container');
if (container) {
if (container && !container.hasAttribute('data-external')) {
container.remove();
}
// Re-enable main game input
if (this.mainGameScene) {
// Re-enable main game input if we have a main game scene
if (this.mainGameScene && this.mainGameScene.input) {
this.mainGameScene.input.mouse.enabled = true;
this.mainGameScene.input.keyboard.enabled = true;
}
// Call completion callback
if (this.currentMinigame.params.onComplete) {
if (this.currentMinigame.params && this.currentMinigame.params.onComplete) {
this.currentMinigame.params.onComplete(success, result);
}

View File

@@ -4,11 +4,13 @@ export { MinigameScene } from './framework/base-minigame.js';
// Export minigame implementations
export { LockpickingMinigame } from './lockpicking/lockpicking-game.js';
export { LockpickingMinigamePhaser } from './lockpicking/lockpicking-game-phaser.js';
export { DustingMinigame } from './dusting/dusting-game.js';
// Initialize the global minigame framework for backward compatibility
import { MinigameFramework } from './framework/minigame-manager.js';
import { LockpickingMinigame } from './lockpicking/lockpicking-game.js';
import { LockpickingMinigamePhaser } from './lockpicking/lockpicking-game-phaser.js';
// Make the framework available globally
window.MinigameFramework = MinigameFramework;
@@ -17,5 +19,7 @@ window.MinigameFramework = MinigameFramework;
import { DustingMinigame } from './dusting/dusting-game.js';
// Register minigames
MinigameFramework.registerScene('lockpicking', LockpickingMinigame);
MinigameFramework.registerScene('lockpicking', LockpickingMinigamePhaser); // Use Phaser version as default
MinigameFramework.registerScene('lockpicking-legacy', LockpickingMinigame); // Keep old version for backward compatibility
MinigameFramework.registerScene('lockpicking-phaser', LockpickingMinigamePhaser); // Keep explicit phaser name
MinigameFramework.registerScene('dusting', DustingMinigame);

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,14 @@
import { MinigameScene } from '../framework/base-minigame.js';
// Load lockpicking-specific CSS
const lockpickingCSS = document.createElement('link');
lockpickingCSS.rel = 'stylesheet';
lockpickingCSS.href = 'css/lockpicking.css';
lockpickingCSS.id = 'lockpicking-css';
if (!document.getElementById('lockpicking-css')) {
document.head.appendChild(lockpickingCSS);
}
// Lockpicking Minigame Scene implementation
export class LockpickingMinigame extends MinigameScene {
constructor(container, params) {

View File

@@ -4,6 +4,20 @@
// Bluetooth state management
let bluetoothDevices = [];
let lastBluetoothPanelUpdate = 0;
let newBluetoothDevices = 0;
// Sync with global game state
function syncBluetoothDevices() {
if (!window.gameState) {
window.gameState = {};
}
window.gameState.bluetoothDevices = bluetoothDevices;
}
// Constants
const BLUETOOTH_SCAN_RANGE = 150; // pixels - 2 tiles range for Bluetooth scanning
const BLUETOOTH_SCAN_INTERVAL = 200; // Scan every 200ms for more responsive updates
const BLUETOOTH_UPDATE_THROTTLE = 100; // Update UI every 100ms max
// Initialize the Bluetooth system
export function initializeBluetoothPanel() {
@@ -45,6 +59,8 @@ export function initializeBluetoothPanel() {
// Initialize bluetooth panel
updateBluetoothPanel();
updateBluetoothCount();
syncBluetoothDevices();
}
// Check for Bluetooth devices
@@ -54,7 +70,9 @@ export function checkBluetoothDevices() {
item.scenarioData?.type === "bluetooth_scanner"
);
if (!scanner) return;
if (!scanner) {
return;
}
// Show the Bluetooth toggle button if it's not already visible
const bluetoothToggle = document.getElementById('bluetooth-toggle');
@@ -63,11 +81,15 @@ export function checkBluetoothDevices() {
}
// Find all Bluetooth devices in the current room
if (!window.currentPlayerRoom || !window.rooms[window.currentPlayerRoom] || !window.rooms[window.currentPlayerRoom].objects) return;
if (!window.currentPlayerRoom || !window.rooms[window.currentPlayerRoom] || !window.rooms[window.currentPlayerRoom].objects) {
return;
}
const room = window.rooms[window.currentPlayerRoom];
const player = window.player;
if (!player) return;
if (!player) {
return;
}
// Keep track of devices detected in this scan
const detectedDevices = new Set();
@@ -80,36 +102,35 @@ export function checkBluetoothDevices() {
);
const deviceMac = obj.scenarioData?.mac || "Unknown";
const BLUETOOTH_SCAN_RANGE = 150; // pixels
const deviceName = obj.scenarioData?.name || "Unknown Device";
if (distance <= BLUETOOTH_SCAN_RANGE) {
detectedDevices.add(deviceMac);
console.log('BLUETOOTH DEVICE DETECTED', {
deviceName: obj.scenarioData?.name,
deviceMac: deviceMac,
distance: Math.round(distance),
range: BLUETOOTH_SCAN_RANGE
});
detectedDevices.add(`${deviceMac}|${deviceName}`); // Use combination for uniqueness
// Add to Bluetooth scanner panel
const deviceName = obj.scenarioData?.name || "Unknown Device";
const signalStrength = Math.max(0, Math.round(100 - (distance / BLUETOOTH_SCAN_RANGE * 100)));
const details = `Type: ${obj.scenarioData?.type || "Unknown"}\nDistance: ${Math.round(distance)} units\nSignal Strength: ${signalStrength}%`;
const signalStrengthPercentage = Math.max(0, Math.round(100 - (distance / BLUETOOTH_SCAN_RANGE * 100)));
// Convert percentage to dBm format (-100 to -30 dBm range)
const signalStrength = Math.round(-100 + (signalStrengthPercentage * 0.7)); // -100 to -30 dBm
const details = `Type: ${obj.scenarioData?.type || "Unknown"}\nDistance: ${Math.round(distance)} units\nSignal Strength: ${signalStrength}dBm (${signalStrengthPercentage}%)`;
// Check if device already exists in our list
const existingDevice = bluetoothDevices.find(device => device.mac === deviceMac);
// Check if device already exists in our list (by MAC + name combination for uniqueness)
const existingDevice = bluetoothDevices.find(device =>
device.mac === deviceMac && device.name === deviceName
);
if (existingDevice) {
// Update existing device details with real-time data
const oldSignalStrength = existingDevice.signalStrength;
const wasNearby = existingDevice.nearby;
const oldSignalStrengthPercentage = existingDevice.signalStrengthPercentage || 0;
existingDevice.details = details;
existingDevice.lastSeen = new Date();
existingDevice.nearby = true;
existingDevice.signalStrength = signalStrength;
existingDevice.signalStrengthPercentage = signalStrengthPercentage;
// Only mark for update if signal strength changed significantly
if (Math.abs(oldSignalStrength - signalStrength) > 5) {
// Always update if device came back into range or signal strength changed significantly
if (!wasNearby || Math.abs(oldSignalStrengthPercentage - signalStrengthPercentage) > 5) {
needsUpdate = true;
}
} else {
@@ -117,7 +138,10 @@ export function checkBluetoothDevices() {
const newDevice = addBluetoothDevice(deviceName, deviceMac, details, true);
if (newDevice) {
newDevice.signalStrength = signalStrength;
window.gameAlert(`Bluetooth device detected: ${deviceName} (MAC: ${deviceMac})`, 'info', 'Bluetooth Scanner', 4000);
newDevice.signalStrengthPercentage = signalStrengthPercentage;
if (window.gameAlert) {
window.gameAlert(`Bluetooth device detected: ${deviceName} (MAC: ${deviceMac})`, 'info', 'Bluetooth Scanner', 4000);
}
needsUpdate = true;
}
}
@@ -127,54 +151,148 @@ export function checkBluetoothDevices() {
// Mark devices that weren't detected in this scan as not nearby
bluetoothDevices.forEach(device => {
if (device.nearby && !detectedDevices.has(device.mac)) {
const deviceKey = `${device.mac}|${device.name}`;
if (device.nearby && !detectedDevices.has(deviceKey)) {
device.nearby = false;
device.lastSeen = new Date();
needsUpdate = true;
}
});
// Only update the panel if needed and not too frequently
const now = Date.now();
if (needsUpdate && now - lastBluetoothPanelUpdate > 1000) { // 1 second throttle
updateBluetoothPanel();
// Force immediate UI update if panel is open and devices changed nearby status
if (needsUpdate) {
const bluetoothPanel = document.getElementById('bluetooth-panel');
if (bluetoothPanel && bluetoothPanel.style.display === 'block') {
// Force update by resetting throttle timer
lastBluetoothPanelUpdate = 0;
}
}
// Always update the count and sync devices when there are changes
if (needsUpdate) {
updateBluetoothCount();
lastBluetoothPanelUpdate = now;
syncBluetoothDevices();
// Update the panel UI if it's visible
const bluetoothPanel = document.getElementById('bluetooth-panel');
if (bluetoothPanel && bluetoothPanel.style.display === 'block') {
const now = Date.now();
if (now - lastBluetoothPanelUpdate > BLUETOOTH_UPDATE_THROTTLE) {
updateBluetoothPanel();
lastBluetoothPanelUpdate = now;
}
}
}
}
// Add a Bluetooth device to the scanner panel
export function addBluetoothDevice(name, mac, details = "", nearby = true) {
// Check if device already exists
const existingDevice = bluetoothDevices.find(device => device.mac === mac);
if (existingDevice) {
// Update existing device
existingDevice.details = details;
existingDevice.lastSeen = new Date();
// Check if a device with the same MAC + name combination already exists
const deviceExists = bluetoothDevices.some(device => device.mac === mac && device.name === name);
// If the device already exists, update its nearby status
if (deviceExists) {
const existingDevice = bluetoothDevices.find(device => device.mac === mac && device.name === name);
existingDevice.nearby = nearby;
return existingDevice;
existingDevice.lastSeen = new Date();
updateBluetoothPanel();
syncBluetoothDevices();
return null;
}
// Create new device
const newDevice = {
const device = {
id: Date.now(),
name: name,
mac: mac,
details: details,
nearby: nearby,
saved: false,
firstSeen: new Date(),
lastSeen: new Date(),
signalStrength: 0
signalStrength: -100, // Default to weak signal (-100 dBm)
signalStrengthPercentage: 0 // Default to 0% for visual display
};
bluetoothDevices.push(newDevice);
return newDevice;
bluetoothDevices.push(device);
updateBluetoothPanel();
updateBluetoothCount();
syncBluetoothDevices();
// Show notification for new device
if (window.showNotification) {
window.showNotification(`New Bluetooth device detected: ${name}`, 'info', 'Bluetooth Scanner', 3000);
}
return device;
}
// Update the Bluetooth scanner panel with current devices
export function updateBluetoothPanel() {
const bluetoothContent = document.getElementById('bluetooth-content');
if (!bluetoothContent) return;
const searchTerm = document.getElementById('bluetooth-search')?.value?.toLowerCase() || '';
// Get active category
const activeCategory = document.querySelector('.bluetooth-category.active')?.dataset.category || 'all';
// Store the currently hovered device, if any
const hoveredDevice = document.querySelector('.bluetooth-device:hover');
const hoveredDeviceId = hoveredDevice ? hoveredDevice.dataset.id : null;
// Add Bluetooth-locked items from inventory to the main bluetoothDevices array
if (window.inventory && window.inventory.items) {
window.inventory.items.forEach(item => {
if (item.scenarioData?.lockType === "bluetooth" && item.scenarioData?.locked) {
// Check if this device is already in our list
const deviceMac = item.scenarioData?.mac || "Unknown";
// Normalize MAC address format (ensure lowercase for comparison)
const normalizedMac = deviceMac.toLowerCase();
// Check if device already exists in our list (by MAC + name combination)
const deviceName = item.scenarioData?.name || item.name || "Unknown Device";
const existingDeviceIndex = bluetoothDevices.findIndex(device =>
device.mac.toLowerCase() === normalizedMac && device.name === deviceName
);
if (existingDeviceIndex === -1) {
// Add as a new device
const details = `Type: ${item.scenarioData?.type || "Unknown"}\nLocation: Inventory\nStatus: Locked`;
const newDevice = {
id: `inv_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
name: deviceName,
mac: deviceMac,
details: details,
lastSeen: new Date(),
nearby: true, // Always nearby since it's in inventory
saved: true, // Auto-save inventory items
signalStrength: -30, // Max strength for inventory items (-30 dBm)
signalStrengthPercentage: 100, // 100% for visual display
inInventory: true // Mark as inventory item
};
// Add to the main bluetoothDevices array
bluetoothDevices.push(newDevice);
console.log('Added inventory device to bluetoothDevices:', newDevice);
syncBluetoothDevices();
} else {
// Update existing device
const existingDevice = bluetoothDevices[existingDeviceIndex];
existingDevice.inInventory = true;
existingDevice.nearby = true;
existingDevice.signalStrength = -30; // -30 dBm for inventory items
existingDevice.signalStrengthPercentage = 100; // 100% for visual display
existingDevice.lastSeen = new Date();
existingDevice.details = `Type: ${item.scenarioData?.type || "Unknown"}\nLocation: Inventory\nStatus: Locked`;
console.log('Updated existing device with inventory info:', existingDevice);
syncBluetoothDevices();
}
}
});
}
// Filter devices based on search and category
let filteredDevices = [...bluetoothDevices];
@@ -182,23 +300,36 @@ export function updateBluetoothPanel() {
if (activeCategory === 'nearby') {
filteredDevices = filteredDevices.filter(device => device.nearby);
} else if (activeCategory === 'saved') {
filteredDevices = filteredDevices.filter(device => !device.nearby);
filteredDevices = filteredDevices.filter(device => device.saved);
}
// Apply search filter
if (searchTerm) {
filteredDevices = filteredDevices.filter(device =>
device.name.toLowerCase().includes(searchTerm) ||
device.mac.toLowerCase().includes(searchTerm)
device.mac.toLowerCase().includes(searchTerm) ||
device.details.toLowerCase().includes(searchTerm)
);
}
// Sort devices by signal strength (nearby first, then by signal strength)
// Sort devices with inventory items first, then nearby ones, then by signal strength
filteredDevices.sort((a, b) => {
// Inventory items first
if (a.inInventory !== b.inInventory) {
return a.inInventory ? -1 : 1;
}
// Then nearby items
if (a.nearby !== b.nearby) {
return a.nearby ? -1 : 1;
}
return (b.signalStrength || 0) - (a.signalStrength || 0);
// For nearby devices, sort by signal strength
if (a.nearby && b.nearby && a.signalStrength !== b.signalStrength) {
return b.signalStrength - a.signalStrength;
}
return new Date(b.lastSeen) - new Date(a.lastSeen);
});
// Clear current content
@@ -207,46 +338,101 @@ export function updateBluetoothPanel() {
// Add devices
if (filteredDevices.length === 0) {
if (searchTerm) {
bluetoothContent.innerHTML = '<div class="device-item">No devices match your search.</div>';
} else if (activeCategory === 'nearby') {
bluetoothContent.innerHTML = '<div class="device-item">No nearby devices found.</div>';
} else if (activeCategory === 'saved') {
bluetoothContent.innerHTML = '<div class="device-item">No saved devices found.</div>';
bluetoothContent.innerHTML = '<div class="bluetooth-device">No devices match your search.</div>';
} else if (activeCategory !== 'all') {
bluetoothContent.innerHTML = `<div class="bluetooth-device">No ${activeCategory} devices found.</div>`;
} else {
bluetoothContent.innerHTML = '<div class="device-item">No devices detected yet.</div>';
bluetoothContent.innerHTML = '<div class="bluetooth-device">No devices detected yet.</div>';
}
} else {
filteredDevices.forEach(device => {
const deviceElement = document.createElement('div');
deviceElement.className = 'device-item';
deviceElement.dataset.mac = device.mac;
deviceElement.className = 'bluetooth-device';
deviceElement.dataset.id = device.id;
const formattedTime = device.lastSeen ? device.lastSeen.toLocaleString() : 'Unknown';
const signalStrength = device.signalStrength || 0;
// If this was the hovered device, add the hover class
if (hoveredDeviceId && device.id === hoveredDeviceId) {
deviceElement.classList.add('hover-preserved');
}
deviceElement.innerHTML = `
<div class="device-info">
<div class="device-name">${device.name}</div>
<div class="device-address">${device.mac}</div>
</div>
<div class="device-signal">${signalStrength}%</div>
<div class="device-status ${device.nearby ? 'nearby' : 'saved'}">
${device.nearby ? 'Nearby' : 'Not in range'}
</div>
`;
// Format the timestamp
const timestamp = new Date(device.lastSeen);
const formattedDate = timestamp.toLocaleDateString();
const formattedTime = timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
// Get signal color based on strength
const getSignalColor = (strength) => {
if (strength >= 80) return '#00cc00'; // Strong - green
if (strength >= 50) return '#cccc00'; // Medium - yellow
return '#cc5500'; // Weak - orange
};
let deviceContent = `<div class="bluetooth-device-name">
<span>${device.name}</span>
<div class="bluetooth-device-icons">`;
if (device.nearby && typeof device.signalStrength === 'number') {
// Use percentage for visual display
const signalPercentage = device.signalStrengthPercentage || Math.max(0, Math.round(((device.signalStrength + 100) / 70) * 100));
const signalColor = getSignalColor(signalPercentage);
// Calculate how many bars should be active based on signal strength percentage
const activeBars = Math.ceil(signalPercentage / 20); // 0-20% = 1 bar, 21-40% = 2 bars, etc.
deviceContent += `<div class="bluetooth-signal-bar-container">
<div class="bluetooth-signal-bars">`;
for (let i = 1; i <= 5; i++) {
const isActive = i <= activeBars;
deviceContent += `<div class="bluetooth-signal-bar ${isActive ? 'active' : ''}" style="color: ${signalColor};"></div>`;
}
deviceContent += `</div></div>`;
} else if (device.nearby) {
// Fallback if signal strength not available
deviceContent += `<span class="bluetooth-device-icon">📶</span>`;
}
if (device.saved) {
deviceContent += `<span class="bluetooth-device-icon">💾</span>`;
}
if (device.inInventory) {
deviceContent += `<span class="bluetooth-device-icon">🎒</span>`;
}
deviceContent += `</div></div>`;
deviceContent += `<div class="bluetooth-device-details">MAC: ${device.mac}\n${device.details}</div>`;
deviceContent += `<div class="bluetooth-device-timestamp">Last seen: ${formattedDate} ${formattedTime}</div>`;
deviceElement.innerHTML = deviceContent;
// Toggle expanded state when clicked
deviceElement.addEventListener('click', (event) => {
deviceElement.classList.toggle('expanded');
// Mark as saved when expanded
if (!device.saved && deviceElement.classList.contains('expanded')) {
device.saved = true;
updateBluetoothCount();
updateBluetoothPanel();
syncBluetoothDevices();
}
});
bluetoothContent.appendChild(deviceElement);
});
}
updateBluetoothCount();
}
// Update the new Bluetooth devices count
export function updateBluetoothCount() {
const bluetoothCount = document.getElementById('bluetooth-count');
if (bluetoothCount) {
const nearbyCount = bluetoothDevices.filter(device => device.nearby).length;
bluetoothCount.textContent = nearbyCount;
newBluetoothDevices = bluetoothDevices.filter(device => !device.saved && device.nearby).length;
bluetoothCount.textContent = newBluetoothDevices;
bluetoothCount.style.display = newBluetoothDevices > 0 ? 'flex' : 'none';
}
}
@@ -257,16 +443,44 @@ export function toggleBluetoothPanel() {
const isVisible = bluetoothPanel.style.display === 'block';
bluetoothPanel.style.display = isVisible ? 'none' : 'block';
// Update panel content when opening
// Always update panel content when opening to show current state
if (!isVisible) {
updateBluetoothPanel();
// Reset the throttle timer so updates happen immediately when panel is open
lastBluetoothPanelUpdate = 0;
}
}
// Function to unlock a Bluetooth-locked inventory item by MAC address
export function unlockInventoryDeviceByMac(mac) {
console.log('Attempting to unlock inventory device with MAC:', mac);
// Normalize MAC address for comparison
const normalizedMac = mac.toLowerCase();
// Find the inventory item with this MAC address
const item = window.inventory.items.find(item =>
item.scenarioData?.mac?.toLowerCase() === normalizedMac &&
item.scenarioData?.lockType === "bluetooth" &&
item.scenarioData?.locked
);
if (!item) {
console.error('Inventory item not found with MAC:', mac);
if (window.gameAlert) {
window.gameAlert("Device not found in inventory.", 'error', 'Unlock Failed', 3000);
}
return;
}
console.log('Found inventory item to unlock:', item);
}
// Export for global access
window.initializeBluetoothPanel = initializeBluetoothPanel;
window.checkBluetoothDevices = checkBluetoothDevices;
window.addBluetoothDevice = addBluetoothDevice;
window.toggleBluetoothPanel = toggleBluetoothPanel;
window.updateBluetoothPanel = updateBluetoothPanel;
window.updateBluetoothCount = updateBluetoothCount;
window.updateBluetoothCount = updateBluetoothCount;
window.unlockInventoryDeviceByMac = unlockInventoryDeviceByMac;

View File

@@ -861,8 +861,8 @@ function startLockpickingMinigame(lockable, scene, difficulty = 'medium', callba
window.MinigameFramework.init(scene);
}
// Start the lockpicking minigame
window.MinigameFramework.startMinigame('lockpicking', {
// Start the lockpicking minigame (Phaser version)
window.MinigameFramework.startMinigame('lockpicking', null, {
lockable: lockable,
difficulty: difficulty,
onComplete: (success, result) => {

View File

@@ -4,8 +4,14 @@
import { showNotification } from './notifications.js?v=5';
import { formatTime } from '../utils/helpers.js?v=16';
// Game notes array
const gameNotes = [];
// Initialize game state if not exists
if (!window.gameState) {
window.gameState = {};
}
if (!window.gameState.notes) {
window.gameState.notes = [];
}
let unreadNotes = 0;
// Initialize the notes system
@@ -26,10 +32,12 @@ export function initializeNotes() {
const categories = document.querySelectorAll('.notes-category');
categories.forEach(category => {
category.addEventListener('click', () => {
console.log('NOTES DEBUG: Category clicked:', category.dataset.category);
// Remove active class from all categories
categories.forEach(c => c.classList.remove('active'));
// Add active class to clicked category
category.classList.add('active');
console.log('NOTES DEBUG: Active category set to:', category.dataset.category);
// Update notes panel
updateNotesPanel();
});
@@ -43,8 +51,10 @@ export function initializeNotes() {
// Add a note to the notes panel
export function addNote(title, text, important = false) {
console.log('NOTES DEBUG: Adding note', { title, important, textLength: text.length });
// Check if a note with the same title and text already exists
const existingNote = gameNotes.find(note => note.title === title && note.text === text);
const existingNote = window.gameState.notes.find(note => note.title === title && note.text === text);
// If the note already exists, don't add it again but mark it as read
if (existingNote) {
@@ -69,7 +79,9 @@ export function addNote(title, text, important = false) {
important: important
};
gameNotes.push(note);
console.log('NOTES DEBUG: Note created', note);
window.gameState.notes.push(note);
updateNotesPanel();
updateNotesCount();
@@ -87,8 +99,18 @@ export function updateNotesPanel() {
// Get active category
const activeCategory = document.querySelector('.notes-category.active')?.dataset.category || 'all';
console.log('NOTES DEBUG: Updating panel', {
activeCategory,
totalNotes: window.gameState.notes.length,
notesData: window.gameState.notes.map(note => ({
title: note.title,
important: note.important,
read: note.read
}))
});
// Filter notes based on search and category
let filteredNotes = [...gameNotes];
let filteredNotes = [...window.gameState.notes];
// Apply category filter
if (activeCategory === 'important') {
@@ -97,6 +119,16 @@ export function updateNotesPanel() {
filteredNotes = filteredNotes.filter(note => !note.read);
}
console.log('NOTES DEBUG: After filtering', {
activeCategory,
filteredCount: filteredNotes.length,
filteredNotes: filteredNotes.map(note => ({
title: note.title,
important: note.important,
read: note.read
}))
});
// Apply search filter
if (searchTerm) {
filteredNotes = filteredNotes.filter(note =>
@@ -171,7 +203,7 @@ export function updateNotesPanel() {
// Update the unread notes count
export function updateNotesCount() {
const notesCount = document.getElementById('notes-count');
unreadNotes = gameNotes.filter(note => !note.read).length;
unreadNotes = window.gameState.notes.filter(note => !note.read).length;
notesCount.textContent = unreadNotes;
notesCount.style.display = unreadNotes > 0 ? 'flex' : 'none';

228
lockpicking-comparison.html Normal file
View File

@@ -0,0 +1,228 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lockpicking Minigame Comparison</title>
<style>
body {
font-family: Arial, sans-serif;
background: #1a1a1a;
color: #ffffff;
margin: 0;
padding: 20px;
}
.comparison-container {
display: flex;
gap: 20px;
max-width: 1400px;
margin: 0 auto;
}
.minigame-section {
flex: 1;
background: #2a2a2a;
border-radius: 10px;
padding: 20px;
border: 2px solid #444;
}
.minigame-section h2 {
text-align: center;
margin-bottom: 20px;
color: #00ff00;
}
.minigame-container {
background: #333;
border-radius: 5px;
padding: 15px;
min-height: 500px;
}
.controls {
margin-top: 20px;
text-align: center;
}
.btn {
background: #00ff00;
color: #000;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
margin: 5px;
}
.btn:hover {
background: #00cc00;
}
.btn:disabled {
background: #666;
cursor: not-allowed;
}
.info-panel {
background: #444;
border-radius: 5px;
padding: 15px;
margin-top: 20px;
}
.info-panel h3 {
margin-top: 0;
color: #00ff00;
}
.feature-list {
list-style: none;
padding: 0;
}
.feature-list li {
padding: 5px 0;
border-bottom: 1px solid #555;
}
.feature-list li:last-child {
border-bottom: none;
}
.feature-list .pro {
color: #00ff00;
}
.feature-list .con {
color: #ff4444;
}
.header {
text-align: center;
margin-bottom: 30px;
}
.header h1 {
color: #00ff00;
margin-bottom: 10px;
}
.header p {
color: #ccc;
font-size: 16px;
}
</style>
</head>
<body>
<div class="header">
<h1>Lockpicking Minigame Comparison</h1>
<p>Compare the original HTML/JS version with the new Phaser.js implementation</p>
</div>
<div class="comparison-container">
<div class="minigame-section">
<h2>Original HTML/JS Version</h2>
<div class="minigame-container" id="original-container">
<!-- Original minigame will be loaded here -->
</div>
<div class="controls">
<button class="btn" onclick="startOriginal()">Start Original</button>
<button class="btn" onclick="stopOriginal()">Stop</button>
</div>
<div class="info-panel">
<h3>Features</h3>
<ul class="feature-list">
<li class="pro">✓ Lightweight - no additional dependencies</li>
<li class="pro">✓ Simple DOM manipulation</li>
<li class="pro">✓ Easy to customize with CSS</li>
<li class="con">✗ Limited animation capabilities</li>
<li class="con">✗ Basic graphics rendering</li>
<li class="con">✗ Manual input handling</li>
</ul>
</div>
</div>
<div class="minigame-section">
<h2>Phaser.js Version</h2>
<div class="minigame-container" id="phaser-container">
<!-- Phaser minigame will be loaded here -->
</div>
<div class="controls">
<button class="btn" onclick="startPhaser()">Start Phaser</button>
<button class="btn" onclick="stopPhaser()">Stop</button>
</div>
<div class="info-panel">
<h3>Features</h3>
<ul class="feature-list">
<li class="pro">✓ Rich graphics and animations</li>
<li class="pro">✓ Built-in game engine features</li>
<li class="pro">✓ Professional game development tools</li>
<li class="con">✗ Larger bundle size</li>
<li class="con">✗ More complex setup</li>
<li class="con">✗ Learning curve for Phaser API</li>
</ul>
</div>
</div>
</div>
<!-- Load Phaser.js -->
<script src="https://cdn.jsdelivr.net/npm/phaser@3.60.0/dist/phaser.min.js"></script>
<!-- Load minigame framework -->
<script type="module">
import { MinigameFramework } from './js/minigames/index.js';
let originalMinigame = null;
let phaserMinigame = null;
window.startOriginal = function() {
if (originalMinigame) {
originalMinigame.cleanup();
}
const container = document.getElementById('original-container');
container.setAttribute('data-external', 'true');
originalMinigame = MinigameFramework.startMinigame('lockpicking-legacy', container, {
lockable: 'test-lock',
difficulty: 'medium'
});
};
window.stopOriginal = function() {
if (originalMinigame) {
originalMinigame.complete(false);
originalMinigame = null;
}
};
window.startPhaser = function() {
if (phaserMinigame) {
phaserMinigame.cleanup();
}
const container = document.getElementById('phaser-container');
container.setAttribute('data-external', 'true');
phaserMinigame = MinigameFramework.startMinigame('lockpicking', container, {
lockable: 'test-lock',
difficulty: 'medium'
});
};
window.stopPhaser = function() {
if (phaserMinigame) {
phaserMinigame.complete(false);
phaserMinigame = null;
}
};
// Auto-start both minigames after a short delay
setTimeout(() => {
startOriginal();
startPhaser();
}, 1000);
</script>
</body>
</html>

560
locksmith-forge.html Normal file
View File

@@ -0,0 +1,560 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Locksmith Forge - Lockpicking Challenges</title>
<!-- Google Fonts - Press Start 2P, VT323 -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=VT323&display=swap" rel="stylesheet">
<!-- Web Font Loader script to ensure fonts load properly -->
<script src="https://ajax.googleapis.com/ajax/libs/webfont/1.6.26/webfont.js"></script>
<script>
WebFont.load({
google: {
families: ['Press Start 2P', 'VT323']
},
active: function() {
console.log('Fonts loaded successfully');
}
});
</script>
<style>
body {
font-family: 'VT323', monospace;
background: #333;
color: #ffffff;
margin: 0;
padding: 20px;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
}
.minigame-close-button, #minigame-cancel {
display: none;
}
.header {
background: rgba(0, 0, 0, 0.95);
padding: 20px;
border-radius: 10px;
margin-bottom: 20px;
border: 2px solid #444;
box-shadow: 0 0 30px rgba(0, 0, 0, 0.8);
max-width: 800px;
width: 100%;
color: #00ff00;
}
.level-display {
font-family: 'Press Start 2P', monospace;
font-size: 20px;
font-weight: bold;
margin-bottom: 15px;
text-shadow: 0 0 10px #00ff00;
}
.stats {
display: flex;
justify-content: space-around;
font-size: 14px;
}
.stat {
background: #333;
padding: 8px 15px;
border-radius: 5px;
border: 1px solid #00ff00;
}
.game-container {
background: rgba(0, 0, 0, 0.95);
border-radius: 10px;
padding: 20px;
border: 2px solid #444;
box-shadow: 0 0 30px rgba(0, 0, 0, 0.8);
margin-bottom: 20px;
min-height: 400px;
position: relative;
max-width: 800px;
width: 100%;
}
#gameContainer {
width: 100%;
background: #1a1a1a;
/* border: 1px solid #444; */
border-radius: 5px;
position: relative;
}
.phaser-game-container {
width: 100%;
height: 100%;
position: relative;
}
canvas {
display: block;
margin: 0 auto;
}
input:disabled, select:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.control-group label {
color: #00ff00;
font-weight: bold;
}
.control-group input:disabled + .range-value {
color: #00ff00;
font-weight: bold;
}
.controls {
background: #333;
padding: 15px;
border-radius: 10px;
margin-bottom: 20px;
border: 1px solid #00ff00;
}
.control-group {
margin: 10px 0;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
label {
font-weight: bold;
min-width: 120px;
text-align: right;
}
input[type="range"] {
width: 200px;
}
.range-value {
min-width: 40px;
text-align: left;
}
button {
background: #00ff00;
color: #000;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-family: 'Courier New', monospace;
font-weight: bold;
margin: 5px;
}
button:hover {
background: #00cc00;
}
button:disabled {
background: #666;
cursor: not-allowed;
}
.status {
background: rgba(0, 0, 0, 0.8);
padding: 15px;
border-radius: 5px;
margin: 10px 0;
border: 1px solid #444;
min-height: 20px;
font-family: 'VT323', monospace;
font-size: 18px;
max-width: 800px;
width: 100%;
}
.progress-bar {
width: 100%;
height: 20px;
background: rgba(0, 0, 0, 0.8);
border-radius: 10px;
border: 1px solid #444;
overflow: hidden;
margin: 10px 0;
max-width: 800px;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #00ff00, #00cc00);
width: 0%;
transition: width 0.3s ease;
}
.achievement {
font-family: 'Press Start 2P', monospace;
background: #ffaa00;
color: #000;
padding: 10px;
border-radius: 5px;
margin: 10px 0;
font-weight: bold;
animation: glow 2s ease-in-out infinite alternate;
}
@keyframes glow {
from { box-shadow: 0 0 5px #ffaa00; }
to { box-shadow: 0 0 20px #ffaa00, 0 0 30px #ffaa00; }
}
</style>
</head>
<body>
<div class="header">
<div class="level-display">LEVEL <span id="currentLevel">1</span></div>
<div class="stats">
<div class="stat">Pins: <span id="pinCount">3</span></div>
<div class="stat">Difficulty: <span id="difficulty">Easy</span></div>
<div class="stat">Sensitivity: <span id="sensitivity">5</span></div>
<div class="stat">Lift Speed: <span id="liftSpeed">1.0</span></div>
</div>
</div>
<div class="game-container">
<div id="gameContainer"></div>
</div>
<div class="controls" style="display: none;">
<!-- Controls hidden - parameters are automatically set by level -->
<div class="control-group">
<label>Threshold Sensitivity:</label>
<input type="range" id="thresholdSensitivity" min="1" max="10" value="5" step="1" disabled>
<span class="range-value" id="thresholdSensitivityValue">5</span>
</div>
<div class="control-group">
<label>Highlight Binding Order:</label>
<select id="highlightBindingOrder" disabled>
<option value="enabled">Enabled</option>
<option value="disabled">Disabled</option>
</select>
</div>
<div class="control-group">
<label>Pin Alignment Highlighting:</label>
<select id="pinAlignmentHighlighting" disabled>
<option value="enabled">Enabled</option>
<option value="disabled">Disabled</option>
</select>
</div>
<div class="control-group">
<label>Lift Speed:</label>
<input type="range" id="liftSpeedRange" min="0.5" max="3.0" value="1.0" step="0.1" disabled>
<span class="range-value" id="liftSpeedValue">1.0</span>
</div>
<div class="control-group">
<button id="startButton">Restart Level</button>
<button id="resetButton">Reset Progress</button>
<button id="skipButton">Skip Level</button>
</div>
</div>
<div class="status" id="status">Ready to start Level 1</div>
<div class="progress-bar">
<div class="progress-fill" id="progressFill"></div>
</div>
<div id="achievements"></div>
<script src="https://cdn.jsdelivr.net/npm/phaser@3.60.0/dist/phaser.min.js"></script>
<script type="module">
import { LockpickingMinigamePhaser } from './js/minigames/lockpicking/lockpicking-game-phaser.js';
class ProgressiveLockpicking {
constructor() {
this.currentLevel = 1;
this.maxLevel = 50;
this.successCount = 0;
this.failureCount = 0;
this.currentGame = null;
this.levelConfig = this.generateLevelConfig();
this.initializeUI();
this.bindEvents();
this.updateDisplay();
}
generateLevelConfig() {
const config = {};
for (let level = 1; level <= this.maxLevel; level++) {
// Base progression
let pinCount = Math.min(3 + Math.floor((level - 1) / 5), 8); // 3-8 pins
let difficulty = this.getDifficulty(level);
let sensitivity = Math.max(1, Math.min(10, 5 + Math.floor((level - 1) / 3))); // 1-10
let liftSpeed = Math.max(0.5, Math.min(3.0, 1.0 + (level - 1) * 0.1)); // 0.5-3.0
// Add some randomness and complexity
if (level > 10) {
// Randomly disable hints for higher levels
const disableHints = Math.random() > 0.7;
if (disableHints) {
sensitivity = Math.max(1, sensitivity - 2);
}
}
if (level > 20) {
// Increase difficulty more aggressively
liftSpeed = Math.min(3.0, liftSpeed + 0.2);
}
if (level > 30) {
// Very challenging levels
pinCount = Math.min(8, pinCount + 1);
sensitivity = Math.max(1, sensitivity - 1);
}
config[level] = {
pinCount,
difficulty,
sensitivity,
liftSpeed: Math.round(liftSpeed * 10) / 10,
highlightBindingOrder: level <= 15 ? 'enabled' : (Math.random() > 0.5 ? 'enabled' : 'disabled'),
pinAlignmentHighlighting: level <= 10 ? 'enabled' : (Math.random() > 0.6 ? 'enabled' : 'disabled')
};
}
return config;
}
getDifficulty(level) {
if (level <= 5) return 'easy';
if (level <= 15) return 'medium';
if (level <= 30) return 'difficult';
if (level <= 40) return 'hard';
return 'expert';
}
initializeUI() {
// Initialize range inputs
this.updateRangeValue('thresholdSensitivity');
this.updateRangeValue('liftSpeedRange');
// Set initial values from level config
this.updateControlsFromLevel();
}
bindEvents() {
// Range input events
document.getElementById('thresholdSensitivity').addEventListener('input', (e) => {
this.updateRangeValue('thresholdSensitivity');
});
document.getElementById('liftSpeedRange').addEventListener('input', (e) => {
this.updateRangeValue('liftSpeedRange');
});
// Button events
document.getElementById('startButton').addEventListener('click', () => {
this.startChallenge();
});
document.getElementById('resetButton').addEventListener('click', () => {
this.resetLevel();
});
document.getElementById('skipButton').addEventListener('click', () => {
this.skipLevel();
});
}
updateRangeValue(id) {
const input = document.getElementById(id);
const value = input.value;
const valueDisplay = document.getElementById(id + 'Value');
if (valueDisplay) {
valueDisplay.textContent = value;
}
}
updateControlsFromLevel() {
const config = this.levelConfig[this.currentLevel];
if (!config) return;
document.getElementById('thresholdSensitivity').value = config.sensitivity;
document.getElementById('liftSpeedRange').value = config.liftSpeed;
document.getElementById('highlightBindingOrder').value = config.highlightBindingOrder;
document.getElementById('pinAlignmentHighlighting').value = config.pinAlignmentHighlighting;
this.updateRangeValue('thresholdSensitivity');
this.updateRangeValue('liftSpeedRange');
}
startChallenge() {
if (this.currentGame) {
this.currentGame.cleanup();
}
const config = this.levelConfig[this.currentLevel];
const params = {
pinCount: config.pinCount,
difficulty: config.difficulty,
thresholdSensitivity: parseInt(document.getElementById('thresholdSensitivity').value),
highlightingBindingOrder: document.getElementById('highlightBindingOrder').value,
pinAlignmentHighlighting: document.getElementById('pinAlignmentHighlighting').value,
liftSpeed: parseFloat(document.getElementById('liftSpeedRange').value),
lockable: { id: 'progressive-challenge' },
closeButtonText: 'Reset',
closeButtonAction: 'reset'
};
this.updateStatus(`Starting Level ${this.currentLevel}...`);
console.log('Creating minigame with params:', params);
console.log('Game container:', document.getElementById('gameContainer'));
this.currentGame = new LockpickingMinigamePhaser(
document.getElementById('gameContainer'),
params
);
console.log('Minigame created:', this.currentGame);
// Initialize the minigame
this.currentGame.init();
console.log('Minigame initialized');
// Start the minigame
this.currentGame.start();
console.log('Minigame started');
// Listen for completion by overriding the complete method
const originalComplete = this.currentGame.complete.bind(this.currentGame);
this.currentGame.complete = (success) => {
console.log('Minigame completed with success:', success);
this.handleChallengeComplete(success);
originalComplete(success);
};
}
handleChallengeComplete(success) {
if (success) {
this.successCount++;
this.updateStatus(`Level ${this.currentLevel} completed successfully!`);
this.showAchievement(`Level ${this.currentLevel} Complete!`);
// Progress to next level after a delay
setTimeout(() => {
this.currentLevel = Math.min(this.currentLevel + 1, this.maxLevel);
this.updateDisplay();
this.updateControlsFromLevel();
this.updateStatus(`Starting Level ${this.currentLevel}...`);
// Auto-start the next level
setTimeout(() => {
this.startChallenge();
}, 1000);
}, 2000);
} else {
this.failureCount++;
this.updateStatus(`Level ${this.currentLevel} failed. Retrying...`);
// Auto-retry after a delay
setTimeout(() => {
this.startChallenge();
}, 2000);
}
this.updateProgress();
}
resetLevel() {
this.currentLevel = 1;
this.successCount = 0;
this.failureCount = 0;
this.updateDisplay();
this.updateControlsFromLevel();
this.updateStatus(`Progress reset. Ready for Level ${this.currentLevel}`);
this.updateProgress();
// Auto-start the first level
setTimeout(() => {
this.startChallenge();
}, 500);
}
skipLevel() {
this.currentLevel = Math.min(this.currentLevel + 1, this.maxLevel);
this.updateDisplay();
this.updateControlsFromLevel();
this.updateStatus(`Skipped to Level ${this.currentLevel}`);
this.updateProgress();
}
updateDisplay() {
document.getElementById('currentLevel').textContent = this.currentLevel;
const config = this.levelConfig[this.currentLevel];
if (config) {
document.getElementById('pinCount').textContent = config.pinCount;
document.getElementById('difficulty').textContent = config.difficulty;
document.getElementById('sensitivity').textContent = config.sensitivity;
document.getElementById('liftSpeed').textContent = config.liftSpeed;
}
}
updateStatus(message) {
document.getElementById('status').textContent = message;
}
updateProgress() {
const progress = (this.currentLevel - 1) / this.maxLevel * 100;
document.getElementById('progressFill').style.width = progress + '%';
}
showAchievement(message) {
const achievements = document.getElementById('achievements');
const achievement = document.createElement('div');
achievement.className = 'achievement';
achievement.textContent = message;
achievements.appendChild(achievement);
// Remove after 5 seconds
setTimeout(() => {
if (achievement.parentNode) {
achievement.parentNode.removeChild(achievement);
}
}, 5000);
}
}
// Initialize the progressive lockpicking system
document.addEventListener('DOMContentLoaded', () => {
const progressiveSystem = new ProgressiveLockpicking();
// Auto-start the first level
setTimeout(() => {
progressiveSystem.startChallenge();
}, 500);
});
</script>
</body>
</html>

View File

@@ -41,6 +41,7 @@
"takeable": true,
"locked": true,
"lockType": "bluetooth",
"requires": "bluetooth",
"mac": "00:11:22:33:44:55",
"observations": "A locked tablet device that requires Bluetooth pairing"
},

View File

@@ -0,0 +1,307 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Phaser Lockpicking Test</title>
<!-- Google Fonts - Press Start 2P, VT323 -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=VT323&display=swap" rel="stylesheet">
<!-- Web Font Loader script to ensure fonts load properly -->
<script src="https://ajax.googleapis.com/ajax/libs/webfont/1.6.26/webfont.js"></script>
<script>
WebFont.load({
google: {
families: ['Press Start 2P', 'VT323']
},
active: function() {
console.log('Fonts loaded successfully');
}
});
</script>
<style>
body {
font-family: 'VT323', monospace;
background: #1a1a1a;
color: #ffffff;
margin: 0;
padding: 20px;
}
.test-container {
max-width: 1000px;
margin: 0 auto;
background: #2a2a2a;
border-radius: 10px;
padding: 20px;
}
.test-container h1 {
text-align: center;
color: #00ff00;
font-family: 'Press Start 2P', cursive;
font-size: 24px;
margin-bottom: 20px;
}
.controls-section {
background: #333;
border-radius: 5px;
padding: 15px;
margin-bottom: 20px;
}
.controls-section h2 {
color: #00ff00;
font-family: 'Press Start 2P', cursive;
font-size: 14px;
margin-top: 0;
margin-bottom: 15px;
}
.control-group {
display: flex;
align-items: center;
margin-bottom: 10px;
gap: 10px;
}
.control-group label {
min-width: 120px;
font-size: 16px;
}
.control-group input, .control-group select {
background: #444;
border: 1px solid #666;
color: #fff;
padding: 5px 10px;
border-radius: 3px;
font-family: 'VT323', monospace;
font-size: 16px;
}
.control-group input[type="range"] {
flex: 1;
min-width: 150px;
}
.range-value {
min-width: 30px;
text-align: center;
font-size: 16px;
}
.minigame-container {
background: #333;
border-radius: 5px;
padding: 15px;
min-height: 500px;
margin-bottom: 20px;
}
.controls {
text-align: center;
}
.btn {
background: #00ff00;
color: #000;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-family: 'Press Start 2P', cursive;
font-size: 12px;
margin: 5px;
}
.btn:hover {
background: #00cc00;
}
.btn:disabled {
background: #666;
color: #999;
cursor: not-allowed;
}
.status {
text-align: center;
margin-top: 10px;
font-size: 16px;
color: #00ff00;
}
</style>
</head>
<body>
<div class="test-container">
<h1>Phaser Lockpicking Minigame Test</h1>
<div class="controls-section">
<h2>Game Parameters</h2>
<div class="control-group">
<label for="pinCount">Pin Count:</label>
<input type="range" id="pinCount" min="3" max="8" value="5" step="1">
<span class="range-value" id="pinCountValue">5</span>
</div>
<div class="control-group">
<label for="difficulty">Difficulty:</label>
<select id="difficulty">
<option value="easy">Easy</option>
<option value="medium" selected>Medium</option>
<option value="hard">Hard</option>
</select>
</div>
<div class="control-group">
<label for="lockable">Lockable ID:</label>
<input type="text" id="lockable" value="test-lock" placeholder="Enter lockable ID">
</div>
<div class="control-group">
<label for="thresholdSensitivity">Threshold Sensitivity:</label>
<input type="range" id="thresholdSensitivity" min="1" max="10" value="5" step="1">
<span class="range-value" id="thresholdSensitivityValue">5</span>
</div>
<div class="control-group">
<label for="liftSpeed">Lift Speed:</label>
<input type="range" id="liftSpeed" min="0.5" max="5" value="1" step="0.1">
<span class="range-value" id="liftSpeedValue">1.0</span>
</div>
<div class="control-group">
<label for="highlightBindingOrder">Highlight Binding Order:</label>
<select id="highlightBindingOrder">
<option value="true" selected>Enabled</option>
<option value="false">Disabled</option>
</select>
</div>
<div class="control-group">
<label for="highlightPinAlignment">Highlight Pin Alignment:</label>
<select id="highlightPinAlignment">
<option value="true" selected>Enabled</option>
<option value="false">Disabled</option>
</select>
</div>
</div>
<div class="minigame-container" id="test-container">
<!-- Minigame will be loaded here -->
</div>
<div class="controls">
<button class="btn" id="startBtn" onclick="startTest()">Start Test</button>
<button class="btn" id="stopBtn" onclick="stopTest()" disabled>Stop Test</button>
</div>
<div class="status" id="status">Ready to start</div>
</div>
<!-- Load Phaser.js -->
<script src="https://cdn.jsdelivr.net/npm/phaser@3.60.0/dist/phaser.min.js"></script>
<!-- Load minigame framework -->
<script type="module">
import { MinigameFramework } from './js/minigames/index.js';
let testMinigame = null;
// Update range value displays
function updateRangeValue(rangeId, valueId) {
const range = document.getElementById(rangeId);
const value = document.getElementById(valueId);
value.textContent = range.value;
}
// Initialize range value displays
document.addEventListener('DOMContentLoaded', function() {
updateRangeValue('pinCount', 'pinCountValue');
updateRangeValue('thresholdSensitivity', 'thresholdSensitivityValue');
updateRangeValue('liftSpeed', 'liftSpeedValue');
// Add event listeners for range inputs
document.getElementById('pinCount').addEventListener('input', function() {
updateRangeValue('pinCount', 'pinCountValue');
});
document.getElementById('thresholdSensitivity').addEventListener('input', function() {
updateRangeValue('thresholdSensitivity', 'thresholdSensitivityValue');
});
document.getElementById('liftSpeed').addEventListener('input', function() {
updateRangeValue('liftSpeed', 'liftSpeedValue');
});
});
window.startTest = function() {
if (testMinigame) {
testMinigame.cleanup();
}
const container = document.getElementById('test-container');
container.setAttribute('data-external', 'true');
// Get parameters from controls
const pinCount = parseInt(document.getElementById('pinCount').value);
const difficulty = document.getElementById('difficulty').value;
const lockable = document.getElementById('lockable').value || 'test-lock';
const thresholdSensitivity = parseInt(document.getElementById('thresholdSensitivity').value);
const liftSpeed = parseFloat(document.getElementById('liftSpeed').value);
const highlightBindingOrder = document.getElementById('highlightBindingOrder').value === 'true';
const highlightPinAlignment = document.getElementById('highlightPinAlignment').value === 'true';
// Update UI
document.getElementById('startBtn').disabled = true;
document.getElementById('stopBtn').disabled = false;
const alignmentText = highlightPinAlignment ? 'with' : 'without';
const bindingText = highlightBindingOrder ? 'with' : 'without';
document.getElementById('status').textContent = `Starting with ${pinCount} pins, ${difficulty} difficulty, sensitivity ${thresholdSensitivity}, lift speed ${liftSpeed}, ${alignmentText} alignment highlighting, ${bindingText} binding hints...`;
testMinigame = MinigameFramework.startMinigame('lockpicking', container, {
lockable: lockable,
difficulty: difficulty,
pinCount: pinCount,
thresholdSensitivity: thresholdSensitivity,
liftSpeed: liftSpeed,
highlightBindingOrder: highlightBindingOrder,
highlightPinAlignment: highlightPinAlignment,
onComplete: (success, result) => {
const status = document.getElementById('status');
if (success) {
status.textContent = `Success! Lock picked in ${result.time || 'unknown'} seconds`;
status.style.color = '#00ff00';
} else {
status.textContent = 'Failed to pick lock';
status.style.color = '#ff0000';
}
// Re-enable start button
document.getElementById('startBtn').disabled = false;
document.getElementById('stopBtn').disabled = true;
}
});
};
window.stopTest = function() {
if (testMinigame) {
testMinigame.complete(false);
testMinigame = null;
}
// Update UI
document.getElementById('startBtn').disabled = false;
document.getElementById('stopBtn').disabled = true;
document.getElementById('status').textContent = 'Test stopped';
document.getElementById('status').style.color = '#ffffff';
};
// Auto-start the test after a delay
setTimeout(() => {
startTest();
}, 1000);
</script>
</body>
</html>