500 lines
18 KiB
TypeScript
500 lines
18 KiB
TypeScript
import { inject, injectable } from "tsyringe";
|
|
|
|
import { IPmcData } from "../models/eft/common/IPmcData";
|
|
import { Victim } from "../models/eft/common/tables/IBotBase";
|
|
import { Item } from "../models/eft/common/tables/IItem";
|
|
import { ISaveProgressRequestData } from "../models/eft/inRaid/ISaveProgressRequestData";
|
|
import { ConfigTypes } from "../models/enums/ConfigTypes";
|
|
import { ILostOnDeathConfig } from "../models/spt/config/ILostOnDeathConfig";
|
|
import { ILogger } from "../models/spt/utils/ILogger";
|
|
import { ConfigServer } from "../servers/ConfigServer";
|
|
import { DatabaseServer } from "../servers/DatabaseServer";
|
|
import { SaveServer } from "../servers/SaveServer";
|
|
import { LocalisationService } from "../services/LocalisationService";
|
|
import { ProfileFixerService } from "../services/ProfileFixerService";
|
|
import { JsonUtil } from "../utils/JsonUtil";
|
|
import { InventoryHelper } from "./InventoryHelper";
|
|
import { ItemHelper } from "./ItemHelper";
|
|
import { PaymentHelper } from "./PaymentHelper";
|
|
|
|
@injectable()
|
|
export class InRaidHelper
|
|
{
|
|
protected lostOnDeathConfig: ILostOnDeathConfig;
|
|
|
|
constructor(
|
|
@inject("WinstonLogger") protected logger: ILogger,
|
|
@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("PaymentHelper") protected paymentHelper: PaymentHelper,
|
|
@inject("LocalisationService") protected localisationService: LocalisationService,
|
|
@inject("ProfileFixerService") protected profileFixerService: ProfileFixerService,
|
|
@inject("ConfigServer") protected configServer: ConfigServer
|
|
)
|
|
{
|
|
this.lostOnDeathConfig = this.configServer.getConfig(ConfigTypes.LOST_ON_DEATH);
|
|
}
|
|
|
|
/**
|
|
* Should quest items be removed from player inventory on death
|
|
* @returns True if items should be removed from inventory
|
|
*/
|
|
public removeQuestItemsOnDeath(): boolean
|
|
{
|
|
return this.lostOnDeathConfig.questItems;
|
|
}
|
|
|
|
/**
|
|
* Check an array of items and add an upd object to money items 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)
|
|
{
|
|
if (this.paymentHelper.isMoneyTpl(item._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.getStandingChangeForKill(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 getStandingChangeForKill(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
|
|
return botTypes[victim.Side.toLowerCase()]?.experience?.standingForKill;
|
|
}
|
|
|
|
/**
|
|
* Reset a profile to a baseline, used post-raid
|
|
* Reset points earned during session property
|
|
* Increment exp
|
|
* Remove Labs keycard
|
|
* @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): IPmcData
|
|
{
|
|
// remove old skill fatigue
|
|
for (const skill of saveProgressRequest.profile.Skills.Common)
|
|
{
|
|
skill.PointsEarnedDuringSession = 0.0;
|
|
}
|
|
|
|
// set profile data
|
|
profileData.Info.Level = saveProgressRequest.profile.Info.Level;
|
|
profileData.Skills = saveProgressRequest.profile.Skills;
|
|
profileData.Stats = saveProgressRequest.profile.Stats;
|
|
profileData.Encyclopedia = saveProgressRequest.profile.Encyclopedia;
|
|
profileData.ConditionCounters = saveProgressRequest.profile.ConditionCounters;
|
|
profileData.Quests = saveProgressRequest.profile.Quests;
|
|
|
|
// Transfer effects from request to profile
|
|
this.transferPostRaidLimbEffectsToProfile(saveProgressRequest, profileData);
|
|
|
|
profileData.SurvivorClass = saveProgressRequest.profile.SurvivorClass;
|
|
|
|
// add experience points
|
|
profileData.Info.Experience += profileData.Stats.TotalSessionExperience;
|
|
profileData.Stats.TotalSessionExperience = 0;
|
|
|
|
// Remove the Lab card
|
|
this.removeMapAccessKey(saveProgressRequest, sessionID);
|
|
|
|
this.setPlayerInRaidLocationStatusToNone(sessionID);
|
|
|
|
if (!saveProgressRequest.isPlayerScav)
|
|
{
|
|
this.profileFixerService.checkForAndFixPmcProfileIssues(profileData);
|
|
}
|
|
|
|
return profileData;
|
|
}
|
|
|
|
/**
|
|
* 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};
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Some maps have one-time-use keys (e.g. Labs
|
|
* Remove the relevant key from an inventory based on the post-raid request data passed in
|
|
* @param offraidData post-raid data
|
|
* @param sessionID Session id
|
|
*/
|
|
protected removeMapAccessKey(offraidData: ISaveProgressRequestData, sessionID: string): void
|
|
{
|
|
const locationName = this.saveServer.getProfile(sessionID).inraid.location.toLowerCase();
|
|
const mapKey = this.databaseServer.getTables().locations[locationName].base.AccessKeys[0];
|
|
|
|
if (!mapKey)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (const item of offraidData.profile.Inventory.items)
|
|
{
|
|
if (item._tpl === mapKey && item.slotId.toLowerCase() !== "hideout")
|
|
{
|
|
this.inventoryHelper.removeItem(offraidData.profile, item._id, sessionID);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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";
|
|
}
|
|
|
|
/**
|
|
* Adds SpawnedInSession property to items found in a raid
|
|
* Removes SpawnedInSession for non-scav players if item was taken into raid with SpawnedInSession = true
|
|
* @param preRaidProfile profile to update
|
|
* @param postRaidProfile profile to update inventory contents of
|
|
* @param isPlayerScav Was this a p scav raid
|
|
* @returns profile with FiR items properly tagged
|
|
*/
|
|
public addSpawnedInSessionPropertyToItems(preRaidProfile: IPmcData, postRaidProfile: IPmcData, isPlayerScav: boolean): IPmcData
|
|
{
|
|
for (const item of postRaidProfile.Inventory.items)
|
|
{
|
|
if (!isPlayerScav)
|
|
{
|
|
const itemExistsInProfile = preRaidProfile.Inventory.items.find((itemData) => item._id === itemData._id);
|
|
if (itemExistsInProfile)
|
|
{
|
|
// if the item exists and is taken inside the raid, remove the taken in raid status
|
|
delete item.upd?.SpawnedInSession;
|
|
|
|
continue;
|
|
}
|
|
}
|
|
|
|
item.upd = item.upd ?? {};
|
|
item.upd.SpawnedInSession = true;
|
|
}
|
|
|
|
return postRaidProfile;
|
|
}
|
|
|
|
/**
|
|
* 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: IPmcData): IPmcData
|
|
{
|
|
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;
|
|
});
|
|
|
|
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 pmcData Profile to update
|
|
* @param postRaidProfile Profile returned by client after a raid
|
|
* @returns Updated profile
|
|
*/
|
|
public setInventory(sessionID: string, pmcData: IPmcData, postRaidProfile: IPmcData): IPmcData
|
|
{
|
|
// store insurance (as removeItem removes insurance also)
|
|
const insured = this.jsonUtil.clone(pmcData.InsuredItems);
|
|
|
|
// remove possible equipped items from before the raid
|
|
this.inventoryHelper.removeItem(pmcData, pmcData.Inventory.equipment, sessionID);
|
|
this.inventoryHelper.removeItem(pmcData, pmcData.Inventory.questRaidItems, sessionID);
|
|
this.inventoryHelper.removeItem(pmcData, pmcData.Inventory.sortingTable, sessionID);
|
|
|
|
// add the new items
|
|
pmcData.Inventory.items = [...postRaidProfile.Inventory.items, ...pmcData.Inventory.items];
|
|
pmcData.Inventory.fastPanel = postRaidProfile.Inventory.fastPanel;
|
|
pmcData.InsuredItems = insured;
|
|
|
|
return pmcData;
|
|
}
|
|
|
|
/**
|
|
* 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 =>
|
|
{
|
|
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 equipment = pmcProfile?.Inventory?.equipment;
|
|
const questRaidItems = pmcProfile?.Inventory?.questRaidItems;
|
|
|
|
return inventoryItems.filter(x =>
|
|
{
|
|
// Keep items flagged as kept after death
|
|
if (this.isItemKeptAfterDeath(pmcProfile, x))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Remove normal items or quest raid items
|
|
if (x.parentId === equipment || x.parentId === questRaidItems)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Pocket items are not lost on death
|
|
if (x.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 means its a 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 (!this.lostOnDeathConfig.questItems && itemToCheck.parentId === pmcData.Inventory.questRaidItems)
|
|
{
|
|
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",
|
|
"SecuredContainer"
|
|
];
|
|
|
|
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;
|
|
}
|
|
} |