1549 lines
69 KiB
C#
1549 lines
69 KiB
C#
using Barotrauma.Abilities;
|
|
using Barotrauma.Abilities;
|
|
using Barotrauma.Extensions;
|
|
using Barotrauma.Extensions;
|
|
using Barotrauma.LuaCs.Events;
|
|
using Barotrauma.Networking;
|
|
using Barotrauma.Networking;
|
|
using Microsoft.Xna.Framework;
|
|
using MoonSharp.Interpreter;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Xml.Linq;
|
|
using static OneOf.Types.TrueFalseOrNull;
|
|
|
|
namespace Barotrauma
|
|
{
|
|
partial class CharacterHealth
|
|
{
|
|
public class LimbHealth
|
|
{
|
|
public Sprite IndicatorSprite;
|
|
public Sprite HighlightSprite;
|
|
|
|
public Rectangle HighlightArea;
|
|
|
|
public readonly LocalizedString Name;
|
|
|
|
//public readonly List<Affliction> Afflictions = new List<Affliction>();
|
|
|
|
public readonly Dictionary<Identifier, float> VitalityMultipliers = new Dictionary<Identifier, float>();
|
|
public readonly Dictionary<Identifier, float> VitalityTypeMultipliers = new Dictionary<Identifier, float>();
|
|
|
|
public LimbHealth() { }
|
|
|
|
public LimbHealth(ContentXElement element, CharacterHealth characterHealth)
|
|
{
|
|
string limbName = element.GetAttributeString("name", null) ?? "generic";
|
|
if (limbName != "generic")
|
|
{
|
|
Name = TextManager.Get("HealthLimbName." + limbName);
|
|
}
|
|
foreach (var subElement in element.Elements())
|
|
{
|
|
switch (subElement.Name.ToString().ToLowerInvariant())
|
|
{
|
|
case "sprite":
|
|
IndicatorSprite = new Sprite(subElement);
|
|
HighlightArea = subElement.GetAttributeRect("highlightarea", new Rectangle(0, 0, (int)IndicatorSprite.size.X, (int)IndicatorSprite.size.Y));
|
|
break;
|
|
case "highlightsprite":
|
|
HighlightSprite = new Sprite(subElement);
|
|
break;
|
|
case "vitalitymultiplier":
|
|
if (subElement.GetAttribute("name") != null)
|
|
{
|
|
DebugConsole.ThrowError("Error in character health config (" + characterHealth.Character.Name + ") - define vitality multipliers using affliction identifiers or types instead of names.",
|
|
contentPackage: element.ContentPackage);
|
|
continue;
|
|
}
|
|
var vitalityMultipliers = subElement.GetAttributeIdentifierArray("identifier", null) ?? subElement.GetAttributeIdentifierArray("identifiers", null);
|
|
if (vitalityMultipliers != null)
|
|
{
|
|
float multiplier = subElement.GetAttributeFloat("multiplier", 1.0f);
|
|
foreach (var vitalityMultiplier in vitalityMultipliers)
|
|
{
|
|
VitalityMultipliers.Add(vitalityMultiplier, multiplier);
|
|
if (AfflictionPrefab.Prefabs.None(p => p.Identifier == vitalityMultiplier))
|
|
{
|
|
DebugConsole.AddWarning($"Potentially incorrectly defined vitality multiplier in \"{characterHealth.Character.Name}\". Could not find any afflictions with the identifier \"{vitalityMultiplier}\". Did you mean to define the afflictions by type instead?",
|
|
contentPackage: element.ContentPackage);
|
|
}
|
|
}
|
|
}
|
|
var vitalityTypeMultipliers = subElement.GetAttributeIdentifierArray("type", null) ?? subElement.GetAttributeIdentifierArray("types", null);
|
|
if (vitalityTypeMultipliers != null)
|
|
{
|
|
float multiplier = subElement.GetAttributeFloat("multiplier", 1.0f);
|
|
foreach (var vitalityTypeMultiplier in vitalityTypeMultipliers)
|
|
{
|
|
VitalityTypeMultipliers.Add(vitalityTypeMultiplier, multiplier);
|
|
if (AfflictionPrefab.Prefabs.None(p => p.AfflictionType == vitalityTypeMultiplier))
|
|
{
|
|
DebugConsole.AddWarning($"Potentially incorrectly defined vitality multiplier in \"{characterHealth.Character.Name}\". Could not find any afflictions of the type \"{vitalityTypeMultiplier}\". Did you mean to define the afflictions by identifier instead?",
|
|
contentPackage: element.ContentPackage);
|
|
}
|
|
}
|
|
}
|
|
if (vitalityMultipliers == null && VitalityTypeMultipliers == null)
|
|
{
|
|
DebugConsole.ThrowError($"Error in character health config {characterHealth.Character.Name}: affliction identifier(s) or type(s) not defined in the \"VitalityMultiplier\" elements!",
|
|
contentPackage: element.ContentPackage);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public const float InsufficientOxygenThreshold = 30.0f;
|
|
public const float LowOxygenThreshold = 50.0f;
|
|
protected float minVitality;
|
|
|
|
/// <summary>
|
|
/// Maximum vitality without talent- or job-based modifiers
|
|
/// </summary>
|
|
protected float UnmodifiedMaxVitality
|
|
{
|
|
get => Character.Params.Health.Vitality;
|
|
set => Character.Params.Health.Vitality = value;
|
|
}
|
|
|
|
public bool Unkillable;
|
|
|
|
public bool DoesBleed
|
|
{
|
|
get => Character.Params.Health.DoesBleed && !Character.Params.IsMachine;
|
|
private set => Character.Params.Health.DoesBleed = value;
|
|
}
|
|
|
|
public bool UseHealthWindow
|
|
{
|
|
get => Character.Params.Health.UseHealthWindow;
|
|
set => Character.Params.Health.UseHealthWindow = value;
|
|
}
|
|
|
|
public float CrushDepth
|
|
{
|
|
get => Character.Params.Health.CrushDepth;
|
|
private set => Character.Params.Health.CrushDepth = value;
|
|
}
|
|
|
|
private readonly List<LimbHealth> limbHealths = new List<LimbHealth>();
|
|
|
|
private readonly Dictionary<Affliction, LimbHealth> afflictions = new Dictionary<Affliction, LimbHealth>();
|
|
private readonly HashSet<Affliction> irremovableAfflictions = new HashSet<Affliction>();
|
|
private Affliction bloodlossAffliction;
|
|
private Affliction oxygenLowAffliction;
|
|
private Affliction pressureAffliction;
|
|
private Affliction stunAffliction;
|
|
public Affliction BloodlossAffliction { get => bloodlossAffliction; }
|
|
|
|
/// <summary>
|
|
/// Is the character dead or below 0 vitality and not able to stay conscious?
|
|
/// </summary>
|
|
public bool IsUnconscious
|
|
{
|
|
get { return Character.IsDead || (Vitality <= 0.0f && !Character.HasAbilityFlag(AbilityFlags.AlwaysStayConscious)); }
|
|
}
|
|
|
|
public float PressureKillDelay { get; private set; } = 5.0f;
|
|
|
|
private float vitality;
|
|
public float Vitality
|
|
{
|
|
get
|
|
{
|
|
if (Character.IsDead)
|
|
{
|
|
return minVitality;
|
|
}
|
|
return vitality;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// How much vitality the character would have if it was alive?
|
|
/// E.g. a character killed by disconnection or with console commands may not have any vitality-reducing afflictions despite being dead
|
|
/// </summary>
|
|
public float VitalityDisregardingDeath => vitality;
|
|
|
|
public float HealthPercentage => MathUtils.Percentage(Vitality, MaxVitality);
|
|
|
|
public float MaxVitality
|
|
{
|
|
get
|
|
{
|
|
float max = UnmodifiedMaxVitality;
|
|
if (Character?.Info?.Job?.Prefab != null)
|
|
{
|
|
max += Character.Info.Job.Prefab.VitalityModifier;
|
|
}
|
|
max *= Character.HumanPrefabHealthMultiplier;
|
|
if (GameMain.GameSession?.Campaign is CampaignMode campaign)
|
|
{
|
|
max *= Character.IsOnPlayerTeam
|
|
? campaign.Settings.CrewVitalityMultiplier
|
|
: campaign.Settings.NonCrewVitalityMultiplier;
|
|
}
|
|
max *= 1f + Character.GetStatValue(StatTypes.MaximumHealthMultiplier);
|
|
return max * Character.HealthMultiplier;
|
|
}
|
|
}
|
|
|
|
public float MinVitality
|
|
{
|
|
get
|
|
{
|
|
if (Character?.Info?.Job?.Prefab != null)
|
|
{
|
|
return -MaxVitality;
|
|
}
|
|
return minVitality;
|
|
}
|
|
}
|
|
|
|
public static readonly Color DefaultFaceTint = Color.TransparentBlack;
|
|
|
|
public Color FaceTint
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
public Color BodyTint
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
public float OxygenAmount
|
|
{
|
|
get
|
|
{
|
|
if (!Character.NeedsOxygen || Unkillable || Character.GodMode) { return 100.0f; }
|
|
return -oxygenLowAffliction.Strength + 100;
|
|
}
|
|
set
|
|
{
|
|
if (!Character.NeedsOxygen || Unkillable || Character.GodMode) { return; }
|
|
oxygenLowAffliction.Strength = MathHelper.Clamp(-value + 100, 0.0f, 200.0f);
|
|
}
|
|
}
|
|
|
|
public float BloodlossAmount
|
|
{
|
|
get { return bloodlossAffliction.Strength; }
|
|
set { bloodlossAffliction.Strength = MathHelper.Clamp(value, 0, bloodlossAffliction.Prefab.MaxStrength); }
|
|
}
|
|
|
|
public float Stun
|
|
{
|
|
get { return stunAffliction.Strength; }
|
|
set
|
|
{
|
|
if (Character.GodMode) { return; }
|
|
stunAffliction.Strength = MathHelper.Clamp(value, 0.0f, stunAffliction.Prefab.MaxStrength);
|
|
}
|
|
}
|
|
|
|
public bool IsParalyzed { get; private set; }
|
|
|
|
public float StunTimer { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Was the character in full health at the beginning of the frame?
|
|
/// </summary>
|
|
public bool WasInFullHealth { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Show the blood overlay screen space effect when the character takes damage.
|
|
/// Enabled normally, but can be disabled for some special cases.
|
|
/// </summary>
|
|
public bool ShowDamageOverlay = true;
|
|
|
|
public Affliction PressureAffliction
|
|
{
|
|
get { return pressureAffliction; }
|
|
}
|
|
|
|
public readonly Character Character;
|
|
|
|
public CharacterHealth(Character character)
|
|
{
|
|
this.Character = character;
|
|
vitality = 100.0f;
|
|
|
|
DoesBleed = true;
|
|
UseHealthWindow = false;
|
|
|
|
InitIrremovableAfflictions();
|
|
|
|
limbHealths.Add(new LimbHealth());
|
|
|
|
InitProjSpecific(null, character);
|
|
}
|
|
|
|
public CharacterHealth(ContentXElement element, Character character, ContentXElement limbHealthElement = null)
|
|
{
|
|
this.Character = character;
|
|
InitIrremovableAfflictions();
|
|
|
|
vitality = UnmodifiedMaxVitality;
|
|
|
|
minVitality = element.GetAttributeFloat(nameof(MinVitality), character.IsHuman ? -100.0f : 0.0f);
|
|
|
|
limbHealths.Clear();
|
|
limbHealthElement ??= element;
|
|
foreach (var subElement in limbHealthElement.Elements())
|
|
{
|
|
if (!subElement.Name.ToString().Equals("limb", StringComparison.OrdinalIgnoreCase)) { continue; }
|
|
limbHealths.Add(new LimbHealth(subElement, this));
|
|
}
|
|
if (limbHealths.Count == 0)
|
|
{
|
|
limbHealths.Add(new LimbHealth());
|
|
}
|
|
|
|
InitProjSpecific(element, character);
|
|
}
|
|
|
|
public void CheckForErrors()
|
|
{
|
|
for (int i = 0; i < limbHealths.Count; i++)
|
|
{
|
|
if (Character.AnimController.Limbs.None(l => l.HealthIndex == i))
|
|
{
|
|
DebugConsole.AddWarning(
|
|
$"Potential error in character \"{Character.Prefab.Identifier}\": none of the limbs have been set to use the LimbHealth #{i}, and it will do nothing. "
|
|
+ "Did you forget to set the HealthIndex values of the limbs?", contentPackage: Character.ContentPackage);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void InitIrremovableAfflictions()
|
|
{
|
|
irremovableAfflictions.Add(bloodlossAffliction = new Affliction(AfflictionPrefab.Bloodloss, 0.0f));
|
|
irremovableAfflictions.Add(stunAffliction = new Affliction(AfflictionPrefab.Stun, 0.0f));
|
|
irremovableAfflictions.Add(pressureAffliction = new Affliction(AfflictionPrefab.Pressure, 0.0f));
|
|
irremovableAfflictions.Add(oxygenLowAffliction = new Affliction(AfflictionPrefab.OxygenLow, 0.0f));
|
|
foreach (Affliction affliction in irremovableAfflictions)
|
|
{
|
|
afflictions.Add(affliction, null);
|
|
}
|
|
}
|
|
|
|
partial void InitProjSpecific(ContentXElement element, Character character);
|
|
|
|
public IReadOnlyCollection<Affliction> GetAllAfflictions()
|
|
{
|
|
return afflictions.Keys;
|
|
}
|
|
|
|
public IEnumerable<Affliction> GetAllAfflictions(Func<Affliction, bool> limbHealthFilter)
|
|
{
|
|
return afflictions.Keys.Where(limbHealthFilter);
|
|
}
|
|
|
|
private float GetTotalDamage(LimbHealth limbHealth)
|
|
{
|
|
float totalDamage = 0.0f;
|
|
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
|
|
{
|
|
if (kvp.Value != limbHealth) { continue; }
|
|
var affliction = kvp.Key;
|
|
totalDamage += affliction.GetVitalityDecrease(this);
|
|
}
|
|
return totalDamage;
|
|
}
|
|
|
|
private LimbHealth GetMatchingLimbHealth(Limb limb) => limb == null ? null : limbHealths[limb.HealthIndex];
|
|
private LimbHealth GetMatchingLimbHealth(Affliction affliction) => GetMatchingLimbHealth(Character.AnimController.GetLimb(affliction.Prefab.IndicatorLimb, excludeSevered: false));
|
|
|
|
public Affliction GetAffliction(string identifier, bool allowLimbAfflictions = true) =>
|
|
GetAffliction(identifier.ToIdentifier(), allowLimbAfflictions);
|
|
|
|
public Affliction GetAffliction(Identifier identifier, bool allowLimbAfflictions = true)
|
|
=> GetAffliction(a => a.Prefab.Identifier == identifier, allowLimbAfflictions);
|
|
|
|
public Affliction GetAfflictionOfType(Identifier afflictionType, bool allowLimbAfflictions = true)
|
|
=> GetAffliction(a => a.Prefab.AfflictionType == afflictionType, allowLimbAfflictions);
|
|
|
|
private Affliction GetAffliction(Func<Affliction, bool> predicate, bool allowLimbAfflictions = true)
|
|
{
|
|
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
|
|
{
|
|
if (!allowLimbAfflictions && kvp.Value != null) { continue; }
|
|
if (predicate(kvp.Key)) { return kvp.Key; }
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public T GetAffliction<T>(Identifier identifier, bool allowLimbAfflictions = true) where T : Affliction
|
|
{
|
|
return GetAffliction(identifier, allowLimbAfflictions) as T;
|
|
}
|
|
|
|
public Affliction GetAffliction(Identifier identifier, Limb limb)
|
|
{
|
|
if (limb.HealthIndex < 0 || limb.HealthIndex >= limbHealths.Count)
|
|
{
|
|
DebugConsole.ThrowError("Limb health index out of bounds. Character\"" + Character.Name +
|
|
"\" only has health configured for" + limbHealths.Count + " limbs but the limb " + limb.type + " is targeting index " + limb.HealthIndex);
|
|
return null;
|
|
}
|
|
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
|
|
{
|
|
if (limbHealths[limb.HealthIndex] == kvp.Value && kvp.Key.Prefab.Identifier == identifier) { return kvp.Key; }
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public Limb GetAfflictionLimb(Affliction affliction)
|
|
{
|
|
if (affliction != null && afflictions.TryGetValue(affliction, out LimbHealth limbHealth))
|
|
{
|
|
if (limbHealth == null) { return null; }
|
|
int limbHealthIndex = limbHealths.IndexOf(limbHealth);
|
|
foreach (Limb limb in Character.AnimController.Limbs)
|
|
{
|
|
if (limb.HealthIndex == limbHealthIndex) { return limb; }
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the total strength of the afflictions of a specific type attached to a specific limb
|
|
/// </summary>
|
|
/// <param name="afflictionType">Type of the affliction</param>
|
|
/// <param name="limb">The limb the affliction is attached to</param>
|
|
/// <param name="requireLimbSpecific">Does the affliction have to be attached to only the specific limb.
|
|
/// Most monsters for example don't have separate healths for different limbs, essentially meaning that every affliction is applied to every limb.</param>
|
|
public float GetAfflictionStrength(Identifier afflictionType, Limb limb, bool requireLimbSpecific)
|
|
{
|
|
if (requireLimbSpecific && limbHealths.Count == 1) { return 0.0f; }
|
|
|
|
float strength = 0.0f;
|
|
LimbHealth limbHealth = limbHealths[limb.HealthIndex];
|
|
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
|
|
{
|
|
if (kvp.Value == limbHealth)
|
|
{
|
|
Affliction affliction = kvp.Key;
|
|
if (affliction.Strength < affliction.Prefab.ActivationThreshold) { continue; }
|
|
if (affliction.Prefab.AfflictionType == afflictionType)
|
|
{
|
|
strength += affliction.Strength;
|
|
}
|
|
}
|
|
}
|
|
return strength;
|
|
}
|
|
|
|
public float GetAfflictionStrengthByType(Identifier afflictionType, bool allowLimbAfflictions = true)
|
|
{
|
|
return GetAfflictionStrength(afflictionType, afflictionidentifier: Identifier.Empty, allowLimbAfflictions);
|
|
}
|
|
|
|
public float GetAfflictionStrengthByIdentifier(Identifier afflictionIdentifier, bool allowLimbAfflictions = true)
|
|
{
|
|
return GetAfflictionStrength(afflictionType: Identifier.Empty, afflictionIdentifier, allowLimbAfflictions);
|
|
}
|
|
|
|
public float GetAfflictionStrength(Identifier afflictionType, Identifier afflictionidentifier, bool allowLimbAfflictions = true)
|
|
{
|
|
float strength = 0.0f;
|
|
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
|
|
{
|
|
if (!allowLimbAfflictions && kvp.Value != null) { continue; }
|
|
var affliction = kvp.Key;
|
|
if (affliction.Strength < affliction.Prefab.ActivationThreshold) { continue; }
|
|
if ((affliction.Prefab.AfflictionType == afflictionType || afflictionType.IsEmpty) &&
|
|
(affliction.Prefab.Identifier == afflictionidentifier || afflictionidentifier.IsEmpty))
|
|
{
|
|
strength += affliction.Strength;
|
|
}
|
|
}
|
|
return strength;
|
|
}
|
|
|
|
public void ApplyAffliction(Limb targetLimb, Affliction affliction, bool allowStacking = true, bool ignoreUnkillability = false, bool recalculateVitality = true)
|
|
{
|
|
if (Character.GodMode) { return; }
|
|
if (!ignoreUnkillability)
|
|
{
|
|
if (!affliction.Prefab.IsBuff && Unkillable) { return; }
|
|
}
|
|
if (affliction.Prefab.LimbSpecific)
|
|
{
|
|
if (targetLimb == null)
|
|
{
|
|
//if a limb-specific affliction is applied to no specific limb, apply to all limbs
|
|
foreach (LimbHealth limbHealth in limbHealths)
|
|
{
|
|
AddLimbAffliction(limbHealth, limb: null, affliction, allowStacking: allowStacking, recalculateVitality: recalculateVitality);
|
|
}
|
|
|
|
}
|
|
else
|
|
{
|
|
AddLimbAffliction(targetLimb, affliction, allowStacking: allowStacking, recalculateVitality: recalculateVitality);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
AddAffliction(affliction, allowStacking: allowStacking);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// How much resistance all the afflictions the character has give to the specified affliction?
|
|
/// </summary>
|
|
public float GetResistance(AfflictionPrefab afflictionPrefab, LimbType limbType)
|
|
{
|
|
lock (afflictions) {
|
|
// This is a % resistance (0 to 1.0)
|
|
float resistance = 0.0f;
|
|
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
|
|
{
|
|
var affliction = kvp.Key;
|
|
resistance += affliction.GetResistance(afflictionPrefab.Identifier, limbType);
|
|
}
|
|
// This is a multiplier, ie. 0.0 = 100% resistance and 1.0 = 0% resistance
|
|
float abilityResistanceMultiplier = Character.GetAbilityResistance(afflictionPrefab);
|
|
// The returned value is calculated to be a % resistance again
|
|
return 1 - ((1 - resistance) * abilityResistanceMultiplier);
|
|
}
|
|
}
|
|
|
|
public float GetStatValue(StatTypes statType)
|
|
{
|
|
float value = 0f;
|
|
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
|
|
{
|
|
var affliction = kvp.Key;
|
|
value += affliction.GetStatValue(statType);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
public bool HasFlag(AbilityFlags flagType)
|
|
{
|
|
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
|
|
{
|
|
var affliction = kvp.Key;
|
|
if (affliction.HasFlag(flagType)) { return true; }
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private readonly List<Affliction> matchingAfflictions = new List<Affliction>();
|
|
|
|
public void ReduceAllAfflictionsOnAllLimbs(float amount, ActionType? treatmentAction = null)
|
|
{
|
|
matchingAfflictions.Clear();
|
|
matchingAfflictions.AddRange(afflictions.Keys);
|
|
|
|
ReduceMatchingAfflictions(amount, treatmentAction);
|
|
}
|
|
|
|
public void ReduceAfflictionOnAllLimbs(Identifier afflictionIdOrType, float amount, ActionType? treatmentAction = null, Character attacker = null)
|
|
{
|
|
if (afflictionIdOrType.IsEmpty) { throw new ArgumentException($"{nameof(afflictionIdOrType)} is empty"); }
|
|
|
|
matchingAfflictions.Clear();
|
|
foreach (var affliction in afflictions)
|
|
{
|
|
if (affliction.Key.Prefab.Identifier == afflictionIdOrType || affliction.Key.Prefab.AfflictionType == afflictionIdOrType)
|
|
{
|
|
matchingAfflictions.Add(affliction.Key);
|
|
}
|
|
}
|
|
|
|
ReduceMatchingAfflictions(amount, treatmentAction, attacker);
|
|
}
|
|
|
|
private IEnumerable<Affliction> GetAfflictionsForLimb(Limb targetLimb)
|
|
=> afflictions.Keys.Where(k => afflictions[k] == limbHealths[targetLimb.HealthIndex]);
|
|
|
|
public void ReduceAllAfflictionsOnLimb(Limb targetLimb, float amount, ActionType? treatmentAction = null)
|
|
{
|
|
if (targetLimb is null) { throw new ArgumentNullException(nameof(targetLimb)); }
|
|
|
|
matchingAfflictions.Clear();
|
|
matchingAfflictions.AddRange(GetAfflictionsForLimb(targetLimb));
|
|
|
|
ReduceMatchingAfflictions(amount, treatmentAction);
|
|
}
|
|
|
|
public void ReduceAfflictionOnLimb(Limb targetLimb, Identifier afflictionIdOrType, float amount, ActionType? treatmentAction = null, Character attacker = null)
|
|
{
|
|
if (afflictionIdOrType.IsEmpty) { throw new ArgumentException($"{nameof(afflictionIdOrType)} is empty"); }
|
|
if (targetLimb is null) { throw new ArgumentNullException(nameof(targetLimb)); }
|
|
|
|
matchingAfflictions.Clear();
|
|
var targetLimbHealth = limbHealths[targetLimb.HealthIndex];
|
|
foreach (var affliction in afflictions)
|
|
{
|
|
if ((affliction.Key.Prefab.Identifier == afflictionIdOrType || affliction.Key.Prefab.AfflictionType == afflictionIdOrType) &&
|
|
affliction.Value == targetLimbHealth)
|
|
{
|
|
matchingAfflictions.Add(affliction.Key);
|
|
}
|
|
}
|
|
ReduceMatchingAfflictions(amount, treatmentAction, attacker);
|
|
}
|
|
|
|
private void ReduceMatchingAfflictions(float amount, ActionType? treatmentAction, Character attacker = null)
|
|
{
|
|
if (matchingAfflictions.Count == 0) { return; }
|
|
|
|
float reduceAmount = amount / matchingAfflictions.Count;
|
|
|
|
if (reduceAmount > 0f)
|
|
{
|
|
var abilityReduceAffliction = new AbilityReduceAffliction(Character, reduceAmount);
|
|
attacker?.CheckTalents(AbilityEffectType.OnReduceAffliction, abilityReduceAffliction);
|
|
reduceAmount = abilityReduceAffliction.Value;
|
|
}
|
|
|
|
for (int i = matchingAfflictions.Count - 1; i >= 0; i--)
|
|
{
|
|
var matchingAffliction = matchingAfflictions[i];
|
|
|
|
if (matchingAffliction.Strength < reduceAmount)
|
|
{
|
|
float surplus = reduceAmount - matchingAffliction.Strength;
|
|
amount -= matchingAffliction.Strength;
|
|
matchingAffliction.Strength = 0.0f;
|
|
matchingAfflictions.RemoveAt(i);
|
|
if (i == 0) { i = matchingAfflictions.Count; }
|
|
if (i > 0) { reduceAmount += surplus / i; }
|
|
AchievementManager.OnAfflictionRemoved(matchingAffliction, Character);
|
|
}
|
|
else
|
|
{
|
|
matchingAffliction.Strength -= reduceAmount;
|
|
amount -= reduceAmount;
|
|
if (treatmentAction != null)
|
|
{
|
|
if (treatmentAction.Value == ActionType.OnUse || treatmentAction.Value == ActionType.OnSuccess)
|
|
{
|
|
matchingAffliction.AppliedAsSuccessfulTreatmentTime = Timing.TotalTime;
|
|
}
|
|
else if (treatmentAction.Value == ActionType.OnFailure)
|
|
{
|
|
matchingAffliction.AppliedAsFailedTreatmentTime = Timing.TotalTime;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
CalculateVitality();
|
|
}
|
|
|
|
/// <summary>
|
|
///
|
|
/// </summary>
|
|
/// <param name="recalculateVitality">Set false only as an optimization when you manually call <see cref="RecalculateVitality"/>. Only applies to limb specific afflictions.</param>
|
|
public void ApplyDamage(Limb hitLimb, AttackResult attackResult, bool allowStacking = true, bool recalculateVitality = true)
|
|
{
|
|
if (Unkillable || Character.GodMode) { return; }
|
|
if (hitLimb.HealthIndex < 0 || hitLimb.HealthIndex >= limbHealths.Count)
|
|
{
|
|
DebugConsole.ThrowError("Limb health index out of bounds. Character\"" + Character.Name +
|
|
"\" only has health configured for" + limbHealths.Count + " limbs but the limb " + hitLimb.type + " is targeting index " + hitLimb.HealthIndex);
|
|
return;
|
|
}
|
|
|
|
bool? should = null;
|
|
LuaCsSetup.Instance.EventService.PublishEvent<IEventCharacterApplyDamage>(x => should = x.OnCharacterApplyDamage(this, attackResult, hitLimb, allowStacking) ?? should);
|
|
if (should != null && should.Value) { return; }
|
|
|
|
foreach (Affliction newAffliction in attackResult.Afflictions)
|
|
{
|
|
if (newAffliction.Prefab.LimbSpecific)
|
|
{
|
|
AddLimbAffliction(hitLimb, newAffliction, allowStacking, recalculateVitality: recalculateVitality);
|
|
}
|
|
else
|
|
{
|
|
// Always recalculate vitality for non-limb specific afflictions.
|
|
AddAffliction(newAffliction, allowStacking);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void KillIfOutOfVitality()
|
|
{
|
|
if (Vitality <= MinVitality &&
|
|
!Character.HasAbilityFlag(AbilityFlags.CanNotDieToAfflictions))
|
|
{
|
|
Kill();
|
|
}
|
|
}
|
|
|
|
private readonly static List<Affliction> afflictionsToRemove = new List<Affliction>();
|
|
private readonly static List<KeyValuePair<Affliction, LimbHealth>> afflictionsToUpdate = new List<KeyValuePair<Affliction, LimbHealth>>();
|
|
public void SetAllDamage(float damageAmount, float bleedingDamageAmount, float burnDamageAmount)
|
|
{
|
|
if (Unkillable || Character.GodMode) { return; }
|
|
|
|
afflictionsToRemove.Clear();
|
|
afflictionsToRemove.AddRange(afflictions.Keys.Where(a =>
|
|
a.Prefab.AfflictionType == AfflictionPrefab.InternalDamage.AfflictionType ||
|
|
a.Prefab.AfflictionType == AfflictionPrefab.Burn.AfflictionType ||
|
|
a.Prefab.AfflictionType == AfflictionPrefab.Bleeding.AfflictionType));
|
|
foreach (var affliction in afflictionsToRemove)
|
|
{
|
|
afflictions.Remove(affliction);
|
|
}
|
|
|
|
foreach (LimbHealth limbHealth in limbHealths)
|
|
{
|
|
if (damageAmount > 0.0f) { afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(damageAmount), limbHealth); }
|
|
if (bleedingDamageAmount > 0.0f && DoesBleed) { afflictions.Add(AfflictionPrefab.Bleeding.Instantiate(bleedingDamageAmount), limbHealth); }
|
|
if (burnDamageAmount > 0.0f) { afflictions.Add(AfflictionPrefab.Burn.Instantiate(burnDamageAmount), limbHealth); }
|
|
}
|
|
|
|
RecalculateVitality();
|
|
}
|
|
|
|
public float GetLimbDamage(Limb limb, Identifier afflictionType)
|
|
{
|
|
float damageStrength;
|
|
if (limb.IsSevered)
|
|
{
|
|
return 1;
|
|
}
|
|
else
|
|
{
|
|
// Instead of using the limbhealth count here, I think it's best to define the max vitality per limb roughly with a constant value.
|
|
// Therefore with e.g. 80 health, the max damage per limb would be 40.
|
|
// Having at least 40 damage on both legs would cause maximum limping.
|
|
float max = MaxVitality / 2;
|
|
if (afflictionType.IsEmpty)
|
|
{
|
|
float damage = GetAfflictionStrength(AfflictionPrefab.DamageType, limb, true);
|
|
float bleeding = GetAfflictionStrength(AfflictionPrefab.BleedingType, limb, true);
|
|
float burn = GetAfflictionStrength(AfflictionPrefab.BurnType, limb, true);
|
|
damageStrength = Math.Min(damage + bleeding + burn, max);
|
|
}
|
|
else
|
|
{
|
|
damageStrength = Math.Min(GetAfflictionStrength(afflictionType, limb, true), max);
|
|
}
|
|
return damageStrength / max;
|
|
}
|
|
}
|
|
|
|
public void RemoveAfflictions(Func<Affliction, bool> predicate)
|
|
{
|
|
afflictionsToRemove.Clear();
|
|
afflictionsToRemove.AddRange(afflictions.Keys.Where(affliction => predicate(affliction)));
|
|
foreach (var affliction in afflictionsToRemove)
|
|
{
|
|
afflictions.Remove(affliction);
|
|
}
|
|
CalculateVitality();
|
|
}
|
|
|
|
public void RemoveAllAfflictions()
|
|
{
|
|
afflictionsToRemove.Clear();
|
|
afflictionsToRemove.AddRange(afflictions.Keys.Where(a => !irremovableAfflictions.Contains(a)));
|
|
foreach (var affliction in afflictionsToRemove)
|
|
{
|
|
//set strength to 0 in case the affliction needs to react to becoming inactive
|
|
affliction.Strength = 0.0f;
|
|
afflictions.Remove(affliction);
|
|
}
|
|
foreach (Affliction affliction in irremovableAfflictions)
|
|
{
|
|
affliction.Strength = 0.0f;
|
|
}
|
|
CalculateVitality();
|
|
}
|
|
|
|
public void RemoveNegativeAfflictions()
|
|
{
|
|
afflictionsToRemove.Clear();
|
|
afflictionsToRemove.AddRange(afflictions.Keys.Where(a =>
|
|
!irremovableAfflictions.Contains(a) &&
|
|
!a.Prefab.IsBuff &&
|
|
a.Prefab.AfflictionType != "geneticmaterialbuff" &&
|
|
a.Prefab.AfflictionType != "geneticmaterialdebuff"));
|
|
foreach (var affliction in afflictionsToRemove)
|
|
{
|
|
afflictions.Remove(affliction);
|
|
}
|
|
foreach (Affliction affliction in irremovableAfflictions)
|
|
{
|
|
affliction.Strength = 0.0f;
|
|
}
|
|
CalculateVitality();
|
|
}
|
|
|
|
/// <summary>
|
|
///
|
|
/// </summary>
|
|
/// <param name="recalculateVitality">Set false only as an optimization when you manually call <see cref="RecalculateVitality"/></param>
|
|
private void AddLimbAffliction(Limb limb, Affliction newAffliction, bool allowStacking = true, bool recalculateVitality = true)
|
|
{
|
|
if (!newAffliction.Prefab.LimbSpecific || limb == null) { return; }
|
|
if (limb.HealthIndex < 0 || limb.HealthIndex >= limbHealths.Count)
|
|
{
|
|
DebugConsole.ThrowError("Limb health index out of bounds. Character\"" + Character.Name +
|
|
"\" only has health configured for" + limbHealths.Count + " limbs but the limb " + limb.type + " is targeting index " + limb.HealthIndex);
|
|
return;
|
|
}
|
|
AddLimbAffliction(limbHealths[limb.HealthIndex], limb, newAffliction, allowStacking, recalculateVitality);
|
|
}
|
|
|
|
/// <summary>
|
|
///
|
|
/// </summary>
|
|
/// <param name="recalculateVitality">Set false only as an optimization when you manually call <see cref="RecalculateVitality"/></param>
|
|
private void AddLimbAffliction(LimbHealth limbHealth, Limb limb, Affliction newAffliction, bool allowStacking = true, bool recalculateVitality = true)
|
|
{
|
|
LimbType limbType = limb?.type ?? LimbType.None;
|
|
if (Character.Params.IsMachine && !newAffliction.Prefab.AffectMachines) { return; }
|
|
if (!DoesBleed && newAffliction is AfflictionBleeding) { return; }
|
|
if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) { return; }
|
|
if (Character.Params.Health.StunImmunity && newAffliction.Prefab.AfflictionType == AfflictionPrefab.StunType)
|
|
{
|
|
if (Character.EmpVulnerability <= 0 || GetAfflictionStrengthByType(AfflictionPrefab.EMPType, allowLimbAfflictions: false) <= 0)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
if (Character.Params.Health.PoisonImmunity)
|
|
{
|
|
if (newAffliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType || newAffliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
if (Character.EmpVulnerability <= 0 && newAffliction.Prefab.AfflictionType == AfflictionPrefab.EMPType) { return; }
|
|
if (newAffliction.Prefab.TargetSpecies.Any() && newAffliction.Prefab.TargetSpecies.None(s => s == Character.SpeciesName)) { return; }
|
|
if (Character.Params.Health.ImmunityIdentifiers.Contains(newAffliction.Identifier)) { return; }
|
|
|
|
bool? should = null;
|
|
LuaCsSetup.Instance.EventService.PublishEvent<IEventCharacterApplyAffliction>(x => should = x.OnCharacterApplyAffliction(this, limbHealth, newAffliction, allowStacking) ?? should);
|
|
if (should != null && should.Value) { return; }
|
|
|
|
Affliction existingAffliction = null;
|
|
foreach ((Affliction affliction, LimbHealth value) in afflictions)
|
|
{
|
|
if (value == limbHealth && affliction.Prefab == newAffliction.Prefab)
|
|
{
|
|
existingAffliction = affliction;
|
|
break;
|
|
}
|
|
}
|
|
|
|
float modifiedStrength = newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(newAffliction.Prefab, limbType));
|
|
if (newAffliction.Prefab.AfflictionType == AfflictionPrefab.StunType)
|
|
{
|
|
//don't allow stunning for less than one frame
|
|
//fixes monsters/enemies that take some minuscule amount of stun from a weapon still being noticeable affected by the stun,
|
|
//because even a one-frame stun briefly disables the animations and makes the character stop
|
|
if (modifiedStrength < Timing.Step && Stun <= 0.0f)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (existingAffliction != null)
|
|
{
|
|
float newStrength = modifiedStrength;
|
|
if (allowStacking)
|
|
{
|
|
// Add the existing strength
|
|
newStrength += existingAffliction.Strength;
|
|
}
|
|
newStrength = Math.Min(existingAffliction.Prefab.MaxStrength, newStrength);
|
|
existingAffliction.Strength = newStrength;
|
|
//set stun after setting the strength, because stun multipliers might want to set the strength to something else
|
|
if (existingAffliction == stunAffliction) { Character.SetStun(newStrength, allowStunDecrease: true, isNetworkMessage: true); }
|
|
existingAffliction.Duration = existingAffliction.Prefab.Duration;
|
|
if (newAffliction.Source != null) { existingAffliction.Source = newAffliction.Source; }
|
|
if (recalculateVitality)
|
|
{
|
|
RecalculateVitality();
|
|
}
|
|
return;
|
|
}
|
|
|
|
//create a new instance of the affliction to make sure we don't use the same instance for multiple characters
|
|
//or modify the affliction instance of an Attack or a StatusEffect
|
|
var copyAffliction = newAffliction.Prefab.Instantiate(
|
|
Math.Min(newAffliction.Prefab.MaxStrength, modifiedStrength),
|
|
newAffliction.Source);
|
|
afflictions.Add(copyAffliction, limbHealth);
|
|
AchievementManager.OnAfflictionReceived(copyAffliction, Character);
|
|
MedicalClinic.OnAfflictionCountChanged(Character);
|
|
|
|
Character.HealthUpdateInterval = 0.0f;
|
|
|
|
if (recalculateVitality)
|
|
{
|
|
RecalculateVitality();
|
|
}
|
|
#if CLIENT
|
|
if (OpenHealthWindow != this && limbHealth != null)
|
|
{
|
|
selectedLimbIndex = -1;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
private void AddAffliction(Affliction newAffliction, bool allowStacking = true)
|
|
{
|
|
AddLimbAffliction(limbHealth: null, limb: null, newAffliction, allowStacking);
|
|
}
|
|
|
|
partial void UpdateSkinTint();
|
|
|
|
partial void UpdateLimbAfflictionOverlays();
|
|
|
|
public void Update(float deltaTime)
|
|
{
|
|
WasInFullHealth = vitality >= MaxVitality;
|
|
|
|
UpdateOxygen(deltaTime);
|
|
|
|
StunTimer = Stun > 0 ? StunTimer + deltaTime : 0;
|
|
|
|
if (!Character.GodMode)
|
|
{
|
|
afflictionsToRemove.Clear();
|
|
afflictionsToUpdate.Clear();
|
|
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
|
|
{
|
|
var affliction = kvp.Key;
|
|
if (affliction.Strength <= 0.0f)
|
|
{
|
|
AchievementManager.OnAfflictionRemoved(affliction, Character);
|
|
if (!irremovableAfflictions.Contains(affliction)) { afflictionsToRemove.Add(affliction); }
|
|
continue;
|
|
}
|
|
if (affliction.Prefab.Duration > 0.0f)
|
|
{
|
|
affliction.Duration -= deltaTime;
|
|
if (affliction.Duration <= 0.0f)
|
|
{
|
|
//set strength to 0 in case the affliction needs to react to becoming inactive
|
|
affliction.Strength = 0.0f;
|
|
afflictionsToRemove.Add(affliction);
|
|
continue;
|
|
}
|
|
}
|
|
afflictionsToUpdate.Add(kvp);
|
|
}
|
|
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictionsToUpdate)
|
|
{
|
|
var affliction = kvp.Key;
|
|
Limb targetLimb = null;
|
|
if (kvp.Value != null)
|
|
{
|
|
int healthIndex = limbHealths.IndexOf(kvp.Value);
|
|
targetLimb =
|
|
Character.AnimController.Limbs.LastOrDefault(l => !l.IsSevered && !l.Hidden && l.HealthIndex == healthIndex) ??
|
|
Character.AnimController.MainLimb;
|
|
}
|
|
affliction.Update(this, targetLimb, deltaTime);
|
|
affliction.DamagePerSecondTimer += deltaTime;
|
|
if (affliction is AfflictionBleeding bleeding)
|
|
{
|
|
UpdateBleedingProjSpecific(bleeding, targetLimb, deltaTime);
|
|
}
|
|
Character.StackSpeedMultiplier(affliction.GetSpeedMultiplier());
|
|
}
|
|
|
|
foreach (var affliction in afflictionsToRemove)
|
|
{
|
|
afflictions.Remove(affliction);
|
|
}
|
|
|
|
if (afflictionsToRemove.Count is not 0)
|
|
{
|
|
MedicalClinic.OnAfflictionCountChanged(Character);
|
|
}
|
|
}
|
|
|
|
Character.StackSpeedMultiplier(1f + Character.GetStatValue(StatTypes.MovementSpeed));
|
|
if (Character.InWater)
|
|
{
|
|
Character.StackSpeedMultiplier(1f + Character.GetStatValue(StatTypes.SwimmingSpeed));
|
|
}
|
|
else
|
|
{
|
|
Character.StackSpeedMultiplier(1f + Character.GetStatValue(StatTypes.WalkingSpeed));
|
|
}
|
|
|
|
UpdateDamageReductions(deltaTime);
|
|
|
|
if (!Character.GodMode)
|
|
{
|
|
#if CLIENT
|
|
updateVisualsTimer -= deltaTime;
|
|
if (Character.IsVisible && updateVisualsTimer <= 0.0f)
|
|
{
|
|
UpdateLimbAfflictionOverlays();
|
|
UpdateSkinTint();
|
|
updateVisualsTimer = UpdateVisualsInterval;
|
|
}
|
|
#endif
|
|
RecalculateVitality();
|
|
}
|
|
}
|
|
|
|
public void ForceUpdateVisuals()
|
|
{
|
|
UpdateLimbAfflictionOverlays();
|
|
UpdateSkinTint();
|
|
}
|
|
|
|
private void UpdateDamageReductions(float deltaTime)
|
|
{
|
|
float healthRegen = Character.Params.Health.ConstantHealthRegeneration;
|
|
if (healthRegen > 0)
|
|
{
|
|
ReduceAfflictionOnAllLimbs("damage".ToIdentifier(), healthRegen * deltaTime);
|
|
}
|
|
float burnReduction = Character.Params.Health.BurnReduction;
|
|
if (burnReduction > 0)
|
|
{
|
|
ReduceAfflictionOnAllLimbs("burn".ToIdentifier(), burnReduction * deltaTime);
|
|
}
|
|
float bleedingReduction = Character.Params.Health.BleedingReduction;
|
|
if (bleedingReduction > 0)
|
|
{
|
|
ReduceAfflictionOnAllLimbs("bleeding".ToIdentifier(), bleedingReduction * deltaTime);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 0-1.
|
|
/// </summary>
|
|
public float OxygenLowResistance => !Character.NeedsOxygen ? 1 : GetResistance(oxygenLowAffliction.Prefab, LimbType.None);
|
|
|
|
private void UpdateOxygen(float deltaTime)
|
|
{
|
|
if (!Character.NeedsOxygen)
|
|
{
|
|
oxygenLowAffliction.Strength = 0.0f;
|
|
return;
|
|
}
|
|
|
|
float oxygenlowResistance = GetResistance(oxygenLowAffliction.Prefab, LimbType.None);
|
|
float prevOxygen = OxygenAmount;
|
|
if (IsUnconscious)
|
|
{
|
|
//clamp above 0.1 (no amount of oxygen low resistance should keep the character alive indefinitely)
|
|
float decreaseSpeed = Math.Max(0.1f, 1f - oxygenlowResistance);
|
|
//the character dies of oxygen deprivation in 100 seconds after losing consciousness
|
|
OxygenAmount = MathHelper.Clamp(OxygenAmount - decreaseSpeed * deltaTime, -100.0f, 100.0f);
|
|
}
|
|
else
|
|
{
|
|
float decreaseSpeed = -5.0f;
|
|
float increaseSpeed = 10.0f;
|
|
decreaseSpeed *= (1f - oxygenlowResistance);
|
|
increaseSpeed *= (1f + oxygenlowResistance);
|
|
float holdBreathMultiplier = Character.GetStatValue(StatTypes.HoldBreathMultiplier);
|
|
if (holdBreathMultiplier <= -1.0f)
|
|
{
|
|
OxygenAmount = -100.0f;
|
|
}
|
|
else
|
|
{
|
|
decreaseSpeed /= 1.0f + Character.GetStatValue(StatTypes.HoldBreathMultiplier);
|
|
OxygenAmount = MathHelper.Clamp(OxygenAmount + deltaTime * (Character.OxygenAvailable < InsufficientOxygenThreshold ? decreaseSpeed : increaseSpeed), -100.0f, 100.0f);
|
|
}
|
|
}
|
|
|
|
UpdateOxygenProjSpecific(prevOxygen, deltaTime);
|
|
}
|
|
|
|
partial void UpdateOxygenProjSpecific(float prevOxygen, float deltaTime);
|
|
|
|
partial void UpdateBleedingProjSpecific(AfflictionBleeding affliction, Limb targetLimb, float deltaTime);
|
|
|
|
public void SetVitality(float newVitality)
|
|
{
|
|
UnmodifiedMaxVitality = newVitality;
|
|
CalculateVitality();
|
|
}
|
|
|
|
private void CalculateVitality()
|
|
{
|
|
vitality = MaxVitality;
|
|
IsParalyzed = false;
|
|
if (Unkillable || Character.GodMode) { return; }
|
|
|
|
foreach ((Affliction affliction, LimbHealth limbHealth) in afflictions)
|
|
{
|
|
float vitalityDecrease = affliction.GetVitalityDecrease(this);
|
|
if (limbHealth != null)
|
|
{
|
|
vitalityDecrease *= GetVitalityMultiplier(affliction, limbHealth);
|
|
}
|
|
vitality -= vitalityDecrease;
|
|
affliction.CalculateDamagePerSecond(vitalityDecrease);
|
|
|
|
if (affliction.Strength >= affliction.Prefab.MaxStrength &&
|
|
affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType)
|
|
{
|
|
IsParalyzed = true;
|
|
}
|
|
}
|
|
#if CLIENT
|
|
if (IsUnconscious)
|
|
{
|
|
HintManager.OnCharacterUnconscious(Character);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
public void RecalculateVitality()
|
|
{
|
|
CalculateVitality();
|
|
KillIfOutOfVitality();
|
|
}
|
|
|
|
private static float GetVitalityMultiplier(Affliction affliction, LimbHealth limbHealth)
|
|
{
|
|
float multiplier = 1.0f;
|
|
if (limbHealth.VitalityMultipliers.TryGetValue(affliction.Prefab.Identifier, out float vitalityMultiplier))
|
|
{
|
|
multiplier *= vitalityMultiplier;
|
|
}
|
|
if (limbHealth.VitalityTypeMultipliers.TryGetValue(affliction.Prefab.AfflictionType, out float vitalityTypeMultiplier))
|
|
{
|
|
multiplier *= vitalityTypeMultiplier;
|
|
}
|
|
return multiplier;
|
|
}
|
|
|
|
/// <summary>
|
|
/// How much vitality the affliction reduces, taking into account the effects of vitality modifiers on the limb the affliction is on (if limb-based)
|
|
/// </summary>
|
|
private float GetVitalityDecreaseWithVitalityMultipliers(Affliction affliction)
|
|
{
|
|
float vitalityDecrease = affliction.GetVitalityDecrease(this);
|
|
if (afflictions.TryGetValue(affliction, out LimbHealth limbHealth) && limbHealth != null)
|
|
{
|
|
vitalityDecrease *= GetVitalityMultiplier(affliction, limbHealth);
|
|
}
|
|
return vitalityDecrease;
|
|
}
|
|
|
|
private void Kill()
|
|
{
|
|
if (Unkillable || Character.GodMode) { return; }
|
|
|
|
var (type, affliction) = GetCauseOfDeath();
|
|
UpdateLimbAfflictionOverlays();
|
|
UpdateSkinTint();
|
|
Character.Kill(type, affliction);
|
|
|
|
WasInFullHealth = false;
|
|
#if CLIENT
|
|
DisplayVitalityDelay = 0.0f;
|
|
DisplayedVitality = Vitality;
|
|
#endif
|
|
}
|
|
|
|
// We need to use another list of the afflictions when we call the status effects triggered by afflictions,
|
|
// because those status effects may add or remove other afflictions while iterating the collection.
|
|
private readonly List<Affliction> afflictionsCopy = [];
|
|
|
|
private bool isApplyingAfflictionStatusEffects;
|
|
public void ApplyAfflictionStatusEffects(ActionType type)
|
|
{
|
|
if (isApplyingAfflictionStatusEffects)
|
|
{
|
|
//pretty hacky: if we're already in the process of applying afflictions' status effects
|
|
//(i.e. calling this method caused some additional afflictions to appear and trigger status effects)
|
|
//let's instantiate a new list so we don't end up modifying afflictionsCopy while enumerating it
|
|
foreach (Affliction affliction in afflictions.Keys.ToList())
|
|
{
|
|
affliction.ApplyStatusEffects(type, 1.0f, this, targetLimb: GetAfflictionLimb(affliction));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
isApplyingAfflictionStatusEffects = true;
|
|
afflictionsCopy.Clear();
|
|
afflictionsCopy.AddRange(afflictions.Keys);
|
|
isApplyingAfflictionStatusEffects = true;
|
|
foreach (Affliction affliction in afflictionsCopy)
|
|
{
|
|
affliction.ApplyStatusEffects(type, 1.0f, this, targetLimb: GetAfflictionLimb(affliction));
|
|
}
|
|
isApplyingAfflictionStatusEffects = false;
|
|
}
|
|
}
|
|
|
|
public (CauseOfDeathType type, Affliction affliction) GetCauseOfDeath()
|
|
{
|
|
IEnumerable<Affliction> currentAfflictions = GetAllAfflictions(true);
|
|
|
|
Affliction strongestAffliction = null;
|
|
float largestStrength = 0.0f;
|
|
foreach (Affliction affliction in currentAfflictions)
|
|
{
|
|
if (strongestAffliction == null || affliction.GetVitalityDecrease(this) > largestStrength)
|
|
{
|
|
strongestAffliction = affliction;
|
|
largestStrength = affliction.GetVitalityDecrease(this);
|
|
}
|
|
}
|
|
|
|
CauseOfDeathType causeOfDeath = strongestAffliction == null ? CauseOfDeathType.Unknown : CauseOfDeathType.Affliction;
|
|
if (strongestAffliction == oxygenLowAffliction)
|
|
{
|
|
causeOfDeath = Character.AnimController.InWater ? CauseOfDeathType.Drowning : CauseOfDeathType.Suffocation;
|
|
}
|
|
|
|
return (causeOfDeath, strongestAffliction);
|
|
}
|
|
|
|
private readonly List<Affliction> allAfflictions = new List<Affliction>();
|
|
private IEnumerable<Affliction> GetAllAfflictions(bool mergeSameAfflictions, Func<Affliction, bool> predicate = null)
|
|
{
|
|
allAfflictions.Clear();
|
|
if (!mergeSameAfflictions)
|
|
{
|
|
allAfflictions.AddRange(predicate == null ? afflictions.Keys : afflictions.Keys.Where(predicate));
|
|
}
|
|
else
|
|
{
|
|
foreach (Affliction affliction in afflictions.Keys)
|
|
{
|
|
if (predicate != null && !predicate(affliction)) { continue; }
|
|
var existingAffliction = allAfflictions.Find(a => a.Prefab == affliction.Prefab);
|
|
if (existingAffliction == null)
|
|
{
|
|
var newAffliction = affliction.Prefab.Instantiate(affliction.Strength);
|
|
if (affliction.Source != null) { newAffliction.Source = affliction.Source; }
|
|
newAffliction.DamagePerSecond = affliction.DamagePerSecond;
|
|
newAffliction.DamagePerSecondTimer = affliction.DamagePerSecondTimer;
|
|
allAfflictions.Add(newAffliction);
|
|
}
|
|
else
|
|
{
|
|
existingAffliction.DamagePerSecond += affliction.DamagePerSecond;
|
|
existingAffliction.Strength += affliction.Strength;
|
|
}
|
|
}
|
|
}
|
|
return allAfflictions;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the identifiers of the items that can be used to treat the character. Takes into account all the afflictions the character has,
|
|
/// and negative treatment suitabilities (e.g. a medicine that causes oxygen loss may not be suitable if the character is already suffocating)
|
|
/// </summary>
|
|
/// <param name="treatmentSuitability">A dictionary where the key is the identifier of the item and the value the suitability</param>
|
|
/// <param name="predictFutureDuration">If above 0, the method will take into account how much currently active status effects while affect the afflictions in the next x seconds.</param>
|
|
/// <param name="checkTreatmentThreshold">Should the method check whether the afflictions are above <see cref="AfflictionPrefab.TreatmentThreshold"/> (whether they're severe enough for AI to treat)?</param>
|
|
/// <param name="checkTreatmentSuggestionThreshold">Should the method check whether the afflictions are above <see cref="AfflictionPrefab.TreatmentSuggestionThreshold"/> (whether treatment suggestions are shown in the health interface)?</param>
|
|
public void GetSuitableTreatments(Dictionary<Identifier, float> treatmentSuitability, Character user, Limb limb = null, bool ignoreHiddenAfflictions = false,
|
|
bool checkTreatmentThreshold = true, bool checkTreatmentSuggestionThreshold = true,
|
|
float predictFutureDuration = 0.0f)
|
|
{
|
|
//key = item identifier
|
|
//float = suitability
|
|
treatmentSuitability.Clear();
|
|
float minSuitability = -10, maxSuitability = 10;
|
|
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
|
|
{
|
|
var affliction = kvp.Key;
|
|
var limbHealth = kvp.Value;
|
|
if (limb != null &&
|
|
affliction.Prefab.LimbSpecific &&
|
|
GetMatchingLimbHealth(affliction) != GetMatchingLimbHealth(limb))
|
|
{
|
|
if (limbHealth == null) { continue; }
|
|
int healthIndex = limbHealths.IndexOf(limbHealth);
|
|
if (limb.HealthIndex != healthIndex) { continue; }
|
|
}
|
|
|
|
float strength = affliction.Strength;
|
|
if (predictFutureDuration > 0.0f)
|
|
{
|
|
strength = GetPredictedStrength(affliction, predictFutureDuration, limb);
|
|
}
|
|
|
|
//other afflictions of the same type increase the "treatability"
|
|
// e.g. we might want to ignore burns below 5%, but not if the character has them on all limbs
|
|
float totalAfflictionStrength = strength + GetTotalAdjustedAfflictionStrength(affliction, includeSameAffliction: false);
|
|
|
|
if (afflictions.Any(otherAffliction => affliction.Prefab.IgnoreTreatmentIfAfflictedBy.Contains(otherAffliction.Key.Identifier))) { continue; }
|
|
|
|
if (ignoreHiddenAfflictions)
|
|
{
|
|
if (user == Character)
|
|
{
|
|
if (strength < affliction.Prefab.ShowIconThreshold) { continue; }
|
|
}
|
|
else
|
|
{
|
|
if (strength < affliction.Prefab.ShowIconToOthersThreshold) { continue; }
|
|
}
|
|
}
|
|
|
|
foreach (KeyValuePair<Identifier, float> treatment in affliction.Prefab.TreatmentSuitabilities)
|
|
{
|
|
float suitability = treatment.Value * strength;
|
|
if (suitability > 0)
|
|
{
|
|
//if this a suitable treatment, ignore it if the affliction isn't severe enough to treat
|
|
//if the suitability is negative though, we need to take it into account!
|
|
//otherwise we may end up e.g. giving too much opiates to someone already close to overdosing
|
|
if (checkTreatmentThreshold)
|
|
{
|
|
if (totalAfflictionStrength < affliction.Prefab.TreatmentThreshold) { continue; }
|
|
}
|
|
if (checkTreatmentSuggestionThreshold)
|
|
{
|
|
if (totalAfflictionStrength < affliction.Prefab.TreatmentSuggestionThreshold) { continue; }
|
|
}
|
|
}
|
|
if (treatment.Value > strength)
|
|
{
|
|
//avoid using very effective meds on small injuries
|
|
float overtreatmentFactor = MathHelper.Clamp(treatment.Value / strength, 1.0f, 10.0f);
|
|
suitability /= overtreatmentFactor;
|
|
}
|
|
if (!treatmentSuitability.ContainsKey(treatment.Key))
|
|
{
|
|
treatmentSuitability[treatment.Key] = suitability;
|
|
}
|
|
else
|
|
{
|
|
treatmentSuitability[treatment.Key] += suitability;
|
|
}
|
|
minSuitability = Math.Min(treatmentSuitability[treatment.Key], minSuitability);
|
|
maxSuitability = Math.Max(treatmentSuitability[treatment.Key], maxSuitability);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the total strength of instances of the same affliction on all the characters limbs,
|
|
/// with a smaller weight given to the other afflictions on other limbs
|
|
/// </summary>
|
|
/// <param name="otherAfflictionMultiplier">Multiplier on the strengths of the afflictions on other limbs.</param>
|
|
/// <param name="includeSameAffliction">Should the strength of the provided affliction be included too?</param>
|
|
public float GetTotalAdjustedAfflictionStrength(Affliction affliction, float otherAfflictionMultiplier = 0.3f, bool includeSameAffliction = true)
|
|
{
|
|
float totalAfflictionStrength = includeSameAffliction ? affliction.Strength : 0;
|
|
if (affliction.Prefab.LimbSpecific)
|
|
{
|
|
foreach (Affliction otherAffliction in afflictions.Keys)
|
|
{
|
|
if (affliction.Prefab == otherAffliction.Prefab && affliction != otherAffliction)
|
|
{
|
|
totalAfflictionStrength += otherAffliction.Strength * otherAfflictionMultiplier;
|
|
}
|
|
}
|
|
}
|
|
return totalAfflictionStrength;
|
|
}
|
|
|
|
private readonly HashSet<Identifier> afflictionTags = new HashSet<Identifier>();
|
|
public IEnumerable<Identifier> GetActiveAfflictionTags()
|
|
{
|
|
afflictionTags.Clear();
|
|
foreach (Affliction affliction in afflictions.Keys)
|
|
{
|
|
var currentEffect = affliction.GetActiveEffect();
|
|
if (currentEffect is { Tag.IsEmpty: false })
|
|
{
|
|
afflictionTags.Add(currentEffect.Tag);
|
|
}
|
|
}
|
|
return afflictionTags;
|
|
}
|
|
|
|
public float GetPredictedStrength(Affliction affliction, float predictFutureDuration, Limb limb = null)
|
|
{
|
|
float strength = affliction.Strength;
|
|
foreach (var statusEffect in StatusEffect.DurationList)
|
|
{
|
|
if (!statusEffect.Targets.Any(t => t == Character || (limb != null && Character.AnimController.Limbs.Contains(t)))) { continue; }
|
|
float statusEffectDuration = Math.Min(statusEffect.Timer, predictFutureDuration);
|
|
foreach (var statusEffectAffliction in statusEffect.Parent.Afflictions)
|
|
{
|
|
if (statusEffectAffliction.Prefab == affliction.Prefab)
|
|
{
|
|
strength += statusEffectAffliction.Strength * statusEffectDuration;
|
|
}
|
|
}
|
|
foreach (var statusEffectAffliction in statusEffect.Parent.ReduceAffliction)
|
|
{
|
|
if (statusEffectAffliction.AfflictionIdentifier == affliction.Identifier ||
|
|
statusEffectAffliction.AfflictionIdentifier == affliction.Prefab.AfflictionType)
|
|
{
|
|
strength -= statusEffectAffliction.ReduceAmount * statusEffectDuration;
|
|
}
|
|
}
|
|
}
|
|
return MathHelper.Clamp(strength, 0.0f, affliction.Prefab.MaxStrength);
|
|
}
|
|
|
|
private readonly List<Affliction> activeAfflictions = new List<Affliction>();
|
|
private readonly List<(LimbHealth limbHealth, Affliction affliction)> limbAfflictions = new List<(LimbHealth limbHealth, Affliction affliction)>();
|
|
public void ServerWrite(IWriteMessage msg)
|
|
{
|
|
activeAfflictions.Clear();
|
|
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
|
|
{
|
|
var affliction = kvp.Key;
|
|
var limbHealth = kvp.Value;
|
|
if (limbHealth != null) { continue; }
|
|
if (affliction.Strength > 0.0f && affliction.Strength >= affliction.Prefab.ActivationThreshold)
|
|
{
|
|
activeAfflictions.Add(affliction);
|
|
}
|
|
}
|
|
msg.WriteByte((byte)activeAfflictions.Count);
|
|
foreach (Affliction affliction in activeAfflictions)
|
|
{
|
|
msg.WriteUInt32(affliction.Prefab.UintIdentifier);
|
|
msg.WriteRangedSingle(
|
|
MathHelper.Clamp(affliction.Strength, 0.0f, affliction.Prefab.MaxStrength),
|
|
0.0f, affliction.Prefab.MaxStrength, 8);
|
|
msg.WriteByte((byte)affliction.Prefab.PeriodicEffects.Count);
|
|
foreach (AfflictionPrefab.PeriodicEffect periodicEffect in affliction.Prefab.PeriodicEffects)
|
|
{
|
|
msg.WriteRangedSingle(affliction.PeriodicEffectTimers[periodicEffect], 0, periodicEffect.MaxInterval, 8);
|
|
}
|
|
}
|
|
|
|
limbAfflictions.Clear();
|
|
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
|
|
{
|
|
var limbAffliction = kvp.Key;
|
|
var limbHealth = kvp.Value;
|
|
if (limbHealth == null) { continue; }
|
|
if (limbAffliction.Strength <= 0.0f || limbAffliction.Strength < limbAffliction.Prefab.ActivationThreshold) { continue; }
|
|
limbAfflictions.Add((limbHealth, limbAffliction));
|
|
}
|
|
|
|
msg.WriteByte((byte)limbAfflictions.Count);
|
|
foreach (var (limbHealth, affliction) in limbAfflictions)
|
|
{
|
|
msg.WriteRangedInteger(limbHealths.IndexOf(limbHealth), 0, limbHealths.Count - 1);
|
|
msg.WriteUInt32(affliction.Prefab.UintIdentifier);
|
|
msg.WriteRangedSingle(
|
|
MathHelper.Clamp(affliction.Strength, 0.0f, affliction.Prefab.MaxStrength),
|
|
0.0f, affliction.Prefab.MaxStrength, 8);
|
|
msg.WriteByte((byte)affliction.Prefab.PeriodicEffects.Count);
|
|
foreach (AfflictionPrefab.PeriodicEffect periodicEffect in affliction.Prefab.PeriodicEffects)
|
|
{
|
|
msg.WriteRangedSingle(affliction.PeriodicEffectTimers[periodicEffect], periodicEffect.MinInterval, periodicEffect.MaxInterval, 8);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void Remove()
|
|
{
|
|
RemoveProjSpecific();
|
|
afflictionsToRemove.Clear();
|
|
afflictionsToUpdate.Clear();
|
|
}
|
|
|
|
partial void RemoveProjSpecific();
|
|
|
|
/// <summary>
|
|
/// Automatically filters out buffs.
|
|
/// </summary>
|
|
public static IEnumerable<Affliction> SortAfflictionsBySeverity(IEnumerable<Affliction> afflictions, bool excludeBuffs = true) =>
|
|
afflictions.Where(a => !excludeBuffs || !a.Prefab.IsBuff).OrderByDescending(a => a.DamagePerSecond).ThenByDescending(a => a.Strength / a.Prefab.MaxStrength);
|
|
|
|
public void Save(XElement healthElement)
|
|
{
|
|
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
|
|
{
|
|
var affliction = kvp.Key;
|
|
var limbHealth = kvp.Value;
|
|
if (affliction.Strength <= 0.0f || limbHealth != null) { continue; }
|
|
if (kvp.Key.Prefab.ResetBetweenRounds) { continue; }
|
|
healthElement.Add(new XElement("Affliction",
|
|
new XAttribute("identifier", affliction.Identifier),
|
|
new XAttribute("strength", affliction.Strength.ToString("G", CultureInfo.InvariantCulture))));
|
|
}
|
|
|
|
for (int i = 0; i < limbHealths.Count; i++)
|
|
{
|
|
var limbHealthElement = new XElement("LimbHealth", new XAttribute("i", i));
|
|
healthElement.Add(limbHealthElement);
|
|
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions.Where(a => a.Value == limbHealths[i]))
|
|
{
|
|
var affliction = kvp.Key;
|
|
var limbHealth = kvp.Value;
|
|
if (affliction.Strength <= 0.0f) { continue; }
|
|
limbHealthElement.Add(new XElement("Affliction",
|
|
new XAttribute("identifier", affliction.Identifier),
|
|
new XAttribute("strength", affliction.Strength.ToString("G", CultureInfo.InvariantCulture))));
|
|
}
|
|
}
|
|
}
|
|
|
|
public void Load(XElement element, Func<AfflictionPrefab, bool> afflictionPredicate = null)
|
|
{
|
|
foreach (var subElement in element.Elements())
|
|
{
|
|
switch (subElement.Name.ToString().ToLowerInvariant())
|
|
{
|
|
case "affliction":
|
|
LoadAffliction(subElement);
|
|
break;
|
|
case "limbhealth":
|
|
int limbHealthIndex = subElement.GetAttributeInt("i", -1);
|
|
if (limbHealthIndex < 0 || limbHealthIndex >= limbHealths.Count)
|
|
{
|
|
DebugConsole.ThrowError($"Error while loading character health: limb index \"{limbHealthIndex}\" out of range.");
|
|
continue;
|
|
}
|
|
foreach (XElement afflictionElement in subElement.Elements())
|
|
{
|
|
LoadAffliction(afflictionElement, limbHealths[limbHealthIndex]);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
void LoadAffliction(XElement afflictionElement, LimbHealth limbHealth = null)
|
|
{
|
|
string id = afflictionElement.GetAttributeString("identifier", "");
|
|
var afflictionPrefab = AfflictionPrefab.Prefabs.Find(a => a.Identifier == id);
|
|
if (afflictionPrefab == null)
|
|
{
|
|
DebugConsole.ThrowError($"Error while loading character health: affliction \"{id}\" not found.");
|
|
return;
|
|
}
|
|
if (afflictionPredicate != null && !afflictionPredicate.Invoke(afflictionPrefab)) { return; }
|
|
float strength = afflictionElement.GetAttributeFloat("strength", 0.0f);
|
|
var irremovableAffliction = irremovableAfflictions.FirstOrDefault(a => a.Prefab == afflictionPrefab);
|
|
if (irremovableAffliction != null)
|
|
{
|
|
irremovableAffliction.Strength = strength;
|
|
}
|
|
else
|
|
{
|
|
afflictions.Add(afflictionPrefab.Instantiate(strength), limbHealth);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|