Add KeySelection module to lockpicking minigame: Extract key generation logic and integrate into LockpickingMinigamePhaser for improved modularity.

This commit is contained in:
Z. Cliffe Schreuders
2025-10-27 15:58:56 +00:00
parent a6d04d3e28
commit cf776cb693
3 changed files with 194 additions and 123 deletions

View File

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

View File

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

View File

@@ -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}
}};
"""