Files
LuaCsForBarotraumaEP/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs
2026-04-30 21:59:54 +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)) { continue; }
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; }
}
}