From e0f3bf0a0633292cf70ef10fff62ecf3978505dc Mon Sep 17 00:00:00 2001 From: Repo <47093363+Titian3@users.noreply.github.com> Date: Sun, 29 Oct 2023 01:01:11 +1300 Subject: [PATCH] User accessible playtime (#21242) Co-authored-by: metalgearsloth --- .../Info/PlaytimeStats/PlaytimeStatsEntry.cs | 39 +++++ .../PlaytimeStats/PlaytimeStatsEntry.xaml | 20 +++ .../Info/PlaytimeStats/PlaytimeStatsHeader.cs | 86 +++++++++++ .../PlaytimeStats/PlaytimeStatsHeader.xaml | 29 ++++ .../Info/PlaytimeStats/PlaytimeStatsWindow.cs | 146 ++++++++++++++++++ .../PlaytimeStats/PlaytimeStatsWindow.xaml | 25 +++ .../JobRequirementsManager.cs | 22 ++- .../Preferences/UI/CharacterSetupGui.xaml | 7 +- .../Preferences/UI/CharacterSetupGui.xaml.cs | 3 + .../Locale/en-US/info/playtime-stats.ftl | 10 ++ .../preferences/ui/character-setup-gui.ftl | 1 + 11 files changed, 384 insertions(+), 4 deletions(-) create mode 100644 Content.Client/Info/PlaytimeStats/PlaytimeStatsEntry.cs create mode 100644 Content.Client/Info/PlaytimeStats/PlaytimeStatsEntry.xaml create mode 100644 Content.Client/Info/PlaytimeStats/PlaytimeStatsHeader.cs create mode 100644 Content.Client/Info/PlaytimeStats/PlaytimeStatsHeader.xaml create mode 100644 Content.Client/Info/PlaytimeStats/PlaytimeStatsWindow.cs create mode 100644 Content.Client/Info/PlaytimeStats/PlaytimeStatsWindow.xaml create mode 100644 Resources/Locale/en-US/info/playtime-stats.ftl diff --git a/Content.Client/Info/PlaytimeStats/PlaytimeStatsEntry.cs b/Content.Client/Info/PlaytimeStats/PlaytimeStatsEntry.cs new file mode 100644 index 0000000000..aff01800f9 --- /dev/null +++ b/Content.Client/Info/PlaytimeStats/PlaytimeStatsEntry.cs @@ -0,0 +1,39 @@ +using Robust.Client.AutoGenerated; +using Robust.Client.Graphics; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client.Info.PlaytimeStats; + +[GenerateTypedNameReferences] +public sealed partial class PlaytimeStatsEntry : ContainerButton +{ + public TimeSpan Playtime { get; private set; } // new TimeSpan property + + public PlaytimeStatsEntry(string role, TimeSpan playtime, StyleBox styleBox) + { + RobustXamlLoader.Load(this); + + RoleLabel.Text = role; + Playtime = playtime; // store the TimeSpan value directly + PlaytimeLabel.Text = ConvertTimeSpanToHoursMinutes(playtime); // convert to string for display + BackgroundColorPanel.PanelOverride = styleBox; + } + + private static string ConvertTimeSpanToHoursMinutes(TimeSpan timeSpan) + { + var hours = (int)timeSpan.TotalHours; + var minutes = timeSpan.Minutes; + + var formattedTimeLoc = Loc.GetString("ui-playtime-time-format", ("hours", hours), ("minutes", minutes)); + return formattedTimeLoc; + } + + public void UpdateShading(StyleBoxFlat styleBox) + { + BackgroundColorPanel.PanelOverride = styleBox; + } + public string? PlaytimeText => PlaytimeLabel.Text; + + public string? RoleText => RoleLabel.Text; +} diff --git a/Content.Client/Info/PlaytimeStats/PlaytimeStatsEntry.xaml b/Content.Client/Info/PlaytimeStats/PlaytimeStatsEntry.xaml new file mode 100644 index 0000000000..97a66e5cc2 --- /dev/null +++ b/Content.Client/Info/PlaytimeStats/PlaytimeStatsEntry.xaml @@ -0,0 +1,20 @@ + + + + + diff --git a/Content.Client/Info/PlaytimeStats/PlaytimeStatsHeader.cs b/Content.Client/Info/PlaytimeStats/PlaytimeStatsHeader.cs new file mode 100644 index 0000000000..b005c641f7 --- /dev/null +++ b/Content.Client/Info/PlaytimeStats/PlaytimeStatsHeader.cs @@ -0,0 +1,86 @@ +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Input; + +namespace Content.Client.Info.PlaytimeStats; + +[GenerateTypedNameReferences] +public sealed partial class PlaytimeStatsHeader : ContainerButton +{ + public event Action? OnHeaderClicked; + private SortDirection _roleDirection = SortDirection.Ascending; + private SortDirection _playtimeDirection = SortDirection.Descending; + + public PlaytimeStatsHeader() + { + RobustXamlLoader.Load(this); + + RoleLabel.OnKeyBindDown += RoleClicked; + PlaytimeLabel.OnKeyBindDown += PlaytimeClicked; + + UpdateLabels(); + } + + public enum Header : byte + { + Role, + Playtime + } + public enum SortDirection : byte + { + Ascending, + Descending + } + + private void HeaderClicked(GUIBoundKeyEventArgs args, Header header) + { + if (args.Function != EngineKeyFunctions.UIClick) + { + return; + } + + switch (header) + { + case Header.Role: + _roleDirection = _roleDirection == SortDirection.Ascending ? SortDirection.Descending : SortDirection.Ascending; + break; + case Header.Playtime: + _playtimeDirection = _playtimeDirection == SortDirection.Ascending ? SortDirection.Descending : SortDirection.Ascending; + break; + } + + UpdateLabels(); + OnHeaderClicked?.Invoke(header, header == Header.Role ? _roleDirection : _playtimeDirection); + args.Handle(); + } + private void UpdateLabels() + { + RoleLabel.Text = Loc.GetString("ui-playtime-header-role-type") + + (_roleDirection == SortDirection.Ascending ? " ↓" : " ↑"); + PlaytimeLabel.Text = Loc.GetString("ui-playtime-header-role-time") + + (_playtimeDirection == SortDirection.Ascending ? " ↓" : " ↑"); + } + + private void RoleClicked(GUIBoundKeyEventArgs args) + { + HeaderClicked(args, Header.Role); + } + + private void PlaytimeClicked(GUIBoundKeyEventArgs args) + { + HeaderClicked(args, Header.Playtime); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing) + { + RoleLabel.OnKeyBindDown -= RoleClicked; + PlaytimeLabel.OnKeyBindDown -= PlaytimeClicked; + } + } +} diff --git a/Content.Client/Info/PlaytimeStats/PlaytimeStatsHeader.xaml b/Content.Client/Info/PlaytimeStats/PlaytimeStatsHeader.xaml new file mode 100644 index 0000000000..4cf4d8e2cc --- /dev/null +++ b/Content.Client/Info/PlaytimeStats/PlaytimeStatsHeader.xaml @@ -0,0 +1,29 @@ + + + + + + + + + diff --git a/Content.Client/Info/PlaytimeStats/PlaytimeStatsWindow.cs b/Content.Client/Info/PlaytimeStats/PlaytimeStatsWindow.cs new file mode 100644 index 0000000000..3b54bf82da --- /dev/null +++ b/Content.Client/Info/PlaytimeStats/PlaytimeStatsWindow.cs @@ -0,0 +1,146 @@ +using System.Linq; +using System.Text.RegularExpressions; +using Content.Client.Players.PlayTimeTracking; +using Content.Client.UserInterface.Controls; +using Robust.Client.AutoGenerated; +using Robust.Client.Graphics; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client.Info.PlaytimeStats; + +[GenerateTypedNameReferences] +public sealed partial class PlaytimeStatsWindow : FancyWindow +{ + [Dependency] private readonly JobRequirementsManager _jobRequirementsManager = default!; + private ISawmill _sawmill = Logger.GetSawmill("PlaytimeStatsWindow"); + private readonly Color _altColor = Color.FromHex("#292B38"); + private readonly Color _defaultColor = Color.FromHex("#2F2F3B"); + private bool _useAltColor; + + public PlaytimeStatsWindow() + { + IoCManager.InjectDependencies(this); + RobustXamlLoader.Load(this); + + PopulatePlaytimeHeader(); + PopulatePlaytimeData(); + } + + private void PopulatePlaytimeHeader() + { + var header = new PlaytimeStatsHeader(); + header.OnHeaderClicked += HeaderClicked; + header.BackgroundColorPlaytimePanel.PanelOverride = new StyleBoxFlat(_altColor); + RolesPlaytimeList.AddChild(header); + } + + private void HeaderClicked(PlaytimeStatsHeader.Header header, PlaytimeStatsHeader.SortDirection direction) + { + switch (header) + { + case PlaytimeStatsHeader.Header.Role: + SortByRole(direction); + break; + case PlaytimeStatsHeader.Header.Playtime: + SortByPlaytime(direction); + break; + } + } + + private void SortByRole(PlaytimeStatsHeader.SortDirection direction) + { + var header = RolesPlaytimeList.GetChild(0) as PlaytimeStatsHeader; + + var entries = RolesPlaytimeList.Children.OfType().ToList(); + + RolesPlaytimeList.RemoveAllChildren(); + + if (header != null) + RolesPlaytimeList.AddChild(header); + + var sortedEntries = (direction == PlaytimeStatsHeader.SortDirection.Ascending) + ? entries.OrderBy(entry => entry.RoleText).ToList() + : entries.OrderByDescending(entry => entry.RoleText).ToList(); + + _useAltColor = false; + + foreach (var entry in sortedEntries) + { + var styleBox = new StyleBoxFlat { BackgroundColor = _useAltColor ? _altColor : _defaultColor }; + entry.UpdateShading(styleBox); + RolesPlaytimeList.AddChild(entry); + _useAltColor ^= true; + } + } + + private void SortByPlaytime(PlaytimeStatsHeader.SortDirection direction) + { + var header = RolesPlaytimeList.GetChild(0) as PlaytimeStatsHeader; + + var entries = RolesPlaytimeList.Children.OfType().ToList(); + + RolesPlaytimeList.RemoveAllChildren(); + + if (header != null) + RolesPlaytimeList.AddChild(header); + + var sortedEntries = (direction == PlaytimeStatsHeader.SortDirection.Ascending) + ? entries.OrderBy(entry => entry.Playtime).ToList() + : entries.OrderByDescending(entry => entry.Playtime).ToList(); + + _useAltColor = false; + + foreach (var entry in sortedEntries) + { + var styleBox = new StyleBoxFlat { BackgroundColor = _useAltColor ? _altColor : _defaultColor }; + entry.UpdateShading(styleBox); + RolesPlaytimeList.AddChild(entry); + _useAltColor ^= true; + } + } + + + private void PopulatePlaytimeData() + { + var overallPlaytime = _jobRequirementsManager.FetchOverallPlaytime(); + + var formattedPlaytime = ConvertTimeSpanToHoursMinutes(overallPlaytime); + OverallPlaytimeLabel.Text = Loc.GetString("ui-playtime-overall", ("time", formattedPlaytime)); + + var rolePlaytimes = _jobRequirementsManager.FetchPlaytimeByRoles(); + + RolesPlaytimeList.RemoveAllChildren(); + PopulatePlaytimeHeader(); + + foreach (var rolePlaytime in rolePlaytimes) + { + var role = rolePlaytime.Key; + var playtime = rolePlaytime.Value; + AddRolePlaytimeEntryToTable(Loc.GetString(role), playtime.ToString()); + } + } + + private void AddRolePlaytimeEntryToTable(string role, string playtimeString) + { + if (TimeSpan.TryParse(playtimeString, out var playtime)) + { + var entry = new PlaytimeStatsEntry(role, playtime, + new StyleBoxFlat(_useAltColor ? _altColor : _defaultColor)); + RolesPlaytimeList.AddChild(entry); + _useAltColor ^= true; + } + else + { + _sawmill.Error($"The provided playtime string '{playtimeString}' is not in the correct format."); + } + } + + private static string ConvertTimeSpanToHoursMinutes(TimeSpan timeSpan) + { + var hours = (int) timeSpan.TotalHours; + var minutes = timeSpan.Minutes; + + var formattedTimeLoc = Loc.GetString("ui-playtime-time-format", ("hours", hours), ("minutes", minutes)); + return formattedTimeLoc; + } +} diff --git a/Content.Client/Info/PlaytimeStats/PlaytimeStatsWindow.xaml b/Content.Client/Info/PlaytimeStats/PlaytimeStatsWindow.xaml new file mode 100644 index 0000000000..b38394d9a3 --- /dev/null +++ b/Content.Client/Info/PlaytimeStats/PlaytimeStatsWindow.xaml @@ -0,0 +1,25 @@ + + + + + + + + diff --git a/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs b/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs index c67c759dfd..0929b90a41 100644 --- a/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs +++ b/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs @@ -1,6 +1,4 @@ using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Text; using Content.Shared.CCVar; using Content.Shared.Players; using Content.Shared.Players.PlayTimeTracking; @@ -122,4 +120,24 @@ public sealed class JobRequirementsManager reason = reasons.Count == 0 ? null : FormattedMessage.FromMarkup(string.Join('\n', reasons)); return reason == null; } + + public TimeSpan FetchOverallPlaytime() + { + return _roles.TryGetValue("Overall", out var overallPlaytime) ? overallPlaytime : TimeSpan.Zero; + } + + public IEnumerable> FetchPlaytimeByRoles() + { + var jobsToMap = _prototypes.EnumeratePrototypes(); + + foreach (var job in jobsToMap) + { + if (_roles.TryGetValue(job.PlayTimeTracker, out var locJobName)) + { + yield return new KeyValuePair(job.Name, locJobName); + } + } + } + + } diff --git a/Content.Client/Preferences/UI/CharacterSetupGui.xaml b/Content.Client/Preferences/UI/CharacterSetupGui.xaml index 5db8610475..9a76029ce0 100644 --- a/Content.Client/Preferences/UI/CharacterSetupGui.xaml +++ b/Content.Client/Preferences/UI/CharacterSetupGui.xaml @@ -10,10 +10,13 @@