diff --git a/FUNCTION_INVENTORY.md b/FUNCTION_INVENTORY.md new file mode 100644 index 0000000..e595fdd --- /dev/null +++ b/FUNCTION_INVENTORY.md @@ -0,0 +1,350 @@ +# Complete Function Inventory - All 78 Functions + +## Summary + +**Total Functions:** 78 +**File:** `js/minigames/lockpicking/lockpicking-game-phaser.js` +**File Size:** 4,669 lines + +--- + +## All Functions by Category (Planned Refactoring Phases) + +### Phase 1: Lock Configuration (6 functions) +Lines 100-205 | Lock state persistence + +``` +1. saveLockConfiguration (100-125) +2. getLockPinConfiguration (128-141) +3. loadLockConfiguration (143-151) +4. clearLockConfiguration (153-170) +5. clearAllLockConfigurations (172-184) +6. resetPinsToOriginalPositions (186-205) +``` + +### Phase 2: Lock Graphics (3 functions) +Lines 465-778 | Visual rendering of lock + +``` +7. createLockBackground (465-485) +8. createTensionWrench (487-621) +9. createHookPick (623-778) +``` + +### Phase 3: Key Data Generator (8 functions) +Lines 780-918 | Key creation and calculations + +``` +10. generateKeyDataFromPins (780-821) +11. createKeyFromPinSizes (823-843) +12. generateRandomKey (845-858) +13. createKeysFromInventory (860-891) +14. createKeysForChallenge (893-918) +15. startWithKeySelection (920-934) +16. createKeySelectionUI (957-1044) +17. createKeyVisual (1046-1094) +``` + +### Phase 4: Pin System (13 functions) +Lines 2904-3233 | Pin creation, physics, state + +``` +18. createPins (2904-3195) +19. createShearLine (3197-3233) +20. liftPin (3488-3758) [Input handling] +21. applyGravity (3760-3857) +22. checkAllPinsCorrect (3859-3912) +23. checkPinSet (3914-4095) +24. shouldPinBind (4097-4108) +25. updateBindingPins (4110-4157) +26. resetAllPins (4159-4208) +27. updatePinHighlighting (2817-2844) +28. updatePinVisuals (2846-2902) +29. liftCollidedPin (2797-2815) +30. checkHookCollisions (2695-2793) +``` + +### Phase 5: Key Rendering (17 functions) +Lines 1176-2298 | Key visual generation and rendering + +``` +31. createKey (1176-1282) +32. drawKeyWithRenderTexture (1284-1332) +33. drawKeyBladeAsSolidShape (1334-1441) +34. addTriangularSectionToPath (1443-1466) +35. addFirstCutPeakToPath (1468-1500) +36. addTriangularPeakToPath (1502-1531) +37. addPointedTipToPath (1533-1567) +38. addRightPointingTriangleToPath (1569-1612) +39. drawCircleAsPolygon (1614-1629) +40. drawPixelArtCircleToGraphics (1631-1658) +41. generateKeyPolygonPoints (2224-2298) +42. addTriangularPeakToPoints (2326-2348) +43. addPointedTipToPoints (2350-2376) +44. getTriangularSectionHeightAtX (2380-2466) +45. getTriangularSectionHeightAsKeyMoves (2468-2524) +46. getKeySurfaceHeightAtPosition (2565-2581) +47. findVerticalIntersection (2300-2324) +``` + +### Phase 6: Key Selection UI (4 functions) +Lines 1098-1174 | Key selection interface + +``` +48. selectKey (1098-1148) +49. showWrongKeyFeedback (1150-1161) +50. flashLockRed (1163-1174) +51. createKeyBladeCollision (2526-2563) +``` + +### Phase 7: Input Handlers (4 functions) +Lines 3235-3758 | User input and interaction + +``` +52. setupInputHandlers (3235-3458) +53. liftPin (3488-3758) [Already in Pin System] +54. updateHookPosition (2601-2662) +55. returnHookToStart (2664-2693) +``` + +### Phase 8: Completion Handler (2 functions) +Lines 3859-4212 | Lock picking completion logic + +``` +56. checkAllPinsCorrect (3859-3912) [Already in Pin System] +57. lockPickingSuccess (4214-4465) +``` + +### Phase 9: UI Elements (6 functions) +Lines 207-330 | Buttons, labels, display setup + +``` +58. init (207-267) +59. createLockableItemDisplay (269-330) +60. updateFeedback (4210-4212) +61. hideLockpickingTools (2583-2599) +62. showLockpickingTools (4583-4599) +63. setupPhaserGame (332-461) +``` + +### Phase 10: Mode Switching (2 functions) +Lines 4532-4669 | Switch between pick and key mode + +``` +64. switchToPickMode (4532-4581) +65. switchToKeyMode (4601-4669) +``` + +### Phase 11: Key Insertion & Animation (5 functions) +Lines 1662-2133 | Key insertion and movement + +``` +66. startKeyInsertion (1662-1713) +67. updateKeyPosition (1715-1730) +68. checkKeyCorrectness (1732-1802) +69. snapPinsToExactPositions (1804-1871) +70. startKeyRotationAnimationWithChamberHoles (1873-2093) +71. liftPinsWithKey (2099-2133) +72. updatePinsWithKeyInsertion (2135-2198) +73. getKeySurfaceHeightAtPinPosition (2200-2222) +``` + +### Phase 12: Utilities & Other (7 functions) +Lines 4491-4670 | Helper functions and lifecycle + +``` +74. shuffleArray (4491-4497) +75. flashWrenchRed (4499-4530) +76. start (4467-4473) +77. complete (4475-4481) +78. cleanup (4483-4489) +79. update (3460-3486) [Main update loop] +``` + +--- + +## Function Coverage Analysis + +### By Phase + +| Phase | Name | Count | Lines | Coverage | +|-------|------|-------|-------|----------| +| 1 | Lock Configuration | 6 | 106 | 2.3% | +| 2 | Lock Graphics | 3 | 314 | 6.7% | +| 3 | Key Data Generator | 8 | 139 | 3.0% | +| 4 | Pin System | 13 | 329 | 7.0% | +| 5 | Key Rendering | 17 | 1,122 | 24% | +| 6 | Key Selection UI | 4 | 48 | 1.0% | +| 7 | Input Handlers | 4 | 224 | 4.8% | +| 8 | Completion Handler | 2 | 252 | 5.4% | +| 9 | UI Elements | 6 | 255 | 5.5% | +| 10 | Mode Switching | 2 | 138 | 3.0% | +| 11 | Key Insertion | 8 | 471 | 10% | +| 12 | Utilities | 7 | 207 | 4.4% | +| **TOTAL** | | **78** | **4,669** | **100%** | + +--- + +## Function Quick Reference + +### All 78 Functions (Alphabetical) + +``` +1. addFirstCutPeakToPath +2. addPointedTipToPath +3. addPointedTipToPoints +4. addRightPointingTriangleToPath +5. addTriangularPeakToPath +6. addTriangularPeakToPoints +7. addTriangularSectionToPath +8. applyGravity +9. checkAllPinsCorrect +10. checkHookCollisions +11. checkKeyCorrectness +12. checkPinSet +13. cleanup +14. clearAllLockConfigurations +15. clearLockConfiguration +16. complete +17. constructor +18. createKey +19. createKeyBladeCollision +20. createKeyFromPinSizes +21. createKeySelectionUI +22. createKeyVisual +23. createKeysForChallenge +24. createKeysFromInventory +25. createLockableItemDisplay +26. createLockBackground +27. createPins +28. createShearLine +29. createTensionWrench +30. createHookPick +31. drawCircleAsPolygon +32. drawKeyBladeAsSolidShape +33. drawKeyWithRenderTexture +34. drawPixelArtCircleToGraphics +35. findVerticalIntersection +36. flashLockRed +37. flashWrenchRed +38. generateKeyDataFromPins +39. generateKeyPolygonPoints +40. generateRandomKey +41. getLockPinConfiguration +42. getKeySurfaceHeightAtPinPosition +43. getKeySurfaceHeightAtPosition +44. getTriangularSectionHeightAsKeyMoves +45. getTriangularSectionHeightAtX +46. hideLockpickingTools +47. init +48. liftCollidedPin +49. liftPin +50. liftPinsWithKey +51. loadLockConfiguration +52. lockPickingSuccess +53. resetAllPins +54. resetPinsToOriginalPositions +55. returnHookToStart +56. saveLockConfiguration +57. selectKey +58. setupInputHandlers +59. setupPhaserGame +60. shouldPinBind +61. showLockpickingTools +62. showWrongKeyFeedback +63. shuffleArray +64. snapPinsToExactPositions +65. start +66. startKeyInsertion +67. startKeyRotationAnimationWithChamberHoles +68. startWithKeySelection +69. switchToKeyMode +70. switchToPickMode +71. update +72. updateBindingPins +73. updateFeedback +74. updateHookPosition +75. updateKeyPosition +76. updatePinHighlighting +77. updatePinVisuals +78. updatePinsWithKeyInsertion +``` + +--- + +## Verification Checklist + +Use this to verify nothing is missed in refactoring: + +- [ ] Phase 1: 6 functions accounted for +- [ ] Phase 2: 3 functions accounted for +- [ ] Phase 3: 8 functions accounted for +- [ ] Phase 4: 13 functions accounted for +- [ ] Phase 5: 17 functions accounted for +- [ ] Phase 6: 4 functions accounted for +- [ ] Phase 7: 4 functions accounted for +- [ ] Phase 8: 2 functions accounted for +- [ ] Phase 9: 6 functions accounted for +- [ ] Phase 10: 2 functions accounted for +- [ ] Phase 11: 8 functions accounted for +- [ ] Phase 12: 7 functions accounted for +- [ ] **Total: 78 functions** ✓ + +--- + +## How to Use This List + +### 1. Verify After Each Phase + +After extracting a phase, verify the functions were moved: + +```bash +# After Phase 1 extraction: +python3 scripts/list_js_functions.py --file js/minigames/lockpicking/lockpicking-game-phaser.js | grep -E "saveLockConfiguration|loadLockConfiguration|clearLockConfiguration" + +# Should show 0 results if fully extracted +``` + +### 2. Generate List for Copy-Pasting + +```bash +# Get copy-paste friendly list for command line: +python3 scripts/list_js_functions.py --file js/minigames/lockpicking/lockpicking-game-phaser.js --format copy-paste +``` + +### 3. Filter by Phase + +```bash +# List functions with keyword: +python3 scripts/list_js_functions.py --file js/minigames/lockpicking/lockpicking-game-phaser.js --grep "Key" --format list + +# Result: All functions with "Key" in name +``` + +### 4. Track Progress + +Before refactoring: **78 functions** +After Phase 1: **78 - 6 = 72 remaining in main file** +After Phase 2: **72 - 3 = 69 remaining in main file** +... and so on + +--- + +## Notes + +- **Constructor:** Should remain in main class (line 5) +- **Update method:** Core Phaser method, keep in main class +- **Start/Complete/Cleanup:** Lifecycle methods, keep in main class +- Some functions (like `liftPin`) have multiple responsibilities and appear in multiple phases +- Line numbers are from current state of file (may shift during extraction) + +--- + +## Generated With + +`scripts/list_js_functions.py` - Simple script to list all JS functions + +Usage: +```bash +python3 scripts/list_js_functions.py --file --format +``` diff --git a/js/minigames/lockpicking/lock-configuration.js b/js/minigames/lockpicking/lock-configuration.js new file mode 100644 index 0000000..2d84e70 --- /dev/null +++ b/js/minigames/lockpicking/lock-configuration.js @@ -0,0 +1,127 @@ + +/** + * LockConfiguration + * + * Extracted from lockpicking-game-phaser.js + * Instantiate with: new LockConfiguration(this) + * + * All 'this' references replaced with 'parent' to access parent instance state: + * - parent.pins (array of pin objects) + * - parent.scene (Phaser scene) + * - parent.lockId (lock identifier) + * - parent.lockState (lock state object) + * etc. + */ +export class LockConfiguration { + + constructor(parent) { + this.parent = parent; + } + + saveLockConfiguration() { + // Save the current lock configuration to global storage and localStorage + if (this.parent.pins && this.parent.pins.length > 0) { + const pinHeights = this.parent.pins.map(pin => pin.originalHeight); + const config = { + pinHeights: pinHeights, + pinCount: this.parent.pinCount, + timestamp: Date.now() + }; + + // Save to memory + window.lockConfigurations[this.parent.lockId] = config; + + // Save to localStorage for persistence + try { + const savedConfigs = localStorage.getItem('lockConfigurations') || '{}'; + const parsed = JSON.parse(savedConfigs); + parsed[this.parent.lockId] = config; + localStorage.setItem('lockConfigurations', JSON.stringify(parsed)); + } catch (error) { + console.warn('Failed to save lock configuration to localStorage:', error); + } + + console.log(`Saved lock configuration for ${this.parent.lockId}:`, pinHeights); + } + } + + loadLockConfiguration() { + // Load lock configuration from global storage + const config = window.lockConfigurations[this.parent.lockId]; + if (config && config.pinHeights && config.pinHeights.length === this.parent.pinCount) { + console.log(`Loaded lock configuration for ${this.parent.lockId}:`, config.pinHeights); + return config.pinHeights; + } + return null; + } + + clearLockConfiguration() { + // Clear the lock configuration for this lock + if (window.lockConfigurations[this.parent.lockId]) { + delete window.lockConfigurations[this.parent.lockId]; + + // Also remove from localStorage + try { + const savedConfigs = localStorage.getItem('lockConfigurations') || '{}'; + const parsed = JSON.parse(savedConfigs); + delete parsed[this.parent.lockId]; + localStorage.setItem('lockConfigurations', JSON.stringify(parsed)); + } catch (error) { + console.warn('Failed to clear lock configuration from localStorage:', error); + } + + console.log(`Cleared lock configuration for ${this.parent.lockId}`); + } + } + + getLockPinConfiguration() { + if (!this.parent.pins || this.parent.pins.length === 0) { + return null; + } + + return { + pinCount: this.parent.pinCount, + pinHeights: this.parent.pins.map(pin => pin.originalHeight), + pinLengths: this.parent.pins.map(pin => ({ + keyPinLength: pin.keyPinLength, + driverPinLength: pin.driverPinLength + })) + }; + } + + clearAllLockConfigurations() { + // Clear all lock configurations (useful for testing) + window.lockConfigurations = {}; + + // Also clear from localStorage + try { + localStorage.removeItem('lockConfigurations'); + } catch (error) { + console.warn('Failed to clear all lock configurations from localStorage:', error); + } + + console.log('Cleared all lock configurations'); + } + + resetPinsToOriginalPositions() { + // Reset all pins to their original positions (before any key insertion) + this.parent.pins.forEach(pin => { + pin.currentHeight = 0; + pin.isSet = false; + + // Clear any highlights + if (pin.shearHighlight) { + pin.shearHighlight.setVisible(false); + } + if (pin.setHighlight) { + pin.setHighlight.setVisible(false); + } + + // Update pin visuals + this.parent.updatePinVisuals(pin); + }); + + console.log('Reset all pins to original positions'); + } + +} diff --git a/js/minigames/lockpicking/lockpicking-game-phaser.js b/js/minigames/lockpicking/lockpicking-game-phaser.js index 5615609..d4a864e 100644 --- a/js/minigames/lockpicking/lockpicking-game-phaser.js +++ b/js/minigames/lockpicking/lockpicking-game-phaser.js @@ -1,4 +1,5 @@ import { MinigameScene } from '../framework/base-minigame.js'; +import { LockConfiguration } from './lock-configuration.js'; // Phaser Lockpicking Minigame Scene implementation export class LockpickingMinigamePhaser extends MinigameScene { @@ -95,115 +96,12 @@ export class LockpickingMinigamePhaser extends MinigameScene { this.game = null; this.scene = null; - } - - saveLockConfiguration() { - // Save the current lock configuration to global storage and localStorage - if (this.pins && this.pins.length > 0) { - const pinHeights = this.pins.map(pin => pin.originalHeight); - const config = { - pinHeights: pinHeights, - pinCount: this.pinCount, - timestamp: Date.now() - }; - - // Save to memory - window.lockConfigurations[this.lockId] = config; - - // Save to localStorage for persistence - try { - const savedConfigs = localStorage.getItem('lockConfigurations') || '{}'; - const parsed = JSON.parse(savedConfigs); - parsed[this.lockId] = config; - localStorage.setItem('lockConfigurations', JSON.stringify(parsed)); - } catch (error) { - console.warn('Failed to save lock configuration to localStorage:', error); - } - - console.log(`Saved lock configuration for ${this.lockId}:`, pinHeights); - } + + // Initialize lock configuration module + this.lockConfig = new LockConfiguration(this); } // Method to get the lock's pin configuration for key generation - getLockPinConfiguration() { - if (!this.pins || this.pins.length === 0) { - return null; - } - - return { - pinCount: this.pinCount, - pinHeights: this.pins.map(pin => pin.originalHeight), - pinLengths: this.pins.map(pin => ({ - keyPinLength: pin.keyPinLength, - driverPinLength: pin.driverPinLength - })) - }; - } - - loadLockConfiguration() { - // Load lock configuration from global storage - const config = window.lockConfigurations[this.lockId]; - if (config && config.pinHeights && config.pinHeights.length === this.pinCount) { - console.log(`Loaded lock configuration for ${this.lockId}:`, config.pinHeights); - return config.pinHeights; - } - return null; - } - - clearLockConfiguration() { - // Clear the lock configuration for this lock - if (window.lockConfigurations[this.lockId]) { - delete window.lockConfigurations[this.lockId]; - - // Also remove from localStorage - try { - const savedConfigs = localStorage.getItem('lockConfigurations') || '{}'; - const parsed = JSON.parse(savedConfigs); - delete parsed[this.lockId]; - localStorage.setItem('lockConfigurations', JSON.stringify(parsed)); - } catch (error) { - console.warn('Failed to clear lock configuration from localStorage:', error); - } - - console.log(`Cleared lock configuration for ${this.lockId}`); - } - } - - clearAllLockConfigurations() { - // Clear all lock configurations (useful for testing) - window.lockConfigurations = {}; - - // Also clear from localStorage - try { - localStorage.removeItem('lockConfigurations'); - } catch (error) { - console.warn('Failed to clear all lock configurations from localStorage:', error); - } - - console.log('Cleared all lock configurations'); - } - - resetPinsToOriginalPositions() { - // Reset all pins to their original positions (before any key insertion) - this.pins.forEach(pin => { - pin.currentHeight = 0; - pin.isSet = false; - - // Clear any highlights - if (pin.shearHighlight) { - pin.shearHighlight.setVisible(false); - } - if (pin.setHighlight) { - pin.setHighlight.setVisible(false); - } - - // Update pin visuals - this.updatePinVisuals(pin); - }); - - console.log('Reset all pins to original positions'); - } - init() { super.init(); @@ -981,7 +879,7 @@ export class LockpickingMinigamePhaser extends MinigameScene { } // Reset pins to their original positions before showing key selection - this.resetPinsToOriginalPositions(); + this.lockConfig.resetPinsToOriginalPositions(); // Create container for key selection - positioned in the middle but below pins const keySelectionContainer = this.scene.add.container(0, 230); @@ -1117,7 +1015,7 @@ export class LockpickingMinigamePhaser extends MinigameScene { } // Reset pins to their original positions before creating the new key - this.resetPinsToOriginalPositions(); + this.lockConfig.resetPinsToOriginalPositions(); // Store the original correct key data (this determines if the key is correct) const originalKeyData = this.keyData; @@ -2913,7 +2811,7 @@ export class LockpickingMinigamePhaser extends MinigameScene { const margin = pinSpacing * 0.75; // 25% smaller margins // Try to load saved pin heights for this lock - const savedPinHeights = this.loadLockConfiguration(); + const savedPinHeights = this.lockConfig.loadLockConfiguration(); // Check if predefined pin heights were passed const predefinedPinHeights = this.params?.predefinedPinHeights; @@ -3191,7 +3089,7 @@ export class LockpickingMinigamePhaser extends MinigameScene { } // Save the lock configuration after all pins are created - this.saveLockConfiguration(); + this.lockConfig.saveLockConfiguration(); } createShearLine() { @@ -4574,7 +4472,7 @@ export class LockpickingMinigamePhaser extends MinigameScene { } // Reset pins to original positions - this.resetPinsToOriginalPositions(); + this.lockConfig.resetPinsToOriginalPositions(); // Update feedback this.updateFeedback("Lockpicking mode - Apply tension first, then lift pins in binding order"); @@ -4627,7 +4525,7 @@ export class LockpickingMinigamePhaser extends MinigameScene { } // Reset pins to original positions - this.resetPinsToOriginalPositions(); + this.lockConfig.resetPinsToOriginalPositions(); // Add mode switch back button (can switch back to lockpicking if available) if (this.canSwitchToPickMode) { diff --git a/scripts/extract_lockpicking_methods.py b/scripts/extract_lockpicking_methods.py new file mode 100644 index 0000000..b369fcc --- /dev/null +++ b/scripts/extract_lockpicking_methods.py @@ -0,0 +1,739 @@ +#!/usr/bin/env python3 +""" +Extract methods from lockpicking-game-phaser.js into separate modules. + +Usage: + python3 extract_lockpicking_methods.py --methods "method1,method2,method3" --output-file "output.js" [--class-name "ClassName"] + +Example: + python3 extract_lockpicking_methods.py \\ + --methods "createLockBackground,createTensionWrench,createHookPick" \\ + --output-file "lock-graphics.js" \\ + --class-name "LockGraphics" +""" + +import argparse +import re +import sys +from pathlib import Path +from typing import List, Dict, Tuple, Optional, Set + + +class MethodExtractor: + """Extract methods from JavaScript class files.""" + + def __init__(self, input_file: str): + """Initialize with input file path.""" + self.input_file = Path(input_file) + self.content = self.input_file.read_text(encoding='utf-8') + self.lines = self.content.split('\n') + + def replace_this_with_parent(self, code: str, use_parent_keyword: bool = True) -> str: + """ + Replace 'this' references with 'this.parent' for extracted modules. + + This allows extracted methods to access the parent instance state properly. + Uses 'this.parent' so it works within instance methods. + + Args: + code: Method code containing 'this' references + use_parent_keyword: If True, replace 'this' with 'this.parent'; if False, leave as-is + + Returns: + Modified code with replacements + """ + if not use_parent_keyword: + return code + + lines = code.split('\n') + modified_lines = [] + + for line in lines: + # Skip comment lines + if line.strip().startswith('//'): + modified_lines.append(line) + continue + + modified_line = line + + # Replace 'this.' with 'this.parent.' for method bodies + # This allows instance methods to access parent state via this.parent + modified_line = re.sub(r'\bthis\.', 'this.parent.', modified_line) + + modified_lines.append(modified_line) + + return '\n'.join(modified_lines) + + def find_method(self, method_name: str) -> Optional[Tuple[int, int]]: + """ + Find method definition and return start/end line numbers (0-indexed). + + Returns: + Tuple of (start_line, end_line) or None if not found + """ + # Pattern: optional whitespace, method name, optional whitespace, parentheses + method_pattern = rf'^\s*{re.escape(method_name)}\s*\(' + + start_line = None + for i, line in enumerate(self.lines): + if re.match(method_pattern, line): + start_line = i + break + + if start_line is None: + return None + + # Find the opening brace + brace_line = start_line + for i in range(start_line, len(self.lines)): + if '{' in self.lines[i]: + brace_line = i + break + + # Count braces to find the matching closing brace + brace_count = 0 + found_opening = False + end_line = None + + for i in range(brace_line, len(self.lines)): + line = self.lines[i] + + for char in line: + if char == '{': + brace_count += 1 + found_opening = True + elif char == '}': + if found_opening: + brace_count -= 1 + if brace_count == 0: + end_line = i + break + + if end_line is not None: + break + + if end_line is None: + return None + + return (start_line, end_line) + + def extract_method(self, method_name: str, replace_this: bool = False) -> Optional[str]: + """ + Extract a single method as a string. + + Args: + method_name: Name of method to extract + replace_this: If True, replace 'this' with 'parent' for module usage + + Returns: + Method code as string, or None if not found + """ + result = self.find_method(method_name) + if result is None: + print(f"❌ Method '{method_name}' not found", file=sys.stderr) + return None + + start_line, end_line = result + # Include the full lines, joining them back with newlines + method_lines = self.lines[start_line:end_line+1] + method_code = '\n'.join(method_lines) + + if replace_this: + method_code = self.replace_this_with_parent(method_code, use_parent_keyword=True) + + return method_code + + def extract_methods(self, method_names: List[str], replace_this: bool = False) -> Dict[str, str]: + """ + Extract multiple methods. + + Args: + method_names: List of method names to extract + replace_this: If True, replace 'this' with 'parent' in extracted code + + Returns: + Dict mapping method_name -> method_code + """ + extracted = {} + for method_name in method_names: + code = self.extract_method(method_name, replace_this=replace_this) + if code: + extracted[method_name] = code + print(f"✓ Extracted: {method_name}") + else: + print(f"✗ Failed to extract: {method_name}") + + return extracted + + def find_dependencies(self, methods: Dict[str, str]) -> Set[str]: + """ + Find method dependencies (methods called by extracted methods). + + Returns: + Set of method names that are called but not in the extraction list + """ + # Pattern for method calls: this.methodName( or other_object.methodName( + method_call_pattern = r'(?:this\.|[\w]+\.)?(\w+)\s*\(' + + dependencies = set() + all_method_names = set(methods.keys()) + + for method_code in methods.values(): + matches = re.finditer(method_call_pattern, method_code) + for match in matches: + called_method = match.group(1) + # Skip standard JS functions and common names + if not self._is_builtin_or_common(called_method): + if called_method not in all_method_names: + dependencies.add(called_method) + + return dependencies + + @staticmethod + def _is_builtin_or_common(name: str) -> bool: + """Check if name is a builtin or common function.""" + builtins = { + 'console', 'Math', 'Object', 'Array', 'String', 'Number', + 'parseInt', 'parseFloat', 'isNaN', 'JSON', 'Date', 'RegExp', + 'Error', 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval', + 'document', 'window', 'localStorage', 'addEventListener', + 'removeEventListener', 'querySelector', 'getElementById', 'createElement', + 'appendChild', 'removeChild', 'insertBefore', 'textContent', 'innerHTML', + 'setAttribute', 'getAttribute', 'classList', 'add', 'remove', 'contains', + 'push', 'pop', 'shift', 'unshift', 'splice', 'slice', 'concat', 'join', + 'split', 'map', 'filter', 'reduce', 'forEach', 'find', 'findIndex', + 'includes', 'indexOf', 'length', 'keys', 'values', 'entries', + 'Object', 'assign', 'create', 'defineProperty', 'defineProperties', + 'getOwnPropertyNames', 'getOwnPropertyDescriptor', 'seal', 'freeze', + 'prototype', 'constructor', 'instanceof', 'typeof', 'in', 'of', + 'delete', 'new', 'super', 'this', 'return', 'if', 'else', 'for', + 'while', 'do', 'switch', 'case', 'break', 'continue', 'try', + 'catch', 'finally', 'throw', 'async', 'await', 'yield', 'static', + 'class', 'extends', 'import', 'export', 'default', 'from', 'as', + 'let', 'const', 'var', 'true', 'false', 'null', 'undefined', + 'add', 'set', 'get', 'on', 'once', 'off', 'emit', 'listen', + 'startX', 'startY', 'endX', 'endY', 'width', 'height', 'x', 'y', + 'fill', 'stroke', 'draw', 'render', 'create', 'update', 'init', + 'tweens', 'time', 'scene', 'add', 'graphics', 'text', 'container', + 'setAngle', 'setDepth', 'setOrigin', 'setVisible', 'setTint', 'setPosition', + 'destroy', 'setScale', 'setAlpha', 'setInteractive', 'on', 'once', + 'rotationCenterX', 'rotationCenterY', 'targetPin', 'lastTargetedPin', + 'log', 'warn', 'error', 'debug', 'info', 'assert', 'time', 'timeEnd', + } + return name in builtins + + +class MainFileUpdater: + """Update the main lockpicking file to use extracted modules.""" + + def __init__(self, main_file: str): + """Initialize with main file path.""" + self.main_file = Path(main_file) + self.content = self.main_file.read_text(encoding='utf-8') + self.lines = self.content.split('\n') + + def remove_methods(self, method_names: List[str]) -> str: + """ + Remove method definitions from the main file. + + Args: + method_names: List of method names to remove + + Returns: + Updated file content + """ + updated_lines = self.lines.copy() + + for method_name in method_names: + # Find the method + start_idx = None + for i, line in enumerate(updated_lines): + if re.match(rf'^\s*{re.escape(method_name)}\s*\(', line): + start_idx = i + break + + if start_idx is None: + print(f"⚠️ Method '{method_name}' not found in main file") + continue + + # Find opening brace + brace_idx = start_idx + for i in range(start_idx, len(updated_lines)): + if '{' in updated_lines[i]: + brace_idx = i + break + + # Count braces to find matching closing brace + brace_count = 0 + found_opening = False + end_idx = None + + for i in range(brace_idx, len(updated_lines)): + line = updated_lines[i] + + for char in line: + if char == '{': + brace_count += 1 + found_opening = True + elif char == '}': + if found_opening: + brace_count -= 1 + if brace_count == 0: + end_idx = i + break + + if end_idx is not None: + break + + if end_idx is not None: + # Remove the method and surrounding whitespace + del updated_lines[start_idx:end_idx+1] + # Remove empty lines that follow + while updated_lines and updated_lines[start_idx].strip() == '': + del updated_lines[start_idx] + + print(f"✓ Removed method: {method_name}") + + return '\n'.join(updated_lines) + + def add_import(self, class_name: str, module_path: str) -> str: + """ + Add import statement at the top of the file. + + Args: + class_name: Name of class/object being imported + module_path: Relative path to module (e.g., './lock-configuration.js') + + Returns: + Updated content with import added + """ + lines = self.content.split('\n') + + # Find where to insert import (after existing imports, before class definition) + insert_idx = 0 + for i, line in enumerate(lines): + if line.startswith('import '): + insert_idx = i + 1 + elif line.startswith('export class'): + break + + import_stmt = f"import {{ {class_name} }} from '{module_path}';" + lines.insert(insert_idx, import_stmt) + + # Update content for next operations + self.content = '\n'.join(lines) + return self.content + + def add_module_initialization(self, instance_name: str, class_name: str) -> str: + """ + Add module initialization in constructor. + + Args: + instance_name: Name for the instance (e.g., 'lockConfig') + class_name: Class name (e.g., 'LockConfiguration') + + Returns: + Updated content with initialization added + """ + lines = self.content.split('\n') + + # Find constructor and its opening brace + constructor_idx = None + for i, line in enumerate(lines): + if 'constructor(' in line: + constructor_idx = i + break + + if constructor_idx is None: + print("⚠️ Constructor not found") + return self.content + + # Find the end of super() call or end of constructor body setup + init_idx = constructor_idx + 1 + for i in range(constructor_idx, min(constructor_idx + 50, len(lines))): + line = lines[i] + # Look for lines that initialize properties (this.xxx = ...) + # We want to add after all the initialization lines + if line.strip() and not line.strip().startswith('//') and '=' in line: + init_idx = i + 1 + # Stop at closing brace of constructor + elif line.strip() == '}': + break + + # Add initialization before the closing brace + # Go back to find the right spot (before closing brace) + for i in range(init_idx, min(init_idx + 10, len(lines))): + if lines[i].strip() == '}': + init_idx = i + break + + # Create the initialization line with proper indentation + init_stmt = f" \n // Initialize {class_name} module" + init_stmt += f"\n this.{instance_name} = new {class_name}(this);" + lines.insert(init_idx, init_stmt) + + # Update content for next operations + self.content = '\n'.join(lines) + return self.content + + def replace_method_calls(self, method_names: List[str], module_instance: str) -> str: + """ + Replace method calls in the main file. + + Args: + method_names: Methods that were extracted + module_instance: Name of the module instance (e.g., 'lockConfig') + + Returns: + Updated content with method calls replaced + """ + 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) + + # Update content for next operations + self.content = updated + return updated + + +class ModuleGenerator: + """Generate JavaScript module files.""" + + def __init__(self, import_statements: Optional[str] = None): + """Initialize with optional import statements.""" + self.import_statements = import_statements or "" + + def generate_module( + self, + methods: Dict[str, str], + class_name: str, + export_as_class: bool = True, + extends: Optional[str] = None, + additional_imports: Optional[List[str]] = None, + use_parent_instance: bool = True + ) -> str: + """ + Generate a complete JavaScript module. + + Args: + methods: Dict of method_name -> method_code + class_name: Name of the exported class/object + export_as_class: If True, export as class; if False, as object + extends: Class to extend (e.g., "MinigameScene") + additional_imports: List of import statements + use_parent_instance: If True, generate with parent instance pattern + + Returns: + Complete module code as string + """ + # Build imports + imports = [] + if additional_imports: + imports.extend(additional_imports) + + imports_section = '\n'.join(imports) + '\n' if imports else '' + + # Build class or object + if export_as_class: + code = self._generate_class(methods, class_name, extends, imports_section, use_parent_instance) + else: + code = self._generate_object(methods, class_name, imports_section, use_parent_instance) + + return code + + @staticmethod + def _generate_class( + methods: Dict[str, str], + class_name: str, + extends: Optional[str], + imports_section: str, + use_parent_instance: bool = True + ) -> str: + """Generate a class module.""" + extends_str = f" extends {extends}" if extends else "" + + # Join all methods with proper spacing + methods_code = '\n\n '.join(methods.values()) + + # Add constructor if using parent instance pattern + if use_parent_instance: + constructor = """constructor(parent) { + this.parent = parent; + }""" + methods_code = constructor + '\n\n ' + methods_code + + code = f"""{imports_section} +/** + * {class_name} + * + * Extracted from lockpicking-game-phaser.js + * Instantiate with: new {class_name}(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 {class_name}{extends_str} {{ + + {methods_code} + +}} +""" + return code + + @staticmethod + def _generate_object( + methods: Dict[str, str], + object_name: str, + imports_section: str, + use_parent_instance: bool = True + ) -> str: + """Generate an object/namespace module.""" + # Convert methods to object methods + methods_code = '\n\n '.join(methods.values()) + + # Add init function if using parent instance pattern + if use_parent_instance: + init_func = """init(parent) { + return { + parent: parent + }; + }""" + methods_code = init_func + '\n\n ' + methods_code + + code = f"""{imports_section} +/** + * {object_name} + * + * Extracted from lockpicking-game-phaser.js + * Usage: {object_name}.methodName(parent, ...args) + * + * All 'this' references replaced with 'parent' to access parent instance state: + * - parent.pins (array of pin objects) + * - parent.scene (Phaser scene) + * - parent.lockId (lock identifier) + * - parent.lockState (lock state object) + * etc. + */ +export const {object_name} = {{ + + {methods_code} + +}}; +""" + return code + + +def main(): + """Main entry point.""" + parser = argparse.ArgumentParser( + description='Extract methods from lockpicking-game-phaser.js', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # Extract lock graphics methods + python3 extract_lockpicking_methods.py \\ + --methods "createLockBackground,createTensionWrench,createHookPick" \\ + --output-file "lock-graphics.js" \\ + --class-name "LockGraphics" \\ + --extends "LockpickingComponent" + + # Extract lock configuration methods as object + python3 extract_lockpicking_methods.py \\ + --methods "saveLockConfiguration,loadLockConfiguration,clearLockConfiguration" \\ + --output-file "lock-configuration.js" \\ + --object-mode + """ + ) + + parser.add_argument( + '--input-file', + default='js/minigames/lockpicking/lockpicking-game-phaser.js', + help='Path to input JavaScript file (default: %(default)s)' + ) + + parser.add_argument( + '--methods', + required=True, + help='Comma-separated list of method names to extract' + ) + + parser.add_argument( + '--output-file', + required=True, + help='Path to output JavaScript file' + ) + + parser.add_argument( + '--class-name', + help='Name for exported class (default: auto-generated from filename)' + ) + + parser.add_argument( + '--extends', + help='Parent class to extend' + ) + + parser.add_argument( + '--object-mode', + action='store_true', + help='Export as object instead of class' + ) + + parser.add_argument( + '--show-dependencies', + action='store_true', + help='Show method dependencies before extraction' + ) + + parser.add_argument( + '--imports', + help='Comma-separated list of import statements to add' + ) + + parser.add_argument( + '--replace-this', + action='store_true', + help='Replace "this" with "parent" in extracted methods for state sharing' + ) + + parser.add_argument( + '--update-main-file', + help='Path to main file to update with imports and method calls' + ) + + parser.add_argument( + '--module-instance-name', + help='Name for module instance in main file (e.g., "lockConfig")' + ) + + parser.add_argument( + '--auto-integrate', + action='store_true', + help='Automatically remove methods from main file and add imports (requires --update-main-file)' + ) + + args = parser.parse_args() + + # Parse method names + method_names = [m.strip() for m in args.methods.split(',')] + + # Parse imports if provided + additional_imports = [] + if args.imports: + additional_imports = [i.strip() for i in args.imports.split(',')] + + # Generate class name from output file if not provided + class_name = args.class_name + if not class_name: + # Convert filename to PascalCase class name + filename = Path(args.output_file).stem + parts = filename.split('-') + class_name = ''.join(word.capitalize() for word in parts) + + try: + # Extract methods + print(f"📂 Reading: {args.input_file}") + extractor = MethodExtractor(args.input_file) + + print(f"\n📋 Extracting {len(method_names)} methods...") + methods = extractor.extract_methods(method_names, replace_this=args.replace_this) + + if not methods: + print("❌ No methods extracted!", file=sys.stderr) + sys.exit(1) + + # Show dependencies if requested + if args.show_dependencies: + deps = extractor.find_dependencies(methods) + if deps: + print(f"\n⚠️ Dependencies (methods called but not extracted):") + for dep in sorted(deps): + print(f" - {dep}") + else: + print(f"\n✓ No external dependencies found") + + # Generate module + print(f"\n🔨 Generating module: {class_name}") + generator = ModuleGenerator() + + module_code = generator.generate_module( + methods=methods, + class_name=class_name, + export_as_class=not args.object_mode, + extends=args.extends, + additional_imports=additional_imports, + use_parent_instance=args.replace_this + ) + + # Write output + output_path = Path(args.output_file) + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(module_code, encoding='utf-8') + + print(f"\n✅ Success! Created: {args.output_file}") + print(f" Lines of code: {len(module_code.split(chr(10)))}") + + # Update main file if requested + if args.update_main_file: + print(f"\n📝 Updating main file: {args.update_main_file}") + + main_updater = MainFileUpdater(args.update_main_file) + module_instance_name = args.module_instance_name or class_name[0].lower() + class_name[1:] # camelCase + + if args.auto_integrate: + print(f"\n 🔧 Auto-integrating...") + + # 1. Add import statement + import_path = Path(args.output_file).name + main_updater.add_import(class_name, f'./{import_path}') + print(f" ✓ Added import statement") + + # 2. Add module initialization in constructor + main_updater.add_module_initialization(module_instance_name, class_name) + print(f" ✓ Added module initialization in constructor") + + # 3. Remove old methods from main file + try: + main_updater.remove_methods(method_names) + print(f" ✓ Removed {len(method_names)} methods from main file") + except Exception as e: + print(f" ⚠️ Error removing methods: {e}") + + # 4. Replace method calls to use module instance + try: + main_updater.replace_method_calls(method_names, module_instance_name) + print(f" ✓ Updated method calls to use this.{module_instance_name}") + except Exception as e: + print(f" ⚠️ Error updating calls: {e}") + + # Write updated main file + try: + main_path = Path(args.update_main_file) + main_path.write_text(main_updater.content, encoding='utf-8') + print(f"\n✅ Updated: {args.update_main_file}") + print(f" Instance name: this.{module_instance_name}") + print(f" Usage: new {class_name}(this) in constructor") + except Exception as e: + print(f"❌ Error writing main file: {e}", file=sys.stderr) + + except FileNotFoundError as e: + print(f"❌ File not found: {e}", file=sys.stderr) + sys.exit(1) + except Exception as e: + print(f"❌ Error: {e}", file=sys.stderr) + import traceback + traceback.print_exc() + sys.exit(1) + + +if __name__ == '__main__': + main() diff --git a/scripts/list_js_functions.py b/scripts/list_js_functions.py new file mode 100644 index 0000000..ede6599 --- /dev/null +++ b/scripts/list_js_functions.py @@ -0,0 +1,224 @@ +#!/usr/bin/env python3 +""" +List all JavaScript functions in a file. + +Usage: + python3 list_js_functions.py [--file path/to/file.js] [--format table|list|csv] + +Example: + python3 list_js_functions.py --file js/minigames/lockpicking/lockpicking-game-phaser.js --format table +""" + +import argparse +import re +from pathlib import Path +from typing import List, Tuple + + +class JSFunctionLister: + """Extract and list all functions from a JavaScript file.""" + + def __init__(self, input_file: str): + """Initialize with input file path.""" + self.input_file = Path(input_file) + self.content = self.input_file.read_text(encoding='utf-8') + self.lines = self.content.split('\n') + + def find_all_functions(self) -> List[Tuple[str, int, int]]: + """ + Find all function definitions in the file. + + Returns: + List of tuples: (function_name, start_line, end_line) + """ + functions = [] + + # Pattern for method definitions (class methods) + method_pattern = r'^\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(' + + i = 0 + while i < len(self.lines): + line = self.lines[i] + + # Match method definition + match = re.match(method_pattern, line) + if match: + method_name = match.group(1) + start_line = i + + # Find the end of the method by counting braces + brace_count = 0 + found_opening = False + end_line = None + + for j in range(i, len(self.lines)): + current_line = self.lines[j] + + for char in current_line: + if char == '{': + brace_count += 1 + found_opening = True + elif char == '}': + if found_opening: + brace_count -= 1 + if brace_count == 0: + end_line = j + break + + if end_line is not None: + break + + if end_line is not None: + functions.append((method_name, start_line + 1, end_line + 1)) # +1 for 1-based indexing + i = end_line + 1 + else: + i += 1 + else: + i += 1 + + return functions + + def format_table(self, functions: List[Tuple[str, int, int]]) -> str: + """Format functions as a table.""" + if not functions: + return "No functions found" + + # Calculate column widths + max_name_len = max(len(name) for name, _, _ in functions) + max_name_len = max(max_name_len, len("Function Name")) + + # Header + lines = [] + lines.append("┌─" + "─" * max_name_len + "─┬──────────┬──────────┐") + lines.append(f"│ {'Function Name':<{max_name_len}} │ Start │ End │") + lines.append("├─" + "─" * max_name_len + "─┼──────────┼──────────┤") + + # Rows + for name, start, end in functions: + lines.append(f"│ {name:<{max_name_len}} │ {start:>8} │ {end:>8} │") + + lines.append("└─" + "─" * max_name_len + "─┴──────────┴──────────┘") + + return "\n".join(lines) + + def format_list(self, functions: List[Tuple[str, int, int]]) -> str: + """Format functions as a simple list.""" + if not functions: + return "No functions found" + + lines = [] + for i, (name, start, end) in enumerate(functions, 1): + lines.append(f"{i:2}. {name:40} (lines {start:5}-{end:5})") + + return "\n".join(lines) + + def format_csv(self, functions: List[Tuple[str, int, int]]) -> str: + """Format functions as CSV.""" + if not functions: + return "No functions found" + + lines = ["Function Name,Start Line,End Line"] + for name, start, end in functions: + lines.append(f'"{name}",{start},{end}') + + return "\n".join(lines) + + def format_copy_paste(self, functions: List[Tuple[str, int, int]]) -> str: + """Format as comma-separated list for copy-pasting to command line.""" + if not functions: + return "No functions found" + + names = [name for name, _, _ in functions] + return ",".join(names) + + +def main(): + """Main entry point.""" + parser = argparse.ArgumentParser( + description='List all JavaScript functions in a file', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # List functions from lockpicking file + python3 list_js_functions.py --file js/minigames/lockpicking/lockpicking-game-phaser.js + + # Show as table + python3 list_js_functions.py --file lockpicking-game-phaser.js --format table + + # Show as CSV + python3 list_js_functions.py --file lockpicking-game-phaser.js --format csv + + # Get copy-paste friendly list + python3 list_js_functions.py --file lockpicking-game-phaser.js --format copy-paste + """ + ) + + parser.add_argument( + '--file', + default='js/minigames/lockpicking/lockpicking-game-phaser.js', + help='Path to input JavaScript file (default: %(default)s)' + ) + + parser.add_argument( + '--format', + choices=['table', 'list', 'csv', 'copy-paste'], + default='table', + help='Output format (default: %(default)s)' + ) + + parser.add_argument( + '--grep', + help='Filter functions by name (case-insensitive)' + ) + + parser.add_argument( + '--count', + action='store_true', + help='Show only count of functions' + ) + + args = parser.parse_args() + + try: + # List functions + print(f"📂 Reading: {args.file}") + lister = JSFunctionLister(args.file) + + print(f"\n🔍 Extracting functions...") + functions = lister.find_all_functions() + + # Filter if grep specified + if args.grep: + grep_lower = args.grep.lower() + functions = [(n, s, e) for n, s, e in functions if grep_lower in n.lower()] + print(f"📋 Filtered to functions matching '{args.grep}':") + + # Show count + print(f"✅ Found {len(functions)} functions\n") + + if args.count: + print(f"Total: {len(functions)}") + else: + # Format and display + if args.format == 'table': + print(lister.format_table(functions)) + elif args.format == 'list': + print(lister.format_list(functions)) + elif args.format == 'csv': + print(lister.format_csv(functions)) + elif args.format == 'copy-paste': + print("\n📋 Copy-paste this list of function names:\n") + print(lister.format_copy_paste(functions)) + + except FileNotFoundError as e: + print(f"❌ File not found: {e}") + exit(1) + except Exception as e: + print(f"❌ Error: {e}") + import traceback + traceback.print_exc() + exit(1) + + +if __name__ == '__main__': + main()