diff --git a/resources/config2/items.json b/resources/config2/items.json index d4da837..a17c554 100644 --- a/resources/config2/items.json +++ b/resources/config2/items.json @@ -13,6 +13,13 @@ "description": "Это мой журнал куда я вношу свои заметки.", "icon": "resources/w/ui/img/inv/ItemJournal001.png", "selectedIcon": "resources/w/ui/img/inv/ItemSelJournal001.png" + }, + { + "id": "teacher_room_key", + "name": "Ключ от учительской", + "description": "Это ключ от учительской, который я получил от Айпери.", + "icon": "resources/w/ui/img/inv/ItemKey001.png", + "selectedIcon": "resources/w/ui/img/inv/ItemSelKey001.png" }, { "id": "knife", diff --git a/resources/dialogue/dorm_dialogues.json b/resources/dialogue/dorm_dialogues.json index 818a91f..ef4870b 100644 --- a/resources/dialogue/dorm_dialogues.json +++ b/resources/dialogue/dorm_dialogues.json @@ -184,7 +184,8 @@ "portrait": "resources/dialogue/portrait_phone.png", "text": "Так что жду тебя в универе! Не вздумай прогулять!", "next": "end_1", - "bubbleSlot": "message11in" + "bubbleSlot": "message11in", + "questUnlock" : "aiperi_knife" }, { "id": "end_1", diff --git a/resources/dialogue/uni_interior_dialogues.json b/resources/dialogue/uni_interior_dialogues.json index 076d675..9a1901a 100644 --- a/resources/dialogue/uni_interior_dialogues.json +++ b/resources/dialogue/uni_interior_dialogues.json @@ -17,7 +17,41 @@ "type": "Line", "speaker": "Айпери", "portrait": "resources/dialogue/portrait_aiperi.png", - "text": "Пока ты не заберешь нож из учительской, никуда я тебя не выпущу.", + "text": "Вот тебе ключ от учительской. Иди туда и забери нож.", + "next": "line_3", + "luaCallback" : "dialog_aiperi_give_key" + }, + { + "id": "line_3", + "type": "Line", + "speaker": "Айпери", + "portrait": "resources/dialogue/portrait_aiperi.png", + "text": "Пока ты не принесешь мне нож из учительской, никуда я тебя не выпущу.", + "next": "end_1" + }, + { + "id": "end_1", + "type": "End" + } + ] + }, + "id": "knife_dialog002", + "start": "line_1", + "nodes": [ + { + "id": "line_1", + "type": "Line", + "speaker": "Айпери", + "portrait": "resources/dialogue/portrait_aiperi.png", + "text": "Ты куда собрался, Бекзат?", + "next": "line_2" + }, + { + "id": "line_2", + "type": "Line", + "speaker": "Айпери", + "portrait": "resources/dialogue/portrait_aiperi.png", + "text": "Пока ты не принесешь мне нож из учительской, никуда я тебя не выпущу.", "next": "end_1" }, { @@ -244,6 +278,24 @@ "type": "End" } ] + }, + { + "id": "door_teacher_dialog001", + "start": "line_1", + "nodes": [ + { + "id": "line_1", + "type": "Line", + "speaker": "Бекзат", + "portrait": "resources/dialogue/portrait_hero_neutral.png", + "text": "Это дверь в учительскую, и она закрыта. Мне нужно взять ключи у Айпери.", + "next": "end_1" + }, + { + "id": "end_1", + "type": "End" + } + ] }, { "id": "door_unlock_dialog001", @@ -374,6 +426,8 @@ "speaker": "Бекзат", "portrait": "resources/dialogue/portrait_hero_neutral.png", "text": "Ладно...", + "objectiveComplete" : "study_beginning.study_beginning_task", + "questUnlock" : "study_project", "next": "end_1" }, { diff --git a/resources/quests/quests.json b/resources/quests/quests.json index 9c8d997..be5c7b1 100644 --- a/resources/quests/quests.json +++ b/resources/quests/quests.json @@ -1,51 +1,82 @@ { "quests": [ { - "id": "tutorial_open_journal", - "title": "Журнал заданий", - "category": "Tutorial", - "status": "Completed", - "recommendedLevel": 0, - "description": "Теперь у героя есть журнал заданий. В нём можно просматривать текущие поручения, цели и подробные записи о событиях, которые уже произошли в мире игры.", + "id": "tutorial_take_items", + "title": "Телефон и Журнал", + "status": "Available", + "autoComplete": true, + "description": "Когда я куда-то собираюсь идти, я всегда беру с собой свой телефон и свой журнал.", "objectives": [ - { "id": "open_journal", "text": "Открыть журнал заданий", "completed": true } + { "id": "take_phone", "text": "Взять телефон", "completed": false }, + { "id": "take_journal", "text": "Взять журнал", "completed": false } ] }, { - "id": "side_lost_bag", - "title": "Потерянная сумка", - "category": "Side", - "status": "Completed", - "recommendedLevel": 1, - "description": "Путник потерял сумку недалеко от лесной тропы. Внутри оказались вещи, важные для его дальнейшего пути. Возвращение сумки укрепило доверие местных жителей к герою.", + "id": "study_beginning", + "title": "Манасоведение", + "status": "Available", + "autoComplete": true, + "description": "Скоро будет модуль по Манасоведению, а я еще ни разу не ходил на лекции. Сегодня у нас лекция по Манасоведению, надо ее посетить и получить у преподавательницы задание на модуль.", "objectives": [ - { "id": "find_bag", "text": "Найти сумку у лесной тропы", "completed": true }, - { "id": "bring_bag", "text": "Вернуть сумку путнику", "completed": true } + { "id": "study_beginning_lecture", "text": "Посетить лекцию", "completed": false }, + { "id": "study_beginning_task", "text": "Получить задание на модуль", "completed": false } ] }, { - "id": "failed_missing_courier", - "title": "След курьера", - "category": "Side", - "status": "Failed", - "recommendedLevel": 2, - "description": "Курьер исчез до того, как герой смог выйти на его след. Местные говорят, что на дороге остались только следы копыт и обрывок ремня. Возможность узнать, кто забрал посылку, была упущена.", + "id": "study_project", + "title": "Эссе манасчи", + "status": "Hidden", + "description": "Преподавательница по манасоведению Аида Джаныбекова дала мне задачу - найти в библиотеке книгу про манасчи Жусупа Мамая, и написать по этой книге эссе. Книгу нельзя выносить из библиотеки, но можно воспользоваться стоящим в библиотеке компьютером, чтобы написать эссе.", "objectives": [ - { "id": "ask_innkeeper", "text": "Расспросить трактирщика", "completed": true }, - { "id": "find_courier", "text": "Найти курьера до наступления ночи", "completed": false } + { "id": "study_project_book", "text": "Найти книгу", "completed": false }, + { "id": "study_project_write", "text": "Написать эссе", "completed": false }, + { "id": "study_project_give", "text": "Сдать эссе", "completed": false } ] }, { - "id": "contract_old_well", - "title": "Старый колодец", - "category": "Contract", - "status": "Active", - "recommendedLevel": 2, - "description": "На окраине деревни стоит старый колодец. Ночью из него слышны голоса, а утром вокруг находят мокрые следы. Местные боятся подходить к нему, но староста считает, что причина происходящего связана с давним проклятием.", + "id": "ghost_lore", + "title": "Призрак", + "status": "Hidden", + "autoComplete": true, + "description": "В университете каждую ночь появляется призрак студентки. Она заявляет, что ее зовут Бегимай и что она должна сдать курсовую работу. Я не помню студентки с таким именем, но может быть она с другого курса? Я должен узнать об этом подробнее.", "objectives": [ - { "id": "inspect_well", "text": "Осмотреть старый колодец", "completed": false }, - { "id": "search_tracks", "text": "Найти следы возле каменной ограды", "completed": false }, - { "id": "report_elder", "text": "Вернуться к старосте", "completed": false } + { "id": "ghost_lore_teacher", "text": "Поговорить с Аидой Джаныбековой", "completed": false }, + { "id": "ghost_lore_aiperi", "text": "Поговорить с Айпери", "completed": false }, + { "id": "ghost_lore_alik", "text": "Поговорить с Аликом", "completed": false, "visible": false } + ] + }, + { + "id": "ghost_coursework", + "title": "Курсовая Призрака", + "status": "Hidden", + "autoComplete": true, + "description": "История Бегимай еще более загадочная. Во время сдачи курсовой работы в университете происходила генеральная уборка. По случайности, ее курсовая работа оказалась в стопке бумаг на выброс. В результате курсач оказался на помойке, а преподаватель, не обнаружив курсовую работу, поставил ей ноль баллов. По словам Алика, курсовая работа до сих пор лежит в куче мусора во дворе университета.", + "objectives": [ + { "id": "ghost_coursework_find", "text": "Найти курсовую работу Бегимай", "completed": false }, + { "id": "ghost_coursework_mark", "text": "Показать курсовую работу Аиде Джаныбековой", "completed": false } + ] + }, + { + "id": "ghost_release", + "title": "Освобождение", + "status": "Hidden", + "autoComplete": true, + "description": "Студентка по имени Бегимай действительно училась в университете в прошлом году. Она пыталась сдать курсовую работу, но по стечению обстоятельств она не смогла это сделать, и получила ноль баллов. Эта курсовая сломала все ее планы и надежды, и Бегимай выбросилась из окна. Теперь она вернулась в виде призрака, и чтобы получить освобождение, она должна убедиться что ей выставили оценку за курсовую работу.", + "objectives": [ + { "id": "ghost_release_reportcard", "text": "Найти зачетку Бегимай", "completed": false }, + { "id": "ghost_release_mark", "text": "Поставить оценку за курсовую в зачетку", "completed": false }, + { "id": "ghost_release_show", "text": "Показать зачетку с оценкой призраку", "completed": false } + ] + }, + { + "id": "aiperi_knife", + "title": "Серебряный нож", + "status": "Hidden", + "description": "Айпери одолжила мне серебряный нож, который мы использовали чтобы нарезать торт преподавателям на день рождения. Нож до сих пор где-то лежит в учительской, и я должен его забрать и вернуть Айпери.", + "objectives": [ + { "id": "aiperi_knife_keys", "text": "Получить ключи от учительской", "completed": false }, + { "id": "aiperi_knife_take", "text": "Забрать нож", "completed": false }, + { "id": "aiperi_knife_give", "text": "Отдать нож Айпери", "completed": false } ] } ] diff --git a/resources/start_dorm.lua b/resources/start_dorm.lua index 6eedfa7..1907238 100644 --- a/resources/start_dorm.lua +++ b/resources/start_dorm.lua @@ -50,6 +50,7 @@ game_api.pickup_item("phone") game_api.deactivate_interactive_object("Phone001") game_api.start_dialogue("dialog_phone_pickup001") phone_picked_up = true +game_api.quest_set_objective_completed("tutorial_take_items", "take_phone") end function on_journal_pickup() @@ -57,6 +58,7 @@ game_api.pickup_item("journal") game_api.deactivate_interactive_object("Journal001") game_api.start_dialogue("dialog_journal_pickup001") journal_picked_up = true +game_api.quest_set_objective_completed("tutorial_take_items", "take_journal") end --[[ @@ -203,12 +205,12 @@ game_api.set_location_callbacks( game_api.deactivate_interactive_object("Room_Cover_LivingRoom_W_N_2_001") --debug -game_api.deactivate_interactive_object("Room_Cover_Bath_W_N_2_001") +--game_api.deactivate_interactive_object("Room_Cover_Bath_W_N_2_001") -game_api.deactivate_interactive_object("Room_Cover_LivingRoom_W_S_2_001") -game_api.deactivate_interactive_object("Room_Cover_Main_Hall_And_Corridors_002") -game_api.deactivate_interactive_object("Room_Cover_Utility_W_N_3_001") -game_api.deactivate_interactive_object("Room_Cover_Utility_W_S_3_001") +--game_api.deactivate_interactive_object("Room_Cover_LivingRoom_W_S_2_001") +--game_api.deactivate_interactive_object("Room_Cover_Main_Hall_And_Corridors_002") +--game_api.deactivate_interactive_object("Room_Cover_Utility_W_N_3_001") +--game_api.deactivate_interactive_object("Room_Cover_Utility_W_S_3_001") --debug end game_api.rotate_object("Door_Utility_-1_1_2_Leaf_001", 90, 0.01, nil) game_api.rotate_object("Door_Utility_-1_-1_2_Leaf_001", -90, 0.01, nil) diff --git a/resources/start_uni_interior.lua b/resources/start_uni_interior.lua index 78fd4d2..52f5efe 100644 --- a/resources/start_uni_interior.lua +++ b/resources/start_uni_interior.lua @@ -6,6 +6,7 @@ lection_is_over = false player_hold_book = false player_hold_knife = false +player_hold_key = false teacher_arrived = false teacher_told_about_book = false @@ -21,12 +22,22 @@ player_ghost_aware = false ghost_gone = false +function dialog_aiperi_give_key() +if (player_hold_key == false) then +game_api.pickup_item("teacher_room_key") +game_api.quest_unlock("aiperi_knife") +game_api.quest_set_objective_completed("aiperi_knife", "aiperi_knife_keys") +player_hold_key = true +end +end + 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") + game_api.quest_set_objective_completed("study_beginning", "study_beginning_lecture") end end @@ -42,7 +53,11 @@ function knife_dialog_zone001_enter_callback() if (day == 0) then if (player_hold_knife == false) then if lection_is_over then + if (player_hold_key) then + game_api.start_dialogue("knife_dialog002") + else game_api.start_dialogue("knife_dialog001") + end game_api.switch_navigation(4) end else @@ -82,6 +97,7 @@ game_api.set_npc_enabled(1, false) player_hold_knife = true game_api.set_trigger_zone_enabled(2, true) game_api.npc_walk_to(0, -4.57412, 0, 6.78495, on_teacher_arrived2) +game_api.quest_set_objective_completed("aiperi_knife", "aiperi_knife_take") end function on_book_pickup() @@ -136,7 +152,11 @@ function on_npc_interact(npc_index) local day = game_api.getIntValue("day") if (day == 0) then + if (player_hold_key) then + game_api.start_dialogue("knife_dialog002") + else game_api.start_dialogue("knife_dialog001") + end else if (player_ghost_aware) then local player_alik_aware = game_api.getIntValue("player_alik_aware") @@ -261,8 +281,8 @@ function on_teachers_door_click() game_api.start_dialogue("door_dialog001") end - elseif (not teacher_arrived) then - game_api.start_dialogue("door_dialog001") + elseif (not player_hold_key) then + game_api.start_dialogue("door_teacher_dialog001") elseif (not teacher_door_opened) then teacher_door_opened = true game_api.rotate_object("Room_S_2_Leaf001", 90, 0.5, nil) diff --git a/resources/w/ui/img/inv/ItemKey001.png b/resources/w/ui/img/inv/ItemKey001.png new file mode 100644 index 0000000..92aa5f2 --- /dev/null +++ b/resources/w/ui/img/inv/ItemKey001.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9a27befd75206a4484b41f3ff197bc0c4b1104a3aeb5f67e77997810797b78f1 +size 82714 diff --git a/resources/w/ui/img/inv/ItemSelKey001.png b/resources/w/ui/img/inv/ItemSelKey001.png new file mode 100644 index 0000000..6af2a51 --- /dev/null +++ b/resources/w/ui/img/inv/ItemSelKey001.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cd2f9e7dadb419c28ef3f4b1d63ea7c13c77ca19512095384136aa50048e2374 +size 374064 diff --git a/src/Game.cpp b/src/Game.cpp index afdd261..93f54f9 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -15,7 +15,6 @@ #endif - #ifdef EMSCRIPTEN #include #endif @@ -259,7 +258,7 @@ namespace ZL uniInteriorParams.playerPosition = Eigen::Vector3f(0.942694, 0, -9.63104); locations["uni_interior"] = std::make_shared(renderer, inventory); - locations["uni_interior"]->setup(uniInteriorParams); + locations["uni_interior"]->setup(uniInteriorParams, &menuManager.questJournal); locations["uni_interior"]->scriptEngine.setGlobalStore(&globalInts); locations["uni_interior"]->scriptEngine.setGlobalFloatStore(&globalFloats); locations["uni_interior"]->requestDarklandsTransition = [this]() { return startDarklandsTransition(); }; @@ -286,7 +285,7 @@ namespace ZL uniExteriorParams.dialoguesJsonPath = "resources/dialogue/uni_exterior_dialogues.json"; locations["uni_exterior"] = std::make_shared(renderer, inventory); - locations["uni_exterior"]->setup(uniExteriorParams); + locations["uni_exterior"]->setup(uniExteriorParams, &menuManager.questJournal); locations["uni_exterior"]->scriptEngine.setGlobalStore(&globalInts); locations["uni_exterior"]->scriptEngine.setGlobalFloatStore(&globalFloats); locations["uni_exterior"]->requestDarklandsTransition = [this]() { return startDarklandsTransition(); }; @@ -346,7 +345,7 @@ namespace ZL params_dorm.playerPosition = Eigen::Vector3f(6.76345, 0, -14.6022); locations["location_dorm"] = std::make_shared(renderer, inventory); - locations["location_dorm"]->setup(params_dorm); + locations["location_dorm"]->setup(params_dorm, &menuManager.questJournal); locations["location_dorm"]->scriptEngine.setGlobalStore(&globalInts); locations["location_dorm"]->scriptEngine.setGlobalFloatStore(&globalFloats); locations["location_dorm"]->requestDarklandsTransition = [this]() { return startDarklandsTransition(); }; diff --git a/src/Location.cpp b/src/Location.cpp index 5ad7da4..aed6e5f 100644 --- a/src/Location.cpp +++ b/src/Location.cpp @@ -48,7 +48,7 @@ namespace ZL { } - void Location::setup(const LocationSetup& params) + void Location::setup(const LocationSetup& params, Quest::QuestJournal* journal) { // Load static game objects @@ -130,6 +130,7 @@ namespace ZL dialogueSystem.init(renderer, CONST_ZIP_FILE); dialogueSystem.loadDatabase(params.dialoguesJsonPath); + dialogueSystem.setQuestJournal(journal); npcNameText = std::make_unique(); if (!npcNameText->init(renderer, "resources/fonts/DroidSans.ttf", 24, CONST_ZIP_FILE)) { @@ -138,6 +139,7 @@ namespace ZL } scriptEngine.init(this, &inventory, params.scriptPath); + scriptEngine.setQuestJournal(journal); dialogueSystem.setOnCutsceneFinished([this](const std::string& cutsceneId) { scriptEngine.callCutsceneCompleteCallback(cutsceneId); diff --git a/src/Location.h b/src/Location.h index 9e878a7..531b594 100644 --- a/src/Location.h +++ b/src/Location.h @@ -112,7 +112,7 @@ namespace ZL int lastMouseY = 0; - void setup(const LocationSetup& params); + void setup(const LocationSetup& params, Quest::QuestJournal* journal); void setupNavigation(const std::vector& paths); bool switchNavigation(int index); InteractiveObject* raycastInteractiveObjects(const Eigen::Vector3f& rayOrigin, const Eigen::Vector3f& rayDir); diff --git a/src/MenuManager.cpp b/src/MenuManager.cpp index 907e744..ce56d04 100644 --- a/src/MenuManager.cpp +++ b/src/MenuManager.cpp @@ -8,11 +8,10 @@ namespace ZL { static int questStatusPriority(Quest::QuestStatus status) { switch (status) { - case Quest::QuestStatus::Active: return 0; - case Quest::QuestStatus::Available: return 1; - case Quest::QuestStatus::Completed: return 2; - case Quest::QuestStatus::Failed: return 3; - default: return 4; + case Quest::QuestStatus::Available: return 0; + case Quest::QuestStatus::Completed: return 1; + case Quest::QuestStatus::Failed: return 2; + default: return 3; } } @@ -58,8 +57,7 @@ namespace ZL { switch (status) { case Quest::QuestStatus::Completed: return { 0.25f, 0.95f, 0.35f, 1.0f }; case Quest::QuestStatus::Failed: return { 1.0f, 0.25f, 0.25f, 1.0f }; - case Quest::QuestStatus::Active: return { 1.0f, 1.0f, 1.0f, 1.0f }; - case Quest::QuestStatus::Available: return { 0.86f, 0.86f, 0.86f, 1.0f }; + case Quest::QuestStatus::Available: return { 1.0f, 1.0f, 1.0f, 1.0f }; default: return { 0.45f, 0.45f, 0.45f, 1.0f }; } } @@ -468,7 +466,7 @@ namespace ZL { const int pa = questStatusPriority(a->status); const int pb = questStatusPriority(b->status); if (pa != pb) return pa < pb; - return a->orderIndex > b->orderIndex; + return a->orderIndex < b->orderIndex; }); static const char* kItemNames[9] = { @@ -540,10 +538,14 @@ namespace ZL { static const char* kCheckboxes[3] = { "objective1checkbox", "objective2checkbox", "objective3checkbox" }; static const char* kObjNames[3] = { "objective1name", "objective2name", "objective3name" }; + std::vector visibleObjs; + for (const auto& obj : def.objectives) + if (obj.visible) visibleObjs.push_back(&obj); + for (int i = 0; i < 3; ++i) { - if (i < static_cast(def.objectives.size())) { - const auto& obj = def.objectives[i]; - const bool isActive = (i == quest->activeObjectiveIndex); + if (i < static_cast(visibleObjs.size())) { + const auto& obj = *visibleObjs[i]; + const bool isActive = (&obj - def.objectives.data() == quest->activeObjectiveIndex); auto img = uiManager.findStaticImage(kCheckboxes[i]); if (img) img->texture = obj.completed ? texObjectiveCompleted_ : texObjectiveBlank_; diff --git a/src/ScriptEngine.cpp b/src/ScriptEngine.cpp index e3d4cab..6f32abb 100644 --- a/src/ScriptEngine.cpp +++ b/src/ScriptEngine.cpp @@ -20,6 +20,7 @@ namespace ZL { std::unordered_map npcBumpsPlayerCallbacks; std::unordered_map* globalInts = nullptr; std::unordered_map* globalFloats = nullptr; + Quest::QuestJournal* questJournal = nullptr; sol::protected_function locationEnterCallback; sol::protected_function locationExitCallback; sol::protected_function darklandsEnterCallback; @@ -505,6 +506,68 @@ namespace ZL { this_impl->npcBumpsPlayerCallbacks[index] = onNpcBumpsPlayer.as(); }); + // quest_unlock(quest_id) + api.set_function("quest_unlock", + [this_impl = impl.get()](const std::string& questId) -> bool { + if (!this_impl->questJournal) { + std::cerr << "[script] quest_unlock: QuestJournal not set\n"; + return false; + } + return this_impl->questJournal->unlockQuest(questId); + }); + + // quest_complete(quest_id) + api.set_function("quest_complete", + [this_impl = impl.get()](const std::string& questId) -> bool { + if (!this_impl->questJournal) { + std::cerr << "[script] quest_complete: QuestJournal not set\n"; + return false; + } + return this_impl->questJournal->completeQuest(questId); + }); + + // quest_fail(quest_id) + api.set_function("quest_fail", + [this_impl = impl.get()](const std::string& questId) -> bool { + if (!this_impl->questJournal) { + std::cerr << "[script] quest_fail: QuestJournal not set\n"; + return false; + } + return this_impl->questJournal->failQuest(questId); + }); + + // quest_set_objective_completed(quest_id, objective_id [, completed]) + api.set_function("quest_set_objective_completed", + [this_impl = impl.get()](const std::string& questId, const std::string& objId, sol::object completed) -> bool { + if (!this_impl->questJournal) { + std::cerr << "[script] quest_set_objective_completed: QuestJournal not set\n"; + return false; + } + const bool val = completed.is() ? completed.as() : true; + return this_impl->questJournal->setObjectiveCompleted(questId, objId, val); + }); + + // quest_set_objective_visible(quest_id, objective_id [, visible]) + api.set_function("quest_set_objective_visible", + [this_impl = impl.get()](const std::string& questId, const std::string& objId, sol::object visible) -> bool { + if (!this_impl->questJournal) { + std::cerr << "[script] quest_set_objective_visible: QuestJournal not set\n"; + return false; + } + const bool val = visible.is() ? visible.as() : true; + return this_impl->questJournal->setObjectiveVisible(questId, objId, val); + }); + + // quest_set_active_objective(quest_id, objective_index) + api.set_function("quest_set_active_objective", + [this_impl = impl.get()](const std::string& questId, int index) -> bool { + if (!this_impl->questJournal) { + std::cerr << "[script] quest_set_active_objective: QuestJournal not set\n"; + return false; + } + return this_impl->questJournal->setActiveObjective(questId, index); + }); + lua.script_file(scriptPath); } @@ -624,6 +687,10 @@ namespace ZL { if (impl) impl->globalFloats = store; } + void ScriptEngine::setQuestJournal(Quest::QuestJournal* journal) { + if (impl) impl->questJournal = journal; + } + void ScriptEngine::callLocationEnterCallback() { if (!impl || !impl->locationEnterCallback.valid()) return; auto result = impl->locationEnterCallback(); diff --git a/src/ScriptEngine.h b/src/ScriptEngine.h index 682bf4f..58a78fe 100644 --- a/src/ScriptEngine.h +++ b/src/ScriptEngine.h @@ -2,6 +2,7 @@ #include #include #include +#include "quest/QuestJournal.h" namespace ZL { @@ -35,6 +36,7 @@ public: void setGlobalStore(std::unordered_map* store); void setGlobalFloatStore(std::unordered_map* store); + void setQuestJournal(Quest::QuestJournal* journal); void callLocationEnterCallback(); void callLocationExitCallback(); diff --git a/src/dialogue/DialogueDatabase.cpp b/src/dialogue/DialogueDatabase.cpp index 0e658ef..eddb4ab 100644 --- a/src/dialogue/DialogueDatabase.cpp +++ b/src/dialogue/DialogueDatabase.cpp @@ -106,6 +106,11 @@ Node DialogueDatabase::parseNode(const json& j) { node.cutsceneId = j.value("cutsceneId", ""); node.luaCallback = j.value("luaCallback", ""); node.bubbleSlot = j.value("bubbleSlot", ""); + node.questUnlock = j.value("questUnlock", ""); + node.questComplete = j.value("questComplete", ""); + node.questFail = j.value("questFail", ""); + node.objectiveComplete = j.value("objectiveComplete", ""); + node.objectiveVisible = j.value("objectiveVisible", ""); if (j.contains("conditions") && j["conditions"].is_array()) { for (const auto& item : j["conditions"]) { @@ -157,6 +162,11 @@ CutsceneLine DialogueDatabase::parseCutsceneLine(const json& j) { line.luaCallback = j.value("luaCallback", ""); line.durationMs = j.value("durationMs", 0); line.waitForConfirm = j.value("waitForConfirm", false); + line.questUnlock = j.value("questUnlock", ""); + line.questComplete = j.value("questComplete", ""); + line.questFail = j.value("questFail", ""); + line.objectiveComplete = j.value("objectiveComplete", ""); + line.objectiveVisible = j.value("objectiveVisible", ""); return line; } diff --git a/src/dialogue/DialogueRuntime.cpp b/src/dialogue/DialogueRuntime.cpp index 678548d..f22cd90 100644 --- a/src/dialogue/DialogueRuntime.cpp +++ b/src/dialogue/DialogueRuntime.cpp @@ -5,7 +5,13 @@ #include namespace ZL::Dialogue { - + +static std::pair splitDot(const std::string& s) { + const auto dot = s.find('.'); + if (dot == std::string::npos) return {s, ""}; + return {s.substr(0, dot), s.substr(dot + 1)}; +} + void DialogueRuntime::setDatabase(const DialogueDatabase* value) { database = value; } @@ -309,6 +315,29 @@ int DialogueRuntime::getFlag(const std::string& name) const { return (it != flags.end()) ? it->second : 0; } +void DialogueRuntime::setQuestJournal(Quest::QuestJournal* journal) { + questJournal = journal; +} + +void DialogueRuntime::applyQuestActions( + const std::string& questUnlock, const std::string& questComplete, + const std::string& questFail, const std::string& objectiveComplete, + const std::string& objectiveVisible) +{ + if (!questJournal) return; + if (!questUnlock.empty()) questJournal->unlockQuest(questUnlock); + if (!questComplete.empty()) questJournal->completeQuest(questComplete); + if (!questFail.empty()) questJournal->failQuest(questFail); + if (!objectiveComplete.empty()) { + auto [qId, oId] = splitDot(objectiveComplete); + questJournal->setObjectiveCompleted(qId, oId); + } + if (!objectiveVisible.empty()) { + auto [qId, oId] = splitDot(objectiveVisible); + questJournal->setObjectiveVisible(qId, oId); + } +} + bool DialogueRuntime::evaluateConditions(const std::vector& conditions) const { for (const Condition& condition : conditions) { const int currentValue = getFlag(condition.flag); @@ -432,6 +461,8 @@ void DialogueRuntime::presentLine(const Node& node) { revealCharacters = static_cast(node.text.size()); } + applyQuestActions(node.questUnlock, node.questComplete, node.questFail, + node.objectiveComplete, node.objectiveVisible); if (!node.luaCallback.empty() && onDialogueLineStarted) { onDialogueLineStarted(node.luaCallback); } @@ -530,9 +561,12 @@ 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); + if (!activeCutscene->lines.empty()) { + const CutsceneLine& firstLine = activeCutscene->lines[0]; + applyQuestActions(firstLine.questUnlock, firstLine.questComplete, + firstLine.questFail, firstLine.objectiveComplete, firstLine.objectiveVisible); + if (onCutsceneLineStarted && !firstLine.luaCallback.empty()) + onCutsceneLineStarted(firstLine.luaCallback); } std::cout << "[CUTSCENE] start id=" << cutsceneId @@ -621,10 +655,11 @@ void DialogueRuntime::advanceCutsceneLine() { return; } - if (onCutsceneLineStarted) { - const std::string& cb = activeCutscene->lines[currentCutsceneLine].luaCallback; - if (!cb.empty()) onCutsceneLineStarted(cb); - } + const CutsceneLine& newLine = activeCutscene->lines[currentCutsceneLine]; + applyQuestActions(newLine.questUnlock, newLine.questComplete, + newLine.questFail, newLine.objectiveComplete, newLine.objectiveVisible); + if (onCutsceneLineStarted && !newLine.luaCallback.empty()) + onCutsceneLineStarted(newLine.luaCallback); refreshCutscenePresentation(); } diff --git a/src/dialogue/DialogueRuntime.h b/src/dialogue/DialogueRuntime.h index de154de..0c8ab09 100644 --- a/src/dialogue/DialogueRuntime.h +++ b/src/dialogue/DialogueRuntime.h @@ -1,6 +1,7 @@ #pragma once #include "dialogue/DialogueDatabase.h" +#include "quest/QuestJournal.h" #include "external/nlohmann/json.hpp" #include #include @@ -42,6 +43,8 @@ public: void setFlag(const std::string& name, int value); int getFlag(const std::string& name) const; + void setQuestJournal(Quest::QuestJournal* journal); + json buildSaveState() const; bool restoreSaveState(const json& state); @@ -62,6 +65,7 @@ private: bool fadeInCallbackFired = false; const DialogueDatabase* database = nullptr; + Quest::QuestJournal* questJournal = nullptr; const DialogueDefinition* activeDialogue = nullptr; const StaticCutsceneDefinition* activeCutscene = nullptr; @@ -89,6 +93,9 @@ private: bool evaluateConditions(const std::vector& conditions) const; void applyEffects(const std::vector& effects); + void applyQuestActions(const std::string& questUnlock, const std::string& questComplete, + const std::string& questFail, const std::string& objectiveComplete, + const std::string& objectiveVisible); bool enterNode(const std::string& nodeId); void presentLine(const Node& node); diff --git a/src/dialogue/DialogueSystem.h b/src/dialogue/DialogueSystem.h index b869ddb..85f1683 100644 --- a/src/dialogue/DialogueSystem.h +++ b/src/dialogue/DialogueSystem.h @@ -2,6 +2,7 @@ #include "dialogue/DialogueOverlay.h" #include "dialogue/DialogueRuntime.h" +#include "quest/QuestJournal.h" #include #include #include @@ -37,6 +38,8 @@ public: void setFlag(const std::string& name, int value) { runtime.setFlag(name, value); } int getFlag(const std::string& name) const { return runtime.getFlag(name); } + void setQuestJournal(Quest::QuestJournal* journal) { runtime.setQuestJournal(journal); } + private: DialogueDatabase database; DialogueRuntime runtime; diff --git a/src/dialogue/DialogueTypes.h b/src/dialogue/DialogueTypes.h index 69d723b..1a372e4 100644 --- a/src/dialogue/DialogueTypes.h +++ b/src/dialogue/DialogueTypes.h @@ -99,6 +99,13 @@ struct Node { // Name of the UI node (StaticImage) to reveal in the phone chat when this line is shown std::string bubbleSlot; + + // Quest actions fired when this line is presented (empty = no action) + std::string questUnlock; + std::string questComplete; + std::string questFail; + std::string objectiveComplete; // "quest_id.objective_id" + std::string objectiveVisible; // "quest_id.objective_id" }; struct DialogueDefinition { @@ -120,6 +127,13 @@ struct CutsceneLine { int backgroundHeight = 0; // 0 = inherit from cutscene int durationMs = 0; bool waitForConfirm = false; + + // Quest actions fired when this line is shown (empty = no action) + std::string questUnlock; + std::string questComplete; + std::string questFail; + std::string objectiveComplete; // "quest_id.objective_id" + std::string objectiveVisible; // "quest_id.objective_id" }; struct CutsceneCameraPose { diff --git a/src/quest/QuestJournal.cpp b/src/quest/QuestJournal.cpp index 5e9ae3a..0b2fb19 100644 --- a/src/quest/QuestJournal.cpp +++ b/src/quest/QuestJournal.cpp @@ -12,38 +12,19 @@ const char* toString(QuestStatus status) { switch (status) { case QuestStatus::Hidden: return "Hidden"; case QuestStatus::Available: return "Available"; - case QuestStatus::Active: return "Active"; case QuestStatus::Completed: return "Completed"; case QuestStatus::Failed: return "Failed"; default: return "Unknown"; } } -const char* toString(QuestCategory category) { - switch (category) { - case QuestCategory::Main: return "Main"; - case QuestCategory::Side: return "Side"; - case QuestCategory::Contract: return "Contract"; - case QuestCategory::Tutorial: return "Tutorial"; - default: return "Unknown"; - } -} - static QuestStatus parseQuestStatus(const std::string& value) { if (value == "Available") return QuestStatus::Available; - if (value == "Active") return QuestStatus::Active; if (value == "Completed") return QuestStatus::Completed; if (value == "Failed") return QuestStatus::Failed; return QuestStatus::Hidden; } -static QuestCategory parseQuestCategory(const std::string& value) { - if (value == "Main") return QuestCategory::Main; - if (value == "Contract") return QuestCategory::Contract; - if (value == "Tutorial") return QuestCategory::Tutorial; - return QuestCategory::Side; -} - bool QuestJournal::loadFromFile(const std::string& path, const std::string& zipFile) { quests.clear(); questOrder.clear(); @@ -87,9 +68,8 @@ bool QuestJournal::loadFromFile(const std::string& path, const std::string& zipF def.id = item.value("id", ""); def.title = item.value("title", def.id); def.description = item.value("description", ""); - def.category = parseQuestCategory(item.value("category", "Side")); def.initialStatus = parseQuestStatus(item.value("status", "Hidden")); - def.recommendedLevel = item.value("recommendedLevel", 0); + def.autoComplete = item.value("autoComplete", false); if (item.contains("objectives") && item["objectives"].is_array()) { for (const auto& obj : item["objectives"]) { @@ -97,6 +77,7 @@ bool QuestJournal::loadFromFile(const std::string& path, const std::string& zipF objective.id = obj.value("id", ""); objective.text = obj.value("text", ""); objective.completed = obj.value("completed", false); + objective.visible = obj.value("visible", true); if (!objective.id.empty()) { def.objectives.push_back(objective); } @@ -136,10 +117,6 @@ bool QuestJournal::unlockQuest(const std::string& questId) { return true; } -bool QuestJournal::startQuest(const std::string& questId) { - return setStatus(questId, QuestStatus::Active); -} - bool QuestJournal::completeQuest(const std::string& questId) { return setStatus(questId, QuestStatus::Completed); } @@ -155,6 +132,30 @@ bool QuestJournal::setObjectiveCompleted(const std::string& questId, const std:: for (auto& objective : quest->definition.objectives) { if (objective.id == objectiveId) { objective.completed = completed; + + if (completed && quest->definition.autoComplete && quest->status == QuestStatus::Available) { + const bool allDone = std::all_of( + quest->definition.objectives.begin(), + quest->definition.objectives.end(), + [](const QuestObjective& o) { return o.completed; }); + if (allDone) + quest->status = QuestStatus::Completed; + } + + return true; + } + } + + return false; +} + +bool QuestJournal::setObjectiveVisible(const std::string& questId, const std::string& objectiveId, bool visible) { + QuestState* quest = findQuest(questId); + if (!quest) return false; + + for (auto& objective : quest->definition.objectives) { + if (objective.id == objectiveId) { + objective.visible = visible; return true; } } diff --git a/src/quest/QuestJournal.h b/src/quest/QuestJournal.h index 12eec2d..8b3312a 100644 --- a/src/quest/QuestJournal.h +++ b/src/quest/QuestJournal.h @@ -12,11 +12,11 @@ public: bool loadFromFile(const std::string& path, const std::string& zipFile = ""); bool unlockQuest(const std::string& questId); - bool startQuest(const std::string& questId); bool completeQuest(const std::string& questId); bool failQuest(const std::string& questId); bool setObjectiveCompleted(const std::string& questId, const std::string& objectiveId, bool completed = true); + bool setObjectiveVisible(const std::string& questId, const std::string& objectiveId, bool visible = true); bool setActiveObjective(const std::string& questId, int objectiveIndex); QuestState* findQuest(const std::string& questId); diff --git a/src/quest/QuestTypes.h b/src/quest/QuestTypes.h index a9ca481..9deb493 100644 --- a/src/quest/QuestTypes.h +++ b/src/quest/QuestTypes.h @@ -8,31 +8,23 @@ namespace ZL::Quest { enum class QuestStatus { Hidden, Available, - Active, Completed, Failed }; -enum class QuestCategory { - Main, - Side, - Contract, - Tutorial -}; - struct QuestObjective { std::string id; std::string text; bool completed = false; + bool visible = true; }; struct QuestDefinition { std::string id; std::string title; std::string description; - QuestCategory category = QuestCategory::Side; QuestStatus initialStatus = QuestStatus::Hidden; - int recommendedLevel = 0; + bool autoComplete = false; std::vector objectives; }; @@ -44,6 +36,5 @@ struct QuestState { }; const char* toString(QuestStatus status); -const char* toString(QuestCategory category); } // namespace ZL::Quest