Merge branch 'master' of https://dev.sp-tarkov.com/SPT-AKI/Server into 3.8.0

This commit is contained in:
Dev 2023-11-02 09:00:14 +00:00
commit 0a85f6f319
9 changed files with 159 additions and 39 deletions

View File

@ -1460,6 +1460,8 @@
"minKills": 1, "minKills": 1,
"maxBossKills": 1, "maxBossKills": 1,
"minBossKills": 1, "minBossKills": 1,
"maxPmcKills": 2,
"minPmcKills": 1,
"weaponRequirementProb": 0, "weaponRequirementProb": 0,
"weaponCategoryRequirementProb": 0.3, "weaponCategoryRequirementProb": 0.3,
"weaponCategoryRequirements": [{ "weaponCategoryRequirements": [{
@ -1573,6 +1575,8 @@
"minKills": 3, "minKills": 3,
"maxBossKills": 3, "maxBossKills": 3,
"minBossKills": 1, "minBossKills": 1,
"maxPmcKills": 5,
"minPmcKills": 2,
"weaponRequirementProb": 0, "weaponRequirementProb": 0,
"weaponCategoryRequirementProb": 0.3, "weaponCategoryRequirementProb": 0.3,
"weaponCategoryRequirements": [{ "weaponCategoryRequirements": [{

View File

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

View File

@ -2073,7 +2073,6 @@
"Оливье", "Оливье",
"Подружка", "Подружка",
"Шмыга", "Шмыга",
"Шнур",
"Сырок", "Сырок",
"Улётный", "Улётный",
"Васёк", "Васёк",

View File

@ -28,7 +28,6 @@ export class BotController
{ {
protected botConfig: IBotConfig; protected botConfig: IBotConfig;
protected pmcConfig: IPmcConfig; protected pmcConfig: IPmcConfig;
public static readonly pmcTypeLabel = "PMC";
constructor( constructor(
@inject("WinstonLogger") protected logger: ILogger, @inject("WinstonLogger") protected logger: ILogger,

View File

@ -244,11 +244,11 @@ export class RepeatableQuestGenerator
{ {
// get all boss spawn information // get all boss spawn information
const bossSpawns = Object.values(this.databaseServer.getTables().locations).filter(x => "base" in x && "Id" in x.base).map( const bossSpawns = Object.values(this.databaseServer.getTables().locations).filter(x => "base" in x && "Id" in x.base).map(
(x) => ({ "Id": x.base.Id, "BossSpawn": x.base.BossLocationSpawn }) (x) => ({ Id: x.base.Id, BossSpawn: x.base.BossLocationSpawn })
); );
// filter for the current boss to spawn on map // filter for the current boss to spawn on map
const thisBossSpawns = bossSpawns.map( const thisBossSpawns = bossSpawns.map(
(x) => ({ "Id": x.Id, "BossSpawn": x.BossSpawn.filter(e => e.BossName === targetKey) }) (x) => ({ Id: x.Id, BossSpawn: x.BossSpawn.filter(e => e.BossName === targetKey) })
).filter(x => x.BossSpawn.length > 0); ).filter(x => x.BossSpawn.length > 0);
// remove blacklisted locations // remove blacklisted locations
const allowedSpawns = thisBossSpawns.filter(x => !eliminationConfig.distLocationBlacklist.includes(x.Id)); const allowedSpawns = thisBossSpawns.filter(x => !eliminationConfig.distLocationBlacklist.includes(x.Id));
@ -315,9 +315,11 @@ export class RepeatableQuestGenerator
const availableForFinishCondition = quest.conditions.AvailableForFinish[0]; const availableForFinishCondition = quest.conditions.AvailableForFinish[0];
availableForFinishCondition._props.counter.id = this.objectId.generate(); availableForFinishCondition._props.counter.id = this.objectId.generate();
availableForFinishCondition._props.counter.conditions = []; availableForFinishCondition._props.counter.conditions = [];
// Only add specific location condition if specific map selected
if (locationKey !== "any") if (locationKey !== "any")
{ {
availableForFinishCondition._props.counter.conditions.push(this.generateEliminationLocation(locationsConfig[locationKey], allowedWeapon, allowedWeaponsCategory)); availableForFinishCondition._props.counter.conditions.push(this.generateEliminationLocation(locationsConfig[locationKey]));
} }
availableForFinishCondition._props.counter.conditions.push(this.generateEliminationCondition(targetKey, bodyPartsToClient, distance, allowedWeapon, allowedWeaponsCategory)); availableForFinishCondition._props.counter.conditions.push(this.generateEliminationCondition(targetKey, bodyPartsToClient, distance, allowedWeapon, allowedWeaponsCategory));
availableForFinishCondition._props.value = desiredKillCount; availableForFinishCondition._props.value = desiredKillCount;
@ -356,9 +358,9 @@ export class RepeatableQuestGenerator
* This is a helper method for GenerateEliminationQuest to create a location condition. * This is a helper method for GenerateEliminationQuest to create a location condition.
* *
* @param {string} location the location on which to fulfill the elimination quest * @param {string} location the location on which to fulfill the elimination quest
* @returns {object} object of "Elimination"-location-subcondition * @returns {IEliminationCondition} object of "Elimination"-location-subcondition
*/ */
protected generateEliminationLocation(location: string[], allowedWeapon: string, allowedWeaponCategory: string): IEliminationCondition protected generateEliminationLocation(location: string[]): IEliminationCondition
{ {
const propsObject: IEliminationCondition = { const propsObject: IEliminationCondition = {
_props: { _props: {
@ -369,29 +371,19 @@ export class RepeatableQuestGenerator
_parent: "Location" _parent: "Location"
}; };
if (allowedWeapon)
{
propsObject._props.weapon = [allowedWeapon];
}
if (allowedWeaponCategory)
{
propsObject._props.weaponCategories = [allowedWeaponCategory];
}
return propsObject; return propsObject;
} }
/** /**
* A repeatable quest, besides some more or less static components, exists of reward and condition (see assets/database/templates/repeatableQuests.json) * Create kill condition for an elimination quest
* This is a helper method for GenerateEliminationQuest to create a kill condition. * @param target Bot type target of elimination quest e.g. "AnyPmc", "Savage"
* * @param targetedBodyParts Body parts player must hit
* @param {string} target array of target npcs e.g. "AnyPmc", "Savage" * @param distance Distance from which to kill (currently only >= supported
* @param {array} bodyParts array of body parts with which to kill e.g. ["stomach", "thorax"] * @param allowedWeapon What weapon must be used - undefined = any
* @param {number} distance distance from which to kill (currently only >= supported) * @param allowedWeaponCategory What category of weapon must be used - undefined = any
* @returns {object} object of "Elimination"-kill-subcondition * @returns IEliminationCondition object
*/ */
protected generateEliminationCondition(target: string, bodyPart: string[], distance: number, allowedWeapon: string, allowedWeaponCategory: string): IEliminationCondition protected generateEliminationCondition(target: string, targetedBodyParts: string[], distance: number, allowedWeapon: string, allowedWeaponCategory: string): IEliminationCondition
{ {
const killConditionProps: IKillConditionProps = { const killConditionProps: IKillConditionProps = {
target: target, target: target,
@ -406,9 +398,10 @@ export class RepeatableQuestGenerator
killConditionProps.savageRole = [target]; killConditionProps.savageRole = [target];
} }
if (bodyPart) // Has specific body part hit condition
if (targetedBodyParts)
{ {
killConditionProps.bodyPart = bodyPart; killConditionProps.bodyPart = targetedBodyParts;
} }
// Dont allow distance + melee requirement // Dont allow distance + melee requirement
@ -420,11 +413,13 @@ export class RepeatableQuestGenerator
}; };
} }
// Has specific weapon requirement
if (allowedWeapon) if (allowedWeapon)
{ {
killConditionProps.weapon = [allowedWeapon]; killConditionProps.weapon = [allowedWeapon];
} }
// Has specific weapon category requirement
if (allowedWeaponCategory?.length > 0) if (allowedWeaponCategory?.length > 0)
{ {
killConditionProps.weaponCategories = [allowedWeaponCategory]; killConditionProps.weaponCategories = [allowedWeaponCategory];
@ -781,7 +776,7 @@ export class RepeatableQuestGenerator
const rewardSpreadConfig = repeatableConfig.rewardScaling.rewardSpread; const rewardSpreadConfig = repeatableConfig.rewardScaling.rewardSpread;
const reputationConfig = repeatableConfig.rewardScaling.reputation; const reputationConfig = repeatableConfig.rewardScaling.reputation;
if (isNaN(difficulty)) if (Number.isNaN(difficulty))
{ {
difficulty = 1; difficulty = 1;
this.logger.warning(this.localisationService.getText("repeatable-difficulty_was_nan")); this.logger.warning(this.localisationService.getText("repeatable-difficulty_was_nan"));

View File

@ -8,7 +8,7 @@ import { QuestConditionHelper } from "@spt-aki/helpers/QuestConditionHelper";
import { RagfairServerHelper } from "@spt-aki/helpers/RagfairServerHelper"; import { RagfairServerHelper } from "@spt-aki/helpers/RagfairServerHelper";
import { TraderHelper } from "@spt-aki/helpers/TraderHelper"; import { TraderHelper } from "@spt-aki/helpers/TraderHelper";
import { IPmcData } from "@spt-aki/models/eft/common/IPmcData"; 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 { Item } from "@spt-aki/models/eft/common/tables/IItem";
import { AvailableForConditions, AvailableForProps, IQuest, Reward } from "@spt-aki/models/eft/common/tables/IQuest"; import { AvailableForConditions, AvailableForProps, IQuest, Reward } from "@spt-aki/models/eft/common/tables/IQuest";
import { IItemEventRouterResponse } from "@spt-aki/models/eft/itemEvent/IItemEventRouterResponse"; 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 skillName Name of skill to increase skill points of
* @param progressAmount Amount of skill points to add to skill * @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); const indexOfSkillToUpdate = pmcData.Skills.Common.findIndex(s => s.Id === skillName);
if (indexOfSkillToUpdate === -1) if (indexOfSkillToUpdate === -1)
@ -153,10 +153,66 @@ export class QuestHelper
return; 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.Progress += progressAmount;
profileSkill.LastAccess = this.timeUtil.getTimestamp(); 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 * Get quest name by quest id
* @param questId id to get * @param questId id to get

View File

@ -12,6 +12,7 @@ export interface IRepairConfig extends IBaseConfig
repairKitIntellectGainMultiplier: IIntellectGainValues repairKitIntellectGainMultiplier: IIntellectGainValues
//** How much INT can be given to player per repair action */ //** How much INT can be given to player per repair action */
maxIntellectGainPerRepair: IMaxIntellectGainValues; maxIntellectGainPerRepair: IMaxIntellectGainValues;
weaponTreatment: IWeaponTreatmentRepairValues;
repairKit: RepairKit repairKit: RepairKit
} }
@ -27,6 +28,18 @@ export interface IMaxIntellectGainValues
trader: number 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 export interface RepairKit
{ {
armor: BonusSettings armor: BonusSettings

View File

@ -147,10 +147,11 @@ export class RepairService
repairDetails: RepairDetails, repairDetails: RepairDetails,
pmcData: IPmcData): void 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; const skillPoints = this.getWeaponRepairSkillPoints(repairDetails);
this.questHelper.rewardSkillPoints(sessionId, pmcData, "WeaponTreatment", progress);
this.questHelper.rewardSkillPoints(sessionId, pmcData, "WeaponTreatment", skillPoints, true);
} }
// Handle kit repairs of armor // Handle kit repairs of armor
@ -167,7 +168,7 @@ export class RepairService
const isHeavyArmor = itemDetails[1]._props.ArmorType === "Heavy"; const isHeavyArmor = itemDetails[1]._props.ArmorType === "Heavy";
const vestSkillToLevel = (isHeavyArmor) ? "HeavyVests" : "LightVests"; 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); this.questHelper.rewardSkillPoints(sessionId, pmcData, vestSkillToLevel, pointsToAddToVestSkill);
} }
@ -181,7 +182,7 @@ export class RepairService
: this.repairConfig.repairKitIntellectGainMultiplier.armor; : this.repairConfig.repairKitIntellectGainMultiplier.armor;
// limit gain to a max value defined in config.maxIntellectGainPerRepair // 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 else
{ {
@ -192,6 +193,43 @@ export class RepairService
this.questHelper.rewardSkillPoints(sessionId, pmcData, SkillTypes.INTELLECT, intellectGainedFromRepair); 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;
}
/** /**
* *
* @param sessionId Session id * @param sessionId Session id
@ -218,12 +256,13 @@ export class RepairService
const itemsDb = this.databaseServer.getTables().templates.items; const itemsDb = this.databaseServer.getTables().templates.items;
const itemToRepairDetails = itemsDb[itemToRepair._tpl]; const itemToRepairDetails = itemsDb[itemToRepair._tpl];
const repairItemIsArmor = (!!itemToRepairDetails._props.ArmorMaterial); const repairItemIsArmor = (!!itemToRepairDetails._props.ArmorMaterial);
const repairAmount = repairKits[0].count / this.getKitDivisor(itemToRepairDetails, repairItemIsArmor, pmcData);
this.repairHelper.updateItemDurability( this.repairHelper.updateItemDurability(
itemToRepair, itemToRepair,
itemToRepairDetails, itemToRepairDetails,
repairItemIsArmor, repairItemIsArmor,
repairKits[0].count / this.getKitDivisor(itemToRepairDetails, repairItemIsArmor, pmcData), repairAmount,
true, true,
1, 1,
this.repairConfig.applyRandomizeDurabilityLoss); this.repairConfig.applyRandomizeDurabilityLoss);
@ -244,9 +283,10 @@ export class RepairService
} }
return { return {
repairPoints: repairKits[0].count,
repairedItem: itemToRepair, repairedItem: itemToRepair,
repairedItemIsArmor: repairItemIsArmor, repairedItemIsArmor: repairItemIsArmor,
repairAmount: repairKits[0].count, repairAmount: repairAmount,
repairedByKit: true 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 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 durabilityMultiplier = this.getDurabilityMultiplier(receivedDurabilityMaxPercent, durabilityToRestorePercent);
const doBuff = commonBuffMinChanceValue + commonBuffChanceLevelBonus * skillLevel * durabilityMultiplier; const doBuff = commonBuffMinChanceValue + commonBuffChanceLevelBonus * skillLevel * durabilityMultiplier;
@ -483,6 +523,7 @@ export class RepairService
export class RepairDetails export class RepairDetails
{ {
repairCost?: number; repairCost?: number;
repairPoints?: number;
repairedItem: Item; repairedItem: Item;
repairedItemIsArmor: boolean; repairedItemIsArmor: boolean;
repairAmount: number; repairAmount: number;

View File

@ -3,7 +3,13 @@
"compilerOptions": { "compilerOptions": {
"emitDeclarationOnly": true, "emitDeclarationOnly": true,
"declaration": true, "declaration": true,
"declarationDir": "./types" "declarationDir": "./types",
"baseUrl": ".",
"paths": {
"@spt-aki/*": [
"src/*"
]
}
}, },
"exclude": [ "exclude": [
"./types/**/*" "./types/**/*"