Files
LuaCsForBarotraumaEP/Barotrauma/BarotraumaShared/Source/Characters/Limb.cs
2019-03-27 20:52:47 +02:00

715 lines
29 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;
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;
/// <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 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<DamageModifier> 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<WearableSprite> wearingItems;
public List<WearableSprite> WearingItems
{
get { return wearingItems; }
}
public List<WearableSprite> OtherWearables { get; private set; } = 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;
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<string, SerializableProperty> SerializableProperties
{
get;
private set;
}
public Limb(Ragdoll ragdoll, Character character, LimbParams limbParams)
{
this.ragdoll = ragdoll;
this.character = character;
this.limbParams = limbParams;
wearingItems = new List<WearableSprite>();
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<DamageModifier>();
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<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);
}
public AttackResult AddDamage(Vector2 simPosition, List<Affliction> afflictions, bool playSound)
{
List<DamageModifier> appliedDamageModifiers = new List<DamageModifier>();
//create a copy of the original affliction list to prevent modifying the afflictions of an Attack/StatusEffect etc
afflictions = new List<Affliction>(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<Affliction> afflictions, bool playSound, List<DamageModifier> 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);
/// <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)
{
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<Body> 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<Body>();
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;
/// <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(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();
}
}