Files
LuaCsForBarotraumaEP/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs
Eero 45312af297 WIP Make static collections thread-safe using ThreadStatic and ThreadLocal
Refactored various static and instance collections to use [ThreadStatic], ThreadLocal, or local variables to prevent concurrent modification issues during parallel updates. This affects status effect targets, affliction lists, damage modifiers, and cached data in Character, CharacterHealth, Limb, Explosion, Hull, Submarine, and ToolBox classes. Also replaced Dictionary caches with ConcurrentDictionary where appropriate for thread safety.
2025-12-28 14:14:53 +08:00

1524 lines
60 KiB
C#

using FarseerPhysics;
using FarseerPhysics.Dynamics;
using FarseerPhysics.Dynamics.Contacts;
using FarseerPhysics.Dynamics.Joints;
using Microsoft.Xna.Framework;
using Barotrauma.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Networking;
using LimbParams = Barotrauma.RagdollParams.LimbParams;
using JointParams = Barotrauma.RagdollParams.JointParams;
using Barotrauma.Abilities;
namespace Barotrauma
{
public enum LimbType
{
None, LeftHand, RightHand, LeftArm, RightArm, LeftForearm, RightForearm,
LeftLeg, RightLeg, LeftFoot, RightFoot, Head, Torso, Tail, Legs, RightThigh, LeftThigh, Waist, Jaw
};
partial class LimbJoint
{
public bool IsSevered;
public bool CanBeSevered => Params.CanBeSevered;
public readonly JointParams Params;
public readonly Ragdoll ragdoll;
public readonly Limb LimbA, LimbB;
public float Scale => Params.Scale * ragdoll.RagdollParams.JointScale;
public readonly RevoluteJoint revoluteJoint;
public readonly WeldJoint weldJoint;
public Joint Joint => revoluteJoint ?? weldJoint as Joint;
public bool Enabled
{
get => Joint.Enabled;
set => Joint.Enabled = value;
}
public Body BodyA => Joint.BodyA;
public Body BodyB => Joint.BodyB;
public Vector2 WorldAnchorA
{
get => Joint.WorldAnchorA;
set => Joint.WorldAnchorA = value;
}
public Vector2 WorldAnchorB
{
get => Joint.WorldAnchorB;
set => Joint.WorldAnchorB = value;
}
public Vector2 LocalAnchorA
{
get => revoluteJoint != null ? revoluteJoint.LocalAnchorA : weldJoint.LocalAnchorA;
set
{
if (weldJoint != null)
{
weldJoint.LocalAnchorA = value;
}
else
{
revoluteJoint.LocalAnchorA = value;
}
}
}
public Vector2 LocalAnchorB
{
get => revoluteJoint != null ? revoluteJoint.LocalAnchorB : weldJoint.LocalAnchorB;
set
{
if (weldJoint != null)
{
weldJoint.LocalAnchorB = value;
}
else
{
revoluteJoint.LocalAnchorB = value;
}
}
}
public bool LimitEnabled
{
get => revoluteJoint != null ? revoluteJoint.LimitEnabled : false;
set
{
if (revoluteJoint != null)
{
revoluteJoint.LimitEnabled = value;
}
}
}
public float LowerLimit
{
get => revoluteJoint != null ? revoluteJoint.LowerLimit : 0;
set
{
if (revoluteJoint != null)
{
revoluteJoint.LowerLimit = value;
}
}
}
public float UpperLimit
{
get => revoluteJoint != null ? revoluteJoint.UpperLimit : 0;
set
{
if (revoluteJoint != null)
{
revoluteJoint.UpperLimit = value;
}
}
}
public float JointAngle => revoluteJoint != null ? revoluteJoint.JointAngle : weldJoint.ReferenceAngle;
public LimbJoint(Limb limbA, Limb limbB, JointParams jointParams, Ragdoll ragdoll) : this(limbA, limbB, Vector2.One, Vector2.One, jointParams.WeldJoint)
{
Params = jointParams;
this.ragdoll = ragdoll;
LoadParams();
}
public LimbJoint(Limb limbA, Limb limbB, Vector2 anchor1, Vector2 anchor2, bool weld = false)
{
if (weld)
{
weldJoint = new WeldJoint(limbA.body.FarseerBody, limbB.body.FarseerBody, anchor1, anchor2);
}
else
{
revoluteJoint = new RevoluteJoint(limbA.body.FarseerBody, limbB.body.FarseerBody, anchor1, anchor2)
{
MotorEnabled = true,
MaxMotorTorque = 0.25f
};
}
Joint.CollideConnected = false;
LimbA = limbA;
LimbB = limbB;
}
public void LoadParams()
{
if (revoluteJoint != null)
{
revoluteJoint.MaxMotorTorque = Params.Stiffness;
revoluteJoint.LimitEnabled = Params.LimitEnabled;
}
if (float.IsNaN(Params.LowerLimit))
{
Params.LowerLimit = 0;
}
if (float.IsNaN(Params.UpperLimit))
{
Params.UpperLimit = 0;
}
if (ragdoll.IsFlipped)
{
if (weldJoint != null)
{
weldJoint.LocalAnchorA = ConvertUnits.ToSimUnits(new Vector2(-Params.Limb1Anchor.X, Params.Limb1Anchor.Y) * Scale);
weldJoint.LocalAnchorB = ConvertUnits.ToSimUnits(new Vector2(-Params.Limb2Anchor.X, Params.Limb2Anchor.Y) * Scale);
}
else
{
revoluteJoint.LocalAnchorA = ConvertUnits.ToSimUnits(new Vector2(-Params.Limb1Anchor.X, Params.Limb1Anchor.Y) * Scale);
revoluteJoint.LocalAnchorB = ConvertUnits.ToSimUnits(new Vector2(-Params.Limb2Anchor.X, Params.Limb2Anchor.Y) * Scale);
revoluteJoint.UpperLimit = MathHelper.ToRadians(-Params.LowerLimit);
revoluteJoint.LowerLimit = MathHelper.ToRadians(-Params.UpperLimit);
}
}
else
{
if (weldJoint != null)
{
weldJoint.LocalAnchorA = ConvertUnits.ToSimUnits(Params.Limb1Anchor * Scale);
weldJoint.LocalAnchorB = ConvertUnits.ToSimUnits(Params.Limb2Anchor * Scale);
}
else
{
revoluteJoint.LocalAnchorA = ConvertUnits.ToSimUnits(Params.Limb1Anchor * Scale);
revoluteJoint.LocalAnchorB = ConvertUnits.ToSimUnits(Params.Limb2Anchor * Scale);
revoluteJoint.UpperLimit = MathHelper.ToRadians(Params.UpperLimit);
revoluteJoint.LowerLimit = MathHelper.ToRadians(Params.LowerLimit);
}
}
}
}
partial class Limb : ISerializableEntity, ISpatialEntity
{
//how long it takes for severed limbs to fade out
public float SeveredFadeOutTime { get; private set; } = 10;
public readonly Character character;
/// <summary>
/// Note that during the limb initialization, character.AnimController returns null, whereas this field is already assigned.
/// </summary>
public readonly Ragdoll ragdoll;
public readonly LimbParams Params;
/// <summary>
/// The physics body of the limb
/// </summary>
public PhysicsBody body;
public Vector2 StepOffset => ConvertUnits.ToSimUnits(Params.StepOffset) * ragdoll.RagdollParams.JointScale;
public Hull Hull;
public bool InWater { get; set; }
private FixedMouseJoint pullJoint;
public readonly LimbType type;
private bool ignoreCollisions;
public bool IgnoreCollisions
{
get { return ignoreCollisions; }
set
{
ignoreCollisions = value;
if (body != null)
{
if (ignoreCollisions)
{
body.CollisionCategories = Category.None;
body.CollidesWith = Category.None;
}
else
{
//limbs don't collide with each other
body.CollisionCategories = Physics.CollisionCharacter;
body.CollidesWith = Physics.CollisionAll & ~Physics.CollisionCharacter & ~Physics.CollisionItem & ~Physics.CollisionItemBlocking;
}
}
}
}
private bool isSevered;
private float severedFadeOutTimer;
private Vector2? mouthPos;
public Vector2 MouthPos
{
get
{
mouthPos ??= Params.MouthPos;
return mouthPos.Value;
}
set
{
mouthPos = value;
}
}
public readonly Attack attack;
public List<DamageModifier> DamageModifiers { get; private set; } = new List<DamageModifier>();
private Direction dir;
public int HealthIndex => Params.HealthIndex;
public float Scale => Params.Scale * Params.Ragdoll.LimbScale;
public float AttackPriority => Params.AttackPriority;
public bool DoesFlip
{
get
{
if (character?.AnimController.CurrentAnimationParams is GroundedMovementParams && IsLeg)
{
// Legs always has to flip when not swimming
return true;
}
return Params.Flip;
}
}
public bool DoesMirror
{
get
{
if (IsLeg)
{
// Legs always has to mirror
return true;
}
return DoesFlip;
}
}
public float SteerForce => Params.SteerForce;
public Vector2 DebugTargetPos;
public Vector2 DebugRefPos;
/// <summary>
/// Is the limb the waist, a part of a leg or a tail?
/// </summary>
public bool IsLowerBody
{
get
{
switch (type)
{
case LimbType.LeftLeg:
case LimbType.RightLeg:
case LimbType.LeftFoot:
case LimbType.RightFoot:
case LimbType.Tail:
case LimbType.Legs:
case LimbType.LeftThigh:
case LimbType.RightThigh:
case LimbType.Waist:
return true;
default:
return false;
}
}
}
/// <summary>
/// Is the limb a leg or a part of a leg (upper or lower leg or foot)
/// </summary>
public bool IsLeg
{
get
{
return type switch
{
LimbType.LeftFoot or LimbType.LeftLeg or LimbType.LeftThigh or LimbType.RightFoot or LimbType.RightLeg or LimbType.RightThigh => true,
_ => false,
};
}
}
/// <summary>
/// Is the limb an arm or a part of an arm (upper or lower arm or hand)
/// </summary>
public bool IsArm
{
get
{
return type switch
{
LimbType.LeftArm or LimbType.LeftForearm or LimbType.LeftHand or LimbType.RightArm or LimbType.RightForearm or LimbType.RightHand => true,
_ => false,
};
}
}
public bool IsSevered
{
get { return isSevered; }
set
{
if (isSevered == value) { return; }
if (value == true)
{
// If any of the connected limbs have a longer fade out time, use that
var connectedLimbs = GetConnectedLimbs();
SeveredFadeOutTime = Math.Max(Params.SeveredFadeOutTime, connectedLimbs.Any() ? connectedLimbs.Max(l => l.SeveredFadeOutTime) : 0);
}
isSevered = value;
if (isSevered)
{
ragdoll.SubtractMass(this);
if (type == LimbType.Head && character.Params.Health.DieFromBeheading)
{
character.Kill(CauseOfDeathType.Unknown, null);
}
}
else
{
severedFadeOutTimer = 0.0f;
}
#if CLIENT
if (isSevered)
{
damageOverlayStrength = 1.0f;
}
#endif
}
}
public Submarine Submarine => character?.Submarine;
private bool _hidden;
public bool Hidden
{
get => _hidden || Params.Hide;
set => _hidden = value;
}
// Just a wrapper for Hidden, but both can be used via status effects, so it's not safe to remove it.
public bool Hide
{
get => Hidden;
set => Hidden = value;
}
public Vector2 WorldPosition
{
get { return character?.Submarine == null ? Position : Position + character.Submarine.Position; }
}
public Vector2 Position
{
get { return ConvertUnits.ToDisplayUnits(body?.SimPosition ?? Vector2.Zero); }
}
public Vector2 SimPosition
{
get
{
if (Removed)
{
#if DEBUG
DebugConsole.ThrowError("Attempted to access a removed limb.\n" + Environment.StackTrace.CleanupStackTrace());
#endif
GameAnalyticsManager.AddErrorEventOnce("Limb.LinearVelocity:SimPosition", GameAnalyticsManager.ErrorSeverity.Error,
"Attempted to access a removed limb.\n" + Environment.StackTrace.CleanupStackTrace());
return Vector2.Zero;
}
return body.SimPosition;
}
}
public Vector2 DrawPosition
{
get
{
if (Removed)
{
#if DEBUG
DebugConsole.ThrowError("Attempted to access a removed limb.\n" + Environment.StackTrace.CleanupStackTrace());
#endif
GameAnalyticsManager.AddErrorEventOnce("Limb.LinearVelocity:DrawPosition", GameAnalyticsManager.ErrorSeverity.Error,
"Attempted to access a removed limb.\n" + Environment.StackTrace.CleanupStackTrace());
return Vector2.Zero;
}
return body.DrawPosition;
}
}
public float Rotation
{
get
{
if (Removed)
{
#if DEBUG
DebugConsole.ThrowError("Attempted to access a removed limb.\n" + Environment.StackTrace.CleanupStackTrace());
#endif
GameAnalyticsManager.AddErrorEventOnce("Limb.LinearVelocity:SimPosition", GameAnalyticsManager.ErrorSeverity.Error,
"Attempted to access a removed limb.\n" + Environment.StackTrace.CleanupStackTrace());
return 0.0f;
}
return body.Rotation;
}
}
//where an animcontroller is trying to pull the limb, only used for debug visualization
public Vector2 AnimTargetPos { get; private set; }
public float Mass
{
get
{
if (Removed)
{
#if DEBUG
DebugConsole.ThrowError("Attempted to access a removed limb.\n" + Environment.StackTrace.CleanupStackTrace());
#endif
GameAnalyticsManager.AddErrorEventOnce("Limb.Mass:AccessRemoved", GameAnalyticsManager.ErrorSeverity.Error,
"Attempted to access a removed limb.\n" + Environment.StackTrace.CleanupStackTrace());
return 1.0f;
}
return body.Mass;
}
}
public bool Disabled { get; set; }
public Vector2 LinearVelocity
{
get
{
if (Removed)
{
#if DEBUG
DebugConsole.ThrowError("Attempted to access a removed limb.\n" + Environment.StackTrace.CleanupStackTrace());
#endif
GameAnalyticsManager.AddErrorEventOnce("Limb.LinearVelocity:AccessRemoved", GameAnalyticsManager.ErrorSeverity.Error,
"Attempted to access a removed limb.\n" + Environment.StackTrace.CleanupStackTrace());
return Vector2.Zero;
}
return body.LinearVelocity;
}
}
public float Dir
{
get { return (dir == Direction.Left) ? -1.0f : 1.0f; }
set
{
dir = (value == -1.0f) ? Direction.Left : Direction.Right;
if (body != null)
{
body.Dir = Dir;
}
}
}
private float _alpha = 1.0f;
/// <summary>
/// Can be used by status effects
/// </summary>
public float Alpha
{
get => _alpha;
set
{
_alpha = MathHelper.Clamp(value, 0.0f, 1.0f);
}
}
public int RefJointIndex => Params.RefJoint;
public readonly List<WearableSprite> WearingItems = new List<WearableSprite>();
/// <summary>
/// Other wearables attached to the head. I.e. husk sprite, hair, beard, moustache, and face attachments.
/// </summary>
public readonly List<WearableSprite> OtherWearables = new List<WearableSprite>();
public bool PullJointEnabled
{
get { return pullJoint.Enabled; }
set { pullJoint.Enabled = value; }
}
public float PullJointMaxForce
{
get { return pullJoint.MaxForce; }
set { pullJoint.MaxForce = value; }
}
public Vector2 PullJointWorldAnchorA
{
get { return pullJoint.WorldAnchorA; }
set
{
if (!MathUtils.IsValid(value))
{
string errorMsg = "Attempted to set the anchor A of a limb's pull joint to an invalid value (" + value + ")\n" + Environment.StackTrace.CleanupStackTrace();
GameAnalyticsManager.AddErrorEventOnce("Limb.SetPullJointAnchorA:InvalidValue", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
#if DEBUG
DebugConsole.ThrowError(errorMsg);
#endif
return;
}
if (Vector2.DistanceSquared(SimPosition, value) > 50.0f * 50.0f)
{
Vector2 diff = value - SimPosition;
string errorMsg = "Attempted to move the anchor A of a limb's pull joint extremely far from the limb (diff: " + diff +
", limb enabled: " + body.Enabled +
", simple physics enabled: " + character.AnimController.SimplePhysicsEnabled + ")\n"
+ Environment.StackTrace.CleanupStackTrace();
GameAnalyticsManager.AddErrorEventOnce("Limb.SetPullJointAnchorA:ExcessiveValue", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
#if DEBUG
DebugConsole.ThrowError(errorMsg);
#endif
return;
}
pullJoint.WorldAnchorA = value;
}
}
public Vector2 PullJointWorldAnchorB
{
get { return pullJoint.WorldAnchorB; }
set
{
if (!MathUtils.IsValid(value))
{
string errorMsg = "Attempted to set the anchor B of a limb's pull joint to an invalid value (" + value + ")\n" + Environment.StackTrace.CleanupStackTrace();
GameAnalyticsManager.AddErrorEventOnce("Limb.SetPullJointAnchorB:InvalidValue", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
#if DEBUG
DebugConsole.ThrowError(errorMsg);
#endif
return;
}
if (Vector2.DistanceSquared(pullJoint.WorldAnchorA, value) > 50.0f * 50.0f)
{
Vector2 diff = value - pullJoint.WorldAnchorA;
string errorMsg = "Attempted to move the anchor B of a limb's pull joint extremely far from the limb (diff: " + diff +
", limb enabled: " + body.Enabled +
", simple physics enabled: " + character.AnimController.SimplePhysicsEnabled + ")\n"
+ Environment.StackTrace.CleanupStackTrace();
GameAnalyticsManager.AddErrorEventOnce("Limb.SetPullJointAnchorB:ExcessiveValue", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
#if DEBUG
DebugConsole.ThrowError(errorMsg);
#endif
return;
}
pullJoint.WorldAnchorB = value;
}
}
public Vector2 PullJointLocalAnchorA
{
get { return pullJoint.LocalAnchorA; }
}
public bool Removed
{
get;
private set;
}
public Items.Components.Rope AttachedRope { get; set; }
public string Name => Params.Name;
// These properties are exposed for status effects
public bool IsDead => character.IsDead;
public float Health => character.Health;
public float HealthPercentage => character.HealthPercentage;
public bool IsHuman => character.IsHuman;
public AIState AIState => character.AIController is EnemyAIController enemyAI ? enemyAI.State : AIState.Idle;
public bool IsFlipped => character.AnimController.IsFlipped;
public bool CanBeSeveredAlive
{
get
{
if (character.IsHumanoid) { return false; }
// TODO: We might need this or solve the cases where a limb is severed while holding on to an item
//if (character.Params.CanInteract) { return false; }
if (this == character.AnimController.MainLimb) { return false; }
bool canBeSevered = Params.CanBeSeveredAlive;
if (character.AnimController.CanWalk && !character.Params.Health.AllowSeveringLegs)
{
switch (type)
{
case LimbType.LeftFoot:
case LimbType.RightFoot:
case LimbType.LeftLeg:
case LimbType.RightLeg:
case LimbType.LeftThigh:
case LimbType.RightThigh:
case LimbType.Legs:
case LimbType.Waist:
return false;
}
}
return canBeSevered;
}
}
public Dictionary<Identifier, SerializableProperty> SerializableProperties
{
get;
private set;
}
private readonly Dictionary<ActionType, List<StatusEffect>> statusEffects = new Dictionary<ActionType, List<StatusEffect>>();
public Dictionary<ActionType, List<StatusEffect>> StatusEffects { get { return statusEffects; } }
public Limb(Ragdoll ragdoll, Character character, LimbParams limbParams)
{
this.ragdoll = ragdoll;
this.character = character;
this.Params = limbParams;
dir = Direction.Right;
body = new PhysicsBody(limbParams, findNewContacts: false);
type = limbParams.Type;
IgnoreCollisions = limbParams.IgnoreCollisions;
body.UserData = this;
pullJoint = new FixedMouseJoint(body.FarseerBody, ConvertUnits.ToSimUnits(limbParams.PullPos * Scale))
{
Enabled = false,
//MaxForce = ((type == LimbType.LeftHand || type == LimbType.RightHand) ? 400.0f : 150.0f) * body.Mass
// 150 or even 400 is too low if the joint is used for moving the character position from the mainlimb towards the collider position
MaxForce = 1000 * Mass
};
GameMain.World.Add(pullJoint);
var element = limbParams.Element;
body.BodyType = BodyType.Dynamic;
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "attack":
attack = new Attack(subElement, (character == null ? "null" : character.Name) + ", limb " + type);
if (attack.DamageRange <= 0)
{
switch (body.BodyShape)
{
case PhysicsBody.Shape.Circle:
attack.DamageRange = body.Radius;
break;
case PhysicsBody.Shape.Capsule:
attack.DamageRange = body.Height / 2 + body.Radius;
break;
case PhysicsBody.Shape.Rectangle:
attack.DamageRange = new Vector2(body.Width / 2.0f, body.Height / 2.0f).Length();
break;
}
attack.DamageRange = ConvertUnits.ToDisplayUnits(attack.DamageRange);
}
if (character is { VariantOf.IsEmpty: false })
{
var attackElement = character.Params.VariantFile.GetRootExcludingOverride().GetChildElement("attack");
if (attackElement != null)
{
attack.SetInitialDamageMultiplier(attackElement.GetAttributeFloat("damagemultiplier", 1f));
attack.RangeMultiplier = attackElement.GetAttributeFloat("rangemultiplier", 1f);
attack.ImpactMultiplier = attackElement.GetAttributeFloat("impactmultiplier", 1f);
}
}
break;
case "damagemodifier":
DamageModifiers.Add(new DamageModifier(subElement, character.Name));
break;
case "statuseffect":
var statusEffect = StatusEffect.Load(subElement, character.Name + ", " + Name);
if (statusEffect != null)
{
if (!statusEffects.ContainsKey(statusEffect.type))
{
statusEffects.Add(statusEffect.type, new List<StatusEffect>());
}
statusEffects[statusEffect.type].Add(statusEffect);
}
break;
}
}
SerializableProperties = SerializableProperty.GetProperties(this);
InitProjSpecific(element);
}
partial void InitProjSpecific(ContentXElement element);
public void MoveToPos(Vector2 pos, float force, bool pullFromCenter = false)
{
Vector2 pullPos = body.SimPosition;
if (!pullFromCenter)
{
pullPos = pullJoint.WorldAnchorA;
}
AnimTargetPos = pos;
body.MoveToPos(pos, force, pullPos);
}
public void MirrorPullJoint()
{
pullJoint.LocalAnchorA = new Vector2(-pullJoint.LocalAnchorA.X, pullJoint.LocalAnchorA.Y);
}
public AttackResult AddDamage(Vector2 simPosition, float damage, float bleedingDamage, float burnDamage, bool playSound)
{
List<Affliction> afflictions = new List<Affliction>();
if (damage > 0.0f) afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(damage));
if (bleedingDamage > 0.0f) afflictions.Add(AfflictionPrefab.Bleeding.Instantiate(bleedingDamage));
if (burnDamage > 0.0f) afflictions.Add(AfflictionPrefab.Burn.Instantiate(burnDamage));
return AddDamage(simPosition, afflictions, playSound);
}
// Thread-safe: using local variables instead of instance fields to avoid concurrent modification
public AttackResult AddDamage(Vector2 simPosition, IEnumerable<Affliction> afflictions, bool playSound, float damageMultiplier = 1, float penetration = 0f, Character attacker = null)
{
var appliedDamageModifiers = new List<DamageModifier>();
var afflictionsCopy = new List<Affliction>();
foreach (var affliction in afflictions)
{
var tempModifiers = new List<DamageModifier>();
var newAffliction = affliction;
float random = Rand.Value(Rand.RandSync.Unsynced);
bool foundMatchingModifier = false;
bool applyAffliction = true;
foreach (DamageModifier damageModifier in DamageModifiers)
{
if (!damageModifier.MatchesAffliction(affliction)) { continue; }
foundMatchingModifier = true;
if (random > affliction.Probability * damageModifier.ProbabilityMultiplier)
{
applyAffliction = false;
continue;
}
if (SectorHit(damageModifier.ArmorSectorInRadians, simPosition))
{
tempModifiers.Add(damageModifier);
}
}
foreach (WearableSprite wearable in WearingItems)
{
foreach (DamageModifier damageModifier in wearable.WearableComponent.DamageModifiers)
{
if (!damageModifier.MatchesAffliction(affliction)) { continue; }
foundMatchingModifier = true;
if (random > affliction.Probability * damageModifier.ProbabilityMultiplier)
{
applyAffliction = false;
continue;
}
if (SectorHit(damageModifier.ArmorSectorInRadians, simPosition))
{
tempModifiers.Add(damageModifier);
}
}
}
if (!foundMatchingModifier && random > affliction.Probability) { continue; }
float finalDamageModifier = affliction.AffectedByAttackMultipliers ? damageMultiplier : 1.0f;
if (character.EmpVulnerability > 0 && affliction.Prefab.AfflictionType == AfflictionPrefab.EMPType)
{
finalDamageModifier *= character.EmpVulnerability;
}
if (!character.Params.Health.PoisonImmunity)
{
if (affliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType || affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType)
{
finalDamageModifier *= character.PoisonVulnerability;
}
}
foreach (DamageModifier damageModifier in tempModifiers)
{
float damageModifierValue = damageModifier.DamageMultiplier;
if (damageModifier.DeflectProjectiles && damageModifierValue < 1f)
{
damageModifierValue = MathHelper.Lerp(damageModifierValue, 1f, penetration);
}
finalDamageModifier *= damageModifierValue;
}
if (affliction.MultiplyByMaxVitality)
{
finalDamageModifier *= character.MaxVitality / 100f;
}
if (!MathUtils.NearlyEqual(finalDamageModifier, 1.0f))
{
newAffliction = affliction.CreateMultiplied(finalDamageModifier, affliction);
}
else
{
newAffliction.SetStrength(affliction.NonClampedStrength);
}
if (attacker != null)
{
var abilityAfflictionCharacter = new AbilityAfflictionCharacter(newAffliction, character);
attacker.CheckTalents(AbilityEffectType.OnAddDamageAffliction, abilityAfflictionCharacter);
newAffliction = abilityAfflictionCharacter.Affliction;
}
if (applyAffliction)
{
afflictionsCopy.Add(newAffliction);
newAffliction.Source ??= attacker;
}
appliedDamageModifiers.AddRange(tempModifiers);
}
var result = new AttackResult(afflictionsCopy, this, appliedDamageModifiers);
if (result.Afflictions.None())
{
playSound = false;
}
AddDamageProjSpecific(playSound, result);
float bleedingDamage = 0;
if (character.CharacterHealth.DoesBleed)
{
foreach (var affliction in result.Afflictions)
{
if (affliction is AfflictionBleeding)
{
bleedingDamage += affliction.GetVitalityDecrease(character.CharacterHealth);
}
}
if (bleedingDamage > 0)
{
float bloodDecalSize = MathHelper.Clamp(bleedingDamage / 5, 0.1f, 1.0f);
if (character.CurrentHull != null && !string.IsNullOrEmpty(character.BloodDecalName))
{
character.CurrentHull.AddDecal(character.BloodDecalName, WorldPosition, MathHelper.Clamp(bloodDecalSize, 0.5f, 1.0f), isNetworkEvent: false);
}
}
}
return result;
}
partial void AddDamageProjSpecific(bool playSound, AttackResult result);
public bool SectorHit(Vector2 armorSector, Vector2 simPosition)
{
if (armorSector == Vector2.Zero) { return false; }
//sector 360 degrees or more -> always hits
if (Math.Abs(armorSector.Y - armorSector.X) >= MathHelper.TwoPi) { return true; }
float rotation = body.TransformedRotation;
float offset = (MathHelper.PiOver2 - MathUtils.GetMidAngle(armorSector.X, armorSector.Y)) * Dir;
float hitAngle = VectorExtensions.Angle(VectorExtensions.Forward(rotation + offset), SimPosition - simPosition);
float sectorSize = GetArmorSectorSize(armorSector);
return hitAngle < sectorSize / 2;
}
protected float GetArmorSectorSize(Vector2 armorSector)
{
return Math.Abs(armorSector.X - armorSector.Y);
}
public void Update(float deltaTime)
{
UpdateProjSpecific(deltaTime);
ApplyStatusEffects(ActionType.Always, deltaTime);
ApplyStatusEffects(ActionType.OnActive, deltaTime);
if (InWater)
{
body.ApplyWaterForces();
}
if (isSevered)
{
severedFadeOutTimer += deltaTime;
if (severedFadeOutTimer >= SeveredFadeOutTime)
{
body.Enabled = false;
}
else if (character.CurrentHull == null && Hull.FindHull(WorldPosition) != null)
{
severedFadeOutTimer = SeveredFadeOutTime;
}
}
else if (!IsDead && (character.IsPlayer || character.AIState is not AIState.PlayDead))
{
if (Params.BlinkFrequency > 0)
{
if (BlinkTimer > -TotalBlinkDurationOut)
{
BlinkTimer -= deltaTime;
}
else
{
BlinkTimer = Params.BlinkFrequency;
}
}
if (reEnableTimer > 0)
{
reEnableTimer -= deltaTime;
}
else if (reEnableTimer > -1)
{
ReEnable();
}
}
attack?.UpdateCoolDown(deltaTime);
}
private bool temporarilyDisabled;
private float reEnableTimer = -1;
private bool originalIgnoreCollisions;
public void HideAndDisable(float duration = 0, bool ignoreCollisions = true)
{
if (Hidden || Disabled) { return; }
temporarilyDisabled = true;
Hidden = true;
Disabled = true;
originalIgnoreCollisions = IgnoreCollisions;
IgnoreCollisions = ignoreCollisions;
if (duration > 0)
{
reEnableTimer = duration;
}
#if CLIENT
if (Hidden && LightSource != null)
{
LightSource.Enabled = false;
}
#endif
}
public void ReEnable()
{
if (!temporarilyDisabled) { return; }
temporarilyDisabled = false;
Hidden = false;
Disabled = false;
IgnoreCollisions = originalIgnoreCollisions;
reEnableTimer = -1;
}
partial void UpdateProjSpecific(float deltaTime);
// Thread-static to avoid concurrent modification in parallel item updates
[ThreadStatic]
private static List<Body> t_contactBodies;
private static List<Body> ContactBodies => t_contactBodies ??= new List<Body>();
/// <summary>
/// Returns true if the attack successfully hit something. If the distance is not given, it will be calculated.
/// </summary>
public bool UpdateAttack(float deltaTime, Vector2 attackSimPos, IDamageable damageTarget, out AttackResult attackResult, float distance = -1, Limb targetLimb = null)
{
attackResult = default;
var contactBodies = ContactBodies;
Vector2 simPos = ragdoll.SimplePhysicsEnabled ? character.SimPosition : SimPosition;
float dist = distance > -1 ? distance : ConvertUnits.ToDisplayUnits(Vector2.Distance(simPos, attackSimPos));
bool wasRunning = attack.IsRunning;
attack.UpdateAttackTimer(deltaTime, character);
if (attack.Blink)
{
if (attack.ForceOnLimbIndices != null && attack.ForceOnLimbIndices.Any())
{
foreach (int limbIndex in attack.ForceOnLimbIndices)
{
if (limbIndex < 0 || limbIndex >= character.AnimController.Limbs.Length) { continue; }
Limb limb = character.AnimController.Limbs[limbIndex];
if (limb.IsSevered) { continue; }
limb.Blink();
}
}
else
{
Blink();
}
}
bool wasHit = false;
Body structureBody = null;
if (damageTarget != null)
{
switch (attack.HitDetectionType)
{
case HitDetection.Distance:
if (dist < attack.DamageRange)
{
Vector2 rayStart = simPos;
Vector2 rayEnd = attackSimPos;
if (Submarine == null && damageTarget is ISpatialEntity spatialEntity && spatialEntity.Submarine != null)
{
rayStart -= spatialEntity.Submarine.SimPosition;
rayEnd -= spatialEntity.Submarine.SimPosition;
}
structureBody = Submarine.CheckVisibility(rayStart, rayEnd);
if (damageTarget is Item i && i.GetComponent<Items.Components.Door>() != null)
{
// If the attack is aimed to an item and hits an item, it's successful.
// Ignore blocking checks on doors, because it causes cases where a Mudraptor cannot hit the hatch, for example.
wasHit = true;
}
else if (damageTarget is Structure wall && structureBody != null &&
(structureBody.UserData is Structure || (structureBody.UserData is Submarine sub && sub == wall.Submarine)))
{
// If the attack is aimed to a structure (wall) and hits a structure or the sub, it's successful
wasHit = true;
}
else
{
// If there is nothing between, the hit is successful
wasHit = structureBody == null;
}
}
break;
case HitDetection.Contact:
contactBodies.Clear();
if (damageTarget is Character targetCharacter)
{
foreach (Limb limb in targetCharacter.AnimController.Limbs)
{
if (!limb.IsSevered && limb.body?.FarseerBody != null) contactBodies.Add(limb.body.FarseerBody);
}
}
else if (damageTarget is Structure targetStructure)
{
if (character.Submarine == null && targetStructure.Submarine != null)
{
contactBodies.Add(targetStructure.Submarine.PhysicsBody.FarseerBody);
}
else
{
contactBodies.AddRange(targetStructure.Bodies);
}
}
else if (damageTarget is Item)
{
Item targetItem = damageTarget as Item;
if (targetItem.body?.FarseerBody != null) contactBodies.Add(targetItem.body.FarseerBody);
}
ContactEdge contactEdge = body.FarseerBody.ContactList;
while (contactEdge != null)
{
if (contactEdge.Contact != null &&
contactEdge.Contact.IsTouching &&
contactBodies.Any(b => b == contactEdge.Contact.FixtureA?.Body || b == contactEdge.Contact.FixtureB?.Body))
{
structureBody = contactBodies.LastOrDefault();
wasHit = true;
break;
}
contactEdge = contactEdge.Next;
}
break;
}
}
if (wasHit)
{
wasHit = damageTarget != null;
}
if (wasHit || attack.HitDetectionType == HitDetection.None)
{
if (character == Character.Controlled || GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
{
ExecuteAttack(damageTarget, targetLimb, out attackResult);
}
#if SERVER
GameMain.NetworkMember.CreateEntityEvent(character, new Character.ExecuteAttackEventData(
attackLimb: this, targetEntity: damageTarget, targetLimb: targetLimb,
targetSimPos: attackSimPos));
#endif
}
Vector2 diff = attackSimPos - SimPosition;
bool applyForces = !attack.ApplyForcesOnlyOnce || !wasRunning;
if (applyForces)
{
if (attack.ForceOnLimbIndices != null && attack.ForceOnLimbIndices.Count > 0)
{
foreach (int limbIndex in attack.ForceOnLimbIndices)
{
if (limbIndex < 0 || limbIndex >= character.AnimController.Limbs.Length) { continue; }
Limb limb = character.AnimController.Limbs[limbIndex];
if (limb.IsSevered) { continue; }
diff = attackSimPos - limb.SimPosition;
if (diff == Vector2.Zero) { continue; }
limb.body.ApplyTorque(limb.Mass * character.AnimController.Dir * attack.Torque * limb.Params.AttackForceMultiplier);
Vector2 forcePos = limb.pullJoint == null ? limb.body.SimPosition : limb.pullJoint.WorldAnchorA;
limb.body.ApplyLinearImpulse(limb.Mass * attack.Force * limb.Params.AttackForceMultiplier * Vector2.Normalize(diff), forcePos, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
}
}
else if (diff != Vector2.Zero)
{
body.ApplyTorque(Mass * character.AnimController.Dir * attack.Torque * Params.AttackForceMultiplier);
Vector2 forcePos = pullJoint == null ? body.SimPosition : pullJoint.WorldAnchorA;
body.ApplyLinearImpulse(Mass * attack.Force * Params.AttackForceMultiplier * Vector2.Normalize(diff), forcePos, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
}
}
Vector2 forceWorld = attack.CalculateAttackPhase(attack.RootTransitionEasing);
forceWorld.X *= character.AnimController.Dir;
character.AnimController.MainLimb.body.ApplyLinearImpulse(character.Mass * forceWorld, character.SimPosition, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
if (!attack.IsRunning && !attack.Ranged)
{
// Set the main collider where the body lands after the attack
if (Vector2.DistanceSquared(character.AnimController.Collider.SimPosition, character.AnimController.MainLimb.body.SimPosition) > 0.1f * 0.1f)
{
character.AnimController.Collider.SetTransformIgnoreContacts(character.AnimController.MainLimb.body.SimPosition, rotation: character.AnimController.Collider.Rotation);
}
}
return wasHit;
}
public void ExecuteAttack(IDamageable damageTarget, Limb targetLimb, out AttackResult attackResult)
{
bool playSound = false;
#if CLIENT
playSound = LastAttackSoundTime < Timing.TotalTime - SoundInterval;
if (playSound)
{
LastAttackSoundTime = SoundInterval;
}
#endif
attack.ResetDamageMultiplier();
attack.DamageMultiplier *= 1.0f + character.GetStatValue(attack.Ranged ? StatTypes.NaturalRangedAttackMultiplier : StatTypes.NaturalMeleeAttackMultiplier);
if (damageTarget is Character && targetLimb != null)
{
attackResult = attack.DoDamageToLimb(character, targetLimb, WorldPosition, deltaTime: 1.0f, playSound, body, sourceLimb: this);
}
else
{
if (damageTarget is Item targetItem && !targetItem.Prefab.DamagedByMonsters)
{
attackResult = new AttackResult();
}
else
{
attackResult = attack.DoDamage(character, damageTarget, WorldPosition, deltaTime: 1.0f, playSound, body, sourceLimb: this);
}
}
/*if (structureBody != null && attack.StickChance > Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient))
{
// TODO: use the hit pos?
var localFront = body.GetLocalFront(Params.GetSpriteOrientation());
var from = body.FarseerBody.GetWorldPoint(localFront);
var to = from;
var drawPos = body.DrawPosition;
StickTo(structureBody, from, to);
}*/
attack.ResetAttackTimer();
attack.SetCoolDown(applyRandom: !character.IsPlayer);
}
private WeldJoint attachJoint;
private WeldJoint colliderJoint;
public bool IsStuck => attachJoint != null;
/// <summary>
/// Attach the limb to a target with WeldJoints.
/// Uses sim units.
/// </summary>
private void StickTo(Body target, Vector2 from, Vector2 to)
{
if (attachJoint != null)
{
// Already attached to the target body, no need to do anything
if (attachJoint.BodyB == target) { return; }
Release();
}
if (!ragdoll.IsStuck)
{
PhysicsBody mainLimbBody = ragdoll.MainLimb.body;
Body colliderBody = ragdoll.Collider.FarseerBody;
Vector2 mainLimbLocalFront = mainLimbBody.GetLocalFront(ragdoll.MainLimb.Params.GetSpriteOrientation());
if (Dir < 0)
{
mainLimbLocalFront.X = -mainLimbLocalFront.X;
}
Vector2 mainLimbFront = mainLimbBody.FarseerBody.GetWorldPoint(mainLimbLocalFront);
colliderBody.SetTransform(mainLimbBody.SimPosition, mainLimbBody.Rotation);
// Attach the collider to the main body so that they don't go out of sync (TODO: why is the collider still rotated 90d off?)
colliderJoint = new WeldJoint(colliderBody, mainLimbBody.FarseerBody, mainLimbFront, mainLimbFront, true)
{
KinematicBodyB = true,
CollideConnected = false
};
GameMain.World.Add(colliderJoint);
}
attachJoint = new WeldJoint(body.FarseerBody, target, from, to, true)
{
FrequencyHz = 1,
DampingRatio = 0.5f,
KinematicBodyB = true,
CollideConnected = false
};
GameMain.World.Add(attachJoint);
}
public void Release()
{
if (!IsStuck) { return; }
GameMain.World.Remove(attachJoint);
attachJoint = null;
if (colliderJoint != null)
{
GameMain.World.Remove(colliderJoint);
colliderJoint = null;
}
}
// Thread-static to avoid concurrent modification in parallel item updates
[ThreadStatic]
private static List<ISerializableEntity> t_statusEffectTargets;
private static List<ISerializableEntity> StatusEffectTargets => t_statusEffectTargets ??= new List<ISerializableEntity>();
public void ApplyStatusEffects(ActionType actionType, float deltaTime)
{
if (!statusEffects.TryGetValue(actionType, out var statusEffectList)) { return; }
foreach (StatusEffect statusEffect in statusEffectList)
{
if (statusEffect.ShouldWaitForInterval(character, deltaTime)) { return; }
statusEffect.sourceBody = body;
if (statusEffect.type == ActionType.OnDamaged)
{
if (!statusEffect.HasRequiredAfflictions(character.LastDamage)) { continue; }
if (statusEffect.OnlyWhenDamagedByPlayer)
{
if (character.LastAttacker == null || !character.LastAttacker.IsPlayer)
{
continue;
}
}
}
if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
var targets = StatusEffectTargets;
targets.Clear();
statusEffect.AddNearbyTargets(WorldPosition, targets);
statusEffect.Apply(actionType, deltaTime, character, targets);
}
else if (statusEffect.targetLimbs != null)
{
foreach (var limbType in statusEffect.targetLimbs)
{
if (statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs))
{
// Target all matching limbs
foreach (var limb in ragdoll.Limbs)
{
if (limb.IsSevered) { continue; }
if (limb.type == limbType)
{
ApplyToLimb(actionType, deltaTime, statusEffect, character, limb);
}
}
}
else if (statusEffect.HasTargetType(StatusEffect.TargetType.Limb) || statusEffect.HasTargetType(StatusEffect.TargetType.Character) || statusEffect.HasTargetType(StatusEffect.TargetType.This))
{
// Target just the first matching limb
Limb limb = ragdoll.GetLimb(limbType);
if (limb != null)
{
ApplyToLimb(actionType, deltaTime, statusEffect, character, limb);
}
}
else if (statusEffect.HasTargetType(StatusEffect.TargetType.LastLimb))
{
// Target just the last matching limb
Limb limb = ragdoll.Limbs.LastOrDefault(l => l.type == limbType && !l.IsSevered && !l.Hidden);
if (limb != null)
{
ApplyToLimb(actionType, deltaTime, statusEffect, character, limb);
}
}
}
}
else if (statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs))
{
// Target all limbs
foreach (var limb in ragdoll.Limbs)
{
if (limb.IsSevered) { continue; }
ApplyToLimb(actionType, deltaTime, statusEffect, character, limb);
}
}
else if (statusEffect.HasTargetType(StatusEffect.TargetType.Character))
{
statusEffect.Apply(actionType, deltaTime, character, character, WorldPosition);
}
else if (statusEffect.HasTargetType(StatusEffect.TargetType.This) || statusEffect.HasTargetType(StatusEffect.TargetType.Limb))
{
ApplyToLimb(actionType, deltaTime, statusEffect, character, limb: this);
}
}
static void ApplyToLimb(ActionType actionType, float deltaTime, StatusEffect statusEffect, Character character, Limb limb)
{
statusEffect.sourceBody = limb.body;
statusEffect.Apply(actionType, deltaTime, entity: character, target: limb);
}
}
public float BlinkTimer { get; private set; }
public float BlinkPhase { get; set; }
public bool FreezeBlinkState;
private float TotalBlinkDurationOut => Params.BlinkDurationOut + Params.BlinkHoldTime;
public void Blink()
{
BlinkTimer = -TotalBlinkDurationOut;
}
public void UpdateBlink(float deltaTime, float referenceRotation)
{
if (BlinkTimer > -TotalBlinkDurationOut)
{
if (!FreezeBlinkState)
{
BlinkPhase -= deltaTime;
}
if (BlinkPhase > 0)
{
// in
float t = ToolBox.GetEasing(Params.BlinkTransitionIn, MathUtils.InverseLerp(1, 0, BlinkPhase / Params.BlinkDurationIn));
body.SmoothRotate(referenceRotation + MathHelper.ToRadians(Params.BlinkRotationIn) * Dir, Mass * Params.BlinkForce * t, wrapAngle: true);
if (Params.UseTextureOffsetForBlinking)
{
#if CLIENT
ActiveSprite.RelativeOrigin = Vector2.Lerp(Params.BlinkTextureOffsetOut, Params.BlinkTextureOffsetIn, t);
#endif
}
}
else
{
if (Math.Abs(BlinkPhase) < Params.BlinkHoldTime)
{
// hold
body.SmoothRotate(referenceRotation + MathHelper.ToRadians(Params.BlinkRotationIn) * Dir, Mass * Params.BlinkForce, wrapAngle: true);
}
else
{
// out
//float t = ToolBox.GetEasing(Params.BlinkTransitionOut, MathUtils.InverseLerp(0, 1, -blinkPhase / TotalBlinkDurationOut));
float t = ToolBox.GetEasing(Params.BlinkTransitionOut, MathUtils.InverseLerp(0, 1, (-BlinkPhase - Params.BlinkHoldTime) / Params.BlinkDurationOut));
body.SmoothRotate(referenceRotation + MathHelper.ToRadians(Params.BlinkRotationOut) * Dir, Mass * Params.BlinkForce * t, wrapAngle: true);
if (Params.UseTextureOffsetForBlinking)
{
#if CLIENT
ActiveSprite.RelativeOrigin = Vector2.Lerp(Params.BlinkTextureOffsetIn, Params.BlinkTextureOffsetOut, t);
#endif
}
}
}
}
else
{
// out
if (!FreezeBlinkState)
{
BlinkPhase = Params.BlinkDurationIn;
}
body.SmoothRotate(referenceRotation + MathHelper.ToRadians(Params.BlinkRotationOut) * Dir, Mass * Params.BlinkForce, wrapAngle: true);
}
}
public IEnumerable<LimbJoint> GetConnectedJoints() => ragdoll.LimbJoints.Where(j => !j.IsSevered && (j.LimbA == this || j.LimbB == this));
public IEnumerable<Limb> GetConnectedLimbs()
{
var connectedJoints = GetConnectedJoints();
var connectedLimbs = new HashSet<Limb>();
foreach (Limb limb in ragdoll.Limbs)
{
var otherJoints = limb.GetConnectedJoints();
foreach (LimbJoint connectedJoint in connectedJoints)
{
if (otherJoints.Contains(connectedJoint))
{
connectedLimbs.Add(limb);
}
}
}
return connectedLimbs;
}
public void Remove()
{
ragdoll.SubtractMass(this);
body?.Remove();
body = null;
if (pullJoint != null)
{
if (GameMain.World.JointList.Contains(pullJoint))
{
GameMain.World.Remove(pullJoint);
}
pullJoint = null;
}
Release();
RemoveProjSpecific();
Removed = true;
}
partial void RemoveProjSpecific();
public void LoadParams()
{
pullJoint.LocalAnchorA = ConvertUnits.ToSimUnits(Params.PullPos * Scale);
LoadParamsProjSpecific();
}
partial void LoadParamsProjSpecific();
}
class AbilityAfflictionCharacter : AbilityObject, IAbilityAffliction, IAbilityCharacter
{
public AbilityAfflictionCharacter(Affliction affliction, Character character)
{
Affliction = affliction;
Character = character;
}
public Character Character { get; set; }
public Affliction Affliction { get; set; }
}
class AbilityReduceAffliction : AbilityObject, IAbilityCharacter, IAbilityValue
{
public AbilityReduceAffliction(Character character, float value)
{
Character = character;
Value = value;
}
public Character Character { get; set; }
public float Value { get; set; }
}
}