Add key demo HTML and enhance lockpicking minigame: Introduce key-demo.html for testing new key mode functionality, featuring UI elements and game mechanics. Update lockpicking-game-phaser.js to support key mode, including key data generation, insertion logic, and collision detection for improved gameplay experience. Ensure clear documentation and maintainability in the codebase.

This commit is contained in:
Z. Cliffe Schreuders
2025-08-19 16:57:48 +01:00
parent 9aaa73f06f
commit 26e25830ed
2 changed files with 1214 additions and 6 deletions

View File

@@ -29,6 +29,12 @@ export class LockpickingMinigamePhaser extends MinigameScene {
this.closeButtonText = params.closeButtonText || '×';
this.closeButtonAction = params.closeButtonAction || 'close';
// Key mode settings
this.keyMode = params.keyMode || false;
this.keyData = params.keyData || null; // Key data with cuts/ridges
this.keyInsertionProgress = 0; // 0 = not inserted, 1 = fully inserted
this.keyInserting = false;
// Sound effects
this.sounds = {};
@@ -170,8 +176,17 @@ export class LockpickingMinigamePhaser extends MinigameScene {
self.createPins();
self.createHookPick();
self.createShearLine();
// Create key if in key mode
if (self.keyMode) {
self.createKey();
self.hideLockpickingTools();
self.updateFeedback("Click the key to insert it into the lock");
} else {
self.updateFeedback("Apply tension first, then lift pins in binding order - only the binding pin can be set");
}
self.setupInputHandlers();
self.updateFeedback("Apply tension first, then lift pins in binding order - only the binding pin can be set");
console.log('Phaser scene setup complete');
}
@@ -247,12 +262,12 @@ export class LockpickingMinigamePhaser extends MinigameScene {
this.cylinderGraphics.lineStyle(1, 0x8b4513); // Darker bronze border
this.cylinderGraphics.strokeRect(100, 155, 400, 180);
// Create keyway - space where key would enter (from halfway up key pins)
// Create keyway - space where key would enter (moved higher to align with shear line)
this.keywayGraphics = this.scene.add.graphics();
this.keywayGraphics.fillStyle(0x2a2a2a); // Dark gray for keyway
this.keywayGraphics.fillRect(100, 200, 400, 90); // From left edge (x=100), 2/3 height (90 instead of 135)
this.keywayGraphics.fillRect(100, 170, 400, 120); // Moved higher (y=170) and increased height (120)
this.keywayGraphics.lineStyle(1, 0x1a1a1a); // Darker border
this.keywayGraphics.strokeRect(100, 200, 400, 90);
this.keywayGraphics.strokeRect(100, 170, 400, 120);
}
createTensionWrench() {
@@ -546,6 +561,675 @@ export class LockpickingMinigamePhaser extends MinigameScene {
console.log('Hook config initialized - targetPin:', this.hookConfig.targetPin, 'pinCount:', this.pinCount);
}
generateKeyDataFromPins() {
// Generate key cuts based on actual pin heights
// Calculate cut depths so that when key is inserted, pins align at shear line
const cuts = [];
const shearLineY = -45; // Shear line position (in pin container coordinates)
const keyBladeHeight = 110; // Key blade height (from keyConfig)
const pinContainerY = 200; // Pin container Y position (world coordinates)
for (let i = 0; i < this.pinCount; i++) {
const pin = this.pins[i];
const keyPinLength = pin.keyPinLength;
// Simple key cut calculation:
// The cut depth should be the key pin length minus the gap from key blade top to shear line
// Key blade is centered in keyway: keywayStartY + keywayHeight/2 = 170 + 60 = 230
// Key blade top is: 230 - keyBladeHeight/2 = 230 - 55 = 175
// Shear line is at y=155 in world coordinates (200 - 45)
const keyBladeTop_world = 175; // 170 + 60 - 55 (keywayStartY + keywayHeight/2 - keyBladeHeight/2)
const shearLine_world = 155; // 200 - 45 (pin container Y - shear line Y)
const gapFromKeyBladeTopToShearLine = keyBladeTop_world - shearLine_world; // 175 - 155 = 20
// Cut depth = key pin length - gap from key blade top to shear line
const cutDepth_needed = keyPinLength - gapFromKeyBladeTopToShearLine;
// Clamp to valid range (0 to key blade height)
const clampedCutDepth = Math.max(0, Math.min(keyBladeHeight, cutDepth_needed));
console.log(`=== KEY CUT ${i} GENERATION ===`);
console.log(` Pin properties: keyPinLength=${keyPinLength}, driverPinLength=${pin.driverPinLength}`);
console.log(` Key blade top: ${keyBladeTop_world}, shear line: ${shearLine_world}`);
console.log(` Gap from key blade top to shear line: ${gapFromKeyBladeTopToShearLine}`);
console.log(` Cut calculation: cutDepth_needed=${cutDepth_needed} (${keyPinLength} - ${gapFromKeyBladeTopToShearLine})`);
console.log(` Final cut: cutDepth=${clampedCutDepth}px (max ${keyBladeHeight}px)`);
console.log(`=====================================`);
cuts.push(clampedCutDepth);
}
this.keyData = { cuts: cuts };
console.log('Generated key data from pins:', this.keyData);
}
createKey() {
if (!this.keyMode) return;
// Generate key data from actual pin heights if not provided
if (!this.keyData) {
this.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.keyGroup = this.scene.add.container(keyStartX, keyStartY);
// Create key graphics
this.keyGraphics = this.scene.add.graphics();
this.keyGraphics.fillStyle(0xcccccc); // Silver color
// Removed outline - no lineStyle
// Draw key parts from right to left
// 1. Circle (handle) - rightmost part
this.keyGraphics.fillCircle(keyCircleRadius, 0, keyCircleRadius);
// 2. Shoulder - wider rectangle
const shoulderX = keyCircleRadius * 1.9; // After circle
this.keyGraphics.fillRect(shoulderX, -keyShoulderHeight/2, keyShoulderWidth, keyShoulderHeight);
// 3. Blade - main part with cuts/ridges
const bladeX = shoulderX + keyShoulderWidth;
this.keyGraphics.fillRect(bladeX, -keyBladeHeight/2, keyBladeWidth, keyBladeHeight);
// Add cuts/ridges to the blade based on key data
this.addKeyCuts(bladeX, -keyBladeHeight/2, keyBladeWidth, keyBladeHeight, keyBladeWidth);
this.keyGroup.add(this.keyGraphics);
// Set key graphics to low z-index so it appears behind pins
this.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 from leftmost blade tip through the entire keyway and beyond
const keywayClickWidth = fullKeyLength + keywayWidth + 200; // Extend much further right
const clickZone = this.scene.add.rectangle(0, 0,
keywayClickWidth, keyShoulderHeight, 0x000000, 0);
clickZone.setDepth(9999); // Very high z-index for clickability
clickZone.setInteractive();
// Add click zone to key group and sync its position
this.keyGroup.add(clickZone);
this.keyClickZone = clickZone;
console.log('Key click zone created:', {
width: keywayClickWidth,
height: keyShoulderHeight,
position: '0,0 relative to key group'
});
// Store key configuration
this.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.keyConfig);
}
addKeyCuts(bladeX, bladeY, bladeWidth, bladeHeight, totalBladeWidth) {
if (!this.keyData || !this.keyData.cuts) return;
// Draw cuts/ridges on the blade
this.keyGraphics.fillStyle(0x2a2a2a); // Dark color for cuts
const cutWidth = 24; // Width of each cut (same as pin width)
// Calculate pin spacing to match the lock's pin positions
const pinSpacing = 400 / (this.pinCount + 1);
const margin = pinSpacing * 0.75;
// Space cuts to align with actual pin positions
this.keyData.cuts.forEach((cutDepth, index) => {
if (index >= this.pinCount) return; // Only create cuts for existing pins
// Calculate pin position in the lock
const pinX = 100 + margin + index * pinSpacing;
// Calculate cut position relative to the blade
const cutX = bladeX + (pinX - 100); // Offset from blade start to match pin position
// The cut depth directly represents how deep the divot should be (in pixels)
// Small pin = small cut, Large pin = large cut
const cutHeight = cutDepth; // cutDepth is already in pixels
// Draw main cut only on the top of the key (divot)
this.keyGraphics.fillRect(cutX - cutWidth/2, bladeY, cutWidth, cutHeight);
// Draw triangular cuts extending to halfway between pins
if (index < this.pinCount - 1) {
// Calculate next pin position
const nextPinX = 100 + margin + (index + 1) * pinSpacing;
const nextCutX = bladeX + (nextPinX - 100);
// Calculate halfway point between current and next cut
const halfwayX = cutX + (nextCutX - cutX) / 2;
// Draw triangular cut from current cut to halfway point (right side)
this.drawTriangularCut(cutX + cutWidth/2, bladeY, halfwayX, bladeY, cutHeight);
}
// Draw triangular cut from previous halfway point to current cut (left side)
if (index > 0) {
// Calculate previous pin position
const prevPinX = 100 + margin + (index - 1) * pinSpacing;
const prevCutX = bladeX + (prevPinX - 100);
// Calculate halfway point between previous and current cut
const halfwayX = prevCutX + (cutX - prevCutX) / 2;
// Draw triangular cut from halfway point to current cut (left side)
// For left triangles, we want height to increase as we move toward the cut
this.drawTriangularCut(halfwayX, bladeY, cutX - cutWidth/2, bladeY, cutHeight, true);
} else if (index === 0) {
// For the first pin, draw triangular cut from left edge of key blade to first cut
const keyLeftEdge = bladeX;
// Draw triangular cut from left edge to first cut (height increases toward the cut)
this.drawTriangularCut(keyLeftEdge, bladeY, cutX - cutWidth/2, bladeY, cutHeight, true);
}
// For the last pin, add triangular cuts extending to the right edge of the key
if (index === this.pinCount - 1) {
// Calculate hypothetical next pin position
const nextPinX = 100 + margin + (index + 1) * pinSpacing;
const nextCutX = bladeX + (nextPinX - 100);
// Calculate halfway point between last pin and hypothetical next pin
const halfwayX = cutX + (nextCutX - cutX) / 2;
// Calculate the right edge of the key blade
const keyRightEdge = bladeX + totalBladeWidth;
// Draw triangular cut from last pin to halfway point
this.drawTriangularCut(cutX + cutWidth/2, bladeY, halfwayX, bladeY, cutHeight);
// Draw triangular end that meets in the middle (pointed tip)
// Use half the blade height for a consistent 45-degree angle regardless of cut depth
const triangularEndHeight = bladeHeight / 2;
this.drawTriangularEnd(halfwayX, bladeY, keyRightEdge, bladeY + bladeHeight, triangularEndHeight);
}
});
// Reset fill style
this.keyGraphics.fillStyle(0xcccccc);
}
drawTriangularCut(startX, startY, endX, endY, height, isLeftTriangle = false) {
// Draw a triangular cut using pixel art style
const width = Math.abs(endX - startX);
const stepSize = 4; // Consistent pixel size for steps
const steps = Math.max(1, Math.floor(width / stepSize)); // Create steps for pixel art effect
for (let i = 0; i <= steps; i++) {
const progress = i / steps;
const x = startX + (endX - startX) * progress;
let stepHeight;
if (isLeftTriangle) {
// Left triangle: height increases as we move toward the cut
stepHeight = height * progress;
} else {
// Right triangle: height decreases as we move away from the cut
stepHeight = height * (1 - progress);
}
// Draw horizontal line at this step
const stepWidth = width / steps;
this.keyGraphics.fillRect(x, startY, stepWidth, stepHeight);
}
}
drawTriangularCutDownward(startX, startY, endX, endY, height) {
// Draw a downward triangular cut using pixel art style (cuts off top corner)
const width = Math.abs(endX - startX);
const stepSize = 4; // Consistent pixel size for steps
const steps = Math.max(1, Math.floor(width / stepSize)); // Create steps for pixel art effect
for (let i = 0; i <= steps; i++) {
const progress = i / steps;
const x = startX + (endX - startX) * progress;
// Height increases as we move toward the end (cutting deeper into the key)
const stepHeight = height * progress;
// Draw horizontal line at this step, starting from the top and cutting downward
const stepWidth = width / steps;
this.keyGraphics.fillRect(x, startY, stepWidth, stepHeight);
}
}
drawTriangularCutUpward(startX, startY, endX, endY, height) {
// Draw an upward triangular cut using pixel art style (cuts off bottom corner)
const width = Math.abs(endX - startX);
const stepSize = 4; // Consistent pixel size for steps
const steps = Math.max(1, Math.floor(width / stepSize)); // Create steps for pixel art effect
for (let i = 0; i <= steps; i++) {
const progress = i / steps;
const x = startX + (endX - startX) * progress;
// Height increases as we move toward the end (cutting deeper into the key)
const stepHeight = height * progress;
// Draw horizontal line at this step, starting from the bottom and cutting upward
const stepWidth = width / steps;
this.keyGraphics.fillRect(x, startY - stepHeight, stepWidth, stepHeight);
}
}
drawTriangularEnd(startX, startY, endX, endY, maxHeight) {
// Draw a triangular end that creates a pointed tip by cutting from both top and bottom
// The cuts meet in the middle to form a point
const width = Math.abs(endX - startX);
const stepSize = 4; // Consistent pixel size for steps
const steps = Math.max(1, Math.floor(width / stepSize)); // Create steps for pixel art effect
for (let i = 0; i <= steps; i++) {
const progress = i / steps;
const x = startX + (endX - startX) * progress;
// Calculate height for this step - creates a triangular shape
// Height increases as we move toward the end, but we cut from both top and bottom
const stepHeight = maxHeight * progress;
// Draw horizontal line at this step, cutting from the top
const stepWidth = width / steps;
this.keyGraphics.fillRect(x, startY, stepWidth, stepHeight);
// Draw horizontal line at this step, cutting from the bottom
this.keyGraphics.fillRect(x, endY - stepHeight, stepWidth, stepHeight);
}
}
startKeyInsertion() {
console.log('startKeyInsertion called with:', {
hasKeyGroup: !!this.keyGroup,
hasKeyConfig: !!this.keyConfig,
keyInserting: this.keyInserting
});
if (!this.keyGroup || !this.keyConfig || this.keyInserting) {
console.log('startKeyInsertion early return - missing requirements');
return;
}
console.log('Starting key insertion animation...');
this.keyInserting = true;
this.updateFeedback("Inserting key...");
// Calculate target position (shoulder stops at keyway edge, not fully inserted)
const targetX = this.keyConfig.keywayStartX - this.keyConfig.shoulderWidth;
const startX = this.keyGroup.x;
// Calculate target position - move key so RIGHT edge of shoulder touches LEFT edge of keyway
// Key starts at -600px, needs to move so shoulder right edge (at position 286px from center) touches keyway left edge
const keywayLeftEdge = this.keyConfig.keywayStartX; // 100px
const shoulderRightEdge = this.keyConfig.circleRadius * 1.9 + this.keyConfig.shoulderWidth; // 266 + 20 = 286px from key group center
const targetKeyGroupX = keywayLeftEdge - shoulderRightEdge; // 100 - 286 = -186px
const halfwayX = targetKeyGroupX;
// Create smooth animation from left to right
this.scene.tweens.add({
targets: this.keyGroup,
x: halfwayX,
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.keyGroup.x - startX) / (halfwayX - startX);
this.keyInsertionProgress = Math.max(0, Math.min(1, progress));
console.log('Animation update - key position:', this.keyGroup.x, 'progress:', this.keyInsertionProgress);
// Update pin positions based on key cuts as the key is inserted
this.updatePinsWithKeyInsertion(this.keyInsertionProgress);
},
onComplete: () => {
this.keyInserting = false;
this.keyInsertionProgress = 0.5; // Only halfway inserted
this.checkKeyCorrectness();
}
});
}
updateKeyPosition(progress) {
if (!this.keyGroup || !this.keyConfig) return;
// Calculate new position based on insertion progress
// Key moves from left (off-screen) to right (shoulder touches lock edge)
const targetX = this.keyConfig.keywayStartX - this.keyConfig.shoulderWidth; // Shoulder touches lock edge
const currentX = this.keyConfig.startX + (targetX - this.keyConfig.startX) * progress;
this.keyGroup.x = currentX;
this.keyInsertionProgress = progress;
// If fully inserted, check if key is correct
if (progress >= 1.0) {
this.checkKeyCorrectness();
}
}
checkKeyCorrectness() {
if (!this.keyData || !this.keyData.cuts) return;
// Check if pins are aligned at the shear line
let isCorrect = true;
const shearLineY = -45; // Shear line position
const tolerance = 5; // Tolerance for shear line alignment
this.pins.forEach((pin, index) => {
if (index >= this.pinCount) return;
// Calculate the current position of the key pin top
const pinWorldY = 200;
const pinCurrentY = pinWorldY - 50 + pin.driverPinLength + pin.keyPinLength - pin.currentHeight;
const keyPinTop = pinCurrentY - pin.keyPinLength;
// Check if the key pin top is at the shear line
const distanceToShearLine = Math.abs(keyPinTop - shearLineY);
if (distanceToShearLine > tolerance) {
isCorrect = false;
}
});
if (isCorrect) {
// Key is correct - all pins are aligned at the shear line
this.updateFeedback("Key fits perfectly! Lock unlocked.");
// Play success sound
if (this.sounds.success) {
this.sounds.success.play();
}
// Complete the minigame
setTimeout(() => {
this.complete(true);
}, 1000);
} else {
// Key is wrong
this.updateFeedback("Wrong key! Try a different one.");
// Play wrong sound
if (this.sounds.wrong) {
this.sounds.wrong.play();
}
// Reset key position
setTimeout(() => {
this.updateKeyPosition(0);
}, 1000);
}
}
liftPinsWithKey() {
if (!this.keyData || !this.keyData.cuts) return;
// Lift each pin to the correct height based on key cuts
this.keyData.cuts.forEach((cutDepth, index) => {
if (index >= this.pinCount) return;
const pin = this.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.scene.tweens.add({
targets: { height: 0 },
height: actualLift,
duration: 500,
ease: 'Cubic.easeOut',
onUpdate: (tween) => {
pin.currentHeight = tween.targets[0].height;
this.updatePinVisuals(pin);
}
});
});
}
updatePinsWithKeyInsertion(progress) {
if (!this.keyCollisionRects || !this.keyConfig) return;
console.log('Updating pins with key insertion, progress:', progress);
// Calculate key blade position relative to the lock
const keyBladeStartX = this.keyGroup.x + this.keyConfig.circleRadius * 2 + this.keyConfig.shoulderWidth;
// Check each pin for collision with the key blade
this.pins.forEach((pin, index) => {
if (index >= this.pinCount) return;
// Calculate pin position in the lock
const pinSpacing = 400 / (this.pinCount + 1);
const margin = pinSpacing * 0.75;
const pinX = 100 + margin + index * pinSpacing;
// Calculate pin's current world position
const pinWorldY = 200;
const pinCurrentY = pinWorldY - 50 + pin.driverPinLength + pin.keyPinLength - pin.currentHeight;
const keyPinTop = pinCurrentY - pin.keyPinLength;
const keyPinBottom = pinCurrentY;
// Create pin collision rectangle
const pinRect = new Phaser.Geom.Rectangle(pinX - 12, keyPinTop, 24, pin.keyPinLength);
console.log(`Pin ${index} collision rect: x=${pinX - 12}, y=${keyPinTop}, width=24, height=${pin.keyPinLength}`);
// Direct collision detection - check if pin is under the key blade
let collision = false;
let keySurfaceY = 0;
let invertedCutDepth = 0; // Store the inverted cut depth for use in collision block
let cutDepth = 0; // Store the cut depth for use in collision block
let surfaceHeight = 0; // Store the surface height for use in collision block
console.log(`Pin ${index} at X: ${pinX}, key blade start: ${keyBladeStartX}, keyData cuts: ${JSON.stringify(this.keyData.cuts)}`);
// Check if this pin is under the key blade
const keyBladeEndX = keyBladeStartX + this.keyConfig.bladeWidth;
if (pinX >= keyBladeStartX && pinX <= keyBladeEndX) {
collision = true;
// Find which key cut corresponds to this pin
const pinSpacing = 400 / (this.pinCount + 1);
const margin = pinSpacing * 0.75;
for (let i = 0; i < this.pinCount; i++) {
const cutPinX = 100 + margin + i * pinSpacing;
if (Math.abs(pinX - cutPinX) < 12) { // Within pin width
cutDepth = this.keyData.cuts[i] || 50;
const bladeHeight = this.keyConfig.bladeHeight;
// The cut depth directly represents how deep the divot is
// Small pin = small cut, Large pin = large cut
invertedCutDepth = cutDepth; // Store for use in collision block (no inversion needed)
const cutHeight = (bladeHeight / 2) * (cutDepth / 100);
surfaceHeight = bladeHeight - cutHeight;
// Calculate key surface Y position using the same logic as key generation
// The key surface should be positioned so that when the key pin bottom sits on it,
// the key pin top aligns with the shear line
const shearLineY = -45; // Shear line position
const keyPinBottomAtShearLine = shearLineY + pin.keyPinLength;
// Convert from pin container coordinates to world coordinates
const pinContainerY = 200; // Pin container Y position
keySurfaceY = keyPinBottomAtShearLine + pinContainerY;
console.log(`Pin ${index} collision with key cut ${i} - cutDepth: ${cutDepth}, invertedCutDepth: ${invertedCutDepth}, surfaceHeight: ${surfaceHeight}, keySurfaceY: ${keySurfaceY}`);
break;
}
}
}
if (collision) {
// Key is under this pin - calculate lift to sit on key surface
// The key surface is at keySurfaceY, and the pin should sit on it
// Calculate the pin's rest position (when not lifted)
const pinRestY = pinWorldY - 50 + pin.driverPinLength + pin.keyPinLength;
const keyPinBottomAtRest = pinRestY;
// Calculate where the key pin bottom should be to sit on the key surface
const targetKeyPinBottom = keySurfaceY;
// Calculate required lift to move key pin bottom from rest to key surface
const requiredLift = keyPinBottomAtRest - targetKeyPinBottom;
// Quick lift when key is under the pin
const targetLift = Math.max(0, requiredLift);
const currentLift = Math.min(targetLift, pin.currentHeight + 8); // Fast lift
// Calculate the key pin top position after lift (in pin container coordinates)
// keyPinBottomAtRest is in world coordinates, need to convert to pin container coordinates
const pinContainerY = 200; // Pin container Y position
const keyPinBottomAtRest_container = keyPinBottomAtRest - pinContainerY;
const keyPinTopAfterLift = keyPinBottomAtRest_container - pin.keyPinLength - currentLift;
const shearLineY = -45;
const distanceToShearLine = Math.abs(keyPinTopAfterLift - shearLineY);
console.log(`=== PIN ${index} DETAILED DEBUG ===`);
console.log(` Pin properties: keyPinLength=${pin.keyPinLength}, driverPinLength=${pin.driverPinLength}`);
console.log(` World coordinates: keyPinBottomAtRest=${keyPinBottomAtRest}, keySurfaceY=${keySurfaceY}`);
console.log(` Container coordinates: keyPinBottomAtRest_container=${keyPinBottomAtRest_container}`);
console.log(` Lift calculation: requiredLift=${requiredLift}, currentLift=${currentLift}`);
console.log(` Final positions: keyPinTopAfterLift=${keyPinTopAfterLift}, shearLineY=${shearLineY}`);
console.log(` Alignment: distanceToShearLine=${distanceToShearLine} (should be close to 0)`);
console.log(` Key cut info: cutDepth=${cutDepth}, surfaceHeight=${surfaceHeight}`);
console.log(`=====================================`);
// Update pin height
pin.currentHeight = currentLift;
} else {
// Key is not under this pin - let it fall back down quickly
const newHeight = Math.max(0, pin.currentHeight - 4);
console.log(`Pin ${index} no collision - currentHeight: ${pin.currentHeight}, newHeight: ${newHeight}`);
pin.currentHeight = newHeight;
}
// Update pin visuals
this.updatePinVisuals(pin);
});
}
createKeyBladeCollision() {
if (!this.keyData || !this.keyData.cuts || !this.keyConfig) return;
// Create collision rectangles for each section of the key blade
this.keyCollisionRects = [];
const pinSpacing = 400 / (this.pinCount + 1);
const margin = pinSpacing * 0.75;
const bladeStartX = this.keyConfig.circleRadius * 2 + this.keyConfig.shoulderWidth;
console.log('Creating key collision rectangles, bladeStartX:', bladeStartX);
// Create collision rectangles for each pin position
for (let i = 0; i < this.pinCount; i++) {
const cutDepth = this.keyData.cuts[i] || 50;
const bladeHeight = this.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.keyCollisionRects.push(rect);
}
}
getKeySurfaceHeightAtPosition(pinX, keyBladeStartX) {
if (!this.keyCollisionRects || !this.keyConfig) return this.keyConfig ? this.keyConfig.bladeHeight : 0;
// Find the collision rectangle for this pin position
const pinSpacing = 400 / (this.pinCount + 1);
const margin = pinSpacing * 0.75;
for (let i = 0; i < this.pinCount; i++) {
const cutPinX = 100 + margin + i * pinSpacing;
if (Math.abs(pinX - cutPinX) < 12) { // Within pin width
return this.keyCollisionRects[i].height;
}
}
// If no cut found, return full blade height
return this.keyConfig.bladeHeight;
}
hideLockpickingTools() {
// Hide tension wrench and hook pick in key mode
if (this.tensionWrench) {
this.tensionWrench.setVisible(false);
}
if (this.hookGroup) {
this.hookGroup.setVisible(false);
}
// Hide labels
if (this.wrenchText) {
this.wrenchText.setVisible(false);
}
if (this.hookPickLabel) {
this.hookPickLabel.setVisible(false);
}
}
updateHookPosition(pinIndex) {
if (!this.hookGroup || !this.hookConfig) return;
@@ -758,10 +1442,21 @@ export class LockpickingMinigamePhaser extends MinigameScene {
}
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
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);
@@ -1097,10 +1792,15 @@ export class LockpickingMinigamePhaser extends MinigameScene {
}
this.gameState.mouseDown = false;
// Always return hook to resting position when mouse is released
if (this.hookPickGraphics && this.hookConfig) {
// Only return hook to resting position if not in key mode
if (!this.keyMode && this.hookPickGraphics && this.hookConfig) {
this.returnHookToStart();
}
// Stop key insertion if in key mode
if (this.keyMode) {
this.keyInserting = false;
}
});
// Add keyboard bindings
@@ -1283,9 +1983,33 @@ export class LockpickingMinigamePhaser extends MinigameScene {
}
}
});
// Add key interaction handlers if in key mode
if (this.keyMode && this.keyClickZone) {
console.log('Setting up key click handler...');
this.keyClickZone.on('pointerdown', (pointer) => {
console.log('Key clicked! Event triggered.');
// Prevent this event from bubbling up to global handlers
pointer.event.stopPropagation();
if (!this.keyInserting) {
console.log('Starting key insertion animation...');
this.startKeyInsertion();
} else {
console.log('Key insertion already in progress, ignoring click.');
}
});
} else {
console.log('Key mode or click zone not available:', { keyMode: this.keyMode, hasClickZone: !!this.keyClickZone });
}
}
update() {
// Skip normal lockpicking logic if in key mode
if (this.keyMode) {
return;
}
if (this.lockState.currentPin && this.gameState.mouseDown) {
this.liftPin();
}

484
key-demo.html Normal file
View File

@@ -0,0 +1,484 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lockpicking Key Mode Demo</title>
<!-- Google Fonts - Press Start 2P, VT323 -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=VT323&display=swap" rel="stylesheet">
<!-- Web Font Loader script to ensure fonts load properly -->
<script src="https://ajax.googleapis.com/ajax/libs/webfont/1.6.26/webfont.js"></script>
<script>
WebFont.load({
google: {
families: ['Press Start 2P', 'VT323']
},
active: function() {
console.log('Fonts loaded successfully');
}
});
</script>
<style>
body {
font-family: 'VT323', monospace;
background: #333;
color: #ffffff;
margin: 0;
padding: 20px;
min-height: 100vh;
/* Remove text-align center to prevent canvas positioning issues */
}
.page-wrapper {
text-align: center;
max-width: 800px;
margin: 0 auto;
}
.header {
background: rgba(0, 0, 0, 0.95);
padding: 20px;
border-radius: 10px;
margin-bottom: 20px;
border: 2px solid #444;
box-shadow: 0 0 30px rgba(0, 0, 0, 0.8);
color: #00ff00;
}
.title {
font-family: 'Press Start 2P', monospace;
font-size: 24px;
margin-bottom: 15px;
text-shadow: 0 0 10px #00ff00;
}
.description {
font-size: 18px;
margin-bottom: 20px;
line-height: 1.4;
}
.demo-container {
background: rgba(0, 0, 0, 0.95);
border-radius: 10px;
padding: 20px;
border: 2px solid #444;
box-shadow: 0 0 30px rgba(0, 0, 0, 0.8);
margin-bottom: 20px;
}
.game-container {
background: #1a1a1a;
border-radius: 5px;
padding: 15px;
margin-bottom: 20px;
min-height: 400px;
position: relative;
text-align: left;
}
.controls {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 15px;
margin-bottom: 20px;
}
.key-preset {
background: #444;
color: #00ff00;
border: 2px solid #00ff00;
padding: 10px 15px;
border-radius: 5px;
cursor: pointer;
font-family: 'VT323', monospace;
font-size: 16px;
transition: all 0.3s ease;
}
.key-preset:hover {
background: #00ff00;
color: #000;
}
.key-preset.active {
background: #00ff00;
color: #000;
}
.mode-toggle {
background: #666;
color: #fff;
border: 2px solid #666;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-family: 'VT323', monospace;
font-size: 16px;
transition: all 0.3s ease;
}
.mode-toggle:hover {
background: #888;
}
.mode-toggle.active {
background: #00ff00;
color: #000;
border-color: #00ff00;
}
.key-info {
background: #2a2a2a;
padding: 15px;
border-radius: 5px;
border: 1px solid #444;
margin-bottom: 15px;
}
.key-info h3 {
font-family: 'Press Start 2P', monospace;
font-size: 14px;
margin: 0 0 10px 0;
color: #00ff00;
}
.key-cuts {
display: flex;
justify-content: center;
gap: 10px;
flex-wrap: wrap;
}
.cut-indicator {
background: #333;
border: 1px solid #666;
padding: 5px 10px;
border-radius: 3px;
font-size: 14px;
min-width: 30px;
text-align: center;
}
.instructions {
background: #2a2a2a;
padding: 15px;
border-radius: 5px;
border: 1px solid #444;
text-align: left;
font-size: 16px;
line-height: 1.5;
}
.instructions h3 {
font-family: 'Press Start 2P', monospace;
font-size: 14px;
margin: 0 0 10px 0;
color: #00ff00;
}
.instructions ul {
margin: 0;
padding-left: 20px;
}
.instructions li {
margin-bottom: 5px;
}
.feedback {
background: rgba(0, 0, 0, 0.8);
color: #00ff00;
padding: 10px 15px;
border-radius: 5px;
margin: 10px 0;
font-family: 'VT323', monospace;
font-size: 16px;
text-align: center;
border: 1px solid #00ff00;
min-height: 20px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 0 10px rgba(0, 255, 0, 0.3);
}
/* Prevent canvas positioning issues */
canvas {
display: block !important;
margin: 0 auto !important;
text-align: left !important;
}
#gameContainer {
text-align: left;
}
</style>
</head>
<body>
<div class="page-wrapper">
<div class="header">
<div class="title">🔑 LOCKPICKING KEY MODE DEMO 🔑</div>
<div class="description">
Test the new key mode functionality! Try different keys to see which ones work with the lock.
</div>
</div>
<div class="demo-container">
<div class="controls">
<button class="mode-toggle active" id="keyModeToggle">Key Mode</button>
<button class="mode-toggle" id="pickModeToggle">Pick Mode</button>
</div>
<div class="key-info" id="keyInfo">
<h3>Current Key</h3>
<div class="key-cuts" id="keyCuts">
<div class="cut-indicator">25%</div>
<div class="cut-indicator">50%</div>
<div class="cut-indicator">75%</div>
<div class="cut-indicator">30%</div>
<div class="cut-indicator">60%</div>
</div>
</div>
<div class="key-preset active" data-key="key1">Key 1 (Correct)</div>
<div class="key-preset" data-key="key2">Key 2 (Wrong)</div>
<div class="key-preset" data-key="key3">Key 3 (Partial)</div>
<div class="key-preset" data-key="key4">Key 4 (Random)</div>
<div class="key-preset" data-key="key5">Key 5 (Deep)</div>
</div>
<div class="game-container">
<div id="gameContainer"></div>
<div class="feedback" id="feedback">Select a key and try inserting it into the lock</div>
</div>
<div class="demo-container">
<div class="instructions">
<h3>How to Use</h3>
<ul>
<li><strong>Key Mode:</strong> Click and drag the key from left to right to insert it into the lock</li>
<li><strong>Pick Mode:</strong> Use the tension wrench and hook pick to manually pick the lock</li>
<li><strong>Key Cuts:</strong> The numbers show the depth of each cut as a percentage (0-100%)</li>
<li><strong>Success:</strong> When the key cuts match the pin heights, the lock will unlock</li>
<li><strong>Failure:</strong> Wrong keys will be rejected and the key will reset</li>
</ul>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/phaser@3.60.0/dist/phaser.min.js"></script>
<script type="module">
import { LockpickingMinigamePhaser } from './js/minigames/lockpicking/lockpicking-game-phaser.js';
class KeyDemo {
constructor() {
this.currentGame = null;
this.currentMode = 'key';
this.currentKey = 'key1';
// Key presets with different cut patterns
this.keyPresets = {
key1: {
name: "Key 1 (Correct)",
cuts: [25, 50, 75, 30, 60], // This will be overridden by auto-generation
description: "This key is auto-generated to match the actual pin heights"
},
key2: {
name: "Key 2 (Wrong)",
cuts: [10, 20, 30, 40, 50],
description: "This key has cuts that don't match the pins"
},
key3: {
name: "Key 3 (Partial)",
cuts: [25, 50, 75, 40, 60],
description: "This key has some correct cuts but not all"
},
key4: {
name: "Key 4 (Random)",
cuts: [15, 45, 80, 35, 55],
description: "Random cut pattern - might work by chance"
},
key5: {
name: "Key 5 (Deep)",
cuts: [80, 90, 95, 85, 88],
description: "Very deep cuts - probably too deep"
}
};
this.initializeUI();
this.startGame();
}
initializeUI() {
// Mode toggle buttons
document.getElementById('keyModeToggle').addEventListener('click', () => {
this.setMode('key');
});
document.getElementById('pickModeToggle').addEventListener('click', () => {
this.setMode('pick');
});
// Key preset buttons
document.querySelectorAll('.key-preset').forEach(button => {
button.addEventListener('click', () => {
this.setKey(button.dataset.key);
});
});
}
setMode(mode) {
this.currentMode = mode;
// Update button states
document.getElementById('keyModeToggle').classList.toggle('active', mode === 'key');
document.getElementById('pickModeToggle').classList.toggle('active', mode === 'pick');
// Show/hide key info
document.getElementById('keyInfo').style.display = mode === 'key' ? 'block' : 'none';
// Restart game with new mode
this.startGame();
}
setKey(keyId) {
this.currentKey = keyId;
// Update button states
document.querySelectorAll('.key-preset').forEach(button => {
button.classList.toggle('active', button.dataset.key === keyId);
});
// Update key cuts display
this.updateKeyDisplay();
// Restart game with new key
if (this.currentMode === 'key') {
this.startGame();
}
}
updateKeyDisplay() {
const keyData = this.keyPresets[this.currentKey];
const keyCutsContainer = document.getElementById('keyCuts');
keyCutsContainer.innerHTML = '';
if (this.currentKey === 'key1' && this.currentGame && this.currentGame.keyData) {
// Show the actual generated cuts for the correct key
this.currentGame.keyData.cuts.forEach(cut => {
const cutElement = document.createElement('div');
cutElement.className = 'cut-indicator';
cutElement.textContent = cut + '%';
cutElement.style.backgroundColor = '#00ff00'; // Green for correct key
keyCutsContainer.appendChild(cutElement);
});
} else {
// Show the preset cuts for other keys
keyData.cuts.forEach(cut => {
const cutElement = document.createElement('div');
cutElement.className = 'cut-indicator';
cutElement.textContent = cut + '%';
keyCutsContainer.appendChild(cutElement);
});
}
}
startGame() {
if (this.currentGame) {
this.currentGame.cleanup();
}
const params = {
pinCount: 5,
difficulty: 'medium',
thresholdSensitivity: 5,
highlightBindingOrder: true,
pinAlignmentHighlighting: true,
liftSpeed: 1.0,
lockable: { id: 'key-demo-lock' },
closeButtonText: 'Reset',
closeButtonAction: 'reset'
};
// Add key mode parameters if in key mode
if (this.currentMode === 'key') {
params.keyMode = true;
// For the "correct" key, don't pass keyData so it generates from pins
// For other keys, use the preset cuts
if (this.currentKey === 'key1') {
// Let the game generate the correct key from actual pin heights
console.log('Using auto-generated correct key from pin heights');
} else {
params.keyData = {
cuts: this.keyPresets[this.currentKey].cuts
};
}
}
console.log('Starting game with params:', params);
this.currentGame = new LockpickingMinigamePhaser(
document.getElementById('gameContainer'),
params
);
this.currentGame.init();
this.currentGame.start();
// Update key display after game starts (for auto-generated correct key)
if (this.currentMode === 'key' && this.currentKey === 'key1') {
setTimeout(() => {
this.updateKeyDisplay();
}, 100); // Small delay to ensure game is initialized
}
// Override the complete method to handle game completion
const originalComplete = this.currentGame.complete.bind(this.currentGame);
this.currentGame.complete = (success) => {
console.log('Game completed with success:', success);
if (success) {
document.getElementById('feedback').textContent =
`🎉 Success! ${this.keyPresets[this.currentKey].name} worked!`;
} else {
document.getElementById('feedback').textContent =
`❌ Failed! ${this.keyPresets[this.currentKey].name} didn't work.`;
}
// Restart after a delay
setTimeout(() => {
this.startGame();
}, 3000);
originalComplete(success);
};
// Update feedback message
if (this.currentMode === 'key') {
document.getElementById('feedback').textContent =
`Try inserting ${this.keyPresets[this.currentKey].name} into the lock (drag from left to right)`;
} else {
document.getElementById('feedback').textContent =
"Use the tension wrench and hook pick to manually pick the lock";
}
}
}
// Initialize the demo when the page loads
document.addEventListener('DOMContentLoaded', () => {
new KeyDemo();
});
</script>
</body>
</html>