2023-06-20 04:33:09 +00:00
|
|
|
#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<QuestPtr> 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<QuestPtr> 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<Quest>(questPair.second);
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
function<bool (QuestPtr const&)> 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<StringMap<QuestPtr>>(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<String> 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<QuestPtr> QuestManager::getFirstNewQuest() {
|
|
|
|
for (auto& q : serverQuests()) {
|
|
|
|
if (q->state() == QuestState::Offer)
|
|
|
|
return q;
|
|
|
|
}
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
Maybe<QuestPtr> QuestManager::getFirstCompletableQuest() {
|
|
|
|
for (auto& q : serverQuests()) {
|
|
|
|
if (q->state() == QuestState::Complete && q->showDialog())
|
|
|
|
return q;
|
|
|
|
}
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
Maybe<QuestPtr> QuestManager::getFirstFailableQuest() {
|
|
|
|
for (auto& q : serverQuests()) {
|
|
|
|
if (q->state() == QuestState::Failed&& q->showDialog())
|
|
|
|
return q;
|
|
|
|
}
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
Maybe<QuestPtr> QuestManager::getFirstMainQuest() {
|
|
|
|
for (auto& q : serverQuests()) {
|
|
|
|
if (q->state() == QuestState::Active && q->mainQuest())
|
|
|
|
return q;
|
|
|
|
}
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
void sortQuests(List<QuestPtr>& 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<QuestPtr> QuestManager::listActiveQuests() const {
|
|
|
|
List<QuestPtr> result = serverQuests();
|
|
|
|
result.filter([&](QuestPtr quest) {
|
|
|
|
return quest->state() == QuestState::Active && quest->showInLog();
|
|
|
|
});
|
|
|
|
sortQuests(result);
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
List<QuestPtr> QuestManager::listCompletedQuests() const {
|
|
|
|
List<QuestPtr> result = serverQuests();
|
|
|
|
result.filter([](QuestPtr quest) {
|
|
|
|
return quest->state() == QuestState::Complete && quest->showInLog();
|
|
|
|
});
|
|
|
|
sortQuests(result);
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
List<QuestPtr> QuestManager::listFailedQuests() const {
|
|
|
|
List<QuestPtr> result = serverQuests();
|
|
|
|
result.filter([](QuestPtr quest) {
|
|
|
|
return quest->state() == QuestState::Failed && quest->showInLog();
|
|
|
|
});
|
|
|
|
sortQuests(result);
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
Maybe<QuestPtr> 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<QuestPtr> QuestManager::trackedQuest() const {
|
|
|
|
if (m_trackedQuestId && isActive(*m_trackedQuestId)) {
|
|
|
|
auto current = getQuest(*m_trackedQuestId);
|
|
|
|
if (current->showInLog())
|
|
|
|
return current;
|
|
|
|
}
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
Maybe<QuestIndicator> QuestManager::getQuestIndicator(EntityPtr const& entity) const {
|
|
|
|
Maybe<String> indicatorType;
|
|
|
|
Vec2F indicatorPos = entity->position() + Vec2F(0, 2.75);
|
|
|
|
|
|
|
|
if (auto questGiver = as<InteractiveEntity>(entity)) {
|
|
|
|
indicatorPos = questGiver->questIndicatorPosition();
|
|
|
|
|
|
|
|
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");
|
|
|
|
return QuestIndicator{indicatorImage, indicatorPos};
|
|
|
|
}
|
|
|
|
|
|
|
|
for (auto& pair : m_quests) {
|
|
|
|
if (pair.second->state() == QuestState::Active) {
|
|
|
|
if (auto indicatorImage = pair.second->customIndicator(entity))
|
|
|
|
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<Json> QuestManager::receiveMessage(String const& message, bool localMessage, JsonArray const& args) {
|
|
|
|
starAssert(m_world);
|
|
|
|
Maybe<Json> 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() {
|
|
|
|
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([](QuestPtr const& quest) { quest->update(); });
|
|
|
|
}
|
|
|
|
|
|
|
|
List<QuestPtr> 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 =
|
2023-06-27 10:23:44 +00:00
|
|
|
Root::singleton().assets()->json(strf("/quests/quests.config:initialquests.{}", m_player->species())).toArray();
|
2023-06-20 04:33:09 +00:00
|
|
|
for (auto const& questArcJson : startingQuests) {
|
|
|
|
QuestArcDescriptor quest = QuestArcDescriptor::fromJson(questArcJson);
|
|
|
|
if (canStart(quest))
|
|
|
|
offer(make_shared<Quest>(quest, 0, m_player));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void QuestManager::setMostRecentQuestCurrent() {
|
|
|
|
List<QuestPtr> sortedActiveQuests = listActiveQuests();
|
|
|
|
if (sortedActiveQuests.size() > 0)
|
|
|
|
setAsTracked(sortedActiveQuests.last()->questId());
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|