1551a5e793
Feel free to rename everything. We can also use the new function from randomutil and replace code block below on InsuranceController.ts ``` const maxRoll = 9999; const conversionFactor = 100; const returnChance = this.randomUtil.getInt(0, maxRoll) / conversionFactor; const traderReturnChance = this.insuranceConfig.returnChancePercent[traderId]; const roll = returnChance >= traderReturnChance; ``` I killed 2 PMCs with 100% chance and gained 0.07 rep (rounding issue probably somewhere else) ![image](/attachments/00f00922-eb18-4fdc-8958-129b680af704) Some JS problems :S ![image](/attachments/dcbeb1d1-2ee3-4809-890e-e19fe3190840) Implements: https://dev.sp-tarkov.com/SPT-AKI/Issues/issues/150 Co-authored-by: alimoncul <alimoncul@gmail.com> Reviewed-on: https://dev.sp-tarkov.com/SPT-AKI/Server/pulls/204 Co-authored-by: CZPZ <czpz@noreply.dev.sp-tarkov.com> Co-committed-by: CZPZ <czpz@noreply.dev.sp-tarkov.com>
747 lines
29 KiB
TypeScript
747 lines
29 KiB
TypeScript
import { inject, injectable } from "tsyringe";
|
|
|
|
import { InventoryHelper } from "@spt-aki/helpers/InventoryHelper";
|
|
import { ItemHelper } from "@spt-aki/helpers/ItemHelper";
|
|
import { PaymentHelper } from "@spt-aki/helpers/PaymentHelper";
|
|
import { QuestHelper } from "@spt-aki/helpers/QuestHelper";
|
|
import { IPmcData, IPostRaidPmcData } from "@spt-aki/models/eft/common/IPmcData";
|
|
import { IQuestStatus, TraderInfo, Victim } from "@spt-aki/models/eft/common/tables/IBotBase";
|
|
import { Item } from "@spt-aki/models/eft/common/tables/IItem";
|
|
import { ISaveProgressRequestData } from "@spt-aki/models/eft/inRaid/ISaveProgressRequestData";
|
|
import { IFailQuestRequestData } from "@spt-aki/models/eft/quests/IFailQuestRequestData";
|
|
import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes";
|
|
import { QuestStatus } from "@spt-aki/models/enums/QuestStatus";
|
|
import { Traders } from "@spt-aki/models/enums/Traders";
|
|
import { IInRaidConfig } from "@spt-aki/models/spt/config/IInRaidConfig";
|
|
import { ILostOnDeathConfig } from "@spt-aki/models/spt/config/ILostOnDeathConfig";
|
|
import { ILogger } from "@spt-aki/models/spt/utils/ILogger";
|
|
import { ConfigServer } from "@spt-aki/servers/ConfigServer";
|
|
import { DatabaseServer } from "@spt-aki/servers/DatabaseServer";
|
|
import { SaveServer } from "@spt-aki/servers/SaveServer";
|
|
import { LocalisationService } from "@spt-aki/services/LocalisationService";
|
|
import { ProfileFixerService } from "@spt-aki/services/ProfileFixerService";
|
|
import { JsonUtil } from "@spt-aki/utils/JsonUtil";
|
|
import { TimeUtil } from "@spt-aki/utils/TimeUtil";
|
|
import { RandomUtil } from "@spt-aki/utils/RandomUtil";
|
|
import { ProfileHelper } from "./ProfileHelper";
|
|
|
|
@injectable()
|
|
export class InRaidHelper
|
|
{
|
|
protected lostOnDeathConfig: ILostOnDeathConfig;
|
|
protected inRaidConfig: IInRaidConfig;
|
|
|
|
constructor(
|
|
@inject("WinstonLogger") protected logger: ILogger,
|
|
@inject("TimeUtil") protected timeUtil: TimeUtil,
|
|
@inject("SaveServer") protected saveServer: SaveServer,
|
|
@inject("JsonUtil") protected jsonUtil: JsonUtil,
|
|
@inject("ItemHelper") protected itemHelper: ItemHelper,
|
|
@inject("DatabaseServer") protected databaseServer: DatabaseServer,
|
|
@inject("InventoryHelper") protected inventoryHelper: InventoryHelper,
|
|
@inject("ProfileHelper") protected profileHelper: ProfileHelper,
|
|
@inject("QuestHelper") protected questHelper: QuestHelper,
|
|
@inject("PaymentHelper") protected paymentHelper: PaymentHelper,
|
|
@inject("LocalisationService") protected localisationService: LocalisationService,
|
|
@inject("ProfileFixerService") protected profileFixerService: ProfileFixerService,
|
|
@inject("ConfigServer") protected configServer: ConfigServer,
|
|
@inject("RandomUtil") protected randomUtil: RandomUtil
|
|
)
|
|
{
|
|
this.lostOnDeathConfig = this.configServer.getConfig(ConfigTypes.LOST_ON_DEATH);
|
|
this.inRaidConfig = this.configServer.getConfig(ConfigTypes.IN_RAID);
|
|
}
|
|
|
|
/**
|
|
* Lookup quest item loss from lostOnDeath config
|
|
* @returns True if items should be removed from inventory
|
|
*/
|
|
public removeQuestItemsOnDeath(): boolean
|
|
{
|
|
return this.lostOnDeathConfig.questItems;
|
|
}
|
|
|
|
/**
|
|
* Check items array and add an upd object to money with a stack count of 1
|
|
* Single stack money items have no upd object and thus no StackObjectsCount, causing issues
|
|
* @param items Items array to check
|
|
*/
|
|
public addUpdToMoneyFromRaid(items: Item[]): void
|
|
{
|
|
for (const item of items.filter(x => this.paymentHelper.isMoneyTpl(x._tpl)))
|
|
{
|
|
if (!item.upd)
|
|
{
|
|
item.upd = {};
|
|
}
|
|
|
|
if (!item.upd.StackObjectsCount)
|
|
{
|
|
item.upd.StackObjectsCount = 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add karma changes up and return the new value
|
|
* @param existingFenceStanding Current fence standing level
|
|
* @param victims Array of kills player performed
|
|
* @returns adjusted karma level after kills are taken into account
|
|
*/
|
|
public calculateFenceStandingChangeFromKills(existingFenceStanding: number, victims: Victim[]): number
|
|
{
|
|
// Run callback on every victim, adding up the standings gained/lossed, starting value is existing fence standing
|
|
const newFenceStanding = victims.reduce((acc, victim) =>
|
|
{
|
|
const standingForKill = this.getFenceStandingChangeForKillAsScav(victim);
|
|
if (standingForKill)
|
|
{
|
|
return acc + standingForKill;
|
|
}
|
|
this.logger.warning(
|
|
this.localisationService.getText("inraid-missing_standing_for_kill", {
|
|
victimSide: victim.Side,
|
|
victimRole: victim.Role,
|
|
}),
|
|
);
|
|
|
|
return acc;
|
|
}, existingFenceStanding);
|
|
|
|
return newFenceStanding;
|
|
}
|
|
|
|
/**
|
|
* Get the standing gain/loss for killing an npc
|
|
* @param victim Who was killed by player
|
|
* @returns a numerical standing gain or loss
|
|
*/
|
|
protected getFenceStandingChangeForKillAsScav(victim: Victim): number
|
|
{
|
|
const botTypes = this.databaseServer.getTables().bots.types;
|
|
if (victim.Side.toLowerCase() === "savage")
|
|
{
|
|
// Scavs and bosses
|
|
return botTypes[victim.Role.toLowerCase()]?.experience?.standingForKill;
|
|
}
|
|
|
|
// PMCs - get by bear/usec
|
|
let pmcStandingForKill = botTypes[victim.Side.toLowerCase()]?.experience?.standingForKill;
|
|
const pmcKillProbabilityForScavGain = this.inRaidConfig.pmcKillProbabilityForScavGain;
|
|
|
|
if(this.randomUtil.rollForChanceProbability(pmcKillProbabilityForScavGain))
|
|
{
|
|
pmcStandingForKill += this.inRaidConfig.scavExtractGain
|
|
}
|
|
|
|
return pmcStandingForKill;
|
|
}
|
|
|
|
/**
|
|
* Reset a profile to a baseline, used post-raid
|
|
* Reset points earned during session property
|
|
* Increment exp
|
|
* @param profileData Profile to update
|
|
* @param saveProgressRequest post raid save data request data
|
|
* @param sessionID Session id
|
|
* @returns Reset profile object
|
|
*/
|
|
public updateProfileBaseStats(
|
|
profileData: IPmcData,
|
|
saveProgressRequest: ISaveProgressRequestData,
|
|
sessionID: string,
|
|
): void
|
|
{
|
|
// Remove skill fatigue values
|
|
this.resetSkillPointsEarnedDuringRaid(saveProgressRequest.profile);
|
|
|
|
// Set profile data
|
|
profileData.Info.Level = saveProgressRequest.profile.Info.Level;
|
|
profileData.Skills = saveProgressRequest.profile.Skills;
|
|
profileData.Stats.Eft = saveProgressRequest.profile.Stats.Eft;
|
|
profileData.Encyclopedia = saveProgressRequest.profile.Encyclopedia;
|
|
profileData.TaskConditionCounters = saveProgressRequest.profile.TaskConditionCounters;
|
|
|
|
this.validateTaskConditionCounters(saveProgressRequest, profileData);
|
|
|
|
profileData.SurvivorClass = saveProgressRequest.profile.SurvivorClass;
|
|
|
|
// Add experience points
|
|
profileData.Info.Experience += profileData.Stats.Eft.TotalSessionExperience;
|
|
profileData.Stats.Eft.TotalSessionExperience = 0;
|
|
|
|
this.setPlayerInRaidLocationStatusToNone(sessionID);
|
|
}
|
|
|
|
/**
|
|
* Reset the skill points earned in a raid to 0, ready for next raid
|
|
* @param profile Profile to update
|
|
*/
|
|
protected resetSkillPointsEarnedDuringRaid(profile: IPmcData): void
|
|
{
|
|
for (const skill of profile.Skills.Common)
|
|
{
|
|
skill.PointsEarnedDuringSession = 0.0;
|
|
}
|
|
}
|
|
|
|
/** Check counters are correct in profile */
|
|
protected validateTaskConditionCounters(saveProgressRequest: ISaveProgressRequestData, profileData: IPmcData): void
|
|
{
|
|
for (const backendCounterKey in saveProgressRequest.profile.TaskConditionCounters)
|
|
{
|
|
// Skip counters with no id
|
|
if (!saveProgressRequest.profile.TaskConditionCounters[backendCounterKey].id)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
const postRaidValue = saveProgressRequest.profile.TaskConditionCounters[backendCounterKey]?.value;
|
|
if (typeof postRaidValue === "undefined")
|
|
{
|
|
// No value, skip
|
|
continue;
|
|
}
|
|
|
|
const matchingPreRaidCounter = profileData.TaskConditionCounters[backendCounterKey];
|
|
if (!matchingPreRaidCounter)
|
|
{
|
|
this.logger.error(`TaskConditionCounters: ${backendCounterKey} cannot be found in pre-raid data`);
|
|
|
|
continue;
|
|
}
|
|
|
|
if (matchingPreRaidCounter.value !== postRaidValue)
|
|
{
|
|
this.logger.error(
|
|
`TaskConditionCounters: ${backendCounterKey} value is different post raid, old: ${matchingPreRaidCounter.value} new: ${postRaidValue}`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update various serverPMC profile values; quests/limb hp/trader standing with values post-raic
|
|
* @param pmcData Server PMC profile
|
|
* @param saveProgressRequest Post-raid request data
|
|
* @param sessionId Session id
|
|
*/
|
|
public updatePmcProfileDataPostRaid(pmcData: IPmcData, saveProgressRequest: ISaveProgressRequestData, sessionId: string): void
|
|
{
|
|
// Process failed quests then copy everything
|
|
this.processAlteredQuests(sessionId, pmcData, pmcData.Quests, saveProgressRequest.profile);
|
|
pmcData.Quests = saveProgressRequest.profile.Quests;
|
|
|
|
// No need to do this for scav, old scav is deleted and new one generated
|
|
this.transferPostRaidLimbEffectsToProfile(saveProgressRequest, pmcData);
|
|
|
|
// Trader standing only occur on pmc profile, scav kills are handled in handlePostRaidPlayerScavKarmaChanges()
|
|
// Scav client data has standing values of 0 for all traders, DO NOT RUN ON SCAV RAIDS
|
|
this.applyTraderStandingAdjustments(pmcData.TradersInfo, saveProgressRequest.profile.TradersInfo);
|
|
|
|
this.updateProfileAchievements(pmcData, saveProgressRequest.profile.Achievements);
|
|
|
|
this.profileFixerService.checkForAndFixPmcProfileIssues(pmcData);
|
|
}
|
|
|
|
/**
|
|
* Update scav quest values on server profile with updated values post-raid
|
|
* @param scavData Server scav profile
|
|
* @param saveProgressRequest Post-raid request data
|
|
* @param sessionId Session id
|
|
*/
|
|
public updateScavProfileDataPostRaid(scavData: IPmcData, saveProgressRequest: ISaveProgressRequestData, sessionId: string): void
|
|
{
|
|
// Only copy active quests into scav profile // Progress will later to copied over to PMC profile
|
|
const existingActiveQuestIds = scavData.Quests?.filter(x => x.status !== QuestStatus.AvailableForStart).map(x => x.qid);
|
|
if (existingActiveQuestIds)
|
|
{
|
|
scavData.Quests = saveProgressRequest.profile.Quests.filter(x => existingActiveQuestIds.includes(x.qid));
|
|
}
|
|
|
|
this.profileFixerService.checkForAndFixScavProfileIssues(scavData);
|
|
}
|
|
|
|
/**
|
|
* Look for quests with a status different from what it began the raid with
|
|
* @param sessionId Player id
|
|
* @param pmcData Player profile
|
|
* @param preRaidQuests Quests prior to starting raid
|
|
* @param postRaidProfile Profile sent by client with post-raid quests
|
|
*/
|
|
protected processAlteredQuests(
|
|
sessionId: string,
|
|
pmcData: IPmcData,
|
|
preRaidQuests: IQuestStatus[],
|
|
postRaidProfile: IPostRaidPmcData,
|
|
): void
|
|
{
|
|
// TODO: this may break when locked quests are added to profile but player has completed no quests prior to raid
|
|
if (!preRaidQuests)
|
|
{
|
|
// No quests to compare against, skip
|
|
return;
|
|
}
|
|
|
|
// Loop over all quests from post-raid profile
|
|
const newLockedQuests: IQuestStatus[] = [];
|
|
for (const postRaidQuest of postRaidProfile.Quests)
|
|
{
|
|
// postRaidQuest.status has a weird value, need to do some nasty casting to compare it
|
|
const postRaidQuestStatus = <string><unknown>postRaidQuest.status;
|
|
|
|
// Find matching pre-raid quest
|
|
const preRaidQuest = preRaidQuests?.find((x) => x.qid === postRaidQuest.qid);
|
|
if (!preRaidQuest)
|
|
{
|
|
// Some traders gives locked quests (LightKeeper) due to time-gating
|
|
if (postRaidQuestStatus === "Locked")
|
|
{
|
|
// Store new locked quest for future processing
|
|
newLockedQuests.push(postRaidQuest);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
// Already completed/failed before raid, skip
|
|
if ([QuestStatus.Fail, QuestStatus.Success].includes(preRaidQuest.status) )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Quest with time-gate has unlocked
|
|
if (postRaidQuestStatus === "AvailableAfter" && postRaidQuest.availableAfter <= this.timeUtil.getTimestamp())
|
|
{
|
|
// Flag as ready to complete
|
|
postRaidQuest.status = QuestStatus.AvailableForStart;
|
|
postRaidQuest.statusTimers[QuestStatus.AvailableForStart] = this.timeUtil.getTimestamp();
|
|
|
|
this.logger.debug(`Time-locked quest ${postRaidQuest.qid} is now ready to start`);
|
|
|
|
continue;
|
|
}
|
|
|
|
// Quest failed inside raid
|
|
if (postRaidQuestStatus === "Fail")
|
|
{
|
|
// Send failed message
|
|
const failBody: IFailQuestRequestData = {
|
|
Action: "QuestFail",
|
|
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.conditionType === "FindItem");
|
|
const itemsToCollect: string[] = [];
|
|
if (matchingAffFindConditions)
|
|
{
|
|
// Find all items the failed quest wanted
|
|
for (const condition of matchingAffFindConditions)
|
|
{
|
|
itemsToCollect.push(...condition.target);
|
|
}
|
|
}
|
|
|
|
// 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 = [];
|
|
}
|
|
}
|
|
|
|
// Reclassify time-gated quests as time gated until a specific date
|
|
if (newLockedQuests.length > 0)
|
|
{
|
|
for (const lockedQuest of newLockedQuests)
|
|
{
|
|
// Get the quest from Db
|
|
const dbQuest = this.questHelper.getQuestFromDb(lockedQuest.qid, null);
|
|
if (!dbQuest)
|
|
{
|
|
this.logger.warning(`Unable to adjust locked quest: ${lockedQuest.qid} as it wasnt found in db. It may not become available later on`);
|
|
|
|
continue;
|
|
}
|
|
|
|
// Find the time requirement in AvailableForStart array (assuming there is one as quest in locked state === its time-gated)
|
|
const afsRequirement = dbQuest.conditions.AvailableForStart.find(x => x.conditionType === "Quest");
|
|
if (afsRequirement && afsRequirement.availableAfter > 0)
|
|
{
|
|
// Prereq quest has a wait
|
|
// Set quest as AvailableAfter and set timer
|
|
const timestamp = this.timeUtil.getTimestamp() + afsRequirement.availableAfter;
|
|
lockedQuest.availableAfter = timestamp;
|
|
lockedQuest.statusTimers.AvailableAfter = timestamp;
|
|
lockedQuest.status = 9;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Take body part effects from client profile and apply to server profile
|
|
* @param saveProgressRequest post-raid request
|
|
* @param profileData player profile on server
|
|
*/
|
|
protected transferPostRaidLimbEffectsToProfile(
|
|
saveProgressRequest: ISaveProgressRequestData,
|
|
profileData: IPmcData,
|
|
): void
|
|
{
|
|
// Iterate over each body part
|
|
for (const bodyPartId in saveProgressRequest.profile.Health.BodyParts)
|
|
{
|
|
// Get effects on body part from profile
|
|
const bodyPartEffects = saveProgressRequest.profile.Health.BodyParts[bodyPartId].Effects;
|
|
for (const effect in bodyPartEffects)
|
|
{
|
|
const effectDetails = bodyPartEffects[effect];
|
|
|
|
// Null guard
|
|
if (!profileData.Health.BodyParts[bodyPartId].Effects)
|
|
{
|
|
profileData.Health.BodyParts[bodyPartId].Effects = {};
|
|
}
|
|
|
|
// Already exists on server profile, skip
|
|
const profileBodyPartEffects = profileData.Health.BodyParts[bodyPartId].Effects;
|
|
if (profileBodyPartEffects[effect])
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Add effect to server profile
|
|
profileBodyPartEffects[effect] = { Time: effectDetails.Time ?? -1 };
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adjust server trader settings if they differ from data sent by client
|
|
* @param tradersServerProfile Server
|
|
* @param tradersClientProfile Client
|
|
*/
|
|
protected applyTraderStandingAdjustments(
|
|
tradersServerProfile: Record<string, TraderInfo>,
|
|
tradersClientProfile: Record<string, TraderInfo>,
|
|
): void
|
|
{
|
|
for (const traderId in tradersClientProfile)
|
|
{
|
|
if (traderId === Traders.FENCE)
|
|
{
|
|
// Taking a car extract adjusts fence rep values via client/match/offline/end, skip fence for this check
|
|
continue;
|
|
}
|
|
|
|
const serverProfileTrader = tradersServerProfile[traderId];
|
|
const clientProfileTrader = tradersClientProfile[traderId];
|
|
if (!(serverProfileTrader && clientProfileTrader))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (clientProfileTrader.standing !== serverProfileTrader.standing)
|
|
{
|
|
// Difference found, update server profile with values from client profile
|
|
tradersServerProfile[traderId].standing = clientProfileTrader.standing;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Transfer client achievements into profile
|
|
* @param profile Player pmc profile
|
|
* @param clientAchievements Achievements from client
|
|
*/
|
|
protected updateProfileAchievements(profile: IPmcData, clientAchievements: Record<string, number>): void
|
|
{
|
|
if (!profile.Achievements)
|
|
{
|
|
profile.Achievements = {};
|
|
}
|
|
|
|
for (const achievementId in clientAchievements)
|
|
{
|
|
profile.Achievements[achievementId] = clientAchievements[achievementId];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the SPT inraid location Profile property to 'none'
|
|
* @param sessionID Session id
|
|
*/
|
|
protected setPlayerInRaidLocationStatusToNone(sessionID: string): void
|
|
{
|
|
this.saveServer.getProfile(sessionID).inraid.location = "none";
|
|
}
|
|
|
|
/**
|
|
* Iterate over inventory items and remove the property that defines an item as Found in Raid
|
|
* Only removes property if item had FiR when entering raid
|
|
* @param postRaidProfile profile to update items for
|
|
* @returns Updated profile with SpawnedInSession removed
|
|
*/
|
|
public removeSpawnedInSessionPropertyFromItems(postRaidProfile: IPostRaidPmcData): IPostRaidPmcData
|
|
{
|
|
const dbItems = this.databaseServer.getTables().templates.items;
|
|
const itemsToRemovePropertyFrom = postRaidProfile.Inventory.items.filter((x) =>
|
|
{
|
|
// Has upd object + upd.SpawnedInSession property + not a quest item
|
|
return "upd" in x && "SpawnedInSession" in x.upd
|
|
&& !dbItems[x._tpl]._props.QuestItem
|
|
&& !(this.inRaidConfig.keepFiRSecureContainerOnDeath
|
|
&& this.itemHelper.itemIsInsideContainer(x, "SecuredContainer", postRaidProfile.Inventory.items));
|
|
});
|
|
|
|
itemsToRemovePropertyFrom.forEach((item) =>
|
|
{
|
|
delete item.upd.SpawnedInSession;
|
|
});
|
|
|
|
return postRaidProfile;
|
|
}
|
|
|
|
/**
|
|
* Update a players inventory post-raid
|
|
* Remove equipped items from pre-raid
|
|
* Add new items found in raid to profile
|
|
* Store insurance items in profile
|
|
* @param sessionID Session id
|
|
* @param serverProfile Profile to update
|
|
* @param postRaidProfile Profile returned by client after a raid
|
|
* @returns Updated profile
|
|
*/
|
|
public setInventory(sessionID: string, serverProfile: IPmcData, postRaidProfile: IPmcData): IPmcData
|
|
{
|
|
// Store insurance (as removeItem() removes insurance also)
|
|
const insured = this.jsonUtil.clone(serverProfile.InsuredItems);
|
|
|
|
// Remove possible equipped items from before the raid
|
|
this.inventoryHelper.removeItem(serverProfile, serverProfile.Inventory.equipment, sessionID);
|
|
this.inventoryHelper.removeItem(serverProfile, serverProfile.Inventory.questRaidItems, sessionID);
|
|
this.inventoryHelper.removeItem(serverProfile, serverProfile.Inventory.sortingTable, sessionID);
|
|
|
|
// Add the new items
|
|
serverProfile.Inventory.items = [...postRaidProfile.Inventory.items, ...serverProfile.Inventory.items];
|
|
serverProfile.Inventory.fastPanel = postRaidProfile.Inventory.fastPanel; // Quick access items bar
|
|
serverProfile.InsuredItems = insured;
|
|
|
|
return serverProfile;
|
|
}
|
|
|
|
/**
|
|
* Clear pmc inventory of all items except those that are exempt
|
|
* Used post-raid to remove items after death
|
|
* @param pmcData Player profile
|
|
* @param sessionID Session id
|
|
*/
|
|
public deleteInventory(pmcData: IPmcData, sessionID: string): void
|
|
{
|
|
// Get inventory item ids to remove from players profile
|
|
const itemIdsToDeleteFromProfile = this.getInventoryItemsLostOnDeath(pmcData).map((x) => x._id);
|
|
itemIdsToDeleteFromProfile.forEach((x) =>
|
|
{
|
|
// Items inside containers are handed as part of function
|
|
this.inventoryHelper.removeItem(pmcData, x, sessionID);
|
|
});
|
|
|
|
// Remove contents of fast panel
|
|
pmcData.Inventory.fastPanel = {};
|
|
}
|
|
|
|
/**
|
|
* Get an array of items from a profile that will be lost on death
|
|
* @param pmcProfile Profile to get items from
|
|
* @returns Array of items lost on death
|
|
*/
|
|
protected getInventoryItemsLostOnDeath(pmcProfile: IPmcData): Item[]
|
|
{
|
|
const inventoryItems = pmcProfile.Inventory.items ?? [];
|
|
const equipmentRootId = pmcProfile?.Inventory?.equipment;
|
|
const questRaidItemContainerId = pmcProfile?.Inventory?.questRaidItems;
|
|
|
|
return inventoryItems.filter((item) =>
|
|
{
|
|
// Keep items flagged as kept after death
|
|
if (this.isItemKeptAfterDeath(pmcProfile, item))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Remove normal items or quest raid items
|
|
if (item.parentId === equipmentRootId || item.parentId === questRaidItemContainerId)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Pocket items are lost on death
|
|
if (item.slotId.startsWith("pocket"))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get items in vest/pocket/backpack inventory containers (excluding children)
|
|
* @param pmcData Player profile
|
|
* @returns Item array
|
|
*/
|
|
protected getBaseItemsInRigPocketAndBackpack(pmcData: IPmcData): Item[]
|
|
{
|
|
const rig = pmcData.Inventory.items.find((x) => x.slotId === "TacticalVest");
|
|
const pockets = pmcData.Inventory.items.find((x) => x.slotId === "Pockets");
|
|
const backpack = pmcData.Inventory.items.find((x) => x.slotId === "Backpack");
|
|
|
|
const baseItemsInRig = pmcData.Inventory.items.filter((x) => x.parentId === rig?._id);
|
|
const baseItemsInPockets = pmcData.Inventory.items.filter((x) => x.parentId === pockets?._id);
|
|
const baseItemsInBackpack = pmcData.Inventory.items.filter((x) => x.parentId === backpack?._id);
|
|
|
|
return [...baseItemsInRig, ...baseItemsInPockets, ...baseItemsInBackpack];
|
|
}
|
|
|
|
/**
|
|
* Does the provided items slotId mean its kept on the player after death
|
|
* @pmcData Player profile
|
|
* @itemToCheck Item to check should be kept
|
|
* @returns true if item is kept after death
|
|
*/
|
|
protected isItemKeptAfterDeath(pmcData: IPmcData, itemToCheck: Item): boolean
|
|
{
|
|
// No parentId = base inventory item, always keep
|
|
if (!itemToCheck.parentId)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Is item equipped on player
|
|
if (itemToCheck.parentId === pmcData.Inventory.equipment)
|
|
{
|
|
// Check slot id against config, true = delete, false = keep, undefined = delete
|
|
const discard = this.lostOnDeathConfig.equipment[itemToCheck.slotId];
|
|
if (discard === undefined)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return !discard;
|
|
}
|
|
|
|
// Is quest item + quest item not lost on death
|
|
if (itemToCheck.parentId === pmcData.Inventory.questRaidItems && !this.lostOnDeathConfig.questItems)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// special slots are always kept after death
|
|
if (itemToCheck.slotId?.includes("SpecialSlot") && this.lostOnDeathConfig.specialSlotItems)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Return the equipped items from a players inventory
|
|
* @param items Players inventory to search through
|
|
* @returns an array of equipped items
|
|
*/
|
|
public getPlayerGear(items: Item[]): Item[]
|
|
{
|
|
// Player Slots we care about
|
|
const inventorySlots = [
|
|
"FirstPrimaryWeapon",
|
|
"SecondPrimaryWeapon",
|
|
"Holster",
|
|
"Scabbard",
|
|
"Compass",
|
|
"Headwear",
|
|
"Earpiece",
|
|
"Eyewear",
|
|
"FaceCover",
|
|
"ArmBand",
|
|
"ArmorVest",
|
|
"TacticalVest",
|
|
"Backpack",
|
|
"pocket1",
|
|
"pocket2",
|
|
"pocket3",
|
|
"pocket4",
|
|
"SpecialSlot1",
|
|
"SpecialSlot2",
|
|
"SpecialSlot3",
|
|
];
|
|
|
|
let inventoryItems: Item[] = [];
|
|
|
|
// Get an array of root player items
|
|
for (const item of items)
|
|
{
|
|
if (inventorySlots.includes(item.slotId))
|
|
{
|
|
inventoryItems.push(item);
|
|
}
|
|
}
|
|
|
|
// Loop through these items and get all of their children
|
|
let newItems = inventoryItems;
|
|
while (newItems.length > 0)
|
|
{
|
|
const foundItems = [];
|
|
|
|
for (const item of newItems)
|
|
{
|
|
// Find children of this item
|
|
for (const newItem of items)
|
|
{
|
|
if (newItem.parentId === item._id)
|
|
{
|
|
foundItems.push(newItem);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add these new found items to our list of inventory items
|
|
inventoryItems = [...inventoryItems, ...foundItems];
|
|
|
|
// Now find the children of these items
|
|
newItems = foundItems;
|
|
}
|
|
|
|
return inventoryItems;
|
|
}
|
|
}
|