Client LuaCsForBarotrauma
HRManagerUI.cs
3 using Microsoft.Xna.Framework;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using PlayerBalanceElement = Barotrauma.CampaignUI.PlayerBalanceElement;
8 
9 namespace Barotrauma
10 {
15  {
16  private CampaignMode campaign => campaignUI.Campaign;
17  private readonly CampaignUI campaignUI;
18  private readonly GUIComponent parentComponent;
19 
20  private GUIComponent pendingAndCrewPanel;
21  private GUIListBox hireableList, pendingList, crewList;
22  private GUIFrame characterPreviewFrame;
23  private GUIDropDown sortingDropDown;
24  private GUITextBlock totalBlock;
25  private GUIButton validateHiresButton;
26  private GUIButton clearAllButton;
27 
28  private PlayerBalanceElement? playerBalanceElement;
29 
30  private List<CharacterInfo> PendingHires => campaign.Map?.CurrentLocation?.HireManager?.PendingHires;
31 
32 
33  private bool wasReplacingPermanentlyDeadCharacter;
38  private static bool ReplacingPermanentlyDeadCharacter =>
39  GameMain.NetworkMember?.ServerSettings is { RespawnMode: RespawnMode.Permadeath, IronmanMode: false } &&
40  GameMain.Client?.CharacterInfo is { PermanentlyDead: true };
41 
42  private bool hadPermissionToHire;
43  private static bool HasPermissionToHire => ReplacingPermanentlyDeadCharacter ?
44  GameMain.NetworkMember?.ServerSettings.ReplaceCostPercentage <= 0 || CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageMoney) || CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageHires) :
46 
47  private Point resolutionWhenCreated;
48 
49  private bool needsHireableRefresh;
50 
51  private enum SortingMethod
52  {
53  AlphabeticalAsc,
54  JobAsc,
55  PriceAsc,
56  PriceDesc,
57  SkillAsc,
58  SkillDesc
59  }
60 
61  public HRManagerUI(CampaignUI campaignUI, GUIComponent parentComponent)
62  {
63  this.campaignUI = campaignUI;
64  this.parentComponent = parentComponent;
65 
66  CreateUI();
67  UpdateLocationView(campaignUI.Campaign.Map.CurrentLocation, true);
68 
69  campaignUI.Campaign.Map.OnLocationChanged.RegisterOverwriteExisting(
70  "CrewManagement.UpdateLocationView".ToIdentifier(),
71  (locationChangeInfo) => UpdateLocationView(locationChangeInfo.NewLocation, true, locationChangeInfo.PrevLocation));
72  Reputation.OnAnyReputationValueChanged.RegisterOverwriteExisting(
73  "CrewManagement.UpdateLocationView".ToIdentifier(), _ => needsHireableRefresh = true);
74 
75  hadPermissionToHire = HasPermissionToHire;
76  wasReplacingPermanentlyDeadCharacter = ReplacingPermanentlyDeadCharacter;
77  }
78 
79  public void RefreshUI()
80  {
81  RefreshCrewFrames(hireableList);
82  RefreshCrewFrames(crewList);
83  RefreshCrewFrames(pendingList);
84  if (clearAllButton != null) { clearAllButton.Enabled = HasPermissionToHire; }
85  hadPermissionToHire = HasPermissionToHire;
86  wasReplacingPermanentlyDeadCharacter = ReplacingPermanentlyDeadCharacter;
87  }
88 
89  private void RefreshCrewFrames(GUIListBox listBox)
90  {
91  if (listBox == null) { return; }
92  listBox.CanBeFocused = HasPermissionToHire;
93  foreach (GUIComponent child in listBox.Content.Children)
94  {
95  if (child.FindChild(c => c is GUIButton && c.UserData is CharacterInfo, true) is GUIButton buyButton)
96  {
97  CharacterInfo characterInfo = buyButton.UserData as CharacterInfo;
98  buyButton.Enabled =
99  //"normal buying" is disabled when replacing a dead character
100  !ReplacingPermanentlyDeadCharacter &&
101  HasPermissionToHire &&
102  EnoughReputationToHire(characterInfo) && campaign.CanAffordNewCharacter(characterInfo);
103  foreach (GUITextBlock text in child.GetAllChildren<GUITextBlock>())
104  {
105  text.TextColor = new Color(text.TextColor, buyButton.Enabled ? 1.0f : 0.6f);
106  }
107  }
108  }
109  }
110 
111  private void CreateUI()
112  {
113  if (parentComponent.FindChild(c => c.UserData as string == "glow") is GUIComponent glowChild)
114  {
115  parentComponent.RemoveChild(glowChild);
116  }
117  if (parentComponent.FindChild(c => c.UserData as string == "container") is GUIComponent containerChild)
118  {
119  parentComponent.RemoveChild(containerChild);
120  }
121 
122  new GUIFrame(new RectTransform(new Vector2(1.25f, 1.25f), parentComponent.RectTransform, Anchor.Center),
123  style: "OuterGlow", color: Color.Black * 0.7f)
124  {
125  UserData = "glow"
126  };
127  new GUIFrame(new RectTransform(new Vector2(0.95f), parentComponent.RectTransform, anchor: Anchor.Center),
128  style: null)
129  {
130  CanBeFocused = false,
131  UserData = "container"
132  };
133 
134  int panelMaxWidth = (int)(GUI.xScale * (GUI.HorizontalAspectRatio < 1.4f ? 650 : 560));
135  var availableMainGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.4f, 1.0f), campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).RectTransform)
136  {
137  MaxSize = new Point(panelMaxWidth, campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).Rect.Height)
138  })
139  {
140  Stretch = true,
141  RelativeSpacing = 0.02f
142  };
143 
144  // Header ------------------------------------------------
145  var headerGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.75f / 14.0f), availableMainGroup.RectTransform), isHorizontal: true)
146  {
147  RelativeSpacing = 0.005f
148  };
149  var imageWidth = (float)headerGroup.Rect.Height / headerGroup.Rect.Width;
150  new GUIImage(new RectTransform(new Vector2(imageWidth, 1.0f), headerGroup.RectTransform), "CrewManagementHeaderIcon");
151  new GUITextBlock(new RectTransform(new Vector2(1.0f - imageWidth, 1.0f), headerGroup.RectTransform), TextManager.Get("campaigncrew.header"), font: GUIStyle.LargeFont)
152  {
153  CanBeFocused = false,
155  };
156 
157  var hireablesGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.95f), anchor: Anchor.Center,
158  parent: new GUIFrame(new RectTransform(new Vector2(1.0f, 13.25f / 14.0f), availableMainGroup.RectTransform)).RectTransform))
159  {
160  RelativeSpacing = 0.015f,
161  Stretch = true
162  };
163 
164  var sortGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.04f), hireablesGroup.RectTransform), isHorizontal: true)
165  {
166  RelativeSpacing = 0.015f,
167  Stretch = true
168  };
169  new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), sortGroup.RectTransform), text: TextManager.Get("campaignstore.sortby"));
170  sortingDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1.0f), sortGroup.RectTransform), elementCount: 5)
171  {
172  OnSelected = (child, userData) =>
173  {
174  SortCharacters(hireableList, (SortingMethod)userData);
175  return true;
176  }
177  };
178  var tag = "sortingmethod.";
179  sortingDropDown.AddItem(TextManager.Get(tag + SortingMethod.JobAsc), userData: SortingMethod.JobAsc);
180  sortingDropDown.AddItem(TextManager.Get(tag + SortingMethod.SkillAsc), userData: SortingMethod.SkillAsc);
181  sortingDropDown.AddItem(TextManager.Get(tag + SortingMethod.SkillDesc), userData: SortingMethod.SkillDesc);
182  sortingDropDown.AddItem(TextManager.Get(tag + SortingMethod.PriceAsc), userData: SortingMethod.PriceAsc);
183  sortingDropDown.AddItem(TextManager.Get(tag + SortingMethod.PriceDesc), userData: SortingMethod.PriceDesc);
184 
185  hireableList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.96f),
186  hireablesGroup.RectTransform,
187  anchor: Anchor.Center))
188  {
189  Spacing = 1
190  };
191 
192  var pendingAndCrewMainGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.4f, 1.0f), campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).RectTransform, anchor: Anchor.TopRight)
193  {
194  MaxSize = new Point(panelMaxWidth, campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).Rect.Height)
195  })
196  {
197  Stretch = true,
198  RelativeSpacing = 0.02f
199  };
200 
201  playerBalanceElement = CampaignUI.AddBalanceElement(pendingAndCrewMainGroup, new Vector2(1.0f, 0.75f / 14.0f));
202 
203  pendingAndCrewPanel = new GUIFrame(new RectTransform(new Vector2(1.0f, 13.25f / 14.0f), pendingAndCrewMainGroup.RectTransform)
204  {
205  MaxSize = new Point(panelMaxWidth, campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).Rect.Height)
206  });
207 
208  var pendingAndCrewGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.95f), anchor: Anchor.Center,
209  parent: pendingAndCrewPanel.RectTransform));
210 
211  float height = 0.05f;
212  new GUITextBlock(new RectTransform(new Vector2(1.0f, height), pendingAndCrewGroup.RectTransform), TextManager.Get("campaigncrew.pending"), font: GUIStyle.SubHeadingFont);
213  pendingList = new GUIListBox(new RectTransform(new Vector2(1.0f, 8 * height), pendingAndCrewGroup.RectTransform))
214  {
215  Spacing = 1
216  };
217 
218  var crewHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, height), pendingAndCrewGroup.RectTransform), TextManager.Get("campaignmenucrew"), font: GUIStyle.SubHeadingFont);
219 
220  new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), crewHeader.RectTransform, Anchor.CenterRight), string.Empty, textAlignment: Alignment.CenterRight)
221  {
222  TextGetter = () =>
223  {
224  int crewSize = campaign?.CrewManager?.GetCharacterInfos()?.Count() ?? 0;
225  return $"{crewSize}/{CrewManager.MaxCrewSize}";
226  }
227  };
228  crewList = new GUIListBox(new RectTransform(new Vector2(1.0f, 8 * height), pendingAndCrewGroup.RectTransform))
229  {
230  Spacing = 1
231  };
232 
233  var group = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, height), pendingAndCrewGroup.RectTransform), isHorizontal: true);
234  new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), group.RectTransform), TextManager.Get("campaignstore.total"));
235  totalBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), group.RectTransform), "", font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Right)
236  {
237  TextScale = 1.1f
238  };
239  group = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, height), pendingAndCrewGroup.RectTransform), isHorizontal: true, childAnchor: Anchor.TopRight)
240  {
241  RelativeSpacing = 0.01f
242  };
243  validateHiresButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), group.RectTransform), text: TextManager.Get("campaigncrew.validate"))
244  {
245  ClickSound = GUISoundType.ConfirmTransaction,
247  OnClicked = (b, o) => ValidateHires(PendingHires, createNetworkEvent: true)
248  };
249  clearAllButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), group.RectTransform), text: TextManager.Get("campaignstore.clearall"))
250  {
251  ClickSound = GUISoundType.Cart,
253  Enabled = HasPermissionToHire,
254  OnClicked = (b, o) => RemoveAllPendingHires()
255  };
256  GUITextBlock.AutoScaleAndNormalize(validateHiresButton.TextBlock, clearAllButton.TextBlock);
257 
258  resolutionWhenCreated = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight);
259  }
260 
261  private void UpdateLocationView(Location location, bool removePending, Location prevLocation = null)
262  {
263  if (prevLocation != null && prevLocation == location && GameMain.NetworkMember != null) { return; }
264 
265  if (characterPreviewFrame != null)
266  {
267  characterPreviewFrame.Parent?.RemoveChild(characterPreviewFrame);
268  characterPreviewFrame = null;
269  }
270  UpdateHireables(location);
271  if (pendingList != null)
272  {
273  if (removePending)
274  {
275  PendingHires?.Clear();
276  pendingList.Content.ClearChildren();
277  }
278  else
279  {
280  PendingHires?.ForEach(ci => AddPendingHire(ci));
281  }
282  SetTotalHireCost();
283  }
284  UpdateCrew();
285  }
286 
287  public void UpdateHireables()
288  {
289  UpdateHireables(campaign?.CurrentLocation);
290  }
291 
292  private void UpdateHireables(Location location)
293  {
294  if (hireableList == null) { return; }
295  hireableList.Content.Children.ToList().ForEach(c => hireableList.RemoveChild(c));
296  var hireableCharacters = location.GetHireableCharacters();
297  if (hireableCharacters.None())
298  {
299  new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), hireableList.Content.RectTransform), TextManager.Get("HireUnavailable"), textAlignment: Alignment.Center)
300  {
301  CanBeFocused = false
302  };
303  }
304  else
305  {
306  foreach (CharacterInfo c in hireableCharacters)
307  {
308  if (c == null || PendingHires.Contains(c)) { continue; }
309  CreateCharacterFrame(c, hireableList);
310  }
311  }
312  sortingDropDown.SelectItem(SortingMethod.JobAsc);
313  hireableList.UpdateScrollBarSize();
314  }
315 
316  public void SetHireables(Location location, List<CharacterInfo> availableHires)
317  {
318  HireManager hireManager = location.HireManager;
319  if (hireManager == null) { return; }
320  int hireVal = hireManager.AvailableCharacters.Aggregate(0, (curr, hire) => curr + hire.ID);
321  int newVal = availableHires.Aggregate(0, (curr, hire) => curr + hire.ID);
322  if (hireVal != newVal)
323  {
324  location.HireManager.AvailableCharacters = availableHires;
325  UpdateHireables(location);
326  }
327  }
328 
329  public void UpdateCrew()
330  {
331  crewList.Content.Children.ToList().ForEach(c => crewList.Content.RemoveChild(c));
333  {
334  if (c == null || !((c.Character?.IsBot ?? true) || campaign is SinglePlayerCampaign)) { continue; }
335  CreateCharacterFrame(c, crewList);
336  }
337  SortCharacters(crewList, SortingMethod.JobAsc);
338  crewList.UpdateScrollBarSize();
339  }
340 
341  private void SortCharacters(GUIListBox list, SortingMethod sortingMethod)
342  {
343  if (sortingMethod == SortingMethod.AlphabeticalAsc)
344  {
345  list.Content.RectTransform.SortChildren((x, y) =>
346  CompareReputationRequirement(x.GUIComponent, y.GUIComponent) ??
347  ((InfoSkill)x.GUIComponent.UserData).CharacterInfo.Name.CompareTo(((InfoSkill)y.GUIComponent.UserData).CharacterInfo.Name));
348  }
349  else if (sortingMethod == SortingMethod.JobAsc)
350  {
351  SortCharacters(list, SortingMethod.AlphabeticalAsc);
352  list.Content.RectTransform.SortChildren((x, y) =>
353  CompareReputationRequirement(x.GUIComponent, y.GUIComponent) ??
354  string.Compare(((InfoSkill)x.GUIComponent.UserData).CharacterInfo.Job.Name.Value, ((InfoSkill)y.GUIComponent.UserData).CharacterInfo.Job.Name.Value, StringComparison.Ordinal));
355  }
356  else if (sortingMethod == SortingMethod.PriceAsc || sortingMethod == SortingMethod.PriceDesc)
357  {
358  SortCharacters(list, SortingMethod.AlphabeticalAsc);
359  list.Content.RectTransform.SortChildren((x, y) =>
360  CompareReputationRequirement(x.GUIComponent, y.GUIComponent) ??
361  ((InfoSkill)x.GUIComponent.UserData).CharacterInfo.Salary.CompareTo(((InfoSkill)y.GUIComponent.UserData).CharacterInfo.Salary));
362  if (sortingMethod == SortingMethod.PriceDesc) { list.Content.RectTransform.ReverseChildren(); }
363  }
364  else if (sortingMethod == SortingMethod.SkillAsc || sortingMethod == SortingMethod.SkillDesc)
365  {
366  SortCharacters(list, SortingMethod.AlphabeticalAsc);
367  list.Content.RectTransform.SortChildren((x, y) =>
368  CompareReputationRequirement(x.GUIComponent, y.GUIComponent) ??
369  ((InfoSkill)x.GUIComponent.UserData).SkillLevel.CompareTo(((InfoSkill)y.GUIComponent.UserData).SkillLevel));
370  if (sortingMethod == SortingMethod.SkillDesc) { list.Content.RectTransform.ReverseChildren(); }
371  }
372 
373  int? CompareReputationRequirement(GUIComponent c1, GUIComponent c2)
374  {
375  CharacterInfo info1 = ((InfoSkill)c1.UserData).CharacterInfo;
376  CharacterInfo info2 = ((InfoSkill)c2.UserData).CharacterInfo;
377  float requirement1 = EnoughReputationToHire(info1) ? 0 : info1.MinReputationToHire.reputation;
378  float requirement2 = EnoughReputationToHire(info2) ? 0 : info2.MinReputationToHire.reputation;
379  if (MathUtils.NearlyEqual(requirement1, 0.0f) && MathUtils.NearlyEqual(requirement2, 0.0f))
380  {
381  return null;
382  }
383  else
384  {
385  return requirement1.CompareTo(requirement2);
386  }
387  }
388  }
389 
390  private readonly struct InfoSkill
391  {
392  public readonly CharacterInfo CharacterInfo;
393  public readonly float SkillLevel;
394 
395  public InfoSkill(CharacterInfo characterInfo, float skillLevel)
396  {
397  CharacterInfo = characterInfo;
398  SkillLevel = skillLevel;
399  }
400  }
401 
402  public GUIComponent CreateCharacterFrame(CharacterInfo characterInfo, GUIListBox listBox, bool hideSalary = false)
403  {
404  Skill skill = null;
405  Color? jobColor = null;
406  if (characterInfo.Job != null)
407  {
408  skill = characterInfo.Job?.PrimarySkill ?? characterInfo.Job.GetSkills().OrderByDescending(s => s.Level).FirstOrDefault();
409  jobColor = characterInfo.Job.Prefab.UIColor;
410  }
411 
412  GUIFrame frame = new GUIFrame(new RectTransform(new Point(listBox.Content.Rect.Width, (int)(GUI.yScale * 55)), parent: listBox.Content.RectTransform), "ListBoxElement")
413  {
414  UserData = new InfoSkill(characterInfo, skill?.Level ?? 0.0f)
415  };
416  GUILayoutGroup mainGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), frame.RectTransform, anchor: Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft)
417  {
418  Stretch = true
419  };
420 
421  float portraitWidth = (0.8f * mainGroup.Rect.Height) / mainGroup.Rect.Width;
422  var icon = new GUICustomComponent(new RectTransform(new Vector2(portraitWidth, 0.8f), mainGroup.RectTransform),
423  onDraw: (sb, component) => characterInfo.DrawIcon(sb, component.Rect.Center.ToVector2(), targetAreaSize: component.Rect.Size.ToVector2()))
424  {
425  CanBeFocused = false
426  };
427 
428  GUILayoutGroup nameAndJobGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.4f - portraitWidth, 0.8f), mainGroup.RectTransform)) { CanBeFocused = false };
429  GUILayoutGroup nameGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), nameAndJobGroup.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { CanBeFocused = false };
430  GUITextBlock nameBlock = new GUITextBlock(new RectTransform(Vector2.One, nameGroup.RectTransform),
431  listBox == hireableList ? characterInfo.OriginalName : characterInfo.Name,
432  textColor: jobColor, textAlignment: Alignment.BottomLeft)
433  {
434  CanBeFocused = false
435  };
436  nameBlock.Text = ToolBox.LimitString(nameBlock.Text, nameBlock.Font, nameBlock.Rect.Width);
437 
438  GUITextBlock jobBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), nameAndJobGroup.RectTransform),
439  characterInfo.Title ?? characterInfo.Job.Name, textColor: Color.White, font: GUIStyle.SmallFont, textAlignment: Alignment.TopLeft)
440  {
441  CanBeFocused = false
442  };
443  if (!characterInfo.MinReputationToHire.factionId.IsEmpty)
444  {
445  var faction = campaign.Factions.Find(f => f.Prefab.Identifier == characterInfo.MinReputationToHire.factionId);
446  if (faction != null)
447  {
448  jobBlock.TextColor = faction.Prefab.IconColor;
449  }
450  }
451  var fullJobText = jobBlock.Text;
452  jobBlock.Text = ToolBox.LimitString(fullJobText, jobBlock.Font, jobBlock.Rect.Width);
453  if (jobBlock.Text != fullJobText)
454  {
455  jobBlock.ToolTip = fullJobText;
456  jobBlock.CanBeFocused = true;
457  }
458  float width = 0.6f / 3;
459  if (characterInfo.Job != null && skill != null)
460  {
461  GUILayoutGroup skillGroup = new GUILayoutGroup(new RectTransform(new Vector2(width, 0.6f), mainGroup.RectTransform), isHorizontal: true);
462  float iconWidth = (float)skillGroup.Rect.Height / skillGroup.Rect.Width;
463  GUIImage skillIcon = new GUIImage(new RectTransform(Vector2.One, skillGroup.RectTransform, scaleBasis: ScaleBasis.Smallest), skill.Icon, scaleToFit: true)
464  {
465  CanBeFocused = false
466  };
467  if (jobColor.HasValue) { skillIcon.Color = jobColor.Value; }
468  new GUITextBlock(new RectTransform(new Vector2(1.0f - iconWidth, 1.0f), skillGroup.RectTransform), ((int)skill.Level).ToString(), textAlignment: Alignment.CenterLeft)
469  {
470  CanBeFocused = false
471  };
472  }
473 
474  if (!hideSalary)
475  {
476  if (listBox != crewList)
477  {
478  new GUITextBlock(new RectTransform(new Vector2(width, 1.0f), mainGroup.RectTransform),
479  TextManager.FormatCurrency(ReplacingPermanentlyDeadCharacter ? campaign.NewCharacterCost(characterInfo) : HireManager.GetSalaryFor(characterInfo)),
480  textAlignment: Alignment.Center)
481  {
482  CanBeFocused = false
483  };
484  }
485  else
486  {
487  // Just a bit of padding to make list layouts similar
488  new GUIFrame(new RectTransform(new Vector2(width, 1.0f), mainGroup.RectTransform), style: null) { CanBeFocused = false };
489  }
490  }
491 
492  if (listBox == hireableList)
493  {
494  var hireButton = new GUIButton(new RectTransform(new Vector2(width, 0.9f), mainGroup.RectTransform), style: "CrewManagementAddButton")
495  {
496  ToolTip = TextManager.Get("hirebutton"),
497  ClickSound = GUISoundType.Cart,
498  UserData = characterInfo,
499  Enabled = CanHire(characterInfo) && !ReplacingPermanentlyDeadCharacter,
500  OnClicked = (b, o) => AddPendingHire(o as CharacterInfo)
501  };
502  hireButton.OnAddedToGUIUpdateList += (GUIComponent btn) =>
503  {
504  if (ReplacingPermanentlyDeadCharacter)
505  {
506  return;
507  }
508  if (PendingHires.Count + campaign.CrewManager.GetCharacterInfos().Count() >= CrewManager.MaxCrewSize)
509  {
510  if (btn.Enabled)
511  {
512  btn.ToolTip = TextManager.Get("canthiremorecharacters");
513  btn.Enabled = false;
514  }
515  }
516  else if (!btn.Enabled)
517  {
518  btn.ToolTip = string.Empty;
519  btn.Enabled = CanHire(characterInfo);
520  }
521  };
522 
523  if (ReplacingPermanentlyDeadCharacter)
524  {
525  bool canHire = CanHire(characterInfo) && campaign.CanAffordNewCharacter(characterInfo);
526  var takeoverButton = new GUIButton(new RectTransform(new Vector2(width, 0.9f), mainGroup.RectTransform), style: "CrewManagementTakeControlButton")
527  {
528  ToolTip = canHire ? TextManager.Get("hireandtakecontrol") : TextManager.Get("hireandtakecontroldisabled"),
529  ClickSound = GUISoundType.ConfirmTransaction,
530  UserData = characterInfo,
531  Enabled = canHire,
532  OnClicked = (b, o) =>
533  {
534  if (GameMain.Client is not GameClient gameClient)
535  {
536  return false;
537  }
538  Client client = gameClient.ConnectedClients.FirstOrDefault(c => c.SessionId == gameClient.SessionId);
539  if (!campaign.TryPurchase(client, campaign.NewCharacterCost(characterInfo)))
540  {
541  return false;
542  }
543  gameClient.SendTakeOverBotRequest(characterInfo);
544  needsHireableRefresh = true;
545  campaign.ShowCampaignUI = false;
546  return true;
547  }
548  };
549  takeoverButton.OnAddedToGUIUpdateList += (GUIComponent btn) =>
550  {
551  bool canHireCurrently = ReplacingPermanentlyDeadCharacter && CanHire(characterInfo) && campaign.CanAffordNewCharacter(characterInfo);
552  btn.ToolTip = TextManager.Get(canHireCurrently ? "hireandtakecontrol" : "hireandtakecontroldisabled");
553  btn.Visible = GameMain.GameSession is { AllowHrManagerBotTakeover: true };
554  btn.Enabled = canHireCurrently;
555  };
556  }
557  }
558  else if (listBox == pendingList)
559  {
560  new GUIButton(new RectTransform(new Vector2(width, 0.9f), mainGroup.RectTransform), style: "CrewManagementRemoveButton")
561  {
562  ClickSound = GUISoundType.Cart,
563  UserData = characterInfo,
564  Enabled = CanHire(characterInfo),
565  OnClicked = (b, o) => RemovePendingHire(o as CharacterInfo)
566  };
567  }
568  else if (listBox == crewList && campaign != null)
569  {
571  new GUIButton(new RectTransform(new Vector2(width, 0.9f), mainGroup.RectTransform), style: "CrewManagementFireButton")
572  {
573  UserData = characterInfo,
574  //can't fire if there's only one character in the crew
575  Enabled = currentCrew.Contains(characterInfo) && currentCrew.Count() > 1 && HasPermissionToHire,
576  OnClicked = (btn, obj) =>
577  {
578  var confirmDialog = new GUIMessageBox(
579  TextManager.Get("FireWarningHeader"),
580  TextManager.GetWithVariable("FireWarningText", "[charactername]", ((CharacterInfo)obj).Name),
581  new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") });
582  confirmDialog.Buttons[0].UserData = (CharacterInfo)obj;
583  confirmDialog.Buttons[0].OnClicked = FireCharacter;
584  confirmDialog.Buttons[0].OnClicked += confirmDialog.Close;
585  confirmDialog.Buttons[1].OnClicked = confirmDialog.Close;
586  return true;
587  }
588  };
589  }
590 
591  if (listBox == pendingList || listBox == crewList)
592  {
593  nameBlock.RectTransform.Resize(new Point(nameBlock.Rect.Width - nameBlock.Rect.Height, nameBlock.Rect.Height));
594  nameBlock.Text = ToolBox.LimitString(nameBlock.Text, nameBlock.Font, nameBlock.Rect.Width);
595  nameBlock.RectTransform.Resize(new Point((int)(nameBlock.Padding.X + nameBlock.TextSize.X + nameBlock.Padding.Z), nameBlock.Rect.Height));
596  Point size = new Point((int)(0.7f * nameBlock.Rect.Height));
597  new GUIImage(new RectTransform(size, nameGroup.RectTransform), "EditIcon") { CanBeFocused = false };
598  size = new Point(3 * mainGroup.AbsoluteSpacing + icon.Rect.Width + nameAndJobGroup.Rect.Width, mainGroup.Rect.Height);
599  new GUIButton(new RectTransform(size, frame.RectTransform) { RelativeOffset = new Vector2(0.025f) }, style: null)
600  {
601  Enabled = CanHire(characterInfo),
602  ToolTip = TextManager.GetWithVariable("campaigncrew.givenicknametooltip", "[mouseprimary]", PlayerInput.PrimaryMouseLabel),
603  UserData = characterInfo,
604  OnClicked = CreateRenamingComponent
605  };
606  }
607 
608  bool CanHire(CharacterInfo thisCharacterInfo)
609  {
610  if (!HasPermissionToHire) { return false; }
611  return EnoughReputationToHire(thisCharacterInfo);
612  }
613 
614  return frame;
615  }
616 
617  private bool EnoughReputationToHire(CharacterInfo characterInfo)
618  {
619  if (characterInfo.MinReputationToHire.factionId != Identifier.Empty)
620  {
621  if (MathF.Round(campaign.GetReputation(characterInfo.MinReputationToHire.factionId)) < characterInfo.MinReputationToHire.reputation)
622  {
623  return false;
624  }
625  }
626  return true;
627  }
628 
629  private void CreateCharacterPreviewFrame(GUIListBox listBox, GUIFrame characterFrame, CharacterInfo characterInfo)
630  {
631  Pivot pivot = listBox == hireableList ? Pivot.TopLeft : Pivot.TopRight;
632  Point absoluteOffset = new Point(
633  pivot == Pivot.TopLeft ? listBox.Parent.Parent.Rect.Right + 5 : listBox.Parent.Parent.Rect.Left - 5,
634  characterFrame.Rect.Top);
635  Point frameSize = new Point(GUI.IntScale(300), GUI.IntScale(350));
636  if (GameMain.GraphicsHeight - (absoluteOffset.Y + frameSize.Y) < 0)
637  {
638  pivot = listBox == hireableList ? Pivot.BottomLeft : Pivot.BottomRight;
639  absoluteOffset.Y = characterFrame.Rect.Bottom;
640  }
641  characterPreviewFrame = new GUIFrame(new RectTransform(frameSize, parent: campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).Parent.RectTransform, pivot: pivot)
642  {
643  AbsoluteOffset = absoluteOffset
644  }, style: "InnerFrame")
645  {
646  UserData = characterInfo
647  };
648  GUILayoutGroup mainGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.95f), characterPreviewFrame.RectTransform, anchor: Anchor.Center))
649  {
650  RelativeSpacing = 0.01f,
651  Stretch = true
652  };
653 
654  // Character info
655  GUILayoutGroup infoGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.475f), mainGroup.RectTransform), isHorizontal: true);
656  GUILayoutGroup infoLabelGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.4f, 1.0f), infoGroup.RectTransform)) { Stretch = true };
657  GUILayoutGroup infoValueGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.6f, 1.0f), infoGroup.RectTransform)) { Stretch = true };
658  float blockHeight = 1.0f / 4;
659  new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoLabelGroup.RectTransform), TextManager.Get("name"));
660  GUITextBlock nameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoValueGroup.RectTransform), "");
661  string name = listBox == hireableList ? characterInfo.OriginalName : characterInfo.Name;
662  nameBlock.Text = ToolBox.LimitString(name, nameBlock.Font, nameBlock.Rect.Width);
663 
664  if (characterInfo.HasSpecifierTags)
665  {
666  var menuCategoryVar = characterInfo.Prefab.MenuCategoryVar;
667  new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoLabelGroup.RectTransform), TextManager.Get(menuCategoryVar));
668  new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoValueGroup.RectTransform), TextManager.Get(characterInfo.ReplaceVars($"[{menuCategoryVar}]")));
669  }
670  if (characterInfo.Job is Job job)
671  {
672  new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoLabelGroup.RectTransform), TextManager.Get("tabmenu.job"));
673  new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoValueGroup.RectTransform), job.Name);
674  }
675  if (characterInfo.PersonalityTrait is NPCPersonalityTrait trait)
676  {
677  new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoLabelGroup.RectTransform), TextManager.Get("PersonalityTrait"));
678  new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoValueGroup.RectTransform), trait.DisplayName);
679  }
680  infoLabelGroup.Recalculate();
681  infoValueGroup.Recalculate();
682 
683  new GUIImage(new RectTransform(new Vector2(1.0f, 0.05f), mainGroup.RectTransform), "HorizontalLine");
684 
685  // Skills
686  GUILayoutGroup skillGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.475f), mainGroup.RectTransform), isHorizontal: true);
687  GUILayoutGroup skillNameGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 1.0f), skillGroup.RectTransform));
688  GUILayoutGroup skillLevelGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.2f, 1.0f), skillGroup.RectTransform));
689  var characterSkills = characterInfo.Job.GetSkills();
690  blockHeight = 1.0f / characterSkills.Count();
691  foreach (Skill skill in characterSkills)
692  {
693  new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), skillNameGroup.RectTransform), TextManager.Get("SkillName." + skill.Identifier), font: GUIStyle.SmallFont);
694  new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), skillLevelGroup.RectTransform), ((int)skill.Level).ToString(), textAlignment: Alignment.Right);
695  }
696 
697  if (characterInfo.MinReputationToHire.reputation > 0.0f)
698  {
699  var repStr = TextManager.GetWithVariables(
700  "campaignstore.reputationrequired",
701  ("[amount]", ((int)characterInfo.MinReputationToHire.reputation).ToString()),
702  ("[faction]", TextManager.Get("faction." + characterInfo.MinReputationToHire.factionId).Value));
703  new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), mainGroup.RectTransform),
704  repStr, textColor: !EnoughReputationToHire(characterInfo) ? GUIStyle.Orange : GUIStyle.Green,
705  font: GUIStyle.SmallFont, wrap: true, textAlignment: Alignment.Center);
706  }
707  mainGroup.Recalculate();
708  characterPreviewFrame.RectTransform.MinSize =
709  new Point(0, (int)(mainGroup.Children.Sum(c => c.Rect.Height + mainGroup.Rect.Height * mainGroup.RelativeSpacing) / mainGroup.RectTransform.RelativeSize.Y));
710  }
711 
712  private bool SelectCharacter(GUIListBox listBox, GUIFrame characterFrame, CharacterInfo characterInfo)
713  {
714  if (characterPreviewFrame != null && characterPreviewFrame.UserData != characterInfo)
715  {
716  characterPreviewFrame.Parent?.RemoveChild(characterPreviewFrame);
717  characterPreviewFrame = null;
718  }
719 
720  if (listBox == null || characterFrame == null || characterInfo == null) { return false; }
721 
722  if (characterPreviewFrame == null)
723  {
724  CreateCharacterPreviewFrame(listBox, characterFrame, characterInfo);
725  }
726 
727  return true;
728  }
729 
730  private bool AddPendingHire(CharacterInfo characterInfo, bool createNetworkMessage = true)
731  {
732  if (PendingHires.Count + campaign.CrewManager.GetCharacterInfos().Count() >= CrewManager.MaxCrewSize)
733  {
734  return false;
735  }
736 
737  hireableList.Content.RemoveChild(hireableList.Content.FindChild(c => ((InfoSkill)c.UserData).CharacterInfo == characterInfo));
738  hireableList.UpdateScrollBarSize();
739  if (!PendingHires.Contains(characterInfo)) { PendingHires.Add(characterInfo); }
740  CreateCharacterFrame(characterInfo, pendingList);
741  SortCharacters(pendingList, SortingMethod.JobAsc);
742  pendingList.UpdateScrollBarSize();
743  SetTotalHireCost();
744  if (createNetworkMessage) { SendCrewState(true); }
745  return true;
746  }
747 
748  private bool RemovePendingHire(CharacterInfo characterInfo, bool setTotalHireCost = true, bool createNetworkMessage = true)
749  {
750  if (PendingHires.Contains(characterInfo)) { PendingHires.Remove(characterInfo); }
751  pendingList.Content.RemoveChild(pendingList.Content.FindChild(c => ((InfoSkill)c.UserData).CharacterInfo == characterInfo));
752  pendingList.UpdateScrollBarSize();
753 
754  // Server will reset the names to originals in multiplayer
755  if (!GameMain.IsMultiplayer) { characterInfo?.ResetName(); }
756 
757  if (campaign.Map.CurrentLocation.HireManager.AvailableCharacters.Any(info => info.GetIdentifierUsingOriginalName() == characterInfo.GetIdentifierUsingOriginalName()) &&
758  hireableList.Content.Children.None(c => c.UserData is InfoSkill userData && userData.CharacterInfo.GetIdentifierUsingOriginalName() == characterInfo.GetIdentifierUsingOriginalName()))
759  {
760  CreateCharacterFrame(characterInfo, hireableList);
761  SortCharacters(hireableList, (SortingMethod)sortingDropDown.SelectedItemData);
762  hireableList.UpdateScrollBarSize();
763  }
764 
765  if (setTotalHireCost) { SetTotalHireCost(); }
766  if (createNetworkMessage) { SendCrewState(true); }
767  return true;
768  }
769 
770  private bool RemoveAllPendingHires(bool createNetworkMessage = true)
771  {
772  pendingList.Content.Children.ToList().ForEach(c => RemovePendingHire(((InfoSkill)c.UserData).CharacterInfo, setTotalHireCost: false, createNetworkMessage));
773  SetTotalHireCost();
774  return true;
775  }
776 
777  private void SetTotalHireCost()
778  {
779  if (pendingList == null || totalBlock == null || validateHiresButton == null) { return; }
780  var infos = pendingList.Content.Children.Select(static c => ((InfoSkill)c.UserData).CharacterInfo).ToArray();
781  int total = HireManager.GetSalaryFor(infos);
782  totalBlock.Text = TextManager.FormatCurrency(total);
783  bool enoughMoney = campaign == null || campaign.CanAfford(total);
784  totalBlock.TextColor = enoughMoney ? Color.White : Color.Red;
785  validateHiresButton.Enabled = enoughMoney && HasPermissionToHire && pendingList.Content.RectTransform.Children.Any();
786  }
787 
788  public bool ValidateHires(List<CharacterInfo> hires, bool takeMoney = true, bool createNetworkEvent = false, bool createNotification = true)
789  {
790  if (hires == null || hires.None()) { return false; }
791 
792  List<CharacterInfo> nonDuplicateHires = new List<CharacterInfo>();
793  hires.ForEach(hireInfo =>
794  {
795  if (campaign.CrewManager.GetCharacterInfos().None(crewInfo => crewInfo.IsNewHire && crewInfo.GetIdentifierUsingOriginalName() == hireInfo.GetIdentifierUsingOriginalName()))
796  {
797  nonDuplicateHires.Add(hireInfo);
798  }
799  });
800 
801  if (nonDuplicateHires.None()) { return false; }
802 
803  if (takeMoney)
804  {
805  int total = HireManager.GetSalaryFor(nonDuplicateHires);
806  if (!campaign.CanAfford(total)) { return false; }
807  }
808 
809  bool atLeastOneHired = false;
810  foreach (CharacterInfo ci in nonDuplicateHires)
811  {
812  if (campaign.TryHireCharacter(campaign.Map.CurrentLocation, ci, takeMoney: takeMoney))
813  {
814  atLeastOneHired = true;
815  }
816  else
817  {
818  break;
819  }
820  }
821 
822  if (atLeastOneHired)
823  {
824  UpdateLocationView(campaign.Map.CurrentLocation, true);
825  SelectCharacter(null, null, null);
826  if (createNotification)
827  {
828  var dialog = new GUIMessageBox(
829  TextManager.Get("newcrewmembers"),
830  TextManager.GetWithVariable("crewhiredmessage", "[location]", campaignUI?.Campaign?.Map?.CurrentLocation?.DisplayName),
831  new LocalizedString[] { TextManager.Get("Ok") });
832  dialog.Buttons[0].OnClicked += dialog.Close;
833  }
834  }
835 
836  if (createNetworkEvent)
837  {
838  SendCrewState(true, validateHires: true);
839  }
840 
841  return false;
842  }
843 
844  private bool CreateRenamingComponent(GUIButton button, object userData)
845  {
846  if (!HasPermissionToHire || userData is not CharacterInfo characterInfo) { return false; }
847  var outerGlowFrame = new GUIFrame(new RectTransform(new Vector2(1.25f, 1.25f), parentComponent.RectTransform, Anchor.Center),
848  style: "OuterGlow", color: Color.Black * 0.7f);
849  var frame = new GUIFrame(new RectTransform(new Vector2(0.33f, 0.4f), outerGlowFrame.RectTransform, anchor: Anchor.Center)
850  {
851  MaxSize = new Point(400, 300).Multiply(GUI.Scale)
852  });
853  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)
854  {
855  RelativeSpacing = 0.02f,
856  Stretch = true
857  };
858  new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), layoutGroup.RectTransform), TextManager.Get("campaigncrew.givenickname"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center, wrap: true);
859  var groupElementSize = new Vector2(1.0f, 0.25f);
860  var nameBox = new GUITextBox(new RectTransform(groupElementSize, layoutGroup.RectTransform))
861  {
862  MaxTextLength = Client.MaxNameLength
863  };
864  new GUIButton(new RectTransform(groupElementSize, layoutGroup.RectTransform), text: TextManager.Get("confirm"))
865  {
866  OnClicked = (button, userData) =>
867  {
868  if (RenameCharacter(characterInfo, nameBox.Text?.Trim()))
869  {
870  parentComponent.RemoveChild(outerGlowFrame);
871  return true;
872  }
873  else
874  {
875  nameBox.Flash(color: Color.Red);
876  return false;
877  }
878 
879  }
880  };
881  new GUIButton(new RectTransform(groupElementSize, layoutGroup.RectTransform), text: TextManager.Get("cancel"))
882  {
883  OnClicked = (button, userData) =>
884  {
885  parentComponent.RemoveChild(outerGlowFrame);
886  return true;
887  }
888  };
889  layoutGroup.Recalculate();
890  return true;
891  }
892 
893  public bool RenameCharacter(CharacterInfo characterInfo, string newName)
894  {
895  if (characterInfo == null || string.IsNullOrEmpty(newName)) { return false; }
896  if (newName == characterInfo.Name) { return false; }
898  {
899  SendCrewState(false, renameCharacter: (characterInfo, newName));
900  }
901  else
902  {
903  var crewComponent = crewList.Content.FindChild(c => ((InfoSkill)c.UserData).CharacterInfo == characterInfo);
904  if (crewComponent != null)
905  {
906  crewList.Content.RemoveChild(crewComponent);
907  campaign.CrewManager.RenameCharacter(characterInfo, newName);
908  CreateCharacterFrame(characterInfo, crewList);
909  SortCharacters(crewList, SortingMethod.JobAsc);
910  }
911  else
912  {
913  var pendingComponent = pendingList.Content.FindChild(c => ((InfoSkill)c.UserData).CharacterInfo == characterInfo);
914  if (pendingComponent != null)
915  {
916  pendingList.Content.RemoveChild(pendingComponent);
917  campaign.Map.CurrentLocation.HireManager.RenameCharacter(characterInfo, newName);
918  CreateCharacterFrame(characterInfo, pendingList);
919  SortCharacters(pendingList, SortingMethod.JobAsc);
920  SetTotalHireCost();
921  }
922  else
923  {
924  return false;
925  }
926  }
927  }
928  return true;
929  }
930 
931  private bool FireCharacter(GUIButton button, object selection)
932  {
933  if (selection is not CharacterInfo characterInfo) { return false; }
934 
935  campaign.CrewManager.FireCharacter(characterInfo);
936  SelectCharacter(null, null, null);
937  UpdateCrew();
938 
939  SendCrewState(false, firedCharacter: characterInfo);
940  return false;
941  }
942 
943  public void Update()
944  {
945  if (GameMain.GraphicsWidth != resolutionWhenCreated.X || GameMain.GraphicsHeight != resolutionWhenCreated.Y)
946  {
947  CreateUI();
948  UpdateLocationView(campaign.Map.CurrentLocation, false);
949  }
950  else
951  {
952  playerBalanceElement = CampaignUI.UpdateBalanceElement(playerBalanceElement);
953  }
954 
955  // When showing this window to someone hiring a new character, the right side panels aren't needed
956  pendingAndCrewPanel.Visible = !ReplacingPermanentlyDeadCharacter;
957 
958  if (hadPermissionToHire != HasPermissionToHire ||
959  wasReplacingPermanentlyDeadCharacter != ReplacingPermanentlyDeadCharacter)
960  {
961  RefreshUI();
962  }
963 
964  if (needsHireableRefresh)
965  {
966  RefreshCrewFrames(hireableList);
967  if (sortingDropDown?.SelectedItemData != null)
968  {
969  SortCharacters(hireableList, (SortingMethod)sortingDropDown.SelectedItemData);
970  }
971  needsHireableRefresh = false;
972  }
973 
974  (GUIComponent highlightedFrame, CharacterInfo highlightedInfo) = FindHighlightedCharacter(GUI.MouseOn);
975  if (highlightedFrame != null && highlightedInfo != null)
976  {
977  if (characterPreviewFrame == null || highlightedInfo != characterPreviewFrame.UserData)
978  {
979  GUIComponent component = GUI.MouseOn;
980  GUIListBox listBox = null;
981  do
982  {
983  if (component.Parent is GUIListBox)
984  {
985  listBox = component.Parent as GUIListBox;
986  break;
987  }
988  else if (component.Parent != null)
989  {
990  component = component.Parent;
991  }
992  else
993  {
994  break;
995  }
996  } while (listBox == null);
997 
998  if (listBox != null)
999  {
1000  SelectCharacter(listBox, highlightedFrame as GUIFrame, highlightedInfo);
1001  }
1002  }
1003  else
1004  {
1005  // TODO: Reposition the current preview panel if necessary
1006  // Could happen if we scroll a list while hovering?
1007  }
1008  }
1009  else if (characterPreviewFrame != null)
1010  {
1011  characterPreviewFrame.Parent?.RemoveChild(characterPreviewFrame);
1012  characterPreviewFrame = null;
1013  }
1014 
1015  static (GUIComponent GuiComponent, CharacterInfo CharacterInfo) FindHighlightedCharacter(GUIComponent c)
1016  {
1017  if (c == null)
1018  {
1019  return default;
1020  }
1021  if (c.UserData is InfoSkill highlightedData)
1022  {
1023  return (c, highlightedData.CharacterInfo);
1024  }
1025  if (c.Parent != null)
1026  {
1027  if (c.Parent is GUIListBox)
1028  {
1029  return default;
1030  }
1031  return FindHighlightedCharacter(c.Parent);
1032  }
1033  return default;
1034  }
1035  }
1036 
1037  public void SetPendingHires(List<UInt16> characterInfos, Location location)
1038  {
1039  List<CharacterInfo> oldHires = PendingHires.ToList();
1040  foreach (CharacterInfo pendingHire in oldHires)
1041  {
1042  RemovePendingHire(pendingHire, createNetworkMessage: false);
1043  }
1044  PendingHires.Clear();
1045  foreach (UInt16 identifier in characterInfos)
1046  {
1047  CharacterInfo match = location.HireManager.AvailableCharacters.Find(info => info.ID == identifier);
1048  if (match != null)
1049  {
1050  AddPendingHire(match, createNetworkMessage: false);
1051  System.Diagnostics.Debug.Assert(PendingHires.Contains(match));
1052  }
1053  else
1054  {
1055  DebugConsole.ThrowError("Received a hire that doesn't exist.");
1056  }
1057  }
1058  }
1059 
1067  public void SendCrewState(bool updatePending, (CharacterInfo info, string newName) renameCharacter = default, CharacterInfo firedCharacter = null, bool validateHires = false)
1068  {
1069  if (campaign is MultiPlayerCampaign)
1070  {
1071  IWriteMessage msg = new WriteOnlyMessage();
1072  msg.WriteByte((byte)ClientPacketHeader.CREW);
1073 
1074  msg.WriteBoolean(updatePending);
1075  if (updatePending)
1076  {
1077  msg.WriteUInt16((ushort)PendingHires.Count);
1078  foreach (CharacterInfo pendingHire in PendingHires)
1079  {
1080  msg.WriteUInt16(pendingHire.ID);
1081  }
1082  }
1083 
1084  msg.WriteBoolean(validateHires);
1085 
1086  bool validRenaming = renameCharacter.info != null && !string.IsNullOrEmpty(renameCharacter.newName);
1087  msg.WriteBoolean(validRenaming);
1088  if (validRenaming)
1089  {
1090  msg.WriteUInt16(renameCharacter.info.ID);
1091  msg.WriteString(renameCharacter.newName);
1092  bool existingCrewMember = campaign.CrewManager?.GetCharacterInfos().Any(ci => ci.ID == renameCharacter.info.ID) ?? false;
1093  msg.WriteBoolean(existingCrewMember);
1094  }
1095 
1096  msg.WriteBoolean(firedCharacter != null);
1097  if (firedCharacter != null)
1098  {
1099  msg.WriteUInt16(firedCharacter.ID);
1100  }
1101 
1102  GameMain.Client.ClientPeer?.Send(msg, DeliveryMethod.Reliable);
1103  }
1104  }
1105  }
1106 }
static bool AllowedToManageCampaign(ClientPermissions permissions)
There is a server-side implementation of the method in MultiPlayerCampaign
bool TryHireCharacter(Location location, CharacterInfo characterInfo, bool takeMoney=true, Client client=null, bool buyingNewCharacter=false)
static ? PlayerBalanceElement UpdateBalanceElement(PlayerBalanceElement? playerBalanceElement)
Definition: CampaignUI.cs:717
CampaignMode Campaign
Definition: CampaignUI.cs:40
Stores information about the Character that is needed between rounds in the menu etc....
CharacterInfo(Identifier speciesName, string name="", string originalName="", Either< Job, JobPrefab > jobOrJobPrefab=null, int variant=0, Rand.RandSync randSync=Rand.RandSync.Unsynced, Identifier npcIdentifier=default)
void DrawIcon(SpriteBatch spriteBatch, Vector2 screenPos, Vector2 targetAreaSize)
ushort ID
Unique ID given to character infos in MP. Non-persistent. Used by clients to identify which infos are...
Responsible for keeping track of the characters in the player crew, saving and loading their orders,...
IEnumerable< CharacterInfo > GetCharacterInfos()
Note: this only returns AI characters' infos in multiplayer. The infos are used to manage hiring/firi...
void RenameCharacter(CharacterInfo characterInfo, string newName)
override bool Enabled
Definition: GUIButton.cs:27
GUITextBlock TextBlock
Definition: GUIButton.cs:11
virtual void RemoveChild(GUIComponent child)
Definition: GUIComponent.cs:87
virtual void ClearChildren()
GUIComponent FindChild(Func< GUIComponent, bool > predicate, bool recursive=false)
Definition: GUIComponent.cs:95
virtual RichString ToolTip
virtual Rectangle Rect
RectTransform RectTransform
IEnumerable< GUIComponent > Children
Definition: GUIComponent.cs:29
GUIComponent that can be used to render custom content on the UI
GUIComponent AddItem(LocalizedString text, object userData=null, LocalizedString toolTip=null, Color? color=null, Color? textColor=null)
Definition: GUIDropDown.cs:247
override void RemoveChild(GUIComponent child)
Definition: GUIListBox.cs:1249
GUIFrame Content
A frame that contains the contents of the listbox. The frame itself is not rendered.
Definition: GUIListBox.cs:33
override GUIFont Font
Definition: GUITextBlock.cs:66
static int GraphicsWidth
Definition: GameMain.cs:162
static GameSession?? GameSession
Definition: GameMain.cs:88
static int GraphicsHeight
Definition: GameMain.cs:168
static bool IsMultiplayer
Definition: GameMain.cs:35
static NetworkMember NetworkMember
Definition: GameMain.cs:190
static GameClient Client
Definition: GameMain.cs:188
The "HR manager" UI, which is used to hire/fire characters and rename crewmates.
Definition: HRManagerUI.cs:15
GUIComponent CreateCharacterFrame(CharacterInfo characterInfo, GUIListBox listBox, bool hideSalary=false)
Definition: HRManagerUI.cs:402
bool RenameCharacter(CharacterInfo characterInfo, string newName)
Definition: HRManagerUI.cs:893
void SetPendingHires(List< UInt16 > characterInfos, Location location)
bool ValidateHires(List< CharacterInfo > hires, bool takeMoney=true, bool createNetworkEvent=false, bool createNotification=true)
Definition: HRManagerUI.cs:788
void SendCrewState(bool updatePending,(CharacterInfo info, string newName) renameCharacter=default, CharacterInfo firedCharacter=null, bool validateHires=false)
Notify the server of crew changes
void SetHireables(Location location, List< CharacterInfo > availableHires)
Definition: HRManagerUI.cs:316
HRManagerUI(CampaignUI campaignUI, GUIComponent parentComponent)
Definition: HRManagerUI.cs:61
static int GetSalaryFor(IReadOnlyCollection< CharacterInfo > hires)
Definition: HireManager.cs:24
List< CharacterInfo > PendingHires
Definition: HireManager.cs:10
void RenameCharacter(CharacterInfo characterInfo, string newName)
Definition: HireManager.cs:88
List< CharacterInfo > AvailableCharacters
Definition: HireManager.cs:9
Skill PrimarySkill
Definition: Job.cs:22
IEnumerable< Skill > GetSkills()
Definition: Job.cs:84
LocalizedString Name
Definition: Job.cs:14
JobPrefab Prefab
Definition: Job.cs:18
HireManager HireManager
Definition: Location.cs:535
IEnumerable< CharacterInfo > GetHireableCharacters()
Definition: Location.cs:1117
readonly NamedEvent< LocationChangeInfo > OnLocationChanged
From -> To
static readonly LocalizedString PrimaryMouseLabel
void Resize(Point absoluteSize, bool resizeChildren=true)
IEnumerable< RectTransform > Children
void SortChildren(Comparison< RectTransform > comparison)
Point?? MinSize
Min size in pixels. Does not affect scaling.
static readonly NamedEvent< Reputation > OnAnyReputationValueChanged
Definition: Reputation.cs:122
Sprite Icon
Definition: Skill.cs:37
float Level
Definition: Skill.cs:19
GUISoundType
Definition: GUI.cs:21