441 lines
23 KiB
C++
441 lines
23 KiB
C++
#include "zeroerr/assert.h"
|
|
#include "zeroerr/unittest.h"
|
|
|
|
#include "common/Runtime.h"
|
|
#include "dialogue/ConversationCompiler.h"
|
|
#include "i18n/I18n.h"
|
|
#include "map/Map.h"
|
|
#include "map/MapObject.h"
|
|
#include "map/MapObjectContract.h"
|
|
#include "map/Pathfinder.h"
|
|
#include "map/TileConfig.h"
|
|
#include "rpg/EncounterCatalogue.h"
|
|
#include "rpg/Quest.h"
|
|
#include "rpg/Skill.h"
|
|
#include "rpg/StoryEvent.h"
|
|
#include "rpg/Tracker.h"
|
|
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <regex>
|
|
#include <set>
|
|
#include <sstream>
|
|
#include <string>
|
|
|
|
TEST_CASE("rpg.classic.mainScriptCompilesThroughEveScriptFrontend") {
|
|
const std::filesystem::path sourceRoot = std::filesystem::path(__FILE__).parent_path().parent_path();
|
|
const std::filesystem::path scriptPath = sourceRoot / "examples" / "rpg-classic" / "main.nut";
|
|
std::ifstream input(scriptPath, std::ios::binary);
|
|
REQUIRE(input.is_open());
|
|
std::ostringstream source;
|
|
source << input.rdbuf();
|
|
const std::string script = source.str();
|
|
|
|
CHECK(script.find("const SAVE_SLOT_COUNT = 3") != std::string::npos);
|
|
CHECK(script.find("function inspectSaveSlot(slot)") != std::string::npos);
|
|
CHECK(script.find("function migrateLegacySingleSlot()") != std::string::npos);
|
|
CHECK(script.find("loadCheckpoint(true)") != std::string::npos);
|
|
CHECK(script.find("newRunBeforeFirstSave") != std::string::npos);
|
|
CHECK(script.find("const SETTINGS_SCHEMA = \"rpg-classic.settings.v2\"") != std::string::npos);
|
|
CHECK(script.find("const LEGACY_SETTINGS_SCHEMA = \"rpg-classic.settings.v1\"") != std::string::npos);
|
|
CHECK(script.find("function selectProductLanguage(language)") != std::string::npos);
|
|
CHECK(script.find("dialogueFlow.validateLocalization(text, language)") != std::string::npos);
|
|
CHECK(script.find("function validateProductLocalization()") != std::string::npos);
|
|
CHECK(script.find("function openPauseMenu()") != std::string::npos);
|
|
CHECK(script.find("function confirmDeleteSaveSlot()") != std::string::npos);
|
|
CHECK(script.find("function confirmReturnToTitle()") != std::string::npos);
|
|
CHECK(script.find("saveFs.writeTextAtomic(SETTINGS_PATH") != std::string::npos);
|
|
CHECK(script.find("const SAVE_CONTENT_VERSION = \"rpg-classic.content.v9\"") != std::string::npos);
|
|
CHECK(script.find("function loadWorldMap(mapId, x, y, fx, fy)") != std::string::npos);
|
|
CHECK(script.find("function transitionThroughPortal(index)") != std::string::npos);
|
|
CHECK(script.find("function beginQuestNpcDialogue(index)") != std::string::npos);
|
|
CHECK(script.find("function openLootObject(index)") != std::string::npos);
|
|
CHECK(script.find("function claimQuestRewards(questId)") != std::string::npos);
|
|
CHECK(script.find("rpg.replaceQuestsFromJson(questsJson)") != std::string::npos);
|
|
CHECK(script.find("rpg.replaceEncountersFromJson(encountersJson)") != std::string::npos);
|
|
CHECK(script.find("rpg.newEncounterMemberActor(activeEncounterId, memberIndex)") != std::string::npos);
|
|
CHECK(script.find("function selectNextEnemy()") != std::string::npos);
|
|
CHECK(script.find("rpg.settleEncounterVictory(") != std::string::npos);
|
|
CHECK(script.find("previous catalogue retained") != std::string::npos);
|
|
CHECK(script.find("function validateWorldContentLinks()") != std::string::npos);
|
|
CHECK(script.find("dialogueFlow.hasConversation(conversationId)") != std::string::npos);
|
|
CHECK(script.find("rpg.hasQuestDefinition(questId)") != std::string::npos);
|
|
CHECK(script.find("persist acceptedQuestContent = null") != std::string::npos);
|
|
CHECK(script.find("function publishNarrativeContentPackage(reload = false)") != std::string::npos);
|
|
CHECK(script.find(
|
|
"function restoreNarrativeContent(previousQuest, previousDialogue, previousShop, previousEncounter,") !=
|
|
std::string::npos);
|
|
CHECK(script.find("rpg.replaceStoryEventsFromJson(storyEventJson)") != std::string::npos);
|
|
CHECK(script.find("text.replaceBundleFromJson(source)") != std::string::npos);
|
|
CHECK(script.find("dialogueFlow.validateLocalization(text, \"zh-CN\")") != std::string::npos);
|
|
CHECK(script.find("function resumePendingStoryEvent()") != std::string::npos);
|
|
CHECK(script.find("accepted content restored") != std::string::npos);
|
|
CHECK(script.find("publishNarrativeContentPackage(true)") != std::string::npos);
|
|
CHECK(script.find("loadFromFileWithObjectContract(path, worldObjectContract)") != std::string::npos);
|
|
CHECK(script.find("allowCompatibleContentVersion(PREVIOUS_SAVE_CONTENT_VERSION)") != std::string::npos);
|
|
CHECK(script.find("addQuestAdditionMigration(LEGACY_QUEST_CONTENT_VERSION") != std::string::npos);
|
|
CHECK(script.find("worldState.consumeObject(worldMapId, map.getObjectName(index))") != std::string::npos);
|
|
CHECK(script.find("save.mapCode") != std::string::npos);
|
|
|
|
eve::Runtime runtime(2048, ssq::Libs::ALL);
|
|
bool compiled = true;
|
|
try {
|
|
runtime.compileSource(script, "examples/rpg-classic/main.nut");
|
|
} catch (...) {
|
|
compiled = false;
|
|
}
|
|
CHECK(compiled);
|
|
}
|
|
|
|
TEST_CASE("rpg.classic.villageSupportsExplorationInteractionAndThreeEncounters") {
|
|
const std::filesystem::path sourceRoot = std::filesystem::path(__FILE__).parent_path().parent_path();
|
|
const std::filesystem::path mapPath = sourceRoot / "examples" / "rpg-classic" / "data" / "village.json";
|
|
std::ifstream input(mapPath, std::ios::binary);
|
|
REQUIRE(input.is_open());
|
|
std::ostringstream source;
|
|
source << input.rdbuf();
|
|
|
|
std::vector<eve::map::MapObject> objects;
|
|
std::string error;
|
|
auto layers = eve::map::loadMapText(source.str(), &objects, &error);
|
|
CHECK(error.empty());
|
|
REQUIRE_EQ(layers.size(), std::size_t(1));
|
|
auto* layer = layers.front();
|
|
REQUIRE(layer != nullptr);
|
|
CHECK_EQ(layer->getMapWidth(), 20);
|
|
CHECK_EQ(layer->getMapHeight(), 12);
|
|
|
|
eve::map::Pathfinder pathfinder(layer);
|
|
pathfinder.blockGid(2);
|
|
pathfinder.syncFromLayer();
|
|
CHECK(pathfinder.isWalkable(2, 5));
|
|
CHECK(!pathfinder.isWalkable(0, 5));
|
|
CHECK(!pathfinder.isWalkable(9, 2));
|
|
|
|
std::set<std::string> names;
|
|
int spawnCount = 0;
|
|
int questNpcCount = 0;
|
|
int merchantCount = 0;
|
|
int lootCount = 0;
|
|
int encounterCount = 0;
|
|
int portalCount = 0;
|
|
for (const auto& object : objects) {
|
|
CHECK(names.emplace(object.name).second);
|
|
if (object.type == "spawn")
|
|
++spawnCount;
|
|
else if (object.type == "quest_npc")
|
|
++questNpcCount;
|
|
else if (object.type == "merchant")
|
|
++merchantCount;
|
|
else if (object.type == "loot")
|
|
++lootCount;
|
|
else if (object.type == "portal")
|
|
++portalCount;
|
|
else if (object.type == "encounter") {
|
|
++encounterCount;
|
|
CHECK(pathfinder.isWalkable(static_cast<int>(object.x), static_cast<int>(object.y)));
|
|
}
|
|
}
|
|
CHECK_EQ(spawnCount, 1);
|
|
CHECK_EQ(questNpcCount, 1);
|
|
CHECK_EQ(merchantCount, 1);
|
|
CHECK_EQ(lootCount, 1);
|
|
CHECK_EQ(encounterCount, 2);
|
|
CHECK_EQ(portalCount, 1);
|
|
|
|
eve::map::Map objectQueries;
|
|
objectQueries.setObjects(objects);
|
|
const auto elder = objectQueries.findObjectByName("elder");
|
|
REQUIRE(elder.has_value());
|
|
const int elderIndex = static_cast<int>(*elder);
|
|
CHECK_EQ(objectQueries.getObjectType(elderIndex), std::string("quest_npc"));
|
|
CHECK_EQ(objectQueries.getObjectProperty(elderIndex, "questId", ""), std::string("quest.slayer"));
|
|
CHECK_EQ(objectQueries.getObjectProperty(elderIndex, "dialogueBase", ""), std::string("village.elder"));
|
|
const auto encounter = objectQueries.findObjectAt(7.f, 6.f, "encounter");
|
|
REQUIRE(encounter.has_value());
|
|
CHECK_EQ(objectQueries.getObjectName(static_cast<int>(*encounter)), std::string("slime_west"));
|
|
CHECK(!objectQueries.findObjectAt(7.f, 6.f, "merchant").has_value());
|
|
|
|
const auto villageGate = objectQueries.findObjectByName("forest_gate");
|
|
REQUIRE(villageGate.has_value());
|
|
const int villageGateIndex = static_cast<int>(*villageGate);
|
|
CHECK_EQ(objectQueries.getObjectPropertyCount(villageGateIndex), 5);
|
|
CHECK(objectQueries.hasObjectProperty(villageGateIndex, "targetMap"));
|
|
CHECK_EQ(objectQueries.getObjectProperty(villageGateIndex, "targetMap", ""), std::string("forest"));
|
|
CHECK_EQ(objectQueries.getObjectProperty(villageGateIndex, "targetX", ""), std::string("2"));
|
|
CHECK_EQ(objectQueries.getObjectProperty(villageGateIndex, "missing", "fallback"), std::string("fallback"));
|
|
CHECK_EQ(objectQueries.getObjectPropertyName(villageGateIndex, 0), std::string("facingX"));
|
|
|
|
const std::filesystem::path forestPath = sourceRoot / "examples" / "rpg-classic" / "data" / "forest.json";
|
|
std::ifstream forestInput(forestPath, std::ios::binary);
|
|
REQUIRE(forestInput.is_open());
|
|
std::ostringstream forestSource;
|
|
forestSource << forestInput.rdbuf();
|
|
std::vector<eve::map::MapObject> forestObjects;
|
|
auto forestLayers = eve::map::loadMapText(forestSource.str(), &forestObjects, &error);
|
|
CHECK(error.empty());
|
|
REQUIRE_EQ(forestLayers.size(), std::size_t(1));
|
|
|
|
const std::filesystem::path contractPath =
|
|
sourceRoot / "examples" / "rpg-classic" / "data" / "world-object-contract.json";
|
|
std::ifstream contractInput(contractPath, std::ios::binary);
|
|
REQUIRE(contractInput.is_open());
|
|
std::ostringstream contractSource;
|
|
contractSource << contractInput.rdbuf();
|
|
auto villageAdmission = eve::map::validateMapObjects(objects, contractSource.str());
|
|
CHECK(villageAdmission.ok());
|
|
auto forestAdmission = eve::map::validateMapObjects(forestObjects, contractSource.str());
|
|
CHECK(forestAdmission.ok());
|
|
|
|
eve::map::Map transactionalMap;
|
|
auto villageLoad = transactionalMap.loadFromTextWithObjectContract(source.str(), contractSource.str());
|
|
REQUIRE(villageLoad.ok());
|
|
CHECK(transactionalMap.findObjectByName("forest_gate").has_value());
|
|
const std::string restrictiveContract =
|
|
R"({"schema":"eve.map.object-contract","version":1,"types":[{"type":"spawn","properties":[]}]})";
|
|
auto rejectedForest = transactionalMap.loadFromTextWithObjectContract(forestSource.str(), restrictiveContract);
|
|
CHECK(!rejectedForest.ok());
|
|
CHECK(transactionalMap.findObjectByName("forest_gate").has_value());
|
|
CHECK(!transactionalMap.findObjectByName("ranger").has_value());
|
|
eve::map::Pathfinder forestPathfinder(forestLayers.front());
|
|
forestPathfinder.blockGid(2);
|
|
forestPathfinder.syncFromLayer();
|
|
int forestEncounterCount = 0;
|
|
int forestQuestNpcCount = 0;
|
|
int forestLootCount = 0;
|
|
for (const auto& object : forestObjects)
|
|
if (object.type == "encounter")
|
|
++forestEncounterCount;
|
|
else if (object.type == "quest_npc")
|
|
++forestQuestNpcCount;
|
|
else if (object.type == "loot")
|
|
++forestLootCount;
|
|
CHECK_EQ(encounterCount + forestEncounterCount, 3);
|
|
CHECK_EQ(forestQuestNpcCount, 1);
|
|
CHECK_EQ(forestLootCount, 1);
|
|
CHECK(forestPathfinder.isWalkable(std::stoi(objectQueries.getObjectProperty(villageGateIndex, "targetX", "-1")),
|
|
std::stoi(objectQueries.getObjectProperty(villageGateIndex, "targetY", "-1"))));
|
|
|
|
eve::map::Map forestQueries;
|
|
forestQueries.setObjects(forestObjects);
|
|
const auto returnGate = forestQueries.findObjectByName("village_gate");
|
|
REQUIRE(returnGate.has_value());
|
|
const int returnGateIndex = static_cast<int>(*returnGate);
|
|
CHECK_EQ(forestQueries.getObjectProperty(returnGateIndex, "targetMap", ""), std::string("village"));
|
|
CHECK(pathfinder.isWalkable(std::stoi(forestQueries.getObjectProperty(returnGateIndex, "targetX", "-1")),
|
|
std::stoi(forestQueries.getObjectProperty(returnGateIndex, "targetY", "-1"))));
|
|
const auto ranger = forestQueries.findObjectByName("ranger");
|
|
REQUIRE(ranger.has_value());
|
|
CHECK_EQ(forestQueries.getObjectProperty(static_cast<int>(*ranger), "questId", ""),
|
|
std::string("quest.ranger_cache"));
|
|
const auto cache = forestQueries.findObjectByName("forest_cache");
|
|
REQUIRE(cache.has_value());
|
|
const int cacheIndex = static_cast<int>(*cache);
|
|
CHECK_EQ(forestQueries.getObjectProperty(cacheIndex, "requiredQuest", ""), std::string("quest.ranger_cache"));
|
|
CHECK_EQ(forestQueries.getObjectProperty(cacheIndex, "notifyTopic", ""), std::string("interact"));
|
|
CHECK_EQ(forestQueries.getObjectProperty(cacheIndex, "notifyTarget", ""), std::string("forest_cache"));
|
|
}
|
|
|
|
TEST_CASE("rpg.classic.elderDialogueHasOfferReminderTurnInAndCompletionRoutes") {
|
|
const std::filesystem::path sourceRoot = std::filesystem::path(__FILE__).parent_path().parent_path();
|
|
const std::filesystem::path dialoguePath =
|
|
sourceRoot / "examples" / "rpg-classic" / "data" / "village-dialogue.dnut";
|
|
std::ifstream input(dialoguePath, std::ios::binary);
|
|
REQUIRE(input.is_open());
|
|
std::ostringstream source;
|
|
source << input.rdbuf();
|
|
|
|
std::vector<eve::dialogue::ConversationAsset> assets;
|
|
std::vector<eve::dialogue::ConversationDiagnostic> diagnostics;
|
|
CHECK(eve::dialogue::compileDnutConversations(source.str(), dialoguePath.string(), assets, diagnostics));
|
|
CHECK_EQ(assets.size(), std::size_t(9));
|
|
CHECK(diagnostics.empty());
|
|
|
|
const auto findAsset = [&](const std::string& id) -> const eve::dialogue::ConversationAsset* {
|
|
for (const auto& asset : assets)
|
|
if (asset.id == id) return &asset;
|
|
return nullptr;
|
|
};
|
|
const auto* offer = findAsset("village.elder.offer");
|
|
const auto* turnIn = findAsset("village.elder.turnin");
|
|
const auto* rangerOffer = findAsset("forest.ranger.offer");
|
|
const auto* rangerTurnIn = findAsset("forest.ranger.turnin");
|
|
REQUIRE(offer != nullptr);
|
|
REQUIRE(turnIn != nullptr);
|
|
REQUIRE(rangerOffer != nullptr);
|
|
REQUIRE(rangerTurnIn != nullptr);
|
|
const auto* offerChoice = offer->findNode("decision");
|
|
const auto* turnInChoice = turnIn->findNode("decision");
|
|
REQUIRE(offerChoice != nullptr);
|
|
REQUIRE(turnInChoice != nullptr);
|
|
REQUIRE_EQ(offerChoice->routes.size(), std::size_t(2));
|
|
REQUIRE_EQ(turnInChoice->routes.size(), std::size_t(2));
|
|
CHECK_EQ(offerChoice->routes[0].first, std::string("accept"));
|
|
CHECK_EQ(offerChoice->routes[1].first, std::string("later"));
|
|
CHECK_EQ(turnInChoice->routes[0].first, std::string("claim"));
|
|
CHECK_EQ(turnInChoice->routes[1].first, std::string("later"));
|
|
REQUIRE(rangerOffer->findNode("decision") != nullptr);
|
|
REQUIRE(rangerTurnIn->findNode("decision") != nullptr);
|
|
|
|
eve::dialogue::ConversationRunner runner;
|
|
std::string error;
|
|
CHECK(runner.start(offer, eve::StateValue::object(), &error));
|
|
CHECK_EQ(runner.currentNodeId(), std::string("intro"));
|
|
CHECK(runner.advance(&error));
|
|
CHECK_EQ(runner.currentNodeId(), std::string("decision"));
|
|
CHECK(runner.select("accept", &error));
|
|
CHECK_EQ(runner.currentNodeId(), std::string("accepted"));
|
|
CHECK(runner.advance(&error));
|
|
CHECK(!runner.isActive());
|
|
}
|
|
|
|
TEST_CASE("rpg.classic.forestSideQuestHasDataDrivenObjectiveAndRewards") {
|
|
const std::filesystem::path sourceRoot = std::filesystem::path(__FILE__).parent_path().parent_path();
|
|
const std::filesystem::path questPath = sourceRoot / "examples" / "rpg-classic" / "data" / "quests.json";
|
|
std::ifstream input(questPath, std::ios::binary);
|
|
REQUIRE(input.is_open());
|
|
std::ostringstream source;
|
|
source << input.rdbuf();
|
|
|
|
eve::rpg::QuestRegistry::clear();
|
|
REQUIRE_EQ(eve::rpg::QuestRegistry::loadFromJson(source.str()), 2);
|
|
const auto* definition = eve::rpg::QuestRegistry::find("quest.ranger_cache");
|
|
REQUIRE(definition != nullptr);
|
|
REQUIRE_EQ(definition->objectives.size(), std::size_t(1));
|
|
CHECK_EQ(definition->objectives[0].topic, std::string("interact"));
|
|
CHECK_EQ(definition->objectives[0].target, std::string("forest_cache"));
|
|
REQUIRE_EQ(definition->rewards.size(), std::size_t(2));
|
|
CHECK_EQ(definition->rewards[0].type, std::string("item"));
|
|
CHECK_EQ(definition->rewards[0].id, std::string("potion"));
|
|
CHECK_EQ(definition->rewards[1].type, std::string("attribute"));
|
|
CHECK_EQ(definition->rewards[1].id, std::string("gold"));
|
|
|
|
eve::rpg::Tracker tracker;
|
|
CHECK(tracker.activate("quest.ranger_cache"));
|
|
tracker.notify("interact", "forest_cache", 1);
|
|
CHECK_EQ(tracker.getState("quest.ranger_cache"), std::string("ready"));
|
|
CHECK(tracker.claim("quest.ranger_cache"));
|
|
CHECK_EQ(tracker.getState("quest.ranger_cache"), std::string("completed"));
|
|
eve::rpg::QuestRegistry::clear();
|
|
}
|
|
|
|
TEST_CASE("rpg.classic.mapsQuestsAndDialogueComposeWithoutDanglingReferences") {
|
|
const std::filesystem::path root =
|
|
std::filesystem::path(__FILE__).parent_path().parent_path() / "examples" / "rpg-classic" / "data";
|
|
const auto readFile = [&](const std::filesystem::path& path) {
|
|
std::ifstream input(path, std::ios::binary);
|
|
REQUIRE(input.is_open());
|
|
std::ostringstream source;
|
|
source << input.rdbuf();
|
|
return source.str();
|
|
};
|
|
|
|
eve::rpg::QuestRegistry::clear();
|
|
eve::rpg::SkillRegistry::clear();
|
|
eve::rpg::EncounterCatalogue::clear();
|
|
REQUIRE_EQ(eve::rpg::SkillRegistry::loadFromJson(readFile(root / "skills.json")), 6);
|
|
auto quests = eve::rpg::QuestRegistry::replaceFromJsonStrict(readFile(root / "quests.json"));
|
|
REQUIRE(quests.ok());
|
|
auto encounters = eve::rpg::EncounterCatalogue::replaceFromJsonStrict(readFile(root / "encounters.json"));
|
|
REQUIRE(encounters.ok());
|
|
auto storyEvents = eve::rpg::StoryEventCatalogue::replaceFromJsonStrict(readFile(root / "story-events.json"));
|
|
REQUIRE(storyEvents.ok());
|
|
|
|
std::vector<eve::dialogue::ConversationAsset> assets;
|
|
std::vector<eve::dialogue::ConversationDiagnostic> diagnostics;
|
|
REQUIRE(eve::dialogue::compileDnutConversations(readFile(root / "village-dialogue.dnut"), "village-dialogue.dnut",
|
|
assets, diagnostics));
|
|
REQUIRE(diagnostics.empty());
|
|
auto* localization = eve::i18n::I18n::create();
|
|
REQUIRE(localization != nullptr);
|
|
localization->clear();
|
|
auto localizationBundle = localization->replaceBundleFromJson(readFile(root / "localization.json"));
|
|
REQUIRE(localizationBundle.ok());
|
|
CHECK_EQ(localizationBundle.value(), 2);
|
|
const std::vector<std::string> productKeys{
|
|
"content.map.village.name",
|
|
"content.map.forest.name",
|
|
"content.item.potion.name",
|
|
"content.item.iron_sword.name",
|
|
"content.item.leather_armor.name",
|
|
"content.quest.quest.slayer.title",
|
|
"content.quest.quest.ranger_cache.title",
|
|
"content.shop.potion.description",
|
|
"content.skill.skill.fireball.name",
|
|
"content.encounter.slime.forest.name",
|
|
"content.encounterMember.slime.forest.forest.alpha.name",
|
|
"ui.title.continue",
|
|
"ui.adjust.offer",
|
|
"ui.gameOver.summary",
|
|
};
|
|
for (const auto& language : {"zh-CN", "en"})
|
|
for (const auto& key : productKeys) CHECK(localization->hasInLanguage(language, key));
|
|
const std::string scriptSource = readFile(root.parent_path() / "main.nut");
|
|
const std::regex translationCall(R"rx(\btrp?\("([^"]+)")rx");
|
|
for (std::sregex_iterator it(scriptSource.begin(), scriptSource.end(), translationCall), end; it != end; ++it) {
|
|
const std::string key = (*it)[1].str();
|
|
auto coverage = localization->validateKeyCoverage(key);
|
|
CHECK(coverage.ok());
|
|
if (coverage.ok()) CHECK_EQ(coverage.value(), 2);
|
|
}
|
|
const std::regex logCall(R"rx(\blogMessage\("([^"]+)")rx");
|
|
for (std::sregex_iterator it(scriptSource.begin(), scriptSource.end(), logCall), end; it != end; ++it) {
|
|
auto coverage = localization->validateKeyCoverage("gameplayLog." + (*it)[1].str());
|
|
CHECK(coverage.ok());
|
|
if (coverage.ok()) CHECK_EQ(coverage.value(), 2);
|
|
}
|
|
REQUIRE(localization->selectLanguage("zh-CN").ok());
|
|
CHECK_EQ(localization->get("content.item.iron_sword.name"), std::string("铁剑"));
|
|
REQUIRE(localization->selectLanguage("en").ok());
|
|
CHECK_EQ(localization->get("content.item.iron_sword.name"), std::string("Iron Sword"));
|
|
std::set<std::string> conversationIds;
|
|
for (const auto& asset : assets) {
|
|
conversationIds.insert(asset.id);
|
|
for (const auto& node : asset.nodes) {
|
|
if (node.i18nKey.empty()) continue;
|
|
CHECK(localization->hasInLanguage("zh-CN", node.i18nKey));
|
|
CHECK(localization->hasInLanguage("en", node.i18nKey));
|
|
}
|
|
}
|
|
const auto* arrival = eve::rpg::StoryEventCatalogue::find("forest.arrival");
|
|
REQUIRE(arrival != nullptr);
|
|
for (const auto& step : arrival->steps) {
|
|
if (step.kind == eve::rpg::StoryEventStepKind::Dialogue) CHECK(conversationIds.count(step.reference) == 1);
|
|
if (step.kind == eve::rpg::StoryEventStepKind::Message)
|
|
CHECK(localization->validateKeyCoverage(step.reference).ok());
|
|
}
|
|
|
|
const std::vector<std::string> states{"offer", "active", "turnin", "completed"};
|
|
for (const auto& mapName : {"village.json", "forest.json"}) {
|
|
std::vector<eve::map::MapObject> objects;
|
|
std::string error;
|
|
auto layers = eve::map::loadMapText(readFile(root / mapName), &objects, &error);
|
|
REQUIRE(error.empty());
|
|
REQUIRE(!layers.empty());
|
|
for (const auto& object : objects) {
|
|
const auto property = [&](const char* name) {
|
|
const auto found = object.properties.find(name);
|
|
return found == object.properties.end() ? std::string{} : found->second;
|
|
};
|
|
if (object.type == "quest_npc") {
|
|
const auto questId = property("questId");
|
|
const auto dialogueBase = property("dialogueBase");
|
|
CHECK(eve::rpg::QuestRegistry::contains(questId));
|
|
for (const auto& state : states) CHECK(conversationIds.count(dialogueBase + "." + state) == 1);
|
|
} else if (object.type == "loot") {
|
|
const auto requiredQuest = property("requiredQuest");
|
|
const bool validRequiredQuest =
|
|
requiredQuest.empty() || eve::rpg::QuestRegistry::contains(requiredQuest);
|
|
CHECK(validRequiredQuest);
|
|
} else if (object.type == "encounter") {
|
|
CHECK(eve::rpg::EncounterCatalogue::find(property("encounterId")) != nullptr);
|
|
}
|
|
}
|
|
}
|
|
eve::rpg::QuestRegistry::clear();
|
|
eve::rpg::EncounterCatalogue::clear();
|
|
eve::rpg::SkillRegistry::clear();
|
|
eve::rpg::StoryEventCatalogue::clear();
|
|
localization->clear();
|
|
}
|