mirror of
https://github.com/cliffe/BreakEscape.git
synced 2026-02-20 13:50:46 +00:00
800 lines
34 KiB
JavaScript
800 lines
34 KiB
JavaScript
import { MinigameScene } from '../framework/base-minigame.js';
|
||
import { LockConfiguration } from './lock-configuration.js';
|
||
import { LockGraphics } from './lock-graphics.js';
|
||
import { KeyDataGenerator } from './key-data-generator.js';
|
||
import { KeySelection } from './key-selection.js';
|
||
import { KeyOperations } from './key-operations.js';
|
||
import { PinManagement } from './pin-management.js';
|
||
import { ToolManager } from './tool-manager.js';
|
||
import { KeyAnimation } from './key-animation.js';
|
||
import { HookMechanics } from './hook-mechanics.js';
|
||
import { PinVisuals } from './pin-visuals.js';
|
||
import { KeyInsertion } from './key-insertion.js';
|
||
import { KeyDrawing } from './key-drawing.js';
|
||
import { KeyPathDrawing } from './key-path-drawing.js';
|
||
import { KeyGeometry } from './key-geometry.js';
|
||
import { GameUtilities } from './game-utilities.js';
|
||
|
||
// Phaser Lockpicking Minigame Scene implementation
|
||
export class LockpickingMinigamePhaser extends MinigameScene {
|
||
constructor(container, params) {
|
||
super(container, params);
|
||
|
||
// Ensure params is an object
|
||
params = params || {};
|
||
|
||
console.log('🎮 Lockpicking minigame constructor received params:', {
|
||
predefinedPinHeights: params.predefinedPinHeights,
|
||
difficulty: params.difficulty,
|
||
pinCount: params.pinCount,
|
||
lockableType: params.lockable?.doorProperties ? 'door' : params.lockable?.scenarioData ? 'item' : 'unknown'
|
||
});
|
||
|
||
this.lockable = params.lockable || 'default-lock';
|
||
this.lockId = params.lockId || 'default_lock';
|
||
this.difficulty = params.difficulty || 'medium';
|
||
|
||
// Determine pin count: prioritize based on keyPins array length from scenario
|
||
let pinCount = params.pinCount;
|
||
let predefinedPinHeights = params.predefinedPinHeights;
|
||
|
||
console.log('🔍 pinCount determination started:', {
|
||
explicitPinCount: pinCount,
|
||
predefinedPinHeights: predefinedPinHeights,
|
||
difficulty: this.difficulty
|
||
});
|
||
|
||
// If predefinedPinHeights not in params, try to extract from lockable object
|
||
if (!predefinedPinHeights && this.lockable) {
|
||
console.log('🔍 Attempting to extract predefinedPinHeights from lockable object');
|
||
if (this.lockable.doorProperties?.keyPins) {
|
||
predefinedPinHeights = this.lockable.doorProperties.keyPins;
|
||
console.log(`✓ Extracted predefinedPinHeights from lockable.doorProperties:`, predefinedPinHeights);
|
||
} else if (this.lockable.scenarioData?.keyPins) {
|
||
predefinedPinHeights = this.lockable.scenarioData.keyPins;
|
||
console.log(`✓ Extracted predefinedPinHeights from lockable.scenarioData:`, predefinedPinHeights);
|
||
} else if (this.lockable.keyPins) {
|
||
predefinedPinHeights = this.lockable.keyPins;
|
||
console.log(`✓ Extracted predefinedPinHeights from lockable.keyPins:`, predefinedPinHeights);
|
||
} else {
|
||
console.warn('⚠ Could not extract predefinedPinHeights from lockable object');
|
||
}
|
||
}
|
||
|
||
// Store for use in pin management
|
||
this.params = params;
|
||
this.params.predefinedPinHeights = predefinedPinHeights;
|
||
|
||
// If pinCount not explicitly provided, derive from predefinedPinHeights (keyPins from scenario)
|
||
if (!pinCount && predefinedPinHeights && Array.isArray(predefinedPinHeights)) {
|
||
pinCount = predefinedPinHeights.length;
|
||
console.log(`✓ Determined pinCount ${pinCount} from predefinedPinHeights array length: [${predefinedPinHeights.join(', ')}]`);
|
||
}
|
||
|
||
// Fall back to difficulty-based pin count if still not set
|
||
if (!pinCount) {
|
||
pinCount = this.difficulty === 'easy' ? 3 : this.difficulty === 'medium' ? 4 : 5;
|
||
console.log(`⚠ Using difficulty-based pinCount: ${pinCount} (difficulty: ${this.difficulty})`);
|
||
}
|
||
|
||
|
||
this.pinCount = pinCount;
|
||
|
||
// Initialize global lock storage if it doesn't exist
|
||
if (!window.lockConfigurations) {
|
||
window.lockConfigurations = {};
|
||
}
|
||
|
||
// Initialize KeyDataGenerator module
|
||
this.keyDataGen = new KeyDataGenerator(this);
|
||
|
||
// Initialize KeySelection module
|
||
this.keySelection = new KeySelection(this);
|
||
|
||
// Initialize KeyOperations module
|
||
this.keyOps = new KeyOperations(this);
|
||
|
||
// Initialize PinManagement module
|
||
this.pinMgmt = new PinManagement(this);
|
||
|
||
// Initialize ToolManager module
|
||
this.toolMgr = new ToolManager(this);
|
||
|
||
// Initialize KeyAnimation module
|
||
this.keyAnim = new KeyAnimation(this);
|
||
|
||
// Initialize HookMechanics module
|
||
this.hookMech = new HookMechanics(this);
|
||
|
||
// Initialize PinVisuals module
|
||
this.pinVisuals = new PinVisuals(this);
|
||
|
||
// Initialize KeyInsertion module
|
||
this.keyInsertion = new KeyInsertion(this);
|
||
|
||
// Initialize KeyDrawing module
|
||
this.keyDraw = new KeyDrawing(this);
|
||
|
||
// Initialize KeyPathDrawing module
|
||
this.keyPathDraw = new KeyPathDrawing(this);
|
||
|
||
// Initialize KeyGeometry module
|
||
this.keyGeom = new KeyGeometry(this);
|
||
|
||
// Initialize GameUtilities module
|
||
this.gameUtil = new GameUtilities(this);
|
||
|
||
// Also try to load from localStorage for persistence across sessions
|
||
if (!window.lockConfigurations[this.lockId]) {
|
||
try {
|
||
const savedConfigs = localStorage.getItem('lockConfigurations');
|
||
if (savedConfigs) {
|
||
const parsed = JSON.parse(savedConfigs);
|
||
window.lockConfigurations = { ...window.lockConfigurations, ...parsed };
|
||
}
|
||
} catch (error) {
|
||
console.warn('Failed to load lock configurations from localStorage:', error);
|
||
}
|
||
}
|
||
|
||
// Threshold sensitivity for pin setting (1-10, higher = more sensitive)
|
||
this.thresholdSensitivity = params.thresholdSensitivity || 5;
|
||
|
||
// Whether to highlight binding order
|
||
this.highlightBindingOrder = params.highlightBindingOrder !== undefined ? params.highlightBindingOrder : true;
|
||
|
||
// Whether to highlight pin alignment (shear line proximity)
|
||
this.highlightPinAlignment = params.highlightPinAlignment !== undefined ? params.highlightPinAlignment : true;
|
||
|
||
// Lift speed parameter (can be set to fast values, but reasonable default for hard)
|
||
this.liftSpeed = params.liftSpeed || (this.difficulty === 'hard' ? 1.2 : 1);
|
||
|
||
// Close button customization
|
||
this.closeButtonText = params.cancelText || 'Cancel';
|
||
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;
|
||
this.skipStartingKey = params.skipStartingKey || false; // Skip creating initial key if true
|
||
this.keySelectionMode = false; // Track if we're in key selection mode
|
||
|
||
// Mode switching settings
|
||
this.canSwitchToPickMode = params.canSwitchToPickMode || false; // Allow switching from key to pick mode
|
||
this.inventoryKeys = params.inventoryKeys || null; // Stored for mode switching
|
||
this.requirefKeyId = params.requiredKeyId || null; // Track required key ID
|
||
this.canSwitchToKeyMode = params.canSwitchToKeyMode || false; // Allow switching from lockpick to key mode
|
||
this.availableKeys = params.availableKeys || null; // Keys available for mode switching
|
||
|
||
// Sound effects
|
||
this.sounds = {};
|
||
|
||
// Track if any pin has been clicked (for hiding labels)
|
||
this.pinClicked = false;
|
||
|
||
// Log the configuration for debugging
|
||
console.log('Lockpicking minigame config:', {
|
||
lockable: this.lockable,
|
||
difficulty: this.difficulty,
|
||
pinCount: this.pinCount,
|
||
passedPinCount: params.pinCount,
|
||
thresholdSensitivity: this.thresholdSensitivity,
|
||
highlightBindingOrder: this.highlightBindingOrder,
|
||
highlightPinAlignment: this.highlightPinAlignment,
|
||
liftSpeed: this.liftSpeed,
|
||
canSwitchToPickMode: this.canSwitchToPickMode,
|
||
canSwitchToKeyMode: this.canSwitchToKeyMode
|
||
});
|
||
|
||
this.pins = [];
|
||
this.lockState = {
|
||
tensionApplied: false,
|
||
pinsSet: 0,
|
||
currentPin: null
|
||
};
|
||
|
||
this.game = null;
|
||
this.scene = null;
|
||
|
||
// Initialize lock configuration module
|
||
this.lockConfig = new LockConfiguration(this);
|
||
|
||
// Initialize lock graphics module
|
||
this.lockGraphics = new LockGraphics(this);
|
||
}
|
||
|
||
// Method to get the lock's pin configuration for key generation
|
||
init() {
|
||
super.init();
|
||
|
||
// Customize the close button
|
||
const closeBtn = document.getElementById('minigame-close');
|
||
if (closeBtn) {
|
||
closeBtn.textContent = '×';
|
||
|
||
// Remove the default close action
|
||
this._eventListeners = this._eventListeners.filter(listener =>
|
||
!(listener.element === closeBtn && listener.eventType === 'click')
|
||
);
|
||
|
||
// Add custom action based on closeButtonAction parameter
|
||
if (this.closeButtonAction === 'reset') {
|
||
this.addEventListener(closeBtn, 'click', () => {
|
||
this.pinMgmt.resetAllPins();
|
||
this.keyInsertion.updateFeedback("Lock reset - try again");
|
||
});
|
||
} else {
|
||
// Default close action
|
||
this.addEventListener(closeBtn, 'click', () => {
|
||
this.complete(false);
|
||
});
|
||
}
|
||
}
|
||
|
||
// Customize the cancel button
|
||
const cancelBtn = document.getElementById('minigame-cancel');
|
||
if (cancelBtn) {
|
||
cancelBtn.textContent = this.closeButtonText;
|
||
|
||
// Remove the default cancel action
|
||
this._eventListeners = this._eventListeners.filter(listener =>
|
||
!(listener.element === cancelBtn && listener.eventType === 'click')
|
||
);
|
||
|
||
// Add custom action based on closeButtonAction parameter
|
||
if (this.closeButtonAction === 'reset') {
|
||
this.addEventListener(cancelBtn, 'click', () => {
|
||
this.pinMgmt.resetAllPins();
|
||
this.keyInsertion.updateFeedback("Lock reset - try again");
|
||
});
|
||
} else {
|
||
// Default cancel action
|
||
this.addEventListener(cancelBtn, 'click', () => {
|
||
this.complete(false);
|
||
});
|
||
}
|
||
}
|
||
|
||
this.headerElement.innerHTML = `
|
||
<h3>Lockpicking</h3>
|
||
<p>Apply tension and hold click on pins to lift them to the shear line</p>
|
||
`;
|
||
|
||
// Create the lockable item display section if item info is provided
|
||
this.createLockableItemDisplay();
|
||
|
||
this.setupPhaserGame();
|
||
}
|
||
|
||
createLockableItemDisplay() {
|
||
// Create display for the locked item (door, chest, etc.)
|
||
const itemName = this.params?.itemName || this.lockable || 'Locked Item';
|
||
const itemImage = this.params?.itemImage || null;
|
||
const itemObservations = this.params?.itemObservations || '';
|
||
|
||
if (!itemImage) return; // Only create if image is provided
|
||
|
||
// Create container for the item display
|
||
const itemDisplayDiv = document.createElement('div');
|
||
itemDisplayDiv.className = 'lockpicking-item-section';
|
||
itemDisplayDiv.innerHTML = `
|
||
<img src="${itemImage}"
|
||
alt="${itemName}"
|
||
class="lockpicking-item-image">
|
||
<div class="lockpicking-item-info">
|
||
<h4>${itemName}</h4>
|
||
<p>${itemObservations}</p>
|
||
</div>
|
||
`;
|
||
|
||
// Add mode switch button if applicable
|
||
if (this.canSwitchToPickMode && this.keyMode) {
|
||
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.toolMgr.switchToPickMode();
|
||
|
||
buttonContainer.appendChild(switchModeBtn);
|
||
itemDisplayDiv.appendChild(buttonContainer);
|
||
} else if (this.canSwitchToKeyMode && !this.keyMode) {
|
||
// Show switch to key mode button when in lockpicking mode
|
||
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-to-keys-btn';
|
||
switchModeBtn.innerHTML = '<img src="assets/objects/key.png" alt="Key" class="icon-large"> Switch to Key Mode';
|
||
switchModeBtn.onclick = () => this.toolMgr.switchToKeyMode();
|
||
|
||
buttonContainer.appendChild(switchModeBtn);
|
||
itemDisplayDiv.appendChild(buttonContainer);
|
||
}
|
||
|
||
// Insert before the game container
|
||
this.gameContainer.parentElement.insertBefore(itemDisplayDiv, this.gameContainer);
|
||
}
|
||
|
||
setupPhaserGame() {
|
||
// Create a container for the Phaser game
|
||
this.gameContainer.innerHTML = `
|
||
<div class="phaser-game-container" id="phaser-game-container"></div>
|
||
`;
|
||
|
||
// Create feedback element in the minigame container
|
||
this.feedback = document.createElement('div');
|
||
this.feedback.className = 'lockpick-feedback';
|
||
this.gameContainer.appendChild(this.feedback);
|
||
|
||
console.log('Setting up Phaser game...');
|
||
|
||
// Create a custom Phaser scene
|
||
const self = this;
|
||
class LockpickingScene extends Phaser.Scene {
|
||
constructor() {
|
||
super({ key: 'LockpickingScene' });
|
||
}
|
||
|
||
preload() {
|
||
// Load sound effects
|
||
this.load.audio('lockpick_binding', 'assets/sounds/lockpick_binding.mp3');
|
||
this.load.audio('lockpick_click', 'assets/sounds/lockpick_click.mp3');
|
||
this.load.audio('lockpick_overtension', 'assets/sounds/lockpick_overtension.mp3');
|
||
this.load.audio('lockpick_reset', 'assets/sounds/lockpick_reset.mp3');
|
||
this.load.audio('lockpick_set', 'assets/sounds/lockpick_set.mp3');
|
||
this.load.audio('lockpick_success', 'assets/sounds/lockpick_success.mp3');
|
||
this.load.audio('lockpick_tension', 'assets/sounds/lockpick_tension.mp3');
|
||
this.load.audio('lockpick_wrong', 'assets/sounds/lockpick_wrong.mp3');
|
||
}
|
||
|
||
create() {
|
||
console.log('Phaser scene create() called');
|
||
// Store reference to the scene
|
||
self.scene = this;
|
||
|
||
// Initialize sound effects
|
||
self.sounds.binding = this.sound.add('lockpick_binding');
|
||
self.sounds.click = this.sound.add('lockpick_click');
|
||
self.sounds.overtension = this.sound.add('lockpick_overtension');
|
||
self.sounds.reset = this.sound.add('lockpick_reset');
|
||
self.sounds.set = this.sound.add('lockpick_set');
|
||
self.sounds.success = this.sound.add('lockpick_success');
|
||
self.sounds.tension = this.sound.add('lockpick_tension');
|
||
self.sounds.wrong = this.sound.add('lockpick_wrong');
|
||
|
||
// Create game elements
|
||
self.lockGraphics.createLockBackground();
|
||
self.lockGraphics.createTensionWrench();
|
||
self.pinMgmt.createPins();
|
||
self.lockGraphics.createHookPick();
|
||
self.pinMgmt.createShearLine();
|
||
|
||
// Create key if in key mode and not skipping starting key
|
||
if (self.keyMode && !self.skipStartingKey) {
|
||
self.keyOps.createKey();
|
||
self.toolMgr.hideLockpickingTools();
|
||
self.keyInsertion.updateFeedback("Click the key to insert it into the lock");
|
||
} else if (self.keyMode && self.skipStartingKey) {
|
||
// Skip creating initial key, will show key selection instead
|
||
// But we still need to initialize keyData for the correct key
|
||
if (!self.keyData) {
|
||
self.keyDataGen.generateKeyDataFromPins();
|
||
}
|
||
self.toolMgr.hideLockpickingTools();
|
||
self.keyInsertion.updateFeedback("Select a key to begin");
|
||
} else {
|
||
self.keyInsertion.updateFeedback("Apply tension first, then lift pins in binding order - only the binding pin can be set");
|
||
}
|
||
|
||
self.pinMgmt.setupInputHandlers();
|
||
console.log('Phaser scene setup complete');
|
||
}
|
||
|
||
update() {
|
||
if (self.update) {
|
||
self.update();
|
||
}
|
||
}
|
||
}
|
||
|
||
// Initialize Phaser game
|
||
const config = {
|
||
type: Phaser.AUTO,
|
||
parent: 'phaser-game-container',
|
||
width: 600,
|
||
height: 400,
|
||
backgroundColor: '#1a1a1a',
|
||
scene: LockpickingScene,
|
||
scale: {
|
||
mode: Phaser.Scale.FIT,
|
||
autoCenter: Phaser.Scale.CENTER_BOTH
|
||
}
|
||
};
|
||
|
||
// Adjust canvas size for mobile to crop empty space
|
||
if (window.innerWidth <= 768) {
|
||
// Crop the viewport to focus on the lock area
|
||
// Lock is positioned from x=100 to x=500, y=50 to y=350
|
||
// So we can crop to roughly x=80 to x=520, y=30 to y=370
|
||
const cropWidth = 510; // 520 - 80
|
||
const cropHeight = 300; // 370 - 30
|
||
|
||
// Calculate scale to fit the cropped area
|
||
const containerWidth = document.getElementById('phaser-game-container').offsetWidth;
|
||
const containerHeight = document.getElementById('phaser-game-container').offsetHeight;
|
||
|
||
// Scale to fit the cropped area within the container
|
||
const scaleX = containerWidth / cropWidth;
|
||
const scaleY = containerHeight / cropHeight;
|
||
const scale = Math.min(scaleX, scaleY);
|
||
|
||
config.width = cropWidth;
|
||
config.height = cropHeight;
|
||
config.scale = {
|
||
mode: Phaser.Scale.FIT,
|
||
autoCenter: Phaser.Scale.CENTER_BOTH
|
||
};
|
||
}
|
||
|
||
try {
|
||
this.game = new Phaser.Game(config);
|
||
this.scene = this.game.scene.getScene('LockpickingScene');
|
||
console.log('Phaser game created, scene:', this.scene);
|
||
} catch (error) {
|
||
console.error('Error creating Phaser game:', error);
|
||
this.keyInsertion.updateFeedback('Error loading Phaser game: ' + error.message);
|
||
}
|
||
}
|
||
|
||
|
||
|
||
startWithKeySelection(inventoryKeys = null, correctKeyId = null) {
|
||
// Start the minigame with key selection instead of a default key
|
||
// inventoryKeys: array of keys from inventory (optional)
|
||
// correctKeyId: ID of the correct key (optional)
|
||
|
||
this.keySelectionMode = true; // Mark that we're in key selection mode
|
||
|
||
if (inventoryKeys && inventoryKeys.length > 0) {
|
||
// Use provided inventory keys
|
||
this.keySelection.createKeysFromInventory(inventoryKeys, correctKeyId);
|
||
} else {
|
||
// Generate random keys for challenge
|
||
this.keySelection.createKeysForChallenge(correctKeyId || 'challenge_key');
|
||
}
|
||
}
|
||
|
||
// Example usage:
|
||
//
|
||
// 1. For BreakEscape main game with inventory keys:
|
||
// const playerKeys = [
|
||
// { id: 'office_key', cuts: [45, 67, 23, 89, 34], name: 'Office Key' },
|
||
// { id: 'basement_key', cuts: [12, 78, 56, 23, 90], name: 'Basement Key' },
|
||
// { id: 'shed_key', cuts: [67, 34, 89, 12, 45], name: 'Shed Key' }
|
||
// ];
|
||
// this.startWithKeySelection(playerKeys, 'office_key');
|
||
//
|
||
// 2. For challenge mode (like locksmith-forge.html):
|
||
// this.startWithKeySelection(); // Generates 3 random keys, one correct
|
||
//
|
||
// 3. Skip starting key and go straight to selection:
|
||
// const minigame = new LockpickingMinigamePhaser(container, {
|
||
// keyMode: true,
|
||
// skipStartingKey: true, // Don't create initial key
|
||
// lockId: 'office_door_lock'
|
||
// });
|
||
// minigame.startWithKeySelection(playerKeys, 'office_key');
|
||
|
||
createKeySelectionUI(keys, correctKeyId = null) {
|
||
// Create a UI for selecting between multiple keys
|
||
// keys: array of key objects with id, cuts, and optional name properties
|
||
// correctKeyId: ID of the correct key (if null, uses index 0 as fallback)
|
||
// Shows 3 keys at a time with navigation buttons for more than 3 keys
|
||
|
||
// Find the correct key index in the original array
|
||
let correctKeyIndex = 0;
|
||
if (correctKeyId) {
|
||
correctKeyIndex = keys.findIndex(key => key.id === correctKeyId);
|
||
if (correctKeyIndex === -1) {
|
||
correctKeyIndex = 0; // Fallback to first key if ID not found
|
||
}
|
||
}
|
||
|
||
// Remove any existing key from the scene before showing selection UI
|
||
if (this.keyGroup) {
|
||
this.keyGroup.destroy();
|
||
this.keyGroup = null;
|
||
}
|
||
|
||
// Remove any existing click zone
|
||
if (this.keyClickZone) {
|
||
this.keyClickZone.destroy();
|
||
this.keyClickZone = null;
|
||
}
|
||
|
||
// Reset pins to their original positions before showing key selection
|
||
this.lockConfig.resetPinsToOriginalPositions();
|
||
|
||
// Layout constants
|
||
const keyWidth = 140;
|
||
const keyHeight = 80;
|
||
const spacing = 20;
|
||
const padding = 20;
|
||
const labelHeight = 30; // Space for key label below each key
|
||
const keysPerPage = 3; // Always show 3 keys at a time
|
||
const buttonWidth = 30;
|
||
const buttonHeight = 30;
|
||
const buttonSpacing = 10; // Space between button and keys
|
||
|
||
// Calculate container dimensions (always 3 keys wide + buttons on sides with minimal spacing)
|
||
// For 3 keys: [button] padding [key1] spacing [key2] spacing [key3] padding [button]
|
||
const keysWidth = (keysPerPage - 1) * (keyWidth + spacing) + keyWidth; // 3 keys with spacing between them
|
||
const containerWidth = keysWidth + (keys.length > keysPerPage ? buttonWidth * 2 + buttonSpacing * 2 + padding * 2 : padding * 2);
|
||
const containerHeight = keyHeight + labelHeight + spacing + padding * 2 + 50; // +50 for title
|
||
|
||
// Create container for key selection - positioned in the middle but below pins
|
||
const keySelectionContainer = this.scene.add.container(0, 230);
|
||
keySelectionContainer.setDepth(1000); // High z-index to appear above everything
|
||
|
||
// Add background
|
||
const background = this.scene.add.graphics();
|
||
background.fillStyle(0x000000, 0.8);
|
||
background.fillRect(0, 0, containerWidth, containerHeight);
|
||
background.lineStyle(2, 0xffffff);
|
||
background.strokeRect(0, 0, containerWidth - 1, containerHeight - 1);
|
||
keySelectionContainer.add(background);
|
||
|
||
// Add title
|
||
const titleX = containerWidth / 2;
|
||
const title = this.scene.add.text(titleX, 15, 'Select the correct key', {
|
||
fontSize: '24px',
|
||
fill: '#ffffff',
|
||
fontFamily: 'VT323',
|
||
});
|
||
title.setOrigin(0.5, 0);
|
||
keySelectionContainer.add(title);
|
||
|
||
// Track current page
|
||
let currentPage = 0;
|
||
const totalPages = Math.ceil(keys.length / keysPerPage);
|
||
|
||
// Create navigation buttons if more than 3 keys
|
||
let prevButton = null;
|
||
let nextButton = null;
|
||
let prevText = null;
|
||
let nextText = null;
|
||
let pageIndicator = null;
|
||
let itemsToRemoveNext = null; // Track items for cleanup
|
||
|
||
// Create a function to render the current page of keys
|
||
const renderKeyPage = () => {
|
||
// Remove any existing key visuals and labels from the previous page
|
||
const itemsToRemove = [];
|
||
keySelectionContainer.list.forEach(item => {
|
||
if (item !== background && item !== title && item !== prevButton && item !== nextButton && item !== pageIndicator && item !== prevText && item !== nextText) {
|
||
itemsToRemove.push(item);
|
||
}
|
||
});
|
||
itemsToRemove.forEach(item => item.destroy());
|
||
|
||
// Calculate which keys to show on this page
|
||
const startIndex = currentPage * keysPerPage;
|
||
const endIndex = Math.min(startIndex + keysPerPage, keys.length);
|
||
const pageKeys = keys.slice(startIndex, endIndex);
|
||
|
||
// Display keys for this page
|
||
// Position: [button] buttonSpacing [keys] buttonSpacing [button]
|
||
const keysStartX = (keys.length > keysPerPage ? buttonWidth + buttonSpacing : padding);
|
||
const startX = keysStartX + padding / 2;
|
||
const startY = 50;
|
||
|
||
pageKeys.forEach((keyData, pageIndex) => {
|
||
const actualIndex = startIndex + pageIndex;
|
||
const keyX = startX + pageIndex * (keyWidth + spacing);
|
||
const keyY = startY;
|
||
|
||
// Create key visual representation
|
||
const keyVisual = this.keyOps.createKeyVisual(keyData, keyWidth, keyHeight);
|
||
keyVisual.setPosition(keyX, keyY);
|
||
keySelectionContainer.add(keyVisual);
|
||
|
||
// Make key clickable
|
||
keyVisual.setInteractive(new Phaser.Geom.Rectangle(0, 0, keyWidth, keyHeight), Phaser.Geom.Rectangle.Contains);
|
||
keyVisual.on('pointerdown', () => {
|
||
// Close the popup
|
||
keySelectionContainer.destroy();
|
||
// Trigger key selection and insertion
|
||
this.keyOps.selectKey(actualIndex, correctKeyIndex, keyData);
|
||
});
|
||
|
||
// Add key label (use name if available, otherwise use number)
|
||
const keyName = keyData.name || `Key ${actualIndex + 1}`;
|
||
const keyLabel = this.scene.add.text(keyX + keyWidth/2, keyY + keyHeight + 5, keyName, {
|
||
fontSize: '16px',
|
||
fill: '#ffffff',
|
||
fontFamily: 'VT323'
|
||
});
|
||
keyLabel.setOrigin(0.5, 0);
|
||
keySelectionContainer.add(keyLabel);
|
||
});
|
||
|
||
// Update page indicator
|
||
if (pageIndicator) {
|
||
pageIndicator.setText(`${currentPage + 1}/${totalPages}`);
|
||
}
|
||
|
||
// Update button visibility
|
||
if (prevButton) {
|
||
if (currentPage > 0) {
|
||
prevButton.setVisible(true);
|
||
prevText.setVisible(true);
|
||
} else {
|
||
prevButton.setVisible(false);
|
||
prevText.setVisible(false);
|
||
}
|
||
}
|
||
|
||
if (nextButton) {
|
||
if (currentPage < totalPages - 1) {
|
||
nextButton.setVisible(true);
|
||
nextText.setVisible(true);
|
||
} else {
|
||
nextButton.setVisible(false);
|
||
nextText.setVisible(false);
|
||
}
|
||
}
|
||
};
|
||
|
||
if (keys.length > keysPerPage) {
|
||
// Position buttons on the sides of the keys, vertically centered
|
||
const keysAreaCenterY = 50 + (keyHeight + labelHeight) / 2;
|
||
|
||
// Previous button (left side)
|
||
prevButton = this.scene.add.graphics();
|
||
prevButton.fillStyle(0x444444);
|
||
prevButton.fillRect(0, 0, buttonWidth, buttonHeight);
|
||
prevButton.lineStyle(2, 0xffffff);
|
||
prevButton.strokeRect(0, 0, buttonWidth, buttonHeight);
|
||
prevButton.setInteractive(new Phaser.Geom.Rectangle(0, 0, buttonWidth, buttonHeight), Phaser.Geom.Rectangle.Contains);
|
||
prevButton.on('pointerdown', () => {
|
||
if (currentPage > 0) {
|
||
currentPage--;
|
||
renderKeyPage();
|
||
}
|
||
});
|
||
prevButton.setPosition(padding / 2, keysAreaCenterY - buttonHeight / 2);
|
||
prevButton.setDepth(1001);
|
||
prevButton.setVisible(false); // Initially hidden
|
||
keySelectionContainer.add(prevButton);
|
||
|
||
// Previous button text
|
||
prevText = this.scene.add.text(padding / 2 + buttonWidth / 2, keysAreaCenterY, '‹', {
|
||
fontSize: '20px',
|
||
fill: '#ffffff',
|
||
fontFamily: 'VT323'
|
||
});
|
||
prevText.setOrigin(0.5, 0.5);
|
||
prevText.setDepth(1002);
|
||
prevText.setVisible(false); // Initially hidden
|
||
keySelectionContainer.add(prevText);
|
||
|
||
// Next button (right side)
|
||
nextButton = this.scene.add.graphics();
|
||
nextButton.fillStyle(0x444444);
|
||
nextButton.fillRect(0, 0, buttonWidth, buttonHeight);
|
||
nextButton.lineStyle(2, 0xffffff);
|
||
nextButton.strokeRect(0, 0, buttonWidth, buttonHeight);
|
||
nextButton.setInteractive(new Phaser.Geom.Rectangle(0, 0, buttonWidth, buttonHeight), Phaser.Geom.Rectangle.Contains);
|
||
nextButton.on('pointerdown', () => {
|
||
if (currentPage < totalPages - 1) {
|
||
currentPage++;
|
||
renderKeyPage();
|
||
}
|
||
});
|
||
nextButton.setPosition(containerWidth - padding / 2 - buttonWidth, keysAreaCenterY - buttonHeight / 2);
|
||
nextButton.setDepth(1001);
|
||
nextButton.setVisible(false); // Initially hidden
|
||
keySelectionContainer.add(nextButton);
|
||
|
||
// Next button text
|
||
nextText = this.scene.add.text(containerWidth - padding / 2 - buttonWidth / 2, keysAreaCenterY, '›', {
|
||
fontSize: '20px',
|
||
fill: '#ffffff',
|
||
fontFamily: 'VT323'
|
||
});
|
||
nextText.setOrigin(0.5, 0.5);
|
||
nextText.setDepth(1002);
|
||
nextText.setVisible(false); // Initially hidden
|
||
keySelectionContainer.add(nextText);
|
||
|
||
// Page indicator - centered below all keys
|
||
pageIndicator = this.scene.add.text(containerWidth / 2, containerHeight - 20, `1/${totalPages}`, {
|
||
fontSize: '12px',
|
||
fill: '#888888',
|
||
fontFamily: 'VT323'
|
||
});
|
||
pageIndicator.setOrigin(0.5, 0.5);
|
||
keySelectionContainer.add(pageIndicator);
|
||
}
|
||
|
||
// Render the first page
|
||
renderKeyPage();
|
||
|
||
this.keySelectionContainer = keySelectionContainer;
|
||
}
|
||
|
||
drawCircleAsPolygon(graphics, centerX, centerY, radius) {
|
||
// Draw a circle as a polygon path to match the blade drawing method
|
||
const path = new Phaser.Geom.Polygon();
|
||
|
||
// Create circle points
|
||
const segments = 32; // Number of segments for smooth circle
|
||
for (let i = 0; i <= segments; i++) {
|
||
const angle = (i / segments) * Math.PI * 2;
|
||
const x = centerX + Math.cos(angle) * radius;
|
||
const y = centerY + Math.sin(angle) * radius;
|
||
path.points.push(new Phaser.Geom.Point(x, y));
|
||
}
|
||
|
||
// Draw the circle as a polygon
|
||
graphics.fillPoints(path.points, true, true);
|
||
}
|
||
|
||
createKeyBladeCollision() {
|
||
// Method moved to KeyOperations module - call via this.keyOps.createKeyBladeCollision()
|
||
this.keyOps.createKeyBladeCollision();
|
||
}
|
||
|
||
update() {
|
||
// Skip normal lockpicking logic if in key mode
|
||
if (this.keyMode) {
|
||
return;
|
||
}
|
||
|
||
if (this.lockState.currentPin && this.gameState.mouseDown) {
|
||
this.pinMgmt.liftPin();
|
||
}
|
||
|
||
// Apply gravity when tension is not applied (but not when actively lifting)
|
||
if (!this.lockState.tensionApplied && !this.gameState.mouseDown) {
|
||
this.pinMgmt.applyGravity();
|
||
}
|
||
|
||
// Apply gravity to non-binding pins even with tension
|
||
if (this.lockState.tensionApplied && !this.gameState.mouseDown) {
|
||
this.pinMgmt.applyGravity();
|
||
}
|
||
|
||
// Check if all pins are correctly positioned when tension is applied
|
||
if (this.lockState.tensionApplied) {
|
||
this.pinMgmt.checkAllPinsCorrect();
|
||
}
|
||
|
||
// Hook return is now handled directly in pointerup event
|
||
}
|
||
|
||
complete(success) {
|
||
if (this.game) {
|
||
this.game.destroy(true);
|
||
this.game = null;
|
||
}
|
||
super.complete(success, this.gameResult);
|
||
}
|
||
|
||
}
|