Server/project/src/helpers/HealthHelper.ts
Alex 173a726f33 Removed all null references in favor of undefined (!354)
Co-authored-by: clodan <clodan@clodan.com>
Reviewed-on: https://dev.sp-tarkov.com/SPT/Server/pulls/354
Co-authored-by: Alex <clodan@noreply.dev.sp-tarkov.com>
Co-committed-by: Alex <clodan@noreply.dev.sp-tarkov.com>
2024-05-27 20:06:07 +00:00

281 lines
9.3 KiB
TypeScript

import { inject, injectable } from "tsyringe";
import { IPmcData } from "@spt/models/eft/common/IPmcData";
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";
@injectable()
export class HealthHelper
{
protected healthConfig: IHealthConfig;
constructor(
@inject("WinstonLogger") protected logger: ILogger,
@inject("TimeUtil") protected timeUtil: TimeUtil,
@inject("SaveServer") protected saveServer: SaveServer,
@inject("ConfigServer") protected configServer: ConfigServer,
@inject("RecursiveCloner") protected cloner: ICloner,
)
{
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
*/
public resetVitality(sessionID: string): ISptProfile
{
const profile = this.saveServer.getProfile(sessionID);
if (!profile.vitality)
{
// Occurs on newly created profiles
profile.vitality = { health: undefined!, effects: undefined! };
}
profile.vitality.health = {
Hydration: 0,
Energy: 0,
Temperature: 0,
Head: 0,
Chest: 0,
Stomach: 0,
LeftArm: 0,
RightArm: 0,
LeftLeg: 0,
RightLeg: 0,
};
profile.vitality.effects = {
Head: {},
Chest: {},
Stomach: {},
LeftArm: {},
RightArm: {},
LeftLeg: {},
RightLeg: {},
};
return profile;
}
/**
* Update player profile with changes from request object
* @param pmcData Player profile
* @param request Heal request
* @param sessionID Session id
* @param addEffects Should effects be added or removed (default - add)
* @param deleteExistingEffects Should all prior effects be removed before apply new ones
*/
public saveVitality(
pmcData: IPmcData,
request: ISyncHealthRequestData,
sessionID: string,
addEffects = true,
deleteExistingEffects = true,
): void
{
const postRaidBodyParts = request.Health; // post raid health settings
const profile = this.saveServer.getProfile(sessionID);
const profileHealth = profile.vitality.health;
const profileEffects = profile.vitality.effects;
profileHealth.Hydration = request.Hydration!;
profileHealth.Energy = request.Energy!;
profileHealth.Temperature = request.Temperature!;
// Transfer properties from request to profile
for (const bodyPart in postRaidBodyParts)
{
// Transfer effects from request to profile
if (postRaidBodyParts[bodyPart].Effects)
{
profileEffects[bodyPart] = postRaidBodyParts[bodyPart].Effects;
}
if (request.IsAlive === true)
{
// is player alive, not is limb alive
profileHealth[bodyPart] = postRaidBodyParts[bodyPart].Current;
}
else
{
profileHealth[bodyPart]
= pmcData.Health.BodyParts[bodyPart].Health.Maximum * this.healthConfig.healthMultipliers.death;
}
}
// Add effects to body parts
if (addEffects)
{
this.saveEffects(
pmcData,
sessionID,
this.cloner.clone(this.saveServer.getProfile(sessionID).vitality.effects),
deleteExistingEffects,
);
}
// Adjust hydration/energy/temp and limb hp
this.saveHealth(pmcData, sessionID);
this.resetVitality(sessionID);
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
target = Math.round(
pmcData.Health.BodyParts[healthModifier].Health.Maximum
* this.healthConfig.healthMultipliers.blacked,
);
}
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
*/
protected saveEffects(
pmcData: IPmcData,
sessionId: string,
bodyPartsWithEffects: Effects,
deleteExistingEffects = true,
): void
{
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")
{
this.logger.warning(
`Effect ${effectType} found with value of Infinity, changed to -1, this is an issue with HealthListener.cs`,
);
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;
}
}
protected isEmpty(map: Record<string, { Time: number }>): boolean
{
for (const key in map)
{
if (key in map)
{
return false;
}
}
return true;
}
}