diff --git a/project/src/controllers/GameController.ts b/project/src/controllers/GameController.ts index 58d410bf..27f13b7a 100644 --- a/project/src/controllers/GameController.ts +++ b/project/src/controllers/GameController.ts @@ -18,6 +18,7 @@ import { IServerDetails } from "@spt-aki/models/eft/game/IServerDetails"; import { IAkiProfile } from "@spt-aki/models/eft/profile/IAkiProfile"; import { AccountTypes } from "@spt-aki/models/enums/AccountTypes"; import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes"; +import { SkillTypes } from "@spt-aki/models/enums/SkillTypes"; import { Traders } from "@spt-aki/models/enums/Traders"; import { ICoreConfig } from "@spt-aki/models/spt/config/ICoreConfig"; import { IHttpConfig } from "@spt-aki/models/spt/config/IHttpConfig"; @@ -118,6 +119,8 @@ export class GameController this.adjustLooseLootSpawnProbabilities(); + this.checkTraderRepairValuesExist(); + // repeatableQuests are stored by in profile.Quests due to the responses of the client (e.g. Quests in offraidData) // Since we don't want to clutter the Quests list, we need to remove all completed (failed / successful) repeatable quests. // We also have to remove the Counters from the repeatableQuests @@ -227,6 +230,30 @@ export class GameController } } + /** + * Out of date/incorrectly made trader mods forget this data + */ + protected checkTraderRepairValuesExist(): void + { + for (const traderKey in this.databaseServer.getTables().traders) + { + const trader = this.databaseServer.getTables().traders[traderKey]; + if (!trader?.base?.repair) + { + this.logger.warning(`Trader ${trader.base._id} ${trader.base.name} is missing a repair object, adding in default values`); + trader.base.repair = this.jsonUtil.clone(this.databaseServer.getTables().traders.ragfair.base.repair); + + return; + } + + if (trader?.base?.repair?.quality) + { + this.logger.warning(`Trader ${trader.base._id} ${trader.base.name} is missing a repair quality value, adding in default value`); + trader.base.repair.quality = this.jsonUtil.clone(this.databaseServer.getTables().traders.ragfair.base.repair.quality); + } + } + } + protected addCustomLooseLootPositions(): void { const looseLootPositionsToAdd = this.lootConfig.looseLoot; @@ -472,7 +499,7 @@ export class GameController */ protected warnOnActiveBotReloadSkill(pmcProfile: IPmcData): void { - const botReloadSkill = pmcProfile.Skills.Common.find(x => x.Id === "BotReload"); + const botReloadSkill = this.profileHelper.getSkillFromProfile(pmcProfile, SkillTypes.BOT_RELOAD); if (botReloadSkill?.Progress > 0) { this.logger.warning(this.localisationService.getText("server_start_player_active_botreload_skill")); diff --git a/project/src/helpers/HideoutHelper.ts b/project/src/helpers/HideoutHelper.ts index 6a76cb19..dba759df 100644 --- a/project/src/helpers/HideoutHelper.ts +++ b/project/src/helpers/HideoutHelper.ts @@ -3,7 +3,7 @@ import { inject, injectable } from "tsyringe"; import { InventoryHelper } from "@spt-aki/helpers/InventoryHelper"; import { ProfileHelper } from "@spt-aki/helpers/ProfileHelper"; import { IPmcData } from "@spt-aki/models/eft/common/IPmcData"; -import { Common, HideoutArea, IHideoutImprovement, Production, Productive } from "@spt-aki/models/eft/common/tables/IBotBase"; +import { HideoutArea, IHideoutImprovement, Production, Productive } from "@spt-aki/models/eft/common/tables/IBotBase"; import { Upd } from "@spt-aki/models/eft/common/tables/IItem"; import { StageBonus } from "@spt-aki/models/eft/hideout/IHideoutArea"; import { IHideoutContinuousProductionStartRequestData } from "@spt-aki/models/eft/hideout/IHideoutContinuousProductionStartRequestData"; @@ -800,16 +800,6 @@ export class HideoutHelper return productionSlots + (hasManagementSkillSlots ? managementSlotsCount : 0); } - /** - * Does profile have elite hideout management skill - * @param pmcData Profile to look at - * @returns True if profile has skill - */ - protected hasEliteHideoutManagementSkill(pmcData: IPmcData): boolean - { - return this.getHideoutManagementSkill(pmcData)?.Progress >= 5100; // level 51+ - } - /** * Get a count of bitcoins player miner can hold */ @@ -818,16 +808,6 @@ export class HideoutHelper return this.databaseServer.getTables().globals.config.SkillsSettings.HideoutManagement.EliteSlots.BitcoinFarm.Container; } - /** - * Get the hideout management skill from player profile - * @param pmcData Profile to look at - * @returns Hideout management skill object - */ - protected getHideoutManagementSkill(pmcData: IPmcData): Common - { - return pmcData.Skills.Common.find(x => x.Id === SkillTypes.HIDEOUT_MANAGEMENT); - } - /** * HideoutManagement skill gives a consumption bonus the higher the level * 0.5% per level per 1-51, (25.5% at max) @@ -836,7 +816,7 @@ export class HideoutHelper */ protected getHideoutManagementConsumptionBonus(pmcData: IPmcData): number { - const hideoutManagementSkill = this.getHideoutManagementSkill(pmcData); + const hideoutManagementSkill = this.profileHelper.getSkillFromProfile(pmcData, SkillTypes.HIDEOUT_MANAGEMENT); if (!hideoutManagementSkill) { return 0; diff --git a/project/src/helpers/ProfileHelper.ts b/project/src/helpers/ProfileHelper.ts index fad76acb..8ad71cd3 100644 --- a/project/src/helpers/ProfileHelper.ts +++ b/project/src/helpers/ProfileHelper.ts @@ -2,7 +2,7 @@ import { inject, injectable } from "tsyringe"; import { ItemHelper } from "@spt-aki/helpers/ItemHelper"; import { IPmcData } from "@spt-aki/models/eft/common/IPmcData"; -import { CounterKeyValue, Stats } from "@spt-aki/models/eft/common/tables/IBotBase"; +import { Common, CounterKeyValue, Stats } from "@spt-aki/models/eft/common/tables/IBotBase"; import { IAkiProfile } from "@spt-aki/models/eft/profile/IAkiProfile"; import { IValidateNicknameRequestData } from "@spt-aki/models/eft/profile/IValidateNicknameRequestData"; import { SkillTypes } from "@spt-aki/models/enums/SkillTypes"; @@ -423,4 +423,16 @@ export class ProfileHelper profileSkill.Progress += pointsToAdd; profileSkill.LastAccess = this.timeUtil.getTimestamp(); } + + public getSkillFromProfile(pmcData: IPmcData, skill: SkillTypes): Common + { + const skillToReturn = pmcData.Skills.Common.find(x => x.Id === skill); + if (!skillToReturn) + { + this.logger.warning(`Profile ${pmcData.sessionId} does not have a skill named: ${skill}`); + return undefined; + } + + return skillToReturn; + } } \ No newline at end of file diff --git a/project/src/helpers/QuestHelper.ts b/project/src/helpers/QuestHelper.ts index 5bc3e932..2ea06add 100644 --- a/project/src/helpers/QuestHelper.ts +++ b/project/src/helpers/QuestHelper.ts @@ -818,7 +818,7 @@ export class QuestHelper let moneyRewardBonus = moneyRewardBonuses.reduce((acc, cur) => acc + cur.value, 0); // Apply hideout management bonus to money reward (up to 51% bonus) - const hideoutManagementSkill = pmcData.Skills.Common.find(x => x.Id === SkillTypes.HIDEOUT_MANAGEMENT); + const hideoutManagementSkill = this.profileHelper.getSkillFromProfile(pmcData, SkillTypes.HIDEOUT_MANAGEMENT); if (hideoutManagementSkill) { moneyRewardBonus *= (1 + (hideoutManagementSkill.Progress / 10000)); // 5100 becomes 0.51, add 1 to it, 1.51, multiply the moneyreward bonus by it (e.g. 15 x 51) diff --git a/project/src/models/enums/SkillTypes.ts b/project/src/models/enums/SkillTypes.ts index 355e91e1..6ff377ba 100644 --- a/project/src/models/enums/SkillTypes.ts +++ b/project/src/models/enums/SkillTypes.ts @@ -1,5 +1,7 @@ export enum SkillTypes { + BOT_RELOAD = "BotReload", + BOT_SOUND = "BotSound", HIDEOUT_MANAGEMENT = "HideoutManagement", CRAFTING = "Crafting", METABOLISM = "Metabolism", @@ -20,6 +22,7 @@ export enum SkillTypes ATTENTION = "Attention", CHARISMA = "Charisma", MEMORY = "Memory", + MELEE = "Melee", SURGERY = "Surgery", AIM_DRILLS = "AimDrills", TROUBLESHOOTING = "TroubleShooting", @@ -32,6 +35,7 @@ export enum SkillTypes NIGHT_OPS = "NightOps", SILENT_OPS = "SilentOps", LOCKPICKING = "Lockpicking", + /** Also called Weapon Maintenance*/ WEAPON_TREATMENT = "WeaponTreatment", MAG_DRILLS = "MagDrills", FREE_TRADING = "Freetrading", diff --git a/project/src/services/RepairService.ts b/project/src/services/RepairService.ts index cf1b8ee5..918503ea 100644 --- a/project/src/services/RepairService.ts +++ b/project/src/services/RepairService.ts @@ -259,6 +259,7 @@ export class RepairService const itemToRepairDetails = itemsDb[itemToRepair._tpl]; const repairItemIsArmor = (!!itemToRepairDetails._props.ArmorMaterial); const repairAmount = repairKits[0].count / this.getKitDivisor(itemToRepairDetails, repairItemIsArmor, pmcData); + const shouldApplyDurabilityLoss = this.shouldRepairKitApplyDurabilityLoss(pmcData, this.repairConfig.applyRandomizeDurabilityLoss); this.repairHelper.updateItemDurability( itemToRepair, @@ -267,7 +268,7 @@ export class RepairService repairAmount, true, 1, - this.repairConfig.applyRandomizeDurabilityLoss); + shouldApplyDurabilityLoss); // Find and use repair kit defined in body for (const repairKit of repairKits) @@ -306,7 +307,7 @@ export class RepairService const globalRepairSettings = globals.config.RepairSettings; const intellectRepairPointsPerLevel = globals.config.SkillsSettings.Intellect.RepairPointsCostReduction; - const profileIntellectLevel = pmcData.Skills?.Common?.find(s => s.Id === SkillTypes.INTELLECT)?.Progress ?? 0; + const profileIntellectLevel = this.profileHelper.getSkillFromProfile(pmcData, SkillTypes.INTELLECT)?.Progress ?? 0; const intellectPointReduction = intellectRepairPointsPerLevel * Math.trunc(profileIntellectLevel / 100); if (isArmor) @@ -352,6 +353,29 @@ export class RepairService return value; } + /** + * Should a repair kit apply total durability loss on repair + * @param pmcData Player profile + * @param applyRandomizeDurabilityLoss Value from repair config + * @returns True if loss should be applied + */ + protected shouldRepairKitApplyDurabilityLoss(pmcData: IPmcData, applyRandomizeDurabilityLoss: boolean): boolean + { + let shouldApplyDurabilityLoss = applyRandomizeDurabilityLoss; + if (shouldApplyDurabilityLoss) + { + // Random loss not disabled via config, perform charisma check + const hasEliteCharisma = this.profileHelper.hasEliteSkillLevel(SkillTypes.CHARISMA, pmcData); + if (hasEliteCharisma) + { + // 50/50 chance of loss being ignored at elite level + shouldApplyDurabilityLoss = this.randomUtil.getChance100(50); + } + } + + return shouldApplyDurabilityLoss; + } + /** * Update repair kits Resource object if it doesn't exist * @param repairKitDetails Repair kit details from db @@ -448,13 +472,15 @@ export class RepairService 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 commonBuffMinChanceValue = globals.config.SkillsSettings[itemSkillType as string].BuffSettings.CommonBuffMinChanceValue; + const commonBuffChanceLevelBonus = globals.config.SkillsSettings[itemSkillType as string].BuffSettings.CommonBuffChanceLevelBonus; + const receivedDurabilityMaxPercent = globals.config.SkillsSettings[itemSkillType as string].BuffSettings.ReceivedDurabilityMaxPercent; - const skillLevel = Math.trunc((pmcData?.Skills?.Common?.find(s => s.Id === itemSkillType)?.Progress ?? 0) / 100); + const skillLevel = Math.trunc((this.profileHelper.getSkillFromProfile(pmcData, itemSkillType)?.Progress ?? 0) / 100); const durabilityToRestorePercent = repairDetails.repairPoints / template._props.MaxDurability; const durabilityMultiplier = this.getDurabilityMultiplier(receivedDurabilityMaxPercent, durabilityToRestorePercent); @@ -474,26 +500,26 @@ export class RepairService * @param itemTemplate Item to check for skill * @returns Skill name */ - protected getItemSkillType(itemTemplate: ITemplateItem): string + protected getItemSkillType(itemTemplate: ITemplateItem): SkillTypes { if (this.itemHelper.isOfBaseclass(itemTemplate._id, BaseClasses.ARMOR)) { if (itemTemplate._props.ArmorType === "Light") { - return "LightVests"; + return SkillTypes.LIGHT_VESTS; } else if (itemTemplate._props.ArmorType === "Heavy") { - return "HeavyVests"; + return SkillTypes.HEAVY_VESTS; } } else if (this.itemHelper.isOfBaseclass(itemTemplate._id, BaseClasses.WEAPON)) { - return "WeaponTreatment"; + return SkillTypes.WEAPON_TREATMENT; } else if (this.itemHelper.isOfBaseclass(itemTemplate._id, BaseClasses.KNIFE)) { - return "Melee"; + return SkillTypes.MELEE; } return undefined;