From 90b76eab6861064192828eba2b1aeb9aa2712a1e Mon Sep 17 00:00:00 2001 From: Dev Date: Wed, 1 Nov 2023 11:36:13 +0000 Subject: [PATCH 1/7] Correctly check when difficulty is not a number --- project/src/generators/RepeatableQuestGenerator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/src/generators/RepeatableQuestGenerator.ts b/project/src/generators/RepeatableQuestGenerator.ts index 025e0281..f3c48938 100644 --- a/project/src/generators/RepeatableQuestGenerator.ts +++ b/project/src/generators/RepeatableQuestGenerator.ts @@ -781,7 +781,7 @@ export class RepeatableQuestGenerator const rewardSpreadConfig = repeatableConfig.rewardScaling.rewardSpread; const reputationConfig = repeatableConfig.rewardScaling.reputation; - if (isNaN(difficulty)) + if (Number.isNaN(difficulty)) { difficulty = 1; this.logger.warning(this.localisationService.getText("repeatable-difficulty_was_nan")); From bb060089836f403a9fdbd12a1312be2a2aded281 Mon Sep 17 00:00:00 2001 From: Dev Date: Wed, 1 Nov 2023 11:45:24 +0000 Subject: [PATCH 2/7] Add missing `PmcKills` properties to daily scav elimination quest config --- project/assets/configs/quest.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/project/assets/configs/quest.json b/project/assets/configs/quest.json index 31f90218..57a03bbc 100644 --- a/project/assets/configs/quest.json +++ b/project/assets/configs/quest.json @@ -1460,6 +1460,8 @@ "minKills": 1, "maxBossKills": 1, "minBossKills": 1, + "maxPmcKills": 2, + "minPmcKills": 1, "weaponRequirementProb": 0, "weaponCategoryRequirementProb": 0.3, "weaponCategoryRequirements": [{ @@ -1573,6 +1575,8 @@ "minKills": 3, "maxBossKills": 3, "minBossKills": 1, + "maxPmcKills": 5, + "minPmcKills": 2, "weaponRequirementProb": 0, "weaponCategoryRequirementProb": 0.3, "weaponCategoryRequirements": [{ From 9d56030880313dea3b9cfb03f493aeb6521e0f05 Mon Sep 17 00:00:00 2001 From: Dev Date: Wed, 1 Nov 2023 13:06:01 +0000 Subject: [PATCH 3/7] Remove unused class property --- project/src/controllers/BotController.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/project/src/controllers/BotController.ts b/project/src/controllers/BotController.ts index 89fecf8b..d7ecc335 100644 --- a/project/src/controllers/BotController.ts +++ b/project/src/controllers/BotController.ts @@ -28,7 +28,6 @@ export class BotController { protected botConfig: IBotConfig; protected pmcConfig: IPmcConfig; - public static readonly pmcTypeLabel = "PMC"; constructor( @inject("WinstonLogger") protected logger: ILogger, From b1842e1c49ed84b5d84cb989603e3fec8ed59f06 Mon Sep 17 00:00:00 2001 From: Dev Date: Wed, 1 Nov 2023 13:29:47 +0000 Subject: [PATCH 4/7] When generating a specific location condition for an elimination quest, don't add weapon requirement props to it - unnecessary Clean up kill condition generation method comments/param names Replace magic strings with objects --- .../generators/RepeatableQuestGenerator.ts | 45 +++++++++---------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/project/src/generators/RepeatableQuestGenerator.ts b/project/src/generators/RepeatableQuestGenerator.ts index f3c48938..35aef305 100644 --- a/project/src/generators/RepeatableQuestGenerator.ts +++ b/project/src/generators/RepeatableQuestGenerator.ts @@ -244,11 +244,11 @@ export class RepeatableQuestGenerator { // get all boss spawn information 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 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); // remove blacklisted locations const allowedSpawns = thisBossSpawns.filter(x => !eliminationConfig.distLocationBlacklist.includes(x.Id)); @@ -315,9 +315,11 @@ export class RepeatableQuestGenerator const availableForFinishCondition = quest.conditions.AvailableForFinish[0]; availableForFinishCondition._props.counter.id = this.objectId.generate(); availableForFinishCondition._props.counter.conditions = []; + + // Only add specific location condition if specific map selected 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.value = desiredKillCount; @@ -356,9 +358,9 @@ export class RepeatableQuestGenerator * This is a helper method for GenerateEliminationQuest to create a location condition. * * @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 = { _props: { @@ -368,30 +370,20 @@ export class RepeatableQuestGenerator }, _parent: "Location" }; - - if (allowedWeapon) - { - propsObject._props.weapon = [allowedWeapon]; - } - - if (allowedWeaponCategory) - { - propsObject._props.weaponCategories = [allowedWeaponCategory]; - } return propsObject; } /** - * A repeatable quest, besides some more or less static components, exists of reward and condition (see assets/database/templates/repeatableQuests.json) - * This is a helper method for GenerateEliminationQuest to create a kill condition. - * - * @param {string} target array of target npcs e.g. "AnyPmc", "Savage" - * @param {array} bodyParts array of body parts with which to kill e.g. ["stomach", "thorax"] - * @param {number} distance distance from which to kill (currently only >= supported) - * @returns {object} object of "Elimination"-kill-subcondition + * Create kill condition for an elimination quest + * @param target Bot type target of elimination quest e.g. "AnyPmc", "Savage" + * @param targetedBodyParts Body parts player must hit + * @param distance Distance from which to kill (currently only >= supported + * @param allowedWeapon What weapon must be used - undefined = any + * @param allowedWeaponCategory What category of weapon must be used - undefined = any + * @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 = { target: target, @@ -406,9 +398,10 @@ export class RepeatableQuestGenerator killConditionProps.savageRole = [target]; } - if (bodyPart) + // Has specific body part hit condition + if (targetedBodyParts) { - killConditionProps.bodyPart = bodyPart; + killConditionProps.bodyPart = targetedBodyParts; } // Dont allow distance + melee requirement @@ -420,11 +413,13 @@ export class RepeatableQuestGenerator }; } + // Has specific weapon requirement if (allowedWeapon) { killConditionProps.weapon = [allowedWeapon]; } + // Has specific weapon category requirement if (allowedWeaponCategory?.length > 0) { killConditionProps.weaponCategories = [allowedWeaponCategory]; From d74b505a7b3b7c4e40c42333ba761dd4937b82f9 Mon Sep 17 00:00:00 2001 From: Dev Date: Wed, 1 Nov 2023 14:20:27 +0000 Subject: [PATCH 5/7] Fix types generation command not working --- project/tsconfig.typedef.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/project/tsconfig.typedef.json b/project/tsconfig.typedef.json index 235104aa..3b1a4c78 100644 --- a/project/tsconfig.typedef.json +++ b/project/tsconfig.typedef.json @@ -3,7 +3,13 @@ "compilerOptions": { "emitDeclarationOnly": true, "declaration": true, - "declarationDir": "./types" + "declarationDir": "./types", + "baseUrl": ".", + "paths": { + "@spt-aki/*": [ + "src/*" + ] + } }, "exclude": [ "./types/**/*" From ebeda336db4082fa2f84a70b1153ba24cf6c1921 Mon Sep 17 00:00:00 2001 From: DrakiaXYZ Date: Thu, 2 Nov 2023 08:56:02 +0000 Subject: [PATCH 6/7] 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 Co-committed-by: DrakiaXYZ --- project/assets/configs/repair.json | 7 +++ project/src/helpers/QuestHelper.ts | 60 ++++++++++++++++++- .../src/models/spt/config/IRepairConfig.ts | 13 ++++ project/src/services/RepairService.ts | 57 +++++++++++++++--- 4 files changed, 127 insertions(+), 10 deletions(-) diff --git a/project/assets/configs/repair.json b/project/assets/configs/repair.json index e240c804..af1fdd43 100644 --- a/project/assets/configs/repair.json +++ b/project/assets/configs/repair.json @@ -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": { diff --git a/project/src/helpers/QuestHelper.ts b/project/src/helpers/QuestHelper.ts index 2553600f..93edf145 100644 --- a/project/src/helpers/QuestHelper.ts +++ b/project/src/helpers/QuestHelper.ts @@ -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 diff --git a/project/src/models/spt/config/IRepairConfig.ts b/project/src/models/spt/config/IRepairConfig.ts index e9544bbc..0b65b465 100644 --- a/project/src/models/spt/config/IRepairConfig.ts +++ b/project/src/models/spt/config/IRepairConfig.ts @@ -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 diff --git a/project/src/services/RepairService.ts b/project/src/services/RepairService.ts index fc69eb57..5b49b577 100644 --- a/project/src/services/RepairService.ts +++ b/project/src/services/RepairService.ts @@ -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; From c782b58e64ace39f7967badbbe9560f9dec596b1 Mon Sep 17 00:00:00 2001 From: Dev Date: Thu, 2 Nov 2023 08:59:43 +0000 Subject: [PATCH 7/7] Remoev duplicate name --- project/assets/database/bots/types/arenafighterevent.json | 1 - 1 file changed, 1 deletion(-) diff --git a/project/assets/database/bots/types/arenafighterevent.json b/project/assets/database/bots/types/arenafighterevent.json index 17da2deb..ec498b77 100644 --- a/project/assets/database/bots/types/arenafighterevent.json +++ b/project/assets/database/bots/types/arenafighterevent.json @@ -2073,7 +2073,6 @@ "Оливье", "Подружка", "Шмыга", - "Шнур", "Сырок", "Улётный", "Васёк",