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; namespace Barotrauma { public enum LimbType { None, LeftHand, RightHand, LeftArm, RightArm, LeftForearm, RightForearm, LeftLeg, RightLeg, LeftFoot, RightFoot, Head, Torso, Tail, Legs, RightThigh, LeftThigh, Waist }; partial class LimbJoint : RevoluteJoint { public bool IsSevered; public bool CanBeSevered => jointParams.CanBeSevered; public readonly JointParams jointParams; public readonly Ragdoll ragdoll; public readonly Limb LimbA, LimbB; public LimbJoint(Limb limbA, Limb limbB, JointParams jointParams, Ragdoll ragdoll) : this(limbA, limbB, Vector2.One, Vector2.One) { this.jointParams = jointParams; this.ragdoll = ragdoll; LoadParams(); } public LimbJoint(Limb limbA, Limb limbB, Vector2 anchor1, Vector2 anchor2) : base(limbA.body.FarseerBody, limbB.body.FarseerBody, anchor1, anchor2) { CollideConnected = false; MotorEnabled = true; MaxMotorTorque = 0.25f; LimbA = limbA; LimbB = limbB; } public void SaveParams() { // Saving to the params is handled only in the params level. return; jointParams.Stiffness = MaxMotorTorque; if (ragdoll.IsFlipped) { jointParams.Limb1Anchor = ConvertUnits.ToDisplayUnits(new Vector2(-LocalAnchorA.X, LocalAnchorA.Y) / jointParams.Ragdoll.JointScale); jointParams.Limb2Anchor = ConvertUnits.ToDisplayUnits(new Vector2(-LocalAnchorB.X, LocalAnchorB.Y) / jointParams.Ragdoll.JointScale); jointParams.UpperLimit = MathHelper.ToDegrees(-LowerLimit); jointParams.LowerLimit = MathHelper.ToDegrees(-UpperLimit); } else { jointParams.Limb1Anchor = ConvertUnits.ToDisplayUnits(LocalAnchorA / jointParams.Ragdoll.JointScale); jointParams.Limb2Anchor = ConvertUnits.ToDisplayUnits(LocalAnchorB / jointParams.Ragdoll.JointScale); jointParams.UpperLimit = MathHelper.ToDegrees(UpperLimit); jointParams.LowerLimit = MathHelper.ToDegrees(LowerLimit); } } public void LoadParams() { MaxMotorTorque = jointParams.Stiffness; LimitEnabled = jointParams.LimitEnabled; if (float.IsNaN(jointParams.LowerLimit)) { jointParams.LowerLimit = 0; } if (float.IsNaN(jointParams.UpperLimit)) { jointParams.UpperLimit = 0; } if (ragdoll.IsFlipped) { LocalAnchorA = ConvertUnits.ToSimUnits(new Vector2(-jointParams.Limb1Anchor.X, jointParams.Limb1Anchor.Y) * jointParams.Ragdoll.JointScale); LocalAnchorB = ConvertUnits.ToSimUnits(new Vector2(-jointParams.Limb2Anchor.X, jointParams.Limb2Anchor.Y) * jointParams.Ragdoll.JointScale); UpperLimit = MathHelper.ToRadians(-jointParams.LowerLimit); LowerLimit = MathHelper.ToRadians(-jointParams.UpperLimit); } else { LocalAnchorA = ConvertUnits.ToSimUnits(jointParams.Limb1Anchor * jointParams.Ragdoll.JointScale); LocalAnchorB = ConvertUnits.ToSimUnits(jointParams.Limb2Anchor * jointParams.Ragdoll.JointScale); UpperLimit = MathHelper.ToRadians(jointParams.UpperLimit); LowerLimit = MathHelper.ToRadians(jointParams.LowerLimit); } } } partial class Limb : ISerializableEntity { // Note: not used private const float LimbDensity = 15; private const float LimbAngularDamping = 7; //how long it takes for severed limbs to fade out private const float SeveredFadeOutTime = 10.0f; public readonly Character character; /// /// Note that during the limb initialization, character.AnimController returns null, whereas this field is already assigned. /// public readonly Ragdoll ragdoll; public readonly LimbParams limbParams; //the physics body of the limb public PhysicsBody body; public Vector2 StepOffset => ConvertUnits.ToSimUnits(limbParams.StepOffset) * ragdoll.RagdollParams.JointScale; public bool inWater; private readonly FixedMouseJoint pullJoint; public readonly LimbType type; public readonly bool ignoreCollisions; private bool isSevered; private float severedFadeOutTimer; public Vector2? MouthPos; public readonly Attack attack; private List damageModifiers; private Direction dir; public int HealthIndex => limbParams.HealthIndex; public float Scale => limbParams.Ragdoll.LimbScale; public float AttackPriority => limbParams.AttackPriority; public bool DoesFlip => limbParams.Flip; public float SteerForce => limbParams.SteerForce; public Vector2 DebugTargetPos; public Vector2 DebugRefPos; public bool IsSevered { get { return isSevered; } set { isSevered = value; if (!isSevered) severedFadeOutTimer = 0.0f; #if CLIENT if (isSevered) damageOverlayStrength = 100.0f; #endif } } public Vector2 WorldPosition { get { return character.Submarine == null ? Position : Position + character.Submarine.Position; } } public Vector2 Position { get { return ConvertUnits.ToDisplayUnits(body.SimPosition); } } public Vector2 SimPosition { get { return body.SimPosition; } } public float Rotation { get { 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 { return body.Mass; } } public bool Disabled { get; set; } public Vector2 LinearVelocity { get { return body.LinearVelocity; } } public float Dir { get { return ((dir == Direction.Left) ? -1.0f : 1.0f); } set { dir = (value == -1.0f) ? Direction.Left : Direction.Right; } } public int RefJointIndex => limbParams.RefJoint; private List wearingItems; public List WearingItems { get { return wearingItems; } } public List OtherWearables { get; private set; } = new List(); 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; GameAnalyticsManager.AddErrorEventOnce("Limb.SetPullJointAnchorA:InvalidValue", GameAnalyticsSDK.Net.EGAErrorSeverity.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; GameAnalyticsManager.AddErrorEventOnce("Limb.SetPullJointAnchorA:ExcessiveValue", GameAnalyticsSDK.Net.EGAErrorSeverity.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; GameAnalyticsManager.AddErrorEventOnce("Limb.SetPullJointAnchorB:InvalidValue", GameAnalyticsSDK.Net.EGAErrorSeverity.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; GameAnalyticsManager.AddErrorEventOnce("Limb.SetPullJointAnchorB:ExcessiveValue", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); #if DEBUG DebugConsole.ThrowError(errorMsg); #endif return; } pullJoint.WorldAnchorB = value; } } public Vector2 PullJointLocalAnchorA { get { return pullJoint.LocalAnchorA; } } public string Name => limbParams.Name; public Dictionary SerializableProperties { get; private set; } public Limb(Ragdoll ragdoll, Character character, LimbParams limbParams) { this.ragdoll = ragdoll; this.character = character; this.limbParams = limbParams; wearingItems = new List(); dir = Direction.Right; body = new PhysicsBody(limbParams); type = limbParams.Type; if (limbParams.IgnoreCollisions) { body.CollisionCategories = Category.None; body.CollidesWith = Category.None; ignoreCollisions = true; } else { //limbs don't collide with each other body.CollisionCategories = Physics.CollisionCharacter; body.CollidesWith = Physics.CollisionAll & ~Physics.CollisionCharacter & ~Physics.CollisionItem & ~Physics.CollisionItemBlocking; } 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 }; GameMain.World.AddJoint(pullJoint); var element = limbParams.Element; if (element.Attribute("mouthpos") != null) { MouthPos = ConvertUnits.ToSimUnits(element.GetAttributeVector2("mouthpos", Vector2.Zero)); } body.BodyType = BodyType.Dynamic; body.FarseerBody.AngularDamping = LimbAngularDamping; damageModifiers = new List(); foreach (XElement 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); } break; case "damagemodifier": damageModifiers.Add(new DamageModifier(subElement, character.Name)); break; } } SerializableProperties = SerializableProperty.GetProperties(this); InitProjSpecific(element); } partial void InitProjSpecific(XElement 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 afflictions = new List(); 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); } public AttackResult AddDamage(Vector2 simPosition, List afflictions, bool playSound) { List appliedDamageModifiers = new List(); //create a copy of the original affliction list to prevent modifying the afflictions of an Attack/StatusEffect etc afflictions = new List(afflictions); for (int i = 0; i < afflictions.Count; i++) { foreach (DamageModifier damageModifier in damageModifiers) { if (!damageModifier.MatchesAffliction(afflictions[i])) continue; if (SectorHit(damageModifier.ArmorSector, simPosition)) { afflictions[i] = afflictions[i].CreateMultiplied(damageModifier.DamageMultiplier); appliedDamageModifiers.Add(damageModifier); } } foreach (WearableSprite wearable in wearingItems) { foreach (DamageModifier damageModifier in wearable.WearableComponent.DamageModifiers) { if (!damageModifier.MatchesAffliction(afflictions[i])) continue; if (SectorHit(damageModifier.ArmorSector, simPosition)) { afflictions[i] = afflictions[i].CreateMultiplied(damageModifier.DamageMultiplier); appliedDamageModifiers.Add(damageModifier); } } } } AddDamageProjSpecific(simPosition, afflictions, playSound, appliedDamageModifiers); return new AttackResult(afflictions, this, appliedDamageModifiers); } partial void AddDamageProjSpecific(Vector2 simPosition, List afflictions, bool playSound, List appliedDamageModifiers); public bool SectorHit(Vector2 armorSector, Vector2 simPosition) { if (armorSector == Vector2.Zero) { return false; } float rotation = body.TransformedRotation; float offset = (MathHelper.PiOver2 - GetArmorSectorRotationOffset(armorSector)) * Dir; float hitAngle = VectorExtensions.Angle(VectorExtensions.Forward(rotation + offset), SimPosition - simPosition); float sectorSize = GetArmorSectorSize(armorSector); return hitAngle < sectorSize / 2; } protected float GetArmorSectorRotationOffset(Vector2 armorSector) { float midAngle = MathUtils.GetMidAngle(armorSector.X, armorSector.Y); float spritesheetOrientation = MathHelper.ToRadians(limbParams.Ragdoll.SpritesheetOrientation); return midAngle + spritesheetOrientation; } protected float GetArmorSectorSize(Vector2 armorSector) { float min = Math.Min(armorSector.X, armorSector.Y); float max = Math.Max(armorSector.X, armorSector.Y); return max - min; } public void Update(float deltaTime) { UpdateProjSpecific(deltaTime); if (inWater) { body.ApplyWaterForces(); } if (isSevered) { severedFadeOutTimer += deltaTime; if (severedFadeOutTimer > SeveredFadeOutTime) { body.Enabled = false; } } if (attack != null) { attack.UpdateCoolDown(deltaTime); } } partial void UpdateProjSpecific(float deltaTime); /// /// Returns true if the attack successfully hit something. If the distance is not given, it will be calculated. /// public bool UpdateAttack(float deltaTime, Vector2 attackSimPos, IDamageable damageTarget, out AttackResult attackResult, float distance = -1) { attackResult = default(AttackResult); float dist = distance > -1 ? distance : ConvertUnits.ToDisplayUnits(Vector2.Distance(SimPosition, attackSimPos)); bool wasRunning = attack.IsRunning; attack.UpdateAttackTimer(deltaTime); bool wasHit = false; Body structureBody = null; if (damageTarget != null) { switch (attack.HitDetectionType) { case HitDetection.Distance: if (dist < attack.DamageRange) { List ignoredBodies = character.AnimController.Limbs.Select(l => l.body.FarseerBody).ToList(); ignoredBodies.Add(character.AnimController.Collider.FarseerBody); structureBody = Submarine.PickBody( SimPosition, attackSimPos, ignoredBodies, Physics.CollisionWall); if (damageTarget is Item) { // If the attack is aimed to an item and hits an item, it's successful. // Ignore blocking on items, 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 the attack is aimed to a character but hits a structure, the hit is blocked. wasHit = structureBody == null; } } break; case HitDetection.Contact: var targetBodies = new List(); if (damageTarget is Character targetCharacter) { foreach (Limb limb in targetCharacter.AnimController.Limbs) { if (!limb.IsSevered && limb.body?.FarseerBody != null) targetBodies.Add(limb.body.FarseerBody); } } else if (damageTarget is Structure targetStructure) { if (character.Submarine == null && targetStructure.Submarine != null) { targetBodies.Add(targetStructure.Submarine.PhysicsBody.FarseerBody); } else { targetBodies.AddRange(targetStructure.Bodies); } } else if (damageTarget is Item) { Item targetItem = damageTarget as Item; if (targetItem.body?.FarseerBody != null) targetBodies.Add(targetItem.body.FarseerBody); } if (targetBodies != null) { ContactEdge contactEdge = body.FarseerBody.ContactList; while (contactEdge != null) { if (contactEdge.Contact != null && contactEdge.Contact.IsTouching && targetBodies.Any(b => b == contactEdge.Contact.FixtureA?.Body || b == contactEdge.Contact.FixtureB?.Body)) { structureBody = targetBodies.LastOrDefault(); wasHit = true; break; } contactEdge = contactEdge.Next; } } break; } } if (wasHit) { wasHit = damageTarget != null; } if (wasHit) { bool playSound = false; #if CLIENT playSound = LastAttackSoundTime < Timing.TotalTime - SoundInterval; if (playSound) { LastAttackSoundTime = SoundInterval; } #endif attackResult = attack.DoDamage(character, damageTarget, WorldPosition, 1.0f, playSound); if (structureBody != null && attack.StickChance > Rand.Range(0.0f, 1.0f, Rand.RandSync.Server)) { // TODO: use the hit pos? var localFront = body.GetLocalFront(MathHelper.ToRadians(ragdoll.RagdollParams.SpritesheetOrientation)); var from = body.FarseerBody.GetWorldPoint(localFront); var to = from; var drawPos = body.DrawPosition; StickTo(structureBody, from, to); } attack.ResetAttackTimer(); attack.SetCoolDown(); } Vector2 diff = attackSimPos - SimPosition; bool applyForces = (!attack.ApplyForcesOnlyOnce || !wasRunning) && diff.LengthSquared() > 0.00001f; if (applyForces) { body.ApplyTorque(Mass * character.AnimController.Dir * attack.Torque); 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]; Vector2 forcePos = limb.pullJoint == null ? limb.body.SimPosition : limb.pullJoint.WorldAnchorA; limb.body.ApplyLinearImpulse(limb.Mass * attack.Force * Vector2.Normalize(attackSimPos - SimPosition), forcePos); } } else { Vector2 forcePos = pullJoint == null ? body.SimPosition : pullJoint.WorldAnchorA; body.ApplyLinearImpulse(Mass * attack.Force * Vector2.Normalize(attackSimPos - SimPosition), forcePos); } } return wasHit; } private WeldJoint attachJoint; private WeldJoint colliderJoint; public bool IsStuck => attachJoint != null; /// /// Attach the limb to a target with WeldJoints. /// Uses sim units. /// 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(MathHelper.ToRadians(ragdoll.RagdollParams.SpritesheetOrientation)); 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.AddJoint(colliderJoint); } attachJoint = new WeldJoint(body.FarseerBody, target, from, to, true) { FrequencyHz = 1, DampingRatio = 0.5f, KinematicBodyB = true, CollideConnected = false }; GameMain.World.AddJoint(attachJoint); } public void Release() { if (!IsStuck) { return; } GameMain.World.RemoveJoint(attachJoint); attachJoint = null; if (colliderJoint != null) { GameMain.World.RemoveJoint(colliderJoint); colliderJoint = null; } } public void Remove() { body?.Remove(); body = null; Release(); RemoveProjSpecific(); } partial void RemoveProjSpecific(); public void LoadParams() { attack?.Deserialize(); pullJoint.LocalAnchorA = ConvertUnits.ToSimUnits(limbParams.PullPos * Scale); LoadParamsProjSpecific(); } partial void LoadParamsProjSpecific(); } }