import { ApplicationContext } from "@spt/context/ApplicationContext"; import { LocationLootGenerator } from "@spt/generators/LocationLootGenerator"; import { LootGenerator } from "@spt/generators/LootGenerator"; import { PlayerScavGenerator } from "@spt/generators/PlayerScavGenerator"; import { HealthHelper } from "@spt/helpers/HealthHelper"; import { InRaidHelper } from "@spt/helpers/InRaidHelper"; 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 { IEndOfflineRaidRequestData } from "@spt/models/eft/match/IEndOfflineRaidRequestData"; import { ConfigTypes } from "@spt/models/enums/ConfigTypes"; import { MessageType } from "@spt/models/enums/MessageType"; import { Traders } from "@spt/models/enums/Traders"; import { IHideoutConfig } from "@spt/models/spt/config/IHideoutConfig"; import { IInRaidConfig } from "@spt/models/spt/config/IInRaidConfig"; import { ILocationConfig } from "@spt/models/spt/config/ILocationConfig"; import { IMatchConfig } from "@spt/models/spt/config/IMatchConfig"; import { IRagfairConfig } from "@spt/models/spt/config/IRagfairConfig"; import { ITraderConfig } from "@spt/models/spt/config/ITraderConfig"; import type { ILogger } from "@spt/models/spt/utils/ILogger"; import { ConfigServer } from "@spt/servers/ConfigServer"; import { SaveServer } from "@spt/servers/SaveServer"; import { BotGenerationCacheService } from "@spt/services/BotGenerationCacheService"; import { BotLootCacheService } from "@spt/services/BotLootCacheService"; import { DatabaseService } from "@spt/services/DatabaseService"; import { InsuranceService } from "@spt/services/InsuranceService"; import { LocalisationService } from "@spt/services/LocalisationService"; import { MailSendService } from "@spt/services/MailSendService"; import { MatchBotDetailsCacheService } from "@spt/services/MatchBotDetailsCacheService"; import { PmcChatResponseService } from "@spt/services/PmcChatResponseService"; import { RaidTimeAdjustmentService } from "@spt/services/RaidTimeAdjustmentService"; import { HashUtil } from "@spt/utils/HashUtil"; import { RandomUtil } from "@spt/utils/RandomUtil"; import { TimeUtil } from "@spt/utils/TimeUtil"; import type { ICloner } from "@spt/utils/cloners/ICloner"; import { inject } from "tsyringe"; export class LegacyLocationLifecycleService { protected matchConfig: IMatchConfig; protected inRaidConfig: IInRaidConfig; protected traderConfig: ITraderConfig; protected ragfairConfig: IRagfairConfig; protected hideoutConfig: IHideoutConfig; protected locationConfig: ILocationConfig; constructor( @inject("PrimaryLogger") protected logger: ILogger, @inject("HashUtil") protected hashUtil: HashUtil, @inject("SaveServer") protected saveServer: SaveServer, @inject("TimeUtil") protected timeUtil: TimeUtil, @inject("RandomUtil") protected randomUtil: RandomUtil, @inject("ProfileHelper") protected profileHelper: ProfileHelper, @inject("DatabaseService") protected databaseService: DatabaseService, @inject("InRaidHelper") protected inRaidHelper: InRaidHelper, @inject("HealthHelper") protected healthHelper: HealthHelper, @inject("MatchBotDetailsCacheService") protected matchBotDetailsCacheService: MatchBotDetailsCacheService, @inject("PmcChatResponseService") protected pmcChatResponseService: PmcChatResponseService, @inject("PlayerScavGenerator") protected playerScavGenerator: PlayerScavGenerator, @inject("TraderHelper") protected traderHelper: TraderHelper, @inject("LocalisationService") protected localisationService: LocalisationService, @inject("InsuranceService") protected insuranceService: InsuranceService, @inject("BotLootCacheService") protected botLootCacheService: BotLootCacheService, @inject("ConfigServer") protected configServer: ConfigServer, @inject("BotGenerationCacheService") protected botGenerationCacheService: BotGenerationCacheService, @inject("MailSendService") protected mailSendService: MailSendService, @inject("RaidTimeAdjustmentService") protected raidTimeAdjustmentService: RaidTimeAdjustmentService, @inject("LootGenerator") protected lootGenerator: LootGenerator, @inject("ApplicationContext") protected applicationContext: ApplicationContext, @inject("LocationLootGenerator") protected locationLootGenerator: LocationLootGenerator, @inject("PrimaryCloner") protected cloner: ICloner, ) { this.traderConfig = this.configServer.getConfig(ConfigTypes.TRADER); this.ragfairConfig = this.configServer.getConfig(ConfigTypes.RAGFAIR); this.hideoutConfig = this.configServer.getConfig(ConfigTypes.HIDEOUT); this.locationConfig = this.configServer.getConfig(ConfigTypes.LOCATION); } /** * Handle client/match/offline/end * @deprecated */ public endOfflineRaid(info: IEndOfflineRaidRequestData, sessionId: string): void { const pmcData: IPmcData = this.profileHelper.getPmcProfile(sessionId); const extractName = info.exitName; // Save time spent in raid pmcData.Stats.Eft.TotalInGameTime += info.raidSeconds; // Clean up cached bots now raid is over this.botGenerationCacheService.clearStoredBots(); // Clear bot loot cache this.botLootCacheService.clearCache(); if (this.extractWasViaCar(extractName)) { this.handleCarExtract(extractName, pmcData, sessionId); } if (extractName && this.extractWasViaCoop(extractName) && this.traderConfig.fence.coopExtractGift.sendGift) { this.handleCoopExtract(sessionId, pmcData, extractName); this.sendCoopTakenFenceMessage(sessionId); } } /** * Handle when a player extracts using a car - Add rep to fence * @param extractName name of the extract used * @param pmcData Player profile * @param sessionId Session id */ protected handleCarExtract(extractName: string, pmcData: IPmcData, sessionId: string): void { // Ensure key exists for extract if (!(extractName in pmcData.CarExtractCounts)) { pmcData.CarExtractCounts[extractName] = 0; } // Increment extract count value pmcData.CarExtractCounts[extractName] += 1; // Not exact replica of Live behaviour // Simplified for now, no real reason to do the whole (unconfirmed) extra 0.01 standing per day regeneration mechanic const newFenceStanding = this.getFenceStandingAfterExtract( pmcData, this.inRaidConfig.carExtractBaseStandingGain, pmcData.CarExtractCounts[extractName], ); const fenceId: string = Traders.FENCE; pmcData.TradersInfo[fenceId].standing = newFenceStanding; // Check if new standing has leveled up trader this.traderHelper.lvlUp(fenceId, pmcData); pmcData.TradersInfo[fenceId].loyaltyLevel = Math.max(pmcData.TradersInfo[fenceId].loyaltyLevel, 1); this.logger.debug( `Car extract: ${extractName} used, total times taken: ${pmcData.CarExtractCounts[extractName]}`, ); // Copy updated fence rep values into scav profile to ensure consistency const scavData: IPmcData = this.profileHelper.getScavProfile(sessionId); scavData.TradersInfo[fenceId].standing = pmcData.TradersInfo[fenceId].standing; scavData.TradersInfo[fenceId].loyaltyLevel = pmcData.TradersInfo[fenceId].loyaltyLevel; } /** * Get the fence rep gain from using a car or coop extract * @param pmcData Profile * @param baseGain amount gained for the first extract * @param extractCount Number of times extract was taken * @returns Fence standing after taking extract */ protected getFenceStandingAfterExtract(pmcData: IPmcData, baseGain: number, extractCount: number): number { // Get current standing const fenceId: string = Traders.FENCE; let fenceStanding = Number(pmcData.TradersInfo[fenceId].standing); // get standing after taking extract x times, x.xx format, gain from extract can be no smaller than 0.01 fenceStanding += Math.max(baseGain / extractCount, 0.01); // Ensure fence loyalty level is not above/below the range -7 to 15 const newFenceStanding = Math.min(Math.max(fenceStanding, -7), 15); this.logger.debug(`Old vs new fence standing: ${pmcData.TradersInfo[fenceId].standing}, ${newFenceStanding}`); return Number(newFenceStanding.toFixed(2)); } /** * Was extract by car * @param extractName name of extract * @returns true if car extract */ protected extractWasViaCar(extractName: string): boolean { // exit name is undefined on death if (!extractName) { return false; } if (extractName.toLowerCase().includes("v-ex")) { return true; } return this.inRaidConfig.carExtracts.includes(extractName.trim()); } /** * Did player take a COOP extract * @param extractName Name of extract player took * @returns True if coop extract */ protected extractWasViaCoop(extractName: string): boolean { // No extract name, not a coop extract if (!extractName) { return false; } return this.inRaidConfig.coopExtracts.includes(extractName.trim()); } /** * Handle when a player extracts using a coop extract - add rep to fence * @param sessionId Session/player id * @param pmcData Profile * @param extractName Name of extract taken */ protected handleCoopExtract(sessionId: string, pmcData: IPmcData, extractName: string): void { if (!pmcData.CoopExtractCounts) { pmcData.CoopExtractCounts = {}; } // Ensure key exists for extract if (!(extractName in pmcData.CoopExtractCounts)) { pmcData.CoopExtractCounts[extractName] = 0; } // Increment extract count value pmcData.CoopExtractCounts[extractName] += 1; // Get new fence standing value const newFenceStanding = this.getFenceStandingAfterExtract( pmcData, this.inRaidConfig.coopExtractBaseStandingGain, pmcData.CoopExtractCounts[extractName], ); const fenceId: string = Traders.FENCE; pmcData.TradersInfo[fenceId].standing = newFenceStanding; // Check if new standing has leveled up trader this.traderHelper.lvlUp(fenceId, pmcData); pmcData.TradersInfo[fenceId].loyaltyLevel = Math.max(pmcData.TradersInfo[fenceId].loyaltyLevel, 1); // Copy updated fence rep values into scav profile to ensure consistency const scavData: IPmcData = this.profileHelper.getScavProfile(sessionId); scavData.TradersInfo[fenceId].standing = pmcData.TradersInfo[fenceId].standing; scavData.TradersInfo[fenceId].loyaltyLevel = pmcData.TradersInfo[fenceId].loyaltyLevel; } protected sendCoopTakenFenceMessage(sessionId: string): void { // Generate reward for taking coop extract const loot = this.lootGenerator.createRandomLoot(this.traderConfig.fence.coopExtractGift); const mailableLoot: IItem[] = []; const parentId = this.hashUtil.generate(); for (const item of loot) { item.parentId = parentId; mailableLoot.push(item); } // Send message from fence giving player reward generated above this.mailSendService.sendLocalisedNpcMessageToPlayer( sessionId, this.traderHelper.getTraderById(Traders.FENCE), MessageType.MESSAGE_WITH_ITEMS, this.randomUtil.getArrayValue(this.traderConfig.fence.coopExtractGift.messageLocaleIds), mailableLoot, this.timeUtil.getHoursAsSeconds(this.traderConfig.fence.coopExtractGift.giftExpiryHours), ); } }