finish off circleOfCultistService

This commit is contained in:
CWX
2025-01-26 18:13:51 +00:00
parent 75b656bf87
commit e6c1d9fe94
3 changed files with 632 additions and 67 deletions
@@ -147,7 +147,7 @@ public record StageImprovementRequirement
public string? Type { get; set; }
}
public record StageRequirement : RequirementBase
public record StageRequirement
{
[JsonPropertyName("areaType")]
public int? AreaType { get; set; }
@@ -181,4 +181,7 @@ public record StageRequirement : RequirementBase
[JsonPropertyName("skillLevel")]
public int? SkillLevel { get; set; }
[JsonPropertyName("type")]
public string? Type { get; set; }
}
@@ -56,7 +56,7 @@ public record HideoutProduction
public bool? IsCodeProduction { get; set; }
}
public record Requirement : RequirementBase
public record Requirement
{
[JsonPropertyName("templateId")]
public string? TemplateId { get; set; }
@@ -87,10 +87,7 @@ public record Requirement : RequirementBase
[JsonPropertyName("gameVersions")]
public List<string>? GameVersions { get; set; }
}
public record RequirementBase
{
[JsonPropertyName("type")]
public string? Type { get; set; }
}
+626 -61
View File
@@ -1,17 +1,46 @@
using SptCommon.Annotations;
using Core.Helpers;
using SptCommon.Annotations;
using Core.Models.Eft.Common;
using Core.Models.Eft.Common.Tables;
using Core.Models.Eft.Hideout;
using Core.Models.Eft.ItemEvent;
using Core.Models.Eft.Profile;
using Core.Models.Enums;
using Core.Models.Enums.Hideout;
using Core.Models.Spt.Config;
using Core.Models.Spt.Hideout;
using Hideout = Core.Models.Eft.Common.Tables.Hideout;
using Core.Models.Utils;
using Core.Routers;
using Core.Servers;
using Core.Utils;
using Core.Utils.Cloners;
using SptCommon.Extensions;
namespace Core.Services;
[Injectable(InjectionType.Singleton)]
public class CircleOfCultistService
public class CircleOfCultistService(
ISptLogger<CircleOfCultistService> _logger,
TimeUtil _timeUtil,
ICloner _cloner,
EventOutputHolder _eventOutputHolder,
RandomUtil _randomUtil,
HashUtil _hashUtil,
ItemHelper _itemHelper,
PresetHelper _presetHelper,
ProfileHelper _profileHelper,
InventoryHelper _inventoryHelper,
HideoutHelper _hideoutHelper,
QuestHelper _questHelper,
DatabaseService _databaseService,
ItemFilterService _itemFilterService,
SeasonalEventService _seasonalEventService,
ConfigServer _configServer
)
{
protected HideoutConfig _hideoutConfig = _configServer.GetConfig<HideoutConfig>();
public const string CircleOfCultistSlotId = "CircleOfCultistsGrid1";
/// <summary>
/// Start a sacrifice event
/// Generate rewards
@@ -27,49 +56,89 @@ public class CircleOfCultistService
HideoutCircleOfCultistProductionStartRequestData request
)
{
throw new NotImplementedException();
var cultistCircleStashId = pmcData.Inventory.HideoutAreaStashes.GetValueOrDefault(HideoutAreas.CIRCLE_OF_CULTISTS.ToString());
// `cultistRecipes` just has single recipeId
var cultistCraftData = _databaseService.GetHideout().Production.CultistRecipes.FirstOrDefault();
List<Item> sacrificedItems = GetSacrificedItems(pmcData);
var sacrificedItemCostRoubles = sacrificedItems.Aggregate(
0D,
(sum, curr) => sum + (_itemHelper.GetItemPrice(curr.Template) ?? 0)
);
var rewardAmountMultiplier = GetRewardAmountMultiplier(pmcData, _hideoutConfig.CultistCircle);
// Get the rouble amount we generate rewards with from cost of sacrified items * above multipler
var rewardAmountRoubles = Math.Round(sacrificedItemCostRoubles * rewardAmountMultiplier);
// Check if it matches any direct swap recipes
var directRewardsCache = GenerateSacrificedItemsCache(_hideoutConfig.CultistCircle.DirectRewards);
var directRewardSettings = CheckForDirectReward(sessionId, sacrificedItems, directRewardsCache);
var hasDirectReward = directRewardSettings?.Reward.Count > 0;
// Get craft time and bonus status
var craftingInfo = GetCircleCraftingInfo(
rewardAmountRoubles,
_hideoutConfig.CultistCircle,
directRewardSettings
);
// Create production in pmc profile
RegisterCircleOfCultistProduction(
sessionId,
pmcData,
cultistCraftData.Id,
sacrificedItems,
craftingInfo.Time
);
var output = _eventOutputHolder.GetOutput(sessionId);
// Remove sacrificed items from circle inventory
foreach (var item in sacrificedItems)
{
if (item.SlotId == CircleOfCultistService.CircleOfCultistSlotId)
{
_inventoryHelper.RemoveItem(pmcData, item.Id, sessionId, output);
}
}
var rewards = hasDirectReward
? GetDirectRewards(sessionId, directRewardSettings, cultistCircleStashId)
: GetRewardsWithinBudget(
GetCultistCircleRewardPool(sessionId, pmcData, craftingInfo, _hideoutConfig.CultistCircle),
rewardAmountRoubles,
cultistCircleStashId,
_hideoutConfig.CultistCircle
);
// Get the container grid for cultist stash area
var cultistStashDbItem = _itemHelper.GetItem(ItemTpl.HIDEOUTAREACONTAINER_CIRCLEOFCULTISTS_STASH_1);
// Ensure rewards fit into container
var containerGrid = _inventoryHelper.GetContainerSlotMap(cultistStashDbItem.Value.Id);
AddRewardsToCircleContainer(sessionId, pmcData, rewards, containerGrid, cultistCircleStashId, output);
return output;
}
/// <summary>
/// Attempt to add all rewards to cultist circle, if they don't fit remove one and try again until they fit
/// </summary>
/// <param name="sessionId">Session id</param>
/// <param name="pmcData">Player profile</param>
/// <param name="rewards">Rewards to send to player</param>
/// <param name="containerGrid">Cultist grid to add rewards to</param>
/// <param name="cultistCircleStashId">Stash id</param>
/// <param name="output">Client output</param>
protected void AddRewardsToCircleContainer(
string sessionId,
PmcData pmcData,
List<List<Item>> rewards,
List<List<int>> containerGrid,
string cultistCircleStashId,
ItemEventRouterResponse output
)
private double GetRewardAmountMultiplier(PmcData pmcData, CultistCircleSettings cultistCircleSettings)
{
throw new NotImplementedException();
}
// Get a randomised value to multiply the sacrificed rouble cost by
var rewardAmountMultiplier = _randomUtil.GetFloat(
(float)cultistCircleSettings.RewardPriceMultiplerMinMax.Min,
(float)cultistCircleSettings.RewardPriceMultiplerMinMax.Max
);
/// <summary>
/// Create a map of the possible direct rewards, keyed by the items needed to be sacrificed
/// </summary>
/// <param name="directRewards">Direct rewards array from hideout config</param>
/// <returns>Dictionary</returns>
protected Dictionary<string, DirectRewardSettings> GenerateSacrificedItemsCache(List<DirectRewardSettings> directRewards)
{
throw new NotImplementedException();
}
// Adjust value generated by the players hideout management skill
var hideoutManagementSkill = _profileHelper.GetSkillFromProfile(pmcData, SkillTypes.HideoutManagement);
if (hideoutManagementSkill is not null)
{
rewardAmountMultiplier *=
(float)(1 + hideoutManagementSkill.Progress / 10000); // 5100 becomes 0.51, add 1 to it, 1.51, multiply the bonus by it (e.g. 1.2 x 1.51)
}
/// <summary>
/// Get the reward amount multiple value based on players hideout management skill + configs rewardPriceMultiplerMinMax values
/// </summary>
/// <param name="pmcData">Player profile</param>
/// <param name="cultistCircleSettings">Circle config settings</param>
/// <returns>Reward Amount Multiplier</returns>
protected double GetRewardAmountMultiplier(PmcData pmcData, CultistCircleSettings cultistCircleSettings)
{
throw new NotImplementedException();
return rewardAmountMultiplier;
}
/// <summary>
@@ -88,7 +157,17 @@ public class CircleOfCultistService
double craftingTime
)
{
throw new NotImplementedException();
// Create circle production/craft object to add to player profile
var cultistProduction = _hideoutHelper.InitProduction(recipeId, craftingTime, false);
// Flag as cultist circle for code to pick up later
cultistProduction.SptIsCultistCircle = true;
// Add items player sacrificed
cultistProduction.GivenItemsInStart = sacrificedItems;
// Add circle production to profile keyed to recipe id
pmcData.Hideout.Production[recipeId] = cultistProduction;
}
/// <summary>
@@ -105,7 +184,48 @@ public class CircleOfCultistService
DirectRewardSettings directRewardSettings = null
)
{
throw new NotImplementedException();
var result = new CircleCraftDetails
{
Time = -1,
RewardType = CircleRewardType.RANDOM,
RewardAmountRoubles = (int)rewardAmountRoubles,
RewardDetails = null,
};
// Direct reward edge case
if (directRewardSettings is not null)
{
result.Time = directRewardSettings.CraftTimeSeconds;
return result;
}
var random = new Random();
// Get a threshold where sacrificed amount is between thresholds min and max
var matchingThreshold = GetMatchingThreshold(circleConfig.CraftTimeThreshholds, rewardAmountRoubles);
if (
rewardAmountRoubles >= circleConfig.HideoutCraftSacrificeThresholdRub &&
random.Next(0, 1) <= circleConfig.BonusChanceMultiplier
)
{
// Sacrifice amount is enough + passed 25% check to get hideout/task rewards
result.Time =
circleConfig.CraftTimeOverride != -1
? circleConfig.CraftTimeOverride
: circleConfig.HideoutTaskRewardTimeSeconds;
result.RewardType = CircleRewardType.HIDEOUT_TASK;
return result;
}
// Edge case, check if override exists, Otherwise use matching threshold craft time
result.Time =
circleConfig.CraftTimeOverride != -1 ? circleConfig.CraftTimeOverride : matchingThreshold.CraftTimeSeconds;
result.RewardDetails = matchingThreshold;
return result;
}
protected CraftTimeThreshold GetMatchingThreshold(
@@ -113,7 +233,26 @@ public class CircleOfCultistService
double rewardAmountRoubles
)
{
throw new NotImplementedException();
var matchingThreshold = thresholds.FirstOrDefault(
(craftThreshold) => craftThreshold.Min <= rewardAmountRoubles && craftThreshold.Max >= rewardAmountRoubles
);
// No matching threshold, make one
if (matchingThreshold is null)
{
// None found, use a defalt
_logger.Warning("Unable to find a matching cultist circle threshold, using fallback of 12 hours");
// Use first threshold value (cheapest) from parameter array, otherwise use 12 hours
var firstThreshold = thresholds.FirstOrDefault();
var craftTime = firstThreshold?.CraftTimeSeconds is not null && firstThreshold.CraftTimeSeconds > 0
? firstThreshold.CraftTimeSeconds
: _timeUtil.GetHoursAsSeconds(12);
return new CraftTimeThreshold { Min = firstThreshold?.Min ?? 1, Max = firstThreshold?.Max ?? 34999, CraftTimeSeconds = craftTime };
}
return matchingThreshold;
}
/// <summary>
@@ -123,7 +262,23 @@ public class CircleOfCultistService
/// <returns>Array of items from player inventory</returns>
protected List<Item> GetSacrificedItems(PmcData pmcData)
{
throw new NotImplementedException();
// Get root items that are in the cultist sacrifice window
var inventoryRootItemsInCultistGrid = pmcData.Inventory.Items.Where(
(item) => item.SlotId == CircleOfCultistService.CircleOfCultistSlotId
);
// Get rootitem + its children
List<Item> sacrificedItems = [];
foreach (var rootItem in inventoryRootItemsInCultistGrid)
{
var rootItemWithChildren = _itemHelper.FindAndReturnChildrenAsItems(
pmcData.Inventory.Items,
rootItem.Id
);
sacrificedItems.AddRange(rootItemWithChildren);
}
return sacrificedItems;
}
/// <summary>
@@ -140,7 +295,95 @@ public class CircleOfCultistService
CultistCircleSettings circleConfig
)
{
throw new NotImplementedException();
// Prep rewards array (reward can be item with children, hence array of arrays)
List<List<Item>> rewards = [];
// Pick random rewards until we have exhausted the sacrificed items budget
var totalRewardCost = 0;
var rewardItemCount = 0;
var failedAttempts = 0;
while (
totalRewardCost < rewardBudget &&
rewardItemTplPool.Count > 0 &&
rewardItemCount < circleConfig.MaxRewardItemCount
)
{
if (failedAttempts > circleConfig.MaxAttemptsToPickRewardsWithinBudget)
{
_logger.Warning($"Exiting reward generation after {failedAttempts} failed attempts");
break;
}
// Choose a random tpl from pool
var randomItemTplFromPool = _randomUtil.GetArrayValue(rewardItemTplPool);
// Is weapon/armor, handle differently
if (
_itemHelper.ArmorItemHasRemovableOrSoftInsertSlots(randomItemTplFromPool) ||
_itemHelper.IsOfBaseclass(randomItemTplFromPool, BaseClasses.WEAPON)
)
{
var defaultPreset = _presetHelper.GetDefaultPreset(randomItemTplFromPool);
if (defaultPreset is null)
{
_logger.Warning($"Reward tpl: {randomItemTplFromPool} lacks a default preset, skipping reward");
failedAttempts++;
continue;
}
// Ensure preset has unique ids and is cloned so we don't alter the preset data stored in memory
var presetAndMods = _itemHelper.ReplaceIDs(defaultPreset.Items);
_itemHelper.RemapRootItemId(presetAndMods);
rewardItemCount++;
totalRewardCost += (int)_itemHelper.GetItemPrice(randomItemTplFromPool);
rewards.Add(presetAndMods);
continue;
}
// Some items can have variable stack size, e.g. ammo / currency
var stackSize = GetRewardStackSize(
randomItemTplFromPool,
(int)(rewardBudget / (rewardItemCount == 0 ? 1 : rewardItemCount)) // Remaining rouble budget
);
// Not a weapon/armor, standard single item
List<Item> rewardItem =
[
new Item
{
Id = _hashUtil.Generate(),
Template = randomItemTplFromPool,
ParentId = cultistCircleStashId,
SlotId = CircleOfCultistService.CircleOfCultistSlotId,
Upd = new Upd
{
StackObjectsCount = stackSize,
SpawnedInSession = true,
},
},
];
// Edge case - item is ammo container and needs cartridges added
if (_itemHelper.IsOfBaseclass(randomItemTplFromPool, BaseClasses.AMMO_BOX))
{
var itemDetails = _itemHelper.GetItem(randomItemTplFromPool).Value;
_itemHelper.AddCartridgesToAmmoBox(rewardItem, itemDetails);
}
// Increment price of rewards to give to player + add to reward array
rewardItemCount++;
var singleItemPrice = _itemHelper.GetItemPrice(randomItemTplFromPool);
var itemPrice = singleItemPrice * stackSize;
totalRewardCost += (int)itemPrice;
rewards.Add(rewardItem);
}
return rewards;
}
/// <summary>
@@ -156,7 +399,76 @@ public class CircleOfCultistService
string cultistCircleStashId
)
{
throw new NotImplementedException();
// Prep rewards array (reward can be item with children, hence array of arrays)
List<List<Item>> rewards = [];
// Handle special case of tagilla helmets - only one reward is allowed
if (directReward.Reward.Contains(ItemTpl.FACECOVER_TAGILLAS_WELDING_MASK_GORILLA))
{
directReward.Reward = [_randomUtil.GetArrayValue(directReward.Reward)];
}
// Loop because these can include multiple rewards
foreach (var rewardTpl in directReward.Reward)
{
// Is weapon/armor, handle differently
if (
_itemHelper.ArmorItemHasRemovableOrSoftInsertSlots(rewardTpl) ||
_itemHelper.IsOfBaseclass(rewardTpl, BaseClasses.WEAPON)
)
{
var defaultPreset = _presetHelper.GetDefaultPreset(rewardTpl);
if (defaultPreset is null)
{
_logger.Warning($"Reward tpl: {rewardTpl} lacks a default preset, skipping reward");
continue;
}
// Ensure preset has unique ids and is cloned so we don't alter the preset data stored in memory
var presetAndMods = _itemHelper.ReplaceIDs(defaultPreset.Items);
_itemHelper.RemapRootItemId(presetAndMods);
rewards.Add(presetAndMods);
continue;
}
// 'Normal' item, non-preset
var stackSize = GetDirectRewardBaseTypeStackSize(rewardTpl);
List<Item> rewardItem =
[
new Item
{
Id = _hashUtil.Generate(),
Template = rewardTpl,
ParentId = cultistCircleStashId,
SlotId = CircleOfCultistService.CircleOfCultistSlotId,
Upd = new Upd
{
StackObjectsCount = stackSize,
SpawnedInSession = true,
},
},
];
// Edge case - item is ammo container and needs cartridges added
if (_itemHelper.IsOfBaseclass(rewardTpl, BaseClasses.AMMO_BOX))
{
var itemDetails = _itemHelper.GetItem(rewardTpl).Value;
_itemHelper.AddCartridgesToAmmoBox(rewardItem, itemDetails);
}
rewards.Add(rewardItem);
}
// Direct reward is not repeatable, flag collected in profile
if (!directReward.Repeatable)
{
FlagDirectRewardAsAcceptedInProfile(sessionId, directReward);
}
return rewards;
}
/// <summary>
@@ -171,7 +483,28 @@ public class CircleOfCultistService
Dictionary<string, DirectRewardSettings> directRewardsCache
)
{
throw new NotImplementedException();
// Get sacrificed tpls
var sacrificedItemTpls = sacrificedItems.Select((item) => item.Template).ToList();
sacrificedItemTpls.Sort();
// Create md5 key of the items player sacrificed so we can compare against the direct reward cache
var sacrificedItemsKey = _hashUtil.GenerateMd5ForData(string.Concat(sacrificedItemTpls, ","));
var matchingDirectReward = directRewardsCache.GetValueOrDefault(sacrificedItemsKey);
if (matchingDirectReward is null)
{
// No direct reward
return null;
}
var fullProfile = _profileHelper.GetFullProfile(sessionId);
var directRewardHash = GetDirectRewardHashKey(matchingDirectReward);
if (fullProfile.SptData.CultistRewards?.ContainsKey(directRewardHash) ?? false)
{
// Player has already received this direct reward
return null;
}
return matchingDirectReward;
}
/// <summary>
@@ -181,7 +514,15 @@ public class CircleOfCultistService
/// <returns>Key</returns>
protected string GetDirectRewardHashKey(DirectRewardSettings directReward)
{
throw new NotImplementedException();
directReward.RequiredItems.Sort();
directReward.Reward.Sort();
var required = string.Concat(directReward.RequiredItems, ",");
var reward = string.Concat(directReward.Reward, ",");
// Key is sacrificed items separated by commas, a dash, then the rewards separated by commas
var key = $"{{{required}-{reward}}}";
return _hashUtil.GenerateMd5ForData(key);
}
/// <summary>
@@ -191,7 +532,20 @@ public class CircleOfCultistService
/// <returns>stack size of item</returns>
protected int GetDirectRewardBaseTypeStackSize(string rewardTpl)
{
throw new NotImplementedException();
var itemDetails = _itemHelper.GetItem(rewardTpl);
if (!itemDetails.Key) {
_logger.Warning($"{rewardTpl} is not an item, setting stack size to 1");
return 1;
}
// Look for parent in dict
var settings = _hideoutConfig.CultistCircle.DirectRewardStackSize[itemDetails.Value.Parent];
if (settings is null) {
return 1;
}
return _randomUtil.GetInt((int)settings.Min, (int)settings.Max);
}
/// <summary>
@@ -201,7 +555,14 @@ public class CircleOfCultistService
/// <param name="directReward">Reward sent to player</param>
protected void FlagDirectRewardAsAcceptedInProfile(string sessionId, DirectRewardSettings directReward)
{
throw new NotImplementedException();
var fullProfile = _profileHelper.GetFullProfile(sessionId);
AcceptedCultistReward dataToStoreInProfile = new AcceptedCultistReward {
Timestamp = _timeUtil.GetTimeStamp(),
SacrificeItems = directReward.RequiredItems,
RewardItems = directReward.Reward,
};
fullProfile.SptData.CultistRewards[GetDirectRewardHashKey(directReward)] = dataToStoreInProfile;
}
/// <summary>
@@ -213,7 +574,31 @@ public class CircleOfCultistService
/// <returns>Size of stack</returns>
protected int GetRewardStackSize(string itemTpl, int rewardPoolRemaining)
{
throw new NotImplementedException();
if (_itemHelper.IsOfBaseclass(itemTpl, BaseClasses.AMMO)) {
var ammoTemplate = _itemHelper.GetItem(itemTpl).Value;
return _itemHelper.GetRandomisedAmmoStackSize(ammoTemplate);
}
if (_itemHelper.IsOfBaseclass(itemTpl, BaseClasses.MONEY)) {
// Get currency-specific values from config
var settings = _hideoutConfig.CultistCircle.CurrencyRewards[itemTpl];
// What % of the pool remaining should be rewarded as chosen currency
var percentOfPoolToUse = _randomUtil.GetInt((int)settings.Min, (int)settings.Max);
// Rouble amount of pool we want to reward as currency
var roubleAmountToFill = _randomUtil.GetPercentOfValue(percentOfPoolToUse, rewardPoolRemaining);
// Convert currency to roubles
var currencyPriceAsRouble = _itemHelper.GetItemPrice(itemTpl);
// How many items can we fit into chosen pool
var itemCountToReward = Math.Round((roubleAmountToFill / currencyPriceAsRouble) ?? 0);
return (int)itemCountToReward;
}
return 1;
}
/// <summary>
@@ -224,13 +609,64 @@ public class CircleOfCultistService
/// <param name="rewardType">Do we return bonus items (hideout/task items)</param>
/// <param name="cultistCircleConfig">Circle config</param>
/// <returns>Array of tpls</returns>
protected string[] GetCultistCircleRewardPool(
protected List<string> GetCultistCircleRewardPool(
string sessionId,
PmcData pmcData,
CircleCraftDetails craftingInfo,
CultistCircleSettings cultistCircleConfig)
{
throw new NotImplementedException();
var rewardPool = new HashSet<string>();
var hideoutDbData = _databaseService.GetHideout();
var itemsDb = _databaseService.GetItems();
// Get all items that match the blacklisted types and fold into item blacklist below
var itemTypeBlacklist = _itemFilterService.GetItemRewardBaseTypeBlacklist();
var itemsMatchingTypeBlacklist = itemsDb
.Where((templateItem) => _itemHelper.IsOfBaseclasses(templateItem.Key, itemTypeBlacklist))
.Select((templateItem) => templateItem.Key);
// Create set of unique values to ignore
var itemRewardBlacklist = new HashSet<string>();
itemRewardBlacklist.UnionWith(_seasonalEventService.GetInactiveSeasonalEventItems());
itemRewardBlacklist.UnionWith(_itemFilterService.GetItemRewardBlacklist());
itemRewardBlacklist.UnionWith(cultistCircleConfig.RewardItemBlacklist);
itemRewardBlacklist.UnionWith(itemsMatchingTypeBlacklist);
// Hideout and task rewards are ONLY if the bonus is active
switch (craftingInfo.RewardType) {
case CircleRewardType.RANDOM: {
// Does reward pass the high value threshold
var isHighValueReward = craftingInfo.RewardAmountRoubles >= cultistCircleConfig.HighValueThresholdRub;
GenerateRandomisedItemsAndAddToRewardPool(rewardPool, itemRewardBlacklist, isHighValueReward);
break;
}
case CircleRewardType.HIDEOUT_TASK: {
// Hideout/Task loot
AddHideoutUpgradeRequirementsToRewardPool(hideoutDbData, pmcData, itemRewardBlacklist, rewardPool);
AddTaskItemRequirementsToRewardPool(pmcData, itemRewardBlacklist, rewardPool);
// If we have no tasks or hideout stuff left or need more loot to fill it out, default to high value
if (rewardPool.Count < cultistCircleConfig.MaxRewardItemCount + 2) {
GenerateRandomisedItemsAndAddToRewardPool(rewardPool, itemRewardBlacklist, true);
}
break;
}
}
// Add custom rewards from config
if (cultistCircleConfig.AdditionalRewardItemPool.Count > 0) {
foreach (var additionalReward in cultistCircleConfig.AdditionalRewardItemPool) {
if (itemRewardBlacklist.Contains(additionalReward)) {
continue;
}
// Add tpl to reward pool
rewardPool.Add(additionalReward);
}
}
return rewardPool.ToList();
}
/// <summary>
@@ -255,12 +691,35 @@ public class CircleOfCultistService
/// <param name="itemRewardBlacklist">Items not to add to pool</param>
/// <param name="rewardPool">Pool to add items to</param>
protected void AddHideoutUpgradeRequirementsToRewardPool(
Hideout hideoutDbData,
Core.Models.Spt.Hideout.Hideout hideoutDbData,
PmcData pmcData,
HashSet<string> itemRewardBlacklist,
HashSet<string> rewardPool)
{
throw new NotImplementedException();
var dbAreas = hideoutDbData.Areas;
foreach (var profileArea in GetPlayerAccessibleHideoutAreas(pmcData.Hideout.Areas)) {
var currentStageLevel = profileArea.Level;
var areaType = profileArea.Type;
// Get next stage of area
var dbArea = dbAreas.FirstOrDefault((area) => area.Type == areaType);
var nextStageDbData = dbArea?.Stages[(currentStageLevel + 1).ToString()];
if (nextStageDbData is not null) {
// Next stage exists, gather up requirements and add to pool
var itemRequirements = GetItemRequirements(nextStageDbData.Requirements);
foreach (var rewardToAdd in itemRequirements) {
if (
itemRewardBlacklist.Contains(rewardToAdd.TemplateId) ||
!_itemHelper.IsValidItem(rewardToAdd.TemplateId)
) {
// Dont reward items sacrificed
continue;
}
_logger.Debug($"Added Hideout Loot: {_itemHelper.GetItemName(rewardToAdd.TemplateId)}");
rewardPool.Add(rewardToAdd.TemplateId);
}
}
}
}
/// <summary>
@@ -268,9 +727,16 @@ public class CircleOfCultistService
/// </summary>
/// <param name="areas">Hideout areas to iterate over</param>
/// <returns>Active area array</returns>
protected BotHideoutArea[] GetPlayerAccessibleHideoutAreas(BotHideoutArea[] areas)
protected List<BotHideoutArea> GetPlayerAccessibleHideoutAreas(List<BotHideoutArea> areas)
{
throw new NotImplementedException();
return areas.Where((area) => {
if (area.Type == HideoutAreas.CHRISTMAS_TREE && !_seasonalEventService.ChristmasEventEnabled()) {
// Christmas tree area and not Christmas, skip
return false;
}
return true;
}).ToList();
}
/// <summary>
@@ -285,7 +751,33 @@ public class CircleOfCultistService
HashSet<string> itemRewardBlacklist,
bool itemsShouldBeHighValue)
{
throw new NotImplementedException();
var allItems = _itemHelper.GetItems();
var currentItemCount = 0;
var attempts = 0;
// `currentItemCount` var will look for the correct number of items, `attempts` var will keep this from never stopping if the highValueThreshold is too high
while (
currentItemCount < _hideoutConfig.CultistCircle.MaxRewardItemCount + 2 &&
attempts < allItems.Count
) {
attempts++;
var randomItem = _randomUtil.GetArrayValue(allItems);
if (itemRewardBlacklist.Contains(randomItem.Id) || !_itemHelper.IsValidItem(randomItem.Id)) {
continue;
}
// Valuable check
if (itemsShouldBeHighValue) {
var itemValue = _itemHelper.GetItemMaxPrice(randomItem.Id);
if (itemValue < _hideoutConfig.CultistCircle.HighValueThresholdRub) {
continue;
}
}
_logger.Debug($"Added: {_itemHelper.GetItemName(randomItem.Id)}");
rewardPool.Add(randomItem.Id);
currentItemCount++;
}
return rewardPool;
}
/// <summary>
@@ -293,8 +785,81 @@ public class CircleOfCultistService
/// </summary>
/// <param name="requirements">Requirements to iterate over</param>
/// <returns>Array of item requirements</returns>
protected (StageRequirement[] StageRequirement, Requirement[] Requirement) GetItemRequirements(RequirementBase[] requirements)
protected List<StageRequirement> GetItemRequirements(List<StageRequirement> requirements)
{
throw new NotImplementedException();
return requirements.Where((requirement) => requirement.Type == "Item").ToList();
}
/// <summary>
/// Iterate over passed in hideout requirements and return the Item
/// </summary>
/// <param name="requirements">Requirements to iterate over</param>
/// <returns>Array of item requirements</returns>
protected List<Requirement> GetItemRequirements(List<Requirement> requirements)
{
return requirements.Where((requirement) => requirement.Type == "Item").ToList();
}
/// <summary>
/// Create a map of the possible direct rewards, keyed by the items needed to be sacrificed
/// </summary>
/// <param name="directRewards">Direct rewards array from hideout config</param>
/// <returns>Dictionary</returns>
protected Dictionary<string, DirectRewardSettings> GenerateSacrificedItemsCache(List<DirectRewardSettings> directRewards)
{
var result = new Dictionary<string, DirectRewardSettings>();
foreach (var rewardSettings in directRewards) {
rewardSettings.RequiredItems.Sort();
var concat = string.Concat(rewardSettings.RequiredItems, ",");
var key = _hashUtil.GenerateMd5ForData(concat);
result[key] = rewardSettings;
}
return result;
}
/// <summary>
/// Attempt to add all rewards to cultist circle, if they don't fit remove one and try again until they fit
/// </summary>
/// <param name="sessionId">Session id</param>
/// <param name="pmcData">Player profile</param>
/// <param name="rewards">Rewards to send to player</param>
/// <param name="containerGrid">Cultist grid to add rewards to</param>
/// <param name="cultistCircleStashId">Stash id</param>
/// <param name="output">Client output</param>
protected void AddRewardsToCircleContainer(
string sessionId,
PmcData pmcData,
List<List<Item>> rewards,
int[][] containerGrid,
string cultistCircleStashId,
ItemEventRouterResponse output
)
{
var canAddToContainer = false;
while (!canAddToContainer && rewards.Count > 0) {
canAddToContainer = _inventoryHelper.CanPlaceItemsInContainer(
_cloner.Clone(containerGrid), // MUST clone grid before passing in as function modifies grid
rewards
);
// Doesn't fit, remove one item
if (!canAddToContainer) {
rewards.PopFirst();
}
}
foreach (var itemToAdd in rewards) {
_inventoryHelper.PlaceItemInContainer(
containerGrid,
itemToAdd,
cultistCircleStashId,
CircleOfCultistService.CircleOfCultistSlotId
);
// Add item + mods to output and profile inventory
output.ProfileChanges[sessionId].Items.NewItems.AddRange(itemToAdd);
pmcData.Inventory.Items.AddRange(itemToAdd);
}
}
}