Files
LuaCsForBarotraumaEP/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs
2025-03-12 12:56:27 +00:00

570 lines
25 KiB
C#

using Barotrauma.Extensions;
using Barotrauma.Networking;
using FarseerPhysics;
using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
namespace Barotrauma.Items.Components
{
partial class Rope : ItemComponent, IServerSerializable
{
private ISpatialEntity source;
private Item target;
private Vector2? launchDir;
private float currentRopeLength;
private void SetSource(ISpatialEntity source)
{
this.source = source;
if (source is Limb sourceLimb)
{
sourceLimb.AttachedRope = this;
float offset = sourceLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2;
launchDir = VectorExtensions.Forward(sourceLimb.body.TransformedRotation - offset * sourceLimb.character.AnimController.Dir);
}
}
private void ResetSource()
{
if (source is Limb sourceLimb && sourceLimb.AttachedRope == this)
{
sourceLimb.AttachedRope = null;
}
source = null;
}
private float snapTimer;
[Serialize(1.0f, IsPropertySaveable.No, description: "")]
public float SnapAnimDuration
{
get;
set;
}
private float raycastTimer;
private const float RayCastInterval = 0.2f;
[Serialize(0.0f, IsPropertySaveable.No, description: "How much force is applied to pull the projectile the rope is attached to.")]
public float ProjectilePullForce
{
get;
set;
}
[Serialize(0.0f, IsPropertySaveable.No, description: "How much force is applied to pull the target the rope is attached to.")]
public float TargetPullForce
{
get;
set;
}
[Serialize(0.0f, IsPropertySaveable.No, description: "How much force is applied to pull the source the rope is attached to.")]
public float SourcePullForce
{
get;
set;
}
[Serialize(1000.0f, IsPropertySaveable.No, description: "How far the source item can be from the projectile until the rope breaks.")]
public float MaxLength
{
get;
set;
}
[Serialize(200.0f, IsPropertySaveable.No, description: "At which distance the user stops pulling the target?")]
public float MinPullDistance
{
get;
set;
}
[Serialize(360.0f, IsPropertySaveable.No, description: "The maximum angle from the source to the target until the rope breaks.")]
public float MaxAngle
{
get;
set;
}
[Serialize(true, IsPropertySaveable.No, description: "Should the rope snap when it collides with a structure/submarine (if not, it will just go through it).")]
public bool SnapOnCollision
{
get;
set;
}
[Serialize(true, IsPropertySaveable.No, description: "Should the rope snap when the character drops the aim?")]
public bool SnapWhenNotAimed
{
get;
set;
}
[Serialize(true, IsPropertySaveable.No, description: "Should the rope snap when the weapon it was fired from is fired again? I.e. can there be multiple ropes coming from the weapon at the same time?")]
public bool SnapWhenWeaponFiredAgain
{
get;
set;
}
[Serialize(0.9f, IsPropertySaveable.No, description: "Multiplier for the length of the barrel when determining where the rope should start from.")]
public float BarrelLengthMultiplier
{
get;
set;
}
[Serialize(30.0f, IsPropertySaveable.No, description: "How much mass is required for the target to pull the source towards it. Static and kinematic targets are always treated heavy enough.")]
public float TargetMinMass
{
get;
set;
}
[Serialize(false, IsPropertySaveable.No)]
public bool LerpForces
{
get;
set;
}
[Serialize(true, IsPropertySaveable.No, description: "Should the force be dynamically adjusted to make it more difficult for targets to escape the pull?")]
public bool IncreaseForceForEscapingTargets
{
get;
set;
}
private bool isReelingIn;
private bool snapped;
public bool Snapped
{
get { return snapped; }
set
{
if (snapped == value) { return; }
if (GameMain.NetworkMember != null)
{
if (GameMain.NetworkMember.IsClient)
{
return;
}
else
{
#if SERVER
item.CreateServerEvent(this);
#endif
}
}
snapped = value;
if (!snapped)
{
snapTimer = 0;
}
else if (target != null && source != null && target != source)
{
#if CLIENT
// Play a sound at both ends. Initially tested playing the sound in the middle when the rope snaps in the middle,
// but I think it's more important to ensure that the players hear the sound.
PlaySound(snapSound, source.WorldPosition);
PlaySound(snapSound, target.WorldPosition);
#endif
}
}
}
public Rope(Item item, ContentXElement element) : base(item, element)
{
InitProjSpecific(element);
}
partial void InitProjSpecific(ContentXElement element);
public void Snap() => Snapped = true;
public void Attach(ISpatialEntity source, Item target)
{
System.Diagnostics.Debug.Assert(source != null);
System.Diagnostics.Debug.Assert(target != null);
this.target = target;
SetSource(source);
Snapped = false;
ApplyStatusEffects(ActionType.OnUse, 1.0f, worldPosition: item.WorldPosition);
IsActive = true;
}
public override void Update(float deltaTime, Camera cam)
{
UpdateProjSpecific();
isReelingIn = false;
Character user = item.GetComponent<Projectile>()?.User;
if (source == null || target == null || target.Removed ||
source is Entity { Removed: true } ||
source is Limb { Removed: true } ||
user is { Removed: true })
{
ResetSource();
target = null;
IsActive = false;
return;
}
if (Snapped)
{
snapTimer += deltaTime;
if (snapTimer >= SnapAnimDuration)
{
IsActive = false;
}
return;
}
Vector2 diff = target.WorldPosition - GetSourcePos(useDrawPosition: false);
float lengthSqr = diff.LengthSquared();
if (lengthSqr > MaxLength * MaxLength)
{
Snap();
return;
}
if (MaxAngle < 180 && lengthSqr > 2500)
{
launchDir ??= diff;
float angle = MathHelper.ToDegrees(launchDir.Value.Angle(diff));
if (angle > MaxAngle)
{
Snap();
return;
}
}
#if CLIENT
item.ResetCachedVisibleSize();
#endif
var projectile = target.GetComponent<Projectile>();
if (projectile == null) { return; }
if (SnapOnCollision)
{
raycastTimer += deltaTime;
if (raycastTimer > RayCastInterval)
{
if (Submarine.PickBody(ConvertUnits.ToSimUnits(source.WorldPosition), ConvertUnits.ToSimUnits(target.WorldPosition),
collisionCategory: Physics.CollisionLevel | Physics.CollisionWall,
customPredicate: (Fixture f) =>
{
foreach (Body body in projectile.Hits)
{
Submarine alreadyHitSub = null;
if (body.UserData is Structure hitStructure)
{
alreadyHitSub = hitStructure.Submarine;
}
else if (body.UserData is Submarine hitSub)
{
alreadyHitSub = hitSub;
}
if (alreadyHitSub != null)
{
if (f.Body?.UserData is MapEntity me && me.Submarine == alreadyHitSub) { return false; }
if (f.Body?.UserData as Submarine == alreadyHitSub) { return false; }
}
}
Submarine targetSub = projectile.StickTarget?.UserData as Submarine ?? target.Submarine;
if (f.Body?.UserData is MapEntity mapEntity && mapEntity.Submarine != null)
{
if (mapEntity.Submarine == targetSub || mapEntity.Submarine == source.Submarine)
{
return false;
}
}
else if (f.Body?.UserData is Submarine sub)
{
if (sub == targetSub || sub == source.Submarine)
{
return false;
}
}
return true;
}) != null)
{
Snap();
return;
}
raycastTimer = 0.0f;
}
}
Vector2 forceDir = diff;
currentRopeLength = diff.Length();
if (currentRopeLength > 0.001f)
{
forceDir = Vector2.Normalize(forceDir);
}
if (Math.Abs(ProjectilePullForce) > 0.001f)
{
projectile.Item?.body?.ApplyForce(-forceDir * ProjectilePullForce);
}
if (projectile.StickTarget != null)
{
float targetMass = float.MaxValue;
Character targetCharacter = null;
switch (projectile.StickTarget.UserData)
{
case Limb targetLimb:
targetCharacter = targetLimb.character;
targetMass = targetLimb.ragdoll.Mass;
break;
case Character character:
targetCharacter = character;
targetMass = character.Mass;
break;
case Item _:
targetMass = projectile.StickTarget.Mass;
break;
}
if (projectile.StickTarget.BodyType != BodyType.Dynamic)
{
targetMass = float.MaxValue;
}
// Currently can only apply pull forces to the source, when it's a character, not e.g. when the item would be auto-operated by an AI. Might have to change this.
if (user != null)
{
if (!snapped &&
//user can only hold on to the rope if it was launched from a holdable item, or by something else than an item (limb?)
(projectile.Launcher == null || projectile.Launcher.GetComponent<Holdable>() != null))
{
user.AnimController.HoldToRope();
if (targetCharacter != null)
{
targetCharacter.AnimController.DragWithRope();
}
if (user.InWater)
{
user.AnimController.HangWithRope();
}
}
if (Math.Abs(SourcePullForce) > 0.001f && targetMass > TargetMinMass)
{
// This should be the main collider.
var sourceBody = GetBodyToPull(source);
if (sourceBody != null)
{
PhysicsBody targetBody = GetBodyToPull(target);
if (sourceBody.UserData is Character)
{
isReelingIn = user.InWater && user.IsRagdolled || !user.InWater && targetCharacter is { IsIncapacitated: false };
if (isReelingIn)
{
float pullForce = SourcePullForce;
if (!user.InWater)
{
// Apply a tiny amount to the character holding the rope, so that the connection "feels" more real.
pullForce *= 0.1f;
}
float lengthFactor = MathUtils.InverseLerp(0, MaxLength / 2, currentRopeLength);
float force = LerpForces ? MathHelper.Lerp(0, pullForce, lengthFactor) : pullForce;
sourceBody.ApplyForce(forceDir * force);
// Take the target velocity into account.
if (targetBody != null)
{
if (targetCharacter != null)
{
if (targetBody.LinearVelocity != Vector2.Zero && sourceBody.LinearVelocity != Vector2.Zero)
{
Vector2 targetDir = Vector2.Normalize(targetBody.LinearVelocity);
float movementDot = Vector2.Dot(Vector2.Normalize(sourceBody.LinearVelocity), targetDir);
if (movementDot < 0)
{
// Pushing to a different dir -> add some counter force
const float multiplier = 5;
float inverseLengthFactor = MathHelper.Lerp(1, 0, lengthFactor);
sourceBody.ApplyForce(targetBody.LinearVelocity * Math.Min(targetBody.Mass * multiplier, 250) * sourceBody.Mass * -movementDot * inverseLengthFactor);
}
float forceDot = Vector2.Dot(forceDir, targetDir);
if (forceDot > 0)
{
// Pulling to the same dir -> add extra force
float targetSpeed = targetBody.LinearVelocity.Length();
const float multiplier = 25;
sourceBody.ApplyForce(forceDir * targetSpeed * sourceBody.Mass * multiplier * forceDot * lengthFactor);
}
float colliderMainLimbDistance = Vector2.Distance(sourceBody.SimPosition, user.AnimController.MainLimb.SimPosition);
const float minDist = 1;
const float maxDist = 10;
if (colliderMainLimbDistance > minDist && sourceBody.UserData is not Submarine)
{
// Move the ragdoll closer to the collider, if it's too far (the correction force in HumanAnimController is not enough -> the ragdoll would lag behind and get teleported).
float correctionForce = MathHelper.Lerp(10.0f, NetConfig.MaxPhysicsBodyVelocity, MathUtils.InverseLerp(minDist, maxDist, colliderMainLimbDistance));
Vector2 targetPos = sourceBody.SimPosition + new Vector2((float)Math.Sin(-sourceBody.Rotation), (float)Math.Cos(-sourceBody.Rotation)) * 0.4f;
user.AnimController.MainLimb.MoveToPos(targetPos, correctionForce);
}
}
}
else
{
sourceBody.ApplyForce(targetBody.LinearVelocity * sourceBody.Mass);
}
}
}
}
else
{
float distance = Vector2.Distance(source.WorldPosition, target.WorldPosition);
float force = LerpForces ? MathHelper.Lerp(0, SourcePullForce, MathUtils.InverseLerp(0, MaxLength / 2, distance)) : SourcePullForce;
sourceBody.ApplyForce(forceDir * force);
}
}
}
}
if (Math.Abs(TargetPullForce) > 0.001f && user is not { IsRagdolled: true})
{
PhysicsBody targetBody = GetBodyToPull(target);
if (targetBody == null) { return; }
bool lerpForces = LerpForces;
float maxVelocity = NetConfig.MaxPhysicsBodyVelocity * 0.25f;
// The distance where we start pulling with max force.
float maxPullDistance = MaxLength / 3;
float minPullDistance = MinPullDistance;
const float absoluteMinPullDistance = 50;
if (targetCharacter != null)
{
if (targetCharacter.IsRagdolled || targetCharacter.IsUnconscious)
{
if (!targetCharacter.InWater)
{
// Limits the velocity of ragdolled characters on ground/air, because otherwise they tend to move with too high forces.
maxVelocity = NetConfig.MaxPhysicsBodyVelocity * 0.075f;
}
}
else
{
// Target alive and kicking -> Use the absolute min pull distance and full forces to pull.
// Keep some lerping, because it results into smoothing when the target is close by.
minPullDistance = absoluteMinPullDistance;
maxPullDistance = 200;
}
}
minPullDistance = MathHelper.Max(minPullDistance, absoluteMinPullDistance);
if (currentRopeLength < minPullDistance) { return; }
maxPullDistance = MathHelper.Max(minPullDistance * 2, maxPullDistance);
float force = lerpForces
? MathHelper.Lerp(0, TargetPullForce, MathUtils.InverseLerp(minPullDistance, maxPullDistance, currentRopeLength))
: TargetPullForce;
targetBody.ApplyForce(-forceDir * force, maxVelocity);
AnimController targetRagdoll = targetCharacter?.AnimController;
if (targetRagdoll?.Collider != null)
{
isReelingIn = true;
if (targetRagdoll.InWater || targetRagdoll.OnGround)
{
float forceMultiplier = 1;
if (!targetCharacter.IsRagdolled && !targetCharacter.IsIncapacitated && IncreaseForceForEscapingTargets)
{
// Pulling the main collider requires higher forces when the target is trying to move away.
Vector2 targetMovement = targetCharacter.AnimController.TargetMovement;
float dot = Vector2.Dot(Vector2.Normalize(targetMovement), forceDir);
if (dot > 0)
{
const float constMultiplier = 2.5f;
float targetVelocity = targetMovement.Length();
float massFactor = Math.Max((float)Math.Log(targetCharacter.Mass / 10), 1);
forceMultiplier = Math.Max(targetVelocity * massFactor * constMultiplier * dot, 1);
}
}
targetRagdoll.Collider.ApplyForce(-forceDir * force * forceMultiplier, maxVelocity);
}
}
}
}
}
partial void UpdateProjSpecific();
public override void UpdateBroken(float deltaTime, Camera cam)
{
base.UpdateBroken(deltaTime, cam);
if (Snapped)
{
snapTimer += deltaTime;
if (snapTimer >= SnapAnimDuration)
{
IsActive = false;
}
}
}
/// <summary>
/// Get the position the rope starts from (taking into account barrel positions if needed)
/// </summary>
/// <param name="useDrawPosition">Should the interpolated draw position be used? If not, the WorldPosition is used.</param>
private Vector2 GetSourcePos(bool useDrawPosition = false)
{
Vector2 sourcePos = source.WorldPosition;
if (source is Item sourceItem)
{
if (useDrawPosition)
{
sourcePos = sourceItem.DrawPosition;
}
if (!sourceItem.Removed)
{
if (sourceItem.GetComponent<Turret>() is { } turret)
{
sourcePos = new Vector2(sourceItem.WorldRect.X + turret.TransformedBarrelPos.X, sourceItem.WorldRect.Y - turret.TransformedBarrelPos.Y);
}
else if (sourceItem.GetComponent<RangedWeapon>() is { } weapon)
{
sourcePos += ConvertUnits.ToDisplayUnits(weapon.TransformedBarrelPos);
}
}
}
else if (useDrawPosition && source is Limb sourceLimb && sourceLimb.body != null)
{
sourcePos = sourceLimb.body.DrawPosition;
}
return sourcePos;
}
private static PhysicsBody GetBodyToPull(ISpatialEntity target)
{
if (target is Item targetItem)
{
if (targetItem.ParentInventory is CharacterInventory { Owner: Character ownerCharacter })
{
if (ownerCharacter.Removed) { return null; }
return ownerCharacter.AnimController.Collider;
}
var projectile = targetItem.GetComponent<Projectile>();
if (projectile is { StickTarget: not null })
{
return projectile.StickTarget.UserData switch
{
Structure structure => structure.Submarine?.PhysicsBody,
Submarine sub => sub.PhysicsBody,
Item item => item.body,
Limb limb => limb.body,
_ => null
};
}
if (targetItem.body != null) { return targetItem.body; }
if (targetItem.StaticFixtures.Any() && targetItem.Submarine != null) { return targetItem.Submarine.PhysicsBody; }
}
else if (target is Limb targetLimb)
{
return targetLimb.body;
}
return null;
}
}
}