diff --git a/planning_notes/npc/npc_behaviour/PHASE3_TEST_GUIDE.md b/planning_notes/npc/npc_behaviour/PHASE3_TEST_GUIDE.md new file mode 100644 index 0000000..a67f1f1 --- /dev/null +++ b/planning_notes/npc/npc_behaviour/PHASE3_TEST_GUIDE.md @@ -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 diff --git a/scenarios/ink/test-npc.json b/scenarios/ink/test-npc.json new file mode 100644 index 0000000..96cbb2e --- /dev/null +++ b/scenarios/ink/test-npc.json @@ -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":{}} \ No newline at end of file diff --git a/scenarios/ink/test-patrol-toggle.ink b/scenarios/ink/test-patrol-toggle.ink new file mode 100644 index 0000000..516ca0b --- /dev/null +++ b/scenarios/ink/test-patrol-toggle.ink @@ -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 diff --git a/scenarios/test-npc-patrol.json b/scenarios/test-npc-patrol.json new file mode 100644 index 0000000..b381da6 --- /dev/null +++ b/scenarios/test-npc-patrol.json @@ -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)" + } + ] + } + } +}