From 344d5c41929fb5f55a7ae070eb954b637061ec23 Mon Sep 17 00:00:00 2001 From: Damian-I Date: Thu, 13 Mar 2025 04:34:48 +0000 Subject: [PATCH] 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));