using Barotrauma.Networking; using FarseerPhysics; using Microsoft.Xna.Framework; using System; using System.Linq; using Barotrauma.Extensions; namespace Barotrauma { class FishAnimController : AnimController { public override RagdollParams RagdollParams { get { return FishRagdollParams; } protected set { FishRagdollParams = value as FishRagdollParams; } } private FishRagdollParams _ragdollParams; public FishRagdollParams FishRagdollParams { get { if (_ragdollParams == null) { _ragdollParams = FishRagdollParams.GetDefaultRagdollParams(character); } return _ragdollParams; } protected set { _ragdollParams = value; } } private FishWalkParams _fishWalkParams; public FishWalkParams FishWalkParams { get { if (_fishWalkParams == null) { _fishWalkParams = FishWalkParams.GetDefaultAnimParams(character); } return _fishWalkParams; } set { _fishWalkParams = value; } } private FishRunParams _fishRunParams; public FishRunParams FishRunParams { get { if (_fishRunParams == null) { _fishRunParams = FishRunParams.GetDefaultAnimParams(character); } return _fishRunParams; } set { _fishRunParams = value; } } private FishSwimSlowParams _fishSwimSlowParams; public FishSwimSlowParams FishSwimSlowParams { get { if (_fishSwimSlowParams == null) { _fishSwimSlowParams = FishSwimSlowParams.GetDefaultAnimParams(character); } return _fishSwimSlowParams; } set { _fishSwimSlowParams = value; } } private FishSwimFastParams _fishSwimFastParams; public FishSwimFastParams FishSwimFastParams { get { if (_fishSwimFastParams == null) { _fishSwimFastParams = FishSwimFastParams.GetDefaultAnimParams(character); } return _fishSwimFastParams; } set { _fishSwimFastParams = value; } } public IFishAnimation CurrentFishAnimation => CurrentAnimationParams as IFishAnimation; public new FishGroundedParams CurrentGroundedParams => base.CurrentGroundedParams as FishGroundedParams; public new FishSwimParams CurrentSwimParams => base.CurrentSwimParams as FishSwimParams; public float? TailAngle => GetValidOrNull(CurrentAnimationParams, CurrentFishAnimation?.TailAngleInRadians); public float FootTorque => CurrentAnimationParams.FootTorque; public float HeadTorque => CurrentAnimationParams.HeadTorque; public float TorsoTorque => CurrentAnimationParams.TorsoTorque; public float TailTorque => CurrentFishAnimation.TailTorque; public float HeadMoveForce => CurrentGroundedParams.HeadMoveForce; public float TorsoMoveForce => CurrentGroundedParams.TorsoMoveForce; public float FootMoveForce => CurrentGroundedParams.FootMoveForce; public override GroundedMovementParams WalkParams { get { return FishWalkParams; } set { FishWalkParams = value as FishWalkParams; } } public override GroundedMovementParams RunParams { get { return FishRunParams; } set { FishRunParams = value as FishRunParams; } } public override SwimParams SwimSlowParams { get { return FishSwimSlowParams; } set { FishSwimSlowParams = value as FishSwimSlowParams; } } public override SwimParams SwimFastParams { get { return FishSwimFastParams; } set { FishSwimFastParams = value as FishSwimFastParams; } } private float flipTimer, flipCooldown; public FishAnimController(Character character, string seed, FishRagdollParams ragdollParams = null) : base(character, seed, ragdollParams) { } protected override void UpdateAnim(float deltaTime) { //wait a bit for the ragdoll to "settle" (for joints to force the limbs to appropriate positions) before starting to animate if (Timing.TotalTime - character.SpawnTime < 0.1f) { return; } if (Frozen) { return; } if (MainLimb == null) { ResetState(); return; } UpdateConstantTorque(deltaTime); UpdateBlink(deltaTime); var mainLimb = MainLimb; levitatingCollider = !IsHangingWithRope && !IsClimbing; if (!character.CanMove) { UpdateRagdollControlsMovement(); if (character.IsDead && deathAnimTimer < deathAnimDuration) { deathAnimTimer += deltaTime; UpdateDying(deltaTime); } else if (!InWater && !CanWalk && character.AllowInput) { //cannot walk but on dry land -> wiggle around UpdateDying(deltaTime); } ResetState(); return; } else { deathAnimTimer = 0.0f; } //re-enable collider if (!Collider.Enabled) { var lowestLimb = FindLowestLimb(); if (InWater) { Collider.SetTransformIgnoreContacts(new Vector2(Collider.SimPosition.X, MainLimb.SimPosition.Y), 0.0f); } else { Collider.SetTransformIgnoreContacts(new Vector2( Collider.SimPosition.X, Math.Max(lowestLimb.SimPosition.Y + (Collider.Radius + Collider.Height / 2), Collider.SimPosition.Y)), 0.0f); } Collider.Enabled = true; } ResetPullJoints(); if (strongestImpact > 0.0f) { character.Stun = MathHelper.Clamp(strongestImpact * 0.5f, character.Stun, 5.0f); strongestImpact = 0.0f; } if (Aiming) { TargetMovement = TargetMovement.ClampLength(2); } if (IsClimbing) { UpdateClimbing(); } if (inWater && !forceStanding) { Collider.FarseerBody.FixedRotation = false; UpdateSineAnim(deltaTime); } else if (RagdollParams.CanWalk && (currentHull != null || forceStanding)) { if (CurrentGroundedParams != null) { //rotate collider back upright float standAngle = CurrentGroundedParams.ColliderStandAngleInRadians * Dir; if (Math.Abs(MathUtils.GetShortestAngle(Collider.Rotation, standAngle)) > 0.001f) { Collider.AngularVelocity = MathUtils.GetShortestAngle(Collider.Rotation, standAngle) * 60.0f; Collider.FarseerBody.FixedRotation = false; } else { Collider.FarseerBody.FixedRotation = true; } } UpdateWalkAnim(deltaTime); } if (character.SelectedCharacter != null) { DragCharacter(character.SelectedCharacter, deltaTime); ResetState(); return; } if (character.AnimController.AnimationTestPose) { ApplyTestPose(); } //don't flip when simply physics is enabled if (SimplePhysicsEnabled) { ResetState(); return; } if (!character.IsRemotelyControlled && (character.AIController == null || character.AIController.CanFlip) && !Aiming) { if (!inWater || (CurrentSwimParams != null && CurrentSwimParams.Mirror)) { if (targetMovement.X > 0.1f && targetMovement.X > Math.Abs(targetMovement.Y) * 0.2f) { TargetDir = Direction.Right; } else if (targetMovement.X < -0.1f && targetMovement.X < -Math.Abs(targetMovement.Y) * 0.2f) { TargetDir = Direction.Left; } } else { float rotation = MathHelper.WrapAngle(Collider.Rotation); rotation = MathHelper.ToDegrees(rotation); if (rotation < 0.0f) { rotation += 360; } if (rotation > 20 && rotation < 160) { TargetDir = Direction.Left; } else if (rotation > 200 && rotation < 340) { TargetDir = Direction.Right; } } } if (!IsStuck && CurrentFishAnimation.Flip && character.AIController is not { CanFlip: false }) { flipCooldown -= deltaTime; if (TargetDir != Direction.None && TargetDir != dir) { flipTimer += deltaTime; // Speed reductions are not taken into account here. It's intentional: an ai character cannot flip if it's heavily paralyzed (for example). float requiredSpeed = CurrentAnimationParams.MovementSpeed / 2; if (CurrentHull != null) { // Enemy movement speeds are halved inside submarines requiredSpeed /= 2; } bool isMovingFastEnough = Math.Abs(MainLimb.LinearVelocity.X) > requiredSpeed; bool isTryingToMoveHorizontally = Math.Abs(TargetMovement.X) > Math.Abs(TargetMovement.Y); if ((flipTimer > CurrentFishAnimation.FlipDelay && flipCooldown <= 0.0f && ((isMovingFastEnough && isTryingToMoveHorizontally) || IsMovingBackwards)) || character.IsRemotePlayer) { Flip(); if (!inWater || (CurrentSwimParams != null && CurrentSwimParams.Mirror)) { Mirror(CurrentSwimParams != null ? CurrentSwimParams.MirrorLerp : true); } flipTimer = 0.0f; flipCooldown = CurrentFishAnimation.FlipCooldown; } } else { flipTimer = 0.0f; } } ResetState(); void ResetState() { wasAiming = aiming; aiming = false; wasAimingMelee = aimingMelee; aimingMelee = false; } } private bool CanDrag(Character target) { return Mass / target.Mass > 0.1f; } private float eatTimer = 0.0f; public override void DragCharacter(Character target, float deltaTime) { if (target == null) { return; } Limb mouthLimb = GetLimb(LimbType.Head); if (mouthLimb == null) { return; } if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) { //stop dragging if there's something between the pull limb and the target Vector2 sourceSimPos = SimplePhysicsEnabled ? character.SimPosition : mouthLimb.SimPosition; Vector2 targetSimPos = target.SimPosition; if (character.Submarine != null && character.SelectedCharacter.Submarine == null) { targetSimPos -= character.Submarine.SimPosition; } else if (character.Submarine == null && character.SelectedCharacter.Submarine != null) { sourceSimPos -= character.SelectedCharacter.Submarine.SimPosition; } var body = Submarine.CheckVisibility(sourceSimPos, targetSimPos, ignoreSubs: true); if (body != null) { character.DeselectCharacter(); return; } } if (Character.CanEat) { Vector2 mouthPos = SimplePhysicsEnabled ? character.SimPosition : GetMouthPosition() ?? Vector2.Zero; Vector2 attackSimPosition = character.Submarine == null ? ConvertUnits.ToSimUnits(target.WorldPosition) : target.SimPosition; Vector2 limbDiff = attackSimPosition - mouthPos; float extent = Math.Max(mouthLimb.body.GetMaxExtent(), 1); bool tooFar = character.InWater ? limbDiff.LengthSquared() > extent * extent : limbDiff.X > extent; if (tooFar) { character.DeselectCharacter(); return; } float dmg = character.Params.EatingSpeed; float eatSpeed = dmg / ((float)Math.Sqrt(Math.Max(target.Mass, 1)) * 10); eatTimer += deltaTime * eatSpeed; //pull the target character to the position of the mouth //(+ make the force fluctuate to waggle the character a bit) float dragForce = MathHelper.Clamp(eatSpeed * 10, 0, 40); if (dragForce > 0.1f) { Vector2 targetPos = mouthPos; if (target.Submarine != null && character.Submarine == null) { targetPos -= target.Submarine.SimPosition; } else if (target.Submarine == null && character.Submarine != null) { targetPos += character.Submarine.SimPosition; } target.AnimController.MainLimb.body.SmoothRotate(mouthLimb.Rotation, dragForce * 2); if (!target.AnimController.SimplePhysicsEnabled) { target.AnimController.MainLimb.MoveToPos(targetPos, (float)(Math.Sin(eatTimer) + dragForce)); } target.AnimController.Collider.MoveToPos(targetPos, (float)(Math.Sin(eatTimer) + dragForce)); } if (InWater) { //pull the character's mouth to the target character (again with a fluctuating force) float pullStrength = (float)(Math.Sin(eatTimer) * Math.Max(Math.Sin(eatTimer * 0.5f), 0.0f)); mouthLimb.body.ApplyForce(limbDiff * mouthLimb.Mass * 50.0f * pullStrength); } else { float force = (float)Math.Sin(eatTimer * 100) * mouthLimb.Mass; mouthLimb.body.ApplyLinearImpulse(Vector2.UnitY * force * mouthLimb.Params.EatImpulse, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); mouthLimb.body.ApplyTorque(-force * mouthLimb.Params.EatTorque); } var jaw = GetLimb(LimbType.Jaw); if (jaw != null) { jaw.body.ApplyTorque(-(float)Math.Sin(eatTimer * 150) * jaw.Mass * 25); } character.ApplyStatusEffects(ActionType.OnEating, deltaTime); if (target.IsDead) { float particleFrequency = MathHelper.Clamp(eatSpeed / 2, 0.02f, 0.5f); if (Rand.Value() < particleFrequency / 6) { target.AnimController.MainLimb.AddDamage(target.SimPosition, dmg, 0, 0, false); } if (Rand.Value() < particleFrequency) { target.AnimController.MainLimb.AddDamage(target.SimPosition, 0, dmg, 0, false); } if (eatTimer % 1.0f < 0.5f && (eatTimer - deltaTime * eatSpeed) % 1.0f > 0.5f) { static bool CanBeSevered(LimbJoint j) => !j.IsSevered && j.CanBeSevered && j.LimbA is { IsSevered: false } && j.LimbB is { IsSevered: false }; //keep severing joints until there is only one limb left var nonSeveredJoints = target.AnimController.LimbJoints.Where(CanBeSevered); if (nonSeveredJoints.None()) { //small monsters don't eat the contents of the character's inventory if (Mass < target.AnimController.Mass) { target.Inventory?.AllItemsMod.ForEach(it => it?.Drop(dropper: null)); } //only one limb left, the character is now full eaten Entity.Spawner?.AddEntityToRemoveQueue(target); if (Character.AIController is EnemyAIController enemyAi) { enemyAi.PetBehavior?.OnEat(target); } character.DeselectCharacter(); } else //sever a random joint { target.AnimController.SeverLimbJoint(nonSeveredJoints.GetRandomUnsynced()); } } } } } public bool Reverse; void UpdateSineAnim(float deltaTime) { if (CurrentSwimParams == null) { return; } movement = TargetMovement; bool isMoving = movement.LengthSquared() > 0.00001f; var mainLimb = MainLimb; float t = 0.5f; if (isMoving && !SimplePhysicsEnabled && CurrentSwimParams.RotateTowardsMovement) { Vector2 forward = VectorExtensions.Forward(Collider.Rotation + MathHelper.PiOver2); float dot = Vector2.Dot(forward, Vector2.Normalize(movement)); if (dot < 0) { // Reduce the linear movement speed when not facing the movement direction t = MathHelper.Clamp((1 + dot) / 10, 0.01f, 0.1f); } } if (Collider.BodyType == BodyType.Dynamic) { Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, t); } //limbs are disabled when simple physics is enabled, no need to move them if (SimplePhysicsEnabled) { return; } mainLimb.PullJointEnabled = true; if (!isMoving && !CurrentSwimParams.UpdateAnimationWhenNotMoving) { WalkPos = MathHelper.SmoothStep(WalkPos, MathHelper.PiOver2, deltaTime * 5); mainLimb.PullJointWorldAnchorB = Collider.SimPosition; if (aiming) { Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition); Vector2 diff = (mousePos - (GetLimb(LimbType.Torso) ?? MainLimb).SimPosition) * Dir; TargetMovement = new Vector2(0.0f, -0.1f); float newRotation = MathHelper.WrapAngle(MathUtils.VectorToAngle(diff) - MathHelper.PiOver2 * Dir); Collider.SmoothRotate(newRotation, CurrentSwimParams.SteerTorque * character.SpeedMultiplier * 2); if (TorsoAngle.HasValue) { Limb torso = GetLimb(LimbType.Torso); if (torso != null) { SmoothRotateWithoutWrapping(torso, newRotation + TorsoAngle.Value * Dir, mainLimb, TorsoTorque * 2); } } } } else { Vector2 transformedMovement = Reverse ? -movement : movement; float movementAngle = MathUtils.VectorToAngle(transformedMovement) - MathHelper.PiOver2; float mainLimbAngle = 0; if (mainLimb.type == LimbType.Torso && TorsoAngle.HasValue) { mainLimbAngle = TorsoAngle.Value; } else if (mainLimb.type == LimbType.Head && HeadAngle.HasValue) { mainLimbAngle = HeadAngle.Value; } mainLimbAngle *= Dir; while (mainLimb.Rotation - (movementAngle + mainLimbAngle) > MathHelper.Pi) { movementAngle += MathHelper.TwoPi; } while (mainLimb.Rotation - (movementAngle + mainLimbAngle) < -MathHelper.Pi) { movementAngle -= MathHelper.TwoPi; } if (CurrentSwimParams.RotateTowardsMovement) { Collider.SmoothRotate(movementAngle, CurrentSwimParams.SteerTorque * character.SpeedMultiplier); if (TorsoAngle.HasValue) { Limb torso = GetLimb(LimbType.Torso); if (torso != null) { SmoothRotateWithoutWrapping(torso, movementAngle + TorsoAngle.Value * Dir, mainLimb, TorsoTorque); } } if (HeadAngle.HasValue) { Limb head = GetLimb(LimbType.Head); if (head != null) { SmoothRotateWithoutWrapping(head, movementAngle + HeadAngle.Value * Dir, mainLimb, HeadTorque); } } if (TailAngle.HasValue) { bool isAngleApplied = false; foreach (var limb in Limbs) { if (limb.IsSevered) { continue; } if (!limb.Params.ApplyTailAngle) { continue; } RotateTail(limb); isAngleApplied = true; } if (!isAngleApplied) { RotateTail(GetLimb(LimbType.Tail)); } void RotateTail(Limb tail) { if (tail == null) { return; } float? mainLimbTargetAngle = null; if (mainLimb.type == LimbType.Torso) { mainLimbTargetAngle = TorsoAngle; } else if (mainLimb.type == LimbType.Head) { mainLimbTargetAngle = HeadAngle; } float torque = TailTorque; float maxMultiplier = CurrentSwimParams.TailTorqueMultiplier; if (mainLimbTargetAngle.HasValue && maxMultiplier > 1) { float diff = Math.Abs(mainLimb.Rotation - tail.Rotation); float offset = Math.Abs(mainLimbTargetAngle.Value - TailAngle.Value); torque *= MathHelper.Lerp(1, maxMultiplier, MathUtils.InverseLerp(0, MathHelper.PiOver2, diff - offset)); } SmoothRotateWithoutWrapping(tail, movementAngle + TailAngle.Value * Dir, mainLimb, torque); } } } else { movementAngle = Dir > 0 ? -MathHelper.PiOver2 : MathHelper.PiOver2; if (Reverse) { movementAngle = MathUtils.WrapAngleTwoPi(movementAngle - MathHelper.Pi); } if (mainLimb.type == LimbType.Head && HeadAngle.HasValue) { Collider.SmoothRotate(HeadAngle.Value * Dir, CurrentSwimParams.SteerTorque * character.SpeedMultiplier); } else if (mainLimb.type == LimbType.Torso && TorsoAngle.HasValue) { Collider.SmoothRotate(TorsoAngle.Value * Dir, CurrentSwimParams.SteerTorque * character.SpeedMultiplier); } if (TorsoAngle.HasValue) { Limb torso = GetLimb(LimbType.Torso); torso?.body.SmoothRotate(TorsoAngle.Value * Dir, TorsoTorque); } if (HeadAngle.HasValue) { Limb head = GetLimb(LimbType.Head); head?.body.SmoothRotate(HeadAngle.Value * Dir, HeadTorque); } if (TailAngle.HasValue) { bool isAngleApplied = false; foreach (var limb in Limbs) { if (limb.IsSevered) { continue; } if (limb.type != LimbType.Tail) { continue; } if (!limb.Params.ApplyTailAngle) { continue; } RotateTail(limb); isAngleApplied = true; } if (!isAngleApplied) { RotateTail(GetLimb(LimbType.Tail)); } void RotateTail(Limb tail) { if (tail != null) { tail.body.SmoothRotate(TailAngle.Value * Dir, TailTorque); } } } } var waveLength = Math.Abs(CurrentSwimParams.WaveLength * RagdollParams.JointScale); var waveAmplitude = Math.Abs(CurrentSwimParams.WaveAmplitude * character.SpeedMultiplier); if (waveLength > 0 && waveAmplitude > 0) { WalkPos -= transformedMovement.Length() / Math.Abs(waveLength); WalkPos = MathUtils.WrapAngleTwoPi(WalkPos); } foreach (var limb in Limbs) { if (limb.IsSevered) { continue; } if (limb.type is LimbType.LeftFoot or LimbType.RightFoot) { if (CurrentSwimParams.FootAnglesInRadians.ContainsKey(limb.Params.ID)) { SmoothRotateWithoutWrapping(limb, movementAngle + CurrentSwimParams.FootAnglesInRadians[limb.Params.ID] * Dir, mainLimb, FootTorque); } } if (limb.type == LimbType.Tail || limb.Params.ApplySineMovement) { if (waveLength > 0 && waveAmplitude > 0) { float waveRotation = (float)Math.Sin(WalkPos * limb.Params.SineFrequencyMultiplier); limb.body.ApplyTorque(waveRotation * limb.Mass * waveAmplitude * limb.Params.SineAmplitudeMultiplier); } } if (limb.SteerForce <= 0.0f) { continue; } if (!Collider.PhysEnabled) { continue; } Vector2 pullPos = limb.PullJointWorldAnchorA; limb.body.ApplyForce(movement * limb.SteerForce * limb.Mass * Math.Max(character.SpeedMultiplier, 1), pullPos); } Vector2 mainLimbDiff = mainLimb.PullJointWorldAnchorB - mainLimb.SimPosition; if (CurrentSwimParams.UseSineMovement) { mainLimb.PullJointWorldAnchorB = Vector2.SmoothStep( mainLimb.PullJointWorldAnchorB, Collider.SimPosition, mainLimbDiff.LengthSquared() > 10.0f ? 1.0f : (float)Math.Abs(Math.Sin(WalkPos))); } else { //mainLimb.PullJointWorldAnchorB = Collider.SimPosition; mainLimb.PullJointWorldAnchorB = Vector2.Lerp( mainLimb.PullJointWorldAnchorB, Collider.SimPosition, mainLimbDiff.LengthSquared() > 10.0f ? 1.0f : 0.5f); } } floorY = Limbs[0].SimPosition.Y; } void UpdateWalkAnim(float deltaTime) { movement = MathUtils.SmoothStep(movement, TargetMovement, 0.2f); if (Collider.BodyType == BodyType.Dynamic) { Collider.LinearVelocity = new Vector2( movement.X, Collider.LinearVelocity.Y > 0.0f ? Collider.LinearVelocity.Y * 0.5f : Collider.LinearVelocity.Y); } //limbs are disabled when simple physics is enabled, no need to move them if (SimplePhysicsEnabled) { return; } Vector2 colliderBottom = GetColliderBottom(); float movementAngle = 0.0f; var mainLimb = MainLimb; float mainLimbAngle = (mainLimb.type == LimbType.Torso ? TorsoAngle ?? 0 : HeadAngle ?? 0) * Dir; while (mainLimb.Rotation - (movementAngle + mainLimbAngle) > MathHelper.Pi) { movementAngle += MathHelper.TwoPi; } while (mainLimb.Rotation - (movementAngle + mainLimbAngle) < -MathHelper.Pi) { movementAngle -= MathHelper.TwoPi; } float offset = MathHelper.Pi * CurrentGroundedParams.StepLiftOffset; if (character.AnimController.Dir < 0) { offset += MathHelper.Pi * CurrentGroundedParams.StepLiftFrequency; } float stepLift = TargetMovement.X == 0.0f ? 0 : (float)Math.Sin(WalkPos * Dir * CurrentGroundedParams.StepLiftFrequency + offset) * (CurrentGroundedParams.StepLiftAmount / 100); float limpAmount = character.GetLegPenalty(); if (limpAmount > 0) { float walkPosX = (float)Math.Cos(WalkPos); //make the footpos oscillate when limping limpAmount = Math.Max(Math.Abs(walkPosX) * limpAmount, 0.0f) * Math.Min(Math.Abs(TargetMovement.X), 0.3f) * Dir; } Limb torso = GetLimb(LimbType.Torso); if (torso != null) { if (TorsoAngle.HasValue) { SmoothRotateWithoutWrapping(torso, movementAngle + TorsoAngle.Value * Dir, mainLimb, TorsoTorque); } if (TorsoPosition.HasValue && TorsoMoveForce > 0.0f) { Vector2 pos = colliderBottom + new Vector2(limpAmount, TorsoPosition.Value + stepLift); if (torso != mainLimb) { pos.X = torso.SimPosition.X; } torso.MoveToPos(pos, TorsoMoveForce); torso.PullJointEnabled = true; torso.PullJointWorldAnchorB = pos; } } Limb head = GetLimb(LimbType.Head); if (head != null) { bool headFacingBackwards = false; if (HeadAngle.HasValue && head != mainLimb) { SmoothRotateWithoutWrapping(head, movementAngle + HeadAngle.Value * Dir, mainLimb, HeadTorque); if (Math.Sign(head.SimPosition.X - mainLimb.SimPosition.X) != Math.Sign(Dir)) { headFacingBackwards = true; } } if (HeadPosition.HasValue && HeadMoveForce > 0.0f && !headFacingBackwards) { Vector2 pos = colliderBottom + new Vector2(limpAmount, HeadPosition.Value + stepLift * CurrentGroundedParams.StepLiftHeadMultiplier); if (head != mainLimb) { pos.X = head.SimPosition.X; } head.MoveToPos(pos, HeadMoveForce); head.PullJointEnabled = true; head.PullJointWorldAnchorB = pos; } } if (TailAngle.HasValue) { bool isAngleApplied = false; foreach (var limb in Limbs) { if (limb.IsSevered) { continue; } if (limb.type != LimbType.Tail) { continue; } if (!limb.Params.ApplyTailAngle) { continue; } RotateTail(limb); isAngleApplied = true; } if (!isAngleApplied) { RotateTail(GetLimb(LimbType.Tail)); } void RotateTail(Limb tail) { if (tail != null) { SmoothRotateWithoutWrapping(tail, movementAngle + TailAngle.Value * Dir, mainLimb, TailTorque); } } } float prevWalkPos = WalkPos; WalkPos -= mainLimb.LinearVelocity.X * (CurrentAnimationParams.CycleSpeed / RagdollParams.JointScale / 100.0f); Vector2 transformedStepSize = Vector2.Zero; if (Math.Abs(TargetMovement.X) > 0.01f) { transformedStepSize = new Vector2( (float)Math.Cos(WalkPos) * StepSize.Value.X * 3.0f, (float)Math.Sin(WalkPos) * StepSize.Value.Y * 2.0f); } foreach (Limb limb in Limbs) { if (limb.IsSevered) { continue; } switch (limb.type) { case LimbType.LeftFoot: case LimbType.RightFoot: Vector2 footPos = new Vector2(limb.SimPosition.X, colliderBottom.Y); if (limb.RefJointIndex > -1) { if (LimbJoints.Length <= limb.RefJointIndex) { DebugConsole.ThrowError($"Reference joint index {limb.RefJointIndex} is out of array. This is probably due to a missing joint. If you just deleted a joint, don't do that without first removing the reference joint indices from the limbs. If this is not the case, please ensure that you have defined the index to the right joint."); } else { footPos.X = LimbJoints[limb.RefJointIndex].WorldAnchorA.X; } } footPos.X += limb.StepOffset.X * Dir; footPos.Y += limb.StepOffset.Y; bool playFootstepSound = false; if (limb.type == LimbType.LeftFoot) { if (Math.Sign(Math.Sin(prevWalkPos)) > 0 && Math.Sign(transformedStepSize.Y) < 0) { playFootstepSound = true; } limb.DebugRefPos = footPos + Vector2.UnitX * movement.X * 0.1f; limb.DebugTargetPos = footPos + new Vector2( transformedStepSize.X + movement.X * 0.1f, (transformedStepSize.Y > 0.0f) ? transformedStepSize.Y : 0.0f); limb.MoveToPos(limb.DebugTargetPos, FootMoveForce); } else if (limb.type == LimbType.RightFoot) { if (Math.Sign(Math.Sin(prevWalkPos)) < 0 && Math.Sign(transformedStepSize.Y) > 0) { playFootstepSound = true; } limb.DebugRefPos = footPos + Vector2.UnitX * movement.X * 0.1f; limb.DebugTargetPos = footPos + new Vector2( -transformedStepSize.X + movement.X * 0.1f, (-transformedStepSize.Y > 0.0f) ? -transformedStepSize.Y : 0.0f); limb.MoveToPos(limb.DebugTargetPos, FootMoveForce); } if (playFootstepSound) { #if CLIENT PlayImpactSound(limb); #endif } if (CurrentGroundedParams.FootAnglesInRadians.ContainsKey(limb.Params.ID)) { SmoothRotateWithoutWrapping(limb, movementAngle + CurrentGroundedParams.FootAnglesInRadians[limb.Params.ID] * Dir, mainLimb, FootTorque); } break; case LimbType.LeftLeg: case LimbType.RightLeg: if (Math.Abs(CurrentGroundedParams.LegTorque) > 0) { limb.body.ApplyTorque(limb.Mass * CurrentGroundedParams.LegTorque * Dir); } break; } } } void UpdateDying(float deltaTime) { if (deathAnimDuration <= 0.0f) { return; } float noise = (PerlinNoise.GetPerlin(WalkPos * 0.002f, WalkPos * 0.003f) - 0.5f) * 5.0f; float animStrength = (1.0f - deathAnimTimer / deathAnimDuration); Limb baseLimb = GetLimb(LimbType.Head); //if head is the main limb, it technically can't be severed - the rest of the limbs are considered severed if the head gets cut off if (baseLimb == MainLimb) { int connectedToHeadCount = GetConnectedLimbs(baseLimb).Count; //if there's nothing connected to the head, don't make it wiggle by itself if (connectedToHeadCount == 1) { baseLimb = null; } Limb torso = GetLimb(LimbType.Torso, excludeSevered: false); if (torso != null) { //if there are more limbs connected to the torso than to the head, make the torso wiggle instead int connectedToTorsoCount = GetConnectedLimbs(torso).Count; if (connectedToTorsoCount > connectedToHeadCount) { baseLimb = torso; } } } else if (baseLimb == null) { baseLimb = GetLimb(LimbType.Torso, excludeSevered: true); if (baseLimb == null) { return; } } var connectedToBaseLimb = GetConnectedLimbs(baseLimb); Limb tail = GetLimb(LimbType.Tail); if (baseLimb != null) { baseLimb.body.ApplyTorque((float)(Math.Sqrt(baseLimb.Mass) * Dir * (Math.Sin(WalkPos) + noise)) * 30.0f * animStrength); } if (tail != null && connectedToBaseLimb.Contains(tail)) { tail.body.ApplyTorque((float)(Math.Sqrt(tail.Mass) * -Dir * (Math.Sin(WalkPos) + noise)) * 30.0f * animStrength); } WalkPos += deltaTime * 10.0f * animStrength; Vector2 centerOfMass = GetCenterOfMass(); foreach (Limb limb in Limbs) { if (!connectedToBaseLimb.Contains(limb)) { continue; } #if CLIENT if (limb.LightSource != null) { limb.LightSource.Color = Color.Lerp(limb.InitialLightSourceColor, Color.TransparentBlack, deathAnimTimer / deathAnimDuration); if (limb.InitialLightSpriteAlpha.HasValue) { limb.LightSource.OverrideLightSpriteAlpha = MathHelper.Lerp(limb.InitialLightSpriteAlpha.Value, 0.0f, deathAnimTimer / deathAnimDuration); } } #endif if (limb.type == LimbType.Head || limb.type == LimbType.Tail || limb.IsSevered || !limb.body.Enabled) continue; if (limb.Mass <= 0.0f) { string errorMsg = "Creature death animation error: invalid limb mass on character \"" + character.SpeciesName + "\" (type: " + limb.type + ", mass: " + limb.Mass + ")"; DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce("FishAnimController.UpdateDying:InvalidMass" + character.ID, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); deathAnimTimer = deathAnimDuration; return; } Vector2 diff = (centerOfMass - limb.SimPosition); if (!MathUtils.IsValid(diff)) { string errorMsg = "Creature death animation error: invalid diff (center of mass: " + centerOfMass + ", limb position: " + limb.SimPosition + ")"; DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce("FishAnimController.UpdateDying:InvalidDiff" + character.ID, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); deathAnimTimer = deathAnimDuration; return; } limb.body.ApplyForce(diff * (float)(Math.Sin(WalkPos) * Math.Sqrt(limb.Mass)) * 30.0f * animStrength, maxVelocity: 10.0f); } } private void SmoothRotateWithoutWrapping(Limb limb, float angle, Limb referenceLimb, float torque) { //make sure the angle "has the same number of revolutions" as the reference limb //(e.g. we don't want to rotate the legs to 0 if the torso is at 360, because that'd blow up the hip joints) angle = referenceLimb.body.WrapAngleToSameNumberOfRevolutions(angle); limb?.body.SmoothRotate(angle, torque, wrapAngle: false); } public override void Flip() { base.Flip(); foreach (Limb l in Limbs) { if (l.IsSevered) { continue; } if (!l.DoesFlip) { continue; } if (RagdollParams.IsSpritesheetOrientationHorizontal) { //horizontally aligned limbs need to be flipped 180 degrees l.body.SetTransformIgnoreContacts(l.SimPosition, l.body.Rotation + MathHelper.Pi * Dir); } //no need to do anything when flipping vertically oriented limbs //the sprite gets flipped horizontally, which does the job } } public void Mirror(bool lerp = true) { Vector2 centerOfMass = GetCenterOfMass(); foreach (Limb l in Limbs) { if (l.IsSevered) { continue; } float rotation = l.body.Rotation; if (l.DoesMirror) { if (RagdollParams.IsSpritesheetOrientationHorizontal) { //horizontally oriented sprites can be mirrored by rotating 180 deg and inverting the angle rotation = -(l.body.Rotation + MathHelper.Pi); } else { //vertically oriented limbs can be mirrored by inverting the angle (neutral angle is straight upwards) rotation = -l.body.Rotation; } } TrySetLimbPosition(l, centerOfMass, new Vector2(centerOfMass.X - (l.SimPosition.X - centerOfMass.X), l.SimPosition.Y), rotation, lerp); l.body.PositionSmoothingFactor = 0.8f; } if (character.SelectedCharacter != null && CanDrag(character.SelectedCharacter)) { float diff = character.SelectedCharacter.SimPosition.X - centerOfMass.X; if (diff < 100.0f) { character.SelectedCharacter.AnimController.SetPosition( new Vector2(centerOfMass.X - diff, character.SelectedCharacter.SimPosition.Y), lerp); } } } } }