using Core.Annotations; using Core.Models.Eft.Common; using Core.Models.Eft.Common.Tables; using Core.Models.Eft.ItemEvent; using Core.Models.Eft.Quests; using Core.Models.Enums; using Core.Models.Spt.Config; using Core.Models.Utils; using Core.Routers; using Core.Servers; using Core.Services; using Core.Utils; using Core.Utils.Cloners; using Product = Core.Models.Eft.ItemEvent.Product; namespace Core.Helpers; [Injectable] public class QuestHelper { protected ISptLogger _logger; protected TimeUtil _timeUtil; protected HashUtil _hashUtil; protected ItemHelper _itemHelper; protected DatabaseService _databaseService; protected QuestConditionHelper _questConditionHelper; protected EventOutputHolder _eventOutputHolder; protected LocaleService _localeService; protected ProfileHelper _profileHelper; protected QuestRewardHelper _questRewardHelper; protected LocalisationService _localisationService; protected SeasonalEventService _seasonalEventService; protected TraderHelper _traderHelper; protected MailSendService _mailSendService; protected PlayerService _playerService; protected ConfigServer _configServer; protected ICloner _cloner; protected QuestConfig _questConfig; public QuestHelper ( ISptLogger logger, TimeUtil timeUtil, HashUtil hashUtil, ItemHelper itemHelper, DatabaseService databaseService, QuestConditionHelper questConditionHelper, EventOutputHolder eventOutputHolder, LocaleService localeService, ProfileHelper profileHelper, QuestRewardHelper questRewardHelper, LocalisationService localisationService, SeasonalEventService seasonalEventService, TraderHelper traderHelper, MailSendService mailSendService, PlayerService playerService, ConfigServer configServer, ICloner Cloner ) { _logger = logger; _timeUtil = timeUtil; _hashUtil = hashUtil; _itemHelper = itemHelper; _databaseService = databaseService; _questConditionHelper = questConditionHelper; _eventOutputHolder = eventOutputHolder; _localeService = localeService; _profileHelper = profileHelper; _questRewardHelper = questRewardHelper; _localisationService = localisationService; _seasonalEventService = seasonalEventService; _traderHelper = traderHelper; _mailSendService = mailSendService; _playerService = playerService; _configServer = configServer; _cloner = Cloner; _questConfig = configServer.GetConfig(); } /// /// Get status of a quest in player profile by its id /// /// Profile to search /// Quest id to look up /// QuestStatus enum public QuestStatusEnum GetQuestStatus(PmcData pmcData, string questId) { var quest = pmcData.Quests?.FirstOrDefault((q) => q.QId == questId); return quest?.Status ?? QuestStatusEnum.Locked; } /// /// returns true if the level condition is satisfied /// /// Players level /// Quest condition /// true if player level is greater than or equal to quest public bool DoesPlayerLevelFulfilCondition(double playerLevel, QuestCondition condition) { if (condition.ConditionType != "Level") { return true; } var conditionValue = double.Parse(condition.Value.ToString()); switch (condition.CompareMethod) { case ">=": return playerLevel >= conditionValue; case ">": return playerLevel > conditionValue; case "<": return playerLevel < conditionValue; case "<=": return playerLevel <= conditionValue; case "=": return playerLevel == conditionValue; default: _logger.Error( _localisationService.GetText("quest-unable_to_find_compare_condition", condition.CompareMethod) ); return false; } } /// /// Get the quests found in both lists (inner join) /// /// List of quests #1 /// List of quests #2 /// Reduction of cartesian product between two quest lists public List GetDeltaQuests(List before, List after) { throw new System.NotImplementedException(); } /// /// Adjust skill experience for low skill levels, mimicking the official client /// /// the skill experience is being added to /// the amount of experience being added to the skill /// the adjusted skill progress gain public int AdjustSkillExpForLowLevels(Common profileSkill, int progressAmount) { throw new System.NotImplementedException(); } /// /// Get quest name by quest id /// /// id to get /// public string GetQuestNameFromLocale(string questId) { var questNameKey = $"{questId} name"; return _localeService.GetLocaleDb().GetValueOrDefault(questNameKey, "UNKNOWN"); } /// /// Check if trader has sufficient loyalty to fulfill quest requirement /// /// Quest props /// Player profile /// true if loyalty is high enough to fulfill quest requirement public bool TraderLoyaltyLevelRequirementCheck(QuestCondition questProperties, PmcData profile) { var requiredLoyaltyLevel = questProperties.Value as float?; if (!profile.TradersInfo.TryGetValue(questProperties.Target as string, out var trader)) { _logger.Error( _localisationService.GetText("quest-unable_to_find_trader_in_profile", questProperties.Target) ); } return CompareAvailableForValues(trader.LoyaltyLevel.Value, requiredLoyaltyLevel.Value, questProperties.CompareMethod); } /// /// Check if trader has sufficient standing to fulfill quest requirement /// /// Quest props /// Player profile /// true if standing is high enough to fulfill quest requirement public bool TraderStandingRequirementCheck(QuestCondition questProperties, PmcData profile) { var requiredLoyaltyLevel = questProperties.Value as float?; if (!profile.TradersInfo.TryGetValue(questProperties.Target as string, out var trader)) { _logger.Error( _localisationService.GetText("quest-unable_to_find_trader_in_profile", questProperties.Target) ); } return CompareAvailableForValues(trader.Standing.Value, requiredLoyaltyLevel.Value, questProperties.CompareMethod); } protected bool CompareAvailableForValues(double current, float required, string compareMethod) { switch (compareMethod) { case ">=": return current >= required; case ">": return current > required; case "<=": return current <= required; case "<": return current < required; case "!=": return current != required; case "==": return current == required; default: _logger.Error(_localisationService.GetText("quest-compare_operator_unhandled", compareMethod)); return false; } } /** * Look up quest in db by accepted quest id and construct a profile-ready object ready to store in profile * @param pmcData Player profile * @param newState State the new quest should be in when returned * @param acceptedQuest Details of accepted quest from client */ public QuestStatus GetQuestReadyForProfile( PmcData pmcData, QuestStatus newState, AcceptQuestRequestData acceptedQuest ) { throw new NotImplementedException(); } /** * Get quests that can be shown to player after starting a quest * @param startedQuestId Quest started by player * @param sessionID Session id * @returns Quests accessible to player including newly unlocked quests now quest (startedQuestId) was started */ public List GetNewlyAccessibleQuestsWhenStartingQuest(string startedQuestId, string sessionID) { throw new NotImplementedException(); } /** * Should a seasonal/event quest be shown to the player * @param questId Quest to check * @returns true = show to player */ public bool ShowEventQuestToPlayer(string questId) { var isChristmasEventActive = _seasonalEventService.ChristmasEventEnabled(); var isHalloweenEventActive = _seasonalEventService.HalloweenEventEnabled(); // Not christmas + quest is for christmas if (!isChristmasEventActive && _seasonalEventService.IsQuestRelatedToEvent(questId, SeasonalEventType.Christmas)) { return false; } // Not halloween + quest is for halloween if (!isHalloweenEventActive && _seasonalEventService.IsQuestRelatedToEvent(questId, SeasonalEventType.Halloween)) { return false; } // Should non-season event quests be shown to player if (!_questConfig.ShowNonSeasonalEventQuests ?? false && _seasonalEventService.IsQuestRelatedToEvent(questId, SeasonalEventType.None)) { return false; } return true; } /** * Is the quest for the opposite side the player is on * @param playerSide Player side (usec/bear) * @param questId QuestId to check */ public bool QuestIsForOtherSide(string playerSide, string questId) { var isUsec = playerSide.ToLower() == "usec"; if (isUsec && _questConfig.BearOnlyQuests.Contains(questId)) { // Player is usec and quest is bear only, skip return true; } if (!isUsec && _questConfig.UsecOnlyQuests.Contains(questId)) { // Player is bear and quest is usec only, skip return true; } return false; } /** * Is the provided quest prevented from being viewed by the provided game version * (Inclusive filter) * @param gameVersion Game version to check against * @param questId Quest id to check * @returns True Quest should not be visible to game version */ protected bool QuestIsProfileBlacklisted(string gameVersion, string questId) { var questBlacklist = _questConfig.ProfileBlacklist[gameVersion]; if (questBlacklist is null) { // Not blacklisted return false; } return questBlacklist.Contains(questId); } /** * Is the provided quest able to be seen by the provided game version * (Exclusive filter) * @param gameVersion Game version to check against * @param questId Quest id to check * @returns True Quest should be visible to game version */ protected bool QuestIsProfileWhitelisted(string gameVersion, string questId) { var questBlacklist = _questConfig.ProfileBlacklist.GetValueOrDefault(gameVersion); if (questBlacklist is null) { // Not blacklisted return false; } return questBlacklist.Contains(questId); } /** * Get quests that can be shown to player after failing a quest * @param failedQuestId Id of the quest failed by player * @param sessionId Session id * @returns List of Quest */ public List FailedUnlocked(string failedQuestId, string sessionId) { var profile = _profileHelper.GetPmcProfile(sessionId); var profileQuest = profile.Quests.FirstOrDefault((x) => x.QId == failedQuestId); var quests = GetQuestsFromDb() .Where( (q) => { var acceptedQuestCondition = q.Conditions.AvailableForStart.FirstOrDefault( (c) => { return (c.ConditionType == "Quest" && ((List)c.Target).Contains(failedQuestId) && c.Status[0] == QuestStatusEnum.Fail ); } ); if (acceptedQuestCondition is null) { return false; } return profileQuest is not null && profileQuest.Status == QuestStatusEnum.Fail; } ) .ToList(); if (quests.Any()) { return quests; } return GetQuestsWithOnlyLevelRequirementStartCondition(quests); } /** * Sets the item stack to new value, or delete the item if value <= 0 * // TODO maybe merge this function and the one from customization * @param pmcData Profile * @param itemId id of item to adjust stack size of * @param newStackSize Stack size to adjust to * @param sessionID Session id * @param output ItemEvent router response */ public void ChangeItemStack( PmcData pmcData, string itemId, double newStackSize, string sessionID, ItemEventRouterResponse output) { var inventoryItemIndex = pmcData.Inventory.Items.FindIndex((item) => item.Id == itemId); if (inventoryItemIndex < 0) { _logger.Error(_localisationService.GetText("quest-item_not_found_in_inventory", itemId)); return; } if (newStackSize > 0) { var item = pmcData.Inventory.Items[inventoryItemIndex]; _itemHelper.AddUpdObjectToItem(item); item.Upd.StackObjectsCount = newStackSize; AddItemStackSizeChangeIntoEventResponse(output, sessionID, item); } else { // this case is probably dead Code right now, since the only calling function // checks explicitly for Value > 0. output.ProfileChanges[sessionID].Items.DeletedItems.Add(new() { Id = itemId }); pmcData.Inventory.Items.RemoveAt(inventoryItemIndex); } } /** * Add item stack change object into output route event response * @param output Response to add item change event into * @param sessionId Session id * @param item Item that was adjusted */ protected void AddItemStackSizeChangeIntoEventResponse( ItemEventRouterResponse output, string sessionId, Item item) { output.ProfileChanges[sessionId] .Items.ChangedItems.Add( new Product { Id = item.Id, Template = item.Template, ParentId = item.ParentId, SlotId = item.SlotId, Location = (ItemLocation)item.Location, Upd = new Upd { StackObjectsCount = item.Upd.StackObjectsCount }, } ); } /** * Get quests, strip all requirement conditions except level * @param quests quests to process * @returns quest list without conditions */ protected List GetQuestsWithOnlyLevelRequirementStartCondition(List quests) { return quests.Select(GetQuestWithOnlyLevelRequirementStartCondition).ToList(); } /** * Remove all quest conditions except for level requirement * @param quest quest to clean * @returns reset Quest object */ public Quest GetQuestWithOnlyLevelRequirementStartCondition(Quest quest) { var updatedQuest = _cloner.Clone(quest); updatedQuest.Conditions.AvailableForStart = updatedQuest.Conditions.AvailableForStart.Where( (q) => q.ConditionType == "Level" ) .ToList(); return updatedQuest; } /** * Fail a quest in a player profile * @param pmcData Player profile * @param failRequest Fail quest request data * @param sessionID Session id * @param output Client output */ public void FailQuest( PmcData pmcData, FailQuestRequestData failRequest, string sessionID, ItemEventRouterResponse output = null) { var updatedOutput = output; // Prepare response to send back to client if (updatedOutput is null) { updatedOutput = _eventOutputHolder.GetOutput(sessionID); } UpdateQuestState(pmcData, QuestStatusEnum.Fail, failRequest.QuestId); var questRewards = _questRewardHelper.ApplyQuestReward( pmcData, failRequest.QuestId, QuestStatusEnum.Fail, sessionID, updatedOutput ); // Create a dialog message for completing the quest. var quest = GetQuestFromDb(failRequest.QuestId, pmcData); // Merge all daily/weekly/scav daily quests into one array and look for the matching quest by id var matchingRepeatableQuest = pmcData.RepeatableQuests.SelectMany( (repeatableType) => repeatableType.ActiveQuests ) .FirstOrDefault((activeQuest) => activeQuest.Id == failRequest.QuestId); // Quest found and no repeatable found if (quest is not null && matchingRepeatableQuest is null) { if (quest.FailMessageText.Trim().Count() > 0) { _mailSendService.SendLocalisedNpcMessageToPlayer( sessionID, _traderHelper.GetTraderById(quest?.TraderId ?? matchingRepeatableQuest?.TraderId) .ToString(), // Can be undefined when repeatable quest has been moved to inactiveQuests MessageType.QUEST_FAIL, quest.FailMessageText, questRewards.ToList(), _timeUtil.GetHoursAsSeconds((int)GetMailItemRedeemTimeHoursForProfile(pmcData)) ); } } updatedOutput.ProfileChanges[sessionID].Quests.AddRange(FailedUnlocked(failRequest.QuestId, sessionID)); } /** * Get List of All Quests from db * NOT CLONED * @returns List of Quest objects */ public List GetQuestsFromDb() { return _databaseService.GetQuests().Values.ToList(); } /** * Get quest by id from database (repeatables are stored in profile, check there if questId not found) * @param questId Id of quest to find * @param pmcData Player profile * @returns IQuest object */ public Quest GetQuestFromDb(string questId, PmcData pmcData) { // May be a repeatable quest var quest = _databaseService.GetQuests()[questId]; if (quest == null) { // Check daily/weekly objects foreach (var repeatableQuest in pmcData.RepeatableQuests) { quest = repeatableQuest.ActiveQuests.FirstOrDefault(r => r.Id == questId); if (quest != null) break; } } return quest; } /// /// Get a quests startedMessageText key from db, if no startedMessageText key found, use description key instead /// /// startedMessageText property from Quest /// description property from Quest /// message id public string GetMessageIdForQuestStart(string startedMessageTextId, string questDescriptionId) { // Blank or is a guid, use description instead var startedMessageText = GetQuestLocaleIdFromDb(startedMessageTextId); if ( startedMessageText is null || startedMessageText.Trim() == "" || startedMessageText.ToLower() == "test" || startedMessageText.Length == 24 ) { return questDescriptionId; } return startedMessageTextId; } /// /// Get the locale Id from locale db for a quest message /// /// Quest message id to look up /// Locale Id from locale db public string GetQuestLocaleIdFromDb(string questMessageId) { var locale = _localeService.GetLocaleDb(); return locale.GetValueOrDefault(questMessageId, null); } /// /// Alter a quests state + Add a record to its status timers object /// /// Profile to update /// New state the quest should be in /// Id of the quest to alter the status of public void UpdateQuestState(PmcData pmcData, QuestStatusEnum newQuestState, string questId) { // Find quest in profile, update status to desired status var questToUpdate = pmcData.Quests.FirstOrDefault((quest) => quest.QId == questId); if (questToUpdate is not null) { questToUpdate.Status = newQuestState; questToUpdate.StatusTimers[newQuestState] = _timeUtil.GetTimeStamp(); } } /// /// Resets a quests values back to its chosen state /// /// Profile to update /// New state the quest should be in /// Id of the quest to alter the status of public void ResetQuestState(PmcData pmcData, QuestStatusEnum newQuestState, string questId) { var questToUpdate = pmcData.Quests.FirstOrDefault((quest) => quest.QId == questId); if (questToUpdate is not null) { var currentTimestamp = _timeUtil.GetTimeStamp(); questToUpdate.Status = newQuestState; // Only set start time when quest is being started if (newQuestState == QuestStatusEnum.Started) { questToUpdate.StartTime = currentTimestamp; } questToUpdate.StatusTimers[newQuestState] = currentTimestamp; // Delete all status timers after applying new status foreach (var statusKey in questToUpdate.StatusTimers) { if (statusKey.Key > newQuestState) { questToUpdate.StatusTimers.Remove(statusKey.Key); } } // Remove all completed conditions questToUpdate.CompletedConditions = []; } } /** * Find quest with 'findItem' condition that needs the item tpl be handed in * @param itemTpl item tpl to look for * @param questIds Quests to search through for the findItem condition * @returns quest id with 'FindItem' condition id */ public Dictionary GetFindItemConditionByQuestItem( string itemTpl, string[] questIds, List allQuests ) { Dictionary result = new(); foreach (var questId in questIds) { var questInDb = allQuests.FirstOrDefault((x) => x.Id == questId); if (questInDb is null) { _logger.Debug($"Unable to find quest: {questId} in db, cannot get 'FindItem' condition, skipping"); continue; } var condition = questInDb.Conditions.AvailableForFinish.FirstOrDefault( (c) => c.ConditionType == "FindItem" && (((List)c?.Target)?.Contains(itemTpl) ?? false) ); if (condition is not null) { result[questId] = condition.Id; break; } } return result; } /** * Add all quests to a profile with the provided statuses * @param pmcProfile profile to update * @param statuses statuses quests should have added to profile */ public void AddAllQuestsToProfile(PmcData pmcProfile, List statuses) { // Iterate over all quests in db var quests = _databaseService.GetQuests(); foreach (var (key, questData) in quests) { // Quest from db matches quests in profile, skip if (pmcProfile.Quests.Any((x) => x.QId == questData.Id)) { continue; } // Create dict of status to add to quest in profile var statusesDict = new Dictionary(); foreach (var status in statuses) { statusesDict.Add(status, _timeUtil.GetTimeStamp()); } var questRecordToAdd = new QuestStatus { QId = key, StartTime = _timeUtil.GetTimeStamp(), Status = statuses[^1], // Get last status in list as currently active status StatusTimers = statusesDict, CompletedConditions = [], AvailableAfter = 0, }; // Check if the quest already exists in the profile var existingQuest = pmcProfile.Quests.FirstOrDefault(x => x.QId == key); if (existingQuest != null) { // Update existing quest existingQuest.Status = questRecordToAdd.Status; existingQuest.StatusTimers = questRecordToAdd.StatusTimers; } else { // Add new quest to the profile pmcProfile.Quests.Add(questRecordToAdd); } } } public void FindAndRemoveQuestFromArrayIfExists(string questId, List quests) { var pmcQuestToReplaceStatus = quests.FirstOrDefault((quest) => quest.QId == questId); if (pmcQuestToReplaceStatus is not null) { var index = quests.IndexOf(pmcQuestToReplaceStatus); quests.RemoveAt(index); } } /** * Return a list of quests that would fail when supplied quest is completed * @param completedQuestId quest completed id * @returns array of Quest objects */ public List GetQuestsFailedByCompletingQuest(string completedQuestId) { var questsInDb = GetQuestsFromDb(); return questsInDb.Where((quest) => { // No fail conditions, exit early if (quest.Conditions.Fail is null || quest.Conditions.Fail.Count == 0) { return false; } return quest.Conditions.Fail.Any((condition) => (((List)condition.Target)?.Contains(completedQuestId)) ?? false); }).ToList(); } /** * Get the hours a mails items can be collected for by profile type * @param pmcData Profile to get hours for * @returns Hours item will be available for */ public double? GetMailItemRedeemTimeHoursForProfile(PmcData pmcData) { var value = _questConfig.MailRedeemTimeHours.GetValueOrDefault(pmcData.Info.GameVersion); if (value is null) { return 0; } return value; } public ItemEventRouterResponse CompleteQuest(PmcData pmcData, CompleteQuestRequestData body, string sessionID) { var completeQuestResponse = _eventOutputHolder.GetOutput(sessionID); var completedQuest = GetQuestFromDb(body.QuestId, pmcData); var preCompleteProfileQuests = _cloner.Clone(pmcData.Quests); var completedQuestId = body.QuestId; var clientQuestsClone = _cloner.Clone(GetClientQuests(sessionID)); // Must be gathered prior to applyQuestReward() & failQuests() var newQuestState = QuestStatusEnum.Success; UpdateQuestState(pmcData, newQuestState, completedQuestId); var questRewards = _questRewardHelper.ApplyQuestReward( pmcData, body.QuestId, newQuestState, sessionID, completeQuestResponse ); // Check for linked failed + unrestartable quests (only get quests not already failed var questsToFail = GetQuestsFromProfileFailedByCompletingQuest(completedQuestId, pmcData); if (questsToFail?.Count > 0) { FailQuests(sessionID, pmcData, questsToFail, completeQuestResponse); } // Show modal on player screen SendSuccessDialogMessageOnQuestComplete(sessionID, pmcData, completedQuestId, questRewards.ToList()); // Add diff of quests before completion vs after for client response var questDelta = GetDeltaQuests(clientQuestsClone, GetClientQuests(sessionID)); // Check newly available + failed quests for timegates and add them to profile AddTimeLockedQuestsToProfile(pmcData, questDelta, body.QuestId); // Inform client of quest changes completeQuestResponse.ProfileChanges[sessionID].Quests.AddRange(questDelta); // Check if it's a repeatable quest. If so, remove from Quests foreach (var currentRepeatable in pmcData.RepeatableQuests) { var repeatableQuest = currentRepeatable.ActiveQuests.FirstOrDefault( (activeRepeatable) => activeRepeatable.Id == completedQuestId ); if (repeatableQuest is not null) { // Need to remove redundant scav quest object as its no longer necessary, is tracked in pmc profile if (repeatableQuest.Side == "Scav") { RemoveQuestFromScavProfile(sessionID, repeatableQuest.Id); } } } // Hydrate client response questsStatus array with data var questStatusChanges = GetQuestsWithDifferentStatuses(preCompleteProfileQuests, pmcData.Quests); if (questStatusChanges is not null) { completeQuestResponse.ProfileChanges[sessionID].QuestsStatus.AddRange(questStatusChanges); } // Recalculate level in event player leveled up pmcData.Info.Level = _playerService.CalculateLevel(pmcData); return completeQuestResponse; } /** * Handle client/quest/list * Get all quests visible to player * Exclude quests with incomplete preconditions (level/loyalty) * @param sessionID session id * @returns array of Quest */ public List GetClientQuests(string sessionID) { List questsToShowPlayer = []; var profile = _profileHelper.GetPmcProfile(sessionID); if (profile is null) { _logger.Error($"Profile {sessionID} not found, unable to return quests"); return []; } var allQuests = GetQuestsFromDb(); foreach (var quest in allQuests) { // Player already accepted the quest, show it regardless of status var questInProfile = profile.Quests.FirstOrDefault(x => x.QId == quest.Id); if (questInProfile is not null) { quest.SptStatus = questInProfile.Status; questsToShowPlayer.Add(quest); continue; } // Filter out bear quests for USEC and vice versa if (QuestIsForOtherSide(profile.Info.Side, quest.Id)) { continue; } if (!ShowEventQuestToPlayer(quest.Id)) { continue; } // Don't add quests that have a level higher than the user's if (!PlayerLevelFulfillsQuestRequirement(quest, profile.Info.Level.Value)) { continue; } // Player can use trader mods then remove them, leaving quests behind if (!profile.TradersInfo.TryGetValue(quest.TraderId, out var trader)) { _logger.Debug($"Unable to show quest: {quest.QuestName} as its for a trader: {quest.TraderId} that no longer exists."); continue; } var questRequirements = _questConditionHelper.GetQuestConditions(quest.Conditions.AvailableForStart); var loyaltyRequirements = _questConditionHelper.GetLoyaltyConditions(quest.Conditions.AvailableForStart); var standingRequirements = _questConditionHelper.GetStandingConditions(quest.Conditions.AvailableForStart); // Quest has no conditions, standing or loyalty conditions, add to visible quest list if (questRequirements.Count == 0 && loyaltyRequirements.Count == 0 && standingRequirements.Count == 0) { quest.SptStatus = QuestStatusEnum.AvailableForStart; questsToShowPlayer.Add(quest); continue; } // Check the status of each quest condition, if any are not completed // then this quest should not be visible var haveCompletedPreviousQuest = true; foreach (var conditionToFulfil in questRequirements) { // If the previous quest isn't in the user profile, it hasn't been completed or started var questIdsToFulfil = conditionToFulfil.Target as string[] ?? []; var prerequisiteQuest = profile.Quests.FirstOrDefault(profileQuest => questIdsToFulfil.Contains(profileQuest.QId)); if (prerequisiteQuest is null) { haveCompletedPreviousQuest = false; break; } // Prereq does not have its status requirement fulfilled // Some bsg status ids are strings, MUST convert to number before doing includes check if (!conditionToFulfil.Status.Contains(prerequisiteQuest.Status.Value)) { haveCompletedPreviousQuest = false; break; } // Has a wait timer if (conditionToFulfil.AvailableAfter > 0) { // Compare current time to unlock time for previous quest prerequisiteQuest.StatusTimers.TryGetValue(prerequisiteQuest.Status.Value, out var previousQuestCompleteTime); var unlockTime = previousQuestCompleteTime + conditionToFulfil.AvailableAfter; if (unlockTime > _timeUtil.GetTimeStamp()) { _logger.Debug( $"Quest {quest.QuestName} is locked for another: {unlockTime - _timeUtil.GetTimeStamp()} seconds" ); } } } // Previous quest not completed, skip if (!haveCompletedPreviousQuest) { continue; } var passesLoyaltyRequirements = true; foreach (var condition in loyaltyRequirements) { if (!TraderLoyaltyLevelRequirementCheck(condition, profile)) { passesLoyaltyRequirements = false; break; } } var passesStandingRequirements = true; foreach (var condition in standingRequirements) { if (!TraderStandingRequirementCheck(condition, profile)) { passesStandingRequirements = false; break; } } if (haveCompletedPreviousQuest && passesLoyaltyRequirements && passesStandingRequirements) { quest.SptStatus = QuestStatusEnum.AvailableForStart; questsToShowPlayer.Add(quest); } } return questsToShowPlayer; } /** * Create a clone of the given quest array with the rewards updated to reflect the * given game version * @param quests List of quests to check * @param gameVersion Game version of the profile * @returns Array of Quest objects with the rewards filtered correctly for the game version */ protected List UpdateQuestsForGameEdition(List quests, string gameVersion) { throw new NotImplementedException(); } /** * Return a list of quests that would fail when supplied quest is completed * @param completedQuestId Quest completed id * @returns Array of Quest objects */ protected List GetQuestsFromProfileFailedByCompletingQuest(string completedQuestId, PmcData pmcProfile) { throw new NotImplementedException(); } /** * Fail the provided quests * Update quest in profile, otherwise add fresh quest object with failed status * @param sessionID session id * @param pmcData player profile * @param questsToFail quests to fail * @param output Client output */ protected void FailQuests( string sessionID, PmcData pmcData, List questsToFail, ItemEventRouterResponse output ) { throw new NotImplementedException(); } /** * Send a popup to player on successful completion of a quest * @param sessionID session id * @param pmcData Player profile * @param completedQuestId Completed quest id * @param questRewards Rewards given to player */ protected void SendSuccessDialogMessageOnQuestComplete( string sessionID, PmcData pmcData, string completedQuestId, List questRewards ) { throw new NotImplementedException(); } /** * Look for newly available quests after completing a quest with a requirement to wait x minutes (time-locked) before being available and add data to profile * @param pmcData Player profile to update * @param quests Quests to look for wait conditions in * @param completedQuestId Quest just completed */ protected void AddTimeLockedQuestsToProfile(PmcData pmcData, List quests, string completedQuestId) { throw new NotImplementedException(); } /** * Remove a quest entirely from a profile * @param sessionId Player id * @param questIdToRemove Qid of quest to remove */ protected void RemoveQuestFromScavProfile(string sessionId, string questIdToRemove) { throw new NotImplementedException(); } /** * Return quests that have different statuses * @param preQuestStatusus Quests before * @param postQuestStatuses Quests after * @returns QuestStatusChange array */ protected List GetQuestsWithDifferentStatuses( List preQuestStatuses, List postQuestStatuses ) { throw new NotImplementedException(); } /** * Does a provided quest have a level requirement equal to or below defined level * @param quest Quest to check * @param playerLevel level of player to test against quest * @returns true if quest can be seen/accepted by player of defined level */ protected bool PlayerLevelFulfillsQuestRequirement(Quest quest, double playerLevel) { if (quest.Conditions is null) { // No conditions return true; } var levelConditions = _questConditionHelper.GetLevelConditions(quest.Conditions.AvailableForStart); if (levelConditions is not null) { foreach (var levelCondition in levelConditions) { if (!DoesPlayerLevelFulfilCondition(playerLevel, levelCondition)) { // Not valid, exit out return false; } } } // All conditions passed / has no level requirement, valid return true; } }