371 lines
14 KiB
C++
371 lines
14 KiB
C++
#include "zeroerr/assert.h"
|
|
#include "zeroerr/unittest.h"
|
|
|
|
#include "rpg/Quest.h"
|
|
#include "rpg/Tracker.h"
|
|
#include "rpg/RPG.h"
|
|
|
|
using namespace eve::rpg;
|
|
|
|
TEST_CASE("rpg.quest.registryCRUD") {
|
|
QuestRegistry::clear();
|
|
QuestDefinition def;
|
|
def.id = "quest.intro";
|
|
def.tags = {"main"};
|
|
def.extra["title"] = "Intro";
|
|
QuestRegistry::registerQuest(def);
|
|
CHECK_EQ(QuestRegistry::count(), 1);
|
|
const QuestDefinition *found = QuestRegistry::find("quest.intro");
|
|
REQUIRE(found != nullptr);
|
|
CHECK(found->hasTag("main"));
|
|
CHECK_EQ(found->getExtra("title", ""), std::string("Intro"));
|
|
CHECK_EQ(found->startPolicy, std::string("manual"));
|
|
CHECK_EQ(found->completePolicy, std::string("auto"));
|
|
CHECK(QuestRegistry::remove("quest.intro"));
|
|
CHECK_EQ(QuestRegistry::count(), 0);
|
|
QuestRegistry::clear();
|
|
}
|
|
|
|
TEST_CASE("rpg.quest.loadFromJsonDefaultsAndRewards") {
|
|
QuestRegistry::clear();
|
|
int n = QuestRegistry::loadFromJson(R"([
|
|
{
|
|
"id": "quest.hunt",
|
|
"startPolicy": "manual",
|
|
"completePolicy": "claim",
|
|
"requires": ["quest.intro"],
|
|
"tags": ["main"],
|
|
"objectives": [
|
|
{"id": "kill_slime", "topic": "kill", "target": "slime", "count": 10},
|
|
{"id": "any_kill", "topic": "kill", "count": 0}
|
|
],
|
|
"rewards": [
|
|
{"type": "item", "id": "potion", "amount": 3},
|
|
{"type": "attribute", "id": "xp", "amount": 50}
|
|
],
|
|
"extra": {"title": "Hunt"}
|
|
}
|
|
])");
|
|
CHECK_EQ(n, 1);
|
|
const QuestDefinition *d = QuestRegistry::find("quest.hunt");
|
|
REQUIRE(d != nullptr);
|
|
CHECK_EQ(d->startPolicy, std::string("manual"));
|
|
CHECK_EQ(d->completePolicy, std::string("claim"));
|
|
REQUIRE(d->requiresIds.size() == 1);
|
|
CHECK_EQ(d->requiresIds[0], std::string("quest.intro"));
|
|
REQUIRE(d->objectives.size() == 2);
|
|
CHECK_EQ(d->objectives[1].count, 1); // count<=0 → 1
|
|
CHECK(d->objectives[1].target.empty());
|
|
REQUIRE(d->rewards.size() == 2);
|
|
CHECK_EQ(d->rewards[0].type, std::string("item"));
|
|
CHECK_EQ(d->rewards[1].amount, 50.0);
|
|
CHECK_EQ(d->getExtra("title", ""), std::string("Hunt"));
|
|
QuestRegistry::clear();
|
|
}
|
|
|
|
TEST_CASE("rpg.quest.loadFromJsonRejectsBadAndCycles") {
|
|
QuestRegistry::clear();
|
|
int n = QuestRegistry::loadFromJson(R"([
|
|
{"startPolicy": "auto"},
|
|
{
|
|
"id": "dup.obj",
|
|
"objectives": [
|
|
{"id": "a", "topic": "kill", "count": 1},
|
|
{"id": "a", "topic": "talk", "count": 1}
|
|
]
|
|
},
|
|
{"id": "cyc.a", "requires": ["cyc.b"]},
|
|
{"id": "cyc.b", "requires": ["cyc.a"]},
|
|
{"id": "ok.solo", "startPolicy": "weird", "completePolicy": "nope"}
|
|
])");
|
|
CHECK_EQ(n, 1);
|
|
CHECK(QuestRegistry::find("dup.obj") == nullptr);
|
|
CHECK(QuestRegistry::find("cyc.a") == nullptr);
|
|
CHECK(QuestRegistry::find("cyc.b") == nullptr);
|
|
const QuestDefinition *ok = QuestRegistry::find("ok.solo");
|
|
REQUIRE(ok != nullptr);
|
|
CHECK_EQ(ok->startPolicy, std::string("manual"));
|
|
CHECK_EQ(ok->completePolicy, std::string("auto"));
|
|
QuestRegistry::clear();
|
|
}
|
|
|
|
TEST_CASE("rpg.quest.trackerInitialStatesAndSyncAuto") {
|
|
QuestRegistry::clear();
|
|
QuestRegistry::loadFromJson(R"([
|
|
{"id":"q.intro","startPolicy":"manual"},
|
|
{"id":"q.auto","startPolicy":"auto","objectives":[{"id":"o","topic":"kill","target":"x","count":1}]},
|
|
{"id":"q.locked","startPolicy":"auto","requires":["q.missing"]},
|
|
{"id":"q.chain","startPolicy":"manual","requires":["q.intro"]}
|
|
])");
|
|
|
|
Tracker log;
|
|
CHECK_EQ(log.getCount(), 4);
|
|
CHECK_EQ(log.getState("q.intro"), std::string("inactive"));
|
|
CHECK_EQ(log.getState("q.auto"), std::string("active"));
|
|
CHECK_EQ(log.getState("q.locked"), std::string("locked"));
|
|
CHECK_EQ(log.getState("q.chain"), std::string("locked"));
|
|
CHECK_EQ(log.getState("nope"), std::string(""));
|
|
|
|
QuestRegistry::loadFromJson(R"([{"id":"q.late","startPolicy":"auto","objectives":[{"id":"o","topic":"kill","target":"x","count":1}]}])");
|
|
CHECK_EQ(log.getCount(), 4);
|
|
log.syncAuto();
|
|
CHECK_EQ(log.getCount(), 5);
|
|
CHECK_EQ(log.getState("q.late"), std::string("active"));
|
|
CHECK(log.hasTag("q.intro", "x") == false);
|
|
|
|
QuestRegistry::clear();
|
|
}
|
|
|
|
TEST_CASE("rpg.quest.activateAndNotify") {
|
|
QuestRegistry::clear();
|
|
QuestRegistry::loadFromJson(R"([
|
|
{"id":"q.lock","startPolicy":"manual","requires":["nope"]},
|
|
{"id":"q.hunt","startPolicy":"manual","completePolicy":"auto",
|
|
"objectives":[
|
|
{"id":"k","topic":"kill","target":"slime","count":2},
|
|
{"id":"any","topic":"kill","count":3}
|
|
]},
|
|
{"id":"q.flag","startPolicy":"manual","completePolicy":"auto","objectives":[]}
|
|
])");
|
|
Tracker log;
|
|
CHECK_EQ(log.canActivateReason("q.lock"), std::string("locked"));
|
|
CHECK(!log.activate("q.lock"));
|
|
CHECK_EQ(log.getState("q.lock"), std::string("locked"));
|
|
|
|
CHECK(log.canActivate("q.hunt"));
|
|
CHECK(log.activate("q.hunt"));
|
|
CHECK_EQ(log.getState("q.hunt"), std::string("active"));
|
|
CHECK_EQ(log.canActivateReason("q.hunt"), std::string("alreadyActive"));
|
|
|
|
log.notify("talk", "npc", 1);
|
|
CHECK_EQ(log.getObjectiveCurrent("q.hunt", 0), 0);
|
|
|
|
log.notify("kill", "slime", 1);
|
|
CHECK_EQ(log.getObjectiveCurrent("q.hunt", 0), 1);
|
|
CHECK_EQ(log.getObjectiveCurrent("q.hunt", 1), 1); // target 空,吃所有 kill
|
|
log.notify("kill", "goblin", 5);
|
|
CHECK_EQ(log.getObjectiveCurrent("q.hunt", 0), 1); // slime 不涨
|
|
CHECK_EQ(log.getObjectiveCurrent("q.hunt", 1), 3); // 封顶 3
|
|
CHECK(log.isObjectiveDone("q.hunt", 1));
|
|
|
|
log.notify("kill", "slime", 0);
|
|
CHECK_EQ(log.getObjectiveCurrent("q.hunt", 0), 1);
|
|
log.notify("kill", "slime", 1);
|
|
CHECK(log.isObjectiveDone("q.hunt", 0));
|
|
CHECK_EQ(log.getState("q.hunt"), std::string("completed"));
|
|
|
|
CHECK(log.activate("q.flag"));
|
|
CHECK_EQ(log.getState("q.flag"), std::string("completed"));
|
|
|
|
QuestRegistry::clear();
|
|
}
|
|
|
|
TEST_CASE("rpg.quest.claimEventsAndChain") {
|
|
QuestRegistry::clear();
|
|
QuestRegistry::loadFromJson(R"([
|
|
{"id":"a","startPolicy":"manual","completePolicy":"claim",
|
|
"objectives":[{"id":"k","topic":"kill","target":"slime","count":1}]},
|
|
{"id":"b","startPolicy":"auto","completePolicy":"auto","requires":["a"],
|
|
"objectives":[{"id":"t","topic":"talk","target":"npc","count":1}]}
|
|
])");
|
|
Tracker log;
|
|
CHECK_EQ(log.getState("b"), std::string("locked"));
|
|
CHECK(log.activate("a"));
|
|
log.notify("kill", "slime", 1);
|
|
CHECK_EQ(log.getState("a"), std::string("ready"));
|
|
CHECK(!log.claim("nope"));
|
|
CHECK(!log.claim("b"));
|
|
CHECK(log.claim("a"));
|
|
CHECK_EQ(log.getState("a"), std::string("completed"));
|
|
CHECK_EQ(log.getState("b"), std::string("active"));
|
|
|
|
log.pollEvents();
|
|
bool sawProgress = false, sawReady = false, sawComplete = false, sawActivateB = false;
|
|
for (int i = 0; i < log.getEventCount(); ++i) {
|
|
auto act = log.getEventAction(i);
|
|
auto id = log.getEventEntryId(i);
|
|
if (act == "progress" && id == "a") sawProgress = true;
|
|
if (act == "ready" && id == "a") sawReady = true;
|
|
if (act == "complete" && id == "a") sawComplete = true;
|
|
if (act == "activate" && id == "b") sawActivateB = true;
|
|
}
|
|
CHECK(sawProgress);
|
|
CHECK(sawReady);
|
|
CHECK(sawComplete);
|
|
CHECK(sawActivateB);
|
|
|
|
Tracker ach;
|
|
QuestRegistry::loadFromJson(R"([
|
|
{"id":"ach.kill","startPolicy":"auto","completePolicy":"auto","tags":["achievement"],
|
|
"objectives":[{"id":"k","topic":"kill","target":"slime","count":1}]}
|
|
])");
|
|
ach.syncAuto();
|
|
log.notify("kill", "slime", 1);
|
|
ach.notify("kill", "slime", 1);
|
|
CHECK_EQ(log.getState("ach.kill"), std::string("")); // log 未 sync 这条也可;隔离重点是事件
|
|
CHECK_EQ(ach.getState("ach.kill"), std::string("completed"));
|
|
log.pollEvents();
|
|
ach.pollEvents();
|
|
bool logCompletedAch = false;
|
|
for (int i = 0; i < log.getEventCount(); ++i)
|
|
if (log.getEventEntryId(i) == "ach.kill") logCompletedAch = true;
|
|
CHECK(!logCompletedAch);
|
|
CHECK(ach.getEventCount() >= 1);
|
|
|
|
QuestRegistry::clear();
|
|
}
|
|
|
|
TEST_CASE("rpg.quest.resetAbandonFailNoCascade") {
|
|
QuestRegistry::clear();
|
|
QuestRegistry::loadFromJson(R"([
|
|
{"id":"daily","startPolicy":"auto","completePolicy":"auto",
|
|
"objectives":[{"id":"k","topic":"kill","target":"slime","count":1}]},
|
|
{"id":"main","startPolicy":"manual","completePolicy":"auto","requires":["daily"],
|
|
"objectives":[{"id":"t","topic":"talk","target":"npc","count":1}]}
|
|
])");
|
|
Tracker log;
|
|
log.notify("kill", "slime", 1);
|
|
CHECK_EQ(log.getState("daily"), std::string("completed"));
|
|
CHECK(log.activate("main"));
|
|
CHECK_EQ(log.getState("main"), std::string("active"));
|
|
|
|
CHECK(log.reset("daily"));
|
|
CHECK_EQ(log.getState("daily"), std::string("active"));
|
|
CHECK_EQ(log.getObjectiveCurrent("daily", 0), 0);
|
|
CHECK_EQ(log.getState("main"), std::string("active")); // 不级联
|
|
|
|
CHECK(log.abandon("main"));
|
|
CHECK_EQ(log.getState("main"), std::string("failed"));
|
|
log.notify("talk", "npc", 1);
|
|
CHECK_EQ(log.getObjectiveCurrent("main", 0), 0);
|
|
CHECK(!log.activate("main"));
|
|
CHECK_EQ(log.canActivateReason("main"), std::string("failed"));
|
|
CHECK(log.reset("main"));
|
|
// daily 已 reset 且未完成,main 的 requires 不满足 → locked
|
|
CHECK_EQ(log.getState("main"), std::string("locked"));
|
|
|
|
Tracker t2;
|
|
QuestRegistry::loadFromJson(R"([{"id":"only","startPolicy":"manual"}])");
|
|
t2.syncAuto();
|
|
CHECK(t2.fail("only", "timeout"));
|
|
CHECK_EQ(t2.getState("only"), std::string("failed"));
|
|
t2.pollEvents();
|
|
bool sawTimeout = false;
|
|
for (int i = 0; i < t2.getEventCount(); ++i)
|
|
if (t2.getEventAction(i) == "fail" && t2.getEventReason(i) == "timeout") sawTimeout = true;
|
|
CHECK(sawTimeout);
|
|
|
|
QuestRegistry::clear();
|
|
}
|
|
|
|
TEST_CASE("rpg.quest.facadeManualClaim") {
|
|
auto *rpg = RPG::create();
|
|
rpg->clearQuestDefinitions();
|
|
int n = rpg->registerQuestsFromJson(R"([
|
|
{"id":"quest.hunt","startPolicy":"manual","completePolicy":"claim",
|
|
"objectives":[{"id":"k","topic":"kill","target":"slime","count":1}],
|
|
"rewards":[{"type":"item","id":"potion","amount":3}]}
|
|
])");
|
|
CHECK_EQ(n, 1);
|
|
CHECK_EQ(rpg->getQuestDefinitionCount(), 1);
|
|
|
|
Tracker *log = rpg->newTracker();
|
|
REQUIRE(log != nullptr);
|
|
CHECK(log->activate("quest.hunt"));
|
|
log->notify("kill", "slime", 1);
|
|
CHECK_EQ(log->getState("quest.hunt"), std::string("ready"));
|
|
CHECK(log->claim("quest.hunt"));
|
|
CHECK_EQ(log->getState("quest.hunt"), std::string("completed"));
|
|
CHECK_EQ(log->getRewardCount("quest.hunt"), 1);
|
|
CHECK_EQ(log->getRewardId("quest.hunt", 0), std::string("potion"));
|
|
|
|
log->pollEvents();
|
|
CHECK(log->getEventCount() >= 1);
|
|
|
|
rpg->clearQuestDefinitions();
|
|
}
|
|
|
|
TEST_CASE("rpg.quest.versionedSnapshotRoundTripAndRegistryMismatchRollback") {
|
|
QuestRegistry::clear();
|
|
REQUIRE_EQ(QuestRegistry::loadFromJson(R"([
|
|
{"id":"quest.hunt","startPolicy":"auto","completePolicy":"claim",
|
|
"objectives":[{"id":"kill","topic":"kill","target":"slime","count":3}]}
|
|
])"), 1);
|
|
Tracker source;
|
|
source.notify("kill", "slime", 2);
|
|
source.pollEvents();
|
|
auto encoded = source.snapshotJson();
|
|
REQUIRE(encoded.ok());
|
|
|
|
Tracker restored;
|
|
restored.notify("kill", "slime", 1);
|
|
REQUIRE(restored.restoreSnapshotJson(encoded.value()).ok());
|
|
CHECK_EQ(restored.getState("quest.hunt"), std::string("active"));
|
|
CHECK_EQ(restored.getObjectiveCurrent("quest.hunt", 0), 2);
|
|
restored.pollEvents();
|
|
CHECK_EQ(restored.getEventCount(), 0);
|
|
|
|
const auto before = restored.snapshotJson();
|
|
REQUIRE(before.ok());
|
|
auto malformed = restored.restoreSnapshotJson(
|
|
R"({"schema":"eve.rpg.quest-tracker","version":1,"entries":[{"id":"quest.hunt","state":"completed","objectives":[{"id":"kill","current":1}]}]})");
|
|
CHECK(!malformed.ok());
|
|
auto afterMalformed = restored.snapshotJson();
|
|
REQUIRE(afterMalformed.ok());
|
|
CHECK_EQ(afterMalformed.value(), before.value());
|
|
|
|
QuestRegistry::clear();
|
|
REQUIRE_EQ(QuestRegistry::loadFromJson(R"([
|
|
{"id":"quest.hunt","startPolicy":"auto","completePolicy":"claim",
|
|
"objectives":[{"id":"kill-v2","topic":"kill","target":"slime","count":3}]}
|
|
])"), 1);
|
|
auto mismatch = restored.restoreSnapshotJson(encoded.value());
|
|
CHECK(!mismatch.ok());
|
|
CHECK_EQ(restored.getObjectiveCurrent("quest.hunt", 0), 2);
|
|
QuestRegistry::clear();
|
|
}
|
|
|
|
TEST_CASE("rpg.quest.strictReplacementCommitsCompleteCatalogueAtomically") {
|
|
QuestRegistry::clear();
|
|
REQUIRE_EQ(QuestRegistry::loadFromJson(R"([{"id":"old.quest"}])"), 1);
|
|
|
|
auto replaced = QuestRegistry::replaceFromJsonStrict(R"([
|
|
{"id":"quest.intro","startPolicy":"manual","completePolicy":"auto",
|
|
"objectives":[{"id":"talk","topic":"talk","target":"elder","count":1}]},
|
|
{"id":"quest.hunt","startPolicy":"auto","completePolicy":"claim","requires":["quest.intro"],
|
|
"objectives":[{"id":"kill","topic":"kill","target":"slime","count":3}],
|
|
"rewards":[{"type":"item","id":"potion","amount":2}],
|
|
"tags":["main"],"extra":{"title":"Hunt"}}
|
|
])");
|
|
REQUIRE(replaced.ok());
|
|
CHECK_EQ(replaced.value(), 2);
|
|
CHECK(QuestRegistry::find("old.quest") == nullptr);
|
|
REQUIRE(QuestRegistry::find("quest.hunt") != nullptr);
|
|
CHECK_EQ(QuestRegistry::find("quest.hunt")->getExtra("title"), std::string("Hunt"));
|
|
QuestRegistry::clear();
|
|
}
|
|
|
|
TEST_CASE("rpg.quest.strictReplacementRejectsInvalidBatchWithoutMutation") {
|
|
QuestRegistry::clear();
|
|
auto initial = QuestRegistry::replaceFromJsonStrict(R"([{"id":"quest.stable"}])");
|
|
REQUIRE(initial.ok());
|
|
|
|
auto invalidPolicy = QuestRegistry::replaceFromJsonStrict(
|
|
R"([{"id":"quest.new","startPolicy":"sometimes"}])");
|
|
CHECK(!invalidPolicy.ok());
|
|
CHECK(QuestRegistry::find("quest.stable") != nullptr);
|
|
CHECK(QuestRegistry::find("quest.new") == nullptr);
|
|
|
|
auto missingDependency = QuestRegistry::replaceFromJsonStrict(
|
|
R"([{"id":"quest.new","requires":["quest.missing"]}])");
|
|
CHECK(!missingDependency.ok());
|
|
CHECK(QuestRegistry::find("quest.stable") != nullptr);
|
|
|
|
auto partialBatch = QuestRegistry::replaceFromJsonStrict(
|
|
R"([{"id":"quest.good"},{"id":"quest.bad","objectives":[{"id":"o","topic":"kill","count":0}]}])");
|
|
CHECK(!partialBatch.ok());
|
|
CHECK(QuestRegistry::find("quest.good") == nullptr);
|
|
CHECK(QuestRegistry::find("quest.stable") != nullptr);
|
|
QuestRegistry::clear();
|
|
}
|