diff --git a/assets/objects/fingerprint_kit.png b/assets/objects/fingerprint_kit.png
new file mode 100644
index 0000000..db802b0
Binary files /dev/null and b/assets/objects/fingerprint_kit.png differ
diff --git a/assets/rooms/room_office.json b/assets/rooms/room_office.json
index bad8649..f94e3ec 100644
--- a/assets/rooms/room_office.json
+++ b/assets/rooms/room_office.json
@@ -316,7 +316,32 @@
"width":48,
"x":100.927703875072,
"y":101.732793522267
- }],
+ },
+ {
+ "height":48,
+ "id":15,
+ "name":"fingerprint_kit",
+ "rotation":0,
+ "type":"",
+ "visible":true,
+ "width":48,
+ "x":390,
+ "y":144
+ },
+ {
+ "height": 48,
+ "id": 16,
+ "name": "spoofing_kit",
+ "rotation": 0,
+ "type": "",
+ "visible": true,
+ "width": 48,
+ "x": 340,
+ "y": 144
+ }
+
+
+ ],
"opacity":1,
"type":"objectgroup",
"visible":true,
@@ -324,7 +349,7 @@
"y":0
}],
"nextlayerid":12,
- "nextobjectid":15,
+ "nextobjectid":16,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"1.11.0",
diff --git a/assets/scenarios/ceo_exfil.json b/assets/scenarios/ceo_exfil.json
index 701d051..d91af48 100644
--- a/assets/scenarios/ceo_exfil.json
+++ b/assets/scenarios/ceo_exfil.json
@@ -62,7 +62,10 @@
"name": "Office Computer",
"takeable": false,
"requires": "password",
- "observations": "A computer with a cybersecurity alert on screen"
+ "hasFingerprint": true,
+ "fingerprintOwner": "ceo",
+ "fingerprintQuality": 0.9,
+ "observations": "A computer with a cybersecurity alert on screen. There might be fingerprints on the keyboard."
},
{
"type": "notes",
@@ -71,6 +74,18 @@
"readable": true,
"text": "URGENT: Multiple unauthorized access attempts detected from CEO's office IP address",
"observations": "A concerning IT department memo"
+ },
+ {
+ "type": "fingerprint_kit",
+ "name": "Fingerprint Kit",
+ "takeable": true,
+ "observations": "A kit used for collecting fingerprints from surfaces"
+ },
+ {
+ "type": "spoofing_kit",
+ "name": "Fingerprint Spoofing Kit",
+ "takeable": true,
+ "observations": "A specialized kit containing silicone, gelatin, and other materials for creating artificial fingerprints"
}
]
},
diff --git a/index.html b/index.html
index 290608d..606f074 100644
--- a/index.html
+++ b/index.html
@@ -195,6 +195,57 @@
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
@@ -228,6 +279,7 @@
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.json('gameScenarioJSON', 'assets/scenarios/ceo_exfil.json');
@@ -453,6 +505,9 @@
// 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() {
@@ -1418,6 +1473,28 @@
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`;
@@ -2143,6 +2220,784 @@
});
}
+ // 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 = `
+ Type: ${sample.type}
+ Owner: ${sample.owner}
+ Quality: ${qualityPercentage}%
+ ID: ${sample.id}
+ ${sample.isSpoofed ? 'SPOOFED SAMPLE
' : ''}
+ `;
+
+ // 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 = `
+
+ Drag to dust the surface and reveal fingerprints.
+ 🔍 Gray = Light dusting
+ 🟢 Green = Fingerprint found!
+ ⚠️ White = Over-dusted (avoid this)
+ Find all fingerprints with minimal over-dusting.
+