using SPTarkov.DI.Annotations; using SPTarkov.Server.Core.Extensions; using SPTarkov.Server.Core.Models.Common; using SPTarkov.Server.Core.Models.Eft.Common; using SPTarkov.Server.Core.Models.Eft.Common.Tables; using SPTarkov.Server.Core.Models.Eft.ItemEvent; using SPTarkov.Server.Core.Models.Enums; using SPTarkov.Server.Core.Models.Utils; using SPTarkov.Server.Core.Services; using SPTarkov.Server.Core.Utils.Cloners; namespace SPTarkov.Server.Core.Helpers; [Injectable] public class QuestRewardHelper( ISptLogger logger, PaymentHelper paymentHelper, DatabaseService databaseService, ProfileHelper profileHelper, RewardHelper rewardHelper, ServerLocalisationService serverLocalisationService, ICloner cloner ) { /// /// Value for in game reward traders to not duplicate quest rewards. /// Value can be modified by modders by overriding this value with new traders. /// Ensure to add Lightkeeper's ID (638f541a29ffd1183d187f57) and BTR Driver's ID (656f0f98d80a697f855d34b1) /// protected readonly MongoId[] InGameTraders = [Traders.LIGHTHOUSEKEEPER, Traders.BTR]; /// /// Give player quest rewards - Skills/exp/trader standing/items/assort unlocks - Returns reward items player earned /// SKIP quests completed in-game /// /// Player profile (scav or pmc) /// questId of quest to get rewards for /// State of the quest to get rewards for /// Session id /// Response to send back to client /// Array of reward items player was given public IEnumerable ApplyQuestReward( PmcData profileData, MongoId questId, QuestStatusEnum state, MongoId sessionId, ItemEventRouterResponse questResponse ) { // Repeatable quest base data is always in PMCProfile, `profileData` may be scav profile // TODO: Move repeatable quest data to profile-agnostic location var fullProfile = profileHelper.GetFullProfile(sessionId); var pmcProfile = fullProfile.CharacterData.PmcData; if (pmcProfile is null) { logger.Error($"Unable to get pmc profile for: {sessionId}, no rewards given"); return []; } var questDetails = GetQuestFromDb(questId, pmcProfile); if (questDetails is null) { logger.Warning(serverLocalisationService.GetText("quest-unable_to_find_quest_in_db_no_quest_rewards", questId)); return []; } if (IsInGameTrader(questDetails)) { // Assuming in-game traders give ALL rewards logger.Debug( $"Skipping quest rewards for quest: {questDetails.Id}, trader: {questDetails.TraderId} in InGameRewardTrader list" ); return []; } var questMoneyRewardBonusMultiplier = GetQuestMoneyRewardBonusMultiplier(pmcProfile); if (questMoneyRewardBonusMultiplier > 0) // money = money + (money * IntelCenterBonus / 100) { questDetails = ApplyMoneyBoost(questDetails, questMoneyRewardBonusMultiplier, state); } // e.g. 'Success' or 'AvailableForFinish' var rewards = questDetails.Rewards[state.ToString()]; return rewardHelper.ApplyRewards(rewards, CustomisationSource.UNLOCKED_IN_GAME, fullProfile, profileData, questId, questResponse); } /// /// Determines if quest rewards are given in raid by the trader instead of through messaging system. /// /// The quest to check. /// True if the quest's trader is in the in-game reward trader list; otherwise, false. protected bool IsInGameTrader(Quest quest) { return InGameTraders.Contains(quest.TraderId); } /// /// Get quest by id from database (repeatable quests are stored in profile, check there if questId not found) /// /// Id of quest to find /// Player profile /// IQuest object protected Quest? GetQuestFromDb(MongoId questId, PmcData pmcData) { // Look for quest in db if (databaseService.GetQuests().TryGetValue(questId, out var quest)) { return quest; } // Group daily/weekly/scav repeatable subtypes into one collection and find first that matched desired quest id return pmcData .RepeatableQuests?.SelectMany(repeatableQuestSubType => repeatableQuestSubType.ActiveQuests) .FirstOrDefault(repeatableQuest => repeatableQuest.Id == questId); } /// /// Get players money reward bonus from profile /// /// player profile /// bonus as a percent protected double GetQuestMoneyRewardBonusMultiplier(PmcData pmcData) { // Check player has intel center var moneyRewardBonuses = pmcData.Bonuses.Where(bonus => bonus.Type == BonusType.QuestMoneyReward); // Get a total of the quest money reward percent bonuses var moneyRewardBonusPercent = moneyRewardBonuses.Aggregate(0D, (accumulate, bonus) => accumulate + bonus.Value ?? 0); // Calculate hideout management bonus as a percentage (up to 51% bonus) var hideoutManagementSkill = pmcData.GetSkillFromProfile(SkillTypes.HideoutManagement); // 5100 becomes 0.51, add 1 to it, 1.51 // We multiply the money reward bonuses by the hideout management skill multiplier, giving the new result var hideoutManagementBonusMultiplier = hideoutManagementSkill != null ? 2 + hideoutManagementSkill.Progress / 1000 : 1; // e.g 15% * 1.4 return moneyRewardBonusPercent + hideoutManagementBonusMultiplier; } /// /// Adjust a quests money rewards by supplied multiplier /// /// Quest to apply bonus to /// Percent to adjust money rewards by /// Status of quest to apply money boost to rewards of /// Updated quest public Quest ApplyMoneyBoost(Quest quest, double bonusPercent, QuestStatusEnum questStatus) { var clonedQuest = cloner.Clone(quest); if (clonedQuest?.Rewards?["Success"] == null) { return clonedQuest; } // Grab just the money rewards from quest reward pool var moneyRewards = clonedQuest .Rewards["Success"] .Where(reward => reward.Type == RewardType.Item && reward.Items != null && reward.Items.Count > 0 && paymentHelper.IsMoneyTpl(reward.Items.FirstOrDefault().Template) ); foreach (var moneyReward in moneyRewards) { // Add % bonus to existing StackObjectsCount var rewardItem = moneyReward.Items?.FirstOrDefault(); if (rewardItem is null) { logger.Error($"Unable to apply money reward bonus to quest: {quest.Name} as no money item found"); continue; } var newCurrencyAmount = Math.Floor((rewardItem.Upd.StackObjectsCount ?? 0) * (1 + (bonusPercent / 100))); rewardItem.Upd.StackObjectsCount = newCurrencyAmount; moneyReward.Value = newCurrencyAmount; } return clonedQuest; } }