feat(npc): Enhance NPC interaction with room navigation events and refined greeting logic

This commit is contained in:
Z. Cliffe Schreuders
2025-10-31 16:31:07 +00:00
parent c80aa5b68d
commit 40da85dac4
6 changed files with 190 additions and 32 deletions

View File

@@ -121,14 +121,22 @@ VAR has_given_item = false
### 2. Define the Entry Points
Every Ink story needs two key knots:
Every Ink story needs a properly structured start flow to avoid repeating messages:
```ink
// Initial greeting - shown only on first contact
// State variable to track if greeting has been shown
VAR has_greeted = false
// Initial entry point - shows greeting once, then goes to menu
=== start ===
Hello! I'm here to help you with your mission. 👋
How are things going?
-> main_menu
{ has_greeted:
-> main_menu
- else:
Hello! I'm here to help you with your mission. 👋
How are things going?
~ has_greeted = true
-> main_menu
}
// Main menu - shown when returning to conversation
=== main_menu ===
@@ -137,10 +145,20 @@ How are things going?
+ [Say goodbye] -> goodbye
```
**Key Pattern**:
- `start` shows the initial greeting message and immediately redirects to `main_menu`
- `main_menu` presents only choices (no repeated text) since all messages are in conversation history
- All conversation knots should redirect to `main_menu` (not `start`) to avoid repeating the greeting
**Key Pattern for Avoiding Repeated Messages**:
- Add a `has_greeted` variable at the top of your Ink file
- `start` knot checks if greeting has been shown:
- If already greeted, skip directly to `main_menu`
- If not greeted, show greeting text, set `has_greeted = true`, then go to `main_menu`
- `main_menu` presents only choices (no text) since conversation history shows all previous messages
- All conversation knots redirect to `main_menu` (not `start`) to avoid re-triggering the greeting
- Event-triggered barks also redirect to `main_menu` for seamless conversation continuation
**Why This Works**:
- First contact: Player sees greeting, then choices
- After barks: Player sees bark message (in history), then choices - no repeated greeting
- Reopening conversation: Player sees full history, then choices - no repeated greeting
- The greeting appears in conversation history but never repeats as a new message
---
@@ -248,6 +266,21 @@ Excellent work on that challenge! 🎯
- ❌ Using `-> END` - prevents conversation from continuing
- ❌ Long messages - barks should be brief notifications
**Important: Avoiding Repeated Greetings**
When a bark redirects to `main_menu` (not `start`), the conversation flow works like this:
1. Player performs action (e.g., enters a room)
2. Bark notification appears with contextual message
3. Player clicks the bark to open conversation
4. Conversation shows:
- Original greeting (in history)
- Bark message (just clicked)
- Menu choices (from `main_menu`)
5. **No repeated greeting** because we skipped the `start` knot
If barks redirect to `start`, the `has_greeted` check prevents re-showing the greeting text, but it's cleaner to go straight to `main_menu`.
---
## Configuring NPCs in Scenario JSON
@@ -444,10 +477,12 @@ The JSON file is what gets loaded by the game engine.
### Conversation Design
1. **Use clear variable names**: `trust_level`, `has_given_keycard`, `knows_secret`
2. **Gate important actions behind trust**: Players should build rapport before getting help
3. **Provide multiple conversation paths**: Not everyone plays the same way
4. **Use emojis sparingly**: They add personality but shouldn't overwhelm
5. **Keep initial greeting brief**: Players want to get to choices quickly
2. **Always add `has_greeted` variable**: Prevents repeated greetings across sessions
3. **Gate important actions behind trust**: Players should build rapport before getting help
4. **Provide multiple conversation paths**: Not everyone plays the same way
5. **Use emojis sparingly**: They add personality but shouldn't overwhelm
6. **Keep initial greeting brief**: Players want to get to choices quickly
7. **Check `has_greeted` in start knot**: Skip directly to `main_menu` if already greeted
### Bark Design
@@ -555,11 +590,17 @@ VAR trust_level = 0
VAR has_given_advice = false
VAR mission_briefed = false
VAR rooms_discovered = 0
VAR has_greeted = false
=== start ===
Hey, I'm glad you're on this case. This is going to be tricky. 🕵️
Let me know if you need guidance.
-> main_menu
{ has_greeted:
-> main_menu
- else:
Hey, I'm glad you're on this case. This is going to be tricky. 🕵️
Let me know if you need guidance.
~ has_greeted = true
-> main_menu
}
=== main_menu ===
+ [What should I be looking for?] -> mission_briefing
@@ -705,7 +746,8 @@ When integrating a new NPC:
**Ink Story Creation**:
- [ ] Create `.ink` file in `scenarios/ink/`
- [ ] Define state variables at top of file
- [ ] Create `start` knot with initial greeting
- [ ] Add `has_greeted` variable to prevent repeated greetings
- [ ] Create `start` knot with greeting + `has_greeted` check
- [ ] Create `main_menu` knot with choices (no repeated text)
- [ ] Create conversation knots that redirect to `main_menu`
- [ ] Create event-triggered bark knots (also redirect to `main_menu`)

View File

@@ -53,6 +53,11 @@ import { initializeCollision, createWallCollisionBoxes, removeTilesUnderDoor, re
export let rooms = {};
export let currentRoom = '';
export let currentPlayerRoom = '';
// Track which rooms have been DISCOVERED by the player
// NOTE: "Discovered" means the player has ENTERED the room via door transition.
// This is separate from "revealed" (graphics visible). Rooms can be revealed
// (loaded for graphics/performance) without being discovered (player hasn't entered yet).
// This distinction is important for NPC event triggers like "room_discovered".
export let discoveredRooms = new Set();
// Helper function to check if a position overlaps with existing items
@@ -516,6 +521,10 @@ function loadRoom(roomId) {
console.log(`Lazy loading room: ${roomId}`);
createRoom(roomId, roomData, position);
// Reveal (make visible) but do NOT mark as discovered
// The room will only be marked as "discovered" when the player
// actually enters it via door transition
revealRoom(roomId);
}
@@ -527,6 +536,9 @@ export function initializeRooms(gameInstance) {
currentRoom = '';
currentPlayerRoom = '';
window.currentPlayerRoom = '';
// Clear discovered rooms on scenario load
// This ensures "first visit" detection works correctly for NPC events
discoveredRooms = new Set();
// Update global reference
window.discoveredRooms = discoveredRooms;
@@ -1603,6 +1615,19 @@ export function createRoom(roomId, roomData, position) {
}
export function revealRoom(roomId) {
// IMPORTANT: revealRoom() makes graphics VISIBLE but does NOT mark as DISCOVERED
//
// "Revealed" = graphics are loaded and visible (for rendering/performance)
// "Discovered" = player has actually ENTERED the room (for gameplay/events)
//
// This separation allows us to:
// 1. Preload/reveal rooms for performance without marking them as "visited"
// 2. Trigger "room_discovered" events when player first ENTERS a room
// 3. Keep "first visit" detection accurate for NPC reactions
//
// Rooms are marked as "discovered" in the door transition code, AFTER
// the room_discovered event is emitted.
if (rooms[roomId]) {
const room = rooms[roomId];
@@ -1637,9 +1662,10 @@ export function revealRoom(roomId) {
console.log(`No objects found in room ${roomId}`);
}
discoveredRooms.add(roomId);
// Update global reference
window.discoveredRooms = discoveredRooms;
// NOTE: We do NOT add to discoveredRooms here!
// Rooms are only marked as "discovered" when the player actually enters them
// via door transition. This allows revealRoom() to be used for preloading/visibility
// without affecting the "first visit" detection for NPC events.
}
currentRoom = roomId;
}
@@ -1662,13 +1688,23 @@ export function updatePlayerRoom() {
currentPlayerRoom = doorTransitionRoom;
window.currentPlayerRoom = doorTransitionRoom;
// Reveal the room if not already discovered
// Check if this is the first time the player has ENTERED this room
// NOTE: The room may already be "revealed" (graphics visible) from preloading,
// but we only mark it as "discovered" when the player actually walks through
// a door into it. This keeps first-visit detection accurate for NPC events.
const isFirstVisit = !discoveredRooms.has(doorTransitionRoom);
if (isFirstVisit) {
// Reveal graphics if needed (may already be revealed from preloading)
revealRoom(doorTransitionRoom);
}
// Emit NPC event for room entry
console.log(`🚪 Door transition detected: ${previousRoom}${doorTransitionRoom}`);
console.log(` eventDispatcher exists: ${!!window.eventDispatcher}`);
console.log(` previousRoom !== doorTransitionRoom: ${previousRoom !== doorTransitionRoom}`);
console.log(` isFirstVisit: ${isFirstVisit}`);
if (window.eventDispatcher && previousRoom !== doorTransitionRoom) {
console.log(`🚪 Emitting room_entered event: ${doorTransitionRoom} (firstVisit: ${isFirstVisit})`);
window.eventDispatcher.emit('room_entered', {
@@ -1691,6 +1727,16 @@ export function updatePlayerRoom() {
roomId: doorTransitionRoom,
previousRoom: previousRoom
});
// Mark as discovered AFTER emitting the event
// This is the ONLY place where rooms are added to discoveredRooms!
// By marking discovered here (not in revealRoom), we ensure:
// 1. The first door transition into a room triggers room_discovered
// 2. NPCs can react to the player's first visit
// 3. Subsequent visits don't re-trigger the event
discoveredRooms.add(doorTransitionRoom);
window.discoveredRooms = discoveredRooms;
console.log(`✅ Marked room ${doorTransitionRoom} as discovered`);
}
if (previousRoom) {
@@ -1699,6 +1745,8 @@ export function updatePlayerRoom() {
nextRoom: doorTransitionRoom
});
}
} else {
console.warn(`⚠️ NOT emitting room events - eventDispatcher: ${!!window.eventDispatcher}, previousRoom: ${previousRoom}, doorTransitionRoom: ${doorTransitionRoom}`);
}
// Player depth is now handled by the simplified updatePlayerDepth function in player.js

View File

@@ -325,10 +325,22 @@
- [x] Verify bark frequency limits (maxTriggers) ✅ Implemented!
6. **Polish UI/UX** 🔄 IN PROGRESS
- [ ] Sound effects (message_received.wav, bark_notification.wav)
- [ ] Better NPC avatars (32x32px pixel art)
- [x] Room navigation events (Priority 2) ✅ COMPLETE (2024-10-31)
- [x] Added `room_entered`, `room_entered:${roomId}`, `room_discovered`, `room_exited` events
- [x] Created Ink reaction knots (on_room_discovered, on_ceo_office_entered)
- [x] Added event mappings to scenario JSON
- [x] Fixed conversation flow (main_menu vs start)
- [x] Updated unlock messages to be generic (key or lockpick)
- [ ] Sound effects (Priority 1)
- [ ] Message received sound - assets/sounds/message_received.mp3
- [ ] NPC avatars (Priority 3)
- [ ] Create default avatars (helper, adversary, neutral)
- [ ] Add avatar support in scenarios
- [ ] More game events (Priority 2 continued)
- [ ] objective_completed
- [ ] evidence_collected
- [ ] player_detected
- [ ] Objective notification system
- [ ] Secret/discovery UI
- [ ] Achievement/progress tracking
7. **Performance optimization** ⏳ NEXT
@@ -338,8 +350,33 @@
- [ ] Optimize bark rendering for multiple simultaneous barks
---
**Last Updated:** 2024-10-31 (Phase 4 Event-Driven Reactions COMPLETE & TESTED)
**Status:** Phase 4 Complete ✅ - Moving to Phase 5: Polish & Additional Features
**Last Updated:** 2024-10-31 (Phase 5 Room Navigation Events COMPLETE)
**Status:** Phase 5 In Progress - Room navigation events ✅, moving to sound effects and additional events
## Recent Improvements (2024-10-31 - Phase 5)
### ✅ Room Navigation Events (Priority 2 - Partial)
- **Event emissions in rooms.js**:
- `room_entered` - General room change event
- `room_entered:${roomId}` - Specific room entry
- `room_discovered` - First-time room visits
- `room_exited` - Leaving a room
- **Ink reactions in helper-npc.ink**:
- `on_room_discovered` - Generic exploration encouragement
- `on_ceo_office_entered` - Special CEO office reaction with trust reward
- **Event mappings configured**:
- `room_discovered` with 15s cooldown, max 5 triggers
- `room_entered:ceo` one-time only reaction
- **Conversation flow refinements**:
- Split `start` and `main_menu` knots
- Barks redirect to `main_menu` (not `start`) to avoid repeated greeting
- "What can I do for you?" only appears in initial greeting
- **Message updates**:
- Unlock messages now generic (work for key or lockpick)
- Lockpicking success bark doesn't assume method
- **Documentation created**:
- `NPC_INTEGRATION_GUIDE.md` - Comprehensive guide for adding NPCs to scenarios
- Includes phone setup, Ink structure, event mappings, testing checklist
## Recent Improvements (2025-10-30)

View File

@@ -75,6 +75,12 @@
"targetKnot": "on_item_found",
"cooldown": 20000
},
{
"eventPattern": "room_entered",
"targetKnot": "on_room_entered",
"cooldown": 45000,
"maxTriggers": 3
},
{
"eventPattern": "room_discovered",
"targetKnot": "on_room_discovered",

View File

@@ -7,11 +7,17 @@ VAR has_unlocked_ceo = false
VAR has_given_lockpick = false
VAR saw_lockpick_used = false
VAR saw_door_unlock = false
VAR has_greeted = false
=== start ===
Hey there! I'm here to help you out if you need it. 👋
What can I do for you?
-> main_menu
{ has_greeted:
-> main_menu
- else:
Hey there! I'm here to help you out if you need it. 👋
What can I do for you?
~ has_greeted = true
-> main_menu
}
=== main_menu ===
+ [Who are you?] -> who_are_you
@@ -139,10 +145,29 @@ That door's locked tight. You'll need to find a way to unlock it. 🔒
}
-> main_menu
// Triggered when player enters any room (general progress check)
=== on_room_entered ===
{ has_unlocked_ceo:
Keep searching for that evidence! 🔍
- else:
{ trust_level >= 1:
You're making progress through the building. 🚶
- else:
Exploring new areas... 🚶
}
}
-> main_menu
// Triggered when player discovers a new room for the first time
=== on_room_discovered ===
{ trust_level >= 1:
Interesting! You've found a new area. Be careful exploring. 🗺️
{ trust_level >= 2:
Great find! This new area might have what we need. 🗺️
- else:
{ trust_level >= 1:
Interesting! You've found a new area. Be careful exploring. 🗺️
- else:
A new room... wonder what's inside. 🚪
}
}
-> main_menu

File diff suppressed because one or more lines are too long