Files
BreakEscape/index.html
2025-03-08 13:19:58 +00:00

4622 lines
193 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: fixed;
bottom: 20px;
right: 20px;
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;
}
#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;
}
</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>
<div id="notes-toggle">
<span>📝</span>
<div id="notes-count">0</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 noteExists = gameNotes.some(note => note.title === title && note.text === text);
// If the note already exists, don't add it again
if (noteExists) {
debugLog(`Note "${title}" already exists, not adding duplicate`, 2);
return null;
}
const note = {
id: Date.now(),
title: title,
text: text,
timestamp: new Date(),
read: false,
important: important
};
gameNotes.push(note);
updateNotesPanel();
updateNotesCount();
// Show notification for new note
showNotification(`New note added: ${title}`, 'info', 'Note Added', 3000);
return note;
}
// Update the notes panel with current notes
function updateNotesPanel() {
const notesContent = document.getElementById('notes-content');
const searchTerm = document.getElementById('notes-search')?.value?.toLowerCase() || '';
// Get active category
const activeCategory = document.querySelector('.notes-category.active')?.dataset.category || 'all';
// Filter notes based on search and category
let filteredNotes = [...gameNotes];
// Apply category filter
if (activeCategory === 'important') {
filteredNotes = filteredNotes.filter(note => note.important);
} else if (activeCategory === 'unread') {
filteredNotes = filteredNotes.filter(note => !note.read);
}
// Apply search filter
if (searchTerm) {
filteredNotes = filteredNotes.filter(note =>
note.title.toLowerCase().includes(searchTerm) ||
note.text.toLowerCase().includes(searchTerm)
);
}
// Sort notes with important ones first, then by timestamp (newest first)
filteredNotes.sort((a, b) => {
if (a.important !== b.important) {
return a.important ? -1 : 1;
}
return b.timestamp - a.timestamp;
});
// Clear current content
notesContent.innerHTML = '';
// Add notes
if (filteredNotes.length === 0) {
if (searchTerm) {
notesContent.innerHTML = '<div class="note-item">No notes match your search.</div>';
} else if (activeCategory !== 'all') {
notesContent.innerHTML = `<div class="note-item">No ${activeCategory} notes found.</div>`;
} else {
notesContent.innerHTML = '<div class="note-item">No notes yet.</div>';
}
} else {
filteredNotes.forEach(note => {
const noteElement = document.createElement('div');
noteElement.className = 'note-item';
noteElement.dataset.id = note.id;
// Format the timestamp
const timestamp = new Date(note.timestamp);
const formattedDate = timestamp.toLocaleDateString();
const formattedTime = timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
let noteContent = `<div class="note-title">
<span>${note.title}</span>
<div class="note-icons">`;
if (note.important) {
noteContent += `<span class="note-icon">⭐</span>`;
}
if (!note.read) {
noteContent += `<span class="note-icon">📌</span>`;
}
noteContent += `</div></div>`;
noteContent += `<div class="note-text">${note.text}</div>`;
noteContent += `<div class="note-timestamp">${formattedDate} ${formattedTime}</div>`;
noteElement.innerHTML = noteContent;
// Toggle expanded state when clicked
noteElement.addEventListener('click', () => {
noteElement.classList.toggle('expanded');
// Mark as read when expanded
if (!note.read && noteElement.classList.contains('expanded')) {
note.read = true;
updateNotesCount();
updateNotesPanel();
}
});
notesContent.appendChild(noteElement);
});
}
}
// Update the unread notes count
function updateNotesCount() {
const notesCount = document.getElementById('notes-count');
unreadNotes = gameNotes.filter(note => !note.read).length;
notesCount.textContent = unreadNotes;
notesCount.style.display = unreadNotes > 0 ? 'flex' : 'none';
}
// Toggle the notes panel
function toggleNotesPanel() {
const notesPanel = document.getElementById('notes-panel');
const isVisible = notesPanel.style.display === 'block';
notesPanel.style.display = isVisible ? 'none' : 'block';
}
// Replace alert with our custom notification system
function gameAlert(message, type = 'info', title = '', duration = 5000) {
return showNotification(message, type, title, duration);
}
// Debug logging function that only logs when debug mode is active
function debugLog(message, data = null, level = 1) {
if (!debugMode || debugLevel < level) return;
// Check if the first argument is a string
if (typeof message === 'string') {
// Create the formatted debug message
const formattedMessage = `[DEBUG] === ${message} ===`;
// Determine color based on message content
let color = '#0077FF'; // Default blue for general info
let fontWeight = 'bold';
// Success messages - green
if (message.includes('SUCCESS') ||
message.includes('UNLOCKED') ||
message.includes('NOT LOCKED')) {
color = '#00AA00'; // Green
}
// Error/failure messages - red
else if (message.includes('FAIL') ||
message.includes('ERROR') ||
message.includes('NO LOCK REQUIREMENTS FOUND')) {
color = '#DD0000'; // Red
}
// Sensitive information - purple
else if (message.includes('PIN') ||
message.includes('PASSWORD') ||
message.includes('KEY') ||
message.includes('LOCK REQUIREMENTS')) {
color = '#AA00AA'; // Purple
}
// Add level indicator to the message
const levelIndicator = level > 1 ? ` [L${level}]` : '';
const finalMessage = formattedMessage + levelIndicator;
// Log with formatting
if (data) {
console.log(`%c${finalMessage}`, `color: ${color}; font-weight: ${fontWeight};`, data);
} else {
console.log(`%c${finalMessage}`, `color: ${color}; font-weight: ${fontWeight};`);
}
} else {
// If not a string, just log as is
console.log(message, data);
}
}
// Function to display tension debug info only when debug mode is active
function logTensionDebugInfo() {
if (!debugMode) return; // Skip all calculations if debug mode is off
// Your existing debug code here
debugLog("Tension debug information:", null, 2);
// Add other debug information as needed
}
// Listen for backtick key to toggle debug mode
document.addEventListener('keydown', function(event) {
// Toggle debug mode with backtick
if (event.key === '`') {
if (event.shiftKey) {
// Toggle visual debug mode with Shift+backtick
visualDebugMode = !visualDebugMode;
console.log(`%c[DEBUG] === VISUAL DEBUG MODE ${visualDebugMode ? 'ENABLED' : 'DISABLED'} ===`,
`color: ${visualDebugMode ? '#00AA00' : '#DD0000'}; font-weight: bold;`);
// Update physics debug display if game exists
if (game && game.scene && game.scene.scenes && game.scene.scenes[0]) {
const scene = game.scene.scenes[0];
if (scene.physics && scene.physics.world) {
scene.physics.world.drawDebug = debugMode && visualDebugMode;
}
}
} else if (event.ctrlKey) {
// Cycle through debug levels with Ctrl+backtick
if (debugMode) {
debugLevel = (debugLevel % 3) + 1; // Cycle through 1, 2, 3
console.log(`%c[DEBUG] === DEBUG LEVEL ${debugLevel} ===`,
`color: #0077FF; font-weight: bold;`);
}
} else {
// Regular debug mode toggle
debugMode = !debugMode;
console.log(`%c[DEBUG] === DEBUG MODE ${debugMode ? 'ENABLED' : 'DISABLED'} ===`,
`color: ${debugMode ? '#00AA00' : '#DD0000'}; font-weight: bold;`);
// Update physics debug display if game exists
if (game && game.scene && game.scene.scenes && game.scene.scenes[0]) {
const scene = game.scene.scenes[0];
if (scene.physics && scene.physics.world) {
scene.physics.world.drawDebug = debugMode && visualDebugMode;
}
}
}
}
});
// Initialize notes panel
document.addEventListener('DOMContentLoaded', function() {
// Set up notes toggle button
const notesToggle = document.getElementById('notes-toggle');
notesToggle.addEventListener('click', toggleNotesPanel);
// Set up notes close button
const notesClose = document.getElementById('notes-close');
notesClose.addEventListener('click', toggleNotesPanel);
// Set up search functionality
const notesSearch = document.getElementById('notes-search');
notesSearch.addEventListener('input', updateNotesPanel);
// Set up category filters
const categories = document.querySelectorAll('.notes-category');
categories.forEach(category => {
category.addEventListener('click', () => {
// Remove active class from all categories
categories.forEach(c => c.classList.remove('active'));
// Add active class to clicked category
category.classList.add('active');
// Update notes panel
updateNotesPanel();
});
});
// Initialize notes count
updateNotesCount();
});
// 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 = 500; // Scan every 500ms
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 spoofing
const SPOOFING_TIME = 3000; // 3 seconds to create spoof
const SPOOF_QUALITY_MULTIPLIER = 0.8; // Spoofed prints are slightly lower quality
// Add these constants for the dusting minigame
const DUST_COLORS = {
NONE: 0x000000,
LIGHT: 0x444444,
MEDIUM: 0x888888,
HEAVY: 0xcccccc,
REVEALED: 0x00ff00
};
const DEBUG_MODE = {
get enabled() { return debugMode; },
set enabled(value) { debugMode = value; },
toggle: function() {
debugMode = !debugMode;
// No need to log here as the global event listener will handle it
},
log: function(message, data = null) {
if (!debugMode) return;
if (data) {
console.log(`%c[DEBUG] === ${message} ===`, 'color: #0077FF; font-weight: bold;', data);
} else {
console.log(`%c[DEBUG] === ${message} ===`, 'color: #0077FF; font-weight: bold;');
}
}
};
// preloads the assets
function preload() {
// Show loading text
document.getElementById('loading').style.display = 'block';
// Load tilemap files and regular tilesets first
this.load.tilemapTiledJSON('room_reception', 'assets/rooms/room_reception.json');
this.load.tilemapTiledJSON('room_office', 'assets/rooms/room_office.json');
this.load.tilemapTiledJSON('room_ceo', 'assets/rooms/room_ceo.json');
this.load.tilemapTiledJSON('room_closet', 'assets/rooms/room_closet.json');
this.load.tilemapTiledJSON('room_servers', 'assets/rooms/room_servers.json');
this.load.image('Modern_Office_48x48', 'assets/Modern_Office_48x48.png');
this.load.image('Room_Builder_48x48', 'assets/Room_Builder_48x48.png');
this.load.image('19_Hospital_Shadowless_48x48', 'assets/19_Hospital_Shadowless_48x48.png');
this.load.image('18_Jail_Shadowless_48x48', 'assets/18_Jail_Shadowless_48x48.png');
this.load.image('1_Generic_Shadowless_48x48', 'assets/1_Generic_Shadowless_48x48.png');
this.load.image('11_Halloween_Shadowless_48x48', 'assets/11_Halloween_Shadowless_48x48.png');
this.load.image('5_Classroom_and_library_Shadowless_48x48', 'assets/5_Classroom_and_library_Shadowless_48x48.png');
// Load object sprites
this.load.image('pc', 'assets/objects/pc.png');
this.load.image('key', 'assets/objects/key.png');
this.load.image('notes', 'assets/objects/notes.png');
this.load.image('phone', 'assets/objects/phone.png');
this.load.image('suitcase', 'assets/objects/suitcase.png');
this.load.image('smartscreen', 'assets/objects/smartscreen.png');
this.load.image('photo', 'assets/objects/photo.png');
this.load.image('suitcase', 'assets/objects/suitcase.png');
this.load.image('safe', 'assets/objects/safe.png');
this.load.image('book', 'assets/objects/book.png');
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.json('gameScenarioJSON', 'assets/scenarios/ceo_exfil.json');
gameScenario = this.cache.json.get('gameScenarioJSON');
}
// creates the workstation
function addCryptoWorkstation() {
// console.log('CyberChef: Adding crypto workstation...');
const workstationData = {
type: "workstation",
name: "Crypto Analysis Station",
observations: "A powerful workstation for cryptographic analysis"
};
// Create the workstation sprite
const workstationSprite = this.add.sprite(0, 0, 'workstation');
workstationSprite.setVisible(false);
workstationSprite.name = "workstation";
workstationSprite.scenarioData = workstationData;
workstationSprite.setInteractive({ useHandCursor: true });
// Override the default handleObjectInteraction for this specific item
workstationSprite.openCryptoWorkstation = function() {
// console.log('CyberChef: Workstation custom interaction triggered');
// Create popup
let popup = document.getElementById('laptop-popup');
if (!popup) {
// console.log('CyberChef: Creating new 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';
return true;
};
// Add to inventory directly
addToInventory(workstationSprite);
// console.log('CyberChef: Workstation added to inventory');
}
// 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);
// Add the workstation to inventory
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', 8000);
}
// 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,
hasWorkstation: !!sprite.openCryptoWorkstation
}, 2);
if (sprite.openCryptoWorkstation && sprite.openCryptoWorkstation()) {
debugLog('WORKSTATION OPENED', null, 1);
return;
}
if (!sprite || !sprite.scenarioData) {
console.warn('Invalid sprite or missing scenario data');
return;
}
// Skip range check for inventory items
const isInventoryItem = inventory.items.includes(sprite);
if (!isInventoryItem) {
// Check if player is in range
const dx = player.x - sprite.x;
const dy = player.y - sprite.y;
const distanceSq = dx * dx + dy * dy;
if (distanceSq > INTERACTION_RANGE_SQ) {
// Show notification instead of alert
//gameAlert("Too far away to interact with this object.", 'warning', '', 2000);
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);
}
}
}
}
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;
// 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, 7000);
}
// 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 {
// 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;
// Copy over the custom interaction if it exists
if (sprite.openCryptoWorkstation) {
inventorySprite.openCryptoWorkstation = sprite.openCryptoWorkstation;
}
// Set depth higher than container
inventorySprite.setDepth(2003);
// Add pointer events
inventorySprite.on('pointerdown', function(event) {
event.stopPropagation();
handleObjectInteraction(this);
});
inventorySprite.on('pointerover', function() {
this.setTint(0xdddddd);
});
inventorySprite.on('pointerout', function() {
this.clearTint();
});
inventory.container.add(inventorySprite);
inventory.items.push(inventorySprite);
// Use debugLog with level 3 (verbose) for detailed inventory tracking
debugLog('INVENTORY ITEM DETAILS', {
name: sprite.name,
totalItems: inventory.items.length
}, 3);
} catch (error) {
console.error('Error adding item to inventory:', error);
}
}
// 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
}
// 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
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);
}
} 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 'bluetooth':
if (lockable.scenarioData?.locked) {
gameAlert("You need a Bluetooth scanner to unlock this device.", 'warning', 'Bluetooth Required', 4000);
// Don't return here - allow the item to be picked up even without scanner
if (type === 'item' && lockable.scenarioData?.takeable) {
addToInventory(lockable);
// Remove from room objects if it exists there
if (currentRoom && rooms[currentRoom].objects) {
delete rooms[currentRoom].objects[lockable.name];
}
}
return;
}
// Calculate distance between player and tablet
const distance = Phaser.Math.Distance.Between(
player.x, player.y,
lockable.x, lockable.y
);
debugLog('BLUETOOTH DISTANCE', distance, 3);
// Check if player is within range (using BLUETOOTH_SCAN_RANGE)
if (distance <= BLUETOOTH_SCAN_RANGE) {
debugLog('BLUETOOTH UNLOCK SUCCESS', {
itemName: lockable.scenarioData?.name,
itemMac: lockable.scenarioData?.mac,
distance: Math.round(distance),
range: BLUETOOTH_SCAN_RANGE
}, 1);
unlockTarget(lockable, type, lockable.layer);
gameAlert("Bluetooth connection established. Device unlocked.", 'success', 'Connection Successful', 4000);
return;
}
gameAlert("Too far from device to establish Bluetooth connection.", 'error', 'Connection Failed', 3000);
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,
requires: targetRoom.requires
};
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,
requires: item.requires || item.scenarioData?.requires,
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);
}
function checkBluetoothDevices() {
// Find scanner in inventory
const scanner = inventory.items.find(item =>
item.scenarioData?.type === "bluetooth_scanner"
);
if (!scanner) return;
// Find all tablets in the current room
if (!currentRoom || !rooms[currentRoom] || !rooms[currentRoom].objects) return;
Object.values(rooms[currentRoom].objects).forEach(obj => {
if (obj.scenarioData?.type === "tablet" && obj.scenarioData?.locked) {
const distance = Phaser.Math.Distance.Between(
player.x, player.y,
obj.x, obj.y
);
if (distance <= BLUETOOTH_SCAN_RANGE) {
debugLog('BLUETOOTH DEVICE DETECTED', {
distance: Math.round(distance),
range: BLUETOOTH_SCAN_RANGE
}, 2);
// Unlock the tablet
obj.scenarioData.locked = false;
debugLog('BLUETOOTH UNLOCK SUCCESS', null, 1);
}
}
});
}
// Add helper function to generate fingerprint data
function generateFingerprintData(item) {
// In a real implementation, this would generate unique fingerprint patterns
// For now, we'll just create a unique identifier
return `fp_${item.scenarioData.fingerprintOwner}_${Date.now()}`;
}
// Add helper function to check if player has required collection tools
function hasItemInInventory(itemType) {
return inventory.items.some(item =>
item && item.scenarioData &&
item.scenarioData.type === itemType
);
}
// Add this function after the other utility functions
function handleBiometricScan(scanner, player) {
const scannerId = scanner.scenarioData.id || scanner.name;
// Check if scanner is locked out
if (scannerState.lockoutTimers[scannerId] &&
Date.now() < scannerState.lockoutTimers[scannerId]) {
const remainingTime = Math.ceil((scannerState.lockoutTimers[scannerId] - Date.now()) / 1000);
gameAlert(`Scanner locked out. Try again in ${remainingTime} seconds.`, 'error', 'Scanner Locked', 4000);
return false;
}
if (!scanner.scenarioData?.biometricType === 'fingerprint') {
debugLog('SCANNER TYPE ERROR - FAIL', scanner.scenarioData, 2);
gameAlert('Invalid scanner type', 'error', 'Scanner Error', 3000);
return false;
}
// Check if player has valid fingerprint sample
const validSample = gameState.biometricSamples.find(sample =>
sample.type === 'fingerprint' &&
scanner.scenarioData.acceptedSamples.includes(sample.owner)
);
if (!validSample) {
handleScannerFailure(scannerId);
gameAlert("No valid fingerprint sample found.", 'error', 'Scan Failed', 4000);
return false;
}
// Check sample quality
const qualityThreshold = 0.7;
if (validSample.quality < qualityThreshold) {
handleScannerFailure(scannerId);
gameAlert("Fingerprint sample quality too poor for scanning.", 'error', 'Scan Failed', 4000);
return false;
}
// Success case - reset failed attempts
scannerState.failedAttempts[scannerId] = 0;
gameAlert("Biometric scan successful!", 'success', 'Scan Successful', 4000);
// Add visual feedback
const successEffect = scanner.scene.add.circle(
scanner.x,
scanner.y,
32,
0x00ff00,
0.5
);
scanner.scene.tweens.add({
targets: successEffect,
alpha: 0,
scale: 2,
duration: 1000,
onComplete: () => successEffect.destroy()
});
// If the scanner is protecting something, unlock it
if (scanner.scenarioData.unlocks) {
const targetObject = rooms[currentRoom].objects[scanner.scenarioData.unlocks];
if (targetObject) {
targetObject.scenarioData.locked = false;
targetObject.scenarioData.isUnlockedButNotCollected = true;
}
}
return true;
}
// Add this new function to handle scanner failures
function handleScannerFailure(scannerId) {
// Initialize failed attempts if not exists
if (!scannerState.failedAttempts[scannerId]) {
scannerState.failedAttempts[scannerId] = 0;
}
// Increment failed attempts
scannerState.failedAttempts[scannerId]++;
// Check if we should lockout
if (scannerState.failedAttempts[scannerId] >= MAX_FAILED_ATTEMPTS) {
scannerState.lockoutTimers[scannerId] = Date.now() + SCANNER_LOCKOUT_TIME;
gameAlert(`Too many failed attempts. Scanner locked for ${SCANNER_LOCKOUT_TIME/1000} seconds.`, 'error', 'Scanner Locked', 5000);
} else {
const remainingAttempts = MAX_FAILED_ATTEMPTS - scannerState.failedAttempts[scannerId];
gameAlert(`Scan failed. ${remainingAttempts} attempts remaining before lockout.`, 'warning', 'Scan Failed', 4000);
}
}
// Modify collectFingerprint to include visual feedback
function collectFingerprint(item) {
if (!item.scenarioData?.hasFingerprint) {
gameAlert("No fingerprints found on this surface.", 'info', 'No Fingerprints', 3000);
return null;
}
// Check if player has required items
if (!hasItemInInventory('fingerprint_kit')) {
gameAlert("You need a fingerprint kit to collect samples!", 'warning', 'Missing Equipment', 4000);
return null;
}
// Start the dusting minigame
startDustingMinigame(item);
return true;
}
// Add this function to check for object interactions
function checkObjectInteractions() {
// Skip if not enough time has passed since last check
const currentTime = performance.now();
if (this.lastInteractionCheck &&
currentTime - this.lastInteractionCheck < INTERACTION_CHECK_INTERVAL) {
return;
}
this.lastInteractionCheck = currentTime;
const playerRoom = currentPlayerRoom;
if (!playerRoom || !rooms[playerRoom].objects) return;
// Cache player position
const px = player.x;
const py = player.y;
// Get only objects within viewport bounds plus some margin
const camera = this.cameras.main;
const margin = INTERACTION_RANGE;
const viewBounds = {
left: camera.scrollX - margin,
right: camera.scrollX + camera.width + margin,
top: camera.scrollY - margin,
bottom: camera.scrollY + camera.height + margin
};
Object.values(rooms[playerRoom].objects).forEach(obj => {
// Skip inactive objects and those outside viewport
if (!obj.active ||
obj.x < viewBounds.left ||
obj.x > viewBounds.right ||
obj.y < viewBounds.top ||
obj.y > viewBounds.bottom) {
return;
}
// Use squared distance for performance
const dx = px - obj.x;
const dy = py - obj.y;
const distanceSq = dx * dx + dy * dy;
if (distanceSq <= INTERACTION_RANGE_SQ) {
if (!obj.isHighlighted) {
obj.isHighlighted = true;
obj.setTint(0xdddddd); // Simple highlight without tween
}
} else if (obj.isHighlighted) {
obj.isHighlighted = false;
obj.clearTint();
}
});
}
// Add this function to setup scanner interactions
function setupScannerInteractions() {
Object.values(rooms).forEach(room => {
if (!room.objects) return;
Object.values(room.objects).forEach(obj => {
if (obj.scenarioData?.biometricType === 'fingerprint') {
// Add visual indicator for scanner
const indicator = obj.scene.add.circle(
obj.x,
obj.y,
20,
0x0000ff,
0.3
);
// Add pulsing effect
obj.scene.tweens.add({
targets: indicator,
alpha: { from: 0.3, to: 0.1 },
scale: { from: 1, to: 1.2 },
duration: 1000,
yoyo: true,
repeat: -1
});
// Store reference to indicator
obj.scannerIndicator = indicator;
// Add hover effect
obj.on('pointerover', function() {
if (this.scannerIndicator) {
this.scannerIndicator.setAlpha(0.5);
}
});
obj.on('pointerout', function() {
if (this.scannerIndicator) {
this.scannerIndicator.setAlpha(0.3);
}
});
}
});
});
}
// 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>
${sample.isSpoofed ? '<strong style="color: #ff9900;">SPOOFED SAMPLE</strong><br>' : ''}
`;
// Add quality bar
const qualityBar = document.createElement('div');
qualityBar.style.cssText = `
width: 100%;
height: 5px;
background: #333;
margin-top: 5px;
border-radius: 2px;
`;
const qualityFill = document.createElement('div');
qualityFill.style.cssText = `
width: ${qualityPercentage}%;
height: 100%;
background: ${getQualityColor(sample.quality)};
border-radius: 2px;
transition: width 0.3s ease;
`;
qualityBar.appendChild(qualityFill);
sampleElement.appendChild(qualityBar);
// Add spoof button if not already spoofed
if (!sample.isSpoofed && hasItemInInventory('spoofing_kit')) {
const spoofButton = document.createElement('button');
spoofButton.textContent = 'Create Spoof';
spoofButton.style.cssText = `
margin-top: 10px;
padding: 5px 10px;
background: #444;
border: none;
color: white;
border-radius: 3px;
cursor: pointer;
`;
spoofButton.onclick = async () => {
spoofButton.disabled = true;
spoofButton.textContent = 'Creating spoof...';
// Add progress bar
const progressBar = document.createElement('div');
progressBar.style.cssText = `
width: 100%;
height: 2px;
background: #333;
margin-top: 5px;
`;
const progress = document.createElement('div');
progress.style.cssText = `
width: 0%;
height: 100%;
background: #ff9900;
transition: width 0.1s linear;
`;
progressBar.appendChild(progress);
sampleElement.appendChild(progressBar);
// Animate progress
let currentProgress = 0;
const interval = setInterval(() => {
currentProgress += 2;
progress.style.width = `${currentProgress}%`;
}, SPOOFING_TIME / 50);
// Create spoof after delay
setTimeout(() => {
clearInterval(interval);
const spoofedSample = createSpoofedSample(sample);
if (spoofedSample) {
gameState.biometricSamples.push(spoofedSample);
showSamplesUI(); // Refresh UI
}
}, SPOOFING_TIME);
};
sampleElement.appendChild(spoofButton);
}
samplesUI.appendChild(sampleElement);
});
}
// Helper function to hide samples UI
function hideSamplesUI() {
const samplesUI = document.getElementById('biometric-samples-ui');
if (samplesUI) {
samplesUI.style.display = 'none';
}
}
// Helper function to get color based on quality
function getQualityColor(quality) {
if (quality >= 0.8) return '#00ff00';
if (quality >= 0.6) return '#ffff00';
return '#ff0000';
}
// Add keyboard shortcut to view samples (press 'B')
function setupSamplesUIControls() {
document.addEventListener('keydown', (event) => {
if (event.key.toLowerCase() === 'b') {
showSamplesUI();
}
if (event.key === 'Escape') {
hideSamplesUI();
}
});
}
// Add this to your scene's create function
function initializeSamplesUI() {
createSamplesUI();
setupSamplesUIControls();
}
function generateFingerprintData(sample) {
// For spoofed samples, we generate from the original sample data
if (sample.data) {
return `spoofed_${sample.data}`;
}
// For original samples from items, we use the item's data
if (sample.scenarioData?.fingerprintOwner) {
return `fp_${sample.scenarioData.fingerprintOwner}_${Date.now()}`;
}
// Fallback unique identifier
return `fp_unknown_${Date.now()}`;
}
// Add spoofing functionality
function createSpoofedSample(originalSample) {
if (!originalSample) {
alert("No sample to spoof from!");
return null;
}
// Check if player has required items
const hasSpoofingKit = hasItemInInventory('spoofing_kit');
if (!hasSpoofingKit) {
alert("You need a spoofing kit to create fake fingerprints!");
return null;
}
// Create spoofed sample with slightly degraded quality
const spoofedSample = {
id: `spoofed_${originalSample.owner}_${Date.now()}`,
type: originalSample.type,
owner: originalSample.owner,
quality: originalSample.quality * SPOOF_QUALITY_MULTIPLIER,
data: generateFingerprintData(originalSample),
isSpoofed: true
};
return spoofedSample;
}
// Add dusting minigame
function startDustingMinigame(item) {
// Create iframe container
const iframe = document.createElement('div');
iframe.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 60%;
height: 60%;
background: rgba(0, 0, 0, 0.9);
border: 1px solid #444;
z-index: 1000;
padding: 20px;
border-radius: 5px;
`;
// Create game container
const gameContainer = document.createElement('div');
gameContainer.style.cssText = `
width: 100%;
height: calc(100% - 60px);
display: grid;
grid-template-columns: repeat(20, minmax(0, 1fr));
grid-template-rows: repeat(20, minmax(0, 1fr));
gap: 1px;
background: #222;
padding: 10px;
margin-top: 40px;
`;
// Add instructions
const instructions = document.createElement('div');
instructions.innerHTML = `
<h3 style="margin: 0; color: #fff; text-align: center;">Fingerprint Dusting</h3>
<p style="margin: 5px 0; color: #ccc; text-align: center; font-size: 14px;">
Drag to dust the surface and reveal fingerprints.<br>
🔍 Gray = Light dusting<br>
🟢 Green = Fingerprint found!<br>
⚠️ White = Over-dusted (avoid this)<br>
Find all fingerprints with minimal over-dusting.
</p>
`;
instructions.style.cssText = `
position: absolute;
top: 10px;
left: 50%;
transform: translateX(-50%);
width: 90%;
`;
// Add progress display
const progressText = document.createElement('div');
progressText.style.cssText = `
position: absolute;
bottom: 10px;
left: 50%;
transform: translateX(-50%);
color: white;
text-align: center;
font-size: 16px;
`;
// Generate fingerprint pattern
const gridSize = 20;
const fingerprintCells = new Set();
const centerX = Math.floor(gridSize / 2);
const centerY = Math.floor(gridSize / 2);
// Create a larger pattern (about 50 cells) spread around the center
for (let i = 0; i < 50; i++) {
const x = centerX + Math.floor(Math.random() * 10 - 5); // Increased spread
const y = centerY + Math.floor(Math.random() * 10 - 5); // Increased spread
if (x >= 0 && x < gridSize && y >= 0 && y < gridSize) {
fingerprintCells.add(`${x},${y}`);
}
}
// If we didn't get enough cells, add more until we reach target
while (fingerprintCells.size < 50) {
const x = centerX + Math.floor(Math.random() * 12 - 6);
const y = centerY + Math.floor(Math.random() * 12 - 6);
if (x >= 0 && x < gridSize && y >= 0 && y < gridSize) {
fingerprintCells.add(`${x},${y}`);
}
}
// Track progress
let revealedPrints = 0;
let totalPrints = fingerprintCells.size;
let overDusted = 0;
// Create grid cells
for (let y = 0; y < gridSize; y++) {
for (let x = 0; x < gridSize; x++) {
const cell = document.createElement('div');
cell.style.cssText = `
width: 100%;
height: 100%;
background: black;
position: relative;
cursor: pointer;
`;
cell.dataset.x = x;
cell.dataset.y = y;
cell.dataset.dustLevel = '0';
cell.dataset.hasFingerprint = fingerprintCells.has(`${x},${y}`);
gameContainer.appendChild(cell);
}
}
// Add dragging interaction at container level
let isDragging = false;
let lastDustTime = {}; // Track last dust time for each cell
gameContainer.addEventListener('mousedown', () => isDragging = true);
gameContainer.addEventListener('mouseup', () => isDragging = false);
gameContainer.addEventListener('mouseleave', () => isDragging = false);
gameContainer.addEventListener('mousemove', (e) => {
if (!isDragging) return;
// Get the cell element under the cursor
const cell = document.elementFromPoint(e.clientX, e.clientY);
if (cell && cell.dataset.dustLevel !== undefined) {
const cellId = `${cell.dataset.x},${cell.dataset.y}`;
const currentTime = Date.now();
const dustLevel = parseInt(cell.dataset.dustLevel);
// Only allow dusting every 100ms for each cell
if (!lastDustTime[cellId] || currentTime - lastDustTime[cellId] > 100) {
if (dustLevel < 3) {
// Increment dust level with 33% chance after level 1
if (dustLevel < 1 || Math.random() < 0.33) {
cell.dataset.dustLevel = (dustLevel + 1).toString();
updateCellColor(cell);
checkProgress();
}
lastDustTime[cellId] = currentTime;
}
}
}
});
function updateCellColor(cell) {
const dustLevel = parseInt(cell.dataset.dustLevel);
const hasFingerprint = cell.dataset.hasFingerprint === 'true';
if (dustLevel === 0) {
cell.style.background = 'black';
}
else if (dustLevel === 1) {
cell.style.background = '#444';
}
else if (dustLevel === 2) {
cell.style.background = hasFingerprint ? '#0f0' : '#888';
}
else {
cell.style.background = '#ccc';
}
}
function checkProgress() {
revealedPrints = 0;
overDusted = 0;
gameContainer.childNodes.forEach(cell => {
const dustLevel = parseInt(cell.dataset.dustLevel);
const hasFingerprint = cell.dataset.hasFingerprint === 'true';
if (hasFingerprint && dustLevel === 2) revealedPrints++;
if (dustLevel === 3) overDusted++;
});
const requiredPrints = Math.ceil(totalPrints * 0.4); // 40% requirement
progressText.innerHTML = `
<div style="color: #0f0;">Found: ${revealedPrints}/${requiredPrints} required prints</div>
<div style="color: ${overDusted > 24 ? '#f00' : '#fff'}">
Over-dusted: ${overDusted}/25 max
</div>
`;
// Check fail condition first
if (overDusted >= 25) {
// Show failure message
const failureMessage = document.createElement('div');
failureMessage.style.cssText = `
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.9);
padding: 20px;
border-radius: 5px;
color: #f00;
font-size: 20px;
text-align: center;
z-index: 1001;
`;
failureMessage.textContent = "Too many over-dusted areas!";
iframe.appendChild(failureMessage);
// Disable further interaction
isDragging = false;
gameContainer.style.pointerEvents = 'none';
setTimeout(() => {
document.body.removeChild(iframe);
scene.input.mouse.enabled = true;
}, 1500);
return;
}
// Check win condition (existing code)
if (revealedPrints >= requiredPrints && overDusted < 25) {
// Show success message
const successMessage = document.createElement('div');
successMessage.style.cssText = `
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.9);
padding: 20px;
border-radius: 5px;
color: #0f0;
font-size: 20px;
text-align: center;
z-index: 1001;
`;
successMessage.textContent = "Fingerprint successfully collected!";
iframe.appendChild(successMessage);
// Disable further interaction
isDragging = false;
gameContainer.style.pointerEvents = 'none';
setTimeout(() => {
// Add fingerprint to gameState
if (!gameState.biometricSamples) {
gameState.biometricSamples = [];
}
const sample = {
id: generateFingerprintData(item),
type: 'fingerprint',
owner: item.scenarioData.fingerprintOwner,
quality: Math.random() * 0.3 + 0.7, // Random quality between 0.7 and 1.0
data: generateFingerprintData(item)
};
gameState.biometricSamples.push(sample);
// Remove the minigame
document.body.removeChild(iframe);
scene.input.mouse.enabled = true;
// Mark item as collected
if (item.scenarioData) {
item.scenarioData.hasFingerprint = false;
}
}, 1500);
}
}
// Add close button
const closeButton = document.createElement('button');
closeButton.textContent = 'X';
closeButton.style.cssText = `
position: absolute;
right: 10px;
top: 10px;
background: none;
border: none;
color: white;
font-size: 20px;
cursor: pointer;
`;
closeButton.onclick = () => {
document.body.removeChild(iframe);
scene.input.mouse.enabled = true;
};
// Assemble the interface
iframe.appendChild(closeButton);
iframe.appendChild(instructions);
iframe.appendChild(gameContainer);
iframe.appendChild(progressText);
document.body.appendChild(iframe);
// Disable game movement
const scene = item.scene;
scene.input.mouse.enabled = false;
}
function startLockpickingMinigame(lockable, currentScene, difficulty) {
// Create iframe container
const iframe = document.createElement('div');
iframe.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 60%;
height: 60%;
background: rgba(0, 0, 0, 0.9);
border: 1px solid #444;
z-index: 1000;
padding: 20px;
border-radius: 5px;
`;
// Add instructions
const instructions = document.createElement('div');
instructions.innerHTML = `
<h3 style="margin: 0; color: #fff; text-align: center;">Lock Picking</h3>
<p style="margin: 5px 0; color: #ccc; text-align: center; font-size: 14px;">
Use spacebar or click to toggle tension levels. Each pin requires the right amount of tension.<br>
🔵 Blue = Pin moving<br>
🟢 Green = Pin set correctly<br>
🔴 Red = Over-pushed (reset)<br>
Set all pins in the correct order with the right tension without resetting.
</p>
`;
instructions.style.cssText = `
position: absolute;
top: 10px;
left: 50%;
transform: translateX(-50%);
width: 90%;
`;
// Create game container
const gameContainer = document.createElement('div');
gameContainer.style.cssText = `
width: 100%;
height: calc(100% - 60px);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 20px;
margin-top: 40px;
background: #222;
padding: 10px;
`;
// Add difficulty selection
const difficultySelect = document.createElement('select');
difficultySelect.style.cssText = `
margin-bottom: 10px;
padding: 5px;
background: #444;
color: white;
border: 1px solid #666;
border-radius: 3px;
`;
const difficulties = ['Easy - Pins Visible', 'Hard - Audio Only'];
difficulties.forEach(diff => {
const option = document.createElement('option');
option.value = diff;
option.textContent = diff;
difficultySelect.appendChild(option);
});
// Add audio feedback
const lockSounds = {
click: null, // Basic pin movement sound
binding: null, // Sound when a pin is binding (correct tension)
set: null, // Sound when a pin is successfully set
reset: null, // Sound when pins are reset
wrong: null, // Sound when wrong tension or wrong pin
tension: null, // Sound when changing tension
success: null, // Sound when successfully picking the lock
overTension: null // Sound when over-tensioning the lock
};
const initAudio = () => {
if (!lockSounds.click) {
// Basic click sound (when pressing a pin)
lockSounds.click = new Audio('assets/sounds/lockpick_click.mp3');
// Binding sound (when a pin is binding with correct tension)
lockSounds.binding = new Audio('assets/sounds/lockpick_binding.mp3');
// Set sound (when a pin is successfully set)
lockSounds.set = new Audio('assets/sounds/lockpick_set.mp3');
// Reset sound (when pins are reset)
lockSounds.reset = new Audio('assets/sounds/lockpick_reset.mp3');
// Wrong sound (when using wrong tension or pressing wrong pin)
lockSounds.wrong = new Audio('assets/sounds/lockpick_wrong.mp3');
// Tension sound (when changing tension levels)
lockSounds.tension = new Audio('assets/sounds/lockpick_tension.mp3');
// Success sound (when successfully picking the lock)
lockSounds.success = new Audio('assets/sounds/lockpick_success.mp3');
// Over-tension sound (when applying too much tension)
lockSounds.overTension = new Audio('assets/sounds/lockpick_overtension.mp3');
}
};
// Initialize audio on first interaction
gameContainer.addEventListener('mousedown', initAudio, { once: true });
// Add pin binding order and game state
const numPins = getPinCountForDifficulty(difficulty);
const bindingOrder = Array.from({length: numPins}, (_, i) => i)
.sort(() => Math.random() - 0.5);
const gameState = {
tensionLevel: 1, // Start with light tension instead of none
pinStates: Array(numPins).fill(0), // 0 = down, 1 = moving, 2 = set
pinPressTime: Array(numPins).fill(0), // Track how long each pin is pressed
currentBindingIndex: 0,
hardMode: false,
maxPressTime: 1000, // Max time to hold a pin (ms)
failCount: 0,
maxFails: 3,
overTensioned: false,
lastPinSetTime: 0, // Track when the last pin was set
isActivelyPickingPin: false, // Track if we're actively working on a pin
tensionRequirements: Array(numPins).fill(0).map(() => {
// Each pin requires a specific tension level (1, 2, or 3)
const randomValue = Math.random();
if (randomValue < 0.33) return 1; // Light tension
if (randomValue < 0.66) return 2; // Medium tension
return 3; // Heavy tension
})
};
// Create tension wrench toggle
const tensionWrench = document.createElement('div');
tensionWrench.style.cssText = `
width: 100px;
height: 30px;
background: ${gameState.tensionLevel === 1 ? '#666' : gameState.tensionLevel === 2 ? '#888' : '#aaa'};
border: 2px solid #888;
border-radius: 5px;
cursor: pointer;
margin-bottom: 20px;
text-align: center;
line-height: 30px;
color: white;
`;
tensionWrench.textContent = 'Tension: OFF';
// Function to reset pins
function resetPins(showVisual = true) {
gameState.pinStates.fill(0);
gameState.pinPressTime.fill(0);
gameState.currentBindingIndex = 0;
gameState.failCount++;
if (showVisual) {
Array.from(pinsContainer.children).forEach(pin => {
pin.style.background = '#555';
if (!gameState.hardMode) {
pin.style.transition = 'background-color 0.3s';
pin.style.background = '#f00';
setTimeout(() => pin.style.background = '#555', 300);
}
});
}
if (gameState.failCount >= gameState.maxFails) {
alert("Lock picking failed! The lock is now jammed.");
// Safely remove iframe if it exists in the document
const existingIframe = document.querySelector('div[style*="z-index: 1000"]');
if (existingIframe && existingIframe.parentNode) {
existingIframe.parentNode.removeChild(existingIframe);
}
if (currentScene && currentScene.input && currentScene.input.mouse) {
currentScene.input.mouse.enabled = true;
}
}
}
// Create a single function for toggling tension
function toggleTension() {
// Toggle between 3 tension levels (light -> medium -> heavy -> light)
const previousTensionLevel = gameState.tensionLevel;
gameState.tensionLevel = (gameState.tensionLevel % 3) + 1;
// Play tension change sound
if (lockSounds.tension) {
lockSounds.tension.currentTime = 0;
lockSounds.tension.play().catch(e => console.log('Audio play failed:', e));
}
updateTensionWrench();
// Check if we're over-tensioning - but only if we've started interacting with pins
// AND only if we're actively working on a pin (not just changing tension)
const timeSinceLastPinSet = Date.now() - gameState.lastPinSetTime;
const isActivelyPickingLock = gameState.isActivelyPickingPin;
if (gameState.tensionLevel === 3 &&
gameState.currentBindingIndex > 0 &&
timeSinceLastPinSet > 1000 &&
isActivelyPickingLock) {
// 30% chance of over-tensioning with heavy pressure
if (Math.random() < 0.3) {
gameState.overTensioned = true;
tensionWrench.style.background = '#ff3333';
tensionWrench.style.transform = 'rotate(15deg)';
// Play over-tension sound
if (lockSounds.overTension) {
lockSounds.overTension.currentTime = 0;
lockSounds.overTension.play().catch(e => console.log('Audio play failed:', e));
}
setTimeout(() => {
alert("You applied too much tension and jammed the lock!");
resetPins();
}, 500);
}
}
}
// Add this new function to update tension wrench visuals
function updateTensionWrench() {
// Update tension wrench appearance based on level
switch(gameState.tensionLevel) {
case 1: // Light
tensionWrench.style.background = '#666';
tensionWrench.style.transform = 'rotate(2deg)';
tensionWrench.textContent = 'Tension: LIGHT';
break;
case 2: // Medium
tensionWrench.style.background = '#888';
tensionWrench.style.transform = 'rotate(5deg)';
tensionWrench.textContent = 'Tension: MEDIUM';
break;
case 3: // Heavy
tensionWrench.style.background = '#aaa';
tensionWrench.style.transform = 'rotate(8deg)';
tensionWrench.textContent = 'Tension: HEAVY';
break;
}
}
// Create pins container
const pinsContainer = document.createElement('div');
pinsContainer.style.cssText = `
display: flex;
gap: 10px;
background: #333;
padding: 20px;
border-radius: 10px;
`;
// Create individual pins
for (let i = 0; i < numPins; i++) {
const pin = document.createElement('div');
pin.style.cssText = `
width: 30px;
height: 100px;
background: #555;
border: 2px solid #777;
border-radius: 5px;
cursor: pointer;
position: relative;
transition: transform 0.1s, background-color 0.3s;
`;
// Add a subtle indicator at the bottom of each pin
const pinIndicator = document.createElement('div');
pinIndicator.style.cssText = `
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 5px;
background: #555;
transition: background-color 0.3s;
`;
pin.appendChild(pinIndicator);
const pinNumber = document.createElement('div');
pinNumber.style.cssText = `
position: absolute;
top: -20px;
width: 100%;
text-align: center;
color: white;
`;
pinNumber.textContent = (i + 1).toString();
pin.appendChild(pinNumber);
// Function to update pin appearance based on its state and binding status
function updatePinAppearance() {
// Reset to default first
pin.style.background = '#555';
pinIndicator.style.background = '#555';
pin.style.animation = '';
pin.style.borderColor = '#777';
// If the pin is set, show it as green
if (gameState.pinStates[i] === 2) {
pin.style.background = '#0f0';
pin.style.cursor = 'default';
pinIndicator.style.background = '#0f0';
return;
}
// Get the current binding pin
const bindingPin = bindingOrder[gameState.currentBindingIndex];
// Generate consistent red herrings based on the current binding index
// This ensures the same pins are highlighted as red herrings until the next pin is set
const redHerringSeeds = [
(bindingPin * 3 + 7) % numPins,
(bindingPin * 5 + 3) % numPins
];
// Filter out the binding pin and already set pins from red herrings
const redHerrings = redHerringSeeds.filter(index =>
index !== bindingPin && gameState.pinStates[index] !== 2);
// If this is the current binding pin, give a subtle hint based on difficulty
if (i === bindingPin) {
// For easy difficulty, make the binding pin more obvious
if (difficulty === 'easy') {
pinIndicator.style.background = '#ff9900'; // Orange indicator for binding pin
// Also show the required tension level with a color hint
const requiredTension = gameState.tensionRequirements[i];
if (requiredTension === 1) {
pin.style.borderColor = '#66ccff'; // Light blue for light tension
} else if (requiredTension === 2) {
pin.style.borderColor = '#9966ff'; // Purple for medium tension
} else {
pin.style.borderColor = '#ff6666'; // Red for heavy tension
}
// Add a subtle animation
pin.style.animation = 'pinWiggle 2s infinite';
}
// For medium difficulty, just show which pin is binding with less obvious cues
else if (difficulty === 'medium') {
pinIndicator.style.background = '#ff9900'; // Orange indicator for binding pin
pin.style.animation = 'pinWiggle 3s infinite';
}
// For hard difficulty, very subtle indication
else if (difficulty === 'hard') {
pin.style.animation = 'pinWiggle 4s infinite 0.5s';
}
}
// If this is a red herring, give misleading feedback
else if (redHerrings.includes(i) && gameState.currentBindingIndex > 0 && difficulty !== 'easy') {
// The amount of misleading feedback depends on difficulty
if (difficulty === 'medium') {
// For medium, make red herrings somewhat convincing
pinIndicator.style.background = '#ff9900'; // Same color as real binding pin
pin.style.animation = 'pinWiggle 3.5s infinite 0.7s'; // Similar wiggle to real pin
// Randomly assign fake tension indicators to confuse
const fakeTension = Math.floor(Math.random() * 3) + 1;
if (fakeTension === 1) {
pin.style.borderColor = '#66ccff';
} else if (fakeTension === 2) {
pin.style.borderColor = '#9966ff';
} else {
pin.style.borderColor = '#ff6666';
}
}
else if (difficulty === 'hard') {
// For hard, make red herrings very convincing
pin.style.animation = 'pinWiggle 4s infinite 0.3s';
// On hard, sometimes make a red herring more convincing than the real pin
if (Math.random() < 0.5) {
pinIndicator.style.background = '#ff9900';
}
}
}
}
// Add the wiggle animation to the document
if (!document.getElementById('pinWiggleAnimation')) {
const style = document.createElement('style');
style.id = 'pinWiggleAnimation';
style.textContent = `
@keyframes pinWiggle {
0% { transform: translateY(0); }
15% { transform: translateY(-2px); }
30% { transform: translateY(0); }
45% { transform: translateY(-1px); }
60% { transform: translateY(0); }
75% { transform: translateY(-0.5px); }
100% { transform: translateY(0); }
}
`;
document.head.appendChild(style);
}
// Update all pins whenever a pin state changes
function updateAllPins() {
// Get the current binding pin
const bindingPin = bindingOrder[gameState.currentBindingIndex];
// Generate consistent red herrings based on the current binding index
const redHerringSeeds = [
(bindingPin * 3 + 7) % numPins,
(bindingPin * 5 + 3) % numPins
];
// Filter out the binding pin and already set pins from red herrings
const redHerrings = redHerringSeeds.filter(index =>
index !== bindingPin && gameState.pinStates[index] !== 2);
Array.from(pinsContainer.children).forEach((pin, index) => {
// Find the indicator within this pin
const indicator = pin.querySelector('div:first-child');
// Reset styles first
pin.style.background = '#555';
pin.style.animation = '';
pin.style.borderColor = '#777';
if (indicator) indicator.style.background = '#555';
// Update based on current game state
if (gameState.pinStates[index] === 2) {
pin.style.background = '#0f0';
pin.style.cursor = 'default';
if (indicator) indicator.style.background = '#0f0';
} else {
pin.style.cursor = 'pointer';
// Check if this is the binding pin
if (index === bindingPin && !gameState.hardMode) {
if (difficulty === 'easy') {
if (indicator) indicator.style.background = '#ff9900';
// Show tension hint
const requiredTension = gameState.tensionRequirements[index];
if (requiredTension === 1) {
pin.style.borderColor = '#66ccff';
} else if (requiredTension === 2) {
pin.style.borderColor = '#9966ff';
} else {
pin.style.borderColor = '#ff6666';
}
pin.style.animation = 'pinWiggle 2s infinite';
}
else if (difficulty === 'medium') {
if (indicator) indicator.style.background = '#ff9900';
pin.style.animation = 'pinWiggle 3s infinite';
}
else if (difficulty === 'hard') {
pin.style.animation = 'pinWiggle 4s infinite 0.5s';
}
}
// Check if this is a red herring, but only for medium and hard difficulties
else if (redHerrings.includes(index) && gameState.currentBindingIndex > 0 && difficulty !== 'easy') {
if (difficulty === 'medium') {
if (indicator) indicator.style.background = '#ff9900';
pin.style.animation = 'pinWiggle 3.5s infinite 0.7s';
const fakeTension = Math.floor(Math.random() * 3) + 1;
if (fakeTension === 1) {
pin.style.borderColor = '#66ccff';
} else if (fakeTension === 2) {
pin.style.borderColor = '#9966ff';
} else {
pin.style.borderColor = '#ff6666';
}
}
else if (difficulty === 'hard') {
pin.style.animation = 'pinWiggle 4s infinite 0.3s';
if (Math.random() < 0.5 && indicator) {
indicator.style.background = '#ff9900';
}
}
}
}
});
}
// Call updatePinAppearance initially and whenever the game state changes
updatePinAppearance();
let pressStartTime = 0;
let pressTimer = null;
function checkPinPress() {
if (pressStartTime === 0) return;
const pressDuration = Date.now() - pressStartTime;
if (pressDuration > gameState.maxPressTime) {
// Clear the timer first before calling resetPins
clearInterval(pressTimer);
pressTimer = null;
resetPins();
}
}
pin.onmousedown = () => {
// First check if this pin is already set
const pinIndex = Array.from(pinsContainer.children).indexOf(pin);
if (gameState.pinStates[pinIndex] === 2) {
// Pin is already set, don't allow interaction
return;
}
// Set the flag to indicate we're actively picking a pin
gameState.isActivelyPickingPin = true;
// Play basic click sound
if (lockSounds.click) {
lockSounds.click.currentTime = 0;
lockSounds.click.play().catch(e => console.log('Audio play failed:', e));
}
pressStartTime = Date.now();
pressTimer = setInterval(checkPinPress, 100);
pin.style.transform = 'translateY(-10px)';
// Each pin has different tension requirements
const bindingPin = bindingOrder[gameState.currentBindingIndex];
const requiredTension = gameState.tensionRequirements[pinIndex];
// Check if this is the current binding pin
if (pinIndex === bindingPin) {
// This pin needs exactly the right tension level
const correctTension = (gameState.tensionLevel === requiredTension);
if (correctTension && !gameState.overTensioned) {
// Play binding sound - correct pin with correct tension
if (lockSounds.binding) {
lockSounds.binding.currentTime = 0;
lockSounds.binding.play().catch(e => console.log('Audio play failed:', e));
}
if (!gameState.hardMode) {
pin.style.background = '#00f';
}
// Start a timer to set the pin
setTimeout(() => {
if (pressStartTime !== 0) { // Still pressing
// Double-check tension is still correct
const stillCorrectTension = (gameState.tensionLevel === requiredTension);
if (stillCorrectTension && !gameState.overTensioned) {
gameState.pinStates[pinIndex] = 2;
gameState.currentBindingIndex++;
gameState.lastPinSetTime = Date.now();
// Play set sound - pin successfully set
if (lockSounds.set) {
lockSounds.set.currentTime = 0;
lockSounds.set.play().catch(e => console.log('Audio play failed:', e));
}
if (!gameState.hardMode) {
pin.style.background = '#0f0';
pinIndicator.style.background = '#0f0';
}
// Update all pins to show new binding state
updateAllPins();
checkWinCondition();
}
}
}, 500);
} else if (gameState.tensionLevel > 0) {
// Wrong tension but trying - give feedback
// Play wrong sound - wrong tension on correct pin
if (lockSounds.wrong) {
lockSounds.wrong.currentTime = 0;
lockSounds.wrong.play().catch(e => console.log('Audio play failed:', e));
}
if (!gameState.hardMode) {
pin.style.background = '#00f';
}
// Start counting towards potential reset
gameState.pinPressTime[pinIndex] = Date.now();
}
} else if (gameState.tensionLevel > 0 && gameState.pinStates[pinIndex] !== 2) {
// Wrong pin - give feedback
if (lockSounds.wrong) {
lockSounds.wrong.currentTime = 0;
lockSounds.wrong.play().catch(e => console.log('Audio play failed:', e));
}
if (!gameState.hardMode) {
pin.style.background = '#00f';
}
// Start counting towards potential reset
gameState.pinPressTime[pinIndex] = Date.now();
}
};
pin.onmouseup = pin.onmouseleave = () => {
// Clear the flag to indicate we're no longer actively picking a pin
gameState.isActivelyPickingPin = false;
pressStartTime = 0;
if (pressTimer) {
clearInterval(pressTimer);
pressTimer = null;
}
pin.style.transform = 'translateY(0)';
if (gameState.pinStates[i] !== 2) {
pin.style.background = '#555';
// Update appearance to show binding status
updatePinAppearance();
}
};
pinsContainer.appendChild(pin);
}
difficultySelect.onchange = () => {
gameState.hardMode = difficultySelect.value.includes('Hard');
Array.from(pinsContainer.children).forEach(pin => {
pin.style.opacity = gameState.hardMode ? '0.1' : '1';
});
};
// Add components to game container
gameContainer.appendChild(difficultySelect);
gameContainer.appendChild(tensionWrench);
gameContainer.appendChild(pinsContainer);
// Add close button
const closeButton = document.createElement('button');
closeButton.textContent = 'X';
closeButton.style.cssText = `
position: absolute;
right: 10px;
top: 10px;
background: none;
border: none;
color: white;
font-size: 20px;
cursor: pointer;
`;
closeButton.onclick = () => {
document.body.removeChild(iframe);
if (currentScene && currentScene.input && currentScene.input.mouse) {
currentScene.input.mouse.enabled = true;
}
};
// Assemble the interface
iframe.appendChild(closeButton);
iframe.appendChild(instructions);
iframe.appendChild(gameContainer);
document.body.appendChild(iframe);
// Disable game movement
if (currentScene && currentScene.input && currentScene.input.mouse) {
currentScene.input.mouse.enabled = false;
}
// Add this function before the pin creation loop
function checkWinCondition() {
if (gameState.currentBindingIndex >= numPins) {
// Play success sound
if (lockSounds.success) {
lockSounds.success.currentTime = 0;
lockSounds.success.play().catch(e => console.log('Audio play failed:', e));
}
console.log('Lock picked successfully:', lockable);
// Create success message
const successMessage = document.createElement('div');
successMessage.style.cssText = `
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.8);
color: #0f0;
padding: 20px;
border-radius: 10px;
font-size: 24px;
text-align: center;
z-index: 1002;
`;
successMessage.textContent = "Lock successfully picked!";
iframe.appendChild(successMessage);
// Disable further interaction
gameContainer.style.pointerEvents = 'none';
setTimeout(() => {
// For doors, we need to check properties and handle differently
if (lockable && lockable.properties && lockable.properties.locked) {
console.log('Unlocking door tile:', lockable);
unlockDoor(lockable, lockable.layer);
}
// For containers and other lockable items
else if (lockable && lockable.scenarioData) {
console.log('Unlocking container:', lockable.scenarioData);
lockable.scenarioData.locked = false;
// Set the flag to indicate the container is unlocked but contents not collected
if (lockable.scenarioData.contents && lockable.scenarioData.contents.length > 0) {
lockable.scenarioData.isUnlockedButNotCollected = true;
debugLog('Container unlocked and ready for collection', lockable.scenarioData, 1);
}
}
// Remove the minigame
document.body.removeChild(iframe);
if (currentScene && currentScene.input && currentScene.input.mouse) {
currentScene.input.mouse.enabled = true;
}
}, 1500);
return true;
}
return false;
}
// Use the toggleTension function for both click and keyboard events
tensionWrench.onclick = toggleTension;
document.addEventListener('keydown', function(event) {
// Only process if the lockpicking minigame is active
if (!document.querySelector('div[style*="z-index: 1000"]')) return;
if (event.code === 'Space') {
event.preventDefault(); // Prevent page scrolling
toggleTension();
}
});
// Keep only the table debug function
function logTensionDebugInfo() {
// Only show debug info if debug mode is enabled
if (!DEBUG_MODE.enabled) return;
DEBUG_MODE.log("=== LOCKPICKING DEBUG INFO ===");
DEBUG_MODE.log("Pin binding order and tension requirements:");
const tableData = [];
for (let orderIndex = 0; orderIndex < numPins; orderIndex++) {
const pinIndex = bindingOrder[orderIndex];
const requiredTension = gameState.tensionRequirements[pinIndex];
let tensionNeeded;
switch(requiredTension) {
case 1: tensionNeeded = 'Light'; break;
case 2: tensionNeeded = 'Medium'; break;
case 3: tensionNeeded = 'Heavy'; break;
default: tensionNeeded = 'Unknown';
}
tableData.push({
'Binding Order': orderIndex + 1,
'Pin #': pinIndex + 1,
'Tension Required': tensionNeeded
});
}
console.table(tableData);
}
// Call this function instead of addTensionDebugDisplay
logTensionDebugInfo();
}
// Add this function to get pin count based on difficulty
function getPinCountForDifficulty(difficulty) {
switch(difficulty?.toLowerCase()) {
case 'easy':
return 3;
case 'medium':
return 5;
case 'hard':
return 7;
default:
return 5; // Default to medium difficulty
}
}
// removes an item from the inventory
function removeFromInventory(sprite) {
if (!sprite || !inventory.items) {
return false;
}
try {
// Find the index of the sprite in the inventory
const index = inventory.items.indexOf(sprite);
if (index === -1) {
return false; // Item not found in inventory
}
// Remove from container
inventory.container.remove(sprite);
// Remove from items array
inventory.items.splice(index, 1);
// Destroy the sprite
sprite.destroy();
// Rearrange remaining items
rearrangeInventoryItems();
// Log the removal
debugLog('INVENTORY ITEM REMOVED', {
name: sprite.name,
totalItems: inventory.items.length
}, 2);
return true;
} catch (error) {
console.error('Error removing item from inventory:', error);
return false;
}
}
// Rearrange inventory items after removal
function rearrangeInventoryItems() {
inventory.items.forEach((item, index) => {
item.x = index * 60 + 100;
});
}
</script>
</body>
</html>