using Barotrauma.Networking; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Globalization; using System.Linq; namespace Barotrauma { class RoundSummary { private float crewListAnimDelay = 0.25f; private float missionIconAnimDelay; private const float JobColumnWidthPercentage = 0.05f; private const float CharacterColumnWidthPercentage = 0.35f; private const float StatusColumnWidthPercentage = 0.25f; private const float KillColumnWidthPercentage = 0.05f; private const float DeathColumnWidthPercentage = 0.05f; private int jobColumnWidth, characterColumnWidth, statusColumnWidth, killColumnWidth, deathColumnWidth; private readonly List selectedMissions; private readonly Location startLocation, endLocation; private readonly GameMode gameMode; private readonly Dictionary initialFactionReputations = new Dictionary(); public GUILayoutGroup ButtonArea { get; private set; } public GUIButton ContinueButton { get; private set; } public GUIComponent Frame { get; private set; } public RoundSummary(GameMode gameMode, IEnumerable selectedMissions, Location startLocation, Location endLocation) { this.gameMode = gameMode; this.selectedMissions = selectedMissions.ToList(); this.startLocation = startLocation; this.endLocation = endLocation; if (gameMode is CampaignMode campaignMode) { foreach (Faction faction in campaignMode.Factions) { initialFactionReputations.Add(faction.Prefab.Identifier, faction.Reputation.Value); } } } public GUIFrame CreateSummaryFrame(GameSession gameSession, string endMessage, CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None, TraitorManager.TraitorResults? traitorResults = null) { bool singleplayer = GameMain.NetworkMember == null; bool gameOver = gameSession.GameMode.IsSinglePlayer ? gameSession.CrewManager.GetCharacters().All(c => c.IsDead || c.IsIncapacitated) : gameSession.CrewManager.GetCharacters().All(c => c.IsDead || c.IsIncapacitated || c.IsBot); if (!singleplayer) { SoundPlayer.OverrideMusicType = (gameOver ? "crewdead" : "endround").ToIdentifier(); SoundPlayer.OverrideMusicDuration = 18.0f; } GUIFrame background = new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, GUI.Canvas, Anchor.Center), style: "GUIBackgroundBlocker") { UserData = this }; List rightPanels = new List(); int minWidth = 400, minHeight = 350; int padding = GUI.IntScale(25.0f); //crew panel ------------------------------------------------------------------------------- GUIFrame crewFrame = new GUIFrame(new RectTransform(new Vector2(0.35f, 0.4f), background.RectTransform, Anchor.TopCenter, minSize: new Point(minWidth, minHeight))); GUIFrame crewFrameInner = new GUIFrame(new RectTransform(new Point(crewFrame.Rect.Width - padding * 2, crewFrame.Rect.Height - padding * 2), crewFrame.RectTransform, Anchor.Center), style: "InnerFrame"); var crewContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), crewFrameInner.RectTransform, Anchor.Center)) { Stretch = true }; var crewHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), crewContent.RectTransform), TextManager.Get("crew"), textAlignment: Alignment.TopLeft, font: GUIStyle.SubHeadingFont); crewHeader.RectTransform.MinSize = new Point(0, GUI.IntScale(crewHeader.Rect.Height * 2.0f)); var crewList = CreateCrewList(crewContent, gameSession.CrewManager.GetCharacterInfos().Where(c => c.TeamID != CharacterTeamType.Team2), traitorResults); if (traitorResults != null && traitorResults.Value.VotedAsTraitorClientSessionId > 0) { var traitorInfoPanel = CreateTraitorInfoPanel(crewList.Content, traitorResults.Value, crewListAnimDelay); traitorInfoPanel.RectTransform.SetAsFirstChild(); var spacing = new GUIFrame(new RectTransform(new Point(0, GUI.IntScale(20)), crewList.Content.RectTransform), style: null); spacing.RectTransform.RepositionChildInHierarchy(1); } //another crew frame for the 2nd team in combat missions if (gameSession.Missions.Any(m => m is CombatMission)) { crewHeader.Text = CombatMission.GetTeamName(CharacterTeamType.Team1); GUIFrame crewFrame2 = new GUIFrame(new RectTransform(crewFrame.RectTransform.RelativeSize, background.RectTransform, Anchor.TopCenter, minSize: new Point(minWidth, minHeight))); rightPanels.Add(crewFrame2); GUIFrame crewFrameInner2 = new GUIFrame(new RectTransform(new Point(crewFrame2.Rect.Width - padding * 2, crewFrame2.Rect.Height - padding * 2), crewFrame2.RectTransform, Anchor.Center), style: "InnerFrame"); var crewContent2 = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), crewFrameInner2.RectTransform, Anchor.Center)) { Stretch = true }; var crewHeader2 = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), crewContent2.RectTransform), CombatMission.GetTeamName(CharacterTeamType.Team2), textAlignment: Alignment.TopLeft, font: GUIStyle.SubHeadingFont); crewHeader2.RectTransform.MinSize = new Point(0, GUI.IntScale(crewHeader2.Rect.Height * 2.0f)); CreateCrewList(crewContent2, gameSession.CrewManager.GetCharacterInfos().Where(c => c.TeamID == CharacterTeamType.Team2), traitorResults); if (CombatMission.Winner != CharacterTeamType.None) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), crewHeader.RectTransform), TextManager.Get(CombatMission.Winner == CharacterTeamType.Team1 ? "pvpmode.victory" : "pvpmode.defeat"), textAlignment: Alignment.TopRight, font: GUIStyle.SubHeadingFont, textColor: CombatMission.Winner == CharacterTeamType.Team1 ? GUIStyle.Green : GUIStyle.Red); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), crewHeader2.RectTransform), TextManager.Get(CombatMission.Winner == CharacterTeamType.Team2 ? "pvpmode.victory" : "pvpmode.defeat"), textAlignment: Alignment.TopRight, font: GUIStyle.SubHeadingFont, textColor: CombatMission.Winner == CharacterTeamType.Team2 ? GUIStyle.Green : GUIStyle.Red); } } //header ------------------------------------------------------------------------------- LocalizedString headerText = GetHeaderText(gameOver, transitionType); GUITextBlock headerTextBlock = null; if (!headerText.IsNullOrEmpty()) { headerTextBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), crewFrame.RectTransform, Anchor.TopLeft, Pivot.BottomLeft), headerText, textAlignment: Alignment.BottomLeft, font: GUIStyle.LargeFont, wrap: true); } //reputation panel ------------------------------------------------------------------------------- var campaignMode = gameMode as CampaignMode; if (campaignMode != null) { GUIFrame reputationframe = new GUIFrame(new RectTransform(crewFrame.RectTransform.RelativeSize, background.RectTransform, Anchor.TopCenter, minSize: crewFrame.RectTransform.MinSize)); rightPanels.Add(reputationframe); GUIFrame reputationframeInner = new GUIFrame(new RectTransform(new Point(reputationframe.Rect.Width - padding * 2, reputationframe.Rect.Height - padding * 2), reputationframe.RectTransform, Anchor.Center), style: "InnerFrame"); var reputationContent = new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 0.95f), reputationframeInner.RectTransform, Anchor.Center)) { Stretch = true }; var reputationHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), reputationContent.RectTransform), TextManager.Get("reputation"), textAlignment: Alignment.TopLeft, font: GUIStyle.SubHeadingFont); reputationHeader.RectTransform.MinSize = new Point(0, GUI.IntScale(reputationHeader.Rect.Height * 2.0f)); CreateReputationInfoPanel(reputationContent, campaignMode); } //mission panel ------------------------------------------------------------------------------- GUIFrame missionframe = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.4f), background.RectTransform, Anchor.TopCenter, minSize: new Point(minWidth, minHeight / 4))); GUILayoutGroup missionFrameContent = new GUILayoutGroup(new RectTransform(new Point(missionframe.Rect.Width - padding * 2, missionframe.Rect.Height - padding * 2), missionframe.RectTransform, Anchor.Center)) { Stretch = true, RelativeSpacing = 0.03f }; GUIFrame missionframeInner = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.9f), missionFrameContent.RectTransform, Anchor.Center), style: "InnerFrame"); var missionContent = new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 0.93f), missionframeInner.RectTransform, Anchor.Center)) { Stretch = true }; List missionsToDisplay = new List(selectedMissions.Where(m => m.Prefab.ShowInMenus)); if (startLocation != null) { foreach (Mission mission in startLocation.SelectedMissions) { if (missionsToDisplay.Contains(mission)) { continue; } if (!mission.Prefab.ShowInMenus) { continue; } if (mission.Locations[0] == mission.Locations[1] || mission.Locations.Contains(campaignMode?.Map.SelectedLocation)) { missionsToDisplay.Add(mission); } } } GUIListBox missionList = new GUIListBox(new RectTransform(Vector2.One, missionContent.RectTransform, Anchor.Center)) { Spacing = GUI.IntScale(15) }; missionList.ContentBackground.Color = Color.Transparent; ButtonArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), missionFrameContent.RectTransform, Anchor.BottomCenter), isHorizontal: true, childAnchor: Anchor.BottomRight) { RelativeSpacing = 0.025f }; missionFrameContent.Recalculate(); missionContent.Recalculate(); if (!string.IsNullOrWhiteSpace(endMessage)) { var endText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionList.Content.RectTransform), TextManager.GetServerMessage(endMessage), wrap: true) { CanBeFocused = false }; } float animDelay = missionIconAnimDelay; foreach (Mission mission in missionsToDisplay) { var textContent = new List(); if (selectedMissions.Contains(mission)) { textContent.Add(mission.Completed ? mission.SuccessMessage : mission.FailureMessage); var repText = mission.GetReputationRewardText(); if (!repText.IsNullOrEmpty()) { textContent.Add(repText); } int totalReward = mission.GetFinalReward(Submarine.MainSub); if (totalReward > 0) { textContent.Add(mission.GetMissionRewardText(Submarine.MainSub)); if (GameMain.IsMultiplayer && Character.Controlled is { } controlled && mission.Completed) { var (share, percentage, _) = Mission.GetRewardShare(controlled.Wallet.RewardDistribution, GameSession.GetSessionCrewCharacters(CharacterType.Player).Where(c => c != controlled), Option.Some(totalReward)); if (share > 0) { string shareFormatted = string.Format(CultureInfo.InvariantCulture, "{0:N0}", share); RichString yourShareString = TextManager.GetWithVariables("crewwallet.missionreward.get", ("[money]", $"{shareFormatted}"), ("[share]", $"{percentage}")); textContent.Add(yourShareString); } } } } else { var repText = mission.GetReputationRewardText(); if (!repText.IsNullOrEmpty()) { textContent.Add(repText); } textContent.Add(mission.GetMissionRewardText(Submarine.MainSub)); textContent.Add(mission.Description); textContent.AddRange(mission.ShownMessages); } CreateMissionEntry( missionList.Content, mission.Name, textContent, mission.Difficulty ?? 0, mission.Prefab.Icon, mission.Prefab.IconColor, mission.GetDifficultyToolTipText(), out GUIImage missionIcon); if (selectedMissions.Contains(mission)) { UpdateMissionStateIcon(mission is CombatMission combatMission ? CombatMission.IsInWinningTeam(GameMain.Client?.Character) : mission.Completed, missionIcon, animDelay); animDelay += 0.25f; } } if (!missionsToDisplay.Any()) { var missionContentHorizontal = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.4f), missionList.Content.RectTransform), childAnchor: Anchor.TopLeft, isHorizontal: true) { RelativeSpacing = 0.025f, Stretch = true, CanBeFocused = true }; GUIImage missionIcon = new GUIImage(new RectTransform(new Point(missionContentHorizontal.Rect.Height), missionContentHorizontal.RectTransform), style: "NoMissionIcon", scaleToFit: true); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionContentHorizontal.RectTransform), TextManager.Get("nomission"), font: GUIStyle.LargeFont); } gameSession?.EventManager?.EventLog?.CreateEventLogUI(missionList.Content, traitorResults); AddSeparators(missionList.Content); ContinueButton = new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), ButtonArea.RectTransform), TextManager.Get("Close")); ButtonArea.RectTransform.NonScaledSize = new Point(ButtonArea.Rect.Width, ContinueButton.Rect.Height); ButtonArea.RectTransform.IsFixedSize = true; missionFrameContent.Recalculate(); // set layout ------------------------------------------------------------------- int panelSpacing = GUI.IntScale(20); int totalHeight = crewFrame.Rect.Height + panelSpacing + missionframe.Rect.Height; int totalWidth = crewFrame.Rect.Width; crewFrame.RectTransform.AbsoluteOffset = new Point(0, (GameMain.GraphicsHeight - totalHeight) / 2); missionframe.RectTransform.AbsoluteOffset = new Point(0, crewFrame.Rect.Bottom + panelSpacing); if (rightPanels.Any()) { totalWidth = crewFrame.Rect.Width * 2 + panelSpacing; if (headerTextBlock != null) { headerTextBlock.RectTransform.MinSize = new Point(totalWidth, 0); } crewFrame.RectTransform.AbsoluteOffset = new Point(-(crewFrame.Rect.Width + panelSpacing) / 2, crewFrame.RectTransform.AbsoluteOffset.Y); foreach (var rightPanel in rightPanels) { rightPanel.RectTransform.AbsoluteOffset = new Point((rightPanel.Rect.Width + panelSpacing) / 2, crewFrame.RectTransform.AbsoluteOffset.Y); } } Frame = background; return background; } public void CreateReputationInfoPanel(GUIComponent parent, CampaignMode campaignMode) { GUIListBox reputationList = new GUIListBox(new RectTransform(Vector2.One, parent.RectTransform)); reputationList.ContentBackground.Color = Color.Transparent; foreach (Faction faction in campaignMode.Factions.OrderBy(f => f.Prefab.MenuOrder).ThenBy(f => f.Prefab.Name)) { float initialReputation = faction.Reputation.Value; if (!initialFactionReputations.TryGetValue(faction.Prefab.Identifier, out initialReputation)) { DebugConsole.AddWarning($"Could not determine reputation change for faction \"{faction.Prefab.Name}\" (faction was not present at the start of the round)."); } var factionFrame = CreateReputationElement( reputationList.Content, faction.Prefab.Name, faction.Reputation, initialReputation, faction.Prefab.ShortDescription, faction.Prefab.Description, faction.Prefab.Icon, faction.Prefab.BackgroundPortrait, faction.Prefab.IconColor); CreatePathUnlockElement(factionFrame, faction, null); } float maxDescriptionHeight = 0.0f; foreach (GUIComponent child in reputationList.Content.Children) { var descriptionElement = child.FindChild("description", recursive: true) as GUITextBlock; float descriptionHeight = descriptionElement.TextSize.Y * 1.1f; if (child.FindChild("unlockinfo") is GUIComponent unlockInfoComponent) { descriptionHeight += 1.25f * unlockInfoComponent.Rect.Height; } maxDescriptionHeight = Math.Max(maxDescriptionHeight, descriptionHeight); } foreach (GUIComponent child in reputationList.Content.Children) { var headerElement = child.FindChild("header", recursive: true) as GUITextBlock; var descriptionElement = child.FindChild("description", recursive: true) as GUITextBlock; descriptionElement.RectTransform.NonScaledSize = new Point(descriptionElement.Rect.Width, (int)maxDescriptionHeight); descriptionElement.RectTransform.IsFixedSize = true; child.RectTransform.NonScaledSize = new Point(child.Rect.Width, headerElement.Rect.Height + descriptionElement.RectTransform.Parent.Children.Sum(c => c.Rect.Height + ((GUILayoutGroup)descriptionElement.Parent).AbsoluteSpacing)); } void CreatePathUnlockElement(GUIComponent reputationFrame, Faction faction, Location location) { if (GameMain.GameSession?.Campaign?.Map == null) { return; } IEnumerable connectionsBetweenBiomes = GameMain.GameSession.Campaign.Map.Connections.Where(c => c.Locations[0].Biome != c.Locations[1].Biome); foreach (LocationConnection connection in connectionsBetweenBiomes) { if (!connection.Locked || (!connection.Locations[0].Discovered && !connection.Locations[1].Discovered)) { continue; } //don't show the "reputation required to unlock" text if another connection between the biomes has already been unlocked if (connectionsBetweenBiomes.Where(c => !c.Locked).Any(c => (c.Locations[0].Biome == connection.Locations[0].Biome && c.Locations[1].Biome == connection.Locations[1].Biome) || (c.Locations[1].Biome == connection.Locations[0].Biome && c.Locations[0].Biome == connection.Locations[1].Biome))) { continue; } var gateLocation = connection.Locations[0].IsGateBetweenBiomes ? connection.Locations[0] : connection.Locations[1]; var unlockEvent = EventPrefab.GetUnlockPathEvent(gateLocation.LevelData.Biome.Identifier, gateLocation.Faction); if (unlockEvent == null) { continue; } if (unlockEvent.Faction.IsEmpty) { if (location == null || gateLocation != location) { continue; } } else { if (faction == null || faction.Prefab.Identifier != unlockEvent.Faction) { continue; } } if (unlockEvent != null) { Reputation unlockReputation = gateLocation.Reputation; Faction unlockFaction = null; if (!unlockEvent.Faction.IsEmpty) { unlockFaction = GameMain.GameSession.Campaign.Factions.Find(f => f.Prefab.Identifier == unlockEvent.Faction); unlockReputation = unlockFaction?.Reputation; } float normalizedUnlockReputation = MathUtils.InverseLerp(unlockReputation.MinReputation, unlockReputation.MaxReputation, unlockEvent.UnlockPathReputation); RichString unlockText = RichString.Rich(TextManager.GetWithVariables( "lockedpathreputationrequirement", ("[reputation]", Reputation.GetFormattedReputationText(normalizedUnlockReputation, unlockEvent.UnlockPathReputation, addColorTags: true)), ("[biomename]", $"‖color:gui.orange‖{connection.LevelData.Biome.DisplayName}‖end‖"))); var unlockInfoPanel = new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.0f), reputationFrame.RectTransform, Anchor.BottomCenter) { MinSize = new Point(0, GUI.IntScale(30)), AbsoluteOffset = new Point(0, GUI.IntScale(3)) }, unlockText, style: "GUIButtonRound", textAlignment: Alignment.Center, textColor: GUIStyle.TextColorNormal); unlockInfoPanel.Color = Color.Lerp(unlockInfoPanel.Color, Color.Black, 0.8f); unlockInfoPanel.UserData = "unlockinfo"; if (unlockInfoPanel.TextSize.X > unlockInfoPanel.Rect.Width * 0.7f) { unlockInfoPanel.Font = GUIStyle.SmallFont; } } } } } private static GUIComponent CreateTraitorInfoPanel(GUIComponent parent, TraitorManager.TraitorResults traitorResults, float iconAnimDelay) { var traitorCharacter = traitorResults.GetTraitorClient()?.Character; string resultTag = traitorResults.VotedCorrectTraitor ? traitorResults.ObjectiveSuccessful ? "traitor.blameresult.correct.objectivesuccessful" : "traitor.blameresult.correct.objectivefailed" : "traitor.blameresult.failure"; var textContent = new List() { TextManager.GetWithVariable("traitor.blameresult", "[name]", traitorCharacter?.Name ?? "unknown"), TextManager.Get(resultTag) }; if (traitorResults.MoneyPenalty > 0) { textContent.Add( TextManager.GetWithVariable( "traitor.blameresult.failure.penalty", "[money]", TextManager.FormatCurrency(traitorResults.MoneyPenalty, includeCurrencySymbol: false))); } var icon = GUIStyle.GetComponentStyle("TraitorMissionIcon")?.GetDefaultSprite(); var content = CreateMissionEntry( parent, string.Empty, textContent, difficultyIconCount: 0, icon, GUIStyle.Red, difficultyTooltipText: null, out GUIImage missionIcon); UpdateMissionStateIcon(traitorResults.VotedCorrectTraitor, missionIcon, iconAnimDelay); return content; } public static GUIComponent CreateMissionEntry(GUIComponent parent, LocalizedString header, List textContent, int difficultyIconCount, Sprite icon, Color iconColor, RichString difficultyTooltipText, out GUIImage missionIcon) { int spacing = GUI.IntScale(5); int defaultLineHeight = (int)GUIStyle.Font.MeasureChar('T').Y; //make the icon big enough for header + some lines of text int iconSize = (int)(GUIStyle.SubHeadingFont.MeasureChar('T').Y + defaultLineHeight * 6); GUILayoutGroup content = new GUILayoutGroup(new RectTransform(new Point(parent.Rect.Width, iconSize), parent.RectTransform), isHorizontal: true) { Stretch = true, AbsoluteSpacing = spacing, CanBeFocused = true }; if (icon != null) { missionIcon = new GUIImage(new RectTransform(new Point(iconSize), content.RectTransform), icon, null, true) { Color = iconColor, HoverColor = iconColor, SelectedColor = iconColor, CanBeFocused = false }; missionIcon.RectTransform.IsFixedSize = true; } else { missionIcon = null; } GUILayoutGroup missionTextGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.744f, 0f), content.RectTransform, Anchor.CenterLeft), childAnchor: Anchor.TopLeft) { AbsoluteSpacing = spacing }; content.Recalculate(); RichString missionNameString = RichString.Rich(header); List contentStrings = new List(textContent.Select(t => RichString.Rich(t))); if (!header.IsNullOrEmpty()) { var nameText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionNameString, font: GUIStyle.SubHeadingFont, wrap: true); nameText.RectTransform.MinSize = new Point(0, (int)nameText.TextSize.Y); } GUILayoutGroup difficultyIndicatorGroup = null; if (difficultyIconCount > 0) { difficultyIndicatorGroup = new GUILayoutGroup(new RectTransform(new Point(missionTextGroup.Rect.Width, defaultLineHeight), parent: missionTextGroup.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { AbsoluteSpacing = 1, CanBeFocused = true }; difficultyIndicatorGroup.RectTransform.MinSize = new Point(0, defaultLineHeight); var difficultyColor = Mission.GetDifficultyColor(difficultyIconCount); for (int i = 0; i < difficultyIconCount; i++) { new GUIImage(new RectTransform(Vector2.One, difficultyIndicatorGroup.RectTransform, scaleBasis: ScaleBasis.Smallest), "DifficultyIndicator", scaleToFit: true) { Color = difficultyColor, ToolTip = difficultyTooltipText }; } } GUITextBlock firstContentText = null; foreach (var contentString in contentStrings) { var text = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), contentString, wrap: true); text.RectTransform.MinSize = new Point(0, (int)text.TextSize.Y); firstContentText ??= text; } if (difficultyIndicatorGroup != null && firstContentText != null) { //make the icons align with the text content difficultyIndicatorGroup.RectTransform.AbsoluteOffset = new Point((int)firstContentText.Padding.X, 0); } missionTextGroup.RectTransform.MinSize = new Point(0, missionTextGroup.Children.Sum(c => c.Rect.Height + missionTextGroup.AbsoluteSpacing) - missionTextGroup.AbsoluteSpacing); missionTextGroup.Recalculate(); content.RectTransform.MinSize = new Point(0, Math.Max(missionTextGroup.Rect.Height, iconSize)); return content; } public static void AddSeparators(GUIComponent container) { var children = container.Children.ToList(); if (children.Count < 2) { return; } var lastChild = children.Last(); foreach (var child in children) { if (child != lastChild) { var separator = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.1f), container.RectTransform), style: "HorizontalLine"); separator.RectTransform.RepositionChildInHierarchy(container.GetChildIndex(child) + 1); } } } public static void UpdateMissionStateIcon(bool success, GUIImage missionIcon, float delay = 0.5f) { if (missionIcon == null) { return; } string style = success ? "MissionCompletedIcon" : "MissionFailedIcon"; GUIImage stateIcon = missionIcon.GetChild(); if (string.IsNullOrEmpty(style)) { if (stateIcon != null) { stateIcon.Visible = false; } } else { bool wasVisible = stateIcon is { Visible: true }; stateIcon ??= new GUIImage(new RectTransform(Vector2.One, missionIcon.RectTransform, Anchor.Center), style, scaleToFit: true); stateIcon.Visible = true; if (!wasVisible) { stateIcon.FadeIn(delay, 0.15f); stateIcon.Pulsate(Vector2.One, Vector2.One * 1.5f, 1.0f + delay); } } } private LocalizedString GetHeaderText(bool gameOver, CampaignMode.TransitionType transitionType) { LocalizedString locationName = Submarine.MainSub is { AtEndExit: true } ? endLocation?.DisplayName : startLocation?.DisplayName; string textTag; if (gameMode is PvPMode) { textTag = "RoundSummaryRoundHasEnded"; } else if (gameOver) { textTag = "RoundSummaryGameOver"; } else { switch (transitionType) { case CampaignMode.TransitionType.LeaveLocation: locationName = startLocation?.DisplayName; textTag = "RoundSummaryLeaving"; break; case CampaignMode.TransitionType.ProgressToNextLocation: locationName = endLocation?.DisplayName; textTag = "RoundSummaryProgress"; break; case CampaignMode.TransitionType.ProgressToNextEmptyLocation: locationName = endLocation?.DisplayName; textTag = "RoundSummaryProgressToEmptyLocation"; break; case CampaignMode.TransitionType.ReturnToPreviousLocation: locationName = startLocation?.DisplayName; textTag = "RoundSummaryReturn"; break; case CampaignMode.TransitionType.ReturnToPreviousEmptyLocation: locationName = startLocation?.DisplayName; textTag = "RoundSummaryReturnToEmptyLocation"; break; default: if (Submarine.MainSub == null) { textTag = "RoundSummaryRoundHasEnded"; } else { textTag = Submarine.MainSub.AtEndExit ? "RoundSummaryProgress" : "RoundSummaryReturn"; } break; } } if (startLocation?.Biome != null && startLocation.Biome.IsEndBiome) { locationName ??= startLocation.DisplayName; } if (textTag == null) { return ""; } if (locationName == null) { DebugConsole.ThrowError($"Error while creating round summary: could not determine destination location. Start location: {startLocation?.DisplayName ?? "null"}, end location: {endLocation?.DisplayName ?? "null"}"); locationName = "[UNKNOWN]"; } LocalizedString subName = string.Empty; SubmarineInfo currentOrPending = SubmarineSelection.CurrentOrPendingSubmarine(); if (currentOrPending != null) { subName = currentOrPending.DisplayName; } return TextManager.GetWithVariables(textTag, ("[sub]", subName), ("[location]", locationName)); } private GUIListBox CreateCrewList(GUIComponent parent, IEnumerable characterInfos, TraitorManager.TraitorResults? traitorResults) { var headerFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform, Anchor.TopCenter, minSize: new Point(0, (int)(30 * GUI.Scale))) { }, isHorizontal: true) { AbsoluteSpacing = 2, Stretch = true }; GUIButton jobButton = new GUIButton(new RectTransform(new Vector2(JobColumnWidthPercentage, 1f), headerFrame.RectTransform), TextManager.Get("tabmenu.job"), style: "GUIButtonSmallFreeScale"); GUIButton characterButton = new GUIButton(new RectTransform(new Vector2(CharacterColumnWidthPercentage, 1f), headerFrame.RectTransform), TextManager.Get("name"), style: "GUIButtonSmallFreeScale"); if (gameMode is PvPMode && GameMain.NetworkMember?.RespawnManager != null) { var killButton = new GUIButton(new RectTransform(new Vector2(KillColumnWidthPercentage, 1f), headerFrame.RectTransform), TextManager.Get("killcount"), style: "GUIButtonSmallFreeScale"); killColumnWidth = killButton.Rect.Width; var deathButton = new GUIButton(new RectTransform(new Vector2(DeathColumnWidthPercentage, 1f), headerFrame.RectTransform), TextManager.Get("deathcount"), style: "GUIButtonSmallFreeScale"); deathColumnWidth = deathButton.Rect.Width; } else { GUIButton statusButton = new GUIButton(new RectTransform(new Vector2(StatusColumnWidthPercentage, 1f), headerFrame.RectTransform), TextManager.Get("label.statuslabel"), style: "GUIButtonSmallFreeScale"); statusColumnWidth = statusButton.Rect.Width; } foreach (var btn in headerFrame.GetAllChildren()) { btn.TextBlock.Font = GUIStyle.HotkeyFont; btn.ForceUpperCase = ForceUpperCase.Yes; btn.CanBeFocused = false; } jobColumnWidth = jobButton.Rect.Width; characterColumnWidth = characterButton.Rect.Width; GUIListBox crewList = new GUIListBox(new RectTransform(Vector2.One, parent.RectTransform)) { Padding = new Vector4(4, 10, 0, 0) * GUI.Scale, AutoHideScrollBar = false }; crewList.ContentBackground.Color = Color.Transparent; headerFrame.RectTransform.RelativeSize -= new Vector2(crewList.ScrollBar.RectTransform.RelativeSize.X, 0.0f); killCounts.Clear(); if (GameMain.NetworkMember != null) { foreach (CharacterInfo characterInfo in characterInfos) { if (characterInfo == null) { continue; } Character character = characterInfo.Character; Client ownerClient = GameMain.NetworkMember.ConnectedClients.FirstOrDefault(c => c.Character == character); int killCount = 0, deathCount = 0; foreach (var mission in selectedMissions) { if (mission is not CombatMission combatMission) { continue; } killCount += ownerClient == null ? combatMission.GetBotKillCount(characterInfo) : combatMission.GetClientKillCount(ownerClient); deathCount += ownerClient == null ? combatMission.GetBotDeathCount(characterInfo) : combatMission.GetClientDeathCount(ownerClient); } killCounts[characterInfo] = killCount; deathCounts[characterInfo] = deathCount; } } float delay = crewListAnimDelay; foreach (CharacterInfo characterInfo in characterInfos.OrderByDescending(ci => killCounts.GetValueOrDefault(ci))) { if (characterInfo == null) { continue; } CreateCharacterElement(characterInfo, crewList, traitorResults, delay); delay += crewListAnimDelay; } missionIconAnimDelay = delay; return crewList; } private readonly Dictionary killCounts = new(); private readonly Dictionary deathCounts = new(); private void CreateCharacterElement(CharacterInfo characterInfo, GUIListBox listBox, TraitorManager.TraitorResults? traitorResults, float animDelay) { GUIFrame frame = new GUIFrame(new RectTransform(new Point(listBox.Content.Rect.Width, GUI.IntScale(45)), listBox.Content.RectTransform), style: "ListBoxElement") { CanBeFocused = false, UserData = characterInfo, Color = (Character.Controlled?.Info == characterInfo) ? TabMenu.OwnCharacterBGColor : Color.Transparent }; var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), frame.RectTransform, Anchor.Center), isHorizontal: true) { AbsoluteSpacing = 2, Stretch = true }; new GUICustomComponent(new RectTransform(new Point(jobColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform, Anchor.Center), onDraw: (sb, component) => characterInfo.DrawJobIcon(sb, component.Rect)) { ToolTip = characterInfo.Job.Name ?? "", HoverColor = Color.White, SelectedColor = Color.White }; GUITextBlock characterNameBlock = new GUITextBlock(new RectTransform(new Point(characterColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), ToolBox.LimitString(characterInfo.Name, GUIStyle.Font, characterColumnWidth), textAlignment: Alignment.Center, textColor: characterInfo.Job.Prefab.UIColor); LocalizedString statusText = TextManager.Get("StatusOK"); Color statusColor = GUIStyle.Green; Character character = characterInfo.Character; if (character == null || character.IsDead) { if (character == null && (characterInfo.IsNewHire || characterInfo.BotStatus == BotStatus.ActiveService) && characterInfo.CauseOfDeath == null) { statusText = TextManager.Get("CampaignCrew.NewHire"); statusColor = GUIStyle.Blue; } else if (characterInfo.CauseOfDeath == null) { statusText = TextManager.Get("CauseOfDeathDescription.Unknown"); statusColor = Color.DarkRed; } else if (characterInfo.CauseOfDeath.Type == CauseOfDeathType.Affliction && characterInfo.CauseOfDeath.Affliction == null) { string errorMsg = "Character \"[name]\" had an invalid cause of death (the type of the cause of death was Affliction, but affliction was not specified)."; DebugConsole.ThrowError(errorMsg.Replace("[name]", characterInfo.Name)); GameAnalyticsManager.AddErrorEventOnce("RoundSummary:InvalidCauseOfDeath", GameAnalyticsManager.ErrorSeverity.Error, errorMsg.Replace("[name]", characterInfo.SpeciesName.Value)); statusText = TextManager.Get("CauseOfDeathDescription.Unknown"); statusColor = GUIStyle.Red; } else { statusText = characterInfo.CauseOfDeath.Type == CauseOfDeathType.Affliction ? characterInfo.CauseOfDeath.Affliction.CauseOfDeathDescription : TextManager.Get("CauseOfDeathDescription." + characterInfo.CauseOfDeath.Type.ToString()); statusColor = Color.DarkRed; } } else { if (character.IsUnconscious) { statusText = TextManager.Get("Unconscious"); statusColor = Color.DarkOrange; } else if (character.Vitality / character.MaxVitality < 0.8f) { statusText = TextManager.Get("Injured"); statusColor = Color.DarkOrange; } } if (gameMode is PvPMode && GameMain.NetworkMember?.RespawnManager != null) { new GUITextBlock(new RectTransform(new Point(killColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), killCounts.GetValueOrDefault(characterInfo).ToString(), textAlignment: Alignment.Center); new GUITextBlock(new RectTransform(new Point(deathColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), deathCounts.GetValueOrDefault(characterInfo).ToString(), textAlignment: Alignment.Center); } else { GUITextBlock statusBlock = new GUITextBlock(new RectTransform(new Point(statusColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), ToolBox.LimitString(statusText.Value, GUIStyle.SmallFont, statusColumnWidth), textAlignment: Alignment.Center, textColor: statusColor, font: GUIStyle.SmallFont) { ToolTip = statusText.Value }; } frame.FadeIn(animDelay, 0.15f); foreach (var child in frame.GetAllChildren()) { child.FadeIn(animDelay, 0.15f); } if (traitorResults.HasValue && GameMain.NetworkMember != null) { var clientVotedAsTraitor = traitorResults.Value.GetTraitorClient(); bool isTraitor = clientVotedAsTraitor != null && clientVotedAsTraitor.Character == character; if (isTraitor) { var img = new GUIImage(new RectTransform(new Point(paddedFrame.Rect.Height), paddedFrame.RectTransform, Anchor.CenterRight), style: "TraitorVoteButton") { IgnoreLayoutGroups = true, ToolTip = TextManager.GetWithVariable("traitor.blameresult", "[name]", characterInfo.Name) }; img.FadeIn(1.0f + animDelay, 0.15f); img.Pulsate(Vector2.One, Vector2.One * 1.5f, 1.5f + animDelay); } } } private static GUIFrame CreateReputationElement(GUIComponent parent, LocalizedString name, Reputation reputation, float initialReputation, LocalizedString shortDescription, LocalizedString fullDescription, Sprite icon, Sprite backgroundPortrait, Color iconColor) { var factionFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), parent.RectTransform), style: null); if (backgroundPortrait != null) { new GUICustomComponent(new RectTransform(Vector2.One, factionFrame.RectTransform), onDraw: (sb, customComponent) => { backgroundPortrait.Draw(sb, customComponent.Rect.Center.ToVector2(), customComponent.Color, backgroundPortrait.size / 2, scale: customComponent.Rect.Width / backgroundPortrait.size.X); }) { HideElementsOutsideFrame = true, IgnoreLayoutGroups = true, Color = iconColor * 0.2f }; } var factionInfoHorizontal = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), factionFrame.RectTransform, Anchor.Center), childAnchor: Anchor.CenterRight, isHorizontal: true) { AbsoluteSpacing = GUI.IntScale(5), Stretch = true }; var factionIcon = new GUIImage(new RectTransform(Vector2.One * 0.7f, factionInfoHorizontal.RectTransform, scaleBasis: ScaleBasis.Smallest), icon, scaleToFit: true) { Color = iconColor }; var factionTextContent = new GUILayoutGroup(new RectTransform(Vector2.One, factionInfoHorizontal.RectTransform)) { AbsoluteSpacing = GUI.IntScale(10), Stretch = true }; factionInfoHorizontal.Recalculate(); var header = new GUITextBlock(new RectTransform(new Point(factionTextContent.Rect.Width, GUI.IntScale(40)), factionTextContent.RectTransform), name, font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero, UserData = "header" }; header.RectTransform.IsFixedSize = true; var sliderHolder = new GUILayoutGroup(new RectTransform(new Point((int)(factionTextContent.Rect.Width * 0.8f), GUI.IntScale(20.0f)), factionTextContent.RectTransform), childAnchor: Anchor.CenterLeft, isHorizontal: true) { RelativeSpacing = 0.05f, Stretch = true }; sliderHolder.RectTransform.IsFixedSize = true; factionTextContent.Recalculate(); new GUICustomComponent(new RectTransform(new Vector2(0.8f, 1.0f), sliderHolder.RectTransform), onDraw: (sb, customComponent) => DrawReputationBar(sb, customComponent.Rect, reputation.NormalizedValue, reputation.MinReputation, reputation.MaxReputation)); var reputationText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), sliderHolder.RectTransform), string.Empty, textAlignment: Alignment.CenterLeft, font: GUIStyle.SubHeadingFont); SetReputationText(reputationText); reputation?.OnReputationValueChanged.RegisterOverwriteExisting("RefreshRoundSummary".ToIdentifier(), _ => { SetReputationText(reputationText); }); void SetReputationText(GUITextBlock textBlock) { LocalizedString reputationText = Reputation.GetFormattedReputationText(reputation.NormalizedValue, reputation.Value, addColorTags: true); int reputationChange = (int)Math.Round(reputation.Value - initialReputation); if (Math.Abs(reputationChange) > 0) { string changeText = $"{(reputationChange > 0 ? "+" : "") + reputationChange}"; string colorStr = XMLExtensions.ToStringHex(reputationChange > 0 ? GUIStyle.Green : GUIStyle.Red); textBlock.Text = RichString.Rich($"{reputationText} (‖color:{colorStr}‖{changeText}‖color:end‖)"); } else { textBlock.Text = RichString.Rich(reputationText); } } //spacing new GUIFrame(new RectTransform(new Vector2(1.0f, 0.0f), factionTextContent.RectTransform) { MinSize = new Point(0, GUI.IntScale(5)) }, style: null); var factionDescription = new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.6f), factionTextContent.RectTransform), shortDescription, font: GUIStyle.SmallFont, wrap: true) { UserData = "description", Padding = Vector4.Zero }; if (shortDescription != fullDescription && !fullDescription.IsNullOrEmpty()) { factionDescription.ToolTip = fullDescription; } //spacing new GUIFrame(new RectTransform(new Vector2(1.0f, 0.0f), factionTextContent.RectTransform) { MinSize = new Point(0, GUI.IntScale(5)) }, style: null); factionInfoHorizontal.Recalculate(); factionTextContent.Recalculate(); return factionFrame; } public static void DrawReputationBar(SpriteBatch sb, Rectangle rect, float normalizedReputation, float minReputation, float maxReputation) { int segmentWidth = rect.Width / 5; rect.Width = segmentWidth * 5; for (int i = 0; i < 5; i++) { GUI.DrawRectangle(sb, new Rectangle(rect.X + (segmentWidth * i), rect.Y, segmentWidth, rect.Height), Reputation.GetReputationColor(i / 5.0f), isFilled: true); GUI.DrawRectangle(sb, new Rectangle(rect.X + (segmentWidth * i), rect.Y, segmentWidth, rect.Height), GUIStyle.ColorInventoryBackground, isFilled: false); } GUI.DrawRectangle(sb, rect, GUIStyle.ColorInventoryBackground, isFilled: false); GUI.Arrow.Draw(sb, new Vector2(rect.X + rect.Width * normalizedReputation, rect.Y), GUIStyle.ColorInventoryBackground, scale: GUI.Scale, spriteEffect: SpriteEffects.FlipVertically); GUI.Arrow.Draw(sb, new Vector2(rect.X + rect.Width * normalizedReputation, rect.Y), GUIStyle.TextColorNormal, scale: GUI.Scale * 0.8f, spriteEffect: SpriteEffects.FlipVertically); GUI.DrawString(sb, new Vector2(rect.X, rect.Bottom), ((int)minReputation).ToString(), GUIStyle.TextColorNormal, font: GUIStyle.SmallFont); string maxRepText = ((int)maxReputation).ToString(); Vector2 textSize = GUIStyle.SmallFont.MeasureString(maxRepText); GUI.DrawString(sb, new Vector2(rect.Right - textSize.X, rect.Bottom), maxRepText, GUIStyle.TextColorNormal, font: GUIStyle.SmallFont); } } }