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

# Conflicts:
#	project/src/controllers/HideoutController.ts
This commit is contained in:
Dev 2023-12-03 11:13:22 +00:00
commit 1db690afb3
13 changed files with 475 additions and 309 deletions

View File

@ -512,7 +512,53 @@
"RandomTimeSpawn": false, "RandomTimeSpawn": false,
"ChanceGroup": 0 "ChanceGroup": 0
} }
] ],
"rezervbase": [
{
"sptId": "sptBearReserveNormalSpawn",
"BossName": "sptBear",
"BossChance": 15,
"BossZone": "ZoneBarrack,ZonePTOR1,ZonePTOR2,ZoneSubCommand,ZoneSubStorage,ZoneRailStrorage",
"BossPlayer": false,
"BossDifficult": "normal",
"BossEscortType": "sptBear",
"BossEscortDifficult": "normal",
"BossEscortAmount": "1",
"Time": -1,
"Supports": [{
"BossEscortType": "sptBear",
"BossEscortDifficult": [
"normal"
],
"BossEscortAmount": "1"
}
],
"RandomTimeSpawn": false,
"ChanceGroup": 0
},
{
"sptId": "sptUsecReserveNormalSpawn",
"BossName": "sptUsec",
"BossChance": 15,
"BossZone": "ZoneBarrack,ZonePTOR1,ZonePTOR2,ZoneSubCommand,ZoneSubStorage,ZoneRailStrorage",
"BossPlayer": false,
"BossDifficult": "normal",
"BossEscortType": "sptUsec",
"BossEscortDifficult": "normal",
"BossEscortAmount": "1",
"Time": -1,
"Supports": [{
"BossEscortType": "sptUsec",
"BossEscortDifficult": [
"normal"
],
"BossEscortAmount": "1"
}
],
"RandomTimeSpawn": false,
"ChanceGroup": 0
}
]
}, },
"normal": { "normal": {
"factory4_day": [{ "factory4_day": [{
@ -795,190 +841,194 @@
"allowDuplicateItemsInStaticContainers": true, "allowDuplicateItemsInStaticContainers": true,
"looseLootBlacklist": {}, "looseLootBlacklist": {},
"scavRaidTimeSettings": { "scavRaidTimeSettings": {
"bigmap": { "settings": {
"reduceLootByPercent": true, "trainArrivalDelayObservedSeconds": 90
"minDynamicLootPercent": 50,
"minStaticLootPercent": 40,
"reducedChancePercent": 70,
"reductionPercentWeights": {
"20": 1,
"25": 2,
"30": 4,
"35": 4,
"40": 4,
"45": 4,
"50": 4,
"60": 2,
"70": 2,
"80": 1
},
"adjustWaves": true
}, },
"factory4_day": { "maps": {
"reduceLootByPercent": true, "bigmap": {
"minDynamicLootPercent": 50, "reduceLootByPercent": true,
"minStaticLootPercent": 40, "minDynamicLootPercent": 50,
"reducedChancePercent": 80, "minStaticLootPercent": 40,
"reductionPercentWeights": { "reducedChancePercent": 95,
"5": 2, "reductionPercentWeights": {
"20": 3, "20": 1,
"25": 3, "25": 2,
"30": 5, "30": 4,
"40": 5, "35": 4,
"50": 5, "40": 4,
"60": 2, "45": 4,
"70": 2, "50": 4,
"80": 2, "60": 2,
"85": 1 "70": 2,
"80": 1
},
"adjustWaves": true
}, },
"adjustWaves": true "factory4_day": {
}, "reduceLootByPercent": true,
"factory4_night": { "minDynamicLootPercent": 50,
"reduceLootByPercent": true, "minStaticLootPercent": 40,
"minDynamicLootPercent": 50, "reducedChancePercent": 95,
"minStaticLootPercent": 40, "reductionPercentWeights": {
"reducedChancePercent": 70, "5": 2,
"reductionPercentWeights": { "20": 3,
"20": 4, "25": 3,
"30": 3, "30": 5,
"40": 3, "40": 5,
"60": 2, "50": 5,
"70": 2, "60": 2,
"80": 1 "70": 2,
"75": 2
},
"adjustWaves": true
}, },
"adjustWaves": true "factory4_night": {
}, "reduceLootByPercent": true,
"interchange": { "minDynamicLootPercent": 50,
"reduceLootByPercent": true, "minStaticLootPercent": 40,
"minDynamicLootPercent": 50, "reducedChancePercent": 75,
"minStaticLootPercent": 40, "reductionPercentWeights": {
"reducedChancePercent": 70, "20": 4,
"reductionPercentWeights": { "30": 3,
"20": 5, "40": 3,
"25": 5, "60": 2,
"30": 5, "70": 2,
"35": 5, "75": 1
"40": 5, },
"50": 5, "adjustWaves": true
"60": 2,
"80": 1
}, },
"adjustWaves": true "interchange": {
}, "reduceLootByPercent": true,
"rezervbase": { "minDynamicLootPercent": 50,
"reduceLootByPercent": true, "minStaticLootPercent": 40,
"minDynamicLootPercent": 50, "reducedChancePercent": 95,
"minStaticLootPercent": 40, "reductionPercentWeights": {
"reducedChancePercent": 70, "20": 5,
"25": 5,
"30": 5,
"35": 5,
"40": 5,
"50": 5,
"60": 2,
"80": 1
},
"adjustWaves": true
},
"rezervbase": {
"reduceLootByPercent": true,
"minDynamicLootPercent": 50,
"minStaticLootPercent": 40,
"reducedChancePercent": 95,
"reductionPercentWeights": { "reductionPercentWeights": {
"20": 3, "20": 3,
"30": 3, "30": 3,
"40": 4, "40": 4,
"50": 4, "50": 4,
"60": 2, "60": 2,
"70": 1, "70": 1,
"80": 1 "80": 1
},
"adjustWaves": true
}, },
"adjustWaves": true "laboratory": {
}, "reduceLootByPercent": true,
"laboratory": { "minDynamicLootPercent": 50,
"reduceLootByPercent": true, "minStaticLootPercent": 40,
"minDynamicLootPercent": 50, "reducedChancePercent": 95,
"minStaticLootPercent": 40, "reductionPercentWeights": {
"reducedChancePercent": 70, "20": 3,
"reductionPercentWeights": { "30": 5,
"20": 3, "40": 5,
"30": 5, "50": 5,
"40": 5, "60": 2,
"50": 5, "80": 1
"60": 2, },
"80": 1 "adjustWaves": true
}, },
"adjustWaves": true "lighthouse": {
}, "reduceLootByPercent": true,
"lighthouse": { "minDynamicLootPercent": 50,
"reduceLootByPercent": true, "minStaticLootPercent": 40,
"minDynamicLootPercent": 50, "reducedChancePercent": 95,
"minStaticLootPercent": 40, "reductionPercentWeights": {
"reducedChancePercent": 60, "20": 2,
"reductionPercentWeights": { "25": 2,
"20": 2, "30": 4,
"25": 2, "40": 4,
"30": 4, "50": 4,
"40": 4, "60": 2,
"50": 4, "80": 1
"60": 2, },
"80": 1 "adjustWaves": true
}, },
"adjustWaves": true "shoreline": {
}, "reduceLootByPercent": true,
"shoreline": { "minDynamicLootPercent": 50,
"reduceLootByPercent": true, "minStaticLootPercent": 40,
"minDynamicLootPercent": 50, "reducedChancePercent": 95,
"minStaticLootPercent": 40, "reductionPercentWeights": {
"reducedChancePercent": 60, "20": 2,
"reductionPercentWeights": { "25": 3,
"20": 2, "30": 5,
"25": 3, "35": 5,
"30": 5, "40": 5,
"35": 5, "50": 5,
"40": 5, "60": 2,
"50": 5, "70": 1,
"60": 2, "80": 1
"70": 1, },
"80": 1 "adjustWaves": true
}, },
"adjustWaves": true "tarkovstreets": {
}, "reduceLootByPercent": true,
"tarkovstreets": { "minDynamicLootPercent": 50,
"reduceLootByPercent": true, "minStaticLootPercent": 40,
"minDynamicLootPercent": 50, "reducedChancePercent": 95,
"minStaticLootPercent": 40, "reductionPercentWeights": {
"reducedChancePercent": 70, "20": 2,
"reductionPercentWeights": { "30": 4,
"20": 2, "40": 4,
"30": 4, "50": 4,
"40": 4, "60": 4,
"50": 4, "70": 1,
"60": 4, "80": 1
"70": 1, },
"80": 1 "adjustWaves": true
}, },
"adjustWaves": true "woods": {
}, "reduceLootByPercent": true,
"woods": { "minDynamicLootPercent": 50,
"reduceLootByPercent": true, "minStaticLootPercent": 40,
"minDynamicLootPercent": 50, "reducedChancePercent": 95,
"minStaticLootPercent": 40, "reductionPercentWeights": {
"reducedChancePercent": 60, "20": 3,
"reductionPercentWeights": { "30": 5,
"20": 3, "40": 5,
"30": 5, "50": 5,
"40": 5, "60": 1,
"50": 5, "70": 1,
"60": 1, "80": 1
"70": 1, },
"80": 1 "adjustWaves": true
}, },
"adjustWaves": true "default": {
}, "reduceLootByPercent": true,
"default": { "minDynamicLootPercent": 50,
"reduceLootByPercent": true, "minStaticLootPercent": 50,
"minDynamicLootPercent": 50, "reducedChancePercent": 95,
"minStaticLootPercent": 50, "reductionPercentWeights": {
"reducedChancePercent": 50, "10": 1,
"reductionPercentWeights": { "20": 2,
"10": 1, "30": 5,
"20": 2, "40": 5,
"30": 5, "50": 5,
"40": 5, "60": 2,
"50": 5, "70": 1,
"60": 2, "80": 1
"70": 1, },
"80": 1 "adjustWaves": true
}, }
"adjustWaves": true
} }
} }
} }

View File

@ -1,33 +1,33 @@
{ {
"fail": {},
"started": {}, "started": {},
"success": { "success": {
"6507ff21644a656aee0f758f": "596b455186f77457cb50eccb", "6492e44bf4287b13040fcbae": "6179b5eabca27a099552e052",
"6507ff21644a656aee0f74b7": "5b4794cb86f774598100d5d4", "6492e44bf4287b13040fccf6": "647710905320c660d91c15a5",
"6507ff20644a656aee0f744d": "5c0bbaa886f7746941031d82", "64a8578f0e9876295f0f83ed": "649af47d717cb30e7e4b5e26",
"6507ff22644a656aee0f75d3": "5c0bbaa886f7746941031d82", "64a8578f0e9876295f0f83ee": "649af47d717cb30e7e4b5e26",
"6507ff20644a656aee0f73e4": "596b36c586f77450d6045ad2", "64a8578f0e9876295f0f83ef": "649af47d717cb30e7e4b5e26",
"6507ff22644a656aee0f76e1": "596b36c586f77450d6045ad2",
"6507ff20644a656aee0f743f": "5979eee086f774311955e614",
"6507ff22644a656aee0f75e5": "5b478ff486f7744d184ecbbf",
"6507ff21644a656aee0f75a8": "5a27d2af86f7744e1115b323",
"6507ff21644a656aee0f7481": "5c0bdb5286f774166e38eed4",
"6507ff20644a656aee0f744b": "5c1234c286f77406fa13baeb",
"6507ff20644a656aee0f7465": "596b43fb86f77457ca186186",
"6507ff20644a656aee0f7447": "5a27b87686f77460de0252a8",
"6507ff20644a656aee0f73d6": "5967725e86f774601a446662",
"652376e2f6c67195e4061382": "6179b5eabca27a099552e052",
"6507ff20644a656aee0f73bc": "6193850f60b34236ee0483de",
"6507ff20644a656aee0f738e": "6179b4f16e9dd54ac275e407", "6507ff20644a656aee0f738e": "6179b4f16e9dd54ac275e407",
"6507ff20644a656aee0f73bc": "6193850f60b34236ee0483de",
"6507ff20644a656aee0f73d4": "6179b4f16e9dd54ac275e407", "6507ff20644a656aee0f73d4": "6179b4f16e9dd54ac275e407",
"6507ff20644a656aee0f73d6": "5967725e86f774601a446662",
"6507ff20644a656aee0f73e4": "596b36c586f77450d6045ad2",
"6507ff20644a656aee0f743f": "5979eee086f774311955e614",
"6507ff20644a656aee0f7447": "5a27b87686f77460de0252a8",
"6507ff20644a656aee0f744b": "5c1234c286f77406fa13baeb",
"6507ff20644a656aee0f744d": "5c0bbaa886f7746941031d82",
"6507ff20644a656aee0f7465": "596b43fb86f77457ca186186",
"6507ff21644a656aee0f7481": "5c0bdb5286f774166e38eed4",
"6507ff21644a656aee0f7498": "6179b4f16e9dd54ac275e407", "6507ff21644a656aee0f7498": "6179b4f16e9dd54ac275e407",
"6507ff21644a656aee0f74a6": "5ac242ab86f77412464f68b4", "6507ff21644a656aee0f74a6": "5ac242ab86f77412464f68b4",
"6507ff21644a656aee0f757d": "639873003693c63d86328f25", "6507ff21644a656aee0f74b7": "5b4794cb86f774598100d5d4",
"6507ff22644a656aee0f7624": "64f5e20652fc01298e2c61e3",
"6507ff21644a656aee0f751c": "64f6aafd67e11a7c6206e0d0", "6507ff21644a656aee0f751c": "64f6aafd67e11a7c6206e0d0",
"64cac5c1d45ace5bc90c74a8": "649af47d717cb30e7e4b5e26", "6507ff21644a656aee0f757d": "639873003693c63d86328f25",
"64cac5c1d45ace5bc90c74a9": "649af47d717cb30e7e4b5e26", "6507ff21644a656aee0f758f": "596b455186f77457cb50eccb",
"64cac5c1d45ace5bc90c74aa": "649af47d717cb30e7e4b5e26", "6507ff21644a656aee0f75a8": "5a27d2af86f7744e1115b323",
"64cac5c1d45ace5bc90c74ab": "647710905320c660d91c15a5" "6507ff22644a656aee0f75d3": "5c0bbaa886f7746941031d82",
}, "6507ff22644a656aee0f75e5": "5b478ff486f7744d184ecbbf",
"fail": {} "6507ff22644a656aee0f7624": "64f5e20652fc01298e2c61e3",
"6507ff22644a656aee0f76e1": "596b36c586f77450d6045ad2"
}
} }

View File

@ -320,6 +320,7 @@ export class HideoutController
{ {
// Update existing items container tpl to point to new id (tpl) // Update existing items container tpl to point to new id (tpl)
existingInventoryItem._tpl = hideoutStage.container; existingInventoryItem._tpl = hideoutStage.container;
return; return;
} }
@ -357,7 +358,7 @@ export class HideoutController
* Handle HideoutPutItemsInAreaSlots * Handle HideoutPutItemsInAreaSlots
* Create item in hideout slot item array, remove item from player inventory * Create item in hideout slot item array, remove item from player inventory
* @param pmcData Profile data * @param pmcData Profile data
* @param addItemToHideoutRequest request from client to place item in area slot * @param addItemToHideoutRequest reqeust from client to place item in area slot
* @param sessionID Session id * @param sessionID Session id
* @returns IItemEventRouterResponse object * @returns IItemEventRouterResponse object
*/ */
@ -490,7 +491,14 @@ export class HideoutController
const itemToReturn = hideoutArea.slots.find((x) => x.locationIndex === slotIndexToRemove).item[0]; const itemToReturn = hideoutArea.slots.find((x) => x.locationIndex === slotIndexToRemove).item[0];
const newReq = { items: [{ item_id: itemToReturn._tpl, count: 1 }], tid: "ragfair" }; const newReq = {
items: [{
// eslint-disable-next-line @typescript-eslint/naming-convention
item_id: itemToReturn._tpl,
count: 1,
}],
tid: "ragfair",
};
output = this.inventoryHelper.addItem( output = this.inventoryHelper.addItem(
pmcData, pmcData,
@ -550,7 +558,7 @@ export class HideoutController
* Handle HideoutSingleProductionStart event * Handle HideoutSingleProductionStart event
* Start production for an item from hideout area * Start production for an item from hideout area
* @param pmcData Player profile * @param pmcData Player profile
* @param body Start production of single item request * @param body Start prodution of single item request
* @param sessionID Session id * @param sessionID Session id
* @returns IItemEventRouterResponse * @returns IItemEventRouterResponse
*/ */
@ -665,6 +673,7 @@ export class HideoutController
{ {
return productionTime; return productionTime;
} }
return productionTime * fenceLevel.ScavCaseTimeModifier; return productionTime * fenceLevel.ScavCaseTimeModifier;
} }
@ -682,7 +691,7 @@ export class HideoutController
/** /**
* Start production of continuously created item * Start production of continuously created item
* @param pmcData Player profile * @param pmcData Player profile
* @param request Continuous production request * @param request Continious production request
* @param sessionID Session id * @param sessionID Session id
* @returns IItemEventRouterResponse * @returns IItemEventRouterResponse
*/ */
@ -693,6 +702,7 @@ export class HideoutController
): IItemEventRouterResponse ): IItemEventRouterResponse
{ {
this.registerProduction(pmcData, request, sessionID); this.registerProduction(pmcData, request, sessionID);
return this.eventOutputHolder.getOutput(sessionID); return this.eventOutputHolder.getOutput(sessionID);
} }
@ -757,7 +767,7 @@ export class HideoutController
output: IItemEventRouterResponse, output: IItemEventRouterResponse,
): IItemEventRouterResponse ): IItemEventRouterResponse
{ {
// Variables for management of skill // Variables for managemnet of skill
let craftingExpAmount = 0; let craftingExpAmount = 0;
// ? move the logic of BackendCounters in new method? // ? move the logic of BackendCounters in new method?
@ -781,7 +791,14 @@ export class HideoutController
id = this.presetHelper.getDefaultPreset(id)._id; id = this.presetHelper.getDefaultPreset(id)._id;
} }
const newReq = { items: [{ item_id: id, count: recipe.count }], tid: "ragfair" }; const newReq = {
items: [{
// eslint-disable-next-line @typescript-eslint/naming-convention
item_id: id,
count: recipe.count,
}],
tid: "ragfair",
};
const entries = Object.entries(pmcData.Hideout.Production); const entries = Object.entries(pmcData.Hideout.Production);
let prodId: string; let prodId: string;
@ -901,7 +918,6 @@ export class HideoutController
let prodId: string; let prodId: string;
for (const production of ongoingProductions) for (const production of ongoingProductions)
{ {
// Production or ScavCase
if (this.hideoutHelper.isProductionType(production[1])) if (this.hideoutHelper.isProductionType(production[1]))
{ // Production or ScavCase { // Production or ScavCase
if ((production[1] as ScavCase).RecipeId === request.recipeId) if ((production[1] as ScavCase).RecipeId === request.recipeId)
@ -927,34 +943,38 @@ export class HideoutController
// Create rewards for scav case // Create rewards for scav case
const scavCaseRewards = this.scavCaseRewardGenerator.generate(request.recipeId); const scavCaseRewards = this.scavCaseRewardGenerator.generate(request.recipeId);
// Add scav case rewards to player profile
pmcData.Hideout.Production[prodId].Products = scavCaseRewards; pmcData.Hideout.Production[prodId].Products = scavCaseRewards;
// Remove the old production from output object before its sent to client // Remove the old production from output object before its sent to client
delete output.profileChanges[sessionID].production[request.recipeId]; delete output.profileChanges[sessionID].production[request.recipeId];
// Get array of item created + count of them after completing hideout craft
const itemsToAdd = pmcData.Hideout.Production[prodId].Products.map( const itemsToAdd = pmcData.Hideout.Production[prodId].Products.map(
(x: { _tpl: string; upd?: { StackObjectsCount?: number; }; }) => (x: { _tpl: string; upd?: { StackObjectsCount?: number; }; }) =>
{ {
let id = x._tpl; const itemTpl = this.presetHelper.hasPreset(x._tpl)
if (this.presetHelper.hasPreset(id)) ? this.presetHelper.getDefaultPreset(x._tpl)._id
{ : x._tpl;
id = this.presetHelper.getDefaultPreset(id)._id;
} // Count of items crafted
const numOfItems = !x.upd?.StackObjectsCount ? 1 : x.upd.StackObjectsCount; const numOfItems = !x.upd?.StackObjectsCount ? 1 : x.upd.StackObjectsCount;
return { item_id: id, count: numOfItems }; // eslint-disable-next-line @typescript-eslint/naming-convention
return { item_id: itemTpl, count: numOfItems };
}, },
); );
const newReq = { items: itemsToAdd, tid: "ragfair" }; const newReq = { items: itemsToAdd, tid: "ragfair" };
const callback = () => const callback = () =>
{ {
// Flag as complete - will be cleaned up later by update() process // Flag as complete - will be cleaned up later by hideoutController.update()
pmcData.Hideout.Production[prodId].sptIsComplete = true; pmcData.Hideout.Production[prodId].sptIsComplete = true;
// Crafting complete, flag as such
pmcData.Hideout.Production[prodId].inProgress = false; pmcData.Hideout.Production[prodId].inProgress = false;
}; };
// Add crafted item to player inventory
return this.inventoryHelper.addItem(pmcData, newReq, output, sessionID, callback, true); return this.inventoryHelper.addItem(pmcData, newReq, output, sessionID, callback, true);
} }
@ -976,10 +996,11 @@ export class HideoutController
/** /**
* Get quick time event list for hideout * Get quick time event list for hideout
* // TODO: Implement this * // TODO - implement this
* @param sessionId Session id * @param sessionId Session id
* @returns IQteData array * @returns IQteData array
*/ */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public getQteList(sessionId: string): IQteData[] public getQteList(sessionId: string): IQteData[]
{ {
return this.databaseServer.getTables().hideout.qte; return this.databaseServer.getTables().hideout.qte;
@ -992,6 +1013,7 @@ export class HideoutController
* @param pmcData Profile to adjust * @param pmcData Profile to adjust
* @param request QTE result object * @param request QTE result object
*/ */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public handleQTEEventOutcome( public handleQTEEventOutcome(
sessionId: string, sessionId: string,
pmcData: IPmcData, pmcData: IPmcData,
@ -999,10 +1021,10 @@ export class HideoutController
): IItemEventRouterResponse ): IItemEventRouterResponse
{ {
// { // {
// Action: "HideoutQuickTimeEvent", // "Action": "HideoutQuickTimeEvent",
// results: [true, false, true, true, true, true, true, true, true, false, false, false, false, false, false], // "results": [true, false, true, true, true, true, true, true, true, false, false, false, false, false, false],
// id: "63b16feb5d012c402c01f6ef", // "id": "63b16feb5d012c402c01f6ef",
// timestamp: 1672585349 // "timestamp": 1672585349
// } // }
// Skill changes are done in // Skill changes are done in
@ -1036,7 +1058,7 @@ export class HideoutController
request: IRecordShootingRangePoints, request: IRecordShootingRangePoints,
): IItemEventRouterResponse ): IItemEventRouterResponse
{ {
// Check if counter exists, add placeholder if it doesn't // Check if counter exists, add placeholder if it doesnt
if (!pmcData.Stats.Eft.OverallCounters.Items.find((x) => x.Key.includes("ShootingRangePoints"))) if (!pmcData.Stats.Eft.OverallCounters.Items.find((x) => x.Key.includes("ShootingRangePoints")))
{ {
pmcData.Stats.Eft.OverallCounters.Items.push({ Key: ["ShootingRangePoints"], Value: 0 }); pmcData.Stats.Eft.OverallCounters.Items.push({ Key: ["ShootingRangePoints"], Value: 0 });
@ -1048,7 +1070,7 @@ export class HideoutController
); );
shootingRangeHighScore.Value = request.points; shootingRangeHighScore.Value = request.points;
// Check against live, maybe a response isn't necessary // Check against live, maybe a response isnt necessary
return this.eventOutputHolder.getOutput(sessionId); return this.eventOutputHolder.getOutput(sessionId);
} }
@ -1066,7 +1088,7 @@ export class HideoutController
{ {
const output = this.eventOutputHolder.getOutput(sessionId); const output = this.eventOutputHolder.getOutput(sessionId);
// Create mapping of required item with corresponding item from player inventory // Create mapping of required item with corrisponding item from player inventory
const items = request.items.map((reqItem) => const items = request.items.map((reqItem) =>
{ {
const item = pmcData.Inventory.items.find((invItem) => invItem._id === reqItem.id); const item = pmcData.Inventory.items.find((invItem) => invItem._id === reqItem.id);
@ -1115,7 +1137,7 @@ export class HideoutController
return this.httpResponse.appendErrorToOutput(output); return this.httpResponse.appendErrorToOutput(output);
} }
// Add all improvements to output object // Add all improvemets to output object
const improvements = hideoutDbData.stages[profileHideoutArea.level].improvements; const improvements = hideoutDbData.stages[profileHideoutArea.level].improvements;
const timestamp = this.timeUtil.getTimestamp(); const timestamp = this.timeUtil.getTimestamp();
@ -1164,7 +1186,7 @@ export class HideoutController
// Null out production data so client gets informed when response send back // Null out production data so client gets informed when response send back
pmcData.Hideout.Production[request.recipeId] = null; pmcData.Hideout.Production[request.recipeId] = null;
// TODO: handle timestamp somehow? // TODO - handle timestamp somehow?
return output; return output;
} }

View File

@ -107,17 +107,17 @@ export class InraidController
*/ */
protected savePmcProgress(sessionID: string, postRaidRequest: ISaveProgressRequestData): void protected savePmcProgress(sessionID: string, postRaidRequest: ISaveProgressRequestData): void
{ {
const serveProfile = this.saveServer.getProfile(sessionID); const serverProfile = this.saveServer.getProfile(sessionID);
const locationName = serveProfile.inraid.location.toLowerCase(); const locationName = serverProfile.inraid.location.toLowerCase();
const map: ILocationBase = this.databaseServer.getTables().locations[locationName].base; const map: ILocationBase = this.databaseServer.getTables().locations[locationName].base;
const mapHasInsuranceEnabled = map.Insurance; const mapHasInsuranceEnabled = map.Insurance;
let serverPmcProfile = serveProfile.characters.pmc; let serverPmcProfile = serverProfile.characters.pmc;
const isDead = this.isPlayerDead(postRaidRequest.exit); const isDead = this.isPlayerDead(postRaidRequest.exit);
const preRaidGear = this.inRaidHelper.getPlayerGear(serverPmcProfile.Inventory.items); const preRaidGear = this.inRaidHelper.getPlayerGear(serverPmcProfile.Inventory.items);
serveProfile.inraid.character = "pmc"; serverProfile.inraid.character = "pmc";
this.inRaidHelper.updateProfileBaseStats(serverPmcProfile, postRaidRequest, sessionID); this.inRaidHelper.updateProfileBaseStats(serverPmcProfile, postRaidRequest, sessionID);
this.inRaidHelper.updatePmcProfileDataPostRaid(serverPmcProfile, postRaidRequest, sessionID); this.inRaidHelper.updatePmcProfileDataPostRaid(serverPmcProfile, postRaidRequest, sessionID);
@ -220,7 +220,7 @@ export class InraidController
activeQuestIdsInProfile, activeQuestIdsInProfile,
allQuests, allQuests,
); );
if (questAndFindItemConditionId) if (Object.keys(questAndFindItemConditionId)?.length > 0)
{ {
this.profileHelper.removeCompletedQuestConditionFromProfile(pmcData, questAndFindItemConditionId); this.profileHelper.removeCompletedQuestConditionFromProfile(pmcData, questAndFindItemConditionId);
} }

View File

@ -216,7 +216,7 @@ export class BotWeaponGeneratorHelper
// Iterate over each grid in the container and look for a big enough space for the item to be placed in // Iterate over each grid in the container and look for a big enough space for the item to be placed in
let currentGridCount = 1; let currentGridCount = 1;
const slotGridCount = containerTemplate[1]._props.Grids.length; const totalSlotGridCount = containerTemplate[1]._props.Grids.length;
for (const slotGrid of containerTemplate[1]._props.Grids) for (const slotGrid of containerTemplate[1]._props.Grids)
{ {
// Grid is empty, skip // Grid is empty, skip
@ -280,7 +280,7 @@ export class BotWeaponGeneratorHelper
} }
// If we've checked all grids in container and reached this point, there's no space for item // If we've checked all grids in container and reached this point, there's no space for item
if (slotGridCount >= currentGridCount) if (currentGridCount >= totalSlotGridCount)
{ {
return ItemAddedResult.NO_SPACE; return ItemAddedResult.NO_SPACE;
} }

View File

@ -218,7 +218,7 @@ export class InRaidHelper
public updatePmcProfileDataPostRaid(pmcData: IPmcData, saveProgressRequest: ISaveProgressRequestData, sessionId: string): void public updatePmcProfileDataPostRaid(pmcData: IPmcData, saveProgressRequest: ISaveProgressRequestData, sessionId: string): void
{ {
// Process failed quests then copy everything // Process failed quests then copy everything
this.processFailedQuests(sessionId, pmcData, pmcData.Quests, saveProgressRequest.profile.Quests); this.processFailedQuests(sessionId, pmcData, pmcData.Quests, saveProgressRequest.profile);
pmcData.Quests = saveProgressRequest.profile.Quests; pmcData.Quests = saveProgressRequest.profile.Quests;
// No need to do this for scav, old scav is deleted and new one generated // No need to do this for scav, old scav is deleted and new one generated
@ -254,13 +254,13 @@ export class InRaidHelper
* @param sessionId Player id * @param sessionId Player id
* @param pmcData Player profile * @param pmcData Player profile
* @param preRaidQuests Quests prior to starting raid * @param preRaidQuests Quests prior to starting raid
* @param postRaidQuests Quest after raid * @param postRaidProfile Profile sent by client
*/ */
protected processFailedQuests( protected processFailedQuests(
sessionId: string, sessionId: string,
pmcData: IPmcData, pmcData: IPmcData,
preRaidQuests: IQuestStatus[], preRaidQuests: IQuestStatus[],
postRaidQuests: IQuestStatus[], postRaidProfile: IPostRaidPmcData,
): void ): void
{ {
if (!preRaidQuests) if (!preRaidQuests)
@ -270,24 +270,81 @@ export class InRaidHelper
} }
// Loop over all quests from post-raid profile // Loop over all quests from post-raid profile
for (const postRaidQuest of postRaidQuests) for (const postRaidQuest of postRaidProfile.Quests)
{ {
// Find matching pre-raid quest // Find matching pre-raid quest + not already failed
const preRaidQuest = preRaidQuests?.find((x) => x.qid === postRaidQuest.qid); const preRaidQuest = preRaidQuests?.find((x) => x.qid === postRaidQuest.qid);
if (preRaidQuest) if (!preRaidQuest)
{ {
// Post-raid quest is failed but wasn't pre-raid continue;
// postRaidQuest.status has a weird value, need to do some nasty casting to compare it }
if (<string><unknown>postRaidQuest.status === "Fail" && preRaidQuest.status !== QuestStatus.Fail)
// Already failed before raid, skip
if (preRaidQuest.status === QuestStatus.Fail)
{
continue;
}
if (preRaidQuest.status === QuestStatus.Success)
{
continue;
}
// Quest failed inside raid, need to handle
// postRaidQuest.status has a weird value, need to do some nasty casting to compare it
const postRaidQuestStatus = <string><unknown>postRaidQuest.status;
if (postRaidQuestStatus === "Fail")
{
// Send failed message
const failBody: IFailQuestRequestData = {
Action: "QuestComplete",
qid: postRaidQuest.qid,
removeExcessItems: true,
};
this.questHelper.failQuest(pmcData, failBody, sessionId);
}
// Restartable quests need special actions
else if (postRaidQuestStatus === "FailRestartable")
{
// Does failed quest have requirement to collect items from raid
const questDbData = this.questHelper.getQuestFromDb(postRaidQuest.qid, pmcData);
// AvailableForFinish
const matchingAffFindConditions = questDbData.conditions.AvailableForFinish.filter(x => x._parent === "FindItem");
const itemsToCollect: string[] = [];
if (matchingAffFindConditions)
{ {
// Send failed message // Find all items the failed quest wanted
const failBody: IFailQuestRequestData = { for (const condition of matchingAffFindConditions)
Action: "QuestComplete", {
qid: postRaidQuest.qid, itemsToCollect.push(...condition._props.target);
removeExcessItems: true, }
};
this.questHelper.failQuest(pmcData, failBody, sessionId);
} }
// Remove quest items from profile as quest has failed and may still be alive
// Required as restarting the quest from main menu does not remove value from CarriedQuestItems array
postRaidProfile.Stats.Eft.CarriedQuestItems = postRaidProfile.Stats.Eft.CarriedQuestItems.filter(x => !itemsToCollect.includes(x))
// Remove quest item from profile now quest is failed
// updateProfileBaseStats() has already passed by ref EFT.Stats, all changes applied to postRaid profile also apply to server profile
for (const itemTpl of itemsToCollect)
{
// Look for sessioncounter and remove it
const counterIndex = postRaidProfile.Stats.Eft.SessionCounters.Items.findIndex(x => x.Key.includes(itemTpl) && x.Key.includes("LootItem"));
if (counterIndex > -1)
{
postRaidProfile.Stats.Eft.SessionCounters.Items.splice(counterIndex, 1);
}
// Look for quest item and remove it
const inventoryItemIndex = postRaidProfile.Inventory.items.findIndex(x => x._tpl === itemTpl);
if (inventoryItemIndex > -1)
{
postRaidProfile.Inventory.items.splice(inventoryItemIndex, 1);
}
}
// Clear out any completed conditions
postRaidQuest.completedConditions = [];
} }
} }
} }

View File

@ -951,7 +951,7 @@ export class QuestHelper
const questInDb = allQuests.find((x) => x._id === questId); const questInDb = allQuests.find((x) => x._id === questId);
if (!questInDb) if (!questInDb)
{ {
this.logger.warning( this.logger.debug(
`Unable to find quest: ${questId} in db, cannot get 'FindItem' condition, skipping`, `Unable to find quest: ${questId} in db, cannot get 'FindItem' condition, skipping`,
); );
continue; continue;

View File

@ -22,6 +22,7 @@ export class WeightedRandomHelper
{ {
const itemKeys = Object.keys(itemArray); const itemKeys = Object.keys(itemArray);
const weights = Object.values(itemArray); const weights = Object.values(itemArray);
const chosenItem = this.weightedRandom(itemKeys, weights); const chosenItem = this.weightedRandom(itemKeys, weights);
return chosenItem.item; return chosenItem.item;
@ -41,18 +42,23 @@ export class WeightedRandomHelper
* @param {number[]} weights * @param {number[]} weights
* @returns {{item: any, index: number}} * @returns {{item: any, index: number}}
*/ */
public weightedRandom(items: string | any[], weights: string | any[]): { item: any; index: number; } public weightedRandom(items: any[], weights: any[]): { item: any; index: number; }
{ {
if (!items || items.length === 0)
{
throw new Error("Items must not be empty");
}
if (!weights || weights.length === 0)
{
throw new Error("Item weights must not be empty");
}
if (items.length !== weights.length) if (items.length !== weights.length)
{ {
throw new Error("Items and weight inputs must be of the same length"); throw new Error("Items and weight inputs must be of the same length");
} }
if (!items.length)
{
throw new Error("Items must not be empty");
}
// Preparing the cumulative weights array. // Preparing the cumulative weights array.
// For example: // For example:
// - weights = [1, 4, 3] // - weights = [1, 4, 3]

View File

@ -468,7 +468,7 @@ export interface IQuestStatus
startTime: number; startTime: number;
status: QuestStatus; status: QuestStatus;
statusTimers?: Record<string, number>; statusTimers?: Record<string, number>;
/** SPT specific property */ /** Property does not exist in live profile data, but is used by ProfileChanges.questsStatus when sent to client*/
completedConditions?: string[]; completedConditions?: string[];
availableAfter?: number; availableAfter?: number;
} }
@ -483,16 +483,6 @@ export interface TraderInfo
disabled: boolean; disabled: boolean;
} }
/** This object is sent to the client as part of traderRelations */
export interface TraderData
{
salesSum: number;
standing: number;
loyalty: number;
unlocked: boolean;
disabled: boolean;
}
export interface RagfairInfo export interface RagfairInfo
{ {
rating: number; rating: number;

View File

@ -1,4 +1,4 @@
import { Health, IQuestStatus, Productive, Skills, TraderData } from "@spt-aki/models/eft/common/tables/IBotBase"; import { Health, IQuestStatus, Productive, Skills } from "@spt-aki/models/eft/common/tables/IBotBase";
import { Item, Upd } from "@spt-aki/models/eft/common/tables/IItem"; import { Item, Upd } from "@spt-aki/models/eft/common/tables/IItem";
import { IQuest } from "@spt-aki/models/eft/common/tables/IQuest"; import { IQuest } from "@spt-aki/models/eft/common/tables/IQuest";
import { IPmcDataRepeatableQuest } from "@spt-aki/models/eft/common/tables/IRepeatableQuests"; import { IPmcDataRepeatableQuest } from "@spt-aki/models/eft/common/tables/IRepeatableQuests";
@ -80,6 +80,16 @@ export interface Improvement
improveCompleteTimestamp: number; improveCompleteTimestamp: number;
} }
/** Related to TraderInfo */
export interface TraderData
{
salesSum: number;
standing: number;
loyalty: number;
unlocked: boolean;
disabled: boolean;
}
export interface Product export interface Product
{ {
_id: string; _id: string;

View File

@ -39,31 +39,7 @@ export interface ILocationConfig extends IBaseConfig
/** Key: map, value: loose loot ids to ignore */ /** Key: map, value: loose loot ids to ignore */
looseLootBlacklist: Record<string, string[]>; looseLootBlacklist: Record<string, string[]>;
/** Key: map, value: settings to control how long scav raids are*/ /** Key: map, value: settings to control how long scav raids are*/
scavRaidTimeSettings: Record<string, IScavRaidTimeLocationSettings>; scavRaidTimeSettings: IScavRaidTimeSettings;
}
export interface IScavRaidTimeLocationSettings
{
/** Should loot be reduced by same percent length of raid is reduced by */
reduceLootByPercent: boolean;
minStaticLootPercent: number;
minDynamicLootPercent: number;
/** Chance raid time is reduced */
reducedChancePercent: number;
reductionPercentWeights: Record<string, number>;
/** Should bot waves be removed / spawn times be adjusted */
adjustWaves: boolean;
}
export interface IContainerRandomistionSettings
{
enabled: boolean;
/** What maps can use the container randomisation feature */
maps: Record<string, boolean>;
/** Some container types don't work when randomised */
containerTypesToNotRandomise: string[];
containerGroupMinSizeMultiplier: number;
containerGroupMaxSizeMultiplier: number;
} }
export interface IFixEmptyBotWavesSettings export interface IFixEmptyBotWavesSettings
@ -117,3 +93,38 @@ export interface LootMultiplier
terminal: number; terminal: number;
town: number; town: number;
} }
export interface IContainerRandomistionSettings
{
enabled: boolean;
/** What maps can use the container randomisation feature */
maps: Record<string, boolean>;
/** Some container types don't work when randomised */
containerTypesToNotRandomise: string[];
containerGroupMinSizeMultiplier: number;
containerGroupMaxSizeMultiplier: number;
}
export interface IScavRaidTimeSettings
{
settings: IScavRaidTimeConfigSettings
maps: Record<string, IScavRaidTimeLocationSettings>
}
export interface IScavRaidTimeConfigSettings
{
trainArrivalDelayObservedSeconds: number
}
export interface IScavRaidTimeLocationSettings
{
/** Should loot be reduced by same percent length of raid is reduced by */
reduceLootByPercent: boolean;
minStaticLootPercent: number;
minDynamicLootPercent: number;
/** Chance raid time is reduced */
reducedChancePercent: number;
reductionPercentWeights: Record<string, number>;
/** Should bot waves be removed / spawn times be adjusted */
adjustWaves: boolean;
}

View File

@ -2,8 +2,8 @@ import { inject, injectable } from "tsyringe";
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 { IHideoutImprovement, Productive, TraderData, TraderInfo } from "@spt-aki/models/eft/common/tables/IBotBase"; import { IHideoutImprovement, Productive, TraderInfo } from "@spt-aki/models/eft/common/tables/IBotBase";
import { ProfileChange } from "@spt-aki/models/eft/itemEvent/IItemEventRouterBase"; import { ProfileChange, TraderData } from "@spt-aki/models/eft/itemEvent/IItemEventRouterBase";
import { IItemEventRouterResponse } from "@spt-aki/models/eft/itemEvent/IItemEventRouterResponse"; import { IItemEventRouterResponse } from "@spt-aki/models/eft/itemEvent/IItemEventRouterResponse";
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";

View File

@ -171,11 +171,11 @@ export class RaidTimeAdjustmentService
*/ */
protected getMapSettings(location: string): IScavRaidTimeLocationSettings protected getMapSettings(location: string): IScavRaidTimeLocationSettings
{ {
const mapSettings = this.locationConfig.scavRaidTimeSettings[location.toLowerCase()]; const mapSettings = this.locationConfig.scavRaidTimeSettings.maps[location.toLowerCase()];
if (!mapSettings) if (!mapSettings)
{ {
this.logger.warning(`Unable to find scav raid time settings for map: ${location}, using defaults`); this.logger.warning(`Unable to find scav raid time settings for map: ${location}, using defaults`);
return this.locationConfig.scavRaidTimeSettings.default; return this.locationConfig.scavRaidTimeSettings.maps.default;
} }
return mapSettings; return mapSettings;
@ -206,27 +206,47 @@ export class RaidTimeAdjustmentService
Chance: null Chance: null
} }
// At what minute we simulate the player joining the raid
const simulatedRaidEntryTimeMinutes = mapBase.EscapeTimeLimit - newRaidTimeMinutes;
// How many seconds have elapsed in the raid when the player joins
const reductionSeconds = simulatedRaidEntryTimeMinutes * 60;
// Delay between the train extract activating and it becoming available to board
//
// Test method for determining this value:
// 1) Set MinTime, MaxTime, and Count for the train extract all to 120
// 2) Load into Reserve or Lighthouse as a PMC (both have the same result)
// 3) Board the train when it arrives
// 4) Check the raid time on the Raid Ended Screen (it should always be the same)
//
// trainArrivalDelaySeconds = [raid time on raid-ended screen] - MaxTime - Count - ExfiltrationTime
// Example: Raid Time = 5:33 = 333 seconds
// trainArrivalDelaySeconds = 333 - 120 - 120 - 5 = 88
//
// I added 2 seconds just to be safe...
//
const trainArrivalDelaySeconds = this.locationConfig.scavRaidTimeSettings.settings.trainArrivalDelayObservedSeconds;
// Determine the earliest possible time in the raid when the train would leave
const earliestPossibleDepartureMinutes = (exit.MinTime + exit.Count + exit.ExfiltrationTime + trainArrivalDelaySeconds) / 60;
// If raid is after last moment train can leave, assume train has already left, disable extract // If raid is after last moment train can leave, assume train has already left, disable extract
const latestPossibleDepartureMinutes = (exit.MaxTime + exit.Count) / 60; const mostPossibleTimeRemainingAfterDeparture = mapBase.EscapeTimeLimit - earliestPossibleDepartureMinutes;
if (newRaidTimeMinutes < latestPossibleDepartureMinutes) if (newRaidTimeMinutes < mostPossibleTimeRemainingAfterDeparture)
{ {
exitChange.Chance = 0; exitChange.Chance = 0;
this.logger.debug(`Train Exit: ${exit.Name} disabled as new raid time ${newRaidTimeMinutes} minutes is below ${latestPossibleDepartureMinutes} minutes`); this.logger.debug(`Train Exit: ${exit.Name} disabled as new raid time ${newRaidTimeMinutes} minutes is below ${mostPossibleTimeRemainingAfterDeparture} minutes`);
result.push(exitChange); result.push(exitChange);
continue; continue;
} }
// What minute we simulate the player joining a raid at // Reduce extract arrival times. Negative values seem to make extract turn red in game.
const simulatedRaidEntryTimeMinutes = mapBase.EscapeTimeLimit - newRaidTimeMinutes; exitChange.MinTime = Math.max(exit.MinTime - reductionSeconds, 0);
exitChange.MaxTime = Math.max(exit.MaxTime - reductionSeconds, 0);
// How many seconds to reduce extract arrival times by, negative values seem to make extract turn red in game
const reductionSeconds = simulatedRaidEntryTimeMinutes * 60;
exitChange.MinTime = exit.MinTime - reductionSeconds;
exitChange.MaxTime = exit.MaxTime - reductionSeconds;
this.logger.debug(`Train appears between: ${exitChange.MinTime} and ${exitChange.MaxTime} seconds raid time`); this.logger.debug(`Train appears between: ${exitChange.MinTime} and ${exitChange.MaxTime} seconds raid time`);