2023-03-03 16:23:46 +01:00
import { inject , injectable } from "tsyringe" ;
2023-10-19 19:21:17 +02:00
import { ApplicationContext } from "@spt-aki/context/ApplicationContext" ;
import { ContextVariableType } from "@spt-aki/context/ContextVariableType" ;
import { PlayerScavGenerator } from "@spt-aki/generators/PlayerScavGenerator" ;
import { HealthHelper } from "@spt-aki/helpers/HealthHelper" ;
import { InRaidHelper } from "@spt-aki/helpers/InRaidHelper" ;
import { ItemHelper } from "@spt-aki/helpers/ItemHelper" ;
import { ProfileHelper } from "@spt-aki/helpers/ProfileHelper" ;
import { QuestHelper } from "@spt-aki/helpers/QuestHelper" ;
import { TraderHelper } from "@spt-aki/helpers/TraderHelper" ;
import { ILocationBase } from "@spt-aki/models/eft/common/ILocationBase" ;
import { IPmcData } from "@spt-aki/models/eft/common/IPmcData" ;
import { BodyPartHealth } from "@spt-aki/models/eft/common/tables/IBotBase" ;
import { Item } from "@spt-aki/models/eft/common/tables/IItem" ;
import { IRegisterPlayerRequestData } from "@spt-aki/models/eft/inRaid/IRegisterPlayerRequestData" ;
import { ISaveProgressRequestData } from "@spt-aki/models/eft/inRaid/ISaveProgressRequestData" ;
import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes" ;
import { PlayerRaidEndState } from "@spt-aki/models/enums/PlayerRaidEndState" ;
import { QuestStatus } from "@spt-aki/models/enums/QuestStatus" ;
import { Traders } from "@spt-aki/models/enums/Traders" ;
import { IAirdropConfig } from "@spt-aki/models/spt/config/IAirdropConfig" ;
import { IInRaidConfig } from "@spt-aki/models/spt/config/IInRaidConfig" ;
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 { InsuranceService } from "@spt-aki/services/InsuranceService" ;
import { MatchBotDetailsCacheService } from "@spt-aki/services/MatchBotDetailsCacheService" ;
import { PmcChatResponseService } from "@spt-aki/services/PmcChatResponseService" ;
import { JsonUtil } from "@spt-aki/utils/JsonUtil" ;
import { TimeUtil } from "@spt-aki/utils/TimeUtil" ;
2023-03-03 16:23:46 +01:00
/ * *
* Logic for handling In Raid callbacks
* /
@injectable ( )
export class InraidController
{
protected airdropConfig : IAirdropConfig ;
protected inraidConfig : IInRaidConfig ;
constructor (
@inject ( "WinstonLogger" ) protected logger : ILogger ,
@inject ( "SaveServer" ) protected saveServer : SaveServer ,
@inject ( "JsonUtil" ) protected jsonUtil : JsonUtil ,
@inject ( "TimeUtil" ) protected timeUtil : TimeUtil ,
@inject ( "DatabaseServer" ) protected databaseServer : DatabaseServer ,
2023-03-08 14:26:32 +01:00
@inject ( "PmcChatResponseService" ) protected pmcChatResponseService : PmcChatResponseService ,
2023-03-22 11:25:34 +01:00
@inject ( "MatchBotDetailsCacheService" ) protected matchBotDetailsCacheService : MatchBotDetailsCacheService ,
2023-03-03 16:23:46 +01:00
@inject ( "QuestHelper" ) protected questHelper : QuestHelper ,
@inject ( "ItemHelper" ) protected itemHelper : ItemHelper ,
@inject ( "ProfileHelper" ) protected profileHelper : ProfileHelper ,
@inject ( "PlayerScavGenerator" ) protected playerScavGenerator : PlayerScavGenerator ,
@inject ( "HealthHelper" ) protected healthHelper : HealthHelper ,
@inject ( "TraderHelper" ) protected traderHelper : TraderHelper ,
@inject ( "InsuranceService" ) protected insuranceService : InsuranceService ,
@inject ( "InRaidHelper" ) protected inRaidHelper : InRaidHelper ,
@inject ( "ApplicationContext" ) protected applicationContext : ApplicationContext ,
@inject ( "ConfigServer" ) protected configServer : ConfigServer
)
{
this . airdropConfig = this . configServer . getConfig ( ConfigTypes . AIRDROP ) ;
this . inraidConfig = this . configServer . getConfig ( ConfigTypes . IN_RAID ) ;
}
/ * *
* Save locationId to active profiles inraid object AND app context
* @param sessionID Session id
* @param info Register player request
* /
public addPlayer ( sessionID : string , info : IRegisterPlayerRequestData ) : void
{
this . applicationContext . addValue ( ContextVariableType . REGISTER_PLAYER_REQUEST , info ) ;
this . saveServer . getProfile ( sessionID ) . inraid . location = info . locationId ;
}
/ * *
2023-07-15 15:49:25 +02:00
* Handle raid / profile / save
2023-03-03 16:23:46 +01:00
* Save profile state to disk
* Handles pmc / pscav
* @param offraidData post - raid request data
* @param sessionID Session id
* /
public savePostRaidProgress ( offraidData : ISaveProgressRequestData , sessionID : string ) : void
{
2023-10-11 11:47:54 +02:00
this . logger . debug ( ` Raid outcome: ${ offraidData . exit } ` ) ;
2023-10-10 13:03:20 +02:00
2023-03-03 16:23:46 +01:00
if ( ! this . inraidConfig . save . loot )
{
return ;
}
if ( offraidData . isPlayerScav )
{
this . savePlayerScavProgress ( sessionID , offraidData ) ;
}
else
{
this . savePmcProgress ( sessionID , offraidData ) ;
}
}
/ * *
* Handle updating player profile post - pmc raid
2023-10-11 11:47:54 +02:00
* @param sessionID Session id
* @param postRaidRequest Post - raid data
2023-03-03 16:23:46 +01:00
* /
2023-10-11 11:47:54 +02:00
protected savePmcProgress ( sessionID : string , postRaidRequest : ISaveProgressRequestData ) : void
2023-03-03 16:23:46 +01:00
{
2023-10-11 11:47:54 +02:00
const serverProfile = this . saveServer . getProfile ( sessionID ) ;
const locationName = serverProfile . inraid . location . toLowerCase ( ) ;
2023-03-03 16:23:46 +01:00
const map : ILocationBase = this . databaseServer . getTables ( ) . locations [ locationName ] . base ;
2023-10-10 13:03:20 +02:00
const mapHasInsuranceEnabled = map . Insurance ;
2023-03-03 16:23:46 +01:00
2023-10-11 11:47:54 +02:00
let serverPmcData = serverProfile . characters . pmc ;
const isDead = this . isPlayerDead ( postRaidRequest . exit ) ;
const preRaidGear = this . inRaidHelper . getPlayerGear ( serverPmcData . Inventory . items ) ;
serverProfile . inraid . character = "pmc" ;
2023-03-03 16:23:46 +01:00
2023-10-11 11:47:54 +02:00
serverPmcData = this . inRaidHelper . updateProfileBaseStats ( serverPmcData , postRaidRequest , sessionID ) ;
2023-03-03 16:23:46 +01:00
// Check for exit status
2023-10-11 11:47:54 +02:00
this . markOrRemoveFoundInRaidItems ( postRaidRequest ) ;
2023-03-03 16:23:46 +01:00
2023-10-11 11:47:54 +02:00
postRaidRequest . profile . Inventory . items = this . itemHelper . replaceIDs ( postRaidRequest . profile , postRaidRequest . profile . Inventory . items , serverPmcData . InsuredItems , postRaidRequest . profile . Inventory . fastPanel ) ;
this . inRaidHelper . addUpdToMoneyFromRaid ( postRaidRequest . profile . Inventory . items ) ;
2023-03-03 16:23:46 +01:00
2023-10-11 11:47:54 +02:00
// Purge profile of equipment/container items
serverPmcData = this . inRaidHelper . setInventory ( sessionID , serverPmcData , postRaidRequest . profile ) ;
this . healthHelper . saveVitality ( serverPmcData , postRaidRequest . health , sessionID ) ;
2023-03-03 16:23:46 +01:00
2023-03-03 18:53:28 +01:00
// Remove inventory if player died and send insurance items
2023-10-10 13:03:20 +02:00
if ( mapHasInsuranceEnabled )
2023-03-03 16:23:46 +01:00
{
2023-10-11 11:47:54 +02:00
this . insuranceService . storeLostGear ( serverPmcData , postRaidRequest , preRaidGear , sessionID , isDead ) ;
2023-03-03 18:53:28 +01:00
}
else
{
2023-08-07 23:43:00 +02:00
this . insuranceService . sendLostInsuranceMessage ( sessionID , locationName ) ;
2023-03-03 16:23:46 +01:00
}
2023-10-10 13:03:20 +02:00
// Edge case - Handle usec players leaving lighthouse with Rogues angry at them
2023-10-11 11:47:54 +02:00
if ( locationName === "lighthouse" && postRaidRequest . profile . Info . Side . toLowerCase ( ) === "usec" )
2023-10-10 13:03:20 +02:00
{
// Decrement counter if it exists, don't go below 0
2023-10-11 11:47:54 +02:00
const remainingCounter = serverPmcData ? . Stats . Eft . OverallCounters . Items . find ( x = > x . Key . includes ( "UsecRaidRemainKills" ) ) ;
2023-10-10 13:03:20 +02:00
if ( remainingCounter ? . Value > 0 )
{
remainingCounter . Value -- ;
}
}
2023-03-03 16:23:46 +01:00
if ( isDead )
{
2023-10-11 11:47:54 +02:00
this . pmcChatResponseService . sendKillerResponse ( sessionID , serverPmcData , postRaidRequest . profile . Stats . Eft . Aggressor ) ;
2023-03-22 11:25:34 +01:00
this . matchBotDetailsCacheService . clearCache ( ) ;
2023-10-11 11:47:54 +02:00
serverPmcData = this . performPostRaidActionsWhenDead ( postRaidRequest , serverPmcData , mapHasInsuranceEnabled , preRaidGear , sessionID ) ;
2023-03-03 16:23:46 +01:00
}
2023-10-11 11:47:54 +02:00
const victims = postRaidRequest . profile . Stats . Eft . Victims . filter ( x = > [ "sptbear" , "sptusec" ] . includes ( x . Role . toLowerCase ( ) ) ) ;
2023-03-08 14:26:32 +01:00
if ( victims ? . length > 0 )
{
2023-10-11 11:47:54 +02:00
this . pmcChatResponseService . sendVictimResponse ( sessionID , victims , serverPmcData ) ;
2023-03-08 14:26:32 +01:00
}
2023-10-10 13:03:20 +02:00
if ( mapHasInsuranceEnabled )
2023-03-03 16:23:46 +01:00
{
2023-10-11 11:47:54 +02:00
this . insuranceService . sendInsuredItems ( serverPmcData , sessionID , map . Id ) ;
2023-03-03 16:23:46 +01:00
}
}
/ * *
* Make changes to pmc profile after they ' ve died in raid ,
* Alter bodypart hp , handle insurance , delete inventory items , remove carried quest items
* @param postRaidSaveRequest Post - raid save request
* @param pmcData Pmc profile
* @param insuranceEnabled Is insurance enabled
* @param preRaidGear Gear player had before raid
* @param sessionID Session id
* @returns Updated profile object
* /
protected performPostRaidActionsWhenDead ( postRaidSaveRequest : ISaveProgressRequestData , pmcData : IPmcData , insuranceEnabled : boolean , preRaidGear : Item [ ] , sessionID : string ) : IPmcData
{
this . updatePmcHealthPostRaid ( postRaidSaveRequest , pmcData ) ;
this . inRaidHelper . deleteInventory ( pmcData , sessionID ) ;
if ( this . inRaidHelper . removeQuestItemsOnDeath ( ) )
{
2023-10-10 13:03:20 +02:00
// Find and remove the completed condition from profile if player died, otherwise quest is stuck in limbo and quest items cannot be picked up again
const allQuests = this . questHelper . getQuestsFromDb ( ) ;
const activeQuestIdsInProfile = pmcData . Quests . filter ( x = > ! [ QuestStatus . AvailableForStart , QuestStatus . Success , QuestStatus . Expired ] . includes ( x . status ) ) . map ( x = > x . qid ) ;
for ( const questItem of postRaidSaveRequest . profile . Stats . Eft . CarriedQuestItems )
2023-03-03 16:23:46 +01:00
{
2023-10-10 13:03:20 +02:00
// Get quest/find condition for carried quest item
const questAndFindItemConditionId = this . questHelper . getFindItemConditionByQuestItem ( questItem , activeQuestIdsInProfile , allQuests ) ;
if ( questAndFindItemConditionId )
{
this . profileHelper . removeCompletedQuestConditionFromProfile ( pmcData , questAndFindItemConditionId ) ;
}
2023-03-03 16:23:46 +01:00
}
2023-10-10 13:03:20 +02:00
// Empty out stored quest items from player inventory
pmcData . Stats . Eft . CarriedQuestItems = [ ] ;
2023-03-03 16:23:46 +01:00
}
return pmcData ;
}
/ * *
* Adjust player characters bodypart hp post - raid
* @param postRaidSaveRequest post raid data
* @param pmcData player profile
* /
protected updatePmcHealthPostRaid ( postRaidSaveRequest : ISaveProgressRequestData , pmcData : IPmcData ) : void
{
switch ( postRaidSaveRequest . exit )
{
2023-07-24 17:30:03 +02:00
case PlayerRaidEndState . LEFT . toString ( ) :
2023-03-03 16:23:46 +01:00
// Naughty pmc left the raid early!
this . reducePmcHealthToPercent ( pmcData , 0.01 ) ; // 1%
break ;
2023-07-24 17:30:03 +02:00
case PlayerRaidEndState . MISSING_IN_ACTION . toString ( ) :
2023-03-03 16:23:46 +01:00
// Didn't reach exit in time
this . reducePmcHealthToPercent ( pmcData , 0.3 ) ; // 30%
break ;
default :
// Left raid properly, don't make any adjustments
break ;
}
}
/ * *
* Reduce body part hp to % of max
* @param pmcData profile to edit
* @param multipler multipler to apply to max health
* /
protected reducePmcHealthToPercent ( pmcData : IPmcData , multipler : number ) : void
{
for ( const bodyPart of Object . values ( pmcData . Health . BodyParts ) )
{
( < BodyPartHealth > bodyPart ) . Health . Current = ( < BodyPartHealth > bodyPart ) . Health . Maximum * multipler ;
}
}
/ * *
* Handle updating the profile post - pscav raid
2023-10-11 11:47:54 +02:00
* @param sessionID Session id
* @param postRaidRequest Post - raid data of raid
2023-03-03 16:23:46 +01:00
* /
2023-10-11 11:47:54 +02:00
protected savePlayerScavProgress ( sessionID : string , postRaidRequest : ISaveProgressRequestData ) : void
2023-03-03 16:23:46 +01:00
{
const pmcData = this . profileHelper . getPmcProfile ( sessionID ) ;
let scavData = this . profileHelper . getScavProfile ( sessionID ) ;
2023-10-11 11:47:54 +02:00
const isDead = this . isPlayerDead ( postRaidRequest . exit ) ;
2023-03-03 16:23:46 +01:00
this . saveServer . getProfile ( sessionID ) . inraid . character = "scav" ;
2023-10-11 11:47:54 +02:00
scavData = this . inRaidHelper . updateProfileBaseStats ( scavData , postRaidRequest , sessionID ) ;
2023-03-03 16:23:46 +01:00
2023-10-17 23:04:19 +02:00
// Completing scav quests create ConditionCounters, these values need to be transported to the PMC profile
if ( this . profileHasConditionCounters ( scavData ) )
{
// Scav quest progress needs to be moved to pmc so player can see it in menu / hand them in
this . migrateScavQuestProgressToPmcProfile ( scavData , pmcData ) ;
}
2023-03-03 16:23:46 +01:00
// Check for exit status
2023-10-11 11:47:54 +02:00
this . markOrRemoveFoundInRaidItems ( postRaidRequest ) ;
2023-03-03 16:23:46 +01:00
2023-10-11 11:47:54 +02:00
postRaidRequest . profile . Inventory . items = this . itemHelper . replaceIDs ( postRaidRequest . profile , postRaidRequest . profile . Inventory . items , pmcData . InsuredItems , postRaidRequest . profile . Inventory . fastPanel ) ;
this . inRaidHelper . addUpdToMoneyFromRaid ( postRaidRequest . profile . Inventory . items ) ;
2023-03-03 16:23:46 +01:00
2023-10-11 11:47:54 +02:00
this . handlePostRaidPlayerScavProcess ( scavData , sessionID , postRaidRequest , pmcData , isDead ) ;
2023-03-03 16:23:46 +01:00
}
2023-10-17 23:04:19 +02:00
/ * *
* Does provided profile contain any condition counters
* @param profile Profile to check for condition counters
* @returns
* /
protected profileHasConditionCounters ( profile : IPmcData ) : boolean
{
if ( ! profile . ConditionCounters . Counters )
{
return false ;
}
return profile . ConditionCounters . Counters . length > 0 ;
}
protected migrateScavQuestProgressToPmcProfile ( scavProfile : IPmcData , pmcProfile : IPmcData ) : void
{
for ( const quest of scavProfile . Quests )
{
const pmcQuest = pmcProfile . Quests . find ( x = > x . qid === quest . qid ) ;
if ( ! pmcQuest )
{
this . logger . warning ( ` No PMC quest found for ID: ${ quest . qid } ` ) ;
continue ;
}
// Post-raid status is enum word e.g. `Started` but pmc quest status is number e.g. 2
// Status values mismatch or statusTimers counts mismatch
if ( quest . status !== < any > QuestStatus [ pmcQuest . status ] || quest . statusTimers . length !== pmcQuest . statusTimers . length )
{
this . logger . warning ( ` Quest: ${ quest . qid } found in PMC profile has different status/statustimer. Scav: ${ quest . status } vs PMC: ${ pmcQuest . status } ` ) ;
pmcQuest . status = < any > QuestStatus [ quest . status ] ;
pmcQuest . statusTimers = quest . statusTimers ;
}
}
// Loop over all scav counters and add into pmc profile
for ( const scavCounter of scavProfile . ConditionCounters . Counters )
{
this . logger . warning ( ` Processing counter: ${ scavCounter . id } value: ${ scavCounter . value } quest: ${ scavCounter . qid } ` ) ;
const counterInPmcProfile = pmcProfile . ConditionCounters . Counters . find ( x = > x . id === scavCounter . id ) ;
if ( ! counterInPmcProfile )
{
// Doesn't exist yet, push it straight in
pmcProfile . ConditionCounters . Counters . push ( scavCounter ) ;
continue ;
}
this . logger . warning ( ` Counter id: ${ scavCounter . id } already exists in pmc profile! with value: ${ counterInPmcProfile . value } for quest: ${ counterInPmcProfile . qid } ` ) ;
// Only adjust counter value if its changed
if ( counterInPmcProfile . value !== scavCounter . value )
{
2023-10-19 23:55:53 +02:00
this . logger . warning ( ` OVERWRITING with values: ${ scavCounter . value } quest: ${ scavCounter . qid } ` ) ;
2023-10-17 23:04:19 +02:00
counterInPmcProfile . value = scavCounter . value ;
}
}
}
2023-03-03 16:23:46 +01:00
/ * *
* Is the player dead after a raid - dead is anything other than "survived" / "runner"
* @param statusOnExit exit value from offraidData object
* @returns true if dead
* /
2023-07-24 17:30:03 +02:00
protected isPlayerDead ( statusOnExit : PlayerRaidEndState ) : boolean
2023-03-03 16:23:46 +01:00
{
2023-07-24 17:30:03 +02:00
return ( statusOnExit !== PlayerRaidEndState . SURVIVED && statusOnExit !== PlayerRaidEndState . RUNNER ) ;
2023-03-03 16:23:46 +01:00
}
/ * *
* Mark inventory items as FiR if player survived raid , otherwise remove FiR from them
* @param offraidData Save Progress Request
* /
2023-10-10 13:03:20 +02:00
protected markOrRemoveFoundInRaidItems ( offraidData : ISaveProgressRequestData ) : void
2023-03-03 16:23:46 +01:00
{
2023-10-10 13:03:20 +02:00
if ( offraidData . exit !== PlayerRaidEndState . SURVIVED )
2023-03-03 16:23:46 +01:00
{
// Remove FIR status if the player havn't survived
offraidData . profile = this . inRaidHelper . removeSpawnedInSessionPropertyFromItems ( offraidData . profile ) ;
}
}
/ * *
* Update profile after player completes scav raid
* @param scavData Scav profile
* @param sessionID Session id
* @param offraidData Post - raid save request
* @param pmcData Pmc profile
* @param isDead Is player dead
* /
protected handlePostRaidPlayerScavProcess ( scavData : IPmcData , sessionID : string , offraidData : ISaveProgressRequestData , pmcData : IPmcData , isDead : boolean ) : void
{
// Update scav profile inventory
scavData = this . inRaidHelper . setInventory ( sessionID , scavData , offraidData . profile ) ;
// Reset scav hp and save to json
this . healthHelper . resetVitality ( sessionID ) ;
this . saveServer . getProfile ( sessionID ) . characters . scav = scavData ;
// Scav karma
this . handlePostRaidPlayerScavKarmaChanges ( pmcData , offraidData , scavData , sessionID ) ;
// Scav died, regen scav loadout and set timer
if ( isDead )
{
this . playerScavGenerator . generate ( sessionID ) ;
}
// Update last played property
pmcData . Info . LastTimePlayedAsSavage = this . timeUtil . getTimestamp ( ) ;
this . saveServer . saveProfile ( sessionID ) ;
}
/ * *
* Update profile with scav karma values based on in - raid actions
* @param pmcData Pmc profile
* @param offraidData Post - raid save request
* @param scavData Scav profile
* @param sessionID Session id
* /
protected handlePostRaidPlayerScavKarmaChanges ( pmcData : IPmcData , offraidData : ISaveProgressRequestData , scavData : IPmcData , sessionID : string ) : void
{
const fenceId = Traders . FENCE ;
let fenceStanding = Number ( pmcData . TradersInfo [ fenceId ] . standing ) ;
2023-05-30 14:36:15 +02:00
this . logger . debug ( ` Old fence standing: ${ fenceStanding } ` ) ;
2023-10-10 13:03:20 +02:00
fenceStanding = this . inRaidHelper . calculateFenceStandingChangeFromKills ( fenceStanding , offraidData . profile . Stats . Eft . Victims ) ;
2023-03-03 16:23:46 +01:00
// Successful extract with scav adds 0.01 standing
2023-07-24 17:30:03 +02:00
if ( offraidData . exit === PlayerRaidEndState . SURVIVED )
2023-03-03 16:23:46 +01:00
{
fenceStanding += this . inraidConfig . scavExtractGain ;
}
2023-10-10 13:03:20 +02:00
2023-03-03 16:23:46 +01:00
// Make standing changes to pmc profile
2023-10-10 13:03:20 +02:00
pmcData . TradersInfo [ fenceId ] . standing = Math . min ( Math . max ( fenceStanding , - 7 ) , 15 ) ; // Ensure it stays between -7 and 15
this . logger . debug ( ` New fence standing: ${ pmcData . TradersInfo [ fenceId ] . standing } ` ) ;
this . traderHelper . lvlUp ( fenceId , pmcData ) ;
2023-03-03 16:23:46 +01:00
pmcData . TradersInfo [ fenceId ] . loyaltyLevel = Math . max ( pmcData . TradersInfo [ fenceId ] . loyaltyLevel , 1 ) ;
}
/ * *
* Get the inraid config from configs / inraid . json
* @returns InRaid Config
* /
public getInraidConfig ( ) : IInRaidConfig
{
return this . inraidConfig ;
}
/ * *
* Get airdrop config from configs / airdrop . json
* @returns Airdrop config
* /
public getAirdropConfig ( ) : IAirdropConfig
{
return this . airdropConfig ;
}
}