From 7d479f33234e6b30bc98fd10d2ee37a3ec4dc5ec Mon Sep 17 00:00:00 2001 From: Damian-I Date: Mon, 10 Mar 2025 20:07:51 +0000 Subject: [PATCH 01/24] removed spoofing functionality from fingerprint as it was redundant --- assets/rooms/room_office.json | 13 +- assets/scenarios/ceo_exfil.json | 6 - index.html | 370 ++++++++------------------------ 3 files changed, 94 insertions(+), 295 deletions(-) diff --git a/assets/rooms/room_office.json b/assets/rooms/room_office.json index b79f272..6130598 100644 --- a/assets/rooms/room_office.json +++ b/assets/rooms/room_office.json @@ -331,17 +331,6 @@ { "height": 48, "id": 16, - "name": "spoofing_kit", - "rotation": 0, - "type": "", - "visible": true, - "width": 48, - "x": 340, - "y": 144 - }, - { - "height": 48, - "id": 17, "name": "lockpick", "rotation": 0, "type": "", @@ -360,7 +349,7 @@ "y":0 }], "nextlayerid":12, - "nextobjectid":18, + "nextobjectid":17, "orientation":"orthogonal", "renderorder":"right-down", "tiledversion":"1.11.0", diff --git a/assets/scenarios/ceo_exfil.json b/assets/scenarios/ceo_exfil.json index aec2aa3..709730d 100644 --- a/assets/scenarios/ceo_exfil.json +++ b/assets/scenarios/ceo_exfil.json @@ -94,12 +94,6 @@ "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 2d8e836..f8ad5e1 100644 --- a/index.html +++ b/index.html @@ -684,7 +684,7 @@ #biometrics-panel { position: fixed; bottom: 80px; - right: 160px; + right: 20px; width: 350px; max-height: 500px; background-color: rgba(0, 0, 0, 0.9); @@ -710,8 +710,8 @@ #biometrics-title { font-weight: bold; - font-size: 18px; - color: #e74c3c; + font-size: 16px; + color: #2ecc71; } #biometrics-close { @@ -724,13 +724,13 @@ #biometrics-close:hover { color: white; } - + #biometrics-search-container { padding: 10px 15px; background-color: #333; border-bottom: 1px solid #444; } - + #biometrics-search { width: 100%; padding: 8px 10px; @@ -740,10 +740,10 @@ color: white; font-size: 14px; } - + #biometrics-search:focus { outline: none; - box-shadow: 0 0 0 2px rgba(231, 76, 60, 0.5); + box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.5); } #biometrics-categories { @@ -752,7 +752,7 @@ background-color: #2c2c2c; border-bottom: 1px solid #444; } - + .biometrics-category { padding: 5px 10px; margin-right: 5px; @@ -761,12 +761,12 @@ font-size: 12px; transition: all 0.2s; } - + .biometrics-category.active { - background-color: #e74c3c; + background-color: #2ecc71; color: white; } - + .biometrics-category:hover:not(.active) { background-color: #444; } @@ -798,49 +798,22 @@ } .biometric-sample-name { + display: flex; + justify-content: space-between; font-weight: bold; margin-bottom: 5px; font-size: 14px; - color: #e74c3c; - display: flex; - justify-content: space-between; - align-items: center; + color: #2ecc71; } - + .biometric-sample-icons { display: flex; gap: 5px; } - - .biometric-sample-icon { - font-size: 12px; - color: #aaa; - } - - .biometric-sample-details { - font-size: 13px; - line-height: 1.4; - white-space: pre-wrap; - max-height: 80px; - overflow: hidden; - transition: max-height 0.3s; - } - - .biometric-sample.expanded .biometric-sample-details { - max-height: 1000px; - } - - .biometric-sample-timestamp { - font-size: 11px; - color: #888; - margin-top: 5px; - text-align: right; - } .biometric-quality-bar { - width: 100%; height: 5px; - background: #333; + background-color: #333; margin-top: 8px; border-radius: 2px; margin-bottom: 8px; @@ -852,45 +825,27 @@ transition: width 0.3s ease; } - #biometrics-toggle { - position: relative; - width: 60px; - height: 60px; - background-color: #e74c3c; - color: white; - border-radius: 50%; - display: flex; - justify-content: center; - align-items: center; - cursor: pointer; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3); - z-index: 1998; - font-size: 28px; - transition: all 0.3s ease; - margin-left: 10px; + .biometric-sample-details { + font-size: 13px; + line-height: 1.4; + white-space: pre-wrap; + max-height: 0; + overflow: hidden; + transition: max-height 0.3s; + } + + .biometric-sample.expanded .biometric-sample-details { + max-height: 200px; } - #biometrics-toggle:hover { - background-color: #c0392b; - transform: scale(1.1); - } - - #biometrics-count { - position: absolute; - top: 0; - right: 0; - background-color: #c0392b; - color: white; - border-radius: 50%; - width: 22px; - height: 22px; - font-size: 12px; + .biometric-sample-timestamp { + font-size: 11px; + color: #888; + margin-top: 5px; + text-align: right; display: none; - justify-content: center; - align-items: center; - font-weight: bold; } - + /* Rest of existing styles follow */ .biometric-sample-timestamp { font-size: 11px; @@ -913,6 +868,46 @@ #game-container { position: relative; } + + /* Toggle Buttons */ + #biometrics-toggle { + position: relative; + width: 60px; + height: 60px; + background-color: #2ecc71; + color: white; + border-radius: 50%; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3); + z-index: 1998; + font-size: 28px; + transition: all 0.3s ease; + margin-left: 10px; + } + + #biometrics-toggle:hover { + background-color: #27ae60; + transform: scale(1.1); + } + + #biometrics-count { + position: absolute; + top: -5px; + right: -5px; + background-color: #e74c3c; + color: white; + border-radius: 50%; + width: 24px; + height: 24px; + font-size: 14px; + display: flex; + justify-content: center; + align-items: center; + font-weight: bold; + } @@ -987,7 +982,6 @@
All
Fingerprints
-
Spoofed
@@ -1475,10 +1469,6 @@ 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, @@ -3830,9 +3820,13 @@ // 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()}`; + // For original samples from items, we use the item's data + if (item.scenarioData?.fingerprintOwner) { + return `fp_${item.scenarioData.fingerprintOwner}_${Date.now()}`; + } + + // Fallback unique identifier + return `fp_unknown_${Date.now()}`; } // Add helper function to check if player has required collection tools @@ -4152,7 +4146,6 @@ Owner: ${sample.owner}
Quality: ${qualityPercentage}%
ID: ${sample.id}
- ${sample.isSpoofed ? 'SPOOFED SAMPLE
' : ''} `; // Add quality bar @@ -4176,72 +4169,14 @@ 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'; - } + const biometricsPanel = document.getElementById('biometrics-panel'); + biometricsPanel.style.display = 'none'; } // Helper function to get color based on quality @@ -4270,11 +4205,6 @@ } 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()}`; @@ -4284,33 +4214,6 @@ 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 @@ -6171,12 +6074,9 @@ // Apply category filter if (activeCategory === 'fingerprint') { - // Only show non-spoofed fingerprints in the fingerprint category filteredSamples = filteredSamples.filter(sample => - sample.type === 'fingerprint' && !sample.isSpoofed + sample.type === 'fingerprint' ); - } else if (activeCategory === 'spoofed') { - filteredSamples = filteredSamples.filter(sample => sample.isSpoofed); } // The 'all' category shows everything by default @@ -6217,13 +6117,7 @@ let sampleContent = `
${sample.type.charAt(0).toUpperCase() + sample.type.slice(1)} - ${sample.owner}
-
`; - - if (sample.isSpoofed) { - sampleContent += `🔄`; - } - - sampleContent += `
`; +
`; // Add quality bar sampleContent += `
@@ -6241,27 +6135,6 @@ sampleElement.innerHTML = sampleContent; - // 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: #e74c3c; - border: none; - color: white; - border-radius: 3px; - cursor: pointer; - width: 100%; - `; - spoofButton.onclick = (event) => { - event.stopPropagation(); // Prevent toggling expand/collapse - createSpoofedSampleUI(sample); - }; - sampleElement.appendChild(spoofButton); - } - // Toggle expanded state when clicked sampleElement.addEventListener('click', () => { sampleElement.classList.toggle('expanded'); @@ -6271,51 +6144,6 @@ }); } - // Function to create spoofed sample with UI - function createSpoofedSampleUI(sample) { - // Find the sample element - const sampleElement = document.querySelector(`.biometric-sample[data-id="${sample.id}"]`); - if (!sampleElement) return; - - // Disable any existing spoof buttons - const existingButton = sampleElement.querySelector('button'); - if (existingButton) { - existingButton.disabled = true; - existingButton.textContent = 'Creating spoof...'; - } - - // Add progress bar - const progressBar = document.createElement('div'); - progressBar.className = 'biometric-quality-bar'; - - const progress = document.createElement('div'); - progress.className = 'biometric-quality-fill'; - progress.style.width = '0%'; - progress.style.background = '#e67e22'; - - 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); - updateBiometricsPanel(); // Refresh UI - updateBiometricsCount(); // Update count - gameAlert("Successfully created spoofed sample.", 'success', 'Spoofing Complete', 3000); - } - }, SPOOFING_TIME); - } - // Update the biometrics count function updateBiometricsCount() { const biometricsCount = document.getElementById('biometrics-count'); @@ -6345,15 +6173,21 @@ function initializeBiometricsPanel() { // Set up biometrics toggle button const biometricsToggle = document.getElementById('biometrics-toggle'); - biometricsToggle.addEventListener('click', toggleBiometricsPanel); + if (biometricsToggle) { + biometricsToggle.addEventListener('click', toggleBiometricsPanel); + } // Set up biometrics close button const biometricsClose = document.getElementById('biometrics-close'); - biometricsClose.addEventListener('click', toggleBiometricsPanel); + if (biometricsClose) { + biometricsClose.addEventListener('click', toggleBiometricsPanel); + } // Set up search functionality const biometricsSearch = document.getElementById('biometrics-search'); - biometricsSearch.addEventListener('input', updateBiometricsPanel); + if (biometricsSearch) { + biometricsSearch.addEventListener('input', updateBiometricsPanel); + } // Set up category filters const categories = document.querySelectorAll('.biometrics-category'); @@ -6390,24 +6224,6 @@ const biometricsPanel = document.getElementById('biometrics-panel'); biometricsPanel.style.display = 'none'; } - - // Override createSpoofedSample to add timestamp - const originalCreateSpoofedSample = createSpoofedSample; - createSpoofedSample = function(sample) { - const spoofedSample = originalCreateSpoofedSample(sample); - if (spoofedSample) { - spoofedSample.timestamp = Date.now(); - } - return spoofedSample; - }; - - // We don't need this duplicate initialization since we already added it to the existing DOMContentLoaded handler - // document.addEventListener('DOMContentLoaded', function() { - // // Existing initialization... - // - // // Initialize biometrics panel - // initializeBiometricsPanel(); - // }); // Function to drop an inventory item function dropInventoryItem(item) { From 3bdafd49af91163e698c792ba4d9f10b182054dc Mon Sep 17 00:00:00 2001 From: Damian-I Date: Mon, 10 Mar 2025 22:42:06 +0000 Subject: [PATCH 02/24] Enhance fingerprint dusting minigame with advanced mechanics and visual improvements --- index.html | 679 ++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 539 insertions(+), 140 deletions(-) diff --git a/index.html b/index.html index f8ad5e1..51ea2e2 100644 --- a/index.html +++ b/index.html @@ -4236,69 +4236,338 @@ const gameContainer = document.createElement('div'); gameContainer.style.cssText = ` width: 100%; - height: calc(100% - 60px); + height: calc(100% - 100px); display: grid; grid-template-columns: repeat(20, minmax(0, 1fr)); grid-template-rows: repeat(20, minmax(0, 1fr)); - gap: 1px; - background: #222; + gap: 2px; + background: #1a1a1a; padding: 10px; - margin-top: 40px; + margin-top: 70px; + border-radius: 5px; + box-shadow: 0 0 15px rgba(0, 0, 0, 0.5) inset; + position: relative; + overflow: hidden; `; - // Add instructions - const instructions = document.createElement('div'); - instructions.innerHTML = ` -

Fingerprint Dusting

-

- 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. -

- `; - instructions.style.cssText = ` + // Add background texture/pattern for a more realistic surface + const gridBackground = document.createElement('div'); + gridBackground.style.cssText = ` position: absolute; - top: 10px; - left: 50%; - transform: translateX(-50%); - width: 90%; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 0.3; + pointer-events: none; + z-index: 0; + `; + + // Create the grid pattern using encoded SVG + const svgGrid = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' fill='%23111'/%3E%3Cpath d='M0 50h100M50 0v100' stroke='%23222' stroke-width='0.5'/%3E%3Cpath d='M25 0v100M75 0v100M0 25h100M0 75h100' stroke='%23191919' stroke-width='0.3'/%3E%3C/svg%3E`; + + gridBackground.style.backgroundImage = `url('${svgGrid}')`; + gameContainer.appendChild(gridBackground); + + // Add instructions and header with difficulty selection + const header = document.createElement('div'); + header.style.cssText = ` + position: absolute; + top: 0; + left: 0; + width: 100%; + background: rgba(30, 30, 30, 0.9); + padding: 10px; + display: flex; + flex-direction: column; + align-items: center; + `; + + header.innerHTML = ` +

Fingerprint Dusting

+
+ + + +
+

+ Drag to dust the surface and reveal fingerprints. Avoid over-dusting! +

`; // Add progress display const progressText = document.createElement('div'); progressText.style.cssText = ` position: absolute; - bottom: 10px; + bottom: 15px; left: 50%; transform: translateX(-50%); color: white; text-align: center; font-size: 16px; + background: rgba(0, 0, 0, 0.6); + padding: 5px 15px; + border-radius: 15px; + z-index: 10; `; + // Add tool selection + const toolsContainer = document.createElement('div'); + toolsContainer.style.cssText = ` + position: absolute; + bottom: 15px; + left: 15px; + display: flex; + gap: 10px; + z-index: 10; + `; + + const tools = [ + { name: 'Fine Brush', size: 1, color: '#3498db' }, + { name: 'Medium Brush', size: 2, color: '#2ecc71' }, + { name: 'Wide Brush', size: 3, color: '#e67e22' } + ]; + + let currentTool = tools[1]; // Start with medium brush + + tools.forEach(tool => { + const toolButton = document.createElement('button'); + toolButton.className = tool.name === currentTool.name ? 'tool-button active' : 'tool-button'; + toolButton.textContent = tool.name; + toolButton.style.cssText = ` + background: ${tool.color}; + color: white; + border: none; + border-radius: 3px; + padding: 5px 10px; + cursor: pointer; + opacity: ${tool.name === currentTool.name ? '1' : '0.7'}; + `; + + toolButton.addEventListener('click', () => { + document.querySelectorAll('.tool-button').forEach(btn => { + btn.classList.remove('active'); + btn.style.opacity = '0.7'; + }); + toolButton.classList.add('active'); + toolButton.style.opacity = '1'; + currentTool = tool; + }); + + toolsContainer.appendChild(toolButton); + }); + // Generate fingerprint pattern const gridSize = 20; - const fingerprintCells = new Set(); + let 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}`); + + // Game state variables + let difficultySettings = { + easy: { + requiredCoverage: 0.3, // 30% of prints + maxOverDusted: 35, + fingerprints: 40, + pattern: 'simple' + }, + medium: { + requiredCoverage: 0.4, // 40% of prints + maxOverDusted: 25, + fingerprints: 50, + pattern: 'medium' + }, + hard: { + requiredCoverage: 0.5, // 50% of prints + maxOverDusted: 15, + fingerprints: 60, + pattern: 'complex' } + }; + + let currentDifficulty = 'medium'; + + // Generate fingerprint pattern based on difficulty + function generateFingerprint(difficulty) { + const pattern = difficultySettings[difficulty].pattern; + const numPrints = difficultySettings[difficulty].fingerprints; + const newFingerprintCells = new Set(); + + if (pattern === 'simple') { + // Simple oval-like pattern + for (let i = 0; i < numPrints; i++) { + const angle = (i / numPrints) * Math.PI * 2; + const distance = 5 + Math.random() * 3; + const x = Math.floor(centerX + Math.cos(angle) * distance); + const y = Math.floor(centerY + Math.sin(angle) * distance); + + if (x >= 0 && x < gridSize && y >= 0 && y < gridSize) { + newFingerprintCells.add(`${x},${y}`); + + // Add a few adjacent cells to make it less sparse + for (let j = 0; j < 2; j++) { + const nx = x + Math.floor(Math.random() * 3) - 1; + const ny = y + Math.floor(Math.random() * 3) - 1; + if (nx >= 0 && nx < gridSize && ny >= 0 && ny < gridSize) { + newFingerprintCells.add(`${nx},${ny}`); + } + } + } + } + } else if (pattern === 'medium') { + // Medium complexity - spiral pattern with variations + for (let i = 0; i < numPrints; i++) { + const t = i / numPrints * 5; + const distance = 2 + t * 0.8; + const noise = Math.random() * 2 - 1; + const x = Math.floor(centerX + Math.cos(t * Math.PI * 2) * (distance + noise)); + const y = Math.floor(centerY + Math.sin(t * Math.PI * 2) * (distance + noise)); + + if (x >= 0 && x < gridSize && y >= 0 && y < gridSize) { + newFingerprintCells.add(`${x},${y}`); + } + } + + // Add whorls and arches + for (let i = 0; i < 20; i++) { + const angle = (i / 20) * Math.PI * 2; + const distance = 7; + const x = Math.floor(centerX + Math.cos(angle) * distance); + const y = Math.floor(centerY + Math.sin(angle) * distance); + + if (x >= 0 && x < gridSize && y >= 0 && y < gridSize) { + newFingerprintCells.add(`${x},${y}`); + + // Add ridge-like pattern + for (let j = 1; j <= 3; j++) { + const ridgeX = Math.floor(centerX + Math.cos(angle) * (distance - j)); + const ridgeY = Math.floor(centerY + Math.sin(angle) * (distance - j)); + if (ridgeX >= 0 && ridgeX < gridSize && ridgeY >= 0 && ridgeY < gridSize) { + newFingerprintCells.add(`${ridgeX},${ridgeY}`); + } + } + } + } + } else { + // Complex pattern - detailed whorls and ridge patterns + for (let i = 0; i < numPrints; i++) { + // Main loop - create a complex whorl pattern + const t = i / numPrints * 8; + const distance = 2 + t * 0.6; + const noise = Math.sin(t * 5) * 1.5; + const x = Math.floor(centerX + Math.cos(t * Math.PI * 2) * (distance + noise)); + const y = Math.floor(centerY + Math.sin(t * Math.PI * 2) * (distance + noise)); + + if (x >= 0 && x < gridSize && y >= 0 && y < gridSize) { + newFingerprintCells.add(`${x},${y}`); + } + + // Add bifurcations and ridge endings + if (i % 5 === 0) { + const bifAngle = t * Math.PI * 2 + Math.PI/4; + const bx = Math.floor(x + Math.cos(bifAngle) * 1); + const by = Math.floor(y + Math.sin(bifAngle) * 1); + if (bx >= 0 && bx < gridSize && by >= 0 && by < gridSize) { + newFingerprintCells.add(`${bx},${by}`); + } + } + } + + // Add delta patterns + for (let d = 0; d < 3; d++) { + const deltaAngle = (d / 3) * Math.PI * 2; + const deltaX = Math.floor(centerX + Math.cos(deltaAngle) * 8); + const deltaY = Math.floor(centerY + Math.sin(deltaAngle) * 8); + + for (let r = 0; r < 5; r++) { + for (let a = 0; a < 3; a++) { + const rayAngle = deltaAngle + (a - 1) * Math.PI/4; + const rx = Math.floor(deltaX + Math.cos(rayAngle) * r); + const ry = Math.floor(deltaY + Math.sin(rayAngle) * r); + if (rx >= 0 && rx < gridSize && ry >= 0 && ry < gridSize) { + newFingerprintCells.add(`${rx},${ry}`); + } + } + } + } + } + + // Ensure we have at least the minimum number of cells + while (newFingerprintCells.size < numPrints) { + const x = centerX + Math.floor(Math.random() * 12 - 6); + const y = centerY + Math.floor(Math.random() * 12 - 6); + if (x >= 0 && x < gridSize && y >= 0 && y < gridSize) { + newFingerprintCells.add(`${x},${y}`); + } + } + + return newFingerprintCells; } - - // 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}`); + + // Initialize with medium difficulty + fingerprintCells = generateFingerprint(currentDifficulty); + + // Create particle container for dust effects + const particleContainer = document.createElement('div'); + particleContainer.style.cssText = ` + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 5; + overflow: hidden; + `; + + // Create function to add dust particles + function createDustParticles(x, y, intensity, color) { + const numParticles = Math.floor(5 + intensity * 5); // 5-10 particles based on intensity + + for (let i = 0; i < numParticles; i++) { + const particle = document.createElement('div'); + const size = Math.random() * 3 + 1; // 1-4px + const angle = Math.random() * Math.PI * 2; + const distance = Math.random() * 20 * intensity; + const duration = Math.random() * 1000 + 500; // 500-1500ms + + particle.style.cssText = ` + position: absolute; + width: ${size}px; + height: ${size}px; + background: ${color}; + border-radius: 50%; + opacity: ${Math.random() * 0.3 + 0.3}; + top: ${y}px; + left: ${x}px; + transform: translate(-50%, -50%); + pointer-events: none; + z-index: 6; + `; + + particleContainer.appendChild(particle); + + // Animate the particle + const animation = particle.animate([ + { + transform: 'translate(-50%, -50%)', + opacity: particle.style.opacity + }, + { + transform: `translate( + calc(-50% + ${Math.cos(angle) * distance}px), + calc(-50% + ${Math.sin(angle) * distance}px) + )`, + opacity: 0 + } + ], { + duration: duration, + easing: 'cubic-bezier(0.25, 1, 0.5, 1)' + }); + + animation.onfinish = () => { + particle.remove(); + }; } } @@ -4306,6 +4575,7 @@ let revealedPrints = 0; let totalPrints = fingerprintCells.size; let overDusted = 0; + let requiredPrints = Math.ceil(totalPrints * difficultySettings[currentDifficulty].requiredCoverage); // Create grid cells for (let y = 0; y < gridSize; y++) { @@ -4316,21 +4586,22 @@ height: 100%; background: black; position: relative; + transition: background-color 0.1s; cursor: pointer; `; cell.dataset.x = x; cell.dataset.y = y; cell.dataset.dustLevel = '0'; - cell.dataset.hasFingerprint = fingerprintCells.has(`${x},${y}`); + cell.dataset.hasFingerprint = fingerprintCells.has(`${x},${y}`) ? 'true' : 'false'; gameContainer.appendChild(cell); } } - // Add dragging interaction at container level + // Add dragging interaction 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); @@ -4340,17 +4611,41 @@ // Get the cell element under the cursor const cell = document.elementFromPoint(e.clientX, e.clientY); if (cell && cell.dataset.dustLevel !== undefined) { + // Get cell position for particles + const cellRect = cell.getBoundingClientRect(); + + // Calculate relative position within the gameContainer (not the viewport) + const gameContainerRect = gameContainer.getBoundingClientRect(); + const particleContainerRect = particleContainer.getBoundingClientRect(); + + // Calculate position relative to particle container + const cellCenterX = (cellRect.left + cellRect.width / 2) - particleContainerRect.left; + const cellCenterY = (cellRect.top + cellRect.height / 2) - particleContainerRect.top; + 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) { + // Tool intensity affects dusting rate and particle effects + const toolIntensity = currentTool.size / 3; // 0.33 to 1 + + // Only allow dusting every 50-150ms for each cell (based on tool size) + const cooldown = 150 - (toolIntensity * 100); // 50ms for wide brush, 150ms for fine + + if (!lastDustTime[cellId] || currentTime - lastDustTime[cellId] > cooldown) { if (dustLevel < 3) { - // Increment dust level with 33% chance after level 1 - if (dustLevel < 1 || Math.random() < 0.33) { + // Increment dust level with a probability based on tool intensity + const dustProbability = toolIntensity * 0.5 + 0.1; // 0.1-0.6 chance based on tool + + if (dustLevel < 1 || Math.random() < dustProbability) { cell.dataset.dustLevel = (dustLevel + 1).toString(); updateCellColor(cell); + + // Create dust particles + const hasFingerprint = cell.dataset.hasFingerprint === 'true'; + let particleColor = dustLevel === 1 ? '#666' : (hasFingerprint ? '#1aff1a' : '#aaa'); + createDustParticles(cellCenterX, cellCenterY, toolIntensity, particleColor); + checkProgress(); } lastDustTime[cellId] = currentTime; @@ -4365,15 +4660,24 @@ if (dustLevel === 0) { cell.style.background = 'black'; + cell.style.boxShadow = 'none'; } else if (dustLevel === 1) { cell.style.background = '#444'; + cell.style.boxShadow = 'inset 0 0 3px rgba(255,255,255,0.2)'; } else if (dustLevel === 2) { - cell.style.background = hasFingerprint ? '#0f0' : '#888'; + if (hasFingerprint) { + cell.style.background = '#0f0'; + cell.style.boxShadow = 'inset 0 0 5px rgba(0,255,0,0.5), 0 0 5px rgba(0,255,0,0.3)'; + } else { + cell.style.background = '#888'; + cell.style.boxShadow = 'inset 0 0 4px rgba(255,255,255,0.3)'; + } } else { cell.style.background = '#ccc'; + cell.style.boxShadow = 'inset 0 0 5px rgba(255,255,255,0.5)'; } } @@ -4382,115 +4686,204 @@ 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++; + if (cell.dataset) { // Check if it's a cell element + const dustLevel = parseInt(cell.dataset.dustLevel || '0'); + const hasFingerprint = cell.dataset.hasFingerprint === 'true'; + + if (hasFingerprint && dustLevel === 2) revealedPrints++; + if (dustLevel === 3) overDusted++; + } }); - const requiredPrints = Math.ceil(totalPrints * 0.4); // 40% requirement + // Update progress display progressText.innerHTML = ` -
Found: ${revealedPrints}/${requiredPrints} required prints
-
- Over-dusted: ${overDusted}/25 max +
+ Found: ${revealedPrints}/${requiredPrints} required prints + + Over-dusted: ${overDusted}/${difficultySettings[currentDifficulty].maxOverDusted} max + +
+
+
`; // 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); + if (overDusted >= difficultySettings[currentDifficulty].maxOverDusted) { + showFailure("Too many over-dusted areas!"); 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), - timestamp: Date.now() - }; - - 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; - } - - // Update the biometrics panel and count - updateBiometricsPanel(); - updateBiometricsCount(); - - // Show notification - gameAlert(`Collected ${sample.owner}'s fingerprint sample`, 'success', 'Sample Acquired', 3000); - }, 1500); + // Check win condition + if (revealedPrints >= requiredPrints) { + showSuccess(); } } + function showSuccess() { + // Calculate quality based on dusting precision + const dustPenalty = overDusted / difficultySettings[currentDifficulty].maxOverDusted; // 0-1 + const coverageBonus = revealedPrints / totalPrints; // 0-1 + + // Higher quality for more coverage and less over-dusting + const quality = 0.7 + (coverageBonus * 0.25) - (dustPenalty * 0.15); + + // 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; + box-shadow: 0 0 20px rgba(0, 255, 0, 0.3); + `; + + const qualityPercentage = Math.round(quality * 100); + const qualityRating = qualityPercentage >= 95 ? 'Perfect' : + qualityPercentage >= 85 ? 'Excellent' : + qualityPercentage >= 75 ? 'Good' : 'Acceptable'; + + successMessage.innerHTML = ` +
Fingerprint successfully collected!
+
Quality: ${qualityRating} (${qualityPercentage}%)
+
+ Prints revealed: ${revealedPrints}/${totalPrints}
+ Over-dusted areas: ${overDusted}
+ Difficulty: ${currentDifficulty.charAt(0).toUpperCase() + currentDifficulty.slice(1)} +
+ `; + + 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: quality, // Quality between 0.7 and ~1.0 + data: generateFingerprintData(item), + timestamp: Date.now() + }; + + 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; + } + + // Update the biometrics panel and count + updateBiometricsPanel(); + updateBiometricsCount(); + + // Show notification + gameAlert(`Collected ${sample.owner}'s fingerprint sample (${qualityRating} quality)`, 'success', 'Sample Acquired', 3000); + }, 2000); + } + + function showFailure(reason) { + // 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; + box-shadow: 0 0 20px rgba(255, 0, 0, 0.3); + `; + failureMessage.innerHTML = ` +
${reason}
+
Try again with more careful dusting.
+ `; + + iframe.appendChild(failureMessage); + + // Disable further interaction + isDragging = false; + gameContainer.style.pointerEvents = 'none'; + + setTimeout(() => { + document.body.removeChild(iframe); + scene.input.mouse.enabled = true; + }, 2000); + } + + // Handle difficulty buttons + iframe.addEventListener('click', (e) => { + if (e.target.classList.contains('difficulty-button')) { + // Get difficulty from button + const newDifficulty = e.target.id.split('-')[0]; // easy, medium, or hard + + // Only change if different + if (newDifficulty !== currentDifficulty) { + currentDifficulty = newDifficulty; + + // Update active button + document.querySelectorAll('.difficulty-button').forEach(btn => { + btn.classList.remove('active'); + btn.style.opacity = '0.7'; + }); + e.target.classList.add('active'); + e.target.style.opacity = '1'; + + // Reset the game with new difficulty + resetGame(); + } + } + }); + + function resetGame() { + // Reset game state + fingerprintCells = generateFingerprint(currentDifficulty); + totalPrints = fingerprintCells.size; + requiredPrints = Math.ceil(totalPrints * difficultySettings[currentDifficulty].requiredCoverage); + revealedPrints = 0; + overDusted = 0; + + // Reset cells + gameContainer.childNodes.forEach(node => { + if (node.dataset && node.dataset.x !== undefined) { + const x = parseInt(node.dataset.x); + const y = parseInt(node.dataset.y); + node.dataset.hasFingerprint = fingerprintCells.has(`${x},${y}`) ? 'true' : 'false'; + node.dataset.dustLevel = '0'; + updateCellColor(node); + } + }); + // Update progress + checkProgress(); + } + // Add close button const closeButton = document.createElement('button'); - closeButton.textContent = 'X'; + closeButton.textContent = '✕'; closeButton.style.cssText = ` position: absolute; right: 10px; @@ -4500,6 +4893,7 @@ color: white; font-size: 20px; cursor: pointer; + z-index: 10; `; closeButton.onclick = () => { document.body.removeChild(iframe); @@ -4508,11 +4902,16 @@ // Assemble the interface iframe.appendChild(closeButton); - iframe.appendChild(instructions); + iframe.appendChild(header); iframe.appendChild(gameContainer); + iframe.appendChild(particleContainer); iframe.appendChild(progressText); + iframe.appendChild(toolsContainer); document.body.appendChild(iframe); + // Start the game + checkProgress(); + // Disable game movement const scene = item.scene; scene.input.mouse.enabled = false; From 6accd5c54f6ec93d2f0aafe15e95fb3d2760b0c9 Mon Sep 17 00:00:00 2001 From: Damian-I Date: Mon, 10 Mar 2025 22:45:57 +0000 Subject: [PATCH 03/24] Implement advanced brush mechanics for fingerprint dusting minigame --- index.html | 119 +++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 84 insertions(+), 35 deletions(-) diff --git a/index.html b/index.html index 51ea2e2..7a3d889 100644 --- a/index.html +++ b/index.html @@ -4323,9 +4323,9 @@ `; const tools = [ - { name: 'Fine Brush', size: 1, color: '#3498db' }, - { name: 'Medium Brush', size: 2, color: '#2ecc71' }, - { name: 'Wide Brush', size: 3, color: '#e67e22' } + { name: 'Fine Brush', size: 1, color: '#3498db', radius: 0 }, // Only affects current cell + { name: 'Medium Brush', size: 2, color: '#2ecc71', radius: 1 }, // Affects current cell and adjacent + { name: 'Wide Brush', size: 3, color: '#e67e22', radius: 2 } // Affects current cell and 2 cells around ]; let currentTool = tools[1]; // Start with medium brush @@ -4611,46 +4611,95 @@ // Get the cell element under the cursor const cell = document.elementFromPoint(e.clientX, e.clientY); if (cell && cell.dataset.dustLevel !== undefined) { - // Get cell position for particles + // Get current cell coordinates + const centerX = parseInt(cell.dataset.x); + const centerY = parseInt(cell.dataset.y); + + // Get a list of cells to dust based on the brush radius + const cellsToDust = []; + const radius = currentTool.radius; + + // Add the current cell and cells within radius + for (let y = centerY - radius; y <= centerY + radius; y++) { + for (let x = centerX - radius; x <= centerX + radius; x++) { + // Skip cells outside the grid + if (x < 0 || x >= gridSize || y < 0 || y >= gridSize) continue; + + // For medium brush, use a diamond pattern (taxicab distance) + if (currentTool.size === 2) { + // Manhattan distance: |x1-x2| + |y1-y2| + const distance = Math.abs(x - centerX) + Math.abs(y - centerY); + if (distance > radius) continue; // Skip if too far away + } + // For wide brush, use a circle pattern (Euclidean distance) + else if (currentTool.size === 3) { + // Euclidean distance: √[(x1-x2)² + (y1-y2)²] + const distance = Math.sqrt(Math.pow(x - centerX, 2) + Math.pow(y - centerY, 2)); + if (distance > radius) continue; // Skip if too far away + } + + // Find this cell in the DOM + const targetCell = gameContainer.querySelector(`[data-x="${x}"][data-y="${y}"]`); + if (targetCell) { + cellsToDust.push(targetCell); + } + } + } + + // Get cell position for particles (center cell) const cellRect = cell.getBoundingClientRect(); - - // Calculate relative position within the gameContainer (not the viewport) - const gameContainerRect = gameContainer.getBoundingClientRect(); const particleContainerRect = particleContainer.getBoundingClientRect(); - - // Calculate position relative to particle container const cellCenterX = (cellRect.left + cellRect.width / 2) - particleContainerRect.left; const cellCenterY = (cellRect.top + cellRect.height / 2) - particleContainerRect.top; - const cellId = `${cell.dataset.x},${cell.dataset.y}`; - const currentTime = Date.now(); - const dustLevel = parseInt(cell.dataset.dustLevel); - - // Tool intensity affects dusting rate and particle effects - const toolIntensity = currentTool.size / 3; // 0.33 to 1 - - // Only allow dusting every 50-150ms for each cell (based on tool size) - const cooldown = 150 - (toolIntensity * 100); // 50ms for wide brush, 150ms for fine - - if (!lastDustTime[cellId] || currentTime - lastDustTime[cellId] > cooldown) { - if (dustLevel < 3) { - // Increment dust level with a probability based on tool intensity - const dustProbability = toolIntensity * 0.5 + 0.1; // 0.1-0.6 chance based on tool - - if (dustLevel < 1 || Math.random() < dustProbability) { - cell.dataset.dustLevel = (dustLevel + 1).toString(); - updateCellColor(cell); + // Process all cells to dust + cellsToDust.forEach(targetCell => { + const cellId = `${targetCell.dataset.x},${targetCell.dataset.y}`; + const currentTime = Date.now(); + const dustLevel = parseInt(targetCell.dataset.dustLevel); + + // Tool intensity affects dusting rate and particle effects + const toolIntensity = currentTool.size / 3; // 0.33 to 1 + + // Only allow dusting every 50-150ms for each cell (based on tool size) + const cooldown = 150 - (toolIntensity * 100); // 50ms for wide brush, 150ms for fine + + if (!lastDustTime[cellId] || currentTime - lastDustTime[cellId] > cooldown) { + if (dustLevel < 3) { + // Increment dust level with a probability based on tool intensity + const dustProbability = toolIntensity * 0.5 + 0.1; // 0.1-0.6 chance based on tool - // Create dust particles - const hasFingerprint = cell.dataset.hasFingerprint === 'true'; - let particleColor = dustLevel === 1 ? '#666' : (hasFingerprint ? '#1aff1a' : '#aaa'); - createDustParticles(cellCenterX, cellCenterY, toolIntensity, particleColor); - - checkProgress(); + if (dustLevel < 1 || Math.random() < dustProbability) { + targetCell.dataset.dustLevel = (dustLevel + 1).toString(); + updateCellColor(targetCell); + + // Create dust particles for the current cell or at a position calculated for surrounding cells + if (targetCell === cell) { + // Center cell - use the already calculated position + const hasFingerprint = targetCell.dataset.hasFingerprint === 'true'; + let particleColor = dustLevel === 1 ? '#666' : (hasFingerprint ? '#1aff1a' : '#aaa'); + createDustParticles(cellCenterX, cellCenterY, toolIntensity, particleColor); + } else { + // For surrounding cells, calculate their relative position from the center cell + const targetCellRect = targetCell.getBoundingClientRect(); + const targetCellX = (targetCellRect.left + targetCellRect.width / 2) - particleContainerRect.left; + const targetCellY = (targetCellRect.top + targetCellRect.height / 2) - particleContainerRect.top; + + const hasFingerprint = targetCell.dataset.hasFingerprint === 'true'; + let particleColor = dustLevel === 1 ? '#666' : (hasFingerprint ? '#1aff1a' : '#aaa'); + + // Create fewer particles for surrounding cells + const reducedIntensity = toolIntensity * 0.6; + createDustParticles(targetCellX, targetCellY, reducedIntensity, particleColor); + } + } + lastDustTime[cellId] = currentTime; } - lastDustTime[cellId] = currentTime; } - } + }); + + // Update progress after dusting + checkProgress(); } }); From 5ebe37a267062ae624ea18e0b39902c073a7b160 Mon Sep 17 00:00:00 2001 From: Damian-I Date: Mon, 10 Mar 2025 22:54:01 +0000 Subject: [PATCH 04/24] Change size of cells and grid --- index.html | 47 ++++++++++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/index.html b/index.html index 7a3d889..0b02912 100644 --- a/index.html +++ b/index.html @@ -4223,27 +4223,32 @@ top: 50%; left: 50%; transform: translate(-50%, -50%); - width: 60%; - height: 60%; + width: 75%; + height: 75%; background: rgba(0, 0, 0, 0.9); border: 1px solid #444; z-index: 1000; padding: 20px; border-radius: 5px; + display: flex; + flex-direction: column; + overflow: hidden; `; // Create game container const gameContainer = document.createElement('div'); gameContainer.style.cssText = ` - width: 100%; - height: calc(100% - 100px); + width: 80%; + height: 80%; + max-width: 600px; + max-height: 600px; display: grid; - grid-template-columns: repeat(20, minmax(0, 1fr)); - grid-template-rows: repeat(20, minmax(0, 1fr)); - gap: 2px; + grid-template-columns: repeat(30, 1fr); + grid-template-rows: repeat(30, 1fr); + gap: 1px; background: #1a1a1a; - padding: 10px; - margin-top: 70px; + padding: 5px; + margin: 70px auto 20px auto; border-radius: 5px; box-shadow: 0 0 15px rgba(0, 0, 0, 0.5) inset; position: relative; @@ -4309,6 +4314,8 @@ padding: 5px 15px; border-radius: 15px; z-index: 10; + width: 80%; + max-width: 500px; `; // Add tool selection @@ -4320,12 +4327,14 @@ display: flex; gap: 10px; z-index: 10; + flex-wrap: wrap; + max-width: 30%; `; const tools = [ - { name: 'Fine Brush', size: 1, color: '#3498db', radius: 0 }, // Only affects current cell - { name: 'Medium Brush', size: 2, color: '#2ecc71', radius: 1 }, // Affects current cell and adjacent - { name: 'Wide Brush', size: 3, color: '#e67e22', radius: 2 } // Affects current cell and 2 cells around + { name: 'Fine', size: 1, color: '#3498db', radius: 0 }, // Only affects current cell + { name: 'Medium', size: 2, color: '#2ecc71', radius: 1 }, // Affects current cell and adjacent + { name: 'Wide', size: 3, color: '#e67e22', radius: 2 } // Affects current cell and 2 cells around ]; let currentTool = tools[1]; // Start with medium brush @@ -4358,7 +4367,7 @@ }); // Generate fingerprint pattern - const gridSize = 20; + const gridSize = 30; // Increased from 20 to 30 let fingerprintCells = new Set(); const centerX = Math.floor(gridSize / 2); const centerY = Math.floor(gridSize / 2); @@ -4367,20 +4376,20 @@ let difficultySettings = { easy: { requiredCoverage: 0.3, // 30% of prints - maxOverDusted: 35, - fingerprints: 40, + maxOverDusted: 50, // Increased due to more cells + fingerprints: 60, // Increased proportionally pattern: 'simple' }, medium: { requiredCoverage: 0.4, // 40% of prints - maxOverDusted: 25, - fingerprints: 50, + maxOverDusted: 40, // Increased due to more cells + fingerprints: 75, // Increased proportionally pattern: 'medium' }, hard: { requiredCoverage: 0.5, // 50% of prints - maxOverDusted: 15, - fingerprints: 60, + maxOverDusted: 25, // Increased due to more cells + fingerprints: 90, // Increased proportionally pattern: 'complex' } }; From 957f706770ddd24b17c568299a57b4cb56ef1f0d Mon Sep 17 00:00:00 2001 From: Damian-I Date: Tue, 11 Mar 2025 00:22:23 +0000 Subject: [PATCH 05/24] Refactor biometric authentication with dynamic difficulty and improved fingerprint collection mechanics --- assets/scenarios/biometric_breach.json | 30 ++++++-- index.html | 97 +++++++++++++------------- 2 files changed, 71 insertions(+), 56 deletions(-) diff --git a/assets/scenarios/biometric_breach.json b/assets/scenarios/biometric_breach.json index 306d757..4605295 100644 --- a/assets/scenarios/biometric_breach.json +++ b/assets/scenarios/biometric_breach.json @@ -38,8 +38,8 @@ "takeable": false, "hasFingerprint": true, "fingerprintOwner": "receptionist", - "fingerprintQuality": 0.8, - "observations": "The reception computer shows a security alert screen. There might be fingerprints on the keyboard." + "fingerprintDifficulty": "easy", + "observations": "The reception computer shows a security alert screen. There are clear fingerprints on the keyboard." }, { "type": "lockpick", @@ -55,6 +55,14 @@ "inInventory": true, "observations": "A powerful workstation for cryptographic analysis" }, + { + "type": "notes", + "name": "Biometric Security Notice", + "takeable": true, + "readable": true, + "text": "ALERT: SECURITY PROTOCOLS UPDATED\n\nAll internal doors now require biometric authentication due to the security breach.\n\nTo proceed: Use your fingerprint kit to collect prints, then present them at door scanners. The main office door requires the receptionist's credentials.\n\nReport any unauthorized access attempts to security immediately.", + "observations": "An important notice about the facility's security measures" + }, { "type": "notes", "name": "Facility Map", @@ -71,6 +79,10 @@ "north": ["office2", "office3"], "south": "reception" }, + "locked": true, + "lockType": "biometric", + "requires": "receptionist", + "biometricMatchThreshold": 0.6, "objects": [ { "type": "pc", @@ -78,7 +90,7 @@ "takeable": false, "hasFingerprint": true, "fingerprintOwner": "researcher", - "fingerprintQuality": 0.9, + "fingerprintDifficulty": "medium", "observations": "A research computer with data analysis software running. There might be fingerprints on the keyboard." }, { @@ -105,12 +117,12 @@ "observations": "A backup key for the biometrics lab, kept for emergencies" }, { - "type": "photo", - "name": "Team Photo", + "type": "notes", + "name": "Team Information", "takeable": true, "readable": true, "text": "Project Sentinel Team:\nDr. Eleanor Chen (Director)\nDr. Marcus Patel (Lead Researcher)\nDr. Wei Zhang (Biometrics Specialist)\nAlex Morgan (Security Consultant)", - "observations": "A framed photo of the Project Sentinel research team" + "observations": "Information about the Project Sentinel research team" }, { "type": "notes", @@ -128,6 +140,10 @@ "north": "ceo", "south": "office1" }, + "locked": true, + "lockType": "biometric", + "requires": "researcher", + "biometricMatchThreshold": 0.7, "objects": [ { "type": "pc", @@ -135,7 +151,7 @@ "takeable": false, "hasFingerprint": true, "fingerprintOwner": "intruder", - "fingerprintQuality": 0.85, + "fingerprintDifficulty": "medium", "observations": "A specialized workstation for biometric research. The screen shows someone was recently using it." }, { diff --git a/index.html b/index.html index 0b02912..a2b6050 100644 --- a/index.html +++ b/index.html @@ -3322,56 +3322,60 @@ break; case 'biometric': - debugLog('BIOMETRIC AUTHENTICATION REQUESTED', null, 2); - // Check if player has fingerprint kit - const hasFingerPrintKit = inventory.items.some(item => - item && item.scenarioData && - item.scenarioData.type === 'fingerprint_kit' - ); - - if (!hasFingerPrintKit) { - gameAlert("You need a fingerprint kit to use biometric scanners.", 'warning', 'Missing Equipment', 4000); - return; - } - - // Check if player has required fingerprint sample const requiredFingerprint = lockRequirements.requires; - debugLog('FINGERPRINT REQUIRED', requiredFingerprint, 2); + debugLog('BIOMETRIC LOCK REQUIRES', requiredFingerprint, 2); - // Check if player has a valid fingerprint sample - const validSample = gameState.biometricSamples.find(sample => - sample.type === 'fingerprint' && - sample.owner === requiredFingerprint && - sample.quality >= 0.7 // Quality threshold - ); - - if (validSample) { - debugLog('BIOMETRIC UNLOCK SUCCESS', validSample, 1); - unlockTarget(lockable, type, lockable.layer); - gameAlert(`You successfully used ${validSample.owner}'s fingerprint to unlock the ${type}.`, - 'success', 'Biometric Authentication Successful', 5000); + // Check if the player has collected the required fingerprint + if (gameState.collectedFingerprints && gameState.collectedFingerprints[requiredFingerprint]) { + const fingerprintQuality = gameState.collectedFingerprints[requiredFingerprint]; - // Play success sound and visual effect - const successEffect = lockable.scene ? lockable.scene.add.circle( - lockable.x, - lockable.y, - 32, - 0x00ff00, - 0.5 - ) : null; + // Get the required match threshold from the object or use default + const requiredThreshold = lockable.biometricMatchThreshold || 0.75; - if (successEffect) { - lockable.scene.tweens.add({ - targets: successEffect, - alpha: 0, - scale: 2, - duration: 1000, - onComplete: () => successEffect.destroy() + // Get scenario difficulty and adjust threshold accordingly + const scenarioDifficulty = gameState.activeScenario?.difficulty || "medium"; + let adjustedThreshold = requiredThreshold; + + // Apply difficulty-based adjustments + if (scenarioDifficulty === "easy") { + // Make it easier to unlock on easy difficulty + adjustedThreshold = Math.max(0.5, adjustedThreshold - 0.1); + } else if (scenarioDifficulty === "hard") { + // Make it harder to unlock on hard difficulty + adjustedThreshold = Math.min(0.95, adjustedThreshold + 0.1); + } + + debugLog('BIOMETRIC CHECK', + `Required: ${requiredFingerprint}, Quality: ${fingerprintQuality}, Base Threshold: ${requiredThreshold}, Adjusted for ${scenarioDifficulty}: ${adjustedThreshold}`, 2); + + // Check if the fingerprint quality meets the adjusted threshold + if (fingerprintQuality >= adjustedThreshold) { + debugLog('BIOMETRIC UNLOCK SUCCESS', null, 1); + unlockTarget(lockable, type, lockable.layer); + gameAlert(`You successfully unlocked the ${type} with ${requiredFingerprint}'s fingerprint.`, + 'success', 'Biometric Unlock Successful', 5000); + + // Log the successful biometric unlock + if (!gameState.biometricUnlocks) { + gameState.biometricUnlocks = []; + } + gameState.biometricUnlocks.push({ + location: type, + fingerprint: requiredFingerprint, + quality: fingerprintQuality, + threshold: adjustedThreshold, + timestamp: new Date().toISOString() }); + } else { + debugLog('BIOMETRIC QUALITY TOO LOW', `${fingerprintQuality} < ${adjustedThreshold}`, 2); + gameAlert(`The fingerprint quality (${Math.round(fingerprintQuality * 100)}%) is too low for this lock. + It requires at least ${Math.round(adjustedThreshold * 100)}% quality.`, + 'error', 'Biometric Authentication Failed', 5000); } } else { - debugLog('BIOMETRIC UNLOCK FAILED', null, 2); - gameAlert(`You don't have the required fingerprint sample.`, 'error', 'Biometric Authentication Failed', 4000); + debugLog('MISSING REQUIRED FINGERPRINT', requiredFingerprint, 2); + gameAlert(`This ${type} requires ${requiredFingerprint}'s fingerprint, which you haven't collected yet.`, + 'error', 'Biometric Authentication Failed', 5000); } break; @@ -4290,11 +4294,6 @@ header.innerHTML = `

Fingerprint Dusting

-
- - - -

Drag to dust the surface and reveal fingerprints. Avoid over-dusting!

@@ -4394,7 +4393,7 @@ } }; - let currentDifficulty = 'medium'; + let currentDifficulty = item.scenarioData.fingerprintDifficulty; // Generate fingerprint pattern based on difficulty function generateFingerprint(difficulty) { From 34ac5f4b895c5338807aad02534ebc34e1355ccd Mon Sep 17 00:00:00 2001 From: Damian-I Date: Tue, 11 Mar 2025 01:19:29 +0000 Subject: [PATCH 06/24] Adjust biometric match thresholds and remove advanced fingerprint kit from scenario TODO: fix biometric unlock --- assets/scenarios/biometric_breach.json | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/assets/scenarios/biometric_breach.json b/assets/scenarios/biometric_breach.json index 4605295..ca61018 100644 --- a/assets/scenarios/biometric_breach.json +++ b/assets/scenarios/biometric_breach.json @@ -82,7 +82,7 @@ "locked": true, "lockType": "biometric", "requires": "receptionist", - "biometricMatchThreshold": 0.6, + "biometricMatchThreshold": 0.5, "objects": [ { "type": "pc", @@ -293,7 +293,7 @@ "locked": true, "lockType": "biometric", "requires": "intruder", - "difficulty": "hard", + "biometricMatchThreshold": 0.9, "observations": "A well-hidden wall safe behind a painting with a fingerprint scanner", "contents": [ { @@ -321,12 +321,6 @@ "readable": true, "text": "A = Meet at dock, 4AM\nN = Bring everything\nM = Getaway car ready\n\nLH will pay other half when delivered.", "observations": "A hastily scribbled note, partially crumpled" - }, - { - "type": "fingerprint_kit", - "name": "Advanced Fingerprint Kit", - "takeable": true, - "observations": "A more advanced fingerprint collection kit with higher resolution scanning" } ] }, @@ -356,7 +350,7 @@ "locked": true, "lockType": "biometric", "requires": "intruder", - "difficulty": "medium", + "biometricMatchThreshold": 0.9, "observations": "A secure safe with a fingerprint scanner containing the sensitive research data", "contents": [ { @@ -385,7 +379,7 @@ "locked": true, "lockType": "biometric", "requires": "intruder", - "difficulty": "hard", + "biometricMatchThreshold": 0.9, "observations": "A suspicious case hidden behind server racks with a fingerprint scanner", "contents": [ { From 4bfa492d40c5509aba596de4553662122bd20282 Mon Sep 17 00:00:00 2001 From: Damian-I Date: Tue, 11 Mar 2025 17:04:53 +0000 Subject: [PATCH 07/24] Fixed biometric locktype bug --- index.html | 126 +++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 102 insertions(+), 24 deletions(-) diff --git a/index.html b/index.html index a2b6050..9d2ae97 100644 --- a/index.html +++ b/index.html @@ -3325,33 +3325,51 @@ const requiredFingerprint = lockRequirements.requires; debugLog('BIOMETRIC LOCK REQUIRES', requiredFingerprint, 2); - // Check if the player has collected the required fingerprint - if (gameState.collectedFingerprints && gameState.collectedFingerprints[requiredFingerprint]) { - const fingerprintQuality = gameState.collectedFingerprints[requiredFingerprint]; + // Check if we have fingerprints in the biometricSamples collection + const biometricSamples = gameState.biometricSamples || []; + + // Enhanced debugging - Show collected fingerprints + debugLog('BIOMETRIC SAMPLES', JSON.stringify(biometricSamples), 2); + + // Get the required match threshold from the object or use default + const requiredThreshold = typeof lockable.biometricMatchThreshold === 'number' ? + lockable.biometricMatchThreshold : 0.4; + + debugLog('BIOMETRIC THRESHOLD', requiredThreshold, 2); + + // Find the fingerprint sample for the required person + const fingerprintSample = biometricSamples.find(sample => + sample.owner === requiredFingerprint + ); + + const hasFingerprint = fingerprintSample !== undefined; + debugLog('FINGERPRINT CHECK', `Looking for '${requiredFingerprint}'. Found: ${hasFingerprint}`, 2); + + if (hasFingerprint) { + // Get the quality from the sample + let fingerprintQuality = fingerprintSample.quality; - // Get the required match threshold from the object or use default - const requiredThreshold = lockable.biometricMatchThreshold || 0.75; - - // Get scenario difficulty and adjust threshold accordingly - const scenarioDifficulty = gameState.activeScenario?.difficulty || "medium"; - let adjustedThreshold = requiredThreshold; - - // Apply difficulty-based adjustments - if (scenarioDifficulty === "easy") { - // Make it easier to unlock on easy difficulty - adjustedThreshold = Math.max(0.5, adjustedThreshold - 0.1); - } else if (scenarioDifficulty === "hard") { - // Make it harder to unlock on hard difficulty - adjustedThreshold = Math.min(0.95, adjustedThreshold + 0.1); + // Normalize quality to 0-1 range if it's in percentage format + if (fingerprintQuality > 1) { + fingerprintQuality = fingerprintQuality / 100; } debugLog('BIOMETRIC CHECK', - `Required: ${requiredFingerprint}, Quality: ${fingerprintQuality}, Base Threshold: ${requiredThreshold}, Adjusted for ${scenarioDifficulty}: ${adjustedThreshold}`, 2); + `Required: ${requiredFingerprint}, Quality: ${fingerprintQuality} (${Math.round(fingerprintQuality * 100)}%), Threshold: ${requiredThreshold} (${Math.round(requiredThreshold * 100)}%)`, 2); - // Check if the fingerprint quality meets the adjusted threshold - if (fingerprintQuality >= adjustedThreshold) { + debugLog('QUALITY CHECK', + `Is ${fingerprintQuality} >= ${requiredThreshold}? ${fingerprintQuality >= requiredThreshold}`, 2); + + // Check if the fingerprint quality meets the threshold + if (fingerprintQuality >= requiredThreshold) { debugLog('BIOMETRIC UNLOCK SUCCESS', null, 1); unlockTarget(lockable, type, lockable.layer); + + // Play unlock sound if available + if (typeof playSound === 'function') { + playSound("unlock"); + } + gameAlert(`You successfully unlocked the ${type} with ${requiredFingerprint}'s fingerprint.`, 'success', 'Biometric Unlock Successful', 5000); @@ -3363,17 +3381,19 @@ location: type, fingerprint: requiredFingerprint, quality: fingerprintQuality, - threshold: adjustedThreshold, + threshold: requiredThreshold, timestamp: new Date().toISOString() }); } else { - debugLog('BIOMETRIC QUALITY TOO LOW', `${fingerprintQuality} < ${adjustedThreshold}`, 2); + debugLog('BIOMETRIC QUALITY TOO LOW', + `Quality: ${fingerprintQuality} (${Math.round(fingerprintQuality * 100)}%) < Threshold: ${requiredThreshold} (${Math.round(requiredThreshold * 100)}%)`, 2); gameAlert(`The fingerprint quality (${Math.round(fingerprintQuality * 100)}%) is too low for this lock. - It requires at least ${Math.round(adjustedThreshold * 100)}% quality.`, + It requires at least ${Math.round(requiredThreshold * 100)}% quality.`, 'error', 'Biometric Authentication Failed', 5000); } } else { - debugLog('MISSING REQUIRED FINGERPRINT', requiredFingerprint, 2); + debugLog('MISSING REQUIRED FINGERPRINT', + `Required: '${requiredFingerprint}', Available: ${biometricSamples.map(s => s.owner).join(", ") || "none"}`, 2); gameAlert(`This ${type} requires ${requiredFingerprint}'s fingerprint, which you haven't collected yet.`, 'error', 'Biometric Authentication Failed', 5000); } @@ -6813,6 +6833,64 @@ popup.style.display = 'flex'; } + // Add this function to handle biometric matching near the other utility functions + function performBiometricMatch(lockable, ownerFingerprint, qualityThreshold, type) { + // Get available biometric samples - assuming they're stored in a global variable + // This may need to be adjusted based on how your biometric samples are actually stored + const availableSamples = biometricSamples || []; + + if (!availableSamples || availableSamples.length === 0) { + $("#biometric-status").text("No fingerprint samples available."); + gameAlert("No fingerprint samples found in database.", 'error', 'Scan Failed', 3000); + return; + } + + // Find if we have the owner's fingerprint + const matchingSample = availableSamples.find(sample => sample.id === ownerFingerprint); + + if (!matchingSample) { + // No matching sample found + $("#biometric-status").text("No match found. Access denied."); + gameAlert("Biometric authentication failed.", 'error', 'Access Denied', 3000); + + setTimeout(function() { + $("#biometrics-panel").hide(); + }, 2000); + return; + } + + // Check quality against threshold + const sampleQuality = matchingSample.quality || 0; + + if (sampleQuality >= qualityThreshold) { + // Successful match with sufficient quality + $("#biometric-status").text("Match found! Unlocking..."); + + setTimeout(function() { + // Unlock the target + unlockTarget(lockable, type, lockable.layer); + $("#biometrics-panel").hide(); + + // Play unlock sound if available + if (typeof playSound === 'function') { + playSound("unlock"); + } + + gameAlert(`Biometric match confirmed. ${type.charAt(0).toUpperCase() + type.slice(1)} unlocked.`, 'success', 'Access Granted', 4000); + debugLog('BIOMETRIC AUTHENTICATION SUCCESS', null, 1); + }, 1500); + } else { + // Match found but quality insufficient + $("#biometric-status").text("Sample quality insufficient. Access denied."); + gameAlert(`Biometric sample quality too low (${sampleQuality}/${qualityThreshold} required).`, 'error', 'Low Quality Sample', 3000); + debugLog('BIOMETRIC AUTHENTICATION FAILED - LOW QUALITY', null, 2); + + setTimeout(function() { + $("#biometrics-panel").hide(); + }, 2000); + } + } + \ No newline at end of file From 9d9555606ca574014d55c2a723b823fcad97d1c4 Mon Sep 17 00:00:00 2001 From: Damian-I Date: Wed, 12 Mar 2025 11:04:42 +0000 Subject: [PATCH 08/24] Small oversight updates, scenario metadata still not loading threshold but logic works. --- assets/scenarios/biometric_breach.json | 4 ++-- index.html | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/assets/scenarios/biometric_breach.json b/assets/scenarios/biometric_breach.json index ca61018..79c93b7 100644 --- a/assets/scenarios/biometric_breach.json +++ b/assets/scenarios/biometric_breach.json @@ -227,7 +227,7 @@ "takeable": false, "hasFingerprint": true, "fingerprintOwner": "director", - "fingerprintQuality": 0.95, + "fingerprintDifficulty": "hard", "observations": "The director's high-security computer. Multiple fingerprints visible on the keyboard." }, { @@ -340,7 +340,7 @@ "takeable": false, "hasFingerprint": true, "fingerprintOwner": "intruder", - "fingerprintQuality": 0.98, + "fingerprintDifficulty": "medium", "observations": "The main server terminal controlling access to research data. There are clear fingerprints on the screen." }, { diff --git a/index.html b/index.html index 9d2ae97..688905d 100644 --- a/index.html +++ b/index.html @@ -3334,8 +3334,8 @@ // Get the required match threshold from the object or use default const requiredThreshold = typeof lockable.biometricMatchThreshold === 'number' ? lockable.biometricMatchThreshold : 0.4; - - debugLog('BIOMETRIC THRESHOLD', requiredThreshold, 2); + // this needs to be changed to use the scenario, bugged. + debugLog('BIOMETRIC THRESHOLD', requiredThreshold, "this needs to be changed to use the scenario, bugged.",2); // Find the fingerprint sample for the required person const fingerprintSample = biometricSamples.find(sample => From 3c3a99c36ad27bf3d8ee6eb7f0be6b4f43522a0e Mon Sep 17 00:00:00 2001 From: Damian-I Date: Thu, 13 Mar 2025 01:20:40 +0000 Subject: [PATCH 09/24] updated default scenario --- assets/scenarios/ceo_exfil.json | 2 +- index.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/assets/scenarios/ceo_exfil.json b/assets/scenarios/ceo_exfil.json index 709730d..af9b60f 100644 --- a/assets/scenarios/ceo_exfil.json +++ b/assets/scenarios/ceo_exfil.json @@ -78,7 +78,7 @@ "requires": "password", "hasFingerprint": true, "fingerprintOwner": "ceo", - "fingerprintQuality": 0.9, + "fingerprintDifficulty": "medium", "observations": "A computer with a cybersecurity alert on screen. There might be fingerprints on the keyboard." }, { diff --git a/index.html b/index.html index 688905d..b04c5f1 100644 --- a/index.html +++ b/index.html @@ -1533,8 +1533,8 @@ this.load.image('fingerprint_kit', 'assets/objects/fingerprint_kit.png'); this.load.image('lockpick', 'assets/objects/lockpick.png'); - this.load.json('gameScenarioJSON', 'assets/scenarios/biometric_breach.json'); - //this.load.json('gameScenarioJSON', 'assets/scenarios/ceo_exfil.json'); + //this.load.json('gameScenarioJSON', 'assets/scenarios/biometric_breach.json'); + this.load.json('gameScenarioJSON', 'assets/scenarios/ceo_exfil.json'); gameScenario = this.cache.json.get('gameScenarioJSON'); } From 7eeda201d9ae4f5e989b203183b017756b94520b Mon Sep 17 00:00:00 2001 From: Damian-I Date: Thu, 13 Mar 2025 01:45:05 +0000 Subject: [PATCH 10/24] removed bluetooth pairing --- assets/rooms/room_reception.json | 13 +- assets/scenarios/ceo_exfil.json | 8 - index.html | 527 +------------------------------ 3 files changed, 3 insertions(+), 545 deletions(-) diff --git a/assets/rooms/room_reception.json b/assets/rooms/room_reception.json index 265cd2b..32f41d0 100644 --- a/assets/rooms/room_reception.json +++ b/assets/rooms/room_reception.json @@ -244,17 +244,6 @@ "width":48, "x":380, "y":166 - }, - { - "height":48, - "id":13, - "name":"bluetooth_spoofer", - "rotation":0, - "type":"", - "visible":true, - "width":48, - "x":320, - "y":166 } ], "opacity":1, @@ -264,7 +253,7 @@ "y":0 }], "nextlayerid":12, - "nextobjectid":14, + "nextobjectid":13, "orientation":"orthogonal", "renderorder":"right-down", "tiledversion":"1.11.0", diff --git a/assets/scenarios/ceo_exfil.json b/assets/scenarios/ceo_exfil.json index af9b60f..ea4b4ba 100644 --- a/assets/scenarios/ceo_exfil.json +++ b/assets/scenarios/ceo_exfil.json @@ -53,14 +53,6 @@ "takeable": true, "inInventory": true, "observations": "A powerful workstation for cryptographic analysis" - }, - { - "type": "bluetooth_spoofer", - "name": "Bluetooth Spoofer", - "takeable": true, - "observations": "A specialized device that can mimic Bluetooth signals from other devices", - "canSpoofBluetooth": true, - "mac": "00:11:22:33:44:55" } ] }, diff --git a/index.html b/index.html index b04c5f1..c7dfd2c 100644 --- a/index.html +++ b/index.html @@ -623,32 +623,6 @@ display: none; } - /* Bluetooth Pairing Button */ - .bluetooth-pair-button { - display: inline-block; - margin-top: 8px; - padding: 5px 10px; - background-color: #9b59b6; - color: white; - border: none; - border-radius: 3px; - font-size: 12px; - cursor: pointer; - transition: background-color 0.2s; - } - - .bluetooth-pair-button:hover { - background-color: #8e44ad; - } - - .bluetooth-pair-button.paired { - background-color: #27ae60; - } - - .bluetooth-pair-button.paired:hover { - background-color: #219653; - } - /* Bluetooth Unlock Button */ .bluetooth-unlock-button { display: inline-block; @@ -676,7 +650,7 @@ .bluetooth-device:hover .bluetooth-device-name, .bluetooth-device:hover .bluetooth-device-details, .bluetooth-device:hover .bluetooth-device-timestamp, - .bluetooth-device:hover .bluetooth-pair-button { + .bluetooth-device:hover { pointer-events: auto; } @@ -1528,7 +1502,6 @@ 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('bluetooth_spoofer', 'assets/objects/bluetooth_spoofer.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'); @@ -3401,14 +3374,6 @@ case 'bluetooth': if (lockable.scenarioData?.locked) { - // Try to spoof the Bluetooth device - const spoofResult = spoofBluetoothDevice(lockable); - - if (spoofResult) { - // If spoofing was successful, unlock the target - unlockTarget(lockable, type, lockable.layer); - } - // Allow the item to be picked up even if locked if (type === 'item' && lockable.scenarioData?.takeable) { // Check if the item is already in the inventory before adding it @@ -3746,102 +3711,6 @@ } } - function spoofBluetoothDevice(target) { - // Find spoofer in inventory - const spoofer = inventory.items.find(item => - item.scenarioData?.type === "bluetooth_spoofer" - ); - - if (!spoofer) { - gameAlert("You need a Bluetooth spoofer to unlock this device.", 'warning', 'Bluetooth Spoofer Required', 4000); - return false; - } - - // Check if target is in inventory or in the environment - const isInventoryItem = inventory.items.includes(target); - - // If it's an environment object, check distance - if (!isInventoryItem) { - // Calculate distance between player and target device - const distance = Phaser.Math.Distance.Between( - player.x, player.y, - target.x, target.y - ); - - debugLog('BLUETOOTH SPOOF ATTEMPT', { - deviceName: target.scenarioData?.name, - deviceMac: target.scenarioData?.mac, - spooferMac: spoofer.scenarioData?.macPaired, - distance: Math.round(distance), - isInventoryItem: false - }, 2); - - // Check if player is within range - if (distance > BLUETOOTH_SCAN_RANGE) { - gameAlert("Too far from device to establish Bluetooth connection.", 'error', 'Connection Failed', 3000); - return false; - } - } else { - // Log attempt for inventory item - debugLog('BLUETOOTH SPOOF ATTEMPT', { - deviceName: target.scenarioData?.name, - deviceMac: target.scenarioData?.mac, - spooferMac: spoofer.scenarioData?.macPaired, - isInventoryItem: true, - distance: 0 // Inventory items are always at distance 0 - }, 2); - } - - // Normalize MAC addresses for comparison - const targetMac = target.scenarioData?.mac?.toLowerCase() || ""; - const spooferMac = spoofer.scenarioData?.macPaired?.toLowerCase() || ""; - - // Check if the spoofer has the correct MAC address - if (spooferMac && targetMac && spooferMac === targetMac) { - debugLog('BLUETOOTH SPOOF SUCCESS', { - deviceName: target.scenarioData?.name, - mac: target.scenarioData?.mac, - isInventoryItem: isInventoryItem - }, 1); - - // Unlock the device - target.scenarioData.locked = false; - - // If it's an inventory item, update its appearance if needed - if (isInventoryItem) { - // Update the item's texture if it has an unlocked version - if (target.scenarioData?.unlockedTexture) { - target.setTexture(target.scenarioData.unlockedTexture); - } - - // If the item has contents, allow them to be collected - if (target.scenarioData?.contents) { - target.scenarioData.isUnlockedButNotCollected = true; - collectContainerContents(target); - - // Remove the device from the bluetoothDevices array - const deviceIndex = bluetoothDevices.findIndex(device => - device.mac.toLowerCase() === targetMac && device.inInventory - ); - - if (deviceIndex !== -1) { - bluetoothDevices.splice(deviceIndex, 1); - } - } - } - - gameAlert("Bluetooth connection spoofed. Device unlocked.", 'success', 'Spoofing Successful', 4000); - - // Update the Bluetooth panel to reflect the unlocked state - updateBluetoothPanel(); - - return true; - } else { - gameAlert("Bluetooth spoofer MAC address doesn't match the target device.", 'error', 'Spoofing Failed', 4000); - return false; - } - } - // Add helper function to generate fingerprint data function generateFingerprintData(item) { // For original samples from items, we use the item's data @@ -6047,34 +5916,7 @@ deviceContent += `
`; deviceContent += `
MAC: ${device.mac}\n${device.details}
`; - // Add pairing button only if device is nearby and player has a Bluetooth spoofer - if (device.nearby) { - // Check if player has a Bluetooth spoofer in inventory - const spoofer = inventory.items.find(item => - item.scenarioData?.type === "bluetooth_spoofer" - ); - - if (spoofer) { - // Check if this device is already paired with (MAC address programmed to spoofer) - const isPaired = spoofer.scenarioData?.macPaired === device.mac; - const buttonClass = isPaired ? 'bluetooth-pair-button paired' : 'bluetooth-pair-button'; - const buttonText = isPaired ? 'MAC Address Paired' : 'Pair MAC Address'; - - deviceContent += ``; - - // If device is paired, add a hint about using it to unlock - if (isPaired) { - deviceContent += `
- You can now use this MAC address to unlock matching Bluetooth locks -
`; - - // If this is an inventory item, add an unlock button - if (device.inInventory) { - deviceContent += ``; - } - } - } - } + deviceContent += `
Last seen: ${formattedDate} ${formattedTime}
`; @@ -6082,11 +5924,6 @@ // Toggle expanded state when clicked deviceElement.addEventListener('click', (event) => { - // Don't toggle if clicking on the pair button - if (event.target.classList.contains('bluetooth-pair-button') || - event.target.closest('.bluetooth-pair-button')) { - return; - } deviceElement.classList.toggle('expanded'); @@ -6146,49 +5983,6 @@ updateBluetoothPanel(); }); }); - - // Set up global event delegation for the pairing buttons - document.addEventListener('click', function(event) { - if (event.target.classList.contains('bluetooth-pair-button')) { - const mac = event.target.dataset.mac; - console.log('Attempting to pair with device MAC:', mac); - - // Find the device in our list - const device = bluetoothDevices.find(device => device.mac === mac); - console.log('Found device:', device); - - if (device) { - attemptPairingWithDevice(mac); - } else { - gameAlert("Device not found in Bluetooth devices list.", 'error', 'Pairing Failed', 3000); - } - - event.stopPropagation(); // Prevent device expanding/collapsing when clicking the button - } - - // Handle unlock button clicks - if (event.target.classList.contains('bluetooth-unlock-button')) { - const mac = event.target.dataset.mac; - console.log('Attempting to unlock device MAC:', mac); - - // Find the inventory item with this MAC address - const item = inventory.items.find(item => - item.scenarioData?.mac === mac && - item.scenarioData?.lockType === "bluetooth" && - item.scenarioData?.locked - ); - console.log('Found inventory item:', item); - - if (item) { - unlockInventoryDeviceByMac(mac); - } else { - gameAlert("Device not found in inventory.", 'error', 'Unlock Failed', 3000); - } - - event.stopPropagation(); // Prevent device expanding/collapsing when clicking the button - } - }); - // Initialize Bluetooth count updateBluetoothCount(); } @@ -6214,323 +6008,6 @@ } console.log('Found inventory item to unlock:', item); - - // Attempt to spoof the device - const success = spoofBluetoothDevice(item); - - // If successful, remove the device from the bluetoothDevices array - if (success) { - // Find the device in the bluetoothDevices array - const deviceIndex = bluetoothDevices.findIndex(device => - device.mac.toLowerCase() === normalizedMac && device.inInventory - ); - - // Remove it if found - if (deviceIndex !== -1) { - console.log('Removing unlocked device from bluetoothDevices array'); - bluetoothDevices.splice(deviceIndex, 1); - - // Update the Bluetooth panel to reflect the changes - updateBluetoothPanel(); - } - } - } - - // Function to handle pairing attempts with Bluetooth devices - function attemptPairingWithDevice(mac) { - console.log('Attempting to pair with MAC:', mac); - console.log('All Bluetooth devices:', bluetoothDevices); - - // Find the device in our list (case-insensitive comparison) - const normalizedMac = mac.toLowerCase(); - const device = bluetoothDevices.find(device => device.mac.toLowerCase() === normalizedMac); - - if (!device) { - console.error('Device not found with MAC:', mac); - gameAlert("Device not found.", 'error', 'Pairing Failed', 3000); - return; - } - - console.log('Found device for pairing:', device); - - // Find spoofer in inventory - const spoofer = inventory.items.find(item => - item.scenarioData?.type === "bluetooth_spoofer" - ); - - if (!spoofer) { - gameAlert("You need a Bluetooth spoofer to pair with this device.", 'warning', 'Spoofer Required', 3000); - return; - } - - // Check if player is close enough to the device (using the proximity from real-time scanning) - // Skip proximity check for inventory items - if (!device.nearby && !device.inInventory) { - gameAlert("You need to be closer to the device to pair with it.", 'warning', 'Too Far', 3000); - return; - } - - // Check if this device is already paired with - if (spoofer.scenarioData?.macPaired && spoofer.scenarioData.macPaired.toLowerCase() === normalizedMac) { - gameAlert(`This device's MAC address (${mac}) is already programmed into your spoofer.`, 'info', 'Already Paired', 3000); - return; - } - - // Launch the MAC address pairing minigame - startBluetoothPairingMinigame(device); - } - - // Bluetooth MAC Address Pairing Minigame - function startBluetoothPairingMinigame(device) { - // Create minigame container - const minigameContainer = document.createElement('div'); - minigameContainer.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; - display: flex; - flex-direction: column; - align-items: center; - `; - - // Add instructions - const instructions = document.createElement('div'); - instructions.innerHTML = ` -

MAC Address Spoofing

-

- Align the signal frequencies to match the target device's MAC address pattern.
- Drag the sliders to adjust each frequency band until all segments turn green.
- When all segments are aligned, the MAC address will be successfully paired. -

- `; - instructions.style.cssText = ` - position: absolute; - top: 10px; - left: 50%; - transform: translateX(-50%); - width: 90%; - `; - - // Create device info display - const deviceInfo = document.createElement('div'); - deviceInfo.innerHTML = ` -
Target Device: ${device.name}
-
MAC Address: ${device.mac}
- `; - deviceInfo.style.cssText = ` - margin-top: 70px; - text-align: center; - width: 100%; - `; - - // Create signal visualization - const signalVisualization = document.createElement('div'); - signalVisualization.style.cssText = ` - width: 90%; - height: 120px; - background: #111; - border: 1px solid #333; - margin: 20px 0; - position: relative; - overflow: hidden; - `; - - // Create frequency bands (6 for MAC address octets) - const frequencyBands = document.createElement('div'); - frequencyBands.style.cssText = ` - display: flex; - width: 90%; - justify-content: space-between; - margin-bottom: 30px; - `; - - // Create sliders for each frequency band - const sliders = []; - const targetValues = []; - const currentValues = []; - const segments = []; - - // Parse MAC address to generate target values - const macParts = device.mac.split(':'); - - for (let i = 0; i < 6; i++) { - // Convert hex to decimal for target value (0-100 range) - const hexValue = macParts[i] || '00'; - const decimalValue = parseInt(hexValue, 16); - const targetValue = Math.round((decimalValue / 255) * 100); - targetValues.push(targetValue); - - // Start with random values - const initialValue = Math.floor(Math.random() * 100); - currentValues.push(initialValue); - - // Create slider container - const sliderContainer = document.createElement('div'); - sliderContainer.style.cssText = ` - display: flex; - flex-direction: column; - align-items: center; - width: 40px; - `; - - // Create slider - const slider = document.createElement('input'); - slider.type = 'range'; - slider.min = 0; - slider.max = 100; - slider.value = initialValue; - slider.style.cssText = ` - width: 120px; - transform: rotate(-90deg); - margin: 50px 0; - `; - - // Create segment in visualization - const segment = document.createElement('div'); - segment.style.cssText = ` - position: absolute; - bottom: 0; - left: ${i * (100/6)}%; - width: ${100/6}%; - height: ${initialValue}%; - background: #cc5500; - transition: height 0.2s, background-color 0.3s; - `; - segments.push(segment); - signalVisualization.appendChild(segment); - - // Create label - const label = document.createElement('div'); - label.textContent = hexValue.toUpperCase(); - label.style.cssText = ` - color: #aaa; - font-size: 12px; - margin-top: 10px; - `; - - // Add event listener to slider - slider.addEventListener('input', (e) => { - const value = parseInt(e.target.value); - currentValues[i] = value; - segment.style.height = `${value}%`; - - // Check if this segment is aligned correctly - const isAligned = Math.abs(value - targetValues[i]) <= 5; - segment.style.backgroundColor = isAligned ? '#00cc00' : '#cc5500'; - - // Check if all segments are aligned - checkCompletion(); - }); - - sliders.push(slider); - sliderContainer.appendChild(slider); - sliderContainer.appendChild(label); - frequencyBands.appendChild(sliderContainer); - } - - // Create status message - const statusMessage = document.createElement('div'); - statusMessage.style.cssText = ` - color: #aaa; - font-size: 14px; - margin-top: 15px; - text-align: center; - `; - statusMessage.textContent = 'Align all frequency bands...'; - - // Create cancel button - const cancelButton = document.createElement('button'); - cancelButton.textContent = 'Cancel'; - cancelButton.style.cssText = ` - background-color: #555; - color: white; - border: none; - border-radius: 3px; - padding: 8px 15px; - margin-top: 20px; - cursor: pointer; - font-size: 14px; - `; - cancelButton.addEventListener('click', () => { - endMinigame(false); - }); - - // Add all elements to container - minigameContainer.appendChild(instructions); - minigameContainer.appendChild(deviceInfo); - minigameContainer.appendChild(signalVisualization); - minigameContainer.appendChild(frequencyBands); - minigameContainer.appendChild(statusMessage); - minigameContainer.appendChild(cancelButton); - - // Add container to document - document.body.appendChild(minigameContainer); - - // Function to check if all segments are aligned - function checkCompletion() { - const allAligned = currentValues.every((value, index) => - Math.abs(value - targetValues[index]) <= 5 - ); - - if (allAligned) { - statusMessage.textContent = 'MAC Address pattern matched!'; - statusMessage.style.color = '#00cc00'; - - // Add success animation - segments.forEach(segment => { - segment.style.backgroundColor = '#00cc00'; - }); - - // End minigame with success after a short delay - setTimeout(() => { - endMinigame(true); - }, 1500); - } - } - - // Function to end minigame - function endMinigame(success) { - // Remove minigame container - document.body.removeChild(minigameContainer); - - // Call callback with result - if (success) { - // Find spoofer in inventory - const spoofer = inventory.items.find(item => - item.scenarioData?.type === "bluetooth_spoofer" - ); - - if (spoofer) { - console.log('Programming spoofer with MAC address:', device.mac); - - // Program the spoofer with the target MAC address - spoofer.scenarioData.macPaired = device.mac; - - // Show success message - gameAlert(`Successfully programmed spoofer with MAC address: ${device.mac}`, 'success', 'Pairing Complete', 4000); - debugLog('BLUETOOTH SPOOFER PROGRAMMED', { - deviceName: device.name, - deviceMac: device.mac - }, 1); - - // Update the UI to show the pairing was successful - device.paired = true; - - // Update the Bluetooth panel to reflect the pairing - updateBluetoothPanel(); - } - } else { - gameAlert('Pairing canceled.', 'info', 'Pairing Canceled', 3000); - } - } } // Biometrics Panel System From 0db7a7e4a6bb399b8d6678555196f06c037645e9 Mon Sep 17 00:00:00 2001 From: Damian-I Date: Thu, 13 Mar 2025 02:20:48 +0000 Subject: [PATCH 11/24] bluetooth remade for "carry unlock" --- assets/scenarios/ceo_exfil.json | 3 ++- index.html | 42 +++++++++++++++++++++++++++++---- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/assets/scenarios/ceo_exfil.json b/assets/scenarios/ceo_exfil.json index ea4b4ba..8048d26 100644 --- a/assets/scenarios/ceo_exfil.json +++ b/assets/scenarios/ceo_exfil.json @@ -131,7 +131,8 @@ "type": "pc", "name": "IT Staff Computer", "takeable": false, - "requires": "password", + "requires": "bluetooth", + "mac": "00:11:22:33:44:55", "observations": "An IT staff computer showing network security logs" }, { diff --git a/index.html b/index.html index c7dfd2c..1c77b10 100644 --- a/index.html +++ b/index.html @@ -3372,9 +3372,12 @@ } break; - case 'bluetooth': + case 'bluetooth': if (lockable.scenarioData?.locked) { - // Allow the item to be picked up even if locked + // Check if we have a matching Bluetooth device in the current room + const matchFound = findMatchingBluetoothDevice(lockable); + + // Allow the item to be picked up regardless of match if it's takeable if (type === 'item' && lockable.scenarioData?.takeable) { // Check if the item is already in the inventory before adding it const isAlreadyInInventory = inventory.items.some(item => item.name === lockable.name); @@ -3386,11 +3389,27 @@ delete rooms[currentRoom].objects[lockable.name]; } } + + // Only unlock if there's a matching device + if (matchFound) { + unlockTarget(lockable, type, lockable.layer); + gameAlert(`Bluetooth connection established with ${matchFound.name}`, 'success', 'Bluetooth Connected', 3000); + } else { + gameAlert('Item added to inventory but still locked - no matching Bluetooth device in range', 'warning', 'Partial Success', 3000); + return; + } + } else if (matchFound) { + // For non-takeable items, only proceed if there's a match + unlockTarget(lockable, type, lockable.layer); + gameAlert(`Bluetooth connection established with ${matchFound.name}`, 'success', 'Bluetooth Connected', 3000); + } else { + gameAlert('No matching Bluetooth device in range', 'error', 'Connection Failed', 3000); + return; } - return; } + console.log('Bluetooth processing complete'); break; - + default: gameAlert(`Requires: ${lockRequirements.requires}`, 'warning', 'Locked', 4000); } @@ -6368,6 +6387,21 @@ } } + // Function to find a matching Bluetooth device in the current room + function findMatchingBluetoothDevice(device) { + if (!device.scenarioData?.mac || !currentRoom) return null; + + const targetMac = device.scenarioData.mac; + const roomObjects = Object.values(rooms[currentRoom].objects || {}); + + // Find any object in the current room with a matching MAC address + return roomObjects.find(obj => + obj.scenarioData?.mac && + obj.scenarioData.mac === targetMac && + obj.name !== device.name // Ensure it's not the same device + ); + } + \ No newline at end of file From 819a04373688b1c7c18910baf215348586324276 Mon Sep 17 00:00:00 2001 From: Damian-I Date: Thu, 13 Mar 2025 02:35:54 +0000 Subject: [PATCH 12/24] bluetooth unlocks with pc in range, both appear on scanner (done??) --- assets/scenarios/ceo_exfil.json | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/scenarios/ceo_exfil.json b/assets/scenarios/ceo_exfil.json index 8048d26..9fa3558 100644 --- a/assets/scenarios/ceo_exfil.json +++ b/assets/scenarios/ceo_exfil.json @@ -132,6 +132,7 @@ "name": "IT Staff Computer", "takeable": false, "requires": "bluetooth", + "lockType": "bluetooth", "mac": "00:11:22:33:44:55", "observations": "An IT staff computer showing network security logs" }, From 461104c87d757738129fe621858c1f436eda35d9 Mon Sep 17 00:00:00 2001 From: Damian-I Date: Thu, 13 Mar 2025 02:53:49 +0000 Subject: [PATCH 13/24] Added option to remove timer on game alerts --- index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.html b/index.html index 1c77b10..3cadd0e 100644 --- a/index.html +++ b/index.html @@ -1008,7 +1008,7 @@ let unreadNotes = 0; // Show a notification instead of using alert() - function showNotification(message, type = 'info', title = '', duration = 5000) { + function showNotification(message, type = 'info', title = '', duration = 5000, position = 'corner') { const notificationContainer = document.getElementById('notification-container'); // Create notification element @@ -1711,7 +1711,7 @@ addNote("Mission Brief", gameScenario.scenario_brief, true); // Show notification - gameAlert(gameScenario.scenario_brief, 'info', 'Mission Brief', 8000); + gameAlert(gameScenario.scenario_brief, 'info', 'Mission Brief', 0); } // initializes the rooms From d6916ff41341de96d7cfe65245520b25afc357c7 Mon Sep 17 00:00:00 2001 From: Damian-I Date: Thu, 13 Mar 2025 03:11:05 +0000 Subject: [PATCH 14/24] removed useless code --- index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.html b/index.html index 3cadd0e..12104e8 100644 --- a/index.html +++ b/index.html @@ -1008,7 +1008,7 @@ let unreadNotes = 0; // Show a notification instead of using alert() - function showNotification(message, type = 'info', title = '', duration = 5000, position = 'corner') { + function showNotification(message, type = 'info', title = '', duration = 5000) { const notificationContainer = document.getElementById('notification-container'); // Create notification element From d63c828504aec1cda921c432299a9b5ae398711c Mon Sep 17 00:00:00 2001 From: Damian-I Date: Thu, 13 Mar 2025 03:43:33 +0000 Subject: [PATCH 15/24] reimplemented lockpicking to be more realistic (WIP) --- index.html | 1510 ++++++++++++++++++++++++---------------------------- 1 file changed, 687 insertions(+), 823 deletions(-) diff --git a/index.html b/index.html index 12104e8..bb6ee55 100644 --- a/index.html +++ b/index.html @@ -4882,849 +4882,713 @@ scene.input.mouse.enabled = false; } - function startLockpickingMinigame(lockable, currentScene, difficulty) { - // Create iframe container - const iframe = document.createElement('div'); - iframe.style.cssText = ` - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 60%; - height: 60%; - background: rgba(0, 0, 0, 0.9); - border: 1px solid #444; - z-index: 1000; - padding: 20px; - border-radius: 5px; - `; + // Minigame Framework + const MinigameFramework = { + activeMinigame: null, + mainGameScene: null, - // Add instructions - const instructions = document.createElement('div'); - instructions.innerHTML = ` -

Lock Picking

-

- Use spacebar or click to toggle tension levels. Each pin requires the right amount of tension.
- 🔵 Blue = Pin moving
- 🟢 Green = Pin set correctly
- 🔴 Red = Over-pushed (reset)
- Set all pins in the correct order with the right tension without resetting. -

- `; - instructions.style.cssText = ` - position: absolute; - top: 10px; - left: 50%; - transform: translateX(-50%); - width: 90%; - `; - - // Create game container - const gameContainer = document.createElement('div'); - gameContainer.style.cssText = ` - width: 100%; - height: calc(100% - 60px); - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - gap: 20px; - margin-top: 40px; - background: #222; - padding: 10px; - `; - - // Add difficulty selection - const difficultySelect = document.createElement('select'); - difficultySelect.style.cssText = ` - margin-bottom: 10px; - padding: 5px; - background: #444; - color: white; - border: 1px solid #666; - border-radius: 3px; - `; - const difficulties = ['Easy - Pins Visible', 'Hard - Audio Only']; - difficulties.forEach(diff => { - const option = document.createElement('option'); - option.value = diff; - option.textContent = diff; - difficultySelect.appendChild(option); - }); - - // Add audio feedback - const lockSounds = { - click: null, // Basic pin movement sound - binding: null, // Sound when a pin is binding (correct tension) - set: null, // Sound when a pin is successfully set - reset: null, // Sound when pins are reset - wrong: null, // Sound when wrong tension or wrong pin - tension: null, // Sound when changing tension - success: null, // Sound when successfully picking the lock - overTension: null // Sound when over-tensioning the lock - }; - - const initAudio = () => { - if (!lockSounds.click) { - // Basic click sound (when pressing a pin) - lockSounds.click = new Audio('assets/sounds/lockpick_click.mp3'); - - // Binding sound (when a pin is binding with correct tension) - lockSounds.binding = new Audio('assets/sounds/lockpick_binding.mp3'); - - // Set sound (when a pin is successfully set) - lockSounds.set = new Audio('assets/sounds/lockpick_set.mp3'); - - // Reset sound (when pins are reset) - lockSounds.reset = new Audio('assets/sounds/lockpick_reset.mp3'); - - // Wrong sound (when using wrong tension or pressing wrong pin) - lockSounds.wrong = new Audio('assets/sounds/lockpick_wrong.mp3'); - - // Tension sound (when changing tension levels) - lockSounds.tension = new Audio('assets/sounds/lockpick_tension.mp3'); - - // Success sound (when successfully picking the lock) - lockSounds.success = new Audio('assets/sounds/lockpick_success.mp3'); - - // Over-tension sound (when applying too much tension) - lockSounds.overTension = new Audio('assets/sounds/lockpick_overtension.mp3'); - } - }; - - // Initialize audio on first interaction - gameContainer.addEventListener('mousedown', initAudio, { once: true }); - - // Add pin binding order and game state - const numPins = getPinCountForDifficulty(difficulty); - const bindingOrder = Array.from({length: numPins}, (_, i) => i) - .sort(() => Math.random() - 0.5); - - const gameState = { - tensionLevel: 1, // Start with light tension (1) - pinStates: Array(numPins).fill(0), // 0 = down, 1 = moving, 2 = set - pinPressTime: Array(numPins).fill(0), // Track how long each pin is pressed - currentBindingIndex: 0, - hardMode: false, - maxPressTime: 1000, // Max time to hold a pin (ms) - failCount: 0, - maxFails: 3, - overTensioned: false, - lastPinSetTime: 0, // Track when the last pin was set - isActivelyPickingPin: false, // Track if we're actively working on a pin - tensionRequirements: Array(numPins).fill(0).map(() => { - // Each pin requires a specific tension level (1, 2, or 3) - const randomValue = Math.random(); - if (randomValue < 0.33) return 1; // Light tension - if (randomValue < 0.66) return 2; // Medium tension - return 3; // Heavy tension - }) - }; - - // Create tension wrench toggle - const tensionWrench = document.createElement('div'); - tensionWrench.style.cssText = ` - width: 100px; - height: 30px; - background: #666; - border: 2px solid #888; - border-radius: 5px; - cursor: pointer; - margin-bottom: 20px; - text-align: center; - line-height: 30px; - color: white; - transform: rotate(2deg); - `; - tensionWrench.textContent = 'Tension: LIGHT'; - - // Function to reset pins - function resetPins(showVisual = true) { - gameState.pinStates.fill(0); - gameState.pinPressTime.fill(0); - gameState.currentBindingIndex = 0; - gameState.failCount++; - - if (showVisual) { - Array.from(pinsContainer.children).forEach(pin => { - pin.style.background = '#555'; - if (!gameState.hardMode) { - pin.style.transition = 'background-color 0.3s'; - pin.style.background = '#f00'; - setTimeout(() => pin.style.background = '#555', 300); - } - }); - } - - if (gameState.failCount >= gameState.maxFails) { - alert("Lock picking failed! The lock is now jammed."); - // Safely remove iframe if it exists in the document - const existingIframe = document.querySelector('div[style*="z-index: 1000"]'); - if (existingIframe && existingIframe.parentNode) { - existingIframe.parentNode.removeChild(existingIframe); - } - if (currentScene && currentScene.input && currentScene.input.mouse) { - currentScene.input.mouse.enabled = true; - } - } - } - - // Create a single function for toggling tension - function toggleTension() { - // Toggle between 3 tension levels (light -> medium -> heavy -> light) - const previousTensionLevel = gameState.tensionLevel; - gameState.tensionLevel = (gameState.tensionLevel % 3) + 1; + // Initialize the framework + init: function(mainScene) { + this.mainGameScene = mainScene; - // Play tension change sound - if (lockSounds.tension) { - lockSounds.tension.currentTime = 0; - lockSounds.tension.play().catch(e => console.log('Audio play failed:', e)); - } - - updateTensionWrench(); - - // Check if we're over-tensioning - but only if we've started interacting with pins - // AND only if we're actively working on a pin (not just changing tension) - const timeSinceLastPinSet = Date.now() - gameState.lastPinSetTime; - const isActivelyPickingLock = gameState.isActivelyPickingPin; - - if (gameState.tensionLevel === 3 && - gameState.currentBindingIndex > 0 && - timeSinceLastPinSet > 1000 && - isActivelyPickingLock) { + // Create container for all minigames if it doesn't exist + if (!document.getElementById('minigame-container')) { + const container = document.createElement('div'); + container.id = 'minigame-container'; + document.body.appendChild(container); - // 30% chance of over-tensioning with heavy pressure - if (Math.random() < 0.3) { - gameState.overTensioned = true; - tensionWrench.style.background = '#ff3333'; - tensionWrench.style.transform = 'rotate(15deg)'; - - // Play over-tension sound - if (lockSounds.overTension) { - lockSounds.overTension.currentTime = 0; - lockSounds.overTension.play().catch(e => console.log('Audio play failed:', e)); - } - - setTimeout(() => { - alert("You applied too much tension and jammed the lock!"); - resetPins(); - }, 500); - } - } - } - - // Add this new function to update tension wrench visuals - function updateTensionWrench() { - // Update tension wrench appearance based on level - switch(gameState.tensionLevel) { - case 1: // Light - tensionWrench.style.background = '#666'; - tensionWrench.style.transform = 'rotate(2deg)'; - tensionWrench.textContent = 'Tension: LIGHT'; - break; - case 2: // Medium - tensionWrench.style.background = '#888'; - tensionWrench.style.transform = 'rotate(5deg)'; - tensionWrench.textContent = 'Tension: MEDIUM'; - break; - case 3: // Heavy - tensionWrench.style.background = '#aaa'; - tensionWrench.style.transform = 'rotate(8deg)'; - tensionWrench.textContent = 'Tension: HEAVY'; - break; - } - } - - // Create pins container - const pinsContainer = document.createElement('div'); - pinsContainer.style.cssText = ` - display: flex; - gap: 10px; - background: #333; - padding: 20px; - border-radius: 10px; - `; - - // Create individual pins - for (let i = 0; i < numPins; i++) { - const pin = document.createElement('div'); - pin.style.cssText = ` - width: 30px; - height: 100px; - background: #555; - border: 2px solid #777; - border-radius: 5px; - cursor: pointer; - position: relative; - transition: transform 0.1s, background-color 0.3s; - `; - - // Add a subtle indicator at the bottom of each pin - const pinIndicator = document.createElement('div'); - pinIndicator.style.cssText = ` - position: absolute; - bottom: 0; - left: 0; - width: 100%; - height: 5px; - background: #555; - transition: background-color 0.3s; - `; - pin.appendChild(pinIndicator); - - const pinNumber = document.createElement('div'); - pinNumber.style.cssText = ` - position: absolute; - top: -20px; - width: 100%; - text-align: center; - color: white; - `; - pinNumber.textContent = (i + 1).toString(); - pin.appendChild(pinNumber); - - // Function to update pin appearance based on its state and binding status - function updatePinAppearance() { - // Reset to default first - pin.style.background = '#555'; - pinIndicator.style.background = '#555'; - pin.style.animation = ''; - pin.style.borderColor = '#777'; - - // If the pin is set, show it as green - if (gameState.pinStates[i] === 2) { - pin.style.background = '#0f0'; - pin.style.cursor = 'default'; - pinIndicator.style.background = '#0f0'; - return; - } - - // Get the current binding pin - const bindingPin = bindingOrder[gameState.currentBindingIndex]; - - // Generate consistent red herrings based on the current binding index - // This ensures the same pins are highlighted as red herrings until the next pin is set - const redHerringSeeds = [ - (bindingPin * 3 + 7) % numPins, - (bindingPin * 5 + 3) % numPins - ]; - - // Filter out the binding pin and already set pins from red herrings - const redHerrings = redHerringSeeds.filter(index => - index !== bindingPin && gameState.pinStates[index] !== 2); - - // If this is the current binding pin, give a subtle hint based on difficulty - if (i === bindingPin) { - // For easy difficulty, make the binding pin more obvious - if (difficulty === 'easy') { - pinIndicator.style.background = '#ff9900'; // Orange indicator for binding pin - - // Also show the required tension level with a color hint - const requiredTension = gameState.tensionRequirements[i]; - if (requiredTension === 1) { - pin.style.borderColor = '#66ccff'; // Light blue for light tension - } else if (requiredTension === 2) { - pin.style.borderColor = '#9966ff'; // Purple for medium tension - } else { - pin.style.borderColor = '#ff6666'; // Red for heavy tension - } - - // Add a subtle animation - pin.style.animation = 'pinWiggle 2s infinite'; - } - // For medium difficulty, just show which pin is binding with less obvious cues - else if (difficulty === 'medium') { - pinIndicator.style.background = '#ff9900'; // Orange indicator for binding pin - pin.style.animation = 'pinWiggle 3s infinite'; - } - // For hard difficulty, very subtle indication - else if (difficulty === 'hard') { - pin.style.animation = 'pinWiggle 4s infinite 0.5s'; - } - } - // If this is a red herring, give misleading feedback - else if (redHerrings.includes(i) && gameState.currentBindingIndex > 0 && difficulty !== 'easy') { - // The amount of misleading feedback depends on difficulty - if (difficulty === 'medium') { - // For medium, make red herrings somewhat convincing - pinIndicator.style.background = '#ff9900'; // Same color as real binding pin - pin.style.animation = 'pinWiggle 3.5s infinite 0.7s'; // Similar wiggle to real pin - - // Randomly assign fake tension indicators to confuse - const fakeTension = Math.floor(Math.random() * 3) + 1; - if (fakeTension === 1) { - pin.style.borderColor = '#66ccff'; - } else if (fakeTension === 2) { - pin.style.borderColor = '#9966ff'; - } else { - pin.style.borderColor = '#ff6666'; - } - } - else if (difficulty === 'hard') { - // For hard, make red herrings very convincing - pin.style.animation = 'pinWiggle 4s infinite 0.3s'; - - // On hard, sometimes make a red herring more convincing than the real pin - if (Math.random() < 0.5) { - pinIndicator.style.background = '#ff9900'; - } - } - } - } - - // Add the wiggle animation to the document - if (!document.getElementById('pinWiggleAnimation')) { + // Add base styles const style = document.createElement('style'); - style.id = 'pinWiggleAnimation'; + style.id = 'minigame-framework-styles'; style.textContent = ` - @keyframes pinWiggle { - 0% { transform: translateY(0); } - 15% { transform: translateY(-2px); } - 30% { transform: translateY(0); } - 45% { transform: translateY(-1px); } - 60% { transform: translateY(0); } - 75% { transform: translateY(-0.5px); } - 100% { transform: translateY(0); } + #minigame-container { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 1000; + display: none; + } + + #minigame-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + z-index: 1001; + } + + .minigame-scene { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: #222; + color: white; + border-radius: 10px; + box-shadow: 0 0 20px rgba(0,0,0,0.5); + z-index: 1002; + display: none; + } + + .minigame-close { + background: #555; + color: white; + border: none; + padding: 10px 20px; + border-radius: 5px; + cursor: pointer; + margin-top: 10px; + align-self: center; } `; document.head.appendChild(style); } - - // Update all pins whenever a pin state changes - function updateAllPins() { - // Get the current binding pin - const bindingPin = bindingOrder[gameState.currentBindingIndex]; - - // Generate consistent red herrings based on the current binding index - const redHerringSeeds = [ - (bindingPin * 3 + 7) % numPins, - (bindingPin * 5 + 3) % numPins - ]; - - // Filter out the binding pin and already set pins from red herrings - const redHerrings = redHerringSeeds.filter(index => - index !== bindingPin && gameState.pinStates[index] !== 2); - - Array.from(pinsContainer.children).forEach((pin, index) => { - // Find the indicator within this pin - const indicator = pin.querySelector('div:first-child'); - - // Reset styles first - pin.style.background = '#555'; - pin.style.animation = ''; - pin.style.borderColor = '#777'; - if (indicator) indicator.style.background = '#555'; - - // Update based on current game state - if (gameState.pinStates[index] === 2) { - pin.style.background = '#0f0'; - pin.style.cursor = 'default'; - if (indicator) indicator.style.background = '#0f0'; - } else { - pin.style.cursor = 'pointer'; - - // Check if this is the binding pin - if (index === bindingPin && !gameState.hardMode) { - if (difficulty === 'easy') { - if (indicator) indicator.style.background = '#ff9900'; - - // Show tension hint - const requiredTension = gameState.tensionRequirements[index]; - if (requiredTension === 1) { - pin.style.borderColor = '#66ccff'; - } else if (requiredTension === 2) { - pin.style.borderColor = '#9966ff'; - } else { - pin.style.borderColor = '#ff6666'; - } - - pin.style.animation = 'pinWiggle 2s infinite'; - } - else if (difficulty === 'medium') { - if (indicator) indicator.style.background = '#ff9900'; - pin.style.animation = 'pinWiggle 3s infinite'; - } - else if (difficulty === 'hard') { - pin.style.animation = 'pinWiggle 4s infinite 0.5s'; - } - } - // Check if this is a red herring, but only for medium and hard difficulties - else if (redHerrings.includes(index) && gameState.currentBindingIndex > 0 && difficulty !== 'easy') { - if (difficulty === 'medium') { - if (indicator) indicator.style.background = '#ff9900'; - pin.style.animation = 'pinWiggle 3.5s infinite 0.7s'; - - const fakeTension = Math.floor(Math.random() * 3) + 1; - if (fakeTension === 1) { - pin.style.borderColor = '#66ccff'; - } else if (fakeTension === 2) { - pin.style.borderColor = '#9966ff'; - } else { - pin.style.borderColor = '#ff6666'; - } - } - else if (difficulty === 'hard') { - pin.style.animation = 'pinWiggle 4s infinite 0.3s'; - if (Math.random() < 0.5 && indicator) { - indicator.style.background = '#ff9900'; - } - } - } - } - }); + }, + + // Launch a minigame + startMinigame: function(minigameId, params = {}) { + // Don't allow multiple minigames at once + if (this.activeMinigame) { + console.warn('A minigame is already running!'); + return false; } - - // Call updatePinAppearance initially and whenever the game state changes - updatePinAppearance(); - - let pressStartTime = 0; - let pressTimer = null; - - function checkPinPress() { - if (pressStartTime === 0) return; - - const pressDuration = Date.now() - pressStartTime; - if (pressDuration > gameState.maxPressTime) { - // Clear the timer first before calling resetPins - clearInterval(pressTimer); - pressTimer = null; - resetPins(); + + // Get the minigame scene constructor + const MinigameScene = this.scenes[minigameId]; + if (!MinigameScene) { + console.error(`Minigame "${minigameId}" not found!`); + return false; + } + + // Pause the main game + if (this.mainGameScene) { + // Pause Phaser scene + if (this.mainGameScene.scene && typeof this.mainGameScene.scene.pause === 'function') { + this.mainGameScene.scene.pause(); } } - - pin.onmousedown = () => { - // First check if this pin is already set - const pinIndex = Array.from(pinsContainer.children).indexOf(pin); - if (gameState.pinStates[pinIndex] === 2) { - // Pin is already set, don't allow interaction - return; + + // Show the minigame container + const container = document.getElementById('minigame-container'); + container.style.display = 'block'; + + // Create overlay + const overlay = document.createElement('div'); + overlay.id = 'minigame-overlay'; + container.appendChild(overlay); + + // Create scene element + const sceneElement = document.createElement('div'); + sceneElement.className = 'minigame-scene'; + sceneElement.id = `minigame-scene-${minigameId}`; + container.appendChild(sceneElement); + + // Instantiate the minigame scene + this.activeMinigame = new MinigameScene(sceneElement, { + ...params, + onComplete: (success, result) => { + this.endMinigame(success, result); + if (params.onComplete) params.onComplete(success, result); } - - // Set the flag to indicate we're actively picking a pin - gameState.isActivelyPickingPin = true; - - // Play basic click sound - if (lockSounds.click) { - lockSounds.click.currentTime = 0; - lockSounds.click.play().catch(e => console.log('Audio play failed:', e)); - } - - pressStartTime = Date.now(); - pressTimer = setInterval(checkPinPress, 100); - - pin.style.transform = 'translateY(-10px)'; - - // Each pin has different tension requirements - const bindingPin = bindingOrder[gameState.currentBindingIndex]; - const requiredTension = gameState.tensionRequirements[pinIndex]; - - // Check if this is the current binding pin - if (pinIndex === bindingPin) { - // This pin needs exactly the right tension level - const correctTension = (gameState.tensionLevel === requiredTension); - - if (correctTension && !gameState.overTensioned) { - // Play binding sound - correct pin with correct tension - if (lockSounds.binding) { - lockSounds.binding.currentTime = 0; - lockSounds.binding.play().catch(e => console.log('Audio play failed:', e)); - } - - if (!gameState.hardMode) { - pin.style.background = '#00f'; - } - - // Start a timer to set the pin - setTimeout(() => { - if (pressStartTime !== 0) { // Still pressing - // Double-check tension is still correct - const stillCorrectTension = (gameState.tensionLevel === requiredTension); - - if (stillCorrectTension && !gameState.overTensioned) { - gameState.pinStates[pinIndex] = 2; - gameState.currentBindingIndex++; - gameState.lastPinSetTime = Date.now(); - - // Play set sound - pin successfully set - if (lockSounds.set) { - lockSounds.set.currentTime = 0; - lockSounds.set.play().catch(e => console.log('Audio play failed:', e)); - } - - if (!gameState.hardMode) { - pin.style.background = '#0f0'; - pinIndicator.style.background = '#0f0'; - } - - // Update all pins to show new binding state - updateAllPins(); - - checkWinCondition(); - } - } - }, 500); - } else if (gameState.tensionLevel > 0) { - // Wrong tension but trying - give feedback - // Play wrong sound - wrong tension on correct pin - if (lockSounds.wrong) { - lockSounds.wrong.currentTime = 0; - lockSounds.wrong.play().catch(e => console.log('Audio play failed:', e)); - } - - if (!gameState.hardMode) { - pin.style.background = '#00f'; - } - - // Start counting towards potential reset - gameState.pinPressTime[pinIndex] = Date.now(); - } - } else if (gameState.tensionLevel > 0 && gameState.pinStates[pinIndex] !== 2) { - // Wrong pin - give feedback - if (lockSounds.wrong) { - lockSounds.wrong.currentTime = 0; - lockSounds.wrong.play().catch(e => console.log('Audio play failed:', e)); - } - - if (!gameState.hardMode) { - pin.style.background = '#00f'; - } - // Start counting towards potential reset - gameState.pinPressTime[pinIndex] = Date.now(); - } - }; - - pin.onmouseup = pin.onmouseleave = () => { - // Clear the flag to indicate we're no longer actively picking a pin - gameState.isActivelyPickingPin = false; - - pressStartTime = 0; - if (pressTimer) { - clearInterval(pressTimer); - pressTimer = null; - } - - pin.style.transform = 'translateY(0)'; - if (gameState.pinStates[i] !== 2) { - pin.style.background = '#555'; - // Update appearance to show binding status - updatePinAppearance(); - } - }; - - pinsContainer.appendChild(pin); - } - - difficultySelect.onchange = () => { - gameState.hardMode = difficultySelect.value.includes('Hard'); - Array.from(pinsContainer.children).forEach(pin => { - pin.style.opacity = gameState.hardMode ? '0.1' : '1'; }); - }; - - // Add components to game container - gameContainer.appendChild(difficultySelect); - gameContainer.appendChild(tensionWrench); - gameContainer.appendChild(pinsContainer); - - // Add close button - const closeButton = document.createElement('button'); - closeButton.textContent = 'X'; - closeButton.style.cssText = ` - position: absolute; - right: 10px; - top: 10px; - background: none; - border: none; - color: white; - font-size: 20px; - cursor: pointer; - `; - closeButton.onclick = () => { - document.body.removeChild(iframe); - if (currentScene && currentScene.input && currentScene.input.mouse) { - currentScene.input.mouse.enabled = true; - } - }; - - // Assemble the interface - iframe.appendChild(closeButton); - iframe.appendChild(instructions); - iframe.appendChild(gameContainer); - document.body.appendChild(iframe); - - // Disable game movement - if (currentScene && currentScene.input && currentScene.input.mouse) { - currentScene.input.mouse.enabled = false; - } - - // Add this function before the pin creation loop - function checkWinCondition() { - if (gameState.currentBindingIndex >= numPins) { - // Play success sound - if (lockSounds.success) { - lockSounds.success.currentTime = 0; - lockSounds.success.play().catch(e => console.log('Audio play failed:', e)); - } - - console.log('Lock picked successfully:', lockable); - - // Create success message - const successMessage = document.createElement('div'); - successMessage.style.cssText = ` - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - background: rgba(0, 0, 0, 0.8); - color: #0f0; - padding: 20px; - border-radius: 10px; - font-size: 24px; - text-align: center; - z-index: 1002; - `; - successMessage.textContent = "Lock successfully picked!"; - iframe.appendChild(successMessage); - - // Disable further interaction - gameContainer.style.pointerEvents = 'none'; - - setTimeout(() => { - // For doors, we need to check properties and handle differently - if (lockable && lockable.properties && lockable.properties.locked) { - console.log('Unlocking door tile:', lockable); - unlockDoor(lockable, lockable.layer); - } - // For containers and other lockable items - else if (lockable && lockable.scenarioData) { - console.log('Unlocking container:', lockable.scenarioData); - lockable.scenarioData.locked = false; - - // Set the flag to indicate the container is unlocked but contents not collected - if (lockable.scenarioData.contents && lockable.scenarioData.contents.length > 0) { - lockable.scenarioData.isUnlockedButNotCollected = true; - debugLog('Container unlocked and ready for collection', lockable.scenarioData, 1); - } - } - - // Remove the minigame - document.body.removeChild(iframe); - if (currentScene && currentScene.input && currentScene.input.mouse) { - currentScene.input.mouse.enabled = true; - } - }, 1500); - - return true; - } - return false; - } - - // Use the toggleTension function for both click and keyboard events - tensionWrench.onclick = toggleTension; - - document.addEventListener('keydown', function(event) { - // Only process if the lockpicking minigame is active - if (!document.querySelector('div[style*="z-index: 1000"]')) return; - if (event.code === 'Space') { - event.preventDefault(); // Prevent page scrolling - toggleTension(); - } - }); - - // Keep only the table debug function - function logTensionDebugInfo() { - // Only show debug info if debug mode is enabled - if (!DEBUG_MODE.enabled) return; - - DEBUG_MODE.log("=== LOCKPICKING DEBUG INFO ==="); - DEBUG_MODE.log("Pin binding order and tension requirements:"); - - const tableData = []; - - for (let orderIndex = 0; orderIndex < numPins; orderIndex++) { - const pinIndex = bindingOrder[orderIndex]; - const requiredTension = gameState.tensionRequirements[pinIndex]; - let tensionNeeded; - - switch(requiredTension) { - case 1: tensionNeeded = 'Light'; break; - case 2: tensionNeeded = 'Medium'; break; - case 3: tensionNeeded = 'Heavy'; break; - default: tensionNeeded = 'Unknown'; - } - - tableData.push({ - 'Binding Order': orderIndex + 1, - 'Pin #': pinIndex + 1, - 'Tension Required': tensionNeeded - }); - } - - console.table(tableData); - } - - // Call this function instead of addTensionDebugDisplay - logTensionDebugInfo(); - } - - // Add this function to get pin count based on difficulty - function getPinCountForDifficulty(difficulty) { - switch(difficulty?.toLowerCase()) { - case 'easy': - return 3; - case 'medium': - return 5; - case 'hard': - return 7; - default: - return 5; // Default to medium difficulty - } - } - - // removes an item from the inventory - function removeFromInventory(sprite) { - if (!sprite || !inventory.items) { - return false; - } - - try { - // Find the index of the sprite in the inventory - const index = inventory.items.indexOf(sprite); - - if (index === -1) { - return false; // Item not found in inventory - } - - // Remove from container - inventory.container.remove(sprite); - - // Remove from items array - inventory.items.splice(index, 1); - - // Destroy the sprite - sprite.destroy(); - - // Rearrange remaining items - rearrangeInventoryItems(); - - // Log the removal - debugLog('INVENTORY ITEM REMOVED', { - name: sprite.name, - totalItems: inventory.items.length - }, 2); + // Initialize and start the minigame + this.activeMinigame.init(); + this.activeMinigame.start(); return true; - } catch (error) { - console.error('Error removing item from inventory:', error); + }, + + // End the active minigame + endMinigame: function(success = false, result = null) { + if (!this.activeMinigame) return; + + // Call the minigame's cleanup method + if (typeof this.activeMinigame.cleanup === 'function') { + this.activeMinigame.cleanup(); + } + + // Remove the minigame elements + const container = document.getElementById('minigame-container'); + container.innerHTML = ''; + container.style.display = 'none'; + + // Unpause the main game + if (this.mainGameScene) { + // Resume Phaser scene + if (this.mainGameScene.scene && typeof this.mainGameScene.scene.resume === 'function') { + this.mainGameScene.scene.resume(); + } + } + + this.activeMinigame = null; + }, + + // Register a new minigame scene + registerScene: function(id, sceneClass) { + if (!this.scenes) this.scenes = {}; + this.scenes[id] = sceneClass; + }, + + // Base class for minigame scenes + MinigameScene: class { + constructor(container, params = {}) { + this.container = container; + this.params = params; + this.isActive = false; + } + + init() { + // Show the scene container + this.container.style.display = 'flex'; + + // Add escape key handler + this.escHandler = (e) => { + if (e.key === 'Escape' && this.isActive) { + this.complete(false); + } + }; + document.addEventListener('keydown', this.escHandler); + } + + start() { + this.isActive = true; + } + + complete(success, result = null) { + this.isActive = false; + document.removeEventListener('keydown', this.escHandler); + if (typeof this.params.onComplete === 'function') { + this.params.onComplete(success, result); + } + } + + cleanup() { + // Cleanup resources + } + } + }; + + // Lockpicking Minigame Scene implementation + class LockpickingMinigame extends MinigameFramework.MinigameScene { + constructor(container, params) { + super(container, params); + + this.lockable = params.lockable; + this.difficulty = params.difficulty || 'medium'; + this.pinCount = this.difficulty === 'easy' ? 3 : this.difficulty === 'medium' ? 4 : 5; + + this.pins = []; + this.gameState = { + tension: 0, + gameActive: false, + pinsSet: 0 + }; + } + + init() { + super.init(); + + // Apply styles + this.container.style.width = '90%'; + this.container.style.maxWidth = '500px'; + this.container.style.padding = '20px'; + this.container.style.display = 'flex'; + this.container.style.flexDirection = 'column'; + this.container.style.gap = '15px'; + + // Add styles if they don't exist + if (!document.getElementById('lockpicking-styles')) { + const style = document.createElement('style'); + style.id = 'lockpicking-styles'; + style.textContent = ` + .lock-visual { + display: flex; + justify-content: center; + align-items: flex-end; + gap: 15px; + height: 120px; + background: #333; + border-radius: 5px; + padding: 15px; + position: relative; + } + + .pin { + width: 40px; + height: 100px; + position: relative; + background: #444; + border-radius: 4px 4px 0 0; + overflow: visible; + cursor: pointer; + transition: transform 0.1s; + } + + .pin:hover { + background: #555; + transform: translateY(-2px); + } + + .pin:active { + transform: translateY(0); + } + + .shear-line { + position: absolute; + width: 100%; + height: 2px; + background: #aaa; + bottom: 40px; + z-index: 5; + } + + .key-pin { + position: absolute; + bottom: 0; + width: 100%; + height: 0px; + background: #dd3333; + transition: height 0.1s; + } + + .driver-pin { + position: absolute; + width: 100%; + height: 40px; + background: #3355dd; + transition: bottom 0.1s; + bottom: 40px; + } + + .spring { + position: absolute; + bottom: 80px; + width: 100%; + height: 20px; + background: linear-gradient(to bottom, + transparent 0%, transparent 20%, + #999 20%, #999 30%, + transparent 30%, transparent 40%, + #999 40%, #999 50%, + transparent 50%, transparent 60%, + #999 60%, #999 70%, + transparent 70%, transparent 80%, + #999 80%, #999 90%, + transparent 90%, transparent 100% + ); + transition: height 0.1s; + } + + .pin.binding { + box-shadow: 0 0 8px 2px #ffcc00; + } + + .pin.set .driver-pin { + bottom: 42px; + } + + .pin.set .key-pin { + height: 39px; + } + + .tension-control { + display: flex; + flex-direction: column; + padding: 10px; + background: #333; + border-radius: 5px; + } + + .tension-control label { + margin-bottom: 5px; + } + + .tension-slider { + margin: 10px 0; + } + + .tension-meter { + width: 100%; + height: 10px; + background: #444; + border-radius: 5px; + overflow: hidden; + } + + .tension-fill { + height: 100%; + width: 0%; + background: linear-gradient(to right, #33cc33, #ffcc00, #cc3333); + transition: width 0.2s; + } + + .lockpick-feedback { + padding: 10px; + background: #333; + border-radius: 5px; + text-align: center; + min-height: 20px; + } + + .lockpick-timer { + padding: 5px 10px; + background: #333; + border-radius: 5px; + text-align: center; + } + + .minigame-scene.success { + border: 2px solid #33cc33; + } + + .minigame-scene.failure { + border: 2px solid #cc3333; + } + `; + document.head.appendChild(style); + } + + // Create header with title and instructions + const header = document.createElement('div'); + header.className = 'lockpick-header'; + header.innerHTML = ` +

Lockpicking

+

Apply tension with the wrench, then click on pins to lift them to the shear line

+ `; + this.container.appendChild(header); + + // Create the lock cylinder visualization + const lockVisual = document.createElement('div'); + lockVisual.className = 'lock-visual'; + this.container.appendChild(lockVisual); + + // Create pins with random binding order + const bindingOrder = this.shuffleArray([...Array(this.pinCount).keys()]); + + for (let i = 0; i < this.pinCount; i++) { + // Create pin container + const pinElement = document.createElement('div'); + pinElement.className = 'pin'; + pinElement.dataset.index = i; + lockVisual.appendChild(pinElement); + + // Create shear line + const shearLine = document.createElement('div'); + shearLine.className = 'shear-line'; + pinElement.appendChild(shearLine); + + // Create key pin (bottom pin) + const keyPin = document.createElement('div'); + keyPin.className = 'key-pin'; + pinElement.appendChild(keyPin); + + // Create driver pin (top pin) + const driverPin = document.createElement('div'); + driverPin.className = 'driver-pin'; + pinElement.appendChild(driverPin); + + // Create spring + const spring = document.createElement('div'); + spring.className = 'spring'; + pinElement.appendChild(spring); + + // Store pin data + const pin = { + index: i, + binding: bindingOrder.indexOf(i), + setPoint: Math.random() * 0.4 + 0.3, // Point between 30-70% where pin sets + currentHeight: 0, + isSet: false, + elements: { + container: pinElement, + keyPin: keyPin, + driverPin: driverPin, + spring: spring + } + }; + + this.pins.push(pin); + + // Add click event to pin + pinElement.addEventListener('click', () => { + if (!this.gameState.gameActive) return; + + // Only allow clicking if we have tension applied + if (this.gameState.tension < 20) { + this.updateFeedback("Apply more tension with the wrench first"); + return; + } + + // Animate the pin moving up + if (!pin.isSet) { + this.animatePinLift(pin); + } + }); + } + + // Add tension wrench control + const tensionControl = document.createElement('div'); + tensionControl.className = 'tension-control'; + tensionControl.innerHTML = ` + + +
+ `; + this.container.appendChild(tensionControl); + + // Feedback area + this.feedback = document.createElement('div'); + this.feedback.className = 'lockpick-feedback'; + this.feedback.textContent = 'Apply tension with the wrench, then click pins to try picking the lock'; + this.container.appendChild(this.feedback); + + // No timer display anymore + + // Tension wrench control event + const tensionSlider = tensionControl.querySelector('.tension-slider'); + const tensionFill = tensionControl.querySelector('.tension-fill'); + + tensionSlider.addEventListener('input', () => { + this.gameState.tension = parseInt(tensionSlider.value); + tensionFill.style.width = `${this.gameState.tension}%`; + + // Update which pins are binding + this.updatePinVisuals(); + + // If tension is suddenly reduced, pins may drop + if (this.gameState.tension < 10 && this.gameState.pinsSet > 0) { + // Drop all set pins + this.pins.forEach(pin => { + if (pin.isSet) { + pin.isSet = false; + pin.currentHeight = 0; + } + }); + this.gameState.pinsSet = 0; + this.updatePinVisuals(); + this.updateFeedback("Tension released - all pins dropped!"); + } + }); + } + + start() { + super.start(); + + // Set game as active + this.gameState.gameActive = true; + + // No timer setup anymore + } + + cleanup() { + // No timer to clear anymore + } + + updatePinVisuals() { + this.pins.forEach(pin => { + // Update key pin and driver pin heights + pin.elements.keyPin.style.height = `${pin.currentHeight * 40}px`; + pin.elements.driverPin.style.bottom = `${pin.currentHeight * 40 + 1}px`; + pin.elements.spring.style.height = `${20 - pin.currentHeight * 5}px`; + + // Show binding state + if (this.shouldPinBind(pin) && !pin.isSet) { + pin.elements.container.classList.add('binding'); + } else { + pin.elements.container.classList.remove('binding'); + } + + // Show set state + if (pin.isSet) { + pin.elements.container.classList.add('set'); + } else { + pin.elements.container.classList.remove('set'); + } + }); + } + + shouldPinBind(pin) { + if (this.gameState.tension < 20) return false; + + // Find the next unset pin in binding order + for (let order = 0; order < this.pinCount; order++) { + const nextPin = this.pins.find(p => p.binding === order && !p.isSet); + if (nextPin) { + return pin.index === nextPin.index; + } + } return false; } + + animatePinLift(pin) { + // Only the binding pin can be set + if (!this.shouldPinBind(pin)) { + this.updateFeedback("This pin isn't binding yet"); + + // Small bounce animation to show it's stuck + let height = 0; + const interval = setInterval(() => { + height += 0.05; + if (height >= 0.2) { + clearInterval(interval); + + // Fall back down + const fallInterval = setInterval(() => { + height -= 0.05; + if (height <= 0) { + height = 0; + clearInterval(fallInterval); + } + pin.currentHeight = height; + this.updatePinVisuals(); + }, 20); + } + pin.currentHeight = height; + this.updatePinVisuals(); + }, 20); + + return; + } + + // Start height at current position + let height = pin.currentHeight; + + // Animate lifting to the potential set point + const liftInterval = setInterval(() => { + height += 0.05; + + // Check if we're at or past the set point + if (Math.abs(height - pin.setPoint) < 0.1) { + // At the correct height - set the pin! + clearInterval(liftInterval); + pin.currentHeight = pin.setPoint; + pin.isSet = true; + this.gameState.pinsSet++; + this.updatePinVisuals(); + this.updateFeedback(`Pin set at the shear line! (${this.gameState.pinsSet}/${this.pinCount})`); + + // Check for win condition + if (this.gameState.pinsSet === this.pinCount) { + this.endGame(true); + } + } + else if (height > pin.setPoint + 0.1) { + // We went too far - overset the pin + clearInterval(liftInterval); + this.updateFeedback("Pin pushed too far - overset!"); + + // Random chance of dropping another pin + if (this.gameState.tension > 60 && Math.random() < 0.3) { + const randomSetPin = this.pins.find(p => p.isSet && Math.random() < 0.5); + if (randomSetPin) { + randomSetPin.isSet = false; + this.gameState.pinsSet--; + this.updateFeedback("A pin dropped! Too much movement"); + this.updatePinVisuals(); + } + } + + // Animate falling back down + const fallInterval = setInterval(() => { + height -= 0.05; + if (height <= 0) { + height = 0; + clearInterval(fallInterval); + } + pin.currentHeight = height; + this.updatePinVisuals(); + }, 20); + } + + // If we reach max height without setting + if (height >= 1) { + clearInterval(liftInterval); + this.updateFeedback("Pin pushed too far!"); + + // Fall back down + const fallInterval = setInterval(() => { + height -= 0.05; + if (height <= 0) { + height = 0; + clearInterval(fallInterval); + } + pin.currentHeight = height; + this.updatePinVisuals(); + }, 20); + } + + pin.currentHeight = height; + this.updatePinVisuals(); + }, 20); + } + + updateFeedback(message) { + this.feedback.textContent = message; + } + + endGame(success) { + this.gameState.gameActive = false; + + if (success) { + this.container.classList.add('success'); + this.updateFeedback("Lock picked successfully!"); + + // Unlock the object in the game + if (typeof unlockTarget === 'function') { + unlockTarget(this.lockable, this.lockable.type || 'object', this.lockable.layer); + } + } else { + this.container.classList.add('failure'); + this.updateFeedback("Failed to pick the lock"); + } + + // Add close button + const closeBtn = document.createElement('button'); + closeBtn.textContent = "Close"; + closeBtn.className = "minigame-close"; + closeBtn.onclick = () => { + this.complete(success, { lockable: this.lockable }); + }; + this.container.appendChild(closeBtn); + } + + shuffleArray(array) { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + return array; + } } - - // Rearrange inventory items after removal - function rearrangeInventoryItems() { - inventory.items.forEach((item, index) => { - item.x = index * 60 + 100; + + // Register the lockpicking minigame with the framework + MinigameFramework.registerScene('lockpicking', LockpickingMinigame); + + // Replacement for the startLockpickingMinigame function + function startLockpickingMinigame(lockable, scene, difficulty = 'medium') { + // Initialize the framework if not already done + if (!MinigameFramework.mainGameScene) { + MinigameFramework.init(scene); + } + + // Start the lockpicking minigame + MinigameFramework.startMinigame('lockpicking', { + lockable: lockable, + difficulty: difficulty, + onComplete: (success, result) => { + if (success) { + debugLog('LOCKPICK SUCCESS', null, 1); + gameAlert(`Successfully picked the lock!`, 'success', 'Lockpicking', 4000); + } else { + debugLog('LOCKPICK FAILED', null, 2); + gameAlert(`Failed to pick the lock.`, 'error', 'Lockpicking', 4000); + } + } }); } From 344d5c41929fb5f55a7ae070eb954b637061ec23 Mon Sep 17 00:00:00 2001 From: Damian-I Date: Thu, 13 Mar 2025 04:34:48 +0000 Subject: [PATCH 16/24] lockpick checkpoint (scenario changed for debugging) --- assets/scenarios/ceo_exfil.json | 5 + index.html | 579 ++++++++++++++++++++------------ 2 files changed, 370 insertions(+), 214 deletions(-) diff --git a/assets/scenarios/ceo_exfil.json b/assets/scenarios/ceo_exfil.json index 9fa3558..2e8da9d 100644 --- a/assets/scenarios/ceo_exfil.json +++ b/assets/scenarios/ceo_exfil.json @@ -4,6 +4,10 @@ "rooms": { "reception": { "type": "room_reception", + "locked": true, + "lockType": "key", + "requires": "ceo_office_key", + "difficulty": "easy", "connections": { "north": "office1" }, @@ -148,6 +152,7 @@ "type": "lockpick", "name": "Lock Pick Kit", "takeable": true, + "inInventory": true, "observations": "A professional lock picking kit with various picks and tension wrenches" } ] diff --git a/index.html b/index.html index bb6ee55..fe5fe47 100644 --- a/index.html +++ b/index.html @@ -882,6 +882,60 @@ align-items: center; font-weight: bold; } + + .tension-control { + display: flex; + align-items: center; + background: #333; + padding: 10px 15px; + border-radius: 5px; + font-size: 14px; + gap: 15px; + } + + .tension-status { + font-size: 13px; + color: #ddd; + } + + .tension-wrench { + position: relative; + height: 25px; + width: 60px; + cursor: pointer; + } + + .wrench-handle { + position: absolute; + top: 10px; + right: 0; + width: 40px; + height: 5px; + background: #aaa; + border-radius: 2px; + transform-origin: right center; + transition: transform 0.3s; + } + + .wrench-tip { + position: absolute; + right: 0; + top: 5px; + width: 10px; + height: 15px; + background: #888; + border-radius: 2px; + } + + .tension-wrench.active .wrench-handle { + transform: rotate(-20deg); + background: #2196F3; + } + + .cylinder { + height: 20px; + margin-top: -5px; + } @@ -4902,12 +4956,12 @@ style.id = 'minigame-framework-styles'; style.textContent = ` #minigame-container { - position: fixed; + position: fixed; top: 0; left: 0; width: 100%; height: 100%; - z-index: 1000; + z-index: 1000; display: none; } @@ -4915,7 +4969,7 @@ position: fixed; top: 0; left: 0; - width: 100%; + width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); z-index: 1001; @@ -4926,7 +4980,7 @@ top: 50%; left: 50%; transform: translate(-50%, -50%); - background: #222; + background: #222; color: white; border-radius: 10px; box-shadow: 0 0 20px rgba(0,0,0,0.5); @@ -4936,7 +4990,7 @@ .minigame-close { background: #555; - color: white; + color: white; border: none; padding: 10px 20px; border-radius: 5px; @@ -5084,9 +5138,11 @@ this.pins = []; this.gameState = { - tension: 0, + tensionApplied: false, gameActive: false, - pinsSet: 0 + pinsSet: 0, + currentPin: null, + mouseDown: false }; } @@ -5107,12 +5163,12 @@ style.id = 'lockpicking-styles'; style.textContent = ` .lock-visual { - display: flex; + display: flex; justify-content: center; align-items: flex-end; gap: 15px; height: 120px; - background: #333; + background: #333; border-radius: 5px; padding: 15px; position: relative; @@ -5120,8 +5176,8 @@ .pin { width: 40px; - height: 100px; - position: relative; + height: 100px; + position: relative; background: #444; border-radius: 4px 4px 0 0; overflow: visible; @@ -5131,11 +5187,6 @@ .pin:hover { background: #555; - transform: translateY(-2px); - } - - .pin:active { - transform: translateY(0); } .shear-line { @@ -5148,21 +5199,22 @@ } .key-pin { - position: absolute; - bottom: 0; - width: 100%; + position: absolute; + bottom: 0; + width: 100%; height: 0px; background: #dd3333; - transition: height 0.1s; + transition: height 0.05s; } .driver-pin { - position: absolute; - width: 100%; + position: absolute; + width: 100%; height: 40px; background: #3355dd; - transition: bottom 0.1s; + transition: bottom 0.05s; bottom: 40px; + border-radius: 0 0 3px 3px; } .spring { @@ -5181,7 +5233,7 @@ #999 80%, #999 90%, transparent 90%, transparent 100% ); - transition: height 0.1s; + transition: height 0.05s; } .pin.binding { @@ -5189,42 +5241,92 @@ } .pin.set .driver-pin { - bottom: 42px; + bottom: 42px; /* Just above shear line */ + background: #22aa22; /* Green to indicate set */ } .pin.set .key-pin { - height: 39px; + height: 39px; /* Just below shear line */ } - .tension-control { - display: flex; - flex-direction: column; - padding: 10px; - background: #333; - border-radius: 5px; + .tension-toggle { + display: inline-block; + position: relative; + width: 60px; + height: 34px; + margin: 10px; } - .tension-control label { - margin-bottom: 5px; + .tension-toggle input { + opacity: 0; + width: 0; + height: 0; } .tension-slider { - margin: 10px 0; + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + -webkit-transition: .4s; + transition: .4s; + border-radius: 34px; } - .tension-meter { + .tension-slider:before { + position: absolute; + content: ""; + height: 26px; + width: 26px; + left: 4px; + bottom: 4px; + background-color: white; + -webkit-transition: .4s; + transition: .4s; + border-radius: 50%; + } + + input:checked + .tension-slider { + background-color: #2196F3; + } + + input:focus + .tension-slider { + box-shadow: 0 0 1px #2196F3; + } + + input:checked + .tension-slider:before { + -webkit-transform: translateX(26px); + -ms-transform: translateX(26px); + transform: translateX(26px); + } + + .cylinder { + display: flex; + justify-content: center; + align-items: center; width: 100%; - height: 10px; - background: #444; + height: 30px; + background: #222; border-radius: 5px; - overflow: hidden; + margin-top: -15px; + position: relative; + z-index: 0; } - .tension-fill { - height: 100%; - width: 0%; - background: linear-gradient(to right, #33cc33, #ffcc00, #cc3333); - transition: width 0.2s; + .cylinder-inner { + width: 80%; + height: 20px; + background: #333; + border-radius: 3px; + transform-origin: center; + transition: transform 0.3s; + } + + .cylinder.rotated .cylinder-inner { + transform: rotate(15deg); } .lockpick-feedback { @@ -5235,11 +5337,16 @@ min-height: 20px; } - .lockpick-timer { - padding: 5px 10px; + .tension-control { + display: flex; + align-items: center; background: #333; + padding: 10px; border-radius: 5px; - text-align: center; + } + + .tension-control label { + flex: 1; } .minigame-scene.success { @@ -5252,13 +5359,13 @@ `; document.head.appendChild(style); } - + // Create header with title and instructions const header = document.createElement('div'); header.className = 'lockpick-header'; header.innerHTML = `

Lockpicking

-

Apply tension with the wrench, then click on pins to lift them to the shear line

+

Apply tension and hold click on pins to lift them to the shear line

`; this.container.appendChild(header); @@ -5267,6 +5374,12 @@ lockVisual.className = 'lock-visual'; this.container.appendChild(lockVisual); + // Create the cylinder that will rotate when tension is applied + const cylinder = document.createElement('div'); + cylinder.className = 'cylinder'; + cylinder.innerHTML = '
'; + this.container.appendChild(cylinder); + // Create pins with random binding order const bindingOrder = this.shuffleArray([...Array(this.pinCount).keys()]); @@ -5314,106 +5427,224 @@ this.pins.push(pin); - // Add click event to pin - pinElement.addEventListener('click', () => { - if (!this.gameState.gameActive) return; + // Mouse down event - start lifting pin + pinElement.addEventListener('mousedown', (e) => { + if (!this.gameState.gameActive || pin.isSet) return; - // Only allow clicking if we have tension applied - if (this.gameState.tension < 20) { - this.updateFeedback("Apply more tension with the wrench first"); - return; - } + // Only proceed if tension is applied + if (!this.gameState.tensionApplied) { + this.updateFeedback("Apply tension first by toggling the wrench"); + return; + } + + // Start lifting the pin + this.gameState.mouseDown = true; + this.gameState.currentPin = pin; + this.liftPin(); - // Animate the pin moving up - if (!pin.isSet) { - this.animatePinLift(pin); - } + // Prevent text selection + e.preventDefault(); }); } - // Add tension wrench control + // Mouse up event - stop lifting pin + document.addEventListener('mouseup', () => { + this.gameState.mouseDown = false; + + // If we were in the process of lifting a pin, check if it sets or drops + if (this.gameState.currentPin) { + this.checkPinSet(this.gameState.currentPin); + this.gameState.currentPin = null; + } + }); + + // Add tension toggle const tensionControl = document.createElement('div'); tensionControl.className = 'tension-control'; tensionControl.innerHTML = ` - - -
+
+
+
+
+ Click wrench to apply tension `; this.container.appendChild(tensionControl); // Feedback area this.feedback = document.createElement('div'); this.feedback.className = 'lockpick-feedback'; - this.feedback.textContent = 'Apply tension with the wrench, then click pins to try picking the lock'; + this.feedback.textContent = 'Apply tension first, then click and hold on pins to lift them'; this.container.appendChild(this.feedback); - // No timer display anymore - - // Tension wrench control event - const tensionSlider = tensionControl.querySelector('.tension-slider'); - const tensionFill = tensionControl.querySelector('.tension-fill'); - - tensionSlider.addEventListener('input', () => { - this.gameState.tension = parseInt(tensionSlider.value); - tensionFill.style.width = `${this.gameState.tension}%`; + // Tension toggle event + const tensionWrench = tensionControl.querySelector('.tension-wrench'); + const tensionStatus = tensionControl.querySelector('.tension-status'); + + tensionWrench.addEventListener('click', () => { + this.gameState.tensionApplied = !this.gameState.tensionApplied; + tensionWrench.classList.toggle('active', this.gameState.tensionApplied); + cylinder.classList.toggle('rotated', this.gameState.tensionApplied); + + // Update status text + tensionStatus.textContent = this.gameState.tensionApplied ? + 'Tension applied' : 'Click wrench to apply tension'; // Update which pins are binding - this.updatePinVisuals(); + this.updatePinBindings(); - // If tension is suddenly reduced, pins may drop - if (this.gameState.tension < 10 && this.gameState.pinsSet > 0) { - // Drop all set pins + // If tension is toggled off, reset any unset pins + if (!this.gameState.tensionApplied) { this.pins.forEach(pin => { - if (pin.isSet) { - pin.isSet = false; + if (!pin.isSet) { pin.currentHeight = 0; + this.updatePinVisual(pin); } }); - this.gameState.pinsSet = 0; - this.updatePinVisuals(); - this.updateFeedback("Tension released - all pins dropped!"); + this.updateFeedback("Tension released - apply tension before lifting pins"); + } else { + this.updateFeedback("Tension applied - click and hold on pins to lift them"); } }); } start() { super.start(); - - // Set game as active this.gameState.gameActive = true; + } + + // Continuously lift the current pin while mouse is down + liftPin() { + if (!this.gameState.mouseDown || !this.gameState.currentPin || + !this.gameState.gameActive || !this.gameState.tensionApplied) { + return; + } - // No timer setup anymore + const pin = this.gameState.currentPin; + + // Only binding pins can be lifted effectively + if (!this.shouldPinBind(pin)) { + // Non-binding pins can be lifted, but with resistance and limited height + pin.currentHeight += 0.01; + if (pin.currentHeight > 0.3) { + pin.currentHeight = 0.3; // Can't lift non-binding pins very high + } + } else { + // Binding pins lift smoothly + pin.currentHeight += 0.03; + if (pin.currentHeight > 1) { + pin.currentHeight = 1; // Max height + } + } + + // Update visual + this.updatePinVisual(pin); + + // Continue lifting while mouse is down + requestAnimationFrame(() => this.liftPin()); } - cleanup() { - // No timer to clear anymore - } - - updatePinVisuals() { - this.pins.forEach(pin => { - // Update key pin and driver pin heights - pin.elements.keyPin.style.height = `${pin.currentHeight * 40}px`; - pin.elements.driverPin.style.bottom = `${pin.currentHeight * 40 + 1}px`; - pin.elements.spring.style.height = `${20 - pin.currentHeight * 5}px`; + // Check if a pin should be set or dropped + checkPinSet(pin) { + if (!this.gameState.tensionApplied || !this.shouldPinBind(pin)) { + // If no tension or not binding, the pin drops + this.dropPin(pin); + return; + } + + // Check if pin is at the correct height (with some tolerance) + const heightDiff = Math.abs(pin.currentHeight - pin.setPoint); + + if (heightDiff < 0.1) { + // Pin set successfully! + pin.isSet = true; + this.gameState.pinsSet++; + this.updateFeedback(`Pin set at the shear line! (${this.gameState.pinsSet}/${this.pinCount})`); + this.updatePinVisual(pin); - // Show binding state - if (this.shouldPinBind(pin) && !pin.isSet) { - pin.elements.container.classList.add('binding'); + // Check if all pins are set + if (this.gameState.pinsSet === this.pinCount) { + this.endGame(true); + return; + } + + // Update which pin is binding next + this.updatePinBindings(); + } else { + // Pin not at the correct height, drops back down + this.dropPin(pin); + + if (pin.currentHeight > pin.setPoint + 0.1) { + this.updateFeedback("Pin was pushed too far and dropped"); } else { + this.updateFeedback("Pin wasn't lifted high enough and dropped"); + } + } + } + + // Animate a pin dropping down + dropPin(pin) { + // Don't drop pins that are already set + if (pin.isSet) return; + + const dropInterval = setInterval(() => { + pin.currentHeight -= 0.05; + + if (pin.currentHeight <= 0) { + pin.currentHeight = 0; + clearInterval(dropInterval); + } + + this.updatePinVisual(pin); + }, 10); + } + + // Update a single pin's visual appearance + updatePinVisual(pin) { + // Update key pin and driver pin heights + pin.elements.keyPin.style.height = `${pin.currentHeight * 40}px`; + pin.elements.driverPin.style.bottom = `${pin.currentHeight * 40 + 1}px`; + pin.elements.spring.style.height = `${20 - pin.currentHeight * 5}px`; + + // Show set state + pin.elements.container.classList.toggle('set', pin.isSet); + } + + // Update which pins are binding based on binding order + updatePinBindings() { + if (!this.gameState.tensionApplied) { + // No binding if no tension + this.pins.forEach(pin => { pin.elements.container.classList.remove('binding'); + }); + return; + } + + // Find the next unset pin in binding order + let bindingPinFound = false; + + for (let order = 0; order < this.pinCount; order++) { + const nextPin = this.pins.find(p => p.binding === order && !p.isSet); + if (nextPin) { + // Mark this pin as binding + this.pins.forEach(pin => { + pin.elements.container.classList.toggle('binding', pin.index === nextPin.index); + }); + bindingPinFound = true; + break; } - - // Show set state - if (pin.isSet) { - pin.elements.container.classList.add('set'); - } else { - pin.elements.container.classList.remove('set'); - } - }); + } + + // If no binding pin was found (all pins set), remove binding class from all + if (!bindingPinFound) { + this.pins.forEach(pin => { + pin.elements.container.classList.remove('binding'); + }); + } } + // Check if a pin should bind based on binding order shouldPinBind(pin) { - if (this.gameState.tension < 20) return false; + if (!this.gameState.tensionApplied) return false; // Find the next unset pin in binding order for (let order = 0; order < this.pinCount; order++) { @@ -5424,109 +5655,7 @@ } return false; } - - animatePinLift(pin) { - // Only the binding pin can be set - if (!this.shouldPinBind(pin)) { - this.updateFeedback("This pin isn't binding yet"); - - // Small bounce animation to show it's stuck - let height = 0; - const interval = setInterval(() => { - height += 0.05; - if (height >= 0.2) { - clearInterval(interval); - - // Fall back down - const fallInterval = setInterval(() => { - height -= 0.05; - if (height <= 0) { - height = 0; - clearInterval(fallInterval); - } - pin.currentHeight = height; - this.updatePinVisuals(); - }, 20); - } - pin.currentHeight = height; - this.updatePinVisuals(); - }, 20); - - return; - } - - // Start height at current position - let height = pin.currentHeight; - - // Animate lifting to the potential set point - const liftInterval = setInterval(() => { - height += 0.05; - - // Check if we're at or past the set point - if (Math.abs(height - pin.setPoint) < 0.1) { - // At the correct height - set the pin! - clearInterval(liftInterval); - pin.currentHeight = pin.setPoint; - pin.isSet = true; - this.gameState.pinsSet++; - this.updatePinVisuals(); - this.updateFeedback(`Pin set at the shear line! (${this.gameState.pinsSet}/${this.pinCount})`); - - // Check for win condition - if (this.gameState.pinsSet === this.pinCount) { - this.endGame(true); - } - } - else if (height > pin.setPoint + 0.1) { - // We went too far - overset the pin - clearInterval(liftInterval); - this.updateFeedback("Pin pushed too far - overset!"); - - // Random chance of dropping another pin - if (this.gameState.tension > 60 && Math.random() < 0.3) { - const randomSetPin = this.pins.find(p => p.isSet && Math.random() < 0.5); - if (randomSetPin) { - randomSetPin.isSet = false; - this.gameState.pinsSet--; - this.updateFeedback("A pin dropped! Too much movement"); - this.updatePinVisuals(); - } - } - - // Animate falling back down - const fallInterval = setInterval(() => { - height -= 0.05; - if (height <= 0) { - height = 0; - clearInterval(fallInterval); - } - pin.currentHeight = height; - this.updatePinVisuals(); - }, 20); - } - - // If we reach max height without setting - if (height >= 1) { - clearInterval(liftInterval); - this.updateFeedback("Pin pushed too far!"); - - // Fall back down - const fallInterval = setInterval(() => { - height -= 0.05; - if (height <= 0) { - height = 0; - clearInterval(fallInterval); - } - pin.currentHeight = height; - this.updatePinVisuals(); - }, 20); - } - - pin.currentHeight = height; - this.updatePinVisuals(); - }, 20); - } - + updateFeedback(message) { this.feedback.textContent = message; } @@ -5534,13 +5663,30 @@ endGame(success) { this.gameState.gameActive = false; + // Remove mouse event listeners + document.removeEventListener('mouseup', this.mouseUpHandler); + if (success) { this.container.classList.add('success'); this.updateFeedback("Lock picked successfully!"); // Unlock the object in the game - if (typeof unlockTarget === 'function') { - unlockTarget(this.lockable, this.lockable.type || 'object', this.lockable.layer); + if (this.lockable) { + // Set locked to false - this is the crucial part + this.lockable.locked = false; + + // If it's a scenarioData object, also update that property + if (this.lockable.scenarioData) { + this.lockable.scenarioData.locked = false; + } + + // Log successful unlock + if (typeof debugLog === 'function') { + debugLog('LOCKPICK UNLOCK', { + object: this.lockable, + success: true + }, 1); + } } } else { this.container.classList.add('failure'); @@ -5557,6 +5703,11 @@ this.container.appendChild(closeBtn); } + cleanup() { + // Remove any event listeners + document.removeEventListener('mouseup', this.mouseUpHandler); + } + shuffleArray(array) { for (let i = array.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); From 6a301c8bc3c5b19c2cc23bd2c7b1c1658ac5e4c2 Mon Sep 17 00:00:00 2001 From: Damian-I Date: Thu, 13 Mar 2025 18:49:24 +0000 Subject: [PATCH 17/24] Lockpick unlocks target --- index.html | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/index.html b/index.html index fe5fe47..178d006 100644 --- a/index.html +++ b/index.html @@ -3313,7 +3313,11 @@ // If not found, try object-level difficulty difficulty = difficulty || lockable.scenarioData?.difficulty || lockable.properties?.difficulty; debugLog('STARTING LOCKPICK MINIGAME', { difficulty }, 2); - startLockpickingMinigame(lockable, game.scene.scenes[0], difficulty); + startLockpickingMinigame(lockable, game.scene.scenes[0], difficulty, () => { + // Add callback to handle successful lockpicking + unlockTarget(lockable, type, lockable.layer); + gameAlert(`Successfully picked the lock!`, 'success', 'Lock Picked', 4000); + }); } } else { debugLog('KEY NOT FOUND - FAIL', null, 2); @@ -5721,7 +5725,7 @@ MinigameFramework.registerScene('lockpicking', LockpickingMinigame); // Replacement for the startLockpickingMinigame function - function startLockpickingMinigame(lockable, scene, difficulty = 'medium') { + function startLockpickingMinigame(lockable, scene, difficulty = 'medium', callback) { // Initialize the framework if not already done if (!MinigameFramework.mainGameScene) { MinigameFramework.init(scene); @@ -5735,6 +5739,7 @@ if (success) { debugLog('LOCKPICK SUCCESS', null, 1); gameAlert(`Successfully picked the lock!`, 'success', 'Lockpicking', 4000); + callback(); } else { debugLog('LOCKPICK FAILED', null, 2); gameAlert(`Failed to pick the lock.`, 'error', 'Lockpicking', 4000); From 0f2f699b69a3489a8a9cafce2fa5a5424fe9e435 Mon Sep 17 00:00:00 2001 From: Damian-I Date: Fri, 14 Mar 2025 00:57:03 +0000 Subject: [PATCH 18/24] extended dusting minigame to use framework --- index.html | 1560 +++++++++++++++++++++++++++------------------------- 1 file changed, 803 insertions(+), 757 deletions(-) diff --git a/index.html b/index.html index 178d006..065f38e 100644 --- a/index.html +++ b/index.html @@ -1560,8 +1560,8 @@ this.load.image('fingerprint_kit', 'assets/objects/fingerprint_kit.png'); this.load.image('lockpick', 'assets/objects/lockpick.png'); - //this.load.json('gameScenarioJSON', 'assets/scenarios/biometric_breach.json'); - this.load.json('gameScenarioJSON', 'assets/scenarios/ceo_exfil.json'); + this.load.json('gameScenarioJSON', 'assets/scenarios/biometric_breach.json'); + //this.load.json('gameScenarioJSON', 'assets/scenarios/ceo_exfil.json'); gameScenario = this.cache.json.get('gameScenarioJSON'); } @@ -4184,761 +4184,6 @@ return `fp_unknown_${Date.now()}`; } - // 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: 75%; - height: 75%; - background: rgba(0, 0, 0, 0.9); - border: 1px solid #444; - z-index: 1000; - padding: 20px; - border-radius: 5px; - display: flex; - flex-direction: column; - overflow: hidden; - `; - - // Create game container - const gameContainer = document.createElement('div'); - gameContainer.style.cssText = ` - width: 80%; - height: 80%; - max-width: 600px; - max-height: 600px; - display: grid; - grid-template-columns: repeat(30, 1fr); - grid-template-rows: repeat(30, 1fr); - gap: 1px; - background: #1a1a1a; - padding: 5px; - margin: 70px auto 20px auto; - border-radius: 5px; - box-shadow: 0 0 15px rgba(0, 0, 0, 0.5) inset; - position: relative; - overflow: hidden; - `; - - // Add background texture/pattern for a more realistic surface - const gridBackground = document.createElement('div'); - gridBackground.style.cssText = ` - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - opacity: 0.3; - pointer-events: none; - z-index: 0; - `; - - // Create the grid pattern using encoded SVG - const svgGrid = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' fill='%23111'/%3E%3Cpath d='M0 50h100M50 0v100' stroke='%23222' stroke-width='0.5'/%3E%3Cpath d='M25 0v100M75 0v100M0 25h100M0 75h100' stroke='%23191919' stroke-width='0.3'/%3E%3C/svg%3E`; - - gridBackground.style.backgroundImage = `url('${svgGrid}')`; - gameContainer.appendChild(gridBackground); - - // Add instructions and header with difficulty selection - const header = document.createElement('div'); - header.style.cssText = ` - position: absolute; - top: 0; - left: 0; - width: 100%; - background: rgba(30, 30, 30, 0.9); - padding: 10px; - display: flex; - flex-direction: column; - align-items: center; - `; - - header.innerHTML = ` -

Fingerprint Dusting

-

- Drag to dust the surface and reveal fingerprints. Avoid over-dusting! -

- `; - - // Add progress display - const progressText = document.createElement('div'); - progressText.style.cssText = ` - position: absolute; - bottom: 15px; - left: 50%; - transform: translateX(-50%); - color: white; - text-align: center; - font-size: 16px; - background: rgba(0, 0, 0, 0.6); - padding: 5px 15px; - border-radius: 15px; - z-index: 10; - width: 80%; - max-width: 500px; - `; - - // Add tool selection - const toolsContainer = document.createElement('div'); - toolsContainer.style.cssText = ` - position: absolute; - bottom: 15px; - left: 15px; - display: flex; - gap: 10px; - z-index: 10; - flex-wrap: wrap; - max-width: 30%; - `; - - const tools = [ - { name: 'Fine', size: 1, color: '#3498db', radius: 0 }, // Only affects current cell - { name: 'Medium', size: 2, color: '#2ecc71', radius: 1 }, // Affects current cell and adjacent - { name: 'Wide', size: 3, color: '#e67e22', radius: 2 } // Affects current cell and 2 cells around - ]; - - let currentTool = tools[1]; // Start with medium brush - - tools.forEach(tool => { - const toolButton = document.createElement('button'); - toolButton.className = tool.name === currentTool.name ? 'tool-button active' : 'tool-button'; - toolButton.textContent = tool.name; - toolButton.style.cssText = ` - background: ${tool.color}; - color: white; - border: none; - border-radius: 3px; - padding: 5px 10px; - cursor: pointer; - opacity: ${tool.name === currentTool.name ? '1' : '0.7'}; - `; - - toolButton.addEventListener('click', () => { - document.querySelectorAll('.tool-button').forEach(btn => { - btn.classList.remove('active'); - btn.style.opacity = '0.7'; - }); - toolButton.classList.add('active'); - toolButton.style.opacity = '1'; - currentTool = tool; - }); - - toolsContainer.appendChild(toolButton); - }); - - // Generate fingerprint pattern - const gridSize = 30; // Increased from 20 to 30 - let fingerprintCells = new Set(); - const centerX = Math.floor(gridSize / 2); - const centerY = Math.floor(gridSize / 2); - - // Game state variables - let difficultySettings = { - easy: { - requiredCoverage: 0.3, // 30% of prints - maxOverDusted: 50, // Increased due to more cells - fingerprints: 60, // Increased proportionally - pattern: 'simple' - }, - medium: { - requiredCoverage: 0.4, // 40% of prints - maxOverDusted: 40, // Increased due to more cells - fingerprints: 75, // Increased proportionally - pattern: 'medium' - }, - hard: { - requiredCoverage: 0.5, // 50% of prints - maxOverDusted: 25, // Increased due to more cells - fingerprints: 90, // Increased proportionally - pattern: 'complex' - } - }; - - let currentDifficulty = item.scenarioData.fingerprintDifficulty; - - // Generate fingerprint pattern based on difficulty - function generateFingerprint(difficulty) { - const pattern = difficultySettings[difficulty].pattern; - const numPrints = difficultySettings[difficulty].fingerprints; - const newFingerprintCells = new Set(); - - if (pattern === 'simple') { - // Simple oval-like pattern - for (let i = 0; i < numPrints; i++) { - const angle = (i / numPrints) * Math.PI * 2; - const distance = 5 + Math.random() * 3; - const x = Math.floor(centerX + Math.cos(angle) * distance); - const y = Math.floor(centerY + Math.sin(angle) * distance); - - if (x >= 0 && x < gridSize && y >= 0 && y < gridSize) { - newFingerprintCells.add(`${x},${y}`); - - // Add a few adjacent cells to make it less sparse - for (let j = 0; j < 2; j++) { - const nx = x + Math.floor(Math.random() * 3) - 1; - const ny = y + Math.floor(Math.random() * 3) - 1; - if (nx >= 0 && nx < gridSize && ny >= 0 && ny < gridSize) { - newFingerprintCells.add(`${nx},${ny}`); - } - } - } - } - } else if (pattern === 'medium') { - // Medium complexity - spiral pattern with variations - for (let i = 0; i < numPrints; i++) { - const t = i / numPrints * 5; - const distance = 2 + t * 0.8; - const noise = Math.random() * 2 - 1; - const x = Math.floor(centerX + Math.cos(t * Math.PI * 2) * (distance + noise)); - const y = Math.floor(centerY + Math.sin(t * Math.PI * 2) * (distance + noise)); - - if (x >= 0 && x < gridSize && y >= 0 && y < gridSize) { - newFingerprintCells.add(`${x},${y}`); - } - } - - // Add whorls and arches - for (let i = 0; i < 20; i++) { - const angle = (i / 20) * Math.PI * 2; - const distance = 7; - const x = Math.floor(centerX + Math.cos(angle) * distance); - const y = Math.floor(centerY + Math.sin(angle) * distance); - - if (x >= 0 && x < gridSize && y >= 0 && y < gridSize) { - newFingerprintCells.add(`${x},${y}`); - - // Add ridge-like pattern - for (let j = 1; j <= 3; j++) { - const ridgeX = Math.floor(centerX + Math.cos(angle) * (distance - j)); - const ridgeY = Math.floor(centerY + Math.sin(angle) * (distance - j)); - if (ridgeX >= 0 && ridgeX < gridSize && ridgeY >= 0 && ridgeY < gridSize) { - newFingerprintCells.add(`${ridgeX},${ridgeY}`); - } - } - } - } - } else { - // Complex pattern - detailed whorls and ridge patterns - for (let i = 0; i < numPrints; i++) { - // Main loop - create a complex whorl pattern - const t = i / numPrints * 8; - const distance = 2 + t * 0.6; - const noise = Math.sin(t * 5) * 1.5; - const x = Math.floor(centerX + Math.cos(t * Math.PI * 2) * (distance + noise)); - const y = Math.floor(centerY + Math.sin(t * Math.PI * 2) * (distance + noise)); - - if (x >= 0 && x < gridSize && y >= 0 && y < gridSize) { - newFingerprintCells.add(`${x},${y}`); - } - - // Add bifurcations and ridge endings - if (i % 5 === 0) { - const bifAngle = t * Math.PI * 2 + Math.PI/4; - const bx = Math.floor(x + Math.cos(bifAngle) * 1); - const by = Math.floor(y + Math.sin(bifAngle) * 1); - if (bx >= 0 && bx < gridSize && by >= 0 && by < gridSize) { - newFingerprintCells.add(`${bx},${by}`); - } - } - } - - // Add delta patterns - for (let d = 0; d < 3; d++) { - const deltaAngle = (d / 3) * Math.PI * 2; - const deltaX = Math.floor(centerX + Math.cos(deltaAngle) * 8); - const deltaY = Math.floor(centerY + Math.sin(deltaAngle) * 8); - - for (let r = 0; r < 5; r++) { - for (let a = 0; a < 3; a++) { - const rayAngle = deltaAngle + (a - 1) * Math.PI/4; - const rx = Math.floor(deltaX + Math.cos(rayAngle) * r); - const ry = Math.floor(deltaY + Math.sin(rayAngle) * r); - if (rx >= 0 && rx < gridSize && ry >= 0 && ry < gridSize) { - newFingerprintCells.add(`${rx},${ry}`); - } - } - } - } - } - - // Ensure we have at least the minimum number of cells - while (newFingerprintCells.size < numPrints) { - const x = centerX + Math.floor(Math.random() * 12 - 6); - const y = centerY + Math.floor(Math.random() * 12 - 6); - if (x >= 0 && x < gridSize && y >= 0 && y < gridSize) { - newFingerprintCells.add(`${x},${y}`); - } - } - - return newFingerprintCells; - } - - // Initialize with medium difficulty - fingerprintCells = generateFingerprint(currentDifficulty); - - // Create particle container for dust effects - const particleContainer = document.createElement('div'); - particleContainer.style.cssText = ` - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - pointer-events: none; - z-index: 5; - overflow: hidden; - `; - - // Create function to add dust particles - function createDustParticles(x, y, intensity, color) { - const numParticles = Math.floor(5 + intensity * 5); // 5-10 particles based on intensity - - for (let i = 0; i < numParticles; i++) { - const particle = document.createElement('div'); - const size = Math.random() * 3 + 1; // 1-4px - const angle = Math.random() * Math.PI * 2; - const distance = Math.random() * 20 * intensity; - const duration = Math.random() * 1000 + 500; // 500-1500ms - - particle.style.cssText = ` - position: absolute; - width: ${size}px; - height: ${size}px; - background: ${color}; - border-radius: 50%; - opacity: ${Math.random() * 0.3 + 0.3}; - top: ${y}px; - left: ${x}px; - transform: translate(-50%, -50%); - pointer-events: none; - z-index: 6; - `; - - particleContainer.appendChild(particle); - - // Animate the particle - const animation = particle.animate([ - { - transform: 'translate(-50%, -50%)', - opacity: particle.style.opacity - }, - { - transform: `translate( - calc(-50% + ${Math.cos(angle) * distance}px), - calc(-50% + ${Math.sin(angle) * distance}px) - )`, - opacity: 0 - } - ], { - duration: duration, - easing: 'cubic-bezier(0.25, 1, 0.5, 1)' - }); - - animation.onfinish = () => { - particle.remove(); - }; - } - } - - // Track progress - let revealedPrints = 0; - let totalPrints = fingerprintCells.size; - let overDusted = 0; - let requiredPrints = Math.ceil(totalPrints * difficultySettings[currentDifficulty].requiredCoverage); - - // 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; - transition: background-color 0.1s; - cursor: pointer; - `; - cell.dataset.x = x; - cell.dataset.y = y; - cell.dataset.dustLevel = '0'; - cell.dataset.hasFingerprint = fingerprintCells.has(`${x},${y}`) ? 'true' : 'false'; - - gameContainer.appendChild(cell); - } - } - - // Add dragging interaction - 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) { - // Get current cell coordinates - const centerX = parseInt(cell.dataset.x); - const centerY = parseInt(cell.dataset.y); - - // Get a list of cells to dust based on the brush radius - const cellsToDust = []; - const radius = currentTool.radius; - - // Add the current cell and cells within radius - for (let y = centerY - radius; y <= centerY + radius; y++) { - for (let x = centerX - radius; x <= centerX + radius; x++) { - // Skip cells outside the grid - if (x < 0 || x >= gridSize || y < 0 || y >= gridSize) continue; - - // For medium brush, use a diamond pattern (taxicab distance) - if (currentTool.size === 2) { - // Manhattan distance: |x1-x2| + |y1-y2| - const distance = Math.abs(x - centerX) + Math.abs(y - centerY); - if (distance > radius) continue; // Skip if too far away - } - // For wide brush, use a circle pattern (Euclidean distance) - else if (currentTool.size === 3) { - // Euclidean distance: √[(x1-x2)² + (y1-y2)²] - const distance = Math.sqrt(Math.pow(x - centerX, 2) + Math.pow(y - centerY, 2)); - if (distance > radius) continue; // Skip if too far away - } - - // Find this cell in the DOM - const targetCell = gameContainer.querySelector(`[data-x="${x}"][data-y="${y}"]`); - if (targetCell) { - cellsToDust.push(targetCell); - } - } - } - - // Get cell position for particles (center cell) - const cellRect = cell.getBoundingClientRect(); - const particleContainerRect = particleContainer.getBoundingClientRect(); - const cellCenterX = (cellRect.left + cellRect.width / 2) - particleContainerRect.left; - const cellCenterY = (cellRect.top + cellRect.height / 2) - particleContainerRect.top; - - // Process all cells to dust - cellsToDust.forEach(targetCell => { - const cellId = `${targetCell.dataset.x},${targetCell.dataset.y}`; - const currentTime = Date.now(); - const dustLevel = parseInt(targetCell.dataset.dustLevel); - - // Tool intensity affects dusting rate and particle effects - const toolIntensity = currentTool.size / 3; // 0.33 to 1 - - // Only allow dusting every 50-150ms for each cell (based on tool size) - const cooldown = 150 - (toolIntensity * 100); // 50ms for wide brush, 150ms for fine - - if (!lastDustTime[cellId] || currentTime - lastDustTime[cellId] > cooldown) { - if (dustLevel < 3) { - // Increment dust level with a probability based on tool intensity - const dustProbability = toolIntensity * 0.5 + 0.1; // 0.1-0.6 chance based on tool - - if (dustLevel < 1 || Math.random() < dustProbability) { - targetCell.dataset.dustLevel = (dustLevel + 1).toString(); - updateCellColor(targetCell); - - // Create dust particles for the current cell or at a position calculated for surrounding cells - if (targetCell === cell) { - // Center cell - use the already calculated position - const hasFingerprint = targetCell.dataset.hasFingerprint === 'true'; - let particleColor = dustLevel === 1 ? '#666' : (hasFingerprint ? '#1aff1a' : '#aaa'); - createDustParticles(cellCenterX, cellCenterY, toolIntensity, particleColor); - } else { - // For surrounding cells, calculate their relative position from the center cell - const targetCellRect = targetCell.getBoundingClientRect(); - const targetCellX = (targetCellRect.left + targetCellRect.width / 2) - particleContainerRect.left; - const targetCellY = (targetCellRect.top + targetCellRect.height / 2) - particleContainerRect.top; - - const hasFingerprint = targetCell.dataset.hasFingerprint === 'true'; - let particleColor = dustLevel === 1 ? '#666' : (hasFingerprint ? '#1aff1a' : '#aaa'); - - // Create fewer particles for surrounding cells - const reducedIntensity = toolIntensity * 0.6; - createDustParticles(targetCellX, targetCellY, reducedIntensity, particleColor); - } - } - lastDustTime[cellId] = currentTime; - } - } - }); - - // Update progress after dusting - checkProgress(); - } - }); - - function updateCellColor(cell) { - const dustLevel = parseInt(cell.dataset.dustLevel); - const hasFingerprint = cell.dataset.hasFingerprint === 'true'; - - if (dustLevel === 0) { - cell.style.background = 'black'; - cell.style.boxShadow = 'none'; - } - else if (dustLevel === 1) { - cell.style.background = '#444'; - cell.style.boxShadow = 'inset 0 0 3px rgba(255,255,255,0.2)'; - } - else if (dustLevel === 2) { - if (hasFingerprint) { - cell.style.background = '#0f0'; - cell.style.boxShadow = 'inset 0 0 5px rgba(0,255,0,0.5), 0 0 5px rgba(0,255,0,0.3)'; - } else { - cell.style.background = '#888'; - cell.style.boxShadow = 'inset 0 0 4px rgba(255,255,255,0.3)'; - } - } - else { - cell.style.background = '#ccc'; - cell.style.boxShadow = 'inset 0 0 5px rgba(255,255,255,0.5)'; - } - } - - function checkProgress() { - revealedPrints = 0; - overDusted = 0; - - gameContainer.childNodes.forEach(cell => { - if (cell.dataset) { // Check if it's a cell element - const dustLevel = parseInt(cell.dataset.dustLevel || '0'); - const hasFingerprint = cell.dataset.hasFingerprint === 'true'; - - if (hasFingerprint && dustLevel === 2) revealedPrints++; - if (dustLevel === 3) overDusted++; - } - }); - - // Update progress display - progressText.innerHTML = ` -
- Found: ${revealedPrints}/${requiredPrints} required prints - - Over-dusted: ${overDusted}/${difficultySettings[currentDifficulty].maxOverDusted} max - -
-
-
-
- `; - - // Check fail condition first - if (overDusted >= difficultySettings[currentDifficulty].maxOverDusted) { - showFailure("Too many over-dusted areas!"); - return; - } - - // Check win condition - if (revealedPrints >= requiredPrints) { - showSuccess(); - } - } - - function showSuccess() { - // Calculate quality based on dusting precision - const dustPenalty = overDusted / difficultySettings[currentDifficulty].maxOverDusted; // 0-1 - const coverageBonus = revealedPrints / totalPrints; // 0-1 - - // Higher quality for more coverage and less over-dusting - const quality = 0.7 + (coverageBonus * 0.25) - (dustPenalty * 0.15); - - // 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; - box-shadow: 0 0 20px rgba(0, 255, 0, 0.3); - `; - - const qualityPercentage = Math.round(quality * 100); - const qualityRating = qualityPercentage >= 95 ? 'Perfect' : - qualityPercentage >= 85 ? 'Excellent' : - qualityPercentage >= 75 ? 'Good' : 'Acceptable'; - - successMessage.innerHTML = ` -
Fingerprint successfully collected!
-
Quality: ${qualityRating} (${qualityPercentage}%)
-
- Prints revealed: ${revealedPrints}/${totalPrints}
- Over-dusted areas: ${overDusted}
- Difficulty: ${currentDifficulty.charAt(0).toUpperCase() + currentDifficulty.slice(1)} -
- `; - - 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: quality, // Quality between 0.7 and ~1.0 - data: generateFingerprintData(item), - timestamp: Date.now() - }; - - 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; - } - - // Update the biometrics panel and count - updateBiometricsPanel(); - updateBiometricsCount(); - - // Show notification - gameAlert(`Collected ${sample.owner}'s fingerprint sample (${qualityRating} quality)`, 'success', 'Sample Acquired', 3000); - }, 2000); - } - - function showFailure(reason) { - // 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; - box-shadow: 0 0 20px rgba(255, 0, 0, 0.3); - `; - failureMessage.innerHTML = ` -
${reason}
-
Try again with more careful dusting.
- `; - - iframe.appendChild(failureMessage); - - // Disable further interaction - isDragging = false; - gameContainer.style.pointerEvents = 'none'; - - setTimeout(() => { - document.body.removeChild(iframe); - scene.input.mouse.enabled = true; - }, 2000); - } - - // Handle difficulty buttons - iframe.addEventListener('click', (e) => { - if (e.target.classList.contains('difficulty-button')) { - // Get difficulty from button - const newDifficulty = e.target.id.split('-')[0]; // easy, medium, or hard - - // Only change if different - if (newDifficulty !== currentDifficulty) { - currentDifficulty = newDifficulty; - - // Update active button - document.querySelectorAll('.difficulty-button').forEach(btn => { - btn.classList.remove('active'); - btn.style.opacity = '0.7'; - }); - e.target.classList.add('active'); - e.target.style.opacity = '1'; - - // Reset the game with new difficulty - resetGame(); - } - } - }); - - function resetGame() { - // Reset game state - fingerprintCells = generateFingerprint(currentDifficulty); - totalPrints = fingerprintCells.size; - requiredPrints = Math.ceil(totalPrints * difficultySettings[currentDifficulty].requiredCoverage); - revealedPrints = 0; - overDusted = 0; - - // Reset cells - gameContainer.childNodes.forEach(node => { - if (node.dataset && node.dataset.x !== undefined) { - const x = parseInt(node.dataset.x); - const y = parseInt(node.dataset.y); - node.dataset.hasFingerprint = fingerprintCells.has(`${x},${y}`) ? 'true' : 'false'; - node.dataset.dustLevel = '0'; - updateCellColor(node); - } - }); - // Update progress - checkProgress(); - } - - // 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; - z-index: 10; - `; - closeButton.onclick = () => { - document.body.removeChild(iframe); - scene.input.mouse.enabled = true; - }; - - // Assemble the interface - iframe.appendChild(closeButton); - iframe.appendChild(header); - iframe.appendChild(gameContainer); - iframe.appendChild(particleContainer); - iframe.appendChild(progressText); - iframe.appendChild(toolsContainer); - document.body.appendChild(iframe); - - // Start the game - checkProgress(); - - // Disable game movement - const scene = item.scene; - scene.input.mouse.enabled = false; - } // Minigame Framework const MinigameFramework = { @@ -6422,6 +5667,807 @@ ); } + // Dusting Minigame Scene implementation + class DustingMinigame extends MinigameFramework.MinigameScene { + constructor(container, params) { + super(container, params); + + this.item = params.item; + + // Game state variables + this.difficultySettings = { + easy: { + requiredCoverage: 0.3, // 30% of prints + maxOverDusted: 50, // Increased due to more cells + fingerprints: 60, // Increased proportionally + pattern: 'simple' + }, + medium: { + requiredCoverage: 0.4, // 40% of prints + maxOverDusted: 40, // Increased due to more cells + fingerprints: 75, // Increased proportionally + pattern: 'medium' + }, + hard: { + requiredCoverage: 0.5, // 50% of prints + maxOverDusted: 25, // Increased due to more cells + fingerprints: 90, // Increased proportionally + pattern: 'complex' + } + }; + + this.currentDifficulty = this.item.scenarioData.fingerprintDifficulty; + this.gridSize = 30; + this.fingerprintCells = new Set(); + this.revealedPrints = 0; + this.overDusted = 0; + this.isDragging = false; + this.lastDustTime = {}; + + // Tools configuration + this.tools = [ + { name: 'Fine', size: 1, color: '#3498db', radius: 0 }, // Only affects current cell + { name: 'Medium', size: 2, color: '#2ecc71', radius: 1 }, // Affects current cell and adjacent + { name: 'Wide', size: 3, color: '#e67e22', radius: 2 } // Affects current cell and 2 cells around + ]; + this.currentTool = this.tools[1]; // Start with medium brush + } + + init() { + super.init(); + + console.log("Dusting minigame container:", this.container); + + // Fix: Apply proper display style (flex instead of relying on the framework) + this.container.style.display = 'flex'; + + // Apply CSS styles to the container + this.container.style.cssText = ` + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 75%; + height: 75%; + background: rgba(0, 0, 0, 0.9); + border: 1px solid #444; + z-index: 1002; /* Important: Higher than the framework overlay */ + padding: 20px; + border-radius: 5px; + display: flex; + flex-direction: column; + overflow: hidden; + `; + + // Create header with instructions + const header = document.createElement('div'); + header.style.cssText = ` + position: absolute; + top: 0; + left: 0; + width: 100%; + background: rgba(30, 30, 30, 0.9); + padding: 10px; + display: flex; + flex-direction: column; + align-items: center; + z-index: 5; + `; + + header.innerHTML = ` +

Fingerprint Dusting

+

+ Drag to dust the surface and reveal fingerprints. Avoid over-dusting! +

+ `; + + // Create game container + const gameContainer = document.createElement('div'); + gameContainer.style.cssText = ` + width: 80%; + height: 80%; + max-width: 600px; + max-height: 600px; + display: grid; + grid-template-columns: repeat(30, 1fr); + grid-template-rows: repeat(30, 1fr); + gap: 1px; + background: #1a1a1a; + padding: 5px; + margin: 70px auto 20px auto; + border-radius: 5px; + box-shadow: 0 0 15px rgba(0, 0, 0, 0.5) inset; + position: relative; + overflow: hidden; + cursor: crosshair; + `; + + // Add background texture/pattern for a more realistic surface + const gridBackground = document.createElement('div'); + gridBackground.style.cssText = ` + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 0.3; + pointer-events: none; + z-index: 0; + `; + + // Create the grid pattern using encoded SVG + const svgGrid = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' fill='%23111'/%3E%3Cpath d='M0 50h100M50 0v100' stroke='%23222' stroke-width='0.5'/%3E%3Cpath d='M25 0v100M75 0v100M0 25h100M0 75h100' stroke='%23191919' stroke-width='0.3'/%3E%3C/svg%3E`; + + gridBackground.style.backgroundImage = `url('${svgGrid}')`; + gameContainer.appendChild(gridBackground); + + // Add progress display + const progressText = document.createElement('div'); + progressText.style.cssText = ` + position: absolute; + bottom: 15px; + left: 50%; + transform: translateX(-50%); + color: white; + text-align: center; + font-size: 16px; + background: rgba(0, 0, 0, 0.6); + padding: 5px 15px; + border-radius: 15px; + z-index: 10; + width: 80%; + max-width: 500px; + `; + + // Add tool selection + const toolsContainer = document.createElement('div'); + toolsContainer.style.cssText = ` + position: absolute; + bottom: 15px; + left: 15px; + display: flex; + gap: 10px; + z-index: 10; + flex-wrap: wrap; + max-width: 30%; + `; + + this.tools.forEach(tool => { + const toolButton = document.createElement('button'); + toolButton.className = tool.name === this.currentTool.name ? 'tool-button active' : 'tool-button'; + toolButton.textContent = tool.name; + toolButton.style.cssText = ` + background: ${tool.color}; + color: white; + border: none; + border-radius: 3px; + padding: 5px 10px; + cursor: pointer; + opacity: ${tool.name === this.currentTool.name ? '1' : '0.7'}; + `; + + toolButton.addEventListener('click', () => { + document.querySelectorAll('.tool-button').forEach(btn => { + btn.classList.remove('active'); + btn.style.opacity = '0.7'; + }); + toolButton.classList.add('active'); + toolButton.style.opacity = '1'; + this.currentTool = tool; + }); + + toolsContainer.appendChild(toolButton); + }); + + // Generate fingerprint pattern + this.fingerprintCells = this.generateFingerprint(this.currentDifficulty); + + // Create particle container for dust effects + const particleContainer = document.createElement('div'); + particleContainer.style.cssText = ` + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 5; + overflow: hidden; + `; + + // Total prints and required prints calculations + this.totalPrints = this.fingerprintCells.size; + this.requiredPrints = Math.ceil(this.totalPrints * this.difficultySettings[this.currentDifficulty].requiredCoverage); + + // Create grid cells + for (let y = 0; y < this.gridSize; y++) { + for (let x = 0; x < this.gridSize; x++) { + const cell = document.createElement('div'); + cell.style.cssText = ` + width: 100%; + height: 100%; + background: black; + position: relative; + transition: background-color 0.1s; + cursor: pointer; + `; + cell.dataset.x = x; + cell.dataset.y = y; + cell.dataset.dustLevel = '0'; + cell.dataset.hasFingerprint = this.fingerprintCells.has(`${x},${y}`) ? 'true' : 'false'; + + gameContainer.appendChild(cell); + } + } + + // Store references to elements we'll need to update + this.gameContainer = gameContainer; + this.progressText = progressText; + this.particleContainer = particleContainer; + + // Add dragging interaction - bind these to the instance + this.mouseDownHandler = this.handleMouseDown.bind(this); + this.mouseUpHandler = this.handleMouseUp.bind(this); + this.mouseLeaveHandler = this.handleMouseLeave.bind(this); + this.mouseMoveHandler = this.handleMouseMove.bind(this); + + gameContainer.addEventListener('mousedown', this.mouseDownHandler); + document.addEventListener('mouseup', this.mouseUpHandler); + gameContainer.addEventListener('mouseleave', this.mouseLeaveHandler); + gameContainer.addEventListener('mousemove', this.mouseMoveHandler); + + // 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; + z-index: 10; + `; + closeButton.onclick = () => { + this.complete(false); + }; + + // Assemble the interface + this.container.appendChild(closeButton); + this.container.appendChild(header); + this.container.appendChild(gameContainer); + this.container.appendChild(particleContainer); + this.container.appendChild(progressText); + this.container.appendChild(toolsContainer); + + // Check initial progress + this.checkProgress(); + + // Log to console that the game is initialized + console.log("Dusting minigame initialized"); + } + + // Mouse event handlers + handleMouseDown(e) { + console.log("Mouse down"); + this.isDragging = true; + // Immediately start dusting at this position + this.handleMouseMove(e); + } + + handleMouseUp() { + console.log("Mouse up"); + this.isDragging = false; + } + + handleMouseLeave() { + console.log("Mouse leave"); + this.isDragging = false; + } + + handleMouseMove(e) { + if (!this.isDragging) return; + + console.log("Mouse move while dragging"); + + // Get the cell element under the cursor + const cell = document.elementFromPoint(e.clientX, e.clientY); + if (!cell || !cell.dataset || cell.dataset.dustLevel === undefined) return; + + // Get current cell coordinates + const centerX = parseInt(cell.dataset.x); + const centerY = parseInt(cell.dataset.y); + + // Get a list of cells to dust based on the brush radius + const cellsToDust = []; + const radius = this.currentTool.radius; + + // Add the current cell and cells within radius + for (let y = centerY - radius; y <= centerY + radius; y++) { + for (let x = centerX - radius; x <= centerX + radius; x++) { + // Skip cells outside the grid + if (x < 0 || x >= this.gridSize || y < 0 || y >= this.gridSize) continue; + + // For medium brush, use a diamond pattern (taxicab distance) + if (this.currentTool.size === 2) { + // Manhattan distance: |x1-x2| + |y1-y2| + const distance = Math.abs(x - centerX) + Math.abs(y - centerY); + if (distance > radius) continue; // Skip if too far away + } + // For wide brush, use a circle pattern (Euclidean distance) + else if (this.currentTool.size === 3) { + // Euclidean distance: √[(x1-x2)² + (y1-y2)²] + const distance = Math.sqrt(Math.pow(x - centerX, 2) + Math.pow(y - centerY, 2)); + if (distance > radius) continue; // Skip if too far away + } + + // Find this cell in the DOM + const targetCell = this.gameContainer.querySelector(`[data-x="${x}"][data-y="${y}"]`); + if (targetCell) { + cellsToDust.push(targetCell); + } + } + } + + // Get cell position for particles (center cell) + const cellRect = cell.getBoundingClientRect(); + const particleContainerRect = this.particleContainer.getBoundingClientRect(); + const cellCenterX = (cellRect.left + cellRect.width / 2) - particleContainerRect.left; + const cellCenterY = (cellRect.top + cellRect.height / 2) - particleContainerRect.top; + + // Process all cells to dust + cellsToDust.forEach(targetCell => { + const cellId = `${targetCell.dataset.x},${targetCell.dataset.y}`; + const currentTime = Date.now(); + const dustLevel = parseInt(targetCell.dataset.dustLevel); + + // Tool intensity affects dusting rate and particle effects + const toolIntensity = this.currentTool.size / 3; // 0.33 to 1 + + // Only allow dusting every 50-150ms for each cell (based on tool size) + const cooldown = 150 - (toolIntensity * 100); // 50ms for wide brush, 150ms for fine + + if (!this.lastDustTime[cellId] || currentTime - this.lastDustTime[cellId] > cooldown) { + if (dustLevel < 3) { + // Increment dust level with a probability based on tool intensity + const dustProbability = toolIntensity * 0.5 + 0.1; // 0.1-0.6 chance based on tool + + if (dustLevel < 1 || Math.random() < dustProbability) { + targetCell.dataset.dustLevel = (dustLevel + 1).toString(); + this.updateCellColor(targetCell); + + // Create dust particles for the current cell or at a position calculated for surrounding cells + if (targetCell === cell) { + // Center cell - use the already calculated position + const hasFingerprint = targetCell.dataset.hasFingerprint === 'true'; + let particleColor = dustLevel === 1 ? '#666' : (hasFingerprint ? '#1aff1a' : '#aaa'); + this.createDustParticles(cellCenterX, cellCenterY, toolIntensity, particleColor); + } else { + // For surrounding cells, calculate their relative position from the center cell + const targetCellRect = targetCell.getBoundingClientRect(); + const targetCellX = (targetCellRect.left + targetCellRect.width / 2) - particleContainerRect.left; + const targetCellY = (targetCellRect.top + targetCellRect.height / 2) - particleContainerRect.top; + + const hasFingerprint = targetCell.dataset.hasFingerprint === 'true'; + let particleColor = dustLevel === 1 ? '#666' : (hasFingerprint ? '#1aff1a' : '#aaa'); + + // Create fewer particles for surrounding cells + const reducedIntensity = toolIntensity * 0.6; + this.createDustParticles(targetCellX, targetCellY, reducedIntensity, particleColor); + } + } + this.lastDustTime[cellId] = currentTime; + } + } + }); + + // Update progress after dusting + this.checkProgress(); + } + + start() { + super.start(); + console.log("Dusting minigame started"); + // Disable game movement + if (this.params.scene) { + this.params.scene.input.mouse.enabled = false; + } + } + + cleanup() { + // Remove event listeners + this.gameContainer.removeEventListener('mousedown', this.mouseDownHandler); + document.removeEventListener('mouseup', this.mouseUpHandler); + this.gameContainer.removeEventListener('mouseleave', this.mouseLeaveHandler); + this.gameContainer.removeEventListener('mousemove', this.mouseMoveHandler); + + // Re-enable game movement + if (this.params.scene) { + this.params.scene.input.mouse.enabled = true; + } + + console.log("Dusting minigame cleaned up"); + } + + createDustParticles(x, y, intensity, color) { + const numParticles = Math.floor(5 + intensity * 5); // 5-10 particles based on intensity + + for (let i = 0; i < numParticles; i++) { + const particle = document.createElement('div'); + const size = Math.random() * 3 + 1; // 1-4px + const angle = Math.random() * Math.PI * 2; + const distance = Math.random() * 20 * intensity; + const duration = Math.random() * 1000 + 500; // 500-1500ms + + particle.style.cssText = ` + position: absolute; + width: ${size}px; + height: ${size}px; + background: ${color}; + border-radius: 50%; + opacity: ${Math.random() * 0.3 + 0.3}; + top: ${y}px; + left: ${x}px; + transform: translate(-50%, -50%); + pointer-events: none; + z-index: 6; + `; + + this.particleContainer.appendChild(particle); + + // Animate the particle + const animation = particle.animate([ + { + transform: 'translate(-50%, -50%)', + opacity: particle.style.opacity + }, + { + transform: `translate( + calc(-50% + ${Math.cos(angle) * distance}px), + calc(-50% + ${Math.sin(angle) * distance}px) + )`, + opacity: 0 + } + ], { + duration: duration, + easing: 'cubic-bezier(0.25, 1, 0.5, 1)' + }); + + animation.onfinish = () => { + particle.remove(); + }; + } + } + + updateCellColor(cell) { + const dustLevel = parseInt(cell.dataset.dustLevel); + const hasFingerprint = cell.dataset.hasFingerprint === 'true'; + + if (dustLevel === 0) { + cell.style.background = 'black'; + cell.style.boxShadow = 'none'; + } + else if (dustLevel === 1) { + cell.style.background = '#444'; + cell.style.boxShadow = 'inset 0 0 3px rgba(255,255,255,0.2)'; + } + else if (dustLevel === 2) { + if (hasFingerprint) { + cell.style.background = '#0f0'; + cell.style.boxShadow = 'inset 0 0 5px rgba(0,255,0,0.5), 0 0 5px rgba(0,255,0,0.3)'; + } else { + cell.style.background = '#888'; + cell.style.boxShadow = 'inset 0 0 4px rgba(255,255,255,0.3)'; + } + } + else { + cell.style.background = '#ccc'; + cell.style.boxShadow = 'inset 0 0 5px rgba(255,255,255,0.5)'; + } + } + + checkProgress() { + this.revealedPrints = 0; + this.overDusted = 0; + + this.gameContainer.childNodes.forEach(cell => { + if (cell.dataset) { // Check if it's a cell element + const dustLevel = parseInt(cell.dataset.dustLevel || '0'); + const hasFingerprint = cell.dataset.hasFingerprint === 'true'; + + if (hasFingerprint && dustLevel === 2) this.revealedPrints++; + if (dustLevel === 3) this.overDusted++; + } + }); + + // Update progress display + this.progressText.innerHTML = ` +
+ Found: ${this.revealedPrints}/${this.requiredPrints} required prints + + Over-dusted: ${this.overDusted}/${this.difficultySettings[this.currentDifficulty].maxOverDusted} max + +
+
+
+
+ `; + + // Check fail condition first + if (this.overDusted >= this.difficultySettings[this.currentDifficulty].maxOverDusted) { + this.showFailure("Too many over-dusted areas!"); + return; + } + + // Check win condition + if (this.revealedPrints >= this.requiredPrints) { + this.showSuccess(); + } + } + + showSuccess() { + // Calculate quality based on dusting precision + const dustPenalty = this.overDusted / this.difficultySettings[this.currentDifficulty].maxOverDusted; // 0-1 + const coverageBonus = this.revealedPrints / this.totalPrints; // 0-1 + + // Higher quality for more coverage and less over-dusting + const quality = 0.7 + (coverageBonus * 0.25) - (dustPenalty * 0.15); + + // 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; + box-shadow: 0 0 20px rgba(0, 255, 0, 0.3); + `; + + const qualityPercentage = Math.round(quality * 100); + const qualityRating = qualityPercentage >= 95 ? 'Perfect' : + qualityPercentage >= 85 ? 'Excellent' : + qualityPercentage >= 75 ? 'Good' : 'Acceptable'; + + successMessage.innerHTML = ` +
Fingerprint successfully collected!
+
Quality: ${qualityRating} (${qualityPercentage}%)
+
+ Prints revealed: ${this.revealedPrints}/${this.totalPrints}
+ Over-dusted areas: ${this.overDusted}
+ Difficulty: ${this.currentDifficulty.charAt(0).toUpperCase() + this.currentDifficulty.slice(1)} +
+ `; + + this.container.appendChild(successMessage); + + // Disable further interaction + this.isDragging = false; + this.gameContainer.style.pointerEvents = 'none'; + + setTimeout(() => { + this.complete(true, { + quality: quality, + rating: qualityRating + }); + }, 2000); + } + + showFailure(reason) { + // 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; + box-shadow: 0 0 20px rgba(255, 0, 0, 0.3); + `; + failureMessage.innerHTML = ` +
${reason}
+
Try again with more careful dusting.
+ `; + + this.container.appendChild(failureMessage); + + // Disable further interaction + this.isDragging = false; + this.gameContainer.style.pointerEvents = 'none'; + + setTimeout(() => { + this.complete(false); + }, 2000); + } + + generateFingerprint(difficulty) { + const pattern = this.difficultySettings[difficulty].pattern; + const numPrints = this.difficultySettings[difficulty].fingerprints; + const newFingerprintCells = new Set(); + const centerX = Math.floor(this.gridSize / 2); + const centerY = Math.floor(this.gridSize / 2); + + if (pattern === 'simple') { + // Simple oval-like pattern + for (let i = 0; i < numPrints; i++) { + const angle = (i / numPrints) * Math.PI * 2; + const distance = 5 + Math.random() * 3; + const x = Math.floor(centerX + Math.cos(angle) * distance); + const y = Math.floor(centerY + Math.sin(angle) * distance); + + if (x >= 0 && x < this.gridSize && y >= 0 && y < this.gridSize) { + newFingerprintCells.add(`${x},${y}`); + + // Add a few adjacent cells to make it less sparse + for (let j = 0; j < 2; j++) { + const nx = x + Math.floor(Math.random() * 3) - 1; + const ny = y + Math.floor(Math.random() * 3) - 1; + if (nx >= 0 && nx < this.gridSize && ny >= 0 && ny < this.gridSize) { + newFingerprintCells.add(`${nx},${ny}`); + } + } + } + } + } else if (pattern === 'medium') { + // Medium complexity - spiral pattern with variations + for (let i = 0; i < numPrints; i++) { + const t = i / numPrints * 5; + const distance = 2 + t * 0.8; + const noise = Math.random() * 2 - 1; + const x = Math.floor(centerX + Math.cos(t * Math.PI * 2) * (distance + noise)); + const y = Math.floor(centerY + Math.sin(t * Math.PI * 2) * (distance + noise)); + + if (x >= 0 && x < this.gridSize && y >= 0 && y < this.gridSize) { + newFingerprintCells.add(`${x},${y}`); + } + } + + // Add whorls and arches + for (let i = 0; i < 20; i++) { + const angle = (i / 20) * Math.PI * 2; + const distance = 7; + const x = Math.floor(centerX + Math.cos(angle) * distance); + const y = Math.floor(centerY + Math.sin(angle) * distance); + + if (x >= 0 && x < this.gridSize && y >= 0 && y < this.gridSize) { + newFingerprintCells.add(`${x},${y}`); + + // Add ridge-like pattern + for (let j = 1; j <= 3; j++) { + const ridgeX = Math.floor(centerX + Math.cos(angle) * (distance - j)); + const ridgeY = Math.floor(centerY + Math.sin(angle) * (distance - j)); + if (ridgeX >= 0 && ridgeX < this.gridSize && ridgeY >= 0 && ridgeY < this.gridSize) { + newFingerprintCells.add(`${ridgeX},${ridgeY}`); + } + } + } + } + } else { + // Complex pattern - detailed whorls and ridge patterns + for (let i = 0; i < numPrints; i++) { + // Main loop - create a complex whorl pattern + const t = i / numPrints * 8; + const distance = 2 + t * 0.6; + const noise = Math.sin(t * 5) * 1.5; + const x = Math.floor(centerX + Math.cos(t * Math.PI * 2) * (distance + noise)); + const y = Math.floor(centerY + Math.sin(t * Math.PI * 2) * (distance + noise)); + + if (x >= 0 && x < this.gridSize && y >= 0 && y < this.gridSize) { + newFingerprintCells.add(`${x},${y}`); + } + + // Add bifurcations and ridge endings + if (i % 5 === 0) { + const bifAngle = t * Math.PI * 2 + Math.PI/4; + const bx = Math.floor(x + Math.cos(bifAngle) * 1); + const by = Math.floor(y + Math.sin(bifAngle) * 1); + if (bx >= 0 && bx < this.gridSize && by >= 0 && by < this.gridSize) { + newFingerprintCells.add(`${bx},${by}`); + } + } + } + + // Add delta patterns + for (let d = 0; d < 3; d++) { + const deltaAngle = (d / 3) * Math.PI * 2; + const deltaX = Math.floor(centerX + Math.cos(deltaAngle) * 8); + const deltaY = Math.floor(centerY + Math.sin(deltaAngle) * 8); + + for (let r = 0; r < 5; r++) { + for (let a = 0; a < 3; a++) { + const rayAngle = deltaAngle + (a - 1) * Math.PI/4; + const rx = Math.floor(deltaX + Math.cos(rayAngle) * r); + const ry = Math.floor(deltaY + Math.sin(rayAngle) * r); + if (rx >= 0 && rx < this.gridSize && ry >= 0 && ry < this.gridSize) { + newFingerprintCells.add(`${rx},${ry}`); + } + } + } + } + } + + // Ensure we have at least the minimum number of cells + while (newFingerprintCells.size < numPrints) { + const x = centerX + Math.floor(Math.random() * 12 - 6); + const y = centerY + Math.floor(Math.random() * 12 - 6); + if (x >= 0 && x < this.gridSize && y >= 0 && y < this.gridSize) { + newFingerprintCells.add(`${x},${y}`); + } + } + + return newFingerprintCells; + } + } + + // Register the dusting minigame with the framework + MinigameFramework.registerScene('dusting', DustingMinigame); + + // Replacement for the startDustingMinigame function + function startDustingMinigame(item) { + // Initialize the framework if not already done + if (!MinigameFramework.mainGameScene) { + MinigameFramework.init(item.scene); + } + + // Start the dusting minigame + MinigameFramework.startMinigame('dusting', { + item: item, + scene: item.scene, + onComplete: (success, result) => { + if (success) { + debugLog('DUSTING SUCCESS', result, 1); + + // Add fingerprint to gameState + if (!gameState.biometricSamples) { + gameState.biometricSamples = []; + } + + const sample = { + id: generateFingerprintData(item), + type: 'fingerprint', + owner: item.scenarioData.fingerprintOwner, + quality: result.quality, // Quality between 0.7 and ~1.0 + data: generateFingerprintData(item), + timestamp: Date.now() + }; + + gameState.biometricSamples.push(sample); + + // Mark item as collected + if (item.scenarioData) { + item.scenarioData.hasFingerprint = false; + } + + // Update the biometrics panel and count + updateBiometricsPanel(); + updateBiometricsCount(); + + // Show notification + gameAlert(`Collected ${sample.owner}'s fingerprint sample (${result.rating} quality)`, 'success', 'Sample Acquired', 3000); + } else { + debugLog('DUSTING FAILED', null, 2); + gameAlert(`Failed to collect the fingerprint sample.`, 'error', 'Dusting Failed', 3000); + } + } + }); + } + + // Bluetooth scanner system +// ... existing code ... \ No newline at end of file From 5961a6801d75e38a10781d645a9258511267fdb0 Mon Sep 17 00:00:00 2001 From: Damian-I Date: Fri, 14 Mar 2025 01:22:13 +0000 Subject: [PATCH 19/24] Refactor minigame styles and structure to utilize framework classes for consistency and improved maintainability. Added new UI elements for success and failure messages, and updated header and button styles accordingly. --- index.html | 258 +++++++++++++++++++++++++++++------------------------ 1 file changed, 142 insertions(+), 116 deletions(-) diff --git a/index.html b/index.html index 065f38e..8456610 100644 --- a/index.html +++ b/index.html @@ -4204,24 +4204,27 @@ const style = document.createElement('style'); style.id = 'minigame-framework-styles'; style.textContent = ` + /* Framework base styles */ #minigame-container { - position: fixed; + position: fixed; top: 0; left: 0; width: 100%; height: 100%; - z-index: 1000; + z-index: 1000; display: none; + pointer-events: all; } #minigame-overlay { position: fixed; top: 0; left: 0; - width: 100%; + width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); z-index: 1001; + pointer-events: all; } .minigame-scene { @@ -4229,17 +4232,18 @@ top: 50%; left: 50%; transform: translate(-50%, -50%); - background: #222; + background: #222; color: white; border-radius: 10px; box-shadow: 0 0 20px rgba(0,0,0,0.5); z-index: 1002; - display: none; + pointer-events: all; } + /* Common minigame UI elements */ .minigame-close { background: #555; - color: white; + color: white; border: none; padding: 10px 20px; border-radius: 5px; @@ -4247,6 +4251,100 @@ margin-top: 10px; align-self: center; } + + .minigame-header { + position: absolute; + top: 0; + left: 0; + width: 100%; + background: rgba(30, 30, 30, 0.9); + padding: 10px; + display: flex; + flex-direction: column; + align-items: center; + z-index: 5; + } + + .minigame-header h3 { + margin: 0 0 8px 0; + color: #fff; + text-align: center; + } + + .minigame-header p { + margin: 5px 0; + color: #ccc; + text-align: center; + font-size: 12px; + } + + .minigame-success-message { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: rgba(0, 0, 0, 0.9); + padding: 20px; + border-radius: 5px; + color: #0f0; + font-size: 20px; + text-align: center; + z-index: 1003; + box-shadow: 0 0 20px rgba(0, 255, 0, 0.3); + } + + .minigame-failure-message { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: rgba(0, 0, 0, 0.9); + padding: 20px; + border-radius: 5px; + color: #f00; + font-size: 20px; + text-align: center; + z-index: 1003; + box-shadow: 0 0 20px rgba(255, 0, 0, 0.3); + } + + .minigame-tool-button { + background-color: #444; + color: white; + border: none; + border-radius: 3px; + padding: 5px 10px; + cursor: pointer; + opacity: 0.7; + transition: opacity 0.2s, background-color 0.2s; + } + + .minigame-tool-button.active { + opacity: 1; + } + + .minigame-progress-container { + height: 6px; + width: 100%; + background: #333; + border-radius: 3px; + overflow: hidden; + } + + .minigame-progress-bar { + height: 100%; + width: 0%; + background: #2ecc71; + transition: width 0.3s; + } + + .minigame-scene.success { + border: 2px solid #33cc33; + } + + .minigame-scene.failure { + border: 2px solid #cc3333; + } `; document.head.appendChild(style); } @@ -4398,7 +4496,7 @@ init() { super.init(); - // Apply styles + // Apply styles from the framework instead of inline this.container.style.width = '90%'; this.container.style.maxWidth = '500px'; this.container.style.padding = '20px'; @@ -4412,12 +4510,12 @@ style.id = 'lockpicking-styles'; style.textContent = ` .lock-visual { - display: flex; + display: flex; justify-content: center; align-items: flex-end; gap: 15px; height: 120px; - background: #333; + background: #333; border-radius: 5px; padding: 15px; position: relative; @@ -4425,8 +4523,8 @@ .pin { width: 40px; - height: 100px; - position: relative; + height: 100px; + position: relative; background: #444; border-radius: 4px 4px 0 0; overflow: visible; @@ -4448,17 +4546,17 @@ } .key-pin { - position: absolute; - bottom: 0; - width: 100%; + position: absolute; + bottom: 0; + width: 100%; height: 0px; background: #dd3333; transition: height 0.05s; } .driver-pin { - position: absolute; - width: 100%; + position: absolute; + width: 100%; height: 40px; background: #3355dd; transition: bottom 0.05s; @@ -4597,23 +4695,15 @@ .tension-control label { flex: 1; } - - .minigame-scene.success { - border: 2px solid #33cc33; - } - - .minigame-scene.failure { - border: 2px solid #cc3333; - } `; document.head.appendChild(style); } - // Create header with title and instructions + // Create header with title and instructions using the framework's header class const header = document.createElement('div'); - header.className = 'lockpick-header'; + header.className = 'minigame-header'; header.innerHTML = ` -

Lockpicking

+

Lockpicking

Apply tension and hold click on pins to lift them to the shear line

`; this.container.appendChild(header); @@ -4683,9 +4773,9 @@ // Only proceed if tension is applied if (!this.gameState.tensionApplied) { this.updateFeedback("Apply tension first by toggling the wrench"); - return; - } - + return; + } + // Start lifting the pin this.gameState.mouseDown = true; this.gameState.currentPin = pin; @@ -4942,7 +5032,7 @@ this.updateFeedback("Failed to pick the lock"); } - // Add close button + // Add close button using the framework's close button class const closeBtn = document.createElement('button'); closeBtn.textContent = "Close"; closeBtn.className = "minigame-close"; @@ -5718,47 +5808,19 @@ console.log("Dusting minigame container:", this.container); - // Fix: Apply proper display style (flex instead of relying on the framework) + // Set container to flex display via class instead of inline style + this.container.style.width = '75%'; + this.container.style.height = '75%'; + this.container.style.padding = '20px'; this.container.style.display = 'flex'; - - // Apply CSS styles to the container - this.container.style.cssText = ` - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 75%; - height: 75%; - background: rgba(0, 0, 0, 0.9); - border: 1px solid #444; - z-index: 1002; /* Important: Higher than the framework overlay */ - padding: 20px; - border-radius: 5px; - display: flex; - flex-direction: column; - overflow: hidden; - `; + this.container.style.flexDirection = 'column'; // Create header with instructions const header = document.createElement('div'); - header.style.cssText = ` - position: absolute; - top: 0; - left: 0; - width: 100%; - background: rgba(30, 30, 30, 0.9); - padding: 10px; - display: flex; - flex-direction: column; - align-items: center; - z-index: 5; - `; - + header.className = 'minigame-header'; header.innerHTML = ` -

Fingerprint Dusting

-

- Drag to dust the surface and reveal fingerprints. Avoid over-dusting! -

+

Fingerprint Dusting

+

Drag to dust the surface and reveal fingerprints. Avoid over-dusting!

`; // Create game container @@ -5834,25 +5896,15 @@ this.tools.forEach(tool => { const toolButton = document.createElement('button'); - toolButton.className = tool.name === this.currentTool.name ? 'tool-button active' : 'tool-button'; + toolButton.className = `minigame-tool-button ${tool.name === this.currentTool.name ? 'active' : ''}`; toolButton.textContent = tool.name; - toolButton.style.cssText = ` - background: ${tool.color}; - color: white; - border: none; - border-radius: 3px; - padding: 5px 10px; - cursor: pointer; - opacity: ${tool.name === this.currentTool.name ? '1' : '0.7'}; - `; + toolButton.style.backgroundColor = tool.color; toolButton.addEventListener('click', () => { - document.querySelectorAll('.tool-button').forEach(btn => { + document.querySelectorAll('.minigame-tool-button').forEach(btn => { btn.classList.remove('active'); - btn.style.opacity = '0.7'; }); toolButton.classList.add('active'); - toolButton.style.opacity = '1'; this.currentTool = tool; }); @@ -5944,9 +5996,6 @@ // Check initial progress this.checkProgress(); - - // Log to console that the game is initialized - console.log("Dusting minigame initialized"); } // Mouse event handlers @@ -6182,7 +6231,7 @@ } }); - // Update progress display + // Update progress display using the framework's progress bar classes this.progressText.innerHTML = `
Found: ${this.revealedPrints}/${this.requiredPrints} required prints @@ -6190,8 +6239,8 @@ Over-dusted: ${this.overDusted}/${this.difficultySettings[this.currentDifficulty].maxOverDusted} max
-
-
+
+
`; @@ -6215,22 +6264,9 @@ // Higher quality for more coverage and less over-dusting const quality = 0.7 + (coverageBonus * 0.25) - (dustPenalty * 0.15); - // Show success message + // Show success message using the framework's success message class 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; - box-shadow: 0 0 20px rgba(0, 255, 0, 0.3); - `; + successMessage.className = 'minigame-success-message'; const qualityPercentage = Math.round(quality * 100); const qualityRating = qualityPercentage >= 95 ? 'Perfect' : @@ -6248,6 +6284,7 @@ `; this.container.appendChild(successMessage); + this.container.classList.add('success'); // Disable further interaction this.isDragging = false; @@ -6262,28 +6299,17 @@ } showFailure(reason) { - // Show failure message + // Show failure message using the framework's failure message class 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; - box-shadow: 0 0 20px rgba(255, 0, 0, 0.3); - `; + failureMessage.className = 'minigame-failure-message'; + failureMessage.innerHTML = `
${reason}
Try again with more careful dusting.
`; this.container.appendChild(failureMessage); + this.container.classList.add('failure'); // Disable further interaction this.isDragging = false; From cff5f7d3ff5549a5a5f01aa35e43481dcf23ad82 Mon Sep 17 00:00:00 2001 From: Damian-I Date: Fri, 14 Mar 2025 01:33:41 +0000 Subject: [PATCH 20/24] Refactor minigame framework to enhance game state management and event handling. Introduced a unified game state object for better tracking of game status, mouse interactions, and progress updates. Updated lockpicking and dusting minigames to utilize the new structure, improving maintainability and consistency across UI elements. --- index.html | 1007 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 636 insertions(+), 371 deletions(-) diff --git a/index.html b/index.html index 8456610..9ad4cdd 100644 --- a/index.html +++ b/index.html @@ -4440,36 +4440,364 @@ constructor(container, params = {}) { this.container = container; this.params = params; - this.isActive = false; + + // Common game state management + this.gameState = { + isActive: false, + isDragging: false, + mousePosition: { x: 0, y: 0 }, + mouseDown: false, + mouseButtonsPressed: [false, false, false], + keyState: {}, + success: false, + gameComplete: false, + startTime: 0, + elapsedTime: 0 + }; + + // Bind event handlers to the instance + this._handleMouseDown = this._handleMouseDown.bind(this); + this._handleMouseUp = this._handleMouseUp.bind(this); + this._handleMouseMove = this._handleMouseMove.bind(this); + this._handleMouseLeave = this._handleMouseLeave.bind(this); + this._handleKeyDown = this._handleKeyDown.bind(this); + this._handleKeyUp = this._handleKeyUp.bind(this); + + // Store event listeners for cleanup + this._eventListeners = []; } init() { - // Show the scene container + console.log(`Initializing minigame scene`); + + // Ensure the scene container is visible this.container.style.display = 'flex'; + // Create standard layout elements + this.createLayout(); + // Add escape key handler this.escHandler = (e) => { - if (e.key === 'Escape' && this.isActive) { + if (e.key === 'Escape' && this.gameState.isActive) { this.complete(false); } }; document.addEventListener('keydown', this.escHandler); + + // Setup common event handling + this.setupEventHandling(); + + // Record start time + this.gameState.startTime = Date.now(); + } + + // Create standard layout for minigames + createLayout() { + // Create and add the header + this.headerElement = document.createElement('div'); + this.headerElement.className = 'minigame-header'; + this.container.appendChild(this.headerElement); + + // Create the main game container + this.gameContainer = document.createElement('div'); + this.gameContainer.className = 'minigame-game-container'; + this.gameContainer.style.cssText = ` + width: 80%; + height: 80%; + margin: 70px auto 20px auto; + background: #1a1a1a; + border-radius: 5px; + box-shadow: 0 0 15px rgba(0, 0, 0, 0.5) inset; + position: relative; + overflow: hidden; + `; + this.container.appendChild(this.gameContainer); + + // Create message container for notifications, success/failure + this.messageContainer = document.createElement('div'); + this.messageContainer.className = 'minigame-message-container'; + this.messageContainer.style.cssText = ` + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 1000; + `; + this.container.appendChild(this.messageContainer); + + // Create progress display + this.progressContainer = document.createElement('div'); + this.progressContainer.style.cssText = ` + position: absolute; + bottom: 15px; + left: 50%; + transform: translateX(-50%); + color: white; + text-align: center; + font-size: 16px; + background: rgba(0, 0, 0, 0.6); + padding: 5px 15px; + border-radius: 15px; + z-index: 10; + width: 80%; + max-width: 500px; + `; + this.container.appendChild(this.progressContainer); + + // Create standard close button + const closeButton = document.createElement('button'); + closeButton.textContent = '✕'; + closeButton.style.cssText = ` + position: absolute; + right: 10px; + top: 10px; + background: none; + border: none; + color: white; + font-size: 20px; + cursor: pointer; + z-index: 100; + `; + closeButton.onclick = () => this.complete(false); + this.container.appendChild(closeButton); + } + + // Set up common event handling for mouse, touch, keyboard + setupEventHandling() { + // Mouse events + this.addEventListenerWithCleanup(this.gameContainer, 'mousedown', this._handleMouseDown); + this.addEventListenerWithCleanup(document, 'mouseup', this._handleMouseUp); + this.addEventListenerWithCleanup(this.gameContainer, 'mousemove', this._handleMouseMove); + this.addEventListenerWithCleanup(this.gameContainer, 'mouseleave', this._handleMouseLeave); + + // Keyboard events + this.addEventListenerWithCleanup(document, 'keydown', this._handleKeyDown); + this.addEventListenerWithCleanup(document, 'keyup', this._handleKeyUp); + + // Prevent context menu + this.addEventListenerWithCleanup(this.gameContainer, 'contextmenu', (e) => e.preventDefault()); + } + + // Utility to add event listener and track it for cleanup + addEventListenerWithCleanup(element, eventType, handler, options) { + element.addEventListener(eventType, handler, options); + this._eventListeners.push({ element, eventType, handler }); + } + + // Event handlers + _handleMouseDown(e) { + console.log("Mouse down in framework handler"); + this.gameState.mouseDown = true; + this.gameState.mouseButtonsPressed[e.button] = true; + this.gameState.isDragging = true; + + // Call subclass handler if it exists + if (typeof this.handleMouseDown === 'function') { + this.handleMouseDown(e); + } + } + + _handleMouseUp(e) { + console.log("Mouse up in framework handler"); + this.gameState.mouseDown = false; + this.gameState.mouseButtonsPressed[e.button] = false; + this.gameState.isDragging = false; + + // Call subclass handler if it exists + if (typeof this.handleMouseUp === 'function') { + this.handleMouseUp(e); + } + } + + _handleMouseMove(e) { + // Update mouse position + const rect = this.gameContainer.getBoundingClientRect(); + this.gameState.mousePosition = { + x: e.clientX - rect.left, + y: e.clientY - rect.top, + clientX: e.clientX, + clientY: e.clientY, + screenX: e.screenX, + screenY: e.screenY + }; + + // Call subclass handler if it exists + if (typeof this.handleMouseMove === 'function') { + this.handleMouseMove(e); + } + } + + _handleMouseLeave(e) { + this.gameState.isDragging = false; + + // Call subclass handler if it exists + if (typeof this.handleMouseLeave === 'function') { + this.handleMouseLeave(e); + } + } + + _handleKeyDown(e) { + this.gameState.keyState[e.code] = true; + + // Call subclass handler if it exists + if (typeof this.handleKeyDown === 'function') { + this.handleKeyDown(e); + } + } + + _handleKeyUp(e) { + this.gameState.keyState[e.code] = false; + + // Call subclass handler if it exists + if (typeof this.handleKeyUp === 'function') { + this.handleKeyUp(e); + } + } + + // Message display system + showMessage(message, type = 'info', duration = 0) { + // Create a message element + const messageElement = document.createElement('div'); + messageElement.className = `minigame-message minigame-message-${type}`; + messageElement.innerHTML = message; + + // Style based on type + let styles = ` + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + padding: 20px; + border-radius: 5px; + text-align: center; + z-index: 1001; + pointer-events: all; + max-width: 90%; + transition: opacity 0.3s; + `; + + switch(type) { + case 'success': + messageElement.className = 'minigame-success-message'; + break; + case 'failure': + case 'error': + messageElement.className = 'minigame-failure-message'; + break; + default: + styles += ` + background: rgba(0, 0, 0, 0.8); + color: white; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); + `; + } + + // Apply styles + messageElement.style.cssText = styles; + + // Add to the message container + this.messageContainer.appendChild(messageElement); + + // Auto-remove after duration, if specified + if (duration > 0) { + setTimeout(() => { + messageElement.style.opacity = '0'; + setTimeout(() => { + if (messageElement.parentNode) { + messageElement.parentNode.removeChild(messageElement); + } + }, 300); + }, duration); + } + + return messageElement; + } + + // Success/failure methods + showSuccess(message, autoComplete = true, delay = 2000) { + this.gameState.success = true; + this.gameState.gameComplete = true; + this.container.classList.add('success'); + + // Create success message + const successMessage = this.showMessage(message, 'success'); + + // Auto-complete after delay + if (autoComplete) { + setTimeout(() => { + this.complete(true); + }, delay); + } + + return successMessage; + } + + showFailure(message, autoComplete = true, delay = 2000) { + this.gameState.success = false; + this.gameState.gameComplete = true; + this.container.classList.add('failure'); + + // Create failure message + const failureMessage = this.showMessage(message, 'failure'); + + // Auto-complete after delay + if (autoComplete) { + setTimeout(() => { + this.complete(false); + }, delay); + } + + return failureMessage; + } + + // Progress updates + updateProgress(current, total, label = '') { + const percentage = Math.min(100, Math.max(0, (current / total) * 100)); + + this.progressContainer.innerHTML = ` + ${label ? `
${label}
` : ''} +
+
+
+ `; } start() { - this.isActive = true; + this.gameState.isActive = true; + console.log("Minigame started"); + } + + update(deltaTime) { + // Update elapsed time + this.gameState.elapsedTime = Date.now() - this.gameState.startTime; + + // Override in subclass for game-specific updates } complete(success, result = null) { - this.isActive = false; + console.log(`Minigame complete, success: ${success}`); + this.gameState.isActive = false; + this.gameState.success = success; + + // Remove event listeners document.removeEventListener('keydown', this.escHandler); + this.cleanup(); + if (typeof this.params.onComplete === 'function') { this.params.onComplete(success, result); } } cleanup() { - // Cleanup resources + console.log("Cleaning up minigame"); + + // Remove all tracked event listeners + this._eventListeners.forEach(({ element, eventType, handler }) => { + element.removeEventListener(eventType, handler); + }); + this._eventListeners = []; } } }; @@ -4484,27 +4812,34 @@ this.pinCount = this.difficulty === 'easy' ? 3 : this.difficulty === 'medium' ? 4 : 5; this.pins = []; - this.gameState = { + + // Use gameState from the framework but extend it with lockpicking-specific properties + this.lockState = { tensionApplied: false, - gameActive: false, pinsSet: 0, - currentPin: null, - mouseDown: false + currentPin: null }; } init() { + // Call parent init to set up common components super.init(); - // Apply styles from the framework instead of inline + console.log("Lockpicking minigame initializing"); + + // Configure container size and layout this.container.style.width = '90%'; this.container.style.maxWidth = '500px'; this.container.style.padding = '20px'; - this.container.style.display = 'flex'; - this.container.style.flexDirection = 'column'; this.container.style.gap = '15px'; - // Add styles if they don't exist + // Set up header content + this.headerElement.innerHTML = ` +

Lockpicking

+

Apply tension and hold click on pins to lift them to the shear line

+ `; + + // Add custom styles for the lockpicking minigame if they don't exist if (!document.getElementById('lockpicking-styles')) { const style = document.createElement('style'); style.id = 'lockpicking-styles'; @@ -4596,60 +4931,6 @@ height: 39px; /* Just below shear line */ } - .tension-toggle { - display: inline-block; - position: relative; - width: 60px; - height: 34px; - margin: 10px; - } - - .tension-toggle input { - opacity: 0; - width: 0; - height: 0; - } - - .tension-slider { - position: absolute; - cursor: pointer; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: #ccc; - -webkit-transition: .4s; - transition: .4s; - border-radius: 34px; - } - - .tension-slider:before { - position: absolute; - content: ""; - height: 26px; - width: 26px; - left: 4px; - bottom: 4px; - background-color: white; - -webkit-transition: .4s; - transition: .4s; - border-radius: 50%; - } - - input:checked + .tension-slider { - background-color: #2196F3; - } - - input:focus + .tension-slider { - box-shadow: 0 0 1px #2196F3; - } - - input:checked + .tension-slider:before { - -webkit-transform: translateX(26px); - -ms-transform: translateX(26px); - transform: translateX(26px); - } - .cylinder { display: flex; justify-content: center; @@ -4695,31 +4976,116 @@ .tension-control label { flex: 1; } + + .tension-wrench { + width: 50px; + height: 30px; + background: #666; + border-radius: 4px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + margin-right: 10px; + transition: transform 0.2s; + } + + .tension-wrench:hover { + background: #777; + } + + .tension-wrench.active { + background: #2196F3; + transform: rotate(-5deg); + } `; document.head.appendChild(style); } - - // Create header with title and instructions using the framework's header class - const header = document.createElement('div'); - header.className = 'minigame-header'; - header.innerHTML = ` -

Lockpicking

-

Apply tension and hold click on pins to lift them to the shear line

- `; - this.container.appendChild(header); - // Create the lock cylinder visualization + // Replace the game container with custom lockpicking interface + this.setupLockpickingInterface(); + + // Create pins with random binding order + this.createPins(); + + // Update the progress display with lockpicking instructions + this.updateFeedback("Apply tension first, then click and hold on pins to lift them"); + } + + setupLockpickingInterface() { + // Remove default game container (we'll use custom layout) + if (this.gameContainer.parentNode) { + this.gameContainer.parentNode.removeChild(this.gameContainer); + } + + // Create the lock visual container const lockVisual = document.createElement('div'); lockVisual.className = 'lock-visual'; this.container.appendChild(lockVisual); + this.lockVisual = lockVisual; // Create the cylinder that will rotate when tension is applied const cylinder = document.createElement('div'); cylinder.className = 'cylinder'; cylinder.innerHTML = '
'; this.container.appendChild(cylinder); + this.cylinder = cylinder; - // Create pins with random binding order + // Add tension toggle control + const tensionControl = document.createElement('div'); + tensionControl.className = 'tension-control'; + tensionControl.innerHTML = ` +
+
+
+
+ Click wrench to apply tension + `; + this.container.appendChild(tensionControl); + + // Feedback area + const feedback = document.createElement('div'); + feedback.className = 'lockpick-feedback'; + this.container.appendChild(feedback); + this.feedback = feedback; + + // Set up tension wrench interaction + const tensionWrench = tensionControl.querySelector('.tension-wrench'); + const tensionStatus = tensionControl.querySelector('.tension-status'); + + tensionWrench.addEventListener('click', () => { + this.lockState.tensionApplied = !this.lockState.tensionApplied; + tensionWrench.classList.toggle('active', this.lockState.tensionApplied); + cylinder.classList.toggle('rotated', this.lockState.tensionApplied); + + // Update status text + tensionStatus.textContent = this.lockState.tensionApplied ? + 'Tension applied' : 'Click wrench to apply tension'; + + // Update which pins are binding + this.updatePinBindings(); + + // If tension is toggled off, reset any unset pins + if (!this.lockState.tensionApplied) { + this.pins.forEach(pin => { + if (!pin.isSet) { + pin.currentHeight = 0; + this.updatePinVisual(pin); + } + }); + this.updateFeedback("Tension released - apply tension before lifting pins"); + } else { + this.updateFeedback("Tension applied - click and hold on pins to lift them"); + } + }); + + // Store references + this.tensionWrench = tensionWrench; + this.tensionStatus = tensionStatus; + } + + createPins() { + // Generate random binding order const bindingOrder = this.shuffleArray([...Array(this.pinCount).keys()]); for (let i = 0; i < this.pinCount; i++) { @@ -4727,7 +5093,7 @@ const pinElement = document.createElement('div'); pinElement.className = 'pin'; pinElement.dataset.index = i; - lockVisual.appendChild(pinElement); + this.lockVisual.appendChild(pinElement); // Create shear line const shearLine = document.createElement('div'); @@ -4766,99 +5132,48 @@ this.pins.push(pin); - // Mouse down event - start lifting pin - pinElement.addEventListener('mousedown', (e) => { - if (!this.gameState.gameActive || pin.isSet) return; - - // Only proceed if tension is applied - if (!this.gameState.tensionApplied) { - this.updateFeedback("Apply tension first by toggling the wrench"); - return; - } - - // Start lifting the pin - this.gameState.mouseDown = true; - this.gameState.currentPin = pin; - this.liftPin(); - - // Prevent text selection - e.preventDefault(); + // Add custom pin click handler + this.addEventListenerWithCleanup(pinElement, 'mousedown', (e) => { + this.handlePinMouseDown(pin, e); }); } - - // Mouse up event - stop lifting pin - document.addEventListener('mouseup', () => { - this.gameState.mouseDown = false; - - // If we were in the process of lifting a pin, check if it sets or drops - if (this.gameState.currentPin) { - this.checkPinSet(this.gameState.currentPin); - this.gameState.currentPin = null; - } - }); - - // Add tension toggle - const tensionControl = document.createElement('div'); - tensionControl.className = 'tension-control'; - tensionControl.innerHTML = ` -
-
-
-
- Click wrench to apply tension - `; - this.container.appendChild(tensionControl); - - // Feedback area - this.feedback = document.createElement('div'); - this.feedback.className = 'lockpick-feedback'; - this.feedback.textContent = 'Apply tension first, then click and hold on pins to lift them'; - this.container.appendChild(this.feedback); - - // Tension toggle event - const tensionWrench = tensionControl.querySelector('.tension-wrench'); - const tensionStatus = tensionControl.querySelector('.tension-status'); - - tensionWrench.addEventListener('click', () => { - this.gameState.tensionApplied = !this.gameState.tensionApplied; - tensionWrench.classList.toggle('active', this.gameState.tensionApplied); - cylinder.classList.toggle('rotated', this.gameState.tensionApplied); - - // Update status text - tensionStatus.textContent = this.gameState.tensionApplied ? - 'Tension applied' : 'Click wrench to apply tension'; - - // Update which pins are binding - this.updatePinBindings(); - - // If tension is toggled off, reset any unset pins - if (!this.gameState.tensionApplied) { - this.pins.forEach(pin => { - if (!pin.isSet) { - pin.currentHeight = 0; - this.updatePinVisual(pin); - } - }); - this.updateFeedback("Tension released - apply tension before lifting pins"); - } else { - this.updateFeedback("Tension applied - click and hold on pins to lift them"); - } - }); } - start() { - super.start(); - this.gameState.gameActive = true; - } - - // Continuously lift the current pin while mouse is down - liftPin() { - if (!this.gameState.mouseDown || !this.gameState.currentPin || - !this.gameState.gameActive || !this.gameState.tensionApplied) { + // Custom pin mouse down handler + handlePinMouseDown(pin, e) { + if (!this.gameState.isActive || pin.isSet) return; + + // Only proceed if tension is applied + if (!this.lockState.tensionApplied) { + this.updateFeedback("Apply tension first by toggling the wrench"); return; } - const pin = this.gameState.currentPin; + // Start lifting the pin + this.lockState.currentPin = pin; + this.liftPin(); + + // Prevent text selection + e.preventDefault(); + } + + // Override framework's mouse event handlers + handleMouseUp(e) { + // If we were in the process of lifting a pin, check if it sets or drops + if (this.lockState.currentPin) { + this.checkPinSet(this.lockState.currentPin); + this.lockState.currentPin = null; + } + } + + // Pin-lifting logic + liftPin() { + if (!this.gameState.mouseDown || !this.lockState.currentPin || + !this.gameState.isActive || !this.lockState.tensionApplied) { + return; + } + + const pin = this.lockState.currentPin; // Only binding pins can be lifted effectively if (!this.shouldPinBind(pin)) { @@ -4884,7 +5199,7 @@ // Check if a pin should be set or dropped checkPinSet(pin) { - if (!this.gameState.tensionApplied || !this.shouldPinBind(pin)) { + if (!this.lockState.tensionApplied || !this.shouldPinBind(pin)) { // If no tension or not binding, the pin drops this.dropPin(pin); return; @@ -4896,13 +5211,16 @@ if (heightDiff < 0.1) { // Pin set successfully! pin.isSet = true; - this.gameState.pinsSet++; - this.updateFeedback(`Pin set at the shear line! (${this.gameState.pinsSet}/${this.pinCount})`); + this.lockState.pinsSet++; + this.updateFeedback(`Pin set at the shear line! (${this.lockState.pinsSet}/${this.pinCount})`); this.updatePinVisual(pin); + // Update progress + this.updateProgress(this.lockState.pinsSet, this.pinCount); + // Check if all pins are set - if (this.gameState.pinsSet === this.pinCount) { - this.endGame(true); + if (this.lockState.pinsSet === this.pinCount) { + this.lockPickingSuccess(); return; } @@ -4950,7 +5268,7 @@ // Update which pins are binding based on binding order updatePinBindings() { - if (!this.gameState.tensionApplied) { + if (!this.lockState.tensionApplied) { // No binding if no tension this.pins.forEach(pin => { pin.elements.container.classList.remove('binding'); @@ -4983,7 +5301,7 @@ // Check if a pin should bind based on binding order shouldPinBind(pin) { - if (!this.gameState.tensionApplied) return false; + if (!this.lockState.tensionApplied) return false; // Find the next unset pin in binding order for (let order = 0; order < this.pinCount; order++) { @@ -4994,59 +5312,86 @@ } return false; } - + + // Update feedback text updateFeedback(message) { this.feedback.textContent = message; } - endGame(success) { - this.gameState.gameActive = false; + // Handle successful lockpicking + lockPickingSuccess() { + // Disable game interaction + this.gameState.isActive = false; - // Remove mouse event listeners - document.removeEventListener('mouseup', this.mouseUpHandler); + // Update UI + this.updateFeedback("Lock picked successfully!"); - if (success) { - this.container.classList.add('success'); - this.updateFeedback("Lock picked successfully!"); + // Unlock the object in the game + if (this.lockable) { + // Set locked to false + this.lockable.locked = false; - // Unlock the object in the game - if (this.lockable) { - // Set locked to false - this is the crucial part - this.lockable.locked = false; - - // If it's a scenarioData object, also update that property - if (this.lockable.scenarioData) { - this.lockable.scenarioData.locked = false; - } - - // Log successful unlock - if (typeof debugLog === 'function') { - debugLog('LOCKPICK UNLOCK', { - object: this.lockable, - success: true - }, 1); - } + // If it's a scenarioData object, also update that property + if (this.lockable.scenarioData) { + this.lockable.scenarioData.locked = false; + } + + // Log successful unlock + if (typeof debugLog === 'function') { + debugLog('LOCKPICK UNLOCK', { + object: this.lockable, + success: true + }, 1); } - } else { - this.container.classList.add('failure'); - this.updateFeedback("Failed to pick the lock"); } - // Add close button using the framework's close button class - const closeBtn = document.createElement('button'); - closeBtn.textContent = "Close"; - closeBtn.className = "minigame-close"; - closeBtn.onclick = () => { - this.complete(success, { lockable: this.lockable }); - }; - this.container.appendChild(closeBtn); + // Show success message + const successHTML = ` +
Lock picked successfully!
+
All pins set at the shear line
+
+ Difficulty: ${this.difficulty.charAt(0).toUpperCase() + this.difficulty.slice(1)}
+ Pins: ${this.pinCount} +
+ `; + + // Use the framework's success message + this.showSuccess(successHTML, true, 2000); + + // Store lockable for the result + this.gameResult = { lockable: this.lockable }; } - cleanup() { - // Remove any event listeners - document.removeEventListener('mouseup', this.mouseUpHandler); + lockPickingFailure() { + // Show failure message + const failureHTML = ` +
Failed to pick the lock
+
Try again with more careful pin manipulation
+ `; + + // Use the framework's failure message + this.showFailure(failureHTML, true, 2000); } + start() { + super.start(); + console.log("Lockpicking minigame started"); + + // Initialize game state + this.gameState.isActive = true; + this.lockState.tensionApplied = false; + this.lockState.pinsSet = 0; + + // Initialize progress + this.updateProgress(0, this.pinCount); + } + + complete(success) { + // Call parent complete with result + super.complete(success, this.gameResult); + } + + // Utility function to shuffle an array shuffleArray(array) { for (let i = array.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); @@ -5764,7 +6109,7 @@ this.item = params.item; - // Game state variables + // Game state variables - using framework's gameState as base this.difficultySettings = { easy: { requiredCoverage: 0.3, // 30% of prints @@ -5791,7 +6136,6 @@ this.fingerprintCells = new Set(); this.revealedPrints = 0; this.overDusted = 0; - this.isDragging = false; this.lastDustTime = {}; // Tools configuration @@ -5804,28 +6148,24 @@ } init() { + // Call parent init to set up common components super.init(); - console.log("Dusting minigame container:", this.container); + console.log("Dusting minigame initializing"); - // Set container to flex display via class instead of inline style + // Set container dimensions this.container.style.width = '75%'; this.container.style.height = '75%'; this.container.style.padding = '20px'; - this.container.style.display = 'flex'; - this.container.style.flexDirection = 'column'; - // Create header with instructions - const header = document.createElement('div'); - header.className = 'minigame-header'; - header.innerHTML = ` + // Set up header content + this.headerElement.innerHTML = `

Fingerprint Dusting

Drag to dust the surface and reveal fingerprints. Avoid over-dusting!

`; - // Create game container - const gameContainer = document.createElement('div'); - gameContainer.style.cssText = ` + // Configure game container + this.gameContainer.style.cssText = ` width: 80%; height: 80%; max-width: 600px; @@ -5861,25 +6201,7 @@ const svgGrid = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' fill='%23111'/%3E%3Cpath d='M0 50h100M50 0v100' stroke='%23222' stroke-width='0.5'/%3E%3Cpath d='M25 0v100M75 0v100M0 25h100M0 75h100' stroke='%23191919' stroke-width='0.3'/%3E%3C/svg%3E`; gridBackground.style.backgroundImage = `url('${svgGrid}')`; - gameContainer.appendChild(gridBackground); - - // Add progress display - const progressText = document.createElement('div'); - progressText.style.cssText = ` - position: absolute; - bottom: 15px; - left: 50%; - transform: translateX(-50%); - color: white; - text-align: center; - font-size: 16px; - background: rgba(0, 0, 0, 0.6); - padding: 5px 15px; - border-radius: 15px; - z-index: 10; - width: 80%; - max-width: 500px; - `; + this.gameContainer.appendChild(gridBackground); // Add tool selection const toolsContainer = document.createElement('div'); @@ -5910,13 +6232,11 @@ toolsContainer.appendChild(toolButton); }); - - // Generate fingerprint pattern - this.fingerprintCells = this.generateFingerprint(this.currentDifficulty); + this.container.appendChild(toolsContainer); // Create particle container for dust effects - const particleContainer = document.createElement('div'); - particleContainer.style.cssText = ` + this.particleContainer = document.createElement('div'); + this.particleContainer.style.cssText = ` position: absolute; top: 0; left: 0; @@ -5926,11 +6246,27 @@ z-index: 5; overflow: hidden; `; + this.container.appendChild(this.particleContainer); + + // Generate fingerprint pattern and set up cells + this.fingerprintCells = this.generateFingerprint(this.currentDifficulty); + this.setupGrid(); // Total prints and required prints calculations this.totalPrints = this.fingerprintCells.size; this.requiredPrints = Math.ceil(this.totalPrints * this.difficultySettings[this.currentDifficulty].requiredCoverage); + // Check initial progress + this.checkProgress(); + } + + // Set up the grid of cells + setupGrid() { + // Clear any existing cells + while (this.gameContainer.firstChild) { + this.gameContainer.removeChild(this.gameContainer.firstChild); + } + // Create grid cells for (let y = 0; y < this.gridSize; y++) { for (let x = 0; x < this.gridSize; x++) { @@ -5948,78 +6284,14 @@ cell.dataset.dustLevel = '0'; cell.dataset.hasFingerprint = this.fingerprintCells.has(`${x},${y}`) ? 'true' : 'false'; - gameContainer.appendChild(cell); + this.gameContainer.appendChild(cell); } } - - // Store references to elements we'll need to update - this.gameContainer = gameContainer; - this.progressText = progressText; - this.particleContainer = particleContainer; - - // Add dragging interaction - bind these to the instance - this.mouseDownHandler = this.handleMouseDown.bind(this); - this.mouseUpHandler = this.handleMouseUp.bind(this); - this.mouseLeaveHandler = this.handleMouseLeave.bind(this); - this.mouseMoveHandler = this.handleMouseMove.bind(this); - - gameContainer.addEventListener('mousedown', this.mouseDownHandler); - document.addEventListener('mouseup', this.mouseUpHandler); - gameContainer.addEventListener('mouseleave', this.mouseLeaveHandler); - gameContainer.addEventListener('mousemove', this.mouseMoveHandler); - - // 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; - z-index: 10; - `; - closeButton.onclick = () => { - this.complete(false); - }; - - // Assemble the interface - this.container.appendChild(closeButton); - this.container.appendChild(header); - this.container.appendChild(gameContainer); - this.container.appendChild(particleContainer); - this.container.appendChild(progressText); - this.container.appendChild(toolsContainer); - - // Check initial progress - this.checkProgress(); - } - - // Mouse event handlers - handleMouseDown(e) { - console.log("Mouse down"); - this.isDragging = true; - // Immediately start dusting at this position - this.handleMouseMove(e); - } - - handleMouseUp() { - console.log("Mouse up"); - this.isDragging = false; - } - - handleMouseLeave() { - console.log("Mouse leave"); - this.isDragging = false; } + // Override the framework's mouse event handlers handleMouseMove(e) { - if (!this.isDragging) return; - - console.log("Mouse move while dragging"); + if (!this.gameState.isDragging) return; // Get the cell element under the cursor const cell = document.elementFromPoint(e.clientX, e.clientY); @@ -6116,30 +6388,12 @@ this.checkProgress(); } - start() { - super.start(); - console.log("Dusting minigame started"); - // Disable game movement - if (this.params.scene) { - this.params.scene.input.mouse.enabled = false; - } + // Use the framework's mouseDown handler directly + handleMouseDown(e) { + // Just start dusting immediately + this.handleMouseMove(e); } - - cleanup() { - // Remove event listeners - this.gameContainer.removeEventListener('mousedown', this.mouseDownHandler); - document.removeEventListener('mouseup', this.mouseUpHandler); - this.gameContainer.removeEventListener('mouseleave', this.mouseLeaveHandler); - this.gameContainer.removeEventListener('mousemove', this.mouseMoveHandler); - - // Re-enable game movement - if (this.params.scene) { - this.params.scene.input.mouse.enabled = true; - } - - console.log("Dusting minigame cleaned up"); - } - + createDustParticles(x, y, intensity, color) { const numParticles = Math.floor(5 + intensity * 5); // 5-10 particles based on intensity @@ -6231,8 +6485,8 @@ } }); - // Update progress display using the framework's progress bar classes - this.progressText.innerHTML = ` + // Update progress display + this.progressContainer.innerHTML = `
Found: ${this.revealedPrints}/${this.requiredPrints} required prints @@ -6246,34 +6500,30 @@ // Check fail condition first if (this.overDusted >= this.difficultySettings[this.currentDifficulty].maxOverDusted) { - this.showFailure("Too many over-dusted areas!"); + this.showFinalFailure("Too many over-dusted areas!"); return; } // Check win condition if (this.revealedPrints >= this.requiredPrints) { - this.showSuccess(); + this.showFinalSuccess(); } } - showSuccess() { + showFinalSuccess() { // Calculate quality based on dusting precision const dustPenalty = this.overDusted / this.difficultySettings[this.currentDifficulty].maxOverDusted; // 0-1 const coverageBonus = this.revealedPrints / this.totalPrints; // 0-1 // Higher quality for more coverage and less over-dusting const quality = 0.7 + (coverageBonus * 0.25) - (dustPenalty * 0.15); - - // Show success message using the framework's success message class - const successMessage = document.createElement('div'); - successMessage.className = 'minigame-success-message'; - const qualityPercentage = Math.round(quality * 100); const qualityRating = qualityPercentage >= 95 ? 'Perfect' : qualityPercentage >= 85 ? 'Excellent' : qualityPercentage >= 75 ? 'Good' : 'Acceptable'; - successMessage.innerHTML = ` + // Build success message with detailed stats + const successHTML = `
Fingerprint successfully collected!
Quality: ${qualityRating} (${qualityPercentage}%)
@@ -6283,44 +6533,50 @@
`; - this.container.appendChild(successMessage); - this.container.classList.add('success'); + // Use the framework's success message system + this.showSuccess(successHTML, true, 2000); // Disable further interaction - this.isDragging = false; this.gameContainer.style.pointerEvents = 'none'; - setTimeout(() => { - this.complete(true, { - quality: quality, - rating: qualityRating - }); - }, 2000); + // Store result for onComplete callback + this.gameResult = { + quality: quality, + rating: qualityRating + }; } - showFailure(reason) { - // Show failure message using the framework's failure message class - const failureMessage = document.createElement('div'); - failureMessage.className = 'minigame-failure-message'; - - failureMessage.innerHTML = ` + showFinalFailure(reason) { + // Build failure message + const failureHTML = `
${reason}
Try again with more careful dusting.
`; - this.container.appendChild(failureMessage); - this.container.classList.add('failure'); + // Use the framework's failure message system + this.showFailure(failureHTML, true, 2000); // Disable further interaction - this.isDragging = false; this.gameContainer.style.pointerEvents = 'none'; + } + + start() { + super.start(); + console.log("Dusting minigame started"); - setTimeout(() => { - this.complete(false); - }, 2000); + // Disable game movement in the main scene + if (this.params.scene) { + this.params.scene.input.mouse.enabled = false; + } + } + + complete(success) { + // Call parent complete with result + super.complete(success, this.gameResult); } generateFingerprint(difficulty) { + // Existing fingerprint generation logic remains the same const pattern = this.difficultySettings[difficulty].pattern; const numPrints = this.difficultySettings[difficulty].fingerprints; const newFingerprintCells = new Set(); @@ -6437,6 +6693,15 @@ return newFingerprintCells; } + + cleanup() { + super.cleanup(); + + // Re-enable game movement + if (this.params.scene) { + this.params.scene.input.mouse.enabled = true; + } + } } // Register the dusting minigame with the framework From 34d7aad58f65fbb169e7f1280b5a5d72bb2cf4ed Mon Sep 17 00:00:00 2001 From: Damian-I Date: Fri, 14 Mar 2025 02:07:10 +0000 Subject: [PATCH 21/24] Adjusted some alerts --- index.html | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/index.html b/index.html index 9ad4cdd..a926073 100644 --- a/index.html +++ b/index.html @@ -1560,8 +1560,8 @@ this.load.image('fingerprint_kit', 'assets/objects/fingerprint_kit.png'); this.load.image('lockpick', 'assets/objects/lockpick.png'); - this.load.json('gameScenarioJSON', 'assets/scenarios/biometric_breach.json'); - //this.load.json('gameScenarioJSON', 'assets/scenarios/ceo_exfil.json'); + //this.load.json('gameScenarioJSON', 'assets/scenarios/biometric_breach.json'); + this.load.json('gameScenarioJSON', 'assets/scenarios/ceo_exfil.json'); gameScenario = this.cache.json.get('gameScenarioJSON'); } @@ -2860,7 +2860,7 @@ } // Show notification instead of alert - gameAlert(message, 'info', data.name, 7000); + gameAlert(message, 'info', data.name, 0); } // adds an item to the inventory @@ -6756,9 +6756,6 @@ } }); } - - // Bluetooth scanner system -// ... existing code ... \ No newline at end of file From c2cbc4ec7305e714b6656043a929232a5fdd8a8d Mon Sep 17 00:00:00 2001 From: Damian-I Date: Fri, 14 Mar 2025 12:07:20 +0000 Subject: [PATCH 22/24] fixed lockpicking minigame (need to fix css) --- index.html | 89 +++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 64 insertions(+), 25 deletions(-) diff --git a/index.html b/index.html index a926073..9736ee1 100644 --- a/index.html +++ b/index.html @@ -4846,29 +4846,31 @@ style.textContent = ` .lock-visual { display: flex; - justify-content: center; - align-items: flex-end; - gap: 15px; - height: 120px; - background: #333; + justify-content: space-evenly; + align-items: center; + gap: 10px; + height: 150px; + background: #222; border-radius: 5px; padding: 15px; position: relative; + margin-bottom: 10px; } .pin { - width: 40px; + width: 30px; height: 100px; position: relative; - background: #444; + background: #e0d8b0; border-radius: 4px 4px 0 0; overflow: visible; cursor: pointer; transition: transform 0.1s; + margin: 0 5px; } .pin:hover { - background: #555; + background: #f0e8c0; } .shear-line { @@ -4885,7 +4887,8 @@ bottom: 0; width: 100%; height: 0px; - background: #dd3333; + background: #66a3ff; + border-radius: 3px 3px 0 0; transition: height 0.05s; } @@ -4893,7 +4896,7 @@ position: absolute; width: 100%; height: 40px; - background: #3355dd; + background: #66a3ff; transition: bottom 0.05s; bottom: 40px; border-radius: 0 0 3px 3px; @@ -4929,6 +4932,7 @@ .pin.set .key-pin { height: 39px; /* Just below shear line */ + background: #22aa22; /* Green to indicate set */ } .cylinder { @@ -4939,7 +4943,7 @@ height: 30px; background: #222; border-radius: 5px; - margin-top: -15px; + margin-top: 5px; position: relative; z-index: 0; } @@ -4963,31 +4967,40 @@ border-radius: 5px; text-align: center; min-height: 20px; + margin-top: 10px; } .tension-control { display: flex; + justify-content: space-between; align-items: center; background: #333; - padding: 10px; + padding: 15px; border-radius: 5px; + margin-top: 10px; } .tension-control label { flex: 1; } + .tension-status { + flex: 1; + text-align: right; + } + .tension-wrench { - width: 50px; - height: 30px; + width: 60px; + height: 40px; background: #666; border-radius: 4px; cursor: pointer; display: flex; align-items: center; justify-content: center; - margin-right: 10px; + margin-right: 15px; transition: transform 0.2s; + position: relative; } .tension-wrench:hover { @@ -4998,6 +5011,21 @@ background: #2196F3; transform: rotate(-5deg); } + + .wrench-handle { + width: 60%; + height: 10px; + background: #999; + position: absolute; + } + + .wrench-tip { + width: 15px; + height: 25px; + background: #999; + position: absolute; + left: 5px; + } `; document.head.appendChild(style); } @@ -5151,25 +5179,34 @@ // Start lifting the pin this.lockState.currentPin = pin; + this.gameState.mouseDown = true; // Add this line to track mouse state this.liftPin(); + // Add mouse up listener to document + const mouseUpHandler = () => { + this.gameState.mouseDown = false; + this.checkPinSet(this.lockState.currentPin); + this.lockState.currentPin = null; + document.removeEventListener('mouseup', mouseUpHandler); + }; + + document.addEventListener('mouseup', mouseUpHandler); + // Prevent text selection e.preventDefault(); } // Override framework's mouse event handlers handleMouseUp(e) { - // If we were in the process of lifting a pin, check if it sets or drops - if (this.lockState.currentPin) { - this.checkPinSet(this.lockState.currentPin); - this.lockState.currentPin = null; - } + // The document-level handler above will take care of this + // This is still needed for framework compatibility + this.gameState.mouseDown = false; } // Pin-lifting logic liftPin() { - if (!this.gameState.mouseDown || !this.lockState.currentPin || - !this.gameState.isActive || !this.lockState.tensionApplied) { + if (!this.lockState.currentPin || !this.gameState.isActive || + !this.lockState.tensionApplied || !this.gameState.mouseDown) { return; } @@ -5194,7 +5231,9 @@ this.updatePinVisual(pin); // Continue lifting while mouse is down - requestAnimationFrame(() => this.liftPin()); + if (this.gameState.mouseDown) { + requestAnimationFrame(() => this.liftPin()); + } } // Check if a pin should be set or dropped @@ -5355,7 +5394,7 @@
`; - // Use the framework's success message + // Use the framework's success message system this.showSuccess(successHTML, true, 2000); // Store lockable for the result @@ -5369,7 +5408,7 @@
Try again with more careful pin manipulation
`; - // Use the framework's failure message + // Use the framework's failure message system this.showFailure(failureHTML, true, 2000); } From cf292e4f0dc9c2cfcd5e6fb25d5a5b01016d7ca7 Mon Sep 17 00:00:00 2001 From: Damian-I Date: Fri, 14 Mar 2025 17:05:17 +0000 Subject: [PATCH 23/24] lockpocking dramatically updated --- index.html | 864 +++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 614 insertions(+), 250 deletions(-) diff --git a/index.html b/index.html index 9736ee1..af13d1b 100644 --- a/index.html +++ b/index.html @@ -884,58 +884,224 @@ } .tension-control { - display: flex; + display: grid; + grid-template-columns: auto 1fr; + gap: 20px; align-items: center; background: #333; - padding: 10px 15px; + padding: 20px; border-radius: 5px; - font-size: 14px; - gap: 15px; + margin-top: 20px; } - - .tension-status { - font-size: 13px; - color: #ddd; - } - - .tension-wrench { + + .tension-wrench-container { + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; position: relative; - height: 25px; + width: 150px; + height: 60px; + } + + .tension-track { + width: 100%; + height: 10px; + background: #444; + border-radius: 5px; + position: relative; + overflow: hidden; + } + + .tension-progress { + position: absolute; + height: 100%; + width: 0%; + background: linear-gradient(to right, #666, #2196F3); + transition: width 0.3s; + } + + .tension-status { + font-size: 16px; + text-align: left; + padding-left: 10px; + } + + .tension-wrench { width: 60px; + height: 40px; + background: #666; + border-radius: 4px; cursor: pointer; - } - - .wrench-handle { + display: flex; + align-items: center; + justify-content: center; + transition: transform 0.3s, background-color 0.3s; position: absolute; - top: 10px; - right: 0; - width: 40px; - height: 5px; - background: #aaa; - border-radius: 2px; - transform-origin: right center; - transition: transform 0.3s; + left: 0; + top: 20px; + z-index: 2; + box-shadow: 0 2px 5px rgba(0,0,0,0.3); } - - .wrench-tip { - position: absolute; - right: 0; - top: 5px; - width: 10px; - height: 15px; - background: #888; - border-radius: 2px; + + .tension-wrench:hover { + background: #777; } - - .tension-wrench.active .wrench-handle { - transform: rotate(-20deg); + + .tension-wrench.active { background: #2196F3; } + .wrench-handle { + width: 60%; + height: 10px; + background: #999; + position: absolute; + } + + .wrench-tip { + width: 20px; + height: 30px; + background: #999; + position: absolute; + left: 5px; + } + .cylinder { height: 20px; margin-top: -5px; } + + .lock-visual { + display: flex; + justify-content: space-evenly; + align-items: center; + gap: 10px; + height: 160px; + background: #f0e6a6; /* Light yellow/beige background */ + border-radius: 5px; + padding: 15px; + position: relative; + margin-bottom: 10px; + border: 2px solid #887722; + } + + .pin { + width: 30px; + height: 110px; + position: relative; + background: transparent; + border-radius: 4px 4px 0 0; + overflow: visible; + cursor: pointer; + transition: transform 0.1s; + margin: 0 5px; + } + + .pin:hover { + opacity: 0.9; + } + + .shear-line { + position: absolute; + width: 100%; + height: 2px; + background: #aa8833; + bottom: 50px; + z-index: 5; + } + + .key-pin { + position: absolute; + bottom: 0; + width: 100%; + height: 0px; + background: #dd3333; /* Red for key pins */ + transition: height 0.05s; + border-radius: 0 0 0 0; + clip-path: polygon(0 0, 100% 0, 100% 70%, 50% 100%, 0 70%); /* Pointed bottom */ + } + + .driver-pin { + position: absolute; + width: 100%; + height: 50px; + background: #3388dd; /* Blue for driver pins */ + transition: bottom 0.05s; + bottom: 50px; + border-radius: 0 0 0 0; + } + + .spring { + position: absolute; + bottom: 100px; + width: 100%; + height: 25px; + background: linear-gradient(to bottom, + #cccccc 0%, #cccccc 20%, + #999999 20%, #999999 25%, + #cccccc 25%, #cccccc 40%, + #999999 40%, #999999 45%, + #cccccc 45%, #cccccc 60%, + #999999 60%, #999999 65%, + #cccccc 65%, #cccccc 80%, + #999999 80%, #999999 85%, + #cccccc 85%, #cccccc 100% + ); + transition: height 0.05s; + } + + .pin.binding { + box-shadow: 0 0 8px 2px #ffcc00; + } + + .pin.set .driver-pin { + bottom: 52px; /* Just above shear line */ + background: #22aa22; /* Green to indicate set */ + } + + .pin.set .key-pin { + height: 49px; /* Just below shear line */ + background: #22aa22; /* Green to indicate set */ + clip-path: polygon(0 0, 100% 0, 100% 70%, 50% 100%, 0 70%); + } + + .cylinder { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 30px; + background: #ddbb77; + border-radius: 5px; + margin-top: 5px; + position: relative; + z-index: 0; + border: 2px solid #887722; + } + + .cylinder-inner { + width: 80%; + height: 20px; + background: #ccaa66; + border-radius: 3px; + transform-origin: center; + transition: transform 0.3s; + } + + .cylinder.rotated .cylinder-inner { + transform: rotate(15deg); + } + + .lockpick-feedback { + padding: 15px; + background: #333; + border-radius: 5px; + text-align: center; + min-height: 30px; + margin-top: 20px; + font-size: 16px; + } @@ -4833,106 +4999,149 @@ this.container.style.padding = '20px'; this.container.style.gap = '15px'; - // Set up header content + // Set up header content with proper spacing this.headerElement.innerHTML = ` -

Lockpicking

-

Apply tension and hold click on pins to lift them to the shear line

+

Lockpicking

+

Apply tension and hold click on pins to lift them to the shear line

`; + this.headerElement.style.marginBottom = '30px'; // Add more space below header // Add custom styles for the lockpicking minigame if they don't exist if (!document.getElementById('lockpicking-styles')) { const style = document.createElement('style'); style.id = 'lockpicking-styles'; style.textContent = ` + /* Game container styles */ + .minigame-container { + padding-top: 80px !important; /* Add padding at top to prevent header overlap */ + position: relative; + } + + .minigame-header { + position: absolute; + top: 0; + left: 0; + right: 0; + z-index: 10; + background: rgba(34, 34, 34, 0.95); + border-bottom: 1px solid #444; + padding: 10px 20px; + margin-bottom: 20px; + } + .lock-visual { display: flex; justify-content: space-evenly; align-items: center; - gap: 10px; - height: 150px; - background: #222; + gap: 20px; + height: 200px; + background: #f0e6a6; /* Light yellow/beige background */ border-radius: 5px; - padding: 15px; + padding: 25px; position: relative; - margin-bottom: 10px; + margin-top: 20px; /* Add top margin for better spacing from header */ + margin-bottom: 20px; + border: 2px solid #887722; + z-index: 1; /* Ensure pins are below header */ } + /* Rest of existing CSS */ .pin { - width: 30px; - height: 100px; + width: 40px; + height: 150px; position: relative; - background: #e0d8b0; + background: transparent; border-radius: 4px 4px 0 0; overflow: visible; cursor: pointer; transition: transform 0.1s; - margin: 0 5px; + margin: 0 15px; } .pin:hover { - background: #f0e8c0; + opacity: 0.9; } .shear-line { position: absolute; width: 100%; height: 2px; - background: #aaa; - bottom: 40px; + background: #aa8833; + bottom: 70px; z-index: 5; } + .pin-assembly { + position: absolute; + bottom: 0; + width: 100%; + height: 140px; + transition: transform 0.05s; + } + .key-pin { position: absolute; bottom: 0; width: 100%; - height: 0px; - background: #66a3ff; - border-radius: 3px 3px 0 0; - transition: height 0.05s; + height: 50px; /* Fixed height for all pins */ + background: #dd3333; /* Red for key pins */ + border-radius: 0 0 0 0; + clip-path: polygon(0 0, 100% 0, 100% 70%, 50% 100%, 0 70%); /* Pointed bottom */ + transition: transform 0.3s; } .driver-pin { position: absolute; width: 100%; - height: 40px; - background: #66a3ff; - transition: bottom 0.05s; - bottom: 40px; - border-radius: 0 0 3px 3px; + height: 70px; + background: #3388dd; /* Blue for driver pins */ + bottom: 50px; /* Position right above key pin */ + border-radius: 0 0 0 0; + transition: transform 0.3s, background-color 0.3s; } .spring { position: absolute; - bottom: 80px; + bottom: 120px; width: 100%; - height: 20px; + height: 40px; background: linear-gradient(to bottom, - transparent 0%, transparent 20%, - #999 20%, #999 30%, - transparent 30%, transparent 40%, - #999 40%, #999 50%, - transparent 50%, transparent 60%, - #999 60%, #999 70%, - transparent 70%, transparent 80%, - #999 80%, #999 90%, - transparent 90%, transparent 100% + #cccccc 0%, #cccccc 20%, + #999999 20%, #999999 25%, + #cccccc 25%, #cccccc 40%, + #999999 40%, #999999 45%, + #cccccc 45%, #cccccc 60%, + #999999 60%, #999999 65%, + #cccccc 65%, #cccccc 80%, + #999999 80%, #999999 85%, + #cccccc 85%, #cccccc 100% ); - transition: height 0.05s; + transition: transform 0.3s; } .pin.binding { box-shadow: 0 0 8px 2px #ffcc00; } - .pin.set .driver-pin { - bottom: 42px; /* Just above shear line */ - background: #22aa22; /* Green to indicate set */ + /* Remove the pin-assembly transform for set pins */ + .pin.set .pin-assembly { + transform: none; /* Reset transform so we can control individual pieces */ } - .pin.set .key-pin { - height: 39px; /* Just below shear line */ + /* Keep driver pin (blue) above the shear line when set */ + .pin.set .driver-pin { background: #22aa22; /* Green to indicate set */ + transform: translateY(-22px); /* Move up above shear line */ + } + + /* Reset key pin (red) to the bottom when set */ + .pin.set .key-pin { + transform: translateY(0); /* Keep at bottom */ + } + + /* Move spring up with driver pin when set */ + .pin.set .spring { + transform: translateY(-22px); /* Move up with driver pin */ } .cylinder { @@ -4941,17 +5150,18 @@ align-items: center; width: 100%; height: 30px; - background: #222; + background: #ddbb77; border-radius: 5px; margin-top: 5px; position: relative; z-index: 0; + border: 2px solid #887722; } .cylinder-inner { width: 80%; height: 20px; - background: #333; + background: #ccaa66; border-radius: 3px; transform-origin: center; transition: transform 0.3s; @@ -4962,31 +5172,57 @@ } .lockpick-feedback { - padding: 10px; + padding: 15px; background: #333; border-radius: 5px; text-align: center; - min-height: 20px; - margin-top: 10px; + min-height: 30px; + margin-top: 20px; + font-size: 16px; } .tension-control { - display: flex; - justify-content: space-between; + display: grid; + grid-template-columns: auto 1fr; + gap: 20px; align-items: center; background: #333; - padding: 15px; + padding: 20px; border-radius: 5px; - margin-top: 10px; + margin-top: 20px; } - .tension-control label { - flex: 1; + .tension-wrench-container { + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; + position: relative; + width: 150px; + height: 60px; + } + + .tension-track { + width: 100%; + height: 10px; + background: #444; + border-radius: 5px; + position: relative; + overflow: hidden; + } + + .tension-progress { + position: absolute; + height: 100%; + width: 0%; + background: linear-gradient(to right, #666, #2196F3); + transition: width 0.3s; } .tension-status { - flex: 1; - text-align: right; + font-size: 16px; + text-align: left; + padding-left: 10px; } .tension-wrench { @@ -4998,9 +5234,12 @@ display: flex; align-items: center; justify-content: center; - margin-right: 15px; - transition: transform 0.2s; - position: relative; + transition: transform 0.3s, background-color 0.3s; + position: absolute; + left: 0; + top: 20px; + z-index: 2; + box-shadow: 0 2px 5px rgba(0,0,0,0.3); } .tension-wrench:hover { @@ -5009,7 +5248,6 @@ .tension-wrench.active { background: #2196F3; - transform: rotate(-5deg); } .wrench-handle { @@ -5020,8 +5258,8 @@ } .wrench-tip { - width: 15px; - height: 25px; + width: 20px; + height: 30px; background: #999; position: absolute; left: 5px; @@ -5030,6 +5268,10 @@ document.head.appendChild(style); } + // Add a class to the container for positioning + this.container.classList.add('minigame-container'); + this.headerElement.classList.add('minigame-header'); + // Replace the game container with custom lockpicking interface this.setupLockpickingInterface(); @@ -5046,52 +5288,94 @@ this.gameContainer.parentNode.removeChild(this.gameContainer); } + // Create the content wrapper with padding to avoid header overlap + const contentWrapper = document.createElement('div'); + contentWrapper.style.paddingTop = '10px'; + this.container.appendChild(contentWrapper); + this.contentWrapper = contentWrapper; + + // Create instructions + const instructions = document.createElement('div'); + instructions.className = 'instructions'; + instructions.textContent = 'Apply tension first, then click and hold on pins to lift them to the shear line'; + contentWrapper.appendChild(instructions); + // Create the lock visual container const lockVisual = document.createElement('div'); lockVisual.className = 'lock-visual'; - this.container.appendChild(lockVisual); + contentWrapper.appendChild(lockVisual); this.lockVisual = lockVisual; - // Create the cylinder that will rotate when tension is applied - const cylinder = document.createElement('div'); - cylinder.className = 'cylinder'; - cylinder.innerHTML = '
'; - this.container.appendChild(cylinder); - this.cylinder = cylinder; + // Remove cylinder creation - it's no longer needed - // Add tension toggle control + // Add tension toggle control with horizontal movement const tensionControl = document.createElement('div'); tensionControl.className = 'tension-control'; - tensionControl.innerHTML = ` -
-
-
-
- Click wrench to apply tension + + const wrenchContainer = document.createElement('div'); + wrenchContainer.className = 'tension-wrench-container'; + + const wrenchLabel = document.createElement('div'); + wrenchLabel.textContent = 'Tension Wrench'; + wrenchLabel.style.fontSize = '14px'; + wrenchContainer.appendChild(wrenchLabel); + + // Add tension track and progress + const tensionTrack = document.createElement('div'); + tensionTrack.className = 'tension-track'; + + const tensionProgress = document.createElement('div'); + tensionProgress.className = 'tension-progress'; + tensionTrack.appendChild(tensionProgress); + + wrenchContainer.appendChild(tensionTrack); + + const tensionWrench = document.createElement('div'); + tensionWrench.className = 'tension-wrench'; + tensionWrench.innerHTML = ` +
+
`; - this.container.appendChild(tensionControl); + wrenchContainer.appendChild(tensionWrench); + + tensionControl.appendChild(wrenchContainer); + + const tensionStatus = document.createElement('div'); + tensionStatus.className = 'tension-status'; + tensionStatus.textContent = 'Click wrench to apply tension'; + tensionControl.appendChild(tensionStatus); + + this.contentWrapper.appendChild(tensionControl); // Feedback area const feedback = document.createElement('div'); feedback.className = 'lockpick-feedback'; - this.container.appendChild(feedback); + this.contentWrapper.appendChild(feedback); this.feedback = feedback; - // Set up tension wrench interaction - const tensionWrench = tensionControl.querySelector('.tension-wrench'); - const tensionStatus = tensionControl.querySelector('.tension-status'); - + // Set up tension wrench interaction with horizontal movement tensionWrench.addEventListener('click', () => { this.lockState.tensionApplied = !this.lockState.tensionApplied; tensionWrench.classList.toggle('active', this.lockState.tensionApplied); - cylinder.classList.toggle('rotated', this.lockState.tensionApplied); + + // Move wrench horizontally instead of rotating + if (this.lockState.tensionApplied) { + // Move to initial right position (25%) + this.updateTensionPosition(25); + } else { + // Return to left position (0%) + this.updateTensionPosition(0); + + // Reset progress fill + tensionProgress.style.width = '0%'; + } // Update status text tensionStatus.textContent = this.lockState.tensionApplied ? - 'Tension applied' : 'Click wrench to apply tension'; + 'Tension applied - now lift pins' : 'Click wrench to apply tension'; // Update which pins are binding - this.updatePinBindings(); + this.updateBindingPins(); // If tension is toggled off, reset any unset pins if (!this.lockState.tensionApplied) { @@ -5110,6 +5394,27 @@ // Store references this.tensionWrench = tensionWrench; this.tensionStatus = tensionStatus; + this.tensionProgress = tensionProgress; + } + + // New method to update the tension wrench position + updateTensionPosition(percentage) { + if (percentage < 0) percentage = 0; + if (percentage > 100) percentage = 100; + + // Calculate position based on container width + const containerWidth = this.tensionWrench.parentElement.offsetWidth; + const wrenchWidth = this.tensionWrench.offsetWidth; + const maxOffset = containerWidth - wrenchWidth; + const position = (maxOffset * percentage) / 100; + + // Update wrench position + this.tensionWrench.style.transform = `translateX(${position}px)`; + + // Update progress bar fill + if (this.tensionProgress) { + this.tensionProgress.style.width = `${percentage}%`; + } } createPins() { @@ -5128,30 +5433,49 @@ shearLine.className = 'shear-line'; pinElement.appendChild(shearLine); - // Create key pin (bottom pin) + // Create pin assembly container + const pinAssembly = document.createElement('div'); + pinAssembly.className = 'pin-assembly'; + pinElement.appendChild(pinAssembly); + + // Create key pin (bottom pin) with varying height const keyPin = document.createElement('div'); keyPin.className = 'key-pin'; - pinElement.appendChild(keyPin); + pinAssembly.appendChild(keyPin); - // Create driver pin (top pin) + // Generate random key pin height (30px to 60px) + const keyPinHeight = Math.floor(Math.random() * 31) + 30; + keyPin.style.height = `${keyPinHeight}px`; + + // Create driver pin (top pin) with consistent height const driverPin = document.createElement('div'); driverPin.className = 'driver-pin'; - pinElement.appendChild(driverPin); + pinAssembly.appendChild(driverPin); + // Position driver pin right above the key pin + driverPin.style.bottom = `${keyPinHeight}px`; // Create spring const spring = document.createElement('div'); spring.className = 'spring'; - pinElement.appendChild(spring); + pinAssembly.appendChild(spring); + // Position spring above driver pin + spring.style.bottom = `${keyPinHeight + 70}px`; // 70px is driver pin height + + // Calculate the distance from the bottom of the pin to the shear line (70px from bottom) + const distanceToShearLine = 70 - keyPinHeight; // Store pin data const pin = { index: i, binding: bindingOrder.indexOf(i), - setPoint: Math.random() * 0.4 + 0.3, // Point between 30-70% where pin sets - currentHeight: 0, + keyPinHeight: keyPinHeight, + distanceToShearLine: distanceToShearLine, + currentHeight: 0, // How high the pin is currently lifted (0-1 scale) isSet: false, + resistance: Math.random() * 0.02 + 0.01, elements: { container: pinElement, + assembly: pinAssembly, keyPin: keyPin, driverPin: driverPin, spring: spring @@ -5160,99 +5484,89 @@ this.pins.push(pin); - // Add custom pin click handler - this.addEventListenerWithCleanup(pinElement, 'mousedown', (e) => { - this.handlePinMouseDown(pin, e); + // Fix: Use an arrow function to preserve 'this' context + // and define the handler inline instead of referencing this.handlePinMouseDown + const self = this; // Store reference to 'this' + this.addEventListenerWithCleanup(pinElement, 'mousedown', function(e) { + // Skip if game is not active or pin is already set + if (!self.gameState.isActive || pin.isSet) return; + + // Only proceed if tension is applied + if (!self.lockState.tensionApplied) { + self.updateFeedback("Apply tension first by toggling the wrench"); + return; + } + + // Play a sound effect when interacting with pins + if (typeof self.playSound === 'function') { + self.playSound('pin_click'); + } + + // Start lifting the pin + self.lockState.currentPin = pin; + self.gameState.mouseDown = true; + self.liftPin(); + + // Add mouse up listener to document + const mouseUpHandler = function() { + self.gameState.mouseDown = false; + self.checkPinSet(self.lockState.currentPin); + self.lockState.currentPin = null; + document.removeEventListener('mouseup', mouseUpHandler); + }; + + document.addEventListener('mouseup', mouseUpHandler); + + // Prevent text selection + e.preventDefault(); }); } } - // Custom pin mouse down handler - handlePinMouseDown(pin, e) { - if (!this.gameState.isActive || pin.isSet) return; - - // Only proceed if tension is applied - if (!this.lockState.tensionApplied) { - this.updateFeedback("Apply tension first by toggling the wrench"); - return; - } - - // Start lifting the pin - this.lockState.currentPin = pin; - this.gameState.mouseDown = true; // Add this line to track mouse state - this.liftPin(); - - // Add mouse up listener to document - const mouseUpHandler = () => { - this.gameState.mouseDown = false; - this.checkPinSet(this.lockState.currentPin); - this.lockState.currentPin = null; - document.removeEventListener('mouseup', mouseUpHandler); - }; - - document.addEventListener('mouseup', mouseUpHandler); - - // Prevent text selection - e.preventDefault(); - } - - // Override framework's mouse event handlers - handleMouseUp(e) { - // The document-level handler above will take care of this - // This is still needed for framework compatibility - this.gameState.mouseDown = false; - } - - // Pin-lifting logic - liftPin() { - if (!this.lockState.currentPin || !this.gameState.isActive || - !this.lockState.tensionApplied || !this.gameState.mouseDown) { - return; - } - - const pin = this.lockState.currentPin; - - // Only binding pins can be lifted effectively - if (!this.shouldPinBind(pin)) { - // Non-binding pins can be lifted, but with resistance and limited height - pin.currentHeight += 0.01; - if (pin.currentHeight > 0.3) { - pin.currentHeight = 0.3; // Can't lift non-binding pins very high - } - } else { - // Binding pins lift smoothly - pin.currentHeight += 0.03; - if (pin.currentHeight > 1) { - pin.currentHeight = 1; // Max height - } - } - - // Update visual - this.updatePinVisual(pin); - - // Continue lifting while mouse is down - if (this.gameState.mouseDown) { - requestAnimationFrame(() => this.liftPin()); - } - } - // Check if a pin should be set or dropped checkPinSet(pin) { if (!this.lockState.tensionApplied || !this.shouldPinBind(pin)) { - // If no tension or not binding, the pin drops - this.dropPin(pin); + // Define dropPin function inline since it's not being found + this.animatePinDrop(pin); return; } - // Check if pin is at the correct height (with some tolerance) - const heightDiff = Math.abs(pin.currentHeight - pin.setPoint); + // Calculate current pin height in pixels + const currentLiftInPixels = pin.currentHeight * pin.distanceToShearLine; - if (heightDiff < 0.1) { + // Check if the top of the key pin (or bottom of driver pin) is exactly at the shear line + // Allow a small tolerance of 2 pixels + const tolerance = 2; + const isAtShearLine = Math.abs(currentLiftInPixels - pin.distanceToShearLine) <= tolerance; + + if (isAtShearLine) { // Pin set successfully! pin.isSet = true; this.lockState.pinsSet++; + + // Play a satisfying click sound when pin sets + if (typeof this.playSound === 'function') { + this.playSound('pin_set'); + } + + // First reset the assembly position + pin.elements.assembly.style.transform = 'none'; + + // Calculate exact position for the pin junction to be at the shear line + const exactLift = pin.distanceToShearLine; + pin.elements.assembly.style.transform = `translateY(-${exactLift}px)`; + + // Mark the pin as set + pin.elements.container.classList.add('set'); + + // Change color of the driver pin to green + pin.elements.driverPin.style.backgroundColor = '#22aa22'; + this.updateFeedback(`Pin set at the shear line! (${this.lockState.pinsSet}/${this.pinCount})`); - this.updatePinVisual(pin); + + // Move the tension wrench further right based on progress + const progressPercentage = 25 + (this.lockState.pinsSet / this.pinCount * 75); + this.updateTensionPosition(progressPercentage); // Update progress this.updateProgress(this.lockState.pinsSet, this.pinCount); @@ -5264,26 +5578,29 @@ } // Update which pin is binding next - this.updatePinBindings(); + this.updateBindingPins(); } else { // Pin not at the correct height, drops back down - this.dropPin(pin); + this.animatePinDrop(pin); - if (pin.currentHeight > pin.setPoint + 0.1) { - this.updateFeedback("Pin was pushed too far and dropped"); + if (currentLiftInPixels > pin.distanceToShearLine) { + this.updateFeedback("Pin was pushed too far past the shear line"); } else { - this.updateFeedback("Pin wasn't lifted high enough and dropped"); + this.updateFeedback("Pin wasn't lifted high enough to reach the shear line"); } } } - // Animate a pin dropping down - dropPin(pin) { + // Define the animatePinDrop method to replace the missing dropPin method + animatePinDrop(pin) { // Don't drop pins that are already set if (pin.isSet) return; + // Calculate drop speed based on how high the pin is + const dropSpeed = 0.05 + (pin.currentHeight * 0.1); + const dropInterval = setInterval(() => { - pin.currentHeight -= 0.05; + pin.currentHeight -= dropSpeed; if (pin.currentHeight <= 0) { pin.currentHeight = 0; @@ -5294,47 +5611,70 @@ }, 10); } - // Update a single pin's visual appearance + // Update pin visual based on current height updatePinVisual(pin) { - // Update key pin and driver pin heights - pin.elements.keyPin.style.height = `${pin.currentHeight * 40}px`; - pin.elements.driverPin.style.bottom = `${pin.currentHeight * 40 + 1}px`; - pin.elements.spring.style.height = `${20 - pin.currentHeight * 5}px`; + // Skip visualization update if the pin is set + if (pin.isSet) return; - // Show set state - pin.elements.container.classList.toggle('set', pin.isSet); + // Calculate the lift in pixels based on the current progress (0-1) times the distance to the shear line + const translateY = pin.currentHeight * pin.distanceToShearLine * -1; // Negative because we're moving up + + // Move the entire pin assembly up + pin.elements.assembly.style.transform = `translateY(${translateY}px)`; } - // Update which pins are binding based on binding order - updatePinBindings() { - if (!this.lockState.tensionApplied) { - // No binding if no tension - this.pins.forEach(pin => { - pin.elements.container.classList.remove('binding'); - }); + // Pin-lifting logic with realistic physics + liftPin() { + if (!this.lockState.currentPin || !this.gameState.isActive || + !this.lockState.tensionApplied || !this.gameState.mouseDown) { return; } - // Find the next unset pin in binding order - let bindingPinFound = false; + const pin = this.lockState.currentPin; - for (let order = 0; order < this.pinCount; order++) { - const nextPin = this.pins.find(p => p.binding === order && !p.isSet); - if (nextPin) { - // Mark this pin as binding - this.pins.forEach(pin => { - pin.elements.container.classList.toggle('binding', pin.index === nextPin.index); - }); - bindingPinFound = true; - break; + // Add realistic resistance based on binding state + let liftAmount = 0; + + // Only binding pins can be lifted effectively + if (!this.shouldPinBind(pin)) { + // Non-binding pins can be lifted, but with resistance and limited height + liftAmount = 0.01; + if (pin.currentHeight > 0.3) { + liftAmount = 0.005; // Increased resistance at higher positions } + } else { + // Binding pins lift more smoothly but still have some resistance + liftAmount = 0.03 - (pin.resistance * pin.currentHeight); + + // Add slight random variation to simulate realistic feel + liftAmount += (Math.random() * 0.01 - 0.005); } - // If no binding pin was found (all pins set), remove binding class from all - if (!bindingPinFound) { - this.pins.forEach(pin => { - pin.elements.container.classList.remove('binding'); - }); + // Update pin height + pin.currentHeight += liftAmount; + + // Cap at maximum height + if (pin.currentHeight > 1.2) { // Allow overshooting the shear line a bit + pin.currentHeight = 1.2; + } + + // Update visual + this.updatePinVisual(pin); + + // Add subtle feedback when pin is near the shear line + const currentLiftInPixels = pin.currentHeight * pin.distanceToShearLine; + const distanceToShearLine = Math.abs(currentLiftInPixels - pin.distanceToShearLine); + + if (distanceToShearLine < 5) { + // Pin is close to the shear line + pin.elements.container.style.boxShadow = "0 0 5px #ffffff"; + } else { + pin.elements.container.style.boxShadow = ""; + } + + // Continue lifting while mouse is down + if (this.gameState.mouseDown) { + requestAnimationFrame(() => this.liftPin()); } } @@ -5438,6 +5778,39 @@ } return array; } + + // Add new method for updating binding pins + updateBindingPins() { + if (!this.lockState.tensionApplied) { + // No binding if no tension + this.pins.forEach(pin => { + pin.elements.container.classList.remove('binding'); + }); + return; + } + + // Find the next unset pin in binding order + let bindingPinFound = false; + + for (let order = 0; order < this.pinCount; order++) { + const nextPin = this.pins.find(p => p.binding === order && !p.isSet); + if (nextPin) { + // Mark this pin as binding + this.pins.forEach(pin => { + pin.elements.container.classList.toggle('binding', pin.index === nextPin.index); + }); + bindingPinFound = true; + break; + } + } + + // If no binding pin was found (all pins set), remove binding class from all + if (!bindingPinFound) { + this.pins.forEach(pin => { + pin.elements.container.classList.remove('binding'); + }); + } + } } // Register the lockpicking minigame with the framework @@ -6666,15 +7039,6 @@ if (x >= 0 && x < this.gridSize && y >= 0 && y < this.gridSize) { newFingerprintCells.add(`${x},${y}`); - - // Add ridge-like pattern - for (let j = 1; j <= 3; j++) { - const ridgeX = Math.floor(centerX + Math.cos(angle) * (distance - j)); - const ridgeY = Math.floor(centerY + Math.sin(angle) * (distance - j)); - if (ridgeX >= 0 && ridgeX < this.gridSize && ridgeY >= 0 && ridgeY < this.gridSize) { - newFingerprintCells.add(`${ridgeX},${ridgeY}`); - } - } } } } else { From 80edb79450883e8e2868aef2fb6e75468934e2a5 Mon Sep 17 00:00:00 2001 From: Damian-I Date: Fri, 14 Mar 2025 18:07:09 +0000 Subject: [PATCH 24/24] fixed issue where pins moved after being set --- index.html | 6 ------ 1 file changed, 6 deletions(-) diff --git a/index.html b/index.html index af13d1b..aaacd63 100644 --- a/index.html +++ b/index.html @@ -5131,7 +5131,6 @@ /* Keep driver pin (blue) above the shear line when set */ .pin.set .driver-pin { background: #22aa22; /* Green to indicate set */ - transform: translateY(-22px); /* Move up above shear line */ } /* Reset key pin (red) to the bottom when set */ @@ -5139,11 +5138,6 @@ transform: translateY(0); /* Keep at bottom */ } - /* Move spring up with driver pin when set */ - .pin.set .spring { - transform: translateY(-22px); /* Move up with driver pin */ - } - .cylinder { display: flex; justify-content: center;