1524 lines
60 KiB
C#
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; }
|
|
}
|
|
|
|
}
|