- Introduced a data-driven global variable system to manage narrative state across NPC interactions. - Added support for global variables in scenario JSON, allowing for easy extension and management. - Implemented synchronization of global variables between Ink stories and the game state, ensuring real-time updates across conversations. - Enhanced state persistence, allowing global variables to survive page reloads and be restored during conversations. - Created comprehensive documentation and testing guides to facilitate usage and verification of the new system.
10 KiB
Global Ink Variables System
Overview
This document describes the global variable system that allows narrative state to be shared across all NPC conversations in a scenario. Global variables are stored in window.gameState.globalVariables and are automatically synced to all loaded Ink stories.
How It Works
Single Source of Truth
window.gameState.globalVariables is the authoritative store for all global narrative state. When a variable changes in any NPC's story, it updates here and is then synced to all other loaded stories.
Data Flow
┌─────────────────────────────────┐
│ window.gameState.globalVariables│ ← Single source of truth
│ { player_joined_organization... }│
└──────────────┬──────────────────┘
│
┌───────┴────────┐
│ On Load/Sync │
└───────┬────────┘
↓
┌──────────────────────┐
│ NPC Ink Stories │
│ - test_npc_back │
│ - equipment_officer │
│ - helper_npc │
└──────────────────────┘
Initialization Flow
-
Game Start (
js/core/game.js-create())- Scenario JSON is loaded with
globalVariablessection window.gameState.globalVariablesis initialized from scenario defaults
- Scenario JSON is loaded with
-
Story Load (
js/systems/npc-manager.js-getInkEngine())- Story JSON is compiled from Ink source
- Auto-discovers
global_*variables not in scenario - Syncs all global variables FROM window.gameState INTO the story
- Sets up variable change listener to sync back
-
Variable Change Detection (
js/systems/npc-conversation-state.js)- Ink's
variableChangedEventfires when any variable changes - If variable is global, updates window.gameState
- Broadcasts change to all other loaded stories
- Ink's
Declaring Global Variables
Method 1: Scenario JSON (Recommended)
Add a globalVariables section to your scenario file:
{
"scenario_brief": "My Scenario",
"globalVariables": {
"player_joined_organization": false,
"main_quest_complete": false,
"player_reputation": 0
},
"startRoom": "lobby",
...
}
Advantages:
- Centralized location for all narrative state
- Visible to designers and developers
- Type-safe (defaults define types)
- Clear which variables are shared
Method 2: Naming Convention (Fallback)
Add variables starting with global_ to any Ink file:
VAR global_research_complete = false
VAR global_alliance_formed = false
Advantages:
- Quick prototyping without editing scenario file
- Third-party Ink files can declare their own globals
- Graceful degradation for scenarios without globalVariables section
Using Global Variables in Ink
Global variables are automatically synced to Ink stories on load. Just declare them with the same name:
// Will be synced from window.gameState.globalVariables automatically
VAR player_joined_organization = false
=== check_status ===
{player_joined_organization:
This NPC recognizes you as a member!
- else:
Welcome, outsider.
}
Conditional Choice Display
To show/hide choices based on global variables, use the conditional syntax directly in choice brackets:
// Shows this choice only if player_joined_organization is true
+ {player_joined_organization} [Show me everything]
-> show_inventory
// Regular choice always visible
* [Show me specialist items]
-> show_filtered
Important: The syntax is + {variable} [choice text], NOT {variable: + [choice text]}
Accessing Global Variables from JavaScript/Phaser
Read global variables:
const hasJoined = window.gameState.globalVariables.player_joined_organization;
Write global variables (syncs automatically to next conversation):
window.gameState.globalVariables.player_joined_organization = true;
Get all global variables:
console.log(window.gameState.globalVariables);
How State Persistence Works
When an NPC conversation ends:
npcConversationStateManager.saveNPCState()captures:- Full story state (if mid-conversation)
- NPC-specific variables only
- Snapshot of global variables
On next conversation:
npcConversationStateManager.restoreNPCState():- Restores global variables first
- Loads full story state or just variables
- Syncs globals into the story
Critical Syncing Points
For global variables to work correctly, syncing must happen at specific times:
-
After Player Choice (
person-chat-minigame.js-handleChoice())- Reads all global variables that changed in the Ink story
- Updates
window.gameState.globalVariables - Broadcasts changes to other loaded stories
-
Before Showing Dialogue (
person-chat-minigame.js-start())- Re-syncs all globals into the current story
- Critical because Ink evaluates conditionals at
continue()time - Ensures conditional choices reflect current state from other NPCs
-
On Story Load (
npc-manager.js-getInkEngine())- Initial sync of globals into newly loaded story
- Sets up listeners for future changes
Implementation Details
Key Files
-
js/main.js(line 46-52)- Initializes
window.gameStatewithglobalVariables
- Initializes
-
js/core/game.js(line 461-467)- Loads scenario and initializes
window.gameState.globalVariables
- Loads scenario and initializes
-
js/systems/npc-conversation-state.jsgetGlobalVariableNames()- Lists all global variablesisGlobalVariable(name)- Checks if a variable is globaldiscoverGlobalVariables(story)- Auto-discoversglobal_*variablessyncGlobalVariablesToStory(story)- Syncs FROM window → InksyncGlobalVariablesFromStory(story)- Syncs FROM Ink → windowobserveGlobalVariableChanges(story, npcId)- Sets up listenersbroadcastGlobalVariableChange()- Propagates changes to all stories
-
js/systems/npc-manager.js(line 702-712)- Calls sync methods after loading each story
Type Handling
Ink's Value.Create() is used through the indexer to ensure proper type wrapping:
story.variablesState[variableName] = value; // Uses Ink's Value.Create internally
This handles:
boolean→BoolValuenumber→IntValueorFloatValuestring→StringValue
Loop Prevention
When broadcasting changes to other stories, the event listener is temporarily disabled to prevent infinite loops:
const oldHandler = story.variablesState.variableChangedEvent;
story.variablesState.variableChangedEvent = null;
story.variablesState[variableName] = value;
story.variablesState.variableChangedEvent = oldHandler;
Example: Equipment Officer Scenario
Scenario File (npc-sprite-test2.json)
{
"globalVariables": {
"player_joined_organization": false
},
...
}
First NPC (test2.ink)
VAR player_joined_organization = false
=== player_closing ===
# speaker:player
* [I'd love to join your organization!]
~ player_joined_organization = true
Excellent! Welcome aboard.
Second NPC (equipment-officer.ink)
VAR player_joined_organization = false // Synced from test2.ink
=== hub ===
// This option only appears if player joined organization
+ {player_joined_organization} [Show me everything]
-> show_inventory
Result:
- Player talks to first NPC, chooses to join
player_joined_organization→truein window.gameState- Player talks to second NPC
- Variable is synced into their story
- Full inventory option now appears!
Debugging & Troubleshooting
Conditional Choices Not Appearing?
Most Common Cause: Ink files must be recompiled after editing.
# Recompile the Ink file:
inklecate -ojv scenarios/compiled/equipment-officer.json scenarios/ink/equipment-officer.ink
Then hard refresh the browser:
- Windows/Linux:
Ctrl + Shift + R - Mac:
Cmd + Shift + R
Variable Changed But Choices Still Wrong?
Cause: Conditionals evaluated before variable synced.
Solution: Ensure you're using the correct Ink syntax:
// ✅ CORRECT - conditional in choice brackets
+ {player_joined_organization} [Show me everything]
// ❌ WRONG - wrapping entire choice block
{player_joined_organization:
+ [Show me everything]
}
Check Global Variables
window.gameState.globalVariables
Enable Debug Mode
window.npcConversationStateManager._log('debug', 'message', data);
Verify Scenario Loaded Correctly
window.gameScenario.globalVariables
Check Cached Stories
window.npcManager.inkEngineCache
View Console Logs
Look for these patterns in browser console:
✅ Synced player_joined_organization = true to story- Variable synced successfully🔄 Global variable player_joined_organization changed from false to true- Variable changed🌐 Synced X global variable(s) after choice- Changes propagated after player choice
Best Practices
- Declare in Scenario - Use the
globalVariablessection for main narrative state - Consistent Naming - Use snake_case:
player_joined_organization,quest_complete - Type Consistency - Keep the same type (bool, number, string) across all uses
- Document Intent - Add comments in Ink files explaining what globals mean
- Test State Persistence - Verify globals persist across page reloads
- Avoid Circular Logic - Don't create mutually-dependent conditional branches
Migration Guide
Adding Global Variables to Existing Scenarios
- Add
globalVariablessection to scenario JSON:
{
"globalVariables": {
"new_variable": false
},
...
}
- Add to Ink files that use it:
VAR new_variable = false
- Use in conditionals or assignments:
{new_variable:
Conditions when variable is true
}
No Breaking Changes
- Scenarios without
globalVariableswork fine (empty object) - Existing variables remain NPC-specific unless added to
globalVariables global_*convention works for quick prototyping