Files
BreakEscape/index.html

7171 lines
298 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Office Adventure Game</title>
<style>
body {
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: #333;
}
#game-container {
position: relative;
}
#loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-family: Arial, sans-serif;
font-size: 24px;
display: none;
}
/* 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;
}
</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: 1280,
height: 720,
parent: 'game-container',
pixelArt: true,
physics: {
default: 'arcade',
arcade: {
gravity: { y: 0 },
debug: true
}
},
scene: {
preload: preload,
create: create,
update: update
},
inventory: {
items: [],
display: null
}
};
const TILE_SIZE = 48;
const DOOR_ALIGN_OVERLAP = 48*3;
const GRID_SIZE = 32;
const MOVEMENT_SPEED = 150;
const ARRIVAL_THRESHOLD = 8;
const PATH_UPDATE_INTERVAL = 500;
const STUCK_THRESHOLD = 1;
const STUCK_TIME = 500;
// Hide rooms initially and on exit
const hideRoomsInitially = true;
const hideRoomsOnExit = false;
const hideNonAdjacentRooms = false;
// 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');
// 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 first
player = this.add.rectangle(400, 300, 32, 32, 0xff0000);
this.physics.add.existing(player);
player.body.setSize(16, 16);
player.body.setOffset(8, 8);
player.body.setCollideWorldBounds(true);
player.body.setBounce(0);
player.body.setDrag(0);
player.body.setFriction(0);
player.setDepth(1000);
// Initialize room layout after player creation
initializeRooms.call(this);
validateDoorsByRoomOverlap.call(this);
// Hide all rooms initially if hideRoomsInitially is true
if (hideRoomsInitially) {
Object.keys(gameScenario.rooms).forEach(roomId => {
hideRoom.call(this, roomId);
});
}
// Explicitly reveal the starting room and ensure its doors are visible
const startingRoom = gameScenario.startRoom;
revealRoom.call(this, startingRoom);
// Force doors visibility for starting room
if (rooms[startingRoom] && rooms[startingRoom].doorsLayer) {
rooms[startingRoom].doorsLayer.setVisible(true);
rooms[startingRoom].doorsLayer.setAlpha(1);
console.log(`Starting room doors layer:`, {
visible: rooms[startingRoom].doorsLayer.visible,
alpha: rooms[startingRoom].doorsLayer.alpha,
depth: rooms[startingRoom].doorsLayer.depth
});
}
// Setup camera
this.cameras.main.startFollow(player);
this.cameras.main.setZoom(1);
// Setup input with proper context
this.input.on('pointerdown', (pointer) => {
// Check if click is in inventory area
const inventoryArea = {
y: this.cameras.main.height - 70,
height: 70
};
if (pointer.y > inventoryArea.y) {
// Find clicked inventory item
const clickedItem = inventory.items.find(item => {
if (!item) return false;
const bounds = item.getBounds();
return Phaser.Geom.Rectangle.Contains(
bounds,
pointer.x,
pointer.y
);
});
if (clickedItem) {
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);
});
// creates the inventory display
createInventoryDisplay.call(this);
// Add this new call after all rooms are created
processAllDoorCollisions.call(this);
// Initialize pathfinder
initializePathfinder.call(this);
// Initialize game systems
initializeInventory.call(this);
// 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);
// 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() {
// 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);
targetPoint = { x, y };
isMoving = true;
}
// updates the player's movement
// moves the player towards the target point
// stops if a collision is detected
function updatePlayerMovement() {
if (!isMoving || !targetPoint) {
if (player.body.velocity.x !== 0 || player.body.velocity.y !== 0) {
player.body.setVelocity(0, 0);
}
return;
}
// Cache player position
const px = player.x;
const py = player.y;
// Use squared distance for performance
const dx = targetPoint.x - px;
const dy = targetPoint.y - py;
const distanceSq = dx * dx + dy * dy;
// Reached target point
if (distanceSq < ARRIVAL_THRESHOLD * ARRIVAL_THRESHOLD) {
isMoving = false;
player.body.setVelocity(0, 0);
return;
}
// Only check room transitions periodically
const movedX = Math.abs(px - lastPlayerPosition.x);
const movedY = Math.abs(py - lastPlayerPosition.y);
if (movedX > ROOM_CHECK_THRESHOLD || movedY > ROOM_CHECK_THRESHOLD) {
updatePlayerRoom();
lastPlayerPosition.x = px;
lastPlayerPosition.y = py;
}
// Normalize movement vector for consistent speed
const distance = Math.sqrt(distanceSq);
const velocityX = (dx / distance) * MOVEMENT_SPEED;
const velocityY = (dy / distance) * MOVEMENT_SPEED;
// Only update velocity if it changed significantly
const currentVX = player.body.velocity.x;
const currentVY = player.body.velocity.y;
const velocityDiffX = Math.abs(currentVX - velocityX);
const velocityDiffY = Math.abs(currentVY - velocityY);
if (velocityDiffX > 1 || velocityDiffY > 1) {
player.body.setVelocity(velocityX, velocityY);
}
// Stop if collision detected
if (player.body.blocked.none === false) {
isMoving = false;
player.body.setVelocity(0, 0);
}
}
// creates the inventory display
// creates the background and slot outlines
function createInventoryDisplay() {
// Create slot outlines
const slotsContainer = this.add.container(110, this.cameras.main.height - 60)
.setScrollFactor(0)
.setDepth(2001);
// Create 10 slot outlines
for (let i = 0; i < 10; i++) {
const outline = this.add.rectangle(
i * 60,
0,
50,
50,
0x666666,
0.3
);
outline.setStrokeStyle(1, 0x666666);
slotsContainer.add(outline);
}
// Initialize inventory container with highest depth
inventory.container = this.add.container(110, this.cameras.main.height - 60)
.setScrollFactor(0)
.setDepth(2002);
// Modify the input event to check if clicking on inventory
this.input.on('pointerdown', (pointer) => {
// Convert pointer position to world coordinates
const worldPoint = this.cameras.main.getWorldPoint(pointer.x, pointer.y);
// Check if click is in inventory area
const inventoryArea = {
x: 100,
y: this.cameras.main.height - 70,
width: this.cameras.main.width - 200,
height: 70
};
if (pointer.y > inventoryArea.y) {
// Click is in inventory area, let the inventory sprites handle it
return;
}
// Otherwise, handle as movement click
debugLog('CLICK DETECTED', { x: worldPoint.x, y: worldPoint.y }, 3);
movePlayerToPoint.call(this, worldPoint.x, worldPoint.y);
});
}
// checks for object interactions
// highlights the object if the player is in range
// handles the click event for the object
function checkObjectInteractions() {
// Skip if not enough time has passed since last check
const currentTime = performance.now();
if (this.lastInteractionCheck &&
currentTime - this.lastInteractionCheck < INTERACTION_CHECK_INTERVAL) {
return;
}
this.lastInteractionCheck = currentTime;
const playerRoom = currentPlayerRoom;
if (!playerRoom || !rooms[playerRoom].objects) return;
// Cache player position
const px = player.x;
const py = player.y;
// Get only objects within viewport bounds plus some margin
const camera = this.cameras.main;
const margin = INTERACTION_RANGE;
const viewBounds = {
left: camera.scrollX - margin,
right: camera.scrollX + camera.width + margin,
top: camera.scrollY - margin,
bottom: camera.scrollY + camera.height + margin
};
Object.values(rooms[playerRoom].objects).forEach(obj => {
// Skip inactive objects and those outside viewport
if (!obj.active ||
obj.x < viewBounds.left ||
obj.x > viewBounds.right ||
obj.y < viewBounds.top ||
obj.y > viewBounds.bottom) {
return;
}
// Use squared distance for performance
const dx = px - obj.x;
const dy = py - obj.y;
const distanceSq = dx * dx + dy * dy;
if (distanceSq <= INTERACTION_RANGE_SQ) {
if (!obj.isHighlighted) {
obj.isHighlighted = true;
obj.setTint(0xdddddd); // Simple highlight without tween
}
} else if (obj.isHighlighted) {
obj.isHighlighted = false;
obj.clearTint();
}
});
}
// checks for room transitions
function checkRoomTransitions() {
// Now handled by physics overlap
}
// calculates the world bounds
function calculateWorldBounds() {
if (!gameScenario || !gameScenario.rooms) {
console.error('Game scenario not loaded properly');
// Return default bounds
return {
x: -1800,
y: -1800,
width: 3600,
height: 3600
};
}
let minX = -1800, minY = -1800, maxX = 1800, maxY = 1800;
// Check all room positions to determine world bounds
Object.values(gameScenario.rooms).forEach(room => {
const position = calculateRoomPositions()[room.id];
if (position) {
// Assuming each room is 800x600
minX = Math.min(minX, position.x);
minY = Math.min(minY, position.y);
maxX = Math.max(maxX, position.x + 800);
maxY = Math.max(maxY, position.y + 600);
}
});
// Add some padding
const padding = 200;
return {
x: minX - padding,
y: minY - padding,
width: (maxX - minX) + (padding * 2),
height: (maxY - minY) + (padding * 2)
};
}
// processes all door-wall interactions
function processAllDoorCollisions() {
Object.entries(rooms).forEach(([roomId, room]) => {
if (room.doorsLayer) {
const doorTiles = room.doorsLayer.getTilesWithin()
.filter(tile => tile.index !== -1);
// Find all rooms that overlap with this room
Object.entries(rooms).forEach(([otherId, otherRoom]) => {
if (roomsOverlap(room.position, otherRoom.position)) {
otherRoom.wallsLayers.forEach(wallLayer => {
processDoorCollisions(doorTiles, wallLayer, room.doorsLayer);
});
}
});
}
});
}
// processes door collisions
// sets the collision of the door tile to false
// visually indicates the door opening
function processDoorCollisions(doorTiles, wallLayer, doorsLayer) {
doorTiles.forEach(doorTile => {
// Convert door tile coordinates to world coordinates
const worldX = doorsLayer.x + (doorTile.x * doorsLayer.tilemap.tileWidth);
const worldY = doorsLayer.y + (doorTile.y * doorsLayer.tilemap.tileHeight);
// Convert world coordinates back to the wall layer's local coordinates
const wallX = Math.floor((worldX - wallLayer.x) / wallLayer.tilemap.tileWidth);
const wallY = Math.floor((worldY - wallLayer.y) / wallLayer.tilemap.tileHeight);
const wallTile = wallLayer.getTileAt(wallX, wallY);
if (wallTile) {
if (doorTile.properties?.locked) {
wallTile.setCollision(true);
} else {
wallTile.setCollision(false);
}
}
});
}
// checks if two rooms overlap
function roomsOverlap(pos1, pos2) {
// Add some tolerance for overlap detection
const OVERLAP_TOLERANCE = 48; // One tile width
const ROOM_WIDTH = 800;
const ROOM_HEIGHT = 600;
return !(pos1.x + ROOM_WIDTH - OVERLAP_TOLERANCE < pos2.x ||
pos1.x > pos2.x + ROOM_WIDTH - OVERLAP_TOLERANCE ||
pos1.y + ROOM_HEIGHT - OVERLAP_TOLERANCE < pos2.y ||
pos1.y > pos2.y + ROOM_HEIGHT - OVERLAP_TOLERANCE);
}
// initializes the pathfinder
// creates a grid of the world
function initializePathfinder() {
const worldBounds = this.physics.world.bounds;
const gridWidth = Math.ceil(worldBounds.width / GRID_SIZE);
const gridHeight = Math.ceil(worldBounds.height / GRID_SIZE);
try {
pathfinder = new EasyStar.js();
const grid = Array(gridHeight).fill().map(() => Array(gridWidth).fill(0));
// Mark walls
Object.values(rooms).forEach(room => {
room.wallsLayers.forEach(wallLayer => {
wallLayer.getTilesWithin().forEach(tile => {
// Only mark as unwalkable if the tile collides AND hasn't been disabled for doors
if (tile.collides && tile.canCollide) { // Add check for canCollide
const gridX = Math.floor((tile.x * TILE_SIZE + wallLayer.x - worldBounds.x) / GRID_SIZE);
const gridY = Math.floor((tile.y * TILE_SIZE + wallLayer.y - worldBounds.y) / GRID_SIZE);
if (gridX >= 0 && gridX < gridWidth && gridY >= 0 && gridY < gridHeight) {
grid[gridY][gridX] = 1;
}
}
});
});
});
pathfinder.setGrid(grid);
pathfinder.setAcceptableTiles([0]);
pathfinder.enableDiagonals();
console.log('Pathfinding initialized successfully');
} catch (error) {
console.error('Error initializing pathfinder:', error);
}
}
// smooths the path
function smoothPath(path) {
if (path.length <= 2) return path;
const smoothed = [path[0]];
for (let i = 1; i < path.length - 1; i++) {
const prev = path[i - 1];
const current = path[i];
const next = path[i + 1];
// Calculate the angle change
const angle1 = Phaser.Math.Angle.Between(prev.x, prev.y, current.x, current.y);
const angle2 = Phaser.Math.Angle.Between(current.x, current.y, next.x, next.y);
const angleDiff = Math.abs(Phaser.Math.Angle.Wrap(angle1 - angle2));
// Only keep points where there's a significant direction change
if (angleDiff > 0.2) { // About 11.5 degrees
smoothed.push(current);
}
}
smoothed.push(path[path.length - 1]);
return smoothed;
}
// debugs the path
function debugPath(path) {
if (!path) return;
console.log('Current path:', {
pathLength: path.length,
currentTarget: path[0],
playerPos: { x: player.x, y: player.y },
isMoving: isMoving
});
}
// optimizes the path
function optimizePath(path) {
if (path.length <= 2) return path;
const optimized = [path[0]];
let currentPoint = 0;
while (currentPoint < path.length - 1) {
// Look ahead as far as possible along a straight line
let furthestVisible = currentPoint + 1;
for (let i = currentPoint + 2; i < path.length; i++) {
if (canMoveDirectly(path[currentPoint], path[i])) {
furthestVisible = i;
} else {
break;
}
}
// Add the furthest visible point to our optimized path
optimized.push(path[furthestVisible]);
currentPoint = furthestVisible;
}
return optimized;
}
// checks if direct movement is possible
function canMoveDirectly(start, end) {
// Check if there are any walls between start and end points
const distance = Phaser.Math.Distance.Between(start.x, start.y, end.x, end.y);
const angle = Phaser.Math.Angle.Between(start.x, start.y, end.x, end.y);
// Check several points along the line
const steps = Math.ceil(distance / (GRID_SIZE / 2));
const stepSize = distance / steps;
for (let i = 1; i < steps; i++) {
const pointX = start.x + Math.cos(angle) * (stepSize * i);
const pointY = start.y + Math.sin(angle) * (stepSize * i);
// Check if this point intersects with any walls
let collision = false;
Object.values(rooms).forEach(room => {
room.wallsLayers.forEach(wallLayer => {
const tile = wallLayer.getTileAtWorldXY(pointX, pointY);
if (tile && tile.collides) {
collision = true;
}
});
});
if (collision) {
return false;
}
}
return true;
}
// updates the player's room
function updatePlayerRoom() {
// Update last position
lastPlayerPosition = { x: player.x, y: player.y };
let overlappingRooms = [];
// Check all rooms for overlap
for (const [roomId, room] of Object.entries(rooms)) {
const bounds = getRoomBounds(roomId);
if (isPlayerInBounds(bounds)) {
overlappingRooms.push(roomId);
// Reveal room if not already visible
if (!discoveredRooms.has(roomId)) {
console.log(`Player overlapping room: ${roomId}`);
revealRoom(roomId);
}
}
}
// If we're not overlapping any rooms
if (overlappingRooms.length === 0) {
console.log('Player not in any room');
currentPlayerRoom = null;
return null;
}
// Update current room (use the first overlapping room as the "main" room)
if (currentPlayerRoom !== overlappingRooms[0]) {
console.log(`Player's main room changed to: ${overlappingRooms[0]}`);
currentPlayerRoom = overlappingRooms[0];
onRoomChange(overlappingRooms[0]);
}
return currentPlayerRoom;
}
// gets the bounds of a room
function getRoomBounds(roomId) {
const room = rooms[roomId];
return {
x: room.position.x,
y: room.position.y,
width: room.map.widthInPixels,
height: room.map.heightInPixels
};
}
// checks if the player is in bounds
function isPlayerInBounds(bounds) {
const buffer = 0; // Changed from TILE_SIZE (48) to 0
return (
player.x >= bounds.x - buffer &&
player.x <= bounds.x + bounds.width + buffer &&
player.y >= bounds.y - buffer &&
player.y <= bounds.y + bounds.height + buffer
);
}
// handles room changes
// reveals the new room
// hides rooms that aren't connected and aren't currently being overlapped
function onRoomChange(newRoomId) {
// Reveal the new room (although it should already be revealed)
revealRoom.call(this, newRoomId);
// Only hide rooms that aren't connected AND aren't currently being overlapped
Object.keys(rooms).forEach(roomId => {
const bounds = getRoomBounds(roomId);
const playerOverlapping = isPlayerInBounds(bounds);
if (hideNonAdjacentRooms && !playerOverlapping && !isConnectedRoom(newRoomId, roomId)) {
hideRoom.call(this, roomId);
}
});
}
// hides a room
function hideRoom(roomId) {
if (rooms[roomId]) {
const room = rooms[roomId];
// Hide all layers
Object.values(room.layers).forEach(layer => {
if (layer && layer.setVisible) {
layer.setVisible(false);
layer.setAlpha(0);
}
});
// Hide all objects (both active and inactive)
if (room.objects) {
Object.values(room.objects).forEach(obj => {
if (obj && obj.setVisible) {
obj.setVisible(false);
}
});
}
}
}
// checks if rooms are connected
function isConnectedRoom(currentRoomId, checkRoomId) {
const currentRoom = gameScenario.rooms[currentRoomId];
if (!currentRoom || !currentRoom.connections) return false;
// Check all connections
return Object.values(currentRoom.connections).some(connection => {
if (Array.isArray(connection)) {
return connection.includes(checkRoomId);
}
return connection === checkRoomId;
});
}
// handles interactions with objects
// displays the object's data in an alert
function handleObjectInteraction(sprite) {
// 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;
}
const scene = sprite.scene;
// Create new sprite for inventory
const inventorySprite = scene.add.sprite(
inventory.items.length * 60 + 100,
0,
sprite.name
);
inventorySprite.setScale(0.8);
inventorySprite.setInteractive({ useHandCursor: true, pixelPerfect: true });
inventorySprite.scenarioData = {
...sprite.scenarioData,
foundIn: currentRoom ? gameScenario.rooms[currentRoom].name || currentRoom : 'unknown location'
};
inventorySprite.name = sprite.name;
// Set depth higher than container
inventorySprite.setDepth(2003);
// Add pointer events
inventorySprite.on('pointerdown', function(pointer) {
// Check if this is the Bluetooth scanner
if (this.scenarioData.type === "bluetooth_scanner") {
// Toggle the Bluetooth scanner panel
toggleBluetoothPanel();
return;
}
// Handle other inventory items as before
handleObjectInteraction(this);
});
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(110, this.cameras.main.height - 60) // Shifted 100px to the right
.setScrollFactor(0)
.setDepth(2001);
// Create 10 slot outlines
for (let i = 0; i < 10; i++) {
const outline = this.add.rectangle(
i * 60,
0,
50, // slightly smaller than spacing
50,
0x666666,
0.3
);
outline.setStrokeStyle(1, 0x666666);
slotsContainer.add(outline);
}
// Initialize inventory container
inventory.container = this.add.container(10, this.cameras.main.height - 60)
.setScrollFactor(0)
.setDepth(2001);
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">&times;</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);
}
}
});
}
</script>
</body>
</html>