feat: Implement keyPins normalization and conversion logic for accurate cut generation in minigames

This commit is contained in:
Z. Cliffe Schreuders
2025-10-28 20:09:07 +00:00
parent cf3268e6b5
commit f8068ecadd
3 changed files with 93 additions and 30 deletions

View File

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

View File

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

View File

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