Refactor lockpicking minigame: Extract lock configuration logic into LockConfiguration class

- Created a new LockConfiguration class in lock-configuration.js to encapsulate lock configuration methods.
- Removed lock configuration methods from lockpicking-game-phaser.js and replaced them with an instance of LockConfiguration.
- Added methods for saving, loading, clearing, and resetting lock configurations in the new class.
- Updated lockpicking-game-phaser.js to utilize the new LockConfiguration class for managing lock states.
- Introduced scripts for extracting methods and listing JavaScript functions for better code organization and maintainability.
This commit is contained in:
Z. Cliffe Schreuders
2025-10-27 14:52:43 +00:00
parent 7acb65babc
commit cb9f7d1cba
5 changed files with 1450 additions and 112 deletions

350
FUNCTION_INVENTORY.md Normal file
View File

@@ -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 <file.js> --format <table|list|csv|copy-paste>
```

View File

@@ -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');
}
}

View File

@@ -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) {

View File

@@ -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()

View File

@@ -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()