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": [{ 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/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 @@ "Оливье", "Подружка", "Шмыга", - "Шнур", "Сырок", "Улётный", "Васёк", 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, diff --git a/project/src/generators/RepeatableQuestGenerator.ts b/project/src/generators/RepeatableQuestGenerator.ts index 025e0281..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]; @@ -781,7 +776,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")); 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; 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/**/*"