mirror of
https://github.com/cliffe/BreakEscape.git
synced 2026-02-20 13:50:46 +00:00
feat(npc): Implement Phase 4 - Personal Space Behavior Testing
✅ Phase 4: Personal Space Behavior COMPLETE
Test scenario with 10 NPCs, comprehensive documentation (1000+ lines),
and Ink toggle testing for personal space behavior.
Includes:
- 10 test NPCs with varied configurations
- Distance tests (32px to 128px bubbles)
- Backing speed tests (3px to 10px increments)
- Wall collision detection tests
- Personal space + patrol integration
- Ink toggle for runtime distance control
- Complete test guide with debugging tools
Key features tested:
- Gradual backing away (5px increments)
- Face player while backing
- Wall collision detection
- Priority system integration
- Idle animations (not walk)
- Distance-based activation
This commit is contained in:
753
planning_notes/npc/npc_behaviour/PHASE4_TEST_GUIDE.md
Normal file
753
planning_notes/npc/npc_behaviour/PHASE4_TEST_GUIDE.md
Normal file
@@ -0,0 +1,753 @@
|
||||
# Phase 4: Personal Space Behavior - Test Guide
|
||||
|
||||
## Overview
|
||||
|
||||
Phase 4 focuses on testing and verifying the **Personal Space** behavior. This makes NPCs back away from the player when they get too close, creating realistic social distancing behavior.
|
||||
|
||||
**Status**: ✅ Implementation Complete, Ready for Testing
|
||||
|
||||
---
|
||||
|
||||
## What Was Implemented
|
||||
|
||||
### Core Functionality
|
||||
|
||||
1. **Distance-Based Activation**
|
||||
- NPCs detect when player enters personal space bubble
|
||||
- Configurable distance (default: 48px / 1.5 tiles)
|
||||
- Only activates when enabled
|
||||
|
||||
2. **Gradual Backing Away**
|
||||
- Small incremental movements (default: 5px per update)
|
||||
- Configurable back-away distance
|
||||
- Smooth, natural-looking retreat
|
||||
|
||||
3. **Face Player While Backing**
|
||||
- NPC maintains eye contact during retreat
|
||||
- Uses idle animation (not walk)
|
||||
- Direction updates to face player
|
||||
|
||||
4. **Wall Collision Detection**
|
||||
- NPCs can't back through walls
|
||||
- Position validation after movement attempt
|
||||
- Falls back to face-only when blocked
|
||||
|
||||
5. **Priority Integration**
|
||||
- Personal Space is Priority 3 (high priority)
|
||||
- Overrides patrol and face player
|
||||
- Only overridden by hostile behaviors (chase/flee)
|
||||
|
||||
6. **Ink Tag Control**
|
||||
- `#personal_space:64` - Set distance to 64px
|
||||
- `#personal_space:0` - Disable personal space
|
||||
- Runtime distance adjustment
|
||||
|
||||
---
|
||||
|
||||
## Test Scenario
|
||||
|
||||
**File**: `scenarios/test-npc-personal-space.json`
|
||||
|
||||
This scenario contains 10 NPCs testing various personal space configurations:
|
||||
|
||||
### Test NPCs
|
||||
|
||||
| NPC ID | Position | Distance | Back Speed | Increment | Test Purpose |
|
||||
|--------|----------|----------|------------|-----------|--------------|
|
||||
| `personal_space_basic` | (5,5) | 48px | 30px/s | 5px | Standard config |
|
||||
| `personal_space_large` | (8,3) | 96px | 30px/s | 5px | Large bubble (3 tiles) |
|
||||
| `personal_space_small` | (2,3) | 32px | 30px/s | 5px | Small bubble (1 tile) |
|
||||
| `personal_space_fast` | (8,8) | 48px | 60px/s | 10px | Fast backing |
|
||||
| `personal_space_slow` | (2,8) | 48px | 15px/s | 3px | Slow backing |
|
||||
| `personal_space_corner` | (1,1) | 64px | 30px/s | 5px | Wall collision test |
|
||||
| `personal_space_with_patrol` | (5,2) | 48px | 30px/s | 5px | Patrol + space |
|
||||
| `personal_space_toggle` | (10,5) | 48px | 30px/s | 5px | Ink toggle test |
|
||||
| `personal_space_very_shy` | (5,8) | 128px | 40px/s | 8px | Extreme (4 tiles) |
|
||||
| `no_personal_space` | (9,1) | N/A | N/A | N/A | Disabled |
|
||||
|
||||
### Visual Layout
|
||||
|
||||
```
|
||||
Room: test_personal_space (room_office)
|
||||
|
||||
1 2 3 4 5 6 7 8 9 10
|
||||
1 [Corner] [Disabled]
|
||||
2 [WithPatrol]
|
||||
3 [Small] [Large]
|
||||
4
|
||||
5 [Basic] [Toggle]
|
||||
6
|
||||
7
|
||||
8 [Slow] [VeryShy] [Fast]
|
||||
9
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## How to Test
|
||||
|
||||
### Setup
|
||||
|
||||
1. **Load Test Scenario**:
|
||||
```javascript
|
||||
window.gameScenario = await fetch('scenarios/test-npc-personal-space.json').then(r => r.json());
|
||||
// Then reload game
|
||||
```
|
||||
|
||||
2. **Verify Behavior Manager**:
|
||||
```javascript
|
||||
console.log('Behavior Manager:', window.npcBehaviorManager);
|
||||
const behavior = window.npcBehaviorManager.getBehavior('personal_space_basic');
|
||||
console.log('Personal space config:', behavior.config.personalSpace);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Test Procedures
|
||||
|
||||
### Test 1: Basic Personal Space
|
||||
|
||||
**NPC**: `personal_space_basic` (center, blue)
|
||||
|
||||
**Configuration**:
|
||||
- Distance: 48px (1.5 tiles)
|
||||
- Back speed: 30px/s
|
||||
- Increment: 5px
|
||||
|
||||
**Procedure**:
|
||||
1. Start far from NPC (> 3 tiles away)
|
||||
2. Slowly walk toward NPC
|
||||
3. Observe at different distances
|
||||
|
||||
**Expected Behavior**:
|
||||
|
||||
**When Far (> 48px)**:
|
||||
- ✅ NPC turns to face player (face player behavior)
|
||||
- ✅ NPC stays in place
|
||||
- ✅ Uses idle animation
|
||||
- ✅ State: `'face_player'`
|
||||
|
||||
**When Close (< 48px)**:
|
||||
- ✅ NPC starts backing away
|
||||
- ✅ NPC moves in 5px increments
|
||||
- ✅ NPC faces player while backing
|
||||
- ✅ Uses idle animation (NOT walk)
|
||||
- ✅ State: `'maintain_space'`
|
||||
- ✅ Backing is gradual and smooth
|
||||
|
||||
**When Very Close (touching)**:
|
||||
- ✅ NPC continues backing until blocked or out of range
|
||||
- ✅ Movement is continuous but slow
|
||||
- ✅ Direction adjusts as player circles NPC
|
||||
|
||||
**Debug**:
|
||||
```javascript
|
||||
const behavior = window.npcBehaviorManager.getBehavior('personal_space_basic');
|
||||
const sprite = window.npcManager.npcs.get('personal_space_basic')._sprite;
|
||||
const player = window.player;
|
||||
|
||||
setInterval(() => {
|
||||
const dx = sprite.x - player.x;
|
||||
const dy = sprite.y - player.y;
|
||||
const dist = Math.sqrt(dx*dx + dy*dy);
|
||||
console.log('Distance:', Math.round(dist),
|
||||
'State:', behavior.currentState,
|
||||
'Backing:', behavior.backingAway);
|
||||
}, 500);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Test 2: Personal Space Bubble Sizes
|
||||
|
||||
**NPCs**: Small (32px), Basic (48px), Large (96px), Very Shy (128px)
|
||||
|
||||
**Procedure**:
|
||||
1. Stand exactly 2 tiles (64px) from each NPC
|
||||
2. Observe which NPCs react
|
||||
3. Slowly approach each NPC
|
||||
4. Note activation distances
|
||||
|
||||
**Expected Results**:
|
||||
|
||||
| NPC | Distance | At 64px | At 48px | At 32px |
|
||||
|-----|----------|---------|---------|---------|
|
||||
| Small (32px) | 1 tile | No reaction | No reaction | ✅ Backs away |
|
||||
| Basic (48px) | 1.5 tiles | No reaction | ✅ Backs away | ✅ Backs away |
|
||||
| Large (96px) | 3 tiles | ✅ Backs away | ✅ Backs away | ✅ Backs away |
|
||||
| Very Shy (128px) | 4 tiles | ✅ Backs away | ✅ Backs away | ✅ Backs away |
|
||||
|
||||
**Verification**:
|
||||
```javascript
|
||||
// Check personal space distances
|
||||
['personal_space_small', 'personal_space_basic',
|
||||
'personal_space_large', 'personal_space_very_shy'].forEach(id => {
|
||||
const behavior = window.npcBehaviorManager.getBehavior(id);
|
||||
console.log(`${id}: ${behavior.config.personalSpace.distance}px`);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Test 3: Backing Speed Variations
|
||||
|
||||
**NPCs**: Slow (3px), Basic (5px), Fast (10px)
|
||||
|
||||
**Procedure**:
|
||||
1. Approach each NPC within personal space range
|
||||
2. Stand still and let them back away
|
||||
3. Compare backing speeds visually
|
||||
|
||||
**Expected Behavior**:
|
||||
|
||||
**Slow (3px increments)**:
|
||||
- ✅ Very subtle backing
|
||||
- ✅ Barely noticeable movement
|
||||
- ✅ Natural, gentle retreat
|
||||
|
||||
**Basic (5px increments)**:
|
||||
- ✅ Moderate backing speed
|
||||
- ✅ Clearly visible but not jarring
|
||||
- ✅ Smooth, natural movement
|
||||
|
||||
**Fast (10px increments)**:
|
||||
- ✅ Noticeably faster retreat
|
||||
- ✅ More obvious backing behavior
|
||||
- ✅ Still smooth (not teleporting)
|
||||
|
||||
**Measurement**:
|
||||
```javascript
|
||||
// Track backing distance over time
|
||||
const sprite = window.npcManager.npcs.get('personal_space_fast')._sprite;
|
||||
let startX = sprite.x;
|
||||
let startY = sprite.y;
|
||||
|
||||
setTimeout(() => {
|
||||
const dx = sprite.x - startX;
|
||||
const dy = sprite.y - startY;
|
||||
const totalDist = Math.sqrt(dx*dx + dy*dy);
|
||||
console.log('Distance backed in 5s:', Math.round(totalDist), 'px');
|
||||
// Fast should be ~2x Basic
|
||||
}, 5000);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Test 4: Wall Collision Detection
|
||||
|
||||
**NPC**: `personal_space_corner` (position 1,1 - top-left corner)
|
||||
|
||||
**Setup**: NPC is positioned near room corner
|
||||
|
||||
**Procedure**:
|
||||
1. Approach NPC from the southeast (open side)
|
||||
2. Push NPC toward the corner (walls at north and west)
|
||||
3. Continue approaching
|
||||
|
||||
**Expected Behavior**:
|
||||
|
||||
**When Space Available**:
|
||||
- ✅ NPC backs away normally
|
||||
- ✅ Moves in configured increments (5px)
|
||||
- ✅ Faces player while backing
|
||||
|
||||
**When Backed Into Wall**:
|
||||
- ✅ NPC attempts to back away
|
||||
- ✅ Position doesn't change (wall blocks)
|
||||
- ✅ NPC still faces player
|
||||
- ✅ State remains `'maintain_space'`
|
||||
- ✅ No console errors
|
||||
- ✅ No jittering or stuck behavior
|
||||
|
||||
**When Player Leaves Range**:
|
||||
- ✅ NPC stops backing attempt
|
||||
- ✅ Returns to normal face player behavior
|
||||
- ✅ State: `'face_player'` or `'idle'`
|
||||
|
||||
**Debug**:
|
||||
```javascript
|
||||
const sprite = window.npcManager.npcs.get('personal_space_corner')._sprite;
|
||||
let lastX = sprite.x;
|
||||
let lastY = sprite.y;
|
||||
|
||||
setInterval(() => {
|
||||
const movedX = sprite.x - lastX;
|
||||
const movedY = sprite.y - lastY;
|
||||
if (movedX === 0 && movedY === 0) {
|
||||
console.log('🚧 NPC blocked by wall (not moving)');
|
||||
}
|
||||
lastX = sprite.x;
|
||||
lastY = sprite.y;
|
||||
}, 100);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Test 5: Personal Space + Patrol Integration
|
||||
|
||||
**NPC**: `personal_space_with_patrol` (position 5,2)
|
||||
|
||||
**Configuration**:
|
||||
- Patrol: enabled, 80px/s
|
||||
- Personal space: 48px, 5px increments
|
||||
|
||||
**Procedure**:
|
||||
1. Stay far from NPC (> 3 tiles)
|
||||
2. Observe patrol behavior
|
||||
3. Approach within 48px while NPC patrols
|
||||
4. Walk away
|
||||
|
||||
**Expected Behavior**:
|
||||
|
||||
**When Far (patrolling)**:
|
||||
- ✅ NPC patrols area normally
|
||||
- ✅ Uses walk animations
|
||||
- ✅ Changes direction periodically
|
||||
- ✅ State: `'patrol'`
|
||||
|
||||
**When Near (personal space violated)**:
|
||||
- ✅ NPC stops patrolling immediately
|
||||
- ✅ NPC backs away from player
|
||||
- ✅ Uses idle animation (not walk)
|
||||
- ✅ Faces player while backing
|
||||
- ✅ State: `'maintain_space'`
|
||||
- ✅ Velocity becomes minimal (not patrol velocity)
|
||||
|
||||
**When Leaving (player exits bubble)**:
|
||||
- ✅ NPC resumes patrol
|
||||
- ✅ Picks new patrol target
|
||||
- ✅ Resumes walk animations
|
||||
- ✅ State returns to `'patrol'`
|
||||
|
||||
**State Priority Check**:
|
||||
```javascript
|
||||
const behavior = window.npcBehaviorManager.getBehavior('personal_space_with_patrol');
|
||||
setInterval(() => {
|
||||
const sprite = window.npcManager.npcs.get('personal_space_with_patrol')._sprite;
|
||||
const player = window.player;
|
||||
const dist = Math.sqrt((sprite.x - player.x)**2 + (sprite.y - player.y)**2);
|
||||
|
||||
console.log('Distance:', Math.round(dist),
|
||||
'State:', behavior.currentState,
|
||||
'Expected:', dist < 48 ? 'maintain_space' : 'patrol');
|
||||
}, 500);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Test 6: Direction While Backing
|
||||
|
||||
**NPC**: Any NPC with personal space enabled
|
||||
|
||||
**Procedure**:
|
||||
1. Approach NPC from the north (below them)
|
||||
2. NPC should back away north and face south (down)
|
||||
3. Circle around NPC while staying close
|
||||
4. Observe direction changes
|
||||
|
||||
**Expected Behavior**:
|
||||
|
||||
**Approach from South**:
|
||||
- ✅ NPC backs north (away from player)
|
||||
- ✅ NPC faces south (toward player)
|
||||
- ✅ Direction: `'down'`
|
||||
|
||||
**Approach from North**:
|
||||
- ✅ NPC backs south
|
||||
- ✅ NPC faces north
|
||||
- ✅ Direction: `'up'`
|
||||
|
||||
**Approach from East**:
|
||||
- ✅ NPC backs west
|
||||
- ✅ NPC faces east (right)
|
||||
- ✅ Direction: `'right'`
|
||||
|
||||
**Approach from West**:
|
||||
- ✅ NPC backs east
|
||||
- ✅ NPC faces west (left with flipX)
|
||||
- ✅ Direction: `'left'`
|
||||
|
||||
**Diagonal Approaches**:
|
||||
- ✅ NPC backs in opposite diagonal
|
||||
- ✅ Faces player in diagonal direction
|
||||
- ✅ Directions: `'up-left'`, `'up-right'`, `'down-left'`, `'down-right'`
|
||||
|
||||
**Direction Calculation**:
|
||||
```javascript
|
||||
// In maintainPersonalSpace():
|
||||
// Backing direction: (dx, dy) = away from player
|
||||
// Facing direction: (-dx, -dy) = toward player
|
||||
const behavior = window.npcBehaviorManager.getBehavior('personal_space_basic');
|
||||
console.log('Facing direction:', behavior.direction);
|
||||
console.log('Should face player, not back direction');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Test 7: Animation During Personal Space
|
||||
|
||||
**Procedure**:
|
||||
1. Trigger personal space behavior
|
||||
2. Check animation state
|
||||
|
||||
**Expected**:
|
||||
- ✅ Uses idle animation (NOT walk)
|
||||
- ✅ Animation matches facing direction
|
||||
- ✅ FlipX applied for left directions
|
||||
- ✅ No animation flickering
|
||||
|
||||
**Why Idle, Not Walk?**:
|
||||
- Backing away is not "walking"
|
||||
- Movement is slow and incremental
|
||||
- Creates subtle, polite retreat behavior
|
||||
- Walk animation would look unnatural for small movements
|
||||
|
||||
**Verification**:
|
||||
```javascript
|
||||
const sprite = window.npcManager.npcs.get('personal_space_basic')._sprite;
|
||||
const behavior = window.npcBehaviorManager.getBehavior('personal_space_basic');
|
||||
|
||||
// When in personal space
|
||||
console.log('Animation:', sprite.anims.currentAnim?.key);
|
||||
console.log('Expected:', `npc-personal_space_basic-idle-${behavior.direction}`);
|
||||
console.log('State:', behavior.currentState); // Should be 'maintain_space'
|
||||
console.log('Is Moving:', behavior.isMoving); // Should be false
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Test 8: Staying Within Interaction Range
|
||||
|
||||
**Concept**: NPCs should stay close enough to interact (64px) even while backing
|
||||
|
||||
**Procedure**:
|
||||
1. Approach NPC with 48px personal space
|
||||
2. Stay at exactly 48px
|
||||
3. NPC should stop backing (at boundary)
|
||||
|
||||
**Expected**:
|
||||
- ✅ NPC backs away when < 48px
|
||||
- ✅ NPC stops when ≥ 48px
|
||||
- ✅ NPC remains within interaction range (64px typical)
|
||||
- ✅ Can still talk to NPC (E key works)
|
||||
|
||||
**Note**: Personal space distance should be less than interaction range
|
||||
- Default interaction: 64px (2 tiles)
|
||||
- Default personal space: 48px (1.5 tiles)
|
||||
- ✅ Gap of 16px ensures NPC stays interactable
|
||||
|
||||
---
|
||||
|
||||
### Test 9: Personal Space Toggle via Ink
|
||||
|
||||
**NPC**: `personal_space_toggle` (position 10,5)
|
||||
|
||||
**Procedure**:
|
||||
1. Approach NPC - should NOT back away (disabled initially)
|
||||
2. Talk to NPC
|
||||
3. Select "Enable personal space (64px)"
|
||||
4. Exit conversation
|
||||
5. Approach again - should back away now
|
||||
6. Talk again, select "Disable personal space"
|
||||
7. Approach - should NOT back away anymore
|
||||
|
||||
**Expected Behavior**:
|
||||
|
||||
**Initial State (disabled)**:
|
||||
- ✅ NPC faces player when close
|
||||
- ✅ Does NOT back away
|
||||
- ✅ Personal space enabled: `false`
|
||||
- ✅ State: `'face_player'`
|
||||
|
||||
**After "Enable personal space (64px)"**:
|
||||
- ✅ Tag `#personal_space:64` processed
|
||||
- ✅ Personal space enabled: `true`
|
||||
- ✅ Distance set to 64px
|
||||
- ✅ NPC backs away when < 64px
|
||||
- ✅ State: `'maintain_space'` when close
|
||||
|
||||
**After "Disable personal space"**:
|
||||
- ✅ Tag `#personal_space:0` processed
|
||||
- ✅ Personal space enabled: `false`
|
||||
- ✅ NPC stops backing away
|
||||
- ✅ Returns to face player only
|
||||
|
||||
**Verification**:
|
||||
```javascript
|
||||
const behavior = window.npcBehaviorManager.getBehavior('personal_space_toggle');
|
||||
console.log('Enabled:', behavior.config.personalSpace.enabled);
|
||||
console.log('Distance:', behavior.config.personalSpace.distance);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Test 10: Disabled Personal Space
|
||||
|
||||
**NPC**: `no_personal_space` (position 9,1)
|
||||
|
||||
**Configuration**: `personalSpace.enabled = false`
|
||||
|
||||
**Procedure**:
|
||||
1. Approach NPC very closely
|
||||
2. Get right on top of NPC
|
||||
3. Circle around NPC
|
||||
|
||||
**Expected Behavior**:
|
||||
- ✅ NPC never backs away
|
||||
- ✅ NPC only faces player (normal behavior)
|
||||
- ✅ Can walk right up to NPC
|
||||
- ✅ State: `'face_player'` or `'idle'`
|
||||
- ✅ No personal space state ever triggered
|
||||
|
||||
**Comparison Test**:
|
||||
```javascript
|
||||
// Compare disabled vs enabled
|
||||
const disabled = window.npcBehaviorManager.getBehavior('no_personal_space');
|
||||
const enabled = window.npcBehaviorManager.getBehavior('personal_space_basic');
|
||||
|
||||
console.log('Disabled enabled?', disabled.config.personalSpace.enabled); // false
|
||||
console.log('Enabled enabled?', enabled.config.personalSpace.enabled); // true
|
||||
|
||||
// Get very close to both and observe difference
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Edge Cases
|
||||
|
||||
### Edge Case 1: Player Exactly on NPC Position
|
||||
|
||||
**Scenario**: Player moves to exact same position as NPC
|
||||
|
||||
**Expected**:
|
||||
- Distance = 0
|
||||
- Division by zero check: `if (distance === 0) return false`
|
||||
- ✅ No crash
|
||||
- ✅ NPC doesn't move (can't calculate direction)
|
||||
- ✅ Falls back to face player behavior
|
||||
|
||||
**Test**:
|
||||
```javascript
|
||||
// Teleport player to NPC position
|
||||
const sprite = window.npcManager.npcs.get('personal_space_basic')._sprite;
|
||||
window.player.setPosition(sprite.x, sprite.y);
|
||||
// Should not crash, NPC should handle gracefully
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Edge Case 2: Continuous Pressure
|
||||
|
||||
**Scenario**: Player continuously walks into NPC
|
||||
|
||||
**Expected**:
|
||||
- ✅ NPC continuously backs away
|
||||
- ✅ Movement is smooth (not jittery)
|
||||
- ✅ NPC doesn't get "stuck"
|
||||
- ✅ Backs until blocked by wall or out of bounds
|
||||
|
||||
---
|
||||
|
||||
### Edge Case 3: Multiple Players (Not Applicable)
|
||||
|
||||
**Scenario**: Only one player in game
|
||||
|
||||
**Note**: Personal space only tracks main player position
|
||||
|
||||
---
|
||||
|
||||
### Edge Case 4: Very Large Personal Space
|
||||
|
||||
**Scenario**: `personal_space_very_shy` with 128px (4 tiles)
|
||||
|
||||
**Expected**:
|
||||
- ✅ NPC backs away from very far
|
||||
- ✅ Player can barely approach
|
||||
- ✅ No performance issues
|
||||
- ✅ State changes correctly
|
||||
|
||||
---
|
||||
|
||||
### Edge Case 5: Backing Into Another NPC
|
||||
|
||||
**Scenario**: NPC backs into another NPC
|
||||
|
||||
**Expected**:
|
||||
- ✅ Collision with other NPC prevents movement
|
||||
- ✅ Position doesn't change (blocked)
|
||||
- ✅ Falls back to face player
|
||||
- ✅ No errors
|
||||
|
||||
**Note**: NPCs have collision with each other (from Phase 1 setup)
|
||||
|
||||
---
|
||||
|
||||
## Performance Testing
|
||||
|
||||
### Test: Multiple NPCs with Personal Space
|
||||
|
||||
**Procedure**:
|
||||
1. Load test scenario (10 NPCs, 9 with personal space)
|
||||
2. Walk around room triggering multiple personal space zones
|
||||
3. Monitor FPS
|
||||
|
||||
**Expected Performance**:
|
||||
- ✅ 60 FPS with all NPCs active
|
||||
- ✅ No lag when triggering personal space
|
||||
- ✅ Smooth backing animations
|
||||
- ✅ CPU usage reasonable
|
||||
|
||||
**Debug**:
|
||||
```javascript
|
||||
// Monitor FPS
|
||||
let frames = 0;
|
||||
window.game.scene.scenes[0].events.on('postupdate', () => frames++);
|
||||
setInterval(() => {
|
||||
console.log('FPS:', frames);
|
||||
frames = 0;
|
||||
}, 1000);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Issues
|
||||
|
||||
### Issue 1: NPC Not Backing Away
|
||||
|
||||
**Symptoms**: NPC faces player but doesn't back away
|
||||
|
||||
**Possible Causes**:
|
||||
1. Personal space disabled: `enabled === false`
|
||||
2. Player outside distance: `playerDist >= config.distance`
|
||||
3. Higher priority behavior active (shouldn't be any)
|
||||
4. Backed into wall (position not changing)
|
||||
|
||||
**Debug**:
|
||||
```javascript
|
||||
const behavior = window.npcBehaviorManager.getBehavior('npc_id');
|
||||
const sprite = window.npcManager.npcs.get('npc_id')._sprite;
|
||||
const player = window.player;
|
||||
const dx = sprite.x - player.x;
|
||||
const dy = sprite.y - player.y;
|
||||
const dist = Math.sqrt(dx*dx + dy*dy);
|
||||
|
||||
console.log('Enabled:', behavior.config.personalSpace.enabled);
|
||||
console.log('Distance:', Math.round(dist), '/', behavior.config.personalSpace.distance);
|
||||
console.log('State:', behavior.currentState);
|
||||
console.log('Backing:', behavior.backingAway);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Issue 2: NPC Backing Too Fast/Slow
|
||||
|
||||
**Symptoms**: Backing speed doesn't match config
|
||||
|
||||
**Check**:
|
||||
```javascript
|
||||
const behavior = window.npcBehaviorManager.getBehavior('npc_id');
|
||||
console.log('Back away distance:', behavior.config.personalSpace.backAwayDistance);
|
||||
console.log('Back away speed:', behavior.config.personalSpace.backAwaySpeed);
|
||||
```
|
||||
|
||||
**Note**: `backAwaySpeed` is not currently used (may be for future enhancements)
|
||||
- Actual speed is `backAwayDistance` per update cycle (50ms)
|
||||
- Effective speed ≈ `backAwayDistance * 20` px/s
|
||||
|
||||
---
|
||||
|
||||
### Issue 3: Wrong Facing Direction
|
||||
|
||||
**Symptoms**: NPC faces away from player while backing
|
||||
|
||||
**Expected**: NPC should face TOWARD player (negative of backing direction)
|
||||
|
||||
**Check**:
|
||||
```javascript
|
||||
// In maintainPersonalSpace():
|
||||
// Backing vector: (dx, dy) = sprite.x - player.x (away from player)
|
||||
// Facing vector: (-dx, -dy) = negative (toward player)
|
||||
this.direction = this.calculateDirection(-dx, -dy);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Issue 4: Walk Animation Instead of Idle
|
||||
|
||||
**Symptoms**: NPC uses walk animation while backing
|
||||
|
||||
**Expected**: Should use idle animation
|
||||
|
||||
**Check**:
|
||||
```javascript
|
||||
const sprite = window.npcManager.npcs.get('npc_id')._sprite;
|
||||
console.log('Animation:', sprite.anims.currentAnim?.key);
|
||||
// Should be: npc-{npcId}-idle-{direction}
|
||||
// NOT: npc-{npcId}-walk-{direction}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
✅ **Phase 4 Complete When**:
|
||||
|
||||
1. [ ] All 10 test NPCs implemented
|
||||
2. [ ] Basic personal space backing works
|
||||
3. [ ] Bubble size variations work (32px to 128px)
|
||||
4. [ ] Backing speed variations work
|
||||
5. [ ] Wall collision detection works (can't back through walls)
|
||||
6. [ ] Personal space + patrol integration works
|
||||
7. [ ] NPCs face player while backing
|
||||
8. [ ] Idle animations used (not walk)
|
||||
9. [ ] NPCs stay within interaction range
|
||||
10. [ ] Ink toggle enables/disables personal space
|
||||
11. [ ] Disabled NPCs don't back away
|
||||
12. [ ] No console errors
|
||||
13. [ ] Performance good with 9 NPCs (60 FPS)
|
||||
|
||||
---
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Personal Space Algorithm
|
||||
|
||||
```javascript
|
||||
maintainPersonalSpace(playerPos, delta):
|
||||
1. Check if enabled and player position valid
|
||||
2. Calculate dx, dy (away from player)
|
||||
3. Calculate distance to player
|
||||
4. If distance = 0: return (avoid division by zero)
|
||||
5. Calculate target position (5px away)
|
||||
6. Attempt to move to target
|
||||
7. Check if position changed:
|
||||
- Changed: Successfully backed away
|
||||
- Not changed: Blocked by wall, face player instead
|
||||
8. Calculate facing direction (toward player)
|
||||
9. Play idle animation for facing direction
|
||||
10. Set isMoving = false, backingAway = true
|
||||
11. Return true (personal space active)
|
||||
```
|
||||
|
||||
### Why Small Increments?
|
||||
|
||||
- **5px per update** (every 50ms) = **100px/s effective speed**
|
||||
- Creates smooth, gradual retreat
|
||||
- Natural-looking social distancing
|
||||
- Gives player time to react
|
||||
- Doesn't look like NPC is "fleeing"
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
After Phase 4:
|
||||
- **Phase 5**: Ink Integration comprehensive testing
|
||||
- **Phase 6**: Hostile visual feedback
|
||||
- **Phase 7**: Advanced features (chase/flee stubs)
|
||||
|
||||
---
|
||||
|
||||
**Document Status**: Test Guide v1.0
|
||||
**Last Updated**: 2025-11-09
|
||||
**Phase**: 4 - Personal Space Behavior Testing
|
||||
106
scenarios/ink/test-personal-space-toggle.ink
Normal file
106
scenarios/ink/test-personal-space-toggle.ink
Normal file
@@ -0,0 +1,106 @@
|
||||
// Behavior Test - Personal Space Toggle
|
||||
// Demonstrates toggling personal space distance via Ink tags
|
||||
|
||||
VAR personal_space_enabled = false
|
||||
VAR personal_space_distance = 0
|
||||
|
||||
=== start ===
|
||||
# speaker:npc
|
||||
Hi! I'm the personal space toggle test NPC.
|
||||
|
||||
{personal_space_enabled:
|
||||
Right now I have personal space enabled ({personal_space_distance}px).
|
||||
|
||||
If you get too close, I'll back away while still facing you.
|
||||
-
|
||||
Right now I don't mind how close you get.
|
||||
|
||||
I'll just turn to face you when you approach.
|
||||
}
|
||||
|
||||
-> hub
|
||||
|
||||
=== hub ===
|
||||
* [Enable personal space (48px)]
|
||||
-> enable_small
|
||||
|
||||
* [Enable personal space (64px)]
|
||||
-> enable_medium
|
||||
|
||||
* [Enable personal space (96px)]
|
||||
-> enable_large
|
||||
|
||||
* [Disable personal space]
|
||||
-> disable_space
|
||||
|
||||
* [Check status]
|
||||
-> check_status
|
||||
|
||||
+ [Exit] #exit_conversation
|
||||
# speaker:npc
|
||||
{personal_space_enabled: Don't get too close! | Come back anytime!}
|
||||
|
||||
-> hub
|
||||
|
||||
=== enable_small ===
|
||||
# speaker:npc
|
||||
# personal_space:48
|
||||
~ personal_space_enabled = true
|
||||
~ personal_space_distance = 48
|
||||
|
||||
Okay, I'll back away if you get within 48 pixels (1.5 tiles).
|
||||
|
||||
That's pretty close - I like my personal bubble small.
|
||||
-> hub
|
||||
|
||||
=== enable_medium ===
|
||||
# speaker:npc
|
||||
# personal_space:64
|
||||
~ personal_space_enabled = true
|
||||
~ personal_space_distance = 64
|
||||
|
||||
Alright, I'll need at least 64 pixels (2 tiles) of space.
|
||||
|
||||
This is a comfortable distance for conversation.
|
||||
-> hub
|
||||
|
||||
=== enable_large ===
|
||||
# speaker:npc
|
||||
# personal_space:96
|
||||
~ personal_space_enabled = true
|
||||
~ personal_space_distance = 96
|
||||
|
||||
I need a lot of space! I'll back away if you're within 96 pixels (3 tiles).
|
||||
|
||||
I'm a bit shy, please don't crowd me.
|
||||
-> hub
|
||||
|
||||
=== disable_space ===
|
||||
# speaker:npc
|
||||
# personal_space:0
|
||||
~ personal_space_enabled = false
|
||||
~ personal_space_distance = 0
|
||||
|
||||
Personal space disabled! You can get as close as you want.
|
||||
|
||||
I won't back away anymore, just turn to face you.
|
||||
-> hub
|
||||
|
||||
=== check_status ===
|
||||
# speaker:npc
|
||||
Current status:
|
||||
- Personal Space: {personal_space_enabled: ENABLED ({personal_space_distance}px) | DISABLED}
|
||||
- Face Player: ENABLED
|
||||
|
||||
{personal_space_enabled:
|
||||
If you approach within {personal_space_distance} pixels, I'll slowly back away while facing you.
|
||||
|
||||
I back away in small 5px increments, so it's a gentle retreat.
|
||||
|
||||
If there's a wall behind me, I'll stop backing and just face you instead.
|
||||
-
|
||||
Personal space is off, so I won't back away at all.
|
||||
|
||||
I'll just turn to face you when you're nearby.
|
||||
}
|
||||
-> hub
|
||||
272
scenarios/test-npc-personal-space.json
Normal file
272
scenarios/test-npc-personal-space.json
Normal file
@@ -0,0 +1,272 @@
|
||||
{
|
||||
"scenario_brief": "Test scenario for NPC personal space behavior - Phase 4",
|
||||
"endGoal": "Test NPCs maintaining personal space and backing away from player",
|
||||
"startRoom": "test_personal_space",
|
||||
|
||||
"player": {
|
||||
"id": "player",
|
||||
"displayName": "Test Agent",
|
||||
"spriteSheet": "hacker",
|
||||
"spriteTalk": "assets/characters/hacker-talk.png",
|
||||
"spriteConfig": {
|
||||
"idleFrameStart": 20,
|
||||
"idleFrameEnd": 23
|
||||
}
|
||||
},
|
||||
|
||||
"rooms": {
|
||||
"test_personal_space": {
|
||||
"type": "room_office",
|
||||
"connections": {},
|
||||
"npcs": [
|
||||
{
|
||||
"id": "personal_space_basic",
|
||||
"displayName": "Basic Personal Space",
|
||||
"npcType": "person",
|
||||
"position": { "x": 5, "y": 5 },
|
||||
"spriteSheet": "hacker",
|
||||
"spriteConfig": {
|
||||
"idleFrameStart": 20,
|
||||
"idleFrameEnd": 23
|
||||
},
|
||||
"storyPath": "scenarios/ink/test-npc.json",
|
||||
"currentKnot": "start",
|
||||
"behavior": {
|
||||
"facePlayer": true,
|
||||
"personalSpace": {
|
||||
"enabled": true,
|
||||
"distance": 48,
|
||||
"backAwaySpeed": 30,
|
||||
"backAwayDistance": 5
|
||||
}
|
||||
},
|
||||
"_comment": "Standard personal space: backs away when player within 48px (1.5 tiles), 5px increments"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "personal_space_large",
|
||||
"displayName": "Large Personal Space",
|
||||
"npcType": "person",
|
||||
"position": { "x": 8, "y": 3 },
|
||||
"spriteSheet": "hacker-red",
|
||||
"spriteConfig": {
|
||||
"idleFrameStart": 20,
|
||||
"idleFrameEnd": 23
|
||||
},
|
||||
"storyPath": "scenarios/ink/test-npc.json",
|
||||
"currentKnot": "start",
|
||||
"behavior": {
|
||||
"facePlayer": true,
|
||||
"personalSpace": {
|
||||
"enabled": true,
|
||||
"distance": 96,
|
||||
"backAwaySpeed": 30,
|
||||
"backAwayDistance": 5
|
||||
}
|
||||
},
|
||||
"_comment": "Large bubble: backs away when player within 96px (3 tiles)"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "personal_space_small",
|
||||
"displayName": "Small Personal Space",
|
||||
"npcType": "person",
|
||||
"position": { "x": 2, "y": 3 },
|
||||
"spriteSheet": "hacker",
|
||||
"spriteConfig": {
|
||||
"idleFrameStart": 20,
|
||||
"idleFrameEnd": 23
|
||||
},
|
||||
"storyPath": "scenarios/ink/test-npc.json",
|
||||
"currentKnot": "start",
|
||||
"behavior": {
|
||||
"facePlayer": true,
|
||||
"personalSpace": {
|
||||
"enabled": true,
|
||||
"distance": 32,
|
||||
"backAwaySpeed": 30,
|
||||
"backAwayDistance": 5
|
||||
}
|
||||
},
|
||||
"_comment": "Small bubble: only backs away when very close (32px, 1 tile)"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "personal_space_fast",
|
||||
"displayName": "Fast Backing",
|
||||
"npcType": "person",
|
||||
"position": { "x": 8, "y": 8 },
|
||||
"spriteSheet": "hacker-red",
|
||||
"spriteConfig": {
|
||||
"idleFrameStart": 20,
|
||||
"idleFrameEnd": 23
|
||||
},
|
||||
"storyPath": "scenarios/ink/test-npc.json",
|
||||
"currentKnot": "start",
|
||||
"behavior": {
|
||||
"facePlayer": true,
|
||||
"personalSpace": {
|
||||
"enabled": true,
|
||||
"distance": 48,
|
||||
"backAwaySpeed": 60,
|
||||
"backAwayDistance": 10
|
||||
}
|
||||
},
|
||||
"_comment": "Fast backing: 10px increments instead of 5px"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "personal_space_slow",
|
||||
"displayName": "Slow Backing",
|
||||
"npcType": "person",
|
||||
"position": { "x": 2, "y": 8 },
|
||||
"spriteSheet": "hacker",
|
||||
"spriteConfig": {
|
||||
"idleFrameStart": 20,
|
||||
"idleFrameEnd": 23
|
||||
},
|
||||
"storyPath": "scenarios/ink/test-npc.json",
|
||||
"currentKnot": "start",
|
||||
"behavior": {
|
||||
"facePlayer": true,
|
||||
"personalSpace": {
|
||||
"enabled": true,
|
||||
"distance": 48,
|
||||
"backAwaySpeed": 15,
|
||||
"backAwayDistance": 3
|
||||
}
|
||||
},
|
||||
"_comment": "Slow backing: 3px increments, very subtle"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "personal_space_corner",
|
||||
"displayName": "Corner Test (Wall Blocking)",
|
||||
"npcType": "person",
|
||||
"position": { "x": 1, "y": 1 },
|
||||
"spriteSheet": "hacker-red",
|
||||
"spriteConfig": {
|
||||
"idleFrameStart": 20,
|
||||
"idleFrameEnd": 23
|
||||
},
|
||||
"storyPath": "scenarios/ink/test-npc.json",
|
||||
"currentKnot": "start",
|
||||
"behavior": {
|
||||
"facePlayer": true,
|
||||
"personalSpace": {
|
||||
"enabled": true,
|
||||
"distance": 64,
|
||||
"backAwaySpeed": 30,
|
||||
"backAwayDistance": 5
|
||||
}
|
||||
},
|
||||
"_comment": "Positioned in corner - tests wall collision detection when backing"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "personal_space_with_patrol",
|
||||
"displayName": "Personal Space + Patrol",
|
||||
"npcType": "person",
|
||||
"position": { "x": 5, "y": 2 },
|
||||
"spriteSheet": "hacker",
|
||||
"spriteConfig": {
|
||||
"idleFrameStart": 20,
|
||||
"idleFrameEnd": 23
|
||||
},
|
||||
"storyPath": "scenarios/ink/test-npc.json",
|
||||
"currentKnot": "start",
|
||||
"behavior": {
|
||||
"facePlayer": true,
|
||||
"patrol": {
|
||||
"enabled": true,
|
||||
"speed": 80,
|
||||
"changeDirectionInterval": 3000,
|
||||
"bounds": {
|
||||
"x": 128,
|
||||
"y": 32,
|
||||
"width": 96,
|
||||
"height": 96
|
||||
}
|
||||
},
|
||||
"personalSpace": {
|
||||
"enabled": true,
|
||||
"distance": 48,
|
||||
"backAwaySpeed": 30,
|
||||
"backAwayDistance": 5
|
||||
}
|
||||
},
|
||||
"_comment": "Patrols normally, but backs away when player gets close (priority test)"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "personal_space_toggle",
|
||||
"displayName": "Personal Space Toggle",
|
||||
"npcType": "person",
|
||||
"position": { "x": 10, "y": 5 },
|
||||
"spriteSheet": "hacker-red",
|
||||
"spriteConfig": {
|
||||
"idleFrameStart": 20,
|
||||
"idleFrameEnd": 23
|
||||
},
|
||||
"storyPath": "scenarios/ink/test-personal-space-toggle.json",
|
||||
"currentKnot": "start",
|
||||
"behavior": {
|
||||
"facePlayer": true,
|
||||
"personalSpace": {
|
||||
"enabled": false,
|
||||
"distance": 48,
|
||||
"backAwaySpeed": 30,
|
||||
"backAwayDistance": 5
|
||||
}
|
||||
},
|
||||
"_comment": "Starts disabled, can be toggled via Ink #personal_space:64 tag"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "personal_space_very_shy",
|
||||
"displayName": "Very Shy (Extreme)",
|
||||
"npcType": "person",
|
||||
"position": { "x": 5, "y": 8 },
|
||||
"spriteSheet": "hacker",
|
||||
"spriteConfig": {
|
||||
"idleFrameStart": 20,
|
||||
"idleFrameEnd": 23
|
||||
},
|
||||
"storyPath": "scenarios/ink/test-npc.json",
|
||||
"currentKnot": "start",
|
||||
"behavior": {
|
||||
"facePlayer": true,
|
||||
"personalSpace": {
|
||||
"enabled": true,
|
||||
"distance": 128,
|
||||
"backAwaySpeed": 40,
|
||||
"backAwayDistance": 8
|
||||
}
|
||||
},
|
||||
"_comment": "Extreme personal space: 128px (4 tiles), backs away quickly"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "no_personal_space",
|
||||
"displayName": "No Personal Space (Disabled)",
|
||||
"npcType": "person",
|
||||
"position": { "x": 9, "y": 1 },
|
||||
"spriteSheet": "hacker-red",
|
||||
"spriteConfig": {
|
||||
"idleFrameStart": 20,
|
||||
"idleFrameEnd": 23
|
||||
},
|
||||
"storyPath": "scenarios/ink/test-npc.json",
|
||||
"currentKnot": "start",
|
||||
"behavior": {
|
||||
"facePlayer": true,
|
||||
"personalSpace": {
|
||||
"enabled": false
|
||||
}
|
||||
},
|
||||
"_comment": "Personal space disabled - should NOT back away, just face player"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user