diff --git a/project/assets/configs/quest.json b/project/assets/configs/quest.json index 57a03bbc..b197865d 100644 --- a/project/assets/configs/quest.json +++ b/project/assets/configs/quest.json @@ -146,9 +146,11 @@ "levels": [1, 10, 20, 30, 40, 50, 60], "experience": [1000, 12000, 42000, 89000, 177000, 300000, 500000], "roubles": [15000, 40000, 75000, 100000, 140000, 170000, 210000], - "items": [4, 4, 5, 5, 5, 6, 6], - "reputation": [0.01, 0.01, 0.02, 0.02], - "rewardSpread": 0.5 + "items": [3, 4, 5, 5, 5, 5, 5], + "reputation": [0.01, 0.01, 0.02, 0.02, 0.03, 0.03, 0.03], + "rewardSpread": 0.5, + "skillRewardChance": [0, 0.01, 0.05, 0.1, 0.15, 0.2, 0.25], + "skillPointReward": [10, 15, 20, 25, 30, 35, 40] }, "locations": { "any": ["any"], @@ -188,6 +190,7 @@ "questConfig": { "Exploration": { "maxExtracts": 3, + "possibleSkillRewards": ["Endurance", "Strength", "Vitality"], "specificExits": { "probability": 0.25, "passageRequirementWhitelist": [ @@ -201,6 +204,7 @@ } }, "Completion": { + "possibleSkillRewards": ["Endurance", "Strength", "Vitality"], "minRequestedAmount": 1, "maxRequestedAmount": 5, "minRequestedBulletAmount": 20, @@ -213,6 +217,7 @@ "min": 1, "max": 15 }, + "possibleSkillRewards": ["Endurance", "Strength", "Vitality"], "targets": [{ "key": "Savage", "relativeProbability": 1, @@ -326,6 +331,7 @@ "min": 16, "max": 40 }, + "possibleSkillRewards": ["Endurance", "Strength", "Vitality"], "targets": [{ "key": "Savage", "relativeProbability": 9, @@ -507,6 +513,7 @@ "min": 41, "max": 100 }, + "possibleSkillRewards": ["Endurance", "Strength", "Vitality"], "targets": [{ "key": "Savage", "relativeProbability": 9, @@ -704,9 +711,11 @@ "levels": [1, 10, 20, 30, 40, 50, 60], "experience": [5000, 25000, 60000, 130000, 240000, 390000, 750000], "roubles": [50000, 150000, 300000, 425000, 550000, 675000, 850000], - "items": [5, 5, 5, 6, 6, 7, 7], - "reputation": [0.02, 0.02, 0.03, 0.03, 0.03, 0.03, 0.03], - "rewardSpread": 0.5 + "items": [4, 5, 5, 6, 6, 7, 7], + "reputation": [0.02, 0.03, 0.04, 0.04, 0.05, 0.05, 0.05], + "rewardSpread": 0.5, + "skillRewardChance": [0, 0.05, 0.1, 0.2, 0.3, 0.35, 0.4], + "skillPointReward": [25, 35, 45, 50, 55, 60, 65] }, "locations": { "any": ["any"], @@ -745,6 +754,7 @@ ], "questConfig": { "Exploration": { + "possibleSkillRewards": ["Endurance", "Strength", "Vitality"], "maxExtracts": 10, "specificExits": { "probability": 0.4, @@ -759,6 +769,7 @@ } }, "Completion": { + "possibleSkillRewards": ["Endurance", "Strength", "Vitality"], "minRequestedAmount": 2, "maxRequestedAmount": 10, "minRequestedBulletAmount": 20, @@ -771,6 +782,7 @@ "min": 1, "max": 15 }, + "possibleSkillRewards": ["Endurance", "Strength", "Vitality"], "targets": [{ "key": "Savage", "relativeProbability": 15, @@ -951,6 +963,7 @@ "min": 16, "max": 40 }, + "possibleSkillRewards": ["Endurance", "Strength", "Vitality"], "targets": [{ "key": "Savage", "relativeProbability": 7, @@ -1131,6 +1144,7 @@ "min": 41, "max": 100 }, + "possibleSkillRewards": ["Endurance", "Strength", "Vitality"], "targets": [{ "key": "Savage", "relativeProbability": 7, @@ -1331,7 +1345,9 @@ "roubles": [6000, 10000, 100000, 250000], "items": [2, 3, 4, 4], "reputation": [0.01, 0.02, 0.05, 0.05], - "rewardSpread": 0.5 + "rewardSpread": 0.5, + "skillRewardChance": [0, 0, 0, 0, 0, 0, 0], + "skillPointReward": [10, 15, 20, 25, 30, 35, 40] }, "locations": { "any": ["any"], @@ -1351,6 +1367,7 @@ ], "questConfig": { "Exploration": { + "possibleSkillRewards": ["Endurance", "Strength", "Vitality"], "maxExtracts": 3, "specificExits": { "probability": 0.25, @@ -1364,6 +1381,7 @@ } }, "Pickup": { + "possibleSkillRewards": ["Endurance", "Strength", "Vitality"], "ItemTypeToFetchWithMaxCount": [{ "itemType": "5b47574386f77428ca22b335", "minPickupCount": 2, @@ -1410,6 +1428,7 @@ "maxItemFetchCount": 3 }, "Completion": { + "possibleSkillRewards": ["Endurance", "Strength", "Vitality"], "minRequestedAmount": 1, "maxRequestedAmount": 5, "minRequestedBulletAmount": 20, @@ -1422,6 +1441,7 @@ "min": 1, "max": 15 }, + "possibleSkillRewards": ["Endurance", "Strength", "Vitality"], "targets": [{ "key": "AnyPmc", @@ -1460,7 +1480,7 @@ "minKills": 1, "maxBossKills": 1, "minBossKills": 1, - "maxPmcKills": 2, + "maxPmcKills": 2, "minPmcKills": 1, "weaponRequirementProb": 0, "weaponCategoryRequirementProb": 0.3, @@ -1537,6 +1557,7 @@ "min": 16, "max": 100 }, + "possibleSkillRewards": ["Endurance", "Strength", "Vitality"], "targets": [{ "key": "AnyPmc", @@ -1575,7 +1596,7 @@ "minKills": 3, "maxBossKills": 3, "minBossKills": 1, - "maxPmcKills": 5, + "maxPmcKills": 5, "minPmcKills": 2, "weaponRequirementProb": 0, "weaponCategoryRequirementProb": 0.3, 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/controllers/HideoutController.ts b/project/src/controllers/HideoutController.ts index fb2d7e0d..e14c37d7 100644 --- a/project/src/controllers/HideoutController.ts +++ b/project/src/controllers/HideoutController.ts @@ -200,7 +200,7 @@ export class HideoutController } // Add Skill Points Per Area Upgrade - this.playerService.incrementSkillLevel(pmcData, SkillTypes.HIDEOUT_MANAGEMENT, db.globals.config.SkillsSettings.HideoutManagement.SkillPointsPerAreaUpgrade); + this.profileHelper.addSkillPointsToPlayer(pmcData, SkillTypes.HIDEOUT_MANAGEMENT, db.globals.config.SkillsSettings.HideoutManagement.SkillPointsPerAreaUpgrade); return output; } @@ -723,12 +723,12 @@ export class HideoutController // manager Hideout skill // ? use a configuration variable for the value? const globals = this.databaseServer.getTables().globals; - this.playerService.incrementSkillLevel(pmcData, SkillTypes.HIDEOUT_MANAGEMENT, globals.config.SkillsSettings.HideoutManagement.SkillPointsPerCraft, true); + this.profileHelper.addSkillPointsToPlayer(pmcData, SkillTypes.HIDEOUT_MANAGEMENT, globals.config.SkillsSettings.HideoutManagement.SkillPointsPerCraft, true); //manager Crafting skill if (craftingExpAmount > 0) { - this.playerService.incrementSkillLevel(pmcData, SkillTypes.CRAFTING, craftingExpAmount); - this.playerService.incrementSkillLevel(pmcData, SkillTypes.INTELLECT, 0.5 * (Math.round(craftingExpAmount / 15))); + this.profileHelper.addSkillPointsToPlayer(pmcData, SkillTypes.CRAFTING, craftingExpAmount); + this.profileHelper.addSkillPointsToPlayer(pmcData, SkillTypes.INTELLECT, 0.5 * (Math.round(craftingExpAmount / 15))); } area.lastRecipe = request.recipeId; counterHoursCrafting.value = hoursCrafting; diff --git a/project/src/controllers/InsuranceController.ts b/project/src/controllers/InsuranceController.ts index 124d9b9c..d281893d 100644 --- a/project/src/controllers/InsuranceController.ts +++ b/project/src/controllers/InsuranceController.ts @@ -10,10 +10,11 @@ import { IGetInsuranceCostRequestData } from "@spt-aki/models/eft/insurance/IGet import { IGetInsuranceCostResponseData } from "@spt-aki/models/eft/insurance/IGetInsuranceCostResponseData"; import { IInsureRequestData } from "@spt-aki/models/eft/insurance/IInsureRequestData"; import { IItemEventRouterResponse } from "@spt-aki/models/eft/itemEvent/IItemEventRouterResponse"; -import { Insurance, ISystemData } from "@spt-aki/models/eft/profile/IAkiProfile"; +import { ISystemData, Insurance } from "@spt-aki/models/eft/profile/IAkiProfile"; import { IProcessBuyTradeRequestData } from "@spt-aki/models/eft/trade/IProcessBuyTradeRequestData"; import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes"; import { MessageType } from "@spt-aki/models/enums/MessageType"; +import { SkillTypes } from "@spt-aki/models/enums/SkillTypes"; import { IInsuranceConfig } from "@spt-aki/models/spt/config/IInsuranceConfig"; import { ILogger } from "@spt-aki/models/spt/utils/ILogger"; import { EventOutputHolder } from "@spt-aki/routers/EventOutputHolder"; @@ -552,6 +553,7 @@ export class InsuranceController public insure(pmcData: IPmcData, body: IInsureRequestData, sessionID: string): IItemEventRouterResponse { let output = this.eventOutputHolder.getOutput(sessionID); + const itemsToInsureCount = body.items.length; const itemsToPay = []; const inventoryItemsHash = {}; @@ -598,6 +600,8 @@ export class InsuranceController }); } + this.profileHelper.addSkillPointsToPlayer(pmcData, SkillTypes.CHARISMA, itemsToInsureCount * 0.01); + return output; } diff --git a/project/src/controllers/InventoryController.ts b/project/src/controllers/InventoryController.ts index 715ec150..73ab6778 100644 --- a/project/src/controllers/InventoryController.ts +++ b/project/src/controllers/InventoryController.ts @@ -599,7 +599,7 @@ export class InventoryController pmcData.Encyclopedia[itemId] = true; // TODO: update this with correct calculation using values from globals json - this.questHelper.rewardSkillPoints(sessionID, pmcData, SkillTypes.INTELLECT, 0.5); + this.profileHelper.addSkillPointsToPlayer(pmcData, SkillTypes.INTELLECT, 0.5); } return this.eventOutputHolder.getOutput(sessionID); diff --git a/project/src/controllers/RepeatableQuestController.ts b/project/src/controllers/RepeatableQuestController.ts index bae6ecac..e9337c0a 100644 --- a/project/src/controllers/RepeatableQuestController.ts +++ b/project/src/controllers/RepeatableQuestController.ts @@ -14,11 +14,13 @@ import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes"; import { ELocationName } from "@spt-aki/models/enums/ELocationName"; import { HideoutAreas } from "@spt-aki/models/enums/HideoutAreas"; import { QuestStatus } from "@spt-aki/models/enums/QuestStatus"; +import { SkillTypes } from "@spt-aki/models/enums/SkillTypes"; import { IQuestConfig, IRepeatableQuestConfig } from "@spt-aki/models/spt/config/IQuestConfig"; import { IQuestTypePool } from "@spt-aki/models/spt/repeatable/IQuestTypePool"; import { ILogger } from "@spt-aki/models/spt/utils/ILogger"; import { EventOutputHolder } from "@spt-aki/routers/EventOutputHolder"; import { ConfigServer } from "@spt-aki/servers/ConfigServer"; +import { DatabaseServer } from "@spt-aki/servers/DatabaseServer"; import { PaymentService } from "@spt-aki/services/PaymentService"; import { ProfileFixerService } from "@spt-aki/services/ProfileFixerService"; import { HttpResponseUtil } from "@spt-aki/utils/HttpResponseUtil"; @@ -33,8 +35,9 @@ export class RepeatableQuestController protected questConfig: IQuestConfig; constructor( - @inject("TimeUtil") protected timeUtil: TimeUtil, @inject("WinstonLogger") protected logger: ILogger, + @inject("DatabaseServer") protected databaseServer: DatabaseServer, + @inject("TimeUtil") protected timeUtil: TimeUtil, @inject("RandomUtil") protected randomUtil: RandomUtil, @inject("HttpResponseUtil") protected httpResponse: HttpResponseUtil, @inject("JsonUtil") protected jsonUtil: JsonUtil, @@ -131,7 +134,7 @@ export class RepeatableQuestController const questTypePool = this.generateQuestPool(repeatableConfig, pmcData.Info.Level); // Add daily quests - for (let i = 0; i < repeatableConfig.numQuests; i++) + for (let i = 0; i < this.getQuestCount(repeatableConfig, pmcData); i++) { let quest = null; let lifeline = 0; @@ -188,6 +191,23 @@ export class RepeatableQuestController return returnData; } + /** + * Get the number of quests to generate - takes into account charisma state of player + * @param repeatableConfig Config + * @param pmcData Player profile + * @returns Quest count + */ + protected getQuestCount(repeatableConfig: IRepeatableQuestConfig, pmcData: IPmcData): number + { + if (repeatableConfig.name.toLowerCase() === "daily" && this.profileHelper.hasEliteSkillLevel(SkillTypes.CHARISMA, pmcData)) + { + // Elite charisma skill gives extra daily quest(s) + return repeatableConfig.numQuests + this.databaseServer.getTables().globals.config.SkillsSettings.Charisma.BonusSettings.EliteBonusSettings.RepeatableQuestExtraCount; + } + + return repeatableConfig.numQuests; + } + /** * Get repeatable quest data from profile from name (daily/weekly), creates base repeatable quest object if none exists * @param repeatableConfig daily/weekly config diff --git a/project/src/generators/RepeatableQuestGenerator.ts b/project/src/generators/RepeatableQuestGenerator.ts index 35aef305..f85fe623 100644 --- a/project/src/generators/RepeatableQuestGenerator.ts +++ b/project/src/generators/RepeatableQuestGenerator.ts @@ -24,7 +24,7 @@ import { BaseClasses } from "@spt-aki/models/enums/BaseClasses"; import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes"; import { Money } from "@spt-aki/models/enums/Money"; import { Traders } from "@spt-aki/models/enums/Traders"; -import { IBossInfo, IEliminationConfig, IQuestConfig, IRepeatableQuestConfig } from "@spt-aki/models/spt/config/IQuestConfig"; +import { IBaseQuestConfig, IBossInfo, IEliminationConfig, IQuestConfig, IRepeatableQuestConfig } from "@spt-aki/models/spt/config/IQuestConfig"; import { IQuestTypePool } from "@spt-aki/models/spt/repeatable/IQuestTypePool"; import { ILogger } from "@spt-aki/models/spt/utils/ILogger"; import { EventOutputHolder } from "@spt-aki/routers/EventOutputHolder"; @@ -326,7 +326,7 @@ export class RepeatableQuestGenerator availableForFinishCondition._props.id = this.objectId.generate(); quest.location = this.getQuestLocationByMapId(locationKey); - quest.rewards = this.generateReward(pmcLevel, Math.min(difficulty, 1), traderId, repeatableConfig); + quest.rewards = this.generateReward(pmcLevel, Math.min(difficulty, 1), traderId, repeatableConfig, eliminationConfig); return quest; } @@ -544,7 +544,7 @@ export class RepeatableQuestGenerator } } - quest.rewards = this.generateReward(pmcLevel, 1, traderId, repeatableConfig); + quest.rewards = this.generateReward(pmcLevel, 1, traderId, repeatableConfig, completionConfig); return quest; } @@ -673,7 +673,7 @@ export class RepeatableQuestGenerator // Difficulty for exploration goes from 1 extract to maxExtracts // Difficulty for reward goes from 0.2...1 -> map const difficulty = this.mathUtil.mapToRange(numExtracts, 1, explorationConfig.maxExtracts, 0.2, 1); - quest.rewards = this.generateReward(pmcLevel, difficulty, traderId, repeatableConfig); + quest.rewards = this.generateReward(pmcLevel, difficulty, traderId, repeatableConfig, explorationConfig); return quest; } @@ -707,7 +707,7 @@ export class RepeatableQuestGenerator (equipmentCondition._props as IEquipmentConditionProps).equipmentInclusive = [[itemTypeToFetchWithCount.itemType]]; // Add rewards - quest.rewards = this.generateReward(pmcLevel, 1, traderId, repeatableConfig); + quest.rewards = this.generateReward(pmcLevel, 1, traderId, repeatableConfig, pickupConfig); return quest; } @@ -765,7 +765,8 @@ export class RepeatableQuestGenerator pmcLevel: number, difficulty: number, traderId: string, - repeatableConfig: IRepeatableQuestConfig + repeatableConfig: IRepeatableQuestConfig, + questConfig: IBaseQuestConfig ): IRewards { // difficulty could go from 0.2 ... -> for lowest diffuculty receive 0.2*nominal reward @@ -774,6 +775,8 @@ export class RepeatableQuestGenerator const xpConfig = repeatableConfig.rewardScaling.experience; const itemsConfig = repeatableConfig.rewardScaling.items; const rewardSpreadConfig = repeatableConfig.rewardScaling.rewardSpread; + const skillRewardChanceConfig = repeatableConfig.rewardScaling.skillRewardChance; + const skillPointRewardConfig = repeatableConfig.rewardScaling.skillPointReward; const reputationConfig = repeatableConfig.rewardScaling.reputation; if (Number.isNaN(difficulty)) @@ -788,6 +791,8 @@ export class RepeatableQuestGenerator const rewardNumItems = this.randomUtil.randInt(1, Math.round(this.mathUtil.interp1(pmcLevel, levelsConfig, itemsConfig)) + 1); const rewardReputation = Math.round(100 * difficulty * this.mathUtil.interp1(pmcLevel, levelsConfig, reputationConfig) * this.randomUtil.getFloat(1 - rewardSpreadConfig, 1 + rewardSpreadConfig)) / 100; + const skillRewardChance = this.mathUtil.interp1(pmcLevel, levelsConfig, skillRewardChanceConfig); + const skillPointReward = this.mathUtil.interp1(pmcLevel, levelsConfig, skillPointRewardConfig); // Possible improvement -> draw trader-specific items e.g. with this.itemHelper.isOfBaseclass(val._id, ItemHelper.BASECLASS.FoodDrink) let roublesBudget = rewardRoubles; @@ -879,6 +884,18 @@ export class RepeatableQuestGenerator rewards.Success.push(reward); } + if (this.randomUtil.getChance100(skillRewardChance * 100)) + { + index++; + const reward: IReward = { + target: this.randomUtil.getArrayValue(questConfig.possibleSkillRewards), + value: skillPointReward, + type: "Skill", + index: index + }; + rewards.Success.push(reward); + } + return rewards; } diff --git a/project/src/helpers/HideoutHelper.ts b/project/src/helpers/HideoutHelper.ts index 5dcf48c5..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"; @@ -403,7 +403,7 @@ export class HideoutHelper //check unit consumed for increment skill point if (pmcData && Math.floor(pointsConsumed / 10) >= 1) { - this.playerService.incrementSkillLevel(pmcData, SkillTypes.HIDEOUT_MANAGEMENT, 1); + this.profileHelper.addSkillPointsToPlayer(pmcData, SkillTypes.HIDEOUT_MANAGEMENT, 1); pointsConsumed -= 10; } @@ -511,7 +511,7 @@ export class HideoutHelper // Check amount of units consumed for possible increment of hideout mgmt skill point if (pmcData && Math.floor(pointsConsumed / 10) >= 1) { - this.playerService.incrementSkillLevel(pmcData, SkillTypes.HIDEOUT_MANAGEMENT, 1); + this.profileHelper.addSkillPointsToPlayer(pmcData, SkillTypes.HIDEOUT_MANAGEMENT, 1); pointsConsumed -= 10; } @@ -637,7 +637,7 @@ export class HideoutHelper //check unit consumed for increment skill point if (pmcData && Math.floor(pointsConsumed / 10) >= 1) { - this.playerService.incrementSkillLevel(pmcData, SkillTypes.HIDEOUT_MANAGEMENT, 1); + this.profileHelper.addSkillPointsToPlayer(pmcData, SkillTypes.HIDEOUT_MANAGEMENT, 1); pointsConsumed -= 10; } @@ -794,22 +794,12 @@ export class HideoutHelper { const bitcoinProduction = this.databaseServer.getTables().hideout.production.find(p => p._id === HideoutHelper.bitcoinFarm); const productionSlots = bitcoinProduction?.productionLimitCount || 3; - const hasManagementSkillSlots = this.hasEliteHideoutManagementSkill(pmcData); + const hasManagementSkillSlots = this.profileHelper.hasEliteSkillLevel(SkillTypes.HIDEOUT_MANAGEMENT, pmcData); const managementSlotsCount = this.getBitcoinMinerContainerSlotSize() || 2; 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 5bd043fe..8ad71cd3 100644 --- a/project/src/helpers/ProfileHelper.ts +++ b/project/src/helpers/ProfileHelper.ts @@ -2,12 +2,14 @@ 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"; import { ILogger } from "@spt-aki/models/spt/utils/ILogger"; import { DatabaseServer } from "@spt-aki/servers/DatabaseServer"; import { SaveServer } from "@spt-aki/servers/SaveServer"; +import { LocalisationService } from "@spt-aki/services/LocalisationService"; import { ProfileSnapshotService } from "@spt-aki/services/ProfileSnapshotService"; import { JsonUtil } from "@spt-aki/utils/JsonUtil"; import { TimeUtil } from "@spt-aki/utils/TimeUtil"; @@ -24,7 +26,8 @@ export class ProfileHelper @inject("SaveServer") protected saveServer: SaveServer, @inject("DatabaseServer") protected databaseServer: DatabaseServer, @inject("ItemHelper") protected itemHelper: ItemHelper, - @inject("ProfileSnapshotService") protected profileSnapshotService: ProfileSnapshotService + @inject("ProfileSnapshotService") protected profileSnapshotService: ProfileSnapshotService, + @inject("LocalisationService") protected localisationService: LocalisationService ) { } @@ -353,4 +356,83 @@ export class ProfileHelper stat.Value++; } } + + /** + * Check if player has a skill at elite level + * @param skillType Skill to check + * @param pmcProfile Profile to find skill in + * @returns True if player has skill at elite level + */ + public hasEliteSkillLevel(skillType: SkillTypes, pmcProfile: IPmcData): boolean + { + const profileSkills = pmcProfile?.Skills?.Common; + if (!profileSkills) + { + return false; + } + + const profileSkill = profileSkills.find(x => x.Id === skillType); + if (!profileSkill) + { + this.logger.warning(`Unable to check for elite skill ${skillType}, not found in profile`); + + return false; + } + return profileSkill.Progress >= 5100; // level 51 + } + + /** + * Add points to a specific skill in player profile + * @param skill Skill to add points to + * @param pointsToAdd Points to add + * @param pmcProfile Player profile with skill + * @param useSkillProgressRateMultipler Skills are multiplied by a value in globals, default is off to maintain compatibility with legacy code + * @returns + */ + public addSkillPointsToPlayer(pmcProfile: IPmcData, skill: SkillTypes, pointsToAdd: number, useSkillProgressRateMultipler = false): void + { + if (!pointsToAdd || pointsToAdd < 0) + { + this.logger.error(this.localisationService.getText("player-attempt_to_increment_skill_with_negative_value", skill)); + return; + } + + const profileSkills = pmcProfile?.Skills?.Common; + if (!profileSkills) + { + this.logger.warning(`Unable to add ${pointsToAdd} points to ${skill}, profile has no skills`); + + return; + } + + const profileSkill = profileSkills.find(x => x.Id === skill); + if (!profileSkill) + { + this.logger.error(this.localisationService.getText("quest-no_skill_found", skill)); + + return; + } + + if (useSkillProgressRateMultipler) + { + const globals = this.databaseServer.getTables().globals; + const skillProgressRate = globals.config.SkillsSettings.SkillProgressRate; + pointsToAdd = skillProgressRate * pointsToAdd; + } + + 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 93edf145..2ea06add 100644 --- a/project/src/helpers/QuestHelper.ts +++ b/project/src/helpers/QuestHelper.ts @@ -18,6 +18,7 @@ import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes"; import { MessageType } from "@spt-aki/models/enums/MessageType"; import { QuestRewardType } from "@spt-aki/models/enums/QuestRewardType"; import { QuestStatus } from "@spt-aki/models/enums/QuestStatus"; +import { SkillTypes } from "@spt-aki/models/enums/SkillTypes"; import { IQuestConfig } from "@spt-aki/models/spt/config/IQuestConfig"; import { ILogger } from "@spt-aki/models/spt/utils/ILogger"; import { EventOutputHolder } from "@spt-aki/routers/EventOutputHolder"; @@ -127,42 +128,6 @@ export class QuestHelper return after; } - /** - * Increase skill points of a skill on player profile - * Dupe of PlayerService.incrementSkillLevel() - * @param sessionID Session id - * @param pmcData Player profile - * @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, scaleToSkillLevel: boolean = false): void - { - const indexOfSkillToUpdate = pmcData.Skills.Common.findIndex(s => s.Id === skillName); - if (indexOfSkillToUpdate === -1) - { - this.logger.error(this.localisationService.getText("quest-no_skill_found", skillName)); - - return; - } - - const profileSkill = pmcData.Skills.Common[indexOfSkillToUpdate]; - if (!profileSkill) - { - this.logger.error(this.localisationService.getText("quest-no_skill_found", skillName)); - - 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 @@ -771,7 +736,7 @@ export class QuestHelper switch (reward.type) { case QuestRewardType.SKILL: - this.rewardSkillPoints(sessionId, pmcData, reward.target, Number(reward.value)); + this.profileHelper.addSkillPointsToPlayer(pmcData, reward.target as SkillTypes, Number(reward.value)); break; case QuestRewardType.EXPERIENCE: this.profileHelper.addExperienceToPmc(sessionId, parseInt(reward.value)); // this must occur first as the output object needs to take the modified profile exp value @@ -853,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 === "HideoutManagement"); + 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/models/spt/config/IQuestConfig.ts b/project/src/models/spt/config/IQuestConfig.ts index 39a82437..34753847 100644 --- a/project/src/models/spt/config/IQuestConfig.ts +++ b/project/src/models/spt/config/IQuestConfig.ts @@ -68,6 +68,8 @@ export interface IRewardScaling items: number[] reputation: number[] rewardSpread: number + skillRewardChance: number[] + skillPointReward: number[] } export interface ITraderWhitelist @@ -84,7 +86,7 @@ export interface IRepeatableQuestTypesConfig Elimination: IEliminationConfig[] } -export interface IExploration +export interface IExploration extends IBaseQuestConfig { maxExtracts: number specificExits: ISpecificExits @@ -96,7 +98,7 @@ export interface ISpecificExits passageRequirementWhitelist: string[] } -export interface ICompletion +export interface ICompletion extends IBaseQuestConfig { minRequestedAmount: number maxRequestedAmount: number @@ -106,7 +108,7 @@ export interface ICompletion useBlacklist: boolean } -export interface IPickup +export interface IPickup extends IBaseQuestConfig { ItemTypeToFetchWithMaxCount: IPickupTypeWithMaxCount[] } @@ -118,7 +120,7 @@ export interface IPickupTypeWithMaxCount minPickupCount: number } -export interface IEliminationConfig +export interface IEliminationConfig extends IBaseQuestConfig { levelRange: MinMax targets: ITarget[] @@ -141,6 +143,11 @@ export interface IEliminationConfig weaponRequirements: IWeaponRequirement[] } +export interface IBaseQuestConfig +{ + possibleSkillRewards: string[] +} + export interface ITarget extends IProbabilityObject { data: IBossInfo diff --git a/project/src/services/PlayerService.ts b/project/src/services/PlayerService.ts index 190ce346..8ef07062 100644 --- a/project/src/services/PlayerService.ts +++ b/project/src/services/PlayerService.ts @@ -18,41 +18,6 @@ export class PlayerService ) { } - /** - * Dupe of QuestHelper.rewardsSkillPoints() - * Add xp to a player skill - * @param pmcData Player profile - * @param skillName Name of skill to increment - * @param amount Amount of skill points to add to skill - * @param useSkillProgressRateMultipler Skills are multiplied by a value in globals, default is off to maintain compatibility with legacy code - */ - public incrementSkillLevel(pmcData: IPmcData, skillName: string, amount: number, useSkillProgressRateMultipler = false): void - { - if (!amount || amount < 0) - { - this.logger.error(this.localisationService.getText("player-attempt_to_increment_skill_with_negative_value", skillName)); - return; - } - - const profileSkill = pmcData.Skills.Common.find(skill => skill.Id === skillName); - if (!profileSkill) - { - this.logger.error(this.localisationService.getText("quest-no_skill_found", skillName)); - - return; - } - - if (useSkillProgressRateMultipler) - { - const globals = this.databaseServer.getTables().globals; - const skillProgressRate = globals.config.SkillsSettings.SkillProgressRate; - amount = skillProgressRate * amount; - } - - profileSkill.Progress += amount; - profileSkill.LastAccess = this.timeUtil.getTimestamp(); - } - /** * Get level of player * @param pmcData Player profile diff --git a/project/src/services/RepairService.ts b/project/src/services/RepairService.ts index 5b49b577..918503ea 100644 --- a/project/src/services/RepairService.ts +++ b/project/src/services/RepairService.ts @@ -1,7 +1,7 @@ import { inject, injectable } from "tsyringe"; import { ItemHelper } from "@spt-aki/helpers/ItemHelper"; -import { QuestHelper } from "@spt-aki/helpers/QuestHelper"; +import { ProfileHelper } from "@spt-aki/helpers/ProfileHelper"; import { RepairHelper } from "@spt-aki/helpers/RepairHelper"; import { TraderHelper } from "@spt-aki/helpers/TraderHelper"; import { WeightedRandomHelper } from "@spt-aki/helpers/WeightedRandomHelper"; @@ -31,7 +31,7 @@ export class RepairService constructor( @inject("WinstonLogger") protected logger: ILogger, @inject("DatabaseServer") protected databaseServer: DatabaseServer, - @inject("QuestHelper") protected questHelper: QuestHelper, + @inject("ProfileHelper") protected profileHelper: ProfileHelper, @inject("RandomUtil") protected randomUtil: RandomUtil, @inject("ItemHelper") protected itemHelper: ItemHelper, @inject("TraderHelper") protected traderHelper: TraderHelper, @@ -151,7 +151,7 @@ export class RepairService { const skillPoints = this.getWeaponRepairSkillPoints(repairDetails); - this.questHelper.rewardSkillPoints(sessionId, pmcData, "WeaponTreatment", skillPoints, true); + this.profileHelper.addSkillPointsToPlayer(pmcData, SkillTypes.WEAPON_TREATMENT, skillPoints, true); } // Handle kit repairs of armor @@ -167,10 +167,12 @@ export class RepairService } const isHeavyArmor = itemDetails[1]._props.ArmorType === "Heavy"; - const vestSkillToLevel = (isHeavyArmor) ? "HeavyVests" : "LightVests"; + const vestSkillToLevel = (isHeavyArmor) + ? SkillTypes.HEAVY_VESTS + : SkillTypes.LIGHT_VESTS; const pointsToAddToVestSkill = repairDetails.repairPoints * this.repairConfig.armorKitSkillPointGainPerRepairPointMultiplier; - this.questHelper.rewardSkillPoints(sessionId, pmcData, vestSkillToLevel, pointsToAddToVestSkill); + this.profileHelper.addSkillPointsToPlayer(pmcData, vestSkillToLevel, pointsToAddToVestSkill); } // Handle giving INT to player - differs if using kit/trader and weapon vs armor @@ -190,7 +192,7 @@ export class RepairService intellectGainedFromRepair = Math.min(repairDetails.repairAmount / 10, this.repairConfig.maxIntellectGainPerRepair.trader); } - this.questHelper.rewardSkillPoints(sessionId, pmcData, SkillTypes.INTELLECT, intellectGainedFromRepair); + this.profileHelper.addSkillPointsToPlayer(pmcData, SkillTypes.INTELLECT, intellectGainedFromRepair); } /** @@ -257,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, @@ -265,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) @@ -304,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) @@ -350,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 @@ -446,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); @@ -472,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;