From 37bc334813512465c25dd99017f6529f634433ce Mon Sep 17 00:00:00 2001 From: "Z. Cliffe Schreuders" Date: Wed, 19 Nov 2025 13:44:30 +0000 Subject: [PATCH] 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 --- .../person-chat/person-chat-conversation.js | 56 ++++++++++++++---- .../phone-chat/phone-chat-conversation.js | 59 ++++++++++++++++++- js/systems/ink/ink-engine.js | 14 ++++- 3 files changed, 115 insertions(+), 14 deletions(-) diff --git a/js/minigames/person-chat/person-chat-conversation.js b/js/minigames/person-chat/person-chat-conversation.js index d4d910b..599b9f1 100644 --- a/js/minigames/person-chat/person-chat-conversation.js +++ b/js/minigames/person-chat/person-chat-conversation.js @@ -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) => { diff --git a/js/minigames/phone-chat/phone-chat-conversation.js b/js/minigames/phone-chat/phone-chat-conversation.js index 7dd7f93..46d15a5 100644 --- a/js/minigames/phone-chat/phone-chat-conversation.js +++ b/js/minigames/phone-chat/phone-chat-conversation.js @@ -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 diff --git a/js/systems/ink/ink-engine.js b/js/systems/ink/ink-engine.js index f2372a3..df13ef6 100644 --- a/js/systems/ink/ink-engine.js +++ b/js/systems/ink/ink-engine.js @@ -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); + } + } }