24 KiB
NPC Integration Guide
A comprehensive guide for integrating NPCs into Break Escape scenarios. This document explains how to create Ink stories, structure conversation knots, hook up game events, and configure NPCs in scenario JSON files.
Table of Contents
- Overview
- Creating the Ink Story
- Structuring Conversation Knots
- Event-Triggered Barks
- Configuring NPCs in Scenario JSON
- Available Game Events
- Best Practices
- Testing Your NPC
Overview
Break Escape NPCs use the Ink narrative scripting language for conversations and event-driven reactions. The NPC system supports:
- Full conversations with branching dialogue choices
- Event-triggered barks (short messages) that react to player actions
- State management using Ink variables for trust, progression, etc.
- Action tags (
# unlock_door:id,# give_item:id) to trigger game effects - Conditional logic for dynamic responses based on game state
NPCs communicate through the phone chat minigame interface, appearing as contacts in the player's phone.
Quick Start: Adding NPCs to Your Scenario
Before diving into Ink scripting, you need to set up three things in your scenario JSON:
1. Add the Phone to Player's Inventory
NPCs are accessed through the player's phone, so add it to startItemsInInventory:
{
"startItemsInInventory": [
{
"type": "phone",
"name": "Your Phone",
"takeable": true,
"phoneId": "player_phone",
"npcIds": ["helper_contact", "tech_support", "informant"],
"observations": "Your personal phone with some interesting contacts"
}
]
}
Phone Properties:
type: Must be"phone"for the game to recognize itname: Display name shown in inventorytakeable: Set totrue(phone is portable)phoneId: Unique identifier (typically"player_phone")npcIds: Array of NPC IDs that appear as contacts in this phoneobservations: Description shown when examining the phone
2. Configure NPCs in the Scenario
Add an npcs array at the root level of your scenario JSON:
{
"scenario_brief": "Your mission...",
"startRoom": "lobby",
"startItemsInInventory": [ /* phone with npcIds */ ],
"npcs": [
{
"id": "helper_contact",
"name": "Helpful Contact",
"storyPath": "scenarios/ink/helper-npc.json",
"phoneNumber": "555-0123",
"description": "A friendly insider who can help you",
"startingState": "available",
"phoneId": "player_phone",
"npcType": "phone",
"eventMappings": [ /* covered later */ ]
}
]
}
Critical NPC Properties:
id: Must match an entry in the phone'snpcIdsarrayphoneId: Must match thephoneIdof the phone item (e.g.,"player_phone")npcType: Set to"phone"for phone-based NPCsstoryPath: Path to compiled Ink JSON file (not.inkfile!)
3. Create and Compile the Ink Story
Create your Ink story file (covered in detail below), then compile it to JSON:
cd scenarios/ink
/path/to/inklecate -j -o helper-npc.json helper-npc.ink
Important: The storyPath in your scenario JSON must point to the .json file, not the .ink source file.
Creating the Ink Story
1. Create the Ink File
Create a new .ink file in scenarios/ink/ directory:
// my-npc.ink
// Description of the NPC's role and personality
// State variables - track progression and decisions
VAR trust_level = 0
VAR has_unlocked_door = false
VAR has_given_item = false
2. Define the Entry Points
Every Ink story needs a properly structured start flow to avoid repeating messages:
// State variable to track if greeting has been shown
VAR has_greeted = false
// Initial entry point - shows greeting once, then goes to menu
=== start ===
{ 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 ===
+ [Ask for help] -> ask_help
+ [Request an item] -> request_item
+ [Say goodbye] -> goodbye
Key Pattern for Avoiding Repeated Messages:
- Add a
has_greetedvariable at the top of your Ink file startknot 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 tomain_menu
- If already greeted, skip directly to
main_menupresents only choices (no text) since conversation history shows all previous messages- All conversation knots redirect to
main_menu(notstart) to avoid re-triggering the greeting - Event-triggered barks also redirect to
main_menufor 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
Structuring Conversation Knots
Basic Conversation Knot
=== ask_help ===
{ trust_level >= 1:
I can help you! What do you need?
~ trust_level = trust_level + 1
- else:
I don't know you well enough yet. Talk to me more first.
}
-> main_menu
Knot with Action Tags
Use # tags to trigger game effects:
=== unlock_door_for_player ===
{ trust_level >= 2:
Alright, I'll unlock that door for you.
~ has_unlocked_door = true
# unlock_door:office_door
There you go! It's open now. 🚪
- else:
I need to trust you more before I can do that.
}
-> main_menu
Available Action Tags:
# unlock_door:doorId- Unlocks a specific door# give_item:itemType- Adds item to player's inventory
Conditional Choices
Show choices only when conditions are met:
=== main_menu ===
+ [Ask for help] -> ask_help
+ {trust_level >= 1} [Request special item] -> give_special_item
+ {has_given_item} [Thanks for the item!] -> thank_you
+ [Goodbye] -> goodbye
Ending Conversations
=== goodbye ===
Good luck out there! Contact me if you need anything.
-> END
Event-Triggered Barks
Barks are short messages triggered automatically by game events. They appear as notifications and clicking them opens the full conversation.
Creating Bark Knots
// ==========================================
// EVENT-TRIGGERED BARKS
// These knots are triggered automatically by the NPC system
// Note: These redirect to 'main_menu' so clicking the bark opens full conversation
// ==========================================
// Triggered when player unlocks a door
=== on_door_unlocked ===
{ has_unlocked_door:
Another door open! You're doing great. 🚪✓
- else:
Nice! You got through that door.
}
-> main_menu
// Triggered when player picks up an item
=== on_item_pickup ===
Good find! That could be useful for your mission. 📦
-> main_menu
// Triggered when player completes a minigame
=== on_minigame_complete ===
Excellent work on that challenge! 🎯
~ trust_level = trust_level + 1
-> main_menu
Critical Bark Patterns:
- ✅ Always redirect to
main_menu(notstartorEND) - ✅ Keep messages short (1-2 lines)
- ✅ Use emojis for visual interest
- ✅ Can update variables (
~ trust_level = trust_level + 1) - ✅ Can use conditional logic to vary messages
Common Mistakes:
- ❌ Redirecting to
start- causes greeting to repeat - ❌ 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:
- Player performs action (e.g., enters a room)
- Bark notification appears with contextual message
- Player clicks the bark to open conversation
- Conversation shows:
- Original greeting (in history)
- Bark message (just clicked)
- Menu choices (from
main_menu)
- No repeated greeting because we skipped the
startknot
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
Complete NPC Configuration
Each NPC in the npcs array requires the following properties:
{
"id": "helper_contact",
"name": "Helpful Contact",
"storyPath": "scenarios/ink/helper-npc.json",
"phoneNumber": "555-0123",
"description": "A friendly insider who can help you",
"startingState": "available",
"phoneId": "player_phone",
"npcType": "phone",
"eventMappings": [
{
"eventPattern": "door_unlocked",
"targetKnot": "on_door_unlocked",
"cooldown": 30000
}
]
}
NPC Property Reference:
| Property | Type | Required | Description |
|---|---|---|---|
id |
string | ✅ | Unique NPC identifier, must match entry in phone's npcIds array |
name |
string | ✅ | Display name shown in phone contacts list |
storyPath |
string | ✅ | Path to compiled Ink JSON file (e.g., "scenarios/ink/npc-name.json") |
phoneNumber |
string | ✅ | Phone number displayed in contact (e.g., "555-0123") |
description |
string | ✅ | Brief description of NPC shown in contact details |
startingState |
string | ✅ | Initial availability ("available", "locked", or "hidden") |
phoneId |
string | ✅ | Must match the phoneId of the phone item containing this NPC |
npcType |
string | ✅ | Set to "phone" for phone-based NPCs |
eventMappings |
array | ❌ | Array of event-to-bark mappings (see Event Mapping section) |
Starting State Options:
"available"- NPC appears in contacts immediately and can be contacted"locked"- NPC appears but cannot be contacted until unlocked by game event"hidden"- NPC doesn't appear until revealed by game event
Linking NPCs to Phones
For NPCs to appear in a phone, both the phone item and NPC config must reference each other:
In the phone item (startItemsInInventory):
{
"type": "phone",
"phoneId": "player_phone",
"npcIds": ["helper_contact", "tech_support"]
}
In each NPC config (npcs array):
{
"id": "helper_contact",
"phoneId": "player_phone"
}
This two-way linkage ensures NPCs appear in the correct phone and allows for scenarios with multiple phones.
Event Mapping Configuration
Each event mapping connects a game event to an Ink knot:
{
"eventPattern": "room_entered:ceo",
"targetKnot": "on_ceo_office_entered",
"cooldown": 15000,
"maxTriggers": 1,
"condition": "data.firstVisit === true"
}
Event Mapping Properties:
-
eventPattern(required): Event name to listen for- Can use wildcards:
item_picked_up:*matches any item - Can be specific:
room_entered:ceomatches only CEO room
- Can use wildcards:
-
targetKnot(required): Name of Ink knot to trigger (without===) -
cooldown(optional): Milliseconds before this bark can trigger again- Default: 0 (can trigger immediately)
- Recommended: 10000-30000 for most barks
-
maxTriggers(optional): Maximum times this bark can ever trigger- Default: unlimited
- Use
1for one-time reactions - Use
3-5to avoid spam on repeated actions
-
condition(optional): JavaScript expression that must evaluate totrue- Has access to
dataobject from event - Example:
"data.objectType === 'desk_ceo'" - Example:
"data.firstVisit === true"
- Has access to
-
onceOnly(optional): Shorthand for"maxTriggers": 1- Use for unique milestone reactions
3. Compile Ink to JSON
After creating/editing your .ink file, compile it:
cd scenarios/ink
/path/to/inklecate -j -o my-npc.json my-npc.ink
The JSON file is what gets loaded by the game engine.
Available Game Events
Player Actions
| Event Pattern | Data Properties | Description |
|---|---|---|
item_picked_up:* |
itemType, itemName |
Player picks up any item |
item_picked_up:lockpick_set |
itemType, itemName |
Player picks up specific item type |
object_interacted |
objectType, objectName |
Player interacts with object |
Room Navigation
| Event Pattern | Data Properties | Description |
|---|---|---|
room_entered |
roomId, previousRoom, firstVisit |
Player enters any room |
room_entered:ceo |
roomId, previousRoom, firstVisit |
Player enters specific room |
room_discovered |
roomId, previousRoom |
Player enters room for first time |
room_exited |
roomId, nextRoom |
Player leaves a room |
Unlocking & Doors
| Event Pattern | Data Properties | Description |
|---|---|---|
door_unlocked |
doorId, targetRoom, unlockMethod |
Any door unlocked |
door_unlock_attempt |
doorId, success |
Player tries to unlock door |
item_unlocked |
objectType, objectName, unlockMethod |
Container/object unlocked |
Minigames
| Event Pattern | Data Properties | Description |
|---|---|---|
minigame_completed |
minigameType, success, data |
Minigame finished successfully |
minigame_completed:lockpicking |
minigameType, success, data |
Specific minigame completed |
minigame_failed |
minigameType, reason |
Minigame failed |
Example Event Mappings
{
"eventMappings": [
{
"eventPattern": "item_picked_up:lockpick_set",
"targetKnot": "on_lockpick_pickup",
"cooldown": 5000,
"onceOnly": true
},
{
"eventPattern": "minigame_completed:lockpicking",
"targetKnot": "on_lockpick_success",
"cooldown": 20000
},
{
"eventPattern": "room_discovered",
"targetKnot": "on_room_discovered",
"cooldown": 15000,
"maxTriggers": 5
},
{
"eventPattern": "object_interacted",
"targetKnot": "on_ceo_desk_interact",
"condition": "data.objectType === 'desk_ceo'",
"cooldown": 10000
}
]
}
Best Practices
Conversation Design
- Use clear variable names:
trust_level,has_given_keycard,knows_secret - Always add
has_greetedvariable: Prevents repeated greetings across sessions - Gate important actions behind trust: Players should build rapport before getting help
- Provide multiple conversation paths: Not everyone plays the same way
- Use emojis sparingly: They add personality but shouldn't overwhelm
- Keep initial greeting brief: Players want to get to choices quickly
- Check
has_greetedin start knot: Skip directly tomain_menuif already greeted
Bark Design
- Keep barks short: 1-2 sentences maximum
- Make barks contextual: Reference what the player just did
- Use cooldowns: Prevent spam (15-30 seconds typically)
- Limit repetition: Use
maxTriggersto cap how many times a bark can appear - Vary messages: Use conditionals to show different reactions based on state
- Always redirect to main_menu: Never use
-> startor-> ENDin bark knots
Event Mapping Strategy
- Start with key milestones: First item pickup, entering important rooms
- Add context-specific reactions: Different messages for different rooms/items
- Use conditions for precision:
data.objectType === 'specific_object' - Balance frequency: Too many barks = annoying, too few = feels disconnected
- Test trigger limits: Use
maxTriggersto prevent spam on repeated actions
State Management
- Track important decisions: Variables for what player has learned/received
- Use trust/reputation systems: Let player build relationship over time
- Reference past actions: Show the NPC remembers previous interactions
- Unlock new options: Add conditional choices as trust increases
Testing Your NPC
1. Verify Compilation
cd scenarios/ink
/path/to/inklecate -j -o your-npc.json your-npc.ink
Should output:
{"compile-success": true}
{"issues":[]}
2. Check Scenario Configuration
- NPC listed in
npcsarray storyPathpoints to compiled.jsonfile (not.ink)- All event mappings reference valid knot names
- Event patterns match available game events
3. In-Game Testing
Test Initial Contact:
- Open phone
- Find NPC in contacts
- Verify greeting appears
- Check all conversation choices work
Test Event-Triggered Barks:
- Perform actions that should trigger barks (pick up item, unlock door, etc.)
- Verify bark notification appears
- Click bark to open conversation
- Ensure conversation continues from bark (no repeated greeting)
- Test cooldowns (same action twice quickly)
- Test maxTriggers (repeat action beyond limit)
Test Conditional Logic:
- Try choices that require trust before building trust
- Build trust through conversation
- Verify new choices appear
- Test action tags (door unlocking, item giving)
4. Common Issues
Barks repeat greeting when clicked:
- ❌ Bark knot uses
-> start - ✅ Change to
-> main_menu
Bark prevents conversation from continuing:
- ❌ Bark knot uses
-> END - ✅ Change to
-> main_menu
Events not triggering barks:
- Check
eventPatternmatches actual event name - Verify event is being emitted (check browser console with debug on)
- Check cooldown hasn't blocked the bark
- Verify condition evaluates to true
Compilation errors:
- Check for duplicate
===knot declarations - Ensure all knots have at least one line of content
- Verify all
->redirects point to valid knot names - Check for unmatched braces in conditional logic
Example: Complete NPC Implementation
File: scenarios/ink/mentor-npc.ink
// mentor-npc.ink
// An experienced security professional guiding the player
VAR trust_level = 0
VAR has_given_advice = false
VAR mission_briefed = false
VAR rooms_discovered = 0
VAR has_greeted = false
=== start ===
{ 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
+ [Can you give me some advice?] -> get_advice
+ {trust_level >= 2} [I need help with the server room] -> server_room_help
+ [I'll check back later] -> goodbye
=== mission_briefing ===
{ mission_briefed:
Remember: find evidence of the data breach, avoid detection, get out clean.
-> main_menu
- else:
Your objective is to find evidence of the data breach without getting caught.
Look for documents, logs, anything that proves what happened.
~ mission_briefed = true
~ trust_level = trust_level + 1
-> main_menu
}
=== get_advice ===
{ has_given_advice:
I told you - check the CEO's computer and look for financial records.
-> main_menu
- else:
My sources say the CEO's office has what you need.
But you'll need to get through security first.
~ has_given_advice = true
~ trust_level = trust_level + 1
-> main_menu
}
=== server_room_help ===
The server room door has a biometric lock. You'll need an authorized fingerprint.
Try to find a way to lift prints from someone with access.
# unlock_door:server_room
Actually, I just remotely disabled that lock for you. Move quickly! ⚡
~ trust_level = trust_level + 2
-> main_menu
=== goodbye ===
Stay safe. Contact me if things get dicey.
-> END
// ==========================================
// EVENT-TRIGGERED BARKS
// ==========================================
=== on_first_item ===
Good thinking! Collecting evidence is key. 📋
-> main_menu
=== on_room_discovered ===
~ rooms_discovered = rooms_discovered + 1
{ rooms_discovered >= 3:
You're doing great exploring! Keep mapping out the building. 🗺️
- else:
Nice, you found a new area. Stay alert. 👀
}
-> main_menu
=== on_lockpick_success ===
{ trust_level >= 1:
Impressive lockpicking! You've got skills. 🔓
- else:
You picked that lock? Interesting... you're more capable than I thought.
~ trust_level = trust_level + 1
}
-> main_menu
=== on_security_alert ===
Careful! Security might be onto you. Lay low for a bit. 🚨
-> main_menu
File: scenarios/my_mission.json (excerpt)
{
"scenario_brief": "Infiltrate the corporation and find evidence of the data breach",
"startRoom": "lobby",
"startItemsInInventory": [
{
"type": "phone",
"name": "Your Phone",
"takeable": true,
"phoneId": "player_phone",
"npcIds": ["mentor"],
"observations": "Your personal phone with a secure contact"
}
],
"npcs": [
{
"id": "mentor",
"name": "The Mentor",
"storyPath": "scenarios/ink/mentor-npc.json",
"phoneNumber": "555-0199",
"description": "Your experienced contact",
"startingState": "available",
"phoneId": "player_phone",
"npcType": "phone",
"eventMappings": [
{
"eventPattern": "item_picked_up:*",
"targetKnot": "on_first_item",
"onceOnly": true
},
{
"eventPattern": "room_discovered",
"targetKnot": "on_room_discovered",
"cooldown": 20000,
"maxTriggers": 5
},
{
"eventPattern": "minigame_completed:lockpicking",
"targetKnot": "on_lockpick_success",
"cooldown": 30000
},
{
"eventPattern": "security_alert",
"targetKnot": "on_security_alert",
"cooldown": 60000
}
]
}
]
}
Summary Checklist
When integrating a new NPC:
Scenario Setup:
- Add phone item to
startItemsInInventorywithphoneIdandnpcIdsarray - Add NPC to scenario's
npcsarray - Ensure NPC's
idmatches entry in phone'snpcIdsarray - Ensure NPC's
phoneIdmatches phone item'sphoneId - Set NPC's
npcTypeto"phone" - Configure
startingState("available","locked", or"hidden")
Ink Story Creation:
- Create
.inkfile inscenarios/ink/ - Define state variables at top of file
- Add
has_greetedvariable to prevent repeated greetings - Create
startknot with greeting +has_greetedcheck - Create
main_menuknot with choices (no repeated text) - Create conversation knots that redirect to
main_menu - Create event-triggered bark knots (also redirect to
main_menu) - Use action tags (
# unlock_door:id,# give_item:id) where needed - Compile Ink to JSON using inklecate
- Verify
storyPathin scenario points to compiled.jsonfile
Event Configuration:
- Add event mappings to NPC config in scenario JSON
- Configure appropriate cooldowns and maxTriggers for each event
- Add conditions for context-specific barks
Testing:
- Test initial conversation in-game
- Verify NPC appears in phone contacts
- Test all event-triggered barks
- Verify bark-to-conversation flow works smoothly
- Check conditional logic and state changes
- Test action tags (door unlocking, item giving)
Additional Resources
- Ink Documentation: https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md
- Example NPCs: See
scenarios/ink/helper-npc.inkfor complete working example - Event Reference: See
js/systems/event-dispatcher.jsfor all available events - NPC Manager Code: See
js/systems/npc-manager.jsfor implementation details
Last Updated: Phase 4 Implementation Complete (Event-Driven NPC Reactions)