mirror of
https://github.com/sp-tarkov/server.git
synced 2025-02-13 09:50:43 -05:00
361 lines
15 KiB
TypeScript
361 lines
15 KiB
TypeScript
import { ItemHelper } from "@spt/helpers/ItemHelper";
|
|
import { ProfileHelper } from "@spt/helpers/ProfileHelper";
|
|
import { TraderHelper } from "@spt/helpers/TraderHelper";
|
|
import { IPmcData } from "@spt/models/eft/common/IPmcData";
|
|
import { IItem } from "@spt/models/eft/common/tables/IItem";
|
|
import { ITraderBase } from "@spt/models/eft/common/tables/ITrader";
|
|
import { BonusType } from "@spt/models/enums/BonusType";
|
|
import { ConfigTypes } from "@spt/models/enums/ConfigTypes";
|
|
import { ItemTpl } from "@spt/models/enums/ItemTpl";
|
|
import { MessageType } from "@spt/models/enums/MessageType";
|
|
import { IInsuranceConfig } from "@spt/models/spt/config/IInsuranceConfig";
|
|
import { IInsuranceEquipmentPkg } from "@spt/models/spt/services/IInsuranceEquipmentPkg";
|
|
import { ILogger } from "@spt/models/spt/utils/ILogger";
|
|
import { ConfigServer } from "@spt/servers/ConfigServer";
|
|
import { SaveServer } from "@spt/servers/SaveServer";
|
|
import { DatabaseService } from "@spt/services/DatabaseService";
|
|
import { LocalisationService } from "@spt/services/LocalisationService";
|
|
import { MailSendService } from "@spt/services/MailSendService";
|
|
import { HashUtil } from "@spt/utils/HashUtil";
|
|
import { RandomUtil } from "@spt/utils/RandomUtil";
|
|
import { TimeUtil } from "@spt/utils/TimeUtil";
|
|
import { ICloner } from "@spt/utils/cloners/ICloner";
|
|
import { inject, injectable } from "tsyringe";
|
|
|
|
@injectable()
|
|
export class InsuranceService {
|
|
protected insured: Record<string, Record<string, IItem[]>> = {};
|
|
protected insuranceConfig: IInsuranceConfig;
|
|
|
|
constructor(
|
|
@inject("PrimaryLogger") protected logger: ILogger,
|
|
@inject("DatabaseService") protected databaseService: DatabaseService,
|
|
@inject("RandomUtil") protected randomUtil: RandomUtil,
|
|
@inject("ItemHelper") protected itemHelper: ItemHelper,
|
|
@inject("HashUtil") protected hashUtil: HashUtil,
|
|
@inject("TimeUtil") protected timeUtil: TimeUtil,
|
|
@inject("SaveServer") protected saveServer: SaveServer,
|
|
@inject("TraderHelper") protected traderHelper: TraderHelper,
|
|
@inject("ProfileHelper") protected profileHelper: ProfileHelper,
|
|
@inject("LocalisationService") protected localisationService: LocalisationService,
|
|
@inject("MailSendService") protected mailSendService: MailSendService,
|
|
@inject("ConfigServer") protected configServer: ConfigServer,
|
|
@inject("PrimaryCloner") protected cloner: ICloner,
|
|
) {
|
|
this.insuranceConfig = this.configServer.getConfig(ConfigTypes.INSURANCE);
|
|
}
|
|
|
|
/**
|
|
* Does player have insurance array
|
|
* @param sessionId Player id
|
|
* @returns True if exists
|
|
*/
|
|
public isuranceDictionaryExists(sessionId: string): boolean {
|
|
return this.insured[sessionId] !== undefined;
|
|
}
|
|
|
|
/**
|
|
* Get all insured items by all traders for a profile
|
|
* @param sessionId Profile id (session id)
|
|
* @returns Item array
|
|
*/
|
|
public getInsurance(sessionId: string): Record<string, IItem[]> {
|
|
return this.insured[sessionId];
|
|
}
|
|
|
|
public resetInsurance(sessionId: string): void {
|
|
this.insured[sessionId] = {};
|
|
}
|
|
|
|
/**
|
|
* Sends `i will go look for your stuff` trader message +
|
|
* Store lost insurance items inside profile for later retreval
|
|
* @param pmcData Profile to send insured items to
|
|
* @param sessionID SessionId of current player
|
|
* @param mapId Id of the location player died/exited that caused the insurance to be issued on
|
|
*/
|
|
public startPostRaidInsuranceLostProcess(pmcData: IPmcData, sessionID: string, mapId: string): void {
|
|
// Get insurance items for each trader
|
|
const globals = this.databaseService.getGlobals();
|
|
for (const traderId in this.getInsurance(sessionID)) {
|
|
const traderBase = this.traderHelper.getTrader(traderId, sessionID);
|
|
if (!traderBase) {
|
|
throw new Error(this.localisationService.getText("insurance-unable_to_find_trader_by_id", traderId));
|
|
}
|
|
|
|
const dialogueTemplates = this.databaseService.getTrader(traderId).dialogue;
|
|
if (!dialogueTemplates) {
|
|
throw new Error(this.localisationService.getText("insurance-trader_lacks_dialogue_property", traderId));
|
|
}
|
|
|
|
const systemData = {
|
|
date: this.timeUtil.getDateMailFormat(),
|
|
time: this.timeUtil.getTimeMailFormat(),
|
|
location: mapId,
|
|
};
|
|
|
|
const traderEnum = this.traderHelper.getTraderById(traderId);
|
|
if (!traderEnum) {
|
|
throw new Error(this.localisationService.getText("insurance-trader_missing_from_enum", traderId));
|
|
}
|
|
// Send "i will go look for your stuff" message from trader to player
|
|
this.mailSendService.sendLocalisedNpcMessageToPlayer(
|
|
sessionID,
|
|
traderEnum,
|
|
MessageType.NPC_TRADER,
|
|
this.randomUtil.getArrayValue(dialogueTemplates?.insuranceStart ?? ["INSURANCE START MESSAGE MISSING"]),
|
|
undefined,
|
|
this.timeUtil.getHoursAsSeconds(globals.config.Insurance.MaxStorageTimeInHour),
|
|
systemData,
|
|
);
|
|
|
|
// Store insurance to send to player later in profile
|
|
// Store insurance return details in profile + "hey i found your stuff, here you go!" message details to send to player at a later date
|
|
this.saveServer.getProfile(sessionID).insurance.push({
|
|
scheduledTime: this.getInsuranceReturnTimestamp(pmcData, traderBase),
|
|
traderId: traderId,
|
|
maxStorageTime: this.getMaxInsuranceStorageTime(traderBase),
|
|
systemData: systemData,
|
|
messageType: MessageType.INSURANCE_RETURN,
|
|
messageTemplateId: this.randomUtil.getArrayValue(dialogueTemplates.insuranceFound),
|
|
items: this.getInsurance(sessionID)[traderId],
|
|
});
|
|
}
|
|
|
|
this.resetInsurance(sessionID);
|
|
}
|
|
|
|
/**
|
|
* Get a timestamp of when insurance items should be sent to player based on trader used to insure
|
|
* Apply insurance return bonus if found in profile
|
|
* @param pmcData Player profile
|
|
* @param trader Trader base used to insure items
|
|
* @returns Timestamp to return items to player in seconds
|
|
*/
|
|
protected getInsuranceReturnTimestamp(pmcData: IPmcData, trader: ITraderBase): number {
|
|
// If override in config is non-zero, use that instead of trader values
|
|
if (this.insuranceConfig.returnTimeOverrideSeconds > 0) {
|
|
this.logger.debug(
|
|
`Insurance override used: returning in ${this.insuranceConfig.returnTimeOverrideSeconds} seconds`,
|
|
);
|
|
return this.timeUtil.getTimestamp() + this.insuranceConfig.returnTimeOverrideSeconds;
|
|
}
|
|
|
|
const insuranceReturnTimeBonusSum = this.profileHelper.getBonusValueFromProfile(
|
|
pmcData,
|
|
BonusType.INSURANCE_RETURN_TIME,
|
|
);
|
|
|
|
// A negative bonus implies a faster return, since we subtract later, invert the value here
|
|
const insuranceReturnTimeBonusPercent = -(insuranceReturnTimeBonusSum / 100);
|
|
|
|
const traderMinReturnAsSeconds = trader.insurance.min_return_hour * TimeUtil.ONE_HOUR_AS_SECONDS;
|
|
const traderMaxReturnAsSeconds = trader.insurance.max_return_hour * TimeUtil.ONE_HOUR_AS_SECONDS;
|
|
let randomisedReturnTimeSeconds = this.randomUtil.getInt(traderMinReturnAsSeconds, traderMaxReturnAsSeconds);
|
|
|
|
// Check for Mark of The Unheard in players special slots (only slot item can fit)
|
|
const globals = this.databaseService.getGlobals();
|
|
const hasMarkOfUnheard = this.itemHelper.hasItemWithTpl(
|
|
pmcData.Inventory.items,
|
|
ItemTpl.MARKOFUNKNOWN_MARK_OF_THE_UNHEARD,
|
|
"SpecialSlot",
|
|
);
|
|
if (hasMarkOfUnheard) {
|
|
// Reduce return time by globals multipler value
|
|
randomisedReturnTimeSeconds *= globals.config.Insurance.CoefOfHavingMarkOfUnknown;
|
|
}
|
|
|
|
// EoD has 30% faster returns
|
|
const editionModifier = globals.config.Insurance.EditionSendingMessageTime[pmcData.Info.GameVersion];
|
|
if (editionModifier) {
|
|
randomisedReturnTimeSeconds *= editionModifier.multiplier;
|
|
}
|
|
|
|
// Calculate the final return time based on our bonus percent
|
|
const finalReturnTimeSeconds = randomisedReturnTimeSeconds * (1.0 - insuranceReturnTimeBonusPercent);
|
|
return this.timeUtil.getTimestamp() + finalReturnTimeSeconds;
|
|
}
|
|
|
|
/**
|
|
* Take the insurance item packages within a profile session and ensure that each of the items in that package are
|
|
* not orphaned from their parent ID.
|
|
*
|
|
* @param sessionID The session ID to update insurance equipment packages in.
|
|
* @returns void
|
|
*/
|
|
protected adoptOrphanedInsEquipment(sessionID: string): void {
|
|
const rootID = this.getRootItemParentID(sessionID);
|
|
const insuranceData = this.getInsurance(sessionID);
|
|
for (const [traderId, items] of Object.entries(insuranceData)) {
|
|
this.insured[sessionID][traderId] = this.itemHelper.adoptOrphanedItems(rootID, items);
|
|
}
|
|
}
|
|
|
|
protected getMaxInsuranceStorageTime(traderBase: ITraderBase): number {
|
|
if (this.insuranceConfig.storageTimeOverrideSeconds > 0) {
|
|
// Override exists, use instead of traders value
|
|
return this.insuranceConfig.storageTimeOverrideSeconds;
|
|
}
|
|
|
|
return this.timeUtil.getHoursAsSeconds(traderBase.insurance.max_storage_time);
|
|
}
|
|
|
|
/**
|
|
* Store lost gear post-raid inside profile, ready for later code to pick it up and mail it
|
|
* @param equipmentPkg Gear to store - generated by getGearLostInRaid()
|
|
*/
|
|
public storeGearLostInRaidToSendLater(sessionID: string, equipmentPkg: IInsuranceEquipmentPkg[]): void {
|
|
// Process all insured items lost in-raid
|
|
for (const gear of equipmentPkg) {
|
|
this.addGearToSend(gear);
|
|
}
|
|
|
|
// Items are separated into their individual trader packages, now we can ensure that they all have valid parents
|
|
this.adoptOrphanedInsEquipment(sessionID);
|
|
}
|
|
|
|
/**
|
|
* For the passed in items, find the trader it was insured against
|
|
* @param sessionId Session id
|
|
* @param lostInsuredItems Insured items lost in a raid
|
|
* @param pmcProfile Player profile
|
|
* @returns IInsuranceEquipmentPkg array
|
|
*/
|
|
public mapInsuredItemsToTrader(
|
|
sessionId: string,
|
|
lostInsuredItems: IItem[],
|
|
pmcProfile: IPmcData,
|
|
): IInsuranceEquipmentPkg[] {
|
|
const result: IInsuranceEquipmentPkg[] = [];
|
|
|
|
for (const lostItem of lostInsuredItems) {
|
|
const insuranceDetails = pmcProfile.InsuredItems.find((insuredItem) => insuredItem.itemId === lostItem._id);
|
|
if (!insuranceDetails) {
|
|
this.logger.error(
|
|
`unable to find insurance details for item id: ${lostItem._id} with tpl: ${lostItem._tpl}`,
|
|
);
|
|
|
|
continue;
|
|
}
|
|
|
|
if (this.itemCannotBeLostOnDeath(lostItem, pmcProfile.Inventory.items)) {
|
|
continue;
|
|
}
|
|
|
|
// Add insured item + details to return array
|
|
result.push({
|
|
sessionID: sessionId,
|
|
itemToReturnToPlayer: lostItem,
|
|
pmcData: pmcProfile,
|
|
traderId: insuranceDetails.tid,
|
|
});
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Some items should never be returned in insurance but BSG send them in the request
|
|
* @param lostItem Item being returned in insurance
|
|
* @param inventoryItems Player inventory
|
|
* @returns True if item
|
|
*/
|
|
protected itemCannotBeLostOnDeath(lostItem: IItem, inventoryItems: IItem[]): boolean {
|
|
if (lostItem.slotId?.toLowerCase().startsWith("specialslot")) {
|
|
return true;
|
|
}
|
|
|
|
// We check secure container items even tho they are omitted from lostInsuredItems, just in case
|
|
if (this.itemHelper.itemIsInsideContainer(lostItem, "SecuredContainer", inventoryItems)) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Add gear item to InsuredItems array in player profile
|
|
* @param sessionID Session id
|
|
* @param pmcData Player profile
|
|
* @param itemToReturnToPlayer item to store
|
|
* @param traderId Id of trader item was insured with
|
|
*/
|
|
protected addGearToSend(gear: IInsuranceEquipmentPkg): void {
|
|
const sessionId = gear.sessionID;
|
|
const pmcData = gear.pmcData;
|
|
const itemToReturnToPlayer = gear.itemToReturnToPlayer;
|
|
const traderId = gear.traderId;
|
|
|
|
// Ensure insurance array is init
|
|
if (!this.isuranceDictionaryExists(sessionId)) {
|
|
this.resetInsurance(sessionId);
|
|
}
|
|
|
|
// init trader insurance array
|
|
if (!this.insuranceTraderArrayExists(sessionId, traderId)) {
|
|
this.resetInsuranceTraderArray(sessionId, traderId);
|
|
}
|
|
|
|
this.addInsuranceItemToArray(sessionId, traderId, itemToReturnToPlayer);
|
|
|
|
// Remove item from insured items array as its been processed
|
|
pmcData.InsuredItems = pmcData.InsuredItems.filter((item) => {
|
|
return item.itemId !== itemToReturnToPlayer._id;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Does insurance exist for a player and by trader
|
|
* @param sessionId Player id (session id)
|
|
* @param traderId Trader items insured with
|
|
* @returns True if exists
|
|
*/
|
|
protected insuranceTraderArrayExists(sessionId: string, traderId: string): boolean {
|
|
return this.insured[sessionId][traderId] !== undefined;
|
|
}
|
|
|
|
/**
|
|
* Empty out array holding insured items by sessionid + traderid
|
|
* @param sessionId Player id (session id)
|
|
* @param traderId Trader items insured with
|
|
*/
|
|
public resetInsuranceTraderArray(sessionId: string, traderId: string): void {
|
|
this.insured[sessionId][traderId] = [];
|
|
}
|
|
|
|
/**
|
|
* Store insured item
|
|
* @param sessionId Player id (session id)
|
|
* @param traderId Trader item insured with
|
|
* @param itemToAdd Insured item (with children)
|
|
*/
|
|
public addInsuranceItemToArray(sessionId: string, traderId: string, itemToAdd: IItem): void {
|
|
this.insured[sessionId][traderId].push(itemToAdd);
|
|
}
|
|
|
|
/**
|
|
* Get price of insurance * multiplier from config
|
|
* @param pmcData Player profile
|
|
* @param inventoryItem Item to be insured
|
|
* @param traderId Trader item is insured with
|
|
* @returns price in roubles
|
|
*/
|
|
public getRoublePriceToInsureItemWithTrader(pmcData: IPmcData, inventoryItem: IItem, traderId: string): number {
|
|
const price =
|
|
this.itemHelper.getStaticItemPrice(inventoryItem._tpl) *
|
|
(this.traderHelper.getLoyaltyLevel(traderId, pmcData).insurance_price_coef / 100);
|
|
|
|
return Math.ceil(price);
|
|
}
|
|
|
|
/**
|
|
* Returns the ID that should be used for a root-level Item's parentId property value within in the context of insurance.
|
|
* @param sessionID Players id
|
|
* @returns The root item Id.
|
|
*/
|
|
public getRootItemParentID(sessionID: string): string {
|
|
// Try to use the equipment id from the profile. I'm not sure this is strictly required, but it feels neat.
|
|
return this.saveServer.getProfile(sessionID)?.characters?.pmc?.Inventory?.equipment ?? this.hashUtil.generate();
|
|
}
|
|
}
|