Implement live-like calculation of Weapon Maintenance XP (!164)

- Implement formula based on 30 weapon repairs done on live
- Return the repair amount as `repairAmount` from `repairItemByKit`
- Add an additional `repairPoints` to `RepairDetails` to return the repair points used
- Update `repairAmount` references to `repairPoints` to keep old behavior
- Add new parameter to rewardSkillPoints that applies live-like level scaling
- Only give weapon maintenance XP when using a repair kit

This implementation comes with a "Crit Fail" and "Crit Success" mechanic to account for live sometimes randomly being -4 or +4 off from my estimated values. By default the chance of each is 10%, and they can overlap and cancel each other out

Spreadsheet of live repair data:
https://docs.google.com/spreadsheets/d/1-tR4WYelhZfKZ3ZDbxr3nd73Y60E1wQRjDWONpMVSew/edit?usp=sharing

Useful columns:
C: The amount of dura attempted to be repaired, this is used in the estimated skill calculated
G: Hand entered value of how much skill gain I actually saw on live (Multiplied by 10 for readability. So "3.2" would be "32")
J: The estimated skill gain, based on the calculation included in this merge request
K: How far off the estimated skill gain was (Negative implies we guessed high and the actual result was lower)

One thing of note:
I've modified all the existing references to `repairAmount` to be the new `repairPoints` when a repair kit is used. This is to keep the existing behaviour outside of my direct changes as much as possible.

However, this seems to be incorrect in some cases (For example, buff chance is repairPoints/maxDura, but repairPoints will go down the higher your int. I'm assuming this is meant to be repairedDura/maxDura). May want to update these references to use `repairAmount` once they've been confirmed to expect the repair amount instead of repair points used.

Co-authored-by: DrakiaXYZ <565558+TheDgtl@users.noreply.github.com>
Reviewed-on: https://dev.sp-tarkov.com/SPT-AKI/Server/pulls/164
Co-authored-by: DrakiaXYZ <drakiaxyz@noreply.dev.sp-tarkov.com>
Co-committed-by: DrakiaXYZ <drakiaxyz@noreply.dev.sp-tarkov.com>
This commit is contained in:
DrakiaXYZ 2023-11-02 08:56:02 +00:00 committed by chomp
parent d74b505a7b
commit ebeda336db
4 changed files with 127 additions and 10 deletions

View File

@ -11,6 +11,13 @@
"kit": 0.6,
"trader": 0.6
},
"weaponTreatment": {
"critSuccessChance": 0.10,
"critSuccessAmount": 4,
"critFailureChance": 0.10,
"critFailureAmount": 4,
"pointGainMultiplier": 0.6
},
"repairKit": {
"armor": {
"rarityWeight": {

View File

@ -8,7 +8,7 @@ import { QuestConditionHelper } from "@spt-aki/helpers/QuestConditionHelper";
import { RagfairServerHelper } from "@spt-aki/helpers/RagfairServerHelper";
import { TraderHelper } from "@spt-aki/helpers/TraderHelper";
import { IPmcData } from "@spt-aki/models/eft/common/IPmcData";
import { IQuestStatus } from "@spt-aki/models/eft/common/tables/IBotBase";
import { Common, IQuestStatus } from "@spt-aki/models/eft/common/tables/IBotBase";
import { Item } from "@spt-aki/models/eft/common/tables/IItem";
import { AvailableForConditions, AvailableForProps, IQuest, Reward } from "@spt-aki/models/eft/common/tables/IQuest";
import { IItemEventRouterResponse } from "@spt-aki/models/eft/itemEvent/IItemEventRouterResponse";
@ -135,7 +135,7 @@ export class QuestHelper
* @param skillName Name of skill to increase skill points of
* @param progressAmount Amount of skill points to add to skill
*/
public rewardSkillPoints(sessionID: string, pmcData: IPmcData, skillName: string, progressAmount: number): void
public rewardSkillPoints(sessionID: string, pmcData: IPmcData, skillName: string, progressAmount: number, scaleToSkillLevel: boolean = false): void
{
const indexOfSkillToUpdate = pmcData.Skills.Common.findIndex(s => s.Id === skillName);
if (indexOfSkillToUpdate === -1)
@ -153,10 +153,66 @@ export class QuestHelper
return;
}
// Tarkov has special handling of skills under level 9 to scale them to the lower XP requirement
if (scaleToSkillLevel)
{
progressAmount = this.adjustSkillExpForLowLevels(profileSkill, progressAmount);
}
profileSkill.Progress += progressAmount;
profileSkill.LastAccess = this.timeUtil.getTimestamp();
}
/**
* Adjust skill experience for low skill levels, mimicing the official client
* @param profileSkill the skill experience is being added to
* @param progressAmount the amount of experience being added to the skill
* @returns the adjusted skill progress gain
*/
public adjustSkillExpForLowLevels(profileSkill: Common, progressAmount: number): number
{
let currentLevel = Math.floor(profileSkill.Progress / 100);
// Only run this if the current level is under 9
if (currentLevel >= 9)
{
return progressAmount;
}
// This calculates how much progress we have in the skill's starting level
let startingLevelProgress = (profileSkill.Progress % 100) * ((currentLevel + 1) / 10);
// The code below assumes a 1/10th progress skill amount
let remainingProgress = progressAmount / 10;
// We have to do this loop to handle edge cases where the provided XP bumps your level up
// See "CalculateExpOnFirstLevels" in client for original logic
let adjustedSkillProgress = 0;
while (remainingProgress > 0 && currentLevel < 9)
{
// Calculate how much progress to add, limiting it to the current level max progress
const currentLevelRemainingProgress = ((currentLevel + 1) * 10) - startingLevelProgress;
this.logger.debug(`currentLevelRemainingProgress: ${currentLevelRemainingProgress}`);
const progressToAdd = Math.min(remainingProgress, currentLevelRemainingProgress);
const adjustedProgressToAdd = (10 / (currentLevel + 1)) * progressToAdd;
this.logger.debug(`Progress To Add: ${progressToAdd} Adjusted for level: ${adjustedProgressToAdd}`);
// Add the progress amount adjusted by level
adjustedSkillProgress += adjustedProgressToAdd;
remainingProgress -= progressToAdd;
startingLevelProgress = 0;
currentLevel++;
}
// If there's any remaining progress, add it. This handles if you go from level 8 -> 9
if (remainingProgress > 0)
{
adjustedSkillProgress += remainingProgress;
}
return adjustedSkillProgress;
}
/**
* Get quest name by quest id
* @param questId id to get

View File

@ -12,6 +12,7 @@ export interface IRepairConfig extends IBaseConfig
repairKitIntellectGainMultiplier: IIntellectGainValues
//** How much INT can be given to player per repair action */
maxIntellectGainPerRepair: IMaxIntellectGainValues;
weaponTreatment: IWeaponTreatmentRepairValues;
repairKit: RepairKit
}
@ -27,6 +28,18 @@ export interface IMaxIntellectGainValues
trader: number
}
export interface IWeaponTreatmentRepairValues
{
/** The chance to gain more weapon maintenance skill */
critSuccessChance: number
critSuccessAmount: number
/** The chance to gain less weapon maintenance skill */
critFailureChance: number
critFailureAmount: number
/** The multiplier used for calculating weapon maintenance XP */
pointGainMultiplier: number
}
export interface RepairKit
{
armor: BonusSettings

View File

@ -147,10 +147,11 @@ export class RepairService
repairDetails: RepairDetails,
pmcData: IPmcData): void
{
if (this.itemHelper.isOfBaseclass(repairDetails.repairedItem._tpl, BaseClasses.WEAPON))
if (repairDetails.repairedByKit && 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);
const skillPoints = this.getWeaponRepairSkillPoints(repairDetails);
this.questHelper.rewardSkillPoints(sessionId, pmcData, "WeaponTreatment", skillPoints, true);
}
// Handle kit repairs of armor
@ -167,7 +168,7 @@ export class RepairService
const isHeavyArmor = itemDetails[1]._props.ArmorType === "Heavy";
const vestSkillToLevel = (isHeavyArmor) ? "HeavyVests" : "LightVests";
const pointsToAddToVestSkill = repairDetails.repairAmount * this.repairConfig.armorKitSkillPointGainPerRepairPointMultiplier;
const pointsToAddToVestSkill = repairDetails.repairPoints * this.repairConfig.armorKitSkillPointGainPerRepairPointMultiplier;
this.questHelper.rewardSkillPoints(sessionId, pmcData, vestSkillToLevel, pointsToAddToVestSkill);
}
@ -181,7 +182,7 @@ export class RepairService
: this.repairConfig.repairKitIntellectGainMultiplier.armor;
// limit gain to a max value defined in config.maxIntellectGainPerRepair
intellectGainedFromRepair = Math.min(repairDetails.repairAmount * intRepairMultiplier, this.repairConfig.maxIntellectGainPerRepair.kit);
intellectGainedFromRepair = Math.min(repairDetails.repairPoints * intRepairMultiplier, this.repairConfig.maxIntellectGainPerRepair.kit);
}
else
{
@ -191,6 +192,43 @@ export class RepairService
this.questHelper.rewardSkillPoints(sessionId, pmcData, SkillTypes.INTELLECT, intellectGainedFromRepair);
}
/**
* 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 skillPoints;
}
/**
*
@ -218,12 +256,13 @@ export class RepairService
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);
this.repairHelper.updateItemDurability(
itemToRepair,
itemToRepairDetails,
repairItemIsArmor,
repairKits[0].count / this.getKitDivisor(itemToRepairDetails, repairItemIsArmor, pmcData),
repairAmount,
true,
1,
this.repairConfig.applyRandomizeDurabilityLoss);
@ -244,9 +283,10 @@ export class RepairService
}
return {
repairPoints: repairKits[0].count,
repairedItem: itemToRepair,
repairedItemIsArmor: repairItemIsArmor,
repairAmount: repairKits[0].count,
repairAmount: repairAmount,
repairedByKit: true
};
}
@ -414,7 +454,7 @@ export class RepairService
const skillLevel = Math.trunc((pmcData?.Skills?.Common?.find(s => s.Id === itemSkillType)?.Progress ?? 0) / 100);
const durabilityToRestorePercent = repairDetails.repairAmount / template._props.MaxDurability;
const durabilityToRestorePercent = repairDetails.repairPoints / template._props.MaxDurability;
const durabilityMultiplier = this.getDurabilityMultiplier(receivedDurabilityMaxPercent, durabilityToRestorePercent);
const doBuff = commonBuffMinChanceValue + commonBuffChanceLevelBonus * skillLevel * durabilityMultiplier;
@ -483,6 +523,7 @@ export class RepairService
export class RepairDetails
{
repairCost?: number;
repairPoints?: number;
repairedItem: Item;
repairedItemIsArmor: boolean;
repairAmount: number;