Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs
5 using FarseerPhysics;
6 using FarseerPhysics.Dynamics;
7 using Microsoft.Xna.Framework;
8 using System;
9 using System.Collections.Generic;
10 using System.Collections.Immutable;
11 using System.Linq;
12 using System.Xml.Linq;
13 
14 namespace Barotrauma
15 {
17  {
18  public readonly StatusEffect Parent;
19  public readonly Entity Entity;
20  public float Duration
21  {
22  get;
23  private set;
24  }
25  public readonly List<ISerializableEntity> Targets;
26  public Character User { get; private set; }
27 
28  public float Timer;
29 
30  public DurationListElement(StatusEffect parentEffect, Entity parentEntity, IEnumerable<ISerializableEntity> targets, float duration, Character user)
31  {
32  Parent = parentEffect;
33  Entity = parentEntity;
34  Targets = new List<ISerializableEntity>(targets);
35  Timer = Duration = duration;
36  User = user;
37  }
38 
39  public void Reset(float duration, Character newUser)
40  {
41  Timer = Duration = duration;
42  User = newUser;
43  }
44  }
45 
71  partial class StatusEffect
72  {
73  private static readonly ImmutableHashSet<Identifier> FieldNames;
74  static StatusEffect()
75  {
76  FieldNames = typeof(StatusEffect).GetFields().AsEnumerable().Select(f => f.Name.ToIdentifier()).ToImmutableHashSet();
77  }
78 
79  [Flags]
80  public enum TargetType
81  {
85  This = 1,
89  Parent = 2,
93  Character = 4,
97  Contained = 8,
101  NearbyCharacters = 16,
105  NearbyItems = 32,
109  UseTarget = 64,
113  Hull = 128,
117  Limb = 256,
121  AllLimbs = 512,
125  LastLimb = 1024
126  }
127 
131  class ItemSpawnInfo
132  {
133  public enum SpawnPositionType
134  {
138  This,
142  ThisInventory,
146  SameInventory,
150  ContainedInventory,
154  Target
155  }
156 
157  public enum SpawnRotationType
158  {
162  None,
166  This,
170  Target,
174  Limb,
178  MainLimb,
182  Collider,
186  Random
187  }
188 
189  public readonly ItemPrefab ItemPrefab;
193  public readonly SpawnPositionType SpawnPosition;
194 
198  public readonly bool SpawnIfInventoryFull;
202  public readonly bool SpawnIfNotInInventory;
206  public readonly bool SpawnIfCantBeContained;
210  public readonly float Impulse;
211  public readonly float RotationRad;
215  public readonly int Count;
219  public readonly float Spread;
223  public readonly SpawnRotationType RotationType;
227  public readonly float AimSpreadRad;
231  public readonly bool Equip;
235  public readonly float Condition;
236 
237  public bool InheritEventTags { get; private set; }
238 
239  public ItemSpawnInfo(ContentXElement element, string parentDebugName)
240  {
241  if (element.GetAttribute("name") != null)
242  {
243  //backwards compatibility
244  DebugConsole.ThrowError("Error in StatusEffect config (" + element.ToString() + ") - use item identifier instead of the name.", contentPackage: element.ContentPackage);
245  string itemPrefabName = element.GetAttributeString("name", "");
246  ItemPrefab = ItemPrefab.Prefabs.Find(m => m.NameMatches(itemPrefabName, StringComparison.InvariantCultureIgnoreCase) || m.Tags.Contains(itemPrefabName));
247  if (ItemPrefab == null)
248  {
249  DebugConsole.ThrowError("Error in StatusEffect \"" + parentDebugName + "\" - item prefab \"" + itemPrefabName + "\" not found.", contentPackage: element.ContentPackage);
250  }
251  }
252  else
253  {
254  string itemPrefabIdentifier = element.GetAttributeString("identifier", "");
255  if (string.IsNullOrEmpty(itemPrefabIdentifier)) itemPrefabIdentifier = element.GetAttributeString("identifiers", "");
256  if (string.IsNullOrEmpty(itemPrefabIdentifier))
257  {
258  DebugConsole.ThrowError("Invalid item spawn in StatusEffect \"" + parentDebugName + "\" - identifier not found in the element \"" + element.ToString() + "\".", contentPackage: element.ContentPackage);
259  }
260  ItemPrefab = ItemPrefab.Prefabs.Find(m => m.Identifier == itemPrefabIdentifier);
261  if (ItemPrefab == null)
262  {
263  DebugConsole.ThrowError("Error in StatusEffect config - item prefab with the identifier \"" + itemPrefabIdentifier + "\" not found.", contentPackage: element.ContentPackage);
264  return;
265  }
266  }
267 
268  SpawnIfInventoryFull = element.GetAttributeBool(nameof(SpawnIfInventoryFull), false);
269  SpawnIfNotInInventory = element.GetAttributeBool(nameof(SpawnIfNotInInventory), false);
270  SpawnIfCantBeContained = element.GetAttributeBool(nameof(SpawnIfCantBeContained), true);
271  Impulse = element.GetAttributeFloat("impulse", element.GetAttributeFloat("launchimpulse", element.GetAttributeFloat("speed", 0.0f)));
272 
273  Condition = MathHelper.Clamp(element.GetAttributeFloat("condition", 1.0f), 0.0f, 1.0f);
274 
275  RotationRad = MathHelper.ToRadians(element.GetAttributeFloat("rotation", 0.0f));
276  Count = element.GetAttributeInt("count", 1);
277  Spread = element.GetAttributeFloat("spread", 0f);
278  AimSpreadRad = MathHelper.ToRadians(element.GetAttributeFloat("aimspread", 0f));
279  Equip = element.GetAttributeBool("equip", false);
280 
281  SpawnPosition = element.GetAttributeEnum("spawnposition", SpawnPositionType.This);
282 
283  if (element.GetAttributeString("rotationtype", string.Empty).Equals("Fixed", StringComparison.OrdinalIgnoreCase))
284  {
285  //backwards compatibility, "This" was previously (inaccurately) called "Fixed"
286  RotationType = SpawnRotationType.This;
287  }
288  else
289  {
290  RotationType = element.GetAttributeEnum("rotationtype", RotationRad != 0 ? SpawnRotationType.This : SpawnRotationType.Target);
291  }
292  InheritEventTags = element.GetAttributeBool(nameof(InheritEventTags), false);
293  }
294  }
295 
305  {
306  public AbilityStatusEffectIdentifier(Identifier effectIdentifier)
307  {
308  EffectIdentifier = effectIdentifier;
309  }
310  public Identifier EffectIdentifier { get; set; }
311  }
312 
316  public class GiveTalentInfo
317  {
321  public Identifier[] TalentIdentifiers;
325  public bool GiveRandom;
326 
327  public GiveTalentInfo(XElement element, string _)
328  {
329  TalentIdentifiers = element.GetAttributeIdentifierArray("talentidentifiers", Array.Empty<Identifier>());
330  GiveRandom = element.GetAttributeBool("giverandom", false);
331  }
332  }
333 
337  public class GiveSkill
338  {
342  public readonly Identifier SkillIdentifier;
346  public readonly float Amount;
350  public readonly bool TriggerTalents;
354  public readonly bool UseDeltaTime;
359  public readonly bool Proportional;
360 
361  public GiveSkill(ContentXElement element, string parentDebugName)
362  {
363  SkillIdentifier = element.GetAttributeIdentifier(nameof(SkillIdentifier), Identifier.Empty);
364  Amount = element.GetAttributeFloat(nameof(Amount), 0);
365  TriggerTalents = element.GetAttributeBool(nameof(TriggerTalents), true);
366  UseDeltaTime = element.GetAttributeBool(nameof(UseDeltaTime), false);
367  Proportional = element.GetAttributeBool(nameof(Proportional), false);
368 
369  if (SkillIdentifier == Identifier.Empty)
370  {
371  DebugConsole.ThrowError($"GiveSkill StatusEffect did not have a skill identifier defined in {parentDebugName}!", contentPackage: element.ContentPackage);
372  }
373  }
374  }
375 
380  {
381  public string Name => $"Character Spawn Info ({SpeciesName})";
382  public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; set; }
383 
384  [Serialize("", IsPropertySaveable.No, description: "The species name (identifier) of the character to spawn.")]
385  public Identifier SpeciesName { get; private set; }
386 
387  [Serialize(1, IsPropertySaveable.No, description: "How many characters to spawn.")]
388  public int Count { get; private set; }
389 
390  [Serialize(false, IsPropertySaveable.No, description:
391  "Should the buffs of the character executing the effect be transferred to the spawned character?"+
392  " Useful for effects that \"transform\" a character to something else by deleting the character and spawning a new one on its place.")]
393  public bool TransferBuffs { get; private set; }
394 
395  [Serialize(false, IsPropertySaveable.No, description:
396  "Should the afflictions of the character executing the effect be transferred to the spawned character?" +
397  " Useful for effects that \"transform\" a character to something else by deleting the character and spawning a new one on its place.")]
398  public bool TransferAfflictions { get; private set; }
399 
400  [Serialize(false, IsPropertySaveable.No, description:
401  "Should the the items from the character executing the effect be transferred to the spawned character?" +
402  " Useful for effects that \"transform\" a character to something else by deleting the character and spawning a new one on its place.")]
403  public bool TransferInventory { get; private set; }
404 
405  [Serialize(0, IsPropertySaveable.No, description:
406  "The maximum number of creatures of the given species and team that can exist in the current level before this status effect stops spawning any more.")]
407  public int TotalMaxCount { get; private set; }
408 
409  [Serialize(0, IsPropertySaveable.No, description: "Amount of stun to apply on the spawned character.")]
410  public int Stun { get; private set; }
411 
412  [Serialize("", IsPropertySaveable.No, description: "An affliction to apply on the spawned character.")]
413  public Identifier AfflictionOnSpawn { get; private set; }
414 
415  [Serialize(1, IsPropertySaveable.No, description:
416  $"The strength of the affliction applied on the spawned character. Only relevant if {nameof(AfflictionOnSpawn)} is defined.")]
417  public int AfflictionStrength { get; private set; }
418 
419  [Serialize(false, IsPropertySaveable.No, description:
420  "Should the player controlling the character that executes the effect gain control of the spawned character?" +
421  " Useful for effects that \"transform\" a character to something else by deleting the character and spawning a new one on its place.")]
422  public bool TransferControl { get; private set; }
423 
424  [Serialize(false, IsPropertySaveable.No, description:
425  "Should the character that executes the effect be removed when the effect executes?" +
426  " Useful for effects that \"transform\" a character to something else by deleting the character and spawning a new one on its place.")]
427  public bool RemovePreviousCharacter { get; private set; }
428 
429  [Serialize(0f, IsPropertySaveable.No, description: "Amount of random spread to add to the spawn position. " +
430  "Can be used to prevent all the characters from spawning at the exact same position if the effect spawns multiple ones.")]
431  public float Spread { get; private set; }
432 
433  [Serialize("0,0", IsPropertySaveable.No, description:
434  "Offset added to the spawn position. " +
435  "Can be used to for example spawn a character a bit up from the center of an item executing the effect.")]
436  public Vector2 Offset { get; private set; }
437 
438  [Serialize(false, IsPropertySaveable.No)]
439  public bool InheritEventTags { get; private set; }
440 
441  public CharacterSpawnInfo(ContentXElement element, string parentDebugName)
442  {
444  if (SpeciesName.IsEmpty)
445  {
446  DebugConsole.ThrowError($"Invalid character spawn ({Name}) in StatusEffect \"{parentDebugName}\" - identifier not found in the element \"{element}\".", contentPackage: element.ContentPackage);
447  }
448  }
449  }
450 
455  {
456  public string Name => "ai trigger";
457 
458  public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; set; }
459 
460  [Serialize(AIState.Idle, IsPropertySaveable.No, description: "The AI state the character should switch to.")]
461  public AIState State { get; private set; }
462 
463  [Serialize(0f, IsPropertySaveable.No, description: "How long should the character stay in the specified state? If 0, the effect is permanent (unless overridden by another AITrigger).")]
464  public float Duration { get; private set; }
465 
466  [Serialize(1f, IsPropertySaveable.No, description: "How likely is the AI to change the state when this effect executes? 1 = always, 0.5 = 50% chance, 0 = never.")]
467  public float Probability { get; private set; }
468 
469  [Serialize(0f, IsPropertySaveable.No, description:
470  "How much damage the character must receive for this AITrigger to become active? " +
471  "Checks the amount of damage the latest attack did to the character.")]
472  public float MinDamage { get; private set; }
473 
474  [Serialize(true, IsPropertySaveable.No, description: "Can this AITrigger override other active AITriggers?")]
475  public bool AllowToOverride { get; private set; }
476 
477  [Serialize(true, IsPropertySaveable.No, description: "Can this AITrigger be overridden by other AITriggers?")]
478  public bool AllowToBeOverridden { get; private set; }
479 
480  public bool IsTriggered { get; private set; }
481 
482  public float Timer { get; private set; }
483 
484  public bool IsActive { get; private set; }
485 
486  public bool IsPermanent { get; private set; }
487 
488  public void Launch()
489  {
490  IsTriggered = true;
491  IsActive = true;
492  IsPermanent = Duration <= 0;
493  if (!IsPermanent)
494  {
495  Timer = Duration;
496  }
497  }
498 
499  public void Reset()
500  {
501  IsTriggered = false;
502  IsActive = false;
503  Timer = 0;
504  }
505 
506  public void UpdateTimer(float deltaTime)
507  {
508  if (IsPermanent) { return; }
509  Timer -= deltaTime;
510  if (Timer < 0)
511  {
512  Timer = 0;
513  IsActive = false;
514  }
515  }
516 
517  public AITrigger(XElement element)
518  {
520  }
521  }
522 
523 
527  private readonly TargetType targetTypes;
528 
532  public int TargetSlot = -1;
533 
534  private readonly List<RelatedItem> requiredItems;
535 
536  public readonly ImmutableArray<(Identifier propertyName, object value)> PropertyEffects;
537 
538  private readonly PropertyConditional.LogicalOperatorType conditionalLogicalOperator = PropertyConditional.LogicalOperatorType.Or;
539  private readonly List<PropertyConditional> propertyConditionals;
540  public bool HasConditions => propertyConditionals != null && propertyConditionals.Any();
541 
545  private readonly bool setValue;
546 
552  private readonly bool disableDeltaTime;
553 
557  private readonly HashSet<Identifier> tags;
558 
565  private readonly float lifeTime;
566  private float lifeTimer;
567 
568  private Dictionary<Entity, float> intervalTimers;
569 
573  private readonly bool oneShot;
574 
575  public static readonly List<DurationListElement> DurationList = new List<DurationListElement>();
576 
583  public readonly bool CheckConditionalAlways;
584 
588  public readonly bool Stackable = true;
589 
595  public readonly float Interval;
596 
597 #if CLIENT
601  private readonly bool playSoundOnRequiredItemFailure = false;
602 #endif
603 
604  private readonly int useItemCount;
605 
606  private readonly bool removeItem, dropContainedItems, dropItem, removeCharacter, breakLimb, hideLimb;
607  private readonly float hideLimbTimer;
608 
609  public readonly ActionType type = ActionType.OnActive;
610 
611  private readonly List<Explosion> explosions;
612  public IEnumerable<Explosion> Explosions
613  {
614  get { return explosions ?? Enumerable.Empty<Explosion>(); }
615  }
616 
617  private readonly List<ItemSpawnInfo> spawnItems;
618 
622  private readonly bool spawnItemRandomly;
623  private readonly List<CharacterSpawnInfo> spawnCharacters;
624 
625  public readonly List<GiveTalentInfo> giveTalentInfos;
626 
627  private readonly List<AITrigger> aiTriggers;
628 
634  private readonly List<EventPrefab> triggeredEvents;
635 
640  private readonly Identifier triggeredEventTargetTag = "statuseffecttarget".ToIdentifier();
641 
646  private readonly Identifier triggeredEventEntityTag = "statuseffectentity".ToIdentifier();
647 
652  private readonly Identifier triggeredEventUserTag = "statuseffectuser".ToIdentifier();
653 
657  private readonly List<(Identifier eventIdentifier, Identifier tag)> eventTargetTags;
658 
659  private Character user;
660 
661  public readonly float FireSize;
662 
666  public readonly LimbType[] targetLimbs;
667 
671  public readonly float SeverLimbsProbability;
672 
674 
678  public readonly bool OnlyInside;
682  public readonly bool OnlyOutside;
683 
688  public readonly bool OnlyWhenDamagedByPlayer;
689 
693  public readonly bool AllowWhenBroken = false;
694 
698  public readonly ImmutableHashSet<Identifier> TargetIdentifiers;
699 
704  public readonly string TargetItemComponent;
708  private readonly HashSet<(Identifier affliction, float strength)> requiredAfflictions;
709 
710  public float AfflictionMultiplier = 1.0f;
711 
712  public List<Affliction> Afflictions
713  {
714  get;
715  private set;
716  } = new List<Affliction>();
717 
723  private readonly bool multiplyAfflictionsByMaxVitality;
724 
725  public IEnumerable<CharacterSpawnInfo> SpawnCharacters
726  {
727  get { return spawnCharacters ?? Enumerable.Empty<CharacterSpawnInfo>(); }
728  }
729 
730  public readonly List<(Identifier AfflictionIdentifier, float ReduceAmount)> ReduceAffliction = new List<(Identifier affliction, float amount)>();
731 
732  private readonly List<Identifier> talentTriggers;
733  private readonly List<int> giveExperiences;
734  private readonly List<GiveSkill> giveSkills;
735  private readonly List<(string, ContentXElement)> luaHook;
736 
737 
738  private HashSet<(Character targetCharacter, AnimLoadInfo anim)> failedAnimations;
739  public readonly record struct AnimLoadInfo(AnimationType Type, Either<string, ContentPath> File, float Priority, ImmutableArray<Identifier> ExpectedSpeciesNames);
740  private readonly List<AnimLoadInfo> animationsToTrigger;
741 
748  public readonly float Duration;
749 
753  public float Range
754  {
755  get;
756  private set;
757  }
758 
763  public Vector2 Offset { get; private set; }
764 
765  public string Tags
766  {
767  get { return string.Join(",", tags); }
768  set
769  {
770  tags.Clear();
771  if (value == null) return;
772 
773  string[] newTags = value.Split(',');
774  foreach (string tag in newTags)
775  {
776  Identifier newTag = tag.Trim().ToIdentifier();
777  if (!tags.Contains(newTag)) { tags.Add(newTag); };
778  }
779  }
780  }
781 
782  public bool Disabled { get; private set; }
783 
784  public static StatusEffect Load(ContentXElement element, string parentDebugName)
785  {
786  if (element.GetAttribute("delay") != null || element.GetAttribute("delaytype") != null)
787  {
788  return new DelayedEffect(element, parentDebugName);
789  }
790 
791  return new StatusEffect(element, parentDebugName);
792  }
793 
794  protected StatusEffect(ContentXElement element, string parentDebugName)
795  {
796  tags = new HashSet<Identifier>(element.GetAttributeString("tags", "").Split(',').ToIdentifiers());
797  OnlyInside = element.GetAttributeBool("onlyinside", false);
798  OnlyOutside = element.GetAttributeBool("onlyoutside", false);
799  OnlyWhenDamagedByPlayer = element.GetAttributeBool("onlyplayertriggered", element.GetAttributeBool("onlywhendamagedbyplayer", false));
800  AllowWhenBroken = element.GetAttributeBool("allowwhenbroken", false);
801 
802  Interval = element.GetAttributeFloat("interval", 0.0f);
803  Duration = element.GetAttributeFloat("duration", 0.0f);
804  disableDeltaTime = element.GetAttributeBool("disabledeltatime", false);
805  setValue = element.GetAttributeBool("setvalue", false);
806  Stackable = element.GetAttributeBool("stackable", true);
807  lifeTime = lifeTimer = element.GetAttributeFloat("lifetime", 0.0f);
808  CheckConditionalAlways = element.GetAttributeBool("checkconditionalalways", false);
809 
810  TargetItemComponent = element.GetAttributeString("targetitemcomponent", string.Empty);
811  TargetSlot = element.GetAttributeInt("targetslot", -1);
812 
813  Range = element.GetAttributeFloat("range", 0.0f);
814  Offset = element.GetAttributeVector2("offset", Vector2.Zero);
815  string[] targetLimbNames = element.GetAttributeStringArray("targetlimb", null) ?? element.GetAttributeStringArray("targetlimbs", null);
816  if (targetLimbNames != null)
817  {
818  List<LimbType> targetLimbs = new List<LimbType>();
819  foreach (string targetLimbName in targetLimbNames)
820  {
821  if (Enum.TryParse(targetLimbName, ignoreCase: true, out LimbType targetLimb)) { targetLimbs.Add(targetLimb); }
822  }
823  if (targetLimbs.Count > 0) { this.targetLimbs = targetLimbs.ToArray(); }
824  }
825 
826  SeverLimbsProbability = MathHelper.Clamp(element.GetAttributeFloat(0.0f, "severlimbs", "severlimbsprobability"), 0.0f, 1.0f);
827 
828  string[] targetTypesStr =
829  element.GetAttributeStringArray("target", null) ??
830  element.GetAttributeStringArray("targettype", Array.Empty<string>());
831  foreach (string s in targetTypesStr)
832  {
833  if (!Enum.TryParse(s, true, out TargetType targetType))
834  {
835  DebugConsole.ThrowError($"Invalid target type \"{s}\" in StatusEffect ({parentDebugName})", contentPackage: element.ContentPackage);
836  }
837  else
838  {
839  targetTypes |= targetType;
840  }
841  }
842  if (targetTypes == 0)
843  {
844  string errorMessage = $"Potential error in StatusEffect ({parentDebugName}). Target not defined, the effect might not work correctly. Use target=\"This\" if you want the effect to target the entity it's defined in. Setting \"This\" as the target.";
845  DebugConsole.AddSafeError(errorMessage);
846  }
847 
848  var targetIdentifiers = element.GetAttributeIdentifierArray(Array.Empty<Identifier>(), "targetnames", "targets", "targetidentifiers", "targettags");
849  if (targetIdentifiers.Any())
850  {
851  TargetIdentifiers = targetIdentifiers.ToImmutableHashSet();
852  }
853 
854  triggeredEventTargetTag = element.GetAttributeIdentifier("eventtargettag", triggeredEventTargetTag);
855  triggeredEventEntityTag = element.GetAttributeIdentifier("evententitytag", triggeredEventEntityTag);
856  triggeredEventUserTag = element.GetAttributeIdentifier("eventusertag", triggeredEventUserTag);
857  spawnItemRandomly = element.GetAttributeBool("spawnitemrandomly", false);
858  multiplyAfflictionsByMaxVitality = element.GetAttributeBool(nameof(multiplyAfflictionsByMaxVitality), false);
859 #if CLIENT
860  playSoundOnRequiredItemFailure = element.GetAttributeBool("playsoundonrequireditemfailure", false);
861 #endif
862 
863  List<XAttribute> propertyAttributes = new List<XAttribute>();
864  propertyConditionals = new List<PropertyConditional>();
865  foreach (XAttribute attribute in element.Attributes())
866  {
867  switch (attribute.Name.ToString().ToLowerInvariant())
868  {
869  case "type":
870  if (!Enum.TryParse(attribute.Value, true, out type))
871  {
872  DebugConsole.ThrowError($"Invalid action type \"{attribute.Value}\" in StatusEffect ({parentDebugName})", contentPackage: element.ContentPackage);
873  }
874  break;
875  case "targettype":
876  case "target":
877  case "targetnames":
878  case "targets":
879  case "targetidentifiers":
880  case "targettags":
881  case "severlimbs":
882  case "targetlimb":
883  case "delay":
884  case "interval":
885  //aliases for fields we're already reading above, and which shouldn't be interpreted as values we're trying to set
886  break;
887  case "allowedafflictions":
888  case "requiredafflictions":
889  //backwards compatibility, should be defined as child elements instead
890  string[] types = attribute.Value.Split(',');
891  requiredAfflictions ??= new HashSet<(Identifier, float)>();
892  for (int i = 0; i < types.Length; i++)
893  {
894  requiredAfflictions.Add((types[i].Trim().ToIdentifier(), 0.0f));
895  }
896  break;
897  case "conditionalcomparison":
898  case "comparison":
899  if (!Enum.TryParse(attribute.Value, ignoreCase: true, out conditionalLogicalOperator))
900  {
901  DebugConsole.ThrowError($"Invalid conditional comparison type \"{attribute.Value}\" in StatusEffect ({parentDebugName})", contentPackage: element.ContentPackage);
902  }
903  break;
904  case "sound":
905  DebugConsole.ThrowError($"Error in StatusEffect ({parentDebugName}): sounds should be defined as child elements of the StatusEffect, not as attributes.", contentPackage: element.ContentPackage);
906  break;
907  case "range":
908  if (!HasTargetType(TargetType.NearbyCharacters) && !HasTargetType(TargetType.NearbyItems))
909  {
910  propertyAttributes.Add(attribute);
911  }
912  break;
913  case "tags":
914  if (Duration <= 0.0f || setValue)
915  {
916  //a workaround to "tags" possibly meaning either an item's tags or this status effect's tags:
917  //if the status effect doesn't have a duration, assume tags mean an item's tags, not this status effect's tags
918  propertyAttributes.Add(attribute);
919  }
920  break;
921  case "oneshot":
922  oneShot = attribute.GetAttributeBool(false);
923  break;
924  default:
925  if (FieldNames.Contains(attribute.Name.ToIdentifier())) { continue; }
926  propertyAttributes.Add(attribute);
927  break;
928  }
929  }
930 
931  if (Duration > 0.0f && !setValue)
932  {
933  //a workaround to "tags" possibly meaning either an item's tags or this status effect's tags:
934  //if the status effect has a duration, assume tags mean this status effect's tags and leave item tags untouched.
935  propertyAttributes.RemoveAll(a => a.Name.ToString().Equals("tags", StringComparison.OrdinalIgnoreCase));
936  }
937 
938  List<(Identifier propertyName, object value)> propertyEffects = new List<(Identifier propertyName, object value)>();
939  foreach (XAttribute attribute in propertyAttributes)
940  {
941  propertyEffects.Add((attribute.NameAsIdentifier(), XMLExtensions.GetAttributeObject(attribute)));
942  }
943  PropertyEffects = propertyEffects.ToImmutableArray();
944 
945  foreach (var subElement in element.Elements())
946  {
947  switch (subElement.Name.ToString().ToLowerInvariant())
948  {
949  case "explosion":
950  explosions ??= new List<Explosion>();
951  explosions.Add(new Explosion(subElement, parentDebugName));
952  break;
953  case "fire":
954  FireSize = subElement.GetAttributeFloat("size", 10.0f);
955  break;
956  case "use":
957  case "useitem":
958  useItemCount++;
959  break;
960  case "remove":
961  case "removeitem":
962  removeItem = true;
963  break;
964  case "dropcontaineditems":
965  dropContainedItems = true;
966  break;
967  case "dropitem":
968  dropItem = true;
969  break;
970  case "removecharacter":
971  removeCharacter = true;
972  break;
973  case "breaklimb":
974  breakLimb = true;
975  break;
976  case "hidelimb":
977  hideLimb = true;
978  hideLimbTimer = subElement.GetAttributeFloat("duration", 0);
979  break;
980  case "requireditem":
981  case "requireditems":
982  requiredItems ??= new List<RelatedItem>();
983  RelatedItem newRequiredItem = RelatedItem.Load(subElement, returnEmpty: false, parentDebugName: parentDebugName);
984  if (newRequiredItem == null)
985  {
986  DebugConsole.ThrowError("Error in StatusEffect config - requires an item with no identifiers.", contentPackage: element.ContentPackage);
987  continue;
988  }
989  requiredItems.Add(newRequiredItem);
990  break;
991  case "requiredaffliction":
992  requiredAfflictions ??= new HashSet<(Identifier, float)>();
993  Identifier[] ids = subElement.GetAttributeIdentifierArray("identifier", null) ?? subElement.GetAttributeIdentifierArray("type", Array.Empty<Identifier>());
994  foreach (var afflictionId in ids)
995  {
996  requiredAfflictions.Add((
997  afflictionId,
998  subElement.GetAttributeFloat("minstrength", 0.0f)));
999  }
1000  break;
1001  case "conditional":
1002  propertyConditionals.AddRange(PropertyConditional.FromXElement(subElement));
1003  break;
1004  case "affliction":
1005  AfflictionPrefab afflictionPrefab;
1006  if (subElement.GetAttribute("name") != null)
1007  {
1008  DebugConsole.ThrowError("Error in StatusEffect (" + parentDebugName + ") - define afflictions using identifiers instead of names.", contentPackage: element.ContentPackage);
1009  string afflictionName = subElement.GetAttributeString("name", "");
1010  afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Name.Equals(afflictionName, StringComparison.OrdinalIgnoreCase));
1011  if (afflictionPrefab == null)
1012  {
1013  DebugConsole.ThrowError("Error in StatusEffect (" + parentDebugName + ") - Affliction prefab \"" + afflictionName + "\" not found.", contentPackage: element.ContentPackage);
1014  continue;
1015  }
1016  }
1017  else
1018  {
1019  Identifier afflictionIdentifier = subElement.GetAttributeIdentifier("identifier", "");
1020  afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier == afflictionIdentifier);
1021  if (afflictionPrefab == null)
1022  {
1023  DebugConsole.ThrowError("Error in StatusEffect (" + parentDebugName + ") - Affliction prefab with the identifier \"" + afflictionIdentifier + "\" not found.", contentPackage: element.ContentPackage);
1024  continue;
1025  }
1026  }
1027 
1028  Affliction afflictionInstance = afflictionPrefab.Instantiate(subElement.GetAttributeFloat(1.0f, "amount", nameof(afflictionInstance.Strength)));
1029  // Deserializing the object normally might cause some unexpected side effects. At least it clamps the strength of the affliction, which we don't want here.
1030  // Could probably be solved by using the NonClampedStrength or by bypassing the clamping, but ran out of time and played it safe here.
1031  afflictionInstance.Probability = subElement.GetAttributeFloat(1.0f, nameof(afflictionInstance.Probability));
1032  afflictionInstance.MultiplyByMaxVitality = subElement.GetAttributeBool(nameof(afflictionInstance.MultiplyByMaxVitality), false);
1033  Afflictions.Add(afflictionInstance);
1034  break;
1035  case "reduceaffliction":
1036  if (subElement.GetAttribute("name") != null)
1037  {
1038  DebugConsole.ThrowError("Error in StatusEffect (" + parentDebugName + ") - define afflictions using identifiers or types instead of names.", contentPackage: element.ContentPackage);
1039  ReduceAffliction.Add((
1040  subElement.GetAttributeIdentifier("name", ""),
1041  subElement.GetAttributeFloat(1.0f, "amount", "strength", "reduceamount")));
1042  }
1043  else
1044  {
1045  Identifier name = subElement.GetAttributeIdentifier("identifier", subElement.GetAttributeIdentifier("type", Identifier.Empty));
1046 
1047  if (AfflictionPrefab.List.Any(ap => ap.Identifier == name || ap.AfflictionType == name))
1048  {
1049  ReduceAffliction.Add((name, subElement.GetAttributeFloat(1.0f, "amount", "strength", "reduceamount")));
1050  }
1051  else
1052  {
1053  DebugConsole.ThrowError("Error in StatusEffect (" + parentDebugName + ") - Affliction prefab with the identifier or type \"" + name + "\" not found.", contentPackage: element.ContentPackage);
1054  }
1055  }
1056  break;
1057  case "spawnitem":
1058  var newSpawnItem = new ItemSpawnInfo(subElement, parentDebugName);
1059  if (newSpawnItem.ItemPrefab != null)
1060  {
1061  spawnItems ??= new List<ItemSpawnInfo>();
1062  spawnItems.Add(newSpawnItem);
1063  }
1064  break;
1065  case "triggerevent":
1066  triggeredEvents ??= new List<EventPrefab>();
1067  Identifier identifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty);
1068  if (!identifier.IsEmpty)
1069  {
1070  EventPrefab prefab = EventSet.GetEventPrefab(identifier);
1071  if (prefab != null)
1072  {
1073  triggeredEvents.Add(prefab);
1074  }
1075  }
1076  foreach (var eventElement in subElement.Elements())
1077  {
1078  if (eventElement.NameAsIdentifier() != "ScriptedEvent") { continue; }
1079  triggeredEvents.Add(new EventPrefab(eventElement, file: null));
1080  }
1081  break;
1082  case "spawncharacter":
1083  var newSpawnCharacter = new CharacterSpawnInfo(subElement, parentDebugName);
1084  if (!newSpawnCharacter.SpeciesName.IsEmpty)
1085  {
1086  spawnCharacters ??= new List<CharacterSpawnInfo>();
1087  spawnCharacters.Add(newSpawnCharacter);
1088  }
1089  break;
1090  case "givetalentinfo":
1091  var newGiveTalentInfo = new GiveTalentInfo(subElement, parentDebugName);
1092  if (newGiveTalentInfo.TalentIdentifiers.Any())
1093  {
1094  giveTalentInfos ??= new List<GiveTalentInfo>();
1095  giveTalentInfos.Add(newGiveTalentInfo);
1096  }
1097  break;
1098  case "aitrigger":
1099  aiTriggers ??= new List<AITrigger>();
1100  aiTriggers.Add(new AITrigger(subElement));
1101  break;
1102  case "talenttrigger":
1103  talentTriggers ??= new List<Identifier>();
1104  talentTriggers.Add(subElement.GetAttributeIdentifier("effectidentifier", Identifier.Empty));
1105  break;
1106  case "eventtarget":
1107  eventTargetTags ??= new List<(Identifier eventIdentifier, Identifier tag)>();
1108  eventTargetTags.Add(
1109  (subElement.GetAttributeIdentifier("eventidentifier", Identifier.Empty),
1110  subElement.GetAttributeIdentifier("tag", Identifier.Empty)));
1111  break;
1112  case "giveexperience":
1113  giveExperiences ??= new List<int>();
1114  giveExperiences.Add(subElement.GetAttributeInt("amount", 0));
1115  break;
1116  case "giveskill":
1117  giveSkills ??= new List<GiveSkill>();
1118  giveSkills.Add(new GiveSkill(subElement, parentDebugName));
1119  break;
1120  case "luahook":
1121  case "hook":
1122  luaHook ??= new List<(string, ContentXElement)>();
1123  luaHook.Add((subElement.GetAttributeString("name", ""), subElement));
1124  break;
1125  case "triggeranimation":
1126  AnimationType animType = subElement.GetAttributeEnum("type", def: AnimationType.NotDefined);
1127  string fileName = subElement.GetAttributeString("filename", def: null) ?? subElement.GetAttributeString("file", def: null);
1128  Either<string, ContentPath> file = fileName != null ? fileName.ToLowerInvariant() : subElement.GetAttributeContentPath("path");
1129  if (!file.TryGet(out string _))
1130  {
1131  if (!file.TryGet(out ContentPath _) || (file.TryGet(out ContentPath contentPath) && contentPath.IsNullOrWhiteSpace()))
1132  {
1133  DebugConsole.ThrowError($"Error in a <TriggerAnimation> element of {subElement.ParseContentPathFromUri()}: neither path nor filename defined!",
1134  contentPackage: subElement.ContentPackage);
1135  break;
1136  }
1137  }
1138  float priority = subElement.GetAttributeFloat("priority", def: 0f);
1139  Identifier[] expectedSpeciesNames = subElement.GetAttributeIdentifierArray("expectedspecies", Array.Empty<Identifier>());
1140  animationsToTrigger ??= new List<AnimLoadInfo>();
1141  animationsToTrigger.Add(new AnimLoadInfo(animType, file, priority, expectedSpeciesNames.ToImmutableArray()));
1142 
1143  break;
1144  }
1145  }
1146  InitProjSpecific(element, parentDebugName);
1147  }
1148 
1149  partial void InitProjSpecific(ContentXElement element, string parentDebugName);
1150 
1151  public bool HasTargetType(TargetType targetType)
1152  {
1153  return (targetTypes & targetType) != 0;
1154  }
1155 
1156  public bool ReducesItemCondition()
1157  {
1158  foreach (var (propertyName, value) in PropertyEffects)
1159  {
1160  if (propertyName == "condition" && value.GetType() == typeof(float))
1161  {
1162  return (float)value < 0.0f || (setValue && (float)value <= 0.0f);
1163  }
1164  }
1165  return false;
1166  }
1167 
1169  {
1170  foreach (var (propertyName, value) in PropertyEffects)
1171  {
1172  if (propertyName == "condition" && value.GetType() == typeof(float))
1173  {
1174  return (float)value > 0.0f || (setValue && (float)value > 0.0f);
1175  }
1176  }
1177  return false;
1178  }
1179 
1180  public bool MatchesTagConditionals(ItemPrefab itemPrefab)
1181  {
1182  if (itemPrefab == null || !HasConditions)
1183  {
1184  return false;
1185  }
1186  else
1187  {
1188  return itemPrefab.Tags.Any(t => propertyConditionals.Any(pc => pc.TargetTagMatchesTagCondition(t)));
1189  }
1190  }
1191 
1192  public bool HasRequiredAfflictions(AttackResult attackResult)
1193  {
1194  if (requiredAfflictions == null) { return true; }
1195  if (attackResult.Afflictions == null) { return false; }
1196  if (attackResult.Afflictions.None(a => requiredAfflictions.Any(a2 => a.Strength >= a2.strength && (a.Identifier == a2.affliction || a.Prefab.AfflictionType == a2.affliction))))
1197  {
1198  return false;
1199  }
1200  return true;
1201  }
1202 
1203  public virtual bool HasRequiredItems(Entity entity)
1204  {
1205  if (entity == null || requiredItems == null) { return true; }
1206  foreach (RelatedItem requiredItem in requiredItems)
1207  {
1208  if (entity is Item item)
1209  {
1210  if (!requiredItem.CheckRequirements(null, item)) { return false; }
1211  }
1212  else if (entity is Character character)
1213  {
1214  if (!requiredItem.CheckRequirements(character, null)) { return false; }
1215  }
1216  }
1217  return true;
1218  }
1219 
1220  public void AddNearbyTargets(Vector2 worldPosition, List<ISerializableEntity> targets)
1221  {
1222  if (Range <= 0.0f) { return; }
1223  if (HasTargetType(TargetType.NearbyCharacters))
1224  {
1225  foreach (Character c in Character.CharacterList)
1226  {
1227  if (c.Enabled && !c.Removed && CheckDistance(c) && IsValidTarget(c))
1228  {
1229  targets.Add(c);
1230  }
1231  }
1232  }
1233  if (HasTargetType(TargetType.NearbyItems))
1234  {
1235  //optimization for powered components that can be easily fetched from Powered.PoweredList
1236  if (TargetIdentifiers != null &&
1237  TargetIdentifiers.Count == 1 &&
1238  (TargetIdentifiers.Contains("powered") || TargetIdentifiers.Contains("junctionbox") || TargetIdentifiers.Contains("relaycomponent")))
1239  {
1240  foreach (Powered powered in Powered.PoweredList)
1241  {
1242  //make sure we didn't already add this item due to it having some other Powered component
1243  if (targets.Contains(powered)) { continue; }
1244  Item item = powered.Item;
1245  if (!item.Removed && CheckDistance(item) && IsValidTarget(item))
1246  {
1247  targets.AddRange(item.AllPropertyObjects);
1248  }
1249  }
1250  }
1251  else
1252  {
1253  foreach (Item item in Item.ItemList)
1254  {
1255  if (!item.Removed && CheckDistance(item) && IsValidTarget(item))
1256  {
1257  targets.AddRange(item.AllPropertyObjects);
1258  }
1259  }
1260  }
1261  }
1262 
1263  bool CheckDistance(ISpatialEntity e)
1264  {
1265  float xDiff = Math.Abs(e.WorldPosition.X - worldPosition.X);
1266  if (xDiff > Range) { return false; }
1267  float yDiff = Math.Abs(e.WorldPosition.Y - worldPosition.Y);
1268  if (yDiff > Range) { return false; }
1269  if (xDiff * xDiff + yDiff * yDiff < Range * Range)
1270  {
1271  return true;
1272  }
1273  return false;
1274  }
1275  }
1276 
1277  public bool HasRequiredConditions(IReadOnlyList<ISerializableEntity> targets)
1278  {
1279  return HasRequiredConditions(targets, propertyConditionals);
1280  }
1281 
1282  private delegate bool ShouldShortCircuit(bool condition, out bool valueToReturn);
1283 
1287  private static bool ShouldShortCircuitLogicalOrOperator(bool condition, out bool valueToReturn)
1288  {
1289  valueToReturn = true;
1290  return condition;
1291  }
1292 
1296  private static bool ShouldShortCircuitLogicalAndOperator(bool condition, out bool valueToReturn)
1297  {
1298  valueToReturn = false;
1299  return !condition;
1300  }
1301 
1302  private bool HasRequiredConditions(IReadOnlyList<ISerializableEntity> targets, IReadOnlyList<PropertyConditional> conditionals, bool targetingContainer = false)
1303  {
1304  if (conditionals.Count == 0) { return true; }
1305  if (targets.Count == 0 && requiredItems != null && requiredItems.All(ri => ri.MatchOnEmpty)) { return true; }
1306 
1307  (ShouldShortCircuit, bool) shortCircuitMethodPair = conditionalLogicalOperator switch
1308  {
1309  PropertyConditional.LogicalOperatorType.Or => (ShouldShortCircuitLogicalOrOperator, false),
1310  PropertyConditional.LogicalOperatorType.And => (ShouldShortCircuitLogicalAndOperator, true),
1311  _ => throw new NotImplementedException()
1312  };
1313  var (shouldShortCircuit, didNotShortCircuit) = shortCircuitMethodPair;
1314 
1315  for (int i = 0; i < conditionals.Count; i++)
1316  {
1317  bool valueToReturn;
1318 
1319  var pc = conditionals[i];
1320  if (!pc.TargetContainer || targetingContainer)
1321  {
1322  if (shouldShortCircuit(AnyTargetMatches(targets, pc.TargetItemComponent, pc), out valueToReturn)) { return valueToReturn; }
1323  continue;
1324  }
1325 
1326  var target = FindTargetItemOrComponent(targets);
1327  var targetItem = target as Item ?? (target as ItemComponent)?.Item;
1328  if (targetItem?.ParentInventory == null)
1329  {
1330  //if we're checking for inequality, not being inside a valid container counts as success
1331  //(not inside a container = the container doesn't have a specific tag/value)
1332  bool comparisonIsNeq = pc.ComparisonOperator == PropertyConditional.ComparisonOperatorType.NotEquals;
1333  if (shouldShortCircuit(comparisonIsNeq, out valueToReturn))
1334  {
1335  return valueToReturn;
1336  }
1337  continue;
1338  }
1339  var owner = targetItem.ParentInventory.Owner;
1340  if (pc.TargetGrandParent && owner is Item ownerItem)
1341  {
1342  owner = ownerItem.ParentInventory?.Owner;
1343  }
1344  if (owner is Item container)
1345  {
1346  if (pc.Type == PropertyConditional.ConditionType.HasTag)
1347  {
1348  //if we're checking for tags, just check the Item object, not the ItemComponents
1349  if (shouldShortCircuit(pc.Matches(container), out valueToReturn)) { return valueToReturn; }
1350  }
1351  else
1352  {
1353  if (shouldShortCircuit(AnyTargetMatches(container.AllPropertyObjects, pc.TargetItemComponent, pc), out valueToReturn)) { return valueToReturn; }
1354  }
1355  }
1356  if (owner is Character character && shouldShortCircuit(pc.Matches(character), out valueToReturn)) { return valueToReturn; }
1357  }
1358  return didNotShortCircuit;
1359 
1360  static bool AnyTargetMatches(IReadOnlyList<ISerializableEntity> targets, string targetItemComponentName, PropertyConditional conditional)
1361  {
1362  for (int i = 0; i < targets.Count; i++)
1363  {
1364  if (!string.IsNullOrEmpty(targetItemComponentName))
1365  {
1366  if (!(targets[i] is ItemComponent ic) || ic.Name != targetItemComponentName) { continue; }
1367  }
1368  if (conditional.Matches(targets[i]))
1369  {
1370  return true;
1371  }
1372  }
1373  return false;
1374  }
1375 
1376  static ISerializableEntity FindTargetItemOrComponent(IReadOnlyList<ISerializableEntity> targets)
1377  {
1378  for (int i = 0; i < targets.Count; i++)
1379  {
1380  if (targets[i] is Item || targets[i] is ItemComponent) { return targets[i]; }
1381  }
1382  return null;
1383  }
1384  }
1385 
1386  protected bool IsValidTarget(ISerializableEntity entity)
1387  {
1388  if (entity is Item item)
1389  {
1390  return IsValidTarget(item);
1391  }
1392  else if (entity is ItemComponent itemComponent)
1393  {
1394  return IsValidTarget(itemComponent);
1395  }
1396  else if (entity is Structure structure)
1397  {
1398  if (TargetIdentifiers == null) { return true; }
1399  if (TargetIdentifiers.Contains("structure")) { return true; }
1400  if (TargetIdentifiers.Contains(structure.Prefab.Identifier)) { return true; }
1401  }
1402  else if (entity is Character character)
1403  {
1404  return IsValidTarget(character);
1405  }
1406  if (TargetIdentifiers == null) { return true; }
1407  return TargetIdentifiers.Contains(entity.Name);
1408  }
1409 
1410  protected bool IsValidTarget(ItemComponent itemComponent)
1411  {
1412  if (OnlyInside && itemComponent.Item.CurrentHull == null) { return false; }
1413  if (OnlyOutside && itemComponent.Item.CurrentHull != null) { return false; }
1414  if (!TargetItemComponent.IsNullOrEmpty() && itemComponent.Name != TargetItemComponent) { return false; }
1415  if (TargetIdentifiers == null) { return true; }
1416  if (TargetIdentifiers.Contains("itemcomponent")) { return true; }
1417  if (itemComponent.Item.HasTag(TargetIdentifiers)) { return true; }
1418  return TargetIdentifiers.Contains(itemComponent.Item.Prefab.Identifier);
1419  }
1420 
1421  protected bool IsValidTarget(Item item)
1422  {
1423  if (OnlyInside && item.CurrentHull == null) { return false; }
1424  if (OnlyOutside && item.CurrentHull != null) { return false; }
1425  if (TargetIdentifiers == null) { return true; }
1426  if (TargetIdentifiers.Contains("item")) { return true; }
1427  if (item.HasTag(TargetIdentifiers)) { return true; }
1428  return TargetIdentifiers.Contains(item.Prefab.Identifier);
1429  }
1430 
1431  protected bool IsValidTarget(Character character)
1432  {
1433  if (OnlyInside && character.CurrentHull == null) { return false; }
1434  if (OnlyOutside && character.CurrentHull != null) { return false; }
1435  if (TargetIdentifiers == null) { return true; }
1436  if (TargetIdentifiers.Contains("character")) { return true; }
1437  if (TargetIdentifiers.Contains("monster"))
1438  {
1439  return !character.IsHuman && character.Group != CharacterPrefab.HumanSpeciesName;
1440  }
1441  return TargetIdentifiers.Contains(character.SpeciesName);
1442  }
1443 
1444  public void SetUser(Character user)
1445  {
1446  this.user = user;
1447  foreach (Affliction affliction in Afflictions)
1448  {
1449  affliction.Source = user;
1450  }
1451  }
1452 
1453  private static readonly List<Entity> intervalsToRemove = new List<Entity>();
1454 
1455  public bool ShouldWaitForInterval(Entity entity, float deltaTime)
1456  {
1457  if (Interval > 0.0f && entity != null && intervalTimers != null)
1458  {
1459  if (intervalTimers.ContainsKey(entity))
1460  {
1461  intervalTimers[entity] -= deltaTime;
1462  if (intervalTimers[entity] > 0.0f) { return true; }
1463  }
1464  intervalsToRemove.Clear();
1465  intervalsToRemove.AddRange(intervalTimers.Keys.Where(e => e.Removed));
1466  foreach (var toRemove in intervalsToRemove)
1467  {
1468  intervalTimers.Remove(toRemove);
1469  }
1470  }
1471  return false;
1472  }
1473 
1474  public virtual void Apply(ActionType type, float deltaTime, Entity entity, ISerializableEntity target, Vector2? worldPosition = null)
1475  {
1476  if (Disabled) { return; }
1477  if (this.type != type || !HasRequiredItems(entity)) { return; }
1478 
1479  if (!IsValidTarget(target)) { return; }
1480 
1481  if (Duration > 0.0f && !Stackable)
1482  {
1483  //ignore if not stackable and there's already an identical statuseffect
1484  DurationListElement existingEffect = DurationList.Find(d => d.Parent == this && d.Targets.FirstOrDefault() == target);
1485  if (existingEffect != null)
1486  {
1487  existingEffect.Reset(Math.Max(existingEffect.Timer, Duration), user);
1488  return;
1489  }
1490  }
1491 
1492  currentTargets.Clear();
1493  currentTargets.Add(target);
1494  if (!HasRequiredConditions(currentTargets)) { return; }
1495  Apply(deltaTime, entity, currentTargets, worldPosition);
1496  }
1497 
1498  protected readonly List<ISerializableEntity> currentTargets = new List<ISerializableEntity>();
1499  public virtual void Apply(ActionType type, float deltaTime, Entity entity, IReadOnlyList<ISerializableEntity> targets, Vector2? worldPosition = null)
1500  {
1501  if (Disabled) { return; }
1502  if (this.type != type) { return; }
1503  if (ShouldWaitForInterval(entity, deltaTime)) { return; }
1504 
1505  currentTargets.Clear();
1506  foreach (ISerializableEntity target in targets)
1507  {
1508  if (!IsValidTarget(target)) { continue; }
1509  currentTargets.Add(target);
1510  }
1511 
1512  if (TargetIdentifiers != null && currentTargets.Count == 0) { return; }
1513 
1514  bool hasRequiredItems = HasRequiredItems(entity);
1515  if (!hasRequiredItems || !HasRequiredConditions(currentTargets))
1516  {
1517 #if CLIENT
1518  if (!hasRequiredItems && playSoundOnRequiredItemFailure)
1519  {
1520  PlaySound(entity, GetHull(entity), GetPosition(entity, targets, worldPosition));
1521  }
1522 #endif
1523  return;
1524  }
1525 
1526  if (Duration > 0.0f && !Stackable)
1527  {
1528  //ignore if not stackable and there's already an identical statuseffect
1529  DurationListElement existingEffect = DurationList.Find(d => d.Parent == this && d.Targets.SequenceEqual(currentTargets));
1530  if (existingEffect != null)
1531  {
1532  existingEffect?.Reset(Math.Max(existingEffect.Timer, Duration), user);
1533  return;
1534  }
1535  }
1536 
1537  Apply(deltaTime, entity, currentTargets, worldPosition);
1538  }
1539 
1540  private Hull GetHull(Entity entity)
1541  {
1542  Hull hull = null;
1543  if (entity is Character character)
1544  {
1545  hull = character.AnimController.CurrentHull;
1546  }
1547  else if (entity is Item item)
1548  {
1549  hull = item.CurrentHull;
1550  }
1551  return hull;
1552  }
1553 
1554  private Vector2 GetPosition(Entity entity, IReadOnlyList<ISerializableEntity> targets, Vector2? worldPosition = null)
1555  {
1556  Vector2 position = worldPosition ?? (entity == null || entity.Removed ? Vector2.Zero : entity.WorldPosition);
1557  if (worldPosition == null)
1558  {
1559  if (entity is Character character && !character.Removed && targetLimbs != null)
1560  {
1561  foreach (var targetLimbType in targetLimbs)
1562  {
1563  Limb limb = character.AnimController.GetLimb(targetLimbType);
1564  if (limb != null && !limb.Removed)
1565  {
1566  position = limb.WorldPosition;
1567  break;
1568  }
1569  }
1570  }
1571  else if (HasTargetType(TargetType.Contained))
1572  {
1573  for (int i = 0; i < targets.Count; i++)
1574  {
1575  if (targets[i] is Item targetItem)
1576  {
1577  position = targetItem.WorldPosition;
1578  break;
1579  }
1580  }
1581  }
1582  else
1583  {
1584  for (int i = 0; i < targets.Count; i++)
1585  {
1586  if (targets[i] is Limb targetLimb && !targetLimb.Removed)
1587  {
1588  position = targetLimb.WorldPosition;
1589  break;
1590  }
1591  }
1592  }
1593 
1594  }
1595  position += Offset;
1596  return position;
1597  }
1598 
1599  protected void Apply(float deltaTime, Entity entity, IReadOnlyList<ISerializableEntity> targets, Vector2? worldPosition = null)
1600  {
1601  if (Disabled) { return; }
1602  if (lifeTime > 0)
1603  {
1604  lifeTimer -= deltaTime;
1605  if (lifeTimer <= 0) { return; }
1606  }
1607  if (ShouldWaitForInterval(entity, deltaTime)) { return; }
1608 
1609  {
1610  if (entity is Item item)
1611  {
1612  var result = GameMain.LuaCs.Hook.Call<bool?>("statusEffect.apply." + item.Prefab.Identifier, this, deltaTime, entity, targets, worldPosition);
1613 
1614  if (result != null && result.Value) { return; }
1615  }
1616 
1617  if (entity is Character character)
1618  {
1619  var result = GameMain.LuaCs.Hook.Call<bool?>("statusEffect.apply." + character.SpeciesName, this, deltaTime, entity, targets, worldPosition);
1620 
1621  if (result != null && result.Value) { return; }
1622  }
1623  }
1624 
1625  if (luaHook != null)
1626  {
1627  foreach ((string hookName, ContentXElement element) in luaHook)
1628  {
1629  var result = GameMain.LuaCs.Hook.Call<bool?>(hookName, this, deltaTime, entity, targets, worldPosition, element);
1630 
1631  if (result != null && result.Value) { return; }
1632  }
1633  }
1634 
1635  Item parentItem = entity as Item;
1636  PhysicsBody parentItemBody = parentItem?.body;
1637  Hull hull = GetHull(entity);
1638  Vector2 position = GetPosition(entity, targets, worldPosition);
1639  if (useItemCount > 0)
1640  {
1641  Character useTargetCharacter = null;
1642  Limb useTargetLimb = null;
1643  for (int i = 0; i < targets.Count; i++)
1644  {
1645  if (targets[i] is Character character && !character.Removed)
1646  {
1647  useTargetCharacter = character;
1648  break;
1649  }
1650  else if (targets[i] is Limb limb && limb.character != null && !limb.character.Removed)
1651  {
1652  useTargetLimb = limb;
1653  useTargetCharacter ??= limb.character;
1654  break;
1655  }
1656  }
1657  for (int i = 0; i < targets.Count; i++)
1658  {
1659  if (targets[i] is not Item item) { continue; }
1660  for (int j = 0; j < useItemCount; j++)
1661  {
1662  if (item.Removed) { continue; }
1663  item.Use(deltaTime, user: null, useTargetLimb, useTargetCharacter);
1664  }
1665  }
1666  }
1667 
1668  if (dropItem)
1669  {
1670  for (int i = 0; i < targets.Count; i++)
1671  {
1672  if (targets[i] is Item item)
1673  {
1674  item.Drop(dropper: null);
1675  }
1676  }
1677  }
1678  if (dropContainedItems)
1679  {
1680  for (int i = 0; i < targets.Count; i++)
1681  {
1682  if (targets[i] is Item item)
1683  {
1684  foreach (var itemContainer in item.GetComponents<ItemContainer>())
1685  {
1686  foreach (var containedItem in itemContainer.Inventory.AllItemsMod)
1687  {
1688  containedItem.Drop(dropper: null);
1689  }
1690  }
1691  }
1692  else if (targets[i] is Character character && character.Inventory != null)
1693  {
1694  foreach (var containedItem in character.Inventory.AllItemsMod)
1695  {
1696  containedItem.Drop(dropper: null);
1697  }
1698  }
1699  }
1700  }
1701  if (removeItem)
1702  {
1703  for (int i = 0; i < targets.Count; i++)
1704  {
1705  if (targets[i] is Item item) { Entity.Spawner?.AddItemToRemoveQueue(item); }
1706  }
1707  }
1708  if (removeCharacter)
1709  {
1710  for (int i = 0; i < targets.Count; i++)
1711  {
1712  var target = targets[i];
1713  if (target is Character character)
1714  {
1716  }
1717  else if (target is Limb limb)
1718  {
1719  Entity.Spawner?.AddEntityToRemoveQueue(limb.character);
1720  }
1721  }
1722  }
1723  if (breakLimb || hideLimb)
1724  {
1725  for (int i = 0; i < targets.Count; i++)
1726  {
1727  var target = targets[i];
1728  Limb targetLimb = target as Limb;
1729  if (targetLimb == null && target is Character character)
1730  {
1731  foreach (Limb limb in character.AnimController.Limbs)
1732  {
1733  if (limb.body == sourceBody)
1734  {
1735  targetLimb = limb;
1736  break;
1737  }
1738  }
1739  }
1740  if (targetLimb != null)
1741  {
1742  if (breakLimb)
1743  {
1744  targetLimb.character.TrySeverLimbJoints(targetLimb, severLimbsProbability: 1, damage: -1, allowBeheading: true, ignoreSeveranceProbabilityModifier: true, attacker: user);
1745  }
1746  if (hideLimb)
1747  {
1748  targetLimb.HideAndDisable(hideLimbTimer);
1749  }
1750  }
1751  }
1752  }
1753 
1754  if (Duration > 0.0f)
1755  {
1756  DurationList.Add(new DurationListElement(this, entity, targets, Duration, user));
1757  }
1758  else
1759  {
1760  for (int i = 0; i < targets.Count; i++)
1761  {
1762  var target = targets[i];
1763  if (target?.SerializableProperties == null) { continue; }
1764  if (target is Entity targetEntity)
1765  {
1766  if (targetEntity.Removed) { continue; }
1767  }
1768  else if (target is Limb limb)
1769  {
1770  if (limb.Removed) { continue; }
1771  position = limb.WorldPosition + Offset;
1772  }
1773  foreach (var (propertyName, value) in PropertyEffects)
1774  {
1775  if (!target.SerializableProperties.TryGetValue(propertyName, out SerializableProperty property))
1776  {
1777  continue;
1778  }
1779  ApplyToProperty(target, property, value, deltaTime);
1780  }
1781  }
1782  }
1783 
1784  if (explosions != null)
1785  {
1786  foreach (Explosion explosion in explosions)
1787  {
1788  explosion.Explode(position, damageSource: entity, attacker: user);
1789  }
1790  }
1791 
1792  bool isNotClient = GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient;
1793 
1794  for (int i = 0; i < targets.Count; i++)
1795  {
1796  var target = targets[i];
1797  //if the effect has a duration, these will be done in the UpdateAll method
1798  if (Duration > 0) { break; }
1799  if (target == null) { continue; }
1800  foreach (Affliction affliction in Afflictions)
1801  {
1802  Affliction newAffliction = affliction;
1803  if (target is Character character)
1804  {
1805  if (character.Removed) { continue; }
1806  newAffliction = GetMultipliedAffliction(affliction, entity, character, deltaTime, multiplyAfflictionsByMaxVitality);
1807  character.LastDamageSource = entity;
1808  foreach (Limb limb in character.AnimController.Limbs)
1809  {
1810  if (limb.Removed) { continue; }
1811  if (limb.IsSevered) { continue; }
1812  if (targetLimbs != null && !targetLimbs.Contains(limb.type)) { continue; }
1813  AttackResult result = limb.character.DamageLimb(position, limb, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: Vector2.Zero, attacker: affliction.Source, allowStacking: !setValue);
1814  limb.character.TrySeverLimbJoints(limb, SeverLimbsProbability, disableDeltaTime ? result.Damage : result.Damage / deltaTime, allowBeheading: true, attacker: affliction.Source);
1815  RegisterTreatmentResults(user, entity as Item, limb, affliction, result);
1816  //only apply non-limb-specific afflictions to the first limb
1817  if (!affliction.Prefab.LimbSpecific) { break; }
1818  }
1819  }
1820  else if (target is Limb limb)
1821  {
1822  if (limb.IsSevered) { continue; }
1823  if (limb.character.Removed || limb.Removed) { continue; }
1824  if (targetLimbs != null && !targetLimbs.Contains(limb.type)) { continue; }
1825  newAffliction = GetMultipliedAffliction(affliction, entity, limb.character, deltaTime, multiplyAfflictionsByMaxVitality);
1826  AttackResult result = limb.character.DamageLimb(position, limb, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: Vector2.Zero, attacker: affliction.Source, allowStacking: !setValue);
1827  limb.character.TrySeverLimbJoints(limb, SeverLimbsProbability, disableDeltaTime ? result.Damage : result.Damage / deltaTime, allowBeheading: true, attacker: affliction.Source);
1828  RegisterTreatmentResults(user, entity as Item, limb, affliction, result);
1829  }
1830  }
1831 
1832  foreach ((Identifier affliction, float amount) in ReduceAffliction)
1833  {
1834  Limb targetLimb = null;
1835  Character targetCharacter = null;
1836  if (target is Character character)
1837  {
1838  targetCharacter = character;
1839  }
1840  else if (target is Limb limb && !limb.Removed)
1841  {
1842  targetLimb = limb;
1843  targetCharacter = limb.character;
1844  }
1845  if (targetCharacter != null && !targetCharacter.Removed)
1846  {
1847  ActionType? actionType = null;
1848  if (entity is Item item && item.UseInHealthInterface) { actionType = type; }
1849  float reduceAmount = amount * GetAfflictionMultiplier(entity, targetCharacter, deltaTime);
1850  float prevVitality = targetCharacter.Vitality;
1851  if (targetLimb != null)
1852  {
1853  targetCharacter.CharacterHealth.ReduceAfflictionOnLimb(targetLimb, affliction, reduceAmount, attacker: user, treatmentAction: actionType);
1854  }
1855  else
1856  {
1857  targetCharacter.CharacterHealth.ReduceAfflictionOnAllLimbs(affliction, reduceAmount, attacker: user, treatmentAction: actionType);
1858  }
1859  if (!targetCharacter.IsDead)
1860  {
1861  float healthChange = targetCharacter.Vitality - prevVitality;
1862  targetCharacter.AIController?.OnHealed(healer: user, healthChange);
1863  if (user != null)
1864  {
1865  targetCharacter.TryAdjustHealerSkill(user, healthChange);
1866 #if SERVER
1867  GameMain.Server.KarmaManager.OnCharacterHealthChanged(targetCharacter, user, -healthChange, 0.0f);
1868 #endif
1869  }
1870  }
1871  }
1872  }
1873 
1874  if (aiTriggers != null)
1875  {
1876  Character targetCharacter = target as Character;
1877  if (targetCharacter == null)
1878  {
1879  if (target is Limb targetLimb && !targetLimb.Removed)
1880  {
1881  targetCharacter = targetLimb.character;
1882  }
1883  }
1884 
1885  Character entityCharacter = entity as Character;
1886  targetCharacter ??= entityCharacter;
1887  if (targetCharacter != null && !targetCharacter.Removed && !targetCharacter.IsPlayer)
1888  {
1889  if (targetCharacter.AIController is EnemyAIController enemyAI)
1890  {
1891  foreach (AITrigger trigger in aiTriggers)
1892  {
1893  if (Rand.Value(Rand.RandSync.Unsynced) > trigger.Probability) { continue; }
1894  if (entityCharacter != targetCharacter)
1895  {
1896  if (target is Limb targetLimb && targetCharacter.LastDamage.HitLimb is Limb hitLimb)
1897  {
1898  if (hitLimb != targetLimb) { continue; }
1899  }
1900  }
1901  if (targetCharacter.LastDamage.Damage < trigger.MinDamage) { continue; }
1902  enemyAI.LaunchTrigger(trigger);
1903  break;
1904  }
1905  }
1906  }
1907  }
1908 
1909  if (talentTriggers != null)
1910  {
1911  Character targetCharacter = CharacterFromTarget(target);
1912  if (targetCharacter != null && !targetCharacter.Removed)
1913  {
1914  foreach (Identifier talentTrigger in talentTriggers)
1915  {
1916  targetCharacter.CheckTalents(AbilityEffectType.OnStatusEffectIdentifier, new AbilityStatusEffectIdentifier(talentTrigger));
1917  }
1918  }
1919  }
1920 
1921  TryTriggerAnimation(target, entity);
1922 
1923  if (isNotClient)
1924  {
1925  // these effects do not need to be run clientside, as they are replicated from server to clients anyway
1926  if (giveExperiences != null)
1927  {
1928  foreach (int giveExperience in giveExperiences)
1929  {
1930  Character targetCharacter = CharacterFromTarget(target);
1931  if (targetCharacter != null && !targetCharacter.Removed)
1932  {
1933  targetCharacter?.Info?.GiveExperience(giveExperience);
1934  }
1935  }
1936  }
1937 
1938  if (giveSkills != null)
1939  {
1940  Character targetCharacter = CharacterFromTarget(target);
1941  if (targetCharacter is { Removed: false })
1942  {
1943  foreach (GiveSkill giveSkill in giveSkills)
1944  {
1945  Identifier skillIdentifier = giveSkill.SkillIdentifier == "randomskill" ? GetRandomSkill() : giveSkill.SkillIdentifier;
1946  float amount = giveSkill.UseDeltaTime ? giveSkill.Amount * deltaTime : giveSkill.Amount;
1947 
1948  if (giveSkill.Proportional)
1949  {
1950  targetCharacter.Info?.ApplySkillGain(skillIdentifier, amount, !giveSkill.TriggerTalents);
1951  }
1952  else
1953  {
1954  targetCharacter.Info?.IncreaseSkillLevel(skillIdentifier, amount, !giveSkill.TriggerTalents);
1955  }
1956 
1957  Identifier GetRandomSkill()
1958  {
1959  return targetCharacter.Info?.Job?.GetSkills().GetRandomUnsynced()?.Identifier ?? Identifier.Empty;
1960  }
1961  }
1962  }
1963  }
1964 
1965  if (giveTalentInfos != null)
1966  {
1967  Character targetCharacter = CharacterFromTarget(target);
1968  if (targetCharacter?.Info == null) { continue; }
1969  if (!TalentTree.JobTalentTrees.TryGet(targetCharacter.Info.Job.Prefab.Identifier, out TalentTree characterTalentTree)) { continue; }
1970 
1971  foreach (GiveTalentInfo giveTalentInfo in giveTalentInfos)
1972  {
1973  if (giveTalentInfo.GiveRandom)
1974  {
1975  // for the sake of technical simplicity, for now do not allow talents to be given if the character could unlock them in their talent tree as well
1976  IEnumerable<Identifier> viableTalents = giveTalentInfo.TalentIdentifiers.Where(id => !targetCharacter.Info.UnlockedTalents.Contains(id) && !characterTalentTree.AllTalentIdentifiers.Contains(id));
1977  if (viableTalents.None()) { continue; }
1978  targetCharacter.GiveTalent(viableTalents.GetRandomUnsynced(), true);
1979  }
1980  else
1981  {
1982  foreach (Identifier id in giveTalentInfo.TalentIdentifiers)
1983  {
1984  if (targetCharacter.Info.UnlockedTalents.Contains(id) || characterTalentTree.AllTalentIdentifiers.Contains(id)) { continue; }
1985  targetCharacter.GiveTalent(id, true);
1986  }
1987  }
1988  }
1989  }
1990 
1991  if (eventTargetTags != null)
1992  {
1993  foreach ((Identifier eventId, Identifier tag) in eventTargetTags)
1994  {
1995  if (GameMain.GameSession.EventManager.ActiveEvents.FirstOrDefault(e => e.Prefab.Identifier == eventId) is ScriptedEvent ev)
1996  {
1997  targets.Where(t => t is Entity).ForEach(t => ev.AddTarget(tag, (Entity)t));
1998  }
1999  }
2000  }
2001  }
2002  }
2003 
2004  if (FireSize > 0.0f && entity != null)
2005  {
2006  var fire = new FireSource(position, hull, sourceCharacter: user);
2007  fire.Size = new Vector2(FireSize, fire.Size.Y);
2008  }
2009 
2010  if (isNotClient && triggeredEvents != null && GameMain.GameSession?.EventManager is { } eventManager)
2011  {
2012  foreach (EventPrefab eventPrefab in triggeredEvents)
2013  {
2014  Event ev = eventPrefab.CreateInstance(eventManager.RandomSeed);
2015  if (ev == null) { continue; }
2016  eventManager.QueuedEvents.Enqueue(ev);
2017  if (ev is ScriptedEvent scriptedEvent)
2018  {
2019  if (!triggeredEventTargetTag.IsEmpty)
2020  {
2021  IEnumerable<ISerializableEntity> eventTargets = targets.Where(t => t is Entity);
2022  if (eventTargets.Any())
2023  {
2024  scriptedEvent.Targets.Add(triggeredEventTargetTag, eventTargets.Cast<Entity>().ToList());
2025  }
2026  }
2027  if (!triggeredEventEntityTag.IsEmpty && entity != null)
2028  {
2029  scriptedEvent.Targets.Add(triggeredEventEntityTag, new List<Entity> { entity });
2030  }
2031  if (!triggeredEventUserTag.IsEmpty && user != null)
2032  {
2033  scriptedEvent.Targets.Add(triggeredEventUserTag, new List<Entity> { user });
2034  }
2035  }
2036  }
2037  }
2038 
2039  if (isNotClient && entity != null && Entity.Spawner != null) //clients are not allowed to spawn entities
2040  {
2041  if (spawnCharacters != null)
2042  {
2043  foreach (CharacterSpawnInfo characterSpawnInfo in spawnCharacters)
2044  {
2045  var characters = new List<Character>();
2046  for (int i = 0; i < characterSpawnInfo.Count; i++)
2047  {
2048  Entity.Spawner.AddCharacterToSpawnQueue(characterSpawnInfo.SpeciesName, position + Rand.Vector(characterSpawnInfo.Spread, Rand.RandSync.Unsynced) + characterSpawnInfo.Offset,
2049  onSpawn: newCharacter =>
2050  {
2051  if (characterSpawnInfo.TotalMaxCount > 0)
2052  {
2053  if (Character.CharacterList.Count(c => c.SpeciesName == characterSpawnInfo.SpeciesName && c.TeamID == newCharacter.TeamID) > characterSpawnInfo.TotalMaxCount)
2054  {
2055  Entity.Spawner?.AddEntityToRemoveQueue(newCharacter);
2056  return;
2057  }
2058  }
2059  if (newCharacter.AIController is EnemyAIController enemyAi &&
2060  enemyAi.PetBehavior != null &&
2061  entity is Item item &&
2062  item.ParentInventory is CharacterInventory inv)
2063  {
2064  enemyAi.PetBehavior.Owner = inv.Owner as Character;
2065  }
2066  characters.Add(newCharacter);
2067  if (characters.Count == characterSpawnInfo.Count)
2068  {
2069  SwarmBehavior.CreateSwarm(characters.Cast<AICharacter>());
2070  }
2071  if (!characterSpawnInfo.AfflictionOnSpawn.IsEmpty)
2072  {
2073  if (!AfflictionPrefab.Prefabs.TryGet(characterSpawnInfo.AfflictionOnSpawn, out AfflictionPrefab afflictionPrefab))
2074  {
2075  DebugConsole.NewMessage($"Could not apply an affliction to the spawned character(s). No affliction with the identifier \"{characterSpawnInfo.AfflictionOnSpawn}\" found.", Color.Red);
2076  return;
2077  }
2078  newCharacter.CharacterHealth.ApplyAffliction(newCharacter.AnimController.MainLimb, afflictionPrefab.Instantiate(characterSpawnInfo.AfflictionStrength));
2079  }
2080  if (characterSpawnInfo.Stun > 0)
2081  {
2082  newCharacter.SetStun(characterSpawnInfo.Stun);
2083  }
2084  foreach (var target in targets)
2085  {
2086  if (target is not Character character) { continue; }
2087  if (characterSpawnInfo.TransferInventory && character.Inventory != null && newCharacter.Inventory != null)
2088  {
2089  if (character.Inventory.Capacity != newCharacter.Inventory.Capacity) { return; }
2090  for (int i = 0; i < character.Inventory.Capacity && i < newCharacter.Inventory.Capacity; i++)
2091  {
2092  character.Inventory.GetItemsAt(i).ForEachMod(item => newCharacter.Inventory.TryPutItem(item, i, allowSwapping: true, allowCombine: false, user: null));
2093  }
2094  }
2095  if (characterSpawnInfo.TransferBuffs || characterSpawnInfo.TransferAfflictions)
2096  {
2097  foreach (Affliction affliction in character.CharacterHealth.GetAllAfflictions())
2098  {
2099  if (affliction.Prefab.IsBuff)
2100  {
2101  if (!characterSpawnInfo.TransferBuffs) { continue; }
2102  }
2103  else
2104  {
2105  if (!characterSpawnInfo.TransferAfflictions) { continue; }
2106  }
2107  //ApplyAffliction modified the strength based on max vitality, let's undo that before transferring the affliction
2108  //(otherwise e.g. a character with 1000 vitality would only get a tenth of the strength)
2109  float afflictionStrength = affliction.Strength * (newCharacter.MaxVitality / 100.0f);
2110  newCharacter.CharacterHealth.ApplyAffliction(newCharacter.AnimController.MainLimb, affliction.Prefab.Instantiate(afflictionStrength));
2111  }
2112  }
2113  if (i == characterSpawnInfo.Count) // Only perform the below actions if this is the last character being spawned.
2114  {
2115  if (characterSpawnInfo.TransferControl)
2116  {
2117 #if CLIENT
2118  if (Character.Controlled == target)
2119  {
2120  Character.Controlled = newCharacter;
2121  }
2122 #elif SERVER
2123  foreach (Client c in GameMain.Server.ConnectedClients)
2124  {
2125  if (c.Character != target) { continue; }
2126  GameMain.Server.SetClientCharacter(c, newCharacter);
2127  }
2128 #endif
2129  }
2130  if (characterSpawnInfo.RemovePreviousCharacter) { Entity.Spawner?.AddEntityToRemoveQueue(character); }
2131  }
2132  }
2133  if (characterSpawnInfo.InheritEventTags)
2134  {
2135  foreach (var activeEvent in GameMain.GameSession.EventManager.ActiveEvents)
2136  {
2137  if (activeEvent is ScriptedEvent scriptedEvent)
2138  {
2139  scriptedEvent.InheritTags(entity, newCharacter);
2140  }
2141  }
2142  }
2143  });
2144  }
2145  }
2146  }
2147 
2148  if (spawnItems != null && spawnItems.Count > 0)
2149  {
2150  if (spawnItemRandomly)
2151  {
2152  if (spawnItems.Count > 0)
2153  {
2154  var randomSpawn = spawnItems.GetRandomUnsynced();
2155  for (int i = 0; i < randomSpawn.Count; i++)
2156  {
2157  ProcessItemSpawnInfo(randomSpawn);
2158  }
2159  }
2160  }
2161  else
2162  {
2163  foreach (ItemSpawnInfo itemSpawnInfo in spawnItems)
2164  {
2165  for (int i = 0; i < itemSpawnInfo.Count; i++)
2166  {
2167  ProcessItemSpawnInfo(itemSpawnInfo);
2168  }
2169  }
2170  }
2171 
2172  void ProcessItemSpawnInfo(ItemSpawnInfo spawnInfo)
2173  {
2174  if (spawnInfo.SpawnPosition == ItemSpawnInfo.SpawnPositionType.Target)
2175  {
2176  foreach (var target in targets)
2177  {
2178  if (target is Entity targetEntity)
2179  {
2180  SpawnItem(spawnInfo, entity, sourceBody, position, targetEntity);
2181  }
2182  }
2183  }
2184  else
2185  {
2186  SpawnItem(spawnInfo, entity, sourceBody, position, targetEntity: null);
2187  }
2188  }
2189  }
2190  }
2191 
2192  ApplyProjSpecific(deltaTime, entity, targets, hull, position, playSound: true);
2193 
2194  if (oneShot)
2195  {
2196  Disabled = true;
2197  }
2198  if (Interval > 0.0f && entity != null)
2199  {
2200  intervalTimers ??= new Dictionary<Entity, float>();
2201  intervalTimers[entity] = Interval;
2202  }
2203 
2204  }
2205  private static Character CharacterFromTarget(ISerializableEntity target)
2206  {
2207  Character targetCharacter = target as Character;
2208  if (targetCharacter == null)
2209  {
2210  if (target is Limb targetLimb && !targetLimb.Removed)
2211  {
2212  targetCharacter = targetLimb.character;
2213  }
2214  }
2215  return targetCharacter;
2216  }
2217 
2218  void SpawnItem(ItemSpawnInfo chosenItemSpawnInfo, Entity entity, PhysicsBody sourceBody, Vector2 position, Entity targetEntity)
2219  {
2220  Item parentItem = entity as Item;
2221  PhysicsBody parentItemBody = parentItem?.body;
2222  if (user == null && parentItem != null)
2223  {
2224  // Set the user for projectiles spawned from status effects (e.g. flak shrapnels)
2225  SetUser(parentItem.GetComponent<Projectile>()?.User);
2226  }
2227 
2228  if (chosenItemSpawnInfo.SpawnPosition == ItemSpawnInfo.SpawnPositionType.Target && targetEntity != null)
2229  {
2230  entity = targetEntity;
2231  position = entity.WorldPosition;
2232  if (entity is Item it)
2233  {
2234  sourceBody ??=
2235  (entity as Item)?.body ??
2236  (entity as Character)?.AnimController.Collider;
2237  }
2238  }
2239 
2240  switch (chosenItemSpawnInfo.SpawnPosition)
2241  {
2242  case ItemSpawnInfo.SpawnPositionType.This:
2243  case ItemSpawnInfo.SpawnPositionType.Target:
2244  Entity.Spawner?.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, position + Rand.Vector(chosenItemSpawnInfo.Spread, Rand.RandSync.Unsynced), onSpawned: newItem =>
2245  {
2246  Projectile projectile = newItem.GetComponent<Projectile>();
2247  if (entity != null)
2248  {
2249  var rope = newItem.GetComponent<Rope>();
2250  if (rope != null && sourceBody != null && sourceBody.UserData is Limb sourceLimb)
2251  {
2252  rope.Attach(sourceLimb, newItem);
2253 #if SERVER
2254  newItem.CreateServerEvent(rope);
2255 #endif
2256  }
2257  float spread = Rand.Range(-chosenItemSpawnInfo.AimSpreadRad, chosenItemSpawnInfo.AimSpreadRad);
2258  float rotation = chosenItemSpawnInfo.RotationRad;
2259  Vector2 worldPos;
2260  if (sourceBody != null)
2261  {
2262  worldPos = sourceBody.Position;
2263  if (user?.Submarine != null)
2264  {
2265  worldPos += user.Submarine.Position;
2266  }
2267  }
2268  else
2269  {
2270  worldPos = entity.WorldPosition;
2271  }
2272  switch (chosenItemSpawnInfo.RotationType)
2273  {
2274  case ItemSpawnInfo.SpawnRotationType.None:
2275  rotation = chosenItemSpawnInfo.RotationRad;
2276  break;
2277  case ItemSpawnInfo.SpawnRotationType.This:
2278  if (sourceBody != null)
2279  {
2280  rotation = sourceBody.TransformRotation(chosenItemSpawnInfo.RotationRad);
2281  }
2282  else if (parentItemBody != null)
2283  {
2284  rotation = parentItemBody.TransformRotation(chosenItemSpawnInfo.RotationRad);
2285  }
2286  break;
2287  case ItemSpawnInfo.SpawnRotationType.Target:
2288  rotation = MathUtils.VectorToAngle(entity.WorldPosition - worldPos);
2289  break;
2290  case ItemSpawnInfo.SpawnRotationType.Limb:
2291  if (sourceBody != null)
2292  {
2293  rotation = sourceBody.TransformedRotation;
2294  }
2295  break;
2296  case ItemSpawnInfo.SpawnRotationType.Collider:
2297  if (parentItemBody != null)
2298  {
2299  rotation = parentItemBody.TransformedRotation;
2300  }
2301  else if (user != null)
2302  {
2303  rotation = user.AnimController.Collider.Rotation + MathHelper.PiOver2;
2304  }
2305  break;
2306  case ItemSpawnInfo.SpawnRotationType.MainLimb:
2307  if (user != null)
2308  {
2309  rotation = user.AnimController.MainLimb.body.TransformedRotation;
2310  }
2311  break;
2312  case ItemSpawnInfo.SpawnRotationType.Random:
2313  if (projectile != null)
2314  {
2315  DebugConsole.LogError("Random rotation is not supported for Projectiles.");
2316  }
2317  else
2318  {
2319  rotation = Rand.Range(0f, MathHelper.TwoPi, Rand.RandSync.Unsynced);
2320  }
2321  break;
2322  default:
2323  throw new NotImplementedException("Item spawn rotation type not implemented: " + chosenItemSpawnInfo.RotationType);
2324  }
2325  if (user != null)
2326  {
2327  rotation += chosenItemSpawnInfo.RotationRad * user.AnimController.Dir;
2328  }
2329  rotation += spread;
2330  if (projectile != null)
2331  {
2332  var sourceEntity = (sourceBody?.UserData as ISpatialEntity) ?? entity;
2333  Vector2 spawnPos = sourceEntity.SimPosition;
2334  List<Body> ignoredBodies = null;
2335  if (!projectile.DamageUser)
2336  {
2337  ignoredBodies = user?.AnimController.Limbs.Where(l => !l.IsSevered).Select(l => l.body.FarseerBody).ToList();
2338  }
2339  projectile.Shoot(user, spawnPos, spawnPos, rotation,
2340  ignoredBodies: ignoredBodies, createNetworkEvent: true);
2341  projectile.Item.Submarine = projectile.LaunchSub = sourceEntity?.Submarine;
2342  }
2343  else if (newItem.body != null)
2344  {
2345  newItem.body.SetTransform(newItem.SimPosition, rotation);
2346  Vector2 impulseDir = new Vector2(MathF.Cos(rotation), MathF.Sin(rotation));
2347  newItem.body.ApplyLinearImpulse(impulseDir * chosenItemSpawnInfo.Impulse);
2348  }
2349  }
2350  OnItemSpawned(newItem, chosenItemSpawnInfo);
2351  });
2352  break;
2353  case ItemSpawnInfo.SpawnPositionType.ThisInventory:
2354  {
2355  Inventory inventory = null;
2356  if (entity is Character character && character.Inventory != null)
2357  {
2358  inventory = character.Inventory;
2359  }
2360  else if (entity is Item item)
2361  {
2362  foreach (ItemContainer itemContainer in item.GetComponents<ItemContainer>())
2363  {
2364  if (itemContainer.CanBeContained(chosenItemSpawnInfo.ItemPrefab))
2365  {
2366  inventory = itemContainer?.Inventory;
2367  break;
2368  }
2369  }
2370  if (!chosenItemSpawnInfo.SpawnIfCantBeContained && inventory == null)
2371  {
2372  return;
2373  }
2374  }
2375  if (inventory != null && (inventory.CanBePut(chosenItemSpawnInfo.ItemPrefab) || chosenItemSpawnInfo.SpawnIfInventoryFull))
2376  {
2377  Entity.Spawner.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, inventory, spawnIfInventoryFull: chosenItemSpawnInfo.SpawnIfInventoryFull, onSpawned: item =>
2378  {
2379  if (chosenItemSpawnInfo.Equip && entity is Character character && character.Inventory != null)
2380  {
2381  //if the item is both pickable and wearable, try to wear it instead of picking it up
2382  List<InvSlotType> allowedSlots =
2383  item.GetComponents<Pickable>().Count() > 1 ?
2384  new List<InvSlotType>(item.GetComponent<Wearable>()?.AllowedSlots ?? item.GetComponent<Pickable>().AllowedSlots) :
2385  new List<InvSlotType>(item.AllowedSlots);
2386  allowedSlots.Remove(InvSlotType.Any);
2387  character.Inventory.TryPutItem(item, null, allowedSlots);
2388  }
2389  OnItemSpawned(item, chosenItemSpawnInfo);
2390  });
2391  }
2392  }
2393  break;
2394  case ItemSpawnInfo.SpawnPositionType.SameInventory:
2395  {
2396  Inventory inventory = null;
2397  if (entity is Character character)
2398  {
2399  inventory = character.Inventory;
2400  }
2401  else if (entity is Item item)
2402  {
2403  inventory = item.ParentInventory;
2404  }
2405  if (inventory != null)
2406  {
2407  Entity.Spawner.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, inventory, spawnIfInventoryFull: chosenItemSpawnInfo.SpawnIfInventoryFull, onSpawned: (Item newItem) =>
2408  {
2409  OnItemSpawned(newItem, chosenItemSpawnInfo);
2410  });
2411  }
2412  else if (chosenItemSpawnInfo.SpawnIfNotInInventory)
2413  {
2414  Entity.Spawner.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, position, onSpawned: (Item newItem) =>
2415  {
2416  OnItemSpawned(newItem, chosenItemSpawnInfo);
2417  });
2418  }
2419  }
2420  break;
2421  case ItemSpawnInfo.SpawnPositionType.ContainedInventory:
2422  {
2423  Inventory thisInventory = null;
2424  if (entity is Character character)
2425  {
2426  thisInventory = character.Inventory;
2427  }
2428  else if (entity is Item item)
2429  {
2430  var itemContainer = item.GetComponent<ItemContainer>();
2431  thisInventory = itemContainer?.Inventory;
2432  if (!chosenItemSpawnInfo.SpawnIfCantBeContained && !itemContainer.CanBeContained(chosenItemSpawnInfo.ItemPrefab))
2433  {
2434  return;
2435  }
2436  }
2437  if (thisInventory != null)
2438  {
2439  foreach (Item item in thisInventory.AllItems)
2440  {
2441  Inventory containedInventory = item.GetComponent<ItemContainer>()?.Inventory;
2442  if (containedInventory != null && (containedInventory.CanBePut(chosenItemSpawnInfo.ItemPrefab) || chosenItemSpawnInfo.SpawnIfInventoryFull))
2443  {
2444  Entity.Spawner.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, containedInventory, spawnIfInventoryFull: chosenItemSpawnInfo.SpawnIfInventoryFull, onSpawned: (Item newItem) =>
2445  {
2446  OnItemSpawned(newItem, chosenItemSpawnInfo);
2447  });
2448  break;
2449  }
2450  }
2451  }
2452  }
2453  break;
2454  }
2455  void OnItemSpawned(Item newItem, ItemSpawnInfo itemSpawnInfo)
2456  {
2457  newItem.Condition = newItem.MaxCondition * itemSpawnInfo.Condition;
2458  if (itemSpawnInfo.InheritEventTags)
2459  {
2460  foreach (var activeEvent in GameMain.GameSession.EventManager.ActiveEvents)
2461  {
2462  if (activeEvent is ScriptedEvent scriptedEvent)
2463  {
2464  scriptedEvent.InheritTags(entity, newItem);
2465  }
2466  }
2467  }
2468  }
2469  }
2470 
2471  private void TryTriggerAnimation(ISerializableEntity target, Entity entity)
2472  {
2473  if (animationsToTrigger == null) { return; }
2474  // Could probably use a similar pattern in other places above too, but refactoring statuseffects is very volatile.
2475  if ((CharacterFromTarget(target) ?? entity as Character) is Character targetCharacter)
2476  {
2477  foreach (AnimLoadInfo animLoadInfo in animationsToTrigger)
2478  {
2479  if (failedAnimations != null && failedAnimations.Contains((targetCharacter, animLoadInfo))) { continue; }
2480  if (!targetCharacter.AnimController.TryLoadTemporaryAnimation(animLoadInfo, throwErrors: animLoadInfo.ExpectedSpeciesNames.Contains(targetCharacter.SpeciesName)))
2481  {
2482  failedAnimations ??= new HashSet<(Character, AnimLoadInfo)>();
2483  failedAnimations.Add((targetCharacter, animLoadInfo));
2484  }
2485  }
2486  }
2487  }
2488 
2489  partial void ApplyProjSpecific(float deltaTime, Entity entity, IReadOnlyList<ISerializableEntity> targets, Hull currentHull, Vector2 worldPosition, bool playSound);
2490 
2491  private void ApplyToProperty(ISerializableEntity target, SerializableProperty property, object value, float deltaTime)
2492  {
2493  if (disableDeltaTime || setValue) { deltaTime = 1.0f; }
2494  if (value is int || value is float)
2495  {
2496  float propertyValueF = property.GetFloatValue(target);
2497  if (property.PropertyType == typeof(float))
2498  {
2499  float floatValue = value is float single ? single : (int)value;
2500  floatValue *= deltaTime;
2501  if (!setValue)
2502  {
2503  floatValue += propertyValueF;
2504  }
2505  property.TrySetValue(target, floatValue);
2506  return;
2507  }
2508  else if (property.PropertyType == typeof(int))
2509  {
2510  int intValue = (int)(value is float single ? single * deltaTime : (int)value * deltaTime);
2511  if (!setValue)
2512  {
2513  intValue += (int)propertyValueF;
2514  }
2515  property.TrySetValue(target, intValue);
2516  return;
2517  }
2518  }
2519  else if (value is bool propertyValueBool)
2520  {
2521  property.TrySetValue(target, propertyValueBool);
2522  return;
2523  }
2524  property.TrySetValue(target, value);
2525  }
2526 
2527  public static void UpdateAll(float deltaTime)
2528  {
2529  UpdateAllProjSpecific(deltaTime);
2530 
2531  DelayedEffect.Update(deltaTime);
2532  for (int i = DurationList.Count - 1; i >= 0; i--)
2533  {
2534  DurationListElement element = DurationList[i];
2535 
2536  if (element.Parent.CheckConditionalAlways && !element.Parent.HasRequiredConditions(element.Targets))
2537  {
2538  DurationList.RemoveAt(i);
2539  continue;
2540  }
2541 
2542  element.Targets.RemoveAll(t =>
2543  (t is Entity entity && entity.Removed) ||
2544  (t is Limb limb && (limb.character == null || limb.character.Removed)));
2545  if (element.Targets.Count == 0)
2546  {
2547  DurationList.RemoveAt(i);
2548  continue;
2549  }
2550 
2551  foreach (ISerializableEntity target in element.Targets)
2552  {
2553  if (target?.SerializableProperties != null)
2554  {
2555  foreach (var (propertyName, value) in element.Parent.PropertyEffects)
2556  {
2557  if (!target.SerializableProperties.TryGetValue(propertyName, out SerializableProperty property))
2558  {
2559  continue;
2560  }
2561  element.Parent.ApplyToProperty(target, property, value, CoroutineManager.DeltaTime);
2562  }
2563  }
2564 
2565  foreach (Affliction affliction in element.Parent.Afflictions)
2566  {
2567  Affliction newAffliction = affliction;
2568  if (target is Character character)
2569  {
2570  if (character.Removed) { continue; }
2571  newAffliction = element.Parent.GetMultipliedAffliction(affliction, element.Entity, character, deltaTime, element.Parent.multiplyAfflictionsByMaxVitality);
2572  var result = character.AddDamage(character.WorldPosition, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attacker: element.User);
2573  element.Parent.RegisterTreatmentResults(element.Parent.user, element.Entity as Item, result.HitLimb, affliction, result);
2574  }
2575  else if (target is Limb limb)
2576  {
2577  if (limb.character.Removed || limb.Removed) { continue; }
2578  newAffliction = element.Parent.GetMultipliedAffliction(affliction, element.Entity, limb.character, deltaTime, element.Parent.multiplyAfflictionsByMaxVitality);
2579  var result = limb.character.DamageLimb(limb.WorldPosition, limb, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: Vector2.Zero, attacker: element.User);
2580  element.Parent.RegisterTreatmentResults(element.Parent.user, element.Entity as Item, limb, affliction, result);
2581  }
2582  }
2583 
2584  foreach ((Identifier affliction, float amount) in element.Parent.ReduceAffliction)
2585  {
2586  Limb targetLimb = null;
2587  Character targetCharacter = null;
2588  if (target is Character character)
2589  {
2590  targetCharacter = character;
2591  }
2592  else if (target is Limb limb)
2593  {
2594  targetLimb = limb;
2595  targetCharacter = limb.character;
2596  }
2597  if (targetCharacter != null && !targetCharacter.Removed)
2598  {
2599  ActionType? actionType = null;
2600  if (element.Entity is Item item && item.UseInHealthInterface) { actionType = element.Parent.type; }
2601  float reduceAmount = amount * element.Parent.GetAfflictionMultiplier(element.Entity, targetCharacter, deltaTime);
2602  float prevVitality = targetCharacter.Vitality;
2603  if (targetLimb != null)
2604  {
2605  targetCharacter.CharacterHealth.ReduceAfflictionOnLimb(targetLimb, affliction, reduceAmount, treatmentAction: actionType, attacker: element.User);
2606  }
2607  else
2608  {
2609  targetCharacter.CharacterHealth.ReduceAfflictionOnAllLimbs(affliction, reduceAmount, treatmentAction: actionType, attacker: element.User);
2610  }
2611  if (!targetCharacter.IsDead)
2612  {
2613  float healthChange = targetCharacter.Vitality - prevVitality;
2614  targetCharacter.AIController?.OnHealed(healer: element.User, healthChange);
2615  if (element.User != null)
2616  {
2617  targetCharacter.TryAdjustHealerSkill(element.User, healthChange);
2618 #if SERVER
2619  GameMain.Server.KarmaManager.OnCharacterHealthChanged(targetCharacter, element.User, -healthChange, 0.0f);
2620 #endif
2621  }
2622  }
2623  }
2624  }
2625 
2626  element.Parent.TryTriggerAnimation(target, element.Entity);
2627  }
2628 
2629  element.Parent.ApplyProjSpecific(deltaTime,
2630  element.Entity,
2631  element.Targets,
2632  element.Parent.GetHull(element.Entity),
2633  element.Parent.GetPosition(element.Entity, element.Targets),
2634  playSound: element.Timer >= element.Duration);
2635 
2636  element.Timer -= deltaTime;
2637 
2638  if (element.Timer > 0.0f) { continue; }
2639  DurationList.Remove(element);
2640  }
2641  }
2642 
2643  private float GetAfflictionMultiplier(Entity entity, Character targetCharacter, float deltaTime)
2644  {
2645  float afflictionMultiplier = !setValue && !disableDeltaTime ? deltaTime : 1.0f;
2646  if (entity is Item sourceItem)
2647  {
2648  if (sourceItem.HasTag(Barotrauma.Tags.MedicalItem))
2649  {
2650  afflictionMultiplier *= 1 + targetCharacter.GetStatValue(StatTypes.MedicalItemEffectivenessMultiplier);
2651  if (user is not null)
2652  {
2653  afflictionMultiplier *= 1 + user.GetStatValue(StatTypes.MedicalItemApplyingMultiplier);
2654  }
2655  }
2656  else if (sourceItem.HasTag(AfflictionPrefab.PoisonType) && user is not null)
2657  {
2658  afflictionMultiplier *= 1 + user.GetStatValue(StatTypes.PoisonMultiplier);
2659  }
2660  }
2661  return afflictionMultiplier * AfflictionMultiplier;
2662  }
2663 
2664  private Affliction GetMultipliedAffliction(Affliction affliction, Entity entity, Character targetCharacter, float deltaTime, bool multiplyByMaxVitality)
2665  {
2666  float afflictionMultiplier = GetAfflictionMultiplier(entity, targetCharacter, deltaTime);
2667  if (multiplyByMaxVitality)
2668  {
2669  afflictionMultiplier *= targetCharacter.MaxVitality / 100f;
2670  }
2671  if (user is not null)
2672  {
2673  if (affliction.Prefab.IsBuff)
2674  {
2675  afflictionMultiplier *= 1 + user.GetStatValue(StatTypes.BuffItemApplyingMultiplier);
2676  }
2677  else if (affliction.Prefab.Identifier == "organdamage" && targetCharacter.CharacterHealth.GetActiveAfflictionTags().Any(t => t == "poisoned"))
2678  {
2679  afflictionMultiplier *= 1 + user.GetStatValue(StatTypes.PoisonMultiplier);
2680  }
2681  }
2682  if (!MathUtils.NearlyEqual(afflictionMultiplier, 1.0f))
2683  {
2684  return affliction.CreateMultiplied(afflictionMultiplier, affliction);
2685  }
2686  return affliction;
2687  }
2688 
2689  private void RegisterTreatmentResults(Character user, Item item, Limb limb, Affliction affliction, AttackResult result)
2690  {
2691  if (item == null) { return; }
2692  if (!item.UseInHealthInterface) { return; }
2693  if (limb == null) { return; }
2694  foreach (Affliction limbAffliction in limb.character.CharacterHealth.GetAllAfflictions())
2695  {
2696  if (result.Afflictions != null && result.Afflictions.Any(a => a.Prefab == limbAffliction.Prefab) &&
2697  (!affliction.Prefab.LimbSpecific || limb.character.CharacterHealth.GetAfflictionLimb(affliction) == limb))
2698  {
2699  if (type == ActionType.OnUse || type == ActionType.OnSuccess)
2700  {
2701  limbAffliction.AppliedAsSuccessfulTreatmentTime = Timing.TotalTime;
2702  limb.character.TryAdjustHealerSkill(user, affliction: affliction);
2703  }
2704  else if (type == ActionType.OnFailure)
2705  {
2706  limbAffliction.AppliedAsFailedTreatmentTime = Timing.TotalTime;
2707  limb.character.TryAdjustHealerSkill(user, affliction: affliction);
2708  }
2709  }
2710  }
2711  }
2712 
2713  static partial void UpdateAllProjSpecific(float deltaTime);
2714 
2715  public static void StopAll()
2716  {
2717  CoroutineManager.StopCoroutines("statuseffect");
2718  DelayedEffect.DelayList.Clear();
2719  DurationList.Clear();
2720  }
2721 
2722  public void AddTag(Identifier tag)
2723  {
2724  if (tags.Contains(tag)) { return; }
2725  tags.Add(tag);
2726  }
2727 
2728  public bool HasTag(Identifier tag)
2729  {
2730  if (tag == null) { return true; }
2731  return tags.Contains(tag) || tags.Contains(tag);
2732  }
2733  }
2734 }
virtual void OnHealed(Character healer, float healAmount)
virtual float Strength
Definition: Affliction.cs:31
Character Source
Which character gave this affliction
Definition: Affliction.cs:88
readonly AfflictionPrefab Prefab
Definition: Affliction.cs:12
AfflictionPrefab is a prefab that defines a type of affliction that can be applied to a character....
Affliction Instantiate(float strength, Character source=null)
static IEnumerable< AfflictionPrefab > List
readonly bool LimbSpecific
If set to true, the affliction affects individual limbs. Otherwise, it affects the whole character.
void ReduceAfflictionOnLimb(Limb targetLimb, Identifier afflictionIdOrType, float amount, ActionType? treatmentAction=null, Character attacker=null)
void ReduceAfflictionOnAllLimbs(Identifier afflictionIdOrType, float amount, ActionType? treatmentAction=null, Character attacker=null)
void CheckTalents(AbilityEffectType abilityEffectType, AbilityObject abilityObject)
void TrySeverLimbJoints(Limb targetLimb, float severLimbsProbability, float damage, bool allowBeheading, bool ignoreSeveranceProbabilityModifier=false, Character attacker=null)
float GetStatValue(StatTypes statType, bool includeSaved=true)
void TryAdjustHealerSkill(Character healer, float healthChange=0, Affliction affliction=null)
AttackResult DamageLimb(Vector2 worldPosition, Limb hitLimb, IEnumerable< Affliction > afflictions, float stun, bool playSound, Vector2 attackImpulse, Character attacker=null, float damageMultiplier=1, bool allowStacking=true, float penetration=0f, bool shouldImplode=false)
void ApplySkillGain(Identifier skillIdentifier, float baseGain, bool gainedFromAbility=false, float maxGain=2f)
Increases the characters skill at a rate proportional to their current skill. If you want to increase...
void IncreaseSkillLevel(Identifier skillIdentifier, float increase, bool gainedFromAbility=false)
Increase the skill by a specific amount. Talents may affect the actual, final skill increase.
static readonly Identifier HumanSpeciesName
string? GetAttributeString(string key, string? def)
float GetAttributeFloat(string key, float def)
ContentPackage? ContentPackage
Vector2 GetAttributeVector2(string key, in Vector2 def)
bool GetAttributeBool(string key, bool def)
int GetAttributeInt(string key, int def)
string?[] GetAttributeStringArray(string key, string[]? def, bool convertToLowerInvariant=false)
XAttribute? GetAttribute(string name)
Identifier GetAttributeIdentifier(string key, string def)
static void Update(float deltaTime)
static readonly List< DelayedListElement > DelayList
DurationListElement(StatusEffect parentEffect, Entity parentEntity, IEnumerable< ISerializableEntity > targets, float duration, Character user)
static EntitySpawner Spawner
Definition: Entity.cs:31
virtual Vector2 WorldPosition
Definition: Entity.cs:49
Submarine Submarine
Definition: Entity.cs:53
void AddCharacterToSpawnQueue(Identifier speciesName, Vector2 worldPosition, Action< Character > onSpawn=null)
Event CreateInstance(int seed)
Definition: EventPrefab.cs:115
Event sets are sets of random events that occur within a level (most commonly, monster spawns and scr...
Definition: EventSet.cs:31
static EventPrefab GetEventPrefab(Identifier identifier)
Definition: EventSet.cs:80
Explosions are area of effect attacks that can damage characters, items and structures.
void Explode(Vector2 worldPosition, Entity damageSource, Character attacker=null)
static GameSession?? GameSession
Definition: GameMain.cs:88
static NetworkMember NetworkMember
Definition: GameMain.cs:190
static LuaCsSetup LuaCs
Definition: GameMain.cs:26
Inventory(Entity owner, int capacity, int slotsPerRow=5)
IReadOnlyList< ISerializableEntity > AllPropertyObjects
static readonly List< Item > ItemList
override ImmutableHashSet< Identifier > Tags
The base class for components holding the different functionalities of the item
List< InvSlotType > AllowedSlots
Definition: Pickable.cs:25
void Shoot(Character user, Vector2 weaponPos, Vector2 spawnPos, float rotation, List< Body > ignoredBodies, bool createNetworkEvent, float damageMultiplier=1f, float launchImpulseModifier=0f)
IEnumerable< Skill > GetSkills()
Definition: Job.cs:84
JobPrefab Prefab
Definition: Job.cs:18
void HideAndDisable(float duration=0, bool ignoreCollisions=true)
object Call(string name, params object[] args)
readonly Identifier Identifier
Definition: Prefab.cs:34
Conditionals are used by some in-game mechanics to require one or more conditions to be met for those...
static IEnumerable< PropertyConditional > FromXElement(ContentXElement element, Predicate< XAttribute >? predicate=null)
static Dictionary< Identifier, SerializableProperty > DeserializeProperties(object obj, XElement element=null)
Can be used to trigger a behavior change of some kind on an AI character. Only applicable for enemy c...
Dictionary< Identifier, SerializableProperty > SerializableProperties
Can be used by AbilityConditionStatusEffectIdentifier to check whether some specific StatusEffect is ...
Defines characters spawned by the effect, and where and how they're spawned.
Increases a character's skills when the effect executes. Only valid if the target is a character or a...
readonly bool TriggerTalents
Should the talents that trigger when the character gains skills be triggered by the effect?
readonly bool Proportional
Should the amount be inversely proportional to the current skill level? Meaning, the higher the skill...
readonly bool UseDeltaTime
Should the amount be multiplied by delta time? Useful if you want to give a skill increase per frame.
readonly Identifier SkillIdentifier
The identifier of the skill to increase.
Unlocks a talent, or multiple talents when the effect executes. Only valid if the target is a charact...
Identifier[] TalentIdentifiers
The identifier(s) of the talents that should be unlocked.
bool GiveRandom
If true and there's multiple identifiers defined, a random one will be chosen instead of unlocking al...
StatusEffects can be used to execute various kinds of effects: modifying the state of some entity in ...
Vector2 Offset
An offset added to the position of the effect is executed at. Only relevant if the effect does someth...
void Apply(float deltaTime, Entity entity, IReadOnlyList< ISerializableEntity > targets, Vector2? worldPosition=null)
readonly bool OnlyOutside
If enabled, this effect can only execute outside hulls.
StatusEffect(ContentXElement element, string parentDebugName)
readonly bool Stackable
Only valid if the effect has a duration or delay. Can the effect be applied on the same target(s) if ...
readonly float SeverLimbsProbability
The probability of severing a limb damaged by this status effect. Only valid when targeting character...
static readonly List< DurationListElement > DurationList
readonly bool AllowWhenBroken
Can the StatusEffect be applied when the item applying it is broken?
static StatusEffect Load(ContentXElement element, string parentDebugName)
readonly string TargetItemComponent
If set to the name of one of the target's ItemComponents, the effect is only applied on that componen...
void AddNearbyTargets(Vector2 worldPosition, List< ISerializableEntity > targets)
readonly bool OnlyWhenDamagedByPlayer
If enabled, the effect only executes when the entity receives damage from a player character (a chara...
readonly record struct AnimLoadInfo(AnimationType Type, Either< string, ContentPath > File, float Priority, ImmutableArray< Identifier > ExpectedSpeciesNames)
float Range
How close to the entity executing the effect the targets must be. Only applicable if targeting Nearby...
readonly ImmutableArray<(Identifier propertyName, object value)> PropertyEffects
readonly float Interval
The interval at which the effect is executed. The difference between delay and interval is that effec...
readonly List<(Identifier AfflictionIdentifier, float ReduceAmount)> ReduceAffliction
int TargetSlot
Index of the slot the target must be in. Only valid when targeting a Contained item.
bool HasRequiredConditions(IReadOnlyList< ISerializableEntity > targets)
virtual void Apply(ActionType type, float deltaTime, Entity entity, IReadOnlyList< ISerializableEntity > targets, Vector2? worldPosition=null)
readonly LimbType[] targetLimbs
Which types of limbs this effect can target? Only valid when targeting characters or limbs.
readonly bool CheckConditionalAlways
Only applicable for StatusEffects with a duration or delay. Should the conditional checks only be don...
readonly float Duration
How long the effect runs (in seconds). Note that if Stackable is true, there can be multiple instance...
readonly ImmutableHashSet< Identifier > TargetIdentifiers
Identifier(s), tag(s) or species name(s) of the entity the effect can target. Null if there's no iden...
virtual void Apply(ActionType type, float deltaTime, Entity entity, ISerializableEntity target, Vector2? worldPosition=null)
readonly bool OnlyInside
If enabled, this effect can only execute inside a hull.
Submarine(SubmarineInfo info, bool showErrorMessages=true, Func< Submarine, List< MapEntity >> loadEntities=null, IdRemap linkedRemap=null)
Dictionary< Identifier, SerializableProperty > SerializableProperties
ActionType
ActionTypes define when a StatusEffect is executed.
Definition: Enums.cs:19
AbilityEffectType
Definition: Enums.cs:125
StatTypes
StatTypes are used to alter several traits of a character. They are mostly used by talents.
Definition: Enums.cs:180