Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/Characters/Attack.cs
2 using Microsoft.Xna.Framework;
3 using System;
4 using System.Collections.Generic;
5 using System.Linq;
6 using System.Xml.Linq;
7 
8 namespace Barotrauma
9 {
10  public enum HitDetection
11  {
12  Distance,
13  Contact,
14  None
15  }
16 
17  public enum AttackContext
18  {
19  Any,
20  Water,
21  Ground,
22  Inside,
23  Outside,
25  }
26 
27  public enum AttackTarget
28  {
29  Any,
30  Character,
31  Structure // Including hulls etc. Evaluated as anything but a character.
32  }
33 
35  {
36  FallBack,
39  Pursue,
40  Eat,
45  Reverse,
47  }
48 
49  struct AttackResult
50  {
51  public float Damage
52  {
53  get;
54  private set;
55  }
56  public readonly List<Affliction> Afflictions;
57 
58  public readonly Limb HitLimb;
59 
60  public readonly List<DamageModifier> AppliedDamageModifiers;
61 
62  public AttackResult(List<Affliction> afflictions, Limb hitLimb, List<DamageModifier> appliedDamageModifiers = null)
63  {
64  HitLimb = hitLimb;
65  Afflictions = new List<Affliction>();
66 
67  foreach (Affliction affliction in afflictions)
68  {
69  Afflictions.Add(affliction.Prefab.Instantiate(affliction.Strength, affliction.Source));
70  }
71  AppliedDamageModifiers = appliedDamageModifiers;
72  Damage = Afflictions.Sum(a => a.GetVitalityDecrease(null));
73  }
74 
75  public AttackResult(float damage, List<DamageModifier> appliedDamageModifiers = null)
76  {
77  Damage = damage;
78  HitLimb = null;
79  Afflictions = null;
80  AppliedDamageModifiers = appliedDamageModifiers;
81  }
82  }
83 
89  partial class Attack : ISerializableEntity
90  {
91  [Serialize(AttackContext.Any, IsPropertySaveable.Yes, description: "The attack will be used only in this context."), Editable]
92  public AttackContext Context { get; private set; }
93 
94  [Serialize(AttackTarget.Any, IsPropertySaveable.Yes, description: "Does the attack target only specific targets?"), Editable]
95  public AttackTarget TargetType { get; private set; }
96 
97  [Serialize(LimbType.None, IsPropertySaveable.Yes, description: "To which limb is the attack aimed at? If not defined or set to none, the closest limb is used (default)."), Editable]
98  public LimbType TargetLimbType { get; private set; }
99 
100  [Serialize(HitDetection.Distance, IsPropertySaveable.Yes, description: "Collision detection is more accurate, but it only affects targets that are in contact with the limb."), Editable]
101  public HitDetection HitDetectionType { get; private set; }
102 
103  [Serialize(AIBehaviorAfterAttack.FallBack, IsPropertySaveable.Yes, description: "The preferred AI behavior after the attack."), Editable]
104  public AIBehaviorAfterAttack AfterAttack { get; set; }
105 
106  [Serialize(0f, IsPropertySaveable.Yes, description: "A delay before reacting after performing an attack."), Editable]
107  public float AfterAttackDelay { get; set; }
108 
110  description: "Secondary AI behavior after the attack. The character first executes the AfterAttack behavior, then after AfterAttackSecondaryDelay passes, switches to this one. Ignored if AfterAttackSecondaryDelay is 0 or less."), Editable]
112 
113  [Serialize(0.0f, IsPropertySaveable.Yes, description: "How long the character executes the AfterAttack before switching to AfterAttackSecondary. The secondary behavior is ignored if this value is 0 or less."), Editable]
114  public float AfterAttackSecondaryDelay { get; set; }
115 
116  [Serialize(false, IsPropertySaveable.Yes, description: "Should the AI try to turn around when aiming with this attack?"), Editable]
117  public bool Reverse { get; private set; }
118 
119  [Serialize(true, IsPropertySaveable.Yes, description: "Should the rope attached to this limb snap upon choosing a new attack?"), Editable]
120  public bool SnapRopeOnNewAttack { get; private set; }
121 
122  [Serialize(false, IsPropertySaveable.Yes, description: "Should the AI try to steer away from the target when aiming with this attack? Best combined with PassiveAggressive behavior."), Editable]
123  public bool Retreat { get; private set; }
124 
125  private float _range;
126  [Serialize(0.0f, IsPropertySaveable.Yes, description: "The min distance from the attack limb to the target before the AI tries to attack."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)]
127  public float Range
128  {
129  get => _range * RangeMultiplier;
130  set => _range = value;
131  }
132 
133  private float _damageRange;
134  [Serialize(0.0f, IsPropertySaveable.Yes, description: "The min distance from the attack limb to the target to do damage. In distance-based hit detection, the hit will be registered as soon as the target is within the damage range, unless the attack duration has expired."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)]
135  public float DamageRange
136  {
137  get => _damageRange * RangeMultiplier;
138  set => _damageRange = value;
139  }
140 
141  [Serialize(0.0f, IsPropertySaveable.Yes, description: "Used by enemy AI to determine the minimum range required for the attack to hit."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)]
142  public float MinRange { get; private set; }
143 
144  [Serialize(0.25f, IsPropertySaveable.Yes, description: "An approximation of the attack duration. Effectively defines the time window in which the hit can be registered. If set to too low value, it's possible that the attack won't hit the target in time."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f, DecimalCount = 2)]
145  public float Duration { get; private set; }
146 
147  [Serialize(5f, IsPropertySaveable.Yes, description: "How long the AI must wait before it can use this attack again."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, DecimalCount = 2)]
148  public float CoolDown { get; set; } = 5;
149 
150 
151  [Serialize(0f, IsPropertySaveable.Yes, description: "When the attack cooldown is running and when there are other valid attacks possible for the character to use, the secondary cooldown is used instead of the regular cooldown. Does not have an effect, if set to 0 or less than the regular cooldown value."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, DecimalCount = 2)]
152  public float SecondaryCoolDown { get; set; } = 0;
153 
154  [Serialize(0f, IsPropertySaveable.Yes, description: "A random factor applied to all cooldowns. Example: 0.1 -> adds a random value between -10% and 10% of the cooldown. Min 0 (default), Max 1 (could disable or double the cooldown in extreme cases)."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)]
155  public float CoolDownRandomFactor { get; private set; } = 0;
156 
157  [Serialize(false, IsPropertySaveable.Yes, description: "When set to true, causes the enemy AI to use the fast movement animations when the attack is on cooldown."), Editable]
158  public bool FullSpeedAfterAttack { get; private set; }
159 
160  private float _structureDamage;
161  [Serialize(0.0f, IsPropertySaveable.Yes, description: "How much damage the attack does to submarine walls."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)]
162  public float StructureDamage
163  {
164  get => _structureDamage * DamageMultiplier;
165  set => _structureDamage = value;
166  }
167 
168  [Serialize(false, IsPropertySaveable.Yes, description: "If the attack causes an explosion of wall damage shrapnel, should some of the shrapnel be launched as projectiles that can go through walls?"), Editable]
169  public bool CreateWallDamageProjectiles { get; private set; }
170 
171  [Serialize(true, IsPropertySaveable.Yes, description: "Whether or not damaging structures with the attack causes damage particles to emit."), Editable]
172  public bool EmitStructureDamageParticles { get; private set; }
173 
174  private float _itemDamage;
175  [Serialize(0.0f, IsPropertySaveable.Yes, description: "How much damage the attack does to items."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)]
176  public float ItemDamage
177  {
178  get =>_itemDamage * DamageMultiplier;
179  set => _itemDamage = value;
180  }
181 
182  [Serialize(0.0f, IsPropertySaveable.Yes, description: "Percentage of damage mitigation ignored when hitting armored body parts (deflecting limbs)."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1f)]
183  public float Penetration { get; private set; }
184 
188  public float DamageMultiplier { get; set; } = 1;
189 
193  public float RangeMultiplier { get; set; } = 1;
194 
198  public float ImpactMultiplier { get; set; } = 1;
199 
200  [Serialize(0.0f, IsPropertySaveable.Yes, description: "How much damage the attack does to level walls."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)]
201  public float LevelWallDamage { get; set; }
202 
203  [Serialize(false, IsPropertySaveable.Yes, description: "Sets whether or not the attack is ranged or not."), Editable]
204  public bool Ranged { get; set; }
205 
206  [Serialize(false, IsPropertySaveable.Yes, description:"When enabled the attack will not be launched when there's a friendly character in the way. Only affects ranged attacks."), Editable]
207  public bool AvoidFriendlyFire { get; set; }
208 
209  [Serialize(20f, IsPropertySaveable.Yes, description: "Used by enemy AI to determine how accurately the attack needs to be aimed for the attack to trigger. Only affects ranged attacks."), Editable]
210  public float RequiredAngle { get; set; }
211 
212  [Serialize(0f, IsPropertySaveable.Yes, description: "By default uses the same value as RequiredAngle. Use if you want to allow selecting the attack but not shooting until the angle is smaller. Only affects ranged attacks."), Editable]
213  public float RequiredAngleToShoot { get; set; }
214 
215  [Serialize(0f, IsPropertySaveable.Yes, description: "How much the attack limb is rotated towards the target. Default 0 = no rotation. Only affects ranged attacks."), Editable]
216  public float AimRotationTorque { get; set; }
217 
218  [Serialize(-1, IsPropertySaveable.Yes, description: "Reference to the limb we apply the aim rotation to. By default same as the attack limb. Only affects ranged attacks."), Editable]
219  public int RotationLimbIndex { get; set; }
220 
221  [Serialize(0f, IsPropertySaveable.Yes, description:"How much the held weapon is swayed back and forth while aiming. Only affects monsters using ranged weapons (items). Default 0 means the weapon is not swayed at all."), Editable]
222  public float SwayAmount { get; set; }
223 
224  [Serialize(5f, IsPropertySaveable.Yes, description: "How fast the held weapon is swayed back and forth while aiming. Only affects monsters using ranged weapons (items)."), Editable]
225  public float SwayFrequency { get; set; }
226 
227  [Serialize(0.0f, IsPropertySaveable.No, description: "Legacy functionality. Behaves otherwise the same as stuns defined as afflictions, but explosions only apply the stun once instead of dividing it between the limbs.")]
228  public float Stun { get; set; }
229 
230  [Serialize(false, IsPropertySaveable.Yes, description: "Can damage only Humans."), Editable]
231  public bool OnlyHumans { get; set; }
232 
233  [Serialize("", IsPropertySaveable.Yes, description: "List of limb indices to apply the force into."), Editable]
234  public string ApplyForceOnLimbs
235  {
236  get
237  {
238  return string.Join(", ", ForceOnLimbIndices);
239  }
240  set
241  {
242  ForceOnLimbIndices.Clear();
243  if (string.IsNullOrEmpty(value)) { return; }
244  foreach (string limbIndexStr in value.Split(','))
245  {
246  if (int.TryParse(limbIndexStr.Trim(), out int limbIndex))
247  {
248  ForceOnLimbIndices.Add(limbIndex);
249  }
250  }
251  }
252  }
253 
254  [Serialize(0.0f, IsPropertySaveable.Yes, description: "Applied to the attacking limb (or limbs defined using ApplyForceOnLimbs). The direction of the force is towards the target that's being attacked."), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)]
255  public float Force { get; private set; }
256 
257  [Serialize("0.0, 0.0", IsPropertySaveable.Yes, description: "Applied to the main limb. In world space coordinates(i.e. 0, 1 pushes the character upwards a bit). The attacker's facing direction is taken into account."), Editable]
258  public Vector2 RootForceWorldStart { get; private set; }
259 
260  [Serialize("0.0, 0.0", IsPropertySaveable.Yes, description: "Applied to the main limb. In world space coordinates(i.e. 0, 1 pushes the character upwards a bit). The attacker's facing direction is taken into account."), Editable]
261  public Vector2 RootForceWorldMiddle { get; private set; }
262 
263  [Serialize("0.0, 0.0", IsPropertySaveable.Yes, description: "Applied to the main limb. In world space coordinates(i.e. 0, 1 pushes the character upwards a bit). The attacker's facing direction is taken into account."), Editable]
264  public Vector2 RootForceWorldEnd { get; private set; }
265 
266  [Serialize(TransitionMode.Linear, IsPropertySaveable.Yes, description:"Applied to the main limb. The transition smoothing of the applied force."), Editable]
267  public TransitionMode RootTransitionEasing { get; private set; }
268 
269  [Serialize(0.0f, IsPropertySaveable.Yes, description: "Applied to the attacking limb (or limbs defined using ApplyForceOnLimbs)"), Editable(MinValueFloat = -10000.0f, MaxValueFloat = 10000.0f)]
270  public float Torque { get; private set; }
271 
272  [Serialize(false, IsPropertySaveable.Yes, description: "Only apply the force once during the attacks lifetime."), Editable]
273  public bool ApplyForcesOnlyOnce { get; private set; }
274 
275  [Serialize(0.0f, IsPropertySaveable.Yes, description: "Applied to the target the attack hits. The direction of the impulse is from this limb towards the target (use negative values to pull the target closer)."), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)]
276  public float TargetImpulse { get; private set; }
277 
278  [Serialize("0.0, 0.0", IsPropertySaveable.Yes, description: "Applied to the target, in world space coordinates(i.e. 0, -1 pushes the target downwards). The attacker's facing direction is taken into account."), Editable]
279  public Vector2 TargetImpulseWorld { get; private set; }
280 
281  [Serialize(0.0f, IsPropertySaveable.Yes, description: "Applied to the target the attack hits. The direction of the force is from this limb towards the target (use negative values to pull the target closer)."), Editable(-1000.0f, 1000.0f)]
282  public float TargetForce { get; private set; }
283 
284  [Serialize("0.0, 0.0", IsPropertySaveable.Yes, description: "Applied to the target, in world space coordinates(i.e. 0, -1 pushes the target downwards). The attacker's facing direction is taken into account."), Editable]
285  public Vector2 TargetForceWorld { get; private set; }
286 
287  [Serialize(1.0f, IsPropertySaveable.Yes, description: "Affects the strength of the impact effects the limb causes when it hits a submarine."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)]
288  public float SubmarineImpactMultiplier { get; private set; }
289 
290  [Serialize(0.0f, IsPropertySaveable.Yes, description: "How likely the attack causes target limbs to be severed."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)]
291  public float SeverLimbsProbability { get; set; }
292 
293  // TODO: disabled because not synced
294  //[Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)]
295  //public float StickChance { get; set; }
296  public float StickChance => 0f;
297 
298  [Serialize(0.0f, IsPropertySaveable.Yes, description: "Used by enemy AI to determine the priority when selecting attacks. When random attacks are disabled on the character it is multiplied with distance to determine the which attack to use. Only attacks that are currently valid are taken into consideration when making the decision."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)]
299  public float Priority { get; private set; }
300 
301  [Serialize(false, IsPropertySaveable.Yes, description: "Triggers the 'blink' animation on the attacking limbs when the attack executes. Used e.g. by abyss monsters to make their jaws close when attacking."), Editable]
302  public bool Blink { get; private set; }
303 
304  public IEnumerable<StatusEffect> StatusEffects
305  {
306  get { return statusEffects; }
307  }
308 
309  public string Name => "Attack";
310 
311  public Dictionary<Identifier, SerializableProperty> SerializableProperties
312  {
313  get;
314  private set;
315  } = new Dictionary<Identifier, SerializableProperty>();
316 
317  //the indices of the limbs Force is applied on
318  //(if none, force is applied only to the limb the attack is attached to)
319  public readonly List<int> ForceOnLimbIndices = new List<int>();
320 
321  public readonly Dictionary<Affliction, XElement> Afflictions = new Dictionary<Affliction, XElement>();
322 
326  public List<PropertyConditional> Conditionals { get; private set; } = new List<PropertyConditional>();
327 
332  private readonly List<StatusEffect> statusEffects = new List<StatusEffect>();
333 
334  public void SetUser(Character user)
335  {
336  if (statusEffects == null) { return; }
337  foreach (StatusEffect statusEffect in statusEffects)
338  {
339  statusEffect.SetUser(user);
340  }
341  }
342 
343  // used for talents/ability conditions
344  public Item SourceItem { get; set; }
345 
346  public List<Affliction> GetMultipliedAfflictions(float multiplier)
347  {
348  List<Affliction> multipliedAfflictions = new List<Affliction>();
349  foreach (Affliction affliction in Afflictions.Keys)
350  {
351  multipliedAfflictions.Add(affliction.CreateMultiplied(multiplier, affliction));
352  }
353  return multipliedAfflictions;
354  }
355 
356  public float GetStructureDamage(float deltaTime)
357  {
358  return (Duration == 0.0f) ? StructureDamage : StructureDamage * deltaTime;
359  }
360 
361  public float GetLevelWallDamage(float deltaTime)
362  {
363  return (Duration == 0.0f) ? LevelWallDamage : LevelWallDamage * deltaTime;
364  }
365 
366  public float GetItemDamage(float deltaTime, float multiplier = 1)
367  {
368  float dmg = ItemDamage * multiplier;
369  return (Duration == 0.0f) ? dmg : dmg * deltaTime;
370  }
371 
372  public float GetTotalDamage(bool includeStructureDamage = false)
373  {
374  float totalDamage = includeStructureDamage ? StructureDamage : 0.0f;
375  foreach (Affliction affliction in Afflictions.Keys)
376  {
377  totalDamage += affliction.GetVitalityDecrease(null);
378  }
379  return totalDamage * DamageMultiplier;
380  }
381 
382  public Attack(float damage, float bleedingDamage, float burnDamage, float structureDamage, float itemDamage, float range = 0.0f)
383  {
384  if (damage > 0.0f) { Afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(damage), null); }
385  if (bleedingDamage > 0.0f) { Afflictions.Add(AfflictionPrefab.Bleeding.Instantiate(bleedingDamage), null); }
386  if (burnDamage > 0.0f) { Afflictions.Add(AfflictionPrefab.Burn.Instantiate(burnDamage), null); }
387 
388  Range = range;
389  DamageRange = range;
390  StructureDamage = LevelWallDamage = structureDamage;
391  ItemDamage = itemDamage;
392  }
393 
394  public Attack(ContentXElement element, string parentDebugName, Item sourceItem) : this(element, parentDebugName)
395  {
396  SourceItem = sourceItem;
397  }
398 
399  public Attack(ContentXElement element, string parentDebugName)
400  {
401  Deserialize(element, parentDebugName);
402 
403  if (element.GetAttribute("damage") != null ||
404  element.GetAttribute("bluntdamage") != null ||
405  element.GetAttribute("burndamage") != null ||
406  element.GetAttribute("bleedingdamage") != null)
407  {
408  DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Define damage as afflictions instead of using the damage attribute (e.g. <Affliction identifier=\"internaldamage\" strength=\"10\" />).",
409  contentPackage: element.ContentPackage);
410  }
411 
412  //if level wall damage is not defined, default to the structure damage
413  if (element.GetAttribute("LevelWallDamage") == null &&
414  element.GetAttribute("levelwalldamage") == null)
415  {
417  }
418 
419  InitProjSpecific(element);
420 
421  foreach (var subElement in element.Elements())
422  {
423  switch (subElement.Name.ToString().ToLowerInvariant())
424  {
425  case "statuseffect":
426  statusEffects.Add(StatusEffect.Load(subElement, parentDebugName));
427  break;
428  case "affliction":
429  AfflictionPrefab afflictionPrefab;
430  if (subElement.GetAttribute("name") != null)
431  {
432  DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - define afflictions using identifiers instead of names.",
433  contentPackage: element.ContentPackage);
434  string afflictionName = subElement.GetAttributeString("name", "").ToLowerInvariant();
435  afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Name.Equals(afflictionName, System.StringComparison.OrdinalIgnoreCase));
436  if (afflictionPrefab == null)
437  {
438  DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Affliction prefab \"" + afflictionName + "\" not found.",
439  contentPackage: element.ContentPackage);
440  continue;
441  }
442  }
443  else
444  {
445  Identifier afflictionIdentifier = subElement.GetAttributeIdentifier("identifier", "");
446  if (!AfflictionPrefab.Prefabs.TryGet(afflictionIdentifier, out afflictionPrefab))
447  {
448  DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Affliction prefab \"" + afflictionIdentifier + "\" not found.",
449  contentPackage: element.ContentPackage);
450  continue;
451  }
452  }
453  break;
454  case "conditional":
455  Conditionals.AddRange(PropertyConditional.FromXElement(subElement));
456  break;
457  }
458  }
459 
461  {
462  DebugConsole.AddWarning($"Potentially misconfigured attack in {parentDebugName}. Secondary cooldown should not be longer than the primary cooldown.",
463  contentPackage: element.ContentPackage);
464  }
465  }
466  partial void InitProjSpecific(ContentXElement element);
467 
468  public void ReloadAfflictions(ContentXElement element, string parentDebugName)
469  {
470  Afflictions.Clear();
471  foreach (var subElement in element.GetChildElements("affliction"))
472  {
473  Affliction affliction;
474  Identifier afflictionIdentifier = subElement.GetAttributeIdentifier("identifier", "");
475  if (!AfflictionPrefab.Prefabs.TryGet(afflictionIdentifier, out AfflictionPrefab afflictionPrefab))
476  {
477  DebugConsole.ThrowError($"Error in an Attack defined in \"{parentDebugName}\" - could not find an affliction with the identifier \"{afflictionIdentifier}\".",
478  contentPackage: element.ContentPackage);
479  continue;
480  }
481  affliction = afflictionPrefab.Instantiate(0.0f);
482  affliction.Deserialize(subElement);
483  // add the affliction anyway, so that it can be shown in the editor.
484  Afflictions.Add(affliction, subElement);
485  }
486  }
487 
488  public void Serialize(ContentXElement element)
489  {
490  SerializableProperty.SerializeProperties(this, element, true);
491  foreach (var affliction in Afflictions)
492  {
493  if (affliction.Value != null)
494  {
495  affliction.Key.Serialize(affliction.Value);
496  }
497  }
498  }
499 
500  public void Deserialize(ContentXElement element, string parentDebugName)
501  {
503  ReloadAfflictions(element, parentDebugName);
504  }
505 
506  public AttackResult DoDamage(Character attacker, IDamageable target, Vector2 worldPosition, float deltaTime, bool playSound = true, PhysicsBody sourceBody = null, Limb sourceLimb = null)
507  {
508  Character targetCharacter = target as Character;
509  if (OnlyHumans)
510  {
511  if (targetCharacter != null && !targetCharacter.IsHuman)
512  {
513  return new AttackResult();
514  }
515  }
516 
517  SetUser(attacker);
518 
519  DamageParticles(deltaTime, worldPosition);
520 
521  Vector2 impulseDirection = GetImpulseDirection(target as ISpatialEntity, worldPosition, SourceItem);
522  var attackResult = target?.AddDamage(attacker, worldPosition, this, impulseDirection, deltaTime, playSound) ?? new AttackResult();
523  var conditionalEffectType = attackResult.Damage > 0.0f ? ActionType.OnSuccess : ActionType.OnFailure;
524  var additionalEffectType = ActionType.OnUse;
525  if (targetCharacter != null && targetCharacter.IsDead)
526  {
527  additionalEffectType = ActionType.OnEating;
528  }
529 
530  foreach (StatusEffect effect in statusEffects)
531  {
532  effect.sourceBody = sourceBody;
533  if (effect.HasTargetType(StatusEffect.TargetType.This) || effect.HasTargetType(StatusEffect.TargetType.Character))
534  {
535  var t = sourceLimb ?? attacker as ISerializableEntity;
536  if (additionalEffectType != ActionType.OnEating)
537  {
538  effect.Apply(conditionalEffectType, deltaTime, attacker, t, worldPosition);
539  }
540  effect.Apply(additionalEffectType, deltaTime, attacker, t, worldPosition);
541  }
542  if (effect.HasTargetType(StatusEffect.TargetType.Parent))
543  {
544  if (additionalEffectType != ActionType.OnEating)
545  {
546  effect.Apply(conditionalEffectType, deltaTime, attacker, attacker);
547  }
548  effect.Apply(additionalEffectType, deltaTime, attacker, attacker);
549  }
550  if (targetCharacter != null)
551  {
552  if (effect.HasTargetType(StatusEffect.TargetType.Limb))
553  {
554  if (additionalEffectType != ActionType.OnEating)
555  {
556  effect.Apply(conditionalEffectType, deltaTime, targetCharacter, attackResult.HitLimb);
557  }
558  effect.Apply(additionalEffectType, deltaTime, targetCharacter, attackResult.HitLimb);
559  }
560  if (effect.HasTargetType(StatusEffect.TargetType.AllLimbs))
561  {
562  var targets = targetCharacter.AnimController.Limbs;
563  if (additionalEffectType != ActionType.OnEating)
564  {
565  effect.Apply(conditionalEffectType, deltaTime, targetCharacter, targets);
566  }
567  effect.Apply(additionalEffectType, deltaTime, targetCharacter, targets);
568  }
569  }
570  if (target is Entity targetEntity)
571  {
572  if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
573  effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
574  {
575  targets.Clear();
576  effect.AddNearbyTargets(worldPosition, targets);
577  if (additionalEffectType != ActionType.OnEating)
578  {
579  effect.Apply(conditionalEffectType, deltaTime, targetEntity, targets);
580  }
581  effect.Apply(additionalEffectType, deltaTime, targetEntity, targets);
582  }
583  if (effect.HasTargetType(StatusEffect.TargetType.UseTarget))
584  {
585  if (additionalEffectType != ActionType.OnEating)
586  {
587  effect.Apply(conditionalEffectType, deltaTime, targetEntity, targetEntity as ISerializableEntity, worldPosition);
588  }
589  effect.Apply(additionalEffectType, deltaTime, targetEntity, targetEntity as ISerializableEntity, worldPosition);
590  }
591  }
592  if (effect.HasTargetType(StatusEffect.TargetType.Contained))
593  {
594  targets.Clear();
595  targets.AddRange(attacker.Inventory.AllItems);
596  if (additionalEffectType != ActionType.OnEating)
597  {
598  effect.Apply(conditionalEffectType, deltaTime, attacker, targets);
599  }
600  effect.Apply(additionalEffectType, deltaTime, attacker, targets);
601  }
602  }
603 
604  return attackResult;
605  }
606 
607  readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
608  public AttackResult DoDamageToLimb(Character attacker, Limb targetLimb, Vector2 worldPosition, float deltaTime, bool playSound = true, PhysicsBody sourceBody = null, Limb sourceLimb = null)
609  {
610  if (targetLimb == null)
611  {
612  return new AttackResult();
613  }
614 
615  if (OnlyHumans)
616  {
617  if (targetLimb.character != null && !targetLimb.character.IsHuman)
618  {
619  return new AttackResult();
620  }
621  }
622 
623  SetUser(attacker);
624 
625  DamageParticles(deltaTime, worldPosition);
626 
627  float penetration = Penetration;
628 
629  RangedWeapon weapon =
630  SourceItem?.GetComponent<RangedWeapon>() ??
631  SourceItem?.GetComponent<Projectile>()?.Launcher?.GetComponent<RangedWeapon>();
632  float? penetrationValue = weapon?.Penetration;
633  if (penetrationValue.HasValue)
634  {
635  penetration += penetrationValue.Value;
636  }
637 
638  Vector2 impulseDirection = GetImpulseDirection(targetLimb, worldPosition, SourceItem);
639  var attackResult = targetLimb.character.ApplyAttack(attacker, worldPosition, this, deltaTime, impulseDirection, playSound, targetLimb, penetration);
640  var conditionalEffectType = attackResult.Damage > 0.0f ? ActionType.OnSuccess : ActionType.OnFailure;
641 
642  foreach (StatusEffect effect in statusEffects)
643  {
644  effect.sourceBody = sourceBody;
645  if (effect.HasTargetType(StatusEffect.TargetType.This) || effect.HasTargetType(StatusEffect.TargetType.Character))
646  {
647  effect.Apply(conditionalEffectType, deltaTime, attacker, sourceLimb ?? attacker as ISerializableEntity);
648  effect.Apply(ActionType.OnUse, deltaTime, attacker, sourceLimb ?? attacker as ISerializableEntity);
649  }
650  if (effect.HasTargetType(StatusEffect.TargetType.Parent))
651  {
652  effect.Apply(conditionalEffectType, deltaTime, attacker, attacker);
653  effect.Apply(ActionType.OnUse, deltaTime, attacker, attacker);
654  }
655  if (effect.HasTargetType(StatusEffect.TargetType.UseTarget))
656  {
657  effect.Apply(conditionalEffectType, deltaTime, targetLimb.character, targetLimb.character);
658  effect.Apply(ActionType.OnUse, deltaTime, targetLimb.character, targetLimb.character);
659  }
660  if (effect.HasTargetType(StatusEffect.TargetType.Limb))
661  {
662  effect.Apply(conditionalEffectType, deltaTime, targetLimb.character, targetLimb);
663  effect.Apply(ActionType.OnUse, deltaTime, targetLimb.character, targetLimb);
664  }
665  if (effect.HasTargetType(StatusEffect.TargetType.AllLimbs))
666  {
667  var targets = targetLimb.character.AnimController.Limbs;
668  effect.Apply(conditionalEffectType, deltaTime, targetLimb.character, targets);
669  effect.Apply(ActionType.OnUse, deltaTime, targetLimb.character, targets);
670  }
671  if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
672  effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
673  {
674  targets.Clear();
675  effect.AddNearbyTargets(worldPosition, targets);
676  effect.Apply(conditionalEffectType, deltaTime, targetLimb.character, targets);
677  effect.Apply(ActionType.OnUse, deltaTime, targetLimb.character, targets);
678  }
679  if (effect.HasTargetType(StatusEffect.TargetType.Contained))
680  {
681  targets.Clear();
682  targets.AddRange(attacker.Inventory.AllItems);
683  effect.Apply(conditionalEffectType, deltaTime, attacker, targets);
684  effect.Apply(ActionType.OnUse, deltaTime, attacker, targets);
685  }
686  }
687 
688  return attackResult;
689  }
690 
691  private Vector2 GetImpulseDirection(ISpatialEntity target, Vector2 sourceWorldPosition, Item sourceItem)
692  {
693  Vector2 impulseDirection = Vector2.Zero;
694  if (target != null)
695  {
696  impulseDirection = target.WorldPosition - sourceWorldPosition;
697  }
698 
699  if (sourceItem?.body != null && sourceItem.body.Enabled && sourceItem.body.LinearVelocity.LengthSquared() > 0.0f)
700  {
701  impulseDirection = sourceItem.body.LinearVelocity;
702  }
703  else
704  {
705  var projectileComponent = sourceItem?.GetComponent<Projectile>();
706  if (projectileComponent != null)
707  {
708  impulseDirection = new Vector2(MathF.Cos(SourceItem.Rotation), MathF.Sin(SourceItem.Rotation));
709  }
710  }
711 
712  if (impulseDirection.LengthSquared() > 0.0001f)
713  {
714  impulseDirection = Vector2.Normalize(impulseDirection);
715  }
716  return impulseDirection;
717  }
718 
719  public float AttackTimer { get; private set; }
720  public float CoolDownTimer { get; set; }
721  public float CurrentRandomCoolDown { get; private set; }
722  public float SecondaryCoolDownTimer { get; set; }
723  public bool IsRunning { get; private set; }
724 
725  public float AfterAttackTimer { get; set; }
726 
727  public void UpdateCoolDown(float deltaTime)
728  {
729  CoolDownTimer -= deltaTime;
730  SecondaryCoolDownTimer -= deltaTime;
731  if (CoolDownTimer < 0) { CoolDownTimer = 0; }
733  }
734 
735  public void UpdateAttackTimer(float deltaTime, Character character)
736  {
737  IsRunning = true;
738  AttackTimer += deltaTime;
739  if (AttackTimer >= Duration)
740  {
742  SetCoolDown(applyRandom: !character.IsPlayer);
743  }
744  }
745 
746  public void ResetAttackTimer()
747  {
748  AfterAttackTimer = 0;
749  AttackTimer = 0;
750  IsRunning = false;
751  }
752 
753  public void SetCoolDown(bool applyRandom)
754  {
755  if (applyRandom)
756  {
757  float randomFraction = CoolDown * CoolDownRandomFactor;
758  CurrentRandomCoolDown = MathHelper.Lerp(-randomFraction, randomFraction, Rand.Value());
760  randomFraction = SecondaryCoolDown * CoolDownRandomFactor;
761  SecondaryCoolDownTimer = SecondaryCoolDown + MathHelper.Lerp(-randomFraction, randomFraction, Rand.Value());
762  }
763  else
764  {
768  }
769  }
770 
771  public void ResetCoolDown()
772  {
773  CoolDownTimer = 0;
776  }
777 
778  partial void DamageParticles(float deltaTime, Vector2 worldPosition);
779 
780  public bool IsValidContext(AttackContext context) => Context == context || Context == AttackContext.Any || Context == AttackContext.NotDefined;
781 
782  public bool IsValidContext(IEnumerable<AttackContext> contexts)
783  {
784  foreach (var context in contexts)
785  {
786  switch (context)
787  {
788  case AttackContext.Ground:
789  if (Context == AttackContext.Water)
790  {
791  return false;
792  }
793  break;
794  case AttackContext.Water:
795  if (Context == AttackContext.Ground)
796  {
797  return false;
798  }
799  break;
800  case AttackContext.Inside:
801  if (Context == AttackContext.Outside)
802  {
803  return false;
804  }
805  break;
806  case AttackContext.Outside:
807  if (Context == AttackContext.Inside)
808  {
809  return false;
810  }
811  break;
812  default:
813  continue;
814  }
815  }
816  return true;
817  }
818 
819  public bool IsValidTarget(AttackTarget targetType) => TargetType == AttackTarget.Any || TargetType == targetType;
820 
821  public bool IsValidTarget(Entity target)
822  {
823  return TargetType switch
824  {
825  AttackTarget.Character => target is Character,
826  AttackTarget.Structure => !(target is Character),
827  _ => true,
828  };
829  }
830 
831  public Vector2 CalculateAttackPhase(TransitionMode easing = TransitionMode.Linear)
832  {
833  float t = AttackTimer / Duration;
834  return MathUtils.Bezier(RootForceWorldStart, RootForceWorldMiddle, RootForceWorldEnd, ToolBox.GetEasing(easing, t));
835  }
836  }
837 }
void Deserialize(XElement element)
Definition: Affliction.cs:130
float GetVitalityDecrease(CharacterHealth characterHealth)
Definition: Affliction.cs:171
Affliction CreateMultiplied(float multiplier, Affliction affliction)
Definition: Affliction.cs:140
virtual float Strength
Definition: Affliction.cs:31
void Serialize(XElement element)
Definition: Affliction.cs:125
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 AfflictionPrefab InternalDamage
static IEnumerable< AfflictionPrefab > List
static AfflictionPrefab Burn
static AfflictionPrefab Bleeding
static readonly PrefabCollection< AfflictionPrefab > Prefabs
Attacks are used to deal damage to characters, structures and items. They can be defined in the weapo...
void Deserialize(ContentXElement element, string parentDebugName)
AttackResult DoDamageToLimb(Character attacker, Limb targetLimb, Vector2 worldPosition, float deltaTime, bool playSound=true, PhysicsBody sourceBody=null, Limb sourceLimb=null)
Attack(ContentXElement element, string parentDebugName, Item sourceItem)
bool IsValidContext(IEnumerable< AttackContext > contexts)
float RangeMultiplier
Used for multiplying all the ranges.
Dictionary< Identifier, SerializableProperty > SerializableProperties
bool IsValidContext(AttackContext context)
Vector2 CalculateAttackPhase(TransitionMode easing=TransitionMode.Linear)
float ImpactMultiplier
Used for multiplying the physics forces.
float GetTotalDamage(bool includeStructureDamage=false)
void UpdateAttackTimer(float deltaTime, Character character)
bool IsValidTarget(AttackTarget targetType)
Attack(float damage, float bleedingDamage, float burnDamage, float structureDamage, float itemDamage, float range=0.0f)
void ReloadAfflictions(ContentXElement element, string parentDebugName)
float DamageMultiplier
Used for multiplying all the damage.
AttackResult DoDamage(Character attacker, IDamageable target, Vector2 worldPosition, float deltaTime, bool playSound=true, PhysicsBody sourceBody=null, Limb sourceLimb=null)
List< Affliction > GetMultipliedAfflictions(float multiplier)
readonly Dictionary< Affliction, XElement > Afflictions
float GetItemDamage(float deltaTime, float multiplier=1)
List< PropertyConditional > Conditionals
Only affects ai decision making. All the conditionals has to be met in order to select the attack....
Attack(ContentXElement element, string parentDebugName)
AttackResult ApplyAttack(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, Vector2 impulseDirection, bool playSound=false, Limb targetLimb=null, float penetration=0f)
Apply the specified attack to this character. If the targetLimb is not specified, the limb closest to...
ContentPackage? ContentPackage
XAttribute? GetAttribute(string name)
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)
static void SerializeProperties(ISerializableEntity obj, XElement element, bool saveIfDefault=false, bool ignoreEditable=false)
StatusEffects can be used to execute various kinds of effects: modifying the state of some entity in ...
static StatusEffect Load(ContentXElement element, string parentDebugName)
void AddNearbyTargets(Vector2 worldPosition, List< ISerializableEntity > targets)
virtual void Apply(ActionType type, float deltaTime, Entity entity, ISerializableEntity target, Vector2? worldPosition=null)
AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, Vector2 impulseDirection, float deltaTime, bool playSound=true)
ActionType
ActionTypes define when a StatusEffect is executed.
Definition: Enums.cs:19
TransitionMode
Definition: Enums.cs:6
readonly List< DamageModifier > AppliedDamageModifiers
AttackResult(float damage, List< DamageModifier > appliedDamageModifiers=null)
AttackResult(List< Affliction > afflictions, Limb hitLimb, List< DamageModifier > appliedDamageModifiers=null)