mirror of
https://github.com/cliffe/BreakEscape.git
synced 2026-02-20 13:50:46 +00:00
feat(npc): Implement Phase 3 - Patrol Behavior Testing & Verification
✅ Phase 3: Patrol Behavior COMPLETE
Test scenario with 9 NPCs, comprehensive documentation (800+ lines),
and Ink toggle testing for patrol behavior.
Includes:
- 9 test NPCs with varied configurations
- Speed tests (50-200 px/s)
- Bounds validation tests
- Stuck detection tests
- Narrow corridor tests
- Patrol + face player integration
- Complete test guide with debugging tools
This commit is contained in:
672
planning_notes/npc/npc_behaviour/PHASE3_TEST_GUIDE.md
Normal file
672
planning_notes/npc/npc_behaviour/PHASE3_TEST_GUIDE.md
Normal file
@@ -0,0 +1,672 @@
|
||||
# Phase 3: Patrol Behavior - Test Guide
|
||||
|
||||
## Overview
|
||||
|
||||
Phase 3 focuses on testing and verifying the **Patrol** behavior. This makes NPCs move randomly within defined bounds, creating dynamic, living environments.
|
||||
|
||||
**Status**: ✅ Implementation Complete, Ready for Testing
|
||||
|
||||
---
|
||||
|
||||
## What Was Implemented
|
||||
|
||||
### Core Functionality
|
||||
|
||||
1. **Random Movement Within Bounds**
|
||||
- NPCs pick random target points within configured bounds
|
||||
- Move toward target using velocity-based physics
|
||||
- Pick new target when reached (< 8px distance)
|
||||
|
||||
2. **Timed Direction Changes**
|
||||
- Configurable interval (default: 3000ms)
|
||||
- New random target chosen at each interval
|
||||
- Prevents NPCs from getting "stuck" in patterns
|
||||
|
||||
3. **Stuck Detection & Recovery**
|
||||
- Detects when NPC is blocked by collision
|
||||
- 500ms timeout before choosing new direction
|
||||
- Prevents infinite collision loops
|
||||
|
||||
4. **Walk Animations**
|
||||
- 8-way walk animations during movement
|
||||
- Direction calculated based on velocity
|
||||
- Smooth animation transitions
|
||||
|
||||
5. **Collision Handling**
|
||||
- NPCs collide with walls, chairs, and other objects
|
||||
- Physics-based collision response
|
||||
- Automatic recovery from stuck states
|
||||
|
||||
6. **Priority Integration**
|
||||
- Patrol is Priority 2 (overridden by higher priorities)
|
||||
- Face Player (Priority 1) can interrupt patrol
|
||||
- Personal Space (Priority 3) overrides patrol
|
||||
|
||||
---
|
||||
|
||||
## Test Scenario
|
||||
|
||||
**File**: `scenarios/test-npc-patrol.json`
|
||||
|
||||
This scenario contains 9 NPCs testing various patrol configurations:
|
||||
|
||||
### Test NPCs
|
||||
|
||||
| NPC ID | Position | Speed | Interval | Bounds | Test Purpose |
|
||||
|--------|----------|-------|----------|--------|--------------|
|
||||
| `patrol_basic` | (3,3) | 100 | 3000ms | 6x6 tiles | Standard patrol |
|
||||
| `patrol_fast` | (8,3) | 200 | 2000ms | 4x4 tiles | High speed |
|
||||
| `patrol_slow` | (3,8) | 50 | 5000ms | 4x3 tiles | Low speed |
|
||||
| `patrol_small` | (8,8) | 80 | 2000ms | 2x2 tiles | Tiny area |
|
||||
| `patrol_with_face` | (5,5) | 100 | 4000ms | 4x4 tiles | Patrol + face player |
|
||||
| `patrol_narrow_horizontal` | (1,1) | 100 | 3000ms | 8x1 tiles | Corridor test |
|
||||
| `patrol_narrow_vertical` | (1,5) | 100 | 3000ms | 1x5 tiles | Corridor test |
|
||||
| `patrol_initially_disabled` | (10,5) | 100 | 3000ms | 3x3 tiles | Toggle via Ink |
|
||||
| `patrol_stuck_test` | (6,1) | 120 | 4000ms | 3x3 tiles | Collision test |
|
||||
|
||||
### Visual Layout
|
||||
|
||||
```
|
||||
Room: test_patrol (room_office)
|
||||
|
||||
1 2 3 4 5 6 7 8 9 10
|
||||
1 [NarrowH] [NarrowH] [Stuck]
|
||||
2
|
||||
3 [Basic] [Fast]
|
||||
4
|
||||
5 [NarrowV] [WithFace] [Toggle]
|
||||
6
|
||||
7
|
||||
8 [Slow] [Small]
|
||||
9
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## How to Test
|
||||
|
||||
### Setup
|
||||
|
||||
1. **Load Test Scenario**:
|
||||
```javascript
|
||||
window.gameScenario = await fetch('scenarios/test-npc-patrol.json').then(r => r.json());
|
||||
// Then reload game
|
||||
```
|
||||
|
||||
2. **Verify Behavior Manager**:
|
||||
```javascript
|
||||
console.log('Behavior Manager:', window.npcBehaviorManager);
|
||||
console.log('Registered behaviors:', window.npcBehaviorManager.behaviors.size);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Test Procedures
|
||||
|
||||
### Test 1: Basic Patrol Movement
|
||||
|
||||
**NPC**: `patrol_basic` (blue, position 3,3)
|
||||
|
||||
**Configuration**:
|
||||
- Speed: 100px/s
|
||||
- Interval: 3000ms (3 seconds)
|
||||
- Bounds: 6x6 tiles (192x192px)
|
||||
|
||||
**Procedure**:
|
||||
1. Observe NPC from a distance (don't approach)
|
||||
2. Watch for 30 seconds
|
||||
|
||||
**Expected Behavior**:
|
||||
- ✅ NPC should walk to random points within 6x6 area
|
||||
- ✅ Changes direction every 3 seconds
|
||||
- ✅ Uses walk animations (8 directions)
|
||||
- ✅ Smooth movement, no jittering
|
||||
- ✅ Stays within bounds (2-7 tiles from origin)
|
||||
- ✅ Direction matches movement (walks forward, not sideways)
|
||||
|
||||
**Measurements**:
|
||||
```javascript
|
||||
const behavior = window.npcBehaviorManager.getBehavior('patrol_basic');
|
||||
console.log('Current target:', behavior.patrolTarget);
|
||||
console.log('Direction:', behavior.direction);
|
||||
console.log('Is moving:', behavior.isMoving);
|
||||
console.log('Current state:', behavior.currentState); // Should be 'patrol'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Test 2: Speed Variations
|
||||
|
||||
**NPCs**: `patrol_fast` (200px/s) vs `patrol_slow` (50px/s)
|
||||
|
||||
**Procedure**:
|
||||
1. Observe both NPCs simultaneously
|
||||
2. Compare movement speeds visually
|
||||
|
||||
**Expected Behavior**:
|
||||
- ✅ `patrol_fast` moves noticeably faster (2x basic speed)
|
||||
- ✅ `patrol_slow` moves noticeably slower (0.5x basic speed)
|
||||
- ✅ Both use correct walk animation frame rate (8 fps)
|
||||
- ✅ Animation doesn't look sped up/slowed down (velocity changes, not animation)
|
||||
- ✅ Fast NPC reaches targets quicker
|
||||
- ✅ Slow NPC appears to "stroll"
|
||||
|
||||
**Debug**:
|
||||
```javascript
|
||||
// Check velocities
|
||||
const fast = window.npcManager.npcs.get('patrol_fast')._sprite;
|
||||
const slow = window.npcManager.npcs.get('patrol_slow')._sprite;
|
||||
console.log('Fast velocity:', Math.sqrt(fast.body.velocity.x**2 + fast.body.velocity.y**2));
|
||||
console.log('Slow velocity:', Math.sqrt(slow.body.velocity.x**2 + slow.body.velocity.y**2));
|
||||
// Fast should be ~200, Slow should be ~50
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Test 3: Direction Change Intervals
|
||||
|
||||
**NPCs**: Various intervals
|
||||
|
||||
- `patrol_fast`: 2000ms (2 seconds)
|
||||
- `patrol_basic`: 3000ms (3 seconds)
|
||||
- `patrol_slow`: 5000ms (5 seconds)
|
||||
|
||||
**Procedure**:
|
||||
1. Time direction changes with a stopwatch
|
||||
2. Observe for 30 seconds
|
||||
3. Count direction changes
|
||||
|
||||
**Expected Results**:
|
||||
- ✅ `patrol_fast`: ~15 direction changes in 30s
|
||||
- ✅ `patrol_basic`: ~10 direction changes in 30s
|
||||
- ✅ `patrol_slow`: ~6 direction changes in 30s
|
||||
- ✅ Changes are roughly consistent (±10%)
|
||||
- ✅ NPC picks different target each time (not same point)
|
||||
|
||||
**Debug**:
|
||||
```javascript
|
||||
// Monitor direction changes
|
||||
const behavior = window.npcBehaviorManager.getBehavior('patrol_basic');
|
||||
let lastTarget = null;
|
||||
setInterval(() => {
|
||||
if (JSON.stringify(behavior.patrolTarget) !== lastTarget) {
|
||||
console.log('Direction changed:', behavior.patrolTarget);
|
||||
lastTarget = JSON.stringify(behavior.patrolTarget);
|
||||
}
|
||||
}, 100);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Test 4: Bounds Validation
|
||||
|
||||
**NPC**: `patrol_basic` (6x6 tile bounds)
|
||||
|
||||
**Bounds Configuration**:
|
||||
```json
|
||||
{
|
||||
"x": 64,
|
||||
"y": 64,
|
||||
"width": 192,
|
||||
"height": 192
|
||||
}
|
||||
```
|
||||
|
||||
**World Coordinates**: (64, 64) to (256, 256)
|
||||
|
||||
**Procedure**:
|
||||
1. Observe NPC for 1 minute
|
||||
2. Note maximum/minimum X and Y positions reached
|
||||
|
||||
**Expected Behavior**:
|
||||
- ✅ NPC X position: 64 ≤ X ≤ 256
|
||||
- ✅ NPC Y position: 64 ≤ Y ≤ 256
|
||||
- ✅ NPC never leaves bounds area
|
||||
- ✅ Targets are distributed throughout bounds (not clustered)
|
||||
|
||||
**Debug**:
|
||||
```javascript
|
||||
// Track bounds violations
|
||||
const behavior = window.npcBehaviorManager.getBehavior('patrol_basic');
|
||||
const sprite = window.npcManager.npcs.get('patrol_basic')._sprite;
|
||||
const bounds = behavior.config.patrol.worldBounds;
|
||||
|
||||
setInterval(() => {
|
||||
const x = sprite.x;
|
||||
const y = sprite.y;
|
||||
if (x < bounds.x || x > bounds.x + bounds.width ||
|
||||
y < bounds.y || y > bounds.y + bounds.height) {
|
||||
console.error('❌ BOUNDS VIOLATION:', {x, y, bounds});
|
||||
}
|
||||
}, 100);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Test 5: Stuck Detection & Recovery
|
||||
|
||||
**NPC**: `patrol_stuck_test`
|
||||
|
||||
**Setup**:
|
||||
1. Place obstacles in patrol area (if possible)
|
||||
2. Observe NPC encountering obstacles
|
||||
|
||||
**Procedure**:
|
||||
1. Watch NPC patrol
|
||||
2. Wait for NPC to hit a wall or obstacle
|
||||
3. Observe recovery behavior
|
||||
|
||||
**Expected Behavior**:
|
||||
- ✅ NPC walks toward wall/obstacle
|
||||
- ✅ NPC stops when colliding (blocked state)
|
||||
- ✅ After ~500ms, NPC chooses new direction
|
||||
- ✅ New direction avoids the obstacle
|
||||
- ✅ NPC doesn't get permanently stuck
|
||||
- ✅ No console errors
|
||||
|
||||
**Debug**:
|
||||
```javascript
|
||||
// Monitor stuck states
|
||||
const behavior = window.npcBehaviorManager.getBehavior('patrol_stuck_test');
|
||||
const sprite = window.npcManager.npcs.get('patrol_stuck_test')._sprite;
|
||||
|
||||
setInterval(() => {
|
||||
const isBlocked = sprite.body.blocked.none === false;
|
||||
if (isBlocked) {
|
||||
console.log('🚧 NPC stuck! Timer:', behavior.stuckTimer, 'ms');
|
||||
}
|
||||
}, 100);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Test 6: Narrow Area Patrol
|
||||
|
||||
**NPCs**:
|
||||
- `patrol_narrow_horizontal` (8x1 tiles - horizontal corridor)
|
||||
- `patrol_narrow_vertical` (1x5 tiles - vertical corridor)
|
||||
|
||||
**Procedure**:
|
||||
1. Observe horizontal NPC - should mostly move left/right
|
||||
2. Observe vertical NPC - should mostly move up/down
|
||||
3. Check animations match movement direction
|
||||
|
||||
**Expected Behavior**:
|
||||
|
||||
**Horizontal NPC**:
|
||||
- ✅ Primarily uses `walk-left` and `walk-right` animations
|
||||
- ✅ Rarely uses vertical animations
|
||||
- ✅ Stays within 8-tile wide corridor
|
||||
- ✅ Smooth horizontal movement
|
||||
|
||||
**Vertical NPC**:
|
||||
- ✅ Primarily uses `walk-up` and `walk-down` animations
|
||||
- ✅ Rarely uses horizontal animations
|
||||
- ✅ Stays within 1-tile wide corridor
|
||||
- ✅ Smooth vertical movement
|
||||
|
||||
---
|
||||
|
||||
### Test 7: Patrol + Face Player Interaction
|
||||
|
||||
**NPC**: `patrol_with_face` (center, red sprite)
|
||||
|
||||
**Configuration**:
|
||||
- Patrol: enabled, 100px/s
|
||||
- Face Player: enabled, 96px range
|
||||
|
||||
**Procedure**:
|
||||
1. Stay far from NPC (>3 tiles)
|
||||
2. Observe patrol behavior
|
||||
3. Approach within 3 tiles
|
||||
4. Walk away
|
||||
|
||||
**Expected Behavior**:
|
||||
|
||||
**When Far (>3 tiles)**:
|
||||
- ✅ NPC patrols normally
|
||||
- ✅ Uses walk animations
|
||||
- ✅ Changes direction every 4 seconds
|
||||
- ✅ State: `'patrol'`
|
||||
|
||||
**When Near (<3 tiles)**:
|
||||
- ✅ NPC stops patrolling
|
||||
- ✅ NPC turns to face player
|
||||
- ✅ Uses idle animation facing player
|
||||
- ✅ Velocity becomes (0, 0)
|
||||
- ✅ State: `'face_player'`
|
||||
|
||||
**When Leaving**:
|
||||
- ✅ NPC resumes patrol after player leaves range
|
||||
- ✅ Picks new random target
|
||||
- ✅ Resumes walk animations
|
||||
- ✅ State returns to `'patrol'`
|
||||
|
||||
**Debug**:
|
||||
```javascript
|
||||
const behavior = window.npcBehaviorManager.getBehavior('patrol_with_face');
|
||||
setInterval(() => {
|
||||
console.log('State:', behavior.currentState,
|
||||
'Is Moving:', behavior.isMoving,
|
||||
'Direction:', behavior.direction);
|
||||
}, 500);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Test 8: Small Area Patrol
|
||||
|
||||
**NPC**: `patrol_small` (2x2 tiles only)
|
||||
|
||||
**Procedure**:
|
||||
1. Observe NPC in tiny area
|
||||
2. Watch for 30 seconds
|
||||
|
||||
**Expected Behavior**:
|
||||
- ✅ NPC moves within 2x2 tile area only
|
||||
- ✅ Frequent direction changes (targets nearby)
|
||||
- ✅ Reaches targets quickly (small distances)
|
||||
- ✅ No getting stuck in corners
|
||||
- ✅ Smooth transitions despite small space
|
||||
|
||||
**Edge Case Check**:
|
||||
- Target point might be very close to current position
|
||||
- Should still move smoothly, not jitter
|
||||
|
||||
---
|
||||
|
||||
### Test 9: Patrol Toggle via Ink
|
||||
|
||||
**NPC**: `patrol_initially_disabled`
|
||||
|
||||
**Procedure**:
|
||||
1. Observe NPC initially - should be stationary
|
||||
2. Talk to NPC (E key when nearby)
|
||||
3. Select "Start patrolling"
|
||||
4. Exit conversation - NPC should start moving
|
||||
5. Talk again, select "Stop patrolling"
|
||||
6. Exit - NPC should stop
|
||||
|
||||
**Expected Behavior**:
|
||||
|
||||
**Initial State**:
|
||||
- ✅ NPC is stationary (idle animation)
|
||||
- ✅ NPC faces player when nearby
|
||||
- ✅ State: `'face_player'` or `'idle'`
|
||||
- ✅ Patrol enabled: `false`
|
||||
|
||||
**After "Start patrolling"**:
|
||||
- ✅ Tag `#patrol_mode:on` processed
|
||||
- ✅ NPC starts moving after conversation ends
|
||||
- ✅ State changes to `'patrol'`
|
||||
- ✅ Uses walk animations
|
||||
- ✅ Patrol enabled: `true`
|
||||
|
||||
**After "Stop patrolling"**:
|
||||
- ✅ Tag `#patrol_mode:off` processed
|
||||
- ✅ NPC stops moving
|
||||
- ✅ Returns to idle/face player behavior
|
||||
- ✅ Patrol enabled: `false`
|
||||
|
||||
**Debug**:
|
||||
```javascript
|
||||
const behavior = window.npcBehaviorManager.getBehavior('patrol_initially_disabled');
|
||||
console.log('Patrol enabled:', behavior.config.patrol.enabled);
|
||||
console.log('Current state:', behavior.currentState);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Testing
|
||||
|
||||
### Test: Multiple Patrolling NPCs
|
||||
|
||||
**Procedure**:
|
||||
1. Load test scenario (9 NPCs, 8 patrolling)
|
||||
2. Let all NPCs patrol simultaneously
|
||||
3. Monitor FPS and performance
|
||||
|
||||
**Expected Performance**:
|
||||
- ✅ Stable 60 FPS with 8 patrolling NPCs
|
||||
- ✅ No visible lag or stuttering
|
||||
- ✅ Smooth animations for all NPCs
|
||||
- ✅ CPU usage reasonable (<20% spike)
|
||||
|
||||
**Debug**:
|
||||
```javascript
|
||||
// Monitor FPS
|
||||
let lastTime = performance.now();
|
||||
let frames = 0;
|
||||
setInterval(() => {
|
||||
const now = performance.now();
|
||||
const fps = frames / ((now - lastTime) / 1000);
|
||||
console.log('FPS:', fps.toFixed(1));
|
||||
frames = 0;
|
||||
lastTime = now;
|
||||
}, 1000);
|
||||
window.game.scene.scenes[0].events.on('postupdate', () => frames++);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Animation Testing
|
||||
|
||||
### Expected Animation States
|
||||
|
||||
**While Patrolling**:
|
||||
- Animation: `npc-{npcId}-walk-{direction}`
|
||||
- Direction: Matches movement vector
|
||||
- Frame rate: 8 fps
|
||||
- FlipX: true for left-facing directions
|
||||
|
||||
**When Reaching Target**:
|
||||
- Brief moment at target (< 8px)
|
||||
- May show idle frame for 1 frame
|
||||
- Quickly picks new target and resumes walking
|
||||
|
||||
**When Blocked**:
|
||||
- Walk animation continues briefly
|
||||
- After 500ms stuck timeout, picks new direction
|
||||
- Changes to new walk animation
|
||||
|
||||
### Debug Animations
|
||||
|
||||
```javascript
|
||||
const sprite = window.npcManager.npcs.get('patrol_basic')._sprite;
|
||||
console.log('Current animation:', sprite.anims.currentAnim?.key);
|
||||
console.log('Is playing:', sprite.anims.isPlaying);
|
||||
console.log('FlipX:', sprite.flipX);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Edge Cases
|
||||
|
||||
### Edge Case 1: Target Point on Wall
|
||||
|
||||
**Scenario**: Random target is inside a wall
|
||||
|
||||
**Expected**:
|
||||
- NPC walks toward target
|
||||
- Hits wall, becomes blocked
|
||||
- Stuck timer triggers after 500ms
|
||||
- New target chosen (likely not in wall)
|
||||
- ✅ No infinite loop
|
||||
|
||||
### Edge Case 2: NPC Starts Outside Bounds
|
||||
|
||||
**Scenario**: NPC spawned outside configured patrol bounds
|
||||
|
||||
**Handling**:
|
||||
- Bounds auto-expand to include starting position (implemented in parseConfig)
|
||||
- ✅ NPC patrols normally
|
||||
- ✅ Console warning logged
|
||||
|
||||
**Test**:
|
||||
```javascript
|
||||
// Check if bounds were expanded
|
||||
const behavior = window.npcBehaviorManager.getBehavior('patrol_basic');
|
||||
const sprite = window.npcManager.npcs.get('patrol_basic')._sprite;
|
||||
console.log('Start pos:', sprite.x, sprite.y);
|
||||
console.log('Bounds:', behavior.config.patrol.worldBounds);
|
||||
// Bounds should include start position
|
||||
```
|
||||
|
||||
### Edge Case 3: Very Small Bounds
|
||||
|
||||
**Scenario**: Bounds smaller than NPC sprite
|
||||
|
||||
**Expected**:
|
||||
- NPC still picks targets within bounds
|
||||
- May appear to jitter if bounds very tiny
|
||||
- Should not crash
|
||||
|
||||
### Edge Case 4: Reached Target Exactly
|
||||
|
||||
**Scenario**: NPC reaches within 8px of target
|
||||
|
||||
**Expected**:
|
||||
- ✅ New target chosen immediately
|
||||
- ✅ No stopping at target (seamless transition)
|
||||
- ✅ Direction changes smoothly
|
||||
|
||||
### Edge Case 5: Direction Change During Collision
|
||||
|
||||
**Scenario**: Direction interval expires while NPC is stuck
|
||||
|
||||
**Expected**:
|
||||
- ✅ New target chosen
|
||||
- ✅ Stuck timer resets
|
||||
- ✅ NPC attempts to move to new target
|
||||
- ✅ If still blocked, stuck timer continues
|
||||
|
||||
---
|
||||
|
||||
## Common Issues
|
||||
|
||||
### Issue 1: NPC Not Moving
|
||||
|
||||
**Symptoms**: NPC stationary, not patrolling
|
||||
|
||||
**Possible Causes**:
|
||||
1. Patrol disabled: `behavior.config.patrol.enabled === false`
|
||||
2. No bounds configured: `behavior.config.patrol.worldBounds === null`
|
||||
3. Higher priority behavior active (face player, personal space)
|
||||
4. NPC stuck permanently (rare)
|
||||
|
||||
**Debug**:
|
||||
```javascript
|
||||
const behavior = window.npcBehaviorManager.getBehavior('npc_id');
|
||||
console.log('Patrol enabled:', behavior.config.patrol.enabled);
|
||||
console.log('Bounds:', behavior.config.patrol.worldBounds);
|
||||
console.log('Current state:', behavior.currentState);
|
||||
console.log('Patrol target:', behavior.patrolTarget);
|
||||
```
|
||||
|
||||
**Fix**:
|
||||
- Enable patrol: `window.npcGameBridge.setNPCPatrol('npc_id', true)`
|
||||
- Check state priority
|
||||
|
||||
---
|
||||
|
||||
### Issue 2: NPC Leaving Bounds
|
||||
|
||||
**Symptoms**: NPC wanders outside configured area
|
||||
|
||||
**Possible Causes**:
|
||||
1. Bounds in room coordinates, not world coordinates
|
||||
2. Bounds calculation error
|
||||
3. Collision pushing NPC out
|
||||
|
||||
**Debug**:
|
||||
```javascript
|
||||
const behavior = window.npcBehaviorManager.getBehavior('npc_id');
|
||||
const sprite = window.npcManager.npcs.get('npc_id')._sprite;
|
||||
const bounds = behavior.config.patrol.worldBounds;
|
||||
console.log('NPC pos:', sprite.x, sprite.y);
|
||||
console.log('Bounds:', bounds);
|
||||
console.log('In bounds?',
|
||||
sprite.x >= bounds.x && sprite.x <= bounds.x + bounds.width &&
|
||||
sprite.y >= bounds.y && sprite.y <= bounds.y + bounds.height
|
||||
);
|
||||
```
|
||||
|
||||
**Note**: Bounds are converted to world coordinates in parseConfig()
|
||||
|
||||
---
|
||||
|
||||
### Issue 3: NPC Getting Stuck
|
||||
|
||||
**Symptoms**: NPC stops moving for >1 second
|
||||
|
||||
**Possible Causes**:
|
||||
1. Stuck in corner with bad target
|
||||
2. Collision not resolving properly
|
||||
3. Stuck detection not working
|
||||
|
||||
**Debug**:
|
||||
```javascript
|
||||
const behavior = window.npcBehaviorManager.getBehavior('npc_id');
|
||||
const sprite = window.npcManager.npcs.get('npc_id')._sprite;
|
||||
console.log('Blocked:', sprite.body.blocked);
|
||||
console.log('Stuck timer:', behavior.stuckTimer);
|
||||
console.log('Target:', behavior.patrolTarget);
|
||||
```
|
||||
|
||||
**Expected**: Stuck timer should reach 500ms and reset
|
||||
|
||||
---
|
||||
|
||||
### Issue 4: Wrong Animation
|
||||
|
||||
**Symptoms**: Walk animation doesn't match direction
|
||||
|
||||
**Possible Causes**:
|
||||
1. Direction calculation error
|
||||
2. Animation not created (using fallback idle)
|
||||
3. FlipX not applied for left directions
|
||||
|
||||
**Debug**:
|
||||
```javascript
|
||||
const behavior = window.npcBehaviorManager.getBehavior('npc_id');
|
||||
const sprite = window.npcManager.npcs.get('npc_id')._sprite;
|
||||
console.log('Direction:', behavior.direction);
|
||||
console.log('Animation:', sprite.anims.currentAnim?.key);
|
||||
console.log('FlipX:', sprite.flipX);
|
||||
console.log('Velocity:', sprite.body.velocity);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
✅ **Phase 3 Complete When**:
|
||||
|
||||
1. [ ] Basic patrol works (random movement in bounds)
|
||||
2. [ ] Speed variations work correctly (fast/slow)
|
||||
3. [ ] Direction changes occur at configured intervals
|
||||
4. [ ] NPCs stay within configured bounds
|
||||
5. [ ] Stuck detection recovers from collisions
|
||||
6. [ ] Narrow area patrols work (corridors)
|
||||
7. [ ] Patrol + face player interaction works
|
||||
8. [ ] Small area patrol works without jittering
|
||||
9. [ ] Patrol can be toggled via Ink tags
|
||||
10. [ ] Walk animations match movement direction
|
||||
11. [ ] Performance acceptable with 8+ patrolling NPCs
|
||||
12. [ ] No console errors during patrol
|
||||
13. [ ] Edge cases handled gracefully
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
After Phase 3:
|
||||
- **Phase 4**: Personal Space behavior testing
|
||||
- **Phase 5**: Ink integration testing
|
||||
- **Phase 6**: Hostile visual feedback
|
||||
|
||||
---
|
||||
|
||||
**Document Status**: Test Guide v1.0
|
||||
**Last Updated**: 2025-11-09
|
||||
**Phase**: 3 - Patrol Behavior Testing
|
||||
1
scenarios/ink/test-npc.json
Normal file
1
scenarios/ink/test-npc.json
Normal file
@@ -0,0 +1 @@
|
||||
{"inkVersion":21,"root":[[["done",{"#n":"g-0"}],null],"done",{"start":["ev",{"VAR?":"conversation_count"},1,"+",{"VAR=":"conversation_count","re":true},"/ev","ev",{"VAR?":"npc_name"},"out","/ev","^: Hey there! This is conversation ","#","ev",{"VAR?":"conversation_count"},"out","/ev","^.","/#","\n","ev",{"VAR?":"npc_name"},"out","/ev","^: What can I help you with?","\n",{"->":"hub"},null],"hub":[["ev",{"VAR?":"asked_question"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n",["ev",{"^->":"hub.0.4.b.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^Introduce yourself","/str","/ev",{"*":".^.^.c-0","flg":22},{"s":["^once ",{"->":"$r","var":true},null]}],{"->":"hub.0.5"},{"c-0":["ev",{"^->":"hub.0.4.b.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"\n","ev","str","^Nice to meet you!","/str","/ev",{"VAR=":"npc_name","re":true},{"->":"introduction"},{"#f":5}]}]}],"nop","\n","ev",{"VAR?":"asked_question"},"/ev",[{"->":".^.b","c":true},{"b":["\n","ev","str","^Remind me about that question","/str","/ev",{"*":".^.c-0","flg":4},{"->":"hub.0.12"},{"c-0":["\n",{"->":"question_reminder"},null]}]}],[{"->":".^.b"},{"b":["\n","ev","str","^Ask a question","/str","/ev",{"*":".^.c-0","flg":4},{"->":"hub.0.12"},{"c-0":["\n",{"->":"question"},null]}]}],"nop","\n","ev",{"VAR?":"asked_about_passwords"},"/ev",[{"->":".^.b","c":true},{"b":["\n","ev","str","^Tell me more about passwords","/str","/ev",{"*":".^.c-0","flg":4},{"->":"hub.0.19"},{"c-0":["\n",{"->":"passwords_advanced"},null]}]}],[{"->":".^.b"},{"b":["\n","ev","str","^Ask about password security","/str","/ev",{"*":".^.c-0","flg":4},{"->":"hub.0.19"},{"c-0":["\n",{"->":"ask_passwords"},null]}]}],"nop","\n","ev","str","^Say hello","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Leave","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n",{"->":"greeting"},null],"c-1":["^ ","#","^exit_conversation","/#","\n","ev",{"VAR?":"npc_name"},"out","/ev","^: See you later!","\n",{"->":"hub"},null]}],null],"introduction":["ev",{"VAR?":"npc_name"},"out","/ev","^: Nice to meet you too! I'm ","ev",{"VAR?":"npc_name"},"out","/ev","^.","\n","ev",{"VAR?":"npc_name"},"out","/ev","^: Feel free to ask me anything.","\n",{"->":"hub"},null],"ask_passwords":["ev",true,"/ev",{"VAR=":"asked_about_passwords","re":true},"ev",{"VAR?":"npc_name"},"out","/ev","^: Passwords should be long and complex...","\n","ev",{"VAR?":"npc_name"},"out","/ev","^: Use at least 12 characters with mixed case and numbers.","\n",{"->":"hub"},null],"question_reminder":["ev",{"VAR?":"npc_name"},"out","/ev","^: As I said before, passwords should be strong and unique.","\n","ev",{"VAR?":"npc_name"},"out","/ev","^: Anything else?","\n",{"->":"hub"},null],"passwords_advanced":["ev",{"VAR?":"npc_name"},"out","/ev","^: For advanced security, use a password manager to generate unique passwords for each site.","\n","ev",{"VAR?":"npc_name"},"out","/ev","^: Never reuse passwords across different services.","\n",{"->":"hub"},null],"question":["ev",{"VAR?":"npc_name"},"out","/ev","^: That's a good question. Let me think about it...","\n","ev",{"VAR?":"npc_name"},"out","/ev","^: I'm not sure I have all the answers right now.","\n",{"->":"hub"},null],"greeting":["ev",{"VAR?":"npc_name"},"out","/ev","^: Hello to you too! Nice to chat with you.","\n",{"->":"hub"},null],"global decl":["ev","str","^NPC","/str",{"VAR=":"npc_name"},0,{"VAR=":"conversation_count"},false,{"VAR=":"asked_question"},false,{"VAR=":"asked_about_passwords"},"/ev","end",null]}],"listDefs":{}}
|
||||
60
scenarios/ink/test-patrol-toggle.ink
Normal file
60
scenarios/ink/test-patrol-toggle.ink
Normal file
@@ -0,0 +1,60 @@
|
||||
// Behavior Test - Patrol Toggle
|
||||
// Demonstrates toggling patrol mode via Ink tags
|
||||
|
||||
VAR is_patrolling = false
|
||||
|
||||
=== start ===
|
||||
# speaker:npc
|
||||
Hi! I'm the patrol toggle test NPC.
|
||||
|
||||
Right now I'm {is_patrolling: patrolling around | standing still}.
|
||||
|
||||
-> hub
|
||||
|
||||
=== hub ===
|
||||
* [Start patrolling]
|
||||
-> start_patrol
|
||||
|
||||
* [Stop patrolling]
|
||||
-> stop_patrol
|
||||
|
||||
* [Check status]
|
||||
-> check_status
|
||||
|
||||
+ [Exit] #exit_conversation
|
||||
# speaker:npc
|
||||
See you later!
|
||||
|
||||
-> hub
|
||||
|
||||
=== start_patrol ===
|
||||
# speaker:npc
|
||||
# patrol_mode:on
|
||||
~ is_patrolling = true
|
||||
|
||||
Okay, I'll start patrolling my area now!
|
||||
|
||||
Watch me walk around. I'll still face you if you approach while I'm patrolling.
|
||||
-> hub
|
||||
|
||||
=== stop_patrol ===
|
||||
# speaker:npc
|
||||
# patrol_mode:off
|
||||
~ is_patrolling = false
|
||||
|
||||
Alright, I'll stop patrolling and stay in one place.
|
||||
-> hub
|
||||
|
||||
=== check_status ===
|
||||
# speaker:npc
|
||||
Current status:
|
||||
- Patrolling: {is_patrolling: YES | NO}
|
||||
- Face Player: ENABLED
|
||||
- Patrol bounds: 3x3 tiles around my starting position
|
||||
|
||||
{is_patrolling:
|
||||
I'm currently walking around randomly within my patrol area.
|
||||
-
|
||||
I'm currently stationary, just facing you when you approach.
|
||||
}
|
||||
-> hub
|
||||
286
scenarios/test-npc-patrol.json
Normal file
286
scenarios/test-npc-patrol.json
Normal file
@@ -0,0 +1,286 @@
|
||||
{
|
||||
"scenario_brief": "Test scenario for NPC patrol behavior - Phase 3",
|
||||
"endGoal": "Test NPCs patrolling with various configurations and constraints",
|
||||
"startRoom": "test_patrol",
|
||||
|
||||
"player": {
|
||||
"id": "player",
|
||||
"displayName": "Test Agent",
|
||||
"spriteSheet": "hacker",
|
||||
"spriteTalk": "assets/characters/hacker-talk.png",
|
||||
"spriteConfig": {
|
||||
"idleFrameStart": 20,
|
||||
"idleFrameEnd": 23
|
||||
}
|
||||
},
|
||||
|
||||
"rooms": {
|
||||
"test_patrol": {
|
||||
"type": "room_office",
|
||||
"connections": {},
|
||||
"npcs": [
|
||||
{
|
||||
"id": "patrol_basic",
|
||||
"displayName": "Basic Patrol (Large Area)",
|
||||
"npcType": "person",
|
||||
"position": { "x": 3, "y": 3 },
|
||||
"spriteSheet": "hacker",
|
||||
"spriteConfig": {
|
||||
"idleFrameStart": 20,
|
||||
"idleFrameEnd": 23
|
||||
},
|
||||
"storyPath": "scenarios/ink/test-npc.json",
|
||||
"currentKnot": "start",
|
||||
"behavior": {
|
||||
"facePlayer": false,
|
||||
"patrol": {
|
||||
"enabled": true,
|
||||
"speed": 100,
|
||||
"changeDirectionInterval": 3000,
|
||||
"bounds": {
|
||||
"x": 64,
|
||||
"y": 64,
|
||||
"width": 192,
|
||||
"height": 192
|
||||
}
|
||||
}
|
||||
},
|
||||
"_comment": "Patrols large 6x6 tile area, changes direction every 3s, speed 100px/s"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "patrol_fast",
|
||||
"displayName": "Fast Patrol",
|
||||
"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": false,
|
||||
"patrol": {
|
||||
"enabled": true,
|
||||
"speed": 200,
|
||||
"changeDirectionInterval": 2000,
|
||||
"bounds": {
|
||||
"x": 224,
|
||||
"y": 64,
|
||||
"width": 128,
|
||||
"height": 128
|
||||
}
|
||||
}
|
||||
},
|
||||
"_comment": "Fast patrol (200px/s), quick direction changes (2s)"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "patrol_slow",
|
||||
"displayName": "Slow Patrol",
|
||||
"npcType": "person",
|
||||
"position": { "x": 3, "y": 8 },
|
||||
"spriteSheet": "hacker",
|
||||
"spriteConfig": {
|
||||
"idleFrameStart": 20,
|
||||
"idleFrameEnd": 23
|
||||
},
|
||||
"storyPath": "scenarios/ink/test-npc.json",
|
||||
"currentKnot": "start",
|
||||
"behavior": {
|
||||
"facePlayer": false,
|
||||
"patrol": {
|
||||
"enabled": true,
|
||||
"speed": 50,
|
||||
"changeDirectionInterval": 5000,
|
||||
"bounds": {
|
||||
"x": 64,
|
||||
"y": 224,
|
||||
"width": 128,
|
||||
"height": 96
|
||||
}
|
||||
}
|
||||
},
|
||||
"_comment": "Slow patrol (50px/s), long direction changes (5s)"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "patrol_small",
|
||||
"displayName": "Small Area Patrol",
|
||||
"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": false,
|
||||
"patrol": {
|
||||
"enabled": true,
|
||||
"speed": 80,
|
||||
"changeDirectionInterval": 2000,
|
||||
"bounds": {
|
||||
"x": 224,
|
||||
"y": 224,
|
||||
"width": 64,
|
||||
"height": 64
|
||||
}
|
||||
}
|
||||
},
|
||||
"_comment": "Small 2x2 tile area, frequent direction changes"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "patrol_with_face",
|
||||
"displayName": "Patrol + Face Player",
|
||||
"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,
|
||||
"facePlayerDistance": 96,
|
||||
"patrol": {
|
||||
"enabled": true,
|
||||
"speed": 100,
|
||||
"changeDirectionInterval": 4000,
|
||||
"bounds": {
|
||||
"x": 128,
|
||||
"y": 128,
|
||||
"width": 128,
|
||||
"height": 128
|
||||
}
|
||||
}
|
||||
},
|
||||
"_comment": "Patrols normally, but stops to face player when within 3 tiles"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "patrol_narrow_horizontal",
|
||||
"displayName": "Narrow Horizontal Patrol",
|
||||
"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": false,
|
||||
"patrol": {
|
||||
"enabled": true,
|
||||
"speed": 100,
|
||||
"changeDirectionInterval": 3000,
|
||||
"bounds": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 256,
|
||||
"height": 32
|
||||
}
|
||||
}
|
||||
},
|
||||
"_comment": "Patrols horizontal corridor (8 tiles wide, 1 tile tall)"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "patrol_narrow_vertical",
|
||||
"displayName": "Narrow Vertical Patrol",
|
||||
"npcType": "person",
|
||||
"position": { "x": 1, "y": 5 },
|
||||
"spriteSheet": "hacker",
|
||||
"spriteConfig": {
|
||||
"idleFrameStart": 20,
|
||||
"idleFrameEnd": 23
|
||||
},
|
||||
"storyPath": "scenarios/ink/test-npc.json",
|
||||
"currentKnot": "start",
|
||||
"behavior": {
|
||||
"facePlayer": false,
|
||||
"patrol": {
|
||||
"enabled": true,
|
||||
"speed": 100,
|
||||
"changeDirectionInterval": 3000,
|
||||
"bounds": {
|
||||
"x": 0,
|
||||
"y": 128,
|
||||
"width": 32,
|
||||
"height": 160
|
||||
}
|
||||
}
|
||||
},
|
||||
"_comment": "Patrols vertical corridor (1 tile wide, 5 tiles tall)"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "patrol_initially_disabled",
|
||||
"displayName": "Initially Disabled Patrol",
|
||||
"npcType": "person",
|
||||
"position": { "x": 10, "y": 5 },
|
||||
"spriteSheet": "hacker-red",
|
||||
"spriteConfig": {
|
||||
"idleFrameStart": 20,
|
||||
"idleFrameEnd": 23
|
||||
},
|
||||
"storyPath": "scenarios/ink/test-patrol-toggle.json",
|
||||
"currentKnot": "start",
|
||||
"behavior": {
|
||||
"facePlayer": true,
|
||||
"patrol": {
|
||||
"enabled": false,
|
||||
"speed": 100,
|
||||
"changeDirectionInterval": 3000,
|
||||
"bounds": {
|
||||
"x": 288,
|
||||
"y": 128,
|
||||
"width": 96,
|
||||
"height": 96
|
||||
}
|
||||
}
|
||||
},
|
||||
"_comment": "Starts with patrol disabled, can be enabled via Ink tag #patrol_mode:on"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "patrol_stuck_test",
|
||||
"displayName": "Stuck Detection Test",
|
||||
"npcType": "person",
|
||||
"position": { "x": 6, "y": 1 },
|
||||
"spriteSheet": "hacker",
|
||||
"spriteConfig": {
|
||||
"idleFrameStart": 20,
|
||||
"idleFrameEnd": 23
|
||||
},
|
||||
"storyPath": "scenarios/ink/test-npc.json",
|
||||
"currentKnot": "start",
|
||||
"behavior": {
|
||||
"facePlayer": false,
|
||||
"patrol": {
|
||||
"enabled": true,
|
||||
"speed": 120,
|
||||
"changeDirectionInterval": 4000,
|
||||
"bounds": {
|
||||
"x": 160,
|
||||
"y": 0,
|
||||
"width": 96,
|
||||
"height": 96
|
||||
}
|
||||
}
|
||||
},
|
||||
"_comment": "Patrol area with potential obstacles to test stuck detection (500ms timeout)"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user