This commit is contained in:
Alex
2025-01-24 17:06:50 +00:00
5 changed files with 389 additions and 74 deletions
+2 -2
View File
@@ -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
+1 -28
View File
@@ -432,7 +432,7 @@ public record Config
public AirdropGlobalSettings? Airdrop { get; set; }
[JsonPropertyName("ArmorMaterials")]
public ArmorMaterials? ArmorMaterials { get; set; }
public Dictionary<string, ArmorType>? ArmorMaterials { get; set; }
[JsonPropertyName("ArenaEftTransferSettings")]
public ArenaEftTransferSettings
@@ -1408,33 +1408,6 @@ public record ArenaEftTransferSettings
public Dictionary<string, double>? 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")]
@@ -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
@@ -1,4 +1,4 @@
using Core.Models.Common;
using Core.Models.Common;
namespace Core.Models.Spt.Config;
+383 -41
View File
@@ -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<RepairService> _logger,
RandomUtil randomUtil,
DatabaseService _databaseService,
ItemHelper _itemHelper,
TraderHelper _traderHelper,
PaymentService _paymentService,
ProfileHelper _profileHelper,
RepairHelper _repairHelper,
LocalisationService _localisationService,
ConfigServer _configServer,
WeightedRandomHelper _weightedRandomHelper)
{
private readonly ISptLogger<RepairService> _logger;
private readonly RandomUtil _randomUtil;
private readonly WeightedRandomHelper _weightedRandomHelper;
private readonly RepairConfig _repairConfig = _configServer.GetConfig<RepairConfig>();
public RepairService(
ISptLogger<RepairService> _logger,
RandomUtil randomUtil,
WeightedRandomHelper weightedRandomHelper)
{
this._logger = _logger;
_randomUtil = randomUtil;
_weightedRandomHelper = weightedRandomHelper;
}
/// <summary>
/// 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 };
}
/// <summary>
@@ -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);
}
/// <summary>
@@ -75,12 +142,95 @@ public class RepairService
/// <param name="pmcData">Profile to add points to</param>
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);
}
/// <summary>
@@ -88,9 +238,36 @@ public class RepairService
/// </summary>
/// <param name="repairDetails">The repair details to calculate skill points for</param>
/// <returns>The number of skill points to reward the user</returns>
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);
}
/// <summary>
@@ -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
};
}
/// <summary>
@@ -119,9 +345,37 @@ public class RepairService
/// <param name="isArmor">Is the item being repaired armor</param>
/// <param name="pmcData">Player profile</param>
/// <returns>Number to divide kit points by</returns>
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;
}
/// <summary>
@@ -130,9 +384,17 @@ public class RepairService
/// <param name="skillBonus">Bonus to get multiplier of</param>
/// <param name="pmcData">Player profile to look in for skill</param>
/// <returns>Multiplier value</returns>
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;
}
/// <summary>
@@ -143,7 +405,19 @@ public class RepairService
/// <returns>True if loss should be applied</returns>
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;
}
/// <summary>
@@ -153,7 +427,16 @@ public class RepairService
/// <param name="repairKitInInventory">Repair kit to update</param>
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 };
}
}
/// <summary>
@@ -163,7 +446,33 @@ public class RepairService
/// <param name="pmcData">Player profile</param>
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
}
}
/// <summary>
@@ -173,23 +482,22 @@ public class RepairService
/// <param name="item">Item to repair</param>
public void AddBuff(Models.Spt.Config.BonusSettings itemConfig, Item item)
{
_logger.Error("NOT IMPLEMENTED - AddBuff");
//var bonusRarity = _weightedRandomHelper.GetWeightedValue<string>(itemConfig.RarityWeight);
//var bonusType = _weightedRandomHelper.GetWeightedValue<string>(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))
};
}
/// <summary>
@@ -211,7 +519,38 @@ public class RepairService
/// <returns>Skill name</returns>
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;
}
/// <summary>
@@ -222,7 +561,10 @@ public class RepairService
/// <returns>durability multiplier value</returns>
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));
}
}