Files
EVEngine/test/rpg_classic_playthrough.cpp

269 lines
12 KiB
C++

#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
#include "dialogue/Conversation.h"
#include "dialogue/ConversationCompiler.h"
#include "i18n/I18n.h"
#include "inventory/Bag.h"
#include "inventory/Equipment.h"
#include "inventory/Item.h"
#include "map/Map.h"
#include "map/MapObject.h"
#include "rpg/Battle.h"
#include "rpg/BattleTactics.h"
#include "rpg/EncounterCatalogue.h"
#include "rpg/GameState.h"
#include "rpg/Party.h"
#include "rpg/Quest.h"
#include "rpg/QuestReward.h"
#include "rpg/RPG.h"
#include "rpg/RPGActor.h"
#include "rpg/RPGSaveSession.h"
#include "rpg/ShopCatalogue.h"
#include "rpg/ShopTransaction.h"
#include "rpg/Skill.h"
#include "rpg/StoryEvent.h"
#include "rpg/Tracker.h"
#include "rpg/WorldInteraction.h"
#include "rpg/WorldState.h"
#include <filesystem>
#include <fstream>
#include <sstream>
#include <string>
#include <vector>
namespace {
std::string readClassicContent(const std::filesystem::path& root, const char* name) {
std::ifstream input(root / name, std::ios::binary);
if (!input.is_open()) return {};
std::ostringstream source;
source << input.rdbuf();
return source.str();
}
const eve::dialogue::ConversationAsset* findConversation(const std::vector<eve::dialogue::ConversationAsset>& assets,
const std::string& id) {
for (const auto& asset : assets)
if (asset.id == id) return &asset;
return nullptr;
}
void acceptQuestThroughDialogue(const std::vector<eve::dialogue::ConversationAsset>& assets,
const std::string& conversationId, eve::rpg::Tracker& tracker,
const std::string& questId) {
const auto* conversation = findConversation(assets, conversationId);
REQUIRE(conversation != nullptr);
eve::dialogue::ConversationRunner runner;
std::string error;
REQUIRE(runner.start(conversation, eve::StateValue::object(), &error));
REQUIRE(runner.advance(&error));
REQUIRE_EQ(runner.currentNodeId(), std::string("decision"));
REQUIRE(runner.select("accept", &error));
REQUIRE(tracker.activate(questId));
}
void winEncounter(eve::rpg::Party& party, eve::rpg::RPGActor& hero, eve::rpg::RPGActor& companion,
eve::rpg::Tracker& tracker, eve::rpg::GameState& gameState, const std::string& mapId,
const std::string& objectId, const std::string& encounterId) {
eve::rpg::Battle battle;
auto composed = party.addToBattle(&battle, eve::rpg::BattleSide::Party);
REQUIRE(composed.ok());
REQUIRE_EQ(composed.value(), 2);
std::vector<eve::rpg::RPGActor*> enemies;
const int memberCount = eve::rpg::EncounterCatalogue::memberCount(encounterId);
REQUIRE(memberCount > 0);
for (int memberIndex = 0; memberIndex < memberCount; ++memberIndex) {
auto* enemy = eve::rpg::EncounterCatalogue::createMemberActor(encounterId, memberIndex);
REQUIRE(enemy != nullptr);
enemies.push_back(enemy);
battle.addActor(enemy, eve::rpg::BattleSide::Enemies);
}
while (!battle.isFinished()) {
if (!hero.isDead("hp")) REQUIRE(battle.setActionChecked(&hero, "", nullptr).ok());
if (!companion.isDead("hp"))
REQUIRE(eve::rpg::BattleTacticsCatalogue::queueAction(&battle, &companion, "companion.ranger").ok());
battle.autoEnemyActions();
battle.startRound();
while (battle.executeNextAction() && !battle.isFinished()) {
}
}
REQUIRE(battle.isVictory());
eve::rpg::RPG rpg;
auto settled = rpg.settleEncounterVictory(&hero, &tracker, &gameState, encounterId, mapId, objectId);
REQUIRE(settled.ok());
for (auto* enemy : enemies) enemy->release();
}
} // namespace
TEST_CASE("rpg.classic.playthroughCompletesBothQuestsAndRestoresCheckpoint") {
const std::filesystem::path contentRoot =
std::filesystem::path(__FILE__).parent_path().parent_path() / "examples" / "rpg-classic" / "data";
eve::rpg::QuestRegistry::clear();
eve::rpg::SkillRegistry::clear();
eve::rpg::EncounterCatalogue::clear();
eve::inventory::ItemRegistry::clear();
REQUIRE_EQ(eve::rpg::SkillRegistry::loadFromJson(readClassicContent(contentRoot, "skills.json")), 6);
auto quests = eve::rpg::QuestRegistry::replaceFromJsonStrict(readClassicContent(contentRoot, "quests.json"));
REQUIRE(quests.ok());
auto encounters =
eve::rpg::EncounterCatalogue::replaceFromJsonStrict(readClassicContent(contentRoot, "encounters.json"));
REQUIRE(encounters.ok());
REQUIRE_EQ(encounters.value(), 3);
auto tactics =
eve::rpg::BattleTacticsCatalogue::replaceFromJsonStrict(readClassicContent(contentRoot, "battle-tactics.json"));
REQUIRE(tactics.ok());
auto storyEvents =
eve::rpg::StoryEventCatalogue::replaceFromJsonStrict(readClassicContent(contentRoot, "story-events.json"));
REQUIRE(storyEvents.ok());
REQUIRE_EQ(storyEvents.value(), 1);
REQUIRE_EQ(eve::rpg::EncounterCatalogue::memberCount("slime.forest"), 2);
REQUIRE_EQ(eve::inventory::ItemRegistry::loadFromJson(readClassicContent(contentRoot, "items.json")), 3);
auto shopCatalogue = eve::rpg::ShopCatalogue::replaceFromJsonStrict(readClassicContent(contentRoot, "shop.json"));
REQUIRE(shopCatalogue.ok());
std::vector<eve::dialogue::ConversationAsset> conversations;
std::vector<eve::dialogue::ConversationDiagnostic> diagnostics;
REQUIRE(eve::dialogue::compileDnutConversations(readClassicContent(contentRoot, "village-dialogue.dnut"),
"village-dialogue.dnut", conversations, diagnostics));
REQUIRE(diagnostics.empty());
auto* localization = eve::i18n::I18n::create();
REQUIRE(localization != nullptr);
localization->clear();
auto localized = localization->replaceBundleFromJson(readClassicContent(contentRoot, "localization.json"));
REQUIRE(localized.ok());
for (const auto& conversation : conversations)
for (const auto& node : conversation.nodes)
if (!node.i18nKey.empty()) REQUIRE(localization->hasInLanguage("zh-CN", node.i18nKey));
eve::rpg::GameState gameState;
gameState.setVariable("gold", 0.0);
gameState.setVariable("kills", 0.0);
gameState.setVariable("statPoints", 0.0);
gameState.setVariable("save.mapCode", 0.0);
eve::rpg::WorldState world(gameState);
eve::rpg::Tracker tracker;
eve::rpg::RPGActor* hero = eve::rpg::RPGActor::createActor();
eve::rpg::RPGActor* companion = eve::rpg::RPGActor::createActor();
REQUIRE(hero != nullptr);
REQUIRE(companion != nullptr);
hero->setBaseAttribute("attack", 30.0);
hero->setBaseAttribute("defense", 5.0);
hero->setBaseAttribute("hp", 100.0);
hero->setBaseAttribute("speed", 10.0);
hero->setCurrent("hp", 100.0);
companion->setBaseAttribute("attack", 16.0);
companion->setBaseAttribute("defense", 8.0);
companion->setBaseAttribute("hp", 80.0);
companion->setBaseAttribute("speed", 12.0);
companion->setCurrent("hp", 80.0);
companion->learnSkill("skill.strike");
companion->learnSkill("skill.ranger_aid");
eve::rpg::Party party;
REQUIRE(party.addMember("hero", hero).ok());
REQUIRE(party.addMember("companion.ranger", companion).ok());
eve::inventory::Bag bag(12);
eve::inventory::EquipmentSet equipment;
equipment.setId("hero");
equipment.defineSlot("weapon");
equipment.defineSlot("armor");
eve::rpg::StoryEventSession arrival;
REQUIRE(arrival.begin("forest.arrival", &gameState).ok());
REQUIRE_EQ(arrival.getStepKind(), std::string("dialogue"));
REQUIRE(findConversation(conversations, arrival.getReference()) != nullptr);
REQUIRE(arrival.advance(&gameState).ok());
REQUIRE_EQ(arrival.getStepKind(), std::string("wait"));
eve::rpg::RPGSaveSession eventSave;
eventSave.bindParty(gameState, tracker, party, bag, equipment);
REQUIRE(eventSave.setContentVersion("rpg-classic.content.v9").ok());
auto eventCheckpoint = eventSave.snapshotJson();
REQUIRE(eventCheckpoint.ok());
REQUIRE(arrival.advance(&gameState).ok());
REQUIRE(eventSave.restoreSnapshotJson(eventCheckpoint.value()).ok());
eve::rpg::StoryEventSession resumedArrival;
REQUIRE(resumedArrival.begin("forest.arrival", &gameState).ok());
CHECK_EQ(resumedArrival.getStepIndex(), 1);
REQUIRE(resumedArrival.advance(&gameState).ok());
CHECK_EQ(resumedArrival.getStepKind(), std::string("message"));
REQUIRE(resumedArrival.advance(&gameState).ok());
CHECK(resumedArrival.isFinished());
acceptQuestThroughDialogue(conversations, "village.elder.offer", tracker, "quest.slayer");
winEncounter(party, *hero, *companion, tracker, gameState, "village", "slime_west", "slime.west");
winEncounter(party, *hero, *companion, tracker, gameState, "village", "slime_north", "slime.north");
gameState.setVariable("save.mapCode", 1.0);
winEncounter(party, *hero, *companion, tracker, gameState, "forest", "forest_slime", "slime.forest");
REQUIRE_EQ(tracker.getState("quest.slayer"), std::string("ready"));
auto mainRewards = eve::rpg::QuestReward::claim(tracker, gameState, bag, "quest.slayer");
REQUIRE(mainRewards.ok());
acceptQuestThroughDialogue(conversations, "forest.ranger.offer", tracker, "quest.ranger_cache");
eve::rpg::WorldLootRequest cache;
cache.mapId = "forest";
cache.objectId = "forest_cache";
cache.requiredQuestId = "quest.ranger_cache";
cache.itemId = "potion";
cache.itemQuantity = 1;
cache.attributeId = "gold";
cache.attributeAmount = 10.0;
cache.notifyTopic = "interact";
cache.notifyTarget = "forest_cache";
cache.notifyAmount = 1;
auto cacheLoot = eve::rpg::WorldInteraction::collectLoot(gameState, tracker, bag, cache);
REQUIRE(cacheLoot.ok());
REQUIRE_EQ(tracker.getState("quest.ranger_cache"), std::string("ready"));
auto sideRewards = eve::rpg::QuestReward::claim(tracker, gameState, bag, "quest.ranger_cache");
REQUIRE(sideRewards.ok());
auto boughtPotion = eve::rpg::ShopTransaction::buyOffer(gameState, bag, "gold", "potion", 1);
REQUIRE(boughtPotion.ok());
auto soldPotion = eve::rpg::ShopTransaction::sellOffer(gameState, bag, "gold", "potion", 1);
REQUIRE(soldPotion.ok());
REQUIRE_EQ(tracker.getState("quest.slayer"), std::string("completed"));
REQUIRE_EQ(tracker.getState("quest.ranger_cache"), std::string("completed"));
REQUIRE_EQ(gameState.getVariable("gold"), 115.0);
REQUIRE_EQ(gameState.getVariable("kills"), 3.0);
REQUIRE_EQ(gameState.getVariable("statPoints"), 3.0);
REQUIRE_EQ(bag.countItem("potion"), 2);
eve::rpg::RPGSaveSession save;
save.bindParty(gameState, tracker, party, bag, equipment);
REQUIRE(save.setContentVersion("rpg-classic.content.v9").ok());
auto checkpoint = save.snapshotJson();
REQUIRE(checkpoint.ok());
gameState.setVariable("gold", 0.0);
gameState.setVariable("kills", 0.0);
gameState.setVariable("statPoints", 0.0);
gameState.setVariable("save.mapCode", 0.0);
bag.clear();
hero->setCurrent("hp", 1.0);
companion->setCurrent("hp", 1.0);
REQUIRE(world.resetObject("forest", "forest_cache").ok());
REQUIRE(save.restoreSnapshotJson(checkpoint.value()).ok());
CHECK_EQ(gameState.getVariable("gold"), 115.0);
CHECK_EQ(gameState.getVariable("kills"), 3.0);
CHECK_EQ(gameState.getVariable("statPoints"), 3.0);
CHECK_EQ(gameState.getVariable("save.mapCode"), 1.0);
CHECK_EQ(tracker.getState("quest.slayer"), std::string("completed"));
CHECK_EQ(tracker.getState("quest.ranger_cache"), std::string("completed"));
CHECK_EQ(bag.countItem("potion"), 2);
CHECK_EQ(hero->getCurrent("hp"), 100.0);
CHECK(companion->getCurrent("hp") > 1.0);
CHECK(world.isObjectConsumed("village", "slime_west"));
CHECK(world.isObjectConsumed("village", "slime_north"));
CHECK(world.isObjectConsumed("forest", "forest_slime"));
CHECK(world.isObjectConsumed("forest", "forest_cache"));
hero->release();
companion->release();
eve::inventory::ItemRegistry::clear();
eve::rpg::QuestRegistry::clear();
eve::rpg::StoryEventCatalogue::clear();
}