From f27ad53cc29eccd12928ea4ce00c83fd42c0725d Mon Sep 17 00:00:00 2001 From: "Z. Cliffe Schreuders" Date: Mon, 27 Oct 2025 16:34:48 +0000 Subject: [PATCH] feat: Add ToolManager class for managing lockpicking tools and modes - Implemented ToolManager to handle the visibility and behavior of lockpicking tools. - Added methods for switching between key mode and lockpicking mode. - Included functionality to return hook to starting position and flash wrench red. - Integrated cleanup and initialization processes for the lockpicking game state. - Enhanced user feedback and UI updates during mode transitions. --- js/minigames/lockpicking/key-animation.js | 601 +++++ js/minigames/lockpicking/key-operations.js | 4 +- js/minigames/lockpicking/lock-graphics.js | 2 +- .../lockpicking/lockpicking-game-phaser.js | 1944 +---------------- js/minigames/lockpicking/pin-management.js | 1103 ++++++++++ js/minigames/lockpicking/tool-manager.js | 258 +++ 6 files changed, 1992 insertions(+), 1920 deletions(-) create mode 100644 js/minigames/lockpicking/key-animation.js create mode 100644 js/minigames/lockpicking/pin-management.js create mode 100644 js/minigames/lockpicking/tool-manager.js diff --git a/js/minigames/lockpicking/key-animation.js b/js/minigames/lockpicking/key-animation.js new file mode 100644 index 0000000..4df3a1a --- /dev/null +++ b/js/minigames/lockpicking/key-animation.js @@ -0,0 +1,601 @@ + +/** + * KeyAnimation + * + * Extracted from lockpicking-game-phaser.js + * Instantiate with: new KeyAnimation(this) + * + * All 'this' references replaced with 'this.parent' to access parent instance state: + * - this.parent.pins (array of pin objects) + * - this.parent.scene (Phaser scene) + * - this.parent.lockId (lock identifier) + * - this.parent.lockState (lock state object) + * etc. + */ +export class KeyAnimation { + + constructor(parent) { + this.parent = parent; + } + + snapPinsToExactPositions() { + // Use selected key data for visual positioning, but original key data for correctness + const keyDataToUse = this.parent.selectedKeyData || this.parent.keyData; + if (!keyDataToUse || !keyDataToUse.cuts) return; + + console.log('Snapping pins to exact positions based on key cuts for shear line alignment'); + + // Ensure key data matches lock pin count + if (keyDataToUse.cuts.length !== this.parent.pinCount) { + console.warn(`Key has ${keyDataToUse.cuts.length} cuts but lock has ${this.parent.pinCount} pins. Adjusting key data.`); + // Truncate or pad cuts to match pin count + if (keyDataToUse.cuts.length > this.parent.pinCount) { + keyDataToUse.cuts = keyDataToUse.cuts.slice(0, this.parent.pinCount); + } else { + // Pad with default cuts if key has fewer cuts than lock has pins + while (keyDataToUse.cuts.length < this.parent.pinCount) { + keyDataToUse.cuts.push(40); // Default cut depth + } + } + } + + // Set each pin to the exact final position based on key cut dimensions + keyDataToUse.cuts.forEach((cutDepth, index) => { + if (index >= this.parent.pinCount) { + console.warn(`Key has ${keyDataToUse.cuts.length} cuts but lock only has ${this.parent.pinCount} pins. Skipping cut ${index}.`); + return; + } + + const pin = this.parent.pins[index]; + if (!pin) { + console.error(`Pin at index ${index} is undefined. Available pins: ${this.parent.pins.length}`); + return; + } + + // Calculate the exact position where the pin should rest on the key cut + // The cut depth represents how deep the cut is from the blade top + // We need to position the pin so its bottom rests exactly on the cut surface + + // Key blade dimensions + const bladeHeight = this.parent.keyConfig.bladeHeight; + const keyBladeBaseY = this.parent.keyGroup.y - bladeHeight / 2; + + // Calculate the Y position of the cut surface + const cutSurfaceY = keyBladeBaseY + cutDepth; + + // Calculate where the pin bottom should be to rest on the cut surface + // Add safety check for undefined properties + if (!pin.driverPinLength || !pin.keyPinLength) { + console.warn(`Pin ${pin.index} missing length properties:`, pin); + return; // Skip this pin if properties are missing + } + const pinRestY = 200 - 50 + pin.driverPinLength + pin.keyPinLength; // Pin rest position + const targetKeyPinBottom = cutSurfaceY; + + // Calculate the exact lift needed to move pin bottom from rest to cut surface + const exactLift = pinRestY - targetKeyPinBottom; + + // Snap to exact position + pin.currentHeight = Math.max(0, exactLift); + + // Update pin visuals immediately + this.parent.updatePinVisuals(pin); + + console.log(`Pin ${index}: cutDepth=${cutDepth}, cutSurfaceY=${cutSurfaceY}, exactLift=${exactLift}, currentHeight=${pin.currentHeight}, keyBladeBaseY=${keyBladeBaseY}, bladeHeight=${bladeHeight}`); + }); + + // Note: Rotation animation will be triggered by checkKeyCorrectness() only if key is correct + } + + startKeyRotationAnimationWithChamberHoles() { + // Animation configuration variables - same as lockpicking success + const KEY_PIN_TOP_SHRINK = 10; // How much the key pin top moves down + const KEY_PIN_BOTTOM_SHRINK = 5; // How much the key pin bottom moves up + const KEY_PIN_TOTAL_SHRINK = KEY_PIN_TOP_SHRINK + KEY_PIN_BOTTOM_SHRINK; // Total key pin shrink + const CHANNEL_MOVEMENT = 25; // How much channels move down + const KEYWAY_SHRINK = 20; // How much keyway shrinks + const KEY_SHRINK_FACTOR = 0.7; // How much the key shrinks on Y axis to simulate rotation + + // Play success sound + if (this.parent.sounds.success) { + this.parent.sounds.success.play(); + if (typeof navigator !== 'undefined' && navigator.vibrate) { + navigator.vibrate(500); + } + } + + this.parent.updateFeedback("Key inserted successfully! Lock turning..."); + + // Create upper edge effect - a copy of the entire key group that stays in place + // Position at the key's current position (after insertion, before rotation) + const upperEdgeKeyGroup = this.parent.scene.add.container(this.parent.keyGroup.x, this.parent.keyGroup.y); + upperEdgeKeyGroup.setDepth(0); // Behind the original key + + // Copy the handle (circle) + const upperEdgeHandle = this.parent.scene.add.graphics(); + upperEdgeHandle.fillStyle(0xaaaaaa); // Slightly darker tone for the upper edge + upperEdgeHandle.fillCircle(this.parent.keyConfig.circleRadius, 0, this.parent.keyConfig.circleRadius); + upperEdgeKeyGroup.add(upperEdgeHandle); + + // Copy the shoulder and blade using render texture + const upperEdgeRenderTexture = this.parent.scene.add.renderTexture(0, 0, this.parent.keyRenderTexture.width, this.parent.keyRenderTexture.height); + upperEdgeRenderTexture.setTint(0xaaaaaa); // Apply darker tone + upperEdgeRenderTexture.setOrigin(0, 0.5); // Match the original key's origin + upperEdgeKeyGroup.add(upperEdgeRenderTexture); + + // Draw the shoulder and blade to the upper edge render texture + const upperEdgeGraphics = this.parent.scene.add.graphics(); + upperEdgeGraphics.fillStyle(0xaaaaaa); // Slightly darker tone + + // Draw shoulder + const shoulderX = this.parent.keyConfig.circleRadius * 1.9; + upperEdgeGraphics.fillRect(shoulderX, 0, this.parent.keyConfig.shoulderWidth, this.parent.keyConfig.shoulderHeight); + + // Draw blade - adjust Y position to account for container offset + const bladeX = shoulderX + this.parent.keyConfig.shoulderWidth; + const bladeY = this.parent.keyConfig.shoulderHeight/2 - this.parent.keyConfig.bladeHeight/2; + this.parent.drawKeyBladeAsSolidShape(upperEdgeGraphics, bladeX, bladeY, this.parent.keyConfig.bladeWidth, this.parent.keyConfig.bladeHeight); + + upperEdgeRenderTexture.draw(upperEdgeGraphics); + upperEdgeGraphics.destroy(); + + // Initially hide the upper edge + upperEdgeKeyGroup.setVisible(false); + + // Animate key shrinking on Y axis to simulate rotation + this.parent.scene.tweens.add({ + targets: this.parent.keyGroup, + scaleY: KEY_SHRINK_FACTOR, + duration: 1400, + ease: 'Cubic.easeInOut', + onStart: () => { + // Show the upper edge when rotation starts + upperEdgeKeyGroup.setVisible(true); + } + }); + + // Animate the upper edge copy to shrink and move upward (keeping top edge in place) + this.parent.scene.tweens.add({ + targets: upperEdgeKeyGroup, + scaleY: KEY_SHRINK_FACTOR, + y: upperEdgeKeyGroup.y - 6, // Simple upward movement + duration: 1400, + ease: 'Cubic.easeInOut' + }); + + // Shrink key pins downward and add half circles to simulate cylinder rotation + this.parent.pins.forEach(pin => { + // Hide all highlights + if (pin.shearHighlight) pin.shearHighlight.setVisible(false); + if (pin.setHighlight) pin.setHighlight.setVisible(false); + if (pin.bindingHighlight) pin.bindingHighlight.setVisible(false); + if (pin.overpickedHighlight) pin.overpickedHighlight.setVisible(false); + if (pin.failureHighlight) pin.failureHighlight.setVisible(false); + + // Create chamber hole circle that expands at the actual chamber position + const chamberCircle = this.parent.scene.add.graphics(); + chamberCircle.fillStyle(0x666666); // Dark gray color for chamber holes + chamberCircle.x = pin.x; // Center horizontally on the pin + + // Position at actual chamber hole location (shear line) + const chamberY = pin.y + (-45); // Shear line position + chamberCircle.y = chamberY; + chamberCircle.setDepth(5); // Above all other elements + + // Create a temporary object to hold the circle expansion data + const circleData = { + width: 24, // Start full width (same as key pin) + height: 2, // Start very thin (flat top) + y: chamberY + }; + + // Animate the chamber hole circle expanding to full circle (stays at chamber position) + this.parent.scene.tweens.add({ + targets: circleData, + width: 24, // Full circle width (stays same) + height: 16, // Full circle height (expands from 2 to 16) + y: chamberY, // Stay at the chamber position (no movement) + duration: 1400, + ease: 'Cubic.easeInOut', + onUpdate: function() { + chamberCircle.clear(); + chamberCircle.fillStyle(0xff0000); // Light red for chamber holes filled with key pin + + // Calculate animation progress (0 to 1) + const progress = (circleData.height - 2) / (16 - 2); // From 2 to 16 height + + // Draw different circle shapes based on progress (widest in middle) + if (progress < 0.1) { + // Start: just a thin line (flat top) + chamberCircle.fillRect(-12, 0, 24, 2); + } else if (progress < 0.3) { + // Early: thin oval with middle bulge + chamberCircle.fillRect(-8, 0, 16, 2); // narrow top + chamberCircle.fillRect(-12, 2, 24, 2); // wide middle + chamberCircle.fillRect(-8, 4, 16, 2); // narrow bottom + } else if (progress < 0.5) { + // Middle: growing circle with middle bulge + chamberCircle.fillRect(-6, 0, 12, 2); // narrow top + chamberCircle.fillRect(-10, 2, 20, 2); // wider + chamberCircle.fillRect(-12, 4, 24, 2); // widest middle + chamberCircle.fillRect(-10, 6, 20, 2); // wider + chamberCircle.fillRect(-6, 8, 12, 2); // narrow bottom + } else if (progress < 0.7) { + // Later: more circle-like with middle bulge + chamberCircle.fillRect(-4, 0, 8, 2); // narrow top + chamberCircle.fillRect(-8, 2, 16, 2); // wider + chamberCircle.fillRect(-12, 4, 24, 2); // widest middle + chamberCircle.fillRect(-12, 6, 24, 2); // widest middle + chamberCircle.fillRect(-8, 8, 16, 2); // wider + chamberCircle.fillRect(-4, 10, 8, 2); // narrow bottom + } else if (progress < 0.9) { + // Almost full: near complete circle + chamberCircle.fillRect(-2, 0, 4, 2); // narrow top + chamberCircle.fillRect(-6, 2, 12, 2); // wider + chamberCircle.fillRect(-10, 4, 20, 2); // wider + chamberCircle.fillRect(-12, 6, 24, 2); // widest middle + chamberCircle.fillRect(-12, 8, 24, 2); // widest middle + chamberCircle.fillRect(-10, 10, 20, 2); // wider + chamberCircle.fillRect(-6, 12, 12, 2); // wider + chamberCircle.fillRect(-2, 14, 4, 2); // narrow bottom + } else { + // Full: complete pixel art circle + chamberCircle.fillRect(-2, 0, 4, 2); // narrow top + chamberCircle.fillRect(-6, 2, 12, 2); // wider + chamberCircle.fillRect(-10, 4, 20, 2); // wider + chamberCircle.fillRect(-12, 6, 24, 2); // widest middle + chamberCircle.fillRect(-12, 8, 24, 2); // widest middle + chamberCircle.fillRect(-10, 10, 20, 2); // wider + chamberCircle.fillRect(-6, 12, 12, 2); // wider + chamberCircle.fillRect(-2, 14, 4, 2); // narrow bottom + } + + // Update position + chamberCircle.y = circleData.y; + } + }); + + // Animate key pin moving down as a unit (staying connected to chamber hole) + const keyPinData = { + yOffset: 0 // How much the entire key pin moves down + }; + this.parent.scene.tweens.add({ + targets: keyPinData, + yOffset: KEY_PIN_TOP_SHRINK, // Move entire key pin down + duration: 1400, + ease: 'Cubic.easeInOut', + onUpdate: function() { + pin.keyPin.clear(); + pin.keyPin.fillStyle(0xdd3333); + + // Calculate position: entire key pin moves down as a unit + const originalTopY = -50 + pin.driverPinLength - pin.currentHeight; // Current top position (at shear line) + const newTopY = originalTopY + keyPinData.yOffset; // Entire key pin moves down + const newBottomY = newTopY + pin.keyPinLength; // Bottom position + + // Draw rectangular part of key pin (moves down as unit) + pin.keyPin.fillRect(-12, newTopY, 24, pin.keyPinLength - 8); + + // Draw triangular bottom in pixel art style (moves down with key pin) + pin.keyPin.fillRect(-12, newBottomY - 8, 24, 2); + pin.keyPin.fillRect(-10, newBottomY - 6, 20, 2); + pin.keyPin.fillRect(-8, newBottomY - 4, 16, 2); + pin.keyPin.fillRect(-6, newBottomY - 2, 12, 2); + } + }); + + // Animate key pin channel rectangle moving down with the channel circles + if (pin.channelRect) { + this.parent.scene.tweens.add({ + targets: pin.channelRect, + y: pin.channelRect.y + CHANNEL_MOVEMENT, // Move down by channel movement amount + duration: 1400, + ease: 'Cubic.easeInOut' + }); + } + }); + + // Animate the keyway shrinking (keeping bottom in place) to make cylinder appear to grow + const keywayData = { height: 90 }; + this.parent.scene.tweens.add({ + targets: keywayData, + height: 90 - KEYWAY_SHRINK, // Shrink by keyway shrink amount + duration: 1400, + ease: 'Cubic.easeInOut', + onUpdate: function() { + // Update keyway visual to show shrinking + // This would need to be implemented based on how the keyway is drawn + } + }); + } + + liftPinsWithKey() { + if (!this.parent.keyData || !this.parent.keyData.cuts) return; + + // Lift each pin to the correct height based on key cuts + this.parent.keyData.cuts.forEach((cutDepth, index) => { + if (index >= this.parent.pinCount) return; + + const pin = this.parent.pins[index]; + + // Calculate the height needed to lift the pin so it aligns at the shear line + const shearLineY = -45; // Shear line position + const keyPinTopAtShearLine = shearLineY; // Key pin top should be at shear line + const keyPinBottomAtRest = -50 + pin.driverPinLength + pin.keyPinLength; // Key pin bottom when not lifted + const requiredLift = keyPinBottomAtRest - keyPinTopAtShearLine; // How much to lift + + // The cut depth should match the required lift + const maxLift = pin.keyPinLength; // Maximum possible lift (full key pin height) + const requiredCutDepth = (requiredLift / maxLift) * 100; // Convert to percentage + + // Calculate the actual lift based on the key cut depth + const actualLift = (cutDepth / 100) * maxLift; + + // Animate pin to correct position + this.parent.scene.tweens.add({ + targets: { height: 0 }, + height: actualLift, + duration: 500, + ease: 'Cubic.easeOut', + onUpdate: (tween) => { + pin.currentHeight = tween.targets[0].height; + this.parent.updatePinVisuals(pin); + } + }); + }); + } + + lockPickingSuccess() { + // Animation configuration variables - easy to tweak + const KEY_PIN_TOP_SHRINK = 10; // How much the key pin top moves down + const KEY_PIN_BOTTOM_SHRINK = 5; // How much the key pin bottom moves up + const KEY_PIN_TOTAL_SHRINK = KEY_PIN_TOP_SHRINK + KEY_PIN_BOTTOM_SHRINK; // Total key pin shrink + const CHANNEL_MOVEMENT = 25; // How much channels move down + const KEYWAY_SHRINK = 20; // How much keyway shrinks + const WRENCH_VERTICAL_SHRINK = 60; // How much wrench vertical arm shrinks + const WRENCH_HORIZONTAL_SHRINK = 5; // How much wrench horizontal arm gets thinner + const WRENCH_MOVEMENT = 10; // How much wrench moves down + + this.parent.gameState.isActive = false; + + // Play success sound + if (this.parent.sounds.success) { + this.parent.sounds.success.play(); + if (typeof navigator !== 'undefined' && navigator.vibrate) { + navigator.vibrate(500); + } + } + + this.parent.updateFeedback("Lock picked successfully!"); + + // Shrink key pins downward and add half circles to simulate cylinder rotation + this.parent.pins.forEach(pin => { + // Hide all highlights + if (pin.shearHighlight) pin.shearHighlight.setVisible(false); + if (pin.setHighlight) pin.setHighlight.setVisible(false); + if (pin.bindingHighlight) pin.bindingHighlight.setVisible(false); + if (pin.overpickedHighlight) pin.overpickedHighlight.setVisible(false); + if (pin.failureHighlight) pin.failureHighlight.setVisible(false); + + // Create squashed circle that expands and moves to stay aligned with key pin top + const squashedCircle = this.parent.scene.add.graphics(); + //was 0xdd3333 Red color (key pin color) + squashedCircle.fillStyle(0xffffff); // white color for testing purposes + squashedCircle.x = pin.x; // Center horizontally on the pin + + // Start position: aligned with the top of the key pin + const startTopY = pin.y + (-50 + pin.driverPinLength); // Top of key pin position + squashedCircle.y = startTopY; + squashedCircle.setDepth(3); // Above driver pins so they're visible + + // Create a temporary object to hold the circle expansion data + const circleData = { + width: 24, // Start full width (same as key pin) + height: 2, // Start very thin (flat top) + y: startTopY + }; + + // Animate the squashed circle expanding to full circle (stays at top of key pin) + this.parent.scene.tweens.add({ + targets: circleData, + width: 24, // Full circle width (stays same) + height: 16, // Full circle height (expands from 2 to 16) + y: startTopY, // Stay at the top of the key pin (no movement) + duration: 1400, + ease: 'Cubic.easeInOut', + onUpdate: function() { + squashedCircle.clear(); + squashedCircle.fillStyle(0xff3333); // Red color (key pin color) + + // Calculate animation progress (0 to 1) + const progress = (circleData.height - 2) / (16 - 2); // From 2 to 16 height + + // Draw different circle shapes based on progress (widest in middle) + if (progress < 0.1) { + // Start: just a thin line (flat top) + squashedCircle.fillRect(-12, 0, 24, 2); + } else if (progress < 0.3) { + // Early: thin oval with middle bulge + squashedCircle.fillRect(-8, 0, 16, 2); // narrow top + squashedCircle.fillRect(-12, 2, 24, 2); // wide middle + squashedCircle.fillRect(-8, 4, 16, 2); // narrow bottom + } else if (progress < 0.5) { + // Middle: growing circle with middle bulge + squashedCircle.fillRect(-6, 0, 12, 2); // narrow top + squashedCircle.fillRect(-10, 2, 20, 2); // wider + squashedCircle.fillRect(-12, 4, 24, 2); // widest middle + squashedCircle.fillRect(-10, 6, 20, 2); // wider + squashedCircle.fillRect(-6, 8, 12, 2); // narrow bottom + } else if (progress < 0.7) { + // Later: more circle-like with middle bulge + squashedCircle.fillRect(-4, 0, 8, 2); // narrow top + squashedCircle.fillRect(-8, 2, 16, 2); // wider + squashedCircle.fillRect(-12, 4, 24, 2); // widest middle + squashedCircle.fillRect(-12, 6, 24, 2); // widest middle + squashedCircle.fillRect(-8, 8, 16, 2); // wider + squashedCircle.fillRect(-4, 10, 8, 2); // narrow bottom + } else if (progress < 0.9) { + // Almost full: near complete circle + squashedCircle.fillRect(-2, 0, 4, 2); // narrow top + squashedCircle.fillRect(-6, 2, 12, 2); // wider + squashedCircle.fillRect(-10, 4, 20, 2); // wider + squashedCircle.fillRect(-12, 6, 24, 2); // widest middle + squashedCircle.fillRect(-12, 8, 24, 2); // widest middle + squashedCircle.fillRect(-10, 10, 20, 2); // wider + squashedCircle.fillRect(-6, 12, 12, 2); // wider + squashedCircle.fillRect(-2, 14, 4, 2); // narrow bottom + } else { + // Full: complete pixel art circle + squashedCircle.fillRect(-2, 0, 4, 2); // narrow top + squashedCircle.fillRect(-6, 2, 12, 2); // wider + squashedCircle.fillRect(-10, 4, 20, 2); // wider + squashedCircle.fillRect(-12, 6, 24, 2); // widest middle + squashedCircle.fillRect(-12, 8, 24, 2); // widest middle + squashedCircle.fillRect(-10, 10, 20, 2); // wider + squashedCircle.fillRect(-6, 12, 12, 2); // wider + squashedCircle.fillRect(-2, 14, 4, 2); // narrow bottom + } + + // Update position + squashedCircle.y = circleData.y; + } + }); + + // Animate key pin shrinking from both top and bottom + const keyPinData = { height: pin.keyPinLength, topOffset: 0 }; + this.parent.scene.tweens.add({ + targets: keyPinData, + height: pin.keyPinLength - KEY_PIN_TOTAL_SHRINK, // Shrink by total amount + topOffset: KEY_PIN_TOP_SHRINK, // Move top down + duration: 1400, + ease: 'Cubic.easeInOut', + onUpdate: function() { + pin.keyPin.clear(); + pin.keyPin.fillStyle(0xdd3333); + + // Calculate new position: top moves down, bottom moves up + const originalTopY = -50 + pin.driverPinLength; // Original top of key pin + const newTopY = originalTopY + keyPinData.topOffset; // Top moves down + const newBottomY = newTopY + keyPinData.height; // Bottom position + + // Draw rectangular part of key pin (shrunk from both ends) + pin.keyPin.fillRect(-12, newTopY, 24, keyPinData.height - 8); + + // Draw triangular bottom in pixel art style (bottom moves up) + pin.keyPin.fillRect(-12, newBottomY - 8, 24, 2); + pin.keyPin.fillRect(-10, newBottomY - 6, 20, 2); + pin.keyPin.fillRect(-8, newBottomY - 4, 16, 2); + pin.keyPin.fillRect(-6, newBottomY - 2, 12, 2); + } + }); + + // Animate key pin channel rectangle moving down with the channel circles + this.parent.scene.tweens.add({ + targets: pin.channelRect, + y: pin.channelRect.y + CHANNEL_MOVEMENT, // Move down by channel movement amount + duration: 1400, + ease: 'Cubic.easeInOut' + }); + }); + + // Animate the keyway shrinking (keeping bottom in place) to make cylinder appear to grow + // Create a temporary object to hold the height value for tweening + const keywayData = { height: 90 }; + this.parent.scene.tweens.add({ + targets: keywayData, + height: 90 - KEYWAY_SHRINK, // Shrink by keyway shrink amount + duration: 1400, + ease: 'Cubic.easeInOut', + onUpdate: function() { + this.parent.keywayGraphics.clear(); + this.parent.keywayGraphics.fillStyle(0x2a2a2a); + // Move top down: y increases as height shrinks, keeping bottom at y=290 + const newY = 200 + (90 - keywayData.height); // Move top down + this.parent.keywayGraphics.fillRect(100, newY, 400, keywayData.height); + this.parent.keywayGraphics.lineStyle(1, 0x1a1a1a); + this.parent.keywayGraphics.strokeRect(100, newY, 400, keywayData.height); + }.bind(this) + }); + + // Animate tension wrench shrinking and moving down + if (this.parent.tensionWrench) { + // Create a temporary object to hold the height value for tweening + const wrenchData = { height: 170, y: 0, horizontalHeight: 10 }; // Original vertical arm height, y offset, and horizontal arm height + this.parent.scene.tweens.add({ + targets: wrenchData, + height: 170 - WRENCH_VERTICAL_SHRINK, // Shrink by vertical shrink amount + y: WRENCH_MOVEMENT, // Move entire wrench down + horizontalHeight: 10 - WRENCH_HORIZONTAL_SHRINK, // Make horizontal arm thinner + duration: 1400, + ease: 'Cubic.easeInOut', + onUpdate: function() { + // Update the wrench graphics (both active and inactive states) + this.parent.wrenchGraphics.clear(); + this.parent.wrenchGraphics.fillStyle(this.parent.lockState.tensionApplied ? 0x00ff00 : 0x888888); + + // Calculate new top position (move top down as height shrinks) + const originalTop = -120; // Original top position + const newTop = originalTop + (170 - wrenchData.height) + wrenchData.y; // Move top down and add y offset + + // Long vertical arm (left side of L) - top moves down and shrinks + this.parent.wrenchGraphics.fillRect(0, newTop, 10, wrenchData.height); + + // Short horizontal arm (bottom of L) - also moves down with top and gets thinner + this.parent.wrenchGraphics.fillRect(0, newTop + wrenchData.height, 37.5, wrenchData.horizontalHeight); + }.bind(this) + }); + } + + // Channel rectangles are already created during initial render + + // Animate pixel-art circles (channels) moving down from above the shear line + this.parent.pins.forEach(pin => { + // Calculate starting position: above the shear line (behind driver pins) + const pinX = pin.x; + const pinY = pin.y; + const shearLineY = -45; // Shear line position + const circleStartY = pinY + shearLineY - 20; // Start above shear line + const circleEndY = circleStartY + CHANNEL_MOVEMENT; // Move down same distance as cylinder + + // Create pixel-art circle graphics + const channelCircle = this.parent.scene.add.graphics(); + channelCircle.x = pinX; + channelCircle.y = circleStartY; + // Pixel-art circle: red color (like key pins) + const color = 0x333333; // Red color (key pin color) + channelCircle.fillStyle(color, 1); + // Create a proper circle shape with pixel-art steps (middle widest) + channelCircle.fillRect(-6, 0, 12, 2); // bottom (narrowest) + channelCircle.fillRect(-8, 2, 16, 2); // wider + channelCircle.fillRect(-10, 4, 20, 2); // wider + channelCircle.fillRect(-12, 6, 24, 2); // widest (middle) + channelCircle.fillRect(-12, 8, 24, 2); // widest (middle) + channelCircle.fillRect(-10, 10, 20, 2); // narrower + channelCircle.fillRect(-8, 12, 16, 2); // narrower + channelCircle.fillRect(-6, 14, 12, 2); // top (narrowest) + channelCircle.setDepth(1); // Normal depth for circles + + // Animate the circle moving down + this.parent.scene.tweens.add({ + targets: channelCircle, + y: circleEndY, + duration: 1400, + ease: 'Cubic.easeInOut', + }); + }); + + // Show success message immediately but delay the game completion + const successHTML = ` +
Lock picked successfully!
+ `; + // this.showSuccess(successHTML, false, 2000); + + // Delay the actual game completion until animation finishes + setTimeout(() => { + // Now trigger the success callback that unlocks the game + this.parent.showSuccess(successHTML, true, 2000); + this.parent.gameResult = { lockable: this.parent.lockable }; + }, 1500); // Wait 1.5 seconds (slightly longer than animation duration) + } + +} diff --git a/js/minigames/lockpicking/key-operations.js b/js/minigames/lockpicking/key-operations.js index aa34cf3..2827234 100644 --- a/js/minigames/lockpicking/key-operations.js +++ b/js/minigames/lockpicking/key-operations.js @@ -173,7 +173,7 @@ export class KeyOperations { this.parent.keyInsertionProgress = 1.0; // Fully inserted // Snap pins to exact final positions based on key cut dimensions - this.parent.snapPinsToExactPositions(); + this.parent.keyAnim.snapPinsToExactPositions(); this.checkKeyCorrectness(); } @@ -214,7 +214,7 @@ export class KeyOperations { // Start the rotation animation for correct key this.parent.scene.time.delayedCall(500, () => { - this.parent.startKeyRotationAnimationWithChamberHoles(); + this.parent.keyAnim.startKeyRotationAnimationWithChamberHoles(); }); // Complete the minigame after rotation animation diff --git a/js/minigames/lockpicking/lock-graphics.js b/js/minigames/lockpicking/lock-graphics.js index f1e6b31..3f0a62d 100644 --- a/js/minigames/lockpicking/lock-graphics.js +++ b/js/minigames/lockpicking/lock-graphics.js @@ -172,7 +172,7 @@ export class LockGraphics { this.parent.lockState.pinsSet = 0; } - this.parent.updateBindingPins(); + this.parent.pinMgmt.updateBindingPins(); }); } diff --git a/js/minigames/lockpicking/lockpicking-game-phaser.js b/js/minigames/lockpicking/lockpicking-game-phaser.js index e2bfef5..71a9055 100644 --- a/js/minigames/lockpicking/lockpicking-game-phaser.js +++ b/js/minigames/lockpicking/lockpicking-game-phaser.js @@ -4,6 +4,9 @@ import { LockGraphics } from './lock-graphics.js'; import { KeyDataGenerator } from './key-data-generator.js'; import { KeySelection } from './key-selection.js'; import { KeyOperations } from './key-operations.js'; +import { PinManagement } from './pin-management.js'; +import { ToolManager } from './tool-manager.js'; +import { KeyAnimation } from './key-animation.js'; // Phaser Lockpicking Minigame Scene implementation export class LockpickingMinigamePhaser extends MinigameScene { @@ -34,6 +37,15 @@ export class LockpickingMinigamePhaser extends MinigameScene { // Initialize KeyOperations module this.keyOps = new KeyOperations(this); + + // Initialize PinManagement module + this.pinMgmt = new PinManagement(this); + + // Initialize ToolManager module + this.toolMgr = new ToolManager(this); + + // Initialize KeyAnimation module + this.keyAnim = new KeyAnimation(this); } // Also try to load from localStorage for persistence across sessions @@ -134,7 +146,7 @@ export class LockpickingMinigamePhaser extends MinigameScene { // Add custom action based on closeButtonAction parameter if (this.closeButtonAction === 'reset') { this.addEventListener(closeBtn, 'click', () => { - this.resetAllPins(); + this.pinMgmt.resetAllPins(); this.updateFeedback("Lock reset - try again"); }); } else { @@ -158,7 +170,7 @@ export class LockpickingMinigamePhaser extends MinigameScene { // Add custom action based on closeButtonAction parameter if (this.closeButtonAction === 'reset') { this.addEventListener(cancelBtn, 'click', () => { - this.resetAllPins(); + this.pinMgmt.resetAllPins(); this.updateFeedback("Lock reset - try again"); }); } else { @@ -215,7 +227,7 @@ export class LockpickingMinigamePhaser extends MinigameScene { switchModeBtn.className = 'minigame-button'; switchModeBtn.id = 'lockpicking-switch-mode-btn'; switchModeBtn.innerHTML = 'Lockpick Switch to Lockpicking'; - switchModeBtn.onclick = () => this.switchToPickMode(); + switchModeBtn.onclick = () => this.toolMgr.switchToPickMode(); buttonContainer.appendChild(switchModeBtn); itemDisplayDiv.appendChild(buttonContainer); @@ -233,7 +245,7 @@ export class LockpickingMinigamePhaser extends MinigameScene { switchModeBtn.className = 'minigame-button'; switchModeBtn.id = 'lockpicking-switch-to-keys-btn'; switchModeBtn.innerHTML = 'Key Switch to Key Mode'; - switchModeBtn.onclick = () => this.switchToKeyMode(); + switchModeBtn.onclick = () => this.toolMgr.switchToKeyMode(); buttonContainer.appendChild(switchModeBtn); itemDisplayDiv.appendChild(buttonContainer); @@ -293,14 +305,14 @@ export class LockpickingMinigamePhaser extends MinigameScene { // Create game elements self.lockGraphics.createLockBackground(); self.lockGraphics.createTensionWrench(); - self.createPins(); + self.pinMgmt.createPins(); self.lockGraphics.createHookPick(); - self.createShearLine(); + self.pinMgmt.createShearLine(); // Create key if in key mode and not skipping starting key if (self.keyMode && !self.skipStartingKey) { self.keyOps.createKey(); - self.hideLockpickingTools(); + self.toolMgr.hideLockpickingTools(); self.updateFeedback("Click the key to insert it into the lock"); } else if (self.keyMode && self.skipStartingKey) { // Skip creating initial key, will show key selection instead @@ -308,13 +320,13 @@ export class LockpickingMinigamePhaser extends MinigameScene { if (!self.keyData) { self.keyDataGen.generateKeyDataFromPins(); } - self.hideLockpickingTools(); + self.toolMgr.hideLockpickingTools(); self.updateFeedback("Select a key to begin"); } else { self.updateFeedback("Apply tension first, then lift pins in binding order - only the binding pin can be set"); } - self.setupInputHandlers(); + self.pinMgmt.setupInputHandlers(); console.log('Phaser scene setup complete'); } @@ -897,337 +909,6 @@ export class LockpickingMinigamePhaser extends MinigameScene { } } - snapPinsToExactPositions() { - // Use selected key data for visual positioning, but original key data for correctness - const keyDataToUse = this.selectedKeyData || this.keyData; - if (!keyDataToUse || !keyDataToUse.cuts) return; - - console.log('Snapping pins to exact positions based on key cuts for shear line alignment'); - - // Ensure key data matches lock pin count - if (keyDataToUse.cuts.length !== this.pinCount) { - console.warn(`Key has ${keyDataToUse.cuts.length} cuts but lock has ${this.pinCount} pins. Adjusting key data.`); - // Truncate or pad cuts to match pin count - if (keyDataToUse.cuts.length > this.pinCount) { - keyDataToUse.cuts = keyDataToUse.cuts.slice(0, this.pinCount); - } else { - // Pad with default cuts if key has fewer cuts than lock has pins - while (keyDataToUse.cuts.length < this.pinCount) { - keyDataToUse.cuts.push(40); // Default cut depth - } - } - } - - // Set each pin to the exact final position based on key cut dimensions - keyDataToUse.cuts.forEach((cutDepth, index) => { - if (index >= this.pinCount) { - console.warn(`Key has ${keyDataToUse.cuts.length} cuts but lock only has ${this.pinCount} pins. Skipping cut ${index}.`); - return; - } - - const pin = this.pins[index]; - if (!pin) { - console.error(`Pin at index ${index} is undefined. Available pins: ${this.pins.length}`); - return; - } - - // Calculate the exact position where the pin should rest on the key cut - // The cut depth represents how deep the cut is from the blade top - // We need to position the pin so its bottom rests exactly on the cut surface - - // Key blade dimensions - const bladeHeight = this.keyConfig.bladeHeight; - const keyBladeBaseY = this.keyGroup.y - bladeHeight / 2; - - // Calculate the Y position of the cut surface - const cutSurfaceY = keyBladeBaseY + cutDepth; - - // Calculate where the pin bottom should be to rest on the cut surface - // Add safety check for undefined properties - if (!pin.driverPinLength || !pin.keyPinLength) { - console.warn(`Pin ${pin.index} missing length properties:`, pin); - return; // Skip this pin if properties are missing - } - const pinRestY = 200 - 50 + pin.driverPinLength + pin.keyPinLength; // Pin rest position - const targetKeyPinBottom = cutSurfaceY; - - // Calculate the exact lift needed to move pin bottom from rest to cut surface - const exactLift = pinRestY - targetKeyPinBottom; - - // Snap to exact position - pin.currentHeight = Math.max(0, exactLift); - - // Update pin visuals immediately - this.updatePinVisuals(pin); - - console.log(`Pin ${index}: cutDepth=${cutDepth}, cutSurfaceY=${cutSurfaceY}, exactLift=${exactLift}, currentHeight=${pin.currentHeight}, keyBladeBaseY=${keyBladeBaseY}, bladeHeight=${bladeHeight}`); - }); - - // Note: Rotation animation will be triggered by checkKeyCorrectness() only if key is correct - } - - startKeyRotationAnimationWithChamberHoles() { - // Animation configuration variables - same as lockpicking success - const KEY_PIN_TOP_SHRINK = 10; // How much the key pin top moves down - const KEY_PIN_BOTTOM_SHRINK = 5; // How much the key pin bottom moves up - const KEY_PIN_TOTAL_SHRINK = KEY_PIN_TOP_SHRINK + KEY_PIN_BOTTOM_SHRINK; // Total key pin shrink - const CHANNEL_MOVEMENT = 25; // How much channels move down - const KEYWAY_SHRINK = 20; // How much keyway shrinks - const KEY_SHRINK_FACTOR = 0.7; // How much the key shrinks on Y axis to simulate rotation - - // Play success sound - if (this.sounds.success) { - this.sounds.success.play(); - if (typeof navigator !== 'undefined' && navigator.vibrate) { - navigator.vibrate(500); - } - } - - this.updateFeedback("Key inserted successfully! Lock turning..."); - - // Create upper edge effect - a copy of the entire key group that stays in place - // Position at the key's current position (after insertion, before rotation) - const upperEdgeKeyGroup = this.scene.add.container(this.keyGroup.x, this.keyGroup.y); - upperEdgeKeyGroup.setDepth(0); // Behind the original key - - // Copy the handle (circle) - const upperEdgeHandle = this.scene.add.graphics(); - upperEdgeHandle.fillStyle(0xaaaaaa); // Slightly darker tone for the upper edge - upperEdgeHandle.fillCircle(this.keyConfig.circleRadius, 0, this.keyConfig.circleRadius); - upperEdgeKeyGroup.add(upperEdgeHandle); - - // Copy the shoulder and blade using render texture - const upperEdgeRenderTexture = this.scene.add.renderTexture(0, 0, this.keyRenderTexture.width, this.keyRenderTexture.height); - upperEdgeRenderTexture.setTint(0xaaaaaa); // Apply darker tone - upperEdgeRenderTexture.setOrigin(0, 0.5); // Match the original key's origin - upperEdgeKeyGroup.add(upperEdgeRenderTexture); - - // Draw the shoulder and blade to the upper edge render texture - const upperEdgeGraphics = this.scene.add.graphics(); - upperEdgeGraphics.fillStyle(0xaaaaaa); // Slightly darker tone - - // Draw shoulder - const shoulderX = this.keyConfig.circleRadius * 1.9; - upperEdgeGraphics.fillRect(shoulderX, 0, this.keyConfig.shoulderWidth, this.keyConfig.shoulderHeight); - - // Draw blade - adjust Y position to account for container offset - const bladeX = shoulderX + this.keyConfig.shoulderWidth; - const bladeY = this.keyConfig.shoulderHeight/2 - this.keyConfig.bladeHeight/2; - this.drawKeyBladeAsSolidShape(upperEdgeGraphics, bladeX, bladeY, this.keyConfig.bladeWidth, this.keyConfig.bladeHeight); - - upperEdgeRenderTexture.draw(upperEdgeGraphics); - upperEdgeGraphics.destroy(); - - // Initially hide the upper edge - upperEdgeKeyGroup.setVisible(false); - - // Animate key shrinking on Y axis to simulate rotation - this.scene.tweens.add({ - targets: this.keyGroup, - scaleY: KEY_SHRINK_FACTOR, - duration: 1400, - ease: 'Cubic.easeInOut', - onStart: () => { - // Show the upper edge when rotation starts - upperEdgeKeyGroup.setVisible(true); - } - }); - - // Animate the upper edge copy to shrink and move upward (keeping top edge in place) - this.scene.tweens.add({ - targets: upperEdgeKeyGroup, - scaleY: KEY_SHRINK_FACTOR, - y: upperEdgeKeyGroup.y - 6, // Simple upward movement - duration: 1400, - ease: 'Cubic.easeInOut' - }); - - // Shrink key pins downward and add half circles to simulate cylinder rotation - this.pins.forEach(pin => { - // Hide all highlights - if (pin.shearHighlight) pin.shearHighlight.setVisible(false); - if (pin.setHighlight) pin.setHighlight.setVisible(false); - if (pin.bindingHighlight) pin.bindingHighlight.setVisible(false); - if (pin.overpickedHighlight) pin.overpickedHighlight.setVisible(false); - if (pin.failureHighlight) pin.failureHighlight.setVisible(false); - - // Create chamber hole circle that expands at the actual chamber position - const chamberCircle = this.scene.add.graphics(); - chamberCircle.fillStyle(0x666666); // Dark gray color for chamber holes - chamberCircle.x = pin.x; // Center horizontally on the pin - - // Position at actual chamber hole location (shear line) - const chamberY = pin.y + (-45); // Shear line position - chamberCircle.y = chamberY; - chamberCircle.setDepth(5); // Above all other elements - - // Create a temporary object to hold the circle expansion data - const circleData = { - width: 24, // Start full width (same as key pin) - height: 2, // Start very thin (flat top) - y: chamberY - }; - - // Animate the chamber hole circle expanding to full circle (stays at chamber position) - this.scene.tweens.add({ - targets: circleData, - width: 24, // Full circle width (stays same) - height: 16, // Full circle height (expands from 2 to 16) - y: chamberY, // Stay at the chamber position (no movement) - duration: 1400, - ease: 'Cubic.easeInOut', - onUpdate: function() { - chamberCircle.clear(); - chamberCircle.fillStyle(0xff0000); // Light red for chamber holes filled with key pin - - // Calculate animation progress (0 to 1) - const progress = (circleData.height - 2) / (16 - 2); // From 2 to 16 height - - // Draw different circle shapes based on progress (widest in middle) - if (progress < 0.1) { - // Start: just a thin line (flat top) - chamberCircle.fillRect(-12, 0, 24, 2); - } else if (progress < 0.3) { - // Early: thin oval with middle bulge - chamberCircle.fillRect(-8, 0, 16, 2); // narrow top - chamberCircle.fillRect(-12, 2, 24, 2); // wide middle - chamberCircle.fillRect(-8, 4, 16, 2); // narrow bottom - } else if (progress < 0.5) { - // Middle: growing circle with middle bulge - chamberCircle.fillRect(-6, 0, 12, 2); // narrow top - chamberCircle.fillRect(-10, 2, 20, 2); // wider - chamberCircle.fillRect(-12, 4, 24, 2); // widest middle - chamberCircle.fillRect(-10, 6, 20, 2); // wider - chamberCircle.fillRect(-6, 8, 12, 2); // narrow bottom - } else if (progress < 0.7) { - // Later: more circle-like with middle bulge - chamberCircle.fillRect(-4, 0, 8, 2); // narrow top - chamberCircle.fillRect(-8, 2, 16, 2); // wider - chamberCircle.fillRect(-12, 4, 24, 2); // widest middle - chamberCircle.fillRect(-12, 6, 24, 2); // widest middle - chamberCircle.fillRect(-8, 8, 16, 2); // wider - chamberCircle.fillRect(-4, 10, 8, 2); // narrow bottom - } else if (progress < 0.9) { - // Almost full: near complete circle - chamberCircle.fillRect(-2, 0, 4, 2); // narrow top - chamberCircle.fillRect(-6, 2, 12, 2); // wider - chamberCircle.fillRect(-10, 4, 20, 2); // wider - chamberCircle.fillRect(-12, 6, 24, 2); // widest middle - chamberCircle.fillRect(-12, 8, 24, 2); // widest middle - chamberCircle.fillRect(-10, 10, 20, 2); // wider - chamberCircle.fillRect(-6, 12, 12, 2); // wider - chamberCircle.fillRect(-2, 14, 4, 2); // narrow bottom - } else { - // Full: complete pixel art circle - chamberCircle.fillRect(-2, 0, 4, 2); // narrow top - chamberCircle.fillRect(-6, 2, 12, 2); // wider - chamberCircle.fillRect(-10, 4, 20, 2); // wider - chamberCircle.fillRect(-12, 6, 24, 2); // widest middle - chamberCircle.fillRect(-12, 8, 24, 2); // widest middle - chamberCircle.fillRect(-10, 10, 20, 2); // wider - chamberCircle.fillRect(-6, 12, 12, 2); // wider - chamberCircle.fillRect(-2, 14, 4, 2); // narrow bottom - } - - // Update position - chamberCircle.y = circleData.y; - } - }); - - // Animate key pin moving down as a unit (staying connected to chamber hole) - const keyPinData = { - yOffset: 0 // How much the entire key pin moves down - }; - this.scene.tweens.add({ - targets: keyPinData, - yOffset: KEY_PIN_TOP_SHRINK, // Move entire key pin down - duration: 1400, - ease: 'Cubic.easeInOut', - onUpdate: function() { - pin.keyPin.clear(); - pin.keyPin.fillStyle(0xdd3333); - - // Calculate position: entire key pin moves down as a unit - const originalTopY = -50 + pin.driverPinLength - pin.currentHeight; // Current top position (at shear line) - const newTopY = originalTopY + keyPinData.yOffset; // Entire key pin moves down - const newBottomY = newTopY + pin.keyPinLength; // Bottom position - - // Draw rectangular part of key pin (moves down as unit) - pin.keyPin.fillRect(-12, newTopY, 24, pin.keyPinLength - 8); - - // Draw triangular bottom in pixel art style (moves down with key pin) - pin.keyPin.fillRect(-12, newBottomY - 8, 24, 2); - pin.keyPin.fillRect(-10, newBottomY - 6, 20, 2); - pin.keyPin.fillRect(-8, newBottomY - 4, 16, 2); - pin.keyPin.fillRect(-6, newBottomY - 2, 12, 2); - } - }); - - // Animate key pin channel rectangle moving down with the channel circles - if (pin.channelRect) { - this.scene.tweens.add({ - targets: pin.channelRect, - y: pin.channelRect.y + CHANNEL_MOVEMENT, // Move down by channel movement amount - duration: 1400, - ease: 'Cubic.easeInOut' - }); - } - }); - - // Animate the keyway shrinking (keeping bottom in place) to make cylinder appear to grow - const keywayData = { height: 90 }; - this.scene.tweens.add({ - targets: keywayData, - height: 90 - KEYWAY_SHRINK, // Shrink by keyway shrink amount - duration: 1400, - ease: 'Cubic.easeInOut', - onUpdate: function() { - // Update keyway visual to show shrinking - // This would need to be implemented based on how the keyway is drawn - } - }); - } - - - - - - liftPinsWithKey() { - if (!this.keyData || !this.keyData.cuts) return; - - // Lift each pin to the correct height based on key cuts - this.keyData.cuts.forEach((cutDepth, index) => { - if (index >= this.pinCount) return; - - const pin = this.pins[index]; - - // Calculate the height needed to lift the pin so it aligns at the shear line - const shearLineY = -45; // Shear line position - const keyPinTopAtShearLine = shearLineY; // Key pin top should be at shear line - const keyPinBottomAtRest = -50 + pin.driverPinLength + pin.keyPinLength; // Key pin bottom when not lifted - const requiredLift = keyPinBottomAtRest - keyPinTopAtShearLine; // How much to lift - - // The cut depth should match the required lift - const maxLift = pin.keyPinLength; // Maximum possible lift (full key pin height) - const requiredCutDepth = (requiredLift / maxLift) * 100; // Convert to percentage - - // Calculate the actual lift based on the key cut depth - const actualLift = (cutDepth / 100) * maxLift; - - // Animate pin to correct position - this.scene.tweens.add({ - targets: { height: 0 }, - height: actualLift, - duration: 500, - ease: 'Cubic.easeOut', - onUpdate: (tween) => { - pin.currentHeight = tween.targets[0].height; - this.updatePinVisuals(pin); - } - }); - }); - } - updatePinsWithKeyInsertion(progress) { if (!this.keyConfig) return; @@ -1629,24 +1310,6 @@ export class LockpickingMinigamePhaser extends MinigameScene { return this.keyOps.getKeySurfaceHeightAtPosition(pinX, keyBladeStartX); } - hideLockpickingTools() { - // Hide tension wrench and hook pick in key mode - if (this.tensionWrench) { - this.tensionWrench.setVisible(false); - } - if (this.hookGroup) { - this.hookGroup.setVisible(false); - } - - // Hide labels - if (this.wrenchText) { - this.wrenchText.setVisible(false); - } - if (this.hookPickLabel) { - this.hookPickLabel.setVisible(false); - } - } - updateHookPosition(pinIndex) { if (!this.hookGroup || !this.hookConfig) return; @@ -1710,37 +1373,6 @@ export class LockpickingMinigamePhaser extends MinigameScene { console.log('Hook update - pinX:', pinX, 'newTipX:', newTipX, 'currentTargetY:', currentTargetY, 'heightDifference:', heightDifference, 'pinMaxRotation:', pinMaxRotation, 'pinMovementPercentage:', pinMovementPercentage.toFixed(1) + '%', 'pinSpeedFactor:', pinSpeedFactor.toFixed(2), 'rotationAngle:', rotationAngle.toFixed(1)); } - returnHookToStart() { - if (!this.hookGroup || !this.hookConfig) return; - - const config = this.hookConfig; - - console.log('Returning hook to starting position (no rotation)'); - - // Get the current X position from the last targeted pin - const pinSpacing = 400 / (this.pinCount + 1); - const margin = pinSpacing * 0.75; - const targetPinIndex = config.lastTargetedPin; - const currentX = 100 + margin + targetPinIndex * pinSpacing; // Last targeted pin's X position - - // Calculate the tip position for the current pin - const totalHookHeight = (config.diagonalSegments + config.verticalSegments) * config.segmentStep; - const tipX = currentX - totalHookHeight + 48; // Add 48px offset (24px + 24px further right) - - // Calculate resting Y position (a few pixels lower than original) - const restingY = config.hookStartY - 24; // 24px lower than original position (was 15px) - - // Reset position and rotation - this.hookGroup.x = tipX; - this.hookGroup.y = restingY; - this.hookGroup.setAngle(0); - - // Clear debug graphics when hook returns to start - if (this.debugGraphics) { - this.debugGraphics.clear(); - } - } - checkHookCollisions(targetPinIndex, hookCurrentY) { if (!this.hookConfig || !this.gameState.mouseDown) return; @@ -1950,562 +1582,6 @@ export class LockpickingMinigamePhaser extends MinigameScene { } } - createPins() { - // Create random binding order - const bindingOrder = []; - for (let i = 0; i < this.pinCount; i++) { - bindingOrder.push(i); - } - this.shuffleArray(bindingOrder); - - const pinSpacing = 400 / (this.pinCount + 1); - const margin = pinSpacing * 0.75; // 25% smaller margins - - // Try to load saved pin heights for this lock - const savedPinHeights = this.lockConfig.loadLockConfiguration(); - - // Check if predefined pin heights were passed - const predefinedPinHeights = this.params?.predefinedPinHeights; - - console.log(`DEBUG: Lockpicking minigame received parameters:`); - console.log(` - pinCount: ${this.pinCount}`); - console.log(` - this.params:`, this.params); - console.log(` - predefinedPinHeights: [${predefinedPinHeights ? predefinedPinHeights.join(', ') : 'none'}]`); - console.log(` - savedPinHeights: [${savedPinHeights ? savedPinHeights.join(', ') : 'none'}]`); - - for (let i = 0; i < this.pinCount; i++) { - const pinX = 100 + margin + i * pinSpacing; - const pinY = 200; - - // Use predefined pin heights if available, otherwise use saved or generate random ones - let keyPinLength, driverPinLength; - if (predefinedPinHeights && predefinedPinHeights[i] !== undefined) { - // Use predefined configuration - keyPinLength = predefinedPinHeights[i]; - driverPinLength = 75 - keyPinLength; // Total height is 75 - console.log(`✓ Pin ${i}: Using predefined pin height: ${keyPinLength} (driver: ${driverPinLength})`); - } else if (savedPinHeights && savedPinHeights[i] !== undefined) { - // Use saved configuration - keyPinLength = savedPinHeights[i]; - driverPinLength = 75 - keyPinLength; // Total height is 75 - console.log(`✓ Pin ${i}: Using saved pin height: ${keyPinLength} (driver: ${driverPinLength})`); - } else { - // Generate random pin lengths that add up to 75 (total height - 25% increase from 60) - keyPinLength = 25 + Math.random() * 37.5; // 25-62.5 (25% increase) - driverPinLength = 75 - keyPinLength; // Remaining to make 75 total - console.log(`⚠ Pin ${i}: Generated random pin height: ${keyPinLength} (driver: ${driverPinLength})`); - } - - const pin = { - index: i, - binding: bindingOrder[i], - isSet: false, - currentHeight: 0, - originalHeight: keyPinLength, // Store original height for consistency - keyPinHeight: 0, // Track key pin position separately - driverPinHeight: 0, // Track driver pin position separately - keyPinLength: keyPinLength, - driverPinLength: driverPinLength, - x: pinX, - y: pinY, - container: null, - keyPin: null, - driverPin: null, - spring: null - }; - - // Ensure pin properties are valid - if (!pin.keyPinLength || !pin.driverPinLength) { - console.error(`Pin ${i} created with invalid lengths:`, pin); - pin.keyPinLength = pin.keyPinLength || 30; // Default fallback - pin.driverPinLength = pin.driverPinLength || 45; // Default fallback - } - - // Create pin container - pin.container = this.scene.add.container(pinX, pinY); - - // Add all highlights FIRST (so they appear behind pins) - // Add hover effect using a highlight rectangle - 25% less wide, full height from spring top to pin bottom (extended down) - pin.highlight = this.scene.add.graphics(); - pin.highlight.fillStyle(0xffff00, 0.3); - pin.highlight.fillRect(-22.5, -110, 45, 140); - pin.highlight.setVisible(false); - pin.container.add(pin.highlight); - - // Add overpicked highlight - pin.overpickedHighlight = this.scene.add.graphics(); - pin.overpickedHighlight.fillStyle(0xff0000, 0.6); - pin.overpickedHighlight.fillRect(-22.5, -110, 45, 140); - pin.overpickedHighlight.setVisible(false); - pin.container.add(pin.overpickedHighlight); - - // Add failure highlight for overpicked set pins - pin.failureHighlight = this.scene.add.graphics(); - pin.failureHighlight.fillStyle(0xff6600, 0.7); - pin.failureHighlight.fillRect(-22.5, -110, 45, 140); - pin.failureHighlight.setVisible(false); - pin.container.add(pin.failureHighlight); - - // Create spring (top part) - 12 segments with correct initial spacing - pin.spring = this.scene.add.graphics(); - pin.spring.fillStyle(0x666666); - const springTop = -130; - const springBottom = -50; // Driver pin top when not lifted - const springHeight = springBottom - springTop; - - // Calculate total spring space and distribute segments evenly - const totalSpringSpace = springHeight; - const segmentSpacing = totalSpringSpace / 11; // 11 gaps between 12 segments - - for (let s = 0; s < 12; s++) { - const segmentY = springTop + (s * segmentSpacing); - pin.spring.fillRect(-12, segmentY, 24, 4); - } - pin.container.add(pin.spring); - - // Create driver pin (middle part) - starts at y=-50 - pin.driverPin = this.scene.add.graphics(); - pin.driverPin.fillStyle(0x3388dd); - pin.driverPin.fillRect(-12, -50, 24, driverPinLength); - pin.container.add(pin.driverPin); - - // Set container depth to ensure driver pins are above circles - pin.container.setDepth(2); - - // Create key pin (bottom part) - starts below driver pin with triangular bottom - pin.keyPin = this.scene.add.graphics(); - pin.keyPin.fillStyle(0xdd3333); - - // Draw rectangular part of key pin - pin.keyPin.fillRect(-12, -50 + driverPinLength, 24, keyPinLength - 8); - - // Draw triangular bottom in pixel art style - pin.keyPin.fillRect(-12, -50 + driverPinLength + keyPinLength - 8, 24, 2); - pin.keyPin.fillRect(-10, -50 + driverPinLength + keyPinLength - 6, 20, 2); - pin.keyPin.fillRect(-8, -50 + driverPinLength + keyPinLength - 4, 16, 2); - pin.keyPin.fillRect(-6, -50 + driverPinLength + keyPinLength - 2, 12, 2); - - pin.container.add(pin.keyPin); - - // Add labels for pin components (only for the first pin to avoid clutter) - if (i === 0) { - // Spring label - const springLabel = this.scene.add.text(pinX, pinY - 140, 'Spring', { - fontSize: '18px', - fontFamily: 'VT323', - fill: '#00ff00', - fontWeight: 'bold' - }); - springLabel.setOrigin(0.5); - springLabel.setDepth(100); // Bring to front - - // Driver pin label - positioned below the shear line - const driverPinX = 100 + margin + 1 * pinSpacing; // Pin index 1 (2nd pin) - const driverPinLabel = this.scene.add.text(driverPinX, pinY - 35, 'Driver Pin', { - fontSize: '18px', - fontFamily: 'VT323', - fill: '#00ff00', - fontWeight: 'bold' - }); - driverPinLabel.setOrigin(0.5); - driverPinLabel.setDepth(100); // Bring to front - - // Key pin label - positioned at the middle of the key pin - const keyPinX = 100 + margin + 2 * pinSpacing; // Pin index 2 (3rd pin) - const keyPinLabel = this.scene.add.text(keyPinX, pinY - 50 + driverPinLength + (keyPinLength / 2), 'Key Pin', { - fontSize: '18px', - fontFamily: 'VT323', - fill: '#00ff00', - fontWeight: 'bold' - }); - keyPinLabel.setOrigin(0.5); - keyPinLabel.setDepth(100); // Bring to front - - // Store references to labels for hiding - this.springLabel = springLabel; - this.driverPinLabel = driverPinLabel; - this.keyPinLabel = keyPinLabel; - } - - // Create channel rectangle (keyway for this pin) - above cylinder but behind key pins - const shearLineY = -45; // Shear line position - const keywayTopY = 200; // Top of the main keyway - const channelHeight = keywayTopY - (pinY + shearLineY); // From keyway to shear line - - // Create channel rectangle graphics - pin.channelRect = this.scene.add.graphics(); - pin.channelRect.x = pinX; - pin.channelRect.y = pinY + shearLineY - 15; // Start at circle start position (20px above shear line) - pin.channelRect.fillStyle(0x2a2a2a, 1); // Same color as keyway - pin.channelRect.fillRect(-13, 3, 26, channelHeight + 15 - 3); // 3px margin except at shear line - pin.channelRect.setDepth(0); // Behind key pins but above cylinder - - // Add border to match keyway style - pin.channelRect.lineStyle(1, 0x1a1a1a); - pin.channelRect.strokeRect(-13, 3, 26, channelHeight + 20 - 3); - - // Create spring channel rectangle - behind spring, above cylinder - const springChannelHeight = springBottom - springTop; // Spring height - - // Create spring channel rectangle graphics - pin.springChannelRect = this.scene.add.graphics(); - pin.springChannelRect.x = pinX; - pin.springChannelRect.y = pinY + springTop; // Start at spring top - pin.springChannelRect.fillStyle(0x2a2a2a, 1); // Same color as keyway - pin.springChannelRect.fillRect(-13, 3, 26, springChannelHeight - 3); // 3px margin except at shear line - pin.springChannelRect.setDepth(1); // Behind spring but above cylinder - - // Add border to match keyway style - pin.springChannelRect.lineStyle(1, 0x1a1a1a); - pin.springChannelRect.strokeRect(-13, 3, 26, springChannelHeight - 3); - - // Make pin interactive - 25% less wide, full height from spring top to bottom of keyway (extended down) - pin.container.setInteractive(new Phaser.Geom.Rectangle(-18.75, -110, 37.5, 230), Phaser.Geom.Rectangle.Contains); - - // Add pin number - const pinText = this.scene.add.text(0, 40, (i + 1).toString(), { - fontSize: '18px', - fontFamily: 'VT323', - fill: '#ffffff', - fontWeight: 'bold' - }); - pinText.setOrigin(0.5); - pin.container.add(pinText); - - // Store reference to pin text for hiding - pin.pinText = pinText; - - pin.container.on('pointerover', () => { - if (this.lockState.tensionApplied && !pin.isSet) { - pin.highlight.setVisible(true); - } - }); - - pin.container.on('pointerout', () => { - pin.highlight.setVisible(false); - }); - - // Add event handlers - pin.container.on('pointerdown', () => { - console.log('Pin clicked:', pin.index); - this.lockState.currentPin = pin; - this.gameState.mouseDown = true; - console.log('Pin interaction started'); - - // Play click sound - if (this.sounds.click) { - this.sounds.click.play(); - if (typeof navigator !== 'undefined' && navigator.vibrate) { - navigator.vibrate(50); - } - } - - // Hide labels on first pin click - if (!this.pinClicked) { - this.pinClicked = true; - if (this.wrenchText) { - this.wrenchText.setVisible(false); - } - if (this.shearLineText) { - this.shearLineText.setVisible(false); - } - if (this.hookPickLabel) { - this.hookPickLabel.setVisible(false); - } - if (this.springLabel) { - this.springLabel.setVisible(false); - } - if (this.driverPinLabel) { - this.driverPinLabel.setVisible(false); - } - if (this.keyPinLabel) { - this.keyPinLabel.setVisible(false); - } - - // Hide all pin numbers - this.pins.forEach(pin => { - if (pin.pinText) { - pin.pinText.setVisible(false); - } - }); - } - - if (!this.lockState.tensionApplied) { - this.updateFeedback("Apply tension first before picking pins"); - this.flashWrenchRed(); - } - }); - - this.pins.push(pin); - } - - // Save the lock configuration after all pins are created - this.lockConfig.saveLockConfiguration(); - } - - createShearLine() { - // Create a more visible shear line at y=155 (which is -45 in pin coordinates) - const graphics = this.scene.add.graphics(); - graphics.lineStyle(3, 0x00ff00); - graphics.beginPath(); - graphics.moveTo(100, 155); - graphics.lineTo(500, 155); - graphics.strokePath(); - - // Add a dashed line effect - graphics.lineStyle(1, 0x00ff00, 0.5); - for (let x = 100; x < 500; x += 10) { - graphics.beginPath(); - graphics.moveTo(x, 150); - graphics.lineTo(x, 160); - graphics.strokePath(); - } - - // Add shear line label - const shearLineText = this.scene.add.text(430, 135, 'SHEAR LINE', { - fontSize: '16px', - fontFamily: 'VT323', - fill: '#00ff00', - fontWeight: 'bold' - }); - shearLineText.setDepth(100); // Bring to front - - // Store reference to shear line text for hiding - this.shearLineText = shearLineText; - - // // Add instruction text - // this.scene.add.text(300, 180, 'Align key/driver pins at the shear line', { - // fontSize: '12px', - // fill: '#00ff00', - // fontStyle: 'italic' - // }).setOrigin(0.5); - } - - setupInputHandlers() { - this.scene.input.on('pointerup', () => { - if (this.lockState.currentPin) { - this.checkPinSet(this.lockState.currentPin); - this.lockState.currentPin = null; - } - this.gameState.mouseDown = false; - - // Only return hook to resting position if not in key mode - if (!this.keyMode && this.hookPickGraphics && this.hookConfig) { - this.returnHookToStart(); - } - - // Stop key insertion if in key mode - if (this.keyMode) { - this.keyInserting = false; - } - }); - - // Add keyboard bindings - this.scene.input.keyboard.on('keydown', (event) => { - const key = event.key; - - // Pin number keys (1-8) - if (key >= '1' && key <= '8') { - const pinIndex = parseInt(key) - 1; // Convert 1-8 to 0-7 - - // Check if pin exists - if (pinIndex < this.pinCount) { - const pin = this.pins[pinIndex]; - if (pin) { - // Simulate pin click - this.lockState.currentPin = pin; - this.gameState.mouseDown = true; - - // Play click sound - if (this.sounds.click) { - this.sounds.click.play(); - if (typeof navigator !== 'undefined' && navigator.vibrate) { - navigator.vibrate(50); - } - } - - // Hide labels on first pin click - if (!this.pinClicked) { - this.pinClicked = true; - if (this.wrenchText) { - this.wrenchText.setVisible(false); - } - if (this.shearLineText) { - this.shearLineText.setVisible(false); - } - if (this.hookPickLabel) { - this.hookPickLabel.setVisible(false); - } - if (this.springLabel) { - this.springLabel.setVisible(false); - } - if (this.driverPinLabel) { - this.driverPinLabel.setVisible(false); - } - if (this.keyPinLabel) { - this.keyPinLabel.setVisible(false); - } - - // Hide all pin numbers - this.pins.forEach(pin => { - if (pin.pinText) { - pin.pinText.setVisible(false); - } - }); - } - - if (!this.lockState.tensionApplied) { - this.updateFeedback("Apply tension first before picking pins"); - this.flashWrenchRed(); - } - } - } - } - - // SPACE key for tension wrench toggle - if (key === ' ') { - event.preventDefault(); // Prevent page scroll - - // Simulate tension wrench click - this.lockState.tensionApplied = !this.lockState.tensionApplied; - - // Play tension sound - if (this.sounds.tension) { - this.sounds.tension.play(); - if (typeof navigator !== 'undefined' && navigator.vibrate) { - navigator.vibrate([200]); - } - } - - if (this.lockState.tensionApplied) { - this.wrenchGraphics.clear(); - this.wrenchGraphics.fillStyle(0x00ff00); - - // Long vertical arm (left side of L) - same dimensions as inactive - this.wrenchGraphics.fillRect(0, -120, 10, 170); - - // Short horizontal arm (bottom of L) extending into keyway - same dimensions as inactive - this.wrenchGraphics.fillRect(0, 40, 37.5, 10); - - this.updateFeedback("Tension applied. Only the binding pin can be set - others will fall back down."); - } else { - this.wrenchGraphics.clear(); - this.wrenchGraphics.fillStyle(0x888888); - - // Long vertical arm (left side of L) - same dimensions as active - this.wrenchGraphics.fillRect(0, -120, 10, 170); - - // Short horizontal arm (bottom of L) extending into keyway - same dimensions as active - this.wrenchGraphics.fillRect(0, 40, 37.5, 10); - - this.updateFeedback("Tension released. All pins will fall back down."); - - // Play reset sound - if (this.sounds.reset) { - this.sounds.reset.play(); - } - - // Reset ALL pins when tension is released (including set and overpicked ones) - this.pins.forEach(pin => { - pin.isSet = false; - pin.isOverpicked = false; - pin.currentHeight = 0; - pin.keyPinHeight = 0; // Reset key pin height - pin.driverPinHeight = 0; // Reset driver pin height - pin.overpickingTimer = null; // Reset overpicking timer - - // Reset visual - pin.keyPin.clear(); - pin.keyPin.fillStyle(0xdd3333); - - // Draw rectangular part of key pin - pin.keyPin.fillRect(-12, -50 + pin.driverPinLength, 24, pin.keyPinLength - 8); - - // Draw triangular bottom in pixel art style - pin.keyPin.fillRect(-12, -50 + pin.driverPinLength + pin.keyPinLength - 8, 24, 2); - pin.keyPin.fillRect(-10, -50 + pin.driverPinLength + pin.keyPinLength - 6, 20, 2); - pin.keyPin.fillRect(-8, -50 + pin.driverPinLength + pin.keyPinLength - 4, 16, 2); - pin.keyPin.fillRect(-6, -50 + pin.driverPinLength + pin.keyPinLength - 2, 12, 2); - - pin.driverPin.clear(); - pin.driverPin.fillStyle(0x3388dd); - pin.driverPin.fillRect(-12, -50, 24, pin.driverPinLength); - - // Reset spring to original position - pin.spring.clear(); - pin.spring.fillStyle(0x666666); - const springTop = -130; // Fixed spring top - const springBottom = -50; // Driver pin top when not lifted - const springHeight = springBottom - springTop; - - // Calculate total spring space and distribute segments evenly - const totalSpringSpace = springHeight; - const segmentSpacing = totalSpringSpace / 11; // 11 gaps between 12 segments - - for (let s = 0; s < 12; s++) { - const segmentHeight = 4; - const segmentY = springTop + (s * segmentSpacing); - pin.spring.fillRect(-12, segmentY, 24, segmentHeight); - } - - // Hide all highlights - if (pin.shearHighlight) pin.shearHighlight.setVisible(false); - if (pin.setHighlight) pin.setHighlight.setVisible(false); - if (pin.bindingHighlight) pin.bindingHighlight.setVisible(false); - if (pin.overpickedHighlight) pin.overpickedHighlight.setVisible(false); - if (pin.failureHighlight) pin.failureHighlight.setVisible(false); - }); - - // Reset lock state - this.lockState.pinsSet = 0; - } - - this.updateBindingPins(); - } - }); - - // Add keyboard release handler for pin keys - this.scene.input.keyboard.on('keyup', (event) => { - const key = event.key; - - // Pin number keys (1-8) - if (key >= '1' && key <= '8') { - const pinIndex = parseInt(key) - 1; // Convert 1-8 to 0-7 - - // Check if pin exists and is currently being held - if (pinIndex < this.pinCount && this.lockState.currentPin && this.lockState.currentPin.index === pinIndex) { - this.checkPinSet(this.lockState.currentPin); - this.lockState.currentPin = null; - this.gameState.mouseDown = false; - - // Return hook to resting position - if (this.hookPickGraphics && this.hookConfig) { - this.returnHookToStart(); - } - } - } - }); - - // Add key interaction handlers if in key mode - if (this.keyMode && this.keyClickZone) { - console.log('Setting up key click handler...'); - this.keyClickZone.on('pointerdown', (pointer) => { - console.log('Key clicked! Event triggered.'); - // Prevent this event from bubbling up to global handlers - pointer.event.stopPropagation(); - - if (!this.keyInserting) { - console.log('Starting key insertion animation...'); - this.keyOps.startKeyInsertion(); - } else { - console.log('Key insertion already in progress, ignoring click.'); - } - }); - } else { - console.log('Key mode or click zone not available:', { keyMode: this.keyMode, hasClickZone: !!this.keyClickZone }); - } - } - update() { // Skip normal lockpicking logic if in key mode if (this.keyMode) { @@ -2513,453 +1589,27 @@ export class LockpickingMinigamePhaser extends MinigameScene { } if (this.lockState.currentPin && this.gameState.mouseDown) { - this.liftPin(); + this.pinMgmt.liftPin(); } // Apply gravity when tension is not applied (but not when actively lifting) if (!this.lockState.tensionApplied && !this.gameState.mouseDown) { - this.applyGravity(); + this.pinMgmt.applyGravity(); } // Apply gravity to non-binding pins even with tension if (this.lockState.tensionApplied && !this.gameState.mouseDown) { - this.applyGravity(); + this.pinMgmt.applyGravity(); } // Check if all pins are correctly positioned when tension is applied if (this.lockState.tensionApplied) { - this.checkAllPinsCorrect(); + this.pinMgmt.checkAllPinsCorrect(); } // Hook return is now handled directly in pointerup event } - liftPin() { - if (!this.lockState.currentPin || !this.gameState.mouseDown) return; - - const pin = this.lockState.currentPin; - const liftSpeed = this.liftSpeed; - const shearLineY = -45; - - // If pin is set and not already overpicked, allow key pin to move up, driver pin stays at SL - if (pin.isSet && !pin.isOverpicked) { - // Move key pin up gradually from its dropped position (slower when not connected to driver pin) - const keyPinLiftSpeed = liftSpeed * 0.5; // Half speed for key pin movement - // Key pin should stop when its top surface reaches the shear line - // The key pin's top is at: -50 + pin.driverPinLength - pin.keyPinHeight - // We want this to equal -45 (shear line) - // So: -50 + pin.driverPinLength - pin.keyPinHeight = -45 - // Therefore: pin.keyPinHeight = pin.driverPinLength - 5 - const maxKeyPinHeight = pin.driverPinLength - 5; // Top of key pin at shear line - pin.keyPinHeight = Math.min(pin.keyPinHeight + keyPinLiftSpeed, maxKeyPinHeight); - - // If key pin reaches driver pin, start overpicking timer - if (pin.keyPinHeight >= maxKeyPinHeight) { // Key pin top at shear line - // Start overpicking timer if not already started - if (!pin.overpickingTimer) { - pin.overpickingTimer = Date.now(); - this.updateFeedback("Key pin at shear line. Release now or continue to overpick..."); - } - - // Check if 500ms have passed since reaching shear line - if (Date.now() - pin.overpickingTimer >= 500) { - // Both move up together - pin.isOverpicked = true; - pin.keyPinHeight = 90; // Move both up above SL - pin.driverPinHeight = 90; // Driver pin moves up too - - // Play overpicking sound - if (this.sounds.overtension) { - this.sounds.overtension.play(); - if (typeof navigator !== 'undefined' && navigator.vibrate) { - navigator.vibrate(500); - } - - } - - // Mark as overpicked and stuck - this.updateFeedback("Set pin overpicked! Release tension to reset."); - if (!pin.failureHighlight) { - pin.failureHighlight = this.scene.add.graphics(); - pin.failureHighlight.fillStyle(0xff6600, 0.7); - pin.failureHighlight.fillRect(-22.5, -110, 45, 140); - pin.container.add(pin.failureHighlight); - } - pin.failureHighlight.setVisible(true); - if (pin.setHighlight) pin.setHighlight.setVisible(false); - } - } - - // Draw key pin (rectangular part) - move gradually from dropped position - pin.keyPin.clear(); - pin.keyPin.fillStyle(0xdd3333); - // Calculate key pin position based on keyPinHeight (gradual movement from dropped position) - const keyPinY = -50 + pin.driverPinLength - pin.keyPinHeight; - pin.keyPin.fillRect(-12, keyPinY, 24, pin.keyPinLength - 8); - // Draw triangle - pin.keyPin.fillRect(-12, keyPinY + pin.keyPinLength - 8, 24, 2); - pin.keyPin.fillRect(-10, keyPinY + pin.keyPinLength - 6, 20, 2); - pin.keyPin.fillRect(-8, keyPinY + pin.keyPinLength - 4, 16, 2); - pin.keyPin.fillRect(-6, keyPinY + pin.keyPinLength - 2, 12, 2); - // Draw driver pin at shear line (stays at SL until overpicked) - pin.driverPin.clear(); - pin.driverPin.fillStyle(0x3388dd); - const shearLineY = -45; - const driverPinY = shearLineY - pin.driverPinLength; // Driver pin bottom at shear line - pin.driverPin.fillRect(-12, driverPinY, 24, pin.driverPinLength); - // Spring - pin.spring.clear(); - pin.spring.fillStyle(0x666666); - const springTop = -130; - const springBottom = shearLineY - pin.driverPinLength; // Driver pin top (at shear line) - const springHeight = springBottom - springTop; - const totalSpringSpace = springHeight; - const segmentSpacing = totalSpringSpace / 11; - for (let s = 0; s < 12; s++) { - const segmentHeight = 4 * 0.3; - const segmentY = springTop + (s * segmentSpacing); - if (segmentY + segmentHeight <= springBottom) { - pin.spring.fillRect(-12, segmentY, 24, segmentHeight); - } - } - // Continue lifting if mouse is still down - if (this.gameState.mouseDown && !pin.isOverpicked) { - requestAnimationFrame(() => this.liftPin()); - } - return; // Exit early for set pins - don't run normal lifting logic - } - - // Existing overpicking and normal lifting logic follows... - // Check for overpicking when tension is applied (for binding pins and set pins) - if (this.lockState.tensionApplied && (this.shouldPinBind(pin) || pin.isSet)) { - // For set pins, use keyPinHeight; for normal pins, use currentHeight - const heightToCheck = pin.isSet ? pin.keyPinHeight : pin.currentHeight; - const boundaryPosition = -50 + pin.driverPinLength - heightToCheck; - - // If key pin is pushed too far beyond shear line, it gets stuck - if (boundaryPosition < shearLineY - 10) { - // Check if this pin being overpicked would prevent automatic success - // If all other pins are correctly positioned, don't allow overpicking - let otherPinsCorrect = true; - this.pins.forEach(otherPin => { - if (otherPin !== pin && !otherPin.isOverpicked) { - const otherBoundaryPosition = -50 + otherPin.driverPinLength - otherPin.currentHeight; - const otherDistanceToShearLine = Math.abs(otherBoundaryPosition - shearLineY); - if (otherDistanceToShearLine > 8) { - otherPinsCorrect = false; - } - } - }); - - // If other pins are correct and this pin is being actively moved, prevent overpicking - if (otherPinsCorrect && this.gameState.mouseDown) { - // Stop the pin from moving further up but don't mark as overpicked - if (pin.isSet) { - const maxKeyPinHeight = pin.driverPinLength - 5; // Top of key pin at shear line - pin.keyPinHeight = Math.min(pin.keyPinHeight, maxKeyPinHeight); - } else { - // Use pin-specific maximum height for overpicking prevention - const baseMaxHeight = 75; - const maxHeightReduction = 15; - const pinHeightFactor = pin.index / (this.pinCount - 1); - const pinMaxHeight = baseMaxHeight - (maxHeightReduction * pinHeightFactor); - pin.currentHeight = Math.min(pin.currentHeight, pinMaxHeight); - } - return; - } - - // Otherwise, allow normal overpicking behavior - pin.isOverpicked = true; - - // Play overpicking sound - if (this.sounds.overtension) { - this.sounds.overtension.play(); - if (typeof navigator !== 'undefined' && navigator.vibrate) { - navigator.vibrate(500); - } - } - - if (pin.isSet) { - this.updateFeedback("Set pin overpicked! Release tension to reset."); - - // Show failure highlight for overpicked set pins - if (!pin.failureHighlight) { - pin.failureHighlight = this.scene.add.graphics(); - pin.failureHighlight.fillStyle(0xff6600, 0.7); - pin.failureHighlight.fillRect(-22.5, -110, 45, 140); - pin.container.add(pin.failureHighlight); - } - pin.failureHighlight.setVisible(true); - - // Hide set highlight - if (pin.setHighlight) pin.setHighlight.setVisible(false); - } else { - this.updateFeedback("Pin overpicked! Release tension to reset."); - - // Show overpicked highlight for regular pins - if (!pin.overpickedHighlight) { - pin.overpickedHighlight = this.scene.add.graphics(); - pin.overpickedHighlight.fillStyle(0xff0000, 0.6); - pin.overpickedHighlight.fillRect(-22.5, -110, 45, 140); - pin.container.add(pin.overpickedHighlight); - } - pin.overpickedHighlight.setVisible(true); - } - - // Don't return - allow further pushing even when overpicked - } - } - - // Calculate pin-specific maximum height (further pins have less upward movement) - const baseMaxHeight = 75; // Base maximum height for closest pin - const maxHeightReduction = 15; // Maximum reduction for furthest pin - const pinHeightFactor = pin.index / (this.pinCount - 1); // 0 for first pin, 1 for last pin - const pinMaxHeight = baseMaxHeight - (maxHeightReduction * pinHeightFactor); - - pin.currentHeight = Math.min(pin.currentHeight + liftSpeed, pinMaxHeight); - - // Update visual - both pins move up together toward the spring - pin.keyPin.clear(); - pin.keyPin.fillStyle(0xdd3333); - - // Draw rectangular part of key pin - pin.keyPin.fillRect(-12, -50 + pin.driverPinLength - pin.currentHeight, 24, pin.keyPinLength - 8); - - // Update hook position to follow any moving pin - if (pin.currentHeight > 0) { - this.updateHookPosition(pin.index); - } - - // Draw triangular bottom in pixel art style - pin.keyPin.fillRect(-12, -50 + pin.driverPinLength - pin.currentHeight + pin.keyPinLength - 8, 24, 2); - pin.keyPin.fillRect(-10, -50 + pin.driverPinLength - pin.currentHeight + pin.keyPinLength - 6, 20, 2); - pin.keyPin.fillRect(-8, -50 + pin.driverPinLength - pin.currentHeight + pin.keyPinLength - 4, 16, 2); - pin.keyPin.fillRect(-6, -50 + pin.driverPinLength - pin.currentHeight + pin.keyPinLength - 2, 12, 2); - - pin.driverPin.clear(); - pin.driverPin.fillStyle(0x3388dd); - pin.driverPin.fillRect(-12, -50 - pin.currentHeight, 24, pin.driverPinLength); - - // Spring compresses as pins push up (segments get shorter and closer together) - pin.spring.clear(); - pin.spring.fillStyle(0x666666); - const springCompression = pin.currentHeight; - const compressionFactor = Math.max(0.3, 1 - (springCompression / 60)); // Segments get shorter, minimum 30% size (1.2px) - - // Fixed spring top position - const springTop = -130; - // Spring bottom follows driver pin top - const driverPinTop = -50 - pin.currentHeight; - const springBottom = driverPinTop; - const springHeight = springBottom - springTop; - - // Calculate total spring space and distribute segments evenly - const totalSpringSpace = springHeight; - const segmentSpacing = totalSpringSpace / 11; // 11 gaps between 12 segments - keep consistent spacing - - for (let s = 0; s < 12; s++) { - const segmentHeight = 4 * compressionFactor; - const segmentY = springTop + (s * segmentSpacing); - - if (segmentY + segmentHeight <= springBottom) { // Only show segments within spring bounds - pin.spring.fillRect(-12, segmentY, 24, segmentHeight); - } - } - - // Check if the key/driver boundary is at the shear line (much higher position) - const boundaryPosition = -50 + pin.driverPinLength - pin.currentHeight; - const distanceToShearLine = Math.abs(boundaryPosition - shearLineY); - - // Calculate threshold based on sensitivity (same as pin setting logic) - const baseThreshold = 8; - const sensitivityFactor = (9 - this.thresholdSensitivity) / 8; // Updated for 1-8 range - const threshold = baseThreshold * sensitivityFactor; - - if (distanceToShearLine < threshold && this.highlightPinAlignment) { - // Show green highlight when boundary is at shear line (only if alignment highlighting is enabled) - if (!pin.shearHighlight) { - pin.shearHighlight = this.scene.add.graphics(); - pin.shearHighlight.fillStyle(0x00ff00, 0.4); - pin.shearHighlight.fillRect(-22.5, -110, 45, 140); - pin.container.addAt(pin.shearHighlight, 0); // Add at beginning to appear behind pins - } - - // Check if highlight is transitioning from hidden to visible - const wasHidden = !pin.shearHighlight.visible; - pin.shearHighlight.setVisible(true); - - // Play feedback when highlight first appears - if (wasHidden) { - if (this.sounds.click) { - this.sounds.click.play(); - } - if (typeof navigator !== 'undefined' && navigator.vibrate) { - if (typeof navigator !== 'undefined' && navigator.vibrate) { - navigator.vibrate(100); - } - } - } - } else { - if (pin.shearHighlight) { - pin.shearHighlight.setVisible(false); - } - } - } - - applyGravity() { - // When tension is not applied, all pins fall back down (except overpicked ones) - // Also, pins that are not binding fall back down even with tension - this.pins.forEach(pin => { - const shouldFall = !this.lockState.tensionApplied || (!this.shouldPinBind(pin) && !pin.isSet); - if (pin.currentHeight > 0 && !pin.isOverpicked && shouldFall) { - pin.currentHeight = Math.max(0, pin.currentHeight - 2.25); // Fall faster than lift (25% slower: 2.25 instead of 3) - - // Update visual - pin.keyPin.clear(); - pin.keyPin.fillStyle(0xdd3333); - - // Draw rectangular part of key pin - pin.keyPin.fillRect(-12, -50 + pin.driverPinLength - pin.currentHeight, 24, pin.keyPinLength - 8); - - // Update hook position to follow any moving pin - this.updateHookPosition(pin.index); - - // Draw triangular bottom in pixel art style - pin.keyPin.fillRect(-12, -50 + pin.driverPinLength - pin.currentHeight + pin.keyPinLength - 8, 24, 2); - pin.keyPin.fillRect(-10, -50 + pin.driverPinLength - pin.currentHeight + pin.keyPinLength - 6, 20, 2); - pin.keyPin.fillRect(-8, -50 + pin.driverPinLength - pin.currentHeight + pin.keyPinLength - 4, 16, 2); - pin.keyPin.fillRect(-6, -50 + pin.driverPinLength - pin.currentHeight + pin.keyPinLength - 2, 12, 2); - - pin.driverPin.clear(); - pin.driverPin.fillStyle(0x3388dd); - pin.driverPin.fillRect(-12, -50 - pin.currentHeight, 24, pin.driverPinLength); - - // Spring decompresses as pins fall - pin.spring.clear(); - pin.spring.fillStyle(0x666666); - const springCompression = pin.currentHeight; - const compressionFactor = Math.max(0.3, 1 - (springCompression / 60)); // Segments get shorter, minimum 30% size (1.2px) - - // Fixed spring top position - const springTop = -130; - // Spring bottom follows driver pin top - const driverPinTop = -50 - pin.currentHeight; - const springBottom = driverPinTop; - const springHeight = springBottom - springTop; - - // Calculate total spring space and distribute segments evenly - const totalSpringSpace = springHeight; - const segmentSpacing = totalSpringSpace / 11; // 11 gaps between 12 segments - keep consistent spacing - - for (let s = 0; s < 12; s++) { - const segmentHeight = 4 * compressionFactor; - const segmentY = springTop + (s * segmentSpacing); - - if (segmentY + segmentHeight <= springBottom) { // Only show segments within spring bounds - pin.spring.fillRect(-12, segmentY, 24, segmentHeight); - } - } - - // Hide highlights when falling - if (pin.shearHighlight) pin.shearHighlight.setVisible(false); - if (pin.setHighlight) pin.setHighlight.setVisible(false); - if (pin.bindingHighlight) pin.bindingHighlight.setVisible(false); - if (pin.overpickedHighlight) pin.overpickedHighlight.setVisible(false); - if (pin.failureHighlight) pin.failureHighlight.setVisible(false); - } else if (pin.isSet && shouldFall) { - // Set pins fall back down when tension is released - pin.isSet = false; - pin.keyPinHeight = 0; - pin.driverPinHeight = 0; - pin.currentHeight = 0; - - // Reset visual to original position - pin.keyPin.clear(); - pin.keyPin.fillStyle(0xdd3333); - pin.keyPin.fillRect(-12, -50 + pin.driverPinLength, 24, pin.keyPinLength - 8); - pin.keyPin.fillRect(-12, -50 + pin.driverPinLength + pin.keyPinLength - 8, 24, 2); - pin.keyPin.fillRect(-10, -50 + pin.driverPinLength + pin.keyPinLength - 6, 20, 2); - pin.keyPin.fillRect(-8, -50 + pin.driverPinLength + pin.keyPinLength - 4, 16, 2); - pin.keyPin.fillRect(-6, -50 + pin.driverPinLength + pin.keyPinLength - 2, 12, 2); - - pin.driverPin.clear(); - pin.driverPin.fillStyle(0x3388dd); - pin.driverPin.fillRect(-12, -50, 24, pin.driverPinLength); - - // Reset spring - pin.spring.clear(); - pin.spring.fillStyle(0x666666); - const springTop = -130; - const springBottom = -50; - const springHeight = springBottom - springTop; - const segmentSpacing = springHeight / 11; - for (let s = 0; s < 12; s++) { - const segmentHeight = 4; - const segmentY = springTop + (s * segmentSpacing); - pin.spring.fillRect(-12, segmentY, 24, segmentHeight); - } - - // Hide set highlight - if (pin.setHighlight) pin.setHighlight.setVisible(false); - } - }); - } - - checkAllPinsCorrect() { - const shearLineY = -45; - const threshold = 8; // Same threshold as individual pin checking - - let allCorrect = true; - - this.pins.forEach(pin => { - if (pin.isOverpicked) { - allCorrect = false; - return; - } - - // Calculate current boundary position between key and driver pins - const boundaryPosition = -50 + pin.driverPinLength - pin.currentHeight; - const distanceToShearLine = Math.abs(boundaryPosition - shearLineY); - - // Check if driver pin is above shear line and key pin is below - const driverPinBottom = boundaryPosition; - const keyPinTop = boundaryPosition; - - // Driver pin should be above shear line, key pin should be below - if (driverPinBottom > shearLineY + threshold || keyPinTop < shearLineY - threshold) { - allCorrect = false; - } - }); - - // If all pins are correctly positioned, set them all and complete the lock - if (allCorrect && this.lockState.pinsSet < this.pinCount) { - this.pins.forEach(pin => { - if (!pin.isSet) { - pin.isSet = true; - - // Show set pin highlight - if (!pin.setHighlight) { - pin.setHighlight = this.scene.add.graphics(); - pin.setHighlight.fillStyle(0x00ff00, 0.5); - pin.setHighlight.fillRect(-22.5, -110, 45, 140); - pin.container.addAt(pin.setHighlight, 0); // Add at beginning to appear behind pins - } - pin.setHighlight.setVisible(true); - - // Hide other highlights - if (pin.shearHighlight) pin.shearHighlight.setVisible(false); - if (pin.highlight) pin.highlight.setVisible(false); - if (pin.overpickedHighlight) pin.overpickedHighlight.setVisible(false); - if (pin.failureHighlight) pin.failureHighlight.setVisible(false); - } - }); - - this.lockState.pinsSet = this.pinCount; - this.updateFeedback("All pins correctly positioned! Lock picked successfully!"); - this.lockPickingSuccess(); - } - } - checkPinSet(pin) { // Check if the key/driver boundary is at the shear line const boundaryPosition = -50 + pin.driverPinLength - pin.currentHeight; @@ -3051,10 +1701,10 @@ export class LockpickingMinigamePhaser extends MinigameScene { } this.updateFeedback(`Pin ${pin.index + 1} set! (${this.lockState.pinsSet}/${this.pinCount})`); - this.updateBindingPins(); + this.pinMgmt.updateBindingPins(); if (this.lockState.pinsSet === this.pinCount) { - this.lockPickingSuccess(); + this.keyAnim.lockPickingSuccess(); } } else if (pin.isOverpicked) { // Pin is overpicked - stays stuck until tension is released @@ -3156,371 +1806,10 @@ export class LockpickingMinigamePhaser extends MinigameScene { return false; } - updateBindingPins() { - if (!this.lockState.tensionApplied || !this.highlightBindingOrder) { - this.pins.forEach(pin => { - // Hide binding highlight - if (pin.bindingHighlight) { - pin.bindingHighlight.setVisible(false); - } - }); - return; - } - - // 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) { - this.pins.forEach(pin => { - if (pin.index === nextPin.index && !pin.isSet) { - // Show binding highlight for next pin - if (!pin.bindingHighlight) { - pin.bindingHighlight = this.scene.add.graphics(); - pin.bindingHighlight.fillStyle(0xffff00, 0.6); - pin.bindingHighlight.fillRect(-22.5, -110, 45, 140); - pin.container.addAt(pin.bindingHighlight, 0); // Add at beginning to appear behind pins - } - pin.bindingHighlight.setVisible(true); - - // Play binding sound when highlighting next binding pin - if (this.sounds.binding) { - this.sounds.binding.play(); - } - } else if (!pin.isSet) { - // Hide binding highlight for other pins - if (pin.bindingHighlight) { - pin.bindingHighlight.setVisible(false); - } - } - }); - return; - } - } - - // All pins set - this.pins.forEach(pin => { - if (!pin.isSet && pin.bindingHighlight) { - pin.bindingHighlight.setVisible(false); - } - }); - } - - resetAllPins() { - this.pins.forEach(pin => { - if (!pin.isSet) { - pin.currentHeight = 0; - pin.isOverpicked = false; // Reset overpicked state - pin.keyPinHeight = 0; // Reset key pin height - pin.driverPinHeight = 0; // Reset driver pin height - - // Reset key pin to original position - pin.keyPin.clear(); - pin.keyPin.fillStyle(0xdd3333); - - // Draw rectangular part of key pin - pin.keyPin.fillRect(-12, -50 + pin.driverPinLength, 24, pin.keyPinLength - 8); - - // Draw triangular bottom in pixel art style - pin.keyPin.fillRect(-12, -50 + pin.driverPinLength + pin.keyPinLength - 8, 24, 2); - pin.keyPin.fillRect(-10, -50 + pin.driverPinLength + pin.keyPinLength - 6, 20, 2); - pin.keyPin.fillRect(-8, -50 + pin.driverPinLength + pin.keyPinLength - 4, 16, 2); - pin.keyPin.fillRect(-6, -50 + pin.driverPinLength + pin.keyPinLength - 2, 12, 2); - - // Reset driver pin to original position - pin.driverPin.clear(); - pin.driverPin.fillStyle(0x3388dd); - pin.driverPin.fillRect(-12, -50, 24, pin.driverPinLength); - - // Reset spring to original position (all 12 segments visible) - pin.spring.clear(); - pin.spring.fillStyle(0x666666); - const springTop = -130; // Fixed spring top - const springBottom = -50; // Driver pin top when not lifted - const springHeight = springBottom - springTop; - - // Calculate total spring space and distribute segments evenly - const totalSpringSpace = springHeight; - const segmentSpacing = totalSpringSpace / 11; // 11 gaps between 12 segments - - for (let s = 0; s < 12; s++) { - const segmentHeight = 4; - const segmentY = springTop + (s * segmentSpacing); - pin.spring.fillRect(-12, segmentY, 24, segmentHeight); - } - - // Hide all highlights - if (pin.shearHighlight) pin.shearHighlight.setVisible(false); - if (pin.setHighlight) pin.setHighlight.setVisible(false); - if (pin.bindingHighlight) pin.bindingHighlight.setVisible(false); - } - }); - } - updateFeedback(message) { this.feedback.textContent = message; } - lockPickingSuccess() { - // Animation configuration variables - easy to tweak - const KEY_PIN_TOP_SHRINK = 10; // How much the key pin top moves down - const KEY_PIN_BOTTOM_SHRINK = 5; // How much the key pin bottom moves up - const KEY_PIN_TOTAL_SHRINK = KEY_PIN_TOP_SHRINK + KEY_PIN_BOTTOM_SHRINK; // Total key pin shrink - const CHANNEL_MOVEMENT = 25; // How much channels move down - const KEYWAY_SHRINK = 20; // How much keyway shrinks - const WRENCH_VERTICAL_SHRINK = 60; // How much wrench vertical arm shrinks - const WRENCH_HORIZONTAL_SHRINK = 5; // How much wrench horizontal arm gets thinner - const WRENCH_MOVEMENT = 10; // How much wrench moves down - - this.gameState.isActive = false; - - // Play success sound - if (this.sounds.success) { - this.sounds.success.play(); - if (typeof navigator !== 'undefined' && navigator.vibrate) { - navigator.vibrate(500); - } - } - - this.updateFeedback("Lock picked successfully!"); - - // Shrink key pins downward and add half circles to simulate cylinder rotation - this.pins.forEach(pin => { - // Hide all highlights - if (pin.shearHighlight) pin.shearHighlight.setVisible(false); - if (pin.setHighlight) pin.setHighlight.setVisible(false); - if (pin.bindingHighlight) pin.bindingHighlight.setVisible(false); - if (pin.overpickedHighlight) pin.overpickedHighlight.setVisible(false); - if (pin.failureHighlight) pin.failureHighlight.setVisible(false); - - // Create squashed circle that expands and moves to stay aligned with key pin top - const squashedCircle = this.scene.add.graphics(); - //was 0xdd3333 Red color (key pin color) - squashedCircle.fillStyle(0xffffff); // white color for testing purposes - squashedCircle.x = pin.x; // Center horizontally on the pin - - // Start position: aligned with the top of the key pin - const startTopY = pin.y + (-50 + pin.driverPinLength); // Top of key pin position - squashedCircle.y = startTopY; - squashedCircle.setDepth(3); // Above driver pins so they're visible - - // Create a temporary object to hold the circle expansion data - const circleData = { - width: 24, // Start full width (same as key pin) - height: 2, // Start very thin (flat top) - y: startTopY - }; - - // Animate the squashed circle expanding to full circle (stays at top of key pin) - this.scene.tweens.add({ - targets: circleData, - width: 24, // Full circle width (stays same) - height: 16, // Full circle height (expands from 2 to 16) - y: startTopY, // Stay at the top of the key pin (no movement) - duration: 1400, - ease: 'Cubic.easeInOut', - onUpdate: function() { - squashedCircle.clear(); - squashedCircle.fillStyle(0xff3333); // Red color (key pin color) - - // Calculate animation progress (0 to 1) - const progress = (circleData.height - 2) / (16 - 2); // From 2 to 16 height - - // Draw different circle shapes based on progress (widest in middle) - if (progress < 0.1) { - // Start: just a thin line (flat top) - squashedCircle.fillRect(-12, 0, 24, 2); - } else if (progress < 0.3) { - // Early: thin oval with middle bulge - squashedCircle.fillRect(-8, 0, 16, 2); // narrow top - squashedCircle.fillRect(-12, 2, 24, 2); // wide middle - squashedCircle.fillRect(-8, 4, 16, 2); // narrow bottom - } else if (progress < 0.5) { - // Middle: growing circle with middle bulge - squashedCircle.fillRect(-6, 0, 12, 2); // narrow top - squashedCircle.fillRect(-10, 2, 20, 2); // wider - squashedCircle.fillRect(-12, 4, 24, 2); // widest middle - squashedCircle.fillRect(-10, 6, 20, 2); // wider - squashedCircle.fillRect(-6, 8, 12, 2); // narrow bottom - } else if (progress < 0.7) { - // Later: more circle-like with middle bulge - squashedCircle.fillRect(-4, 0, 8, 2); // narrow top - squashedCircle.fillRect(-8, 2, 16, 2); // wider - squashedCircle.fillRect(-12, 4, 24, 2); // widest middle - squashedCircle.fillRect(-12, 6, 24, 2); // widest middle - squashedCircle.fillRect(-8, 8, 16, 2); // wider - squashedCircle.fillRect(-4, 10, 8, 2); // narrow bottom - } else if (progress < 0.9) { - // Almost full: near complete circle - squashedCircle.fillRect(-2, 0, 4, 2); // narrow top - squashedCircle.fillRect(-6, 2, 12, 2); // wider - squashedCircle.fillRect(-10, 4, 20, 2); // wider - squashedCircle.fillRect(-12, 6, 24, 2); // widest middle - squashedCircle.fillRect(-12, 8, 24, 2); // widest middle - squashedCircle.fillRect(-10, 10, 20, 2); // wider - squashedCircle.fillRect(-6, 12, 12, 2); // wider - squashedCircle.fillRect(-2, 14, 4, 2); // narrow bottom - } else { - // Full: complete pixel art circle - squashedCircle.fillRect(-2, 0, 4, 2); // narrow top - squashedCircle.fillRect(-6, 2, 12, 2); // wider - squashedCircle.fillRect(-10, 4, 20, 2); // wider - squashedCircle.fillRect(-12, 6, 24, 2); // widest middle - squashedCircle.fillRect(-12, 8, 24, 2); // widest middle - squashedCircle.fillRect(-10, 10, 20, 2); // wider - squashedCircle.fillRect(-6, 12, 12, 2); // wider - squashedCircle.fillRect(-2, 14, 4, 2); // narrow bottom - } - - // Update position - squashedCircle.y = circleData.y; - } - }); - - // Animate key pin shrinking from both top and bottom - const keyPinData = { height: pin.keyPinLength, topOffset: 0 }; - this.scene.tweens.add({ - targets: keyPinData, - height: pin.keyPinLength - KEY_PIN_TOTAL_SHRINK, // Shrink by total amount - topOffset: KEY_PIN_TOP_SHRINK, // Move top down - duration: 1400, - ease: 'Cubic.easeInOut', - onUpdate: function() { - pin.keyPin.clear(); - pin.keyPin.fillStyle(0xdd3333); - - // Calculate new position: top moves down, bottom moves up - const originalTopY = -50 + pin.driverPinLength; // Original top of key pin - const newTopY = originalTopY + keyPinData.topOffset; // Top moves down - const newBottomY = newTopY + keyPinData.height; // Bottom position - - // Draw rectangular part of key pin (shrunk from both ends) - pin.keyPin.fillRect(-12, newTopY, 24, keyPinData.height - 8); - - // Draw triangular bottom in pixel art style (bottom moves up) - pin.keyPin.fillRect(-12, newBottomY - 8, 24, 2); - pin.keyPin.fillRect(-10, newBottomY - 6, 20, 2); - pin.keyPin.fillRect(-8, newBottomY - 4, 16, 2); - pin.keyPin.fillRect(-6, newBottomY - 2, 12, 2); - } - }); - - // Animate key pin channel rectangle moving down with the channel circles - this.scene.tweens.add({ - targets: pin.channelRect, - y: pin.channelRect.y + CHANNEL_MOVEMENT, // Move down by channel movement amount - duration: 1400, - ease: 'Cubic.easeInOut' - }); - }); - - // Animate the keyway shrinking (keeping bottom in place) to make cylinder appear to grow - // Create a temporary object to hold the height value for tweening - const keywayData = { height: 90 }; - this.scene.tweens.add({ - targets: keywayData, - height: 90 - KEYWAY_SHRINK, // Shrink by keyway shrink amount - duration: 1400, - ease: 'Cubic.easeInOut', - onUpdate: function() { - this.keywayGraphics.clear(); - this.keywayGraphics.fillStyle(0x2a2a2a); - // Move top down: y increases as height shrinks, keeping bottom at y=290 - const newY = 200 + (90 - keywayData.height); // Move top down - this.keywayGraphics.fillRect(100, newY, 400, keywayData.height); - this.keywayGraphics.lineStyle(1, 0x1a1a1a); - this.keywayGraphics.strokeRect(100, newY, 400, keywayData.height); - }.bind(this) - }); - - // Animate tension wrench shrinking and moving down - if (this.tensionWrench) { - // Create a temporary object to hold the height value for tweening - const wrenchData = { height: 170, y: 0, horizontalHeight: 10 }; // Original vertical arm height, y offset, and horizontal arm height - this.scene.tweens.add({ - targets: wrenchData, - height: 170 - WRENCH_VERTICAL_SHRINK, // Shrink by vertical shrink amount - y: WRENCH_MOVEMENT, // Move entire wrench down - horizontalHeight: 10 - WRENCH_HORIZONTAL_SHRINK, // Make horizontal arm thinner - duration: 1400, - ease: 'Cubic.easeInOut', - onUpdate: function() { - // Update the wrench graphics (both active and inactive states) - this.wrenchGraphics.clear(); - this.wrenchGraphics.fillStyle(this.lockState.tensionApplied ? 0x00ff00 : 0x888888); - - // Calculate new top position (move top down as height shrinks) - const originalTop = -120; // Original top position - const newTop = originalTop + (170 - wrenchData.height) + wrenchData.y; // Move top down and add y offset - - // Long vertical arm (left side of L) - top moves down and shrinks - this.wrenchGraphics.fillRect(0, newTop, 10, wrenchData.height); - - // Short horizontal arm (bottom of L) - also moves down with top and gets thinner - this.wrenchGraphics.fillRect(0, newTop + wrenchData.height, 37.5, wrenchData.horizontalHeight); - }.bind(this) - }); - } - - // Channel rectangles are already created during initial render - - // Animate pixel-art circles (channels) moving down from above the shear line - this.pins.forEach(pin => { - // Calculate starting position: above the shear line (behind driver pins) - const pinX = pin.x; - const pinY = pin.y; - const shearLineY = -45; // Shear line position - const circleStartY = pinY + shearLineY - 20; // Start above shear line - const circleEndY = circleStartY + CHANNEL_MOVEMENT; // Move down same distance as cylinder - - // Create pixel-art circle graphics - const channelCircle = this.scene.add.graphics(); - channelCircle.x = pinX; - channelCircle.y = circleStartY; - // Pixel-art circle: red color (like key pins) - const color = 0x333333; // Red color (key pin color) - channelCircle.fillStyle(color, 1); - // Create a proper circle shape with pixel-art steps (middle widest) - channelCircle.fillRect(-6, 0, 12, 2); // bottom (narrowest) - channelCircle.fillRect(-8, 2, 16, 2); // wider - channelCircle.fillRect(-10, 4, 20, 2); // wider - channelCircle.fillRect(-12, 6, 24, 2); // widest (middle) - channelCircle.fillRect(-12, 8, 24, 2); // widest (middle) - channelCircle.fillRect(-10, 10, 20, 2); // narrower - channelCircle.fillRect(-8, 12, 16, 2); // narrower - channelCircle.fillRect(-6, 14, 12, 2); // top (narrowest) - channelCircle.setDepth(1); // Normal depth for circles - - // Animate the circle moving down - this.scene.tweens.add({ - targets: channelCircle, - y: circleEndY, - duration: 1400, - ease: 'Cubic.easeInOut', - }); - }); - - // Show success message immediately but delay the game completion - const successHTML = ` -
Lock picked successfully!
- `; - // this.showSuccess(successHTML, false, 2000); - - // Delay the actual game completion until animation finishes - setTimeout(() => { - // Now trigger the success callback that unlocks the game - this.showSuccess(successHTML, true, 2000); - this.gameResult = { lockable: this.lockable }; - }, 1500); // Wait 1.5 seconds (slightly longer than animation duration) - } - - start() { - super.start(); - this.gameState.isActive = true; - this.lockState.tensionApplied = false; - this.lockState.pinsSet = 0; - this.updateProgress(0, this.pinCount); - } - complete(success) { if (this.game) { this.game.destroy(true); @@ -3529,14 +1818,6 @@ export class LockpickingMinigamePhaser extends MinigameScene { super.complete(success, this.gameResult); } - cleanup() { - if (this.game) { - this.game.destroy(true); - this.game = null; - } - super.cleanup(); - } - shuffleArray(array) { for (let i = array.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); @@ -3545,175 +1826,4 @@ export class LockpickingMinigamePhaser extends MinigameScene { return array; } - flashWrenchRed() { - // Flash the tension wrench red to indicate tension is needed - if (!this.wrenchGraphics) return; - - const originalFillStyle = this.lockState.tensionApplied ? 0x00ff00 : 0x888888; - - // Store original state - const originalClear = this.wrenchGraphics.clear.bind(this.wrenchGraphics); - - // Flash red 3 times - for (let i = 0; i < 3; i++) { - this.scene.time.delayedCall(i * 150, () => { - this.wrenchGraphics.clear(); - this.wrenchGraphics.fillStyle(0xff0000); // Red - - // Long vertical arm - this.wrenchGraphics.fillRect(0, -120, 10, 170); - // Short horizontal arm - this.wrenchGraphics.fillRect(0, 40, 37.5, 10); - }); - - this.scene.time.delayedCall(i * 150 + 75, () => { - this.wrenchGraphics.clear(); - this.wrenchGraphics.fillStyle(originalFillStyle); // Back to original color - - // Long vertical arm - this.wrenchGraphics.fillRect(0, -120, 10, 170); - // Short horizontal arm - this.wrenchGraphics.fillRect(0, 40, 37.5, 10); - }); - } - } - - switchToPickMode() { - // Switch from key selection mode to lockpicking mode - console.log('Switching from key mode to lockpicking mode'); - - // Hide the mode switch button - const switchBtn = document.getElementById('lockpicking-switch-mode-btn'); - if (switchBtn) { - switchBtn.style.display = 'none'; - } - - // Exit key mode - this.keyMode = false; - this.keySelectionMode = false; - - // Clean up key selection UI if visible - if (this.keySelectionContainer) { - this.keySelectionContainer.destroy(); - this.keySelectionContainer = null; - } - - // Clean up any key visuals - if (this.keyGroup) { - this.keyGroup.destroy(); - this.keyGroup = null; - } - if (this.keyClickZone) { - this.keyClickZone.destroy(); - this.keyClickZone = null; - } - - // Show lockpicking tools - if (this.tensionWrench) { - this.tensionWrench.setVisible(true); - } - if (this.hookGroup) { - this.hookGroup.setVisible(true); - } - if (this.wrenchText) { - this.wrenchText.setVisible(true); - } - if (this.hookPickLabel) { - this.hookPickLabel.setVisible(true); - } - - // Reset pins to original positions - this.lockConfig.resetPinsToOriginalPositions(); - - // Update feedback - this.updateFeedback("Lockpicking mode - Apply tension first, then lift pins in binding order"); - } - - showLockpickingTools() { - // Show tension wrench and hook pick in lockpicking mode - if (this.tensionWrench) { - this.tensionWrench.setVisible(true); - } - if (this.hookGroup) { - this.hookGroup.setVisible(true); - } - - // Show labels - if (this.wrenchText) { - this.wrenchText.setVisible(true); - } - if (this.hookPickLabel) { - this.hookPickLabel.setVisible(true); - } - } - - switchToKeyMode() { - // Switch from lockpicking mode to key selection mode - console.log('Switching from lockpicking mode to key mode'); - - // Hide the mode switch button - const switchBtn = document.getElementById('lockpicking-switch-to-keys-btn'); - if (switchBtn) { - switchBtn.style.display = 'none'; - } - - // Enter key mode - this.keyMode = true; - this.keySelectionMode = true; - - // Hide lockpicking tools - if (this.tensionWrench) { - this.tensionWrench.setVisible(false); - } - if (this.hookGroup) { - this.hookGroup.setVisible(false); - } - if (this.wrenchText) { - this.wrenchText.setVisible(false); - } - if (this.hookPickLabel) { - this.hookPickLabel.setVisible(false); - } - - // Reset pins to original positions - this.lockConfig.resetPinsToOriginalPositions(); - - // Add mode switch back button (can switch back to lockpicking if available) - if (this.canSwitchToPickMode) { - const itemDisplayDiv = document.querySelector('.lockpicking-item-section'); - if (itemDisplayDiv) { - // Remove any existing button container - const existingButtonContainer = itemDisplayDiv.querySelector('div[style*="margin-top"]'); - if (existingButtonContainer) { - existingButtonContainer.remove(); - } - - // Add new button container - const buttonContainer = document.createElement('div'); - buttonContainer.style.cssText = ` - display: flex; - gap: 10px; - margin-top: 10px; - justify-content: center; - `; - - const switchModeBtn = document.createElement('button'); - switchModeBtn.className = 'minigame-button'; - switchModeBtn.id = 'lockpicking-switch-mode-btn'; - switchModeBtn.innerHTML = 'Lockpick Switch to Lockpicking'; - switchModeBtn.onclick = () => this.switchToPickMode(); - - buttonContainer.appendChild(switchModeBtn); - itemDisplayDiv.appendChild(buttonContainer); - } - } - - // Show key selection UI with available keys - if (this.availableKeys && this.availableKeys.length > 0) { - this.createKeySelectionUI(this.availableKeys, this.requiredKeyId); - this.updateFeedback("Select a key to use"); - } else { - this.updateFeedback("No keys available"); - } - } } \ No newline at end of file diff --git a/js/minigames/lockpicking/pin-management.js b/js/minigames/lockpicking/pin-management.js new file mode 100644 index 0000000..b3ad788 --- /dev/null +++ b/js/minigames/lockpicking/pin-management.js @@ -0,0 +1,1103 @@ + +/** + * PinManagement + * + * Extracted from lockpicking-game-phaser.js + * Instantiate with: new PinManagement(this) + * + * All 'this' references replaced with 'this.parent' to access parent instance state: + * - this.parent.pins (array of pin objects) + * - this.parent.scene (Phaser scene) + * - this.parent.lockId (lock identifier) + * - this.parent.lockState (lock state object) + * etc. + */ +export class PinManagement { + + constructor(parent) { + this.parent = parent; + } + + createPins() { + // Create random binding order + const bindingOrder = []; + for (let i = 0; i < this.parent.pinCount; i++) { + bindingOrder.push(i); + } + this.parent.shuffleArray(bindingOrder); + + const pinSpacing = 400 / (this.parent.pinCount + 1); + const margin = pinSpacing * 0.75; // 25% smaller margins + + // Try to load saved pin heights for this lock + const savedPinHeights = this.parent.lockConfig.loadLockConfiguration(); + + // Check if predefined pin heights were passed + const predefinedPinHeights = this.parent.params?.predefinedPinHeights; + + console.log(`DEBUG: Lockpicking minigame received parameters:`); + console.log(` - pinCount: ${this.parent.pinCount}`); + console.log(` - this.parent.params:`, this.parent.params); + console.log(` - predefinedPinHeights: [${predefinedPinHeights ? predefinedPinHeights.join(', ') : 'none'}]`); + console.log(` - savedPinHeights: [${savedPinHeights ? savedPinHeights.join(', ') : 'none'}]`); + + for (let i = 0; i < this.parent.pinCount; i++) { + const pinX = 100 + margin + i * pinSpacing; + const pinY = 200; + + // Use predefined pin heights if available, otherwise use saved or generate random ones + let keyPinLength, driverPinLength; + if (predefinedPinHeights && predefinedPinHeights[i] !== undefined) { + // Use predefined configuration + keyPinLength = predefinedPinHeights[i]; + driverPinLength = 75 - keyPinLength; // Total height is 75 + console.log(`✓ Pin ${i}: Using predefined pin height: ${keyPinLength} (driver: ${driverPinLength})`); + } else if (savedPinHeights && savedPinHeights[i] !== undefined) { + // Use saved configuration + keyPinLength = savedPinHeights[i]; + driverPinLength = 75 - keyPinLength; // Total height is 75 + console.log(`✓ Pin ${i}: Using saved pin height: ${keyPinLength} (driver: ${driverPinLength})`); + } else { + // Generate random pin lengths that add up to 75 (total height - 25% increase from 60) + keyPinLength = 25 + Math.random() * 37.5; // 25-62.5 (25% increase) + driverPinLength = 75 - keyPinLength; // Remaining to make 75 total + console.log(`⚠ Pin ${i}: Generated random pin height: ${keyPinLength} (driver: ${driverPinLength})`); + } + + const pin = { + index: i, + binding: bindingOrder[i], + isSet: false, + currentHeight: 0, + originalHeight: keyPinLength, // Store original height for consistency + keyPinHeight: 0, // Track key pin position separately + driverPinHeight: 0, // Track driver pin position separately + keyPinLength: keyPinLength, + driverPinLength: driverPinLength, + x: pinX, + y: pinY, + container: null, + keyPin: null, + driverPin: null, + spring: null + }; + + // Ensure pin properties are valid + if (!pin.keyPinLength || !pin.driverPinLength) { + console.error(`Pin ${i} created with invalid lengths:`, pin); + pin.keyPinLength = pin.keyPinLength || 30; // Default fallback + pin.driverPinLength = pin.driverPinLength || 45; // Default fallback + } + + // Create pin container + pin.container = this.parent.scene.add.container(pinX, pinY); + + // Add all highlights FIRST (so they appear behind pins) + // Add hover effect using a highlight rectangle - 25% less wide, full height from spring top to pin bottom (extended down) + pin.highlight = this.parent.scene.add.graphics(); + pin.highlight.fillStyle(0xffff00, 0.3); + pin.highlight.fillRect(-22.5, -110, 45, 140); + pin.highlight.setVisible(false); + pin.container.add(pin.highlight); + + // Add overpicked highlight + pin.overpickedHighlight = this.parent.scene.add.graphics(); + pin.overpickedHighlight.fillStyle(0xff0000, 0.6); + pin.overpickedHighlight.fillRect(-22.5, -110, 45, 140); + pin.overpickedHighlight.setVisible(false); + pin.container.add(pin.overpickedHighlight); + + // Add failure highlight for overpicked set pins + pin.failureHighlight = this.parent.scene.add.graphics(); + pin.failureHighlight.fillStyle(0xff6600, 0.7); + pin.failureHighlight.fillRect(-22.5, -110, 45, 140); + pin.failureHighlight.setVisible(false); + pin.container.add(pin.failureHighlight); + + // Create spring (top part) - 12 segments with correct initial spacing + pin.spring = this.parent.scene.add.graphics(); + pin.spring.fillStyle(0x666666); + const springTop = -130; + const springBottom = -50; // Driver pin top when not lifted + const springHeight = springBottom - springTop; + + // Calculate total spring space and distribute segments evenly + const totalSpringSpace = springHeight; + const segmentSpacing = totalSpringSpace / 11; // 11 gaps between 12 segments + + for (let s = 0; s < 12; s++) { + const segmentY = springTop + (s * segmentSpacing); + pin.spring.fillRect(-12, segmentY, 24, 4); + } + pin.container.add(pin.spring); + + // Create driver pin (middle part) - starts at y=-50 + pin.driverPin = this.parent.scene.add.graphics(); + pin.driverPin.fillStyle(0x3388dd); + pin.driverPin.fillRect(-12, -50, 24, driverPinLength); + pin.container.add(pin.driverPin); + + // Set container depth to ensure driver pins are above circles + pin.container.setDepth(2); + + // Create key pin (bottom part) - starts below driver pin with triangular bottom + pin.keyPin = this.parent.scene.add.graphics(); + pin.keyPin.fillStyle(0xdd3333); + + // Draw rectangular part of key pin + pin.keyPin.fillRect(-12, -50 + driverPinLength, 24, keyPinLength - 8); + + // Draw triangular bottom in pixel art style + pin.keyPin.fillRect(-12, -50 + driverPinLength + keyPinLength - 8, 24, 2); + pin.keyPin.fillRect(-10, -50 + driverPinLength + keyPinLength - 6, 20, 2); + pin.keyPin.fillRect(-8, -50 + driverPinLength + keyPinLength - 4, 16, 2); + pin.keyPin.fillRect(-6, -50 + driverPinLength + keyPinLength - 2, 12, 2); + + pin.container.add(pin.keyPin); + + // Add labels for pin components (only for the first pin to avoid clutter) + if (i === 0) { + // Spring label + const springLabel = this.parent.scene.add.text(pinX, pinY - 140, 'Spring', { + fontSize: '18px', + fontFamily: 'VT323', + fill: '#00ff00', + fontWeight: 'bold' + }); + springLabel.setOrigin(0.5); + springLabel.setDepth(100); // Bring to front + + // Driver pin label - positioned below the shear line + const driverPinX = 100 + margin + 1 * pinSpacing; // Pin index 1 (2nd pin) + const driverPinLabel = this.parent.scene.add.text(driverPinX, pinY - 35, 'Driver Pin', { + fontSize: '18px', + fontFamily: 'VT323', + fill: '#00ff00', + fontWeight: 'bold' + }); + driverPinLabel.setOrigin(0.5); + driverPinLabel.setDepth(100); // Bring to front + + // Key pin label - positioned at the middle of the key pin + const keyPinX = 100 + margin + 2 * pinSpacing; // Pin index 2 (3rd pin) + const keyPinLabel = this.parent.scene.add.text(keyPinX, pinY - 50 + driverPinLength + (keyPinLength / 2), 'Key Pin', { + fontSize: '18px', + fontFamily: 'VT323', + fill: '#00ff00', + fontWeight: 'bold' + }); + keyPinLabel.setOrigin(0.5); + keyPinLabel.setDepth(100); // Bring to front + + // Store references to labels for hiding + this.parent.springLabel = springLabel; + this.parent.driverPinLabel = driverPinLabel; + this.parent.keyPinLabel = keyPinLabel; + } + + // Create channel rectangle (keyway for this pin) - above cylinder but behind key pins + const shearLineY = -45; // Shear line position + const keywayTopY = 200; // Top of the main keyway + const channelHeight = keywayTopY - (pinY + shearLineY); // From keyway to shear line + + // Create channel rectangle graphics + pin.channelRect = this.parent.scene.add.graphics(); + pin.channelRect.x = pinX; + pin.channelRect.y = pinY + shearLineY - 15; // Start at circle start position (20px above shear line) + pin.channelRect.fillStyle(0x2a2a2a, 1); // Same color as keyway + pin.channelRect.fillRect(-13, 3, 26, channelHeight + 15 - 3); // 3px margin except at shear line + pin.channelRect.setDepth(0); // Behind key pins but above cylinder + + // Add border to match keyway style + pin.channelRect.lineStyle(1, 0x1a1a1a); + pin.channelRect.strokeRect(-13, 3, 26, channelHeight + 20 - 3); + + // Create spring channel rectangle - behind spring, above cylinder + const springChannelHeight = springBottom - springTop; // Spring height + + // Create spring channel rectangle graphics + pin.springChannelRect = this.parent.scene.add.graphics(); + pin.springChannelRect.x = pinX; + pin.springChannelRect.y = pinY + springTop; // Start at spring top + pin.springChannelRect.fillStyle(0x2a2a2a, 1); // Same color as keyway + pin.springChannelRect.fillRect(-13, 3, 26, springChannelHeight - 3); // 3px margin except at shear line + pin.springChannelRect.setDepth(1); // Behind spring but above cylinder + + // Add border to match keyway style + pin.springChannelRect.lineStyle(1, 0x1a1a1a); + pin.springChannelRect.strokeRect(-13, 3, 26, springChannelHeight - 3); + + // Make pin interactive - 25% less wide, full height from spring top to bottom of keyway (extended down) + pin.container.setInteractive(new Phaser.Geom.Rectangle(-18.75, -110, 37.5, 230), Phaser.Geom.Rectangle.Contains); + + // Add pin number + const pinText = this.parent.scene.add.text(0, 40, (i + 1).toString(), { + fontSize: '18px', + fontFamily: 'VT323', + fill: '#ffffff', + fontWeight: 'bold' + }); + pinText.setOrigin(0.5); + pin.container.add(pinText); + + // Store reference to pin text for hiding + pin.pinText = pinText; + + pin.container.on('pointerover', () => { + if (this.parent.lockState.tensionApplied && !pin.isSet) { + pin.highlight.setVisible(true); + } + }); + + pin.container.on('pointerout', () => { + pin.highlight.setVisible(false); + }); + + // Add event handlers + pin.container.on('pointerdown', () => { + console.log('Pin clicked:', pin.index); + this.parent.lockState.currentPin = pin; + this.parent.gameState.mouseDown = true; + console.log('Pin interaction started'); + + // Play click sound + if (this.parent.sounds.click) { + this.parent.sounds.click.play(); + if (typeof navigator !== 'undefined' && navigator.vibrate) { + navigator.vibrate(50); + } + } + + // Hide labels on first pin click + if (!this.parent.pinClicked) { + this.parent.pinClicked = true; + if (this.parent.wrenchText) { + this.parent.wrenchText.setVisible(false); + } + if (this.parent.shearLineText) { + this.parent.shearLineText.setVisible(false); + } + if (this.parent.hookPickLabel) { + this.parent.hookPickLabel.setVisible(false); + } + if (this.parent.springLabel) { + this.parent.springLabel.setVisible(false); + } + if (this.parent.driverPinLabel) { + this.parent.driverPinLabel.setVisible(false); + } + if (this.parent.keyPinLabel) { + this.parent.keyPinLabel.setVisible(false); + } + + // Hide all pin numbers + this.parent.pins.forEach(pin => { + if (pin.pinText) { + pin.pinText.setVisible(false); + } + }); + } + + if (!this.parent.lockState.tensionApplied) { + this.parent.updateFeedback("Apply tension first before picking pins"); + this.parent.flashWrenchRed(); + } + }); + + this.parent.pins.push(pin); + } + + // Save the lock configuration after all pins are created + this.parent.lockConfig.saveLockConfiguration(); + } + + createShearLine() { + // Create a more visible shear line at y=155 (which is -45 in pin coordinates) + const graphics = this.parent.scene.add.graphics(); + graphics.lineStyle(3, 0x00ff00); + graphics.beginPath(); + graphics.moveTo(100, 155); + graphics.lineTo(500, 155); + graphics.strokePath(); + + // Add a dashed line effect + graphics.lineStyle(1, 0x00ff00, 0.5); + for (let x = 100; x < 500; x += 10) { + graphics.beginPath(); + graphics.moveTo(x, 150); + graphics.lineTo(x, 160); + graphics.strokePath(); + } + + // Add shear line label + const shearLineText = this.parent.scene.add.text(430, 135, 'SHEAR LINE', { + fontSize: '16px', + fontFamily: 'VT323', + fill: '#00ff00', + fontWeight: 'bold' + }); + shearLineText.setDepth(100); // Bring to front + + // Store reference to shear line text for hiding + this.parent.shearLineText = shearLineText; + + // // Add instruction text + // this.scene.add.text(300, 180, 'Align key/driver pins at the shear line', { + // fontSize: '12px', + // fill: '#00ff00', + // fontStyle: 'italic' + // }).setOrigin(0.5); + } + + setupInputHandlers() { + this.parent.scene.input.on('pointerup', () => { + if (this.parent.lockState.currentPin) { + this.parent.checkPinSet(this.parent.lockState.currentPin); + this.parent.lockState.currentPin = null; + } + this.parent.gameState.mouseDown = false; + + // Only return hook to resting position if not in key mode + if (!this.parent.keyMode && this.parent.hookPickGraphics && this.parent.hookConfig) { + this.parent.toolMgr.returnHookToStart(); + } + + // Stop key insertion if in key mode + if (this.parent.keyMode) { + this.parent.keyInserting = false; + } + }); + + // Add keyboard bindings + this.parent.scene.input.keyboard.on('keydown', (event) => { + const key = event.key; + + // Pin number keys (1-8) + if (key >= '1' && key <= '8') { + const pinIndex = parseInt(key) - 1; // Convert 1-8 to 0-7 + + // Check if pin exists + if (pinIndex < this.parent.pinCount) { + const pin = this.parent.pins[pinIndex]; + if (pin) { + // Simulate pin click + this.parent.lockState.currentPin = pin; + this.parent.gameState.mouseDown = true; + + // Play click sound + if (this.parent.sounds.click) { + this.parent.sounds.click.play(); + if (typeof navigator !== 'undefined' && navigator.vibrate) { + navigator.vibrate(50); + } + } + + // Hide labels on first pin click + if (!this.parent.pinClicked) { + this.parent.pinClicked = true; + if (this.parent.wrenchText) { + this.parent.wrenchText.setVisible(false); + } + if (this.parent.shearLineText) { + this.parent.shearLineText.setVisible(false); + } + if (this.parent.hookPickLabel) { + this.parent.hookPickLabel.setVisible(false); + } + if (this.parent.springLabel) { + this.parent.springLabel.setVisible(false); + } + if (this.parent.driverPinLabel) { + this.parent.driverPinLabel.setVisible(false); + } + if (this.parent.keyPinLabel) { + this.parent.keyPinLabel.setVisible(false); + } + + // Hide all pin numbers + this.parent.pins.forEach(pin => { + if (pin.pinText) { + pin.pinText.setVisible(false); + } + }); + } + + if (!this.parent.lockState.tensionApplied) { + this.parent.updateFeedback("Apply tension first before picking pins"); + this.parent.flashWrenchRed(); + } + } + } + } + + // SPACE key for tension wrench toggle + if (key === ' ') { + event.preventDefault(); // Prevent page scroll + + // Simulate tension wrench click + this.parent.lockState.tensionApplied = !this.parent.lockState.tensionApplied; + + // Play tension sound + if (this.parent.sounds.tension) { + this.parent.sounds.tension.play(); + if (typeof navigator !== 'undefined' && navigator.vibrate) { + navigator.vibrate([200]); + } + } + + if (this.parent.lockState.tensionApplied) { + this.parent.wrenchGraphics.clear(); + this.parent.wrenchGraphics.fillStyle(0x00ff00); + + // Long vertical arm (left side of L) - same dimensions as inactive + this.parent.wrenchGraphics.fillRect(0, -120, 10, 170); + + // Short horizontal arm (bottom of L) extending into keyway - same dimensions as inactive + this.parent.wrenchGraphics.fillRect(0, 40, 37.5, 10); + + this.parent.updateFeedback("Tension applied. Only the binding pin can be set - others will fall back down."); + } else { + this.parent.wrenchGraphics.clear(); + this.parent.wrenchGraphics.fillStyle(0x888888); + + // Long vertical arm (left side of L) - same dimensions as active + this.parent.wrenchGraphics.fillRect(0, -120, 10, 170); + + // Short horizontal arm (bottom of L) extending into keyway - same dimensions as active + this.parent.wrenchGraphics.fillRect(0, 40, 37.5, 10); + + this.parent.updateFeedback("Tension released. All pins will fall back down."); + + // Play reset sound + if (this.parent.sounds.reset) { + this.parent.sounds.reset.play(); + } + + // Reset ALL pins when tension is released (including set and overpicked ones) + this.parent.pins.forEach(pin => { + pin.isSet = false; + pin.isOverpicked = false; + pin.currentHeight = 0; + pin.keyPinHeight = 0; // Reset key pin height + pin.driverPinHeight = 0; // Reset driver pin height + pin.overpickingTimer = null; // Reset overpicking timer + + // Reset visual + pin.keyPin.clear(); + pin.keyPin.fillStyle(0xdd3333); + + // Draw rectangular part of key pin + pin.keyPin.fillRect(-12, -50 + pin.driverPinLength, 24, pin.keyPinLength - 8); + + // Draw triangular bottom in pixel art style + pin.keyPin.fillRect(-12, -50 + pin.driverPinLength + pin.keyPinLength - 8, 24, 2); + pin.keyPin.fillRect(-10, -50 + pin.driverPinLength + pin.keyPinLength - 6, 20, 2); + pin.keyPin.fillRect(-8, -50 + pin.driverPinLength + pin.keyPinLength - 4, 16, 2); + pin.keyPin.fillRect(-6, -50 + pin.driverPinLength + pin.keyPinLength - 2, 12, 2); + + pin.driverPin.clear(); + pin.driverPin.fillStyle(0x3388dd); + pin.driverPin.fillRect(-12, -50, 24, pin.driverPinLength); + + // Reset spring to original position + pin.spring.clear(); + pin.spring.fillStyle(0x666666); + const springTop = -130; // Fixed spring top + const springBottom = -50; // Driver pin top when not lifted + const springHeight = springBottom - springTop; + + // Calculate total spring space and distribute segments evenly + const totalSpringSpace = springHeight; + const segmentSpacing = totalSpringSpace / 11; // 11 gaps between 12 segments + + for (let s = 0; s < 12; s++) { + const segmentHeight = 4; + const segmentY = springTop + (s * segmentSpacing); + pin.spring.fillRect(-12, segmentY, 24, segmentHeight); + } + + // Hide all highlights + if (pin.shearHighlight) pin.shearHighlight.setVisible(false); + if (pin.setHighlight) pin.setHighlight.setVisible(false); + if (pin.bindingHighlight) pin.bindingHighlight.setVisible(false); + if (pin.overpickedHighlight) pin.overpickedHighlight.setVisible(false); + if (pin.failureHighlight) pin.failureHighlight.setVisible(false); + }); + + // Reset lock state + this.parent.lockState.pinsSet = 0; + } + + this.updateBindingPins(); + } + }); + + // Add keyboard release handler for pin keys + this.parent.scene.input.keyboard.on('keyup', (event) => { + const key = event.key; + + // Pin number keys (1-8) + if (key >= '1' && key <= '8') { + const pinIndex = parseInt(key) - 1; // Convert 1-8 to 0-7 + + // Check if pin exists and is currently being held + if (pinIndex < this.parent.pinCount && this.parent.lockState.currentPin && this.parent.lockState.currentPin.index === pinIndex) { + this.parent.checkPinSet(this.parent.lockState.currentPin); + this.parent.lockState.currentPin = null; + this.parent.gameState.mouseDown = false; + + // Return hook to resting position + if (this.parent.hookPickGraphics && this.parent.hookConfig) { + this.parent.toolMgr.returnHookToStart(); + } + } + } + }); + + // Add key interaction handlers if in key mode + if (this.parent.keyMode && this.parent.keyClickZone) { + console.log('Setting up key click handler...'); + this.parent.keyClickZone.on('pointerdown', (pointer) => { + console.log('Key clicked! Event triggered.'); + // Prevent this event from bubbling up to global handlers + pointer.event.stopPropagation(); + + if (!this.parent.keyInserting) { + console.log('Starting key insertion animation...'); + this.parent.keyOps.startKeyInsertion(); + } else { + console.log('Key insertion already in progress, ignoring click.'); + } + }); + } else { + console.log('Key mode or click zone not available:', { keyMode: this.parent.keyMode, hasClickZone: !!this.parent.keyClickZone }); + } + } + + liftPin() { + if (!this.parent.lockState.currentPin || !this.parent.gameState.mouseDown) return; + + const pin = this.parent.lockState.currentPin; + const liftSpeed = this.parent.liftSpeed; + const shearLineY = -45; + + // If pin is set and not already overpicked, allow key pin to move up, driver pin stays at SL + if (pin.isSet && !pin.isOverpicked) { + // Move key pin up gradually from its dropped position (slower when not connected to driver pin) + const keyPinLiftSpeed = liftSpeed * 0.5; // Half speed for key pin movement + // Key pin should stop when its top surface reaches the shear line + // The key pin's top is at: -50 + pin.driverPinLength - pin.keyPinHeight + // We want this to equal -45 (shear line) + // So: -50 + pin.driverPinLength - pin.keyPinHeight = -45 + // Therefore: pin.keyPinHeight = pin.driverPinLength - 5 + const maxKeyPinHeight = pin.driverPinLength - 5; // Top of key pin at shear line + pin.keyPinHeight = Math.min(pin.keyPinHeight + keyPinLiftSpeed, maxKeyPinHeight); + + // If key pin reaches driver pin, start overpicking timer + if (pin.keyPinHeight >= maxKeyPinHeight) { // Key pin top at shear line + // Start overpicking timer if not already started + if (!pin.overpickingTimer) { + pin.overpickingTimer = Date.now(); + this.parent.updateFeedback("Key pin at shear line. Release now or continue to overpick..."); + } + + // Check if 500ms have passed since reaching shear line + if (Date.now() - pin.overpickingTimer >= 500) { + // Both move up together + pin.isOverpicked = true; + pin.keyPinHeight = 90; // Move both up above SL + pin.driverPinHeight = 90; // Driver pin moves up too + + // Play overpicking sound + if (this.parent.sounds.overtension) { + this.parent.sounds.overtension.play(); + if (typeof navigator !== 'undefined' && navigator.vibrate) { + navigator.vibrate(500); + } + + } + + // Mark as overpicked and stuck + this.parent.updateFeedback("Set pin overpicked! Release tension to reset."); + if (!pin.failureHighlight) { + pin.failureHighlight = this.parent.scene.add.graphics(); + pin.failureHighlight.fillStyle(0xff6600, 0.7); + pin.failureHighlight.fillRect(-22.5, -110, 45, 140); + pin.container.add(pin.failureHighlight); + } + pin.failureHighlight.setVisible(true); + if (pin.setHighlight) pin.setHighlight.setVisible(false); + } + } + + // Draw key pin (rectangular part) - move gradually from dropped position + pin.keyPin.clear(); + pin.keyPin.fillStyle(0xdd3333); + // Calculate key pin position based on keyPinHeight (gradual movement from dropped position) + const keyPinY = -50 + pin.driverPinLength - pin.keyPinHeight; + pin.keyPin.fillRect(-12, keyPinY, 24, pin.keyPinLength - 8); + // Draw triangle + pin.keyPin.fillRect(-12, keyPinY + pin.keyPinLength - 8, 24, 2); + pin.keyPin.fillRect(-10, keyPinY + pin.keyPinLength - 6, 20, 2); + pin.keyPin.fillRect(-8, keyPinY + pin.keyPinLength - 4, 16, 2); + pin.keyPin.fillRect(-6, keyPinY + pin.keyPinLength - 2, 12, 2); + // Draw driver pin at shear line (stays at SL until overpicked) + pin.driverPin.clear(); + pin.driverPin.fillStyle(0x3388dd); + const shearLineY = -45; + const driverPinY = shearLineY - pin.driverPinLength; // Driver pin bottom at shear line + pin.driverPin.fillRect(-12, driverPinY, 24, pin.driverPinLength); + // Spring + pin.spring.clear(); + pin.spring.fillStyle(0x666666); + const springTop = -130; + const springBottom = shearLineY - pin.driverPinLength; // Driver pin top (at shear line) + const springHeight = springBottom - springTop; + const totalSpringSpace = springHeight; + const segmentSpacing = totalSpringSpace / 11; + for (let s = 0; s < 12; s++) { + const segmentHeight = 4 * 0.3; + const segmentY = springTop + (s * segmentSpacing); + if (segmentY + segmentHeight <= springBottom) { + pin.spring.fillRect(-12, segmentY, 24, segmentHeight); + } + } + // Continue lifting if mouse is still down + if (this.parent.gameState.mouseDown && !pin.isOverpicked) { + requestAnimationFrame(() => this.liftPin()); + } + return; // Exit early for set pins - don't run normal lifting logic + } + + // Existing overpicking and normal lifting logic follows... + // Check for overpicking when tension is applied (for binding pins and set pins) + if (this.parent.lockState.tensionApplied && (this.parent.shouldPinBind(pin) || pin.isSet)) { + // For set pins, use keyPinHeight; for normal pins, use currentHeight + const heightToCheck = pin.isSet ? pin.keyPinHeight : pin.currentHeight; + const boundaryPosition = -50 + pin.driverPinLength - heightToCheck; + + // If key pin is pushed too far beyond shear line, it gets stuck + if (boundaryPosition < shearLineY - 10) { + // Check if this pin being overpicked would prevent automatic success + // If all other pins are correctly positioned, don't allow overpicking + let otherPinsCorrect = true; + this.parent.pins.forEach(otherPin => { + if (otherPin !== pin && !otherPin.isOverpicked) { + const otherBoundaryPosition = -50 + otherPin.driverPinLength - otherPin.currentHeight; + const otherDistanceToShearLine = Math.abs(otherBoundaryPosition - shearLineY); + if (otherDistanceToShearLine > 8) { + otherPinsCorrect = false; + } + } + }); + + // If other pins are correct and this pin is being actively moved, prevent overpicking + if (otherPinsCorrect && this.parent.gameState.mouseDown) { + // Stop the pin from moving further up but don't mark as overpicked + if (pin.isSet) { + const maxKeyPinHeight = pin.driverPinLength - 5; // Top of key pin at shear line + pin.keyPinHeight = Math.min(pin.keyPinHeight, maxKeyPinHeight); + } else { + // Use pin-specific maximum height for overpicking prevention + const baseMaxHeight = 75; + const maxHeightReduction = 15; + const pinHeightFactor = pin.index / (this.parent.pinCount - 1); + const pinMaxHeight = baseMaxHeight - (maxHeightReduction * pinHeightFactor); + pin.currentHeight = Math.min(pin.currentHeight, pinMaxHeight); + } + return; + } + + // Otherwise, allow normal overpicking behavior + pin.isOverpicked = true; + + // Play overpicking sound + if (this.parent.sounds.overtension) { + this.parent.sounds.overtension.play(); + if (typeof navigator !== 'undefined' && navigator.vibrate) { + navigator.vibrate(500); + } + } + + if (pin.isSet) { + this.parent.updateFeedback("Set pin overpicked! Release tension to reset."); + + // Show failure highlight for overpicked set pins + if (!pin.failureHighlight) { + pin.failureHighlight = this.parent.scene.add.graphics(); + pin.failureHighlight.fillStyle(0xff6600, 0.7); + pin.failureHighlight.fillRect(-22.5, -110, 45, 140); + pin.container.add(pin.failureHighlight); + } + pin.failureHighlight.setVisible(true); + + // Hide set highlight + if (pin.setHighlight) pin.setHighlight.setVisible(false); + } else { + this.parent.updateFeedback("Pin overpicked! Release tension to reset."); + + // Show overpicked highlight for regular pins + if (!pin.overpickedHighlight) { + pin.overpickedHighlight = this.parent.scene.add.graphics(); + pin.overpickedHighlight.fillStyle(0xff0000, 0.6); + pin.overpickedHighlight.fillRect(-22.5, -110, 45, 140); + pin.container.add(pin.overpickedHighlight); + } + pin.overpickedHighlight.setVisible(true); + } + + // Don't return - allow further pushing even when overpicked + } + } + + // Calculate pin-specific maximum height (further pins have less upward movement) + const baseMaxHeight = 75; // Base maximum height for closest pin + const maxHeightReduction = 15; // Maximum reduction for furthest pin + const pinHeightFactor = pin.index / (this.parent.pinCount - 1); // 0 for first pin, 1 for last pin + const pinMaxHeight = baseMaxHeight - (maxHeightReduction * pinHeightFactor); + + pin.currentHeight = Math.min(pin.currentHeight + liftSpeed, pinMaxHeight); + + // Update visual - both pins move up together toward the spring + pin.keyPin.clear(); + pin.keyPin.fillStyle(0xdd3333); + + // Draw rectangular part of key pin + pin.keyPin.fillRect(-12, -50 + pin.driverPinLength - pin.currentHeight, 24, pin.keyPinLength - 8); + + // Update hook position to follow any moving pin + if (pin.currentHeight > 0) { + this.parent.updateHookPosition(pin.index); + } + + // Draw triangular bottom in pixel art style + pin.keyPin.fillRect(-12, -50 + pin.driverPinLength - pin.currentHeight + pin.keyPinLength - 8, 24, 2); + pin.keyPin.fillRect(-10, -50 + pin.driverPinLength - pin.currentHeight + pin.keyPinLength - 6, 20, 2); + pin.keyPin.fillRect(-8, -50 + pin.driverPinLength - pin.currentHeight + pin.keyPinLength - 4, 16, 2); + pin.keyPin.fillRect(-6, -50 + pin.driverPinLength - pin.currentHeight + pin.keyPinLength - 2, 12, 2); + + pin.driverPin.clear(); + pin.driverPin.fillStyle(0x3388dd); + pin.driverPin.fillRect(-12, -50 - pin.currentHeight, 24, pin.driverPinLength); + + // Spring compresses as pins push up (segments get shorter and closer together) + pin.spring.clear(); + pin.spring.fillStyle(0x666666); + const springCompression = pin.currentHeight; + const compressionFactor = Math.max(0.3, 1 - (springCompression / 60)); // Segments get shorter, minimum 30% size (1.2px) + + // Fixed spring top position + const springTop = -130; + // Spring bottom follows driver pin top + const driverPinTop = -50 - pin.currentHeight; + const springBottom = driverPinTop; + const springHeight = springBottom - springTop; + + // Calculate total spring space and distribute segments evenly + const totalSpringSpace = springHeight; + const segmentSpacing = totalSpringSpace / 11; // 11 gaps between 12 segments - keep consistent spacing + + for (let s = 0; s < 12; s++) { + const segmentHeight = 4 * compressionFactor; + const segmentY = springTop + (s * segmentSpacing); + + if (segmentY + segmentHeight <= springBottom) { // Only show segments within spring bounds + pin.spring.fillRect(-12, segmentY, 24, segmentHeight); + } + } + + // Check if the key/driver boundary is at the shear line (much higher position) + const boundaryPosition = -50 + pin.driverPinLength - pin.currentHeight; + const distanceToShearLine = Math.abs(boundaryPosition - shearLineY); + + // Calculate threshold based on sensitivity (same as pin setting logic) + const baseThreshold = 8; + const sensitivityFactor = (9 - this.parent.thresholdSensitivity) / 8; // Updated for 1-8 range + const threshold = baseThreshold * sensitivityFactor; + + if (distanceToShearLine < threshold && this.parent.highlightPinAlignment) { + // Show green highlight when boundary is at shear line (only if alignment highlighting is enabled) + if (!pin.shearHighlight) { + pin.shearHighlight = this.parent.scene.add.graphics(); + pin.shearHighlight.fillStyle(0x00ff00, 0.4); + pin.shearHighlight.fillRect(-22.5, -110, 45, 140); + pin.container.addAt(pin.shearHighlight, 0); // Add at beginning to appear behind pins + } + + // Check if highlight is transitioning from hidden to visible + const wasHidden = !pin.shearHighlight.visible; + pin.shearHighlight.setVisible(true); + + // Play feedback when highlight first appears + if (wasHidden) { + if (this.parent.sounds.click) { + this.parent.sounds.click.play(); + } + if (typeof navigator !== 'undefined' && navigator.vibrate) { + if (typeof navigator !== 'undefined' && navigator.vibrate) { + navigator.vibrate(100); + } + } + } + } else { + if (pin.shearHighlight) { + pin.shearHighlight.setVisible(false); + } + } + } + + applyGravity() { + // When tension is not applied, all pins fall back down (except overpicked ones) + // Also, pins that are not binding fall back down even with tension + this.parent.pins.forEach(pin => { + const shouldFall = !this.parent.lockState.tensionApplied || (!this.parent.shouldPinBind(pin) && !pin.isSet); + if (pin.currentHeight > 0 && !pin.isOverpicked && shouldFall) { + pin.currentHeight = Math.max(0, pin.currentHeight - 2.25); // Fall faster than lift (25% slower: 2.25 instead of 3) + + // Update visual + pin.keyPin.clear(); + pin.keyPin.fillStyle(0xdd3333); + + // Draw rectangular part of key pin + pin.keyPin.fillRect(-12, -50 + pin.driverPinLength - pin.currentHeight, 24, pin.keyPinLength - 8); + + // Update hook position to follow any moving pin + this.parent.updateHookPosition(pin.index); + + // Draw triangular bottom in pixel art style + pin.keyPin.fillRect(-12, -50 + pin.driverPinLength - pin.currentHeight + pin.keyPinLength - 8, 24, 2); + pin.keyPin.fillRect(-10, -50 + pin.driverPinLength - pin.currentHeight + pin.keyPinLength - 6, 20, 2); + pin.keyPin.fillRect(-8, -50 + pin.driverPinLength - pin.currentHeight + pin.keyPinLength - 4, 16, 2); + pin.keyPin.fillRect(-6, -50 + pin.driverPinLength - pin.currentHeight + pin.keyPinLength - 2, 12, 2); + + pin.driverPin.clear(); + pin.driverPin.fillStyle(0x3388dd); + pin.driverPin.fillRect(-12, -50 - pin.currentHeight, 24, pin.driverPinLength); + + // Spring decompresses as pins fall + pin.spring.clear(); + pin.spring.fillStyle(0x666666); + const springCompression = pin.currentHeight; + const compressionFactor = Math.max(0.3, 1 - (springCompression / 60)); // Segments get shorter, minimum 30% size (1.2px) + + // Fixed spring top position + const springTop = -130; + // Spring bottom follows driver pin top + const driverPinTop = -50 - pin.currentHeight; + const springBottom = driverPinTop; + const springHeight = springBottom - springTop; + + // Calculate total spring space and distribute segments evenly + const totalSpringSpace = springHeight; + const segmentSpacing = totalSpringSpace / 11; // 11 gaps between 12 segments - keep consistent spacing + + for (let s = 0; s < 12; s++) { + const segmentHeight = 4 * compressionFactor; + const segmentY = springTop + (s * segmentSpacing); + + if (segmentY + segmentHeight <= springBottom) { // Only show segments within spring bounds + pin.spring.fillRect(-12, segmentY, 24, segmentHeight); + } + } + + // Hide highlights when falling + if (pin.shearHighlight) pin.shearHighlight.setVisible(false); + if (pin.setHighlight) pin.setHighlight.setVisible(false); + if (pin.bindingHighlight) pin.bindingHighlight.setVisible(false); + if (pin.overpickedHighlight) pin.overpickedHighlight.setVisible(false); + if (pin.failureHighlight) pin.failureHighlight.setVisible(false); + } else if (pin.isSet && shouldFall) { + // Set pins fall back down when tension is released + pin.isSet = false; + pin.keyPinHeight = 0; + pin.driverPinHeight = 0; + pin.currentHeight = 0; + + // Reset visual to original position + pin.keyPin.clear(); + pin.keyPin.fillStyle(0xdd3333); + pin.keyPin.fillRect(-12, -50 + pin.driverPinLength, 24, pin.keyPinLength - 8); + pin.keyPin.fillRect(-12, -50 + pin.driverPinLength + pin.keyPinLength - 8, 24, 2); + pin.keyPin.fillRect(-10, -50 + pin.driverPinLength + pin.keyPinLength - 6, 20, 2); + pin.keyPin.fillRect(-8, -50 + pin.driverPinLength + pin.keyPinLength - 4, 16, 2); + pin.keyPin.fillRect(-6, -50 + pin.driverPinLength + pin.keyPinLength - 2, 12, 2); + + pin.driverPin.clear(); + pin.driverPin.fillStyle(0x3388dd); + pin.driverPin.fillRect(-12, -50, 24, pin.driverPinLength); + + // Reset spring + pin.spring.clear(); + pin.spring.fillStyle(0x666666); + const springTop = -130; + const springBottom = -50; + const springHeight = springBottom - springTop; + const segmentSpacing = springHeight / 11; + for (let s = 0; s < 12; s++) { + const segmentHeight = 4; + const segmentY = springTop + (s * segmentSpacing); + pin.spring.fillRect(-12, segmentY, 24, segmentHeight); + } + + // Hide set highlight + if (pin.setHighlight) pin.setHighlight.setVisible(false); + } + }); + } + + checkAllPinsCorrect() { + const shearLineY = -45; + const threshold = 8; // Same threshold as individual pin checking + + let allCorrect = true; + + this.parent.pins.forEach(pin => { + if (pin.isOverpicked) { + allCorrect = false; + return; + } + + // Calculate current boundary position between key and driver pins + const boundaryPosition = -50 + pin.driverPinLength - pin.currentHeight; + const distanceToShearLine = Math.abs(boundaryPosition - shearLineY); + + // Check if driver pin is above shear line and key pin is below + const driverPinBottom = boundaryPosition; + const keyPinTop = boundaryPosition; + + // Driver pin should be above shear line, key pin should be below + if (driverPinBottom > shearLineY + threshold || keyPinTop < shearLineY - threshold) { + allCorrect = false; + } + }); + + // If all pins are correctly positioned, set them all and complete the lock + if (allCorrect && this.parent.lockState.pinsSet < this.parent.pinCount) { + this.parent.pins.forEach(pin => { + if (!pin.isSet) { + pin.isSet = true; + + // Show set pin highlight + if (!pin.setHighlight) { + pin.setHighlight = this.parent.scene.add.graphics(); + pin.setHighlight.fillStyle(0x00ff00, 0.5); + pin.setHighlight.fillRect(-22.5, -110, 45, 140); + pin.container.addAt(pin.setHighlight, 0); // Add at beginning to appear behind pins + } + pin.setHighlight.setVisible(true); + + // Hide other highlights + if (pin.shearHighlight) pin.shearHighlight.setVisible(false); + if (pin.highlight) pin.highlight.setVisible(false); + if (pin.overpickedHighlight) pin.overpickedHighlight.setVisible(false); + if (pin.failureHighlight) pin.failureHighlight.setVisible(false); + } + }); + + this.parent.lockState.pinsSet = this.parent.pinCount; + this.parent.updateFeedback("All pins correctly positioned! Lock picked successfully!"); + this.parent.keyAnim.lockPickingSuccess(); + } + } + + updateBindingPins() { + if (!this.parent.lockState.tensionApplied || !this.parent.highlightBindingOrder) { + this.parent.pins.forEach(pin => { + // Hide binding highlight + if (pin.bindingHighlight) { + pin.bindingHighlight.setVisible(false); + } + }); + return; + } + + // Find the next unset pin in binding order + for (let order = 0; order < this.parent.pinCount; order++) { + const nextPin = this.parent.pins.find(p => p.binding === order && !p.isSet); + if (nextPin) { + this.parent.pins.forEach(pin => { + if (pin.index === nextPin.index && !pin.isSet) { + // Show binding highlight for next pin + if (!pin.bindingHighlight) { + pin.bindingHighlight = this.parent.scene.add.graphics(); + pin.bindingHighlight.fillStyle(0xffff00, 0.6); + pin.bindingHighlight.fillRect(-22.5, -110, 45, 140); + pin.container.addAt(pin.bindingHighlight, 0); // Add at beginning to appear behind pins + } + pin.bindingHighlight.setVisible(true); + + // Play binding sound when highlighting next binding pin + if (this.parent.sounds.binding) { + this.parent.sounds.binding.play(); + } + } else if (!pin.isSet) { + // Hide binding highlight for other pins + if (pin.bindingHighlight) { + pin.bindingHighlight.setVisible(false); + } + } + }); + return; + } + } + + // All pins set + this.parent.pins.forEach(pin => { + if (!pin.isSet && pin.bindingHighlight) { + pin.bindingHighlight.setVisible(false); + } + }); + } + + resetAllPins() { + this.parent.pins.forEach(pin => { + if (!pin.isSet) { + pin.currentHeight = 0; + pin.isOverpicked = false; // Reset overpicked state + pin.keyPinHeight = 0; // Reset key pin height + pin.driverPinHeight = 0; // Reset driver pin height + + // Reset key pin to original position + pin.keyPin.clear(); + pin.keyPin.fillStyle(0xdd3333); + + // Draw rectangular part of key pin + pin.keyPin.fillRect(-12, -50 + pin.driverPinLength, 24, pin.keyPinLength - 8); + + // Draw triangular bottom in pixel art style + pin.keyPin.fillRect(-12, -50 + pin.driverPinLength + pin.keyPinLength - 8, 24, 2); + pin.keyPin.fillRect(-10, -50 + pin.driverPinLength + pin.keyPinLength - 6, 20, 2); + pin.keyPin.fillRect(-8, -50 + pin.driverPinLength + pin.keyPinLength - 4, 16, 2); + pin.keyPin.fillRect(-6, -50 + pin.driverPinLength + pin.keyPinLength - 2, 12, 2); + + // Reset driver pin to original position + pin.driverPin.clear(); + pin.driverPin.fillStyle(0x3388dd); + pin.driverPin.fillRect(-12, -50, 24, pin.driverPinLength); + + // Reset spring to original position (all 12 segments visible) + pin.spring.clear(); + pin.spring.fillStyle(0x666666); + const springTop = -130; // Fixed spring top + const springBottom = -50; // Driver pin top when not lifted + const springHeight = springBottom - springTop; + + // Calculate total spring space and distribute segments evenly + const totalSpringSpace = springHeight; + const segmentSpacing = totalSpringSpace / 11; // 11 gaps between 12 segments + + for (let s = 0; s < 12; s++) { + const segmentHeight = 4; + const segmentY = springTop + (s * segmentSpacing); + pin.spring.fillRect(-12, segmentY, 24, segmentHeight); + } + + // Hide all highlights + if (pin.shearHighlight) pin.shearHighlight.setVisible(false); + if (pin.setHighlight) pin.setHighlight.setVisible(false); + if (pin.bindingHighlight) pin.bindingHighlight.setVisible(false); + } + }); + } + +} diff --git a/js/minigames/lockpicking/tool-manager.js b/js/minigames/lockpicking/tool-manager.js new file mode 100644 index 0000000..b4bc4a6 --- /dev/null +++ b/js/minigames/lockpicking/tool-manager.js @@ -0,0 +1,258 @@ + +/** + * ToolManager + * + * Extracted from lockpicking-game-phaser.js + * Instantiate with: new ToolManager(this) + * + * All 'this' references replaced with 'this.parent' to access parent instance state: + * - this.parent.pins (array of pin objects) + * - this.parent.scene (Phaser scene) + * - this.parent.lockId (lock identifier) + * - this.parent.lockState (lock state object) + * etc. + */ +export class ToolManager { + + constructor(parent) { + this.parent = parent; + } + + hideLockpickingTools() { + // Hide tension wrench and hook pick in key mode + if (this.parent.tensionWrench) { + this.parent.tensionWrench.setVisible(false); + } + if (this.parent.hookGroup) { + this.parent.hookGroup.setVisible(false); + } + + // Hide labels + if (this.parent.wrenchText) { + this.parent.wrenchText.setVisible(false); + } + if (this.parent.hookPickLabel) { + this.parent.hookPickLabel.setVisible(false); + } + } + + returnHookToStart() { + if (!this.parent.hookGroup || !this.parent.hookConfig) return; + + const config = this.parent.hookConfig; + + console.log('Returning hook to starting position (no rotation)'); + + // Get the current X position from the last targeted pin + const pinSpacing = 400 / (this.parent.pinCount + 1); + const margin = pinSpacing * 0.75; + const targetPinIndex = config.lastTargetedPin; + const currentX = 100 + margin + targetPinIndex * pinSpacing; // Last targeted pin's X position + + // Calculate the tip position for the current pin + const totalHookHeight = (config.diagonalSegments + config.verticalSegments) * config.segmentStep; + const tipX = currentX - totalHookHeight + 48; // Add 48px offset (24px + 24px further right) + + // Calculate resting Y position (a few pixels lower than original) + const restingY = config.hookStartY - 24; // 24px lower than original position (was 15px) + + // Reset position and rotation + this.parent.hookGroup.x = tipX; + this.parent.hookGroup.y = restingY; + this.parent.hookGroup.setAngle(0); + + // Clear debug graphics when hook returns to start + if (this.parent.debugGraphics) { + this.parent.debugGraphics.clear(); + } + } + + start() { + super.start(); + this.parent.gameState.isActive = true; + this.parent.lockState.tensionApplied = false; + this.parent.lockState.pinsSet = 0; + this.parent.updateProgress(0, this.parent.pinCount); + } + + cleanup() { + if (this.parent.game) { + this.parent.game.destroy(true); + this.parent.game = null; + } + super.cleanup(); + } + + flashWrenchRed() { + // Flash the tension wrench red to indicate tension is needed + if (!this.parent.wrenchGraphics) return; + + const originalFillStyle = this.parent.lockState.tensionApplied ? 0x00ff00 : 0x888888; + + // Store original state + const originalClear = this.parent.wrenchGraphics.clear.bind(this.parent.wrenchGraphics); + + // Flash red 3 times + for (let i = 0; i < 3; i++) { + this.parent.scene.time.delayedCall(i * 150, () => { + this.parent.wrenchGraphics.clear(); + this.parent.wrenchGraphics.fillStyle(0xff0000); // Red + + // Long vertical arm + this.parent.wrenchGraphics.fillRect(0, -120, 10, 170); + // Short horizontal arm + this.parent.wrenchGraphics.fillRect(0, 40, 37.5, 10); + }); + + this.parent.scene.time.delayedCall(i * 150 + 75, () => { + this.parent.wrenchGraphics.clear(); + this.parent.wrenchGraphics.fillStyle(originalFillStyle); // Back to original color + + // Long vertical arm + this.parent.wrenchGraphics.fillRect(0, -120, 10, 170); + // Short horizontal arm + this.parent.wrenchGraphics.fillRect(0, 40, 37.5, 10); + }); + } + } + + switchToPickMode() { + // Switch from key selection mode to lockpicking mode + console.log('Switching from key mode to lockpicking mode'); + + // Hide the mode switch button + const switchBtn = document.getElementById('lockpicking-switch-mode-btn'); + if (switchBtn) { + switchBtn.style.display = 'none'; + } + + // Exit key mode + this.parent.keyMode = false; + this.parent.keySelectionMode = false; + + // Clean up key selection UI if visible + if (this.parent.keySelectionContainer) { + this.parent.keySelectionContainer.destroy(); + this.parent.keySelectionContainer = null; + } + + // Clean up any key visuals + if (this.parent.keyGroup) { + this.parent.keyGroup.destroy(); + this.parent.keyGroup = null; + } + if (this.parent.keyClickZone) { + this.parent.keyClickZone.destroy(); + this.parent.keyClickZone = null; + } + + // Show lockpicking tools + if (this.parent.tensionWrench) { + this.parent.tensionWrench.setVisible(true); + } + if (this.parent.hookGroup) { + this.parent.hookGroup.setVisible(true); + } + if (this.parent.wrenchText) { + this.parent.wrenchText.setVisible(true); + } + if (this.parent.hookPickLabel) { + this.parent.hookPickLabel.setVisible(true); + } + + // Reset pins to original positions + this.parent.lockConfig.resetPinsToOriginalPositions(); + + // Update feedback + this.parent.updateFeedback("Lockpicking mode - Apply tension first, then lift pins in binding order"); + } + + showLockpickingTools() { + // Show tension wrench and hook pick in lockpicking mode + if (this.parent.tensionWrench) { + this.parent.tensionWrench.setVisible(true); + } + if (this.parent.hookGroup) { + this.parent.hookGroup.setVisible(true); + } + + // Show labels + if (this.parent.wrenchText) { + this.parent.wrenchText.setVisible(true); + } + if (this.parent.hookPickLabel) { + this.parent.hookPickLabel.setVisible(true); + } + } + + switchToKeyMode() { + // Switch from lockpicking mode to key selection mode + console.log('Switching from lockpicking mode to key mode'); + + // Hide the mode switch button + const switchBtn = document.getElementById('lockpicking-switch-to-keys-btn'); + if (switchBtn) { + switchBtn.style.display = 'none'; + } + + // Enter key mode + this.parent.keyMode = true; + this.parent.keySelectionMode = true; + + // Hide lockpicking tools + if (this.parent.tensionWrench) { + this.parent.tensionWrench.setVisible(false); + } + if (this.parent.hookGroup) { + this.parent.hookGroup.setVisible(false); + } + if (this.parent.wrenchText) { + this.parent.wrenchText.setVisible(false); + } + if (this.parent.hookPickLabel) { + this.parent.hookPickLabel.setVisible(false); + } + + // Reset pins to original positions + this.parent.lockConfig.resetPinsToOriginalPositions(); + + // Add mode switch back button (can switch back to lockpicking if available) + if (this.parent.canSwitchToPickMode) { + const itemDisplayDiv = document.querySelector('.lockpicking-item-section'); + if (itemDisplayDiv) { + // Remove any existing button container + const existingButtonContainer = itemDisplayDiv.querySelector('div[style*="margin-top"]'); + if (existingButtonContainer) { + existingButtonContainer.remove(); + } + + // Add new button container + const buttonContainer = document.createElement('div'); + buttonContainer.style.cssText = ` + display: flex; + gap: 10px; + margin-top: 10px; + justify-content: center; + `; + + const switchModeBtn = document.createElement('button'); + switchModeBtn.className = 'minigame-button'; + switchModeBtn.id = 'lockpicking-switch-mode-btn'; + switchModeBtn.innerHTML = 'Lockpick Switch to Lockpicking'; + switchModeBtn.onclick = () => this.parent.switchToPickMode(); + + buttonContainer.appendChild(switchModeBtn); + itemDisplayDiv.appendChild(buttonContainer); + } + } + + // Show key selection UI with available keys + if (this.parent.availableKeys && this.parent.availableKeys.length > 0) { + this.parent.createKeySelectionUI(this.parent.availableKeys, this.parent.requiredKeyId); + this.parent.updateFeedback("Select a key to use"); + } else { + this.parent.updateFeedback("No keys available"); + } + } + +}