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 Prefabs = new PrefabSelector(); 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() { } } /// /// AfflictionPrefabHusk is a special type of affliction that has added functionality for husk infection. /// 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; /// /// The minimum strength at which husk infection will be in the dormant stage. /// It must be less than or equal to ActiveThreshold. /// public readonly float DormantThreshold; /// /// 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. /// public readonly float ActiveThreshold; /// /// The minimum strength at which husk infection will be in its final stage. /// It must be greater than or equal to ActiveThreshold. /// public readonly float TransitionThreshold; /// /// The minimum strength the affliction must have for the affected character /// to transform into a husk upon death. /// public readonly float TransformThresholdOnDeath; /// /// The species of husk to convert the affected character to /// once husk infection reaches its final stage. /// public readonly Identifier HuskedSpeciesName; /// /// If set to true, all buffs are transferred to the converted /// character after husk transformation is complete. /// public readonly bool TransferBuffs; /// /// 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. /// public readonly bool SendMessages; /// /// If set to true, affected characters will have their speech impeded once the affliction /// reaches the dormant stage. /// public readonly bool CauseSpeechImpediment; /// /// If set to false, affected characters will no longer require air /// once the affliction reaches the active stage. /// public readonly bool NeedsAir; /// /// If set to true, affected players will retain control of their character /// after transforming into a husk. /// 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); } } /// /// 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. /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// class AfflictionPrefab : PrefabWithUintIdentifier { /// /// Effects are the primary way to add functionality to afflictions. /// /// /// /// /// Enables the specified flag on the character as long as the effect is active. /// /// /// /// Flag that will be enabled for the character as long as the effect is active. /// /// /// /// /// /// /// /// /// /// /// Which ability flag to enable. /// /// /// 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; } /// /// A list of identifiers of afflictions that the affected character will be /// resistant to when this effect is active. /// public readonly ImmutableArray 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; } /// /// StatType that will be applied to the affected character when the effect is active that is proportional to the effect's strength. /// /// /// /// /// /// /// /// /// /// /// public readonly struct AppliedStatValue { /// /// Which StatType to apply /// public readonly StatTypes StatType; /// /// Minimum value to apply /// public readonly float MinValue; /// /// Minimum value to apply /// public readonly float MaxValue; /// /// Constant value to apply, will be ignored if MinValue or MaxValue are set /// 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); } } /// /// Prevents AfflictionHusks with the specified identifier(s) from transforming the character into an AI-controlled character. /// public readonly ImmutableArray BlockTransformation; /// /// StatType that will be applied to the affected character when the effect is active that is proportional to the effect's strength. /// public readonly ImmutableDictionary AfflictionStatValues; public readonly AbilityFlags AfflictionAbilityFlags; //statuseffects applied on the character when the affliction is active public readonly ImmutableArray StatusEffects; public Effect(ContentXElement element, string parentDebugName) { SerializableProperty.DeserializeProperties(this, element); ResistanceFor = element.GetAttributeIdentifierArray("resistancefor", Array.Empty())!.ToImmutableArray(); BlockTransformation = element.GetAttributeIdentifierArray("blocktransformation", Array.Empty())!.ToImmutableArray(); var afflictionStatValues = new Dictionary(); var statusEffects = new List(); 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(); } /// /// Returns 0 if affliction.Strength is MinStrength, /// 1 if affliction.Strength is MaxStrength /// public float GetStrengthFactor(Affliction affliction) => GetStrengthFactor(affliction.Strength); /// /// Returns 0 if affliction.Strength is MinStrength, /// 1 if affliction.Strength is MaxStrength /// public float GetStrengthFactor(float strength) => MathUtils.InverseLerp( MinStrength, MaxStrength, strength); } /// /// 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. /// /// /// /// Raw text for the description. /// /// public sealed class Description { public enum TargetType { /// /// Everyone can see the description. /// Any, /// /// Only the affected character can see the description. /// Self, /// /// The affected character cannot see the description but others can. /// OtherCharacter } /// /// Raw text for the description. /// public readonly LocalizedString Text; /// /// Text tag used to set the text from the localization files. /// public readonly Identifier TextTag; /// /// Minimum strength required for the description to be shown. /// public readonly float MinStrength; /// /// Maximum strength required for the description to be shown. /// public readonly float MaxStrength; /// /// Who can see the description. /// 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); } } /// /// PeriodicEffect applies StatusEffects to the character periodically. /// /// /// /// /// How often the status effect is applied in seconds. /// Setting this attribute will set both the min and max interval to the specified value. /// /// /// Minimum interval between applying the status effect in seconds. /// /// /// Maximum interval between applying the status effect in seconds. /// /// public sealed class PeriodicEffect { public readonly List StatusEffects = new List(); 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 Prefabs = new PrefabCollection(); public static IEnumerable 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 Descriptions; /// /// Arbitrary string that is used to identify the type of the affliction. /// public readonly Identifier AfflictionType; /// /// If set to true, the affliction affects individual limbs. Otherwise, it affects the whole character. /// public readonly bool LimbSpecific; /// /// 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. /// public readonly LimbType IndicatorLimb; /// /// Can be set to the identifier of another affliction to make this affliction /// reuse the same name and description. /// public readonly Identifier TranslationIdentifier; /// /// 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. /// public readonly bool IsBuff; /// /// If set to true, this affliction can affect characters that are marked as /// machines, such as the Fractal Guardian. /// public readonly bool AffectMachines; /// /// If set to true, this affliction can be healed at the medical clinic. /// /// /// /// false if the affliction is a buff or has the type "geneticmaterialbuff" or "geneticmaterialdebuff", true otherwise. /// /// public readonly bool HealableInMedicalClinic; /// /// How much each unit of this affliction's strength will add /// to the cost of healing at the medical clinic. /// public readonly float HealCostMultiplier; /// /// The minimum cost of healing this affliction at the medical clinic. /// public readonly int BaseHealCost; /// /// If set to false, the health UI will not show the strength of the affliction /// as a bar under its indicator. /// public readonly bool ShowBarInHealthMenu; /// /// If set to true, this affliction's icon will be hidden from the HUD after 5 seconds. /// public readonly bool HideIconAfterDelay; /// /// How high the strength has to be for the affliction to take effect /// public readonly float ActivationThreshold = 0.0f; /// /// How high the strength has to be for the affliction icon to be shown in the UI /// public readonly float ShowIconThreshold = 0.05f; /// /// 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 /// public readonly float ShowIconToOthersThreshold = 0.05f; /// /// The maximum strength this affliction can have. /// public readonly float MaxStrength = 100.0f; /// /// The strength of the radiation grain effect to apply when the strength of this affliction increases. /// public readonly float GrainBurst; /// /// How high the strength has to be for the affliction icon to be shown with a health scanner /// public readonly float ShowInHealthScannerThreshold; /// /// 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. /// public readonly float TreatmentThreshold; /// /// Bots will not try to treat the affliction if the character has any of these afflictions /// public ImmutableHashSet IgnoreTreatmentIfAfflictedBy; /// /// The duration of the affliction, in seconds. If set to 0, the affliction does not expire. /// public readonly float Duration; /// /// How much karma changes when a player applies this affliction to someone (per strength of the affliction) /// public float KarmaChangeOnApplied; /// /// Opacity of the burn effect (darker tint) on limbs affected by this affliction. 1 = full strength. /// public readonly float BurnOverlayAlpha; /// /// Opacity of the bloody damage overlay on limbs affected by this affliction. 1 = full strength. /// public readonly float DamageOverlayAlpha; /// Steam achievement given when the controlled character receives the affliction. /// public readonly Identifier AchievementOnReceived; /// /// Steam achievement given when the affliction is removed from the controlled character. /// public readonly Identifier AchievementOnRemoved; /// /// A gradient that defines which color to render this affliction's icon /// with, based on the affliction's current strength. /// public readonly Color[] IconColors; /// /// 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. /// public readonly bool AfflictionOverlayAlphaIsLinear; /// /// If set to true, this affliction will not persist between rounds. /// public readonly bool ResetBetweenRounds; /// /// Should damage particles be emitted when a character receives this affliction? /// Only relevant if the affliction is of the type "bleeding" or "damage". /// public readonly bool DamageParticles; /// /// 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. /// public readonly float MedicalSkillGain; /// /// 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. /// public readonly float WeaponsSkillGain; /// /// A list of species this affliction is allowed to affect. /// public Identifier[] TargetSpecies { get; protected set; } /// /// 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. /// private readonly List effects = new List(); /// /// PeriodicEffect applies StatusEffects to the character periodically. /// private readonly List periodicEffects = new List(); public IEnumerable Effects => effects; public IList PeriodicEffects => periodicEffects; private readonly ConstructorInfo constructor; /// /// An icon that’s used in the UI to represent this affliction. /// public readonly Sprite Icon; /// /// 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 /// public readonly Sprite AfflictionOverlay; public ImmutableDictionary TreatmentSuitabilities { get; private set; } = new Dictionary().ToImmutableDictionary(); /// /// Can this affliction be treated with some item? /// 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()).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(), 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 descriptions = new List(); 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(); 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; } /// /// Should be called before each round: loads all StatusEffects and refreshes treatment suitabilities. /// public static void LoadAllEffectsAndTreatmentSuitabilities() { foreach (var prefab in Prefabs) { prefab.RefreshTreatmentSuitabilities(); prefab.LoadEffects(); } } /// /// 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! /// 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)); } } }