diff --git a/Libraries/Core/Helpers/RepairHelper.cs b/Libraries/Core/Helpers/RepairHelper.cs index 2bf6d933..f11703c1 100644 --- a/Libraries/Core/Helpers/RepairHelper.cs +++ b/Libraries/Core/Helpers/RepairHelper.cs @@ -1,4 +1,4 @@ -using SptCommon.Annotations; +using SptCommon.Annotations; using Core.Models.Eft.Common.Tables; using Props = Core.Models.Eft.Common.Props; @@ -21,7 +21,7 @@ public class RepairHelper Item itemToRepair, TemplateItem itemToRepairDetails, bool isArmor, - int amountToRepair, + double amountToRepair, bool useRepairKit, double traderQualityMultipler, bool applyMaxDurabilityDegradation = true diff --git a/Libraries/Core/Models/Eft/Common/Globals.cs b/Libraries/Core/Models/Eft/Common/Globals.cs index 1185fc2d..852ab8f6 100644 --- a/Libraries/Core/Models/Eft/Common/Globals.cs +++ b/Libraries/Core/Models/Eft/Common/Globals.cs @@ -432,7 +432,7 @@ public record Config public AirdropGlobalSettings? Airdrop { get; set; } [JsonPropertyName("ArmorMaterials")] - public ArmorMaterials? ArmorMaterials { get; set; } + public Dictionary? ArmorMaterials { get; set; } [JsonPropertyName("ArenaEftTransferSettings")] public ArenaEftTransferSettings @@ -1408,33 +1408,6 @@ public record ArenaEftTransferSettings public Dictionary? TransferLimitsSettings { get; set; } } -public record ArmorMaterials -{ - [JsonPropertyName("UHMWPE")] - public ArmorType? UHMWPE { get; set; } - - [JsonPropertyName("Aramid")] - public ArmorType? Aramid { get; set; } - - [JsonPropertyName("Combined")] - public ArmorType? Combined { get; set; } - - [JsonPropertyName("Titan")] - public ArmorType? Titan { get; set; } - - [JsonPropertyName("Aluminium")] - public ArmorType? Aluminium { get; set; } - - [JsonPropertyName("ArmoredSteel")] - public ArmorType? ArmoredSteel { get; set; } - - [JsonPropertyName("Ceramic")] - public ArmorType? Ceramic { get; set; } - - [JsonPropertyName("Glass")] - public ArmorType? Glass { get; set; } -} - public record ArmorType { [JsonPropertyName("Destructibility")] diff --git a/Libraries/Core/Models/Eft/Common/Tables/Item.cs b/Libraries/Core/Models/Eft/Common/Tables/Item.cs index d1da8c72..2a374f05 100644 --- a/Libraries/Core/Models/Eft/Common/Tables/Item.cs +++ b/Libraries/Core/Models/Eft/Common/Tables/Item.cs @@ -100,10 +100,10 @@ public record UpdBuff public string? BuffType { get; set; } [JsonPropertyName("Value")] - public int? Value { get; set; } + public double? Value { get; set; } [JsonPropertyName("ThresholdDurability")] - public int? ThresholdDurability { get; set; } + public double? ThresholdDurability { get; set; } } public record UpdTogglable diff --git a/Libraries/Core/Models/Spt/Config/RepairConfig.cs b/Libraries/Core/Models/Spt/Config/RepairConfig.cs index 21c989c5..81cc3d33 100644 --- a/Libraries/Core/Models/Spt/Config/RepairConfig.cs +++ b/Libraries/Core/Models/Spt/Config/RepairConfig.cs @@ -1,4 +1,4 @@ -using Core.Models.Common; +using Core.Models.Common; namespace Core.Models.Spt.Config; diff --git a/Libraries/Core/Services/RepairService.cs b/Libraries/Core/Services/RepairService.cs index 1deecc4e..10044c18 100644 --- a/Libraries/Core/Services/RepairService.cs +++ b/Libraries/Core/Services/RepairService.cs @@ -8,26 +8,32 @@ using Core.Models.Eft.ItemEvent; using Core.Models.Eft.Repair; using Core.Models.Enums; using Core.Models.Utils; +using Core.Servers; using Core.Utils; +using Core.Models.Spt.Config; +using Core.Models.Eft.Trade; namespace Core.Services; [Injectable(InjectionType.Singleton)] -public class RepairService +public class RepairService( + ISptLogger _logger, + RandomUtil randomUtil, + DatabaseService _databaseService, + ItemHelper _itemHelper, + TraderHelper _traderHelper, + PaymentService _paymentService, + ProfileHelper _profileHelper, + RepairHelper _repairHelper, + LocalisationService _localisationService, + ConfigServer _configServer, + WeightedRandomHelper _weightedRandomHelper) { private readonly ISptLogger _logger; private readonly RandomUtil _randomUtil; private readonly WeightedRandomHelper _weightedRandomHelper; + private readonly RepairConfig _repairConfig = _configServer.GetConfig(); - public RepairService( - ISptLogger _logger, - RandomUtil randomUtil, - WeightedRandomHelper weightedRandomHelper) - { - this._logger = _logger; - _randomUtil = randomUtil; - _weightedRandomHelper = weightedRandomHelper; - } /// /// Use trader to repair an items durability @@ -44,7 +50,58 @@ public class RepairService string traderId ) { - throw new NotImplementedException(); + var itemToRepair = pmcData.Inventory.Items.FirstOrDefault((item) => item.Id == repairItemDetails.Id); + if (itemToRepair is null) + { + _logger.Error( + _localisationService.GetText( + "repair-unable_to_find_item_in_inventory_cant_repair", + repairItemDetails.Id) + ); + } + + var priceCoef = _traderHelper.GetLoyaltyLevel(traderId, pmcData).RepairPriceCoefficient; + var traderRepairDetails = _traderHelper.GetTrader(traderId, sessionID)?.Repair; + if (traderRepairDetails is null) + { + _logger.Error(_localisationService.GetText("repair-unable_to_find_trader_details_by_id", traderId)); + } + var repairQualityMultiplier = traderRepairDetails.Quality; + var repairRate = priceCoef <= 0 ? 1 : priceCoef / 100 + 1; + + var items = _databaseService.GetItems(); + var itemToRepairDetails = items[itemToRepair.Template]; + var repairItemIsArmor = itemToRepairDetails.Properties.ArmorMaterial is not null; + + _repairHelper.UpdateItemDurability( + itemToRepair, + itemToRepairDetails, + repairItemIsArmor, + repairItemDetails.Count.Value, + false, + repairQualityMultiplier.Value, + repairQualityMultiplier != 0 && _repairConfig.ApplyRandomizeDurabilityLoss); + + // get repair price + var itemRepairCost = items[itemToRepair.Template].Properties.RepairCost; + if (itemRepairCost is null) + { + _logger.Error( + _localisationService.GetText("repair-unable_to_find_item_repair_cost", itemToRepair.Template)); + } + var repairCost = Math.Round( + itemRepairCost.Value * repairItemDetails.Count.Value * repairRate.Value * _repairConfig.PriceMultiplier); + + _logger.Debug($"item base repair cost: ${ itemRepairCost}"); + _logger.Debug($"price multiplier: ${ _repairConfig.PriceMultiplier}"); + _logger.Debug($"repair cost: ${ repairCost}"); + + return new RepairDetails{ + RepairCost = repairCost, + RepairedItem = itemToRepair, + RepairedItemIsArmor = repairItemIsArmor, + RepairAmount = repairItemDetails.Count, + RepairedByKit = false }; } /// @@ -64,7 +121,17 @@ public class RepairService ItemEventRouterResponse output ) { - throw new NotImplementedException(); + var options = new ProcessBuyTradeRequestData { + SchemeItems = [new() { Count = Math.Round(repairCost), Id = Money.ROUBLES }], + TransactionId = traderId, + Action = "SptRepair", + Type = "", + ItemId = "", + Count = 0, + SchemeId = 0 + }; + + _paymentService.PayMoney(pmcData, options, sessionID, output); } /// @@ -75,12 +142,95 @@ public class RepairService /// Profile to add points to public void AddRepairSkillPoints(string sessionId, RepairDetails repairDetails, PmcData pmcData) { - throw new NotImplementedException(); + // Handle kit repair of weapon + if ( + repairDetails.RepairedByKit.GetValueOrDefault(false) && + _itemHelper.IsOfBaseclass(repairDetails.RepairedItem.Template, BaseClasses.WEAPON) + ) + { + var skillPoints = GetWeaponRepairSkillPoints(repairDetails); + + if (skillPoints > 0) + { + _logger.Debug($"Added: { skillPoints} WEAPON_TREATMENT points to skill"); + _profileHelper.AddSkillPointsToPlayer(pmcData, SkillTypes.WeaponTreatment, skillPoints, true); + } + } + + // Handle kit repair of armor + if ( + repairDetails.RepairedByKit.GetValueOrDefault(false) && + _itemHelper.IsOfBaseclasses(repairDetails.RepairedItem.Template, [ + BaseClasses.ARMOR_PLATE, + BaseClasses.BUILT_IN_INSERTS, + ]) + ) + { + var itemDetails = _itemHelper.GetItem(repairDetails.RepairedItem.Template); + if (!itemDetails.Key) + { + // No item found + _logger.Error( + _localisationService.GetText( + "repair-unable_to_find_item_in_db", + repairDetails.RepairedItem.Template) + ); + + return; + } + + var isHeavyArmor = itemDetails.Value.Properties.ArmorType == "Heavy"; + var vestSkillToLevel = isHeavyArmor ? SkillTypes.HeavyVests : SkillTypes.LightVests; + if (repairDetails.RepairPoints is null) + { + _logger.Error( + _localisationService.GetText( + "repair-item_has_no_repair_points", + repairDetails.RepairedItem.Template) + ); + } + var pointsToAddToVestSkill = + repairDetails.RepairPoints * _repairConfig.ArmorKitSkillPointGainPerRepairPointMultiplier; + + _logger.Debug($"Added: { pointsToAddToVestSkill} { vestSkillToLevel} skill"); + _profileHelper.AddSkillPointsToPlayer(pmcData, vestSkillToLevel, pointsToAddToVestSkill); + } + + // Handle giving INT to player - differs if using kit/trader and weapon vs armor + var intellectGainedFromRepair = GetIntellectGainedFromRepair(repairDetails); + if (intellectGainedFromRepair > 0) + { + _logger.Debug($"Added: { intellectGainedFromRepair} intellect skill"); + _profileHelper.AddSkillPointsToPlayer(pmcData, SkillTypes.Intellect, intellectGainedFromRepair); + } } - protected decimal GetIntellectGainedFromRepair(RepairDetails repairDetails) + protected double GetIntellectGainedFromRepair(RepairDetails repairDetails) { - throw new NotImplementedException(); + if (repairDetails.RepairedByKit.GetValueOrDefault(false)) + { + // Weapons/armor have different multipliers + var intRepairMultiplier = _itemHelper.IsOfBaseclass(repairDetails.RepairedItem.Template, BaseClasses.WEAPON) + ? _repairConfig.RepairKitIntellectGainMultiplier.Weapon + : _repairConfig.RepairKitIntellectGainMultiplier.Armor; + + // Limit gain to a max value defined in config.maxIntellectGainPerRepair + if (repairDetails.RepairPoints is null) + { + _logger.Error( + _localisationService.GetText( + "repair-item_has_no_repair_points", + repairDetails.RepairedItem.Template) + ); + } + + return Math.Min( + repairDetails.RepairPoints.Value * intRepairMultiplier, + _repairConfig.MaxIntellectGainPerRepair.Kit); + } + + // Trader repair - Not as accurate as kit, needs data from live + return Math.Min(repairDetails.RepairAmount.Value / 10, _repairConfig.MaxIntellectGainPerRepair.Trader); } /// @@ -88,9 +238,36 @@ public class RepairService /// /// The repair details to calculate skill points for /// The number of skill points to reward the user - protected decimal GetWeaponRepairSkillPoints(RepairDetails repairDetails) + protected double GetWeaponRepairSkillPoints(RepairDetails repairDetails) { - throw new NotImplementedException(); + var random = new Random(); + // This formula and associated configs is calculated based on 30 repairs done on live + // The points always came out 2-aligned, which is why there's a divide/multiply by 2 with ceil calls + var gainMult = _repairConfig.WeaponTreatment.PointGainMultiplier; + + // First we get a baseline based on our repair amount, and gain multiplier with a bit of rounding + var step1 = Math.Ceiling(repairDetails.RepairAmount.Value / 2) * gainMult; + + // Then we have to get the next even number + var step2 = Math.Ceiling(step1 / 2) * 2; + + // Then multiply by 2 again to hopefully get to what live would give us + var skillPoints = step2 * 2; + + // You can both crit fail and succeed at the same time, for fun (Balances out to 0 with default settings) + // Add a random chance to crit-fail + if (random.Next() <= _repairConfig.WeaponTreatment.CritFailureChance) + { + skillPoints -= _repairConfig.WeaponTreatment.CritFailureAmount; + } + + // Add a random chance to crit-succeed + if (random.Next() <= _repairConfig.WeaponTreatment.CritSuccessChance) + { + skillPoints += _repairConfig.WeaponTreatment.CritSuccessAmount; + } + + return Math.Max(skillPoints, 0); } /// @@ -109,7 +286,56 @@ public class RepairService ItemEventRouterResponse output ) { - throw new NotImplementedException(); + // Find item to repair in inventory + var itemToRepair = pmcData.Inventory.Items.FirstOrDefault((x) => x.Id == itemToRepairId); + if (itemToRepair is null) + { + _logger.Error(_localisationService.GetText("repair-item_not_found_unable_to_repair", itemToRepairId)); + } + + var itemsDb = _databaseService.GetItems(); + var itemToRepairDetails = itemsDb[itemToRepair.Template]; + var repairItemIsArmor = itemToRepairDetails.Properties.ArmorMaterial is not null; + var repairAmount = repairKits[0].Count / GetKitDivisor(itemToRepairDetails, repairItemIsArmor, pmcData); + var shouldApplyDurabilityLoss = ShouldRepairKitApplyDurabilityLoss( + pmcData, + _repairConfig.ApplyRandomizeDurabilityLoss); + + _repairHelper.UpdateItemDurability( + itemToRepair, + itemToRepairDetails, + repairItemIsArmor, + repairAmount.Value, + true, + 1, + shouldApplyDurabilityLoss); + + // Find and use repair kit defined in body + foreach (var repairKit in repairKits) { + var repairKitInInventory = pmcData.Inventory.Items.FirstOrDefault((item) => item.Id == repairKit.Id); + if (repairKitInInventory is null) + { + _logger.Error( + _localisationService.GetText("repair-repair_kit_not_found_in_inventory", repairKit.Id)); + } + var repairKitDetails = itemsDb[repairKitInInventory.Template]; + var repairKitReductionAmount = repairKit.Count; + + AddMaxResourceToKitIfMissing(repairKitDetails, repairKitInInventory); + + // reduce usages on repairkit used + repairKitInInventory.Upd.RepairKit.Resource -= repairKitReductionAmount; + + output.ProfileChanges[sessionId].Items.ChangedItems.Add(repairKitInInventory); + } + + return new RepairDetails{ + RepairPoints = repairKits[0].Count, + RepairedItem = itemToRepair, + RepairedItemIsArmor = repairItemIsArmor, + RepairAmount = repairAmount, + RepairedByKit = true + }; } /// @@ -119,9 +345,37 @@ public class RepairService /// Is the item being repaired armor /// Player profile /// Number to divide kit points by - protected decimal GetKitDivisor(TemplateItem itemToRepairDetails, bool isArmor, PmcData pmcData) + protected double GetKitDivisor(TemplateItem itemToRepairDetails, bool isArmor, PmcData pmcData) { - throw new NotImplementedException(); + var globals = _databaseService.GetGlobals(); + var globalConfig = globals.Configuration; + var globalRepairSettings = globalConfig.RepairSettings; + + var intellectRepairPointsPerLevel = globalConfig.SkillsSettings.Intellect.RepairPointsCostReduction; + var profileIntellectLevel = + _profileHelper.GetSkillFromProfile(pmcData, SkillTypes.Intellect)?.Progress ?? 0; + var intellectPointReduction = intellectRepairPointsPerLevel * Math.Truncate(profileIntellectLevel / 100); + + if (isArmor) + { + var durabilityPointCostArmor = globalRepairSettings.DurabilityPointCostArmor; + var repairArmorBonus = GetBonusMultiplierValue(BonusType.RepairArmorBonus, pmcData); + var armorBonus = 1.0 - (repairArmorBonus - 1.0) - intellectPointReduction; + var materialType = itemToRepairDetails.Properties.ArmorMaterial ?? ""; + var armorMaterial = globalConfig.ArmorMaterials[materialType]; + var destructability = 1 + armorMaterial.Destructibility; + var armorClass = itemToRepairDetails.Properties.ArmorClass.Value; + var armorClassDivisor = globals.Configuration.RepairSettings.ArmorClassDivisor; + var armorClassMultiplier = 1.0 + armorClass / armorClassDivisor; + + return durabilityPointCostArmor.Value * armorBonus.Value * destructability.Value * armorClassMultiplier.Value; + } + + var repairWeaponBonus = GetBonusMultiplierValue(BonusType.RepairWeaponBonus, pmcData) - 1; + var repairPointMultiplier = 1.0 - repairWeaponBonus - intellectPointReduction; + var durabilityPointCostGuns = globals.Configuration.RepairSettings.DurabilityPointCostGuns; + + return durabilityPointCostGuns.Value * repairPointMultiplier.Value; } /// @@ -130,9 +384,17 @@ public class RepairService /// Bonus to get multiplier of /// Player profile to look in for skill /// Multiplier value - protected decimal GetBonusMultiplierValue(BonusType skillBonus, PmcData pmcData) + protected double GetBonusMultiplierValue(BonusType skillBonus, PmcData pmcData) { - throw new NotImplementedException(); + var bonusesMatched = pmcData?.Bonuses?.Where((b) => b.Type == skillBonus); + var value = 1d; + if (bonusesMatched is not null) + { + var summedPercentage = bonusesMatched.Sum(x => x.Value ?? 0); + value = 1 + summedPercentage / 100; + } + + return value; } /// @@ -143,7 +405,19 @@ public class RepairService /// True if loss should be applied protected bool ShouldRepairKitApplyDurabilityLoss(PmcData pmcData, bool applyRandomizeDurabilityLoss) { - throw new NotImplementedException(); + var shouldApplyDurabilityLoss = applyRandomizeDurabilityLoss; + if (shouldApplyDurabilityLoss) + { + // Random loss not disabled via config, perform charisma check + var hasEliteCharisma = _profileHelper.HasEliteSkillLevel(SkillTypes.Charisma, pmcData); + if (hasEliteCharisma) + { + // 50/50 chance of loss being ignored at elite level + shouldApplyDurabilityLoss = _randomUtil.GetChance100(50); + } + } + + return shouldApplyDurabilityLoss; } /// @@ -153,7 +427,16 @@ public class RepairService /// Repair kit to update protected void AddMaxResourceToKitIfMissing(TemplateItem repairKitDetails, Item repairKitInInventory) { - throw new NotImplementedException(); + var maxRepairAmount = repairKitDetails.Properties.MaxRepairResource; + if (repairKitInInventory.Upd is null) + { + _logger.Debug($"Repair kit: ${ repairKitInInventory.Id} in inventory lacks upd object, adding"); + repairKitInInventory.Upd = new Upd{ RepairKit = new UpdRepairKit{ Resource = maxRepairAmount } }; + } + if (repairKitInInventory.Upd.RepairKit?.Resource is null) + { + repairKitInInventory.Upd.RepairKit = new UpdRepairKit{ Resource = maxRepairAmount }; + } } /// @@ -163,7 +446,33 @@ public class RepairService /// Player profile public void AddBuffToItem(RepairDetails repairDetails, PmcData pmcData) { - throw new NotImplementedException(); + // Buffs are repair kit only + if (!repairDetails.RepairedByKit.GetValueOrDefault(false)) + { + return; + } + + if (ShouldBuffItem(repairDetails, pmcData)) + { + if ( + _itemHelper.IsOfBaseclasses(repairDetails.RepairedItem.Template, [ + BaseClasses.ARMOR, + BaseClasses.VEST, + BaseClasses.HEADWEAR, + BaseClasses.ARMOR_PLATE, + ]) + ) + { + var armorConfig = _repairConfig.RepairKit.Armor; + AddBuff(armorConfig, repairDetails.RepairedItem); + } + else if (_itemHelper.IsOfBaseclass(repairDetails.RepairedItem.Template, BaseClasses.WEAPON)) + { + var weaponConfig = _repairConfig.RepairKit.Weapon; + AddBuff(weaponConfig, repairDetails.RepairedItem); + } + // TODO: Knife repair kits may be added at some point, a bracket needs to be added here + } } /// @@ -173,23 +482,22 @@ public class RepairService /// Item to repair public void AddBuff(Models.Spt.Config.BonusSettings itemConfig, Item item) { - _logger.Error("NOT IMPLEMENTED - AddBuff"); - //var bonusRarity = _weightedRandomHelper.GetWeightedValue(itemConfig.RarityWeight); - //var bonusType = _weightedRandomHelper.GetWeightedValue(itemConfig.BonusTypeWeight); + var bonusRarityName = _weightedRandomHelper.GetWeightedValue(itemConfig.RarityWeight); + var bonusTypeName = _weightedRandomHelper.GetWeightedValue(itemConfig.BonusTypeWeight); - //var bonusValues = itemConfig[bonusRarity][bonusType].valuesMinMax; - //var bonusValue = _randomUtil.GetFloat(bonusValues.min, bonusValues.max); + var bonusRarity = bonusRarityName == "Rare" ? itemConfig.Rare : itemConfig.Common; + var bonusValues = bonusRarity[bonusTypeName].ValuesMinMax; + var bonusValue = _randomUtil.GetDouble(bonusValues.Min.Value, bonusValues.Max.Value); - //var bonusThresholdPercents = itemConfig[bonusRarity][bonusType].activeDurabilityPercentMinMax; - //var bonusThresholdPercent = _randomUtil.GetInt(bonusThresholdPercents.min, bonusThresholdPercents.max); + var bonusThresholdPercents = bonusRarity[bonusTypeName].ActiveDurabilityPercentMinMax; + var bonusThresholdPercent = _randomUtil.GetDouble(bonusThresholdPercents.Min.Value, bonusThresholdPercents.Max.Value); - //item.Upd.Buff = new UpdBuff { - // Rarity = bonusRarity, - // BuffType = bonusType, - // Value = bonusValue, - // ThresholdDurability = _randomUtil.GetPercentOfValue(bonusThresholdPercent, item.Upd.Repairable.Durability, 2).toFixed(2), - // ) - //}; + item.Upd.Buff = new UpdBuff { + Rarity = bonusRarityName, + BuffType = bonusTypeName, + Value = bonusValue, + ThresholdDurability = Math.Round(_randomUtil.GetPercentOfValue(bonusThresholdPercent, item.Upd.Repairable.Durability.Value)) + }; } /// @@ -211,7 +519,38 @@ public class RepairService /// Skill name protected SkillTypes? GetItemSkillType(TemplateItem itemTemplate) { - throw new NotImplementedException(); + var isArmorRelated = _itemHelper.IsOfBaseclasses(itemTemplate.Id, [ + BaseClasses.ARMOR, + BaseClasses.VEST, + BaseClasses.HEADWEAR, + BaseClasses.ARMOR_PLATE, + ]); + + if (isArmorRelated) + { + var armorType = itemTemplate.Properties.ArmorType; + if (armorType == "Light") + { + return SkillTypes.LightVests; + } + + if (armorType == "Heavy") + { + return SkillTypes.HeavyVests; + } + } + + if (_itemHelper.IsOfBaseclass(itemTemplate.Id, BaseClasses.WEAPON)) + { + return SkillTypes.WeaponTreatment; + } + + if (_itemHelper.IsOfBaseclass(itemTemplate.Id, BaseClasses.KNIFE)) + { + return SkillTypes.Melee; + } + + return null; } /// @@ -222,7 +561,10 @@ public class RepairService /// durability multiplier value protected double GetDurabilityMultiplier(double receiveDurabilityMaxPercent, double receiveDurabilityPercent) { - throw new NotImplementedException(); + // Ensure the max percent is at least 0.01 + var validMaxPercent = Math.Max(0.01, receiveDurabilityMaxPercent); + // Calculate the ratio and constrain it between 0.01 and 1 + return Math.Min(1, Math.Max(0.01, receiveDurabilityPercent / validMaxPercent)); } }