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.Eft.Ragfair; using SPTarkov.Server.Core.Models.Enums; using SPTarkov.Server.Core.Models.Enums.Hideout; namespace SPTarkov.Server.Core.Extensions; public static class ProfileExtensions { /// /// Return all quest items current in the supplied profile /// /// Profile to get quest items from /// List of item objects public static IEnumerable GetQuestItemsInProfile(this PmcData profile) { return profile?.Inventory?.Items.Where(i => i.ParentId == profile.Inventory.QuestRaidItems).ToList(); } /// /// Upgrade hideout wall from starting level to interactable level if necessary stations have been upgraded /// /// Profile to upgrade wall in public static void UnlockHideoutWallInProfile(this PmcData profile) { var profileHideoutAreas = profile.Hideout.Areas; var waterCollector = profileHideoutAreas.FirstOrDefault(x => x.Type == HideoutAreas.WaterCollector); var medStation = profileHideoutAreas.FirstOrDefault(x => x.Type == HideoutAreas.MedStation); var wall = profileHideoutAreas.FirstOrDefault(x => x.Type == HideoutAreas.EmergencyWall); // No collector or med station, skip if (waterCollector is null && medStation is null) { return; } // If med-station > level 1 AND water collector > level 1 AND wall is level 0 if (waterCollector?.Level >= 1 && medStation?.Level >= 1 && wall?.Level <= 0) { wall.Level = 3; } } /// /// Does the provided profile contain any condition counters /// /// Profile to check for condition counters /// Profile has condition counters public static bool ProfileHasConditionCounters(this PmcData profile) { if (profile.TaskConditionCounters is null) { return false; } return profile.TaskConditionCounters.Count > 0; } /// /// Get a specific common skill from supplied profile /// /// Player profile /// Skill to look up and return value from /// Common skill object from desired profile public static CommonSkill? GetSkillFromProfile(this PmcData profile, SkillTypes skill) { return profile?.Skills?.Common?.FirstOrDefault(s => s.Id == skill); } /// /// Get a multiplier based on player's skill level and value per level /// /// Player profile /// Player skill from profile /// Value from globals.config.SkillsSettings - `PerLevel` /// Multiplier from 0 to 1 public static double GetSkillBonusMultipliedBySkillLevel(this PmcData pmcData, SkillTypes skill, double valuePerLevel) { var profileSkill = pmcData.GetSkillFromProfile(skill); if (profileSkill is null || profileSkill.Progress == 0) { return 0; } // If the level is 51 we need to round it at 50 so on elite you dont get 25.5% // at level 1 you already get 0.5%, so it goes up until level 50. For some reason the wiki // says that it caps at level 51 with 25% but as per dump data that is incorrect apparently var roundedLevel = Math.Floor(profileSkill.Progress / 100); roundedLevel = roundedLevel.Approx(51d) ? roundedLevel - 1 : roundedLevel; return roundedLevel * valuePerLevel / 100; } /// /// Get the scav karma level for a profile /// Is also the fence trader rep level /// /// pmc profile /// karma level public static double GetScavKarmaLevel(this PmcData pmcData) { // can be empty during profile creation if (!pmcData.TradersInfo.TryGetValue(Traders.FENCE, out var fenceInfo)) { return 0; } if (fenceInfo.Standing > 6) { return 6; } return Math.Floor(fenceInfo.Standing ?? 0); } public static Skills GetSkillsOrDefault(this PmcData profile) { return profile?.Skills ?? GetDefaultSkills(); } private static Skills GetDefaultSkills() { return new Skills { Common = [], Mastering = [], Points = 0, }; } /// /// Recursively checks if the given item is /// inside the stash, that is it has the stash as /// ancestor with slotId=hideout /// /// Player profile /// Item to look for /// True if item exists inside stash public static bool IsItemInStash(this PmcData pmcData, Item itemToCheck) { // Start recursive check return pmcData.IsParentInStash(itemToCheck.Id); } public static bool IsParentInStash(this PmcData pmcData, MongoId itemId) { // Item not found / has no parent var item = pmcData.Inventory.Items.FirstOrDefault(item => item.Id == itemId); if (item?.ParentId is null) { return false; } // Root level. Items parent is the stash with slotId "hideout" if (item.ParentId == pmcData.Inventory.Stash && item.SlotId == "hideout") { return true; } // Recursive case: Check the items parent return IsParentInStash(pmcData, item.ParentId); } /// /// Iterate over all bonuses and sum up all bonuses of desired type in provided profile /// /// Player profile /// Bonus to sum up /// Summed bonus value or 0 if no bonus found public static double GetBonusValueFromProfile(this PmcData pmcProfile, BonusType desiredBonus) { var bonuses = pmcProfile?.Bonuses?.Where(b => b.Type == desiredBonus); if (!bonuses.Any()) { return 0; } // Sum all bonuses found above return bonuses?.Sum(bonus => bonus?.Value ?? 0) ?? 0; } public static bool PlayerIsFleaBanned(this PmcData pmcProfile, long currentTimestamp) { return pmcProfile?.Info?.Bans?.Any(b => b.BanType == BanType.RagFair && currentTimestamp < b.DateTime) ?? false; } /// /// Calculates the current level of a player based on their accumulated experience points. /// This method iterates through an experience table to determine the highest level achieved /// by comparing the player's experience against cumulative thresholds. /// /// Player profile /// Experience table from globals.json /// /// The calculated level of the player as an integer, or null if the level cannot be determined. /// This value is also assigned to within the provided profile. /// public static int? CalculateLevel(this PmcData pmcData, ExpTable[] expTable) { var accExp = 0; for (var i = 0; i < expTable.Length; i++) { accExp += expTable[i].Experience; if (pmcData.Info.Experience < accExp) { break; } pmcData.Info.Level = i + 1; } return pmcData.Info.Level; } /// /// Does the provided item have a root item with the provided id /// /// Profile with items /// Item to check /// Root item id to check for /// True when item has rootId, false when not public static bool DoesItemHaveRootId(this PmcData pmcData, Item item, MongoId rootId) { var currentItem = item; while (currentItem is not null) { // If we've found the equipment root ID, return true if (currentItem.Id == rootId) { return true; } // Otherwise get the parent item currentItem = pmcData.Inventory.Items.FirstOrDefault(item => item.Id == currentItem.ParentId); } return false; } /// /// Get status of a quest in player profile by its id /// /// Profile to search /// Quest id to look up /// QuestStatus enum public static QuestStatusEnum GetQuestStatus(this PmcData pmcData, MongoId questId) { var quest = pmcData.Quests?.FirstOrDefault(q => q.QId == questId); return quest?.Status ?? QuestStatusEnum.Locked; } /// /// Handle Remove event /// Remove item from player inventory + insured items array /// Also deletes child items /// /// Profile to remove item from (pmc or scav) /// Items id to remove /// Session id /// OPTIONAL - ItemEventRouterResponse public static void RemoveItem(this PmcData profile, MongoId itemId, MongoId sessionId, ItemEventRouterResponse? output = null) { if (itemId.IsEmpty) { return; } // Get children of item, they get deleted too var itemAndChildrenToRemove = profile.Inventory.Items.GetItemWithChildren(itemId); if (!itemAndChildrenToRemove.Any()) { return; } var inventoryItems = profile.Inventory.Items; var insuredItems = profile.InsuredItems; // We have output object, inform client of root item deletion, not children output?.ProfileChanges[sessionId].Items.DeletedItems.Add(new DeletedItem { Id = itemId }); foreach (var item in itemAndChildrenToRemove) { // We expect that each inventory item and each insured item has unique "_id", respective "itemId". // Therefore, we want to use a NON-Greedy function and escape the iteration as soon as we find requested item. var inventoryIndex = inventoryItems.FindIndex(inventoryItem => inventoryItem.Id == item.Id); if (inventoryIndex != -1) { inventoryItems.RemoveAt(inventoryIndex); } var insuredItemIndex = insuredItems.FindIndex(insuredItem => insuredItem.ItemId == item.Id); if (insuredItemIndex != -1) { insuredItems.RemoveAt(insuredItemIndex); } } } /// /// Does Player have necessary trader loyalty to purchase flea offer /// /// Player profile /// Flea offer being bought /// True if player can buy offer public static bool ProfileMeetsTraderLoyaltyLevelToBuyOffer(this PmcData pmcData, RagfairOffer fleaOffer) { if (fleaOffer.LoyaltyLevel == 0) { // No requirement, always passes return true; } if (pmcData.TradersInfo.TryGetValue(fleaOffer.User.Id, out var traderInfo)) { // Trader exists in profile ,do loyalty level check return traderInfo.LoyaltyLevel >= fleaOffer.LoyaltyLevel; } // No trader data on player profile, fail check return false; } /// /// Get Ids of traders with an unlocked status of "false" /// /// Player profile /// Hashset of Trader ids public static HashSet GetLockedTraderIds(this PmcData pmcData) { return pmcData.TradersInfo?.Where(trader => trader.Value.Unlocked == false).Select(t => t.Key).ToHashSet() ?? []; } }