1378 lines
55 KiB
C#
1378 lines
55 KiB
C#
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.Hideout;
|
|
using SPTarkov.Server.Core.Models.Eft.Inventory;
|
|
using SPTarkov.Server.Core.Models.Eft.ItemEvent;
|
|
using SPTarkov.Server.Core.Models.Enums;
|
|
using SPTarkov.Server.Core.Models.Utils;
|
|
using SPTarkov.Server.Core.Routers;
|
|
using SPTarkov.Server.Core.Services;
|
|
using SPTarkov.Server.Core.Utils;
|
|
using SPTarkov.Server.Core.Utils.Cloners;
|
|
using LogLevel = SPTarkov.Server.Core.Models.Spt.Logging.LogLevel;
|
|
|
|
namespace SPTarkov.Server.Core.Helpers;
|
|
|
|
[Injectable]
|
|
public class HideoutHelper(
|
|
ISptLogger<HideoutHelper> logger,
|
|
TimeUtil timeUtil,
|
|
ServerLocalisationService serverLocalisationService,
|
|
DatabaseService databaseService,
|
|
EventOutputHolder eventOutputHolder,
|
|
HttpResponseUtil httpResponseUtil,
|
|
ProfileHelper profileHelper,
|
|
InventoryHelper inventoryHelper,
|
|
ItemHelper itemHelper,
|
|
NotificationSendHelper notificationSendHelper,
|
|
NotifierHelper notifierHelper,
|
|
ICloner cloner
|
|
)
|
|
{
|
|
public static readonly MongoId BitcoinProductionId = new("5d5c205bd582a50d042a3c0e");
|
|
public static readonly MongoId WaterCollectorId = new("5d5589c1f934db045e6c5492");
|
|
public const int MaxSkillPoint = 5000;
|
|
|
|
/// <summary>
|
|
/// Add production to profiles' Hideout.Production array
|
|
/// </summary>
|
|
/// <param name="pmcData">Profile to add production to</param>
|
|
/// <param name="productionRequest">Production request</param>
|
|
/// <param name="sessionID">Session id</param>
|
|
/// <returns>client response</returns>
|
|
public void RegisterProduction(PmcData pmcData, HideoutSingleProductionStartRequestData productionRequest, MongoId sessionID)
|
|
{
|
|
var recipe = databaseService
|
|
.GetHideout()
|
|
.Production.Recipes.FirstOrDefault(production => production.Id == productionRequest.RecipeId);
|
|
if (recipe is null)
|
|
{
|
|
logger.Error(serverLocalisationService.GetText("hideout-missing_recipe_in_db", productionRequest.RecipeId));
|
|
|
|
httpResponseUtil.AppendErrorToOutput(eventOutputHolder.GetOutput(sessionID));
|
|
}
|
|
|
|
// @Important: Here we need to be very exact:
|
|
// - normal recipe: Production time value is stored in attribute "productionType" with small "p"
|
|
// - scav case recipe: Production time value is stored in attribute "ProductionType" with capital "P"
|
|
pmcData.Hideout.Production ??= [];
|
|
|
|
var modifiedProductionTime = GetAdjustedCraftTimeWithSkills(pmcData, productionRequest.RecipeId);
|
|
|
|
var production = InitProduction(productionRequest.RecipeId, modifiedProductionTime ?? 0, recipe.NeedFuelForAllProductionTime);
|
|
|
|
// Store the tools used for this production, so we can return them later
|
|
if (productionRequest.Tools?.Count > 0)
|
|
{
|
|
production.SptRequiredTools = [];
|
|
|
|
foreach (var tool in productionRequest.Tools)
|
|
{
|
|
var toolItem = cloner.Clone(pmcData.Inventory.Items.FirstOrDefault(x => x.Id == tool.Id));
|
|
if (toolItem is null)
|
|
{
|
|
logger.Warning($"Unable to find tool item: {tool.Id}");
|
|
|
|
continue;
|
|
}
|
|
|
|
// Make sure we only return as many as we took
|
|
toolItem.AddUpd();
|
|
|
|
toolItem.Upd.StackObjectsCount = tool.Count;
|
|
|
|
production.SptRequiredTools.Add(
|
|
new Item
|
|
{
|
|
Id = new MongoId(),
|
|
Template = toolItem.Template,
|
|
Upd = toolItem.Upd,
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
pmcData.Hideout.Production[productionRequest.RecipeId] = production;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add production to profiles' Hideout.Production array
|
|
/// </summary>
|
|
/// <param name="pmcData">Profile to add production to</param>
|
|
/// <param name="productionRequest">Production request</param>
|
|
/// <param name="sessionID">Session id</param>
|
|
/// <returns>client response</returns>
|
|
public void RegisterProduction(PmcData pmcData, HideoutContinuousProductionStartRequestData productionRequest, MongoId sessionID)
|
|
{
|
|
var recipe = databaseService
|
|
.GetHideout()
|
|
.Production.Recipes.FirstOrDefault(production => production.Id == productionRequest.RecipeId);
|
|
if (recipe is null)
|
|
{
|
|
logger.Error(serverLocalisationService.GetText("hideout-missing_recipe_in_db", productionRequest.RecipeId));
|
|
|
|
httpResponseUtil.AppendErrorToOutput(eventOutputHolder.GetOutput(sessionID));
|
|
}
|
|
|
|
// @Important: Here we need to be very exact:
|
|
// - normal recipe: Production time value is stored in attribute "productionType" with small "p"
|
|
// - scav case recipe: Production time value is stored in attribute "ProductionType" with capital "P"
|
|
pmcData.Hideout.Production ??= [];
|
|
|
|
var modifiedProductionTime = GetAdjustedCraftTimeWithSkills(pmcData, productionRequest.RecipeId.Value);
|
|
|
|
var production = InitProduction(productionRequest.RecipeId.Value, modifiedProductionTime ?? 0, recipe.NeedFuelForAllProductionTime);
|
|
|
|
pmcData.Hideout.Production[productionRequest.RecipeId.Value] = production;
|
|
}
|
|
|
|
/// <summary>
|
|
/// This convenience function initializes new Production Object
|
|
/// with all the constants.
|
|
/// </summary>
|
|
public Production InitProduction(MongoId recipeId, double productionTime, bool? needFuelForAllProductionTime)
|
|
{
|
|
return new Production
|
|
{
|
|
Progress = 0,
|
|
InProgress = true,
|
|
RecipeId = recipeId,
|
|
StartTimestamp = timeUtil.GetTimeStamp(),
|
|
ProductionTime = productionTime,
|
|
Products = [],
|
|
GivenItemsInStart = [],
|
|
Interrupted = false,
|
|
NeedFuelForAllProductionTime = needFuelForAllProductionTime, // Used when sending to client
|
|
needFuelForAllProductionTime = needFuelForAllProductionTime, // used when stored in production.json
|
|
SkipTime = 0,
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Apply bonus to player profile given after completing hideout upgrades
|
|
/// </summary>
|
|
/// <param name="profileData">Profile to add bonus to</param>
|
|
/// <param name="bonus">Bonus to add to profile</param>
|
|
public void ApplyPlayerUpgradesBonuses(PmcData profileData, Bonus bonus)
|
|
{
|
|
// Handle additional changes some bonuses need before being added
|
|
switch (bonus.Type)
|
|
{
|
|
case BonusType.StashSize:
|
|
{
|
|
// Find stash item and adjust tpl to new tpl from bonus
|
|
var stashItem = profileData.Inventory.Items.FirstOrDefault(x => x.Id == profileData.Inventory.Stash);
|
|
if (stashItem is null)
|
|
{
|
|
logger.Warning(
|
|
serverLocalisationService.GetText(
|
|
"hideout-unable_to_apply_stashsize_bonus_no_stash_found",
|
|
profileData.Inventory.Stash
|
|
)
|
|
);
|
|
}
|
|
|
|
stashItem.Template = bonus.TemplateId.Value;
|
|
|
|
break;
|
|
}
|
|
case BonusType.MaximumEnergyReserve:
|
|
// Amend max energy in profile
|
|
profileData.Health.Energy.Maximum += bonus.Value;
|
|
break;
|
|
case BonusType.TextBonus:
|
|
// Delete values before they're added to profile
|
|
bonus.IsPassive = null;
|
|
bonus.IsProduction = null;
|
|
bonus.IsVisible = null;
|
|
break;
|
|
}
|
|
|
|
// Add bonus to player bonuses array in profile
|
|
// EnergyRegeneration, HealthRegeneration, RagfairCommission, ScavCooldownTimer, SkillGroupLevelingBoost, ExperienceRate, QuestMoneyReward etc
|
|
if (logger.IsLogEnabled(LogLevel.Debug))
|
|
{
|
|
logger.Debug($"Adding bonus: {bonus.Type} to profile, value: {bonus.Value}");
|
|
}
|
|
|
|
profileData.Bonuses.Add(bonus);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Process a players hideout, update areas that use resources + increment production timers
|
|
/// </summary>
|
|
/// <param name="sessionID">Session id</param>
|
|
public void UpdatePlayerHideout(MongoId sessionID)
|
|
{
|
|
var pmcData = profileHelper.GetPmcProfile(sessionID);
|
|
var hideoutProperties = GetHideoutProperties(pmcData);
|
|
|
|
pmcData.Hideout.SptUpdateLastRunTimestamp ??= timeUtil.GetTimeStamp();
|
|
|
|
UpdateAreasWithResources(sessionID, pmcData, hideoutProperties);
|
|
UpdateProductionTimers(pmcData, hideoutProperties);
|
|
pmcData.Hideout.SptUpdateLastRunTimestamp = timeUtil.GetTimeStamp();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get various properties that will be passed to hideout update-related functions
|
|
/// </summary>
|
|
/// <param name="pmcData">Player profile</param>
|
|
/// <returns>Hideout-related values</returns>
|
|
protected HideoutProperties GetHideoutProperties(PmcData pmcData)
|
|
{
|
|
var bitcoinFarm = pmcData.Hideout.Areas.FirstOrDefault(area => area.Type == HideoutAreas.BitcoinFarm);
|
|
var bitcoinCount = (bitcoinFarm?.Slots).Count(slot => slot.Items is not null); // Get slots with an item property
|
|
|
|
var hideoutProperties = new HideoutProperties
|
|
{
|
|
BtcFarmGcs = bitcoinCount,
|
|
IsGeneratorOn = pmcData.Hideout.Areas.FirstOrDefault(area => area.Type == HideoutAreas.Generator)?.Active ?? false,
|
|
WaterCollectorHasFilter = DoesWaterCollectorHaveFilter(
|
|
pmcData.Hideout.Areas.FirstOrDefault(area => area.Type == HideoutAreas.WaterCollector)
|
|
),
|
|
};
|
|
|
|
return hideoutProperties;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Does a water collection hideout area have a water filter installed
|
|
/// </summary>
|
|
/// <param name="waterCollector">Hideout area to check</param>
|
|
/// <returns></returns>
|
|
protected static bool DoesWaterCollectorHaveFilter(BotHideoutArea waterCollector)
|
|
{
|
|
// Can put filters in from L3
|
|
if (waterCollector.Level == 3)
|
|
// Has filter in at least one slot
|
|
{
|
|
return waterCollector.Slots.Any(slot => slot.Items is not null);
|
|
}
|
|
|
|
// No Filter
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Iterate over productions and update their progress timers
|
|
/// </summary>
|
|
/// <param name="pmcData">Profile to check for productions and update</param>
|
|
/// <param name="hideoutProperties">Hideout properties</param>
|
|
protected void UpdateProductionTimers(PmcData pmcData, HideoutProperties hideoutProperties)
|
|
{
|
|
var recipes = databaseService.GetHideout().Production;
|
|
|
|
// Check each production and handle edge cases if necessary
|
|
foreach (var prodId in pmcData.Hideout?.Production)
|
|
{
|
|
if (pmcData.Hideout.Production.TryGetValue(prodId.Key, out var craft) && craft is null)
|
|
{
|
|
// Craft value is undefined, get rid of it (could be from cancelling craft that needs cleaning up)
|
|
pmcData.Hideout.Production.Remove(prodId.Key);
|
|
|
|
continue;
|
|
}
|
|
|
|
if (craft.Progress == null)
|
|
{
|
|
logger.Warning(serverLocalisationService.GetText("hideout-craft_has_undefined_progress_value_defaulting", prodId));
|
|
craft.Progress = 0;
|
|
}
|
|
|
|
// Skip processing (Don't skip continuous crafts like bitcoin farm or cultist circle)
|
|
if (craft.IsCraftComplete())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Special handling required
|
|
if (craft.IsCraftOfType(HideoutAreas.ScavCase))
|
|
{
|
|
UpdateScavCaseProductionTimer(pmcData, prodId.Key);
|
|
|
|
continue;
|
|
}
|
|
|
|
if (craft.IsCraftOfType(HideoutAreas.WaterCollector))
|
|
{
|
|
UpdateWaterCollectorProductionTimer(pmcData, prodId.Key, hideoutProperties);
|
|
|
|
continue;
|
|
}
|
|
|
|
// Continuous craft
|
|
if (craft.IsCraftOfType(HideoutAreas.BitcoinFarm))
|
|
{
|
|
UpdateBitcoinFarm(
|
|
pmcData,
|
|
pmcData.Hideout.Production.GetValueOrDefault(prodId.Key),
|
|
hideoutProperties.BtcFarmGcs,
|
|
hideoutProperties.IsGeneratorOn
|
|
);
|
|
|
|
continue;
|
|
}
|
|
|
|
// No recipe, needs special handling
|
|
if (craft.IsCraftOfType(HideoutAreas.CircleOfCultists))
|
|
{
|
|
UpdateCultistCircleCraftProgress(pmcData, prodId.Key);
|
|
|
|
continue;
|
|
}
|
|
|
|
// Ensure recipe exists before using it in updateProductionProgress()
|
|
var recipe = recipes?.Recipes?.FirstOrDefault(r => r.Id == prodId.Key);
|
|
if (recipe is null)
|
|
{
|
|
logger.Error(serverLocalisationService.GetText("hideout-missing_recipe_for_area", prodId));
|
|
|
|
continue;
|
|
}
|
|
|
|
UpdateProductionProgress(pmcData, prodId.Key, recipe, hideoutProperties);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update progress timer for water collector
|
|
/// </summary>
|
|
/// <param name="pmcData">profile to update</param>
|
|
/// <param name="productionId">id of water collection production to update</param>
|
|
/// <param name="hideoutProperties">Hideout properties</param>
|
|
protected void UpdateWaterCollectorProductionTimer(PmcData pmcData, string productionId, HideoutProperties hideoutProperties)
|
|
{
|
|
var timeElapsed = GetTimeElapsedSinceLastServerTick(pmcData, hideoutProperties.IsGeneratorOn);
|
|
if (hideoutProperties.WaterCollectorHasFilter)
|
|
{
|
|
pmcData.Hideout.Production[productionId].Progress += timeElapsed;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update a productions progress value based on the amount of time that has passed
|
|
/// </summary>
|
|
/// <param name="pmcData">Player profile</param>
|
|
/// <param name="prodId">Production id being crafted</param>
|
|
/// <param name="recipe">Recipe data being crafted</param>
|
|
/// <param name="hideoutProperties"></param>
|
|
protected void UpdateProductionProgress(PmcData pmcData, string prodId, HideoutProduction recipe, HideoutProperties hideoutProperties)
|
|
{
|
|
// Production is complete, no need to do any calculations
|
|
if (DoesProgressMatchProductionTime(pmcData, prodId))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Get seconds since last hideout update + now
|
|
var timeElapsed = GetTimeElapsedSinceLastServerTick(pmcData, hideoutProperties.IsGeneratorOn, recipe);
|
|
|
|
// Increment progress by time passed
|
|
pmcData.Hideout.Production.TryGetValue(prodId, out var production);
|
|
|
|
// Some items NEED power to craft (e.g. DSP)
|
|
if (production.needFuelForAllProductionTime.GetValueOrDefault() && hideoutProperties.IsGeneratorOn)
|
|
{
|
|
production.Progress += timeElapsed;
|
|
}
|
|
else if (!production.needFuelForAllProductionTime.GetValueOrDefault())
|
|
// Increment progress if production does not necessarily need fuel to continue
|
|
{
|
|
production.Progress += timeElapsed;
|
|
}
|
|
|
|
// Limit progress to total production time if progress is over (don't run for continuous crafts)
|
|
if (!(recipe.Continuous ?? false))
|
|
// If progress is larger than prod time, return ProductionTime, hard cap the value
|
|
{
|
|
production.Progress = Math.Min(production.Progress ?? 0, production.ProductionTime ?? 0);
|
|
}
|
|
}
|
|
|
|
protected void UpdateCultistCircleCraftProgress(PmcData pmcData, string prodId)
|
|
{
|
|
pmcData.Hideout.Production.TryGetValue(prodId, out var production);
|
|
|
|
// Check if we're already complete, skip
|
|
if ((production.AvailableForFinish ?? false) && !production.InProgress.GetValueOrDefault(false))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Get seconds since last hideout update
|
|
var timeElapsedSeconds = timeUtil.GetTimeStamp() - pmcData.Hideout.SptUpdateLastRunTimestamp;
|
|
|
|
// Increment progress by time passed if progress is less than time needed
|
|
if (production.Progress < production.ProductionTime)
|
|
{
|
|
production.Progress += timeElapsedSeconds;
|
|
|
|
// Check if craft is complete
|
|
if (production.Progress >= production.ProductionTime)
|
|
{
|
|
FlagCultistCircleCraftAsComplete(production);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Craft in complete
|
|
FlagCultistCircleCraftAsComplete(production);
|
|
}
|
|
|
|
protected static void FlagCultistCircleCraftAsComplete(Production production)
|
|
{
|
|
// Craft is complete, flag as such
|
|
production.AvailableForFinish = true;
|
|
|
|
// The client expects `Progress` to be 0, and `inProgress` to be false when a circle is complete
|
|
production.Progress = 0;
|
|
production.InProgress = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check if a productions progress value matches its corresponding recipes production time value
|
|
/// </summary>
|
|
/// <param name="pmcData">Player profile</param>
|
|
/// <param name="prodId">Production id</param>
|
|
/// <returns>progress matches productionTime from recipe</returns>
|
|
protected static bool DoesProgressMatchProductionTime(PmcData pmcData, string prodId)
|
|
{
|
|
return pmcData.Hideout.Production[prodId].Progress == pmcData.Hideout.Production[prodId].ProductionTime;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update progress timer for scav case
|
|
/// </summary>
|
|
/// <param name="pmcData">Profile to update</param>
|
|
/// <param name="productionId">Id of scav case production to update</param>
|
|
protected void UpdateScavCaseProductionTimer(PmcData pmcData, string productionId)
|
|
{
|
|
var timeElapsed =
|
|
timeUtil.GetTimeStamp()
|
|
- pmcData.Hideout.Production[productionId].StartTimestamp
|
|
- pmcData.Hideout.Production[productionId].Progress;
|
|
|
|
pmcData.Hideout.Production[productionId].Progress += timeElapsed;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Iterate over hideout areas that use resources (fuel/filters etc) and update associated values
|
|
/// </summary>
|
|
/// <param name="sessionID">Session id</param>
|
|
/// <param name="pmcData">Profile to update areas of</param>
|
|
/// <param name="hideoutProperties">hideout properties</param>
|
|
protected void UpdateAreasWithResources(MongoId sessionID, PmcData pmcData, HideoutProperties hideoutProperties)
|
|
{
|
|
foreach (var area in pmcData.Hideout.Areas)
|
|
{
|
|
switch (area.Type)
|
|
{
|
|
case HideoutAreas.Generator:
|
|
if (hideoutProperties.IsGeneratorOn)
|
|
{
|
|
UpdateFuel(area, pmcData, hideoutProperties.IsGeneratorOn);
|
|
}
|
|
|
|
break;
|
|
case HideoutAreas.WaterCollector:
|
|
UpdateWaterCollector(sessionID, pmcData, area, hideoutProperties);
|
|
break;
|
|
|
|
case HideoutAreas.AirFilteringUnit:
|
|
if (hideoutProperties.IsGeneratorOn)
|
|
{
|
|
UpdateAirFilters(area, pmcData, hideoutProperties.IsGeneratorOn);
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Decrease fuel from generator slots based on amount of time since last time this occurred
|
|
/// </summary>
|
|
/// <param name="generatorArea">Hideout area</param>
|
|
/// <param name="pmcData">Player profile</param>
|
|
/// <param name="isGeneratorOn">Is the generator turned on since last update</param>
|
|
protected void UpdateFuel(BotHideoutArea generatorArea, PmcData pmcData, bool isGeneratorOn)
|
|
{
|
|
// 1 resource last 14 min 27 sec, 1/14.45/60 = 0.00115
|
|
// 10-10-2021 From wiki, 1 resource last 12 minutes 38 seconds, 1/12.63333/60 = 0.00131
|
|
var fuelUsedSinceLastTick =
|
|
databaseService.GetHideout().Settings.GeneratorFuelFlowRate * GetTimeElapsedSinceLastServerTick(pmcData, isGeneratorOn);
|
|
|
|
// Get all fuel consumption bonuses, returns an empty array if none found
|
|
var profileFuelConsomptionBonusSum = pmcData.GetBonusValueFromProfile(BonusType.FuelConsumption);
|
|
|
|
// An increase in "bonus" consumption is actually an increase in consumption, so invert this for later use
|
|
var fuelConsumptionBonusRate = -(profileFuelConsomptionBonusSum / 100);
|
|
|
|
// An increase in hideout management bonus is a decrease in consumption
|
|
var hideoutManagementConsumptionBonusRate = GetHideoutManagementConsumptionBonus(pmcData);
|
|
|
|
var combinedBonus = 1.0 - (fuelConsumptionBonusRate + hideoutManagementConsumptionBonusRate);
|
|
|
|
// Sanity check, never let fuel consumption go negative, otherwise it returns fuel to the player
|
|
if (combinedBonus < 0)
|
|
{
|
|
combinedBonus = 0;
|
|
}
|
|
|
|
fuelUsedSinceLastTick *= combinedBonus;
|
|
|
|
var hasFuelRemaining = false;
|
|
double pointsConsumed;
|
|
for (var i = 0; i < generatorArea.Slots.Count; i++)
|
|
{
|
|
var generatorSlot = generatorArea.Slots[i];
|
|
if (generatorSlot?.Items is null)
|
|
// No item in slot, skip
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var fuelItemInSlot = generatorSlot?.Items.FirstOrDefault();
|
|
if (fuelItemInSlot is null)
|
|
// No item in slot, skip
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var fuelRemaining = fuelItemInSlot.Upd?.Resource?.Value;
|
|
if (fuelRemaining == 0)
|
|
// No fuel left, skip
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Undefined fuel, fresh fuel item and needs its max fuel amount looked up
|
|
if (fuelRemaining is null)
|
|
{
|
|
var fuelItemTemplate = itemHelper.GetItem(fuelItemInSlot.Template).Value;
|
|
pointsConsumed = fuelUsedSinceLastTick ?? 0;
|
|
fuelRemaining = fuelItemTemplate.Properties.MaxResource - fuelUsedSinceLastTick;
|
|
}
|
|
else
|
|
{
|
|
// Fuel exists already, deduct fuel from item remaining value
|
|
pointsConsumed = (double)((fuelItemInSlot.Upd.Resource.UnitsConsumed ?? 0) + fuelUsedSinceLastTick);
|
|
fuelRemaining -= fuelUsedSinceLastTick;
|
|
}
|
|
|
|
// Round values to keep accuracy
|
|
fuelRemaining = Math.Round(fuelRemaining * 10000 ?? 0) / 10000;
|
|
pointsConsumed = Math.Round(pointsConsumed * 10000) / 10000;
|
|
|
|
// Fuel consumed / 10 is over 1, add hideout management skill point
|
|
if (pmcData is not null && Math.Floor(pointsConsumed / 10) >= 1)
|
|
{
|
|
profileHelper.AddSkillPointsToPlayer(pmcData, SkillTypes.HideoutManagement, 1);
|
|
pointsConsumed -= 10;
|
|
}
|
|
|
|
var isFuelItemFoundInRaid = fuelItemInSlot.Upd?.SpawnedInSession ?? false;
|
|
if (fuelRemaining > 0)
|
|
{
|
|
// Deducted all used fuel from this container, clean up and exit loop
|
|
fuelItemInSlot.Upd = GetAreaUpdObject(1, fuelRemaining, pointsConsumed, isFuelItemFoundInRaid);
|
|
|
|
if (logger.IsLogEnabled(LogLevel.Debug))
|
|
{
|
|
logger.Debug($"Profile: {pmcData.Id} Generator has: {fuelRemaining} fuel left in slot {i + 1}");
|
|
}
|
|
|
|
hasFuelRemaining = true;
|
|
|
|
break; // Break to avoid updating all the fuel tanks
|
|
}
|
|
|
|
fuelItemInSlot.Upd = GetAreaUpdObject(1, 0, 0, isFuelItemFoundInRaid);
|
|
|
|
// Ran out of fuel items to deduct fuel from
|
|
fuelUsedSinceLastTick = Math.Abs(fuelRemaining ?? 0);
|
|
if (logger.IsLogEnabled(LogLevel.Debug))
|
|
{
|
|
logger.Debug($"Profile: {pmcData.Id} Generator ran out of fuel");
|
|
}
|
|
}
|
|
|
|
// Out of fuel, flag generator as offline
|
|
if (!hasFuelRemaining)
|
|
{
|
|
generatorArea.Active = false;
|
|
}
|
|
}
|
|
|
|
protected void UpdateWaterCollector(MongoId sessionId, PmcData pmcData, BotHideoutArea area, HideoutProperties hideoutProperties)
|
|
{
|
|
// Skip water collector when not level 3 (cant collect until 3)
|
|
if (area.Level != 3)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!hideoutProperties.WaterCollectorHasFilter)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Canister with purified water craft exists
|
|
if (
|
|
pmcData.Hideout.Production.TryGetValue(WaterCollectorId, out var purifiedWaterCraft)
|
|
&& purifiedWaterCraft.GetType() == typeof(Production)
|
|
)
|
|
{
|
|
// Update craft time to account for increases in players craft time skill
|
|
purifiedWaterCraft.ProductionTime = GetAdjustedCraftTimeWithSkills(pmcData, purifiedWaterCraft.RecipeId, true);
|
|
|
|
UpdateWaterFilters(area, purifiedWaterCraft, hideoutProperties.IsGeneratorOn, pmcData);
|
|
}
|
|
else
|
|
{
|
|
// continuousProductionStart()
|
|
// seem to not trigger consistently
|
|
var recipe = new HideoutSingleProductionStartRequestData
|
|
{
|
|
RecipeId = WaterCollectorId,
|
|
Action = "HideoutSingleProductionStart",
|
|
Items = [],
|
|
Tools = [],
|
|
Timestamp = timeUtil.GetTimeStamp(),
|
|
};
|
|
|
|
RegisterProduction(pmcData, recipe, sessionId);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get craft time and make adjustments to account for dev profile + crafting skill level
|
|
/// </summary>
|
|
/// <param name="pmcData">Player profile making craft</param>
|
|
/// <param name="recipeId">Recipe being crafted</param>
|
|
/// <param name="applyHideoutManagementBonus">Should the hideout management bonus be applied to the calculation</param>
|
|
/// <returns>Items craft time with bonuses subtracted</returns>
|
|
public double? GetAdjustedCraftTimeWithSkills(PmcData pmcData, MongoId recipeId, bool applyHideoutManagementBonus = false)
|
|
{
|
|
var globalSkillsDb = databaseService.GetGlobals().Configuration.SkillsSettings;
|
|
|
|
var recipe = databaseService.GetHideout().Production.Recipes.FirstOrDefault(production => production.Id == recipeId);
|
|
if (recipe is null)
|
|
{
|
|
logger.Error(serverLocalisationService.GetText("hideout-missing_recipe_in_db", recipeId));
|
|
|
|
return null;
|
|
}
|
|
|
|
var timeReductionSeconds = 0D;
|
|
|
|
// Bitcoin farm is excluded from crafting skill cooldown reduction
|
|
if (recipeId != BitcoinProductionId)
|
|
// Seconds to deduct from crafts total time
|
|
{
|
|
timeReductionSeconds += GetSkillProductionTimeReduction(
|
|
pmcData,
|
|
recipe.ProductionTime ?? 0,
|
|
SkillTypes.Crafting,
|
|
globalSkillsDb.Crafting.ProductionTimeReductionPerLevel
|
|
);
|
|
}
|
|
|
|
// Some crafts take into account hideout management, e.g. fuel, water/air filters
|
|
if (applyHideoutManagementBonus)
|
|
{
|
|
timeReductionSeconds += GetSkillProductionTimeReduction(
|
|
pmcData,
|
|
recipe.ProductionTime ?? 0,
|
|
SkillTypes.HideoutManagement,
|
|
globalSkillsDb.HideoutManagement.ConsumptionReductionPerLevel
|
|
);
|
|
}
|
|
|
|
var modifiedProductionTime = recipe.ProductionTime - timeReductionSeconds;
|
|
if (modifiedProductionTime > 0 && profileHelper.IsDeveloperAccount(pmcData.Id.Value))
|
|
{
|
|
modifiedProductionTime = 40;
|
|
}
|
|
|
|
// Sanity check, don't let anything craft in less than 5 seconds
|
|
if (modifiedProductionTime < 5)
|
|
{
|
|
modifiedProductionTime = 5;
|
|
}
|
|
|
|
return modifiedProductionTime;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adjust water filter objects resourceValue or delete when they reach 0 resource
|
|
/// </summary>
|
|
/// <param name="waterFilterArea">Water filter area to update</param>
|
|
/// <param name="production">Production object</param>
|
|
/// <param name="isGeneratorOn">Is generator enabled</param>
|
|
/// <param name="pmcData">Player profile</param>
|
|
protected void UpdateWaterFilters(BotHideoutArea waterFilterArea, Production production, bool isGeneratorOn, PmcData pmcData)
|
|
{
|
|
var filterDrainRate = GetWaterFilterDrainRate(pmcData);
|
|
var craftProductionTime = GetTotalProductionTimeSeconds(WaterCollectorId);
|
|
var secondsSinceServerTick = GetTimeElapsedSinceLastServerTick(pmcData, isGeneratorOn);
|
|
|
|
filterDrainRate = GetTimeAdjustedWaterFilterDrainRate(
|
|
secondsSinceServerTick ?? 0,
|
|
craftProductionTime,
|
|
production.Progress ?? 0,
|
|
filterDrainRate
|
|
);
|
|
|
|
// Check progress against the productions craft time (don't use base time as it doesn't include any time bonuses profile has)
|
|
if (production.Progress > production.ProductionTime)
|
|
// Craft is complete nothing to do
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Check all slots that take water filters until we find one with filter in it
|
|
for (var i = 0; i < waterFilterArea.Slots.Count; i++)
|
|
{
|
|
// No water filter in slot, skip
|
|
if (waterFilterArea.Slots[i].Items is null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var waterFilterItemInSlot = waterFilterArea.Slots[i].Items.FirstOrDefault();
|
|
|
|
// How many units of filter are left
|
|
var resourceValue = waterFilterItemInSlot?.Upd?.Resource?.Value;
|
|
double pointsConsumed;
|
|
if (resourceValue is null)
|
|
{
|
|
// Missing, is new filter, add default and subtract usage
|
|
resourceValue = 100 - filterDrainRate;
|
|
pointsConsumed = filterDrainRate;
|
|
}
|
|
else
|
|
{
|
|
pointsConsumed = (waterFilterItemInSlot.Upd.Resource.UnitsConsumed ?? 0) + filterDrainRate;
|
|
resourceValue -= filterDrainRate;
|
|
}
|
|
|
|
// Round to get values to 3dp
|
|
resourceValue = Math.Round(resourceValue * 1000 ?? 0) / 1000;
|
|
pointsConsumed = Math.Round(pointsConsumed * 1000) / 1000;
|
|
|
|
// Check units consumed for possible increment of hideout mgmt skill point
|
|
if (pmcData is not null && Math.Floor(pointsConsumed / 10) >= 1)
|
|
{
|
|
profileHelper.AddSkillPointsToPlayer(pmcData, SkillTypes.HideoutManagement, 1);
|
|
pointsConsumed -= 10;
|
|
}
|
|
|
|
// Filter has some fuel left in it after our adjustment
|
|
if (resourceValue > 0)
|
|
{
|
|
var isWaterFilterFoundInRaid = waterFilterItemInSlot.Upd?.SpawnedInSession ?? false;
|
|
|
|
// Set filters consumed amount
|
|
waterFilterItemInSlot.Upd = GetAreaUpdObject(1, resourceValue, pointsConsumed, isWaterFilterFoundInRaid);
|
|
if (logger.IsLogEnabled(LogLevel.Debug))
|
|
{
|
|
logger.Debug($"Water filter has: {resourceValue} units left in slot {i + 1}");
|
|
}
|
|
|
|
break; // Break here to avoid iterating other filters now we're done
|
|
}
|
|
|
|
// Filter ran out / used up
|
|
waterFilterArea.Slots[i].Items = null;
|
|
// Update remaining resources to be subtracted
|
|
filterDrainRate = Math.Abs(resourceValue ?? 0);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get an adjusted water filter drain rate based on time elapsed since last run,
|
|
/// handle edge case when craft time has gone on longer than total production time
|
|
/// </summary>
|
|
/// <param name="secondsSinceServerTick">Time passed</param>
|
|
/// <param name="totalProductionTime">Total time collecting water</param>
|
|
/// <param name="productionProgress">How far water collector has progressed</param>
|
|
/// <param name="baseFilterDrainRate">Base drain rate</param>
|
|
/// <returns>Drain rate (adjusted)</returns>
|
|
protected static double GetTimeAdjustedWaterFilterDrainRate(
|
|
long secondsSinceServerTick,
|
|
double totalProductionTime,
|
|
double productionProgress,
|
|
double baseFilterDrainRate
|
|
)
|
|
{
|
|
var drainTimeSeconds =
|
|
secondsSinceServerTick > totalProductionTime
|
|
? totalProductionTime - productionProgress // More time passed than prod time, get total minus the current progress
|
|
: secondsSinceServerTick;
|
|
|
|
// Multiply base drain rate by time passed
|
|
return baseFilterDrainRate * drainTimeSeconds;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the water filter drain rate based on hideout bonuses player has
|
|
/// </summary>
|
|
/// <param name="pmcData">Player profile</param>
|
|
/// <returns>Drain rate</returns>
|
|
protected double GetWaterFilterDrainRate(PmcData pmcData)
|
|
{
|
|
var globalSkillsDb = databaseService.GetGlobals().Configuration.SkillsSettings;
|
|
|
|
// 100 resources last 8 hrs 20 min, 100/8.33/60/60 = 0.00333
|
|
const double filterDrainRate = 0.00333d;
|
|
|
|
var hideoutManagementConsumptionBonus = GetSkillBonusMultipliedBySkillLevel(
|
|
pmcData,
|
|
SkillTypes.HideoutManagement,
|
|
globalSkillsDb.HideoutManagement.ConsumptionReductionPerLevel
|
|
);
|
|
var craftSkillTimeReductionMultiplier = GetSkillBonusMultipliedBySkillLevel(
|
|
pmcData,
|
|
SkillTypes.Crafting,
|
|
globalSkillsDb.Crafting.CraftTimeReductionPerLevel
|
|
);
|
|
|
|
// Never let bonus become 0
|
|
var reductionBonus =
|
|
hideoutManagementConsumptionBonus + craftSkillTimeReductionMultiplier == 0
|
|
? 1
|
|
: 1 - (hideoutManagementConsumptionBonus + craftSkillTimeReductionMultiplier);
|
|
|
|
return filterDrainRate * reductionBonus;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the production time in seconds for the desired production
|
|
/// </summary>
|
|
/// <param name="prodId">Id, e.g. Water collector id</param>
|
|
/// <returns>Seconds to produce item</returns>
|
|
protected double GetTotalProductionTimeSeconds(MongoId prodId)
|
|
{
|
|
return databaseService.GetHideout().Production.Recipes.FirstOrDefault(prod => prod.Id == prodId)?.ProductionTime ?? 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create an upd object using passed in parameters
|
|
/// </summary>
|
|
/// <param name="stackCount"></param>
|
|
/// <param name="resourceValue"></param>
|
|
/// <param name="resourceUnitsConsumed"></param>
|
|
/// <param name="isFoundInRaid"></param>
|
|
/// <returns>Upd</returns>
|
|
protected Upd GetAreaUpdObject(double stackCount, double? resourceValue, double resourceUnitsConsumed, bool isFoundInRaid)
|
|
{
|
|
return new Upd
|
|
{
|
|
StackObjectsCount = stackCount,
|
|
Resource = new UpdResource { Value = resourceValue, UnitsConsumed = resourceUnitsConsumed },
|
|
SpawnedInSession = isFoundInRaid,
|
|
};
|
|
}
|
|
|
|
protected void UpdateAirFilters(BotHideoutArea airFilterArea, PmcData pmcData, bool isGeneratorOn)
|
|
{
|
|
// 300 resources last 20 hrs, 300/20/60/60 = 0.00416
|
|
// 10-10-2021 from WIKI (https://escapefromtarkov.fandom.com/wiki/FP-100_filter_absorber)
|
|
// Lasts for 17 hours 38 minutes and 49 seconds (23 hours 31 minutes and 45 seconds with elite hideout management skill),
|
|
// 300/17.64694/60/60 = 0.004722
|
|
var filterDrainRate =
|
|
databaseService.GetHideout().Settings.AirFilterUnitFlowRate * GetTimeElapsedSinceLastServerTick(pmcData, isGeneratorOn);
|
|
|
|
// Hideout management resource consumption bonus:
|
|
var hideoutManagementConsumptionBonus = 1.0 - GetHideoutManagementConsumptionBonus(pmcData);
|
|
filterDrainRate *= hideoutManagementConsumptionBonus;
|
|
|
|
for (var i = 0; i < airFilterArea.Slots.Count; i++)
|
|
{
|
|
if (airFilterArea.Slots[i]?.Items is null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var resourceValue = airFilterArea.Slots[i].Items[0].Upd?.Resource is not null
|
|
? airFilterArea.Slots[i].Items[0].Upd.Resource.Value
|
|
: null;
|
|
|
|
double pointsConsumed;
|
|
if (resourceValue is null)
|
|
{
|
|
resourceValue = 300 - filterDrainRate;
|
|
pointsConsumed = filterDrainRate ?? 0;
|
|
}
|
|
else
|
|
{
|
|
pointsConsumed = (airFilterArea.Slots[i].Items[0].Upd.Resource.UnitsConsumed ?? 0) + filterDrainRate ?? 0;
|
|
resourceValue -= filterDrainRate;
|
|
}
|
|
|
|
resourceValue = Math.Round(resourceValue * 10000 ?? 0) / 10000;
|
|
pointsConsumed = Math.Round(pointsConsumed * 10000) / 10000;
|
|
|
|
// check unit consumed for increment skill point
|
|
if (pmcData is not null && Math.Floor(pointsConsumed / 10) >= 1)
|
|
{
|
|
profileHelper.AddSkillPointsToPlayer(pmcData, SkillTypes.HideoutManagement, 1);
|
|
pointsConsumed -= 10;
|
|
}
|
|
|
|
if (resourceValue > 0)
|
|
{
|
|
airFilterArea.Slots[i].Items[0].Upd = new Upd
|
|
{
|
|
StackObjectsCount = 1,
|
|
Resource = new UpdResource { Value = resourceValue, UnitsConsumed = pointsConsumed },
|
|
};
|
|
if (logger.IsLogEnabled(LogLevel.Debug))
|
|
{
|
|
logger.Debug($"Air filter: {resourceValue} filter left on slot {i + 1}");
|
|
}
|
|
|
|
break; // Break here to avoid updating all filters
|
|
}
|
|
|
|
airFilterArea.Slots[i].Items = null;
|
|
// Update remaining resources to be subtracted
|
|
filterDrainRate = Math.Abs(resourceValue ?? 0);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Increment bitcoin farm progress
|
|
/// </summary>
|
|
/// <param name="pmcData">Player profile</param>
|
|
/// <param name="btcProduction">Hideout btc craft</param>
|
|
/// <param name="btcFarmCGs"></param>
|
|
/// <param name="isGeneratorOn">Is hideout generator powered</param>
|
|
protected void UpdateBitcoinFarm(PmcData pmcData, Production? btcProduction, int? btcFarmCGs, bool isGeneratorOn)
|
|
{
|
|
if (btcProduction is null)
|
|
{
|
|
logger.Error(serverLocalisationService.GetText("hideout-bitcoin_craft_missing"));
|
|
|
|
return;
|
|
}
|
|
|
|
var isBtcProd = btcProduction.GetType() == typeof(Production);
|
|
if (!isBtcProd)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// The wiki has a wrong formula!
|
|
// Do not change unless you validate it with the Client code files!
|
|
// This formula was found on the client files:
|
|
// *******************************************************
|
|
/*
|
|
public override int InstalledSuppliesCount
|
|
{
|
|
get
|
|
{
|
|
return this.int_1;
|
|
}
|
|
protected set
|
|
{
|
|
if (this.int_1 === value)
|
|
{
|
|
return;
|
|
}
|
|
this.int_1 = value;
|
|
base.Single_0 = ((this.int_1 === 0) ? 0f : (1f + (float)(this.int_1 - 1) * this.float_4));
|
|
}
|
|
}
|
|
*/
|
|
// **********************************************************
|
|
// At the time of writing this comment, this was GClass1667
|
|
// To find it in case of weird results, use DNSpy and look for usages on class AreaData
|
|
// Look for a GClassXXXX that has a method called "InitDetails" and the only parameter is the AreaData
|
|
// That should be the bitcoin farm production. To validate, try to find the snippet below:
|
|
/*
|
|
protected override void InitDetails(AreaData data)
|
|
{
|
|
base.InitDetails(data);
|
|
this.gclass1678_1.Type = EDetailsType.Farming;
|
|
}
|
|
*/
|
|
// Needs power to function
|
|
if (!isGeneratorOn)
|
|
// Return with no changes
|
|
{
|
|
return;
|
|
}
|
|
|
|
var coinSlotCount = GetBTCSlots(pmcData);
|
|
|
|
// Full of bitcoins, halt progress
|
|
if (btcProduction.Products.Count >= coinSlotCount)
|
|
{
|
|
// Set progress to 0
|
|
btcProduction.Progress = 0;
|
|
|
|
return;
|
|
}
|
|
|
|
var bitcoinProdData = databaseService
|
|
.GetHideout()
|
|
.Production.Recipes.FirstOrDefault(production => production.Id == BitcoinProductionId);
|
|
|
|
// BSG finally fixed their settings, they now get loaded from the settings and used in the client
|
|
var adjustedCraftTime =
|
|
(profileHelper.IsDeveloperAccount(pmcData.SessionId.Value) ? 40 : bitcoinProdData.ProductionTime)
|
|
/ (1 + (btcFarmCGs - 1) * databaseService.GetHideout().Settings.GpuBoostRate);
|
|
|
|
// The progress should be adjusted based on the GPU boost rate, but the target is still the base productionTime
|
|
var timeMultiplier = bitcoinProdData.ProductionTime / adjustedCraftTime;
|
|
var timeElapsedSeconds = GetTimeElapsedSinceLastServerTick(pmcData, isGeneratorOn);
|
|
btcProduction.Progress += Math.Floor(timeElapsedSeconds * timeMultiplier ?? 0);
|
|
|
|
while (btcProduction.Progress >= bitcoinProdData.ProductionTime)
|
|
{
|
|
if (btcProduction.Products.Count < coinSlotCount)
|
|
// Has space to add a coin to production rewards
|
|
{
|
|
AddBtcToProduction(btcProduction, bitcoinProdData.ProductionTime ?? 0);
|
|
}
|
|
else
|
|
// Filled up bitcoin storage
|
|
{
|
|
btcProduction.Progress = 0;
|
|
}
|
|
}
|
|
|
|
btcProduction.StartTimestamp = timeUtil.GetTimeStamp();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add bitcoin object to btc production products array and set progress time
|
|
/// </summary>
|
|
/// <param name="btcProd">Bitcoin production object</param>
|
|
/// <param name="coinCraftTimeSeconds">Time to craft a bitcoin</param>
|
|
protected void AddBtcToProduction(Production btcProd, double coinCraftTimeSeconds)
|
|
{
|
|
btcProd.Products.Add(
|
|
new Item
|
|
{
|
|
Id = new MongoId(),
|
|
Template = ItemTpl.BARTER_PHYSICAL_BITCOIN,
|
|
Upd = new Upd { StackObjectsCount = 1 },
|
|
}
|
|
);
|
|
|
|
// Deduct time spent crafting from progress
|
|
btcProd.Progress -= coinCraftTimeSeconds;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get number of ticks that have passed since hideout areas were last processed, reduced when generator is off
|
|
/// </summary>
|
|
/// <param name="pmcData">Player profile</param>
|
|
/// <param name="isGeneratorOn">Is the generator on for the duration of elapsed time</param>
|
|
/// <param name="recipe">Hideout production recipe being crafted we need the ticks for</param>
|
|
/// <returns>Amount of time elapsed in seconds</returns>
|
|
protected long? GetTimeElapsedSinceLastServerTick(PmcData pmcData, bool isGeneratorOn, HideoutProduction? recipe = null)
|
|
{
|
|
// Reduce time elapsed (and progress) when generator is off
|
|
var timeElapsed = timeUtil.GetTimeStamp() - pmcData.Hideout.SptUpdateLastRunTimestamp;
|
|
|
|
if (recipe is not null)
|
|
{
|
|
var hideoutArea = databaseService.GetHideout().Areas.FirstOrDefault(area => area.Type == recipe.AreaType);
|
|
if (!(hideoutArea.NeedsFuel ?? false))
|
|
// e.g. Lavatory works at 100% when power is on / off
|
|
{
|
|
return timeElapsed;
|
|
}
|
|
}
|
|
|
|
if (!isGeneratorOn)
|
|
{
|
|
timeElapsed = (long)(timeElapsed * databaseService.GetHideout().Settings.GeneratorSpeedWithoutFuel);
|
|
}
|
|
|
|
return timeElapsed;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get a count of how many possible BTC can be gathered by the profile
|
|
/// </summary>
|
|
/// <param name="pmcData">Profile to look up</param>
|
|
/// <returns>Coin slot count</returns>
|
|
protected double GetBTCSlots(PmcData pmcData)
|
|
{
|
|
var bitcoinProductions = databaseService
|
|
.GetHideout()
|
|
.Production.Recipes.FirstOrDefault(production => production.Id == BitcoinProductionId);
|
|
var productionSlots = bitcoinProductions?.ProductionLimitCount ?? 3; // Default to 3 if none found
|
|
var hasManagementSkillSlots = profileHelper.HasEliteSkillLevel(SkillTypes.HideoutManagement, pmcData);
|
|
var managementSlotsCount = GetEliteSkillAdditionalBitcoinSlotCount() ?? 2;
|
|
|
|
return productionSlots + (hasManagementSkillSlots ? managementSlotsCount : 0);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get a count of how many additional bitcoins player hideout can hold with elite skill
|
|
/// </summary>
|
|
protected double? GetEliteSkillAdditionalBitcoinSlotCount()
|
|
{
|
|
return databaseService.GetGlobals().Configuration.SkillsSettings.HideoutManagement.EliteSlots.BitcoinFarm.Container;
|
|
}
|
|
|
|
/// <summary>
|
|
/// HideoutManagement skill gives a consumption bonus the higher the level
|
|
/// 0.5% per level per 1-51, (25.5% at max)
|
|
/// </summary>
|
|
/// <param name="pmcData">Profile to get hideout consumption level from</param>
|
|
/// <returns>Consumption bonus</returns>
|
|
protected double? GetHideoutManagementConsumptionBonus(PmcData pmcData)
|
|
{
|
|
var hideoutManagementSkill = pmcData.GetSkillFromProfile(SkillTypes.HideoutManagement);
|
|
if (hideoutManagementSkill is null || hideoutManagementSkill.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(hideoutManagementSkill.Progress / 100);
|
|
roundedLevel = roundedLevel == 51 ? roundedLevel - 1 : roundedLevel;
|
|
|
|
return roundedLevel
|
|
* databaseService.GetGlobals().Configuration.SkillsSettings.HideoutManagement.ConsumptionReductionPerLevel
|
|
/ 100;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get a multiplier based on player's skill level and value per level
|
|
/// </summary>
|
|
/// <param name="pmcData">Player profile</param>
|
|
/// <param name="skill">Player skill from profile</param>
|
|
/// <param name="valuePerLevel">Value from globals.config.SkillsSettings - `PerLevel`</param>
|
|
/// <returns>Multiplier from 0 to 1</returns>
|
|
protected double GetSkillBonusMultipliedBySkillLevel(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 == 51 ? roundedLevel - 1 : roundedLevel;
|
|
|
|
return roundedLevel * valuePerLevel / 100;
|
|
}
|
|
|
|
/// <summary>
|
|
/// </summary>
|
|
/// <param name="pmcData">Player profile</param>
|
|
/// <param name="productionTime">Time to complete hideout craft in seconds</param>
|
|
/// <param name="skill">Skill bonus to get reduction from</param>
|
|
/// <param name="amountPerLevel">Skill bonus amount to apply</param>
|
|
/// <returns>Seconds to reduce craft time by</returns>
|
|
public double GetSkillProductionTimeReduction(PmcData pmcData, double productionTime, SkillTypes skill, double amountPerLevel)
|
|
{
|
|
var skillTimeReductionMultiplier = GetSkillBonusMultipliedBySkillLevel(pmcData, skill, amountPerLevel);
|
|
|
|
return productionTime * skillTimeReductionMultiplier;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gather crafted BTC from hideout area and add to inventory
|
|
/// Reset production start timestamp if hideout area at full coin capacity
|
|
/// </summary>
|
|
/// <param name="pmcData">Player profile</param>
|
|
/// <param name="request">Take production request</param>
|
|
/// <param name="sessionId">Session id</param>
|
|
/// <param name="output">Output object to update</param>
|
|
public void GetBTC(PmcData pmcData, HideoutTakeProductionRequestData request, MongoId sessionId, ItemEventRouterResponse output)
|
|
{
|
|
// Get how many coins were crafted and ready to pick up
|
|
pmcData.Hideout.Production.TryGetValue(BitcoinProductionId, out var bitcoinCraft);
|
|
var craftedCoinCount = bitcoinCraft?.Products?.Count;
|
|
if (bitcoinCraft is null || craftedCoinCount is null)
|
|
{
|
|
var errorMsg = serverLocalisationService.GetText("hideout-no_bitcoins_to_collect");
|
|
logger.Error(errorMsg);
|
|
|
|
httpResponseUtil.AppendErrorToOutput(output, errorMsg);
|
|
|
|
return;
|
|
}
|
|
|
|
List<List<Item>> itemsToAdd = [];
|
|
for (var index = 0; index < craftedCoinCount; index++)
|
|
{
|
|
itemsToAdd.Add(
|
|
[
|
|
new Item
|
|
{
|
|
Id = new MongoId(),
|
|
Template = ItemTpl.BARTER_PHYSICAL_BITCOIN,
|
|
Upd = new Upd { StackObjectsCount = 1 },
|
|
},
|
|
]
|
|
);
|
|
}
|
|
|
|
// Create request for what we want to add to stash
|
|
var addItemsRequest = new AddItemsDirectRequest
|
|
{
|
|
ItemsWithModsToAdd = itemsToAdd,
|
|
FoundInRaid = true,
|
|
UseSortingTable = false,
|
|
Callback = null,
|
|
};
|
|
|
|
// Add FiR coins to player inventory
|
|
inventoryHelper.AddItemsToStash(sessionId, addItemsRequest, pmcData, output);
|
|
if (output.Warnings?.Count > 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Is at max capacity + we collected all coins - reset production start time
|
|
var coinSlotCount = GetBTCSlots(pmcData);
|
|
if (pmcData.Hideout.Production[BitcoinProductionId].Products.Count >= coinSlotCount)
|
|
// Set start to now
|
|
{
|
|
pmcData.Hideout.Production[BitcoinProductionId].StartTimestamp = timeUtil.GetTimeStamp();
|
|
}
|
|
|
|
// Remove crafted coins from production in profile now they've been collected
|
|
// Can only collect all coins, not individually
|
|
pmcData.Hideout.Production[BitcoinProductionId].Products = [];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Hideout improvement is flagged as complete
|
|
/// </summary>
|
|
/// <param name="improvement">hideout improvement object</param>
|
|
/// <returns>true if complete</returns>
|
|
protected bool HideoutImprovementIsComplete(HideoutImprovement improvement)
|
|
{
|
|
return improvement?.Completed ?? false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Iterate over hideout improvements not completed and check if they need to be adjusted
|
|
/// </summary>
|
|
/// <param name="profileData">Profile to adjust</param>
|
|
public void SetHideoutImprovementsToCompleted(PmcData profileData)
|
|
{
|
|
foreach (var improvementId in profileData.Hideout.Improvements)
|
|
{
|
|
if (!profileData.Hideout.Improvements.TryGetValue(improvementId.Key, out var improvementDetails))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (improvementDetails.Completed == false && improvementDetails.ImproveCompleteTimestamp < timeUtil.GetTimeStamp())
|
|
{
|
|
improvementDetails.Completed = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add/remove bonus combat skill based on number of dogtags in place of fame hideout area
|
|
/// </summary>
|
|
/// <param name="pmcData">Player profile</param>
|
|
public void ApplyPlaceOfFameDogtagBonus(PmcData pmcData)
|
|
{
|
|
var fameAreaProfile = pmcData.Hideout.Areas.FirstOrDefault(area => area.Type == HideoutAreas.PlaceOfFame);
|
|
|
|
// Get hideout area 16 bonus array
|
|
var fameAreaDb = databaseService.GetHideout().Areas.FirstOrDefault(area => area.Type == HideoutAreas.PlaceOfFame);
|
|
|
|
// Get SkillGroupLevelingBoost object
|
|
var combatBoostBonusDb = fameAreaDb
|
|
.Stages[fameAreaProfile.Level.ToString()]
|
|
.Bonuses.FirstOrDefault(bonus => bonus.Type.ToString() == "SkillGroupLevelingBoost");
|
|
|
|
// Get SkillGroupLevelingBoost object in profile
|
|
var combatBonusProfile = pmcData.Bonuses.FirstOrDefault(bonus => bonus.Id == combatBoostBonusDb.Id);
|
|
|
|
// Get all slotted dogtag items
|
|
var activeDogtags = pmcData.Inventory.Items.Where(item => item?.SlotId?.StartsWith("dogtag") ?? false);
|
|
|
|
// Calculate bonus percent (apply hideoutManagement bonus)
|
|
var hideoutManagementSkill = pmcData.GetSkillFromProfile(SkillTypes.HideoutManagement);
|
|
var hideoutManagementSkillBonusPercent = 1 + hideoutManagementSkill.Progress / 10000; // 5100 becomes 0.51, add 1 to it, 1.51
|
|
var bonus = GetDogtagCombatSkillBonusPercent(pmcData, activeDogtags) * hideoutManagementSkillBonusPercent;
|
|
|
|
// Update bonus value to above calcualted value
|
|
combatBonusProfile.Value = Math.Round(bonus, 2);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculate the raw dogtag combat skill bonus for place of fame based on number of dogtags
|
|
/// Reverse engineered from client code
|
|
/// </summary>
|
|
/// <param name="pmcData">Player profile</param>
|
|
/// <param name="activeDogtags">Active dogtags in place of fame dogtag slots</param>
|
|
/// <returns>Combat bonus</returns>
|
|
protected static double GetDogtagCombatSkillBonusPercent(PmcData pmcData, IEnumerable<Item> activeDogtags)
|
|
{
|
|
// Not own dogtag
|
|
// Side = opposite of player
|
|
var result = 0D;
|
|
foreach (var dogtag in activeDogtags)
|
|
{
|
|
if (dogtag.Upd?.Dogtag?.AccountId is null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (int.Parse(dogtag.Upd.Dogtag.AccountId) == pmcData.Aid)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
result += 0.01 * dogtag.Upd.Dogtag.Level ?? 0;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The wall pollutes a profile with various temp buffs/debuffs,
|
|
/// Remove them all
|
|
/// </summary>
|
|
/// <param name="wallAreaDb">Hideout area data</param>
|
|
/// <param name="pmcData">Player profile</param>
|
|
public void RemoveHideoutWallBuffsAndDebuffs(HideoutArea wallAreaDb, PmcData pmcData)
|
|
{
|
|
// Smush all stage bonuses into one array for easy iteration
|
|
var wallBonuses = wallAreaDb.Stages.SelectMany(stage => stage.Value.Bonuses);
|
|
|
|
// Get all bonus Ids that the wall adds
|
|
HashSet<string> bonusIdsToRemove = [];
|
|
foreach (var bonus in wallBonuses)
|
|
{
|
|
bonusIdsToRemove.Add(bonus.Id);
|
|
}
|
|
|
|
if (logger.IsLogEnabled(LogLevel.Debug))
|
|
{
|
|
logger.Debug($"Removing: {bonusIdsToRemove.Count} bonuses from profile");
|
|
}
|
|
|
|
// Remove the wall bonuses from profile by id
|
|
pmcData.Bonuses = pmcData.Bonuses.Where(bonus => !bonusIdsToRemove.Contains(bonus.Id)).ToList();
|
|
}
|
|
}
|