using Core.Models.Eft.Common; using Core.Models.Eft.Common.Tables; using Core.Models.Eft.Profile; using Core.Models.Spt.Config; using Core.Servers; using Core.Services; using Core.Utils; using SptCommon.Annotations; using SptCommon.Extensions; using BodyPartHealth = Core.Models.Eft.Common.Tables.BodyPartHealth; using Vitality = Core.Models.Eft.Profile.Vitality; namespace Core.Helpers; [Injectable] public class HealthHelper( TimeUtil _timeUtil, SaveServer _saveServer, DatabaseService _databaseService, ConfigServer _configServer ) { protected HealthConfig _healthConfig = _configServer.GetConfig(); /// /// Resets the profiles vitality/health and vitality/effects properties to their defaults /// /// Session Id /// Updated profile public SptProfile ResetVitality(string sessionID) { var profile = _saveServer.GetProfile(sessionID); DefaultVitality(profile.VitalityData); return profile; } public void DefaultVitality(Vitality? vitality) { vitality ??= new Vitality { Health = null, Energy = 0, Temperature = 0, Hydration = 0 }; vitality.Health = new Dictionary { { "Head", new BodyPartHealth { Health = new CurrentMinMax { Current = 0 }, Effects = new Dictionary() } }, { "Chest", new BodyPartHealth { Health = new CurrentMinMax { Current = 0 }, Effects = new Dictionary() } }, { "Stomach", new BodyPartHealth { Health = new CurrentMinMax { Current = 0 }, Effects = new Dictionary() } }, { "LeftArm", new BodyPartHealth { Health = new CurrentMinMax { Current = 0 }, Effects = new Dictionary() } }, { "RightArm", new BodyPartHealth { Health = new CurrentMinMax { Current = 0 }, Effects = new Dictionary() } }, { "LeftLeg", new BodyPartHealth { Health = new CurrentMinMax { Current = 0 }, Effects = new Dictionary() } }, { "RightLeg", new BodyPartHealth { Health = new CurrentMinMax { Current = 0 }, Effects = new Dictionary() } } }; } /// /// Update player profile vitality values with changes from client request object /// /// Player profile /// Post raid data /// Session id /// Is player dead /// Should effects be added to profile (default - true) /// Should all prior effects be removed before apply new ones (default - true) public void UpdateProfileHealthPostRaid( PmcData pmcData, BotBaseHealth postRaidHealth, string sessionID, bool isDead) { var fullProfile = _saveServer.GetProfile(sessionID); var profileEdition = fullProfile.ProfileInfo.Edition; var profileSide = fullProfile.CharacterData.PmcData.Info.Side; var defaultTemperature = _databaseService.GetProfiles() .GetByJsonProp(profileEdition) .GetByJsonProp(profileSide.ToLower()) ?.Character?.Health?.Temperature ?? new CurrentMinMax { Current = 36.6 }; StoreHydrationEnergyTempInProfile( fullProfile, postRaidHealth.Hydration.Current ?? 0, postRaidHealth.Energy.Current ?? 0, defaultTemperature.Current ?? 0 // Reset profile temp to the default to prevent very cold/hot temps persisting into next raid ); // Store limb effects from post-raid in profile foreach (var bodyPart in postRaidHealth.BodyParts) { // Effects if (postRaidHealth.BodyParts[bodyPart.Key].Effects is not null) { fullProfile.VitalityData.Health[bodyPart.Key].Effects = postRaidHealth.BodyParts[bodyPart.Key].Effects; } // Limb hp if (!isDead) // Player alive, not is limb alive { fullProfile.VitalityData.Health[bodyPart.Key].Health.Current = postRaidHealth.BodyParts[bodyPart.Key].Health.Current ?? 0; } else { fullProfile.VitalityData.Health[bodyPart.Key].Health.Current = pmcData.Health.BodyParts[bodyPart.Key].Health.Maximum * _healthConfig.HealthMultipliers.Death ?? 0; } } TransferPostRaidLimbEffectsToProfile(postRaidHealth.BodyParts, pmcData); // Adjust hydration/energy/temp and limb hp using temp storage hydated above SaveHealth(pmcData, sessionID); // Reset temp storage ResetVitality(sessionID); // Update last edited timestamp pmcData.Health.UpdateTime = _timeUtil.GetTimeStamp(); } protected void StoreHydrationEnergyTempInProfile( SptProfile fullProfile, double hydration, double energy, double temprature) { fullProfile.VitalityData.Hydration = hydration; fullProfile.VitalityData.Energy = energy; fullProfile.VitalityData.Temperature = temprature; } /// /// Take body part effects from client profile and apply to server profile /// /// Post-raid body part data /// Player profile on server protected void TransferPostRaidLimbEffectsToProfile(Dictionary postRaidBodyParts, PmcData profileData) { // Iterate over each body part HashSet effectsToIgnore = ["Dehydration", "Exhaustion"]; foreach (var bodyPartId in postRaidBodyParts) { // Get effects on body part from profile var bodyPartEffects = postRaidBodyParts[bodyPartId.Key].Effects; foreach (var effect in bodyPartEffects) { var effectDetails = bodyPartEffects[effect.Key]; // Null guard profileData.Health.BodyParts[bodyPartId.Key].Effects ??= new Dictionary(); // Effect already exists on limb in server profile, skip var profileBodyPartEffects = profileData.Health.BodyParts[bodyPartId.Key].Effects; if (profileBodyPartEffects.TryGetValue(effect.Key, out var dictEffect)) { if (effectsToIgnore.Contains(effect.Key)) // Get rid of certain effects we dont want to persist out of raid { dictEffect = null; } continue; } if (effectsToIgnore.Contains(effect.Key)) // Do not pass some effects to out of raid profile { continue; } var effectToAdd = new BodyPartEffectProperties { Time = effectDetails.Time ?? -1 }; // Add effect to server profile if (profileBodyPartEffects.TryAdd(effect.Key, effectToAdd)) { profileBodyPartEffects[effect.Key] = effectToAdd; } } } } /// /// Adjust hydration/energy/temperate and body part hp values in player profile to values in profile.vitality /// /// Profile to update /// Session id protected void SaveHealth(PmcData pmcData, string sessionID) { if (!_healthConfig.Save.Health) { return; } var profileHealth = _saveServer.GetProfile(sessionID).VitalityData; if (profileHealth.Hydration > pmcData.Health.Hydration.Maximum) { profileHealth.Hydration = pmcData.Health.Hydration.Maximum; } if (profileHealth.Energy > pmcData.Health.Energy.Maximum) { profileHealth.Energy = pmcData.Health.Energy.Maximum; } if (profileHealth.Temperature > pmcData.Health.Temperature.Maximum) { profileHealth.Temperature = pmcData.Health.Temperature.Maximum; } pmcData.Health.Hydration.Current = Math.Round(profileHealth.Hydration ?? 0); pmcData.Health.Energy.Current = Math.Round(profileHealth.Energy ?? 0); pmcData.Health.Temperature.Current = Math.Round(profileHealth.Temperature ?? 0); foreach (var bodyPart in pmcData.Health.BodyParts) { if (profileHealth.Health[bodyPart.Key].Health.Maximum > bodyPart.Value.Health.Maximum) { profileHealth.Health[bodyPart.Key].Health.Maximum = bodyPart.Value.Health.Maximum; } if (profileHealth.Health[bodyPart.Key].Health.Current == 0) { profileHealth.Health[bodyPart.Key].Health.Current = bodyPart.Value.Health.Maximum * _healthConfig.HealthMultipliers.Blacked; } bodyPart.Value.Health.Current = Math.Round(profileHealth.Health[bodyPart.Key].Health.Current ?? 0); } } /// /// Save effects to profile /// Works by removing all effects and adding them back from profile /// Removes empty 'Effects' objects if found /// /// Player profile /// Session id /// Dictionary of body parts with effects that should be added to profile /// Should effects be added back to profile protected void SaveEffects( PmcData pmcData, string sessionID, Dictionary bodyPartsWithEffects, bool deleteExistingEffects = true) { // TODO: this will need to change, typing is all fucked up if (!_healthConfig.Save.Effects) { return; } foreach (var bodyPart in bodyPartsWithEffects) { // clear effects from profile bodyPart if (deleteExistingEffects) { pmcData.Health.BodyParts[bodyPart.Key].Effects = new Dictionary(); } foreach (var effectType in bodyPartsWithEffects[bodyPart.Key].Effects) { var time = effectType.Value.Time; if (time is not null && time > 0) { AddEffect(pmcData, effectType, time); } else { AddEffect(pmcData, effectType); } } } } /// /// Add effect to body part in profile /// /// Player profile /// Body part to edit /// Effect to add to body part /// How long the effect has left in seconds (-1 by default, no duration). protected void AddEffect(PmcData pmcData, KeyValuePair effectType, double? duration = -1) { var profileBodyPart = pmcData.Health.BodyParts[effectType.Key]; profileBodyPart.Effects ??= new Dictionary(); profileBodyPart.Effects[effectType.Key] = new BodyPartEffectProperties { Time = duration }; } }