mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-10 02:53:51 +00:00
feat(pipeline): add WQT (Wowee Quest Template) format
Novel open replacement for AzerothCore-style quest_template
SQL tables PLUS the Blizzard Quest.dbc / QuestObjective.dbc
trio. The 15th open format added to the editor — and the
last gameplay-graph piece the catalog needed.
Cross-references with previously-added formats:
WQT.giverCreatureId -> WCRT.entry.creatureId
WQT.turninCreatureId -> WCRT.entry.creatureId
WQT.objective.targetId -> WCRT (kill) / WIT (collect) /
WOB (interact)
WQT.rewardItem.itemId -> WIT.entry.itemId
WQT.prevQuestId -> WQT.entry.questId (intra-format)
WQT.nextQuestId -> WQT.entry.questId
Together with WIT / WCRT / WLOT / WSPN / WOMX / WOL / WOW /
WSND, a content pack can now ship a complete RPG zone
(terrain + props + atmosphere + sounds + creatures + items
+ loot + spawns + quests) entirely in open formats with no
SQL or .dbc dependencies. 15 of 15 expected slots filled.
Format:
• magic "WQTM", version 1, little-endian
• per quest: questId / title / objective / description /
minLevel..maxLevel + questLevel / requiredClass+RaceMask /
prev+nextQuestId / giver+turninCreatureId /
objectives[] / xpReward + moneyCopperReward /
rewardItems[] / flags
Per-objective:
kind (kill/collect/interact/visit/escort/cast),
targetId, quantity
Per-reward:
itemId, qty, pickFlags (AutoGiven / PlayerChoice)
Quest flags: Daily / Weekly / Raid / Group / AutoComplete /
AutoAccept / Repeatable / ClassQuest / Pvp
API: WoweeQuestLoader::save / load / exists / findById;
presets makeStarter (1 simple kill quest, references the
bandit creatureId=1000), makeChain (3-quest chain with
prev/next links + AutoComplete bridge + player-choice
rewards), makeDaily (Daily+Repeatable+AutoAccept combo).
CLI added (5 flags, 500 documented total — round milestone):
--gen-quests / --gen-quests-chain / --gen-quests-daily
--info-wqt / --validate-wqt
Validator catches: questId=0+duplicates, level=0,
maxLevel<minLevel, empty title, no objectives without
AutoComplete (player can't finish), no rewards at all,
Daily without Repeatable (incoherent), targetId=0,
quantity=0, unknown objective kind, reward itemId=0 or qty=0.
The 3-quest chain demo exercises every major feature:
• multiple objective kinds (visit / collect / kill)
• prev/next chain links
• AutoComplete dialogue-bridge quest
• PlayerChoice reward (1 of 2 weapons)
This commit is contained in:
parent
24bc52ab11
commit
02ae17740e
8 changed files with 842 additions and 1 deletions
|
|
@ -602,6 +602,7 @@ set(WOWEE_SOURCES
|
|||
src/pipeline/wowee_items.cpp
|
||||
src/pipeline/wowee_loot.cpp
|
||||
src/pipeline/wowee_creatures.cpp
|
||||
src/pipeline/wowee_quests.cpp
|
||||
src/pipeline/custom_zone_discovery.cpp
|
||||
src/pipeline/dbc_layout.cpp
|
||||
|
||||
|
|
@ -1350,6 +1351,7 @@ add_executable(wowee_editor
|
|||
tools/editor/cli_items_catalog.cpp
|
||||
tools/editor/cli_loot_catalog.cpp
|
||||
tools/editor/cli_creatures_catalog.cpp
|
||||
tools/editor/cli_quests_catalog.cpp
|
||||
tools/editor/cli_quest_objective.cpp
|
||||
tools/editor/cli_quest_reward.cpp
|
||||
tools/editor/cli_clone.cpp
|
||||
|
|
@ -1430,6 +1432,7 @@ add_executable(wowee_editor
|
|||
src/pipeline/wowee_items.cpp
|
||||
src/pipeline/wowee_loot.cpp
|
||||
src/pipeline/wowee_creatures.cpp
|
||||
src/pipeline/wowee_quests.cpp
|
||||
src/pipeline/custom_zone_discovery.cpp
|
||||
src/pipeline/terrain_mesh.cpp
|
||||
|
||||
|
|
|
|||
153
include/pipeline/wowee_quests.hpp
Normal file
153
include/pipeline/wowee_quests.hpp
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace wowee {
|
||||
namespace pipeline {
|
||||
|
||||
// Wowee Open Quest Template (.wqt) — novel replacement for
|
||||
// AzerothCore-style quest_template SQL tables PLUS the
|
||||
// Blizzard Quest.dbc / QuestObjective.dbc trio. The 15th
|
||||
// open format added to the editor.
|
||||
//
|
||||
// Cross-references with previously-added formats:
|
||||
// WQT.giverCreatureId → WCRT.entry.creatureId
|
||||
// WQT.turninCreatureId → WCRT.entry.creatureId
|
||||
// WQT.objective.targetId → WCRT (kill) / WIT (collect) / WOB (interact)
|
||||
// WQT.rewardItem.itemId → WIT.entry.itemId
|
||||
// WQT.prevQuestId → WQT.entry.questId (intra-format chain)
|
||||
// WQT.nextQuestId → WQT.entry.questId
|
||||
//
|
||||
// Together with WIT / WCRT / WLOT / WSPN this completes the
|
||||
// gameplay graph: a content pack can ship items + creatures
|
||||
// + spawns + loot tables + quests with no SQL or .dbc files.
|
||||
//
|
||||
// Binary layout (little-endian):
|
||||
// magic[4] = "WQTM"
|
||||
// version (uint32) = current 1
|
||||
// nameLen + name (catalog label)
|
||||
// entryCount (uint32)
|
||||
// entries (each):
|
||||
// questId (uint32)
|
||||
// titleLen + title
|
||||
// objectiveLen + objective -- 1-line "Kill 10 wolves"
|
||||
// descriptionLen + description -- flavor text
|
||||
// minLevel (uint16) / questLevel (uint16) / maxLevel (uint16) / pad
|
||||
// requiredClassMask (uint32) -- bitmask of class IDs; 0 = any
|
||||
// requiredRaceMask (uint32)
|
||||
// prevQuestId (uint32) -- 0 = chain start
|
||||
// nextQuestId (uint32)
|
||||
// giverCreatureId (uint32)
|
||||
// turninCreatureId (uint32)
|
||||
// objectiveCount (uint8) + pad[3]
|
||||
// objectives (objectiveCount × {
|
||||
// kind (uint8) + pad[3]
|
||||
// targetId (uint32)
|
||||
// quantity (uint16) + pad[2]
|
||||
// })
|
||||
// xpReward (uint32)
|
||||
// moneyCopperReward (uint32)
|
||||
// rewardItemCount (uint8) + pad[3]
|
||||
// rewardItems (rewardItemCount × {
|
||||
// itemId (uint32)
|
||||
// qty (uint8) + pickFlags (uint8) + pad[2]
|
||||
// })
|
||||
// flags (uint32)
|
||||
struct WoweeQuest {
|
||||
enum ObjectiveKind : uint8_t {
|
||||
KillCreature = 0,
|
||||
CollectItem = 1,
|
||||
InteractObject = 2,
|
||||
VisitArea = 3,
|
||||
EscortNpc = 4,
|
||||
SpellCast = 5,
|
||||
};
|
||||
|
||||
enum Flags : uint32_t {
|
||||
Daily = 0x01,
|
||||
Weekly = 0x02,
|
||||
Raid = 0x04,
|
||||
Group = 0x08, // requires party / not solo-completable
|
||||
AutoComplete = 0x10, // turns in immediately on objective met
|
||||
AutoAccept = 0x20, // accepts on giver-NPC interaction
|
||||
Repeatable = 0x40,
|
||||
ClassQuest = 0x80,
|
||||
Pvp = 0x100,
|
||||
};
|
||||
|
||||
enum RewardPickFlags : uint8_t {
|
||||
AutoGiven = 0x01, // always handed out on turn-in
|
||||
PlayerChoice = 0x02, // player picks from a list at turn-in
|
||||
};
|
||||
|
||||
struct Objective {
|
||||
uint8_t kind = KillCreature;
|
||||
uint32_t targetId = 0;
|
||||
uint16_t quantity = 1;
|
||||
};
|
||||
|
||||
struct RewardItem {
|
||||
uint32_t itemId = 0;
|
||||
uint8_t qty = 1;
|
||||
uint8_t pickFlags = AutoGiven;
|
||||
};
|
||||
|
||||
struct Entry {
|
||||
uint32_t questId = 0;
|
||||
std::string title;
|
||||
std::string objective;
|
||||
std::string description;
|
||||
uint16_t minLevel = 1;
|
||||
uint16_t questLevel = 1;
|
||||
uint16_t maxLevel = 0; // 0 = no upper cap
|
||||
uint32_t requiredClassMask = 0;
|
||||
uint32_t requiredRaceMask = 0;
|
||||
uint32_t prevQuestId = 0;
|
||||
uint32_t nextQuestId = 0;
|
||||
uint32_t giverCreatureId = 0;
|
||||
uint32_t turninCreatureId = 0;
|
||||
std::vector<Objective> objectives;
|
||||
uint32_t xpReward = 0;
|
||||
uint32_t moneyCopperReward = 0;
|
||||
std::vector<RewardItem> rewardItems;
|
||||
uint32_t flags = 0;
|
||||
};
|
||||
|
||||
std::string name;
|
||||
std::vector<Entry> entries;
|
||||
|
||||
bool isValid() const { return !entries.empty(); }
|
||||
|
||||
// Lookup by questId — nullptr if not present.
|
||||
const Entry* findById(uint32_t questId) const;
|
||||
|
||||
static const char* objectiveKindName(uint8_t k);
|
||||
};
|
||||
|
||||
class WoweeQuestLoader {
|
||||
public:
|
||||
static bool save(const WoweeQuest& cat,
|
||||
const std::string& basePath);
|
||||
static WoweeQuest load(const std::string& basePath);
|
||||
static bool exists(const std::string& basePath);
|
||||
|
||||
// Preset emitters used by --gen-quests* variants.
|
||||
//
|
||||
// makeStarter — 1 short kill quest: "Kill 10 bandits" with
|
||||
// XP + small money reward. Cross-references
|
||||
// WCRT bandit (creatureId=1000) + WCRT
|
||||
// village innkeeper (giver/turnin=4001).
|
||||
// makeChain — 3-quest chain: "Investigate" -> "Recover"
|
||||
// -> "Report Back". Each quest's nextQuestId
|
||||
// points to the next; the third closes the loop.
|
||||
// makeDaily — 1 daily repeatable quest with the Daily +
|
||||
// Repeatable + AutoAccept flag combo.
|
||||
static WoweeQuest makeStarter(const std::string& catalogName);
|
||||
static WoweeQuest makeChain(const std::string& catalogName);
|
||||
static WoweeQuest makeDaily(const std::string& catalogName);
|
||||
};
|
||||
|
||||
} // namespace pipeline
|
||||
} // namespace wowee
|
||||
330
src/pipeline/wowee_quests.cpp
Normal file
330
src/pipeline/wowee_quests.cpp
Normal file
|
|
@ -0,0 +1,330 @@
|
|||
#include "pipeline/wowee_quests.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
|
||||
namespace wowee {
|
||||
namespace pipeline {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr char kMagic[4] = {'W', 'Q', 'T', 'M'};
|
||||
constexpr uint32_t kVersion = 1;
|
||||
|
||||
template <typename T>
|
||||
void writePOD(std::ofstream& os, const T& v) {
|
||||
os.write(reinterpret_cast<const char*>(&v), sizeof(T));
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
bool readPOD(std::ifstream& is, T& v) {
|
||||
is.read(reinterpret_cast<char*>(&v), sizeof(T));
|
||||
return is.gcount() == static_cast<std::streamsize>(sizeof(T));
|
||||
}
|
||||
|
||||
void writeStr(std::ofstream& os, const std::string& s) {
|
||||
uint32_t n = static_cast<uint32_t>(s.size());
|
||||
writePOD(os, n);
|
||||
if (n > 0) os.write(s.data(), n);
|
||||
}
|
||||
|
||||
bool readStr(std::ifstream& is, std::string& s) {
|
||||
uint32_t n = 0;
|
||||
if (!readPOD(is, n)) return false;
|
||||
if (n > (1u << 20)) return false;
|
||||
s.resize(n);
|
||||
if (n > 0) {
|
||||
is.read(s.data(), n);
|
||||
if (is.gcount() != static_cast<std::streamsize>(n)) {
|
||||
s.clear();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string normalizePath(std::string base) {
|
||||
if (base.size() < 4 || base.substr(base.size() - 4) != ".wqt") {
|
||||
base += ".wqt";
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const WoweeQuest::Entry* WoweeQuest::findById(uint32_t questId) const {
|
||||
for (const auto& e : entries) {
|
||||
if (e.questId == questId) return &e;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const char* WoweeQuest::objectiveKindName(uint8_t k) {
|
||||
switch (k) {
|
||||
case KillCreature: return "kill";
|
||||
case CollectItem: return "collect";
|
||||
case InteractObject: return "interact";
|
||||
case VisitArea: return "visit";
|
||||
case EscortNpc: return "escort";
|
||||
case SpellCast: return "cast";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
bool WoweeQuestLoader::save(const WoweeQuest& cat,
|
||||
const std::string& basePath) {
|
||||
std::ofstream os(normalizePath(basePath), std::ios::binary);
|
||||
if (!os) return false;
|
||||
os.write(kMagic, 4);
|
||||
writePOD(os, kVersion);
|
||||
writeStr(os, cat.name);
|
||||
uint32_t entryCount = static_cast<uint32_t>(cat.entries.size());
|
||||
writePOD(os, entryCount);
|
||||
for (const auto& e : cat.entries) {
|
||||
writePOD(os, e.questId);
|
||||
writeStr(os, e.title);
|
||||
writeStr(os, e.objective);
|
||||
writeStr(os, e.description);
|
||||
writePOD(os, e.minLevel);
|
||||
writePOD(os, e.questLevel);
|
||||
writePOD(os, e.maxLevel);
|
||||
uint16_t pad16 = 0;
|
||||
writePOD(os, pad16);
|
||||
writePOD(os, e.requiredClassMask);
|
||||
writePOD(os, e.requiredRaceMask);
|
||||
writePOD(os, e.prevQuestId);
|
||||
writePOD(os, e.nextQuestId);
|
||||
writePOD(os, e.giverCreatureId);
|
||||
writePOD(os, e.turninCreatureId);
|
||||
uint8_t objCount = static_cast<uint8_t>(
|
||||
e.objectives.size() > 255 ? 255 : e.objectives.size());
|
||||
writePOD(os, objCount);
|
||||
uint8_t pad3[3] = {0, 0, 0};
|
||||
os.write(reinterpret_cast<const char*>(pad3), 3);
|
||||
for (uint8_t k = 0; k < objCount; ++k) {
|
||||
const auto& o = e.objectives[k];
|
||||
writePOD(os, o.kind);
|
||||
os.write(reinterpret_cast<const char*>(pad3), 3);
|
||||
writePOD(os, o.targetId);
|
||||
writePOD(os, o.quantity);
|
||||
writePOD(os, pad16);
|
||||
}
|
||||
writePOD(os, e.xpReward);
|
||||
writePOD(os, e.moneyCopperReward);
|
||||
uint8_t rewCount = static_cast<uint8_t>(
|
||||
e.rewardItems.size() > 255 ? 255 : e.rewardItems.size());
|
||||
writePOD(os, rewCount);
|
||||
os.write(reinterpret_cast<const char*>(pad3), 3);
|
||||
for (uint8_t k = 0; k < rewCount; ++k) {
|
||||
const auto& r = e.rewardItems[k];
|
||||
writePOD(os, r.itemId);
|
||||
writePOD(os, r.qty);
|
||||
writePOD(os, r.pickFlags);
|
||||
writePOD(os, pad16);
|
||||
}
|
||||
writePOD(os, e.flags);
|
||||
}
|
||||
return os.good();
|
||||
}
|
||||
|
||||
WoweeQuest WoweeQuestLoader::load(const std::string& basePath) {
|
||||
WoweeQuest out;
|
||||
std::ifstream is(normalizePath(basePath), std::ios::binary);
|
||||
if (!is) return out;
|
||||
char magic[4];
|
||||
is.read(magic, 4);
|
||||
if (std::memcmp(magic, kMagic, 4) != 0) return out;
|
||||
uint32_t version = 0;
|
||||
if (!readPOD(is, version) || version != kVersion) return out;
|
||||
if (!readStr(is, out.name)) return out;
|
||||
uint32_t entryCount = 0;
|
||||
if (!readPOD(is, entryCount)) return out;
|
||||
if (entryCount > (1u << 20)) return out;
|
||||
out.entries.resize(entryCount);
|
||||
for (auto& e : out.entries) {
|
||||
if (!readPOD(is, e.questId)) { out.entries.clear(); return out; }
|
||||
if (!readStr(is, e.title) || !readStr(is, e.objective) ||
|
||||
!readStr(is, e.description)) {
|
||||
out.entries.clear(); return out;
|
||||
}
|
||||
if (!readPOD(is, e.minLevel) ||
|
||||
!readPOD(is, e.questLevel) ||
|
||||
!readPOD(is, e.maxLevel)) {
|
||||
out.entries.clear(); return out;
|
||||
}
|
||||
uint16_t pad16 = 0;
|
||||
if (!readPOD(is, pad16)) { out.entries.clear(); return out; }
|
||||
if (!readPOD(is, e.requiredClassMask) ||
|
||||
!readPOD(is, e.requiredRaceMask) ||
|
||||
!readPOD(is, e.prevQuestId) ||
|
||||
!readPOD(is, e.nextQuestId) ||
|
||||
!readPOD(is, e.giverCreatureId) ||
|
||||
!readPOD(is, e.turninCreatureId)) {
|
||||
out.entries.clear(); return out;
|
||||
}
|
||||
uint8_t objCount = 0;
|
||||
if (!readPOD(is, objCount)) { out.entries.clear(); return out; }
|
||||
uint8_t pad3[3];
|
||||
is.read(reinterpret_cast<char*>(pad3), 3);
|
||||
if (is.gcount() != 3) { out.entries.clear(); return out; }
|
||||
e.objectives.resize(objCount);
|
||||
for (uint8_t k = 0; k < objCount; ++k) {
|
||||
auto& o = e.objectives[k];
|
||||
if (!readPOD(is, o.kind)) {
|
||||
out.entries.clear(); return out;
|
||||
}
|
||||
is.read(reinterpret_cast<char*>(pad3), 3);
|
||||
if (is.gcount() != 3) { out.entries.clear(); return out; }
|
||||
if (!readPOD(is, o.targetId) || !readPOD(is, o.quantity)) {
|
||||
out.entries.clear(); return out;
|
||||
}
|
||||
uint16_t opad = 0;
|
||||
if (!readPOD(is, opad)) {
|
||||
out.entries.clear(); return out;
|
||||
}
|
||||
}
|
||||
if (!readPOD(is, e.xpReward) ||
|
||||
!readPOD(is, e.moneyCopperReward)) {
|
||||
out.entries.clear(); return out;
|
||||
}
|
||||
uint8_t rewCount = 0;
|
||||
if (!readPOD(is, rewCount)) { out.entries.clear(); return out; }
|
||||
is.read(reinterpret_cast<char*>(pad3), 3);
|
||||
if (is.gcount() != 3) { out.entries.clear(); return out; }
|
||||
e.rewardItems.resize(rewCount);
|
||||
for (uint8_t k = 0; k < rewCount; ++k) {
|
||||
auto& r = e.rewardItems[k];
|
||||
if (!readPOD(is, r.itemId) ||
|
||||
!readPOD(is, r.qty) ||
|
||||
!readPOD(is, r.pickFlags)) {
|
||||
out.entries.clear(); return out;
|
||||
}
|
||||
uint16_t rpad = 0;
|
||||
if (!readPOD(is, rpad)) {
|
||||
out.entries.clear(); return out;
|
||||
}
|
||||
}
|
||||
if (!readPOD(is, e.flags)) {
|
||||
out.entries.clear(); return out;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
bool WoweeQuestLoader::exists(const std::string& basePath) {
|
||||
std::ifstream is(normalizePath(basePath), std::ios::binary);
|
||||
return is.good();
|
||||
}
|
||||
|
||||
WoweeQuest WoweeQuestLoader::makeStarter(const std::string& catalogName) {
|
||||
WoweeQuest c;
|
||||
c.name = catalogName;
|
||||
{
|
||||
WoweeQuest::Entry e;
|
||||
e.questId = 1;
|
||||
e.title = "Bandit Trouble";
|
||||
e.objective = "Kill 10 Defias Bandits.";
|
||||
e.description =
|
||||
"The Defias have been raiding our farms. Make them pay.";
|
||||
e.minLevel = 5;
|
||||
e.questLevel = 6;
|
||||
e.giverCreatureId = 4001; // village innkeeper from WCRT
|
||||
e.turninCreatureId = 4001;
|
||||
e.objectives.push_back({WoweeQuest::KillCreature, 1000, 10});
|
||||
e.xpReward = 500;
|
||||
e.moneyCopperReward = 250; // 2s 50c
|
||||
e.rewardItems.push_back({3, 2, WoweeQuest::AutoGiven}); // 2 healing potions
|
||||
c.entries.push_back(e);
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
WoweeQuest WoweeQuestLoader::makeChain(const std::string& catalogName) {
|
||||
WoweeQuest c;
|
||||
c.name = catalogName;
|
||||
{
|
||||
WoweeQuest::Entry e;
|
||||
e.questId = 100;
|
||||
e.title = "Investigate the Camp";
|
||||
e.objective = "Visit the bandit camp clearing.";
|
||||
e.description =
|
||||
"Scout reports place a bandit camp east of here. "
|
||||
"See for yourself.";
|
||||
e.minLevel = 5; e.questLevel = 5;
|
||||
e.giverCreatureId = 4001;
|
||||
e.turninCreatureId = 4001;
|
||||
e.nextQuestId = 101;
|
||||
e.objectives.push_back({WoweeQuest::VisitArea, 9001, 1});
|
||||
e.xpReward = 200;
|
||||
e.moneyCopperReward = 100;
|
||||
c.entries.push_back(e);
|
||||
}
|
||||
{
|
||||
WoweeQuest::Entry e;
|
||||
e.questId = 101;
|
||||
e.title = "Recover the Stolen Goods";
|
||||
e.objective = "Recover 5 Stolen Letters from Defias Bandits.";
|
||||
e.description =
|
||||
"The bandits stole letters from the inn. "
|
||||
"Recover them.";
|
||||
e.minLevel = 6; e.questLevel = 7;
|
||||
e.giverCreatureId = 4001;
|
||||
e.turninCreatureId = 4001;
|
||||
e.prevQuestId = 100;
|
||||
e.nextQuestId = 102;
|
||||
e.objectives.push_back({WoweeQuest::CollectItem, 4, 5});
|
||||
e.xpReward = 600;
|
||||
e.moneyCopperReward = 350;
|
||||
e.rewardItems.push_back({2, 1, WoweeQuest::AutoGiven}); // linen vest
|
||||
c.entries.push_back(e);
|
||||
}
|
||||
{
|
||||
WoweeQuest::Entry e;
|
||||
e.questId = 102;
|
||||
e.title = "Report Back";
|
||||
e.objective = "Return to the Innkeeper with the recovered letters.";
|
||||
e.description =
|
||||
"Bring the recovered letters to the Innkeeper.";
|
||||
e.minLevel = 6; e.questLevel = 7;
|
||||
e.giverCreatureId = 4001;
|
||||
e.turninCreatureId = 4001;
|
||||
e.prevQuestId = 101;
|
||||
// No nextQuestId — chain end.
|
||||
// No objectives — quest completes on dialogue alone.
|
||||
e.flags = WoweeQuest::AutoComplete;
|
||||
e.xpReward = 200;
|
||||
e.moneyCopperReward = 500; // 5s
|
||||
// Player choice: 1 of 2 weapons.
|
||||
e.rewardItems.push_back({1001, 1, WoweeQuest::PlayerChoice}); // sword
|
||||
e.rewardItems.push_back({1002, 1, WoweeQuest::PlayerChoice}); // blade
|
||||
c.entries.push_back(e);
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
WoweeQuest WoweeQuestLoader::makeDaily(const std::string& catalogName) {
|
||||
WoweeQuest c;
|
||||
c.name = catalogName;
|
||||
{
|
||||
WoweeQuest::Entry e;
|
||||
e.questId = 200;
|
||||
e.title = "Daily: Wolf Cull";
|
||||
e.objective = "Kill 8 wolves outside the village gate.";
|
||||
e.description = "The wolves are getting bolder. Thin the pack.";
|
||||
e.minLevel = 5; e.questLevel = 5;
|
||||
e.giverCreatureId = 4002; // smith from WCRT village
|
||||
e.turninCreatureId = 4002;
|
||||
e.objectives.push_back({WoweeQuest::KillCreature, 2001, 8});
|
||||
e.xpReward = 250;
|
||||
e.moneyCopperReward = 1000; // 10s = good daily payout
|
||||
e.flags = WoweeQuest::Daily |
|
||||
WoweeQuest::Repeatable |
|
||||
WoweeQuest::AutoAccept;
|
||||
c.entries.push_back(e);
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
} // namespace pipeline
|
||||
} // namespace wowee
|
||||
|
|
@ -40,6 +40,8 @@ const char* const kArgRequired[] = {
|
|||
"--export-wlot-json", "--import-wlot-json",
|
||||
"--gen-creatures", "--gen-creatures-bandit", "--gen-creatures-merchants",
|
||||
"--info-wcrt", "--validate-wcrt",
|
||||
"--gen-quests", "--gen-quests-chain", "--gen-quests-daily",
|
||||
"--info-wqt", "--validate-wqt",
|
||||
"--gen-weather-temperate", "--gen-weather-arctic",
|
||||
"--gen-weather-desert", "--gen-weather-stormy",
|
||||
"--gen-zone-atmosphere",
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@
|
|||
#include "cli_items_catalog.hpp"
|
||||
#include "cli_loot_catalog.hpp"
|
||||
#include "cli_creatures_catalog.hpp"
|
||||
#include "cli_quests_catalog.hpp"
|
||||
#include "cli_quest_objective.hpp"
|
||||
#include "cli_quest_reward.hpp"
|
||||
#include "cli_clone.hpp"
|
||||
|
|
@ -125,6 +126,7 @@ constexpr DispatchFn kDispatchTable[] = {
|
|||
handleItemsCatalog,
|
||||
handleLootCatalog,
|
||||
handleCreaturesCatalog,
|
||||
handleQuestsCatalog,
|
||||
handleQuestObjective,
|
||||
handleQuestReward,
|
||||
handleClone,
|
||||
|
|
|
|||
|
|
@ -904,7 +904,17 @@ void printUsage(const char* argv0) {
|
|||
std::printf(" --info-wcrt <wcrt-base> [--json]\n");
|
||||
std::printf(" Print WCRT entries (id / level / hp / type / faction / npc-flags / name + subname)\n");
|
||||
std::printf(" --validate-wcrt <wcrt-base> [--json]\n");
|
||||
std::printf(" Static checks: creatureId>0+unique, level/hp>0, min<=max, attackSpeed>0, AI flag conflicts\n");
|
||||
std::printf(" Static checks: creatureId>0+unique, level/hp>0, min<=max, attackSpeed>0, behavior flag conflicts\n");
|
||||
std::printf(" --gen-quests <wqt-base> [name]\n");
|
||||
std::printf(" Emit .wqt starter quest: 'Kill 10 Defias Bandits' giver=4001 (matches WCRT village innkeeper)\n");
|
||||
std::printf(" --gen-quests-chain <wqt-base> [name]\n");
|
||||
std::printf(" Emit .wqt 3-quest chain: Investigate -> Recover -> Report (chained via prev/next questId)\n");
|
||||
std::printf(" --gen-quests-daily <wqt-base> [name]\n");
|
||||
std::printf(" Emit .wqt daily repeatable quest with the Daily + Repeatable + AutoAccept flag combo\n");
|
||||
std::printf(" --info-wqt <wqt-base> [--json]\n");
|
||||
std::printf(" Print WQT entries (questId / level / giver / objectives / rewards / chain links)\n");
|
||||
std::printf(" --validate-wqt <wqt-base> [--json]\n");
|
||||
std::printf(" Static checks: questId>0+unique, level>0+min<=max, title not empty, no rewards warning, daily needs repeatable\n");
|
||||
std::printf(" --gen-weather-temperate <wow-base> [zoneName]\n");
|
||||
std::printf(" Emit .wow weather schedule: clear-dominant + occasional rain + fog (forest / grassland)\n");
|
||||
std::printf(" --gen-weather-arctic <wow-base> [zoneName]\n");
|
||||
|
|
|
|||
330
tools/editor/cli_quests_catalog.cpp
Normal file
330
tools/editor/cli_quests_catalog.cpp
Normal file
|
|
@ -0,0 +1,330 @@
|
|||
#include "cli_quests_catalog.hpp"
|
||||
#include "cli_arg_parse.hpp"
|
||||
#include "cli_box_emitter.hpp"
|
||||
|
||||
#include "pipeline/wowee_quests.hpp"
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace wowee {
|
||||
namespace editor {
|
||||
namespace cli {
|
||||
|
||||
namespace {
|
||||
|
||||
std::string stripWqtExt(std::string base) {
|
||||
stripExt(base, ".wqt");
|
||||
return base;
|
||||
}
|
||||
|
||||
void appendQuestFlagsStr(std::string& s, uint32_t flags) {
|
||||
if (flags & wowee::pipeline::WoweeQuest::Daily) s += "daily ";
|
||||
if (flags & wowee::pipeline::WoweeQuest::Weekly) s += "weekly ";
|
||||
if (flags & wowee::pipeline::WoweeQuest::Raid) s += "raid ";
|
||||
if (flags & wowee::pipeline::WoweeQuest::Group) s += "group ";
|
||||
if (flags & wowee::pipeline::WoweeQuest::AutoComplete) s += "auto-complete ";
|
||||
if (flags & wowee::pipeline::WoweeQuest::AutoAccept) s += "auto-accept ";
|
||||
if (flags & wowee::pipeline::WoweeQuest::Repeatable) s += "repeatable ";
|
||||
if (flags & wowee::pipeline::WoweeQuest::ClassQuest) s += "class ";
|
||||
if (flags & wowee::pipeline::WoweeQuest::Pvp) s += "pvp ";
|
||||
if (s.empty()) s = "-";
|
||||
else if (s.back() == ' ') s.pop_back();
|
||||
}
|
||||
|
||||
bool saveOrError(const wowee::pipeline::WoweeQuest& c,
|
||||
const std::string& base, const char* cmd) {
|
||||
if (!wowee::pipeline::WoweeQuestLoader::save(c, base)) {
|
||||
std::fprintf(stderr, "%s: failed to save %s.wqt\n",
|
||||
cmd, base.c_str());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void printGenSummary(const wowee::pipeline::WoweeQuest& c,
|
||||
const std::string& base) {
|
||||
std::printf("Wrote %s.wqt\n", base.c_str());
|
||||
std::printf(" catalog : %s\n", c.name.c_str());
|
||||
std::printf(" quests : %zu\n", c.entries.size());
|
||||
}
|
||||
|
||||
int handleGenStarter(int& i, int argc, char** argv) {
|
||||
std::string base = argv[++i];
|
||||
std::string name = "StarterQuests";
|
||||
if (parseOptArg(i, argc, argv)) name = argv[++i];
|
||||
base = stripWqtExt(base);
|
||||
auto c = wowee::pipeline::WoweeQuestLoader::makeStarter(name);
|
||||
if (!saveOrError(c, base, "gen-quests")) return 1;
|
||||
printGenSummary(c, base);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int handleGenChain(int& i, int argc, char** argv) {
|
||||
std::string base = argv[++i];
|
||||
std::string name = "QuestChain";
|
||||
if (parseOptArg(i, argc, argv)) name = argv[++i];
|
||||
base = stripWqtExt(base);
|
||||
auto c = wowee::pipeline::WoweeQuestLoader::makeChain(name);
|
||||
if (!saveOrError(c, base, "gen-quests-chain")) return 1;
|
||||
printGenSummary(c, base);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int handleGenDaily(int& i, int argc, char** argv) {
|
||||
std::string base = argv[++i];
|
||||
std::string name = "DailyQuests";
|
||||
if (parseOptArg(i, argc, argv)) name = argv[++i];
|
||||
base = stripWqtExt(base);
|
||||
auto c = wowee::pipeline::WoweeQuestLoader::makeDaily(name);
|
||||
if (!saveOrError(c, base, "gen-quests-daily")) return 1;
|
||||
printGenSummary(c, base);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int handleInfo(int& i, int argc, char** argv) {
|
||||
std::string base = argv[++i];
|
||||
bool jsonOut = consumeJsonFlag(i, argc, argv);
|
||||
base = stripWqtExt(base);
|
||||
if (!wowee::pipeline::WoweeQuestLoader::exists(base)) {
|
||||
std::fprintf(stderr, "WQT not found: %s.wqt\n", base.c_str());
|
||||
return 1;
|
||||
}
|
||||
auto c = wowee::pipeline::WoweeQuestLoader::load(base);
|
||||
if (jsonOut) {
|
||||
nlohmann::json j;
|
||||
j["wqt"] = base + ".wqt";
|
||||
j["name"] = c.name;
|
||||
j["count"] = c.entries.size();
|
||||
nlohmann::json arr = nlohmann::json::array();
|
||||
for (const auto& e : c.entries) {
|
||||
std::string fs;
|
||||
appendQuestFlagsStr(fs, e.flags);
|
||||
nlohmann::json je;
|
||||
je["questId"] = e.questId;
|
||||
je["title"] = e.title;
|
||||
je["objective"] = e.objective;
|
||||
je["description"] = e.description;
|
||||
je["minLevel"] = e.minLevel;
|
||||
je["questLevel"] = e.questLevel;
|
||||
je["maxLevel"] = e.maxLevel;
|
||||
je["requiredClassMask"] = e.requiredClassMask;
|
||||
je["requiredRaceMask"] = e.requiredRaceMask;
|
||||
je["prevQuestId"] = e.prevQuestId;
|
||||
je["nextQuestId"] = e.nextQuestId;
|
||||
je["giverCreatureId"] = e.giverCreatureId;
|
||||
je["turninCreatureId"] = e.turninCreatureId;
|
||||
nlohmann::json oa = nlohmann::json::array();
|
||||
for (const auto& o : e.objectives) {
|
||||
oa.push_back({
|
||||
{"kind", o.kind},
|
||||
{"kindName", wowee::pipeline::WoweeQuest::objectiveKindName(o.kind)},
|
||||
{"targetId", o.targetId},
|
||||
{"quantity", o.quantity},
|
||||
});
|
||||
}
|
||||
je["objectives"] = oa;
|
||||
je["xpReward"] = e.xpReward;
|
||||
je["moneyCopperReward"] = e.moneyCopperReward;
|
||||
nlohmann::json ra = nlohmann::json::array();
|
||||
for (const auto& r : e.rewardItems) {
|
||||
ra.push_back({
|
||||
{"itemId", r.itemId},
|
||||
{"qty", r.qty},
|
||||
{"pickFlags", r.pickFlags},
|
||||
});
|
||||
}
|
||||
je["rewardItems"] = ra;
|
||||
je["flags"] = e.flags;
|
||||
je["flagsStr"] = fs;
|
||||
arr.push_back(je);
|
||||
}
|
||||
j["entries"] = arr;
|
||||
std::printf("%s\n", j.dump(2).c_str());
|
||||
return 0;
|
||||
}
|
||||
std::printf("WQT: %s.wqt\n", base.c_str());
|
||||
std::printf(" catalog : %s\n", c.name.c_str());
|
||||
std::printf(" quests : %zu\n", c.entries.size());
|
||||
if (c.entries.empty()) return 0;
|
||||
for (const auto& e : c.entries) {
|
||||
std::string fs;
|
||||
appendQuestFlagsStr(fs, e.flags);
|
||||
std::printf("\n questId=%u level=%u giver=%u flags=%s\n",
|
||||
e.questId, e.questLevel, e.giverCreatureId, fs.c_str());
|
||||
std::printf(" title : %s\n", e.title.c_str());
|
||||
std::printf(" objective: %s\n", e.objective.c_str());
|
||||
if (!e.objectives.empty()) {
|
||||
std::printf(" targets : ");
|
||||
for (size_t k = 0; k < e.objectives.size(); ++k) {
|
||||
const auto& o = e.objectives[k];
|
||||
std::printf("%s%s id=%u x%u",
|
||||
k > 0 ? ", " : "",
|
||||
wowee::pipeline::WoweeQuest::objectiveKindName(o.kind),
|
||||
o.targetId, o.quantity);
|
||||
}
|
||||
std::printf("\n");
|
||||
}
|
||||
std::printf(" reward : %u xp + %u copper",
|
||||
e.xpReward, e.moneyCopperReward);
|
||||
if (!e.rewardItems.empty()) {
|
||||
std::printf(" + items: ");
|
||||
for (size_t k = 0; k < e.rewardItems.size(); ++k) {
|
||||
const auto& r = e.rewardItems[k];
|
||||
std::printf("%sitem %u x%u%s",
|
||||
k > 0 ? ", " : "",
|
||||
r.itemId, r.qty,
|
||||
(r.pickFlags & wowee::pipeline::WoweeQuest::PlayerChoice) ?
|
||||
" (choice)" : "");
|
||||
}
|
||||
}
|
||||
std::printf("\n");
|
||||
if (e.prevQuestId != 0 || e.nextQuestId != 0) {
|
||||
std::printf(" chain : prev=%u, next=%u\n",
|
||||
e.prevQuestId, e.nextQuestId);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int handleValidate(int& i, int argc, char** argv) {
|
||||
std::string base = argv[++i];
|
||||
bool jsonOut = consumeJsonFlag(i, argc, argv);
|
||||
base = stripWqtExt(base);
|
||||
if (!wowee::pipeline::WoweeQuestLoader::exists(base)) {
|
||||
std::fprintf(stderr,
|
||||
"validate-wqt: WQT not found: %s.wqt\n", base.c_str());
|
||||
return 1;
|
||||
}
|
||||
auto c = wowee::pipeline::WoweeQuestLoader::load(base);
|
||||
std::vector<std::string> errors;
|
||||
std::vector<std::string> warnings;
|
||||
if (c.entries.empty()) {
|
||||
warnings.push_back("catalog has zero entries");
|
||||
}
|
||||
std::vector<uint32_t> idsSeen;
|
||||
idsSeen.reserve(c.entries.size());
|
||||
for (size_t k = 0; k < c.entries.size(); ++k) {
|
||||
const auto& e = c.entries[k];
|
||||
std::string ctx = "quest " + std::to_string(e.questId);
|
||||
if (!e.title.empty()) ctx += " (" + e.title + ")";
|
||||
if (e.questId == 0) {
|
||||
errors.push_back(ctx + ": questId is 0");
|
||||
}
|
||||
if (e.minLevel == 0) {
|
||||
errors.push_back(ctx + ": minLevel is 0");
|
||||
}
|
||||
if (e.maxLevel != 0 && e.maxLevel < e.minLevel) {
|
||||
errors.push_back(ctx + ": maxLevel < minLevel");
|
||||
}
|
||||
if (e.title.empty()) {
|
||||
errors.push_back(ctx + ": title is empty");
|
||||
}
|
||||
// A quest with no objectives only makes sense if it's
|
||||
// a chain-bridge (auto-complete on dialogue).
|
||||
if (e.objectives.empty() &&
|
||||
!(e.flags & wowee::pipeline::WoweeQuest::AutoComplete)) {
|
||||
warnings.push_back(ctx +
|
||||
": no objectives and not AutoComplete (player can't finish)");
|
||||
}
|
||||
// No reward at all is technically valid for chain bridges
|
||||
// but is usually a mistake.
|
||||
if (e.xpReward == 0 && e.moneyCopperReward == 0 &&
|
||||
e.rewardItems.empty()) {
|
||||
warnings.push_back(ctx + ": no rewards (xp / money / items)");
|
||||
}
|
||||
// Daily without Repeatable is contradictory.
|
||||
if ((e.flags & wowee::pipeline::WoweeQuest::Daily) &&
|
||||
!(e.flags & wowee::pipeline::WoweeQuest::Repeatable)) {
|
||||
warnings.push_back(ctx +
|
||||
": Daily quest is not flagged Repeatable");
|
||||
}
|
||||
for (size_t oi = 0; oi < e.objectives.size(); ++oi) {
|
||||
const auto& o = e.objectives[oi];
|
||||
std::string octx = ctx + " obj " + std::to_string(oi);
|
||||
if (o.targetId == 0) {
|
||||
errors.push_back(octx + ": targetId is 0");
|
||||
}
|
||||
if (o.quantity == 0) {
|
||||
errors.push_back(octx + ": quantity is 0");
|
||||
}
|
||||
if (o.kind > wowee::pipeline::WoweeQuest::SpellCast) {
|
||||
errors.push_back(octx + ": kind " +
|
||||
std::to_string(o.kind) + " not in known range 0..5");
|
||||
}
|
||||
}
|
||||
for (size_t ri = 0; ri < e.rewardItems.size(); ++ri) {
|
||||
const auto& r = e.rewardItems[ri];
|
||||
std::string rctx = ctx + " reward " + std::to_string(ri);
|
||||
if (r.itemId == 0) {
|
||||
errors.push_back(rctx + ": itemId is 0");
|
||||
}
|
||||
if (r.qty == 0) {
|
||||
errors.push_back(rctx + ": qty is 0");
|
||||
}
|
||||
}
|
||||
for (uint32_t prev : idsSeen) {
|
||||
if (prev == e.questId) {
|
||||
errors.push_back(ctx + ": duplicate questId");
|
||||
break;
|
||||
}
|
||||
}
|
||||
idsSeen.push_back(e.questId);
|
||||
}
|
||||
bool ok = errors.empty();
|
||||
if (jsonOut) {
|
||||
nlohmann::json j;
|
||||
j["wqt"] = base + ".wqt";
|
||||
j["ok"] = ok;
|
||||
j["errors"] = errors;
|
||||
j["warnings"] = warnings;
|
||||
std::printf("%s\n", j.dump(2).c_str());
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
std::printf("validate-wqt: %s.wqt\n", base.c_str());
|
||||
if (ok && warnings.empty()) {
|
||||
std::printf(" OK — %zu quests, all questIds unique\n",
|
||||
c.entries.size());
|
||||
return 0;
|
||||
}
|
||||
if (!warnings.empty()) {
|
||||
std::printf(" warnings (%zu):\n", warnings.size());
|
||||
for (const auto& w : warnings)
|
||||
std::printf(" - %s\n", w.c_str());
|
||||
}
|
||||
if (!errors.empty()) {
|
||||
std::printf(" ERRORS (%zu):\n", errors.size());
|
||||
for (const auto& e : errors)
|
||||
std::printf(" - %s\n", e.c_str());
|
||||
}
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool handleQuestsCatalog(int& i, int argc, char** argv, int& outRc) {
|
||||
if (std::strcmp(argv[i], "--gen-quests") == 0 && i + 1 < argc) {
|
||||
outRc = handleGenStarter(i, argc, argv); return true;
|
||||
}
|
||||
if (std::strcmp(argv[i], "--gen-quests-chain") == 0 && i + 1 < argc) {
|
||||
outRc = handleGenChain(i, argc, argv); return true;
|
||||
}
|
||||
if (std::strcmp(argv[i], "--gen-quests-daily") == 0 && i + 1 < argc) {
|
||||
outRc = handleGenDaily(i, argc, argv); return true;
|
||||
}
|
||||
if (std::strcmp(argv[i], "--info-wqt") == 0 && i + 1 < argc) {
|
||||
outRc = handleInfo(i, argc, argv); return true;
|
||||
}
|
||||
if (std::strcmp(argv[i], "--validate-wqt") == 0 && i + 1 < argc) {
|
||||
outRc = handleValidate(i, argc, argv); return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace cli
|
||||
} // namespace editor
|
||||
} // namespace wowee
|
||||
11
tools/editor/cli_quests_catalog.hpp
Normal file
11
tools/editor/cli_quests_catalog.hpp
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
#pragma once
|
||||
|
||||
namespace wowee {
|
||||
namespace editor {
|
||||
namespace cli {
|
||||
|
||||
bool handleQuestsCatalog(int& i, int argc, char** argv, int& outRc);
|
||||
|
||||
} // namespace cli
|
||||
} // namespace editor
|
||||
} // namespace wowee
|
||||
Loading…
Add table
Add a link
Reference in a new issue