diff --git a/assets/rooms/room_office.json b/assets/rooms/room_office.json index e092352..43c1a5a 100644 --- a/assets/rooms/room_office.json +++ b/assets/rooms/room_office.json @@ -3,15 +3,15 @@ "infinite":false, "layers":[ { - "data":[11645, 11646, 11647, 11648, 11649, 11650, 11651, 11652, 11653, 11654, - 11655, 11656, 11657, 11658, 11659, 11660, 11661, 11662, 11663, 11664, - 11665, 0, 0, 0, 0, 0, 0, 0, 0, 11674, - 11675, 11676, 11677, 0, 0, 0, 0, 11682, 11683, 11684, - 11685, 11686, 11687, 0, 0, 0, 0, 11692, 11693, 11694, - 11695, 11696, 11697, 0, 0, 0, 0, 11702, 11703, 11704, - 11705, 0, 0, 0, 0, 0, 0, 0, 0, 11714, - 11715, 0, 0, 0, 0, 0, 0, 0, 0, 11724, - 11725, 11726, 11727, 11728, 11729, 11730, 11731, 11732, 11733, 11734], + "data":[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, + 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, + 21, 0, 0, 0, 0, 0, 0, 0, 0, 30, + 31, 32, 33, 0, 0, 0, 0, 38, 39, 40, + 41, 42, 43, 0, 0, 0, 0, 48, 49, 50, + 51, 52, 53, 0, 0, 0, 0, 58, 59, 60, + 61, 0, 0, 0, 0, 0, 0, 0, 0, 70, + 71, 0, 0, 0, 0, 0, 0, 0, 0, 80, + 81, 82, 83, 84, 85, 86, 87, 88, 89, 90], "height":9, "id":1, "name":"walls", @@ -23,15 +23,15 @@ "y":0 }, { - "data":[11645, 11646, 11647, 11648, 11649, 11650, 11651, 11652, 11653, 11654, - 11655, 11656, 11657, 11658, 11659, 11660, 11661, 11662, 11663, 11664, - 11665, 11666, 11667, 11668, 11669, 11670, 11671, 11672, 11673, 11674, - 11675, 11676, 11677, 11678, 11679, 11680, 11681, 11682, 11683, 11684, - 11685, 11686, 11687, 11688, 11689, 11690, 11691, 11692, 11693, 11694, - 11695, 11696, 11697, 11698, 11699, 11700, 11701, 11702, 11703, 11704, - 11705, 11706, 11707, 11708, 11709, 11710, 11711, 11712, 11713, 11714, - 11715, 11716, 11717, 11718, 11719, 11720, 11721, 11722, 11723, 11724, - 11725, 11726, 11727, 11728, 11729, 11730, 11731, 11732, 11733, 11734], + "data":[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, + 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, + 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, + 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, + 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, + 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, + 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, + 81, 82, 83, 84, 85, 86, 87, 88, 89, 90], "height":9, "id":13, "name":"props", @@ -43,8 +43,8 @@ "y":0 }, { - "data":[0, 12993, 0, 0, 0, 0, 0, 0, 12993, 0, - 0, 12994, 0, 0, 0, 0, 0, 0, 12994, 0, + "data":[0, 101, 0, 0, 0, 0, 0, 0, 101, 0, + 0, 102, 0, 0, 0, 0, 0, 0, 102, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, @@ -234,81 +234,16 @@ "x":0, "y":0 }], - "nextlayerid":14, - "nextobjectid":18, + "nextlayerid":12, + "nextobjectid":17, "orientation":"orthogonal", "renderorder":"right-down", "tiledversion":"1.11.0", "tileheight":48, "tilesets":[ - { - "columns":16, - "firstgid":1, - "image":"..\/Modern_Office_48x48.png", - "imageheight":2544, - "imagewidth":768, - "margin":0, - "name":"Modern_Office_48x48", - "spacing":0, - "tilecount":848, - "tileheight":48, - "tilewidth":48 - }, - { - "columns":76, - "firstgid":849, - "image":"..\/Room_Builder_48x48.png", - "imageheight":5232, - "imagewidth":3648, - "margin":0, - "name":"Room_Builder_48x48", - "spacing":0, - "tilecount":8284, - "tileheight":48, - "tilewidth":48 - }, - { - "columns":16, - "firstgid":9133, - "image":"..\/5_Classroom_and_library_Shadowless_48x48.png", - "imageheight":1632, - "imagewidth":768, - "margin":0, - "name":"5_Classroom_and_library_Shadowless_48x48", - "spacing":0, - "tilecount":544, - "tileheight":48, - "tilewidth":48 - }, - { - "columns":16, - "firstgid":9677, - "image":"..\/18_Jail_Shadowless_48x48.png", - "imageheight":2160, - "imagewidth":768, - "margin":0, - "name":"18_Jail_Shadowless_48x48", - "spacing":0, - "tilecount":720, - "tileheight":48, - "tilewidth":48 - }, - { - "columns":16, - "firstgid":10397, - "image":"..\/1_Generic_Shadowless_48x48.png", - "imageheight":3744, - "imagewidth":768, - "margin":0, - "name":"1_Generic_Shadowless_48x48", - "spacing":0, - "tilecount":1248, - "tileheight":48, - "tilewidth":48 - }, { "columns":10, - "firstgid":11645, + "firstgid":1, "image":"room_office_l.png", "imageheight":480, "imagewidth":480, @@ -319,22 +254,9 @@ "tileheight":48, "tilewidth":48 }, - { - "columns":16, - "firstgid":11745, - "image":"..\/1_Generic_Shadowless_48x48.png", - "imageheight":3744, - "imagewidth":768, - "margin":0, - "name":"1_Generic_Shadowless_48x48", - "spacing":0, - "tilecount":1248, - "tileheight":48, - "tilewidth":48 - }, { "columns":1, - "firstgid":12993, + "firstgid":101, "image":"..\/tiles\/door.png", "imageheight":96, "imagewidth":48, diff --git a/assets/rooms/room_reception.json b/assets/rooms/room_reception.json index 3b9381f..17e2b25 100644 --- a/assets/rooms/room_reception.json +++ b/assets/rooms/room_reception.json @@ -156,8 +156,8 @@ "x":0, "y":0 }], - "nextlayerid":13, - "nextobjectid":14, + "nextlayerid":12, + "nextobjectid":13, "orientation":"orthogonal", "renderorder":"right-down", "tiledversion":"1.11.0", @@ -193,4 +193,4 @@ "type":"map", "version":"1.10", "width":10 -} \ No newline at end of file +} diff --git a/assets/scenarios/biometric_breach.json b/assets/scenarios/biometric_breach.json index 306d757..79c93b7 100644 --- a/assets/scenarios/biometric_breach.json +++ b/assets/scenarios/biometric_breach.json @@ -38,8 +38,8 @@ "takeable": false, "hasFingerprint": true, "fingerprintOwner": "receptionist", - "fingerprintQuality": 0.8, - "observations": "The reception computer shows a security alert screen. There might be fingerprints on the keyboard." + "fingerprintDifficulty": "easy", + "observations": "The reception computer shows a security alert screen. There are clear fingerprints on the keyboard." }, { "type": "lockpick", @@ -55,6 +55,14 @@ "inInventory": true, "observations": "A powerful workstation for cryptographic analysis" }, + { + "type": "notes", + "name": "Biometric Security Notice", + "takeable": true, + "readable": true, + "text": "ALERT: SECURITY PROTOCOLS UPDATED\n\nAll internal doors now require biometric authentication due to the security breach.\n\nTo proceed: Use your fingerprint kit to collect prints, then present them at door scanners. The main office door requires the receptionist's credentials.\n\nReport any unauthorized access attempts to security immediately.", + "observations": "An important notice about the facility's security measures" + }, { "type": "notes", "name": "Facility Map", @@ -71,6 +79,10 @@ "north": ["office2", "office3"], "south": "reception" }, + "locked": true, + "lockType": "biometric", + "requires": "receptionist", + "biometricMatchThreshold": 0.5, "objects": [ { "type": "pc", @@ -78,7 +90,7 @@ "takeable": false, "hasFingerprint": true, "fingerprintOwner": "researcher", - "fingerprintQuality": 0.9, + "fingerprintDifficulty": "medium", "observations": "A research computer with data analysis software running. There might be fingerprints on the keyboard." }, { @@ -105,12 +117,12 @@ "observations": "A backup key for the biometrics lab, kept for emergencies" }, { - "type": "photo", - "name": "Team Photo", + "type": "notes", + "name": "Team Information", "takeable": true, "readable": true, "text": "Project Sentinel Team:\nDr. Eleanor Chen (Director)\nDr. Marcus Patel (Lead Researcher)\nDr. Wei Zhang (Biometrics Specialist)\nAlex Morgan (Security Consultant)", - "observations": "A framed photo of the Project Sentinel research team" + "observations": "Information about the Project Sentinel research team" }, { "type": "notes", @@ -128,6 +140,10 @@ "north": "ceo", "south": "office1" }, + "locked": true, + "lockType": "biometric", + "requires": "researcher", + "biometricMatchThreshold": 0.7, "objects": [ { "type": "pc", @@ -135,7 +151,7 @@ "takeable": false, "hasFingerprint": true, "fingerprintOwner": "intruder", - "fingerprintQuality": 0.85, + "fingerprintDifficulty": "medium", "observations": "A specialized workstation for biometric research. The screen shows someone was recently using it." }, { @@ -211,7 +227,7 @@ "takeable": false, "hasFingerprint": true, "fingerprintOwner": "director", - "fingerprintQuality": 0.95, + "fingerprintDifficulty": "hard", "observations": "The director's high-security computer. Multiple fingerprints visible on the keyboard." }, { @@ -277,7 +293,7 @@ "locked": true, "lockType": "biometric", "requires": "intruder", - "difficulty": "hard", + "biometricMatchThreshold": 0.9, "observations": "A well-hidden wall safe behind a painting with a fingerprint scanner", "contents": [ { @@ -305,12 +321,6 @@ "readable": true, "text": "A = Meet at dock, 4AM\nN = Bring everything\nM = Getaway car ready\n\nLH will pay other half when delivered.", "observations": "A hastily scribbled note, partially crumpled" - }, - { - "type": "fingerprint_kit", - "name": "Advanced Fingerprint Kit", - "takeable": true, - "observations": "A more advanced fingerprint collection kit with higher resolution scanning" } ] }, @@ -330,7 +340,7 @@ "takeable": false, "hasFingerprint": true, "fingerprintOwner": "intruder", - "fingerprintQuality": 0.98, + "fingerprintDifficulty": "medium", "observations": "The main server terminal controlling access to research data. There are clear fingerprints on the screen." }, { @@ -340,7 +350,7 @@ "locked": true, "lockType": "biometric", "requires": "intruder", - "difficulty": "medium", + "biometricMatchThreshold": 0.9, "observations": "A secure safe with a fingerprint scanner containing the sensitive research data", "contents": [ { @@ -369,7 +379,7 @@ "locked": true, "lockType": "biometric", "requires": "intruder", - "difficulty": "hard", + "biometricMatchThreshold": 0.9, "observations": "A suspicious case hidden behind server racks with a fingerprint scanner", "contents": [ { diff --git a/assets/scenarios/ceo_exfil.json b/assets/scenarios/ceo_exfil.json index aec2aa3..2e8da9d 100644 --- a/assets/scenarios/ceo_exfil.json +++ b/assets/scenarios/ceo_exfil.json @@ -4,6 +4,10 @@ "rooms": { "reception": { "type": "room_reception", + "locked": true, + "lockType": "key", + "requires": "ceo_office_key", + "difficulty": "easy", "connections": { "north": "office1" }, @@ -53,14 +57,6 @@ "takeable": true, "inInventory": true, "observations": "A powerful workstation for cryptographic analysis" - }, - { - "type": "bluetooth_spoofer", - "name": "Bluetooth Spoofer", - "takeable": true, - "observations": "A specialized device that can mimic Bluetooth signals from other devices", - "canSpoofBluetooth": true, - "mac": "00:11:22:33:44:55" } ] }, @@ -78,7 +74,7 @@ "requires": "password", "hasFingerprint": true, "fingerprintOwner": "ceo", - "fingerprintQuality": 0.9, + "fingerprintDifficulty": "medium", "observations": "A computer with a cybersecurity alert on screen. There might be fingerprints on the keyboard." }, { @@ -94,12 +90,6 @@ "name": "Fingerprint Kit", "takeable": true, "observations": "A kit used for collecting fingerprints from surfaces" - }, - { - "type": "spoofing_kit", - "name": "Fingerprint Spoofing Kit", - "takeable": true, - "observations": "A specialized kit containing silicone, gelatin, and other materials for creating artificial fingerprints" } ] }, @@ -145,7 +135,9 @@ "type": "pc", "name": "IT Staff Computer", "takeable": false, - "requires": "password", + "requires": "bluetooth", + "lockType": "bluetooth", + "mac": "00:11:22:33:44:55", "observations": "An IT staff computer showing network security logs" }, { @@ -160,6 +152,7 @@ "type": "lockpick", "name": "Lock Pick Kit", "takeable": true, + "inInventory": true, "observations": "A professional lock picking kit with various picks and tension wrenches" } ] diff --git a/index.html b/index.html index 63398a8..3a9daa7 100644 --- a/index.html +++ b/index.html @@ -623,32 +623,6 @@ display: none; } - /* Bluetooth Pairing Button */ - .bluetooth-pair-button { - display: inline-block; - margin-top: 8px; - padding: 5px 10px; - background-color: #9b59b6; - color: white; - border: none; - border-radius: 3px; - font-size: 12px; - cursor: pointer; - transition: background-color 0.2s; - } - - .bluetooth-pair-button:hover { - background-color: #8e44ad; - } - - .bluetooth-pair-button.paired { - background-color: #27ae60; - } - - .bluetooth-pair-button.paired:hover { - background-color: #219653; - } - /* Bluetooth Unlock Button */ .bluetooth-unlock-button { display: inline-block; @@ -676,7 +650,7 @@ .bluetooth-device:hover .bluetooth-device-name, .bluetooth-device:hover .bluetooth-device-details, .bluetooth-device:hover .bluetooth-device-timestamp, - .bluetooth-device:hover .bluetooth-pair-button { + .bluetooth-device:hover { pointer-events: auto; } @@ -684,7 +658,7 @@ #biometrics-panel { position: fixed; bottom: 80px; - right: 160px; + right: 20px; width: 350px; max-height: 500px; background-color: rgba(0, 0, 0, 0.9); @@ -710,8 +684,8 @@ #biometrics-title { font-weight: bold; - font-size: 18px; - color: #e74c3c; + font-size: 16px; + color: #2ecc71; } #biometrics-close { @@ -724,13 +698,13 @@ #biometrics-close:hover { color: white; } - + #biometrics-search-container { padding: 10px 15px; background-color: #333; border-bottom: 1px solid #444; } - + #biometrics-search { width: 100%; padding: 8px 10px; @@ -740,10 +714,10 @@ color: white; font-size: 14px; } - + #biometrics-search:focus { outline: none; - box-shadow: 0 0 0 2px rgba(231, 76, 60, 0.5); + box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.5); } #biometrics-categories { @@ -752,7 +726,7 @@ background-color: #2c2c2c; border-bottom: 1px solid #444; } - + .biometrics-category { padding: 5px 10px; margin-right: 5px; @@ -761,12 +735,12 @@ font-size: 12px; transition: all 0.2s; } - + .biometrics-category.active { - background-color: #e74c3c; + background-color: #2ecc71; color: white; } - + .biometrics-category:hover:not(.active) { background-color: #444; } @@ -798,49 +772,22 @@ } .biometric-sample-name { + display: flex; + justify-content: space-between; font-weight: bold; margin-bottom: 5px; font-size: 14px; - color: #e74c3c; - display: flex; - justify-content: space-between; - align-items: center; + color: #2ecc71; } - + .biometric-sample-icons { display: flex; gap: 5px; } - - .biometric-sample-icon { - font-size: 12px; - color: #aaa; - } - - .biometric-sample-details { - font-size: 13px; - line-height: 1.4; - white-space: pre-wrap; - max-height: 80px; - overflow: hidden; - transition: max-height 0.3s; - } - - .biometric-sample.expanded .biometric-sample-details { - max-height: 1000px; - } - - .biometric-sample-timestamp { - font-size: 11px; - color: #888; - margin-top: 5px; - text-align: right; - } .biometric-quality-bar { - width: 100%; height: 5px; - background: #333; + background-color: #333; margin-top: 8px; border-radius: 2px; margin-bottom: 8px; @@ -852,45 +799,27 @@ transition: width 0.3s ease; } - #biometrics-toggle { - position: relative; - width: 60px; - height: 60px; - background-color: #e74c3c; - color: white; - border-radius: 50%; - display: flex; - justify-content: center; - align-items: center; - cursor: pointer; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3); - z-index: 1998; - font-size: 28px; - transition: all 0.3s ease; - margin-left: 10px; + .biometric-sample-details { + font-size: 13px; + line-height: 1.4; + white-space: pre-wrap; + max-height: 0; + overflow: hidden; + transition: max-height 0.3s; + } + + .biometric-sample.expanded .biometric-sample-details { + max-height: 200px; } - #biometrics-toggle:hover { - background-color: #c0392b; - transform: scale(1.1); - } - - #biometrics-count { - position: absolute; - top: 0; - right: 0; - background-color: #c0392b; - color: white; - border-radius: 50%; - width: 22px; - height: 22px; - font-size: 12px; + .biometric-sample-timestamp { + font-size: 11px; + color: #888; + margin-top: 5px; + text-align: right; display: none; - justify-content: center; - align-items: center; - font-weight: bold; } - + /* Rest of existing styles follow */ .biometric-sample-timestamp { font-size: 11px; @@ -913,6 +842,266 @@ #game-container { position: relative; } + + /* Toggle Buttons */ + #biometrics-toggle { + position: relative; + width: 60px; + height: 60px; + background-color: #2ecc71; + color: white; + border-radius: 50%; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3); + z-index: 1998; + font-size: 28px; + transition: all 0.3s ease; + margin-left: 10px; + } + + #biometrics-toggle:hover { + background-color: #27ae60; + transform: scale(1.1); + } + + #biometrics-count { + position: absolute; + top: -5px; + right: -5px; + background-color: #e74c3c; + color: white; + border-radius: 50%; + width: 24px; + height: 24px; + font-size: 14px; + display: flex; + justify-content: center; + align-items: center; + font-weight: bold; + } + + .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-size: 16px; + } @@ -987,7 +1176,6 @@
All
Fingerprints
-
Spoofed
@@ -1475,10 +1663,6 @@ display: 'none' }; - // Add these constants for spoofing - const SPOOFING_TIME = 3000; // 3 seconds to create spoof - const SPOOF_QUALITY_MULTIPLIER = 0.8; // Spoofed prints are slightly lower quality - // Add these constants for the dusting minigame const DUST_COLORS = { NONE: 0x000000, @@ -1545,7 +1729,6 @@ this.load.image('book', 'assets/objects/book.png'); this.load.image('workstation', 'assets/objects/workstation.png'); this.load.image('bluetooth_scanner', 'assets/objects/bluetooth_scanner.png'); - this.load.image('bluetooth_spoofer', 'assets/objects/bluetooth_spoofer.png'); this.load.image('tablet', 'assets/objects/tablet.png'); this.load.image('fingerprint_kit', 'assets/objects/fingerprint_kit.png'); this.load.image('lockpick', 'assets/objects/lockpick.png'); @@ -1756,7 +1939,7 @@ addNote("Mission Brief", gameScenario.scenario_brief, true); // Show notification - gameAlert(gameScenario.scenario_brief, 'info', 'Mission Brief', 8000); + gameAlert(gameScenario.scenario_brief, 'info', 'Mission Brief', 0); } // initializes the rooms @@ -2851,7 +3034,7 @@ } // Show notification instead of alert - gameAlert(message, 'info', data.name, 7000); + gameAlert(message, 'info', data.name, 0); } // adds an item to the inventory @@ -3304,7 +3487,11 @@ // If not found, try object-level difficulty difficulty = difficulty || lockable.scenarioData?.difficulty || lockable.properties?.difficulty; debugLog('STARTING LOCKPICK MINIGAME', { difficulty }, 2); - startLockpickingMinigame(lockable, game.scene.scenes[0], difficulty); + startLockpickingMinigame(lockable, game.scene.scenes[0], difficulty, () => { + // Add callback to handle successful lockpicking + unlockTarget(lockable, type, lockable.layer); + gameAlert(`Successfully picked the lock!`, 'success', 'Lock Picked', 4000); + }); } } else { debugLog('KEY NOT FOUND - FAIL', null, 2); @@ -3340,70 +3527,89 @@ break; case 'biometric': - debugLog('BIOMETRIC AUTHENTICATION REQUESTED', null, 2); - // Check if player has fingerprint kit - const hasFingerPrintKit = inventory.items.some(item => - item && item.scenarioData && - item.scenarioData.type === 'fingerprint_kit' - ); - - if (!hasFingerPrintKit) { - gameAlert("You need a fingerprint kit to use biometric scanners.", 'warning', 'Missing Equipment', 4000); - return; - } - - // Check if player has required fingerprint sample const requiredFingerprint = lockRequirements.requires; - debugLog('FINGERPRINT REQUIRED', requiredFingerprint, 2); + debugLog('BIOMETRIC LOCK REQUIRES', requiredFingerprint, 2); - // Check if player has a valid fingerprint sample - const validSample = gameState.biometricSamples.find(sample => - sample.type === 'fingerprint' && - sample.owner === requiredFingerprint && - sample.quality >= 0.7 // Quality threshold + // Check if we have fingerprints in the biometricSamples collection + const biometricSamples = gameState.biometricSamples || []; + + // Enhanced debugging - Show collected fingerprints + debugLog('BIOMETRIC SAMPLES', JSON.stringify(biometricSamples), 2); + + // Get the required match threshold from the object or use default + const requiredThreshold = typeof lockable.biometricMatchThreshold === 'number' ? + lockable.biometricMatchThreshold : 0.4; + // this needs to be changed to use the scenario, bugged. + debugLog('BIOMETRIC THRESHOLD', requiredThreshold, "this needs to be changed to use the scenario, bugged.",2); + + // Find the fingerprint sample for the required person + const fingerprintSample = biometricSamples.find(sample => + sample.owner === requiredFingerprint ); - if (validSample) { - debugLog('BIOMETRIC UNLOCK SUCCESS', validSample, 1); - unlockTarget(lockable, type, lockable.layer); - gameAlert(`You successfully used ${validSample.owner}'s fingerprint to unlock the ${type}.`, - 'success', 'Biometric Authentication Successful', 5000); + const hasFingerprint = fingerprintSample !== undefined; + debugLog('FINGERPRINT CHECK', `Looking for '${requiredFingerprint}'. Found: ${hasFingerprint}`, 2); + + if (hasFingerprint) { + // Get the quality from the sample + let fingerprintQuality = fingerprintSample.quality; - // Play success sound and visual effect - const successEffect = lockable.scene ? lockable.scene.add.circle( - lockable.x, - lockable.y, - 32, - 0x00ff00, - 0.5 - ) : null; + // Normalize quality to 0-1 range if it's in percentage format + if (fingerprintQuality > 1) { + fingerprintQuality = fingerprintQuality / 100; + } - if (successEffect) { - lockable.scene.tweens.add({ - targets: successEffect, - alpha: 0, - scale: 2, - duration: 1000, - onComplete: () => successEffect.destroy() + debugLog('BIOMETRIC CHECK', + `Required: ${requiredFingerprint}, Quality: ${fingerprintQuality} (${Math.round(fingerprintQuality * 100)}%), Threshold: ${requiredThreshold} (${Math.round(requiredThreshold * 100)}%)`, 2); + + debugLog('QUALITY CHECK', + `Is ${fingerprintQuality} >= ${requiredThreshold}? ${fingerprintQuality >= requiredThreshold}`, 2); + + // Check if the fingerprint quality meets the threshold + if (fingerprintQuality >= requiredThreshold) { + debugLog('BIOMETRIC UNLOCK SUCCESS', null, 1); + unlockTarget(lockable, type, lockable.layer); + + // Play unlock sound if available + if (typeof playSound === 'function') { + playSound("unlock"); + } + + gameAlert(`You successfully unlocked the ${type} with ${requiredFingerprint}'s fingerprint.`, + 'success', 'Biometric Unlock Successful', 5000); + + // Log the successful biometric unlock + if (!gameState.biometricUnlocks) { + gameState.biometricUnlocks = []; + } + gameState.biometricUnlocks.push({ + location: type, + fingerprint: requiredFingerprint, + quality: fingerprintQuality, + threshold: requiredThreshold, + timestamp: new Date().toISOString() }); + } else { + debugLog('BIOMETRIC QUALITY TOO LOW', + `Quality: ${fingerprintQuality} (${Math.round(fingerprintQuality * 100)}%) < Threshold: ${requiredThreshold} (${Math.round(requiredThreshold * 100)}%)`, 2); + gameAlert(`The fingerprint quality (${Math.round(fingerprintQuality * 100)}%) is too low for this lock. + It requires at least ${Math.round(requiredThreshold * 100)}% quality.`, + 'error', 'Biometric Authentication Failed', 5000); } } else { - debugLog('BIOMETRIC UNLOCK FAILED', null, 2); - gameAlert(`You don't have the required fingerprint sample.`, 'error', 'Biometric Authentication Failed', 4000); + debugLog('MISSING REQUIRED FINGERPRINT', + `Required: '${requiredFingerprint}', Available: ${biometricSamples.map(s => s.owner).join(", ") || "none"}`, 2); + gameAlert(`This ${type} requires ${requiredFingerprint}'s fingerprint, which you haven't collected yet.`, + 'error', 'Biometric Authentication Failed', 5000); } break; - case 'bluetooth': + case 'bluetooth': if (lockable.scenarioData?.locked) { - // Try to spoof the Bluetooth device - const spoofResult = spoofBluetoothDevice(lockable); + // Check if we have a matching Bluetooth device in the current room + const matchFound = findMatchingBluetoothDevice(lockable); - if (spoofResult) { - // If spoofing was successful, unlock the target - unlockTarget(lockable, type, lockable.layer); - } - - // Allow the item to be picked up even if locked + // Allow the item to be picked up regardless of match if it's takeable if (type === 'item' && lockable.scenarioData?.takeable) { // Check if the item is already in the inventory before adding it const isAlreadyInInventory = inventory.items.some(item => item.name === lockable.name); @@ -3415,11 +3621,27 @@ delete rooms[currentRoom].objects[lockable.name]; } } + + // Only unlock if there's a matching device + if (matchFound) { + unlockTarget(lockable, type, lockable.layer); + gameAlert(`Bluetooth connection established with ${matchFound.name}`, 'success', 'Bluetooth Connected', 3000); + } else { + gameAlert('Item added to inventory but still locked - no matching Bluetooth device in range', 'warning', 'Partial Success', 3000); + return; + } + } else if (matchFound) { + // For non-takeable items, only proceed if there's a match + unlockTarget(lockable, type, lockable.layer); + gameAlert(`Bluetooth connection established with ${matchFound.name}`, 'success', 'Bluetooth Connected', 3000); + } else { + gameAlert('No matching Bluetooth device in range', 'error', 'Connection Failed', 3000); + return; } - return; } + console.log('Bluetooth processing complete'); break; - + default: gameAlert(`Requires: ${lockRequirements.requires}`, 'warning', 'Locked', 4000); } @@ -3740,107 +3962,15 @@ } } - function spoofBluetoothDevice(target) { - // Find spoofer in inventory - const spoofer = inventory.items.find(item => - item.scenarioData?.type === "bluetooth_spoofer" - ); - - if (!spoofer) { - gameAlert("You need a Bluetooth spoofer to unlock this device.", 'warning', 'Bluetooth Spoofer Required', 4000); - return false; - } - - // Check if target is in inventory or in the environment - const isInventoryItem = inventory.items.includes(target); - - // If it's an environment object, check distance - if (!isInventoryItem) { - // Calculate distance between player and target device - const distance = Phaser.Math.Distance.Between( - player.x, player.y, - target.x, target.y - ); - - debugLog('BLUETOOTH SPOOF ATTEMPT', { - deviceName: target.scenarioData?.name, - deviceMac: target.scenarioData?.mac, - spooferMac: spoofer.scenarioData?.macPaired, - distance: Math.round(distance), - isInventoryItem: false - }, 2); - - // Check if player is within range - if (distance > BLUETOOTH_SCAN_RANGE) { - gameAlert("Too far from device to establish Bluetooth connection.", 'error', 'Connection Failed', 3000); - return false; - } - } else { - // Log attempt for inventory item - debugLog('BLUETOOTH SPOOF ATTEMPT', { - deviceName: target.scenarioData?.name, - deviceMac: target.scenarioData?.mac, - spooferMac: spoofer.scenarioData?.macPaired, - isInventoryItem: true, - distance: 0 // Inventory items are always at distance 0 - }, 2); - } - - // Normalize MAC addresses for comparison - const targetMac = target.scenarioData?.mac?.toLowerCase() || ""; - const spooferMac = spoofer.scenarioData?.macPaired?.toLowerCase() || ""; - - // Check if the spoofer has the correct MAC address - if (spooferMac && targetMac && spooferMac === targetMac) { - debugLog('BLUETOOTH SPOOF SUCCESS', { - deviceName: target.scenarioData?.name, - mac: target.scenarioData?.mac, - isInventoryItem: isInventoryItem - }, 1); - - // Unlock the device - target.scenarioData.locked = false; - - // If it's an inventory item, update its appearance if needed - if (isInventoryItem) { - // Update the item's texture if it has an unlocked version - if (target.scenarioData?.unlockedTexture) { - target.setTexture(target.scenarioData.unlockedTexture); - } - - // If the item has contents, allow them to be collected - if (target.scenarioData?.contents) { - target.scenarioData.isUnlockedButNotCollected = true; - collectContainerContents(target); - - // Remove the device from the bluetoothDevices array - const deviceIndex = bluetoothDevices.findIndex(device => - device.mac.toLowerCase() === targetMac && device.inInventory - ); - - if (deviceIndex !== -1) { - bluetoothDevices.splice(deviceIndex, 1); - } - } - } - - gameAlert("Bluetooth connection spoofed. Device unlocked.", 'success', 'Spoofing Successful', 4000); - - // Update the Bluetooth panel to reflect the unlocked state - updateBluetoothPanel(); - - return true; - } else { - gameAlert("Bluetooth spoofer MAC address doesn't match the target device.", 'error', 'Spoofing Failed', 4000); - return false; - } - } - // Add helper function to generate fingerprint data function generateFingerprintData(item) { - // In a real implementation, this would generate unique fingerprint patterns - // For now, we'll just create a unique identifier - return `fp_${item.scenarioData.fingerprintOwner}_${Date.now()}`; + // For original samples from items, we use the item's data + if (item.scenarioData?.fingerprintOwner) { + return `fp_${item.scenarioData.fingerprintOwner}_${Date.now()}`; + } + + // Fallback unique identifier + return `fp_unknown_${Date.now()}`; } // Add helper function to check if player has required collection tools @@ -4160,7 +4290,6 @@ Owner: ${sample.owner}
Quality: ${qualityPercentage}%
ID: ${sample.id}
- ${sample.isSpoofed ? 'SPOOFED SAMPLE
' : ''} `; // Add quality bar @@ -4184,72 +4313,14 @@ qualityBar.appendChild(qualityFill); sampleElement.appendChild(qualityBar); - - // Add spoof button if not already spoofed - if (!sample.isSpoofed && hasItemInInventory('spoofing_kit')) { - const spoofButton = document.createElement('button'); - spoofButton.textContent = 'Create Spoof'; - spoofButton.style.cssText = ` - margin-top: 10px; - padding: 5px 10px; - background: #444; - border: none; - color: white; - border-radius: 3px; - cursor: pointer; - `; - spoofButton.onclick = async () => { - spoofButton.disabled = true; - spoofButton.textContent = 'Creating spoof...'; - - // Add progress bar - const progressBar = document.createElement('div'); - progressBar.style.cssText = ` - width: 100%; - height: 2px; - background: #333; - margin-top: 5px; - `; - const progress = document.createElement('div'); - progress.style.cssText = ` - width: 0%; - height: 100%; - background: #ff9900; - transition: width 0.1s linear; - `; - progressBar.appendChild(progress); - sampleElement.appendChild(progressBar); - - // Animate progress - let currentProgress = 0; - const interval = setInterval(() => { - currentProgress += 2; - progress.style.width = `${currentProgress}%`; - }, SPOOFING_TIME / 50); - - // Create spoof after delay - setTimeout(() => { - clearInterval(interval); - const spoofedSample = createSpoofedSample(sample); - if (spoofedSample) { - gameState.biometricSamples.push(spoofedSample); - showSamplesUI(); // Refresh UI - } - }, SPOOFING_TIME); - }; - sampleElement.appendChild(spoofButton); - } - samplesUI.appendChild(sampleElement); }); } // Helper function to hide samples UI function hideSamplesUI() { - const samplesUI = document.getElementById('biometric-samples-ui'); - if (samplesUI) { - samplesUI.style.display = 'none'; - } + const biometricsPanel = document.getElementById('biometrics-panel'); + biometricsPanel.style.display = 'none'; } // Helper function to get color based on quality @@ -4278,11 +4349,6 @@ } function generateFingerprintData(sample) { - // For spoofed samples, we generate from the original sample data - if (sample.data) { - return `spoofed_${sample.data}`; - } - // For original samples from items, we use the item's data if (sample.scenarioData?.fingerprintOwner) { return `fp_${sample.scenarioData.fingerprintOwner}_${Date.now()}`; @@ -4292,1180 +4358,1487 @@ return `fp_unknown_${Date.now()}`; } - // Add spoofing functionality - function createSpoofedSample(originalSample) { - if (!originalSample) { - alert("No sample to spoof from!"); - return null; - } - - // Check if player has required items - const hasSpoofingKit = hasItemInInventory('spoofing_kit'); - if (!hasSpoofingKit) { - alert("You need a spoofing kit to create fake fingerprints!"); - return null; - } - - // Create spoofed sample with slightly degraded quality - const spoofedSample = { - id: `spoofed_${originalSample.owner}_${Date.now()}`, - type: originalSample.type, - owner: originalSample.owner, - quality: originalSample.quality * SPOOF_QUALITY_MULTIPLIER, - data: generateFingerprintData(originalSample), - isSpoofed: true - }; - - return spoofedSample; - } - // Add dusting minigame - function startDustingMinigame(item) { - // Create iframe container - const iframe = document.createElement('div'); - iframe.style.cssText = ` - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 60%; - height: 60%; - background: rgba(0, 0, 0, 0.9); - border: 1px solid #444; - z-index: 1000; - padding: 20px; - border-radius: 5px; - `; + // Minigame Framework + const MinigameFramework = { + activeMinigame: null, + mainGameScene: null, - // Create game container - const gameContainer = document.createElement('div'); - gameContainer.style.cssText = ` - width: 100%; - height: calc(100% - 60px); - display: grid; - grid-template-columns: repeat(20, minmax(0, 1fr)); - grid-template-rows: repeat(20, minmax(0, 1fr)); - gap: 1px; - background: #222; - padding: 10px; - margin-top: 40px; - `; - - // Add instructions - const instructions = document.createElement('div'); - instructions.innerHTML = ` -

Fingerprint Dusting

-

- Drag to dust the surface and reveal fingerprints.
- 🔍 Gray = Light dusting
- 🟢 Green = Fingerprint found!
- ⚠️ White = Over-dusted (avoid this)
- Find all fingerprints with minimal over-dusting. -

- `; - instructions.style.cssText = ` - position: absolute; - top: 10px; - left: 50%; - transform: translateX(-50%); - width: 90%; - `; - - // Add progress display - const progressText = document.createElement('div'); - progressText.style.cssText = ` - position: absolute; - bottom: 10px; - left: 50%; - transform: translateX(-50%); - color: white; - text-align: center; - font-size: 16px; - `; - - // Generate fingerprint pattern - const gridSize = 20; - const fingerprintCells = new Set(); - const centerX = Math.floor(gridSize / 2); - const centerY = Math.floor(gridSize / 2); - - // Create a larger pattern (about 50 cells) spread around the center - for (let i = 0; i < 50; i++) { - const x = centerX + Math.floor(Math.random() * 10 - 5); // Increased spread - const y = centerY + Math.floor(Math.random() * 10 - 5); // Increased spread - if (x >= 0 && x < gridSize && y >= 0 && y < gridSize) { - fingerprintCells.add(`${x},${y}`); - } - } - - // If we didn't get enough cells, add more until we reach target - while (fingerprintCells.size < 50) { - const x = centerX + Math.floor(Math.random() * 12 - 6); - const y = centerY + Math.floor(Math.random() * 12 - 6); - if (x >= 0 && x < gridSize && y >= 0 && y < gridSize) { - fingerprintCells.add(`${x},${y}`); - } - } - - // Track progress - let revealedPrints = 0; - let totalPrints = fingerprintCells.size; - let overDusted = 0; - - // Create grid cells - for (let y = 0; y < gridSize; y++) { - for (let x = 0; x < gridSize; x++) { - const cell = document.createElement('div'); - cell.style.cssText = ` - width: 100%; - height: 100%; - background: black; - position: relative; - cursor: pointer; - `; - cell.dataset.x = x; - cell.dataset.y = y; - cell.dataset.dustLevel = '0'; - cell.dataset.hasFingerprint = fingerprintCells.has(`${x},${y}`); - - gameContainer.appendChild(cell); - } - } - - // Add dragging interaction at container level - let isDragging = false; - let lastDustTime = {}; // Track last dust time for each cell - - gameContainer.addEventListener('mousedown', () => isDragging = true); - gameContainer.addEventListener('mouseup', () => isDragging = false); - gameContainer.addEventListener('mouseleave', () => isDragging = false); - gameContainer.addEventListener('mousemove', (e) => { - if (!isDragging) return; + // Initialize the framework + init: function(mainScene) { + this.mainGameScene = mainScene; - // Get the cell element under the cursor - const cell = document.elementFromPoint(e.clientX, e.clientY); - if (cell && cell.dataset.dustLevel !== undefined) { - const cellId = `${cell.dataset.x},${cell.dataset.y}`; - const currentTime = Date.now(); - const dustLevel = parseInt(cell.dataset.dustLevel); + // Create container for all minigames if it doesn't exist + if (!document.getElementById('minigame-container')) { + const container = document.createElement('div'); + container.id = 'minigame-container'; + document.body.appendChild(container); - // Only allow dusting every 100ms for each cell - if (!lastDustTime[cellId] || currentTime - lastDustTime[cellId] > 100) { - if (dustLevel < 3) { - // Increment dust level with 33% chance after level 1 - if (dustLevel < 1 || Math.random() < 0.33) { - cell.dataset.dustLevel = (dustLevel + 1).toString(); - updateCellColor(cell); - checkProgress(); - } - lastDustTime[cellId] = currentTime; - } - } - } - }); - - function updateCellColor(cell) { - const dustLevel = parseInt(cell.dataset.dustLevel); - const hasFingerprint = cell.dataset.hasFingerprint === 'true'; - - if (dustLevel === 0) { - cell.style.background = 'black'; - } - else if (dustLevel === 1) { - cell.style.background = '#444'; - } - else if (dustLevel === 2) { - cell.style.background = hasFingerprint ? '#0f0' : '#888'; - } - else { - cell.style.background = '#ccc'; - } - } - - function checkProgress() { - revealedPrints = 0; - overDusted = 0; - - gameContainer.childNodes.forEach(cell => { - const dustLevel = parseInt(cell.dataset.dustLevel); - const hasFingerprint = cell.dataset.hasFingerprint === 'true'; - - if (hasFingerprint && dustLevel === 2) revealedPrints++; - if (dustLevel === 3) overDusted++; - }); - - const requiredPrints = Math.ceil(totalPrints * 0.4); // 40% requirement - progressText.innerHTML = ` -
Found: ${revealedPrints}/${requiredPrints} required prints
-
- Over-dusted: ${overDusted}/25 max -
- `; - - // Check fail condition first - if (overDusted >= 25) { - // Show failure message - const failureMessage = document.createElement('div'); - failureMessage.style.cssText = ` - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - background: rgba(0, 0, 0, 0.9); - padding: 20px; - border-radius: 5px; - color: #f00; - font-size: 20px; - text-align: center; - z-index: 1001; - `; - failureMessage.textContent = "Too many over-dusted areas!"; - iframe.appendChild(failureMessage); - - // Disable further interaction - isDragging = false; - gameContainer.style.pointerEvents = 'none'; - - setTimeout(() => { - document.body.removeChild(iframe); - scene.input.mouse.enabled = true; - }, 1500); - return; - } - - // Check win condition (existing code) - if (revealedPrints >= requiredPrints && overDusted < 25) { - // Show success message - const successMessage = document.createElement('div'); - successMessage.style.cssText = ` - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - background: rgba(0, 0, 0, 0.9); - padding: 20px; - border-radius: 5px; - color: #0f0; - font-size: 20px; - text-align: center; - z-index: 1001; - `; - successMessage.textContent = "Fingerprint successfully collected!"; - iframe.appendChild(successMessage); - - // Disable further interaction - isDragging = false; - gameContainer.style.pointerEvents = 'none'; - - setTimeout(() => { - // Add fingerprint to gameState - if (!gameState.biometricSamples) { - gameState.biometricSamples = []; - } - - const sample = { - id: generateFingerprintData(item), - type: 'fingerprint', - owner: item.scenarioData.fingerprintOwner, - quality: Math.random() * 0.3 + 0.7, // Random quality between 0.7 and 1.0 - data: generateFingerprintData(item), - timestamp: Date.now() - }; - - gameState.biometricSamples.push(sample); - - // Remove the minigame - document.body.removeChild(iframe); - scene.input.mouse.enabled = true; - - // Mark item as collected - if (item.scenarioData) { - item.scenarioData.hasFingerprint = false; - } - - // Update the biometrics panel and count - updateBiometricsPanel(); - updateBiometricsCount(); - - // Show notification - gameAlert(`Collected ${sample.owner}'s fingerprint sample`, 'success', 'Sample Acquired', 3000); - }, 1500); - } - } - - // Add close button - const closeButton = document.createElement('button'); - closeButton.textContent = 'X'; - closeButton.style.cssText = ` - position: absolute; - right: 10px; - top: 10px; - background: none; - border: none; - color: white; - font-size: 20px; - cursor: pointer; - `; - closeButton.onclick = () => { - document.body.removeChild(iframe); - scene.input.mouse.enabled = true; - }; - - // Assemble the interface - iframe.appendChild(closeButton); - iframe.appendChild(instructions); - iframe.appendChild(gameContainer); - iframe.appendChild(progressText); - document.body.appendChild(iframe); - - // Disable game movement - const scene = item.scene; - scene.input.mouse.enabled = false; - } - - function startLockpickingMinigame(lockable, currentScene, difficulty) { - // Create iframe container - const iframe = document.createElement('div'); - iframe.style.cssText = ` - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 60%; - height: 60%; - background: rgba(0, 0, 0, 0.9); - border: 1px solid #444; - z-index: 1000; - padding: 20px; - border-radius: 5px; - `; - - // Add instructions - const instructions = document.createElement('div'); - instructions.innerHTML = ` -

Lock Picking

-

- Use spacebar or click to toggle tension levels. Each pin requires the right amount of tension.
- 🔵 Blue = Pin moving
- 🟢 Green = Pin set correctly
- 🔴 Red = Over-pushed (reset)
- Set all pins in the correct order with the right tension without resetting. -

- `; - instructions.style.cssText = ` - position: absolute; - top: 10px; - left: 50%; - transform: translateX(-50%); - width: 90%; - `; - - // Create game container - const gameContainer = document.createElement('div'); - gameContainer.style.cssText = ` - width: 100%; - height: calc(100% - 60px); - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - gap: 20px; - margin-top: 40px; - background: #222; - padding: 10px; - `; - - // Add difficulty selection - const difficultySelect = document.createElement('select'); - difficultySelect.style.cssText = ` - margin-bottom: 10px; - padding: 5px; - background: #444; - color: white; - border: 1px solid #666; - border-radius: 3px; - `; - const difficulties = ['Easy - Pins Visible', 'Hard - Audio Only']; - difficulties.forEach(diff => { - const option = document.createElement('option'); - option.value = diff; - option.textContent = diff; - difficultySelect.appendChild(option); - }); - - // Add audio feedback - const lockSounds = { - click: null, // Basic pin movement sound - binding: null, // Sound when a pin is binding (correct tension) - set: null, // Sound when a pin is successfully set - reset: null, // Sound when pins are reset - wrong: null, // Sound when wrong tension or wrong pin - tension: null, // Sound when changing tension - success: null, // Sound when successfully picking the lock - overTension: null // Sound when over-tensioning the lock - }; - - const initAudio = () => { - if (!lockSounds.click) { - // Basic click sound (when pressing a pin) - lockSounds.click = new Audio('assets/sounds/lockpick_click.mp3'); - - // Binding sound (when a pin is binding with correct tension) - lockSounds.binding = new Audio('assets/sounds/lockpick_binding.mp3'); - - // Set sound (when a pin is successfully set) - lockSounds.set = new Audio('assets/sounds/lockpick_set.mp3'); - - // Reset sound (when pins are reset) - lockSounds.reset = new Audio('assets/sounds/lockpick_reset.mp3'); - - // Wrong sound (when using wrong tension or pressing wrong pin) - lockSounds.wrong = new Audio('assets/sounds/lockpick_wrong.mp3'); - - // Tension sound (when changing tension levels) - lockSounds.tension = new Audio('assets/sounds/lockpick_tension.mp3'); - - // Success sound (when successfully picking the lock) - lockSounds.success = new Audio('assets/sounds/lockpick_success.mp3'); - - // Over-tension sound (when applying too much tension) - lockSounds.overTension = new Audio('assets/sounds/lockpick_overtension.mp3'); - } - }; - - // Initialize audio on first interaction - gameContainer.addEventListener('mousedown', initAudio, { once: true }); - - // Add pin binding order and game state - const numPins = getPinCountForDifficulty(difficulty); - const bindingOrder = Array.from({length: numPins}, (_, i) => i) - .sort(() => Math.random() - 0.5); - - const gameState = { - tensionLevel: 1, // Start with light tension (1) - pinStates: Array(numPins).fill(0), // 0 = down, 1 = moving, 2 = set - pinPressTime: Array(numPins).fill(0), // Track how long each pin is pressed - currentBindingIndex: 0, - hardMode: false, - maxPressTime: 1000, // Max time to hold a pin (ms) - failCount: 0, - maxFails: 3, - overTensioned: false, - lastPinSetTime: 0, // Track when the last pin was set - isActivelyPickingPin: false, // Track if we're actively working on a pin - tensionRequirements: Array(numPins).fill(0).map(() => { - // Each pin requires a specific tension level (1, 2, or 3) - const randomValue = Math.random(); - if (randomValue < 0.33) return 1; // Light tension - if (randomValue < 0.66) return 2; // Medium tension - return 3; // Heavy tension - }) - }; - - // Create tension wrench toggle - const tensionWrench = document.createElement('div'); - tensionWrench.style.cssText = ` - width: 100px; - height: 30px; - background: #666; - border: 2px solid #888; - border-radius: 5px; - cursor: pointer; - margin-bottom: 20px; - text-align: center; - line-height: 30px; - color: white; - transform: rotate(2deg); - `; - tensionWrench.textContent = 'Tension: LIGHT'; - - // Function to reset pins - function resetPins(showVisual = true) { - gameState.pinStates.fill(0); - gameState.pinPressTime.fill(0); - gameState.currentBindingIndex = 0; - gameState.failCount++; - - if (showVisual) { - Array.from(pinsContainer.children).forEach(pin => { - pin.style.background = '#555'; - if (!gameState.hardMode) { - pin.style.transition = 'background-color 0.3s'; - pin.style.background = '#f00'; - setTimeout(() => pin.style.background = '#555', 300); - } - }); - } - - if (gameState.failCount >= gameState.maxFails) { - alert("Lock picking failed! The lock is now jammed."); - // Safely remove iframe if it exists in the document - const existingIframe = document.querySelector('div[style*="z-index: 1000"]'); - if (existingIframe && existingIframe.parentNode) { - existingIframe.parentNode.removeChild(existingIframe); - } - if (currentScene && currentScene.input && currentScene.input.mouse) { - currentScene.input.mouse.enabled = true; - } - } - } - - // Create a single function for toggling tension - function toggleTension() { - // Toggle between 3 tension levels (light -> medium -> heavy -> light) - const previousTensionLevel = gameState.tensionLevel; - gameState.tensionLevel = (gameState.tensionLevel % 3) + 1; - - // Play tension change sound - if (lockSounds.tension) { - lockSounds.tension.currentTime = 0; - lockSounds.tension.play().catch(e => console.log('Audio play failed:', e)); - } - - updateTensionWrench(); - - // Check if we're over-tensioning - but only if we've started interacting with pins - // AND only if we're actively working on a pin (not just changing tension) - const timeSinceLastPinSet = Date.now() - gameState.lastPinSetTime; - const isActivelyPickingLock = gameState.isActivelyPickingPin; - - if (gameState.tensionLevel === 3 && - gameState.currentBindingIndex > 0 && - timeSinceLastPinSet > 1000 && - isActivelyPickingLock) { - - // 30% chance of over-tensioning with heavy pressure - if (Math.random() < 0.3) { - gameState.overTensioned = true; - tensionWrench.style.background = '#ff3333'; - tensionWrench.style.transform = 'rotate(15deg)'; - - // Play over-tension sound - if (lockSounds.overTension) { - lockSounds.overTension.currentTime = 0; - lockSounds.overTension.play().catch(e => console.log('Audio play failed:', e)); - } - - setTimeout(() => { - alert("You applied too much tension and jammed the lock!"); - resetPins(); - }, 500); - } - } - } - - // Add this new function to update tension wrench visuals - function updateTensionWrench() { - // Update tension wrench appearance based on level - switch(gameState.tensionLevel) { - case 1: // Light - tensionWrench.style.background = '#666'; - tensionWrench.style.transform = 'rotate(2deg)'; - tensionWrench.textContent = 'Tension: LIGHT'; - break; - case 2: // Medium - tensionWrench.style.background = '#888'; - tensionWrench.style.transform = 'rotate(5deg)'; - tensionWrench.textContent = 'Tension: MEDIUM'; - break; - case 3: // Heavy - tensionWrench.style.background = '#aaa'; - tensionWrench.style.transform = 'rotate(8deg)'; - tensionWrench.textContent = 'Tension: HEAVY'; - break; - } - } - - // Create pins container - const pinsContainer = document.createElement('div'); - pinsContainer.style.cssText = ` - display: flex; - gap: 10px; - background: #333; - padding: 20px; - border-radius: 10px; - `; - - // Create individual pins - for (let i = 0; i < numPins; i++) { - const pin = document.createElement('div'); - pin.style.cssText = ` - width: 30px; - height: 100px; - background: #555; - border: 2px solid #777; - border-radius: 5px; - cursor: pointer; - position: relative; - transition: transform 0.1s, background-color 0.3s; - `; - - // Add a subtle indicator at the bottom of each pin - const pinIndicator = document.createElement('div'); - pinIndicator.style.cssText = ` - position: absolute; - bottom: 0; - left: 0; - width: 100%; - height: 5px; - background: #555; - transition: background-color 0.3s; - `; - pin.appendChild(pinIndicator); - - const pinNumber = document.createElement('div'); - pinNumber.style.cssText = ` - position: absolute; - top: -20px; - width: 100%; - text-align: center; - color: white; - `; - pinNumber.textContent = (i + 1).toString(); - pin.appendChild(pinNumber); - - // Function to update pin appearance based on its state and binding status - function updatePinAppearance() { - // Reset to default first - pin.style.background = '#555'; - pinIndicator.style.background = '#555'; - pin.style.animation = ''; - pin.style.borderColor = '#777'; - - // If the pin is set, show it as green - if (gameState.pinStates[i] === 2) { - pin.style.background = '#0f0'; - pin.style.cursor = 'default'; - pinIndicator.style.background = '#0f0'; - return; - } - - // Get the current binding pin - const bindingPin = bindingOrder[gameState.currentBindingIndex]; - - // Generate consistent red herrings based on the current binding index - // This ensures the same pins are highlighted as red herrings until the next pin is set - const redHerringSeeds = [ - (bindingPin * 3 + 7) % numPins, - (bindingPin * 5 + 3) % numPins - ]; - - // Filter out the binding pin and already set pins from red herrings - const redHerrings = redHerringSeeds.filter(index => - index !== bindingPin && gameState.pinStates[index] !== 2); - - // If this is the current binding pin, give a subtle hint based on difficulty - if (i === bindingPin) { - // For easy difficulty, make the binding pin more obvious - if (difficulty === 'easy') { - pinIndicator.style.background = '#ff9900'; // Orange indicator for binding pin - - // Also show the required tension level with a color hint - const requiredTension = gameState.tensionRequirements[i]; - if (requiredTension === 1) { - pin.style.borderColor = '#66ccff'; // Light blue for light tension - } else if (requiredTension === 2) { - pin.style.borderColor = '#9966ff'; // Purple for medium tension - } else { - pin.style.borderColor = '#ff6666'; // Red for heavy tension - } - - // Add a subtle animation - pin.style.animation = 'pinWiggle 2s infinite'; - } - // For medium difficulty, just show which pin is binding with less obvious cues - else if (difficulty === 'medium') { - pinIndicator.style.background = '#ff9900'; // Orange indicator for binding pin - pin.style.animation = 'pinWiggle 3s infinite'; - } - // For hard difficulty, very subtle indication - else if (difficulty === 'hard') { - pin.style.animation = 'pinWiggle 4s infinite 0.5s'; - } - } - // If this is a red herring, give misleading feedback - else if (redHerrings.includes(i) && gameState.currentBindingIndex > 0 && difficulty !== 'easy') { - // The amount of misleading feedback depends on difficulty - if (difficulty === 'medium') { - // For medium, make red herrings somewhat convincing - pinIndicator.style.background = '#ff9900'; // Same color as real binding pin - pin.style.animation = 'pinWiggle 3.5s infinite 0.7s'; // Similar wiggle to real pin - - // Randomly assign fake tension indicators to confuse - const fakeTension = Math.floor(Math.random() * 3) + 1; - if (fakeTension === 1) { - pin.style.borderColor = '#66ccff'; - } else if (fakeTension === 2) { - pin.style.borderColor = '#9966ff'; - } else { - pin.style.borderColor = '#ff6666'; - } - } - else if (difficulty === 'hard') { - // For hard, make red herrings very convincing - pin.style.animation = 'pinWiggle 4s infinite 0.3s'; - - // On hard, sometimes make a red herring more convincing than the real pin - if (Math.random() < 0.5) { - pinIndicator.style.background = '#ff9900'; - } - } - } - } - - // Add the wiggle animation to the document - if (!document.getElementById('pinWiggleAnimation')) { + // Add base styles const style = document.createElement('style'); - style.id = 'pinWiggleAnimation'; + style.id = 'minigame-framework-styles'; style.textContent = ` - @keyframes pinWiggle { - 0% { transform: translateY(0); } - 15% { transform: translateY(-2px); } - 30% { transform: translateY(0); } - 45% { transform: translateY(-1px); } - 60% { transform: translateY(0); } - 75% { transform: translateY(-0.5px); } - 100% { transform: translateY(0); } + /* Framework base styles */ + #minigame-container { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 1000; + display: none; + pointer-events: all; + } + + #minigame-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + z-index: 1001; + pointer-events: all; + } + + .minigame-scene { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: #222; + color: white; + border-radius: 10px; + box-shadow: 0 0 20px rgba(0,0,0,0.5); + z-index: 1002; + pointer-events: all; + } + + /* Common minigame UI elements */ + .minigame-close { + background: #555; + color: white; + border: none; + padding: 10px 20px; + border-radius: 5px; + cursor: pointer; + margin-top: 10px; + align-self: center; + } + + .minigame-header { + position: absolute; + top: 0; + left: 0; + width: 100%; + background: rgba(30, 30, 30, 0.9); + padding: 10px; + display: flex; + flex-direction: column; + align-items: center; + z-index: 5; + } + + .minigame-header h3 { + margin: 0 0 8px 0; + color: #fff; + text-align: center; + } + + .minigame-header p { + margin: 5px 0; + color: #ccc; + text-align: center; + font-size: 12px; + } + + .minigame-success-message { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: rgba(0, 0, 0, 0.9); + padding: 20px; + border-radius: 5px; + color: #0f0; + font-size: 20px; + text-align: center; + z-index: 1003; + box-shadow: 0 0 20px rgba(0, 255, 0, 0.3); + } + + .minigame-failure-message { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: rgba(0, 0, 0, 0.9); + padding: 20px; + border-radius: 5px; + color: #f00; + font-size: 20px; + text-align: center; + z-index: 1003; + box-shadow: 0 0 20px rgba(255, 0, 0, 0.3); + } + + .minigame-tool-button { + background-color: #444; + color: white; + border: none; + border-radius: 3px; + padding: 5px 10px; + cursor: pointer; + opacity: 0.7; + transition: opacity 0.2s, background-color 0.2s; + } + + .minigame-tool-button.active { + opacity: 1; + } + + .minigame-progress-container { + height: 6px; + width: 100%; + background: #333; + border-radius: 3px; + overflow: hidden; + } + + .minigame-progress-bar { + height: 100%; + width: 0%; + background: #2ecc71; + transition: width 0.3s; + } + + .minigame-scene.success { + border: 2px solid #33cc33; + } + + .minigame-scene.failure { + border: 2px solid #cc3333; } `; document.head.appendChild(style); } - - // Update all pins whenever a pin state changes - function updateAllPins() { - // Get the current binding pin - const bindingPin = bindingOrder[gameState.currentBindingIndex]; - - // Generate consistent red herrings based on the current binding index - const redHerringSeeds = [ - (bindingPin * 3 + 7) % numPins, - (bindingPin * 5 + 3) % numPins - ]; - - // Filter out the binding pin and already set pins from red herrings - const redHerrings = redHerringSeeds.filter(index => - index !== bindingPin && gameState.pinStates[index] !== 2); - - Array.from(pinsContainer.children).forEach((pin, index) => { - // Find the indicator within this pin - const indicator = pin.querySelector('div:first-child'); - - // Reset styles first - pin.style.background = '#555'; - pin.style.animation = ''; - pin.style.borderColor = '#777'; - if (indicator) indicator.style.background = '#555'; - - // Update based on current game state - if (gameState.pinStates[index] === 2) { - pin.style.background = '#0f0'; - pin.style.cursor = 'default'; - if (indicator) indicator.style.background = '#0f0'; - } else { - pin.style.cursor = 'pointer'; - - // Check if this is the binding pin - if (index === bindingPin && !gameState.hardMode) { - if (difficulty === 'easy') { - if (indicator) indicator.style.background = '#ff9900'; - - // Show tension hint - const requiredTension = gameState.tensionRequirements[index]; - if (requiredTension === 1) { - pin.style.borderColor = '#66ccff'; - } else if (requiredTension === 2) { - pin.style.borderColor = '#9966ff'; - } else { - pin.style.borderColor = '#ff6666'; - } - - pin.style.animation = 'pinWiggle 2s infinite'; - } - else if (difficulty === 'medium') { - if (indicator) indicator.style.background = '#ff9900'; - pin.style.animation = 'pinWiggle 3s infinite'; - } - else if (difficulty === 'hard') { - pin.style.animation = 'pinWiggle 4s infinite 0.5s'; - } - } - // Check if this is a red herring, but only for medium and hard difficulties - else if (redHerrings.includes(index) && gameState.currentBindingIndex > 0 && difficulty !== 'easy') { - if (difficulty === 'medium') { - if (indicator) indicator.style.background = '#ff9900'; - pin.style.animation = 'pinWiggle 3.5s infinite 0.7s'; - - const fakeTension = Math.floor(Math.random() * 3) + 1; - if (fakeTension === 1) { - pin.style.borderColor = '#66ccff'; - } else if (fakeTension === 2) { - pin.style.borderColor = '#9966ff'; - } else { - pin.style.borderColor = '#ff6666'; - } - } - else if (difficulty === 'hard') { - pin.style.animation = 'pinWiggle 4s infinite 0.3s'; - if (Math.random() < 0.5 && indicator) { - indicator.style.background = '#ff9900'; - } - } - } - } - }); - } - - // Call updatePinAppearance initially and whenever the game state changes - updatePinAppearance(); - - let pressStartTime = 0; - let pressTimer = null; - - function checkPinPress() { - if (pressStartTime === 0) return; - - const pressDuration = Date.now() - pressStartTime; - if (pressDuration > gameState.maxPressTime) { - // Clear the timer first before calling resetPins - clearInterval(pressTimer); - pressTimer = null; - resetPins(); - } - } - - pin.onmousedown = () => { - // First check if this pin is already set - const pinIndex = Array.from(pinsContainer.children).indexOf(pin); - if (gameState.pinStates[pinIndex] === 2) { - // Pin is already set, don't allow interaction - return; - } - - // Set the flag to indicate we're actively picking a pin - gameState.isActivelyPickingPin = true; - - // Play basic click sound - if (lockSounds.click) { - lockSounds.click.currentTime = 0; - lockSounds.click.play().catch(e => console.log('Audio play failed:', e)); - } - - pressStartTime = Date.now(); - pressTimer = setInterval(checkPinPress, 100); - - pin.style.transform = 'translateY(-10px)'; - - // Each pin has different tension requirements - const bindingPin = bindingOrder[gameState.currentBindingIndex]; - const requiredTension = gameState.tensionRequirements[pinIndex]; - - // Check if this is the current binding pin - if (pinIndex === bindingPin) { - // This pin needs exactly the right tension level - const correctTension = (gameState.tensionLevel === requiredTension); - - if (correctTension && !gameState.overTensioned) { - // Play binding sound - correct pin with correct tension - if (lockSounds.binding) { - lockSounds.binding.currentTime = 0; - lockSounds.binding.play().catch(e => console.log('Audio play failed:', e)); - } - - if (!gameState.hardMode) { - pin.style.background = '#00f'; - } - - // Start a timer to set the pin - setTimeout(() => { - if (pressStartTime !== 0) { // Still pressing - // Double-check tension is still correct - const stillCorrectTension = (gameState.tensionLevel === requiredTension); - - if (stillCorrectTension && !gameState.overTensioned) { - gameState.pinStates[pinIndex] = 2; - gameState.currentBindingIndex++; - gameState.lastPinSetTime = Date.now(); - - // Play set sound - pin successfully set - if (lockSounds.set) { - lockSounds.set.currentTime = 0; - lockSounds.set.play().catch(e => console.log('Audio play failed:', e)); - } - - if (!gameState.hardMode) { - pin.style.background = '#0f0'; - pinIndicator.style.background = '#0f0'; - } - - // Update all pins to show new binding state - updateAllPins(); - - checkWinCondition(); - } - } - }, 500); - } else if (gameState.tensionLevel > 0) { - // Wrong tension but trying - give feedback - // Play wrong sound - wrong tension on correct pin - if (lockSounds.wrong) { - lockSounds.wrong.currentTime = 0; - lockSounds.wrong.play().catch(e => console.log('Audio play failed:', e)); - } - - if (!gameState.hardMode) { - pin.style.background = '#00f'; - } - - // Start counting towards potential reset - gameState.pinPressTime[pinIndex] = Date.now(); - } - } else if (gameState.tensionLevel > 0 && gameState.pinStates[pinIndex] !== 2) { - // Wrong pin - give feedback - if (lockSounds.wrong) { - lockSounds.wrong.currentTime = 0; - lockSounds.wrong.play().catch(e => console.log('Audio play failed:', e)); - } - - if (!gameState.hardMode) { - pin.style.background = '#00f'; - } - // Start counting towards potential reset - gameState.pinPressTime[pinIndex] = Date.now(); - } - }; - - pin.onmouseup = pin.onmouseleave = () => { - // Clear the flag to indicate we're no longer actively picking a pin - gameState.isActivelyPickingPin = false; - - pressStartTime = 0; - if (pressTimer) { - clearInterval(pressTimer); - pressTimer = null; - } - - pin.style.transform = 'translateY(0)'; - if (gameState.pinStates[i] !== 2) { - pin.style.background = '#555'; - // Update appearance to show binding status - updatePinAppearance(); - } - }; - - pinsContainer.appendChild(pin); - } - - difficultySelect.onchange = () => { - gameState.hardMode = difficultySelect.value.includes('Hard'); - Array.from(pinsContainer.children).forEach(pin => { - pin.style.opacity = gameState.hardMode ? '0.1' : '1'; - }); - }; - - // Add components to game container - gameContainer.appendChild(difficultySelect); - gameContainer.appendChild(tensionWrench); - gameContainer.appendChild(pinsContainer); - - // Add close button - const closeButton = document.createElement('button'); - closeButton.textContent = 'X'; - closeButton.style.cssText = ` - position: absolute; - right: 10px; - top: 10px; - background: none; - border: none; - color: white; - font-size: 20px; - cursor: pointer; - `; - closeButton.onclick = () => { - document.body.removeChild(iframe); - if (currentScene && currentScene.input && currentScene.input.mouse) { - currentScene.input.mouse.enabled = true; - } - }; - - // Assemble the interface - iframe.appendChild(closeButton); - iframe.appendChild(instructions); - iframe.appendChild(gameContainer); - document.body.appendChild(iframe); + }, - // Disable game movement - if (currentScene && currentScene.input && currentScene.input.mouse) { - currentScene.input.mouse.enabled = false; - } - - // Add this function before the pin creation loop - function checkWinCondition() { - if (gameState.currentBindingIndex >= numPins) { - // Play success sound - if (lockSounds.success) { - lockSounds.success.currentTime = 0; - lockSounds.success.play().catch(e => console.log('Audio play failed:', e)); + // Launch a minigame + startMinigame: function(minigameId, params = {}) { + // Don't allow multiple minigames at once + if (this.activeMinigame) { + console.warn('A minigame is already running!'); + return false; + } + + // Get the minigame scene constructor + const MinigameScene = this.scenes[minigameId]; + if (!MinigameScene) { + console.error(`Minigame "${minigameId}" not found!`); + return false; + } + + // Pause the main game + if (this.mainGameScene) { + // Pause Phaser scene + if (this.mainGameScene.scene && typeof this.mainGameScene.scene.pause === 'function') { + this.mainGameScene.scene.pause(); } + } + + // Show the minigame container + const container = document.getElementById('minigame-container'); + container.style.display = 'block'; + + // Create overlay + const overlay = document.createElement('div'); + overlay.id = 'minigame-overlay'; + container.appendChild(overlay); + + // Create scene element + const sceneElement = document.createElement('div'); + sceneElement.className = 'minigame-scene'; + sceneElement.id = `minigame-scene-${minigameId}`; + container.appendChild(sceneElement); + + // Instantiate the minigame scene + this.activeMinigame = new MinigameScene(sceneElement, { + ...params, + onComplete: (success, result) => { + this.endMinigame(success, result); + if (params.onComplete) params.onComplete(success, result); + } + }); + + // Initialize and start the minigame + this.activeMinigame.init(); + this.activeMinigame.start(); + + return true; + }, + + // End the active minigame + endMinigame: function(success = false, result = null) { + if (!this.activeMinigame) return; + + // Call the minigame's cleanup method + if (typeof this.activeMinigame.cleanup === 'function') { + this.activeMinigame.cleanup(); + } + + // Remove the minigame elements + const container = document.getElementById('minigame-container'); + container.innerHTML = ''; + container.style.display = 'none'; + + // Unpause the main game + if (this.mainGameScene) { + // Resume Phaser scene + if (this.mainGameScene.scene && typeof this.mainGameScene.scene.resume === 'function') { + this.mainGameScene.scene.resume(); + } + } + + this.activeMinigame = null; + }, + + // Register a new minigame scene + registerScene: function(id, sceneClass) { + if (!this.scenes) this.scenes = {}; + this.scenes[id] = sceneClass; + }, + + // Base class for minigame scenes + MinigameScene: class { + constructor(container, params = {}) { + this.container = container; + this.params = params; - console.log('Lock picked successfully:', lockable); + // Common game state management + this.gameState = { + isActive: false, + isDragging: false, + mousePosition: { x: 0, y: 0 }, + mouseDown: false, + mouseButtonsPressed: [false, false, false], + keyState: {}, + success: false, + gameComplete: false, + startTime: 0, + elapsedTime: 0 + }; - // Create success message - const successMessage = document.createElement('div'); - successMessage.style.cssText = ` + // Bind event handlers to the instance + this._handleMouseDown = this._handleMouseDown.bind(this); + this._handleMouseUp = this._handleMouseUp.bind(this); + this._handleMouseMove = this._handleMouseMove.bind(this); + this._handleMouseLeave = this._handleMouseLeave.bind(this); + this._handleKeyDown = this._handleKeyDown.bind(this); + this._handleKeyUp = this._handleKeyUp.bind(this); + + // Store event listeners for cleanup + this._eventListeners = []; + } + + init() { + console.log(`Initializing minigame scene`); + + // Ensure the scene container is visible + this.container.style.display = 'flex'; + + // Create standard layout elements + this.createLayout(); + + // Add escape key handler + this.escHandler = (e) => { + if (e.key === 'Escape' && this.gameState.isActive) { + this.complete(false); + } + }; + document.addEventListener('keydown', this.escHandler); + + // Setup common event handling + this.setupEventHandling(); + + // Record start time + this.gameState.startTime = Date.now(); + } + + // Create standard layout for minigames + createLayout() { + // Create and add the header + this.headerElement = document.createElement('div'); + this.headerElement.className = 'minigame-header'; + this.container.appendChild(this.headerElement); + + // Create the main game container + this.gameContainer = document.createElement('div'); + this.gameContainer.className = 'minigame-game-container'; + this.gameContainer.style.cssText = ` + width: 80%; + height: 80%; + margin: 70px auto 20px auto; + background: #1a1a1a; + border-radius: 5px; + box-shadow: 0 0 15px rgba(0, 0, 0, 0.5) inset; + position: relative; + overflow: hidden; + `; + this.container.appendChild(this.gameContainer); + + // Create message container for notifications, success/failure + this.messageContainer = document.createElement('div'); + this.messageContainer.className = 'minigame-message-container'; + this.messageContainer.style.cssText = ` + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 1000; + `; + this.container.appendChild(this.messageContainer); + + // Create progress display + this.progressContainer = document.createElement('div'); + this.progressContainer.style.cssText = ` + position: absolute; + bottom: 15px; + left: 50%; + transform: translateX(-50%); + color: white; + text-align: center; + font-size: 16px; + background: rgba(0, 0, 0, 0.6); + padding: 5px 15px; + border-radius: 15px; + z-index: 10; + width: 80%; + max-width: 500px; + `; + this.container.appendChild(this.progressContainer); + + // Create standard close button + const closeButton = document.createElement('button'); + closeButton.textContent = '✕'; + closeButton.style.cssText = ` + position: absolute; + right: 10px; + top: 10px; + background: none; + border: none; + color: white; + font-size: 20px; + cursor: pointer; + z-index: 100; + `; + closeButton.onclick = () => this.complete(false); + this.container.appendChild(closeButton); + } + + // Set up common event handling for mouse, touch, keyboard + setupEventHandling() { + // Mouse events + this.addEventListenerWithCleanup(this.gameContainer, 'mousedown', this._handleMouseDown); + this.addEventListenerWithCleanup(document, 'mouseup', this._handleMouseUp); + this.addEventListenerWithCleanup(this.gameContainer, 'mousemove', this._handleMouseMove); + this.addEventListenerWithCleanup(this.gameContainer, 'mouseleave', this._handleMouseLeave); + + // Keyboard events + this.addEventListenerWithCleanup(document, 'keydown', this._handleKeyDown); + this.addEventListenerWithCleanup(document, 'keyup', this._handleKeyUp); + + // Prevent context menu + this.addEventListenerWithCleanup(this.gameContainer, 'contextmenu', (e) => e.preventDefault()); + } + + // Utility to add event listener and track it for cleanup + addEventListenerWithCleanup(element, eventType, handler, options) { + element.addEventListener(eventType, handler, options); + this._eventListeners.push({ element, eventType, handler }); + } + + // Event handlers + _handleMouseDown(e) { + console.log("Mouse down in framework handler"); + this.gameState.mouseDown = true; + this.gameState.mouseButtonsPressed[e.button] = true; + this.gameState.isDragging = true; + + // Call subclass handler if it exists + if (typeof this.handleMouseDown === 'function') { + this.handleMouseDown(e); + } + } + + _handleMouseUp(e) { + console.log("Mouse up in framework handler"); + this.gameState.mouseDown = false; + this.gameState.mouseButtonsPressed[e.button] = false; + this.gameState.isDragging = false; + + // Call subclass handler if it exists + if (typeof this.handleMouseUp === 'function') { + this.handleMouseUp(e); + } + } + + _handleMouseMove(e) { + // Update mouse position + const rect = this.gameContainer.getBoundingClientRect(); + this.gameState.mousePosition = { + x: e.clientX - rect.left, + y: e.clientY - rect.top, + clientX: e.clientX, + clientY: e.clientY, + screenX: e.screenX, + screenY: e.screenY + }; + + // Call subclass handler if it exists + if (typeof this.handleMouseMove === 'function') { + this.handleMouseMove(e); + } + } + + _handleMouseLeave(e) { + this.gameState.isDragging = false; + + // Call subclass handler if it exists + if (typeof this.handleMouseLeave === 'function') { + this.handleMouseLeave(e); + } + } + + _handleKeyDown(e) { + this.gameState.keyState[e.code] = true; + + // Call subclass handler if it exists + if (typeof this.handleKeyDown === 'function') { + this.handleKeyDown(e); + } + } + + _handleKeyUp(e) { + this.gameState.keyState[e.code] = false; + + // Call subclass handler if it exists + if (typeof this.handleKeyUp === 'function') { + this.handleKeyUp(e); + } + } + + // Message display system + showMessage(message, type = 'info', duration = 0) { + // Create a message element + const messageElement = document.createElement('div'); + messageElement.className = `minigame-message minigame-message-${type}`; + messageElement.innerHTML = message; + + // Style based on type + let styles = ` position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); - background: rgba(0, 0, 0, 0.8); - color: #0f0; padding: 20px; - border-radius: 10px; - font-size: 24px; + border-radius: 5px; text-align: center; - z-index: 1002; + z-index: 1001; + pointer-events: all; + max-width: 90%; + transition: opacity 0.3s; `; - successMessage.textContent = "Lock successfully picked!"; - iframe.appendChild(successMessage); - // Disable further interaction - gameContainer.style.pointerEvents = 'none'; - - setTimeout(() => { - // For doors, we need to check properties and handle differently - if (lockable && lockable.properties && lockable.properties.locked) { - console.log('Unlocking door tile:', lockable); - unlockDoor(lockable, lockable.layer); - } - // For containers and other lockable items - else if (lockable && lockable.scenarioData) { - console.log('Unlocking container:', lockable.scenarioData); - lockable.scenarioData.locked = false; - - // Set the flag to indicate the container is unlocked but contents not collected - if (lockable.scenarioData.contents && lockable.scenarioData.contents.length > 0) { - lockable.scenarioData.isUnlockedButNotCollected = true; - debugLog('Container unlocked and ready for collection', lockable.scenarioData, 1); - } - } - - // Remove the minigame - document.body.removeChild(iframe); - if (currentScene && currentScene.input && currentScene.input.mouse) { - currentScene.input.mouse.enabled = true; - } - }, 1500); - - return true; - } - return false; - } - - // Use the toggleTension function for both click and keyboard events - tensionWrench.onclick = toggleTension; - - document.addEventListener('keydown', function(event) { - // Only process if the lockpicking minigame is active - if (!document.querySelector('div[style*="z-index: 1000"]')) return; - - if (event.code === 'Space') { - event.preventDefault(); // Prevent page scrolling - toggleTension(); - } - }); - - // Keep only the table debug function - function logTensionDebugInfo() { - // Only show debug info if debug mode is enabled - if (!DEBUG_MODE.enabled) return; - - DEBUG_MODE.log("=== LOCKPICKING DEBUG INFO ==="); - DEBUG_MODE.log("Pin binding order and tension requirements:"); - - const tableData = []; - - for (let orderIndex = 0; orderIndex < numPins; orderIndex++) { - const pinIndex = bindingOrder[orderIndex]; - const requiredTension = gameState.tensionRequirements[pinIndex]; - let tensionNeeded; - - switch(requiredTension) { - case 1: tensionNeeded = 'Light'; break; - case 2: tensionNeeded = 'Medium'; break; - case 3: tensionNeeded = 'Heavy'; break; - default: tensionNeeded = 'Unknown'; + switch(type) { + case 'success': + messageElement.className = 'minigame-success-message'; + break; + case 'failure': + case 'error': + messageElement.className = 'minigame-failure-message'; + break; + default: + styles += ` + background: rgba(0, 0, 0, 0.8); + color: white; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); + `; } - tableData.push({ - 'Binding Order': orderIndex + 1, - 'Pin #': pinIndex + 1, - 'Tension Required': tensionNeeded - }); + // Apply styles + messageElement.style.cssText = styles; + + // Add to the message container + this.messageContainer.appendChild(messageElement); + + // Auto-remove after duration, if specified + if (duration > 0) { + setTimeout(() => { + messageElement.style.opacity = '0'; + setTimeout(() => { + if (messageElement.parentNode) { + messageElement.parentNode.removeChild(messageElement); + } + }, 300); + }, duration); + } + + return messageElement; } - console.table(tableData); + // Success/failure methods + showSuccess(message, autoComplete = true, delay = 2000) { + this.gameState.success = true; + this.gameState.gameComplete = true; + this.container.classList.add('success'); + + // Create success message + const successMessage = this.showMessage(message, 'success'); + + // Auto-complete after delay + if (autoComplete) { + setTimeout(() => { + this.complete(true); + }, delay); + } + + return successMessage; + } + + showFailure(message, autoComplete = true, delay = 2000) { + this.gameState.success = false; + this.gameState.gameComplete = true; + this.container.classList.add('failure'); + + // Create failure message + const failureMessage = this.showMessage(message, 'failure'); + + // Auto-complete after delay + if (autoComplete) { + setTimeout(() => { + this.complete(false); + }, delay); + } + + return failureMessage; + } + + // Progress updates + updateProgress(current, total, label = '') { + const percentage = Math.min(100, Math.max(0, (current / total) * 100)); + + this.progressContainer.innerHTML = ` + ${label ? `
${label}
` : ''} +
+
+
+ `; + } + + start() { + this.gameState.isActive = true; + console.log("Minigame started"); + } + + update(deltaTime) { + // Update elapsed time + this.gameState.elapsedTime = Date.now() - this.gameState.startTime; + + // Override in subclass for game-specific updates + } + + complete(success, result = null) { + console.log(`Minigame complete, success: ${success}`); + this.gameState.isActive = false; + this.gameState.success = success; + + // Remove event listeners + document.removeEventListener('keydown', this.escHandler); + this.cleanup(); + + if (typeof this.params.onComplete === 'function') { + this.params.onComplete(success, result); + } + } + + cleanup() { + console.log("Cleaning up minigame"); + + // Remove all tracked event listeners + this._eventListeners.forEach(({ element, eventType, handler }) => { + element.removeEventListener(eventType, handler); + }); + this._eventListeners = []; + } } + }; - // Call this function instead of addTensionDebugDisplay - logTensionDebugInfo(); - } - - // Add this function to get pin count based on difficulty - function getPinCountForDifficulty(difficulty) { - switch(difficulty?.toLowerCase()) { - case 'easy': - return 3; - case 'medium': - return 5; - case 'hard': - return 7; - default: - return 5; // Default to medium difficulty + // Lockpicking Minigame Scene implementation + class LockpickingMinigame extends MinigameFramework.MinigameScene { + constructor(container, params) { + super(container, params); + + this.lockable = params.lockable; + this.difficulty = params.difficulty || 'medium'; + this.pinCount = this.difficulty === 'easy' ? 3 : this.difficulty === 'medium' ? 4 : 5; + + this.pins = []; + + // Use gameState from the framework but extend it with lockpicking-specific properties + this.lockState = { + tensionApplied: false, + pinsSet: 0, + currentPin: null + }; } - } - - // removes an item from the inventory - function removeFromInventory(sprite) { - if (!sprite || !inventory.items) { + + init() { + // Call parent init to set up common components + super.init(); + + console.log("Lockpicking minigame initializing"); + + // Configure container size and layout + this.container.style.width = '90%'; + this.container.style.maxWidth = '500px'; + this.container.style.padding = '20px'; + this.container.style.gap = '15px'; + + // Set up header content with proper spacing + this.headerElement.innerHTML = ` +

Lockpicking

+

Apply tension and hold click on pins to lift them to the shear line

+ `; + this.headerElement.style.marginBottom = '30px'; // Add more space below header + + // Add custom styles for the lockpicking minigame if they don't exist + if (!document.getElementById('lockpicking-styles')) { + const style = document.createElement('style'); + style.id = 'lockpicking-styles'; + style.textContent = ` + /* Game container styles */ + .minigame-container { + padding-top: 80px !important; /* Add padding at top to prevent header overlap */ + position: relative; + } + + .minigame-header { + position: absolute; + top: 0; + left: 0; + right: 0; + z-index: 10; + background: rgba(34, 34, 34, 0.95); + border-bottom: 1px solid #444; + padding: 10px 20px; + margin-bottom: 20px; + } + + .lock-visual { + display: flex; + justify-content: space-evenly; + align-items: center; + gap: 20px; + height: 200px; + background: #f0e6a6; /* Light yellow/beige background */ + border-radius: 5px; + padding: 25px; + position: relative; + margin-top: 20px; /* Add top margin for better spacing from header */ + margin-bottom: 20px; + border: 2px solid #887722; + z-index: 1; /* Ensure pins are below header */ + } + + /* Rest of existing CSS */ + .pin { + width: 40px; + height: 150px; + 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: 70px; + z-index: 5; + } + + .pin-assembly { + position: absolute; + bottom: 0; + width: 100%; + height: 140px; + transition: transform 0.05s; + } + + .key-pin { + position: absolute; + bottom: 0; + width: 100%; + height: 50px; /* Fixed height for all pins */ + 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: transform 0.3s; + } + + .driver-pin { + position: absolute; + width: 100%; + height: 70px; + background: #3388dd; /* Blue for driver pins */ + bottom: 50px; /* Position right above key pin */ + border-radius: 0 0 0 0; + transition: transform 0.3s, background-color 0.3s; + } + + .spring { + position: absolute; + bottom: 120px; + width: 100%; + height: 40px; + 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: transform 0.3s; + } + + .pin.binding { + box-shadow: 0 0 8px 2px #ffcc00; + } + + /* Remove the pin-assembly transform for set pins */ + .pin.set .pin-assembly { + transform: none; /* Reset transform so we can control individual pieces */ + } + + /* Keep driver pin (blue) above the shear line when set */ + .pin.set .driver-pin { + background: #22aa22; /* Green to indicate set */ + } + + /* Reset key pin (red) to the bottom when set */ + .pin.set .key-pin { + transform: translateY(0); /* Keep at bottom */ + } + + .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; + } + + .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; + } + `; + document.head.appendChild(style); + } + + // Add a class to the container for positioning + this.container.classList.add('minigame-container'); + this.headerElement.classList.add('minigame-header'); + + // Replace the game container with custom lockpicking interface + this.setupLockpickingInterface(); + + // Create pins with random binding order + this.createPins(); + + // Update the progress display with lockpicking instructions + this.updateFeedback("Apply tension first, then click and hold on pins to lift them"); + } + + setupLockpickingInterface() { + // Remove default game container (we'll use custom layout) + if (this.gameContainer.parentNode) { + this.gameContainer.parentNode.removeChild(this.gameContainer); + } + + // Create the content wrapper with padding to avoid header overlap + const contentWrapper = document.createElement('div'); + contentWrapper.style.paddingTop = '10px'; + this.container.appendChild(contentWrapper); + this.contentWrapper = contentWrapper; + + // Create instructions + const instructions = document.createElement('div'); + instructions.className = 'instructions'; + instructions.textContent = 'Apply tension first, then click and hold on pins to lift them to the shear line'; + contentWrapper.appendChild(instructions); + + // Create the lock visual container + const lockVisual = document.createElement('div'); + lockVisual.className = 'lock-visual'; + contentWrapper.appendChild(lockVisual); + this.lockVisual = lockVisual; + + // Remove cylinder creation - it's no longer needed + + // Add tension toggle control with horizontal movement + const tensionControl = document.createElement('div'); + tensionControl.className = 'tension-control'; + + const wrenchContainer = document.createElement('div'); + wrenchContainer.className = 'tension-wrench-container'; + + const wrenchLabel = document.createElement('div'); + wrenchLabel.textContent = 'Tension Wrench'; + wrenchLabel.style.fontSize = '14px'; + wrenchContainer.appendChild(wrenchLabel); + + // Add tension track and progress + const tensionTrack = document.createElement('div'); + tensionTrack.className = 'tension-track'; + + const tensionProgress = document.createElement('div'); + tensionProgress.className = 'tension-progress'; + tensionTrack.appendChild(tensionProgress); + + wrenchContainer.appendChild(tensionTrack); + + const tensionWrench = document.createElement('div'); + tensionWrench.className = 'tension-wrench'; + tensionWrench.innerHTML = ` +
+
+ `; + wrenchContainer.appendChild(tensionWrench); + + tensionControl.appendChild(wrenchContainer); + + const tensionStatus = document.createElement('div'); + tensionStatus.className = 'tension-status'; + tensionStatus.textContent = 'Click wrench to apply tension'; + tensionControl.appendChild(tensionStatus); + + this.contentWrapper.appendChild(tensionControl); + + // Feedback area + const feedback = document.createElement('div'); + feedback.className = 'lockpick-feedback'; + this.contentWrapper.appendChild(feedback); + this.feedback = feedback; + + // Set up tension wrench interaction with horizontal movement + tensionWrench.addEventListener('click', () => { + this.lockState.tensionApplied = !this.lockState.tensionApplied; + tensionWrench.classList.toggle('active', this.lockState.tensionApplied); + + // Move wrench horizontally instead of rotating + if (this.lockState.tensionApplied) { + // Move to initial right position (25%) + this.updateTensionPosition(25); + } else { + // Return to left position (0%) + this.updateTensionPosition(0); + + // Reset progress fill + tensionProgress.style.width = '0%'; + } + + // Update status text + tensionStatus.textContent = this.lockState.tensionApplied ? + 'Tension applied - now lift pins' : 'Click wrench to apply tension'; + + // Update which pins are binding + this.updateBindingPins(); + + // If tension is toggled off, reset any unset pins + if (!this.lockState.tensionApplied) { + this.pins.forEach(pin => { + if (!pin.isSet) { + pin.currentHeight = 0; + this.updatePinVisual(pin); + } + }); + this.updateFeedback("Tension released - apply tension before lifting pins"); + } else { + this.updateFeedback("Tension applied - click and hold on pins to lift them"); + } + }); + + // Store references + this.tensionWrench = tensionWrench; + this.tensionStatus = tensionStatus; + this.tensionProgress = tensionProgress; + } + + // New method to update the tension wrench position + updateTensionPosition(percentage) { + if (percentage < 0) percentage = 0; + if (percentage > 100) percentage = 100; + + // Calculate position based on container width + const containerWidth = this.tensionWrench.parentElement.offsetWidth; + const wrenchWidth = this.tensionWrench.offsetWidth; + const maxOffset = containerWidth - wrenchWidth; + const position = (maxOffset * percentage) / 100; + + // Update wrench position + this.tensionWrench.style.transform = `translateX(${position}px)`; + + // Update progress bar fill + if (this.tensionProgress) { + this.tensionProgress.style.width = `${percentage}%`; + } + } + + createPins() { + // Generate random binding order + const bindingOrder = this.shuffleArray([...Array(this.pinCount).keys()]); + + for (let i = 0; i < this.pinCount; i++) { + // Create pin container + const pinElement = document.createElement('div'); + pinElement.className = 'pin'; + pinElement.dataset.index = i; + this.lockVisual.appendChild(pinElement); + + // Create shear line + const shearLine = document.createElement('div'); + shearLine.className = 'shear-line'; + pinElement.appendChild(shearLine); + + // Create pin assembly container + const pinAssembly = document.createElement('div'); + pinAssembly.className = 'pin-assembly'; + pinElement.appendChild(pinAssembly); + + // Create key pin (bottom pin) with varying height + const keyPin = document.createElement('div'); + keyPin.className = 'key-pin'; + pinAssembly.appendChild(keyPin); + + // Generate random key pin height (30px to 60px) + const keyPinHeight = Math.floor(Math.random() * 31) + 30; + keyPin.style.height = `${keyPinHeight}px`; + + // Create driver pin (top pin) with consistent height + const driverPin = document.createElement('div'); + driverPin.className = 'driver-pin'; + pinAssembly.appendChild(driverPin); + // Position driver pin right above the key pin + driverPin.style.bottom = `${keyPinHeight}px`; + + // Create spring + const spring = document.createElement('div'); + spring.className = 'spring'; + pinAssembly.appendChild(spring); + // Position spring above driver pin + spring.style.bottom = `${keyPinHeight + 70}px`; // 70px is driver pin height + + // Calculate the distance from the bottom of the pin to the shear line (70px from bottom) + const distanceToShearLine = 70 - keyPinHeight; + + // Store pin data + const pin = { + index: i, + binding: bindingOrder.indexOf(i), + keyPinHeight: keyPinHeight, + distanceToShearLine: distanceToShearLine, + currentHeight: 0, // How high the pin is currently lifted (0-1 scale) + isSet: false, + resistance: Math.random() * 0.02 + 0.01, + elements: { + container: pinElement, + assembly: pinAssembly, + keyPin: keyPin, + driverPin: driverPin, + spring: spring + } + }; + + this.pins.push(pin); + + // Fix: Use an arrow function to preserve 'this' context + // and define the handler inline instead of referencing this.handlePinMouseDown + const self = this; // Store reference to 'this' + this.addEventListenerWithCleanup(pinElement, 'mousedown', function(e) { + // Skip if game is not active or pin is already set + if (!self.gameState.isActive || pin.isSet) return; + + // Only proceed if tension is applied + if (!self.lockState.tensionApplied) { + self.updateFeedback("Apply tension first by toggling the wrench"); + return; + } + + // Play a sound effect when interacting with pins + if (typeof self.playSound === 'function') { + self.playSound('pin_click'); + } + + // Start lifting the pin + self.lockState.currentPin = pin; + self.gameState.mouseDown = true; + self.liftPin(); + + // Add mouse up listener to document + const mouseUpHandler = function() { + self.gameState.mouseDown = false; + self.checkPinSet(self.lockState.currentPin); + self.lockState.currentPin = null; + document.removeEventListener('mouseup', mouseUpHandler); + }; + + document.addEventListener('mouseup', mouseUpHandler); + + // Prevent text selection + e.preventDefault(); + }); + } + } + + // Check if a pin should be set or dropped + checkPinSet(pin) { + if (!this.lockState.tensionApplied || !this.shouldPinBind(pin)) { + // Define dropPin function inline since it's not being found + this.animatePinDrop(pin); + return; + } + + // Calculate current pin height in pixels + const currentLiftInPixels = pin.currentHeight * pin.distanceToShearLine; + + // Check if the top of the key pin (or bottom of driver pin) is exactly at the shear line + // Allow a small tolerance of 2 pixels + const tolerance = 2; + const isAtShearLine = Math.abs(currentLiftInPixels - pin.distanceToShearLine) <= tolerance; + + if (isAtShearLine) { + // Pin set successfully! + pin.isSet = true; + this.lockState.pinsSet++; + + // Play a satisfying click sound when pin sets + if (typeof this.playSound === 'function') { + this.playSound('pin_set'); + } + + // First reset the assembly position + pin.elements.assembly.style.transform = 'none'; + + // Calculate exact position for the pin junction to be at the shear line + const exactLift = pin.distanceToShearLine; + pin.elements.assembly.style.transform = `translateY(-${exactLift}px)`; + + // Mark the pin as set + pin.elements.container.classList.add('set'); + + // Change color of the driver pin to green + pin.elements.driverPin.style.backgroundColor = '#22aa22'; + + this.updateFeedback(`Pin set at the shear line! (${this.lockState.pinsSet}/${this.pinCount})`); + + // Move the tension wrench further right based on progress + const progressPercentage = 25 + (this.lockState.pinsSet / this.pinCount * 75); + this.updateTensionPosition(progressPercentage); + + // Update progress + this.updateProgress(this.lockState.pinsSet, this.pinCount); + + // Check if all pins are set + if (this.lockState.pinsSet === this.pinCount) { + this.lockPickingSuccess(); + return; + } + + // Update which pin is binding next + this.updateBindingPins(); + } else { + // Pin not at the correct height, drops back down + this.animatePinDrop(pin); + + if (currentLiftInPixels > pin.distanceToShearLine) { + this.updateFeedback("Pin was pushed too far past the shear line"); + } else { + this.updateFeedback("Pin wasn't lifted high enough to reach the shear line"); + } + } + } + + // Define the animatePinDrop method to replace the missing dropPin method + animatePinDrop(pin) { + // Don't drop pins that are already set + if (pin.isSet) return; + + // Calculate drop speed based on how high the pin is + const dropSpeed = 0.05 + (pin.currentHeight * 0.1); + + const dropInterval = setInterval(() => { + pin.currentHeight -= dropSpeed; + + if (pin.currentHeight <= 0) { + pin.currentHeight = 0; + clearInterval(dropInterval); + } + + this.updatePinVisual(pin); + }, 10); + } + + // Update pin visual based on current height + updatePinVisual(pin) { + // Skip visualization update if the pin is set + if (pin.isSet) return; + + // Calculate the lift in pixels based on the current progress (0-1) times the distance to the shear line + const translateY = pin.currentHeight * pin.distanceToShearLine * -1; // Negative because we're moving up + + // Move the entire pin assembly up + pin.elements.assembly.style.transform = `translateY(${translateY}px)`; + } + + // Pin-lifting logic with realistic physics + liftPin() { + if (!this.lockState.currentPin || !this.gameState.isActive || + !this.lockState.tensionApplied || !this.gameState.mouseDown) { + return; + } + + const pin = this.lockState.currentPin; + + // Add realistic resistance based on binding state + let liftAmount = 0; + + // Only binding pins can be lifted effectively + if (!this.shouldPinBind(pin)) { + // Non-binding pins can be lifted, but with resistance and limited height + liftAmount = 0.01; + if (pin.currentHeight > 0.3) { + liftAmount = 0.005; // Increased resistance at higher positions + } + } else { + // Binding pins lift more smoothly but still have some resistance + liftAmount = 0.03 - (pin.resistance * pin.currentHeight); + + // Add slight random variation to simulate realistic feel + liftAmount += (Math.random() * 0.01 - 0.005); + } + + // Update pin height + pin.currentHeight += liftAmount; + + // Cap at maximum height + if (pin.currentHeight > 1.2) { // Allow overshooting the shear line a bit + pin.currentHeight = 1.2; + } + + // Update visual + this.updatePinVisual(pin); + + // Add subtle feedback when pin is near the shear line + const currentLiftInPixels = pin.currentHeight * pin.distanceToShearLine; + const distanceToShearLine = Math.abs(currentLiftInPixels - pin.distanceToShearLine); + + if (distanceToShearLine < 5) { + // Pin is close to the shear line + pin.elements.container.style.boxShadow = "0 0 5px #ffffff"; + } else { + pin.elements.container.style.boxShadow = ""; + } + + // Continue lifting while mouse is down + if (this.gameState.mouseDown) { + requestAnimationFrame(() => this.liftPin()); + } + } + + // Check if a pin should bind based on binding order + 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; } - try { - // Find the index of the sprite in the inventory - const index = inventory.items.indexOf(sprite); + // Update feedback text + updateFeedback(message) { + this.feedback.textContent = message; + } + + // Handle successful lockpicking + lockPickingSuccess() { + // Disable game interaction + this.gameState.isActive = false; - if (index === -1) { - return false; // Item not found in inventory + // Update UI + this.updateFeedback("Lock picked successfully!"); + + // Unlock the object in the game + if (this.lockable) { + // Set locked to false + this.lockable.locked = false; + + // If it's a scenarioData object, also update that property + if (this.lockable.scenarioData) { + this.lockable.scenarioData.locked = false; + } + + // Log successful unlock + if (typeof debugLog === 'function') { + debugLog('LOCKPICK UNLOCK', { + object: this.lockable, + success: true + }, 1); + } } - // Remove from container - inventory.container.remove(sprite); + // Show success message + const successHTML = ` +
Lock picked successfully!
+
All pins set at the shear line
+
+ Difficulty: ${this.difficulty.charAt(0).toUpperCase() + this.difficulty.slice(1)}
+ Pins: ${this.pinCount} +
+ `; - // Remove from items array - inventory.items.splice(index, 1); + // Use the framework's success message system + this.showSuccess(successHTML, true, 2000); - // Destroy the sprite - sprite.destroy(); + // Store lockable for the result + this.gameResult = { lockable: this.lockable }; + } + + lockPickingFailure() { + // Show failure message + const failureHTML = ` +
Failed to pick the lock
+
Try again with more careful pin manipulation
+ `; - // Rearrange remaining items - rearrangeInventoryItems(); + // Use the framework's failure message system + this.showFailure(failureHTML, true, 2000); + } + + start() { + super.start(); + console.log("Lockpicking minigame started"); - // Log the removal - debugLog('INVENTORY ITEM REMOVED', { - name: sprite.name, - totalItems: inventory.items.length - }, 2); + // Initialize game state + this.gameState.isActive = true; + this.lockState.tensionApplied = false; + this.lockState.pinsSet = 0; - return true; - } catch (error) { - console.error('Error removing item from inventory:', error); - return false; + // Initialize progress + this.updateProgress(0, this.pinCount); + } + + complete(success) { + // Call parent complete with result + super.complete(success, this.gameResult); + } + + // Utility function to shuffle an array + 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; + } + + // Add new method for updating binding pins + updateBindingPins() { + if (!this.lockState.tensionApplied) { + // No binding if no tension + this.pins.forEach(pin => { + pin.elements.container.classList.remove('binding'); + }); + return; + } + + // Find the next unset pin in binding order + let bindingPinFound = false; + + for (let order = 0; order < this.pinCount; order++) { + const nextPin = this.pins.find(p => p.binding === order && !p.isSet); + if (nextPin) { + // Mark this pin as binding + this.pins.forEach(pin => { + pin.elements.container.classList.toggle('binding', pin.index === nextPin.index); + }); + bindingPinFound = true; + break; + } + } + + // If no binding pin was found (all pins set), remove binding class from all + if (!bindingPinFound) { + this.pins.forEach(pin => { + pin.elements.container.classList.remove('binding'); + }); + } } } - - // Rearrange inventory items after removal - function rearrangeInventoryItems() { - inventory.items.forEach((item, index) => { - item.x = index * 60 + 100; + + // Register the lockpicking minigame with the framework + MinigameFramework.registerScene('lockpicking', LockpickingMinigame); + + // Replacement for the startLockpickingMinigame function + function startLockpickingMinigame(lockable, scene, difficulty = 'medium', callback) { + // Initialize the framework if not already done + if (!MinigameFramework.mainGameScene) { + MinigameFramework.init(scene); + } + + // Start the lockpicking minigame + MinigameFramework.startMinigame('lockpicking', { + lockable: lockable, + difficulty: difficulty, + onComplete: (success, result) => { + if (success) { + debugLog('LOCKPICK SUCCESS', null, 1); + gameAlert(`Successfully picked the lock!`, 'success', 'Lockpicking', 4000); + callback(); + } else { + debugLog('LOCKPICK FAILED', null, 2); + gameAlert(`Failed to pick the lock.`, 'error', 'Lockpicking', 4000); + } + } }); } @@ -5676,34 +6049,7 @@ deviceContent += ``; deviceContent += `
MAC: ${device.mac}\n${device.details}
`; - // Add pairing button only if device is nearby and player has a Bluetooth spoofer - if (device.nearby) { - // Check if player has a Bluetooth spoofer in inventory - const spoofer = inventory.items.find(item => - item.scenarioData?.type === "bluetooth_spoofer" - ); - - if (spoofer) { - // Check if this device is already paired with (MAC address programmed to spoofer) - const isPaired = spoofer.scenarioData?.macPaired === device.mac; - const buttonClass = isPaired ? 'bluetooth-pair-button paired' : 'bluetooth-pair-button'; - const buttonText = isPaired ? 'MAC Address Paired' : 'Pair MAC Address'; - - deviceContent += ``; - - // If device is paired, add a hint about using it to unlock - if (isPaired) { - deviceContent += `
- You can now use this MAC address to unlock matching Bluetooth locks -
`; - - // If this is an inventory item, add an unlock button - if (device.inInventory) { - deviceContent += ``; - } - } - } - } + deviceContent += `
Last seen: ${formattedDate} ${formattedTime}
`; @@ -5711,11 +6057,6 @@ // Toggle expanded state when clicked deviceElement.addEventListener('click', (event) => { - // Don't toggle if clicking on the pair button - if (event.target.classList.contains('bluetooth-pair-button') || - event.target.closest('.bluetooth-pair-button')) { - return; - } deviceElement.classList.toggle('expanded'); @@ -5775,49 +6116,6 @@ updateBluetoothPanel(); }); }); - - // Set up global event delegation for the pairing buttons - document.addEventListener('click', function(event) { - if (event.target.classList.contains('bluetooth-pair-button')) { - const mac = event.target.dataset.mac; - console.log('Attempting to pair with device MAC:', mac); - - // Find the device in our list - const device = bluetoothDevices.find(device => device.mac === mac); - console.log('Found device:', device); - - if (device) { - attemptPairingWithDevice(mac); - } else { - gameAlert("Device not found in Bluetooth devices list.", 'error', 'Pairing Failed', 3000); - } - - event.stopPropagation(); // Prevent device expanding/collapsing when clicking the button - } - - // Handle unlock button clicks - if (event.target.classList.contains('bluetooth-unlock-button')) { - const mac = event.target.dataset.mac; - console.log('Attempting to unlock device MAC:', mac); - - // Find the inventory item with this MAC address - const item = inventory.items.find(item => - item.scenarioData?.mac === mac && - item.scenarioData?.lockType === "bluetooth" && - item.scenarioData?.locked - ); - console.log('Found inventory item:', item); - - if (item) { - unlockInventoryDeviceByMac(mac); - } else { - gameAlert("Device not found in inventory.", 'error', 'Unlock Failed', 3000); - } - - event.stopPropagation(); // Prevent device expanding/collapsing when clicking the button - } - }); - // Initialize Bluetooth count updateBluetoothCount(); } @@ -5843,323 +6141,6 @@ } console.log('Found inventory item to unlock:', item); - - // Attempt to spoof the device - const success = spoofBluetoothDevice(item); - - // If successful, remove the device from the bluetoothDevices array - if (success) { - // Find the device in the bluetoothDevices array - const deviceIndex = bluetoothDevices.findIndex(device => - device.mac.toLowerCase() === normalizedMac && device.inInventory - ); - - // Remove it if found - if (deviceIndex !== -1) { - console.log('Removing unlocked device from bluetoothDevices array'); - bluetoothDevices.splice(deviceIndex, 1); - - // Update the Bluetooth panel to reflect the changes - updateBluetoothPanel(); - } - } - } - - // Function to handle pairing attempts with Bluetooth devices - function attemptPairingWithDevice(mac) { - console.log('Attempting to pair with MAC:', mac); - console.log('All Bluetooth devices:', bluetoothDevices); - - // Find the device in our list (case-insensitive comparison) - const normalizedMac = mac.toLowerCase(); - const device = bluetoothDevices.find(device => device.mac.toLowerCase() === normalizedMac); - - if (!device) { - console.error('Device not found with MAC:', mac); - gameAlert("Device not found.", 'error', 'Pairing Failed', 3000); - return; - } - - console.log('Found device for pairing:', device); - - // Find spoofer in inventory - const spoofer = inventory.items.find(item => - item.scenarioData?.type === "bluetooth_spoofer" - ); - - if (!spoofer) { - gameAlert("You need a Bluetooth spoofer to pair with this device.", 'warning', 'Spoofer Required', 3000); - return; - } - - // Check if player is close enough to the device (using the proximity from real-time scanning) - // Skip proximity check for inventory items - if (!device.nearby && !device.inInventory) { - gameAlert("You need to be closer to the device to pair with it.", 'warning', 'Too Far', 3000); - return; - } - - // Check if this device is already paired with - if (spoofer.scenarioData?.macPaired && spoofer.scenarioData.macPaired.toLowerCase() === normalizedMac) { - gameAlert(`This device's MAC address (${mac}) is already programmed into your spoofer.`, 'info', 'Already Paired', 3000); - return; - } - - // Launch the MAC address pairing minigame - startBluetoothPairingMinigame(device); - } - - // Bluetooth MAC Address Pairing Minigame - function startBluetoothPairingMinigame(device) { - // Create minigame container - const minigameContainer = document.createElement('div'); - minigameContainer.style.cssText = ` - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 60%; - height: 60%; - background: rgba(0, 0, 0, 0.9); - border: 1px solid #444; - z-index: 1000; - padding: 20px; - border-radius: 5px; - display: flex; - flex-direction: column; - align-items: center; - `; - - // Add instructions - const instructions = document.createElement('div'); - instructions.innerHTML = ` -

MAC Address Spoofing

-

- Align the signal frequencies to match the target device's MAC address pattern.
- Drag the sliders to adjust each frequency band until all segments turn green.
- When all segments are aligned, the MAC address will be successfully paired. -

- `; - instructions.style.cssText = ` - position: absolute; - top: 10px; - left: 50%; - transform: translateX(-50%); - width: 90%; - `; - - // Create device info display - const deviceInfo = document.createElement('div'); - deviceInfo.innerHTML = ` -
Target Device: ${device.name}
-
MAC Address: ${device.mac}
- `; - deviceInfo.style.cssText = ` - margin-top: 70px; - text-align: center; - width: 100%; - `; - - // Create signal visualization - const signalVisualization = document.createElement('div'); - signalVisualization.style.cssText = ` - width: 90%; - height: 120px; - background: #111; - border: 1px solid #333; - margin: 20px 0; - position: relative; - overflow: hidden; - `; - - // Create frequency bands (6 for MAC address octets) - const frequencyBands = document.createElement('div'); - frequencyBands.style.cssText = ` - display: flex; - width: 90%; - justify-content: space-between; - margin-bottom: 30px; - `; - - // Create sliders for each frequency band - const sliders = []; - const targetValues = []; - const currentValues = []; - const segments = []; - - // Parse MAC address to generate target values - const macParts = device.mac.split(':'); - - for (let i = 0; i < 6; i++) { - // Convert hex to decimal for target value (0-100 range) - const hexValue = macParts[i] || '00'; - const decimalValue = parseInt(hexValue, 16); - const targetValue = Math.round((decimalValue / 255) * 100); - targetValues.push(targetValue); - - // Start with random values - const initialValue = Math.floor(Math.random() * 100); - currentValues.push(initialValue); - - // Create slider container - const sliderContainer = document.createElement('div'); - sliderContainer.style.cssText = ` - display: flex; - flex-direction: column; - align-items: center; - width: 40px; - `; - - // Create slider - const slider = document.createElement('input'); - slider.type = 'range'; - slider.min = 0; - slider.max = 100; - slider.value = initialValue; - slider.style.cssText = ` - width: 120px; - transform: rotate(-90deg); - margin: 50px 0; - `; - - // Create segment in visualization - const segment = document.createElement('div'); - segment.style.cssText = ` - position: absolute; - bottom: 0; - left: ${i * (100/6)}%; - width: ${100/6}%; - height: ${initialValue}%; - background: #cc5500; - transition: height 0.2s, background-color 0.3s; - `; - segments.push(segment); - signalVisualization.appendChild(segment); - - // Create label - const label = document.createElement('div'); - label.textContent = hexValue.toUpperCase(); - label.style.cssText = ` - color: #aaa; - font-size: 12px; - margin-top: 10px; - `; - - // Add event listener to slider - slider.addEventListener('input', (e) => { - const value = parseInt(e.target.value); - currentValues[i] = value; - segment.style.height = `${value}%`; - - // Check if this segment is aligned correctly - const isAligned = Math.abs(value - targetValues[i]) <= 5; - segment.style.backgroundColor = isAligned ? '#00cc00' : '#cc5500'; - - // Check if all segments are aligned - checkCompletion(); - }); - - sliders.push(slider); - sliderContainer.appendChild(slider); - sliderContainer.appendChild(label); - frequencyBands.appendChild(sliderContainer); - } - - // Create status message - const statusMessage = document.createElement('div'); - statusMessage.style.cssText = ` - color: #aaa; - font-size: 14px; - margin-top: 15px; - text-align: center; - `; - statusMessage.textContent = 'Align all frequency bands...'; - - // Create cancel button - const cancelButton = document.createElement('button'); - cancelButton.textContent = 'Cancel'; - cancelButton.style.cssText = ` - background-color: #555; - color: white; - border: none; - border-radius: 3px; - padding: 8px 15px; - margin-top: 20px; - cursor: pointer; - font-size: 14px; - `; - cancelButton.addEventListener('click', () => { - endMinigame(false); - }); - - // Add all elements to container - minigameContainer.appendChild(instructions); - minigameContainer.appendChild(deviceInfo); - minigameContainer.appendChild(signalVisualization); - minigameContainer.appendChild(frequencyBands); - minigameContainer.appendChild(statusMessage); - minigameContainer.appendChild(cancelButton); - - // Add container to document - document.body.appendChild(minigameContainer); - - // Function to check if all segments are aligned - function checkCompletion() { - const allAligned = currentValues.every((value, index) => - Math.abs(value - targetValues[index]) <= 5 - ); - - if (allAligned) { - statusMessage.textContent = 'MAC Address pattern matched!'; - statusMessage.style.color = '#00cc00'; - - // Add success animation - segments.forEach(segment => { - segment.style.backgroundColor = '#00cc00'; - }); - - // End minigame with success after a short delay - setTimeout(() => { - endMinigame(true); - }, 1500); - } - } - - // Function to end minigame - function endMinigame(success) { - // Remove minigame container - document.body.removeChild(minigameContainer); - - // Call callback with result - if (success) { - // Find spoofer in inventory - const spoofer = inventory.items.find(item => - item.scenarioData?.type === "bluetooth_spoofer" - ); - - if (spoofer) { - console.log('Programming spoofer with MAC address:', device.mac); - - // Program the spoofer with the target MAC address - spoofer.scenarioData.macPaired = device.mac; - - // Show success message - gameAlert(`Successfully programmed spoofer with MAC address: ${device.mac}`, 'success', 'Pairing Complete', 4000); - debugLog('BLUETOOTH SPOOFER PROGRAMMED', { - deviceName: device.name, - deviceMac: device.mac - }, 1); - - // Update the UI to show the pairing was successful - device.paired = true; - - // Update the Bluetooth panel to reflect the pairing - updateBluetoothPanel(); - } - } else { - gameAlert('Pairing canceled.', 'info', 'Pairing Canceled', 3000); - } - } } // Biometrics Panel System @@ -6179,12 +6160,9 @@ // Apply category filter if (activeCategory === 'fingerprint') { - // Only show non-spoofed fingerprints in the fingerprint category filteredSamples = filteredSamples.filter(sample => - sample.type === 'fingerprint' && !sample.isSpoofed + sample.type === 'fingerprint' ); - } else if (activeCategory === 'spoofed') { - filteredSamples = filteredSamples.filter(sample => sample.isSpoofed); } // The 'all' category shows everything by default @@ -6225,13 +6203,7 @@ let sampleContent = `
${sample.type.charAt(0).toUpperCase() + sample.type.slice(1)} - ${sample.owner}
-
`; - - if (sample.isSpoofed) { - sampleContent += `🔄`; - } - - sampleContent += `
`; +
`; // Add quality bar sampleContent += `
@@ -6249,27 +6221,6 @@ sampleElement.innerHTML = sampleContent; - // Add spoof button if not already spoofed - if (!sample.isSpoofed && hasItemInInventory('spoofing_kit')) { - const spoofButton = document.createElement('button'); - spoofButton.textContent = 'Create Spoof'; - spoofButton.style.cssText = ` - margin-top: 10px; - padding: 5px 10px; - background: #e74c3c; - border: none; - color: white; - border-radius: 3px; - cursor: pointer; - width: 100%; - `; - spoofButton.onclick = (event) => { - event.stopPropagation(); // Prevent toggling expand/collapse - createSpoofedSampleUI(sample); - }; - sampleElement.appendChild(spoofButton); - } - // Toggle expanded state when clicked sampleElement.addEventListener('click', () => { sampleElement.classList.toggle('expanded'); @@ -6279,51 +6230,6 @@ }); } - // Function to create spoofed sample with UI - function createSpoofedSampleUI(sample) { - // Find the sample element - const sampleElement = document.querySelector(`.biometric-sample[data-id="${sample.id}"]`); - if (!sampleElement) return; - - // Disable any existing spoof buttons - const existingButton = sampleElement.querySelector('button'); - if (existingButton) { - existingButton.disabled = true; - existingButton.textContent = 'Creating spoof...'; - } - - // Add progress bar - const progressBar = document.createElement('div'); - progressBar.className = 'biometric-quality-bar'; - - const progress = document.createElement('div'); - progress.className = 'biometric-quality-fill'; - progress.style.width = '0%'; - progress.style.background = '#e67e22'; - - progressBar.appendChild(progress); - sampleElement.appendChild(progressBar); - - // Animate progress - let currentProgress = 0; - const interval = setInterval(() => { - currentProgress += 2; - progress.style.width = `${currentProgress}%`; - }, SPOOFING_TIME / 50); - - // Create spoof after delay - setTimeout(() => { - clearInterval(interval); - const spoofedSample = createSpoofedSample(sample); - if (spoofedSample) { - gameState.biometricSamples.push(spoofedSample); - updateBiometricsPanel(); // Refresh UI - updateBiometricsCount(); // Update count - gameAlert("Successfully created spoofed sample.", 'success', 'Spoofing Complete', 3000); - } - }, SPOOFING_TIME); - } - // Update the biometrics count function updateBiometricsCount() { const biometricsCount = document.getElementById('biometrics-count'); @@ -6353,15 +6259,21 @@ function initializeBiometricsPanel() { // Set up biometrics toggle button const biometricsToggle = document.getElementById('biometrics-toggle'); - biometricsToggle.addEventListener('click', toggleBiometricsPanel); + if (biometricsToggle) { + biometricsToggle.addEventListener('click', toggleBiometricsPanel); + } // Set up biometrics close button const biometricsClose = document.getElementById('biometrics-close'); - biometricsClose.addEventListener('click', toggleBiometricsPanel); + if (biometricsClose) { + biometricsClose.addEventListener('click', toggleBiometricsPanel); + } // Set up search functionality const biometricsSearch = document.getElementById('biometrics-search'); - biometricsSearch.addEventListener('input', updateBiometricsPanel); + if (biometricsSearch) { + biometricsSearch.addEventListener('input', updateBiometricsPanel); + } // Set up category filters const categories = document.querySelectorAll('.biometrics-category'); @@ -6398,24 +6310,6 @@ const biometricsPanel = document.getElementById('biometrics-panel'); biometricsPanel.style.display = 'none'; } - - // Override createSpoofedSample to add timestamp - const originalCreateSpoofedSample = createSpoofedSample; - createSpoofedSample = function(sample) { - const spoofedSample = originalCreateSpoofedSample(sample); - if (spoofedSample) { - spoofedSample.timestamp = Date.now(); - } - return spoofedSample; - }; - - // We don't need this duplicate initialization since we already added it to the existing DOMContentLoaded handler - // document.addEventListener('DOMContentLoaded', function() { - // // Existing initialization... - // - // // Initialize biometrics panel - // initializeBiometricsPanel(); - // }); // Function to drop an inventory item function dropInventoryItem(item) { @@ -6549,6 +6443,724 @@ popup.style.display = 'flex'; } + // Add this function to handle biometric matching near the other utility functions + function performBiometricMatch(lockable, ownerFingerprint, qualityThreshold, type) { + // Get available biometric samples - assuming they're stored in a global variable + // This may need to be adjusted based on how your biometric samples are actually stored + const availableSamples = biometricSamples || []; + + if (!availableSamples || availableSamples.length === 0) { + $("#biometric-status").text("No fingerprint samples available."); + gameAlert("No fingerprint samples found in database.", 'error', 'Scan Failed', 3000); + return; + } + + // Find if we have the owner's fingerprint + const matchingSample = availableSamples.find(sample => sample.id === ownerFingerprint); + + if (!matchingSample) { + // No matching sample found + $("#biometric-status").text("No match found. Access denied."); + gameAlert("Biometric authentication failed.", 'error', 'Access Denied', 3000); + + setTimeout(function() { + $("#biometrics-panel").hide(); + }, 2000); + return; + } + + // Check quality against threshold + const sampleQuality = matchingSample.quality || 0; + + if (sampleQuality >= qualityThreshold) { + // Successful match with sufficient quality + $("#biometric-status").text("Match found! Unlocking..."); + + setTimeout(function() { + // Unlock the target + unlockTarget(lockable, type, lockable.layer); + $("#biometrics-panel").hide(); + + // Play unlock sound if available + if (typeof playSound === 'function') { + playSound("unlock"); + } + + gameAlert(`Biometric match confirmed. ${type.charAt(0).toUpperCase() + type.slice(1)} unlocked.`, 'success', 'Access Granted', 4000); + debugLog('BIOMETRIC AUTHENTICATION SUCCESS', null, 1); + }, 1500); + } else { + // Match found but quality insufficient + $("#biometric-status").text("Sample quality insufficient. Access denied."); + gameAlert(`Biometric sample quality too low (${sampleQuality}/${qualityThreshold} required).`, 'error', 'Low Quality Sample', 3000); + debugLog('BIOMETRIC AUTHENTICATION FAILED - LOW QUALITY', null, 2); + + setTimeout(function() { + $("#biometrics-panel").hide(); + }, 2000); + } + } + + // Function to find a matching Bluetooth device in the current room + function findMatchingBluetoothDevice(device) { + if (!device.scenarioData?.mac || !currentRoom) return null; + + const targetMac = device.scenarioData.mac; + const roomObjects = Object.values(rooms[currentRoom].objects || {}); + + // Find any object in the current room with a matching MAC address + return roomObjects.find(obj => + obj.scenarioData?.mac && + obj.scenarioData.mac === targetMac && + obj.name !== device.name // Ensure it's not the same device + ); + } + + // Dusting Minigame Scene implementation + class DustingMinigame extends MinigameFramework.MinigameScene { + constructor(container, params) { + super(container, params); + + this.item = params.item; + + // Game state variables - using framework's gameState as base + this.difficultySettings = { + easy: { + requiredCoverage: 0.3, // 30% of prints + maxOverDusted: 50, // Increased due to more cells + fingerprints: 60, // Increased proportionally + pattern: 'simple' + }, + medium: { + requiredCoverage: 0.4, // 40% of prints + maxOverDusted: 40, // Increased due to more cells + fingerprints: 75, // Increased proportionally + pattern: 'medium' + }, + hard: { + requiredCoverage: 0.5, // 50% of prints + maxOverDusted: 25, // Increased due to more cells + fingerprints: 90, // Increased proportionally + pattern: 'complex' + } + }; + + this.currentDifficulty = this.item.scenarioData.fingerprintDifficulty; + this.gridSize = 30; + this.fingerprintCells = new Set(); + this.revealedPrints = 0; + this.overDusted = 0; + this.lastDustTime = {}; + + // Tools configuration + this.tools = [ + { name: 'Fine', size: 1, color: '#3498db', radius: 0 }, // Only affects current cell + { name: 'Medium', size: 2, color: '#2ecc71', radius: 1 }, // Affects current cell and adjacent + { name: 'Wide', size: 3, color: '#e67e22', radius: 2 } // Affects current cell and 2 cells around + ]; + this.currentTool = this.tools[1]; // Start with medium brush + } + + init() { + // Call parent init to set up common components + super.init(); + + console.log("Dusting minigame initializing"); + + // Set container dimensions + this.container.style.width = '75%'; + this.container.style.height = '75%'; + this.container.style.padding = '20px'; + + // Set up header content + this.headerElement.innerHTML = ` +

Fingerprint Dusting

+

Drag to dust the surface and reveal fingerprints. Avoid over-dusting!

+ `; + + // Configure game container + this.gameContainer.style.cssText = ` + width: 80%; + height: 80%; + max-width: 600px; + max-height: 600px; + display: grid; + grid-template-columns: repeat(30, 1fr); + grid-template-rows: repeat(30, 1fr); + gap: 1px; + background: #1a1a1a; + padding: 5px; + margin: 70px auto 20px auto; + border-radius: 5px; + box-shadow: 0 0 15px rgba(0, 0, 0, 0.5) inset; + position: relative; + overflow: hidden; + cursor: crosshair; + `; + + // Add background texture/pattern for a more realistic surface + const gridBackground = document.createElement('div'); + gridBackground.style.cssText = ` + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 0.3; + pointer-events: none; + z-index: 0; + `; + + // Create the grid pattern using encoded SVG + const svgGrid = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' fill='%23111'/%3E%3Cpath d='M0 50h100M50 0v100' stroke='%23222' stroke-width='0.5'/%3E%3Cpath d='M25 0v100M75 0v100M0 25h100M0 75h100' stroke='%23191919' stroke-width='0.3'/%3E%3C/svg%3E`; + + gridBackground.style.backgroundImage = `url('${svgGrid}')`; + this.gameContainer.appendChild(gridBackground); + + // Add tool selection + const toolsContainer = document.createElement('div'); + toolsContainer.style.cssText = ` + position: absolute; + bottom: 15px; + left: 15px; + display: flex; + gap: 10px; + z-index: 10; + flex-wrap: wrap; + max-width: 30%; + `; + + this.tools.forEach(tool => { + const toolButton = document.createElement('button'); + toolButton.className = `minigame-tool-button ${tool.name === this.currentTool.name ? 'active' : ''}`; + toolButton.textContent = tool.name; + toolButton.style.backgroundColor = tool.color; + + toolButton.addEventListener('click', () => { + document.querySelectorAll('.minigame-tool-button').forEach(btn => { + btn.classList.remove('active'); + }); + toolButton.classList.add('active'); + this.currentTool = tool; + }); + + toolsContainer.appendChild(toolButton); + }); + this.container.appendChild(toolsContainer); + + // Create particle container for dust effects + this.particleContainer = document.createElement('div'); + this.particleContainer.style.cssText = ` + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 5; + overflow: hidden; + `; + this.container.appendChild(this.particleContainer); + + // Generate fingerprint pattern and set up cells + this.fingerprintCells = this.generateFingerprint(this.currentDifficulty); + this.setupGrid(); + + // Total prints and required prints calculations + this.totalPrints = this.fingerprintCells.size; + this.requiredPrints = Math.ceil(this.totalPrints * this.difficultySettings[this.currentDifficulty].requiredCoverage); + + // Check initial progress + this.checkProgress(); + } + + // Set up the grid of cells + setupGrid() { + // Clear any existing cells + while (this.gameContainer.firstChild) { + this.gameContainer.removeChild(this.gameContainer.firstChild); + } + + // Create grid cells + for (let y = 0; y < this.gridSize; y++) { + for (let x = 0; x < this.gridSize; x++) { + const cell = document.createElement('div'); + cell.style.cssText = ` + width: 100%; + height: 100%; + background: black; + position: relative; + transition: background-color 0.1s; + cursor: pointer; + `; + cell.dataset.x = x; + cell.dataset.y = y; + cell.dataset.dustLevel = '0'; + cell.dataset.hasFingerprint = this.fingerprintCells.has(`${x},${y}`) ? 'true' : 'false'; + + this.gameContainer.appendChild(cell); + } + } + } + + // Override the framework's mouse event handlers + handleMouseMove(e) { + if (!this.gameState.isDragging) return; + + // Get the cell element under the cursor + const cell = document.elementFromPoint(e.clientX, e.clientY); + if (!cell || !cell.dataset || cell.dataset.dustLevel === undefined) return; + + // Get current cell coordinates + const centerX = parseInt(cell.dataset.x); + const centerY = parseInt(cell.dataset.y); + + // Get a list of cells to dust based on the brush radius + const cellsToDust = []; + const radius = this.currentTool.radius; + + // Add the current cell and cells within radius + for (let y = centerY - radius; y <= centerY + radius; y++) { + for (let x = centerX - radius; x <= centerX + radius; x++) { + // Skip cells outside the grid + if (x < 0 || x >= this.gridSize || y < 0 || y >= this.gridSize) continue; + + // For medium brush, use a diamond pattern (taxicab distance) + if (this.currentTool.size === 2) { + // Manhattan distance: |x1-x2| + |y1-y2| + const distance = Math.abs(x - centerX) + Math.abs(y - centerY); + if (distance > radius) continue; // Skip if too far away + } + // For wide brush, use a circle pattern (Euclidean distance) + else if (this.currentTool.size === 3) { + // Euclidean distance: √[(x1-x2)² + (y1-y2)²] + const distance = Math.sqrt(Math.pow(x - centerX, 2) + Math.pow(y - centerY, 2)); + if (distance > radius) continue; // Skip if too far away + } + + // Find this cell in the DOM + const targetCell = this.gameContainer.querySelector(`[data-x="${x}"][data-y="${y}"]`); + if (targetCell) { + cellsToDust.push(targetCell); + } + } + } + + // Get cell position for particles (center cell) + const cellRect = cell.getBoundingClientRect(); + const particleContainerRect = this.particleContainer.getBoundingClientRect(); + const cellCenterX = (cellRect.left + cellRect.width / 2) - particleContainerRect.left; + const cellCenterY = (cellRect.top + cellRect.height / 2) - particleContainerRect.top; + + // Process all cells to dust + cellsToDust.forEach(targetCell => { + const cellId = `${targetCell.dataset.x},${targetCell.dataset.y}`; + const currentTime = Date.now(); + const dustLevel = parseInt(targetCell.dataset.dustLevel); + + // Tool intensity affects dusting rate and particle effects + const toolIntensity = this.currentTool.size / 3; // 0.33 to 1 + + // Only allow dusting every 50-150ms for each cell (based on tool size) + const cooldown = 150 - (toolIntensity * 100); // 50ms for wide brush, 150ms for fine + + if (!this.lastDustTime[cellId] || currentTime - this.lastDustTime[cellId] > cooldown) { + if (dustLevel < 3) { + // Increment dust level with a probability based on tool intensity + const dustProbability = toolIntensity * 0.5 + 0.1; // 0.1-0.6 chance based on tool + + if (dustLevel < 1 || Math.random() < dustProbability) { + targetCell.dataset.dustLevel = (dustLevel + 1).toString(); + this.updateCellColor(targetCell); + + // Create dust particles for the current cell or at a position calculated for surrounding cells + if (targetCell === cell) { + // Center cell - use the already calculated position + const hasFingerprint = targetCell.dataset.hasFingerprint === 'true'; + let particleColor = dustLevel === 1 ? '#666' : (hasFingerprint ? '#1aff1a' : '#aaa'); + this.createDustParticles(cellCenterX, cellCenterY, toolIntensity, particleColor); + } else { + // For surrounding cells, calculate their relative position from the center cell + const targetCellRect = targetCell.getBoundingClientRect(); + const targetCellX = (targetCellRect.left + targetCellRect.width / 2) - particleContainerRect.left; + const targetCellY = (targetCellRect.top + targetCellRect.height / 2) - particleContainerRect.top; + + const hasFingerprint = targetCell.dataset.hasFingerprint === 'true'; + let particleColor = dustLevel === 1 ? '#666' : (hasFingerprint ? '#1aff1a' : '#aaa'); + + // Create fewer particles for surrounding cells + const reducedIntensity = toolIntensity * 0.6; + this.createDustParticles(targetCellX, targetCellY, reducedIntensity, particleColor); + } + } + this.lastDustTime[cellId] = currentTime; + } + } + }); + + // Update progress after dusting + this.checkProgress(); + } + + // Use the framework's mouseDown handler directly + handleMouseDown(e) { + // Just start dusting immediately + this.handleMouseMove(e); + } + + createDustParticles(x, y, intensity, color) { + const numParticles = Math.floor(5 + intensity * 5); // 5-10 particles based on intensity + + for (let i = 0; i < numParticles; i++) { + const particle = document.createElement('div'); + const size = Math.random() * 3 + 1; // 1-4px + const angle = Math.random() * Math.PI * 2; + const distance = Math.random() * 20 * intensity; + const duration = Math.random() * 1000 + 500; // 500-1500ms + + particle.style.cssText = ` + position: absolute; + width: ${size}px; + height: ${size}px; + background: ${color}; + border-radius: 50%; + opacity: ${Math.random() * 0.3 + 0.3}; + top: ${y}px; + left: ${x}px; + transform: translate(-50%, -50%); + pointer-events: none; + z-index: 6; + `; + + this.particleContainer.appendChild(particle); + + // Animate the particle + const animation = particle.animate([ + { + transform: 'translate(-50%, -50%)', + opacity: particle.style.opacity + }, + { + transform: `translate( + calc(-50% + ${Math.cos(angle) * distance}px), + calc(-50% + ${Math.sin(angle) * distance}px) + )`, + opacity: 0 + } + ], { + duration: duration, + easing: 'cubic-bezier(0.25, 1, 0.5, 1)' + }); + + animation.onfinish = () => { + particle.remove(); + }; + } + } + + updateCellColor(cell) { + const dustLevel = parseInt(cell.dataset.dustLevel); + const hasFingerprint = cell.dataset.hasFingerprint === 'true'; + + if (dustLevel === 0) { + cell.style.background = 'black'; + cell.style.boxShadow = 'none'; + } + else if (dustLevel === 1) { + cell.style.background = '#444'; + cell.style.boxShadow = 'inset 0 0 3px rgba(255,255,255,0.2)'; + } + else if (dustLevel === 2) { + if (hasFingerprint) { + cell.style.background = '#0f0'; + cell.style.boxShadow = 'inset 0 0 5px rgba(0,255,0,0.5), 0 0 5px rgba(0,255,0,0.3)'; + } else { + cell.style.background = '#888'; + cell.style.boxShadow = 'inset 0 0 4px rgba(255,255,255,0.3)'; + } + } + else { + cell.style.background = '#ccc'; + cell.style.boxShadow = 'inset 0 0 5px rgba(255,255,255,0.5)'; + } + } + + checkProgress() { + this.revealedPrints = 0; + this.overDusted = 0; + + this.gameContainer.childNodes.forEach(cell => { + if (cell.dataset) { // Check if it's a cell element + const dustLevel = parseInt(cell.dataset.dustLevel || '0'); + const hasFingerprint = cell.dataset.hasFingerprint === 'true'; + + if (hasFingerprint && dustLevel === 2) this.revealedPrints++; + if (dustLevel === 3) this.overDusted++; + } + }); + + // Update progress display + this.progressContainer.innerHTML = ` +
+ Found: ${this.revealedPrints}/${this.requiredPrints} required prints + + Over-dusted: ${this.overDusted}/${this.difficultySettings[this.currentDifficulty].maxOverDusted} max + +
+
+
+
+ `; + + // Check fail condition first + if (this.overDusted >= this.difficultySettings[this.currentDifficulty].maxOverDusted) { + this.showFinalFailure("Too many over-dusted areas!"); + return; + } + + // Check win condition + if (this.revealedPrints >= this.requiredPrints) { + this.showFinalSuccess(); + } + } + + showFinalSuccess() { + // Calculate quality based on dusting precision + const dustPenalty = this.overDusted / this.difficultySettings[this.currentDifficulty].maxOverDusted; // 0-1 + const coverageBonus = this.revealedPrints / this.totalPrints; // 0-1 + + // Higher quality for more coverage and less over-dusting + const quality = 0.7 + (coverageBonus * 0.25) - (dustPenalty * 0.15); + const qualityPercentage = Math.round(quality * 100); + const qualityRating = qualityPercentage >= 95 ? 'Perfect' : + qualityPercentage >= 85 ? 'Excellent' : + qualityPercentage >= 75 ? 'Good' : 'Acceptable'; + + // Build success message with detailed stats + const successHTML = ` +
Fingerprint successfully collected!
+
Quality: ${qualityRating} (${qualityPercentage}%)
+
+ Prints revealed: ${this.revealedPrints}/${this.totalPrints}
+ Over-dusted areas: ${this.overDusted}
+ Difficulty: ${this.currentDifficulty.charAt(0).toUpperCase() + this.currentDifficulty.slice(1)} +
+ `; + + // Use the framework's success message system + this.showSuccess(successHTML, true, 2000); + + // Disable further interaction + this.gameContainer.style.pointerEvents = 'none'; + + // Store result for onComplete callback + this.gameResult = { + quality: quality, + rating: qualityRating + }; + } + + showFinalFailure(reason) { + // Build failure message + const failureHTML = ` +
${reason}
+
Try again with more careful dusting.
+ `; + + // Use the framework's failure message system + this.showFailure(failureHTML, true, 2000); + + // Disable further interaction + this.gameContainer.style.pointerEvents = 'none'; + } + + start() { + super.start(); + console.log("Dusting minigame started"); + + // Disable game movement in the main scene + if (this.params.scene) { + this.params.scene.input.mouse.enabled = false; + } + } + + complete(success) { + // Call parent complete with result + super.complete(success, this.gameResult); + } + + generateFingerprint(difficulty) { + // Existing fingerprint generation logic remains the same + const pattern = this.difficultySettings[difficulty].pattern; + const numPrints = this.difficultySettings[difficulty].fingerprints; + const newFingerprintCells = new Set(); + const centerX = Math.floor(this.gridSize / 2); + const centerY = Math.floor(this.gridSize / 2); + + if (pattern === 'simple') { + // Simple oval-like pattern + for (let i = 0; i < numPrints; i++) { + const angle = (i / numPrints) * Math.PI * 2; + const distance = 5 + Math.random() * 3; + const x = Math.floor(centerX + Math.cos(angle) * distance); + const y = Math.floor(centerY + Math.sin(angle) * distance); + + if (x >= 0 && x < this.gridSize && y >= 0 && y < this.gridSize) { + newFingerprintCells.add(`${x},${y}`); + + // Add a few adjacent cells to make it less sparse + for (let j = 0; j < 2; j++) { + const nx = x + Math.floor(Math.random() * 3) - 1; + const ny = y + Math.floor(Math.random() * 3) - 1; + if (nx >= 0 && nx < this.gridSize && ny >= 0 && ny < this.gridSize) { + newFingerprintCells.add(`${nx},${ny}`); + } + } + } + } + } else if (pattern === 'medium') { + // Medium complexity - spiral pattern with variations + for (let i = 0; i < numPrints; i++) { + const t = i / numPrints * 5; + const distance = 2 + t * 0.8; + const noise = Math.random() * 2 - 1; + const x = Math.floor(centerX + Math.cos(t * Math.PI * 2) * (distance + noise)); + const y = Math.floor(centerY + Math.sin(t * Math.PI * 2) * (distance + noise)); + + if (x >= 0 && x < this.gridSize && y >= 0 && y < this.gridSize) { + newFingerprintCells.add(`${x},${y}`); + } + } + + // Add whorls and arches + for (let i = 0; i < 20; i++) { + const angle = (i / 20) * Math.PI * 2; + const distance = 7; + const x = Math.floor(centerX + Math.cos(angle) * distance); + const y = Math.floor(centerY + Math.sin(angle) * distance); + + if (x >= 0 && x < this.gridSize && y >= 0 && y < this.gridSize) { + newFingerprintCells.add(`${x},${y}`); + } + } + } else { + // Complex pattern - detailed whorls and ridge patterns + for (let i = 0; i < numPrints; i++) { + // Main loop - create a complex whorl pattern + const t = i / numPrints * 8; + const distance = 2 + t * 0.6; + const noise = Math.sin(t * 5) * 1.5; + const x = Math.floor(centerX + Math.cos(t * Math.PI * 2) * (distance + noise)); + const y = Math.floor(centerY + Math.sin(t * Math.PI * 2) * (distance + noise)); + + if (x >= 0 && x < this.gridSize && y >= 0 && y < this.gridSize) { + newFingerprintCells.add(`${x},${y}`); + } + + // Add bifurcations and ridge endings + if (i % 5 === 0) { + const bifAngle = t * Math.PI * 2 + Math.PI/4; + const bx = Math.floor(x + Math.cos(bifAngle) * 1); + const by = Math.floor(y + Math.sin(bifAngle) * 1); + if (bx >= 0 && bx < this.gridSize && by >= 0 && by < this.gridSize) { + newFingerprintCells.add(`${bx},${by}`); + } + } + } + + // Add delta patterns + for (let d = 0; d < 3; d++) { + const deltaAngle = (d / 3) * Math.PI * 2; + const deltaX = Math.floor(centerX + Math.cos(deltaAngle) * 8); + const deltaY = Math.floor(centerY + Math.sin(deltaAngle) * 8); + + for (let r = 0; r < 5; r++) { + for (let a = 0; a < 3; a++) { + const rayAngle = deltaAngle + (a - 1) * Math.PI/4; + const rx = Math.floor(deltaX + Math.cos(rayAngle) * r); + const ry = Math.floor(deltaY + Math.sin(rayAngle) * r); + if (rx >= 0 && rx < this.gridSize && ry >= 0 && ry < this.gridSize) { + newFingerprintCells.add(`${rx},${ry}`); + } + } + } + } + } + + // Ensure we have at least the minimum number of cells + while (newFingerprintCells.size < numPrints) { + const x = centerX + Math.floor(Math.random() * 12 - 6); + const y = centerY + Math.floor(Math.random() * 12 - 6); + if (x >= 0 && x < this.gridSize && y >= 0 && y < this.gridSize) { + newFingerprintCells.add(`${x},${y}`); + } + } + + return newFingerprintCells; + } + + cleanup() { + super.cleanup(); + + // Re-enable game movement + if (this.params.scene) { + this.params.scene.input.mouse.enabled = true; + } + } + } + + // Register the dusting minigame with the framework + MinigameFramework.registerScene('dusting', DustingMinigame); + + // Replacement for the startDustingMinigame function + function startDustingMinigame(item) { + // Initialize the framework if not already done + if (!MinigameFramework.mainGameScene) { + MinigameFramework.init(item.scene); + } + + // Start the dusting minigame + MinigameFramework.startMinigame('dusting', { + item: item, + scene: item.scene, + onComplete: (success, result) => { + if (success) { + debugLog('DUSTING SUCCESS', result, 1); + + // Add fingerprint to gameState + if (!gameState.biometricSamples) { + gameState.biometricSamples = []; + } + + const sample = { + id: generateFingerprintData(item), + type: 'fingerprint', + owner: item.scenarioData.fingerprintOwner, + quality: result.quality, // Quality between 0.7 and ~1.0 + data: generateFingerprintData(item), + timestamp: Date.now() + }; + + gameState.biometricSamples.push(sample); + + // Mark item as collected + if (item.scenarioData) { + item.scenarioData.hasFingerprint = false; + } + + // Update the biometrics panel and count + updateBiometricsPanel(); + updateBiometricsCount(); + + // Show notification + gameAlert(`Collected ${sample.owner}'s fingerprint sample (${result.rating} quality)`, 'success', 'Sample Acquired', 3000); + } else { + debugLog('DUSTING FAILED', null, 2); + gameAlert(`Failed to collect the fingerprint sample.`, 'error', 'Dusting Failed', 3000); + } + } + }); + }