mirror of
https://github.com/cliffe/BreakEscape.git
synced 2026-02-21 11:18:08 +00:00
Implemented CSS media queries to enhance responsiveness for mobile devices. Adjusted layout and font sizes for better visibility on portrait orientation with coarse pointers. Not perfect, but an improvement
7368 lines
308 KiB
HTML
7368 lines
308 KiB
HTML
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<title>Break Escape 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;
|
||
}
|
||
|
||
/* 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 {
|
||
position: relative;
|
||
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;
|
||
margin-left: 10px;
|
||
}
|
||
|
||
#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;
|
||
}
|
||
|
||
#laptop-popup {
|
||
display: none;
|
||
position: fixed;
|
||
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;
|
||
}
|
||
|
||
.laptop-frame {
|
||
background: #1a1a1a;
|
||
border-radius: 15px;
|
||
padding: 20px;
|
||
width: 100%;
|
||
height: 75%;
|
||
box-shadow: 0 0 30px rgba(0, 0, 0, 0.5);
|
||
margin-bottom: 80px;
|
||
pointer-events: auto;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
#cyberchef-container {
|
||
flex: 1;
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
#cyberchef-container iframe {
|
||
width: 100%;
|
||
height: 100%;
|
||
border: none;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
/* 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;
|
||
}
|
||
|
||
/* Bluetooth Signal Strength Bar */
|
||
.bluetooth-signal-bar-container {
|
||
margin: 0;
|
||
display: flex;
|
||
align-items: flex-end;
|
||
height: 16px;
|
||
}
|
||
|
||
.bluetooth-signal-bars {
|
||
display: flex;
|
||
align-items: flex-end;
|
||
height: 16px;
|
||
gap: 1px;
|
||
}
|
||
|
||
.bluetooth-signal-bar {
|
||
width: 3px;
|
||
background-color: #444;
|
||
border-radius: 1px;
|
||
transition: background-color 0.3s ease;
|
||
}
|
||
|
||
.bluetooth-signal-bar.active {
|
||
background-color: currentColor;
|
||
}
|
||
|
||
.bluetooth-signal-bar:nth-child(1) { height: 3px; }
|
||
.bluetooth-signal-bar:nth-child(2) { height: 6px; }
|
||
.bluetooth-signal-bar:nth-child(3) { height: 9px; }
|
||
.bluetooth-signal-bar:nth-child(4) { height: 12px; }
|
||
.bluetooth-signal-bar:nth-child(5) { height: 16px; }
|
||
|
||
.bluetooth-signal-text {
|
||
display: none;
|
||
}
|
||
|
||
#bluetooth-toggle {
|
||
position: relative;
|
||
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;
|
||
margin-left: 10px;
|
||
}
|
||
|
||
#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;
|
||
}
|
||
|
||
/* 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;
|
||
}
|
||
|
||
/* 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 {
|
||
pointer-events: auto;
|
||
}
|
||
|
||
/* Biometrics Panel */
|
||
#biometrics-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;
|
||
}
|
||
|
||
#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: 16px;
|
||
color: #2ecc71;
|
||
}
|
||
|
||
#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(52, 152, 219, 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: #2ecc71;
|
||
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 {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
font-weight: bold;
|
||
margin-bottom: 5px;
|
||
font-size: 14px;
|
||
color: #2ecc71;
|
||
}
|
||
|
||
.biometric-sample-icons {
|
||
display: flex;
|
||
gap: 5px;
|
||
}
|
||
|
||
.biometric-quality-bar {
|
||
height: 5px;
|
||
background-color: #333;
|
||
margin-top: 8px;
|
||
border-radius: 2px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.biometric-quality-fill {
|
||
height: 100%;
|
||
border-radius: 2px;
|
||
transition: width 0.3s ease;
|
||
}
|
||
|
||
.biometric-sample-details {
|
||
font-size: 13px;
|
||
line-height: 1.4;
|
||
white-space: pre-wrap;
|
||
max-height: 0;
|
||
overflow: hidden;
|
||
transition: max-height 0.3s;
|
||
}
|
||
|
||
.biometric-sample.expanded .biometric-sample-details {
|
||
max-height: 200px;
|
||
}
|
||
|
||
.biometric-sample-timestamp {
|
||
font-size: 11px;
|
||
color: #888;
|
||
margin-top: 5px;
|
||
text-align: right;
|
||
display: none;
|
||
}
|
||
|
||
/* Rest of existing styles follow */
|
||
.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;
|
||
}
|
||
|
||
/* Toggle Buttons */
|
||
#biometrics-toggle {
|
||
position: relative;
|
||
width: 60px;
|
||
height: 60px;
|
||
background-color: #2ecc71;
|
||
color: white;
|
||
/* border-radius: 50%; */
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
cursor: pointer;
|
||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
||
z-index: 1998;
|
||
font-size: 28px;
|
||
transition: all 0.3s ease;
|
||
margin-left: 10px;
|
||
}
|
||
|
||
#biometrics-toggle:hover {
|
||
background-color: #27ae60;
|
||
/* transform: scale(1.1); */
|
||
}
|
||
|
||
#biometrics-count {
|
||
position: absolute;
|
||
top: -5px;
|
||
right: -5px;
|
||
background-color: #e74c3c;
|
||
color: white;
|
||
border-radius: 50%;
|
||
width: 24px;
|
||
height: 24px;
|
||
font-size: 14px;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.tension-control {
|
||
display: grid;
|
||
grid-template-columns: auto 1fr;
|
||
gap: 20px;
|
||
align-items: center;
|
||
background: #333;
|
||
padding: 20px;
|
||
border-radius: 5px;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.tension-wrench-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 10px;
|
||
position: relative;
|
||
width: 150px;
|
||
height: 60px;
|
||
}
|
||
|
||
.tension-track {
|
||
width: 100%;
|
||
height: 10px;
|
||
background: #444;
|
||
border-radius: 5px;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.tension-progress {
|
||
position: absolute;
|
||
height: 100%;
|
||
width: 0%;
|
||
background: linear-gradient(to right, #666, #2196F3);
|
||
transition: width 0.3s;
|
||
}
|
||
|
||
.tension-status {
|
||
font-size: 16px;
|
||
text-align: left;
|
||
padding-left: 10px;
|
||
}
|
||
|
||
.tension-wrench {
|
||
width: 60px;
|
||
height: 40px;
|
||
background: #666;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: transform 0.3s, background-color 0.3s;
|
||
position: absolute;
|
||
left: 0;
|
||
top: 20px;
|
||
z-index: 2;
|
||
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
|
||
}
|
||
|
||
.tension-wrench:hover {
|
||
background: #777;
|
||
}
|
||
|
||
.tension-wrench.active {
|
||
background: #2196F3;
|
||
}
|
||
|
||
.wrench-handle {
|
||
width: 60%;
|
||
height: 10px;
|
||
background: #999;
|
||
position: absolute;
|
||
}
|
||
|
||
.wrench-tip {
|
||
width: 20px;
|
||
height: 30px;
|
||
background: #999;
|
||
position: absolute;
|
||
left: 5px;
|
||
}
|
||
|
||
.cylinder {
|
||
height: 20px;
|
||
margin-top: -5px;
|
||
}
|
||
|
||
.lock-visual {
|
||
display: flex;
|
||
justify-content: space-evenly;
|
||
align-items: center;
|
||
gap: 10px;
|
||
height: 160px;
|
||
background: #f0e6a6; /* Light yellow/beige background */
|
||
border-radius: 5px;
|
||
padding: 15px;
|
||
position: relative;
|
||
margin-bottom: 10px;
|
||
border: 2px solid #887722;
|
||
}
|
||
|
||
.pin {
|
||
width: 30px;
|
||
height: 110px;
|
||
position: relative;
|
||
background: transparent;
|
||
border-radius: 4px 4px 0 0;
|
||
overflow: visible;
|
||
cursor: pointer;
|
||
transition: transform 0.1s;
|
||
margin: 0 5px;
|
||
}
|
||
|
||
.pin:hover {
|
||
opacity: 0.9;
|
||
}
|
||
|
||
.shear-line {
|
||
position: absolute;
|
||
width: 100%;
|
||
height: 2px;
|
||
background: #aa8833;
|
||
bottom: 50px;
|
||
z-index: 5;
|
||
}
|
||
|
||
.key-pin {
|
||
position: absolute;
|
||
bottom: 0;
|
||
width: 100%;
|
||
height: 0px;
|
||
background: #dd3333; /* Red for key pins */
|
||
transition: height 0.05s;
|
||
border-radius: 0 0 0 0;
|
||
clip-path: polygon(0 0, 100% 0, 100% 70%, 50% 100%, 0 70%); /* Pointed bottom */
|
||
}
|
||
|
||
.driver-pin {
|
||
position: absolute;
|
||
width: 100%;
|
||
height: 50px;
|
||
background: #3388dd; /* Blue for driver pins */
|
||
transition: bottom 0.05s;
|
||
bottom: 50px;
|
||
border-radius: 0 0 0 0;
|
||
}
|
||
|
||
.spring {
|
||
position: absolute;
|
||
bottom: 100px;
|
||
width: 100%;
|
||
height: 25px;
|
||
background: linear-gradient(to bottom,
|
||
#cccccc 0%, #cccccc 20%,
|
||
#999999 20%, #999999 25%,
|
||
#cccccc 25%, #cccccc 40%,
|
||
#999999 40%, #999999 45%,
|
||
#cccccc 45%, #cccccc 60%,
|
||
#999999 60%, #999999 65%,
|
||
#cccccc 65%, #cccccc 80%,
|
||
#999999 80%, #999999 85%,
|
||
#cccccc 85%, #cccccc 100%
|
||
);
|
||
transition: height 0.05s;
|
||
}
|
||
|
||
.pin.binding {
|
||
box-shadow: 0 0 8px 2px #ffcc00;
|
||
}
|
||
|
||
.pin.set .driver-pin {
|
||
bottom: 52px; /* Just above shear line */
|
||
background: #22aa22; /* Green to indicate set */
|
||
}
|
||
|
||
.pin.set .key-pin {
|
||
height: 49px; /* Just below shear line */
|
||
background: #22aa22; /* Green to indicate set */
|
||
clip-path: polygon(0 0, 100% 0, 100% 70%, 50% 100%, 0 70%);
|
||
}
|
||
|
||
.cylinder {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
width: 100%;
|
||
height: 30px;
|
||
background: #ddbb77;
|
||
border-radius: 5px;
|
||
margin-top: 5px;
|
||
position: relative;
|
||
z-index: 0;
|
||
border: 2px solid #887722;
|
||
}
|
||
|
||
.cylinder-inner {
|
||
width: 80%;
|
||
height: 20px;
|
||
background: #ccaa66;
|
||
border-radius: 3px;
|
||
transform-origin: center;
|
||
transition: transform 0.3s;
|
||
}
|
||
|
||
.cylinder.rotated .cylinder-inner {
|
||
transform: rotate(15deg);
|
||
}
|
||
|
||
.lockpick-feedback {
|
||
padding: 15px;
|
||
background: #333;
|
||
border-radius: 5px;
|
||
text-align: center;
|
||
min-height: 30px;
|
||
margin-top: 20px;
|
||
font-size: 16px;
|
||
}
|
||
|
||
#game-container {
|
||
transform: scale(1.25); /* Adjust the scale factor as needed */
|
||
transform-origin: center; /* Set the origin for scaling */
|
||
}
|
||
|
||
/* Mobile device detection and responsive scaling */
|
||
@media (orientation: portrait) and (pointer: coarse) {
|
||
html, body {
|
||
width: 100%;
|
||
height: 100%;
|
||
overflow-x: hidden;
|
||
transform-origin: top left;
|
||
}
|
||
#game-container {
|
||
max-width: 100vw;
|
||
overflow: visible;
|
||
}
|
||
/* Increase font sizes by percentage */
|
||
.notification-title, .notification-message, .note-title, .bluetooth-device-name, .biometric-sample-name, .note-text, .bluetooth-device-details, .biometric-sample-details, #notes-title, #bluetooth-title, #biometrics-title, #notes-close, #bluetooth-close, #biometrics-close, .notification-close {
|
||
font-size: 200%;
|
||
}
|
||
}
|
||
</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>
|
||
|
||
<!-- 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...">
|
||
</div>
|
||
<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>
|
||
|
||
<!-- Toggle Buttons Container -->
|
||
<div id="toggle-buttons-container">
|
||
<div id="notes-toggle">
|
||
<img src="assets/objects/notes.png" alt="Notes" style="width: 64px; height: 64px;">
|
||
<div id="notes-count">0</div>
|
||
</div>
|
||
<div id="bluetooth-toggle" style="display: none;">
|
||
<img src="assets/objects/bluetooth_scanner.png" alt="Bluetooth" style="width: 64px; height: 64px;">
|
||
<div id="bluetooth-count">0</div>
|
||
</div>
|
||
<div id="biometrics-toggle" style="display: none;">
|
||
<img src="assets/objects/fingerprint.png" alt="Biometrics" style="width: 64px; height: 64px;">
|
||
<div id="biometrics-count">0</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Bluetooth Scanner Panel -->
|
||
<div id="bluetooth-panel">
|
||
<div id="bluetooth-header">
|
||
<div id="bluetooth-title">Bluetooth Scanner</div>
|
||
<div id="bluetooth-close">×</div>
|
||
</div>
|
||
<div id="bluetooth-search-container">
|
||
<input type="text" id="bluetooth-search" placeholder="Search devices...">
|
||
</div>
|
||
<div id="bluetooth-categories">
|
||
<div class="bluetooth-category active" data-category="all">All</div>
|
||
<div class="bluetooth-category" data-category="nearby">Nearby</div>
|
||
<div class="bluetooth-category" data-category="saved">Saved</div>
|
||
</div>
|
||
<div id="bluetooth-content"></div>
|
||
</div>
|
||
|
||
<!-- Biometrics Panel -->
|
||
<div id="biometrics-panel">
|
||
<div id="biometrics-header">
|
||
<div id="biometrics-title">Biometric Samples</div>
|
||
<div id="biometrics-close">×</div>
|
||
</div>
|
||
<div id="biometrics-search-container">
|
||
<input type="text" id="biometrics-search" placeholder="Search samples...">
|
||
</div>
|
||
<div id="biometrics-categories">
|
||
<div class="biometrics-category active" data-category="all">All</div>
|
||
<div class="biometrics-category" data-category="fingerprint">Fingerprints</div>
|
||
</div>
|
||
<div id="biometrics-content"></div>
|
||
</div>
|
||
|
||
<script>
|
||
const config = {
|
||
type: Phaser.AUTO,
|
||
width: window.innerWidth * 0.80, // Set width to 75% of the window (for scaling)
|
||
height: window.innerHeight * 0.80, // Set height to 75% of the window (for scaling)
|
||
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;
|
||
const INVENTORY_X_OFFSET = 50;
|
||
const INVENTORY_Y_OFFSET = 50;
|
||
const CLICK_INDICATOR_DURATION = 800; // milliseconds
|
||
const CLICK_INDICATOR_SIZE = 20; // pixels
|
||
const PLAYER_FEET_OFFSET_Y = 30; // Adjust based on your sprite's feet position
|
||
|
||
// Hide rooms initially and on exit
|
||
const hideRoomsInitially = true;
|
||
const hideRoomsOnExit = false;
|
||
const hideNonAdjacentRooms = false;
|
||
|
||
// 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 existingNote = gameNotes.find(note => note.title === title && note.text === text);
|
||
|
||
// If the note already exists, don't add it again but mark it as read
|
||
if (existingNote) {
|
||
debugLog(`Note "${title}" already exists, not adding duplicate`, existingNote, 2);
|
||
|
||
// Mark as read if it wasn't already
|
||
if (!existingNote.read) {
|
||
existingNote.read = true;
|
||
updateNotesPanel();
|
||
updateNotesCount();
|
||
}
|
||
|
||
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();
|
||
|
||
// Initialize Bluetooth scanner panel
|
||
initializeBluetoothPanel();
|
||
|
||
// Initialize biometrics panel
|
||
initializeBiometricsPanel();
|
||
});
|
||
|
||
// Function to create or update the FPS counter
|
||
function updateFPSCounter() {
|
||
if (fpsCounter) {
|
||
fpsCounter.textContent = `FPS: ${Math.round(game.loop.actualFps)}`;
|
||
}
|
||
}
|
||
|
||
// 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;
|
||
|
||
// Bluetooth constants
|
||
const BLUETOOTH_SCAN_RANGE = TILE_SIZE * 2; // 2 tiles range for Bluetooth scanning
|
||
let lastBluetoothScan = 0; // Track last scan time
|
||
const BLUETOOTH_SCAN_INTERVAL = 200; // Scan every 200ms for more responsive updates
|
||
|
||
const gameState = {
|
||
biometricSamples: [],
|
||
inventory: inventory
|
||
};
|
||
|
||
// 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 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');
|
||
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');
|
||
|
||
this.load.image('room_reception_l', 'assets/rooms/room_reception_l.png');
|
||
this.load.image('room_office_l', 'assets/rooms/room_office_l.png');
|
||
this.load.image('room_server_l', 'assets/rooms/room_server_l.png');
|
||
this.load.image('room_ceo_l', 'assets/rooms/room_ceo_l.png');
|
||
this.load.image('room_spooky_basement_l', 'assets/rooms/room_spooky_basement_l.png');
|
||
this.load.image('door', 'assets/tiles/door.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');
|
||
this.load.image('workstation', 'assets/objects/workstation.png');
|
||
this.load.image('bluetooth_scanner', 'assets/objects/bluetooth_scanner.png');
|
||
this.load.image('tablet', 'assets/objects/tablet.png');
|
||
this.load.image('fingerprint_kit', 'assets/objects/fingerprint_kit.png');
|
||
this.load.image('lockpick', 'assets/objects/lockpick.png');
|
||
this.load.image('spoofing_kit', 'assets/objects/spoofing_kit.png');
|
||
|
||
// Load character sprite sheet instead of single image
|
||
this.load.spritesheet('hacker', 'assets/characters/hacker.png', {
|
||
frameWidth: 64,
|
||
frameHeight: 64
|
||
});
|
||
|
||
// Get scenario from URL parameter or use default
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const scenarioFile = urlParams.get('scenario') || 'assets/scenarios/ceo_exfil.json';
|
||
|
||
// Load the specified scenario
|
||
this.load.json('gameScenarioJSON', scenarioFile);
|
||
gameScenario = this.cache.json.get('gameScenarioJSON');
|
||
}
|
||
|
||
// creates the workstation
|
||
function createCryptoWorkstation(objectData) {
|
||
// Create the workstation sprite
|
||
const workstationSprite = this.add.sprite(0, 0, 'workstation');
|
||
workstationSprite.setVisible(false);
|
||
workstationSprite.name = "workstation";
|
||
workstationSprite.scenarioData = objectData;
|
||
workstationSprite.setInteractive({ useHandCursor: true });
|
||
|
||
return workstationSprite;
|
||
}
|
||
|
||
// 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 as sprite
|
||
player = this.add.sprite(250, 200, 'hacker', 20);
|
||
this.physics.add.existing(player);
|
||
|
||
// Scale the character up by 25%
|
||
player.setScale(1.25);
|
||
|
||
// Set smaller collision box at the feet
|
||
player.body.setSize(15, 10);
|
||
player.body.setOffset(25, 50); // Adjusted offset to account for scaling
|
||
|
||
player.body.setCollideWorldBounds(true);
|
||
player.body.setBounce(0);
|
||
player.body.setDrag(0);
|
||
player.body.setFriction(0);
|
||
|
||
// // Enable physics debug to see the collision box
|
||
// this.physics.world.debugGraphic.clear();
|
||
// this.physics.world.drawDebug = true;
|
||
|
||
// // Force debug display to be visible and on top
|
||
// this.physics.world.debugGraphic.setVisible(true);
|
||
// this.physics.world.debugGraphic.setDepth(9999);
|
||
|
||
// Set player depth to ensure it renders above most objects
|
||
player.setDepth(2000);
|
||
|
||
// Track player direction and movement state
|
||
player.direction = 'down'; // Initial direction
|
||
player.isMoving = false;
|
||
player.lastDirection = 'down';
|
||
|
||
// Create animations for each direction
|
||
this.anims.create({
|
||
key: 'walk-right',
|
||
frames: this.anims.generateFrameNumbers('hacker', { start: 1, end: 4 }),
|
||
frameRate: 8,
|
||
repeat: -1
|
||
});
|
||
|
||
this.anims.create({
|
||
key: 'walk-down',
|
||
frames: this.anims.generateFrameNumbers('hacker', { start: 6, end: 9 }),
|
||
frameRate: 8,
|
||
repeat: -1
|
||
});
|
||
|
||
this.anims.create({
|
||
key: 'walk-up',
|
||
frames: this.anims.generateFrameNumbers('hacker', { start: 11, end: 14 }),
|
||
frameRate: 8,
|
||
repeat: -1
|
||
});
|
||
|
||
this.anims.create({
|
||
key: 'walk-up-right',
|
||
frames: this.anims.generateFrameNumbers('hacker', { start: 16, end: 19 }),
|
||
frameRate: 8,
|
||
repeat: -1
|
||
});
|
||
|
||
this.anims.create({
|
||
key: 'walk-down-right',
|
||
frames: this.anims.generateFrameNumbers('hacker', { start: 21, end: 24 }),
|
||
frameRate: 8,
|
||
repeat: -1
|
||
});
|
||
|
||
// Create idle frames (first frame of each row)
|
||
this.anims.create({
|
||
key: 'idle-right',
|
||
frames: [{ key: 'hacker', frame: 0 }],
|
||
frameRate: 1
|
||
});
|
||
|
||
this.anims.create({
|
||
key: 'idle-down',
|
||
frames: [{ key: 'hacker', frame: 5 }],
|
||
frameRate: 1
|
||
});
|
||
|
||
this.anims.create({
|
||
key: 'idle-up',
|
||
frames: [{ key: 'hacker', frame: 10 }],
|
||
frameRate: 1
|
||
});
|
||
|
||
this.anims.create({
|
||
key: 'idle-up-right',
|
||
frames: [{ key: 'hacker', frame: 15 }],
|
||
frameRate: 1
|
||
});
|
||
|
||
this.anims.create({
|
||
key: 'idle-down-right',
|
||
frames: [{ key: 'hacker', frame: 20 }],
|
||
frameRate: 1
|
||
});
|
||
|
||
// 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);
|
||
|
||
// Add this new call after all rooms are created
|
||
processAllDoorCollisions.call(this);
|
||
|
||
// Initialize pathfinder
|
||
initializePathfinder.call(this);
|
||
|
||
// Initialize game systems
|
||
initializeInventory.call(this);
|
||
|
||
// Process items marked with inInventory: true in the scenario data
|
||
processInitialInventoryItems.call(this);
|
||
|
||
// NOTE: Crypto workstation is now handled by processInitialInventoryItems
|
||
// based on inInventory flag in scenario data - addCryptoWorkstation.call(this);
|
||
|
||
|
||
// Setup input with proper context
|
||
this.input.on('pointerdown', (pointer) => {
|
||
// Convert pointer position to world coordinates
|
||
|
||
// Calculate the inventory area based on the container's position and size
|
||
const inventoryBounds = inventory.container.getBounds();
|
||
|
||
if (pointer.y > inventoryBounds.y && pointer.y < inventoryBounds.y + inventoryBounds.height &&
|
||
pointer.x > inventoryBounds.x && pointer.x < inventoryBounds.x + inventoryBounds.width) {
|
||
// 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) {
|
||
debugLog('INVENTORY ITEM CLICKED', { name: clickedItem.name }, 2);
|
||
handleObjectInteraction(clickedItem);
|
||
return;
|
||
}
|
||
}
|
||
|
||
// if not clicking inventory, handle as movement
|
||
debugLog('CLICK DETECTED', { x: pointer.worldX, y: pointer.worldY }, 3);
|
||
movePlayerToPoint.call(this, pointer.worldX, pointer.worldY);
|
||
});
|
||
|
||
// 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);
|
||
|
||
// 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;");
|
||
}
|
||
|
||
function update() {
|
||
// Make sure debug is always enabled
|
||
// this.physics.world.drawDebug = true;
|
||
|
||
// updates the player's movement
|
||
updatePlayerMovement.call(this);
|
||
|
||
// checks for object interactions
|
||
checkObjectInteractions.call(this);
|
||
|
||
// checks for room transitions
|
||
checkRoomTransitions.call(this);
|
||
|
||
// Check for Bluetooth devices
|
||
const currentTime = this.time.now;
|
||
if (currentTime - lastBluetoothScan >= BLUETOOTH_SCAN_INTERVAL) {
|
||
checkBluetoothDevices.call(this);
|
||
lastBluetoothScan = currentTime;
|
||
}
|
||
|
||
// 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);
|
||
|
||
// Add scenario brief as an important note
|
||
addNote("Mission Brief", gameScenario.scenario_brief, true);
|
||
|
||
// Show notification
|
||
gameAlert(gameScenario.scenario_brief, 'info', 'Mission Brief', 0);
|
||
}
|
||
|
||
// 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
|
||
};
|
||
debugLog('ROOM DIMENSIONS', { roomId, dimensions: roomDimensions[roomId] }, 3);
|
||
} 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)) {
|
||
debugLog('ROOM ALREADY PROCESSED', { roomId: connected }, 3);
|
||
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 = {};
|
||
|
||
// Create a map of room objects by type for easy lookup
|
||
const roomObjectsByType = {};
|
||
objectsLayer.objects.forEach(obj => {
|
||
if (!roomObjectsByType[obj.name]) {
|
||
roomObjectsByType[obj.name] = [];
|
||
}
|
||
roomObjectsByType[obj.name].push(obj);
|
||
});
|
||
|
||
// Process scenario objects first
|
||
if (gameScenario.rooms[roomId].objects) {
|
||
gameScenario.rooms[roomId].objects.forEach((scenarioObj, index) => {
|
||
const objType = scenarioObj.type;
|
||
// skip "inInventory": true,
|
||
if (scenarioObj.inInventory) {
|
||
return;
|
||
}
|
||
|
||
// Try to find a matching room object
|
||
let roomObj = null;
|
||
if (roomObjectsByType[objType] && roomObjectsByType[objType].length > 0) {
|
||
// Take the first available room object of this type
|
||
roomObj = roomObjectsByType[objType].shift();
|
||
}
|
||
|
||
let sprite;
|
||
|
||
if (roomObj) {
|
||
// Create sprite at the room object's position
|
||
sprite = this.add.sprite(
|
||
position.x + roomObj.x,
|
||
position.y + (roomObj.gid !== undefined ? roomObj.y - roomObj.height : roomObj.y),
|
||
objType
|
||
);
|
||
|
||
if (roomObj.rotation) {
|
||
sprite.setRotation(Phaser.Math.DegToRad(roomObj.rotation));
|
||
}
|
||
|
||
// Create a unique key using the room object's ID
|
||
sprite.objectId = `${objType}_${roomObj.id || index}`;
|
||
} else {
|
||
// No matching room object, create at random position
|
||
// Assuming room size is 10x9 tiles of 48px each
|
||
const roomWidth = 10 * 48;
|
||
const roomHeight = 9 * 48;
|
||
|
||
// Add some padding from the edges (2 tile width)
|
||
const padding = 48*2;
|
||
|
||
const randomX = position.x + padding + Math.random() * (roomWidth - padding * 2);
|
||
const randomY = position.y + padding + Math.random() * (roomHeight - padding * 2);
|
||
|
||
sprite = this.add.sprite(randomX, randomY, objType);
|
||
console.log(`Created object ${objType} at random position (${randomX}, ${randomY})`);
|
||
}
|
||
|
||
// SIMPLIFIED NAMING APPROACH
|
||
// Use a consistent format: roomId_type_index
|
||
const objectId = `${roomId}_${objType}_${index}`;
|
||
|
||
// Set common properties
|
||
sprite.setOrigin(0, 0);
|
||
sprite.name = objType; // Keep name as the object type for texture loading
|
||
sprite.objectId = objectId; // Use our simplified ID format
|
||
sprite.setInteractive({ useHandCursor: true });
|
||
sprite.setDepth(1001);
|
||
sprite.originalAlpha = 1;
|
||
sprite.active = true;
|
||
|
||
// Store scenario data with sprite
|
||
sprite.scenarioData = scenarioObj;
|
||
|
||
// Initially hide the object
|
||
sprite.setVisible(false);
|
||
|
||
// Store the object
|
||
rooms[roomId].objects[objectId] = sprite;
|
||
|
||
// Add click handler
|
||
sprite.on('pointerdown', () => {
|
||
debugLog('OBJECT CLICKED', { name: objType, id: objectId }, 2);
|
||
handleObjectInteraction(sprite);
|
||
});
|
||
});
|
||
}
|
||
}
|
||
} 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);
|
||
|
||
// Create click indicator
|
||
createClickIndicator.call(this, x, y);
|
||
|
||
targetPoint = { x, y };
|
||
isMoving = true;
|
||
}
|
||
|
||
// Add this new function to create a visual click indicator
|
||
function createClickIndicator(x, y) {
|
||
// Create a circle at the click position
|
||
const indicator = this.add.circle(x, y, CLICK_INDICATOR_SIZE, 0xffffff, 0.7);
|
||
indicator.setDepth(1000); // Above ground but below player
|
||
|
||
// Add a pulsing animation
|
||
this.tweens.add({
|
||
targets: indicator,
|
||
scale: { from: 0.5, to: 1.5 },
|
||
alpha: { from: 0.7, to: 0 },
|
||
duration: CLICK_INDICATOR_DURATION,
|
||
ease: 'Sine.easeOut',
|
||
onComplete: () => {
|
||
indicator.destroy();
|
||
}
|
||
});
|
||
|
||
|
||
}
|
||
|
||
// 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);
|
||
player.isMoving = false;
|
||
|
||
// Play idle animation based on last direction
|
||
player.anims.play(`idle-${player.direction}`, true);
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Cache player position - adjust for feet position
|
||
const px = player.x;
|
||
const py = player.y + PLAYER_FEET_OFFSET_Y; // Add offset to target the feet
|
||
|
||
// Use squared distance for performance
|
||
const dx = targetPoint.x - px;
|
||
const dy = targetPoint.y - py; // Compare with feet position
|
||
const distanceSq = dx * dx + dy * dy;
|
||
|
||
// Reached target point
|
||
if (distanceSq < ARRIVAL_THRESHOLD * ARRIVAL_THRESHOLD) {
|
||
isMoving = false;
|
||
player.body.setVelocity(0, 0);
|
||
player.isMoving = false;
|
||
|
||
// Play idle animation based on last direction
|
||
player.anims.play(`idle-${player.direction}`, true);
|
||
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 - PLAYER_FEET_OFFSET_Y; // Store actual player position
|
||
}
|
||
|
||
// Normalize movement vector for consistent speed
|
||
const distance = Math.sqrt(distanceSq);
|
||
const velocityX = (dx / distance) * MOVEMENT_SPEED;
|
||
const velocityY = (dy / distance) * MOVEMENT_SPEED;
|
||
|
||
// Set velocity directly without checking for changes
|
||
player.body.setVelocity(velocityX, velocityY);
|
||
|
||
// Determine direction based on velocity
|
||
const absVX = Math.abs(velocityX);
|
||
const absVY = Math.abs(velocityY);
|
||
|
||
// Set player direction and animation
|
||
if (absVX > absVY * 2) {
|
||
// Mostly horizontal movement
|
||
player.direction = velocityX > 0 ? 'right' : 'right'; // Use right animation but flip
|
||
player.setFlipX(velocityX < 0); // Flip sprite horizontally if moving left
|
||
} else if (absVY > absVX * 2) {
|
||
// Mostly vertical movement
|
||
player.direction = velocityY > 0 ? 'down' : 'up';
|
||
player.setFlipX(false);
|
||
} else {
|
||
// Diagonal movement
|
||
if (velocityY > 0) {
|
||
player.direction = 'down-right';
|
||
} else {
|
||
player.direction = 'up-right';
|
||
}
|
||
player.setFlipX(velocityX < 0); // Flip sprite horizontally if moving left
|
||
}
|
||
|
||
// Play appropriate animation if not already playing
|
||
if (!player.isMoving || player.lastDirection !== player.direction) {
|
||
player.anims.play(`walk-${player.direction}`, true);
|
||
player.isMoving = true;
|
||
player.lastDirection = player.direction;
|
||
}
|
||
|
||
// Stop if collision detected
|
||
if (player.body.blocked.none === false) {
|
||
isMoving = false;
|
||
player.body.setVelocity(0, 0);
|
||
player.isMoving = false;
|
||
player.anims.play(`idle-${player.direction}`, true);
|
||
}
|
||
}
|
||
|
||
// 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) {
|
||
// Use the player's physics body bounds for more precise detection
|
||
const playerBody = player.body;
|
||
const playerBounds = {
|
||
left: playerBody.x,
|
||
right: playerBody.x + playerBody.width,
|
||
top: playerBody.y,
|
||
bottom: playerBody.y + playerBody.height
|
||
};
|
||
|
||
// Calculate the overlap area between player and room
|
||
const overlapWidth = Math.min(playerBounds.right, bounds.x + bounds.width) -
|
||
Math.max(playerBounds.left, bounds.x);
|
||
const overlapHeight = Math.min(playerBounds.bottom, bounds.y + bounds.height) -
|
||
Math.max(playerBounds.top, bounds.y);
|
||
|
||
// Require a minimum overlap percentage (e.g., 50% of player width/height)
|
||
const minOverlapPercent = 0.5;
|
||
const playerWidth = playerBounds.right - playerBounds.left;
|
||
const playerHeight = playerBounds.bottom - playerBounds.top;
|
||
|
||
const widthOverlapPercent = overlapWidth / playerWidth;
|
||
const heightOverlapPercent = overlapHeight / playerHeight;
|
||
|
||
return overlapWidth > 0 &&
|
||
overlapHeight > 0 &&
|
||
widthOverlapPercent >= minOverlapPercent &&
|
||
heightOverlapPercent >= minOverlapPercent;
|
||
}
|
||
|
||
// 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) {
|
||
// Only log detailed object interactions at debug level 2+
|
||
debugLog('OBJECT INTERACTION', {
|
||
name: sprite.name,
|
||
id: sprite.objectId,
|
||
scenarioData: sprite.scenarioData
|
||
}, 2);
|
||
|
||
if (!sprite || !sprite.scenarioData) {
|
||
console.warn('Invalid sprite or missing scenario data');
|
||
return;
|
||
}
|
||
|
||
// Log the full object data to help debug
|
||
console.log("Interacting with object:", sprite.scenarioData);
|
||
|
||
// Handle the Crypto Workstation
|
||
if (sprite.scenarioData.type === "workstation") {
|
||
openCryptoWorkstation();
|
||
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) {
|
||
// Player is too far away to interact
|
||
debugLog('INTERACTION_OUT_OF_RANGE', {
|
||
objectName: sprite.name,
|
||
objectId: sprite.objectId,
|
||
distance: Math.sqrt(distanceSq),
|
||
maxRange: Math.sqrt(INTERACTION_RANGE_SQ)
|
||
}, 2);
|
||
return;
|
||
}
|
||
}
|
||
|
||
const data = sprite.scenarioData;
|
||
|
||
// 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
|
||
}
|
||
}
|
||
}
|
||
|
||
// 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`;
|
||
data.contents.forEach(item => {
|
||
message += `- ${item.name}\n`;
|
||
});
|
||
|
||
// Show notification instead of alert
|
||
gameAlert(message, 'success', 'Items Found', 5000);
|
||
|
||
// Add all contents to inventory
|
||
data.contents.forEach(item => {
|
||
const contentSprite = createInventorySprite({
|
||
...item,
|
||
type: item.type.toLowerCase()
|
||
});
|
||
if (contentSprite) {
|
||
addToInventory(contentSprite);
|
||
}
|
||
});
|
||
|
||
// Clear contents after adding to inventory
|
||
data.contents = [];
|
||
data.isUnlockedButNotCollected = false;
|
||
return;
|
||
}
|
||
|
||
// Check if item is locked
|
||
if (data.locked === true) {
|
||
debugLog('ITEM LOCKED', data, 2);
|
||
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`;
|
||
|
||
// 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);
|
||
}
|
||
} else {
|
||
// If the note was a duplicate and it's a note in the room (not inventory),
|
||
// still remove it from the room even though we didn't add it to notes again
|
||
if (!isInventoryItem && data.type === 'notes' && currentRoom &&
|
||
rooms[currentRoom] && rooms[currentRoom].objects &&
|
||
rooms[currentRoom].objects[sprite.name]) {
|
||
|
||
const roomObj = rooms[currentRoom].objects[sprite.name];
|
||
roomObj.setVisible(false);
|
||
roomObj.active = false;
|
||
debugLog(`Note "${data.name}" was a duplicate, but still removed from room`, 2);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
console.log("Interacting with object:", data);
|
||
|
||
if (data.takeable) {
|
||
// 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.hasNoSpecialPurpose; // Add this flag to notes that need to be in inventory
|
||
|
||
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.objectId];
|
||
|
||
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.objectId]) {
|
||
|
||
const roomObj = rooms[currentRoom].objects[sprite.objectId];
|
||
roomObj.setVisible(false);
|
||
roomObj.active = false;
|
||
|
||
// Also set the internal property to indicate it's been collected
|
||
sprite.collected = true;
|
||
|
||
// Show notification about adding to notes instead of inventory
|
||
gameAlert(`Information recorded in your notes.`, 'success', 'Note Recorded', 3000);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Show notification instead of alert
|
||
gameAlert(message, 'info', data.name, 0);
|
||
}
|
||
|
||
// 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 {
|
||
// Debug logging
|
||
console.log("Trying to add to inventory:", {
|
||
objectId: sprite.objectId,
|
||
name: sprite.name,
|
||
type: sprite.scenarioData?.type,
|
||
currentRoom: currentRoom
|
||
});
|
||
|
||
// Check if the item is already in the inventory
|
||
const itemIdentifier = createItemIdentifier(sprite.scenarioData);
|
||
const isAlreadyInInventory = inventory.items.some(item =>
|
||
createItemIdentifier(item.scenarioData) === itemIdentifier
|
||
);
|
||
|
||
if (isAlreadyInInventory) {
|
||
console.log(`Item ${itemIdentifier} is already in inventory`);
|
||
return false;
|
||
}
|
||
|
||
// Remove from room if it exists
|
||
if (currentRoom && rooms[currentRoom] && rooms[currentRoom].objects) {
|
||
// Try to find by objectId first
|
||
if (rooms[currentRoom].objects[sprite.objectId]) {
|
||
const roomObj = rooms[currentRoom].objects[sprite.objectId];
|
||
roomObj.setVisible(false);
|
||
roomObj.active = false;
|
||
console.log(`Removed object ${sprite.objectId} from room`);
|
||
}
|
||
}
|
||
|
||
sprite.setVisible(false);
|
||
|
||
const scene = sprite.scene;
|
||
|
||
// SIMPLIFIED INVENTORY NAMING
|
||
// Use a consistent format: inventory_type_index
|
||
const inventoryId = `inventory_${sprite.name}_${inventory.items.length}`;
|
||
|
||
// Create new sprite for inventory
|
||
const inventorySprite = scene.add.sprite(
|
||
inventory.items.length * 60,
|
||
0,
|
||
sprite.name
|
||
);
|
||
|
||
inventorySprite.setInteractive({ useHandCursor: true, pixelPerfect: true });
|
||
inventorySprite.scenarioData = {
|
||
...sprite.scenarioData,
|
||
foundIn: currentRoom ? gameScenario.rooms[currentRoom].name || currentRoom : 'unknown location'
|
||
};
|
||
inventorySprite.name = sprite.name;
|
||
inventorySprite.objectId = inventoryId;
|
||
|
||
// Set depth higher than container
|
||
inventorySprite.setDepth(2003);
|
||
|
||
// Add pointer events
|
||
inventorySprite.on('pointerdown', function(pointer) {
|
||
// Handle inventory item interaction
|
||
handleObjectInteraction(inventorySprite);
|
||
});
|
||
|
||
inventorySprite.on('pointerover', function() {
|
||
this.setTint(0xdddddd);
|
||
|
||
// 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;
|
||
});
|
||
|
||
inventorySprite.on('pointerout', function() {
|
||
this.clearTint();
|
||
|
||
if (this.tooltip) {
|
||
this.tooltip.destroy();
|
||
this.tooltip = null;
|
||
}
|
||
});
|
||
|
||
// Add to inventory array
|
||
inventory.items.push(inventorySprite);
|
||
|
||
// 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';
|
||
}
|
||
|
||
// 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';
|
||
}
|
||
|
||
return true;
|
||
} catch (error) {
|
||
console.error('Error adding to inventory:', error);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// initializes inventory
|
||
// creates the background and slot outlines
|
||
function initializeInventory() {
|
||
// Reset inventory state
|
||
inventory.items = [];
|
||
|
||
// Create slot outlines
|
||
const slotsContainer = this.add.container(INVENTORY_X_OFFSET, this.cameras.main.height - INVENTORY_Y_OFFSET)
|
||
.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 - use the same X offset as slots
|
||
inventory.container = this.add.container(INVENTORY_X_OFFSET, this.cameras.main.height - INVENTORY_Y_OFFSET)
|
||
.setScrollFactor(0)
|
||
.setDepth(2001);
|
||
|
||
debugLog('INVENTORY INITIALIZED', inventory, 2); // Debug log at level 2
|
||
}
|
||
|
||
// Process all items marked with inInventory: true in the scenario data
|
||
function processInitialInventoryItems() {
|
||
debugLog('PROCESSING INITIAL INVENTORY ITEMS', null, 2);
|
||
|
||
// Loop through all rooms in the scenario
|
||
Object.entries(gameScenario.rooms).forEach(([roomId, roomData]) => {
|
||
if (!roomData.objects) return;
|
||
|
||
// Check each object in the room
|
||
roomData.objects.forEach(objectData => {
|
||
// If the object has inInventory: true, add it to the player's inventory
|
||
if (objectData.takeable && objectData.inInventory === true) {
|
||
debugLog('ADDING INITIAL INVENTORY ITEM', objectData, 2);
|
||
|
||
// Create a sprite for the item
|
||
let sprite;
|
||
|
||
// Special handling for workstation type
|
||
if (objectData.type === "workstation") {
|
||
sprite = createCryptoWorkstation.call(this, objectData);
|
||
} else {
|
||
sprite = createInventorySprite(objectData);
|
||
}
|
||
|
||
if (sprite) {
|
||
// Add to inventory
|
||
addToInventory(sprite);
|
||
}
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// 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) {
|
||
// Just use type and name as the identifier
|
||
return `${scenarioData.type}|${scenarioData.name}`;
|
||
}
|
||
|
||
// 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) {
|
||
debugLog('DOOR LOCKED - ATTEMPTING UNLOCK', null, 2);
|
||
colorDoorTiles(doorTile, room);
|
||
handleDoorUnlock(doorTile, room);
|
||
} else {
|
||
debugLog('DOOR NOT LOCKED', null, 2);
|
||
}
|
||
} else {
|
||
debugLog('DOOR TOO FAR TO INTERACT', null, 2);
|
||
}
|
||
});
|
||
|
||
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) {
|
||
// No need to log here since handleUnlock will log 'UNLOCK ATTEMPT'
|
||
doorTile.layer = room.doorsLayer; // Ensure layer reference is set
|
||
handleUnlock(doorTile, 'door');
|
||
}
|
||
|
||
function handleUnlock(lockable, type) {
|
||
debugLog('UNLOCK ATTEMPT', null, 2);
|
||
|
||
// Check locked state in scenarioData for items
|
||
const isLocked = type === 'door' ?
|
||
lockable.properties?.locked :
|
||
lockable.scenarioData?.locked;
|
||
|
||
if (!isLocked) {
|
||
debugLog('OBJECT NOT LOCKED', null, 2);
|
||
return;
|
||
}
|
||
|
||
// Get lock requirements based on type
|
||
// lockRequirements should contain:
|
||
// - lockType: 'key', 'pin', 'password', 'bluetooth', or 'biometric'
|
||
// - requires: Key ID, PIN code, password, MAC address, or fingerprint owner name
|
||
const lockRequirements = type === 'door'
|
||
? getLockRequirementsForDoor(lockable)
|
||
: getLockRequirementsForItem(lockable);
|
||
|
||
// Don't log lock requirements here since it's already logged in the getter functions
|
||
|
||
if (!lockRequirements) {
|
||
// Don't log here since it's already logged in the getter functions if applicable
|
||
return;
|
||
}
|
||
|
||
switch(lockRequirements.lockType) {
|
||
case 'key':
|
||
const requiredKey = lockRequirements.requires;
|
||
debugLog('KEY REQUIRED', requiredKey, 2);
|
||
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';
|
||
|
||
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);
|
||
} else {
|
||
// 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, () => {
|
||
// Add callback to handle successful lockpicking
|
||
unlockTarget(lockable, type, lockable.layer);
|
||
gameAlert(`Successfully picked the lock!`, 'success', 'Lock Picked', 4000);
|
||
});
|
||
}
|
||
} else {
|
||
debugLog('KEY NOT FOUND - FAIL', null, 2);
|
||
gameAlert(`Requires key: ${requiredKey}`, 'error', 'Locked', 4000);
|
||
}
|
||
}
|
||
break;
|
||
|
||
case 'pin':
|
||
debugLog('PIN CODE REQUESTED', null, 2);
|
||
const pinInput = prompt(`Enter PIN code:`);
|
||
if (pinInput === lockRequirements.requires) {
|
||
unlockTarget(lockable, type, lockable.layer); // Pass the layer here
|
||
debugLog('PIN CODE SUCCESS', null, 1);
|
||
gameAlert(`Correct PIN! The ${type} is now unlocked.`, 'success', 'PIN Accepted', 4000);
|
||
} else if (pinInput !== null) {
|
||
debugLog('PIN CODE FAIL', null, 2);
|
||
gameAlert("Incorrect PIN code.", 'error', 'PIN Rejected', 3000);
|
||
}
|
||
break;
|
||
|
||
case 'password':
|
||
debugLog('PASSWORD REQUESTED', null, 2);
|
||
const passwordInput = prompt(`Enter password:`);
|
||
if (passwordInput === lockRequirements.requires) {
|
||
unlockTarget(lockable, type, lockable.layer); // Pass the layer here
|
||
debugLog('PASSWORD SUCCESS', null, 1);
|
||
gameAlert(`Correct password! The ${type} is now unlocked.`, 'success', 'Password Accepted', 4000);
|
||
} else if (passwordInput !== null) {
|
||
debugLog('PASSWORD FAIL', null, 2);
|
||
gameAlert("Incorrect password.", 'error', 'Password Rejected', 3000);
|
||
}
|
||
break;
|
||
|
||
case 'biometric':
|
||
const requiredFingerprint = lockRequirements.requires;
|
||
debugLog('BIOMETRIC LOCK REQUIRES', requiredFingerprint, 2);
|
||
|
||
// Check if we have fingerprints in the biometricSamples collection
|
||
const biometricSamples = gameState.biometricSamples || [];
|
||
|
||
// Enhanced debugging - Show collected fingerprints
|
||
debugLog('BIOMETRIC SAMPLES', JSON.stringify(biometricSamples), 2);
|
||
|
||
// Get the required match threshold from the object or use default
|
||
const requiredThreshold = typeof lockable.biometricMatchThreshold === 'number' ?
|
||
lockable.biometricMatchThreshold : 0.4;
|
||
// this needs to be changed to use the scenario, bugged.
|
||
debugLog('BIOMETRIC THRESHOLD', requiredThreshold, "this needs to be changed to use the scenario, bugged.",2);
|
||
|
||
// Find the fingerprint sample for the required person
|
||
const fingerprintSample = biometricSamples.find(sample =>
|
||
sample.owner === requiredFingerprint
|
||
);
|
||
|
||
const hasFingerprint = fingerprintSample !== undefined;
|
||
debugLog('FINGERPRINT CHECK', `Looking for '${requiredFingerprint}'. Found: ${hasFingerprint}`, 2);
|
||
|
||
if (hasFingerprint) {
|
||
// Get the quality from the sample
|
||
let fingerprintQuality = fingerprintSample.quality;
|
||
|
||
// Normalize quality to 0-1 range if it's in percentage format
|
||
if (fingerprintQuality > 1) {
|
||
fingerprintQuality = fingerprintQuality / 100;
|
||
}
|
||
|
||
debugLog('BIOMETRIC CHECK',
|
||
`Required: ${requiredFingerprint}, Quality: ${fingerprintQuality} (${Math.round(fingerprintQuality * 100)}%), Threshold: ${requiredThreshold} (${Math.round(requiredThreshold * 100)}%)`, 2);
|
||
|
||
debugLog('QUALITY CHECK',
|
||
`Is ${fingerprintQuality} >= ${requiredThreshold}? ${fingerprintQuality >= requiredThreshold}`, 2);
|
||
|
||
// Check if the fingerprint quality meets the threshold
|
||
if (fingerprintQuality >= requiredThreshold) {
|
||
debugLog('BIOMETRIC UNLOCK SUCCESS', null, 1);
|
||
unlockTarget(lockable, type, lockable.layer);
|
||
|
||
// Play unlock sound if available
|
||
if (typeof playSound === 'function') {
|
||
playSound("unlock");
|
||
}
|
||
|
||
gameAlert(`You successfully unlocked the ${type} with ${requiredFingerprint}'s fingerprint.`,
|
||
'success', 'Biometric Unlock Successful', 5000);
|
||
|
||
// Log the successful biometric unlock
|
||
if (!gameState.biometricUnlocks) {
|
||
gameState.biometricUnlocks = [];
|
||
}
|
||
gameState.biometricUnlocks.push({
|
||
location: type,
|
||
fingerprint: requiredFingerprint,
|
||
quality: fingerprintQuality,
|
||
threshold: requiredThreshold,
|
||
timestamp: new Date().toISOString()
|
||
});
|
||
} else {
|
||
debugLog('BIOMETRIC QUALITY TOO LOW',
|
||
`Quality: ${fingerprintQuality} (${Math.round(fingerprintQuality * 100)}%) < Threshold: ${requiredThreshold} (${Math.round(requiredThreshold * 100)}%)`, 2);
|
||
gameAlert(`The fingerprint quality (${Math.round(fingerprintQuality * 100)}%) is too low for this lock.
|
||
It requires at least ${Math.round(requiredThreshold * 100)}% quality.`,
|
||
'error', 'Biometric Authentication Failed', 5000);
|
||
}
|
||
} else {
|
||
debugLog('MISSING REQUIRED FINGERPRINT',
|
||
`Required: '${requiredFingerprint}', Available: ${biometricSamples.map(s => s.owner).join(", ") || "none"}`, 2);
|
||
gameAlert(`This ${type} requires ${requiredFingerprint}'s fingerprint, which you haven't collected yet.`,
|
||
'error', 'Biometric Authentication Failed', 5000);
|
||
}
|
||
break;
|
||
|
||
case 'bluetooth':
|
||
if (lockable.scenarioData?.locked) {
|
||
// Check if we have a matching Bluetooth device in the current room
|
||
const matchFound = findMatchingBluetoothDevice(lockable);
|
||
|
||
// Allow the item to be picked up regardless of match if it's takeable
|
||
if (type === 'item' && lockable.scenarioData?.takeable) {
|
||
// Check if the item is already in the inventory before adding it
|
||
const isAlreadyInInventory = inventory.items.some(item => item.name === lockable.name);
|
||
|
||
if (!isAlreadyInInventory) {
|
||
addToInventory(lockable);
|
||
// Remove from room objects if it exists there
|
||
if (currentRoom && rooms[currentRoom].objects) {
|
||
delete rooms[currentRoom].objects[lockable.name];
|
||
}
|
||
}
|
||
|
||
// Only unlock if there's a matching device
|
||
if (matchFound) {
|
||
unlockTarget(lockable, type, lockable.layer);
|
||
gameAlert(`Bluetooth connection established with ${matchFound.name}`, 'success', 'Bluetooth Connected', 3000);
|
||
} else {
|
||
gameAlert('Item added to inventory but still locked - no matching Bluetooth device in range', 'warning', 'Partial Success', 3000);
|
||
return;
|
||
}
|
||
} else if (matchFound) {
|
||
// For non-takeable items, only proceed if there's a match
|
||
unlockTarget(lockable, type, lockable.layer);
|
||
gameAlert(`Bluetooth connection established with ${matchFound.name}`, 'success', 'Bluetooth Connected', 3000);
|
||
} else {
|
||
gameAlert('No matching Bluetooth device in range', 'error', 'Connection Failed', 3000);
|
||
return;
|
||
}
|
||
}
|
||
console.log('Bluetooth processing complete');
|
||
break;
|
||
|
||
default:
|
||
gameAlert(`Requires: ${lockRequirements.requires}`, 'warning', 'Locked', 4000);
|
||
}
|
||
}
|
||
|
||
// Modify the unlockTarget function
|
||
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;
|
||
// Set new state for containers with contents
|
||
if (lockable.scenarioData.contents) {
|
||
lockable.scenarioData.isUnlockedButNotCollected = true;
|
||
return; // Return early to prevent automatic collection
|
||
}
|
||
} else {
|
||
lockable.locked = false;
|
||
if (lockable.contents) {
|
||
lockable.isUnlockedButNotCollected = true;
|
||
return; // Return early to prevent automatic collection
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 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
|
||
sprite.setInteractive({ useHandCursor: true, pixelPerfect: true });
|
||
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) {
|
||
debugLog('CHECKING DOOR REQUIREMENTS', null, 3);
|
||
|
||
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);
|
||
|
||
debugLog('DOOR COORDINATES', { doorWorldX, doorWorldY }, 3);
|
||
|
||
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)) {
|
||
debugLog(`ROOM ${roomId} OVERLAPS WITH DOOR`, null, 3);
|
||
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
|
||
});
|
||
}
|
||
});
|
||
|
||
debugLog('OVERLAPPING ROOMS', overlappingRooms, 3);
|
||
|
||
const lockedRooms = overlappingRooms
|
||
.filter(r => r.locked)
|
||
.sort((a, b) => b.distance - a.distance);
|
||
|
||
debugLog('LOCKED ROOMS', lockedRooms, 3);
|
||
|
||
if (lockedRooms.length > 0) {
|
||
const targetRoom = lockedRooms[0];
|
||
const requirements = {
|
||
lockType: targetRoom.lockType, // Can be: 'key', 'pin', 'password', 'bluetooth', or 'biometric'
|
||
requires: targetRoom.requires // Key ID, PIN code, password, BT MAC address, or fingerprint owner name
|
||
};
|
||
debugLog('LOCK REQUIREMENTS', requirements, 2);
|
||
return requirements;
|
||
}
|
||
|
||
debugLog('NO LOCK REQUIREMENTS FOUND', null, 2);
|
||
return null;
|
||
}
|
||
|
||
function getLockRequirementsForItem(item) {
|
||
return {
|
||
lockType: item.lockType || item.scenarioData?.lockType, // Can be: 'key', 'pin', 'password', 'bluetooth', or 'biometric'
|
||
requires: item.requires || item.scenarioData?.requires, // Key ID, PIN code, password, BT MAC address, or fingerprint owner name
|
||
isUnlockedButNotCollected: false
|
||
};
|
||
}
|
||
|
||
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;
|
||
gameAlert('You collected the items from the container.', 'success', 'Items Collected', 4000);
|
||
}
|
||
|
||
// Add a throttle mechanism for Bluetooth panel updates
|
||
let lastBluetoothPanelUpdate = 0;
|
||
const BLUETOOTH_UPDATE_THROTTLE = 500; // milliseconds
|
||
|
||
function checkBluetoothDevices() {
|
||
// Find scanner in inventory
|
||
const scanner = inventory.items.find(item =>
|
||
item.scenarioData?.type === "bluetooth_scanner"
|
||
);
|
||
|
||
if (!scanner) return;
|
||
|
||
// Show the Bluetooth toggle button if it's not already visible
|
||
const bluetoothToggle = document.getElementById('bluetooth-toggle');
|
||
if (bluetoothToggle.style.display === 'none') {
|
||
bluetoothToggle.style.display = 'flex';
|
||
}
|
||
|
||
// Find all Bluetooth devices in the current room
|
||
if (!currentRoom || !rooms[currentRoom] || !rooms[currentRoom].objects) return;
|
||
|
||
// Keep track of devices detected in this scan
|
||
const detectedDevices = new Set();
|
||
let needsUpdate = false;
|
||
|
||
Object.values(rooms[currentRoom].objects).forEach(obj => {
|
||
if (obj.scenarioData?.lockType === "bluetooth") {
|
||
const distance = Phaser.Math.Distance.Between(
|
||
player.x, player.y,
|
||
obj.x, obj.y
|
||
);
|
||
|
||
const deviceMac = obj.scenarioData?.mac || "Unknown";
|
||
|
||
if (distance <= BLUETOOTH_SCAN_RANGE) {
|
||
detectedDevices.add(deviceMac);
|
||
|
||
debugLog('BLUETOOTH DEVICE DETECTED', {
|
||
deviceName: obj.scenarioData?.name,
|
||
deviceMac: deviceMac,
|
||
distance: Math.round(distance),
|
||
range: BLUETOOTH_SCAN_RANGE
|
||
}, 2);
|
||
|
||
// Add to Bluetooth scanner panel
|
||
const deviceName = obj.scenarioData?.name || "Unknown Device";
|
||
const signalStrength = Math.max(0, Math.round(100 - (distance / BLUETOOTH_SCAN_RANGE * 100)));
|
||
const details = `Type: ${obj.scenarioData?.type || "Unknown"}\nDistance: ${Math.round(distance)} units\nSignal Strength: ${signalStrength}%`;
|
||
|
||
// Check if device already exists in our list
|
||
const existingDevice = bluetoothDevices.find(device => device.mac === deviceMac);
|
||
|
||
if (existingDevice) {
|
||
// Update existing device details with real-time data
|
||
const oldSignalStrength = existingDevice.signalStrength;
|
||
existingDevice.details = details;
|
||
existingDevice.lastSeen = new Date();
|
||
existingDevice.nearby = true;
|
||
existingDevice.signalStrength = signalStrength;
|
||
|
||
// Only mark for update if signal strength changed significantly
|
||
if (Math.abs(oldSignalStrength - signalStrength) > 5) {
|
||
needsUpdate = true;
|
||
}
|
||
} else {
|
||
// Add as new device if not already in our list
|
||
const newDevice = addBluetoothDevice(deviceName, deviceMac, details, true);
|
||
if (newDevice) {
|
||
newDevice.signalStrength = signalStrength;
|
||
gameAlert(`Bluetooth device detected: ${deviceName} (MAC: ${deviceMac})`, 'info', 'Bluetooth Scanner', 4000);
|
||
needsUpdate = true;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
// Mark devices that weren't detected in this scan as not nearby
|
||
bluetoothDevices.forEach(device => {
|
||
if (device.nearby && !detectedDevices.has(device.mac)) {
|
||
device.nearby = false;
|
||
device.lastSeen = new Date();
|
||
needsUpdate = true;
|
||
}
|
||
});
|
||
|
||
// Only update the panel if needed and not too frequently
|
||
const now = Date.now();
|
||
if (needsUpdate && now - lastBluetoothPanelUpdate > BLUETOOTH_UPDATE_THROTTLE) {
|
||
updateBluetoothPanel();
|
||
updateBluetoothCount();
|
||
lastBluetoothPanelUpdate = now;
|
||
}
|
||
}
|
||
|
||
// Add helper function to generate fingerprint data
|
||
function generateFingerprintData(item) {
|
||
// For original samples from items, we use the item's data
|
||
if (item.scenarioData?.fingerprintOwner) {
|
||
return `fp_${item.scenarioData.fingerprintOwner}_${Date.now()}`;
|
||
}
|
||
|
||
// Fallback unique identifier
|
||
return `fp_unknown_${Date.now()}`;
|
||
}
|
||
|
||
// Add helper function to check if player has required collection tools
|
||
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);
|
||
}
|
||
});
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// 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: {}
|
||
};
|
||
}
|
||
|
||
// 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>
|
||
`;
|
||
|
||
// 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);
|
||
samplesUI.appendChild(sampleElement);
|
||
});
|
||
}
|
||
|
||
// Helper function to hide samples UI
|
||
function hideSamplesUI() {
|
||
const biometricsPanel = document.getElementById('biometrics-panel');
|
||
biometricsPanel.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 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()}`;
|
||
}
|
||
|
||
|
||
// Minigame Framework
|
||
const MinigameFramework = {
|
||
activeMinigame: null,
|
||
mainGameScene: null,
|
||
|
||
// Initialize the framework
|
||
init: function(mainScene) {
|
||
this.mainGameScene = mainScene;
|
||
|
||
// Create container for all minigames if it doesn't exist
|
||
if (!document.getElementById('minigame-container')) {
|
||
const container = document.createElement('div');
|
||
container.id = 'minigame-container';
|
||
document.body.appendChild(container);
|
||
|
||
// Add base styles
|
||
const style = document.createElement('style');
|
||
style.id = 'minigame-framework-styles';
|
||
style.textContent = `
|
||
/* Framework base styles */
|
||
#minigame-container {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
z-index: 1000;
|
||
display: none;
|
||
pointer-events: all;
|
||
}
|
||
|
||
#minigame-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
z-index: 1001;
|
||
pointer-events: all;
|
||
}
|
||
|
||
.minigame-scene {
|
||
position: fixed;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
background: #222;
|
||
color: white;
|
||
border-radius: 10px;
|
||
box-shadow: 0 0 20px rgba(0,0,0,0.5);
|
||
z-index: 1002;
|
||
pointer-events: all;
|
||
}
|
||
|
||
/* Common minigame UI elements */
|
||
.minigame-close {
|
||
background: #555;
|
||
color: white;
|
||
border: none;
|
||
padding: 10px 20px;
|
||
border-radius: 5px;
|
||
cursor: pointer;
|
||
margin-top: 10px;
|
||
align-self: center;
|
||
}
|
||
|
||
.minigame-header {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
background: rgba(30, 30, 30, 0.9);
|
||
padding: 10px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
z-index: 5;
|
||
}
|
||
|
||
.minigame-header h3 {
|
||
margin: 0 0 8px 0;
|
||
color: #fff;
|
||
text-align: center;
|
||
}
|
||
|
||
.minigame-header p {
|
||
margin: 5px 0;
|
||
color: #ccc;
|
||
text-align: center;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.minigame-success-message {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
background: rgba(0, 0, 0, 0.9);
|
||
padding: 20px;
|
||
border-radius: 5px;
|
||
color: #0f0;
|
||
font-size: 20px;
|
||
text-align: center;
|
||
z-index: 1003;
|
||
box-shadow: 0 0 20px rgba(0, 255, 0, 0.3);
|
||
}
|
||
|
||
.minigame-failure-message {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
background: rgba(0, 0, 0, 0.9);
|
||
padding: 20px;
|
||
border-radius: 5px;
|
||
color: #f00;
|
||
font-size: 20px;
|
||
text-align: center;
|
||
z-index: 1003;
|
||
box-shadow: 0 0 20px rgba(255, 0, 0, 0.3);
|
||
}
|
||
|
||
.minigame-tool-button {
|
||
background-color: #444;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 3px;
|
||
padding: 5px 10px;
|
||
cursor: pointer;
|
||
opacity: 0.7;
|
||
transition: opacity 0.2s, background-color 0.2s;
|
||
}
|
||
|
||
.minigame-tool-button.active {
|
||
opacity: 1;
|
||
}
|
||
|
||
.minigame-progress-container {
|
||
height: 6px;
|
||
width: 100%;
|
||
background: #333;
|
||
border-radius: 3px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.minigame-progress-bar {
|
||
height: 100%;
|
||
width: 0%;
|
||
background: #2ecc71;
|
||
transition: width 0.3s;
|
||
}
|
||
|
||
.minigame-scene.success {
|
||
border: 2px solid #33cc33;
|
||
}
|
||
|
||
.minigame-scene.failure {
|
||
border: 2px solid #cc3333;
|
||
}
|
||
`;
|
||
document.head.appendChild(style);
|
||
}
|
||
},
|
||
|
||
// Launch a minigame
|
||
startMinigame: function(minigameId, params = {}) {
|
||
// Don't allow multiple minigames at once
|
||
if (this.activeMinigame) {
|
||
console.warn('A minigame is already running!');
|
||
return false;
|
||
}
|
||
|
||
// Get the minigame scene constructor
|
||
const MinigameScene = this.scenes[minigameId];
|
||
if (!MinigameScene) {
|
||
console.error(`Minigame "${minigameId}" not found!`);
|
||
return false;
|
||
}
|
||
|
||
// Pause the main game
|
||
if (this.mainGameScene) {
|
||
// Pause Phaser scene
|
||
if (this.mainGameScene.scene && typeof this.mainGameScene.scene.pause === 'function') {
|
||
this.mainGameScene.scene.pause();
|
||
}
|
||
}
|
||
|
||
// Show the minigame container
|
||
const container = document.getElementById('minigame-container');
|
||
container.style.display = 'block';
|
||
|
||
// Create overlay
|
||
const overlay = document.createElement('div');
|
||
overlay.id = 'minigame-overlay';
|
||
container.appendChild(overlay);
|
||
|
||
// Create scene element
|
||
const sceneElement = document.createElement('div');
|
||
sceneElement.className = 'minigame-scene';
|
||
sceneElement.id = `minigame-scene-${minigameId}`;
|
||
container.appendChild(sceneElement);
|
||
|
||
// Instantiate the minigame scene
|
||
this.activeMinigame = new MinigameScene(sceneElement, {
|
||
...params,
|
||
onComplete: (success, result) => {
|
||
this.endMinigame(success, result);
|
||
if (params.onComplete) params.onComplete(success, result);
|
||
}
|
||
});
|
||
|
||
// Initialize and start the minigame
|
||
this.activeMinigame.init();
|
||
this.activeMinigame.start();
|
||
|
||
return true;
|
||
},
|
||
|
||
// End the active minigame
|
||
endMinigame: function(success = false, result = null) {
|
||
if (!this.activeMinigame) return;
|
||
|
||
// Call the minigame's cleanup method
|
||
if (typeof this.activeMinigame.cleanup === 'function') {
|
||
this.activeMinigame.cleanup();
|
||
}
|
||
|
||
// Remove the minigame elements
|
||
const container = document.getElementById('minigame-container');
|
||
container.innerHTML = '';
|
||
container.style.display = 'none';
|
||
|
||
// Unpause the main game
|
||
if (this.mainGameScene) {
|
||
// Resume Phaser scene
|
||
if (this.mainGameScene.scene && typeof this.mainGameScene.scene.resume === 'function') {
|
||
this.mainGameScene.scene.resume();
|
||
}
|
||
}
|
||
|
||
this.activeMinigame = null;
|
||
},
|
||
|
||
// Register a new minigame scene
|
||
registerScene: function(id, sceneClass) {
|
||
if (!this.scenes) this.scenes = {};
|
||
this.scenes[id] = sceneClass;
|
||
},
|
||
|
||
// Base class for minigame scenes
|
||
MinigameScene: class {
|
||
constructor(container, params = {}) {
|
||
this.container = container;
|
||
this.params = params;
|
||
|
||
// Common game state management
|
||
this.gameState = {
|
||
isActive: false,
|
||
isDragging: false,
|
||
mousePosition: { x: 0, y: 0 },
|
||
mouseDown: false,
|
||
mouseButtonsPressed: [false, false, false],
|
||
keyState: {},
|
||
success: false,
|
||
gameComplete: false,
|
||
startTime: 0,
|
||
elapsedTime: 0
|
||
};
|
||
|
||
// Bind event handlers to the instance
|
||
this._handleMouseDown = this._handleMouseDown.bind(this);
|
||
this._handleMouseUp = this._handleMouseUp.bind(this);
|
||
this._handleMouseMove = this._handleMouseMove.bind(this);
|
||
this._handleMouseLeave = this._handleMouseLeave.bind(this);
|
||
this._handleKeyDown = this._handleKeyDown.bind(this);
|
||
this._handleKeyUp = this._handleKeyUp.bind(this);
|
||
|
||
// Store event listeners for cleanup
|
||
this._eventListeners = [];
|
||
}
|
||
|
||
init() {
|
||
console.log(`Initializing minigame scene`);
|
||
|
||
// Ensure the scene container is visible
|
||
this.container.style.display = 'flex';
|
||
|
||
// Create standard layout elements
|
||
this.createLayout();
|
||
|
||
// Add escape key handler
|
||
this.escHandler = (e) => {
|
||
if (e.key === 'Escape' && this.gameState.isActive) {
|
||
this.complete(false);
|
||
}
|
||
};
|
||
document.addEventListener('keydown', this.escHandler);
|
||
|
||
// Setup common event handling
|
||
this.setupEventHandling();
|
||
|
||
// Record start time
|
||
this.gameState.startTime = Date.now();
|
||
}
|
||
|
||
// Create standard layout for minigames
|
||
createLayout() {
|
||
// Create and add the header
|
||
this.headerElement = document.createElement('div');
|
||
this.headerElement.className = 'minigame-header';
|
||
this.container.appendChild(this.headerElement);
|
||
|
||
// Create the main game container
|
||
this.gameContainer = document.createElement('div');
|
||
this.gameContainer.className = 'minigame-game-container';
|
||
this.gameContainer.style.cssText = `
|
||
width: 80%;
|
||
height: 80%;
|
||
margin: 70px auto 20px auto;
|
||
background: #1a1a1a;
|
||
border-radius: 5px;
|
||
box-shadow: 0 0 15px rgba(0, 0, 0, 0.5) inset;
|
||
position: relative;
|
||
overflow: hidden;
|
||
`;
|
||
this.container.appendChild(this.gameContainer);
|
||
|
||
// Create message container for notifications, success/failure
|
||
this.messageContainer = document.createElement('div');
|
||
this.messageContainer.className = 'minigame-message-container';
|
||
this.messageContainer.style.cssText = `
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
pointer-events: none;
|
||
z-index: 1000;
|
||
`;
|
||
this.container.appendChild(this.messageContainer);
|
||
|
||
// Create progress display
|
||
this.progressContainer = document.createElement('div');
|
||
this.progressContainer.style.cssText = `
|
||
position: absolute;
|
||
bottom: 15px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
color: white;
|
||
text-align: center;
|
||
font-size: 16px;
|
||
background: rgba(0, 0, 0, 0.6);
|
||
padding: 5px 15px;
|
||
border-radius: 15px;
|
||
z-index: 10;
|
||
width: 80%;
|
||
max-width: 500px;
|
||
`;
|
||
this.container.appendChild(this.progressContainer);
|
||
|
||
// Create standard close button
|
||
const closeButton = document.createElement('button');
|
||
closeButton.textContent = '✕';
|
||
closeButton.style.cssText = `
|
||
position: absolute;
|
||
right: 10px;
|
||
top: 10px;
|
||
background: none;
|
||
border: none;
|
||
color: white;
|
||
font-size: 20px;
|
||
cursor: pointer;
|
||
z-index: 100;
|
||
`;
|
||
closeButton.onclick = () => this.complete(false);
|
||
this.container.appendChild(closeButton);
|
||
}
|
||
|
||
// Set up common event handling for mouse, touch, keyboard
|
||
setupEventHandling() {
|
||
// Mouse events
|
||
this.addEventListenerWithCleanup(this.gameContainer, 'mousedown', this._handleMouseDown);
|
||
this.addEventListenerWithCleanup(document, 'mouseup', this._handleMouseUp);
|
||
this.addEventListenerWithCleanup(this.gameContainer, 'mousemove', this._handleMouseMove);
|
||
this.addEventListenerWithCleanup(this.gameContainer, 'mouseleave', this._handleMouseLeave);
|
||
|
||
// Keyboard events
|
||
this.addEventListenerWithCleanup(document, 'keydown', this._handleKeyDown);
|
||
this.addEventListenerWithCleanup(document, 'keyup', this._handleKeyUp);
|
||
|
||
// Prevent context menu
|
||
this.addEventListenerWithCleanup(this.gameContainer, 'contextmenu', (e) => e.preventDefault());
|
||
}
|
||
|
||
// Utility to add event listener and track it for cleanup
|
||
addEventListenerWithCleanup(element, eventType, handler, options) {
|
||
element.addEventListener(eventType, handler, options);
|
||
this._eventListeners.push({ element, eventType, handler });
|
||
}
|
||
|
||
// Event handlers
|
||
_handleMouseDown(e) {
|
||
console.log("Mouse down in framework handler");
|
||
this.gameState.mouseDown = true;
|
||
this.gameState.mouseButtonsPressed[e.button] = true;
|
||
this.gameState.isDragging = true;
|
||
|
||
// Call subclass handler if it exists
|
||
if (typeof this.handleMouseDown === 'function') {
|
||
this.handleMouseDown(e);
|
||
}
|
||
}
|
||
|
||
_handleMouseUp(e) {
|
||
console.log("Mouse up in framework handler");
|
||
this.gameState.mouseDown = false;
|
||
this.gameState.mouseButtonsPressed[e.button] = false;
|
||
this.gameState.isDragging = false;
|
||
|
||
// Call subclass handler if it exists
|
||
if (typeof this.handleMouseUp === 'function') {
|
||
this.handleMouseUp(e);
|
||
}
|
||
}
|
||
|
||
_handleMouseMove(e) {
|
||
// Update mouse position
|
||
const rect = this.gameContainer.getBoundingClientRect();
|
||
this.gameState.mousePosition = {
|
||
x: e.clientX - rect.left,
|
||
y: e.clientY - rect.top,
|
||
clientX: e.clientX,
|
||
clientY: e.clientY,
|
||
screenX: e.screenX,
|
||
screenY: e.screenY
|
||
};
|
||
|
||
// Call subclass handler if it exists
|
||
if (typeof this.handleMouseMove === 'function') {
|
||
this.handleMouseMove(e);
|
||
}
|
||
}
|
||
|
||
_handleMouseLeave(e) {
|
||
this.gameState.isDragging = false;
|
||
|
||
// Call subclass handler if it exists
|
||
if (typeof this.handleMouseLeave === 'function') {
|
||
this.handleMouseLeave(e);
|
||
}
|
||
}
|
||
|
||
_handleKeyDown(e) {
|
||
this.gameState.keyState[e.code] = true;
|
||
|
||
// Call subclass handler if it exists
|
||
if (typeof this.handleKeyDown === 'function') {
|
||
this.handleKeyDown(e);
|
||
}
|
||
}
|
||
|
||
_handleKeyUp(e) {
|
||
this.gameState.keyState[e.code] = false;
|
||
|
||
// Call subclass handler if it exists
|
||
if (typeof this.handleKeyUp === 'function') {
|
||
this.handleKeyUp(e);
|
||
}
|
||
}
|
||
|
||
// Message display system
|
||
showMessage(message, type = 'info', duration = 0) {
|
||
// Create a message element
|
||
const messageElement = document.createElement('div');
|
||
messageElement.className = `minigame-message minigame-message-${type}`;
|
||
messageElement.innerHTML = message;
|
||
|
||
// Style based on type
|
||
let styles = `
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
padding: 20px;
|
||
border-radius: 5px;
|
||
text-align: center;
|
||
z-index: 1001;
|
||
pointer-events: all;
|
||
max-width: 90%;
|
||
transition: opacity 0.3s;
|
||
`;
|
||
|
||
switch(type) {
|
||
case 'success':
|
||
messageElement.className = 'minigame-success-message';
|
||
break;
|
||
case 'failure':
|
||
case 'error':
|
||
messageElement.className = 'minigame-failure-message';
|
||
break;
|
||
default:
|
||
styles += `
|
||
background: rgba(0, 0, 0, 0.8);
|
||
color: white;
|
||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
|
||
`;
|
||
}
|
||
|
||
// Apply styles
|
||
messageElement.style.cssText = styles;
|
||
|
||
// Add to the message container
|
||
this.messageContainer.appendChild(messageElement);
|
||
|
||
// Auto-remove after duration, if specified
|
||
if (duration > 0) {
|
||
setTimeout(() => {
|
||
messageElement.style.opacity = '0';
|
||
setTimeout(() => {
|
||
if (messageElement.parentNode) {
|
||
messageElement.parentNode.removeChild(messageElement);
|
||
}
|
||
}, 300);
|
||
}, duration);
|
||
}
|
||
|
||
return messageElement;
|
||
}
|
||
|
||
// Success/failure methods
|
||
showSuccess(message, autoComplete = true, delay = 2000) {
|
||
this.gameState.success = true;
|
||
this.gameState.gameComplete = true;
|
||
this.container.classList.add('success');
|
||
|
||
// Create success message
|
||
const successMessage = this.showMessage(message, 'success');
|
||
|
||
// Auto-complete after delay
|
||
if (autoComplete) {
|
||
setTimeout(() => {
|
||
this.complete(true);
|
||
}, delay);
|
||
}
|
||
|
||
return successMessage;
|
||
}
|
||
|
||
showFailure(message, autoComplete = true, delay = 2000) {
|
||
this.gameState.success = false;
|
||
this.gameState.gameComplete = true;
|
||
this.container.classList.add('failure');
|
||
|
||
// Create failure message
|
||
const failureMessage = this.showMessage(message, 'failure');
|
||
|
||
// Auto-complete after delay
|
||
if (autoComplete) {
|
||
setTimeout(() => {
|
||
this.complete(false);
|
||
}, delay);
|
||
}
|
||
|
||
return failureMessage;
|
||
}
|
||
|
||
// Progress updates
|
||
updateProgress(current, total, label = '') {
|
||
const percentage = Math.min(100, Math.max(0, (current / total) * 100));
|
||
|
||
this.progressContainer.innerHTML = `
|
||
${label ? `<div style="margin-bottom: 5px;">${label}</div>` : ''}
|
||
<div class="minigame-progress-container">
|
||
<div class="minigame-progress-bar" style="width: ${percentage}%;"></div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
start() {
|
||
this.gameState.isActive = true;
|
||
console.log("Minigame started");
|
||
}
|
||
|
||
update(deltaTime) {
|
||
// Update elapsed time
|
||
this.gameState.elapsedTime = Date.now() - this.gameState.startTime;
|
||
|
||
// Override in subclass for game-specific updates
|
||
}
|
||
|
||
complete(success, result = null) {
|
||
console.log(`Minigame complete, success: ${success}`);
|
||
this.gameState.isActive = false;
|
||
this.gameState.success = success;
|
||
|
||
// Remove event listeners
|
||
document.removeEventListener('keydown', this.escHandler);
|
||
this.cleanup();
|
||
|
||
if (typeof this.params.onComplete === 'function') {
|
||
this.params.onComplete(success, result);
|
||
}
|
||
}
|
||
|
||
cleanup() {
|
||
console.log("Cleaning up minigame");
|
||
|
||
// Remove all tracked event listeners
|
||
this._eventListeners.forEach(({ element, eventType, handler }) => {
|
||
element.removeEventListener(eventType, handler);
|
||
});
|
||
this._eventListeners = [];
|
||
}
|
||
}
|
||
};
|
||
|
||
// Lockpicking Minigame Scene implementation
|
||
class LockpickingMinigame extends MinigameFramework.MinigameScene {
|
||
constructor(container, params) {
|
||
super(container, params);
|
||
|
||
this.lockable = params.lockable;
|
||
this.difficulty = params.difficulty || 'medium';
|
||
this.pinCount = this.difficulty === 'easy' ? 3 : this.difficulty === 'medium' ? 4 : 5;
|
||
|
||
this.pins = [];
|
||
|
||
// Use gameState from the framework but extend it with lockpicking-specific properties
|
||
this.lockState = {
|
||
tensionApplied: false,
|
||
pinsSet: 0,
|
||
currentPin: null
|
||
};
|
||
}
|
||
|
||
init() {
|
||
// Call parent init to set up common components
|
||
super.init();
|
||
|
||
console.log("Lockpicking minigame initializing");
|
||
|
||
// Configure container size and layout
|
||
this.container.style.width = '90%';
|
||
this.container.style.maxWidth = '500px';
|
||
this.container.style.padding = '20px';
|
||
this.container.style.gap = '15px';
|
||
|
||
// Set up header content with proper spacing
|
||
this.headerElement.innerHTML = `
|
||
<h3 style="margin-top: 0; margin-bottom: 10px;">Lockpicking</h3>
|
||
<p style="margin-bottom: 20px;">Apply tension and hold click on pins to lift them to the shear line</p>
|
||
`;
|
||
this.headerElement.style.marginBottom = '30px'; // Add more space below header
|
||
|
||
// Add custom styles for the lockpicking minigame if they don't exist
|
||
if (!document.getElementById('lockpicking-styles')) {
|
||
const style = document.createElement('style');
|
||
style.id = 'lockpicking-styles';
|
||
style.textContent = `
|
||
/* Game container styles */
|
||
.minigame-container {
|
||
padding-top: 80px !important; /* Add padding at top to prevent header overlap */
|
||
position: relative;
|
||
}
|
||
|
||
.minigame-header {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
z-index: 10;
|
||
background: rgba(34, 34, 34, 0.95);
|
||
border-bottom: 1px solid #444;
|
||
padding: 10px 20px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.lock-visual {
|
||
display: flex;
|
||
justify-content: space-evenly;
|
||
align-items: center;
|
||
gap: 20px;
|
||
height: 200px;
|
||
background: #f0e6a6; /* Light yellow/beige background */
|
||
border-radius: 5px;
|
||
padding: 25px;
|
||
position: relative;
|
||
margin-top: 20px; /* Add top margin for better spacing from header */
|
||
margin-bottom: 20px;
|
||
border: 2px solid #887722;
|
||
z-index: 1; /* Ensure pins are below header */
|
||
}
|
||
|
||
/* Rest of existing CSS */
|
||
.pin {
|
||
width: 40px;
|
||
height: 150px;
|
||
position: relative;
|
||
background: transparent;
|
||
border-radius: 4px 4px 0 0;
|
||
overflow: visible;
|
||
cursor: pointer;
|
||
transition: transform 0.1s;
|
||
margin: 0 15px;
|
||
}
|
||
|
||
.pin:hover {
|
||
opacity: 0.9;
|
||
}
|
||
|
||
.shear-line {
|
||
position: absolute;
|
||
width: 100%;
|
||
height: 2px;
|
||
background: #aa8833;
|
||
bottom: 70px;
|
||
z-index: 5;
|
||
}
|
||
|
||
.pin-assembly {
|
||
position: absolute;
|
||
bottom: 0;
|
||
width: 100%;
|
||
height: 140px;
|
||
transition: transform 0.05s;
|
||
}
|
||
|
||
.key-pin {
|
||
position: absolute;
|
||
bottom: 0;
|
||
width: 100%;
|
||
height: 50px; /* Fixed height for all pins */
|
||
background: #dd3333; /* Red for key pins */
|
||
border-radius: 0 0 0 0;
|
||
clip-path: polygon(0 0, 100% 0, 100% 70%, 50% 100%, 0 70%); /* Pointed bottom */
|
||
transition: transform 0.3s;
|
||
}
|
||
|
||
.driver-pin {
|
||
position: absolute;
|
||
width: 100%;
|
||
height: 70px;
|
||
background: #3388dd; /* Blue for driver pins */
|
||
bottom: 50px; /* Position right above key pin */
|
||
border-radius: 0 0 0 0;
|
||
transition: transform 0.3s, background-color 0.3s;
|
||
}
|
||
|
||
.spring {
|
||
position: absolute;
|
||
bottom: 120px;
|
||
width: 100%;
|
||
height: 40px;
|
||
background: linear-gradient(to bottom,
|
||
#cccccc 0%, #cccccc 20%,
|
||
#999999 20%, #999999 25%,
|
||
#cccccc 25%, #cccccc 40%,
|
||
#999999 40%, #999999 45%,
|
||
#cccccc 45%, #cccccc 60%,
|
||
#999999 60%, #999999 65%,
|
||
#cccccc 65%, #cccccc 80%,
|
||
#999999 80%, #999999 85%,
|
||
#cccccc 85%, #cccccc 100%
|
||
);
|
||
transition: transform 0.3s;
|
||
}
|
||
|
||
.pin.binding {
|
||
box-shadow: 0 0 8px 2px #ffcc00;
|
||
}
|
||
|
||
/* Remove the pin-assembly transform for set pins */
|
||
.pin.set .pin-assembly {
|
||
transform: none; /* Reset transform so we can control individual pieces */
|
||
}
|
||
|
||
/* Keep driver pin (blue) above the shear line when set */
|
||
.pin.set .driver-pin {
|
||
background: #22aa22; /* Green to indicate set */
|
||
}
|
||
|
||
/* Reset key pin (red) to the bottom when set */
|
||
.pin.set .key-pin {
|
||
transform: translateY(0); /* Keep at bottom */
|
||
}
|
||
|
||
.cylinder {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
width: 100%;
|
||
height: 30px;
|
||
background: #ddbb77;
|
||
border-radius: 5px;
|
||
margin-top: 5px;
|
||
position: relative;
|
||
z-index: 0;
|
||
border: 2px solid #887722;
|
||
}
|
||
|
||
.cylinder-inner {
|
||
width: 80%;
|
||
height: 20px;
|
||
background: #ccaa66;
|
||
border-radius: 3px;
|
||
transform-origin: center;
|
||
transition: transform 0.3s;
|
||
}
|
||
|
||
.cylinder.rotated .cylinder-inner {
|
||
transform: rotate(15deg);
|
||
}
|
||
|
||
.lockpick-feedback {
|
||
padding: 15px;
|
||
background: #333;
|
||
border-radius: 5px;
|
||
text-align: center;
|
||
min-height: 30px;
|
||
margin-top: 20px;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.tension-control {
|
||
display: grid;
|
||
grid-template-columns: auto 1fr;
|
||
gap: 20px;
|
||
align-items: center;
|
||
background: #333;
|
||
padding: 20px;
|
||
border-radius: 5px;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.tension-wrench-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 10px;
|
||
position: relative;
|
||
width: 150px;
|
||
height: 60px;
|
||
}
|
||
|
||
.tension-track {
|
||
width: 100%;
|
||
height: 10px;
|
||
background: #444;
|
||
border-radius: 5px;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.tension-progress {
|
||
position: absolute;
|
||
height: 100%;
|
||
width: 0%;
|
||
background: linear-gradient(to right, #666, #2196F3);
|
||
transition: width 0.3s;
|
||
}
|
||
|
||
.tension-status {
|
||
font-size: 16px;
|
||
text-align: left;
|
||
padding-left: 10px;
|
||
}
|
||
|
||
.tension-wrench {
|
||
width: 60px;
|
||
height: 40px;
|
||
background: #666;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: transform 0.3s, background-color 0.3s;
|
||
position: absolute;
|
||
left: 0;
|
||
top: 20px;
|
||
z-index: 2;
|
||
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
|
||
}
|
||
|
||
.tension-wrench:hover {
|
||
background: #777;
|
||
}
|
||
|
||
.tension-wrench.active {
|
||
background: #2196F3;
|
||
}
|
||
|
||
.wrench-handle {
|
||
width: 60%;
|
||
height: 10px;
|
||
background: #999;
|
||
position: absolute;
|
||
}
|
||
|
||
.wrench-tip {
|
||
width: 20px;
|
||
height: 30px;
|
||
background: #999;
|
||
position: absolute;
|
||
left: 5px;
|
||
}
|
||
`;
|
||
document.head.appendChild(style);
|
||
}
|
||
|
||
// Add a class to the container for positioning
|
||
this.container.classList.add('minigame-container');
|
||
this.headerElement.classList.add('minigame-header');
|
||
|
||
// Replace the game container with custom lockpicking interface
|
||
this.setupLockpickingInterface();
|
||
|
||
// Create pins with random binding order
|
||
this.createPins();
|
||
|
||
// Update the progress display with lockpicking instructions
|
||
this.updateFeedback("Apply tension first, then click and hold on pins to lift them");
|
||
}
|
||
|
||
setupLockpickingInterface() {
|
||
// Remove default game container (we'll use custom layout)
|
||
if (this.gameContainer.parentNode) {
|
||
this.gameContainer.parentNode.removeChild(this.gameContainer);
|
||
}
|
||
|
||
// Create the content wrapper with padding to avoid header overlap
|
||
const contentWrapper = document.createElement('div');
|
||
contentWrapper.style.paddingTop = '10px';
|
||
this.container.appendChild(contentWrapper);
|
||
this.contentWrapper = contentWrapper;
|
||
|
||
// Create instructions
|
||
const instructions = document.createElement('div');
|
||
instructions.className = 'instructions';
|
||
instructions.textContent = 'Apply tension first, then click and hold on pins to lift them to the shear line';
|
||
contentWrapper.appendChild(instructions);
|
||
|
||
// Create the lock visual container
|
||
const lockVisual = document.createElement('div');
|
||
lockVisual.className = 'lock-visual';
|
||
contentWrapper.appendChild(lockVisual);
|
||
this.lockVisual = lockVisual;
|
||
|
||
// Remove cylinder creation - it's no longer needed
|
||
|
||
// Add tension toggle control with horizontal movement
|
||
const tensionControl = document.createElement('div');
|
||
tensionControl.className = 'tension-control';
|
||
|
||
const wrenchContainer = document.createElement('div');
|
||
wrenchContainer.className = 'tension-wrench-container';
|
||
|
||
const wrenchLabel = document.createElement('div');
|
||
wrenchLabel.textContent = 'Tension Wrench';
|
||
wrenchLabel.style.fontSize = '14px';
|
||
wrenchContainer.appendChild(wrenchLabel);
|
||
|
||
// Add tension track and progress
|
||
const tensionTrack = document.createElement('div');
|
||
tensionTrack.className = 'tension-track';
|
||
|
||
const tensionProgress = document.createElement('div');
|
||
tensionProgress.className = 'tension-progress';
|
||
tensionTrack.appendChild(tensionProgress);
|
||
|
||
wrenchContainer.appendChild(tensionTrack);
|
||
|
||
const tensionWrench = document.createElement('div');
|
||
tensionWrench.className = 'tension-wrench';
|
||
tensionWrench.innerHTML = `
|
||
<div class="wrench-handle"></div>
|
||
<div class="wrench-tip"></div>
|
||
`;
|
||
wrenchContainer.appendChild(tensionWrench);
|
||
|
||
tensionControl.appendChild(wrenchContainer);
|
||
|
||
const tensionStatus = document.createElement('div');
|
||
tensionStatus.className = 'tension-status';
|
||
tensionStatus.textContent = 'Click wrench to apply tension';
|
||
tensionControl.appendChild(tensionStatus);
|
||
|
||
this.contentWrapper.appendChild(tensionControl);
|
||
|
||
// Feedback area
|
||
const feedback = document.createElement('div');
|
||
feedback.className = 'lockpick-feedback';
|
||
this.contentWrapper.appendChild(feedback);
|
||
this.feedback = feedback;
|
||
|
||
// Set up tension wrench interaction with horizontal movement
|
||
tensionWrench.addEventListener('click', () => {
|
||
this.lockState.tensionApplied = !this.lockState.tensionApplied;
|
||
tensionWrench.classList.toggle('active', this.lockState.tensionApplied);
|
||
|
||
// Move wrench horizontally instead of rotating
|
||
if (this.lockState.tensionApplied) {
|
||
// Move to initial right position (25%)
|
||
this.updateTensionPosition(25);
|
||
} else {
|
||
// Return to left position (0%)
|
||
this.updateTensionPosition(0);
|
||
|
||
// Reset progress fill
|
||
tensionProgress.style.width = '0%';
|
||
}
|
||
|
||
// Update status text
|
||
tensionStatus.textContent = this.lockState.tensionApplied ?
|
||
'Tension applied - now lift pins' : 'Click wrench to apply tension';
|
||
|
||
// Update which pins are binding
|
||
this.updateBindingPins();
|
||
|
||
// If tension is toggled off, reset any unset pins
|
||
if (!this.lockState.tensionApplied) {
|
||
this.pins.forEach(pin => {
|
||
if (!pin.isSet) {
|
||
pin.currentHeight = 0;
|
||
this.updatePinVisual(pin);
|
||
}
|
||
});
|
||
this.updateFeedback("Tension released - apply tension before lifting pins");
|
||
} else {
|
||
this.updateFeedback("Tension applied - click and hold on pins to lift them");
|
||
}
|
||
});
|
||
|
||
// Store references
|
||
this.tensionWrench = tensionWrench;
|
||
this.tensionStatus = tensionStatus;
|
||
this.tensionProgress = tensionProgress;
|
||
}
|
||
|
||
// New method to update the tension wrench position
|
||
updateTensionPosition(percentage) {
|
||
if (percentage < 0) percentage = 0;
|
||
if (percentage > 100) percentage = 100;
|
||
|
||
// Calculate position based on container width
|
||
const containerWidth = this.tensionWrench.parentElement.offsetWidth;
|
||
const wrenchWidth = this.tensionWrench.offsetWidth;
|
||
const maxOffset = containerWidth - wrenchWidth;
|
||
const position = (maxOffset * percentage) / 100;
|
||
|
||
// Update wrench position
|
||
this.tensionWrench.style.transform = `translateX(${position}px)`;
|
||
|
||
// Update progress bar fill
|
||
if (this.tensionProgress) {
|
||
this.tensionProgress.style.width = `${percentage}%`;
|
||
}
|
||
}
|
||
|
||
createPins() {
|
||
// Generate random binding order
|
||
const bindingOrder = this.shuffleArray([...Array(this.pinCount).keys()]);
|
||
|
||
for (let i = 0; i < this.pinCount; i++) {
|
||
// Create pin container
|
||
const pinElement = document.createElement('div');
|
||
pinElement.className = 'pin';
|
||
pinElement.dataset.index = i;
|
||
this.lockVisual.appendChild(pinElement);
|
||
|
||
// Create shear line
|
||
const shearLine = document.createElement('div');
|
||
shearLine.className = 'shear-line';
|
||
pinElement.appendChild(shearLine);
|
||
|
||
// Create pin assembly container
|
||
const pinAssembly = document.createElement('div');
|
||
pinAssembly.className = 'pin-assembly';
|
||
pinElement.appendChild(pinAssembly);
|
||
|
||
// Create key pin (bottom pin) with varying height
|
||
const keyPin = document.createElement('div');
|
||
keyPin.className = 'key-pin';
|
||
pinAssembly.appendChild(keyPin);
|
||
|
||
// Generate random key pin height (30px to 60px)
|
||
const keyPinHeight = Math.floor(Math.random() * 31) + 30;
|
||
keyPin.style.height = `${keyPinHeight}px`;
|
||
|
||
// Create driver pin (top pin) with consistent height
|
||
const driverPin = document.createElement('div');
|
||
driverPin.className = 'driver-pin';
|
||
pinAssembly.appendChild(driverPin);
|
||
// Position driver pin right above the key pin
|
||
driverPin.style.bottom = `${keyPinHeight}px`;
|
||
|
||
// Create spring
|
||
const spring = document.createElement('div');
|
||
spring.className = 'spring';
|
||
pinAssembly.appendChild(spring);
|
||
// Position spring above driver pin
|
||
spring.style.bottom = `${keyPinHeight + 70}px`; // 70px is driver pin height
|
||
|
||
// Calculate the distance from the bottom of the pin to the shear line (70px from bottom)
|
||
const distanceToShearLine = 70 - keyPinHeight;
|
||
|
||
// Store pin data
|
||
const pin = {
|
||
index: i,
|
||
binding: bindingOrder.indexOf(i),
|
||
keyPinHeight: keyPinHeight,
|
||
distanceToShearLine: distanceToShearLine,
|
||
currentHeight: 0, // How high the pin is currently lifted (0-1 scale)
|
||
isSet: false,
|
||
resistance: Math.random() * 0.02 + 0.01,
|
||
elements: {
|
||
container: pinElement,
|
||
assembly: pinAssembly,
|
||
keyPin: keyPin,
|
||
driverPin: driverPin,
|
||
spring: spring
|
||
}
|
||
};
|
||
|
||
this.pins.push(pin);
|
||
|
||
// Fix: Use an arrow function to preserve 'this' context
|
||
// and define the handler inline instead of referencing this.handlePinMouseDown
|
||
const self = this; // Store reference to 'this'
|
||
this.addEventListenerWithCleanup(pinElement, 'mousedown', function(e) {
|
||
// Skip if game is not active or pin is already set
|
||
if (!self.gameState.isActive || pin.isSet) return;
|
||
|
||
// Only proceed if tension is applied
|
||
if (!self.lockState.tensionApplied) {
|
||
self.updateFeedback("Apply tension first by toggling the wrench");
|
||
return;
|
||
}
|
||
|
||
// Play a sound effect when interacting with pins
|
||
if (typeof self.playSound === 'function') {
|
||
self.playSound('pin_click');
|
||
}
|
||
|
||
// Start lifting the pin
|
||
self.lockState.currentPin = pin;
|
||
self.gameState.mouseDown = true;
|
||
self.liftPin();
|
||
|
||
// Add mouse up listener to document
|
||
const mouseUpHandler = function() {
|
||
self.gameState.mouseDown = false;
|
||
self.checkPinSet(self.lockState.currentPin);
|
||
self.lockState.currentPin = null;
|
||
document.removeEventListener('mouseup', mouseUpHandler);
|
||
};
|
||
|
||
document.addEventListener('mouseup', mouseUpHandler);
|
||
|
||
// Prevent text selection
|
||
e.preventDefault();
|
||
});
|
||
}
|
||
}
|
||
|
||
// Check if a pin should be set or dropped
|
||
checkPinSet(pin) {
|
||
if (!this.lockState.tensionApplied || !this.shouldPinBind(pin)) {
|
||
// Define dropPin function inline since it's not being found
|
||
this.animatePinDrop(pin);
|
||
return;
|
||
}
|
||
|
||
// Calculate current pin height in pixels
|
||
const currentLiftInPixels = pin.currentHeight * pin.distanceToShearLine;
|
||
|
||
// Check if the top of the key pin (or bottom of driver pin) is exactly at the shear line
|
||
// Allow a small tolerance of 2 pixels
|
||
const tolerance = 2;
|
||
const isAtShearLine = Math.abs(currentLiftInPixels - pin.distanceToShearLine) <= tolerance;
|
||
|
||
if (isAtShearLine) {
|
||
// Pin set successfully!
|
||
pin.isSet = true;
|
||
this.lockState.pinsSet++;
|
||
|
||
// Play a satisfying click sound when pin sets
|
||
if (typeof this.playSound === 'function') {
|
||
this.playSound('pin_set');
|
||
}
|
||
|
||
// First reset the assembly position
|
||
pin.elements.assembly.style.transform = 'none';
|
||
|
||
// Calculate exact position for the pin junction to be at the shear line
|
||
const exactLift = pin.distanceToShearLine;
|
||
pin.elements.assembly.style.transform = `translateY(-${exactLift}px)`;
|
||
|
||
// Mark the pin as set
|
||
pin.elements.container.classList.add('set');
|
||
|
||
// Change color of the driver pin to green
|
||
pin.elements.driverPin.style.backgroundColor = '#22aa22';
|
||
|
||
this.updateFeedback(`Pin set at the shear line! (${this.lockState.pinsSet}/${this.pinCount})`);
|
||
|
||
// Move the tension wrench further right based on progress
|
||
const progressPercentage = 25 + (this.lockState.pinsSet / this.pinCount * 75);
|
||
this.updateTensionPosition(progressPercentage);
|
||
|
||
// Update progress
|
||
this.updateProgress(this.lockState.pinsSet, this.pinCount);
|
||
|
||
// Check if all pins are set
|
||
if (this.lockState.pinsSet === this.pinCount) {
|
||
this.lockPickingSuccess();
|
||
return;
|
||
}
|
||
|
||
// Update which pin is binding next
|
||
this.updateBindingPins();
|
||
} else {
|
||
// Pin not at the correct height, drops back down
|
||
this.animatePinDrop(pin);
|
||
|
||
if (currentLiftInPixels > pin.distanceToShearLine) {
|
||
this.updateFeedback("Pin was pushed too far past the shear line");
|
||
} else {
|
||
this.updateFeedback("Pin wasn't lifted high enough to reach the shear line");
|
||
}
|
||
}
|
||
}
|
||
|
||
// Define the animatePinDrop method to replace the missing dropPin method
|
||
animatePinDrop(pin) {
|
||
// Don't drop pins that are already set
|
||
if (pin.isSet) return;
|
||
|
||
// Calculate drop speed based on how high the pin is
|
||
const dropSpeed = 0.05 + (pin.currentHeight * 0.1);
|
||
|
||
const dropInterval = setInterval(() => {
|
||
pin.currentHeight -= dropSpeed;
|
||
|
||
if (pin.currentHeight <= 0) {
|
||
pin.currentHeight = 0;
|
||
clearInterval(dropInterval);
|
||
}
|
||
|
||
this.updatePinVisual(pin);
|
||
}, 10);
|
||
}
|
||
|
||
// Update pin visual based on current height
|
||
updatePinVisual(pin) {
|
||
// Skip visualization update if the pin is set
|
||
if (pin.isSet) return;
|
||
|
||
// Calculate the lift in pixels based on the current progress (0-1) times the distance to the shear line
|
||
const translateY = pin.currentHeight * pin.distanceToShearLine * -1; // Negative because we're moving up
|
||
|
||
// Move the entire pin assembly up
|
||
pin.elements.assembly.style.transform = `translateY(${translateY}px)`;
|
||
}
|
||
|
||
// Pin-lifting logic with realistic physics
|
||
liftPin() {
|
||
if (!this.lockState.currentPin || !this.gameState.isActive ||
|
||
!this.lockState.tensionApplied || !this.gameState.mouseDown) {
|
||
return;
|
||
}
|
||
|
||
const pin = this.lockState.currentPin;
|
||
|
||
// Add realistic resistance based on binding state
|
||
let liftAmount = 0;
|
||
|
||
// Only binding pins can be lifted effectively
|
||
if (!this.shouldPinBind(pin)) {
|
||
// Non-binding pins can be lifted, but with resistance and limited height
|
||
liftAmount = 0.01;
|
||
if (pin.currentHeight > 0.3) {
|
||
liftAmount = 0.005; // Increased resistance at higher positions
|
||
}
|
||
} else {
|
||
// Binding pins lift more smoothly but still have some resistance
|
||
liftAmount = 0.03 - (pin.resistance * pin.currentHeight);
|
||
|
||
// Add slight random variation to simulate realistic feel
|
||
liftAmount += (Math.random() * 0.01 - 0.005);
|
||
}
|
||
|
||
// Update pin height
|
||
pin.currentHeight += liftAmount;
|
||
|
||
// Cap at maximum height
|
||
if (pin.currentHeight > 1.2) { // Allow overshooting the shear line a bit
|
||
pin.currentHeight = 1.2;
|
||
}
|
||
|
||
// Update visual
|
||
this.updatePinVisual(pin);
|
||
|
||
// Add subtle feedback when pin is near the shear line
|
||
const currentLiftInPixels = pin.currentHeight * pin.distanceToShearLine;
|
||
const distanceToShearLine = Math.abs(currentLiftInPixels - pin.distanceToShearLine);
|
||
|
||
if (distanceToShearLine < 5) {
|
||
// Pin is close to the shear line
|
||
pin.elements.container.style.boxShadow = "0 0 5px #ffffff";
|
||
} else {
|
||
pin.elements.container.style.boxShadow = "";
|
||
}
|
||
|
||
// Continue lifting while mouse is down
|
||
if (this.gameState.mouseDown) {
|
||
requestAnimationFrame(() => this.liftPin());
|
||
}
|
||
}
|
||
|
||
// Check if a pin should bind based on binding order
|
||
shouldPinBind(pin) {
|
||
if (!this.lockState.tensionApplied) return false;
|
||
|
||
// Find the next unset pin in binding order
|
||
for (let order = 0; order < this.pinCount; order++) {
|
||
const nextPin = this.pins.find(p => p.binding === order && !p.isSet);
|
||
if (nextPin) {
|
||
return pin.index === nextPin.index;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// Update feedback text
|
||
updateFeedback(message) {
|
||
this.feedback.textContent = message;
|
||
}
|
||
|
||
// Handle successful lockpicking
|
||
lockPickingSuccess() {
|
||
// Disable game interaction
|
||
this.gameState.isActive = false;
|
||
|
||
// Update UI
|
||
this.updateFeedback("Lock picked successfully!");
|
||
|
||
// Unlock the object in the game
|
||
if (this.lockable) {
|
||
// Set locked to false
|
||
this.lockable.locked = false;
|
||
|
||
// If it's a scenarioData object, also update that property
|
||
if (this.lockable.scenarioData) {
|
||
this.lockable.scenarioData.locked = false;
|
||
}
|
||
|
||
// Log successful unlock
|
||
if (typeof debugLog === 'function') {
|
||
debugLog('LOCKPICK UNLOCK', {
|
||
object: this.lockable,
|
||
success: true
|
||
}, 1);
|
||
}
|
||
}
|
||
|
||
// Show success message
|
||
const successHTML = `
|
||
<div style="font-weight: bold; font-size: 24px; margin-bottom: 10px;">Lock picked successfully!</div>
|
||
<div style="font-size: 18px; margin-bottom: 15px;">All pins set at the shear line</div>
|
||
<div style="font-size: 14px; color: #aaa;">
|
||
Difficulty: ${this.difficulty.charAt(0).toUpperCase() + this.difficulty.slice(1)}<br>
|
||
Pins: ${this.pinCount}
|
||
</div>
|
||
`;
|
||
|
||
// Use the framework's success message system
|
||
this.showSuccess(successHTML, true, 2000);
|
||
|
||
// Store lockable for the result
|
||
this.gameResult = { lockable: this.lockable };
|
||
}
|
||
|
||
lockPickingFailure() {
|
||
// Show failure message
|
||
const failureHTML = `
|
||
<div style="font-weight: bold; margin-bottom: 10px;">Failed to pick the lock</div>
|
||
<div style="font-size: 16px; margin-top: 5px;">Try again with more careful pin manipulation</div>
|
||
`;
|
||
|
||
// Use the framework's failure message system
|
||
this.showFailure(failureHTML, true, 2000);
|
||
}
|
||
|
||
start() {
|
||
super.start();
|
||
console.log("Lockpicking minigame started");
|
||
|
||
// Initialize game state
|
||
this.gameState.isActive = true;
|
||
this.lockState.tensionApplied = false;
|
||
this.lockState.pinsSet = 0;
|
||
|
||
// Initialize progress
|
||
this.updateProgress(0, this.pinCount);
|
||
}
|
||
|
||
complete(success) {
|
||
// Call parent complete with result
|
||
super.complete(success, this.gameResult);
|
||
}
|
||
|
||
// Utility function to shuffle an array
|
||
shuffleArray(array) {
|
||
for (let i = array.length - 1; i > 0; i--) {
|
||
const j = Math.floor(Math.random() * (i + 1));
|
||
[array[i], array[j]] = [array[j], array[i]];
|
||
}
|
||
return array;
|
||
}
|
||
|
||
// Add new method for updating binding pins
|
||
updateBindingPins() {
|
||
if (!this.lockState.tensionApplied) {
|
||
// No binding if no tension
|
||
this.pins.forEach(pin => {
|
||
pin.elements.container.classList.remove('binding');
|
||
});
|
||
return;
|
||
}
|
||
|
||
// Find the next unset pin in binding order
|
||
let bindingPinFound = false;
|
||
|
||
for (let order = 0; order < this.pinCount; order++) {
|
||
const nextPin = this.pins.find(p => p.binding === order && !p.isSet);
|
||
if (nextPin) {
|
||
// Mark this pin as binding
|
||
this.pins.forEach(pin => {
|
||
pin.elements.container.classList.toggle('binding', pin.index === nextPin.index);
|
||
});
|
||
bindingPinFound = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
// If no binding pin was found (all pins set), remove binding class from all
|
||
if (!bindingPinFound) {
|
||
this.pins.forEach(pin => {
|
||
pin.elements.container.classList.remove('binding');
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
// Register the lockpicking minigame with the framework
|
||
MinigameFramework.registerScene('lockpicking', LockpickingMinigame);
|
||
|
||
// Replacement for the startLockpickingMinigame function
|
||
function startLockpickingMinigame(lockable, scene, difficulty = 'medium', callback) {
|
||
// Initialize the framework if not already done
|
||
if (!MinigameFramework.mainGameScene) {
|
||
MinigameFramework.init(scene);
|
||
}
|
||
|
||
// Start the lockpicking minigame
|
||
MinigameFramework.startMinigame('lockpicking', {
|
||
lockable: lockable,
|
||
difficulty: difficulty,
|
||
onComplete: (success, result) => {
|
||
if (success) {
|
||
debugLog('LOCKPICK SUCCESS', null, 1);
|
||
gameAlert(`Successfully picked the lock!`, 'success', 'Lockpicking', 4000);
|
||
callback();
|
||
} else {
|
||
debugLog('LOCKPICK FAILED', null, 2);
|
||
gameAlert(`Failed to pick the lock.`, 'error', 'Lockpicking', 4000);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// 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';
|
||
|
||
// Store the currently hovered device, if any
|
||
const hoveredDevice = document.querySelector('.bluetooth-device:hover');
|
||
const hoveredDeviceId = hoveredDevice ? hoveredDevice.dataset.id : null;
|
||
|
||
// Add Bluetooth-locked items from inventory to the main bluetoothDevices array
|
||
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);
|
||
}
|
||
}
|
||
});
|
||
|
||
// Filter devices based on search and category
|
||
let filteredDevices = [...bluetoothDevices];
|
||
|
||
// Apply category filter
|
||
if (activeCategory === 'nearby') {
|
||
filteredDevices = filteredDevices.filter(device => device.nearby);
|
||
} else if (activeCategory === 'saved') {
|
||
filteredDevices = filteredDevices.filter(device => device.saved);
|
||
}
|
||
|
||
// Apply search filter
|
||
if (searchTerm) {
|
||
filteredDevices = filteredDevices.filter(device =>
|
||
device.name.toLowerCase().includes(searchTerm) ||
|
||
device.mac.toLowerCase().includes(searchTerm) ||
|
||
device.details.toLowerCase().includes(searchTerm)
|
||
);
|
||
}
|
||
|
||
// Sort devices with nearby ones first, then by signal strength (highest first for nearby), then by last seen (newest first)
|
||
filteredDevices.sort((a, b) => {
|
||
// Inventory items first
|
||
if (a.inInventory !== b.inInventory) {
|
||
return a.inInventory ? -1 : 1;
|
||
}
|
||
|
||
// Then nearby items
|
||
if (a.nearby !== b.nearby) {
|
||
return a.nearby ? -1 : 1;
|
||
}
|
||
|
||
// For nearby devices, sort by signal strength
|
||
if (a.nearby && b.nearby && a.signalStrength !== b.signalStrength) {
|
||
return b.signalStrength - a.signalStrength;
|
||
}
|
||
|
||
return new Date(b.lastSeen) - new Date(a.lastSeen);
|
||
});
|
||
|
||
// Clear current content
|
||
bluetoothContent.innerHTML = '';
|
||
|
||
// Add devices
|
||
if (filteredDevices.length === 0) {
|
||
if (searchTerm) {
|
||
bluetoothContent.innerHTML = '<div class="bluetooth-device">No devices match your search.</div>';
|
||
} else if (activeCategory !== 'all') {
|
||
bluetoothContent.innerHTML = `<div class="bluetooth-device">No ${activeCategory} devices found.</div>`;
|
||
} else {
|
||
bluetoothContent.innerHTML = '<div class="bluetooth-device">No devices detected yet.</div>';
|
||
}
|
||
} else {
|
||
filteredDevices.forEach(device => {
|
||
const deviceElement = document.createElement('div');
|
||
deviceElement.className = 'bluetooth-device';
|
||
deviceElement.dataset.id = device.id;
|
||
|
||
// If this was the hovered device, add the hover class
|
||
if (hoveredDeviceId && device.id === hoveredDeviceId) {
|
||
deviceElement.classList.add('hover-preserved');
|
||
}
|
||
|
||
// Format the timestamp
|
||
const timestamp = new Date(device.lastSeen);
|
||
const formattedDate = timestamp.toLocaleDateString();
|
||
const formattedTime = timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||
|
||
// Get signal color based on strength
|
||
const getSignalColor = (strength) => {
|
||
if (strength >= 80) return '#00cc00'; // Strong - green
|
||
if (strength >= 50) return '#cccc00'; // Medium - yellow
|
||
return '#cc5500'; // Weak - orange
|
||
};
|
||
|
||
let deviceContent = `<div class="bluetooth-device-name">
|
||
<span>${device.name}</span>
|
||
<div class="bluetooth-device-icons">`;
|
||
|
||
if (device.nearby && typeof device.signalStrength === 'number') {
|
||
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
|
||
deviceContent += `<span class="bluetooth-device-icon">📶</span>`;
|
||
}
|
||
|
||
if (device.saved) {
|
||
deviceContent += `<span class="bluetooth-device-icon">💾</span>`;
|
||
}
|
||
|
||
if (device.inInventory) {
|
||
deviceContent += `<span class="bluetooth-device-icon">🎒</span>`;
|
||
}
|
||
|
||
deviceContent += `</div></div>`;
|
||
deviceContent += `<div class="bluetooth-device-details">MAC: ${device.mac}\n${device.details}</div>`;
|
||
|
||
|
||
|
||
deviceContent += `<div class="bluetooth-device-timestamp">Last seen: ${formattedDate} ${formattedTime}</div>`;
|
||
|
||
deviceElement.innerHTML = deviceContent;
|
||
|
||
// Toggle expanded state when clicked
|
||
deviceElement.addEventListener('click', (event) => {
|
||
|
||
deviceElement.classList.toggle('expanded');
|
||
|
||
// Mark as saved when expanded
|
||
if (!device.saved && deviceElement.classList.contains('expanded')) {
|
||
device.saved = true;
|
||
updateBluetoothCount();
|
||
updateBluetoothPanel();
|
||
}
|
||
});
|
||
|
||
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();
|
||
});
|
||
});
|
||
// Initialize Bluetooth count
|
||
updateBluetoothCount();
|
||
}
|
||
|
||
// 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);
|
||
}
|
||
|
||
// 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') {
|
||
filteredSamples = filteredSamples.filter(sample =>
|
||
sample.type === 'fingerprint'
|
||
);
|
||
}
|
||
// 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"></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;
|
||
|
||
// Toggle expanded state when clicked
|
||
sampleElement.addEventListener('click', () => {
|
||
sampleElement.classList.toggle('expanded');
|
||
});
|
||
|
||
biometricsContent.appendChild(sampleElement);
|
||
});
|
||
}
|
||
|
||
// 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');
|
||
if (biometricsToggle) {
|
||
biometricsToggle.addEventListener('click', toggleBiometricsPanel);
|
||
}
|
||
|
||
// Set up biometrics close button
|
||
const biometricsClose = document.getElementById('biometrics-close');
|
||
if (biometricsClose) {
|
||
biometricsClose.addEventListener('click', toggleBiometricsPanel);
|
||
}
|
||
|
||
// Set up search functionality
|
||
const biometricsSearch = document.getElementById('biometrics-search');
|
||
if (biometricsSearch) {
|
||
biometricsSearch.addEventListener('input', updateBiometricsPanel);
|
||
}
|
||
|
||
// Set up category filters
|
||
const categories = document.querySelectorAll('.biometrics-category');
|
||
categories.forEach(category => {
|
||
category.addEventListener('click', () => {
|
||
// Remove active class from all categories
|
||
categories.forEach(c => c.classList.remove('active'));
|
||
// Add active class to clicked category
|
||
category.classList.add('active');
|
||
// Update biometrics panel
|
||
updateBiometricsPanel();
|
||
});
|
||
});
|
||
|
||
// Initialize biometrics count
|
||
updateBiometricsCount();
|
||
|
||
// 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';
|
||
}
|
||
|
||
// 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;
|
||
}
|
||
}
|
||
|
||
// Function to initialize the toggle buttons container
|
||
function initializeToggleButtons() {
|
||
// Set up notes toggle button
|
||
const notesToggle = document.getElementById('notes-toggle');
|
||
if (notesToggle) {
|
||
notesToggle.addEventListener('click', toggleNotesPanel);
|
||
}
|
||
|
||
// Set up bluetooth toggle button
|
||
const bluetoothToggle = document.getElementById('bluetooth-toggle');
|
||
if (bluetoothToggle) {
|
||
bluetoothToggle.addEventListener('click', toggleBluetoothPanel);
|
||
}
|
||
|
||
// Set up biometrics toggle button
|
||
const biometricsToggle = document.getElementById('biometrics-toggle');
|
||
if (biometricsToggle) {
|
||
biometricsToggle.addEventListener('click', toggleBiometricsPanel);
|
||
}
|
||
}
|
||
|
||
// Call the initialization function when the game starts
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
initializeToggleButtons();
|
||
});
|
||
|
||
// Function to open the crypto workstation
|
||
function openCryptoWorkstation() {
|
||
debugLog('OPENING CRYPTO WORKSTATION', null, 1);
|
||
|
||
// Create popup
|
||
let popup = document.getElementById('laptop-popup');
|
||
if (!popup) {
|
||
popup = document.createElement('div');
|
||
popup.id = 'laptop-popup';
|
||
popup.innerHTML = `
|
||
<div class="popup-overlay"></div>
|
||
<div class="laptop-frame">
|
||
<div class="laptop-screen">
|
||
<div class="title-bar">
|
||
<span>CryptoWorkstation</span>
|
||
<button class="close-btn">×</button>
|
||
</div>
|
||
<div id="cyberchef-container"></div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
document.body.appendChild(popup);
|
||
|
||
// 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);
|
||
});
|
||
|
||
popup.querySelector('.close-btn').addEventListener('click', () => {
|
||
popup.style.display = 'none';
|
||
});
|
||
}
|
||
popup.style.display = 'flex';
|
||
}
|
||
|
||
// Add this function to handle biometric matching near the other utility functions
|
||
function performBiometricMatch(lockable, ownerFingerprint, qualityThreshold, type) {
|
||
// Get available biometric samples - assuming they're stored in a global variable
|
||
// This may need to be adjusted based on how your biometric samples are actually stored
|
||
const availableSamples = biometricSamples || [];
|
||
|
||
if (!availableSamples || availableSamples.length === 0) {
|
||
$("#biometric-status").text("No fingerprint samples available.");
|
||
gameAlert("No fingerprint samples found in database.", 'error', 'Scan Failed', 3000);
|
||
return;
|
||
}
|
||
|
||
// Find if we have the owner's fingerprint
|
||
const matchingSample = availableSamples.find(sample => sample.id === ownerFingerprint);
|
||
|
||
if (!matchingSample) {
|
||
// No matching sample found
|
||
$("#biometric-status").text("No match found. Access denied.");
|
||
gameAlert("Biometric authentication failed.", 'error', 'Access Denied', 3000);
|
||
|
||
setTimeout(function() {
|
||
$("#biometrics-panel").hide();
|
||
}, 2000);
|
||
return;
|
||
}
|
||
|
||
// Check quality against threshold
|
||
const sampleQuality = matchingSample.quality || 0;
|
||
|
||
if (sampleQuality >= qualityThreshold) {
|
||
// Successful match with sufficient quality
|
||
$("#biometric-status").text("Match found! Unlocking...");
|
||
|
||
setTimeout(function() {
|
||
// Unlock the target
|
||
unlockTarget(lockable, type, lockable.layer);
|
||
$("#biometrics-panel").hide();
|
||
|
||
// Play unlock sound if available
|
||
if (typeof playSound === 'function') {
|
||
playSound("unlock");
|
||
}
|
||
|
||
gameAlert(`Biometric match confirmed. ${type.charAt(0).toUpperCase() + type.slice(1)} unlocked.`, 'success', 'Access Granted', 4000);
|
||
debugLog('BIOMETRIC AUTHENTICATION SUCCESS', null, 1);
|
||
}, 1500);
|
||
} else {
|
||
// Match found but quality insufficient
|
||
$("#biometric-status").text("Sample quality insufficient. Access denied.");
|
||
gameAlert(`Biometric sample quality too low (${sampleQuality}/${qualityThreshold} required).`, 'error', 'Low Quality Sample', 3000);
|
||
debugLog('BIOMETRIC AUTHENTICATION FAILED - LOW QUALITY', null, 2);
|
||
|
||
setTimeout(function() {
|
||
$("#biometrics-panel").hide();
|
||
}, 2000);
|
||
}
|
||
}
|
||
|
||
// Function to find a matching Bluetooth device in the current room
|
||
function findMatchingBluetoothDevice(device) {
|
||
if (!device.scenarioData?.mac || !currentRoom) return null;
|
||
|
||
const targetMac = device.scenarioData.mac;
|
||
const roomObjects = Object.values(rooms[currentRoom].objects || {});
|
||
|
||
// Find any object in the current room with a matching MAC address
|
||
return roomObjects.find(obj =>
|
||
obj.scenarioData?.mac &&
|
||
obj.scenarioData.mac === targetMac &&
|
||
obj.name !== device.name // Ensure it's not the same device
|
||
);
|
||
}
|
||
|
||
// Dusting Minigame Scene implementation
|
||
class DustingMinigame extends MinigameFramework.MinigameScene {
|
||
constructor(container, params) {
|
||
super(container, params);
|
||
|
||
this.item = params.item;
|
||
|
||
// Game state variables - using framework's gameState as base
|
||
this.difficultySettings = {
|
||
easy: {
|
||
requiredCoverage: 0.3, // 30% of prints
|
||
maxOverDusted: 50, // Increased due to more cells
|
||
fingerprints: 60, // Increased proportionally
|
||
pattern: 'simple'
|
||
},
|
||
medium: {
|
||
requiredCoverage: 0.4, // 40% of prints
|
||
maxOverDusted: 40, // Increased due to more cells
|
||
fingerprints: 75, // Increased proportionally
|
||
pattern: 'medium'
|
||
},
|
||
hard: {
|
||
requiredCoverage: 0.5, // 50% of prints
|
||
maxOverDusted: 25, // Increased due to more cells
|
||
fingerprints: 90, // Increased proportionally
|
||
pattern: 'complex'
|
||
}
|
||
};
|
||
|
||
this.currentDifficulty = this.item.scenarioData.fingerprintDifficulty;
|
||
this.gridSize = 30;
|
||
this.fingerprintCells = new Set();
|
||
this.revealedPrints = 0;
|
||
this.overDusted = 0;
|
||
this.lastDustTime = {};
|
||
|
||
// Tools configuration
|
||
this.tools = [
|
||
{ name: 'Fine', size: 1, color: '#3498db', radius: 0 }, // Only affects current cell
|
||
{ name: 'Medium', size: 2, color: '#2ecc71', radius: 1 }, // Affects current cell and adjacent
|
||
{ name: 'Wide', size: 3, color: '#e67e22', radius: 2 } // Affects current cell and 2 cells around
|
||
];
|
||
this.currentTool = this.tools[1]; // Start with medium brush
|
||
}
|
||
|
||
init() {
|
||
// Call parent init to set up common components
|
||
super.init();
|
||
|
||
console.log("Dusting minigame initializing");
|
||
|
||
// Set container dimensions
|
||
this.container.style.width = '75%';
|
||
this.container.style.height = '75%';
|
||
this.container.style.padding = '20px';
|
||
|
||
// Set up header content
|
||
this.headerElement.innerHTML = `
|
||
<h3>Fingerprint Dusting</h3>
|
||
<p>Drag to dust the surface and reveal fingerprints. Avoid over-dusting!</p>
|
||
`;
|
||
|
||
// Configure game container
|
||
this.gameContainer.style.cssText = `
|
||
width: 80%;
|
||
height: 80%;
|
||
max-width: 600px;
|
||
max-height: 600px;
|
||
display: grid;
|
||
grid-template-columns: repeat(30, 1fr);
|
||
grid-template-rows: repeat(30, 1fr);
|
||
gap: 1px;
|
||
background: #1a1a1a;
|
||
padding: 5px;
|
||
margin: 70px auto 20px auto;
|
||
border-radius: 5px;
|
||
box-shadow: 0 0 15px rgba(0, 0, 0, 0.5) inset;
|
||
position: relative;
|
||
overflow: hidden;
|
||
cursor: crosshair;
|
||
`;
|
||
|
||
// Add background texture/pattern for a more realistic surface
|
||
const gridBackground = document.createElement('div');
|
||
gridBackground.style.cssText = `
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
opacity: 0.3;
|
||
pointer-events: none;
|
||
z-index: 0;
|
||
`;
|
||
|
||
// Create the grid pattern using encoded SVG
|
||
const svgGrid = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' fill='%23111'/%3E%3Cpath d='M0 50h100M50 0v100' stroke='%23222' stroke-width='0.5'/%3E%3Cpath d='M25 0v100M75 0v100M0 25h100M0 75h100' stroke='%23191919' stroke-width='0.3'/%3E%3C/svg%3E`;
|
||
|
||
gridBackground.style.backgroundImage = `url('${svgGrid}')`;
|
||
this.gameContainer.appendChild(gridBackground);
|
||
|
||
// Add tool selection
|
||
const toolsContainer = document.createElement('div');
|
||
toolsContainer.style.cssText = `
|
||
position: absolute;
|
||
bottom: 15px;
|
||
left: 15px;
|
||
display: flex;
|
||
gap: 10px;
|
||
z-index: 10;
|
||
flex-wrap: wrap;
|
||
max-width: 30%;
|
||
`;
|
||
|
||
this.tools.forEach(tool => {
|
||
const toolButton = document.createElement('button');
|
||
toolButton.className = `minigame-tool-button ${tool.name === this.currentTool.name ? 'active' : ''}`;
|
||
toolButton.textContent = tool.name;
|
||
toolButton.style.backgroundColor = tool.color;
|
||
|
||
toolButton.addEventListener('click', () => {
|
||
document.querySelectorAll('.minigame-tool-button').forEach(btn => {
|
||
btn.classList.remove('active');
|
||
});
|
||
toolButton.classList.add('active');
|
||
this.currentTool = tool;
|
||
});
|
||
|
||
toolsContainer.appendChild(toolButton);
|
||
});
|
||
this.container.appendChild(toolsContainer);
|
||
|
||
// Create particle container for dust effects
|
||
this.particleContainer = document.createElement('div');
|
||
this.particleContainer.style.cssText = `
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
pointer-events: none;
|
||
z-index: 5;
|
||
overflow: hidden;
|
||
`;
|
||
this.container.appendChild(this.particleContainer);
|
||
|
||
// Generate fingerprint pattern and set up cells
|
||
this.fingerprintCells = this.generateFingerprint(this.currentDifficulty);
|
||
this.setupGrid();
|
||
|
||
// Total prints and required prints calculations
|
||
this.totalPrints = this.fingerprintCells.size;
|
||
this.requiredPrints = Math.ceil(this.totalPrints * this.difficultySettings[this.currentDifficulty].requiredCoverage);
|
||
|
||
// Check initial progress
|
||
this.checkProgress();
|
||
}
|
||
|
||
// Set up the grid of cells
|
||
setupGrid() {
|
||
// Clear any existing cells
|
||
while (this.gameContainer.firstChild) {
|
||
this.gameContainer.removeChild(this.gameContainer.firstChild);
|
||
}
|
||
|
||
// Create grid cells
|
||
for (let y = 0; y < this.gridSize; y++) {
|
||
for (let x = 0; x < this.gridSize; x++) {
|
||
const cell = document.createElement('div');
|
||
cell.style.cssText = `
|
||
width: 100%;
|
||
height: 100%;
|
||
background: black;
|
||
position: relative;
|
||
transition: background-color 0.1s;
|
||
cursor: pointer;
|
||
`;
|
||
cell.dataset.x = x;
|
||
cell.dataset.y = y;
|
||
cell.dataset.dustLevel = '0';
|
||
cell.dataset.hasFingerprint = this.fingerprintCells.has(`${x},${y}`) ? 'true' : 'false';
|
||
|
||
this.gameContainer.appendChild(cell);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Override the framework's mouse event handlers
|
||
handleMouseMove(e) {
|
||
if (!this.gameState.isDragging) return;
|
||
|
||
// Get the cell element under the cursor
|
||
const cell = document.elementFromPoint(e.clientX, e.clientY);
|
||
if (!cell || !cell.dataset || cell.dataset.dustLevel === undefined) return;
|
||
|
||
// Get current cell coordinates
|
||
const centerX = parseInt(cell.dataset.x);
|
||
const centerY = parseInt(cell.dataset.y);
|
||
|
||
// Get a list of cells to dust based on the brush radius
|
||
const cellsToDust = [];
|
||
const radius = this.currentTool.radius;
|
||
|
||
// Add the current cell and cells within radius
|
||
for (let y = centerY - radius; y <= centerY + radius; y++) {
|
||
for (let x = centerX - radius; x <= centerX + radius; x++) {
|
||
// Skip cells outside the grid
|
||
if (x < 0 || x >= this.gridSize || y < 0 || y >= this.gridSize) continue;
|
||
|
||
// For medium brush, use a diamond pattern (taxicab distance)
|
||
if (this.currentTool.size === 2) {
|
||
// Manhattan distance: |x1-x2| + |y1-y2|
|
||
const distance = Math.abs(x - centerX) + Math.abs(y - centerY);
|
||
if (distance > radius) continue; // Skip if too far away
|
||
}
|
||
// For wide brush, use a circle pattern (Euclidean distance)
|
||
else if (this.currentTool.size === 3) {
|
||
// Euclidean distance: √[(x1-x2)² + (y1-y2)²]
|
||
const distance = Math.sqrt(Math.pow(x - centerX, 2) + Math.pow(y - centerY, 2));
|
||
if (distance > radius) continue; // Skip if too far away
|
||
}
|
||
|
||
// Find this cell in the DOM
|
||
const targetCell = this.gameContainer.querySelector(`[data-x="${x}"][data-y="${y}"]`);
|
||
if (targetCell) {
|
||
cellsToDust.push(targetCell);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Get cell position for particles (center cell)
|
||
const cellRect = cell.getBoundingClientRect();
|
||
const particleContainerRect = this.particleContainer.getBoundingClientRect();
|
||
const cellCenterX = (cellRect.left + cellRect.width / 2) - particleContainerRect.left;
|
||
const cellCenterY = (cellRect.top + cellRect.height / 2) - particleContainerRect.top;
|
||
|
||
// Process all cells to dust
|
||
cellsToDust.forEach(targetCell => {
|
||
const cellId = `${targetCell.dataset.x},${targetCell.dataset.y}`;
|
||
const currentTime = Date.now();
|
||
const dustLevel = parseInt(targetCell.dataset.dustLevel);
|
||
|
||
// Tool intensity affects dusting rate and particle effects
|
||
const toolIntensity = this.currentTool.size / 3; // 0.33 to 1
|
||
|
||
// Only allow dusting every 50-150ms for each cell (based on tool size)
|
||
const cooldown = 150 - (toolIntensity * 100); // 50ms for wide brush, 150ms for fine
|
||
|
||
if (!this.lastDustTime[cellId] || currentTime - this.lastDustTime[cellId] > cooldown) {
|
||
if (dustLevel < 3) {
|
||
// Increment dust level with a probability based on tool intensity
|
||
const dustProbability = toolIntensity * 0.5 + 0.1; // 0.1-0.6 chance based on tool
|
||
|
||
if (dustLevel < 1 || Math.random() < dustProbability) {
|
||
targetCell.dataset.dustLevel = (dustLevel + 1).toString();
|
||
this.updateCellColor(targetCell);
|
||
|
||
// Create dust particles for the current cell or at a position calculated for surrounding cells
|
||
if (targetCell === cell) {
|
||
// Center cell - use the already calculated position
|
||
const hasFingerprint = targetCell.dataset.hasFingerprint === 'true';
|
||
let particleColor = dustLevel === 1 ? '#666' : (hasFingerprint ? '#1aff1a' : '#aaa');
|
||
this.createDustParticles(cellCenterX, cellCenterY, toolIntensity, particleColor);
|
||
} else {
|
||
// For surrounding cells, calculate their relative position from the center cell
|
||
const targetCellRect = targetCell.getBoundingClientRect();
|
||
const targetCellX = (targetCellRect.left + targetCellRect.width / 2) - particleContainerRect.left;
|
||
const targetCellY = (targetCellRect.top + targetCellRect.height / 2) - particleContainerRect.top;
|
||
|
||
const hasFingerprint = targetCell.dataset.hasFingerprint === 'true';
|
||
let particleColor = dustLevel === 1 ? '#666' : (hasFingerprint ? '#1aff1a' : '#aaa');
|
||
|
||
// Create fewer particles for surrounding cells
|
||
const reducedIntensity = toolIntensity * 0.6;
|
||
this.createDustParticles(targetCellX, targetCellY, reducedIntensity, particleColor);
|
||
}
|
||
}
|
||
this.lastDustTime[cellId] = currentTime;
|
||
}
|
||
}
|
||
});
|
||
|
||
// Update progress after dusting
|
||
this.checkProgress();
|
||
}
|
||
|
||
// Use the framework's mouseDown handler directly
|
||
handleMouseDown(e) {
|
||
// Just start dusting immediately
|
||
this.handleMouseMove(e);
|
||
}
|
||
|
||
createDustParticles(x, y, intensity, color) {
|
||
const numParticles = Math.floor(5 + intensity * 5); // 5-10 particles based on intensity
|
||
|
||
for (let i = 0; i < numParticles; i++) {
|
||
const particle = document.createElement('div');
|
||
const size = Math.random() * 3 + 1; // 1-4px
|
||
const angle = Math.random() * Math.PI * 2;
|
||
const distance = Math.random() * 20 * intensity;
|
||
const duration = Math.random() * 1000 + 500; // 500-1500ms
|
||
|
||
particle.style.cssText = `
|
||
position: absolute;
|
||
width: ${size}px;
|
||
height: ${size}px;
|
||
background: ${color};
|
||
border-radius: 50%;
|
||
opacity: ${Math.random() * 0.3 + 0.3};
|
||
top: ${y}px;
|
||
left: ${x}px;
|
||
transform: translate(-50%, -50%);
|
||
pointer-events: none;
|
||
z-index: 6;
|
||
`;
|
||
|
||
this.particleContainer.appendChild(particle);
|
||
|
||
// Animate the particle
|
||
const animation = particle.animate([
|
||
{
|
||
transform: 'translate(-50%, -50%)',
|
||
opacity: particle.style.opacity
|
||
},
|
||
{
|
||
transform: `translate(
|
||
calc(-50% + ${Math.cos(angle) * distance}px),
|
||
calc(-50% + ${Math.sin(angle) * distance}px)
|
||
)`,
|
||
opacity: 0
|
||
}
|
||
], {
|
||
duration: duration,
|
||
easing: 'cubic-bezier(0.25, 1, 0.5, 1)'
|
||
});
|
||
|
||
animation.onfinish = () => {
|
||
particle.remove();
|
||
};
|
||
}
|
||
}
|
||
|
||
updateCellColor(cell) {
|
||
const dustLevel = parseInt(cell.dataset.dustLevel);
|
||
const hasFingerprint = cell.dataset.hasFingerprint === 'true';
|
||
|
||
if (dustLevel === 0) {
|
||
cell.style.background = 'black';
|
||
cell.style.boxShadow = 'none';
|
||
}
|
||
else if (dustLevel === 1) {
|
||
cell.style.background = '#444';
|
||
cell.style.boxShadow = 'inset 0 0 3px rgba(255,255,255,0.2)';
|
||
}
|
||
else if (dustLevel === 2) {
|
||
if (hasFingerprint) {
|
||
cell.style.background = '#0f0';
|
||
cell.style.boxShadow = 'inset 0 0 5px rgba(0,255,0,0.5), 0 0 5px rgba(0,255,0,0.3)';
|
||
} else {
|
||
cell.style.background = '#888';
|
||
cell.style.boxShadow = 'inset 0 0 4px rgba(255,255,255,0.3)';
|
||
}
|
||
}
|
||
else {
|
||
cell.style.background = '#ccc';
|
||
cell.style.boxShadow = 'inset 0 0 5px rgba(255,255,255,0.5)';
|
||
}
|
||
}
|
||
|
||
checkProgress() {
|
||
this.revealedPrints = 0;
|
||
this.overDusted = 0;
|
||
|
||
this.gameContainer.childNodes.forEach(cell => {
|
||
if (cell.dataset) { // Check if it's a cell element
|
||
const dustLevel = parseInt(cell.dataset.dustLevel || '0');
|
||
const hasFingerprint = cell.dataset.hasFingerprint === 'true';
|
||
|
||
if (hasFingerprint && dustLevel === 2) this.revealedPrints++;
|
||
if (dustLevel === 3) this.overDusted++;
|
||
}
|
||
});
|
||
|
||
// Update progress display
|
||
this.progressContainer.innerHTML = `
|
||
<div style="margin-bottom: 5px;">
|
||
<span style="color: #2ecc71;">Found: ${this.revealedPrints}/${this.requiredPrints} required prints</span>
|
||
<span style="margin-left: 15px; color: ${this.overDusted > this.difficultySettings[this.currentDifficulty].maxOverDusted * 0.7 ? '#e74c3c' : '#fff'};">
|
||
Over-dusted: ${this.overDusted}/${this.difficultySettings[this.currentDifficulty].maxOverDusted} max
|
||
</span>
|
||
</div>
|
||
<div class="minigame-progress-container">
|
||
<div class="minigame-progress-bar" style="width: ${(this.revealedPrints/this.requiredPrints)*100}%;"></div>
|
||
</div>
|
||
`;
|
||
|
||
// Check fail condition first
|
||
if (this.overDusted >= this.difficultySettings[this.currentDifficulty].maxOverDusted) {
|
||
this.showFinalFailure("Too many over-dusted areas!");
|
||
return;
|
||
}
|
||
|
||
// Check win condition
|
||
if (this.revealedPrints >= this.requiredPrints) {
|
||
this.showFinalSuccess();
|
||
}
|
||
}
|
||
|
||
showFinalSuccess() {
|
||
// Calculate quality based on dusting precision
|
||
const dustPenalty = this.overDusted / this.difficultySettings[this.currentDifficulty].maxOverDusted; // 0-1
|
||
const coverageBonus = this.revealedPrints / this.totalPrints; // 0-1
|
||
|
||
// Higher quality for more coverage and less over-dusting
|
||
const quality = 0.7 + (coverageBonus * 0.25) - (dustPenalty * 0.15);
|
||
const qualityPercentage = Math.round(quality * 100);
|
||
const qualityRating = qualityPercentage >= 95 ? 'Perfect' :
|
||
qualityPercentage >= 85 ? 'Excellent' :
|
||
qualityPercentage >= 75 ? 'Good' : 'Acceptable';
|
||
|
||
// Build success message with detailed stats
|
||
const successHTML = `
|
||
<div style="font-weight: bold; font-size: 24px; margin-bottom: 10px;">Fingerprint successfully collected!</div>
|
||
<div style="font-size: 18px; margin-bottom: 15px;">Quality: ${qualityRating} (${qualityPercentage}%)</div>
|
||
<div style="font-size: 14px; color: #aaa;">
|
||
Prints revealed: ${this.revealedPrints}/${this.totalPrints}<br>
|
||
Over-dusted areas: ${this.overDusted}<br>
|
||
Difficulty: ${this.currentDifficulty.charAt(0).toUpperCase() + this.currentDifficulty.slice(1)}
|
||
</div>
|
||
`;
|
||
|
||
// Use the framework's success message system
|
||
this.showSuccess(successHTML, true, 2000);
|
||
|
||
// Disable further interaction
|
||
this.gameContainer.style.pointerEvents = 'none';
|
||
|
||
// Store result for onComplete callback
|
||
this.gameResult = {
|
||
quality: quality,
|
||
rating: qualityRating
|
||
};
|
||
}
|
||
|
||
showFinalFailure(reason) {
|
||
// Build failure message
|
||
const failureHTML = `
|
||
<div style="font-weight: bold; margin-bottom: 10px;">${reason}</div>
|
||
<div style="font-size: 16px; margin-top: 5px;">Try again with more careful dusting.</div>
|
||
`;
|
||
|
||
// Use the framework's failure message system
|
||
this.showFailure(failureHTML, true, 2000);
|
||
|
||
// Disable further interaction
|
||
this.gameContainer.style.pointerEvents = 'none';
|
||
}
|
||
|
||
start() {
|
||
super.start();
|
||
console.log("Dusting minigame started");
|
||
|
||
// Disable game movement in the main scene
|
||
if (this.params.scene) {
|
||
this.params.scene.input.mouse.enabled = false;
|
||
}
|
||
}
|
||
|
||
complete(success) {
|
||
// Call parent complete with result
|
||
super.complete(success, this.gameResult);
|
||
}
|
||
|
||
generateFingerprint(difficulty) {
|
||
// Existing fingerprint generation logic remains the same
|
||
const pattern = this.difficultySettings[difficulty].pattern;
|
||
const numPrints = this.difficultySettings[difficulty].fingerprints;
|
||
const newFingerprintCells = new Set();
|
||
const centerX = Math.floor(this.gridSize / 2);
|
||
const centerY = Math.floor(this.gridSize / 2);
|
||
|
||
if (pattern === 'simple') {
|
||
// Simple oval-like pattern
|
||
for (let i = 0; i < numPrints; i++) {
|
||
const angle = (i / numPrints) * Math.PI * 2;
|
||
const distance = 5 + Math.random() * 3;
|
||
const x = Math.floor(centerX + Math.cos(angle) * distance);
|
||
const y = Math.floor(centerY + Math.sin(angle) * distance);
|
||
|
||
if (x >= 0 && x < this.gridSize && y >= 0 && y < this.gridSize) {
|
||
newFingerprintCells.add(`${x},${y}`);
|
||
|
||
// Add a few adjacent cells to make it less sparse
|
||
for (let j = 0; j < 2; j++) {
|
||
const nx = x + Math.floor(Math.random() * 3) - 1;
|
||
const ny = y + Math.floor(Math.random() * 3) - 1;
|
||
if (nx >= 0 && nx < this.gridSize && ny >= 0 && ny < this.gridSize) {
|
||
newFingerprintCells.add(`${nx},${ny}`);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} else if (pattern === 'medium') {
|
||
// Medium complexity - spiral pattern with variations
|
||
for (let i = 0; i < numPrints; i++) {
|
||
const t = i / numPrints * 5;
|
||
const distance = 2 + t * 0.8;
|
||
const noise = Math.random() * 2 - 1;
|
||
const x = Math.floor(centerX + Math.cos(t * Math.PI * 2) * (distance + noise));
|
||
const y = Math.floor(centerY + Math.sin(t * Math.PI * 2) * (distance + noise));
|
||
|
||
if (x >= 0 && x < this.gridSize && y >= 0 && y < this.gridSize) {
|
||
newFingerprintCells.add(`${x},${y}`);
|
||
}
|
||
}
|
||
|
||
// Add whorls and arches
|
||
for (let i = 0; i < 20; i++) {
|
||
const angle = (i / 20) * Math.PI * 2;
|
||
const distance = 7;
|
||
const x = Math.floor(centerX + Math.cos(angle) * distance);
|
||
const y = Math.floor(centerY + Math.sin(angle) * distance);
|
||
|
||
if (x >= 0 && x < this.gridSize && y >= 0 && y < this.gridSize) {
|
||
newFingerprintCells.add(`${x},${y}`);
|
||
}
|
||
}
|
||
} else {
|
||
// Complex pattern - detailed whorls and ridge patterns
|
||
for (let i = 0; i < numPrints; i++) {
|
||
// Main loop - create a complex whorl pattern
|
||
const t = i / numPrints * 8;
|
||
const distance = 2 + t * 0.6;
|
||
const noise = Math.sin(t * 5) * 1.5;
|
||
const x = Math.floor(centerX + Math.cos(t * Math.PI * 2) * (distance + noise));
|
||
const y = Math.floor(centerY + Math.sin(t * Math.PI * 2) * (distance + noise));
|
||
|
||
if (x >= 0 && x < this.gridSize && y >= 0 && y < this.gridSize) {
|
||
newFingerprintCells.add(`${x},${y}`);
|
||
}
|
||
|
||
// Add bifurcations and ridge endings
|
||
if (i % 5 === 0) {
|
||
const bifAngle = t * Math.PI * 2 + Math.PI/4;
|
||
const bx = Math.floor(x + Math.cos(bifAngle) * 1);
|
||
const by = Math.floor(y + Math.sin(bifAngle) * 1);
|
||
if (bx >= 0 && bx < this.gridSize && by >= 0 && by < this.gridSize) {
|
||
newFingerprintCells.add(`${bx},${by}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Add delta patterns
|
||
for (let d = 0; d < 3; d++) {
|
||
const deltaAngle = (d / 3) * Math.PI * 2;
|
||
const deltaX = Math.floor(centerX + Math.cos(deltaAngle) * 8);
|
||
const deltaY = Math.floor(centerY + Math.sin(deltaAngle) * 8);
|
||
|
||
for (let r = 0; r < 5; r++) {
|
||
for (let a = 0; a < 3; a++) {
|
||
const rayAngle = deltaAngle + (a - 1) * Math.PI/4;
|
||
const rx = Math.floor(deltaX + Math.cos(rayAngle) * r);
|
||
const ry = Math.floor(deltaY + Math.sin(rayAngle) * r);
|
||
if (rx >= 0 && rx < this.gridSize && ry >= 0 && ry < this.gridSize) {
|
||
newFingerprintCells.add(`${rx},${ry}`);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Ensure we have at least the minimum number of cells
|
||
while (newFingerprintCells.size < numPrints) {
|
||
const x = centerX + Math.floor(Math.random() * 12 - 6);
|
||
const y = centerY + Math.floor(Math.random() * 12 - 6);
|
||
if (x >= 0 && x < this.gridSize && y >= 0 && y < this.gridSize) {
|
||
newFingerprintCells.add(`${x},${y}`);
|
||
}
|
||
}
|
||
|
||
return newFingerprintCells;
|
||
}
|
||
|
||
cleanup() {
|
||
super.cleanup();
|
||
|
||
// Re-enable game movement
|
||
if (this.params.scene) {
|
||
this.params.scene.input.mouse.enabled = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Register the dusting minigame with the framework
|
||
MinigameFramework.registerScene('dusting', DustingMinigame);
|
||
|
||
// Replacement for the startDustingMinigame function
|
||
function startDustingMinigame(item) {
|
||
// Initialize the framework if not already done
|
||
if (!MinigameFramework.mainGameScene) {
|
||
MinigameFramework.init(item.scene);
|
||
}
|
||
|
||
// Start the dusting minigame
|
||
MinigameFramework.startMinigame('dusting', {
|
||
item: item,
|
||
scene: item.scene,
|
||
onComplete: (success, result) => {
|
||
if (success) {
|
||
debugLog('DUSTING SUCCESS', result, 1);
|
||
|
||
// Add fingerprint to gameState
|
||
if (!gameState.biometricSamples) {
|
||
gameState.biometricSamples = [];
|
||
}
|
||
|
||
const sample = {
|
||
id: generateFingerprintData(item),
|
||
type: 'fingerprint',
|
||
owner: item.scenarioData.fingerprintOwner,
|
||
quality: result.quality, // Quality between 0.7 and ~1.0
|
||
data: generateFingerprintData(item),
|
||
timestamp: Date.now()
|
||
};
|
||
|
||
gameState.biometricSamples.push(sample);
|
||
|
||
// Mark item as collected
|
||
if (item.scenarioData) {
|
||
item.scenarioData.hasFingerprint = false;
|
||
}
|
||
|
||
// Update the biometrics panel and count
|
||
updateBiometricsPanel();
|
||
updateBiometricsCount();
|
||
|
||
// Show notification
|
||
gameAlert(`Collected ${sample.owner}'s fingerprint sample (${result.rating} quality)`, 'success', 'Sample Acquired', 3000);
|
||
} else {
|
||
debugLog('DUSTING FAILED', null, 2);
|
||
gameAlert(`Failed to collect the fingerprint sample.`, 'error', 'Dusting Failed', 3000);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// Add event listener for window resize
|
||
window.addEventListener('resize', () => {
|
||
const width = window.innerWidth * 0.80;
|
||
const height = window.innerHeight * 0.80;
|
||
game.scale.resize(width, height);
|
||
// TODO: Adjust inventory display position based on new size
|
||
});
|
||
</script>
|
||
</body>
|
||
</html> |