mirror of
https://github.com/cliffe/BreakEscape.git
synced 2026-02-20 13:50:46 +00:00
3329 lines
137 KiB
HTML
3329 lines
137 KiB
HTML
<!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;
|
||
}
|
||
|
||
#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>
|
||
|
||
<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;
|
||
|
||
// 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
|
||
};
|
||
|
||
// 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">×</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) {
|
||
console.log('Inventory item clicked:', clickedItem.name);
|
||
handleObjectInteraction(clickedItem);
|
||
return;
|
||
}
|
||
}
|
||
|
||
// if not clicking inventory, handle as movement
|
||
console.log('Click detected at:', pointer.worldX, pointer.worldY);
|
||
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();
|
||
}
|
||
|
||
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);
|
||
alert(gameScenario.scenario_brief);
|
||
}
|
||
|
||
// 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
|
||
};
|
||
console.log(`Room ${roomId} dimensions:`, roomDimensions[roomId]);
|
||
} 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)) {
|
||
console.log(`Room ${connected} already processed, skipping`);
|
||
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) {
|
||
console.log(`Clicked active object ${obj.name}`);
|
||
handleObjectInteraction(sprite);
|
||
} else {
|
||
alert("Nothing of note here");
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
} 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
|
||
console.log('Click detected at:', worldPoint.x, worldPoint.y);
|
||
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) {
|
||
console.log('CyberChef: handleObjectInteraction called for:', sprite.name, 'Has open workstation:', !!sprite.openCryptoWorkstation);
|
||
|
||
if (sprite.openCryptoWorkstation && sprite.openCryptoWorkstation()) {
|
||
console.log('CyberChef: Crypto workstation opened');
|
||
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) {
|
||
// alert("Too far away to interact with this object.");
|
||
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`;
|
||
});
|
||
alert(message);
|
||
|
||
// 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 for locked state in scenarioData
|
||
if (data.locked === true) {
|
||
console.log('Item is locked:', data);
|
||
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`;
|
||
}
|
||
|
||
if (data.takeable) {
|
||
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) {
|
||
console.log('Adding item to inventory:', itemIdentifier);
|
||
addToInventory(sprite);
|
||
}
|
||
}
|
||
|
||
alert(message);
|
||
}
|
||
|
||
// 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);
|
||
|
||
console.log('Item added to inventory:', {
|
||
name: sprite.name,
|
||
totalItems: inventory.items.length
|
||
});
|
||
} 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);
|
||
|
||
console.log('Inventory initialized:', inventory); // Debug log
|
||
}
|
||
|
||
// 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) {
|
||
console.log('Door is locked, attempting unlock');
|
||
colorDoorTiles(doorTile, room);
|
||
handleDoorUnlock(doorTile, room);
|
||
} else {
|
||
console.log('Door is not locked');
|
||
}
|
||
} else {
|
||
console.log("Too far from door to interact");
|
||
}
|
||
});
|
||
|
||
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) {
|
||
console.log('handleDoorUnlock called:', { doorTile, room });
|
||
doorTile.layer = room.doorsLayer; // Ensure layer reference is set
|
||
handleUnlock(doorTile, 'door');
|
||
}
|
||
|
||
function handleUnlock(lockable, type) {
|
||
console.log('handleUnlock called:', { type, lockable });
|
||
|
||
// Check locked state in scenarioData for items
|
||
const isLocked = type === 'door' ?
|
||
lockable.properties?.locked :
|
||
lockable.scenarioData?.locked;
|
||
|
||
if (!isLocked) {
|
||
console.log('Object is not locked');
|
||
return;
|
||
}
|
||
|
||
// Get lock requirements based on type
|
||
const lockRequirements = type === 'door'
|
||
? getLockRequirementsForDoor(lockable)
|
||
: getLockRequirementsForItem(lockable);
|
||
|
||
console.log('Lock requirements:', lockRequirements);
|
||
|
||
if (!lockRequirements) {
|
||
console.log('No lock requirements found');
|
||
return;
|
||
}
|
||
|
||
switch(lockRequirements.lockType) {
|
||
case 'key':
|
||
const requiredKey = lockRequirements.requires;
|
||
console.log('Checking for key:', requiredKey);
|
||
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';
|
||
|
||
unlockTarget(lockable, type, lockable.layer);
|
||
alert(`You used the ${keyName} that you found in ${keyLocation} to unlock the ${type}.`);
|
||
} else {
|
||
// Check for lockpick kit
|
||
const hasLockpick = inventory.items.some(item =>
|
||
item && item.scenarioData &&
|
||
item.scenarioData.type === 'lockpick'
|
||
);
|
||
|
||
if (hasLockpick) {
|
||
if (confirm("Would you like to attempt picking this lock?")) {
|
||
startLockpickingMinigame(lockable, game.scene.scenes[0]); // Pass the main scene
|
||
}
|
||
} else {
|
||
alert(`Requires key: ${requiredKey}`);
|
||
}
|
||
}
|
||
break;
|
||
|
||
case 'pin':
|
||
console.log('Handling PIN lock');
|
||
const pinInput = prompt(`Enter PIN code:`);
|
||
if (pinInput === lockRequirements.requires) {
|
||
unlockTarget(lockable, type, lockable.layer); // Pass the layer here
|
||
alert(`Correct PIN! The ${type} is now unlocked.`);
|
||
} else if (pinInput !== null) {
|
||
alert("Incorrect PIN code.");
|
||
}
|
||
break;
|
||
|
||
case 'password':
|
||
console.log('Handling password lock');
|
||
const passwordInput = prompt(`Enter password:`);
|
||
if (passwordInput === lockRequirements.requires) {
|
||
unlockTarget(lockable, type, lockable.layer); // Pass the layer here
|
||
alert(`Correct password! The ${type} is now unlocked.`);
|
||
} else if (passwordInput !== null) {
|
||
alert("Incorrect password.");
|
||
}
|
||
break;
|
||
|
||
case 'bluetooth':
|
||
if (lockable.scenarioData?.locked) {
|
||
alert("You need a Bluetooth scanner to unlock this device.");
|
||
// 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
|
||
);
|
||
|
||
console.log('Distance to tablet:', distance);
|
||
|
||
// Check if player is within range (using BLUETOOTH_SCAN_RANGE)
|
||
if (distance <= BLUETOOTH_SCAN_RANGE) {
|
||
console.log('Bluetooth unlock success: Player in range', {
|
||
itemName: lockable.scenarioData?.name,
|
||
itemMac: lockable.scenarioData?.mac,
|
||
distance: distance
|
||
});
|
||
unlockTarget(lockable, type, lockable.layer);
|
||
alert(`Bluetooth connection established. Device unlocked.`);
|
||
return;
|
||
}
|
||
|
||
alert("Too far from device to establish Bluetooth connection.");
|
||
break;
|
||
|
||
default:
|
||
alert(`Requires: ${lockRequirements.requires}`);
|
||
}
|
||
}
|
||
|
||
// 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) {
|
||
console.log('Getting lock requirements for door:', doorTile);
|
||
|
||
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);
|
||
|
||
console.log('Door world coordinates:', { doorWorldX, doorWorldY });
|
||
|
||
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)) {
|
||
console.log(`Room ${roomId} overlaps with door`);
|
||
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
|
||
});
|
||
}
|
||
});
|
||
|
||
console.log('Overlapping rooms:', overlappingRooms);
|
||
|
||
const lockedRooms = overlappingRooms
|
||
.filter(r => r.locked)
|
||
.sort((a, b) => b.distance - a.distance);
|
||
|
||
console.log('Locked rooms:', lockedRooms);
|
||
|
||
if (lockedRooms.length > 0) {
|
||
const targetRoom = lockedRooms[0];
|
||
const requirements = {
|
||
lockType: targetRoom.lockType,
|
||
requires: targetRoom.requires
|
||
};
|
||
console.log('Returning lock requirements:', requirements);
|
||
return requirements;
|
||
}
|
||
|
||
console.log('No lock requirements found');
|
||
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;
|
||
alert('You collected the items from the container.');
|
||
}
|
||
|
||
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) {
|
||
console.log('🔍 TABLET IN RANGE:', {
|
||
distance: Math.round(distance),
|
||
range: BLUETOOTH_SCAN_RANGE
|
||
});
|
||
|
||
// Unlock the tablet
|
||
obj.scenarioData.locked = false;
|
||
console.log('🔓 TABLET UNLOCKED!');
|
||
alert('Bluetooth connection established. Device unlocked.');
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// 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);
|
||
alert(`Scanner locked out. Try again in ${remainingTime} seconds.`);
|
||
return false;
|
||
}
|
||
|
||
if (!scanner.scenarioData?.biometricType === 'fingerprint') {
|
||
console.warn('Invalid scanner type');
|
||
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);
|
||
alert("No valid fingerprint sample found.");
|
||
return false;
|
||
}
|
||
|
||
// Check sample quality
|
||
const qualityThreshold = 0.7;
|
||
if (validSample.quality < qualityThreshold) {
|
||
handleScannerFailure(scannerId);
|
||
alert("Fingerprint sample quality too poor for scanning.");
|
||
return false;
|
||
}
|
||
|
||
// Success case - reset failed attempts
|
||
scannerState.failedAttempts[scannerId] = 0;
|
||
alert("Biometric scan successful!");
|
||
|
||
// 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;
|
||
alert(`Too many failed attempts. Scanner locked for ${SCANNER_LOCKOUT_TIME/1000} seconds.`);
|
||
} else {
|
||
const remainingAttempts = MAX_FAILED_ATTEMPTS - scannerState.failedAttempts[scannerId];
|
||
alert(`Scan failed. ${remainingAttempts} attempts remaining before lockout.`);
|
||
}
|
||
}
|
||
|
||
// Modify collectFingerprint to include visual feedback
|
||
function collectFingerprint(item) {
|
||
if (!item.scenarioData?.hasFingerprint) {
|
||
alert("No fingerprints found on this surface.");
|
||
return null;
|
||
}
|
||
|
||
// Check if player has required items
|
||
if (!hasItemInInventory('fingerprint_kit')) {
|
||
alert("You need a fingerprint kit to collect samples!");
|
||
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) {
|
||
// 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;">
|
||
Toggle tension wrench and manipulate pins to unlock.<br>
|
||
🔵 Blue = Pin moving<br>
|
||
🟢 Green = Pin set correctly<br>
|
||
🔴 Red = Over-pushed (reset)<br>
|
||
Set all pins in the correct order 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
|
||
let clickSound = null;
|
||
const initAudio = () => {
|
||
if (!clickSound) {
|
||
clickSound = new Audio('data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQoGAACBhYqFbF1fdH2Dam9ycG5wdX2Dg4CBhIWCgX+Af4B/goCDgIJ/gn+BfoF/gn+CgIF/gn+Cf4F/g4CDgIJ/hIGFgoWBhYKGgoaCh4OIg4mEioWLhIyFjYaOhI+FkIaRhZKGk4eUhpWHloeXiJiImYiaiZuKnIqdi56LoIyhjaKOo4+kj6WQppGnkqiTqZSqlauWrJetmK6Zr5qwm7GcsZ2yn7OgtKG1oraitqO3pLiltqe4qLmpuqq7q7ysvq2/rsCwwbHCssOzxbTGtce2yLfJuMq5y7rMu827zrzPvdC+0b/SwdPC1MPVxNbF18bYx9nI2sjbydvK3Mvdy97M383gzuHP4tDj0eTT5dTm1efW6Nfp2Ora69vt3O7d797w3/Lh8+P05fXn9+j46Prs+/D89v77AQYHDA8VGh8lKi8zOD1CRkpPVFhbX2NobHBydHZ4enx+gIGCg4SFhoeIiYqLjI2Oj5CRkpOUlZaXmJmam5ydnp+goaKjpKWmp6ipqqusra6vsLGys7S1tre4ubq7vL2+v8DBwsPExcbHyMnKy8zNzs/Q0dLT1NXW19jZ2tvc3d7f4OHi4+Tl5ufo6err7O3u7/Dx8vP09fb3+Pn6+/z9/v8AAQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyAhIiMkJSYnKCkqKywtLi8wMTIzNDU2Nzg5Ojs8PT4/QEFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaW1xdXl9gYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXp7fH1+f4CBgoOEhYaHiImKi4yNjo+QkZKTlJWWl5iZmpucnZ6foKGio6SlpqeoqaqrrK2ur7CxsrO0tba3uLm6u7y9vr/AwcLDxMXGx8jJysvMzc7P0NHS09TV1tfY2drb3N3e3+Dh4uPk5ebn6Onq6+zt7u/w8fLz9PX29/j5+vv8/f7/AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn+AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKztLW2t7i5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+/w==');
|
||
}
|
||
};
|
||
|
||
// Initialize audio on first interaction
|
||
gameContainer.addEventListener('mousedown', initAudio, { once: true });
|
||
|
||
// Add pin binding order and game state
|
||
const numPins = 5;
|
||
const bindingOrder = Array.from({length: numPins}, (_, i) => i)
|
||
.sort(() => Math.random() - 0.5);
|
||
|
||
const gameState = {
|
||
tensionApplied: false,
|
||
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
|
||
};
|
||
|
||
// Create tension wrench toggle
|
||
const tensionWrench = document.createElement('div');
|
||
tensionWrench.style.cssText = `
|
||
width: 100px;
|
||
height: 30px;
|
||
background: ${gameState.tensionApplied ? '#666' : '#444'};
|
||
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.");
|
||
document.body.removeChild(iframe);
|
||
if (currentScene && currentScene.input && currentScene.input.mouse) {
|
||
currentScene.input.mouse.enabled = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
tensionWrench.onclick = () => {
|
||
gameState.tensionApplied = !gameState.tensionApplied;
|
||
tensionWrench.style.background = gameState.tensionApplied ? '#666' : '#444';
|
||
tensionWrench.textContent = `Tension: ${gameState.tensionApplied ? 'ON' : 'OFF'}`;
|
||
if (!gameState.tensionApplied) resetPins(false);
|
||
};
|
||
|
||
// 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;
|
||
`;
|
||
|
||
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);
|
||
|
||
let pressStartTime = 0;
|
||
let pressTimer = null;
|
||
|
||
function checkPinPress() {
|
||
if (pressStartTime === 0) return;
|
||
|
||
const pressDuration = Date.now() - pressStartTime;
|
||
if (pressDuration > gameState.maxPressTime) {
|
||
resetPins();
|
||
clearInterval(pressTimer);
|
||
pressTimer = null;
|
||
}
|
||
}
|
||
|
||
pin.onmousedown = () => {
|
||
pressStartTime = Date.now();
|
||
pressTimer = setInterval(checkPinPress, 100);
|
||
|
||
pin.style.transform = 'translateY(-10px)';
|
||
|
||
if (gameState.tensionApplied) {
|
||
const bindingPin = bindingOrder[gameState.currentBindingIndex];
|
||
if (i === bindingPin) {
|
||
if (!gameState.hardMode) {
|
||
pin.style.background = '#00f';
|
||
}
|
||
|
||
// Start a timer to set the pin
|
||
setTimeout(() => {
|
||
if (pressStartTime !== 0) { // Still pressing
|
||
gameState.pinStates[i] = 2;
|
||
gameState.currentBindingIndex++;
|
||
|
||
if (clickSound) {
|
||
clickSound.currentTime = 0;
|
||
clickSound.play().catch(e => console.log('Audio play failed:', e));
|
||
}
|
||
|
||
if (!gameState.hardMode) {
|
||
pin.style.background = '#0f0';
|
||
}
|
||
|
||
checkWinCondition();
|
||
}
|
||
}, 500); // Need to hold for 500ms to set
|
||
} else if (gameState.pinStates[i] !== 2) {
|
||
if (!gameState.hardMode) {
|
||
pin.style.background = '#00f';
|
||
}
|
||
// Start counting towards potential reset
|
||
gameState.pinPressTime[i] = Date.now();
|
||
}
|
||
}
|
||
};
|
||
|
||
pin.onmouseup = pin.onmouseleave = () => {
|
||
pressStartTime = 0;
|
||
if (pressTimer) {
|
||
clearInterval(pressTimer);
|
||
pressTimer = null;
|
||
}
|
||
|
||
pin.style.transform = 'translateY(0)';
|
||
if (!gameState.tensionApplied || gameState.pinStates[i] !== 2) {
|
||
pin.style.background = '#555';
|
||
}
|
||
};
|
||
|
||
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) {
|
||
setTimeout(() => {
|
||
unlockTarget(lockable, 'door', lockable.layer);
|
||
alert("Lock successfully picked!");
|
||
document.body.removeChild(iframe);
|
||
if (currentScene && currentScene.input && currentScene.input.mouse) {
|
||
currentScene.input.mouse.enabled = true;
|
||
}
|
||
}, 500);
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
}
|
||
|
||
</script>
|
||
</body>
|
||
</html> |