diff --git a/js/minigames/lockpicking/key-selection.js b/js/minigames/lockpicking/key-selection.js new file mode 100644 index 0000000..3fb37b0 --- /dev/null +++ b/js/minigames/lockpicking/key-selection.js @@ -0,0 +1,118 @@ + +/** + * KeySelection + * + * Extracted from lockpicking-game-phaser.js + * Instantiate with: new KeySelection(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 KeySelection { + + constructor(parent) { + this.parent = parent; + } + + createKeyFromPinSizes(pinSizes) { + // Create a complete key object based on a set of pin sizes + // pinSizes: array of numbers representing the depth of each cut (0-100) + + const keyConfig = { + pinCount: pinSizes.length, + cuts: pinSizes, + // Standard key dimensions + circleRadius: 20, + shoulderWidth: 30, + shoulderHeight: 130, + bladeWidth: 420, + bladeHeight: 110, + keywayStartX: 100, + keywayStartY: 170, + keywayWidth: 400, + keywayHeight: 120 + }; + + return keyConfig; + } + + generateRandomKey(pinCount = 5) { + // Generate a random key with the specified number of pins + const cuts = []; + for (let i = 0; i < pinCount; i++) { + // Generate random cut depth between 20-80 (avoiding extremes) + cuts.push(Math.floor(Math.random() * 60) + 20); + } + return { + id: `random_key_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + cuts, + name: `Random Key`, + pinCount: pinCount + }; + } + + createKeysFromInventory(inventoryKeys, correctKeyId) { + // Create key selection from inventory keys + // inventoryKeys: array of key objects from player inventory + // correctKeyId: ID of the key that should work with this lock + + // Filter keys to only include those with cuts data + const validKeys = inventoryKeys.filter(key => key.cuts && Array.isArray(key.cuts)); + + if (validKeys.length === 0) { + // No valid keys in inventory, generate random ones + const key1 = this.parent.generateRandomKey(this.parent.pinCount); + const key2 = this.parent.generateRandomKey(this.parent.pinCount); + const key3 = this.parent.generateRandomKey(this.parent.pinCount); + + // Make the first key correct + key1.cuts = this.parent.keyData.cuts; + key1.id = correctKeyId || 'correct_key'; + key1.name = 'Correct Key'; + + // Randomize the order + const keys = [key1, key2, key3]; + this.parent.shuffleArray(keys); + + return this.parent.createKeySelectionUI(keys, correctKeyId); + } + + // Use inventory keys and randomize their order + const shuffledKeys = [...validKeys]; + this.parent.shuffleArray(shuffledKeys); + + return this.parent.createKeySelectionUI(shuffledKeys, correctKeyId); + } + + createKeysForChallenge(correctKeyId = 'challenge_key') { + // Create keys for challenge mode (like locksmith-forge.html) + // Generates 3 keys with one guaranteed correct key + + const key1 = this.parent.generateRandomKey(this.parent.pinCount); + const key2 = this.parent.generateRandomKey(this.parent.pinCount); + const key3 = this.parent.generateRandomKey(this.parent.pinCount); + + // Make the first key correct by copying the actual key cuts + key1.cuts = this.parent.keyData.cuts; + key1.id = correctKeyId; + key1.name = 'Correct Key'; + + // Give other keys descriptive names + key2.name = 'Wrong Key 1'; + key3.name = 'Wrong Key 2'; + + // Randomize the order of keys + const keys = [key1, key2, key3]; + this.parent.shuffleArray(keys); + + // Find the new index of the correct key after shuffling + const correctKeyIndex = keys.findIndex(key => key.id === correctKeyId); + + return this.parent.createKeySelectionUI(keys, correctKeyId); + } + +} diff --git a/js/minigames/lockpicking/lockpicking-game-phaser.js b/js/minigames/lockpicking/lockpicking-game-phaser.js index 0b33236..3f6ea39 100644 --- a/js/minigames/lockpicking/lockpicking-game-phaser.js +++ b/js/minigames/lockpicking/lockpicking-game-phaser.js @@ -2,6 +2,7 @@ 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'; // Phaser Lockpicking Minigame Scene implementation export class LockpickingMinigamePhaser extends MinigameScene { @@ -26,6 +27,9 @@ export class LockpickingMinigamePhaser extends MinigameScene { // Initialize KeyDataGenerator module this.keyDataGen = new KeyDataGenerator(this); + + // Initialize KeySelection module + this.keySelection = new KeySelection(this); } // Also try to load from localStorage for persistence across sessions @@ -368,103 +372,6 @@ export class LockpickingMinigamePhaser extends MinigameScene { - createKeyFromPinSizes(pinSizes) { - // Create a complete key object based on a set of pin sizes - // pinSizes: array of numbers representing the depth of each cut (0-100) - - const keyConfig = { - pinCount: pinSizes.length, - cuts: pinSizes, - // Standard key dimensions - circleRadius: 20, - shoulderWidth: 30, - shoulderHeight: 130, - bladeWidth: 420, - bladeHeight: 110, - keywayStartX: 100, - keywayStartY: 170, - keywayWidth: 400, - keywayHeight: 120 - }; - - return keyConfig; - } - - generateRandomKey(pinCount = 5) { - // Generate a random key with the specified number of pins - const cuts = []; - for (let i = 0; i < pinCount; i++) { - // Generate random cut depth between 20-80 (avoiding extremes) - cuts.push(Math.floor(Math.random() * 60) + 20); - } - return { - id: `random_key_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, - cuts, - name: `Random Key`, - pinCount: pinCount - }; - } - - createKeysFromInventory(inventoryKeys, correctKeyId) { - // Create key selection from inventory keys - // inventoryKeys: array of key objects from player inventory - // correctKeyId: ID of the key that should work with this lock - - // Filter keys to only include those with cuts data - const validKeys = inventoryKeys.filter(key => key.cuts && Array.isArray(key.cuts)); - - if (validKeys.length === 0) { - // No valid keys in inventory, generate random ones - const key1 = this.generateRandomKey(this.pinCount); - const key2 = this.generateRandomKey(this.pinCount); - const key3 = this.generateRandomKey(this.pinCount); - - // Make the first key correct - key1.cuts = this.keyData.cuts; - key1.id = correctKeyId || 'correct_key'; - key1.name = 'Correct Key'; - - // Randomize the order - const keys = [key1, key2, key3]; - this.shuffleArray(keys); - - return this.createKeySelectionUI(keys, correctKeyId); - } - - // Use inventory keys and randomize their order - const shuffledKeys = [...validKeys]; - this.shuffleArray(shuffledKeys); - - return this.createKeySelectionUI(shuffledKeys, correctKeyId); - } - - createKeysForChallenge(correctKeyId = 'challenge_key') { - // Create keys for challenge mode (like locksmith-forge.html) - // Generates 3 keys with one guaranteed correct key - - const key1 = this.generateRandomKey(this.pinCount); - const key2 = this.generateRandomKey(this.pinCount); - const key3 = this.generateRandomKey(this.pinCount); - - // Make the first key correct by copying the actual key cuts - key1.cuts = this.keyData.cuts; - key1.id = correctKeyId; - key1.name = 'Correct Key'; - - // Give other keys descriptive names - key2.name = 'Wrong Key 1'; - key3.name = 'Wrong Key 2'; - - // Randomize the order of keys - const keys = [key1, key2, key3]; - this.shuffleArray(keys); - - // Find the new index of the correct key after shuffling - const correctKeyIndex = keys.findIndex(key => key.id === correctKeyId); - - return this.createKeySelectionUI(keys, correctKeyId); - } - startWithKeySelection(inventoryKeys = null, correctKeyId = null) { // Start the minigame with key selection instead of a default key // inventoryKeys: array of keys from inventory (optional) @@ -474,10 +381,10 @@ export class LockpickingMinigamePhaser extends MinigameScene { if (inventoryKeys && inventoryKeys.length > 0) { // Use provided inventory keys - this.createKeysFromInventory(inventoryKeys, correctKeyId); + this.keySelection.createKeysFromInventory(inventoryKeys, correctKeyId); } else { // Generate random keys for challenge - this.createKeysForChallenge(correctKeyId || 'challenge_key'); + this.keySelection.createKeysForChallenge(correctKeyId || 'challenge_key'); } } @@ -1339,7 +1246,7 @@ export class LockpickingMinigamePhaser extends MinigameScene { // For challenge mode (locksmith-forge.html), use the training interface if (this.params?.lockable?.id === 'progressive-challenge') { // This is the locksmith-forge.html challenge mode - this.createKeysForChallenge('correct_key'); + this.keySelection.createKeysForChallenge('correct_key'); } else { // This is the main game - go back to key selection this.startWithKeySelection(); diff --git a/scripts/extract_lockpicking_methods.py b/scripts/extract_lockpicking_methods.py index b369fcc..2a74cd8 100644 --- a/scripts/extract_lockpicking_methods.py +++ b/scripts/extract_lockpicking_methods.py @@ -134,9 +134,19 @@ class MethodExtractor: return None start_line, end_line = result - # Include the full lines, joining them back with newlines + # Extract lines as-is from the source method_lines = self.lines[start_line:end_line+1] - method_code = '\n'.join(method_lines) + + # Strip the leading 4-space class indentation from all non-empty lines, + # since the module template will apply the correct indentation level + dedented_lines = [] + for line in method_lines: + if line.startswith(' ') and line.strip(): # Has 4-space indent and not empty + dedented_lines.append(line[4:]) # Remove the 4-space class indent + else: + dedented_lines.append(line) # Keep as-is (empty or already correct) + + method_code = '\n'.join(dedented_lines) if replace_this: method_code = self.replace_this_with_parent(method_code, use_parent_keyword=True) @@ -294,7 +304,10 @@ class MainFileUpdater: print(f"✓ Removed method: {method_name}") - return '\n'.join(updated_lines) + # Update both self.lines and self.content + self.lines = updated_lines + self.content = '\n'.join(updated_lines) + return self.content def add_import(self, class_name: str, module_path: str) -> str: """ @@ -309,6 +322,13 @@ class MainFileUpdater: """ lines = self.content.split('\n') + # Check if this import already exists + import_stmt = f"import {{ {class_name} }} from '{module_path}';" + for line in lines: + if import_stmt in line: + # Import already exists, no need to add + return self.content + # Find where to insert import (after existing imports, before class definition) insert_idx = 0 for i, line in enumerate(lines): @@ -317,11 +337,12 @@ class MainFileUpdater: elif line.startswith('export class'): break - import_stmt = f"import {{ {class_name} }} from '{module_path}';" + # Insert the new import statement lines.insert(insert_idx, import_stmt) # Update content for next operations self.content = '\n'.join(lines) + self.lines = lines return self.content def add_module_initialization(self, instance_name: str, class_name: str) -> str: @@ -335,6 +356,12 @@ class MainFileUpdater: Returns: Updated content with initialization added """ + # Check if initialization already exists to prevent duplicates + init_pattern = f'this.{instance_name} = new {class_name}(this)' + if init_pattern in self.content: + print(f"ℹ️ Initialization for {instance_name} already exists, skipping") + return self.content + lines = self.content.split('\n') # Find constructor and its opening brace @@ -372,7 +399,8 @@ class MainFileUpdater: init_stmt += f"\n this.{instance_name} = new {class_name}(this);" lines.insert(init_idx, init_stmt) - # Update content for next operations + # Update content and lines for next operations + self.lines = lines self.content = '\n'.join(lines) return self.content @@ -390,11 +418,16 @@ class MainFileUpdater: updated = self.content for method_name in method_names: - # Pattern: this.methodName( - # Replace with: this.moduleInstance.methodName( - pattern = rf'this\.{method_name}\(' - replacement = f'this.{module_instance}.{method_name}(' - updated = re.sub(pattern, replacement, updated) + # Pattern 1: this.methodName( -> this.moduleInstance.methodName( + pattern_this = rf'this\.{method_name}\(' + replacement_this = f'this.{module_instance}.{method_name}(' + updated = re.sub(pattern_this, replacement_this, updated) + + # Pattern 2: self.methodName( -> self.moduleInstance.methodName( + # (common pattern in Phaser where scenes save const self = this) + pattern_self = rf'self\.{method_name}\(' + replacement_self = f'self.{module_instance}.{method_name}(' + updated = re.sub(pattern_self, replacement_self, updated) # Update content for next operations self.content = updated @@ -456,16 +489,23 @@ class ModuleGenerator: ) -> str: """Generate a class module.""" extends_str = f" extends {extends}" if extends else "" + + # Join all methods without adding additional indentation. Extracted + # methods already contain their original leading whitespace, so we + # preserve them exactly by joining with blank lines only. + methods_code = '\n\n'.join(methods.values()) - # Join all methods with proper spacing - methods_code = '\n\n '.join(methods.values()) - - # Add constructor if using parent instance pattern + # Add 4-space indentation to every line of methods_code to indent at class level + indented_methods = '\n'.join(' ' + line if line.strip() else line + for line in methods_code.split('\n')) + + # Add constructor if using parent instance pattern. Constructor + # should use the same 4-space method indentation level. if use_parent_instance: - constructor = """constructor(parent) { + constructor = """ constructor(parent) { this.parent = parent; }""" - methods_code = constructor + '\n\n ' + methods_code + indented_methods = constructor + '\n\n' + indented_methods code = f"""{imports_section} /** @@ -483,7 +523,7 @@ class ModuleGenerator: */ export class {class_name}{extends_str} {{ - {methods_code} +{indented_methods} }} """ @@ -497,17 +537,23 @@ export class {class_name}{extends_str} {{ use_parent_instance: bool = True ) -> str: """Generate an object/namespace module.""" - # Convert methods to object methods - methods_code = '\n\n '.join(methods.values()) + # Convert methods to object methods. Preserve original leading + # whitespace from extracted methods by joining with blank lines only. + methods_code = '\n\n'.join(methods.values()) - # Add init function if using parent instance pattern + # Add 4-space indentation to every line of methods_code to indent at object level + indented_methods = '\n'.join(' ' + line if line.strip() else line + for line in methods_code.split('\n')) + + # Add init function if using parent instance pattern. Use 4-space + # indentation for the init function to match method indentation. if use_parent_instance: - init_func = """init(parent) { + init_func = """ init(parent) { return { parent: parent }; }""" - methods_code = init_func + '\n\n ' + methods_code + indented_methods = init_func + '\n\n' + indented_methods code = f"""{imports_section} /** @@ -525,7 +571,7 @@ export class {class_name}{extends_str} {{ */ export const {object_name} = {{ - {methods_code} +{indented_methods} }}; """