feat: Add ToolManager class for managing lockpicking tools and modes

- Implemented ToolManager to handle the visibility and behavior of lockpicking tools.
- Added methods for switching between key mode and lockpicking mode.
- Included functionality to return hook to starting position and flash wrench red.
- Integrated cleanup and initialization processes for the lockpicking game state.
- Enhanced user feedback and UI updates during mode transitions.
This commit is contained in:
Z. Cliffe Schreuders
2025-10-27 16:34:48 +00:00
parent d9703930e1
commit f27ad53cc2
6 changed files with 1992 additions and 1920 deletions

View File

@@ -0,0 +1,601 @@
/**
* KeyAnimation
*
* Extracted from lockpicking-game-phaser.js
* Instantiate with: new KeyAnimation(this)
*
* All 'this' references replaced with 'this.parent' to access parent instance state:
* - this.parent.pins (array of pin objects)
* - this.parent.scene (Phaser scene)
* - this.parent.lockId (lock identifier)
* - this.parent.lockState (lock state object)
* etc.
*/
export class KeyAnimation {
constructor(parent) {
this.parent = parent;
}
snapPinsToExactPositions() {
// Use selected key data for visual positioning, but original key data for correctness
const keyDataToUse = this.parent.selectedKeyData || this.parent.keyData;
if (!keyDataToUse || !keyDataToUse.cuts) return;
console.log('Snapping pins to exact positions based on key cuts for shear line alignment');
// Ensure key data matches lock pin count
if (keyDataToUse.cuts.length !== this.parent.pinCount) {
console.warn(`Key has ${keyDataToUse.cuts.length} cuts but lock has ${this.parent.pinCount} pins. Adjusting key data.`);
// Truncate or pad cuts to match pin count
if (keyDataToUse.cuts.length > this.parent.pinCount) {
keyDataToUse.cuts = keyDataToUse.cuts.slice(0, this.parent.pinCount);
} else {
// Pad with default cuts if key has fewer cuts than lock has pins
while (keyDataToUse.cuts.length < this.parent.pinCount) {
keyDataToUse.cuts.push(40); // Default cut depth
}
}
}
// Set each pin to the exact final position based on key cut dimensions
keyDataToUse.cuts.forEach((cutDepth, index) => {
if (index >= this.parent.pinCount) {
console.warn(`Key has ${keyDataToUse.cuts.length} cuts but lock only has ${this.parent.pinCount} pins. Skipping cut ${index}.`);
return;
}
const pin = this.parent.pins[index];
if (!pin) {
console.error(`Pin at index ${index} is undefined. Available pins: ${this.parent.pins.length}`);
return;
}
// Calculate the exact position where the pin should rest on the key cut
// The cut depth represents how deep the cut is from the blade top
// We need to position the pin so its bottom rests exactly on the cut surface
// Key blade dimensions
const bladeHeight = this.parent.keyConfig.bladeHeight;
const keyBladeBaseY = this.parent.keyGroup.y - bladeHeight / 2;
// Calculate the Y position of the cut surface
const cutSurfaceY = keyBladeBaseY + cutDepth;
// Calculate where the pin bottom should be to rest on the cut surface
// Add safety check for undefined properties
if (!pin.driverPinLength || !pin.keyPinLength) {
console.warn(`Pin ${pin.index} missing length properties:`, pin);
return; // Skip this pin if properties are missing
}
const pinRestY = 200 - 50 + pin.driverPinLength + pin.keyPinLength; // Pin rest position
const targetKeyPinBottom = cutSurfaceY;
// Calculate the exact lift needed to move pin bottom from rest to cut surface
const exactLift = pinRestY - targetKeyPinBottom;
// Snap to exact position
pin.currentHeight = Math.max(0, exactLift);
// Update pin visuals immediately
this.parent.updatePinVisuals(pin);
console.log(`Pin ${index}: cutDepth=${cutDepth}, cutSurfaceY=${cutSurfaceY}, exactLift=${exactLift}, currentHeight=${pin.currentHeight}, keyBladeBaseY=${keyBladeBaseY}, bladeHeight=${bladeHeight}`);
});
// Note: Rotation animation will be triggered by checkKeyCorrectness() only if key is correct
}
startKeyRotationAnimationWithChamberHoles() {
// Animation configuration variables - same as lockpicking success
const KEY_PIN_TOP_SHRINK = 10; // How much the key pin top moves down
const KEY_PIN_BOTTOM_SHRINK = 5; // How much the key pin bottom moves up
const KEY_PIN_TOTAL_SHRINK = KEY_PIN_TOP_SHRINK + KEY_PIN_BOTTOM_SHRINK; // Total key pin shrink
const CHANNEL_MOVEMENT = 25; // How much channels move down
const KEYWAY_SHRINK = 20; // How much keyway shrinks
const KEY_SHRINK_FACTOR = 0.7; // How much the key shrinks on Y axis to simulate rotation
// Play success sound
if (this.parent.sounds.success) {
this.parent.sounds.success.play();
if (typeof navigator !== 'undefined' && navigator.vibrate) {
navigator.vibrate(500);
}
}
this.parent.updateFeedback("Key inserted successfully! Lock turning...");
// Create upper edge effect - a copy of the entire key group that stays in place
// Position at the key's current position (after insertion, before rotation)
const upperEdgeKeyGroup = this.parent.scene.add.container(this.parent.keyGroup.x, this.parent.keyGroup.y);
upperEdgeKeyGroup.setDepth(0); // Behind the original key
// Copy the handle (circle)
const upperEdgeHandle = this.parent.scene.add.graphics();
upperEdgeHandle.fillStyle(0xaaaaaa); // Slightly darker tone for the upper edge
upperEdgeHandle.fillCircle(this.parent.keyConfig.circleRadius, 0, this.parent.keyConfig.circleRadius);
upperEdgeKeyGroup.add(upperEdgeHandle);
// Copy the shoulder and blade using render texture
const upperEdgeRenderTexture = this.parent.scene.add.renderTexture(0, 0, this.parent.keyRenderTexture.width, this.parent.keyRenderTexture.height);
upperEdgeRenderTexture.setTint(0xaaaaaa); // Apply darker tone
upperEdgeRenderTexture.setOrigin(0, 0.5); // Match the original key's origin
upperEdgeKeyGroup.add(upperEdgeRenderTexture);
// Draw the shoulder and blade to the upper edge render texture
const upperEdgeGraphics = this.parent.scene.add.graphics();
upperEdgeGraphics.fillStyle(0xaaaaaa); // Slightly darker tone
// Draw shoulder
const shoulderX = this.parent.keyConfig.circleRadius * 1.9;
upperEdgeGraphics.fillRect(shoulderX, 0, this.parent.keyConfig.shoulderWidth, this.parent.keyConfig.shoulderHeight);
// Draw blade - adjust Y position to account for container offset
const bladeX = shoulderX + this.parent.keyConfig.shoulderWidth;
const bladeY = this.parent.keyConfig.shoulderHeight/2 - this.parent.keyConfig.bladeHeight/2;
this.parent.drawKeyBladeAsSolidShape(upperEdgeGraphics, bladeX, bladeY, this.parent.keyConfig.bladeWidth, this.parent.keyConfig.bladeHeight);
upperEdgeRenderTexture.draw(upperEdgeGraphics);
upperEdgeGraphics.destroy();
// Initially hide the upper edge
upperEdgeKeyGroup.setVisible(false);
// Animate key shrinking on Y axis to simulate rotation
this.parent.scene.tweens.add({
targets: this.parent.keyGroup,
scaleY: KEY_SHRINK_FACTOR,
duration: 1400,
ease: 'Cubic.easeInOut',
onStart: () => {
// Show the upper edge when rotation starts
upperEdgeKeyGroup.setVisible(true);
}
});
// Animate the upper edge copy to shrink and move upward (keeping top edge in place)
this.parent.scene.tweens.add({
targets: upperEdgeKeyGroup,
scaleY: KEY_SHRINK_FACTOR,
y: upperEdgeKeyGroup.y - 6, // Simple upward movement
duration: 1400,
ease: 'Cubic.easeInOut'
});
// Shrink key pins downward and add half circles to simulate cylinder rotation
this.parent.pins.forEach(pin => {
// Hide all highlights
if (pin.shearHighlight) pin.shearHighlight.setVisible(false);
if (pin.setHighlight) pin.setHighlight.setVisible(false);
if (pin.bindingHighlight) pin.bindingHighlight.setVisible(false);
if (pin.overpickedHighlight) pin.overpickedHighlight.setVisible(false);
if (pin.failureHighlight) pin.failureHighlight.setVisible(false);
// Create chamber hole circle that expands at the actual chamber position
const chamberCircle = this.parent.scene.add.graphics();
chamberCircle.fillStyle(0x666666); // Dark gray color for chamber holes
chamberCircle.x = pin.x; // Center horizontally on the pin
// Position at actual chamber hole location (shear line)
const chamberY = pin.y + (-45); // Shear line position
chamberCircle.y = chamberY;
chamberCircle.setDepth(5); // Above all other elements
// Create a temporary object to hold the circle expansion data
const circleData = {
width: 24, // Start full width (same as key pin)
height: 2, // Start very thin (flat top)
y: chamberY
};
// Animate the chamber hole circle expanding to full circle (stays at chamber position)
this.parent.scene.tweens.add({
targets: circleData,
width: 24, // Full circle width (stays same)
height: 16, // Full circle height (expands from 2 to 16)
y: chamberY, // Stay at the chamber position (no movement)
duration: 1400,
ease: 'Cubic.easeInOut',
onUpdate: function() {
chamberCircle.clear();
chamberCircle.fillStyle(0xff0000); // Light red for chamber holes filled with key pin
// Calculate animation progress (0 to 1)
const progress = (circleData.height - 2) / (16 - 2); // From 2 to 16 height
// Draw different circle shapes based on progress (widest in middle)
if (progress < 0.1) {
// Start: just a thin line (flat top)
chamberCircle.fillRect(-12, 0, 24, 2);
} else if (progress < 0.3) {
// Early: thin oval with middle bulge
chamberCircle.fillRect(-8, 0, 16, 2); // narrow top
chamberCircle.fillRect(-12, 2, 24, 2); // wide middle
chamberCircle.fillRect(-8, 4, 16, 2); // narrow bottom
} else if (progress < 0.5) {
// Middle: growing circle with middle bulge
chamberCircle.fillRect(-6, 0, 12, 2); // narrow top
chamberCircle.fillRect(-10, 2, 20, 2); // wider
chamberCircle.fillRect(-12, 4, 24, 2); // widest middle
chamberCircle.fillRect(-10, 6, 20, 2); // wider
chamberCircle.fillRect(-6, 8, 12, 2); // narrow bottom
} else if (progress < 0.7) {
// Later: more circle-like with middle bulge
chamberCircle.fillRect(-4, 0, 8, 2); // narrow top
chamberCircle.fillRect(-8, 2, 16, 2); // wider
chamberCircle.fillRect(-12, 4, 24, 2); // widest middle
chamberCircle.fillRect(-12, 6, 24, 2); // widest middle
chamberCircle.fillRect(-8, 8, 16, 2); // wider
chamberCircle.fillRect(-4, 10, 8, 2); // narrow bottom
} else if (progress < 0.9) {
// Almost full: near complete circle
chamberCircle.fillRect(-2, 0, 4, 2); // narrow top
chamberCircle.fillRect(-6, 2, 12, 2); // wider
chamberCircle.fillRect(-10, 4, 20, 2); // wider
chamberCircle.fillRect(-12, 6, 24, 2); // widest middle
chamberCircle.fillRect(-12, 8, 24, 2); // widest middle
chamberCircle.fillRect(-10, 10, 20, 2); // wider
chamberCircle.fillRect(-6, 12, 12, 2); // wider
chamberCircle.fillRect(-2, 14, 4, 2); // narrow bottom
} else {
// Full: complete pixel art circle
chamberCircle.fillRect(-2, 0, 4, 2); // narrow top
chamberCircle.fillRect(-6, 2, 12, 2); // wider
chamberCircle.fillRect(-10, 4, 20, 2); // wider
chamberCircle.fillRect(-12, 6, 24, 2); // widest middle
chamberCircle.fillRect(-12, 8, 24, 2); // widest middle
chamberCircle.fillRect(-10, 10, 20, 2); // wider
chamberCircle.fillRect(-6, 12, 12, 2); // wider
chamberCircle.fillRect(-2, 14, 4, 2); // narrow bottom
}
// Update position
chamberCircle.y = circleData.y;
}
});
// Animate key pin moving down as a unit (staying connected to chamber hole)
const keyPinData = {
yOffset: 0 // How much the entire key pin moves down
};
this.parent.scene.tweens.add({
targets: keyPinData,
yOffset: KEY_PIN_TOP_SHRINK, // Move entire key pin down
duration: 1400,
ease: 'Cubic.easeInOut',
onUpdate: function() {
pin.keyPin.clear();
pin.keyPin.fillStyle(0xdd3333);
// Calculate position: entire key pin moves down as a unit
const originalTopY = -50 + pin.driverPinLength - pin.currentHeight; // Current top position (at shear line)
const newTopY = originalTopY + keyPinData.yOffset; // Entire key pin moves down
const newBottomY = newTopY + pin.keyPinLength; // Bottom position
// Draw rectangular part of key pin (moves down as unit)
pin.keyPin.fillRect(-12, newTopY, 24, pin.keyPinLength - 8);
// Draw triangular bottom in pixel art style (moves down with key pin)
pin.keyPin.fillRect(-12, newBottomY - 8, 24, 2);
pin.keyPin.fillRect(-10, newBottomY - 6, 20, 2);
pin.keyPin.fillRect(-8, newBottomY - 4, 16, 2);
pin.keyPin.fillRect(-6, newBottomY - 2, 12, 2);
}
});
// Animate key pin channel rectangle moving down with the channel circles
if (pin.channelRect) {
this.parent.scene.tweens.add({
targets: pin.channelRect,
y: pin.channelRect.y + CHANNEL_MOVEMENT, // Move down by channel movement amount
duration: 1400,
ease: 'Cubic.easeInOut'
});
}
});
// Animate the keyway shrinking (keeping bottom in place) to make cylinder appear to grow
const keywayData = { height: 90 };
this.parent.scene.tweens.add({
targets: keywayData,
height: 90 - KEYWAY_SHRINK, // Shrink by keyway shrink amount
duration: 1400,
ease: 'Cubic.easeInOut',
onUpdate: function() {
// Update keyway visual to show shrinking
// This would need to be implemented based on how the keyway is drawn
}
});
}
liftPinsWithKey() {
if (!this.parent.keyData || !this.parent.keyData.cuts) return;
// Lift each pin to the correct height based on key cuts
this.parent.keyData.cuts.forEach((cutDepth, index) => {
if (index >= this.parent.pinCount) return;
const pin = this.parent.pins[index];
// Calculate the height needed to lift the pin so it aligns at the shear line
const shearLineY = -45; // Shear line position
const keyPinTopAtShearLine = shearLineY; // Key pin top should be at shear line
const keyPinBottomAtRest = -50 + pin.driverPinLength + pin.keyPinLength; // Key pin bottom when not lifted
const requiredLift = keyPinBottomAtRest - keyPinTopAtShearLine; // How much to lift
// The cut depth should match the required lift
const maxLift = pin.keyPinLength; // Maximum possible lift (full key pin height)
const requiredCutDepth = (requiredLift / maxLift) * 100; // Convert to percentage
// Calculate the actual lift based on the key cut depth
const actualLift = (cutDepth / 100) * maxLift;
// Animate pin to correct position
this.parent.scene.tweens.add({
targets: { height: 0 },
height: actualLift,
duration: 500,
ease: 'Cubic.easeOut',
onUpdate: (tween) => {
pin.currentHeight = tween.targets[0].height;
this.parent.updatePinVisuals(pin);
}
});
});
}
lockPickingSuccess() {
// Animation configuration variables - easy to tweak
const KEY_PIN_TOP_SHRINK = 10; // How much the key pin top moves down
const KEY_PIN_BOTTOM_SHRINK = 5; // How much the key pin bottom moves up
const KEY_PIN_TOTAL_SHRINK = KEY_PIN_TOP_SHRINK + KEY_PIN_BOTTOM_SHRINK; // Total key pin shrink
const CHANNEL_MOVEMENT = 25; // How much channels move down
const KEYWAY_SHRINK = 20; // How much keyway shrinks
const WRENCH_VERTICAL_SHRINK = 60; // How much wrench vertical arm shrinks
const WRENCH_HORIZONTAL_SHRINK = 5; // How much wrench horizontal arm gets thinner
const WRENCH_MOVEMENT = 10; // How much wrench moves down
this.parent.gameState.isActive = false;
// Play success sound
if (this.parent.sounds.success) {
this.parent.sounds.success.play();
if (typeof navigator !== 'undefined' && navigator.vibrate) {
navigator.vibrate(500);
}
}
this.parent.updateFeedback("Lock picked successfully!");
// Shrink key pins downward and add half circles to simulate cylinder rotation
this.parent.pins.forEach(pin => {
// Hide all highlights
if (pin.shearHighlight) pin.shearHighlight.setVisible(false);
if (pin.setHighlight) pin.setHighlight.setVisible(false);
if (pin.bindingHighlight) pin.bindingHighlight.setVisible(false);
if (pin.overpickedHighlight) pin.overpickedHighlight.setVisible(false);
if (pin.failureHighlight) pin.failureHighlight.setVisible(false);
// Create squashed circle that expands and moves to stay aligned with key pin top
const squashedCircle = this.parent.scene.add.graphics();
//was 0xdd3333 Red color (key pin color)
squashedCircle.fillStyle(0xffffff); // white color for testing purposes
squashedCircle.x = pin.x; // Center horizontally on the pin
// Start position: aligned with the top of the key pin
const startTopY = pin.y + (-50 + pin.driverPinLength); // Top of key pin position
squashedCircle.y = startTopY;
squashedCircle.setDepth(3); // Above driver pins so they're visible
// Create a temporary object to hold the circle expansion data
const circleData = {
width: 24, // Start full width (same as key pin)
height: 2, // Start very thin (flat top)
y: startTopY
};
// Animate the squashed circle expanding to full circle (stays at top of key pin)
this.parent.scene.tweens.add({
targets: circleData,
width: 24, // Full circle width (stays same)
height: 16, // Full circle height (expands from 2 to 16)
y: startTopY, // Stay at the top of the key pin (no movement)
duration: 1400,
ease: 'Cubic.easeInOut',
onUpdate: function() {
squashedCircle.clear();
squashedCircle.fillStyle(0xff3333); // Red color (key pin color)
// Calculate animation progress (0 to 1)
const progress = (circleData.height - 2) / (16 - 2); // From 2 to 16 height
// Draw different circle shapes based on progress (widest in middle)
if (progress < 0.1) {
// Start: just a thin line (flat top)
squashedCircle.fillRect(-12, 0, 24, 2);
} else if (progress < 0.3) {
// Early: thin oval with middle bulge
squashedCircle.fillRect(-8, 0, 16, 2); // narrow top
squashedCircle.fillRect(-12, 2, 24, 2); // wide middle
squashedCircle.fillRect(-8, 4, 16, 2); // narrow bottom
} else if (progress < 0.5) {
// Middle: growing circle with middle bulge
squashedCircle.fillRect(-6, 0, 12, 2); // narrow top
squashedCircle.fillRect(-10, 2, 20, 2); // wider
squashedCircle.fillRect(-12, 4, 24, 2); // widest middle
squashedCircle.fillRect(-10, 6, 20, 2); // wider
squashedCircle.fillRect(-6, 8, 12, 2); // narrow bottom
} else if (progress < 0.7) {
// Later: more circle-like with middle bulge
squashedCircle.fillRect(-4, 0, 8, 2); // narrow top
squashedCircle.fillRect(-8, 2, 16, 2); // wider
squashedCircle.fillRect(-12, 4, 24, 2); // widest middle
squashedCircle.fillRect(-12, 6, 24, 2); // widest middle
squashedCircle.fillRect(-8, 8, 16, 2); // wider
squashedCircle.fillRect(-4, 10, 8, 2); // narrow bottom
} else if (progress < 0.9) {
// Almost full: near complete circle
squashedCircle.fillRect(-2, 0, 4, 2); // narrow top
squashedCircle.fillRect(-6, 2, 12, 2); // wider
squashedCircle.fillRect(-10, 4, 20, 2); // wider
squashedCircle.fillRect(-12, 6, 24, 2); // widest middle
squashedCircle.fillRect(-12, 8, 24, 2); // widest middle
squashedCircle.fillRect(-10, 10, 20, 2); // wider
squashedCircle.fillRect(-6, 12, 12, 2); // wider
squashedCircle.fillRect(-2, 14, 4, 2); // narrow bottom
} else {
// Full: complete pixel art circle
squashedCircle.fillRect(-2, 0, 4, 2); // narrow top
squashedCircle.fillRect(-6, 2, 12, 2); // wider
squashedCircle.fillRect(-10, 4, 20, 2); // wider
squashedCircle.fillRect(-12, 6, 24, 2); // widest middle
squashedCircle.fillRect(-12, 8, 24, 2); // widest middle
squashedCircle.fillRect(-10, 10, 20, 2); // wider
squashedCircle.fillRect(-6, 12, 12, 2); // wider
squashedCircle.fillRect(-2, 14, 4, 2); // narrow bottom
}
// Update position
squashedCircle.y = circleData.y;
}
});
// Animate key pin shrinking from both top and bottom
const keyPinData = { height: pin.keyPinLength, topOffset: 0 };
this.parent.scene.tweens.add({
targets: keyPinData,
height: pin.keyPinLength - KEY_PIN_TOTAL_SHRINK, // Shrink by total amount
topOffset: KEY_PIN_TOP_SHRINK, // Move top down
duration: 1400,
ease: 'Cubic.easeInOut',
onUpdate: function() {
pin.keyPin.clear();
pin.keyPin.fillStyle(0xdd3333);
// Calculate new position: top moves down, bottom moves up
const originalTopY = -50 + pin.driverPinLength; // Original top of key pin
const newTopY = originalTopY + keyPinData.topOffset; // Top moves down
const newBottomY = newTopY + keyPinData.height; // Bottom position
// Draw rectangular part of key pin (shrunk from both ends)
pin.keyPin.fillRect(-12, newTopY, 24, keyPinData.height - 8);
// Draw triangular bottom in pixel art style (bottom moves up)
pin.keyPin.fillRect(-12, newBottomY - 8, 24, 2);
pin.keyPin.fillRect(-10, newBottomY - 6, 20, 2);
pin.keyPin.fillRect(-8, newBottomY - 4, 16, 2);
pin.keyPin.fillRect(-6, newBottomY - 2, 12, 2);
}
});
// Animate key pin channel rectangle moving down with the channel circles
this.parent.scene.tweens.add({
targets: pin.channelRect,
y: pin.channelRect.y + CHANNEL_MOVEMENT, // Move down by channel movement amount
duration: 1400,
ease: 'Cubic.easeInOut'
});
});
// Animate the keyway shrinking (keeping bottom in place) to make cylinder appear to grow
// Create a temporary object to hold the height value for tweening
const keywayData = { height: 90 };
this.parent.scene.tweens.add({
targets: keywayData,
height: 90 - KEYWAY_SHRINK, // Shrink by keyway shrink amount
duration: 1400,
ease: 'Cubic.easeInOut',
onUpdate: function() {
this.parent.keywayGraphics.clear();
this.parent.keywayGraphics.fillStyle(0x2a2a2a);
// Move top down: y increases as height shrinks, keeping bottom at y=290
const newY = 200 + (90 - keywayData.height); // Move top down
this.parent.keywayGraphics.fillRect(100, newY, 400, keywayData.height);
this.parent.keywayGraphics.lineStyle(1, 0x1a1a1a);
this.parent.keywayGraphics.strokeRect(100, newY, 400, keywayData.height);
}.bind(this)
});
// Animate tension wrench shrinking and moving down
if (this.parent.tensionWrench) {
// Create a temporary object to hold the height value for tweening
const wrenchData = { height: 170, y: 0, horizontalHeight: 10 }; // Original vertical arm height, y offset, and horizontal arm height
this.parent.scene.tweens.add({
targets: wrenchData,
height: 170 - WRENCH_VERTICAL_SHRINK, // Shrink by vertical shrink amount
y: WRENCH_MOVEMENT, // Move entire wrench down
horizontalHeight: 10 - WRENCH_HORIZONTAL_SHRINK, // Make horizontal arm thinner
duration: 1400,
ease: 'Cubic.easeInOut',
onUpdate: function() {
// Update the wrench graphics (both active and inactive states)
this.parent.wrenchGraphics.clear();
this.parent.wrenchGraphics.fillStyle(this.parent.lockState.tensionApplied ? 0x00ff00 : 0x888888);
// Calculate new top position (move top down as height shrinks)
const originalTop = -120; // Original top position
const newTop = originalTop + (170 - wrenchData.height) + wrenchData.y; // Move top down and add y offset
// Long vertical arm (left side of L) - top moves down and shrinks
this.parent.wrenchGraphics.fillRect(0, newTop, 10, wrenchData.height);
// Short horizontal arm (bottom of L) - also moves down with top and gets thinner
this.parent.wrenchGraphics.fillRect(0, newTop + wrenchData.height, 37.5, wrenchData.horizontalHeight);
}.bind(this)
});
}
// Channel rectangles are already created during initial render
// Animate pixel-art circles (channels) moving down from above the shear line
this.parent.pins.forEach(pin => {
// Calculate starting position: above the shear line (behind driver pins)
const pinX = pin.x;
const pinY = pin.y;
const shearLineY = -45; // Shear line position
const circleStartY = pinY + shearLineY - 20; // Start above shear line
const circleEndY = circleStartY + CHANNEL_MOVEMENT; // Move down same distance as cylinder
// Create pixel-art circle graphics
const channelCircle = this.parent.scene.add.graphics();
channelCircle.x = pinX;
channelCircle.y = circleStartY;
// Pixel-art circle: red color (like key pins)
const color = 0x333333; // Red color (key pin color)
channelCircle.fillStyle(color, 1);
// Create a proper circle shape with pixel-art steps (middle widest)
channelCircle.fillRect(-6, 0, 12, 2); // bottom (narrowest)
channelCircle.fillRect(-8, 2, 16, 2); // wider
channelCircle.fillRect(-10, 4, 20, 2); // wider
channelCircle.fillRect(-12, 6, 24, 2); // widest (middle)
channelCircle.fillRect(-12, 8, 24, 2); // widest (middle)
channelCircle.fillRect(-10, 10, 20, 2); // narrower
channelCircle.fillRect(-8, 12, 16, 2); // narrower
channelCircle.fillRect(-6, 14, 12, 2); // top (narrowest)
channelCircle.setDepth(1); // Normal depth for circles
// Animate the circle moving down
this.parent.scene.tweens.add({
targets: channelCircle,
y: circleEndY,
duration: 1400,
ease: 'Cubic.easeInOut',
});
});
// Show success message immediately but delay the game completion
const successHTML = `
<div style="font-weight: bold; font-size: 18px; margin-bottom: 10px;">Lock picked successfully!</div>
`;
// this.showSuccess(successHTML, false, 2000);
// Delay the actual game completion until animation finishes
setTimeout(() => {
// Now trigger the success callback that unlocks the game
this.parent.showSuccess(successHTML, true, 2000);
this.parent.gameResult = { lockable: this.parent.lockable };
}, 1500); // Wait 1.5 seconds (slightly longer than animation duration)
}
}

View File

@@ -173,7 +173,7 @@ export class KeyOperations {
this.parent.keyInsertionProgress = 1.0; // Fully inserted
// Snap pins to exact final positions based on key cut dimensions
this.parent.snapPinsToExactPositions();
this.parent.keyAnim.snapPinsToExactPositions();
this.checkKeyCorrectness();
}
@@ -214,7 +214,7 @@ export class KeyOperations {
// Start the rotation animation for correct key
this.parent.scene.time.delayedCall(500, () => {
this.parent.startKeyRotationAnimationWithChamberHoles();
this.parent.keyAnim.startKeyRotationAnimationWithChamberHoles();
});
// Complete the minigame after rotation animation

View File

@@ -172,7 +172,7 @@ export class LockGraphics {
this.parent.lockState.pinsSet = 0;
}
this.parent.updateBindingPins();
this.parent.pinMgmt.updateBindingPins();
});
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,258 @@
/**
* ToolManager
*
* Extracted from lockpicking-game-phaser.js
* Instantiate with: new ToolManager(this)
*
* All 'this' references replaced with 'this.parent' to access parent instance state:
* - this.parent.pins (array of pin objects)
* - this.parent.scene (Phaser scene)
* - this.parent.lockId (lock identifier)
* - this.parent.lockState (lock state object)
* etc.
*/
export class ToolManager {
constructor(parent) {
this.parent = parent;
}
hideLockpickingTools() {
// Hide tension wrench and hook pick in key mode
if (this.parent.tensionWrench) {
this.parent.tensionWrench.setVisible(false);
}
if (this.parent.hookGroup) {
this.parent.hookGroup.setVisible(false);
}
// Hide labels
if (this.parent.wrenchText) {
this.parent.wrenchText.setVisible(false);
}
if (this.parent.hookPickLabel) {
this.parent.hookPickLabel.setVisible(false);
}
}
returnHookToStart() {
if (!this.parent.hookGroup || !this.parent.hookConfig) return;
const config = this.parent.hookConfig;
console.log('Returning hook to starting position (no rotation)');
// Get the current X position from the last targeted pin
const pinSpacing = 400 / (this.parent.pinCount + 1);
const margin = pinSpacing * 0.75;
const targetPinIndex = config.lastTargetedPin;
const currentX = 100 + margin + targetPinIndex * pinSpacing; // Last targeted pin's X position
// Calculate the tip position for the current pin
const totalHookHeight = (config.diagonalSegments + config.verticalSegments) * config.segmentStep;
const tipX = currentX - totalHookHeight + 48; // Add 48px offset (24px + 24px further right)
// Calculate resting Y position (a few pixels lower than original)
const restingY = config.hookStartY - 24; // 24px lower than original position (was 15px)
// Reset position and rotation
this.parent.hookGroup.x = tipX;
this.parent.hookGroup.y = restingY;
this.parent.hookGroup.setAngle(0);
// Clear debug graphics when hook returns to start
if (this.parent.debugGraphics) {
this.parent.debugGraphics.clear();
}
}
start() {
super.start();
this.parent.gameState.isActive = true;
this.parent.lockState.tensionApplied = false;
this.parent.lockState.pinsSet = 0;
this.parent.updateProgress(0, this.parent.pinCount);
}
cleanup() {
if (this.parent.game) {
this.parent.game.destroy(true);
this.parent.game = null;
}
super.cleanup();
}
flashWrenchRed() {
// Flash the tension wrench red to indicate tension is needed
if (!this.parent.wrenchGraphics) return;
const originalFillStyle = this.parent.lockState.tensionApplied ? 0x00ff00 : 0x888888;
// Store original state
const originalClear = this.parent.wrenchGraphics.clear.bind(this.parent.wrenchGraphics);
// Flash red 3 times
for (let i = 0; i < 3; i++) {
this.parent.scene.time.delayedCall(i * 150, () => {
this.parent.wrenchGraphics.clear();
this.parent.wrenchGraphics.fillStyle(0xff0000); // Red
// Long vertical arm
this.parent.wrenchGraphics.fillRect(0, -120, 10, 170);
// Short horizontal arm
this.parent.wrenchGraphics.fillRect(0, 40, 37.5, 10);
});
this.parent.scene.time.delayedCall(i * 150 + 75, () => {
this.parent.wrenchGraphics.clear();
this.parent.wrenchGraphics.fillStyle(originalFillStyle); // Back to original color
// Long vertical arm
this.parent.wrenchGraphics.fillRect(0, -120, 10, 170);
// Short horizontal arm
this.parent.wrenchGraphics.fillRect(0, 40, 37.5, 10);
});
}
}
switchToPickMode() {
// Switch from key selection mode to lockpicking mode
console.log('Switching from key mode to lockpicking mode');
// Hide the mode switch button
const switchBtn = document.getElementById('lockpicking-switch-mode-btn');
if (switchBtn) {
switchBtn.style.display = 'none';
}
// Exit key mode
this.parent.keyMode = false;
this.parent.keySelectionMode = false;
// Clean up key selection UI if visible
if (this.parent.keySelectionContainer) {
this.parent.keySelectionContainer.destroy();
this.parent.keySelectionContainer = null;
}
// Clean up any key visuals
if (this.parent.keyGroup) {
this.parent.keyGroup.destroy();
this.parent.keyGroup = null;
}
if (this.parent.keyClickZone) {
this.parent.keyClickZone.destroy();
this.parent.keyClickZone = null;
}
// Show lockpicking tools
if (this.parent.tensionWrench) {
this.parent.tensionWrench.setVisible(true);
}
if (this.parent.hookGroup) {
this.parent.hookGroup.setVisible(true);
}
if (this.parent.wrenchText) {
this.parent.wrenchText.setVisible(true);
}
if (this.parent.hookPickLabel) {
this.parent.hookPickLabel.setVisible(true);
}
// Reset pins to original positions
this.parent.lockConfig.resetPinsToOriginalPositions();
// Update feedback
this.parent.updateFeedback("Lockpicking mode - Apply tension first, then lift pins in binding order");
}
showLockpickingTools() {
// Show tension wrench and hook pick in lockpicking mode
if (this.parent.tensionWrench) {
this.parent.tensionWrench.setVisible(true);
}
if (this.parent.hookGroup) {
this.parent.hookGroup.setVisible(true);
}
// Show labels
if (this.parent.wrenchText) {
this.parent.wrenchText.setVisible(true);
}
if (this.parent.hookPickLabel) {
this.parent.hookPickLabel.setVisible(true);
}
}
switchToKeyMode() {
// Switch from lockpicking mode to key selection mode
console.log('Switching from lockpicking mode to key mode');
// Hide the mode switch button
const switchBtn = document.getElementById('lockpicking-switch-to-keys-btn');
if (switchBtn) {
switchBtn.style.display = 'none';
}
// Enter key mode
this.parent.keyMode = true;
this.parent.keySelectionMode = true;
// Hide lockpicking tools
if (this.parent.tensionWrench) {
this.parent.tensionWrench.setVisible(false);
}
if (this.parent.hookGroup) {
this.parent.hookGroup.setVisible(false);
}
if (this.parent.wrenchText) {
this.parent.wrenchText.setVisible(false);
}
if (this.parent.hookPickLabel) {
this.parent.hookPickLabel.setVisible(false);
}
// Reset pins to original positions
this.parent.lockConfig.resetPinsToOriginalPositions();
// Add mode switch back button (can switch back to lockpicking if available)
if (this.parent.canSwitchToPickMode) {
const itemDisplayDiv = document.querySelector('.lockpicking-item-section');
if (itemDisplayDiv) {
// Remove any existing button container
const existingButtonContainer = itemDisplayDiv.querySelector('div[style*="margin-top"]');
if (existingButtonContainer) {
existingButtonContainer.remove();
}
// Add new button container
const buttonContainer = document.createElement('div');
buttonContainer.style.cssText = `
display: flex;
gap: 10px;
margin-top: 10px;
justify-content: center;
`;
const switchModeBtn = document.createElement('button');
switchModeBtn.className = 'minigame-button';
switchModeBtn.id = 'lockpicking-switch-mode-btn';
switchModeBtn.innerHTML = '<img src="assets/objects/lockpick.png" alt="Lockpick" class="icon-large"> Switch to Lockpicking';
switchModeBtn.onclick = () => this.parent.switchToPickMode();
buttonContainer.appendChild(switchModeBtn);
itemDisplayDiv.appendChild(buttonContainer);
}
}
// Show key selection UI with available keys
if (this.parent.availableKeys && this.parent.availableKeys.length > 0) {
this.parent.createKeySelectionUI(this.parent.availableKeys, this.parent.requiredKeyId);
this.parent.updateFeedback("Select a key to use");
} else {
this.parent.updateFeedback("No keys available");
}
}
}