fix: Add external function bindings for NPC hub ink files

- Add bindExternalFunction method to InkEngine
- Bind all required EXTERNAL functions in PersonChatConversation:
  - player_name() - returns player's agent name
  - current_mission_id() - returns active mission ID
  - npc_location() - returns conversation location
  - mission_phase() - returns mission phase (planning/active/debriefing/downtime)
  - operational_stress_level() - returns stress level for handler
  - equipment_status() - returns equipment status for Dr. Chen
- Add setupExternalFunctions to PhoneChatConversation
- Fixes: "Missing function binding for externals" error
This commit is contained in:
Z. Cliffe Schreuders
2025-11-19 13:44:30 +00:00
parent 867404e1b0
commit 37bc334813
3 changed files with 115 additions and 14 deletions

View File

@@ -80,13 +80,7 @@ export default class PersonChatConversation {
*/
setupExternalFunctions() {
if (!this.inkEngine) return;
// Example: Allow Ink to call game functions
// this.inkEngine.bindFunction('unlock_door', (doorId) => {
// console.log(`🔓 Unlocking door: ${doorId}`);
// // Handle door unlock
// });
// Store NPC metadata in global game state
if (!window.gameState) {
window.gameState = {};
@@ -94,14 +88,54 @@ export default class PersonChatConversation {
if (!window.gameState.npcInteractions) {
window.gameState.npcInteractions = {};
}
// Bind EXTERNAL functions that return values
// These are called from ink scripts with parentheses: {player_name()}
// Player name - return player's agent name or default
this.inkEngine.bindExternalFunction('player_name', () => {
return window.gameState?.playerName || 'Agent';
});
// Current mission ID - return active mission identifier
this.inkEngine.bindExternalFunction('current_mission_id', () => {
return window.gameState?.currentMissionId || 'mission_001';
});
// NPC location - where the conversation is happening
this.inkEngine.bindExternalFunction('npc_location', () => {
// Return location based on NPC or default
if (this.npc.id === 'dr_chen') {
return window.gameState?.npcLocation || 'lab';
} else if (this.npc.id === 'director_netherton') {
return window.gameState?.npcLocation || 'office';
} else if (this.npc.id === 'haxolottle') {
return window.gameState?.npcLocation || 'handler_station';
}
return window.gameState?.npcLocation || 'safehouse';
});
// Mission phase - what part of the mission we're in
this.inkEngine.bindExternalFunction('mission_phase', () => {
return window.gameState?.missionPhase || 'downtime';
});
// Operational stress level - for handler conversations
this.inkEngine.bindExternalFunction('operational_stress_level', () => {
return window.gameState?.operationalStressLevel || 'low';
});
// Equipment status - for Dr. Chen conversations
this.inkEngine.bindExternalFunction('equipment_status', () => {
return window.gameState?.equipmentStatus || 'nominal';
});
// Set variables in the Ink engine using setVariable instead of bindVariable
this.inkEngine.setVariable('last_interaction_type', 'person');
this.inkEngine.setVariable('player_name', 'Player');
// Sync NPC items to Ink variables
this.syncItemsToInk();
// Set up event listener for item changes
if (window.eventDispatcher) {
this._itemsChangedListener = (data) => {

View File

@@ -71,10 +71,13 @@ export default class PhoneChatConversation {
// Note: We don't set npc_name variable here because it causes issues with state serialization.
// The NPC display name is handled in the UI layer instead.
this.storyLoaded = true;
this.storyEnded = false;
// Set up external functions
this.setupExternalFunctions();
// Sync NPC items to Ink variables
this.syncItemsToInk();
@@ -98,6 +101,58 @@ export default class PhoneChatConversation {
}
}
/**
* Set up external functions for Ink story
* These allow Ink to call game functions and get dynamic values
*/
setupExternalFunctions() {
if (!this.engine || !this.engine.story) return;
// Bind EXTERNAL functions that return values
// These are called from ink scripts with parentheses: {player_name()}
// Player name - return player's agent name or default
this.engine.bindExternalFunction('player_name', () => {
return window.gameState?.playerName || 'Agent';
});
// Current mission ID - return active mission identifier
this.engine.bindExternalFunction('current_mission_id', () => {
return window.gameState?.currentMissionId || 'mission_001';
});
// NPC location - where the conversation is happening
this.engine.bindExternalFunction('npc_location', () => {
const npc = this.npcManager.getNPC(this.npcId);
// Return location based on NPC or default
if (this.npcId === 'dr_chen' || npc?.id === 'dr_chen') {
return window.gameState?.npcLocation || 'lab';
} else if (this.npcId === 'director_netherton' || npc?.id === 'director_netherton') {
return window.gameState?.npcLocation || 'office';
} else if (this.npcId === 'haxolottle' || npc?.id === 'haxolottle') {
return window.gameState?.npcLocation || 'handler_station';
}
return window.gameState?.npcLocation || 'safehouse';
});
// Mission phase - what part of the mission we're in
this.engine.bindExternalFunction('mission_phase', () => {
return window.gameState?.missionPhase || 'downtime';
});
// Operational stress level - for handler conversations
this.engine.bindExternalFunction('operational_stress_level', () => {
return window.gameState?.operationalStressLevel || 'low';
});
// Equipment status - for Dr. Chen conversations
this.engine.bindExternalFunction('equipment_status', () => {
return window.gameState?.equipmentStatus || 'nominal';
});
console.log(`✅ External functions bound for ${this.npcId}`);
}
/**
* Navigate to a specific knot in the story
* @param {string} knotName - Name of the knot to navigate to

View File

@@ -116,7 +116,7 @@ export default class InkEngine {
setVariable(name, value) {
if (!this.story) throw new Error('Story not loaded');
// Let Ink handle the value type conversion through the indexer
// which properly wraps values in Runtime.Value objects
try {
@@ -125,4 +125,16 @@ export default class InkEngine {
console.warn(`⚠️ Failed to set variable ${name}:`, err.message);
}
}
// Bind an external function that Ink can call
bindExternalFunction(name, func) {
if (!this.story) throw new Error('Story not loaded');
try {
this.story.BindExternalFunction(name, func);
console.log(`✅ Bound external function: ${name}`);
} catch (err) {
console.warn(`⚠️ Failed to bind external function ${name}:`, err.message);
}
}
}