Server/project/src/services/RepairService.ts
snowythefox811 f6192f1891 Added The ability to have enhancements on armored rigs and helmets. (!200)
Added the vest and headwear classes to be eligible for armor enhancement

Co-authored-by: Snow <Notarealemail@email.com>
Reviewed-on: https://dev.sp-tarkov.com/SPT-AKI/Server/pulls/200
Co-authored-by: snowythefox811 <snowythefox811@noreply.dev.sp-tarkov.com>
Co-committed-by: snowythefox811 <snowythefox811@noreply.dev.sp-tarkov.com>
(cherry picked from commit 35eb9feebf)
2024-01-18 09:08:22 +00:00

596 lines
24 KiB
TypeScript

import { inject, injectable } from "tsyringe";
import { ItemHelper } from "@spt-aki/helpers/ItemHelper";
import { ProfileHelper } from "@spt-aki/helpers/ProfileHelper";
import { RepairHelper } from "@spt-aki/helpers/RepairHelper";
import { TraderHelper } from "@spt-aki/helpers/TraderHelper";
import { WeightedRandomHelper } from "@spt-aki/helpers/WeightedRandomHelper";
import { IArmorType } from "@spt-aki/models/eft/common/IGlobals";
import { IPmcData } from "@spt-aki/models/eft/common/IPmcData";
import { Item } from "@spt-aki/models/eft/common/tables/IItem";
import { ITemplateItem } from "@spt-aki/models/eft/common/tables/ITemplateItem";
import { IItemEventRouterResponse } from "@spt-aki/models/eft/itemEvent/IItemEventRouterResponse";
import { RepairKitsInfo } from "@spt-aki/models/eft/repair/IRepairActionDataRequest";
import { RepairItem } from "@spt-aki/models/eft/repair/ITraderRepairActionDataRequest";
import { IProcessBuyTradeRequestData } from "@spt-aki/models/eft/trade/IProcessBuyTradeRequestData";
import { BaseClasses } from "@spt-aki/models/enums/BaseClasses";
import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes";
import { SkillTypes } from "@spt-aki/models/enums/SkillTypes";
import { BonusSettings, IRepairConfig } from "@spt-aki/models/spt/config/IRepairConfig";
import { ILogger } from "@spt-aki/models/spt/utils/ILogger";
import { ConfigServer } from "@spt-aki/servers/ConfigServer";
import { DatabaseServer } from "@spt-aki/servers/DatabaseServer";
import { LocalisationService } from "@spt-aki/services/LocalisationService";
import { PaymentService } from "@spt-aki/services/PaymentService";
import { RandomUtil } from "@spt-aki/utils/RandomUtil";
@injectable()
export class RepairService
{
protected repairConfig: IRepairConfig;
constructor(
@inject("WinstonLogger") protected logger: ILogger,
@inject("DatabaseServer") protected databaseServer: DatabaseServer,
@inject("ProfileHelper") protected profileHelper: ProfileHelper,
@inject("RandomUtil") protected randomUtil: RandomUtil,
@inject("ItemHelper") protected itemHelper: ItemHelper,
@inject("TraderHelper") protected traderHelper: TraderHelper,
@inject("WeightedRandomHelper") protected weightedRandomHelper: WeightedRandomHelper,
@inject("PaymentService") protected paymentService: PaymentService,
@inject("RepairHelper") protected repairHelper: RepairHelper,
@inject("LocalisationService") protected localisationService: LocalisationService,
@inject("ConfigServer") protected configServer: ConfigServer,
)
{
this.repairConfig = this.configServer.getConfig(ConfigTypes.REPAIR);
}
/**
* Use trader to repair an items durability
* @param sessionID Session id
* @param pmcData profile to find item to repair in
* @param repairItemDetails details of the item to repair
* @param traderId Trader being used to repair item
* @returns RepairDetails object
*/
public repairItemByTrader(
sessionID: string,
pmcData: IPmcData,
repairItemDetails: RepairItem,
traderId: string,
): RepairDetails
{
const itemToRepair = pmcData.Inventory.items.find((x) => x._id === repairItemDetails._id);
if (itemToRepair === undefined)
{
throw new Error(`Item ${repairItemDetails._id} not found in profile inventory, unable to repair`);
}
const priceCoef = this.traderHelper.getLoyaltyLevel(traderId, pmcData).repair_price_coef;
const traderRepairDetails = this.traderHelper.getTrader(traderId, sessionID).repair;
const repairQualityMultiplier = Number(traderRepairDetails.quality);
const repairRate = (priceCoef <= 0) ? 1 : (priceCoef / 100 + 1);
const itemToRepairDetails = this.databaseServer.getTables().templates.items[itemToRepair._tpl];
const repairItemIsArmor = !!itemToRepairDetails._props.ArmorMaterial;
this.repairHelper.updateItemDurability(
itemToRepair,
itemToRepairDetails,
repairItemIsArmor,
repairItemDetails.count,
false,
repairQualityMultiplier,
repairQualityMultiplier !== 0 && this.repairConfig.applyRandomizeDurabilityLoss,
);
// get repair price
const itemRepairCost = this.databaseServer.getTables().templates.items[itemToRepair._tpl]._props.RepairCost;
const repairCost = Math.round(
(itemRepairCost * repairItemDetails.count * repairRate) * this.repairConfig.priceMultiplier,
);
this.logger.debug(`item base repair cost: ${itemRepairCost}`, true);
this.logger.debug(`price multipler: ${this.repairConfig.priceMultiplier}`, true);
this.logger.debug(`repair cost: ${repairCost}`, true);
return {
repairCost: repairCost,
repairedItem: itemToRepair,
repairedItemIsArmor: repairItemIsArmor,
repairAmount: repairItemDetails.count,
repairedByKit: false,
};
}
/**
* @param sessionID Session id
* @param pmcData profile to take money from
* @param repairedItemId Repaired item id
* @param repairCost Cost to repair item in roubles
* @param traderId Id of the trader who repaired the item / who is paid
* @param output
*/
public payForRepair(
sessionID: string,
pmcData: IPmcData,
repairedItemId: string,
repairCost: number,
traderId: string,
output: IItemEventRouterResponse,
): void
{
const options: IProcessBuyTradeRequestData = {
scheme_items: [{
id: "5449016a4bdc2d6f028b456f", // Rouble tpl
count: Math.round(repairCost)
}],
tid: traderId,
Action: "SptRepair",
type: "",
item_id: "",
count: 0,
scheme_id: 0,
};
this.paymentService.payMoney(pmcData, options, sessionID, output);
}
/**
* Add skill points to profile after repairing an item
* @param sessionId Session id
* @param repairDetails details of item repaired, cost/item
* @param pmcData Profile to add points to
*/
public addRepairSkillPoints(sessionId: string, repairDetails: RepairDetails, pmcData: IPmcData): void
{
// Handle kit repair of weapon
if (
repairDetails.repairedByKit
&& this.itemHelper.isOfBaseclass(repairDetails.repairedItem._tpl, BaseClasses.WEAPON)
)
{
const skillPoints = this.getWeaponRepairSkillPoints(repairDetails);
if (skillPoints > 0)
{
this.profileHelper.addSkillPointsToPlayer(pmcData, SkillTypes.WEAPON_TREATMENT, skillPoints, true);
}
}
// Handle kit repair of armor
if (
repairDetails.repairedByKit
&& this.itemHelper.isOfBaseclasses(repairDetails.repairedItem._tpl, [BaseClasses.ARMOR, BaseClasses.VEST])
)
{
const itemDetails = this.itemHelper.getItem(repairDetails.repairedItem._tpl);
if (!itemDetails[0])
{
// No item found
this.logger.error(
this.localisationService.getText(
"repair-unable_to_find_item_in_db",
repairDetails.repairedItem._tpl,
),
);
return;
}
const isHeavyArmor = itemDetails[1]._props.ArmorType === "Heavy";
const vestSkillToLevel = isHeavyArmor ? SkillTypes.HEAVY_VESTS : SkillTypes.LIGHT_VESTS;
const pointsToAddToVestSkill = repairDetails.repairPoints
* this.repairConfig.armorKitSkillPointGainPerRepairPointMultiplier;
this.profileHelper.addSkillPointsToPlayer(pmcData, vestSkillToLevel, pointsToAddToVestSkill);
}
// Handle giving INT to player - differs if using kit/trader and weapon vs armor
const intellectGainedFromRepair = this.getIntellectGainedFromRepair(repairDetails);
if (intellectGainedFromRepair > 0)
{
this.profileHelper.addSkillPointsToPlayer(pmcData, SkillTypes.INTELLECT, intellectGainedFromRepair);
}
}
protected getIntellectGainedFromRepair(repairDetails: RepairDetails): number
{
if (repairDetails.repairedByKit)
{
// Weapons/armor have different multipliers
const intRepairMultiplier =
(this.itemHelper.isOfBaseclass(repairDetails.repairedItem._tpl, BaseClasses.WEAPON))
? this.repairConfig.repairKitIntellectGainMultiplier.weapon
: this.repairConfig.repairKitIntellectGainMultiplier.armor;
// Limit gain to a max value defined in config.maxIntellectGainPerRepair
return Math.min(
repairDetails.repairPoints * intRepairMultiplier,
this.repairConfig.maxIntellectGainPerRepair.kit,
);
}
else
{
// Trader repair - Not as accurate as kit, needs data from live
return Math.min(
repairDetails.repairAmount / 10,
this.repairConfig.maxIntellectGainPerRepair.trader,
);
}
}
/**
* Return an appromixation of the amount of skill points live would return for the given repairDetails
* @param repairDetails the repair details to calculate skill points for
* @returns the number of skill points to reward the user
*/
protected getWeaponRepairSkillPoints(repairDetails: RepairDetails): number
{
// 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
const gainMult = this.repairConfig.weaponTreatment.pointGainMultiplier;
// First we get a baseline based on our repair amount, and gain multiplier with a bit of rounding
const step1 = Math.ceil(repairDetails.repairAmount / 2) * gainMult;
// Then we have to get the next even number
const step2 = Math.ceil(step1 / 2) * 2;
// Then multiply by 2 again to hopefully get to what live would give us
let 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 (Math.random() <= this.repairConfig.weaponTreatment.critFailureChance)
{
skillPoints -= this.repairConfig.weaponTreatment.critFailureAmount;
}
// Add a random chance to crit-succeed
if (Math.random() <= this.repairConfig.weaponTreatment.critSuccessChance)
{
skillPoints += this.repairConfig.weaponTreatment.critSuccessAmount;
}
return Math.max(skillPoints, 0);
}
/**
* @param sessionId Session id
* @param pmcData Profile to update repaired item in
* @param repairKits Array of Repair kits to use
* @param itemToRepairId Item id to repair
* @param output IItemEventRouterResponse
* @returns Details of repair, item/price
*/
public repairItemByKit(
sessionId: string,
pmcData: IPmcData,
repairKits: RepairKitsInfo[],
itemToRepairId: string,
output: IItemEventRouterResponse,
): RepairDetails
{
// Find item to repair in inventory
const itemToRepair = pmcData.Inventory.items.find((x: { _id: string; }) => x._id === itemToRepairId);
if (itemToRepair === undefined)
{
throw new Error(`Item ${itemToRepairId} not found, unable to repair`);
}
const itemsDb = this.databaseServer.getTables().templates.items;
const itemToRepairDetails = itemsDb[itemToRepair._tpl];
const repairItemIsArmor = !!itemToRepairDetails._props.ArmorMaterial;
const repairAmount = repairKits[0].count / this.getKitDivisor(itemToRepairDetails, repairItemIsArmor, pmcData);
const shouldApplyDurabilityLoss = this.shouldRepairKitApplyDurabilityLoss(
pmcData,
this.repairConfig.applyRandomizeDurabilityLoss,
);
this.repairHelper.updateItemDurability(
itemToRepair,
itemToRepairDetails,
repairItemIsArmor,
repairAmount,
true,
1,
shouldApplyDurabilityLoss,
);
// Find and use repair kit defined in body
for (const repairKit of repairKits)
{
const repairKitInInventory = pmcData.Inventory.items.find((x) => x._id === repairKit._id);
const repairKitDetails = itemsDb[repairKitInInventory._tpl];
const repairKitReductionAmount = repairKit.count;
this.addMaxResourceToKitIfMissing(repairKitDetails, repairKitInInventory);
// reduce usages on repairkit used
repairKitInInventory.upd.RepairKit.Resource -= repairKitReductionAmount;
output.profileChanges[sessionId].items.change.push(repairKitInInventory);
}
return {
repairPoints: repairKits[0].count,
repairedItem: itemToRepair,
repairedItemIsArmor: repairItemIsArmor,
repairAmount: repairAmount,
repairedByKit: true,
};
}
/**
* Calculate value repairkit points need to be divided by to get the durability points to be added to an item
* @param itemToRepairDetails Item to repair details
* @param isArmor Is the item being repaired armor
* @param pmcData Player profile
* @returns Number to divide kit points by
*/
protected getKitDivisor(itemToRepairDetails: ITemplateItem, isArmor: boolean, pmcData: IPmcData): number
{
const globals = this.databaseServer.getTables().globals;
const globalRepairSettings = globals.config.RepairSettings;
const intellectRepairPointsPerLevel = globals.config.SkillsSettings.Intellect.RepairPointsCostReduction;
const profileIntellectLevel = this.profileHelper.getSkillFromProfile(pmcData, SkillTypes.INTELLECT)?.Progress
?? 0;
const intellectPointReduction = intellectRepairPointsPerLevel * Math.trunc(profileIntellectLevel / 100);
if (isArmor)
{
const durabilityPointCostArmor = globalRepairSettings.durabilityPointCostArmor;
const repairArmorBonus = this.getBonusMultiplierValue("RepairArmorBonus", pmcData);
const armorBonus = 1.0 - (repairArmorBonus - 1.0) - intellectPointReduction;
const materialType = itemToRepairDetails._props.ArmorMaterial ?? "";
const armorMaterial = globals.config.ArmorMaterials[materialType] as IArmorType;
const destructability = 1 + armorMaterial.Destructibility;
const armorClass = parseInt(`${itemToRepairDetails._props.armorClass}`);
const armorClassDivisor = globals.config.RepairSettings.armorClassDivisor;
const armorClassMultiplier = 1.0 + armorClass / armorClassDivisor;
return durabilityPointCostArmor * armorBonus * destructability * armorClassMultiplier;
}
else
{
const repairWeaponBonus = this.getBonusMultiplierValue("RepairWeaponBonus", pmcData) - 1;
const repairPointMultiplier = 1.0 - repairWeaponBonus - intellectPointReduction;
const durabilityPointCostGuns = globals.config.RepairSettings.durabilityPointCostGuns;
return durabilityPointCostGuns * repairPointMultiplier;
}
}
/**
* Get the bonus multiplier for a skill from a player profile
* @param skillBonusName Name of bonus to get multipler of
* @param pmcData Player profile to look in for skill
* @returns Multiplier value
*/
protected getBonusMultiplierValue(skillBonusName: string, pmcData: IPmcData): number
{
const bonusesMatched = pmcData?.Bonuses?.filter((b) => b.type === skillBonusName);
let value = 1;
if (bonusesMatched != null)
{
const sumedPercentage = bonusesMatched.map((b) => b.value).reduce((v1, v2) => v1 + v2, 0);
value = 1 + sumedPercentage / 100;
}
return value;
}
/**
* Should a repair kit apply total durability loss on repair
* @param pmcData Player profile
* @param applyRandomizeDurabilityLoss Value from repair config
* @returns True if loss should be applied
*/
protected shouldRepairKitApplyDurabilityLoss(pmcData: IPmcData, applyRandomizeDurabilityLoss: boolean): boolean
{
let shouldApplyDurabilityLoss = applyRandomizeDurabilityLoss;
if (shouldApplyDurabilityLoss)
{
// Random loss not disabled via config, perform charisma check
const hasEliteCharisma = this.profileHelper.hasEliteSkillLevel(SkillTypes.CHARISMA, pmcData);
if (hasEliteCharisma)
{
// 50/50 chance of loss being ignored at elite level
shouldApplyDurabilityLoss = this.randomUtil.getChance100(50);
}
}
return shouldApplyDurabilityLoss;
}
/**
* Update repair kits Resource object if it doesn't exist
* @param repairKitDetails Repair kit details from db
* @param repairKitInInventory Repair kit to update
*/
protected addMaxResourceToKitIfMissing(repairKitDetails: ITemplateItem, repairKitInInventory: Item): void
{
const maxRepairAmount = repairKitDetails._props.MaxRepairResource;
if (!repairKitInInventory.upd)
{
this.logger.debug(`Repair kit: ${repairKitInInventory._id} in inventory lacks upd object, adding`);
repairKitInInventory.upd = { RepairKit: { Resource: maxRepairAmount } };
}
if (!repairKitInInventory.upd.RepairKit?.Resource)
{
repairKitInInventory.upd.RepairKit = { Resource: maxRepairAmount };
}
}
/**
* Chance to apply buff to an item (Armor/weapon) if repaired by armor kit
* @param repairDetails Repair details of item
* @param pmcData Player profile
*/
public addBuffToItem(repairDetails: RepairDetails, pmcData: IPmcData): void
{
// Buffs are repair kit only
if (!repairDetails.repairedByKit)
{
return;
}
if (this.shouldBuffItem(repairDetails, pmcData))
{
if (this.itemHelper.isOfBaseclasses(repairDetails.repairedItem._tpl, [BaseClasses.ARMOR, BaseClasses.VEST, BaseClasses.HEADWEAR]))
{
const armorConfig = this.repairConfig.repairKit.armor;
this.addBuff(armorConfig, repairDetails.repairedItem);
}
else if (this.itemHelper.isOfBaseclass(repairDetails.repairedItem._tpl, BaseClasses.WEAPON))
{
const weaponConfig = this.repairConfig.repairKit.weapon;
this.addBuff(weaponConfig, repairDetails.repairedItem);
}
// TODO: Knife repair kits may be added at some point, a bracket needs to be added here
}
}
/**
* Add random buff to item
* @param itemConfig weapon/armor config
* @param repairDetails Details for item to repair
*/
public addBuff(itemConfig: BonusSettings, item: Item): void
{
const bonusRarity = this.weightedRandomHelper.getWeightedValue<string>(itemConfig.rarityWeight);
const bonusType = this.weightedRandomHelper.getWeightedValue<string>(itemConfig.bonusTypeWeight);
const bonusValues = itemConfig[bonusRarity][bonusType].valuesMinMax;
const bonusValue = this.randomUtil.getFloat(bonusValues.min, bonusValues.max);
const bonusThresholdPercents = itemConfig[bonusRarity][bonusType].activeDurabilityPercentMinMax;
const bonusThresholdPercent = this.randomUtil.getInt(bonusThresholdPercents.min, bonusThresholdPercents.max);
item.upd.Buff = {
rarity: bonusRarity,
buffType: bonusType,
value: bonusValue,
thresholdDurability: this.randomUtil.getPercentOfValue(
bonusThresholdPercent,
item.upd.Repairable.Durability,
),
};
}
/**
* Check if item should be buffed by checking the item type and relevant player skill level
* @param repairDetails Item that was repaired
* @param itemTpl tpl of item to be buffed
* @param pmcData Player profile
* @returns True if item should have buff applied
*/
protected shouldBuffItem(repairDetails: RepairDetails, pmcData: IPmcData): boolean
{
const globals = this.databaseServer.getTables().globals;
const hasTemplate = this.itemHelper.getItem(repairDetails.repairedItem._tpl);
if (!hasTemplate[0])
{
return false;
}
const template = hasTemplate[1];
const itemSkillType = this.getItemSkillType(template);
if (!itemSkillType)
{
return false;
}
const commonBuffMinChanceValue =
globals.config.SkillsSettings[itemSkillType as string].BuffSettings.CommonBuffMinChanceValue;
const commonBuffChanceLevelBonus =
globals.config.SkillsSettings[itemSkillType as string].BuffSettings.CommonBuffChanceLevelBonus;
const receivedDurabilityMaxPercent =
globals.config.SkillsSettings[itemSkillType as string].BuffSettings.ReceivedDurabilityMaxPercent;
const skillLevel = Math.trunc(
(this.profileHelper.getSkillFromProfile(pmcData, itemSkillType)?.Progress ?? 0) / 100,
);
const durabilityToRestorePercent = repairDetails.repairPoints / template._props.MaxDurability;
const durabilityMultiplier = this.getDurabilityMultiplier(
receivedDurabilityMaxPercent,
durabilityToRestorePercent,
);
const doBuff = commonBuffMinChanceValue + commonBuffChanceLevelBonus * skillLevel * durabilityMultiplier;
if (Math.random() <= doBuff)
{
return true;
}
return false;
}
/**
* Based on item, what underlying skill does this item use for buff settings
* @param itemTemplate Item to check for skill
* @returns Skill name
*/
protected getItemSkillType(itemTemplate: ITemplateItem): SkillTypes
{
if (this.itemHelper.isOfBaseclasses(itemTemplate._id, [BaseClasses.ARMOR, BaseClasses.VEST, BaseClasses.HEADWEAR]))
{
if (itemTemplate._props.ArmorType === "Light")
{
return SkillTypes.LIGHT_VESTS;
}
else if (itemTemplate._props.ArmorType === "Heavy")
{
return SkillTypes.HEAVY_VESTS;
}
}
else if (this.itemHelper.isOfBaseclass(itemTemplate._id, BaseClasses.WEAPON))
{
return SkillTypes.WEAPON_TREATMENT;
}
else if (this.itemHelper.isOfBaseclass(itemTemplate._id, BaseClasses.KNIFE))
{
return SkillTypes.MELEE;
}
return undefined;
}
/**
* Ensure multiplier is between 1 and 0.01
* @param receiveDurabilityMaxPercent Max durabiltiy percent
* @param receiveDurabilityPercent current durability percent
* @returns durability multipler value
*/
protected getDurabilityMultiplier(receiveDurabilityMaxPercent: number, receiveDurabilityPercent: number): number
{
receiveDurabilityMaxPercent = (receiveDurabilityMaxPercent > 0) ? receiveDurabilityMaxPercent : 0.01;
const num = receiveDurabilityPercent / receiveDurabilityMaxPercent;
if (num > 1)
{
return 1.0;
}
if (num < 0.01)
{
return 0.01;
}
return num;
}
}
export class RepairDetails
{
repairCost?: number;
repairPoints?: number;
repairedItem: Item;
repairedItemIsArmor: boolean;
repairAmount: number;
repairedByKit: boolean;
}