0
0
mirror of https://github.com/sp-tarkov/server.git synced 2025-02-13 09:50:43 -05:00
server/project/src/services/InsuranceService.ts

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();
}
}