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 = '
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 = '
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 = '
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 = '
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");
+ }
+ }
+
+}