#include "StarQuestManager.hpp" #include "StarRoot.hpp" #include "StarPlayer.hpp" #include "StarPlayerInventory.hpp" #include "StarAssets.hpp" #include "StarItemDatabase.hpp" #include "StarRandom.hpp" #include "StarJsonExtra.hpp" #include "StarTime.hpp" #include "StarClientContext.hpp" #include "StarUniverseClient.hpp" namespace Star { QuestManager::QuestManager(Player* player) { m_player = player; m_world = nullptr; m_trackOnWorldQuests = false; } QuestTemplatePtr getTemplate(String const& templateId) { return Root::singleton().questTemplateDatabase()->questTemplate(templateId); } StringMap readQuests(Json const& json) { auto versioningDatabase = Root::singleton().versioningDatabase(); auto questTemplateDatabase = Root::singleton().questTemplateDatabase(); auto validateArc = [questTemplateDatabase](QuestArcDescriptor const& arc) { for (auto quest : arc.quests) { if (!questTemplateDatabase->questTemplate(quest.templateId)) return false; } return true; }; StringMap result; for (auto const& questPair : json.iterateObject()) { // don't load the quest unless all quests in the arc exist Json diskStore = versioningDatabase->loadVersionedJson(VersionedJson::fromJson(questPair.second), "Quest"); auto questArc = QuestArcDescriptor::diskLoad(diskStore.get("arc")); if (validateArc(questArc)) result[questPair.first] = make_shared(questPair.second); } return result; } function questFilter(QuestState state) { return [state](QuestPtr const& quest) { return quest->state() == state; }; } void QuestManager::diskLoad(Json const& quests) { m_quests = readQuests(quests.get("quests", JsonObject{})); m_trackedQuestId = quests.optString("currentQuest"); } Json QuestManager::diskStore() { auto questPtrToJson = [](QuestPtr const& quest) { return quest->diskStore(); }; auto quests = m_quests.values(); return JsonObject{ {"quests", jsonFromMapV>(m_quests, questPtrToJson)}, {"currentQuest", jsonFromMaybe(m_trackedQuestId)} }; } void QuestManager::setUniverseClient(UniverseClient* client) { m_client = client; } void QuestManager::init(World* world) { m_world = world; serverQuests().exec([this, world](QuestPtr const& quest) { quest->init(m_player, world, m_client); }); m_trackOnWorldQuests = true; // untrack tracked quest if it's not cross-server, and we're on a different server if (auto tracked = m_trackedQuestId) { if (auto maybeQuest = m_quests.maybe(*tracked)) { auto quest = *maybeQuest; if (quest->hideCrossServer() && quest->serverUuid() && *quest->serverUuid() != m_player->clientContext()->serverUuid()) m_trackedQuestId = {}; } } } void QuestManager::uninit() { m_quests.values().exec([](QuestPtr const& quest) { quest->uninit(); }); m_world = nullptr; } bool QuestManager::canStart(QuestArcDescriptor const& questArc) const { if (questArc.quests.size() == 0) return false; for (auto questDesc : questArc.quests) { auto questTemplate = getTemplate(questDesc.templateId); if (!questTemplate) return false; if (auto quest = m_quests.maybe(questDesc.questId)) if ((*quest)->state() != QuestState::Failed) return false; for (auto const& prerequisiteQuestId : questTemplate->prerequisiteQuests) { if (!hasCompleted(prerequisiteQuestId)) return false; } for (auto item : questTemplate->requiredItems) if (!m_player->inventory()->hasItem(item)) return false; if (questTemplate->requiredShipLevel) { if (m_player->clientContext()->shipUpgrades().shipLevel < *questTemplate->requiredShipLevel) return false; } } return true; } void QuestManager::offer(QuestPtr const& quest) { m_quests[quest->questId()] = quest; quest->init(m_player, m_world, m_client); quest->offer(); } QuestPtr QuestManager::getQuest(String const& questId) const { return m_quests.get(questId); } bool QuestManager::hasQuest(String const& questId) const { return m_quests.contains(questId); } bool QuestManager::hasAcceptedQuest(String const& questId) const { return m_quests.contains(questId) && m_quests.get(questId)->state() != QuestState::New && m_quests.get(questId)->state() != QuestState::Offer; } bool QuestManager::isActive(String const& questId) const { return m_quests.contains(questId) && m_quests.get(questId)->state() == QuestState::Active; } bool QuestManager::isCurrent(String const& questId) const { return m_onWorldQuestId.orMaybe(m_trackedQuestId) == questId; } bool QuestManager::isTracked(String const& questId) const { return m_trackedQuestId == questId; } void QuestManager::setAsTracked(Maybe const& questId) { if (questId && isActive(*questId)) { m_trackedQuestId = questId; if (m_onWorldQuestId) { // stop auto tracking quests on this world m_onWorldQuestId = {}; m_trackOnWorldQuests = false; } } else { m_trackedQuestId = {}; m_trackOnWorldQuests = true; } } void QuestManager::markAsRead(String const& questId) { if (!m_quests.contains(questId)) return; getQuest(questId)->markAsRead(); } bool QuestManager::hasCompleted(String const& questId) const { if (auto quest = m_quests.maybe(questId)) if ((*quest)->state() == QuestState::Complete) return true; return false; } bool QuestManager::canTurnIn(String const& questId) const { if (auto quest = m_quests.maybe(questId)) if ((*quest)->state() == QuestState::Active && (*quest)->canTurnIn()) return true; return false; } Maybe QuestManager::getFirstNewQuest() { for (auto& q : serverQuests()) { if (q->state() == QuestState::Offer) return q; } return {}; } Maybe QuestManager::getFirstCompletableQuest() { for (auto& q : serverQuests()) { if (q->state() == QuestState::Complete && q->showDialog()) return q; } return {}; } Maybe QuestManager::getFirstFailableQuest() { for (auto& q : serverQuests()) { if (q->state() == QuestState::Failed&& q->showDialog()) return q; } return {}; } Maybe QuestManager::getFirstMainQuest() { for (auto& q : serverQuests()) { if (q->state() == QuestState::Active && q->mainQuest()) return q; } return {}; } void sortQuests(List& quests) { std::sort(quests.begin(), quests.end(), [](QuestPtr const& left, QuestPtr const& right) -> bool { int64_t leftUpdated = left->lastUpdatedOn(); int64_t rightUpdated = right->lastUpdatedOn(); String leftQuestId = left->templateId(); String rightQuestId = right->templateId(); return std::tie(leftUpdated, leftQuestId) < std::tie(rightUpdated, rightQuestId); }); } List QuestManager::listActiveQuests() const { List result = serverQuests(); result.filter([&](QuestPtr quest) { return quest->state() == QuestState::Active && quest->showInLog(); }); sortQuests(result); return result; } List QuestManager::listCompletedQuests() const { List result = serverQuests(); result.filter([](QuestPtr quest) { return quest->state() == QuestState::Complete && quest->showInLog(); }); sortQuests(result); return result; } List QuestManager::listFailedQuests() const { List result = serverQuests(); result.filter([](QuestPtr quest) { return quest->state() == QuestState::Failed && quest->showInLog(); }); sortQuests(result); return result; } Maybe QuestManager::currentQuest() const { auto questId = m_onWorldQuestId.orMaybe(m_trackedQuestId); if (questId && isActive(*questId)) { auto current = getQuest(*questId); if (current->showInLog()) return current; } return {}; } Maybe QuestManager::trackedQuest() const { if (m_trackedQuestId && isActive(*m_trackedQuestId)) { auto current = getQuest(*m_trackedQuestId); if (current->showInLog()) return current; } return {}; } Maybe QuestManager::getQuestIndicator(EntityPtr const& entity) const { Maybe indicatorType; Vec2F indicatorPos = entity->position() + Vec2F(0, 2.75); auto questGiver = as(entity); if (questGiver) { if (!indicatorType) { for (auto& questId : questGiver->turnInQuests()) { if (!isActive(questId)) continue; auto quest = getQuest(questId); if (quest->canTurnIn()) { indicatorType = quest->questReceiverIndicator(); break; } } } if (!indicatorType) { auto questTemplateDatabase = Root::singleton().questTemplateDatabase(); for (auto& questArc : questGiver->offeredQuests()) { if (canStart(questArc) && questArc.quests.size() > 0) { auto& questDesc = questArc.quests[0]; auto questTemplate = questTemplateDatabase->questTemplate(questDesc.templateId); indicatorType = questTemplate->questGiverIndicator; break; } } } } if (indicatorType) { Json indicators = Root::singleton().assets()->json("/quests/quests.config:indicators"); String indicatorImage = indicators.get(*indicatorType).getString("image"); if (questGiver) indicatorPos = questGiver->questIndicatorPosition(); return QuestIndicator{indicatorImage, indicatorPos}; } for (auto& pair : m_quests) { if (pair.second->state() == QuestState::Active) { if (auto indicatorImage = pair.second->customIndicator(entity)) { if (questGiver) indicatorPos = questGiver->questIndicatorPosition(); return QuestIndicator{ *indicatorImage, indicatorPos }; } } } return {}; } StringSet QuestManager::interestingObjects() { StringSet result; m_quests.values().exec([&result](QuestPtr const& quest) { if (auto questObjects = quest->receiveMessage("interestingObjects", true, JsonArray())) result.addAll(jsonToStringSet(*questObjects)); }); return result; } Maybe QuestManager::receiveMessage(String const& message, bool localMessage, JsonArray const& args) { starAssert(m_world); Maybe result; m_quests.values().exec([&result, message, localMessage, args]( QuestPtr const& quest) { result = result.orMaybe(quest->receiveMessage(message, localMessage, args)); }); return result; } void QuestManager::update(float dt) { startInitialQuests(); if (m_trackedQuestId && !isActive(*m_trackedQuestId)) m_trackedQuestId = {}; if (m_onWorldQuestId) { auto active = isActive(*m_onWorldQuestId); if (active) { auto worldId = getQuest(*m_onWorldQuestId)->worldId(); if (!worldId || m_player->clientContext()->playerWorldId() != *worldId) active = false; } if (!active) m_onWorldQuestId = {}; } else if (m_trackOnWorldQuests) { auto playerWorldId = m_client->clientContext()->playerWorldId(); auto trackedWorld = currentQuest().sequence([](QuestPtr q) { return q->worldId(); }); if (!trackedWorld || playerWorldId != *trackedWorld) { // the currently tracked quest is not on this world, track another quest on this world for (auto quest : listActiveQuests()) { if (auto questWorld = quest->worldId()) { if (playerWorldId == *questWorld) m_onWorldQuestId = quest->questId(); } } } } for (auto& entry : m_quests) { auto quest = entry.second; QuestState state = quest->state(); bool finished = state == QuestState::Complete || state == QuestState::Failed; if (state == QuestState::New || (finished && quest->ephemeral() && !quest->showDialog())) { quest->uninit(); m_quests.remove(entry.first); } } serverQuests().exec([dt](QuestPtr const& quest) { quest->update(dt); }); } List QuestManager::serverQuests() const { return m_quests.values().filtered([this](QuestPtr const& q) -> bool { if (q->hideCrossServer() && q->serverUuid().isValid() && *q->serverUuid() != m_player->clientContext()->serverUuid()) return false; return true; }); } void QuestManager::startInitialQuests() { auto startingQuests = Root::singleton().assets()->json(strf("/quests/quests.config:initialquests.{}", m_player->species())).toArray(); for (auto const& questArcJson : startingQuests) { QuestArcDescriptor quest = QuestArcDescriptor::fromJson(questArcJson); if (canStart(quest)) offer(make_shared(quest, 0, m_player)); } } void QuestManager::setMostRecentQuestCurrent() { List sortedActiveQuests = listActiveQuests(); if (sortedActiveQuests.size() > 0) setAsTracked(sortedActiveQuests.last()->questId()); } }