using Barotrauma.Extensions; using Barotrauma.Networking; using FarseerPhysics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using System.Linq; namespace Barotrauma.Items.Components { partial class Steering : Powered, IServerSerializable, IClientSerializable { private GUIButton steeringModeSwitch; private GUITickBox autopilotIndicator, manualPilotIndicator; enum Destination { MaintainPos, LevelEnd, LevelStart }; private GUITickBox maintainPosTickBox, levelEndTickBox, levelStartTickBox; private GUIComponent statusContainer, dockingContainer; public GUIComponent ControlContainer { get; private set; } private bool dockingNetworkMessagePending; private GUIButton dockingButton; private LocalizedString dockText, undockText; private GUIComponent steerArea; private GUITextBlock pressureWarningText, iceSpireWarningText; private GUITextBlock tipContainer; private LocalizedString noPowerTip, autoPilotMaintainPosTip, autoPilotLevelStartTip, autoPilotLevelEndTip; private Sprite maintainPosIndicator, maintainPosOriginIndicator; private Sprite steeringIndicator; private List connectedPorts = new List(); private float checkConnectedPortsTimer; private const float CheckConnectedPortsInterval = 1.0f; public DockingPort ActiveDockingSource, DockingTarget; private Vector2 keyboardInput = Vector2.Zero; private float inputCumulation; private bool? swapDestinationOrder; private GUIMessageBox enterOutpostPrompt, exitOutpostPrompt; private bool levelStartSelected; [Serialize(defaultValue: false, isSaveable: IsPropertySaveable.Yes, AlwaysUseInstanceValues = true)] public bool LevelStartSelected { get { return levelStartTickBox?.Selected ?? levelStartSelected; } set { TrySetTickBoxSelected(levelStartTickBox, ref levelStartSelected, value); } } private bool levelEndSelected; [Serialize(defaultValue: false, isSaveable: IsPropertySaveable.Yes, AlwaysUseInstanceValues = true)] public bool LevelEndSelected { get { return levelEndTickBox?.Selected ?? levelEndSelected; } set { TrySetTickBoxSelected(levelEndTickBox, ref levelEndSelected, value); } } private bool maintainPos; [Serialize(defaultValue: false, isSaveable: IsPropertySaveable.Yes, AlwaysUseInstanceValues = true)] public bool MaintainPos { get { return maintainPosTickBox?.Selected ?? maintainPos; } set { TrySetTickBoxSelected(maintainPosTickBox, ref maintainPos, value); } } private float steerRadius; public float? SteerRadius { get { return steerRadius; } set { steerRadius = value ?? (steerArea.Rect.Width / 2); } } private bool disableControls; /// /// Can be used by status effects to disable all the UI controls /// public bool DisableControls { get { return disableControls; } set { if (disableControls == value) { return; } disableControls = value; UpdateGUIElements(); } } public override bool RecreateGUIOnResolutionChange => true; partial void InitProjSpecific(ContentXElement element) { foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "steeringindicator": steeringIndicator = new Sprite(subElement); break; case "maintainposindicator": maintainPosIndicator = new Sprite(subElement); break; case "maintainposoriginindicator": maintainPosOriginIndicator = new Sprite(subElement); break; } } CreateGUI(); } protected override void CreateGUI() { ControlContainer = new GUIFrame(new RectTransform(new Vector2(Sonar.controlBoxSize.X, 1 - Sonar.controlBoxSize.Y * 2), GuiFrame.RectTransform, Anchor.CenterRight), "ItemUI"); var paddedControlContainer = new GUIFrame(new RectTransform(ControlContainer.Rect.Size - GUIStyle.ItemFrameMargin, ControlContainer.RectTransform, Anchor.Center) { AbsoluteOffset = GUIStyle.ItemFrameOffset }, style: null); var steeringModeArea = new GUIFrame(new RectTransform(new Vector2(1, 0.4f), paddedControlContainer.RectTransform, Anchor.TopLeft), style: null); steeringModeSwitch = new GUIButton(new RectTransform(new Vector2(0.2f, 1), steeringModeArea.RectTransform), string.Empty, style: "SwitchVertical") { UserData = UIHighlightAction.ElementId.SteeringModeSwitch, Selected = autoPilot, Enabled = true, ClickSound = GUISoundType.UISwitch, OnClicked = (button, data) => { button.Selected = !button.Selected; AutoPilot = button.Selected; if (GameMain.Client != null) { unsentChanges = true; user = Character.Controlled; } return true; } }; var steeringModeRightSide = new GUIFrame(new RectTransform(new Vector2(1.0f - steeringModeSwitch.RectTransform.RelativeSize.X, 0.8f), steeringModeArea.RectTransform, Anchor.CenterLeft) { RelativeOffset = new Vector2(steeringModeSwitch.RectTransform.RelativeSize.X, 0) }, style: null); manualPilotIndicator = new GUITickBox(new RectTransform(new Vector2(1, 0.45f), steeringModeRightSide.RectTransform, Anchor.TopLeft), TextManager.Get("SteeringManual"), font: GUIStyle.SubHeadingFont, style: "IndicatorLightRedSmall") { Selected = !autoPilot, Enabled = false }; autopilotIndicator = new GUITickBox(new RectTransform(new Vector2(1, 0.45f), steeringModeRightSide.RectTransform, Anchor.BottomLeft), TextManager.Get("SteeringAutoPilot"), font: GUIStyle.SubHeadingFont, style: "IndicatorLightRedSmall") { Selected = autoPilot, Enabled = false }; manualPilotIndicator.TextBlock.OverrideTextColor(GUIStyle.TextColorNormal); autopilotIndicator.TextBlock.OverrideTextColor(GUIStyle.TextColorNormal); GUITextBlock.AutoScaleAndNormalize(manualPilotIndicator.TextBlock, autopilotIndicator.TextBlock); var autoPilotControls = new GUIFrame(new RectTransform(new Vector2(0.75f, 0.62f), paddedControlContainer.RectTransform, Anchor.BottomCenter), "OutlineFrame"); var paddedAutoPilotControls = new GUIFrame(new RectTransform(new Vector2(0.92f, 0.88f), autoPilotControls.RectTransform, Anchor.Center), style: null); int textLimit = (int)(paddedAutoPilotControls.Rect.Width * 0.75f); maintainPosTickBox = new GUITickBox(new RectTransform(new Vector2(1, 0.333f), paddedAutoPilotControls.RectTransform, Anchor.TopCenter), ToolBox.LimitString(TextManager.Get("SteeringMaintainPos"), GUIStyle.SmallFont, textLimit), font: GUIStyle.SmallFont, style: "GUIRadioButton") { UserData = UIHighlightAction.ElementId.MaintainPosTickBox, Enabled = autoPilot, Selected = maintainPos, OnSelected = tickBox => { if (maintainPos != tickBox.Selected) { unsentChanges = true; user = Character.Controlled; maintainPos = tickBox.Selected; if (maintainPos) { if (controlledSub == null) { posToMaintain = null; } else { posToMaintain = controlledSub.WorldPosition; } } else if (!LevelEndSelected && !LevelStartSelected) { AutoPilot = false; } if (!maintainPos) { posToMaintain = null; } } return true; } }; levelStartTickBox = new GUITickBox(new RectTransform(new Vector2(1, 0.333f), paddedAutoPilotControls.RectTransform, Anchor.Center), GameMain.GameSession?.StartLocation == null ? "" : ToolBox.LimitString(GameMain.GameSession.StartLocation.DisplayName, GUIStyle.SmallFont, textLimit), font: GUIStyle.SmallFont, style: "GUIRadioButton") { Enabled = autoPilot, Selected = levelStartSelected, OnSelected = tickBox => { if (levelStartSelected != tickBox.Selected) { unsentChanges = true; user = Character.Controlled; levelStartSelected = tickBox.Selected; levelEndSelected = !levelStartSelected; if (levelStartSelected) { UpdatePath(); } else if (!MaintainPos && !LevelEndSelected) { AutoPilot = false; } } return true; } }; levelEndTickBox = new GUITickBox(new RectTransform(new Vector2(1, 0.333f), paddedAutoPilotControls.RectTransform, Anchor.BottomCenter), (GameMain.GameSession?.EndLocation == null || Level.IsLoadedOutpost) ? "" : ToolBox.LimitString(GameMain.GameSession.EndLocation.DisplayName, GUIStyle.SmallFont, textLimit), font: GUIStyle.SmallFont, style: "GUIRadioButton") { Enabled = autoPilot, Selected = levelEndSelected, Visible = GameMain.GameSession?.EndLocation != null, OnSelected = tickBox => { if (levelEndSelected != tickBox.Selected) { unsentChanges = true; user = Character.Controlled; levelEndSelected = tickBox.Selected; levelStartSelected = !levelEndSelected; if (levelEndSelected) { UpdatePath(); } else if (!MaintainPos && !LevelStartSelected) { AutoPilot = false; } } return true; } }; maintainPosTickBox.RectTransform.IsFixedSize = levelStartTickBox.RectTransform.IsFixedSize = levelEndTickBox.RectTransform.IsFixedSize = false; maintainPosTickBox.RectTransform.MaxSize = levelStartTickBox.RectTransform.MaxSize = levelEndTickBox.RectTransform.MaxSize = new Point(int.MaxValue, paddedAutoPilotControls.Rect.Height / 3); maintainPosTickBox.RectTransform.MinSize = levelStartTickBox.RectTransform.MinSize = levelEndTickBox.RectTransform.MinSize = Point.Zero; GUITextBlock.AutoScaleAndNormalize(scaleHorizontal: false, scaleVertical: true, maintainPosTickBox.TextBlock, levelStartTickBox.TextBlock, levelEndTickBox.TextBlock); GUIRadioButtonGroup destinations = new GUIRadioButtonGroup(); destinations.AddRadioButton((int)Destination.MaintainPos, maintainPosTickBox); destinations.AddRadioButton((int)Destination.LevelStart, levelStartTickBox); destinations.AddRadioButton((int)Destination.LevelEnd, levelEndTickBox); destinations.Selected = (int)(maintainPos ? Destination.MaintainPos : levelStartSelected ? Destination.LevelStart : Destination.LevelEnd); // Status -> statusContainer = new GUIFrame(new RectTransform(Sonar.controlBoxSize, GuiFrame.RectTransform, Anchor.BottomRight) { RelativeOffset = Sonar.controlBoxOffset }, "ItemUI"); var paddedStatusContainer = new GUIFrame(new RectTransform(statusContainer.Rect.Size - GUIStyle.ItemFrameMargin, statusContainer.RectTransform, Anchor.Center, isFixedSize: false) { AbsoluteOffset = GUIStyle.ItemFrameOffset }, style: null); var elements = GUI.CreateElements(3, new Vector2(1f, 0.333f), paddedStatusContainer.RectTransform, rt => new GUIFrame(rt, style: null), Anchor.TopCenter, relativeSpacing: 0.01f); List leftElements = new List(), centerElements = new List(), rightElements = new List(); for (int i = 0; i < elements.Count; i++) { var e = elements[i]; var group = new GUILayoutGroup(new RectTransform(Vector2.One, e.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { RelativeSpacing = 0.01f, Stretch = true }; var left = new GUIFrame(new RectTransform(new Vector2(0.45f, 1), group.RectTransform), style: null); var center = new GUIFrame(new RectTransform(new Vector2(0.15f, 1), group.RectTransform), style: null); var right = new GUIFrame(new RectTransform(new Vector2(0.4f, 0.8f), group.RectTransform), style: null); leftElements.Add(left); centerElements.Add(center); rightElements.Add(right); LocalizedString leftText = string.Empty, centerText = string.Empty; GUITextBlock.TextGetterHandler rightTextGetter = null; switch (i) { case 0: leftText = TextManager.Get("DescentVelocity"); centerText = TextManager.Get("KilometersPerHour"); rightTextGetter = () => { Vector2 vel = controlledSub == null ? Vector2.Zero : controlledSub.Velocity; var realWorldVel = ConvertUnits.ToDisplayUnits(vel.Y * Physics.DisplayToRealWorldRatio) * 3.6f; return (-realWorldVel).ToString("0.0"); }; break; case 1: leftText = TextManager.Get("Velocity"); centerText = TextManager.Get("KilometersPerHour"); rightTextGetter = () => { Vector2 vel = controlledSub == null ? Vector2.Zero : controlledSub.Velocity; var realWorldVel = ConvertUnits.ToDisplayUnits(vel.X * Physics.DisplayToRealWorldRatio) * 3.6f; if (controlledSub != null && controlledSub.FlippedX) { realWorldVel *= -1; } return realWorldVel.ToString("0.0"); }; break; case 2: leftText = TextManager.Get("Depth"); centerText = TextManager.Get("Meter"); rightTextGetter = () => { if (Level.Loaded is { IsEndBiome: true }) { return Timing.TotalTime % 5.0f < 0.5f ? Rand.Range(-9000, 9000).ToString() : "ERROR"; } float realWorldDepth = controlledSub == null ? -1000.0f : controlledSub.RealWorldDepth; return ((int)realWorldDepth).ToString(); }; break; } new GUITextBlock(new RectTransform(Vector2.One, left.RectTransform), leftText, font: GUIStyle.SubHeadingFont, wrap: leftText.Contains(" "), textAlignment: Alignment.CenterRight); new GUITextBlock(new RectTransform(Vector2.One, center.RectTransform), centerText, font: GUIStyle.Font, textAlignment: Alignment.Center) { Padding = Vector4.Zero }; var digitalFrame = new GUIFrame(new RectTransform(Vector2.One, right.RectTransform), style: "DigitalFrameDark"); new GUITextBlock(new RectTransform(Vector2.One * 0.85f, digitalFrame.RectTransform, Anchor.Center), "12345", GUIStyle.TextColorDark, GUIStyle.DigitalFont, Alignment.CenterRight) { TextGetter = rightTextGetter }; } GUITextBlock.AutoScaleAndNormalize(leftElements.SelectMany(e => e.GetAllChildren())); GUITextBlock.AutoScaleAndNormalize(centerElements.SelectMany(e => e.GetAllChildren())); GUITextBlock.AutoScaleAndNormalize(rightElements.SelectMany(e => e.GetAllChildren())); //docking interface ---------------------------------------------------- float dockingButtonSize = 1.1f; float elementScale = 0.6f; dockingContainer = new GUIFrame(new RectTransform(Sonar.controlBoxSize, GuiFrame.RectTransform, Anchor.BottomRight, scaleBasis: ScaleBasis.Smallest) { RelativeOffset = new Vector2(Sonar.controlBoxOffset.X + 0.05f, -0.05f) }, style: null); dockText = TextManager.Get("label.navterminaldock", "captain.dock"); undockText = TextManager.Get("label.navterminalundock", "captain.undock"); dockingButton = new GUIButton(new RectTransform(new Vector2(elementScale), dockingContainer.RectTransform, Anchor.Center), dockText, style: "PowerButton") { OnClicked = (btn, userdata) => { if (GameMain.GameSession?.Missions.Any(m => !m.AllowUndocking) ?? false) { new GUIMessageBox("", TextManager.Get("undockingdisabledbymission")); return false; } else if (GameMain.GameSession?.Campaign is CampaignMode campaign) { if (Level.IsLoadedOutpost && DockingSources.Any(d => d.Docked && (d.DockingTarget?.Item.Submarine?.Info?.IsOutpost ?? false))) { // Undocking from an outpost if (!ObjectiveManager.AllActiveObjectivesCompleted()) { exitOutpostPrompt = new GUIMessageBox("", TextManager.GetWithVariable("CampaignExitTutorialOutpostPrompt", "[locationname]", campaign.Map.CurrentLocation.DisplayName), new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") }); exitOutpostPrompt.Buttons[0].OnClicked += (_, _) => { exitOutpostPrompt.Close(); return OpenMap(campaign); }; exitOutpostPrompt.Buttons[1].OnClicked += exitOutpostPrompt.Close; return false; } return OpenMap(campaign); } else if (!Level.IsLoadedOutpost && DockingModeEnabled && ActiveDockingSource != null && !ActiveDockingSource.Docked && DockingTarget?.Item?.Submarine == Level.Loaded.StartOutpost && (DockingTarget?.Item?.Submarine?.Info.IsOutpost ?? false)) { // Docking to an outpost var subsToLeaveBehind = CampaignMode.GetSubsToLeaveBehind(Item.Submarine); if (subsToLeaveBehind.Any()) { enterOutpostPrompt = new GUIMessageBox( TextManager.GetWithVariable("enterlocation", "[locationname]", DockingTarget.Item.Submarine.Info.Name), TextManager.Get(subsToLeaveBehind.Count == 1 ? "LeaveSubBehind" : "LeaveSubsBehind"), new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") }); } else { enterOutpostPrompt = new GUIMessageBox("", TextManager.GetWithVariable("campaignenteroutpostprompt", "[locationname]", DockingTarget.Item.Submarine.Info.Name), new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") }); } enterOutpostPrompt.Buttons[0].OnClicked += (btn, userdata) => { SendDockingSignal(); enterOutpostPrompt.Close(); return true; }; enterOutpostPrompt.Buttons[1].OnClicked += enterOutpostPrompt.Close; return false; } } SendDockingSignal(); return true; } }; bool OpenMap(CampaignMode campaign) { campaign.ShowCampaignUI = true; campaign.CampaignUI.SelectTab(CampaignMode.InteractionType.Map); return false; } void SendDockingSignal() { if (GameMain.Client == null) { item.SendSignal(new Signal("1", sender: Character.Controlled), "toggle_docking"); } else { dockingNetworkMessagePending = true; item.CreateClientEvent(this); } } dockingButton.Font = GUIStyle.SubHeadingFont; dockingButton.TextBlock.RectTransform.MaxSize = new Point((int)(dockingButton.Rect.Width * 0.7f), int.MaxValue); dockingButton.TextBlock.AutoScaleHorizontal = true; var style = GUIStyle.GetComponentStyle("DockingButtonUp"); Sprite buttonSprite = style.Sprites.FirstOrDefault().Value.FirstOrDefault()?.Sprite; Point buttonSize = buttonSprite != null ? buttonSprite.size.ToPoint() : new Point(149, 52); Point horizontalButtonSize = buttonSize.Multiply(elementScale * GUI.Scale * dockingButtonSize); Point verticalButtonSize = horizontalButtonSize.Flip(); var leftButton = new GUIButton(new RectTransform(verticalButtonSize, dockingContainer.RectTransform, Anchor.CenterLeft), "", style: "DockingButtonLeft") { OnClicked = NudgeButtonClicked, UserData = -Vector2.UnitX }; var rightButton = new GUIButton(new RectTransform(verticalButtonSize, dockingContainer.RectTransform, Anchor.CenterRight), "", style: "DockingButtonRight") { OnClicked = NudgeButtonClicked, UserData = Vector2.UnitX }; var upButton = new GUIButton(new RectTransform(horizontalButtonSize, dockingContainer.RectTransform, Anchor.TopCenter), "", style: "DockingButtonUp") { OnClicked = NudgeButtonClicked, UserData = Vector2.UnitY }; var downButton = new GUIButton(new RectTransform(horizontalButtonSize, dockingContainer.RectTransform, Anchor.BottomCenter), "", style: "DockingButtonDown") { OnClicked = NudgeButtonClicked, UserData = -Vector2.UnitY }; // Sonar area steerArea = new GUICustomComponent(new RectTransform(Sonar.GUISizeCalculation, GuiFrame.RectTransform, Anchor.CenterLeft, scaleBasis: ScaleBasis.Smallest), (spriteBatch, guiCustomComponent) => { DrawHUD(spriteBatch, guiCustomComponent.Rect); }, null); steerRadius = steerArea.Rect.Width / 2; iceSpireWarningText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.25f), steerArea.RectTransform, Anchor.Center, Pivot.TopCenter), TextManager.Get("NavTerminalIceSpireWarning"), GUIStyle.Red, GUIStyle.SubHeadingFont, Alignment.Center, color: Color.Black * 0.8f, wrap: true) { Visible = false }; pressureWarningText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.25f), steerArea.RectTransform, Anchor.Center, Pivot.TopCenter), TextManager.Get("SteeringDepthWarning"), GUIStyle.Red, GUIStyle.SubHeadingFont, Alignment.Center, color: Color.Black * 0.8f) { Visible = false }; // Tooltip/helper text tipContainer = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.1f), steerArea.RectTransform, Anchor.BottomCenter, Pivot.TopCenter) , "", font: GUIStyle.Font, wrap: true, style: "GUIToolTip", textAlignment: Alignment.Center) { AutoScaleHorizontal = true }; noPowerTip = TextManager.Get("SteeringNoPowerTip"); autoPilotMaintainPosTip = TextManager.Get("SteeringAutoPilotMaintainPosTip"); autoPilotLevelStartTip = TextManager.GetWithVariable("SteeringAutoPilotLocationTip", "[locationname]", GameMain.GameSession?.StartLocation == null ? "Start" : GameMain.GameSession.StartLocation.DisplayName); autoPilotLevelEndTip = TextManager.GetWithVariable("SteeringAutoPilotLocationTip", "[locationname]", GameMain.GameSession?.EndLocation == null ? "End" : GameMain.GameSession.EndLocation.DisplayName); } protected override void OnResolutionChanged() { UpdateGUIElements(); } /// /// Makes the sonar view CustomComponent render the steering HUD, preventing it from being drawn behing the sonar /// public void AttachToSonarHUD(GUICustomComponent sonarView) { steerArea.Visible = false; sonarView.OnDraw += (spriteBatch, guiCustomComponent) => { DrawHUD(spriteBatch, guiCustomComponent.Rect); steerArea.DrawChildren(spriteBatch, recursive: true); }; } /// /// Map the rectangular steering vector to a circular area using FG-Squircular Mapping which preserves the angle of the vector. /// private static Vector2 MapSquareToCircle(Vector2 steeringVector) { float xSqr = steeringVector.X * steeringVector.X; float ySqr = steeringVector.Y * steeringVector.Y; float length = MathF.Sqrt(ySqr + xSqr); if (MathUtils.NearlyEqual(length, 0.0f)) { return Vector2.Zero; } //FG-Squircular mapping formula from https://arxiv.org/ftp/arxiv/papers/1509/1509.06344.pdf float x = steeringVector.X * MathF.Sqrt(xSqr + ySqr - xSqr * ySqr) / length; float y = steeringVector.Y * MathF.Sqrt(xSqr + ySqr - xSqr * ySqr) / length; return new Vector2(x, y); } public void DrawHUD(SpriteBatch spriteBatch, Rectangle rect) { int width = rect.Width, height = rect.Height; int x = rect.X; int y = rect.Y; if (!HasPower) { return; } Rectangle velRect = new Rectangle(x + 20, y + 20, width - 40, height - 40); Vector2 steeringOrigin = steerArea.Rect.Center.ToVector2(); if (!AutoPilot) { //map input from rectangle to circle Vector2 steeringInputPos = MapSquareToCircle(steeringInput / 100f) * 100.0f; steeringInputPos.Y = -steeringInputPos.Y; steeringInputPos += steeringOrigin; if (steeringIndicator != null) { Vector2 dir = steeringInputPos - steeringOrigin; float angle = (float)Math.Atan2(dir.Y, dir.X); steeringIndicator.Draw(spriteBatch, steeringOrigin, Color.White, origin: steeringIndicator.Origin, rotate: angle, scale: new Vector2(dir.Length() / steeringIndicator.size.X, 1.0f)); } else { GUI.DrawLine(spriteBatch, steeringOrigin, steeringInputPos, Color.LightGray); GUI.DrawRectangle(spriteBatch, new Rectangle((int)steeringInputPos.X - 5, (int)steeringInputPos.Y - 5, 10, 10), Color.White); } if (velRect.Contains(PlayerInput.MousePosition)) { GUI.DrawRectangle(spriteBatch, new Rectangle((int)steeringInputPos.X - 4, (int)steeringInputPos.Y - 4, 8, 8), GUIStyle.Red, thickness: 2); } } else if (posToMaintain.HasValue && !LevelStartSelected && !LevelEndSelected) { Sonar sonar = item.GetComponent(); if (sonar != null && controlledSub != null) { Vector2 displayPosToMaintain = ((posToMaintain.Value - controlledSub.WorldPosition)) * sonar.DisplayScale; displayPosToMaintain.Y = -displayPosToMaintain.Y; displayPosToMaintain = displayPosToMaintain.ClampLength(velRect.Width / 2); displayPosToMaintain = steerArea.Rect.Center.ToVector2() + displayPosToMaintain; Color crosshairColor = GUIStyle.Orange * (0.5f + ((float)Math.Sin(Timing.TotalTime * 5.0f) + 1.0f) / 4.0f); if (maintainPosIndicator != null) { maintainPosIndicator.Draw(spriteBatch, displayPosToMaintain, crosshairColor, scale: 0.5f * sonar.Zoom); } else { float crossHairSize = 8.0f; GUI.DrawLine(spriteBatch, displayPosToMaintain + Vector2.UnitY * crossHairSize, displayPosToMaintain - Vector2.UnitY * crossHairSize, crosshairColor, width: 3); GUI.DrawLine(spriteBatch, displayPosToMaintain + Vector2.UnitX * crossHairSize, displayPosToMaintain - Vector2.UnitX * crossHairSize, crosshairColor, width: 3); } if (maintainPosOriginIndicator != null) { maintainPosOriginIndicator.Draw(spriteBatch, steeringOrigin, GUIStyle.Orange, scale: 0.5f * sonar.Zoom); } else { GUI.DrawRectangle(spriteBatch, new Rectangle((int)steeringOrigin.X - 5, (int)steeringOrigin.Y - 5, 10, 10), GUIStyle.Orange); } } } //map velocity from rectangle to circle Vector2 steeringPos = MapSquareToCircle(targetVelocity / 100f) * 90.0f; steeringPos.Y = -steeringPos.Y; steeringPos += steeringOrigin; if (steeringIndicator != null) { Vector2 dir = steeringPos - steeringOrigin; float angle = (float)Math.Atan2(dir.Y, dir.X); steeringIndicator.Draw(spriteBatch, steeringOrigin, Color.Gray, origin: steeringIndicator.Origin, rotate: angle, scale: new Vector2(dir.Length() / steeringIndicator.size.X, 0.7f)); } else { GUI.DrawLine(spriteBatch, steeringOrigin, steeringPos, Color.CadetBlue, 0, 2); } } public void DebugDrawHUD(SpriteBatch spriteBatch, Vector2 transducerCenter, float displayScale, float displayRadius, Vector2 center) { if (SteeringPath == null) return; Vector2 prevPos = Vector2.Zero; foreach (WayPoint wp in SteeringPath.Nodes) { Vector2 pos = (wp.Position - transducerCenter) * displayScale; if (pos.Length() > displayRadius) continue; pos.Y = -pos.Y; pos += center; GUI.DrawRectangle(spriteBatch, new Rectangle((int)pos.X - 3 / 2, (int)pos.Y - 3, 6, 6), (SteeringPath.CurrentNode == wp) ? Color.LightGreen : GUIStyle.Green, false); if (prevPos != Vector2.Zero) { GUI.DrawLine(spriteBatch, pos, prevPos, GUIStyle.Green); } prevPos = pos; } foreach (ObstacleDebugInfo obstacle in debugDrawObstacles) { Vector2 pos1 = (obstacle.Point1 - transducerCenter) * displayScale; pos1.Y = -pos1.Y; pos1 += center; Vector2 pos2 = (obstacle.Point2 - transducerCenter) * displayScale; pos2.Y = -pos2.Y; pos2 += center; GUI.DrawLine(spriteBatch, pos1, pos2, GUIStyle.Red * 0.6f, width: 3); if (obstacle.Intersection.HasValue) { Vector2 intersectionPos = (obstacle.Intersection.Value - transducerCenter) * displayScale; intersectionPos.Y = -intersectionPos.Y; intersectionPos += center; GUI.DrawRectangle(spriteBatch, intersectionPos - Vector2.One * 2, Vector2.One * 4, GUIStyle.Red); } Vector2 obstacleCenter = (pos1 + pos2) / 2; if (obstacle.AvoidStrength.LengthSquared() > 0.01f) { GUI.DrawLine(spriteBatch, obstacleCenter, obstacleCenter + new Vector2(obstacle.AvoidStrength.X, -obstacle.AvoidStrength.Y) * 100, Color.Lerp(GUIStyle.Green, GUIStyle.Orange, obstacle.Dot), width: 2); } } } public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam) { if (swapDestinationOrder == null) { swapDestinationOrder = item.Submarine != null && item.Submarine.FlippedX; if (swapDestinationOrder.Value) { levelStartTickBox.RectTransform.SetAsLastChild(); } } if (steerArea.Rect.Contains(PlayerInput.MousePosition)) { if (!PlayerInput.KeyDown(InputType.Deselect) && !PlayerInput.KeyHit(InputType.Deselect)) { Character.DisableControls = true; } } if (DisableControls) { dockingModeEnabled = false; } dockingContainer.Visible = DockingModeEnabled; statusContainer.Visible = !DockingModeEnabled; if (!DockingModeEnabled) { enterOutpostPrompt?.Close(); } if (DockingModeEnabled && ActiveDockingSource != null) { if (Math.Abs(ActiveDockingSource.Item.WorldPosition.X - DockingTarget.Item.WorldPosition.X) < ActiveDockingSource.DistanceTolerance.X && Math.Abs(ActiveDockingSource.Item.WorldPosition.Y - DockingTarget.Item.WorldPosition.Y) < ActiveDockingSource.DistanceTolerance.Y) { dockingButton.Text = dockText; if (dockingButton.FlashTimer <= 0.0f) { dockingButton.Flash(GUIStyle.Blue, 0.5f, useCircularFlash: true); dockingButton.Pulsate(Vector2.One, Vector2.One * 1.2f, dockingButton.FlashTimer); } } else { enterOutpostPrompt?.Close(); } } else if (connectedPorts.Any(d => d.Docked)) { dockingButton.Text = undockText; dockingContainer.Visible = true; statusContainer.Visible = false; if (dockingButton.FlashTimer <= 0.0f) { dockingButton.Flash(GUIStyle.Orange, useCircularFlash: true); dockingButton.Pulsate(Vector2.One, Vector2.One * 1.2f, dockingButton.FlashTimer); } } else { dockingButton.Text = dockText; } if (!HasPower) { tipContainer.Visible = true; tipContainer.Text = noPowerTip; return; } tipContainer.Visible = AutoPilot; if (AutoPilot) { if (maintainPos) { tipContainer.Text = autoPilotMaintainPosTip; } else if (LevelStartSelected) { tipContainer.Text = autoPilotLevelStartTip; } else if (LevelEndSelected) { tipContainer.Text = autoPilotLevelEndTip; } if (DockingModeEnabled && DockingTarget != null) { posToMaintain += ConvertUnits.ToDisplayUnits(DockingTarget.Item.Submarine.Velocity) * deltaTime; } } pressureWarningText.Visible = item.Submarine != null && Timing.TotalTime % 1.0f < 0.8f; if (Level.Loaded != null && pressureWarningText.Visible && item.Submarine.RealWorldDepth > Level.Loaded.RealWorldCrushDepth - PressureWarningThreshold && item.Submarine.RealWorldDepth > item.Submarine.RealWorldCrushDepth - PressureWarningThreshold) { pressureWarningText.Visible = true; pressureWarningText.Text = item.Submarine.AtDamageDepth ? TextManager.Get("SteeringDepthWarning") : TextManager.GetWithVariable("SteeringDepthWarningLow", "[crushdepth]", ((int)item.Submarine.RealWorldCrushDepth).ToString()); } else { pressureWarningText.Visible = false; } iceSpireWarningText.Visible = item.Submarine != null && !pressureWarningText.Visible && showIceSpireWarning && Timing.TotalTime % 1.0f < 0.8f; if (!disableControls && Vector2.DistanceSquared(PlayerInput.MousePosition, steerArea.Rect.Center.ToVector2()) < steerRadius * steerRadius) { if (PlayerInput.PrimaryMouseButtonHeld() && !CrewManager.IsCommandInterfaceOpen && !GameSession.IsTabMenuOpen && (!GameMain.GameSession?.Campaign?.ShowCampaignUI ?? true) && !GUIMessageBox.MessageBoxes.Any(msgBox => msgBox is GUIMessageBox { MessageBoxType: GUIMessageBox.Type.Default })) { Vector2 inputPos = PlayerInput.MousePosition - steerArea.Rect.Center.ToVector2(); inputPos.Y = -inputPos.Y; if (AutoPilot && !LevelStartSelected && !LevelEndSelected) { posToMaintain = controlledSub != null ? controlledSub.WorldPosition + inputPos / sonar.DisplayRadius * sonar.Range / sonar.Zoom : item.Submarine == null ? item.WorldPosition : item.Submarine.WorldPosition; } else { SteeringInput = inputPos; } unsentChanges = true; user = Character.Controlled; } } if (!AutoPilot && Character.DisableControls && GUI.KeyboardDispatcher.Subscriber == null) { steeringAdjustSpeed = character == null ? DefaultSteeringAdjustSpeed : MathHelper.Lerp(0.2f, 1.0f, character.GetSkillLevel(Tags.HelmSkill) / 100.0f); Vector2 input = Vector2.Zero; if (PlayerInput.KeyDown(InputType.Left)) { input -= Vector2.UnitX; } if (PlayerInput.KeyDown(InputType.Right)) { input += Vector2.UnitX; } if (PlayerInput.KeyDown(InputType.Up)) { input += Vector2.UnitY; } if (PlayerInput.KeyDown(InputType.Down)) { input -= Vector2.UnitY; } if (PlayerInput.KeyDown(InputType.Run)) { SteeringInput += input * deltaTime * 200; inputCumulation = 0; keyboardInput = Vector2.Zero; unsentChanges = true; } else { float step = deltaTime * 5; if (input.Length() > 0) { inputCumulation += step; } else { inputCumulation -= step; } float maxCumulation = 1; inputCumulation = MathHelper.Clamp(inputCumulation, 0, maxCumulation); float length = MathHelper.Lerp(0, 0.2f, MathUtils.InverseLerp(0, maxCumulation, inputCumulation)); var normalizedInput = Vector2.Normalize(input); if (MathUtils.IsValid(normalizedInput)) { keyboardInput += normalizedInput * length; } if (keyboardInput.LengthSquared() > 0.01f) { SteeringInput += keyboardInput; unsentChanges = true; user = Character.Controlled; keyboardInput *= MathHelper.Clamp(1 - step, 0, 1); } } } else { inputCumulation = 0; keyboardInput = Vector2.Zero; } if (!UseAutoDocking || DisableControls) { return; } if (checkConnectedPortsTimer <= 0.0f) { Connection dockingConnection = item.Connections?.FirstOrDefault(c => c.Name == "toggle_docking"); if (dockingConnection != null) { connectedPorts = item.GetConnectedComponentsRecursive(dockingConnection, ignoreInactiveRelays: true, allowTraversingBackwards: false); } checkConnectedPortsTimer = CheckConnectedPortsInterval; } else { checkConnectedPortsTimer -= deltaTime; } DockingModeEnabled = false; if (connectedPorts.None()) { return; } float closestDist = DockingAssistThreshold * DockingAssistThreshold; foreach (DockingPort sourcePort in connectedPorts) { if (sourcePort.Docked || sourcePort.Item.Submarine == null) { continue; } if (sourcePort.Item.Submarine != controlledSub) { continue; } int sourceDir = sourcePort.GetDir(); foreach (DockingPort targetPort in DockingPort.List) { if (targetPort.Docked || targetPort.Item.Submarine == null) { continue; } if (targetPort.Item.Submarine == controlledSub || targetPort.IsHorizontal != sourcePort.IsHorizontal) { continue; } if (targetPort.Item.Submarine.DockedTo?.Contains(sourcePort.Item.Submarine) ?? false) { continue; } if (targetPort.Item.Submarine.IsAboveLevel) { continue; } if (sourceDir == targetPort.GetDir()) { continue; } float dist = Vector2.DistanceSquared(sourcePort.Item.WorldPosition, targetPort.Item.WorldPosition); if (dist < closestDist) { closestDist = dist; DockingModeEnabled = true; ActiveDockingSource = sourcePort; DockingTarget = targetPort; } } } } /// /// Sets the value of the specified tickbox, or if it hasn't been instantiated (yet?), just the value of the backing field. /// private void TrySetTickBoxSelected(GUITickBox tickBox, ref bool backingValue, bool newValue) { if (tickBox == null) { backingValue = newValue; } else { tickBox.Selected = newValue; } } private bool NudgeButtonClicked(GUIButton btn, object userdata) { if (!MaintainPos || !AutoPilot) { AutoPilot = true; posToMaintain = item.Submarine.WorldPosition; } MaintainPos = true; if (userdata is Vector2 nudgeAmount) { if (item.GetComponent() is Sonar sonar) { nudgeAmount *= 500.0f / sonar.Zoom; } PosToMaintain += nudgeAmount; } return true; } protected override void RemoveComponentSpecific() { base.RemoveComponentSpecific(); maintainPosIndicator?.Remove(); maintainPosOriginIndicator?.Remove(); steeringIndicator?.Remove(); enterOutpostPrompt?.Close(); exitOutpostPrompt?.Close(); pathFinder = null; } public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) { msg.WriteBoolean(AutoPilot); msg.WriteBoolean(dockingNetworkMessagePending); dockingNetworkMessagePending = false; if (!AutoPilot) { //no need to write steering info if autopilot is controlling msg.WriteSingle(steeringInput.X); msg.WriteSingle(steeringInput.Y); } else { msg.WriteBoolean(posToMaintain != null); if (posToMaintain != null) { msg.WriteSingle(((Vector2)posToMaintain).X); msg.WriteSingle(((Vector2)posToMaintain).Y); } else { msg.WriteBoolean(LevelStartSelected); } } } public void ClientEventRead(IReadMessage msg, float sendingTime) { int msgStartPos = msg.BitPosition; bool autoPilot = msg.ReadBoolean(); bool dockingButtonClicked = msg.ReadBoolean(); ushort userID = msg.ReadUInt16(); Vector2 newSteeringInput = steeringInput; Vector2 newTargetVelocity = targetVelocity; float newSteeringAdjustSpeed = steeringAdjustSpeed; Vector2? newPosToMaintain = null; bool headingToStart = false; if (dockingButtonClicked) { item.SendSignal(new Signal("1", sender: Entity.FindEntityByID(userID) as Character), "toggle_docking"); } if (autoPilot) { if (msg.ReadBoolean()) { newPosToMaintain = new Vector2( msg.ReadSingle(), msg.ReadSingle()); } else { headingToStart = msg.ReadBoolean(); } } else { newSteeringInput = new Vector2(msg.ReadSingle(), msg.ReadSingle()); newTargetVelocity = new Vector2(msg.ReadSingle(), msg.ReadSingle()); newSteeringAdjustSpeed = msg.ReadSingle(); } if (correctionTimer > 0.0f) { int msgLength = (int)(msg.BitPosition - msgStartPos); msg.BitPosition = msgStartPos; StartDelayedCorrection(msg.ExtractBits(msgLength), sendingTime); return; } AutoPilot = autoPilot; if (!AutoPilot) { SteeringInput = newSteeringInput; TargetVelocity = newTargetVelocity; steeringAdjustSpeed = newSteeringAdjustSpeed; } else { MaintainPos = newPosToMaintain != null; posToMaintain = newPosToMaintain; if (posToMaintain == null) { LevelStartSelected = headingToStart; LevelEndSelected = !headingToStart; UpdatePath(); } else { LevelStartSelected = false; LevelEndSelected = false; } } } private void UpdateGUIElements() { steeringModeSwitch.Selected = AutoPilot; autopilotIndicator.Selected = AutoPilot; manualPilotIndicator.Selected = !AutoPilot; if (DisableControls) { steeringModeSwitch.Enabled = false; maintainPosTickBox.Enabled = false; levelEndTickBox.Enabled = false; levelStartTickBox.Enabled = false; } else { steeringModeSwitch.Enabled = true; maintainPosTickBox.Enabled = AutoPilot; levelEndTickBox.Enabled = AutoPilot; levelStartTickBox.Enabled = AutoPilot; } } } }