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

# Conflicts:
#	project/src/controllers/HealthController.ts
This commit is contained in:
Dev 2023-11-18 14:13:08 +00:00
commit 0d205ed50c
7 changed files with 93 additions and 47 deletions

View File

@ -58,7 +58,8 @@
"5a0c27731526d80618476ac4", "5a0c27731526d80618476ac4",
"57864c8c245977548867e7f1", "57864c8c245977548867e7f1",
"6087e570b998180e9f76dc24", "6087e570b998180e9f76dc24",
"6391fcf5744e45201147080f" "6391fcf5744e45201147080f",
"614451b71e5874611e2c7ae5"
] ]
}, },
"pocketLoot": { "pocketLoot": {
@ -158,7 +159,8 @@
"5a0c27731526d80618476ac4", "5a0c27731526d80618476ac4",
"6087e570b998180e9f76dc24", "6087e570b998180e9f76dc24",
"63495c500c297e20065a08b1", "63495c500c297e20065a08b1",
"6391fcf5744e45201147080f" "6391fcf5744e45201147080f",
"614451b71e5874611e2c7ae5"
] ]
}, },
"dynamicLoot": { "dynamicLoot": {

View File

@ -144,12 +144,12 @@
"minPlayerLevel": 5, "minPlayerLevel": 5,
"rewardScaling": { "rewardScaling": {
"levels": [1, 10, 20, 30, 40, 50, 60], "levels": [1, 10, 20, 30, 40, 50, 60],
"experience": [1000, 2000, 8000, 14000, 20000, 24000, 28000], "experience": [1000, 2000, 8000, 13000, 19000, 24000, 30000],
"roubles": [11000, 20000, 45000, 60000, 77000, 95000, 115000], "roubles": [11000, 20000, 32000, 45000, 58000, 70000, 82000],
"items": [2, 4, 5, 5, 5, 5, 5], "items": [2, 4, 5, 5, 5, 5, 5],
"reputation": [0.01, 0.01, 0.02, 0.02, 0.03, 0.03, 0.03], "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], "skillRewardChance": [0, 0.01, 0.05, 0.1, 0.15, 0.2, 0.2],
"skillPointReward": [10, 15, 20, 25, 30, 35, 40] "skillPointReward": [10, 15, 20, 25, 30, 35, 40]
}, },
"locations": { "locations": {
@ -262,7 +262,8 @@
], ],
"questConfig": { "questConfig": {
"Exploration": { "Exploration": {
"maxExtracts": 3, "maxExtracts": 5,
"maxExtractsWithSpecificExit": 3,
"possibleSkillRewards": ["Endurance", "Strength", "Vitality"], "possibleSkillRewards": ["Endurance", "Strength", "Vitality"],
"specificExits": { "specificExits": {
"probability": 0.25, "probability": 0.25,
@ -279,7 +280,7 @@
"Completion": { "Completion": {
"possibleSkillRewards": ["Endurance", "Strength", "Vitality"], "possibleSkillRewards": ["Endurance", "Strength", "Vitality"],
"minRequestedAmount": 1, "minRequestedAmount": 1,
"maxRequestedAmount": 5, "maxRequestedAmount": 4,
"minRequestedBulletAmount": 20, "minRequestedBulletAmount": 20,
"maxRequestedBulletAmount": 60, "maxRequestedBulletAmount": 60,
"useWhitelist": true, "useWhitelist": true,
@ -782,12 +783,12 @@
"minPlayerLevel": 15, "minPlayerLevel": 15,
"rewardScaling": { "rewardScaling": {
"levels": [1, 10, 20, 30, 40, 50, 60], "levels": [1, 10, 20, 30, 40, 50, 60],
"experience": [5000, 25000, 40000, 110000, 220000, 370000, 720000], "experience": [5000, 15000, 27000, 80000, 142000, 220000, 290000],
"roubles": [50000, 150000, 300000, 425000, 550000, 675000, 850000], "roubles": [20000, 50000, 175000, 350000, 540000, 710000, 880000],
"items": [4, 5, 5, 6, 6, 7, 7], "items": [4, 5, 5, 6, 6, 7, 7],
"reputation": [0.02, 0.03, 0.04, 0.04, 0.05, 0.05, 0.05], "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], "skillRewardChance": [0, 0.05, 0.1, 0.15, 0.25, 0.3, 0.3],
"skillPointReward": [25, 35, 45, 50, 55, 60, 65] "skillPointReward": [25, 35, 45, 50, 55, 60, 65]
}, },
"locations": { "locations": {
@ -901,7 +902,8 @@
"questConfig": { "questConfig": {
"Exploration": { "Exploration": {
"possibleSkillRewards": ["Endurance", "Strength", "Vitality"], "possibleSkillRewards": ["Endurance", "Strength", "Vitality"],
"maxExtracts": 10, "maxExtracts": 25,
"maxExtractsWithSpecificExit": 12,
"specificExits": { "specificExits": {
"probability": 0.4, "probability": 0.4,
"passageRequirementWhitelist": [ "passageRequirementWhitelist": [
@ -1486,11 +1488,11 @@
"numQuests": 1, "numQuests": 1,
"minPlayerLevel": 1, "minPlayerLevel": 1,
"rewardScaling": { "rewardScaling": {
"levels": [1, 20, 45, 100], "levels": [1, 10, 20, 30, 40, 50, 60],
"experience": [1000, 4000, 20000, 80000], "experience": [0, 0, 0, 0, 0, 0, 0],
"roubles": [6000, 10000, 100000, 250000], "roubles": [11000, 20000, 32000, 45000, 58000, 70000, 82000],
"items": [3, 3, 4, 4], "items": [2, 3, 3, 3, 3, 4, 4],
"reputation": [0.02, 0.02, 0.05, 0.05], "reputation": [0.02, 0.02, 0.03, 0.03, 0.04, 0.04, 0.05],
"rewardSpread": 0.5, "rewardSpread": 0.5,
"skillRewardChance": [0, 0, 0, 0, 0, 0, 0], "skillRewardChance": [0, 0, 0, 0, 0, 0, 0],
"skillPointReward": [10, 15, 20, 25, 30, 35, 40] "skillPointReward": [10, 15, 20, 25, 30, 35, 40]
@ -1522,7 +1524,8 @@
"questConfig": { "questConfig": {
"Exploration": { "Exploration": {
"possibleSkillRewards": ["Endurance", "Strength", "Vitality"], "possibleSkillRewards": ["Endurance", "Strength", "Vitality"],
"maxExtracts": 3, "maxExtracts": 4,
"maxExtractsWithSpecificExit": 2,
"specificExits": { "specificExits": {
"probability": 0.25, "probability": 0.25,
"passageRequirementWhitelist": [ "passageRequirementWhitelist": [

View File

@ -91,8 +91,8 @@ export class HealthController
else else
{ {
// Get max healing from db // Get max healing from db
const maxHp = this.itemHelper.getItem(healingItemToUse._tpl)[1]._props.MaxHpResource; const maxhp = this.itemHelper.getItem(healingItemToUse._tpl)[1]._props.MaxHpResource;
healingItemToUse.upd.MedKit = { HpResource: maxHp - request.count }; // Subtract amount used from max healingItemToUse.upd.MedKit = { HpResource: maxhp - request.count }; // Subtract amout used from max
} }
// Resource in medkit is spent, delete it // Resource in medkit is spent, delete it
@ -169,10 +169,13 @@ export class HealthController
const payMoneyRequest: IProcessBuyTradeRequestData = { const payMoneyRequest: IProcessBuyTradeRequestData = {
Action: healthTreatmentRequest.Action, Action: healthTreatmentRequest.Action,
tid: Traders.THERAPIST, tid: Traders.THERAPIST,
// eslint-disable-next-line @typescript-eslint/naming-convention
scheme_items: healthTreatmentRequest.items, scheme_items: healthTreatmentRequest.items,
type: "", type: "",
// eslint-disable-next-line @typescript-eslint/naming-convention
item_id: "", item_id: "",
count: 0, count: 0,
// eslint-disable-next-line @typescript-eslint/naming-convention
scheme_id: 0, scheme_id: 0,
}; };
@ -188,8 +191,12 @@ export class HealthController
const partRequest: BodyPart = healthTreatmentRequest.difference.BodyParts[bodyPartKey]; const partRequest: BodyPart = healthTreatmentRequest.difference.BodyParts[bodyPartKey];
const profilePart = pmcData.Health.BodyParts[bodyPartKey]; const profilePart = pmcData.Health.BodyParts[bodyPartKey];
// Set profile body part to max // Bodypart healing is chosen when part request hp is above 0
if (partRequest.Health > 0)
{
// Heal bodypart
profilePart.Health.Current = profilePart.Health.Maximum; profilePart.Health.Current = profilePart.Health.Maximum;
}
// Check for effects to remove // Check for effects to remove
if (partRequest.Effects?.length > 0) if (partRequest.Effects?.length > 0)
@ -220,6 +227,7 @@ export class HealthController
* @param info Request data * @param info Request data
* @param sessionID * @param sessionID
*/ */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public applyWorkoutChanges(pmcData: IPmcData, info: IWorkoutData, sessionId: string): void public applyWorkoutChanges(pmcData: IPmcData, info: IWorkoutData, sessionId: string): void
{ {
// https://dev.sp-tarkov.com/SPT-AKI/Server/issues/2674 // https://dev.sp-tarkov.com/SPT-AKI/Server/issues/2674

View File

@ -654,7 +654,7 @@ export class BotEquipmentModGenerator
modTpl, modTpl,
modSlot, modSlot,
); );
if (!modCompatibilityResult.incompatible) if (!modCompatibilityResult.incompatible && !this.weaponModComboIsIncompatible(weapon, modTpl))
{ {
found = true; found = true;
@ -666,6 +666,7 @@ export class BotEquipmentModGenerator
if (modCompatibilityResult.incompatible && parentSlot._required) if (modCompatibilityResult.incompatible && parentSlot._required)
{ {
this.logger.debug(modCompatibilityResult.reason); this.logger.debug(modCompatibilityResult.reason);
this.logger.debug(`Weapon: ${weapon.map(x => `${x._tpl} ${x.slotId ?? ""}`).join(",")}`)
} }
} }
@ -699,6 +700,23 @@ export class BotEquipmentModGenerator
return this.itemHelper.getItem(modTpl); return this.itemHelper.getItem(modTpl);
} }
/**
* Temp fix to prevent certain combinations of weapons with mods that are known to be incompatible
* @param weapon Weapon
* @param modTpl Mod to check compatibility with weapon
* @returns True if incompatible
*/
protected weaponModComboIsIncompatible(weapon: Item[], modTpl: string): boolean
{
// STM-9 + AR-15 Lone Star Ion Lite handguard
if (weapon[0]._tpl === "60339954d62c9b14ed777c06" && modTpl === "5d4405f0a4b9361e6a4e6bd9")
{
return true;
}
return false;
}
/** /**
* Create a mod item with parameters as properties * Create a mod item with parameters as properties
* @param modId _id * @param modId _id

View File

@ -620,25 +620,26 @@ export class RepeatableQuestGenerator
* A repeatable quest, besides some more or less static components, exists of reward and condition (see assets/database/templates/repeatableQuests.json) * A repeatable quest, besides some more or less static components, exists of reward and condition (see assets/database/templates/repeatableQuests.json)
* This is a helper method for GenerateCompletionQuest to create a completion condition (of which a completion quest theoretically can have many) * This is a helper method for GenerateCompletionQuest to create a completion condition (of which a completion quest theoretically can have many)
* *
* @param {string} targetItemId id of the item to request * @param {string} itemTpl id of the item to request
* @param {integer} value amount of items of this specific type to request * @param {integer} value amount of items of this specific type to request
* @returns {object} object of "Completion"-condition * @returns {object} object of "Completion"-condition
*/ */
protected generateCompletionAvailableForFinish(targetItemId: string, value: number): ICompletionAvailableFor protected generateCompletionAvailableForFinish(itemTpl: string, value: number): ICompletionAvailableFor
{ {
let minDurability = 0; let minDurability = 0;
let onlyFoundInRaid = true; let onlyFoundInRaid = true;
if ( if (
this.itemHelper.isOfBaseclass(targetItemId, BaseClasses.WEAPON) this.itemHelper.isOfBaseclass(itemTpl, BaseClasses.WEAPON)
|| this.itemHelper.isOfBaseclass(targetItemId, BaseClasses.ARMOR) || this.itemHelper.isOfBaseclass(itemTpl, BaseClasses.ARMOR)
) )
{ {
minDurability = 80; minDurability = this.randomUtil.getArrayValue([60, 80]);
} }
// By default all collected items must be FiR, except dog tags
if ( if (
this.itemHelper.isOfBaseclass(targetItemId, BaseClasses.DOG_TAG_USEC) this.itemHelper.isOfBaseclass(itemTpl, BaseClasses.DOG_TAG_USEC)
|| this.itemHelper.isOfBaseclass(targetItemId, BaseClasses.DOG_TAG_BEAR) || this.itemHelper.isOfBaseclass(itemTpl, BaseClasses.DOG_TAG_BEAR)
) )
{ {
onlyFoundInRaid = false; onlyFoundInRaid = false;
@ -651,7 +652,7 @@ export class RepeatableQuestGenerator
dynamicLocale: true, dynamicLocale: true,
index: 0, index: 0,
visibilityConditions: [], visibilityConditions: [],
target: [targetItemId], target: [itemTpl],
value: value, value: value,
minDurability: minDurability, minDurability: minDurability,
maxDurability: 100, maxDurability: 100,
@ -680,6 +681,7 @@ export class RepeatableQuestGenerator
): IExploration ): IExploration
{ {
const explorationConfig = repeatableConfig.questConfig.Exploration; const explorationConfig = repeatableConfig.questConfig.Exploration;
const requiresSpecificExtract = Math.random() < repeatableConfig.questConfig.Exploration.specificExits.probability;
if (Object.keys(questTypePool.pool.Exploration.locations).length === 0) if (Object.keys(questTypePool.pool.Exploration.locations).length === 0)
{ {
@ -696,7 +698,8 @@ export class RepeatableQuestGenerator
// remove the location from the available pool // remove the location from the available pool
delete questTypePool.pool.Exploration.locations[locationKey]; delete questTypePool.pool.Exploration.locations[locationKey];
const numExtracts = this.randomUtil.randInt(1, explorationConfig.maxExtracts + 1); // Different max extract count when specific extract needed
const numExtracts = this.randomUtil.randInt(1, requiresSpecificExtract ? explorationConfig.maxExtractsWithSpecificExit : explorationConfig.maxExtracts + 1);
const quest = this.generateRepeatableTemplate("Exploration", traderId, repeatableConfig.side) as IExploration; const quest = this.generateRepeatableTemplate("Exploration", traderId, repeatableConfig.side) as IExploration;
@ -715,7 +718,8 @@ export class RepeatableQuestGenerator
quest.conditions.AvailableForFinish[0]._props.id = this.objectId.generate(); quest.conditions.AvailableForFinish[0]._props.id = this.objectId.generate();
quest.location = this.getQuestLocationByMapId(locationKey); quest.location = this.getQuestLocationByMapId(locationKey);
if (Math.random() < repeatableConfig.questConfig.Exploration.specificExits.probability)
if (requiresSpecificExtract)
{ {
// Filter by whitelist, it's also possible that the field "PassageRequirement" does not exist (e.g. Shoreline) // Filter by whitelist, it's also possible that the field "PassageRequirement" does not exist (e.g. Shoreline)
// Scav exits are not listed at all in locations.base currently. If that changes at some point, additional filtering will be required // Scav exits are not listed at all in locations.base currently. If that changes at some point, additional filtering will be required
@ -875,27 +879,34 @@ export class RepeatableQuestGenerator
let roublesBudget = rewardRoubles; let roublesBudget = rewardRoubles;
let rewardItemPool = this.chooseRewardItemsWithinBudget(repeatableConfig, roublesBudget, traderId); let rewardItemPool = this.chooseRewardItemsWithinBudget(repeatableConfig, roublesBudget, traderId);
// Add xp reward
const rewards: IRewards = { const rewards: IRewards = {
Started: [], Started: [],
Success: [{ value: rewardXP, type: "Experience", index: 0 }], Success: [],
Fail: [], Fail: [],
}; };
let rewardIndex = 0;
// Add xp reward
if (rewardXP > 0)
{
rewards.Success.push({ value: rewardXP, type: "Experience", index: rewardIndex });
rewardIndex++;
}
// Add money reward // Add money reward
if (traderId === Traders.PEACEKEEPER) if (traderId === Traders.PEACEKEEPER || traderId === Traders.FENCE)
{ {
// convert to equivalent dollars // convert to equivalent dollars
rewards.Success.push( rewards.Success.push(
this.generateRewardItem(Money.EUROS, this.handbookHelper.fromRUB(rewardRoubles, Money.EUROS), 1), this.generateRewardItem(Money.EUROS, this.handbookHelper.fromRUB(rewardRoubles, Money.EUROS), rewardIndex),
); );
} }
else else
{ {
rewards.Success.push(this.generateRewardItem(Money.ROUBLES, rewardRoubles, 1)); rewards.Success.push(this.generateRewardItem(Money.ROUBLES, rewardRoubles, rewardIndex));
} }
rewardIndex++;
let index = 2;
if (rewardItemPool.length > 0) if (rewardItemPool.length > 0)
{ {
let weaponRewardCount = 0; let weaponRewardCount = 0;
@ -946,14 +957,15 @@ export class RepeatableQuestGenerator
itemCount = 2; itemCount = 2;
} }
rewards.Success.push(this.generateRewardItem(itemSelected._id, itemCount, index, children)); rewards.Success.push(this.generateRewardItem(itemSelected._id, itemCount, rewardIndex, children));
rewardIndex++;
const itemCost = (this.itemHelper.isOfBaseclass(itemSelected._id, BaseClasses.WEAPON)) const itemCost = (this.itemHelper.isOfBaseclass(itemSelected._id, BaseClasses.WEAPON))
? this.itemHelper.getItemMaxPrice(children[0]._tpl) // use if preset is not default : this.itemHelper.getWeaponPresetPrice(children[0], children, this.itemHelper.getStaticItemPrice(itemSelected._id)) ? this.itemHelper.getItemMaxPrice(children[0]._tpl) // use if preset is not default : this.itemHelper.getWeaponPresetPrice(children[0], children, this.itemHelper.getStaticItemPrice(itemSelected._id))
: this.itemHelper.getStaticItemPrice(itemSelected._id); : this.itemHelper.getStaticItemPrice(itemSelected._id);
roublesBudget -= itemCount * itemCost; roublesBudget -= itemCount * itemCost;
index += 1;
// if we still have budget narrow down the items // If we still have budget narrow down possible items
if (roublesBudget > 0) if (roublesBudget > 0)
{ {
// Filter possible reward items to only items with a price below the remaining budget // Filter possible reward items to only items with a price below the remaining budget
@ -975,18 +987,19 @@ export class RepeatableQuestGenerator
// Add rep reward to rewards array // Add rep reward to rewards array
if (rewardReputation > 0) if (rewardReputation > 0)
{ {
const reward: IReward = { target: traderId, value: rewardReputation, type: "TraderStanding", index: index }; const reward: IReward = { target: traderId, value: rewardReputation, type: "TraderStanding", index: rewardIndex };
rewards.Success.push(reward); rewards.Success.push(reward);
rewardIndex++;
} }
// Chance of adding skill reward
if (this.randomUtil.getChance100(skillRewardChance * 100)) if (this.randomUtil.getChance100(skillRewardChance * 100))
{ {
index++;
const reward: IReward = { const reward: IReward = {
target: this.randomUtil.getArrayValue(questConfig.possibleSkillRewards), target: this.randomUtil.getArrayValue(questConfig.possibleSkillRewards),
value: skillPointReward, value: skillPointReward,
type: "Skill", type: "Skill",
index: index, index: rewardIndex,
}; };
rewards.Success.push(reward); rewards.Success.push(reward);
} }

View File

@ -89,8 +89,9 @@ export interface IRepeatableQuestTypesConfig
export interface IExploration extends IBaseQuestConfig export interface IExploration extends IBaseQuestConfig
{ {
maxExtracts: number; maxExtracts: number
specificExits: ISpecificExits; maxExtractsWithSpecificExit: number
specificExits: ISpecificExits
} }
export interface ISpecificExits export interface ISpecificExits

View File

@ -184,7 +184,8 @@ export class EventOutputHolder
} }
} }
return productions; // Return null of there's no crafts to send to client to match live behaviour
return (Object.keys(productions).length > 0) ? productions : null
} }
/** /**