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