using Barotrauma.Extensions; using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; using PlayerBalanceElement = Barotrauma.CampaignUI.PlayerBalanceElement; namespace Barotrauma { class CrewManagement { private CampaignMode campaign => campaignUI.Campaign; private readonly CampaignUI campaignUI; private readonly GUIComponent parentComponent; private GUIListBox hireableList, pendingList, crewList; private GUIFrame characterPreviewFrame; private GUIDropDown sortingDropDown; private GUITextBlock totalBlock; private GUIButton validateHiresButton; private GUIButton clearAllButton; private PlayerBalanceElement? playerBalanceElement; private List PendingHires => campaign.Map?.CurrentLocation?.HireManager?.PendingHires; private bool HasPermission => CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageHires); private Point resolutionWhenCreated; private bool needsHireableRefresh; private enum SortingMethod { AlphabeticalAsc, JobAsc, PriceAsc, PriceDesc, SkillAsc, SkillDesc } public CrewManagement(CampaignUI campaignUI, GUIComponent parentComponent) { this.campaignUI = campaignUI; this.parentComponent = parentComponent; CreateUI(); UpdateLocationView(campaignUI.Campaign.Map.CurrentLocation, true); campaignUI.Campaign.Map.OnLocationChanged.RegisterOverwriteExisting( "CrewManagement.UpdateLocationView".ToIdentifier(), (locationChangeInfo) => UpdateLocationView(locationChangeInfo.NewLocation, true, locationChangeInfo.PrevLocation)); Reputation.OnAnyReputationValueChanged.RegisterOverwriteExisting( "CrewManagement.UpdateLocationView".ToIdentifier(), _ => needsHireableRefresh = true); } public void RefreshPermissions() { RefreshCrewFrames(hireableList); RefreshCrewFrames(crewList); RefreshCrewFrames(pendingList); if (clearAllButton != null) { clearAllButton.Enabled = HasPermission; } } private void RefreshCrewFrames(GUIListBox listBox) { if (listBox == null) { return; } listBox.CanBeFocused = HasPermission; foreach (GUIComponent child in listBox.Content.Children) { if (child.FindChild(c => c is GUIButton && c.UserData is CharacterInfo, true) is GUIButton buyButton) { CharacterInfo characterInfo = buyButton.UserData as CharacterInfo; bool enoughReputationToHire = EnoughReputationToHire(characterInfo); buyButton.Enabled = HasPermission && enoughReputationToHire; foreach (GUITextBlock text in child.GetAllChildren()) { text.TextColor = new Color(text.TextColor, buyButton.Enabled ? 1.0f : 0.6f); } } } } private void CreateUI() { if (parentComponent.FindChild(c => c.UserData as string == "glow") is GUIComponent glowChild) { parentComponent.RemoveChild(glowChild); } if (parentComponent.FindChild(c => c.UserData as string == "container") is GUIComponent containerChild) { parentComponent.RemoveChild(containerChild); } new GUIFrame(new RectTransform(new Vector2(1.25f, 1.25f), parentComponent.RectTransform, Anchor.Center), style: "OuterGlow", color: Color.Black * 0.7f) { UserData = "glow" }; new GUIFrame(new RectTransform(new Vector2(0.95f), parentComponent.RectTransform, anchor: Anchor.Center), style: null) { CanBeFocused = false, UserData = "container" }; int panelMaxWidth = (int)(GUI.xScale * (GUI.HorizontalAspectRatio < 1.4f ? 650 : 560)); var availableMainGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.4f, 1.0f), campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).RectTransform) { MaxSize = new Point(panelMaxWidth, campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).Rect.Height) }) { Stretch = true, RelativeSpacing = 0.02f }; // Header ------------------------------------------------ var headerGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.75f / 14.0f), availableMainGroup.RectTransform), isHorizontal: true) { RelativeSpacing = 0.005f }; var imageWidth = (float)headerGroup.Rect.Height / headerGroup.Rect.Width; new GUIImage(new RectTransform(new Vector2(imageWidth, 1.0f), headerGroup.RectTransform), "CrewManagementHeaderIcon"); new GUITextBlock(new RectTransform(new Vector2(1.0f - imageWidth, 1.0f), headerGroup.RectTransform), TextManager.Get("campaigncrew.header"), font: GUIStyle.LargeFont) { CanBeFocused = false, ForceUpperCase = ForceUpperCase.Yes }; var hireablesGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.95f), anchor: Anchor.Center, parent: new GUIFrame(new RectTransform(new Vector2(1.0f, 13.25f / 14.0f), availableMainGroup.RectTransform)).RectTransform)) { RelativeSpacing = 0.015f, Stretch = true }; var sortGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.04f), hireablesGroup.RectTransform), isHorizontal: true) { RelativeSpacing = 0.015f, Stretch = true }; new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), sortGroup.RectTransform), text: TextManager.Get("campaignstore.sortby")); sortingDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1.0f), sortGroup.RectTransform), elementCount: 5) { OnSelected = (child, userData) => { SortCharacters(hireableList, (SortingMethod)userData); return true; } }; var tag = "sortingmethod."; sortingDropDown.AddItem(TextManager.Get(tag + SortingMethod.JobAsc), userData: SortingMethod.JobAsc); sortingDropDown.AddItem(TextManager.Get(tag + SortingMethod.SkillAsc), userData: SortingMethod.SkillAsc); sortingDropDown.AddItem(TextManager.Get(tag + SortingMethod.SkillDesc), userData: SortingMethod.SkillDesc); sortingDropDown.AddItem(TextManager.Get(tag + SortingMethod.PriceAsc), userData: SortingMethod.PriceAsc); sortingDropDown.AddItem(TextManager.Get(tag + SortingMethod.PriceDesc), userData: SortingMethod.PriceDesc); hireableList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.96f), hireablesGroup.RectTransform, anchor: Anchor.Center)) { Spacing = 1 }; var pendingAndCrewMainGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.4f, 1.0f), campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).RectTransform, anchor: Anchor.TopRight) { MaxSize = new Point(panelMaxWidth, campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).Rect.Height) }) { Stretch = true, RelativeSpacing = 0.02f }; playerBalanceElement = CampaignUI.AddBalanceElement(pendingAndCrewMainGroup, new Vector2(1.0f, 0.75f / 14.0f)); var pendingAndCrewGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.95f), anchor: Anchor.Center, parent: new GUIFrame(new RectTransform(new Vector2(1.0f, 13.25f / 14.0f), pendingAndCrewMainGroup.RectTransform) { MaxSize = new Point(panelMaxWidth, campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).Rect.Height) }).RectTransform)); float height = 0.05f; new GUITextBlock(new RectTransform(new Vector2(1.0f, height), pendingAndCrewGroup.RectTransform), TextManager.Get("campaigncrew.pending"), font: GUIStyle.SubHeadingFont); pendingList = new GUIListBox(new RectTransform(new Vector2(1.0f, 8 * height), pendingAndCrewGroup.RectTransform)) { Spacing = 1 }; new GUITextBlock(new RectTransform(new Vector2(1.0f, height), pendingAndCrewGroup.RectTransform), TextManager.Get("campaignmenucrew"), font: GUIStyle.SubHeadingFont); crewList = new GUIListBox(new RectTransform(new Vector2(1.0f, 8 * height), pendingAndCrewGroup.RectTransform)) { Spacing = 1 }; var group = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, height), pendingAndCrewGroup.RectTransform), isHorizontal: true); new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), group.RectTransform), TextManager.Get("campaignstore.total")); totalBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), group.RectTransform), "", font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Right) { TextScale = 1.1f }; group = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, height), pendingAndCrewGroup.RectTransform), isHorizontal: true, childAnchor: Anchor.TopRight) { RelativeSpacing = 0.01f }; validateHiresButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), group.RectTransform), text: TextManager.Get("campaigncrew.validate")) { ClickSound = GUISoundType.ConfirmTransaction, ForceUpperCase = ForceUpperCase.Yes, OnClicked = (b, o) => ValidateHires(PendingHires, true) }; clearAllButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), group.RectTransform), text: TextManager.Get("campaignstore.clearall")) { ClickSound = GUISoundType.Cart, ForceUpperCase = ForceUpperCase.Yes, Enabled = HasPermission, OnClicked = (b, o) => RemoveAllPendingHires() }; GUITextBlock.AutoScaleAndNormalize(validateHiresButton.TextBlock, clearAllButton.TextBlock); resolutionWhenCreated = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); } private void UpdateLocationView(Location location, bool removePending, Location prevLocation = null) { if (prevLocation != null && prevLocation == location && GameMain.NetworkMember != null) { return; } if (characterPreviewFrame != null) { characterPreviewFrame.Parent?.RemoveChild(characterPreviewFrame); characterPreviewFrame = null; } UpdateHireables(location); if (pendingList != null) { if (removePending) { PendingHires?.Clear(); pendingList.Content.ClearChildren(); } else { PendingHires?.ForEach(ci => AddPendingHire(ci)); } SetTotalHireCost(); } UpdateCrew(); } private void UpdateHireables(Location location) { if (hireableList != null) { hireableList.Content.Children.ToList().ForEach(c => hireableList.RemoveChild(c)); var hireableCharacters = location.GetHireableCharacters(); if (hireableCharacters.None()) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), hireableList.Content.RectTransform), TextManager.Get("HireUnavailable"), textAlignment: Alignment.Center) { CanBeFocused = false }; } else { foreach (CharacterInfo c in hireableCharacters) { if (c == null) { continue; } CreateCharacterFrame(c, hireableList); } } sortingDropDown.SelectItem(SortingMethod.JobAsc); hireableList.UpdateScrollBarSize(); } } public void SetHireables(Location location, List availableHires) { HireManager hireManager = location.HireManager; if (hireManager == null) { return; } int hireVal = hireManager.AvailableCharacters.Aggregate(0, (curr, hire) => curr + hire.GetIdentifier()); int newVal = availableHires.Aggregate(0, (curr, hire) => curr + hire.GetIdentifier()); if (hireVal != newVal) { location.HireManager.AvailableCharacters = availableHires; UpdateHireables(location); } } public void UpdateCrew() { crewList.Content.Children.ToList().ForEach(c => crewList.Content.RemoveChild(c)); foreach (CharacterInfo c in GameMain.GameSession.CrewManager.GetCharacterInfos()) { if (c == null || !((c.Character?.IsBot ?? true) || campaign is SinglePlayerCampaign)) { continue; } CreateCharacterFrame(c, crewList); } SortCharacters(crewList, SortingMethod.JobAsc); crewList.UpdateScrollBarSize(); } private void SortCharacters(GUIListBox list, SortingMethod sortingMethod) { if (sortingMethod == SortingMethod.AlphabeticalAsc) { list.Content.RectTransform.SortChildren((x, y) => CompareReputationRequirement(x.GUIComponent, y.GUIComponent) ?? ((InfoSkill)x.GUIComponent.UserData).CharacterInfo.Name.CompareTo(((InfoSkill)y.GUIComponent.UserData).CharacterInfo.Name)); } else if (sortingMethod == SortingMethod.JobAsc) { SortCharacters(list, SortingMethod.AlphabeticalAsc); list.Content.RectTransform.SortChildren((x, y) => CompareReputationRequirement(x.GUIComponent, y.GUIComponent) ?? string.Compare(((InfoSkill)x.GUIComponent.UserData).CharacterInfo.Job.Name.Value, ((InfoSkill)y.GUIComponent.UserData).CharacterInfo.Job.Name.Value, StringComparison.Ordinal)); } else if (sortingMethod == SortingMethod.PriceAsc || sortingMethod == SortingMethod.PriceDesc) { SortCharacters(list, SortingMethod.AlphabeticalAsc); list.Content.RectTransform.SortChildren((x, y) => CompareReputationRequirement(x.GUIComponent, y.GUIComponent) ?? ((InfoSkill)x.GUIComponent.UserData).CharacterInfo.Salary.CompareTo(((InfoSkill)y.GUIComponent.UserData).CharacterInfo.Salary)); if (sortingMethod == SortingMethod.PriceDesc) { list.Content.RectTransform.ReverseChildren(); } } else if (sortingMethod == SortingMethod.SkillAsc || sortingMethod == SortingMethod.SkillDesc) { SortCharacters(list, SortingMethod.AlphabeticalAsc); list.Content.RectTransform.SortChildren((x, y) => CompareReputationRequirement(x.GUIComponent, y.GUIComponent) ?? ((InfoSkill)x.GUIComponent.UserData).SkillLevel.CompareTo(((InfoSkill)y.GUIComponent.UserData).SkillLevel)); if (sortingMethod == SortingMethod.SkillDesc) { list.Content.RectTransform.ReverseChildren(); } } int? CompareReputationRequirement(GUIComponent c1, GUIComponent c2) { CharacterInfo info1 = ((InfoSkill)c1.UserData).CharacterInfo; CharacterInfo info2 = ((InfoSkill)c2.UserData).CharacterInfo; float requirement1 = EnoughReputationToHire(info1) ? 0 : info1.MinReputationToHire.reputation; float requirement2 = EnoughReputationToHire(info2) ? 0 : info2.MinReputationToHire.reputation; if (MathUtils.NearlyEqual(requirement1, 0.0f) && MathUtils.NearlyEqual(requirement2, 0.0f)) { return null; } else { return requirement1.CompareTo(requirement2); } } } private readonly struct InfoSkill { public readonly CharacterInfo CharacterInfo; public readonly float SkillLevel; public InfoSkill(CharacterInfo characterInfo, float skillLevel) { CharacterInfo = characterInfo; SkillLevel = skillLevel; } } private void CreateCharacterFrame(CharacterInfo characterInfo, GUIListBox listBox) { Skill skill = null; Color? jobColor = null; if (characterInfo.Job != null) { skill = characterInfo.Job?.PrimarySkill ?? characterInfo.Job.GetSkills().OrderByDescending(s => s.Level).FirstOrDefault(); jobColor = characterInfo.Job.Prefab.UIColor; } GUIFrame frame = new GUIFrame(new RectTransform(new Point(listBox.Content.Rect.Width, (int)(GUI.yScale * 55)), parent: listBox.Content.RectTransform), "ListBoxElement") { UserData = new InfoSkill(characterInfo, skill?.Level ?? 0.0f) }; GUILayoutGroup mainGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), frame.RectTransform, anchor: Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; float portraitWidth = (0.8f * mainGroup.Rect.Height) / mainGroup.Rect.Width; var icon = new GUICustomComponent(new RectTransform(new Vector2(portraitWidth, 0.8f), mainGroup.RectTransform), onDraw: (sb, component) => characterInfo.DrawIcon(sb, component.Rect.Center.ToVector2(), targetAreaSize: component.Rect.Size.ToVector2())) { CanBeFocused = false }; GUILayoutGroup nameAndJobGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.4f - portraitWidth, 0.8f), mainGroup.RectTransform)) { CanBeFocused = false }; GUILayoutGroup nameGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), nameAndJobGroup.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { CanBeFocused = false }; GUITextBlock nameBlock = new GUITextBlock(new RectTransform(Vector2.One, nameGroup.RectTransform), listBox == hireableList ? characterInfo.OriginalName : characterInfo.Name, textColor: jobColor, textAlignment: Alignment.BottomLeft) { CanBeFocused = false }; nameBlock.Text = ToolBox.LimitString(nameBlock.Text, nameBlock.Font, nameBlock.Rect.Width); GUITextBlock jobBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), nameAndJobGroup.RectTransform), characterInfo.Title ?? characterInfo.Job.Name, textColor: Color.White, font: GUIStyle.SmallFont, textAlignment: Alignment.TopLeft) { CanBeFocused = false }; if (!characterInfo.MinReputationToHire.factionId.IsEmpty) { var faction = campaign.Factions.Find(f => f.Prefab.Identifier == characterInfo.MinReputationToHire.factionId); if (faction != null) { jobBlock.TextColor = faction.Prefab.IconColor; } } var fullJobText = jobBlock.Text; jobBlock.Text = ToolBox.LimitString(fullJobText, jobBlock.Font, jobBlock.Rect.Width); if (jobBlock.Text != fullJobText) { jobBlock.ToolTip = fullJobText; jobBlock.CanBeFocused = true; } float width = 0.6f / 3; if (characterInfo.Job != null && skill != null) { GUILayoutGroup skillGroup = new GUILayoutGroup(new RectTransform(new Vector2(width, 0.6f), mainGroup.RectTransform), isHorizontal: true); float iconWidth = (float)skillGroup.Rect.Height / skillGroup.Rect.Width; GUIImage skillIcon = new GUIImage(new RectTransform(Vector2.One, skillGroup.RectTransform, scaleBasis: ScaleBasis.Smallest), skill.Icon, scaleToFit: true) { CanBeFocused = false }; if (jobColor.HasValue) { skillIcon.Color = jobColor.Value; } new GUITextBlock(new RectTransform(new Vector2(1.0f - iconWidth, 1.0f), skillGroup.RectTransform), ((int)skill.Level).ToString(), textAlignment: Alignment.CenterLeft) { CanBeFocused = false }; } if (listBox != crewList) { new GUITextBlock(new RectTransform(new Vector2(width, 1.0f), mainGroup.RectTransform), TextManager.FormatCurrency(characterInfo.Salary), textAlignment: Alignment.Center) { CanBeFocused = false }; } else { // Just a bit of padding to make list layouts similar new GUIFrame(new RectTransform(new Vector2(width, 1.0f), mainGroup.RectTransform), style: null) { CanBeFocused = false }; } if (listBox == hireableList) { var hireButton = new GUIButton(new RectTransform(new Vector2(width, 0.9f), mainGroup.RectTransform), style: "CrewManagementAddButton") { ClickSound = GUISoundType.Cart, UserData = characterInfo, Enabled = CanHire(characterInfo), OnClicked = (b, o) => AddPendingHire(o as CharacterInfo) }; hireButton.OnAddedToGUIUpdateList += (GUIComponent btn) => { if (PendingHires.Count + campaign.CrewManager.GetCharacterInfos().Count() >= CrewManager.MaxCrewSize) { if (btn.Enabled) { btn.ToolTip = TextManager.Get("canthiremorecharacters"); btn.Enabled = false; } } else if (!btn.Enabled) { btn.ToolTip = string.Empty; btn.Enabled = CanHire(characterInfo); } }; } else if (listBox == pendingList) { new GUIButton(new RectTransform(new Vector2(width, 0.9f), mainGroup.RectTransform), style: "CrewManagementRemoveButton") { ClickSound = GUISoundType.Cart, UserData = characterInfo, Enabled = CanHire(characterInfo), OnClicked = (b, o) => RemovePendingHire(o as CharacterInfo) }; } else if (listBox == crewList && campaign != null) { var currentCrew = GameMain.GameSession.CrewManager.GetCharacterInfos(); new GUIButton(new RectTransform(new Vector2(width, 0.9f), mainGroup.RectTransform), style: "CrewManagementFireButton") { UserData = characterInfo, //can't fire if there's only one character in the crew Enabled = currentCrew.Contains(characterInfo) && currentCrew.Count() > 1 && HasPermission, OnClicked = (btn, obj) => { var confirmDialog = new GUIMessageBox( TextManager.Get("FireWarningHeader"), TextManager.GetWithVariable("FireWarningText", "[charactername]", ((CharacterInfo)obj).Name), new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }); confirmDialog.Buttons[0].UserData = (CharacterInfo)obj; confirmDialog.Buttons[0].OnClicked = FireCharacter; confirmDialog.Buttons[0].OnClicked += confirmDialog.Close; confirmDialog.Buttons[1].OnClicked = confirmDialog.Close; return true; } }; } if (listBox == pendingList || listBox == crewList) { nameBlock.RectTransform.Resize(new Point(nameBlock.Rect.Width - nameBlock.Rect.Height, nameBlock.Rect.Height)); nameBlock.Text = ToolBox.LimitString(nameBlock.Text, nameBlock.Font, nameBlock.Rect.Width); nameBlock.RectTransform.Resize(new Point((int)(nameBlock.Padding.X + nameBlock.TextSize.X + nameBlock.Padding.Z), nameBlock.Rect.Height)); Point size = new Point((int)(0.7f * nameBlock.Rect.Height)); new GUIImage(new RectTransform(size, nameGroup.RectTransform), "EditIcon") { CanBeFocused = false }; size = new Point(3 * mainGroup.AbsoluteSpacing + icon.Rect.Width + nameAndJobGroup.Rect.Width, mainGroup.Rect.Height); new GUIButton(new RectTransform(size, frame.RectTransform) { RelativeOffset = new Vector2(0.025f) }, style: null) { Enabled = CanHire(characterInfo), ToolTip = TextManager.GetWithVariable("campaigncrew.givenicknametooltip", "[mouseprimary]", PlayerInput.PrimaryMouseLabel), UserData = characterInfo, OnClicked = CreateRenamingComponent }; } bool CanHire(CharacterInfo characterInfo) { if (!HasPermission) { return false; } return EnoughReputationToHire(characterInfo); } } private bool EnoughReputationToHire(CharacterInfo characterInfo) { if (characterInfo.MinReputationToHire.factionId != Identifier.Empty) { if (MathF.Round(campaign.GetReputation(characterInfo.MinReputationToHire.factionId)) < characterInfo.MinReputationToHire.reputation) { return false; } } return true; } private void CreateCharacterPreviewFrame(GUIListBox listBox, GUIFrame characterFrame, CharacterInfo characterInfo) { Pivot pivot = listBox == hireableList ? Pivot.TopLeft : Pivot.TopRight; Point absoluteOffset = new Point( pivot == Pivot.TopLeft ? listBox.Parent.Parent.Rect.Right + 5 : listBox.Parent.Parent.Rect.Left - 5, characterFrame.Rect.Top); Point frameSize = new Point(GUI.IntScale(300), GUI.IntScale(350)); if (GameMain.GraphicsHeight - (absoluteOffset.Y + frameSize.Y) < 0) { pivot = listBox == hireableList ? Pivot.BottomLeft : Pivot.BottomRight; absoluteOffset.Y = characterFrame.Rect.Bottom; } characterPreviewFrame = new GUIFrame(new RectTransform(frameSize, parent: campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).Parent.RectTransform, pivot: pivot) { AbsoluteOffset = absoluteOffset }, style: "InnerFrame") { UserData = characterInfo }; GUILayoutGroup mainGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.95f), characterPreviewFrame.RectTransform, anchor: Anchor.Center)) { RelativeSpacing = 0.01f, Stretch = true }; // Character info GUILayoutGroup infoGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.475f), mainGroup.RectTransform), isHorizontal: true); GUILayoutGroup infoLabelGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.4f, 1.0f), infoGroup.RectTransform)) { Stretch = true }; GUILayoutGroup infoValueGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.6f, 1.0f), infoGroup.RectTransform)) { Stretch = true }; float blockHeight = 1.0f / 4; new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoLabelGroup.RectTransform), TextManager.Get("name")); GUITextBlock nameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoValueGroup.RectTransform), ""); string name = listBox == hireableList ? characterInfo.OriginalName : characterInfo.Name; nameBlock.Text = ToolBox.LimitString(name, nameBlock.Font, nameBlock.Rect.Width); if (characterInfo.HasSpecifierTags) { var menuCategoryVar = characterInfo.Prefab.MenuCategoryVar; new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoLabelGroup.RectTransform), TextManager.Get(menuCategoryVar)); new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoValueGroup.RectTransform), TextManager.Get(characterInfo.ReplaceVars($"[{menuCategoryVar}]"))); } if (characterInfo.Job is Job job) { new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoLabelGroup.RectTransform), TextManager.Get("tabmenu.job")); new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoValueGroup.RectTransform), job.Name); } if (characterInfo.PersonalityTrait is NPCPersonalityTrait trait) { new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoLabelGroup.RectTransform), TextManager.Get("PersonalityTrait")); new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoValueGroup.RectTransform), trait.DisplayName); } infoLabelGroup.Recalculate(); infoValueGroup.Recalculate(); new GUIImage(new RectTransform(new Vector2(1.0f, 0.05f), mainGroup.RectTransform), "HorizontalLine"); // Skills GUILayoutGroup skillGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.475f), mainGroup.RectTransform), isHorizontal: true); GUILayoutGroup skillNameGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 1.0f), skillGroup.RectTransform)); GUILayoutGroup skillLevelGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.2f, 1.0f), skillGroup.RectTransform)); var characterSkills = characterInfo.Job.GetSkills(); blockHeight = 1.0f / characterSkills.Count(); foreach (Skill skill in characterSkills) { new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), skillNameGroup.RectTransform), TextManager.Get("SkillName." + skill.Identifier), font: GUIStyle.SmallFont); new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), skillLevelGroup.RectTransform), ((int)skill.Level).ToString(), textAlignment: Alignment.Right); } if (characterInfo.MinReputationToHire.reputation > 0.0f) { var repStr = TextManager.GetWithVariables( "campaignstore.reputationrequired", ("[amount]", ((int)characterInfo.MinReputationToHire.reputation).ToString()), ("[faction]", TextManager.Get("faction." + characterInfo.MinReputationToHire.factionId).Value)); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), mainGroup.RectTransform), repStr, textColor: !EnoughReputationToHire(characterInfo) ? GUIStyle.Orange : GUIStyle.Green, font: GUIStyle.SmallFont, wrap: true, textAlignment: Alignment.Center); } mainGroup.Recalculate(); characterPreviewFrame.RectTransform.MinSize = new Point(0, (int)(mainGroup.Children.Sum(c => c.Rect.Height + mainGroup.Rect.Height * mainGroup.RelativeSpacing) / mainGroup.RectTransform.RelativeSize.Y)); } private bool SelectCharacter(GUIListBox listBox, GUIFrame characterFrame, CharacterInfo characterInfo) { if (characterPreviewFrame != null && characterPreviewFrame.UserData != characterInfo) { characterPreviewFrame.Parent?.RemoveChild(characterPreviewFrame); characterPreviewFrame = null; } if (listBox == null || characterFrame == null || characterInfo == null) { return false; } if (characterPreviewFrame == null) { CreateCharacterPreviewFrame(listBox, characterFrame, characterInfo); } return true; } private bool AddPendingHire(CharacterInfo characterInfo, bool createNetworkMessage = true) { if (PendingHires.Count + campaign.CrewManager.GetCharacters().Count() >= CrewManager.MaxCrewSize) { return false; } hireableList.Content.RemoveChild(hireableList.Content.FindChild(c => ((InfoSkill)c.UserData).CharacterInfo == characterInfo)); hireableList.UpdateScrollBarSize(); if (!PendingHires.Contains(characterInfo)) { PendingHires.Add(characterInfo); } CreateCharacterFrame(characterInfo, pendingList); SortCharacters(pendingList, SortingMethod.JobAsc); pendingList.UpdateScrollBarSize(); SetTotalHireCost(); if (createNetworkMessage) { SendCrewState(true); } return true; } private bool RemovePendingHire(CharacterInfo characterInfo, bool setTotalHireCost = true, bool createNetworkMessage = true) { if (PendingHires.Contains(characterInfo)) { PendingHires.Remove(characterInfo); } pendingList.Content.RemoveChild(pendingList.Content.FindChild(c => ((InfoSkill)c.UserData).CharacterInfo == characterInfo)); pendingList.UpdateScrollBarSize(); // Server will reset the names to originals in multiplayer if (!GameMain.IsMultiplayer) { characterInfo?.ResetName(); } if (campaign.Map.CurrentLocation.HireManager.AvailableCharacters.Any(info => info.GetIdentifierUsingOriginalName() == characterInfo.GetIdentifierUsingOriginalName()) && hireableList.Content.Children.None(c => c.UserData is InfoSkill userData && userData.CharacterInfo.GetIdentifierUsingOriginalName() == characterInfo.GetIdentifierUsingOriginalName())) { CreateCharacterFrame(characterInfo, hireableList); SortCharacters(hireableList, (SortingMethod)sortingDropDown.SelectedItemData); hireableList.UpdateScrollBarSize(); } if (setTotalHireCost) { SetTotalHireCost(); } if (createNetworkMessage) { SendCrewState(true); } return true; } private bool RemoveAllPendingHires(bool createNetworkMessage = true) { pendingList.Content.Children.ToList().ForEach(c => RemovePendingHire(((InfoSkill)c.UserData).CharacterInfo, setTotalHireCost: false, createNetworkMessage)); SetTotalHireCost(); return true; } private void SetTotalHireCost() { if (pendingList == null || totalBlock == null || validateHiresButton == null) { return; } int total = 0; pendingList.Content.Children.ForEach(c => { total += ((InfoSkill)c.UserData).CharacterInfo.Salary; }); totalBlock.Text = TextManager.FormatCurrency(total); bool enoughMoney = campaign == null || campaign.CanAfford(total); totalBlock.TextColor = enoughMoney ? Color.White : Color.Red; validateHiresButton.Enabled = enoughMoney && HasPermission && pendingList.Content.RectTransform.Children.Any(); } public bool ValidateHires(List hires, bool createNetworkEvent = false) { if (hires == null || hires.None()) { return false; } List nonDuplicateHires = new List(); hires.ForEach(hireInfo => { if (campaign.CrewManager.GetCharacterInfos().None(crewInfo => crewInfo.IsNewHire && crewInfo.GetIdentifierUsingOriginalName() == hireInfo.GetIdentifierUsingOriginalName())) { nonDuplicateHires.Add(hireInfo); } }); if (nonDuplicateHires.None()) { return false; } int total = nonDuplicateHires.Aggregate(0, (total, info) => total + info.Salary); if (!campaign.CanAfford(total)) { return false; } bool atLeastOneHired = false; foreach (CharacterInfo ci in nonDuplicateHires) { if (campaign.TryHireCharacter(campaign.Map.CurrentLocation, ci)) { atLeastOneHired = true; } else { break; } } if (atLeastOneHired) { UpdateLocationView(campaign.Map.CurrentLocation, true); SelectCharacter(null, null, null); var dialog = new GUIMessageBox( TextManager.Get("newcrewmembers"), TextManager.GetWithVariable("crewhiredmessage", "[location]", campaignUI?.Campaign?.Map?.CurrentLocation?.Name), new LocalizedString[] { TextManager.Get("Ok") }); dialog.Buttons[0].OnClicked += dialog.Close; } if (createNetworkEvent) { SendCrewState(true, validateHires: true); } return false; } private bool CreateRenamingComponent(GUIButton button, object userData) { if (!HasPermission || userData is not CharacterInfo characterInfo) { return false; } var outerGlowFrame = new GUIFrame(new RectTransform(new Vector2(1.25f, 1.25f), parentComponent.RectTransform, Anchor.Center), style: "OuterGlow", color: Color.Black * 0.7f); var frame = new GUIFrame(new RectTransform(new Vector2(0.33f, 0.4f), outerGlowFrame.RectTransform, anchor: Anchor.Center) { MaxSize = new Point(400, 300).Multiply(GUI.Scale) }); var layoutGroup = new GUILayoutGroup(new RectTransform((frame.Rect.Size - GUIStyle.ItemFrameMargin).Multiply(new Vector2(0.75f, 1.0f)), frame.RectTransform, anchor: Anchor.Center), childAnchor: Anchor.TopCenter) { RelativeSpacing = 0.02f, Stretch = true }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), layoutGroup.RectTransform), TextManager.Get("campaigncrew.givenickname"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center, wrap: true); var groupElementSize = new Vector2(1.0f, 0.25f); var nameBox = new GUITextBox(new RectTransform(groupElementSize, layoutGroup.RectTransform)) { MaxTextLength = Client.MaxNameLength }; new GUIButton(new RectTransform(groupElementSize, layoutGroup.RectTransform), text: TextManager.Get("confirm")) { OnClicked = (button, userData) => { if (RenameCharacter(characterInfo, nameBox.Text?.Trim())) { parentComponent.RemoveChild(outerGlowFrame); return true; } else { nameBox.Flash(color: Color.Red); return false; } } }; new GUIButton(new RectTransform(groupElementSize, layoutGroup.RectTransform), text: TextManager.Get("cancel")) { OnClicked = (button, userData) => { parentComponent.RemoveChild(outerGlowFrame); return true; } }; layoutGroup.Recalculate(); return true; } public bool RenameCharacter(CharacterInfo characterInfo, string newName) { if (characterInfo == null || string.IsNullOrEmpty(newName)) { return false; } if (newName == characterInfo.Name) { return false; } if (GameMain.IsMultiplayer) { SendCrewState(false, renameCharacter: (characterInfo, newName)); } else { var crewComponent = crewList.Content.FindChild(c => ((InfoSkill)c.UserData).CharacterInfo == characterInfo); if (crewComponent != null) { crewList.Content.RemoveChild(crewComponent); campaign.CrewManager.RenameCharacter(characterInfo, newName); CreateCharacterFrame(characterInfo, crewList); SortCharacters(crewList, SortingMethod.JobAsc); } else { var pendingComponent = pendingList.Content.FindChild(c => ((InfoSkill)c.UserData).CharacterInfo == characterInfo); if (pendingComponent != null) { pendingList.Content.RemoveChild(pendingComponent); campaign.Map.CurrentLocation.HireManager.RenameCharacter(characterInfo, newName); CreateCharacterFrame(characterInfo, pendingList); SortCharacters(pendingList, SortingMethod.JobAsc); SetTotalHireCost(); } else { return false; } } } return true; } private bool FireCharacter(GUIButton button, object selection) { if (selection is not CharacterInfo characterInfo) { return false; } campaign.CrewManager.FireCharacter(characterInfo); SelectCharacter(null, null, null); UpdateCrew(); SendCrewState(false, firedCharacter: characterInfo); return false; } public void Update() { if (GameMain.GraphicsWidth != resolutionWhenCreated.X || GameMain.GraphicsHeight != resolutionWhenCreated.Y) { CreateUI(); UpdateLocationView(campaign.Map.CurrentLocation, false); } else { playerBalanceElement = CampaignUI.UpdateBalanceElement(playerBalanceElement); } if (needsHireableRefresh) { RefreshCrewFrames(hireableList); if (sortingDropDown?.SelectedItemData != null) { SortCharacters(hireableList, (SortingMethod)sortingDropDown.SelectedItemData); } needsHireableRefresh = false; } (GUIComponent highlightedFrame, CharacterInfo highlightedInfo) = FindHighlightedCharacter(GUI.MouseOn); if (highlightedFrame != null && highlightedInfo != null) { if (characterPreviewFrame == null || highlightedInfo != characterPreviewFrame.UserData) { GUIComponent component = GUI.MouseOn; GUIListBox listBox = null; do { if (component.Parent is GUIListBox) { listBox = component.Parent as GUIListBox; break; } else if (component.Parent != null) { component = component.Parent; } else { break; } } while (listBox == null); if (listBox != null) { SelectCharacter(listBox, highlightedFrame as GUIFrame, highlightedInfo); } } else { // TODO: Reposition the current preview panel if necessary // Could happen if we scroll a list while hovering? } } else if (characterPreviewFrame != null) { characterPreviewFrame.Parent?.RemoveChild(characterPreviewFrame); characterPreviewFrame = null; } static (GUIComponent GuiComponent, CharacterInfo CharacterInfo) FindHighlightedCharacter(GUIComponent c) { if (c == null) { return default; } if (c.UserData is InfoSkill highlightedData) { return (c, highlightedData.CharacterInfo); } if (c.Parent != null) { if (c.Parent is GUIListBox) { return default; } return FindHighlightedCharacter(c.Parent); } return default; } } public void SetPendingHires(List characterInfos, Location location) { List oldHires = PendingHires.ToList(); foreach (CharacterInfo pendingHire in oldHires) { RemovePendingHire(pendingHire, createNetworkMessage: false); } PendingHires.Clear(); foreach (int identifier in characterInfos) { CharacterInfo match = location.HireManager.AvailableCharacters.Find(info => info.GetIdentifierUsingOriginalName() == identifier); if (match != null) { PendingHires.Add(match); AddPendingHire(match, createNetworkMessage: false); } else { DebugConsole.ThrowError("Received a hire that doesn't exist."); } } } /// /// Notify the server of crew changes /// /// When set to true will tell the server to update the pending hires /// When not null tell the server to rename this character. Item1 is the character to rename, Item2 is the new name, Item3 indicates whether the renamed character is already a part of the crew. /// When not null tell the server to fire this character /// When set to true will tell the server to validate pending hires public void SendCrewState(bool updatePending, (CharacterInfo info, string newName) renameCharacter = default, CharacterInfo firedCharacter = null, bool validateHires = false) { if (campaign is MultiPlayerCampaign) { IWriteMessage msg = new WriteOnlyMessage(); msg.WriteByte((byte)ClientPacketHeader.CREW); msg.WriteBoolean(updatePending); if (updatePending) { msg.WriteUInt16((ushort)PendingHires.Count); foreach (CharacterInfo pendingHire in PendingHires) { msg.WriteInt32(pendingHire.GetIdentifierUsingOriginalName()); } } msg.WriteBoolean(validateHires); bool validRenaming = renameCharacter.info != null && !string.IsNullOrEmpty(renameCharacter.newName); msg.WriteBoolean(validRenaming); if (validRenaming) { int identifier = renameCharacter.info.GetIdentifierUsingOriginalName(); msg.WriteInt32(identifier); msg.WriteString(renameCharacter.newName); bool existingCrewMember = campaign.CrewManager?.GetCharacterInfos().Any(ci => ci.GetIdentifierUsingOriginalName() == identifier) ?? false; msg.WriteBoolean(existingCrewMember); } msg.WriteBoolean(firedCharacter != null); if (firedCharacter != null) { msg.WriteInt32(firedCharacter.GetIdentifier()); } GameMain.Client.ClientPeer?.Send(msg, DeliveryMethod.Reliable); } } } }