mirror of
https://github.com/cliffe/BreakEscape.git
synced 2026-02-20 13:50:46 +00:00
Implement Minigames for Bluetooth Scanner, Biometrics, and Lockpick Set: Transition Bluetooth and biometrics functionalities to dedicated minigames, enhancing user interaction and gameplay experience. Introduce new CSS styles for each minigame and update the main game logic to support these changes. Remove legacy systems for biometrics and Bluetooth management to streamline code and improve maintainability.
This commit is contained in:
BIN
assets/objects/fingerprint_small.png
Normal file
BIN
assets/objects/fingerprint_small.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 251 B |
@@ -492,7 +492,7 @@
|
||||
}],
|
||||
"opacity":1,
|
||||
"type":"objectgroup",
|
||||
"visible":false,
|
||||
"visible":true,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
@@ -677,8 +677,8 @@
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":13,
|
||||
"x":242.5,
|
||||
"y":207.5
|
||||
"x":242.869344413666,
|
||||
"y":217.102954755309
|
||||
},
|
||||
{
|
||||
"gid":227,
|
||||
@@ -701,8 +701,8 @@
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":10,
|
||||
"x":30.5,
|
||||
"y":163
|
||||
"x":111.386426592798,
|
||||
"y":165.58541089566
|
||||
},
|
||||
{
|
||||
"gid":236,
|
||||
@@ -764,10 +764,22 @@
|
||||
"width":14,
|
||||
"x":220,
|
||||
"y":67
|
||||
},
|
||||
{
|
||||
"gid":367,
|
||||
"height":30,
|
||||
"id":150,
|
||||
"name":"",
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":26,
|
||||
"x":34.2760849492151,
|
||||
"y":216.662049861496
|
||||
}],
|
||||
"opacity":1,
|
||||
"type":"objectgroup",
|
||||
"visible":false,
|
||||
"visible":true,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
@@ -783,7 +795,7 @@
|
||||
"y":0
|
||||
}],
|
||||
"nextlayerid":11,
|
||||
"nextobjectid":150,
|
||||
"nextobjectid":151,
|
||||
"orientation":"orthogonal",
|
||||
"renderorder":"right-down",
|
||||
"tiledversion":"1.11.2",
|
||||
@@ -2252,14 +2264,14 @@
|
||||
{
|
||||
"id":243,
|
||||
"image":"..\/objects\/key.png",
|
||||
"imageheight":27,
|
||||
"imagewidth":13
|
||||
"imageheight":21,
|
||||
"imagewidth":12
|
||||
},
|
||||
{
|
||||
"id":244,
|
||||
"image":"..\/objects\/lockpick.png",
|
||||
"imageheight":64,
|
||||
"imagewidth":64
|
||||
"imageheight":30,
|
||||
"imagewidth":26
|
||||
},
|
||||
{
|
||||
"id":245,
|
||||
|
||||
@@ -55,12 +55,12 @@
|
||||
{
|
||||
"data":[0, 101, 0, 0, 0, 0, 0, 0, 101, 0,
|
||||
0, 107, 0, 0, 0, 0, 0, 0, 107, 0,
|
||||
418, 0, 0, 0, 0, 0, 0, 0, 0, 418,
|
||||
420, 0, 0, 0, 0, 0, 0, 0, 0, 420,
|
||||
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, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
418, 0, 0, 0, 0, 0, 0, 0, 0, 418,
|
||||
420, 0, 0, 0, 0, 0, 0, 0, 0, 420,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
"height":10,
|
||||
@@ -138,7 +138,7 @@
|
||||
"name":"table_items",
|
||||
"objects":[
|
||||
{
|
||||
"gid":189,
|
||||
"gid":191,
|
||||
"height":14,
|
||||
"id":90,
|
||||
"name":"",
|
||||
@@ -150,7 +150,7 @@
|
||||
"y":144.666666666667
|
||||
},
|
||||
{
|
||||
"gid":199,
|
||||
"gid":201,
|
||||
"height":15,
|
||||
"id":105,
|
||||
"name":"",
|
||||
@@ -162,7 +162,7 @@
|
||||
"y":169
|
||||
},
|
||||
{
|
||||
"gid":336,
|
||||
"gid":338,
|
||||
"height":12,
|
||||
"id":55,
|
||||
"name":"",
|
||||
@@ -174,7 +174,7 @@
|
||||
"y":180.666666666667
|
||||
},
|
||||
{
|
||||
"gid":185,
|
||||
"gid":187,
|
||||
"height":18,
|
||||
"id":88,
|
||||
"name":"",
|
||||
@@ -186,7 +186,7 @@
|
||||
"y":136.333333333333
|
||||
},
|
||||
{
|
||||
"gid":189,
|
||||
"gid":191,
|
||||
"height":14,
|
||||
"id":101,
|
||||
"name":"",
|
||||
@@ -198,7 +198,7 @@
|
||||
"y":122
|
||||
},
|
||||
{
|
||||
"gid":187,
|
||||
"gid":189,
|
||||
"height":14,
|
||||
"id":45,
|
||||
"name":"",
|
||||
@@ -210,7 +210,7 @@
|
||||
"y":130.75
|
||||
},
|
||||
{
|
||||
"gid":198,
|
||||
"gid":200,
|
||||
"height":8,
|
||||
"id":104,
|
||||
"name":"",
|
||||
@@ -222,7 +222,7 @@
|
||||
"y":121
|
||||
},
|
||||
{
|
||||
"gid":196,
|
||||
"gid":198,
|
||||
"height":7,
|
||||
"id":103,
|
||||
"name":"",
|
||||
@@ -234,7 +234,7 @@
|
||||
"y":121
|
||||
},
|
||||
{
|
||||
"gid":191,
|
||||
"gid":193,
|
||||
"height":18,
|
||||
"id":102,
|
||||
"name":"",
|
||||
@@ -246,7 +246,7 @@
|
||||
"y":124
|
||||
},
|
||||
{
|
||||
"gid":189,
|
||||
"gid":191,
|
||||
"height":14,
|
||||
"id":100,
|
||||
"name":"",
|
||||
@@ -259,7 +259,7 @@
|
||||
},
|
||||
|
||||
{
|
||||
"gid":188,
|
||||
"gid":190,
|
||||
"height":11,
|
||||
"id":99,
|
||||
"name":"",
|
||||
@@ -271,7 +271,7 @@
|
||||
"y":171.5
|
||||
},
|
||||
{
|
||||
"gid":201,
|
||||
"gid":203,
|
||||
"height":15,
|
||||
"id":106,
|
||||
"name":"",
|
||||
@@ -283,7 +283,7 @@
|
||||
"y":119.5
|
||||
},
|
||||
{
|
||||
"gid":202,
|
||||
"gid":204,
|
||||
"height":20,
|
||||
"id":107,
|
||||
"name":"",
|
||||
@@ -295,7 +295,7 @@
|
||||
"y":170.630655586334
|
||||
},
|
||||
{
|
||||
"gid":203,
|
||||
"gid":205,
|
||||
"height":18,
|
||||
"id":108,
|
||||
"name":"",
|
||||
@@ -307,7 +307,7 @@
|
||||
"y":174
|
||||
},
|
||||
{
|
||||
"gid":203,
|
||||
"gid":205,
|
||||
"height":18,
|
||||
"id":109,
|
||||
"name":"",
|
||||
@@ -319,7 +319,7 @@
|
||||
"y":174
|
||||
},
|
||||
{
|
||||
"gid":205,
|
||||
"gid":207,
|
||||
"height":11,
|
||||
"id":110,
|
||||
"name":"",
|
||||
@@ -331,7 +331,7 @@
|
||||
"y":132.5
|
||||
},
|
||||
{
|
||||
"gid":217,
|
||||
"gid":219,
|
||||
"height":12,
|
||||
"id":111,
|
||||
"name":"",
|
||||
@@ -343,7 +343,7 @@
|
||||
"y":119
|
||||
},
|
||||
{
|
||||
"gid":221,
|
||||
"gid":223,
|
||||
"height":16,
|
||||
"id":112,
|
||||
"name":"",
|
||||
@@ -355,7 +355,7 @@
|
||||
"y":129
|
||||
},
|
||||
{
|
||||
"gid":225,
|
||||
"gid":227,
|
||||
"height":16,
|
||||
"id":113,
|
||||
"name":"",
|
||||
@@ -367,7 +367,7 @@
|
||||
"y":183.5
|
||||
},
|
||||
{
|
||||
"gid":334,
|
||||
"gid":336,
|
||||
"height":12,
|
||||
"id":131,
|
||||
"name":"",
|
||||
@@ -380,7 +380,7 @@
|
||||
},
|
||||
|
||||
{
|
||||
"gid":333,
|
||||
"gid":335,
|
||||
"height":18,
|
||||
"id":132,
|
||||
"name":"",
|
||||
@@ -392,7 +392,7 @@
|
||||
"y":121.5
|
||||
},
|
||||
{
|
||||
"gid":341,
|
||||
"gid":343,
|
||||
"height":18,
|
||||
"id":133,
|
||||
"name":"",
|
||||
@@ -404,7 +404,7 @@
|
||||
"y":180.5
|
||||
},
|
||||
{
|
||||
"gid":336,
|
||||
"gid":338,
|
||||
"height":12,
|
||||
"id":134,
|
||||
"name":"",
|
||||
@@ -416,7 +416,7 @@
|
||||
"y":133.5
|
||||
},
|
||||
{
|
||||
"gid":337,
|
||||
"gid":339,
|
||||
"height":14,
|
||||
"id":135,
|
||||
"name":"",
|
||||
@@ -428,7 +428,7 @@
|
||||
"y":132
|
||||
},
|
||||
{
|
||||
"gid":374,
|
||||
"gid":376,
|
||||
"height":22,
|
||||
"id":141,
|
||||
"name":"",
|
||||
@@ -440,7 +440,7 @@
|
||||
"y":132.5
|
||||
},
|
||||
{
|
||||
"gid":372,
|
||||
"gid":374,
|
||||
"height":30,
|
||||
"id":142,
|
||||
"name":"",
|
||||
@@ -452,7 +452,7 @@
|
||||
"y":181
|
||||
},
|
||||
{
|
||||
"gid":377,
|
||||
"gid":379,
|
||||
"height":21,
|
||||
"id":143,
|
||||
"name":"",
|
||||
@@ -475,7 +475,7 @@
|
||||
"name":"conditional_table_items",
|
||||
"objects":[
|
||||
{
|
||||
"gid":218,
|
||||
"gid":220,
|
||||
"height":11,
|
||||
"id":46,
|
||||
"name":"",
|
||||
@@ -487,7 +487,7 @@
|
||||
"y":135.75
|
||||
},
|
||||
{
|
||||
"gid":310,
|
||||
"gid":312,
|
||||
"height":16,
|
||||
"id":54,
|
||||
"name":"",
|
||||
@@ -500,7 +500,7 @@
|
||||
}],
|
||||
"opacity":1,
|
||||
"type":"objectgroup",
|
||||
"visible":false,
|
||||
"visible":true,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
@@ -510,7 +510,7 @@
|
||||
"name":"items",
|
||||
"objects":[
|
||||
{
|
||||
"gid":122,
|
||||
"gid":124,
|
||||
"height":16,
|
||||
"id":96,
|
||||
"name":"",
|
||||
@@ -522,7 +522,7 @@
|
||||
"y":152.5
|
||||
},
|
||||
{
|
||||
"gid":122,
|
||||
"gid":124,
|
||||
"height":16,
|
||||
"id":97,
|
||||
"name":"",
|
||||
@@ -534,7 +534,7 @@
|
||||
"y":203
|
||||
},
|
||||
{
|
||||
"gid":342,
|
||||
"gid":344,
|
||||
"height":52,
|
||||
"id":139,
|
||||
"name":"",
|
||||
@@ -546,7 +546,7 @@
|
||||
"y":65.2613111726685
|
||||
},
|
||||
{
|
||||
"gid":344,
|
||||
"gid":346,
|
||||
"height":54,
|
||||
"id":137,
|
||||
"name":"",
|
||||
@@ -558,7 +558,7 @@
|
||||
"y":68.5
|
||||
},
|
||||
{
|
||||
"gid":345,
|
||||
"gid":347,
|
||||
"height":50,
|
||||
"id":138,
|
||||
"name":"",
|
||||
@@ -570,7 +570,7 @@
|
||||
"y":66
|
||||
},
|
||||
{
|
||||
"gid":410,
|
||||
"gid":412,
|
||||
"height":32,
|
||||
"id":144,
|
||||
"name":"",
|
||||
@@ -582,7 +582,7 @@
|
||||
"y":216.923361034164
|
||||
},
|
||||
{
|
||||
"gid":409,
|
||||
"gid":411,
|
||||
"height":32,
|
||||
"id":145,
|
||||
"name":"",
|
||||
@@ -594,7 +594,7 @@
|
||||
"y":170.755309325946
|
||||
},
|
||||
{
|
||||
"gid":403,
|
||||
"gid":405,
|
||||
"height":32,
|
||||
"id":146,
|
||||
"name":"",
|
||||
@@ -606,7 +606,7 @@
|
||||
"y":172.971375807941
|
||||
},
|
||||
{
|
||||
"gid":400,
|
||||
"gid":402,
|
||||
"height":32,
|
||||
"id":148,
|
||||
"name":"",
|
||||
@@ -618,7 +618,7 @@
|
||||
"y":79.8965835641736
|
||||
},
|
||||
{
|
||||
"gid":398,
|
||||
"gid":400,
|
||||
"height":32,
|
||||
"id":149,
|
||||
"name":"",
|
||||
@@ -641,7 +641,7 @@
|
||||
"name":"conditional_items",
|
||||
"objects":[
|
||||
{
|
||||
"gid":131,
|
||||
"gid":133,
|
||||
"height":21,
|
||||
"id":40,
|
||||
"name":"",
|
||||
@@ -653,7 +653,7 @@
|
||||
"y":215.5
|
||||
},
|
||||
{
|
||||
"gid":308,
|
||||
"gid":310,
|
||||
"height":30,
|
||||
"id":62,
|
||||
"name":"",
|
||||
@@ -665,7 +665,7 @@
|
||||
"y":66
|
||||
},
|
||||
{
|
||||
"gid":307,
|
||||
"gid":309,
|
||||
"height":33,
|
||||
"id":63,
|
||||
"name":"",
|
||||
@@ -677,7 +677,7 @@
|
||||
"y":64.5
|
||||
},
|
||||
{
|
||||
"gid":364,
|
||||
"gid":366,
|
||||
"height":27,
|
||||
"id":75,
|
||||
"name":"",
|
||||
@@ -685,11 +685,11 @@
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":13,
|
||||
"x":242.5,
|
||||
"y":207.5
|
||||
"x":242.869344413666,
|
||||
"y":217.102954755309
|
||||
},
|
||||
{
|
||||
"gid":225,
|
||||
"gid":227,
|
||||
"height":16,
|
||||
"id":48,
|
||||
"name":"",
|
||||
@@ -701,7 +701,7 @@
|
||||
"y":51.5
|
||||
},
|
||||
{
|
||||
"gid":121,
|
||||
"gid":123,
|
||||
"height":24,
|
||||
"id":98,
|
||||
"name":"",
|
||||
@@ -709,11 +709,11 @@
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":10,
|
||||
"x":30.5,
|
||||
"y":163
|
||||
"x":111.386426592798,
|
||||
"y":165.58541089566
|
||||
},
|
||||
{
|
||||
"gid":234,
|
||||
"gid":236,
|
||||
"height":20,
|
||||
"id":115,
|
||||
"name":"",
|
||||
@@ -725,7 +725,7 @@
|
||||
"y":209.5
|
||||
},
|
||||
{
|
||||
"gid":236,
|
||||
"gid":238,
|
||||
"height":21,
|
||||
"id":116,
|
||||
"name":"",
|
||||
@@ -737,7 +737,7 @@
|
||||
"y":69
|
||||
},
|
||||
{
|
||||
"gid":240,
|
||||
"gid":242,
|
||||
"height":21,
|
||||
"id":117,
|
||||
"name":"",
|
||||
@@ -749,7 +749,7 @@
|
||||
"y":69
|
||||
},
|
||||
{
|
||||
"gid":242,
|
||||
"gid":244,
|
||||
"height":21,
|
||||
"id":118,
|
||||
"name":"",
|
||||
@@ -762,7 +762,7 @@
|
||||
},
|
||||
|
||||
{
|
||||
"gid":246,
|
||||
"gid":248,
|
||||
"height":24,
|
||||
"id":119,
|
||||
"name":"",
|
||||
@@ -772,10 +772,22 @@
|
||||
"width":14,
|
||||
"x":220,
|
||||
"y":67
|
||||
},
|
||||
{
|
||||
"gid":367,
|
||||
"height":30,
|
||||
"id":150,
|
||||
"name":"",
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":26,
|
||||
"x":34.2760849492151,
|
||||
"y":216.662049861496
|
||||
}],
|
||||
"opacity":1,
|
||||
"type":"objectgroup",
|
||||
"visible":false,
|
||||
"visible":true,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
@@ -791,7 +803,7 @@
|
||||
"y":0
|
||||
}],
|
||||
"nextlayerid":11,
|
||||
"nextobjectid":150,
|
||||
"nextobjectid":151,
|
||||
"orientation":"orthogonal",
|
||||
"renderorder":"right-down",
|
||||
"tiledversion":"1.11.2",
|
||||
@@ -820,7 +832,7 @@
|
||||
},
|
||||
{
|
||||
"columns":0,
|
||||
"firstgid":121,
|
||||
"firstgid":123,
|
||||
"grid":
|
||||
{
|
||||
"height":1,
|
||||
@@ -2202,14 +2214,14 @@
|
||||
{
|
||||
"id":243,
|
||||
"image":"..\/objects\/key.png",
|
||||
"imageheight":27,
|
||||
"imagewidth":13
|
||||
"imageheight":21,
|
||||
"imagewidth":12
|
||||
},
|
||||
{
|
||||
"id":244,
|
||||
"image":"..\/objects\/lockpick.png",
|
||||
"imageheight":64,
|
||||
"imagewidth":64
|
||||
"imageheight":30,
|
||||
"imagewidth":26
|
||||
},
|
||||
{
|
||||
"id":245,
|
||||
@@ -2531,15 +2543,15 @@
|
||||
"tilewidth":221
|
||||
},
|
||||
{
|
||||
"firstgid":418,
|
||||
"firstgid":420,
|
||||
"source":"..\/..\/..\/assets\/rooms\/door_side_sheet_32.tsx"
|
||||
},
|
||||
{
|
||||
"firstgid":424,
|
||||
"firstgid":426,
|
||||
"source":"room14.tsx"
|
||||
},
|
||||
{
|
||||
"firstgid":524,
|
||||
"firstgid":526,
|
||||
"source":"room18.tsx"
|
||||
}],
|
||||
"tilewidth":32,
|
||||
|
||||
528
css/biometrics-minigame.css
Normal file
528
css/biometrics-minigame.css
Normal file
@@ -0,0 +1,528 @@
|
||||
/* Biometrics Minigame Styles */
|
||||
|
||||
.biometrics-minigame-container {
|
||||
/* Compact interface similar to Bluetooth scanner */
|
||||
position: fixed !important;
|
||||
top: 5vh !important;
|
||||
right: 2vw !important;
|
||||
width: 350px !important;
|
||||
height: auto !important;
|
||||
max-height: 60vh !important;
|
||||
background: linear-gradient(135deg, #2e1a1a 0%, #3e1616 50%, #600f0f 100%) !important;
|
||||
border: 2px solid #e74c3c !important;
|
||||
box-shadow: 0 0 20px rgba(231, 76, 60, 0.3), inset 0 0 10px rgba(231, 76, 60, 0.1) !important;
|
||||
border-radius: 10px !important;
|
||||
font-family: 'VT323', monospace !important;
|
||||
color: #e0e0e0 !important;
|
||||
overflow: hidden !important;
|
||||
transition: all 0.3s ease !important;
|
||||
}
|
||||
|
||||
.biometrics-minigame-container.expanded {
|
||||
width: 450px !important;
|
||||
max-height: 70vh !important;
|
||||
}
|
||||
|
||||
.biometrics-minigame-game-container {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
max-width: none !important;
|
||||
background: transparent !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: none !important;
|
||||
position: relative !important;
|
||||
overflow: visible !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
padding: 15px !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
/* Scanner Header */
|
||||
.biometrics-scanner-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
background: rgba(231, 76, 60, 0.1);
|
||||
border: 1px solid #e74c3c;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 0 10px rgba(231, 76, 60, 0.2);
|
||||
}
|
||||
|
||||
.biometrics-scanner-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #e74c3c;
|
||||
text-shadow: 0 0 5px rgba(231, 76, 60, 0.5);
|
||||
}
|
||||
|
||||
.samples-count-header {
|
||||
font-size: 12px;
|
||||
color: #4caf50;
|
||||
background: rgba(76, 175, 80, 0.2);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #4caf50;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.scanner-icon {
|
||||
height: 24px;
|
||||
filter: drop-shadow(0 0 3px rgba(231, 76, 60, 0.5));
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.biometrics-scanner-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
.scanner-indicator {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #4caf50;
|
||||
box-shadow: 0 0 6px rgba(76, 175, 80, 0.8);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.scanner-indicator.active {
|
||||
background: #4caf50;
|
||||
}
|
||||
|
||||
.scanner-indicator.inactive {
|
||||
background: #f44336;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.5; transform: scale(1.2); }
|
||||
100% { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
|
||||
/* Expand/Collapse Toggle */
|
||||
.biometrics-expand-toggle {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: rgba(231, 76, 60, 0.2);
|
||||
border: 1px solid #e74c3c;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
color: #e74c3c;
|
||||
transition: all 0.3s ease;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.biometrics-expand-toggle:hover {
|
||||
background: rgba(231, 76, 60, 0.3);
|
||||
box-shadow: 0 0 8px rgba(231, 76, 60, 0.4);
|
||||
}
|
||||
|
||||
.biometrics-expand-toggle.expanded {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* Search Room Button */
|
||||
.biometrics-search-room-container {
|
||||
margin-bottom: 15px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
background: rgba(231, 76, 60, 0.1);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 0 10px rgba(231, 76, 60, 0.2);
|
||||
}
|
||||
|
||||
/* Controls */
|
||||
.biometrics-scanner-controls {
|
||||
margin-bottom: 10px;
|
||||
transition: all 0.3s ease;
|
||||
max-height: 200px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.biometrics-minigame-container:not(.expanded) .biometrics-scanner-controls {
|
||||
max-height: 0;
|
||||
margin-bottom: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.biometrics-search-container {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.biometrics-search-input {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid #e74c3c;
|
||||
border-radius: 6px;
|
||||
color: #e0e0e0;
|
||||
font-family: 'VT323', monospace;
|
||||
font-size: 14px;
|
||||
box-shadow: 0 0 8px rgba(231, 76, 60, 0.2);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.biometrics-search-input:focus {
|
||||
outline: none;
|
||||
border-color: #4caf50;
|
||||
box-shadow: 0 0 15px rgba(76, 175, 80, 0.4);
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.biometrics-search-input::placeholder {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.biometrics-categories {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.biometrics-category {
|
||||
padding: 6px 12px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid #555;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
color: #ccc;
|
||||
transition: all 0.3s ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.biometrics-category:hover {
|
||||
background: rgba(231, 76, 60, 0.2);
|
||||
border-color: #e74c3c;
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
.biometrics-category.active {
|
||||
background: rgba(231, 76, 60, 0.3);
|
||||
border-color: #e74c3c;
|
||||
color: #e74c3c;
|
||||
box-shadow: 0 0 10px rgba(231, 76, 60, 0.3);
|
||||
}
|
||||
|
||||
/* Action Buttons */
|
||||
.biometrics-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.biometrics-action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 12px;
|
||||
background: rgba(231, 76, 60, 0.2);
|
||||
border: 1px solid #e74c3c;
|
||||
border-radius: 6px;
|
||||
color: #e74c3c;
|
||||
font-family: 'VT323', monospace;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.biometrics-action-btn:hover {
|
||||
background: rgba(231, 76, 60, 0.3);
|
||||
box-shadow: 0 0 10px rgba(231, 76, 60, 0.4);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.biometrics-action-btn.active {
|
||||
background: rgba(76, 175, 80, 0.3);
|
||||
border-color: #4caf50;
|
||||
color: #4caf50;
|
||||
box-shadow: 0 0 15px rgba(76, 175, 80, 0.4);
|
||||
}
|
||||
|
||||
.biometrics-action-btn .btn-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.biometrics-action-btn .btn-text {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Samples List */
|
||||
.biometrics-samples-list-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
transition: all 0.3s ease;
|
||||
max-height: 300px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.biometrics-minigame-container:not(.expanded) .biometrics-samples-list-container {
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.biometrics-samples-list-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
padding: 8px 12px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid #444;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
.samples-count {
|
||||
font-size: 12px;
|
||||
color: #4caf50;
|
||||
background: rgba(76, 175, 80, 0.2);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #4caf50;
|
||||
}
|
||||
|
||||
.biometrics-samples-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid #333;
|
||||
border-radius: 6px;
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
.biometrics-samples-list::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.biometrics-samples-list::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.biometrics-samples-list::-webkit-scrollbar-thumb {
|
||||
background: #e74c3c;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.biometrics-samples-list::-webkit-scrollbar-thumb:hover {
|
||||
background: #4caf50;
|
||||
}
|
||||
|
||||
/* Sample Items */
|
||||
.sample-item {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid #444;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 6px;
|
||||
padding: 10px;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sample-item:hover {
|
||||
background: rgba(231, 76, 60, 0.1);
|
||||
border-color: #e74c3c;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(231, 76, 60, 0.2);
|
||||
}
|
||||
|
||||
.sample-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.sample-header strong {
|
||||
color: #e0e0e0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.sample-type {
|
||||
font-size: 12px;
|
||||
color: #e74c3c;
|
||||
background: rgba(231, 76, 60, 0.2);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #e74c3c;
|
||||
}
|
||||
|
||||
.sample-details {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.sample-quality {
|
||||
font-weight: bold;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
.sample-quality.quality-perfect {
|
||||
color: #4caf50;
|
||||
background: rgba(76, 175, 80, 0.2);
|
||||
border-color: #4caf50;
|
||||
}
|
||||
|
||||
.sample-quality.quality-excellent {
|
||||
color: #8bc34a;
|
||||
background: rgba(139, 195, 74, 0.2);
|
||||
border-color: #8bc34a;
|
||||
}
|
||||
|
||||
.sample-quality.quality-good {
|
||||
color: #ffc107;
|
||||
background: rgba(255, 193, 7, 0.2);
|
||||
border-color: #ffc107;
|
||||
}
|
||||
|
||||
.sample-quality.quality-fair {
|
||||
color: #ff9800;
|
||||
background: rgba(255, 152, 0, 0.2);
|
||||
border-color: #ff9800;
|
||||
}
|
||||
|
||||
.sample-quality.quality-acceptable {
|
||||
color: #ff5722;
|
||||
background: rgba(255, 87, 34, 0.2);
|
||||
border-color: #ff5722;
|
||||
}
|
||||
|
||||
.sample-quality.quality-poor {
|
||||
color: #f44336;
|
||||
background: rgba(244, 67, 54, 0.2);
|
||||
border-color: #f44336;
|
||||
}
|
||||
|
||||
.sample-date {
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
/* Instructions */
|
||||
.biometrics-scanner-instructions {
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid #444;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
color: #ccc;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.biometrics-minigame-container:not(.expanded) .biometrics-scanner-instructions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.instruction-text {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.instruction-text strong {
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.biometrics-minigame-container {
|
||||
top: 2vh !important;
|
||||
right: 2vw !important;
|
||||
left: 2vw !important;
|
||||
width: 96vw !important;
|
||||
max-width: 400px !important;
|
||||
}
|
||||
|
||||
.biometrics-minigame-container.expanded {
|
||||
width: 96vw !important;
|
||||
max-width: 500px !important;
|
||||
}
|
||||
|
||||
.biometrics-scanner-title {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.biometrics-categories {
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.biometrics-category {
|
||||
text-align: center;
|
||||
padding: 4px 8px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.biometrics-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.biometrics-action-btn {
|
||||
justify-content: center;
|
||||
padding: 6px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.biometrics-expand-toggle {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Animation for new samples */
|
||||
@keyframes sampleAppear {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateX(-20px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.sample-item.new-sample {
|
||||
animation: sampleAppear 0.5s ease-out;
|
||||
}
|
||||
|
||||
/* Hover preservation for smooth updates */
|
||||
.sample-item.hover-preserved {
|
||||
background: rgba(231, 76, 60, 0.1);
|
||||
border-color: #e74c3c;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(231, 76, 60, 0.2);
|
||||
}
|
||||
462
css/bluetooth-scanner.css
Normal file
462
css/bluetooth-scanner.css
Normal file
@@ -0,0 +1,462 @@
|
||||
/* Bluetooth Scanner Minigame Styles */
|
||||
|
||||
.bluetooth-scanner-minigame-container {
|
||||
/* Much smaller, compact interface */
|
||||
position: fixed !important;
|
||||
top: 5vh !important;
|
||||
right: 2vw !important;
|
||||
width: 350px !important;
|
||||
height: auto !important;
|
||||
max-height: 60vh !important;
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%) !important;
|
||||
border: 2px solid #00bcd4 !important;
|
||||
box-shadow: 0 0 20px rgba(0, 188, 212, 0.3), inset 0 0 10px rgba(0, 188, 212, 0.1) !important;
|
||||
border-radius: 10px !important;
|
||||
font-family: 'VT323', monospace !important;
|
||||
color: #e0e0e0 !important;
|
||||
overflow: hidden !important;
|
||||
transition: all 0.3s ease !important;
|
||||
}
|
||||
|
||||
.bluetooth-scanner-minigame-container.expanded {
|
||||
width: 450px !important;
|
||||
max-height: 70vh !important;
|
||||
}
|
||||
|
||||
.bluetooth-scanner-minigame-game-container {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
max-width: none !important;
|
||||
background: transparent !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: none !important;
|
||||
position: relative !important;
|
||||
overflow: visible !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
padding: 15px !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
/* Scanner Header */
|
||||
.bluetooth-scanner-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
background: rgba(0, 188, 212, 0.1);
|
||||
border: 1px solid #00bcd4;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 0 10px rgba(0, 188, 212, 0.2);
|
||||
}
|
||||
|
||||
.bluetooth-scanner-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #00bcd4;
|
||||
text-shadow: 0 0 5px rgba(0, 188, 212, 0.5);
|
||||
}
|
||||
|
||||
.scanner-icon {
|
||||
height: 24px;
|
||||
filter: drop-shadow(0 0 3px rgba(0, 188, 212, 0.5));
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.bluetooth-scanner-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
.scanner-indicator {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #4caf50;
|
||||
box-shadow: 0 0 6px rgba(76, 175, 80, 0.8);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.scanner-indicator.active {
|
||||
background: #4caf50;
|
||||
}
|
||||
|
||||
.scanner-indicator.inactive {
|
||||
background: #f44336;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.5; transform: scale(1.2); }
|
||||
100% { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
|
||||
/* Expand/Collapse Toggle */
|
||||
.bluetooth-scanner-expand-toggle {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: rgba(0, 188, 212, 0.2);
|
||||
border: 1px solid #00bcd4;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
color: #00bcd4;
|
||||
transition: all 0.3s ease;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.bluetooth-scanner-expand-toggle:hover {
|
||||
background: rgba(0, 188, 212, 0.3);
|
||||
box-shadow: 0 0 8px rgba(0, 188, 212, 0.4);
|
||||
}
|
||||
|
||||
.bluetooth-scanner-expand-toggle.expanded {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* Controls */
|
||||
.bluetooth-scanner-controls {
|
||||
margin-bottom: 10px;
|
||||
transition: all 0.3s ease;
|
||||
max-height: 200px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bluetooth-scanner-minigame-container:not(.expanded) .bluetooth-scanner-controls {
|
||||
max-height: 0;
|
||||
margin-bottom: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.bluetooth-search-container {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.bluetooth-search-input {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid #00bcd4;
|
||||
border-radius: 6px;
|
||||
color: #e0e0e0;
|
||||
font-family: 'VT323', monospace;
|
||||
font-size: 14px;
|
||||
box-shadow: 0 0 8px rgba(0, 188, 212, 0.2);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.bluetooth-search-input:focus {
|
||||
outline: none;
|
||||
border-color: #4caf50;
|
||||
box-shadow: 0 0 15px rgba(76, 175, 80, 0.4);
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.bluetooth-search-input::placeholder {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.bluetooth-categories {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.bluetooth-category {
|
||||
padding: 6px 12px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid #555;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
color: #ccc;
|
||||
transition: all 0.3s ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.bluetooth-category:hover {
|
||||
background: rgba(0, 188, 212, 0.2);
|
||||
border-color: #00bcd4;
|
||||
color: #00bcd4;
|
||||
}
|
||||
|
||||
.bluetooth-category.active {
|
||||
background: rgba(0, 188, 212, 0.3);
|
||||
border-color: #00bcd4;
|
||||
color: #00bcd4;
|
||||
box-shadow: 0 0 10px rgba(0, 188, 212, 0.3);
|
||||
}
|
||||
|
||||
/* Device List */
|
||||
.bluetooth-device-list-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.bluetooth-scanner-minigame-container:not(.expanded) .bluetooth-device-list-container {
|
||||
max-height: 200px;
|
||||
}
|
||||
|
||||
.bluetooth-device-list-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
padding: 8px 12px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid #444;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #00bcd4;
|
||||
}
|
||||
|
||||
.device-count {
|
||||
font-size: 12px;
|
||||
color: #4caf50;
|
||||
background: rgba(76, 175, 80, 0.2);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #4caf50;
|
||||
}
|
||||
|
||||
.bluetooth-device-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid #333;
|
||||
border-radius: 6px;
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
.bluetooth-device-list::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.bluetooth-device-list::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.bluetooth-device-list::-webkit-scrollbar-thumb {
|
||||
background: #00bcd4;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.bluetooth-device-list::-webkit-scrollbar-thumb:hover {
|
||||
background: #4caf50;
|
||||
}
|
||||
|
||||
/* Device Items */
|
||||
.bluetooth-device {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid #444;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 6px;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bluetooth-device:hover {
|
||||
background: rgba(0, 188, 212, 0.1);
|
||||
border-color: #00bcd4;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(0, 188, 212, 0.2);
|
||||
}
|
||||
|
||||
.bluetooth-device.expanded {
|
||||
background: rgba(0, 188, 212, 0.15);
|
||||
border-color: #00bcd4;
|
||||
box-shadow: 0 0 20px rgba(0, 188, 212, 0.3);
|
||||
}
|
||||
|
||||
.bluetooth-device-name {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.bluetooth-device-icons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.bluetooth-device-icon {
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Signal Strength Bars */
|
||||
.bluetooth-signal-bar-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.bluetooth-signal-bars {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 1px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.bluetooth-signal-bar {
|
||||
width: 3px;
|
||||
background: #666;
|
||||
border-radius: 1px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.bluetooth-signal-bar:nth-child(1) { height: 3px; }
|
||||
.bluetooth-signal-bar:nth-child(2) { height: 6px; }
|
||||
.bluetooth-signal-bar:nth-child(3) { height: 9px; }
|
||||
.bluetooth-signal-bar:nth-child(4) { height: 12px; }
|
||||
.bluetooth-signal-bar:nth-child(5) { height: 16px; }
|
||||
|
||||
.bluetooth-signal-bar.active {
|
||||
background: currentColor;
|
||||
box-shadow: 0 0 5px currentColor;
|
||||
}
|
||||
|
||||
.bluetooth-device-details {
|
||||
font-size: 12px;
|
||||
color: #aaa;
|
||||
white-space: pre-line;
|
||||
margin-bottom: 6px;
|
||||
line-height: 1.3;
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease;
|
||||
}
|
||||
|
||||
.bluetooth-device.expanded .bluetooth-device-details {
|
||||
max-height: 200px;
|
||||
}
|
||||
|
||||
.bluetooth-device-timestamp {
|
||||
font-size: 10px;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Instructions */
|
||||
.bluetooth-scanner-instructions {
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid #444;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
color: #ccc;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.bluetooth-scanner-minigame-container:not(.expanded) .bluetooth-scanner-instructions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.instruction-text {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.instruction-text strong {
|
||||
color: #00bcd4;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.bluetooth-scanner-minigame-container {
|
||||
top: 2vh !important;
|
||||
right: 2vw !important;
|
||||
left: 2vw !important;
|
||||
width: 96vw !important;
|
||||
max-width: 400px !important;
|
||||
}
|
||||
|
||||
.bluetooth-scanner-minigame-container.expanded {
|
||||
width: 96vw !important;
|
||||
max-width: 500px !important;
|
||||
}
|
||||
|
||||
.bluetooth-scanner-title {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.bluetooth-categories {
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.bluetooth-category {
|
||||
text-align: center;
|
||||
padding: 4px 8px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.bluetooth-device-name {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.bluetooth-device-icons {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.bluetooth-scanner-expand-toggle {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Animation for new devices */
|
||||
@keyframes deviceAppear {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateX(-20px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.bluetooth-device.new-device {
|
||||
animation: deviceAppear 0.5s ease-out;
|
||||
}
|
||||
|
||||
/* Hover preservation for smooth updates */
|
||||
.bluetooth-device.hover-preserved {
|
||||
background: rgba(0, 188, 212, 0.1);
|
||||
border-color: #00bcd4;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(0, 188, 212, 0.2);
|
||||
}
|
||||
251
css/lockpick-set-minigame.css
Normal file
251
css/lockpick-set-minigame.css
Normal file
@@ -0,0 +1,251 @@
|
||||
/* Lockpick Set Minigame Styles */
|
||||
|
||||
.lockpick-set-minigame-container {
|
||||
/* Compact interface similar to other minigames */
|
||||
position: fixed !important;
|
||||
top: 5vh !important;
|
||||
right: 2vw !important;
|
||||
width: 350px !important;
|
||||
height: auto !important;
|
||||
max-height: 60vh !important;
|
||||
background: linear-gradient(135deg, #1a2e1a 0%, #163e16 50%, #0f600f 100%) !important;
|
||||
border: 2px solid #4caf50 !important;
|
||||
box-shadow: 0 0 20px rgba(76, 175, 80, 0.3), inset 0 0 10px rgba(76, 175, 80, 0.1) !important;
|
||||
border-radius: 10px !important;
|
||||
font-family: 'VT323', monospace !important;
|
||||
color: #e0e0e0 !important;
|
||||
overflow: hidden !important;
|
||||
transition: all 0.3s ease !important;
|
||||
}
|
||||
|
||||
.lockpick-set-minigame-container.expanded {
|
||||
width: 450px !important;
|
||||
max-height: 70vh !important;
|
||||
}
|
||||
|
||||
.lockpick-set-minigame-game-container {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
max-width: none !important;
|
||||
background: transparent !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: none !important;
|
||||
position: relative !important;
|
||||
overflow: visible !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
padding: 15px !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
/* Lockpick Header */
|
||||
.lockpick-set-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
background: rgba(76, 175, 80, 0.1);
|
||||
border: 1px solid #4caf50;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 0 10px rgba(76, 175, 80, 0.2);
|
||||
}
|
||||
|
||||
.lockpick-set-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #4caf50;
|
||||
text-shadow: 0 0 5px rgba(76, 175, 80, 0.5);
|
||||
}
|
||||
|
||||
.lockpick-icon {
|
||||
height: 24px;
|
||||
filter: drop-shadow(0 0 3px rgba(76, 175, 80, 0.5));
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.lockpick-set-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
.lockpick-indicator {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #4caf50;
|
||||
box-shadow: 0 0 6px rgba(76, 175, 80, 0.8);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.lockpick-indicator.active {
|
||||
background: #4caf50;
|
||||
}
|
||||
|
||||
.lockpick-indicator.inactive {
|
||||
background: #f44336;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.5; transform: scale(1.2); }
|
||||
100% { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
|
||||
/* Expand/Collapse Toggle */
|
||||
.lockpick-expand-toggle {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: rgba(76, 175, 80, 0.2);
|
||||
border: 1px solid #4caf50;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
color: #4caf50;
|
||||
transition: all 0.3s ease;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.lockpick-expand-toggle:hover {
|
||||
background: rgba(76, 175, 80, 0.3);
|
||||
box-shadow: 0 0 8px rgba(76, 175, 80, 0.4);
|
||||
}
|
||||
|
||||
.lockpick-expand-toggle.expanded {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* Search Room Button */
|
||||
.lockpick-search-room-container {
|
||||
margin-bottom: 15px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
background: rgba(76, 175, 80, 0.1);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 0 10px rgba(76, 175, 80, 0.2);
|
||||
}
|
||||
|
||||
/* Action Buttons */
|
||||
.lockpick-action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 12px;
|
||||
background: rgba(76, 175, 80, 0.2);
|
||||
border: 1px solid #4caf50;
|
||||
border-radius: 6px;
|
||||
color: #4caf50;
|
||||
font-family: 'VT323', monospace;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.lockpick-action-btn:hover {
|
||||
background: rgba(76, 175, 80, 0.3);
|
||||
box-shadow: 0 0 10px rgba(76, 175, 80, 0.4);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.lockpick-action-btn.active {
|
||||
background: rgba(255, 193, 7, 0.3);
|
||||
border-color: #ffc107;
|
||||
color: #ffc107;
|
||||
box-shadow: 0 0 15px rgba(255, 193, 7, 0.4);
|
||||
}
|
||||
|
||||
.lockpick-action-btn .btn-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.lockpick-action-btn .btn-text {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Instructions */
|
||||
.lockpick-set-instructions {
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid #444;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
color: #ccc;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.lockpick-set-minigame-container:not(.expanded) .lockpick-set-instructions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.instruction-text {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.instruction-text strong {
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.lockpick-set-minigame-container {
|
||||
top: 2vh !important;
|
||||
right: 2vw !important;
|
||||
left: 2vw !important;
|
||||
width: 96vw !important;
|
||||
max-width: 400px !important;
|
||||
}
|
||||
|
||||
.lockpick-set-minigame-container.expanded {
|
||||
width: 96vw !important;
|
||||
max-width: 500px !important;
|
||||
}
|
||||
|
||||
.lockpick-set-title {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.lockpick-action-btn {
|
||||
justify-content: center;
|
||||
padding: 6px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.lockpick-expand-toggle {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Animation for new highlights */
|
||||
@keyframes lockpickAppear {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateX(-20px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.lockpick-indicator.new-highlight {
|
||||
animation: lockpickAppear 0.5s ease-out;
|
||||
}
|
||||
45
index.html
45
index.html
@@ -35,6 +35,9 @@
|
||||
<link rel="stylesheet" href="css/lockpicking.css">
|
||||
<link rel="stylesheet" href="css/modals.css">
|
||||
<link rel="stylesheet" href="css/notes.css">
|
||||
<link rel="stylesheet" href="css/bluetooth-scanner.css">
|
||||
<link rel="stylesheet" href="css/biometrics-minigame.css">
|
||||
<link rel="stylesheet" href="css/lockpick-set-minigame.css">
|
||||
|
||||
<!-- External JavaScript libraries -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/phaser@3.60.0/dist/phaser.min.js"></script>
|
||||
@@ -50,47 +53,7 @@
|
||||
|
||||
<!-- Toggle Buttons Container -->
|
||||
<div id="toggle-buttons-container">
|
||||
<div id="bluetooth-toggle" style="display: none;">
|
||||
<img src="assets/objects/bluetooth_scanner.png" alt="Bluetooth">
|
||||
<div id="bluetooth-count">0</div>
|
||||
</div>
|
||||
<div id="biometrics-toggle" style="display: none;">
|
||||
<img src="assets/objects/fingerprint.png" alt="Biometrics">
|
||||
<div id="biometrics-count">0</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bluetooth Scanner Panel -->
|
||||
<div id="bluetooth-panel">
|
||||
<div id="bluetooth-header">
|
||||
<div id="bluetooth-title">Bluetooth Scanner</div>
|
||||
<div id="bluetooth-close">×</div>
|
||||
</div>
|
||||
<div id="bluetooth-search-container">
|
||||
<input type="text" id="bluetooth-search" placeholder="Search devices...">
|
||||
</div>
|
||||
<div id="bluetooth-categories">
|
||||
<div class="bluetooth-category active" data-category="all">All</div>
|
||||
<div class="bluetooth-category" data-category="nearby">Nearby</div>
|
||||
<div class="bluetooth-category" data-category="saved">Saved</div>
|
||||
</div>
|
||||
<div id="bluetooth-content"></div>
|
||||
</div>
|
||||
|
||||
<!-- Biometrics Panel -->
|
||||
<div id="biometrics-panel">
|
||||
<div id="biometrics-header">
|
||||
<div id="biometrics-title">Biometric Samples</div>
|
||||
<div id="biometrics-close">×</div>
|
||||
</div>
|
||||
<div id="biometrics-search-container">
|
||||
<input type="text" id="biometrics-search" placeholder="Search samples...">
|
||||
</div>
|
||||
<div id="biometrics-categories">
|
||||
<div class="biometrics-category active" data-category="all">All</div>
|
||||
<div class="biometrics-category" data-category="fingerprint">Fingerprints</div>
|
||||
</div>
|
||||
<div id="biometrics-content"></div>
|
||||
<!-- Biometrics is now handled as a minigame -->
|
||||
</div>
|
||||
|
||||
<!-- Inventory Container -->
|
||||
|
||||
@@ -60,7 +60,7 @@ export function preload() {
|
||||
this.load.image('bluetooth_scanner', 'assets/objects/bluetooth_scanner.png');
|
||||
this.load.image('bluetooth', 'assets/objects/bluetooth.png');
|
||||
this.load.image('tablet', 'assets/objects/tablet.png');
|
||||
this.load.image('fingerprint', 'assets/objects/fingerprint.png');
|
||||
this.load.image('fingerprint', 'assets/objects/fingerprint_small.png');
|
||||
this.load.image('lockpick', 'assets/objects/lockpick.png');
|
||||
this.load.image('spoofing_kit', 'assets/objects/office-misc-headphones.png');
|
||||
|
||||
@@ -524,18 +524,10 @@ export function update() {
|
||||
window.updateSwivelChairRotation();
|
||||
}
|
||||
|
||||
// Check for Bluetooth devices
|
||||
const currentTime = Date.now();
|
||||
if (currentTime - lastBluetoothScan >= 200) { // 200ms interval for more responsive updates
|
||||
if (window.checkBluetoothDevices) {
|
||||
window.checkBluetoothDevices();
|
||||
}
|
||||
lastBluetoothScan = currentTime;
|
||||
}
|
||||
// Bluetooth device scanning is now handled by the minigame when active
|
||||
}
|
||||
|
||||
// Add timing variables at module level
|
||||
let lastBluetoothScan = 0;
|
||||
// Bluetooth scanning is now handled by the minigame
|
||||
|
||||
// Helper functions
|
||||
|
||||
|
||||
12
js/main.js
12
js/main.js
@@ -1,8 +1,9 @@
|
||||
import { GAME_CONFIG } from './utils/constants.js?v=7';
|
||||
import { preload, create, update } from './core/game.js?v=32';
|
||||
import { initializeNotifications } from './systems/notifications.js?v=7';
|
||||
import { initializeBluetoothPanel } from './systems/bluetooth.js?v=8';
|
||||
import { initializeBiometricsPanel } from './systems/biometrics.js?v=22';
|
||||
// Bluetooth scanner is now handled as a minigame
|
||||
// Biometrics is now handled as a minigame
|
||||
import { startLockpickingMinigame } from './systems/interactions.js?v=23';
|
||||
import { initializeDebugSystem } from './systems/debug.js?v=7';
|
||||
import { initializeUI } from './ui/panels.js?v=9';
|
||||
import { initializeModals } from './ui/modals.js?v=7';
|
||||
@@ -64,8 +65,11 @@ function initializeGame() {
|
||||
|
||||
// Initialize all systems
|
||||
initializeNotifications();
|
||||
initializeBluetoothPanel();
|
||||
initializeBiometricsPanel();
|
||||
// Bluetooth scanner and biometrics are now handled as minigames
|
||||
|
||||
// Make lockpicking function available globally
|
||||
window.startLockpickingMinigame = startLockpickingMinigame;
|
||||
|
||||
initializeDebugSystem();
|
||||
initializeUI();
|
||||
initializeModals();
|
||||
|
||||
631
js/minigames/biometrics/biometrics-minigame.js
Normal file
631
js/minigames/biometrics/biometrics-minigame.js
Normal file
@@ -0,0 +1,631 @@
|
||||
import { MinigameScene } from '../framework/base-minigame.js';
|
||||
|
||||
// Biometrics Minigame Scene implementation
|
||||
export class BiometricsMinigame extends MinigameScene {
|
||||
constructor(container, params) {
|
||||
// Ensure params is defined before calling parent constructor
|
||||
params = params || {};
|
||||
|
||||
// Set default title if not provided
|
||||
params.title = 'Biometric Scanner';
|
||||
|
||||
// Enable cancel button for biometrics minigame with custom text
|
||||
params.showCancel = true;
|
||||
params.cancelText = 'Close Scanner';
|
||||
|
||||
super(container, params);
|
||||
|
||||
this.item = params.item;
|
||||
this.biometricSamples = [];
|
||||
this.searchingMode = false;
|
||||
this.highlightedObjects = [];
|
||||
|
||||
// Scanner state management
|
||||
this.scannerState = {
|
||||
failedAttempts: {},
|
||||
lockoutTimers: {}
|
||||
};
|
||||
|
||||
// Constants
|
||||
this.MAX_FAILED_ATTEMPTS = 3;
|
||||
this.SCANNER_LOCKOUT_TIME = 30000; // 30 seconds
|
||||
this.BIOMETRIC_QUALITY_THRESHOLD = 0.7;
|
||||
}
|
||||
|
||||
init() {
|
||||
// Call parent init to set up common components
|
||||
super.init();
|
||||
|
||||
console.log("Biometrics minigame initializing");
|
||||
|
||||
// Set container dimensions to be compact like the Bluetooth scanner
|
||||
this.container.className += ' biometrics-minigame-container';
|
||||
|
||||
// Clear header content
|
||||
this.headerElement.innerHTML = '';
|
||||
|
||||
// Configure game container with scanner background
|
||||
this.gameContainer.className += ' biometrics-minigame-game-container';
|
||||
|
||||
// Create scanner interface
|
||||
this.createScannerInterface();
|
||||
|
||||
// Initialize biometric samples from global state
|
||||
this.initializeBiometricSamples();
|
||||
}
|
||||
|
||||
createScannerInterface() {
|
||||
// Create expand/collapse toggle button
|
||||
const expandToggle = document.createElement('div');
|
||||
expandToggle.className = 'biometrics-expand-toggle';
|
||||
expandToggle.innerHTML = '▼';
|
||||
expandToggle.title = 'Expand/Collapse';
|
||||
|
||||
// Create scanner header
|
||||
const scannerHeader = document.createElement('div');
|
||||
scannerHeader.className = 'biometrics-scanner-header';
|
||||
scannerHeader.innerHTML = `
|
||||
<div class="biometrics-scanner-title">
|
||||
<img src="assets/objects/fingerprint.png" alt="Biometric Samples" class="scanner-icon">
|
||||
<span>Biometric Samples</span>
|
||||
<span class="samples-count-header">0 samples</span>
|
||||
</div>
|
||||
<div class="biometrics-scanner-status">
|
||||
<div class="scanner-indicator active"></div>
|
||||
<span>Ready</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Create search room button (above samples list)
|
||||
const searchRoomContainer = document.createElement('div');
|
||||
searchRoomContainer.className = 'biometrics-search-room-container';
|
||||
searchRoomContainer.innerHTML = `
|
||||
<button id="search-room-btn" class="biometrics-action-btn">
|
||||
<span class="btn-icon">🔍</span>
|
||||
<span class="btn-text">Search Room for Fingerprints</span>
|
||||
</button>
|
||||
`;
|
||||
|
||||
// Create controls container (for expanded view)
|
||||
const controlsContainer = document.createElement('div');
|
||||
controlsContainer.className = 'biometrics-scanner-controls';
|
||||
controlsContainer.innerHTML = `
|
||||
<div class="biometrics-search-container">
|
||||
<input type="text" id="biometrics-search" placeholder="Search samples..." class="biometrics-search-input">
|
||||
</div>
|
||||
<div class="biometrics-categories">
|
||||
<div class="biometrics-category active" data-category="all">All</div>
|
||||
<div class="biometrics-category" data-category="fingerprint">Fingerprints</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Create samples list container
|
||||
const samplesListContainer = document.createElement('div');
|
||||
samplesListContainer.className = 'biometrics-samples-list-container';
|
||||
samplesListContainer.innerHTML = `
|
||||
<div class="biometrics-samples-list-header">
|
||||
<span>Collected Samples</span>
|
||||
<div class="samples-count">0 samples</div>
|
||||
</div>
|
||||
<div class="biometrics-samples-list" id="biometrics-samples-list"></div>
|
||||
`;
|
||||
|
||||
// Create instructions
|
||||
const instructionsContainer = document.createElement('div');
|
||||
instructionsContainer.className = 'biometrics-scanner-instructions';
|
||||
instructionsContainer.innerHTML = `
|
||||
<div class="instruction-text">
|
||||
<strong>Instructions:</strong><br>
|
||||
• Use "Search Room" to highlight objects with fingerprints<br>
|
||||
• Click highlighted objects to collect fingerprint samples<br>
|
||||
• Collected samples can be used to unlock biometric scanners<br>
|
||||
• Higher quality samples have better success rates
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Assemble the interface
|
||||
this.gameContainer.appendChild(expandToggle);
|
||||
this.gameContainer.appendChild(searchRoomContainer);
|
||||
this.gameContainer.appendChild(scannerHeader);
|
||||
this.gameContainer.appendChild(controlsContainer);
|
||||
this.gameContainer.appendChild(samplesListContainer);
|
||||
this.gameContainer.appendChild(instructionsContainer);
|
||||
|
||||
// Set up event listeners
|
||||
this.setupEventListeners();
|
||||
|
||||
// Set up expand/collapse functionality
|
||||
this.setupExpandToggle(expandToggle);
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Search functionality
|
||||
const biometricsSearch = document.getElementById('biometrics-search');
|
||||
if (biometricsSearch) {
|
||||
this.addEventListener(biometricsSearch, 'input', () => this.updateBiometricsPanel());
|
||||
}
|
||||
|
||||
// Category filters
|
||||
const categories = this.gameContainer.querySelectorAll('.biometrics-category');
|
||||
categories.forEach(category => {
|
||||
this.addEventListener(category, 'click', () => {
|
||||
// Remove active class from all categories
|
||||
categories.forEach(c => c.classList.remove('active'));
|
||||
// Add active class to clicked category
|
||||
category.classList.add('active');
|
||||
// Update biometrics panel
|
||||
this.updateBiometricsPanel();
|
||||
});
|
||||
});
|
||||
|
||||
// Search room button
|
||||
const searchRoomBtn = document.getElementById('search-room-btn');
|
||||
if (searchRoomBtn) {
|
||||
this.addEventListener(searchRoomBtn, 'click', () => this.toggleRoomSearching());
|
||||
}
|
||||
}
|
||||
|
||||
setupExpandToggle(expandToggle) {
|
||||
this.addEventListener(expandToggle, 'click', () => {
|
||||
const isExpanded = this.container.classList.contains('expanded');
|
||||
|
||||
if (isExpanded) {
|
||||
// Collapse
|
||||
this.container.classList.remove('expanded');
|
||||
expandToggle.innerHTML = '▼';
|
||||
expandToggle.title = 'Expand';
|
||||
} else {
|
||||
// Expand
|
||||
this.container.classList.add('expanded');
|
||||
expandToggle.innerHTML = '▲';
|
||||
expandToggle.title = 'Collapse';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initializeBiometricSamples() {
|
||||
// Initialize from global state if available
|
||||
if (window.gameState && window.gameState.biometricSamples) {
|
||||
this.biometricSamples = [...window.gameState.biometricSamples];
|
||||
} else {
|
||||
this.biometricSamples = [];
|
||||
}
|
||||
|
||||
// Update the panel
|
||||
this.updateBiometricsPanel();
|
||||
}
|
||||
|
||||
toggleRoomSearching() {
|
||||
this.searchingMode = !this.searchingMode;
|
||||
const searchBtn = document.getElementById('search-room-btn');
|
||||
|
||||
if (this.searchingMode) {
|
||||
// Start searching mode
|
||||
searchBtn.classList.add('active');
|
||||
searchBtn.querySelector('.btn-text').textContent = 'Stop Searching';
|
||||
this.highlightFingerprintObjects();
|
||||
console.log('Room searching started');
|
||||
} else {
|
||||
// Stop searching mode
|
||||
searchBtn.classList.remove('active');
|
||||
searchBtn.querySelector('.btn-text').textContent = 'Search Room for Fingerprints';
|
||||
this.clearHighlights();
|
||||
console.log('Room searching stopped');
|
||||
}
|
||||
}
|
||||
|
||||
highlightFingerprintObjects() {
|
||||
// Clear existing highlights
|
||||
this.clearHighlights();
|
||||
|
||||
// Find all objects in the current room that have fingerprints
|
||||
if (!window.currentPlayerRoom || !window.rooms[window.currentPlayerRoom] || !window.rooms[window.currentPlayerRoom].objects) {
|
||||
return;
|
||||
}
|
||||
|
||||
const room = window.rooms[window.currentPlayerRoom];
|
||||
this.highlightedObjects = [];
|
||||
|
||||
Object.values(room.objects).forEach(obj => {
|
||||
if (obj.scenarioData?.hasFingerprint === true) {
|
||||
// Add red highlight effect to the object
|
||||
if (obj.setTint) {
|
||||
obj.setTint(0xff0000); // Red tint for fingerprint objects
|
||||
this.highlightedObjects.push(obj);
|
||||
}
|
||||
|
||||
// Add a visual indicator
|
||||
this.addFingerprintIndicator(obj);
|
||||
}
|
||||
});
|
||||
|
||||
if (this.highlightedObjects.length > 0) {
|
||||
console.log(`Highlighted ${this.highlightedObjects.length} objects with fingerprints`);
|
||||
} else {
|
||||
console.log('No objects with fingerprints found in this room');
|
||||
}
|
||||
}
|
||||
|
||||
addFingerprintIndicator(obj) {
|
||||
// Create a fingerprint image indicator directly over the object
|
||||
if (obj.scene && obj.scene.add) {
|
||||
const indicator = obj.scene.add.image(obj.x, obj.y, 'fingerprint');
|
||||
indicator.setDepth(1000); // High depth to appear on top
|
||||
indicator.setOrigin(-0.25, 0);
|
||||
// indicator.setScale(0.5); // Make it smaller
|
||||
indicator.setTint(0xff0000); // Red tint
|
||||
|
||||
// Add pulsing animation
|
||||
obj.scene.tweens.add({
|
||||
targets: indicator,
|
||||
alpha: { from: 1, to: 0.3 },
|
||||
duration: 1000,
|
||||
yoyo: true,
|
||||
repeat: -1
|
||||
});
|
||||
|
||||
// Store reference for cleanup
|
||||
obj.fingerprintIndicator = indicator;
|
||||
}
|
||||
}
|
||||
|
||||
clearHighlights() {
|
||||
// Remove highlights from all objects
|
||||
this.highlightedObjects.forEach(obj => {
|
||||
if (obj.clearTint) {
|
||||
obj.clearTint();
|
||||
}
|
||||
if (obj.fingerprintIndicator) {
|
||||
obj.fingerprintIndicator.destroy();
|
||||
delete obj.fingerprintIndicator;
|
||||
}
|
||||
});
|
||||
this.highlightedObjects = [];
|
||||
}
|
||||
|
||||
collectFingerprintFromObject(obj) {
|
||||
if (!obj.scenarioData) return;
|
||||
|
||||
// Use the fingerprint owner if specified, otherwise use the object's name
|
||||
const owner = obj.scenarioData.fingerprintOwner || obj.scenarioData.name || obj.scenarioData.owner || 'Unknown';
|
||||
|
||||
// Generate fingerprint sample with quality based on difficulty
|
||||
let quality = obj.scenarioData.fingerprintQuality;
|
||||
if (!quality) {
|
||||
// Generate quality based on difficulty
|
||||
const difficulty = obj.scenarioData.fingerprintDifficulty;
|
||||
if (difficulty === 'easy') {
|
||||
quality = 0.8 + Math.random() * 0.2; // 80-100%
|
||||
} else if (difficulty === 'medium') {
|
||||
quality = 0.6 + Math.random() * 0.3; // 60-90%
|
||||
} else if (difficulty === 'hard') {
|
||||
quality = 0.4 + Math.random() * 0.3; // 40-70%
|
||||
} else {
|
||||
quality = 0.6 + Math.random() * 0.4; // 60-100% default
|
||||
}
|
||||
}
|
||||
|
||||
const sample = this.generateFingerprintSample(owner, quality);
|
||||
|
||||
// Add to collection
|
||||
this.addBiometricSample(sample);
|
||||
|
||||
// Remove highlight from this object
|
||||
if (obj.clearTint) {
|
||||
obj.clearTint();
|
||||
}
|
||||
if (obj.fingerprintIndicator) {
|
||||
obj.fingerprintIndicator.destroy();
|
||||
delete obj.fingerprintIndicator;
|
||||
}
|
||||
|
||||
// Remove from highlighted objects
|
||||
const index = this.highlightedObjects.indexOf(obj);
|
||||
if (index > -1) {
|
||||
this.highlightedObjects.splice(index, 1);
|
||||
}
|
||||
|
||||
// Show success message
|
||||
if (window.gameAlert) {
|
||||
window.gameAlert(`Fingerprint collected from ${owner} (${sample.rating})`, 'success', 'Sample Collected', 3000);
|
||||
}
|
||||
|
||||
console.log('Fingerprint collected:', sample);
|
||||
}
|
||||
|
||||
generateFingerprintSample(owner, quality = null) {
|
||||
// If no quality provided, generate based on random factors
|
||||
if (quality === null) {
|
||||
quality = 0.6 + (Math.random() * 0.4); // 60-100% quality range
|
||||
}
|
||||
|
||||
const rating = this.getRatingFromQuality(quality);
|
||||
|
||||
return {
|
||||
owner: owner || 'Unknown',
|
||||
type: 'fingerprint',
|
||||
quality: quality,
|
||||
rating: rating,
|
||||
id: this.generateSampleId(),
|
||||
collectedAt: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
getRatingFromQuality(quality) {
|
||||
const qualityPercentage = Math.round(quality * 100);
|
||||
if (qualityPercentage >= 95) return 'Perfect';
|
||||
if (qualityPercentage >= 85) return 'Excellent';
|
||||
if (qualityPercentage >= 75) return 'Good';
|
||||
if (qualityPercentage >= 60) return 'Fair';
|
||||
if (qualityPercentage >= 40) return 'Acceptable';
|
||||
return 'Poor';
|
||||
}
|
||||
|
||||
generateSampleId() {
|
||||
return 'sample_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
||||
}
|
||||
|
||||
addBiometricSample(sample) {
|
||||
// Check if sample already exists
|
||||
const existingSample = this.biometricSamples.find(s =>
|
||||
s.owner === sample.owner && s.type === sample.type
|
||||
);
|
||||
|
||||
if (existingSample) {
|
||||
// Update existing sample with better quality if applicable
|
||||
if (sample.quality > existingSample.quality) {
|
||||
existingSample.quality = sample.quality;
|
||||
existingSample.rating = sample.rating;
|
||||
existingSample.collectedAt = sample.collectedAt;
|
||||
}
|
||||
} else {
|
||||
// Add new sample
|
||||
this.biometricSamples.push(sample);
|
||||
}
|
||||
|
||||
this.updateBiometricsPanel();
|
||||
this.syncBiometricSamples();
|
||||
console.log('Biometric sample added:', sample);
|
||||
}
|
||||
|
||||
updateBiometricsPanel() {
|
||||
const biometricsContent = document.getElementById('biometrics-samples-list');
|
||||
if (!biometricsContent) return;
|
||||
|
||||
const searchTerm = document.getElementById('biometrics-search')?.value?.toLowerCase() || '';
|
||||
const activeCategory = this.gameContainer.querySelector('.biometrics-category.active')?.dataset.category || 'all';
|
||||
|
||||
// Filter samples based on search and category
|
||||
let filteredSamples = [...this.biometricSamples];
|
||||
|
||||
// Apply category filter
|
||||
if (activeCategory === 'fingerprint') {
|
||||
filteredSamples = filteredSamples.filter(sample => sample.type === 'fingerprint');
|
||||
}
|
||||
|
||||
// Apply search filter
|
||||
if (searchTerm) {
|
||||
filteredSamples = filteredSamples.filter(sample =>
|
||||
sample.owner.toLowerCase().includes(searchTerm) ||
|
||||
sample.type.toLowerCase().includes(searchTerm)
|
||||
);
|
||||
}
|
||||
|
||||
// Sort samples by quality (highest first)
|
||||
filteredSamples.sort((a, b) => b.quality - a.quality);
|
||||
|
||||
// Update samples count in both header and list
|
||||
const samplesCount = this.gameContainer.querySelector('.samples-count');
|
||||
const samplesCountHeader = this.gameContainer.querySelector('.samples-count-header');
|
||||
const totalSamples = this.biometricSamples.length;
|
||||
|
||||
if (samplesCount) {
|
||||
samplesCount.textContent = `${filteredSamples.length} sample${filteredSamples.length !== 1 ? 's' : ''}`;
|
||||
}
|
||||
|
||||
if (samplesCountHeader) {
|
||||
samplesCountHeader.textContent = `${totalSamples} sample${totalSamples !== 1 ? 's' : ''}`;
|
||||
}
|
||||
|
||||
// Clear current content
|
||||
biometricsContent.innerHTML = '';
|
||||
|
||||
// Add samples
|
||||
if (filteredSamples.length === 0) {
|
||||
if (searchTerm) {
|
||||
biometricsContent.innerHTML = '<div class="sample-item">No samples match your search.</div>';
|
||||
} else if (activeCategory !== 'all') {
|
||||
biometricsContent.innerHTML = `<div class="sample-item">No ${activeCategory} samples found.</div>`;
|
||||
} else {
|
||||
biometricsContent.innerHTML = '<div class="sample-item">No samples collected yet. Use "Search Room" to find fingerprint objects.</div>';
|
||||
}
|
||||
} else {
|
||||
filteredSamples.forEach(sample => {
|
||||
const sampleElement = document.createElement('div');
|
||||
sampleElement.className = 'sample-item';
|
||||
sampleElement.dataset.id = sample.id || 'unknown';
|
||||
|
||||
const owner = sample.owner || 'Unknown';
|
||||
const type = sample.type || 'fingerprint';
|
||||
const quality = sample.quality || 0;
|
||||
const rating = sample.rating || this.getRatingFromQuality(quality);
|
||||
const collectedAt = sample.collectedAt || new Date().toISOString();
|
||||
|
||||
const qualityPercentage = Math.round(quality * 100);
|
||||
const timestamp = new Date(collectedAt);
|
||||
const formattedTime = timestamp.toLocaleDateString() + ' ' + timestamp.toLocaleTimeString();
|
||||
|
||||
sampleElement.innerHTML = `
|
||||
<div class="sample-header">
|
||||
<strong>${owner}</strong>
|
||||
<span class="sample-type">${type}</span>
|
||||
</div>
|
||||
<div class="sample-details">
|
||||
<span class="sample-quality quality-${rating.toLowerCase()}">${rating} (${qualityPercentage}%)</span>
|
||||
<span class="sample-date">${formattedTime}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
biometricsContent.appendChild(sampleElement);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
syncBiometricSamples() {
|
||||
if (!window.gameState) {
|
||||
window.gameState = {};
|
||||
}
|
||||
window.gameState.biometricSamples = this.biometricSamples;
|
||||
}
|
||||
|
||||
// Handle biometric scanner interaction (for unlocking doors, etc.)
|
||||
handleBiometricScan(scannerId, requiredOwner) {
|
||||
console.log('Biometric scan requested:', { scannerId, requiredOwner });
|
||||
|
||||
// Check if scanner is locked out
|
||||
if (this.scannerState.lockoutTimers[scannerId]) {
|
||||
const lockoutEnd = this.scannerState.lockoutTimers[scannerId];
|
||||
const now = Date.now();
|
||||
|
||||
if (now < lockoutEnd) {
|
||||
const remainingTime = Math.ceil((lockoutEnd - now) / 1000);
|
||||
if (window.gameAlert) {
|
||||
window.gameAlert(`Scanner locked out. Try again in ${remainingTime} seconds.`, 'error', 'Scanner Locked', 3000);
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
// Lockout expired, clear it
|
||||
delete this.scannerState.lockoutTimers[scannerId];
|
||||
delete this.scannerState.failedAttempts[scannerId];
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we have a matching biometric sample
|
||||
const matchingSample = this.biometricSamples.find(sample =>
|
||||
sample.owner === requiredOwner && sample.quality >= this.BIOMETRIC_QUALITY_THRESHOLD
|
||||
);
|
||||
|
||||
if (matchingSample) {
|
||||
console.log('Biometric scan successful:', matchingSample);
|
||||
|
||||
if (window.gameAlert) {
|
||||
window.gameAlert(`Biometric scan successful! Authenticated as ${requiredOwner}.`, 'success', 'Scan Successful', 4000);
|
||||
}
|
||||
|
||||
// Reset failed attempts on success
|
||||
delete this.scannerState.failedAttempts[scannerId];
|
||||
|
||||
return true;
|
||||
} else {
|
||||
console.log('Biometric scan failed');
|
||||
this.handleScannerFailure(scannerId);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
handleScannerFailure(scannerId) {
|
||||
// Initialize failed attempts if not exists
|
||||
if (!this.scannerState.failedAttempts[scannerId]) {
|
||||
this.scannerState.failedAttempts[scannerId] = 0;
|
||||
}
|
||||
|
||||
// Increment failed attempts
|
||||
this.scannerState.failedAttempts[scannerId]++;
|
||||
|
||||
// Check if we should lockout
|
||||
if (this.scannerState.failedAttempts[scannerId] >= this.MAX_FAILED_ATTEMPTS) {
|
||||
this.scannerState.lockoutTimers[scannerId] = Date.now() + this.SCANNER_LOCKOUT_TIME;
|
||||
if (window.gameAlert) {
|
||||
window.gameAlert(`Too many failed attempts. Scanner locked for ${this.SCANNER_LOCKOUT_TIME/1000} seconds.`, 'error', 'Scanner Locked', 5000);
|
||||
}
|
||||
} else {
|
||||
const remainingAttempts = this.MAX_FAILED_ATTEMPTS - this.scannerState.failedAttempts[scannerId];
|
||||
if (window.gameAlert) {
|
||||
window.gameAlert(`Scan failed. ${remainingAttempts} attempts remaining before lockout.`, 'warning', 'Scan Failed', 4000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
start() {
|
||||
super.start();
|
||||
console.log("Biometrics minigame started");
|
||||
|
||||
// Set up global interaction handler for fingerprint objects
|
||||
this.setupFingerprintInteractionHandler();
|
||||
}
|
||||
|
||||
setupFingerprintInteractionHandler() {
|
||||
// Store the original interaction handler
|
||||
this.originalInteractionHandler = window.handleObjectInteraction;
|
||||
|
||||
// Override the interaction handler to handle fingerprint collection
|
||||
window.handleObjectInteraction = (sprite) => {
|
||||
// Check if we're in searching mode and this object has fingerprints
|
||||
if (this.searchingMode && sprite.scenarioData && sprite.scenarioData.hasFingerprint === true) {
|
||||
|
||||
console.log('Collecting fingerprint from object:', sprite);
|
||||
this.collectFingerprintFromObject(sprite);
|
||||
return; // Don't call the original handler
|
||||
}
|
||||
|
||||
// Call the original handler for all other interactions
|
||||
if (this.originalInteractionHandler) {
|
||||
this.originalInteractionHandler(sprite);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
complete(success) {
|
||||
// Stop searching mode and clear highlights
|
||||
if (this.searchingMode) {
|
||||
this.toggleRoomSearching();
|
||||
}
|
||||
|
||||
// Sync final state
|
||||
this.syncBiometricSamples();
|
||||
|
||||
// Call parent complete with result
|
||||
super.complete(success, this.gameResult);
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
// Restore original interaction handler
|
||||
if (this.originalInteractionHandler) {
|
||||
window.handleObjectInteraction = this.originalInteractionHandler;
|
||||
}
|
||||
|
||||
// Clear highlights
|
||||
this.clearHighlights();
|
||||
|
||||
// Call parent cleanup
|
||||
super.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
// Function to start the biometrics minigame
|
||||
export function startBiometricsMinigame(item) {
|
||||
console.log('Starting biometrics minigame with:', { item });
|
||||
|
||||
// Make sure the minigame is registered
|
||||
if (window.MinigameFramework && !window.MinigameFramework.registeredScenes['biometrics']) {
|
||||
window.MinigameFramework.registerScene('biometrics', BiometricsMinigame);
|
||||
console.log('Biometrics minigame registered on demand');
|
||||
}
|
||||
|
||||
// Initialize the framework if not already done
|
||||
if (!window.MinigameFramework.mainGameScene && item && item.scene) {
|
||||
window.MinigameFramework.init(item.scene);
|
||||
}
|
||||
|
||||
// Start the biometrics minigame with proper parameters
|
||||
const params = {
|
||||
title: 'Biometric Scanner',
|
||||
item: item,
|
||||
onComplete: (success, result) => {
|
||||
console.log('Biometrics minigame completed with success:', success);
|
||||
}
|
||||
};
|
||||
|
||||
console.log('Starting biometrics minigame with params:', params);
|
||||
window.MinigameFramework.startMinigame('biometrics', null, params);
|
||||
}
|
||||
593
js/minigames/bluetooth/bluetooth-scanner-minigame.js
Normal file
593
js/minigames/bluetooth/bluetooth-scanner-minigame.js
Normal file
@@ -0,0 +1,593 @@
|
||||
import { MinigameScene } from '../framework/base-minigame.js';
|
||||
|
||||
// Bluetooth Scanner Minigame Scene implementation
|
||||
export class BluetoothScannerMinigame extends MinigameScene {
|
||||
constructor(container, params) {
|
||||
// Ensure params is defined before calling parent constructor
|
||||
params = params || {};
|
||||
|
||||
// Set default title if not provided
|
||||
if (!params.title) {
|
||||
params.title = 'Bluetooth Scanner';
|
||||
}
|
||||
|
||||
// Enable cancel button for bluetooth scanner minigame with custom text
|
||||
params.showCancel = true;
|
||||
params.cancelText = 'Close Scanner';
|
||||
|
||||
super(container, params);
|
||||
|
||||
this.item = params.item;
|
||||
this.bluetoothDevices = [];
|
||||
this.lastBluetoothPanelUpdate = 0;
|
||||
this.newBluetoothDevices = 0;
|
||||
this.scanInterval = null;
|
||||
|
||||
// Constants
|
||||
this.BLUETOOTH_SCAN_RANGE = 150; // pixels - 2 tiles range for Bluetooth scanning
|
||||
this.BLUETOOTH_SCAN_INTERVAL = 200; // Scan every 200ms for more responsive updates
|
||||
this.BLUETOOTH_UPDATE_THROTTLE = 100; // Update UI every 100ms max
|
||||
}
|
||||
|
||||
init() {
|
||||
// Call parent init to set up common components
|
||||
super.init();
|
||||
|
||||
console.log("Bluetooth scanner minigame initializing");
|
||||
|
||||
// Set container dimensions to be smaller than full screen
|
||||
this.container.className += ' bluetooth-scanner-minigame-container';
|
||||
|
||||
// Clear header content
|
||||
this.headerElement.innerHTML = '';
|
||||
|
||||
// Configure game container with scanner background
|
||||
this.gameContainer.className += ' bluetooth-scanner-minigame-game-container';
|
||||
|
||||
// Create scanner interface
|
||||
this.createScannerInterface();
|
||||
|
||||
// Initialize bluetooth devices from global state
|
||||
this.initializeBluetoothDevices();
|
||||
}
|
||||
|
||||
createScannerInterface() {
|
||||
// Create expand/collapse toggle button
|
||||
const expandToggle = document.createElement('div');
|
||||
expandToggle.className = 'bluetooth-scanner-expand-toggle';
|
||||
expandToggle.innerHTML = '▼';
|
||||
expandToggle.title = 'Expand/Collapse';
|
||||
|
||||
// Create scanner header
|
||||
const scannerHeader = document.createElement('div');
|
||||
scannerHeader.className = 'bluetooth-scanner-header';
|
||||
scannerHeader.innerHTML = `
|
||||
<div class="bluetooth-scanner-title">
|
||||
<img src="assets/objects/bluetooth_scanner.png" alt="Bluetooth Scanner" class="scanner-icon">
|
||||
<span>Bluetooth Scanner</span>
|
||||
</div>
|
||||
<div class="bluetooth-scanner-status">
|
||||
<div class="scanner-indicator active"></div>
|
||||
<span>Scanning...</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Create search and filter controls
|
||||
const controlsContainer = document.createElement('div');
|
||||
controlsContainer.className = 'bluetooth-scanner-controls';
|
||||
controlsContainer.innerHTML = `
|
||||
<div class="bluetooth-search-container">
|
||||
<input type="text" id="bluetooth-search" placeholder="Search devices..." class="bluetooth-search-input">
|
||||
</div>
|
||||
<div class="bluetooth-categories">
|
||||
<div class="bluetooth-category active" data-category="all">All</div>
|
||||
<div class="bluetooth-category" data-category="nearby">Nearby</div>
|
||||
<div class="bluetooth-category" data-category="saved">Saved</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Create device list container
|
||||
const deviceListContainer = document.createElement('div');
|
||||
deviceListContainer.className = 'bluetooth-device-list-container';
|
||||
deviceListContainer.innerHTML = `
|
||||
<div class="bluetooth-device-list-header">
|
||||
<span>Detected Devices</span>
|
||||
<div class="device-count">0 devices</div>
|
||||
</div>
|
||||
<div class="bluetooth-device-list" id="bluetooth-device-list"></div>
|
||||
`;
|
||||
|
||||
// Create instructions
|
||||
const instructionsContainer = document.createElement('div');
|
||||
instructionsContainer.className = 'bluetooth-scanner-instructions';
|
||||
instructionsContainer.innerHTML = `
|
||||
<div class="instruction-text">
|
||||
<strong>Instructions:</strong><br>
|
||||
• Walk around to detect Bluetooth devices<br>
|
||||
• Green signal bars indicate nearby devices<br>
|
||||
• Click devices to save them for later reference<br>
|
||||
• Devices in your inventory are always visible
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Assemble the interface
|
||||
this.gameContainer.appendChild(expandToggle);
|
||||
this.gameContainer.appendChild(scannerHeader);
|
||||
this.gameContainer.appendChild(controlsContainer);
|
||||
this.gameContainer.appendChild(deviceListContainer);
|
||||
this.gameContainer.appendChild(instructionsContainer);
|
||||
|
||||
// Set up event listeners
|
||||
this.setupEventListeners();
|
||||
|
||||
// Set up expand/collapse functionality
|
||||
this.setupExpandToggle(expandToggle);
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Search functionality
|
||||
const bluetoothSearch = document.getElementById('bluetooth-search');
|
||||
if (bluetoothSearch) {
|
||||
this.addEventListener(bluetoothSearch, 'input', () => this.updateBluetoothPanel());
|
||||
}
|
||||
|
||||
// Category filters
|
||||
const categories = this.gameContainer.querySelectorAll('.bluetooth-category');
|
||||
categories.forEach(category => {
|
||||
this.addEventListener(category, 'click', () => {
|
||||
// Remove active class from all categories
|
||||
categories.forEach(c => c.classList.remove('active'));
|
||||
// Add active class to clicked category
|
||||
category.classList.add('active');
|
||||
// Update bluetooth panel
|
||||
this.updateBluetoothPanel();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setupExpandToggle(expandToggle) {
|
||||
this.addEventListener(expandToggle, 'click', () => {
|
||||
const isExpanded = this.container.classList.contains('expanded');
|
||||
|
||||
if (isExpanded) {
|
||||
// Collapse
|
||||
this.container.classList.remove('expanded');
|
||||
expandToggle.innerHTML = '▼';
|
||||
expandToggle.title = 'Expand';
|
||||
} else {
|
||||
// Expand
|
||||
this.container.classList.add('expanded');
|
||||
expandToggle.innerHTML = '▲';
|
||||
expandToggle.title = 'Collapse';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initializeBluetoothDevices() {
|
||||
// Initialize from global state if available
|
||||
if (window.gameState && window.gameState.bluetoothDevices) {
|
||||
this.bluetoothDevices = [...window.gameState.bluetoothDevices];
|
||||
} else {
|
||||
this.bluetoothDevices = [];
|
||||
}
|
||||
|
||||
// Start scanning for devices
|
||||
this.startScanning();
|
||||
|
||||
// Update the panel
|
||||
this.updateBluetoothPanel();
|
||||
}
|
||||
|
||||
startScanning() {
|
||||
// Start the scanning interval
|
||||
this.scanInterval = setInterval(() => {
|
||||
this.checkBluetoothDevices();
|
||||
}, this.BLUETOOTH_SCAN_INTERVAL);
|
||||
|
||||
console.log('Bluetooth scanning started');
|
||||
}
|
||||
|
||||
stopScanning() {
|
||||
if (this.scanInterval) {
|
||||
clearInterval(this.scanInterval);
|
||||
this.scanInterval = null;
|
||||
console.log('Bluetooth scanning stopped');
|
||||
}
|
||||
}
|
||||
|
||||
checkBluetoothDevices() {
|
||||
// Find all Bluetooth devices in the current room
|
||||
if (!window.currentPlayerRoom || !window.rooms[window.currentPlayerRoom] || !window.rooms[window.currentPlayerRoom].objects) {
|
||||
return;
|
||||
}
|
||||
|
||||
const room = window.rooms[window.currentPlayerRoom];
|
||||
const player = window.player;
|
||||
if (!player) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Keep track of devices detected in this scan
|
||||
const detectedDevices = new Set();
|
||||
let needsUpdate = false;
|
||||
|
||||
Object.values(room.objects).forEach(obj => {
|
||||
if (obj.scenarioData?.lockType === "bluetooth") {
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(player.x - obj.x, 2) + Math.pow(player.y - obj.y, 2)
|
||||
);
|
||||
|
||||
const deviceMac = obj.scenarioData?.mac || "Unknown";
|
||||
const deviceName = obj.scenarioData?.name || "Unknown Device";
|
||||
|
||||
if (distance <= this.BLUETOOTH_SCAN_RANGE) {
|
||||
detectedDevices.add(`${deviceMac}|${deviceName}`); // Use combination for uniqueness
|
||||
|
||||
// Add to Bluetooth scanner panel
|
||||
const signalStrengthPercentage = Math.max(0, Math.round(100 - (distance / this.BLUETOOTH_SCAN_RANGE * 100)));
|
||||
// Convert percentage to dBm format (-100 to -30 dBm range)
|
||||
const signalStrength = Math.round(-100 + (signalStrengthPercentage * 0.7)); // -100 to -30 dBm
|
||||
const details = `Type: ${obj.scenarioData?.type || "Unknown"}\nDistance: ${Math.round(distance)} units\nSignal Strength: ${signalStrength}dBm (${signalStrengthPercentage}%)`;
|
||||
|
||||
// Check if device already exists in our list (by MAC + name combination for uniqueness)
|
||||
const existingDevice = this.bluetoothDevices.find(device =>
|
||||
device.mac === deviceMac && device.name === deviceName
|
||||
);
|
||||
|
||||
if (existingDevice) {
|
||||
// Update existing device details with real-time data
|
||||
const wasNearby = existingDevice.nearby;
|
||||
const oldSignalStrengthPercentage = existingDevice.signalStrengthPercentage || 0;
|
||||
|
||||
existingDevice.details = details;
|
||||
existingDevice.lastSeen = new Date();
|
||||
existingDevice.nearby = true;
|
||||
existingDevice.signalStrength = signalStrength;
|
||||
existingDevice.signalStrengthPercentage = signalStrengthPercentage;
|
||||
|
||||
// Always update if device came back into range or signal strength changed significantly
|
||||
if (!wasNearby || Math.abs(oldSignalStrengthPercentage - signalStrengthPercentage) > 5) {
|
||||
needsUpdate = true;
|
||||
}
|
||||
} else {
|
||||
// Add as new device if not already in our list
|
||||
const newDevice = this.addBluetoothDevice(deviceName, deviceMac, details, true);
|
||||
if (newDevice) {
|
||||
newDevice.signalStrength = signalStrength;
|
||||
newDevice.signalStrengthPercentage = signalStrengthPercentage;
|
||||
needsUpdate = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Mark devices that weren't detected in this scan as not nearby
|
||||
this.bluetoothDevices.forEach(device => {
|
||||
const deviceKey = `${device.mac}|${device.name}`;
|
||||
if (device.nearby && !detectedDevices.has(deviceKey)) {
|
||||
device.nearby = false;
|
||||
device.lastSeen = new Date();
|
||||
needsUpdate = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Always update the count and sync devices when there are changes
|
||||
if (needsUpdate) {
|
||||
this.updateBluetoothCount();
|
||||
this.syncBluetoothDevices();
|
||||
|
||||
// Update the panel UI
|
||||
const now = Date.now();
|
||||
if (now - this.lastBluetoothPanelUpdate > this.BLUETOOTH_UPDATE_THROTTLE) {
|
||||
this.updateBluetoothPanel();
|
||||
this.lastBluetoothPanelUpdate = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addBluetoothDevice(name, mac, details = "", nearby = true) {
|
||||
// Check if a device with the same MAC + name combination already exists
|
||||
const deviceExists = this.bluetoothDevices.some(device => device.mac === mac && device.name === name);
|
||||
|
||||
// If the device already exists, update its nearby status
|
||||
if (deviceExists) {
|
||||
const existingDevice = this.bluetoothDevices.find(device => device.mac === mac && device.name === name);
|
||||
existingDevice.nearby = nearby;
|
||||
existingDevice.lastSeen = new Date();
|
||||
this.updateBluetoothPanel();
|
||||
this.syncBluetoothDevices();
|
||||
return null;
|
||||
}
|
||||
|
||||
const device = {
|
||||
id: Date.now(),
|
||||
name: name,
|
||||
mac: mac,
|
||||
details: details,
|
||||
nearby: nearby,
|
||||
saved: false,
|
||||
firstSeen: new Date(),
|
||||
lastSeen: new Date(),
|
||||
signalStrength: -100, // Default to weak signal (-100 dBm)
|
||||
signalStrengthPercentage: 0 // Default to 0% for visual display
|
||||
};
|
||||
|
||||
this.bluetoothDevices.push(device);
|
||||
this.updateBluetoothPanel();
|
||||
this.updateBluetoothCount();
|
||||
this.syncBluetoothDevices();
|
||||
|
||||
return device;
|
||||
}
|
||||
|
||||
updateBluetoothPanel() {
|
||||
const bluetoothContent = document.getElementById('bluetooth-device-list');
|
||||
if (!bluetoothContent) return;
|
||||
|
||||
const searchTerm = document.getElementById('bluetooth-search')?.value?.toLowerCase() || '';
|
||||
|
||||
// Get active category
|
||||
const activeCategory = this.gameContainer.querySelector('.bluetooth-category.active')?.dataset.category || 'all';
|
||||
|
||||
// Store the currently hovered device, if any
|
||||
const hoveredDevice = bluetoothContent.querySelector('.bluetooth-device:hover');
|
||||
const hoveredDeviceId = hoveredDevice ? hoveredDevice.dataset.id : null;
|
||||
|
||||
// Add Bluetooth-locked items from inventory to the main bluetoothDevices array
|
||||
if (window.inventory && window.inventory.items) {
|
||||
window.inventory.items.forEach(item => {
|
||||
if (item.scenarioData?.lockType === "bluetooth" && item.scenarioData?.locked) {
|
||||
// Check if this device is already in our list
|
||||
const deviceMac = item.scenarioData?.mac || "Unknown";
|
||||
|
||||
// Normalize MAC address format (ensure lowercase for comparison)
|
||||
const normalizedMac = deviceMac.toLowerCase();
|
||||
|
||||
// Check if device already exists in our list (by MAC + name combination)
|
||||
const deviceName = item.scenarioData?.name || item.name || "Unknown Device";
|
||||
const existingDeviceIndex = this.bluetoothDevices.findIndex(device =>
|
||||
device.mac.toLowerCase() === normalizedMac && device.name === deviceName
|
||||
);
|
||||
|
||||
if (existingDeviceIndex === -1) {
|
||||
// Add as a new device
|
||||
const details = `Type: ${item.scenarioData?.type || "Unknown"}\nLocation: Inventory\nStatus: Locked`;
|
||||
|
||||
const newDevice = {
|
||||
id: `inv_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
name: deviceName,
|
||||
mac: deviceMac,
|
||||
details: details,
|
||||
lastSeen: new Date(),
|
||||
nearby: true, // Always nearby since it's in inventory
|
||||
saved: true, // Auto-save inventory items
|
||||
signalStrength: -30, // Max strength for inventory items (-30 dBm)
|
||||
signalStrengthPercentage: 100, // 100% for visual display
|
||||
inInventory: true // Mark as inventory item
|
||||
};
|
||||
|
||||
// Add to the main bluetoothDevices array
|
||||
this.bluetoothDevices.push(newDevice);
|
||||
console.log('Added inventory device to bluetoothDevices:', newDevice);
|
||||
this.syncBluetoothDevices();
|
||||
} else {
|
||||
// Update existing device
|
||||
const existingDevice = this.bluetoothDevices[existingDeviceIndex];
|
||||
existingDevice.inInventory = true;
|
||||
existingDevice.nearby = true;
|
||||
existingDevice.signalStrength = -30; // -30 dBm for inventory items
|
||||
existingDevice.signalStrengthPercentage = 100; // 100% for visual display
|
||||
existingDevice.lastSeen = new Date();
|
||||
existingDevice.details = `Type: ${item.scenarioData?.type || "Unknown"}\nLocation: Inventory\nStatus: Locked`;
|
||||
console.log('Updated existing device with inventory info:', existingDevice);
|
||||
this.syncBluetoothDevices();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Filter devices based on search and category
|
||||
let filteredDevices = [...this.bluetoothDevices];
|
||||
|
||||
// Apply category filter
|
||||
if (activeCategory === 'nearby') {
|
||||
filteredDevices = filteredDevices.filter(device => device.nearby);
|
||||
} else if (activeCategory === 'saved') {
|
||||
filteredDevices = filteredDevices.filter(device => device.saved);
|
||||
}
|
||||
|
||||
// Apply search filter
|
||||
if (searchTerm) {
|
||||
filteredDevices = filteredDevices.filter(device =>
|
||||
device.name.toLowerCase().includes(searchTerm) ||
|
||||
device.mac.toLowerCase().includes(searchTerm) ||
|
||||
device.details.toLowerCase().includes(searchTerm)
|
||||
);
|
||||
}
|
||||
|
||||
// Sort devices with inventory items first, then nearby ones, then by signal strength
|
||||
filteredDevices.sort((a, b) => {
|
||||
// Inventory items first
|
||||
if (a.inInventory !== b.inInventory) {
|
||||
return a.inInventory ? -1 : 1;
|
||||
}
|
||||
|
||||
// Then nearby items
|
||||
if (a.nearby !== b.nearby) {
|
||||
return a.nearby ? -1 : 1;
|
||||
}
|
||||
|
||||
// For nearby devices, sort by signal strength
|
||||
if (a.nearby && b.nearby && a.signalStrength !== b.signalStrength) {
|
||||
return b.signalStrength - a.signalStrength;
|
||||
}
|
||||
|
||||
return new Date(b.lastSeen) - new Date(a.lastSeen);
|
||||
});
|
||||
|
||||
// Update device count
|
||||
const deviceCount = this.gameContainer.querySelector('.device-count');
|
||||
if (deviceCount) {
|
||||
deviceCount.textContent = `${filteredDevices.length} device${filteredDevices.length !== 1 ? 's' : ''}`;
|
||||
}
|
||||
|
||||
// Clear current content
|
||||
bluetoothContent.innerHTML = '';
|
||||
|
||||
// Add devices
|
||||
if (filteredDevices.length === 0) {
|
||||
if (searchTerm) {
|
||||
bluetoothContent.innerHTML = '<div class="bluetooth-device">No devices match your search.</div>';
|
||||
} else if (activeCategory !== 'all') {
|
||||
bluetoothContent.innerHTML = `<div class="bluetooth-device">No ${activeCategory} devices found.</div>`;
|
||||
} else {
|
||||
bluetoothContent.innerHTML = '<div class="bluetooth-device">No devices detected yet. Walk around to find Bluetooth devices.</div>';
|
||||
}
|
||||
} else {
|
||||
filteredDevices.forEach(device => {
|
||||
const deviceElement = document.createElement('div');
|
||||
deviceElement.className = 'bluetooth-device';
|
||||
deviceElement.dataset.id = device.id;
|
||||
|
||||
// If this was the hovered device, add the hover class
|
||||
if (hoveredDeviceId && device.id === hoveredDeviceId) {
|
||||
deviceElement.classList.add('hover-preserved');
|
||||
}
|
||||
|
||||
// Format the timestamp
|
||||
const timestamp = new Date(device.lastSeen);
|
||||
const formattedDate = timestamp.toLocaleDateString();
|
||||
const formattedTime = timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
|
||||
// Get signal color based on strength
|
||||
const getSignalColor = (strength) => {
|
||||
if (strength >= 80) return '#00cc00'; // Strong - green
|
||||
if (strength >= 50) return '#cccc00'; // Medium - yellow
|
||||
return '#cc5500'; // Weak - orange
|
||||
};
|
||||
|
||||
let deviceContent = `<div class="bluetooth-device-name">
|
||||
<span>${device.name}</span>
|
||||
<div class="bluetooth-device-icons">`;
|
||||
|
||||
if (device.nearby && typeof device.signalStrength === 'number') {
|
||||
// Use percentage for visual display
|
||||
const signalPercentage = device.signalStrengthPercentage || Math.max(0, Math.round(((device.signalStrength + 100) / 70) * 100));
|
||||
const signalColor = getSignalColor(signalPercentage);
|
||||
|
||||
// Calculate how many bars should be active based on signal strength percentage
|
||||
const activeBars = Math.ceil(signalPercentage / 20); // 0-20% = 1 bar, 21-40% = 2 bars, etc.
|
||||
|
||||
deviceContent += `<div class="bluetooth-signal-bar-container">
|
||||
<div class="bluetooth-signal-bars">`;
|
||||
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
const isActive = i <= activeBars;
|
||||
deviceContent += `<div class="bluetooth-signal-bar ${isActive ? 'active' : ''}" style="color: ${signalColor};"></div>`;
|
||||
}
|
||||
|
||||
deviceContent += `</div></div>`;
|
||||
} else if (device.nearby) {
|
||||
// Fallback if signal strength not available
|
||||
deviceContent += `<span class="bluetooth-device-icon">📶</span>`;
|
||||
}
|
||||
|
||||
if (device.saved) {
|
||||
deviceContent += `<span class="bluetooth-device-icon">💾</span>`;
|
||||
}
|
||||
|
||||
if (device.inInventory) {
|
||||
deviceContent += `<span class="bluetooth-device-icon">🎒</span>`;
|
||||
}
|
||||
|
||||
deviceContent += `</div></div>`;
|
||||
deviceContent += `<div class="bluetooth-device-details">MAC: ${device.mac}\n${device.details}</div>`;
|
||||
deviceContent += `<div class="bluetooth-device-timestamp">Last seen: ${formattedDate} ${formattedTime}</div>`;
|
||||
|
||||
deviceElement.innerHTML = deviceContent;
|
||||
|
||||
// Toggle expanded state when clicked
|
||||
this.addEventListener(deviceElement, 'click', (event) => {
|
||||
deviceElement.classList.toggle('expanded');
|
||||
|
||||
// Mark as saved when expanded
|
||||
if (!device.saved && deviceElement.classList.contains('expanded')) {
|
||||
device.saved = true;
|
||||
this.updateBluetoothCount();
|
||||
this.updateBluetoothPanel();
|
||||
this.syncBluetoothDevices();
|
||||
}
|
||||
});
|
||||
|
||||
bluetoothContent.appendChild(deviceElement);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
updateBluetoothCount() {
|
||||
this.newBluetoothDevices = this.bluetoothDevices.filter(device => !device.saved && device.nearby).length;
|
||||
}
|
||||
|
||||
syncBluetoothDevices() {
|
||||
if (!window.gameState) {
|
||||
window.gameState = {};
|
||||
}
|
||||
window.gameState.bluetoothDevices = this.bluetoothDevices;
|
||||
}
|
||||
|
||||
start() {
|
||||
super.start();
|
||||
console.log("Bluetooth scanner minigame started");
|
||||
|
||||
// Start scanning
|
||||
this.startScanning();
|
||||
}
|
||||
|
||||
complete(success) {
|
||||
// Stop scanning when minigame ends
|
||||
this.stopScanning();
|
||||
|
||||
// Sync final state
|
||||
this.syncBluetoothDevices();
|
||||
|
||||
// Call parent complete with result
|
||||
super.complete(success, this.gameResult);
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
// Stop scanning
|
||||
this.stopScanning();
|
||||
|
||||
// Call parent cleanup
|
||||
super.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
// Function to start the bluetooth scanner minigame
|
||||
export function startBluetoothScannerMinigame(item) {
|
||||
console.log('Starting bluetooth scanner minigame with:', { item });
|
||||
|
||||
// Make sure the minigame is registered
|
||||
if (window.MinigameFramework && !window.MinigameFramework.registeredScenes['bluetooth-scanner']) {
|
||||
window.MinigameFramework.registerScene('bluetooth-scanner', BluetoothScannerMinigame);
|
||||
console.log('Bluetooth scanner minigame registered on demand');
|
||||
}
|
||||
|
||||
// Initialize the framework if not already done
|
||||
if (!window.MinigameFramework.mainGameScene && item && item.scene) {
|
||||
window.MinigameFramework.init(item.scene);
|
||||
}
|
||||
|
||||
// Start the bluetooth scanner minigame with proper parameters
|
||||
const params = {
|
||||
title: 'Bluetooth Scanner',
|
||||
item: item,
|
||||
onComplete: (success, result) => {
|
||||
console.log('Bluetooth scanner minigame completed with success:', success);
|
||||
}
|
||||
};
|
||||
|
||||
console.log('Starting bluetooth scanner minigame with params:', params);
|
||||
window.MinigameFramework.startMinigame('bluetooth-scanner', null, params);
|
||||
}
|
||||
@@ -6,6 +6,9 @@ export { MinigameScene } from './framework/base-minigame.js';
|
||||
export { LockpickingMinigamePhaser } from './lockpicking/lockpicking-game-phaser.js';
|
||||
export { DustingMinigame } from './dusting/dusting-game.js';
|
||||
export { NotesMinigame, startNotesMinigame, showMissionBrief } from './notes/notes-minigame.js';
|
||||
export { BluetoothScannerMinigame, startBluetoothScannerMinigame } from './bluetooth/bluetooth-scanner-minigame.js';
|
||||
export { BiometricsMinigame, startBiometricsMinigame } from './biometrics/biometrics-minigame.js';
|
||||
export { LockpickSetMinigame, startLockpickSetMinigame } from './lockpick/lockpick-set-minigame.js';
|
||||
|
||||
// Initialize the global minigame framework for backward compatibility
|
||||
import { MinigameFramework } from './framework/minigame-manager.js';
|
||||
@@ -20,12 +23,27 @@ import { DustingMinigame } from './dusting/dusting-game.js';
|
||||
// Import the notes minigame
|
||||
import { NotesMinigame, startNotesMinigame, showMissionBrief } from './notes/notes-minigame.js';
|
||||
|
||||
// Import the bluetooth scanner minigame
|
||||
import { BluetoothScannerMinigame, startBluetoothScannerMinigame } from './bluetooth/bluetooth-scanner-minigame.js';
|
||||
|
||||
// Import the biometrics minigame
|
||||
import { BiometricsMinigame, startBiometricsMinigame } from './biometrics/biometrics-minigame.js';
|
||||
|
||||
// Import the lockpick set minigame
|
||||
import { LockpickSetMinigame, startLockpickSetMinigame } from './lockpick/lockpick-set-minigame.js';
|
||||
|
||||
// Register minigames
|
||||
MinigameFramework.registerScene('lockpicking', LockpickingMinigamePhaser); // Use Phaser version as default
|
||||
MinigameFramework.registerScene('lockpicking-phaser', LockpickingMinigamePhaser); // Keep explicit phaser name
|
||||
MinigameFramework.registerScene('dusting', DustingMinigame);
|
||||
MinigameFramework.registerScene('notes', NotesMinigame);
|
||||
MinigameFramework.registerScene('bluetooth-scanner', BluetoothScannerMinigame);
|
||||
MinigameFramework.registerScene('biometrics', BiometricsMinigame);
|
||||
MinigameFramework.registerScene('lockpick-set', LockpickSetMinigame);
|
||||
|
||||
// Make notes minigame functions available globally
|
||||
// Make minigame functions available globally
|
||||
window.startNotesMinigame = startNotesMinigame;
|
||||
window.showMissionBrief = showMissionBrief;
|
||||
window.showMissionBrief = showMissionBrief;
|
||||
window.startBluetoothScannerMinigame = startBluetoothScannerMinigame;
|
||||
window.startBiometricsMinigame = startBiometricsMinigame;
|
||||
window.startLockpickSetMinigame = startLockpickSetMinigame;
|
||||
325
js/minigames/lockpick/lockpick-set-minigame.js
Normal file
325
js/minigames/lockpick/lockpick-set-minigame.js
Normal file
@@ -0,0 +1,325 @@
|
||||
import { MinigameScene } from '../framework/base-minigame.js';
|
||||
|
||||
// Lockpick Set Minigame Scene implementation
|
||||
export class LockpickSetMinigame extends MinigameScene {
|
||||
constructor(container, params) {
|
||||
// Ensure params is defined before calling parent constructor
|
||||
params = params || {};
|
||||
|
||||
// Set default title if not provided
|
||||
params.title = 'Lockpick Set';
|
||||
|
||||
// Enable cancel button for lockpick minigame with custom text
|
||||
params.showCancel = true;
|
||||
params.cancelText = 'Close';
|
||||
|
||||
super(container, params);
|
||||
|
||||
this.item = params.item;
|
||||
this.searchingMode = false;
|
||||
this.highlightedObjects = [];
|
||||
}
|
||||
|
||||
init() {
|
||||
// Call parent init to set up common components
|
||||
super.init();
|
||||
|
||||
console.log("Lockpick set minigame initializing");
|
||||
|
||||
// Set container dimensions to be compact
|
||||
this.container.className += ' lockpick-set-minigame-container';
|
||||
|
||||
// Clear header content
|
||||
this.headerElement.innerHTML = '';
|
||||
|
||||
// Configure game container
|
||||
this.gameContainer.className += ' lockpick-set-minigame-game-container';
|
||||
|
||||
// Create lockpick interface
|
||||
this.createLockpickInterface();
|
||||
}
|
||||
|
||||
createLockpickInterface() {
|
||||
// Create expand/collapse toggle button
|
||||
const expandToggle = document.createElement('div');
|
||||
expandToggle.className = 'lockpick-expand-toggle';
|
||||
expandToggle.innerHTML = '▼';
|
||||
expandToggle.title = 'Expand/Collapse';
|
||||
|
||||
// Create search room button (at the top)
|
||||
const searchRoomContainer = document.createElement('div');
|
||||
searchRoomContainer.className = 'lockpick-search-room-container';
|
||||
searchRoomContainer.innerHTML = `
|
||||
<button id="search-locks-btn" class="lockpick-action-btn">
|
||||
<span class="btn-icon">🔍</span>
|
||||
<span class="btn-text">Search for Pickable Locks</span>
|
||||
</button>
|
||||
`;
|
||||
|
||||
// Create lockpick header
|
||||
const lockpickHeader = document.createElement('div');
|
||||
lockpickHeader.className = 'lockpick-set-header';
|
||||
lockpickHeader.innerHTML = `
|
||||
<div class="lockpick-set-title">
|
||||
<img src="assets/objects/lockpick.png" alt="Lockpick Set" class="lockpick-icon">
|
||||
<span>Lockpick Set</span>
|
||||
</div>
|
||||
<div class="lockpick-set-status">
|
||||
<div class="lockpick-indicator active"></div>
|
||||
<span>Ready</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Create instructions
|
||||
const instructionsContainer = document.createElement('div');
|
||||
instructionsContainer.className = 'lockpick-set-instructions';
|
||||
instructionsContainer.innerHTML = `
|
||||
<div class="instruction-text">
|
||||
<strong>Instructions:</strong><br>
|
||||
• Use "Search for Pickable Locks" to highlight locks in the room<br>
|
||||
• Click highlighted locks to attempt lockpicking<br>
|
||||
• Different locks have different difficulty levels<br>
|
||||
• Higher skill and better tools improve success rates
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Assemble the interface
|
||||
this.gameContainer.appendChild(expandToggle);
|
||||
this.gameContainer.appendChild(searchRoomContainer);
|
||||
this.gameContainer.appendChild(lockpickHeader);
|
||||
this.gameContainer.appendChild(instructionsContainer);
|
||||
|
||||
// Set up event listeners
|
||||
this.setupEventListeners();
|
||||
|
||||
// Set up expand/collapse functionality
|
||||
this.setupExpandToggle(expandToggle);
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Search locks button
|
||||
const searchLocksBtn = document.getElementById('search-locks-btn');
|
||||
if (searchLocksBtn) {
|
||||
this.addEventListener(searchLocksBtn, 'click', () => this.toggleLockSearching());
|
||||
}
|
||||
}
|
||||
|
||||
setupExpandToggle(expandToggle) {
|
||||
this.addEventListener(expandToggle, 'click', () => {
|
||||
const isExpanded = this.container.classList.contains('expanded');
|
||||
|
||||
if (isExpanded) {
|
||||
// Collapse
|
||||
this.container.classList.remove('expanded');
|
||||
expandToggle.innerHTML = '▼';
|
||||
expandToggle.title = 'Expand';
|
||||
} else {
|
||||
// Expand
|
||||
this.container.classList.add('expanded');
|
||||
expandToggle.innerHTML = '▲';
|
||||
expandToggle.title = 'Collapse';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toggleLockSearching() {
|
||||
this.searchingMode = !this.searchingMode;
|
||||
const searchBtn = document.getElementById('search-locks-btn');
|
||||
|
||||
if (this.searchingMode) {
|
||||
// Start searching mode
|
||||
searchBtn.classList.add('active');
|
||||
searchBtn.querySelector('.btn-text').textContent = 'Stop Searching';
|
||||
this.highlightPickableLocks();
|
||||
console.log('Lock searching started');
|
||||
} else {
|
||||
// Stop searching mode
|
||||
searchBtn.classList.remove('active');
|
||||
searchBtn.querySelector('.btn-text').textContent = 'Search for Pickable Locks';
|
||||
this.clearHighlights();
|
||||
console.log('Lock searching stopped');
|
||||
}
|
||||
}
|
||||
|
||||
highlightPickableLocks() {
|
||||
// Clear existing highlights
|
||||
this.clearHighlights();
|
||||
|
||||
// Find all objects in the current room that have pickable locks
|
||||
if (!window.currentPlayerRoom || !window.rooms[window.currentPlayerRoom]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const room = window.rooms[window.currentPlayerRoom];
|
||||
this.highlightedObjects = [];
|
||||
|
||||
// Check regular objects
|
||||
if (room.objects) {
|
||||
Object.values(room.objects).forEach(obj => {
|
||||
// Check if object has a lock that can be picked (key locks can be picked)
|
||||
if (obj.scenarioData?.lockType === 'key' || obj.scenarioData?.pickable === true) {
|
||||
// Add highlight effect to the object
|
||||
if (obj.setTint) {
|
||||
obj.setTint(0x00ff00); // Green tint for pickable locks
|
||||
this.highlightedObjects.push(obj);
|
||||
}
|
||||
|
||||
// Add a visual indicator
|
||||
this.addLockpickIndicator(obj);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Check doors (they're stored separately)
|
||||
if (room.doors) {
|
||||
Object.values(room.doors).forEach(door => {
|
||||
// Check if door has a lock that can be picked
|
||||
if (door.properties?.lockType === 'key' || door.scenarioData?.lockType === 'key') {
|
||||
// Add highlight effect to the door
|
||||
if (door.setTint) {
|
||||
door.setTint(0x00ff00); // Green tint for pickable locks
|
||||
this.highlightedObjects.push(door);
|
||||
}
|
||||
|
||||
// Add a visual indicator
|
||||
this.addLockpickIndicator(door);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (this.highlightedObjects.length > 0) {
|
||||
console.log(`Highlighted ${this.highlightedObjects.length} pickable locks:`, this.highlightedObjects.map(obj => ({
|
||||
id: obj.objectId,
|
||||
name: obj.scenarioData?.name,
|
||||
type: obj.scenarioData?.type,
|
||||
lockType: obj.scenarioData?.lockType
|
||||
})));
|
||||
} else {
|
||||
console.log('No pickable locks found in this room');
|
||||
}
|
||||
}
|
||||
|
||||
addLockpickIndicator(obj) {
|
||||
// Create a lockpick image indicator directly over the object
|
||||
if (obj.scene && obj.scene.add) {
|
||||
const indicator = obj.scene.add.image(obj.x, obj.y, 'lockpick');
|
||||
indicator.setDepth(1000); // High depth to appear on top
|
||||
indicator.setOrigin(-0.25, 0);
|
||||
indicator.setTint(0x00ff00); // Green tint
|
||||
|
||||
// Add pulsing animation
|
||||
obj.scene.tweens.add({
|
||||
targets: indicator,
|
||||
alpha: { from: 1, to: 0.3 },
|
||||
duration: 1000,
|
||||
yoyo: true,
|
||||
repeat: -1
|
||||
});
|
||||
|
||||
// Store reference for cleanup
|
||||
obj.lockpickIndicator = indicator;
|
||||
}
|
||||
}
|
||||
|
||||
clearHighlights() {
|
||||
// Remove highlights from all objects
|
||||
this.highlightedObjects.forEach(obj => {
|
||||
if (obj.clearTint) {
|
||||
obj.clearTint();
|
||||
}
|
||||
if (obj.lockpickIndicator) {
|
||||
obj.lockpickIndicator.destroy();
|
||||
delete obj.lockpickIndicator;
|
||||
}
|
||||
});
|
||||
this.highlightedObjects = [];
|
||||
}
|
||||
|
||||
start() {
|
||||
super.start();
|
||||
console.log("Lockpick set minigame started");
|
||||
|
||||
// Override the cancel button to just close without completion logic
|
||||
if (this.cancelButton) {
|
||||
this.cancelButton.onclick = () => {
|
||||
console.log("Closing lockpick set tool");
|
||||
this.complete(true);
|
||||
};
|
||||
}
|
||||
|
||||
// No need to override interaction handler - just highlight objects
|
||||
// The normal interaction system will handle clicking on highlighted objects
|
||||
}
|
||||
|
||||
attemptLockpicking(obj) {
|
||||
// Start the actual lockpicking minigame using the existing system
|
||||
if (window.startLockpickingMinigame) {
|
||||
console.log('Starting lockpicking minigame for object:', obj);
|
||||
|
||||
// Get difficulty from object data
|
||||
const difficulty = obj.scenarioData?.difficulty || obj.scenarioData?.lockDifficulty || 'medium';
|
||||
|
||||
// Use the existing lockpicking system
|
||||
window.startLockpickingMinigame(obj, window.game, difficulty, (success) => {
|
||||
if (success) {
|
||||
console.log('Lockpicking successful');
|
||||
// The existing system should handle unlocking
|
||||
} else {
|
||||
console.log('Lockpicking failed');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.error('Lockpicking minigame not available');
|
||||
if (window.gameAlert) {
|
||||
window.gameAlert('Lockpicking minigame not available', 'error', 'Error', 3000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
complete(success) {
|
||||
// Stop searching mode and clear highlights
|
||||
if (this.searchingMode) {
|
||||
this.toggleLockSearching();
|
||||
}
|
||||
|
||||
// For the lockpick set minigame, we don't need success/failure logic
|
||||
// Just close the minigame without any completion state
|
||||
super.complete(true, null);
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
// Clear highlights
|
||||
this.clearHighlights();
|
||||
|
||||
// Call parent cleanup
|
||||
super.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
// Function to start the lockpick set minigame
|
||||
export function startLockpickSetMinigame(item) {
|
||||
console.log('Starting lockpick set minigame with:', { item });
|
||||
|
||||
// Make sure the minigame is registered
|
||||
if (window.MinigameFramework && !window.MinigameFramework.registeredScenes['lockpick-set']) {
|
||||
window.MinigameFramework.registerScene('lockpick-set', LockpickSetMinigame);
|
||||
console.log('Lockpick set minigame registered on demand');
|
||||
}
|
||||
|
||||
// Initialize the framework if not already done
|
||||
if (!window.MinigameFramework.mainGameScene && item && item.scene) {
|
||||
window.MinigameFramework.init(item.scene);
|
||||
}
|
||||
|
||||
// Start the lockpick set minigame with proper parameters
|
||||
const params = {
|
||||
title: 'Lockpick Set',
|
||||
item: item,
|
||||
onComplete: (success, result) => {
|
||||
console.log('Lockpick set minigame completed with success:', success);
|
||||
}
|
||||
};
|
||||
|
||||
console.log('Starting lockpick set minigame with params:', params);
|
||||
window.MinigameFramework.startMinigame('lockpick-set', null, params);
|
||||
}
|
||||
@@ -1,375 +0,0 @@
|
||||
// Biometrics System
|
||||
// Handles biometric sample collection and fingerprint scanning
|
||||
|
||||
// Initialize the biometrics system
|
||||
export function initializeBiometricsPanel() {
|
||||
console.log('Biometrics system initialized');
|
||||
|
||||
// Set up biometric scanner state
|
||||
if (!window.gameState.biometricSamples) {
|
||||
window.gameState.biometricSamples = [];
|
||||
}
|
||||
|
||||
// Scanner state management
|
||||
window.scannerState = {
|
||||
failedAttempts: {},
|
||||
lockoutTimers: {}
|
||||
};
|
||||
|
||||
// Scanner constants
|
||||
window.MAX_FAILED_ATTEMPTS = 3;
|
||||
window.SCANNER_LOCKOUT_TIME = 30000; // 30 seconds
|
||||
window.BIOMETRIC_QUALITY_THRESHOLD = 0.7;
|
||||
|
||||
// Initialize biometric panel UI
|
||||
setupBiometricPanel();
|
||||
|
||||
// Set up biometrics toggle button
|
||||
const biometricsToggle = document.getElementById('biometrics-toggle');
|
||||
if (biometricsToggle) {
|
||||
biometricsToggle.addEventListener('click', toggleBiometricsPanel);
|
||||
}
|
||||
|
||||
// Set up biometrics close button
|
||||
const biometricsClose = document.getElementById('biometrics-close');
|
||||
if (biometricsClose) {
|
||||
biometricsClose.addEventListener('click', toggleBiometricsPanel);
|
||||
}
|
||||
|
||||
// Set up search functionality
|
||||
const biometricsSearch = document.getElementById('biometrics-search');
|
||||
if (biometricsSearch) {
|
||||
biometricsSearch.addEventListener('input', updateBiometricsPanel);
|
||||
}
|
||||
|
||||
// Set up category filters
|
||||
const categories = document.querySelectorAll('.biometrics-category');
|
||||
categories.forEach(category => {
|
||||
category.addEventListener('click', () => {
|
||||
// Remove active class from all categories
|
||||
categories.forEach(c => c.classList.remove('active'));
|
||||
// Add active class to clicked category
|
||||
category.classList.add('active');
|
||||
// Update biometrics panel
|
||||
updateBiometricsPanel();
|
||||
});
|
||||
});
|
||||
|
||||
// Initialize biometrics count
|
||||
updateBiometricsCount();
|
||||
}
|
||||
|
||||
function setupBiometricPanel() {
|
||||
const biometricPanel = document.getElementById('biometrics-panel');
|
||||
if (!biometricPanel) {
|
||||
console.error('Biometric panel not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Use existing biometrics content container
|
||||
const biometricsContent = document.getElementById('biometrics-content');
|
||||
if (biometricsContent) {
|
||||
biometricsContent.innerHTML = `
|
||||
<div class="panel-section">
|
||||
<h4>Collected Samples</h4>
|
||||
<div id="samples-list">
|
||||
<p>No samples collected yet</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-section">
|
||||
<h4>Scanner Status</h4>
|
||||
<div id="scanner-status">
|
||||
<p>Ready</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
updateBiometricDisplay();
|
||||
}
|
||||
|
||||
// Add a biometric sample to the collection
|
||||
export function addBiometricSample(sample) {
|
||||
if (!window.gameState.biometricSamples) {
|
||||
window.gameState.biometricSamples = [];
|
||||
}
|
||||
|
||||
// Ensure sample has all required properties with proper defaults
|
||||
const normalizedSample = {
|
||||
owner: sample.owner || 'Unknown',
|
||||
type: sample.type || 'fingerprint',
|
||||
quality: sample.quality || 0,
|
||||
rating: sample.rating || getRatingFromQuality(sample.quality || 0),
|
||||
data: sample.data || null,
|
||||
id: sample.id || generateSampleId(),
|
||||
collectedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
// Check if sample already exists
|
||||
const existingSample = window.gameState.biometricSamples.find(s =>
|
||||
s.owner === normalizedSample.owner && s.type === normalizedSample.type
|
||||
);
|
||||
|
||||
if (existingSample) {
|
||||
// Update existing sample with better quality if applicable
|
||||
if (normalizedSample.quality > existingSample.quality) {
|
||||
existingSample.quality = normalizedSample.quality;
|
||||
existingSample.rating = normalizedSample.rating;
|
||||
existingSample.collectedAt = normalizedSample.collectedAt;
|
||||
}
|
||||
} else {
|
||||
// Add new sample
|
||||
window.gameState.biometricSamples.push(normalizedSample);
|
||||
}
|
||||
|
||||
updateBiometricsPanel();
|
||||
updateBiometricsCount();
|
||||
console.log('Biometric sample added:', normalizedSample);
|
||||
}
|
||||
|
||||
function updateBiometricDisplay() {
|
||||
const samplesList = document.getElementById('samples-list');
|
||||
const scannerStatus = document.getElementById('scanner-status');
|
||||
|
||||
if (!samplesList || !scannerStatus) return;
|
||||
|
||||
if (window.gameState.biometricSamples.length === 0) {
|
||||
samplesList.innerHTML = '<p>No samples collected yet</p>';
|
||||
} else {
|
||||
samplesList.innerHTML = window.gameState.biometricSamples.map(sample => {
|
||||
// Ensure all properties exist with safe defaults
|
||||
const owner = sample.owner || 'Unknown';
|
||||
const type = sample.type || 'fingerprint';
|
||||
const quality = sample.quality || 0;
|
||||
const rating = sample.rating || getRatingFromQuality(quality);
|
||||
const collectedAt = sample.collectedAt || new Date().toISOString();
|
||||
|
||||
return `
|
||||
<div class="sample-item">
|
||||
<strong>${owner}</strong>
|
||||
<div class="sample-details">
|
||||
<span class="sample-type">${type}</span>
|
||||
<span class="sample-quality quality-${rating.toLowerCase()}">${rating} (${Math.round(quality * 100)}%)</span>
|
||||
</div>
|
||||
<div class="sample-date">${new Date(collectedAt).toLocaleString()}</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Update scanner status
|
||||
scannerStatus.innerHTML = '<p>Ready</p>';
|
||||
}
|
||||
|
||||
// Helper function to generate rating from quality
|
||||
function getRatingFromQuality(quality) {
|
||||
const qualityPercentage = Math.round(quality * 100);
|
||||
if (qualityPercentage >= 95) return 'Perfect';
|
||||
if (qualityPercentage >= 85) return 'Excellent';
|
||||
if (qualityPercentage >= 75) return 'Good';
|
||||
if (qualityPercentage >= 60) return 'Fair';
|
||||
if (qualityPercentage >= 40) return 'Acceptable';
|
||||
return 'Poor';
|
||||
}
|
||||
|
||||
// Helper function to generate unique sample ID
|
||||
function generateSampleId() {
|
||||
return 'sample_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
||||
}
|
||||
|
||||
// Handle biometric scanner interaction
|
||||
export function handleBiometricScan(scannerId, requiredOwner) {
|
||||
console.log('Biometric scan requested:', { scannerId, requiredOwner });
|
||||
|
||||
// Check if scanner is locked out
|
||||
if (window.scannerState.lockoutTimers[scannerId]) {
|
||||
const lockoutEnd = window.scannerState.lockoutTimers[scannerId];
|
||||
const now = Date.now();
|
||||
|
||||
if (now < lockoutEnd) {
|
||||
const remainingTime = Math.ceil((lockoutEnd - now) / 1000);
|
||||
window.gameAlert(`Scanner locked out. Try again in ${remainingTime} seconds.`, 'error', 'Scanner Locked', 3000);
|
||||
return false;
|
||||
} else {
|
||||
// Lockout expired, clear it
|
||||
delete window.scannerState.lockoutTimers[scannerId];
|
||||
delete window.scannerState.failedAttempts[scannerId];
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we have a matching biometric sample
|
||||
const matchingSample = window.gameState.biometricSamples.find(sample =>
|
||||
sample.owner === requiredOwner && sample.quality >= window.BIOMETRIC_QUALITY_THRESHOLD
|
||||
);
|
||||
|
||||
if (matchingSample) {
|
||||
console.log('Biometric scan successful:', matchingSample);
|
||||
|
||||
// Visual success feedback
|
||||
const scannerElement = document.querySelector(`[data-scanner-id="${scannerId}"]`);
|
||||
if (scannerElement) {
|
||||
scannerElement.style.border = '2px solid #00ff00';
|
||||
setTimeout(() => {
|
||||
scannerElement.style.border = '';
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
window.gameAlert(`Biometric scan successful! Authenticated as ${requiredOwner}.`, 'success', 'Scan Successful', 4000);
|
||||
|
||||
// Reset failed attempts on success
|
||||
delete window.scannerState.failedAttempts[scannerId];
|
||||
|
||||
return true;
|
||||
} else {
|
||||
console.log('Biometric scan failed');
|
||||
handleScannerFailure(scannerId);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleScannerFailure(scannerId) {
|
||||
// Initialize failed attempts if not exists
|
||||
if (!window.scannerState.failedAttempts[scannerId]) {
|
||||
window.scannerState.failedAttempts[scannerId] = 0;
|
||||
}
|
||||
|
||||
// Increment failed attempts
|
||||
window.scannerState.failedAttempts[scannerId]++;
|
||||
|
||||
// Check if we should lockout
|
||||
if (window.scannerState.failedAttempts[scannerId] >= window.MAX_FAILED_ATTEMPTS) {
|
||||
window.scannerState.lockoutTimers[scannerId] = Date.now() + window.SCANNER_LOCKOUT_TIME;
|
||||
window.gameAlert(`Too many failed attempts. Scanner locked for ${window.SCANNER_LOCKOUT_TIME/1000} seconds.`, 'error', 'Scanner Locked', 5000);
|
||||
} else {
|
||||
const remainingAttempts = window.MAX_FAILED_ATTEMPTS - window.scannerState.failedAttempts[scannerId];
|
||||
window.gameAlert(`Scan failed. ${remainingAttempts} attempts remaining before lockout.`, 'warning', 'Scan Failed', 4000);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a fingerprint sample with quality assessment
|
||||
export function generateFingerprintSample(owner, quality = null) {
|
||||
// If no quality provided, generate based on random factors
|
||||
if (quality === null) {
|
||||
quality = 0.6 + (Math.random() * 0.4); // 60-100% quality range
|
||||
}
|
||||
|
||||
const rating = getRatingFromQuality(quality);
|
||||
|
||||
return {
|
||||
owner: owner || 'Unknown',
|
||||
type: 'fingerprint',
|
||||
quality: quality,
|
||||
rating: rating,
|
||||
id: generateSampleId(),
|
||||
collectedFrom: 'evidence'
|
||||
};
|
||||
}
|
||||
|
||||
// Toggle the biometrics panel
|
||||
export function toggleBiometricsPanel() {
|
||||
const biometricsPanel = document.getElementById('biometrics-panel');
|
||||
if (!biometricsPanel) return;
|
||||
|
||||
const isVisible = biometricsPanel.style.display === 'block';
|
||||
biometricsPanel.style.display = isVisible ? 'none' : 'block';
|
||||
|
||||
// Update panel content when opening
|
||||
if (!isVisible) {
|
||||
updateBiometricsPanel();
|
||||
}
|
||||
}
|
||||
|
||||
// Update biometrics panel with current samples
|
||||
export function updateBiometricsPanel() {
|
||||
const biometricsContent = document.getElementById('biometrics-content');
|
||||
if (!biometricsContent) return;
|
||||
|
||||
const searchTerm = document.getElementById('biometrics-search')?.value?.toLowerCase() || '';
|
||||
const activeCategory = document.querySelector('.biometrics-category.active')?.dataset.category || 'all';
|
||||
|
||||
// Filter samples based on search and category
|
||||
let filteredSamples = [...(window.gameState.biometricSamples || [])];
|
||||
|
||||
// Apply category filter
|
||||
if (activeCategory === 'fingerprint') {
|
||||
filteredSamples = filteredSamples.filter(sample => sample.type === 'fingerprint');
|
||||
}
|
||||
|
||||
// Apply search filter
|
||||
if (searchTerm) {
|
||||
filteredSamples = filteredSamples.filter(sample =>
|
||||
sample.owner.toLowerCase().includes(searchTerm) ||
|
||||
sample.type.toLowerCase().includes(searchTerm)
|
||||
);
|
||||
}
|
||||
|
||||
// Sort samples by quality (highest first)
|
||||
filteredSamples.sort((a, b) => b.quality - a.quality);
|
||||
|
||||
// Clear current content
|
||||
biometricsContent.innerHTML = '';
|
||||
|
||||
// Add samples
|
||||
if (filteredSamples.length === 0) {
|
||||
if (searchTerm) {
|
||||
biometricsContent.innerHTML = '<div class="sample-item">No samples match your search.</div>';
|
||||
} else if (activeCategory !== 'all') {
|
||||
biometricsContent.innerHTML = `<div class="sample-item">No ${activeCategory} samples found.</div>`;
|
||||
} else {
|
||||
biometricsContent.innerHTML = '<div class="sample-item">No samples collected yet.</div>';
|
||||
}
|
||||
} else {
|
||||
filteredSamples.forEach(sample => {
|
||||
const sampleElement = document.createElement('div');
|
||||
sampleElement.className = 'sample-item';
|
||||
sampleElement.dataset.id = sample.id || 'unknown';
|
||||
|
||||
// Ensure all properties exist with safe defaults
|
||||
const owner = sample.owner || 'Unknown';
|
||||
const type = sample.type || 'fingerprint';
|
||||
const quality = sample.quality || 0;
|
||||
const rating = sample.rating || getRatingFromQuality(quality);
|
||||
const collectedAt = sample.collectedAt || new Date().toISOString();
|
||||
|
||||
const qualityPercentage = Math.round(quality * 100);
|
||||
const timestamp = new Date(collectedAt);
|
||||
const formattedTime = timestamp.toLocaleDateString() + ' ' + timestamp.toLocaleTimeString();
|
||||
|
||||
sampleElement.innerHTML = `
|
||||
<strong>${owner}</strong>
|
||||
<div class="sample-details">
|
||||
<span class="sample-type">${type}</span>
|
||||
<span class="sample-quality quality-${rating.toLowerCase()}">${rating} (${qualityPercentage}%)</span>
|
||||
</div>
|
||||
<div class="sample-date">${formattedTime}</div>
|
||||
`;
|
||||
|
||||
biometricsContent.appendChild(sampleElement);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update biometrics count in the toggle button
|
||||
export function updateBiometricsCount() {
|
||||
const countElement = document.getElementById('biometrics-count');
|
||||
if (countElement && window.gameState?.biometricSamples) {
|
||||
const count = window.gameState.biometricSamples.length;
|
||||
countElement.textContent = count;
|
||||
countElement.style.display = count > 0 ? 'flex' : 'none';
|
||||
|
||||
// Show the biometrics toggle if we have samples
|
||||
const biometricsToggle = document.getElementById('biometrics-toggle');
|
||||
if (biometricsToggle && count > 0) {
|
||||
biometricsToggle.style.display = 'block';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export for global access
|
||||
window.initializeBiometricsPanel = initializeBiometricsPanel;
|
||||
window.addBiometricSample = addBiometricSample;
|
||||
window.handleBiometricScan = handleBiometricScan;
|
||||
window.generateFingerprintSample = generateFingerprintSample;
|
||||
window.toggleBiometricsPanel = toggleBiometricsPanel;
|
||||
window.updateBiometricsPanel = updateBiometricsPanel;
|
||||
window.updateBiometricsCount = updateBiometricsCount;
|
||||
@@ -1,486 +0,0 @@
|
||||
// Bluetooth System
|
||||
// Handles Bluetooth device scanning and management
|
||||
|
||||
// Bluetooth state management
|
||||
let bluetoothDevices = [];
|
||||
let lastBluetoothPanelUpdate = 0;
|
||||
let newBluetoothDevices = 0;
|
||||
|
||||
// Sync with global game state
|
||||
function syncBluetoothDevices() {
|
||||
if (!window.gameState) {
|
||||
window.gameState = {};
|
||||
}
|
||||
window.gameState.bluetoothDevices = bluetoothDevices;
|
||||
}
|
||||
|
||||
// Constants
|
||||
const BLUETOOTH_SCAN_RANGE = 150; // pixels - 2 tiles range for Bluetooth scanning
|
||||
const BLUETOOTH_SCAN_INTERVAL = 200; // Scan every 200ms for more responsive updates
|
||||
const BLUETOOTH_UPDATE_THROTTLE = 100; // Update UI every 100ms max
|
||||
|
||||
// Initialize the Bluetooth system
|
||||
export function initializeBluetoothPanel() {
|
||||
console.log('Bluetooth system initialized');
|
||||
|
||||
// Create bluetooth device list
|
||||
bluetoothDevices = [];
|
||||
|
||||
// Set up bluetooth toggle button handler
|
||||
const bluetoothToggle = document.getElementById('bluetooth-toggle');
|
||||
if (bluetoothToggle) {
|
||||
bluetoothToggle.addEventListener('click', toggleBluetoothPanel);
|
||||
}
|
||||
|
||||
// Set up bluetooth close button
|
||||
const bluetoothClose = document.getElementById('bluetooth-close');
|
||||
if (bluetoothClose) {
|
||||
bluetoothClose.addEventListener('click', toggleBluetoothPanel);
|
||||
}
|
||||
|
||||
// Set up search functionality
|
||||
const bluetoothSearch = document.getElementById('bluetooth-search');
|
||||
if (bluetoothSearch) {
|
||||
bluetoothSearch.addEventListener('input', updateBluetoothPanel);
|
||||
}
|
||||
|
||||
// Set up category filters
|
||||
const categories = document.querySelectorAll('.bluetooth-category');
|
||||
categories.forEach(category => {
|
||||
category.addEventListener('click', () => {
|
||||
// Remove active class from all categories
|
||||
categories.forEach(c => c.classList.remove('active'));
|
||||
// Add active class to clicked category
|
||||
category.classList.add('active');
|
||||
// Update bluetooth panel
|
||||
updateBluetoothPanel();
|
||||
});
|
||||
});
|
||||
|
||||
// Initialize bluetooth panel
|
||||
updateBluetoothPanel();
|
||||
updateBluetoothCount();
|
||||
syncBluetoothDevices();
|
||||
}
|
||||
|
||||
// Check for Bluetooth devices
|
||||
export function checkBluetoothDevices() {
|
||||
// Find scanner in inventory
|
||||
const scanner = window.inventory.items.find(item =>
|
||||
item.scenarioData?.type === "bluetooth_scanner"
|
||||
);
|
||||
|
||||
if (!scanner) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Show the Bluetooth toggle button if it's not already visible
|
||||
const bluetoothToggle = document.getElementById('bluetooth-toggle');
|
||||
if (bluetoothToggle && bluetoothToggle.style.display === 'none') {
|
||||
bluetoothToggle.style.display = 'flex';
|
||||
}
|
||||
|
||||
// Find all Bluetooth devices in the current room
|
||||
if (!window.currentPlayerRoom || !window.rooms[window.currentPlayerRoom] || !window.rooms[window.currentPlayerRoom].objects) {
|
||||
return;
|
||||
}
|
||||
|
||||
const room = window.rooms[window.currentPlayerRoom];
|
||||
const player = window.player;
|
||||
if (!player) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Keep track of devices detected in this scan
|
||||
const detectedDevices = new Set();
|
||||
let needsUpdate = false;
|
||||
|
||||
Object.values(room.objects).forEach(obj => {
|
||||
if (obj.scenarioData?.lockType === "bluetooth") {
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(player.x - obj.x, 2) + Math.pow(player.y - obj.y, 2)
|
||||
);
|
||||
|
||||
const deviceMac = obj.scenarioData?.mac || "Unknown";
|
||||
const deviceName = obj.scenarioData?.name || "Unknown Device";
|
||||
|
||||
if (distance <= BLUETOOTH_SCAN_RANGE) {
|
||||
detectedDevices.add(`${deviceMac}|${deviceName}`); // Use combination for uniqueness
|
||||
|
||||
// Add to Bluetooth scanner panel
|
||||
const signalStrengthPercentage = Math.max(0, Math.round(100 - (distance / BLUETOOTH_SCAN_RANGE * 100)));
|
||||
// Convert percentage to dBm format (-100 to -30 dBm range)
|
||||
const signalStrength = Math.round(-100 + (signalStrengthPercentage * 0.7)); // -100 to -30 dBm
|
||||
const details = `Type: ${obj.scenarioData?.type || "Unknown"}\nDistance: ${Math.round(distance)} units\nSignal Strength: ${signalStrength}dBm (${signalStrengthPercentage}%)`;
|
||||
|
||||
// Check if device already exists in our list (by MAC + name combination for uniqueness)
|
||||
const existingDevice = bluetoothDevices.find(device =>
|
||||
device.mac === deviceMac && device.name === deviceName
|
||||
);
|
||||
|
||||
if (existingDevice) {
|
||||
// Update existing device details with real-time data
|
||||
const wasNearby = existingDevice.nearby;
|
||||
const oldSignalStrengthPercentage = existingDevice.signalStrengthPercentage || 0;
|
||||
|
||||
existingDevice.details = details;
|
||||
existingDevice.lastSeen = new Date();
|
||||
existingDevice.nearby = true;
|
||||
existingDevice.signalStrength = signalStrength;
|
||||
existingDevice.signalStrengthPercentage = signalStrengthPercentage;
|
||||
|
||||
// Always update if device came back into range or signal strength changed significantly
|
||||
if (!wasNearby || Math.abs(oldSignalStrengthPercentage - signalStrengthPercentage) > 5) {
|
||||
needsUpdate = true;
|
||||
}
|
||||
} else {
|
||||
// Add as new device if not already in our list
|
||||
const newDevice = addBluetoothDevice(deviceName, deviceMac, details, true);
|
||||
if (newDevice) {
|
||||
newDevice.signalStrength = signalStrength;
|
||||
newDevice.signalStrengthPercentage = signalStrengthPercentage;
|
||||
if (window.gameAlert) {
|
||||
window.gameAlert(`Bluetooth device detected: ${deviceName} (MAC: ${deviceMac})`, 'info', 'Bluetooth Scanner', 4000);
|
||||
}
|
||||
needsUpdate = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Mark devices that weren't detected in this scan as not nearby
|
||||
bluetoothDevices.forEach(device => {
|
||||
const deviceKey = `${device.mac}|${device.name}`;
|
||||
if (device.nearby && !detectedDevices.has(deviceKey)) {
|
||||
device.nearby = false;
|
||||
device.lastSeen = new Date();
|
||||
needsUpdate = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Force immediate UI update if panel is open and devices changed nearby status
|
||||
if (needsUpdate) {
|
||||
const bluetoothPanel = document.getElementById('bluetooth-panel');
|
||||
if (bluetoothPanel && bluetoothPanel.style.display === 'block') {
|
||||
// Force update by resetting throttle timer
|
||||
lastBluetoothPanelUpdate = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Always update the count and sync devices when there are changes
|
||||
if (needsUpdate) {
|
||||
updateBluetoothCount();
|
||||
syncBluetoothDevices();
|
||||
|
||||
// Update the panel UI if it's visible
|
||||
const bluetoothPanel = document.getElementById('bluetooth-panel');
|
||||
if (bluetoothPanel && bluetoothPanel.style.display === 'block') {
|
||||
const now = Date.now();
|
||||
if (now - lastBluetoothPanelUpdate > BLUETOOTH_UPDATE_THROTTLE) {
|
||||
updateBluetoothPanel();
|
||||
lastBluetoothPanelUpdate = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add a Bluetooth device to the scanner panel
|
||||
export function addBluetoothDevice(name, mac, details = "", nearby = true) {
|
||||
// Check if a device with the same MAC + name combination already exists
|
||||
const deviceExists = bluetoothDevices.some(device => device.mac === mac && device.name === name);
|
||||
|
||||
// If the device already exists, update its nearby status
|
||||
if (deviceExists) {
|
||||
const existingDevice = bluetoothDevices.find(device => device.mac === mac && device.name === name);
|
||||
existingDevice.nearby = nearby;
|
||||
existingDevice.lastSeen = new Date();
|
||||
updateBluetoothPanel();
|
||||
syncBluetoothDevices();
|
||||
return null;
|
||||
}
|
||||
|
||||
const device = {
|
||||
id: Date.now(),
|
||||
name: name,
|
||||
mac: mac,
|
||||
details: details,
|
||||
nearby: nearby,
|
||||
saved: false,
|
||||
firstSeen: new Date(),
|
||||
lastSeen: new Date(),
|
||||
signalStrength: -100, // Default to weak signal (-100 dBm)
|
||||
signalStrengthPercentage: 0 // Default to 0% for visual display
|
||||
};
|
||||
|
||||
bluetoothDevices.push(device);
|
||||
updateBluetoothPanel();
|
||||
updateBluetoothCount();
|
||||
syncBluetoothDevices();
|
||||
|
||||
// Show notification for new device
|
||||
if (window.showNotification) {
|
||||
window.showNotification(`New Bluetooth device detected: ${name}`, 'info', 'Bluetooth Scanner', 3000);
|
||||
}
|
||||
|
||||
return device;
|
||||
}
|
||||
|
||||
// Update the Bluetooth scanner panel with current devices
|
||||
export function updateBluetoothPanel() {
|
||||
const bluetoothContent = document.getElementById('bluetooth-content');
|
||||
if (!bluetoothContent) return;
|
||||
|
||||
const searchTerm = document.getElementById('bluetooth-search')?.value?.toLowerCase() || '';
|
||||
|
||||
// Get active category
|
||||
const activeCategory = document.querySelector('.bluetooth-category.active')?.dataset.category || 'all';
|
||||
|
||||
// Store the currently hovered device, if any
|
||||
const hoveredDevice = document.querySelector('.bluetooth-device:hover');
|
||||
const hoveredDeviceId = hoveredDevice ? hoveredDevice.dataset.id : null;
|
||||
|
||||
// Add Bluetooth-locked items from inventory to the main bluetoothDevices array
|
||||
if (window.inventory && window.inventory.items) {
|
||||
window.inventory.items.forEach(item => {
|
||||
if (item.scenarioData?.lockType === "bluetooth" && item.scenarioData?.locked) {
|
||||
// Check if this device is already in our list
|
||||
const deviceMac = item.scenarioData?.mac || "Unknown";
|
||||
|
||||
// Normalize MAC address format (ensure lowercase for comparison)
|
||||
const normalizedMac = deviceMac.toLowerCase();
|
||||
|
||||
// Check if device already exists in our list (by MAC + name combination)
|
||||
const deviceName = item.scenarioData?.name || item.name || "Unknown Device";
|
||||
const existingDeviceIndex = bluetoothDevices.findIndex(device =>
|
||||
device.mac.toLowerCase() === normalizedMac && device.name === deviceName
|
||||
);
|
||||
|
||||
if (existingDeviceIndex === -1) {
|
||||
// Add as a new device
|
||||
const details = `Type: ${item.scenarioData?.type || "Unknown"}\nLocation: Inventory\nStatus: Locked`;
|
||||
|
||||
const newDevice = {
|
||||
id: `inv_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
name: deviceName,
|
||||
mac: deviceMac,
|
||||
details: details,
|
||||
lastSeen: new Date(),
|
||||
nearby: true, // Always nearby since it's in inventory
|
||||
saved: true, // Auto-save inventory items
|
||||
signalStrength: -30, // Max strength for inventory items (-30 dBm)
|
||||
signalStrengthPercentage: 100, // 100% for visual display
|
||||
inInventory: true // Mark as inventory item
|
||||
};
|
||||
|
||||
// Add to the main bluetoothDevices array
|
||||
bluetoothDevices.push(newDevice);
|
||||
console.log('Added inventory device to bluetoothDevices:', newDevice);
|
||||
syncBluetoothDevices();
|
||||
} else {
|
||||
// Update existing device
|
||||
const existingDevice = bluetoothDevices[existingDeviceIndex];
|
||||
existingDevice.inInventory = true;
|
||||
existingDevice.nearby = true;
|
||||
existingDevice.signalStrength = -30; // -30 dBm for inventory items
|
||||
existingDevice.signalStrengthPercentage = 100; // 100% for visual display
|
||||
existingDevice.lastSeen = new Date();
|
||||
existingDevice.details = `Type: ${item.scenarioData?.type || "Unknown"}\nLocation: Inventory\nStatus: Locked`;
|
||||
console.log('Updated existing device with inventory info:', existingDevice);
|
||||
syncBluetoothDevices();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Filter devices based on search and category
|
||||
let filteredDevices = [...bluetoothDevices];
|
||||
|
||||
// Apply category filter
|
||||
if (activeCategory === 'nearby') {
|
||||
filteredDevices = filteredDevices.filter(device => device.nearby);
|
||||
} else if (activeCategory === 'saved') {
|
||||
filteredDevices = filteredDevices.filter(device => device.saved);
|
||||
}
|
||||
|
||||
// Apply search filter
|
||||
if (searchTerm) {
|
||||
filteredDevices = filteredDevices.filter(device =>
|
||||
device.name.toLowerCase().includes(searchTerm) ||
|
||||
device.mac.toLowerCase().includes(searchTerm) ||
|
||||
device.details.toLowerCase().includes(searchTerm)
|
||||
);
|
||||
}
|
||||
|
||||
// Sort devices with inventory items first, then nearby ones, then by signal strength
|
||||
filteredDevices.sort((a, b) => {
|
||||
// Inventory items first
|
||||
if (a.inInventory !== b.inInventory) {
|
||||
return a.inInventory ? -1 : 1;
|
||||
}
|
||||
|
||||
// Then nearby items
|
||||
if (a.nearby !== b.nearby) {
|
||||
return a.nearby ? -1 : 1;
|
||||
}
|
||||
|
||||
// For nearby devices, sort by signal strength
|
||||
if (a.nearby && b.nearby && a.signalStrength !== b.signalStrength) {
|
||||
return b.signalStrength - a.signalStrength;
|
||||
}
|
||||
|
||||
return new Date(b.lastSeen) - new Date(a.lastSeen);
|
||||
});
|
||||
|
||||
// Clear current content
|
||||
bluetoothContent.innerHTML = '';
|
||||
|
||||
// Add devices
|
||||
if (filteredDevices.length === 0) {
|
||||
if (searchTerm) {
|
||||
bluetoothContent.innerHTML = '<div class="bluetooth-device">No devices match your search.</div>';
|
||||
} else if (activeCategory !== 'all') {
|
||||
bluetoothContent.innerHTML = `<div class="bluetooth-device">No ${activeCategory} devices found.</div>`;
|
||||
} else {
|
||||
bluetoothContent.innerHTML = '<div class="bluetooth-device">No devices detected yet.</div>';
|
||||
}
|
||||
} else {
|
||||
filteredDevices.forEach(device => {
|
||||
const deviceElement = document.createElement('div');
|
||||
deviceElement.className = 'bluetooth-device';
|
||||
deviceElement.dataset.id = device.id;
|
||||
|
||||
// If this was the hovered device, add the hover class
|
||||
if (hoveredDeviceId && device.id === hoveredDeviceId) {
|
||||
deviceElement.classList.add('hover-preserved');
|
||||
}
|
||||
|
||||
// Format the timestamp
|
||||
const timestamp = new Date(device.lastSeen);
|
||||
const formattedDate = timestamp.toLocaleDateString();
|
||||
const formattedTime = timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
|
||||
// Get signal color based on strength
|
||||
const getSignalColor = (strength) => {
|
||||
if (strength >= 80) return '#00cc00'; // Strong - green
|
||||
if (strength >= 50) return '#cccc00'; // Medium - yellow
|
||||
return '#cc5500'; // Weak - orange
|
||||
};
|
||||
|
||||
let deviceContent = `<div class="bluetooth-device-name">
|
||||
<span>${device.name}</span>
|
||||
<div class="bluetooth-device-icons">`;
|
||||
|
||||
if (device.nearby && typeof device.signalStrength === 'number') {
|
||||
// Use percentage for visual display
|
||||
const signalPercentage = device.signalStrengthPercentage || Math.max(0, Math.round(((device.signalStrength + 100) / 70) * 100));
|
||||
const signalColor = getSignalColor(signalPercentage);
|
||||
|
||||
// Calculate how many bars should be active based on signal strength percentage
|
||||
const activeBars = Math.ceil(signalPercentage / 20); // 0-20% = 1 bar, 21-40% = 2 bars, etc.
|
||||
|
||||
deviceContent += `<div class="bluetooth-signal-bar-container">
|
||||
<div class="bluetooth-signal-bars">`;
|
||||
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
const isActive = i <= activeBars;
|
||||
deviceContent += `<div class="bluetooth-signal-bar ${isActive ? 'active' : ''}" style="color: ${signalColor};"></div>`;
|
||||
}
|
||||
|
||||
deviceContent += `</div></div>`;
|
||||
} else if (device.nearby) {
|
||||
// Fallback if signal strength not available
|
||||
deviceContent += `<span class="bluetooth-device-icon">📶</span>`;
|
||||
}
|
||||
|
||||
if (device.saved) {
|
||||
deviceContent += `<span class="bluetooth-device-icon">💾</span>`;
|
||||
}
|
||||
|
||||
if (device.inInventory) {
|
||||
deviceContent += `<span class="bluetooth-device-icon">🎒</span>`;
|
||||
}
|
||||
|
||||
deviceContent += `</div></div>`;
|
||||
deviceContent += `<div class="bluetooth-device-details">MAC: ${device.mac}\n${device.details}</div>`;
|
||||
deviceContent += `<div class="bluetooth-device-timestamp">Last seen: ${formattedDate} ${formattedTime}</div>`;
|
||||
|
||||
deviceElement.innerHTML = deviceContent;
|
||||
|
||||
// Toggle expanded state when clicked
|
||||
deviceElement.addEventListener('click', (event) => {
|
||||
deviceElement.classList.toggle('expanded');
|
||||
|
||||
// Mark as saved when expanded
|
||||
if (!device.saved && deviceElement.classList.contains('expanded')) {
|
||||
device.saved = true;
|
||||
updateBluetoothCount();
|
||||
updateBluetoothPanel();
|
||||
syncBluetoothDevices();
|
||||
}
|
||||
});
|
||||
|
||||
bluetoothContent.appendChild(deviceElement);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update the new Bluetooth devices count
|
||||
export function updateBluetoothCount() {
|
||||
const bluetoothCount = document.getElementById('bluetooth-count');
|
||||
if (bluetoothCount) {
|
||||
newBluetoothDevices = bluetoothDevices.filter(device => !device.saved && device.nearby).length;
|
||||
|
||||
bluetoothCount.textContent = newBluetoothDevices;
|
||||
bluetoothCount.style.display = newBluetoothDevices > 0 ? 'flex' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
export function toggleBluetoothPanel() {
|
||||
const bluetoothPanel = document.getElementById('bluetooth-panel');
|
||||
if (!bluetoothPanel) return;
|
||||
|
||||
const isVisible = bluetoothPanel.style.display === 'block';
|
||||
bluetoothPanel.style.display = isVisible ? 'none' : 'block';
|
||||
|
||||
// Always update panel content when opening to show current state
|
||||
if (!isVisible) {
|
||||
updateBluetoothPanel();
|
||||
// Reset the throttle timer so updates happen immediately when panel is open
|
||||
lastBluetoothPanelUpdate = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Function to unlock a Bluetooth-locked inventory item by MAC address
|
||||
export function unlockInventoryDeviceByMac(mac) {
|
||||
console.log('Attempting to unlock inventory device with MAC:', mac);
|
||||
|
||||
// Normalize MAC address for comparison
|
||||
const normalizedMac = mac.toLowerCase();
|
||||
|
||||
// Find the inventory item with this MAC address
|
||||
const item = window.inventory.items.find(item =>
|
||||
item.scenarioData?.mac?.toLowerCase() === normalizedMac &&
|
||||
item.scenarioData?.lockType === "bluetooth" &&
|
||||
item.scenarioData?.locked
|
||||
);
|
||||
|
||||
if (!item) {
|
||||
console.error('Inventory item not found with MAC:', mac);
|
||||
if (window.gameAlert) {
|
||||
window.gameAlert("Device not found in inventory.", 'error', 'Unlock Failed', 3000);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Found inventory item to unlock:', item);
|
||||
}
|
||||
|
||||
// Export for global access
|
||||
window.initializeBluetoothPanel = initializeBluetoothPanel;
|
||||
window.checkBluetoothDevices = checkBluetoothDevices;
|
||||
window.addBluetoothDevice = addBluetoothDevice;
|
||||
window.toggleBluetoothPanel = toggleBluetoothPanel;
|
||||
window.updateBluetoothPanel = updateBluetoothPanel;
|
||||
window.updateBluetoothCount = updateBluetoothCount;
|
||||
window.unlockInventoryDeviceByMac = unlockInventoryDeviceByMac;
|
||||
@@ -379,6 +379,45 @@ export function handleObjectInteraction(sprite) {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle the Bluetooth Scanner - only open minigame if it's already in inventory
|
||||
if (sprite.scenarioData.type === "bluetooth_scanner") {
|
||||
// Check if this is an inventory item (clicked from inventory)
|
||||
const isInventoryItem = sprite.objectId && sprite.objectId.startsWith('inventory_');
|
||||
|
||||
if (isInventoryItem && window.startBluetoothScannerMinigame) {
|
||||
console.log('Starting bluetooth scanner minigame from inventory');
|
||||
window.startBluetoothScannerMinigame(sprite);
|
||||
return;
|
||||
}
|
||||
// If it's not in inventory, let it fall through to the takeable logic below
|
||||
}
|
||||
|
||||
// Handle the Fingerprint Kit - only open minigame if it's already in inventory
|
||||
if (sprite.scenarioData.type === "fingerprint_kit") {
|
||||
// Check if this is an inventory item (clicked from inventory)
|
||||
const isInventoryItem = sprite.objectId && sprite.objectId.startsWith('inventory_');
|
||||
|
||||
if (isInventoryItem && window.startBiometricsMinigame) {
|
||||
console.log('Starting biometrics minigame from inventory');
|
||||
window.startBiometricsMinigame(sprite);
|
||||
return;
|
||||
}
|
||||
// If it's not in inventory, let it fall through to the takeable logic below
|
||||
}
|
||||
|
||||
// Handle the Lockpick Set - only open minigame if it's already in inventory
|
||||
if (sprite.scenarioData.type === "lockpick" || sprite.scenarioData.type === "lockpickset") {
|
||||
// Check if this is an inventory item (clicked from inventory)
|
||||
const isInventoryItem = sprite.objectId && sprite.objectId.startsWith('inventory_');
|
||||
|
||||
if (isInventoryItem && window.startLockpickSetMinigame) {
|
||||
console.log('Starting lockpick set minigame from inventory');
|
||||
window.startLockpickSetMinigame(sprite);
|
||||
return;
|
||||
}
|
||||
// If it's not in inventory, let it fall through to the takeable logic below
|
||||
}
|
||||
|
||||
// Handle biometric scanner interaction
|
||||
if (sprite.scenarioData.biometricType === 'fingerprint') {
|
||||
handleBiometricScan(sprite);
|
||||
@@ -577,22 +616,35 @@ function addToInventory(sprite) {
|
||||
// Show notification
|
||||
window.gameAlert(`Added ${sprite.scenarioData.name} to inventory`, 'success', 'Item Collected', 3000);
|
||||
|
||||
// If this is the Bluetooth scanner, show the toggle button
|
||||
if (sprite.scenarioData.type === "bluetooth_scanner") {
|
||||
const bluetoothToggle = document.getElementById('bluetooth-toggle');
|
||||
if (bluetoothToggle) {
|
||||
bluetoothToggle.style.display = 'flex';
|
||||
}
|
||||
// If this is the Bluetooth scanner, automatically open the minigame after adding to inventory
|
||||
if (sprite.scenarioData.type === "bluetooth_scanner" && window.startBluetoothScannerMinigame) {
|
||||
// Small delay to ensure the item is fully added to inventory
|
||||
setTimeout(() => {
|
||||
console.log('Auto-opening bluetooth scanner minigame after adding to inventory');
|
||||
window.startBluetoothScannerMinigame(itemImg);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// If this is the fingerprint kit, show the biometrics toggle button
|
||||
if (sprite.scenarioData.type === "fingerprint_kit") {
|
||||
const biometricsToggle = document.getElementById('biometrics-toggle');
|
||||
if (biometricsToggle) {
|
||||
biometricsToggle.style.display = 'flex';
|
||||
}
|
||||
// If this is the Fingerprint Kit, automatically open the minigame after adding to inventory
|
||||
if (sprite.scenarioData.type === "fingerprint_kit" && window.startBiometricsMinigame) {
|
||||
// Small delay to ensure the item is fully added to inventory
|
||||
setTimeout(() => {
|
||||
console.log('Auto-opening biometrics minigame after adding to inventory');
|
||||
window.startBiometricsMinigame(itemImg);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// If this is the Lockpick Set, automatically open the minigame after adding to inventory
|
||||
if ((sprite.scenarioData.type === "lockpick" || sprite.scenarioData.type === "lockpickset") && window.startLockpickSetMinigame) {
|
||||
// Small delay to ensure the item is fully added to inventory
|
||||
setTimeout(() => {
|
||||
console.log('Auto-opening lockpick set minigame after adding to inventory');
|
||||
window.startLockpickSetMinigame(itemImg);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// Fingerprint kit is now handled as a minigame when clicked from inventory
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error adding to inventory:', error);
|
||||
|
||||
@@ -62,7 +62,7 @@ function createInventorySprite(itemData) {
|
||||
// Create a pseudo-sprite object that can be used in inventory
|
||||
const sprite = {
|
||||
name: itemData.type,
|
||||
objectId: `initial_${itemData.type}_${Date.now()}`,
|
||||
objectId: `inventory_${itemData.type}_${Date.now()}`,
|
||||
scenarioData: itemData,
|
||||
setVisible: function(visible) {
|
||||
// For inventory items, visibility is handled by DOM
|
||||
@@ -149,22 +149,35 @@ function addToInventory(sprite) {
|
||||
window.gameAlert(`Added ${sprite.scenarioData.name} to inventory`, 'success', 'Item Collected', 3000);
|
||||
}
|
||||
|
||||
// If this is the Bluetooth scanner, show the toggle button
|
||||
if (sprite.scenarioData.type === "bluetooth_scanner") {
|
||||
const bluetoothToggle = document.getElementById('bluetooth-toggle');
|
||||
if (bluetoothToggle) {
|
||||
bluetoothToggle.style.display = 'flex';
|
||||
}
|
||||
// If this is the Bluetooth scanner, automatically open the minigame after adding to inventory
|
||||
if (sprite.scenarioData.type === "bluetooth_scanner" && window.startBluetoothScannerMinigame) {
|
||||
// Small delay to ensure the item is fully added to inventory
|
||||
setTimeout(() => {
|
||||
console.log('Auto-opening bluetooth scanner minigame after adding to inventory');
|
||||
window.startBluetoothScannerMinigame(itemImg);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// If this is the fingerprint kit, show the biometrics toggle button
|
||||
if (sprite.scenarioData.type === "fingerprint_kit") {
|
||||
const biometricsToggle = document.getElementById('biometrics-toggle');
|
||||
if (biometricsToggle) {
|
||||
biometricsToggle.style.display = 'flex';
|
||||
}
|
||||
// If this is the Fingerprint Kit, automatically open the minigame after adding to inventory
|
||||
if (sprite.scenarioData.type === "fingerprint_kit" && window.startBiometricsMinigame) {
|
||||
// Small delay to ensure the item is fully added to inventory
|
||||
setTimeout(() => {
|
||||
console.log('Auto-opening biometrics minigame after adding to inventory');
|
||||
window.startBiometricsMinigame(itemImg);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// If this is the Lockpick Set, automatically open the minigame after adding to inventory
|
||||
if ((sprite.scenarioData.type === "lockpick" || sprite.scenarioData.type === "lockpickset") && window.startLockpickSetMinigame) {
|
||||
// Small delay to ensure the item is fully added to inventory
|
||||
setTimeout(() => {
|
||||
console.log('Auto-opening lockpick set minigame after adding to inventory');
|
||||
window.startLockpickSetMinigame(itemImg);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// Fingerprint kit is now handled as a minigame when clicked from inventory
|
||||
|
||||
// Handle crypto workstation - use the proper modal implementation from helpers.js
|
||||
if (sprite.scenarioData.type === "workstation") {
|
||||
// Don't override the openCryptoWorkstation function - it's already properly defined in helpers.js
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
export function initializeUI() {
|
||||
console.log('UI panels system initialized');
|
||||
|
||||
// Note: Individual systems (notes.js, biometrics.js, bluetooth.js) handle their own panel setup
|
||||
// Note: Individual systems (notes.js, biometrics.js) handle their own panel setup
|
||||
// This file only provides utility functions for generic panel operations
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user