Add Codebase Review Document and Refactor Key Systems: Introduce a comprehensive review of the codebase organization, highlighting areas for improvement and recent refactoring successes. Refactor key-lock and unlock systems for better modularity and maintainability, consolidating logic into dedicated modules. Update relevant files and documentation to reflect these changes, ensuring a clearer structure and improved performance.

This commit is contained in:
Z. Cliffe Schreuders
2025-10-12 23:11:32 +01:00
parent d46fa79718
commit 40edca9fc7
17 changed files with 1823 additions and 1742 deletions

View File

@@ -7,11 +7,13 @@ This document provides a comprehensive overview of the BreakEscape codebase arch
1. [Architecture Overview](#architecture-overview)
2. [File Layout](#file-layout)
3. [Core Components](#core-components)
4. [Game Systems](#game-systems)
5. [Asset Organization](#asset-organization)
6. [Implementing New Mini-Games](#implementing-new-mini-games)
7. [CSS Architecture](#css-architecture)
8. [Development Workflow](#development-workflow)
4. [Recent Refactoring (2024)](#recent-refactoring-2024)
5. [Game Systems](#game-systems)
6. [Asset Organization](#asset-organization)
7. [Implementing New Mini-Games](#implementing-new-mini-games)
8. [CSS Architecture](#css-architecture)
9. [Development Workflow](#development-workflow)
10. [Architecture Notes](#architecture-notes)
## Architecture Overview
@@ -30,6 +32,17 @@ BreakEscape is built using modern web technologies with a modular architecture:
3. **Maintainability**: Clean separation between game logic, UI, and data
4. **Performance**: Efficient asset loading and memory management
### Recent Improvements (2024)
The codebase recently underwent significant refactoring:
-**Reduced code duplication** - Eliminated ~245 lines of duplicate code
-**Better organization** - Split monolithic files into focused modules
-**Fixed critical bugs** - Biometric and Bluetooth locks now work correctly
-**Single source of truth** - Unified unlock system for all lock types
-**Improved robustness** - Better handling of dynamic room loading
See [Recent Refactoring (2024)](#recent-refactoring-2024) for details.
## File Layout
```
@@ -57,13 +70,18 @@ BreakEscape/
│ │ └── pathfinding.js # A* pathfinding for player movement
│ │
│ ├── systems/ # Game systems and mechanics
│ │ ├── inventory.js # Inventory management
│ │ ├── interactions.js # Object interaction and collision detection
│ │ ├── notifications.js # In-game notification system
│ │ ├── notes.js # Notes panel for clues and information
│ │ ├── biometrics.js # Fingerprint collection and matching
│ │ ├── bluetooth.js # Bluetooth device scanning
│ │ ── debug.js # Debug tools and development helpers
│ │ ├── interactions.js # Core interaction routing - refactored!
│ │ ├── unlock-system.js # Centralized unlock logic for all lock types
│ │ ├── key-lock-system.js # Key-lock mapping and validation
│ │ ├── biometrics.js # Fingerprint collection and dusting
│ │ ├── minigame-starters.js # Minigame initialization
│ │ ├── inventory.js # Inventory management and item handling
│ │ ── doors.js # Door sprites, interactions, and transitions
│ │ ├── collision.js # Wall collision detection and management
│ │ ├── object-physics.js # Chair physics and object collisions
│ │ ├── player-effects.js # Visual effects for player interactions
│ │ ├── notifications.js # In-game notification system
│ │ └── debug.js # Debug tools and development helpers
│ │
│ ├── ui/ # User interface components
│ │ ├── panels.js # Side panels (biometrics, bluetooth, notes)
@@ -80,9 +98,17 @@ BreakEscape/
│ │ ├── base-minigame.js # Base class for all mini-games
│ │ └── minigame-manager.js # Mini-game lifecycle management
│ ├── lockpicking/ # Lockpicking mini-game
│ │ └── lockpicking-game.js
── dusting/ # Fingerprint dusting mini-game
└── dusting-game.js
│ │ └── lockpicking-game-phaser.js
── dusting/ # Fingerprint dusting mini-game
└── dusting-game.js
│ ├── biometrics/ # Biometric scanner minigame
│ │ └── biometrics-minigame.js
│ ├── bluetooth/ # Bluetooth scanner minigame
│ │ └── bluetooth-scanner-minigame.js
│ ├── notes/ # Notes viewing minigame
│ │ └── notes-minigame.js
│ └── lockpick/ # Lockpick set minigame
│ └── lockpick-set-minigame.js
├── assets/ # Game assets and resources
│ ├── characters/ # Character sprites and animations
@@ -135,34 +161,105 @@ BreakEscape/
### 2. Game Systems (`js/systems/`)
The game systems have been refactored into specialized, focused modules for better maintainability and code organization.
#### interactions.js (Recently Refactored!)
- **Purpose**: Core interaction routing and object handling
- **Key Features**:
- Click detection on game objects
- Routes interactions to appropriate systems
- Object state management (opened, unlocked, etc.)
- Container object support (safes, suitcases)
- Takeable item handling
- **Architecture**: Lean routing layer that delegates to specialized systems
- **Improvement**: Reduced from 1,605 lines (81% reduction) by extracting specialized functionality
#### unlock-system.js (New!)
- **Purpose**: Centralized unlock logic for all lock types
- **Key Features**:
- Unified unlock handling for doors and items
- Supports 5 lock types: key, PIN, password, biometric, Bluetooth
- Comprehensive biometric validation (fingerprint quality thresholds)
- Bluetooth device matching with signal strength validation
- Dynamic lockpick difficulty per object
- Single source of truth for all unlock logic
- **Benefits**: Eliminates code duplication, consistent behavior across all locked objects
#### key-lock-system.js (New!)
- **Purpose**: Key-lock mapping and pin height generation
- **Key Features**:
- Global key-lock mapping system
- Predefined lock configurations
- Key cut generation for visual representation
- Pin height validation
- Lock-key compatibility checking
- **Integration**: Used by lockpicking minigame for accurate pin representation
#### biometrics.js (New!)
- **Purpose**: Fingerprint collection and analysis
- **Key Features**:
- Fingerprint collection from objects
- Quality-based fingerprint data generation
- Integration with dusting minigame
- Biometric scan handling
- Owner-specific fingerprint matching
- **Workflow**: Collect → Dust → Store → Validate against locks
#### minigame-starters.js (New!)
- **Purpose**: Minigame initialization and setup
- **Key Features**:
- Lockpicking minigame launcher
- Key selection minigame launcher
- Callback management for minigame completion
- Timing coordination with game scene cleanup
- **Architecture**: Handles the bridge between game objects and minigame framework
#### inventory.js
- **Purpose**: Item collection, storage, and usage management
- **Key Features**:
- Drag-and-drop item interaction
- Item usage on objects and locks
- Item addition and removal
- Visual inventory display with item icons
- Drag-and-drop item interaction
- Item identifier creation
- Notepad integration
- **Exports**: Now properly exports functions for use by other systems
#### interactions.js
- **Purpose**: Object interaction detection and processing
#### doors.js
- **Purpose**: Door sprites, interactions, and room transitions
- **Key Features**:
- Click detection on game objects
- Lock validation and unlocking logic
- Object state management (opened, unlocked, etc.)
- Container object support (safes, suitcases)
- Door sprite creation and management
- Door interaction handling
- Door opening animations
- Room transition detection
- Door visibility management
- Collision processing
- **Recent Improvement**: Removed duplicate unlock logic, now uses unlock-system.js
#### biometrics.js
- **Purpose**: Fingerprint collection, analysis, and matching
#### collision.js
- **Purpose**: Wall collision detection and tile management
- **Key Features**:
- Fingerprint collection from objects
- Quality-based matching algorithms
- Biometric panel UI integration
- Wall collision box creation
- Tile removal under doors
- Room-specific collision management
- Player collision registration
- **Robustness**: Uses window.game fallback for dynamic room loading
#### bluetooth.js
- **Purpose**: Bluetooth device simulation and scanning
#### object-physics.js
- **Purpose**: Chair physics and object collisions
- **Key Features**:
- Device discovery based on player proximity
- MAC address tracking
- Bluetooth panel UI integration
- Swivel chair rotation mechanics
- Chair-to-chair collision detection
- Chair-to-wall collision setup
- Collision management for newly loaded rooms
- **Robustness**: Handles collisions for dynamically loaded rooms
#### player-effects.js
- **Purpose**: Visual effects for player interactions
- **Key Features**:
- Bump effects when colliding with objects
- Plant sway animations
- Sprite depth management
- **Polish**: Adds visual feedback to enhance player experience
### 3. UI Framework (`js/ui/`)
@@ -180,6 +277,63 @@ BreakEscape/
- Item examination
- System messages and confirmations
## Recent Refactoring (2024)
The codebase underwent a major refactoring to improve maintainability, eliminate code duplication, and fix critical bugs in the lock system.
### What Changed
#### 1. interactions.js - Massive Reduction (81% smaller!)
- **Before**: 1,605 lines of mixed responsibilities
- **After**: 289 lines of focused interaction routing
- **Extracted**:
- Unlock logic → `unlock-system.js`
- Key-lock mapping → `key-lock-system.js`
- Biometric collection → `biometrics.js`
- Minigame initialization → `minigame-starters.js`
- Inventory functions → `inventory.js`
#### 2. doors.js - Eliminated Duplication
- **Before**: 1,004 lines with duplicate unlock logic
- **After**: 880 lines using centralized unlock system
- **Improvement**: Removed 124 lines of duplicate code, now uses `unlock-system.js`
#### 3. Unified Unlock System
- **Problem**: Door unlock logic was duplicated in two places with inconsistent behavior
- **Solution**: Created `unlock-system.js` as single source of truth
- **Impact**:
- Fixed broken biometric locks (now validates specific fingerprints with quality thresholds)
- Fixed broken Bluetooth locks (now validates specific devices with signal strength)
- Eliminated ~120 lines of duplicate code
- Consistent behavior for all lock types
#### 4. Fixed Dynamic Room Loading
- **Problem**: Collisions and references broke when rooms loaded after minigames
- **Solution**: Updated `collision.js`, `object-physics.js`, and `doors.js` to use `window.game` and `window.rooms` fallbacks
- **Impact**: Proper collision detection in dynamically loaded rooms
### Benefits of Refactoring
1. **Better Code Organization**
- Clear separation of concerns
- Easier to locate specific functionality
- Reduced cognitive load when reading code
2. **Eliminated Bugs**
- Biometric locks now work correctly (specific fingerprint + quality validation)
- Bluetooth locks now work correctly (device matching + signal strength)
- Collision system robust to async room loading
3. **Improved Maintainability**
- Single source of truth for unlock logic
- No code duplication to keep in sync
- Easier to add new lock types or features
4. **Better Testing**
- Smaller, focused modules are easier to test
- Clear interfaces between components
- Fewer dependencies to mock
## Game Systems
### Scenario System
@@ -187,8 +341,14 @@ BreakEscape/
- **Components**: Rooms, objects, locks, and victory conditions
- **Flexibility**: Complete customization without code changes
### Lock System
### Lock System (Recently Improved!)
- **Types**: Key, PIN, password, biometric, Bluetooth proximity
- **Architecture**: Centralized in `unlock-system.js` for consistency
- **Features**:
- Biometric locks validate specific fingerprints with quality thresholds
- Bluetooth locks validate specific devices with signal strength requirements
- Dynamic lockpick difficulty per object
- Comprehensive error messaging
- **Integration**: Works with rooms, objects, and containers
- **Progression**: Supports complex unlocking sequences
@@ -571,11 +731,62 @@ playSound(soundName) {
### Adding New Features
1. Create feature branch
2. Implement in appropriate module
3. Add necessary styles to CSS files
4. Update scenario JSON if needed
5. Test with multiple scenarios
6. Document changes
2. **Identify the right module**: Use the refactored structure
- Interaction routing → `interactions.js`
- Lock logic → `unlock-system.js`
- Key mapping → `key-lock-system.js`
- Biometrics → `biometrics.js`
- Minigames → `minigame-starters.js`
- Inventory → `inventory.js`
3. Implement in appropriate module
4. Add necessary styles to CSS files
5. Update scenario JSON if needed
6. Test with multiple scenarios
7. Document changes
### Code Organization Best Practices
Based on the recent refactoring, follow these principles:
1. **Keep files focused and small** (< 500 lines is ideal, < 1000 is acceptable)
2. **Single Responsibility Principle**: Each module should have one clear purpose
3. **Avoid duplication**: Create shared modules for common functionality
4. **Use proper imports/exports**: Make dependencies explicit
5. **Handle async operations**: Use `window.game` and `window.rooms` fallbacks for dynamic content
6. **Clean up resources**: Always implement proper cleanup in lifecycle methods
### Refactoring Guidelines
When a file grows too large or has mixed responsibilities:
1. **Identify distinct concerns**: Look for natural separation points
2. **Extract to new modules**: Create focused files for each concern
3. **Update imports**: Ensure all references are updated
4. **Test thoroughly**: Verify all functionality still works
5. **Document changes**: Update this README and create migration notes
### Common Patterns
**Global State Access:**
```javascript
// Use fallbacks for dynamic content
const game = gameRef || window.game;
const allRooms = window.rooms || {};
```
**Minigame Integration:**
```javascript
// Use minigame-starters.js for consistency
import { startLockpickingMinigame } from './minigame-starters.js';
startLockpickingMinigame(lockable, window.game, difficulty, callback);
```
**Lock Handling:**
```javascript
// Use centralized unlock system
import { handleUnlock } from './unlock-system.js';
handleUnlock(lockable, 'door'); // or 'item'
```
### Testing Mini-Games
1. Create test scenario with your mini-game object
@@ -583,11 +794,52 @@ playSound(soundName) {
3. Verify cleanup and state management
4. Test on different screen sizes
5. Ensure integration with main game systems
6. Test minigame → room loading transition (timing)
### Performance Considerations
- Use efficient asset loading
- Implement proper cleanup in all systems
- Monitor memory usage with browser dev tools
- Optimize for mobile devices
- Use `setTimeout` delays for minigame → room transitions (100ms recommended)
### Debugging Tips
**Module Reference Issues:**
- If collisions fail in newly loaded rooms, check for `gameRef` vs `window.game`
- If rooms aren't found, use `window.rooms` instead of local `rooms` variable
**Lock System Issues:**
- All lock logic should be in `unlock-system.js` (single source of truth)
- Check `doorProperties` for doors, `scenarioData` for items
**Minigame Timing:**
- Use `setTimeout` callbacks to allow cleanup before room operations
- Default 100ms delay works well for most cases
## Architecture Notes
### Module Dependencies
Current clean architecture (no circular dependencies):
```
interactions.js → unlock-system.js → minigame-starters.js
doors.js → unlock-system.js → minigame-starters.js
unlock-system.js → doors.js (for unlockDoor callback only)
```
**Avoid creating new circular dependencies!** If two modules need each other, create an intermediary module.
### Global State Pattern
The game uses `window.*` for shared state:
- `window.game` - Phaser game instance
- `window.rooms` - Room data
- `window.player` - Player sprite
- `window.inventory` - Inventory system
- `window.gameState` - Game progress data
This pattern works well for a game of this size and simplifies debugging (accessible from console).
This documentation provides a comprehensive foundation for understanding and extending the BreakEscape codebase. For specific implementation questions, refer to the existing code examples in the repository.

View File

@@ -1,173 +0,0 @@
# Break Escape Game - Refactoring Summary
## Overview
The Break Escape game has been successfully refactored from a single monolithic HTML file (`index.html` - 7544 lines) into a modular structure with separate JavaScript modules and CSS files. This refactoring maintains all existing functionality while making the codebase much more maintainable and organized.
## New File Structure
```
BreakEscape/
├── index_new.html (simplified HTML structure)
├── css/
│ ├── main.css (base styles)
│ ├── notifications.css (notification system styles)
│ ├── panels.css (notes, bluetooth, biometrics panels)
│ ├── inventory.css (inventory system styles)
│ ├── minigames.css (lockpicking, dusting game styles)
│ └── modals.css (password modal, etc.)
├── js/
│ ├── main.js (game initialization and configuration)
│ ├── core/
│ │ ├── game.js (Phaser game setup, preload, create, update)
│ │ ├── player.js (player movement, animation, controls)
│ │ ├── rooms.js (room creation, positioning, management)
│ │ └── pathfinding.js (pathfinding system)
│ ├── systems/
│ │ ├── inventory.js (inventory management)
│ │ ├── notifications.js (notification system)
│ │ ├── notes.js (notes panel system)
│ │ ├── bluetooth.js (bluetooth scanning system)
│ │ ├── biometrics.js (biometrics system)
│ │ ├── interactions.js (object interactions)
│ │ └── debug.js (debug system)
│ ├── ui/
│ │ ├── panels.js (UI panel management)
│ │ └── modals.js (password modal, etc.)
│ └── utils/
│ ├── constants.js (game constants)
│ └── helpers.js (utility functions)
├── assets/ (unchanged)
└── scenarios/ (moved from assets/scenarios/)
```
## What Was Refactored
### 1. **JavaScript Code Separation**
- **Core Game Systems**: Phaser.js game logic, player management, room management
- **Game Systems**: Inventory, notifications, notes, bluetooth, biometrics, interactions
- **UI Components**: Panels, modals, and UI management
- **Utilities**: Constants, helper functions, debug system
### 2. **CSS Organization**
- **Main CSS**: Base styles and game container
- **Component-specific CSS**: Notifications, panels, inventory, minigames, modals
- **Responsive Design**: Mobile-friendly styles maintained
### 3. **Modular Architecture**
- **ES6 Modules**: All JavaScript uses modern import/export syntax
- **Separation of Concerns**: Each module has a specific responsibility
- **Global Variable Management**: Controlled exposure of necessary globals
- **Backwards Compatibility**: Key functions still accessible globally where needed
### 4. **External Dependencies**
- **Preserved**: Phaser.js, EasyStar.js, WebFont.js
- **Scenario Files**: Moved to `/scenarios/` for easier management
## Key Benefits
1. **Maintainability**: Code is now organized by functionality
2. **Readability**: Smaller, focused files are easier to understand
3. **Reusability**: Modular components can be reused or extended
4. **Debugging**: Issues can be isolated to specific modules
5. **Team Development**: Multiple developers can work on different modules
6. **Performance**: Better tree-shaking and loading optimization potential
## Implementation Status
### ✅ Completed
- [x] File structure created
- [x] Constants extracted and organized
- [x] Main game entry point (`main.js`)
- [x] Core game functions (`game.js`)
- [x] Notification system (`notifications.js`)
- [x] Notes system (`notes.js`)
- [x] Debug system (`debug.js`)
- [x] All CSS files organized and separated
- [x] HTML structure simplified
- [x] Scenario files relocated
### 🚧 Stub Implementation (Ready for Full Implementation)
- [ ] Player movement and controls (`player.js`)
- [ ] Room management system (`rooms.js`)
- [ ] Pathfinding system (`pathfinding.js`)
- [ ] Inventory system (`inventory.js`)
- [ ] Bluetooth scanning (`bluetooth.js`)
- [ ] Biometrics system (`biometrics.js`)
- [ ] Object interactions (`interactions.js`)
- [ ] UI panels (`panels.js`)
- [ ] Minigame systems (framework exists, games need implementation)
## Testing Instructions
### 1. **Basic Functionality Test**
```bash
# Start the HTTP server (already running)
python3 -m http.server 8080
# Navigate to: http://localhost:8080/index_new.html
```
### 2. **What Should Work**
- [x] Game loads without errors
- [x] Notification system works
- [x] Notes system works (add note functionality)
- [x] Debug system works (backtick key toggles)
- [x] Basic Phaser.js game initialization
- [x] Player sprite creation and animations
- [x] CSS styling properly applied
### 3. **Debug Controls**
- **`** (backtick): Toggle debug mode
- **Shift + `**: Toggle visual debug mode
- **Ctrl + `**: Cycle through debug levels (1-3)
### 4. **Expected Behavior**
- Game should load and show the player character
- Notifications should appear for system initialization
- Notes panel should be accessible via the button
- All CSS styling should be applied correctly
- Console should show module loading and initialization messages
## Next Steps for Full Implementation
1. **Complete Core Systems**:
- Implement full room management with tilemap loading
- Add complete player movement and pathfinding
- Implement inventory system with drag-and-drop
2. **Game Systems**:
- Complete bluetooth scanning functionality
- Implement biometrics collection system
- Add object interaction system
3. **Minigames**:
- Complete lockpicking minigame implementation
- Add fingerprint dusting minigame
- Implement minigame framework
4. **Testing**:
- Add unit tests for each module
- Test cross-module communication
- Verify all original functionality works
## Backwards Compatibility
The refactored code maintains backwards compatibility by:
- Exposing key functions to `window` object where needed
- Preserving all original CSS class names and IDs
- Maintaining the same HTML structure for UI elements
- Keeping scenario file format unchanged
## Original vs. Refactored
| Aspect | Original | Refactored |
|--------|----------|------------|
| **Files** | 1 HTML file (7544 lines) | 20+ modular files |
| **Maintainability** | Difficult | Easy |
| **Code Organization** | Monolithic | Modular |
| **CSS** | Embedded | Separate files |
| **JavaScript** | Embedded | ES6 modules |
| **Functionality** | ✅ Complete | ✅ Preserved (stubs for completion) |
The refactoring successfully transforms a monolithic codebase into a modern, maintainable structure while preserving all existing functionality.

View File

@@ -1,13 +1,9 @@
{
"scenario_brief": "You are a cyber investigator tasked with uncovering evidence of corporate espionage. Anonymous tips suggest the CEO has been selling company secrets, but you need proof.",
"scenario_brief": "Hi, You are a cyber investigator tasked with uncovering evidence of corporate espionage. Anonymous tips suggest the CEO has been selling company secrets, but you need proof.",
"startRoom": "reception",
"rooms": {
"reception": {
"type": "room_reception",
"locked": true,
"lockType": "key",
"requires": "ceo_office_key",
"difficulty": "easy",
"connections": {
"north": "office1"
},
@@ -41,6 +37,7 @@
"takeable": true,
"locked": true,
"lockType": "bluetooth",
"requires": "bluetooth",
"mac": "00:11:22:33:44:55",
"observations": "A locked tablet device that requires Bluetooth pairing"
},
@@ -57,11 +54,23 @@
"takeable": true,
"inInventory": true,
"observations": "A powerful workstation for cryptographic analysis"
},
{
"type": "key",
"name": "Office Key",
"takeable": true,
"key_id": "office1_key:40,35,38,32,36",
"observations": "A key to access the office areas"
}
]
},
"office1": {
"type": "room_office",
"locked": true,
"lockType": "key",
"requires": "office1_key:40,35,38,32,36",
"difficulty": "easy",
"connections": {
"north": ["office2", "office3"],
"south": "reception"
@@ -119,7 +128,7 @@
"type": "key",
"name": "CEO Office Key",
"takeable": true,
"key_id": "ceo_office_key",
"key_id": "ceo_office_key:28,42,35,31",
"observations": "A spare key to the CEO's office, carelessly left behind"
}
]
@@ -165,7 +174,7 @@
},
"locked": true,
"lockType": "key",
"requires": "ceo_office_key",
"requires": "ceo_office_key:28,42,35,31",
"difficulty": "easy",
"objects": [
{
@@ -180,7 +189,7 @@
"takeable": false,
"locked": true,
"lockType": "key",
"requires": "briefcase_key",
"requires": "briefcase_key:45,32,38,41",
"difficulty": "medium",
"observations": "An expensive leather briefcase with a sturdy lock",
"contents": [
@@ -196,7 +205,7 @@
"type": "key",
"name": "Safe Key",
"takeable": true,
"key_id": "safe_key",
"key_id": "safe_key:52,29,44,37",
"observations": "A heavy-duty safe key hidden behind server equipment"
}
]
@@ -226,7 +235,7 @@
"takeable": false,
"locked": true,
"lockType": "key",
"requires": "safe_key",
"requires": "safe_key:52,29,44,37",
"difficulty": "hard",
"observations": "A well-hidden wall safe behind a painting",
"contents": [
@@ -261,7 +270,7 @@
"type": "key",
"name": "Briefcase Key",
"takeable": true,
"key_id": "briefcase_key",
"key_id": "briefcase_key:45,32,38,41",
"observations": "A small key labeled 'Personal - Do Not Copy'"
}
]

View File

@@ -48,6 +48,54 @@ body {
position: relative;
}
.laptop-screen {
width: 100%;
height: 100%;
background: #1a1a1a;
border-radius: 8px;
display: flex;
flex-direction: column;
overflow: hidden;
}
.title-bar {
background: #2a2a2a;
color: #fff;
padding: 10px 15px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #444;
font-family: 'VT323', monospace;
font-size: 16px;
}
.title-bar .close-btn {
background: #e74c3c;
color: white;
border: none;
border-radius: 3px;
width: 24px;
height: 24px;
cursor: pointer;
font-size: 16px;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
}
.title-bar .close-btn:hover {
background: #c0392b;
}
#cyberchef-container {
flex: 1;
width: 100%;
height: 100%;
overflow: hidden;
}
#cyberchef-frame {
width: 100%;
height: 100%;

View File

@@ -353,6 +353,16 @@ export function createRoom(roomId, roomData, position) {
console.log(`Creating room ${roomId} of type ${roomData.type}`);
const gameScenario = window.gameScenario;
// Safety check: if gameRef is null, use window.game as fallback
if (!gameRef && window.game) {
console.log('gameRef was null, using window.game as fallback');
gameRef = window.game;
}
if (!gameRef) {
throw new Error('Game reference is null - cannot create room. This should not happen if called after game initialization.');
}
const map = gameRef.make.tilemap({ key: roomData.type });
const tilesets = [];

View File

@@ -3,7 +3,7 @@ import { preload, create, update } from './core/game.js?v=32';
import { initializeNotifications } from './systems/notifications.js?v=7';
// Bluetooth scanner is now handled as a minigame
// Biometrics is now handled as a minigame
import { startLockpickingMinigame } from './systems/interactions.js?v=23';
import { startLockpickingMinigame } from './systems/minigame-starters.js?v=1';
import { initializeDebugSystem } from './systems/debug.js?v=7';
import { initializeUI } from './ui/panels.js?v=9';
import { initializeModals } from './ui/modals.js?v=7';

View File

@@ -8,6 +8,9 @@ export class LockpickingMinigamePhaser extends MinigameScene {
// Ensure params is an object
params = params || {};
console.log('DEBUG: Lockpicking minigame constructor received params:', params);
console.log('DEBUG: predefinedPinHeights from params:', params.predefinedPinHeights);
this.lockable = params.lockable || 'default-lock';
this.lockId = params.lockId || 'default_lock';
this.difficulty = params.difficulty || 'medium';
@@ -2836,6 +2839,12 @@ export class LockpickingMinigamePhaser extends MinigameScene {
// Check if predefined pin heights were passed
const predefinedPinHeights = this.params?.predefinedPinHeights;
console.log(`DEBUG: Lockpicking minigame received parameters:`);
console.log(` - pinCount: ${this.pinCount}`);
console.log(` - this.params:`, this.params);
console.log(` - predefinedPinHeights: [${predefinedPinHeights ? predefinedPinHeights.join(', ') : 'none'}]`);
console.log(` - savedPinHeights: [${savedPinHeights ? savedPinHeights.join(', ') : 'none'}]`);
for (let i = 0; i < this.pinCount; i++) {
const pinX = 100 + margin + i * pinSpacing;
const pinY = 200;
@@ -2846,15 +2855,17 @@ export class LockpickingMinigamePhaser extends MinigameScene {
// Use predefined configuration
keyPinLength = predefinedPinHeights[i];
driverPinLength = 75 - keyPinLength; // Total height is 75
console.log(`Using predefined pin height for pin ${i}: ${keyPinLength}`);
console.log(`✓ Pin ${i}: Using predefined pin height: ${keyPinLength} (driver: ${driverPinLength})`);
} else if (savedPinHeights && savedPinHeights[i] !== undefined) {
// Use saved configuration
keyPinLength = savedPinHeights[i];
driverPinLength = 75 - keyPinLength; // Total height is 75
console.log(`✓ Pin ${i}: Using saved pin height: ${keyPinLength} (driver: ${driverPinLength})`);
} else {
// Generate random pin lengths that add up to 75 (total height - 25% increase from 60)
keyPinLength = 25 + Math.random() * 37.5; // 25-62.5 (25% increase)
driverPinLength = 75 - keyPinLength; // Remaining to make 75 total
console.log(`⚠ Pin ${i}: Generated random pin height: ${keyPinLength} (driver: ${driverPinLength})`);
}
const pin = {

168
js/systems/biometrics.js Normal file
View File

@@ -0,0 +1,168 @@
/**
* BIOMETRICS SYSTEM
* =================
*
* Handles fingerprint collection and biometric scanning functionality.
* Includes dusting minigame integration and biometric sample management.
*/
import { INTERACTION_RANGE_SQ } from '../utils/constants.js';
// Fingerprint collection function
export function collectFingerprint(item) {
if (!item.scenarioData?.hasFingerprint) {
window.gameAlert("No fingerprints found on this surface.", 'info', 'No Fingerprints', 3000);
return null;
}
// Start the dusting minigame
startDustingMinigame(item);
return true;
}
// Handle biometric scanner interaction
export function handleBiometricScan(sprite) {
const player = window.player;
if (!player) return;
// Check if player is in range
const dx = player.x - sprite.x;
const dy = player.y - sprite.y;
const distanceSq = dx * dx + dy * dy;
if (distanceSq > INTERACTION_RANGE_SQ) {
window.gameAlert('You need to be closer to use the biometric scanner.', 'warning', 'Too Far', 3000);
return;
}
// Show biometric authentication interface
window.gameAlert('Place your finger on the scanner...', 'info', 'Biometric Scan', 2000);
// Simulate biometric scan process
setTimeout(() => {
// For now, just show a message - can be enhanced with actual authentication logic
window.gameAlert('Biometric scan complete.', 'success', 'Scan Complete', 3000);
}, 2000);
}
// Start fingerprint dusting minigame
export function startDustingMinigame(item) {
console.log('Starting dusting minigame for item:', item);
// Check if MinigameFramework is available
if (!window.MinigameFramework) {
console.error('MinigameFramework not available - using fallback');
// Fallback to simple collection
window.gameAlert('Collecting fingerprint sample...', 'info', 'Dusting', 2000);
setTimeout(() => {
const quality = 0.7 + Math.random() * 0.3;
const rating = quality >= 0.9 ? 'Excellent' :
quality >= 0.8 ? 'Good' :
quality >= 0.7 ? 'Fair' : 'Poor';
if (!window.gameState) {
window.gameState = { biometricSamples: [] };
}
if (!window.gameState.biometricSamples) {
window.gameState.biometricSamples = [];
}
const sample = {
id: `sample_${Date.now()}`,
type: 'fingerprint',
owner: item.scenarioData.fingerprintOwner || 'Unknown',
quality: quality,
data: generateFingerprintData(item),
timestamp: Date.now()
};
window.gameState.biometricSamples.push(sample);
if (item.scenarioData) {
item.scenarioData.hasFingerprint = false;
}
if (window.updateBiometricsPanel) {
window.updateBiometricsPanel();
}
if (window.updateBiometricsCount) {
window.updateBiometricsCount();
}
window.gameAlert(`Collected ${sample.owner}'s fingerprint sample (${rating} quality)`, 'success', 'Sample Acquired', 4000);
}, 2000);
return;
}
// Initialize the framework if not already done
if (!window.MinigameFramework.mainGameScene) {
window.MinigameFramework.init(window.game);
}
// Add scene reference to item for the minigame
item.scene = window.game;
// Start the dusting minigame
window.MinigameFramework.startMinigame('dusting', null, {
item: item,
scene: item.scene,
onComplete: (success, result) => {
if (success) {
console.log('DUSTING SUCCESS', result);
// Add fingerprint to gameState
if (!window.gameState) {
window.gameState = { biometricSamples: [] };
}
if (!window.gameState.biometricSamples) {
window.gameState.biometricSamples = [];
}
const sample = {
id: generateFingerprintData(item),
type: 'fingerprint',
owner: item.scenarioData.fingerprintOwner || 'Unknown',
quality: result.quality, // Quality between 0.7 and ~1.0
data: generateFingerprintData(item),
timestamp: Date.now()
};
window.gameState.biometricSamples.push(sample);
// Mark item as collected
if (item.scenarioData) {
item.scenarioData.hasFingerprint = false;
}
// Update the biometrics panel and count
if (window.updateBiometricsPanel) {
window.updateBiometricsPanel();
}
if (window.updateBiometricsCount) {
window.updateBiometricsCount();
}
// Show notification
window.gameAlert(`Collected ${sample.owner}'s fingerprint sample (${result.rating} quality)`, 'success', 'Sample Acquired', 4000);
} else {
console.log('DUSTING FAILED');
window.gameAlert(`Failed to collect the fingerprint sample.`, 'error', 'Dusting Failed', 4000);
}
}
});
}
// Generate fingerprint data
export function generateFingerprintData(item) {
const owner = item.scenarioData?.fingerprintOwner || 'Unknown';
const timestamp = Date.now();
return `${owner}_${timestamp}_${Math.random().toString(36).substr(2, 9)}`;
}
// Export for global access
window.collectFingerprint = collectFingerprint;
window.handleBiometricScan = handleBiometricScan;
window.startDustingMinigame = startDustingMinigame;
window.generateFingerprintData = generateFingerprintData;

View File

@@ -22,8 +22,22 @@ export function initializeCollision(gameInstance, roomsRef) {
export function createWallCollisionBoxes(wallLayer, roomId, position) {
console.log(`Creating wall collision boxes for room ${roomId}`);
// Use window.rooms to ensure we see the latest state
const room = window.rooms ? window.rooms[roomId] : null;
if (!room) {
console.error(`Room ${roomId} not found in window.rooms, cannot create collision boxes`);
return;
}
// Ensure we have a valid game reference
const game = gameRef || window.game;
if (!game) {
console.error('No game reference available, cannot create collision boxes');
return;
}
// Get room dimensions from the map
const map = rooms[roomId].map;
const map = room.map;
const roomWidth = map.widthInPixels;
const roomHeight = map.heightInPixels;
@@ -45,7 +59,7 @@ export function createWallCollisionBoxes(wallLayer, roomId, position) {
// North wall (top 2 rows) - collision on south edge
if (tileY < 2) {
const collisionBox = gameRef.add.rectangle(
const collisionBox = game.add.rectangle(
worldX + TILE_SIZE / 2,
worldY + TILE_SIZE - 4, // 4px from south edge
TILE_SIZE,
@@ -58,7 +72,7 @@ export function createWallCollisionBoxes(wallLayer, roomId, position) {
// South wall (bottom row) - collision on south edge
if (tileY === map.height - 1) {
const collisionBox = gameRef.add.rectangle(
const collisionBox = game.add.rectangle(
worldX + TILE_SIZE / 2,
worldY + TILE_SIZE - 4, // 4px from south edge
TILE_SIZE,
@@ -71,7 +85,7 @@ export function createWallCollisionBoxes(wallLayer, roomId, position) {
// West wall (left column) - collision on east edge
if (tileX === 0) {
const collisionBox = gameRef.add.rectangle(
const collisionBox = game.add.rectangle(
worldX + TILE_SIZE - 4, // 4px from east edge
worldY + TILE_SIZE / 2,
8, // Thicker collision box
@@ -84,7 +98,7 @@ export function createWallCollisionBoxes(wallLayer, roomId, position) {
// East wall (right column) - collision on west edge
if (tileX === map.width - 1) {
const collisionBox = gameRef.add.rectangle(
const collisionBox = game.add.rectangle(
worldX + 4, // 4px from west edge
worldY + TILE_SIZE / 2,
8, // Thicker collision box
@@ -98,10 +112,10 @@ export function createWallCollisionBoxes(wallLayer, roomId, position) {
// Set up all collision boxes for this tile
tileCollisionBoxes.forEach(collisionBox => {
collisionBox.setVisible(false);
gameRef.physics.add.existing(collisionBox, true);
game.physics.add.existing(collisionBox, true);
// Wait for the next frame to ensure body is fully initialized
gameRef.time.delayedCall(0, () => {
game.time.delayedCall(0, () => {
if (collisionBox.body) {
// Use direct property assignment (fallback method)
collisionBox.body.immovable = true;
@@ -118,22 +132,22 @@ export function createWallCollisionBoxes(wallLayer, roomId, position) {
const player = window.player;
if (player && player.body) {
collisionBoxes.forEach(collisionBox => {
gameRef.physics.add.collider(player, collisionBox);
game.physics.add.collider(player, collisionBox);
});
console.log(`Added ${collisionBoxes.length} wall collision boxes for room ${roomId}`);
console.log(`Added ${collisionBoxes.length} wall collision boxes for room ${roomId} with player collision`);
} else {
console.warn(`Player not ready for room ${roomId}, storing ${collisionBoxes.length} collision boxes for later`);
if (!rooms[roomId].pendingWallCollisionBoxes) {
rooms[roomId].pendingWallCollisionBoxes = [];
if (!room.pendingWallCollisionBoxes) {
room.pendingWallCollisionBoxes = [];
}
rooms[roomId].pendingWallCollisionBoxes.push(...collisionBoxes);
room.pendingWallCollisionBoxes.push(...collisionBoxes);
}
// Store collision boxes in room for cleanup
if (!rooms[roomId].wallCollisionBoxes) {
rooms[roomId].wallCollisionBoxes = [];
if (!room.wallCollisionBoxes) {
room.wallCollisionBoxes = [];
}
rooms[roomId].wallCollisionBoxes.push(...collisionBoxes);
room.wallCollisionBoxes.push(...collisionBoxes);
}
// Function to remove wall tiles under doors
@@ -148,8 +162,15 @@ export function removeTilesUnderDoor(wallLayer, roomId, position) {
return;
}
// Ensure we have a valid game reference
const game = gameRef || window.game;
if (!game) {
console.error('No game reference available, cannot remove tiles under door');
return;
}
// Get room dimensions for door positioning (same as door sprite creation)
const map = gameRef.cache.tilemap.get(roomData.type);
const map = game.cache.tilemap.get(roomData.type);
let roomWidth = 800, roomHeight = 600; // fallback
if (map) {
@@ -349,7 +370,8 @@ export function removeTilesUnderDoor(wallLayer, roomId, position) {
export function removeWallTilesForDoorInRoom(roomId, fromRoomId, direction, doorWorldX, doorWorldY) {
console.log(`Removing wall tiles in room ${roomId} for door from ${fromRoomId} (${direction}) at world position (${doorWorldX}, ${doorWorldY})`);
const room = rooms[roomId];
// Use window.rooms to ensure we see the latest state
const room = window.rooms ? window.rooms[roomId] : null;
if (!room || !room.wallsLayers || room.wallsLayers.length === 0) {
console.log(`No wall layers found for room ${roomId}`);
return;

View File

@@ -7,7 +7,7 @@
*/
import { TILE_SIZE } from '../utils/constants.js';
import { handleUnlock, getLockRequirementsForDoor, startLockpickingMinigame, startKeySelectionMinigame } from './interactions.js';
import { handleUnlock } from './unlock-system.js';
let gameRef = null;
let rooms = null;
@@ -318,135 +318,14 @@ function handleDoorInteraction(doorSprite) {
if (props.locked) {
console.log(`Door is locked. Type: ${props.lockType}, Requires: ${props.requires}`);
// Use the door properties directly since we already have the lock information
handleDoorUnlockDirect(doorSprite, props);
// Use unified unlock system for consistent behavior with items
handleUnlock(doorSprite, 'door');
} else {
openDoor(doorSprite);
}
}
// Function to handle door unlocking directly using door properties
function handleDoorUnlockDirect(doorSprite, props) {
console.log('DOOR UNLOCK ATTEMPT (direct)');
switch(props.lockType) {
case 'key':
const requiredKey = props.requires;
console.log('KEY REQUIRED', requiredKey);
// Get all keys from player's inventory
const playerKeys = window.inventory.items.filter(item =>
item && item.scenarioData &&
item.scenarioData.type === 'key'
);
if (playerKeys.length > 0) {
// Show key selection interface
startKeySelectionMinigame(doorSprite, 'door', playerKeys, requiredKey);
} else {
// Check for lockpick kit
const hasLockpick = window.inventory.items.some(item =>
item && item.scenarioData &&
item.scenarioData.type === 'lockpick'
);
if (hasLockpick) {
console.log('LOCKPICK AVAILABLE');
if (confirm("Would you like to attempt picking this lock?")) {
let difficulty = 'medium';
console.log('STARTING LOCKPICK MINIGAME', { difficulty });
startLockpickingMinigame(doorSprite, window.game, difficulty, (success) => {
if (success) {
unlockDoor(doorSprite);
window.gameAlert(`Successfully picked the lock!`, 'success', 'Lock Picked', 4000);
} else {
console.log('LOCKPICK FAILED');
window.gameAlert('Failed to pick the lock. Try again.', 'error', 'Pick Failed', 3000);
}
});
}
} else {
console.log('NO KEYS OR LOCKPICK AVAILABLE');
window.gameAlert(`Requires key: ${requiredKey}`, 'error', 'Locked', 4000);
}
}
break;
case 'pin':
console.log('PIN CODE REQUESTED');
const pinInput = prompt(`Enter PIN code:`);
if (pinInput === props.requires) {
unlockDoor(doorSprite);
console.log('PIN CODE SUCCESS');
window.gameAlert(`Correct PIN! The door is now unlocked.`, 'success', 'PIN Accepted', 4000);
} else if (pinInput !== null) {
console.log('PIN CODE FAIL');
window.gameAlert("Incorrect PIN code.", 'error', 'PIN Rejected', 3000);
}
break;
case 'password':
console.log('PASSWORD REQUESTED');
if (window.showPasswordModal) {
window.showPasswordModal(function(passwordInput) {
if (passwordInput === props.requires) {
unlockDoor(doorSprite);
console.log('PASSWORD SUCCESS');
window.gameAlert(`Correct password! The door is now unlocked.`, 'success', 'Password Accepted', 4000);
} else if (passwordInput !== null) {
console.log('PASSWORD FAIL');
window.gameAlert("Incorrect password.", 'error', 'Password Rejected', 3000);
}
});
} else {
// Fallback to prompt
const passwordInput = prompt(`Enter password:`);
if (passwordInput === props.requires) {
unlockDoor(doorSprite);
console.log('PASSWORD SUCCESS');
window.gameAlert(`Correct password! The door is now unlocked.`, 'success', 'Password Accepted', 4000);
} else if (passwordInput !== null) {
console.log('PASSWORD FAIL');
window.gameAlert("Incorrect password.", 'error', 'Password Rejected', 3000);
}
}
break;
case 'biometric':
console.log('BIOMETRIC REQUIRED');
const hasBiometric = window.gameState?.biometricSamples?.length > 0;
if (hasBiometric) {
if (confirm("Use biometric authentication?")) {
unlockDoor(doorSprite);
window.gameAlert(`Biometric authentication successful!`, 'success', 'Access Granted', 4000);
}
} else {
window.gameAlert(`Biometric authentication required.`, 'error', 'Access Denied', 4000);
}
break;
case 'bluetooth':
console.log('BLUETOOTH REQUIRED');
const hasBluetooth = window.gameState?.bluetoothDevices?.length > 0;
if (hasBluetooth) {
if (confirm("Use Bluetooth device?")) {
unlockDoor(doorSprite);
window.gameAlert(`Bluetooth authentication successful!`, 'success', 'Access Granted', 4000);
}
} else {
window.gameAlert(`Bluetooth device required.`, 'error', 'Access Denied', 4000);
}
break;
default:
console.log('UNKNOWN LOCK TYPE:', props.lockType);
window.gameAlert(`Unknown lock type: ${props.lockType}`, 'error', 'Locked', 4000);
break;
}
}
// Function to unlock a door (called by interactions.js after successful unlock)
// Function to unlock a door (called after successful unlock)
function unlockDoor(doorSprite) {
const props = doorSprite.doorProperties;
console.log(`Unlocking door: ${props.roomId} -> ${props.connectedRoom}`);
@@ -465,40 +344,103 @@ function openDoor(doorSprite) {
const props = doorSprite.doorProperties;
console.log(`Opening door: ${props.roomId} -> ${props.connectedRoom}`);
// Load the connected room if it doesn't exist
if (!rooms[props.connectedRoom]) {
console.log(`Loading room: ${props.connectedRoom}`);
// Import the loadRoom function from rooms.js
if (window.loadRoom) {
window.loadRoom(props.connectedRoom);
// Wait for game scene to be ready before proceeding
// This prevents crashes when called immediately after minigame cleanup
const finishOpeningDoor = () => {
// Load the connected room if it doesn't exist
// Use window.rooms to ensure we see the latest state
const needsLoading = !window.rooms || !window.rooms[props.connectedRoom];
if (needsLoading) {
console.log(`Loading room: ${props.connectedRoom}`);
if (window.loadRoom) {
window.loadRoom(props.connectedRoom);
}
}
// Process door sprites after room is ready
const processRoomDoors = () => {
console.log('Processing room doors after load');
// Remove wall tiles from the connected room under the door position
if (window.removeWallTilesForDoorInRoom) {
window.removeWallTilesForDoorInRoom(props.connectedRoom, props.roomId, props.direction, doorSprite.x, doorSprite.y);
}
// Remove the matching door sprite from the connected room
removeMatchingDoorSprite(props.connectedRoom, props.roomId, props.direction, doorSprite.x, doorSprite.y);
// Create animated door sprite on the opposite side
createAnimatedDoorOnOppositeSide(props.connectedRoom, props.roomId, props.direction, doorSprite.x, doorSprite.y);
// Remove the door sprite
doorSprite.destroy();
if (doorSprite.interactionZone) {
doorSprite.interactionZone.destroy();
}
props.open = true;
};
// If we just loaded the room, wait for it to be fully created
// before manipulating its door sprites
if (needsLoading) {
console.log('Room just loaded, waiting for creation to complete...');
// Poll until the room actually exists in window.rooms
let attempts = 0;
const maxAttempts = 20; // Max 1 second (20 * 50ms)
const waitForRoom = () => {
attempts++;
// Check if room exists AND is fully initialized (has doorSprites array)
const room = window.rooms ? window.rooms[props.connectedRoom] : null;
const isFullyInitialized = room && room.doorSprites !== undefined;
if (isFullyInitialized) {
console.log(`Room ${props.connectedRoom} is now fully initialized (after ${attempts * 50}ms)`);
processRoomDoors();
} else if (attempts >= maxAttempts) {
console.error(`Room ${props.connectedRoom} failed to fully initialize after ${attempts * 50}ms`);
console.error('Room state:', room);
// Try anyway as a last resort
processRoomDoors();
} else {
const roomExists = room !== null;
const hasDoorSprites = room && room.doorSprites !== undefined;
console.log(`Waiting for room ${props.connectedRoom}... (attempt ${attempts}), exists: ${roomExists}, doorSprites: ${hasDoorSprites}`);
setTimeout(waitForRoom, 50);
}
};
waitForRoom();
} else {
console.log('Room already exists, processing doors immediately');
processRoomDoors();
}
};
// Check if game scene is ready using the global window.game reference
// This is critical because rooms.js uses its own gameRef that must also be ready
if (window.game && window.game.scene && window.game.scene.isActive('default')) {
console.log('Game scene ready, opening door immediately');
finishOpeningDoor();
} else {
console.log('Game scene not ready, waiting...');
const waitForGameReady = () => {
if (window.game && window.game.scene && window.game.scene.isActive('default')) {
console.log('Game scene now ready, opening door');
finishOpeningDoor();
} else {
setTimeout(waitForGameReady, 50);
}
};
waitForGameReady();
}
// Remove wall tiles from the connected room under the door position
if (window.removeWallTilesForDoorInRoom) {
window.removeWallTilesForDoorInRoom(props.connectedRoom, props.roomId, props.direction, doorSprite.x, doorSprite.y);
}
// Remove the matching door sprite from the connected room
removeMatchingDoorSprite(props.connectedRoom, props.roomId, props.direction, doorSprite.x, doorSprite.y);
// Create animated door sprite on the opposite side
createAnimatedDoorOnOppositeSide(props.connectedRoom, props.roomId, props.direction, doorSprite.x, doorSprite.y);
// Remove the door sprite
doorSprite.destroy();
if (doorSprite.interactionZone) {
doorSprite.interactionZone.destroy();
}
props.open = true;
}
// Function to remove the matching door sprite from the connected room
function removeMatchingDoorSprite(roomId, fromRoomId, direction, doorWorldX, doorWorldY) {
console.log(`Removing matching door sprite in room ${roomId} for door from ${fromRoomId} (${direction})`);
const room = rooms[roomId];
// Use window.rooms to ensure we see the latest state
const room = window.rooms ? window.rooms[roomId] : null;
if (!room || !room.doorSprites) {
console.log(`No door sprites found for room ${roomId}`);
return;
@@ -531,7 +473,8 @@ function removeMatchingDoorSprite(roomId, fromRoomId, direction, doorWorldX, doo
function createAnimatedDoorOnOppositeSide(roomId, fromRoomId, direction, doorWorldX, doorWorldY) {
console.log(`Creating animated door on opposite side in room ${roomId} for door from ${fromRoomId} (${direction}) at world position (${doorWorldX}, ${doorWorldY})`);
const room = rooms[roomId];
// Use window.rooms to ensure we see the latest state
const room = window.rooms ? window.rooms[roomId] : null;
if (!room) {
console.log(`Room ${roomId} not found, cannot create animated door`);
return;
@@ -788,10 +731,150 @@ function boundsOverlap(rect1, rect2) {
rect1.y + rect1.height > rect2.y;
}
// Process all door collisions
export function processAllDoorCollisions() {
console.log('Processing door collisions');
Object.entries(rooms).forEach(([roomId, room]) => {
if (room.doorsLayer) {
const doorTiles = room.doorsLayer.getTilesWithin()
.filter(tile => tile.index !== -1);
// Find all rooms that overlap with this room
Object.entries(rooms).forEach(([otherId, otherRoom]) => {
if (roomsOverlap(room.position, otherRoom.position)) {
otherRoom.wallsLayers.forEach(wallLayer => {
processDoorCollisions(doorTiles, wallLayer, room.doorsLayer);
});
}
});
}
});
}
function processDoorCollisions(doorTiles, wallLayer, doorsLayer) {
doorTiles.forEach(doorTile => {
// Convert door tile coordinates to world coordinates
const worldX = doorsLayer.x + (doorTile.x * doorsLayer.tilemap.tileWidth);
const worldY = doorsLayer.y + (doorTile.y * doorsLayer.tilemap.tileHeight);
// Convert world coordinates back to the wall layer's local coordinates
const wallX = Math.floor((worldX - wallLayer.x) / wallLayer.tilemap.tileWidth);
const wallY = Math.floor((worldY - wallLayer.y) / wallLayer.tilemap.tileHeight);
const wallTile = wallLayer.getTileAt(wallX, wallY);
if (wallTile) {
if (doorTile.properties?.locked) {
wallTile.setCollision(true);
} else {
wallTile.setCollision(false);
}
}
});
}
function roomsOverlap(pos1, pos2) {
// Add some tolerance for overlap detection
const OVERLAP_TOLERANCE = 48; // One tile width
const ROOM_WIDTH = 800;
const ROOM_HEIGHT = 600;
return !(pos1.x + ROOM_WIDTH - OVERLAP_TOLERANCE < pos2.x ||
pos1.x > pos2.x + ROOM_WIDTH - OVERLAP_TOLERANCE ||
pos1.y + ROOM_HEIGHT - OVERLAP_TOLERANCE < pos2.y ||
pos1.y > pos2.y + ROOM_HEIGHT - OVERLAP_TOLERANCE);
}
// Store door zones globally so we can manage them
window.doorZones = window.doorZones || new Map();
export function setupDoorOverlapChecks() {
if (!gameRef) {
console.error('Game reference not set in doors.js');
return;
}
const DOOR_INTERACTION_RANGE = 2 * TILE_SIZE;
// Clear existing door zones
if (window.doorZones) {
window.doorZones.forEach(zone => {
if (zone && zone.destroy) {
zone.destroy();
}
});
window.doorZones.clear();
}
Object.entries(rooms).forEach(([roomId, room]) => {
if (!room.doorSprites) return;
const doorSprites = room.doorSprites;
// Get room data to check if this room should be locked
const gameScenario = window.gameScenario;
const roomData = gameScenario?.rooms?.[roomId];
doorSprites.forEach(doorSprite => {
const zone = gameRef.add.zone(doorSprite.x, doorSprite.y, TILE_SIZE, TILE_SIZE * 2);
zone.setInteractive({ useHandCursor: true });
// Store zone reference for later management
const zoneKey = `${roomId}_${doorSprite.doorProperties.topTile.x}_${doorSprite.doorProperties.topTile.y}`;
window.doorZones.set(zoneKey, zone);
zone.on('pointerdown', () => {
console.log('Door clicked:', { doorSprite, room });
console.log('Door properties:', doorSprite.doorProperties);
console.log('Door open state:', doorSprite.doorProperties?.open);
console.log('Door sprite position:', { x: doorSprite.x, y: doorSprite.y });
const player = window.player;
if (!player) return;
const distance = Phaser.Math.Distance.Between(
player.x, player.y,
doorSprite.x, doorSprite.y
);
if (distance <= DOOR_INTERACTION_RANGE) {
handleDoorInteraction(doorSprite);
} else {
console.log('DOOR TOO FAR TO INTERACT');
}
});
gameRef.physics.world.enable(zone);
});
});
}
// Function to update door zone visibility based on room visibility
export function updateDoorZoneVisibility() {
if (!window.doorZones || !gameRef) return;
const discoveredRooms = window.discoveredRooms || new Set();
window.doorZones.forEach((zone, zoneKey) => {
const [roomId] = zoneKey.split('_');
// Show zone if this room is discovered
if (discoveredRooms.has(roomId)) {
zone.setVisible(true);
zone.setInteractive({ useHandCursor: true });
} else {
zone.setVisible(false);
zone.setInteractive(false);
}
});
}
// Export for global access
window.updateDoorSpritesVisibility = updateDoorSpritesVisibility;
window.checkDoorTransitions = checkDoorTransitions;
window.setupDoorOverlapChecks = setupDoorOverlapChecks;
window.updateDoorZoneVisibility = updateDoorZoneVisibility;
window.processAllDoorCollisions = processAllDoorCollisions;
// Export functions for use by other modules
export { unlockDoor };

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,14 @@
// Inventory System
// Handles inventory management and display
import { rooms } from '../core/rooms.js';
// Helper function to create a unique identifier for an item
export function createItemIdentifier(scenarioData) {
if (!scenarioData) return 'unknown';
return `${scenarioData.type}_${scenarioData.name || 'unnamed'}`;
}
// Initialize the inventory system
export function initializeInventory() {
console.log('Inventory system initialized');
@@ -78,7 +86,7 @@ function createInventorySprite(itemData) {
}
}
function addToInventory(sprite) {
export function addToInventory(sprite) {
if (!sprite || !sprite.scenarioData) {
console.warn('Invalid sprite for inventory');
return false;
@@ -88,13 +96,14 @@ function addToInventory(sprite) {
console.log("Adding to inventory:", {
objectId: sprite.objectId,
name: sprite.name,
type: sprite.scenarioData?.type
type: sprite.scenarioData?.type,
currentRoom: window.currentPlayerRoom
});
// Check if the item is already in the inventory
const itemIdentifier = `${sprite.scenarioData.type}_${sprite.scenarioData.name || 'unnamed'}`;
const itemIdentifier = createItemIdentifier(sprite.scenarioData);
const isAlreadyInInventory = window.inventory.items.some(item =>
item && `${item.scenarioData.type}_${item.scenarioData.name || 'unnamed'}` === itemIdentifier
item && createItemIdentifier(item.scenarioData) === itemIdentifier
);
if (isAlreadyInInventory) {
@@ -102,6 +111,18 @@ function addToInventory(sprite) {
return false;
}
// Remove from room if it exists
if (window.currentPlayerRoom && rooms[window.currentPlayerRoom] && rooms[window.currentPlayerRoom].objects) {
if (rooms[window.currentPlayerRoom].objects[sprite.objectId]) {
const roomObj = rooms[window.currentPlayerRoom].objects[sprite.objectId];
roomObj.setVisible(false);
roomObj.active = false;
console.log(`Removed object ${sprite.objectId} from room`);
}
}
sprite.setVisible(false);
// Create a new slot for this item
const inventoryContainer = document.getElementById('inventory-container');
if (!inventoryContainer) {
@@ -217,7 +238,11 @@ function addNotepadToInventory() {
const notepadSprite = {
name: 'notes5',
objectId: 'notepad_inventory',
scenarioData: notepadData
scenarioData: notepadData,
setVisible: function(visible) {
// For inventory items, visibility is handled by DOM
return this;
}
};
// Add to inventory
@@ -240,8 +265,49 @@ function addNotepadToInventory() {
}
}
// Remove item from inventory
export function removeFromInventory(item) {
try {
// Find the item in the inventory array
const itemIndex = window.inventory.items.indexOf(item);
if (itemIndex === -1) return false;
// Remove from array
window.inventory.items.splice(itemIndex, 1);
// Remove the entire slot from DOM
const slot = item.parentElement;
if (slot && slot.classList.contains('inventory-slot')) {
slot.remove();
}
// Hide bluetooth toggle if we dropped the bluetooth scanner
if (item.scenarioData.type === "bluetooth_scanner") {
const bluetoothToggle = document.getElementById('bluetooth-toggle');
if (bluetoothToggle) {
bluetoothToggle.style.display = 'none';
}
}
// Hide biometrics toggle if we dropped the fingerprint kit
if (item.scenarioData.type === "fingerprint_kit") {
const biometricsToggle = document.getElementById('biometrics-toggle');
if (biometricsToggle) {
biometricsToggle.style.display = 'none';
}
}
return true;
} catch (error) {
console.error('Error removing from inventory:', error);
return false;
}
}
// Export for global access
window.initializeInventory = initializeInventory;
window.processInitialInventoryItems = processInitialInventoryItems;
window.addToInventory = addToInventory;
window.addNotepadToInventory = addNotepadToInventory;
window.removeFromInventory = removeFromInventory;
window.addNotepadToInventory = addNotepadToInventory;
window.createItemIdentifier = createItemIdentifier;

View File

@@ -0,0 +1,306 @@
/**
* KEY-LOCK SYSTEM
* ===============
*
* Manages the relationship between keys and locks in the game.
* Each key is mapped to a specific lock based on scenario definitions.
* This ensures consistent lock configurations and key cuts throughout the game.
*/
// Global key-lock mapping system
// This ensures each key matches exactly one lock in the game
window.keyLockMappings = window.keyLockMappings || {};
// Predefined lock configurations for the game
// Each lock has a unique ID and pin configuration
const PREDEFINED_LOCK_CONFIGS = {
'ceo_briefcase_lock': {
id: 'ceo_briefcase_lock',
pinCount: 4,
pinHeights: [32, 28, 35, 30], // Specific pin heights for CEO briefcase
difficulty: 'medium'
},
'office_drawer_lock': {
id: 'office_drawer_lock',
pinCount: 3,
pinHeights: [25, 30, 28],
difficulty: 'easy'
},
'server_room_lock': {
id: 'server_room_lock',
pinCount: 5,
pinHeights: [40, 35, 38, 32, 36],
difficulty: 'hard'
},
'storage_cabinet_lock': {
id: 'storage_cabinet_lock',
pinCount: 4,
pinHeights: [29, 33, 27, 31],
difficulty: 'medium'
}
};
// Function to assign keys to locks based on scenario definitions
function assignKeysToLocks() {
console.log('Assigning keys to locks based on scenario definitions...');
// Get all keys from inventory
const playerKeys = window.inventory?.items?.filter(item =>
item && item.scenarioData &&
item.scenarioData.type === 'key'
) || [];
console.log(`Found ${playerKeys.length} keys in inventory`);
// Get all rooms from the current scenario
const rooms = window.gameState?.scenario?.rooms || {};
console.log(`Found ${Object.keys(rooms).length} rooms in scenario`);
// Find all locks that require keys
const keyLocks = [];
Object.entries(rooms).forEach(([roomId, roomData]) => {
if (roomData.locked && roomData.lockType === 'key' && roomData.requires) {
keyLocks.push({
roomId: roomId,
requiredKeyId: roomData.requires,
roomName: roomData.type || roomId
});
}
// Also check objects within rooms for key locks
if (roomData.objects) {
roomData.objects.forEach((obj, objIndex) => {
if (obj.locked && obj.lockType === 'key' && obj.requires) {
keyLocks.push({
roomId: roomId,
objectIndex: objIndex,
requiredKeyId: obj.requires,
objectName: obj.name || obj.type
});
}
});
}
});
console.log(`Found ${keyLocks.length} key locks in scenario:`, keyLocks);
// Create mappings based on scenario definitions
keyLocks.forEach(lock => {
const keyId = lock.requiredKeyId;
// Find the key in player inventory
const key = playerKeys.find(k => k.scenarioData.key_id === keyId);
if (key) {
// Create a lock configuration for this specific lock
const lockConfig = {
id: `${lock.roomId}_${lock.objectIndex !== undefined ? `obj_${lock.objectIndex}` : 'room'}`,
pinCount: 4, // Default pin count
pinHeights: generatePinHeightsForLock(lock.roomId, keyId), // Generate consistent pin heights
difficulty: 'medium'
};
// Store the mapping
window.keyLockMappings[keyId] = {
lockId: lockConfig.id,
lockConfig: lockConfig,
keyName: key.scenarioData.name,
roomId: lock.roomId,
objectIndex: lock.objectIndex,
lockName: lock.objectName || lock.roomName
};
console.log(`Assigned key "${key.scenarioData.name}" (${keyId}) to lock in ${lock.roomName}${lock.objectName ? ` - ${lock.objectName}` : ''}`);
} else {
console.warn(`Key "${keyId}" required by lock in ${lock.roomName}${lock.objectName ? ` - ${lock.objectName}` : ''} not found in inventory`);
}
});
console.log('Key-lock mappings based on scenario:', window.keyLockMappings);
}
// Function to generate consistent pin heights for a lock based on room and key
function generatePinHeightsForLock(roomId, keyId) {
// Use a deterministic seed based on room and key IDs
const seed = (roomId + keyId).split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
const random = (min, max) => {
const x = Math.sin(seed++) * 10000;
return Math.floor((x - Math.floor(x)) * (max - min + 1)) + min;
};
const pinHeights = [];
for (let i = 0; i < 4; i++) {
pinHeights.push(25 + random(0, 37)); // 25-62 range
}
return pinHeights;
}
// Function to check if a key matches a specific lock
function doesKeyMatchLock(keyId, lockId) {
if (!window.keyLockMappings || !window.keyLockMappings[keyId]) {
return false;
}
const mapping = window.keyLockMappings[keyId];
return mapping.lockId === lockId;
}
// Function to get the lock ID that a key is assigned to
function getKeyAssignedLock(keyId) {
if (!window.keyLockMappings || !window.keyLockMappings[keyId]) {
return null;
}
return window.keyLockMappings[keyId].lockId;
}
// Console helper functions for testing
window.reassignKeysToLocks = function() {
// Clear existing mappings
window.keyLockMappings = {};
assignKeysToLocks();
console.log('Key-lock mappings reassigned based on current scenario');
};
window.showKeyLockMappings = function() {
console.log('Current key-lock mappings:', window.keyLockMappings);
console.log('Available lock configurations:', PREDEFINED_LOCK_CONFIGS);
// Show scenario-based mappings
if (window.gameState?.scenario?.rooms) {
console.log('Current scenario rooms:', Object.keys(window.gameState.scenario.rooms));
}
};
window.testKeyLockMatch = function(keyId, lockId) {
const matches = doesKeyMatchLock(keyId, lockId);
console.log(`Key "${keyId}" ${matches ? 'MATCHES' : 'DOES NOT MATCH'} lock "${lockId}"`);
return matches;
};
// Function to reinitialize mappings when scenario changes
window.initializeKeyLockMappings = function() {
console.log('Initializing key-lock mappings for current scenario...');
window.keyLockMappings = {};
assignKeysToLocks();
};
// Initialize key-lock mappings when the game starts
if (window.inventory && window.inventory.items) {
assignKeysToLocks();
}
// Function to generate key cuts that match a specific lock's pin configuration
export function generateKeyCutsForLock(key, lockable) {
const keyId = key.scenarioData.key_id;
// Check if this key has a predefined lock assignment
if (window.keyLockMappings && window.keyLockMappings[keyId]) {
const mapping = window.keyLockMappings[keyId];
const lockConfig = mapping.lockConfig;
console.log(`Generating cuts for key "${key.scenarioData.name}" assigned to lock "${mapping.lockId}"`);
// Generate cuts based on the assigned lock's pin configuration
const cuts = [];
const pinHeights = lockConfig.pinHeights || [];
for (let i = 0; i < lockConfig.pinCount; i++) {
const keyPinLength = pinHeights[i] || 30; // Use predefined pin height
// Calculate cut depth with INVERSE relationship to key pin length
// Longer key pins need shallower cuts (less lift required)
// Shorter key pins need deeper cuts (more lift required)
// Based on the lockpicking minigame formula:
// Cut depth = key pin length - gap from key blade top to shear line
const keyBladeTop_world = 175; // Key blade top position
const shearLine_world = 155; // Shear line position
const gapFromKeyBladeTopToShearLine = keyBladeTop_world - shearLine_world; // 20
// Calculate the required cut depth
const cutDepth_needed = keyPinLength - gapFromKeyBladeTopToShearLine;
// Clamp to valid range (0 to 110, which is key blade height)
const clampedCutDepth = Math.max(0, Math.min(110, cutDepth_needed));
cuts.push(Math.round(clampedCutDepth));
console.log(`Pin ${i}: keyPinLength=${keyPinLength}, cutDepth=${clampedCutDepth} (gap=${gapFromKeyBladeTopToShearLine})`);
}
console.log(`Generated cuts for key ${keyId} (assigned to ${mapping.lockId}):`, cuts);
return cuts;
}
// Fallback: Try to get the lock's pin configuration from the minigame framework
let lockConfig = null;
const lockId = lockable.scenarioData?.lockId || lockable.id || 'default_lock';
if (window.lockConfigurations && window.lockConfigurations[lockId]) {
lockConfig = window.lockConfigurations[lockId];
}
// If no saved config, generate a default configuration
if (!lockConfig) {
console.log(`No predefined mapping for key ${keyId} and no saved lock configuration for ${lockId}, generating default cuts`);
// Generate random cuts based on the key_id for consistency
let seed = key.scenarioData.key_id.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
const random = (min, max) => {
const x = Math.sin(seed++) * 10000;
return Math.floor((x - Math.floor(x)) * (max - min + 1)) + min;
};
const cuts = [];
const numCuts = key.scenarioData.pinCount || 4;
for (let i = 0; i < numCuts; i++) {
cuts.push(random(20, 80)); // Random cuts between 20-80
}
return cuts;
}
// Generate cuts based on the lock's actual pin configuration
console.log(`Generating key cuts for lock ${lockId} with config:`, lockConfig);
const cuts = [];
const pinHeights = lockConfig.pinHeights || [];
// Generate cuts that will work with the lock's pin heights
for (let i = 0; i < lockConfig.pinCount; i++) {
const keyPinLength = pinHeights[i] || (25 + Math.random() * 37.5); // Default if missing
// Calculate cut depth with INVERSE relationship to key pin length
// Based on the lockpicking minigame formula:
// Cut depth = key pin length - gap from key blade top to shear line
const keyBladeTop_world = 175; // Key blade top position
const shearLine_world = 155; // Shear line position
const gapFromKeyBladeTopToShearLine = keyBladeTop_world - shearLine_world; // 20
// Calculate the required cut depth
const cutDepth_needed = keyPinLength - gapFromKeyBladeTopToShearLine;
// Clamp to valid range (0 to 110, which is key blade height)
const clampedCutDepth = Math.max(0, Math.min(110, cutDepth_needed));
cuts.push(Math.round(clampedCutDepth));
}
console.log(`Generated cuts for key ${key.scenarioData.key_id}:`, cuts);
return cuts;
}
// Export all functions for use in other modules
export {
PREDEFINED_LOCK_CONFIGS,
assignKeysToLocks,
generatePinHeightsForLock,
doesKeyMatchLock,
getKeyAssignedLock
};
// Export for global access
window.assignKeysToLocks = assignKeysToLocks;
window.doesKeyMatchLock = doesKeyMatchLock;
window.getKeyAssignedLock = getKeyAssignedLock;
window.generateKeyCutsForLock = generateKeyCutsForLock;

View File

@@ -0,0 +1,215 @@
/**
* MINIGAME STARTERS
* =================
*
* Functions to initialize and start various minigames (lockpicking, key selection).
* These are wrappers around the MinigameFramework that handle setup and callbacks.
*/
import { generateKeyCutsForLock, doesKeyMatchLock, PREDEFINED_LOCK_CONFIGS } from './key-lock-system.js';
export function startLockpickingMinigame(lockable, scene, difficulty = 'medium', callback) {
console.log('Starting lockpicking minigame with difficulty:', difficulty);
// Initialize the minigame framework if not already done
if (!window.MinigameFramework) {
console.error('MinigameFramework not available');
// Fallback to simple version
window.gameAlert('Advanced lockpicking unavailable. Using simple pick attempt.', 'warning', 'Lockpicking', 2000);
const success = Math.random() < 0.6; // 60% chance
setTimeout(() => {
if (success) {
window.gameAlert('Successfully picked the lock!', 'success', 'Lock Picked', 2000);
callback(true);
} else {
window.gameAlert('Failed to pick the lock.', 'error', 'Pick Failed', 2000);
callback(false);
}
}, 1000);
return;
}
// Use the advanced minigame framework
if (!window.MinigameFramework.mainGameScene) {
window.MinigameFramework.init(scene);
}
// Start the lockpicking minigame (Phaser version)
window.MinigameFramework.startMinigame('lockpicking', null, {
lockable: lockable,
difficulty: difficulty,
cancelText: 'Close',
onComplete: (success, result) => {
if (success) {
console.log('LOCKPICK SUCCESS');
window.gameAlert('Successfully picked the lock!', 'success', 'Lockpicking', 4000);
callback(true);
} else {
console.log('LOCKPICK FAILED');
window.gameAlert('Failed to pick the lock.', 'error', 'Lockpicking', 4000);
callback(false);
}
}
});
}
export function startKeySelectionMinigame(lockable, type, playerKeys, requiredKeyId, unlockTargetCallback) {
console.log('Starting key selection minigame', { playerKeys, requiredKeyId });
// Initialize the minigame framework if not already done
if (!window.MinigameFramework) {
console.error('MinigameFramework not available');
// Fallback to simple key selection
const correctKey = playerKeys.find(key => key.scenarioData.key_id === requiredKeyId);
if (correctKey) {
window.gameAlert(`You used the ${correctKey.scenarioData.name} to unlock the ${type}.`, 'success', 'Unlock Successful', 4000);
if (unlockTargetCallback) {
unlockTargetCallback(lockable, type, lockable.layer);
}
} else {
window.gameAlert('None of your keys work with this lock.', 'error', 'Wrong Keys', 4000);
}
return;
}
// Use the advanced minigame framework
if (!window.MinigameFramework.mainGameScene) {
window.MinigameFramework.init(window.game);
}
// Determine the lock ID for this lockable based on scenario data
let lockId = null;
// Try to find the lock ID from the scenario data
if (lockable.scenarioData?.requires) {
// This is a key lock, find which key it requires
const requiredKeyId = lockable.scenarioData.requires;
// Find the mapping for this key to get the lock ID
if (window.keyLockMappings && window.keyLockMappings[requiredKeyId]) {
lockId = window.keyLockMappings[requiredKeyId].lockId;
console.log(`Found lock ID "${lockId}" for key "${requiredKeyId}"`);
}
}
// Fallback to default lock ID
if (!lockId) {
lockId = lockable.scenarioData?.lockId || lockable.id || 'default_lock';
console.log(`Using fallback lock ID "${lockId}"`);
}
// Find the key that matches this lock
const matchingKey = playerKeys.find(key => doesKeyMatchLock(key.scenarioData.key_id, lockId));
let keysToShow = playerKeys;
if (matchingKey) {
console.log(`Found matching key "${matchingKey.scenarioData.name}" for lock "${lockId}"`);
// For now, show all keys so player has to figure out which one works
// In the future, you could show only the matching key or give hints
} else {
console.log(`No matching key found for lock "${lockId}", showing all keys`);
}
// Convert inventory keys to the format expected by the minigame
const inventoryKeys = keysToShow.map(key => {
// Generate cuts data if not present
let cuts = key.scenarioData.cuts;
if (!cuts) {
// Generate cuts that match the lock's pin configuration
cuts = generateKeyCutsForLock(key, lockable);
}
return {
id: key.scenarioData.key_id,
name: key.scenarioData.name,
cuts: cuts,
pinCount: key.scenarioData.pinCount || 4, // Default to 4 pins to match most locks
matchesLock: doesKeyMatchLock(key.scenarioData.key_id, lockId) // Add flag for matching
};
});
// Determine which lock configuration to use for this lockable
let lockConfig = null;
// First, try to find the lock configuration from scenario-based mappings
if (lockable.scenarioData?.requires) {
const requiredKeyId = lockable.scenarioData.requires;
if (window.keyLockMappings && window.keyLockMappings[requiredKeyId]) {
lockConfig = window.keyLockMappings[requiredKeyId].lockConfig;
console.log(`Using scenario-based lock configuration for key "${requiredKeyId}":`, lockConfig);
}
}
// Fallback to predefined configurations
if (!lockConfig && PREDEFINED_LOCK_CONFIGS[lockId]) {
lockConfig = PREDEFINED_LOCK_CONFIGS[lockId];
console.log(`Using predefined lock configuration for ${lockId}:`, lockConfig);
}
// Final fallback to default configuration
if (!lockConfig) {
lockConfig = {
id: lockId,
pinCount: 4,
pinHeights: [30, 28, 32, 29],
difficulty: 'medium'
};
console.log(`Using default lock configuration for ${lockId}:`, lockConfig);
}
// Start the key selection minigame
window.MinigameFramework.startMinigame('lockpicking', null, {
keyMode: true,
skipStartingKey: true,
lockable: lockable,
lockId: lockId,
pinCount: lockConfig.pinCount,
predefinedPinHeights: lockConfig.pinHeights, // Pass the predefined pin heights
difficulty: lockConfig.difficulty,
cancelText: 'Close',
onComplete: (success, result) => {
if (success) {
console.log('KEY SELECTION SUCCESS');
window.gameAlert('Successfully unlocked with the correct key!', 'success', 'Unlock Successful', 4000);
// Small delay to ensure minigame cleanup completes before room loading
if (unlockTargetCallback) {
setTimeout(() => {
unlockTargetCallback(lockable, type, lockable.layer);
}, 100);
}
} else {
console.log('KEY SELECTION FAILED');
window.gameAlert('The selected key doesn\'t work with this lock.', 'error', 'Wrong Key', 4000);
}
}
});
// Start with key selection using inventory keys
// 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
const updatedInventoryKeys = playerKeys.map(key => {
let cuts = key.scenarioData.cuts;
if (!cuts) {
cuts = generateKeyCutsForLock(key, lockable);
}
return {
id: key.scenarioData.key_id,
name: key.scenarioData.name,
cuts: cuts,
pinCount: key.scenarioData.pinCount || 4
};
});
window.MinigameFramework.currentMinigame.startWithKeySelection(updatedInventoryKeys, requiredKeyId);
}
}, 500);
}
// Export for global access
window.startLockpickingMinigame = startLockpickingMinigame;
window.startKeySelectionMinigame = startKeySelectionMinigame;

View File

@@ -21,44 +21,54 @@ export function initializeObjectPhysics(gameInstance, roomsRef) {
export function setupChairCollisions(chair) {
if (!chair || !chair.body) return;
// Ensure we have a valid game reference
const game = gameRef || window.game;
if (!game) {
console.error('No game reference available, cannot set up chair collisions');
return;
}
// Use window.rooms to ensure we see the latest state
const allRooms = window.rooms || {};
// Collision with other chairs
if (window.chairs) {
window.chairs.forEach(otherChair => {
if (otherChair !== chair && otherChair.body) {
gameRef.physics.add.collider(chair, otherChair);
game.physics.add.collider(chair, otherChair);
}
});
}
// Collision with tables and other static objects
Object.values(rooms).forEach(room => {
Object.values(allRooms).forEach(room => {
if (room.objects) {
Object.values(room.objects).forEach(obj => {
if (obj !== chair && obj.body && obj.body.immovable) {
gameRef.physics.add.collider(chair, obj);
game.physics.add.collider(chair, obj);
}
});
}
});
// Collision with wall collision boxes
Object.values(rooms).forEach(room => {
Object.values(allRooms).forEach(room => {
if (room.wallCollisionBoxes) {
room.wallCollisionBoxes.forEach(wallBox => {
if (wallBox.body) {
gameRef.physics.add.collider(chair, wallBox);
game.physics.add.collider(chair, wallBox);
}
});
}
});
// Collision with closed door sprites
Object.values(rooms).forEach(room => {
Object.values(allRooms).forEach(room => {
if (room.doorSprites) {
room.doorSprites.forEach(doorSprite => {
// Only collide with closed doors (doors that haven't been opened)
if (doorSprite.body && doorSprite.body.immovable) {
gameRef.physics.add.collider(chair, doorSprite);
game.physics.add.collider(chair, doorSprite);
}
});
}
@@ -69,16 +79,24 @@ export function setupChairCollisions(chair) {
export function setupExistingChairsWithNewRoom(roomId) {
if (!window.chairs) return;
const room = rooms[roomId];
// Use window.rooms to ensure we see the latest state
const room = window.rooms ? window.rooms[roomId] : null;
if (!room) return;
// Ensure we have a valid game reference
const game = gameRef || window.game;
if (!game) {
console.error('No game reference available, cannot set up chair collisions');
return;
}
// Collision with new room's tables and static objects
if (room.objects) {
Object.values(room.objects).forEach(obj => {
if (obj.body && obj.body.immovable) {
window.chairs.forEach(chair => {
if (chair.body) {
gameRef.physics.add.collider(chair, obj);
game.physics.add.collider(chair, obj);
}
});
}
@@ -91,7 +109,7 @@ export function setupExistingChairsWithNewRoom(roomId) {
if (wallBox.body) {
window.chairs.forEach(chair => {
if (chair.body) {
gameRef.physics.add.collider(chair, wallBox);
game.physics.add.collider(chair, wallBox);
}
});
}
@@ -105,12 +123,14 @@ export function setupExistingChairsWithNewRoom(roomId) {
if (doorSprite.body && doorSprite.body.immovable) {
window.chairs.forEach(chair => {
if (chair.body) {
gameRef.physics.add.collider(chair, doorSprite);
game.physics.add.collider(chair, doorSprite);
}
});
}
});
}
console.log(`Set up chair collisions for room ${roomId} with ${window.chairs.length} existing chairs`);
}
// Calculate chair spin direction based on contact point
@@ -185,6 +205,10 @@ export function calculateChairSpinDirection(player, chair) {
export function updateSwivelChairRotation() {
if (!window.chairs) return;
// Ensure we have a valid game reference
const game = gameRef || window.game;
if (!game) return; // Silently return if no game reference
window.chairs.forEach(chair => {
if (!chair.hasWheels || !chair.body) return;
@@ -249,7 +273,7 @@ export function updateSwivelChairRotation() {
}
// Check if texture exists before setting
if (gameRef.textures.exists(newTexture)) {
if (game.textures.exists(newTexture)) {
chair.setTexture(newTexture);
} else {
console.warn(`Texture not found: ${newTexture}`);

348
js/systems/unlock-system.js Normal file
View File

@@ -0,0 +1,348 @@
/**
* 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 } from './minigame-starters.js';
// 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');
// 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;
}
switch(lockRequirements.lockType) {
case 'key':
const requiredKey = lockRequirements.requires;
console.log('KEY REQUIRED', requiredKey);
// Get all keys from player's inventory
const playerKeys = window.inventory.items.filter(item =>
item && item.scenarioData &&
item.scenarioData.type === 'key'
);
if (playerKeys.length > 0) {
// Show key selection interface
startKeySelectionMinigame(lockable, type, playerKeys, requiredKey, unlockTarget);
} else {
// Check for lockpick kit
const hasLockpick = window.inventory.items.some(item =>
item && item.scenarioData &&
item.scenarioData.type === 'lockpick'
);
if (hasLockpick) {
console.log('LOCKPICK AVAILABLE');
if (confirm("Would you like to attempt picking this lock?")) {
let difficulty = lockable.scenarioData?.difficulty || lockable.properties?.difficulty || 'medium';
console.log('STARTING LOCKPICK MINIGAME', { difficulty });
startLockpickingMinigame(lockable, window.game, difficulty, (success) => {
if (success) {
// Small delay to ensure minigame cleanup completes
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);
}
});
}
} else {
console.log('NO KEYS OR LOCKPICK AVAILABLE');
window.gameAlert(`Requires key: ${requiredKey}`, 'error', 'Locked', 4000);
}
}
break;
case 'pin':
console.log('PIN CODE REQUESTED');
const pinInput = prompt(`Enter PIN code:`);
if (pinInput === lockRequirements.requires) {
unlockTarget(lockable, type, lockable.layer);
console.log('PIN CODE SUCCESS');
window.gameAlert(`Correct PIN! The ${type} is now unlocked.`, 'success', 'PIN Accepted', 4000);
} else if (pinInput !== null) {
console.log('PIN CODE FAIL');
window.gameAlert("Incorrect PIN code.", 'error', 'PIN Rejected', 3000);
}
break;
case 'password':
console.log('PASSWORD REQUESTED');
if (window.showPasswordModal) {
window.showPasswordModal(function(passwordInput) {
if (passwordInput === lockRequirements.requires) {
unlockTarget(lockable, type, lockable.layer);
console.log('PASSWORD SUCCESS');
window.gameAlert(`Correct password! The ${type} is now unlocked.`, 'success', 'Password Accepted', 4000);
} else if (passwordInput !== null) {
console.log('PASSWORD FAIL');
window.gameAlert("Incorrect password.", 'error', 'Password Rejected', 3000);
}
});
} else {
// Fallback to prompt
const passwordInput = prompt(`Enter password:`);
if (passwordInput === lockRequirements.requires) {
unlockTarget(lockable, type, lockable.layer);
console.log('PASSWORD SUCCESS');
window.gameAlert(`Correct password! The ${type} is now unlocked.`, 'success', 'Password Accepted', 4000);
} else if (passwordInput !== null) {
console.log('PASSWORD FAIL');
window.gameAlert("Incorrect password.", 'error', 'Password Rejected', 3000);
}
}
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;
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
};
}
}
// 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,
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
};
}
return null;
}
export function getLockRequirementsForItem(item) {
if (!item.scenarioData) return null;
return {
lockType: item.scenarioData.lockType || 'key',
requires: item.scenarioData.requires || ''
};
}
export function unlockTarget(lockable, type, layer) {
if (type === 'door') {
// After unlocking, use the proper door unlock function
unlockDoor(lockable);
} 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;
return; // Return early to prevent automatic collection
}
} else {
lockable.locked = false;
if (lockable.contents) {
lockable.isUnlockedButNotCollected = true;
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;

View File

@@ -4,10 +4,6 @@
"rooms": {
"reception": {
"type": "room_reception",
"locked": true,
"lockType": "key",
"requires": "ceo_office_key",
"difficulty": "easy",
"connections": {
"north": "office1"
},
@@ -58,11 +54,23 @@
"takeable": true,
"inInventory": true,
"observations": "A powerful workstation for cryptographic analysis"
},
{
"type": "key",
"name": "Office Key",
"takeable": true,
"key_id": "office1_key:40,35,38,32,10",
"observations": "A key to access the office areas"
}
]
},
"office1": {
"type": "room_office",
"locked": true,
"lockType": "key",
"requires": "office1_key:40,35,38,32,10",
"difficulty": "easy",
"connections": {
"north": ["office2", "office3"],
"south": "reception"
@@ -120,7 +128,7 @@
"type": "key",
"name": "CEO Office Key",
"takeable": true,
"key_id": "ceo_office_key",
"key_id": "ceo_office_key:10,20,30,40",
"observations": "A spare key to the CEO's office, carelessly left behind"
}
]
@@ -166,7 +174,7 @@
},
"locked": true,
"lockType": "key",
"requires": "ceo_office_key",
"requires": "ceo_office_key:10,20,30,40",
"difficulty": "easy",
"objects": [
{
@@ -181,7 +189,7 @@
"takeable": false,
"locked": true,
"lockType": "key",
"requires": "briefcase_key",
"requires": "briefcase_key:45,35,25,15",
"difficulty": "medium",
"observations": "An expensive leather briefcase with a sturdy lock",
"contents": [
@@ -197,7 +205,7 @@
"type": "key",
"name": "Safe Key",
"takeable": true,
"key_id": "safe_key",
"key_id": "safe_key:52,29,44,37",
"observations": "A heavy-duty safe key hidden behind server equipment"
}
]
@@ -227,7 +235,7 @@
"takeable": false,
"locked": true,
"lockType": "key",
"requires": "safe_key",
"requires": "safe_key:52,29,44,37",
"difficulty": "hard",
"observations": "A well-hidden wall safe behind a painting",
"contents": [
@@ -262,7 +270,7 @@
"type": "key",
"name": "Briefcase Key",
"takeable": true,
"key_id": "briefcase_key",
"key_id": "briefcase_key:45,35,25,15",
"observations": "A small key labeled 'Personal - Do Not Copy'"
}
]