Enhance locksmith-forge.html and lockpicking-game-phaser.js: Update gameplay dynamics by refining pin count, sensitivity, and lift speed calculations in locksmith-forge.html for improved level progression. Add visual feedback for binding order and pin alignment hints. In lockpicking-game-phaser.js, implement keyboard controls for pin interaction and tension wrench toggling, enhancing user experience with updated feedback mechanisms and improved visual elements.

This commit is contained in:
Z. Cliffe Schreuders
2025-08-11 01:42:27 +01:00
parent bba5145891
commit 287ab5c25c
2 changed files with 353 additions and 53 deletions

View File

@@ -130,7 +130,7 @@ export class LockpickingMinigamePhaser extends MinigameScene {
// Create a container for the Phaser game
this.gameContainer.innerHTML = `
<div class="phaser-game-container" id="phaser-game-container"></div>
<div class="lockpick-feedback">Ready to pick</div>
<div class="lockpick-feedback"></div>
`;
this.feedback = this.gameContainer.querySelector('.lockpick-feedback');
@@ -256,13 +256,14 @@ export class LockpickingMinigamePhaser extends MinigameScene {
this.tensionWrench.add(this.wrenchGraphics);
// Make it interactive - larger hit area to include horizontal arm
// Covers vertical arm, horizontal arm, and handle
this.tensionWrench.setInteractive(new Phaser.Geom.Rectangle(-12.5, -138.75, 60, 176.25), Phaser.Geom.Rectangle.Contains);
// Make it interactive - extended hit area to match pin click zones (down to keyway bottom)
// Covers vertical arm, horizontal arm, handle, and extends down to bottom of keyway
this.tensionWrench.setInteractive(new Phaser.Geom.Rectangle(-12.5, -138.75, 60, 268.75), Phaser.Geom.Rectangle.Contains);
// Add text
const wrenchText = this.scene.add.text(-10, 58, 'Tension Wrench', {
fontSize: '14px',
fontSize: '18px',
fontFamily: 'VT323',
fill: '#00ff00',
fontWeight: 'bold'
});
@@ -280,7 +281,7 @@ export class LockpickingMinigamePhaser extends MinigameScene {
// Play tension sound
if (this.sounds.tension) {
this.sounds.tension.play();
navigator.vibrate([200]);
navigator.vibrate([50]);
}
if (this.lockState.tensionApplied) {
@@ -473,7 +474,8 @@ export class LockpickingMinigamePhaser extends MinigameScene {
// Add hook pick label
const hookPickLabel = this.scene.add.text(-10, 85, 'Hook Pick', {
fontSize: '14px',
fontSize: '18px',
fontFamily: 'VT323',
fill: '#00ff00',
fontWeight: 'bold'
});
@@ -885,7 +887,8 @@ export class LockpickingMinigamePhaser extends MinigameScene {
if (i === 0) {
// Spring label
const springLabel = this.scene.add.text(pinX, pinY - 140, 'Spring', {
fontSize: '14px',
fontSize: '18px',
fontFamily: 'VT323',
fill: '#00ff00',
fontWeight: 'bold'
});
@@ -893,8 +896,10 @@ export class LockpickingMinigamePhaser extends MinigameScene {
springLabel.setDepth(100); // Bring to front
// Driver pin label - positioned below the shear line
const driverPinLabel = this.scene.add.text(pinX, pinY - 35, 'Driver Pin', {
fontSize: '14px',
const driverPinX = 100 + margin + 1 * pinSpacing; // Pin index 1 (2nd pin)
const driverPinLabel = this.scene.add.text(driverPinX, pinY - 35, 'Driver Pin', {
fontSize: '18px',
fontFamily: 'VT323',
fill: '#00ff00',
fontWeight: 'bold'
});
@@ -902,8 +907,10 @@ export class LockpickingMinigamePhaser extends MinigameScene {
driverPinLabel.setDepth(100); // Bring to front
// Key pin label - positioned at the middle of the key pin
const keyPinLabel = this.scene.add.text(pinX, pinY - 50 + driverPinLength + (keyPinLength / 2), 'Key Pin', {
fontSize: '14px',
const keyPinX = 100 + margin + 2 * pinSpacing; // Pin index 2 (3rd pin)
const keyPinLabel = this.scene.add.text(keyPinX, pinY - 50 + driverPinLength + (keyPinLength / 2), 'Key Pin', {
fontSize: '18px',
fontFamily: 'VT323',
fill: '#00ff00',
fontWeight: 'bold'
});
@@ -953,7 +960,8 @@ export class LockpickingMinigamePhaser extends MinigameScene {
// Add pin number
const pinText = this.scene.add.text(0, 40, (i + 1).toString(), {
fontSize: '14px',
fontSize: '18px',
fontFamily: 'VT323',
fill: '#ffffff',
fontWeight: 'bold'
});
@@ -983,7 +991,7 @@ export class LockpickingMinigamePhaser extends MinigameScene {
// Play click sound
if (this.sounds.click) {
this.sounds.click.play();
navigator.vibrate(200);
navigator.vibrate(50);
}
// Hide labels on first pin click
@@ -1045,7 +1053,8 @@ export class LockpickingMinigamePhaser extends MinigameScene {
// Add shear line label
const shearLineText = this.scene.add.text(503, 145, 'SHEAR LINE', {
fontSize: '14px',
fontSize: '12px',
fontFamily: 'VT323',
fill: '#00ff00',
fontWeight: 'bold'
});
@@ -1075,6 +1084,187 @@ export class LockpickingMinigamePhaser extends MinigameScene {
this.returnHookToStart();
}
});
// Add keyboard bindings
this.scene.input.keyboard.on('keydown', (event) => {
const key = event.key;
// Pin number keys (1-8)
if (key >= '1' && key <= '8') {
const pinIndex = parseInt(key) - 1; // Convert 1-8 to 0-7
// Check if pin exists
if (pinIndex < this.pinCount) {
const pin = this.pins[pinIndex];
if (pin) {
// Simulate pin click
this.lockState.currentPin = pin;
this.gameState.mouseDown = true;
// Play click sound
if (this.sounds.click) {
this.sounds.click.play();
navigator.vibrate(50);
}
// Hide labels on first pin click
if (!this.pinClicked) {
this.pinClicked = true;
if (this.wrenchText) {
this.wrenchText.setVisible(false);
}
if (this.shearLineText) {
this.shearLineText.setVisible(false);
}
if (this.hookPickLabel) {
this.hookPickLabel.setVisible(false);
}
if (this.springLabel) {
this.springLabel.setVisible(false);
}
if (this.driverPinLabel) {
this.driverPinLabel.setVisible(false);
}
if (this.keyPinLabel) {
this.keyPinLabel.setVisible(false);
}
// Hide all pin numbers
this.pins.forEach(pin => {
if (pin.pinText) {
pin.pinText.setVisible(false);
}
});
}
if (!this.lockState.tensionApplied) {
this.updateFeedback("Apply tension first before picking pins");
}
}
}
}
// SPACE key for tension wrench toggle
if (key === ' ') {
event.preventDefault(); // Prevent page scroll
// Simulate tension wrench click
this.lockState.tensionApplied = !this.lockState.tensionApplied;
// Play tension sound
if (this.sounds.tension) {
this.sounds.tension.play();
navigator.vibrate([200]);
}
if (this.lockState.tensionApplied) {
this.wrenchGraphics.clear();
this.wrenchGraphics.fillStyle(0x00ff00);
// Long vertical arm (left side of L) - same dimensions as inactive
this.wrenchGraphics.fillRect(0, -120, 10, 170);
// Short horizontal arm (bottom of L) extending into keyway - same dimensions as inactive
this.wrenchGraphics.fillRect(0, 40, 37.5, 10);
this.updateFeedback("Tension applied. Only the binding pin can be set - others will fall back down.");
} else {
this.wrenchGraphics.clear();
this.wrenchGraphics.fillStyle(0x888888);
// Long vertical arm (left side of L) - same dimensions as active
this.wrenchGraphics.fillRect(0, -120, 10, 170);
// Short horizontal arm (bottom of L) extending into keyway - same dimensions as active
this.wrenchGraphics.fillRect(0, 40, 37.5, 10);
this.updateFeedback("Tension released. All pins will fall back down.");
// Play reset sound
if (this.sounds.reset) {
this.sounds.reset.play();
}
// Reset ALL pins when tension is released (including set and overpicked ones)
this.pins.forEach(pin => {
pin.isSet = false;
pin.isOverpicked = false;
pin.currentHeight = 0;
pin.keyPinHeight = 0; // Reset key pin height
pin.driverPinHeight = 0; // Reset driver pin height
pin.overpickingTimer = null; // Reset overpicking timer
// Reset visual
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);
pin.driverPin.clear();
pin.driverPin.fillStyle(0x3388dd);
pin.driverPin.fillRect(-12, -50, 24, pin.driverPinLength);
// 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;
// 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);
if (pin.bindingHighlight) pin.bindingHighlight.setVisible(false);
if (pin.overpickedHighlight) pin.overpickedHighlight.setVisible(false);
if (pin.failureHighlight) pin.failureHighlight.setVisible(false);
});
// Reset lock state
this.lockState.pinsSet = 0;
}
this.updateBindingPins();
}
});
// Add keyboard release handler for pin keys
this.scene.input.keyboard.on('keyup', (event) => {
const key = event.key;
// Pin number keys (1-8)
if (key >= '1' && key <= '8') {
const pinIndex = parseInt(key) - 1; // Convert 1-8 to 0-7
// Check if pin exists and is currently being held
if (pinIndex < this.pinCount && this.lockState.currentPin && this.lockState.currentPin.index === pinIndex) {
this.checkPinSet(this.lockState.currentPin);
this.lockState.currentPin = null;
this.gameState.mouseDown = false;
// Return hook to resting position
if (this.hookPickGraphics && this.hookConfig) {
this.returnHookToStart();
}
}
}
});
}
update() {
@@ -1345,7 +1535,20 @@ export class LockpickingMinigamePhaser extends MinigameScene {
pin.shearHighlight.fillRect(-22.5, -110, 45, 140);
pin.container.addAt(pin.shearHighlight, 0); // Add at beginning to appear behind pins
}
// Check if highlight is transitioning from hidden to visible
const wasHidden = !pin.shearHighlight.visible;
pin.shearHighlight.setVisible(true);
// Play feedback when highlight first appears
if (wasHidden) {
if (this.sounds.click) {
this.sounds.click.play();
}
if (typeof navigator !== 'undefined' && navigator.vibrate) {
navigator.vibrate(100);
}
}
} else {
if (pin.shearHighlight) {
pin.shearHighlight.setVisible(false);
@@ -1592,7 +1795,7 @@ export class LockpickingMinigamePhaser extends MinigameScene {
// Play set sound
if (this.sounds.set) {
this.sounds.set.play();
navigator.vibrate([200,100,200]);
navigator.vibrate(500);
}
this.updateFeedback(`Pin ${pin.index + 1} set! (${this.lockState.pinsSet}/${this.pinCount})`);
@@ -1821,7 +2024,7 @@ export class LockpickingMinigamePhaser extends MinigameScene {
// Play success sound
if (this.sounds.success) {
this.sounds.success.play();
navigator.vibrate([200,100,200,100,200]);
navigator.vibrate(500);
}
this.updateFeedback("Lock picked successfully!");
@@ -2045,7 +2248,6 @@ export class LockpickingMinigamePhaser extends MinigameScene {
// 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>
<div style="font-size: 14px; margin-bottom: 15px;">All pins set at the shear line</div>
`;
// this.showSuccess(successHTML, false, 2000);

View File

@@ -236,16 +236,20 @@
max-width: 90%;
z-index: 10;
}
</style>
</head>
<body>
<div class="header">
<div class="level-display">LEVEL <span id="currentLevel">1</span></div>
<div class="stats">
<div class="stat">Pins: <span id="pinCount">3</span></div>
<div class="stat">Hints: <span id="hints">Enabled</span></div>
<div class="stat">Sensitivity: <span id="sensitivity">5</span></div>
<div class="stat">Lift Speed: <span id="liftSpeed">1.0</span></div>
<div class="stat">Binding Order: <span id="bindingHints">Enabled</span></div>
<div class="stat">Pin Alignment: <span id="alignmentHints">Enabled</span></div>
</div>
</div>
@@ -355,39 +359,69 @@
const config = {};
for (let level = 1; level <= this.maxLevel; level++) {
// Base progression
let pinCount = Math.min(3 + Math.floor((level - 1) / 5), 8); // 3-8 pins
let difficulty = this.getDifficulty(level);
let sensitivity = Math.max(1, Math.min(8, 1 + Math.floor((level - 1) / 5.57))); // 1-8, reaches 8 at level 40
let liftSpeed = Math.max(0.5, Math.min(3.0, 0.6 + (level - 1) * 0.03)); // 0.5-3.0 (starts much slower, progresses very gradually)
// Add base progression across 10-level blocks
const blockNumber = Math.floor((level - 1) / 10) + 1;
// Determine position within the current 10-level block (1-10)
const positionInBlock = ((level - 1) % 10) + 1;
// Add some randomness and complexity
if (level > 10) {
// Randomly disable hints for higher levels
const disableHints = Math.random() > 0.7;
if (disableHints) {
sensitivity = Math.max(1, sensitivity - 2);
}
// Each set of 10 levels starts with 3 pins + 1 for each block of 10 levels
let pinCount = Math.min(8, blockNumber + 2); // updated below
console.log('pinCount', pinCount);
console.log('blockNumber', blockNumber);
// Alternate between increasing speed and sensitivity within each 10-level block
let sensitivity = 1;
let liftSpeed = 0.6;
if (positionInBlock <= 5) {
// First 5 levels: increase sensitivity
sensitivity = 1 + Math.floor((positionInBlock - 1) / 2);
} else {
// Last 5 levels: increase speed
const speedLevel = positionInBlock - 5;
liftSpeed = 0.6 + (speedLevel * 0.1);
}
// Add base progression across 10-level blocks
sensitivity += blockNumber;
liftSpeed += (blockNumber * 0.2);
// every 3rd, 5th level, increase pin count
if (positionInBlock >= 5) {
pinCount = Math.min(8, pinCount + 2); // max 8 pins
} else if (positionInBlock >= 3) {
pinCount = Math.min(8, pinCount + 1); // max 8 pins
}
if (level > 20) {
// Increase difficulty more aggressively
liftSpeed = Math.min(3.0, liftSpeed + 0.1);
}
// Ensure values stay within bounds
sensitivity = Math.max(1, Math.min(8, sensitivity));
liftSpeed = Math.max(0.5, Math.min(3.0, liftSpeed));
if (level > 30) {
// Very challenging levels
pinCount = Math.min(8, pinCount + 1);
sensitivity = Math.max(1, sensitivity - 1);
// Hint settings based on position in 10-level block
let highlightBindingOrder = 'enabled';
let pinAlignmentHighlighting = 'enabled';
// Last 3 levels of each 10-level block remove hints progressively
if (positionInBlock === 8) {
// 8th level: remove binding order highlighting
highlightBindingOrder = 'disabled';
} else if (positionInBlock === 9) {
// 9th level: remove pin alignment highlighting
pinAlignmentHighlighting = 'disabled';
} else if (positionInBlock === 10) {
// 10th level: remove both
highlightBindingOrder = 'disabled';
pinAlignmentHighlighting = 'disabled';
}
config[level] = {
pinCount,
difficulty,
difficulty: this.getDifficulty(level),
sensitivity,
liftSpeed: Math.round(liftSpeed * 10) / 10,
highlightBindingOrder: (level % 10 === 0) ? 'disabled' : (level <= 15 ? 'enabled' : (Math.random() > 0.5 ? 'enabled' : 'disabled')),
pinAlignmentHighlighting: (level % 10 === 0) ? 'disabled' : (level <= 10 ? 'enabled' : (Math.random() > 0.6 ? 'enabled' : 'disabled'))
liftSpeed: parseFloat(liftSpeed.toFixed(2)),
highlightBindingOrder,
pinAlignmentHighlighting
};
}
@@ -517,23 +551,23 @@
// Check for milestone levels
const milestoneMessages = {
1: {
status: `🎯 Great start! Level 1 complete! You're on your way to becoming a lockpicking master! 🎯`,
status: `🎯 Great start! 🎯`,
achievement: `🌟 First Steps - Level 1 Complete! 🌟`
},
10: {
status: `🔥 Excellent progress! Level 10 conquered! You're getting the hang of this! 🔥`,
status: `🔥 Excellent progress! 🔥`,
achievement: `⚡ Rising Star - Level 10 Complete! ⚡`
},
20: {
status: `💪 Impressive! Level 20 mastered! Your skills are really developing! 💪`,
status: `💪 Impressive! 💪`,
achievement: `🏅 Skillful Picker - Level 20 Complete! 🏅`
},
30: {
status: `🚀 Outstanding! Level 30 achieved! You're becoming a true expert! 🚀`,
status: `🚀 Outstanding! 🚀`,
achievement: `🎖️ Expert Level - Level 30 Complete! 🎖️`
},
40: {
status: `⚔️ Phenomenal! Level 40 conquered! You're almost at the ultimate challenge! ⚔️`,
status: `⚔️ Phenomenal! ⚔️`,
achievement: `🏆 Elite Picker - Level 40 Complete! 🏆`
}
};
@@ -606,20 +640,84 @@
const config = this.levelConfig[this.currentLevel];
if (config) {
// Update values
document.getElementById('pinCount').textContent = config.pinCount;
// Show hints status based on whether visual highlighting is enabled
const hintsEnabled = config.highlightBindingOrder === 'enabled' || config.pinAlignmentHighlighting === 'enabled';
document.getElementById('hints').textContent = hintsEnabled ? 'Enabled' : 'Disabled';
document.getElementById('bindingHints').textContent = config.highlightBindingOrder === 'enabled' ? 'Visible' : 'Hidden';
document.getElementById('alignmentHints').textContent = config.pinAlignmentHighlighting === 'enabled' ? 'Visible' : 'Hidden';
document.getElementById('sensitivity').textContent = config.sensitivity;
document.getElementById('liftSpeed').textContent = config.liftSpeed;
// Apply highlighting based on level position
this.highlightBasedOnLevel(config);
}
// Update progress bar to reflect current level
this.updateProgress();
}
highlightBasedOnLevel(config) {
// Determine position within the current 10-level block (1-10)
const positionInBlock = ((this.currentLevel - 1) % 10) + 1;
const blockNumber = Math.floor((this.currentLevel - 1) / 10);
// Get all stat elements
const statElements = document.querySelectorAll('.stat');
statElements.forEach(stat => {
const span = stat.querySelector('span');
if (!span) return;
const statType = span.id;
let shouldHighlight = false;
// Determine what should be highlighted based on level position
switch (statType) {
case 'pinCount':
// Highlight on levels 3 and 5 (when pin count increases)
shouldHighlight = (positionInBlock === 3 || positionInBlock === 5);
break;
case 'sensitivity':
// Highlight on levels 1-5 (sensitivity focus phase)
shouldHighlight = (positionInBlock <= 5);
break;
case 'liftSpeed':
// Highlight on levels 6-10 (speed focus phase)
shouldHighlight = (positionInBlock >= 6);
break;
case 'bindingHints':
// Highlight when binding order hints are enabled
shouldHighlight = (config.highlightBindingOrder === 'disabled');
break;
case 'alignmentHints':
// Highlight when pin alignment hints are enabled
shouldHighlight = (config.pinAlignmentHighlighting === 'disabled');
break;
default:
shouldHighlight = false;
}
if (shouldHighlight) {
// Highlight the value
stat.style.backgroundColor = '#2a4a2a';
stat.style.color = '#00ff00';
stat.style.fontWeight = 'bold';
stat.style.transition = 'all 0.3s ease';
stat.style.opacity = '1';
} else {
// Show normally (slightly faded)
stat.style.backgroundColor = '';
stat.style.color = '';
stat.style.fontWeight = '';
stat.style.opacity = '0.7';
}
});
}
updateStatus(message) {
const feedbackElement = document.querySelector('.lockpick-feedback');
if (feedbackElement) {