mirror of
https://github.com/cliffe/BreakEscape.git
synced 2026-02-21 11:18:08 +00:00
7302 lines
304 KiB
HTML
7302 lines
304 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 */
|
||
}
|
||
</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">
|
||
<span>📝</span>
|
||
<div id="notes-count">0</div>
|
||
</div>
|
||
<div id="bluetooth-toggle" style="display: none;">
|
||
<span>📡</span>
|
||
<div id="bluetooth-count">0</div>
|
||
</div>
|
||
<div id="biometrics-toggle" style="display: none;">
|
||
<span>👆</span>
|
||
<div id="biometrics-count">0</div>
|
||
</div>
|
||
</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) {
|
||
alert('INVENTORY ITEM CLICKED');
|
||
// 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 = {};
|
||
|
||
objectsLayer.objects.forEach(obj => {
|
||
// Find matching object in scenario data
|
||
const scenarioObject = gameScenario.rooms[roomId].objects.find(
|
||
item => item.type === obj.name
|
||
);
|
||
|
||
// Check if this object should be active in the current scenario
|
||
const isActiveObject = scenarioObject !== undefined;
|
||
|
||
const sprite = this.add.sprite(
|
||
position.x + obj.x,
|
||
position.y + (obj.gid !== undefined ? obj.y - obj.height : obj.y),
|
||
obj.name
|
||
);
|
||
|
||
sprite.setOrigin(0, 0);
|
||
sprite.name = obj.name;
|
||
sprite.setInteractive({ useHandCursor: true });
|
||
sprite.setDepth(1001);
|
||
sprite.originalAlpha = 1;
|
||
sprite.active = isActiveObject;
|
||
|
||
// Store scenario data with sprite for later use
|
||
if (isActiveObject) {
|
||
sprite.scenarioData = scenarioObject;
|
||
}
|
||
|
||
// Initially hide all objects - they'll be shown when room is revealed
|
||
sprite.setVisible(false);
|
||
|
||
if (obj.rotation) {
|
||
sprite.setRotation(Phaser.Math.DegToRad(obj.rotation));
|
||
}
|
||
|
||
rooms[roomId].objects[obj.name] = sprite;
|
||
|
||
// Add click handler for all objects
|
||
sprite.on('pointerdown', () => {
|
||
if (isActiveObject) {
|
||
debugLog('OBJECT CLICKED', { name: obj.name }, 2);
|
||
handleObjectInteraction(sprite);
|
||
} else {
|
||
gameAlert("Nothing of note here", 'info', '', 2000);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
} 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,
|
||
type: sprite.scenarioData?.type
|
||
}, 2);
|
||
|
||
if (!sprite || !sprite.scenarioData) {
|
||
console.warn('Invalid sprite or missing scenario data');
|
||
return;
|
||
}
|
||
|
||
// 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,
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
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.hasSpecialPurpose; // 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.name];
|
||
|
||
const itemIdentifier = createItemIdentifier(sprite.scenarioData);
|
||
|
||
const isInInventory = inventory.items.some(item =>
|
||
item && createItemIdentifier(item.scenarioData) === itemIdentifier
|
||
);
|
||
|
||
if (isInRoom && !isInInventory) {
|
||
debugLog('INVENTORY ITEM ADDED', { item: itemIdentifier }, 2);
|
||
addToInventory(sprite);
|
||
}
|
||
} else {
|
||
// For informational notes, just remove them from the room after reading
|
||
if (currentRoom &&
|
||
rooms[currentRoom] &&
|
||
rooms[currentRoom].objects &&
|
||
rooms[currentRoom].objects[sprite.name]) {
|
||
|
||
const roomObj = rooms[currentRoom].objects[sprite.name];
|
||
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 {
|
||
// Check if the item is already in the inventory using the unique identifier
|
||
const itemIdentifier = createItemIdentifier(sprite.scenarioData);
|
||
console.log(`Checking if item ${itemIdentifier} is already in inventory`);
|
||
|
||
const isAlreadyInInventory = inventory.items.some(item =>
|
||
createItemIdentifier(item.scenarioData) === itemIdentifier
|
||
);
|
||
|
||
if (isAlreadyInInventory) {
|
||
console.log(`Item ${itemIdentifier} is already in inventory, not adding again`);
|
||
return false;
|
||
}
|
||
|
||
// Remove from room if it exists
|
||
if (currentRoom &&
|
||
rooms[currentRoom] &&
|
||
rooms[currentRoom].objects &&
|
||
rooms[currentRoom].objects[sprite.name]) {
|
||
|
||
const roomObj = rooms[currentRoom].objects[sprite.name];
|
||
roomObj.setVisible(false);
|
||
roomObj.active = false;
|
||
}
|
||
sprite.setVisible(false);
|
||
|
||
const scene = sprite.scene;
|
||
|
||
// Create new sprite for inventory
|
||
const inventorySprite = scene.add.sprite(
|
||
inventory.items.length * 60, // Remove the +100 offset
|
||
0,
|
||
sprite.name
|
||
);
|
||
|
||
// inventorySprite.setScale(0.8);
|
||
inventorySprite.setInteractive({ useHandCursor: true, pixelPerfect: true });
|
||
inventorySprite.scenarioData = {
|
||
...sprite.scenarioData,
|
||
foundIn: currentRoom ? gameScenario.rooms[currentRoom].name || currentRoom : 'unknown location'
|
||
};
|
||
inventorySprite.name = sprite.name;
|
||
|
||
// Set depth higher than container
|
||
inventorySprite.setDepth(2003);
|
||
|
||
// Add pointer events
|
||
inventorySprite.on('pointerdown', function(pointer) {
|
||
// Handle inventory item interaction
|
||
});
|
||
|
||
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) {
|
||
// Combine multiple properties to create a unique identifier
|
||
const identifierParts = [
|
||
scenarioData.type,
|
||
scenarioData.name,
|
||
// Add more unique properties if available
|
||
scenarioData.key_id, // For keys
|
||
scenarioData.requires, // For locks
|
||
scenarioData.text // For readable items
|
||
].filter(Boolean); // Remove any undefined/null values
|
||
|
||
return identifierParts.join('|');
|
||
}
|
||
|
||
// Add this new function after the other function definitions
|
||
function setupDoorOverlapChecks() {
|
||
const DOOR_INTERACTION_RANGE = 2 * TILE_SIZE;
|
||
|
||
Object.entries(rooms).forEach(([roomId, room]) => {
|
||
if (!room.doorsLayer) return;
|
||
|
||
const doorTiles = room.doorsLayer.getTilesWithin().filter(tile => tile.index !== -1);
|
||
|
||
doorTiles.forEach(doorTile => {
|
||
const worldX = room.doorsLayer.x + (doorTile.x * TILE_SIZE);
|
||
const worldY = room.doorsLayer.y + (doorTile.y * TILE_SIZE);
|
||
|
||
const zone = this.add.zone(worldX + TILE_SIZE/2, worldY + TILE_SIZE/2, TILE_SIZE, TILE_SIZE);
|
||
zone.setInteractive({ useHandCursor: true });
|
||
|
||
zone.on('pointerdown', () => {
|
||
console.log('Door clicked:', { doorTile, room });
|
||
const distance = Phaser.Math.Distance.Between(
|
||
player.x, player.y,
|
||
worldX + TILE_SIZE/2, worldY + TILE_SIZE/2
|
||
);
|
||
|
||
if (distance <= DOOR_INTERACTION_RANGE) {
|
||
if (doorTile.properties?.locked) {
|
||
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> |