docs: Add comprehensive RFID keycard lock system planning documentation

Planning documentation for new RFID keycard lock system feature:

Documentation:
- Complete planning docs in planning_notes/rfid_keycard/
- 7 planning documents (~16,000 words)
- 90+ implementation tasks with estimates
- Technical architecture and code design
- Asset specifications and creation guides

Assets Created:
- 4 keycard sprite variants (CEO, Security, Maintenance, Generic)
- RFID cloner device sprite (Flipper Zero-inspired)
- 2 icon assets (RFID icon, NFC waves)
- Helper scripts for placeholder creation

System Documentation:
- Lock & key system architecture reference
- Lock & key quick start guide

Feature Overview:
- New lock type: "rfid"
- New items: keycard, rfid_cloner
- New minigame: Flipper Zero-style RFID reader/cloner
- Ink tag support: # clone_keycard:name|hex
- Inventory integration: Click cards to clone
- Two modes: Unlock (tap/emulate) and Clone (read/save)

Implementation:
- Estimated time: 91 hours (~11 working days)
- 11 new files, 5 modified files
- Full test plan included
- Ready for immediate implementation

All placeholder assets functional for development.
Documentation provides complete roadmap from planning to deployment.
This commit is contained in:
Z. Cliffe Schreuders
2025-11-15 23:48:15 +00:00
parent 5b75d75a01
commit bd8b9a4f85
17 changed files with 5495 additions and 0 deletions

View File

@@ -0,0 +1,285 @@
# Lock & Key System - Quick Start Guide
## Quick Reference: Where Things Are
### Adding a Locked Door/Room
File: `scenario.json`
```json
{
"rooms": {
"office": {
"locked": true,
"lockType": "key", // key, pin, password, biometric, bluetooth
"requires": "office_key", // ID of key/password/etc.
"keyPins": [32, 28, 35, 30] // Optional: for pin tumbler locks
}
}
}
```
### Adding a Key to Inventory
File: `scenario.json`
```json
{
"startItemsInInventory": [
{
"type": "key",
"name": "Office Key",
"key_id": "office_key", // Must match "requires" in lock
"keyPins": [32, 28, 35, 30], // Must match lock's keyPins
"observations": "A brass key"
}
]
}
```
### Adding a Key in a Container
File: `scenario.json`
```json
{
"type": "safe",
"locked": true,
"lockType": "password",
"requires": "1234",
"contents": [
{
"type": "key",
"name": "CEO Key",
"key_id": "ceo_key",
"keyPins": [40, 35, 38, 32, 36]
}
]
}
```
---
## System Entry Points
### When Player Clicks a Locked Object
```
interactions.js: handleObjectInteraction(sprite)
→ Gets scenario data from sprite
→ Calls: handleUnlock(lockable, 'door'|'item')
```
### Unlock System Decision Tree
```
unlock-system.js: handleUnlock()
├─ Lock type: 'key'
│ ├─ Has keys in inventory? → Key Selection Minigame
│ └─ Has lockpick? → Lockpicking Minigame
├─ Lock type: 'pin' → PIN Entry Minigame
├─ Lock type: 'password' → Password Entry Minigame
├─ Lock type: 'biometric' → Check fingerprint samples
└─ Lock type: 'bluetooth' → Check BLE devices
```
---
## Key Code Files
### Primary Lock Logic
```
js/systems/
├─ unlock-system.js [400 lines] Main unlock handler
├─ key-lock-system.js [370 lines] Key-lock mappings & cuts
├─ inventory.js [630 lines] Item/key management
└─ interactions.js [600 lines] Object interaction detector
```
### Lockpicking Minigame (Pin Tumbler)
```
js/minigames/lockpicking/
├─ lockpicking-game-phaser.js [300+ lines] Main controller
├─ pin-management.js [150+ lines] Pin creation
├─ key-operations.js [100+ lines] Key handling
├─ hook-mechanics.js [100+ lines] Tension simulation
├─ pin-visuals.js [80+ lines] Rendering
└─ lock-configuration.js [60+ lines] State storage
```
### Minigame Framework
```
js/minigames/framework/
├─ minigame-manager.js [180 lines] Framework lifecycle
├─ base-minigame.js [150+ lines] Base class
└─ index.js [96 lines] Registration
```
### Conversation Integration
```
js/minigames/helpers/
└─ chat-helpers.js [326 lines] Ink tag processing
js/minigames/person-chat/
└─ person-chat-minigame.js [300+ lines] Conversation UI
```
---
## Key Data Structures
### Pin Tumbler Lock (During Gameplay)
```javascript
pin = {
index: 0,
binding: 2, // Which pin sets first (0-3)
isSet: false,
keyPinLength: 32, // Lock pin height (pixels)
driverPinLength: 43, // Spring pin height
keyPinHeight: 0, // Current key pin position
container: Phaser.Container
}
```
### Key Data (In Inventory)
```javascript
key = {
scenarioData: {
type: 'key',
name: 'Office Key',
key_id: 'office_key',
keyPins: [32, 28, 35, 30], // Lock pin heights this key opens
observations: 'A brass key'
},
objectId: 'inventory_key_office_key'
}
```
### Lock Requirements (From Scenario)
```javascript
lockRequirements = {
lockType: 'key', // Type of lock
requires: 'office_key', // Key ID / password / etc.
keyPins: [32, 28, 35, 30], // For scenario-defined locks
difficulty: 'medium' // For lockpicking
}
```
---
## Common Workflows
### Scenario Designer: Add New Key-Protected Door
1. **Define the lock in room:**
```json
{
"room_name": {
"locked": true,
"lockType": "key",
"requires": "storage_key",
"keyPins": [30, 32, 28, 35] // IMPORTANT: unique pin heights
}
}
```
2. **Add key to inventory or container:**
```json
{
"type": "key",
"name": "Storage Key",
"key_id": "storage_key", // Must match "requires"
"keyPins": [30, 32, 28, 35] // Must match lock exactly
}
```
3. **Test: Player should see key icon when near lock**
### Scenario Designer: Add PIN-Protected Door
```json
{
"room_name": {
"locked": true,
"lockType": "pin",
"requires": "4567" // The PIN code
}
}
```
### Scenario Designer: Add Password-Protected Safe
```json
{
"type": "safe",
"locked": true,
"lockType": "password",
"requires": "correct_password",
"contents": [
{ "type": "notes", "name": "Secret Document" }
]
}
```
### Ink Writer: Give Key During Conversation
In `.ink` file:
```ink
=== hub ===
# speaker:npc
Here's the key you'll need!
# give_item:key|Storage Key
What else can I help with?
```
This triggers:
1. NPC gives item to player
2. Opens container minigame showing the key
3. Player can take it to inventory
---
## Debugging Tips
### Check Key-Lock Mappings
In browser console:
```javascript
window.showKeyLockMappings() // Shows all key-lock pairs
```
### Check Player Inventory
```javascript
console.log(window.inventory.items) // All items
console.log(window.inventory.keyRing) // Keys specifically
```
### Check Lock Requirements
```javascript
window.getLockRequirementsForDoor(doorSprite) // Door lock details
window.getLockRequirementsForItem(item) // Item lock details
```
### Force Unlock (Testing)
```javascript
window.DISABLE_LOCKS = true // Disables all locks temporarily
```
---
## Common Errors & Solutions
| Error | Cause | Solution |
|-------|-------|----------|
| Key doesn't unlock door | `key_id` doesn't match `requires` | Ensure exact match |
| Wrong pins in lock | `keyPins` mismatch | Key's keyPins must match lock's keyPins |
| Key doesn't appear in inventory | Item not in `startItemsInInventory` | Add it to scenario or container |
| Conversation tag not working | Tag format incorrect | Use `# action:param` format |
| Minigame won't start | Framework not initialized | Check if MinigameFramework is loaded |
---
## Implementation Checklist
For adding a new lock type (e.g., RFID/Keycard):
- [ ] Add case in `unlock-system.js` switch statement
- [ ] Check inventory for matching keycard
- [ ] Verify access level (if applicable)
- [ ] Call `unlockTarget()` on success
- [ ] Show appropriate alert messages
- [ ] Update scenario schema with examples
- [ ] Add documentation with tag examples
- [ ] Test with example scenario

View File

@@ -0,0 +1,613 @@
# BreakEscape Lock & Key System Architecture
## Overview
The lock and key system in BreakEscape supports multiple lock types with a hierarchical architecture. Keys work with pintumbler locks through a scenario-based configuration system that ensures consistency between keys and locks.
---
## 1. Lock Types Definition
### Supported Lock Types
Locks are defined in the scenario JSON with the following types:
| Lock Type | Definition Location | Unlocking Method | Key File |
|-----------|-------------------|------------------|----------|
| **key** | Room `lockType` or Object `lockType` | Physical key or lockpicking | `/js/systems/key-lock-system.js` |
| **pin** | Room/Object `lockType: "pin"` | PIN code entry | `/js/systems/unlock-system.js` (case: 'pin') |
| **password** | Room/Object `lockType: "password"` | Password text entry | `/js/systems/unlock-system.js` (case: 'password') |
| **biometric** | Room/Object `lockType: "biometric"` | Fingerprint scanning | `/js/systems/unlock-system.js` (case: 'biometric') |
| **bluetooth** | Object `lockType: "bluetooth"` | Bluetooth device connection | `/js/systems/unlock-system.js` (case: 'bluetooth') |
### Lock Definition in Scenarios
**Door Lock Example (scenario3.json):**
```json
{
"room_reception": {
"locked": true,
"lockType": "key",
"requires": "briefcase_key", // Key ID that unlocks this
"objects": [ ... ]
}
}
```
**Object Lock Example:**
```json
{
"type": "safe",
"name": "Safe1",
"locked": true,
"lockType": "password",
"requires": "TimeIsMoney_123" // Password required
}
```
---
## 2. Pin Tumbler Lock Minigame Implementation
### Lockpicking Minigame Architecture
The pintumbler minigame simulates a realistic lock picking experience with interactive pin manipulation.
**Main Implementation:** `/js/minigames/lockpicking/lockpicking-game-phaser.js`
### Key Components
#### 2.1 Pin Management (`pin-management.js`)
- **createPins()**: Creates pin objects with binding order
- **Pin Structure:**
```javascript
pin = {
index: 0-4,
binding: randomized binding order,
isSet: false,
keyPinLength: 25-65 pixels, // Lock pin height
driverPinLength: 75 - keyPinLength, // Spring pin height
originalHeight: keyPinLength,
currentHeight: 0,
container: Phaser container,
keyPin: graphics object,
driverPin: graphics object,
spring: graphics object
}
```
#### 2.2 Key Operations (`key-operations.js`)
- Handles inserting and manipulating the key
- Calculates how much each pin lifts based on key cut depth
- Formula: `keyPinHeight = (key.cuts[i] / maxCutDepth) * keyPinLength`
#### 2.3 Hook Mechanics (`hook-mechanics.js`)
- Simulates applying tension to the lock
- Implements binding order mechanics
- When tension applied, pins in binding order become "biddable"
- Player can only lift set pins when tension applied
#### 2.4 Pin Visuals (`pin-visuals.js`)
- Renders pins with visual feedback
- Shows alignment with shear line (goal position)
- Highlights when pins are set correctly
### Pintumbler Lock State
**Lock State Object:**
```javascript
lockState = {
id: "lock_id",
pinCount: 4,
pinHeights: [32, 28, 35, 30], // Predefined from scenario keyPins
difficulty: "medium",
pins: [ /* pin objects */ ],
bindingOrder: [2, 0, 3, 1], // Random order
tensionApplied: false,
success: false,
progress: 0
}
```
### Pin Height Configuration
**From Scenario Data (keyPins):**
```json
{
"room": {
"locked": true,
"lockType": "key",
"requires": "office_key",
"keyPins": [32, 28, 35, 30] // Exact pin heights for this lock
}
}
```
The `keyPins` array directly corresponds to the pintumbler lock's key pin lengths. These values are typically normalized to 25-65 pixel range during gameplay.
---
## 3. Key System
### Key Definition in Scenarios
**Key in Starting Inventory:**
```json
{
"startItemsInInventory": [
{
"type": "key",
"name": "Office Key",
"key_id": "office_key",
"keyPins": [32, 28, 35, 30], // Matches lock's pin heights
"observations": "A brass key with a distinctive cut"
}
]
}
```
**Key in Container:**
```json
{
"type": "safe",
"contents": [
{
"type": "key",
"name": "Briefcase Key",
"key_id": "briefcase_key",
"keyPins": [40, 35, 38, 32, 36], // 5-pin lock
"observations": "Found inside the safe"
}
]
}
```
### Key-Lock Mapping System
**File:** `/js/systems/key-lock-system.js`
**Global Key-Lock Mappings:**
```javascript
window.keyLockMappings = {
"office_key": {
lockId: "office_room_lock",
lockConfig: { /* pin config */ },
keyName: "Office Key",
roomId: "office",
lockName: "Office Door"
},
"briefcase_key": {
lockId: "briefcase_lock",
lockConfig: { /* pin config */ },
keyName: "Briefcase Key",
roomId: "ceo_office",
objectIndex: 0,
lockName: "Briefcase"
}
}
```
### Key Ring System
**Inventory Key Ring (`inventory.js`):**
```javascript
window.inventory.keyRing = {
keys: [keySprite1, keySprite2, ...],
slot: DOM element,
itemImg: DOM image
}
```
**Features:**
- Multiple keys grouped into single "Key Ring" UI item
- Key ring shows single key icon if 1 key, multiple key icon if 2+
- Click key ring to see list of available keys
- Tooltip shows key names
### Key Cut Generation
**File:** `/js/systems/key-lock-system.js` - `generateKeyCutsForLock()`
**Formula for Converting Lock Pin Heights to Key Cuts:**
```javascript
cutDepth = keyPinLength - gapFromKeyBladeTopToShearLine
// World coordinates:
keyBladeTop_world = 175 // Top surface of inserted key blade
shearLine_world = 155 // Where lock plug meets housing
gap = 20 // Distance between them (175 - 155)
cutDepth = keyPinLength - 20 // So 65 pixel pin → 45 pixel cut
clampedCut = Math.max(0, Math.min(110, cutDepth)) // 0-110 range
```
---
## 4. Items System
### Item Definition
**File:** `/js/systems/inventory.js`
**Item Types Supported:**
- `key` - Physical key for locks
- `key_ring` - Container for multiple keys
- `lockpick` - Lockpicking tool
- `phone` - Phone for chat conversations
- `notes` - Written notes/clues
- `workstation` - Crypto analysis tool
- `fingerprint_kit` - Biometric collection
- `bluetooth_scanner` - Bluetooth device scanner
- `pc`, `safe`, `tablet` - Interactive objects
- `notepad` - Inventory notepad
### Item Inventory Structure
**Inventory Object:**
```javascript
window.inventory = {
items: [DOM img elements],
container: HTMLElement,
keyRing: {
keys: [key items],
slot: DOM element,
itemImg: DOM image
}
}
```
**Item Data Structure:**
```javascript
inventoryItem = {
scenarioData: {
type: "key",
name: "Office Key",
key_id: "office_key",
keyPins: [32, 28, 35, 30],
locked: false,
lockType: "key",
observations: "A brass key"
},
name: "key",
objectId: "inventory_key_office_key"
}
```
### Item Interaction Flow
1. **Item Added to Inventory:** `addToInventory(sprite)`
- Creates DOM slot and image
- Special handling for keys → key ring
- Stores scenario data with item
- Emits `item_picked_up` event
2. **Item Clicked:** `handleObjectInteraction(item)`
- Checks item type
- Triggers appropriate minigame/action
3. **Key Ring Interaction:** `handleKeyRingInteraction(keyRingItem)`
- Single key: opens lock selection
- Multiple keys: shows tooltip with key list
---
## 5. Minigame Triggering & Management
### Minigame Framework
**File:** `/js/minigames/framework/minigame-manager.js`
**Architecture:**
```javascript
window.MinigameFramework = {
mainGameScene: null,
currentMinigame: null,
registeredScenes: {
'lockpicking': LockpickingMinigamePhaser,
'pin': PinMinigame,
'password': PasswordMinigame,
'person-chat': PersonChatMinigame,
// ... more minigames
},
startMinigame(sceneType, container, params),
endMinigame(success, result),
registerScene(sceneType, SceneClass)
}
```
### Registered Minigames
| Minigame | Scene Type | File | Purpose |
|----------|-----------|------|---------|
| Lockpicking | `lockpicking` | `lockpicking/lockpicking-game-phaser.js` | Pin tumbler lock picking |
| PIN Entry | `pin` | `pin/pin-minigame.js` | Numeric PIN code entry |
| Password | `password` | `password/password-minigame.js` | Text password entry |
| Person Chat | `person-chat` | `person-chat/person-chat-minigame.js` | In-person NPC conversations |
| Phone Chat | `phone-chat` | `phone-chat/phone-chat-minigame.js` | Phone-based conversations |
| Container | `container` | `container/container-minigame.js` | Open containers/safes |
| Notes | `notes` | `notes/notes-minigame.js` | View notes/evidence |
| Biometrics | `biometrics` | `biometrics/biometrics-minigame.js` | Fingerprint scanning |
| Bluetooth Scanner | `bluetooth-scanner` | `bluetooth/bluetooth-scanner-minigame.js` | BLE device scanning |
### Minigame Triggering Flow
**From Object Interaction:**
```
Object clicked → handleObjectInteraction()
→ Checks scenarioData.type
→ If key: Check if locked
→ If locked & requires key: startKeySelectionMinigame()
→ If locked & no key: Check for lockpick → startLockpickingMinigame()
→ If container: handleContainerInteraction()
→ If phone/notes/etc: startMinigame('type')
```
**Lock Unlocking Flow:**
```
handleUnlock(lockable, type='door'|'item')
→ getLockRequirements()
→ Check lockType:
case 'key':
→ if keys available: startKeySelectionMinigame()
→ else if lockpick: startLockpickingMinigame()
case 'pin':
→ startPinMinigame()
case 'password':
→ startPasswordMinigame()
case 'biometric':
→ Check biometric samples, unlock if match
case 'bluetooth':
→ Check BLE devices, unlock if found & signal strong enough
```
### Minigame Lifecycle
```
1. startMinigame(sceneType, container, params)
2. Minigame construction: new MinigameClass(container, params)
3. init() - Setup UI structure
4. start() - Begin minigame logic
5. User interaction/gameplay
6. onComplete callback(success, result)
7. cleanup() - Remove DOM elements
8. Re-enable main game input
```
---
## 6. Ink Conversations Integration
### Ink Engine
**File:** `/js/systems/ink/ink-engine.js`
**InkEngine Wrapper:**
```javascript
export default class InkEngine {
loadStory(storyJson) // Load compiled Ink JSON
continue() // Get next dialogue + choices
goToKnot(knotName) // Jump to specific knot
choose(index) // Select a choice
getVariable(name) // Read Ink variable
setVariable(name, value) // Set Ink variable
}
```
### Ink Tag System
**Tags in Ink Stories (`equipment-officer.ink`):**
```ink
=== start ===
# speaker:npc
Welcome to equipment supply!
-> hub
=== show_inventory ===
# speaker:npc
Here's everything we have!
# give_npc_inventory_items
What else can I help with?
-> hub
```
**Tag Processing:**
**File:** `/js/minigames/helpers/chat-helpers.js`
**Supported Action Tags:**
| Tag | Format | Effect |
|-----|--------|--------|
| `unlock_door` | `# unlock_door:room_id` | Unlocks specified door |
| `give_item` | `# give_item:item_type` | Gives item to player |
| `give_npc_inventory_items` | `# give_npc_inventory_items:type1,type2` | Opens container UI |
| `set_objective` | `# set_objective:text` | Updates mission objective |
| `add_note` | `# add_note:title\|content` | Adds note to collection |
| `reveal_secret` | `# reveal_secret:id\|data` | Reveals game secret |
| `trigger_minigame` | `# trigger_minigame:name` | Triggers a minigame |
| `influence_increased` | `# influence_increased` | Increases NPC influence |
| `influence_decreased` | `# influence_decreased` | Decreases NPC influence |
| `speaker:player` | `# speaker:player` | Sets speaker to player |
| `speaker:npc` | `# speaker:npc` | Sets speaker to NPC |
### Conversation Minigame Handoff
**Person Chat Flow:**
```
PersonChatMinigame.advance()
→ call inkEngine.continue()
→ get currentText + currentChoices + currentTags
→ processGameActionTags(tags, ui)
→ For each action tag, call NPCGameBridge methods
→ Show notifications
→ Display dialogue + choices to player
```
**Tag Execution Example:**
```javascript
// In equipment-officer.ink: "# give_npc_inventory_items:lockpick,workstation"
// becomes:
window.NPCGameBridge.showNPCInventory(npcId, ['lockpick', 'workstation'])
→ Triggers container minigame
→ Shows items filtered by type
→ Player can take items
```
---
## 7. RFID & Keycard System
### Current Status
**RFID/Keycard is mentioned in documentation but NOT YET IMPLEMENTED in game logic.**
**References:**
- `equipment-officer.ink` mentions "keycards for security"
- No corresponding lock type or unlock logic currently
### Architecture for Implementation
**Proposed Keycard Lock Type:**
**Scenario Definition:**
```json
{
"room": {
"locked": true,
"lockType": "keycard",
"requires": "ceo_keycard", // Keycard ID
"requiredAccessLevel": 3 // Optional: access level check
},
"objects": [
{
"type": "keycard",
"name": "CEO Keycard",
"key_id": "ceo_keycard",
"accessLevel": 3,
"accessRooms": ["ceo_office", "server_room"],
"observations": "A magnetic keycard for building access"
}
]
}
```
**Unlock Logic (to be added):**
```javascript
case 'keycard':
const requiredCardId = lockRequirements.requires;
const requiredLevel = lockRequirements.requiredAccessLevel || 1;
// Check inventory for matching keycard
const keycard = window.inventory.items.find(item =>
item.scenarioData?.type === 'keycard' &&
item.scenarioData?.key_id === requiredCardId &&
(item.scenarioData?.accessLevel || 1) >= requiredLevel
);
if (keycard) {
window.gameAlert(`Keycard accepted!`, 'success');
unlockTarget(lockable, type);
} else {
window.gameAlert(`Access denied - keycard required`, 'error');
}
break;
```
---
## Key Files Reference
### Lock/Key System Files
| File | Purpose |
|------|---------|
| `/js/systems/key-lock-system.js` | Key-lock mapping, cut generation |
| `/js/systems/unlock-system.js` | Main unlock logic for all lock types |
| `/js/systems/inventory.js` | Inventory management, key ring |
| `/js/systems/interactions.js` | Object interaction detection |
### Lockpicking Minigame
| File | Purpose |
|------|---------|
| `/js/minigames/lockpicking/lockpicking-game-phaser.js` | Main minigame controller |
| `/js/minigames/lockpicking/pin-management.js` | Pin creation and state |
| `/js/minigames/lockpicking/key-operations.js` | Key insertion and manipulation |
| `/js/minigames/lockpicking/hook-mechanics.js` | Tension and binding order |
| `/js/minigames/lockpicking/pin-visuals.js` | Pin rendering |
| `/js/minigames/lockpicking/key-geometry.js` | Key blade mathematics |
| `/js/minigames/lockpicking/lock-configuration.js` | Lock state management |
### Minigame Framework
| File | Purpose |
|------|---------|
| `/js/minigames/framework/minigame-manager.js` | Framework lifecycle |
| `/js/minigames/framework/base-minigame.js` | Base class for all minigames |
| `/js/minigames/index.js` | Minigame registration |
### Conversation System
| File | Purpose |
|------|---------|
| `/js/minigames/person-chat/person-chat-minigame.js` | Person conversation controller |
| `/js/minigames/person-chat/person-chat-conversation.js` | Conversation flow |
| `/js/minigames/helpers/chat-helpers.js` | Tag processing |
| `/js/systems/ink/ink-engine.js` | Ink story wrapper |
### Scenario Examples
| File | Content |
|------|---------|
| `/scenarios/scenario3.json` | Key locks example |
| `/scenarios/scenario1.json` | Biometric/Bluetooth/Password locks |
| `/scenarios/scenario2.json` | PIN locks |
---
## Summary: Lock Unlock Flow Diagram
```
Player clicks locked door/object
handleObjectInteraction() / handleUnlock()
getLockRequirements() - Extract lock type & requirements
Switch on lockType:
├─ 'key' →
│ ├─ Has keys? → startKeySelectionMinigame()
│ │ └─ Player selects key
│ │ └─ Key cuts checked against lock pins
│ │ └─ If match: unlock, show success
│ │
│ └─ Has lockpick? → startLockpickingMinigame()
│ └─ Interactive pin tumbler UI
│ └─ Player manipulates pins with hook
│ └─ All pins set → shear line alignment
│ └─ Lock opens
├─ 'pin' → startPinMinigame()
│ └─ Numeric keypad UI
│ └─ Enter PIN code
│ └─ If correct: unlock
├─ 'password' → startPasswordMinigame()
│ └─ Text input dialog
│ └─ Enter password
│ └─ If correct: unlock
├─ 'biometric' →
│ └─ Check gameState.biometricSamples
│ └─ If fingerprint quality ≥ threshold: unlock
└─ 'bluetooth' →
└─ Check gameState.bluetoothDevices
└─ If device found & signal strong: unlock
unlockTarget(lockable, type)
├─ If type='door': unlockDoor()
└─ If type='item': set locked=false, show container if contents
Emit 'door_unlocked'/'item_unlocked' event
```