mirror of
https://github.com/cliffe/BreakEscape.git
synced 2026-02-21 11:18:08 +00:00
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:
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"cursor.general.disableHttp2": true
|
||||
"cursor.general.disableHttp2": true,
|
||||
"chat.agent.maxRequests": 50
|
||||
}
|
||||
42
js/minigames/lockpicking/game-utilities.js
Normal file
42
js/minigames/lockpicking/game-utilities.js
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
204
js/minigames/lockpicking/hook-mechanics.js
Normal file
204
js/minigames/lockpicking/hook-mechanics.js
Normal 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}`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 => {
|
||||
|
||||
209
js/minigames/lockpicking/key-drawing.js
Normal file
209
js/minigames/lockpicking/key-drawing.js
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
152
js/minigames/lockpicking/key-geometry.js
Normal file
152
js/minigames/lockpicking/key-geometry.js
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
107
js/minigames/lockpicking/key-insertion.js
Normal file
107
js/minigames/lockpicking/key-insertion.js
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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(() => {
|
||||
|
||||
192
js/minigames/lockpicking/key-path-drawing.js
Normal file
192
js/minigames/lockpicking/key-path-drawing.js
Normal 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));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
219
js/minigames/lockpicking/key-point-geometry.js
Normal file
219
js/minigames/lockpicking/key-point-geometry.js
Normal 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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
291
js/minigames/lockpicking/pin-visuals.js
Normal file
291
js/minigames/lockpicking/pin-visuals.js
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user