mirror of
https://github.com/cliffe/BreakEscape.git
synced 2026-02-20 13:50:46 +00:00
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:
330
README_design.md
330
README_design.md
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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'"
|
||||
}
|
||||
]
|
||||
|
||||
48
css/main.css
48
css/main.css
@@ -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%;
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
168
js/systems/biometrics.js
Normal 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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
306
js/systems/key-lock-system.js
Normal file
306
js/systems/key-lock-system.js
Normal 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;
|
||||
|
||||
215
js/systems/minigame-starters.js
Normal file
215
js/systems/minigame-starters.js
Normal 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;
|
||||
|
||||
@@ -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
348
js/systems/unlock-system.js
Normal 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;
|
||||
|
||||
@@ -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'"
|
||||
}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user