mirror of
https://github.com/cliffe/BreakEscape.git
synced 2026-02-20 13:50:46 +00:00
446 lines
19 KiB
JavaScript
446 lines
19 KiB
JavaScript
|
|
/**
|
|
* KeyOperations
|
|
*
|
|
* Extracted from lockpicking-game-phaser.js
|
|
* Includes key creation, insertion, correctness checking, and visual feedback
|
|
* Instantiate with: new KeyOperations(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 KeyOperations {
|
|
|
|
constructor(parent) {
|
|
this.parent = parent;
|
|
}
|
|
|
|
createKey() {
|
|
if (!this.parent.keyMode) return;
|
|
|
|
// Generate key data from actual pin heights if not provided
|
|
if (!this.parent.keyData) {
|
|
this.parent.keyDataGen.generateKeyDataFromPins();
|
|
}
|
|
|
|
// Key dimensions - make keyway higher so key pins align at shear line
|
|
const keywayWidth = 400; // Width of the keyway
|
|
const keywayHeight = 120; // Increased height to accommodate key cuts
|
|
const keywayStartX = 100; // Left edge of keyway
|
|
const keywayStartY = 170; // Moved higher (was 200) so key pins align at shear line
|
|
|
|
// Key parts dimensions
|
|
const keyCircleRadius = 140; // Circle (handle) - 2x larger (was 15)
|
|
const keyShoulderWidth = 20; // Shoulder width (short)
|
|
const keyShoulderHeight = keywayHeight + 10; // Slightly taller than keyway
|
|
const keyBladeWidth = keywayWidth + 20; // Blade length (reaches end of keyway)
|
|
const keyBladeHeight = keywayHeight - 10; // Slightly smaller than keyway
|
|
|
|
// Key starting position (just outside the keyway to the LEFT) - ready to be inserted
|
|
// Account for full key length: circle + shoulder + blade
|
|
const fullKeyLength = keyCircleRadius * 2 + keyShoulderWidth + keyBladeWidth;
|
|
const keyStartX = keywayStartX - fullKeyLength + 20; // Just the blade tip visible at keyway entrance
|
|
const keyStartY = keywayStartY + keywayHeight / 2; // Centered in keyway
|
|
|
|
// Create key container
|
|
this.parent.keyGroup = this.parent.scene.add.container(keyStartX, keyStartY);
|
|
|
|
// Create render texture for the key - make it wider to accommodate the full circle
|
|
const renderTextureWidth = Math.max(fullKeyLength, keyCircleRadius * 2 + 50); // Ensure enough space for circle
|
|
this.parent.keyRenderTexture = this.parent.scene.add.renderTexture(0, 0, renderTextureWidth, keyShoulderHeight);
|
|
this.parent.keyRenderTexture.setOrigin(0, 0.5);
|
|
|
|
// Draw the key using render texture
|
|
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();
|
|
testGraphics.fillStyle(0x00ff00); // Green
|
|
testGraphics.fillCircle(50, 50, 30);
|
|
this.parent.keyRenderTexture.draw(testGraphics);
|
|
testGraphics.destroy();
|
|
|
|
// Test: Draw circle directly to scene to see if it's a render texture issue
|
|
const directCircle = this.parent.scene.add.graphics();
|
|
directCircle.fillStyle(0xffff00); // Yellow
|
|
directCircle.fillCircle(keyStartX + 100, keyStartY, 50);
|
|
directCircle.setDepth(1000); // High z-index to be visible
|
|
|
|
this.parent.keyGroup.add(this.parent.keyRenderTexture);
|
|
|
|
// Set key graphics to low z-index so it appears behind pins
|
|
this.parent.keyGroup.setDepth(1); // Set low z-index so key appears behind pins
|
|
|
|
// Create click zone covering the entire keyway area in key mode
|
|
// Position click zone to cover the entire keyway from left edge to right edge
|
|
const keywayClickWidth = 400; // Full keyway width
|
|
const keywayClickHeight = 120; // Full keyway height
|
|
const clickZone = this.parent.scene.add.rectangle(0, 0,
|
|
keywayClickWidth, keywayClickHeight, 0x000000, 0);
|
|
clickZone.setDepth(9999); // Very high z-index for clickability
|
|
clickZone.setInteractive();
|
|
|
|
// Position click zone to cover the entire keyway area (not relative to key group)
|
|
clickZone.x = 100; // Keyway start X
|
|
clickZone.y = 170 + keywayClickHeight/2; // Keyway center Y
|
|
this.parent.keyClickZone = clickZone;
|
|
|
|
// Add click handler for key insertion
|
|
clickZone.on('pointerdown', () => {
|
|
if (!this.parent.keyInserting) {
|
|
// Hide labels on first key click (similar to pin clicks)
|
|
if (!this.parent.pinClicked) {
|
|
this.parent.pinClicked = true;
|
|
}
|
|
this.startKeyInsertion();
|
|
}
|
|
});
|
|
|
|
console.log('Key click zone created:', {
|
|
width: keywayClickWidth,
|
|
height: keyShoulderHeight,
|
|
position: '0,0 relative to key group'
|
|
});
|
|
|
|
// Store key configuration
|
|
this.parent.keyConfig = {
|
|
startX: keyStartX,
|
|
startY: keyStartY,
|
|
circleRadius: keyCircleRadius,
|
|
shoulderWidth: keyShoulderWidth,
|
|
shoulderHeight: keyShoulderHeight,
|
|
bladeWidth: keyBladeWidth,
|
|
bladeHeight: keyBladeHeight,
|
|
keywayStartX: keywayStartX,
|
|
keywayStartY: keywayStartY,
|
|
keywayWidth: keywayWidth,
|
|
keywayHeight: keywayHeight
|
|
};
|
|
|
|
// Create collision rectangles for the key blade surface (after config is set)
|
|
this.createKeyBladeCollision();
|
|
|
|
console.log('Key created with config:', this.parent.keyConfig);
|
|
}
|
|
|
|
startKeyInsertion() {
|
|
console.log('startKeyInsertion called with:', {
|
|
hasKeyGroup: !!this.parent.keyGroup,
|
|
hasKeyConfig: !!this.parent.keyConfig,
|
|
keyInserting: this.parent.keyInserting
|
|
});
|
|
|
|
if (!this.parent.keyGroup || !this.parent.keyConfig || this.parent.keyInserting) {
|
|
console.log('startKeyInsertion early return - missing requirements');
|
|
return;
|
|
}
|
|
|
|
console.log('Starting key insertion animation...');
|
|
this.parent.keyInserting = true;
|
|
this.parent.keyInsertion.updateFeedback("Inserting key...");
|
|
|
|
// Calculate target position - key should be fully inserted
|
|
const targetX = this.parent.keyConfig.keywayStartX - this.parent.keyConfig.shoulderWidth;
|
|
const startX = this.parent.keyGroup.x;
|
|
|
|
// Calculate fully inserted position - move key so it's completely inside the keyway
|
|
const keywayLeftEdge = this.parent.keyConfig.keywayStartX; // 100px
|
|
const shoulderRightEdge = this.parent.keyConfig.circleRadius * 1.9 + this.parent.keyConfig.shoulderWidth; // 266 + 20 = 286px from key group center
|
|
const fullyInsertedX = keywayLeftEdge - shoulderRightEdge; // 100 - 286 = -186px
|
|
|
|
// Create smooth animation from left to right
|
|
this.parent.scene.tweens.add({
|
|
targets: this.parent.keyGroup,
|
|
x: fullyInsertedX,
|
|
duration: 4000, // 4 seconds for slower insertion
|
|
ease: 'Cubic.easeInOut',
|
|
onUpdate: (tween) => {
|
|
// Calculate progress (0 to 1) - key moves from left to right
|
|
const progress = (this.parent.keyGroup.x - startX) / (fullyInsertedX - startX);
|
|
this.parent.keyInsertionProgress = Math.max(0, Math.min(1, progress));
|
|
|
|
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.keyInsertion.updatePinsWithKeyInsertion(this.parent.keyInsertionProgress);
|
|
},
|
|
onComplete: () => {
|
|
this.parent.keyInserting = false;
|
|
this.parent.keyInsertionProgress = 1.0; // Fully inserted
|
|
|
|
// Snap pins to exact final positions based on key cut dimensions
|
|
this.parent.keyAnim.snapPinsToExactPositions();
|
|
|
|
this.checkKeyCorrectness();
|
|
}
|
|
});
|
|
}
|
|
|
|
checkKeyCorrectness() {
|
|
if (!this.parent.keyData || !this.parent.keyData.cuts) return;
|
|
|
|
// Check if the selected key matches the correct key
|
|
let isCorrect = false;
|
|
|
|
if (this.parent.selectedKeyData && this.parent.selectedKeyData.cuts) {
|
|
// Compare the selected key cuts with the original correct key cuts
|
|
const selectedCuts = this.parent.selectedKeyData.cuts;
|
|
const correctCuts = this.parent.keyData.cuts;
|
|
|
|
if (selectedCuts.length === correctCuts.length) {
|
|
isCorrect = true;
|
|
for (let i = 0; i < selectedCuts.length; i++) {
|
|
if (Math.abs(selectedCuts[i] - correctCuts[i]) > 5) { // Allow small tolerance
|
|
isCorrect = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log('Key correctness check:', {
|
|
selectedKey: this.parent.selectedKeyData ? this.parent.selectedKeyData.cuts : 'none',
|
|
correctKey: this.parent.keyData.cuts,
|
|
isCorrect: isCorrect
|
|
});
|
|
|
|
if (isCorrect) {
|
|
// Key is correct - all pins are aligned at the shear line
|
|
this.parent.keyInsertion.updateFeedback("Key fits perfectly! Lock unlocked.");
|
|
|
|
// Start the rotation animation for correct key
|
|
this.parent.scene.time.delayedCall(500, () => {
|
|
this.parent.keyAnim.startKeyRotationAnimationWithChamberHoles();
|
|
});
|
|
|
|
// Complete the minigame after rotation animation
|
|
setTimeout(() => {
|
|
this.parent.complete(true);
|
|
}, 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.keyInsertion.updateFeedback("Wrong key! The lock won't turn.");
|
|
|
|
// Play wrong sound
|
|
if (this.parent.sounds.wrong) {
|
|
this.parent.sounds.wrong.play();
|
|
}
|
|
|
|
// Flash the entire lock red
|
|
this.parent.keyVisualFeedback.flashLockRed();
|
|
|
|
// Reset key position and show key selection again after a delay
|
|
setTimeout(() => {
|
|
this.parent.keyInsertion.updateKeyPosition(0);
|
|
// Show key selection again
|
|
if (this.parent.keySelectionMode) {
|
|
// For main game, go back to original key selection interface
|
|
// For challenge mode (locksmith-forge.html), use the training interface
|
|
if (this.parent.params?.lockable?.id === 'progressive-challenge') {
|
|
// This is the locksmith-forge.html challenge mode
|
|
this.parent.keySelection.createKeysForChallenge('correct_key');
|
|
} else {
|
|
// This is the main game - go back to key selection
|
|
this.startWithKeySelection();
|
|
}
|
|
}
|
|
}, 2000); // Longer delay to show the red flash
|
|
}
|
|
}
|
|
|
|
createKeyVisual(keyData, width, height) {
|
|
// Create a visual representation of a key for the selection UI by building the actual key and scaling it down
|
|
const keyContainer = this.parent.scene.add.container(0, 0);
|
|
|
|
// Save the original key data and pin count before temporarily changing them
|
|
const originalKeyData = this.parent.keyData;
|
|
const originalPinCount = this.parent.pinCount;
|
|
|
|
// Temporarily set the key data and pin count to create this specific key
|
|
this.parent.keyData = keyData;
|
|
this.parent.pinCount = keyData.pinCount || 5;
|
|
|
|
// Create the key with this specific key data
|
|
this.createKey();
|
|
|
|
// Get the key group and scale it down
|
|
const keyGroup = this.parent.keyGroup;
|
|
if (keyGroup) {
|
|
// Calculate scale to fit within the selection area
|
|
const maxWidth = width - 20; // Leave 10px margin on each side
|
|
const maxHeight = height - 20;
|
|
|
|
// Get the key's current dimensions
|
|
const keyBounds = keyGroup.getBounds();
|
|
const keyWidth = keyBounds.width;
|
|
const keyHeight = keyBounds.height;
|
|
|
|
// Calculate scale
|
|
const scaleX = maxWidth / keyWidth;
|
|
const scaleY = maxHeight / keyHeight;
|
|
const scale = Math.min(scaleX, scaleY) * 0.9; // Use 90% to leave some margin
|
|
|
|
// Scale the key group
|
|
keyGroup.setScale(scale);
|
|
|
|
// Center the key in the selection area
|
|
const scaledWidth = keyWidth * scale;
|
|
const scaledHeight = keyHeight * scale;
|
|
const offsetX = (width - scaledWidth) / 2;
|
|
const offsetY = (height - scaledHeight) / 2;
|
|
|
|
// Position the key
|
|
keyGroup.setPosition(offsetX, offsetY);
|
|
|
|
// Add the key group to the container
|
|
keyContainer.add(keyGroup);
|
|
}
|
|
|
|
// Restore the original key data and pin count
|
|
this.parent.keyData = originalKeyData;
|
|
this.parent.pinCount = originalPinCount;
|
|
|
|
return keyContainer;
|
|
}
|
|
|
|
selectKey(selectedIndex, correctIndex, keyData) {
|
|
// Handle key selection from the UI
|
|
console.log(`Key ${selectedIndex + 1} selected (correct: ${correctIndex + 1})`);
|
|
|
|
// Close the popup immediately
|
|
if (this.parent.keySelectionContainer) {
|
|
this.parent.keySelectionContainer.destroy();
|
|
}
|
|
|
|
// Remove any existing key from the scene
|
|
if (this.parent.keyGroup) {
|
|
this.parent.keyGroup.destroy();
|
|
this.parent.keyGroup = null;
|
|
}
|
|
|
|
// Remove any existing click zone
|
|
if (this.parent.keyClickZone) {
|
|
this.parent.keyClickZone.destroy();
|
|
this.parent.keyClickZone = null;
|
|
}
|
|
|
|
// Reset pins to their original positions before creating the new key
|
|
this.parent.lockConfig.resetPinsToOriginalPositions();
|
|
|
|
// Store the original correct key data (this determines if the key is correct)
|
|
const originalKeyData = this.parent.keyData;
|
|
|
|
// Store the selected key data for visual purposes
|
|
this.parent.selectedKeyData = keyData;
|
|
|
|
// Create the visual key with the selected key data
|
|
this.parent.keyData = keyData;
|
|
this.parent.pinCount = keyData.pinCount;
|
|
this.createKey();
|
|
|
|
// Restore the original key data for correctness checking
|
|
this.parent.keyData = originalKeyData;
|
|
|
|
// Update feedback - don't reveal if correct/wrong yet
|
|
this.parent.keyInsertion.updateFeedback("Key selected! Inserting into lock...");
|
|
|
|
// Automatically trigger key insertion after a short delay
|
|
setTimeout(() => {
|
|
this.startKeyInsertion();
|
|
}, 300); // Small delay to let the key appear first
|
|
|
|
// Update feedback if available
|
|
if (this.parent.selectKeyCallback) {
|
|
this.parent.selectKeyCallback(selectedIndex, correctIndex, keyData);
|
|
}
|
|
}
|
|
|
|
showWrongKeyFeedback() {
|
|
// Show visual feedback for wrong key selection
|
|
const feedback = this.parent.scene.add.graphics();
|
|
feedback.fillStyle(0xff0000, 0.3);
|
|
feedback.fillRect(0, 0, 800, 600);
|
|
feedback.setDepth(9999);
|
|
|
|
// Remove feedback after a short delay
|
|
this.parent.scene.time.delayedCall(500, () => {
|
|
feedback.destroy();
|
|
});
|
|
}
|
|
|
|
flashLockRed() {
|
|
// Flash the entire lock area red to indicate wrong key
|
|
const flash = this.parent.scene.add.graphics();
|
|
flash.fillStyle(0xff0000, 0.4); // Red with 40% opacity
|
|
flash.fillRect(100, 50, 400, 300); // Cover the entire lock area
|
|
flash.setDepth(9998); // High z-index but below other UI elements
|
|
|
|
// Remove flash after a short delay
|
|
this.parent.scene.time.delayedCall(800, () => {
|
|
flash.destroy();
|
|
});
|
|
}
|
|
|
|
createKeyBladeCollision() {
|
|
if (!this.parent.keyData || !this.parent.keyData.cuts || !this.parent.keyConfig) return;
|
|
|
|
// Create collision rectangles for each section of the key blade
|
|
this.parent.keyCollisionRects = [];
|
|
|
|
const pinSpacing = 400 / (this.parent.pinCount + 1);
|
|
const margin = pinSpacing * 0.75;
|
|
const bladeStartX = this.parent.keyConfig.circleRadius * 2 + this.parent.keyConfig.shoulderWidth;
|
|
|
|
console.log('Creating key collision rectangles, bladeStartX:', bladeStartX);
|
|
|
|
// Create collision rectangles for each pin position
|
|
for (let i = 0; i < this.parent.pinCount; i++) {
|
|
const cutDepth = this.parent.keyData.cuts[i] || 50;
|
|
const bladeHeight = this.parent.keyConfig.bladeHeight;
|
|
|
|
// The cut depth directly represents how deep the divot is
|
|
// Small pin = small cut, Large pin = large cut
|
|
const cutHeight = (bladeHeight / 2) * (cutDepth / 100);
|
|
const surfaceHeight = bladeHeight - cutHeight;
|
|
|
|
// Calculate pin position in the lock
|
|
const pinX = 100 + margin + i * pinSpacing;
|
|
|
|
// Create collision rectangle for this section - position relative to blade start
|
|
const rect = {
|
|
x: pinX - 100, // Position relative to blade start (not absolute)
|
|
y: -bladeHeight/2 + cutHeight, // Position relative to key center
|
|
width: 24, // Pin width
|
|
height: surfaceHeight,
|
|
cutDepth: cutDepth
|
|
};
|
|
|
|
console.log(`Key collision rect ${i}: x=${rect.x}, y=${rect.y}, width=${rect.width}, height=${rect.height}`);
|
|
this.parent.keyCollisionRects.push(rect);
|
|
}
|
|
}
|
|
|
|
getKeySurfaceHeightAtPosition(pinX, keyBladeStartX) {
|
|
if (!this.parent.keyCollisionRects || !this.parent.keyConfig) return this.parent.keyConfig ? this.parent.keyConfig.bladeHeight : 0;
|
|
|
|
// Find the collision rectangle for this pin position
|
|
const pinSpacing = 400 / (this.parent.pinCount + 1);
|
|
const margin = pinSpacing * 0.75;
|
|
|
|
for (let i = 0; i < this.parent.pinCount; i++) {
|
|
const cutPinX = 100 + margin + i * pinSpacing;
|
|
if (Math.abs(pinX - cutPinX) < 12) { // Within pin width
|
|
return this.parent.keyCollisionRects[i].height;
|
|
}
|
|
}
|
|
|
|
// If no cut found, return full blade height
|
|
return this.parent.keyConfig.bladeHeight;
|
|
}
|
|
|
|
}
|