Files
LuaCsForBarotraumaEP/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs
T
2024-01-11 16:19:43 +02:00

1159 lines
55 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Barotrauma.Abilities;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Xml.Linq;
using Barotrauma.Extensions;
using System.Collections.Immutable;
using Barotrauma.Items.Components;
using System.Linq;
namespace Barotrauma
{
class CPRSettings : Prefab
{
public readonly static PrefabSelector<CPRSettings> Prefabs = new PrefabSelector<CPRSettings>();
public static CPRSettings Active => Prefabs.ActivePrefab;
public readonly float ReviveChancePerSkill;
public readonly float ReviveChanceExponent;
public readonly float ReviveChanceMin;
public readonly float ReviveChanceMax;
public readonly float StabilizationPerSkill;
public readonly float StabilizationMin;
public readonly float StabilizationMax;
public readonly float DamageSkillThreshold;
public readonly float DamageSkillMultiplier;
private readonly string insufficientSkillAfflictionIdentifier;
public AfflictionPrefab InsufficientSkillAffliction
{
get
{
return
AfflictionPrefab.Prefabs.ContainsKey(insufficientSkillAfflictionIdentifier) ?
AfflictionPrefab.Prefabs[insufficientSkillAfflictionIdentifier] :
AfflictionPrefab.InternalDamage;
}
}
public CPRSettings(XElement element, AfflictionsFile file) : base(file, file.Path.Value.ToIdentifier())
{
ReviveChancePerSkill = Math.Max(element.GetAttributeFloat("revivechanceperskill", 0.01f), 0.0f);
ReviveChanceExponent = Math.Max(element.GetAttributeFloat("revivechanceexponent", 2.0f), 0.0f);
ReviveChanceMin = MathHelper.Clamp(element.GetAttributeFloat("revivechancemin", 0.05f), 0.0f, 1.0f);
ReviveChanceMax = MathHelper.Clamp(element.GetAttributeFloat("revivechancemax", 0.9f), ReviveChanceMin, 1.0f);
StabilizationPerSkill = Math.Max(element.GetAttributeFloat("stabilizationperskill", 0.01f), 0.0f);
StabilizationMin = MathHelper.Max(element.GetAttributeFloat("stabilizationmin", 0.05f), 0.0f);
StabilizationMax = MathHelper.Max(element.GetAttributeFloat("stabilizationmax", 2.0f), StabilizationMin);
DamageSkillThreshold = MathHelper.Clamp(element.GetAttributeFloat("damageskillthreshold", 40.0f), 0.0f, 100.0f);
DamageSkillMultiplier = MathHelper.Clamp(element.GetAttributeFloat("damageskillmultiplier", 0.1f), 0.0f, 100.0f);
insufficientSkillAfflictionIdentifier = element.GetAttributeString("insufficientskillaffliction", "");
}
public override void Dispose() { }
}
/// <summary>
/// AfflictionPrefabHusk is a special type of affliction that has added functionality for husk infection.
/// </summary>
class AfflictionPrefabHusk : AfflictionPrefab
{
// Use any of these to define which limb the appendage is attached to.
// If multiple are defined, the order of preference is: id, name, type.
public readonly int AttachLimbId;
public readonly string AttachLimbName;
public readonly LimbType AttachLimbType;
/// <summary>
/// The minimum strength at which husk infection will be in the dormant stage.
/// It must be less than or equal to ActiveThreshold.
/// </summary>
public readonly float DormantThreshold;
/// <summary>
/// The minimum strength at which husk infection will be in the active stage.
/// It must be greater than or equal to DormantThreshold and less than or equal to TransitionThreshold.
/// </summary>
public readonly float ActiveThreshold;
/// <summary>
/// The minimum strength at which husk infection will be in its final stage.
/// It must be greater than or equal to ActiveThreshold.
/// </summary>
public readonly float TransitionThreshold;
/// <summary>
/// The minimum strength the affliction must have for the affected character
/// to transform into a husk upon death.
/// </summary>
public readonly float TransformThresholdOnDeath;
/// <summary>
/// The species of husk to convert the affected character to
/// once husk infection reaches its final stage.
/// </summary>
public readonly Identifier HuskedSpeciesName;
/// <summary>
/// If set to true, all buffs are transferred to the converted
/// character after husk transformation is complete.
/// </summary>
public readonly bool TransferBuffs;
/// <summary>
/// If set to true, the affected player will see on-screen messages describing husk infection symptoms
/// and affected bots will speak about their current husk infection stage.
/// </summary>
public readonly bool SendMessages;
/// <summary>
/// If set to true, affected characters will have their speech impeded once the affliction
/// reaches the dormant stage.
/// </summary>
public readonly bool CauseSpeechImpediment;
/// <summary>
/// If set to false, affected characters will no longer require air
/// once the affliction reaches the active stage.
/// </summary>
public readonly bool NeedsAir;
/// <summary>
/// If set to true, affected players will retain control of their character
/// after transforming into a husk.
/// </summary>
public readonly bool ControlHusk;
public AfflictionPrefabHusk(ContentXElement element, AfflictionsFile file, Type type = null) : base(element, file, type)
{
HuskedSpeciesName = element.GetAttributeIdentifier("huskedspeciesname", Identifier.Empty);
if (HuskedSpeciesName.IsEmpty)
{
DebugConsole.NewMessage($"No 'huskedspeciesname' defined for the husk affliction ({Identifier}) in {element}", Color.Orange);
HuskedSpeciesName = "husk".ToIdentifier();
}
// Remove "[speciesname]" for backward support (we don't use it anymore)
HuskedSpeciesName = HuskedSpeciesName.Remove("[speciesname]").ToIdentifier();
if (TargetSpecies.Length == 0)
{
DebugConsole.NewMessage($"No 'targets' defined for the husk affliction ({Identifier}) in {element}", Color.Orange);
TargetSpecies = new Identifier[] { CharacterPrefab.HumanSpeciesName };
}
var attachElement = element.GetChildElement("attachlimb");
if (attachElement != null)
{
AttachLimbId = attachElement.GetAttributeInt("id", -1);
AttachLimbName = attachElement.GetAttributeString("name", null);
AttachLimbType = attachElement.GetAttributeEnum("type", LimbType.None);
}
else
{
AttachLimbId = -1;
AttachLimbName = null;
AttachLimbType = LimbType.None;
}
TransferBuffs = element.GetAttributeBool("transferbuffs", true);
SendMessages = element.GetAttributeBool("sendmessages", true);
CauseSpeechImpediment = element.GetAttributeBool("causespeechimpediment", true);
NeedsAir = element.GetAttributeBool("needsair", false);
ControlHusk = element.GetAttributeBool("controlhusk", false);
DormantThreshold = element.GetAttributeFloat("dormantthreshold", MaxStrength * 0.5f);
ActiveThreshold = element.GetAttributeFloat("activethreshold", MaxStrength * 0.75f);
TransitionThreshold = element.GetAttributeFloat("transitionthreshold", MaxStrength);
if (DormantThreshold > ActiveThreshold)
{
DebugConsole.ThrowError($"Error in \"{Identifier}\": {nameof(DormantThreshold)} is greater than {nameof(ActiveThreshold)} ({DormantThreshold} > {ActiveThreshold})",
contentPackage: element.ContentPackage);
}
if (ActiveThreshold > TransitionThreshold)
{
DebugConsole.ThrowError($"Error in \"{Identifier}\": {nameof(ActiveThreshold)} is greater than {nameof(TransitionThreshold)} ({ActiveThreshold} > {TransitionThreshold})",
contentPackage: element.ContentPackage);
}
TransformThresholdOnDeath = element.GetAttributeFloat("transformthresholdondeath", ActiveThreshold);
}
}
/// <summary>
/// AfflictionPrefab is a prefab that defines a type of affliction that can be applied to a character.
/// There are multiple sub-types of afflictions such as AfflictionPrefabHusk, AfflictionPsychosis and AfflictionBleeding that can be used for additional functionality.
///
/// When defining a new affliction, the type will be determined by the element name.
/// </summary>
/// <example>
/// <code language="xml">
/// <Afflictions>
/// <!-- Defines a regular affliction. -->
/// <Affliction identifier="mycoolaffliction1" />
///
/// <!-- Defines an AfflictionPrefabHusk affliction. -->
/// <AfflictionPrefabHusk identifier="mycoolaffliction2"/>
///
/// <!-- Defines an AfflictionBleeding affliction. -->
/// <AfflictionBleeding identifier="mycoolaffliction3"/>
/// </Afflictions>
/// </code>
/// </example>
class AfflictionPrefab : PrefabWithUintIdentifier
{
/// <summary>
/// Effects are the primary way to add functionality to afflictions.
/// </summary>
/// <doc>
/// <Ignore type="SubElement" identifier="AbilityFlag" />
/// <SubElement identifier="abilityflag" type="AppliedAbilityFlag">
/// Enables the specified flag on the character as long as the effect is active.
/// </SubElement>
/// <Type identifier="AppliedAbilityFlag">
/// <Summary>
/// Flag that will be enabled for the character as long as the effect is active.
/// <example>
/// <code language="xml">
/// <Effect minstrength="0" maxstrength="100">
/// <!-- Grants pressure immunity to the character while the effect is active. -->
/// <AbilityFlag flagtype="ImmuneToPressure" />
/// </Effect>
/// </code>
/// </example>
/// </Summary>
/// <Field identifier="FlagType" type="AbilityFlags" defaultValue="None">
/// Which ability flag to enable.
/// </Field>
/// </Type>
/// </doc>
public sealed class Effect
{
//this effect is applied when the strength is within this range
[Serialize(0.0f, IsPropertySaveable.No, description: "Minimum affliction strength required for this effect to be active.")]
public float MinStrength { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No, description: "Maximum affliction strength for which this effect will be active.")]
public float MaxStrength { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No, description: "The amount of vitality that is lost at this effect's lowest strength.")]
public float MinVitalityDecrease { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No, description: "The amount of vitality that is lost at this effect's highest strength.")]
public float MaxVitalityDecrease { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No, description: "How much the affliction's strength changes every second while this effect is active.")]
public float StrengthChange { get; private set; }
[Serialize(false, IsPropertySaveable.No, description:
"If set to true, MinVitalityDecrease and MaxVitalityDecrease represent a fraction of the affected character's maximum " +
"vitality, with 1 meaning 100%, instead of the same amount for all species.")]
public bool MultiplyByMaxVitality { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No, description: "Blur effect strength at this effect's lowest strength.")]
public float MinScreenBlur { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No, description: "Blur effect strength at this effect's highest strength.")]
public float MaxScreenBlur { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No, description: "Generic distortion effect strength at this effect's lowest strength.")]
public float MinScreenDistort { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No, description: "Generic distortion effect strength at this effect's highest strength.")]
public float MaxScreenDistort { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No, description: "Radial distortion effect strength at this effect's lowest strength.")]
public float MinRadialDistort { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No, description: "Radial distortion effect strength at this effect's highest strength.")]
public float MaxRadialDistort { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No, description: "Chromatic aberration effect strength at this effect's lowest strength.")]
public float MinChromaticAberration { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No, description: "Chromatic aberration effect strength at this effect's highest strength.")]
public float MaxChromaticAberration { get; private set; }
[Serialize("255,255,255,255", IsPropertySaveable.No, description: "Radiation grain effect color.")]
public Color GrainColor { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No, description: "Radiation grain effect strength at this effect's lowest strength.")]
public float MinGrainStrength { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No, description: "Radiation grain effect strength at this effect's highest strength.")]
public float MaxGrainStrength { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No, description:
"The maximum rate of fluctuation to apply to visual effects caused by this affliction effect. " +
"Effective fluctuation is proportional to the affliction's current strength.")]
public float ScreenEffectFluctuationFrequency { get; private set; }
[Serialize(1.0f, IsPropertySaveable.No, description:
"Multiplier for the affliction overlay's opacity at this effect's lowest strength. " +
"See the list of elements for more details.")]
public float MinAfflictionOverlayAlphaMultiplier { get; private set; }
[Serialize(1.0f, IsPropertySaveable.No, description:
"Multiplier for the affliction overlay's opacity at this effect's highest strength. " +
"See the list of elements for more details.")]
public float MaxAfflictionOverlayAlphaMultiplier { get; private set; }
[Serialize(1.0f, IsPropertySaveable.No, description:
"Multiplier for every buff's decay rate at this effect's lowest strength. " +
"Only applies to afflictions of class BuffDurationIncrease.")]
public float MinBuffMultiplier { get; private set; }
[Serialize(1.0f, IsPropertySaveable.No, description:
"Multiplier for every buff's decay rate at this effect's highest strength. " +
"Only applies to afflictions of class BuffDurationIncrease.")]
public float MaxBuffMultiplier { get; private set; }
[Serialize(1.0f, IsPropertySaveable.No, description: "Multiplier to apply to the affected character's speed at this effect's lowest strength.")]
public float MinSpeedMultiplier { get; private set; }
[Serialize(1.0f, IsPropertySaveable.No, description: "Multiplier to apply to the affected character's speed at this effect's highest strength.")]
public float MaxSpeedMultiplier { get; private set; }
[Serialize(1.0f, IsPropertySaveable.No, description: "Multiplier to apply to all of the affected character's skill levels at this effect's lowest strength.")]
public float MinSkillMultiplier { get; private set; }
[Serialize(1.0f, IsPropertySaveable.No, description: "Multiplier to apply to all of the affected character's skill levels at this effect's highest strength.")]
public float MaxSkillMultiplier { get; private set; }
/// <summary>
/// A list of identifiers of afflictions that the affected character will be
/// resistant to when this effect is active.
/// </summary>
public readonly ImmutableArray<Identifier> ResistanceFor;
[Serialize(0.0f, IsPropertySaveable.No,
description: "The amount of resistance to the afflictions specified by ResistanceFor to apply at this effect's lowest strength.")]
public float MinResistance { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No,
description: "The amount of resistance to the afflictions specified by ResistanceFor to apply at this effect's highest strength.")]
public float MaxResistance { get; private set; }
[Serialize("", IsPropertySaveable.No, description: "Identifier used by AI to determine conversation lines to say when this effect is active.")]
public Identifier DialogFlag { get; private set; }
[Serialize("", IsPropertySaveable.No, description: "Tag that enemy AI may use to target the affected character when this effect is active.")]
public Identifier Tag { get; private set; }
[Serialize("0,0,0,0", IsPropertySaveable.No,
description: "Color to tint the affected character's face with at this effect's lowest strength. The alpha channel is used to determine how much to tint the character's face.")]
public Color MinFaceTint { get; private set; }
[Serialize("0,0,0,0", IsPropertySaveable.No,
description: "Color to tint the affected character's face with at this effect's highest strength. The alpha channel is used to determine how much to tint the character's face.")]
public Color MaxFaceTint { get; private set; }
[Serialize("0,0,0,0", IsPropertySaveable.No,
description: "Color to tint the affected character's entire body with at this effect's lowest strength. The alpha channel is used to determine how much to tint the character.")]
public Color MinBodyTint { get; private set; }
[Serialize("0,0,0,0", IsPropertySaveable.No,
description: "Color to tint the affected character's entire body with at this effect's highest strength. The alpha channel is used to determine how much to tint the character.")]
public Color MaxBodyTint { get; private set; }
/// <summary>
/// StatType that will be applied to the affected character when the effect is active that is proportional to the effect's strength.
/// </summary>
/// <example>
/// <code language="xml">
/// <Effect minstrength="0" maxstrength="100">
/// <!-- Walking speed will be increased by 10% at strength 0, 20% at 50 and 30% at 100 -->
/// <StatValue stattype="WalkingSpeed" minvalue="0.1" maxvalue="0.3" />
/// <!-- Maximum health will be increased by 20% regardless of the effect strength -->
/// <StatValue stattype="MaximumHealthMultiplier" value="0.2" />
/// </Effect>
/// </code>
/// </example>
public readonly struct AppliedStatValue
{
/// <summary>
/// Which StatType to apply
/// </summary>
public readonly StatTypes StatType;
/// <summary>
/// Minimum value to apply
/// </summary>
public readonly float MinValue;
/// <summary>
/// Minimum value to apply
/// </summary>
public readonly float MaxValue;
/// <summary>
/// Constant value to apply, will be ignored if MinValue or MaxValue are set
/// </summary>
private readonly float Value;
public AppliedStatValue(ContentXElement element)
{
Value = element.GetAttributeFloat("value", 0.0f);
StatType = element.GetAttributeEnum("stattype", StatTypes.None);
MinValue = element.GetAttributeFloat("minvalue", Value);
MaxValue = element.GetAttributeFloat("maxvalue", Value);
}
}
/// <summary>
/// Prevents AfflictionHusks with the specified identifier(s) from transforming the character into an AI-controlled character.
/// </summary>
public readonly ImmutableArray<Identifier> BlockTransformation;
/// <summary>
/// StatType that will be applied to the affected character when the effect is active that is proportional to the effect's strength.
/// </summary>
public readonly ImmutableDictionary<StatTypes, AppliedStatValue> AfflictionStatValues;
public readonly AbilityFlags AfflictionAbilityFlags;
//statuseffects applied on the character when the affliction is active
public readonly ImmutableArray<StatusEffect> StatusEffects;
public Effect(ContentXElement element, string parentDebugName)
{
SerializableProperty.DeserializeProperties(this, element);
ResistanceFor = element.GetAttributeIdentifierArray("resistancefor", Array.Empty<Identifier>())!.ToImmutableArray();
BlockTransformation = element.GetAttributeIdentifierArray("blocktransformation", Array.Empty<Identifier>())!.ToImmutableArray();
var afflictionStatValues = new Dictionary<StatTypes, AppliedStatValue>();
var statusEffects = new List<StatusEffect>();
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "statuseffect":
statusEffects.Add(StatusEffect.Load(subElement, parentDebugName));
break;
case "statvalue":
var newStatValue = new AppliedStatValue(subElement);
if (newStatValue.StatType == StatTypes.None || !afflictionStatValues.TryAdd(newStatValue.StatType, newStatValue))
{
DebugConsole.ThrowError($"Invalid stat value in the affliction \"{parentDebugName}\".", contentPackage: element.ContentPackage);
}
break;
case "abilityflag":
AbilityFlags flagType = subElement.GetAttributeEnum("flagtype", AbilityFlags.None);
if (flagType is AbilityFlags.None)
{
DebugConsole.ThrowError($"Error in affliction \"{parentDebugName}\" - invalid ability flag type \"{subElement.GetAttributeString("flagtype", "")}\".",
contentPackage: element.ContentPackage);
continue;
}
AfflictionAbilityFlags |= flagType;
break;
case "affliction":
DebugConsole.AddWarning($"Error in affliction \"{parentDebugName}\" - additional afflictions caused by the affliction should be configured inside status effects.",
contentPackage: element.ContentPackage);
break;
}
}
AfflictionStatValues = afflictionStatValues.ToImmutableDictionary();
StatusEffects = statusEffects.ToImmutableArray();
}
/// <summary>
/// Returns 0 if affliction.Strength is MinStrength,
/// 1 if affliction.Strength is MaxStrength
/// </summary>
public float GetStrengthFactor(Affliction affliction) => GetStrengthFactor(affliction.Strength);
/// <summary>
/// Returns 0 if affliction.Strength is MinStrength,
/// 1 if affliction.Strength is MaxStrength
/// </summary>
public float GetStrengthFactor(float strength)
=> MathUtils.InverseLerp(
MinStrength,
MaxStrength,
strength);
}
/// <summary>
/// The description element can be used to define descriptions for the affliction which are shown under specific conditions;
/// for example a description that only shows to other players or only at certain strength levels.
/// </summary>
/// <doc>
/// <Field identifier="Text" type="string" defaultValue="&quot;&quot;">
/// Raw text for the description.
/// </Field>
/// </doc>
public sealed class Description
{
public enum TargetType
{
/// <summary>
/// Everyone can see the description.
/// </summary>
Any,
/// <summary>
/// Only the affected character can see the description.
/// </summary>
Self,
/// <summary>
/// The affected character cannot see the description but others can.
/// </summary>
OtherCharacter
}
/// <summary>
/// Raw text for the description.
/// </summary>
public readonly LocalizedString Text;
/// <summary>
/// Text tag used to set the text from the localization files.
/// </summary>
public readonly Identifier TextTag;
/// <summary>
/// Minimum strength required for the description to be shown.
/// </summary>
public readonly float MinStrength;
/// <summary>
/// Maximum strength required for the description to be shown.
/// </summary>
public readonly float MaxStrength;
/// <summary>
/// Who can see the description.
/// </summary>
public readonly TargetType Target;
public Description(ContentXElement element, AfflictionPrefab affliction)
{
TextTag = element.GetAttributeIdentifier("textidentifier", Identifier.Empty);
if (!TextTag.IsEmpty)
{
Text = TextManager.Get(TextTag);
}
string text = element.GetAttributeString("text", string.Empty);
if (!text.IsNullOrEmpty())
{
Text = Text?.Fallback(text) ?? text;
}
else if (TextTag.IsEmpty)
{
DebugConsole.ThrowError($"Error in affliction \"{affliction.Identifier}\" - no text defined for one of the descriptions.",
contentPackage: element.ContentPackage);
}
MinStrength = element.GetAttributeFloat(nameof(MinStrength), 0.0f);
MaxStrength = element.GetAttributeFloat(nameof(MaxStrength), 100.0f);
if (MinStrength >= MaxStrength)
{
DebugConsole.ThrowError($"Error in affliction \"{affliction.Identifier}\" - max strength is not larger than min.",
contentPackage: element.ContentPackage);
}
Target = element.GetAttributeEnum(nameof(Target), TargetType.Any);
}
}
/// <summary>
/// PeriodicEffect applies StatusEffects to the character periodically.
/// </summary>
/// <doc>
/// <SubElement identifier="StatusEffect" type="StatusEffect" />
/// <Field identifier="Interval" type="float" defaultValue="1.0">
/// How often the status effect is applied in seconds.
/// Setting this attribute will set both the min and max interval to the specified value.
/// </Field>
/// <Field identifier="MinInterval" type="float" defaultValue="1.0">
/// Minimum interval between applying the status effect in seconds.
/// </Field>
/// <Field identifier="MaxInterval" type="float" defaultValue="1.0">
/// Maximum interval between applying the status effect in seconds.
/// </Field>
/// </doc>
public sealed class PeriodicEffect
{
public readonly List<StatusEffect> StatusEffects = new List<StatusEffect>();
public readonly float MinInterval, MaxInterval;
public readonly float MinStrength, MaxStrength;
public PeriodicEffect(ContentXElement element, string parentDebugName)
{
foreach (var subElement in element.Elements())
{
StatusEffects.Add(StatusEffect.Load(subElement, parentDebugName));
}
if (element.GetAttribute("interval") != null)
{
MinInterval = MaxInterval = Math.Max(element.GetAttributeFloat("interval", 1.0f), 1.0f);
}
else
{
MinInterval = Math.Max(element.GetAttributeFloat(nameof(MinInterval), 1.0f), 1.0f);
MaxInterval = Math.Max(element.GetAttributeFloat(nameof(MaxInterval), 1.0f), MinInterval);
MinStrength = Math.Max(element.GetAttributeFloat(nameof(MinStrength), 0f), 0f);
MaxStrength = Math.Max(element.GetAttributeFloat(nameof(MaxStrength), MinStrength), MinStrength);
}
}
}
public static readonly Identifier DamageType = "damage".ToIdentifier();
public static readonly Identifier BurnType = "burn".ToIdentifier();
public static readonly Identifier BleedingType = "bleeding".ToIdentifier();
public static readonly Identifier ParalysisType = "paralysis".ToIdentifier();
public static readonly Identifier PoisonType = "poison".ToIdentifier();
public static readonly Identifier StunType = "stun".ToIdentifier();
public static readonly Identifier EMPType = "emp".ToIdentifier();
public static readonly Identifier SpaceHerpesType = "spaceherpes".ToIdentifier();
public static readonly Identifier AlienInfectedType = "alieninfected".ToIdentifier();
public static readonly Identifier InvertControlsType = "invertcontrols".ToIdentifier();
public static AfflictionPrefab InternalDamage => Prefabs["internaldamage"];
public static AfflictionPrefab BiteWounds => Prefabs["bitewounds"];
public static AfflictionPrefab ImpactDamage => Prefabs["blunttrauma"];
public static AfflictionPrefab Bleeding => Prefabs[BleedingType];
public static AfflictionPrefab Burn => Prefabs[BurnType];
public static AfflictionPrefab OxygenLow => Prefabs["oxygenlow"];
public static AfflictionPrefab Bloodloss => Prefabs["bloodloss"];
public static AfflictionPrefab Pressure => Prefabs["pressure"];
public static AfflictionPrefab OrganDamage => Prefabs["organdamage"];
public static AfflictionPrefab Stun => Prefabs[StunType];
public static AfflictionPrefab RadiationSickness => Prefabs["radiationsickness"];
public static readonly PrefabCollection<AfflictionPrefab> Prefabs = new PrefabCollection<AfflictionPrefab>();
public static IEnumerable<AfflictionPrefab> List => Prefabs;
public override void Dispose() { }
private readonly ContentXElement configElement;
public readonly LocalizedString Name;
public readonly LocalizedString CauseOfDeathDescription, SelfCauseOfDeathDescription;
private readonly LocalizedString defaultDescription;
public readonly ImmutableList<Description> Descriptions;
/// <summary>
/// Arbitrary string that is used to identify the type of the affliction.
/// </summary>
public readonly Identifier AfflictionType;
/// <summary>
/// If set to true, the affliction affects individual limbs. Otherwise, it affects the whole character.
/// </summary>
public readonly bool LimbSpecific;
/// <summary>
/// If the affliction doesn't affect individual limbs, this attribute determines
/// where the game will render the affliction's indicator when viewed in the
/// in-game health UI.
///
/// For example, the psychosis indicator is rendered on the head, and low oxygen
/// is rendered on the torso.
/// </summary>
public readonly LimbType IndicatorLimb;
/// <summary>
/// Can be set to the identifier of another affliction to make this affliction
/// reuse the same name and description.
/// </summary>
public readonly Identifier TranslationIdentifier;
/// <summary>
/// If set to true, the game will recognize this affliction as a buff.
/// This means, among other things, that bots won't attempt to treat it,
/// and the health UI will render the affected limb in green rather than red.
/// </summary>
public readonly bool IsBuff;
/// <summary>
/// If set to true, this affliction can affect characters that are marked as
/// machines, such as the Fractal Guardian.
/// </summary>
public readonly bool AffectMachines;
/// <summary>
/// If set to true, this affliction can be healed at the medical clinic.
/// </summary>
/// <doc>
/// <override type="DefaultValue">
/// false if the affliction is a buff or has the type "geneticmaterialbuff" or "geneticmaterialdebuff", true otherwise.
/// </override>
/// </doc>
public readonly bool HealableInMedicalClinic;
/// <summary>
/// How much each unit of this affliction's strength will add
/// to the cost of healing at the medical clinic.
/// </summary>
public readonly float HealCostMultiplier;
/// <summary>
/// The minimum cost of healing this affliction at the medical clinic.
/// </summary>
public readonly int BaseHealCost;
/// <summary>
/// If set to false, the health UI will not show the strength of the affliction
/// as a bar under its indicator.
/// </summary>
public readonly bool ShowBarInHealthMenu;
/// <summary>
/// If set to true, this affliction's icon will be hidden from the HUD after 5 seconds.
/// </summary>
public readonly bool HideIconAfterDelay;
/// <summary>
/// How high the strength has to be for the affliction to take effect
/// </summary>
public readonly float ActivationThreshold = 0.0f;
/// <summary>
/// How high the strength has to be for the affliction icon to be shown in the UI
/// </summary>
public readonly float ShowIconThreshold = 0.05f;
/// <summary>
/// How high the strength has to be for the affliction icon to be shown to others with a health scanner or via the health interface
/// </summary>
public readonly float ShowIconToOthersThreshold = 0.05f;
/// <summary>
/// The maximum strength this affliction can have.
/// </summary>
public readonly float MaxStrength = 100.0f;
/// <summary>
/// The strength of the radiation grain effect to apply when the strength of this affliction increases.
/// </summary>
public readonly float GrainBurst;
/// <summary>
/// How high the strength has to be for the affliction icon to be shown with a health scanner
/// </summary>
public readonly float ShowInHealthScannerThreshold;
/// <summary>
/// How strong the affliction needs to be before bots attempt to treat it.
/// Also effects when the affliction is shown in the suitable treatments list.
/// </summary>
public readonly float TreatmentThreshold;
/// <summary>
/// Bots will not try to treat the affliction if the character has any of these afflictions
/// </summary>
public ImmutableHashSet<Identifier> IgnoreTreatmentIfAfflictedBy;
/// <summary>
/// The duration of the affliction, in seconds. If set to 0, the affliction does not expire.
/// </summary>
public readonly float Duration;
/// <summary>
/// How much karma changes when a player applies this affliction to someone (per strength of the affliction)
/// </summary>
public float KarmaChangeOnApplied;
/// <summary>
/// Opacity of the burn effect (darker tint) on limbs affected by this affliction. 1 = full strength.
/// </summary>
public readonly float BurnOverlayAlpha;
/// <summary>
/// Opacity of the bloody damage overlay on limbs affected by this affliction. 1 = full strength.
/// </summary>
public readonly float DamageOverlayAlpha;
/// Steam achievement given when the controlled character receives the affliction.
/// </summary>
public readonly Identifier AchievementOnReceived;
/// <summary>
/// Steam achievement given when the affliction is removed from the controlled character.
/// </summary>
public readonly Identifier AchievementOnRemoved;
/// <summary>
/// A gradient that defines which color to render this affliction's icon
/// with, based on the affliction's current strength.
/// </summary>
public readonly Color[] IconColors;
/// <summary>
/// If set to true and the affliction has an AfflictionOverlay element, the overlay's opacity will be strictly proportional to its strength.
/// Otherwise, the overlay's opacity will be determined based on its activation threshold and effects.
/// </summary>
public readonly bool AfflictionOverlayAlphaIsLinear;
/// <summary>
/// If set to true, this affliction will not persist between rounds.
/// </summary>
public readonly bool ResetBetweenRounds;
/// <summary>
/// Should damage particles be emitted when a character receives this affliction?
/// Only relevant if the affliction is of the type "bleeding" or "damage".
/// </summary>
public readonly bool DamageParticles;
/// <summary>
/// An arbitrary modifier that affects how much medical skill is increased when you apply the affliction on a target.
/// If the affliction causes damage or is of the 'poison' or 'paralysis' type, the skill is increased only when the target is hostile.
/// If the affliction is of the 'buff' type, the skill is increased only when the target is friendly.
/// </summary>
public readonly float MedicalSkillGain;
/// <summary>
/// An arbitrary modifier that affects how much weapons skill is increased when you apply the affliction on a target.
/// The skill is increased only when the target is hostile.
/// </summary>
public readonly float WeaponsSkillGain;
/// <summary>
/// A list of species this affliction is allowed to affect.
/// </summary>
public Identifier[] TargetSpecies { get; protected set; }
/// <summary>
/// Effects to apply at various strength levels.
/// Only one effect can be applied at any given moment, so their ranges should be defined with no overlap.
/// </summary>
private readonly List<Effect> effects = new List<Effect>();
/// <summary>
/// PeriodicEffect applies StatusEffects to the character periodically.
/// </summary>
private readonly List<PeriodicEffect> periodicEffects = new List<PeriodicEffect>();
public IEnumerable<Effect> Effects => effects;
public IList<PeriodicEffect> PeriodicEffects => periodicEffects;
private readonly ConstructorInfo constructor;
/// <summary>
/// An icon thats used in the UI to represent this affliction.
/// </summary>
public readonly Sprite Icon;
/// <summary>
/// A sprite that covers the affected player's entire screen when this affliction is active.
/// Its opacity is controlled by the active effect's MinAfflictionOverlayAlphaMultiplier and MaxAfflictionOverlayAlphaMultiplier
/// </summary>
public readonly Sprite AfflictionOverlay;
public ImmutableDictionary<Identifier, float> TreatmentSuitabilities
{
get;
private set;
} = new Dictionary<Identifier, float>().ToImmutableDictionary();
/// <summary>
/// Can this affliction be treated with some item?
/// </summary>
public bool HasTreatments { get; private set; }
public AfflictionPrefab(ContentXElement element, AfflictionsFile file, Type type) : base(file, element.GetAttributeIdentifier("identifier", ""))
{
configElement = element;
AfflictionType = element.GetAttributeIdentifier("type", "");
TranslationIdentifier = element.GetAttributeIdentifier("translationoverride", Identifier);
Name = TextManager.Get($"AfflictionName.{TranslationIdentifier}");
string fallbackName = element.GetAttributeString("name", "");
if (!string.IsNullOrEmpty(fallbackName))
{
Name = Name.Fallback(fallbackName);
}
defaultDescription = TextManager.Get($"AfflictionDescription.{TranslationIdentifier}");
string fallbackDescription = element.GetAttributeString("description", "");
if (!string.IsNullOrEmpty(fallbackDescription))
{
defaultDescription = defaultDescription.Fallback(fallbackDescription);
}
IsBuff = element.GetAttributeBool(nameof(IsBuff), false);
AffectMachines = element.GetAttributeBool(nameof(AffectMachines), true);
ShowBarInHealthMenu = element.GetAttributeBool("showbarinhealthmenu", true);
HealableInMedicalClinic = element.GetAttributeBool("healableinmedicalclinic",
!IsBuff &&
AfflictionType != "geneticmaterialbuff" &&
AfflictionType != "geneticmaterialdebuff");
HealCostMultiplier = element.GetAttributeFloat(nameof(HealCostMultiplier), 1f);
BaseHealCost = element.GetAttributeInt(nameof(BaseHealCost), 0);
IgnoreTreatmentIfAfflictedBy = element.GetAttributeIdentifierArray(nameof(IgnoreTreatmentIfAfflictedBy), Array.Empty<Identifier>()).ToImmutableHashSet();
Duration = element.GetAttributeFloat(nameof(Duration), 0.0f);
if (element.GetAttribute("nameidentifier") != null)
{
Name = TextManager.Get(element.GetAttributeString("nameidentifier", string.Empty)).Fallback(Name);
}
LimbSpecific = element.GetAttributeBool("limbspecific", false);
if (!LimbSpecific)
{
string indicatorLimbName = element.GetAttributeString("indicatorlimb", "Torso");
if (!Enum.TryParse(indicatorLimbName, out IndicatorLimb))
{
DebugConsole.ThrowErrorLocalized("Error in affliction prefab " + Name + " - limb type \"" + indicatorLimbName + "\" not found.");
}
}
HideIconAfterDelay = element.GetAttributeBool(nameof(HideIconAfterDelay), false);
ActivationThreshold = element.GetAttributeFloat(nameof(ActivationThreshold), 0.0f);
ShowIconThreshold = element.GetAttributeFloat(nameof(ShowIconThreshold), Math.Max(ActivationThreshold, 0.05f));
ShowIconToOthersThreshold = element.GetAttributeFloat(nameof(ShowIconToOthersThreshold), ShowIconThreshold);
MaxStrength = element.GetAttributeFloat(nameof(MaxStrength), 100.0f);
GrainBurst = element.GetAttributeFloat(nameof(GrainBurst), 0.0f);
ShowInHealthScannerThreshold = element.GetAttributeFloat(nameof(ShowInHealthScannerThreshold),
Math.Max(ActivationThreshold, AfflictionType == "talentbuff" ? float.MaxValue : ShowIconToOthersThreshold));
TreatmentThreshold = element.GetAttributeFloat(nameof(TreatmentThreshold), Math.Max(ActivationThreshold, 10.0f));
DamageOverlayAlpha = element.GetAttributeFloat(nameof(DamageOverlayAlpha), 0.0f);
BurnOverlayAlpha = element.GetAttributeFloat(nameof(BurnOverlayAlpha), 0.0f);
KarmaChangeOnApplied = element.GetAttributeFloat(nameof(KarmaChangeOnApplied), 0.0f);
CauseOfDeathDescription =
TextManager.Get($"AfflictionCauseOfDeath.{TranslationIdentifier}")
.Fallback(TextManager.Get(element.GetAttributeString("causeofdeathdescription", "")))
.Fallback(element.GetAttributeString("causeofdeathdescription", ""));
SelfCauseOfDeathDescription =
TextManager.Get($"AfflictionCauseOfDeathSelf.{TranslationIdentifier}")
.Fallback(TextManager.Get(element.GetAttributeString("selfcauseofdeathdescription", "")))
.Fallback(element.GetAttributeString("selfcauseofdeathdescription", ""));
IconColors = element.GetAttributeColorArray(nameof(IconColors), null);
AfflictionOverlayAlphaIsLinear = element.GetAttributeBool(nameof(AfflictionOverlayAlphaIsLinear), false);
AchievementOnReceived = element.GetAttributeIdentifier(nameof(AchievementOnReceived), "");
AchievementOnRemoved = element.GetAttributeIdentifier(nameof(AchievementOnRemoved), "");
TargetSpecies = element.GetAttributeIdentifierArray("targets", Array.Empty<Identifier>(), trim: true);
ResetBetweenRounds = element.GetAttributeBool("resetbetweenrounds", false);
DamageParticles = element.GetAttributeBool(nameof(DamageParticles), true);
WeaponsSkillGain = element.GetAttributeFloat(nameof(WeaponsSkillGain), 0.0f);
MedicalSkillGain = element.GetAttributeFloat(nameof(MedicalSkillGain), 0.0f);
List<Description> descriptions = new List<Description>();
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "icon":
Icon = new Sprite(subElement);
break;
case "afflictionoverlay":
AfflictionOverlay = new Sprite(subElement);
break;
case "statvalue":
DebugConsole.ThrowError($"Error in affliction \"{Identifier}\" - stat values should be configured inside the affliction's effects.",
contentPackage: element.ContentPackage);
break;
case "effect":
case "periodiceffect":
break;
case "description":
descriptions.Add(new Description(subElement, this));
break;
default:
DebugConsole.AddWarning($"Unrecognized element in affliction \"{Identifier}\" ({subElement.Name})",
contentPackage: element.ContentPackage);
break;
}
}
Descriptions = descriptions.ToImmutableList();
constructor = type.GetConstructor(new[] { typeof(AfflictionPrefab), typeof(float) });
}
private void RefreshTreatmentSuitabilities()
{
var newTreatmentSuitabilities = new Dictionary<Identifier, float>();
foreach (var itemPrefab in ItemPrefab.Prefabs)
{
float suitability = itemPrefab.GetTreatmentSuitability(Identifier) + itemPrefab.GetTreatmentSuitability(AfflictionType);
if (!MathUtils.NearlyEqual(suitability, 0.0f))
{
newTreatmentSuitabilities.TryAdd(itemPrefab.Identifier, suitability);
}
}
HasTreatments = newTreatmentSuitabilities.Any(kvp => kvp.Value > 0);
TreatmentSuitabilities = newTreatmentSuitabilities.ToImmutableDictionary();
}
public LocalizedString GetDescription(float strength, Description.TargetType targetType)
{
foreach (var description in Descriptions)
{
if (strength < description.MinStrength || strength > description.MaxStrength) { continue; }
switch (targetType)
{
case Description.TargetType.Self:
if (description.Target == Description.TargetType.OtherCharacter) { continue; }
break;
case Description.TargetType.OtherCharacter:
if (description.Target == Description.TargetType.Self) { continue; }
break;
}
return description.Text;
}
return defaultDescription;
}
/// <summary>
/// Should be called before each round: loads all StatusEffects and refreshes treatment suitabilities.
/// </summary>
public static void LoadAllEffectsAndTreatmentSuitabilities()
{
foreach (var prefab in Prefabs)
{
prefab.RefreshTreatmentSuitabilities();
prefab.LoadEffects();
}
}
/// <summary>
/// Removes all the effects of the prefab (including the sounds and other assets defined in them).
/// Note that you need to call LoadAllEffectsAndTreatmentSuitabilities before trying to use the affliction again!
/// </summary>
public static void ClearAllEffects()
{
Prefabs.ForEach(p => p.ClearEffects());
}
private void LoadEffects()
{
ClearEffects();
foreach (var subElement in configElement.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "effect":
effects.Add(new Effect(subElement, Name.Value));
break;
case "periodiceffect":
periodicEffects.Add(new PeriodicEffect(subElement, Name.Value));
break;
}
}
for (int i = 0; i < effects.Count; i++)
{
for (int j = i + 1; j < effects.Count; j++)
{
var a = effects[i];
var b = effects[j];
if (a.MinStrength < b.MaxStrength && b.MinStrength < a.MaxStrength)
{
DebugConsole.AddWarning($"Affliction \"{Identifier}\" contains effects with overlapping strength ranges. Only one effect can be active at a time, meaning one of the effects won't work.",
ContentPackage);
}
}
}
}
private void ClearEffects()
{
effects.Clear();
periodicEffects.Clear();
}
#if CLIENT
public void ReloadSoundsIfNeeded()
{
foreach (var effect in effects)
{
foreach (var statusEffect in effect.StatusEffects)
{
foreach (var sound in statusEffect.Sounds)
{
if (sound.Sound == null) { RoundSound.Reload(sound); }
}
}
}
foreach (var periodicEffect in periodicEffects)
{
foreach (var statusEffect in periodicEffect.StatusEffects)
{
foreach (var sound in statusEffect.Sounds)
{
if (sound.Sound == null) { RoundSound.Reload(sound); }
}
}
}
}
#endif
public override string ToString()
{
return $"AfflictionPrefab ({Name})";
}
public Affliction Instantiate(float strength, Character source = null)
{
object instance = null;
try
{
instance = constructor.Invoke(new object[] { this, strength });
}
catch (Exception ex)
{
DebugConsole.ThrowError(ex.InnerException != null ? ex.InnerException.ToString() : ex.ToString());
}
Affliction affliction = instance as Affliction;
affliction.Source = source;
return affliction;
}
public Effect GetActiveEffect(float currentStrength)
{
foreach (Effect effect in effects)
{
if (currentStrength > effect.MinStrength && currentStrength <= effect.MaxStrength)
{
return effect;
}
}
//if above the strength range of all effects, use the highest strength effect
Effect strongestEffect = null;
float largestStrength = currentStrength;
foreach (Effect effect in effects)
{
if (currentStrength > effect.MaxStrength &&
(strongestEffect == null || effect.MaxStrength > largestStrength))
{
strongestEffect = effect;
largestStrength = effect.MaxStrength;
}
}
return strongestEffect;
}
public float GetTreatmentSuitability(Item item)
{
if (item == null)
{
return 0.0f;
}
return Math.Max(item.Prefab.GetTreatmentSuitability(Identifier), item.Prefab.GetTreatmentSuitability(AfflictionType));
}
}
}