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.
This commit is contained in:
Z. Cliffe Schreuders
2025-10-27 16:53:18 +00:00
parent f27ad53cc2
commit 741ec55864
17 changed files with 1498 additions and 1303 deletions

View File

@@ -1,3 +1,4 @@
{
"cursor.general.disableHttp2": true
"cursor.general.disableHttp2": true,
"chat.agent.maxRequests": 50
}

View File

@@ -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;
}
}

View File

@@ -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}`);
}
}

View File

@@ -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 => {

View File

@@ -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);
}
}
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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(() => {

View File

@@ -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));
}
}
}

View File

@@ -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
}
}

View File

@@ -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);

View File

@@ -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');

View File

@@ -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) {

File diff suppressed because it is too large Load Diff

View File

@@ -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();
}
}

View File

@@ -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);
}
}
}

View File

@@ -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 = '<img src="assets/objects/lockpick.png" alt="Lockpick" class="icon-large"> 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");
}
}