2023-03-03 15:23:46 +00:00
|
|
|
import { inject, injectable } from "tsyringe";
|
2024-05-21 17:59:04 +00:00
|
|
|
import { IPmcData } from "@spt/models/eft/common/IPmcData";
|
2024-07-05 13:32:46 +01:00
|
|
|
import { BodyPartsHealth, Health } from "@spt/models/eft/common/tables/IBotBase";
|
2024-05-21 17:59:04 +00:00
|
|
|
import { ISyncHealthRequestData } from "@spt/models/eft/health/ISyncHealthRequestData";
|
|
|
|
import { Effects, ISptProfile } from "@spt/models/eft/profile/ISptProfile";
|
|
|
|
import { ConfigTypes } from "@spt/models/enums/ConfigTypes";
|
|
|
|
import { IHealthConfig } from "@spt/models/spt/config/IHealthConfig";
|
|
|
|
import { ILogger } from "@spt/models/spt/utils/ILogger";
|
|
|
|
import { ConfigServer } from "@spt/servers/ConfigServer";
|
|
|
|
import { SaveServer } from "@spt/servers/SaveServer";
|
|
|
|
import { ICloner } from "@spt/utils/cloners/ICloner";
|
|
|
|
import { TimeUtil } from "@spt/utils/TimeUtil";
|
2023-03-03 15:23:46 +00:00
|
|
|
|
|
|
|
@injectable()
|
|
|
|
export class HealthHelper
|
|
|
|
{
|
|
|
|
protected healthConfig: IHealthConfig;
|
|
|
|
|
|
|
|
constructor(
|
2024-05-28 14:04:20 +00:00
|
|
|
@inject("PrimaryLogger") protected logger: ILogger,
|
2023-03-03 15:23:46 +00:00
|
|
|
@inject("TimeUtil") protected timeUtil: TimeUtil,
|
|
|
|
@inject("SaveServer") protected saveServer: SaveServer,
|
2023-11-15 20:35:05 -05:00
|
|
|
@inject("ConfigServer") protected configServer: ConfigServer,
|
2024-05-28 14:04:20 +00:00
|
|
|
@inject("PrimaryCloner") protected cloner: ICloner,
|
2023-03-03 15:23:46 +00:00
|
|
|
)
|
|
|
|
{
|
|
|
|
this.healthConfig = this.configServer.getConfig(ConfigTypes.HEALTH);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Resets the profiles vitality/health and vitality/effects properties to their defaults
|
|
|
|
* @param sessionID Session Id
|
|
|
|
* @returns updated profile
|
|
|
|
*/
|
2024-05-21 17:59:04 +00:00
|
|
|
public resetVitality(sessionID: string): ISptProfile
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
const profile = this.saveServer.getProfile(sessionID);
|
|
|
|
|
2023-11-15 20:35:05 -05:00
|
|
|
if (!profile.vitality)
|
2024-05-17 15:32:41 -04:00
|
|
|
{
|
|
|
|
// Occurs on newly created profiles
|
2024-05-27 20:06:07 +00:00
|
|
|
profile.vitality = { health: undefined!, effects: undefined! };
|
2023-03-03 15:23:46 +00:00
|
|
|
}
|
|
|
|
profile.vitality.health = {
|
|
|
|
Hydration: 0,
|
|
|
|
Energy: 0,
|
|
|
|
Temperature: 0,
|
|
|
|
Head: 0,
|
|
|
|
Chest: 0,
|
|
|
|
Stomach: 0,
|
|
|
|
LeftArm: 0,
|
|
|
|
RightArm: 0,
|
|
|
|
LeftLeg: 0,
|
2023-11-15 20:35:05 -05:00
|
|
|
RightLeg: 0,
|
2023-03-03 15:23:46 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
profile.vitality.effects = {
|
|
|
|
Head: {},
|
|
|
|
Chest: {},
|
|
|
|
Stomach: {},
|
|
|
|
LeftArm: {},
|
|
|
|
RightArm: {},
|
|
|
|
LeftLeg: {},
|
2023-11-15 20:35:05 -05:00
|
|
|
RightLeg: {},
|
2023-03-03 15:23:46 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
return profile;
|
|
|
|
}
|
|
|
|
|
2024-07-05 13:32:46 +01:00
|
|
|
/**
|
|
|
|
* Update player profile vitality values with changes from client request object
|
|
|
|
* @param pmcData Player profile
|
|
|
|
* @param postRaidHealth Post raid data
|
|
|
|
* @param sessionID Session id
|
|
|
|
* @param isDead Is player dead
|
|
|
|
* @param addEffects Should effects be added to profile (default - true)
|
|
|
|
* @param deleteExistingEffects Should all prior effects be removed before apply new ones (default - true)
|
|
|
|
*/
|
|
|
|
public updateProfileHealthPostRaid(
|
|
|
|
pmcData: IPmcData,
|
|
|
|
postRaidHealth: Health,
|
|
|
|
sessionID: string,
|
|
|
|
isDead: boolean,
|
|
|
|
): void
|
|
|
|
{
|
|
|
|
const fullProfile = this.saveServer.getProfile(sessionID);
|
|
|
|
|
|
|
|
this.storeHydrationEnergyTempInProfile(
|
|
|
|
fullProfile,
|
|
|
|
postRaidHealth.Hydration.Current,
|
|
|
|
postRaidHealth.Energy.Current,
|
|
|
|
postRaidHealth.Temperature.Current);
|
|
|
|
|
|
|
|
// Store limb effects from post-raid in profile
|
|
|
|
for (const bodyPart in postRaidHealth.BodyParts)
|
|
|
|
{
|
|
|
|
// Effects
|
|
|
|
if (postRaidHealth.BodyParts[bodyPart].Effects)
|
|
|
|
{
|
|
|
|
fullProfile.vitality.effects[bodyPart] = postRaidHealth.BodyParts[bodyPart].Effects;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Limb hp
|
|
|
|
if (!isDead)
|
|
|
|
{
|
|
|
|
// Player alive, not is limb alive
|
2024-07-05 14:39:21 +01:00
|
|
|
fullProfile.vitality.health[bodyPart] = postRaidHealth.BodyParts[bodyPart].Health.Current;
|
2024-07-05 13:32:46 +01:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
fullProfile.vitality.health[bodyPart]
|
|
|
|
= pmcData.Health.BodyParts[bodyPart].Health.Maximum * this.healthConfig.healthMultipliers.death;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
this.transferPostRaidLimbEffectsToProfile(postRaidHealth.BodyParts, pmcData);
|
|
|
|
|
|
|
|
// Adjust hydration/energy/temp and limb hp using temp storage hydated above
|
|
|
|
this.saveHealth(pmcData, sessionID);
|
|
|
|
|
|
|
|
// Reset temp storage
|
|
|
|
this.resetVitality(sessionID);
|
|
|
|
|
|
|
|
// Update last edited timestamp
|
|
|
|
pmcData.Health.UpdateTime = this.timeUtil.getTimestamp();
|
|
|
|
}
|
|
|
|
|
|
|
|
protected storeHydrationEnergyTempInProfile(
|
|
|
|
fullProfile: ISptProfile,
|
|
|
|
hydration: number,
|
|
|
|
energy: number,
|
|
|
|
temprature: number): void
|
|
|
|
{
|
|
|
|
fullProfile.vitality.health.Hydration = hydration;
|
|
|
|
fullProfile.vitality.health.Energy = energy;
|
|
|
|
fullProfile.vitality.health.Temperature = temprature;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Take body part effects from client profile and apply to server profile
|
|
|
|
* @param postRaidBodyParts Post-raid body part data
|
|
|
|
* @param profileData Player profile on server
|
|
|
|
*/
|
|
|
|
protected transferPostRaidLimbEffectsToProfile(
|
|
|
|
postRaidBodyParts: BodyPartsHealth,
|
|
|
|
profileData: IPmcData,
|
|
|
|
): void
|
|
|
|
{
|
|
|
|
// Iterate over each body part
|
|
|
|
for (const bodyPartId in postRaidBodyParts)
|
|
|
|
{
|
|
|
|
// Get effects on body part from profile
|
|
|
|
const bodyPartEffects = postRaidBodyParts[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 };
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-03 15:23:46 +00:00
|
|
|
/**
|
2024-06-30 21:39:58 +01:00
|
|
|
* Update player profile vitality values with changes from client request object
|
2023-03-03 15:23:46 +00:00
|
|
|
* @param pmcData Player profile
|
|
|
|
* @param request Heal request
|
|
|
|
* @param sessionID Session id
|
2024-06-30 21:39:58 +01:00
|
|
|
* @param addEffects Should effects be added to profile (default - true)
|
|
|
|
* @param deleteExistingEffects Should all prior effects be removed before apply new ones (default - true)
|
2023-03-03 15:23:46 +00:00
|
|
|
*/
|
2023-11-15 20:35:05 -05:00
|
|
|
public saveVitality(
|
|
|
|
pmcData: IPmcData,
|
|
|
|
request: ISyncHealthRequestData,
|
|
|
|
sessionID: string,
|
|
|
|
addEffects = true,
|
|
|
|
deleteExistingEffects = true,
|
|
|
|
): void
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
const postRaidBodyParts = request.Health; // post raid health settings
|
2024-07-05 13:32:46 +01:00
|
|
|
const fullProfile = this.saveServer.getProfile(sessionID);
|
|
|
|
const profileEffects = fullProfile.vitality.effects;
|
2023-03-03 15:23:46 +00:00
|
|
|
|
2024-07-05 13:32:46 +01:00
|
|
|
this.storeHydrationEnergyTempInProfile(
|
|
|
|
fullProfile,
|
|
|
|
request.Hydration!,
|
|
|
|
request.Energy!,
|
|
|
|
request.Temperature!);
|
2023-03-03 15:23:46 +00:00
|
|
|
|
2024-06-30 21:39:58 +01:00
|
|
|
// Process request data into profile
|
2023-03-03 15:23:46 +00:00
|
|
|
for (const bodyPart in postRaidBodyParts)
|
|
|
|
{
|
|
|
|
// Transfer effects from request to profile
|
|
|
|
if (postRaidBodyParts[bodyPart].Effects)
|
|
|
|
{
|
|
|
|
profileEffects[bodyPart] = postRaidBodyParts[bodyPart].Effects;
|
|
|
|
}
|
2024-06-30 21:39:58 +01:00
|
|
|
|
|
|
|
if (request.IsAlive)
|
2024-05-17 15:32:41 -04:00
|
|
|
{
|
2024-06-30 21:39:58 +01:00
|
|
|
// Player alive, not is limb alive
|
2024-07-05 13:32:46 +01:00
|
|
|
fullProfile.vitality.health[bodyPart] = postRaidBodyParts[bodyPart].Current;
|
2023-03-03 15:23:46 +00:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2024-07-05 13:32:46 +01:00
|
|
|
fullProfile.vitality.health[bodyPart]
|
2024-05-17 15:32:41 -04:00
|
|
|
= pmcData.Health.BodyParts[bodyPart].Health.Maximum * this.healthConfig.healthMultipliers.death;
|
2023-03-03 15:23:46 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-06-30 21:39:58 +01:00
|
|
|
// Add effects to body parts if enabled
|
2023-03-03 15:23:46 +00:00
|
|
|
if (addEffects)
|
|
|
|
{
|
2023-11-15 20:35:05 -05:00
|
|
|
this.saveEffects(
|
|
|
|
pmcData,
|
|
|
|
sessionID,
|
2024-05-13 17:58:17 +00:00
|
|
|
this.cloner.clone(this.saveServer.getProfile(sessionID).vitality.effects),
|
2023-11-15 20:35:05 -05:00
|
|
|
deleteExistingEffects,
|
|
|
|
);
|
2023-03-03 15:23:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Adjust hydration/energy/temp and limb hp
|
|
|
|
this.saveHealth(pmcData, sessionID);
|
|
|
|
|
|
|
|
this.resetVitality(sessionID);
|
|
|
|
|
2024-06-30 21:39:58 +01:00
|
|
|
// Update last edited timestamp
|
2023-03-03 15:23:46 +00:00
|
|
|
pmcData.Health.UpdateTime = this.timeUtil.getTimestamp();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Adjust hydration/energy/temperate and body part hp values in player profile to values in profile.vitality
|
|
|
|
* @param pmcData Profile to update
|
|
|
|
* @param sessionId Session id
|
|
|
|
*/
|
|
|
|
protected saveHealth(pmcData: IPmcData, sessionID: string): void
|
|
|
|
{
|
|
|
|
if (!this.healthConfig.save.health)
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const profileHealth = this.saveServer.getProfile(sessionID).vitality.health;
|
|
|
|
for (const healthModifier in profileHealth)
|
|
|
|
{
|
|
|
|
let target = profileHealth[healthModifier];
|
|
|
|
|
|
|
|
if (["Hydration", "Energy", "Temperature"].includes(healthModifier))
|
|
|
|
{
|
|
|
|
// Set resources
|
|
|
|
if (target > pmcData.Health[healthModifier].Maximum)
|
|
|
|
{
|
|
|
|
target = pmcData.Health[healthModifier].Maximum;
|
|
|
|
}
|
|
|
|
|
|
|
|
pmcData.Health[healthModifier].Current = Math.round(target);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// Over max, limit
|
|
|
|
if (target > pmcData.Health.BodyParts[healthModifier].Health.Maximum)
|
|
|
|
{
|
|
|
|
target = pmcData.Health.BodyParts[healthModifier].Health.Maximum;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Part was zeroed out in raid
|
|
|
|
if (target === 0)
|
|
|
|
{
|
|
|
|
// Blacked body part
|
2023-11-15 20:35:05 -05:00
|
|
|
target = Math.round(
|
|
|
|
pmcData.Health.BodyParts[healthModifier].Health.Maximum
|
2024-05-07 23:57:08 -04:00
|
|
|
* this.healthConfig.healthMultipliers.blacked,
|
2023-11-15 20:35:05 -05:00
|
|
|
);
|
2023-03-03 15:23:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
pmcData.Health.BodyParts[healthModifier].Health.Current = Math.round(target);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Save effects to profile
|
|
|
|
* Works by removing all effects and adding them back from profile
|
|
|
|
* Removes empty 'Effects' objects if found
|
|
|
|
* @param pmcData Player profile
|
|
|
|
* @param sessionId Session id
|
|
|
|
* @param bodyPartsWithEffects dict of body parts with effects that should be added to profile
|
|
|
|
* @param addEffects Should effects be added back to profile
|
|
|
|
*/
|
2023-11-15 20:35:05 -05:00
|
|
|
protected saveEffects(
|
|
|
|
pmcData: IPmcData,
|
|
|
|
sessionId: string,
|
|
|
|
bodyPartsWithEffects: Effects,
|
|
|
|
deleteExistingEffects = true,
|
|
|
|
): void
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
if (!this.healthConfig.save.effects)
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const bodyPart in bodyPartsWithEffects)
|
|
|
|
{
|
|
|
|
// clear effects from profile bodyPart
|
|
|
|
if (deleteExistingEffects)
|
|
|
|
{
|
|
|
|
delete pmcData.Health.BodyParts[bodyPart].Effects;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const effectType in bodyPartsWithEffects[bodyPart])
|
|
|
|
{
|
|
|
|
if (typeof effectType !== "string")
|
|
|
|
{
|
|
|
|
this.logger.warning(`Effect ${effectType} on body part ${bodyPart} not a string, report this`);
|
|
|
|
}
|
|
|
|
|
|
|
|
// // data can be index or the effect string (e.g. "Fracture") itself
|
|
|
|
// const effect = /^-?\d+$/.test(effectValue) // is an int
|
|
|
|
// ? nodeEffects[bodyPart][effectValue]
|
|
|
|
// : effectValue;
|
|
|
|
let time = bodyPartsWithEffects[bodyPart][effectType];
|
|
|
|
if (time)
|
|
|
|
{
|
|
|
|
// Sometimes the value can be Infinity instead of -1, blame HealthListener.cs in modules
|
|
|
|
if (time === "Infinity")
|
|
|
|
{
|
2023-11-15 20:35:05 -05:00
|
|
|
this.logger.warning(
|
|
|
|
`Effect ${effectType} found with value of Infinity, changed to -1, this is an issue with HealthListener.cs`,
|
|
|
|
);
|
2023-03-03 15:23:46 +00:00
|
|
|
time = -1;
|
|
|
|
}
|
|
|
|
this.addEffect(pmcData, bodyPart, effectType, time);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
this.addEffect(pmcData, bodyPart, effectType);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Add effect to body part in profile
|
|
|
|
* @param pmcData Player profile
|
|
|
|
* @param effectBodyPart body part to edit
|
|
|
|
* @param effectType Effect to add to body part
|
|
|
|
* @param duration How long the effect has left in seconds (-1 by default, no duration).
|
|
|
|
*/
|
|
|
|
protected addEffect(pmcData: IPmcData, effectBodyPart: string, effectType: string, duration = -1): void
|
|
|
|
{
|
|
|
|
const profileBodyPart = pmcData.Health.BodyParts[effectBodyPart];
|
|
|
|
if (!profileBodyPart.Effects)
|
|
|
|
{
|
|
|
|
profileBodyPart.Effects = {};
|
|
|
|
}
|
|
|
|
|
|
|
|
profileBodyPart.Effects[effectType] = { Time: duration };
|
|
|
|
|
|
|
|
// Delete empty property to prevent client bugs
|
|
|
|
if (this.isEmpty(profileBodyPart.Effects))
|
|
|
|
{
|
|
|
|
delete profileBodyPart.Effects;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-05-07 23:57:08 -04:00
|
|
|
protected isEmpty(map: Record<string, { Time: number }>): boolean
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
for (const key in map)
|
|
|
|
{
|
|
|
|
if (key in map)
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
2023-11-15 20:35:05 -05:00
|
|
|
}
|