From 84c8a7a27a7cb17e2b3d55720731ff39206cccc7 Mon Sep 17 00:00:00 2001 From: Vladislav Khorev Date: Fri, 22 May 2026 22:32:05 +0300 Subject: [PATCH] Dialog fixing --- .../dialogue/uni_interior_dialogues.json | 22 +++--- resources/start_uni_interior.lua | 68 +++++++++++-------- src/Character.cpp | 18 +++-- src/Character.h | 2 + src/Game.cpp | 1 - src/Location.cpp | 19 ++++++ src/ScriptEngine.cpp | 5 ++ src/dialogue/DialogueDatabase.cpp | 4 ++ src/dialogue/DialogueRuntime.cpp | 47 +++++++++++++ src/dialogue/DialogueRuntime.h | 7 ++ src/dialogue/DialogueSystem.cpp | 12 ++++ src/dialogue/DialogueSystem.h | 3 + src/dialogue/DialogueTypes.h | 4 ++ 13 files changed, 169 insertions(+), 43 deletions(-) diff --git a/resources/dialogue/uni_interior_dialogues.json b/resources/dialogue/uni_interior_dialogues.json index 7b424d7..64b3a10 100644 --- a/resources/dialogue/uni_interior_dialogues.json +++ b/resources/dialogue/uni_interior_dialogues.json @@ -55,6 +55,7 @@ { "id": "aiperi_dialog001", "start": "line_1", + "uninterruptible": true, "nodes": [ { "id": "line_1", @@ -325,6 +326,7 @@ { "id": "teacher_dialog002", "start": "line_1", + "uninterruptible": true, "nodes": [ { "id": "line_1", @@ -475,6 +477,7 @@ { "id": "teacher_dialog005", "start": "line_1", + "uninterruptible": true, "nodes": [ { "id": "line_1", @@ -513,7 +516,7 @@ "type": "Line", "speaker": "Аида Дженибековна", "portrait": "resources/w/avatar_teacher.png", - "text": "Вот держи зачетку с оценкой, положи ее обратно в шкаф.", + "text": "Вот держи зачетку с оценкой.", "next": "line_6" }, { @@ -521,7 +524,7 @@ "type": "Line", "speaker": "Бекзат", "portrait": "resources/w/gg/gg2_s_podsvetkoy5.png", - "text": "Хорошо!", + "text": "Спасибо!", "next": "end_1" }, { @@ -610,6 +613,7 @@ }, { "id": "dialog_with_ghost001", + "uninterruptible": true, "start": "line_1", "nodes": [ { @@ -678,6 +682,7 @@ }, { "id": "end_1", + "luaCallback" : "on_first_ghost_dialog_over", "type": "End" } ] @@ -786,6 +791,7 @@ }, { "id": "end_1", + "luaCallback" : "on_quest_over", "type": "End" } ] @@ -1061,10 +1067,9 @@ ] }, { - "id": "test_cutscene_02", - "background": "resources/black.png", - "backgroundWidth" : 1280, - "backgroundHeight" : 720, + "id": "computer_cutscene001", + "onFadeInCallback": "on_sleep_cutscene", + "background": "resources/test_cutscene001.png", "durationMs": 5000, "fadeOutMs": 500, "fadeInMs": 500, @@ -1108,9 +1113,8 @@ }, { "id": "darklands_exit001", - "background": "resources/black.png", - "backgroundWidth" : 1280, - "backgroundHeight" : 720, + "onFadeInCallback": "on_darklands_over", + "background": "resources/test_cutscene001.png", "durationMs": 5000, "fadeOutMs": 500, "fadeInMs": 500, diff --git a/resources/start_uni_interior.lua b/resources/start_uni_interior.lua index bc30101..c9cb5ac 100644 --- a/resources/start_uni_interior.lua +++ b/resources/start_uni_interior.lua @@ -25,6 +25,7 @@ function lection_hall_zone001_enter_callback() --game_api.start_dialogue("") --Start cutscene if (lection_is_over == false) then + game_api.player_stop() game_api.start_cutscene("test_cutscene_01") end end @@ -180,25 +181,38 @@ function on_npc_interact(npc_index) local report_card_signed = game_api.getIntValue("report_card_signed") if (report_card_signed==1) then game_api.start_dialogue("dialog_with_ghost003") - ghost_gone = true - game_api.set_npc_enabled(2, false) - game_api.set_npc_enabled(3, false) - game_api.set_npc_enabled(4, false) - game_api.set_npc_enabled(5, false) else game_api.start_dialogue("dialog_with_ghost002") end else game_api.start_dialogue("dialog_with_ghost001") - player_ghost_aware = true end end end +function on_quest_over() + ghost_gone = true + game_api.set_npc_enabled(2, false) + game_api.set_npc_enabled(3, false) + game_api.set_npc_enabled(4, false) + game_api.set_npc_enabled(5, false) +end + +function on_first_ghost_dialog_over() + player_ghost_aware = true +end + + +function on_darklands_over() +game_api.set_player_hp(10) +game_api.resetPlayerAfterDeath() +end + function teacher_zone001_enter_callback() game_api.start_dialogue("teacher_dialog002") game_api.set_trigger_zone_enabled(2, false) teacher_told_about_book = true +game_api.player_stop() end function on_library_door_click() @@ -406,7 +420,7 @@ print("on_computer_clicked--") print("on_computer_clicked--3") if (player_hold_book) then print("on_computer_clicked--4") - game_api.start_cutscene("test_cutscene_02") + game_api.start_cutscene("computer_cutscene001") else print("on_computer_clicked--5") game_api.start_dialogue("book_dialog002") @@ -422,25 +436,7 @@ print("on_computer_clicked--") end end -function on_teacher_arrived_intermediate() -game_api.rotate_object("Room_N_2_Leaf001", -90, 0.5, nil) - game_api.fade_object("Room_Cover_North_3_001", 0, 0.5, function() - game_api.deactivate_interactive_object("Room_Cover_North_3_001") - end) -game_api.switch_navigation(2) -game_api.npc_walk_to(0, 3.19574, 0, 6.45595, on_teacher_arrived) -teacher_arrived = true -end - - -game_api.set_cutscene_callback("test_cutscene_01", function() - print("Cutscene done!") - lection_is_over = true - game_api.set_npc_enabled(1, true) - game_api.npc_walk_to(0, 0.758123, 0, 6.50063, on_teacher_arrived_intermediate) -end) - -game_api.set_cutscene_callback("test_cutscene_02", function() +function on_sleep_cutscene() print("Cutscene 2 done!") night_time = true --game_api.set_trigger_zone_enabled(1, false) @@ -462,9 +458,27 @@ game_api.set_cutscene_callback("test_cutscene_02", function() game_api.set_object_alpha("Room_Cover_North_1_001", 1) game_api.set_object_alpha("Room_Cover_Main_Hall_001", 1) game_api.activate_interactive_object("Note001") +end +function on_teacher_arrived_intermediate() +game_api.rotate_object("Room_N_2_Leaf001", -90, 0.5, nil) + game_api.fade_object("Room_Cover_North_3_001", 0, 0.5, function() + game_api.deactivate_interactive_object("Room_Cover_North_3_001") + end) +game_api.switch_navigation(2) +game_api.npc_walk_to(0, 3.19574, 0, 6.45595, on_teacher_arrived) +teacher_arrived = true +end + + +game_api.set_cutscene_callback("test_cutscene_01", function() + print("Cutscene done!") + lection_is_over = true + game_api.set_npc_enabled(1, true) + game_api.npc_walk_to(0, 0.758123, 0, 6.50063, on_teacher_arrived_intermediate) end) + function on_note_pickup() game_api.start_dialogue("note_dialog001") game_api.pickup_item("note_spell") @@ -542,11 +556,9 @@ game_api.set_darklands_callbacks( game_api.set_npc_enabled(5, false) game_api.setIntValue("day", 1) game_api.setIntValue("need_sleep", 1) - game_api.set_player_hp(10) player_left_darklands = true morning_did_open_door_index = 0 setDay1MorningSetup() - --game_api.switch_navigation(7) local px = game_api.get_player_x() diff --git a/src/Character.cpp b/src/Character.cpp index 4a2708f..8b47a60 100644 --- a/src/Character.cpp +++ b/src/Character.cpp @@ -104,6 +104,19 @@ void Character::stopInPlace() onArrivedCallback = nullptr; } +void Character::resetPlayerAfterDeath() +{ + if (!isPlayer) + { + return; + } + currentState = AnimationState::STAND; + resetAnim = true; + showWeapon = false; + battle_state = 0; + attack = 0; +} + void Character::forceReplan() { if (!pathPlanner) { @@ -456,11 +469,6 @@ void Character::update(int64_t deltaMs) { //onDeathAnimComplete = nullptr; onDeathAnimComplete(); //TODO: need to add some delay here - currentState = AnimationState::STAND; - resetAnim = true; - showWeapon = false; - battle_state = 0; - attack = 0; } } } diff --git a/src/Character.h b/src/Character.h index 8b49224..e7f1323 100644 --- a/src/Character.h +++ b/src/Character.h @@ -66,6 +66,8 @@ public: // character and we don't want it to walk back to where it was. void stopInPlace(); + void resetPlayerAfterDeath(); + // attackDirection is a world-space horizontal vector pointing from the // attacker toward this character — i.e. the direction the hit pushes diff --git a/src/Game.cpp b/src/Game.cpp index 0203980..c8f50fe 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -195,7 +195,6 @@ namespace ZL uniInteriorParams.npcsJsonPath = "resources/config2/npcs_uni_interior.json"; uniInteriorParams.dialoguesJsonPath = "resources/dialogue/uni_interior_dialogues.json"; - uniInteriorParams.navigationJsonPaths = { "resources/navigation/uni_interior0_all_locked.txt", "resources/navigation/uni_interior0_hall.txt", diff --git a/src/Location.cpp b/src/Location.cpp index d01578b..5db9b8d 100644 --- a/src/Location.cpp +++ b/src/Location.cpp @@ -146,6 +146,25 @@ namespace ZL scriptEngine.callCutsceneCompleteCallback(cutsceneId); }); + dialogueSystem.setOnDialogueLineStarted([this](const std::string& fn) { + try { scriptEngine.callActivateFunction(fn); } + catch (const std::exception& e) { + std::cerr << "[dialogue] line callback error: " << e.what() << "\n"; + } + }); + dialogueSystem.setOnCutsceneLineStarted([this](const std::string& fn) { + try { scriptEngine.callActivateFunction(fn); } + catch (const std::exception& e) { + std::cerr << "[cutscene] line callback error: " << e.what() << "\n"; + } + }); + dialogueSystem.setOnCutsceneFadeInComplete([this](const std::string& fn) { + try { scriptEngine.callActivateFunction(fn); } + catch (const std::exception& e) { + std::cerr << "[cutscene] fade-in callback error: " << e.what() << "\n"; + } + }); + } void Location::loadTeleportZones(const std::string& jsonPath, const char* zipFile) diff --git a/src/ScriptEngine.cpp b/src/ScriptEngine.cpp index 7d18a8b..3221708 100644 --- a/src/ScriptEngine.cpp +++ b/src/ScriptEngine.cpp @@ -460,6 +460,11 @@ namespace ZL { std::cerr << "[script] scale_object: object '" << name << "' not found\n"; }); + api.set_function("resetPlayerAfterDeath", + [game]() { + if (game->player) game->player->resetPlayerAfterDeath(); + }); + // player_stop() — cancels the player's current walk target and stops them immediately. api.set_function("player_stop", [game]() { diff --git a/src/dialogue/DialogueDatabase.cpp b/src/dialogue/DialogueDatabase.cpp index 7970eb1..b8ee914 100644 --- a/src/dialogue/DialogueDatabase.cpp +++ b/src/dialogue/DialogueDatabase.cpp @@ -104,6 +104,7 @@ Node DialogueDatabase::parseNode(const json& j) { node.trueNext = j.value("trueNext", ""); node.falseNext = j.value("falseNext", ""); node.cutsceneId = j.value("cutsceneId", ""); + node.luaCallback = j.value("luaCallback", ""); if (j.contains("conditions") && j["conditions"].is_array()) { for (const auto& item : j["conditions"]) { @@ -129,6 +130,7 @@ DialogueDefinition DialogueDatabase::parseDialogue(const json& j) { result.id = j.value("id", ""); result.displayName = j.value("displayName", result.id); result.startNode = j.value("start", ""); + result.uninterruptible = j.value("uninterruptible", false); if (j.contains("nodes") && j["nodes"].is_array()) { for (const auto& item : j["nodes"]) { @@ -151,6 +153,7 @@ CutsceneLine DialogueDatabase::parseCutsceneLine(const json& j) { line.background = j.value("background", ""); line.backgroundWidth = j.value("backgroundWidth", 0); line.backgroundHeight = j.value("backgroundHeight", 0); + line.luaCallback = j.value("luaCallback", ""); line.durationMs = j.value("durationMs", 0); line.waitForConfirm = j.value("waitForConfirm", false); return line; @@ -205,6 +208,7 @@ StaticCutsceneDefinition DialogueDatabase::parseCutscene(const json& j) { cutscene.fadeInMs = j.value("fadeInMs", 0); cutscene.endFadeOutMs = j.value("endFadeOutMs", 0); cutscene.endFadeInMs = j.value("endFadeInMs", 0); + cutscene.onFadeInCallback = j.value("onFadeInCallback", ""); if (j.contains("cameraTrack") && j["cameraTrack"].is_array()) { for (const auto& item : j["cameraTrack"]) { diff --git a/src/dialogue/DialogueRuntime.cpp b/src/dialogue/DialogueRuntime.cpp index 28a98ec..8347271 100644 --- a/src/dialogue/DialogueRuntime.cpp +++ b/src/dialogue/DialogueRuntime.cpp @@ -16,6 +16,11 @@ bool DialogueRuntime::startDialogue(const std::string& dialogueId) { return false; } + if (mode != Mode::Inactive && activeDialogue && activeDialogue->uninterruptible) { + std::cerr << "[dialogue] Blocked: '" << activeDialogue->id << "' is uninterruptible\n"; + return false; + } + const DialogueDefinition* dialogue = database->findDialogue(dialogueId); if (!dialogue) { std::cerr << "[dialogue] Dialogue not found: " << dialogueId << "\n"; @@ -35,6 +40,7 @@ bool DialogueRuntime::startDialogue(const std::string& dialogueId) { cutsceneTotalDurationMs = 0; cutsceneContentDurationMs = 0; currentCutsceneBackground.clear(); + fadeInCallbackFired = false; presentation = {}; presentation.dialogueId = dialogue->id; @@ -53,6 +59,7 @@ bool DialogueRuntime::startStandaloneCutscene(const std::string& cutsceneId) { } activeDialogue = nullptr; activeCutsceneId = cutsceneId; + fadeInCallbackFired = false; presentation = {}; startCutscene(cutsceneId, ""); return true; @@ -62,6 +69,18 @@ void DialogueRuntime::setOnCutsceneFinished(std::function cb) { + onDialogueLineStarted = std::move(cb); +} + +void DialogueRuntime::setOnCutsceneLineStarted(std::function cb) { + onCutsceneLineStarted = std::move(cb); +} + +void DialogueRuntime::setOnCutsceneFadeInComplete(std::function cb) { + onCutsceneFadeInComplete = std::move(cb); +} + void DialogueRuntime::stop() { activeDialogue = nullptr; activeCutscene = nullptr; @@ -76,6 +95,7 @@ void DialogueRuntime::stop() { cutsceneTotalDurationMs = 0; cutsceneContentDurationMs = 0; currentCutsceneBackground.clear(); + fadeInCallbackFired = false; mode = Mode::Inactive; presentation = {}; } @@ -128,6 +148,18 @@ void DialogueRuntime::update(int deltaMs) { return; } + if (!fadeInCallbackFired && onCutsceneFadeInComplete && activeCutscene) { + const int fadeInCompleteMs = activeCutscene->fadeOutMs + activeCutscene->fadeInMs; + if (cutsceneElapsedMs >= fadeInCompleteMs && !activeCutscene->onFadeInCallback.empty()) { + fadeInCallbackFired = true; + onCutsceneFadeInComplete(activeCutscene->onFadeInCallback); + } + } + + if (!activeCutscene || mode != Mode::PlayingCutscene) { + return; + } + const bool subtitlesFinished = activeCutscene->lines.empty() || currentCutsceneLine >= static_cast(activeCutscene->lines.size()); @@ -395,6 +427,10 @@ void DialogueRuntime::presentLine(const Node& node) { presentation.visibleText = node.text; revealCharacters = static_cast(node.text.size()); } + + if (!node.luaCallback.empty() && onDialogueLineStarted) { + onDialogueLineStarted(node.luaCallback); + } } void DialogueRuntime::presentChoices(const Node& node) { @@ -464,6 +500,7 @@ void DialogueRuntime::startCutscene(const std::string& cutsceneId, const std::st mode = Mode::PlayingCutscene; cutsceneElapsedMs = 0; cutsceneTimerMs = 0; + fadeInCallbackFired = false; currentCutsceneLine = activeCutscene->lines.empty() ? -1 : 0; int imageTrackDurationMs = 0; for (size_t i = 0; i < activeCutscene->images.size(); ++i) { @@ -486,6 +523,11 @@ void DialogueRuntime::startCutscene(const std::string& cutsceneId, const std::st cutsceneTotalDurationMs = cutsceneContentDurationMs + activeCutscene->endFadeOutMs + activeCutscene->endFadeInMs; refreshCutscenePresentation(); + if (!activeCutscene->lines.empty() && onCutsceneLineStarted) { + const std::string& cb = activeCutscene->lines[0].luaCallback; + if (!cb.empty()) onCutsceneLineStarted(cb); + } + std::cout << "[CUTSCENE] start id=" << cutsceneId << " lines=" << activeCutscene->lines.size() << " totalDuration=" << cutsceneTotalDurationMs @@ -572,6 +614,11 @@ void DialogueRuntime::advanceCutsceneLine() { return; } + if (onCutsceneLineStarted) { + const std::string& cb = activeCutscene->lines[currentCutsceneLine].luaCallback; + if (!cb.empty()) onCutsceneLineStarted(cb); + } + refreshCutscenePresentation(); } diff --git a/src/dialogue/DialogueRuntime.h b/src/dialogue/DialogueRuntime.h index 473aaf7..b5e28e9 100644 --- a/src/dialogue/DialogueRuntime.h +++ b/src/dialogue/DialogueRuntime.h @@ -19,6 +19,9 @@ public: bool startDialogue(const std::string& dialogueId); bool startStandaloneCutscene(const std::string& cutsceneId); void setOnCutsceneFinished(std::function cb); + void setOnDialogueLineStarted(std::function cb); + void setOnCutsceneLineStarted(std::function cb); + void setOnCutsceneFadeInComplete(std::function cb); void stop(); void update(int deltaMs); @@ -50,7 +53,11 @@ private: }; std::function onCutsceneFinished; + std::function onDialogueLineStarted; + std::function onCutsceneLineStarted; + std::function onCutsceneFadeInComplete; std::string activeCutsceneId; + bool fadeInCallbackFired = false; const DialogueDatabase* database = nullptr; const DialogueDefinition* activeDialogue = nullptr; diff --git a/src/dialogue/DialogueSystem.cpp b/src/dialogue/DialogueSystem.cpp index 2ad9176..2688739 100644 --- a/src/dialogue/DialogueSystem.cpp +++ b/src/dialogue/DialogueSystem.cpp @@ -127,6 +127,18 @@ void DialogueSystem::setOnCutsceneFinished(std::function cb) { + runtime.setOnDialogueLineStarted(std::move(cb)); +} + +void DialogueSystem::setOnCutsceneLineStarted(std::function cb) { + runtime.setOnCutsceneLineStarted(std::move(cb)); +} + +void DialogueSystem::setOnCutsceneFadeInComplete(std::function cb) { + runtime.setOnCutsceneFadeInComplete(std::move(cb)); +} + void DialogueSystem::stopDialogue() { runtime.stop(); } diff --git a/src/dialogue/DialogueSystem.h b/src/dialogue/DialogueSystem.h index 46ddf52..91ecd90 100644 --- a/src/dialogue/DialogueSystem.h +++ b/src/dialogue/DialogueSystem.h @@ -24,6 +24,9 @@ public: bool startDialogue(const std::string& dialogueId); bool startCutscene(const std::string& cutsceneId); void setOnCutsceneFinished(std::function cb); + void setOnDialogueLineStarted(std::function cb); + void setOnCutsceneLineStarted(std::function cb); + void setOnCutsceneFadeInComplete(std::function cb); void stopDialogue(); bool isActive() const { return runtime.isActive(); } diff --git a/src/dialogue/DialogueTypes.h b/src/dialogue/DialogueTypes.h index 342c6d2..e64dd19 100644 --- a/src/dialogue/DialogueTypes.h +++ b/src/dialogue/DialogueTypes.h @@ -83,6 +83,7 @@ struct Node { std::string text; std::string portrait; std::string next; + std::string luaCallback; // For Condition nodes std::string trueNext; @@ -101,6 +102,7 @@ struct DialogueDefinition { std::string id; std::string displayName; std::string startNode; + bool uninterruptible = false; std::unordered_map nodes; }; @@ -110,6 +112,7 @@ struct CutsceneLine { std::string portrait; std::string sfx; std::string background; + std::string luaCallback; int backgroundWidth = 0; // 0 = inherit from cutscene int backgroundHeight = 0; // 0 = inherit from cutscene int durationMs = 0; @@ -151,6 +154,7 @@ struct StaticCutsceneDefinition { int backgroundWidth = 1280; int backgroundHeight = 720; std::string music; + std::string onFadeInCallback; bool skippable = true; int durationMs = 0; int fadeOutMs = 0;