From 741ec558649acd9971830a6034cb8f45b4b714b9 Mon Sep 17 00:00:00 2001 From: "Z. Cliffe Schreuders" Date: Mon, 27 Oct 2025 16:53:18 +0000 Subject: [PATCH] Refactor lockpicking mechanics and visuals - Updated PinManagement to utilize new PinVisuals class for handling pin visuals and interactions. - Replaced direct feedback updates with keyInsertion feedback methods for consistency. - Introduced a new PinVisuals class to encapsulate pin rendering logic and improve code organization. - Adjusted tool manager feedback methods to align with the new structure. - Enhanced pin highlighting and visual updates during gameplay for better user experience. --- .vscode/settings.json | 3 +- js/minigames/lockpicking/game-utilities.js | 42 + js/minigames/lockpicking/hook-mechanics.js | 204 +++ js/minigames/lockpicking/key-animation.js | 10 +- js/minigames/lockpicking/key-drawing.js | 209 +++ js/minigames/lockpicking/key-geometry.js | 152 ++ js/minigames/lockpicking/key-insertion.js | 107 ++ js/minigames/lockpicking/key-operations.js | 18 +- js/minigames/lockpicking/key-path-drawing.js | 192 +++ .../lockpicking/key-point-geometry.js | 219 +++ js/minigames/lockpicking/key-selection.js | 6 +- .../lockpicking/lock-configuration.js | 2 +- js/minigames/lockpicking/lock-graphics.js | 4 +- .../lockpicking/lockpicking-game-phaser.js | 1298 +---------------- js/minigames/lockpicking/pin-management.js | 36 +- js/minigames/lockpicking/pin-visuals.js | 291 ++++ js/minigames/lockpicking/tool-manager.js | 8 +- 17 files changed, 1498 insertions(+), 1303 deletions(-) create mode 100644 js/minigames/lockpicking/game-utilities.js create mode 100644 js/minigames/lockpicking/hook-mechanics.js create mode 100644 js/minigames/lockpicking/key-drawing.js create mode 100644 js/minigames/lockpicking/key-geometry.js create mode 100644 js/minigames/lockpicking/key-insertion.js create mode 100644 js/minigames/lockpicking/key-path-drawing.js create mode 100644 js/minigames/lockpicking/key-point-geometry.js create mode 100644 js/minigames/lockpicking/pin-visuals.js diff --git a/.vscode/settings.json b/.vscode/settings.json index 076ee3e..5b335c6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,4 @@ { - "cursor.general.disableHttp2": true + "cursor.general.disableHttp2": true, + "chat.agent.maxRequests": 50 } \ No newline at end of file diff --git a/js/minigames/lockpicking/game-utilities.js b/js/minigames/lockpicking/game-utilities.js new file mode 100644 index 0000000..de139fa --- /dev/null +++ b/js/minigames/lockpicking/game-utilities.js @@ -0,0 +1,42 @@ + +/** + * GameUtilities + * + * Extracted from lockpicking-game-phaser.js + * Instantiate with: new GameUtilities(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 GameUtilities { + + constructor(parent) { + this.parent = parent; + } + + shouldPinBind(pin) { + if (!this.parent.lockState.tensionApplied) return false; + + // 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) { + return pin.index === nextPin.index; + } + } + return false; + } + + shuffleArray(array) { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + return array; + } + +} diff --git a/js/minigames/lockpicking/hook-mechanics.js b/js/minigames/lockpicking/hook-mechanics.js new file mode 100644 index 0000000..5a3e7c8 --- /dev/null +++ b/js/minigames/lockpicking/hook-mechanics.js @@ -0,0 +1,204 @@ + +/** + * HookMechanics + * + * Extracted from lockpicking-game-phaser.js + * Instantiate with: new HookMechanics(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 HookMechanics { + + constructor(parent) { + this.parent = parent; + } + + updateHookPosition(pinIndex) { + if (!this.parent.hookGroup || !this.parent.hookConfig) return; + + const config = this.parent.hookConfig; + const targetPin = this.parent.pins[pinIndex]; + + if (!targetPin) return; + + // Calculate the target Y position (bottom of the key pin) + const pinWorldY = 200; // Base Y position for pins + const currentTargetY = pinWorldY - 50 + targetPin.driverPinLength + targetPin.keyPinLength - targetPin.currentHeight; + + console.log('Hook update - following pin:', pinIndex, 'currentHeight:', targetPin.currentHeight, 'targetY:', currentTargetY); + + // Update the last targeted pin + this.parent.hookConfig.lastTargetedPin = pinIndex; + + // Calculate the pin's X position (same logic as createPins) + const pinSpacing = 400 / (this.parent.pinCount + 1); + const margin = pinSpacing * 0.75; + const pinX = 100 + margin + pinIndex * pinSpacing; + + // Calculate the pin's base Y position (when currentHeight = 0) + const pinBaseY = pinWorldY - 50 + targetPin.driverPinLength + targetPin.keyPinLength; + + // Calculate how much the pin has moved from its own base position + const heightDifference = pinBaseY - currentTargetY; + + // Calculate rotation angle based on percentage of pin movement and pin number + const maxHeightDifference = 50; // Maximum expected height difference + const minRotationDegrees = 20; // Minimum rotation for highest pin + const maxRotationDegrees = 40; // Maximum rotation for lowest pin + + // Calculate pin-based rotation range (pin 0 = max rotation, pin n-1 = min rotation) + const pinRotationRange = maxRotationDegrees - minRotationDegrees; + const pinRotationFactor = pinIndex / (this.parent.pinCount - 1); // 0 for first pin, 1 for last pin + const pinRotationOffset = pinRotationRange * pinRotationFactor; + const pinMaxRotation = maxRotationDegrees - pinRotationOffset; + + // Calculate percentage of pin movement (0% to 100%) + const pinMovementPercentage = Math.min((heightDifference / maxHeightDifference) * 100, 100); + + // Calculate rotation based on percentage and pin-specific max rotation + // Higher pin indices (further pins) rotate slower by reducing the percentage + const pinSpeedFactor = 1 - (pinIndex / this.parent.pinCount) * 0.5; // 1.0 for pin 0, 0.5 for last pin + const adjustedPercentage = pinMovementPercentage * pinSpeedFactor; + const rotationAngle = (adjustedPercentage / 100) * pinMaxRotation; + + // Calculate the new tip position (hook should point at the current pin) + const totalHookHeight = (config.diagonalSegments + config.verticalSegments) * config.segmentStep; + const newTipX = pinX - totalHookHeight + 34; // Add 34px offset (24px + 10px further right) + + // Update hook position and rotation + this.parent.hookGroup.x = newTipX; + this.parent.hookGroup.y = currentTargetY; + this.parent.hookGroup.setAngle(-rotationAngle); // Negative for anti-clockwise rotation + + // Check for collisions with other pins using hook's current position + this.checkHookCollisions(pinIndex, this.parent.hookGroup.y); + + 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)); + } + + checkHookCollisions(targetPinIndex, hookCurrentY) { + if (!this.parent.hookConfig || !this.parent.gameState.mouseDown) return; + + // Clear previous debug graphics + if (this.parent.debugGraphics) { + this.parent.debugGraphics.clear(); + } else { + this.parent.debugGraphics = this.parent.scene.add.graphics(); + this.parent.debugGraphics.setDepth(100); // Render on top + } + + // Create a temporary rectangle for the hook's horizontal arm using Phaser's physics + const hookArmWidth = 8; + const hookArmLength = 100; + + // Calculate the horizontal arm position relative to the hook's current position + // The horizontal arm extends from the handle to the curve start + const handleStartX = -120; // Handle starts at -120 + const handleWidth = 20; + const armStartX = handleStartX + handleWidth; // Arm starts after handle (-100) + const armEndX = armStartX + hookArmLength; // Arm ends at +40 + + // Position the collision box lower along the arm (not at the tip) + const collisionOffsetY = 35; // Move collision box down by 2350px + + // Convert to world coordinates with rotation + const hookAngle = this.parent.hookGroup.angle * (Math.PI / 180); // Convert degrees to radians + const cosAngle = Math.cos(hookAngle); + const sinAngle = Math.sin(hookAngle); + + // Calculate rotated arm start and end points + const armStartX_rotated = armStartX * cosAngle - collisionOffsetY * sinAngle; + const armStartY_rotated = armStartX * sinAngle + collisionOffsetY * cosAngle; + const armEndX_rotated = armEndX * cosAngle - collisionOffsetY * sinAngle; + const armEndY_rotated = armEndX * sinAngle + collisionOffsetY * cosAngle; + + // Convert to world coordinates + const worldArmStartX = armStartX_rotated + this.parent.hookGroup.x; + const worldArmStartY = armStartY_rotated + this.parent.hookGroup.y; + const worldArmEndX = armEndX_rotated + this.parent.hookGroup.x; + const worldArmEndY = armEndY_rotated + this.parent.hookGroup.y; + + // Create a line for the rotated arm (this is what we'll use for collision detection) + const hookArmLine = new Phaser.Geom.Line(worldArmStartX, worldArmStartY, worldArmEndX, worldArmEndY); + + // // Render hook arm hitbox (red) - draw as a line to show rotation + // this.debugGraphics.lineStyle(3, 0xff0000); + // this.debugGraphics.beginPath(); + // this.debugGraphics.moveTo(worldArmStartX, worldArmStartY); + // this.debugGraphics.lineTo(worldArmEndX, worldArmEndY); + // this.debugGraphics.strokePath(); + + // // Also render a rectangle around the collision area for debugging + // this.debugGraphics.lineStyle(1, 0xff0000); + // this.debugGraphics.strokeRect( + // Math.min(worldArmStartX, worldArmEndX), + // Math.min(worldArmStartY, worldArmEndY), + // Math.abs(worldArmEndX - worldArmStartX), + // Math.abs(worldArmEndY - worldArmStartY) + hookArmWidth + // ); + + // Check each pin for collision using Phaser's geometry + this.parent.pins.forEach((pin, pinIndex) => { + if (pinIndex === targetPinIndex) return; // Skip the target pin + + // Calculate pin position + const pinSpacing = 400 / (this.parent.pinCount + 1); + const margin = pinSpacing * 0.75; + const pinX = 100 + margin + pinIndex * pinSpacing; + const pinWorldY = 200; + + // Calculate pin's current position (including any existing movement) + // Add safety check for undefined properties + if (!pin.driverPinLength || !pin.keyPinLength) { + console.warn(`Pin ${pinIndex} missing length properties in checkHookCollisions:`, pin); + return; // Skip this pin if properties are missing + } + const pinCurrentY = pinWorldY - 50 + pin.driverPinLength + pin.keyPinLength - pin.currentHeight; + const keyPinTop = pinCurrentY - pin.keyPinLength; + const keyPinBottom = pinCurrentY; + + // Create a rectangle for the key pin + const keyPinRect = new Phaser.Geom.Rectangle(pinX - 12, keyPinTop, 24, pin.keyPinLength); + + // // Render pin hitbox (blue) + // this.debugGraphics.lineStyle(2, 0x0000ff); + // this.debugGraphics.strokeRect(pinX - 12, keyPinTop, 24, pin.keyPinLength); + + // Use Phaser's built-in line-to-rectangle intersection + if (Phaser.Geom.Intersects.LineToRectangle(hookArmLine, keyPinRect)) { + // Collision detected - lift this pin + this.liftCollidedPin(pin, pinIndex); + + // // Render collision (green) + // this.debugGraphics.lineStyle(3, 0x00ff00); + // this.debugGraphics.strokeRect(pinX - 12, keyPinTop, 24, pin.keyPinLength); + } + }); + } + + liftCollidedPin(pin, pinIndex) { + // Only lift if the pin isn't already being actively moved + if (this.parent.lockState.currentPin && this.parent.lockState.currentPin.index === pinIndex) return; + + // Calculate pin-specific maximum height + const baseMaxHeight = 75; + const maxHeightReduction = 15; + const pinHeightFactor = pinIndex / (this.parent.pinCount - 1); + const pinMaxHeight = baseMaxHeight - (maxHeightReduction * pinHeightFactor); + + // Lift the pin faster for collision (more responsive) + const collisionLiftSpeed = this.parent.liftSpeed * 0.8; // 80% of normal lift speed (increased from 30%) + pin.currentHeight = Math.min(pin.currentHeight + collisionLiftSpeed, pinMaxHeight * 0.5); // Max 50% of pin's max height + + // Update pin visuals + this.parent.pinVisuals.updatePinVisuals(pin); + + console.log(`Hook collision: Lifting pin ${pinIndex} to height ${pin.currentHeight}`); + } + +} diff --git a/js/minigames/lockpicking/key-animation.js b/js/minigames/lockpicking/key-animation.js index 4df3a1a..3340e72 100644 --- a/js/minigames/lockpicking/key-animation.js +++ b/js/minigames/lockpicking/key-animation.js @@ -79,7 +79,7 @@ export class KeyAnimation { pin.currentHeight = Math.max(0, exactLift); // Update pin visuals immediately - this.parent.updatePinVisuals(pin); + this.parent.pinVisuals.updatePinVisuals(pin); console.log(`Pin ${index}: cutDepth=${cutDepth}, cutSurfaceY=${cutSurfaceY}, exactLift=${exactLift}, currentHeight=${pin.currentHeight}, keyBladeBaseY=${keyBladeBaseY}, bladeHeight=${bladeHeight}`); }); @@ -104,7 +104,7 @@ export class KeyAnimation { } } - this.parent.updateFeedback("Key inserted successfully! Lock turning..."); + this.parent.keyInsertion.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) @@ -134,7 +134,7 @@ export class KeyAnimation { // 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); + this.parent.keyDraw.drawKeyBladeAsSolidShape(upperEdgeGraphics, bladeX, bladeY, this.parent.keyConfig.bladeWidth, this.parent.keyConfig.bladeHeight); upperEdgeRenderTexture.draw(upperEdgeGraphics); upperEdgeGraphics.destroy(); @@ -339,7 +339,7 @@ export class KeyAnimation { ease: 'Cubic.easeOut', onUpdate: (tween) => { pin.currentHeight = tween.targets[0].height; - this.parent.updatePinVisuals(pin); + this.parent.pinVisuals.updatePinVisuals(pin); } }); }); @@ -366,7 +366,7 @@ export class KeyAnimation { } } - this.parent.updateFeedback("Lock picked successfully!"); + this.parent.keyInsertion.updateFeedback("Lock picked successfully!"); // Shrink key pins downward and add half circles to simulate cylinder rotation this.parent.pins.forEach(pin => { diff --git a/js/minigames/lockpicking/key-drawing.js b/js/minigames/lockpicking/key-drawing.js new file mode 100644 index 0000000..597d887 --- /dev/null +++ b/js/minigames/lockpicking/key-drawing.js @@ -0,0 +1,209 @@ + +/** + * KeyDrawing + * + * Extracted from lockpicking-game-phaser.js + * Instantiate with: new KeyDrawing(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 KeyDrawing { + + constructor(parent) { + this.parent = parent; + } + + drawKeyWithRenderTexture(circleRadius, shoulderWidth, shoulderHeight, bladeWidth, bladeHeight, fullKeyLength) { + console.log('drawKeyWithRenderTexture called with:', { + hasKeyData: !!this.parent.keyData, + hasCuts: !!(this.parent.keyData && this.parent.keyData.cuts), + keyData: this.parent.keyData + }); + + if (!this.parent.keyData || !this.parent.keyData.cuts) { + console.log('Early return - missing key data or cuts'); + return; + } + + // Create temporary graphics for drawing to render texture + const tempGraphics = this.parent.scene.add.graphics(); + tempGraphics.fillStyle(0xcccccc); // Silver color for key + + // Calculate positions + const circleX = circleRadius; // Circle center + const shoulderX = circleRadius * 1.9; // After circle + const bladeX = shoulderX + shoulderWidth; // After shoulder + + console.log('Drawing key handle:', { + circleX: circleX, + circleY: shoulderHeight/2, + circleRadius: circleRadius, + shoulderHeight: shoulderHeight, + renderTextureWidth: this.parent.keyRenderTexture.width + }); + + // 1. Draw the circle (handle) - rightmost part as a separate object + const handleGraphics = this.parent.scene.add.graphics(); + handleGraphics.fillStyle(0xcccccc); // Silver color for key + handleGraphics.fillCircle(circleX, 0, circleRadius); // Center at y=0 relative to key group + + // Add handle to the key group + this.parent.keyGroup.add(handleGraphics); + + // 2. Draw the shoulder - rectangle + tempGraphics.fillRect(shoulderX, 0, shoulderWidth, shoulderHeight); + + // 3. Draw the blade with cuts as a solid shape + this.drawKeyBladeAsSolidShape(tempGraphics, bladeX, shoulderHeight/2 - bladeHeight/2, bladeWidth, bladeHeight); + + // Draw the graphics to the render texture (shoulder and blade only) + this.parent.keyRenderTexture.draw(tempGraphics); + + // Clean up temporary graphics + tempGraphics.destroy(); + } + + drawKeyBladeAsSolidShape(graphics, bladeX, bladeY, bladeWidth, bladeHeight) { + // Draw the key blade as a solid shape with cuts removed + // The blade has a pattern like: \_/\_/\_/\_/\ where the cuts _ are based on pin depths + + // ASCII art of the key blade: + // _________ + // / \ ____ + // | | | \_/\_/\_/\_/\ + // | |_|______________/ + // \________/ + + + + const cutWidth = 24; // Width of each cut (same as pin width) + + // Calculate pin spacing to match the lock's pin positions + const pinSpacing = 400 / (this.parent.pinCount + 1); + const margin = pinSpacing * 0.75; + + // Start with the base blade rectangle + const baseBladeRect = { + x: bladeX, + y: bladeY, + width: bladeWidth, + height: bladeHeight + }; + + // Create a path for the solid key blade + const path = new Phaser.Geom.Polygon(); + + // Start at the top-left corner of the blade + path.points.push(new Phaser.Geom.Point(bladeX, bladeY)); + + // Draw the top edge with cuts and ridges + let currentX = bladeX; + + // For each pin position, create the blade profile + for (let i = 0; i <= this.parent.pinCount; i++) { + let cutDepth = 0; + let nextCutDepth = 0; + + if (i < this.parent.pinCount) { + cutDepth = this.parent.keyData.cuts[i] || 0; + } + if (i < this.parent.pinCount - 1) { + nextCutDepth = this.parent.keyData.cuts[i + 1] || 0; + } + + // Calculate pin position + const pinX = 100 + margin + i * pinSpacing; + const cutX = bladeX + (pinX - 100); + + if (i === 0) { + // First section: from left edge (shoulder) to first cut + const firstCutStartX = cutX - cutWidth/2; + + // Draw triangular peak from shoulder to first cut edge (touches exact edge of cut) + this.parent.keyPathDraw.addFirstCutPeakToPath(path, currentX, bladeY, firstCutStartX, bladeY, 0, cutDepth); + currentX = firstCutStartX; + } + + if (i < this.parent.pinCount) { + // Draw the cut (negative space - skip this section) + const cutStartX = cutX - cutWidth/2; + const cutEndX = cutX + cutWidth/2; + + // Move to the bottom of the cut + path.points.push(new Phaser.Geom.Point(cutStartX, bladeY + cutDepth)); + + // Draw the cut bottom + path.points.push(new Phaser.Geom.Point(cutEndX, bladeY + cutDepth)); + + currentX = cutEndX; + } + + if (i < this.parent.pinCount - 1) { + // Draw triangular peak to next cut + const nextPinX = 100 + margin + (i + 1) * pinSpacing; + const nextCutX = bladeX + (nextPinX - 100); + const nextCutStartX = nextCutX - cutWidth/2; + + // Use triangular peak that goes up at 45 degrees to halfway, then down at 45 degrees + this.parent.keyPathDraw.addTriangularPeakToPath(path, currentX, bladeY, nextCutStartX, bladeY, cutDepth, nextCutDepth); + currentX = nextCutStartX; + } else if (i === this.parent.pinCount - 1) { + // Last section: from last cut to right edge - create pointed tip that extends forward + const keyRightEdge = bladeX + bladeWidth; + const tipExtension = 12; // How far the tip extends beyond the blade + const tipEndX = keyRightEdge + tipExtension; + + // First: draw triangular peak from last cut back up to blade top + const peakX = currentX + (keyRightEdge - currentX) * 0.3; // Peak at 30% of the way + this.parent.keyPathDraw.addTriangularPeakToPath(path, currentX, bladeY, peakX, bladeY, cutDepth, 0); + + // Second: draw the pointed tip that extends forward from top and bottom + this.parent.keyPathDraw.addPointedTipToPath(path, peakX, bladeY, tipEndX, bladeHeight); + currentX = tipEndX; + } + } + + // Complete the path: right edge, bottom edge, left edge + path.points.push(new Phaser.Geom.Point(bladeX + bladeWidth, bladeY + bladeHeight)); + path.points.push(new Phaser.Geom.Point(bladeX, bladeY + bladeHeight)); + path.points.push(new Phaser.Geom.Point(bladeX, bladeY)); + + // Draw the solid shape + graphics.fillPoints(path.points, true, true); + } + + drawPixelArtCircleToGraphics(graphics, centerX, centerY, radius) { + // Draw a pixel art circle to the specified graphics object + const stepSize = 4; // Consistent pixel size for steps + const diameter = radius * 2; + const steps = Math.floor(diameter / stepSize); + + // Draw horizontal lines to create the circle shape + for (let i = 0; i <= steps; i++) { + const y = centerY - radius + (i * stepSize); + const distanceFromCenter = Math.abs(y - centerY); + + // Calculate the width of this horizontal line using circle equation + // For a circle: x² + y² = r², so x = √(r² - y²) + const halfWidth = Math.sqrt(radius * radius - distanceFromCenter * distanceFromCenter); + + if (halfWidth > 0) { + // Draw the horizontal line for this row + const lineWidth = halfWidth * 2; + const lineX = centerX - halfWidth; + + // Round to stepSize for pixel art consistency + const roundedWidth = Math.floor(lineWidth / stepSize) * stepSize; + const roundedX = Math.floor(lineX / stepSize) * stepSize; + + graphics.fillRect(roundedX, y, roundedWidth, stepSize); + } + } + } + +} diff --git a/js/minigames/lockpicking/key-geometry.js b/js/minigames/lockpicking/key-geometry.js new file mode 100644 index 0000000..5431d33 --- /dev/null +++ b/js/minigames/lockpicking/key-geometry.js @@ -0,0 +1,152 @@ + +/** + * KeyGeometry + * + * Extracted from lockpicking-game-phaser.js + * Instantiate with: new KeyGeometry(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 KeyGeometry { + + constructor(parent) { + this.parent = parent; + } + + getKeySurfaceHeightAtPinPosition(pinX, keyBladeStartX, keyBladeBaseY) { + // Use collision detection to find the key surface height at a specific pin position + // This method traces a vertical line from the pin position down to find where it intersects the key polygon + + const bladeWidth = this.parent.keyConfig.bladeWidth; + const bladeHeight = this.parent.keyConfig.bladeHeight; + + // Calculate the pin's position relative to the key blade + const pinRelativeToKey = pinX - keyBladeStartX; + + // If pin is beyond the key blade, return base surface + if (pinRelativeToKey < 0 || pinRelativeToKey > bladeWidth) { + return keyBladeBaseY; + } + + // Generate the key polygon points at the current position + const keyPolygonPoints = this.generateKeyPolygonPoints(keyBladeStartX, keyBladeBaseY); + + // Find the intersection point by tracing a vertical line from the pin position + const intersectionY = this.findVerticalIntersection(pinX, keyBladeBaseY, keyBladeBaseY + bladeHeight, keyPolygonPoints); + + return intersectionY !== null ? intersectionY : keyBladeBaseY; + } + + generateKeyPolygonPoints(keyBladeStartX, keyBladeBaseY) { + // Generate the key polygon points at the current position + // This recreates the same polygon logic used in drawKeyBladeAsSolidShape + const points = []; + const bladeWidth = this.parent.keyConfig.bladeWidth; + const bladeHeight = this.parent.keyConfig.bladeHeight; + const cutWidth = 24; + + // Calculate pin spacing + const pinSpacing = 400 / (this.parent.pinCount + 1); + const margin = pinSpacing * 0.75; + + // Start at the top-left corner of the blade + points.push({ x: keyBladeStartX, y: keyBladeBaseY }); + + let currentX = keyBladeStartX; + + // Generate the same path as the drawing method + for (let i = 0; i <= this.parent.pinCount; i++) { + let cutDepth = 0; + let nextCutDepth = 0; + + if (i < this.parent.pinCount) { + cutDepth = (this.parent.selectedKeyData || this.parent.keyData).cuts[i] || 0; + } + if (i < this.parent.pinCount - 1) { + nextCutDepth = (this.parent.selectedKeyData || this.parent.keyData).cuts[i + 1] || 0; + } + + // Calculate pin position + const pinX = 100 + margin + i * pinSpacing; + const cutX = keyBladeStartX + (pinX - 100); + + if (i === 0) { + // First section: from left edge to first cut + const firstCutStartX = cutX - cutWidth/2; + this.parent.keyPointGeom.addTriangularPeakToPoints(points, currentX, keyBladeBaseY, firstCutStartX, keyBladeBaseY, 0, cutDepth); + currentX = firstCutStartX; + } + + if (i < this.parent.pinCount) { + // Draw the cut + const cutStartX = cutX - cutWidth/2; + const cutEndX = cutX + cutWidth/2; + points.push({ x: cutStartX, y: keyBladeBaseY + cutDepth }); + points.push({ x: cutEndX, y: keyBladeBaseY + cutDepth }); + currentX = cutEndX; + } + + if (i < this.parent.pinCount - 1) { + // Draw triangular peak to next cut + const nextPinX = 100 + margin + (i + 1) * pinSpacing; + const nextCutX = keyBladeStartX + (nextPinX - 100); + const nextCutStartX = nextCutX - cutWidth/2; + this.parent.keyPointGeom.addTriangularPeakToPoints(points, currentX, keyBladeBaseY, nextCutStartX, keyBladeBaseY, cutDepth, nextCutDepth); + currentX = nextCutStartX; + } else if (i === this.parent.pinCount - 1) { + // Last section: pointed tip + const keyRightEdge = keyBladeStartX + bladeWidth; + const tipExtension = 12; + const tipEndX = keyRightEdge + tipExtension; + const peakX = currentX + (keyRightEdge - currentX) * 0.3; + this.parent.keyPointGeom.addTriangularPeakToPoints(points, currentX, keyBladeBaseY, peakX, keyBladeBaseY, cutDepth, 0); + this.parent.keyPointGeom.addPointedTipToPoints(points, peakX, keyBladeBaseY, tipEndX, bladeHeight); + currentX = tipEndX; + } + } + + // Complete the path + points.push({ x: keyBladeStartX + bladeWidth, y: keyBladeBaseY + bladeHeight }); + points.push({ x: keyBladeStartX, y: keyBladeBaseY + bladeHeight }); + points.push({ x: keyBladeStartX, y: keyBladeBaseY }); + + return points; + } + + findVerticalIntersection(pinX, startY, endY, polygonPoints) { + // Find where a vertical line at pinX intersects the polygon + // Returns the Y coordinate of the intersection, or null if no intersection + + let intersectionY = null; + + for (let i = 0; i < polygonPoints.length - 1; i++) { + const p1 = polygonPoints[i]; + const p2 = polygonPoints[i + 1]; + + // Check if this line segment crosses the vertical line at pinX + if ((p1.x <= pinX && p2.x >= pinX) || (p1.x >= pinX && p2.x <= pinX)) { + // Calculate intersection + const t = (pinX - p1.x) / (p2.x - p1.x); + const y = p1.y + t * (p2.y - p1.y); + + // Keep the highest intersection point (closest to the pin) + if (intersectionY === null || y < intersectionY) { + intersectionY = y; + } + } + } + + return intersectionY; + } + + getKeySurfaceHeightAtPosition(pinX, keyBladeStartX) { + // Method moved to KeyOperations module - delegate to it + return this.parent.keyOps.getKeySurfaceHeightAtPosition(pinX, keyBladeStartX); + } + +} diff --git a/js/minigames/lockpicking/key-insertion.js b/js/minigames/lockpicking/key-insertion.js new file mode 100644 index 0000000..8052d0b --- /dev/null +++ b/js/minigames/lockpicking/key-insertion.js @@ -0,0 +1,107 @@ + +/** + * KeyInsertion + * + * Extracted from lockpicking-game-phaser.js + * Instantiate with: new KeyInsertion(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 KeyInsertion { + + constructor(parent) { + this.parent = parent; + } + + updateKeyPosition(progress) { + if (!this.parent.keyGroup || !this.parent.keyConfig) return; + + // Calculate new position based on insertion progress + // Key moves from left (off-screen) to right (shoulder touches lock edge) + const targetX = this.parent.keyConfig.keywayStartX - this.parent.keyConfig.shoulderWidth; // Shoulder touches lock edge + const currentX = this.parent.keyConfig.startX + (targetX - this.parent.keyConfig.startX) * progress; + + this.parent.keyGroup.x = currentX; + this.parent.keyInsertionProgress = progress; + + // If fully inserted, check if key is correct + if (progress >= 1.0) { + this.parent.keyOps.checkKeyCorrectness(); + } + } + + updatePinsWithKeyInsertion(progress) { + if (!this.parent.keyConfig) return; + + // Calculate key blade position relative to the lock + const keyBladeStartX = this.parent.keyGroup.x + this.parent.keyConfig.circleRadius * 2 + this.parent.keyConfig.shoulderWidth; + const keyBladeEndX = keyBladeStartX + this.parent.keyConfig.bladeWidth; + + // Key blade base position in world coordinates + const keyBladeBaseY = this.parent.keyGroup.y - this.parent.keyConfig.bladeHeight / 2; + + // Shear line for highlighting + const shearLineY = -45; // Same as lockpicking mode + const tolerance = 10; + + // Check each pin for collision with the key blade + this.parent.pins.forEach((pin, index) => { + if (index >= this.parent.pinCount) return; + + // Calculate pin position in the lock + const pinSpacing = 400 / (this.parent.pinCount + 1); + const margin = pinSpacing * 0.75; + const pinX = 100 + margin + index * pinSpacing; + + // Check if this pin is under the key blade + const pinIsUnderKeyBlade = pinX >= keyBladeStartX && pinX <= keyBladeEndX; + + if (pinIsUnderKeyBlade) { + // Use collision detection to find the key surface height at this pin's position + const keySurfaceY = this.parent.keyGeom.getKeySurfaceHeightAtPinPosition(pinX, keyBladeStartX, keyBladeBaseY); + + // Calculate where the key pin bottom should be to sit on the key surface + const pinRestY = 200 - 50 + pin.driverPinLength + pin.keyPinLength; + const targetKeyPinBottom = keySurfaceY; + + // Calculate required lift to move key pin bottom from rest to key surface + const requiredLift = pinRestY - targetKeyPinBottom; + const targetLift = Math.max(0, requiredLift); + + // Smooth movement toward target + if (pin.currentHeight < targetLift) { + pin.currentHeight = Math.min(targetLift, pin.currentHeight + 2); + } else if (pin.currentHeight > targetLift) { + pin.currentHeight = Math.max(targetLift, pin.currentHeight - 1); + } + } else { + // Pin is not under key blade - keep current position (don't drop back down) + // This ensures pins stay lifted once they've been pushed up by the key + } + + // Check if pin is near shear line for highlighting + // Use the same boundary calculation as lockpicking mode + const boundaryPosition = -50 + pin.driverPinLength - pin.currentHeight; + const distanceToShearLine = Math.abs(boundaryPosition - shearLineY); + + // Debug: Log boundary positions for highlighting + console.log(`Pin ${index} highlighting: boundaryPosition=${boundaryPosition}, distanceToShearLine=${distanceToShearLine}, tolerance=${tolerance}, shouldHighlight=${distanceToShearLine <= tolerance}, hasShearHighlight=${!!pin.shearHighlight}, hasSetHighlight=${!!pin.setHighlight}`); + + // Update pin highlighting based on shear line proximity + this.parent.pinVisuals.updatePinHighlighting(pin, distanceToShearLine, tolerance); + + // Update pin visuals + this.parent.pinVisuals.updatePinVisuals(pin); + }); + } + + updateFeedback(message) { + this.parent.feedback.textContent = message; + } + +} diff --git a/js/minigames/lockpicking/key-operations.js b/js/minigames/lockpicking/key-operations.js index 2827234..cbfa1c5 100644 --- a/js/minigames/lockpicking/key-operations.js +++ b/js/minigames/lockpicking/key-operations.js @@ -55,7 +55,7 @@ export class KeyOperations { this.parent.keyRenderTexture.setOrigin(0, 0.5); // Draw the key using render texture - this.parent.drawKeyWithRenderTexture(keyCircleRadius, keyShoulderWidth, keyShoulderHeight, keyBladeWidth, keyBladeHeight, fullKeyLength); + this.parent.keyDraw.drawKeyWithRenderTexture(keyCircleRadius, keyShoulderWidth, keyShoulderHeight, keyBladeWidth, keyBladeHeight, fullKeyLength); // Test: Draw a simple circle to see if render texture works const testGraphics = this.parent.scene.add.graphics(); @@ -96,7 +96,7 @@ export class KeyOperations { if (!this.parent.pinClicked) { this.parent.pinClicked = true; } - this.parent.startKeyInsertion(); + this.startKeyInsertion(); } }); @@ -141,7 +141,7 @@ export class KeyOperations { console.log('Starting key insertion animation...'); this.parent.keyInserting = true; - this.parent.updateFeedback("Inserting key..."); + this.parent.keyInsertion.updateFeedback("Inserting key..."); // Calculate target position - key should be fully inserted const targetX = this.parent.keyConfig.keywayStartX - this.parent.keyConfig.shoulderWidth; @@ -166,7 +166,7 @@ export class KeyOperations { console.log('Animation update - key position:', this.parent.keyGroup.x, 'progress:', this.parent.keyInsertionProgress); // Update pin positions based on key cuts as the key is inserted - this.parent.updatePinsWithKeyInsertion(this.parent.keyInsertionProgress); + this.parent.keyInsertion.updatePinsWithKeyInsertion(this.parent.keyInsertionProgress); }, onComplete: () => { this.parent.keyInserting = false; @@ -210,7 +210,7 @@ export class KeyOperations { if (isCorrect) { // Key is correct - all pins are aligned at the shear line - this.parent.updateFeedback("Key fits perfectly! Lock unlocked."); + this.parent.keyInsertion.updateFeedback("Key fits perfectly! Lock unlocked."); // Start the rotation animation for correct key this.parent.scene.time.delayedCall(500, () => { @@ -223,7 +223,7 @@ export class KeyOperations { }, 3000); // Longer delay to allow rotation animation to complete } else { // Key is wrong - show red flash and then pop up key selection again - this.parent.updateFeedback("Wrong key! The lock won't turn."); + this.parent.keyInsertion.updateFeedback("Wrong key! The lock won't turn."); // Play wrong sound if (this.parent.sounds.wrong) { @@ -235,7 +235,7 @@ export class KeyOperations { // Reset key position and show key selection again after a delay setTimeout(() => { - this.parent.updateKeyPosition(0); + this.parent.keyInsertion.updateKeyPosition(0); // Show key selection again if (this.parent.keySelectionMode) { // For main game, go back to original key selection interface @@ -245,7 +245,7 @@ export class KeyOperations { this.parent.keySelection.createKeysForChallenge('correct_key'); } else { // This is the main game - go back to key selection - this.parent.startWithKeySelection(); + this.startWithKeySelection(); } } }, 2000); // Longer delay to show the red flash @@ -340,7 +340,7 @@ export class KeyOperations { this.parent.keyData = originalKeyData; // Update feedback - don't reveal if correct/wrong yet - this.parent.updateFeedback("Key selected! Inserting into lock..."); + this.parent.keyInsertion.updateFeedback("Key selected! Inserting into lock..."); // Automatically trigger key insertion after a short delay setTimeout(() => { diff --git a/js/minigames/lockpicking/key-path-drawing.js b/js/minigames/lockpicking/key-path-drawing.js new file mode 100644 index 0000000..649a610 --- /dev/null +++ b/js/minigames/lockpicking/key-path-drawing.js @@ -0,0 +1,192 @@ + +/** + * KeyPathDrawing + * + * Extracted from lockpicking-game-phaser.js + * Instantiate with: new KeyPathDrawing(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 KeyPathDrawing { + + constructor(parent) { + this.parent = parent; + } + + addTriangularSectionToPath(path, startX, startY, endX, endY, cutDepth, isLeftTriangle) { + // Add a triangular section to the path + // This creates the sloping effect between cuts + + const width = Math.abs(endX - startX); + const stepSize = 4; // Consistent pixel size for steps + const steps = Math.max(1, Math.floor(width / stepSize)); + + for (let i = 0; i <= steps; i++) { + const progress = i / steps; + const x = startX + (endX - startX) * progress; + + let y; + if (isLeftTriangle) { + // Left triangle: height increases as we move toward the cut + y = startY + (cutDepth * progress); + } else { + // Right triangle: height decreases as we move away from the cut + y = startY + (cutDepth * (1 - progress)); + } + + path.points.push(new Phaser.Geom.Point(x, y)); + } + } + + addFirstCutPeakToPath(path, startX, startY, endX, endY, startCutDepth, endCutDepth) { + // Add a triangular peak from shoulder to first cut that touches the exact edge of the cut + // This ensures proper alignment without affecting other peaks + + const width = Math.abs(endX - startX); + const stepSize = 4; // Consistent pixel size for steps + const steps = Math.max(1, Math.floor(width / stepSize)); + const halfSteps = Math.floor(steps / 2); + + for (let i = 0; i <= steps; i++) { + const progress = i / steps; + const x = startX + (endX - startX) * progress; + + let y; + if (i <= halfSteps) { + // First half: slope up from start cut depth to peak (blade top) + const upProgress = i / halfSteps; + y = startY + startCutDepth - (startCutDepth * upProgress); // Slope up to blade top + } else { + // Second half: slope down from peak to end cut depth + const downProgress = (i - halfSteps) / halfSteps; + y = startY + (endCutDepth * downProgress); // Slope down from blade top + } + + // Ensure the final point connects to the exact cut edge coordinates + if (i === steps) { + // Connect directly to the cut edge at the calculated depth + y = startY + endCutDepth; + } + + path.points.push(new Phaser.Geom.Point(x, y)); + } + } + + addTriangularPeakToPath(path, startX, startY, endX, endY, startCutDepth, endCutDepth) { + // Add a triangular peak between cuts that goes up at 45 degrees to halfway, then down at 45 degrees + // This creates a more realistic key blade profile with proper peaks between cuts + + const width = Math.abs(endX - startX); + const stepSize = 4; // Consistent pixel size for steps + const steps = Math.max(1, Math.floor(width / stepSize)); + const halfSteps = Math.floor(steps / 2); + + // Calculate the peak height - should be at the blade top (0 depth) at the halfway point + const maxPeakHeight = Math.max(startCutDepth, endCutDepth); // Use the deeper cut as reference + + for (let i = 0; i <= steps; i++) { + const progress = i / steps; + const x = startX + (endX - startX) * progress; + + let y; + if (i <= halfSteps) { + // First half: slope up from start cut depth to peak (blade top) + const upProgress = i / halfSteps; + y = startY + startCutDepth - (startCutDepth * upProgress); // Slope up to blade top + } else { + // Second half: slope down from peak to end cut depth + const downProgress = (i - halfSteps) / halfSteps; + y = startY + (endCutDepth * downProgress); // Slope down from blade top + } + + path.points.push(new Phaser.Geom.Point(x, y)); + } + } + + addPointedTipToPath(path, startX, startY, endX, bladeHeight) { + // Add a pointed tip that extends forward from both top and bottom of the blade + // This creates the key tip as shown in the ASCII art: \_/\_/\_/\_/\_/ + + const width = Math.abs(endX - startX); + const stepSize = 4; // Consistent pixel size for steps + const steps = Math.max(1, Math.floor(width / stepSize)); + + // Calculate the bottom point (directly below the start point) + const bottomX = startX; + const bottomY = startY + bladeHeight; + + // Calculate the tip point (the rightmost point) + const tipX = endX; + const tipY = startY + (bladeHeight / 2); // Center of the blade height + + // Draw the pointed tip: from top to tip to bottom + // First, go from top (startY) to tip (rightmost point) + const topToTipSteps = Math.max(1, Math.floor(width / stepSize)); + for (let i = 0; i <= topToTipSteps; i++) { + const progress = i / topToTipSteps; + const x = startX + (width * progress); + const y = startY + (bladeHeight / 2 * progress); // Slope down from top to center + path.points.push(new Phaser.Geom.Point(x, y)); + } + + // Then, go from tip to bottom + const tipToBottomSteps = Math.max(1, Math.floor(width / stepSize)); + for (let i = 0; i <= tipToBottomSteps; i++) { + const progress = i / tipToBottomSteps; + const x = tipX - (width * progress); + const y = tipY + (bladeHeight / 2 * progress); // Slope down from center to bottom + path.points.push(new Phaser.Geom.Point(x, y)); + } + } + + addRightPointingTriangleToPath(path, peakX, peakY, endX, endY, bladeHeight) { + // Add a triangle that goes from peak down to bottom, with third point facing right |> + // This creates the right-pointing part of the tip + + const width = Math.abs(endX - peakX); + const stepSize = 4; // Consistent pixel size for steps + const steps = Math.max(1, Math.floor(width / stepSize)); + + // Calculate the bottom point (directly below the peak) + const bottomX = peakX; + const bottomY = peakY + bladeHeight; + + // Calculate the rightmost point (the tip pointing to the right) + const tipX = endX; + const tipY = peakY + (bladeHeight / 2); // Center of the blade height + + // Draw the triangle: from peak to bottom to tip + // First, go from peak to bottom + const peakToBottomSteps = Math.max(1, Math.floor(bladeHeight / stepSize)); + for (let i = 0; i <= peakToBottomSteps; i++) { + const progress = i / peakToBottomSteps; + const x = peakX; + const y = peakY + (bladeHeight * progress); + path.points.push(new Phaser.Geom.Point(x, y)); + } + + // Then, go from bottom to tip (rightmost point) + const bottomToTipSteps = Math.max(1, Math.floor(width / stepSize)); + for (let i = 0; i <= bottomToTipSteps; i++) { + const progress = i / bottomToTipSteps; + const x = bottomX + (width * progress); + const y = bottomY - (bladeHeight / 2 * progress); // Slope up from bottom to center + path.points.push(new Phaser.Geom.Point(x, y)); + } + + // Finally, go from tip back to peak + const tipToPeakSteps = Math.max(1, Math.floor(width / stepSize)); + for (let i = 0; i <= tipToPeakSteps; i++) { + const progress = i / tipToPeakSteps; + const x = tipX - (width * progress); + const y = tipY - (bladeHeight / 2 * progress); // Slope up from center to peak + path.points.push(new Phaser.Geom.Point(x, y)); + } + } + +} diff --git a/js/minigames/lockpicking/key-point-geometry.js b/js/minigames/lockpicking/key-point-geometry.js new file mode 100644 index 0000000..b266980 --- /dev/null +++ b/js/minigames/lockpicking/key-point-geometry.js @@ -0,0 +1,219 @@ + +/** + * KeyPointGeometry + * + * Extracted from lockpicking-game-phaser.js + * Instantiate with: new KeyPointGeometry(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 KeyPointGeometry { + + constructor(parent) { + this.parent = parent; + } + + addTriangularPeakToPoints(points, startX, startY, endX, endY, startCutDepth, endCutDepth) { + // Add triangular peak points (same logic as addTriangularPeakToPath) + const width = Math.abs(endX - startX); + const stepSize = 4; + const steps = Math.max(1, Math.floor(width / stepSize)); + const halfSteps = Math.floor(steps / 2); + + for (let i = 0; i <= steps; i++) { + const progress = i / steps; + const x = startX + (endX - startX) * progress; + + let y; + if (i <= halfSteps) { + const upProgress = i / halfSteps; + y = startY + startCutDepth - (startCutDepth * upProgress); + } else { + const downProgress = (i - halfSteps) / halfSteps; + y = startY + (endCutDepth * downProgress); + } + + points.push({ x: x, y: y }); + } + } + + addPointedTipToPoints(points, startX, startY, endX, bladeHeight) { + // Add pointed tip points (same logic as addPointedTipToPath) + const width = Math.abs(endX - startX); + const stepSize = 4; + const steps = Math.max(1, Math.floor(width / stepSize)); + + const tipX = endX; + const tipY = startY + (bladeHeight / 2); + + // From top to tip + const topToTipSteps = Math.max(1, Math.floor(width / stepSize)); + for (let i = 0; i <= topToTipSteps; i++) { + const progress = i / topToTipSteps; + const x = startX + (width * progress); + const y = startY + (bladeHeight / 2 * progress); + points.push({ x: x, y: y }); + } + + // From tip to bottom + const tipToBottomSteps = Math.max(1, Math.floor(width / stepSize)); + for (let i = 0; i <= tipToBottomSteps; i++) { + const progress = i / tipToBottomSteps; + const x = tipX - (width * progress); + const y = tipY + (bladeHeight / 2 * progress); + points.push({ x: x, y: y }); + } + } + + getTriangularSectionHeightAtX(relativeX, bladeWidth, bladeHeight) { + // Calculate height of triangular sections at a given X position + // Creates peaks that go up to blade top between cuts + const cutWidth = 24; + const pinSpacing = 400 / (this.parent.pinCount + 1); + const margin = pinSpacing * 0.75; + + // Check triangular sections between cuts + for (let i = 0; i < this.parent.pinCount - 1; i++) { + const cut1X = margin + i * pinSpacing; + const cut2X = margin + (i + 1) * pinSpacing; + const cut1EndX = cut1X + cutWidth/2; + const cut2StartX = cut2X - cutWidth/2; + + // Check if we're in the triangular section between these cuts + if (relativeX >= cut1EndX && relativeX <= cut2StartX) { + const distanceFromCut1 = relativeX - cut1EndX; + const triangularWidth = cut2StartX - cut1EndX; + const progress = distanceFromCut1 / triangularWidth; + + // Get cut depths for both cuts + const cut1Depth = this.parent.keyData.cuts[i] || 0; + const cut2Depth = this.parent.keyData.cuts[i + 1] || 0; + + // Create a peak: go up from cut1 to blade top, then down to cut2 + const halfWidth = triangularWidth / 2; + + if (distanceFromCut1 <= halfWidth) { + // First half: slope up from cut1 to blade top + const upProgress = distanceFromCut1 / halfWidth; + return cut1Depth + (bladeHeight - cut1Depth) * upProgress; + } else { + // Second half: slope down from blade top to cut2 + const downProgress = (distanceFromCut1 - halfWidth) / halfWidth; + return bladeHeight - (bladeHeight - cut2Depth) * downProgress; + } + } + } + + // Check triangular section from left edge to first cut + const firstCutX = margin; + const firstCutStartX = firstCutX - cutWidth/2; + + if (relativeX >= 0 && relativeX < firstCutStartX) { + const progress = relativeX / firstCutStartX; + const firstCutDepth = this.parent.keyData.cuts[0] || 0; + + // Create a peak: slope up from base to blade top, then down to first cut + const halfWidth = firstCutStartX / 2; + + if (relativeX <= halfWidth) { + // First half: slope up from base (0) to blade top + const upProgress = relativeX / halfWidth; + return bladeHeight * upProgress; + } else { + // Second half: slope down from blade top to first cut depth + const downProgress = (relativeX - halfWidth) / halfWidth; + return bladeHeight - (bladeHeight - firstCutDepth) * downProgress; + } + } + + // Check triangular section from last cut to right edge + const lastCutX = margin + (this.parent.pinCount - 1) * pinSpacing; + const lastCutEndX = lastCutX + cutWidth/2; + + if (relativeX > lastCutEndX && relativeX <= bladeWidth) { + const triangularWidth = bladeWidth - lastCutEndX; + const distanceFromLastCut = relativeX - lastCutEndX; + const progress = distanceFromLastCut / triangularWidth; + const lastCutDepth = this.parent.keyData.cuts[this.parent.pinCount - 1] || 0; + + // Create a peak: slope up from last cut to blade top, then down to base + const halfWidth = triangularWidth / 2; + + if (distanceFromLastCut <= halfWidth) { + // First half: slope up from last cut depth to blade top + const upProgress = distanceFromLastCut / halfWidth; + return lastCutDepth + (bladeHeight - lastCutDepth) * upProgress; + } else { + // Second half: slope down from blade top to base (0) + const downProgress = (distanceFromLastCut - halfWidth) / halfWidth; + return bladeHeight * (1 - downProgress); + } + } + + return 0; // Not in a triangular section + } + + getTriangularSectionHeightAsKeyMoves(pinRelativeToKeyLeadingEdge, bladeWidth, bladeHeight) { + // Calculate triangular section height as the key moves underneath the pin + // This creates the sloping effect as pins follow the key's surface + + const cutWidth = 24; + const pinSpacing = 400 / (this.parent.pinCount + 1); + const margin = pinSpacing * 0.75; + + // Check triangular section from left edge to first cut + const firstCutX = margin; + const firstCutStartX = firstCutX - cutWidth/2; + + if (pinRelativeToKeyLeadingEdge >= 0 && pinRelativeToKeyLeadingEdge < firstCutStartX) { + // Pin is in the triangular section from left edge to first cut + const progress = pinRelativeToKeyLeadingEdge / firstCutStartX; + const firstCutDepth = this.parent.keyData.cuts[0] || 0; + // Start from base level (0) and slope up to first cut depth + return Math.max(0, firstCutDepth * progress); // Ensure we never go below base level + } + + // Check triangular sections between cuts + for (let i = 0; i < this.parent.pinCount - 1; i++) { + const cut1X = margin + i * pinSpacing; + const cut2X = margin + (i + 1) * pinSpacing; + const cut1EndX = cut1X + cutWidth/2; + const cut2StartX = cut2X - cutWidth/2; + + if (pinRelativeToKeyLeadingEdge >= cut1EndX && pinRelativeToKeyLeadingEdge <= cut2StartX) { + // Pin is in triangular section between these cuts + const distanceFromCut1 = pinRelativeToKeyLeadingEdge - cut1EndX; + const triangularWidth = cut2StartX - cut1EndX; + const progress = distanceFromCut1 / triangularWidth; + + // Get cut depths for both cuts + const cut1Depth = this.parent.keyData.cuts[i] || 0; + const cut2Depth = this.parent.keyData.cuts[i + 1] || 0; + + // Interpolate between cut depths (slope from cut1 to cut2) + return cut1Depth + (cut2Depth - cut1Depth) * progress; + } + } + + // Check triangular section from last cut to right edge + const lastCutX = margin + (this.parent.pinCount - 1) * pinSpacing; + const lastCutEndX = lastCutX + cutWidth/2; + + if (pinRelativeToKeyLeadingEdge >= lastCutEndX && pinRelativeToKeyLeadingEdge <= bladeWidth) { + // Pin is in triangular section from last cut to right edge + const distanceFromLastCut = pinRelativeToKeyLeadingEdge - lastCutEndX; + const triangularWidth = bladeWidth - lastCutEndX; + const progress = distanceFromLastCut / triangularWidth; + const lastCutDepth = this.parent.keyData.cuts[this.parent.pinCount - 1] || 0; + return lastCutDepth * (1 - progress); // Slope down from last cut depth to 0 + } + + return 0; // Not in a triangular section + } + +} diff --git a/js/minigames/lockpicking/key-selection.js b/js/minigames/lockpicking/key-selection.js index 3fb37b0..89890e4 100644 --- a/js/minigames/lockpicking/key-selection.js +++ b/js/minigames/lockpicking/key-selection.js @@ -76,14 +76,14 @@ export class KeySelection { // Randomize the order const keys = [key1, key2, key3]; - this.parent.shuffleArray(keys); + this.parent.gameUtil.shuffleArray(keys); return this.parent.createKeySelectionUI(keys, correctKeyId); } // Use inventory keys and randomize their order const shuffledKeys = [...validKeys]; - this.parent.shuffleArray(shuffledKeys); + this.parent.gameUtil.shuffleArray(shuffledKeys); return this.parent.createKeySelectionUI(shuffledKeys, correctKeyId); } @@ -107,7 +107,7 @@ export class KeySelection { // Randomize the order of keys const keys = [key1, key2, key3]; - this.parent.shuffleArray(keys); + this.parent.gameUtil.shuffleArray(keys); // Find the new index of the correct key after shuffling const correctKeyIndex = keys.findIndex(key => key.id === correctKeyId); diff --git a/js/minigames/lockpicking/lock-configuration.js b/js/minigames/lockpicking/lock-configuration.js index 2d84e70..0b5a7aa 100644 --- a/js/minigames/lockpicking/lock-configuration.js +++ b/js/minigames/lockpicking/lock-configuration.js @@ -118,7 +118,7 @@ export class LockConfiguration { } // Update pin visuals - this.parent.updatePinVisuals(pin); + this.parent.pinVisuals.updatePinVisuals(pin); }); console.log('Reset all pins to original positions'); diff --git a/js/minigames/lockpicking/lock-graphics.js b/js/minigames/lockpicking/lock-graphics.js index 3f0a62d..ec033ee 100644 --- a/js/minigames/lockpicking/lock-graphics.js +++ b/js/minigames/lockpicking/lock-graphics.js @@ -99,7 +99,7 @@ export class LockGraphics { // 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."); + this.parent.keyInsertion.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); @@ -110,7 +110,7 @@ export class LockGraphics { // 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."); + this.parent.keyInsertion.updateFeedback("Tension released. All pins will fall back down."); // Play reset sound if (this.parent.sounds.reset) { diff --git a/js/minigames/lockpicking/lockpicking-game-phaser.js b/js/minigames/lockpicking/lockpicking-game-phaser.js index 71a9055..3d61d62 100644 --- a/js/minigames/lockpicking/lockpicking-game-phaser.js +++ b/js/minigames/lockpicking/lockpicking-game-phaser.js @@ -7,6 +7,14 @@ import { KeyOperations } from './key-operations.js'; import { PinManagement } from './pin-management.js'; import { ToolManager } from './tool-manager.js'; import { KeyAnimation } from './key-animation.js'; +import { HookMechanics } from './hook-mechanics.js'; +import { PinVisuals } from './pin-visuals.js'; +import { KeyInsertion } from './key-insertion.js'; +import { KeyDrawing } from './key-drawing.js'; +import { KeyPathDrawing } from './key-path-drawing.js'; +import { KeyGeometry } from './key-geometry.js'; +import { KeyPointGeometry } from './key-point-geometry.js'; +import { GameUtilities } from './game-utilities.js'; // Phaser Lockpicking Minigame Scene implementation export class LockpickingMinigamePhaser extends MinigameScene { @@ -46,6 +54,30 @@ export class LockpickingMinigamePhaser extends MinigameScene { // Initialize KeyAnimation module this.keyAnim = new KeyAnimation(this); + + // Initialize HookMechanics module + this.hookMech = new HookMechanics(this); + + // Initialize PinVisuals module + this.pinVisuals = new PinVisuals(this); + + // Initialize KeyInsertion module + this.keyInsertion = new KeyInsertion(this); + + // Initialize KeyDrawing module + this.keyDraw = new KeyDrawing(this); + + // Initialize KeyPathDrawing module + this.keyPathDraw = new KeyPathDrawing(this); + + // Initialize KeyGeometry module + this.keyGeom = new KeyGeometry(this); + + // Initialize KeyPointGeometry module + this.keyPointGeom = new KeyPointGeometry(this); + + // Initialize GameUtilities module + this.gameUtil = new GameUtilities(this); } // Also try to load from localStorage for persistence across sessions @@ -147,7 +179,7 @@ export class LockpickingMinigamePhaser extends MinigameScene { if (this.closeButtonAction === 'reset') { this.addEventListener(closeBtn, 'click', () => { this.pinMgmt.resetAllPins(); - this.updateFeedback("Lock reset - try again"); + this.keyInsertion.updateFeedback("Lock reset - try again"); }); } else { // Default close action @@ -171,7 +203,7 @@ export class LockpickingMinigamePhaser extends MinigameScene { if (this.closeButtonAction === 'reset') { this.addEventListener(cancelBtn, 'click', () => { this.pinMgmt.resetAllPins(); - this.updateFeedback("Lock reset - try again"); + this.keyInsertion.updateFeedback("Lock reset - try again"); }); } else { // Default cancel action @@ -313,7 +345,7 @@ export class LockpickingMinigamePhaser extends MinigameScene { if (self.keyMode && !self.skipStartingKey) { self.keyOps.createKey(); self.toolMgr.hideLockpickingTools(); - self.updateFeedback("Click the key to insert it into the lock"); + self.keyInsertion.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 // But we still need to initialize keyData for the correct key @@ -321,9 +353,9 @@ export class LockpickingMinigamePhaser extends MinigameScene { self.keyDataGen.generateKeyDataFromPins(); } self.toolMgr.hideLockpickingTools(); - self.updateFeedback("Select a key to begin"); + self.keyInsertion.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.keyInsertion.updateFeedback("Apply tension first, then lift pins in binding order - only the binding pin can be set"); } self.pinMgmt.setupInputHandlers(); @@ -382,7 +414,7 @@ export class LockpickingMinigamePhaser extends MinigameScene { console.log('Phaser game created, scene:', this.scene); } catch (error) { console.error('Error creating Phaser game:', error); - this.updateFeedback('Error loading Phaser game: ' + error.message); + this.keyInsertion.updateFeedback('Error loading Phaser game: ' + error.message); } } @@ -514,336 +546,6 @@ export class LockpickingMinigamePhaser extends MinigameScene { this.keySelectionContainer = keySelectionContainer; } - drawKeyWithRenderTexture(circleRadius, shoulderWidth, shoulderHeight, bladeWidth, bladeHeight, fullKeyLength) { - console.log('drawKeyWithRenderTexture called with:', { - hasKeyData: !!this.keyData, - hasCuts: !!(this.keyData && this.keyData.cuts), - keyData: this.keyData - }); - - if (!this.keyData || !this.keyData.cuts) { - console.log('Early return - missing key data or cuts'); - return; - } - - // Create temporary graphics for drawing to render texture - const tempGraphics = this.scene.add.graphics(); - tempGraphics.fillStyle(0xcccccc); // Silver color for key - - // Calculate positions - const circleX = circleRadius; // Circle center - const shoulderX = circleRadius * 1.9; // After circle - const bladeX = shoulderX + shoulderWidth; // After shoulder - - console.log('Drawing key handle:', { - circleX: circleX, - circleY: shoulderHeight/2, - circleRadius: circleRadius, - shoulderHeight: shoulderHeight, - renderTextureWidth: this.keyRenderTexture.width - }); - - // 1. Draw the circle (handle) - rightmost part as a separate object - const handleGraphics = this.scene.add.graphics(); - handleGraphics.fillStyle(0xcccccc); // Silver color for key - handleGraphics.fillCircle(circleX, 0, circleRadius); // Center at y=0 relative to key group - - // Add handle to the key group - this.keyGroup.add(handleGraphics); - - // 2. Draw the shoulder - rectangle - tempGraphics.fillRect(shoulderX, 0, shoulderWidth, shoulderHeight); - - // 3. Draw the blade with cuts as a solid shape - this.drawKeyBladeAsSolidShape(tempGraphics, bladeX, shoulderHeight/2 - bladeHeight/2, bladeWidth, bladeHeight); - - // Draw the graphics to the render texture (shoulder and blade only) - this.keyRenderTexture.draw(tempGraphics); - - // Clean up temporary graphics - tempGraphics.destroy(); - } - - drawKeyBladeAsSolidShape(graphics, bladeX, bladeY, bladeWidth, bladeHeight) { - // Draw the key blade as a solid shape with cuts removed - // The blade has a pattern like: \_/\_/\_/\_/\ where the cuts _ are based on pin depths - - // ASCII art of the key blade: - // _________ - // / \ ____ - // | | | \_/\_/\_/\_/\ - // | |_|______________/ - // \________/ - - - - const cutWidth = 24; // Width of each cut (same as pin width) - - // Calculate pin spacing to match the lock's pin positions - const pinSpacing = 400 / (this.pinCount + 1); - const margin = pinSpacing * 0.75; - - // Start with the base blade rectangle - const baseBladeRect = { - x: bladeX, - y: bladeY, - width: bladeWidth, - height: bladeHeight - }; - - // Create a path for the solid key blade - const path = new Phaser.Geom.Polygon(); - - // Start at the top-left corner of the blade - path.points.push(new Phaser.Geom.Point(bladeX, bladeY)); - - // Draw the top edge with cuts and ridges - let currentX = bladeX; - - // For each pin position, create the blade profile - for (let i = 0; i <= this.pinCount; i++) { - let cutDepth = 0; - let nextCutDepth = 0; - - if (i < this.pinCount) { - cutDepth = this.keyData.cuts[i] || 0; - } - if (i < this.pinCount - 1) { - nextCutDepth = this.keyData.cuts[i + 1] || 0; - } - - // Calculate pin position - const pinX = 100 + margin + i * pinSpacing; - const cutX = bladeX + (pinX - 100); - - if (i === 0) { - // First section: from left edge (shoulder) to first cut - const firstCutStartX = cutX - cutWidth/2; - - // Draw triangular peak from shoulder to first cut edge (touches exact edge of cut) - this.addFirstCutPeakToPath(path, currentX, bladeY, firstCutStartX, bladeY, 0, cutDepth); - currentX = firstCutStartX; - } - - if (i < this.pinCount) { - // Draw the cut (negative space - skip this section) - const cutStartX = cutX - cutWidth/2; - const cutEndX = cutX + cutWidth/2; - - // Move to the bottom of the cut - path.points.push(new Phaser.Geom.Point(cutStartX, bladeY + cutDepth)); - - // Draw the cut bottom - path.points.push(new Phaser.Geom.Point(cutEndX, bladeY + cutDepth)); - - currentX = cutEndX; - } - - if (i < this.pinCount - 1) { - // Draw triangular peak to next cut - const nextPinX = 100 + margin + (i + 1) * pinSpacing; - const nextCutX = bladeX + (nextPinX - 100); - const nextCutStartX = nextCutX - cutWidth/2; - - // Use triangular peak that goes up at 45 degrees to halfway, then down at 45 degrees - this.addTriangularPeakToPath(path, currentX, bladeY, nextCutStartX, bladeY, cutDepth, nextCutDepth); - currentX = nextCutStartX; - } else if (i === this.pinCount - 1) { - // Last section: from last cut to right edge - create pointed tip that extends forward - const keyRightEdge = bladeX + bladeWidth; - const tipExtension = 12; // How far the tip extends beyond the blade - const tipEndX = keyRightEdge + tipExtension; - - // First: draw triangular peak from last cut back up to blade top - const peakX = currentX + (keyRightEdge - currentX) * 0.3; // Peak at 30% of the way - this.addTriangularPeakToPath(path, currentX, bladeY, peakX, bladeY, cutDepth, 0); - - // Second: draw the pointed tip that extends forward from top and bottom - this.addPointedTipToPath(path, peakX, bladeY, tipEndX, bladeHeight); - currentX = tipEndX; - } - } - - // Complete the path: right edge, bottom edge, left edge - path.points.push(new Phaser.Geom.Point(bladeX + bladeWidth, bladeY + bladeHeight)); - path.points.push(new Phaser.Geom.Point(bladeX, bladeY + bladeHeight)); - path.points.push(new Phaser.Geom.Point(bladeX, bladeY)); - - // Draw the solid shape - graphics.fillPoints(path.points, true, true); - } - - addTriangularSectionToPath(path, startX, startY, endX, endY, cutDepth, isLeftTriangle) { - // Add a triangular section to the path - // This creates the sloping effect between cuts - - const width = Math.abs(endX - startX); - const stepSize = 4; // Consistent pixel size for steps - const steps = Math.max(1, Math.floor(width / stepSize)); - - for (let i = 0; i <= steps; i++) { - const progress = i / steps; - const x = startX + (endX - startX) * progress; - - let y; - if (isLeftTriangle) { - // Left triangle: height increases as we move toward the cut - y = startY + (cutDepth * progress); - } else { - // Right triangle: height decreases as we move away from the cut - y = startY + (cutDepth * (1 - progress)); - } - - path.points.push(new Phaser.Geom.Point(x, y)); - } - } - - addFirstCutPeakToPath(path, startX, startY, endX, endY, startCutDepth, endCutDepth) { - // Add a triangular peak from shoulder to first cut that touches the exact edge of the cut - // This ensures proper alignment without affecting other peaks - - const width = Math.abs(endX - startX); - const stepSize = 4; // Consistent pixel size for steps - const steps = Math.max(1, Math.floor(width / stepSize)); - const halfSteps = Math.floor(steps / 2); - - for (let i = 0; i <= steps; i++) { - const progress = i / steps; - const x = startX + (endX - startX) * progress; - - let y; - if (i <= halfSteps) { - // First half: slope up from start cut depth to peak (blade top) - const upProgress = i / halfSteps; - y = startY + startCutDepth - (startCutDepth * upProgress); // Slope up to blade top - } else { - // Second half: slope down from peak to end cut depth - const downProgress = (i - halfSteps) / halfSteps; - y = startY + (endCutDepth * downProgress); // Slope down from blade top - } - - // Ensure the final point connects to the exact cut edge coordinates - if (i === steps) { - // Connect directly to the cut edge at the calculated depth - y = startY + endCutDepth; - } - - path.points.push(new Phaser.Geom.Point(x, y)); - } - } - - addTriangularPeakToPath(path, startX, startY, endX, endY, startCutDepth, endCutDepth) { - // Add a triangular peak between cuts that goes up at 45 degrees to halfway, then down at 45 degrees - // This creates a more realistic key blade profile with proper peaks between cuts - - const width = Math.abs(endX - startX); - const stepSize = 4; // Consistent pixel size for steps - const steps = Math.max(1, Math.floor(width / stepSize)); - const halfSteps = Math.floor(steps / 2); - - // Calculate the peak height - should be at the blade top (0 depth) at the halfway point - const maxPeakHeight = Math.max(startCutDepth, endCutDepth); // Use the deeper cut as reference - - for (let i = 0; i <= steps; i++) { - const progress = i / steps; - const x = startX + (endX - startX) * progress; - - let y; - if (i <= halfSteps) { - // First half: slope up from start cut depth to peak (blade top) - const upProgress = i / halfSteps; - y = startY + startCutDepth - (startCutDepth * upProgress); // Slope up to blade top - } else { - // Second half: slope down from peak to end cut depth - const downProgress = (i - halfSteps) / halfSteps; - y = startY + (endCutDepth * downProgress); // Slope down from blade top - } - - path.points.push(new Phaser.Geom.Point(x, y)); - } - } - - addPointedTipToPath(path, startX, startY, endX, bladeHeight) { - // Add a pointed tip that extends forward from both top and bottom of the blade - // This creates the key tip as shown in the ASCII art: \_/\_/\_/\_/\_/ - - const width = Math.abs(endX - startX); - const stepSize = 4; // Consistent pixel size for steps - const steps = Math.max(1, Math.floor(width / stepSize)); - - // Calculate the bottom point (directly below the start point) - const bottomX = startX; - const bottomY = startY + bladeHeight; - - // Calculate the tip point (the rightmost point) - const tipX = endX; - const tipY = startY + (bladeHeight / 2); // Center of the blade height - - // Draw the pointed tip: from top to tip to bottom - // First, go from top (startY) to tip (rightmost point) - const topToTipSteps = Math.max(1, Math.floor(width / stepSize)); - for (let i = 0; i <= topToTipSteps; i++) { - const progress = i / topToTipSteps; - const x = startX + (width * progress); - const y = startY + (bladeHeight / 2 * progress); // Slope down from top to center - path.points.push(new Phaser.Geom.Point(x, y)); - } - - // Then, go from tip to bottom - const tipToBottomSteps = Math.max(1, Math.floor(width / stepSize)); - for (let i = 0; i <= tipToBottomSteps; i++) { - const progress = i / tipToBottomSteps; - const x = tipX - (width * progress); - const y = tipY + (bladeHeight / 2 * progress); // Slope down from center to bottom - path.points.push(new Phaser.Geom.Point(x, y)); - } - } - - addRightPointingTriangleToPath(path, peakX, peakY, endX, endY, bladeHeight) { - // Add a triangle that goes from peak down to bottom, with third point facing right |> - // This creates the right-pointing part of the tip - - const width = Math.abs(endX - peakX); - const stepSize = 4; // Consistent pixel size for steps - const steps = Math.max(1, Math.floor(width / stepSize)); - - // Calculate the bottom point (directly below the peak) - const bottomX = peakX; - const bottomY = peakY + bladeHeight; - - // Calculate the rightmost point (the tip pointing to the right) - const tipX = endX; - const tipY = peakY + (bladeHeight / 2); // Center of the blade height - - // Draw the triangle: from peak to bottom to tip - // First, go from peak to bottom - const peakToBottomSteps = Math.max(1, Math.floor(bladeHeight / stepSize)); - for (let i = 0; i <= peakToBottomSteps; i++) { - const progress = i / peakToBottomSteps; - const x = peakX; - const y = peakY + (bladeHeight * progress); - path.points.push(new Phaser.Geom.Point(x, y)); - } - - // Then, go from bottom to tip (rightmost point) - const bottomToTipSteps = Math.max(1, Math.floor(width / stepSize)); - for (let i = 0; i <= bottomToTipSteps; i++) { - const progress = i / bottomToTipSteps; - const x = bottomX + (width * progress); - const y = bottomY - (bladeHeight / 2 * progress); // Slope up from bottom to center - path.points.push(new Phaser.Geom.Point(x, y)); - } - - // Finally, go from tip back to peak - const tipToPeakSteps = Math.max(1, Math.floor(width / stepSize)); - for (let i = 0; i <= tipToPeakSteps; i++) { - const progress = i / tipToPeakSteps; - const x = tipX - (width * progress); - const y = tipY - (bladeHeight / 2 * progress); // Slope up from center to peak - path.points.push(new Phaser.Geom.Point(x, y)); - } - } - drawCircleAsPolygon(graphics, centerX, centerY, radius) { // Draw a circle as a polygon path to match the blade drawing method const path = new Phaser.Geom.Polygon(); @@ -861,727 +563,11 @@ export class LockpickingMinigamePhaser extends MinigameScene { graphics.fillPoints(path.points, true, true); } - drawPixelArtCircleToGraphics(graphics, centerX, centerY, radius) { - // Draw a pixel art circle to the specified graphics object - const stepSize = 4; // Consistent pixel size for steps - const diameter = radius * 2; - const steps = Math.floor(diameter / stepSize); - - // Draw horizontal lines to create the circle shape - for (let i = 0; i <= steps; i++) { - const y = centerY - radius + (i * stepSize); - const distanceFromCenter = Math.abs(y - centerY); - - // Calculate the width of this horizontal line using circle equation - // For a circle: x² + y² = r², so x = √(r² - y²) - const halfWidth = Math.sqrt(radius * radius - distanceFromCenter * distanceFromCenter); - - if (halfWidth > 0) { - // Draw the horizontal line for this row - const lineWidth = halfWidth * 2; - const lineX = centerX - halfWidth; - - // Round to stepSize for pixel art consistency - const roundedWidth = Math.floor(lineWidth / stepSize) * stepSize; - const roundedX = Math.floor(lineX / stepSize) * stepSize; - - graphics.fillRect(roundedX, y, roundedWidth, stepSize); - } - } - } - - - - updateKeyPosition(progress) { - if (!this.keyGroup || !this.keyConfig) return; - - // Calculate new position based on insertion progress - // Key moves from left (off-screen) to right (shoulder touches lock edge) - const targetX = this.keyConfig.keywayStartX - this.keyConfig.shoulderWidth; // Shoulder touches lock edge - const currentX = this.keyConfig.startX + (targetX - this.keyConfig.startX) * progress; - - this.keyGroup.x = currentX; - this.keyInsertionProgress = progress; - - // If fully inserted, check if key is correct - if (progress >= 1.0) { - this.keyOps.checkKeyCorrectness(); - } - } - - updatePinsWithKeyInsertion(progress) { - if (!this.keyConfig) return; - - // Calculate key blade position relative to the lock - const keyBladeStartX = this.keyGroup.x + this.keyConfig.circleRadius * 2 + this.keyConfig.shoulderWidth; - const keyBladeEndX = keyBladeStartX + this.keyConfig.bladeWidth; - - // Key blade base position in world coordinates - const keyBladeBaseY = this.keyGroup.y - this.keyConfig.bladeHeight / 2; - - // Shear line for highlighting - const shearLineY = -45; // Same as lockpicking mode - const tolerance = 10; - - // Check each pin for collision with the key blade - this.pins.forEach((pin, index) => { - if (index >= this.pinCount) return; - - // Calculate pin position in the lock - const pinSpacing = 400 / (this.pinCount + 1); - const margin = pinSpacing * 0.75; - const pinX = 100 + margin + index * pinSpacing; - - // Check if this pin is under the key blade - const pinIsUnderKeyBlade = pinX >= keyBladeStartX && pinX <= keyBladeEndX; - - if (pinIsUnderKeyBlade) { - // Use collision detection to find the key surface height at this pin's position - const keySurfaceY = this.getKeySurfaceHeightAtPinPosition(pinX, keyBladeStartX, keyBladeBaseY); - - // Calculate where the key pin bottom should be to sit on the key surface - const pinRestY = 200 - 50 + pin.driverPinLength + pin.keyPinLength; - const targetKeyPinBottom = keySurfaceY; - - // Calculate required lift to move key pin bottom from rest to key surface - const requiredLift = pinRestY - targetKeyPinBottom; - const targetLift = Math.max(0, requiredLift); - - // Smooth movement toward target - if (pin.currentHeight < targetLift) { - pin.currentHeight = Math.min(targetLift, pin.currentHeight + 2); - } else if (pin.currentHeight > targetLift) { - pin.currentHeight = Math.max(targetLift, pin.currentHeight - 1); - } - } else { - // Pin is not under key blade - keep current position (don't drop back down) - // This ensures pins stay lifted once they've been pushed up by the key - } - - // Check if pin is near shear line for highlighting - // Use the same boundary calculation as lockpicking mode - const boundaryPosition = -50 + pin.driverPinLength - pin.currentHeight; - const distanceToShearLine = Math.abs(boundaryPosition - shearLineY); - - // Debug: Log boundary positions for highlighting - console.log(`Pin ${index} highlighting: boundaryPosition=${boundaryPosition}, distanceToShearLine=${distanceToShearLine}, tolerance=${tolerance}, shouldHighlight=${distanceToShearLine <= tolerance}, hasShearHighlight=${!!pin.shearHighlight}, hasSetHighlight=${!!pin.setHighlight}`); - - // Update pin highlighting based on shear line proximity - this.updatePinHighlighting(pin, distanceToShearLine, tolerance); - - // Update pin visuals - this.updatePinVisuals(pin); - }); - } - - getKeySurfaceHeightAtPinPosition(pinX, keyBladeStartX, keyBladeBaseY) { - // Use collision detection to find the key surface height at a specific pin position - // This method traces a vertical line from the pin position down to find where it intersects the key polygon - - const bladeWidth = this.keyConfig.bladeWidth; - const bladeHeight = this.keyConfig.bladeHeight; - - // Calculate the pin's position relative to the key blade - const pinRelativeToKey = pinX - keyBladeStartX; - - // If pin is beyond the key blade, return base surface - if (pinRelativeToKey < 0 || pinRelativeToKey > bladeWidth) { - return keyBladeBaseY; - } - - // Generate the key polygon points at the current position - const keyPolygonPoints = this.generateKeyPolygonPoints(keyBladeStartX, keyBladeBaseY); - - // Find the intersection point by tracing a vertical line from the pin position - const intersectionY = this.findVerticalIntersection(pinX, keyBladeBaseY, keyBladeBaseY + bladeHeight, keyPolygonPoints); - - return intersectionY !== null ? intersectionY : keyBladeBaseY; - } - - generateKeyPolygonPoints(keyBladeStartX, keyBladeBaseY) { - // Generate the key polygon points at the current position - // This recreates the same polygon logic used in drawKeyBladeAsSolidShape - const points = []; - const bladeWidth = this.keyConfig.bladeWidth; - const bladeHeight = this.keyConfig.bladeHeight; - const cutWidth = 24; - - // Calculate pin spacing - const pinSpacing = 400 / (this.pinCount + 1); - const margin = pinSpacing * 0.75; - - // Start at the top-left corner of the blade - points.push({ x: keyBladeStartX, y: keyBladeBaseY }); - - let currentX = keyBladeStartX; - - // Generate the same path as the drawing method - for (let i = 0; i <= this.pinCount; i++) { - let cutDepth = 0; - let nextCutDepth = 0; - - if (i < this.pinCount) { - cutDepth = (this.selectedKeyData || this.keyData).cuts[i] || 0; - } - if (i < this.pinCount - 1) { - nextCutDepth = (this.selectedKeyData || this.keyData).cuts[i + 1] || 0; - } - - // Calculate pin position - const pinX = 100 + margin + i * pinSpacing; - const cutX = keyBladeStartX + (pinX - 100); - - if (i === 0) { - // First section: from left edge to first cut - const firstCutStartX = cutX - cutWidth/2; - this.addTriangularPeakToPoints(points, currentX, keyBladeBaseY, firstCutStartX, keyBladeBaseY, 0, cutDepth); - currentX = firstCutStartX; - } - - if (i < this.pinCount) { - // Draw the cut - const cutStartX = cutX - cutWidth/2; - const cutEndX = cutX + cutWidth/2; - points.push({ x: cutStartX, y: keyBladeBaseY + cutDepth }); - points.push({ x: cutEndX, y: keyBladeBaseY + cutDepth }); - currentX = cutEndX; - } - - if (i < this.pinCount - 1) { - // Draw triangular peak to next cut - const nextPinX = 100 + margin + (i + 1) * pinSpacing; - const nextCutX = keyBladeStartX + (nextPinX - 100); - const nextCutStartX = nextCutX - cutWidth/2; - this.addTriangularPeakToPoints(points, currentX, keyBladeBaseY, nextCutStartX, keyBladeBaseY, cutDepth, nextCutDepth); - currentX = nextCutStartX; - } else if (i === this.pinCount - 1) { - // Last section: pointed tip - const keyRightEdge = keyBladeStartX + bladeWidth; - const tipExtension = 12; - const tipEndX = keyRightEdge + tipExtension; - const peakX = currentX + (keyRightEdge - currentX) * 0.3; - this.addTriangularPeakToPoints(points, currentX, keyBladeBaseY, peakX, keyBladeBaseY, cutDepth, 0); - this.addPointedTipToPoints(points, peakX, keyBladeBaseY, tipEndX, bladeHeight); - currentX = tipEndX; - } - } - - // Complete the path - points.push({ x: keyBladeStartX + bladeWidth, y: keyBladeBaseY + bladeHeight }); - points.push({ x: keyBladeStartX, y: keyBladeBaseY + bladeHeight }); - points.push({ x: keyBladeStartX, y: keyBladeBaseY }); - - return points; - } - - findVerticalIntersection(pinX, startY, endY, polygonPoints) { - // Find where a vertical line at pinX intersects the polygon - // Returns the Y coordinate of the intersection, or null if no intersection - - let intersectionY = null; - - for (let i = 0; i < polygonPoints.length - 1; i++) { - const p1 = polygonPoints[i]; - const p2 = polygonPoints[i + 1]; - - // Check if this line segment crosses the vertical line at pinX - if ((p1.x <= pinX && p2.x >= pinX) || (p1.x >= pinX && p2.x <= pinX)) { - // Calculate intersection - const t = (pinX - p1.x) / (p2.x - p1.x); - const y = p1.y + t * (p2.y - p1.y); - - // Keep the highest intersection point (closest to the pin) - if (intersectionY === null || y < intersectionY) { - intersectionY = y; - } - } - } - - return intersectionY; - } - - addTriangularPeakToPoints(points, startX, startY, endX, endY, startCutDepth, endCutDepth) { - // Add triangular peak points (same logic as addTriangularPeakToPath) - const width = Math.abs(endX - startX); - const stepSize = 4; - const steps = Math.max(1, Math.floor(width / stepSize)); - const halfSteps = Math.floor(steps / 2); - - for (let i = 0; i <= steps; i++) { - const progress = i / steps; - const x = startX + (endX - startX) * progress; - - let y; - if (i <= halfSteps) { - const upProgress = i / halfSteps; - y = startY + startCutDepth - (startCutDepth * upProgress); - } else { - const downProgress = (i - halfSteps) / halfSteps; - y = startY + (endCutDepth * downProgress); - } - - points.push({ x: x, y: y }); - } - } - - addPointedTipToPoints(points, startX, startY, endX, bladeHeight) { - // Add pointed tip points (same logic as addPointedTipToPath) - const width = Math.abs(endX - startX); - const stepSize = 4; - const steps = Math.max(1, Math.floor(width / stepSize)); - - const tipX = endX; - const tipY = startY + (bladeHeight / 2); - - // From top to tip - const topToTipSteps = Math.max(1, Math.floor(width / stepSize)); - for (let i = 0; i <= topToTipSteps; i++) { - const progress = i / topToTipSteps; - const x = startX + (width * progress); - const y = startY + (bladeHeight / 2 * progress); - points.push({ x: x, y: y }); - } - - // From tip to bottom - const tipToBottomSteps = Math.max(1, Math.floor(width / stepSize)); - for (let i = 0; i <= tipToBottomSteps; i++) { - const progress = i / tipToBottomSteps; - const x = tipX - (width * progress); - const y = tipY + (bladeHeight / 2 * progress); - points.push({ x: x, y: y }); - } - } - - - - getTriangularSectionHeightAtX(relativeX, bladeWidth, bladeHeight) { - // Calculate height of triangular sections at a given X position - // Creates peaks that go up to blade top between cuts - const cutWidth = 24; - const pinSpacing = 400 / (this.pinCount + 1); - const margin = pinSpacing * 0.75; - - // Check triangular sections between cuts - for (let i = 0; i < this.pinCount - 1; i++) { - const cut1X = margin + i * pinSpacing; - const cut2X = margin + (i + 1) * pinSpacing; - const cut1EndX = cut1X + cutWidth/2; - const cut2StartX = cut2X - cutWidth/2; - - // Check if we're in the triangular section between these cuts - if (relativeX >= cut1EndX && relativeX <= cut2StartX) { - const distanceFromCut1 = relativeX - cut1EndX; - const triangularWidth = cut2StartX - cut1EndX; - const progress = distanceFromCut1 / triangularWidth; - - // Get cut depths for both cuts - const cut1Depth = this.keyData.cuts[i] || 0; - const cut2Depth = this.keyData.cuts[i + 1] || 0; - - // Create a peak: go up from cut1 to blade top, then down to cut2 - const halfWidth = triangularWidth / 2; - - if (distanceFromCut1 <= halfWidth) { - // First half: slope up from cut1 to blade top - const upProgress = distanceFromCut1 / halfWidth; - return cut1Depth + (bladeHeight - cut1Depth) * upProgress; - } else { - // Second half: slope down from blade top to cut2 - const downProgress = (distanceFromCut1 - halfWidth) / halfWidth; - return bladeHeight - (bladeHeight - cut2Depth) * downProgress; - } - } - } - - // Check triangular section from left edge to first cut - const firstCutX = margin; - const firstCutStartX = firstCutX - cutWidth/2; - - if (relativeX >= 0 && relativeX < firstCutStartX) { - const progress = relativeX / firstCutStartX; - const firstCutDepth = this.keyData.cuts[0] || 0; - - // Create a peak: slope up from base to blade top, then down to first cut - const halfWidth = firstCutStartX / 2; - - if (relativeX <= halfWidth) { - // First half: slope up from base (0) to blade top - const upProgress = relativeX / halfWidth; - return bladeHeight * upProgress; - } else { - // Second half: slope down from blade top to first cut depth - const downProgress = (relativeX - halfWidth) / halfWidth; - return bladeHeight - (bladeHeight - firstCutDepth) * downProgress; - } - } - - // Check triangular section from last cut to right edge - const lastCutX = margin + (this.pinCount - 1) * pinSpacing; - const lastCutEndX = lastCutX + cutWidth/2; - - if (relativeX > lastCutEndX && relativeX <= bladeWidth) { - const triangularWidth = bladeWidth - lastCutEndX; - const distanceFromLastCut = relativeX - lastCutEndX; - const progress = distanceFromLastCut / triangularWidth; - const lastCutDepth = this.keyData.cuts[this.pinCount - 1] || 0; - - // Create a peak: slope up from last cut to blade top, then down to base - const halfWidth = triangularWidth / 2; - - if (distanceFromLastCut <= halfWidth) { - // First half: slope up from last cut depth to blade top - const upProgress = distanceFromLastCut / halfWidth; - return lastCutDepth + (bladeHeight - lastCutDepth) * upProgress; - } else { - // Second half: slope down from blade top to base (0) - const downProgress = (distanceFromLastCut - halfWidth) / halfWidth; - return bladeHeight * (1 - downProgress); - } - } - - return 0; // Not in a triangular section - } - - getTriangularSectionHeightAsKeyMoves(pinRelativeToKeyLeadingEdge, bladeWidth, bladeHeight) { - // Calculate triangular section height as the key moves underneath the pin - // This creates the sloping effect as pins follow the key's surface - - const cutWidth = 24; - const pinSpacing = 400 / (this.pinCount + 1); - const margin = pinSpacing * 0.75; - - // Check triangular section from left edge to first cut - const firstCutX = margin; - const firstCutStartX = firstCutX - cutWidth/2; - - if (pinRelativeToKeyLeadingEdge >= 0 && pinRelativeToKeyLeadingEdge < firstCutStartX) { - // Pin is in the triangular section from left edge to first cut - const progress = pinRelativeToKeyLeadingEdge / firstCutStartX; - const firstCutDepth = this.keyData.cuts[0] || 0; - // Start from base level (0) and slope up to first cut depth - return Math.max(0, firstCutDepth * progress); // Ensure we never go below base level - } - - // Check triangular sections between cuts - for (let i = 0; i < this.pinCount - 1; i++) { - const cut1X = margin + i * pinSpacing; - const cut2X = margin + (i + 1) * pinSpacing; - const cut1EndX = cut1X + cutWidth/2; - const cut2StartX = cut2X - cutWidth/2; - - if (pinRelativeToKeyLeadingEdge >= cut1EndX && pinRelativeToKeyLeadingEdge <= cut2StartX) { - // Pin is in triangular section between these cuts - const distanceFromCut1 = pinRelativeToKeyLeadingEdge - cut1EndX; - const triangularWidth = cut2StartX - cut1EndX; - const progress = distanceFromCut1 / triangularWidth; - - // Get cut depths for both cuts - const cut1Depth = this.keyData.cuts[i] || 0; - const cut2Depth = this.keyData.cuts[i + 1] || 0; - - // Interpolate between cut depths (slope from cut1 to cut2) - return cut1Depth + (cut2Depth - cut1Depth) * progress; - } - } - - // Check triangular section from last cut to right edge - const lastCutX = margin + (this.pinCount - 1) * pinSpacing; - const lastCutEndX = lastCutX + cutWidth/2; - - if (pinRelativeToKeyLeadingEdge >= lastCutEndX && pinRelativeToKeyLeadingEdge <= bladeWidth) { - // Pin is in triangular section from last cut to right edge - const distanceFromLastCut = pinRelativeToKeyLeadingEdge - lastCutEndX; - const triangularWidth = bladeWidth - lastCutEndX; - const progress = distanceFromLastCut / triangularWidth; - const lastCutDepth = this.keyData.cuts[this.pinCount - 1] || 0; - return lastCutDepth * (1 - progress); // Slope down from last cut depth to 0 - } - - return 0; // Not in a triangular section - } - createKeyBladeCollision() { // Method moved to KeyOperations module - call via this.keyOps.createKeyBladeCollision() this.keyOps.createKeyBladeCollision(); } - getKeySurfaceHeightAtPosition(pinX, keyBladeStartX) { - // Method moved to KeyOperations module - delegate to it - return this.keyOps.getKeySurfaceHeightAtPosition(pinX, keyBladeStartX); - } - - updateHookPosition(pinIndex) { - if (!this.hookGroup || !this.hookConfig) return; - - const config = this.hookConfig; - const targetPin = this.pins[pinIndex]; - - if (!targetPin) return; - - // Calculate the target Y position (bottom of the key pin) - const pinWorldY = 200; // Base Y position for pins - const currentTargetY = pinWorldY - 50 + targetPin.driverPinLength + targetPin.keyPinLength - targetPin.currentHeight; - - console.log('Hook update - following pin:', pinIndex, 'currentHeight:', targetPin.currentHeight, 'targetY:', currentTargetY); - - // Update the last targeted pin - this.hookConfig.lastTargetedPin = pinIndex; - - // Calculate the pin's X position (same logic as createPins) - const pinSpacing = 400 / (this.pinCount + 1); - const margin = pinSpacing * 0.75; - const pinX = 100 + margin + pinIndex * pinSpacing; - - // Calculate the pin's base Y position (when currentHeight = 0) - const pinBaseY = pinWorldY - 50 + targetPin.driverPinLength + targetPin.keyPinLength; - - // Calculate how much the pin has moved from its own base position - const heightDifference = pinBaseY - currentTargetY; - - // Calculate rotation angle based on percentage of pin movement and pin number - const maxHeightDifference = 50; // Maximum expected height difference - const minRotationDegrees = 20; // Minimum rotation for highest pin - const maxRotationDegrees = 40; // Maximum rotation for lowest pin - - // Calculate pin-based rotation range (pin 0 = max rotation, pin n-1 = min rotation) - const pinRotationRange = maxRotationDegrees - minRotationDegrees; - const pinRotationFactor = pinIndex / (this.pinCount - 1); // 0 for first pin, 1 for last pin - const pinRotationOffset = pinRotationRange * pinRotationFactor; - const pinMaxRotation = maxRotationDegrees - pinRotationOffset; - - // Calculate percentage of pin movement (0% to 100%) - const pinMovementPercentage = Math.min((heightDifference / maxHeightDifference) * 100, 100); - - // Calculate rotation based on percentage and pin-specific max rotation - // Higher pin indices (further pins) rotate slower by reducing the percentage - const pinSpeedFactor = 1 - (pinIndex / this.pinCount) * 0.5; // 1.0 for pin 0, 0.5 for last pin - const adjustedPercentage = pinMovementPercentage * pinSpeedFactor; - const rotationAngle = (adjustedPercentage / 100) * pinMaxRotation; - - // Calculate the new tip position (hook should point at the current pin) - const totalHookHeight = (config.diagonalSegments + config.verticalSegments) * config.segmentStep; - const newTipX = pinX - totalHookHeight + 34; // Add 34px offset (24px + 10px further right) - - // Update hook position and rotation - this.hookGroup.x = newTipX; - this.hookGroup.y = currentTargetY; - this.hookGroup.setAngle(-rotationAngle); // Negative for anti-clockwise rotation - - // Check for collisions with other pins using hook's current position - this.checkHookCollisions(pinIndex, this.hookGroup.y); - - 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)); - } - - checkHookCollisions(targetPinIndex, hookCurrentY) { - if (!this.hookConfig || !this.gameState.mouseDown) return; - - // Clear previous debug graphics - if (this.debugGraphics) { - this.debugGraphics.clear(); - } else { - this.debugGraphics = this.scene.add.graphics(); - this.debugGraphics.setDepth(100); // Render on top - } - - // Create a temporary rectangle for the hook's horizontal arm using Phaser's physics - const hookArmWidth = 8; - const hookArmLength = 100; - - // Calculate the horizontal arm position relative to the hook's current position - // The horizontal arm extends from the handle to the curve start - const handleStartX = -120; // Handle starts at -120 - const handleWidth = 20; - const armStartX = handleStartX + handleWidth; // Arm starts after handle (-100) - const armEndX = armStartX + hookArmLength; // Arm ends at +40 - - // Position the collision box lower along the arm (not at the tip) - const collisionOffsetY = 35; // Move collision box down by 2350px - - // Convert to world coordinates with rotation - const hookAngle = this.hookGroup.angle * (Math.PI / 180); // Convert degrees to radians - const cosAngle = Math.cos(hookAngle); - const sinAngle = Math.sin(hookAngle); - - // Calculate rotated arm start and end points - const armStartX_rotated = armStartX * cosAngle - collisionOffsetY * sinAngle; - const armStartY_rotated = armStartX * sinAngle + collisionOffsetY * cosAngle; - const armEndX_rotated = armEndX * cosAngle - collisionOffsetY * sinAngle; - const armEndY_rotated = armEndX * sinAngle + collisionOffsetY * cosAngle; - - // Convert to world coordinates - const worldArmStartX = armStartX_rotated + this.hookGroup.x; - const worldArmStartY = armStartY_rotated + this.hookGroup.y; - const worldArmEndX = armEndX_rotated + this.hookGroup.x; - const worldArmEndY = armEndY_rotated + this.hookGroup.y; - - // Create a line for the rotated arm (this is what we'll use for collision detection) - const hookArmLine = new Phaser.Geom.Line(worldArmStartX, worldArmStartY, worldArmEndX, worldArmEndY); - - // // Render hook arm hitbox (red) - draw as a line to show rotation - // this.debugGraphics.lineStyle(3, 0xff0000); - // this.debugGraphics.beginPath(); - // this.debugGraphics.moveTo(worldArmStartX, worldArmStartY); - // this.debugGraphics.lineTo(worldArmEndX, worldArmEndY); - // this.debugGraphics.strokePath(); - - // // Also render a rectangle around the collision area for debugging - // this.debugGraphics.lineStyle(1, 0xff0000); - // this.debugGraphics.strokeRect( - // Math.min(worldArmStartX, worldArmEndX), - // Math.min(worldArmStartY, worldArmEndY), - // Math.abs(worldArmEndX - worldArmStartX), - // Math.abs(worldArmEndY - worldArmStartY) + hookArmWidth - // ); - - // Check each pin for collision using Phaser's geometry - this.pins.forEach((pin, pinIndex) => { - if (pinIndex === targetPinIndex) return; // Skip the target pin - - // Calculate pin position - const pinSpacing = 400 / (this.pinCount + 1); - const margin = pinSpacing * 0.75; - const pinX = 100 + margin + pinIndex * pinSpacing; - const pinWorldY = 200; - - // Calculate pin's current position (including any existing movement) - // Add safety check for undefined properties - if (!pin.driverPinLength || !pin.keyPinLength) { - console.warn(`Pin ${pinIndex} missing length properties in checkHookCollisions:`, pin); - return; // Skip this pin if properties are missing - } - const pinCurrentY = pinWorldY - 50 + pin.driverPinLength + pin.keyPinLength - pin.currentHeight; - const keyPinTop = pinCurrentY - pin.keyPinLength; - const keyPinBottom = pinCurrentY; - - // Create a rectangle for the key pin - const keyPinRect = new Phaser.Geom.Rectangle(pinX - 12, keyPinTop, 24, pin.keyPinLength); - - // // Render pin hitbox (blue) - // this.debugGraphics.lineStyle(2, 0x0000ff); - // this.debugGraphics.strokeRect(pinX - 12, keyPinTop, 24, pin.keyPinLength); - - // Use Phaser's built-in line-to-rectangle intersection - if (Phaser.Geom.Intersects.LineToRectangle(hookArmLine, keyPinRect)) { - // Collision detected - lift this pin - this.liftCollidedPin(pin, pinIndex); - - // // Render collision (green) - // this.debugGraphics.lineStyle(3, 0x00ff00); - // this.debugGraphics.strokeRect(pinX - 12, keyPinTop, 24, pin.keyPinLength); - } - }); - } - - - - liftCollidedPin(pin, pinIndex) { - // Only lift if the pin isn't already being actively moved - if (this.lockState.currentPin && this.lockState.currentPin.index === pinIndex) return; - - // Calculate pin-specific maximum height - const baseMaxHeight = 75; - const maxHeightReduction = 15; - const pinHeightFactor = pinIndex / (this.pinCount - 1); - const pinMaxHeight = baseMaxHeight - (maxHeightReduction * pinHeightFactor); - - // Lift the pin faster for collision (more responsive) - const collisionLiftSpeed = this.liftSpeed * 0.8; // 80% of normal lift speed (increased from 30%) - pin.currentHeight = Math.min(pin.currentHeight + collisionLiftSpeed, pinMaxHeight * 0.5); // Max 50% of pin's max height - - // Update pin visuals - this.updatePinVisuals(pin); - - console.log(`Hook collision: Lifting pin ${pinIndex} to height ${pin.currentHeight}`); - } - - updatePinHighlighting(pin, distanceToShearLine, tolerance) { - // Update pin highlighting based on distance to shear line - // This provides visual feedback during key insertion - - // Create shear highlight if it doesn't exist - if (!pin.shearHighlight) { - pin.shearHighlight = this.scene.add.graphics(); - pin.shearHighlight.fillStyle(0x00ff00, 0.4); // Green with transparency - pin.shearHighlight.fillRect(-22.5, -110, 45, 140); - pin.container.addAt(pin.shearHighlight, 0); // Add at beginning to appear behind pins - } - - // Hide all highlights first - pin.shearHighlight.setVisible(false); - if (pin.bindingHighlight) pin.bindingHighlight.setVisible(false); - if (pin.overpickedHighlight) pin.overpickedHighlight.setVisible(false); - if (pin.failureHighlight) pin.failureHighlight.setVisible(false); - - // Show green highlight only if pin is at shear line - if (distanceToShearLine <= tolerance) { - // Pin is at shear line - show green highlight - pin.shearHighlight.setVisible(true); - console.log(`Pin ${pin.index} showing GREEN highlight - distance: ${distanceToShearLine}`); - } else { - // Pin is not at shear line - no highlight - console.log(`Pin ${pin.index} NO highlight - distance: ${distanceToShearLine}`); - } - } - - updatePinVisuals(pin) { - console.log(`Updating pin ${pin.index} visuals - currentHeight: ${pin.currentHeight}`); - - // Update key pin visual - pin.keyPin.clear(); - pin.keyPin.fillStyle(0xdd3333); - - // Calculate new position based on currentHeight - // Add safety check for undefined properties - if (!pin.driverPinLength || !pin.keyPinLength) { - console.warn(`Pin ${pin.index} missing length properties in updatePinVisuals:`, pin); - return; // Skip this pin if properties are missing - } - const newKeyPinY = -50 + pin.driverPinLength - pin.currentHeight; - const keyPinTopY = newKeyPinY; - const keyPinBottomY = newKeyPinY + pin.keyPinLength; - const shearLineY = -45; - const distanceToShearLine = Math.abs(keyPinTopY - shearLineY); - - console.log(`Pin ${pin.index} final positioning: keyPinTopY=${keyPinTopY}, keyPinBottomY=${keyPinBottomY}, distanceToShearLine=${distanceToShearLine}`); - - // Draw rectangular part of key pin - pin.keyPin.fillRect(-12, -50 + pin.driverPinLength - pin.currentHeight, 24, pin.keyPinLength - 8); - - // 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); - - // Update driver pin visual - pin.driverPin.clear(); - pin.driverPin.fillStyle(0x3388dd); - pin.driverPin.fillRect(-12, -50 - pin.currentHeight, 24, pin.driverPinLength); - - // Update spring compression - pin.spring.clear(); - pin.spring.fillStyle(0x666666); - const springCompression = pin.currentHeight; - const compressionFactor = Math.max(0.3, 1 - (springCompression / 60)); - - const springTop = -130; - const driverPinTop = -50 - pin.currentHeight; - const springBottom = driverPinTop; - const springHeight = springBottom - springTop; - const totalSpringSpace = springHeight; - const segmentSpacing = totalSpringSpace / 11; - - for (let s = 0; s < 12; s++) { - const segmentHeight = 4 * compressionFactor; - const segmentY = springTop + (s * segmentSpacing); - - if (segmentY + segmentHeight <= springBottom) { - pin.spring.fillRect(-12, segmentY, 24, segmentHeight); - } - } - } - update() { // Skip normal lockpicking logic if in key mode if (this.keyMode) { @@ -1610,206 +596,6 @@ export class LockpickingMinigamePhaser extends MinigameScene { // Hook return is now handled directly in pointerup event } - checkPinSet(pin) { - // Check if the key/driver boundary is at the shear line - const boundaryPosition = -50 + pin.driverPinLength - pin.currentHeight; - const shearLineY = -45; // Shear line is at y=-45 (much higher position) - const distanceToShearLine = Math.abs(boundaryPosition - shearLineY); - const shouldBind = this.shouldPinBind(pin); - - // Calculate threshold based on sensitivity (1-8) - // Higher sensitivity = smaller threshold (easier to set pins) - const baseThreshold = 8; - const sensitivityFactor = (9 - this.thresholdSensitivity) / 8; // Invert so higher sensitivity = smaller threshold - const threshold = baseThreshold * sensitivityFactor; - - // Debug logging for threshold calculation - if (distanceToShearLine < threshold + 2) { // Log when close to threshold - console.log(`Pin ${pin.index + 1}: distance=${distanceToShearLine.toFixed(2)}, threshold=${threshold.toFixed(2)}, sensitivity=${this.thresholdSensitivity}`); - } - - if (distanceToShearLine < threshold && shouldBind) { - // Pin set successfully - pin.isSet = true; - - // Set separate heights for key pin and driver pin - pin.keyPinHeight = 0; // Key pin drops back to original position - pin.driverPinHeight = 60; // Driver pin stays at shear line (60 units from base position) - - // Snap driver pin to shear line - calculate exact position - const shearLineY = -45; - const targetDriverBottom = shearLineY; - const driverPinTop = targetDriverBottom - pin.driverPinLength; - - // Update driver pin to snap to shear line - pin.driverPin.clear(); - pin.driverPin.fillStyle(0x3388dd); - pin.driverPin.fillRect(-12, driverPinTop, 24, pin.driverPinLength); - - // Reset key pin to original position (falls back down) - 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 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; - - for (let s = 0; s < 12; s++) { - const segmentHeight = 4; - const segmentSpacing = springHeight / 12; - - // Calculate segment position from bottom up to ensure bottom segment touches driver pin - const segmentY = springBottom - (segmentHeight + (11 - s) * segmentSpacing); - pin.spring.fillRect(-12, segmentY, 24, segmentHeight); - } - - // 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++; - - // Play set sound - if (this.sounds.set) { - this.sounds.set.play(); - if (typeof navigator !== 'undefined' && navigator.vibrate) { - navigator.vibrate(500); - } - } - - this.updateFeedback(`Pin ${pin.index + 1} set! (${this.lockState.pinsSet}/${this.pinCount})`); - this.pinMgmt.updateBindingPins(); - - if (this.lockState.pinsSet === this.pinCount) { - this.keyAnim.lockPickingSuccess(); - } - } else if (pin.isOverpicked) { - // Pin is overpicked - stays stuck until tension is released - if (pin.isSet) { - this.updateFeedback("Set pin overpicked! Release tension to reset."); - } else { - this.updateFeedback("Pin overpicked! Release tension to reset."); - } - } else if (pin.isSet) { - // Set pin: key pin falls back down, driver pin stays at shear line - pin.keyPinHeight = 0; // Key pin falls back to original position - pin.overpickingTimer = null; // Reset overpicking timer - - // Redraw key pin at 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); - - // Driver pin stays at shear line - pin.driverPin.clear(); - pin.driverPin.fillStyle(0x3388dd); - const shearLineY = -45; - const driverPinY = shearLineY - pin.driverPinLength; - pin.driverPin.fillRect(-12, driverPinY, 24, pin.driverPinLength); - - // Spring stays connected to driver pin at shear line - pin.spring.clear(); - pin.spring.fillStyle(0x666666); - const springTop = -130; - const springBottom = shearLineY - pin.driverPinLength; - const springHeight = springBottom - springTop; - const segmentSpacing = springHeight / 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); - } - } - } else { - // Normal pin falls back down due to gravity - pin.currentHeight = 0; - - // 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); - } - } - - shouldPinBind(pin) { - if (!this.lockState.tensionApplied) return false; - - // Find the next unset pin in binding order - for (let order = 0; order < this.pinCount; order++) { - const nextPin = this.pins.find(p => p.binding === order && !p.isSet); - if (nextPin) { - return pin.index === nextPin.index; - } - } - return false; - } - - updateFeedback(message) { - this.feedback.textContent = message; - } - complete(success) { if (this.game) { this.game.destroy(true); @@ -1818,12 +604,4 @@ export class LockpickingMinigamePhaser extends MinigameScene { super.complete(success, this.gameResult); } - shuffleArray(array) { - for (let i = array.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [array[i], array[j]] = [array[j], array[i]]; - } - return array; - } - } \ No newline at end of file diff --git a/js/minigames/lockpicking/pin-management.js b/js/minigames/lockpicking/pin-management.js index b3ad788..4ac0592 100644 --- a/js/minigames/lockpicking/pin-management.js +++ b/js/minigames/lockpicking/pin-management.js @@ -24,7 +24,7 @@ export class PinManagement { for (let i = 0; i < this.parent.pinCount; i++) { bindingOrder.push(i); } - this.parent.shuffleArray(bindingOrder); + this.parent.gameUtil.shuffleArray(bindingOrder); const pinSpacing = 400 / (this.parent.pinCount + 1); const margin = pinSpacing * 0.75; // 25% smaller margins @@ -299,8 +299,8 @@ export class PinManagement { } if (!this.parent.lockState.tensionApplied) { - this.parent.updateFeedback("Apply tension first before picking pins"); - this.parent.flashWrenchRed(); + this.parent.keyInsertion.updateFeedback("Apply tension first before picking pins"); + this.parent.toolMgr.flashWrenchRed(); } }); @@ -352,7 +352,7 @@ export class PinManagement { setupInputHandlers() { this.parent.scene.input.on('pointerup', () => { if (this.parent.lockState.currentPin) { - this.parent.checkPinSet(this.parent.lockState.currentPin); + this.parent.pinVisuals.checkPinSet(this.parent.lockState.currentPin); this.parent.lockState.currentPin = null; } this.parent.gameState.mouseDown = false; @@ -423,8 +423,8 @@ export class PinManagement { } if (!this.parent.lockState.tensionApplied) { - this.parent.updateFeedback("Apply tension first before picking pins"); - this.parent.flashWrenchRed(); + this.parent.keyInsertion.updateFeedback("Apply tension first before picking pins"); + this.parent.toolMgr.flashWrenchRed(); } } } @@ -455,7 +455,7 @@ export class PinManagement { // 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."); + this.parent.keyInsertion.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); @@ -466,7 +466,7 @@ export class PinManagement { // 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."); + this.parent.keyInsertion.updateFeedback("Tension released. All pins will fall back down."); // Play reset sound if (this.parent.sounds.reset) { @@ -542,7 +542,7 @@ export class PinManagement { // 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.pinVisuals.checkPinSet(this.parent.lockState.currentPin); this.parent.lockState.currentPin = null; this.parent.gameState.mouseDown = false; @@ -598,7 +598,7 @@ export class PinManagement { // 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..."); + this.parent.keyInsertion.updateFeedback("Key pin at shear line. Release now or continue to overpick..."); } // Check if 500ms have passed since reaching shear line @@ -618,7 +618,7 @@ export class PinManagement { } // Mark as overpicked and stuck - this.parent.updateFeedback("Set pin overpicked! Release tension to reset."); + this.parent.keyInsertion.updateFeedback("Set pin overpicked! Release tension to reset."); if (!pin.failureHighlight) { pin.failureHighlight = this.parent.scene.add.graphics(); pin.failureHighlight.fillStyle(0xff6600, 0.7); @@ -671,7 +671,7 @@ export class PinManagement { // 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)) { + if (this.parent.lockState.tensionApplied && (this.parent.gameUtil.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; @@ -720,7 +720,7 @@ export class PinManagement { } if (pin.isSet) { - this.parent.updateFeedback("Set pin overpicked! Release tension to reset."); + this.parent.keyInsertion.updateFeedback("Set pin overpicked! Release tension to reset."); // Show failure highlight for overpicked set pins if (!pin.failureHighlight) { @@ -734,7 +734,7 @@ export class PinManagement { // Hide set highlight if (pin.setHighlight) pin.setHighlight.setVisible(false); } else { - this.parent.updateFeedback("Pin overpicked! Release tension to reset."); + this.parent.keyInsertion.updateFeedback("Pin overpicked! Release tension to reset."); // Show overpicked highlight for regular pins if (!pin.overpickedHighlight) { @@ -767,7 +767,7 @@ export class PinManagement { // Update hook position to follow any moving pin if (pin.currentHeight > 0) { - this.parent.updateHookPosition(pin.index); + this.parent.hookMech.updateHookPosition(pin.index); } // Draw triangular bottom in pixel art style @@ -850,7 +850,7 @@ export class PinManagement { // 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); + const shouldFall = !this.parent.lockState.tensionApplied || (!this.parent.gameUtil.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) @@ -862,7 +862,7 @@ export class PinManagement { 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); + this.parent.hookMech.updateHookPosition(pin.index); // Draw triangular bottom in pixel art style pin.keyPin.fillRect(-12, -50 + pin.driverPinLength - pin.currentHeight + pin.keyPinLength - 8, 24, 2); @@ -995,7 +995,7 @@ export class PinManagement { }); this.parent.lockState.pinsSet = this.parent.pinCount; - this.parent.updateFeedback("All pins correctly positioned! Lock picked successfully!"); + this.parent.keyInsertion.updateFeedback("All pins correctly positioned! Lock picked successfully!"); this.parent.keyAnim.lockPickingSuccess(); } } diff --git a/js/minigames/lockpicking/pin-visuals.js b/js/minigames/lockpicking/pin-visuals.js new file mode 100644 index 0000000..22a21d3 --- /dev/null +++ b/js/minigames/lockpicking/pin-visuals.js @@ -0,0 +1,291 @@ + +/** + * PinVisuals + * + * Extracted from lockpicking-game-phaser.js + * Instantiate with: new PinVisuals(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 PinVisuals { + + constructor(parent) { + this.parent = parent; + } + + updatePinHighlighting(pin, distanceToShearLine, tolerance) { + // Update pin highlighting based on distance to shear line + // This provides visual feedback during key insertion + + // Create shear highlight if it doesn't exist + if (!pin.shearHighlight) { + pin.shearHighlight = this.parent.scene.add.graphics(); + pin.shearHighlight.fillStyle(0x00ff00, 0.4); // Green with transparency + pin.shearHighlight.fillRect(-22.5, -110, 45, 140); + pin.container.addAt(pin.shearHighlight, 0); // Add at beginning to appear behind pins + } + + // Hide all highlights first + pin.shearHighlight.setVisible(false); + if (pin.bindingHighlight) pin.bindingHighlight.setVisible(false); + if (pin.overpickedHighlight) pin.overpickedHighlight.setVisible(false); + if (pin.failureHighlight) pin.failureHighlight.setVisible(false); + + // Show green highlight only if pin is at shear line + if (distanceToShearLine <= tolerance) { + // Pin is at shear line - show green highlight + pin.shearHighlight.setVisible(true); + console.log(`Pin ${pin.index} showing GREEN highlight - distance: ${distanceToShearLine}`); + } else { + // Pin is not at shear line - no highlight + console.log(`Pin ${pin.index} NO highlight - distance: ${distanceToShearLine}`); + } + } + + updatePinVisuals(pin) { + console.log(`Updating pin ${pin.index} visuals - currentHeight: ${pin.currentHeight}`); + + // Update key pin visual + pin.keyPin.clear(); + pin.keyPin.fillStyle(0xdd3333); + + // Calculate new position based on currentHeight + // Add safety check for undefined properties + if (!pin.driverPinLength || !pin.keyPinLength) { + console.warn(`Pin ${pin.index} missing length properties in updatePinVisuals:`, pin); + return; // Skip this pin if properties are missing + } + const newKeyPinY = -50 + pin.driverPinLength - pin.currentHeight; + const keyPinTopY = newKeyPinY; + const keyPinBottomY = newKeyPinY + pin.keyPinLength; + const shearLineY = -45; + const distanceToShearLine = Math.abs(keyPinTopY - shearLineY); + + console.log(`Pin ${pin.index} final positioning: keyPinTopY=${keyPinTopY}, keyPinBottomY=${keyPinBottomY}, distanceToShearLine=${distanceToShearLine}`); + + // Draw rectangular part of key pin + pin.keyPin.fillRect(-12, -50 + pin.driverPinLength - pin.currentHeight, 24, pin.keyPinLength - 8); + + // 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); + + // Update driver pin visual + pin.driverPin.clear(); + pin.driverPin.fillStyle(0x3388dd); + pin.driverPin.fillRect(-12, -50 - pin.currentHeight, 24, pin.driverPinLength); + + // Update spring compression + pin.spring.clear(); + pin.spring.fillStyle(0x666666); + const springCompression = pin.currentHeight; + const compressionFactor = Math.max(0.3, 1 - (springCompression / 60)); + + const springTop = -130; + const driverPinTop = -50 - pin.currentHeight; + const springBottom = driverPinTop; + const springHeight = springBottom - springTop; + const totalSpringSpace = springHeight; + const segmentSpacing = totalSpringSpace / 11; + + for (let s = 0; s < 12; s++) { + const segmentHeight = 4 * compressionFactor; + const segmentY = springTop + (s * segmentSpacing); + + if (segmentY + segmentHeight <= springBottom) { + pin.spring.fillRect(-12, segmentY, 24, segmentHeight); + } + } + } + + checkPinSet(pin) { + // Check if the key/driver boundary is at the shear line + const boundaryPosition = -50 + pin.driverPinLength - pin.currentHeight; + const shearLineY = -45; // Shear line is at y=-45 (much higher position) + const distanceToShearLine = Math.abs(boundaryPosition - shearLineY); + const shouldBind = this.parent.gameUtil.shouldPinBind(pin); + + // Calculate threshold based on sensitivity (1-8) + // Higher sensitivity = smaller threshold (easier to set pins) + const baseThreshold = 8; + const sensitivityFactor = (9 - this.parent.thresholdSensitivity) / 8; // Invert so higher sensitivity = smaller threshold + const threshold = baseThreshold * sensitivityFactor; + + // Debug logging for threshold calculation + if (distanceToShearLine < threshold + 2) { // Log when close to threshold + console.log(`Pin ${pin.index + 1}: distance=${distanceToShearLine.toFixed(2)}, threshold=${threshold.toFixed(2)}, sensitivity=${this.parent.thresholdSensitivity}`); + } + + if (distanceToShearLine < threshold && shouldBind) { + // Pin set successfully + pin.isSet = true; + + // Set separate heights for key pin and driver pin + pin.keyPinHeight = 0; // Key pin drops back to original position + pin.driverPinHeight = 60; // Driver pin stays at shear line (60 units from base position) + + // Snap driver pin to shear line - calculate exact position + const shearLineY = -45; + const targetDriverBottom = shearLineY; + const driverPinTop = targetDriverBottom - pin.driverPinLength; + + // Update driver pin to snap to shear line + pin.driverPin.clear(); + pin.driverPin.fillStyle(0x3388dd); + pin.driverPin.fillRect(-12, driverPinTop, 24, pin.driverPinLength); + + // Reset key pin to original position (falls back down) + 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 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; + + for (let s = 0; s < 12; s++) { + const segmentHeight = 4; + const segmentSpacing = springHeight / 12; + + // Calculate segment position from bottom up to ensure bottom segment touches driver pin + const segmentY = springBottom - (segmentHeight + (11 - s) * segmentSpacing); + pin.spring.fillRect(-12, segmentY, 24, segmentHeight); + } + + // 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++; + + // Play set sound + if (this.parent.sounds.set) { + this.parent.sounds.set.play(); + if (typeof navigator !== 'undefined' && navigator.vibrate) { + navigator.vibrate(500); + } + } + + this.parent.keyInsertion.updateFeedback(`Pin ${pin.index + 1} set! (${this.parent.lockState.pinsSet}/${this.parent.pinCount})`); + this.parent.pinMgmt.updateBindingPins(); + + if (this.parent.lockState.pinsSet === this.parent.pinCount) { + this.parent.keyAnim.lockPickingSuccess(); + } + } else if (pin.isOverpicked) { + // Pin is overpicked - stays stuck until tension is released + if (pin.isSet) { + this.parent.keyInsertion.updateFeedback("Set pin overpicked! Release tension to reset."); + } else { + this.parent.keyInsertion.updateFeedback("Pin overpicked! Release tension to reset."); + } + } else if (pin.isSet) { + // Set pin: key pin falls back down, driver pin stays at shear line + pin.keyPinHeight = 0; // Key pin falls back to original position + pin.overpickingTimer = null; // Reset overpicking timer + + // Redraw key pin at 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); + + // Driver pin stays at shear line + pin.driverPin.clear(); + pin.driverPin.fillStyle(0x3388dd); + const shearLineY = -45; + const driverPinY = shearLineY - pin.driverPinLength; + pin.driverPin.fillRect(-12, driverPinY, 24, pin.driverPinLength); + + // Spring stays connected to driver pin at shear line + pin.spring.clear(); + pin.spring.fillStyle(0x666666); + const springTop = -130; + const springBottom = shearLineY - pin.driverPinLength; + const springHeight = springBottom - springTop; + const segmentSpacing = springHeight / 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); + } + } + } else { + // Normal pin falls back down due to gravity + pin.currentHeight = 0; + + // 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); + } + } + +} diff --git a/js/minigames/lockpicking/tool-manager.js b/js/minigames/lockpicking/tool-manager.js index b4bc4a6..ed2a67d 100644 --- a/js/minigames/lockpicking/tool-manager.js +++ b/js/minigames/lockpicking/tool-manager.js @@ -164,7 +164,7 @@ export class ToolManager { this.parent.lockConfig.resetPinsToOriginalPositions(); // Update feedback - this.parent.updateFeedback("Lockpicking mode - Apply tension first, then lift pins in binding order"); + this.parent.keyInsertion.updateFeedback("Lockpicking mode - Apply tension first, then lift pins in binding order"); } showLockpickingTools() { @@ -239,7 +239,7 @@ export class ToolManager { switchModeBtn.className = 'minigame-button'; switchModeBtn.id = 'lockpicking-switch-mode-btn'; switchModeBtn.innerHTML = 'Lockpick Switch to Lockpicking'; - switchModeBtn.onclick = () => this.parent.switchToPickMode(); + switchModeBtn.onclick = () => this.switchToPickMode(); buttonContainer.appendChild(switchModeBtn); itemDisplayDiv.appendChild(buttonContainer); @@ -249,9 +249,9 @@ export class ToolManager { // 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"); + this.parent.keyInsertion.updateFeedback("Select a key to use"); } else { - this.parent.updateFeedback("No keys available"); + this.parent.keyInsertion.updateFeedback("No keys available"); } }