From f8068ecaddd872a49b4c92e494133d20007eec3a Mon Sep 17 00:00:00 2001 From: "Z. Cliffe Schreuders" Date: Tue, 28 Oct 2025 20:09:07 +0000 Subject: [PATCH] feat: Implement keyPins normalization and conversion logic for accurate cut generation in minigames --- js/core/game.js | 40 ++++++++++++++++++- js/core/rooms.js | 14 ++++++- js/systems/minigame-starters.js | 69 ++++++++++++++++++++------------- 3 files changed, 93 insertions(+), 30 deletions(-) diff --git a/js/core/game.js b/js/core/game.js index fe957d7..c3305ea 100644 --- a/js/core/game.js +++ b/js/core/game.js @@ -672,13 +672,51 @@ function findObjectsAtPosition(worldX, worldY) { return objectsAtPosition; } -// Normalize keyPins from 0-100 scale to 25-65 scale in entire scenario +/** + * Normalize keyPins from 0-100 scale to 25-65 scale in entire scenario + * + * KEYPIN TERMINOLOGY AND RELATIONSHIPS: + * ===================================== + * + * Lock Pin Anatomy: + * - Each lock chamber contains TWO pins stacked vertically: + * 1. DRIVER PIN (top, spring-loaded, blocks rotation) + * 2. KEY PIN (bottom, rests on key when inserted) + * + * - The SHEAR LINE is the boundary between the plug and housing + * - Lock opens when ALL key pins' tops align exactly at the shear line + * + * KeyPins Property: + * - "keyPins" represents the LENGTH of the key pins (bottom pins) in the lock + * - In scenarios: Defined on a 0-100 authoring scale for ease of design + * - In game: Converted to 25-65 pixel range for actual rendering + * - Example: [100, 0, 100, 0] → [65, 25, 65, 25] pixels + * + * KeyPins on LOCKS vs KEYS: + * - On a LOCK/DOOR: The actual pin lengths in that lock + * - On a KEY: The lock configuration this key is designed to open + * (i.e., a key with keyPins: [65, 25, 65, 25] opens locks with those pin lengths) + * + * Relationship: KeyPins → Cuts: + * - CUTS are the depths of notches carved into the key blade + * - Formula: cutDepth = keyPinLength - gapFromKeyBladeTopToShearLine + * - Where gap = 20 pixels (175 - 155) + * - Example: keyPin 65 → cut 45, keyPin 25 → cut 5 + * - When key is inserted, pin rests on cut surface, lifting to shear line + * + * Why Normalization is Critical: + * - Scenarios use 0-100 for easy authoring (percentages) + * - Game needs 25-65 pixel range for proper physics/rendering + * - Normalization must happen EXACTLY ONCE to avoid double-conversion + * - This function runs during scenario load before any sprites are created + */ function normalizeScenarioKeyPins(scenario) { // Helper function to convert a single keyPins value function convertKeyPin(value) { // Convert from 0-100 scale to 25-65 scale // Formula: 25 + (value / 100) * 40 + // This gives us a 40-pixel range centered in the lock chamber return Math.round(25 + (value / 100) * 40); } diff --git a/js/core/rooms.js b/js/core/rooms.js index 26ffbbc..b2e9168 100644 --- a/js/core/rooms.js +++ b/js/core/rooms.js @@ -299,10 +299,20 @@ function applyTiledProperties(sprite, tiledItem) { /** * Helper: Apply game logic properties from scenario to sprite * Stores scenario data and makes sprite interactive + * + * IMPORTANT - KeyPins Normalization: + * =================================== + * KeyPins are already normalized by normalizeScenarioKeyPins() in game.js during scenario load. + * This happens BEFORE any sprites are created, converting 0-100 scale to 25-65 pixel range. + * + * Do NOT normalize keyPins here - it would cause double normalization: + * - Original: [100, 0, 100, 0] + * - After 1st normalization (game.js): [65, 25, 65, 25] ✓ + * - After 2nd normalization (here): [51, 35, 51, 35] ✗ WRONG! + * + * The sprite simply receives the already-normalized values from scenarioObj. */ function applyScenarioProperties(sprite, scenarioObj, roomId, index) { - // NOTE: keyPins are already normalized by normalizeScenarioKeyPins() in game.js - // Do NOT normalize here again to avoid double normalization sprite.scenarioData = scenarioObj; sprite.interactable = true; // Mark scenario items as interactable diff --git a/js/systems/minigame-starters.js b/js/systems/minigame-starters.js index f3c2f96..455db78 100644 --- a/js/systems/minigame-starters.js +++ b/js/systems/minigame-starters.js @@ -123,20 +123,20 @@ export function startLockpickingMinigame(lockable, scene, difficulty = 'medium', individualKeys.forEach(key => { let cuts = key.scenarioData.cuts; - // If no cuts but keyPins exists, keyPins represents the LOCK configuration this key matches - // Generate the cuts that would work with that lock configuration + // KEYPIN TO CUT CONVERSION (for available keys): + // If no cuts but keyPins exists, generate cuts from the lock configuration. + // keyPins on a key = the lock configuration this key opens (already normalized to 25-65) if (!cuts && (key.scenarioData.keyPins || key.keyPins)) { const lockKeyPins = key.scenarioData.keyPins || key.keyPins; - console.log(`Generating cuts from lock keyPins for key "${key.scenarioData.name}":`, lockKeyPins); + console.log(`Generating cuts from lock keyPins for available key "${key.scenarioData.name}":`, lockKeyPins); - // Generate cuts that match this lock configuration - // Use the generateKeyCutsForLock function with the key's keyPins as the lock config + // Convert lock pin lengths to key cut depths cuts = lockKeyPins.map(keyPinLength => { - const keyBladeTop_world = 175; - const shearLine_world = 155; - const gapFromKeyBladeTopToShearLine = keyBladeTop_world - shearLine_world; - const cutDepth_needed = keyPinLength - gapFromKeyBladeTopToShearLine; - const clampedCutDepth = Math.max(0, Math.min(110, cutDepth_needed)); + const keyBladeTop_world = 175; // Key blade top when inserted + const shearLine_world = 155; // Shear line position + const gapFromKeyBladeTopToShearLine = 20; // Distance between them + const cutDepth_needed = keyPinLength - gapFromKeyBladeTopToShearLine; // Cut depth formula + const clampedCutDepth = Math.max(0, Math.min(110, cutDepth_needed)); // Clamp to blade height return Math.round(clampedCutDepth); }); @@ -159,17 +159,18 @@ export function startLockpickingMinigame(lockable, scene, difficulty = 'medium', keyRingItem.scenarioData.allKeys.forEach(keyData => { let cuts = keyData.cuts; - // If no cuts but keyPins exists, generate cuts from lock configuration + // KEYPIN TO CUT CONVERSION (for key ring keys): + // Same conversion as above - keyPins → cuts using the formula if (!cuts && keyData.keyPins) { const lockKeyPins = keyData.keyPins; console.log(`Generating cuts from lock keyPins for key ring key "${keyData.name}":`, lockKeyPins); cuts = lockKeyPins.map(keyPinLength => { - const keyBladeTop_world = 175; - const shearLine_world = 155; - const gapFromKeyBladeTopToShearLine = keyBladeTop_world - shearLine_world; - const cutDepth_needed = keyPinLength - gapFromKeyBladeTopToShearLine; - const clampedCutDepth = Math.max(0, Math.min(110, cutDepth_needed)); + const keyBladeTop_world = 175; // Key blade top when inserted + const shearLine_world = 155; // Shear line position + const gapFromKeyBladeTopToShearLine = 20; // Distance between them + const cutDepth_needed = keyPinLength - gapFromKeyBladeTopToShearLine; // Cut depth formula + const clampedCutDepth = Math.max(0, Math.min(110, cutDepth_needed)); // Clamp to blade height return Math.round(clampedCutDepth); }); @@ -262,23 +263,36 @@ export function startKeySelectionMinigame(lockable, type, playerKeys, requiredKe // Generate cuts data if not present let cuts = key.scenarioData.cuts; - // If no cuts but keyPins exists, keyPins represents the LOCK configuration this key matches - // Generate the cuts that would work with that lock configuration + // KEYPIN TO CUT CONVERSION: + // ========================== + // If no cuts but keyPins exists, we need to generate cuts from the lock configuration. + // + // Remember: keyPins on a KEY represent the LOCK configuration this key is designed to open. + // The keyPins values have already been normalized from 0-100 to 25-65 pixel range. + // + // We convert keyPins (lock pin lengths) to cuts (key blade notch depths) using the formula: + // cutDepth = keyPinLength - gapFromKeyBladeTopToShearLine + // + // This ensures when the key is inserted, each pin rests on its corresponding cut, + // lifting the pin so its top aligns exactly with the shear line. if (!cuts && (key.scenarioData.keyPins || key.keyPins)) { const lockKeyPins = key.scenarioData.keyPins || key.keyPins; console.log(`Generating cuts from lock keyPins for key "${key.scenarioData.name}":`, lockKeyPins); // Generate cuts that match this lock configuration - // keyPins on a key represent the lock's pin configuration, not the key's own properties cuts = lockKeyPins.map(keyPinLength => { - const keyBladeTop_world = 175; // Key blade top position - const shearLine_world = 155; // Shear line position - const gapFromKeyBladeTopToShearLine = keyBladeTop_world - shearLine_world; // 20 + // Key blade geometry (in world coordinates): + const keyBladeTop_world = 175; // Top surface of key blade when inserted + const shearLine_world = 155; // Where lock plug meets housing + const gapFromKeyBladeTopToShearLine = keyBladeTop_world - shearLine_world; // 20 pixels - // Calculate the required cut depth + // Formula: cutDepth = keyPinLength - gap + // Example: If keyPin is 65 pixels long, cut must be 45 pixels deep (65 - 20) + // This ensures the pin's bottom rests on the cut, lifting its top to the shear line const cutDepth_needed = keyPinLength - gapFromKeyBladeTopToShearLine; // Clamp to valid range (0 to 110, which is key blade height) + // This prevents negative cuts or cuts deeper than the blade itself const clampedCutDepth = Math.max(0, Math.min(110, cutDepth_needed)); return Math.round(clampedCutDepth); }); @@ -427,18 +441,19 @@ export function startKeySelectionMinigame(lockable, type, playerKeys, requiredKe // Wait for the minigame to be fully initialized and lock configuration to be saved setTimeout(() => { if (window.MinigameFramework.currentMinigame && window.MinigameFramework.currentMinigame.startWithKeySelection) { - // Regenerate keys with the actual lock configuration now that it's been created + // DEFERRED KEY PREPARATION: + // Regenerate keys after minigame initialization to ensure lock config is saved const updatedInventoryKeys = playerKeys.map(key => { let cuts = key.scenarioData.cuts; - // If no cuts but keyPins exists, keyPins represents the LOCK configuration this key matches - // Generate the cuts that would work with that lock configuration + // KEYPIN TO CUT CONVERSION (deferred update): + // Same as above - convert normalized keyPins (25-65) to cuts for visualization + // This ensures each key displays its actual unique cut pattern if (!cuts && (key.scenarioData.keyPins || key.keyPins)) { const lockKeyPins = key.scenarioData.keyPins || key.keyPins; console.log(`Generating cuts from lock keyPins for key "${key.scenarioData.name}":`, lockKeyPins); // Generate cuts that match this lock configuration - // keyPins on a key represent the lock's pin configuration, not the key's own properties cuts = lockKeyPins.map(keyPinLength => { const keyBladeTop_world = 175; // Key blade top position const shearLine_world = 155; // Shear line position