611b63a4cd
Co-authored-by: Dev <dev@noreply.dev.sp-tarkov.com> Reviewed-on: https://dev.sp-tarkov.com/SPT-AKI/Server/pulls/72
454 lines
19 KiB
TypeScript
454 lines
19 KiB
TypeScript
import { inject, injectable } from "tsyringe";
|
|
|
|
import { ItemHelper } from "../helpers/ItemHelper";
|
|
import { QuestHelper } from "../helpers/QuestHelper";
|
|
import { RepairHelper } from "../helpers/RepairHelper";
|
|
import { TraderHelper } from "../helpers/TraderHelper";
|
|
import { WeightedRandomHelper } from "../helpers/WeightedRandomHelper";
|
|
import { ArmorType } from "../models/eft/common/IGlobals";
|
|
import { IPmcData } from "../models/eft/common/IPmcData";
|
|
import { Item } from "../models/eft/common/tables/IItem";
|
|
import { ITemplateItem } from "../models/eft/common/tables/ITemplateItem";
|
|
import { IItemEventRouterResponse } from "../models/eft/itemEvent/IItemEventRouterResponse";
|
|
import { RepairKitsInfo } from "../models/eft/repair/IRepairActionDataRequest";
|
|
import { RepairItem } from "../models/eft/repair/ITraderRepairActionDataRequest";
|
|
import { IProcessBuyTradeRequestData } from "../models/eft/trade/IProcessBuyTradeRequestData";
|
|
import { BaseClasses } from "../models/enums/BaseClasses";
|
|
import { ConfigTypes } from "../models/enums/ConfigTypes";
|
|
import { BonusSettings, IRepairConfig } from "../models/spt/config/IRepairConfig";
|
|
import { ILogger } from "../models/spt/utils/ILogger";
|
|
import { ConfigServer } from "../servers/ConfigServer";
|
|
import { DatabaseServer } from "../servers/DatabaseServer";
|
|
import { RandomUtil } from "../utils/RandomUtil";
|
|
import { PaymentService } from "./PaymentService";
|
|
|
|
@injectable()
|
|
export class RepairService
|
|
{
|
|
protected repairConfig: IRepairConfig;
|
|
constructor(
|
|
@inject("WinstonLogger") protected logger: ILogger,
|
|
@inject("DatabaseServer") protected databaseServer: DatabaseServer,
|
|
@inject("QuestHelper") protected questHelper: QuestHelper,
|
|
@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("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 = 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 = {
|
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
scheme_items: [
|
|
{
|
|
id: repairedItemId,
|
|
count: Math.round(repairCost)
|
|
}
|
|
],
|
|
tid: traderId,
|
|
Action: "",
|
|
type: "",
|
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
item_id: "",
|
|
count: 0,
|
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
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
|
|
{
|
|
if (this.itemHelper.isOfBaseclass(repairDetails.repairedItem._tpl, BaseClasses.WEAPON))
|
|
{
|
|
const progress = this.databaseServer.getTables().globals.config.SkillsSettings.WeaponTreatment.SkillPointsPerRepair;
|
|
this.questHelper.rewardSkillPoints(sessionId, pmcData, "WeaponTreatment", progress);
|
|
}
|
|
|
|
// Handle kit repairs 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])
|
|
{
|
|
this.logger.error(`Unable to find item ${repairDetails.repairedItem._tpl} in items db, cannot add skill points`);
|
|
return;
|
|
}
|
|
|
|
const isHeavyArmor = itemDetails[1]._props.ArmorType === "Heavy";
|
|
const skillToLevel = (isHeavyArmor) ? "HeavyVests" : "LightVests";
|
|
const pointsToAddToSkill = repairDetails.repairAmount * this.repairConfig.armorKitSkillPointGainPerRepairPointMultiplier;
|
|
|
|
this.questHelper.rewardSkillPoints(sessionId, pmcData, skillToLevel, pointsToAddToSkill);
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @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 itemToRepairDetails = this.databaseServer.getTables().templates.items[itemToRepair._tpl];
|
|
const repairItemIsArmor = (!!itemToRepairDetails._props.ArmorMaterial);
|
|
|
|
this.repairHelper.updateItemDurability(
|
|
itemToRepair,
|
|
itemToRepairDetails,
|
|
repairItemIsArmor,
|
|
repairKits[0].count / this.getKitDivisor(itemToRepairDetails, repairItemIsArmor, pmcData),
|
|
this.repairConfig.applyRandomizeDurabilityLoss,
|
|
1);
|
|
|
|
// 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 = this.databaseServer.getTables().templates.items[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 {
|
|
repairedItem: itemToRepair,
|
|
repairedItemIsArmor: repairItemIsArmor,
|
|
repairAmount: repairKits[0].count,
|
|
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 = pmcData.Skills?.Common?.find(s => s.Id === "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 ArmorType;
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* 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.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]))
|
|
{
|
|
const armorConfig = this.repairConfig.repairKit.armor;
|
|
this.addBuff(armorConfig, repairDetails);
|
|
}
|
|
else if (this.itemHelper.isOfBaseclass(repairDetails.repairedItem._tpl, BaseClasses.WEAPON))
|
|
{
|
|
const weaponConfig = this.repairConfig.repairKit.weapon;
|
|
this.addBuff(weaponConfig, repairDetails);
|
|
}
|
|
// TODO: Knife repair kits may be added at some point, a bracket needs to be added here
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add buff to item
|
|
* @param itemConfig weapon/armor config
|
|
* @param repairDetails Details for item to repair
|
|
*/
|
|
protected addBuff(itemConfig: BonusSettings, repairDetails: RepairDetails): void
|
|
{
|
|
const bonusRarity = this.weightedRandomHelper.getWeightedInventoryItem(itemConfig.rarityWeight);
|
|
const bonusType = this.weightedRandomHelper.getWeightedInventoryItem(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);
|
|
|
|
repairDetails.repairedItem.upd.Buff = {
|
|
rarity: bonusRarity,
|
|
buffType: bonusType,
|
|
value: bonusValue,
|
|
thresholdDurability: this.randomUtil.getPercentOfValue(bonusThresholdPercent, repairDetails.repairedItem.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].BuffSettings.CommonBuffMinChanceValue;
|
|
const commonBuffChanceLevelBonus = globals.config.SkillsSettings[itemSkillType].BuffSettings.CommonBuffChanceLevelBonus;
|
|
const receivedDurabilityMaxPercent = globals.config.SkillsSettings[itemSkillType].BuffSettings.ReceivedDurabilityMaxPercent;
|
|
|
|
const skillLevel = Math.trunc((pmcData?.Skills?.Common?.find(s => s.Id === itemSkillType)?.Progress ?? 0) / 100);
|
|
|
|
const durabilityToRestorePercent = repairDetails.repairAmount / 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): string
|
|
{
|
|
if (this.itemHelper.isOfBaseclass(itemTemplate._id, BaseClasses.ARMOR))
|
|
{
|
|
if (itemTemplate._props.ArmorType === "Light")
|
|
{
|
|
return "LightVests";
|
|
}
|
|
else if (itemTemplate._props.ArmorType === "Heavy")
|
|
{
|
|
return "HeavyVests";
|
|
}
|
|
}
|
|
else if (this.itemHelper.isOfBaseclass(itemTemplate._id, BaseClasses.WEAPON))
|
|
{
|
|
return "WeaponTreatment";
|
|
}
|
|
else if (this.itemHelper.isOfBaseclass(itemTemplate._id, BaseClasses.KNIFE))
|
|
{
|
|
return "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;
|
|
repairedItem: Item;
|
|
repairedItemIsArmor: boolean;
|
|
repairAmount: number;
|
|
repairedByKit: boolean;
|
|
} |