mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-06 00:53:52 +00:00
feat(editor): quest editor with objectives, rewards, and quest chains
- New Quest mode (key 6) with full quest creation panel: - Title, description, required level - Quest giver / turn-in NPC ID linkage - Up to 4 objectives: Kill, Collect, Talk, Explore, Escort, Use Object - Rewards: XP and gold - Quest chain support via nextQuestId linking - Quest list showing all created quests with level and objective count - Save quests to JSON (included in Export Zone package) - Foundation for campaign system: create quest chains across NPCs, link objectives to placed creatures, build storylines
This commit is contained in:
parent
124ff5a54a
commit
f59d79537a
7 changed files with 239 additions and 2 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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_;
|
||||
|
|
|
|||
|
|
@ -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<int>(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<int>(obj.type);
|
||||
ImGui::Combo("Type", &ti, types, 6);
|
||||
obj.type = static_cast<QuestObjectiveType>(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<int>(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<int>(app.getMode())];
|
||||
if (app.hasTerrainLoaded()) {
|
||||
ImGui::Text("[%s] %s [%d,%d]%s", m, app.getLoadedMap().c_str(),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
68
tools/editor/quest_editor.cpp
Normal file
68
tools/editor/quest_editor.cpp
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
#include "quest_editor.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <fstream>
|
||||
#include <filesystem>
|
||||
|
||||
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<int>(quests_.size()))
|
||||
quests_.erase(quests_.begin() + index);
|
||||
}
|
||||
|
||||
Quest* QuestEditor::getQuest(int index) {
|
||||
if (index < 0 || index >= static_cast<int>(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<int>(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
|
||||
67
tools/editor/quest_editor.hpp
Normal file
67
tools/editor/quest_editor.hpp
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
#pragma once
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
|
||||
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<std::string> 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<QuestObjective> 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<Quest>& 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<Quest> quests_;
|
||||
Quest template_;
|
||||
uint32_t nextId_ = 1;
|
||||
};
|
||||
|
||||
} // namespace editor
|
||||
} // namespace wowee
|
||||
Loading…
Add table
Add a link
Reference in a new issue