2024-12-19 17:57:10 +00:00
|
|
|
|
<!DOCTYPE html>
|
|
|
|
|
|
<html>
|
|
|
|
|
|
<head>
|
|
|
|
|
|
<meta charset="utf-8">
|
|
|
|
|
|
<title>Office Adventure Game</title>
|
|
|
|
|
|
<style>
|
|
|
|
|
|
body {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
min-height: 100vh;
|
|
|
|
|
|
background: #333;
|
|
|
|
|
|
}
|
|
|
|
|
|
#game-container {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
}
|
|
|
|
|
|
#loading {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 50%;
|
|
|
|
|
|
left: 50%;
|
|
|
|
|
|
transform: translate(-50%, -50%);
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
font-family: Arial, sans-serif;
|
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
2024-12-19 18:40:48 +00:00
|
|
|
|
|
2025-03-08 13:19:58 +00:00
|
|
|
|
/* Notification System */
|
|
|
|
|
|
#notification-container {
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
top: 20px;
|
|
|
|
|
|
right: 20px;
|
|
|
|
|
|
width: 300px;
|
|
|
|
|
|
max-width: 80%;
|
|
|
|
|
|
z-index: 2000;
|
|
|
|
|
|
font-family: Arial, sans-serif;
|
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.notification {
|
|
|
|
|
|
background-color: rgba(0, 0, 0, 0.8);
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
padding: 15px 20px;
|
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
border-radius: 5px;
|
|
|
|
|
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
transform: translateY(-20px);
|
|
|
|
|
|
pointer-events: auto;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.notification.show {
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
transform: translateY(0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.notification.info {
|
|
|
|
|
|
border-left: 4px solid #3498db;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.notification.success {
|
|
|
|
|
|
border-left: 4px solid #2ecc71;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.notification.warning {
|
|
|
|
|
|
border-left: 4px solid #f39c12;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.notification.error {
|
|
|
|
|
|
border-left: 4px solid #e74c3c;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.notification-title {
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
margin-bottom: 5px;
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.notification-message {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
line-height: 1.4;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.notification-close {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 10px;
|
|
|
|
|
|
right: 10px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
color: #aaa;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.notification-close:hover {
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.notification-progress {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
bottom: 0;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
height: 3px;
|
|
|
|
|
|
background-color: rgba(255, 255, 255, 0.5);
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Notes Panel */
|
|
|
|
|
|
#notes-panel {
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
bottom: 80px;
|
|
|
|
|
|
right: 20px;
|
|
|
|
|
|
width: 350px;
|
|
|
|
|
|
max-height: 500px;
|
|
|
|
|
|
background-color: rgba(0, 0, 0, 0.9);
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
border-radius: 5px;
|
|
|
|
|
|
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.5);
|
|
|
|
|
|
z-index: 1999;
|
|
|
|
|
|
font-family: Arial, sans-serif;
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
|
border: 1px solid #444;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#notes-header {
|
|
|
|
|
|
background-color: #222;
|
|
|
|
|
|
padding: 12px 15px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
border-bottom: 1px solid #444;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#notes-title {
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
color: #3498db;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#notes-close {
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
color: #aaa;
|
|
|
|
|
|
transition: color 0.2s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#notes-close:hover {
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#notes-search-container {
|
|
|
|
|
|
padding: 10px 15px;
|
|
|
|
|
|
background-color: #333;
|
|
|
|
|
|
border-bottom: 1px solid #444;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#notes-search {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
padding: 8px 10px;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
|
background-color: #222;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#notes-search:focus {
|
|
|
|
|
|
outline: none;
|
|
|
|
|
|
box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.5);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#notes-categories {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
padding: 5px 15px;
|
|
|
|
|
|
background-color: #2c2c2c;
|
|
|
|
|
|
border-bottom: 1px solid #444;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.notes-category {
|
|
|
|
|
|
padding: 5px 10px;
|
|
|
|
|
|
margin-right: 5px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.notes-category.active {
|
|
|
|
|
|
background-color: #3498db;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.notes-category:hover:not(.active) {
|
|
|
|
|
|
background-color: #444;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#notes-content {
|
|
|
|
|
|
padding: 15px;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
max-height: 350px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.note-item {
|
|
|
|
|
|
margin-bottom: 15px;
|
|
|
|
|
|
padding-bottom: 15px;
|
|
|
|
|
|
border-bottom: 1px solid #444;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: background-color 0.2s;
|
|
|
|
|
|
padding: 10px;
|
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.note-item:hover {
|
|
|
|
|
|
background-color: #333;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.note-item:last-child {
|
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
|
padding-bottom: 0;
|
|
|
|
|
|
border-bottom: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.note-title {
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
margin-bottom: 5px;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: #3498db;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.note-icons {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 5px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.note-icon {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #aaa;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.note-text {
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
line-height: 1.4;
|
|
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
|
max-height: 80px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
transition: max-height 0.3s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.note-item.expanded .note-text {
|
|
|
|
|
|
max-height: 1000px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.note-timestamp {
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
color: #888;
|
|
|
|
|
|
margin-top: 5px;
|
|
|
|
|
|
text-align: right;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#notes-toggle {
|
2025-03-09 00:07:56 +00:00
|
|
|
|
position: relative;
|
2025-03-08 13:19:58 +00:00
|
|
|
|
width: 60px;
|
|
|
|
|
|
height: 60px;
|
|
|
|
|
|
background-color: #3498db;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
|
|
|
|
|
z-index: 1998;
|
|
|
|
|
|
font-size: 28px;
|
|
|
|
|
|
transition: all 0.3s ease;
|
2025-03-09 00:07:56 +00:00
|
|
|
|
margin-left: 10px;
|
2025-03-08 13:19:58 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#notes-toggle:hover {
|
|
|
|
|
|
background-color: #2980b9;
|
|
|
|
|
|
transform: scale(1.1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#notes-count {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: -5px;
|
|
|
|
|
|
right: -5px;
|
|
|
|
|
|
background-color: #e74c3c;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
width: 24px;
|
|
|
|
|
|
height: 24px;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-12-19 18:40:48 +00:00
|
|
|
|
#laptop-popup {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
position: fixed;
|
2025-02-01 02:19:05 +00:00
|
|
|
|
top: 50%;
|
|
|
|
|
|
left: 50%;
|
|
|
|
|
|
transform: translate(-50%, -50%);
|
|
|
|
|
|
width: 90%;
|
|
|
|
|
|
max-width: 1200px;
|
|
|
|
|
|
height: calc(100% - 160px);
|
|
|
|
|
|
background: none;
|
|
|
|
|
|
z-index: 1000;
|
|
|
|
|
|
pointer-events: none;
|
2024-12-19 18:40:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.laptop-frame {
|
|
|
|
|
|
background: #1a1a1a;
|
|
|
|
|
|
border-radius: 15px;
|
|
|
|
|
|
padding: 20px;
|
2025-02-01 02:19:05 +00:00
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 75%;
|
2024-12-19 18:40:48 +00:00
|
|
|
|
box-shadow: 0 0 30px rgba(0, 0, 0, 0.5);
|
2025-02-01 02:19:05 +00:00
|
|
|
|
margin-bottom: 80px;
|
|
|
|
|
|
pointer-events: auto;
|
2024-12-19 18:40:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.laptop-screen {
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.title-bar {
|
|
|
|
|
|
background: #333;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
padding: 8px 15px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.close-btn {
|
|
|
|
|
|
background: none;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
padding: 0 5px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.close-btn:hover {
|
|
|
|
|
|
color: #ff4444;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.laptop-screen iframe {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
}
|
2025-02-01 02:09:13 +00:00
|
|
|
|
|
|
|
|
|
|
#cyberchef-container {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#cyberchef-container iframe {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
}
|
2025-02-01 02:19:05 +00:00
|
|
|
|
|
|
|
|
|
|
.popup-overlay {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: calc(100% - 80px);
|
|
|
|
|
|
background: rgba(0, 0, 0, 0.5);
|
|
|
|
|
|
z-index: 999;
|
|
|
|
|
|
}
|
2025-03-08 15:00:17 +00:00
|
|
|
|
|
|
|
|
|
|
/* Bluetooth Scanner Panel */
|
|
|
|
|
|
#bluetooth-panel {
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
bottom: 80px;
|
|
|
|
|
|
right: 90px;
|
|
|
|
|
|
width: 350px;
|
|
|
|
|
|
max-height: 500px;
|
|
|
|
|
|
background-color: rgba(0, 0, 0, 0.9);
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
border-radius: 5px;
|
|
|
|
|
|
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.5);
|
|
|
|
|
|
z-index: 1999;
|
|
|
|
|
|
font-family: Arial, sans-serif;
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
|
border: 1px solid #444;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#bluetooth-header {
|
|
|
|
|
|
background-color: #222;
|
|
|
|
|
|
padding: 12px 15px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
border-bottom: 1px solid #444;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#bluetooth-title {
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
color: #9b59b6;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#bluetooth-close {
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
color: #aaa;
|
|
|
|
|
|
transition: color 0.2s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#bluetooth-close:hover {
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#bluetooth-search-container {
|
|
|
|
|
|
padding: 10px 15px;
|
|
|
|
|
|
background-color: #333;
|
|
|
|
|
|
border-bottom: 1px solid #444;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#bluetooth-search {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
padding: 8px 10px;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
|
background-color: #222;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#bluetooth-search:focus {
|
|
|
|
|
|
outline: none;
|
|
|
|
|
|
box-shadow: 0 0 0 2px rgba(155, 89, 182, 0.5);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#bluetooth-categories {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
padding: 5px 15px;
|
|
|
|
|
|
background-color: #2c2c2c;
|
|
|
|
|
|
border-bottom: 1px solid #444;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.bluetooth-category {
|
|
|
|
|
|
padding: 5px 10px;
|
|
|
|
|
|
margin-right: 5px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.bluetooth-category.active {
|
|
|
|
|
|
background-color: #9b59b6;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.bluetooth-category:hover:not(.active) {
|
|
|
|
|
|
background-color: #444;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#bluetooth-content {
|
|
|
|
|
|
padding: 15px;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
max-height: 350px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.bluetooth-device {
|
|
|
|
|
|
margin-bottom: 15px;
|
|
|
|
|
|
padding-bottom: 15px;
|
|
|
|
|
|
border-bottom: 1px solid #444;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: background-color 0.2s;
|
|
|
|
|
|
padding: 10px;
|
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.bluetooth-device:hover {
|
|
|
|
|
|
background-color: #333;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.bluetooth-device:last-child {
|
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
|
padding-bottom: 0;
|
|
|
|
|
|
border-bottom: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.bluetooth-device-name {
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
margin-bottom: 5px;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: #9b59b6;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.bluetooth-device-icons {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 5px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.bluetooth-device-icon {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #aaa;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.bluetooth-device-details {
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
line-height: 1.4;
|
|
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
|
max-height: 80px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
transition: max-height 0.3s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.bluetooth-device.expanded .bluetooth-device-details {
|
|
|
|
|
|
max-height: 1000px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.bluetooth-device-timestamp {
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
color: #888;
|
|
|
|
|
|
margin-top: 5px;
|
|
|
|
|
|
text-align: right;
|
|
|
|
|
|
}
|
2025-03-08 15:53:08 +00:00
|
|
|
|
|
|
|
|
|
|
/* Bluetooth Signal Strength Bar */
|
|
|
|
|
|
.bluetooth-signal-bar-container {
|
2025-03-08 16:00:25 +00:00
|
|
|
|
margin: 0;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: flex-end;
|
|
|
|
|
|
height: 16px;
|
2025-03-08 15:53:08 +00:00
|
|
|
|
}
|
2025-03-08 16:00:25 +00:00
|
|
|
|
|
|
|
|
|
|
.bluetooth-signal-bars {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: flex-end;
|
|
|
|
|
|
height: 16px;
|
|
|
|
|
|
gap: 1px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-08 15:53:08 +00:00
|
|
|
|
.bluetooth-signal-bar {
|
2025-03-08 16:00:25 +00:00
|
|
|
|
width: 3px;
|
|
|
|
|
|
background-color: #444;
|
|
|
|
|
|
border-radius: 1px;
|
|
|
|
|
|
transition: background-color 0.3s ease;
|
2025-03-08 15:53:08 +00:00
|
|
|
|
}
|
2025-03-08 16:00:25 +00:00
|
|
|
|
|
|
|
|
|
|
.bluetooth-signal-bar.active {
|
|
|
|
|
|
background-color: currentColor;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.bluetooth-signal-bar:nth-child(1) { height: 3px; }
|
|
|
|
|
|
.bluetooth-signal-bar:nth-child(2) { height: 6px; }
|
|
|
|
|
|
.bluetooth-signal-bar:nth-child(3) { height: 9px; }
|
|
|
|
|
|
.bluetooth-signal-bar:nth-child(4) { height: 12px; }
|
|
|
|
|
|
.bluetooth-signal-bar:nth-child(5) { height: 16px; }
|
|
|
|
|
|
|
2025-03-08 15:53:08 +00:00
|
|
|
|
.bluetooth-signal-text {
|
2025-03-08 16:00:25 +00:00
|
|
|
|
display: none;
|
2025-03-08 15:53:08 +00:00
|
|
|
|
}
|
2025-03-08 15:00:17 +00:00
|
|
|
|
|
|
|
|
|
|
#bluetooth-toggle {
|
2025-03-09 00:07:56 +00:00
|
|
|
|
position: relative;
|
2025-03-08 15:00:17 +00:00
|
|
|
|
width: 60px;
|
|
|
|
|
|
height: 60px;
|
|
|
|
|
|
background-color: #9b59b6;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
|
|
|
|
|
z-index: 1998;
|
|
|
|
|
|
font-size: 28px;
|
|
|
|
|
|
transition: all 0.3s ease;
|
2025-03-09 00:07:56 +00:00
|
|
|
|
margin-left: 10px;
|
2025-03-08 15:00:17 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#bluetooth-toggle:hover {
|
|
|
|
|
|
background-color: #8e44ad;
|
|
|
|
|
|
transform: scale(1.1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#bluetooth-count {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: -5px;
|
|
|
|
|
|
right: -5px;
|
|
|
|
|
|
background-color: #e74c3c;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
width: 24px;
|
|
|
|
|
|
height: 24px;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
2025-03-08 16:06:45 +00:00
|
|
|
|
|
|
|
|
|
|
/* Bluetooth Pairing Button */
|
|
|
|
|
|
.bluetooth-pair-button {
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
|
padding: 5px 10px;
|
|
|
|
|
|
background-color: #9b59b6;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: background-color 0.2s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.bluetooth-pair-button:hover {
|
|
|
|
|
|
background-color: #8e44ad;
|
|
|
|
|
|
}
|
2025-03-08 16:16:42 +00:00
|
|
|
|
|
|
|
|
|
|
.bluetooth-pair-button.paired {
|
|
|
|
|
|
background-color: #27ae60;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.bluetooth-pair-button.paired:hover {
|
|
|
|
|
|
background-color: #219653;
|
|
|
|
|
|
}
|
2025-03-08 16:52:05 +00:00
|
|
|
|
|
2025-03-08 21:09:17 +00:00
|
|
|
|
/* Bluetooth Unlock Button */
|
|
|
|
|
|
.bluetooth-unlock-button {
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
|
padding: 5px 10px;
|
|
|
|
|
|
background-color: #2980b9;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: background-color 0.2s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.bluetooth-unlock-button:hover {
|
|
|
|
|
|
background-color: #3498db;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-08 16:52:05 +00:00
|
|
|
|
/* Add a class to preserve hover state during updates */
|
|
|
|
|
|
.bluetooth-device.hover-preserved {
|
|
|
|
|
|
background-color: #333;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Add a more specific rule to prevent hover state from being lost when hovering over child elements */
|
|
|
|
|
|
.bluetooth-device:hover .bluetooth-device-name,
|
|
|
|
|
|
.bluetooth-device:hover .bluetooth-device-details,
|
|
|
|
|
|
.bluetooth-device:hover .bluetooth-device-timestamp,
|
|
|
|
|
|
.bluetooth-device:hover .bluetooth-pair-button {
|
|
|
|
|
|
pointer-events: auto;
|
|
|
|
|
|
}
|
2025-03-08 22:03:24 +00:00
|
|
|
|
|
|
|
|
|
|
/* Biometrics Panel */
|
|
|
|
|
|
#biometrics-panel {
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
bottom: 80px;
|
|
|
|
|
|
right: 160px;
|
|
|
|
|
|
width: 350px;
|
|
|
|
|
|
max-height: 500px;
|
|
|
|
|
|
background-color: rgba(0, 0, 0, 0.9);
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
border-radius: 5px;
|
|
|
|
|
|
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.5);
|
|
|
|
|
|
z-index: 1999;
|
|
|
|
|
|
font-family: Arial, sans-serif;
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
|
border: 1px solid #444;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#biometrics-header {
|
|
|
|
|
|
background-color: #222;
|
|
|
|
|
|
padding: 12px 15px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
border-bottom: 1px solid #444;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#biometrics-title {
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
color: #e74c3c;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#biometrics-close {
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
color: #aaa;
|
|
|
|
|
|
transition: color 0.2s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#biometrics-close:hover {
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#biometrics-search-container {
|
|
|
|
|
|
padding: 10px 15px;
|
|
|
|
|
|
background-color: #333;
|
|
|
|
|
|
border-bottom: 1px solid #444;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#biometrics-search {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
padding: 8px 10px;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
|
background-color: #222;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#biometrics-search:focus {
|
|
|
|
|
|
outline: none;
|
|
|
|
|
|
box-shadow: 0 0 0 2px rgba(231, 76, 60, 0.5);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#biometrics-categories {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
padding: 5px 15px;
|
|
|
|
|
|
background-color: #2c2c2c;
|
|
|
|
|
|
border-bottom: 1px solid #444;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.biometrics-category {
|
|
|
|
|
|
padding: 5px 10px;
|
|
|
|
|
|
margin-right: 5px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.biometrics-category.active {
|
|
|
|
|
|
background-color: #e74c3c;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.biometrics-category:hover:not(.active) {
|
|
|
|
|
|
background-color: #444;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#biometrics-content {
|
|
|
|
|
|
padding: 15px;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
max-height: 350px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.biometric-sample {
|
|
|
|
|
|
margin-bottom: 15px;
|
|
|
|
|
|
padding-bottom: 15px;
|
|
|
|
|
|
border-bottom: 1px solid #444;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: background-color 0.2s;
|
|
|
|
|
|
padding: 10px;
|
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.biometric-sample:hover {
|
|
|
|
|
|
background-color: #333;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.biometric-sample:last-child {
|
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
|
padding-bottom: 0;
|
|
|
|
|
|
border-bottom: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.biometric-sample-name {
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
margin-bottom: 5px;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: #e74c3c;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.biometric-sample-icons {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 5px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.biometric-sample-icon {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #aaa;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.biometric-sample-details {
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
line-height: 1.4;
|
|
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
|
max-height: 80px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
transition: max-height 0.3s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.biometric-sample.expanded .biometric-sample-details {
|
|
|
|
|
|
max-height: 1000px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.biometric-sample-timestamp {
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
color: #888;
|
|
|
|
|
|
margin-top: 5px;
|
|
|
|
|
|
text-align: right;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.biometric-quality-bar {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 5px;
|
|
|
|
|
|
background: #333;
|
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
|
border-radius: 2px;
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.biometric-quality-fill {
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
border-radius: 2px;
|
|
|
|
|
|
transition: width 0.3s ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#biometrics-toggle {
|
2025-03-09 00:07:56 +00:00
|
|
|
|
position: relative;
|
2025-03-08 22:03:24 +00:00
|
|
|
|
width: 60px;
|
|
|
|
|
|
height: 60px;
|
|
|
|
|
|
background-color: #e74c3c;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
|
|
|
|
|
z-index: 1998;
|
|
|
|
|
|
font-size: 28px;
|
|
|
|
|
|
transition: all 0.3s ease;
|
2025-03-09 00:07:56 +00:00
|
|
|
|
margin-left: 10px;
|
2025-03-08 22:03:24 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#biometrics-toggle:hover {
|
|
|
|
|
|
background-color: #c0392b;
|
|
|
|
|
|
transform: scale(1.1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#biometrics-count {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
right: 0;
|
|
|
|
|
|
background-color: #c0392b;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
width: 22px;
|
|
|
|
|
|
height: 22px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Rest of existing styles follow */
|
2025-03-09 00:07:56 +00:00
|
|
|
|
.biometric-sample-timestamp {
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
color: #888;
|
|
|
|
|
|
margin-top: 5px;
|
|
|
|
|
|
text-align: right;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Toggle Buttons Container */
|
|
|
|
|
|
#toggle-buttons-container {
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
bottom: 20px;
|
|
|
|
|
|
right: 20px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: row-reverse;
|
|
|
|
|
|
z-index: 1998;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Game container */
|
|
|
|
|
|
#game-container {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
}
|
2024-12-19 17:57:10 +00:00
|
|
|
|
</style>
|
|
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/phaser@3.60.0/dist/phaser.min.js"></script>
|
|
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/easystarjs@0.4.4/bin/easystar-0.4.4.js"></script>
|
|
|
|
|
|
</head>
|
|
|
|
|
|
<body>
|
|
|
|
|
|
<div id="game-container">
|
|
|
|
|
|
<div id="loading">Loading...</div>
|
|
|
|
|
|
</div>
|
2025-03-08 13:19:58 +00:00
|
|
|
|
|
|
|
|
|
|
<!-- Notification System -->
|
|
|
|
|
|
<div id="notification-container"></div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Notes Panel -->
|
|
|
|
|
|
<div id="notes-panel">
|
|
|
|
|
|
<div id="notes-header">
|
|
|
|
|
|
<div id="notes-title">Notes & Information</div>
|
|
|
|
|
|
<div id="notes-close">×</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div id="notes-search-container">
|
|
|
|
|
|
<input type="text" id="notes-search" placeholder="Search notes...">
|
2025-02-18 21:29:02 +00:00
|
|
|
|
</div>
|
2025-03-08 13:19:58 +00:00
|
|
|
|
<div id="notes-categories">
|
|
|
|
|
|
<div class="notes-category active" data-category="all">All</div>
|
|
|
|
|
|
<div class="notes-category" data-category="important">Important</div>
|
|
|
|
|
|
<div class="notes-category" data-category="unread">Unread</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div id="notes-content"></div>
|
|
|
|
|
|
</div>
|
2025-03-09 00:07:56 +00:00
|
|
|
|
|
|
|
|
|
|
<!-- Toggle Buttons Container -->
|
|
|
|
|
|
<div id="toggle-buttons-container">
|
|
|
|
|
|
<div id="notes-toggle">
|
|
|
|
|
|
<span>📝</span>
|
|
|
|
|
|
<div id="notes-count">0</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div id="bluetooth-toggle" style="display: none;">
|
|
|
|
|
|
<span>📡</span>
|
|
|
|
|
|
<div id="bluetooth-count">0</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div id="biometrics-toggle" style="display: none;">
|
|
|
|
|
|
<span>👆</span>
|
|
|
|
|
|
<div id="biometrics-count">0</div>
|
|
|
|
|
|
</div>
|
2025-02-18 21:29:02 +00:00
|
|
|
|
</div>
|
2025-03-08 13:19:58 +00:00
|
|
|
|
|
2025-03-08 15:00:17 +00:00
|
|
|
|
<!-- 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>
|
2025-03-08 22:03:24 +00:00
|
|
|
|
|
|
|
|
|
|
<!-- 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 class="biometrics-category" data-category="spoofed">Spoofed</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div id="biometrics-content"></div>
|
|
|
|
|
|
</div>
|
2025-03-08 15:00:17 +00:00
|
|
|
|
|
2024-12-19 17:57:10 +00:00
|
|
|
|
<script>
|
|
|
|
|
|
const config = {
|
|
|
|
|
|
type: Phaser.AUTO,
|
|
|
|
|
|
width: 1280,
|
|
|
|
|
|
height: 720,
|
|
|
|
|
|
parent: 'game-container',
|
|
|
|
|
|
pixelArt: true,
|
|
|
|
|
|
physics: {
|
|
|
|
|
|
default: 'arcade',
|
|
|
|
|
|
arcade: {
|
|
|
|
|
|
gravity: { y: 0 },
|
|
|
|
|
|
debug: true
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
scene: {
|
|
|
|
|
|
preload: preload,
|
|
|
|
|
|
create: create,
|
|
|
|
|
|
update: update
|
|
|
|
|
|
},
|
|
|
|
|
|
inventory: {
|
|
|
|
|
|
items: [],
|
|
|
|
|
|
display: null
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
const TILE_SIZE = 48;
|
|
|
|
|
|
const DOOR_ALIGN_OVERLAP = 48*3;
|
|
|
|
|
|
const GRID_SIZE = 32;
|
|
|
|
|
|
const MOVEMENT_SPEED = 150;
|
|
|
|
|
|
const ARRIVAL_THRESHOLD = 8;
|
|
|
|
|
|
const PATH_UPDATE_INTERVAL = 500;
|
|
|
|
|
|
const STUCK_THRESHOLD = 1;
|
|
|
|
|
|
const STUCK_TIME = 500;
|
|
|
|
|
|
// Hide rooms initially and on exit
|
|
|
|
|
|
const hideRoomsInitially = true;
|
|
|
|
|
|
const hideRoomsOnExit = false;
|
|
|
|
|
|
const hideNonAdjacentRooms = false;
|
|
|
|
|
|
|
2025-03-08 13:19:58 +00:00
|
|
|
|
// Debug system variables - moved to the top
|
|
|
|
|
|
let debugMode = false;
|
|
|
|
|
|
let debugLevel = 1; // 1 = basic, 2 = detailed, 3 = verbose
|
|
|
|
|
|
let visualDebugMode = false;
|
|
|
|
|
|
let fpsCounter = null;
|
|
|
|
|
|
|
|
|
|
|
|
// Notes and notification system
|
|
|
|
|
|
const gameNotes = [];
|
|
|
|
|
|
let unreadNotes = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// Show a notification instead of using alert()
|
|
|
|
|
|
function showNotification(message, type = 'info', title = '', duration = 5000) {
|
|
|
|
|
|
const notificationContainer = document.getElementById('notification-container');
|
|
|
|
|
|
|
|
|
|
|
|
// Create notification element
|
|
|
|
|
|
const notification = document.createElement('div');
|
|
|
|
|
|
notification.className = `notification ${type}`;
|
|
|
|
|
|
|
|
|
|
|
|
// Create notification content
|
|
|
|
|
|
let notificationContent = '';
|
|
|
|
|
|
if (title) {
|
|
|
|
|
|
notificationContent += `<div class="notification-title">${title}</div>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
notificationContent += `<div class="notification-message">${message}</div>`;
|
|
|
|
|
|
notificationContent += `<div class="notification-close">×</div>`;
|
|
|
|
|
|
|
|
|
|
|
|
if (duration > 0) {
|
|
|
|
|
|
notificationContent += `<div class="notification-progress"></div>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
notification.innerHTML = notificationContent;
|
|
|
|
|
|
|
|
|
|
|
|
// Add to container
|
|
|
|
|
|
notificationContainer.appendChild(notification);
|
|
|
|
|
|
|
|
|
|
|
|
// Show notification with animation
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
notification.classList.add('show');
|
|
|
|
|
|
}, 10);
|
|
|
|
|
|
|
|
|
|
|
|
// Add progress animation if duration is set
|
|
|
|
|
|
if (duration > 0) {
|
|
|
|
|
|
const progress = notification.querySelector('.notification-progress');
|
|
|
|
|
|
progress.style.transition = `width ${duration}ms linear`;
|
|
|
|
|
|
|
|
|
|
|
|
// Start progress animation
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
progress.style.width = '0%';
|
|
|
|
|
|
}, 10);
|
|
|
|
|
|
|
|
|
|
|
|
// Remove notification after duration
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
removeNotification(notification);
|
|
|
|
|
|
}, duration);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Add close button event listener
|
|
|
|
|
|
const closeBtn = notification.querySelector('.notification-close');
|
|
|
|
|
|
closeBtn.addEventListener('click', () => {
|
|
|
|
|
|
removeNotification(notification);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return notification;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Remove a notification with animation
|
|
|
|
|
|
function removeNotification(notification) {
|
|
|
|
|
|
notification.classList.remove('show');
|
|
|
|
|
|
|
|
|
|
|
|
// Remove from DOM after animation
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
if (notification.parentNode) {
|
|
|
|
|
|
notification.parentNode.removeChild(notification);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 300);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Add a note to the notes panel
|
|
|
|
|
|
function addNote(title, text, important = false) {
|
|
|
|
|
|
// Check if a note with the same title and text already exists
|
|
|
|
|
|
const noteExists = gameNotes.some(note => note.title === title && note.text === text);
|
|
|
|
|
|
|
|
|
|
|
|
// If the note already exists, don't add it again
|
|
|
|
|
|
if (noteExists) {
|
|
|
|
|
|
debugLog(`Note "${title}" already exists, not adding duplicate`, 2);
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const note = {
|
|
|
|
|
|
id: Date.now(),
|
|
|
|
|
|
title: title,
|
|
|
|
|
|
text: text,
|
|
|
|
|
|
timestamp: new Date(),
|
|
|
|
|
|
read: false,
|
|
|
|
|
|
important: important
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
gameNotes.push(note);
|
|
|
|
|
|
updateNotesPanel();
|
|
|
|
|
|
updateNotesCount();
|
|
|
|
|
|
|
|
|
|
|
|
// Show notification for new note
|
|
|
|
|
|
showNotification(`New note added: ${title}`, 'info', 'Note Added', 3000);
|
|
|
|
|
|
|
|
|
|
|
|
return note;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Update the notes panel with current notes
|
|
|
|
|
|
function updateNotesPanel() {
|
|
|
|
|
|
const notesContent = document.getElementById('notes-content');
|
|
|
|
|
|
const searchTerm = document.getElementById('notes-search')?.value?.toLowerCase() || '';
|
|
|
|
|
|
|
|
|
|
|
|
// Get active category
|
|
|
|
|
|
const activeCategory = document.querySelector('.notes-category.active')?.dataset.category || 'all';
|
|
|
|
|
|
|
|
|
|
|
|
// Filter notes based on search and category
|
|
|
|
|
|
let filteredNotes = [...gameNotes];
|
|
|
|
|
|
|
|
|
|
|
|
// Apply category filter
|
|
|
|
|
|
if (activeCategory === 'important') {
|
|
|
|
|
|
filteredNotes = filteredNotes.filter(note => note.important);
|
|
|
|
|
|
} else if (activeCategory === 'unread') {
|
|
|
|
|
|
filteredNotes = filteredNotes.filter(note => !note.read);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Apply search filter
|
|
|
|
|
|
if (searchTerm) {
|
|
|
|
|
|
filteredNotes = filteredNotes.filter(note =>
|
|
|
|
|
|
note.title.toLowerCase().includes(searchTerm) ||
|
|
|
|
|
|
note.text.toLowerCase().includes(searchTerm)
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Sort notes with important ones first, then by timestamp (newest first)
|
|
|
|
|
|
filteredNotes.sort((a, b) => {
|
|
|
|
|
|
if (a.important !== b.important) {
|
|
|
|
|
|
return a.important ? -1 : 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
return b.timestamp - a.timestamp;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Clear current content
|
|
|
|
|
|
notesContent.innerHTML = '';
|
|
|
|
|
|
|
|
|
|
|
|
// Add notes
|
|
|
|
|
|
if (filteredNotes.length === 0) {
|
|
|
|
|
|
if (searchTerm) {
|
|
|
|
|
|
notesContent.innerHTML = '<div class="note-item">No notes match your search.</div>';
|
|
|
|
|
|
} else if (activeCategory !== 'all') {
|
|
|
|
|
|
notesContent.innerHTML = `<div class="note-item">No ${activeCategory} notes found.</div>`;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
notesContent.innerHTML = '<div class="note-item">No notes yet.</div>';
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
filteredNotes.forEach(note => {
|
|
|
|
|
|
const noteElement = document.createElement('div');
|
|
|
|
|
|
noteElement.className = 'note-item';
|
|
|
|
|
|
noteElement.dataset.id = note.id;
|
|
|
|
|
|
|
|
|
|
|
|
// Format the timestamp
|
|
|
|
|
|
const timestamp = new Date(note.timestamp);
|
|
|
|
|
|
const formattedDate = timestamp.toLocaleDateString();
|
|
|
|
|
|
const formattedTime = timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
|
|
|
|
|
|
|
|
|
|
let noteContent = `<div class="note-title">
|
|
|
|
|
|
<span>${note.title}</span>
|
|
|
|
|
|
<div class="note-icons">`;
|
|
|
|
|
|
|
|
|
|
|
|
if (note.important) {
|
|
|
|
|
|
noteContent += `<span class="note-icon">⭐</span>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!note.read) {
|
|
|
|
|
|
noteContent += `<span class="note-icon">📌</span>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
noteContent += `</div></div>`;
|
|
|
|
|
|
noteContent += `<div class="note-text">${note.text}</div>`;
|
|
|
|
|
|
noteContent += `<div class="note-timestamp">${formattedDate} ${formattedTime}</div>`;
|
|
|
|
|
|
|
|
|
|
|
|
noteElement.innerHTML = noteContent;
|
|
|
|
|
|
|
|
|
|
|
|
// Toggle expanded state when clicked
|
|
|
|
|
|
noteElement.addEventListener('click', () => {
|
|
|
|
|
|
noteElement.classList.toggle('expanded');
|
|
|
|
|
|
|
|
|
|
|
|
// Mark as read when expanded
|
|
|
|
|
|
if (!note.read && noteElement.classList.contains('expanded')) {
|
|
|
|
|
|
note.read = true;
|
|
|
|
|
|
updateNotesCount();
|
|
|
|
|
|
updateNotesPanel();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
notesContent.appendChild(noteElement);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Update the unread notes count
|
|
|
|
|
|
function updateNotesCount() {
|
|
|
|
|
|
const notesCount = document.getElementById('notes-count');
|
|
|
|
|
|
unreadNotes = gameNotes.filter(note => !note.read).length;
|
|
|
|
|
|
|
|
|
|
|
|
notesCount.textContent = unreadNotes;
|
|
|
|
|
|
notesCount.style.display = unreadNotes > 0 ? 'flex' : 'none';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Toggle the notes panel
|
|
|
|
|
|
function toggleNotesPanel() {
|
|
|
|
|
|
const notesPanel = document.getElementById('notes-panel');
|
|
|
|
|
|
const isVisible = notesPanel.style.display === 'block';
|
|
|
|
|
|
|
|
|
|
|
|
notesPanel.style.display = isVisible ? 'none' : 'block';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Replace alert with our custom notification system
|
|
|
|
|
|
function gameAlert(message, type = 'info', title = '', duration = 5000) {
|
|
|
|
|
|
return showNotification(message, type, title, duration);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Debug logging function that only logs when debug mode is active
|
|
|
|
|
|
function debugLog(message, data = null, level = 1) {
|
|
|
|
|
|
if (!debugMode || debugLevel < level) return;
|
|
|
|
|
|
|
|
|
|
|
|
// Check if the first argument is a string
|
|
|
|
|
|
if (typeof message === 'string') {
|
|
|
|
|
|
// Create the formatted debug message
|
|
|
|
|
|
const formattedMessage = `[DEBUG] === ${message} ===`;
|
|
|
|
|
|
|
|
|
|
|
|
// Determine color based on message content
|
|
|
|
|
|
let color = '#0077FF'; // Default blue for general info
|
|
|
|
|
|
let fontWeight = 'bold';
|
|
|
|
|
|
|
|
|
|
|
|
// Success messages - green
|
|
|
|
|
|
if (message.includes('SUCCESS') ||
|
|
|
|
|
|
message.includes('UNLOCKED') ||
|
|
|
|
|
|
message.includes('NOT LOCKED')) {
|
|
|
|
|
|
color = '#00AA00'; // Green
|
|
|
|
|
|
}
|
|
|
|
|
|
// Error/failure messages - red
|
|
|
|
|
|
else if (message.includes('FAIL') ||
|
|
|
|
|
|
message.includes('ERROR') ||
|
|
|
|
|
|
message.includes('NO LOCK REQUIREMENTS FOUND')) {
|
|
|
|
|
|
color = '#DD0000'; // Red
|
|
|
|
|
|
}
|
|
|
|
|
|
// Sensitive information - purple
|
|
|
|
|
|
else if (message.includes('PIN') ||
|
|
|
|
|
|
message.includes('PASSWORD') ||
|
|
|
|
|
|
message.includes('KEY') ||
|
|
|
|
|
|
message.includes('LOCK REQUIREMENTS')) {
|
|
|
|
|
|
color = '#AA00AA'; // Purple
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Add level indicator to the message
|
|
|
|
|
|
const levelIndicator = level > 1 ? ` [L${level}]` : '';
|
|
|
|
|
|
const finalMessage = formattedMessage + levelIndicator;
|
|
|
|
|
|
|
|
|
|
|
|
// Log with formatting
|
|
|
|
|
|
if (data) {
|
|
|
|
|
|
console.log(`%c${finalMessage}`, `color: ${color}; font-weight: ${fontWeight};`, data);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log(`%c${finalMessage}`, `color: ${color}; font-weight: ${fontWeight};`);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// If not a string, just log as is
|
|
|
|
|
|
console.log(message, data);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Function to display tension debug info only when debug mode is active
|
|
|
|
|
|
function logTensionDebugInfo() {
|
|
|
|
|
|
if (!debugMode) return; // Skip all calculations if debug mode is off
|
|
|
|
|
|
|
|
|
|
|
|
// Your existing debug code here
|
|
|
|
|
|
debugLog("Tension debug information:", null, 2);
|
|
|
|
|
|
// Add other debug information as needed
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Listen for backtick key to toggle debug mode
|
|
|
|
|
|
document.addEventListener('keydown', function(event) {
|
|
|
|
|
|
// Toggle debug mode with backtick
|
|
|
|
|
|
if (event.key === '`') {
|
|
|
|
|
|
if (event.shiftKey) {
|
|
|
|
|
|
// Toggle visual debug mode with Shift+backtick
|
|
|
|
|
|
visualDebugMode = !visualDebugMode;
|
|
|
|
|
|
console.log(`%c[DEBUG] === VISUAL DEBUG MODE ${visualDebugMode ? 'ENABLED' : 'DISABLED'} ===`,
|
|
|
|
|
|
`color: ${visualDebugMode ? '#00AA00' : '#DD0000'}; font-weight: bold;`);
|
|
|
|
|
|
|
|
|
|
|
|
// Update physics debug display if game exists
|
|
|
|
|
|
if (game && game.scene && game.scene.scenes && game.scene.scenes[0]) {
|
|
|
|
|
|
const scene = game.scene.scenes[0];
|
|
|
|
|
|
if (scene.physics && scene.physics.world) {
|
|
|
|
|
|
scene.physics.world.drawDebug = debugMode && visualDebugMode;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (event.ctrlKey) {
|
|
|
|
|
|
// Cycle through debug levels with Ctrl+backtick
|
|
|
|
|
|
if (debugMode) {
|
|
|
|
|
|
debugLevel = (debugLevel % 3) + 1; // Cycle through 1, 2, 3
|
|
|
|
|
|
console.log(`%c[DEBUG] === DEBUG LEVEL ${debugLevel} ===`,
|
|
|
|
|
|
`color: #0077FF; font-weight: bold;`);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Regular debug mode toggle
|
|
|
|
|
|
debugMode = !debugMode;
|
|
|
|
|
|
console.log(`%c[DEBUG] === DEBUG MODE ${debugMode ? 'ENABLED' : 'DISABLED'} ===`,
|
|
|
|
|
|
`color: ${debugMode ? '#00AA00' : '#DD0000'}; font-weight: bold;`);
|
|
|
|
|
|
|
|
|
|
|
|
// Update physics debug display if game exists
|
|
|
|
|
|
if (game && game.scene && game.scene.scenes && game.scene.scenes[0]) {
|
|
|
|
|
|
const scene = game.scene.scenes[0];
|
|
|
|
|
|
if (scene.physics && scene.physics.world) {
|
|
|
|
|
|
scene.physics.world.drawDebug = debugMode && visualDebugMode;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Initialize notes panel
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
|
|
|
|
// Set up notes toggle button
|
|
|
|
|
|
const notesToggle = document.getElementById('notes-toggle');
|
|
|
|
|
|
notesToggle.addEventListener('click', toggleNotesPanel);
|
|
|
|
|
|
|
|
|
|
|
|
// Set up notes close button
|
|
|
|
|
|
const notesClose = document.getElementById('notes-close');
|
|
|
|
|
|
notesClose.addEventListener('click', toggleNotesPanel);
|
|
|
|
|
|
|
|
|
|
|
|
// Set up search functionality
|
|
|
|
|
|
const notesSearch = document.getElementById('notes-search');
|
|
|
|
|
|
notesSearch.addEventListener('input', updateNotesPanel);
|
|
|
|
|
|
|
|
|
|
|
|
// Set up category filters
|
|
|
|
|
|
const categories = document.querySelectorAll('.notes-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 notes panel
|
|
|
|
|
|
updateNotesPanel();
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Initialize notes count
|
|
|
|
|
|
updateNotesCount();
|
2025-03-08 15:00:17 +00:00
|
|
|
|
|
|
|
|
|
|
// Initialize Bluetooth scanner panel
|
|
|
|
|
|
initializeBluetoothPanel();
|
2025-03-08 22:03:24 +00:00
|
|
|
|
|
|
|
|
|
|
// Initialize biometrics panel
|
|
|
|
|
|
initializeBiometricsPanel();
|
2025-03-08 13:19:58 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Function to create or update the FPS counter
|
|
|
|
|
|
function updateFPSCounter() {
|
|
|
|
|
|
if (fpsCounter) {
|
|
|
|
|
|
fpsCounter.textContent = `FPS: ${Math.round(game.loop.actualFps)}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-12-19 17:57:10 +00:00
|
|
|
|
// Declare gameScenario as let (not const) so we can assign it later
|
|
|
|
|
|
let gameScenario = null; // Initialize as null
|
|
|
|
|
|
|
|
|
|
|
|
let game = new Phaser.Game(config);
|
|
|
|
|
|
let player;
|
|
|
|
|
|
let cursors;
|
|
|
|
|
|
let rooms = {};
|
|
|
|
|
|
let currentRoom;
|
|
|
|
|
|
let inventory = {
|
|
|
|
|
|
items: [],
|
|
|
|
|
|
container: null
|
|
|
|
|
|
};
|
|
|
|
|
|
let objectsGroup;
|
|
|
|
|
|
let wallsLayer;
|
|
|
|
|
|
let discoveredRooms = new Set();
|
|
|
|
|
|
let pathfinder;
|
|
|
|
|
|
let currentPath = [];
|
|
|
|
|
|
let isMoving = false;
|
|
|
|
|
|
let targetPoint = null;
|
|
|
|
|
|
let lastPathUpdateTime = 0;
|
|
|
|
|
|
let stuckTimer = 0;
|
|
|
|
|
|
let lastPosition = null;
|
|
|
|
|
|
let stuckTime = 0;
|
|
|
|
|
|
let currentPlayerRoom = null;
|
|
|
|
|
|
let lastPlayerPosition = { x: 0, y: 0 };
|
|
|
|
|
|
const ROOM_CHECK_THRESHOLD = 32; // Only check for room changes when player moves this many pixels
|
|
|
|
|
|
|
|
|
|
|
|
// Add these constants at the top with other constants
|
|
|
|
|
|
const INTERACTION_CHECK_INTERVAL = 100; // Only check interactions every 100ms
|
|
|
|
|
|
const INTERACTION_RANGE = 2 * TILE_SIZE;
|
|
|
|
|
|
const INTERACTION_RANGE_SQ = INTERACTION_RANGE * INTERACTION_RANGE;
|
|
|
|
|
|
|
2025-02-18 17:01:17 +00:00
|
|
|
|
// Bluetooth constants
|
|
|
|
|
|
const BLUETOOTH_SCAN_RANGE = TILE_SIZE * 2; // 2 tiles range for Bluetooth scanning
|
|
|
|
|
|
let lastBluetoothScan = 0; // Track last scan time
|
2025-03-08 15:53:08 +00:00
|
|
|
|
const BLUETOOTH_SCAN_INTERVAL = 200; // Scan every 200ms for more responsive updates
|
2024-12-19 17:57:10 +00:00
|
|
|
|
|
2025-03-08 13:19:58 +00:00
|
|
|
|
const gameState = {
|
|
|
|
|
|
biometricSamples: [],
|
|
|
|
|
|
inventory: inventory
|
|
|
|
|
|
};
|
2024-12-19 17:57:10 +00:00
|
|
|
|
|
2025-03-08 13:19:58 +00:00
|
|
|
|
// Add these constants near the top with other constants
|
|
|
|
|
|
const SCANNER_LOCKOUT_TIME = 30000; // 30 seconds lockout
|
|
|
|
|
|
const MAX_FAILED_ATTEMPTS = 3;
|
|
|
|
|
|
|
|
|
|
|
|
// Add this to track failed attempts
|
|
|
|
|
|
const scannerState = {
|
|
|
|
|
|
failedAttempts: {}, // tracks failures by scanner ID
|
|
|
|
|
|
lockoutTimers: {} // tracks lockout end times
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Add these constants near the top with other constants
|
|
|
|
|
|
const SAMPLE_COLLECTION_TIME = 2000; // 2 seconds for collection animation
|
|
|
|
|
|
const SAMPLE_COLLECTION_COLOR = 0x00ff00; // Green for collection effect
|
|
|
|
|
|
|
|
|
|
|
|
// Add these constants for the UI
|
|
|
|
|
|
const SAMPLE_UI_STYLES = {
|
|
|
|
|
|
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
|
|
|
|
|
padding: '10px',
|
|
|
|
|
|
color: 'white',
|
|
|
|
|
|
fontFamily: 'Arial, sans-serif',
|
|
|
|
|
|
fontSize: '14px',
|
|
|
|
|
|
border: '1px solid #444',
|
|
|
|
|
|
borderRadius: '5px',
|
|
|
|
|
|
position: 'fixed',
|
|
|
|
|
|
top: '50%',
|
|
|
|
|
|
left: '50%',
|
|
|
|
|
|
transform: 'translate(-50%, -50%)',
|
|
|
|
|
|
zIndex: 1000,
|
|
|
|
|
|
maxHeight: '80vh',
|
|
|
|
|
|
overflowY: 'auto',
|
|
|
|
|
|
display: 'none'
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Add these constants for spoofing
|
|
|
|
|
|
const SPOOFING_TIME = 3000; // 3 seconds to create spoof
|
|
|
|
|
|
const SPOOF_QUALITY_MULTIPLIER = 0.8; // Spoofed prints are slightly lower quality
|
|
|
|
|
|
|
|
|
|
|
|
// Add these constants for the dusting minigame
|
|
|
|
|
|
const DUST_COLORS = {
|
|
|
|
|
|
NONE: 0x000000,
|
|
|
|
|
|
LIGHT: 0x444444,
|
|
|
|
|
|
MEDIUM: 0x888888,
|
|
|
|
|
|
HEAVY: 0xcccccc,
|
|
|
|
|
|
REVEALED: 0x00ff00
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const DEBUG_MODE = {
|
|
|
|
|
|
get enabled() { return debugMode; },
|
|
|
|
|
|
set enabled(value) { debugMode = value; },
|
|
|
|
|
|
toggle: function() {
|
|
|
|
|
|
debugMode = !debugMode;
|
|
|
|
|
|
// No need to log here as the global event listener will handle it
|
|
|
|
|
|
},
|
|
|
|
|
|
log: function(message, data = null) {
|
|
|
|
|
|
if (!debugMode) return;
|
|
|
|
|
|
|
|
|
|
|
|
if (data) {
|
|
|
|
|
|
console.log(`%c[DEBUG] === ${message} ===`, 'color: #0077FF; font-weight: bold;', data);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log(`%c[DEBUG] === ${message} ===`, 'color: #0077FF; font-weight: bold;');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// preloads the assets
|
|
|
|
|
|
function preload() {
|
|
|
|
|
|
// Show loading text
|
|
|
|
|
|
document.getElementById('loading').style.display = 'block';
|
|
|
|
|
|
|
|
|
|
|
|
// Load tilemap files and regular tilesets first
|
|
|
|
|
|
this.load.tilemapTiledJSON('room_reception', 'assets/rooms/room_reception.json');
|
2024-12-19 17:57:10 +00:00
|
|
|
|
this.load.tilemapTiledJSON('room_office', 'assets/rooms/room_office.json');
|
|
|
|
|
|
this.load.tilemapTiledJSON('room_ceo', 'assets/rooms/room_ceo.json');
|
|
|
|
|
|
this.load.tilemapTiledJSON('room_closet', 'assets/rooms/room_closet.json');
|
|
|
|
|
|
this.load.tilemapTiledJSON('room_servers', 'assets/rooms/room_servers.json');
|
|
|
|
|
|
this.load.image('Modern_Office_48x48', 'assets/Modern_Office_48x48.png');
|
|
|
|
|
|
this.load.image('Room_Builder_48x48', 'assets/Room_Builder_48x48.png');
|
|
|
|
|
|
this.load.image('19_Hospital_Shadowless_48x48', 'assets/19_Hospital_Shadowless_48x48.png');
|
|
|
|
|
|
this.load.image('18_Jail_Shadowless_48x48', 'assets/18_Jail_Shadowless_48x48.png');
|
|
|
|
|
|
this.load.image('1_Generic_Shadowless_48x48', 'assets/1_Generic_Shadowless_48x48.png');
|
|
|
|
|
|
this.load.image('11_Halloween_Shadowless_48x48', 'assets/11_Halloween_Shadowless_48x48.png');
|
|
|
|
|
|
this.load.image('5_Classroom_and_library_Shadowless_48x48', 'assets/5_Classroom_and_library_Shadowless_48x48.png');
|
|
|
|
|
|
|
|
|
|
|
|
// Load object sprites
|
|
|
|
|
|
this.load.image('pc', 'assets/objects/pc.png');
|
|
|
|
|
|
this.load.image('key', 'assets/objects/key.png');
|
|
|
|
|
|
this.load.image('notes', 'assets/objects/notes.png');
|
|
|
|
|
|
this.load.image('phone', 'assets/objects/phone.png');
|
|
|
|
|
|
this.load.image('suitcase', 'assets/objects/suitcase.png');
|
|
|
|
|
|
this.load.image('smartscreen', 'assets/objects/smartscreen.png');
|
|
|
|
|
|
this.load.image('photo', 'assets/objects/photo.png');
|
|
|
|
|
|
this.load.image('suitcase', 'assets/objects/suitcase.png');
|
|
|
|
|
|
this.load.image('safe', 'assets/objects/safe.png');
|
|
|
|
|
|
this.load.image('book', 'assets/objects/book.png');
|
2024-12-19 18:40:48 +00:00
|
|
|
|
this.load.image('workstation', 'assets/objects/workstation.png');
|
2025-02-18 16:29:37 +00:00
|
|
|
|
this.load.image('bluetooth_scanner', 'assets/objects/bluetooth_scanner.png');
|
2025-03-08 13:27:11 +00:00
|
|
|
|
this.load.image('bluetooth_spoofer', 'assets/objects/bluetooth_spoofer.png');
|
2025-02-18 16:47:27 +00:00
|
|
|
|
this.load.image('tablet', 'assets/objects/tablet.png');
|
2025-03-08 13:19:58 +00:00
|
|
|
|
this.load.image('fingerprint_kit', 'assets/objects/fingerprint_kit.png');
|
|
|
|
|
|
this.load.image('lockpick', 'assets/objects/lockpick.png');
|
2025-02-18 16:47:27 +00:00
|
|
|
|
|
2025-02-18 16:19:37 +00:00
|
|
|
|
|
2024-12-19 17:57:10 +00:00
|
|
|
|
this.load.json('gameScenarioJSON', 'assets/scenarios/ceo_exfil.json');
|
|
|
|
|
|
gameScenario = this.cache.json.get('gameScenarioJSON');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-12-19 18:40:48 +00:00
|
|
|
|
// creates the workstation
|
|
|
|
|
|
function addCryptoWorkstation() {
|
2025-02-01 02:09:13 +00:00
|
|
|
|
// console.log('CyberChef: Adding crypto workstation...');
|
2024-12-19 18:40:48 +00:00
|
|
|
|
const workstationData = {
|
|
|
|
|
|
type: "workstation",
|
|
|
|
|
|
name: "Crypto Analysis Station",
|
|
|
|
|
|
observations: "A powerful workstation for cryptographic analysis"
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Create the workstation sprite
|
|
|
|
|
|
const workstationSprite = this.add.sprite(0, 0, 'workstation');
|
|
|
|
|
|
workstationSprite.setVisible(false);
|
|
|
|
|
|
workstationSprite.name = "workstation";
|
|
|
|
|
|
workstationSprite.scenarioData = workstationData;
|
|
|
|
|
|
workstationSprite.setInteractive({ useHandCursor: true });
|
|
|
|
|
|
|
|
|
|
|
|
// Override the default handleObjectInteraction for this specific item
|
2025-02-01 02:23:36 +00:00
|
|
|
|
workstationSprite.openCryptoWorkstation = function() {
|
2025-02-01 02:09:13 +00:00
|
|
|
|
// console.log('CyberChef: Workstation custom interaction triggered');
|
2024-12-19 18:40:48 +00:00
|
|
|
|
// Create popup
|
|
|
|
|
|
let popup = document.getElementById('laptop-popup');
|
|
|
|
|
|
if (!popup) {
|
2025-02-01 02:09:13 +00:00
|
|
|
|
// console.log('CyberChef: Creating new popup...');
|
2024-12-19 18:40:48 +00:00
|
|
|
|
popup = document.createElement('div');
|
|
|
|
|
|
popup.id = 'laptop-popup';
|
|
|
|
|
|
popup.innerHTML = `
|
2025-02-01 02:19:05 +00:00
|
|
|
|
<div class="popup-overlay"></div>
|
2024-12-19 18:40:48 +00:00
|
|
|
|
<div class="laptop-frame">
|
|
|
|
|
|
<div class="laptop-screen">
|
|
|
|
|
|
<div class="title-bar">
|
|
|
|
|
|
<span>CryptoWorkstation</span>
|
|
|
|
|
|
<button class="close-btn">×</button>
|
|
|
|
|
|
</div>
|
2025-02-01 02:09:13 +00:00
|
|
|
|
<div id="cyberchef-container"></div>
|
2024-12-19 18:40:48 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
document.body.appendChild(popup);
|
|
|
|
|
|
|
2025-02-01 02:09:13 +00:00
|
|
|
|
// Find the CyberChef file
|
|
|
|
|
|
fetch('assets/cyberchef/')
|
|
|
|
|
|
.then(response => response.text())
|
|
|
|
|
|
.then(html => {
|
|
|
|
|
|
// Use regex to find the CyberChef filename
|
|
|
|
|
|
const match = html.match(/CyberChef_v[0-9.]+\.html/);
|
|
|
|
|
|
if (match) {
|
|
|
|
|
|
const cyberchefPath = `assets/cyberchef/${match[0]}`;
|
|
|
|
|
|
// Create and append the iframe with the found path
|
|
|
|
|
|
const iframe = document.createElement('iframe');
|
|
|
|
|
|
iframe.src = cyberchefPath;
|
|
|
|
|
|
iframe.frameBorder = "0";
|
|
|
|
|
|
document.getElementById('cyberchef-container').appendChild(iframe);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.error('Could not find CyberChef file');
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(error => {
|
|
|
|
|
|
console.error('Error loading CyberChef:', error);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2024-12-19 18:40:48 +00:00
|
|
|
|
popup.querySelector('.close-btn').addEventListener('click', () => {
|
|
|
|
|
|
popup.style.display = 'none';
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
popup.style.display = 'flex';
|
|
|
|
|
|
return true;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Add to inventory directly
|
|
|
|
|
|
addToInventory(workstationSprite);
|
2025-02-01 02:09:13 +00:00
|
|
|
|
// console.log('CyberChef: Workstation added to inventory');
|
2024-12-19 18:40:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-12-19 17:57:10 +00:00
|
|
|
|
// creates the game
|
|
|
|
|
|
// hides the loading text
|
|
|
|
|
|
// calculates the world bounds
|
|
|
|
|
|
// sets the physics world bounds
|
|
|
|
|
|
// creates the player
|
|
|
|
|
|
// initializes the rooms
|
|
|
|
|
|
// validates the doors by room overlap
|
|
|
|
|
|
// hides all rooms initially if hideRoomsInitially is true
|
|
|
|
|
|
// reveals only the starting room
|
|
|
|
|
|
// sets up the camera
|
|
|
|
|
|
// sets up the input
|
|
|
|
|
|
// creates the inventory display
|
|
|
|
|
|
// processes all door collisions
|
|
|
|
|
|
// initializes the pathfinder
|
|
|
|
|
|
// initializes the inventory
|
|
|
|
|
|
function create() {
|
|
|
|
|
|
// Hide loading text
|
|
|
|
|
|
document.getElementById('loading').style.display = 'none';
|
|
|
|
|
|
|
|
|
|
|
|
// Ensure gameScenario is loaded before proceeding
|
|
|
|
|
|
if (!gameScenario) {
|
|
|
|
|
|
gameScenario = this.cache.json.get('gameScenarioJSON');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Now calculate world bounds after scenario is loaded
|
|
|
|
|
|
const worldBounds = calculateWorldBounds();
|
|
|
|
|
|
|
|
|
|
|
|
// Set the physics world bounds
|
|
|
|
|
|
this.physics.world.setBounds(
|
|
|
|
|
|
worldBounds.x,
|
|
|
|
|
|
worldBounds.y,
|
|
|
|
|
|
worldBounds.width,
|
|
|
|
|
|
worldBounds.height
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// Create player first
|
|
|
|
|
|
player = this.add.rectangle(400, 300, 32, 32, 0xff0000);
|
|
|
|
|
|
this.physics.add.existing(player);
|
|
|
|
|
|
player.body.setSize(16, 16);
|
|
|
|
|
|
player.body.setOffset(8, 8);
|
|
|
|
|
|
player.body.setCollideWorldBounds(true);
|
|
|
|
|
|
player.body.setBounce(0);
|
|
|
|
|
|
player.body.setDrag(0);
|
|
|
|
|
|
player.body.setFriction(0);
|
|
|
|
|
|
|
|
|
|
|
|
player.setDepth(1000);
|
|
|
|
|
|
|
|
|
|
|
|
// Initialize room layout after player creation
|
|
|
|
|
|
initializeRooms.call(this);
|
|
|
|
|
|
validateDoorsByRoomOverlap.call(this);
|
|
|
|
|
|
|
|
|
|
|
|
// Hide all rooms initially if hideRoomsInitially is true
|
|
|
|
|
|
if (hideRoomsInitially) {
|
|
|
|
|
|
Object.keys(gameScenario.rooms).forEach(roomId => {
|
|
|
|
|
|
hideRoom.call(this, roomId);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Explicitly reveal the starting room and ensure its doors are visible
|
|
|
|
|
|
const startingRoom = gameScenario.startRoom;
|
|
|
|
|
|
revealRoom.call(this, startingRoom);
|
|
|
|
|
|
|
|
|
|
|
|
// Force doors visibility for starting room
|
|
|
|
|
|
if (rooms[startingRoom] && rooms[startingRoom].doorsLayer) {
|
|
|
|
|
|
rooms[startingRoom].doorsLayer.setVisible(true);
|
|
|
|
|
|
rooms[startingRoom].doorsLayer.setAlpha(1);
|
|
|
|
|
|
console.log(`Starting room doors layer:`, {
|
|
|
|
|
|
visible: rooms[startingRoom].doorsLayer.visible,
|
|
|
|
|
|
alpha: rooms[startingRoom].doorsLayer.alpha,
|
|
|
|
|
|
depth: rooms[startingRoom].doorsLayer.depth
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Setup camera
|
|
|
|
|
|
this.cameras.main.startFollow(player);
|
|
|
|
|
|
this.cameras.main.setZoom(1);
|
|
|
|
|
|
|
|
|
|
|
|
// Setup input with proper context
|
|
|
|
|
|
this.input.on('pointerdown', (pointer) => {
|
|
|
|
|
|
// Check if click is in inventory area
|
|
|
|
|
|
const inventoryArea = {
|
|
|
|
|
|
y: this.cameras.main.height - 70,
|
|
|
|
|
|
height: 70
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (pointer.y > inventoryArea.y) {
|
|
|
|
|
|
// Find clicked inventory item
|
|
|
|
|
|
const clickedItem = inventory.items.find(item => {
|
|
|
|
|
|
if (!item) return false;
|
|
|
|
|
|
const bounds = item.getBounds();
|
|
|
|
|
|
return Phaser.Geom.Rectangle.Contains(
|
|
|
|
|
|
bounds,
|
|
|
|
|
|
pointer.x,
|
|
|
|
|
|
pointer.y
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (clickedItem) {
|
2025-03-08 13:19:58 +00:00
|
|
|
|
debugLog('INVENTORY ITEM CLICKED', { name: clickedItem.name }, 2);
|
2024-12-19 17:57:10 +00:00
|
|
|
|
handleObjectInteraction(clickedItem);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// if not clicking inventory, handle as movement
|
2025-03-08 13:19:58 +00:00
|
|
|
|
debugLog('CLICK DETECTED', { x: pointer.worldX, y: pointer.worldY }, 3);
|
2024-12-19 17:57:10 +00:00
|
|
|
|
movePlayerToPoint.call(this, pointer.worldX, pointer.worldY);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// creates the inventory display
|
|
|
|
|
|
createInventoryDisplay.call(this);
|
|
|
|
|
|
|
|
|
|
|
|
// Add this new call after all rooms are created
|
|
|
|
|
|
processAllDoorCollisions.call(this);
|
|
|
|
|
|
|
|
|
|
|
|
// Initialize pathfinder
|
|
|
|
|
|
initializePathfinder.call(this);
|
|
|
|
|
|
|
|
|
|
|
|
// Initialize game systems
|
|
|
|
|
|
initializeInventory.call(this);
|
|
|
|
|
|
|
2024-12-19 18:40:48 +00:00
|
|
|
|
// Add the workstation to inventory
|
|
|
|
|
|
addCryptoWorkstation.call(this);
|
|
|
|
|
|
|
2024-12-19 17:57:10 +00:00
|
|
|
|
// Add this line after processAllDoorCollisions()
|
|
|
|
|
|
setupDoorOverlapChecks.call(this);
|
|
|
|
|
|
|
|
|
|
|
|
// introduce the scenario
|
|
|
|
|
|
introduceScenario.call(this);
|
|
|
|
|
|
|
|
|
|
|
|
// Enable physics debug only in development
|
|
|
|
|
|
this.physics.world.debugGraphic.clear();
|
|
|
|
|
|
this.physics.world.drawDebug = false;
|
|
|
|
|
|
|
|
|
|
|
|
// Optimize physics world
|
|
|
|
|
|
this.physics.world.setBounds(
|
|
|
|
|
|
worldBounds.x,
|
|
|
|
|
|
worldBounds.y,
|
|
|
|
|
|
worldBounds.width,
|
|
|
|
|
|
worldBounds.height,
|
|
|
|
|
|
true // Enable bounds collision
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// Optimize physics settings
|
|
|
|
|
|
this.physics.world.setFPS(60);
|
|
|
|
|
|
this.physics.world.step(1/60);
|
2025-03-08 13:19:58 +00:00
|
|
|
|
|
|
|
|
|
|
// Add this to your scene's create function
|
|
|
|
|
|
initializeSamplesUI();
|
|
|
|
|
|
|
|
|
|
|
|
// Log initial debug status
|
|
|
|
|
|
console.log("%cPress ` (backtick) to toggle debug mode, Ctrl+` to cycle debug levels (1-3), Shift+` for visual debug", "color: #888; font-style: italic;");
|
2024-12-19 17:57:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function update() {
|
|
|
|
|
|
// updates the player's movement
|
|
|
|
|
|
updatePlayerMovement.call(this);
|
|
|
|
|
|
|
|
|
|
|
|
// checks for object interactions
|
|
|
|
|
|
checkObjectInteractions.call(this);
|
|
|
|
|
|
|
|
|
|
|
|
// checks for room transitions
|
|
|
|
|
|
checkRoomTransitions.call(this);
|
|
|
|
|
|
|
2025-02-18 17:01:17 +00:00
|
|
|
|
// Check for Bluetooth devices
|
|
|
|
|
|
const currentTime = this.time.now;
|
|
|
|
|
|
if (currentTime - lastBluetoothScan >= BLUETOOTH_SCAN_INTERVAL) {
|
|
|
|
|
|
checkBluetoothDevices.call(this);
|
|
|
|
|
|
lastBluetoothScan = currentTime;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-12-19 17:57:10 +00:00
|
|
|
|
// adds a circle to the start of the path
|
|
|
|
|
|
if (currentPath && currentPath.length > 0 && isMoving) {
|
|
|
|
|
|
this.add.circle(currentPath[0].x, currentPath[0].y, 5, 0xff0000).setDepth(1000);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// introduces the scenario
|
|
|
|
|
|
function introduceScenario() {
|
|
|
|
|
|
console.log(gameScenario.scenario_brief);
|
2025-03-08 13:19:58 +00:00
|
|
|
|
|
|
|
|
|
|
// Add scenario brief as an important note
|
|
|
|
|
|
addNote("Mission Brief", gameScenario.scenario_brief, true);
|
|
|
|
|
|
|
|
|
|
|
|
// Show notification
|
|
|
|
|
|
gameAlert(gameScenario.scenario_brief, 'info', 'Mission Brief', 8000);
|
2024-12-19 17:57:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// initializes the rooms
|
|
|
|
|
|
// calculates the positions of the rooms
|
|
|
|
|
|
// creates the rooms
|
|
|
|
|
|
function initializeRooms() {
|
|
|
|
|
|
// Calculate room positions and create room instances
|
|
|
|
|
|
let roomPositions = calculateRoomPositions();
|
|
|
|
|
|
|
|
|
|
|
|
Object.entries(gameScenario.rooms).forEach(([roomId, roomData]) => {
|
|
|
|
|
|
const position = roomPositions[roomId];
|
|
|
|
|
|
createRoom.call(this, roomId, roomData, position);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// calculates the positions of the rooms
|
|
|
|
|
|
// calculates the dimensions of the rooms
|
|
|
|
|
|
// calculates the positions of the rooms based on the dimensions and overlaps
|
|
|
|
|
|
function calculateRoomPositions() {
|
|
|
|
|
|
const OVERLAP = 96;
|
|
|
|
|
|
const positions = {};
|
|
|
|
|
|
|
|
|
|
|
|
console.log('=== Starting Room Position Calculations ===');
|
|
|
|
|
|
|
|
|
|
|
|
// Get room dimensions from tilemaps
|
|
|
|
|
|
const roomDimensions = {};
|
|
|
|
|
|
Object.entries(gameScenario.rooms).forEach(([roomId, roomData]) => {
|
|
|
|
|
|
const map = game.cache.tilemap.get(roomData.type);
|
|
|
|
|
|
console.log(`Debug - Room ${roomId}:`, {
|
|
|
|
|
|
mapData: map,
|
|
|
|
|
|
fullData: map?.data,
|
|
|
|
|
|
json: map?.json
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Try different ways to access the data
|
|
|
|
|
|
if (map) {
|
|
|
|
|
|
let width, height;
|
|
|
|
|
|
if (map.json) {
|
|
|
|
|
|
width = map.json.width;
|
|
|
|
|
|
height = map.json.height;
|
|
|
|
|
|
} else if (map.data) {
|
|
|
|
|
|
width = map.data.width;
|
|
|
|
|
|
height = map.data.height;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
width = map.width;
|
|
|
|
|
|
height = map.height;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
roomDimensions[roomId] = {
|
|
|
|
|
|
width: width * 48, // tile width is 48
|
|
|
|
|
|
height: height * 48 // tile height is 48
|
|
|
|
|
|
};
|
2025-03-08 13:19:58 +00:00
|
|
|
|
debugLog('ROOM DIMENSIONS', { roomId, dimensions: roomDimensions[roomId] }, 3);
|
2024-12-19 17:57:10 +00:00
|
|
|
|
} else {
|
|
|
|
|
|
console.error(`Could not find tilemap data for room ${roomId}`);
|
|
|
|
|
|
// Fallback to default dimensions if needed
|
|
|
|
|
|
roomDimensions[roomId] = {
|
|
|
|
|
|
width: 800, // default width
|
|
|
|
|
|
height: 600 // default height
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Start with reception room at origin
|
|
|
|
|
|
positions[gameScenario.startRoom] = { x: 0, y: 0 };
|
|
|
|
|
|
console.log(`Starting room ${gameScenario.startRoom} position:`, positions[gameScenario.startRoom]);
|
|
|
|
|
|
|
|
|
|
|
|
// Process rooms level by level, starting from reception
|
|
|
|
|
|
const processed = new Set([gameScenario.startRoom]);
|
|
|
|
|
|
const queue = [gameScenario.startRoom];
|
|
|
|
|
|
|
|
|
|
|
|
while (queue.length > 0) {
|
|
|
|
|
|
const currentRoomId = queue.shift();
|
|
|
|
|
|
const currentRoom = gameScenario.rooms[currentRoomId];
|
|
|
|
|
|
const currentPos = positions[currentRoomId];
|
|
|
|
|
|
const currentDimensions = roomDimensions[currentRoomId];
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`\nProcessing room ${currentRoomId}`);
|
|
|
|
|
|
console.log('Current position:', currentPos);
|
|
|
|
|
|
console.log('Connections:', currentRoom.connections);
|
|
|
|
|
|
|
|
|
|
|
|
Object.entries(currentRoom.connections).forEach(([direction, connected]) => {
|
|
|
|
|
|
console.log(`\nProcessing ${direction} connection:`, connected);
|
|
|
|
|
|
|
|
|
|
|
|
if (Array.isArray(connected)) {
|
|
|
|
|
|
const rooms = connected.filter(r => !processed.has(r));
|
|
|
|
|
|
console.log('Unprocessed connected rooms:', rooms);
|
|
|
|
|
|
if (rooms.length === 0) return;
|
|
|
|
|
|
|
|
|
|
|
|
if (direction === 'north' || direction === 'south') {
|
|
|
|
|
|
const firstRoom = rooms[0];
|
|
|
|
|
|
const firstRoomWidth = roomDimensions[firstRoom].width;
|
|
|
|
|
|
const firstRoomHeight = roomDimensions[firstRoom].height;
|
|
|
|
|
|
|
|
|
|
|
|
const secondRoom = rooms[1];
|
|
|
|
|
|
const secondRoomWidth = roomDimensions[secondRoom].width;
|
|
|
|
|
|
const secondRoomHeight = roomDimensions[secondRoom].height;
|
|
|
|
|
|
|
|
|
|
|
|
if (direction === 'north') {
|
|
|
|
|
|
// First room - right edge aligns with current room's left edge
|
|
|
|
|
|
positions[firstRoom] = {
|
|
|
|
|
|
x: currentPos.x - firstRoomWidth + DOOR_ALIGN_OVERLAP,
|
|
|
|
|
|
y: currentPos.y - firstRoomHeight + OVERLAP
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Second room - left edge aligns with current room's right edge
|
|
|
|
|
|
positions[secondRoom] = {
|
|
|
|
|
|
x: currentPos.x + currentDimensions.width - DOOR_ALIGN_OVERLAP,
|
|
|
|
|
|
y: currentPos.y - secondRoomHeight + OVERLAP
|
|
|
|
|
|
};
|
|
|
|
|
|
} else if (direction === 'south') {
|
|
|
|
|
|
// First room - left edge aligns with current room's right edge
|
|
|
|
|
|
positions[firstRoom] = {
|
|
|
|
|
|
x: currentPos.x - firstRoomWidth + DOOR_ALIGN_OVERLAP,
|
|
|
|
|
|
y: currentPos.y + currentDimensions.height - OVERLAP
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Second room - right edge aligns with current room's left edge
|
|
|
|
|
|
positions[secondRoom] = {
|
|
|
|
|
|
x: currentPos.x + currentDimensions.width - DOOR_ALIGN_OVERLAP,
|
|
|
|
|
|
y: currentPos.y + currentDimensions.height - secondRoomHeight - OVERLAP
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
rooms.forEach(roomId => {
|
|
|
|
|
|
processed.add(roomId);
|
|
|
|
|
|
queue.push(roomId);
|
|
|
|
|
|
console.log(`Positioned room ${roomId} at:`, positions[roomId]);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if (processed.has(connected)) {
|
2025-03-08 13:19:58 +00:00
|
|
|
|
debugLog('ROOM ALREADY PROCESSED', { roomId: connected }, 3);
|
2024-12-19 17:57:10 +00:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const connectedDimensions = roomDimensions[connected];
|
|
|
|
|
|
|
|
|
|
|
|
// Center the connected room
|
|
|
|
|
|
const x = currentPos.x +
|
|
|
|
|
|
(currentDimensions.width - connectedDimensions.width) / 2;
|
|
|
|
|
|
const y = direction === 'north'
|
|
|
|
|
|
? currentPos.y - connectedDimensions.height + OVERLAP
|
|
|
|
|
|
: currentPos.y + currentDimensions.height - OVERLAP;
|
|
|
|
|
|
|
|
|
|
|
|
positions[connected] = { x, y };
|
|
|
|
|
|
processed.add(connected);
|
|
|
|
|
|
queue.push(connected);
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`Positioned single room ${connected} at:`, positions[connected]);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log('\n=== Final Room Positions ===');
|
|
|
|
|
|
Object.entries(positions).forEach(([roomId, pos]) => {
|
|
|
|
|
|
console.log(`${roomId}:`, pos);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return positions;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// creates a room
|
|
|
|
|
|
// creates the tilemap for the room
|
|
|
|
|
|
// creates the layers for the room
|
|
|
|
|
|
// adds the objects to the room
|
|
|
|
|
|
function createRoom(roomId, roomData, position) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
console.log(`Creating room ${roomId} of type ${roomData.type}`);
|
|
|
|
|
|
|
|
|
|
|
|
const map = this.make.tilemap({ key: roomData.type });
|
|
|
|
|
|
const tilesets = [];
|
|
|
|
|
|
|
|
|
|
|
|
// Add tilesets
|
|
|
|
|
|
const regularTilesets = map.tilesets.filter(t => !t.name.includes('Interiors_48x48'));
|
|
|
|
|
|
regularTilesets.forEach(tileset => {
|
|
|
|
|
|
const loadedTileset = map.addTilesetImage(tileset.name, tileset.name);
|
|
|
|
|
|
if (loadedTileset) {
|
|
|
|
|
|
tilesets.push(loadedTileset);
|
|
|
|
|
|
console.log(`Added regular tileset: ${tileset.name}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Initialize room data structure first
|
|
|
|
|
|
rooms[roomId] = {
|
|
|
|
|
|
map,
|
|
|
|
|
|
layers: {},
|
|
|
|
|
|
wallsLayers: [],
|
|
|
|
|
|
position
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const layers = rooms[roomId].layers;
|
|
|
|
|
|
const wallsLayers = rooms[roomId].wallsLayers;
|
|
|
|
|
|
|
|
|
|
|
|
// IMPORTANT: This counter ensures unique layer IDs across ALL rooms and should not be removed
|
|
|
|
|
|
if (!window.globalLayerCounter) window.globalLayerCounter = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// Calculate base depth for this room's layers
|
|
|
|
|
|
const roomDepth = position.y * 100;
|
|
|
|
|
|
|
|
|
|
|
|
// Create doors layer first with a specific depth
|
|
|
|
|
|
const doorsLayerIndex = map.layers.findIndex(layer =>
|
|
|
|
|
|
layer.name.toLowerCase().includes('doors'));
|
|
|
|
|
|
let doorsLayer = null;
|
|
|
|
|
|
if (doorsLayerIndex !== -1) {
|
|
|
|
|
|
window.globalLayerCounter++;
|
|
|
|
|
|
const uniqueDoorsId = `${roomId}_doors_${window.globalLayerCounter}`;
|
|
|
|
|
|
doorsLayer = map.createLayer(doorsLayerIndex, tilesets, position.x, position.y);
|
|
|
|
|
|
if (doorsLayer) {
|
|
|
|
|
|
doorsLayer.name = uniqueDoorsId;
|
|
|
|
|
|
// Set doors layer depth higher than other layers
|
|
|
|
|
|
doorsLayer.setDepth(roomDepth + 500);
|
|
|
|
|
|
layers[uniqueDoorsId] = doorsLayer;
|
|
|
|
|
|
rooms[roomId].doorsLayer = doorsLayer;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Create other layers with appropriate depths
|
|
|
|
|
|
map.layers.forEach((layerData, index) => {
|
|
|
|
|
|
// Skip the doors layer as we already created it
|
|
|
|
|
|
if (index === doorsLayerIndex) return;
|
|
|
|
|
|
|
|
|
|
|
|
window.globalLayerCounter++;
|
|
|
|
|
|
const uniqueLayerId = `${roomId}_${layerData.name}_${window.globalLayerCounter}`;
|
|
|
|
|
|
|
|
|
|
|
|
const layer = map.createLayer(index, tilesets, position.x, position.y);
|
|
|
|
|
|
if (layer) {
|
|
|
|
|
|
layer.name = uniqueLayerId;
|
|
|
|
|
|
|
|
|
|
|
|
// Set depth based on layer type and room position
|
|
|
|
|
|
if (layerData.name.toLowerCase().includes('floor')) {
|
|
|
|
|
|
layer.setDepth(roomDepth + 100);
|
|
|
|
|
|
} else if (layerData.name.toLowerCase().includes('walls')) {
|
|
|
|
|
|
layer.setDepth(roomDepth + 200);
|
|
|
|
|
|
// Handle walls layer collision
|
|
|
|
|
|
try {
|
|
|
|
|
|
layer.setCollisionByExclusion([-1]);
|
|
|
|
|
|
|
|
|
|
|
|
if (doorsLayer) {
|
|
|
|
|
|
const doorTiles = doorsLayer.getTilesWithin()
|
|
|
|
|
|
.filter(tile => tile.index !== -1);
|
|
|
|
|
|
|
|
|
|
|
|
doorTiles.forEach(doorTile => {
|
|
|
|
|
|
const wallTile = layer.getTileAt(doorTile.x, doorTile.y);
|
|
|
|
|
|
if (wallTile) {
|
|
|
|
|
|
if (!doorTile.properties?.locked) {
|
|
|
|
|
|
wallTile.setCollision(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
wallsLayers.push(layer);
|
|
|
|
|
|
console.log(`Added collision to wall layer: ${uniqueLayerId}`);
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.warn(`Error setting up collisions for ${uniqueLayerId}:`, e);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (layerData.name.toLowerCase().includes('props')) {
|
|
|
|
|
|
layer.setDepth(roomDepth + 300);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
layer.setDepth(roomDepth + 400);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
layers[uniqueLayerId] = layer;
|
|
|
|
|
|
layer.setVisible(false);
|
|
|
|
|
|
layer.setAlpha(0);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Add collisions between player and wall layers
|
|
|
|
|
|
if (player && player.body) {
|
|
|
|
|
|
wallsLayers.forEach(wallLayer => {
|
|
|
|
|
|
if (wallLayer) {
|
|
|
|
|
|
this.physics.add.collider(player, wallLayer);
|
|
|
|
|
|
console.log(`Added collision between player and wall layer: ${wallLayer.name}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Store door layer reference for later processing
|
|
|
|
|
|
if (doorsLayer) {
|
|
|
|
|
|
rooms[roomId].doorsLayer = doorsLayer;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Update object creation to handle new structure
|
|
|
|
|
|
const objectsLayer = map.getObjectLayer('Object Layer 1');
|
|
|
|
|
|
if (objectsLayer && objectsLayer.objects) {
|
|
|
|
|
|
rooms[roomId].objects = {};
|
|
|
|
|
|
|
|
|
|
|
|
objectsLayer.objects.forEach(obj => {
|
|
|
|
|
|
// Find matching object in scenario data
|
|
|
|
|
|
const scenarioObject = gameScenario.rooms[roomId].objects.find(
|
|
|
|
|
|
item => item.type === obj.name
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// Check if this object should be active in the current scenario
|
|
|
|
|
|
const isActiveObject = scenarioObject !== undefined;
|
|
|
|
|
|
|
|
|
|
|
|
const sprite = this.add.sprite(
|
|
|
|
|
|
position.x + obj.x,
|
|
|
|
|
|
position.y + (obj.gid !== undefined ? obj.y - obj.height : obj.y),
|
|
|
|
|
|
obj.name
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
sprite.setOrigin(0, 0);
|
|
|
|
|
|
sprite.name = obj.name;
|
|
|
|
|
|
sprite.setInteractive({ useHandCursor: true });
|
|
|
|
|
|
sprite.setDepth(1001);
|
|
|
|
|
|
sprite.originalAlpha = 1;
|
|
|
|
|
|
sprite.active = isActiveObject;
|
|
|
|
|
|
|
|
|
|
|
|
// Store scenario data with sprite for later use
|
|
|
|
|
|
if (isActiveObject) {
|
|
|
|
|
|
sprite.scenarioData = scenarioObject;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Initially hide all objects - they'll be shown when room is revealed
|
|
|
|
|
|
sprite.setVisible(false);
|
|
|
|
|
|
|
|
|
|
|
|
if (obj.rotation) {
|
|
|
|
|
|
sprite.setRotation(Phaser.Math.DegToRad(obj.rotation));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
rooms[roomId].objects[obj.name] = sprite;
|
|
|
|
|
|
|
|
|
|
|
|
// Add click handler for all objects
|
|
|
|
|
|
sprite.on('pointerdown', () => {
|
|
|
|
|
|
if (isActiveObject) {
|
2025-03-08 13:19:58 +00:00
|
|
|
|
debugLog('OBJECT CLICKED', { name: obj.name }, 2);
|
2024-12-19 17:57:10 +00:00
|
|
|
|
handleObjectInteraction(sprite);
|
|
|
|
|
|
} else {
|
2025-03-08 13:19:58 +00:00
|
|
|
|
gameAlert("Nothing of note here", 'info', '', 2000);
|
2024-12-19 17:57:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error(`Error creating room ${roomId}:`, error);
|
|
|
|
|
|
console.error('Error details:', error.stack);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// reveals a room
|
|
|
|
|
|
// reveals all layers and objects in the room
|
|
|
|
|
|
function revealRoom(roomId) {
|
|
|
|
|
|
if (rooms[roomId]) {
|
|
|
|
|
|
const room = rooms[roomId];
|
|
|
|
|
|
|
|
|
|
|
|
// Reveal all layers
|
|
|
|
|
|
Object.values(room.layers).forEach(layer => {
|
|
|
|
|
|
if (layer && layer.setVisible) {
|
|
|
|
|
|
layer.setVisible(true);
|
|
|
|
|
|
layer.setAlpha(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Explicitly reveal doors layer if it exists
|
|
|
|
|
|
if (room.doorsLayer) {
|
|
|
|
|
|
room.doorsLayer.setVisible(true);
|
|
|
|
|
|
room.doorsLayer.setAlpha(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Show all objects
|
|
|
|
|
|
if (room.objects) {
|
|
|
|
|
|
Object.values(room.objects).forEach(obj => {
|
|
|
|
|
|
if (obj && obj.setVisible && obj.active) { // Only show active objects
|
|
|
|
|
|
obj.setVisible(true);
|
|
|
|
|
|
obj.alpha = obj.active ? (obj.originalAlpha || 1) : 0.3;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
discoveredRooms.add(roomId);
|
|
|
|
|
|
}
|
|
|
|
|
|
currentRoom = roomId;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// moves the player to a point
|
|
|
|
|
|
// ensures the coordinates are within the world bounds
|
|
|
|
|
|
function movePlayerToPoint(x, y) {
|
|
|
|
|
|
const worldBounds = this.physics.world.bounds;
|
|
|
|
|
|
|
|
|
|
|
|
// Ensure coordinates are within bounds
|
|
|
|
|
|
x = Phaser.Math.Clamp(x, worldBounds.x, worldBounds.x + worldBounds.width);
|
|
|
|
|
|
y = Phaser.Math.Clamp(y, worldBounds.y, worldBounds.y + worldBounds.height);
|
|
|
|
|
|
|
|
|
|
|
|
targetPoint = { x, y };
|
|
|
|
|
|
isMoving = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// updates the player's movement
|
|
|
|
|
|
// moves the player towards the target point
|
|
|
|
|
|
// stops if a collision is detected
|
|
|
|
|
|
function updatePlayerMovement() {
|
|
|
|
|
|
if (!isMoving || !targetPoint) {
|
|
|
|
|
|
if (player.body.velocity.x !== 0 || player.body.velocity.y !== 0) {
|
|
|
|
|
|
player.body.setVelocity(0, 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Cache player position
|
|
|
|
|
|
const px = player.x;
|
|
|
|
|
|
const py = player.y;
|
|
|
|
|
|
|
|
|
|
|
|
// Use squared distance for performance
|
|
|
|
|
|
const dx = targetPoint.x - px;
|
|
|
|
|
|
const dy = targetPoint.y - py;
|
|
|
|
|
|
const distanceSq = dx * dx + dy * dy;
|
|
|
|
|
|
|
|
|
|
|
|
// Reached target point
|
|
|
|
|
|
if (distanceSq < ARRIVAL_THRESHOLD * ARRIVAL_THRESHOLD) {
|
|
|
|
|
|
isMoving = false;
|
|
|
|
|
|
player.body.setVelocity(0, 0);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Only check room transitions periodically
|
|
|
|
|
|
const movedX = Math.abs(px - lastPlayerPosition.x);
|
|
|
|
|
|
const movedY = Math.abs(py - lastPlayerPosition.y);
|
|
|
|
|
|
|
|
|
|
|
|
if (movedX > ROOM_CHECK_THRESHOLD || movedY > ROOM_CHECK_THRESHOLD) {
|
|
|
|
|
|
updatePlayerRoom();
|
|
|
|
|
|
lastPlayerPosition.x = px;
|
|
|
|
|
|
lastPlayerPosition.y = py;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Normalize movement vector for consistent speed
|
|
|
|
|
|
const distance = Math.sqrt(distanceSq);
|
|
|
|
|
|
const velocityX = (dx / distance) * MOVEMENT_SPEED;
|
|
|
|
|
|
const velocityY = (dy / distance) * MOVEMENT_SPEED;
|
|
|
|
|
|
|
|
|
|
|
|
// Only update velocity if it changed significantly
|
|
|
|
|
|
const currentVX = player.body.velocity.x;
|
|
|
|
|
|
const currentVY = player.body.velocity.y;
|
|
|
|
|
|
const velocityDiffX = Math.abs(currentVX - velocityX);
|
|
|
|
|
|
const velocityDiffY = Math.abs(currentVY - velocityY);
|
|
|
|
|
|
|
|
|
|
|
|
if (velocityDiffX > 1 || velocityDiffY > 1) {
|
|
|
|
|
|
player.body.setVelocity(velocityX, velocityY);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Stop if collision detected
|
|
|
|
|
|
if (player.body.blocked.none === false) {
|
|
|
|
|
|
isMoving = false;
|
|
|
|
|
|
player.body.setVelocity(0, 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// creates the inventory display
|
|
|
|
|
|
// creates the background and slot outlines
|
|
|
|
|
|
function createInventoryDisplay() {
|
|
|
|
|
|
|
|
|
|
|
|
// Create slot outlines
|
|
|
|
|
|
const slotsContainer = this.add.container(110, this.cameras.main.height - 60)
|
|
|
|
|
|
.setScrollFactor(0)
|
|
|
|
|
|
.setDepth(2001);
|
|
|
|
|
|
|
|
|
|
|
|
// Create 10 slot outlines
|
|
|
|
|
|
for (let i = 0; i < 10; i++) {
|
|
|
|
|
|
const outline = this.add.rectangle(
|
|
|
|
|
|
i * 60,
|
|
|
|
|
|
0,
|
|
|
|
|
|
50,
|
|
|
|
|
|
50,
|
|
|
|
|
|
0x666666,
|
|
|
|
|
|
0.3
|
|
|
|
|
|
);
|
|
|
|
|
|
outline.setStrokeStyle(1, 0x666666);
|
|
|
|
|
|
slotsContainer.add(outline);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Initialize inventory container with highest depth
|
|
|
|
|
|
inventory.container = this.add.container(110, this.cameras.main.height - 60)
|
|
|
|
|
|
.setScrollFactor(0)
|
|
|
|
|
|
.setDepth(2002);
|
|
|
|
|
|
|
|
|
|
|
|
// Modify the input event to check if clicking on inventory
|
|
|
|
|
|
this.input.on('pointerdown', (pointer) => {
|
|
|
|
|
|
// Convert pointer position to world coordinates
|
|
|
|
|
|
const worldPoint = this.cameras.main.getWorldPoint(pointer.x, pointer.y);
|
|
|
|
|
|
|
|
|
|
|
|
// Check if click is in inventory area
|
|
|
|
|
|
const inventoryArea = {
|
|
|
|
|
|
x: 100,
|
|
|
|
|
|
y: this.cameras.main.height - 70,
|
|
|
|
|
|
width: this.cameras.main.width - 200,
|
|
|
|
|
|
height: 70
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (pointer.y > inventoryArea.y) {
|
|
|
|
|
|
// Click is in inventory area, let the inventory sprites handle it
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Otherwise, handle as movement click
|
2025-03-08 13:19:58 +00:00
|
|
|
|
debugLog('CLICK DETECTED', { x: worldPoint.x, y: worldPoint.y }, 3);
|
2024-12-19 17:57:10 +00:00
|
|
|
|
movePlayerToPoint.call(this, worldPoint.x, worldPoint.y);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// checks for object interactions
|
|
|
|
|
|
// highlights the object if the player is in range
|
|
|
|
|
|
// handles the click event for the object
|
|
|
|
|
|
function checkObjectInteractions() {
|
|
|
|
|
|
// Skip if not enough time has passed since last check
|
|
|
|
|
|
const currentTime = performance.now();
|
|
|
|
|
|
if (this.lastInteractionCheck &&
|
|
|
|
|
|
currentTime - this.lastInteractionCheck < INTERACTION_CHECK_INTERVAL) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.lastInteractionCheck = currentTime;
|
|
|
|
|
|
|
|
|
|
|
|
const playerRoom = currentPlayerRoom;
|
|
|
|
|
|
if (!playerRoom || !rooms[playerRoom].objects) return;
|
|
|
|
|
|
|
|
|
|
|
|
// Cache player position
|
|
|
|
|
|
const px = player.x;
|
|
|
|
|
|
const py = player.y;
|
|
|
|
|
|
|
|
|
|
|
|
// Get only objects within viewport bounds plus some margin
|
|
|
|
|
|
const camera = this.cameras.main;
|
|
|
|
|
|
const margin = INTERACTION_RANGE;
|
|
|
|
|
|
const viewBounds = {
|
|
|
|
|
|
left: camera.scrollX - margin,
|
|
|
|
|
|
right: camera.scrollX + camera.width + margin,
|
|
|
|
|
|
top: camera.scrollY - margin,
|
|
|
|
|
|
bottom: camera.scrollY + camera.height + margin
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
Object.values(rooms[playerRoom].objects).forEach(obj => {
|
|
|
|
|
|
// Skip inactive objects and those outside viewport
|
|
|
|
|
|
if (!obj.active ||
|
|
|
|
|
|
obj.x < viewBounds.left ||
|
|
|
|
|
|
obj.x > viewBounds.right ||
|
|
|
|
|
|
obj.y < viewBounds.top ||
|
|
|
|
|
|
obj.y > viewBounds.bottom) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Use squared distance for performance
|
|
|
|
|
|
const dx = px - obj.x;
|
|
|
|
|
|
const dy = py - obj.y;
|
|
|
|
|
|
const distanceSq = dx * dx + dy * dy;
|
|
|
|
|
|
|
|
|
|
|
|
if (distanceSq <= INTERACTION_RANGE_SQ) {
|
|
|
|
|
|
if (!obj.isHighlighted) {
|
|
|
|
|
|
obj.isHighlighted = true;
|
|
|
|
|
|
obj.setTint(0xdddddd); // Simple highlight without tween
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (obj.isHighlighted) {
|
|
|
|
|
|
obj.isHighlighted = false;
|
|
|
|
|
|
obj.clearTint();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// checks for room transitions
|
|
|
|
|
|
function checkRoomTransitions() {
|
|
|
|
|
|
// Now handled by physics overlap
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// calculates the world bounds
|
|
|
|
|
|
function calculateWorldBounds() {
|
|
|
|
|
|
if (!gameScenario || !gameScenario.rooms) {
|
|
|
|
|
|
console.error('Game scenario not loaded properly');
|
|
|
|
|
|
// Return default bounds
|
|
|
|
|
|
return {
|
|
|
|
|
|
x: -1800,
|
|
|
|
|
|
y: -1800,
|
|
|
|
|
|
width: 3600,
|
|
|
|
|
|
height: 3600
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let minX = -1800, minY = -1800, maxX = 1800, maxY = 1800;
|
|
|
|
|
|
|
|
|
|
|
|
// Check all room positions to determine world bounds
|
|
|
|
|
|
Object.values(gameScenario.rooms).forEach(room => {
|
|
|
|
|
|
const position = calculateRoomPositions()[room.id];
|
|
|
|
|
|
if (position) {
|
|
|
|
|
|
// Assuming each room is 800x600
|
|
|
|
|
|
minX = Math.min(minX, position.x);
|
|
|
|
|
|
minY = Math.min(minY, position.y);
|
|
|
|
|
|
maxX = Math.max(maxX, position.x + 800);
|
|
|
|
|
|
maxY = Math.max(maxY, position.y + 600);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Add some padding
|
|
|
|
|
|
const padding = 200;
|
|
|
|
|
|
return {
|
|
|
|
|
|
x: minX - padding,
|
|
|
|
|
|
y: minY - padding,
|
|
|
|
|
|
width: (maxX - minX) + (padding * 2),
|
|
|
|
|
|
height: (maxY - minY) + (padding * 2)
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// processes all door-wall interactions
|
|
|
|
|
|
function processAllDoorCollisions() {
|
|
|
|
|
|
Object.entries(rooms).forEach(([roomId, room]) => {
|
|
|
|
|
|
if (room.doorsLayer) {
|
|
|
|
|
|
const doorTiles = room.doorsLayer.getTilesWithin()
|
|
|
|
|
|
.filter(tile => tile.index !== -1);
|
|
|
|
|
|
|
|
|
|
|
|
// Find all rooms that overlap with this room
|
|
|
|
|
|
Object.entries(rooms).forEach(([otherId, otherRoom]) => {
|
|
|
|
|
|
if (roomsOverlap(room.position, otherRoom.position)) {
|
|
|
|
|
|
otherRoom.wallsLayers.forEach(wallLayer => {
|
|
|
|
|
|
processDoorCollisions(doorTiles, wallLayer, room.doorsLayer);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// processes door collisions
|
|
|
|
|
|
// sets the collision of the door tile to false
|
|
|
|
|
|
// visually indicates the door opening
|
|
|
|
|
|
function processDoorCollisions(doorTiles, wallLayer, doorsLayer) {
|
|
|
|
|
|
doorTiles.forEach(doorTile => {
|
|
|
|
|
|
// Convert door tile coordinates to world coordinates
|
|
|
|
|
|
const worldX = doorsLayer.x + (doorTile.x * doorsLayer.tilemap.tileWidth);
|
|
|
|
|
|
const worldY = doorsLayer.y + (doorTile.y * doorsLayer.tilemap.tileHeight);
|
|
|
|
|
|
|
|
|
|
|
|
// Convert world coordinates back to the wall layer's local coordinates
|
|
|
|
|
|
const wallX = Math.floor((worldX - wallLayer.x) / wallLayer.tilemap.tileWidth);
|
|
|
|
|
|
const wallY = Math.floor((worldY - wallLayer.y) / wallLayer.tilemap.tileHeight);
|
|
|
|
|
|
|
|
|
|
|
|
const wallTile = wallLayer.getTileAt(wallX, wallY);
|
|
|
|
|
|
if (wallTile) {
|
|
|
|
|
|
if (doorTile.properties?.locked) {
|
|
|
|
|
|
wallTile.setCollision(true);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
wallTile.setCollision(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// checks if two rooms overlap
|
|
|
|
|
|
function roomsOverlap(pos1, pos2) {
|
|
|
|
|
|
// Add some tolerance for overlap detection
|
|
|
|
|
|
const OVERLAP_TOLERANCE = 48; // One tile width
|
|
|
|
|
|
const ROOM_WIDTH = 800;
|
|
|
|
|
|
const ROOM_HEIGHT = 600;
|
|
|
|
|
|
|
|
|
|
|
|
return !(pos1.x + ROOM_WIDTH - OVERLAP_TOLERANCE < pos2.x ||
|
|
|
|
|
|
pos1.x > pos2.x + ROOM_WIDTH - OVERLAP_TOLERANCE ||
|
|
|
|
|
|
pos1.y + ROOM_HEIGHT - OVERLAP_TOLERANCE < pos2.y ||
|
|
|
|
|
|
pos1.y > pos2.y + ROOM_HEIGHT - OVERLAP_TOLERANCE);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// initializes the pathfinder
|
|
|
|
|
|
// creates a grid of the world
|
|
|
|
|
|
function initializePathfinder() {
|
|
|
|
|
|
const worldBounds = this.physics.world.bounds;
|
|
|
|
|
|
const gridWidth = Math.ceil(worldBounds.width / GRID_SIZE);
|
|
|
|
|
|
const gridHeight = Math.ceil(worldBounds.height / GRID_SIZE);
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
pathfinder = new EasyStar.js();
|
|
|
|
|
|
const grid = Array(gridHeight).fill().map(() => Array(gridWidth).fill(0));
|
|
|
|
|
|
|
|
|
|
|
|
// Mark walls
|
|
|
|
|
|
Object.values(rooms).forEach(room => {
|
|
|
|
|
|
room.wallsLayers.forEach(wallLayer => {
|
|
|
|
|
|
wallLayer.getTilesWithin().forEach(tile => {
|
|
|
|
|
|
// Only mark as unwalkable if the tile collides AND hasn't been disabled for doors
|
|
|
|
|
|
if (tile.collides && tile.canCollide) { // Add check for canCollide
|
|
|
|
|
|
const gridX = Math.floor((tile.x * TILE_SIZE + wallLayer.x - worldBounds.x) / GRID_SIZE);
|
|
|
|
|
|
const gridY = Math.floor((tile.y * TILE_SIZE + wallLayer.y - worldBounds.y) / GRID_SIZE);
|
|
|
|
|
|
|
|
|
|
|
|
if (gridX >= 0 && gridX < gridWidth && gridY >= 0 && gridY < gridHeight) {
|
|
|
|
|
|
grid[gridY][gridX] = 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
pathfinder.setGrid(grid);
|
|
|
|
|
|
pathfinder.setAcceptableTiles([0]);
|
|
|
|
|
|
pathfinder.enableDiagonals();
|
|
|
|
|
|
|
|
|
|
|
|
console.log('Pathfinding initialized successfully');
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Error initializing pathfinder:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// smooths the path
|
|
|
|
|
|
function smoothPath(path) {
|
|
|
|
|
|
if (path.length <= 2) return path;
|
|
|
|
|
|
|
|
|
|
|
|
const smoothed = [path[0]];
|
|
|
|
|
|
for (let i = 1; i < path.length - 1; i++) {
|
|
|
|
|
|
const prev = path[i - 1];
|
|
|
|
|
|
const current = path[i];
|
|
|
|
|
|
const next = path[i + 1];
|
|
|
|
|
|
|
|
|
|
|
|
// Calculate the angle change
|
|
|
|
|
|
const angle1 = Phaser.Math.Angle.Between(prev.x, prev.y, current.x, current.y);
|
|
|
|
|
|
const angle2 = Phaser.Math.Angle.Between(current.x, current.y, next.x, next.y);
|
|
|
|
|
|
const angleDiff = Math.abs(Phaser.Math.Angle.Wrap(angle1 - angle2));
|
|
|
|
|
|
|
|
|
|
|
|
// Only keep points where there's a significant direction change
|
|
|
|
|
|
if (angleDiff > 0.2) { // About 11.5 degrees
|
|
|
|
|
|
smoothed.push(current);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
smoothed.push(path[path.length - 1]);
|
|
|
|
|
|
|
|
|
|
|
|
return smoothed;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// debugs the path
|
|
|
|
|
|
function debugPath(path) {
|
|
|
|
|
|
if (!path) return;
|
|
|
|
|
|
console.log('Current path:', {
|
|
|
|
|
|
pathLength: path.length,
|
|
|
|
|
|
currentTarget: path[0],
|
|
|
|
|
|
playerPos: { x: player.x, y: player.y },
|
|
|
|
|
|
isMoving: isMoving
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// optimizes the path
|
|
|
|
|
|
function optimizePath(path) {
|
|
|
|
|
|
if (path.length <= 2) return path;
|
|
|
|
|
|
|
|
|
|
|
|
const optimized = [path[0]];
|
|
|
|
|
|
let currentPoint = 0;
|
|
|
|
|
|
|
|
|
|
|
|
while (currentPoint < path.length - 1) {
|
|
|
|
|
|
// Look ahead as far as possible along a straight line
|
|
|
|
|
|
let furthestVisible = currentPoint + 1;
|
|
|
|
|
|
for (let i = currentPoint + 2; i < path.length; i++) {
|
|
|
|
|
|
if (canMoveDirectly(path[currentPoint], path[i])) {
|
|
|
|
|
|
furthestVisible = i;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Add the furthest visible point to our optimized path
|
|
|
|
|
|
optimized.push(path[furthestVisible]);
|
|
|
|
|
|
currentPoint = furthestVisible;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return optimized;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// checks if direct movement is possible
|
|
|
|
|
|
function canMoveDirectly(start, end) {
|
|
|
|
|
|
// Check if there are any walls between start and end points
|
|
|
|
|
|
const distance = Phaser.Math.Distance.Between(start.x, start.y, end.x, end.y);
|
|
|
|
|
|
const angle = Phaser.Math.Angle.Between(start.x, start.y, end.x, end.y);
|
|
|
|
|
|
|
|
|
|
|
|
// Check several points along the line
|
|
|
|
|
|
const steps = Math.ceil(distance / (GRID_SIZE / 2));
|
|
|
|
|
|
const stepSize = distance / steps;
|
|
|
|
|
|
|
|
|
|
|
|
for (let i = 1; i < steps; i++) {
|
|
|
|
|
|
const pointX = start.x + Math.cos(angle) * (stepSize * i);
|
|
|
|
|
|
const pointY = start.y + Math.sin(angle) * (stepSize * i);
|
|
|
|
|
|
|
|
|
|
|
|
// Check if this point intersects with any walls
|
|
|
|
|
|
let collision = false;
|
|
|
|
|
|
Object.values(rooms).forEach(room => {
|
|
|
|
|
|
room.wallsLayers.forEach(wallLayer => {
|
|
|
|
|
|
const tile = wallLayer.getTileAtWorldXY(pointX, pointY);
|
|
|
|
|
|
if (tile && tile.collides) {
|
|
|
|
|
|
collision = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (collision) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// updates the player's room
|
|
|
|
|
|
function updatePlayerRoom() {
|
|
|
|
|
|
// Update last position
|
|
|
|
|
|
lastPlayerPosition = { x: player.x, y: player.y };
|
|
|
|
|
|
|
|
|
|
|
|
let overlappingRooms = [];
|
|
|
|
|
|
|
|
|
|
|
|
// Check all rooms for overlap
|
|
|
|
|
|
for (const [roomId, room] of Object.entries(rooms)) {
|
|
|
|
|
|
const bounds = getRoomBounds(roomId);
|
|
|
|
|
|
if (isPlayerInBounds(bounds)) {
|
|
|
|
|
|
overlappingRooms.push(roomId);
|
|
|
|
|
|
|
|
|
|
|
|
// Reveal room if not already visible
|
|
|
|
|
|
if (!discoveredRooms.has(roomId)) {
|
|
|
|
|
|
console.log(`Player overlapping room: ${roomId}`);
|
|
|
|
|
|
revealRoom(roomId);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// If we're not overlapping any rooms
|
|
|
|
|
|
if (overlappingRooms.length === 0) {
|
|
|
|
|
|
console.log('Player not in any room');
|
|
|
|
|
|
currentPlayerRoom = null;
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Update current room (use the first overlapping room as the "main" room)
|
|
|
|
|
|
if (currentPlayerRoom !== overlappingRooms[0]) {
|
|
|
|
|
|
console.log(`Player's main room changed to: ${overlappingRooms[0]}`);
|
|
|
|
|
|
currentPlayerRoom = overlappingRooms[0];
|
|
|
|
|
|
onRoomChange(overlappingRooms[0]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return currentPlayerRoom;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// gets the bounds of a room
|
|
|
|
|
|
function getRoomBounds(roomId) {
|
|
|
|
|
|
const room = rooms[roomId];
|
|
|
|
|
|
return {
|
|
|
|
|
|
x: room.position.x,
|
|
|
|
|
|
y: room.position.y,
|
|
|
|
|
|
width: room.map.widthInPixels,
|
|
|
|
|
|
height: room.map.heightInPixels
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// checks if the player is in bounds
|
|
|
|
|
|
function isPlayerInBounds(bounds) {
|
|
|
|
|
|
const buffer = 0; // Changed from TILE_SIZE (48) to 0
|
|
|
|
|
|
return (
|
|
|
|
|
|
player.x >= bounds.x - buffer &&
|
|
|
|
|
|
player.x <= bounds.x + bounds.width + buffer &&
|
|
|
|
|
|
player.y >= bounds.y - buffer &&
|
|
|
|
|
|
player.y <= bounds.y + bounds.height + buffer
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// handles room changes
|
|
|
|
|
|
// reveals the new room
|
|
|
|
|
|
// hides rooms that aren't connected and aren't currently being overlapped
|
|
|
|
|
|
function onRoomChange(newRoomId) {
|
|
|
|
|
|
// Reveal the new room (although it should already be revealed)
|
|
|
|
|
|
revealRoom.call(this, newRoomId);
|
|
|
|
|
|
|
|
|
|
|
|
// Only hide rooms that aren't connected AND aren't currently being overlapped
|
|
|
|
|
|
Object.keys(rooms).forEach(roomId => {
|
|
|
|
|
|
const bounds = getRoomBounds(roomId);
|
|
|
|
|
|
const playerOverlapping = isPlayerInBounds(bounds);
|
|
|
|
|
|
|
|
|
|
|
|
if (hideNonAdjacentRooms && !playerOverlapping && !isConnectedRoom(newRoomId, roomId)) {
|
|
|
|
|
|
hideRoom.call(this, roomId);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// hides a room
|
|
|
|
|
|
function hideRoom(roomId) {
|
|
|
|
|
|
if (rooms[roomId]) {
|
|
|
|
|
|
const room = rooms[roomId];
|
|
|
|
|
|
|
|
|
|
|
|
// Hide all layers
|
|
|
|
|
|
Object.values(room.layers).forEach(layer => {
|
|
|
|
|
|
if (layer && layer.setVisible) {
|
|
|
|
|
|
layer.setVisible(false);
|
|
|
|
|
|
layer.setAlpha(0);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Hide all objects (both active and inactive)
|
|
|
|
|
|
if (room.objects) {
|
|
|
|
|
|
Object.values(room.objects).forEach(obj => {
|
|
|
|
|
|
if (obj && obj.setVisible) {
|
|
|
|
|
|
obj.setVisible(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// checks if rooms are connected
|
|
|
|
|
|
function isConnectedRoom(currentRoomId, checkRoomId) {
|
|
|
|
|
|
const currentRoom = gameScenario.rooms[currentRoomId];
|
|
|
|
|
|
if (!currentRoom || !currentRoom.connections) return false;
|
|
|
|
|
|
|
|
|
|
|
|
// Check all connections
|
|
|
|
|
|
return Object.values(currentRoom.connections).some(connection => {
|
|
|
|
|
|
if (Array.isArray(connection)) {
|
|
|
|
|
|
return connection.includes(checkRoomId);
|
|
|
|
|
|
}
|
|
|
|
|
|
return connection === checkRoomId;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// handles interactions with objects
|
|
|
|
|
|
// displays the object's data in an alert
|
|
|
|
|
|
function handleObjectInteraction(sprite) {
|
2025-03-08 13:19:58 +00:00
|
|
|
|
// Only log detailed object interactions at debug level 2+
|
|
|
|
|
|
debugLog('OBJECT INTERACTION', {
|
|
|
|
|
|
name: sprite.name,
|
|
|
|
|
|
hasWorkstation: !!sprite.openCryptoWorkstation
|
|
|
|
|
|
}, 2);
|
2024-12-19 18:40:48 +00:00
|
|
|
|
|
2025-02-01 02:23:36 +00:00
|
|
|
|
if (sprite.openCryptoWorkstation && sprite.openCryptoWorkstation()) {
|
2025-03-08 13:19:58 +00:00
|
|
|
|
debugLog('WORKSTATION OPENED', null, 1);
|
2024-12-19 18:40:48 +00:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-12-19 17:57:10 +00:00
|
|
|
|
if (!sprite || !sprite.scenarioData) {
|
|
|
|
|
|
console.warn('Invalid sprite or missing scenario data');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Skip range check for inventory items
|
|
|
|
|
|
const isInventoryItem = inventory.items.includes(sprite);
|
|
|
|
|
|
if (!isInventoryItem) {
|
|
|
|
|
|
// Check if player is in range
|
|
|
|
|
|
const dx = player.x - sprite.x;
|
|
|
|
|
|
const dy = player.y - sprite.y;
|
|
|
|
|
|
const distanceSq = dx * dx + dy * dy;
|
|
|
|
|
|
|
|
|
|
|
|
if (distanceSq > INTERACTION_RANGE_SQ) {
|
2025-03-08 13:19:58 +00:00
|
|
|
|
// Show notification instead of alert
|
|
|
|
|
|
//gameAlert("Too far away to interact with this object.", 'warning', '', 2000);
|
2024-12-19 17:57:10 +00:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const data = sprite.scenarioData;
|
|
|
|
|
|
|
2025-03-08 13:19:58 +00:00
|
|
|
|
// Add inside handleObjectInteraction before the fingerprint check
|
|
|
|
|
|
if (data.biometricType === 'fingerprint') {
|
|
|
|
|
|
handleBiometricScan(sprite, player);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Check for fingerprint collection possibility
|
|
|
|
|
|
if (data.hasFingerprint) {
|
|
|
|
|
|
// Check if player has fingerprint kit
|
|
|
|
|
|
const hasKit = inventory.items.some(item =>
|
|
|
|
|
|
item && item.scenarioData &&
|
|
|
|
|
|
item.scenarioData.type === 'fingerprint_kit'
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (hasKit) {
|
|
|
|
|
|
const sample = collectFingerprint(sprite);
|
|
|
|
|
|
if (sample) {
|
|
|
|
|
|
return; // Exit after collecting fingerprint
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-02-18 16:47:27 +00:00
|
|
|
|
// Check if this is an unlocked container that hasn't been collected yet
|
|
|
|
|
|
if (data.isUnlockedButNotCollected && data.contents) {
|
|
|
|
|
|
let message = `You found the following items:\n`;
|
2025-02-02 01:31:21 +00:00
|
|
|
|
data.contents.forEach(item => {
|
|
|
|
|
|
message += `- ${item.name}\n`;
|
2024-12-19 17:57:10 +00:00
|
|
|
|
});
|
2025-03-08 13:19:58 +00:00
|
|
|
|
|
|
|
|
|
|
// Show notification instead of alert
|
|
|
|
|
|
gameAlert(message, 'success', 'Items Found', 5000);
|
2024-12-19 17:57:10 +00:00
|
|
|
|
|
|
|
|
|
|
// Add all contents to inventory
|
|
|
|
|
|
data.contents.forEach(item => {
|
2025-02-02 01:31:21 +00:00
|
|
|
|
const contentSprite = createInventorySprite({
|
|
|
|
|
|
...item,
|
|
|
|
|
|
type: item.type.toLowerCase()
|
|
|
|
|
|
});
|
2024-12-19 17:57:10 +00:00
|
|
|
|
if (contentSprite) {
|
|
|
|
|
|
addToInventory(contentSprite);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2025-02-18 16:47:27 +00:00
|
|
|
|
|
2024-12-19 17:57:10 +00:00
|
|
|
|
// Clear contents after adding to inventory
|
|
|
|
|
|
data.contents = [];
|
2025-02-18 16:47:27 +00:00
|
|
|
|
data.isUnlockedButNotCollected = false;
|
2024-12-19 17:57:10 +00:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-08 13:19:58 +00:00
|
|
|
|
// Check if item is locked
|
2025-02-18 16:47:27 +00:00
|
|
|
|
if (data.locked === true) {
|
2025-03-08 13:19:58 +00:00
|
|
|
|
debugLog('ITEM LOCKED', data, 2);
|
2025-02-18 16:47:27 +00:00
|
|
|
|
handleUnlock(sprite, 'item');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let message = `${data.name}\n\n`;
|
|
|
|
|
|
message += `Observations: ${data.observations}\n\n`;
|
|
|
|
|
|
|
|
|
|
|
|
if (data.readable && data.text) {
|
|
|
|
|
|
message += `Text: ${data.text}\n\n`;
|
2025-03-08 13:19:58 +00:00
|
|
|
|
|
|
|
|
|
|
// Add readable text as a note
|
|
|
|
|
|
if (data.text.trim().length > 0) {
|
|
|
|
|
|
const addedNote = addNote(data.name, data.text, data.important || false);
|
|
|
|
|
|
|
|
|
|
|
|
// Only show notification if a new note was actually added (not a duplicate)
|
|
|
|
|
|
if (addedNote) {
|
|
|
|
|
|
gameAlert(`Added "${data.name}" to your notes.`, 'info', 'Note Added', 3000);
|
|
|
|
|
|
|
|
|
|
|
|
// If this is a note in the inventory, remove it after adding to notes list
|
|
|
|
|
|
if (isInventoryItem && data.type === 'notes') {
|
|
|
|
|
|
// Remove from inventory after a short delay to allow the player to see the message
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
if (removeFromInventory(sprite)) {
|
|
|
|
|
|
gameAlert(`Removed "${data.name}" from inventory after recording in notes.`, 'success', 'Inventory Updated', 3000);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 1000);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-02-18 16:47:27 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-12-19 17:57:10 +00:00
|
|
|
|
if (data.takeable) {
|
2025-03-08 13:19:58 +00:00
|
|
|
|
// If it's a note type item that's already been read and added to notes,
|
|
|
|
|
|
// don't add it to inventory unless it has a special purpose
|
|
|
|
|
|
const isJustInformationalNote =
|
|
|
|
|
|
data.type === 'notes' &&
|
|
|
|
|
|
data.readable &&
|
|
|
|
|
|
data.text &&
|
|
|
|
|
|
!data.hasSpecialPurpose; // Add this flag to notes that need to be in inventory
|
2024-12-19 17:57:10 +00:00
|
|
|
|
|
2025-03-08 13:19:58 +00:00
|
|
|
|
if (!isJustInformationalNote) {
|
|
|
|
|
|
message += `This item can be taken\n\n`;
|
|
|
|
|
|
|
|
|
|
|
|
if (!inventory || !Array.isArray(inventory.items)) {
|
|
|
|
|
|
console.error('Inventory not properly initialized');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const isInRoom = currentRoom &&
|
|
|
|
|
|
rooms[currentRoom] &&
|
|
|
|
|
|
rooms[currentRoom].objects &&
|
|
|
|
|
|
rooms[currentRoom].objects[sprite.name];
|
|
|
|
|
|
|
|
|
|
|
|
const itemIdentifier = createItemIdentifier(sprite.scenarioData);
|
|
|
|
|
|
|
|
|
|
|
|
const isInInventory = inventory.items.some(item =>
|
|
|
|
|
|
item && createItemIdentifier(item.scenarioData) === itemIdentifier
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (isInRoom && !isInInventory) {
|
|
|
|
|
|
debugLog('INVENTORY ITEM ADDED', { item: itemIdentifier }, 2);
|
|
|
|
|
|
addToInventory(sprite);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// For informational notes, just remove them from the room after reading
|
|
|
|
|
|
if (currentRoom &&
|
|
|
|
|
|
rooms[currentRoom] &&
|
|
|
|
|
|
rooms[currentRoom].objects &&
|
|
|
|
|
|
rooms[currentRoom].objects[sprite.name]) {
|
2024-12-19 17:57:10 +00:00
|
|
|
|
|
2025-03-08 13:19:58 +00:00
|
|
|
|
const roomObj = rooms[currentRoom].objects[sprite.name];
|
|
|
|
|
|
roomObj.setVisible(false);
|
|
|
|
|
|
roomObj.active = false;
|
2024-12-19 17:57:10 +00:00
|
|
|
|
|
2025-03-08 13:19:58 +00:00
|
|
|
|
// Show notification about adding to notes instead of inventory
|
|
|
|
|
|
gameAlert(`Information recorded in your notes.`, 'success', 'Note Recorded', 3000);
|
|
|
|
|
|
}
|
2024-12-19 17:57:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-08 13:19:58 +00:00
|
|
|
|
// Show notification instead of alert
|
|
|
|
|
|
gameAlert(message, 'info', data.name, 7000);
|
2024-12-19 17:57:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// adds an item to the inventory
|
|
|
|
|
|
// removes the item from the room if it exists
|
|
|
|
|
|
// creates a new sprite for the item in the inventory
|
|
|
|
|
|
function addToInventory(sprite) {
|
|
|
|
|
|
if (!sprite || !sprite.scenarioData) {
|
|
|
|
|
|
console.warn('Invalid sprite for inventory');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
2025-03-08 16:32:29 +00:00
|
|
|
|
// Check if the item is already in the inventory
|
|
|
|
|
|
const isAlreadyInInventory = inventory.items.some(item => item.name === sprite.name);
|
|
|
|
|
|
if (isAlreadyInInventory) {
|
|
|
|
|
|
console.log(`Item ${sprite.name} is already in inventory, not adding again`);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-12-19 17:57:10 +00:00
|
|
|
|
// Remove from room if it exists
|
|
|
|
|
|
if (currentRoom &&
|
|
|
|
|
|
rooms[currentRoom] &&
|
|
|
|
|
|
rooms[currentRoom].objects &&
|
|
|
|
|
|
rooms[currentRoom].objects[sprite.name]) {
|
|
|
|
|
|
|
|
|
|
|
|
const roomObj = rooms[currentRoom].objects[sprite.name];
|
|
|
|
|
|
roomObj.setVisible(false);
|
|
|
|
|
|
roomObj.active = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const scene = sprite.scene;
|
|
|
|
|
|
|
|
|
|
|
|
// Create new sprite for inventory
|
|
|
|
|
|
const inventorySprite = scene.add.sprite(
|
|
|
|
|
|
inventory.items.length * 60 + 100,
|
|
|
|
|
|
0,
|
|
|
|
|
|
sprite.name
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
inventorySprite.setScale(0.8);
|
|
|
|
|
|
inventorySprite.setInteractive({ useHandCursor: true, pixelPerfect: true });
|
|
|
|
|
|
inventorySprite.scenarioData = {
|
|
|
|
|
|
...sprite.scenarioData,
|
|
|
|
|
|
foundIn: currentRoom ? gameScenario.rooms[currentRoom].name || currentRoom : 'unknown location'
|
|
|
|
|
|
};
|
|
|
|
|
|
inventorySprite.name = sprite.name;
|
|
|
|
|
|
|
2024-12-19 18:40:48 +00:00
|
|
|
|
// Copy over the custom interaction if it exists
|
2025-02-01 02:23:36 +00:00
|
|
|
|
if (sprite.openCryptoWorkstation) {
|
|
|
|
|
|
inventorySprite.openCryptoWorkstation = sprite.openCryptoWorkstation;
|
2024-12-19 18:40:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-12-19 17:57:10 +00:00
|
|
|
|
// Set depth higher than container
|
|
|
|
|
|
inventorySprite.setDepth(2003);
|
|
|
|
|
|
|
|
|
|
|
|
// Add pointer events
|
2025-03-08 15:00:17 +00:00
|
|
|
|
inventorySprite.on('pointerdown', function(pointer) {
|
|
|
|
|
|
// Check if this is the Bluetooth scanner
|
|
|
|
|
|
if (this.scenarioData.type === "bluetooth_scanner") {
|
|
|
|
|
|
// Toggle the Bluetooth scanner panel
|
|
|
|
|
|
toggleBluetoothPanel();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Handle other inventory items as before
|
|
|
|
|
|
handleItemInteraction(this, true);
|
2024-12-19 17:57:10 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
inventorySprite.on('pointerover', function() {
|
|
|
|
|
|
this.setTint(0xdddddd);
|
2025-03-08 15:00:17 +00:00
|
|
|
|
|
|
|
|
|
|
// Show tooltip with item name
|
|
|
|
|
|
const tooltipText = scene.add.text(
|
|
|
|
|
|
this.x,
|
|
|
|
|
|
this.y - 40,
|
|
|
|
|
|
this.scenarioData.name,
|
|
|
|
|
|
{
|
|
|
|
|
|
fontSize: '14px',
|
|
|
|
|
|
backgroundColor: '#000',
|
|
|
|
|
|
padding: { x: 5, y: 3 },
|
|
|
|
|
|
fixedWidth: 150,
|
|
|
|
|
|
align: 'center'
|
|
|
|
|
|
}
|
|
|
|
|
|
).setOrigin(0.5, 0.5).setDepth(2004);
|
|
|
|
|
|
|
|
|
|
|
|
this.tooltip = tooltipText;
|
2024-12-19 17:57:10 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
inventorySprite.on('pointerout', function() {
|
|
|
|
|
|
this.clearTint();
|
2025-03-08 15:00:17 +00:00
|
|
|
|
|
|
|
|
|
|
if (this.tooltip) {
|
|
|
|
|
|
this.tooltip.destroy();
|
|
|
|
|
|
this.tooltip = null;
|
|
|
|
|
|
}
|
2024-12-19 17:57:10 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
2025-03-08 15:00:17 +00:00
|
|
|
|
// Add to inventory array
|
2024-12-19 17:57:10 +00:00
|
|
|
|
inventory.items.push(inventorySprite);
|
|
|
|
|
|
|
2025-03-08 15:00:17 +00:00
|
|
|
|
// Add to container
|
|
|
|
|
|
inventory.container.add(inventorySprite);
|
|
|
|
|
|
|
|
|
|
|
|
// Show notification
|
|
|
|
|
|
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');
|
|
|
|
|
|
bluetoothToggle.style.display = 'flex';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-08 22:03:24 +00:00
|
|
|
|
// If this is the fingerprint kit, show the biometrics toggle button
|
|
|
|
|
|
if (sprite.scenarioData.type === "fingerprint_kit") {
|
|
|
|
|
|
const biometricsToggle = document.getElementById('biometrics-toggle');
|
|
|
|
|
|
biometricsToggle.style.display = 'flex';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-08 15:00:17 +00:00
|
|
|
|
return true;
|
2024-12-19 17:57:10 +00:00
|
|
|
|
} catch (error) {
|
2025-03-08 15:00:17 +00:00
|
|
|
|
console.error('Error adding to inventory:', error);
|
|
|
|
|
|
return false;
|
2024-12-19 17:57:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// initializes inventory
|
|
|
|
|
|
// creates the background and slot outlines
|
|
|
|
|
|
function initializeInventory() {
|
|
|
|
|
|
// Reset inventory state
|
|
|
|
|
|
inventory.items = [];
|
|
|
|
|
|
|
|
|
|
|
|
// Create slot outlines
|
|
|
|
|
|
const slotsContainer = this.add.container(110, this.cameras.main.height - 60) // Shifted 100px to the right
|
|
|
|
|
|
.setScrollFactor(0)
|
|
|
|
|
|
.setDepth(2001);
|
|
|
|
|
|
|
|
|
|
|
|
// Create 10 slot outlines
|
|
|
|
|
|
for (let i = 0; i < 10; i++) {
|
|
|
|
|
|
const outline = this.add.rectangle(
|
|
|
|
|
|
i * 60,
|
|
|
|
|
|
0,
|
|
|
|
|
|
50, // slightly smaller than spacing
|
|
|
|
|
|
50,
|
|
|
|
|
|
0x666666,
|
|
|
|
|
|
0.3
|
|
|
|
|
|
);
|
|
|
|
|
|
outline.setStrokeStyle(1, 0x666666);
|
|
|
|
|
|
slotsContainer.add(outline);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Initialize inventory container
|
|
|
|
|
|
inventory.container = this.add.container(10, this.cameras.main.height - 60)
|
|
|
|
|
|
.setScrollFactor(0)
|
|
|
|
|
|
.setDepth(2001);
|
|
|
|
|
|
|
2025-03-08 13:19:58 +00:00
|
|
|
|
debugLog('INVENTORY INITIALIZED', inventory, 2); // Debug log at level 2
|
2024-12-19 17:57:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// runs after rooms are fully set up
|
|
|
|
|
|
// checks if doors are overlapping rooms and removes them if they are not
|
|
|
|
|
|
function validateDoorsByRoomOverlap() {
|
|
|
|
|
|
// First, run the existing door validation
|
|
|
|
|
|
Object.entries(rooms).forEach(([roomId, room]) => {
|
|
|
|
|
|
if (!room.doorsLayer) return;
|
|
|
|
|
|
|
|
|
|
|
|
const doorTiles = room.doorsLayer.getTilesWithin().filter(tile => tile.index !== -1);
|
|
|
|
|
|
|
|
|
|
|
|
doorTiles.forEach(doorTile => {
|
|
|
|
|
|
// Calculate world coordinates for this door tile
|
|
|
|
|
|
const doorWorldX = room.doorsLayer.x + (doorTile.x * TILE_SIZE);
|
|
|
|
|
|
const doorWorldY = room.doorsLayer.y + (doorTile.y * TILE_SIZE);
|
|
|
|
|
|
|
|
|
|
|
|
// Create a door check area that extends in all directions
|
|
|
|
|
|
const doorCheckArea = {
|
|
|
|
|
|
x: doorWorldX - DOOR_ALIGN_OVERLAP,
|
|
|
|
|
|
y: doorWorldY - DOOR_ALIGN_OVERLAP,
|
|
|
|
|
|
width: DOOR_ALIGN_OVERLAP * 2,
|
|
|
|
|
|
height: DOOR_ALIGN_OVERLAP * 2
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Track overlapping rooms and their data
|
|
|
|
|
|
let overlappingRoomData = [];
|
|
|
|
|
|
|
|
|
|
|
|
Object.entries(rooms).forEach(([otherId, otherRoom]) => {
|
|
|
|
|
|
const otherBounds = {
|
|
|
|
|
|
x: otherRoom.position.x,
|
|
|
|
|
|
y: otherRoom.position.y,
|
|
|
|
|
|
width: otherRoom.map.widthInPixels,
|
|
|
|
|
|
height: otherRoom.map.heightInPixels
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Check if the door's check area overlaps with this room
|
|
|
|
|
|
if (boundsOverlap(doorCheckArea, otherBounds)) {
|
|
|
|
|
|
overlappingRoomData.push({
|
|
|
|
|
|
id: otherId,
|
|
|
|
|
|
locked: gameScenario.rooms[otherId].locked,
|
|
|
|
|
|
lockType: gameScenario.rooms[otherId].lockType,
|
|
|
|
|
|
requires: gameScenario.rooms[otherId].requires
|
|
|
|
|
|
});
|
|
|
|
|
|
console.log(`Door at (${doorWorldX}, ${doorWorldY}) overlaps with room ${otherId}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// If door doesn't overlap exactly 2 rooms, remove it
|
|
|
|
|
|
if (overlappingRoomData.length < 2) {
|
|
|
|
|
|
console.log(`Removing door at (${doorWorldX}, ${doorWorldY}) - overlaps ${overlappingRoomData.length} rooms`);
|
|
|
|
|
|
doorTile.index = -1; // Remove the door tile
|
|
|
|
|
|
|
|
|
|
|
|
// Restore wall collision where door was removed
|
|
|
|
|
|
room.wallsLayers.forEach(wallLayer => {
|
|
|
|
|
|
const wallTile = wallLayer.getTileAt(doorTile.x, doorTile.y);
|
|
|
|
|
|
if (wallTile) {
|
|
|
|
|
|
wallTile.setCollision(true);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Check if any of the overlapping rooms are locked
|
|
|
|
|
|
const lockedRoom = overlappingRoomData.find(room => room.locked);
|
|
|
|
|
|
|
|
|
|
|
|
if (lockedRoom) {
|
|
|
|
|
|
// Set the door tile properties to match the locked room
|
|
|
|
|
|
if (!doorTile.properties) doorTile.properties = {};
|
|
|
|
|
|
doorTile.properties.locked = true;
|
|
|
|
|
|
doorTile.properties.lockType = lockedRoom.lockType;
|
|
|
|
|
|
doorTile.properties.requires = lockedRoom.requires;
|
|
|
|
|
|
console.log(`Door at (${doorWorldX}, ${doorWorldY}) marked as locked:`, doorTile.properties);
|
|
|
|
|
|
|
|
|
|
|
|
// Ensure wall collision remains for locked doors
|
|
|
|
|
|
room.wallsLayers.forEach(wallLayer => {
|
|
|
|
|
|
const wallTile = wallLayer.getTileAt(doorTile.x, doorTile.y);
|
|
|
|
|
|
if (wallTile) {
|
|
|
|
|
|
wallTile.setCollision(true);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Valid unlocked door - ensure no wall collision
|
|
|
|
|
|
room.wallsLayers.forEach(wallLayer => {
|
|
|
|
|
|
const wallTile = wallLayer.getTileAt(doorTile.x, doorTile.y);
|
|
|
|
|
|
if (wallTile) {
|
|
|
|
|
|
wallTile.setCollision(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function pointInRoomBounds(x, y, bounds) {
|
|
|
|
|
|
return (x >= bounds.x &&
|
|
|
|
|
|
x <= bounds.x + bounds.width &&
|
|
|
|
|
|
y >= bounds.y &&
|
|
|
|
|
|
y <= bounds.height);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Add this helper function to check if two bounds overlap
|
|
|
|
|
|
function boundsOverlap(bounds1, bounds2) {
|
|
|
|
|
|
return !(bounds1.x + bounds1.width < bounds2.x ||
|
|
|
|
|
|
bounds1.x > bounds2.x + bounds2.width ||
|
|
|
|
|
|
bounds1.y + bounds1.height < bounds2.y ||
|
|
|
|
|
|
bounds1.y > bounds2.y + bounds2.height);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Add this new helper function:
|
|
|
|
|
|
function createItemIdentifier(scenarioData) {
|
|
|
|
|
|
// Combine multiple properties to create a unique identifier
|
|
|
|
|
|
const identifierParts = [
|
|
|
|
|
|
scenarioData.type,
|
|
|
|
|
|
scenarioData.name,
|
|
|
|
|
|
// Add more unique properties if available
|
|
|
|
|
|
scenarioData.key_id, // For keys
|
|
|
|
|
|
scenarioData.requires, // For locks
|
|
|
|
|
|
scenarioData.text // For readable items
|
|
|
|
|
|
].filter(Boolean); // Remove any undefined/null values
|
|
|
|
|
|
|
|
|
|
|
|
return identifierParts.join('|');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Add this new function after the other function definitions
|
|
|
|
|
|
function setupDoorOverlapChecks() {
|
|
|
|
|
|
const DOOR_INTERACTION_RANGE = 2 * TILE_SIZE;
|
|
|
|
|
|
|
|
|
|
|
|
Object.entries(rooms).forEach(([roomId, room]) => {
|
|
|
|
|
|
if (!room.doorsLayer) return;
|
|
|
|
|
|
|
|
|
|
|
|
const doorTiles = room.doorsLayer.getTilesWithin().filter(tile => tile.index !== -1);
|
|
|
|
|
|
|
|
|
|
|
|
doorTiles.forEach(doorTile => {
|
|
|
|
|
|
const worldX = room.doorsLayer.x + (doorTile.x * TILE_SIZE);
|
|
|
|
|
|
const worldY = room.doorsLayer.y + (doorTile.y * TILE_SIZE);
|
|
|
|
|
|
|
|
|
|
|
|
const zone = this.add.zone(worldX + TILE_SIZE/2, worldY + TILE_SIZE/2, TILE_SIZE, TILE_SIZE);
|
|
|
|
|
|
zone.setInteractive({ useHandCursor: true });
|
|
|
|
|
|
|
|
|
|
|
|
zone.on('pointerdown', () => {
|
|
|
|
|
|
console.log('Door clicked:', { doorTile, room });
|
|
|
|
|
|
const distance = Phaser.Math.Distance.Between(
|
|
|
|
|
|
player.x, player.y,
|
|
|
|
|
|
worldX + TILE_SIZE/2, worldY + TILE_SIZE/2
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (distance <= DOOR_INTERACTION_RANGE) {
|
|
|
|
|
|
if (doorTile.properties?.locked) {
|
2025-03-08 13:19:58 +00:00
|
|
|
|
debugLog('DOOR LOCKED - ATTEMPTING UNLOCK', null, 2);
|
2024-12-19 17:57:10 +00:00
|
|
|
|
colorDoorTiles(doorTile, room);
|
|
|
|
|
|
handleDoorUnlock(doorTile, room);
|
|
|
|
|
|
} else {
|
2025-03-08 13:19:58 +00:00
|
|
|
|
debugLog('DOOR NOT LOCKED', null, 2);
|
2024-12-19 17:57:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
} else {
|
2025-03-08 13:19:58 +00:00
|
|
|
|
debugLog('DOOR TOO FAR TO INTERACT', null, 2);
|
2024-12-19 17:57:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
this.physics.world.enable(zone);
|
|
|
|
|
|
this.physics.add.overlap(player, zone, () => {
|
|
|
|
|
|
colorDoorTiles(doorTile, room);
|
|
|
|
|
|
}, null, this);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function colorDoorTiles(doorTile, room) {
|
|
|
|
|
|
// Visual feedback for door tiles
|
|
|
|
|
|
const doorTiles = [
|
|
|
|
|
|
room.doorsLayer.getTileAt(doorTile.x, doorTile.y - 1),
|
|
|
|
|
|
room.doorsLayer.getTileAt(doorTile.x, doorTile.y),
|
|
|
|
|
|
room.doorsLayer.getTileAt(doorTile.x, doorTile.y + 1)
|
|
|
|
|
|
];
|
|
|
|
|
|
doorTiles.forEach(tile => {
|
|
|
|
|
|
if (tile) {
|
|
|
|
|
|
// Use red tint for locked doors, black for unlocked
|
|
|
|
|
|
const tintColor = doorTile.properties?.locked ? 0xff0000 : 0x000000;
|
|
|
|
|
|
tile.tint = tintColor;
|
|
|
|
|
|
tile.tintFill = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleDoorUnlock(doorTile, room) {
|
2025-03-08 13:19:58 +00:00
|
|
|
|
// No need to log here since handleUnlock will log 'UNLOCK ATTEMPT'
|
2024-12-19 17:57:10 +00:00
|
|
|
|
doorTile.layer = room.doorsLayer; // Ensure layer reference is set
|
|
|
|
|
|
handleUnlock(doorTile, 'door');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleUnlock(lockable, type) {
|
2025-03-08 13:19:58 +00:00
|
|
|
|
debugLog('UNLOCK ATTEMPT', null, 2);
|
2024-12-19 17:57:10 +00:00
|
|
|
|
|
|
|
|
|
|
// Check locked state in scenarioData for items
|
|
|
|
|
|
const isLocked = type === 'door' ?
|
|
|
|
|
|
lockable.properties?.locked :
|
|
|
|
|
|
lockable.scenarioData?.locked;
|
|
|
|
|
|
|
|
|
|
|
|
if (!isLocked) {
|
2025-03-08 13:19:58 +00:00
|
|
|
|
debugLog('OBJECT NOT LOCKED', null, 2);
|
2024-12-19 17:57:10 +00:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Get lock requirements based on type
|
|
|
|
|
|
const lockRequirements = type === 'door'
|
|
|
|
|
|
? getLockRequirementsForDoor(lockable)
|
|
|
|
|
|
: getLockRequirementsForItem(lockable);
|
|
|
|
|
|
|
2025-03-08 13:19:58 +00:00
|
|
|
|
// Don't log lock requirements here since it's already logged in the getter functions
|
2024-12-19 17:57:10 +00:00
|
|
|
|
|
|
|
|
|
|
if (!lockRequirements) {
|
2025-03-08 13:19:58 +00:00
|
|
|
|
// Don't log here since it's already logged in the getter functions if applicable
|
2024-12-19 17:57:10 +00:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
switch(lockRequirements.lockType) {
|
|
|
|
|
|
case 'key':
|
|
|
|
|
|
const requiredKey = lockRequirements.requires;
|
2025-03-08 13:19:58 +00:00
|
|
|
|
debugLog('KEY REQUIRED', requiredKey, 2);
|
2024-12-19 17:57:10 +00:00
|
|
|
|
const hasKey = inventory.items.some(item =>
|
|
|
|
|
|
item && item.scenarioData &&
|
|
|
|
|
|
item.scenarioData.key_id === requiredKey
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (hasKey) {
|
|
|
|
|
|
const keyItem = inventory.items.find(item =>
|
|
|
|
|
|
item && item.scenarioData &&
|
|
|
|
|
|
item.scenarioData.key_id === requiredKey
|
|
|
|
|
|
);
|
|
|
|
|
|
const keyName = keyItem?.scenarioData?.name || 'key';
|
|
|
|
|
|
const keyLocation = keyItem?.scenarioData?.foundIn || 'your inventory';
|
|
|
|
|
|
|
2025-03-08 13:19:58 +00:00
|
|
|
|
debugLog('KEY UNLOCK SUCCESS', null, 1);
|
|
|
|
|
|
unlockTarget(lockable, type, lockable.layer);
|
|
|
|
|
|
gameAlert(`You used the ${keyName} that you found in ${keyLocation} to unlock the ${type}.`, 'success', 'Unlock Successful', 5000);
|
2024-12-19 17:57:10 +00:00
|
|
|
|
} else {
|
2025-03-08 13:19:58 +00:00
|
|
|
|
// Check for lockpick kit
|
|
|
|
|
|
const hasLockpick = inventory.items.some(item =>
|
|
|
|
|
|
item && item.scenarioData &&
|
|
|
|
|
|
item.scenarioData.type === 'lockpick'
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (hasLockpick) {
|
|
|
|
|
|
debugLog('LOCKPICK AVAILABLE', null, 2);
|
|
|
|
|
|
if (confirm("Would you like to attempt picking this lock?")) {
|
|
|
|
|
|
let difficulty;
|
|
|
|
|
|
|
|
|
|
|
|
// If this is a room-level lock, get difficulty from gameScenario
|
|
|
|
|
|
if (lockable.properties?.requires) {
|
|
|
|
|
|
// Find which room this lock belongs to
|
|
|
|
|
|
const roomId = Object.keys(gameScenario.rooms).find(roomId => {
|
|
|
|
|
|
const room = gameScenario.rooms[roomId];
|
|
|
|
|
|
return room.requires === lockable.properties.requires;
|
|
|
|
|
|
});
|
|
|
|
|
|
difficulty = roomId ? gameScenario.rooms[roomId].difficulty : null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// If not found, try object-level difficulty
|
|
|
|
|
|
difficulty = difficulty || lockable.scenarioData?.difficulty || lockable.properties?.difficulty;
|
|
|
|
|
|
debugLog('STARTING LOCKPICK MINIGAME', { difficulty }, 2);
|
|
|
|
|
|
startLockpickingMinigame(lockable, game.scene.scenes[0], difficulty);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
debugLog('KEY NOT FOUND - FAIL', null, 2);
|
|
|
|
|
|
gameAlert(`Requires key: ${requiredKey}`, 'error', 'Locked', 4000);
|
|
|
|
|
|
}
|
2024-12-19 17:57:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case 'pin':
|
2025-03-08 13:19:58 +00:00
|
|
|
|
debugLog('PIN CODE REQUESTED', null, 2);
|
2024-12-19 17:57:10 +00:00
|
|
|
|
const pinInput = prompt(`Enter PIN code:`);
|
|
|
|
|
|
if (pinInput === lockRequirements.requires) {
|
|
|
|
|
|
unlockTarget(lockable, type, lockable.layer); // Pass the layer here
|
2025-03-08 13:19:58 +00:00
|
|
|
|
debugLog('PIN CODE SUCCESS', null, 1);
|
|
|
|
|
|
gameAlert(`Correct PIN! The ${type} is now unlocked.`, 'success', 'PIN Accepted', 4000);
|
2024-12-19 17:57:10 +00:00
|
|
|
|
} else if (pinInput !== null) {
|
2025-03-08 13:19:58 +00:00
|
|
|
|
debugLog('PIN CODE FAIL', null, 2);
|
|
|
|
|
|
gameAlert("Incorrect PIN code.", 'error', 'PIN Rejected', 3000);
|
2024-12-19 17:57:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case 'password':
|
2025-03-08 13:19:58 +00:00
|
|
|
|
debugLog('PASSWORD REQUESTED', null, 2);
|
2024-12-19 17:57:10 +00:00
|
|
|
|
const passwordInput = prompt(`Enter password:`);
|
|
|
|
|
|
if (passwordInput === lockRequirements.requires) {
|
|
|
|
|
|
unlockTarget(lockable, type, lockable.layer); // Pass the layer here
|
2025-03-08 13:19:58 +00:00
|
|
|
|
debugLog('PASSWORD SUCCESS', null, 1);
|
|
|
|
|
|
gameAlert(`Correct password! The ${type} is now unlocked.`, 'success', 'Password Accepted', 4000);
|
2024-12-19 17:57:10 +00:00
|
|
|
|
} else if (passwordInput !== null) {
|
2025-03-08 13:19:58 +00:00
|
|
|
|
debugLog('PASSWORD FAIL', null, 2);
|
|
|
|
|
|
gameAlert("Incorrect password.", 'error', 'Password Rejected', 3000);
|
2024-12-19 17:57:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
2025-02-18 16:47:27 +00:00
|
|
|
|
case 'bluetooth':
|
2025-02-18 17:01:17 +00:00
|
|
|
|
if (lockable.scenarioData?.locked) {
|
2025-03-08 15:00:17 +00:00
|
|
|
|
// Try to spoof the Bluetooth device
|
|
|
|
|
|
const spoofResult = spoofBluetoothDevice(lockable);
|
|
|
|
|
|
|
|
|
|
|
|
if (spoofResult) {
|
|
|
|
|
|
// If spoofing was successful, unlock the target
|
|
|
|
|
|
unlockTarget(lockable, type, lockable.layer);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Allow the item to be picked up even if locked
|
2025-02-18 16:47:27 +00:00
|
|
|
|
if (type === 'item' && lockable.scenarioData?.takeable) {
|
2025-03-08 16:32:29 +00:00
|
|
|
|
// Check if the item is already in the inventory before adding it
|
|
|
|
|
|
const isAlreadyInInventory = inventory.items.some(item => item.name === lockable.name);
|
|
|
|
|
|
|
|
|
|
|
|
if (!isAlreadyInInventory) {
|
|
|
|
|
|
addToInventory(lockable);
|
|
|
|
|
|
// Remove from room objects if it exists there
|
|
|
|
|
|
if (currentRoom && rooms[currentRoom].objects) {
|
|
|
|
|
|
delete rooms[currentRoom].objects[lockable.name];
|
|
|
|
|
|
}
|
2025-02-18 16:47:27 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
2024-12-19 17:57:10 +00:00
|
|
|
|
default:
|
2025-03-08 13:19:58 +00:00
|
|
|
|
gameAlert(`Requires: ${lockRequirements.requires}`, 'warning', 'Locked', 4000);
|
2024-12-19 17:57:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-02-18 16:47:27 +00:00
|
|
|
|
// Modify the unlockTarget function
|
2024-12-19 17:57:10 +00:00
|
|
|
|
function unlockTarget(lockable, type, layer) {
|
|
|
|
|
|
if (type === 'door') {
|
|
|
|
|
|
if (!layer) {
|
|
|
|
|
|
console.error('Missing layer for door unlock');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
unlockDoor(lockable, layer);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Handle item unlocking
|
|
|
|
|
|
if (lockable.scenarioData) {
|
|
|
|
|
|
lockable.scenarioData.locked = false;
|
2024-12-19 18:57:26 +00:00
|
|
|
|
// Set new state for containers with contents
|
|
|
|
|
|
if (lockable.scenarioData.contents) {
|
|
|
|
|
|
lockable.scenarioData.isUnlockedButNotCollected = true;
|
2025-02-18 16:47:27 +00:00
|
|
|
|
return; // Return early to prevent automatic collection
|
2024-12-19 18:57:26 +00:00
|
|
|
|
}
|
2024-12-19 17:57:10 +00:00
|
|
|
|
} else {
|
|
|
|
|
|
lockable.locked = false;
|
2024-12-19 18:57:26 +00:00
|
|
|
|
if (lockable.contents) {
|
|
|
|
|
|
lockable.isUnlockedButNotCollected = true;
|
2025-02-18 16:47:27 +00:00
|
|
|
|
return; // Return early to prevent automatic collection
|
2024-12-19 18:57:26 +00:00
|
|
|
|
}
|
2024-12-19 17:57:10 +00:00
|
|
|
|
}
|
2025-02-02 01:31:21 +00:00
|
|
|
|
}
|
2024-12-19 18:57:26 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-12-19 17:57:10 +00:00
|
|
|
|
// Helper function to create inventory sprites for unlocked container contents
|
|
|
|
|
|
function createInventorySprite(itemData) {
|
|
|
|
|
|
const scene = game.scene.scenes[0]; // Get the main scene
|
|
|
|
|
|
if (!scene) return null;
|
|
|
|
|
|
|
|
|
|
|
|
// Create sprite with proper texture key based on item type
|
|
|
|
|
|
const sprite = scene.add.sprite(0, 0, itemData.type.toLowerCase());
|
|
|
|
|
|
sprite.scenarioData = itemData;
|
|
|
|
|
|
sprite.name = itemData.type;
|
|
|
|
|
|
|
|
|
|
|
|
// Set interactive properties
|
2025-03-08 13:19:58 +00:00
|
|
|
|
sprite.setInteractive({ useHandCursor: true, pixelPerfect: true });
|
2024-12-19 17:57:10 +00:00
|
|
|
|
sprite.on('pointerdown', function(event) {
|
|
|
|
|
|
event.stopPropagation();
|
|
|
|
|
|
handleObjectInteraction(this);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
sprite.on('pointerover', function() {
|
|
|
|
|
|
this.setTint(0xdddddd);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
sprite.on('pointerout', function() {
|
|
|
|
|
|
this.clearTint();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return sprite;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function unlockDoor(doorTile, doorsLayer) {
|
|
|
|
|
|
if (!doorsLayer) {
|
|
|
|
|
|
console.error('Missing doorsLayer in unlockDoor');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Remove lock properties from this door and adjacent door tiles
|
|
|
|
|
|
const doorTiles = [
|
|
|
|
|
|
doorsLayer.getTileAt(doorTile.x, doorTile.y - 1),
|
|
|
|
|
|
doorsLayer.getTileAt(doorTile.x, doorTile.y),
|
|
|
|
|
|
doorsLayer.getTileAt(doorTile.x, doorTile.y + 1),
|
|
|
|
|
|
doorsLayer.getTileAt(doorTile.x - 1, doorTile.y),
|
|
|
|
|
|
doorsLayer.getTileAt(doorTile.x + 1, doorTile.y)
|
|
|
|
|
|
].filter(tile => tile && tile.index !== -1);
|
|
|
|
|
|
|
|
|
|
|
|
doorTiles.forEach(tile => {
|
|
|
|
|
|
if (tile.properties) {
|
|
|
|
|
|
tile.properties.locked = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Find the room that contains this doors layer
|
|
|
|
|
|
const room = Object.values(rooms).find(r => r.doorsLayer === doorsLayer);
|
|
|
|
|
|
if (!room) {
|
|
|
|
|
|
console.error('Could not find room for doors layer');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Process each door tile's position to remove wall collisions
|
|
|
|
|
|
doorTiles.forEach(tile => {
|
|
|
|
|
|
const worldX = doorsLayer.x + (tile.x * TILE_SIZE);
|
|
|
|
|
|
const worldY = doorsLayer.y + (tile.y * TILE_SIZE);
|
|
|
|
|
|
|
|
|
|
|
|
const doorCheckArea = {
|
|
|
|
|
|
x: worldX - DOOR_ALIGN_OVERLAP,
|
|
|
|
|
|
y: worldY - DOOR_ALIGN_OVERLAP,
|
|
|
|
|
|
width: DOOR_ALIGN_OVERLAP * 2,
|
|
|
|
|
|
height: DOOR_ALIGN_OVERLAP * 2
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Remove collision for this door in ALL overlapping rooms' wall layers
|
|
|
|
|
|
Object.entries(rooms).forEach(([otherId, otherRoom]) => {
|
|
|
|
|
|
const otherBounds = {
|
|
|
|
|
|
x: otherRoom.position.x,
|
|
|
|
|
|
y: otherRoom.position.y,
|
|
|
|
|
|
width: otherRoom.map.widthInPixels,
|
|
|
|
|
|
height: otherRoom.map.heightInPixels
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (boundsOverlap(doorCheckArea, otherBounds)) {
|
|
|
|
|
|
otherRoom.wallsLayers.forEach(wallLayer => {
|
|
|
|
|
|
const wallX = Math.floor((worldX - wallLayer.x) / TILE_SIZE);
|
|
|
|
|
|
const wallY = Math.floor((worldY - wallLayer.y) / TILE_SIZE);
|
|
|
|
|
|
|
|
|
|
|
|
const wallTile = wallLayer.getTileAt(wallX, wallY);
|
|
|
|
|
|
if (wallTile) {
|
|
|
|
|
|
wallTile.setCollision(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Update door visuals for all affected tiles
|
|
|
|
|
|
doorTiles.forEach(tile => {
|
|
|
|
|
|
colorDoorTiles(tile, room);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getLockRequirementsForDoor(doorTile) {
|
2025-03-08 13:19:58 +00:00
|
|
|
|
debugLog('CHECKING DOOR REQUIREMENTS', null, 3);
|
2024-12-19 17:57:10 +00:00
|
|
|
|
|
|
|
|
|
|
if (!doorTile.layer) {
|
|
|
|
|
|
console.error('Door tile missing layer reference');
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const doorWorldX = doorTile.layer.x + (doorTile.x * TILE_SIZE);
|
|
|
|
|
|
const doorWorldY = doorTile.layer.y + (doorTile.y * TILE_SIZE);
|
|
|
|
|
|
|
2025-03-08 13:19:58 +00:00
|
|
|
|
debugLog('DOOR COORDINATES', { doorWorldX, doorWorldY }, 3);
|
2024-12-19 17:57:10 +00:00
|
|
|
|
|
|
|
|
|
|
const overlappingRooms = [];
|
|
|
|
|
|
Object.entries(rooms).forEach(([roomId, otherRoom]) => {
|
|
|
|
|
|
const doorCheckArea = {
|
|
|
|
|
|
x: doorWorldX - DOOR_ALIGN_OVERLAP,
|
|
|
|
|
|
y: doorWorldY - DOOR_ALIGN_OVERLAP,
|
|
|
|
|
|
width: DOOR_ALIGN_OVERLAP * 2,
|
|
|
|
|
|
height: DOOR_ALIGN_OVERLAP * 2
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const roomBounds = {
|
|
|
|
|
|
x: otherRoom.position.x,
|
|
|
|
|
|
y: otherRoom.position.y,
|
|
|
|
|
|
width: otherRoom.map.widthInPixels,
|
|
|
|
|
|
height: otherRoom.map.heightInPixels
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (boundsOverlap(doorCheckArea, roomBounds)) {
|
2025-03-08 13:19:58 +00:00
|
|
|
|
debugLog(`ROOM ${roomId} OVERLAPS WITH DOOR`, null, 3);
|
2024-12-19 17:57:10 +00:00
|
|
|
|
const roomCenterX = roomBounds.x + (roomBounds.width / 2);
|
|
|
|
|
|
const roomCenterY = roomBounds.y + (roomBounds.height / 2);
|
|
|
|
|
|
const distanceToPlayer = Phaser.Math.Distance.Between(
|
|
|
|
|
|
player.x, player.y,
|
|
|
|
|
|
roomCenterX, roomCenterY
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
overlappingRooms.push({
|
|
|
|
|
|
id: roomId,
|
|
|
|
|
|
room: otherRoom,
|
|
|
|
|
|
distance: distanceToPlayer,
|
|
|
|
|
|
lockType: gameScenario.rooms[roomId].lockType,
|
|
|
|
|
|
requires: gameScenario.rooms[roomId].requires,
|
|
|
|
|
|
locked: gameScenario.rooms[roomId].locked
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-03-08 13:19:58 +00:00
|
|
|
|
debugLog('OVERLAPPING ROOMS', overlappingRooms, 3);
|
2024-12-19 17:57:10 +00:00
|
|
|
|
|
|
|
|
|
|
const lockedRooms = overlappingRooms
|
|
|
|
|
|
.filter(r => r.locked)
|
|
|
|
|
|
.sort((a, b) => b.distance - a.distance);
|
|
|
|
|
|
|
2025-03-08 13:19:58 +00:00
|
|
|
|
debugLog('LOCKED ROOMS', lockedRooms, 3);
|
2024-12-19 17:57:10 +00:00
|
|
|
|
|
|
|
|
|
|
if (lockedRooms.length > 0) {
|
|
|
|
|
|
const targetRoom = lockedRooms[0];
|
|
|
|
|
|
const requirements = {
|
|
|
|
|
|
lockType: targetRoom.lockType,
|
|
|
|
|
|
requires: targetRoom.requires
|
|
|
|
|
|
};
|
2025-03-08 13:19:58 +00:00
|
|
|
|
debugLog('LOCK REQUIREMENTS', requirements, 2);
|
2024-12-19 17:57:10 +00:00
|
|
|
|
return requirements;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-08 13:19:58 +00:00
|
|
|
|
debugLog('NO LOCK REQUIREMENTS FOUND', null, 2);
|
2024-12-19 17:57:10 +00:00
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getLockRequirementsForItem(item) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
lockType: item.lockType || item.scenarioData?.lockType,
|
2024-12-19 18:57:26 +00:00
|
|
|
|
requires: item.requires || item.scenarioData?.requires,
|
|
|
|
|
|
isUnlockedButNotCollected: false
|
2024-12-19 17:57:10 +00:00
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-02-02 01:31:21 +00:00
|
|
|
|
function collectContainerContents(container) {
|
|
|
|
|
|
if (!container.scenarioData?.contents ||
|
|
|
|
|
|
!container.scenarioData?.isUnlockedButNotCollected) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
container.scenarioData.contents.forEach(item => {
|
|
|
|
|
|
const sprite = createInventorySprite(item);
|
|
|
|
|
|
if (sprite) {
|
|
|
|
|
|
addToInventory(sprite);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
container.scenarioData.isUnlockedButNotCollected = false;
|
2025-03-08 13:19:58 +00:00
|
|
|
|
gameAlert('You collected the items from the container.', 'success', 'Items Collected', 4000);
|
2025-02-18 21:29:02 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-08 16:52:05 +00:00
|
|
|
|
// Add a throttle mechanism for Bluetooth panel updates
|
|
|
|
|
|
let lastBluetoothPanelUpdate = 0;
|
|
|
|
|
|
const BLUETOOTH_UPDATE_THROTTLE = 500; // milliseconds
|
|
|
|
|
|
|
2025-02-18 17:01:17 +00:00
|
|
|
|
function checkBluetoothDevices() {
|
|
|
|
|
|
// Find scanner in inventory
|
|
|
|
|
|
const scanner = inventory.items.find(item =>
|
|
|
|
|
|
item.scenarioData?.type === "bluetooth_scanner"
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (!scanner) return;
|
|
|
|
|
|
|
2025-03-08 15:00:17 +00:00
|
|
|
|
// Show the Bluetooth toggle button if it's not already visible
|
|
|
|
|
|
const bluetoothToggle = document.getElementById('bluetooth-toggle');
|
|
|
|
|
|
if (bluetoothToggle.style.display === 'none') {
|
|
|
|
|
|
bluetoothToggle.style.display = 'flex';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Find all Bluetooth devices in the current room
|
2025-02-18 17:01:17 +00:00
|
|
|
|
if (!currentRoom || !rooms[currentRoom] || !rooms[currentRoom].objects) return;
|
|
|
|
|
|
|
2025-03-08 15:53:08 +00:00
|
|
|
|
// Keep track of devices detected in this scan
|
|
|
|
|
|
const detectedDevices = new Set();
|
2025-03-08 16:52:05 +00:00
|
|
|
|
let needsUpdate = false;
|
2025-03-08 15:53:08 +00:00
|
|
|
|
|
2025-02-18 17:01:17 +00:00
|
|
|
|
Object.values(rooms[currentRoom].objects).forEach(obj => {
|
2025-03-08 15:00:17 +00:00
|
|
|
|
if (obj.scenarioData?.lockType === "bluetooth") {
|
2025-02-18 17:01:17 +00:00
|
|
|
|
const distance = Phaser.Math.Distance.Between(
|
|
|
|
|
|
player.x, player.y,
|
|
|
|
|
|
obj.x, obj.y
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-03-08 15:53:08 +00:00
|
|
|
|
const deviceMac = obj.scenarioData?.mac || "Unknown";
|
|
|
|
|
|
|
2025-03-08 13:19:58 +00:00
|
|
|
|
if (distance <= BLUETOOTH_SCAN_RANGE) {
|
2025-03-08 15:53:08 +00:00
|
|
|
|
detectedDevices.add(deviceMac);
|
|
|
|
|
|
|
2025-03-08 13:19:58 +00:00
|
|
|
|
debugLog('BLUETOOTH DEVICE DETECTED', {
|
2025-03-08 15:00:17 +00:00
|
|
|
|
deviceName: obj.scenarioData?.name,
|
2025-03-08 15:53:08 +00:00
|
|
|
|
deviceMac: deviceMac,
|
2025-02-18 17:01:17 +00:00
|
|
|
|
distance: Math.round(distance),
|
2025-03-08 13:19:58 +00:00
|
|
|
|
range: BLUETOOTH_SCAN_RANGE
|
|
|
|
|
|
}, 2);
|
2025-02-18 17:01:17 +00:00
|
|
|
|
|
2025-03-08 15:00:17 +00:00
|
|
|
|
// Add to Bluetooth scanner panel
|
|
|
|
|
|
const deviceName = obj.scenarioData?.name || "Unknown Device";
|
2025-03-08 15:53:08 +00:00
|
|
|
|
const signalStrength = Math.max(0, Math.round(100 - (distance / BLUETOOTH_SCAN_RANGE * 100)));
|
|
|
|
|
|
const details = `Type: ${obj.scenarioData?.type || "Unknown"}\nDistance: ${Math.round(distance)} units\nSignal Strength: ${signalStrength}%`;
|
2025-03-08 15:00:17 +00:00
|
|
|
|
|
2025-03-08 15:53:08 +00:00
|
|
|
|
// Check if device already exists in our list
|
2025-03-08 15:00:17 +00:00
|
|
|
|
const existingDevice = bluetoothDevices.find(device => device.mac === deviceMac);
|
|
|
|
|
|
|
2025-03-08 15:53:08 +00:00
|
|
|
|
if (existingDevice) {
|
|
|
|
|
|
// Update existing device details with real-time data
|
2025-03-08 16:52:05 +00:00
|
|
|
|
const oldSignalStrength = existingDevice.signalStrength;
|
2025-03-08 15:53:08 +00:00
|
|
|
|
existingDevice.details = details;
|
2025-03-08 15:00:17 +00:00
|
|
|
|
existingDevice.lastSeen = new Date();
|
2025-03-08 15:53:08 +00:00
|
|
|
|
existingDevice.nearby = true;
|
|
|
|
|
|
existingDevice.signalStrength = signalStrength;
|
2025-03-08 16:52:05 +00:00
|
|
|
|
|
|
|
|
|
|
// Only mark for update if signal strength changed significantly
|
|
|
|
|
|
if (Math.abs(oldSignalStrength - signalStrength) > 5) {
|
|
|
|
|
|
needsUpdate = true;
|
|
|
|
|
|
}
|
2025-03-08 15:53:08 +00:00
|
|
|
|
} else {
|
|
|
|
|
|
// Add as new device if not already in our list
|
|
|
|
|
|
const newDevice = addBluetoothDevice(deviceName, deviceMac, details, true);
|
|
|
|
|
|
if (newDevice) {
|
|
|
|
|
|
newDevice.signalStrength = signalStrength;
|
|
|
|
|
|
gameAlert(`Bluetooth device detected: ${deviceName} (MAC: ${deviceMac})`, 'info', 'Bluetooth Scanner', 4000);
|
2025-03-08 16:52:05 +00:00
|
|
|
|
needsUpdate = true;
|
2025-03-08 15:53:08 +00:00
|
|
|
|
}
|
2025-03-08 15:00:17 +00:00
|
|
|
|
}
|
2025-02-18 17:01:17 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2025-03-08 15:53:08 +00:00
|
|
|
|
|
|
|
|
|
|
// Mark devices that weren't detected in this scan as not nearby
|
|
|
|
|
|
bluetoothDevices.forEach(device => {
|
|
|
|
|
|
if (device.nearby && !detectedDevices.has(device.mac)) {
|
|
|
|
|
|
device.nearby = false;
|
|
|
|
|
|
device.lastSeen = new Date();
|
2025-03-08 16:52:05 +00:00
|
|
|
|
needsUpdate = true;
|
2025-03-08 15:53:08 +00:00
|
|
|
|
}
|
|
|
|
|
|
});
|
2025-03-08 16:52:05 +00:00
|
|
|
|
|
|
|
|
|
|
// Only update the panel if needed and not too frequently
|
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
|
if (needsUpdate && now - lastBluetoothPanelUpdate > BLUETOOTH_UPDATE_THROTTLE) {
|
|
|
|
|
|
updateBluetoothPanel();
|
|
|
|
|
|
updateBluetoothCount();
|
|
|
|
|
|
lastBluetoothPanelUpdate = now;
|
|
|
|
|
|
}
|
2025-02-18 17:01:17 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-08 15:00:17 +00:00
|
|
|
|
function spoofBluetoothDevice(target) {
|
|
|
|
|
|
// Find spoofer in inventory
|
|
|
|
|
|
const spoofer = inventory.items.find(item =>
|
|
|
|
|
|
item.scenarioData?.type === "bluetooth_spoofer"
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (!spoofer) {
|
|
|
|
|
|
gameAlert("You need a Bluetooth spoofer to unlock this device.", 'warning', 'Bluetooth Spoofer Required', 4000);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-08 21:09:17 +00:00
|
|
|
|
// Check if target is in inventory or in the environment
|
|
|
|
|
|
const isInventoryItem = inventory.items.includes(target);
|
2025-03-08 15:00:17 +00:00
|
|
|
|
|
2025-03-08 21:09:17 +00:00
|
|
|
|
// If it's an environment object, check distance
|
|
|
|
|
|
if (!isInventoryItem) {
|
|
|
|
|
|
// Calculate distance between player and target device
|
|
|
|
|
|
const distance = Phaser.Math.Distance.Between(
|
|
|
|
|
|
player.x, player.y,
|
|
|
|
|
|
target.x, target.y
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
debugLog('BLUETOOTH SPOOF ATTEMPT', {
|
|
|
|
|
|
deviceName: target.scenarioData?.name,
|
|
|
|
|
|
deviceMac: target.scenarioData?.mac,
|
|
|
|
|
|
spooferMac: spoofer.scenarioData?.macPaired,
|
|
|
|
|
|
distance: Math.round(distance),
|
|
|
|
|
|
isInventoryItem: false
|
|
|
|
|
|
}, 2);
|
|
|
|
|
|
|
|
|
|
|
|
// Check if player is within range
|
|
|
|
|
|
if (distance > BLUETOOTH_SCAN_RANGE) {
|
|
|
|
|
|
gameAlert("Too far from device to establish Bluetooth connection.", 'error', 'Connection Failed', 3000);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Log attempt for inventory item
|
|
|
|
|
|
debugLog('BLUETOOTH SPOOF ATTEMPT', {
|
|
|
|
|
|
deviceName: target.scenarioData?.name,
|
|
|
|
|
|
deviceMac: target.scenarioData?.mac,
|
|
|
|
|
|
spooferMac: spoofer.scenarioData?.macPaired,
|
|
|
|
|
|
isInventoryItem: true,
|
|
|
|
|
|
distance: 0 // Inventory items are always at distance 0
|
|
|
|
|
|
}, 2);
|
|
|
|
|
|
}
|
2025-03-08 15:00:17 +00:00
|
|
|
|
|
2025-03-08 21:09:17 +00:00
|
|
|
|
// Normalize MAC addresses for comparison
|
|
|
|
|
|
const targetMac = target.scenarioData?.mac?.toLowerCase() || "";
|
|
|
|
|
|
const spooferMac = spoofer.scenarioData?.macPaired?.toLowerCase() || "";
|
|
|
|
|
|
|
|
|
|
|
|
// Check if the spoofer has the correct MAC address
|
|
|
|
|
|
if (spooferMac && targetMac && spooferMac === targetMac) {
|
|
|
|
|
|
debugLog('BLUETOOTH SPOOF SUCCESS', {
|
|
|
|
|
|
deviceName: target.scenarioData?.name,
|
|
|
|
|
|
mac: target.scenarioData?.mac,
|
|
|
|
|
|
isInventoryItem: isInventoryItem
|
|
|
|
|
|
}, 1);
|
|
|
|
|
|
|
|
|
|
|
|
// Unlock the device
|
|
|
|
|
|
target.scenarioData.locked = false;
|
|
|
|
|
|
|
|
|
|
|
|
// If it's an inventory item, update its appearance if needed
|
|
|
|
|
|
if (isInventoryItem) {
|
|
|
|
|
|
// Update the item's texture if it has an unlocked version
|
|
|
|
|
|
if (target.scenarioData?.unlockedTexture) {
|
|
|
|
|
|
target.setTexture(target.scenarioData.unlockedTexture);
|
|
|
|
|
|
}
|
2025-03-08 15:00:17 +00:00
|
|
|
|
|
2025-03-08 21:09:17 +00:00
|
|
|
|
// If the item has contents, allow them to be collected
|
|
|
|
|
|
if (target.scenarioData?.contents) {
|
|
|
|
|
|
target.scenarioData.isUnlockedButNotCollected = true;
|
|
|
|
|
|
collectContainerContents(target);
|
|
|
|
|
|
|
|
|
|
|
|
// Remove the device from the bluetoothDevices array
|
|
|
|
|
|
const deviceIndex = bluetoothDevices.findIndex(device =>
|
|
|
|
|
|
device.mac.toLowerCase() === targetMac && device.inInventory
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (deviceIndex !== -1) {
|
|
|
|
|
|
bluetoothDevices.splice(deviceIndex, 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-03-08 15:00:17 +00:00
|
|
|
|
}
|
2025-03-08 21:09:17 +00:00
|
|
|
|
|
|
|
|
|
|
gameAlert("Bluetooth connection spoofed. Device unlocked.", 'success', 'Spoofing Successful', 4000);
|
|
|
|
|
|
|
|
|
|
|
|
// Update the Bluetooth panel to reflect the unlocked state
|
|
|
|
|
|
updateBluetoothPanel();
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
2025-03-08 15:00:17 +00:00
|
|
|
|
} else {
|
2025-03-08 21:09:17 +00:00
|
|
|
|
gameAlert("Bluetooth spoofer MAC address doesn't match the target device.", 'error', 'Spoofing Failed', 4000);
|
2025-03-08 15:00:17 +00:00
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-08 13:19:58 +00:00
|
|
|
|
// Add helper function to generate fingerprint data
|
|
|
|
|
|
function generateFingerprintData(item) {
|
|
|
|
|
|
// In a real implementation, this would generate unique fingerprint patterns
|
|
|
|
|
|
// For now, we'll just create a unique identifier
|
|
|
|
|
|
return `fp_${item.scenarioData.fingerprintOwner}_${Date.now()}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Add helper function to check if player has required collection tools
|
|
|
|
|
|
function hasItemInInventory(itemType) {
|
|
|
|
|
|
return inventory.items.some(item =>
|
|
|
|
|
|
item && item.scenarioData &&
|
|
|
|
|
|
item.scenarioData.type === itemType
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Add this function after the other utility functions
|
|
|
|
|
|
function handleBiometricScan(scanner, player) {
|
|
|
|
|
|
const scannerId = scanner.scenarioData.id || scanner.name;
|
|
|
|
|
|
|
|
|
|
|
|
// Check if scanner is locked out
|
|
|
|
|
|
if (scannerState.lockoutTimers[scannerId] &&
|
|
|
|
|
|
Date.now() < scannerState.lockoutTimers[scannerId]) {
|
|
|
|
|
|
const remainingTime = Math.ceil((scannerState.lockoutTimers[scannerId] - Date.now()) / 1000);
|
|
|
|
|
|
gameAlert(`Scanner locked out. Try again in ${remainingTime} seconds.`, 'error', 'Scanner Locked', 4000);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!scanner.scenarioData?.biometricType === 'fingerprint') {
|
|
|
|
|
|
debugLog('SCANNER TYPE ERROR - FAIL', scanner.scenarioData, 2);
|
|
|
|
|
|
gameAlert('Invalid scanner type', 'error', 'Scanner Error', 3000);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Check if player has valid fingerprint sample
|
|
|
|
|
|
const validSample = gameState.biometricSamples.find(sample =>
|
|
|
|
|
|
sample.type === 'fingerprint' &&
|
|
|
|
|
|
scanner.scenarioData.acceptedSamples.includes(sample.owner)
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (!validSample) {
|
|
|
|
|
|
handleScannerFailure(scannerId);
|
|
|
|
|
|
gameAlert("No valid fingerprint sample found.", 'error', 'Scan Failed', 4000);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Check sample quality
|
|
|
|
|
|
const qualityThreshold = 0.7;
|
|
|
|
|
|
if (validSample.quality < qualityThreshold) {
|
|
|
|
|
|
handleScannerFailure(scannerId);
|
|
|
|
|
|
gameAlert("Fingerprint sample quality too poor for scanning.", 'error', 'Scan Failed', 4000);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Success case - reset failed attempts
|
|
|
|
|
|
scannerState.failedAttempts[scannerId] = 0;
|
|
|
|
|
|
gameAlert("Biometric scan successful!", 'success', 'Scan Successful', 4000);
|
|
|
|
|
|
|
|
|
|
|
|
// Add visual feedback
|
|
|
|
|
|
const successEffect = scanner.scene.add.circle(
|
|
|
|
|
|
scanner.x,
|
|
|
|
|
|
scanner.y,
|
|
|
|
|
|
32,
|
|
|
|
|
|
0x00ff00,
|
|
|
|
|
|
0.5
|
|
|
|
|
|
);
|
|
|
|
|
|
scanner.scene.tweens.add({
|
|
|
|
|
|
targets: successEffect,
|
|
|
|
|
|
alpha: 0,
|
|
|
|
|
|
scale: 2,
|
|
|
|
|
|
duration: 1000,
|
|
|
|
|
|
onComplete: () => successEffect.destroy()
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// If the scanner is protecting something, unlock it
|
|
|
|
|
|
if (scanner.scenarioData.unlocks) {
|
|
|
|
|
|
const targetObject = rooms[currentRoom].objects[scanner.scenarioData.unlocks];
|
|
|
|
|
|
if (targetObject) {
|
|
|
|
|
|
targetObject.scenarioData.locked = false;
|
|
|
|
|
|
targetObject.scenarioData.isUnlockedButNotCollected = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Add this new function to handle scanner failures
|
|
|
|
|
|
function handleScannerFailure(scannerId) {
|
|
|
|
|
|
// Initialize failed attempts if not exists
|
|
|
|
|
|
if (!scannerState.failedAttempts[scannerId]) {
|
|
|
|
|
|
scannerState.failedAttempts[scannerId] = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Increment failed attempts
|
|
|
|
|
|
scannerState.failedAttempts[scannerId]++;
|
|
|
|
|
|
|
|
|
|
|
|
// Check if we should lockout
|
|
|
|
|
|
if (scannerState.failedAttempts[scannerId] >= MAX_FAILED_ATTEMPTS) {
|
|
|
|
|
|
scannerState.lockoutTimers[scannerId] = Date.now() + SCANNER_LOCKOUT_TIME;
|
|
|
|
|
|
gameAlert(`Too many failed attempts. Scanner locked for ${SCANNER_LOCKOUT_TIME/1000} seconds.`, 'error', 'Scanner Locked', 5000);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const remainingAttempts = MAX_FAILED_ATTEMPTS - scannerState.failedAttempts[scannerId];
|
|
|
|
|
|
gameAlert(`Scan failed. ${remainingAttempts} attempts remaining before lockout.`, 'warning', 'Scan Failed', 4000);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Modify collectFingerprint to include visual feedback
|
|
|
|
|
|
function collectFingerprint(item) {
|
|
|
|
|
|
if (!item.scenarioData?.hasFingerprint) {
|
|
|
|
|
|
gameAlert("No fingerprints found on this surface.", 'info', 'No Fingerprints', 3000);
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Check if player has required items
|
|
|
|
|
|
if (!hasItemInInventory('fingerprint_kit')) {
|
|
|
|
|
|
gameAlert("You need a fingerprint kit to collect samples!", 'warning', 'Missing Equipment', 4000);
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Start the dusting minigame
|
|
|
|
|
|
startDustingMinigame(item);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Add this function to check for object interactions
|
|
|
|
|
|
function checkObjectInteractions() {
|
|
|
|
|
|
// Skip if not enough time has passed since last check
|
|
|
|
|
|
const currentTime = performance.now();
|
|
|
|
|
|
if (this.lastInteractionCheck &&
|
|
|
|
|
|
currentTime - this.lastInteractionCheck < INTERACTION_CHECK_INTERVAL) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.lastInteractionCheck = currentTime;
|
|
|
|
|
|
|
|
|
|
|
|
const playerRoom = currentPlayerRoom;
|
|
|
|
|
|
if (!playerRoom || !rooms[playerRoom].objects) return;
|
|
|
|
|
|
|
|
|
|
|
|
// Cache player position
|
|
|
|
|
|
const px = player.x;
|
|
|
|
|
|
const py = player.y;
|
|
|
|
|
|
|
|
|
|
|
|
// Get only objects within viewport bounds plus some margin
|
|
|
|
|
|
const camera = this.cameras.main;
|
|
|
|
|
|
const margin = INTERACTION_RANGE;
|
|
|
|
|
|
const viewBounds = {
|
|
|
|
|
|
left: camera.scrollX - margin,
|
|
|
|
|
|
right: camera.scrollX + camera.width + margin,
|
|
|
|
|
|
top: camera.scrollY - margin,
|
|
|
|
|
|
bottom: camera.scrollY + camera.height + margin
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
Object.values(rooms[playerRoom].objects).forEach(obj => {
|
|
|
|
|
|
// Skip inactive objects and those outside viewport
|
|
|
|
|
|
if (!obj.active ||
|
|
|
|
|
|
obj.x < viewBounds.left ||
|
|
|
|
|
|
obj.x > viewBounds.right ||
|
|
|
|
|
|
obj.y < viewBounds.top ||
|
|
|
|
|
|
obj.y > viewBounds.bottom) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Use squared distance for performance
|
|
|
|
|
|
const dx = px - obj.x;
|
|
|
|
|
|
const dy = py - obj.y;
|
|
|
|
|
|
const distanceSq = dx * dx + dy * dy;
|
|
|
|
|
|
|
|
|
|
|
|
if (distanceSq <= INTERACTION_RANGE_SQ) {
|
|
|
|
|
|
if (!obj.isHighlighted) {
|
|
|
|
|
|
obj.isHighlighted = true;
|
|
|
|
|
|
obj.setTint(0xdddddd); // Simple highlight without tween
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (obj.isHighlighted) {
|
|
|
|
|
|
obj.isHighlighted = false;
|
|
|
|
|
|
obj.clearTint();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Add this function to setup scanner interactions
|
|
|
|
|
|
function setupScannerInteractions() {
|
|
|
|
|
|
Object.values(rooms).forEach(room => {
|
|
|
|
|
|
if (!room.objects) return;
|
|
|
|
|
|
|
|
|
|
|
|
Object.values(room.objects).forEach(obj => {
|
|
|
|
|
|
if (obj.scenarioData?.biometricType === 'fingerprint') {
|
|
|
|
|
|
// Add visual indicator for scanner
|
|
|
|
|
|
const indicator = obj.scene.add.circle(
|
|
|
|
|
|
obj.x,
|
|
|
|
|
|
obj.y,
|
|
|
|
|
|
20,
|
|
|
|
|
|
0x0000ff,
|
|
|
|
|
|
0.3
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// Add pulsing effect
|
|
|
|
|
|
obj.scene.tweens.add({
|
|
|
|
|
|
targets: indicator,
|
|
|
|
|
|
alpha: { from: 0.3, to: 0.1 },
|
|
|
|
|
|
scale: { from: 1, to: 1.2 },
|
|
|
|
|
|
duration: 1000,
|
|
|
|
|
|
yoyo: true,
|
|
|
|
|
|
repeat: -1
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Store reference to indicator
|
|
|
|
|
|
obj.scannerIndicator = indicator;
|
|
|
|
|
|
|
|
|
|
|
|
// Add hover effect
|
|
|
|
|
|
obj.on('pointerover', function() {
|
|
|
|
|
|
if (this.scannerIndicator) {
|
|
|
|
|
|
this.scannerIndicator.setAlpha(0.5);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
obj.on('pointerout', function() {
|
|
|
|
|
|
if (this.scannerIndicator) {
|
|
|
|
|
|
this.scannerIndicator.setAlpha(0.3);
|
|
|
|
|
|
}
|
2025-02-18 21:29:02 +00:00
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-03-08 13:19:58 +00:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Add this to your scene initialization
|
|
|
|
|
|
function initializeBiometricSystem() {
|
|
|
|
|
|
// Initialize gameState if not exists
|
|
|
|
|
|
if (!window.gameState) {
|
|
|
|
|
|
window.gameState = {
|
|
|
|
|
|
biometricSamples: []
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Initialize scanner state
|
|
|
|
|
|
if (!window.scannerState) {
|
|
|
|
|
|
window.scannerState = {
|
|
|
|
|
|
failedAttempts: {},
|
|
|
|
|
|
lockoutTimers: {}
|
2025-02-18 21:29:02 +00:00
|
|
|
|
};
|
|
|
|
|
|
}
|
2025-03-08 13:19:58 +00:00
|
|
|
|
|
|
|
|
|
|
// Setup scanner visuals and interactions
|
|
|
|
|
|
setupScannerInteractions();
|
|
|
|
|
|
|
|
|
|
|
|
// Add periodic interaction checks
|
|
|
|
|
|
this.time.addEvent({
|
|
|
|
|
|
delay: 100, // Check every 100ms
|
|
|
|
|
|
callback: checkObjectInteractions,
|
|
|
|
|
|
callbackScope: this,
|
|
|
|
|
|
loop: true
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Add function to create and manage the samples UI
|
|
|
|
|
|
function createSamplesUI() {
|
|
|
|
|
|
// Create container for samples UI if it doesn't exist
|
|
|
|
|
|
let samplesUI = document.getElementById('biometric-samples-ui');
|
|
|
|
|
|
if (!samplesUI) {
|
|
|
|
|
|
samplesUI = document.createElement('div');
|
|
|
|
|
|
samplesUI.id = 'biometric-samples-ui';
|
|
|
|
|
|
|
|
|
|
|
|
// Apply styles
|
|
|
|
|
|
Object.assign(samplesUI.style, SAMPLE_UI_STYLES);
|
|
|
|
|
|
|
|
|
|
|
|
// Add close button
|
|
|
|
|
|
const closeButton = document.createElement('button');
|
|
|
|
|
|
closeButton.textContent = '×';
|
|
|
|
|
|
closeButton.style.cssText = `
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
right: 10px;
|
|
|
|
|
|
top: 10px;
|
|
|
|
|
|
background: none;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
`;
|
|
|
|
|
|
closeButton.onclick = () => hideSamplesUI();
|
|
|
|
|
|
samplesUI.appendChild(closeButton);
|
|
|
|
|
|
|
|
|
|
|
|
document.body.appendChild(samplesUI);
|
|
|
|
|
|
}
|
|
|
|
|
|
return samplesUI;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Function to show samples UI
|
|
|
|
|
|
function showSamplesUI() {
|
|
|
|
|
|
const samplesUI = createSamplesUI();
|
|
|
|
|
|
samplesUI.style.display = 'block';
|
|
|
|
|
|
|
|
|
|
|
|
// Clear existing content
|
|
|
|
|
|
while (samplesUI.children.length > 1) { // Keep close button
|
|
|
|
|
|
samplesUI.removeChild(samplesUI.lastChild);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Add title
|
|
|
|
|
|
const title = document.createElement('h2');
|
|
|
|
|
|
title.textContent = 'Collected Biometric Samples';
|
|
|
|
|
|
title.style.cssText = 'margin-top: 0; color: #fff; text-align: center;';
|
|
|
|
|
|
samplesUI.appendChild(title);
|
|
|
|
|
|
|
|
|
|
|
|
// Add samples
|
|
|
|
|
|
if (!gameState.biometricSamples || gameState.biometricSamples.length === 0) {
|
|
|
|
|
|
const noSamples = document.createElement('p');
|
|
|
|
|
|
noSamples.textContent = 'No samples collected yet.';
|
|
|
|
|
|
noSamples.style.textAlign = 'center';
|
|
|
|
|
|
samplesUI.appendChild(noSamples);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
gameState.biometricSamples.forEach(sample => {
|
|
|
|
|
|
const sampleElement = document.createElement('div');
|
|
|
|
|
|
sampleElement.style.cssText = `
|
|
|
|
|
|
margin: 10px 0;
|
|
|
|
|
|
padding: 10px;
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.1);
|
|
|
|
|
|
border-radius: 5px;
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
const qualityPercentage = Math.round(sample.quality * 100);
|
|
|
|
|
|
sampleElement.innerHTML = `
|
|
|
|
|
|
<strong>Type:</strong> ${sample.type}<br>
|
|
|
|
|
|
<strong>Owner:</strong> ${sample.owner}<br>
|
|
|
|
|
|
<strong>Quality:</strong> ${qualityPercentage}%<br>
|
|
|
|
|
|
<strong>ID:</strong> ${sample.id}<br>
|
|
|
|
|
|
${sample.isSpoofed ? '<strong style="color: #ff9900;">SPOOFED SAMPLE</strong><br>' : ''}
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
// Add quality bar
|
|
|
|
|
|
const qualityBar = document.createElement('div');
|
|
|
|
|
|
qualityBar.style.cssText = `
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 5px;
|
|
|
|
|
|
background: #333;
|
|
|
|
|
|
margin-top: 5px;
|
|
|
|
|
|
border-radius: 2px;
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
const qualityFill = document.createElement('div');
|
|
|
|
|
|
qualityFill.style.cssText = `
|
|
|
|
|
|
width: ${qualityPercentage}%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
background: ${getQualityColor(sample.quality)};
|
|
|
|
|
|
border-radius: 2px;
|
|
|
|
|
|
transition: width 0.3s ease;
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
qualityBar.appendChild(qualityFill);
|
|
|
|
|
|
sampleElement.appendChild(qualityBar);
|
|
|
|
|
|
|
|
|
|
|
|
// Add spoof button if not already spoofed
|
|
|
|
|
|
if (!sample.isSpoofed && hasItemInInventory('spoofing_kit')) {
|
|
|
|
|
|
const spoofButton = document.createElement('button');
|
|
|
|
|
|
spoofButton.textContent = 'Create Spoof';
|
|
|
|
|
|
spoofButton.style.cssText = `
|
|
|
|
|
|
margin-top: 10px;
|
|
|
|
|
|
padding: 5px 10px;
|
|
|
|
|
|
background: #444;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
`;
|
|
|
|
|
|
spoofButton.onclick = async () => {
|
|
|
|
|
|
spoofButton.disabled = true;
|
|
|
|
|
|
spoofButton.textContent = 'Creating spoof...';
|
|
|
|
|
|
|
|
|
|
|
|
// Add progress bar
|
|
|
|
|
|
const progressBar = document.createElement('div');
|
|
|
|
|
|
progressBar.style.cssText = `
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 2px;
|
|
|
|
|
|
background: #333;
|
|
|
|
|
|
margin-top: 5px;
|
|
|
|
|
|
`;
|
|
|
|
|
|
const progress = document.createElement('div');
|
|
|
|
|
|
progress.style.cssText = `
|
|
|
|
|
|
width: 0%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
background: #ff9900;
|
|
|
|
|
|
transition: width 0.1s linear;
|
|
|
|
|
|
`;
|
|
|
|
|
|
progressBar.appendChild(progress);
|
|
|
|
|
|
sampleElement.appendChild(progressBar);
|
|
|
|
|
|
|
|
|
|
|
|
// Animate progress
|
|
|
|
|
|
let currentProgress = 0;
|
|
|
|
|
|
const interval = setInterval(() => {
|
|
|
|
|
|
currentProgress += 2;
|
|
|
|
|
|
progress.style.width = `${currentProgress}%`;
|
|
|
|
|
|
}, SPOOFING_TIME / 50);
|
|
|
|
|
|
|
|
|
|
|
|
// Create spoof after delay
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
clearInterval(interval);
|
|
|
|
|
|
const spoofedSample = createSpoofedSample(sample);
|
|
|
|
|
|
if (spoofedSample) {
|
|
|
|
|
|
gameState.biometricSamples.push(spoofedSample);
|
|
|
|
|
|
showSamplesUI(); // Refresh UI
|
|
|
|
|
|
}
|
|
|
|
|
|
}, SPOOFING_TIME);
|
|
|
|
|
|
};
|
|
|
|
|
|
sampleElement.appendChild(spoofButton);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
samplesUI.appendChild(sampleElement);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Helper function to hide samples UI
|
|
|
|
|
|
function hideSamplesUI() {
|
|
|
|
|
|
const samplesUI = document.getElementById('biometric-samples-ui');
|
|
|
|
|
|
if (samplesUI) {
|
|
|
|
|
|
samplesUI.style.display = 'none';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Helper function to get color based on quality
|
|
|
|
|
|
function getQualityColor(quality) {
|
|
|
|
|
|
if (quality >= 0.8) return '#00ff00';
|
|
|
|
|
|
if (quality >= 0.6) return '#ffff00';
|
|
|
|
|
|
return '#ff0000';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Add keyboard shortcut to view samples (press 'B')
|
|
|
|
|
|
function setupSamplesUIControls() {
|
|
|
|
|
|
document.addEventListener('keydown', (event) => {
|
|
|
|
|
|
if (event.key.toLowerCase() === 'b') {
|
|
|
|
|
|
showSamplesUI();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (event.key === 'Escape') {
|
|
|
|
|
|
hideSamplesUI();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Add this to your scene's create function
|
|
|
|
|
|
function initializeSamplesUI() {
|
|
|
|
|
|
createSamplesUI();
|
|
|
|
|
|
setupSamplesUIControls();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function generateFingerprintData(sample) {
|
|
|
|
|
|
// For spoofed samples, we generate from the original sample data
|
|
|
|
|
|
if (sample.data) {
|
|
|
|
|
|
return `spoofed_${sample.data}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// For original samples from items, we use the item's data
|
|
|
|
|
|
if (sample.scenarioData?.fingerprintOwner) {
|
|
|
|
|
|
return `fp_${sample.scenarioData.fingerprintOwner}_${Date.now()}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Fallback unique identifier
|
|
|
|
|
|
return `fp_unknown_${Date.now()}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Add spoofing functionality
|
|
|
|
|
|
function createSpoofedSample(originalSample) {
|
|
|
|
|
|
if (!originalSample) {
|
|
|
|
|
|
alert("No sample to spoof from!");
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Check if player has required items
|
|
|
|
|
|
const hasSpoofingKit = hasItemInInventory('spoofing_kit');
|
|
|
|
|
|
if (!hasSpoofingKit) {
|
|
|
|
|
|
alert("You need a spoofing kit to create fake fingerprints!");
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Create spoofed sample with slightly degraded quality
|
|
|
|
|
|
const spoofedSample = {
|
|
|
|
|
|
id: `spoofed_${originalSample.owner}_${Date.now()}`,
|
|
|
|
|
|
type: originalSample.type,
|
|
|
|
|
|
owner: originalSample.owner,
|
|
|
|
|
|
quality: originalSample.quality * SPOOF_QUALITY_MULTIPLIER,
|
|
|
|
|
|
data: generateFingerprintData(originalSample),
|
|
|
|
|
|
isSpoofed: true
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return spoofedSample;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Add dusting minigame
|
|
|
|
|
|
function startDustingMinigame(item) {
|
|
|
|
|
|
// Create iframe container
|
|
|
|
|
|
const iframe = document.createElement('div');
|
|
|
|
|
|
iframe.style.cssText = `
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
top: 50%;
|
|
|
|
|
|
left: 50%;
|
|
|
|
|
|
transform: translate(-50%, -50%);
|
|
|
|
|
|
width: 60%;
|
|
|
|
|
|
height: 60%;
|
|
|
|
|
|
background: rgba(0, 0, 0, 0.9);
|
|
|
|
|
|
border: 1px solid #444;
|
|
|
|
|
|
z-index: 1000;
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
border-radius: 5px;
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
// Create game container
|
|
|
|
|
|
const gameContainer = document.createElement('div');
|
|
|
|
|
|
gameContainer.style.cssText = `
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: calc(100% - 60px);
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(20, minmax(0, 1fr));
|
|
|
|
|
|
grid-template-rows: repeat(20, minmax(0, 1fr));
|
|
|
|
|
|
gap: 1px;
|
|
|
|
|
|
background: #222;
|
|
|
|
|
|
padding: 10px;
|
|
|
|
|
|
margin-top: 40px;
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
// Add instructions
|
|
|
|
|
|
const instructions = document.createElement('div');
|
|
|
|
|
|
instructions.innerHTML = `
|
|
|
|
|
|
<h3 style="margin: 0; color: #fff; text-align: center;">Fingerprint Dusting</h3>
|
|
|
|
|
|
<p style="margin: 5px 0; color: #ccc; text-align: center; font-size: 14px;">
|
|
|
|
|
|
Drag to dust the surface and reveal fingerprints.<br>
|
|
|
|
|
|
🔍 Gray = Light dusting<br>
|
|
|
|
|
|
🟢 Green = Fingerprint found!<br>
|
|
|
|
|
|
⚠️ White = Over-dusted (avoid this)<br>
|
|
|
|
|
|
Find all fingerprints with minimal over-dusting.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
`;
|
|
|
|
|
|
instructions.style.cssText = `
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 10px;
|
|
|
|
|
|
left: 50%;
|
|
|
|
|
|
transform: translateX(-50%);
|
|
|
|
|
|
width: 90%;
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
// Add progress display
|
|
|
|
|
|
const progressText = document.createElement('div');
|
|
|
|
|
|
progressText.style.cssText = `
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
bottom: 10px;
|
|
|
|
|
|
left: 50%;
|
|
|
|
|
|
transform: translateX(-50%);
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
// Generate fingerprint pattern
|
|
|
|
|
|
const gridSize = 20;
|
|
|
|
|
|
const fingerprintCells = new Set();
|
|
|
|
|
|
const centerX = Math.floor(gridSize / 2);
|
|
|
|
|
|
const centerY = Math.floor(gridSize / 2);
|
|
|
|
|
|
|
|
|
|
|
|
// Create a larger pattern (about 50 cells) spread around the center
|
|
|
|
|
|
for (let i = 0; i < 50; i++) {
|
|
|
|
|
|
const x = centerX + Math.floor(Math.random() * 10 - 5); // Increased spread
|
|
|
|
|
|
const y = centerY + Math.floor(Math.random() * 10 - 5); // Increased spread
|
|
|
|
|
|
if (x >= 0 && x < gridSize && y >= 0 && y < gridSize) {
|
|
|
|
|
|
fingerprintCells.add(`${x},${y}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// If we didn't get enough cells, add more until we reach target
|
|
|
|
|
|
while (fingerprintCells.size < 50) {
|
|
|
|
|
|
const x = centerX + Math.floor(Math.random() * 12 - 6);
|
|
|
|
|
|
const y = centerY + Math.floor(Math.random() * 12 - 6);
|
|
|
|
|
|
if (x >= 0 && x < gridSize && y >= 0 && y < gridSize) {
|
|
|
|
|
|
fingerprintCells.add(`${x},${y}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Track progress
|
|
|
|
|
|
let revealedPrints = 0;
|
|
|
|
|
|
let totalPrints = fingerprintCells.size;
|
|
|
|
|
|
let overDusted = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// Create grid cells
|
|
|
|
|
|
for (let y = 0; y < gridSize; y++) {
|
|
|
|
|
|
for (let x = 0; x < gridSize; x++) {
|
|
|
|
|
|
const cell = document.createElement('div');
|
|
|
|
|
|
cell.style.cssText = `
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
background: black;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
`;
|
|
|
|
|
|
cell.dataset.x = x;
|
|
|
|
|
|
cell.dataset.y = y;
|
|
|
|
|
|
cell.dataset.dustLevel = '0';
|
|
|
|
|
|
cell.dataset.hasFingerprint = fingerprintCells.has(`${x},${y}`);
|
|
|
|
|
|
|
|
|
|
|
|
gameContainer.appendChild(cell);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Add dragging interaction at container level
|
|
|
|
|
|
let isDragging = false;
|
|
|
|
|
|
let lastDustTime = {}; // Track last dust time for each cell
|
|
|
|
|
|
|
|
|
|
|
|
gameContainer.addEventListener('mousedown', () => isDragging = true);
|
|
|
|
|
|
gameContainer.addEventListener('mouseup', () => isDragging = false);
|
|
|
|
|
|
gameContainer.addEventListener('mouseleave', () => isDragging = false);
|
|
|
|
|
|
gameContainer.addEventListener('mousemove', (e) => {
|
|
|
|
|
|
if (!isDragging) return;
|
|
|
|
|
|
|
|
|
|
|
|
// Get the cell element under the cursor
|
|
|
|
|
|
const cell = document.elementFromPoint(e.clientX, e.clientY);
|
|
|
|
|
|
if (cell && cell.dataset.dustLevel !== undefined) {
|
|
|
|
|
|
const cellId = `${cell.dataset.x},${cell.dataset.y}`;
|
|
|
|
|
|
const currentTime = Date.now();
|
|
|
|
|
|
const dustLevel = parseInt(cell.dataset.dustLevel);
|
|
|
|
|
|
|
|
|
|
|
|
// Only allow dusting every 100ms for each cell
|
|
|
|
|
|
if (!lastDustTime[cellId] || currentTime - lastDustTime[cellId] > 100) {
|
|
|
|
|
|
if (dustLevel < 3) {
|
|
|
|
|
|
// Increment dust level with 33% chance after level 1
|
|
|
|
|
|
if (dustLevel < 1 || Math.random() < 0.33) {
|
|
|
|
|
|
cell.dataset.dustLevel = (dustLevel + 1).toString();
|
|
|
|
|
|
updateCellColor(cell);
|
|
|
|
|
|
checkProgress();
|
|
|
|
|
|
}
|
|
|
|
|
|
lastDustTime[cellId] = currentTime;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
function updateCellColor(cell) {
|
|
|
|
|
|
const dustLevel = parseInt(cell.dataset.dustLevel);
|
|
|
|
|
|
const hasFingerprint = cell.dataset.hasFingerprint === 'true';
|
|
|
|
|
|
|
|
|
|
|
|
if (dustLevel === 0) {
|
|
|
|
|
|
cell.style.background = 'black';
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (dustLevel === 1) {
|
|
|
|
|
|
cell.style.background = '#444';
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (dustLevel === 2) {
|
|
|
|
|
|
cell.style.background = hasFingerprint ? '#0f0' : '#888';
|
|
|
|
|
|
}
|
|
|
|
|
|
else {
|
|
|
|
|
|
cell.style.background = '#ccc';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function checkProgress() {
|
|
|
|
|
|
revealedPrints = 0;
|
|
|
|
|
|
overDusted = 0;
|
|
|
|
|
|
|
|
|
|
|
|
gameContainer.childNodes.forEach(cell => {
|
|
|
|
|
|
const dustLevel = parseInt(cell.dataset.dustLevel);
|
|
|
|
|
|
const hasFingerprint = cell.dataset.hasFingerprint === 'true';
|
|
|
|
|
|
|
|
|
|
|
|
if (hasFingerprint && dustLevel === 2) revealedPrints++;
|
|
|
|
|
|
if (dustLevel === 3) overDusted++;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const requiredPrints = Math.ceil(totalPrints * 0.4); // 40% requirement
|
|
|
|
|
|
progressText.innerHTML = `
|
|
|
|
|
|
<div style="color: #0f0;">Found: ${revealedPrints}/${requiredPrints} required prints</div>
|
|
|
|
|
|
<div style="color: ${overDusted > 24 ? '#f00' : '#fff'}">
|
|
|
|
|
|
Over-dusted: ${overDusted}/25 max
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
// Check fail condition first
|
|
|
|
|
|
if (overDusted >= 25) {
|
|
|
|
|
|
// Show failure message
|
|
|
|
|
|
const failureMessage = document.createElement('div');
|
|
|
|
|
|
failureMessage.style.cssText = `
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 50%;
|
|
|
|
|
|
left: 50%;
|
|
|
|
|
|
transform: translate(-50%, -50%);
|
|
|
|
|
|
background: rgba(0, 0, 0, 0.9);
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
border-radius: 5px;
|
|
|
|
|
|
color: #f00;
|
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
z-index: 1001;
|
|
|
|
|
|
`;
|
|
|
|
|
|
failureMessage.textContent = "Too many over-dusted areas!";
|
|
|
|
|
|
iframe.appendChild(failureMessage);
|
|
|
|
|
|
|
|
|
|
|
|
// Disable further interaction
|
|
|
|
|
|
isDragging = false;
|
|
|
|
|
|
gameContainer.style.pointerEvents = 'none';
|
|
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
document.body.removeChild(iframe);
|
|
|
|
|
|
scene.input.mouse.enabled = true;
|
|
|
|
|
|
}, 1500);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Check win condition (existing code)
|
|
|
|
|
|
if (revealedPrints >= requiredPrints && overDusted < 25) {
|
|
|
|
|
|
// Show success message
|
|
|
|
|
|
const successMessage = document.createElement('div');
|
|
|
|
|
|
successMessage.style.cssText = `
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 50%;
|
|
|
|
|
|
left: 50%;
|
|
|
|
|
|
transform: translate(-50%, -50%);
|
|
|
|
|
|
background: rgba(0, 0, 0, 0.9);
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
border-radius: 5px;
|
|
|
|
|
|
color: #0f0;
|
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
z-index: 1001;
|
|
|
|
|
|
`;
|
|
|
|
|
|
successMessage.textContent = "Fingerprint successfully collected!";
|
|
|
|
|
|
iframe.appendChild(successMessage);
|
|
|
|
|
|
|
|
|
|
|
|
// Disable further interaction
|
|
|
|
|
|
isDragging = false;
|
|
|
|
|
|
gameContainer.style.pointerEvents = 'none';
|
|
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
// Add fingerprint to gameState
|
|
|
|
|
|
if (!gameState.biometricSamples) {
|
|
|
|
|
|
gameState.biometricSamples = [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const sample = {
|
|
|
|
|
|
id: generateFingerprintData(item),
|
|
|
|
|
|
type: 'fingerprint',
|
|
|
|
|
|
owner: item.scenarioData.fingerprintOwner,
|
|
|
|
|
|
quality: Math.random() * 0.3 + 0.7, // Random quality between 0.7 and 1.0
|
2025-03-08 22:03:24 +00:00
|
|
|
|
data: generateFingerprintData(item),
|
|
|
|
|
|
timestamp: Date.now()
|
2025-03-08 13:19:58 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
gameState.biometricSamples.push(sample);
|
|
|
|
|
|
|
|
|
|
|
|
// Remove the minigame
|
|
|
|
|
|
document.body.removeChild(iframe);
|
|
|
|
|
|
scene.input.mouse.enabled = true;
|
|
|
|
|
|
|
|
|
|
|
|
// Mark item as collected
|
|
|
|
|
|
if (item.scenarioData) {
|
|
|
|
|
|
item.scenarioData.hasFingerprint = false;
|
|
|
|
|
|
}
|
2025-03-08 22:03:24 +00:00
|
|
|
|
|
|
|
|
|
|
// Update the biometrics panel and count
|
|
|
|
|
|
updateBiometricsPanel();
|
|
|
|
|
|
updateBiometricsCount();
|
|
|
|
|
|
|
|
|
|
|
|
// Show notification
|
|
|
|
|
|
gameAlert(`Collected ${sample.owner}'s fingerprint sample`, 'success', 'Sample Acquired', 3000);
|
2025-03-08 13:19:58 +00:00
|
|
|
|
}, 1500);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Add close button
|
|
|
|
|
|
const closeButton = document.createElement('button');
|
|
|
|
|
|
closeButton.textContent = 'X';
|
|
|
|
|
|
closeButton.style.cssText = `
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
right: 10px;
|
|
|
|
|
|
top: 10px;
|
|
|
|
|
|
background: none;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
`;
|
|
|
|
|
|
closeButton.onclick = () => {
|
|
|
|
|
|
document.body.removeChild(iframe);
|
|
|
|
|
|
scene.input.mouse.enabled = true;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Assemble the interface
|
|
|
|
|
|
iframe.appendChild(closeButton);
|
|
|
|
|
|
iframe.appendChild(instructions);
|
|
|
|
|
|
iframe.appendChild(gameContainer);
|
|
|
|
|
|
iframe.appendChild(progressText);
|
|
|
|
|
|
document.body.appendChild(iframe);
|
|
|
|
|
|
|
|
|
|
|
|
// Disable game movement
|
|
|
|
|
|
const scene = item.scene;
|
|
|
|
|
|
scene.input.mouse.enabled = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function startLockpickingMinigame(lockable, currentScene, difficulty) {
|
|
|
|
|
|
// Create iframe container
|
|
|
|
|
|
const iframe = document.createElement('div');
|
|
|
|
|
|
iframe.style.cssText = `
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
top: 50%;
|
|
|
|
|
|
left: 50%;
|
|
|
|
|
|
transform: translate(-50%, -50%);
|
|
|
|
|
|
width: 60%;
|
|
|
|
|
|
height: 60%;
|
|
|
|
|
|
background: rgba(0, 0, 0, 0.9);
|
|
|
|
|
|
border: 1px solid #444;
|
|
|
|
|
|
z-index: 1000;
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
border-radius: 5px;
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
// Add instructions
|
|
|
|
|
|
const instructions = document.createElement('div');
|
|
|
|
|
|
instructions.innerHTML = `
|
|
|
|
|
|
<h3 style="margin: 0; color: #fff; text-align: center;">Lock Picking</h3>
|
|
|
|
|
|
<p style="margin: 5px 0; color: #ccc; text-align: center; font-size: 14px;">
|
|
|
|
|
|
Use spacebar or click to toggle tension levels. Each pin requires the right amount of tension.<br>
|
|
|
|
|
|
🔵 Blue = Pin moving<br>
|
|
|
|
|
|
🟢 Green = Pin set correctly<br>
|
|
|
|
|
|
🔴 Red = Over-pushed (reset)<br>
|
|
|
|
|
|
Set all pins in the correct order with the right tension without resetting.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
`;
|
|
|
|
|
|
instructions.style.cssText = `
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 10px;
|
|
|
|
|
|
left: 50%;
|
|
|
|
|
|
transform: translateX(-50%);
|
|
|
|
|
|
width: 90%;
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
// Create game container
|
|
|
|
|
|
const gameContainer = document.createElement('div');
|
|
|
|
|
|
gameContainer.style.cssText = `
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: calc(100% - 60px);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 20px;
|
|
|
|
|
|
margin-top: 40px;
|
|
|
|
|
|
background: #222;
|
|
|
|
|
|
padding: 10px;
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
// Add difficulty selection
|
|
|
|
|
|
const difficultySelect = document.createElement('select');
|
|
|
|
|
|
difficultySelect.style.cssText = `
|
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
padding: 5px;
|
|
|
|
|
|
background: #444;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
border: 1px solid #666;
|
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
|
`;
|
|
|
|
|
|
const difficulties = ['Easy - Pins Visible', 'Hard - Audio Only'];
|
|
|
|
|
|
difficulties.forEach(diff => {
|
|
|
|
|
|
const option = document.createElement('option');
|
|
|
|
|
|
option.value = diff;
|
|
|
|
|
|
option.textContent = diff;
|
|
|
|
|
|
difficultySelect.appendChild(option);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Add audio feedback
|
|
|
|
|
|
const lockSounds = {
|
|
|
|
|
|
click: null, // Basic pin movement sound
|
|
|
|
|
|
binding: null, // Sound when a pin is binding (correct tension)
|
|
|
|
|
|
set: null, // Sound when a pin is successfully set
|
|
|
|
|
|
reset: null, // Sound when pins are reset
|
|
|
|
|
|
wrong: null, // Sound when wrong tension or wrong pin
|
|
|
|
|
|
tension: null, // Sound when changing tension
|
|
|
|
|
|
success: null, // Sound when successfully picking the lock
|
|
|
|
|
|
overTension: null // Sound when over-tensioning the lock
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const initAudio = () => {
|
|
|
|
|
|
if (!lockSounds.click) {
|
|
|
|
|
|
// Basic click sound (when pressing a pin)
|
|
|
|
|
|
lockSounds.click = new Audio('assets/sounds/lockpick_click.mp3');
|
|
|
|
|
|
|
|
|
|
|
|
// Binding sound (when a pin is binding with correct tension)
|
|
|
|
|
|
lockSounds.binding = new Audio('assets/sounds/lockpick_binding.mp3');
|
|
|
|
|
|
|
|
|
|
|
|
// Set sound (when a pin is successfully set)
|
|
|
|
|
|
lockSounds.set = new Audio('assets/sounds/lockpick_set.mp3');
|
|
|
|
|
|
|
|
|
|
|
|
// Reset sound (when pins are reset)
|
|
|
|
|
|
lockSounds.reset = new Audio('assets/sounds/lockpick_reset.mp3');
|
|
|
|
|
|
|
|
|
|
|
|
// Wrong sound (when using wrong tension or pressing wrong pin)
|
|
|
|
|
|
lockSounds.wrong = new Audio('assets/sounds/lockpick_wrong.mp3');
|
|
|
|
|
|
|
|
|
|
|
|
// Tension sound (when changing tension levels)
|
|
|
|
|
|
lockSounds.tension = new Audio('assets/sounds/lockpick_tension.mp3');
|
|
|
|
|
|
|
|
|
|
|
|
// Success sound (when successfully picking the lock)
|
|
|
|
|
|
lockSounds.success = new Audio('assets/sounds/lockpick_success.mp3');
|
|
|
|
|
|
|
|
|
|
|
|
// Over-tension sound (when applying too much tension)
|
|
|
|
|
|
lockSounds.overTension = new Audio('assets/sounds/lockpick_overtension.mp3');
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Initialize audio on first interaction
|
|
|
|
|
|
gameContainer.addEventListener('mousedown', initAudio, { once: true });
|
|
|
|
|
|
|
|
|
|
|
|
// Add pin binding order and game state
|
|
|
|
|
|
const numPins = getPinCountForDifficulty(difficulty);
|
|
|
|
|
|
const bindingOrder = Array.from({length: numPins}, (_, i) => i)
|
|
|
|
|
|
.sort(() => Math.random() - 0.5);
|
|
|
|
|
|
|
|
|
|
|
|
const gameState = {
|
|
|
|
|
|
tensionLevel: 1, // Start with light tension instead of none
|
|
|
|
|
|
pinStates: Array(numPins).fill(0), // 0 = down, 1 = moving, 2 = set
|
|
|
|
|
|
pinPressTime: Array(numPins).fill(0), // Track how long each pin is pressed
|
|
|
|
|
|
currentBindingIndex: 0,
|
|
|
|
|
|
hardMode: false,
|
|
|
|
|
|
maxPressTime: 1000, // Max time to hold a pin (ms)
|
|
|
|
|
|
failCount: 0,
|
|
|
|
|
|
maxFails: 3,
|
|
|
|
|
|
overTensioned: false,
|
|
|
|
|
|
lastPinSetTime: 0, // Track when the last pin was set
|
|
|
|
|
|
isActivelyPickingPin: false, // Track if we're actively working on a pin
|
|
|
|
|
|
tensionRequirements: Array(numPins).fill(0).map(() => {
|
|
|
|
|
|
// Each pin requires a specific tension level (1, 2, or 3)
|
|
|
|
|
|
const randomValue = Math.random();
|
|
|
|
|
|
if (randomValue < 0.33) return 1; // Light tension
|
|
|
|
|
|
if (randomValue < 0.66) return 2; // Medium tension
|
|
|
|
|
|
return 3; // Heavy tension
|
|
|
|
|
|
})
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Create tension wrench toggle
|
|
|
|
|
|
const tensionWrench = document.createElement('div');
|
|
|
|
|
|
tensionWrench.style.cssText = `
|
|
|
|
|
|
width: 100px;
|
|
|
|
|
|
height: 30px;
|
|
|
|
|
|
background: ${gameState.tensionLevel === 1 ? '#666' : gameState.tensionLevel === 2 ? '#888' : '#aaa'};
|
|
|
|
|
|
border: 2px solid #888;
|
|
|
|
|
|
border-radius: 5px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
line-height: 30px;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
`;
|
|
|
|
|
|
tensionWrench.textContent = 'Tension: OFF';
|
|
|
|
|
|
|
|
|
|
|
|
// Function to reset pins
|
|
|
|
|
|
function resetPins(showVisual = true) {
|
|
|
|
|
|
gameState.pinStates.fill(0);
|
|
|
|
|
|
gameState.pinPressTime.fill(0);
|
|
|
|
|
|
gameState.currentBindingIndex = 0;
|
|
|
|
|
|
gameState.failCount++;
|
|
|
|
|
|
|
|
|
|
|
|
if (showVisual) {
|
|
|
|
|
|
Array.from(pinsContainer.children).forEach(pin => {
|
|
|
|
|
|
pin.style.background = '#555';
|
|
|
|
|
|
if (!gameState.hardMode) {
|
|
|
|
|
|
pin.style.transition = 'background-color 0.3s';
|
|
|
|
|
|
pin.style.background = '#f00';
|
|
|
|
|
|
setTimeout(() => pin.style.background = '#555', 300);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (gameState.failCount >= gameState.maxFails) {
|
|
|
|
|
|
alert("Lock picking failed! The lock is now jammed.");
|
|
|
|
|
|
// Safely remove iframe if it exists in the document
|
|
|
|
|
|
const existingIframe = document.querySelector('div[style*="z-index: 1000"]');
|
|
|
|
|
|
if (existingIframe && existingIframe.parentNode) {
|
|
|
|
|
|
existingIframe.parentNode.removeChild(existingIframe);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (currentScene && currentScene.input && currentScene.input.mouse) {
|
|
|
|
|
|
currentScene.input.mouse.enabled = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Create a single function for toggling tension
|
|
|
|
|
|
function toggleTension() {
|
|
|
|
|
|
// Toggle between 3 tension levels (light -> medium -> heavy -> light)
|
|
|
|
|
|
const previousTensionLevel = gameState.tensionLevel;
|
|
|
|
|
|
gameState.tensionLevel = (gameState.tensionLevel % 3) + 1;
|
|
|
|
|
|
|
|
|
|
|
|
// Play tension change sound
|
|
|
|
|
|
if (lockSounds.tension) {
|
|
|
|
|
|
lockSounds.tension.currentTime = 0;
|
|
|
|
|
|
lockSounds.tension.play().catch(e => console.log('Audio play failed:', e));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
updateTensionWrench();
|
|
|
|
|
|
|
|
|
|
|
|
// Check if we're over-tensioning - but only if we've started interacting with pins
|
|
|
|
|
|
// AND only if we're actively working on a pin (not just changing tension)
|
|
|
|
|
|
const timeSinceLastPinSet = Date.now() - gameState.lastPinSetTime;
|
|
|
|
|
|
const isActivelyPickingLock = gameState.isActivelyPickingPin;
|
|
|
|
|
|
|
|
|
|
|
|
if (gameState.tensionLevel === 3 &&
|
|
|
|
|
|
gameState.currentBindingIndex > 0 &&
|
|
|
|
|
|
timeSinceLastPinSet > 1000 &&
|
|
|
|
|
|
isActivelyPickingLock) {
|
|
|
|
|
|
|
|
|
|
|
|
// 30% chance of over-tensioning with heavy pressure
|
|
|
|
|
|
if (Math.random() < 0.3) {
|
|
|
|
|
|
gameState.overTensioned = true;
|
|
|
|
|
|
tensionWrench.style.background = '#ff3333';
|
|
|
|
|
|
tensionWrench.style.transform = 'rotate(15deg)';
|
|
|
|
|
|
|
|
|
|
|
|
// Play over-tension sound
|
|
|
|
|
|
if (lockSounds.overTension) {
|
|
|
|
|
|
lockSounds.overTension.currentTime = 0;
|
|
|
|
|
|
lockSounds.overTension.play().catch(e => console.log('Audio play failed:', e));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
alert("You applied too much tension and jammed the lock!");
|
|
|
|
|
|
resetPins();
|
|
|
|
|
|
}, 500);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Add this new function to update tension wrench visuals
|
|
|
|
|
|
function updateTensionWrench() {
|
|
|
|
|
|
// Update tension wrench appearance based on level
|
|
|
|
|
|
switch(gameState.tensionLevel) {
|
|
|
|
|
|
case 1: // Light
|
|
|
|
|
|
tensionWrench.style.background = '#666';
|
|
|
|
|
|
tensionWrench.style.transform = 'rotate(2deg)';
|
|
|
|
|
|
tensionWrench.textContent = 'Tension: LIGHT';
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 2: // Medium
|
|
|
|
|
|
tensionWrench.style.background = '#888';
|
|
|
|
|
|
tensionWrench.style.transform = 'rotate(5deg)';
|
|
|
|
|
|
tensionWrench.textContent = 'Tension: MEDIUM';
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 3: // Heavy
|
|
|
|
|
|
tensionWrench.style.background = '#aaa';
|
|
|
|
|
|
tensionWrench.style.transform = 'rotate(8deg)';
|
|
|
|
|
|
tensionWrench.textContent = 'Tension: HEAVY';
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Create pins container
|
|
|
|
|
|
const pinsContainer = document.createElement('div');
|
|
|
|
|
|
pinsContainer.style.cssText = `
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
background: #333;
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
// Create individual pins
|
|
|
|
|
|
for (let i = 0; i < numPins; i++) {
|
|
|
|
|
|
const pin = document.createElement('div');
|
|
|
|
|
|
pin.style.cssText = `
|
|
|
|
|
|
width: 30px;
|
|
|
|
|
|
height: 100px;
|
|
|
|
|
|
background: #555;
|
|
|
|
|
|
border: 2px solid #777;
|
|
|
|
|
|
border-radius: 5px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
transition: transform 0.1s, background-color 0.3s;
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
// Add a subtle indicator at the bottom of each pin
|
|
|
|
|
|
const pinIndicator = document.createElement('div');
|
|
|
|
|
|
pinIndicator.style.cssText = `
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
bottom: 0;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 5px;
|
|
|
|
|
|
background: #555;
|
|
|
|
|
|
transition: background-color 0.3s;
|
|
|
|
|
|
`;
|
|
|
|
|
|
pin.appendChild(pinIndicator);
|
|
|
|
|
|
|
|
|
|
|
|
const pinNumber = document.createElement('div');
|
|
|
|
|
|
pinNumber.style.cssText = `
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: -20px;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
`;
|
|
|
|
|
|
pinNumber.textContent = (i + 1).toString();
|
|
|
|
|
|
pin.appendChild(pinNumber);
|
|
|
|
|
|
|
|
|
|
|
|
// Function to update pin appearance based on its state and binding status
|
|
|
|
|
|
function updatePinAppearance() {
|
|
|
|
|
|
// Reset to default first
|
|
|
|
|
|
pin.style.background = '#555';
|
|
|
|
|
|
pinIndicator.style.background = '#555';
|
|
|
|
|
|
pin.style.animation = '';
|
|
|
|
|
|
pin.style.borderColor = '#777';
|
|
|
|
|
|
|
|
|
|
|
|
// If the pin is set, show it as green
|
|
|
|
|
|
if (gameState.pinStates[i] === 2) {
|
|
|
|
|
|
pin.style.background = '#0f0';
|
|
|
|
|
|
pin.style.cursor = 'default';
|
|
|
|
|
|
pinIndicator.style.background = '#0f0';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Get the current binding pin
|
|
|
|
|
|
const bindingPin = bindingOrder[gameState.currentBindingIndex];
|
|
|
|
|
|
|
|
|
|
|
|
// Generate consistent red herrings based on the current binding index
|
|
|
|
|
|
// This ensures the same pins are highlighted as red herrings until the next pin is set
|
|
|
|
|
|
const redHerringSeeds = [
|
|
|
|
|
|
(bindingPin * 3 + 7) % numPins,
|
|
|
|
|
|
(bindingPin * 5 + 3) % numPins
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
// Filter out the binding pin and already set pins from red herrings
|
|
|
|
|
|
const redHerrings = redHerringSeeds.filter(index =>
|
|
|
|
|
|
index !== bindingPin && gameState.pinStates[index] !== 2);
|
|
|
|
|
|
|
|
|
|
|
|
// If this is the current binding pin, give a subtle hint based on difficulty
|
|
|
|
|
|
if (i === bindingPin) {
|
|
|
|
|
|
// For easy difficulty, make the binding pin more obvious
|
|
|
|
|
|
if (difficulty === 'easy') {
|
|
|
|
|
|
pinIndicator.style.background = '#ff9900'; // Orange indicator for binding pin
|
|
|
|
|
|
|
|
|
|
|
|
// Also show the required tension level with a color hint
|
|
|
|
|
|
const requiredTension = gameState.tensionRequirements[i];
|
|
|
|
|
|
if (requiredTension === 1) {
|
|
|
|
|
|
pin.style.borderColor = '#66ccff'; // Light blue for light tension
|
|
|
|
|
|
} else if (requiredTension === 2) {
|
|
|
|
|
|
pin.style.borderColor = '#9966ff'; // Purple for medium tension
|
|
|
|
|
|
} else {
|
|
|
|
|
|
pin.style.borderColor = '#ff6666'; // Red for heavy tension
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Add a subtle animation
|
|
|
|
|
|
pin.style.animation = 'pinWiggle 2s infinite';
|
|
|
|
|
|
}
|
|
|
|
|
|
// For medium difficulty, just show which pin is binding with less obvious cues
|
|
|
|
|
|
else if (difficulty === 'medium') {
|
|
|
|
|
|
pinIndicator.style.background = '#ff9900'; // Orange indicator for binding pin
|
|
|
|
|
|
pin.style.animation = 'pinWiggle 3s infinite';
|
|
|
|
|
|
}
|
|
|
|
|
|
// For hard difficulty, very subtle indication
|
|
|
|
|
|
else if (difficulty === 'hard') {
|
|
|
|
|
|
pin.style.animation = 'pinWiggle 4s infinite 0.5s';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// If this is a red herring, give misleading feedback
|
|
|
|
|
|
else if (redHerrings.includes(i) && gameState.currentBindingIndex > 0 && difficulty !== 'easy') {
|
|
|
|
|
|
// The amount of misleading feedback depends on difficulty
|
|
|
|
|
|
if (difficulty === 'medium') {
|
|
|
|
|
|
// For medium, make red herrings somewhat convincing
|
|
|
|
|
|
pinIndicator.style.background = '#ff9900'; // Same color as real binding pin
|
|
|
|
|
|
pin.style.animation = 'pinWiggle 3.5s infinite 0.7s'; // Similar wiggle to real pin
|
|
|
|
|
|
|
|
|
|
|
|
// Randomly assign fake tension indicators to confuse
|
|
|
|
|
|
const fakeTension = Math.floor(Math.random() * 3) + 1;
|
|
|
|
|
|
if (fakeTension === 1) {
|
|
|
|
|
|
pin.style.borderColor = '#66ccff';
|
|
|
|
|
|
} else if (fakeTension === 2) {
|
|
|
|
|
|
pin.style.borderColor = '#9966ff';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
pin.style.borderColor = '#ff6666';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (difficulty === 'hard') {
|
|
|
|
|
|
// For hard, make red herrings very convincing
|
|
|
|
|
|
pin.style.animation = 'pinWiggle 4s infinite 0.3s';
|
|
|
|
|
|
|
|
|
|
|
|
// On hard, sometimes make a red herring more convincing than the real pin
|
|
|
|
|
|
if (Math.random() < 0.5) {
|
|
|
|
|
|
pinIndicator.style.background = '#ff9900';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Add the wiggle animation to the document
|
|
|
|
|
|
if (!document.getElementById('pinWiggleAnimation')) {
|
|
|
|
|
|
const style = document.createElement('style');
|
|
|
|
|
|
style.id = 'pinWiggleAnimation';
|
|
|
|
|
|
style.textContent = `
|
|
|
|
|
|
@keyframes pinWiggle {
|
|
|
|
|
|
0% { transform: translateY(0); }
|
|
|
|
|
|
15% { transform: translateY(-2px); }
|
|
|
|
|
|
30% { transform: translateY(0); }
|
|
|
|
|
|
45% { transform: translateY(-1px); }
|
|
|
|
|
|
60% { transform: translateY(0); }
|
|
|
|
|
|
75% { transform: translateY(-0.5px); }
|
|
|
|
|
|
100% { transform: translateY(0); }
|
|
|
|
|
|
}
|
|
|
|
|
|
`;
|
|
|
|
|
|
document.head.appendChild(style);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Update all pins whenever a pin state changes
|
|
|
|
|
|
function updateAllPins() {
|
|
|
|
|
|
// Get the current binding pin
|
|
|
|
|
|
const bindingPin = bindingOrder[gameState.currentBindingIndex];
|
|
|
|
|
|
|
|
|
|
|
|
// Generate consistent red herrings based on the current binding index
|
|
|
|
|
|
const redHerringSeeds = [
|
|
|
|
|
|
(bindingPin * 3 + 7) % numPins,
|
|
|
|
|
|
(bindingPin * 5 + 3) % numPins
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
// Filter out the binding pin and already set pins from red herrings
|
|
|
|
|
|
const redHerrings = redHerringSeeds.filter(index =>
|
|
|
|
|
|
index !== bindingPin && gameState.pinStates[index] !== 2);
|
|
|
|
|
|
|
|
|
|
|
|
Array.from(pinsContainer.children).forEach((pin, index) => {
|
|
|
|
|
|
// Find the indicator within this pin
|
|
|
|
|
|
const indicator = pin.querySelector('div:first-child');
|
|
|
|
|
|
|
|
|
|
|
|
// Reset styles first
|
|
|
|
|
|
pin.style.background = '#555';
|
|
|
|
|
|
pin.style.animation = '';
|
|
|
|
|
|
pin.style.borderColor = '#777';
|
|
|
|
|
|
if (indicator) indicator.style.background = '#555';
|
|
|
|
|
|
|
|
|
|
|
|
// Update based on current game state
|
|
|
|
|
|
if (gameState.pinStates[index] === 2) {
|
|
|
|
|
|
pin.style.background = '#0f0';
|
|
|
|
|
|
pin.style.cursor = 'default';
|
|
|
|
|
|
if (indicator) indicator.style.background = '#0f0';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
pin.style.cursor = 'pointer';
|
|
|
|
|
|
|
|
|
|
|
|
// Check if this is the binding pin
|
|
|
|
|
|
if (index === bindingPin && !gameState.hardMode) {
|
|
|
|
|
|
if (difficulty === 'easy') {
|
|
|
|
|
|
if (indicator) indicator.style.background = '#ff9900';
|
|
|
|
|
|
|
|
|
|
|
|
// Show tension hint
|
|
|
|
|
|
const requiredTension = gameState.tensionRequirements[index];
|
|
|
|
|
|
if (requiredTension === 1) {
|
|
|
|
|
|
pin.style.borderColor = '#66ccff';
|
|
|
|
|
|
} else if (requiredTension === 2) {
|
|
|
|
|
|
pin.style.borderColor = '#9966ff';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
pin.style.borderColor = '#ff6666';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pin.style.animation = 'pinWiggle 2s infinite';
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (difficulty === 'medium') {
|
|
|
|
|
|
if (indicator) indicator.style.background = '#ff9900';
|
|
|
|
|
|
pin.style.animation = 'pinWiggle 3s infinite';
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (difficulty === 'hard') {
|
|
|
|
|
|
pin.style.animation = 'pinWiggle 4s infinite 0.5s';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// Check if this is a red herring, but only for medium and hard difficulties
|
|
|
|
|
|
else if (redHerrings.includes(index) && gameState.currentBindingIndex > 0 && difficulty !== 'easy') {
|
|
|
|
|
|
if (difficulty === 'medium') {
|
|
|
|
|
|
if (indicator) indicator.style.background = '#ff9900';
|
|
|
|
|
|
pin.style.animation = 'pinWiggle 3.5s infinite 0.7s';
|
|
|
|
|
|
|
|
|
|
|
|
const fakeTension = Math.floor(Math.random() * 3) + 1;
|
|
|
|
|
|
if (fakeTension === 1) {
|
|
|
|
|
|
pin.style.borderColor = '#66ccff';
|
|
|
|
|
|
} else if (fakeTension === 2) {
|
|
|
|
|
|
pin.style.borderColor = '#9966ff';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
pin.style.borderColor = '#ff6666';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (difficulty === 'hard') {
|
|
|
|
|
|
pin.style.animation = 'pinWiggle 4s infinite 0.3s';
|
|
|
|
|
|
if (Math.random() < 0.5 && indicator) {
|
|
|
|
|
|
indicator.style.background = '#ff9900';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Call updatePinAppearance initially and whenever the game state changes
|
|
|
|
|
|
updatePinAppearance();
|
|
|
|
|
|
|
|
|
|
|
|
let pressStartTime = 0;
|
|
|
|
|
|
let pressTimer = null;
|
|
|
|
|
|
|
|
|
|
|
|
function checkPinPress() {
|
|
|
|
|
|
if (pressStartTime === 0) return;
|
|
|
|
|
|
|
|
|
|
|
|
const pressDuration = Date.now() - pressStartTime;
|
|
|
|
|
|
if (pressDuration > gameState.maxPressTime) {
|
|
|
|
|
|
// Clear the timer first before calling resetPins
|
|
|
|
|
|
clearInterval(pressTimer);
|
|
|
|
|
|
pressTimer = null;
|
|
|
|
|
|
resetPins();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pin.onmousedown = () => {
|
|
|
|
|
|
// First check if this pin is already set
|
|
|
|
|
|
const pinIndex = Array.from(pinsContainer.children).indexOf(pin);
|
|
|
|
|
|
if (gameState.pinStates[pinIndex] === 2) {
|
|
|
|
|
|
// Pin is already set, don't allow interaction
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Set the flag to indicate we're actively picking a pin
|
|
|
|
|
|
gameState.isActivelyPickingPin = true;
|
|
|
|
|
|
|
|
|
|
|
|
// Play basic click sound
|
|
|
|
|
|
if (lockSounds.click) {
|
|
|
|
|
|
lockSounds.click.currentTime = 0;
|
|
|
|
|
|
lockSounds.click.play().catch(e => console.log('Audio play failed:', e));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pressStartTime = Date.now();
|
|
|
|
|
|
pressTimer = setInterval(checkPinPress, 100);
|
|
|
|
|
|
|
|
|
|
|
|
pin.style.transform = 'translateY(-10px)';
|
|
|
|
|
|
|
|
|
|
|
|
// Each pin has different tension requirements
|
|
|
|
|
|
const bindingPin = bindingOrder[gameState.currentBindingIndex];
|
|
|
|
|
|
const requiredTension = gameState.tensionRequirements[pinIndex];
|
|
|
|
|
|
|
|
|
|
|
|
// Check if this is the current binding pin
|
|
|
|
|
|
if (pinIndex === bindingPin) {
|
|
|
|
|
|
// This pin needs exactly the right tension level
|
|
|
|
|
|
const correctTension = (gameState.tensionLevel === requiredTension);
|
|
|
|
|
|
|
|
|
|
|
|
if (correctTension && !gameState.overTensioned) {
|
|
|
|
|
|
// Play binding sound - correct pin with correct tension
|
|
|
|
|
|
if (lockSounds.binding) {
|
|
|
|
|
|
lockSounds.binding.currentTime = 0;
|
|
|
|
|
|
lockSounds.binding.play().catch(e => console.log('Audio play failed:', e));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!gameState.hardMode) {
|
|
|
|
|
|
pin.style.background = '#00f';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Start a timer to set the pin
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
if (pressStartTime !== 0) { // Still pressing
|
|
|
|
|
|
// Double-check tension is still correct
|
|
|
|
|
|
const stillCorrectTension = (gameState.tensionLevel === requiredTension);
|
|
|
|
|
|
|
|
|
|
|
|
if (stillCorrectTension && !gameState.overTensioned) {
|
|
|
|
|
|
gameState.pinStates[pinIndex] = 2;
|
|
|
|
|
|
gameState.currentBindingIndex++;
|
|
|
|
|
|
gameState.lastPinSetTime = Date.now();
|
|
|
|
|
|
|
|
|
|
|
|
// Play set sound - pin successfully set
|
|
|
|
|
|
if (lockSounds.set) {
|
|
|
|
|
|
lockSounds.set.currentTime = 0;
|
|
|
|
|
|
lockSounds.set.play().catch(e => console.log('Audio play failed:', e));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!gameState.hardMode) {
|
|
|
|
|
|
pin.style.background = '#0f0';
|
|
|
|
|
|
pinIndicator.style.background = '#0f0';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Update all pins to show new binding state
|
|
|
|
|
|
updateAllPins();
|
|
|
|
|
|
|
|
|
|
|
|
checkWinCondition();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 500);
|
|
|
|
|
|
} else if (gameState.tensionLevel > 0) {
|
|
|
|
|
|
// Wrong tension but trying - give feedback
|
|
|
|
|
|
// Play wrong sound - wrong tension on correct pin
|
|
|
|
|
|
if (lockSounds.wrong) {
|
|
|
|
|
|
lockSounds.wrong.currentTime = 0;
|
|
|
|
|
|
lockSounds.wrong.play().catch(e => console.log('Audio play failed:', e));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!gameState.hardMode) {
|
|
|
|
|
|
pin.style.background = '#00f';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Start counting towards potential reset
|
|
|
|
|
|
gameState.pinPressTime[pinIndex] = Date.now();
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (gameState.tensionLevel > 0 && gameState.pinStates[pinIndex] !== 2) {
|
|
|
|
|
|
// Wrong pin - give feedback
|
|
|
|
|
|
if (lockSounds.wrong) {
|
|
|
|
|
|
lockSounds.wrong.currentTime = 0;
|
|
|
|
|
|
lockSounds.wrong.play().catch(e => console.log('Audio play failed:', e));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!gameState.hardMode) {
|
|
|
|
|
|
pin.style.background = '#00f';
|
|
|
|
|
|
}
|
|
|
|
|
|
// Start counting towards potential reset
|
|
|
|
|
|
gameState.pinPressTime[pinIndex] = Date.now();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
pin.onmouseup = pin.onmouseleave = () => {
|
|
|
|
|
|
// Clear the flag to indicate we're no longer actively picking a pin
|
|
|
|
|
|
gameState.isActivelyPickingPin = false;
|
|
|
|
|
|
|
|
|
|
|
|
pressStartTime = 0;
|
|
|
|
|
|
if (pressTimer) {
|
|
|
|
|
|
clearInterval(pressTimer);
|
|
|
|
|
|
pressTimer = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pin.style.transform = 'translateY(0)';
|
|
|
|
|
|
if (gameState.pinStates[i] !== 2) {
|
|
|
|
|
|
pin.style.background = '#555';
|
|
|
|
|
|
// Update appearance to show binding status
|
|
|
|
|
|
updatePinAppearance();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
pinsContainer.appendChild(pin);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
difficultySelect.onchange = () => {
|
|
|
|
|
|
gameState.hardMode = difficultySelect.value.includes('Hard');
|
|
|
|
|
|
Array.from(pinsContainer.children).forEach(pin => {
|
|
|
|
|
|
pin.style.opacity = gameState.hardMode ? '0.1' : '1';
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Add components to game container
|
|
|
|
|
|
gameContainer.appendChild(difficultySelect);
|
|
|
|
|
|
gameContainer.appendChild(tensionWrench);
|
|
|
|
|
|
gameContainer.appendChild(pinsContainer);
|
|
|
|
|
|
|
|
|
|
|
|
// Add close button
|
|
|
|
|
|
const closeButton = document.createElement('button');
|
|
|
|
|
|
closeButton.textContent = 'X';
|
|
|
|
|
|
closeButton.style.cssText = `
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
right: 10px;
|
|
|
|
|
|
top: 10px;
|
|
|
|
|
|
background: none;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
`;
|
|
|
|
|
|
closeButton.onclick = () => {
|
|
|
|
|
|
document.body.removeChild(iframe);
|
|
|
|
|
|
if (currentScene && currentScene.input && currentScene.input.mouse) {
|
|
|
|
|
|
currentScene.input.mouse.enabled = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Assemble the interface
|
|
|
|
|
|
iframe.appendChild(closeButton);
|
|
|
|
|
|
iframe.appendChild(instructions);
|
|
|
|
|
|
iframe.appendChild(gameContainer);
|
|
|
|
|
|
document.body.appendChild(iframe);
|
|
|
|
|
|
|
|
|
|
|
|
// Disable game movement
|
|
|
|
|
|
if (currentScene && currentScene.input && currentScene.input.mouse) {
|
|
|
|
|
|
currentScene.input.mouse.enabled = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Add this function before the pin creation loop
|
|
|
|
|
|
function checkWinCondition() {
|
|
|
|
|
|
if (gameState.currentBindingIndex >= numPins) {
|
|
|
|
|
|
// Play success sound
|
|
|
|
|
|
if (lockSounds.success) {
|
|
|
|
|
|
lockSounds.success.currentTime = 0;
|
|
|
|
|
|
lockSounds.success.play().catch(e => console.log('Audio play failed:', e));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log('Lock picked successfully:', lockable);
|
|
|
|
|
|
|
|
|
|
|
|
// Create success message
|
|
|
|
|
|
const successMessage = document.createElement('div');
|
|
|
|
|
|
successMessage.style.cssText = `
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 50%;
|
|
|
|
|
|
left: 50%;
|
|
|
|
|
|
transform: translate(-50%, -50%);
|
|
|
|
|
|
background: rgba(0, 0, 0, 0.8);
|
|
|
|
|
|
color: #0f0;
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
z-index: 1002;
|
|
|
|
|
|
`;
|
|
|
|
|
|
successMessage.textContent = "Lock successfully picked!";
|
|
|
|
|
|
iframe.appendChild(successMessage);
|
|
|
|
|
|
|
|
|
|
|
|
// Disable further interaction
|
|
|
|
|
|
gameContainer.style.pointerEvents = 'none';
|
|
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
// For doors, we need to check properties and handle differently
|
|
|
|
|
|
if (lockable && lockable.properties && lockable.properties.locked) {
|
|
|
|
|
|
console.log('Unlocking door tile:', lockable);
|
|
|
|
|
|
unlockDoor(lockable, lockable.layer);
|
|
|
|
|
|
}
|
|
|
|
|
|
// For containers and other lockable items
|
|
|
|
|
|
else if (lockable && lockable.scenarioData) {
|
|
|
|
|
|
console.log('Unlocking container:', lockable.scenarioData);
|
|
|
|
|
|
lockable.scenarioData.locked = false;
|
|
|
|
|
|
|
|
|
|
|
|
// Set the flag to indicate the container is unlocked but contents not collected
|
|
|
|
|
|
if (lockable.scenarioData.contents && lockable.scenarioData.contents.length > 0) {
|
|
|
|
|
|
lockable.scenarioData.isUnlockedButNotCollected = true;
|
|
|
|
|
|
debugLog('Container unlocked and ready for collection', lockable.scenarioData, 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Remove the minigame
|
|
|
|
|
|
document.body.removeChild(iframe);
|
|
|
|
|
|
if (currentScene && currentScene.input && currentScene.input.mouse) {
|
|
|
|
|
|
currentScene.input.mouse.enabled = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 1500);
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Use the toggleTension function for both click and keyboard events
|
|
|
|
|
|
tensionWrench.onclick = toggleTension;
|
|
|
|
|
|
|
|
|
|
|
|
document.addEventListener('keydown', function(event) {
|
|
|
|
|
|
// Only process if the lockpicking minigame is active
|
|
|
|
|
|
if (!document.querySelector('div[style*="z-index: 1000"]')) return;
|
|
|
|
|
|
|
|
|
|
|
|
if (event.code === 'Space') {
|
|
|
|
|
|
event.preventDefault(); // Prevent page scrolling
|
|
|
|
|
|
toggleTension();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Keep only the table debug function
|
|
|
|
|
|
function logTensionDebugInfo() {
|
|
|
|
|
|
// Only show debug info if debug mode is enabled
|
|
|
|
|
|
if (!DEBUG_MODE.enabled) return;
|
|
|
|
|
|
|
|
|
|
|
|
DEBUG_MODE.log("=== LOCKPICKING DEBUG INFO ===");
|
|
|
|
|
|
DEBUG_MODE.log("Pin binding order and tension requirements:");
|
|
|
|
|
|
|
|
|
|
|
|
const tableData = [];
|
|
|
|
|
|
|
|
|
|
|
|
for (let orderIndex = 0; orderIndex < numPins; orderIndex++) {
|
|
|
|
|
|
const pinIndex = bindingOrder[orderIndex];
|
|
|
|
|
|
const requiredTension = gameState.tensionRequirements[pinIndex];
|
|
|
|
|
|
let tensionNeeded;
|
|
|
|
|
|
|
|
|
|
|
|
switch(requiredTension) {
|
|
|
|
|
|
case 1: tensionNeeded = 'Light'; break;
|
|
|
|
|
|
case 2: tensionNeeded = 'Medium'; break;
|
|
|
|
|
|
case 3: tensionNeeded = 'Heavy'; break;
|
|
|
|
|
|
default: tensionNeeded = 'Unknown';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
tableData.push({
|
|
|
|
|
|
'Binding Order': orderIndex + 1,
|
|
|
|
|
|
'Pin #': pinIndex + 1,
|
|
|
|
|
|
'Tension Required': tensionNeeded
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.table(tableData);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Call this function instead of addTensionDebugDisplay
|
|
|
|
|
|
logTensionDebugInfo();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Add this function to get pin count based on difficulty
|
|
|
|
|
|
function getPinCountForDifficulty(difficulty) {
|
|
|
|
|
|
switch(difficulty?.toLowerCase()) {
|
|
|
|
|
|
case 'easy':
|
|
|
|
|
|
return 3;
|
|
|
|
|
|
case 'medium':
|
|
|
|
|
|
return 5;
|
|
|
|
|
|
case 'hard':
|
|
|
|
|
|
return 7;
|
|
|
|
|
|
default:
|
|
|
|
|
|
return 5; // Default to medium difficulty
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// removes an item from the inventory
|
|
|
|
|
|
function removeFromInventory(sprite) {
|
|
|
|
|
|
if (!sprite || !inventory.items) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// Find the index of the sprite in the inventory
|
|
|
|
|
|
const index = inventory.items.indexOf(sprite);
|
|
|
|
|
|
|
|
|
|
|
|
if (index === -1) {
|
|
|
|
|
|
return false; // Item not found in inventory
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Remove from container
|
|
|
|
|
|
inventory.container.remove(sprite);
|
|
|
|
|
|
|
|
|
|
|
|
// Remove from items array
|
|
|
|
|
|
inventory.items.splice(index, 1);
|
|
|
|
|
|
|
|
|
|
|
|
// Destroy the sprite
|
|
|
|
|
|
sprite.destroy();
|
|
|
|
|
|
|
|
|
|
|
|
// Rearrange remaining items
|
|
|
|
|
|
rearrangeInventoryItems();
|
|
|
|
|
|
|
|
|
|
|
|
// Log the removal
|
|
|
|
|
|
debugLog('INVENTORY ITEM REMOVED', {
|
|
|
|
|
|
name: sprite.name,
|
|
|
|
|
|
totalItems: inventory.items.length
|
|
|
|
|
|
}, 2);
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Error removing item from inventory:', error);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Rearrange inventory items after removal
|
|
|
|
|
|
function rearrangeInventoryItems() {
|
|
|
|
|
|
inventory.items.forEach((item, index) => {
|
|
|
|
|
|
item.x = index * 60 + 100;
|
|
|
|
|
|
});
|
2025-02-18 21:29:02 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-08 15:00:17 +00:00
|
|
|
|
// Bluetooth scanner system
|
|
|
|
|
|
const bluetoothDevices = [];
|
|
|
|
|
|
let newBluetoothDevices = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// Add a Bluetooth device to the scanner panel
|
|
|
|
|
|
function addBluetoothDevice(name, mac, details = "", nearby = true) {
|
|
|
|
|
|
// Check if a device with the same MAC already exists
|
|
|
|
|
|
const deviceExists = bluetoothDevices.some(device => device.mac === mac);
|
|
|
|
|
|
|
|
|
|
|
|
// If the device already exists, update its nearby status
|
|
|
|
|
|
if (deviceExists) {
|
|
|
|
|
|
const existingDevice = bluetoothDevices.find(device => device.mac === mac);
|
|
|
|
|
|
existingDevice.nearby = nearby;
|
|
|
|
|
|
existingDevice.lastSeen = new Date();
|
|
|
|
|
|
updateBluetoothPanel();
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const device = {
|
|
|
|
|
|
id: Date.now(),
|
|
|
|
|
|
name: name,
|
|
|
|
|
|
mac: mac,
|
|
|
|
|
|
details: details,
|
|
|
|
|
|
nearby: nearby,
|
|
|
|
|
|
saved: false,
|
|
|
|
|
|
firstSeen: new Date(),
|
|
|
|
|
|
lastSeen: new Date()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
bluetoothDevices.push(device);
|
|
|
|
|
|
updateBluetoothPanel();
|
|
|
|
|
|
updateBluetoothCount();
|
|
|
|
|
|
|
|
|
|
|
|
// Show notification for new device
|
|
|
|
|
|
showNotification(`New Bluetooth device detected: ${name}`, 'info', 'Bluetooth Scanner', 3000);
|
|
|
|
|
|
|
|
|
|
|
|
return device;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Update the Bluetooth scanner panel with current devices
|
|
|
|
|
|
function updateBluetoothPanel() {
|
|
|
|
|
|
const bluetoothContent = document.getElementById('bluetooth-content');
|
|
|
|
|
|
const searchTerm = document.getElementById('bluetooth-search')?.value?.toLowerCase() || '';
|
|
|
|
|
|
|
|
|
|
|
|
// Get active category
|
|
|
|
|
|
const activeCategory = document.querySelector('.bluetooth-category.active')?.dataset.category || 'all';
|
|
|
|
|
|
|
2025-03-08 16:52:05 +00:00
|
|
|
|
// Store the currently hovered device, if any
|
|
|
|
|
|
const hoveredDevice = document.querySelector('.bluetooth-device:hover');
|
|
|
|
|
|
const hoveredDeviceId = hoveredDevice ? hoveredDevice.dataset.id : null;
|
|
|
|
|
|
|
2025-03-08 21:09:17 +00:00
|
|
|
|
// Add Bluetooth-locked items from inventory to the main bluetoothDevices array
|
|
|
|
|
|
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
|
|
|
|
|
|
const existingDeviceIndex = bluetoothDevices.findIndex(device =>
|
|
|
|
|
|
device.mac.toLowerCase() === normalizedMac
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (existingDeviceIndex === -1) {
|
|
|
|
|
|
// Add as a new device
|
|
|
|
|
|
const deviceName = item.scenarioData?.name || item.name || "Unknown 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: 100, // Max strength for inventory items
|
|
|
|
|
|
inInventory: true // Mark as inventory item
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Add to the main bluetoothDevices array
|
|
|
|
|
|
bluetoothDevices.push(newDevice);
|
|
|
|
|
|
console.log('Added inventory device to bluetoothDevices:', newDevice);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Update existing device
|
|
|
|
|
|
const existingDevice = bluetoothDevices[existingDeviceIndex];
|
|
|
|
|
|
existingDevice.inInventory = true;
|
|
|
|
|
|
existingDevice.nearby = true;
|
|
|
|
|
|
existingDevice.signalStrength = 100;
|
|
|
|
|
|
existingDevice.lastSeen = new Date();
|
|
|
|
|
|
existingDevice.details = `Type: ${item.scenarioData?.type || "Unknown"}\nLocation: Inventory\nStatus: Locked`;
|
|
|
|
|
|
console.log('Updated existing device with inventory info:', existingDevice);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-03-08 15:00:17 +00:00
|
|
|
|
// 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)
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-08 15:53:08 +00:00
|
|
|
|
// Sort devices with nearby ones first, then by signal strength (highest first for nearby), then by last seen (newest first)
|
2025-03-08 15:00:17 +00:00
|
|
|
|
filteredDevices.sort((a, b) => {
|
2025-03-08 21:09:17 +00:00
|
|
|
|
// Inventory items first
|
|
|
|
|
|
if (a.inInventory !== b.inInventory) {
|
|
|
|
|
|
return a.inInventory ? -1 : 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Then nearby items
|
2025-03-08 15:00:17 +00:00
|
|
|
|
if (a.nearby !== b.nearby) {
|
|
|
|
|
|
return a.nearby ? -1 : 1;
|
|
|
|
|
|
}
|
2025-03-08 15:53:08 +00:00
|
|
|
|
|
|
|
|
|
|
// For nearby devices, sort by signal strength
|
|
|
|
|
|
if (a.nearby && b.nearby && a.signalStrength !== b.signalStrength) {
|
|
|
|
|
|
return b.signalStrength - a.signalStrength;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-08 15:00:17 +00:00
|
|
|
|
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;
|
|
|
|
|
|
|
2025-03-08 16:52:05 +00:00
|
|
|
|
// If this was the hovered device, add the hover class
|
|
|
|
|
|
if (hoveredDeviceId && device.id === hoveredDeviceId) {
|
|
|
|
|
|
deviceElement.classList.add('hover-preserved');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-08 15:00:17 +00:00
|
|
|
|
// Format the timestamp
|
|
|
|
|
|
const timestamp = new Date(device.lastSeen);
|
|
|
|
|
|
const formattedDate = timestamp.toLocaleDateString();
|
|
|
|
|
|
const formattedTime = timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
|
|
|
|
|
2025-03-08 15:53:08 +00:00
|
|
|
|
// 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
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-03-08 15:00:17 +00:00
|
|
|
|
let deviceContent = `<div class="bluetooth-device-name">
|
|
|
|
|
|
<span>${device.name}</span>
|
|
|
|
|
|
<div class="bluetooth-device-icons">`;
|
|
|
|
|
|
|
2025-03-08 16:00:25 +00:00
|
|
|
|
if (device.nearby && typeof device.signalStrength === 'number') {
|
|
|
|
|
|
const signalColor = getSignalColor(device.signalStrength);
|
|
|
|
|
|
|
|
|
|
|
|
// Calculate how many bars should be active based on signal strength
|
|
|
|
|
|
const activeBars = Math.ceil(device.signalStrength / 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
|
2025-03-08 15:00:17 +00:00
|
|
|
|
deviceContent += `<span class="bluetooth-device-icon">📶</span>`;
|
|
|
|
|
|
}
|
2025-03-08 16:00:25 +00:00
|
|
|
|
|
2025-03-08 15:00:17 +00:00
|
|
|
|
if (device.saved) {
|
|
|
|
|
|
deviceContent += `<span class="bluetooth-device-icon">💾</span>`;
|
|
|
|
|
|
}
|
2025-03-08 21:09:17 +00:00
|
|
|
|
|
|
|
|
|
|
if (device.inInventory) {
|
|
|
|
|
|
deviceContent += `<span class="bluetooth-device-icon">🎒</span>`;
|
|
|
|
|
|
}
|
2025-03-08 16:00:25 +00:00
|
|
|
|
|
2025-03-08 15:00:17 +00:00
|
|
|
|
deviceContent += `</div></div>`;
|
|
|
|
|
|
deviceContent += `<div class="bluetooth-device-details">MAC: ${device.mac}\n${device.details}</div>`;
|
2025-03-08 16:06:45 +00:00
|
|
|
|
|
|
|
|
|
|
// Add pairing button only if device is nearby and player has a Bluetooth spoofer
|
|
|
|
|
|
if (device.nearby) {
|
|
|
|
|
|
// Check if player has a Bluetooth spoofer in inventory
|
2025-03-08 16:16:42 +00:00
|
|
|
|
const spoofer = inventory.items.find(item =>
|
2025-03-08 16:06:45 +00:00
|
|
|
|
item.scenarioData?.type === "bluetooth_spoofer"
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-03-08 16:16:42 +00:00
|
|
|
|
if (spoofer) {
|
|
|
|
|
|
// Check if this device is already paired with (MAC address programmed to spoofer)
|
2025-03-08 17:02:26 +00:00
|
|
|
|
const isPaired = spoofer.scenarioData?.macPaired === device.mac;
|
2025-03-08 16:16:42 +00:00
|
|
|
|
const buttonClass = isPaired ? 'bluetooth-pair-button paired' : 'bluetooth-pair-button';
|
2025-03-08 17:02:26 +00:00
|
|
|
|
const buttonText = isPaired ? 'MAC Address Paired' : 'Pair MAC Address';
|
2025-03-08 16:16:42 +00:00
|
|
|
|
|
|
|
|
|
|
deviceContent += `<button class="${buttonClass}" data-mac="${device.mac}">${buttonText}</button>`;
|
|
|
|
|
|
|
|
|
|
|
|
// If device is paired, add a hint about using it to unlock
|
|
|
|
|
|
if (isPaired) {
|
|
|
|
|
|
deviceContent += `<div style="font-size: 11px; color: #27ae60; margin-top: 4px;">
|
|
|
|
|
|
You can now use this MAC address to unlock matching Bluetooth locks
|
|
|
|
|
|
</div>`;
|
2025-03-08 21:09:17 +00:00
|
|
|
|
|
|
|
|
|
|
// If this is an inventory item, add an unlock button
|
|
|
|
|
|
if (device.inInventory) {
|
|
|
|
|
|
deviceContent += `<button class="bluetooth-unlock-button" data-mac="${device.mac}">Unlock Device</button>`;
|
|
|
|
|
|
}
|
2025-03-08 16:16:42 +00:00
|
|
|
|
}
|
2025-03-08 16:06:45 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-08 15:00:17 +00:00
|
|
|
|
deviceContent += `<div class="bluetooth-device-timestamp">Last seen: ${formattedDate} ${formattedTime}</div>`;
|
2025-03-08 16:00:25 +00:00
|
|
|
|
|
2025-03-08 15:00:17 +00:00
|
|
|
|
deviceElement.innerHTML = deviceContent;
|
|
|
|
|
|
|
|
|
|
|
|
// Toggle expanded state when clicked
|
2025-03-08 21:09:17 +00:00
|
|
|
|
deviceElement.addEventListener('click', (event) => {
|
|
|
|
|
|
// Don't toggle if clicking on the pair button
|
|
|
|
|
|
if (event.target.classList.contains('bluetooth-pair-button') ||
|
|
|
|
|
|
event.target.closest('.bluetooth-pair-button')) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-08 15:00:17 +00:00
|
|
|
|
deviceElement.classList.toggle('expanded');
|
|
|
|
|
|
|
|
|
|
|
|
// Mark as saved when expanded
|
|
|
|
|
|
if (!device.saved && deviceElement.classList.contains('expanded')) {
|
|
|
|
|
|
device.saved = true;
|
|
|
|
|
|
updateBluetoothCount();
|
|
|
|
|
|
updateBluetoothPanel();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
bluetoothContent.appendChild(deviceElement);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Update the new Bluetooth devices count
|
|
|
|
|
|
function updateBluetoothCount() {
|
|
|
|
|
|
const bluetoothCount = document.getElementById('bluetooth-count');
|
|
|
|
|
|
newBluetoothDevices = bluetoothDevices.filter(device => !device.saved && device.nearby).length;
|
|
|
|
|
|
|
|
|
|
|
|
bluetoothCount.textContent = newBluetoothDevices;
|
|
|
|
|
|
bluetoothCount.style.display = newBluetoothDevices > 0 ? 'flex' : 'none';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Toggle the Bluetooth scanner panel
|
|
|
|
|
|
function toggleBluetoothPanel() {
|
|
|
|
|
|
const bluetoothPanel = document.getElementById('bluetooth-panel');
|
|
|
|
|
|
const isVisible = bluetoothPanel.style.display === 'block';
|
|
|
|
|
|
|
|
|
|
|
|
bluetoothPanel.style.display = isVisible ? 'none' : 'block';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Initialize Bluetooth scanner panel
|
|
|
|
|
|
function initializeBluetoothPanel() {
|
|
|
|
|
|
// Set up Bluetooth toggle button
|
|
|
|
|
|
const bluetoothToggle = document.getElementById('bluetooth-toggle');
|
|
|
|
|
|
bluetoothToggle.addEventListener('click', toggleBluetoothPanel);
|
|
|
|
|
|
|
|
|
|
|
|
// Set up Bluetooth close button
|
|
|
|
|
|
const bluetoothClose = document.getElementById('bluetooth-close');
|
|
|
|
|
|
bluetoothClose.addEventListener('click', toggleBluetoothPanel);
|
|
|
|
|
|
|
|
|
|
|
|
// Set up search functionality
|
|
|
|
|
|
const bluetoothSearch = document.getElementById('bluetooth-search');
|
|
|
|
|
|
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();
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-03-08 16:16:42 +00:00
|
|
|
|
// Set up global event delegation for the pairing buttons
|
|
|
|
|
|
document.addEventListener('click', function(event) {
|
|
|
|
|
|
if (event.target.classList.contains('bluetooth-pair-button')) {
|
|
|
|
|
|
const mac = event.target.dataset.mac;
|
2025-03-08 21:09:17 +00:00
|
|
|
|
console.log('Attempting to pair with device MAC:', mac);
|
|
|
|
|
|
|
|
|
|
|
|
// Find the device in our list
|
|
|
|
|
|
const device = bluetoothDevices.find(device => device.mac === mac);
|
|
|
|
|
|
console.log('Found device:', device);
|
|
|
|
|
|
|
|
|
|
|
|
if (device) {
|
|
|
|
|
|
attemptPairingWithDevice(mac);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
gameAlert("Device not found in Bluetooth devices list.", 'error', 'Pairing Failed', 3000);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
event.stopPropagation(); // Prevent device expanding/collapsing when clicking the button
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Handle unlock button clicks
|
|
|
|
|
|
if (event.target.classList.contains('bluetooth-unlock-button')) {
|
|
|
|
|
|
const mac = event.target.dataset.mac;
|
|
|
|
|
|
console.log('Attempting to unlock device MAC:', mac);
|
|
|
|
|
|
|
|
|
|
|
|
// Find the inventory item with this MAC address
|
|
|
|
|
|
const item = inventory.items.find(item =>
|
|
|
|
|
|
item.scenarioData?.mac === mac &&
|
|
|
|
|
|
item.scenarioData?.lockType === "bluetooth" &&
|
|
|
|
|
|
item.scenarioData?.locked
|
|
|
|
|
|
);
|
|
|
|
|
|
console.log('Found inventory item:', item);
|
|
|
|
|
|
|
|
|
|
|
|
if (item) {
|
|
|
|
|
|
unlockInventoryDeviceByMac(mac);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
gameAlert("Device not found in inventory.", 'error', 'Unlock Failed', 3000);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-08 16:16:42 +00:00
|
|
|
|
event.stopPropagation(); // Prevent device expanding/collapsing when clicking the button
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-03-08 15:00:17 +00:00
|
|
|
|
// Initialize Bluetooth count
|
|
|
|
|
|
updateBluetoothCount();
|
|
|
|
|
|
}
|
2025-03-08 21:09:17 +00:00
|
|
|
|
|
|
|
|
|
|
// Function to unlock a Bluetooth-locked inventory item by MAC address
|
|
|
|
|
|
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 = 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);
|
|
|
|
|
|
gameAlert("Device not found in inventory.", 'error', 'Unlock Failed', 3000);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log('Found inventory item to unlock:', item);
|
|
|
|
|
|
|
|
|
|
|
|
// Attempt to spoof the device
|
|
|
|
|
|
const success = spoofBluetoothDevice(item);
|
|
|
|
|
|
|
|
|
|
|
|
// If successful, remove the device from the bluetoothDevices array
|
|
|
|
|
|
if (success) {
|
|
|
|
|
|
// Find the device in the bluetoothDevices array
|
|
|
|
|
|
const deviceIndex = bluetoothDevices.findIndex(device =>
|
|
|
|
|
|
device.mac.toLowerCase() === normalizedMac && device.inInventory
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// Remove it if found
|
|
|
|
|
|
if (deviceIndex !== -1) {
|
|
|
|
|
|
console.log('Removing unlocked device from bluetoothDevices array');
|
|
|
|
|
|
bluetoothDevices.splice(deviceIndex, 1);
|
|
|
|
|
|
|
|
|
|
|
|
// Update the Bluetooth panel to reflect the changes
|
|
|
|
|
|
updateBluetoothPanel();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-03-08 15:00:17 +00:00
|
|
|
|
|
2025-03-08 16:16:42 +00:00
|
|
|
|
// Function to handle pairing attempts with Bluetooth devices
|
|
|
|
|
|
function attemptPairingWithDevice(mac) {
|
2025-03-08 21:09:17 +00:00
|
|
|
|
console.log('Attempting to pair with MAC:', mac);
|
|
|
|
|
|
console.log('All Bluetooth devices:', bluetoothDevices);
|
|
|
|
|
|
|
|
|
|
|
|
// Find the device in our list (case-insensitive comparison)
|
|
|
|
|
|
const normalizedMac = mac.toLowerCase();
|
|
|
|
|
|
const device = bluetoothDevices.find(device => device.mac.toLowerCase() === normalizedMac);
|
|
|
|
|
|
|
2025-03-08 16:16:42 +00:00
|
|
|
|
if (!device) {
|
2025-03-08 21:09:17 +00:00
|
|
|
|
console.error('Device not found with MAC:', mac);
|
2025-03-08 16:16:42 +00:00
|
|
|
|
gameAlert("Device not found.", 'error', 'Pairing Failed', 3000);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-08 21:09:17 +00:00
|
|
|
|
console.log('Found device for pairing:', device);
|
|
|
|
|
|
|
2025-03-08 16:16:42 +00:00
|
|
|
|
// Find spoofer in inventory
|
|
|
|
|
|
const spoofer = inventory.items.find(item =>
|
|
|
|
|
|
item.scenarioData?.type === "bluetooth_spoofer"
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (!spoofer) {
|
|
|
|
|
|
gameAlert("You need a Bluetooth spoofer to pair with this device.", 'warning', 'Spoofer Required', 3000);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Check if player is close enough to the device (using the proximity from real-time scanning)
|
2025-03-08 21:09:17 +00:00
|
|
|
|
// Skip proximity check for inventory items
|
|
|
|
|
|
if (!device.nearby && !device.inInventory) {
|
2025-03-08 16:16:42 +00:00
|
|
|
|
gameAlert("You need to be closer to the device to pair with it.", 'warning', 'Too Far', 3000);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Check if this device is already paired with
|
2025-03-08 21:09:17 +00:00
|
|
|
|
if (spoofer.scenarioData?.macPaired && spoofer.scenarioData.macPaired.toLowerCase() === normalizedMac) {
|
|
|
|
|
|
gameAlert(`This device's MAC address (${mac}) is already programmed into your spoofer.`, 'info', 'Already Paired', 3000);
|
2025-03-08 16:16:42 +00:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-08 17:02:26 +00:00
|
|
|
|
// Launch the MAC address pairing minigame
|
|
|
|
|
|
startBluetoothPairingMinigame(device);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Bluetooth MAC Address Pairing Minigame
|
|
|
|
|
|
function startBluetoothPairingMinigame(device) {
|
|
|
|
|
|
// Create minigame container
|
|
|
|
|
|
const minigameContainer = document.createElement('div');
|
|
|
|
|
|
minigameContainer.style.cssText = `
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
top: 50%;
|
|
|
|
|
|
left: 50%;
|
|
|
|
|
|
transform: translate(-50%, -50%);
|
|
|
|
|
|
width: 60%;
|
|
|
|
|
|
height: 60%;
|
|
|
|
|
|
background: rgba(0, 0, 0, 0.9);
|
|
|
|
|
|
border: 1px solid #444;
|
|
|
|
|
|
z-index: 1000;
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
border-radius: 5px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
// Add instructions
|
|
|
|
|
|
const instructions = document.createElement('div');
|
|
|
|
|
|
instructions.innerHTML = `
|
|
|
|
|
|
<h3 style="margin: 0; color: #fff; text-align: center;">MAC Address Spoofing</h3>
|
|
|
|
|
|
<p style="margin: 5px 0; color: #ccc; text-align: center; font-size: 14px;">
|
|
|
|
|
|
Align the signal frequencies to match the target device's MAC address pattern.<br>
|
|
|
|
|
|
Drag the sliders to adjust each frequency band until all segments turn green.<br>
|
|
|
|
|
|
When all segments are aligned, the MAC address will be successfully paired.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
`;
|
|
|
|
|
|
instructions.style.cssText = `
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 10px;
|
|
|
|
|
|
left: 50%;
|
|
|
|
|
|
transform: translateX(-50%);
|
|
|
|
|
|
width: 90%;
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
// Create device info display
|
|
|
|
|
|
const deviceInfo = document.createElement('div');
|
|
|
|
|
|
deviceInfo.innerHTML = `
|
|
|
|
|
|
<div style="color: #9b59b6; font-weight: bold; margin-bottom: 5px;">Target Device: ${device.name}</div>
|
|
|
|
|
|
<div style="color: #aaa; font-size: 12px;">MAC Address: ${device.mac}</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
deviceInfo.style.cssText = `
|
|
|
|
|
|
margin-top: 70px;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
// Create signal visualization
|
|
|
|
|
|
const signalVisualization = document.createElement('div');
|
|
|
|
|
|
signalVisualization.style.cssText = `
|
|
|
|
|
|
width: 90%;
|
|
|
|
|
|
height: 120px;
|
|
|
|
|
|
background: #111;
|
|
|
|
|
|
border: 1px solid #333;
|
|
|
|
|
|
margin: 20px 0;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
// Create frequency bands (6 for MAC address octets)
|
|
|
|
|
|
const frequencyBands = document.createElement('div');
|
|
|
|
|
|
frequencyBands.style.cssText = `
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
width: 90%;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
margin-bottom: 30px;
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
// Create sliders for each frequency band
|
|
|
|
|
|
const sliders = [];
|
|
|
|
|
|
const targetValues = [];
|
|
|
|
|
|
const currentValues = [];
|
|
|
|
|
|
const segments = [];
|
|
|
|
|
|
|
|
|
|
|
|
// Parse MAC address to generate target values
|
|
|
|
|
|
const macParts = device.mac.split(':');
|
|
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < 6; i++) {
|
|
|
|
|
|
// Convert hex to decimal for target value (0-100 range)
|
|
|
|
|
|
const hexValue = macParts[i] || '00';
|
|
|
|
|
|
const decimalValue = parseInt(hexValue, 16);
|
|
|
|
|
|
const targetValue = Math.round((decimalValue / 255) * 100);
|
|
|
|
|
|
targetValues.push(targetValue);
|
|
|
|
|
|
|
|
|
|
|
|
// Start with random values
|
|
|
|
|
|
const initialValue = Math.floor(Math.random() * 100);
|
|
|
|
|
|
currentValues.push(initialValue);
|
|
|
|
|
|
|
|
|
|
|
|
// Create slider container
|
|
|
|
|
|
const sliderContainer = document.createElement('div');
|
|
|
|
|
|
sliderContainer.style.cssText = `
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
width: 40px;
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
// Create slider
|
|
|
|
|
|
const slider = document.createElement('input');
|
|
|
|
|
|
slider.type = 'range';
|
|
|
|
|
|
slider.min = 0;
|
|
|
|
|
|
slider.max = 100;
|
|
|
|
|
|
slider.value = initialValue;
|
|
|
|
|
|
slider.style.cssText = `
|
|
|
|
|
|
width: 120px;
|
|
|
|
|
|
transform: rotate(-90deg);
|
|
|
|
|
|
margin: 50px 0;
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
// Create segment in visualization
|
|
|
|
|
|
const segment = document.createElement('div');
|
|
|
|
|
|
segment.style.cssText = `
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
bottom: 0;
|
|
|
|
|
|
left: ${i * (100/6)}%;
|
|
|
|
|
|
width: ${100/6}%;
|
|
|
|
|
|
height: ${initialValue}%;
|
|
|
|
|
|
background: #cc5500;
|
|
|
|
|
|
transition: height 0.2s, background-color 0.3s;
|
|
|
|
|
|
`;
|
|
|
|
|
|
segments.push(segment);
|
|
|
|
|
|
signalVisualization.appendChild(segment);
|
|
|
|
|
|
|
|
|
|
|
|
// Create label
|
|
|
|
|
|
const label = document.createElement('div');
|
|
|
|
|
|
label.textContent = hexValue.toUpperCase();
|
|
|
|
|
|
label.style.cssText = `
|
|
|
|
|
|
color: #aaa;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
margin-top: 10px;
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
// Add event listener to slider
|
|
|
|
|
|
slider.addEventListener('input', (e) => {
|
|
|
|
|
|
const value = parseInt(e.target.value);
|
|
|
|
|
|
currentValues[i] = value;
|
|
|
|
|
|
segment.style.height = `${value}%`;
|
|
|
|
|
|
|
|
|
|
|
|
// Check if this segment is aligned correctly
|
|
|
|
|
|
const isAligned = Math.abs(value - targetValues[i]) <= 5;
|
|
|
|
|
|
segment.style.backgroundColor = isAligned ? '#00cc00' : '#cc5500';
|
|
|
|
|
|
|
|
|
|
|
|
// Check if all segments are aligned
|
|
|
|
|
|
checkCompletion();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
sliders.push(slider);
|
|
|
|
|
|
sliderContainer.appendChild(slider);
|
|
|
|
|
|
sliderContainer.appendChild(label);
|
|
|
|
|
|
frequencyBands.appendChild(sliderContainer);
|
|
|
|
|
|
}
|
2025-03-08 16:16:42 +00:00
|
|
|
|
|
2025-03-08 17:02:26 +00:00
|
|
|
|
// Create status message
|
|
|
|
|
|
const statusMessage = document.createElement('div');
|
|
|
|
|
|
statusMessage.style.cssText = `
|
|
|
|
|
|
color: #aaa;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
margin-top: 15px;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
`;
|
|
|
|
|
|
statusMessage.textContent = 'Align all frequency bands...';
|
2025-03-08 16:16:42 +00:00
|
|
|
|
|
2025-03-08 17:02:26 +00:00
|
|
|
|
// Create cancel button
|
|
|
|
|
|
const cancelButton = document.createElement('button');
|
|
|
|
|
|
cancelButton.textContent = 'Cancel';
|
|
|
|
|
|
cancelButton.style.cssText = `
|
|
|
|
|
|
background-color: #555;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
|
padding: 8px 15px;
|
|
|
|
|
|
margin-top: 20px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
`;
|
|
|
|
|
|
cancelButton.addEventListener('click', () => {
|
|
|
|
|
|
endMinigame(false);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Add all elements to container
|
|
|
|
|
|
minigameContainer.appendChild(instructions);
|
|
|
|
|
|
minigameContainer.appendChild(deviceInfo);
|
|
|
|
|
|
minigameContainer.appendChild(signalVisualization);
|
|
|
|
|
|
minigameContainer.appendChild(frequencyBands);
|
|
|
|
|
|
minigameContainer.appendChild(statusMessage);
|
|
|
|
|
|
minigameContainer.appendChild(cancelButton);
|
|
|
|
|
|
|
|
|
|
|
|
// Add container to document
|
|
|
|
|
|
document.body.appendChild(minigameContainer);
|
|
|
|
|
|
|
|
|
|
|
|
// Function to check if all segments are aligned
|
|
|
|
|
|
function checkCompletion() {
|
|
|
|
|
|
const allAligned = currentValues.every((value, index) =>
|
|
|
|
|
|
Math.abs(value - targetValues[index]) <= 5
|
|
|
|
|
|
);
|
2025-03-08 16:16:42 +00:00
|
|
|
|
|
2025-03-08 17:02:26 +00:00
|
|
|
|
if (allAligned) {
|
|
|
|
|
|
statusMessage.textContent = 'MAC Address pattern matched!';
|
|
|
|
|
|
statusMessage.style.color = '#00cc00';
|
|
|
|
|
|
|
|
|
|
|
|
// Add success animation
|
|
|
|
|
|
segments.forEach(segment => {
|
|
|
|
|
|
segment.style.backgroundColor = '#00cc00';
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// End minigame with success after a short delay
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
endMinigame(true);
|
|
|
|
|
|
}, 1500);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Function to end minigame
|
|
|
|
|
|
function endMinigame(success) {
|
|
|
|
|
|
// Remove minigame container
|
|
|
|
|
|
document.body.removeChild(minigameContainer);
|
|
|
|
|
|
|
|
|
|
|
|
// Call callback with result
|
|
|
|
|
|
if (success) {
|
|
|
|
|
|
// Find spoofer in inventory
|
|
|
|
|
|
const spoofer = inventory.items.find(item =>
|
|
|
|
|
|
item.scenarioData?.type === "bluetooth_spoofer"
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (spoofer) {
|
2025-03-08 21:09:17 +00:00
|
|
|
|
console.log('Programming spoofer with MAC address:', device.mac);
|
|
|
|
|
|
|
2025-03-08 17:02:26 +00:00
|
|
|
|
// Program the spoofer with the target MAC address
|
|
|
|
|
|
spoofer.scenarioData.macPaired = device.mac;
|
|
|
|
|
|
|
|
|
|
|
|
// Show success message
|
|
|
|
|
|
gameAlert(`Successfully programmed spoofer with MAC address: ${device.mac}`, 'success', 'Pairing Complete', 4000);
|
|
|
|
|
|
debugLog('BLUETOOTH SPOOFER PROGRAMMED', {
|
|
|
|
|
|
deviceName: device.name,
|
|
|
|
|
|
deviceMac: device.mac
|
|
|
|
|
|
}, 1);
|
|
|
|
|
|
|
|
|
|
|
|
// Update the UI to show the pairing was successful
|
|
|
|
|
|
device.paired = true;
|
2025-03-08 21:09:17 +00:00
|
|
|
|
|
|
|
|
|
|
// Update the Bluetooth panel to reflect the pairing
|
2025-03-08 17:02:26 +00:00
|
|
|
|
updateBluetoothPanel();
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
gameAlert('Pairing canceled.', 'info', 'Pairing Canceled', 3000);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-03-08 16:16:42 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-08 22:03:24 +00:00
|
|
|
|
// Biometrics Panel System
|
|
|
|
|
|
// Add this variable to track newly collected samples
|
|
|
|
|
|
let newBiometricSamples = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// Function to update the biometrics panel with current samples
|
|
|
|
|
|
function updateBiometricsPanel() {
|
|
|
|
|
|
const biometricsContent = document.getElementById('biometrics-content');
|
|
|
|
|
|
const searchTerm = document.getElementById('biometrics-search')?.value?.toLowerCase() || '';
|
|
|
|
|
|
|
|
|
|
|
|
// Get active category
|
|
|
|
|
|
const activeCategory = document.querySelector('.biometrics-category.active')?.dataset.category || 'all';
|
|
|
|
|
|
|
|
|
|
|
|
// Filter samples based on search and category
|
|
|
|
|
|
let filteredSamples = [...gameState.biometricSamples || []];
|
|
|
|
|
|
|
|
|
|
|
|
// Apply category filter
|
|
|
|
|
|
if (activeCategory === 'fingerprint') {
|
|
|
|
|
|
// Only show non-spoofed fingerprints in the fingerprint category
|
|
|
|
|
|
filteredSamples = filteredSamples.filter(sample =>
|
|
|
|
|
|
sample.type === 'fingerprint' && !sample.isSpoofed
|
|
|
|
|
|
);
|
|
|
|
|
|
} else if (activeCategory === 'spoofed') {
|
|
|
|
|
|
filteredSamples = filteredSamples.filter(sample => sample.isSpoofed);
|
|
|
|
|
|
}
|
|
|
|
|
|
// The 'all' category shows everything by default
|
|
|
|
|
|
|
|
|
|
|
|
// Apply search filter
|
|
|
|
|
|
if (searchTerm) {
|
|
|
|
|
|
filteredSamples = filteredSamples.filter(sample =>
|
|
|
|
|
|
sample.owner.toLowerCase().includes(searchTerm) ||
|
|
|
|
|
|
sample.type.toLowerCase().includes(searchTerm)
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Sort samples with highest quality first
|
|
|
|
|
|
filteredSamples.sort((a, b) => b.quality - a.quality);
|
|
|
|
|
|
|
|
|
|
|
|
// Clear current content
|
|
|
|
|
|
biometricsContent.innerHTML = '';
|
|
|
|
|
|
|
|
|
|
|
|
// Add samples
|
|
|
|
|
|
if (filteredSamples.length === 0) {
|
|
|
|
|
|
const noSamples = document.createElement('p');
|
|
|
|
|
|
noSamples.textContent = 'No samples found.';
|
|
|
|
|
|
noSamples.style.textAlign = 'center';
|
|
|
|
|
|
noSamples.style.color = '#aaa';
|
|
|
|
|
|
biometricsContent.appendChild(noSamples);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
filteredSamples.forEach(sample => {
|
|
|
|
|
|
const sampleElement = document.createElement('div');
|
|
|
|
|
|
sampleElement.className = 'biometric-sample';
|
|
|
|
|
|
sampleElement.dataset.id = sample.id;
|
|
|
|
|
|
|
|
|
|
|
|
const timestamp = new Date(sample.timestamp || Date.now());
|
|
|
|
|
|
const formattedDate = timestamp.toLocaleDateString();
|
|
|
|
|
|
const formattedTime = timestamp.toLocaleTimeString();
|
|
|
|
|
|
|
|
|
|
|
|
const qualityPercentage = Math.round(sample.quality * 100);
|
|
|
|
|
|
|
|
|
|
|
|
let sampleContent = `<div class="biometric-sample-name">
|
|
|
|
|
|
<div>${sample.type.charAt(0).toUpperCase() + sample.type.slice(1)} - ${sample.owner}</div>
|
|
|
|
|
|
<div class="biometric-sample-icons">`;
|
|
|
|
|
|
|
|
|
|
|
|
if (sample.isSpoofed) {
|
|
|
|
|
|
sampleContent += `<span class="biometric-sample-icon">🔄</span>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
sampleContent += `</div></div>`;
|
|
|
|
|
|
|
|
|
|
|
|
// Add quality bar
|
|
|
|
|
|
sampleContent += `<div class="biometric-quality-bar">
|
|
|
|
|
|
<div class="biometric-quality-fill" style="width: ${qualityPercentage}%; background: ${getQualityColor(sample.quality)};"></div>
|
|
|
|
|
|
</div>`;
|
|
|
|
|
|
|
|
|
|
|
|
sampleContent += `<div class="biometric-sample-details">
|
|
|
|
|
|
<strong>Type:</strong> ${sample.type}<br>
|
|
|
|
|
|
<strong>Owner:</strong> ${sample.owner}<br>
|
|
|
|
|
|
<strong>Quality:</strong> ${qualityPercentage}%<br>
|
|
|
|
|
|
<strong>ID:</strong> ${sample.id}
|
|
|
|
|
|
</div>`;
|
|
|
|
|
|
|
|
|
|
|
|
sampleContent += `<div class="biometric-sample-timestamp">Collected: ${formattedDate} ${formattedTime}</div>`;
|
|
|
|
|
|
|
|
|
|
|
|
sampleElement.innerHTML = sampleContent;
|
|
|
|
|
|
|
|
|
|
|
|
// Add spoof button if not already spoofed
|
|
|
|
|
|
if (!sample.isSpoofed && hasItemInInventory('spoofing_kit')) {
|
|
|
|
|
|
const spoofButton = document.createElement('button');
|
|
|
|
|
|
spoofButton.textContent = 'Create Spoof';
|
|
|
|
|
|
spoofButton.style.cssText = `
|
|
|
|
|
|
margin-top: 10px;
|
|
|
|
|
|
padding: 5px 10px;
|
|
|
|
|
|
background: #e74c3c;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
`;
|
|
|
|
|
|
spoofButton.onclick = (event) => {
|
|
|
|
|
|
event.stopPropagation(); // Prevent toggling expand/collapse
|
|
|
|
|
|
createSpoofedSampleUI(sample);
|
|
|
|
|
|
};
|
|
|
|
|
|
sampleElement.appendChild(spoofButton);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Toggle expanded state when clicked
|
|
|
|
|
|
sampleElement.addEventListener('click', () => {
|
|
|
|
|
|
sampleElement.classList.toggle('expanded');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
biometricsContent.appendChild(sampleElement);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Function to create spoofed sample with UI
|
|
|
|
|
|
function createSpoofedSampleUI(sample) {
|
|
|
|
|
|
// Find the sample element
|
|
|
|
|
|
const sampleElement = document.querySelector(`.biometric-sample[data-id="${sample.id}"]`);
|
|
|
|
|
|
if (!sampleElement) return;
|
|
|
|
|
|
|
|
|
|
|
|
// Disable any existing spoof buttons
|
|
|
|
|
|
const existingButton = sampleElement.querySelector('button');
|
|
|
|
|
|
if (existingButton) {
|
|
|
|
|
|
existingButton.disabled = true;
|
|
|
|
|
|
existingButton.textContent = 'Creating spoof...';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Add progress bar
|
|
|
|
|
|
const progressBar = document.createElement('div');
|
|
|
|
|
|
progressBar.className = 'biometric-quality-bar';
|
|
|
|
|
|
|
|
|
|
|
|
const progress = document.createElement('div');
|
|
|
|
|
|
progress.className = 'biometric-quality-fill';
|
|
|
|
|
|
progress.style.width = '0%';
|
|
|
|
|
|
progress.style.background = '#e67e22';
|
|
|
|
|
|
|
|
|
|
|
|
progressBar.appendChild(progress);
|
|
|
|
|
|
sampleElement.appendChild(progressBar);
|
|
|
|
|
|
|
|
|
|
|
|
// Animate progress
|
|
|
|
|
|
let currentProgress = 0;
|
|
|
|
|
|
const interval = setInterval(() => {
|
|
|
|
|
|
currentProgress += 2;
|
|
|
|
|
|
progress.style.width = `${currentProgress}%`;
|
|
|
|
|
|
}, SPOOFING_TIME / 50);
|
|
|
|
|
|
|
|
|
|
|
|
// Create spoof after delay
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
clearInterval(interval);
|
|
|
|
|
|
const spoofedSample = createSpoofedSample(sample);
|
|
|
|
|
|
if (spoofedSample) {
|
|
|
|
|
|
gameState.biometricSamples.push(spoofedSample);
|
|
|
|
|
|
updateBiometricsPanel(); // Refresh UI
|
|
|
|
|
|
updateBiometricsCount(); // Update count
|
|
|
|
|
|
gameAlert("Successfully created spoofed sample.", 'success', 'Spoofing Complete', 3000);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, SPOOFING_TIME);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Update the biometrics count
|
|
|
|
|
|
function updateBiometricsCount() {
|
|
|
|
|
|
const biometricsCount = document.getElementById('biometrics-count');
|
|
|
|
|
|
if (!biometricsCount) return;
|
|
|
|
|
|
|
|
|
|
|
|
// Count all samples
|
|
|
|
|
|
const sampleCount = gameState.biometricSamples ? gameState.biometricSamples.length : 0;
|
|
|
|
|
|
|
|
|
|
|
|
biometricsCount.textContent = sampleCount;
|
|
|
|
|
|
biometricsCount.style.display = sampleCount > 0 ? 'flex' : 'none';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Toggle the biometrics panel
|
|
|
|
|
|
function toggleBiometricsPanel() {
|
|
|
|
|
|
const biometricsPanel = document.getElementById('biometrics-panel');
|
|
|
|
|
|
const isVisible = biometricsPanel.style.display === 'block';
|
|
|
|
|
|
|
|
|
|
|
|
biometricsPanel.style.display = isVisible ? 'none' : 'block';
|
|
|
|
|
|
|
|
|
|
|
|
// Update panel on open
|
|
|
|
|
|
if (!isVisible) {
|
|
|
|
|
|
updateBiometricsPanel();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Initialize biometrics panel
|
|
|
|
|
|
function initializeBiometricsPanel() {
|
|
|
|
|
|
// Set up biometrics toggle button
|
|
|
|
|
|
const biometricsToggle = document.getElementById('biometrics-toggle');
|
|
|
|
|
|
biometricsToggle.addEventListener('click', toggleBiometricsPanel);
|
|
|
|
|
|
|
|
|
|
|
|
// Set up biometrics close button
|
|
|
|
|
|
const biometricsClose = document.getElementById('biometrics-close');
|
|
|
|
|
|
biometricsClose.addEventListener('click', toggleBiometricsPanel);
|
|
|
|
|
|
|
|
|
|
|
|
// Set up search functionality
|
|
|
|
|
|
const biometricsSearch = document.getElementById('biometrics-search');
|
|
|
|
|
|
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();
|
|
|
|
|
|
|
|
|
|
|
|
// Override B key functionality to open biometrics panel
|
|
|
|
|
|
// instead of the old popup
|
|
|
|
|
|
document.removeEventListener('keydown', setupSamplesUIControls);
|
|
|
|
|
|
document.addEventListener('keydown', (event) => {
|
|
|
|
|
|
if (event.key.toLowerCase() === 'b') {
|
|
|
|
|
|
toggleBiometricsPanel();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Override the existing methods
|
|
|
|
|
|
function showSamplesUI() {
|
|
|
|
|
|
toggleBiometricsPanel();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function hideSamplesUI() {
|
|
|
|
|
|
const biometricsPanel = document.getElementById('biometrics-panel');
|
|
|
|
|
|
biometricsPanel.style.display = 'none';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Override createSpoofedSample to add timestamp
|
|
|
|
|
|
const originalCreateSpoofedSample = createSpoofedSample;
|
|
|
|
|
|
createSpoofedSample = function(sample) {
|
|
|
|
|
|
const spoofedSample = originalCreateSpoofedSample(sample);
|
|
|
|
|
|
if (spoofedSample) {
|
|
|
|
|
|
spoofedSample.timestamp = Date.now();
|
|
|
|
|
|
}
|
|
|
|
|
|
return spoofedSample;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// We don't need this duplicate initialization since we already added it to the existing DOMContentLoaded handler
|
|
|
|
|
|
// document.addEventListener('DOMContentLoaded', function() {
|
|
|
|
|
|
// // Existing initialization...
|
|
|
|
|
|
//
|
|
|
|
|
|
// // Initialize biometrics panel
|
|
|
|
|
|
// initializeBiometricsPanel();
|
|
|
|
|
|
// });
|
|
|
|
|
|
|
|
|
|
|
|
// Function to drop an inventory item
|
|
|
|
|
|
function dropInventoryItem(item) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (!item || !item.scenarioData) return false;
|
|
|
|
|
|
|
|
|
|
|
|
// Get player position
|
|
|
|
|
|
const dropPos = {
|
|
|
|
|
|
x: player.x,
|
|
|
|
|
|
y: player.y
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Create a new sprite at the drop position
|
|
|
|
|
|
const droppedItem = createInteractiveSprite(
|
|
|
|
|
|
item.scenarioData,
|
|
|
|
|
|
dropPos.x,
|
|
|
|
|
|
dropPos.y,
|
|
|
|
|
|
currentRoom
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (!droppedItem) return false;
|
|
|
|
|
|
|
|
|
|
|
|
// Remove from inventory array
|
|
|
|
|
|
const itemIndex = inventory.items.indexOf(item);
|
|
|
|
|
|
if (itemIndex !== -1) {
|
|
|
|
|
|
inventory.items.splice(itemIndex, 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Remove from inventory container
|
|
|
|
|
|
inventory.container.remove(item);
|
|
|
|
|
|
|
|
|
|
|
|
// Destroy inventory sprite
|
|
|
|
|
|
item.destroy();
|
|
|
|
|
|
|
|
|
|
|
|
// Recalculate inventory positions
|
|
|
|
|
|
updateInventoryPositions();
|
|
|
|
|
|
|
|
|
|
|
|
// Hide bluetooth toggle if we dropped the bluetooth scanner
|
|
|
|
|
|
if (item.scenarioData.type === "bluetooth_scanner") {
|
|
|
|
|
|
const bluetoothToggle = document.getElementById('bluetooth-toggle');
|
|
|
|
|
|
bluetoothToggle.style.display = 'none';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Hide biometrics toggle if we dropped the fingerprint kit
|
|
|
|
|
|
if (item.scenarioData.type === "fingerprint_kit") {
|
|
|
|
|
|
const biometricsToggle = document.getElementById('biometrics-toggle');
|
|
|
|
|
|
biometricsToggle.style.display = 'none';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Error dropping item:', error);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-09 00:07:56 +00:00
|
|
|
|
// Function to initialize the toggle buttons container
|
|
|
|
|
|
function initializeToggleButtons() {
|
|
|
|
|
|
// Set up notes toggle button
|
|
|
|
|
|
const notesToggle = document.getElementById('notes-toggle');
|
2025-03-09 00:19:56 +00:00
|
|
|
|
if (notesToggle) {
|
|
|
|
|
|
notesToggle.addEventListener('click', toggleNotesPanel);
|
|
|
|
|
|
}
|
2025-03-09 00:07:56 +00:00
|
|
|
|
|
|
|
|
|
|
// Set up bluetooth toggle button
|
|
|
|
|
|
const bluetoothToggle = document.getElementById('bluetooth-toggle');
|
2025-03-09 00:19:56 +00:00
|
|
|
|
if (bluetoothToggle) {
|
|
|
|
|
|
bluetoothToggle.addEventListener('click', toggleBluetoothPanel);
|
|
|
|
|
|
}
|
2025-03-09 00:07:56 +00:00
|
|
|
|
|
|
|
|
|
|
// Set up biometrics toggle button
|
|
|
|
|
|
const biometricsToggle = document.getElementById('biometrics-toggle');
|
2025-03-09 00:19:56 +00:00
|
|
|
|
if (biometricsToggle) {
|
|
|
|
|
|
biometricsToggle.addEventListener('click', toggleBiometricsPanel);
|
|
|
|
|
|
}
|
2025-03-09 00:07:56 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Call the initialization function when the game starts
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
|
|
|
|
initializeToggleButtons();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2024-12-19 17:57:10 +00:00
|
|
|
|
</script>
|
|
|
|
|
|
</body>
|
2024-12-19 18:57:26 +00:00
|
|
|
|
</html>
|