mirror of
https://github.com/cliffe/BreakEscape.git
synced 2026-02-21 11:18:08 +00:00
Implement Line-of-Sight (LOS) System for NPCs
- Added core LOS detection module (`js/systems/npc-los.js`) with functions for distance and angle checks, and debug visualization. - Integrated LOS checks into NPC manager (`js/systems/npc-manager.js`) to enhance lockpicking interruption logic based on player visibility. - Updated scenario configurations for NPCs to include LOS properties. - Created comprehensive documentation covering implementation details, configuration options, and testing procedures. - Enhanced debugging capabilities with console commands and visualization options. - Established performance metrics and future enhancement plans for server-side validation and obstacle detection.
This commit is contained in:
279
docs/NPC_BEHAVIOUR_SCENARIO_FORMAT_COMPARISON.md
Normal file
279
docs/NPC_BEHAVIOUR_SCENARIO_FORMAT_COMPARISON.md
Normal file
@@ -0,0 +1,279 @@
|
||||
# Scenario JSON Format Comparison
|
||||
|
||||
## File Structure Issues in `npc-patrol-lockpick.json`
|
||||
|
||||
### ❌ **Issue 1: Incorrect NPC `patrol` Property Placement**
|
||||
|
||||
**WRONG (npc-patrol-lockpick.json - second NPC):**
|
||||
```json
|
||||
"security_guard": {
|
||||
"los": { ... },
|
||||
"patrol": { // ← This is at NPC root level
|
||||
"route": [ ... ],
|
||||
"speed": 40,
|
||||
"pauseTime": 10
|
||||
},
|
||||
"eventMappings": [ ... ]
|
||||
}
|
||||
```
|
||||
|
||||
**CORRECT (test-npc-patrol.json):**
|
||||
```json
|
||||
"patrol_with_face": {
|
||||
"behavior": { // ← patrol should be INSIDE behavior
|
||||
"facePlayer": true,
|
||||
"patrol": {
|
||||
"enabled": true,
|
||||
"speed": 100,
|
||||
"changeDirectionInterval": 4000,
|
||||
"bounds": { ... }
|
||||
}
|
||||
},
|
||||
"eventMappings": [ ... ]
|
||||
}
|
||||
```
|
||||
|
||||
**Why this matters:** The NPC manager expects `npc.behavior.patrol`, not `npc.patrol`. The current structure will cause the patrol system to not find the configuration.
|
||||
|
||||
---
|
||||
|
||||
### ❌ **Issue 2: Trailing Comma in `patrol` Object**
|
||||
|
||||
**WRONG (npc-patrol-lockpick.json - first NPC):**
|
||||
```json
|
||||
"patrol": {
|
||||
"enabled": true,
|
||||
"speed": 100,
|
||||
"changeDirectionInterval": 4000,
|
||||
"bounds": { ... }
|
||||
}, // ← Trailing comma before eventMappings
|
||||
"eventMappings": [ ... ]
|
||||
```
|
||||
|
||||
**CORRECT (test-npc-patrol.json):**
|
||||
```json
|
||||
"patrol": {
|
||||
"enabled": true,
|
||||
"speed": 100,
|
||||
"changeDirectionInterval": 4000,
|
||||
"bounds": { ... }
|
||||
} // ← No trailing comma - eventMappings should be after behavior closes
|
||||
```
|
||||
|
||||
**Why this matters:** The `eventMappings` is at the wrong nesting level. It should be at the NPC root, not inside `behavior`.
|
||||
|
||||
---
|
||||
|
||||
### ❌ **Issue 3: Missing Root-Level Properties**
|
||||
|
||||
**WRONG (npc-patrol-lockpick.json):**
|
||||
```json
|
||||
{
|
||||
"scenario_brief": "...",
|
||||
"globalVariables": { ... },
|
||||
"startRoom": "patrol_corridor",
|
||||
"startItemsInInventory": [],
|
||||
// Missing: "endGoal"
|
||||
}
|
||||
```
|
||||
|
||||
**CORRECT (test-npc-patrol.json):**
|
||||
```json
|
||||
{
|
||||
"scenario_brief": "...",
|
||||
"endGoal": "Test NPCs patrolling with various configurations and constraints",
|
||||
"startRoom": "test_patrol",
|
||||
// No unnecessary "globalVariables" or "startItemsInInventory" needed
|
||||
}
|
||||
```
|
||||
|
||||
**Why this matters:** `endGoal` is a standard scenario property used for game state and victory conditions.
|
||||
|
||||
---
|
||||
|
||||
### ✅ **Issue 4: Correct Structure for Multiple NPC Types**
|
||||
|
||||
The test file properly shows:
|
||||
|
||||
**Simple Patrol:**
|
||||
```json
|
||||
"behavior": {
|
||||
"facePlayer": false,
|
||||
"patrol": {
|
||||
"enabled": true,
|
||||
"speed": 100,
|
||||
"changeDirectionInterval": 3000,
|
||||
"bounds": { x, y, width, height }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Patrol with Face Player:**
|
||||
```json
|
||||
"behavior": {
|
||||
"facePlayer": true,
|
||||
"facePlayerDistance": 96,
|
||||
"patrol": {
|
||||
"enabled": true,
|
||||
"speed": 100,
|
||||
"changeDirectionInterval": 4000,
|
||||
"bounds": { x, y, width, height }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary of Required Fixes for `npc-patrol-lockpick.json`
|
||||
|
||||
### For `security_guard` NPC:
|
||||
|
||||
**CURRENT (BROKEN):**
|
||||
```json
|
||||
{
|
||||
"id": "security_guard",
|
||||
"los": { ... },
|
||||
"patrol": { // ← WRONG: at root level
|
||||
"route": [ ... ],
|
||||
"speed": 40,
|
||||
"pauseTime": 10
|
||||
},
|
||||
"eventMappings": [ ... ]
|
||||
}
|
||||
```
|
||||
|
||||
**CORRECTED:**
|
||||
```json
|
||||
{
|
||||
"id": "security_guard",
|
||||
"displayName": "Security Guard",
|
||||
"npcType": "person",
|
||||
"position": { "x": 5, "y": 4 },
|
||||
"spriteSheet": "hacker-red",
|
||||
"spriteTalk": "assets/characters/hacker-red-talk.png",
|
||||
"spriteConfig": {
|
||||
"idleFrameStart": 20,
|
||||
"idleFrameEnd": 23
|
||||
},
|
||||
"storyPath": "scenarios/ink/security-guard.json",
|
||||
"currentKnot": "start",
|
||||
"los": {
|
||||
"enabled": true,
|
||||
"range": 300,
|
||||
"angle": 140,
|
||||
"visualize": true
|
||||
},
|
||||
"behavior": {
|
||||
"patrol": { // ← CORRECT: inside behavior
|
||||
"route": [
|
||||
{ "x": 2, "y": 3 },
|
||||
{ "x": 8, "y": 3 },
|
||||
{ "x": 8, "y": 6 },
|
||||
{ "x": 2, "y": 6 }
|
||||
],
|
||||
"speed": 40,
|
||||
"pauseTime": 10
|
||||
}
|
||||
},
|
||||
"eventMappings": [
|
||||
{
|
||||
"eventPattern": "lockpick_used_in_view",
|
||||
"targetKnot": "on_lockpick_used",
|
||||
"conversationMode": "person-chat",
|
||||
"cooldown": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### For `patrol_with_face` NPC:
|
||||
|
||||
**CURRENT (MOSTLY CORRECT):**
|
||||
```json
|
||||
{
|
||||
"id": "patrol_with_face",
|
||||
"los": { ... },
|
||||
"behavior": {
|
||||
"facePlayer": true,
|
||||
"facePlayerDistance": 96,
|
||||
"patrol": { ... },
|
||||
}, // ← Trailing comma here is OK since eventMappings is wrong nesting
|
||||
"eventMappings": [ ... ]
|
||||
}
|
||||
```
|
||||
|
||||
**SHOULD BE:**
|
||||
```json
|
||||
{
|
||||
"id": "patrol_with_face",
|
||||
"behavior": {
|
||||
"facePlayer": true,
|
||||
"facePlayerDistance": 96,
|
||||
"patrol": {
|
||||
"enabled": true,
|
||||
"speed": 100,
|
||||
"changeDirectionInterval": 4000,
|
||||
"bounds": { ... }
|
||||
}
|
||||
},
|
||||
"los": { ... },
|
||||
"eventMappings": [ ... ]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Expected Format According to `test-npc-patrol.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"scenario_brief": "...",
|
||||
"endGoal": "...",
|
||||
"startRoom": "...",
|
||||
|
||||
"player": { ... },
|
||||
|
||||
"rooms": {
|
||||
"room_id": {
|
||||
"type": "room_office",
|
||||
"connections": { ... },
|
||||
"npcs": [
|
||||
{
|
||||
"id": "npc_id",
|
||||
"displayName": "...",
|
||||
"npcType": "person",
|
||||
"position": { "x": 5, "y": 5 },
|
||||
"spriteSheet": "hacker",
|
||||
"spriteConfig": { ... },
|
||||
"storyPath": "scenarios/ink/...",
|
||||
"currentKnot": "start",
|
||||
"behavior": {
|
||||
"facePlayer": false,
|
||||
"patrol": {
|
||||
"enabled": true,
|
||||
"speed": 100,
|
||||
"changeDirectionInterval": 3000,
|
||||
"bounds": { x, y, width, height }
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"objects": [ ... ]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Takeaways
|
||||
|
||||
1. **`patrol` must be inside `behavior`** - not at NPC root
|
||||
2. **All NPC-level properties should come before `behavior`** (displayName, spriteSheet, position, etc.)
|
||||
3. **`eventMappings` is at NPC root level** - after `behavior` closes
|
||||
4. **No trailing commas** - watch for syntax errors
|
||||
5. **`endGoal` should be at scenario root** - describes mission objective
|
||||
|
||||
These are standard structure requirements for the NPC system to properly parse and initialize the patrol behavior.
|
||||
208
docs/NPC_LOS_SYSTEM.md
Normal file
208
docs/NPC_LOS_SYSTEM.md
Normal file
@@ -0,0 +1,208 @@
|
||||
# NPC Line-of-Sight (LOS) System Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
The NPC Line-of-Sight (LOS) system allows NPCs to detect the player and events (like lockpicking) only when the player is within a configurable vision cone. This adds realism to NPC perception and prevents event triggering when NPCs can't "see" the player.
|
||||
|
||||
## Configuration
|
||||
|
||||
### NPC JSON Structure
|
||||
|
||||
Add a `los` property to any NPC definition:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "security_guard",
|
||||
"npcType": "person",
|
||||
"los": {
|
||||
"enabled": true,
|
||||
"range": 300,
|
||||
"angle": 140,
|
||||
"visualize": false
|
||||
},
|
||||
"eventMappings": [
|
||||
{
|
||||
"eventPattern": "lockpick_used_in_view",
|
||||
"targetKnot": "on_lockpick_used",
|
||||
"conversationMode": "person-chat",
|
||||
"cooldown": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### LOS Properties
|
||||
|
||||
| Property | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `enabled` | boolean | true | Whether LOS detection is active |
|
||||
| `range` | number | 300 | Detection range in pixels |
|
||||
| `angle` | number | 120 | Field of view angle in degrees (120° = 60° on each side of facing direction) |
|
||||
| `visualize` | boolean | false | Whether to render the LOS cone for debugging |
|
||||
|
||||
## How It Works
|
||||
|
||||
### Detection Algorithm
|
||||
|
||||
1. **Distance Check**: Player must be within `range` pixels of NPC
|
||||
2. **Angle Check**: Player must be within `angle` degrees of NPC's facing direction
|
||||
3. **Both conditions required**: Player must satisfy both distance AND angle constraints
|
||||
|
||||
### Facing Direction
|
||||
|
||||
The system automatically detects NPC facing direction from:
|
||||
1. Explicit `facingDirection` property (if set on NPC instance)
|
||||
2. Sprite rotation (converted from radians to degrees)
|
||||
3. Direction property (0=down, 1=left, 2=up, 3=right)
|
||||
4. Default: 270° (facing up)
|
||||
|
||||
For NPCs with patrol routes, the facing direction updates based on current movement direction.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Files
|
||||
|
||||
- **`js/systems/npc-los.js`** - Core LOS detection and visualization
|
||||
- `isInLineOfSight(npc, target, losConfig)` - Check if target is in LOS
|
||||
- `drawLOSCone(scene, npc, losConfig)` - Render debug visualization
|
||||
- `clearLOSCone(graphics)` - Clean up graphics
|
||||
|
||||
- **`js/systems/npc-manager.js`** - NPC manager integration
|
||||
- `shouldInterruptLockpickingWithPersonChat(roomId, playerPosition)` - Check if any NPC can see the player attempting to lockpick
|
||||
- `setLOSVisualization(enable, scene)` - Toggle LOS cone rendering
|
||||
- `updateLOSVisualizations(scene)` - Update cone graphics (call from game loop)
|
||||
|
||||
- **`js/systems/unlock-system.js`** - Integration with lock system
|
||||
- Passes player position when checking for NPC interruption
|
||||
|
||||
### Integration with Lockpicking
|
||||
|
||||
When a player attempts to lockpick:
|
||||
|
||||
```javascript
|
||||
// unlock-system.js checks LOS before starting minigame
|
||||
const playerPos = window.player.sprite.getCenter();
|
||||
const interruptingNPC = window.npcManager.shouldInterruptLockpickingWithPersonChat(roomId, playerPos);
|
||||
|
||||
if (interruptingNPC) {
|
||||
// NPC can see player - trigger person-chat instead of lockpicking
|
||||
// emit lockpick_used_in_view event
|
||||
return; // Don't start lockpicking
|
||||
}
|
||||
// Otherwise, proceed with normal lockpicking
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Checking LOS in Code
|
||||
|
||||
```javascript
|
||||
import { isInLineOfSight } from 'js/systems/npc-los.js';
|
||||
|
||||
const losConfig = {
|
||||
range: 300,
|
||||
angle: 120,
|
||||
enabled: true
|
||||
};
|
||||
|
||||
const canSee = isInLineOfSight(npc, playerPosition, losConfig);
|
||||
if (canSee) {
|
||||
console.log('NPC can see player!');
|
||||
}
|
||||
```
|
||||
|
||||
### Debugging with Visualization
|
||||
|
||||
Enable LOS cone rendering to visualize NPC vision:
|
||||
|
||||
```javascript
|
||||
// In console or during game init
|
||||
window.npcManager.setLOSVisualization(true, window.game.scene.scenes[0]);
|
||||
|
||||
// Then call from game loop (in update method)
|
||||
window.npcManager.updateLOSVisualizations(window.game.scene.scenes[0]);
|
||||
```
|
||||
|
||||
The visualization shows:
|
||||
- **Green semi-transparent cone** = NPC's field of view
|
||||
- **Cone origin** = NPC's position
|
||||
- **Cone angle** = Configured `angle` property
|
||||
- **Cone range** = Configured `range` property
|
||||
|
||||
### Server Migration Notes
|
||||
|
||||
Since this system is client-side only, consider:
|
||||
- **Phase 1** (Current): Client-side LOS checks for cosmetic reactions
|
||||
- **Phase 2** (Future): Server validates LOS before accepting unlock attempts
|
||||
- **Migration Path**: Keep client-side system for immediate feedback, server validates actual event
|
||||
|
||||
## Testing
|
||||
|
||||
### Test Scenario
|
||||
|
||||
The file `scenarios/npc-patrol-lockpick.json` includes two NPCs with LOS configured:
|
||||
|
||||
```json
|
||||
"security_guard": {
|
||||
"los": {
|
||||
"enabled": true,
|
||||
"range": 300,
|
||||
"angle": 140,
|
||||
"visualize": false
|
||||
},
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### Test Cases
|
||||
|
||||
1. **In LOS**: Player stands in front of NPC within range → NPC reacts to lockpicking
|
||||
2. **Out of Range**: Player stands far away → NPC does NOT react
|
||||
3. **Behind NPC**: Player behind NPC's facing direction → NPC does NOT react
|
||||
4. **Partial Angle**: Player at edge of FOV cone → Reacts only if within angle bounds
|
||||
|
||||
### Running Tests
|
||||
|
||||
```javascript
|
||||
// Enable LOS visualization
|
||||
window.npcManager.setLOSVisualization(true, window.game.scene.scenes[0]);
|
||||
|
||||
// Manually test LOS
|
||||
const playerPos = window.player.sprite.getCenter();
|
||||
const security_guard = window.npcManager.getNPC('security_guard');
|
||||
const canSee = window.npcManager.shouldInterruptLockpickingWithPersonChat('patrol_corridor', playerPos);
|
||||
console.log('Can NPC see player?', canSee !== null);
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- **LOS checks**: O(n) where n = number of NPCs in room (very fast)
|
||||
- **Distance calculation**: Uses Phaser's `Distance.Between()` (optimized)
|
||||
- **Visualization**: Only enabled for debugging, should be disabled in production
|
||||
- **Angle calculation**: Minimal overhead, only done when needed
|
||||
|
||||
## Common Issues
|
||||
|
||||
### Issue: NPC always sees player
|
||||
- **Check**: Verify `los.enabled: true` in NPC definition
|
||||
- **Check**: Confirm `range` is large enough for test scenario
|
||||
- **Check**: Verify `angle` value is correct (should be 120-180 for typical coverage)
|
||||
|
||||
### Issue: NPC never sees player
|
||||
- **Check**: Player position is correct (check `window.player.sprite.getCenter()`)
|
||||
- **Check**: NPC position is correct
|
||||
- **Check**: NPC facing direction is correct (check `npc.direction` or `npc.facingDirection`)
|
||||
- **Debug**: Enable visualization with `setLOSVisualization(true, scene)`
|
||||
|
||||
### Issue: Visualization cone not showing
|
||||
- **Check**: `visualize` property is set to `true` (or always enabled via `setLOSVisualization`)
|
||||
- **Check**: Scene is passed correctly to `updateLOSVisualizations()`
|
||||
- **Check**: Call `updateLOSVisualizations()` from game's update loop
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
1. **Obstacles**: Add wall/terrain blocking for more realistic LOS
|
||||
2. **Hearing**: Add audio-based detection (separate system)
|
||||
3. **Memory**: Add NPC memory of recent player sightings
|
||||
4. **Alert Levels**: Different LOS ranges based on NPC alert state
|
||||
5. **Dynamic Facing**: Update facing direction based on patrol waypoints
|
||||
Reference in New Issue
Block a user