diff --git a/README_design.md b/README_design.md
index 6dfce23..f7a57c2 100644
--- a/README_design.md
+++ b/README_design.md
@@ -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)
diff --git a/css/dusting.css b/css/dusting.css
new file mode 100644
index 0000000..b4c1f32
--- /dev/null
+++ b/css/dusting.css
@@ -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;
+}
\ No newline at end of file
diff --git a/css/lockpicking.css b/css/lockpicking.css
new file mode 100644
index 0000000..8ee623f
--- /dev/null
+++ b/css/lockpicking.css
@@ -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%;
+}
\ No newline at end of file
diff --git a/css/minigames-framework.css b/css/minigames-framework.css
new file mode 100644
index 0000000..d8516ab
--- /dev/null
+++ b/css/minigames-framework.css
@@ -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;
+}
diff --git a/css/minigames.css b/css/minigames.css
index ec942ac..e033ba6 100644
--- a/css/minigames.css
+++ b/css/minigames.css
@@ -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;
diff --git a/css/panels.css b/css/panels.css
index cd92b2d..3a891e2 100644
--- a/css/panels.css
+++ b/css/panels.css
@@ -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;
diff --git a/index_new.html b/index_new.html
index 5616569..66a0662 100644
--- a/index_new.html
+++ b/index_new.html
@@ -30,7 +30,7 @@
-
+
diff --git a/js/core/game.js b/js/core/game.js
index 613a0ca..e3ccf6e 100644
--- a/js/core/game.js
+++ b/js/core/game.js
@@ -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();
}
diff --git a/js/core/rooms.js b/js/core/rooms.js
index 456a70e..1d58f27 100644
--- a/js/core/rooms.js
+++ b/js/core/rooms.js
@@ -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;
diff --git a/js/main.js b/js/main.js
index 9ba62f6..c9a1775 100644
--- a/js/main.js
+++ b/js/main.js
@@ -36,6 +36,7 @@ window.gameState = {
biometricSamples: [],
biometricUnlocks: [],
bluetoothDevices: [],
+ notes: [],
startTime: null
};
window.lastBluetoothScan = 0;
diff --git a/js/minigames/dusting/dusting-game.js b/js/minigames/dusting/dusting-game.js
index 5470b92..bbd1727 100644
--- a/js/minigames/dusting/dusting-game.js
+++ b/js/minigames/dusting/dusting-game.js
@@ -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) {
diff --git a/js/minigames/framework/minigame-manager.js b/js/minigames/framework/minigame-manager.js
index 8e90012..18b355e 100644
--- a/js/minigames/framework/minigame-manager.js
+++ b/js/minigames/framework/minigame-manager.js
@@ -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);
}
diff --git a/js/minigames/index.js b/js/minigames/index.js
index bc3d24b..5352b5f 100644
--- a/js/minigames/index.js
+++ b/js/minigames/index.js
@@ -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);
\ No newline at end of file
diff --git a/js/minigames/lockpicking/lockpicking-game-phaser.js b/js/minigames/lockpicking/lockpicking-game-phaser.js
new file mode 100644
index 0000000..d68b16b
--- /dev/null
+++ b/js/minigames/lockpicking/lockpicking-game-phaser.js
@@ -0,0 +1,2079 @@
+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);
+}
+
+// Phaser Lockpicking Minigame Scene implementation
+export class LockpickingMinigamePhaser extends MinigameScene {
+ constructor(container, params) {
+ super(container, params);
+
+ // Ensure params is an object
+ params = params || {};
+
+ this.lockable = params.lockable || 'default-lock';
+ this.difficulty = params.difficulty || 'medium';
+ // Use passed pinCount if provided, otherwise calculate based on difficulty
+ this.pinCount = params.pinCount || (this.difficulty === 'easy' ? 3 : this.difficulty === 'medium' ? 4 : 5);
+
+ // Threshold sensitivity for pin setting (1-10, higher = more sensitive)
+ this.thresholdSensitivity = params.thresholdSensitivity || 5;
+
+ // Whether to highlight binding order
+ this.highlightBindingOrder = params.highlightBindingOrder !== undefined ? params.highlightBindingOrder : true;
+
+ // Whether to highlight pin alignment (shear line proximity)
+ this.highlightPinAlignment = params.highlightPinAlignment !== undefined ? params.highlightPinAlignment : true;
+
+ // Lift speed parameter (can be set to fast values, but reasonable default for hard)
+ this.liftSpeed = params.liftSpeed || (this.difficulty === 'hard' ? 1.2 : 1);
+
+ // Close button customization
+ this.closeButtonText = params.closeButtonText || '×';
+ this.closeButtonAction = params.closeButtonAction || 'close';
+
+ // Sound effects
+ this.sounds = {};
+
+ // Track if any pin has been clicked (for hiding labels)
+ this.pinClicked = false;
+
+ // Log the configuration for debugging
+ console.log('Lockpicking minigame config:', {
+ lockable: this.lockable,
+ difficulty: this.difficulty,
+ pinCount: this.pinCount,
+ passedPinCount: params.pinCount,
+ thresholdSensitivity: this.thresholdSensitivity,
+ highlightBindingOrder: this.highlightBindingOrder,
+ highlightPinAlignment: this.highlightPinAlignment,
+ liftSpeed: this.liftSpeed
+ });
+
+ this.pins = [];
+ this.lockState = {
+ tensionApplied: false,
+ pinsSet: 0,
+ currentPin: null
+ };
+
+ this.game = null;
+ this.scene = null;
+ }
+
+ init() {
+ super.init();
+
+ // Customize the close button
+ const closeBtn = document.getElementById('minigame-close');
+ if (closeBtn) {
+ closeBtn.textContent = this.closeButtonText;
+
+ // Remove the default close action
+ this._eventListeners = this._eventListeners.filter(listener =>
+ !(listener.element === closeBtn && listener.eventType === 'click')
+ );
+
+ // Add custom action based on closeButtonAction parameter
+ if (this.closeButtonAction === 'reset') {
+ this.addEventListener(closeBtn, 'click', () => {
+ this.resetAllPins();
+ this.updateFeedback("Lock reset - try again");
+ });
+ } else {
+ // Default close action
+ this.addEventListener(closeBtn, 'click', () => {
+ this.complete(false);
+ });
+ }
+ }
+
+ // Customize the cancel button
+ const cancelBtn = document.getElementById('minigame-cancel');
+ if (cancelBtn) {
+ cancelBtn.textContent = this.closeButtonText;
+
+ // Remove the default cancel action
+ this._eventListeners = this._eventListeners.filter(listener =>
+ !(listener.element === cancelBtn && listener.eventType === 'click')
+ );
+
+ // Add custom action based on closeButtonAction parameter
+ if (this.closeButtonAction === 'reset') {
+ this.addEventListener(cancelBtn, 'click', () => {
+ this.resetAllPins();
+ this.updateFeedback("Lock reset - try again");
+ });
+ } else {
+ // Default cancel action
+ this.addEventListener(cancelBtn, 'click', () => {
+ this.complete(false);
+ });
+ }
+ }
+
+ this.headerElement.innerHTML = `
+
Lockpicking
+ Apply tension and hold click on pins to lift them to the shear line
+ `;
+
+ this.setupPhaserGame();
+ }
+
+ setupPhaserGame() {
+ // Create a container for the Phaser game
+ this.gameContainer.innerHTML = `
+
+ Ready to pick
+ `;
+
+ this.feedback = this.gameContainer.querySelector('.lockpick-feedback');
+
+ console.log('Setting up Phaser game...');
+
+ // Create a custom Phaser scene
+ const self = this;
+ class LockpickingScene extends Phaser.Scene {
+ constructor() {
+ super({ key: 'LockpickingScene' });
+ }
+
+ preload() {
+ // Load sound effects
+ this.load.audio('lockpick_binding', 'assets/sounds/lockpick_binding.mp3');
+ this.load.audio('lockpick_click', 'assets/sounds/lockpick_click.mp3');
+ this.load.audio('lockpick_overtension', 'assets/sounds/lockpick_overtension.mp3');
+ this.load.audio('lockpick_reset', 'assets/sounds/lockpick_reset.mp3');
+ this.load.audio('lockpick_set', 'assets/sounds/lockpick_set.mp3');
+ this.load.audio('lockpick_success', 'assets/sounds/lockpick_success.mp3');
+ this.load.audio('lockpick_tension', 'assets/sounds/lockpick_tension.mp3');
+ this.load.audio('lockpick_wrong', 'assets/sounds/lockpick_wrong.mp3');
+ }
+
+ create() {
+ console.log('Phaser scene create() called');
+ // Store reference to the scene
+ self.scene = this;
+
+ // Initialize sound effects
+ self.sounds.binding = this.sound.add('lockpick_binding');
+ self.sounds.click = this.sound.add('lockpick_click');
+ self.sounds.overtension = this.sound.add('lockpick_overtension');
+ self.sounds.reset = this.sound.add('lockpick_reset');
+ self.sounds.set = this.sound.add('lockpick_set');
+ self.sounds.success = this.sound.add('lockpick_success');
+ self.sounds.tension = this.sound.add('lockpick_tension');
+ self.sounds.wrong = this.sound.add('lockpick_wrong');
+
+ // Create game elements
+ self.createLockBackground();
+ self.createTensionWrench();
+ self.createPins();
+ self.createHookPick();
+ self.createShearLine();
+ self.setupInputHandlers();
+ self.updateFeedback("Apply tension first, then lift pins in binding order - only the binding pin can be set");
+ console.log('Phaser scene setup complete');
+ }
+
+ update() {
+ if (self.update) {
+ self.update();
+ }
+ }
+ }
+
+ // Initialize Phaser game
+ const config = {
+ type: Phaser.AUTO,
+ parent: 'phaser-game-container',
+ width: 600,
+ height: 400,
+ backgroundColor: '#1a1a1a',
+ scene: LockpickingScene,
+ scale: {
+ mode: Phaser.Scale.FIT,
+ autoCenter: Phaser.Scale.CENTER_BOTH
+ }
+ };
+
+ try {
+ this.game = new Phaser.Game(config);
+ this.scene = this.game.scene.getScene('LockpickingScene');
+ console.log('Phaser game created, scene:', this.scene);
+ } catch (error) {
+ console.error('Error creating Phaser game:', error);
+ this.updateFeedback('Error loading Phaser game: ' + error.message);
+ }
+ }
+
+
+
+ createLockBackground() {
+ const graphics = this.scene.add.graphics();
+ graphics.lineStyle(2, 0x666666);
+ graphics.strokeRect(100, 50, 400, 300);
+ graphics.fillStyle(0x333333);
+ graphics.fillRect(100, 50, 400, 300);
+
+ // Create key cylinder - rectangle from shear line to near bottom
+ this.cylinderGraphics = this.scene.add.graphics();
+ this.cylinderGraphics.fillStyle(0xcd7f32); // Bronze color
+ this.cylinderGraphics.fillRect(100, 155, 400, 180); // From shear line (y=155) to near bottom (y=335)
+ this.cylinderGraphics.lineStyle(1, 0x8b4513); // Darker bronze border
+ this.cylinderGraphics.strokeRect(100, 155, 400, 180);
+
+ // Create keyway - space where key would enter (from halfway up key pins)
+ this.keywayGraphics = this.scene.add.graphics();
+ this.keywayGraphics.fillStyle(0x2a2a2a); // Dark gray for keyway
+ this.keywayGraphics.fillRect(100, 200, 400, 90); // From left edge (x=100), 2/3 height (90 instead of 135)
+ this.keywayGraphics.lineStyle(1, 0x1a1a1a); // Darker border
+ this.keywayGraphics.strokeRect(100, 200, 400, 90);
+ }
+
+ createTensionWrench() {
+ const wrenchX = 80; // Position to the left of the lock
+ const wrenchY = 160; // Position down by half the arm width (5 units) from shear line
+
+ // Create tension wrench container
+ this.tensionWrench = this.scene.add.container(wrenchX, wrenchY);
+
+ // Create L-shaped tension wrench graphics (25% larger)
+ this.wrenchGraphics = this.scene.add.graphics();
+ this.wrenchGraphics.fillStyle(0x888888);
+
+ // Long vertical arm (left side of L) - extended above the lock
+ this.wrenchGraphics.fillRect(0, -120, 10, 170);
+
+ // Short horizontal arm (bottom of L) extending into keyway - 25% larger
+ this.wrenchGraphics.fillRect(0, 40, 37.5, 10);
+
+ this.tensionWrench.add(this.wrenchGraphics);
+
+ // Make it interactive - larger hit area to include horizontal arm
+ // Covers vertical arm, horizontal arm, and handle
+ this.tensionWrench.setInteractive(new Phaser.Geom.Rectangle(-12.5, -138.75, 60, 176.25), Phaser.Geom.Rectangle.Contains);
+
+ // Add text
+ const wrenchText = this.scene.add.text(-10, 50, 'Tension Wrench', {
+ fontSize: '14px',
+ fill: '#00ff00',
+ fontWeight: 'bold'
+ });
+ wrenchText.setOrigin(0.5);
+ wrenchText.setDepth(100); // Bring to front
+ this.tensionWrench.add(wrenchText);
+
+ // Store reference to wrench text for hiding
+ this.wrenchText = wrenchText;
+
+ // Add click handler
+ this.tensionWrench.on('pointerdown', () => {
+ this.lockState.tensionApplied = !this.lockState.tensionApplied;
+
+ // Play tension sound
+ if (this.sounds.tension) {
+ this.sounds.tension.play();
+ }
+
+ if (this.lockState.tensionApplied) {
+ this.wrenchGraphics.clear();
+ this.wrenchGraphics.fillStyle(0x00ff00);
+
+ // Long vertical arm (left side of L) - same dimensions as inactive
+ this.wrenchGraphics.fillRect(0, -120, 10, 170);
+
+ // Short horizontal arm (bottom of L) extending into keyway - same dimensions as inactive
+ this.wrenchGraphics.fillRect(0, 40, 37.5, 10);
+
+ this.updateFeedback("Tension applied. Only the binding pin can be set - others will fall back down.");
+ } else {
+ this.wrenchGraphics.clear();
+ this.wrenchGraphics.fillStyle(0x888888);
+
+ // Long vertical arm (left side of L) - same dimensions as active
+ this.wrenchGraphics.fillRect(0, -120, 10, 170);
+
+ // Short horizontal arm (bottom of L) extending into keyway - same dimensions as active
+ this.wrenchGraphics.fillRect(0, 40, 37.5, 10);
+
+ this.updateFeedback("Tension released. All pins will fall back down.");
+
+ // Play reset sound
+ if (this.sounds.reset) {
+ this.sounds.reset.play();
+ }
+
+ // Reset ALL pins when tension is released (including set and overpicked ones)
+ this.pins.forEach(pin => {
+ pin.isSet = false;
+ pin.isOverpicked = false;
+ pin.currentHeight = 0;
+ pin.keyPinHeight = 0; // Reset key pin height
+ pin.driverPinHeight = 0; // Reset driver pin height
+ pin.overpickingTimer = null; // Reset overpicking timer
+
+ // Reset visual
+ pin.keyPin.clear();
+ pin.keyPin.fillStyle(0xdd3333);
+
+ // Draw rectangular part of key pin
+ pin.keyPin.fillRect(-12, -50 + pin.driverPinLength, 24, pin.keyPinLength - 8);
+
+ // Draw triangular bottom in pixel art style
+ pin.keyPin.fillRect(-12, -50 + pin.driverPinLength + pin.keyPinLength - 8, 24, 2);
+ pin.keyPin.fillRect(-10, -50 + pin.driverPinLength + pin.keyPinLength - 6, 20, 2);
+ pin.keyPin.fillRect(-8, -50 + pin.driverPinLength + pin.keyPinLength - 4, 16, 2);
+ pin.keyPin.fillRect(-6, -50 + pin.driverPinLength + pin.keyPinLength - 2, 12, 2);
+
+ pin.driverPin.clear();
+ pin.driverPin.fillStyle(0x3388dd);
+ pin.driverPin.fillRect(-12, -50, 24, pin.driverPinLength);
+
+ // Reset spring to original position
+ pin.spring.clear();
+ pin.spring.fillStyle(0x666666);
+ const springTop = -130; // Fixed spring top
+ const springBottom = -50; // Driver pin top when not lifted
+ const springHeight = springBottom - springTop;
+
+ // Calculate total spring space and distribute segments evenly
+ const totalSpringSpace = springHeight;
+ const segmentSpacing = totalSpringSpace / 11; // 11 gaps between 12 segments
+
+ for (let s = 0; s < 12; s++) {
+ const segmentHeight = 4;
+ const segmentY = springTop + (s * segmentSpacing);
+ pin.spring.fillRect(-12, segmentY, 24, segmentHeight);
+ }
+
+ // Hide all highlights
+ if (pin.shearHighlight) pin.shearHighlight.setVisible(false);
+ if (pin.setHighlight) pin.setHighlight.setVisible(false);
+ if (pin.bindingHighlight) pin.bindingHighlight.setVisible(false);
+ if (pin.overpickedHighlight) pin.overpickedHighlight.setVisible(false);
+ if (pin.failureHighlight) pin.failureHighlight.setVisible(false);
+ });
+
+ // Reset lock state
+ this.lockState.pinsSet = 0;
+ }
+
+ this.updateBindingPins();
+ });
+ }
+
+ createHookPick() {
+ // Create hook pick that comes in from the left side
+ // Handle is off-screen, long horizontal arm curves up to bottom of key pin 1
+
+ // Calculate pin spacing and margin (same as createPins)
+ const pinSpacing = 400 / (this.pinCount + 1);
+ const margin = pinSpacing * 0.75; // 25% smaller margins
+
+ // Hook target coordinates (can be easily changed to point at any pin or coordinate)
+ const targetX = 100 + margin + (this.pinCount - 1) * pinSpacing; // Last pin X position
+ const targetY = -50 + this.pins[this.pinCount - 1].driverPinLength + this.pins[this.pinCount - 1].keyPinLength; // Last pin bottom Y
+
+ // Hook should start 2/3rds down the keyway (keyway is from y=200 to y=290, so 2/3rds down is y=260)
+ const keywayStartY = 200;
+ const keywayEndY = 290;
+ const keywayHeight = keywayEndY - keywayStartY;
+ const hookEntryY = keywayStartY + (keywayHeight * 2/3); // 2/3rds down the keyway
+
+ // Hook pick dimensions and positioning
+ const handleWidth = 20;
+ const handleHeight = 240; // 4x longer (was 60)
+ const armWidth = 8;
+ const armLength = 140; // Horizontal arm length
+
+ // Start position (handle off-screen to the left)
+ const startX = -120; // Handle starts further off-screen (was -30)
+ const startY = hookEntryY; // Handle center Y position (2/3rds down keyway)
+
+ // Calculate hook dimensions based on target
+ const hookStartX = startX + handleWidth + armLength;
+ const hookStartY = startY;
+
+ // Hook segments configuration
+ const segmentSize = 8;
+ const diagonalSegments = 2; // Number of diagonal segments
+ const verticalSegments = 3; // Number of vertical segments (increased by 1)
+ const segmentStep = 8; // Distance between segment centers
+
+ // Calculate total hook height needed
+ const totalHookHeight = (diagonalSegments + verticalSegments) * segmentStep;
+
+ // Calculate required horizontal length to reach target
+ const requiredHorizontalLength = targetX - hookStartX - totalHookHeight + 48; // Add 48px to reach target (24px + 24px further right)
+
+ // Adjust horizontal length to align with target
+ const curveStartX = hookStartX + requiredHorizontalLength;
+
+ // Calculate the tip position (end of the hook)
+ const tipX = curveStartX + (diagonalSegments * segmentStep);
+ const tipY = hookStartY - (diagonalSegments * segmentStep) - (verticalSegments * segmentStep);
+
+ // Create a container for the hook pick with rotation center at the tip
+ this.hookGroup = this.scene.add.container(0, 0);
+ this.hookGroup.x = tipX;
+ this.hookGroup.y = tipY;
+
+ // Create graphics for hook pick (relative to group center)
+ const hookPickGraphics = this.scene.add.graphics();
+ hookPickGraphics.fillStyle(0x888888); // Gray color for the pick
+ hookPickGraphics.lineStyle(2, 0x888888); // Darker border
+
+ // Calculate positions relative to group center (tip position)
+ const relativeStartX = startX - tipX;
+ const relativeStartY = startY - tipY;
+ const relativeHookStartX = hookStartX - tipX;
+ const relativeCurveStartX = curveStartX - tipX;
+
+ // Draw the handle (off-screen)
+ hookPickGraphics.fillRect(relativeStartX, relativeStartY - handleHeight/2, handleWidth, handleHeight);
+ hookPickGraphics.strokeRect(relativeStartX, relativeStartY - handleHeight/2, handleWidth, handleHeight);
+
+ // Draw the horizontal arm (extends from handle to near the lock)
+ const armStartX = relativeStartX + handleWidth;
+ const armEndX = armStartX + armLength;
+ hookPickGraphics.fillRect(armStartX, relativeStartY - armWidth/2, armLength, armWidth);
+ hookPickGraphics.strokeRect(armStartX, relativeStartY - armWidth/2, armLength, armWidth);
+
+ // Draw horizontal part to curve start
+ hookPickGraphics.fillRect(relativeHookStartX, relativeStartY - armWidth/2, relativeCurveStartX - relativeHookStartX, armWidth);
+ hookPickGraphics.strokeRect(relativeHookStartX, relativeStartY - armWidth/2, relativeCurveStartX - relativeHookStartX, armWidth);
+
+ // Draw the hook segments: diagonal then vertical
+ // First 2 segments: up and right (2x scale)
+ for (let i = 0; i < diagonalSegments; i++) {
+ const x = relativeCurveStartX + (i * segmentStep); // Move right 8px each segment
+ const y = relativeStartY - (i * segmentStep); // Move up 8px each segment
+ hookPickGraphics.fillRect(x - armWidth/2, y - segmentSize/2, armWidth, segmentSize);
+ hookPickGraphics.strokeRect(x - armWidth/2, y - segmentSize/2, armWidth, segmentSize);
+ }
+
+ // Next 3 segments: straight up (increased by 1 segment)
+ for (let i = 0; i < verticalSegments; i++) {
+ const x = relativeCurveStartX + (diagonalSegments * segmentStep); // Stay at the rightmost position from diagonal segments
+ const y = relativeStartY - (diagonalSegments * segmentStep) - (i * segmentStep); // Continue moving up from where we left off
+ hookPickGraphics.fillRect(x - armWidth/2, y - segmentSize/2, armWidth, segmentSize);
+ hookPickGraphics.strokeRect(x - armWidth/2, y - segmentSize/2, armWidth, segmentSize);
+ }
+
+ // Add graphics to container
+ this.hookGroup.add(hookPickGraphics);
+
+ // Add hook pick label
+ const hookPickLabel = this.scene.add.text(-10, 80, 'Hook Pick', {
+ fontSize: '14px',
+ fill: '#00ff00',
+ fontWeight: 'bold'
+ });
+ hookPickLabel.setOrigin(0.5);
+ hookPickLabel.setDepth(100); // Bring to front
+ this.tensionWrench.add(hookPickLabel);
+
+ // Store reference to hook pick label for hiding
+ this.hookPickLabel = hookPickLabel;
+
+ // Debug logging
+ console.log('Hook positioning debug:', {
+ targetX,
+ targetY,
+ hookStartX,
+ hookStartY,
+ tipX,
+ tipY,
+ totalHookHeight,
+ requiredHorizontalLength,
+ curveStartX,
+ pinCount: this.pinCount,
+ pinSpacing,
+ margin
+ });
+
+ // Store reference to hook pick for animations
+ this.hookPickGraphics = hookPickGraphics;
+
+ // Store hook configuration for dynamic updates
+ this.hookConfig = {
+ targetPin: this.pinCount - 1, // Default to last pin (should be 4 for 5 pins)
+ lastTargetedPin: this.pinCount - 1, // Track the last pin that was targeted
+ baseTargetX: targetX,
+ baseTargetY: targetY,
+ hookStartX: hookStartX,
+ hookStartY: hookStartY,
+ diagonalSegments: diagonalSegments,
+ verticalSegments: verticalSegments,
+ segmentStep: segmentStep,
+ segmentSize: segmentSize,
+ armWidth: armWidth,
+ curveStartX: curveStartX,
+ tipX: tipX,
+ tipY: tipY,
+ rotationCenterX: tipX,
+ rotationCenterY: tipY
+ };
+
+ console.log('Hook config initialized - targetPin:', this.hookConfig.targetPin, 'pinCount:', this.pinCount);
+ }
+
+ updateHookPosition(pinIndex) {
+ if (!this.hookGroup || !this.hookConfig) return;
+
+ const config = this.hookConfig;
+ const targetPin = this.pins[pinIndex];
+
+ if (!targetPin) return;
+
+ // Calculate the target Y position (bottom of the key pin)
+ const pinWorldY = 200; // Base Y position for pins
+ const currentTargetY = pinWorldY - 50 + targetPin.driverPinLength + targetPin.keyPinLength - targetPin.currentHeight;
+
+ console.log('Hook update - following pin:', pinIndex, 'currentHeight:', targetPin.currentHeight, 'targetY:', currentTargetY);
+
+ // Update the last targeted pin
+ this.hookConfig.lastTargetedPin = pinIndex;
+
+ // Calculate the pin's X position (same logic as createPins)
+ const pinSpacing = 400 / (this.pinCount + 1);
+ const margin = pinSpacing * 0.75;
+ const pinX = 100 + margin + pinIndex * pinSpacing;
+
+ // Calculate the pin's base Y position (when currentHeight = 0)
+ const pinBaseY = pinWorldY - 50 + targetPin.driverPinLength + targetPin.keyPinLength;
+
+ // Calculate how much the pin has moved from its own base position
+ const heightDifference = pinBaseY - currentTargetY;
+
+ // Calculate rotation angle based on percentage of pin movement and pin number
+ const maxHeightDifference = 50; // Maximum expected height difference
+ const minRotationDegrees = 20; // Minimum rotation for highest pin
+ const maxRotationDegrees = 40; // Maximum rotation for lowest pin
+
+ // Calculate pin-based rotation range (pin 0 = max rotation, pin n-1 = min rotation)
+ const pinRotationRange = maxRotationDegrees - minRotationDegrees;
+ const pinRotationFactor = pinIndex / (this.pinCount - 1); // 0 for first pin, 1 for last pin
+ const pinRotationOffset = pinRotationRange * pinRotationFactor;
+ const pinMaxRotation = maxRotationDegrees - pinRotationOffset;
+
+ // Calculate percentage of pin movement (0% to 100%)
+ const pinMovementPercentage = Math.min((heightDifference / maxHeightDifference) * 100, 100);
+
+ // Calculate rotation based on percentage and pin-specific max rotation
+ // Higher pin indices (further pins) rotate slower by reducing the percentage
+ const pinSpeedFactor = 1 - (pinIndex / this.pinCount) * 0.5; // 1.0 for pin 0, 0.5 for last pin
+ const adjustedPercentage = pinMovementPercentage * pinSpeedFactor;
+ const rotationAngle = (adjustedPercentage / 100) * pinMaxRotation;
+
+ // Calculate the new tip position (hook should point at the current pin)
+ const totalHookHeight = (config.diagonalSegments + config.verticalSegments) * config.segmentStep;
+ const newTipX = pinX - totalHookHeight + 34; // Add 34px offset (24px + 10px further right)
+
+ // Update hook position and rotation
+ this.hookGroup.x = newTipX;
+ this.hookGroup.y = currentTargetY;
+ this.hookGroup.setAngle(-rotationAngle); // Negative for anti-clockwise rotation
+
+ // Check for collisions with other pins using hook's current position
+ this.checkHookCollisions(pinIndex, this.hookGroup.y);
+
+ console.log('Hook update - pinX:', pinX, 'newTipX:', newTipX, 'currentTargetY:', currentTargetY, 'heightDifference:', heightDifference, 'pinMaxRotation:', pinMaxRotation, 'pinMovementPercentage:', pinMovementPercentage.toFixed(1) + '%', 'pinSpeedFactor:', pinSpeedFactor.toFixed(2), 'rotationAngle:', rotationAngle.toFixed(1));
+ }
+
+ returnHookToStart() {
+ if (!this.hookGroup || !this.hookConfig) return;
+
+ const config = this.hookConfig;
+
+ console.log('Returning hook to starting position (no rotation)');
+
+ // Get the current X position from the last targeted pin
+ const pinSpacing = 400 / (this.pinCount + 1);
+ const margin = pinSpacing * 0.75;
+ const targetPinIndex = config.lastTargetedPin;
+ const currentX = 100 + margin + targetPinIndex * pinSpacing; // Last targeted pin's X position
+
+ // Calculate the tip position for the current pin
+ const totalHookHeight = (config.diagonalSegments + config.verticalSegments) * config.segmentStep;
+ const tipX = currentX - totalHookHeight + 48; // Add 48px offset (24px + 24px further right)
+
+ // Calculate resting Y position (a few pixels lower than original)
+ const restingY = config.hookStartY - 24; // 24px lower than original position (was 15px)
+
+ // Reset position and rotation
+ this.hookGroup.x = tipX;
+ this.hookGroup.y = restingY;
+ this.hookGroup.setAngle(0);
+
+ // Clear debug graphics when hook returns to start
+ if (this.debugGraphics) {
+ this.debugGraphics.clear();
+ }
+ }
+
+ checkHookCollisions(targetPinIndex, hookCurrentY) {
+ if (!this.hookConfig || !this.gameState.mouseDown) return;
+
+ // Clear previous debug graphics
+ if (this.debugGraphics) {
+ this.debugGraphics.clear();
+ } else {
+ this.debugGraphics = this.scene.add.graphics();
+ this.debugGraphics.setDepth(100); // Render on top
+ }
+
+ // Create a temporary rectangle for the hook's horizontal arm using Phaser's physics
+ const hookArmWidth = 8;
+ const hookArmLength = 100;
+
+ // Calculate the horizontal arm position relative to the hook's current position
+ // The horizontal arm extends from the handle to the curve start
+ const handleStartX = -120; // Handle starts at -120
+ const handleWidth = 20;
+ const armStartX = handleStartX + handleWidth; // Arm starts after handle (-100)
+ const armEndX = armStartX + hookArmLength; // Arm ends at +40
+
+ // Position the collision box lower along the arm (not at the tip)
+ const collisionOffsetY = 35; // Move collision box down by 2350px
+
+ // Convert to world coordinates with rotation
+ const hookAngle = this.hookGroup.angle * (Math.PI / 180); // Convert degrees to radians
+ const cosAngle = Math.cos(hookAngle);
+ const sinAngle = Math.sin(hookAngle);
+
+ // Calculate rotated arm start and end points
+ const armStartX_rotated = armStartX * cosAngle - collisionOffsetY * sinAngle;
+ const armStartY_rotated = armStartX * sinAngle + collisionOffsetY * cosAngle;
+ const armEndX_rotated = armEndX * cosAngle - collisionOffsetY * sinAngle;
+ const armEndY_rotated = armEndX * sinAngle + collisionOffsetY * cosAngle;
+
+ // Convert to world coordinates
+ const worldArmStartX = armStartX_rotated + this.hookGroup.x;
+ const worldArmStartY = armStartY_rotated + this.hookGroup.y;
+ const worldArmEndX = armEndX_rotated + this.hookGroup.x;
+ const worldArmEndY = armEndY_rotated + this.hookGroup.y;
+
+ // Create a line for the rotated arm (this is what we'll use for collision detection)
+ const hookArmLine = new Phaser.Geom.Line(worldArmStartX, worldArmStartY, worldArmEndX, worldArmEndY);
+
+ // // Render hook arm hitbox (red) - draw as a line to show rotation
+ // this.debugGraphics.lineStyle(3, 0xff0000);
+ // this.debugGraphics.beginPath();
+ // this.debugGraphics.moveTo(worldArmStartX, worldArmStartY);
+ // this.debugGraphics.lineTo(worldArmEndX, worldArmEndY);
+ // this.debugGraphics.strokePath();
+
+ // // Also render a rectangle around the collision area for debugging
+ // this.debugGraphics.lineStyle(1, 0xff0000);
+ // this.debugGraphics.strokeRect(
+ // Math.min(worldArmStartX, worldArmEndX),
+ // Math.min(worldArmStartY, worldArmEndY),
+ // Math.abs(worldArmEndX - worldArmStartX),
+ // Math.abs(worldArmEndY - worldArmStartY) + hookArmWidth
+ // );
+
+ // Check each pin for collision using Phaser's geometry
+ this.pins.forEach((pin, pinIndex) => {
+ if (pinIndex === targetPinIndex) return; // Skip the target pin
+
+ // Calculate pin position
+ const pinSpacing = 400 / (this.pinCount + 1);
+ const margin = pinSpacing * 0.75;
+ const pinX = 100 + margin + pinIndex * pinSpacing;
+ const pinWorldY = 200;
+
+ // Calculate pin's current position (including any existing movement)
+ const pinCurrentY = pinWorldY - 50 + pin.driverPinLength + pin.keyPinLength - pin.currentHeight;
+ const keyPinTop = pinCurrentY - pin.keyPinLength;
+ const keyPinBottom = pinCurrentY;
+
+ // Create a rectangle for the key pin
+ const keyPinRect = new Phaser.Geom.Rectangle(pinX - 12, keyPinTop, 24, pin.keyPinLength);
+
+ // // Render pin hitbox (blue)
+ // this.debugGraphics.lineStyle(2, 0x0000ff);
+ // this.debugGraphics.strokeRect(pinX - 12, keyPinTop, 24, pin.keyPinLength);
+
+ // Use Phaser's built-in line-to-rectangle intersection
+ if (Phaser.Geom.Intersects.LineToRectangle(hookArmLine, keyPinRect)) {
+ // Collision detected - lift this pin
+ this.liftCollidedPin(pin, pinIndex);
+
+ // // Render collision (green)
+ // this.debugGraphics.lineStyle(3, 0x00ff00);
+ // this.debugGraphics.strokeRect(pinX - 12, keyPinTop, 24, pin.keyPinLength);
+ }
+ });
+ }
+
+
+
+ liftCollidedPin(pin, pinIndex) {
+ // Only lift if the pin isn't already being actively moved
+ if (this.lockState.currentPin && this.lockState.currentPin.index === pinIndex) return;
+
+ // Calculate pin-specific maximum height
+ const baseMaxHeight = 75;
+ const maxHeightReduction = 15;
+ const pinHeightFactor = pinIndex / (this.pinCount - 1);
+ const pinMaxHeight = baseMaxHeight - (maxHeightReduction * pinHeightFactor);
+
+ // Lift the pin faster for collision (more responsive)
+ const collisionLiftSpeed = this.liftSpeed * 0.8; // 80% of normal lift speed (increased from 30%)
+ pin.currentHeight = Math.min(pin.currentHeight + collisionLiftSpeed, pinMaxHeight * 0.5); // Max 50% of pin's max height
+
+ // Update pin visuals
+ this.updatePinVisuals(pin);
+
+ console.log(`Hook collision: Lifting pin ${pinIndex} to height ${pin.currentHeight}`);
+ }
+
+ updatePinVisuals(pin) {
+ // Update key pin visual
+ pin.keyPin.clear();
+ pin.keyPin.fillStyle(0xdd3333);
+
+ // Draw rectangular part of key pin
+ pin.keyPin.fillRect(-12, -50 + pin.driverPinLength - pin.currentHeight, 24, pin.keyPinLength - 8);
+
+ // Draw triangular bottom in pixel art style
+ pin.keyPin.fillRect(-12, -50 + pin.driverPinLength - pin.currentHeight + pin.keyPinLength - 8, 24, 2);
+ pin.keyPin.fillRect(-10, -50 + pin.driverPinLength - pin.currentHeight + pin.keyPinLength - 6, 20, 2);
+ pin.keyPin.fillRect(-8, -50 + pin.driverPinLength - pin.currentHeight + pin.keyPinLength - 4, 16, 2);
+ pin.keyPin.fillRect(-6, -50 + pin.driverPinLength - pin.currentHeight + pin.keyPinLength - 2, 12, 2);
+
+ // Update driver pin visual
+ pin.driverPin.clear();
+ pin.driverPin.fillStyle(0x3388dd);
+ pin.driverPin.fillRect(-12, -50 - pin.currentHeight, 24, pin.driverPinLength);
+
+ // Update spring compression
+ pin.spring.clear();
+ pin.spring.fillStyle(0x666666);
+ const springCompression = pin.currentHeight;
+ const compressionFactor = Math.max(0.3, 1 - (springCompression / 60));
+
+ const springTop = -130;
+ const driverPinTop = -50 - pin.currentHeight;
+ const springBottom = driverPinTop;
+ const springHeight = springBottom - springTop;
+ const totalSpringSpace = springHeight;
+ const segmentSpacing = totalSpringSpace / 11;
+
+ for (let s = 0; s < 12; s++) {
+ const segmentHeight = 4 * compressionFactor;
+ const segmentY = springTop + (s * segmentSpacing);
+
+ if (segmentY + segmentHeight <= springBottom) {
+ pin.spring.fillRect(-12, segmentY, 24, segmentHeight);
+ }
+ }
+ }
+
+ createPins() {
+ // Create random binding order
+ const bindingOrder = [];
+ for (let i = 0; i < this.pinCount; i++) {
+ bindingOrder.push(i);
+ }
+ this.shuffleArray(bindingOrder);
+
+ const pinSpacing = 400 / (this.pinCount + 1);
+ const margin = pinSpacing * 0.75; // 25% smaller margins
+
+ for (let i = 0; i < this.pinCount; i++) {
+ const pinX = 100 + margin + i * pinSpacing;
+ const pinY = 200;
+
+ // Random pin lengths that add up to 75 (total height - 25% increase from 60)
+ const keyPinLength = 25 + Math.random() * 37.5; // 25-62.5 (25% increase)
+ const driverPinLength = 75 - keyPinLength; // Remaining to make 75 total
+
+ const pin = {
+ index: i,
+ binding: bindingOrder[i],
+ isSet: false,
+ currentHeight: 0,
+ keyPinHeight: 0, // Track key pin position separately
+ driverPinHeight: 0, // Track driver pin position separately
+ keyPinLength: keyPinLength,
+ driverPinLength: driverPinLength,
+ x: pinX,
+ y: pinY,
+ container: null,
+ keyPin: null,
+ driverPin: null,
+ spring: null
+ };
+
+ // Create pin container
+ pin.container = this.scene.add.container(pinX, pinY);
+
+ // Add all highlights FIRST (so they appear behind pins)
+ // Add hover effect using a highlight rectangle - 25% less wide, full height from spring top to pin bottom (extended down)
+ pin.highlight = this.scene.add.graphics();
+ pin.highlight.fillStyle(0xffff00, 0.3);
+ pin.highlight.fillRect(-22.5, -110, 45, 140);
+ pin.highlight.setVisible(false);
+ pin.container.add(pin.highlight);
+
+ // Add overpicked highlight
+ pin.overpickedHighlight = this.scene.add.graphics();
+ pin.overpickedHighlight.fillStyle(0xff0000, 0.6);
+ pin.overpickedHighlight.fillRect(-22.5, -110, 45, 140);
+ pin.overpickedHighlight.setVisible(false);
+ pin.container.add(pin.overpickedHighlight);
+
+ // Add failure highlight for overpicked set pins
+ pin.failureHighlight = this.scene.add.graphics();
+ pin.failureHighlight.fillStyle(0xff6600, 0.7);
+ pin.failureHighlight.fillRect(-22.5, -110, 45, 140);
+ pin.failureHighlight.setVisible(false);
+ pin.container.add(pin.failureHighlight);
+
+ // Create spring (top part) - 12 segments with correct initial spacing
+ pin.spring = this.scene.add.graphics();
+ pin.spring.fillStyle(0x666666);
+ const springTop = -130;
+ const springBottom = -50; // Driver pin top when not lifted
+ const springHeight = springBottom - springTop;
+
+ // Calculate total spring space and distribute segments evenly
+ const totalSpringSpace = springHeight;
+ const segmentSpacing = totalSpringSpace / 11; // 11 gaps between 12 segments
+
+ for (let s = 0; s < 12; s++) {
+ const segmentY = springTop + (s * segmentSpacing);
+ pin.spring.fillRect(-12, segmentY, 24, 4);
+ }
+ pin.container.add(pin.spring);
+
+ // Create driver pin (middle part) - starts at y=-50
+ pin.driverPin = this.scene.add.graphics();
+ pin.driverPin.fillStyle(0x3388dd);
+ pin.driverPin.fillRect(-12, -50, 24, driverPinLength);
+ pin.container.add(pin.driverPin);
+
+ // Set container depth to ensure driver pins are above circles
+ pin.container.setDepth(2);
+
+ // Create key pin (bottom part) - starts below driver pin with triangular bottom
+ pin.keyPin = this.scene.add.graphics();
+ pin.keyPin.fillStyle(0xdd3333);
+
+ // Draw rectangular part of key pin
+ pin.keyPin.fillRect(-12, -50 + driverPinLength, 24, keyPinLength - 8);
+
+ // Draw triangular bottom in pixel art style
+ pin.keyPin.fillRect(-12, -50 + driverPinLength + keyPinLength - 8, 24, 2);
+ pin.keyPin.fillRect(-10, -50 + driverPinLength + keyPinLength - 6, 20, 2);
+ pin.keyPin.fillRect(-8, -50 + driverPinLength + keyPinLength - 4, 16, 2);
+ pin.keyPin.fillRect(-6, -50 + driverPinLength + keyPinLength - 2, 12, 2);
+
+ pin.container.add(pin.keyPin);
+
+ // Add labels for pin components (only for the first pin to avoid clutter)
+ if (i === 0) {
+ // Spring label
+ const springLabel = this.scene.add.text(pinX, pinY - 140, 'Spring', {
+ fontSize: '14px',
+ fill: '#00ff00',
+ fontWeight: 'bold'
+ });
+ springLabel.setOrigin(0.5);
+ springLabel.setDepth(100); // Bring to front
+
+ // Driver pin label - positioned below the shear line
+ const driverPinLabel = this.scene.add.text(pinX, pinY - 30, 'Driver Pin', {
+ fontSize: '14px',
+ fill: '#00ff00',
+ fontWeight: 'bold'
+ });
+ driverPinLabel.setOrigin(0.5);
+ driverPinLabel.setDepth(100); // Bring to front
+
+ // Key pin label - positioned at the middle of the key pin
+ const keyPinLabel = this.scene.add.text(pinX, pinY - 50 + driverPinLength + (keyPinLength / 2), 'Key Pin', {
+ fontSize: '14px',
+ fill: '#00ff00',
+ fontWeight: 'bold'
+ });
+ keyPinLabel.setOrigin(0.5);
+ keyPinLabel.setDepth(100); // Bring to front
+
+ // Store references to labels for hiding
+ this.springLabel = springLabel;
+ this.driverPinLabel = driverPinLabel;
+ this.keyPinLabel = keyPinLabel;
+ }
+
+ // Create channel rectangle (keyway for this pin) - above cylinder but behind key pins
+ const shearLineY = -45; // Shear line position
+ const keywayTopY = 200; // Top of the main keyway
+ const channelHeight = keywayTopY - (pinY + shearLineY); // From keyway to shear line
+
+ // Create channel rectangle graphics
+ pin.channelRect = this.scene.add.graphics();
+ pin.channelRect.x = pinX;
+ pin.channelRect.y = pinY + shearLineY - 15; // Start at circle start position (20px above shear line)
+ pin.channelRect.fillStyle(0x2a2a2a, 1); // Same color as keyway
+ pin.channelRect.fillRect(-13, 3, 26, channelHeight + 15 - 3); // 3px margin except at shear line
+ pin.channelRect.setDepth(0); // Behind key pins but above cylinder
+
+ // Add border to match keyway style
+ pin.channelRect.lineStyle(1, 0x1a1a1a);
+ pin.channelRect.strokeRect(-13, 3, 26, channelHeight + 20 - 3);
+
+ // Create spring channel rectangle - behind spring, above cylinder
+ const springChannelHeight = springBottom - springTop; // Spring height
+
+ // Create spring channel rectangle graphics
+ pin.springChannelRect = this.scene.add.graphics();
+ pin.springChannelRect.x = pinX;
+ pin.springChannelRect.y = pinY + springTop; // Start at spring top
+ pin.springChannelRect.fillStyle(0x2a2a2a, 1); // Same color as keyway
+ pin.springChannelRect.fillRect(-13, 3, 26, springChannelHeight - 3); // 3px margin except at shear line
+ pin.springChannelRect.setDepth(1); // Behind spring but above cylinder
+
+ // Add border to match keyway style
+ pin.springChannelRect.lineStyle(1, 0x1a1a1a);
+ pin.springChannelRect.strokeRect(-13, 3, 26, springChannelHeight - 3);
+
+ // Make pin interactive - 25% less wide, full height from spring top to pin bottom (extended down)
+ pin.container.setInteractive(new Phaser.Geom.Rectangle(-18.75, -110, 37.5, 140), Phaser.Geom.Rectangle.Contains);
+
+ // Add pin number
+ const pinText = this.scene.add.text(0, 40, (i + 1).toString(), {
+ fontSize: '14px',
+ fill: '#ffffff',
+ fontWeight: 'bold'
+ });
+ pinText.setOrigin(0.5);
+ pin.container.add(pinText);
+
+ // Store reference to pin text for hiding
+ pin.pinText = pinText;
+
+ pin.container.on('pointerover', () => {
+ if (this.lockState.tensionApplied && !pin.isSet) {
+ pin.highlight.setVisible(true);
+ }
+ });
+
+ pin.container.on('pointerout', () => {
+ pin.highlight.setVisible(false);
+ });
+
+ // Add event handlers
+ pin.container.on('pointerdown', () => {
+ console.log('Pin clicked:', pin.index);
+ this.lockState.currentPin = pin;
+ this.gameState.mouseDown = true;
+ console.log('Pin interaction started');
+
+ // Play click sound
+ if (this.sounds.click) {
+ this.sounds.click.play();
+ }
+
+ // Hide labels on first pin click
+ if (!this.pinClicked) {
+ this.pinClicked = true;
+ if (this.wrenchText) {
+ this.wrenchText.setVisible(false);
+ }
+ if (this.shearLineText) {
+ this.shearLineText.setVisible(false);
+ }
+ if (this.hookPickLabel) {
+ this.hookPickLabel.setVisible(false);
+ }
+ if (this.springLabel) {
+ this.springLabel.setVisible(false);
+ }
+ if (this.driverPinLabel) {
+ this.driverPinLabel.setVisible(false);
+ }
+ if (this.keyPinLabel) {
+ this.keyPinLabel.setVisible(false);
+ }
+
+ // Hide all pin numbers
+ this.pins.forEach(pin => {
+ if (pin.pinText) {
+ pin.pinText.setVisible(false);
+ }
+ });
+ }
+
+ if (!this.lockState.tensionApplied) {
+ this.updateFeedback("Apply tension first before picking pins");
+ }
+ });
+
+ this.pins.push(pin);
+ }
+ }
+
+ createShearLine() {
+ // Create a more visible shear line at y=155 (which is -45 in pin coordinates)
+ const graphics = this.scene.add.graphics();
+ graphics.lineStyle(3, 0x00ff00);
+ graphics.beginPath();
+ graphics.moveTo(100, 155);
+ graphics.lineTo(500, 155);
+ graphics.strokePath();
+
+ // Add a dashed line effect
+ graphics.lineStyle(1, 0x00ff00, 0.5);
+ for (let x = 100; x < 500; x += 10) {
+ graphics.beginPath();
+ graphics.moveTo(x, 150);
+ graphics.lineTo(x, 160);
+ graphics.strokePath();
+ }
+
+ // Add shear line label
+ const shearLineText = this.scene.add.text(503, 145, 'SHEAR LINE', {
+ fontSize: '14px',
+ fill: '#00ff00',
+ fontWeight: 'bold'
+ });
+ shearLineText.setDepth(100); // Bring to front
+
+ // Store reference to shear line text for hiding
+ this.shearLineText = shearLineText;
+
+ // // Add instruction text
+ // this.scene.add.text(300, 180, 'Align key/driver pins at the shear line', {
+ // fontSize: '12px',
+ // fill: '#00ff00',
+ // fontStyle: 'italic'
+ // }).setOrigin(0.5);
+ }
+
+ setupInputHandlers() {
+ this.scene.input.on('pointerup', () => {
+ if (this.lockState.currentPin) {
+ this.checkPinSet(this.lockState.currentPin);
+ this.lockState.currentPin = null;
+ }
+ this.gameState.mouseDown = false;
+
+ // Always return hook to resting position when mouse is released
+ if (this.hookPickGraphics && this.hookConfig) {
+ this.returnHookToStart();
+ }
+ });
+ }
+
+ update() {
+ if (this.lockState.currentPin && this.gameState.mouseDown) {
+ this.liftPin();
+ }
+
+ // Apply gravity when tension is not applied (but not when actively lifting)
+ if (!this.lockState.tensionApplied && !this.gameState.mouseDown) {
+ this.applyGravity();
+ }
+
+ // Apply gravity to non-binding pins even with tension
+ if (this.lockState.tensionApplied && !this.gameState.mouseDown) {
+ this.applyGravity();
+ }
+
+ // Check if all pins are correctly positioned when tension is applied
+ if (this.lockState.tensionApplied) {
+ this.checkAllPinsCorrect();
+ }
+
+ // Hook return is now handled directly in pointerup event
+ }
+
+ liftPin() {
+ if (!this.lockState.currentPin || !this.gameState.mouseDown) return;
+
+ const pin = this.lockState.currentPin;
+ const liftSpeed = this.liftSpeed;
+ const shearLineY = -45;
+
+ // If pin is set and not already overpicked, allow key pin to move up, driver pin stays at SL
+ if (pin.isSet && !pin.isOverpicked) {
+ // Move key pin up gradually from its dropped position (slower when not connected to driver pin)
+ const keyPinLiftSpeed = liftSpeed * 0.5; // Half speed for key pin movement
+ // Key pin should stop when its top surface reaches the shear line
+ // The key pin's top is at: -50 + pin.driverPinLength - pin.keyPinHeight
+ // We want this to equal -45 (shear line)
+ // So: -50 + pin.driverPinLength - pin.keyPinHeight = -45
+ // Therefore: pin.keyPinHeight = pin.driverPinLength - 5
+ const maxKeyPinHeight = pin.driverPinLength - 5; // Top of key pin at shear line
+ pin.keyPinHeight = Math.min(pin.keyPinHeight + keyPinLiftSpeed, maxKeyPinHeight);
+
+ // If key pin reaches driver pin, start overpicking timer
+ if (pin.keyPinHeight >= maxKeyPinHeight) { // Key pin top at shear line
+ // Start overpicking timer if not already started
+ if (!pin.overpickingTimer) {
+ pin.overpickingTimer = Date.now();
+ this.updateFeedback("Key pin at shear line. Release now or continue to overpick...");
+ }
+
+ // Check if 500ms have passed since reaching shear line
+ if (Date.now() - pin.overpickingTimer >= 500) {
+ // Both move up together
+ pin.isOverpicked = true;
+ pin.keyPinHeight = 90; // Move both up above SL
+ pin.driverPinHeight = 90; // Driver pin moves up too
+
+ // Play overpicking sound
+ if (this.sounds.overtension) {
+ this.sounds.overtension.play();
+ }
+
+ // Mark as overpicked and stuck
+ this.updateFeedback("Set pin overpicked! Release tension to reset.");
+ if (!pin.failureHighlight) {
+ pin.failureHighlight = this.scene.add.graphics();
+ pin.failureHighlight.fillStyle(0xff6600, 0.7);
+ pin.failureHighlight.fillRect(-22.5, -110, 45, 140);
+ pin.container.add(pin.failureHighlight);
+ }
+ pin.failureHighlight.setVisible(true);
+ if (pin.setHighlight) pin.setHighlight.setVisible(false);
+ }
+ }
+
+ // Draw key pin (rectangular part) - move gradually from dropped position
+ pin.keyPin.clear();
+ pin.keyPin.fillStyle(0xdd3333);
+ // Calculate key pin position based on keyPinHeight (gradual movement from dropped position)
+ const keyPinY = -50 + pin.driverPinLength - pin.keyPinHeight;
+ pin.keyPin.fillRect(-12, keyPinY, 24, pin.keyPinLength - 8);
+ // Draw triangle
+ pin.keyPin.fillRect(-12, keyPinY + pin.keyPinLength - 8, 24, 2);
+ pin.keyPin.fillRect(-10, keyPinY + pin.keyPinLength - 6, 20, 2);
+ pin.keyPin.fillRect(-8, keyPinY + pin.keyPinLength - 4, 16, 2);
+ pin.keyPin.fillRect(-6, keyPinY + pin.keyPinLength - 2, 12, 2);
+ // Draw driver pin at shear line (stays at SL until overpicked)
+ pin.driverPin.clear();
+ pin.driverPin.fillStyle(0x3388dd);
+ const shearLineY = -45;
+ const driverPinY = shearLineY - pin.driverPinLength; // Driver pin bottom at shear line
+ pin.driverPin.fillRect(-12, driverPinY, 24, pin.driverPinLength);
+ // Spring
+ pin.spring.clear();
+ pin.spring.fillStyle(0x666666);
+ const springTop = -130;
+ const springBottom = shearLineY - pin.driverPinLength; // Driver pin top (at shear line)
+ const springHeight = springBottom - springTop;
+ const totalSpringSpace = springHeight;
+ const segmentSpacing = totalSpringSpace / 11;
+ for (let s = 0; s < 12; s++) {
+ const segmentHeight = 4 * 0.3;
+ const segmentY = springTop + (s * segmentSpacing);
+ if (segmentY + segmentHeight <= springBottom) {
+ pin.spring.fillRect(-12, segmentY, 24, segmentHeight);
+ }
+ }
+ // Continue lifting if mouse is still down
+ if (this.gameState.mouseDown && !pin.isOverpicked) {
+ requestAnimationFrame(() => this.liftPin());
+ }
+ return; // Exit early for set pins - don't run normal lifting logic
+ }
+
+ // Existing overpicking and normal lifting logic follows...
+ // Check for overpicking when tension is applied (for binding pins and set pins)
+ if (this.lockState.tensionApplied && (this.shouldPinBind(pin) || pin.isSet)) {
+ // For set pins, use keyPinHeight; for normal pins, use currentHeight
+ const heightToCheck = pin.isSet ? pin.keyPinHeight : pin.currentHeight;
+ const boundaryPosition = -50 + pin.driverPinLength - heightToCheck;
+
+ // If key pin is pushed too far beyond shear line, it gets stuck
+ if (boundaryPosition < shearLineY - 10) {
+ // Check if this pin being overpicked would prevent automatic success
+ // If all other pins are correctly positioned, don't allow overpicking
+ let otherPinsCorrect = true;
+ this.pins.forEach(otherPin => {
+ if (otherPin !== pin && !otherPin.isOverpicked) {
+ const otherBoundaryPosition = -50 + otherPin.driverPinLength - otherPin.currentHeight;
+ const otherDistanceToShearLine = Math.abs(otherBoundaryPosition - shearLineY);
+ if (otherDistanceToShearLine > 8) {
+ otherPinsCorrect = false;
+ }
+ }
+ });
+
+ // If other pins are correct and this pin is being actively moved, prevent overpicking
+ if (otherPinsCorrect && this.gameState.mouseDown) {
+ // Stop the pin from moving further up but don't mark as overpicked
+ if (pin.isSet) {
+ const maxKeyPinHeight = pin.driverPinLength - 5; // Top of key pin at shear line
+ pin.keyPinHeight = Math.min(pin.keyPinHeight, maxKeyPinHeight);
+ } else {
+ // Use pin-specific maximum height for overpicking prevention
+ const baseMaxHeight = 75;
+ const maxHeightReduction = 15;
+ const pinHeightFactor = pin.index / (this.pinCount - 1);
+ const pinMaxHeight = baseMaxHeight - (maxHeightReduction * pinHeightFactor);
+ pin.currentHeight = Math.min(pin.currentHeight, pinMaxHeight);
+ }
+ return;
+ }
+
+ // Otherwise, allow normal overpicking behavior
+ pin.isOverpicked = true;
+
+ // Play overpicking sound
+ if (this.sounds.overtension) {
+ this.sounds.overtension.play();
+ }
+
+ if (pin.isSet) {
+ this.updateFeedback("Set pin overpicked! Release tension to reset.");
+
+ // Show failure highlight for overpicked set pins
+ if (!pin.failureHighlight) {
+ pin.failureHighlight = this.scene.add.graphics();
+ pin.failureHighlight.fillStyle(0xff6600, 0.7);
+ pin.failureHighlight.fillRect(-22.5, -110, 45, 140);
+ pin.container.add(pin.failureHighlight);
+ }
+ pin.failureHighlight.setVisible(true);
+
+ // Hide set highlight
+ if (pin.setHighlight) pin.setHighlight.setVisible(false);
+ } else {
+ this.updateFeedback("Pin overpicked! Release tension to reset.");
+
+ // Show overpicked highlight for regular pins
+ if (!pin.overpickedHighlight) {
+ pin.overpickedHighlight = this.scene.add.graphics();
+ pin.overpickedHighlight.fillStyle(0xff0000, 0.6);
+ pin.overpickedHighlight.fillRect(-22.5, -110, 45, 140);
+ pin.container.add(pin.overpickedHighlight);
+ }
+ pin.overpickedHighlight.setVisible(true);
+ }
+
+ // Don't return - allow further pushing even when overpicked
+ }
+ }
+
+ // Calculate pin-specific maximum height (further pins have less upward movement)
+ const baseMaxHeight = 75; // Base maximum height for closest pin
+ const maxHeightReduction = 15; // Maximum reduction for furthest pin
+ const pinHeightFactor = pin.index / (this.pinCount - 1); // 0 for first pin, 1 for last pin
+ const pinMaxHeight = baseMaxHeight - (maxHeightReduction * pinHeightFactor);
+
+ pin.currentHeight = Math.min(pin.currentHeight + liftSpeed, pinMaxHeight);
+
+ // Update visual - both pins move up together toward the spring
+ pin.keyPin.clear();
+ pin.keyPin.fillStyle(0xdd3333);
+
+ // Draw rectangular part of key pin
+ pin.keyPin.fillRect(-12, -50 + pin.driverPinLength - pin.currentHeight, 24, pin.keyPinLength - 8);
+
+ // Update hook position to follow any moving pin
+ if (pin.currentHeight > 0) {
+ this.updateHookPosition(pin.index);
+ }
+
+ // Draw triangular bottom in pixel art style
+ pin.keyPin.fillRect(-12, -50 + pin.driverPinLength - pin.currentHeight + pin.keyPinLength - 8, 24, 2);
+ pin.keyPin.fillRect(-10, -50 + pin.driverPinLength - pin.currentHeight + pin.keyPinLength - 6, 20, 2);
+ pin.keyPin.fillRect(-8, -50 + pin.driverPinLength - pin.currentHeight + pin.keyPinLength - 4, 16, 2);
+ pin.keyPin.fillRect(-6, -50 + pin.driverPinLength - pin.currentHeight + pin.keyPinLength - 2, 12, 2);
+
+ pin.driverPin.clear();
+ pin.driverPin.fillStyle(0x3388dd);
+ pin.driverPin.fillRect(-12, -50 - pin.currentHeight, 24, pin.driverPinLength);
+
+ // Spring compresses as pins push up (segments get shorter and closer together)
+ pin.spring.clear();
+ pin.spring.fillStyle(0x666666);
+ const springCompression = pin.currentHeight;
+ const compressionFactor = Math.max(0.3, 1 - (springCompression / 60)); // Segments get shorter, minimum 30% size (1.2px)
+
+ // Fixed spring top position
+ const springTop = -130;
+ // Spring bottom follows driver pin top
+ const driverPinTop = -50 - pin.currentHeight;
+ const springBottom = driverPinTop;
+ const springHeight = springBottom - springTop;
+
+ // Calculate total spring space and distribute segments evenly
+ const totalSpringSpace = springHeight;
+ const segmentSpacing = totalSpringSpace / 11; // 11 gaps between 12 segments - keep consistent spacing
+
+ for (let s = 0; s < 12; s++) {
+ const segmentHeight = 4 * compressionFactor;
+ const segmentY = springTop + (s * segmentSpacing);
+
+ if (segmentY + segmentHeight <= springBottom) { // Only show segments within spring bounds
+ pin.spring.fillRect(-12, segmentY, 24, segmentHeight);
+ }
+ }
+
+ // Check if the key/driver boundary is at the shear line (much higher position)
+ const boundaryPosition = -50 + pin.driverPinLength - pin.currentHeight;
+ const distanceToShearLine = Math.abs(boundaryPosition - shearLineY);
+
+ if (distanceToShearLine < 5 && this.highlightPinAlignment) {
+ // Show green highlight when boundary is at shear line (only if alignment highlighting is enabled)
+ if (!pin.shearHighlight) {
+ pin.shearHighlight = this.scene.add.graphics();
+ pin.shearHighlight.fillStyle(0x00ff00, 0.4);
+ pin.shearHighlight.fillRect(-22.5, -110, 45, 140);
+ pin.container.addAt(pin.shearHighlight, 0); // Add at beginning to appear behind pins
+ }
+ pin.shearHighlight.setVisible(true);
+ } else {
+ if (pin.shearHighlight) {
+ pin.shearHighlight.setVisible(false);
+ }
+ }
+ }
+
+ applyGravity() {
+ // When tension is not applied, all pins fall back down (except overpicked ones)
+ // Also, pins that are not binding fall back down even with tension
+ this.pins.forEach(pin => {
+ const shouldFall = !this.lockState.tensionApplied || (!this.shouldPinBind(pin) && !pin.isSet);
+ if (pin.currentHeight > 0 && !pin.isOverpicked && shouldFall) {
+ pin.currentHeight = Math.max(0, pin.currentHeight - 2.25); // Fall faster than lift (25% slower: 2.25 instead of 3)
+
+ // Update visual
+ pin.keyPin.clear();
+ pin.keyPin.fillStyle(0xdd3333);
+
+ // Draw rectangular part of key pin
+ pin.keyPin.fillRect(-12, -50 + pin.driverPinLength - pin.currentHeight, 24, pin.keyPinLength - 8);
+
+ // Update hook position to follow any moving pin
+ this.updateHookPosition(pin.index);
+
+ // Draw triangular bottom in pixel art style
+ pin.keyPin.fillRect(-12, -50 + pin.driverPinLength - pin.currentHeight + pin.keyPinLength - 8, 24, 2);
+ pin.keyPin.fillRect(-10, -50 + pin.driverPinLength - pin.currentHeight + pin.keyPinLength - 6, 20, 2);
+ pin.keyPin.fillRect(-8, -50 + pin.driverPinLength - pin.currentHeight + pin.keyPinLength - 4, 16, 2);
+ pin.keyPin.fillRect(-6, -50 + pin.driverPinLength - pin.currentHeight + pin.keyPinLength - 2, 12, 2);
+
+ pin.driverPin.clear();
+ pin.driverPin.fillStyle(0x3388dd);
+ pin.driverPin.fillRect(-12, -50 - pin.currentHeight, 24, pin.driverPinLength);
+
+ // Spring decompresses as pins fall
+ pin.spring.clear();
+ pin.spring.fillStyle(0x666666);
+ const springCompression = pin.currentHeight;
+ const compressionFactor = Math.max(0.3, 1 - (springCompression / 60)); // Segments get shorter, minimum 30% size (1.2px)
+
+ // Fixed spring top position
+ const springTop = -130;
+ // Spring bottom follows driver pin top
+ const driverPinTop = -50 - pin.currentHeight;
+ const springBottom = driverPinTop;
+ const springHeight = springBottom - springTop;
+
+ // Calculate total spring space and distribute segments evenly
+ const totalSpringSpace = springHeight;
+ const segmentSpacing = totalSpringSpace / 11; // 11 gaps between 12 segments - keep consistent spacing
+
+ for (let s = 0; s < 12; s++) {
+ const segmentHeight = 4 * compressionFactor;
+ const segmentY = springTop + (s * segmentSpacing);
+
+ if (segmentY + segmentHeight <= springBottom) { // Only show segments within spring bounds
+ pin.spring.fillRect(-12, segmentY, 24, segmentHeight);
+ }
+ }
+
+ // Hide highlights when falling
+ if (pin.shearHighlight) pin.shearHighlight.setVisible(false);
+ if (pin.setHighlight) pin.setHighlight.setVisible(false);
+ if (pin.bindingHighlight) pin.bindingHighlight.setVisible(false);
+ if (pin.overpickedHighlight) pin.overpickedHighlight.setVisible(false);
+ if (pin.failureHighlight) pin.failureHighlight.setVisible(false);
+ } else if (pin.isSet && shouldFall) {
+ // Set pins fall back down when tension is released
+ pin.isSet = false;
+ pin.keyPinHeight = 0;
+ pin.driverPinHeight = 0;
+ pin.currentHeight = 0;
+
+ // Reset visual to original position
+ pin.keyPin.clear();
+ pin.keyPin.fillStyle(0xdd3333);
+ pin.keyPin.fillRect(-12, -50 + pin.driverPinLength, 24, pin.keyPinLength - 8);
+ pin.keyPin.fillRect(-12, -50 + pin.driverPinLength + pin.keyPinLength - 8, 24, 2);
+ pin.keyPin.fillRect(-10, -50 + pin.driverPinLength + pin.keyPinLength - 6, 20, 2);
+ pin.keyPin.fillRect(-8, -50 + pin.driverPinLength + pin.keyPinLength - 4, 16, 2);
+ pin.keyPin.fillRect(-6, -50 + pin.driverPinLength + pin.keyPinLength - 2, 12, 2);
+
+ pin.driverPin.clear();
+ pin.driverPin.fillStyle(0x3388dd);
+ pin.driverPin.fillRect(-12, -50, 24, pin.driverPinLength);
+
+ // Reset spring
+ pin.spring.clear();
+ pin.spring.fillStyle(0x666666);
+ const springTop = -130;
+ const springBottom = -50;
+ const springHeight = springBottom - springTop;
+ const segmentSpacing = springHeight / 11;
+ for (let s = 0; s < 12; s++) {
+ const segmentHeight = 4;
+ const segmentY = springTop + (s * segmentSpacing);
+ pin.spring.fillRect(-12, segmentY, 24, segmentHeight);
+ }
+
+ // Hide set highlight
+ if (pin.setHighlight) pin.setHighlight.setVisible(false);
+ }
+ });
+ }
+
+ checkAllPinsCorrect() {
+ const shearLineY = -45;
+ const threshold = 8; // Same threshold as individual pin checking
+
+ let allCorrect = true;
+
+ this.pins.forEach(pin => {
+ if (pin.isOverpicked) {
+ allCorrect = false;
+ return;
+ }
+
+ // Calculate current boundary position between key and driver pins
+ const boundaryPosition = -50 + pin.driverPinLength - pin.currentHeight;
+ const distanceToShearLine = Math.abs(boundaryPosition - shearLineY);
+
+ // Check if driver pin is above shear line and key pin is below
+ const driverPinBottom = boundaryPosition;
+ const keyPinTop = boundaryPosition;
+
+ // Driver pin should be above shear line, key pin should be below
+ if (driverPinBottom > shearLineY + threshold || keyPinTop < shearLineY - threshold) {
+ allCorrect = false;
+ }
+ });
+
+ // If all pins are correctly positioned, set them all and complete the lock
+ if (allCorrect && this.lockState.pinsSet < this.pinCount) {
+ this.pins.forEach(pin => {
+ if (!pin.isSet) {
+ pin.isSet = true;
+
+ // Show set pin highlight
+ if (!pin.setHighlight) {
+ pin.setHighlight = this.scene.add.graphics();
+ pin.setHighlight.fillStyle(0x00ff00, 0.5);
+ pin.setHighlight.fillRect(-22.5, -110, 45, 140);
+ pin.container.addAt(pin.setHighlight, 0); // Add at beginning to appear behind pins
+ }
+ pin.setHighlight.setVisible(true);
+
+ // Hide other highlights
+ if (pin.shearHighlight) pin.shearHighlight.setVisible(false);
+ if (pin.highlight) pin.highlight.setVisible(false);
+ if (pin.overpickedHighlight) pin.overpickedHighlight.setVisible(false);
+ if (pin.failureHighlight) pin.failureHighlight.setVisible(false);
+ }
+ });
+
+ this.lockState.pinsSet = this.pinCount;
+ this.updateFeedback("All pins correctly positioned! Lock picked successfully!");
+ this.lockPickingSuccess();
+ }
+ }
+
+ checkPinSet(pin) {
+ // Check if the key/driver boundary is at the shear line
+ const boundaryPosition = -50 + pin.driverPinLength - pin.currentHeight;
+ const shearLineY = -45; // Shear line is at y=-45 (much higher position)
+ const distanceToShearLine = Math.abs(boundaryPosition - shearLineY);
+ const shouldBind = this.shouldPinBind(pin);
+
+ // Calculate threshold based on sensitivity (1-10)
+ // Higher sensitivity = smaller threshold (easier to set pins)
+ const baseThreshold = 8;
+ const sensitivityFactor = (11 - this.thresholdSensitivity) / 10; // Invert so higher sensitivity = smaller threshold
+ const threshold = baseThreshold * sensitivityFactor;
+
+ // Debug logging for threshold calculation
+ if (distanceToShearLine < threshold + 2) { // Log when close to threshold
+ console.log(`Pin ${pin.index + 1}: distance=${distanceToShearLine.toFixed(2)}, threshold=${threshold.toFixed(2)}, sensitivity=${this.thresholdSensitivity}`);
+ }
+
+ if (distanceToShearLine < threshold && shouldBind) {
+ // Pin set successfully
+ pin.isSet = true;
+
+ // Set separate heights for key pin and driver pin
+ pin.keyPinHeight = 0; // Key pin drops back to original position
+ pin.driverPinHeight = 60; // Driver pin stays at shear line (60 units from base position)
+
+ // Snap driver pin to shear line - calculate exact position
+ const shearLineY = -45;
+ const targetDriverBottom = shearLineY;
+ const driverPinTop = targetDriverBottom - pin.driverPinLength;
+
+ // Update driver pin to snap to shear line
+ pin.driverPin.clear();
+ pin.driverPin.fillStyle(0x3388dd);
+ pin.driverPin.fillRect(-12, driverPinTop, 24, pin.driverPinLength);
+
+ // Reset key pin to original position (falls back down)
+ pin.keyPin.clear();
+ pin.keyPin.fillStyle(0xdd3333);
+
+ // Draw rectangular part of key pin
+ pin.keyPin.fillRect(-12, -50 + pin.driverPinLength, 24, pin.keyPinLength - 8);
+
+ // Draw triangular bottom in pixel art style
+ pin.keyPin.fillRect(-12, -50 + pin.driverPinLength + pin.keyPinLength - 8, 24, 2);
+ pin.keyPin.fillRect(-10, -50 + pin.driverPinLength + pin.keyPinLength - 6, 20, 2);
+ pin.keyPin.fillRect(-8, -50 + pin.driverPinLength + pin.keyPinLength - 4, 16, 2);
+ pin.keyPin.fillRect(-6, -50 + pin.driverPinLength + pin.keyPinLength - 2, 12, 2);
+
+ // Reset spring to original position
+ pin.spring.clear();
+ pin.spring.fillStyle(0x666666);
+ const springTop = -130; // Fixed spring top
+ const springBottom = -50; // Driver pin top when not lifted
+ const springHeight = springBottom - springTop;
+
+ for (let s = 0; s < 12; s++) {
+ const segmentHeight = 4;
+ const segmentSpacing = springHeight / 12;
+
+ // Calculate segment position from bottom up to ensure bottom segment touches driver pin
+ const segmentY = springBottom - (segmentHeight + (11 - s) * segmentSpacing);
+ pin.spring.fillRect(-12, segmentY, 24, segmentHeight);
+ }
+
+ // Show set pin highlight
+ if (!pin.setHighlight) {
+ pin.setHighlight = this.scene.add.graphics();
+ pin.setHighlight.fillStyle(0x00ff00, 0.5);
+ pin.setHighlight.fillRect(-22.5, -110, 45, 140);
+ pin.container.addAt(pin.setHighlight, 0); // Add at beginning to appear behind pins
+ }
+ pin.setHighlight.setVisible(true);
+
+ // Hide other highlights
+ if (pin.shearHighlight) pin.shearHighlight.setVisible(false);
+ if (pin.highlight) pin.highlight.setVisible(false);
+ if (pin.overpickedHighlight) pin.overpickedHighlight.setVisible(false);
+ if (pin.failureHighlight) pin.failureHighlight.setVisible(false);
+
+ this.lockState.pinsSet++;
+
+ // Play set sound
+ if (this.sounds.set) {
+ this.sounds.set.play();
+ }
+
+ this.updateFeedback(`Pin ${pin.index + 1} set! (${this.lockState.pinsSet}/${this.pinCount})`);
+ this.updateBindingPins();
+
+ if (this.lockState.pinsSet === this.pinCount) {
+ this.lockPickingSuccess();
+ }
+ } else if (pin.isOverpicked) {
+ // Pin is overpicked - stays stuck until tension is released
+ if (pin.isSet) {
+ this.updateFeedback("Set pin overpicked! Release tension to reset.");
+ } else {
+ this.updateFeedback("Pin overpicked! Release tension to reset.");
+ }
+ } else if (pin.isSet) {
+ // Set pin: key pin falls back down, driver pin stays at shear line
+ pin.keyPinHeight = 0; // Key pin falls back to original position
+ pin.overpickingTimer = null; // Reset overpicking timer
+
+ // Redraw key pin at original position
+ pin.keyPin.clear();
+ pin.keyPin.fillStyle(0xdd3333);
+ pin.keyPin.fillRect(-12, -50 + pin.driverPinLength, 24, pin.keyPinLength - 8);
+ pin.keyPin.fillRect(-12, -50 + pin.driverPinLength + pin.keyPinLength - 8, 24, 2);
+ pin.keyPin.fillRect(-10, -50 + pin.driverPinLength + pin.keyPinLength - 6, 20, 2);
+ pin.keyPin.fillRect(-8, -50 + pin.driverPinLength + pin.keyPinLength - 4, 16, 2);
+ pin.keyPin.fillRect(-6, -50 + pin.driverPinLength + pin.keyPinLength - 2, 12, 2);
+
+ // Driver pin stays at shear line
+ pin.driverPin.clear();
+ pin.driverPin.fillStyle(0x3388dd);
+ const shearLineY = -45;
+ const driverPinY = shearLineY - pin.driverPinLength;
+ pin.driverPin.fillRect(-12, driverPinY, 24, pin.driverPinLength);
+
+ // Spring stays connected to driver pin at shear line
+ pin.spring.clear();
+ pin.spring.fillStyle(0x666666);
+ const springTop = -130;
+ const springBottom = shearLineY - pin.driverPinLength;
+ const springHeight = springBottom - springTop;
+ const segmentSpacing = springHeight / 11;
+ for (let s = 0; s < 12; s++) {
+ const segmentHeight = 4 * 0.3;
+ const segmentY = springTop + (s * segmentSpacing);
+ if (segmentY + segmentHeight <= springBottom) {
+ pin.spring.fillRect(-12, segmentY, 24, segmentHeight);
+ }
+ }
+ } else {
+ // Normal pin falls back down due to gravity
+ pin.currentHeight = 0;
+
+ // Reset key pin to original position
+ pin.keyPin.clear();
+ pin.keyPin.fillStyle(0xdd3333);
+
+ // Draw rectangular part of key pin
+ pin.keyPin.fillRect(-12, -50 + pin.driverPinLength, 24, pin.keyPinLength - 8);
+
+ // Draw triangular bottom in pixel art style
+ pin.keyPin.fillRect(-12, -50 + pin.driverPinLength + pin.keyPinLength - 8, 24, 2);
+ pin.keyPin.fillRect(-10, -50 + pin.driverPinLength + pin.keyPinLength - 6, 20, 2);
+ pin.keyPin.fillRect(-8, -50 + pin.driverPinLength + pin.keyPinLength - 4, 16, 2);
+ pin.keyPin.fillRect(-6, -50 + pin.driverPinLength + pin.keyPinLength - 2, 12, 2);
+
+ // Reset driver pin to original position
+ pin.driverPin.clear();
+ pin.driverPin.fillStyle(0x3388dd);
+ pin.driverPin.fillRect(-12, -50, 24, pin.driverPinLength);
+
+ // Reset spring to original position (all 12 segments visible)
+ pin.spring.clear();
+ pin.spring.fillStyle(0x666666);
+ const springTop = -130; // Fixed spring top
+ const springBottom = -50; // Driver pin top when not lifted
+ const springHeight = springBottom - springTop;
+
+ // Calculate total spring space and distribute segments evenly
+ const totalSpringSpace = springHeight;
+ const segmentSpacing = totalSpringSpace / 11; // 11 gaps between 12 segments
+
+ for (let s = 0; s < 12; s++) {
+ const segmentHeight = 4;
+ const segmentY = springTop + (s * segmentSpacing);
+ pin.spring.fillRect(-12, segmentY, 24, segmentHeight);
+ }
+
+ // Hide all highlights
+ if (pin.shearHighlight) pin.shearHighlight.setVisible(false);
+ if (pin.setHighlight) pin.setHighlight.setVisible(false);
+ }
+ }
+
+ shouldPinBind(pin) {
+ if (!this.lockState.tensionApplied) return false;
+
+ // Find the next unset pin in binding order
+ for (let order = 0; order < this.pinCount; order++) {
+ const nextPin = this.pins.find(p => p.binding === order && !p.isSet);
+ if (nextPin) {
+ return pin.index === nextPin.index;
+ }
+ }
+ return false;
+ }
+
+ updateBindingPins() {
+ if (!this.lockState.tensionApplied || !this.highlightBindingOrder) {
+ this.pins.forEach(pin => {
+ // Hide binding highlight
+ if (pin.bindingHighlight) {
+ pin.bindingHighlight.setVisible(false);
+ }
+ });
+ return;
+ }
+
+ // Find the next unset pin in binding order
+ for (let order = 0; order < this.pinCount; order++) {
+ const nextPin = this.pins.find(p => p.binding === order && !p.isSet);
+ if (nextPin) {
+ this.pins.forEach(pin => {
+ if (pin.index === nextPin.index && !pin.isSet) {
+ // Show binding highlight for next pin
+ if (!pin.bindingHighlight) {
+ pin.bindingHighlight = this.scene.add.graphics();
+ pin.bindingHighlight.fillStyle(0xffff00, 0.6);
+ pin.bindingHighlight.fillRect(-22.5, -110, 45, 140);
+ pin.container.addAt(pin.bindingHighlight, 0); // Add at beginning to appear behind pins
+ }
+ pin.bindingHighlight.setVisible(true);
+
+ // Play binding sound when highlighting next binding pin
+ if (this.sounds.binding) {
+ this.sounds.binding.play();
+ }
+ } else if (!pin.isSet) {
+ // Hide binding highlight for other pins
+ if (pin.bindingHighlight) {
+ pin.bindingHighlight.setVisible(false);
+ }
+ }
+ });
+ return;
+ }
+ }
+
+ // All pins set
+ this.pins.forEach(pin => {
+ if (!pin.isSet && pin.bindingHighlight) {
+ pin.bindingHighlight.setVisible(false);
+ }
+ });
+ }
+
+ resetAllPins() {
+ this.pins.forEach(pin => {
+ if (!pin.isSet) {
+ pin.currentHeight = 0;
+ pin.isOverpicked = false; // Reset overpicked state
+ pin.keyPinHeight = 0; // Reset key pin height
+ pin.driverPinHeight = 0; // Reset driver pin height
+
+ // Reset key pin to original position
+ pin.keyPin.clear();
+ pin.keyPin.fillStyle(0xdd3333);
+
+ // Draw rectangular part of key pin
+ pin.keyPin.fillRect(-12, -50 + pin.driverPinLength, 24, pin.keyPinLength - 8);
+
+ // Draw triangular bottom in pixel art style
+ pin.keyPin.fillRect(-12, -50 + pin.driverPinLength + pin.keyPinLength - 8, 24, 2);
+ pin.keyPin.fillRect(-10, -50 + pin.driverPinLength + pin.keyPinLength - 6, 20, 2);
+ pin.keyPin.fillRect(-8, -50 + pin.driverPinLength + pin.keyPinLength - 4, 16, 2);
+ pin.keyPin.fillRect(-6, -50 + pin.driverPinLength + pin.keyPinLength - 2, 12, 2);
+
+ // Reset driver pin to original position
+ pin.driverPin.clear();
+ pin.driverPin.fillStyle(0x3388dd);
+ pin.driverPin.fillRect(-12, -50, 24, pin.driverPinLength);
+
+ // Reset spring to original position (all 12 segments visible)
+ pin.spring.clear();
+ pin.spring.fillStyle(0x666666);
+ const springTop = -130; // Fixed spring top
+ const springBottom = -50; // Driver pin top when not lifted
+ const springHeight = springBottom - springTop;
+
+ // Calculate total spring space and distribute segments evenly
+ const totalSpringSpace = springHeight;
+ const segmentSpacing = totalSpringSpace / 11; // 11 gaps between 12 segments
+
+ for (let s = 0; s < 12; s++) {
+ const segmentHeight = 4;
+ const segmentY = springTop + (s * segmentSpacing);
+ pin.spring.fillRect(-12, segmentY, 24, segmentHeight);
+ }
+
+ // Hide all highlights
+ if (pin.shearHighlight) pin.shearHighlight.setVisible(false);
+ if (pin.setHighlight) pin.setHighlight.setVisible(false);
+ if (pin.bindingHighlight) pin.bindingHighlight.setVisible(false);
+ }
+ });
+ }
+
+ updateFeedback(message) {
+ this.feedback.textContent = message;
+ }
+
+ lockPickingSuccess() {
+ // Animation configuration variables - easy to tweak
+ const KEY_PIN_TOP_SHRINK = 10; // How much the key pin top moves down
+ const KEY_PIN_BOTTOM_SHRINK = 5; // How much the key pin bottom moves up
+ const KEY_PIN_TOTAL_SHRINK = KEY_PIN_TOP_SHRINK + KEY_PIN_BOTTOM_SHRINK; // Total key pin shrink
+ const CHANNEL_MOVEMENT = 25; // How much channels move down
+ const KEYWAY_SHRINK = 20; // How much keyway shrinks
+ const WRENCH_VERTICAL_SHRINK = 60; // How much wrench vertical arm shrinks
+ const WRENCH_HORIZONTAL_SHRINK = 5; // How much wrench horizontal arm gets thinner
+ const WRENCH_MOVEMENT = 10; // How much wrench moves down
+
+ this.gameState.isActive = false;
+
+ // Play success sound
+ if (this.sounds.success) {
+ this.sounds.success.play();
+ }
+
+ this.updateFeedback("Lock picked successfully!");
+
+ // Shrink key pins downward and add half circles to simulate cylinder rotation
+ this.pins.forEach(pin => {
+ // Hide all highlights
+ if (pin.shearHighlight) pin.shearHighlight.setVisible(false);
+ if (pin.setHighlight) pin.setHighlight.setVisible(false);
+ if (pin.bindingHighlight) pin.bindingHighlight.setVisible(false);
+ if (pin.overpickedHighlight) pin.overpickedHighlight.setVisible(false);
+ if (pin.failureHighlight) pin.failureHighlight.setVisible(false);
+
+ // Create squashed circle that expands and moves to stay aligned with key pin top
+ const squashedCircle = this.scene.add.graphics();
+ //was 0xdd3333 Red color (key pin color)
+ squashedCircle.fillStyle(0xffffff); // white color for testing purposes
+ squashedCircle.x = pin.x; // Center horizontally on the pin
+
+ // Start position: aligned with the top of the key pin
+ const startTopY = pin.y + (-50 + pin.driverPinLength); // Top of key pin position
+ squashedCircle.y = startTopY;
+ squashedCircle.setDepth(3); // Above driver pins so they're visible
+
+ // Create a temporary object to hold the circle expansion data
+ const circleData = {
+ width: 24, // Start full width (same as key pin)
+ height: 2, // Start very thin (flat top)
+ y: startTopY
+ };
+
+ // Animate the squashed circle expanding to full circle (stays at top of key pin)
+ this.scene.tweens.add({
+ targets: circleData,
+ width: 24, // Full circle width (stays same)
+ height: 16, // Full circle height (expands from 2 to 16)
+ y: startTopY, // Stay at the top of the key pin (no movement)
+ duration: 1400,
+ ease: 'Cubic.easeInOut',
+ onUpdate: function() {
+ squashedCircle.clear();
+ squashedCircle.fillStyle(0xff3333); // Red color (key pin color)
+
+ // Calculate animation progress (0 to 1)
+ const progress = (circleData.height - 2) / (16 - 2); // From 2 to 16 height
+
+ // Draw different circle shapes based on progress (widest in middle)
+ if (progress < 0.1) {
+ // Start: just a thin line (flat top)
+ squashedCircle.fillRect(-12, 0, 24, 2);
+ } else if (progress < 0.3) {
+ // Early: thin oval with middle bulge
+ squashedCircle.fillRect(-8, 0, 16, 2); // narrow top
+ squashedCircle.fillRect(-12, 2, 24, 2); // wide middle
+ squashedCircle.fillRect(-8, 4, 16, 2); // narrow bottom
+ } else if (progress < 0.5) {
+ // Middle: growing circle with middle bulge
+ squashedCircle.fillRect(-6, 0, 12, 2); // narrow top
+ squashedCircle.fillRect(-10, 2, 20, 2); // wider
+ squashedCircle.fillRect(-12, 4, 24, 2); // widest middle
+ squashedCircle.fillRect(-10, 6, 20, 2); // wider
+ squashedCircle.fillRect(-6, 8, 12, 2); // narrow bottom
+ } else if (progress < 0.7) {
+ // Later: more circle-like with middle bulge
+ squashedCircle.fillRect(-4, 0, 8, 2); // narrow top
+ squashedCircle.fillRect(-8, 2, 16, 2); // wider
+ squashedCircle.fillRect(-12, 4, 24, 2); // widest middle
+ squashedCircle.fillRect(-12, 6, 24, 2); // widest middle
+ squashedCircle.fillRect(-8, 8, 16, 2); // wider
+ squashedCircle.fillRect(-4, 10, 8, 2); // narrow bottom
+ } else if (progress < 0.9) {
+ // Almost full: near complete circle
+ squashedCircle.fillRect(-2, 0, 4, 2); // narrow top
+ squashedCircle.fillRect(-6, 2, 12, 2); // wider
+ squashedCircle.fillRect(-10, 4, 20, 2); // wider
+ squashedCircle.fillRect(-12, 6, 24, 2); // widest middle
+ squashedCircle.fillRect(-12, 8, 24, 2); // widest middle
+ squashedCircle.fillRect(-10, 10, 20, 2); // wider
+ squashedCircle.fillRect(-6, 12, 12, 2); // wider
+ squashedCircle.fillRect(-2, 14, 4, 2); // narrow bottom
+ } else {
+ // Full: complete pixel art circle
+ squashedCircle.fillRect(-2, 0, 4, 2); // narrow top
+ squashedCircle.fillRect(-6, 2, 12, 2); // wider
+ squashedCircle.fillRect(-10, 4, 20, 2); // wider
+ squashedCircle.fillRect(-12, 6, 24, 2); // widest middle
+ squashedCircle.fillRect(-12, 8, 24, 2); // widest middle
+ squashedCircle.fillRect(-10, 10, 20, 2); // wider
+ squashedCircle.fillRect(-6, 12, 12, 2); // wider
+ squashedCircle.fillRect(-2, 14, 4, 2); // narrow bottom
+ }
+
+ // Update position
+ squashedCircle.y = circleData.y;
+ }
+ });
+
+ // Animate key pin shrinking from both top and bottom
+ const keyPinData = { height: pin.keyPinLength, topOffset: 0 };
+ this.scene.tweens.add({
+ targets: keyPinData,
+ height: pin.keyPinLength - KEY_PIN_TOTAL_SHRINK, // Shrink by total amount
+ topOffset: KEY_PIN_TOP_SHRINK, // Move top down
+ duration: 1400,
+ ease: 'Cubic.easeInOut',
+ onUpdate: function() {
+ pin.keyPin.clear();
+ pin.keyPin.fillStyle(0xdd3333);
+
+ // Calculate new position: top moves down, bottom moves up
+ const originalTopY = -50 + pin.driverPinLength; // Original top of key pin
+ const newTopY = originalTopY + keyPinData.topOffset; // Top moves down
+ const newBottomY = newTopY + keyPinData.height; // Bottom position
+
+ // Draw rectangular part of key pin (shrunk from both ends)
+ pin.keyPin.fillRect(-12, newTopY, 24, keyPinData.height - 8);
+
+ // Draw triangular bottom in pixel art style (bottom moves up)
+ pin.keyPin.fillRect(-12, newBottomY - 8, 24, 2);
+ pin.keyPin.fillRect(-10, newBottomY - 6, 20, 2);
+ pin.keyPin.fillRect(-8, newBottomY - 4, 16, 2);
+ pin.keyPin.fillRect(-6, newBottomY - 2, 12, 2);
+ }
+ });
+
+ // Animate key pin channel rectangle moving down with the channel circles
+ this.scene.tweens.add({
+ targets: pin.channelRect,
+ y: pin.channelRect.y + CHANNEL_MOVEMENT, // Move down by channel movement amount
+ duration: 1400,
+ ease: 'Cubic.easeInOut'
+ });
+ });
+
+ // Animate the keyway shrinking (keeping bottom in place) to make cylinder appear to grow
+ // Create a temporary object to hold the height value for tweening
+ const keywayData = { height: 90 };
+ this.scene.tweens.add({
+ targets: keywayData,
+ height: 90 - KEYWAY_SHRINK, // Shrink by keyway shrink amount
+ duration: 1400,
+ ease: 'Cubic.easeInOut',
+ onUpdate: function() {
+ this.keywayGraphics.clear();
+ this.keywayGraphics.fillStyle(0x2a2a2a);
+ // Move top down: y increases as height shrinks, keeping bottom at y=290
+ const newY = 200 + (90 - keywayData.height); // Move top down
+ this.keywayGraphics.fillRect(100, newY, 400, keywayData.height);
+ this.keywayGraphics.lineStyle(1, 0x1a1a1a);
+ this.keywayGraphics.strokeRect(100, newY, 400, keywayData.height);
+ }.bind(this)
+ });
+
+ // Animate tension wrench shrinking and moving down
+ if (this.tensionWrench) {
+ // Create a temporary object to hold the height value for tweening
+ const wrenchData = { height: 170, y: 0, horizontalHeight: 10 }; // Original vertical arm height, y offset, and horizontal arm height
+ this.scene.tweens.add({
+ targets: wrenchData,
+ height: 170 - WRENCH_VERTICAL_SHRINK, // Shrink by vertical shrink amount
+ y: WRENCH_MOVEMENT, // Move entire wrench down
+ horizontalHeight: 10 - WRENCH_HORIZONTAL_SHRINK, // Make horizontal arm thinner
+ duration: 1400,
+ ease: 'Cubic.easeInOut',
+ onUpdate: function() {
+ // Update the wrench graphics (both active and inactive states)
+ this.wrenchGraphics.clear();
+ this.wrenchGraphics.fillStyle(this.lockState.tensionApplied ? 0x00ff00 : 0x888888);
+
+ // Calculate new top position (move top down as height shrinks)
+ const originalTop = -120; // Original top position
+ const newTop = originalTop + (170 - wrenchData.height) + wrenchData.y; // Move top down and add y offset
+
+ // Long vertical arm (left side of L) - top moves down and shrinks
+ this.wrenchGraphics.fillRect(0, newTop, 10, wrenchData.height);
+
+ // Short horizontal arm (bottom of L) - also moves down with top and gets thinner
+ this.wrenchGraphics.fillRect(0, newTop + wrenchData.height, 37.5, wrenchData.horizontalHeight);
+ }.bind(this)
+ });
+ }
+
+ // Channel rectangles are already created during initial render
+
+ // Animate pixel-art circles (channels) moving down from above the shear line
+ this.pins.forEach(pin => {
+ // Calculate starting position: above the shear line (behind driver pins)
+ const pinX = pin.x;
+ const pinY = pin.y;
+ const shearLineY = -45; // Shear line position
+ const circleStartY = pinY + shearLineY - 20; // Start above shear line
+ const circleEndY = circleStartY + CHANNEL_MOVEMENT; // Move down same distance as cylinder
+
+ // Create pixel-art circle graphics
+ const channelCircle = this.scene.add.graphics();
+ channelCircle.x = pinX;
+ channelCircle.y = circleStartY;
+ // Pixel-art circle: red color (like key pins)
+ const color = 0x333333; // Red color (key pin color)
+ channelCircle.fillStyle(color, 1);
+ // Create a proper circle shape with pixel-art steps (middle widest)
+ channelCircle.fillRect(-6, 0, 12, 2); // bottom (narrowest)
+ channelCircle.fillRect(-8, 2, 16, 2); // wider
+ channelCircle.fillRect(-10, 4, 20, 2); // wider
+ channelCircle.fillRect(-12, 6, 24, 2); // widest (middle)
+ channelCircle.fillRect(-12, 8, 24, 2); // widest (middle)
+ channelCircle.fillRect(-10, 10, 20, 2); // narrower
+ channelCircle.fillRect(-8, 12, 16, 2); // narrower
+ channelCircle.fillRect(-6, 14, 12, 2); // top (narrowest)
+ channelCircle.setDepth(1); // Normal depth for circles
+
+ // Animate the circle moving down
+ this.scene.tweens.add({
+ targets: channelCircle,
+ y: circleEndY,
+ duration: 1400,
+ ease: 'Cubic.easeInOut',
+ });
+ });
+
+ // Show success message immediately but delay the game completion
+ const successHTML = `
+ Lock picked successfully!
+ All pins set at the shear line
+ `;
+ // this.showSuccess(successHTML, false, 2000);
+
+ // Delay the actual game completion until animation finishes
+ setTimeout(() => {
+ // Now trigger the success callback that unlocks the game
+ this.showSuccess(successHTML, true, 2000);
+ this.gameResult = { lockable: this.lockable };
+ }, 1500); // Wait 1.5 seconds (slightly longer than animation duration)
+ }
+
+ start() {
+ super.start();
+ this.gameState.isActive = true;
+ this.lockState.tensionApplied = false;
+ this.lockState.pinsSet = 0;
+ this.updateProgress(0, this.pinCount);
+ }
+
+ complete(success) {
+ if (this.game) {
+ this.game.destroy(true);
+ this.game = null;
+ }
+ super.complete(success, this.gameResult);
+ }
+
+ cleanup() {
+ if (this.game) {
+ this.game.destroy(true);
+ this.game = null;
+ }
+ super.cleanup();
+ }
+
+ shuffleArray(array) {
+ for (let i = array.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [array[i], array[j]] = [array[j], array[i]];
+ }
+ return array;
+ }
+}
\ No newline at end of file
diff --git a/js/minigames/lockpicking/lockpicking-game.js b/js/minigames/lockpicking/lockpicking-game.js
index dc8fdb4..c863ef2 100644
--- a/js/minigames/lockpicking/lockpicking-game.js
+++ b/js/minigames/lockpicking/lockpicking-game.js
@@ -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) {
diff --git a/js/systems/bluetooth.js b/js/systems/bluetooth.js
index cd27a24..b6a75c4 100644
--- a/js/systems/bluetooth.js
+++ b/js/systems/bluetooth.js
@@ -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 = 'No devices match your search.
';
- } else if (activeCategory === 'nearby') {
- bluetoothContent.innerHTML = 'No nearby devices found.
';
- } else if (activeCategory === 'saved') {
- bluetoothContent.innerHTML = 'No saved devices found.
';
+ bluetoothContent.innerHTML = 'No devices match your search.
';
+ } else if (activeCategory !== 'all') {
+ bluetoothContent.innerHTML = `No ${activeCategory} devices found.
`;
} else {
- bluetoothContent.innerHTML = 'No devices detected yet.
';
+ bluetoothContent.innerHTML = 'No devices detected yet.
';
}
} 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 = `
-
-
${device.name}
-
${device.mac}
-
- ${signalStrength}%
-
- ${device.nearby ? 'Nearby' : 'Not in range'}
-
- `;
+ // 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 = `
+
${device.name}
+
`;
+
+ 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 += `
+
`;
+
+ for (let i = 1; i <= 5; i++) {
+ const isActive = i <= activeBars;
+ deviceContent += `
`;
+ }
+
+ deviceContent += `
`;
+ } else if (device.nearby) {
+ // Fallback if signal strength not available
+ deviceContent += `
📶`;
+ }
+
+ if (device.saved) {
+ deviceContent += `
💾`;
+ }
+
+ if (device.inInventory) {
+ deviceContent += `
🎒`;
+ }
+
+ deviceContent += `
`;
+ deviceContent += `MAC: ${device.mac}\n${device.details}
`;
+ deviceContent += `Last seen: ${formattedDate} ${formattedTime}
`;
+
+ 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;
\ No newline at end of file
+window.updateBluetoothCount = updateBluetoothCount;
+window.unlockInventoryDeviceByMac = unlockInventoryDeviceByMac;
\ No newline at end of file
diff --git a/js/systems/interactions.js b/js/systems/interactions.js
index a676228..7b8f7d2 100644
--- a/js/systems/interactions.js
+++ b/js/systems/interactions.js
@@ -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) => {
diff --git a/js/systems/notes.js b/js/systems/notes.js
index 92c28c0..cf31f63 100644
--- a/js/systems/notes.js
+++ b/js/systems/notes.js
@@ -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';
diff --git a/lockpicking-comparison.html b/lockpicking-comparison.html
new file mode 100644
index 0000000..d16a934
--- /dev/null
+++ b/lockpicking-comparison.html
@@ -0,0 +1,228 @@
+
+
+
+
+
+ Lockpicking Minigame Comparison
+
+
+
+
+
+
+
+
Original HTML/JS Version
+
+
+
+
+
+
+
+
+
Features
+
+ - ✓ Lightweight - no additional dependencies
+ - ✓ Simple DOM manipulation
+ - ✓ Easy to customize with CSS
+ - ✗ Limited animation capabilities
+ - ✗ Basic graphics rendering
+ - ✗ Manual input handling
+
+
+
+
+
+
Phaser.js Version
+
+
+
+
+
+
+
+
+
Features
+
+ - ✓ Rich graphics and animations
+ - ✓ Built-in game engine features
+ - ✓ Professional game development tools
+ - ✗ Larger bundle size
+ - ✗ More complex setup
+ - ✗ Learning curve for Phaser API
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/locksmith-forge.html b/locksmith-forge.html
new file mode 100644
index 0000000..3e9e5bc
--- /dev/null
+++ b/locksmith-forge.html
@@ -0,0 +1,560 @@
+
+
+
+
+
+ Locksmith Forge - Lockpicking Challenges
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 5
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1.0
+
+
+
+
+
+
+
+
+
+ Ready to start Level 1
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/scenarios/ceo_exfil.json b/scenarios/ceo_exfil.json
index 9b28d21..b318be5 100644
--- a/scenarios/ceo_exfil.json
+++ b/scenarios/ceo_exfil.json
@@ -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"
},
diff --git a/test-phaser-lockpicking.html b/test-phaser-lockpicking.html
new file mode 100644
index 0000000..bf4fd1d
--- /dev/null
+++ b/test-phaser-lockpicking.html
@@ -0,0 +1,307 @@
+
+
+
+
+
+ Phaser Lockpicking Test
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Phaser Lockpicking Minigame Test
+
+
+
Game Parameters
+
+
+
+ 5
+
+
+
+
+
+
+
+
+
+
+
+
+ 5
+
+
+
+
+ 1.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Ready to start
+
+
+
+
+
+
+
+
+
\ No newline at end of file