using Barotrauma.Extensions; using Barotrauma.Items.Components; using Barotrauma.Particles; using Barotrauma.SpriteDeformations; using FarseerPhysics; using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using System.Linq; namespace Barotrauma { abstract partial class Ragdoll { public HashSet SpriteDeformations { get; protected set; } = new HashSet(); /// /// Inversed draw order, which is used for drawing the limbs in 3d (deformable sprites). /// protected Limb[] inversedLimbDrawOrder; partial void UpdateNetPlayerPositionProjSpecific(float deltaTime, float lowestSubPos) { if (character != GameMain.Client.Character) { //remove states without a timestamp (there may still be ID-based states //in the list when the controlled character switches to timestamp-based interpolation) character.MemState.RemoveAll(m => m.Timestamp == 0.0f); //use simple interpolation for other players' characters if (character.MemState.Count > 0) { CharacterStateInfo serverPos = character.MemState.Last(); if (!character.isSynced) { SetPosition(serverPos.Position, lerp: false); Collider.LinearVelocity = Vector2.Zero; character.MemLocalState.Clear(); character.LastNetworkUpdateID = serverPos.ID; character.isSynced = true; return; } if (character.MemState[0].SelectedCharacter == null || character.MemState[0].SelectedCharacter.Removed) { character.DeselectCharacter(); } else if (character.MemState[0].SelectedCharacter != null) { character.SelectCharacter(character.MemState[0].SelectedCharacter); } if (character.MemState[0].SelectedItem == null || character.MemState[0].SelectedItem.Removed) { character.SelectedItem = null; } else if (character.SelectedItem != character.MemState[0].SelectedItem) { foreach (var ic in character.MemState[0].SelectedItem.Components) { if (ic.CanBeSelected) { ic.Select(character); } } character.SelectedItem = character.MemState[0].SelectedItem; } if (character.MemState[0].SelectedSecondaryItem == null || character.MemState[0].SelectedSecondaryItem.Removed) { character.SelectedSecondaryItem = null; } else if (character.SelectedSecondaryItem != character.MemState[0].SelectedSecondaryItem) { foreach (var ic in character.MemState[0].SelectedSecondaryItem.Components) { if (ic.CanBeSelected) { ic.Select(character); } } character.SelectedSecondaryItem = character.MemState[0].SelectedSecondaryItem; } if (character.MemState[0].Animation == AnimController.Animation.CPR) { character.AnimController.Anim = AnimController.Animation.CPR; } else if (character.AnimController.Anim == AnimController.Animation.CPR) { character.AnimController.Anim = AnimController.Animation.None; } character.AnimController.IgnorePlatforms = character.MemState[0].IgnorePlatforms; character.AnimController.overrideTargetMovement = character.MemState[0].TargetMovement; Vector2 newVelocity = Collider.LinearVelocity; Vector2 newPosition = Collider.SimPosition; float newRotation = Collider.Rotation; float newAngularVelocity = Collider.AngularVelocity; Collider.CorrectPosition(character.MemState, out newPosition, out newVelocity, out newRotation, out newAngularVelocity); if (Collider.BodyType == BodyType.Dynamic) { newVelocity = newVelocity.ClampLength(100.0f); if (!MathUtils.IsValid(newVelocity)) { newVelocity = Vector2.Zero; } Collider.LinearVelocity = newVelocity; Collider.AngularVelocity = newAngularVelocity; } float distSqrd = Vector2.DistanceSquared(newPosition, Collider.SimPosition); float errorTolerance = ColliderControlsMovement && (!character.IsRagdolled || character.AnimController.IsHangingWithRope) ? 0.01f : 0.2f; if (distSqrd > errorTolerance) { character.AnimController.BodyInRest = false; if (distSqrd > 10.0f) { Collider.TargetRotation = newRotation; if (distSqrd > 10.0f) { //teleported very far - see if we need to move to another sub Hull serverHull = Hull.FindHull(ConvertUnits.ToDisplayUnits(newPosition), CurrentHull, newPosition.Y < lowestSubPos); if (currentHull != null && serverHull != null && serverHull.Submarine != currentHull.Submarine) { character.Submarine = serverHull.Submarine; character.CurrentHull = CurrentHull = serverHull; } } SetPosition(newPosition, lerp: distSqrd < 5.0f, ignorePlatforms: false); //make sure ragdoll isn't stuck at the wrong side of a platform if the movement is controlled by the ragdoll, and the ragdoll has come to rest server-side if (!ColliderControlsMovement && newVelocity.LengthSquared() < 0.01f) { TryPlatformCorrection(newPosition); } } else if (ColliderControlsMovement) { Collider.TargetRotation = newRotation; Collider.TargetPosition = newPosition; Collider.MoveToTargetPosition(true); } else { float mainLimbDistSqrd = Vector2.DistanceSquared(MainLimb.PullJointWorldAnchorA, newPosition); float mainLimbErrorTolerance = character == GameMain.Client.Character ? 0.25f : 0.1f; MainLimb.body.LinearVelocity = newVelocity; //if the main limb is roughly at the correct position and the collider isn't moving (much at least), //don't attempt to correct the position. if (mainLimbDistSqrd > mainLimbErrorTolerance) { MainLimb.PullJointWorldAnchorB = newPosition; MainLimb.PullJointEnabled = true; if (!ColliderControlsMovement && newVelocity.LengthSquared() < 0.01f) { TryPlatformCorrection(newPosition); } } } } else if (!ColliderControlsMovement) { //correct velocity regardless of the positional error MainLimb.body.LinearVelocity = newVelocity; } } character.MemLocalState.Clear(); } else { //remove states with a timestamp (there may still timestamp-based states //in the list if the controlled character switches from timestamp-based interpolation to ID-based) character.MemState.RemoveAll(m => m.Timestamp > 0.0f); for (int i = 0; i < character.MemLocalState.Count; i++) { if (character.Submarine == null) { //transform in-sub coordinates to outside coordinates if (character.MemLocalState[i].Position.Y > lowestSubPos) { character.MemLocalState[i].TransformInToOutside(); } } else if (currentHull?.Submarine != null) { //transform outside coordinates to in-sub coordinates if (character.MemLocalState[i].Position.Y < lowestSubPos) { character.MemLocalState[i].TransformOutToInside(currentHull.Submarine); } } } if (character.MemState.Count < 1) { return; } overrideTargetMovement = null; CharacterStateInfo serverPos = character.MemState.Last(); Collider.LastServerState = serverPos; if (!character.isSynced) { SetPosition(serverPos.Position, lerp: false); Collider.LinearVelocity = Vector2.Zero; character.MemLocalState.Clear(); character.LastNetworkUpdateID = serverPos.ID; character.isSynced = true; return; } int localPosIndex = character.MemLocalState.FindIndex(m => m.ID == serverPos.ID); if (localPosIndex > -1) { CharacterStateInfo localPos = character.MemLocalState[localPosIndex]; //the entity we're interacting with doesn't match the server's if (localPos.SelectedCharacter != serverPos.SelectedCharacter) { if (serverPos.SelectedCharacter == null || serverPos.SelectedCharacter.Removed) { character.DeselectCharacter(); } else if (serverPos.SelectedCharacter != null) { character.SelectCharacter(serverPos.SelectedCharacter); } } if (localPos.SelectedItem != serverPos.SelectedItem) { if (serverPos.SelectedItem == null || serverPos.SelectedItem.Removed) { character.SelectedItem = null; } else if (character.SelectedItem != serverPos.SelectedItem) { serverPos.SelectedItem.TryInteract(character, ignoreRequiredItems: true, forceSelectKey: true); character.SelectedItem = serverPos.SelectedItem; } } if (localPos.SelectedSecondaryItem != serverPos.SelectedSecondaryItem) { if (serverPos.SelectedSecondaryItem == null || serverPos.SelectedSecondaryItem.Removed) { character.SelectedSecondaryItem = null; } else if (character.SelectedSecondaryItem != serverPos.SelectedSecondaryItem) { serverPos.SelectedSecondaryItem.TryInteract(character, ignoreRequiredItems: true, forceSelectKey: true); character.SelectedSecondaryItem = serverPos.SelectedSecondaryItem; } } if (localPos.Animation != serverPos.Animation) { if (serverPos.Animation == AnimController.Animation.CPR) { character.AnimController.Anim = AnimController.Animation.CPR; } else if (character.AnimController.Anim == AnimController.Animation.CPR) { character.AnimController.Anim = AnimController.Animation.None; } } Hull serverHull = Hull.FindHull(ConvertUnits.ToDisplayUnits(serverPos.Position), character.CurrentHull, serverPos.Position.Y < lowestSubPos); Hull clientHull = Hull.FindHull(ConvertUnits.ToDisplayUnits(localPos.Position), serverHull, localPos.Position.Y < lowestSubPos); if (serverHull != null && clientHull != null && serverHull.Submarine != clientHull.Submarine) { //hull subs don't match => teleport the camera to the other sub character.Submarine = serverHull.Submarine; character.CurrentHull = CurrentHull = serverHull; SetPosition(serverPos.Position); character.MemLocalState.Clear(); } else { Vector2 positionError = serverPos.Position - localPos.Position; float rotationError = serverPos.Rotation.HasValue && localPos.Rotation.HasValue ? serverPos.Rotation.Value - localPos.Rotation.Value : 0.0f; for (int i = localPosIndex; i < character.MemLocalState.Count; i++) { Hull pointHull = Hull.FindHull(ConvertUnits.ToDisplayUnits(character.MemLocalState[i].Position), clientHull, character.MemLocalState[i].Position.Y < lowestSubPos); if (pointHull != clientHull && ((pointHull == null) || (clientHull == null) || (pointHull.Submarine == clientHull.Submarine))) break; character.MemLocalState[i].Translate(positionError, rotationError); } float errorMagnitude = positionError.Length(); if (errorMagnitude > 0.5f) { character.MemLocalState.Clear(); SetPosition(serverPos.Position, lerp: true, ignorePlatforms: false); } else if (errorMagnitude > 0.01f) { if (ColliderControlsMovement) { Collider.TargetPosition = Collider.SimPosition + positionError; Collider.TargetRotation = Collider.Rotation + rotationError; Collider.MoveToTargetPosition(lerp: true); } else { float mainLimbErrorTolerance = character == GameMain.Client.Character ? 0.25f : 0.1f; //if the main limb is roughly at the correct position and the collider isn't moving (much at least), //don't attempt to correct the position. if (errorMagnitude > mainLimbErrorTolerance) { MainLimb.PullJointWorldAnchorB = MainLimb.SimPosition + positionError; MainLimb.PullJointEnabled = true; if (serverPos.LinearVelocity.LengthSquared() < 0.01f) { TryPlatformCorrection(MainLimb.SimPosition + positionError); } } } } } } if (character.MemLocalState.Count > 120) { character.MemLocalState.RemoveRange(0, character.MemLocalState.Count - 120); } character.MemState.Clear(); } } /// /// Attempts to correct the ragdoll to the correct side of a platform if the server position is above the platform and some of the ragdoll's limbs below it client-side, or vice versa. /// private void TryPlatformCorrection(Vector2 serverPos) { float highestPos = limbs.Where(static l => !l.IsSevered).Max(static l => l.SimPosition.Y); highestPos = Math.Max(serverPos.Y, highestPos); float lowestPos = limbs.Where(static l => !l.IsSevered).Min(static l => l.SimPosition.Y); lowestPos = Math.Min(serverPos.Y, lowestPos); var platform = Submarine.PickBody(new Vector2(serverPos.X, highestPos), new Vector2(serverPos.X, lowestPos), collisionCategory: Physics.CollisionPlatform, allowInsideFixture: true); if (platform == null) { return; } int serverDir = Math.Sign(serverPos.Y - platform.Position.Y); foreach (var limb in limbs) { if (limb.IsSevered) { continue; } int limbDir = Math.Sign(limb.SimPosition.Y - platform.Position.Y); const float Margin = 0.01f; if (limbDir != serverDir) { limb.body.SetTransformIgnoreContacts( new Vector2( limb.SimPosition.X, serverDir > 0 ? Math.Max(serverPos.Y + Margin + limb.body.GetMaxExtent(), limb.SimPosition.Y) : Math.Min(serverPos.Y - Margin - limb.body.GetMaxExtent(), limb.SimPosition.Y)), limb.Rotation); } } } partial void ImpactProjSpecific(float impact, Body body) { float volume = MathHelper.Clamp(impact - 3.0f, 0.5f, 1.0f); if (body.UserData is Limb limb && character.Stun <= 0f) { if (impact > 3.0f) { PlayImpactSound(limb); } } else if (body.UserData is Limb || body == Collider.FarseerBody) { if (!character.IsRemotelyControlled && impact > ImpactTolerance) { SoundPlayer.PlayDamageSound("LimbBlunt", strongestImpact, Collider); } } if (Character.Controlled == character) { GameMain.GameScreen.Cam.Shake = Math.Min(Math.Max(strongestImpact, GameMain.GameScreen.Cam.Shake), 3.0f); } } public void PlayImpactSound(Limb limb) { limb.LastImpactSoundTime = (float)Timing.TotalTime; if (!string.IsNullOrWhiteSpace(limb.HitSoundTag)) { bool inWater = limb.InWater; if (character.CurrentHull != null && character.CurrentHull.Surface > character.CurrentHull.Rect.Y - character.CurrentHull.Rect.Height + 5.0f && limb.SimPosition.Y < ConvertUnits.ToSimUnits(character.CurrentHull.Rect.Y - character.CurrentHull.Rect.Height) + limb.body.GetMaxExtent()) { inWater = true; } SoundPlayer.PlaySound(inWater ? "footstep_water" : limb.HitSoundTag, limb.WorldPosition, hullGuess: character.CurrentHull); } foreach (WearableSprite wearable in limb.WearingItems) { if (limb.type == wearable.Limb && !string.IsNullOrWhiteSpace(wearable.Sound)) { SoundPlayer.PlaySound(wearable.Sound, limb.WorldPosition, hullGuess: character.CurrentHull); } } } partial void Splash(Limb limb, Hull limbHull) { //create a splash particle for (int i = 0; i < MathHelper.Clamp(Math.Abs(limb.LinearVelocity.Y), 1.0f, 5.0f); i++) { var splash = GameMain.ParticleManager.CreateParticle("watersplash", new Vector2(limb.WorldPosition.X, limbHull.WorldSurface), new Vector2(0.0f, Math.Abs(-limb.LinearVelocity.Y * 20.0f)) + Rand.Vector(Math.Abs(limb.LinearVelocity.Y * 10)), Rand.Range(0.0f, MathHelper.TwoPi), limbHull); if (splash != null) { splash.Size *= MathHelper.Clamp(Math.Abs(limb.LinearVelocity.Y) * 0.1f, 1.0f, 2.0f); } } GameMain.ParticleManager.CreateParticle("bubbles", new Vector2(limb.WorldPosition.X, limbHull.WorldSurface), limb.LinearVelocity * 0.001f, 0.0f, limbHull); //if the Character dropped into water, create a wave if (limb.LinearVelocity.Y < 0.0f) { if (splashSoundTimer <= 0.0f) { SoundPlayer.PlaySplashSound(limb.WorldPosition, Math.Abs(limb.LinearVelocity.Y) + Rand.Range(-5.0f, 0.0f)); splashSoundTimer = 0.5f; } //+ some extra bubbles to follow the character underwater GameMain.ParticleManager.CreateParticle("bubbles", new Vector2(limb.WorldPosition.X, limbHull.WorldSurface), limb.LinearVelocity * 10.0f, 0.0f, limbHull); } } partial void SetupDrawOrder() { //make sure every character gets drawn at a distinct "layer" //(instead of having some of the limbs appear behind and some in front of other characters) float startDepth = 0.1f; float increment = 0.001f; foreach (Character otherCharacter in Character.CharacterList) { if (otherCharacter == character) { continue; } startDepth += increment; } //make sure each limb has a distinct depth value List depthSortedLimbs = Limbs.OrderBy(l => l.DefaultSpriteDepth).ToList(); foreach (Limb limb in Limbs) { var sprite = limb.GetActiveSprite(); if (sprite == null) { continue; } sprite.Depth = startDepth + depthSortedLimbs.IndexOf(limb) * 0.00001f; foreach (var conditionalSprite in limb.ConditionalSprites) { if (conditionalSprite.Exclusive) { conditionalSprite.ActiveSprite.Depth = sprite.Depth; } } } foreach (Limb limb in Limbs) { if (limb.ActiveSprite == null) { continue; } if (limb.Params.InheritLimbDepth == LimbType.None) { continue; } var matchingLimb = GetLimb(limb.Params.InheritLimbDepth); if (matchingLimb != null) { limb.ActiveSprite.Depth = matchingLimb.ActiveSprite.Depth - 0.0000001f; } } depthSortedLimbs.Reverse(); inversedLimbDrawOrder = depthSortedLimbs.ToArray(); } partial void UpdateProjSpecific(float deltaTime, Camera cam) { if (!character.IsVisible) { return; } LimbJoints.ForEach(j => j.UpdateDeformations(deltaTime)); foreach (var deformation in SpriteDeformations) { if (character.IsDead && deformation.Params.StopWhenHostIsDead) { continue; } if (!character.AnimController.InWater && deformation.Params.OnlyInWater) { continue; } if (deformation.Params.UseMovementSine) { if (this is AnimController animator) { deformation.Phase = MathUtils.WrapAngleTwoPi(animator.WalkPos * deformation.Params.Frequency + MathHelper.Pi * deformation.Params.SineOffset); } } else { deformation.Update(deltaTime); } } } partial void FlipProjSpecific() { foreach (Limb limb in Limbs) { if (limb == null || limb.IsSevered || !limb.DoesMirror) { continue; } FlipSprite(limb.DeformSprite?.Sprite ?? limb.Sprite); foreach (var conditionalSprite in limb.ConditionalSprites) { FlipSprite(conditionalSprite.DeformableSprite?.Sprite ?? conditionalSprite.Sprite); } } static void FlipSprite(Sprite sprite) { if (sprite == null) { return; } Vector2 spriteOrigin = sprite.Origin; spriteOrigin.X = sprite.SourceRect.Width - spriteOrigin.X; sprite.Origin = spriteOrigin; } } partial void SeverLimbJointProjSpecific(LimbJoint limbJoint, bool playSound) { foreach (Limb limb in new Limb[] { limbJoint.LimbA, limbJoint.LimbB }) { float gibParticleAmount = MathHelper.Clamp(limb.Mass / character.AnimController.Mass, 0.1f, 1.0f); foreach (ParticleEmitter emitter in character.GibEmitters) { if (emitter?.Prefab == null) { continue; } if (inWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Air) { continue; } if (!inWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Water) { continue; } emitter.Emit(1.0f, limb.WorldPosition, character.CurrentHull, amountMultiplier: gibParticleAmount); } } if (playSound) { var damageSound = character.GetSound(s => s.Type == CharacterSound.SoundType.Damage); float range = damageSound != null ? damageSound.Range * 2 : ConvertUnits.ToDisplayUnits(character.AnimController.Collider.GetSize().Length() * 10); if (!limbJoint.Params.BreakSound.IsNullOrEmpty() && !limbJoint.Params.BreakSound.Equals("none", StringComparison.OrdinalIgnoreCase)) { SoundPlayer.PlayDamageSound(limbJoint.Params.BreakSound, 1.0f, limbJoint.LimbA.body.DrawPosition, range: range); } } } public void Draw(SpriteBatch spriteBatch, Camera cam) { if (simplePhysicsEnabled) { return; } Collider.UpdateDrawPosition(); if (Limbs == null) { DebugConsole.ThrowError("Failed to draw a ragdoll, limbs have been removed. Character: \"" + character.Name + "\", removed: " + character.Removed + "\n" + Environment.StackTrace.CleanupStackTrace()); GameAnalyticsManager.AddErrorEventOnce("Ragdoll.Draw:LimbsRemoved", GameAnalyticsManager.ErrorSeverity.Error, "Failed to draw a ragdoll, limbs have been removed. Character: \"" + character.SpeciesName + "\", removed: " + character.Removed + "\n" + Environment.StackTrace.CleanupStackTrace()); return; } Color? color = null; if (character.ExternalHighlight) { color = Color.Lerp(Color.White, GUIStyle.Orange, (float)Math.Sin(Timing.TotalTime * 3.5f)); } float depthOffset = GetDepthOffset(); if (!MathUtils.NearlyEqual(depthOffset, 0.0f)) { foreach (Limb limb in limbs) { limb.ActiveSprite.Depth += depthOffset; } } for (int i = 0; i < limbs.Length; i++) { inversedLimbDrawOrder[i].Draw(spriteBatch, cam, color); } if (!MathUtils.NearlyEqual(depthOffset, 0.0f)) { foreach (Limb limb in limbs) { limb.ActiveSprite.Depth -= depthOffset; } } LimbJoints.ForEach(j => j.Draw(spriteBatch)); } /// /// Offset added to the default draw depth of the character's limbs. For example, climbing on ladders affects the depth of the character to get it to render behind the ladders. /// public float GetDepthOffset() { float maxDepth = 0.0f; float minDepth = 1.0f; float depthOffset = 0.0f; if (character.SelectedSecondaryItem?.GetComponent() is Ladder ladder) { CalculateLimbDepths(); if (character.WorldPosition.X < character.SelectedSecondaryItem.WorldPosition.X) { //at the left side of the ladder, needs to be drawn in front of the rungs if (maxDepth > ladder.BackgroundSpriteDepth) { depthOffset = Math.Max(ladder.BackgroundSpriteDepth - 0.01f - maxDepth, 0.0f); } else { depthOffset = Math.Max(ladder.Item.GetDrawDepth() + 0.0001f - minDepth, -minDepth); } } else { //at the right side of the ladder, needs to be drawn behind the rungs depthOffset = Math.Max(ladder.BackgroundSpriteDepth + 0.01f - minDepth, 0.0f); } } else { CalculateLimbDepths(); AdjustDepthOffset(character.SelectedItem); AdjustDepthOffset(character.SelectedSecondaryItem); void AdjustDepthOffset(Item item) { if (item == null) { return; } foreach (var controller in item.GetComponents()) { if (controller is { ControlCharacterPose: true, UserInCorrectPosition: true } && controller.User == character) { if (controller.Item.SpriteDepth <= maxDepth || controller.DrawUserBehind) { depthOffset = Math.Max(controller.Item.GetDrawDepth() + 0.0001f - minDepth, -minDepth); } else { depthOffset = Math.Max(controller.Item.GetDrawDepth() - 0.0001f - maxDepth, 0.0f); } } } } } void CalculateLimbDepths() { foreach (Limb limb in Limbs) { var activeSprite = limb.ActiveSprite; if (activeSprite != null) { maxDepth = Math.Max(activeSprite.Depth, maxDepth); minDepth = Math.Min(activeSprite.Depth, minDepth); } } } return depthOffset; } public void DebugDraw(SpriteBatch spriteBatch) { if (!GameMain.DebugDraw || !character.Enabled) { return; } if (simplePhysicsEnabled) { return; } foreach (Limb limb in Limbs) { if (limb.PullJointEnabled) { Vector2 pos = ConvertUnits.ToDisplayUnits(limb.PullJointWorldAnchorB); if (currentHull?.Submarine != null) pos += currentHull.Submarine.DrawPosition; pos.Y = -pos.Y; GUI.DrawRectangle(spriteBatch, new Rectangle((int)pos.X, (int)pos.Y, 5, 5), GUIStyle.Red, true, 0.01f); pos = ConvertUnits.ToDisplayUnits(limb.PullJointWorldAnchorA); if (currentHull?.Submarine != null) pos += currentHull.Submarine.DrawPosition; pos.Y = -pos.Y; GUI.DrawRectangle(spriteBatch, new Rectangle((int)pos.X, (int)pos.Y, 5, 5), Color.Cyan, true, 0.01f); } limb.body.DebugDraw(spriteBatch, inWater ? (currentHull == null ? Color.Blue : Color.Cyan) : Color.White); } Collider.DebugDraw(spriteBatch, frozen ? GUIStyle.Red : (inWater ? Color.SkyBlue : Color.Gray)); GUIStyle.Font.DrawString(spriteBatch, Collider.LinearVelocity.X.FormatSingleDecimal(), new Vector2(Collider.DrawPosition.X, -Collider.DrawPosition.Y), Color.Orange); foreach (var joint in LimbJoints) { Vector2 pos = ConvertUnits.ToDisplayUnits(joint.WorldAnchorA); GUI.DrawRectangle(spriteBatch, new Rectangle((int)pos.X, (int)-pos.Y, 5, 5), Color.White, true); pos = ConvertUnits.ToDisplayUnits(joint.WorldAnchorB); GUI.DrawRectangle(spriteBatch, new Rectangle((int)pos.X, (int)-pos.Y, 5, 5), Color.White, true); } foreach (Limb limb in Limbs) { if (limb.body.TargetPosition != null) { Vector2 pos = ConvertUnits.ToDisplayUnits((Vector2)limb.body.TargetPosition); if (currentHull?.Submarine != null) { pos += currentHull.Submarine.DrawPosition; } pos.Y = -pos.Y; GUI.DrawRectangle(spriteBatch, new Rectangle((int)pos.X - 10, (int)pos.Y - 10, 20, 20), Color.Cyan, false, 0.01f); GUI.DrawLine(spriteBatch, pos, new Vector2(limb.WorldPosition.X, -limb.WorldPosition.Y), Color.Cyan); } } if (this is HumanoidAnimController humanoid) { Vector2 pos = ConvertUnits.ToDisplayUnits(humanoid.RightHandIKPos); if (humanoid.character.Submarine != null) { pos += humanoid.character.Submarine.DrawPosition; } GUI.DrawRectangle(spriteBatch, new Rectangle((int)pos.X, (int)-pos.Y, 4, 4), GUIStyle.Green, true); pos = ConvertUnits.ToDisplayUnits(humanoid.LeftHandIKPos); if (humanoid.character.Submarine != null) { pos += humanoid.character.Submarine.DrawPosition; } GUI.DrawRectangle(spriteBatch, new Rectangle((int)pos.X, (int)-pos.Y, 4, 4), GUIStyle.Green, true); Vector2 aimPos = humanoid.AimSourceWorldPos; aimPos.Y = -aimPos.Y; GUI.DrawLine(spriteBatch, aimPos - Vector2.UnitY * 3, aimPos + Vector2.UnitY * 3, Color.Red); GUI.DrawLine(spriteBatch, aimPos - Vector2.UnitX * 3, aimPos + Vector2.UnitX * 3, Color.Red); } if (character.MemState.Count > 1) { Vector2 prevPos = ConvertUnits.ToDisplayUnits(character.MemState[0].Position); if (currentHull?.Submarine != null) { prevPos += currentHull.Submarine.DrawPosition; } prevPos.Y = -prevPos.Y; for (int i = 1; i < character.MemState.Count; i++) { Vector2 currPos = ConvertUnits.ToDisplayUnits(character.MemState[i].Position); if (currentHull?.Submarine != null) { currPos += currentHull.Submarine.DrawPosition; } currPos.Y = -currPos.Y; GUI.DrawRectangle(spriteBatch, new Rectangle((int)currPos.X - 3, (int)currPos.Y - 3, 6, 6), Color.Cyan * 0.6f, true, 0.01f); GUI.DrawLine(spriteBatch, prevPos, currPos, Color.Cyan * 0.6f, 0, 3); prevPos = currPos; } } if (currentHull != null) { Vector2 displayFloorPos = ConvertUnits.ToDisplayUnits(new Vector2(Collider.SimPosition.X, floorY)); if (currentHull?.Submarine != null) { displayFloorPos += currentHull.Submarine.DrawPosition; } displayFloorPos.Y = -displayFloorPos.Y; GUI.DrawLine(spriteBatch, displayFloorPos, displayFloorPos + new Vector2(floorNormal.X, -floorNormal.Y) * 50.0f, Color.Cyan * 0.5f, 0, 2); } if (IgnorePlatforms) { GUI.DrawLine(spriteBatch, new Vector2(Collider.DrawPosition.X, -Collider.DrawPosition.Y), new Vector2(Collider.DrawPosition.X, -Collider.DrawPosition.Y + 50), Color.Orange, 0, 5); } } } }