using SPTarkov.Common.Annotations; using SPTarkov.Common.Extensions; using SPTarkov.Server.Core.Helpers; using SPTarkov.Server.Core.Models.Eft.Common; using SPTarkov.Server.Core.Models.Eft.Common.Tables; using SPTarkov.Server.Core.Models.Eft.Health; using SPTarkov.Server.Core.Models.Eft.ItemEvent; using SPTarkov.Server.Core.Models.Eft.Trade; using SPTarkov.Server.Core.Models.Enums; using SPTarkov.Server.Core.Models.Utils; using SPTarkov.Server.Core.Routers; using SPTarkov.Server.Core.Services; using SPTarkov.Server.Core.Utils; using SPTarkov.Server.Core.Utils.Cloners; namespace SPTarkov.Server.Core.Controllers; [Injectable] public class HealthController( ISptLogger _logger, EventOutputHolder _eventOutputHolder, ItemHelper _itemHelper, PaymentService _paymentService, InventoryHelper _inventoryHelper, LocalisationService _localisationService, HttpResponseUtil _httpResponseUtil, HealthHelper _healthHelper, ICloner _cloner) { /// /// When healing in menu /// /// Player profile /// Healing request /// Player id /// ItemEventRouterResponse public ItemEventRouterResponse OffRaidHeal( PmcData pmcData, OffraidHealRequestData request, string sessionID) { var output = _eventOutputHolder.GetOutput(sessionID); // Update medkit used (hpresource) var healingItemToUse = pmcData.Inventory.Items.FirstOrDefault(item => item.Id == request.Item); if (healingItemToUse is null) { var errorMessage = _localisationService.GetText("health-healing_item_not_found", request.Item); _logger.Error(errorMessage); return _httpResponseUtil.AppendErrorToOutput(output, errorMessage); } // Ensure item has a upd object _itemHelper.AddUpdObjectToItem(healingItemToUse); if (healingItemToUse.Upd.MedKit is not null) { healingItemToUse.Upd.MedKit.HpResource -= request.Count; } else { // Get max healing from db var maxHp = _itemHelper.GetItem(healingItemToUse.Template).Value.Properties.MaxHpResource; healingItemToUse.Upd.MedKit = new UpdMedKit { HpResource = maxHp - request.Count }; // Subtract amout used from max // request.count appears to take into account healing effects removed, e.g. bleeds // Salewa heals limb for 20 and fixes light bleed = (20+45 = 65) } // Resource in medkit is spent, delete it if (healingItemToUse.Upd.MedKit.HpResource <= 0) { _inventoryHelper.RemoveItem(pmcData, request.Item, sessionID, output); } var healingItemDbDetails = _itemHelper.GetItem(healingItemToUse.Template); var healItemEffectDetails = healingItemDbDetails.Value.Properties.EffectsDamage; var bodyPartToHeal = pmcData.Health.BodyParts.GetValueOrDefault(request.Part); if (bodyPartToHeal is null) { _logger.Warning($"Player: {sessionID} Tried to heal a non-existent body part: {request.Part}"); return output; } // Get initial heal amount var amountToHealLimb = request.Count; // Check if healing item removes negative effects var itemRemovesEffects = healingItemDbDetails.Value.Properties.EffectsDamage.Count > 0; if (itemRemovesEffects && bodyPartToHeal.Effects is not null) { // Can remove effects and limb has effects to remove foreach (var effectKvP in bodyPartToHeal.Effects) { // Check enum has effectType if (!Enum.TryParse(effectKvP.Key, out var effect)) // Enum doesnt contain this key { continue; } // Check if healing item removes the effect on limb if (!healItemEffectDetails.TryGetValue(effect, out var matchingEffectFromHealingItem)) // Healing item doesn't have matching effect, it doesn't remove the effect { continue; } // Adjust limb heal amount based on if it's fixing an effect (request.count is TOTAL cost of hp resource on heal item, NOT amount to heal limb) amountToHealLimb -= (int) (matchingEffectFromHealingItem.Cost ?? 0); bodyPartToHeal.Effects.Remove(effectKvP.Key); } } // Adjust body part hp value bodyPartToHeal.Health.Current += amountToHealLimb; // Ensure we've not healed beyond the limbs max hp if (bodyPartToHeal.Health.Current > bodyPartToHeal.Health.Maximum) { bodyPartToHeal.Health.Current = bodyPartToHeal.Health.Maximum; } return output; } /// /// Handle Eat event /// Consume food/water outside of a raid /// /// Player profile /// Eat request /// Session id /// ItemEventRouterResponse public ItemEventRouterResponse OffRaidEat( PmcData pmcData, OffraidEatRequestData request, string sessionID) { var output = _eventOutputHolder.GetOutput(sessionID); var resourceLeft = 0d; var itemToConsume = pmcData.Inventory.Items.FirstOrDefault(item => item.Id == request.Item); if (itemToConsume is null) // Item not found, very bad { return _httpResponseUtil.AppendErrorToOutput( output, _localisationService.GetText("health-unable_to_find_item_to_consume", request.Item) ); } var consumedItemMaxResource = _itemHelper.GetItem(itemToConsume.Template).Value.Properties.MaxResource; if (consumedItemMaxResource > 1) { // Ensure item has a upd object _itemHelper.AddUpdObjectToItem(itemToConsume); if (itemToConsume.Upd.FoodDrink is null) { itemToConsume.Upd.FoodDrink = new UpdFoodDrink { HpPercent = consumedItemMaxResource - request.Count }; } else { itemToConsume.Upd.FoodDrink.HpPercent -= request.Count; } resourceLeft = itemToConsume.Upd.FoodDrink.HpPercent.Value; } // Remove item from inventory if resource has dropped below threshold if (consumedItemMaxResource == 1 || resourceLeft < 1) { _inventoryHelper.RemoveItem(pmcData, request.Item, sessionID, output); } // Check what effect eating item has and handle var foodItemDbDetails = _itemHelper.GetItem(itemToConsume.Template).Value; var foodItemEffectDetails = foodItemDbDetails.Properties.EffectsHealth; var foodIsSingleUse = foodItemDbDetails.Properties.MaxResource == 1; foreach (var (key, effectProps) in foodItemEffectDetails) { switch (key) { case HealthFactor.Hydration: ApplyEdibleEffect(pmcData.Health.Hydration, effectProps, foodIsSingleUse, request); break; case HealthFactor.Energy: ApplyEdibleEffect(pmcData.Health.Energy, effectProps, foodIsSingleUse, request); break; default: _logger.Warning($"Unhandled effect after consuming: {itemToConsume.Template}, {key}"); break; } } return output; } /// /// Apply effects to profile from consumable used /// /// Hydration/Energy /// Properties of consumed item /// Single use item /// Client request protected void ApplyEdibleEffect(CurrentMinMax bodyValue, EffectsHealthProps consumptionDetails, bool foodIsSingleUse, OffraidEatRequestData request) { if (foodIsSingleUse) // Apply whole value from passed in parameter { bodyValue.Current += consumptionDetails.Value; } else { bodyValue.Current += request.Count; } // Ensure current never goes over max if (bodyValue.Current > bodyValue.Maximum) { bodyValue.Current = bodyValue.Maximum; return; } // Same as above but for the lower bound if (bodyValue.Current < 0) { bodyValue.Current = 0; } } /// /// Handle RestoreHealth event /// Occurs on post-raid healing page /// /// player profile /// Request data from client /// Session id /// public ItemEventRouterResponse HealthTreatment( PmcData pmcData, HealthTreatmentRequestData healthTreatmentRequest, string sessionID) { var output = _eventOutputHolder.GetOutput(sessionID); var payMoneyRequest = new ProcessBuyTradeRequestData { Action = healthTreatmentRequest.Action, TransactionId = Traders.THERAPIST, SchemeItems = healthTreatmentRequest.Items, Type = "", ItemId = "", Count = 0, SchemeId = 0 }; _paymentService.PayMoney(pmcData, payMoneyRequest, sessionID, output); if (output.Warnings.Count > 0) { return output; } foreach (var bodyPartKvP in healthTreatmentRequest.Difference.BodyParts.GetAllPropsAsDict()) { // Get body part from request + from pmc profile var partRequest = (BodyPartEffects) bodyPartKvP.Value; var profilePart = pmcData.Health.BodyParts[bodyPartKvP.Key]; // Bodypart healing is chosen when part request hp is above 0 if (partRequest.Health > 0) // Heal bodypart { profilePart.Health.Current = profilePart.Health.Maximum; } // Check for effects to remove if (partRequest.Effects?.Count > 0) { // Found some, loop over them and remove from pmc profile foreach (var effect in partRequest.Effects) { pmcData.Health.BodyParts[bodyPartKvP.Key].Effects.Remove(effect); } // Remove empty effect object if (pmcData.Health.BodyParts[bodyPartKvP.Key].Effects.Count == 0) { pmcData.Health.BodyParts[bodyPartKvP.Key].Effects = null; } } } // Inform client of new post-raid, post-therapist heal values output.ProfileChanges[sessionID].Health = _cloner.Clone(pmcData.Health); return output; } /// /// applies skills from hideout workout. /// /// Player profile /// Request data /// session id public void ApplyWorkoutChanges( PmcData? pmcData, WorkoutData request, string sessionId) { pmcData.Skills.Common = request.Skills.Common; } }