using Barotrauma.Extensions; using Barotrauma.Items.Components; using Barotrauma.Networking; using FarseerPhysics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; using Barotrauma.Steam; namespace Barotrauma { partial class CrewManager { /// /// How long the previously selected character waits doing nothing when switching to another character. Only affects idling. /// const float CharacterWaitOnSwitch = 10.0f; private readonly List characterInfos = new List(); private readonly List characters = new List(); private Point screenResolution; #region UI public GUIComponent ReportButtonFrame { get; set; } private GUIFrame guiFrame; private GUIFrame crewArea; private GUIListBox crewList; private GUIButton commandButton, toggleCrewButton; private float crewListOpenState; private bool toggleCrewListOpen = true; private Point crewListEntrySize; private GUIFrame contextMenu; private GUIListBox subContextMenu; /// /// Present only in single player games. In multiplayer. The chatbox is found from GameSession.Client. /// public ChatBox ChatBox { get; private set; } private float prevUIScale; public bool AllowCharacterSwitch = true; public bool ToggleCrewListOpen { get { return toggleCrewListOpen; } set { if (toggleCrewListOpen == value) { return; } toggleCrewListOpen = GameMain.Config.CrewMenuOpen = value; } } const float CommandNodeAnimDuration = 0.2f; public List OrderOptionButtons = new List(); private Sprite jobIndicatorBackground, previousOrderArrow, cancelIcon; #endregion #region Constructors public CrewManager(XElement element, bool isSinglePlayer) : this(isSinglePlayer) { foreach (XElement subElement in element.Elements()) { if (!subElement.Name.ToString().Equals("character", StringComparison.OrdinalIgnoreCase)) { continue; } var characterInfo = new CharacterInfo(subElement); characterInfos.Add(characterInfo); foreach (XElement invElement in subElement.Elements()) { if (!invElement.Name.ToString().Equals("inventory", StringComparison.OrdinalIgnoreCase)) { continue; } characterInfo.InventoryData = invElement; break; } } } partial void InitProjectSpecific() { guiFrame = new GUIFrame(new RectTransform(Vector2.One, GUICanvas.Instance), null, Color.Transparent) { CanBeFocused = false }; #region Crew Area var crewAreaWithButtons = new GUIFrame( HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.CrewArea, guiFrame.RectTransform), style: null, color: Color.Transparent) { CanBeFocused = false }; var commandButtonHeight = (int)(GUI.Scale * 40); var buttonSize = new Point((int)(182f / 99f * commandButtonHeight), commandButtonHeight); var crewListToggleButtonHeight = (int)(64f * buttonSize.X / 175f); crewArea = new GUIFrame( new RectTransform( new Point(crewAreaWithButtons.Rect.Width, crewAreaWithButtons.Rect.Height - commandButtonHeight - crewListToggleButtonHeight - 2 * HUDLayoutSettings.Padding), crewAreaWithButtons.RectTransform, Anchor.BottomLeft), style: null, color: Color.Transparent) { CanBeFocused = false }; commandButton = new GUIButton( new RectTransform(buttonSize, parent: crewAreaWithButtons.RectTransform), style: "CommandButton") { // TODO: Update keybind if it's changed ToolTip = TextManager.Get("inputtype.command") + " (" + GameMain.Config.KeyBindText(InputType.Command) + ")", OnClicked = (button, userData) => { ToggleCommandUI(); return true; } }; // AbsoluteOffset is set in UpdateProjectSpecific based on crewListOpenState crewList = new GUIListBox( new RectTransform( Vector2.One, crewArea.RectTransform), style: null, isScrollBarOnDefaultSide: false) { AutoHideScrollBar = false, OnSelected = (component, userData) => false, SelectMultiple = false, Spacing = (int)(GUI.Scale * 10) }; buttonSize.Y = crewListToggleButtonHeight; toggleCrewButton = new GUIButton( new RectTransform(buttonSize, parent: crewAreaWithButtons.RectTransform) { AbsoluteOffset = new Point(0, commandButtonHeight + HUDLayoutSettings.Padding) }, style: "CrewListToggleButton") { OnClicked = (GUIButton btn, object userdata) => { ToggleCrewListOpen = !ToggleCrewListOpen; return true; } }; jobIndicatorBackground = new Sprite("Content/UI/CommandUIAtlas.png", new Rectangle(0, 512, 128, 128)); previousOrderArrow = new Sprite("Content/UI/CommandUIAtlas.png", new Rectangle(128, 512, 128, 128)); cancelIcon = new Sprite("Content/UI/CommandUIAtlas.png", new Rectangle(512, 384, 128, 128)); // Calculate and store crew list entry size so it doesn't have to be calculated for every entry crewListEntrySize = new Point(crewList.Content.Rect.Width - HUDLayoutSettings.Padding, 0); int crewListEntryMinHeight = 32; crewListEntrySize.Y = Math.Max(crewListEntryMinHeight, (int)(crewListEntrySize.X / 8f)); float charactersPerView = crewList.Content.Rect.Height / (float)(crewListEntrySize.Y + crewList.Spacing); int adjustedHeight = (int)Math.Ceiling(crewList.Content.Rect.Height / Math.Round(charactersPerView)) - crewList.Spacing; if (adjustedHeight < crewListEntryMinHeight) { adjustedHeight = (int)Math.Ceiling(crewList.Content.Rect.Height / Math.Floor(charactersPerView)) - crewList.Spacing; } crewListEntrySize.Y = adjustedHeight; #endregion #region Chatbox if (IsSinglePlayer) { ChatBox = new ChatBox(guiFrame, isSinglePlayer: true) { OnEnterMessage = (textbox, text) => { if (Character.Controlled?.Info == null) { textbox.Deselect(); textbox.Text = ""; return true; } textbox.TextColor = ChatMessage.MessageColor[(int)ChatMessageType.Default]; if (!string.IsNullOrWhiteSpace(text)) { string msgCommand = ChatMessage.GetChatMessageCommand(text, out string msg); // add to local history ChatBox.ChatManager.Store(text); AddSinglePlayerChatMessage( Character.Controlled.Info.Name, msg, ((msgCommand == "r" || msgCommand == "radio") && ChatMessage.CanUseRadio(Character.Controlled)) ? ChatMessageType.Radio : ChatMessageType.Default, Character.Controlled); var headset = GetHeadset(Character.Controlled, true); if (headset != null && headset.CanTransmit()) { headset.TransmitSignal(stepsTaken: 0, signal: msg, source: headset.Item, sender: Character.Controlled, sendToChat: false); } } textbox.Deselect(); textbox.Text = ""; if (ChatBox.CloseAfterMessageSent) { ChatBox.ToggleOpen = false; ChatBox.CloseAfterMessageSent = false; } return true; } }; ChatBox.InputBox.OnTextChanged += ChatBox.TypingChatMessage; } #endregion #region Reports var chatBox = ChatBox ?? GameMain.Client?.ChatBox; if (chatBox != null) { chatBox.ToggleButton = new GUIButton(new RectTransform(new Point((int)(182f * GUI.Scale * 0.4f), (int)(99f * GUI.Scale * 0.4f)), chatBox.GUIFrame.Parent.RectTransform), style: "ChatToggleButton"); chatBox.ToggleButton.RectTransform.AbsoluteOffset = new Point(0, HUDLayoutSettings.ChatBoxArea.Height - chatBox.ToggleButton.Rect.Height); chatBox.ToggleButton.OnClicked += (GUIButton btn, object userdata) => { chatBox.ToggleOpen = !chatBox.ToggleOpen; chatBox.CloseAfterMessageSent = false; return true; }; } var reports = Order.PrefabList.FindAll(o => o.TargetAllCharacters && o.SymbolSprite != null); if (reports.None()) { DebugConsole.ThrowError("No valid orders for report buttons found! Cannot create report buttons. The orders for the report buttons must have 'targetallcharacters' attribute enabled and a valid 'symbolsprite' defined."); return; } ReportButtonFrame = new GUILayoutGroup(new RectTransform( new Point((HUDLayoutSettings.ChatBoxArea.Height - chatBox.ToggleButton.Rect.Height - (int)((reports.Count - 1) * 5 * GUI.Scale)) / reports.Count, HUDLayoutSettings.ChatBoxArea.Height - chatBox.ToggleButton.Rect.Height), guiFrame.RectTransform)) { AbsoluteSpacing = (int)(5 * GUI.Scale), UserData = "reportbuttons", CanBeFocused = false }; ReportButtonFrame.RectTransform.AbsoluteOffset = new Point(0, -chatBox.ToggleButton.Rect.Height); //report buttons foreach (Order order in reports) { if (!order.TargetAllCharacters || order.SymbolSprite == null) { continue; } var btn = new GUIButton(new RectTransform(new Point(ReportButtonFrame.Rect.Width), ReportButtonFrame.RectTransform), style: null) { OnClicked = (GUIButton button, object userData) => { if (!CanIssueOrders) { return false; } var sub = Character.Controlled.Submarine; if (sub == null || sub.TeamID != Character.Controlled.TeamID || sub.Info.IsWreck) { return false; } SetCharacterOrder(null, order, null, Character.Controlled); var visibleHulls = new List(Character.Controlled.GetVisibleHulls()); foreach (var hull in visibleHulls) { HumanAIController.PropagateHullSafety(Character.Controlled, hull); HumanAIController.RefreshTargets(Character.Controlled, order, hull); } return true; }, UserData = order, ToolTip = order.Name }; new GUIFrame(new RectTransform(new Vector2(1.5f), btn.RectTransform, Anchor.Center), "OuterGlowCircular") { Color = GUI.Style.Red * 0.8f, HoverColor = GUI.Style.Red * 1.0f, PressedColor = GUI.Style.Red * 0.6f, UserData = "highlighted", CanBeFocused = false, Visible = false }; var img = new GUIImage(new RectTransform(Vector2.One, btn.RectTransform), order.Prefab.SymbolSprite, scaleToFit: true) { Color = order.Color, HoverColor = Color.Lerp(order.Color, Color.White, 0.5f), ToolTip = order.Name, SpriteEffects = SpriteEffects.FlipHorizontally }; } #endregion screenResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); prevUIScale = GUI.Scale; ToggleCrewListOpen = GameMain.Config.CrewMenuOpen; dismissedOrderPrefab ??= Order.GetPrefab("dismissed"); } #endregion #region Character list management public Rectangle GetActiveCrewArea() { return crewArea.Rect; } public IEnumerable GetCharacters() { return characters; } public IEnumerable GetCharacterInfos() { return characterInfos; } public void AddCharacter(Character character) { if (character.Removed) { DebugConsole.ThrowError("Tried to add a removed character to CrewManager!\n" + Environment.StackTrace); return; } if (character.IsDead) { DebugConsole.ThrowError("Tried to add a dead character to CrewManager!\n" + Environment.StackTrace); return; } if (!characters.Contains(character)) { characters.Add(character); } if (!characterInfos.Contains(character.Info)) { characterInfos.Add(character.Info); } AddCharacterToCrewList(character); DisplayCharacterOrder(character, character.CurrentOrder, character.CurrentOrderOption); } public void AddCharacterInfo(CharacterInfo characterInfo) { if (characterInfos.Contains(characterInfo)) { DebugConsole.ThrowError("Tried to add the same character info to CrewManager twice.\n" + Environment.StackTrace); return; } characterInfos.Add(characterInfo); } /// /// Remove the character from the crew (and crew menus). /// /// The character to remove /// If the character info is also removed, the character will not be visible in the round summary. public void RemoveCharacter(Character character, bool removeInfo = false) { if (character == null) { DebugConsole.ThrowError("Tried to remove a null character from CrewManager.\n" + Environment.StackTrace); return; } characters.Remove(character); if (removeInfo) { characterInfos.Remove(character.Info); } } /// /// Remove info of a selected character. The character will not be visible in any menus or the round summary. /// /// public void RemoveCharacterInfo(CharacterInfo characterInfo) { characterInfos.Remove(characterInfo); } private void AddCharacterToCrewList(Character character) { if (character == null) { return; } var background = new GUIFrame( new RectTransform(crewListEntrySize, parent: crewList.Content.RectTransform, anchor: Anchor.TopRight), style: "CrewListBackground") { UserData = character, OnSecondaryClicked = (comp, data) => { if (data == null) { return false; } var client = GameMain.NetworkMember?.ConnectedClients?.Find(c => c.Character == data); if (client != null) { CreateModerationContextMenu(PlayerInput.MousePosition.ToPoint(), client); return true; } return false; } }; var iconRelativeWidth = (float)crewListEntrySize.Y / background.Rect.Width; var layoutGroup = new GUILayoutGroup( new RectTransform(Vector2.One, parent: background.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { CanBeFocused = false, RelativeSpacing = 0.1f * iconRelativeWidth, UserData = character }; // "Padding" to prevent member-specific command button from overlapping job indicator var commandButtonAbsoluteHeight = Math.Min(40.0f, 0.67f * background.Rect.Height); var paddingRelativeWidth = 0.35f * commandButtonAbsoluteHeight / background.Rect.Width; new GUIFrame(new RectTransform(new Vector2(paddingRelativeWidth, 1.0f), layoutGroup.RectTransform), style: null); var jobIconBackground = new GUIImage( new RectTransform(new Vector2(0.8f * iconRelativeWidth, 0.8f), layoutGroup.RectTransform), jobIndicatorBackground, scaleToFit: true) { CanBeFocused = false, UserData = "job" }; if (character?.Info?.Job.Prefab?.Icon != null) { new GUIImage( new RectTransform(Vector2.One, jobIconBackground.RectTransform), character.Info.Job.Prefab.Icon, scaleToFit: true) { CanBeFocused = false, Color = character.Info.Job.Prefab.UIColor, HoverColor = character.Info.Job.Prefab.UIColor, PressedColor = character.Info.Job.Prefab.UIColor, SelectedColor = character.Info.Job.Prefab.UIColor }; } var nameRelativeWidth = 1.0f - paddingRelativeWidth - 3.7f * iconRelativeWidth; var font = layoutGroup.Rect.Width < 150 ? GUI.SmallFont : GUI.Font; var nameBlock = new GUITextBlock( new RectTransform( new Vector2(nameRelativeWidth, 1.0f), layoutGroup.RectTransform) { MaxSize = new Point(150, background.Rect.Height) }, ToolBox.LimitString(character.Name, font, (int)(nameRelativeWidth * layoutGroup.Rect.Width)), font: font, textColor: character.Info?.Job?.Prefab?.UIColor) { CanBeFocused = false }; var nameActualRealtiveWidth = Math.Min(nameRelativeWidth * background.Rect.Width, 150) / background.Rect.Width; var characterButton = new GUIButton( new RectTransform( new Vector2(paddingRelativeWidth + 0.8f * iconRelativeWidth + nameActualRealtiveWidth + 2 * layoutGroup.RelativeSpacing, 1.0f), background.RectTransform), style: null) { UserData = character }; // Only create a tooltip if the name doesn't fit the name block if (nameBlock.Text.EndsWith("...")) { var characterTooltip = character.Name; if (character.Info?.Job?.Name != null) { characterTooltip += " (" + character.Info.Job.Name + ")"; }; characterButton.ToolTip = characterTooltip; if (character.Info?.Job?.Prefab != null) { characterButton.TooltipRichTextData = new List() { new RichTextData() { Color = character.Info.Job.Prefab.UIColor, EndIndex = characterTooltip.Length - 1 }}; } } if (IsSinglePlayer) { characterButton.OnClicked = CharacterClicked; } else { characterButton.CanBeFocused = false; characterButton.CanBeSelected = false; } new GUIImage( new RectTransform(new Vector2(0.5f * iconRelativeWidth, 0.5f), layoutGroup.RectTransform), style: "VerticalLine") { CanBeFocused = false }; var soundIcons = new GUIFrame(new RectTransform(new Vector2(0.8f * iconRelativeWidth, 0.8f), layoutGroup.RectTransform), style: null) { CanBeFocused = false, UserData = "soundicons" }; new GUIImage( new RectTransform(Vector2.One, soundIcons.RectTransform), GUI.Style.GetComponentStyle("GUISoundIcon").Sprites[GUIComponent.ComponentState.None].FirstOrDefault().Sprite, scaleToFit: true) { CanBeFocused = false, UserData = new Pair("soundicon", 0.0f), Visible = true }; new GUIImage( new RectTransform(Vector2.One, soundIcons.RectTransform), "GUISoundIconDisabled", scaleToFit: true) { CanBeFocused = true, UserData = "soundicondisabled", Visible = false }; new GUIButton(new RectTransform(new Point((int)commandButtonAbsoluteHeight), background.RectTransform), style: "CrewListCommandButton") { ToolTip = TextManager.Get("inputtype.command"), OnClicked = (component, userData) => { if (!CanIssueOrders) { return false; } CreateCommandUI(character); return true; } }; } /// /// Sets which character is selected in the crew UI (highlight effect etc) /// public bool CharacterClicked(GUIComponent component, object selection) { if (!AllowCharacterSwitch) { return false; } Character character = selection as Character; if (character == null || character.IsDead || character.IsUnconscious) { return false; } SelectCharacter(character); if (GUI.KeyboardDispatcher.Subscriber == crewList) { GUI.KeyboardDispatcher.Subscriber = null; } return true; } public void ReviveCharacter(Character revivedCharacter) { if (crewList.Content.GetChildByUserData(revivedCharacter) is GUIComponent characterComponent) { crewList.Content.RemoveChild(characterComponent); } if (characterInfos.Contains(revivedCharacter.Info)) { AddCharacter(revivedCharacter); } } public void KillCharacter(Character killedCharacter) { if (crewList.Content.GetChildByUserData(killedCharacter) is GUIComponent characterComponent) { CoroutineManager.StartCoroutine(KillCharacterAnim(characterComponent)); } RemoveCharacter(killedCharacter); } private IEnumerable KillCharacterAnim(GUIComponent component) { List components = component.GetAllChildren().ToList(); components.Add(component); components.RemoveAll(c => c.UserData is Pair pair && pair.First == "soundicon" || c.UserData as string == "soundicondisabled"); components.ForEach(c => c.Color = Color.DarkRed); yield return new WaitForSeconds(1.0f); float timer = 0.0f; float hideDuration = 1.0f; while (timer < hideDuration) { foreach (GUIComponent comp in components) { comp.Color = Color.Lerp(Color.DarkRed, Color.Transparent, timer / hideDuration); comp.RectTransform.LocalScale = new Vector2(comp.RectTransform.LocalScale.X, 1.0f - (timer / hideDuration)); } timer += CoroutineManager.DeltaTime; yield return CoroutineStatus.Running; } crewList.Content.RemoveChild(component); // GUITextBlock.AutoScaleAndNormalize(list.Content.GetAllChildren(), defaultScale: 1.0f); crewList.UpdateScrollBarSize(); yield return CoroutineStatus.Success; } #endregion #region Dialog /// /// Adds the message to the single player chatbox. /// public void AddSinglePlayerChatMessage(string senderName, string text, ChatMessageType messageType, Character sender) { if (!IsSinglePlayer) { DebugConsole.ThrowError("Cannot add messages to single player chat box in multiplayer mode!\n" + Environment.StackTrace); return; } if (string.IsNullOrEmpty(text)) { return; } if (sender != null) { GameMain.GameSession.CrewManager.SetCharacterSpeaking(sender); } ChatBox.AddMessage(ChatMessage.Create(senderName, text, messageType, sender)); } private WifiComponent GetHeadset(Character character, bool requireEquipped) { if (character?.Inventory == null) return null; var radioItem = character.Inventory.Items.FirstOrDefault(it => it != null && it.GetComponent() != null); if (radioItem == null) return null; if (requireEquipped && !character.HasEquippedItem(radioItem)) return null; return radioItem.GetComponent(); } partial void CreateRandomConversation() { if (GameMain.Client != null) { //let the server create random conversations in MP return; } List availableSpeakers = Character.CharacterList.FindAll(c => c.AIController is HumanAIController && !c.IsDead && c.SpeechImpediment <= 100.0f); pendingConversationLines.AddRange(NPCConversation.CreateRandom(availableSpeakers)); } #endregion #region Voice chat public void SetPlayerVoiceIconState(Client client, bool muted, bool mutedLocally) { if (client?.Character == null) { return; } if (crewList.Content.GetChildByUserData(client.Character)? .FindChild(c => c is GUILayoutGroup)? .GetChildByUserData("soundicons") is GUIComponent soundIcons) { var soundIcon = soundIcons.FindChild(c => c.UserData is Pair pair && pair.First == "soundicon"); var soundIconDisabled = soundIcons.FindChild("soundicondisabled"); soundIcon.Visible = !muted && !mutedLocally; soundIconDisabled.Visible = muted || mutedLocally; soundIconDisabled.ToolTip = TextManager.Get(mutedLocally ? "MutedLocally" : "MutedGlobally"); } } public void SetClientSpeaking(Client client) { if (client?.Character != null) { SetCharacterSpeaking(client.Character); } } public void SetCharacterSpeaking(Character character) { if (crewList.Content.GetChildByUserData(character)? .FindChild(c => c is GUILayoutGroup)? .GetChildByUserData("soundicons")? .FindChild(c => c.UserData is Pair pair && pair.First == "soundicon") is GUIComponent soundIcon) { soundIcon.Color = Color.White; Pair userdata = soundIcon.UserData as Pair; userdata.Second = 1.0f; } } #endregion #region Crew List Order Displayment /// /// Sets the character's current order (if it's close enough to receive messages from orderGiver) and /// displays the order in the crew UI /// public void SetCharacterOrder(Character character, Order order, string option, Character orderGiver) { if (order != null && order.TargetAllCharacters) { if (orderGiver == null || orderGiver.CurrentHull == null) { return; } var hull = orderGiver.CurrentHull; AddOrder(new Order(order.Prefab ?? order, hull, null, orderGiver), order.FadeOutTime); if (IsSinglePlayer) { orderGiver.Speak( order.GetChatMessage("", hull.DisplayName, givingOrderToSelf: character == orderGiver), ChatMessageType.Order); } else { OrderChatMessage msg = new OrderChatMessage(order, "", hull, null, orderGiver); GameMain.Client?.SendChatMessage(msg); } } else { //can't issue an order if no characters are available if (character == null) { return; } if (IsSinglePlayer) { character.SetOrder(order, option, orderGiver, speak: orderGiver != character); orderGiver?.Speak( order.GetChatMessage(character.Name, orderGiver.CurrentHull?.DisplayName, givingOrderToSelf: character == orderGiver, orderOption: option), null); } else if (orderGiver != null) { OrderChatMessage msg = new OrderChatMessage(order, option, order?.TargetEntity ?? order?.TargetItemComponent?.Item, character, orderGiver); GameMain.Client?.SendChatMessage(msg); } } } /// /// Displays the specified order in the crew UI next to the character. /// public void DisplayCharacterOrder(Character character, Order order, string option) { if (character == null) { return; } var characterFrame = crewList.Content.GetChildByUserData(character); if (characterFrame == null) { return; } GUILayoutGroup layoutGroup = (GUILayoutGroup)characterFrame.FindChild(c => c is GUILayoutGroup); var currentOrderComponent = GetCurrentOrderComponent(layoutGroup); if (order != null) { var prevOrderComponent = GetPreviousOrderComponent(layoutGroup); if (currentOrderComponent?.UserData is OrderInfo currentOrderInfo) { if (order.Identifier == currentOrderInfo.Order.Identifier && option == currentOrderInfo.OrderOption && order.TargetEntity == currentOrderInfo.Order.TargetEntity) { return; } layoutGroup.RemoveChild(prevOrderComponent); DisplayPreviousCharacterOrder(character, layoutGroup, currentOrderInfo); } else if (order.Identifier != dismissedOrderPrefab.Identifier && prevOrderComponent?.UserData is OrderInfo prevOrderInfo && order.Identifier == prevOrderInfo.Order.Identifier && option == prevOrderInfo.OrderOption && order.TargetEntity == prevOrderInfo.Order.TargetEntity) { layoutGroup.RemoveChild(prevOrderComponent); } } layoutGroup.RemoveChild(currentOrderComponent); if (order == null || order.Identifier == dismissedOrderPrefab.Identifier) { return; } var orderFrame = new GUIButton( new RectTransform( layoutGroup.GetChildByUserData("job").RectTransform.RelativeSize, layoutGroup.RectTransform), style: null) { UserData = new OrderInfo(order, option), OnClicked = (button, userData) => { if (!CanIssueOrders) { return false; } SetCharacterOrder(character, dismissedOrderPrefab, null, Character.Controlled); return true; } }; CreateNodeIcon(orderFrame.RectTransform, order.SymbolSprite, order.Color, tooltip: order.Name); new GUIImage( new RectTransform(Vector2.One, orderFrame.RectTransform), cancelIcon, scaleToFit: true) { CanBeFocused = false, UserData = "cancel", Visible = false }; orderFrame.RectTransform.RepositionChildInHierarchy(4); characterFrame.SetAsFirstChild(); } private void DisplayPreviousCharacterOrder(Character character, GUILayoutGroup characterComponent, OrderInfo orderInfo) { if (orderInfo.Order == null || orderInfo.Order.Identifier == dismissedOrderPrefab.Identifier) { return; } var previousOrderInfo = new OrderInfo(orderInfo); var prevOrderFrame = new GUIButton( new RectTransform( characterComponent.GetChildByUserData("job").RectTransform.RelativeSize, characterComponent.RectTransform), style: null) { UserData = previousOrderInfo, OnClicked = (button, userData) => { if (!CanIssueOrders) { return false; } var orderInfo = (OrderInfo)userData; SetCharacterOrder(character, orderInfo.Order, orderInfo.OrderOption, Character.Controlled); return true; } }; var prevOrderIconFrame = new GUIFrame( new RectTransform(new Vector2(0.8f), prevOrderFrame.RectTransform, anchor: Anchor.BottomLeft), style: null); CreateNodeIcon( prevOrderIconFrame.RectTransform, previousOrderInfo.Order.SymbolSprite, previousOrderInfo.Order.Color, tooltip: previousOrderInfo.Order.Name); foreach (GUIComponent c in prevOrderIconFrame.Children) { c.HoverColor = c.Color; c.PressedColor = c.Color; c.SelectedColor = c.Color; } new GUIImage( new RectTransform(new Vector2(0.8f), prevOrderFrame.RectTransform, anchor: Anchor.TopRight), previousOrderArrow, scaleToFit: true) { CanBeFocused = false }; prevOrderFrame.RectTransform.RepositionChildInHierarchy(GetCurrentOrderComponent(characterComponent) != null ? 5 : 4); } private GUIComponent GetCurrentOrderComponent(GUILayoutGroup characterComponent) { return characterComponent?.FindChild(c => c?.UserData is OrderInfo orderInfo && orderInfo.ComponentIdentifier == "currentorder"); } private GUIComponent GetPreviousOrderComponent(GUILayoutGroup characterComponent) { return characterComponent?.FindChild(c => c?.UserData is OrderInfo orderInfo && orderInfo.ComponentIdentifier == "previousorder"); } private struct OrderInfo { public string ComponentIdentifier { get; set; } public Order Order { get; private set; } public string OrderOption { get; private set; } public OrderInfo(Order order, string orderOption) { ComponentIdentifier = "currentorder"; Order = order; OrderOption = orderOption; } public OrderInfo(OrderInfo orderInfo) { ComponentIdentifier = "previousorder"; Order = orderInfo.Order; OrderOption = orderInfo.OrderOption; } } #endregion #region Updating and drawing the UI private void DrawMiniMapOverlay(SpriteBatch spriteBatch, GUICustomComponent container) { Submarine sub = container.UserData as Submarine; if (sub?.HullVertices == null) { return; } var dockedBorders = sub.GetDockedBorders(); dockedBorders.Location += sub.WorldPosition.ToPoint(); float scale = Math.Min( container.Rect.Width / (float)dockedBorders.Width, container.Rect.Height / (float)dockedBorders.Height) * 0.9f; float displayScale = ConvertUnits.ToDisplayUnits(scale); Vector2 offset = (sub.WorldPosition - new Vector2(dockedBorders.Center.X, dockedBorders.Y - dockedBorders.Height / 2)) * scale; Vector2 center = container.Rect.Center.ToVector2(); for (int i = 0; i < sub.HullVertices.Count; i++) { Vector2 start = (sub.HullVertices[i] * displayScale + offset); start.Y = -start.Y; Vector2 end = (sub.HullVertices[(i + 1) % sub.HullVertices.Count] * displayScale + offset); end.Y = -end.Y; GUI.DrawLine(spriteBatch, center + start, center + end, Color.DarkCyan * Rand.Range(0.3f, 0.35f), width: 10); } } #region Context Menu public void CreateModerationContextMenu(Point mousePos, Client client) { if (IsSinglePlayer || client == null || (GameMain.NetworkMember?.ConnectedClients?.All(match => match != client) ?? true)) { return; } contextMenu = new GUIFrame(new RectTransform(new Vector2(0.1f, 0.12f), GUI.Canvas) { ScreenSpaceOffset = mousePos }, style: "GUIToolTip") { UserData = client }; var nameLabel = new GUITextBlock(new RectTransform(new Vector2(1f, 0.2f), contextMenu.RectTransform), client.Name, font: GUI.SubHeadingFont) { Padding = new Vector4(8), TextColor = client.Character?.Info?.Job.Prefab.UIColor ?? Color.White }; var optionsList = new GUIListBox(new RectTransform(new Vector2(1f, 0.8f), contextMenu.RectTransform, Anchor.BottomLeft), style: null) { Padding = new Vector4(4, 0, 4, 4) }; bool hasSteam = client.SteamID > 0 && SteamManager.IsInitialized, canKick = GameMain.Client.HasPermission(ClientPermissions.Kick), canBan = GameMain.Client.HasPermission(ClientPermissions.Ban) && client.AllowKicking, canPromo = GameMain.Client.HasPermission(ClientPermissions.ManagePermissions); // Disable options if we are targeting ourselves if (client.ID == GameMain.Client?.ID) { canKick = canBan = canPromo = false; } RectTransform parent = optionsList.Content.RectTransform; new GUITextBlock(new RectTransform(Point.Zero, parent), TextManager.Get("viewsteamprofile"), font: GUI.SmallFont) { Padding = new Vector4(4), Enabled = hasSteam, UserData = "steam" }; new GUITextBlock(new RectTransform(Point.Zero, parent), TextManager.Get("permissions"), font: GUI.SmallFont) { Padding = new Vector4(4), Enabled = canPromo, UserData = "promote" }; new GUITextBlock(new RectTransform(Point.Zero, parent), TextManager.Get(client.MutedLocally ? "unmute" : "mute"), font: GUI.SmallFont) { Padding = new Vector4(4), Enabled = client.ID != GameMain.Client?.ID, UserData = "mute" }; new GUITextBlock(new RectTransform(Point.Zero, parent), TextManager.Get(canKick ? "kick" : "votetokick"), font: GUI.SmallFont) { Padding = new Vector4(4), Enabled = client.ID != GameMain.Client?.ID && client.AllowKicking, UserData = canKick ? "kick" : "votekick" }; new GUITextBlock(new RectTransform(Point.Zero, parent), TextManager.Get("ban"), font: GUI.SmallFont) { Padding = new Vector4(4), Enabled = canBan, UserData = "ban" }; foreach (GUIComponent c in optionsList.Content.Children) { if (c is GUITextBlock child && !child.Enabled) { child.TextColor *= 0.5f; } } var children = optionsList.Content.Children.ToList(); // Resize all children to the size of their text foreach (GUITextBlock block in children.Where(c => c is GUITextBlock).Cast()) { block.RectTransform.NonScaledSize = new Point((int) (block.TextSize.X + (block.Padding.X + block.Padding.Z)), (int)(18 * GUI.Scale)); } int horizontalPadding = (int)(optionsList.Padding.X + optionsList.Padding.Z); int verticalPadding = (int)(optionsList.Padding.Y + optionsList.Padding.W); int largestWidth = children.Max(c => c.Rect.Width + horizontalPadding); // If the name is bigger than any of the options then overwrite nameLabel.RectTransform.MinSize = new Point((int)(nameLabel.TextSize.X + (nameLabel.Padding.X + nameLabel.Padding.Z)), nameLabel.RectTransform.NonScaledSize.Y); if (largestWidth < nameLabel.RectTransform.MinSize.X) { largestWidth = nameLabel.RectTransform.MinSize.X; } // Resize all children to the size of the longest element foreach (GUIComponent c in children) { c.RectTransform.MinSize = new Point(largestWidth, c.Rect.Height); } // crop the context menu contextMenu.RectTransform.NonScaledSize = new Point(largestWidth, (children.Sum(c => c.Rect.Height) + verticalPadding) + nameLabel.Rect.Height); // if the menu would go off the screen then move it up if (contextMenu.Rect.Bottom > GameMain.GraphicsHeight) { contextMenu.RectTransform.ScreenSpaceOffset = new Point(mousePos.X, mousePos.Y - contextMenu.Rect.Height); } optionsList.OnSelected = (component, obj) => { if (component.Enabled) { switch (obj) { case "steam": Steamworks.SteamFriends.OpenWebOverlay($"https://steamcommunity.com/profiles/{client.SteamID}"); break; case "mute": client.MutedLocally = !client.MutedLocally; break; case "kick": GameMain.Client?.CreateKickReasonPrompt(client.Name, false); break; case "votekick": GameMain.Client?.VoteForKick(client); break; case "ban": GameMain.Client?.CreateKickReasonPrompt(client.Name, true); break; } contextMenu = null; return true; } return false; }; } private void CreatePromoteSubMenu(Point pos, Client client) { if (client == null ) { return; } subContextMenu = new GUIListBox(new RectTransform(new Vector2(0.1f, 0.1f), GUI.Canvas) { ScreenSpaceOffset = pos }, style: "GUIToolTip"); foreach (var rank in PermissionPreset.List) { new GUITextBlock(new RectTransform(Point.Zero, subContextMenu.Content.RectTransform), rank.Name, font: GUI.SmallFont) { ToolTip = rank.Description, UserData = rank, Padding = new Vector4(4) }; } var children = subContextMenu.Content.Children.ToList(); // Resize all children to the size of their text foreach (GUITextBlock block in children.Where(c => c is GUITextBlock).Cast()) { block.RectTransform.NonScaledSize = new Point((int) (block.TextSize.X + (block.Padding.X + block.Padding.Z)), (int)(18 * GUI.Scale)); } int horizontalPadding = (int)(subContextMenu.Padding.X + subContextMenu.Padding.Z); int largestWidth = children.Max(c => c.Rect.Width + horizontalPadding); // Resize all children to the size of the longest element foreach (GUIComponent c in children) { c.RectTransform.MinSize = new Point(largestWidth, c.Rect.Height); } // crop the context menu subContextMenu.RectTransform.NonScaledSize = new Point(largestWidth, children.Sum(c => c.Rect.Height) + horizontalPadding); // if the menu would go off the screen then move it up if (subContextMenu.Rect.Bottom > GameMain.GraphicsHeight) { subContextMenu.RectTransform.ScreenSpaceOffset = new Point(pos.X, pos.Y - subContextMenu.Rect.Height); } subContextMenu.OnSelected = (component, obj) => { if (component.Enabled && obj is PermissionPreset preset) { var label = TextManager.GetWithVariables(preset.Permissions == ClientPermissions.None ? "clearrankprompt" : "giverankprompt", new []{ "[user]", "[rank]" }, new []{ client.Name, preset.Name }); var msgBox = new GUIMessageBox(string.Empty, label, new[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }); msgBox.Buttons[0].OnClicked = (yesBtn, userdata) => { client.SetPermissions(preset.Permissions, preset.PermittedCommands); GameMain.Client.UpdateClientPermissions(client); msgBox.Close(); return true; }; msgBox.Buttons[1].OnClicked = (_, userdata) => { msgBox.Close(); return true; }; contextMenu = null; subContextMenu = null; return true; } return false; }; } private static bool IsMouseOnContextMenu(Rectangle rect) { Rectangle expandedRect = rect; expandedRect.Inflate(20, 20); return expandedRect.Contains(PlayerInput.MousePosition); } #endregion public void AddToGUIUpdateList() { if (GUI.DisableHUD) { return; } commandFrame?.AddToGUIUpdateList(); if (GUI.DisableUpperHUD) { return; } if (GameMain.GraphicsWidth != screenResolution.X || GameMain.GraphicsHeight != screenResolution.Y || prevUIScale != GUI.Scale) { var previousCrewList = crewList; InitProjectSpecific(); foreach (GUIComponent c in previousCrewList.Content.Children) { if (!(c.UserData is Character character) || character.IsDead || character.Removed) { continue; } AddCharacter(character); if (GetPreviousOrderComponent(c.GetChild())?.UserData is OrderInfo prevInfo && crewList.Content.Children.FirstOrDefault(c => c?.UserData == character)?.GetChild() is GUILayoutGroup newLayoutGroup) { DisplayPreviousCharacterOrder(character, newLayoutGroup, prevInfo); } } } guiFrame.AddToGUIUpdateList(); contextMenu?.AddToGUIUpdateList(false, 1); subContextMenu?.AddToGUIUpdateList(false, 1); } public void SelectNextCharacter() { if (!AllowCharacterSwitch || GameMain.IsMultiplayer || characters.None()) { return; } if (crewList.Content.GetChild(TryAdjustIndex(1))?.UserData is Character character) { SelectCharacter(character); } } public void SelectPreviousCharacter() { if (!AllowCharacterSwitch || GameMain.IsMultiplayer || characters.None()) { return; } if (crewList.Content.GetChild(TryAdjustIndex(-1))?.UserData is Character character) { SelectCharacter(character); } } private void SelectCharacter(Character character) { if (!AllowCharacterSwitch) { return; } //make the previously selected character wait in place for some time //(so they don't immediately start idling and walking away from their station) if (Character.Controlled?.AIController?.ObjectiveManager != null) { Character.Controlled.AIController.ObjectiveManager.WaitTimer = CharacterWaitOnSwitch; } DisableCommandUI(); Character.Controlled = character; } private int TryAdjustIndex(int amount) { int index = Character.Controlled == null ? 0 : crewList.Content.GetChildIndex(crewList.Content.GetChildByUserData(Character.Controlled)) + amount; int lastIndex = crewList.Content.CountChildren - 1; if (index > lastIndex) { index = 0; } if (index < 0) { index = lastIndex; } return index; } partial void UpdateProjectSpecific(float deltaTime) { // Quick selection if (!GameMain.IsMultiplayer && GUI.KeyboardDispatcher.Subscriber == null) { if (PlayerInput.KeyHit(InputType.SelectNextCharacter)) { SelectNextCharacter(); } if (PlayerInput.KeyHit(InputType.SelectPreviousCharacter)) { SelectPreviousCharacter(); } } // context menu behavior if (contextMenu != null) { var promote = contextMenu.GetChild()?.Content.GetChildByUserData("promote"); if (promote != null && promote.Enabled) { promote.ExternalHighlight = subContextMenu != null; if (GUI.IsMouseOn(promote)) { if (contextMenu.UserData is Client client && subContextMenu == null) { CreatePromoteSubMenu(new Point(promote.Rect.Right, promote.Rect.Y), client); } } else if (subContextMenu != null && !IsMouseOnContextMenu(subContextMenu.Rect)) { subContextMenu = null; } } else { subContextMenu = null; } if (subContextMenu == null && !IsMouseOnContextMenu(contextMenu.Rect)) { contextMenu = null; } } if (contextMenu == null && subContextMenu != null) { subContextMenu = null; } if (GUI.DisableHUD) { return; } #region Command UI WasCommandInterfaceDisabledThisUpdate = false; if (PlayerInput.KeyDown(InputType.Command) && (GUI.KeyboardDispatcher.Subscriber == null || GUI.KeyboardDispatcher.Subscriber == crewList) && commandFrame == null && !clicklessSelectionActive && CanIssueOrders) { if (PlayerInput.KeyDown(Keys.LeftShift) || PlayerInput.KeyDown(Keys.RightShift)) { CreateCommandUI(FindEntityContext(), true); } else { CreateCommandUI(HUDLayoutSettings.BottomRightInfoArea.Contains(PlayerInput.MousePosition) ? Character.Controlled : GUI.MouseOn?.UserData as Character); } GUI.PlayUISound(GUISoundType.PopupMenu); clicklessSelectionActive = isOpeningClick = true; } if (commandFrame != null) { void ResetNodeSelection(GUIButton newSelectedNode = null) { if (commandFrame == null) { return; } selectedNode?.Children.ForEach(c => c.Color = c.HoverColor * nodeColorMultiplier); selectedNode = newSelectedNode; timeSelected = 0; isSelectionHighlighted = false; } if (!CanIssueOrders) { DisableCommandUI(); } else if (PlayerInput.SecondaryMouseButtonClicked() && characterContext == null && (optionNodes.Any(n => GUI.IsMouseOn(n.Item1)) || shortcutNodes.Any(n => GUI.IsMouseOn(n)))) { var node = optionNodes.Find(n => GUI.IsMouseOn(n.Item1))?.Item1 ?? shortcutNodes.Find(n => GUI.IsMouseOn(n)); // Make sure the node is for an option-less order or an order option if ((node.UserData is Order order && !order.HasOptions && (!order.MustSetTarget || itemContext != null)) || node.UserData is Tuple) { CreateAssignmentNodes(node); } } // TODO: Consider using HUD.CloseHUD() instead of KeyHit(Escape), the former method is also used for health UI else if ((PlayerInput.KeyHit(InputType.Command) && selectedNode == null && !clicklessSelectionActive) || PlayerInput.KeyHit(InputType.Deselect) || PlayerInput.KeyHit(Keys.Escape)) { DisableCommandUI(); } else if (PlayerInput.KeyUp(InputType.Command)) { if (!isOpeningClick && clicklessSelectionActive && timeSelected < 0.15f) { DisableCommandUI(); } else { clicklessSelectionActive = isOpeningClick = false; if (selectedNode != null) { ResetNodeSelection(); } } } else if (PlayerInput.KeyDown(InputType.Command) && (targetFrame == null || !targetFrame.Visible)) { if (!GUI.IsMouseOn(centerNode)) { clicklessSelectionActive = true; var mouseBearing = GetBearing(centerNode.Center, PlayerInput.MousePosition, flipY: true); GUIComponent closestNode = null; float closestBearing = 0; optionNodes.ForEach(n => CheckIfClosest(n.Item1)); CheckIfClosest(returnNode); void CheckIfClosest(GUIComponent comp) { if (comp == null) { return; } var offset = comp.RectTransform.AbsoluteOffset; var nodeBearing = GetBearing(centerNode.RectTransform.AbsoluteOffset.ToVector2(), offset.ToVector2(), flipY: true); if (closestNode == null) { closestNode = comp; closestBearing = Math.Abs(nodeBearing - mouseBearing); } else { var difference = Math.Abs(nodeBearing - mouseBearing); if (difference < closestBearing) { closestNode = comp; closestBearing = difference; } } } if (closestNode != null && closestNode == selectedNode) { timeSelected += deltaTime; if (timeSelected >= selectionTime) { selectedNode.OnClicked?.Invoke(selectedNode, selectedNode.UserData); ResetNodeSelection(); } else if (timeSelected >= 0.15f && !isSelectionHighlighted) { selectedNode.Children.ForEach(c => c.Color = c.HoverColor); isSelectionHighlighted = true; } } else { ResetNodeSelection(closestNode as GUIButton); } } else if (selectedNode != null) { ResetNodeSelection(); } } if (assignmentNodeIcons.Any()) { if (PlayerInput.KeyDown(Keys.LeftShift) || PlayerInput.KeyDown(Keys.RightShift)) { if (assignmentNodeIcons.First().OrderIcon.Visible) { foreach (AssignmentNodeIconSet set in assignmentNodeIcons) { set.OrderIcon.Visible = false; set.JobIcon.Visible = true; } } } else { if (assignmentNodeIcons.First().JobIcon.Visible) { foreach (AssignmentNodeIconSet set in assignmentNodeIcons) { set.JobIcon.Visible = false; set.OrderIcon.Visible = true; } } } } var hotkeyHit = false; foreach (Tuple node in optionNodes) { if (node.Item2 != Keys.None && PlayerInput.KeyHit(node.Item2)) { (node.Item1 as GUIButton)?.OnClicked?.Invoke(node.Item1 as GUIButton, node.Item1.UserData); ResetNodeSelection(); hotkeyHit = true; break; } } if (!hotkeyHit) { if (returnNodeHotkey != Keys.None && PlayerInput.KeyHit(returnNodeHotkey)) { returnNode?.OnClicked?.Invoke(returnNode, returnNode.UserData); ResetNodeSelection(); } else if (expandNodeHotkey != Keys.None && PlayerInput.KeyHit(expandNodeHotkey)) { expandNode?.OnClicked?.Invoke(expandNode, expandNode.UserData); ResetNodeSelection(); } } } else if (!PlayerInput.KeyDown(InputType.Command)) { clicklessSelectionActive = false; } // TODO: Expand crew list to use command button's space when it's not visible if (!IsSinglePlayer && commandButton != null) { if (!CanIssueOrders && commandButton.Visible) { commandButton.Visible = false; } else if (CanIssueOrders && !commandButton.Visible) { commandButton.Visible = true; } } #endregion if (ChatBox != null) { ChatBox.Update(deltaTime); ChatBox.InputBox.Visible = Character.Controlled != null; if (!DebugConsole.IsOpen && ChatBox.InputBox.Visible && GUI.KeyboardDispatcher.Subscriber == null) { if (PlayerInput.KeyHit(InputType.Chat) && !ChatBox.InputBox.Selected) { ChatBox.InputBox.AddToGUIUpdateList(); ChatBox.GUIFrame.Flash(Color.DarkGreen, 0.5f); if (!ChatBox.ToggleOpen) { ChatBox.CloseAfterMessageSent = !ChatBox.ToggleOpen; ChatBox.ToggleOpen = true; } ChatBox.InputBox.Select(ChatBox.InputBox.Text.Length); } if (PlayerInput.KeyHit(InputType.RadioChat) && !ChatBox.InputBox.Selected) { if (Character.Controlled == null || Character.Controlled.SpeechImpediment < 100) { ChatBox.InputBox.AddToGUIUpdateList(); ChatBox.GUIFrame.Flash(Color.YellowGreen, 0.5f); if (!ChatBox.ToggleOpen) { ChatBox.CloseAfterMessageSent = !ChatBox.ToggleOpen; ChatBox.ToggleOpen = true; } if (!ChatBox.InputBox.Text.StartsWith(ChatBox.RadioChatString)) { ChatBox.InputBox.Text = ChatBox.RadioChatString; } ChatBox.InputBox.Select(ChatBox.InputBox.Text.Length); } } } } if (!GUI.DisableUpperHUD) { crewArea.Visible = characters.Count > 0 && CharacterHealth.OpenHealthWindow == null; foreach (GUIComponent child in crewList.Content.Children) { if (child.UserData is Character character) { child.Visible = Character.Controlled == null || Character.Controlled.TeamID == character.TeamID; if (child.Visible) { if (character == Character.Controlled && child.State != GUIComponent.ComponentState.Selected) { crewList.Select(character, force: true); } if (child.FindChild(c => c is GUILayoutGroup) is GUILayoutGroup layoutGroup) { if (GetCurrentOrderComponent(layoutGroup) is GUIComponent orderButton && orderButton.GetChildByUserData("colorsource") is GUIComponent orderIcon && orderButton.GetChildByUserData("cancel") is GUIComponent cancelIcon) { cancelIcon.Visible = GUI.IsMouseOn(orderIcon); } if (layoutGroup.GetChildByUserData("soundicons")? .FindChild(c => c.UserData is Pair pair && pair.First == "soundicon") is GUIImage soundIcon) { VoipClient.UpdateVoiceIndicator(soundIcon, 0.0f, deltaTime); } } } } } crewArea.RectTransform.AbsoluteOffset = Vector2.SmoothStep( new Vector2(-crewArea.Rect.Width - HUDLayoutSettings.Padding, 0.0f), Vector2.Zero, crewListOpenState).ToPoint(); crewListOpenState = ToggleCrewListOpen ? Math.Min(crewListOpenState + deltaTime * 2.0f, 1.0f) : Math.Max(crewListOpenState - deltaTime * 2.0f, 0.0f); if (GUI.KeyboardDispatcher.Subscriber == null && PlayerInput.KeyHit(InputType.CrewOrders)) { GUI.PlayUISound(GUISoundType.PopupMenu); ToggleCrewListOpen = !ToggleCrewListOpen; } } UpdateReports(); } #endregion #region Command UI public static bool IsCommandInterfaceOpen { get { if (GameMain.GameSession?.CrewManager == null) { return false; } else { return GameMain.GameSession.CrewManager.commandFrame != null || GameMain.GameSession.CrewManager.WasCommandInterfaceDisabledThisUpdate; } } } private GUIFrame commandFrame, targetFrame; private GUIButton centerNode, returnNode, expandNode, shortcutCenterNode; private readonly List> optionNodes = new List>(); private Keys returnNodeHotkey = Keys.None, expandNodeHotkey = Keys.None; private readonly List shortcutNodes = new List(); private readonly List extraOptionNodes = new List(); private GUICustomComponent nodeConnectors; private GUIImage background; private GUIButton selectedNode; private readonly float selectionTime = 0.75f; private float timeSelected = 0.0f; private bool clicklessSelectionActive, isOpeningClick, isSelectionHighlighted; private Point centerNodeSize, nodeSize, shortcutCenterNodeSize, shortcutNodeSize, returnNodeSize, assignmentNodeSize; private float centerNodeMargin, optionNodeMargin, shortcutCenterNodeMargin, shortcutNodeMargin, returnNodeMargin; private List availableCategories; private Stack historyNodes = new Stack(); private readonly List extraOptionCharacters = new List(); private readonly List assignmentNodeIcons = new List(); private struct AssignmentNodeIconSet { public GUIImage OrderIcon { get; private set; } public GUIImage JobIcon { get; private set; } public AssignmentNodeIconSet(GUIImage orderIcon, GUIImage jobIcon) { OrderIcon = orderIcon; JobIcon = jobIcon; } } /// /// node.Color = node.HighlightColor * nodeColorMultiplier /// private const float nodeColorMultiplier = 0.75f; private int nodeDistance = (int)(GUI.Scale * 250); private const float returnNodeDistanceModifier = 0.65f; private Order dismissedOrderPrefab; private Character characterContext; private Item itemContext; private Hull hullContext; private bool isContextual; private readonly List contextualOrders = new List(); private Point shorcutCenterNodeOffset; private const int maxShorcutNodeCount = 4; private bool WasCommandInterfaceDisabledThisUpdate { get; set; } private bool CanIssueOrders { get { #if DEBUG return Character.Controlled == null || Character.Controlled.Info != null && Character.Controlled.SpeechImpediment < 100.0f; #else return Character.Controlled?.Info != null && Character.Controlled.SpeechImpediment < 100.0f; #endif } } private bool CanSomeoneHearCharacter() { #if DEBUG return true; #else return Character.Controlled != null && characters.Any(c => c != Character.Controlled && c.CanHearCharacter(Character.Controlled)); #endif } private Entity FindEntityContext() { if (Character.Controlled?.FocusedCharacter != null) { if (Character.Controlled?.FocusedItem != null) { Vector2 mousePos = GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition); if (Vector2.Distance(mousePos, Character.Controlled.FocusedCharacter.WorldPosition) < Vector2.Distance(mousePos, Character.Controlled.FocusedItem.WorldPosition)) { return Character.Controlled.FocusedCharacter; } else { return Character.Controlled.FocusedItem; } } else { return Character.Controlled.FocusedCharacter; } } else if (TryGetBreachedHullAtHoveredWall(out Hull breachedHull)) { return breachedHull; } else { return Character.Controlled?.FocusedItem; } } private void CreateCommandUI(Entity entityContext = null, bool forceContextual = false) { if (commandFrame != null) { DisableCommandUI(); } CharacterHealth.OpenHealthWindow = null; // Character context works differently to others as we still use the "basic" command interface, // but the order will be automatically assigned to this character isContextual = forceContextual; if (entityContext is Character character) { characterContext = character; isContextual = false; } else if (entityContext is Item item) { itemContext = item; isContextual = true; } else if (entityContext is Hull hull) { hullContext = hull; isContextual = true; } ScaleCommandUI(); commandFrame = new GUIFrame( new RectTransform(Vector2.One, GUICanvas.Instance, anchor: Anchor.Center), style: null, color: Color.Transparent); background = new GUIImage( new RectTransform(Vector2.One, commandFrame.RectTransform, anchor: Anchor.Center), "CommandBackground"); background.Color = background.Color * 0.8f; GUIButton startNode = null; if (characterContext == null) { startNode = new GUIButton( new RectTransform(centerNodeSize, parent: commandFrame.RectTransform, anchor: Anchor.Center), style: null); CreateNodeIcon(startNode.RectTransform, "CommandStartNode"); } else { // Button startNode = new GUIButton( new RectTransform(centerNodeSize, parent: commandFrame.RectTransform, anchor: Anchor.Center), style: null); // Container new GUIImage( new RectTransform(Vector2.One, startNode.RectTransform, anchor: Anchor.Center), "CommandNodeContainer", scaleToFit: true) { Color = characterContext.Info.Job.Prefab.UIColor * nodeColorMultiplier, HoverColor = characterContext.Info.Job.Prefab.UIColor, UserData = "colorsource" }; // Character icon new GUICustomComponent( new RectTransform(Vector2.One, startNode.RectTransform, anchor: Anchor.Center), (spriteBatch, _) => { if (!(entityContext is Character character)) { return; } var node = startNode; character.Info.DrawJobIcon(spriteBatch, new Rectangle((int)(node.Rect.X + node.Rect.Width * 0.5f), (int)(node.Rect.Y + node.Rect.Height * 0.1f), (int)(node.Rect.Width * 0.6f), (int)(node.Rect.Height * 0.8f))); character.Info.DrawIcon(spriteBatch, new Vector2(node.Rect.X + node.Rect.Width * 0.35f, node.Center.Y), node.Rect.Size.ToVector2() * 0.7f); }) { ToolTip = characterContext.Info.DisplayName + " (" + characterContext.Info.Job.Name + ")" }; } SetCenterNode(startNode); availableCategories ??= GetAvailableCategories(); dismissedOrderPrefab ??= Order.GetPrefab("dismissed"); if (isContextual) { CreateContextualOrderNodes(); } else { CreateShortcutNodes(); CreateOrderCategoryNodes(); } CreateNodeConnectors(); if (Character.Controlled != null) { Character.Controlled.dontFollowCursor = true; } } private void ToggleCommandUI() { if (commandFrame == null) { if (CanIssueOrders) { CreateCommandUI(); } } else { DisableCommandUI(); } } private void ScaleCommandUI() { // Node sizes nodeSize = new Point((int)(100 * GUI.Scale)); centerNodeSize = nodeSize; returnNodeSize = new Point((int)(48 * GUI.Scale)); assignmentNodeSize = new Point((int)(64 * GUI.Scale)); shortcutCenterNodeSize = returnNodeSize; shortcutNodeSize = assignmentNodeSize; // Node margins (used in drawing the connecting lines) centerNodeMargin = centerNodeSize.X * 0.5f; optionNodeMargin = nodeSize.X * 0.5f; shortcutCenterNodeMargin = shortcutCenterNodeSize.X * 0.45f; shortcutNodeMargin = shortcutNodeSize.X * 0.5f; returnNodeMargin = returnNodeSize.X * 0.5f; nodeDistance = (int)(150 * GUI.Scale); shorcutCenterNodeOffset = new Point(0, (int)(1.25f * nodeDistance)); } private List GetAvailableCategories() { availableCategories = new List(); foreach (OrderCategory category in Enum.GetValues(typeof(OrderCategory))) { if (Order.PrefabList.Any(o => o.Category == category && !o.TargetAllCharacters)) { availableCategories.Add(category); } } return availableCategories; } private void CreateNodeConnectors() { nodeConnectors = new GUICustomComponent( new RectTransform(Vector2.One, commandFrame.RectTransform), onDraw: DrawNodeConnectors); nodeConnectors.SetAsFirstChild(); background.SetAsFirstChild(); } private void DrawNodeConnectors(SpriteBatch spriteBatch, GUIComponent container) { if (centerNode == null || optionNodes == null) { return; } var startNodePos = centerNode.Rect.Center.ToVector2(); // Don't draw connectors for mini map options or assignment nodes if ((targetFrame == null || !targetFrame.Visible) && !(optionNodes.FirstOrDefault()?.Item1.UserData is Character)) { optionNodes.ForEach(n => DrawNodeConnector(startNodePos, centerNodeMargin, n.Item1, optionNodeMargin, spriteBatch)); } DrawNodeConnector(startNodePos, centerNodeMargin, returnNode, returnNodeMargin, spriteBatch); if (shortcutCenterNode == null || !shortcutCenterNode.Visible) { return; } DrawNodeConnector(startNodePos, centerNodeMargin, shortcutCenterNode, shortcutCenterNodeMargin, spriteBatch); startNodePos = shortcutCenterNode.Rect.Center.ToVector2(); shortcutNodes.ForEach(n => DrawNodeConnector(startNodePos, shortcutCenterNodeMargin, n, shortcutNodeMargin, spriteBatch)); } private void DrawNodeConnector(Vector2 startNodePos, float startNodeMargin, GUIComponent endNode, float endNodeMargin, SpriteBatch spriteBatch) { if (endNode == null || !endNode.Visible) { return; } var endNodePos = endNode.Rect.Center.ToVector2(); var direction = (endNodePos - startNodePos) / Vector2.Distance(startNodePos, endNodePos); var start = startNodePos + direction * startNodeMargin; var end = endNodePos - direction * endNodeMargin; var colorSource = endNode.GetChildByUserData("colorsource"); if ((selectedNode == null && endNode != shortcutCenterNode && GUI.IsMouseOn(endNode)) || (isSelectionHighlighted && (endNode == selectedNode || (endNode == shortcutCenterNode && shortcutNodes.Any(n => GUI.IsMouseOn(n)))))) { GUI.DrawLine(spriteBatch, start, end, colorSource != null ? colorSource.HoverColor : Color.White, width: 4); } else { GUI.DrawLine(spriteBatch, start, end, colorSource != null ? colorSource.Color : Color.White * nodeColorMultiplier, width: 2); } } public void DisableCommandUI() { if (commandFrame == null) { return; } WasCommandInterfaceDisabledThisUpdate = true; RemoveOptionNodes(); historyNodes.Clear(); nodeConnectors = null; centerNode = null; returnNode = null; expandNode = null; shortcutCenterNode = null; targetFrame = null; selectedNode = null; timeSelected = 0; background = null; commandFrame = null; extraOptionCharacters.Clear(); assignmentNodeIcons.Clear(); isOpeningClick = isSelectionHighlighted = false; characterContext = null; itemContext = null; isContextual = false; contextualOrders.Clear(); returnNodeHotkey = expandNodeHotkey = Keys.None; if (Character.Controlled != null) { Character.Controlled.dontFollowCursor = false; } } private bool NavigateForward(GUIButton node, object userData) { if (!(optionNodes.Find(n => n.Item1 == node) is Tuple optionNode) || !optionNodes.Remove(optionNode)) { shortcutNodes.Remove(node); }; RemoveOptionNodes(); if (returnNode != null) { returnNode.RemoveChild(returnNode.GetChildByUserData("hotkey")); returnNode.Children.ForEach(child => child.Visible = false); returnNode.Visible = false; historyNodes.Push(returnNode); } // When the mini map is shown, always position the return node on the bottom var offset = node?.UserData is Order order && order.GetMatchingItems(true).Count > 1 ? new Point(0, (int)(returnNodeDistanceModifier * nodeDistance)) : node.RectTransform.AbsoluteOffset.Multiply(-returnNodeDistanceModifier); SetReturnNode(centerNode, offset); SetCenterNode(node); if (shortcutCenterNode != null) { commandFrame.RemoveChild(shortcutCenterNode); shortcutCenterNode = null; } CreateNodes(userData); CreateReturnNodeHotkey(); return true; } private bool NavigateBackward(GUIButton node, object userData) { RemoveOptionNodes(); if (targetFrame != null) { targetFrame.Visible = false; } // TODO: Center node could move to option node instead of being removed commandFrame.RemoveChild(centerNode); SetCenterNode(node); if (historyNodes.Count > 0) { var historyNode = historyNodes.Pop(); SetReturnNode(historyNode, historyNode.RectTransform.AbsoluteOffset); historyNode.Visible = true; historyNode.RemoveChild(historyNode.GetChildByUserData("hotkey")); historyNode.Children.ForEach(child => child.Visible = true); } else { returnNode = null; } CreateNodes(userData); CreateReturnNodeHotkey(); return true; } private void CreateReturnNodeHotkey() { if (returnNode != null && returnNode.Visible) { var hotkey = 1; if (targetFrame == null || !targetFrame.Visible) { hotkey = optionNodes.Count + 1; if (expandNode != null && expandNode.Visible) { hotkey += 1; } } CreateHotkeyIcon(returnNode.RectTransform, hotkey % 10, true); returnNodeHotkey = Keys.D0 + hotkey % 10; } else { returnNodeHotkey = Keys.None; } } private void SetCenterNode(GUIButton node, bool resetAnchor = false) { node.RectTransform.Parent = commandFrame.RectTransform; if (resetAnchor) { node.RectTransform.SetPosition(Anchor.Center); } node.RectTransform.SetPosition(Anchor.Center); node.RectTransform.MoveOverTime(Point.Zero, CommandNodeAnimDuration); node.RectTransform.ScaleOverTime(centerNodeSize, CommandNodeAnimDuration); node.RemoveChild(node.GetChildByUserData("hotkey")); foreach (GUIComponent c in node.Children) { c.Color = c.HoverColor * nodeColorMultiplier; c.HoverColor = c.Color; c.PressedColor = c.Color; c.SelectedColor = c.Color; c.ToolTip = characterContext != null ? characterContext.Info.DisplayName + " (" + characterContext.Info.Job.Name + ")" : null; } node.OnClicked = null; centerNode = node; } private void SetReturnNode(GUIButton node, Point offset) { node.RectTransform.MoveOverTime(offset, CommandNodeAnimDuration); node.RectTransform.ScaleOverTime(returnNodeSize, CommandNodeAnimDuration); foreach (GUIComponent c in node.Children) { c.HoverColor = c.Color * (1 / nodeColorMultiplier); c.PressedColor = c.HoverColor; c.SelectedColor = c.HoverColor; c.ToolTip = TextManager.Get("commandui.return"); } node.OnClicked = NavigateBackward; returnNode = node; } private bool CreateNodes(object userData) { if (userData == null) { if (isContextual) { CreateContextualOrderNodes(); } else { CreateShortcutNodes(); CreateOrderCategoryNodes(); } } else if (userData is OrderCategory category) { CreateOrderNodes(category); } else if (userData is Order order) { CreateOrderOptions(order); } return true; } private void RemoveOptionNodes() { optionNodes.ForEach(node => commandFrame.RemoveChild(node.Item1)); optionNodes.Clear(); shortcutNodes.ForEach(node => commandFrame.RemoveChild(node)); shortcutNodes.Clear(); commandFrame.RemoveChild(expandNode); expandNode = null; expandNodeHotkey = Keys.None; RemoveExtraOptionNodes(); assignmentNodeIcons.Clear(); } private void RemoveExtraOptionNodes() { extraOptionNodes.ForEach(node => commandFrame.RemoveChild(node)); extraOptionNodes.Clear(); } private void CreateOrderCategoryNodes() { // TODO: Calculate firstAngle parameter based on category count var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, nodeDistance, availableCategories.Count, MathHelper.ToRadians(225)); var offsetIndex = 0; availableCategories.ForEach(oc => CreateOrderCategoryNode(oc, offsets[offsetIndex++].ToPoint(), offsetIndex)); } private void CreateOrderCategoryNode(OrderCategory category, Point offset, int hotkey) { var node = new GUIButton( new RectTransform(nodeSize, parent: commandFrame.RectTransform, anchor: Anchor.Center), style: null) { UserData = category, OnClicked = NavigateForward }; node.RectTransform.MoveOverTime(offset, CommandNodeAnimDuration); if (Order.OrderCategoryIcons.TryGetValue(category, out Tuple sprite)) { var tooltip = TextManager.Get("ordercategorytitle." + category.ToString().ToLower()); var categoryDescription = TextManager.Get("ordercategorydescription." + category.ToString(), true); if (!string.IsNullOrWhiteSpace(categoryDescription)) { tooltip += "\n" + categoryDescription; } CreateNodeIcon(node.RectTransform, sprite.Item1, sprite.Item2, tooltip: tooltip); } CreateHotkeyIcon(node.RectTransform, hotkey % 10); optionNodes.Add(new Tuple(node, Keys.D0 + hotkey % 10)); } private void CreateShortcutNodes() { Submarine sub = GetTargetSubmarine(); if (sub == null) { return; } shortcutNodes.Clear(); if (shortcutNodes.Count < maxShorcutNodeCount && sub.GetItems(false).Find(i => i.HasTag("reactor") && !i.NonInteractable)?.GetComponent() is Reactor reactor) { var reactorOutput = -reactor.CurrPowerConsumption; // If player is not an engineer AND the reactor is not powered up AND nobody is using the reactor // ---> Create shortcut node for "Operate Reactor" order's "Power Up" option if ((Character.Controlled == null || Character.Controlled.Info.Job.Prefab != JobPrefab.Get("engineer")) && reactorOutput < float.Epsilon && characters.None(c => c.SelectedConstruction == reactor.Item)) { var order = new Order(Order.GetPrefab("operatereactor"), reactor.Item, reactor, Character.Controlled); var option = order.Prefab.Options[0]; shortcutNodes.Add( CreateOrderOptionNode(shortcutNodeSize, null, Point.Zero, order, option, order.Prefab.GetOptionName(option), -1)); } } // TODO: Reconsider the conditions as bot captain can have the nav term selected without operating it // If player is not a captain AND nobody is using the nav terminal AND the nav terminal is powered up // --> Create shortcut node for Steer order if (shortcutNodes.Count < maxShorcutNodeCount && (Character.Controlled == null || Character.Controlled.Info.Job.Prefab != JobPrefab.Get("captain")) && sub.GetItems(false).Find(i => i.HasTag("navterminal") && !i.NonInteractable) is Item nav && characters.None(c => c.SelectedConstruction == nav) && nav.GetComponent() is Steering steering && steering.Voltage > steering.MinVoltage) { shortcutNodes.Add( CreateOrderNode(shortcutNodeSize, null, Point.Zero, Order.GetPrefab("steer"), -1)); } // If player is not a security officer AND invaders are reported // --> Create shorcut node for Fight Intruders order if (shortcutNodes.Count < maxShorcutNodeCount && (Character.Controlled == null || Character.Controlled.Info.Job.Prefab != JobPrefab.Get("securityofficer")) && (Order.GetPrefab("reportintruders") is Order reportIntruders && ActiveOrders.Any(o => o.First.Prefab == reportIntruders))) { shortcutNodes.Add( CreateOrderNode(shortcutNodeSize, null, Point.Zero, Order.GetPrefab("fightintruders"), -1)); } // If player is not a mechanic AND a breach has been reported // --> Create shorcut node for Fix Leaks order if (shortcutNodes.Count < maxShorcutNodeCount && (Character.Controlled == null || Character.Controlled.Info.Job.Prefab != JobPrefab.Get("mechanic")) && (Order.GetPrefab("reportbreach") is Order reportBreach && ActiveOrders.Any(o => o.First.Prefab == reportBreach))) { shortcutNodes.Add( CreateOrderNode(shortcutNodeSize, null, Point.Zero, Order.GetPrefab("fixleaks"), -1)); } // If player is not an engineer AND broken devices have been reported // --> Create shortcut node for Repair Damaged Systems order if (shortcutNodes.Count < maxShorcutNodeCount && (Character.Controlled == null || Character.Controlled.Info.Job.Prefab != JobPrefab.Get("engineer")) && (Order.GetPrefab("reportbrokendevices") is Order reportBrokenDevices && ActiveOrders.Any(o => o.First.Prefab == reportBrokenDevices))) { shortcutNodes.Add( CreateOrderNode(shortcutNodeSize, null, Point.Zero, Order.GetPrefab("repairsystems"), -1)); } // If fire is reported // --> Create shortcut node for Extinguish Fires order if (shortcutNodes.Count < maxShorcutNodeCount && ActiveOrders.Any(o=> o.First.Prefab == Order.GetPrefab("reportfire"))) { shortcutNodes.Add( CreateOrderNode(shortcutNodeSize, null, Point.Zero, Order.GetPrefab("extinguishfires"), -1)); } if (shortcutNodes.Count < maxShorcutNodeCount && characterContext?.Info?.Job?.Prefab?.AppropriateOrders != null) { foreach (string orderIdentifier in characterContext.Info.Job.Prefab.AppropriateOrders) { if (Order.GetPrefab(orderIdentifier) is Order orderPrefab && shortcutNodes.None(n => (n.UserData is Order order && order.Identifier == orderIdentifier) || (n.UserData is Tuple orderWithOption && orderWithOption.Item1.Identifier == orderIdentifier)) && !orderPrefab.TargetAllCharacters && orderPrefab.Category != null) { if (!orderPrefab.MustSetTarget || orderPrefab.GetMatchingItems(sub, true).Any()) { shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, orderPrefab, -1)); } if (shortcutNodes.Count >= maxShorcutNodeCount) { break; } } } } if (shortcutNodes.Count < 1) { return; } shortcutCenterNode = new GUIButton( new RectTransform(shortcutCenterNodeSize, parent: commandFrame.RectTransform, anchor: Anchor.Center), style: null); CreateNodeIcon(shortcutCenterNode.RectTransform, "CommandShortcutNode"); foreach (GUIComponent c in shortcutCenterNode.Children) { c.HoverColor = c.Color; c.PressedColor = c.Color; c.SelectedColor = c.Color; } shortcutCenterNode.RectTransform.MoveOverTime(shorcutCenterNodeOffset, CommandNodeAnimDuration); var nodeCountForCalculations = shortcutNodes.Count * 2 + 2; var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, 0.75f * nodeDistance, nodeCountForCalculations); var firstOffsetIndex = nodeCountForCalculations / 2 - 1; for (int i = 0; i < shortcutNodes.Count; i++) { shortcutNodes[i].RectTransform.Parent = commandFrame.RectTransform; shortcutNodes[i].RectTransform.MoveOverTime(shorcutCenterNodeOffset + offsets[firstOffsetIndex - i].ToPoint(), CommandNodeAnimDuration); } } private void CreateOrderNodes(OrderCategory orderCategory) { var orders = Order.PrefabList.FindAll(o => o.Category == orderCategory && !o.TargetAllCharacters); Order order; bool disableNode; var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, nodeDistance, GetCircumferencePointCount(orders.Count), GetFirstNodeAngle(orders.Count)); for (int i = 0; i < orders.Count; i++) { order = orders[i]; disableNode = !CanSomeoneHearCharacter() || (order.MustSetTarget && (order.ItemComponentType != null || order.ItemIdentifiers.Length > 0) && order.GetMatchingItems(true).None()); optionNodes.Add(new Tuple( CreateOrderNode(nodeSize, commandFrame.RectTransform, offsets[i].ToPoint(), order, (i + 1) % 10, disableNode: disableNode, checkIfOrderCanBeHeard: false), !disableNode ? Keys.D0 + (i + 1) % 10 : Keys.None)); } } /// /// Create order nodes based on the item context /// private void CreateContextualOrderNodes() { if (contextualOrders.None()) { // Check if targeting an item or a hull if (itemContext != null && !itemContext.NonInteractable) { foreach (Order p in Order.PrefabList) { if ((p.ItemIdentifiers.Length > 0 && (p.ItemIdentifiers.Contains(itemContext.Prefab.Identifier) || itemContext.HasTag(p.ItemIdentifiers))) || (p.ItemComponentType != null && itemContext.Components.Any(c => c?.GetType() == p.ItemComponentType))) { contextualOrders.Add(p.HasOptions ? p : new Order(p, itemContext, itemContext.Components.FirstOrDefault(c => c?.GetType() == p.ItemComponentType), Character.Controlled)); } } // If targeting a periscope connected to a turret, show the 'operateweapons' order var orderIdentifier = "operateweapons"; var operateWeaponsPrefab = Order.GetPrefab(orderIdentifier); if (contextualOrders.None(o => o.Identifier.Equals(orderIdentifier)) && itemContext.Components.Any(c => c is Controller)) { var turret = itemContext.GetConnectedComponents().FirstOrDefault(c => operateWeaponsPrefab.ItemIdentifiers.Contains(c.Item.Prefab.Identifier)) ?? itemContext.GetConnectedComponents(recursive: true).FirstOrDefault(c => operateWeaponsPrefab.ItemIdentifiers.Contains(c.Item.Prefab.Identifier)); if (turret != null) { contextualOrders.Add(new Order(operateWeaponsPrefab, turret.Item, turret, Character.Controlled)); } } // If targeting a repairable item, show the 'repairsystems' order orderIdentifier = "repairsystems"; if (contextualOrders.None(o => o.Identifier.Equals(orderIdentifier)) && itemContext.Repairables.Any()) { contextualOrders.Add(new Order(Order.GetPrefab(orderIdentifier), itemContext, null, Character.Controlled)); if (itemContext.Repairables.Any(r => r != null && r.requiredSkills.Any(s => s != null && s.Identifier.Equals("electrical")))) { contextualOrders.Add(new Order(Order.GetPrefab("repairelectrical"), itemContext, null, Character.Controlled)); } else if (itemContext.Repairables.Any(r => r != null && r.requiredSkills.Any(s => s != null && s.Identifier.Equals("mechanical")))) { contextualOrders.Add(new Order(Order.GetPrefab("repairmechanical"), itemContext, null, Character.Controlled)); } } // Always show the 'wait' order if there are other crew members alive orderIdentifier = "wait"; if (contextualOrders.None(o => o.Identifier.Equals(orderIdentifier)) && characters.Any(c => c != Character.Controlled)) { contextualOrders.Add(new Order(Order.GetPrefab(orderIdentifier), itemContext, null, Character.Controlled)); } // Remove the 'pumpwater' order if the target pump is auto-controlled (as it will immediately overwrite the work done by the bot) orderIdentifier = "pumpwater"; if (contextualOrders.FirstOrDefault(o => o.Identifier.Equals(orderIdentifier)) is Order o && itemContext.Components.FirstOrDefault(c => c.GetType() == o.ItemComponentType) is Pump pump) { if (pump.IsAutoControlled) { contextualOrders.Remove(o); } } } else if(hullContext != null) { contextualOrders.Add(new Order(Order.GetPrefab("fixleaks"), hullContext, null, Character.Controlled)); } // Show the 'follow' and 'dismissed' orders if there are other crew members alive if (characters.Any(c => c != Character.Controlled)) { var orderIdentifier = "follow"; if (contextualOrders.None(o => o.Identifier.Equals(orderIdentifier))) { contextualOrders.Add(Order.GetPrefab(orderIdentifier)); } // Show 'dismissed' order only when there are crew members with active orders orderIdentifier = "dismissed"; if (contextualOrders.None(o => o.Identifier.Equals(orderIdentifier)) && characters.Any(c => c.CurrentOrder != null && !c.CurrentOrder.Identifier.Equals(orderIdentifier))) { contextualOrders.Add(Order.GetPrefab(orderIdentifier)); } } } var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, nodeDistance, contextualOrders.Count, MathHelper.ToRadians(90f + 180f / contextualOrders.Count)); bool disableNode = !CanSomeoneHearCharacter(); for (int i = 0; i < contextualOrders.Count; i++) { optionNodes.Add(new Tuple( CreateOrderNode(nodeSize, commandFrame.RectTransform, offsets[i].ToPoint(), contextualOrders[i], (i + 1) % 10, disableNode: disableNode, checkIfOrderCanBeHeard: false), !disableNode ? Keys.D0 + (i + 1) % 10 : Keys.None)); } } public static bool DoesItemHaveContextualOrders(Item item) { if (Order.PrefabList.Any(o => o.ItemIdentifiers.Length > 0 && o.ItemIdentifiers.Contains(item.Prefab.Identifier))) { return true; } if (Order.PrefabList.Any(o => item.HasTag(o.ItemIdentifiers))) { return true; } if (Order.PrefabList.Any(o => o.ItemComponentType != null && item.Components.Any(c => c?.GetType() == o.ItemComponentType))) { return true; } if (item.Repairables.Any()) { return true; } var operateWeaponsPrefab = Order.GetPrefab("operateweapons"); return item.Components.Any(c => c is Controller) && (item.GetConnectedComponents().Any(c => operateWeaponsPrefab.ItemIdentifiers.Contains(c.Item.Prefab.Identifier)) || item.GetConnectedComponents(recursive: true).Any(c => operateWeaponsPrefab.ItemIdentifiers.Contains(c.Item.Prefab.Identifier))); } private GUIButton CreateOrderNode(Point size, RectTransform parent, Point offset, Order order, int hotkey, bool disableNode = false, bool checkIfOrderCanBeHeard = true) { var node = new GUIButton( new RectTransform(size, parent: parent, anchor: Anchor.Center), style: null) { UserData = order }; node.RectTransform.MoveOverTime(offset, CommandNodeAnimDuration); if (checkIfOrderCanBeHeard && !disableNode) { disableNode = !CanSomeoneHearCharacter(); } var mustSetOptionOrTarget = order.HasOptions || (order.MustSetTarget && itemContext == null); node.OnClicked = (button, userData) => { if (disableNode || !CanIssueOrders) { return false; } var o = userData as Order; if (o.MustManuallyAssign && characterContext == null) { CreateAssignmentNodes(node); } else if (mustSetOptionOrTarget) { NavigateForward(button, userData); } else { SetCharacterOrder(characterContext ?? GetCharacterForQuickAssignment(o), o, null, Character.Controlled); DisableCommandUI(); } return true; }; var icon = CreateNodeIcon(node.RectTransform, order.SymbolSprite, order.Color, tooltip: mustSetOptionOrTarget || characterContext != null ? order.Name : order.Name + "\n" + (!PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.leftmouse") : TextManager.Get("input.rightmouse")) + ": " + TextManager.Get("commandui.quickassigntooltip") + "\n" + (!PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.rightmouse") : TextManager.Get("input.leftmouse")) + ": " + TextManager.Get("commandui.manualassigntooltip")); if (disableNode) { node.CanBeFocused = icon.CanBeFocused = false; CreateBlockIcon(node.RectTransform); } else if (hotkey >= 0) { CreateHotkeyIcon(node.RectTransform, hotkey); } return node; } private void CreateOrderOptions(Order order) { Submarine submarine = GetTargetSubmarine(); var matchingItems = (itemContext == null && order.MustSetTarget) ? order.GetMatchingItems(submarine, true) : new List(); //more than one target item -> create a minimap-like selection with a pic of the sub if (itemContext == null && matchingItems.Count > 1) { // TODO: Further adjustments to frameSize calculations // I just divided the existing sizes by 2 to get it working quickly without it overlapping too much Point frameSize; Rectangle subBorders = submarine.GetDockedBorders(); if (subBorders.Width > subBorders.Height) { frameSize.X = Math.Min(GameMain.GraphicsWidth / 2, GameMain.GraphicsWidth - 50) / 2; //height depends on the dimensions of the sub frameSize.Y = (int)(frameSize.X * (subBorders.Height / (float)subBorders.Width)); } else { frameSize.Y = Math.Min((int)(GameMain.GraphicsHeight * 0.6f), GameMain.GraphicsHeight - 50) / 2; //width depends on the dimensions of the sub frameSize.X = (int)(frameSize.Y * (subBorders.Width / (float)subBorders.Height)); } // TODO: Use the old targetFrame if possible targetFrame = new GUIFrame( new RectTransform(frameSize, parent: commandFrame.RectTransform, anchor: Anchor.Center) { AbsoluteOffset = new Point(0, -150), Pivot = Pivot.BottomCenter }, style: "InnerFrame"); submarine.CreateMiniMap(targetFrame, pointsOfInterest: matchingItems); new GUICustomComponent(new RectTransform(Vector2.One, targetFrame.RectTransform), onDraw: DrawMiniMapOverlay) { CanBeFocused = false, UserData = submarine }; List optionElements = new List(); foreach (Item item in matchingItems) { var itemTargetFrame = targetFrame.Children.First().FindChild(item); if (itemTargetFrame == null) { continue; } var anchor = Anchor.TopLeft; if (itemTargetFrame.RectTransform.RelativeOffset.X < 0.5f && itemTargetFrame.RectTransform.RelativeOffset.Y < 0.5f) { anchor = Anchor.BottomRight; } else if (itemTargetFrame.RectTransform.RelativeOffset.X > 0.5f && itemTargetFrame.RectTransform.RelativeOffset.Y < 0.5f) { anchor = Anchor.BottomLeft; } else if (itemTargetFrame.RectTransform.RelativeOffset.X < 0.5f && itemTargetFrame.RectTransform.RelativeOffset.Y > 0.5f) { anchor = Anchor.TopRight; } GUIComponent optionElement; if (order.Options.Length > 1) { optionElement = new GUIFrame( new RectTransform( new Point((int)(250 * GUI.Scale), (int)((40 + order.Options.Length * 40) * GUI.Scale)), parent: itemTargetFrame.RectTransform, anchor: anchor), style: "InnerFrame"); new GUIFrame( new RectTransform(Vector2.One, optionElement.RectTransform, anchor: Anchor.Center), style: "OuterGlow", color: Color.Black * 0.7f); var optionContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.9f), optionElement.RectTransform, anchor: Anchor.Center)) { RelativeSpacing = 0.05f, Stretch = true }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), optionContainer.RectTransform), item != null ? item.Name : order.Name); for (int i = 0; i < order.Options.Length; i++) { optionNodes.Add(new Tuple( new GUIButton( new RectTransform(new Vector2(1.0f, 0.2f), optionContainer.RectTransform), text: order.GetOptionName(i), style: "GUITextBox") { UserData = new Tuple( item == null ? order : new Order(order, item, item.Components.FirstOrDefault(ic => ic.GetType() == order.ItemComponentType)), order.Options[i]), Font = GUI.SmallFont, OnClicked = (_, userData) => { if (!CanIssueOrders) { return false; } var o = userData as Tuple; SetCharacterOrder(characterContext ?? GetCharacterForQuickAssignment(o.Item1), o.Item1, o.Item2, Character.Controlled); DisableCommandUI(); return true; } }, Keys.None)); } } else { var userData = new Tuple(item == null ? order : new Order(order, item, item.Components.FirstOrDefault(ic => ic.GetType() == order.ItemComponentType)), ""); optionElement = new GUIButton( new RectTransform( new Point((int)(50 * GUI.Scale)), parent: itemTargetFrame.RectTransform, anchor: anchor), style: null) { UserData = userData, Font = GUI.SmallFont, ToolTip = item?.Name ?? order.Name, OnClicked = (_, userData) => { if (!CanIssueOrders) { return false; } var o = userData as Tuple; SetCharacterOrder(characterContext ?? GetCharacterForQuickAssignment(o.Item1), o.Item1, o.Item2, Character.Controlled); DisableCommandUI(); return true; } }; Sprite icon = null; order.MinimapIcons?.TryGetValue(item.Prefab.Identifier, out icon); var colorMultiplier = characters.Any(c => c.CurrentOrder != null && c.CurrentOrder.Identifier == userData.Item1.Identifier && c.CurrentOrder.TargetEntity == userData.Item1.TargetEntity) ? 0.5f : 1f; CreateNodeIcon(optionElement.RectTransform, icon ?? order.SymbolSprite, order.Color * colorMultiplier); optionNodes.Add(new Tuple(optionElement, Keys.None)); } optionElements.Add(optionElement); } GUI.PreventElementOverlap(optionElements, clampArea: new Rectangle(10, 10, GameMain.GraphicsWidth - 20, GameMain.GraphicsHeight - 20)); var shadow = new GUIFrame( new RectTransform(targetFrame.Rect.Size + new Point((int)(200 * GUI.Scale)), targetFrame.RectTransform, anchor: Anchor.Center), style: "OuterGlow", color: matchingItems.Count > 1 ? Color.Black * 0.9f : Color.Black * 0.7f); shadow.SetAsFirstChild(); } //only one target (or an order with no particular targets), just show options else { var item = itemContext != null ? (order.UseController ? itemContext.GetConnectedComponents().FirstOrDefault()?.Item ?? itemContext.GetConnectedComponents(recursive: true).FirstOrDefault()?.Item : itemContext) : (matchingItems.Count > 0 ? matchingItems[0] : null); var o = item == null || !order.IsPrefab ? order : new Order(order, item, item.Components.FirstOrDefault(ic => ic.GetType() == order.ItemComponentType)); var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, nodeDistance, GetCircumferencePointCount(order.Options.Length), GetFirstNodeAngle(order.Options.Length)); var offsetIndex = 0; for (int i = 0; i < order.Options.Length; i++) { optionNodes.Add(new Tuple( CreateOrderOptionNode(nodeSize, commandFrame.RectTransform, offsets[offsetIndex++].ToPoint(), o, order.Options[i], order.GetOptionName(i), (i + 1) % 10), Keys.D0 + (i + 1) % 10)); } } } private GUIButton CreateOrderOptionNode(Point size, RectTransform parent, Point offset, Order order, string option, string optionName, int hotkey) { var node = new GUIButton(new RectTransform(size, parent: parent, anchor: Anchor.Center), style: null) { UserData = new Tuple(order, option), OnClicked = (_, userData) => { if (!CanIssueOrders) { return false; } var o = userData as Tuple; SetCharacterOrder(characterContext ?? GetCharacterForQuickAssignment(o.Item1), o.Item1, o.Item2, Character.Controlled); DisableCommandUI(); return true; } }; node.RectTransform.MoveOverTime(offset, CommandNodeAnimDuration); GUIImage icon = null; if (order.Prefab.OptionSprites.TryGetValue(option, out Sprite sprite)) { icon = CreateNodeIcon(node.RectTransform, sprite, order.Color, tooltip: characterContext != null ? optionName : optionName + "\n" + (!PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.leftmouse") : TextManager.Get("input.rightmouse")) + ": " + TextManager.Get("commandui.quickassigntooltip") + "\n" + (!PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.rightmouse") : TextManager.Get("input.leftmouse")) + ": " + TextManager.Get("commandui.manualassigntooltip")); } if (!CanSomeoneHearCharacter()) { node.CanBeFocused = false; if (icon != null) { icon.CanBeFocused = false; } CreateBlockIcon(node.RectTransform); } else if (hotkey >= 0) { CreateHotkeyIcon(node.RectTransform, hotkey); } return node; } private void CreateAssignmentNodes(GUIComponent node) { var order = (node.UserData is Order) ? new Tuple(node.UserData as Order, null) : node.UserData as Tuple; var characters = GetCharactersForManualAssignment(order.Item1); if (characters.None()) { return; } if (!(optionNodes.Find(n => n.Item1 == node) is Tuple optionNode) || !optionNodes.Remove(optionNode)) { shortcutNodes.Remove(node); }; RemoveOptionNodes(); if (returnNode != null) { returnNode.Children.ForEach(child => child.Visible = false); returnNode.Visible = false; historyNodes.Push(returnNode); } SetReturnNode(centerNode, new Point(0, (int)(returnNodeDistanceModifier * nodeDistance))); if (targetFrame == null || !targetFrame.Visible) { SetCenterNode(node as GUIButton); } else { if (string.IsNullOrEmpty(order.Item2)) { SetCenterNode(node as GUIButton, resetAnchor: true); } else { var clickedOptionNode = new GUIButton( new RectTransform(centerNodeSize, parent: commandFrame.RectTransform, anchor: Anchor.Center), style: null) { UserData = node.UserData }; if (order.Item1.Prefab.OptionSprites.TryGetValue(order.Item2, out Sprite sprite)) { CreateNodeIcon(clickedOptionNode.RectTransform, sprite, order.Item1.Color, tooltip: order.Item2); } SetCenterNode(clickedOptionNode); node = null; } targetFrame.Visible = false; } if (shortcutCenterNode != null) { commandFrame.RemoveChild(shortcutCenterNode); shortcutCenterNode = null; } var characterCount = characters.Count; int hotkey = 1; Vector2[] offsets; var needToExpand = characterCount > 10; if (characterCount > 5) { // First ring var charactersOnFirstRing = needToExpand ? 5 : (int)Math.Floor(characterCount / 2f); offsets = GetAssignmentNodeOffsets(charactersOnFirstRing); for (int i = 0; i < charactersOnFirstRing; i++) { CreateAssignmentNode(order, characters[i], offsets[i].ToPoint(), hotkey++ % 10); } // Second ring var charactersOnSecondRing = needToExpand ? 4 : characterCount - charactersOnFirstRing; offsets = GetAssignmentNodeOffsets(needToExpand ? 5 : charactersOnSecondRing, false); for (int i = 0; i < charactersOnSecondRing; i++) { CreateAssignmentNode(order, characters[charactersOnFirstRing + i], offsets[i].ToPoint(), hotkey++ % 10); } } else { offsets = GetAssignmentNodeOffsets(characterCount); for (int i = 0; i < characterCount; i++) { CreateAssignmentNode(order, characters[i], offsets[i].ToPoint(), hotkey++ % 10); } } if (!needToExpand) { hotkey = optionNodes.Count + 1; CreateHotkeyIcon(returnNode.RectTransform, hotkey % 10, true); returnNodeHotkey = Keys.D0 + hotkey % 10; expandNodeHotkey = Keys.None; return; } extraOptionCharacters.Clear(); // Sort expanded assignment nodes by characters' jobs and then by their names extraOptionCharacters.AddRange(characters.GetRange(hotkey - 1, characterCount - (hotkey - 1)) .OrderBy(c => c?.Info?.Job?.Name).ThenBy(c => c?.Info?.DisplayName)); expandNode = new GUIButton( new RectTransform(assignmentNodeSize, parent: commandFrame.RectTransform, anchor: Anchor.Center) { AbsoluteOffset = offsets.Last().ToPoint() }, style: null) { UserData = order, OnClicked = ExpandAssignmentNodes }; CreateNodeIcon(expandNode.RectTransform, "CommandExpandNode", order.Item1.Color, tooltip: TextManager.Get("commandui.expand")); hotkey = optionNodes.Count + 1; CreateHotkeyIcon(expandNode.RectTransform, hotkey % 10); expandNodeHotkey = Keys.D0 + hotkey % 10; CreateHotkeyIcon(returnNode.RectTransform, ++hotkey % 10, true); returnNodeHotkey = Keys.D0 + hotkey % 10; } private Vector2[] GetAssignmentNodeOffsets(int characters, bool firstRing = true) { var nodeDistance = 1.8f * this.nodeDistance; var nodePositionsOnEachSide = characters % 2 > 0 ? 7 : 6; var nodeCountForCalculation = 2 * nodePositionsOnEachSide + 2; var offsets = MathUtils.GetPointsOnCircumference(firstRing ? new Vector2(0f, 0.5f * nodeDistance) : Vector2.Zero, nodeDistance, nodeCountForCalculation, MathHelper.ToRadians(180f + 360f / nodeCountForCalculation)); var emptySpacesPerSide = (nodePositionsOnEachSide - characters) / 2; var offsetsInUse = new Vector2[nodePositionsOnEachSide - 2 * emptySpacesPerSide]; for (int i = 0; i < offsetsInUse.Length; i++) { offsetsInUse[i] = offsets[i + emptySpacesPerSide]; } return offsetsInUse; } private bool ExpandAssignmentNodes(GUIButton node, object userData) { node.OnClicked = (button, _) => { RemoveExtraOptionNodes(); button.OnClicked = ExpandAssignmentNodes; return true; }; var availableNodePositions = 20; var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, 2.7f * this.nodeDistance, availableNodePositions, firstAngle: MathHelper.ToRadians(-90f - ((extraOptionCharacters.Count - 1) * 0.5f * (360f / availableNodePositions)))); for (int i = 0; i < extraOptionCharacters.Count; i++) { CreateAssignmentNode(userData as Tuple, extraOptionCharacters[i], offsets[i].ToPoint(), -1, nameLabelScale: 1.15f); } return true; } private void CreateAssignmentNode(Tuple order, Character character, Point offset, int hotkey, float nameLabelScale = 1f) { // Button var node = new GUIButton( new RectTransform(assignmentNodeSize, parent: commandFrame.RectTransform, anchor: Anchor.Center), style: null) { UserData = character, OnClicked = (_, userData) => { if (!CanIssueOrders) { return false; } SetCharacterOrder(userData as Character, order.Item1, order.Item2, Character.Controlled); DisableCommandUI(); return true; } }; node.RectTransform.MoveOverTime(offset, CommandNodeAnimDuration); var jobColor = character.Info?.Job?.Prefab?.UIColor ?? Color.White; // Order icon GUIImage orderIcon; if (character.CurrentOrder != null && !character.CurrentOrder.Identifier.Equals("dismissed")) { orderIcon = new GUIImage(new RectTransform(new Vector2(1.2f), node.RectTransform, anchor: Anchor.Center), character.CurrentOrder.SymbolSprite, scaleToFit: true); var tooltip = character.CurrentOrder.Name; if (!string.IsNullOrWhiteSpace(character.CurrentOrderOption)) { tooltip += " (" + character.CurrentOrder.GetOptionName(character.CurrentOrderOption) + ")"; }; orderIcon.ToolTip = tooltip; } else { // TODO: Replace with an icon that symbols the characters dismissed state and their availability to new orders OR localize the text orderIcon = new GUIImage(new RectTransform(new Vector2(1.2f), node.RectTransform, anchor: Anchor.Center), "CommandNodeContainer", scaleToFit: true); var label = new GUITextBlock(new RectTransform(new Vector2(0.9f / 1.2f), orderIcon.RectTransform, anchor: Anchor.Center), "FREE", textColor: jobColor * nodeColorMultiplier, font: GUI.SubHeadingFont, textAlignment: Alignment.Center, style: null) { CanBeFocused = false, ForceUpperCase = true, HoverTextColor = jobColor }; } orderIcon.Color = jobColor * nodeColorMultiplier; orderIcon.HoverColor = jobColor; orderIcon.PressedColor = jobColor; orderIcon.SelectedColor = jobColor; orderIcon.UserData = "colorsource"; // Name label var width = (int)(nameLabelScale * nodeSize.X); var font = GUI.SmallFont; var nameLabel = new GUITextBlock( new RectTransform(new Point(width, 0), parent: node.RectTransform, anchor: Anchor.TopCenter, pivot: Pivot.BottomCenter) { RelativeOffset = new Vector2(0f, -0.1f) }, ToolBox.LimitString(character.Info?.DisplayName, font, width), textColor: jobColor * nodeColorMultiplier, font: font, textAlignment: Alignment.Center, style: null) { CanBeFocused = false, ForceUpperCase = true, HoverTextColor = jobColor }; // Job icon GUIImage jobIcon = null; if (character?.Info?.Job?.Prefab?.Icon is Sprite sprite) { jobIcon = new GUIImage(new RectTransform(new Vector2(1.2f), node.RectTransform, anchor: Anchor.Center), "CommandNodeContainer", scaleToFit: true) { CanBeFocused = false, Color = jobColor * nodeColorMultiplier, HoverColor = jobColor, PressedColor = jobColor, SelectedColor = jobColor, Visible = false }; ; new GUIImage(new RectTransform(new Vector2(0.9f / 1.2f), jobIcon.RectTransform, anchor: Anchor.Center), sprite, scaleToFit: true) { CanBeFocused = false, Color = jobColor * nodeColorMultiplier, HoverColor = jobColor, PressedColor = jobColor, SelectedColor = jobColor }; } assignmentNodeIcons.Add(new AssignmentNodeIconSet(orderIcon, jobIcon)); #if DEBUG bool canHear = true; #else bool canHear = character.CanHearCharacter(Character.Controlled); #endif if (!canHear) { node.CanBeFocused = orderIcon.CanBeFocused = false; CreateBlockIcon(node.RectTransform); } if (hotkey >= 0) { if (canHear) { CreateHotkeyIcon(node.RectTransform, hotkey); } optionNodes.Add(new Tuple(node, canHear ? Keys.D0 + hotkey : Keys.None)); } else { extraOptionNodes.Add(node); } } private GUIImage CreateNodeIcon(RectTransform parent, Sprite sprite, Color color, string tooltip = null) { // Icon return new GUIImage( new RectTransform(Vector2.One, parent), sprite, scaleToFit: true) { Color = color * nodeColorMultiplier, HoverColor = color, PressedColor = color, SelectedColor = color, ToolTip = tooltip, UserData = "colorsource" }; } private void CreateNodeIcon(RectTransform parent, string style, Color? color = null, string tooltip = null) { // Icon var icon = new GUIImage( new RectTransform(Vector2.One, parent), style, scaleToFit: true) { ToolTip = tooltip, UserData = "colorsource" }; if (color.HasValue) { icon.Color = color.Value * nodeColorMultiplier; icon.HoverColor = color.Value; } else { icon.Color = icon.HoverColor * nodeColorMultiplier; } } private void CreateHotkeyIcon(RectTransform parent, int hotkey, bool enlargeIcon = false) { var bg = new GUIImage( new RectTransform(new Vector2(enlargeIcon ? 0.4f : 0.25f), parent, anchor: Anchor.BottomCenter, pivot: Pivot.Center), "CommandHotkeyContainer", scaleToFit: true) { CanBeFocused = false, UserData = "hotkey" }; new GUITextBlock( new RectTransform(Vector2.One, bg.RectTransform, anchor: Anchor.Center), hotkey.ToString(), textColor: Color.Black, textAlignment: Alignment.Center) { CanBeFocused = false }; } private void CreateBlockIcon(RectTransform parent) { new GUIImage(new RectTransform(new Vector2(0.9f), parent, anchor: Anchor.Center), cancelIcon, scaleToFit: true) { CanBeFocused = false, Color = GUI.Style.Red * nodeColorMultiplier, HoverColor = GUI.Style.Red }; } private int GetCircumferencePointCount(int nodes) { return nodes % 2 > 0 ? nodes : nodes + 1; } private float GetFirstNodeAngle(int nodeCount) { var bearing = 90.0f; if (returnNode != null) { bearing = GetBearing( centerNode.RectTransform.AnimTargetPos.ToVector2(), returnNode.RectTransform.AnimTargetPos.ToVector2()); } else if (shortcutCenterNode != null) { bearing = GetBearing( centerNode.RectTransform.AnimTargetPos.ToVector2(), shorcutCenterNodeOffset.ToVector2()); } return nodeCount % 2 > 0 ? MathHelper.ToRadians(bearing + 360.0f / nodeCount / 2) : MathHelper.ToRadians(bearing + 360.0f / (nodeCount + 1)); } private float GetBearing(Vector2 startPoint, Vector2 endPoint, bool flipY = false, bool flipX = false) { var radians = Math.Atan2( !flipY ? endPoint.Y - startPoint.Y : startPoint.Y - endPoint.Y, !flipX ? endPoint.X - startPoint.X : startPoint.X - endPoint.X); var degrees = MathHelper.ToDegrees((float)radians); return (degrees < 0) ? (degrees + 360) : degrees; } private bool TryGetBreachedHullAtHoveredWall(out Hull breachedHull) { breachedHull = null; // Based on the IsValidTarget() method of AIObjectiveFixLeaks class List leaks = Gap.GapList.FindAll(g => g != null && g.ConnectedWall != null && g.ConnectedDoor == null && g.Open > 0 && g.linkedTo.Any(l => l != null) && g.Submarine != null && (Character.Controlled != null && g.Submarine.TeamID == Character.Controlled.TeamID && g.Submarine.Info.IsPlayer)); if (leaks.None()) { return false; } Vector2 mouseWorldPosition = GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition); foreach (Gap leak in leaks) { if (Submarine.RectContains(leak.ConnectedWall.WorldRect, mouseWorldPosition)) { breachedHull = leak.FlowTargetHull; return true; } } return false; } private Submarine GetTargetSubmarine() { var sub = Submarine.MainSub; if (Character.Controlled != null) { // Pick the second main sub when we have two teams (in combat mission) if (Character.Controlled.TeamID == Character.TeamType.Team2 && Submarine.MainSubs.Length > 1) { sub = Submarine.MainSubs[1]; } // Target current submarine (likely a shuttle) when undocked from the main sub if (Character.Controlled.Submarine is Submarine currentSub && currentSub != sub && currentSub.TeamID == Character.Controlled.TeamID && !currentSub.IsConnectedTo(sub)) { sub = currentSub; } } return sub; } #region Crew Member Assignment Logic private Character GetCharacterForQuickAssignment(Order order) { #if !DEBUG if (Character.Controlled == null) { return null; } #endif if (order.Category == OrderCategory.Operate && HumanAIController.IsItemOperatedByAnother(null, order.TargetItemComponent, out Character operatingCharacter)) { return operatingCharacter; } return GetCharactersSortedForOrder(order, false).FirstOrDefault() ?? Character.Controlled; } private List GetCharactersForManualAssignment(Order order) { #if !DEBUG if (Character.Controlled == null) { return new List(); } #endif if (order.Identifier == dismissedOrderPrefab.Identifier) { return characters.FindAll(c => c.CurrentOrder != null && c.CurrentOrder.Identifier != dismissedOrderPrefab.Identifier) .OrderBy(c => c.Info.DisplayName).ToList(); } return GetCharactersSortedForOrder(order, order.Identifier != "follow").ToList(); } private IEnumerable GetCharactersSortedForOrder(Order order, bool includeSelf) { return characters.FindAll(c => Character.Controlled == null || ((includeSelf || c != Character.Controlled) && c.TeamID == Character.Controlled.TeamID)) .OrderByDescending(c => c.CurrentOrder != null && order.Category == OrderCategory.Operate && c.CurrentOrder.Identifier == order.Identifier && c.CurrentOrder.TargetEntity == order.TargetEntity) .ThenByDescending(c => c.CurrentOrder == null || c.CurrentOrder.Identifier == dismissedOrderPrefab.Identifier) .ThenBy(c => c.CurrentOrder != null && c.CurrentOrder.Identifier == order.Identifier && c.CurrentOrder.TargetEntity == order.TargetEntity) .ThenByDescending(c => order.HasAppropriateJob(c)) .ThenBy(c => c.CurrentOrder?.Weight) .ThenByDescending(c => c.GetSkillLevel(order.AppropriateSkill)); } #endregion #endregion #region Reports /// /// Enables/disables report buttons when needed /// public void UpdateReports() { bool canIssueOrders = false; if (Character.Controlled?.CurrentHull?.Submarine != null && Character.Controlled.SpeechImpediment < 100.0f) { WifiComponent radio = GetHeadset(Character.Controlled, true); canIssueOrders = radio != null && radio.CanTransmit() && Character.Controlled?.CurrentHull?.Submarine?.TeamID == Character.Controlled.TeamID && !Character.Controlled.CurrentHull.Submarine.Info.IsWreck; } if (canIssueOrders) { ReportButtonFrame.Visible = !Character.Controlled.ShouldLockHud(); if (!ReportButtonFrame.Visible) { return; } var reportButtonParent = ChatBox ?? GameMain.Client?.ChatBox; if (reportButtonParent == null) { return; } ReportButtonFrame.RectTransform.AbsoluteOffset = new Point(reportButtonParent.GUIFrame.Rect.Right + (int)(10 * GUI.Scale), reportButtonParent.GUIFrame.Rect.Y); bool hasFires = Character.Controlled.CurrentHull.FireSources.Count > 0; ToggleReportButton("reportfire", hasFires); bool hasLeaks = Character.Controlled.CurrentHull.ConnectedGaps.Any(g => !g.IsRoomToRoom && g.Open > 0.0f); ToggleReportButton("reportbreach", hasLeaks); bool hasIntruders = Character.CharacterList.Any(c => c.CurrentHull == Character.Controlled.CurrentHull && AIObjectiveFightIntruders.IsValidTarget(c, Character.Controlled)); ToggleReportButton("reportintruders", hasIntruders); foreach (GUIComponent reportButton in ReportButtonFrame.Children) { var highlight = reportButton.GetChildByUserData("highlighted"); if (highlight.Visible) { highlight.RectTransform.LocalScale = new Vector2(1.25f + (float)Math.Sin(Timing.TotalTime * 5.0f) * 0.25f); } } } else { ReportButtonFrame.Visible = false; } } private void ToggleReportButton(string orderIdentifier, bool enabled) { Order order = Order.GetPrefab(orderIdentifier); var reportButton = ReportButtonFrame.GetChildByUserData(order); if (reportButton != null) { reportButton.GetChildByUserData("highlighted").Visible = enabled; } } #endregion public void InitSinglePlayerRound() { crewList.ClearChildren(); characters.Clear(); WayPoint[] waypoints = WayPoint.SelectCrewSpawnPoints(characterInfos, Submarine.MainSub); for (int i = 0; i < waypoints.Length; i++) { Character character; character = Character.Create(characterInfos[i], waypoints[i].WorldPosition, characterInfos[i].Name); if (character.Info != null) { if (!character.Info.StartItemsGiven && character.Info.InventoryData != null) { DebugConsole.ThrowError($"Error when initializing a single player round: character \"{character.Name}\" has not been given their initial items but has saved inventory data. Using the saved inventory data instead of giving the character new items."); } if (character.Info.InventoryData != null) { character.Info.SpawnInventoryItems(character.Inventory, character.Info.InventoryData); } else if (!character.Info.StartItemsGiven) { character.GiveJobItems(waypoints[i]); } character.Info.StartItemsGiven = true; } AddCharacter(character); if (i == 0) { Character.Controlled = character; } } conversationTimer = Rand.Range(5.0f, 10.0f); } public void EndRound() { //remove characterinfos whose characters have been removed or killed characterInfos.RemoveAll(c => c.Character == null || c.Character.Removed || c.CauseOfDeath != null); characters.Clear(); crewList.ClearChildren(); } public void Reset() { characters.Clear(); characterInfos.Clear(); crewList.ClearChildren(); } public void Save(XElement parentElement) { XElement element = new XElement("crew"); foreach (CharacterInfo ci in characterInfos) { var infoElement = ci.Save(element); if (ci.InventoryData != null) { infoElement.Add(ci.InventoryData); } } parentElement.Add(element); } } }