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

This commit is contained in:
Dev 2023-11-07 20:59:14 +00:00
commit d2736f4004
14 changed files with 266 additions and 146 deletions

View File

@ -146,9 +146,11 @@
"levels": [1, 10, 20, 30, 40, 50, 60], "levels": [1, 10, 20, 30, 40, 50, 60],
"experience": [1000, 12000, 42000, 89000, 177000, 300000, 500000], "experience": [1000, 12000, 42000, 89000, 177000, 300000, 500000],
"roubles": [15000, 40000, 75000, 100000, 140000, 170000, 210000], "roubles": [15000, 40000, 75000, 100000, 140000, 170000, 210000],
"items": [4, 4, 5, 5, 5, 6, 6], "items": [3, 4, 5, 5, 5, 5, 5],
"reputation": [0.01, 0.01, 0.02, 0.02], "reputation": [0.01, 0.01, 0.02, 0.02, 0.03, 0.03, 0.03],
"rewardSpread": 0.5 "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": { "locations": {
"any": ["any"], "any": ["any"],
@ -188,6 +190,7 @@
"questConfig": { "questConfig": {
"Exploration": { "Exploration": {
"maxExtracts": 3, "maxExtracts": 3,
"possibleSkillRewards": ["Endurance", "Strength", "Vitality"],
"specificExits": { "specificExits": {
"probability": 0.25, "probability": 0.25,
"passageRequirementWhitelist": [ "passageRequirementWhitelist": [
@ -201,6 +204,7 @@
} }
}, },
"Completion": { "Completion": {
"possibleSkillRewards": ["Endurance", "Strength", "Vitality"],
"minRequestedAmount": 1, "minRequestedAmount": 1,
"maxRequestedAmount": 5, "maxRequestedAmount": 5,
"minRequestedBulletAmount": 20, "minRequestedBulletAmount": 20,
@ -213,6 +217,7 @@
"min": 1, "min": 1,
"max": 15 "max": 15
}, },
"possibleSkillRewards": ["Endurance", "Strength", "Vitality"],
"targets": [{ "targets": [{
"key": "Savage", "key": "Savage",
"relativeProbability": 1, "relativeProbability": 1,
@ -326,6 +331,7 @@
"min": 16, "min": 16,
"max": 40 "max": 40
}, },
"possibleSkillRewards": ["Endurance", "Strength", "Vitality"],
"targets": [{ "targets": [{
"key": "Savage", "key": "Savage",
"relativeProbability": 9, "relativeProbability": 9,
@ -507,6 +513,7 @@
"min": 41, "min": 41,
"max": 100 "max": 100
}, },
"possibleSkillRewards": ["Endurance", "Strength", "Vitality"],
"targets": [{ "targets": [{
"key": "Savage", "key": "Savage",
"relativeProbability": 9, "relativeProbability": 9,
@ -704,9 +711,11 @@
"levels": [1, 10, 20, 30, 40, 50, 60], "levels": [1, 10, 20, 30, 40, 50, 60],
"experience": [5000, 25000, 60000, 130000, 240000, 390000, 750000], "experience": [5000, 25000, 60000, 130000, 240000, 390000, 750000],
"roubles": [50000, 150000, 300000, 425000, 550000, 675000, 850000], "roubles": [50000, 150000, 300000, 425000, 550000, 675000, 850000],
"items": [5, 5, 5, 6, 6, 7, 7], "items": [4, 5, 5, 6, 6, 7, 7],
"reputation": [0.02, 0.02, 0.03, 0.03, 0.03, 0.03, 0.03], "reputation": [0.02, 0.03, 0.04, 0.04, 0.05, 0.05, 0.05],
"rewardSpread": 0.5 "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": { "locations": {
"any": ["any"], "any": ["any"],
@ -745,6 +754,7 @@
], ],
"questConfig": { "questConfig": {
"Exploration": { "Exploration": {
"possibleSkillRewards": ["Endurance", "Strength", "Vitality"],
"maxExtracts": 10, "maxExtracts": 10,
"specificExits": { "specificExits": {
"probability": 0.4, "probability": 0.4,
@ -759,6 +769,7 @@
} }
}, },
"Completion": { "Completion": {
"possibleSkillRewards": ["Endurance", "Strength", "Vitality"],
"minRequestedAmount": 2, "minRequestedAmount": 2,
"maxRequestedAmount": 10, "maxRequestedAmount": 10,
"minRequestedBulletAmount": 20, "minRequestedBulletAmount": 20,
@ -771,6 +782,7 @@
"min": 1, "min": 1,
"max": 15 "max": 15
}, },
"possibleSkillRewards": ["Endurance", "Strength", "Vitality"],
"targets": [{ "targets": [{
"key": "Savage", "key": "Savage",
"relativeProbability": 15, "relativeProbability": 15,
@ -951,6 +963,7 @@
"min": 16, "min": 16,
"max": 40 "max": 40
}, },
"possibleSkillRewards": ["Endurance", "Strength", "Vitality"],
"targets": [{ "targets": [{
"key": "Savage", "key": "Savage",
"relativeProbability": 7, "relativeProbability": 7,
@ -1131,6 +1144,7 @@
"min": 41, "min": 41,
"max": 100 "max": 100
}, },
"possibleSkillRewards": ["Endurance", "Strength", "Vitality"],
"targets": [{ "targets": [{
"key": "Savage", "key": "Savage",
"relativeProbability": 7, "relativeProbability": 7,
@ -1331,7 +1345,9 @@
"roubles": [6000, 10000, 100000, 250000], "roubles": [6000, 10000, 100000, 250000],
"items": [2, 3, 4, 4], "items": [2, 3, 4, 4],
"reputation": [0.01, 0.02, 0.05, 0.05], "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": { "locations": {
"any": ["any"], "any": ["any"],
@ -1351,6 +1367,7 @@
], ],
"questConfig": { "questConfig": {
"Exploration": { "Exploration": {
"possibleSkillRewards": ["Endurance", "Strength", "Vitality"],
"maxExtracts": 3, "maxExtracts": 3,
"specificExits": { "specificExits": {
"probability": 0.25, "probability": 0.25,
@ -1364,6 +1381,7 @@
} }
}, },
"Pickup": { "Pickup": {
"possibleSkillRewards": ["Endurance", "Strength", "Vitality"],
"ItemTypeToFetchWithMaxCount": [{ "ItemTypeToFetchWithMaxCount": [{
"itemType": "5b47574386f77428ca22b335", "itemType": "5b47574386f77428ca22b335",
"minPickupCount": 2, "minPickupCount": 2,
@ -1410,6 +1428,7 @@
"maxItemFetchCount": 3 "maxItemFetchCount": 3
}, },
"Completion": { "Completion": {
"possibleSkillRewards": ["Endurance", "Strength", "Vitality"],
"minRequestedAmount": 1, "minRequestedAmount": 1,
"maxRequestedAmount": 5, "maxRequestedAmount": 5,
"minRequestedBulletAmount": 20, "minRequestedBulletAmount": 20,
@ -1422,6 +1441,7 @@
"min": 1, "min": 1,
"max": 15 "max": 15
}, },
"possibleSkillRewards": ["Endurance", "Strength", "Vitality"],
"targets": "targets":
[{ [{
"key": "AnyPmc", "key": "AnyPmc",
@ -1460,7 +1480,7 @@
"minKills": 1, "minKills": 1,
"maxBossKills": 1, "maxBossKills": 1,
"minBossKills": 1, "minBossKills": 1,
"maxPmcKills": 2, "maxPmcKills": 2,
"minPmcKills": 1, "minPmcKills": 1,
"weaponRequirementProb": 0, "weaponRequirementProb": 0,
"weaponCategoryRequirementProb": 0.3, "weaponCategoryRequirementProb": 0.3,
@ -1537,6 +1557,7 @@
"min": 16, "min": 16,
"max": 100 "max": 100
}, },
"possibleSkillRewards": ["Endurance", "Strength", "Vitality"],
"targets": "targets":
[{ [{
"key": "AnyPmc", "key": "AnyPmc",
@ -1575,7 +1596,7 @@
"minKills": 3, "minKills": 3,
"maxBossKills": 3, "maxBossKills": 3,
"minBossKills": 1, "minBossKills": 1,
"maxPmcKills": 5, "maxPmcKills": 5,
"minPmcKills": 2, "minPmcKills": 2,
"weaponRequirementProb": 0, "weaponRequirementProb": 0,
"weaponCategoryRequirementProb": 0.3, "weaponCategoryRequirementProb": 0.3,

View File

@ -18,6 +18,7 @@ import { IServerDetails } from "@spt-aki/models/eft/game/IServerDetails";
import { IAkiProfile } from "@spt-aki/models/eft/profile/IAkiProfile"; import { IAkiProfile } from "@spt-aki/models/eft/profile/IAkiProfile";
import { AccountTypes } from "@spt-aki/models/enums/AccountTypes"; import { AccountTypes } from "@spt-aki/models/enums/AccountTypes";
import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes"; 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 { Traders } from "@spt-aki/models/enums/Traders";
import { ICoreConfig } from "@spt-aki/models/spt/config/ICoreConfig"; import { ICoreConfig } from "@spt-aki/models/spt/config/ICoreConfig";
import { IHttpConfig } from "@spt-aki/models/spt/config/IHttpConfig"; import { IHttpConfig } from "@spt-aki/models/spt/config/IHttpConfig";
@ -118,6 +119,8 @@ export class GameController
this.adjustLooseLootSpawnProbabilities(); this.adjustLooseLootSpawnProbabilities();
this.checkTraderRepairValuesExist();
// repeatableQuests are stored by in profile.Quests due to the responses of the client (e.g. Quests in offraidData) // 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. // 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 // 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 protected addCustomLooseLootPositions(): void
{ {
const looseLootPositionsToAdd = this.lootConfig.looseLoot; const looseLootPositionsToAdd = this.lootConfig.looseLoot;
@ -472,7 +499,7 @@ export class GameController
*/ */
protected warnOnActiveBotReloadSkill(pmcProfile: IPmcData): void 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) if (botReloadSkill?.Progress > 0)
{ {
this.logger.warning(this.localisationService.getText("server_start_player_active_botreload_skill")); this.logger.warning(this.localisationService.getText("server_start_player_active_botreload_skill"));

View File

@ -200,7 +200,7 @@ export class HideoutController
} }
// Add Skill Points Per Area Upgrade // 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; return output;
} }
@ -723,12 +723,12 @@ export class HideoutController
// manager Hideout skill // manager Hideout skill
// ? use a configuration variable for the value? // ? use a configuration variable for the value?
const globals = this.databaseServer.getTables().globals; 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 //manager Crafting skill
if (craftingExpAmount > 0) if (craftingExpAmount > 0)
{ {
this.playerService.incrementSkillLevel(pmcData, SkillTypes.CRAFTING, craftingExpAmount); this.profileHelper.addSkillPointsToPlayer(pmcData, SkillTypes.CRAFTING, craftingExpAmount);
this.playerService.incrementSkillLevel(pmcData, SkillTypes.INTELLECT, 0.5 * (Math.round(craftingExpAmount / 15))); this.profileHelper.addSkillPointsToPlayer(pmcData, SkillTypes.INTELLECT, 0.5 * (Math.round(craftingExpAmount / 15)));
} }
area.lastRecipe = request.recipeId; area.lastRecipe = request.recipeId;
counterHoursCrafting.value = hoursCrafting; counterHoursCrafting.value = hoursCrafting;

View File

@ -10,10 +10,11 @@ import { IGetInsuranceCostRequestData } from "@spt-aki/models/eft/insurance/IGet
import { IGetInsuranceCostResponseData } from "@spt-aki/models/eft/insurance/IGetInsuranceCostResponseData"; import { IGetInsuranceCostResponseData } from "@spt-aki/models/eft/insurance/IGetInsuranceCostResponseData";
import { IInsureRequestData } from "@spt-aki/models/eft/insurance/IInsureRequestData"; import { IInsureRequestData } from "@spt-aki/models/eft/insurance/IInsureRequestData";
import { IItemEventRouterResponse } from "@spt-aki/models/eft/itemEvent/IItemEventRouterResponse"; 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 { IProcessBuyTradeRequestData } from "@spt-aki/models/eft/trade/IProcessBuyTradeRequestData";
import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes"; import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes";
import { MessageType } from "@spt-aki/models/enums/MessageType"; 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 { IInsuranceConfig } from "@spt-aki/models/spt/config/IInsuranceConfig";
import { ILogger } from "@spt-aki/models/spt/utils/ILogger"; import { ILogger } from "@spt-aki/models/spt/utils/ILogger";
import { EventOutputHolder } from "@spt-aki/routers/EventOutputHolder"; import { EventOutputHolder } from "@spt-aki/routers/EventOutputHolder";
@ -552,6 +553,7 @@ export class InsuranceController
public insure(pmcData: IPmcData, body: IInsureRequestData, sessionID: string): IItemEventRouterResponse public insure(pmcData: IPmcData, body: IInsureRequestData, sessionID: string): IItemEventRouterResponse
{ {
let output = this.eventOutputHolder.getOutput(sessionID); let output = this.eventOutputHolder.getOutput(sessionID);
const itemsToInsureCount = body.items.length;
const itemsToPay = []; const itemsToPay = [];
const inventoryItemsHash = {}; const inventoryItemsHash = {};
@ -598,6 +600,8 @@ export class InsuranceController
}); });
} }
this.profileHelper.addSkillPointsToPlayer(pmcData, SkillTypes.CHARISMA, itemsToInsureCount * 0.01);
return output; return output;
} }

View File

@ -599,7 +599,7 @@ export class InventoryController
pmcData.Encyclopedia[itemId] = true; pmcData.Encyclopedia[itemId] = true;
// TODO: update this with correct calculation using values from globals json // 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); return this.eventOutputHolder.getOutput(sessionID);

View File

@ -14,11 +14,13 @@ import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes";
import { ELocationName } from "@spt-aki/models/enums/ELocationName"; import { ELocationName } from "@spt-aki/models/enums/ELocationName";
import { HideoutAreas } from "@spt-aki/models/enums/HideoutAreas"; import { HideoutAreas } from "@spt-aki/models/enums/HideoutAreas";
import { QuestStatus } from "@spt-aki/models/enums/QuestStatus"; 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 { IQuestConfig, IRepeatableQuestConfig } from "@spt-aki/models/spt/config/IQuestConfig";
import { IQuestTypePool } from "@spt-aki/models/spt/repeatable/IQuestTypePool"; import { IQuestTypePool } from "@spt-aki/models/spt/repeatable/IQuestTypePool";
import { ILogger } from "@spt-aki/models/spt/utils/ILogger"; import { ILogger } from "@spt-aki/models/spt/utils/ILogger";
import { EventOutputHolder } from "@spt-aki/routers/EventOutputHolder"; import { EventOutputHolder } from "@spt-aki/routers/EventOutputHolder";
import { ConfigServer } from "@spt-aki/servers/ConfigServer"; import { ConfigServer } from "@spt-aki/servers/ConfigServer";
import { DatabaseServer } from "@spt-aki/servers/DatabaseServer";
import { PaymentService } from "@spt-aki/services/PaymentService"; import { PaymentService } from "@spt-aki/services/PaymentService";
import { ProfileFixerService } from "@spt-aki/services/ProfileFixerService"; import { ProfileFixerService } from "@spt-aki/services/ProfileFixerService";
import { HttpResponseUtil } from "@spt-aki/utils/HttpResponseUtil"; import { HttpResponseUtil } from "@spt-aki/utils/HttpResponseUtil";
@ -33,8 +35,9 @@ export class RepeatableQuestController
protected questConfig: IQuestConfig; protected questConfig: IQuestConfig;
constructor( constructor(
@inject("TimeUtil") protected timeUtil: TimeUtil,
@inject("WinstonLogger") protected logger: ILogger, @inject("WinstonLogger") protected logger: ILogger,
@inject("DatabaseServer") protected databaseServer: DatabaseServer,
@inject("TimeUtil") protected timeUtil: TimeUtil,
@inject("RandomUtil") protected randomUtil: RandomUtil, @inject("RandomUtil") protected randomUtil: RandomUtil,
@inject("HttpResponseUtil") protected httpResponse: HttpResponseUtil, @inject("HttpResponseUtil") protected httpResponse: HttpResponseUtil,
@inject("JsonUtil") protected jsonUtil: JsonUtil, @inject("JsonUtil") protected jsonUtil: JsonUtil,
@ -131,7 +134,7 @@ export class RepeatableQuestController
const questTypePool = this.generateQuestPool(repeatableConfig, pmcData.Info.Level); const questTypePool = this.generateQuestPool(repeatableConfig, pmcData.Info.Level);
// Add daily quests // 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 quest = null;
let lifeline = 0; let lifeline = 0;
@ -188,6 +191,23 @@ export class RepeatableQuestController
return returnData; 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 * Get repeatable quest data from profile from name (daily/weekly), creates base repeatable quest object if none exists
* @param repeatableConfig daily/weekly config * @param repeatableConfig daily/weekly config

View File

@ -24,7 +24,7 @@ import { BaseClasses } from "@spt-aki/models/enums/BaseClasses";
import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes"; import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes";
import { Money } from "@spt-aki/models/enums/Money"; import { Money } from "@spt-aki/models/enums/Money";
import { Traders } from "@spt-aki/models/enums/Traders"; 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 { IQuestTypePool } from "@spt-aki/models/spt/repeatable/IQuestTypePool";
import { ILogger } from "@spt-aki/models/spt/utils/ILogger"; import { ILogger } from "@spt-aki/models/spt/utils/ILogger";
import { EventOutputHolder } from "@spt-aki/routers/EventOutputHolder"; import { EventOutputHolder } from "@spt-aki/routers/EventOutputHolder";
@ -326,7 +326,7 @@ export class RepeatableQuestGenerator
availableForFinishCondition._props.id = this.objectId.generate(); availableForFinishCondition._props.id = this.objectId.generate();
quest.location = this.getQuestLocationByMapId(locationKey); 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; 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; return quest;
} }
@ -673,7 +673,7 @@ export class RepeatableQuestGenerator
// Difficulty for exploration goes from 1 extract to maxExtracts // Difficulty for exploration goes from 1 extract to maxExtracts
// Difficulty for reward goes from 0.2...1 -> map // Difficulty for reward goes from 0.2...1 -> map
const difficulty = this.mathUtil.mapToRange(numExtracts, 1, explorationConfig.maxExtracts, 0.2, 1); 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; return quest;
} }
@ -707,7 +707,7 @@ export class RepeatableQuestGenerator
(equipmentCondition._props as IEquipmentConditionProps).equipmentInclusive = [[itemTypeToFetchWithCount.itemType]]; (equipmentCondition._props as IEquipmentConditionProps).equipmentInclusive = [[itemTypeToFetchWithCount.itemType]];
// Add rewards // Add rewards
quest.rewards = this.generateReward(pmcLevel, 1, traderId, repeatableConfig); quest.rewards = this.generateReward(pmcLevel, 1, traderId, repeatableConfig, pickupConfig);
return quest; return quest;
} }
@ -765,7 +765,8 @@ export class RepeatableQuestGenerator
pmcLevel: number, pmcLevel: number,
difficulty: number, difficulty: number,
traderId: string, traderId: string,
repeatableConfig: IRepeatableQuestConfig repeatableConfig: IRepeatableQuestConfig,
questConfig: IBaseQuestConfig
): IRewards ): IRewards
{ {
// difficulty could go from 0.2 ... -> for lowest diffuculty receive 0.2*nominal reward // 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 xpConfig = repeatableConfig.rewardScaling.experience;
const itemsConfig = repeatableConfig.rewardScaling.items; const itemsConfig = repeatableConfig.rewardScaling.items;
const rewardSpreadConfig = repeatableConfig.rewardScaling.rewardSpread; const rewardSpreadConfig = repeatableConfig.rewardScaling.rewardSpread;
const skillRewardChanceConfig = repeatableConfig.rewardScaling.skillRewardChance;
const skillPointRewardConfig = repeatableConfig.rewardScaling.skillPointReward;
const reputationConfig = repeatableConfig.rewardScaling.reputation; const reputationConfig = repeatableConfig.rewardScaling.reputation;
if (Number.isNaN(difficulty)) 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 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) const rewardReputation = Math.round(100 * difficulty * this.mathUtil.interp1(pmcLevel, levelsConfig, reputationConfig)
* this.randomUtil.getFloat(1 - rewardSpreadConfig, 1 + rewardSpreadConfig)) / 100; * 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) // Possible improvement -> draw trader-specific items e.g. with this.itemHelper.isOfBaseclass(val._id, ItemHelper.BASECLASS.FoodDrink)
let roublesBudget = rewardRoubles; let roublesBudget = rewardRoubles;
@ -879,6 +884,18 @@ export class RepeatableQuestGenerator
rewards.Success.push(reward); 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; return rewards;
} }

View File

@ -3,7 +3,7 @@ import { inject, injectable } from "tsyringe";
import { InventoryHelper } from "@spt-aki/helpers/InventoryHelper"; import { InventoryHelper } from "@spt-aki/helpers/InventoryHelper";
import { ProfileHelper } from "@spt-aki/helpers/ProfileHelper"; import { ProfileHelper } from "@spt-aki/helpers/ProfileHelper";
import { IPmcData } from "@spt-aki/models/eft/common/IPmcData"; 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 { Upd } from "@spt-aki/models/eft/common/tables/IItem";
import { StageBonus } from "@spt-aki/models/eft/hideout/IHideoutArea"; import { StageBonus } from "@spt-aki/models/eft/hideout/IHideoutArea";
import { IHideoutContinuousProductionStartRequestData } from "@spt-aki/models/eft/hideout/IHideoutContinuousProductionStartRequestData"; import { IHideoutContinuousProductionStartRequestData } from "@spt-aki/models/eft/hideout/IHideoutContinuousProductionStartRequestData";
@ -403,7 +403,7 @@ export class HideoutHelper
//check unit consumed for increment skill point //check unit consumed for increment skill point
if (pmcData && Math.floor(pointsConsumed / 10) >= 1) 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; pointsConsumed -= 10;
} }
@ -511,7 +511,7 @@ export class HideoutHelper
// Check amount of units consumed for possible increment of hideout mgmt skill point // Check amount of units consumed for possible increment of hideout mgmt skill point
if (pmcData && Math.floor(pointsConsumed / 10) >= 1) 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; pointsConsumed -= 10;
} }
@ -637,7 +637,7 @@ export class HideoutHelper
//check unit consumed for increment skill point //check unit consumed for increment skill point
if (pmcData && Math.floor(pointsConsumed / 10) >= 1) 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; pointsConsumed -= 10;
} }
@ -794,22 +794,12 @@ export class HideoutHelper
{ {
const bitcoinProduction = this.databaseServer.getTables().hideout.production.find(p => p._id === HideoutHelper.bitcoinFarm); const bitcoinProduction = this.databaseServer.getTables().hideout.production.find(p => p._id === HideoutHelper.bitcoinFarm);
const productionSlots = bitcoinProduction?.productionLimitCount || 3; const productionSlots = bitcoinProduction?.productionLimitCount || 3;
const hasManagementSkillSlots = this.hasEliteHideoutManagementSkill(pmcData); const hasManagementSkillSlots = this.profileHelper.hasEliteSkillLevel(SkillTypes.HIDEOUT_MANAGEMENT, pmcData);
const managementSlotsCount = this.getBitcoinMinerContainerSlotSize() || 2; const managementSlotsCount = this.getBitcoinMinerContainerSlotSize() || 2;
return productionSlots + (hasManagementSkillSlots ? managementSlotsCount : 0); 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 * 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; 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 * HideoutManagement skill gives a consumption bonus the higher the level
* 0.5% per level per 1-51, (25.5% at max) * 0.5% per level per 1-51, (25.5% at max)
@ -836,7 +816,7 @@ export class HideoutHelper
*/ */
protected getHideoutManagementConsumptionBonus(pmcData: IPmcData): number protected getHideoutManagementConsumptionBonus(pmcData: IPmcData): number
{ {
const hideoutManagementSkill = this.getHideoutManagementSkill(pmcData); const hideoutManagementSkill = this.profileHelper.getSkillFromProfile(pmcData, SkillTypes.HIDEOUT_MANAGEMENT);
if (!hideoutManagementSkill) if (!hideoutManagementSkill)
{ {
return 0; return 0;

View File

@ -2,12 +2,14 @@ import { inject, injectable } from "tsyringe";
import { ItemHelper } from "@spt-aki/helpers/ItemHelper"; import { ItemHelper } from "@spt-aki/helpers/ItemHelper";
import { IPmcData } from "@spt-aki/models/eft/common/IPmcData"; 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 { IAkiProfile } from "@spt-aki/models/eft/profile/IAkiProfile";
import { IValidateNicknameRequestData } from "@spt-aki/models/eft/profile/IValidateNicknameRequestData"; 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 { ILogger } from "@spt-aki/models/spt/utils/ILogger";
import { DatabaseServer } from "@spt-aki/servers/DatabaseServer"; import { DatabaseServer } from "@spt-aki/servers/DatabaseServer";
import { SaveServer } from "@spt-aki/servers/SaveServer"; import { SaveServer } from "@spt-aki/servers/SaveServer";
import { LocalisationService } from "@spt-aki/services/LocalisationService";
import { ProfileSnapshotService } from "@spt-aki/services/ProfileSnapshotService"; import { ProfileSnapshotService } from "@spt-aki/services/ProfileSnapshotService";
import { JsonUtil } from "@spt-aki/utils/JsonUtil"; import { JsonUtil } from "@spt-aki/utils/JsonUtil";
import { TimeUtil } from "@spt-aki/utils/TimeUtil"; import { TimeUtil } from "@spt-aki/utils/TimeUtil";
@ -24,7 +26,8 @@ export class ProfileHelper
@inject("SaveServer") protected saveServer: SaveServer, @inject("SaveServer") protected saveServer: SaveServer,
@inject("DatabaseServer") protected databaseServer: DatabaseServer, @inject("DatabaseServer") protected databaseServer: DatabaseServer,
@inject("ItemHelper") protected itemHelper: ItemHelper, @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++; 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;
}
} }

View File

@ -18,6 +18,7 @@ import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes";
import { MessageType } from "@spt-aki/models/enums/MessageType"; import { MessageType } from "@spt-aki/models/enums/MessageType";
import { QuestRewardType } from "@spt-aki/models/enums/QuestRewardType"; import { QuestRewardType } from "@spt-aki/models/enums/QuestRewardType";
import { QuestStatus } from "@spt-aki/models/enums/QuestStatus"; 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 { IQuestConfig } from "@spt-aki/models/spt/config/IQuestConfig";
import { ILogger } from "@spt-aki/models/spt/utils/ILogger"; import { ILogger } from "@spt-aki/models/spt/utils/ILogger";
import { EventOutputHolder } from "@spt-aki/routers/EventOutputHolder"; import { EventOutputHolder } from "@spt-aki/routers/EventOutputHolder";
@ -127,42 +128,6 @@ export class QuestHelper
return after; 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 * Adjust skill experience for low skill levels, mimicing the official client
* @param profileSkill the skill experience is being added to * @param profileSkill the skill experience is being added to
@ -771,7 +736,7 @@ export class QuestHelper
switch (reward.type) switch (reward.type)
{ {
case QuestRewardType.SKILL: case QuestRewardType.SKILL:
this.rewardSkillPoints(sessionId, pmcData, reward.target, Number(reward.value)); this.profileHelper.addSkillPointsToPlayer(pmcData, reward.target as SkillTypes, Number(reward.value));
break; break;
case QuestRewardType.EXPERIENCE: case QuestRewardType.EXPERIENCE:
this.profileHelper.addExperienceToPmc(sessionId, parseInt(<string>reward.value)); // this must occur first as the output object needs to take the modified profile exp value this.profileHelper.addExperienceToPmc(sessionId, parseInt(<string>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); let moneyRewardBonus = moneyRewardBonuses.reduce((acc, cur) => acc + cur.value, 0);
// Apply hideout management bonus to money reward (up to 51% bonus) // 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) 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) 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)

View File

@ -1,5 +1,7 @@
export enum SkillTypes export enum SkillTypes
{ {
BOT_RELOAD = "BotReload",
BOT_SOUND = "BotSound",
HIDEOUT_MANAGEMENT = "HideoutManagement", HIDEOUT_MANAGEMENT = "HideoutManagement",
CRAFTING = "Crafting", CRAFTING = "Crafting",
METABOLISM = "Metabolism", METABOLISM = "Metabolism",
@ -20,6 +22,7 @@ export enum SkillTypes
ATTENTION = "Attention", ATTENTION = "Attention",
CHARISMA = "Charisma", CHARISMA = "Charisma",
MEMORY = "Memory", MEMORY = "Memory",
MELEE = "Melee",
SURGERY = "Surgery", SURGERY = "Surgery",
AIM_DRILLS = "AimDrills", AIM_DRILLS = "AimDrills",
TROUBLESHOOTING = "TroubleShooting", TROUBLESHOOTING = "TroubleShooting",
@ -32,6 +35,7 @@ export enum SkillTypes
NIGHT_OPS = "NightOps", NIGHT_OPS = "NightOps",
SILENT_OPS = "SilentOps", SILENT_OPS = "SilentOps",
LOCKPICKING = "Lockpicking", LOCKPICKING = "Lockpicking",
/** Also called Weapon Maintenance*/
WEAPON_TREATMENT = "WeaponTreatment", WEAPON_TREATMENT = "WeaponTreatment",
MAG_DRILLS = "MagDrills", MAG_DRILLS = "MagDrills",
FREE_TRADING = "Freetrading", FREE_TRADING = "Freetrading",

View File

@ -68,6 +68,8 @@ export interface IRewardScaling
items: number[] items: number[]
reputation: number[] reputation: number[]
rewardSpread: number rewardSpread: number
skillRewardChance: number[]
skillPointReward: number[]
} }
export interface ITraderWhitelist export interface ITraderWhitelist
@ -84,7 +86,7 @@ export interface IRepeatableQuestTypesConfig
Elimination: IEliminationConfig[] Elimination: IEliminationConfig[]
} }
export interface IExploration export interface IExploration extends IBaseQuestConfig
{ {
maxExtracts: number maxExtracts: number
specificExits: ISpecificExits specificExits: ISpecificExits
@ -96,7 +98,7 @@ export interface ISpecificExits
passageRequirementWhitelist: string[] passageRequirementWhitelist: string[]
} }
export interface ICompletion export interface ICompletion extends IBaseQuestConfig
{ {
minRequestedAmount: number minRequestedAmount: number
maxRequestedAmount: number maxRequestedAmount: number
@ -106,7 +108,7 @@ export interface ICompletion
useBlacklist: boolean useBlacklist: boolean
} }
export interface IPickup export interface IPickup extends IBaseQuestConfig
{ {
ItemTypeToFetchWithMaxCount: IPickupTypeWithMaxCount[] ItemTypeToFetchWithMaxCount: IPickupTypeWithMaxCount[]
} }
@ -118,7 +120,7 @@ export interface IPickupTypeWithMaxCount
minPickupCount: number minPickupCount: number
} }
export interface IEliminationConfig export interface IEliminationConfig extends IBaseQuestConfig
{ {
levelRange: MinMax levelRange: MinMax
targets: ITarget[] targets: ITarget[]
@ -141,6 +143,11 @@ export interface IEliminationConfig
weaponRequirements: IWeaponRequirement[] weaponRequirements: IWeaponRequirement[]
} }
export interface IBaseQuestConfig
{
possibleSkillRewards: string[]
}
export interface ITarget extends IProbabilityObject export interface ITarget extends IProbabilityObject
{ {
data: IBossInfo data: IBossInfo

View File

@ -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 * Get level of player
* @param pmcData Player profile * @param pmcData Player profile

View File

@ -1,7 +1,7 @@
import { inject, injectable } from "tsyringe"; import { inject, injectable } from "tsyringe";
import { ItemHelper } from "@spt-aki/helpers/ItemHelper"; 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 { RepairHelper } from "@spt-aki/helpers/RepairHelper";
import { TraderHelper } from "@spt-aki/helpers/TraderHelper"; import { TraderHelper } from "@spt-aki/helpers/TraderHelper";
import { WeightedRandomHelper } from "@spt-aki/helpers/WeightedRandomHelper"; import { WeightedRandomHelper } from "@spt-aki/helpers/WeightedRandomHelper";
@ -31,7 +31,7 @@ export class RepairService
constructor( constructor(
@inject("WinstonLogger") protected logger: ILogger, @inject("WinstonLogger") protected logger: ILogger,
@inject("DatabaseServer") protected databaseServer: DatabaseServer, @inject("DatabaseServer") protected databaseServer: DatabaseServer,
@inject("QuestHelper") protected questHelper: QuestHelper, @inject("ProfileHelper") protected profileHelper: ProfileHelper,
@inject("RandomUtil") protected randomUtil: RandomUtil, @inject("RandomUtil") protected randomUtil: RandomUtil,
@inject("ItemHelper") protected itemHelper: ItemHelper, @inject("ItemHelper") protected itemHelper: ItemHelper,
@inject("TraderHelper") protected traderHelper: TraderHelper, @inject("TraderHelper") protected traderHelper: TraderHelper,
@ -151,7 +151,7 @@ export class RepairService
{ {
const skillPoints = this.getWeaponRepairSkillPoints(repairDetails); 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 // Handle kit repairs of armor
@ -167,10 +167,12 @@ export class RepairService
} }
const isHeavyArmor = itemDetails[1]._props.ArmorType === "Heavy"; const isHeavyArmor = itemDetails[1]._props.ArmorType === "Heavy";
const vestSkillToLevel = (isHeavyArmor) ? "HeavyVests" : "LightVests"; const vestSkillToLevel = (isHeavyArmor)
? SkillTypes.HEAVY_VESTS
: SkillTypes.LIGHT_VESTS;
const pointsToAddToVestSkill = repairDetails.repairPoints * this.repairConfig.armorKitSkillPointGainPerRepairPointMultiplier; 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 // 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); 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 itemToRepairDetails = itemsDb[itemToRepair._tpl];
const repairItemIsArmor = (!!itemToRepairDetails._props.ArmorMaterial); const repairItemIsArmor = (!!itemToRepairDetails._props.ArmorMaterial);
const repairAmount = repairKits[0].count / this.getKitDivisor(itemToRepairDetails, repairItemIsArmor, pmcData); const repairAmount = repairKits[0].count / this.getKitDivisor(itemToRepairDetails, repairItemIsArmor, pmcData);
const shouldApplyDurabilityLoss = this.shouldRepairKitApplyDurabilityLoss(pmcData, this.repairConfig.applyRandomizeDurabilityLoss);
this.repairHelper.updateItemDurability( this.repairHelper.updateItemDurability(
itemToRepair, itemToRepair,
@ -265,7 +268,7 @@ export class RepairService
repairAmount, repairAmount,
true, true,
1, 1,
this.repairConfig.applyRandomizeDurabilityLoss); shouldApplyDurabilityLoss);
// Find and use repair kit defined in body // Find and use repair kit defined in body
for (const repairKit of repairKits) for (const repairKit of repairKits)
@ -304,7 +307,7 @@ export class RepairService
const globalRepairSettings = globals.config.RepairSettings; const globalRepairSettings = globals.config.RepairSettings;
const intellectRepairPointsPerLevel = globals.config.SkillsSettings.Intellect.RepairPointsCostReduction; 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); const intellectPointReduction = intellectRepairPointsPerLevel * Math.trunc(profileIntellectLevel / 100);
if (isArmor) if (isArmor)
@ -350,6 +353,29 @@ export class RepairService
return value; 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 * Update repair kits Resource object if it doesn't exist
* @param repairKitDetails Repair kit details from db * @param repairKitDetails Repair kit details from db
@ -446,13 +472,15 @@ export class RepairService
const itemSkillType = this.getItemSkillType(template); const itemSkillType = this.getItemSkillType(template);
if (!itemSkillType) if (!itemSkillType)
{
return false; return false;
}
const commonBuffMinChanceValue = globals.config.SkillsSettings[itemSkillType].BuffSettings.CommonBuffMinChanceValue; const commonBuffMinChanceValue = globals.config.SkillsSettings[itemSkillType as string].BuffSettings.CommonBuffMinChanceValue;
const commonBuffChanceLevelBonus = globals.config.SkillsSettings[itemSkillType].BuffSettings.CommonBuffChanceLevelBonus; const commonBuffChanceLevelBonus = globals.config.SkillsSettings[itemSkillType as string].BuffSettings.CommonBuffChanceLevelBonus;
const receivedDurabilityMaxPercent = globals.config.SkillsSettings[itemSkillType].BuffSettings.ReceivedDurabilityMaxPercent; 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 durabilityToRestorePercent = repairDetails.repairPoints / template._props.MaxDurability;
const durabilityMultiplier = this.getDurabilityMultiplier(receivedDurabilityMaxPercent, durabilityToRestorePercent); const durabilityMultiplier = this.getDurabilityMultiplier(receivedDurabilityMaxPercent, durabilityToRestorePercent);
@ -472,26 +500,26 @@ export class RepairService
* @param itemTemplate Item to check for skill * @param itemTemplate Item to check for skill
* @returns Skill name * @returns Skill name
*/ */
protected getItemSkillType(itemTemplate: ITemplateItem): string protected getItemSkillType(itemTemplate: ITemplateItem): SkillTypes
{ {
if (this.itemHelper.isOfBaseclass(itemTemplate._id, BaseClasses.ARMOR)) if (this.itemHelper.isOfBaseclass(itemTemplate._id, BaseClasses.ARMOR))
{ {
if (itemTemplate._props.ArmorType === "Light") if (itemTemplate._props.ArmorType === "Light")
{ {
return "LightVests"; return SkillTypes.LIGHT_VESTS;
} }
else if (itemTemplate._props.ArmorType === "Heavy") else if (itemTemplate._props.ArmorType === "Heavy")
{ {
return "HeavyVests"; return SkillTypes.HEAVY_VESTS;
} }
} }
else if (this.itemHelper.isOfBaseclass(itemTemplate._id, BaseClasses.WEAPON)) else if (this.itemHelper.isOfBaseclass(itemTemplate._id, BaseClasses.WEAPON))
{ {
return "WeaponTreatment"; return SkillTypes.WEAPON_TREATMENT;
} }
else if (this.itemHelper.isOfBaseclass(itemTemplate._id, BaseClasses.KNIFE)) else if (this.itemHelper.isOfBaseclass(itemTemplate._id, BaseClasses.KNIFE))
{ {
return "Melee"; return SkillTypes.MELEE;
} }
return undefined; return undefined;