mirror of
https://github.com/cliffe/BreakEscape.git
synced 2026-02-21 19:28:03 +00:00
feat: Enhance NPC dialogue and objectives system with event mappings for secret mission completion
This commit is contained in:
@@ -100,32 +100,39 @@ class NPCConversationStateManager {
|
||||
}
|
||||
|
||||
try {
|
||||
// Restore global variables first (before story state/variables)
|
||||
if (state.globalVariablesSnapshot) {
|
||||
window.gameState.globalVariables = { ...state.globalVariablesSnapshot };
|
||||
console.log(`✅ Restored global variables:`, state.globalVariablesSnapshot);
|
||||
}
|
||||
// NOTE: We no longer restore globalVariablesSnapshot here!
|
||||
// Global variables are the single source of truth in window.gameState.globalVariables
|
||||
// They should NOT be overwritten when restoring individual NPC states, because
|
||||
// other NPCs may have changed global variables since this state was saved.
|
||||
// Instead, we sync FROM window.gameState.globalVariables TO the story after loading.
|
||||
|
||||
// If we have saved story state, restore it completely (mid-conversation state)
|
||||
// NOTE: After LoadJson, global variables inside the story may be stale.
|
||||
// The caller should call syncGlobalVariablesToStory() after this returns.
|
||||
if (state.storyState) {
|
||||
story.state.LoadJson(state.storyState);
|
||||
console.log(`✅ Restored full story state for NPC: ${npcId}`, {
|
||||
savedAt: new Date(state.timestamp).toLocaleTimeString(),
|
||||
reason: 'In-progress conversation'
|
||||
reason: 'In-progress conversation (global vars will be re-synced)'
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// If we only have variables (story ended), restore just the variables
|
||||
// If we only have variables (story ended), restore just the NPC-specific variables
|
||||
if (state.variables) {
|
||||
// Load variables into the story
|
||||
// Load NPC-specific variables into the story
|
||||
// Skip global variables - they will be synced separately from window.gameState.globalVariables
|
||||
for (const [key, value] of Object.entries(state.variables)) {
|
||||
// Skip global variables - they're managed by window.gameState.globalVariables
|
||||
if (this.isGlobalVariable(key)) {
|
||||
console.log(`⏭️ Skipping global variable in NPC restore: ${key} (will sync from gameState)`);
|
||||
continue;
|
||||
}
|
||||
story.variablesState[key] = value;
|
||||
}
|
||||
console.log(`✅ Restored variables for NPC: ${npcId}`, {
|
||||
console.log(`✅ Restored NPC-specific variables for NPC: ${npcId}`, {
|
||||
savedAt: new Date(state.timestamp).toLocaleTimeString(),
|
||||
reason: 'Story ended - restarting fresh with saved variables',
|
||||
variables: state.variables
|
||||
reason: 'Story ended - restarting fresh with saved variables'
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -306,13 +306,21 @@ export class ObjectivesManager {
|
||||
// Check aim completion
|
||||
this.checkAimCompletion(task.aimId);
|
||||
|
||||
// Emit event
|
||||
// Emit both generic and specific events for NPC eventMappings
|
||||
// Generic event for wildcard listeners (objective_task_completed:*)
|
||||
this.eventDispatcher.emit('objective_task_completed', {
|
||||
taskId,
|
||||
aimId: task.aimId,
|
||||
task
|
||||
});
|
||||
|
||||
// Specific event for NPC eventMappings (objective_task_completed:talk_to_alice)
|
||||
this.eventDispatcher.emit(`objective_task_completed:${taskId}`, {
|
||||
taskId,
|
||||
aimId: task.aimId,
|
||||
task
|
||||
});
|
||||
|
||||
this.notifyListeners();
|
||||
}
|
||||
|
||||
@@ -390,10 +398,18 @@ export class ObjectivesManager {
|
||||
}
|
||||
});
|
||||
|
||||
// Emit both generic and specific events for NPC eventMappings
|
||||
// Generic event for wildcard listeners (objective_aim_completed:*)
|
||||
this.eventDispatcher.emit('objective_aim_completed', {
|
||||
aimId,
|
||||
aim
|
||||
});
|
||||
|
||||
// Specific event for NPC eventMappings (objective_aim_completed:secret_mission)
|
||||
this.eventDispatcher.emit(`objective_aim_completed:${aimId}`, {
|
||||
aimId,
|
||||
aim
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -86,3 +86,24 @@ That's one secret task down! Bob can help you with the second one.
|
||||
=== final_words ===
|
||||
You've done great! Once Bob helps you finish, come back for the final debrief.
|
||||
-> hub
|
||||
|
||||
=== final_debrief ===
|
||||
// This knot is triggered automatically when the secret_mission aim is completed
|
||||
// via eventMappings: "objective_aim_completed:secret_mission" -> "final_debrief"
|
||||
|
||||
NPC: *Alice looks up as you approach*
|
||||
Narrator: Alice gives you a knowing smile.
|
||||
Bob: You did it! The secret mission is complete.
|
||||
NPC: I just received confirmation from headquarters.
|
||||
#complete_task:final_debrief
|
||||
NPC: Mission accomplished, agent. You've proven yourself.
|
||||
NPC: The objectives system test is now complete. Well done!
|
||||
+ [Thank you, Alice]
|
||||
NPC: Anytime. See you on the next mission.
|
||||
#exit_conversation
|
||||
-> hub
|
||||
+ [What's next?]
|
||||
NPC: Take a break. You've earned it.
|
||||
NPC: When you're ready, there will be more missions waiting.
|
||||
#exit_conversation
|
||||
-> hub
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"inkVersion":21,"root":[[["done",{"#n":"g-0"}],null],"done",{"start":["^Hey there! I'm Alice. Welcome to the objectives system test.","\n","^Player: Hi there!","\n",{"->":"hub"},null],"hub":[["ev","str","^Nice to meet you, Alice","/str",{"VAR?":"alice_talked"},"!","/ev",{"*":".^.c-0","flg":5},"ev","str","^Tell me about the secret mission","/str",{"VAR?":"alice_talked"},{"VAR?":"secret_revealed"},"!","&&","/ev",{"*":".^.c-1","flg":5},"ev","str","^I'm ready for the secret task","/str",{"VAR?":"secret_revealed"},{"VAR?":"secret_task_done"},"!","&&","/ev",{"*":".^.c-2","flg":5},"ev","str","^Any final words?","/str",{"VAR?":"secret_task_done"},"/ev",{"*":".^.c-3","flg":5},"ev","str","^What can you tell me about objectives?","/str","/ev",{"*":".^.c-4","flg":4},"ev","str","^I need to go","/str","/ev",{"*":".^.c-5","flg":4},{"c-0":["\n",{"->":"first_meeting"},null],"c-1":["\n",{"->":"reveal_secret"},null],"c-2":["\n",{"->":"complete_secret_task"},null],"c-3":["\n",{"->":"final_words"},null],"c-4":["\n",{"->":"explain_objectives"},null],"c-5":["^ ","\n","^See you around!","\n","#","^exit_conversation","/#",{"->":"hub"},null]}],null],"first_meeting":["^Great to meet you too! This task is now complete.","\n","#","^complete_task:talk_to_alice","/#","ev",true,"/ev",{"VAR=":"alice_talked","re":true},"^You should go talk to Bob next - I just unlocked that task for you.","\n",{"->":"hub"},null],"explain_objectives":["^The objectives system uses three Ink tags:","\n",{"->":"explain_objectives_detail"},null],"explain_objectives_detail":[["ev","str","^Tell me about complete_task","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Tell me about unlock_task","/str","/ev",{"*":".^.c-1","flg":4},"ev","str","^Tell me about unlock_aim","/str","/ev",{"*":".^.c-2","flg":4},"ev","str","^That's enough info, thanks","/str","/ev",{"*":".^.c-3","flg":4},{"c-0":["\n","^NPC: **complete_task:task_id** marks a task as completed. The ObjectivesManager will update the UI and sync with the server.","\n",{"->":".^.^.^"},null],"c-1":["\n","^NPC: **unlock_task:task_id** unlocks a locked task so it becomes active and visible.","\n",{"->":".^.^.^"},null],"c-2":["\n","^NPC: **unlock_aim:aim_id** unlocks an entire aim (objective group) that was previously locked.","\n",{"->":".^.^.^"},null],"c-3":["\n","^NPC: These tags let NPCs control the player's objectives through dialogue!","\n",{"->":"hub"},null]}],null],"reveal_secret":["^Alright, I'll let you in on a secret...","\n","^There's a hidden mission that only unlocks through dialogue!","\n","#","^unlock_aim:secret_mission","/#","ev",true,"/ev",{"VAR=":"secret_revealed","re":true},"^I've just unlocked the \"Secret Mission\" aim for you. Check your objectives!","\n","^But the tasks inside are still locked. Let me unlock the first one...","\n","#","^unlock_task:secret_task_1","/#","^There! Now you can complete the first secret task.","\n",{"->":"hub"},null],"complete_secret_task":["^Excellent! You're doing great with the secret mission.","\n","#","^complete_task:secret_task_1","/#","ev",true,"/ev",{"VAR=":"secret_task_done","re":true},"^That's one secret task down! Bob can help you with the second one.","\n",{"->":"hub"},null],"final_words":["^You've done great! Once Bob helps you finish, come back for the final debrief.","\n",{"->":"hub"},null],"global decl":["ev",false,{"VAR=":"alice_talked"},false,{"VAR=":"secret_revealed"},false,{"VAR=":"secret_task_done"},"/ev","end",null]}],"listDefs":{}}
|
||||
{"inkVersion":21,"root":[[["done",{"#n":"g-0"}],null],"done",{"start":["^Hey there! I'm Alice. Welcome to the objectives system test.","\n","^Player: Hi there!","\n",{"->":"hub"},null],"hub":[["ev","str","^Nice to meet you, Alice","/str",{"VAR?":"alice_talked"},"!","/ev",{"*":".^.c-0","flg":5},"ev","str","^Tell me about the secret mission","/str",{"VAR?":"alice_talked"},{"VAR?":"secret_revealed"},"!","&&","/ev",{"*":".^.c-1","flg":5},"ev","str","^I'm ready for the secret task","/str",{"VAR?":"secret_revealed"},{"VAR?":"secret_task_done"},"!","&&","/ev",{"*":".^.c-2","flg":5},"ev","str","^Any final words?","/str",{"VAR?":"secret_task_done"},"/ev",{"*":".^.c-3","flg":5},"ev","str","^What can you tell me about objectives?","/str","/ev",{"*":".^.c-4","flg":4},"ev","str","^I need to go","/str","/ev",{"*":".^.c-5","flg":4},{"c-0":["\n",{"->":"first_meeting"},null],"c-1":["\n",{"->":"reveal_secret"},null],"c-2":["\n",{"->":"complete_secret_task"},null],"c-3":["\n",{"->":"final_words"},null],"c-4":["\n",{"->":"explain_objectives"},null],"c-5":["^ ","\n","^See you around!","\n","#","^exit_conversation","/#",{"->":"hub"},null]}],null],"first_meeting":["^Great to meet you too! This task is now complete.","\n","#","^complete_task:talk_to_alice","/#","ev",true,"/ev",{"VAR=":"alice_talked","re":true},"^You should go talk to Bob next - I just unlocked that task for you.","\n",{"->":"hub"},null],"explain_objectives":["^The objectives system uses three Ink tags:","\n",{"->":"explain_objectives_detail"},null],"explain_objectives_detail":[["ev","str","^Tell me about complete_task","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Tell me about unlock_task","/str","/ev",{"*":".^.c-1","flg":4},"ev","str","^Tell me about unlock_aim","/str","/ev",{"*":".^.c-2","flg":4},"ev","str","^That's enough info, thanks","/str","/ev",{"*":".^.c-3","flg":4},{"c-0":["\n","^NPC: **complete_task:task_id** marks a task as completed. The ObjectivesManager will update the UI and sync with the server.","\n",{"->":".^.^.^"},null],"c-1":["\n","^NPC: **unlock_task:task_id** unlocks a locked task so it becomes active and visible.","\n",{"->":".^.^.^"},null],"c-2":["\n","^NPC: **unlock_aim:aim_id** unlocks an entire aim (objective group) that was previously locked.","\n",{"->":".^.^.^"},null],"c-3":["\n","^NPC: These tags let NPCs control the player's objectives through dialogue!","\n",{"->":"hub"},null]}],null],"reveal_secret":["^Alright, I'll let you in on a secret...","\n","^There's a hidden mission that only unlocks through dialogue!","\n","#","^unlock_aim:secret_mission","/#","ev",true,"/ev",{"VAR=":"secret_revealed","re":true},"^I've just unlocked the \"Secret Mission\" aim for you. Check your objectives!","\n","^But the tasks inside are still locked. Let me unlock the first one...","\n","#","^unlock_task:secret_task_1","/#","^There! Now you can complete the first secret task.","\n",{"->":"hub"},null],"complete_secret_task":["^Excellent! You're doing great with the secret mission.","\n","#","^complete_task:secret_task_1","/#","ev",true,"/ev",{"VAR=":"secret_task_done","re":true},"^That's one secret task down! Bob can help you with the second one.","\n",{"->":"hub"},null],"final_words":["^You've done great! Once Bob helps you finish, come back for the final debrief.","\n",{"->":"hub"},null],"final_debrief":[["^NPC: *Alice looks up as you approach*","\n","^Narrator: Alice gives you a knowing smile.","\n","^Bob: You did it! The secret mission is complete.","\n","^NPC: I just received confirmation from headquarters.","\n","#","^complete_task:final_debrief","/#","^NPC: Mission accomplished, agent. You've proven yourself.","\n","^NPC: The objectives system test is now complete. Well done!","\n","ev","str","^Thank you, Alice","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^What's next?","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n","^NPC: Anytime. See you on the next mission.","\n","#","^exit_conversation","/#",{"->":"hub"},null],"c-1":["\n","^NPC: Take a break. You've earned it.","\n","^NPC: When you're ready, there will be more missions waiting.","\n","#","^exit_conversation","/#",{"->":"hub"},null]}],null],"global decl":["ev",false,{"VAR=":"alice_talked"},false,{"VAR=":"secret_revealed"},false,{"VAR=":"secret_task_done"},"/ev","end",null]}],"listDefs":{}}
|
||||
@@ -5,16 +5,19 @@
|
||||
// IMPORTANT: Uses mission hub pattern - never uses -> END
|
||||
// Instead uses #exit_conversation tag to leave the chat
|
||||
|
||||
// Global variables - must be declared in BOTH ink files to sync properly
|
||||
VAR alice_talked = false // global - synced from alice.ink when she sets it true
|
||||
VAR bob_talked = false
|
||||
VAR helped_with_secret = false
|
||||
VAR secret_revealed = false // from alice.ink
|
||||
VAR secret_revealed = false // global from alice.ink
|
||||
|
||||
=== start ===
|
||||
*cough* Oh, hey. I'm Bob. Didn't see you there.
|
||||
Narrator: You see a hooded figure waiting for you.
|
||||
NPC: Oh, hey. I'm Bob.
|
||||
-> hub
|
||||
|
||||
=== hub ===
|
||||
+ {not bob_talked} [Alice sent me to talk to you]
|
||||
+ {alice_talked and not bob_talked} [Alice sent me to talk to you]
|
||||
-> first_meeting
|
||||
|
||||
+ {bob_talked and secret_revealed and not helped_with_secret} [Can you help with the secret task?]
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"inkVersion":21,"root":[[["done",{"#n":"g-0"}],null],"done",{"start":[[["ev",{"^->":"start.0.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^cough* Oh, hey. I'm Bob. Didn't see you there.",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"start.0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"\n",{"->":"hub"},{"#f":5}]}],null],"hub":[["ev","str","^Alice sent me to talk to you","/str",{"VAR?":"bob_talked"},"!","/ev",{"*":".^.c-0","flg":5},"ev","str","^Can you help with the secret task?","/str",{"VAR?":"bob_talked"},{"VAR?":"secret_revealed"},"&&",{"VAR?":"helped_with_secret"},"!","&&","/ev",{"*":".^.c-1","flg":5},"ev","str","^Thanks for the help!","/str",{"VAR?":"helped_with_secret"},"/ev",{"*":".^.c-2","flg":5},"ev","str","^What do you do here?","/str","/ev",{"*":".^.c-3","flg":4},"ev","str","^Goodbye","/str","/ev",{"*":".^.c-4","flg":4},{"c-0":["\n",{"->":"first_meeting"},null],"c-1":["\n",{"->":"secret_task_help"},null],"c-2":["\n",{"->":"thanks_response"},null],"c-3":["\n",{"->":"about_bob"},null],"c-4":["\n","^Later.","\n","#","^exit_conversation","/#",{"->":"hub"},null]}],null],"first_meeting":["^Ah, Alice sent you? Good, good.","\n","#","^complete_task:talk_to_bob","/#","ev",true,"/ev",{"VAR=":"bob_talked","re":true},"^I've just marked that task complete.","\n","^If Alice told you about anything... special... come back and ask me.","\n",{"->":"hub"},null],"about_bob":["^I handle the technical side of things.","\n","^Mostly just unlocking things that need to be unlocked.","\n","^Speaking of which... if there are any locked tasks you need help with, just ask.","\n",{"->":"hub"},null],"secret_task_help":["^The secret task, eh? Let me help you with that.","\n","#","^unlock_task:secret_task_2","/#","#","^complete_task:secret_task_2","/#","ev",true,"/ev",{"VAR=":"helped_with_secret","re":true},"^Done! Both secret tasks are now complete.","\n","^That means the secret aim should be finished too.","\n","#","^unlock_aim:finale","/#","#","^unlock_task:final_debrief","/#","^I've also unlocked the finale for you. Go talk to Alice for the final debrief!","\n",{"->":"hub"},null],"thanks_response":["^No problem! Go see Alice for the final debrief. She's waiting for you.","\n",{"->":"hub"},null],"global decl":["ev",false,{"VAR=":"bob_talked"},false,{"VAR=":"helped_with_secret"},false,{"VAR=":"secret_revealed"},"/ev","end",null]}],"listDefs":{}}
|
||||
{"inkVersion":21,"root":[[["done",{"#n":"g-0"}],null],"done",{"start":["^Narrator: You see a hooded figure waiting for you.","\n","^NPC: Oh, hey. I'm Bob.","\n",{"->":"hub"},null],"hub":[["ev","str","^Alice sent me to talk to you","/str",{"VAR?":"alice_talked"},{"VAR?":"bob_talked"},"!","&&","/ev",{"*":".^.c-0","flg":5},"ev","str","^Can you help with the secret task?","/str",{"VAR?":"bob_talked"},{"VAR?":"secret_revealed"},"&&",{"VAR?":"helped_with_secret"},"!","&&","/ev",{"*":".^.c-1","flg":5},"ev","str","^Thanks for the help!","/str",{"VAR?":"helped_with_secret"},"/ev",{"*":".^.c-2","flg":5},"ev","str","^What do you do here?","/str","/ev",{"*":".^.c-3","flg":4},"ev","str","^Goodbye","/str","/ev",{"*":".^.c-4","flg":4},{"c-0":["\n",{"->":"first_meeting"},null],"c-1":["\n",{"->":"secret_task_help"},null],"c-2":["\n",{"->":"thanks_response"},null],"c-3":["\n",{"->":"about_bob"},null],"c-4":["\n","^Later.","\n","#","^exit_conversation","/#",{"->":"hub"},null]}],null],"first_meeting":["^Ah, Alice sent you? Good, good.","\n","#","^complete_task:talk_to_bob","/#","ev",true,"/ev",{"VAR=":"bob_talked","re":true},"^I've just marked that task complete.","\n","^If Alice told you about anything... special... come back and ask me.","\n",{"->":"hub"},null],"about_bob":["^I handle the technical side of things.","\n","^Mostly just unlocking things that need to be unlocked.","\n","^Speaking of which... if there are any locked tasks you need help with, just ask.","\n",{"->":"hub"},null],"secret_task_help":["^The secret task, eh? Let me help you with that.","\n","#","^unlock_task:secret_task_2","/#","#","^complete_task:secret_task_2","/#","ev",true,"/ev",{"VAR=":"helped_with_secret","re":true},"^Done! Both secret tasks are now complete.","\n","^That means the secret aim should be finished too.","\n","#","^unlock_aim:finale","/#","#","^unlock_task:final_debrief","/#","^I've also unlocked the finale for you. Go talk to Alice for the final debrief!","\n",{"->":"hub"},null],"thanks_response":["^No problem! Go see Alice for the final debrief. She's waiting for you.","\n",{"->":"hub"},null],"global decl":["ev",false,{"VAR=":"alice_talked"},false,{"VAR=":"bob_talked"},false,{"VAR=":"helped_with_secret"},false,{"VAR=":"secret_revealed"},"/ev","end",null]}],"listDefs":{}}
|
||||
@@ -3,16 +3,7 @@
|
||||
"endGoal": "Complete all objectives by talking to the NPCs",
|
||||
"version": "1.0",
|
||||
"startRoom": "lobby",
|
||||
"startItemsInInventory": [
|
||||
{
|
||||
"type": "notepad",
|
||||
"name": "Notepad",
|
||||
"takeable": true,
|
||||
"readable": true,
|
||||
"text": "Use this notepad to review your collected notes and observations.",
|
||||
"observations": "A handy notepad for keeping track of important information."
|
||||
}
|
||||
],
|
||||
"startItemsInInventory": [],
|
||||
"objectives": [
|
||||
{
|
||||
"aimId": "meet_contacts",
|
||||
@@ -83,7 +74,8 @@
|
||||
"globalVariables": {
|
||||
"alice_talked": false,
|
||||
"bob_talked": false,
|
||||
"secret_unlocked": false
|
||||
"secret_unlocked": false,
|
||||
"secret_revealed": false
|
||||
},
|
||||
"rooms": {
|
||||
"lobby": {
|
||||
@@ -109,7 +101,17 @@
|
||||
"persistentVariables": {
|
||||
"alice_talked": false,
|
||||
"secret_revealed": false
|
||||
}
|
||||
},
|
||||
"eventMappings": [
|
||||
{
|
||||
"eventPattern": "objective_aim_completed:secret_mission",
|
||||
"targetKnot": "final_debrief",
|
||||
"conversationMode": "person-chat",
|
||||
"cooldown": 0,
|
||||
"onceOnly": true,
|
||||
"_comment": "Triggers final debrief when secret_mission aim is completed"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "bob",
|
||||
|
||||
Reference in New Issue
Block a user