diff --git a/CMakeLists.txt b/CMakeLists.txt index 8de0b87b..abec4dc8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1291,6 +1291,7 @@ add_executable(wowee_editor tools/editor/object_placer.cpp tools/editor/npc_spawner.cpp tools/editor/npc_presets.cpp + tools/editor/quest_editor.cpp tools/editor/transform_gizmo.cpp tools/editor/zone_manifest.cpp tools/editor/asset_browser.cpp diff --git a/tools/editor/editor_app.cpp b/tools/editor/editor_app.cpp index d542000c..c254d80f 100644 --- a/tools/editor/editor_app.cpp +++ b/tools/editor/editor_app.cpp @@ -660,6 +660,12 @@ void EditorApp::exportZone(const std::string& outputDir) { npcSpawner_.saveToFile(npcPath); } + // Save quests + if (questEditor_.questCount() > 0) { + std::string questPath = base + "/quests.json"; + questEditor_.saveToFile(questPath); + } + // Save placed objects if (objectPlacer_.objectCount() > 0) { std::string objPath = base + "/objects.json"; diff --git a/tools/editor/editor_app.hpp b/tools/editor/editor_app.hpp index bb98e40d..b1a93717 100644 --- a/tools/editor/editor_app.hpp +++ b/tools/editor/editor_app.hpp @@ -8,6 +8,7 @@ #include "object_placer.hpp" #include "npc_spawner.hpp" #include "npc_presets.hpp" +#include "quest_editor.hpp" #include "zone_manifest.hpp" #include "asset_browser.hpp" #include "core/window.hpp" @@ -18,7 +19,7 @@ namespace wowee { namespace editor { -enum class EditorMode { Sculpt, Paint, PlaceObject, Water, NPC }; +enum class EditorMode { Sculpt, Paint, PlaceObject, Water, NPC, Quest }; class EditorApp { public: @@ -47,6 +48,7 @@ public: ObjectPlacer& getObjectPlacer() { return objectPlacer_; } NpcSpawner& getNpcSpawner() { return npcSpawner_; } NpcPresets& getNpcPresets() { return npcPresets_; } + QuestEditor& getQuestEditor() { return questEditor_; } AssetBrowser& getAssetBrowser() { return assetBrowser_; } rendering::TerrainRenderer* getTerrainRenderer(); rendering::M2Renderer* getM2Renderer() { return viewport_.getM2Renderer(); } @@ -110,6 +112,7 @@ private: ObjectPlacer objectPlacer_; NpcSpawner npcSpawner_; NpcPresets npcPresets_; + QuestEditor questEditor_; AssetBrowser assetBrowser_; pipeline::ADTTerrain terrain_; diff --git a/tools/editor/editor_ui.cpp b/tools/editor/editor_ui.cpp index 4cfa2400..12aabd9a 100644 --- a/tools/editor/editor_ui.cpp +++ b/tools/editor/editor_ui.cpp @@ -5,6 +5,7 @@ #include "object_placer.hpp" #include "npc_spawner.hpp" #include "npc_presets.hpp" +#include "quest_editor.hpp" #include "asset_browser.hpp" #include "transform_gizmo.hpp" #include "terrain_biomes.hpp" @@ -42,6 +43,7 @@ void EditorUI::render(EditorApp& app) { case EditorMode::PlaceObject: renderObjectPanel(app); break; case EditorMode::Water: renderWaterPanel(app); break; case EditorMode::NPC: renderNpcPanel(app); break; + case EditorMode::Quest: renderQuestPanel(app); break; } renderContextMenu(app); @@ -271,6 +273,7 @@ void EditorUI::renderToolbar(EditorApp& app) { modeButton("Objects", EditorMode::PlaceObject); modeButton("Water", EditorMode::Water); modeButton("NPCs", EditorMode::NPC); + modeButton("Quests", EditorMode::Quest); } ImGui::End(); ImGui::PopStyleVar(); @@ -1010,6 +1013,94 @@ void EditorUI::renderNpcPanel(EditorApp& app) { ImGui::End(); } +void EditorUI::renderQuestPanel(EditorApp& app) { + ImGuiViewport* vp = ImGui::GetMainViewport(); + ImGui::SetNextWindowPos(ImVec2(vp->Size.x - 400, 90), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowSize(ImVec2(390, 600), ImGuiCond_FirstUseEver); + if (ImGui::Begin("Quest Editor")) { + auto& qe = app.getQuestEditor(); + auto& tmpl = qe.getTemplate(); + + if (ImGui::CollapsingHeader("New Quest", ImGuiTreeNodeFlags_DefaultOpen)) { + static char titleBuf[128] = "New Quest"; + std::strncpy(titleBuf, tmpl.title.c_str(), sizeof(titleBuf) - 1); + if (ImGui::InputText("Title##q", titleBuf, sizeof(titleBuf))) + tmpl.title = titleBuf; + + static char descBuf[512] = ""; + std::strncpy(descBuf, tmpl.description.c_str(), sizeof(descBuf) - 1); + if (ImGui::InputTextMultiline("Description##q", descBuf, sizeof(descBuf), ImVec2(-1, 60))) + tmpl.description = descBuf; + + int lvl = tmpl.requiredLevel; + if (ImGui::InputInt("Required Level", &lvl)) tmpl.requiredLevel = std::max(1, lvl); + + int giver = tmpl.questGiverNpcId; + if (ImGui::InputInt("Giver NPC ID", &giver)) tmpl.questGiverNpcId = std::max(0, giver); + int turnin = tmpl.turnInNpcId; + if (ImGui::InputInt("Turn-in NPC ID", &turnin)) tmpl.turnInNpcId = std::max(0, turnin); + + ImGui::Separator(); + ImGui::Text("Objectives:"); + if (tmpl.objectives.size() < 4 && ImGui::Button("Add Objective")) { + QuestObjective obj; + obj.description = "Kill 5 creatures"; + tmpl.objectives.push_back(obj); + } + for (int oi = 0; oi < static_cast(tmpl.objectives.size()); oi++) { + auto& obj = tmpl.objectives[oi]; + ImGui::PushID(oi); + const char* types[] = {"Kill", "Collect", "Talk", "Explore", "Escort", "Use Object"}; + int ti = static_cast(obj.type); + ImGui::Combo("Type", &ti, types, 6); + obj.type = static_cast(ti); + static char objDesc[128]; + std::strncpy(objDesc, obj.description.c_str(), sizeof(objDesc) - 1); + if (ImGui::InputText("Desc", objDesc, sizeof(objDesc))) obj.description = objDesc; + int cnt = obj.targetCount; + if (ImGui::InputInt("Count", &cnt)) obj.targetCount = std::max(1, cnt); + if (ImGui::SmallButton("Remove")) tmpl.objectives.erase(tmpl.objectives.begin() + oi--); + ImGui::PopID(); + ImGui::Separator(); + } + + ImGui::Text("Rewards:"); + int xp = tmpl.reward.xp; + if (ImGui::InputInt("XP##qr", &xp)) tmpl.reward.xp = std::max(0, xp); + int gold = tmpl.reward.gold; + if (ImGui::InputInt("Gold##qr", &gold)) tmpl.reward.gold = std::max(0, gold); + + if (ImGui::Button("Create Quest", ImVec2(-1, 0))) { + qe.addQuest(tmpl); + app.showToast("Quest created: " + tmpl.title); + } + } + + ImGui::Separator(); + if (ImGui::CollapsingHeader("Quest List", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Text("%zu quests", qe.questCount()); + ImGui::BeginChild("QuestList", ImVec2(0, 120), true); + for (int i = 0; i < static_cast(qe.questCount()); i++) { + auto* q = qe.getQuest(i); + char lbl[128]; + std::snprintf(lbl, sizeof(lbl), "[%u] %s (Lv%u, %zu obj)", + q->id, q->title.c_str(), q->requiredLevel, q->objectives.size()); + if (ImGui::Selectable(lbl)) { /* select for editing */ } + } + ImGui::EndChild(); + } + + ImGui::Separator(); + static char questPath[256] = "output/quests.json"; + ImGui::InputText("File##quest", questPath, sizeof(questPath)); + if (ImGui::Button("Save Quests", ImVec2(-1, 0))) { + qe.saveToFile(questPath); + app.showToast("Quests saved"); + } + } + ImGui::End(); +} + void EditorUI::renderWaterPanel(EditorApp& app) { ImGui::SetNextWindowPos(ImVec2(10, 90), ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize(ImVec2(280, 250), ImGuiCond_FirstUseEver); @@ -1293,7 +1384,7 @@ void EditorUI::renderStatusBar(EditorApp& app) { ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoFocusOnAppearing; if (ImGui::Begin("##StatusBar", nullptr, flags)) { - const char* ms[] = {"Sculpt", "Paint", "Objects", "Water", "NPCs"}; + const char* ms[] = {"Sculpt", "Paint", "Objects", "Water", "NPCs", "Quests"}; const char* m = ms[static_cast(app.getMode())]; if (app.hasTerrainLoaded()) { ImGui::Text("[%s] %s [%d,%d]%s", m, app.getLoadedMap().c_str(), diff --git a/tools/editor/editor_ui.hpp b/tools/editor/editor_ui.hpp index 780e639d..6ed1d2c6 100644 --- a/tools/editor/editor_ui.hpp +++ b/tools/editor/editor_ui.hpp @@ -31,6 +31,7 @@ private: void renderObjectPanel(EditorApp& app); void renderWaterPanel(EditorApp& app); void renderNpcPanel(EditorApp& app); + void renderQuestPanel(EditorApp& app); void renderContextMenu(EditorApp& app); void renderMinimap(EditorApp& app); void renderPropertiesPanel(EditorApp& app); diff --git a/tools/editor/quest_editor.cpp b/tools/editor/quest_editor.cpp new file mode 100644 index 00000000..a2ef7156 --- /dev/null +++ b/tools/editor/quest_editor.cpp @@ -0,0 +1,68 @@ +#include "quest_editor.hpp" +#include "core/logger.hpp" +#include +#include + +namespace wowee { +namespace editor { + +void QuestEditor::addQuest(const Quest& q) { + Quest quest = q; + quest.id = nextId_++; + quests_.push_back(quest); + LOG_INFO("Quest added: [", quest.id, "] ", quest.title); +} + +void QuestEditor::removeQuest(int index) { + if (index >= 0 && index < static_cast(quests_.size())) + quests_.erase(quests_.begin() + index); +} + +Quest* QuestEditor::getQuest(int index) { + if (index < 0 || index >= static_cast(quests_.size())) return nullptr; + return &quests_[index]; +} + +bool QuestEditor::saveToFile(const std::string& path) const { + auto dir = std::filesystem::path(path).parent_path(); + if (!dir.empty()) std::filesystem::create_directories(dir); + + std::ofstream f(path); + if (!f) return false; + + f << "[\n"; + for (size_t i = 0; i < quests_.size(); i++) { + const auto& q = quests_[i]; + f << " {\n"; + f << " \"id\": " << q.id << ",\n"; + f << " \"title\": \"" << q.title << "\",\n"; + f << " \"description\": \"" << q.description << "\",\n"; + f << " \"completionText\": \"" << q.completionText << "\",\n"; + f << " \"requiredLevel\": " << q.requiredLevel << ",\n"; + f << " \"questGiverNpcId\": " << q.questGiverNpcId << ",\n"; + f << " \"turnInNpcId\": " << q.turnInNpcId << ",\n"; + f << " \"nextQuestId\": " << q.nextQuestId << ",\n"; + f << " \"reward\": {\"xp\":" << q.reward.xp + << ",\"gold\":" << q.reward.gold + << ",\"silver\":" << q.reward.silver + << ",\"copper\":" << q.reward.copper << "},\n"; + f << " \"objectives\": ["; + for (size_t j = 0; j < q.objectives.size(); j++) { + const auto& obj = q.objectives[j]; + f << "{\"type\":" << static_cast(obj.type) + << ",\"desc\":\"" << obj.description << "\"" + << ",\"target\":\"" << obj.targetName << "\"" + << ",\"count\":" << obj.targetCount << "}"; + if (j + 1 < q.objectives.size()) f << ","; + } + f << "]\n"; + f << " }" << (i + 1 < quests_.size() ? "," : "") << "\n"; + } + f << "]\n"; + + LOG_INFO("Quests saved: ", path, " (", quests_.size(), " quests)"); + return true; +} + +} // namespace editor +} // namespace wowee diff --git a/tools/editor/quest_editor.hpp b/tools/editor/quest_editor.hpp new file mode 100644 index 00000000..f3b5691b --- /dev/null +++ b/tools/editor/quest_editor.hpp @@ -0,0 +1,67 @@ +#pragma once + +#include +#include +#include +#include + +namespace wowee { +namespace editor { + +enum class QuestObjectiveType { + KillCreature, + CollectItem, + TalkToNPC, + ExploreArea, + EscortNPC, + UseObject +}; + +struct QuestObjective { + QuestObjectiveType type = QuestObjectiveType::KillCreature; + std::string description; + std::string targetName; + uint32_t targetCount = 1; +}; + +struct QuestReward { + uint32_t xp = 100; + uint32_t gold = 0; + uint32_t silver = 0; + uint32_t copper = 0; + std::vector itemRewards; +}; + +struct Quest { + uint32_t id = 0; + std::string title = "New Quest"; + std::string description; + std::string completionText; + uint32_t requiredLevel = 1; + uint32_t questGiverNpcId = 0; + uint32_t turnInNpcId = 0; + std::vector objectives; + QuestReward reward; + uint32_t nextQuestId = 0; // chain link +}; + +class QuestEditor { +public: + void addQuest(const Quest& q); + void removeQuest(int index); + Quest* getQuest(int index); + const std::vector& getQuests() const { return quests_; } + size_t questCount() const { return quests_.size(); } + + bool saveToFile(const std::string& path) const; + + Quest& getTemplate() { return template_; } + +private: + std::vector quests_; + Quest template_; + uint32_t nextId_ = 1; +}; + +} // namespace editor +} // namespace wowee