using SPTarkov.Common.Extensions; using SPTarkov.DI.Annotations; using SPTarkov.Server.Core.Extensions; 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, LocalisationService _localisationService, 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 string[] 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, string questId, QuestStatusEnum state, string 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( _localisationService.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.GetByJsonProp>(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(string 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 ?? 1; } /// /// 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; } }