523 lines
22 KiB
C#
523 lines
22 KiB
C#
using Barotrauma.Extensions;
|
|
using Microsoft.Xna.Framework;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Xml.Linq;
|
|
|
|
namespace Barotrauma
|
|
{
|
|
class Affliction : ISerializableEntity
|
|
{
|
|
public readonly AfflictionPrefab Prefab;
|
|
|
|
public string Name => ToString();
|
|
|
|
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; set; }
|
|
|
|
public float PendingGrainEffectStrength { get; set; }
|
|
public float GrainEffectStrength { get; set; }
|
|
|
|
private float fluctuationTimer;
|
|
|
|
private AfflictionPrefab.Effect activeEffect;
|
|
private float prevActiveEffectStrength;
|
|
protected bool activeEffectDirty = true;
|
|
|
|
protected float _strength;
|
|
|
|
[Serialize(0f, IsPropertySaveable.Yes), Editable]
|
|
public virtual float Strength
|
|
{
|
|
get { return _strength; }
|
|
set
|
|
{
|
|
if (!MathUtils.IsValid(value))
|
|
{
|
|
#if DEBUG
|
|
DebugConsole.ThrowError($"Attempted to set an affliction to an invalid strength ({value})\n" + Environment.StackTrace.CleanupStackTrace());
|
|
#endif
|
|
return;
|
|
}
|
|
|
|
if (_nonClampedStrength < 0 && value > 0)
|
|
{
|
|
_nonClampedStrength = value;
|
|
}
|
|
float newValue = MathHelper.Clamp(value, 0.0f, Prefab.MaxStrength);
|
|
if (newValue > _strength)
|
|
{
|
|
PendingGrainEffectStrength = Prefab.GrainBurst;
|
|
Duration = Prefab.Duration;
|
|
}
|
|
_strength = newValue;
|
|
activeEffectDirty = true;
|
|
}
|
|
}
|
|
|
|
private float _nonClampedStrength = -1;
|
|
public float NonClampedStrength => _nonClampedStrength > 0 ? _nonClampedStrength : _strength;
|
|
|
|
[Serialize("", IsPropertySaveable.Yes), Editable]
|
|
public Identifier Identifier { get; private set; }
|
|
|
|
[Serialize(1.0f, IsPropertySaveable.Yes, description: "The probability for the affliction to be applied."), Editable(minValue: 0f, maxValue: 1f)]
|
|
public float Probability { get; set; } = 1.0f;
|
|
|
|
[Serialize(true, IsPropertySaveable.Yes, description: "Explosion damage is applied per each affected limb. Should this affliction damage be divided by the count of affected limbs (1-15) or applied in full? Default: true. Only affects explosions."), Editable]
|
|
public bool DivideByLimbCount { get; set; }
|
|
|
|
[Serialize(false, IsPropertySaveable.Yes, description: "Is the damage relative to the max vitality (percentage) or absolute (normal)"), Editable]
|
|
public bool MultiplyByMaxVitality { get; set; }
|
|
|
|
public float DamagePerSecond;
|
|
public float DamagePerSecondTimer;
|
|
public float PreviousVitalityDecrease;
|
|
|
|
public (float Value, Affliction Source) StrengthDiminishMultiplier = (1.0f, null);
|
|
|
|
public readonly Dictionary<AfflictionPrefab.PeriodicEffect, float> PeriodicEffectTimers = new Dictionary<AfflictionPrefab.PeriodicEffect, float>();
|
|
|
|
public double AppliedAsSuccessfulTreatmentTime, AppliedAsFailedTreatmentTime;
|
|
|
|
public float Duration;
|
|
|
|
/// <summary>
|
|
/// Which character gave this affliction
|
|
/// </summary>
|
|
public Character Source;
|
|
|
|
private readonly static LocalizedString[] strengthTexts = new LocalizedString[]
|
|
{
|
|
TextManager.Get("AfflictionStrengthLow"),
|
|
TextManager.Get("AfflictionStrengthMedium"),
|
|
TextManager.Get("AfflictionStrengthHigh")
|
|
};
|
|
|
|
public Affliction(AfflictionPrefab prefab, float strength)
|
|
{
|
|
#if CLIENT
|
|
prefab?.ReloadSoundsIfNeeded();
|
|
#endif
|
|
Prefab = prefab;
|
|
PendingGrainEffectStrength = Prefab.GrainBurst;
|
|
_strength = strength;
|
|
Identifier = prefab.Identifier;
|
|
|
|
Duration = prefab.Duration;
|
|
|
|
foreach (var periodicEffect in prefab.PeriodicEffects)
|
|
{
|
|
PeriodicEffectTimers[periodicEffect] = Rand.Range(periodicEffect.MinInterval, periodicEffect.MaxInterval);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Copy properties here instead of using SerializableProperties (with reflection).
|
|
/// </summary>
|
|
public void CopyProperties(Affliction source)
|
|
{
|
|
Probability = source.Probability;
|
|
DivideByLimbCount = source.DivideByLimbCount;
|
|
MultiplyByMaxVitality = source.MultiplyByMaxVitality;
|
|
}
|
|
|
|
public void Serialize(XElement element)
|
|
{
|
|
SerializableProperty.SerializeProperties(this, element);
|
|
}
|
|
|
|
public void Deserialize(XElement element)
|
|
{
|
|
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
|
|
//backwards compatibility
|
|
if (element.GetAttribute("amount") != null && element.GetAttribute("strength") == null)
|
|
{
|
|
Strength = element.GetAttributeFloat("amount", 0.0f);
|
|
}
|
|
}
|
|
|
|
public Affliction CreateMultiplied(float multiplier, Affliction affliction)
|
|
{
|
|
Affliction instance = Prefab.Instantiate(NonClampedStrength * multiplier, Source);
|
|
instance.CopyProperties(affliction);
|
|
return instance;
|
|
}
|
|
|
|
public override string ToString() => Prefab == null ? "Affliction (Invalid)" : $"Affliction ({Prefab.Name})";
|
|
|
|
public LocalizedString GetStrengthText()
|
|
{
|
|
return GetStrengthText(Strength, Prefab.MaxStrength);
|
|
}
|
|
|
|
public static LocalizedString GetStrengthText(float strength, float maxStrength)
|
|
{
|
|
return strengthTexts[
|
|
MathHelper.Clamp((int)Math.Floor(strength / maxStrength * strengthTexts.Length), 0, strengthTexts.Length - 1)];
|
|
}
|
|
|
|
public AfflictionPrefab.Effect GetActiveEffect()
|
|
{
|
|
if (activeEffectDirty)
|
|
{
|
|
activeEffect = Prefab.GetActiveEffect(_strength);
|
|
prevActiveEffectStrength = _strength;
|
|
activeEffectDirty = false;
|
|
}
|
|
return activeEffect;
|
|
}
|
|
|
|
public float GetVitalityDecrease(CharacterHealth characterHealth)
|
|
{
|
|
return GetVitalityDecrease(characterHealth, Strength);
|
|
}
|
|
|
|
public float GetVitalityDecrease(CharacterHealth characterHealth, float strength)
|
|
{
|
|
if (strength < Prefab.ActivationThreshold) { return 0.0f; }
|
|
strength = MathHelper.Clamp(strength, 0.0f, Prefab.MaxStrength);
|
|
AfflictionPrefab.Effect currentEffect = GetActiveEffect();
|
|
if (currentEffect == null) { return 0.0f; }
|
|
if (currentEffect.MaxStrength - currentEffect.MinStrength <= 0.0f) { return 0.0f; }
|
|
|
|
float currVitalityDecrease = MathHelper.Lerp(
|
|
currentEffect.MinVitalityDecrease,
|
|
currentEffect.MaxVitalityDecrease,
|
|
currentEffect.GetStrengthFactor(strength));
|
|
|
|
if (currentEffect.MultiplyByMaxVitality)
|
|
{
|
|
currVitalityDecrease *= characterHealth?.MaxVitality ?? 100.0f;
|
|
}
|
|
|
|
return currVitalityDecrease;
|
|
}
|
|
|
|
|
|
public float GetScreenGrainStrength()
|
|
{
|
|
if (Strength < Prefab.ActivationThreshold) { return 0.0f; }
|
|
AfflictionPrefab.Effect currentEffect = GetActiveEffect();
|
|
if (currentEffect == null) { return 0.0f; }
|
|
if (MathUtils.NearlyEqual(currentEffect.MaxGrainStrength, 0f)) { return 0.0f; }
|
|
|
|
float amount = MathHelper.Lerp(
|
|
currentEffect.MinGrainStrength,
|
|
currentEffect.MaxGrainStrength,
|
|
currentEffect.GetStrengthFactor(this)) * GetScreenEffectFluctuation(currentEffect);
|
|
|
|
if (Prefab.GrainBurst > 0 && GrainEffectStrength > amount)
|
|
{
|
|
return Math.Min(GrainEffectStrength, 1.0f);
|
|
}
|
|
|
|
return amount;
|
|
}
|
|
|
|
public float GetScreenDistortStrength()
|
|
{
|
|
if (Strength < Prefab.ActivationThreshold) { return 0.0f; }
|
|
AfflictionPrefab.Effect currentEffect = GetActiveEffect();
|
|
if (currentEffect == null) { return 0.0f; }
|
|
if (currentEffect.MaxScreenDistort - currentEffect.MinScreenDistort < 0.0f) { return 0.0f; }
|
|
|
|
return MathHelper.Lerp(
|
|
currentEffect.MinScreenDistort,
|
|
currentEffect.MaxScreenDistort,
|
|
currentEffect.GetStrengthFactor(this)) * GetScreenEffectFluctuation(currentEffect);
|
|
}
|
|
|
|
public float GetRadialDistortStrength()
|
|
{
|
|
if (Strength < Prefab.ActivationThreshold) { return 0.0f; }
|
|
AfflictionPrefab.Effect currentEffect = GetActiveEffect();
|
|
if (currentEffect == null) { return 0.0f; }
|
|
if (currentEffect.MaxRadialDistort - currentEffect.MinRadialDistort < 0.0f) { return 0.0f; }
|
|
|
|
return MathHelper.Lerp(
|
|
currentEffect.MinRadialDistort,
|
|
currentEffect.MaxRadialDistort,
|
|
currentEffect.GetStrengthFactor(this)) * GetScreenEffectFluctuation(currentEffect);
|
|
}
|
|
|
|
public float GetChromaticAberrationStrength()
|
|
{
|
|
if (Strength < Prefab.ActivationThreshold) { return 0.0f; }
|
|
AfflictionPrefab.Effect currentEffect = GetActiveEffect();
|
|
if (currentEffect == null) { return 0.0f; }
|
|
if (currentEffect.MaxChromaticAberration - currentEffect.MinChromaticAberration < 0.0f) { return 0.0f; }
|
|
|
|
return MathHelper.Lerp(
|
|
currentEffect.MinChromaticAberration,
|
|
currentEffect.MaxChromaticAberration,
|
|
currentEffect.GetStrengthFactor(this)) * GetScreenEffectFluctuation(currentEffect);
|
|
}
|
|
|
|
public float GetAfflictionOverlayMultiplier()
|
|
{
|
|
//If the overlay's alpha progresses linearly, then don't worry about affliction effects.
|
|
if (Prefab.AfflictionOverlayAlphaIsLinear) { return (Strength / Prefab.MaxStrength); }
|
|
if (Strength < Prefab.ActivationThreshold) { return 0.0f; }
|
|
AfflictionPrefab.Effect currentEffect = GetActiveEffect();
|
|
if (currentEffect == null) { return 0.0f; }
|
|
if (currentEffect.MaxAfflictionOverlayAlphaMultiplier - currentEffect.MinAfflictionOverlayAlphaMultiplier < 0.0f) { return 0.0f; }
|
|
|
|
return MathHelper.Lerp(
|
|
currentEffect.MinAfflictionOverlayAlphaMultiplier,
|
|
currentEffect.MaxAfflictionOverlayAlphaMultiplier,
|
|
currentEffect.GetStrengthFactor(this));
|
|
}
|
|
|
|
public Color GetFaceTint()
|
|
{
|
|
if (Strength < Prefab.ActivationThreshold) { return Color.TransparentBlack; }
|
|
AfflictionPrefab.Effect currentEffect = GetActiveEffect();
|
|
if (currentEffect == null) { return Color.TransparentBlack; }
|
|
|
|
return Color.Lerp(
|
|
currentEffect.MinFaceTint,
|
|
currentEffect.MaxFaceTint,
|
|
currentEffect.GetStrengthFactor(this));
|
|
}
|
|
|
|
public Color GetBodyTint()
|
|
{
|
|
if (Strength < Prefab.ActivationThreshold) { return Color.TransparentBlack; }
|
|
AfflictionPrefab.Effect currentEffect = GetActiveEffect();
|
|
if (currentEffect == null) { return Color.TransparentBlack; }
|
|
|
|
return Color.Lerp(
|
|
currentEffect.MinBodyTint,
|
|
currentEffect.MaxBodyTint,
|
|
currentEffect.GetStrengthFactor(this));
|
|
}
|
|
|
|
public float GetScreenBlurStrength()
|
|
{
|
|
if (Strength < Prefab.ActivationThreshold) { return 0.0f; }
|
|
AfflictionPrefab.Effect currentEffect = GetActiveEffect();
|
|
if (currentEffect == null) { return 0.0f; }
|
|
if (currentEffect.MaxScreenBlur - currentEffect.MinScreenBlur < 0.0f) { return 0.0f; }
|
|
|
|
return MathHelper.Lerp(
|
|
currentEffect.MinScreenBlur,
|
|
currentEffect.MaxScreenBlur,
|
|
currentEffect.GetStrengthFactor(this)) * GetScreenEffectFluctuation(currentEffect);
|
|
}
|
|
|
|
private float GetScreenEffectFluctuation(AfflictionPrefab.Effect currentEffect)
|
|
{
|
|
if (currentEffect == null || currentEffect.ScreenEffectFluctuationFrequency <= 0.0f) { return 1.0f; }
|
|
return ((float)Math.Sin(fluctuationTimer * MathHelper.TwoPi) + 1.0f) * 0.5f;
|
|
}
|
|
|
|
public float GetSkillMultiplier()
|
|
{
|
|
if (Strength < Prefab.ActivationThreshold) { return 1.0f; }
|
|
AfflictionPrefab.Effect currentEffect = GetActiveEffect();
|
|
if (currentEffect == null) { return 1.0f; }
|
|
|
|
float amount = MathHelper.Lerp(
|
|
currentEffect.MinSkillMultiplier,
|
|
currentEffect.MaxSkillMultiplier,
|
|
currentEffect.GetStrengthFactor(this));
|
|
|
|
return amount;
|
|
}
|
|
|
|
public void CalculateDamagePerSecond(float currentVitalityDecrease)
|
|
{
|
|
DamagePerSecond = Math.Max(DamagePerSecond, currentVitalityDecrease - PreviousVitalityDecrease);
|
|
if (DamagePerSecondTimer >= 1.0f)
|
|
{
|
|
DamagePerSecond = currentVitalityDecrease - PreviousVitalityDecrease;
|
|
PreviousVitalityDecrease = currentVitalityDecrease;
|
|
DamagePerSecondTimer = 0.0f;
|
|
}
|
|
}
|
|
|
|
public float GetResistance(Identifier afflictionId)
|
|
{
|
|
if (Strength < Prefab.ActivationThreshold) { return 0.0f; }
|
|
var affliction = AfflictionPrefab.Prefabs[afflictionId];
|
|
AfflictionPrefab.Effect currentEffect = GetActiveEffect();
|
|
if (currentEffect == null) { return 0.0f; }
|
|
if (!currentEffect.ResistanceFor.Any(r =>
|
|
r == affliction.Identifier ||
|
|
r == affliction.AfflictionType))
|
|
{
|
|
return 0.0f;
|
|
}
|
|
return MathHelper.Lerp(
|
|
currentEffect.MinResistance,
|
|
currentEffect.MaxResistance,
|
|
currentEffect.GetStrengthFactor(this));
|
|
}
|
|
|
|
public float GetSpeedMultiplier()
|
|
{
|
|
if (Strength < Prefab.ActivationThreshold) { return 1.0f; }
|
|
AfflictionPrefab.Effect currentEffect = GetActiveEffect();
|
|
if (currentEffect == null) { return 1.0f; }
|
|
return MathHelper.Lerp(
|
|
currentEffect.MinSpeedMultiplier,
|
|
currentEffect.MaxSpeedMultiplier,
|
|
currentEffect.GetStrengthFactor(this));
|
|
}
|
|
|
|
public float GetStatValue(StatTypes statType)
|
|
{
|
|
if (GetViableEffect() is not AfflictionPrefab.Effect currentEffect) { return 0.0f; }
|
|
|
|
if (!currentEffect.AfflictionStatValues.TryGetValue(statType, out var appliedStat)) { return 0.0f; }
|
|
|
|
return MathHelper.Lerp(appliedStat.MinValue, appliedStat.MaxValue, currentEffect.GetStrengthFactor(this));
|
|
}
|
|
|
|
public bool HasFlag(AbilityFlags flagType)
|
|
{
|
|
if (GetViableEffect() is not AfflictionPrefab.Effect currentEffect) { return false; }
|
|
return currentEffect.AfflictionAbilityFlags.HasFlag(flagType);
|
|
}
|
|
|
|
private AfflictionPrefab.Effect GetViableEffect()
|
|
{
|
|
if (Strength < Prefab.ActivationThreshold) { return null; }
|
|
return GetActiveEffect();
|
|
}
|
|
|
|
public virtual void Update(CharacterHealth characterHealth, Limb targetLimb, float deltaTime)
|
|
{
|
|
foreach (AfflictionPrefab.PeriodicEffect periodicEffect in Prefab.PeriodicEffects)
|
|
{
|
|
if (Strength <= periodicEffect.MinStrength) { continue; }
|
|
if (periodicEffect.MaxStrength > 0 && Strength > periodicEffect.MaxStrength) { continue; }
|
|
PeriodicEffectTimers[periodicEffect] -= deltaTime;
|
|
if (PeriodicEffectTimers[periodicEffect] <= 0.0f)
|
|
{
|
|
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
|
|
{
|
|
PeriodicEffectTimers[periodicEffect] = 0.0f;
|
|
}
|
|
else
|
|
{
|
|
foreach (StatusEffect statusEffect in periodicEffect.StatusEffects)
|
|
{
|
|
ApplyStatusEffect(ActionType.OnActive, statusEffect, 1.0f, characterHealth, targetLimb);
|
|
PeriodicEffectTimers[periodicEffect] = Rand.Range(periodicEffect.MinInterval, periodicEffect.MaxInterval);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
AfflictionPrefab.Effect currentEffect = GetActiveEffect();
|
|
if (currentEffect == null) { return; }
|
|
|
|
fluctuationTimer += deltaTime * currentEffect.ScreenEffectFluctuationFrequency;
|
|
fluctuationTimer %= 1.0f;
|
|
|
|
if (currentEffect.StrengthChange < 0) // Only apply StrengthDiminish.Multiplier if affliction is being weakened
|
|
{
|
|
float stat = characterHealth.Character.GetStatValue(
|
|
Prefab.IsBuff
|
|
? StatTypes.BuffDurationMultiplier
|
|
: StatTypes.DebuffDurationMultiplier);
|
|
|
|
float durationMultiplier = 1f / (1f + stat);
|
|
|
|
_strength += currentEffect.StrengthChange * deltaTime * StrengthDiminishMultiplier.Value * durationMultiplier;
|
|
}
|
|
else if (currentEffect.StrengthChange > 0) // Reduce strengthening of afflictions if resistant
|
|
{
|
|
_strength += currentEffect.StrengthChange * deltaTime * (1f - characterHealth.GetResistance(Prefab));
|
|
}
|
|
// Don't use the property, because it's virtual and some afflictions like husk overload it for external use.
|
|
_strength = MathHelper.Clamp(_strength, 0.0f, Prefab.MaxStrength);
|
|
activeEffectDirty |= !MathUtils.NearlyEqual(prevActiveEffectStrength, _strength);
|
|
|
|
foreach (StatusEffect statusEffect in currentEffect.StatusEffects)
|
|
{
|
|
ApplyStatusEffect(ActionType.OnActive, statusEffect, deltaTime, characterHealth, targetLimb);
|
|
}
|
|
|
|
float amount = deltaTime;
|
|
if (Prefab.GrainBurst > 0)
|
|
{
|
|
amount /= Prefab.GrainBurst;
|
|
}
|
|
if (PendingGrainEffectStrength >= 0)
|
|
{
|
|
GrainEffectStrength += amount;
|
|
PendingGrainEffectStrength -= deltaTime;
|
|
}
|
|
else if (GrainEffectStrength > 0)
|
|
{
|
|
GrainEffectStrength -= amount;
|
|
}
|
|
}
|
|
|
|
public void ApplyStatusEffects(ActionType type, float deltaTime, CharacterHealth characterHealth, Limb targetLimb)
|
|
{
|
|
var currentEffect = GetActiveEffect();
|
|
if (currentEffect != null)
|
|
{
|
|
foreach (var statusEffect in currentEffect.StatusEffects)
|
|
{
|
|
ApplyStatusEffect(type, statusEffect, deltaTime, characterHealth, targetLimb);
|
|
}
|
|
}
|
|
}
|
|
|
|
private readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
|
|
public void ApplyStatusEffect(ActionType type, StatusEffect statusEffect, float deltaTime, CharacterHealth characterHealth, Limb targetLimb)
|
|
{
|
|
if (type == ActionType.OnDamaged && !statusEffect.HasRequiredAfflictions(characterHealth.Character.LastDamage)) { return; }
|
|
|
|
statusEffect.SetUser(Source);
|
|
if (statusEffect.HasTargetType(StatusEffect.TargetType.Character))
|
|
{
|
|
statusEffect.Apply(type, deltaTime, characterHealth.Character, characterHealth.Character);
|
|
}
|
|
if (targetLimb != null && statusEffect.HasTargetType(StatusEffect.TargetType.Limb))
|
|
{
|
|
statusEffect.Apply(type, deltaTime, characterHealth.Character, targetLimb);
|
|
}
|
|
if (characterHealth?.Character?.AnimController?.Limbs != null && statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs))
|
|
{
|
|
statusEffect.Apply(type, deltaTime, characterHealth.Character, targets: characterHealth.Character.AnimController.Limbs);
|
|
}
|
|
if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
|
|
statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
|
|
{
|
|
targets.Clear();
|
|
statusEffect.AddNearbyTargets(characterHealth.Character.WorldPosition, targets);
|
|
statusEffect.Apply(type, deltaTime, characterHealth.Character, targets);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Use this method to skip clamping and additional logic of the setters.
|
|
/// Ideally we would keep this private, but doing so would require too much refactoring.
|
|
/// </summary>
|
|
public void SetStrength(float strength)
|
|
{
|
|
if (!MathUtils.IsValid(strength))
|
|
{
|
|
#if DEBUG
|
|
DebugConsole.ThrowError($"Attempted to set an affliction to an invalid strength ({strength})\n" + Environment.StackTrace.CleanupStackTrace());
|
|
#endif
|
|
return;
|
|
}
|
|
_nonClampedStrength = strength;
|
|
_strength = _nonClampedStrength;
|
|
activeEffectDirty |= !MathUtils.NearlyEqual(_strength, prevActiveEffectStrength);
|
|
}
|
|
|
|
public bool ShouldShowIcon(Character afflictedCharacter)
|
|
{
|
|
return Strength >= (afflictedCharacter == Character.Controlled ? Prefab.ShowIconThreshold : Prefab.ShowIconToOthersThreshold);
|
|
}
|
|
}
|
|
}
|