using Barotrauma.Networking; using FarseerPhysics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; namespace Barotrauma.Items.Components { partial class Sonar : Powered, IServerSerializable, IClientSerializable { enum Mode { Active, Passive }; private bool unsentChanges; private float networkUpdateTimer; private GUITickBox activeTickBox, passiveTickBox; private GUITextBlock signalWarningText; private GUIScrollBar zoomSlider; private GUITickBox directionalTickBox; private GUIScrollBar directionalSlider; private GUIComponent activeControlsContainer; private GUICustomComponent sonarView; private float displayBorderSize; private List sonarBlips; private float prevPingRadius; private float prevPassivePingRadius; private Vector2 center; private float displayScale; private float zoomSqrt; //Vector2 = vector from the ping source to the position of the disruption //float = strength of the disruption, between 0-1 List> disruptedDirections = new List>(); private static Color[] blipColorGradient = { Color.TransparentBlack, new Color(0, 50, 160), new Color(0, 133, 166), new Color(2, 159, 30), new Color(255, 255, 255) }; public Vector2 DisplayOffset { get; private set; } public float DisplayRadius { get; private set; } partial void InitProjSpecific(XElement element) { sonarBlips = new List(); int viewSize = (int)Math.Min(GuiFrame.Rect.Width - 150, GuiFrame.Rect.Height * 0.9f); sonarView = new GUICustomComponent(new RectTransform(new Point(viewSize), GuiFrame.RectTransform, Anchor.CenterLeft), (spriteBatch, guiCustomComponent) => { DrawSonar(spriteBatch, guiCustomComponent.Rect); }, null); var controlContainer = new GUIFrame(new RectTransform(new Vector2(0.3f, 0.35f), GuiFrame.RectTransform, Anchor.TopLeft) { MinSize = new Point(150, 0), AbsoluteOffset = new Point((int)(viewSize * 0.9f), 0) }, "SonarFrame"); controlContainer.RectTransform.SetAsFirstChild(); var paddedControlContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), controlContainer.RectTransform, Anchor.Center)) { RelativeSpacing = 0.03f, Stretch = true }; passiveTickBox = new GUITickBox(new RectTransform(new Vector2(0.3f, 0.2f), paddedControlContainer.RectTransform), TextManager.Get("SonarPassive"), style: "GUIRadioButton") { ToolTip = TextManager.Get("SonarTipPassive"), Selected = true }; activeTickBox = new GUITickBox(new RectTransform(new Vector2(0.3f, 0.2f), paddedControlContainer.RectTransform), TextManager.Get("SonarActive"), style: "GUIRadioButton") { ToolTip = TextManager.Get("SonarTipActive"), OnSelected = (GUITickBox box) => { IsActive = box.Selected; if (GameMain.Client != null) { unsentChanges = true; correctionTimer = CorrectionDelay; } return true; } }; var zoomContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.2f), paddedControlContainer.RectTransform), isHorizontal: true); new GUITextBlock(new RectTransform(new Vector2(0.35f, 1.0f), zoomContainer.RectTransform), TextManager.Get("SonarZoom"), font: GUI.SmallFont) { Padding = Vector4.Zero }; zoomSlider = new GUIScrollBar(new RectTransform(new Vector2(0.65f, 1.0f), zoomContainer.RectTransform), barSize: 0.1f, isHorizontal: true) { OnMoved = (scrollbar, scroll) => { zoom = MathHelper.Lerp(MinZoom, MaxZoom, scroll); if (GameMain.Client != null) { unsentChanges = true; correctionTimer = CorrectionDelay; } return true; } }; var activeControls = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.6f), paddedControlContainer.RectTransform) { RelativeOffset = new Vector2(0.1f, 0.0f) }, "InnerFrame"); activeControlsContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.8f), activeControls.RectTransform, Anchor.Center)) { RelativeSpacing = 0.03f, Stretch = true }; directionalTickBox = new GUITickBox(new RectTransform(new Vector2(0.3f, 0.3f), activeControlsContainer.RectTransform), TextManager.Get("SonarDirectionalPing")) { OnSelected = (tickBox) => { useDirectionalPing = tickBox.Selected; if (GameMain.Client != null) { unsentChanges = true; correctionTimer = CorrectionDelay; } return true; } }; directionalSlider = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.3f), activeControlsContainer.RectTransform), barSize: 0.1f, isHorizontal: true) { OnMoved = (scrollbar, scroll) => { float pingAngle = MathHelper.Lerp(0.0f, MathHelper.TwoPi, scroll); pingDirection = new Vector2((float)Math.Cos(pingAngle), (float)Math.Sin(pingAngle)); if (GameMain.Client != null) { unsentChanges = true; correctionTimer = CorrectionDelay; } return true; } }; signalWarningText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), paddedControlContainer.RectTransform), "", Color.Orange, textAlignment: Alignment.Center); GUIRadioButtonGroup sonarMode = new GUIRadioButtonGroup(); sonarMode.AddRadioButton(Mode.Active, activeTickBox); sonarMode.AddRadioButton(Mode.Passive, passiveTickBox); sonarMode.Selected = Mode.Passive; GuiFrame.CanBeFocused = false; } public override void OnItemLoaded() { zoomSlider.BarScroll = MathUtils.InverseLerp(MinZoom, MaxZoom, zoom); //make the sonarView customcomponent render the steering view so it gets drawn in front of the sonar item.GetComponent()?.AttachToSonarHUD(sonarView); } public override void UpdateHUD(Character character, float deltaTime, Camera cam) { if (GameMain.Client != null) { if (unsentChanges) { if (networkUpdateTimer <= 0.0f) { item.CreateClientEvent(this); correctionTimer = CorrectionDelay; networkUpdateTimer = 0.1f; unsentChanges = false; } } networkUpdateTimer -= deltaTime; } if (sonarView.Rect.Contains(PlayerInput.MousePosition)) { float scrollSpeed = PlayerInput.ScrollWheelSpeed / 1000.0f; if (Math.Abs(scrollSpeed) > 0.0001f) { zoomSlider.BarScroll += PlayerInput.ScrollWheelSpeed / 1000.0f; zoomSlider.OnMoved(zoomSlider, zoomSlider.BarScroll); } } float distort = 1.0f - item.Condition / item.MaxCondition; for (int i = sonarBlips.Count - 1; i >= 0; i--) { sonarBlips[i].FadeTimer -= deltaTime * MathHelper.Lerp(0.5f, 2.0f, distort); sonarBlips[i].Position += sonarBlips[i].Velocity * deltaTime; if (sonarBlips[i].FadeTimer <= 0.0f) sonarBlips.RemoveAt(i); } foreach (GUIComponent component in activeControlsContainer.Parent.GetAllChildren()) { if (component is GUITextBlock textBlock) { textBlock.TextColor = IsActive ? textBlock.Style.textColor : textBlock.Style.textColor * 0.5f; } } //sonar view can only get focus when the cursor is inside the circle sonarView.CanBeFocused = Vector2.DistanceSquared(sonarView.Rect.Center.ToVector2(), PlayerInput.MousePosition) < (sonarView.Rect.Width / 2 * sonarView.Rect.Width / 2); if (UseTransducers && connectedTransducers.Count == 0) { return; } Vector2 transducerCenter = UseTransducers ? GetTransducerCenter() : item.WorldPosition; transducerCenter += DisplayOffset; if (Level.Loaded != null) { Dictionary levelTriggerFlows = new Dictionary(); foreach (LevelObject levelObject in Level.Loaded.LevelObjectManager.GetAllObjects(transducerCenter, range * pingState / zoom)) { //gather all nearby triggers that are causing the water to flow into the dictionary foreach (LevelTrigger trigger in levelObject.Triggers) { Vector2 flow = trigger.GetWaterFlowVelocity(); //ignore ones that are barely doing anything (flow^2 < 1) if (flow.LengthSquared() > 1.0f) levelTriggerFlows.Add(trigger, flow); } } foreach (KeyValuePair triggerFlow in levelTriggerFlows) { LevelTrigger trigger = triggerFlow.Key; Vector2 flow = triggerFlow.Value; float flowMagnitude = flow.Length(); if (Rand.Range(0.0f, 1.0f) < flowMagnitude / 1000.0f) { float edgeDist = Rand.Range(0.0f, 1.0f); Vector2 blipPos = trigger.WorldPosition + Rand.Vector(trigger.ColliderRadius * edgeDist); Vector2 blipVel = flow; if (trigger.ForceFalloff) flow *= (1.0f - edgeDist); //go through other triggers in range and add the flows of the ones that the blip is inside foreach (KeyValuePair triggerFlow2 in levelTriggerFlows) { LevelTrigger trigger2 = triggerFlow2.Key; if (trigger2 != trigger && Vector2.DistanceSquared(blipPos, trigger2.WorldPosition) < trigger2.ColliderRadius * trigger2.ColliderRadius) { Vector2 trigger2flow = triggerFlow2.Value; if (trigger2.ForceFalloff) trigger2flow *= (1.0f - Vector2.Distance(blipPos, trigger2.WorldPosition) / trigger2.ColliderRadius); blipVel += trigger2flow; } } var flowBlip = new SonarBlip(blipPos, Rand.Range(0.5f, 1.0f), 1.0f) { Velocity = blipVel * Rand.Range(1.0f, 5.0f), Size = new Vector2(MathHelper.Lerp(0.4f, 5f, flowMagnitude / 500.0f), 0.2f), Rotation = (float)Math.Atan2(-blipVel.Y, blipVel.X) }; sonarBlips.Add(flowBlip); } } } if (IsActive) { float pingRadius = DisplayRadius * pingState / zoom; UpdateDisruptions(transducerCenter, pingRadius / displayScale, prevPingRadius / displayScale); Ping(transducerCenter, transducerCenter, pingRadius, prevPingRadius, displayScale, range / zoom, passive: false, pingStrength: 2.0f); prevPingRadius = pingRadius; return; } float passivePingRadius = (float)(Timing.TotalTime % 1.0f); if (passivePingRadius > 0.0f) { disruptedDirections.Clear(); foreach (AITarget t in AITarget.List) { if (t.SoundRange <= 0.0f || !t.Enabled) { continue; } float distSqr = Vector2.DistanceSquared(t.WorldPosition, transducerCenter); if (distSqr > t.SoundRange * t.SoundRange * 2) { continue; } float dist = (float)Math.Sqrt(distSqr); if (dist > prevPassivePingRadius * Range && dist <= passivePingRadius * Range) { Ping(t.WorldPosition, transducerCenter, Math.Min(t.SoundRange, range * 0.5f) * displayScale, 0, displayScale, Math.Min(t.SoundRange, range * 0.5f), passive: true, pingStrength: 0.5f); sonarBlips.Add(new SonarBlip(t.WorldPosition, 1.0f, 1.0f)); } } } prevPassivePingRadius = passivePingRadius; } private void DrawSonar(SpriteBatch spriteBatch, Rectangle rect) { displayBorderSize = 0.2f; center = rect.Center.ToVector2(); DisplayRadius = (rect.Width / 2.0f) * (1.0f - displayBorderSize); displayScale = DisplayRadius / range * zoom; if (screenBackground != null) { screenBackground.Draw(spriteBatch, center, 0.0f, rect.Width / screenBackground.size.X); } if (IsActive) { if (isLastPingDirectional && directionalPingCircle != null) { directionalPingCircle.Draw(spriteBatch, center, Color.White * (1.0f - pingState), rotate: MathUtils.VectorToAngle(lastPingDirection), scale: (DisplayRadius / directionalPingCircle.size.X) * pingState); } else { pingCircle.Draw(spriteBatch, center, Color.White * (1.0f - pingState), 0.0f, (DisplayRadius * 2 / pingCircle.size.X) * pingState); } } float signalStrength = 1.0f; if (UseTransducers) { signalStrength = 0.0f; foreach (ConnectedTransducer connectedTransducer in connectedTransducers) { signalStrength = Math.Max(signalStrength, connectedTransducer.SignalStrength); } } Vector2 transducerCenter = UseTransducers && connectedTransducers.Count > 0 ? GetTransducerCenter() : item.WorldPosition; if (item.Submarine != null && !DetectSubmarineWalls) { DrawDockingPorts(spriteBatch, transducerCenter, signalStrength); transducerCenter += DisplayOffset; DrawOwnSubmarineBorders(spriteBatch, transducerCenter, signalStrength); } else { DisplayOffset = Vector2.Zero; } if (sonarBlips.Count > 0) { zoomSqrt = (float)Math.Sqrt(zoom); spriteBatch.End(); spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Additive); foreach (SonarBlip sonarBlip in sonarBlips) { DrawBlip(spriteBatch, sonarBlip, transducerCenter, center, sonarBlip.FadeTimer / 2.0f * signalStrength); } spriteBatch.End(); spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend); } if (useDirectionalPing && IsActive) { Vector2 sector1 = MathUtils.RotatePointAroundTarget(pingDirection * DisplayRadius, Vector2.Zero, DirectionalPingSector * 0.5f); Vector2 sector2 = MathUtils.RotatePointAroundTarget(pingDirection * DisplayRadius, Vector2.Zero, -DirectionalPingSector * 0.5f); GUI.DrawLine(spriteBatch, center, center + sector1, Color.LightCyan * 0.2f, width: 3); GUI.DrawLine(spriteBatch, center, center + sector2, Color.LightCyan * 0.2f, width: 3); } if (GameMain.DebugDraw) { GUI.DrawString(spriteBatch, rect.Location.ToVector2(), sonarBlips.Count.ToString(), Color.White); } if (screenOverlay != null) { screenOverlay.Draw(spriteBatch, center, 0.0f, rect.Width / screenOverlay.size.X); } if (signalStrength <= 0.5f) { signalWarningText.Text = TextManager.Get(signalStrength <= 0.0f ? "SonarNoSignal" : "SonarSignalWeak"); signalWarningText.Color = signalStrength <= 0.0f ? Color.Red : Color.Orange; signalWarningText.Visible = true; } else { signalWarningText.Visible = false; } if (GameMain.GameSession == null) return; DrawMarker(spriteBatch, GameMain.GameSession.StartLocation.Name, (Level.Loaded.StartPosition - transducerCenter), displayScale, center, (rect.Width * 0.5f)); DrawMarker(spriteBatch, GameMain.GameSession.EndLocation.Name, (Level.Loaded.EndPosition - transducerCenter), displayScale, center, (rect.Width * 0.5f)); foreach (AITarget aiTarget in AITarget.List) { if (!aiTarget.Enabled) continue; if (string.IsNullOrEmpty(aiTarget.SonarLabel) || aiTarget.SoundRange <= 0.0f) continue; if (Vector2.DistanceSquared(aiTarget.WorldPosition, transducerCenter) < aiTarget.SoundRange * aiTarget.SoundRange) { DrawMarker(spriteBatch, aiTarget.SonarLabel, aiTarget.WorldPosition - transducerCenter, displayScale, center, (rect.Width * 0.47f)); } } if (GameMain.GameSession.Mission != null) { var mission = GameMain.GameSession.Mission; if (!string.IsNullOrWhiteSpace(mission.SonarLabel) && mission.SonarPosition != Vector2.Zero) { DrawMarker(spriteBatch, mission.SonarLabel, mission.SonarPosition - transducerCenter, displayScale, center, (rect.Width * 0.47f)); } } foreach (Submarine sub in Submarine.Loaded) { if (!sub.ShowSonarMarker) { continue; } if (UseTransducers ? connectedTransducers.Any(t => sub == t.Transducer.Item.Submarine || sub.DockedTo.Contains(t.Transducer.Item.Submarine)) : (sub == item.Submarine || sub.DockedTo.Contains(item.Submarine))) { continue; } if (sub.WorldPosition.Y > Level.Loaded.Size.Y) { continue; } DrawMarker(spriteBatch, sub.Name, sub.WorldPosition - transducerCenter, displayScale, center, (rect.Width * 0.45f)); } if (GameMain.DebugDraw) { var steering = item.GetComponent(); steering?.DebugDrawHUD(spriteBatch, transducerCenter, displayScale, DisplayRadius, center); } } private void DrawOwnSubmarineBorders(SpriteBatch spriteBatch, Vector2 transducerCenter, float signalStrength) { float simScale = displayScale * Physics.DisplayToSimRation * zoom; foreach (Submarine submarine in Submarine.Loaded) { if (UseTransducers ? !connectedTransducers.Any(t => submarine == t.Transducer.Item.Submarine || submarine.DockedTo.Contains(t.Transducer.Item.Submarine)) : submarine != item.Submarine && !submarine.DockedTo.Contains(item.Submarine)) continue; if (submarine.HullVertices == null) continue; Vector2 offset = ConvertUnits.ToSimUnits(submarine.WorldPosition - transducerCenter); for (int i = 0; i < submarine.HullVertices.Count; i++) { Vector2 start = (submarine.HullVertices[i] + offset) * simScale; start.Y = -start.Y; Vector2 end = (submarine.HullVertices[(i + 1) % submarine.HullVertices.Count] + offset) * simScale; end.Y = -end.Y; DrawLine(spriteBatch, start, end, Color.LightBlue * signalStrength * 0.5f, width: 3); } } } private void DrawLine(SpriteBatch spriteBatch, Vector2 start, Vector2 end, Color color, int width) { bool startOutside = start.LengthSquared() > DisplayRadius * DisplayRadius; bool endOutside = end.LengthSquared() > DisplayRadius * DisplayRadius; if (startOutside && endOutside) { return; } else if (startOutside) { if (MathUtils.GetLineCircleIntersections(Vector2.Zero, DisplayRadius, end, start, true, out Vector2? intersection1, out Vector2? intersection2) == 1) { GUI.DrawLine(spriteBatch, center + intersection1.Value, center + end, color, width: width); } } else if (endOutside) { if (MathUtils.GetLineCircleIntersections(Vector2.Zero, DisplayRadius, start, end, true, out Vector2? intersection1, out Vector2? intersection2) == 1) { GUI.DrawLine(spriteBatch, center + start, center + intersection1.Value, color, width: width); } } else { GUI.DrawLine(spriteBatch, center + start, center + end, color, width: width); } } private void DrawDockingPorts(SpriteBatch spriteBatch, Vector2 transducerCenter, float signalStrength) { float scale = displayScale * zoom; Steering steering = item.GetComponent(); if (steering != null && steering.DockingModeEnabled) { DisplayOffset = Vector2.Lerp(DisplayOffset, (steering.DockingSource.Item.WorldPosition + steering.DockingTarget.Item.WorldPosition) / 2.0f - transducerCenter, 0.1f); transducerCenter += DisplayOffset; Vector2 sourcePortDiff = (steering.DockingSource.Item.WorldPosition - transducerCenter) * scale; Vector2 sourcePortPos = new Vector2(sourcePortDiff.X, -sourcePortDiff.Y); Vector2 targetPortDiff = (steering.DockingTarget.Item.WorldPosition - transducerCenter) * scale; Vector2 targetPortPos = new Vector2(targetPortDiff.X, -targetPortDiff.Y); Vector2 midPos = (sourcePortPos + targetPortPos) / 2.0f; System.Diagnostics.Debug.Assert(steering.DockingSource.IsHorizontal == steering.DockingTarget.IsHorizontal); float xDist = Math.Abs(steering.DockingSource.Item.WorldPosition.X - steering.DockingTarget.Item.WorldPosition.X); float normalizedXDist = xDist / steering.DockingSource.DistanceTolerance.X; float yDist = Math.Abs(steering.DockingSource.Item.WorldPosition.Y - steering.DockingTarget.Item.WorldPosition.Y); float normalizedYDist = yDist / steering.DockingSource.DistanceTolerance.Y; Color xColor = normalizedXDist <= 1.0f ? Color.Lime : Color.Lerp(Color.Orange, Color.Red, normalizedXDist - 1.0f); Color yColor = normalizedYDist <= 1.0f ? Color.Lime : Color.Lerp(Color.Orange, Color.Red, normalizedYDist - 1.0f); if (steering.DockingSource.IsHorizontal) { if (yDist < steering.DockingSource.DistanceTolerance.Y) { DrawLine(spriteBatch, sourcePortPos, new Vector2(targetPortPos.X, sourcePortPos.Y), xColor, width: 3); } else { DrawLine(spriteBatch, sourcePortPos, new Vector2(midPos.X, sourcePortPos.Y), xColor, width: 3); DrawLine(spriteBatch, targetPortPos, new Vector2(midPos.X, targetPortPos.Y), xColor, width: 3); DrawLine(spriteBatch, new Vector2(midPos.X, sourcePortPos.Y), new Vector2(midPos.X, targetPortPos.Y), yColor, width: 3); } } else { if (xDist < steering.DockingSource.DistanceTolerance.X) { DrawLine(spriteBatch, sourcePortPos, new Vector2(sourcePortPos.X, targetPortPos.Y), yColor, width: 3); } else { DrawLine(spriteBatch, sourcePortPos, new Vector2(sourcePortPos.X, midPos.Y), yColor, width: 3); DrawLine(spriteBatch, targetPortPos, new Vector2(targetPortPos.X, midPos.Y), yColor, width: 3); DrawLine(spriteBatch, new Vector2(sourcePortPos.X, midPos.Y), new Vector2(targetPortPos.X, midPos.Y), xColor, width: 3); } } } else { DisplayOffset = Vector2.Lerp(DisplayOffset, Vector2.Zero, 0.1f); } foreach (DockingPort dockingPort in DockingPort.List) { if (Level.Loaded != null && dockingPort.Item.Submarine.WorldPosition.Y > Level.Loaded.Size.Y) { continue; } Vector2 offset = (dockingPort.Item.WorldPosition - transducerCenter) * scale; offset.Y = -offset.Y; if (offset.LengthSquared() > DisplayRadius * DisplayRadius) { continue; } Vector2 size = dockingPort.Item.Rect.Size.ToVector2() * scale; if (dockingPort.IsHorizontal) { size.X = 0.0f; } else { size.Y = 0.0f; } GUI.DrawLine(spriteBatch, center + offset - size / 2, center + offset + size / 2, Color.LightGreen, width: (int)(zoom)); } } private void UpdateDisruptions(Vector2 pingSource, float worldPingRadius, float worldPrevPingRadius) { float worldPingRadiusSqr = worldPingRadius * worldPingRadius; float worldPrevPingRadiusSqr = worldPrevPingRadius * worldPrevPingRadius; disruptedDirections.Clear(); if (Level.Loaded == null) { return; } foreach (LevelObject levelObject in Level.Loaded.LevelObjectManager.GetAllObjects(pingSource, range * pingState)) { if (levelObject.ActivePrefab?.SonarDisruption <= 0.0f) { continue; } float disruptionStrength = levelObject.ActivePrefab.SonarDisruption; Vector2 disruptionPos = new Vector2(levelObject.Position.X, levelObject.Position.Y); float disruptionDist = Vector2.Distance(pingSource, disruptionPos); disruptedDirections.Add(new Pair((disruptionPos - pingSource) / disruptionDist, disruptionStrength)); if (disruptionDist > worldPrevPingRadius && disruptionDist <= worldPingRadius) { for (int i = 0; i < disruptionStrength * Level.GridCellSize * 0.02f; i++) { var blip = new SonarBlip(disruptionPos + Rand.Vector(Rand.Range(0.0f, Level.GridCellSize * 4 * disruptionStrength)), MathHelper.Lerp(1.0f, 1.5f, disruptionStrength), Rand.Range(1.0f, 2.0f + disruptionStrength)); sonarBlips.Add(blip); } } } } private void Ping(Vector2 pingSource, Vector2 transducerPos, float pingRadius, float prevPingRadius, float displayScale, float range, bool passive, float pingStrength = 1.0f) { float prevPingRadiusSqr = prevPingRadius * prevPingRadius; float pingRadiusSqr = pingRadius * pingRadius; //inside a hull -> only show the edges of the hull if (item.CurrentHull != null && DetectSubmarineWalls) { CreateBlipsForLine( new Vector2(item.CurrentHull.WorldRect.X, item.CurrentHull.WorldRect.Y), new Vector2(item.CurrentHull.WorldRect.Right, item.CurrentHull.WorldRect.Y), pingSource, transducerPos, pingRadius, prevPingRadius, 50.0f, 5.0f, range, 2.0f, passive); CreateBlipsForLine( new Vector2(item.CurrentHull.WorldRect.X, item.CurrentHull.WorldRect.Y - item.CurrentHull.Rect.Height), new Vector2(item.CurrentHull.WorldRect.Right, item.CurrentHull.WorldRect.Y - item.CurrentHull.Rect.Height), pingSource, transducerPos, pingRadius, prevPingRadius, 50.0f, 5.0f, range, 2.0f, passive); CreateBlipsForLine( new Vector2(item.CurrentHull.WorldRect.X, item.CurrentHull.WorldRect.Y), new Vector2(item.CurrentHull.WorldRect.X, item.CurrentHull.WorldRect.Y - item.CurrentHull.Rect.Height), pingSource, transducerPos, pingRadius, prevPingRadius, 50.0f, 5.0f, range, 2.0f, passive); CreateBlipsForLine( new Vector2(item.CurrentHull.WorldRect.Right, item.CurrentHull.WorldRect.Y), new Vector2(item.CurrentHull.WorldRect.Right, item.CurrentHull.WorldRect.Y - item.CurrentHull.Rect.Height), pingSource, transducerPos, pingRadius, prevPingRadius, 50.0f, 5.0f, range, 2.0f, passive); return; } foreach (Submarine submarine in Submarine.Loaded) { if (submarine.HullVertices == null) continue; if (!DetectSubmarineWalls) { if (UseTransducers) { if (connectedTransducers.Any(t => submarine == t.Transducer.Item.Submarine || submarine.DockedTo.Contains(t.Transducer.Item.Submarine))) continue; } else { if (item.Submarine == submarine) continue; if (item.Submarine != null && item.Submarine.DockedTo.Contains(submarine)) continue; } } for (int i = 0; i < submarine.HullVertices.Count; i++) { Vector2 start = ConvertUnits.ToDisplayUnits(submarine.HullVertices[i]); Vector2 end = ConvertUnits.ToDisplayUnits(submarine.HullVertices[(i + 1) % submarine.HullVertices.Count]); if (item.Submarine == submarine) { start += Rand.Vector(500.0f); end += Rand.Vector(500.0f); } CreateBlipsForLine( start + submarine.WorldPosition, end + submarine.WorldPosition, pingSource, transducerPos, pingRadius, prevPingRadius, 200.0f, 2.0f, range, 1.0f, passive); } } if (Level.Loaded != null && (item.CurrentHull == null || !DetectSubmarineWalls)) { if (Level.Loaded.Size.Y - pingSource.Y < range) { CreateBlipsForLine( new Vector2(pingSource.X - range, Level.Loaded.Size.Y), new Vector2(pingSource.X + range, Level.Loaded.Size.Y), pingSource, transducerPos, pingRadius, prevPingRadius, 250.0f, 150.0f, range, pingStrength, passive); } List cells = Level.Loaded.GetCells(pingSource, 7); foreach (Voronoi2.VoronoiCell cell in cells) { foreach (Voronoi2.GraphEdge edge in cell.Edges) { if (!edge.IsSolid) continue; float cellDot = Vector2.Dot(cell.Center - pingSource, (edge.Center + cell.Translation) - cell.Center); if (cellDot > 0) continue; float facingDot = Vector2.Dot( Vector2.Normalize(edge.Point1 - edge.Point2), Vector2.Normalize(cell.Center - pingSource)); CreateBlipsForLine( edge.Point1 + cell.Translation, edge.Point2 + cell.Translation, pingSource, transducerPos, pingRadius, prevPingRadius, 350.0f, 3.0f * (Math.Abs(facingDot) + 1.0f), range, pingStrength, passive); } } foreach (RuinGeneration.Ruin ruin in Level.Loaded.Ruins) { if (!MathUtils.CircleIntersectsRectangle(pingSource, range, ruin.Area)) continue; foreach (var ruinShape in ruin.RuinShapes) { foreach (RuinGeneration.Line wall in ruinShape.Walls) { float cellDot = Vector2.Dot( Vector2.Normalize(ruinShape.Center - pingSource), Vector2.Normalize((wall.A + wall.B) / 2.0f - ruinShape.Center)); if (cellDot > 0) continue; CreateBlipsForLine( wall.A, wall.B, pingSource, transducerPos, pingRadius, prevPingRadius, 100.0f, 1000.0f, range, pingStrength, passive); } } } } foreach (Item item in Item.ItemList) { if (item.CurrentHull == null && item.Prefab.SonarSize > 0.0f) { float pointDist = ((item.WorldPosition - pingSource) * displayScale).LengthSquared(); if (pointDist > prevPingRadiusSqr && pointDist < pingRadiusSqr) { var blip = new SonarBlip( item.WorldPosition + Rand.Vector(item.Prefab.SonarSize), MathHelper.Clamp(item.Prefab.SonarSize, 0.1f, pingStrength), MathHelper.Clamp(item.Prefab.SonarSize * 0.1f, 0.1f, 10.0f)); if (!passive && !CheckBlipVisibility(blip, transducerPos)) continue; sonarBlips.Add(blip); } } } foreach (Character c in Character.CharacterList) { if (c.AnimController.CurrentHull != null || !c.Enabled) continue; if (DetectSubmarineWalls && c.AnimController.CurrentHull == null && item.CurrentHull != null) continue; foreach (Limb limb in c.AnimController.Limbs) { if (!limb.body.Enabled) { continue; } float pointDist = ((limb.WorldPosition - pingSource) * displayScale).LengthSquared(); if (limb.SimPosition == Vector2.Zero || pointDist > DisplayRadius * DisplayRadius) continue; if (pointDist > prevPingRadiusSqr && pointDist < pingRadiusSqr) { var blip = new SonarBlip( limb.WorldPosition + Rand.Vector(limb.Mass / 10.0f), MathHelper.Clamp(limb.Mass, 0.1f, pingStrength), MathHelper.Clamp(limb.Mass * 0.1f, 0.1f, 2.0f)); if (!passive && !CheckBlipVisibility(blip, transducerPos)) continue; sonarBlips.Add(blip); } } } } private void CreateBlipsForLine(Vector2 point1, Vector2 point2, Vector2 pingSource, Vector2 transducerPos, float pingRadius, float prevPingRadius, float lineStep, float zStep, float range, float pingStrength, bool passive) { lineStep /= zoom; zStep /= zoom; range *= displayScale; float length = (point1 - point2).Length(); Vector2 lineDir = (point2 - point1) / length; for (float x = 0; x < length; x += lineStep * Rand.Range(0.8f, 1.2f)) { Vector2 point = point1 + lineDir * x; //ignore if outside the display Vector2 transducerDiff = point - transducerPos; Vector2 transducerDisplayDiff = transducerDiff * displayScale; if (transducerDisplayDiff.LengthSquared() > DisplayRadius * DisplayRadius) continue; //ignore if the point is not within the ping Vector2 pointDiff = point - pingSource; Vector2 displayPointDiff = pointDiff * displayScale; float displayPointDistSqr = displayPointDiff.LengthSquared(); if (displayPointDistSqr < prevPingRadius * prevPingRadius || displayPointDistSqr > pingRadius * pingRadius) continue; //ignore if direction is disrupted float transducerDist = transducerDiff.Length(); Vector2 pingDirection = transducerDiff / transducerDist; bool disrupted = false; foreach (Pair disruptDir in disruptedDirections) { float dot = Vector2.Dot(pingDirection, disruptDir.First); if (dot > 1.0f - disruptDir.Second) { disrupted = true; break; } } if (disrupted) continue; float displayPointDist = (float)Math.Sqrt(displayPointDistSqr); float alpha = pingStrength * Rand.Range(1.5f, 2.0f); for (float z = 0; z < DisplayRadius - transducerDist * displayScale; z += zStep) { Vector2 pos = point + Rand.Vector(150.0f / zoom) + pingDirection * z / displayScale; float fadeTimer = alpha * (1.0f - displayPointDist / range); int minDist = (int)(200 / zoom); sonarBlips.RemoveAll(b => b.FadeTimer < fadeTimer && Math.Abs(pos.X - b.Position.X) < minDist && Math.Abs(pos.Y - b.Position.Y) < minDist); var blip = new SonarBlip(pos, fadeTimer, 1.0f + ((displayPointDist + z) / DisplayRadius)); if (!passive && !CheckBlipVisibility(blip, transducerPos)) continue; sonarBlips.Add(blip); zStep += 0.5f / zoom; if (z == 0) { alpha = Math.Min(alpha - 0.5f, 1.5f); } else { alpha -= 0.1f; } if (alpha < 0) break; } } } private bool CheckBlipVisibility(SonarBlip blip, Vector2 transducerPos) { Vector2 pos = (blip.Position - transducerPos) * displayScale * zoom; pos.Y = -pos.Y; float posDistSqr = pos.LengthSquared(); if (posDistSqr > DisplayRadius * DisplayRadius) { blip.FadeTimer = 0.0f; return false; } Vector2 dir = pos / (float)Math.Sqrt(posDistSqr); if (isLastPingDirectional) { if (Vector2.Dot(lastPingDirection, dir) < DirectionalPingDotProduct) { blip.FadeTimer = 0.0f; return false; } } return true; } private void DrawBlip(SpriteBatch spriteBatch, SonarBlip blip, Vector2 transducerPos, Vector2 center, float strength) { strength = MathHelper.Clamp(strength, 0.0f, 1.0f); float distort = 1.0f - item.Condition / item.MaxCondition; Vector2 pos = (blip.Position - transducerPos) * displayScale * zoom; pos.Y = -pos.Y; if (Rand.Range(0.5f, 2.0f) < distort) pos.X = -pos.X; if (Rand.Range(0.5f, 2.0f) < distort) pos.Y = -pos.Y; float posDistSqr = pos.LengthSquared(); if (posDistSqr > DisplayRadius * DisplayRadius) { blip.FadeTimer = 0.0f; return; } if (sonarBlip == null) { GUI.DrawRectangle(spriteBatch, center + pos, Vector2.One * 4, Color.Magenta, true); return; } Vector2 dir = pos / (float)Math.Sqrt(posDistSqr); Vector2 normal = new Vector2(dir.Y, -dir.X); float scale = (strength + 3.0f) * blip.Scale * zoomSqrt; Color color = ToolBox.GradientLerp(strength, blipColorGradient); sonarBlip.Draw(spriteBatch, center + pos, color, sonarBlip.Origin, blip.Rotation ?? MathUtils.VectorToAngle(pos), blip.Size * scale * 0.04f, SpriteEffects.None, 0); pos += Rand.Range(0.0f, 1.0f) * dir + Rand.Range(-scale, scale) * normal; sonarBlip.Draw(spriteBatch, center + pos, color * 0.5f, sonarBlip.Origin, 0, scale * 0.08f, SpriteEffects.None, 0); } private void DrawMarker(SpriteBatch spriteBatch, string label, Vector2 position, float scale, Vector2 center, float radius) { float dist = position.Length(); position *= zoom; position *= scale; position.Y = -position.Y; float textAlpha = MathHelper.Clamp(1.5f - dist / 50000.0f, 0.5f, 1.0f); Vector2 dir = Vector2.Normalize(position); Vector2 markerPos = (dist * zoom * scale > radius) ? dir * radius : position; markerPos += center; markerPos.X = (int)markerPos.X; markerPos.Y = (int)markerPos.Y; float alpha = 1.0f; if (dist * scale < radius) { float normalizedDist = dist * scale / radius; alpha = Math.Max(normalizedDist - 0.4f, 0.0f); float mouseDist = Vector2.Distance(PlayerInput.MousePosition, markerPos); float hoverThreshold = 150.0f; if (mouseDist < hoverThreshold) { alpha += (hoverThreshold - mouseDist) / hoverThreshold; } } if (!GuiFrame.Children.First().Rect.Contains(markerPos)) { if (MathUtils.GetLineRectangleIntersection(center, markerPos, GuiFrame.Children.First().Rect, out Vector2 intersection)) { markerPos = intersection; } } GUI.DrawRectangle(spriteBatch, new Rectangle((int)markerPos.X - 3, (int)markerPos.Y - 3, 6, 6), Color.Red, thickness: 2); if (alpha <= 0.0f) { return; } string wrappedLabel = ToolBox.WrapText(label, 150, GUI.SmallFont); wrappedLabel += "\n" + ((int)(dist * Physics.DisplayToRealWorldRatio) + " m"); Vector2 labelPos = markerPos; Vector2 textSize = GUI.SmallFont.MeasureString(wrappedLabel); //flip the text to left side when the marker is on the left side or goes outside the right edge of the interface if ((dir.X < 0.0f || labelPos.X + textSize.X + 10 > GuiFrame.Rect.X) && labelPos.X - textSize.X > 0) labelPos.X -= textSize.X + 10; GUI.DrawString(spriteBatch, new Vector2(labelPos.X + 10, labelPos.Y), wrappedLabel, Color.LightBlue * textAlpha * alpha, Color.Black * textAlpha * 0.8f * alpha, 2, GUI.SmallFont); } public void ClientWrite(Lidgren.Network.NetBuffer msg, object[] extraData = null) { msg.Write(IsActive); if (IsActive) { msg.WriteRangedSingle(zoom, MinZoom, MaxZoom, 8); msg.Write(useDirectionalPing); if (useDirectionalPing) { msg.WriteRangedSingle(directionalSlider.BarScroll, 0.0f, 1.0f, 8); } } } public void ClientRead(ServerNetObject type, Lidgren.Network.NetBuffer msg, float sendingTime) { long msgStartPos = msg.Position; bool isActive = msg.ReadBoolean(); float zoomT = 1.0f; bool directionalPing = useDirectionalPing; float directionT = 0.0f; if (isActive) { zoomT = msg.ReadRangedSingle(0.0f, 1.0f, 8); directionalPing = msg.ReadBoolean(); if (directionalPing) { directionT = msg.ReadRangedSingle(0.0f, 1.0f, 8); } } if (correctionTimer > 0.0f) { int msgLength = (int)(msg.Position - msgStartPos); msg.Position = msgStartPos; StartDelayedCorrection(type, msg.ExtractBits(msgLength), sendingTime); return; } IsActive = isActive; if (isActive) { activeTickBox.Selected = true; zoomSlider.BarScroll = zoomT; zoom = MathHelper.Lerp(MinZoom, MaxZoom, zoomT); if (directionalPing) { directionalSlider.BarScroll = directionT; float pingAngle = MathHelper.Lerp(0.0f, MathHelper.TwoPi, directionalSlider.BarScroll); pingDirection = new Vector2((float)Math.Cos(pingAngle), (float)Math.Sin(pingAngle)); } useDirectionalPing = directionalTickBox.Selected = directionalPing; } else { passiveTickBox.Selected = true; } } } class SonarBlip { public float FadeTimer; public Vector2 Position; public float Scale; public Vector2 Velocity; public float? Rotation; public Vector2 Size; public SonarBlip(Vector2 pos, float fadeTimer, float scale) { Position = pos; FadeTimer = Math.Max(fadeTimer, 0.0f); Scale = scale; Size = new Vector2(0.5f, 1.0f); } } }