mirror of
https://github.com/cliffe/BreakEscape.git
synced 2026-02-21 11:18:08 +00:00
Implement comprehensive multi-protocol RFID system with deterministic
card_id-based generation, MIFARE key attacks, and protocol-specific UI.
## New Protocol System (4 protocols):
- EM4100 (low security) - Instant clone, already implemented
- MIFARE_Classic_Weak_Defaults (low) - Dictionary attack succeeds (95%)
- MIFARE_Classic_Custom_Keys (medium) - Requires Darkside attack (30s)
- MIFARE_DESFire (high) - UID only, forces physical theft
## Key Features:
### 1. Protocol Foundation
- Created rfid-protocols.js with protocol definitions
- Added protocol detection, capabilities, security levels
- Defined attack durations and common MIFARE keys
### 2. Deterministic Card Generation
- Updated rfid-data.js with card_id-based generation
- Same card_id always produces same hex/UID (deterministic)
- Simplified scenario format - no manual hex/UID needed
- getCardDisplayData() supports all protocols
### 3. MIFARE Attack System
- Created rfid-attacks.js with MIFAREAttackManager
- Dictionary Attack: Instant, 95% success on weak defaults
- Darkside Attack: 30 sec (10s on weak), cracks all keys
- Nested Attack: 10 sec, uses known key to crack rest
- Protocol-aware attack behavior
### 4. UI Enhancements
- Updated rfid-ui.js with protocol-specific displays
- showProtocolInfo() with color-coded security badges
- showAttackProgress() and updateAttackProgress()
- Protocol headers with icons and frequency info
- Updated showCardDataScreen() and showEmulationScreen()
### 5. Unlock System Integration
- Updated unlock-system.js for card_id matching
- Support multiple valid cards per door (array)
- Added acceptsUIDOnly flag for DESFire UID emulation
- Backward compatible with legacy key_id format
### 6. Minigame Integration
- Updated rfid-minigame.js with attack methods
- startKeyAttack() triggers dictionary/darkside/nested
- handleCardTap() and handleEmulate() use card_id arrays
- UID-only emulation validation for DESFire
- Attack manager cleanup on minigame exit
### 7. Styling
- Added CSS for protocol headers and security badges
- Color-coded security levels (red=low, teal=medium, green=high)
- Attack progress styling with smooth transitions
- Dimmed menu items for unlikely attack options
## Scenario Format Changes:
Before (manual technical data):
```json
{
"type": "keycard",
"rfid_hex": "01AB34CD56",
"rfid_facility": 1,
"key_id": "employee_badge"
}
```
After (simplified with card_id):
```json
{
"type": "keycard",
"card_id": "employee_badge",
"rfid_protocol": "MIFARE_Classic_Weak_Defaults",
"name": "Employee Badge"
}
```
Technical data (hex/UID) generated automatically from card_id.
## Door Configuration:
Multiple valid cards per door:
```json
{
"lockType": "rfid",
"requires": ["employee_badge", "contractor_badge", "master_card"],
"acceptsUIDOnly": false
}
```
## Files Modified:
- js/minigames/rfid/rfid-protocols.js (NEW)
- js/minigames/rfid/rfid-attacks.js (NEW)
- js/minigames/rfid/rfid-data.js
- js/minigames/rfid/rfid-ui.js
- js/minigames/rfid/rfid-minigame.js
- js/systems/unlock-system.js
- css/rfid-minigame.css
- planning_notes/rfid_keycard/protocols_and_interactions/03_UPDATES_SUMMARY.md (NEW)
## Next Steps:
- Phase 5: Ink integration (syncCardProtocolsToInk)
- Test with scenarios for each protocol
- Add Ink variable documentation
Estimated implementation time: ~12 hours (Phases 1-4 complete)
549 lines
24 KiB
JavaScript
549 lines
24 KiB
JavaScript
/**
|
|
* UNLOCK SYSTEM
|
|
* =============
|
|
*
|
|
* Handles all unlock logic for doors and items.
|
|
* Supports multiple lock types: key, pin, password, biometric, bluetooth.
|
|
* This system coordinates between various subsystems to perform unlocking.
|
|
*/
|
|
|
|
import { DOOR_ALIGN_OVERLAP } from '../utils/constants.js';
|
|
import { rooms } from '../core/rooms.js';
|
|
import { unlockDoor } from './doors.js';
|
|
import { startLockpickingMinigame, startKeySelectionMinigame, startPinMinigame, startPasswordMinigame } from './minigame-starters.js';
|
|
import { playUISound } from './ui-sounds.js?v=1';
|
|
|
|
// Helper function to check if two rectangles overlap
|
|
function boundsOverlap(rect1, rect2) {
|
|
return rect1.x < rect2.x + rect2.width &&
|
|
rect1.x + rect1.width > rect2.x &&
|
|
rect1.y < rect2.y + rect2.height &&
|
|
rect1.y + rect1.height > rect2.y;
|
|
}
|
|
|
|
export function handleUnlock(lockable, type) {
|
|
console.log('UNLOCK ATTEMPT');
|
|
playUISound('lock');
|
|
|
|
// Check if locks are disabled for testing
|
|
if (window.DISABLE_LOCKS) {
|
|
console.log('LOCKS DISABLED FOR TESTING - Unlocking directly');
|
|
unlockTarget(lockable, type, lockable.layer);
|
|
return;
|
|
}
|
|
|
|
// Get lock requirements based on type
|
|
const lockRequirements = type === 'door'
|
|
? getLockRequirementsForDoor(lockable)
|
|
: getLockRequirementsForItem(lockable);
|
|
|
|
if (!lockRequirements) {
|
|
console.log('NO LOCK REQUIREMENTS FOUND');
|
|
return;
|
|
}
|
|
|
|
// Check if object is locked based on lock requirements
|
|
const isLocked = lockRequirements.requires;
|
|
|
|
if (!isLocked) {
|
|
console.log('OBJECT NOT LOCKED');
|
|
return;
|
|
}
|
|
|
|
// Emit unlock attempt event
|
|
if (window.eventDispatcher && type === 'door') {
|
|
const doorProps = lockable.doorProperties || {};
|
|
window.eventDispatcher.emit('door_unlock_attempt', {
|
|
roomId: doorProps.roomId,
|
|
connectedRoom: doorProps.connectedRoom,
|
|
direction: doorProps.direction,
|
|
lockType: lockRequirements.lockType
|
|
});
|
|
}
|
|
|
|
switch(lockRequirements.lockType) {
|
|
case 'key':
|
|
const requiredKey = lockRequirements.requires;
|
|
console.log('KEY REQUIRED', requiredKey);
|
|
|
|
// Get all keys from player's inventory (including key ring)
|
|
let playerKeys = [];
|
|
|
|
// Check for individual keys
|
|
const individualKeys = window.inventory.items.filter(item =>
|
|
item && item.scenarioData &&
|
|
item.scenarioData.type === 'key'
|
|
);
|
|
playerKeys = playerKeys.concat(individualKeys);
|
|
|
|
// Check for key ring
|
|
const keyRingItem = window.inventory.items.find(item =>
|
|
item && item.scenarioData &&
|
|
item.scenarioData.type === 'key_ring'
|
|
);
|
|
|
|
if (keyRingItem && keyRingItem.scenarioData.allKeys) {
|
|
// Convert key ring keys to the format expected by the minigame
|
|
const keyRingKeys = keyRingItem.scenarioData.allKeys.map(keyData => {
|
|
// Create a mock inventory item for each key in the ring
|
|
return {
|
|
scenarioData: keyData,
|
|
name: 'key',
|
|
objectId: `key_ring_${keyData.key_id || keyData.name}`
|
|
};
|
|
});
|
|
playerKeys = playerKeys.concat(keyRingKeys);
|
|
}
|
|
|
|
// Check for lockpick kit
|
|
const hasLockpick = window.inventory.items.some(item =>
|
|
item && item.scenarioData &&
|
|
item.scenarioData.type === 'lockpick'
|
|
);
|
|
|
|
if (playerKeys.length > 0) {
|
|
// Keys take priority - go straight to key selection
|
|
console.log('KEYS AVAILABLE - STARTING KEY SELECTION');
|
|
startKeySelectionMinigame(lockable, type, playerKeys, requiredKey, unlockTarget);
|
|
} else if (hasLockpick) {
|
|
// Only lockpick available - launch lockpicking minigame directly
|
|
console.log('LOCKPICK AVAILABLE - STARTING LOCKPICKING MINIGAME');
|
|
|
|
// CHECK: Should any NPC interrupt with person-chat instead?
|
|
const roomId = lockable.doorProperties?.roomId || window.currentRoomId;
|
|
if (window.npcManager && roomId) {
|
|
// Get player position for LOS check
|
|
const playerPos = window.player?.sprite?.getCenter ?
|
|
window.player.sprite.getCenter() :
|
|
{ x: window.player?.x || 0, y: window.player?.y || 0 };
|
|
|
|
const interruptingNPC = window.npcManager.shouldInterruptLockpickingWithPersonChat(roomId, playerPos);
|
|
if (interruptingNPC) {
|
|
console.log(`🚫 LOCKPICKING INTERRUPTED: Triggering person-chat with NPC "${interruptingNPC.id}"`);
|
|
|
|
// Trigger the lockpick event which will start person-chat
|
|
if (window.npcManager.eventDispatcher) {
|
|
window.npcManager.eventDispatcher.emit('lockpick_used_in_view', {
|
|
npcId: interruptingNPC.id,
|
|
roomId: roomId,
|
|
lockable: lockable,
|
|
timestamp: Date.now()
|
|
});
|
|
}
|
|
return; // Don't start lockpicking minigame
|
|
}
|
|
}
|
|
|
|
let difficulty = lockable.doorProperties?.difficulty || lockable.scenarioData?.difficulty || lockable.properties?.difficulty || lockRequirements.difficulty || 'medium';
|
|
// Check for both keyPins (camelCase) and key_pins (snake_case)
|
|
let keyPins = lockable.doorProperties?.keyPins || lockable.doorProperties?.key_pins ||
|
|
lockable.scenarioData?.keyPins || lockable.scenarioData?.key_pins ||
|
|
lockable.properties?.keyPins || lockable.properties?.key_pins ||
|
|
lockRequirements.keyPins || lockRequirements.key_pins;
|
|
|
|
console.log('🔓 Door/Item lock details:', {
|
|
hasDoorProperties: !!lockable.doorProperties,
|
|
doorKeyPins: lockable.doorProperties?.keyPins,
|
|
hasScenarioData: !!lockable.scenarioData,
|
|
scenarioKeyPins: lockable.scenarioData?.keyPins,
|
|
hasProperties: !!lockable.properties,
|
|
propertiesKeyPins: lockable.properties?.keyPins,
|
|
lockRequirementsKeyPins: lockRequirements.keyPins,
|
|
finalKeyPins: keyPins,
|
|
finalDifficulty: difficulty
|
|
});
|
|
|
|
startLockpickingMinigame(lockable, window.game, difficulty, (success) => {
|
|
if (success) {
|
|
setTimeout(() => {
|
|
unlockTarget(lockable, type, lockable.layer);
|
|
window.gameAlert(`Successfully picked the lock!`, 'success', 'Lock Picked', 4000);
|
|
}, 100);
|
|
} else {
|
|
console.log('LOCKPICK FAILED');
|
|
window.gameAlert('Failed to pick the lock. Try again.', 'error', 'Pick Failed', 3000);
|
|
}
|
|
}, keyPins); // Pass keyPins to minigame starter
|
|
} else {
|
|
console.log('NO KEYS OR LOCKPICK AVAILABLE');
|
|
window.gameAlert(`Requires key`, 'error', 'Locked', 4000);
|
|
}
|
|
break;
|
|
|
|
case 'pin':
|
|
console.log('PIN CODE REQUESTED');
|
|
startPinMinigame(lockable, type, lockRequirements.requires, (success) => {
|
|
if (success) {
|
|
unlockTarget(lockable, type, lockable.layer);
|
|
}
|
|
});
|
|
break;
|
|
|
|
case 'password':
|
|
console.log('PASSWORD REQUESTED');
|
|
|
|
// Get password options from the lockable object
|
|
const passwordOptions = {
|
|
passwordHint: lockable.passwordHint || lockable.scenarioData?.passwordHint || '',
|
|
showHint: lockable.showHint || lockable.scenarioData?.showHint || false,
|
|
showKeyboard: lockable.showKeyboard || lockable.scenarioData?.showKeyboard || false,
|
|
maxAttempts: lockable.maxAttempts || lockable.scenarioData?.maxAttempts || 3,
|
|
postitNote: lockable.postitNote || lockable.scenarioData?.postitNote || '',
|
|
showPostit: lockable.showPostit || lockable.scenarioData?.showPostit || false
|
|
};
|
|
|
|
startPasswordMinigame(lockable, type, lockRequirements.requires, (success) => {
|
|
if (success) {
|
|
unlockTarget(lockable, type, lockable.layer);
|
|
}
|
|
}, passwordOptions);
|
|
break;
|
|
|
|
case 'biometric':
|
|
const requiredFingerprint = lockRequirements.requires;
|
|
console.log('BIOMETRIC LOCK REQUIRES', requiredFingerprint);
|
|
|
|
// Check if we have fingerprints in the biometricSamples collection
|
|
const biometricSamples = window.gameState?.biometricSamples || [];
|
|
|
|
console.log('BIOMETRIC SAMPLES', JSON.stringify(biometricSamples));
|
|
|
|
// Get the required match threshold from the object or use default
|
|
const requiredThreshold = lockable.biometricMatchThreshold || 0.4;
|
|
console.log('BIOMETRIC THRESHOLD', requiredThreshold);
|
|
|
|
// Find the fingerprint sample for the required person
|
|
const fingerprintSample = biometricSamples.find(sample =>
|
|
sample.owner === requiredFingerprint
|
|
);
|
|
|
|
const hasFingerprint = fingerprintSample !== undefined;
|
|
console.log('FINGERPRINT CHECK', `Looking for '${requiredFingerprint}'. Found: ${hasFingerprint}`);
|
|
|
|
if (hasFingerprint) {
|
|
// Get the quality from the sample
|
|
let fingerprintQuality = fingerprintSample.quality;
|
|
|
|
// Normalize quality to 0-1 range if it's in percentage format
|
|
if (fingerprintQuality > 1) {
|
|
fingerprintQuality = fingerprintQuality / 100;
|
|
}
|
|
|
|
console.log('BIOMETRIC CHECK',
|
|
`Required: ${requiredFingerprint}, Quality: ${fingerprintQuality} (${Math.round(fingerprintQuality * 100)}%), Threshold: ${requiredThreshold} (${Math.round(requiredThreshold * 100)}%)`);
|
|
|
|
// Check if the fingerprint quality meets the threshold
|
|
if (fingerprintQuality >= requiredThreshold) {
|
|
console.log('BIOMETRIC UNLOCK SUCCESS');
|
|
unlockTarget(lockable, type, lockable.layer);
|
|
window.gameAlert(`You successfully unlocked the ${type} with ${requiredFingerprint}'s fingerprint.`,
|
|
'success', 'Biometric Unlock Successful', 5000);
|
|
} else {
|
|
console.log('BIOMETRIC QUALITY TOO LOW',
|
|
`Quality: ${fingerprintQuality} (${Math.round(fingerprintQuality * 100)}%) < Threshold: ${requiredThreshold} (${Math.round(requiredThreshold * 100)}%)`);
|
|
window.gameAlert(`The fingerprint quality (${Math.round(fingerprintQuality * 100)}%) is too low for this lock.
|
|
It requires at least ${Math.round(requiredThreshold * 100)}% quality.`,
|
|
'error', 'Biometric Authentication Failed', 5000);
|
|
}
|
|
} else {
|
|
console.log('MISSING REQUIRED FINGERPRINT',
|
|
`Required: '${requiredFingerprint}', Available: ${biometricSamples.map(s => s.owner).join(", ") || "none"}`);
|
|
window.gameAlert(`This ${type} requires ${requiredFingerprint}'s fingerprint, which you haven't collected yet.`,
|
|
'error', 'Biometric Authentication Failed', 5000);
|
|
}
|
|
break;
|
|
|
|
case 'bluetooth':
|
|
console.log('BLUETOOTH UNLOCK ATTEMPT');
|
|
const requiredDevice = lockRequirements.requires; // MAC address or device name
|
|
console.log('BLUETOOTH DEVICE REQUIRED', requiredDevice);
|
|
|
|
// Check if we have a bluetooth scanner in inventory
|
|
const hasScanner = window.inventory.items.some(item =>
|
|
item && item.scenarioData &&
|
|
item.scenarioData.type === 'bluetooth_scanner'
|
|
);
|
|
|
|
if (!hasScanner) {
|
|
console.log('NO BLUETOOTH SCANNER');
|
|
window.gameAlert(`You need a Bluetooth scanner to access this ${type}.`, 'error', 'Scanner Required', 4000);
|
|
break;
|
|
}
|
|
|
|
// Check if we have the required device in our bluetooth scan results
|
|
const bluetoothData = window.gameState?.bluetoothDevices || [];
|
|
const requiredDeviceData = bluetoothData.find(device =>
|
|
device.mac === requiredDevice || device.name === requiredDevice
|
|
);
|
|
|
|
console.log('BLUETOOTH SCAN DATA', JSON.stringify(bluetoothData));
|
|
console.log('REQUIRED DEVICE CHECK', { required: requiredDevice, found: !!requiredDeviceData });
|
|
|
|
if (requiredDeviceData) {
|
|
// Check signal strength - need to be close enough
|
|
const minSignalStrength = lockable.minSignalStrength || -70; // dBm
|
|
|
|
if (requiredDeviceData.signalStrength >= minSignalStrength) {
|
|
console.log('BLUETOOTH UNLOCK SUCCESS');
|
|
unlockTarget(lockable, type, lockable.layer);
|
|
window.gameAlert(`Successfully connected to ${requiredDeviceData.name} and unlocked the ${type}.`,
|
|
'success', 'Bluetooth Unlock Successful', 5000);
|
|
} else {
|
|
console.log('BLUETOOTH SIGNAL TOO WEAK',
|
|
`Signal: ${requiredDeviceData.signalStrength}dBm < Required: ${minSignalStrength}dBm`);
|
|
window.gameAlert(`Bluetooth device detected but signal too weak (${requiredDeviceData.signalStrength}dBm). Move closer.`,
|
|
'error', 'Weak Signal', 4000);
|
|
}
|
|
} else {
|
|
console.log('BLUETOOTH DEVICE NOT FOUND',
|
|
`Required: '${requiredDevice}', Available: ${bluetoothData.map(d => d.name || d.mac).join(", ") || "none"}`);
|
|
window.gameAlert(`This ${type} requires connection to '${requiredDevice}', which hasn't been detected yet.`,
|
|
'error', 'Device Not Found', 5000);
|
|
}
|
|
break;
|
|
|
|
case 'rfid':
|
|
console.log('RFID LOCK UNLOCK ATTEMPT');
|
|
|
|
// Support both single card ID (legacy) and array of card IDs
|
|
const requiredCardIds = Array.isArray(lockRequirements.requires) ?
|
|
lockRequirements.requires : [lockRequirements.requires];
|
|
|
|
// Check if door accepts UID-only emulation (for DESFire cards)
|
|
const acceptsUIDOnly = lockRequirements.acceptsUIDOnly || false;
|
|
|
|
console.log('RFID CARD REQUIRED', requiredCardIds, 'acceptsUIDOnly:', acceptsUIDOnly);
|
|
|
|
// Check for keycards in inventory
|
|
const keycards = window.inventory.items.filter(item =>
|
|
item && item.scenarioData &&
|
|
item.scenarioData.type === 'keycard'
|
|
);
|
|
|
|
// Check if any physical card matches
|
|
const hasValidCard = keycards.some(card =>
|
|
requiredCardIds.includes(card.scenarioData.card_id || card.scenarioData.key_id)
|
|
);
|
|
|
|
// Check for RFID cloner with saved cards
|
|
const cloner = window.inventory.items.find(item =>
|
|
item && item.scenarioData &&
|
|
item.scenarioData.type === 'rfid_cloner'
|
|
);
|
|
|
|
const hasCloner = !!cloner;
|
|
const savedCards = cloner?.scenarioData?.saved_cards || [];
|
|
|
|
// Check if any saved card matches
|
|
const hasValidClone = savedCards.some(card =>
|
|
requiredCardIds.includes(card.card_id || card.key_id)
|
|
);
|
|
|
|
console.log('RFID CHECK', {
|
|
requiredCardIds,
|
|
acceptsUIDOnly,
|
|
hasCloner,
|
|
keycardsCount: keycards.length,
|
|
savedCardsCount: savedCards.length,
|
|
hasValidCard,
|
|
hasValidClone
|
|
});
|
|
|
|
if (keycards.length > 0 || savedCards.length > 0) {
|
|
// Start RFID minigame in unlock mode
|
|
window.startRFIDMinigame(lockable, type, {
|
|
mode: 'unlock',
|
|
requiredCardIds: requiredCardIds, // Pass array
|
|
acceptsUIDOnly: acceptsUIDOnly,
|
|
availableCards: keycards,
|
|
hasCloner: hasCloner,
|
|
onComplete: (success) => {
|
|
if (success) {
|
|
setTimeout(() => {
|
|
unlockTarget(lockable, type, lockable.layer);
|
|
window.gameAlert('RFID lock unlocked!', 'success', 'Access Granted', 3000);
|
|
}, 100);
|
|
}
|
|
}
|
|
});
|
|
} else {
|
|
console.log('NO RFID CARDS OR CLONER AVAILABLE');
|
|
window.gameAlert('Requires RFID keycard', 'error', 'Access Denied', 4000);
|
|
}
|
|
break;
|
|
|
|
default:
|
|
window.gameAlert(`This ${type} requires ${lockRequirements.lockType} to unlock.`, 'info', 'Locked', 4000);
|
|
break;
|
|
}
|
|
}
|
|
|
|
export function getLockRequirementsForDoor(doorSprite) {
|
|
// First, check if the door sprite has lock properties directly
|
|
if (doorSprite.doorProperties) {
|
|
const props = doorSprite.doorProperties;
|
|
if (props.locked) {
|
|
return {
|
|
lockType: props.lockType,
|
|
requires: props.requires,
|
|
keyPins: props.keyPins, // Include keyPins for scenario-based locks
|
|
difficulty: props.difficulty
|
|
};
|
|
}
|
|
}
|
|
|
|
// Fallback: Try to find lock requirements from scenario data
|
|
const doorWorldX = doorSprite.x;
|
|
const doorWorldY = doorSprite.y;
|
|
|
|
const overlappingRooms = [];
|
|
Object.entries(rooms).forEach(([roomId, otherRoom]) => {
|
|
const doorCheckArea = {
|
|
x: doorWorldX - DOOR_ALIGN_OVERLAP,
|
|
y: doorWorldY - DOOR_ALIGN_OVERLAP,
|
|
width: DOOR_ALIGN_OVERLAP * 2,
|
|
height: DOOR_ALIGN_OVERLAP * 2
|
|
};
|
|
|
|
const roomBounds = {
|
|
x: otherRoom.position.x,
|
|
y: otherRoom.position.y,
|
|
width: otherRoom.map.widthInPixels,
|
|
height: otherRoom.map.heightInPixels
|
|
};
|
|
|
|
if (boundsOverlap(doorCheckArea, roomBounds)) {
|
|
const roomCenterX = roomBounds.x + (roomBounds.width / 2);
|
|
const roomCenterY = roomBounds.y + (roomBounds.height / 2);
|
|
const player = window.player;
|
|
const distanceToPlayer = player ? Phaser.Math.Distance.Between(
|
|
player.x, player.y,
|
|
roomCenterX, roomCenterY
|
|
) : 0;
|
|
|
|
const gameScenario = window.gameScenario;
|
|
const roomData = gameScenario?.rooms?.[roomId];
|
|
|
|
overlappingRooms.push({
|
|
id: roomId,
|
|
room: otherRoom,
|
|
distance: distanceToPlayer,
|
|
lockType: roomData?.lockType,
|
|
requires: roomData?.requires,
|
|
keyPins: roomData?.keyPins || roomData?.key_pins, // Include keyPins from scenario (supports both cases)
|
|
difficulty: roomData?.difficulty,
|
|
locked: roomData?.locked
|
|
});
|
|
}
|
|
});
|
|
|
|
const lockedRooms = overlappingRooms
|
|
.filter(r => r.locked)
|
|
.sort((a, b) => b.distance - a.distance);
|
|
|
|
if (lockedRooms.length > 0) {
|
|
const targetRoom = lockedRooms[0];
|
|
return {
|
|
lockType: targetRoom.lockType,
|
|
requires: targetRoom.requires,
|
|
keyPins: targetRoom.keyPins, // Include keyPins from scenario
|
|
difficulty: targetRoom.difficulty
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export function getLockRequirementsForItem(item) {
|
|
if (!item.scenarioData) return null;
|
|
|
|
return {
|
|
lockType: item.scenarioData.lockType || 'key',
|
|
requires: item.scenarioData.requires || '',
|
|
keyPins: item.scenarioData.keyPins, // Include keyPins for scenario-based locks
|
|
difficulty: item.scenarioData.difficulty
|
|
};
|
|
}
|
|
|
|
export function unlockTarget(lockable, type, layer) {
|
|
console.log('🔓 unlockTarget called:', { type, lockable });
|
|
|
|
if (type === 'door') {
|
|
// After unlocking, use the proper door unlock function
|
|
unlockDoor(lockable);
|
|
|
|
// Emit door unlocked event
|
|
console.log('🔓 Checking for eventDispatcher:', !!window.eventDispatcher);
|
|
if (window.eventDispatcher) {
|
|
const doorProps = lockable.doorProperties || {};
|
|
console.log('🔓 Emitting door_unlocked event:', doorProps);
|
|
window.eventDispatcher.emit('door_unlocked', {
|
|
roomId: doorProps.roomId,
|
|
connectedRoom: doorProps.connectedRoom,
|
|
direction: doorProps.direction,
|
|
lockType: doorProps.lockType
|
|
});
|
|
}
|
|
} else {
|
|
// Handle item unlocking
|
|
if (lockable.scenarioData) {
|
|
lockable.scenarioData.locked = false;
|
|
// Set new state for containers with contents
|
|
if (lockable.scenarioData.contents) {
|
|
lockable.scenarioData.isUnlockedButNotCollected = true;
|
|
|
|
// Emit item unlocked event
|
|
if (window.eventDispatcher) {
|
|
window.eventDispatcher.emit('item_unlocked', {
|
|
itemType: lockable.scenarioData.type,
|
|
itemName: lockable.scenarioData.name,
|
|
lockType: lockable.scenarioData.lockType
|
|
});
|
|
}
|
|
|
|
// Automatically launch container minigame after unlocking
|
|
setTimeout(() => {
|
|
if (window.handleContainerInteraction) {
|
|
console.log('Auto-launching container minigame after unlock');
|
|
window.handleContainerInteraction(lockable);
|
|
}
|
|
}, 500); // Small delay to ensure unlock message is shown
|
|
|
|
return; // Return early to prevent automatic collection
|
|
}
|
|
} else {
|
|
lockable.locked = false;
|
|
if (lockable.contents) {
|
|
lockable.isUnlockedButNotCollected = true;
|
|
|
|
// Emit item unlocked event
|
|
if (window.eventDispatcher) {
|
|
window.eventDispatcher.emit('item_unlocked', {
|
|
itemType: lockable.type || 'unknown',
|
|
itemName: lockable.name,
|
|
lockType: lockable.lockType
|
|
});
|
|
}
|
|
|
|
// Automatically launch container minigame after unlocking
|
|
setTimeout(() => {
|
|
if (window.handleContainerInteraction) {
|
|
console.log('Auto-launching container minigame after unlock');
|
|
window.handleContainerInteraction(lockable);
|
|
}
|
|
}, 500); // Small delay to ensure unlock message is shown
|
|
|
|
return; // Return early to prevent automatic collection
|
|
}
|
|
}
|
|
}
|
|
console.log(`${type} unlocked successfully`);
|
|
}
|
|
|
|
// Export for global access
|
|
window.handleUnlock = handleUnlock;
|
|
window.getLockRequirementsForDoor = getLockRequirementsForDoor;
|
|
window.getLockRequirementsForItem = getLockRequirementsForItem;
|
|
window.unlockTarget = unlockTarget;
|
|
|