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 + + + +
+

Lockpicking Minigame Comparison

+

Compare the original HTML/JS version with the new Phaser.js implementation

+
+ +
+
+

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 + + + + + + + + + + + + + + +
+
LEVEL 1
+
+
Pins: 3
+
Difficulty: Easy
+
Sensitivity: 5
+
Lift Speed: 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