diff --git a/Content.Client/Lobby/UI/HumanoidProfileEditor.Appearance.cs b/Content.Client/Lobby/UI/HumanoidProfileEditor.Appearance.cs new file mode 100644 index 0000000000..ddc9752b1e --- /dev/null +++ b/Content.Client/Lobby/UI/HumanoidProfileEditor.Appearance.cs @@ -0,0 +1,299 @@ +using System.Linq; +using Content.Client.UserInterface.Systems.Guidebook; +using Content.Shared.Guidebook; +using Content.Shared.Humanoid; +using Content.Shared.Humanoid.Prototypes; +using Content.Shared.Preferences; +using Robust.Client.UserInterface.Controls; +using Robust.Shared.Enums; +using Robust.Shared.Prototypes; + +namespace Content.Client.Lobby.UI; + +public sealed partial class HumanoidProfileEditor +{ + public event Action>>? OnOpenGuidebook; + + private ColorSelectorSliders _rgbSkinColorSelector; + private List _species = new(); + private static readonly ProtoId DefaultSpeciesGuidebook = "Species"; + + public void UpdateSpeciesGuidebookIcon() + { + SpeciesInfoButton.StyleClasses.Clear(); + + var species = Profile?.Species; + if (species is null) + return; + + if (!_prototypeManager.Resolve(species, out var speciesProto)) + return; + + // Don't display the info button if no guide entry is found + if (!_prototypeManager.HasIndex(species)) + return; + + const string style = "SpeciesInfoDefault"; + SpeciesInfoButton.StyleIdentifier = style; + } + + private void UpdateGenderControls() + { + if (Profile == null) + { + return; + } + + PronounsButton.SelectId((int)Profile.Gender); + } + + private void UpdateAgeEdit() + { + AgeEdit.Text = Profile?.Age.ToString() ?? ""; + } + + private void UpdateSexControls() + { + if (Profile == null) + return; + + SexButton.Clear(); + + var sexes = new List(); + + // add species sex options, default to just none if we are in bizzaro world and have no species + if (_prototypeManager.Resolve(Profile.Species, out var speciesProto)) + { + foreach (var sex in speciesProto.Sexes) + { + sexes.Add(sex); + } + } + else + { + sexes.Add(Sex.Unsexed); + } + + // add button for each sex + foreach (var sex in sexes) + { + SexButton.AddItem(Loc.GetString($"humanoid-profile-editor-sex-{sex.ToString().ToLower()}-text"), (int)sex); + } + + if (sexes.Contains(Profile.Sex)) + SexButton.SelectId((int)Profile.Sex); + else + SexButton.SelectId((int)sexes[0]); + } + + private void UpdateEyePickers() + { + if (Profile == null) + { + return; + } + + _markingsModel.SetOrganEyeColor(Profile.Appearance.EyeColor); + EyeColorPicker.SetData(Profile.Appearance.EyeColor); + } + + private void UpdateSkinColor() + { + if (Profile == null) + return; + + var skin = _prototypeManager.Index(Profile.Species).SkinColoration; + var strategy = _prototypeManager.Index(skin).Strategy; + + switch (strategy.InputType) + { + case SkinColorationStrategyInput.Unary: + { + if (!Skin.Visible) + { + Skin.Visible = true; + RgbSkinColorContainer.Visible = false; + } + + Skin.Value = strategy.ToUnary(Profile.Appearance.SkinColor); + + break; + } + case SkinColorationStrategyInput.Color: + { + if (!RgbSkinColorContainer.Visible) + { + Skin.Visible = false; + RgbSkinColorContainer.Visible = true; + } + + _rgbSkinColorSelector.Color = strategy.ClosestSkinColor(Profile.Appearance.SkinColor); + + break; + } + } + } + + private void UpdateSpawnPriorityControls() + { + if (Profile == null) + { + return; + } + + SpawnPriorityButton.SelectId((int)Profile.SpawnPriority); + } + + /// + /// Refreshes the species selector. + /// + public void RefreshSpecies() + { + SpeciesButton.Clear(); + _species.Clear(); + + _species.AddRange(_prototypeManager.EnumeratePrototypes().Where(o => o.RoundStart)); + _species.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.CurrentCultureIgnoreCase)); + var speciesIds = _species.Select(o => o.ID).ToList(); + + for (var i = 0; i < _species.Count; i++) + { + var name = Loc.GetString(_species[i].Name); + SpeciesButton.AddItem(name, i); + + if (Profile?.Species.Equals(_species[i].ID) == true) + { + SpeciesButton.SelectId(i); + } + } + + // If our species isn't available then reset it to default. + if (Profile != null) + { + if (!speciesIds.Contains(Profile.Species)) + { + SetSpecies(HumanoidCharacterProfile.DefaultSpecies); + } + } + } + + private void SetSpecies(string newSpecies) + { + Profile = Profile?.WithSpecies(newSpecies); + OnSkinColorOnValueChanged(); // Species may have special color prefs, make sure to update it. + _markingsModel.OrganData = _markingManager.GetMarkingData(newSpecies); + _markingsModel.ValidateMarkings(); + // In case there's job restrictions for the species + RefreshJobs(); + // In case there's species restrictions for loadouts + RefreshLoadouts(); + UpdateSexControls(); // update sex for new species + UpdateSpeciesGuidebookIcon(); + ReloadPreview(); + } + + private void SetAge(int newAge) + { + Profile = Profile?.WithAge(newAge); + ReloadPreview(); + } + + private void SetSex(Sex newSex) + { + Profile = Profile?.WithSex(newSex); + // for convenience, default to most common gender when new sex is selected + switch (newSex) + { + case Sex.Male: + Profile = Profile?.WithGender(Gender.Male); + break; + case Sex.Female: + Profile = Profile?.WithGender(Gender.Female); + break; + default: + Profile = Profile?.WithGender(Gender.Epicene); + break; + } + + UpdateGenderControls(); + _markingsModel.SetOrganSexes(newSex); + ReloadPreview(); + } + + private void SetGender(Gender newGender) + { + Profile = Profile?.WithGender(newGender); + ReloadPreview(); + } + + private void SetSpawnPriority(SpawnPriorityPreference newSpawnPriority) + { + Profile = Profile?.WithSpawnPriorityPreference(newSpawnPriority); + SetDirty(); + } + + private void OnSpeciesInfoButtonPressed(BaseButton.ButtonEventArgs args) + { + // TODO GUIDEBOOK + // make the species guide book a field on the species prototype. + // I.e., do what jobs/antags do. + + var guidebookController = UserInterfaceManager.GetUIController(); + var species = Profile?.Species ?? HumanoidCharacterProfile.DefaultSpecies; + var page = DefaultSpeciesGuidebook; + if (_prototypeManager.HasIndex(species)) + page = new ProtoId(species.Id); // Gross. See above todo comment. + + if (_prototypeManager.Resolve(DefaultSpeciesGuidebook, out var guideRoot)) + { + var dict = new Dictionary, GuideEntry>(); + dict.Add(DefaultSpeciesGuidebook, guideRoot); + //TODO: Don't close the guidebook if its already open, just go to the correct page + guidebookController.OpenGuidebook(dict, includeChildren: true, selected: page); + } + } + + private void OnSkinColorOnValueChanged() + { + if (Profile is null) return; + + var skin = _prototypeManager.Index(Profile.Species).SkinColoration; + var strategy = _prototypeManager.Index(skin).Strategy; + + switch (strategy.InputType) + { + case SkinColorationStrategyInput.Unary: + { + if (!Skin.Visible) + { + Skin.Visible = true; + RgbSkinColorContainer.Visible = false; + } + + var color = strategy.FromUnary(Skin.Value); + + _markingsModel.SetOrganSkinColor(color); + Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color)); + + break; + } + case SkinColorationStrategyInput.Color: + { + if (!RgbSkinColorContainer.Visible) + { + Skin.Visible = false; + RgbSkinColorContainer.Visible = true; + } + + var color = strategy.ClosestSkinColor(_rgbSkinColorSelector.Color); + + _markingsModel.SetOrganSkinColor(color); + Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color)); + + break; + } + } + + ReloadProfilePreview(); + } +} diff --git a/Content.Client/Lobby/UI/HumanoidProfileEditor.BasicInfo.cs b/Content.Client/Lobby/UI/HumanoidProfileEditor.BasicInfo.cs new file mode 100644 index 0000000000..b5ce8d0d6d --- /dev/null +++ b/Content.Client/Lobby/UI/HumanoidProfileEditor.BasicInfo.cs @@ -0,0 +1,38 @@ + +using Content.Shared.Preferences; + +namespace Content.Client.Lobby.UI; + +public sealed partial class HumanoidProfileEditor +{ + private void SetName(string newName) + { + Profile = Profile?.WithName(newName); + SetDirty(); + + if (!IsDirty) + return; + + SpriteView.SetName(newName); + } + + private void UpdateNameEdit() + { + NameEdit.Text = Profile?.Name ?? ""; + } + + private void RandomizeEverything() + { + Profile = HumanoidCharacterProfile.Random(); + SetProfile(Profile, CharacterSlot); + SetDirty(); + } + + private void RandomizeName() + { + if (Profile == null) return; + var name = HumanoidCharacterProfile.GetName(Profile.Species, Profile.Gender); + SetName(name); + UpdateNameEdit(); + } +} diff --git a/Content.Client/Lobby/UI/HumanoidProfileEditor.Exports.cs b/Content.Client/Lobby/UI/HumanoidProfileEditor.Exports.cs new file mode 100644 index 0000000000..724a353dad --- /dev/null +++ b/Content.Client/Lobby/UI/HumanoidProfileEditor.Exports.cs @@ -0,0 +1,102 @@ +using System.IO; +using Content.Client.Sprite; +using Content.Shared.Preferences; +using Robust.Client.UserInterface; + +namespace Content.Client.Lobby.UI; + +public sealed partial class HumanoidProfileEditor +{ + private bool _exporting; + private bool _imaging; + + private async void ExportImage() + { + if (_imaging) + return; + + var dir = SpriteView.OverrideDirection ?? Direction.South; + + // I tried disabling the button but it looks sorta goofy as it only takes a frame or two to save + _imaging = true; + await _entManager.System().Export(SpriteView.PreviewDummy, dir, includeId: false); + _imaging = false; + } + + private async void ImportProfile() + { + if (_exporting || CharacterSlot == null || Profile == null) + return; + + StartExport(); + await using var file = await _dialogManager.OpenFile(new FileDialogFilters(new FileDialogFilters.Group("yml")), FileAccess.Read); + + if (file == null) + { + EndExport(); + return; + } + + try + { + var profile = HumanoidCharacterProfile.FromStream(file, _playerManager.LocalSession!); + var oldProfile = Profile; + SetProfile(profile, CharacterSlot); + + IsDirty = !profile.MemberwiseEquals(oldProfile); + } + catch (Exception exc) + { + _sawmill.Error($"Error when importing profile\n{exc.StackTrace}"); + } + finally + { + EndExport(); + } + } + + private async void ExportProfile() + { + if (Profile == null || _exporting) + return; + + StartExport(); + var file = await _dialogManager.SaveFile(new FileDialogFilters(new FileDialogFilters.Group("yml"))); + + if (file == null) + { + EndExport(); + return; + } + + try + { + var dataNode = Profile.ToDataNode(); + await using var writer = new StreamWriter(file.Value.fileStream); + dataNode.Write(writer); + } + catch (Exception exc) + { + _sawmill.Error($"Error when exporting profile\n{exc.StackTrace}"); + } + finally + { + EndExport(); + await file.Value.fileStream.DisposeAsync(); + } + } + + private void StartExport() + { + _exporting = true; + ImportButton.Disabled = true; + ExportButton.Disabled = true; + } + + private void EndExport() + { + _exporting = false; + ImportButton.Disabled = false; + ExportButton.Disabled = false; + } +} diff --git a/Content.Client/Lobby/UI/HumanoidProfileEditor.FlavorText.cs b/Content.Client/Lobby/UI/HumanoidProfileEditor.FlavorText.cs new file mode 100644 index 0000000000..bcdeadff66 --- /dev/null +++ b/Content.Client/Lobby/UI/HumanoidProfileEditor.FlavorText.cs @@ -0,0 +1,60 @@ +using Robust.Client.UserInterface.Controls; +using Robust.Shared.Utility; + +namespace Content.Client.Lobby.UI; + +public sealed partial class HumanoidProfileEditor +{ + private bool _allowFlavorText; + + private FlavorText.FlavorText? _flavorText; + private TextEdit? _flavorTextEdit; + + /// + /// Refreshes the flavor text editor status. + /// + public void RefreshFlavorText() + { + if (_allowFlavorText) + { + if (_flavorText != null) + return; + + _flavorText = new FlavorText.FlavorText(); + TabContainer.AddChild(_flavorText); + TabContainer.SetTabTitle(TabContainer.ChildCount - 1, Loc.GetString("humanoid-profile-editor-flavortext-tab")); + _flavorTextEdit = _flavorText.CFlavorTextInput; + + _flavorText.OnFlavorTextChanged += OnFlavorTextChange; + } + else + { + if (_flavorText == null) + return; + + TabContainer.RemoveChild(_flavorText); + _flavorText.OnFlavorTextChanged -= OnFlavorTextChange; + _flavorText.Dispose(); + _flavorTextEdit?.Dispose(); + _flavorTextEdit = null; + _flavorText = null; + } + } + + private void OnFlavorTextChange(string content) + { + if (Profile is null) + return; + + Profile = Profile.WithFlavorText(content); + SetDirty(); + } + + private void UpdateFlavorTextEdit() + { + if (_flavorTextEdit != null) + { + _flavorTextEdit.TextRope = new Rope.Leaf(Profile?.FlavorText ?? ""); + } + } +} diff --git a/Content.Client/Lobby/UI/HumanoidProfileEditor.Markings.cs b/Content.Client/Lobby/UI/HumanoidProfileEditor.Markings.cs new file mode 100644 index 0000000000..29f66cd107 --- /dev/null +++ b/Content.Client/Lobby/UI/HumanoidProfileEditor.Markings.cs @@ -0,0 +1,26 @@ +namespace Content.Client.Lobby.UI; + +public sealed partial class HumanoidProfileEditor +{ + private void UpdateMarkings() + { + if (Profile == null) + { + return; + } + + _markingsModel.OrganData = _markingManager.GetMarkingData(Profile.Species); + _markingsModel.OrganProfileData = _markingManager.GetProfileData(Profile.Species, Profile.Sex, Profile.Appearance.SkinColor, Profile.Appearance.EyeColor); + _markingsModel.Markings = Profile.Appearance.Markings; + } + + private void OnMarkingChange() + { + if (Profile is null) + return; + + Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithMarkings(_markingsModel.Markings)); + ReloadProfilePreview(); + SetDirty(); + } +} diff --git a/Content.Client/Lobby/UI/HumanoidProfileEditor.Roles.cs b/Content.Client/Lobby/UI/HumanoidProfileEditor.Roles.cs new file mode 100644 index 0000000000..f980c267fb --- /dev/null +++ b/Content.Client/Lobby/UI/HumanoidProfileEditor.Roles.cs @@ -0,0 +1,360 @@ +using System.Linq; +using System.Numerics; +using Content.Client.Lobby.UI.Loadouts; +using Content.Client.Lobby.UI.Roles; +using Content.Shared.Clothing; +using Content.Shared.Preferences; +using Content.Shared.Preferences.Loadouts; +using Content.Shared.Roles; +using Robust.Client.Graphics; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Shared.Prototypes; + +namespace Content.Client.Lobby.UI; + +public sealed partial class HumanoidProfileEditor +{ + + /// + /// Temporary override of their selected job, used to preview roles. + /// + public JobPrototype? JobOverride; + + // One at a time. + private LoadoutWindow? _loadoutWindow; + + private List<(string, RequirementsSelector)> _jobPriorities = new(); + + private readonly Dictionary _jobCategories; + + /// + /// Updates selected job priorities to the profile's. + /// + private void UpdateJobPriorities() + { + foreach (var (jobId, prioritySelector) in _jobPriorities) + { + var priority = Profile?.JobPriorities.GetValueOrDefault(jobId, JobPriority.Never) ?? JobPriority.Never; + prioritySelector.Select((int)priority); + } + } + + /// + /// Refresh all loadouts. + /// + public void RefreshLoadouts() + { + _loadoutWindow?.Dispose(); + } + + private void OpenLoadout(JobPrototype? jobProto, RoleLoadout roleLoadout, RoleLoadoutPrototype roleLoadoutProto) + { + _loadoutWindow?.Dispose(); + _loadoutWindow = null; + var collection = IoCManager.Instance; + + if (collection == null || _playerManager.LocalSession == null || Profile == null) + return; + + JobOverride = jobProto; + var session = _playerManager.LocalSession; + + _loadoutWindow = new LoadoutWindow(Profile, roleLoadout, roleLoadoutProto, _playerManager.LocalSession, collection) + { + Title = Loc.GetString("loadout-window-title-loadout", ("job", $"{jobProto?.LocalizedName}")), + }; + + // Refresh the buttons etc. + _loadoutWindow.RefreshLoadouts(roleLoadout, session, collection); + _loadoutWindow.OpenCenteredLeft(); + + _loadoutWindow.OnNameChanged += name => + { + roleLoadout.EntityName = name; + Profile = Profile.WithLoadout(roleLoadout); + SetDirty(); + }; + + _loadoutWindow.OnLoadoutPressed += (loadoutGroup, loadoutProto) => + { + roleLoadout.AddLoadout(loadoutGroup, loadoutProto, _prototypeManager); + _loadoutWindow.RefreshLoadouts(roleLoadout, session, collection); + Profile = Profile?.WithLoadout(roleLoadout); + ReloadPreview(); + }; + + _loadoutWindow.OnLoadoutUnpressed += (loadoutGroup, loadoutProto) => + { + roleLoadout.RemoveLoadout(loadoutGroup, loadoutProto, _prototypeManager); + _loadoutWindow.RefreshLoadouts(roleLoadout, session, collection); + Profile = Profile?.WithLoadout(roleLoadout); + ReloadPreview(); + }; + + JobOverride = jobProto; + ReloadPreview(); + + _loadoutWindow.OnClose += () => + { + JobOverride = null; + ReloadPreview(); + }; + + if (Profile is null) + return; + + UpdateJobPriorities(); + } + + /// + /// Refreshes all job selectors. + /// + public void RefreshJobs() + { + JobList.RemoveAllChildren(); + _jobCategories.Clear(); + _jobPriorities.Clear(); + var firstCategory = true; + + // Get all displayed departments + var departments = new List(); + foreach (var department in _prototypeManager.EnumeratePrototypes()) + { + if (department.EditorHidden) + continue; + + departments.Add(department); + } + + departments.Sort(DepartmentUIComparer.Instance); + + var items = new[] + { + ("humanoid-profile-editor-job-priority-never-button", (int) JobPriority.Never), + ("humanoid-profile-editor-job-priority-low-button", (int) JobPriority.Low), + ("humanoid-profile-editor-job-priority-medium-button", (int) JobPriority.Medium), + ("humanoid-profile-editor-job-priority-high-button", (int) JobPriority.High), + }; + + foreach (var department in departments) + { + var departmentName = Loc.GetString(department.Name); + + if (!_jobCategories.TryGetValue(department.ID, out var category)) + { + category = new BoxContainer + { + Orientation = LayoutOrientation.Vertical, + Name = department.ID, + ToolTip = Loc.GetString("humanoid-profile-editor-jobs-amount-in-department-tooltip", + ("departmentName", departmentName)) + }; + + if (firstCategory) + { + firstCategory = false; + } + else + { + category.AddChild(new Control + { + MinSize = new Vector2(0, 23), + }); + } + + category.AddChild(new PanelContainer + { + PanelOverride = new StyleBoxFlat { BackgroundColor = Color.FromHex("#464966") }, + Children = + { + new Label + { + Text = Loc.GetString("humanoid-profile-editor-department-jobs-label", + ("departmentName", departmentName)), + Margin = new Thickness(5f, 0, 0, 0) + } + } + }); + + _jobCategories[department.ID] = category; + JobList.AddChild(category); + } + + var jobs = department.Roles.Select(jobId => _prototypeManager.Index(jobId)) + .Where(job => job.SetPreference) + .ToArray(); + + Array.Sort(jobs, JobUIComparer.Instance); + + foreach (var job in jobs) + { + var jobContainer = new BoxContainer() + { + Orientation = LayoutOrientation.Horizontal, + }; + + var selector = new RequirementsSelector() + { + Margin = new Thickness(3f, 3f, 3f, 0f), + }; + selector.OnOpenGuidebook += OnOpenGuidebook; + + var icon = new TextureRect + { + TextureScale = new Vector2(2, 2), + VerticalAlignment = VAlignment.Center + }; + var jobIcon = _prototypeManager.Index(job.Icon); + icon.Texture = _sprite.Frame0(jobIcon.Icon); + selector.Setup(items, job.LocalizedName, 200, job.LocalizedDescription, icon, job.Guides); + + if (!_requirements.IsAllowed(job, (HumanoidCharacterProfile?)_preferencesManager.Preferences?.SelectedCharacter, out var reason)) + { + selector.LockRequirements(reason); + } + else + { + selector.UnlockRequirements(); + } + + selector.OnSelected += selectedPrio => + { + var selectedJobPrio = (JobPriority)selectedPrio; + Profile = Profile?.WithJobPriority(job.ID, selectedJobPrio); + + foreach (var (jobId, other) in _jobPriorities) + { + // Sync other selectors with the same job in case of multiple department jobs + if (jobId == job.ID) + { + other.Select(selectedPrio); + continue; + } + + if (selectedJobPrio != JobPriority.High || (JobPriority)other.Selected != JobPriority.High) + continue; + + // Lower any other high priorities to medium. + other.Select((int)JobPriority.Medium); + Profile = Profile?.WithJobPriority(jobId, JobPriority.Medium); + } + + // TODO: Only reload on high change (either to or from). + ReloadPreview(); + + UpdateJobPriorities(); + SetDirty(); + }; + + var loadoutWindowBtn = new Button() + { + Text = Loc.GetString("loadout-window"), + HorizontalAlignment = HAlignment.Right, + VerticalAlignment = VAlignment.Center, + Margin = new Thickness(3f, 3f, 0f, 0f), + }; + + var collection = IoCManager.Instance!; + var protoManager = collection.Resolve(); + + // If no loadout found then disabled button + if (!protoManager.TryIndex(LoadoutSystem.GetJobPrototype(job.ID), out var roleLoadoutProto)) + { + loadoutWindowBtn.Disabled = true; + } + // else + else + { + loadoutWindowBtn.OnPressed += args => + { + RoleLoadout? loadout = null; + + // Clone so we don't modify the underlying loadout. + Profile?.Loadouts.TryGetValue(LoadoutSystem.GetJobPrototype(job.ID), out loadout); + loadout = loadout?.Clone(); + + if (loadout == null) + { + loadout = new RoleLoadout(roleLoadoutProto.ID); + loadout.SetDefault(Profile, _playerManager.LocalSession, _prototypeManager); + } + + OpenLoadout(job, loadout, roleLoadoutProto); + }; + } + + _jobPriorities.Add((job.ID, selector)); + jobContainer.AddChild(selector); + jobContainer.AddChild(loadoutWindowBtn); + category.AddChild(jobContainer); + } + } + + UpdateJobPriorities(); + } + + public void RefreshAntags() + { + AntagList.RemoveAllChildren(); + var items = new[] + { + ("humanoid-profile-editor-antag-preference-yes-button", 0), + ("humanoid-profile-editor-antag-preference-no-button", 1) + }; + + foreach (var antag in _prototypeManager.EnumeratePrototypes().OrderBy(a => Loc.GetString(a.Name))) + { + if (!antag.SetPreference) + continue; + + var antagContainer = new BoxContainer() + { + Orientation = LayoutOrientation.Horizontal, + }; + + var selector = new RequirementsSelector() + { + Margin = new Thickness(3f, 3f, 3f, 0f), + }; + selector.OnOpenGuidebook += OnOpenGuidebook; + + var title = Loc.GetString(antag.Name); + var description = Loc.GetString(antag.Objective); + selector.Setup(items, title, 250, description, guides: antag.Guides); + selector.Select(Profile?.AntagPreferences.Contains(antag.ID) == true ? 0 : 1); + + if (!_requirements.IsAllowed( + antag, + (HumanoidCharacterProfile?)_preferencesManager.Preferences?.SelectedCharacter, + out var reason)) + { + selector.LockRequirements(reason); + Profile = Profile?.WithAntagPreference(antag.ID, false); + SetDirty(); + } + else + { + selector.UnlockRequirements(); + } + + selector.OnSelected += preference => + { + Profile = Profile?.WithAntagPreference(antag.ID, preference == 0); + SetDirty(); + }; + + antagContainer.AddChild(selector); + + antagContainer.AddChild(new Button() + { + Disabled = true, + Text = Loc.GetString("loadout-window"), + HorizontalAlignment = HAlignment.Right, + Margin = new Thickness(3f, 0f, 0f, 0f), + }); + + AntagList.AddChild(antagContainer); + } + } +} diff --git a/Content.Client/Lobby/UI/HumanoidProfileEditor.Traits.cs b/Content.Client/Lobby/UI/HumanoidProfileEditor.Traits.cs new file mode 100644 index 0000000000..484a6fb611 --- /dev/null +++ b/Content.Client/Lobby/UI/HumanoidProfileEditor.Traits.cs @@ -0,0 +1,124 @@ +using System.Linq; +using Content.Client.Lobby.UI.Roles; +using Content.Client.Stylesheets; +using Content.Shared.Traits; +using Robust.Client.UserInterface.Controls; +using Robust.Shared.Utility; + +namespace Content.Client.Lobby.UI; + +public sealed partial class HumanoidProfileEditor +{ + + /// + /// Refreshes traits selector + /// + public void RefreshTraits() + { + TraitsList.RemoveAllChildren(); + + var traits = _prototypeManager.EnumeratePrototypes().OrderBy(t => Loc.GetString(t.Name)).ToList(); + TabContainer.SetTabTitle(3, Loc.GetString("humanoid-profile-editor-traits-tab")); + + if (traits.Count < 1) + { + TraitsList.AddChild(new Label + { + Text = Loc.GetString("humanoid-profile-editor-no-traits"), + FontColorOverride = Color.Gray, + }); + return; + } + + // Setup model + Dictionary> traitGroups = new(); + List defaultTraits = new(); + traitGroups.Add(TraitCategoryPrototype.Default, defaultTraits); + + foreach (var trait in traits) + { + if (trait.Category == null) + { + defaultTraits.Add(trait.ID); + continue; + } + + if (!_prototypeManager.HasIndex(trait.Category)) + continue; + + var group = traitGroups.GetOrNew(trait.Category); + group.Add(trait.ID); + } + + // Create UI view from model + foreach (var (categoryId, categoryTraits) in traitGroups) + { + TraitCategoryPrototype? category = null; + + if (categoryId != TraitCategoryPrototype.Default) + { + category = _prototypeManager.Index(categoryId); + // Label + TraitsList.AddChild(new Label + { + Text = Loc.GetString(category.Name), + Margin = new Thickness(0, 10, 0, 0), + StyleClasses = { StyleClass.LabelHeading }, + }); + } + + List selectors = new(); + var selectionCount = 0; + + foreach (var traitProto in categoryTraits) + { + var trait = _prototypeManager.Index(traitProto); + var selector = new TraitPreferenceSelector(trait); + + selector.Preference = Profile?.TraitPreferences.Contains(trait.ID) == true; + if (selector.Preference) + selectionCount += trait.Cost; + + selector.PreferenceChanged += preference => + { + if (preference) + { + Profile = Profile?.WithTraitPreference(trait.ID, _prototypeManager); + } + else + { + Profile = Profile?.WithoutTraitPreference(trait.ID, _prototypeManager); + } + + SetDirty(); + RefreshTraits(); // If too many traits are selected, they will be reset to the real value. + }; + selectors.Add(selector); + } + + // Selection counter + if (category is { MaxTraitPoints: >= 0 }) + { + TraitsList.AddChild(new Label + { + Text = Loc.GetString("humanoid-profile-editor-trait-count-hint", ("current", selectionCount), ("max", category.MaxTraitPoints)), + FontColorOverride = Color.Gray + }); + } + + foreach (var selector in selectors) + { + if (selector == null) + continue; + + if (category is { MaxTraitPoints: >= 0 } && + selector.Cost + selectionCount > category.MaxTraitPoints) + { + selector.Checkbox.Label.FontColorOverride = Color.Red; + } + + TraitsList.AddChild(selector); + } + } + } +} diff --git a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs index 6db003f091..3b444bf416 100644 --- a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs +++ b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs @@ -1,29 +1,14 @@ -using System.IO; -using System.Linq; -using System.Numerics; using Content.Client.Humanoid; -using Content.Client.Lobby.UI.Loadouts; -using Content.Client.Lobby.UI.Roles; using Content.Client.Message; using Content.Client.Players.PlayTimeTracking; -using Content.Client.Stylesheets; using Content.Client.Sprite; -using Content.Client.UserInterface.Systems.Guidebook; -using Content.Shared.Body; using Content.Shared.CCVar; -using Content.Shared.Clothing; using Content.Shared.GameTicking; -using Content.Shared.Guidebook; using Content.Shared.Humanoid; using Content.Shared.Humanoid.Markings; -using Content.Shared.Humanoid.Prototypes; using Content.Shared.Preferences; -using Content.Shared.Preferences.Loadouts; -using Content.Shared.Roles; -using Content.Shared.Traits; using Robust.Client.AutoGenerated; using Robust.Client.GameObjects; -using Robust.Client.Graphics; using Robust.Client.Player; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; @@ -33,7 +18,6 @@ using Robust.Shared.Configuration; using Robust.Shared.ContentPack; using Robust.Shared.Enums; using Robust.Shared.Prototypes; -using Robust.Shared.Utility; using Direction = Robust.Shared.Maths.Direction; namespace Content.Client.Lobby.UI @@ -56,27 +40,12 @@ namespace Content.Client.Lobby.UI // CCvar. private int _maxNameLength; - private bool _allowFlavorText; - - private FlavorText.FlavorText? _flavorText; - private TextEdit? _flavorTextEdit; - - // One at a time. - private LoadoutWindow? _loadoutWindow; - - private bool _exporting; - private bool _imaging; /// /// If we're attempting to save. /// public event Action? Save; - /// - /// Temporary override of their selected job, used to preview roles. - /// - public JobPrototype? JobOverride; - /// /// The character slot for the current profile. /// @@ -87,21 +56,22 @@ namespace Content.Client.Lobby.UI /// public HumanoidCharacterProfile? Profile; - private List _species = new(); - - private List<(string, RequirementsSelector)> _jobPriorities = new(); - - private readonly Dictionary _jobCategories; - private Direction _previewRotation = Direction.North; - private ColorSelectorSliders _rgbSkinColorSelector; - private bool _isDirty; - private static readonly ProtoId DefaultSpeciesGuidebook = "Species"; + public bool IsDirty + { + get => _isDirty; + set + { + if (_isDirty == value) + return; - public event Action>>? OnOpenGuidebook; + _isDirty = value; + UpdateSaveButton(); + } + } private ISawmill _sawmill; @@ -160,7 +130,7 @@ namespace Content.Client.Lobby.UI ResetButton.OnPressed += args => { - SetProfile((HumanoidCharacterProfile?) _preferencesManager.Preferences?.SelectedCharacter, _preferencesManager.Preferences?.SelectedCharacterIndex); + SetProfile((HumanoidCharacterProfile?)_preferencesManager.Preferences?.SelectedCharacter, _preferencesManager.Preferences?.SelectedCharacterIndex); }; SaveButton.OnPressed += args => @@ -189,7 +159,7 @@ namespace Content.Client.Lobby.UI SexButton.OnItemSelected += args => { SexButton.SelectId(args.Id); - SetSex((Sex) args.Id); + SetSex((Sex)args.Id); }; #endregion Sex @@ -208,15 +178,15 @@ namespace Content.Client.Lobby.UI #region Gender - PronounsButton.AddItem(Loc.GetString("humanoid-profile-editor-pronouns-male-text"), (int) Gender.Male); - PronounsButton.AddItem(Loc.GetString("humanoid-profile-editor-pronouns-female-text"), (int) Gender.Female); - PronounsButton.AddItem(Loc.GetString("humanoid-profile-editor-pronouns-epicene-text"), (int) Gender.Epicene); - PronounsButton.AddItem(Loc.GetString("humanoid-profile-editor-pronouns-neuter-text"), (int) Gender.Neuter); + PronounsButton.AddItem(Loc.GetString("humanoid-profile-editor-pronouns-male-text"), (int)Gender.Male); + PronounsButton.AddItem(Loc.GetString("humanoid-profile-editor-pronouns-female-text"), (int)Gender.Female); + PronounsButton.AddItem(Loc.GetString("humanoid-profile-editor-pronouns-epicene-text"), (int)Gender.Epicene); + PronounsButton.AddItem(Loc.GetString("humanoid-profile-editor-pronouns-neuter-text"), (int)Gender.Neuter); PronounsButton.OnItemSelected += args => { PronounsButton.SelectId(args.Id); - SetGender((Gender) args.Id); + SetGender((Gender)args.Id); }; #endregion Gender @@ -250,13 +220,13 @@ namespace Content.Client.Lobby.UI foreach (var value in Enum.GetValues()) { - SpawnPriorityButton.AddItem(Loc.GetString($"humanoid-profile-editor-preference-spawn-priority-{value.ToString().ToLower()}"), (int) value); + SpawnPriorityButton.AddItem(Loc.GetString($"humanoid-profile-editor-preference-spawn-priority-{value.ToString().ToLower()}"), (int)value); } SpawnPriorityButton.OnItemSelected += args => { SpawnPriorityButton.SelectId(args.Id); - SetSpawnPriority((SpawnPriorityPreference) args.Id); + SetSpawnPriority((SpawnPriorityPreference)args.Id); }; #endregion SpawnPriority @@ -283,16 +253,16 @@ namespace Content.Client.Lobby.UI PreferenceUnavailableButton.AddItem( Loc.GetString("humanoid-profile-editor-preference-unavailable-stay-in-lobby-button"), - (int) PreferenceUnavailableMode.StayInLobby); + (int)PreferenceUnavailableMode.StayInLobby); PreferenceUnavailableButton.AddItem( Loc.GetString("humanoid-profile-editor-preference-unavailable-spawn-as-overflow-button", ("overflowJob", Loc.GetString(SharedGameTicker.FallbackOverflowJobName))), - (int) PreferenceUnavailableMode.SpawnAsOverflow); + (int)PreferenceUnavailableMode.SpawnAsOverflow); PreferenceUnavailableButton.OnItemSelected += args => { PreferenceUnavailableButton.SelectId(args.Id); - Profile = Profile?.WithPreferenceUnavailable((PreferenceUnavailableMode) args.Id); + Profile = Profile?.WithPreferenceUnavailable((PreferenceUnavailableMode)args.Id); SetDirty(); }; @@ -346,246 +316,6 @@ namespace Content.Client.Lobby.UI IsDirty = false; } - /// - /// Refreshes the flavor text editor status. - /// - public void RefreshFlavorText() - { - if (_allowFlavorText) - { - if (_flavorText != null) - return; - - _flavorText = new FlavorText.FlavorText(); - TabContainer.AddChild(_flavorText); - TabContainer.SetTabTitle(TabContainer.ChildCount - 1, Loc.GetString("humanoid-profile-editor-flavortext-tab")); - _flavorTextEdit = _flavorText.CFlavorTextInput; - - _flavorText.OnFlavorTextChanged += OnFlavorTextChange; - } - else - { - if (_flavorText == null) - return; - - TabContainer.RemoveChild(_flavorText); - _flavorText.OnFlavorTextChanged -= OnFlavorTextChange; - _flavorText.Dispose(); - _flavorTextEdit?.Dispose(); - _flavorTextEdit = null; - _flavorText = null; - } - } - - /// - /// Refreshes traits selector - /// - public void RefreshTraits() - { - TraitsList.RemoveAllChildren(); - - var traits = _prototypeManager.EnumeratePrototypes().OrderBy(t => Loc.GetString(t.Name)).ToList(); - TabContainer.SetTabTitle(3, Loc.GetString("humanoid-profile-editor-traits-tab")); - - if (traits.Count < 1) - { - TraitsList.AddChild(new Label - { - Text = Loc.GetString("humanoid-profile-editor-no-traits"), - FontColorOverride = Color.Gray, - }); - return; - } - - // Setup model - Dictionary> traitGroups = new(); - List defaultTraits = new(); - traitGroups.Add(TraitCategoryPrototype.Default, defaultTraits); - - foreach (var trait in traits) - { - if (trait.Category == null) - { - defaultTraits.Add(trait.ID); - continue; - } - - if (!_prototypeManager.HasIndex(trait.Category)) - continue; - - var group = traitGroups.GetOrNew(trait.Category); - group.Add(trait.ID); - } - - // Create UI view from model - foreach (var (categoryId, categoryTraits) in traitGroups) - { - TraitCategoryPrototype? category = null; - - if (categoryId != TraitCategoryPrototype.Default) - { - category = _prototypeManager.Index(categoryId); - // Label - TraitsList.AddChild(new Label - { - Text = Loc.GetString(category.Name), - Margin = new Thickness(0, 10, 0, 0), - StyleClasses = { StyleClass.LabelHeading }, - }); - } - - List selectors = new(); - var selectionCount = 0; - - foreach (var traitProto in categoryTraits) - { - var trait = _prototypeManager.Index(traitProto); - var selector = new TraitPreferenceSelector(trait); - - selector.Preference = Profile?.TraitPreferences.Contains(trait.ID) == true; - if (selector.Preference) - selectionCount += trait.Cost; - - selector.PreferenceChanged += preference => - { - if (preference) - { - Profile = Profile?.WithTraitPreference(trait.ID, _prototypeManager); - } - else - { - Profile = Profile?.WithoutTraitPreference(trait.ID, _prototypeManager); - } - - SetDirty(); - RefreshTraits(); // If too many traits are selected, they will be reset to the real value. - }; - selectors.Add(selector); - } - - // Selection counter - if (category is { MaxTraitPoints: >= 0 }) - { - TraitsList.AddChild(new Label - { - Text = Loc.GetString("humanoid-profile-editor-trait-count-hint", ("current", selectionCount) ,("max", category.MaxTraitPoints)), - FontColorOverride = Color.Gray - }); - } - - foreach (var selector in selectors) - { - if (selector == null) - continue; - - if (category is { MaxTraitPoints: >= 0 } && - selector.Cost + selectionCount > category.MaxTraitPoints) - { - selector.Checkbox.Label.FontColorOverride = Color.Red; - } - - TraitsList.AddChild(selector); - } - } - } - - /// - /// Refreshes the species selector. - /// - public void RefreshSpecies() - { - SpeciesButton.Clear(); - _species.Clear(); - - _species.AddRange(_prototypeManager.EnumeratePrototypes().Where(o => o.RoundStart)); - _species.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.CurrentCultureIgnoreCase)); - var speciesIds = _species.Select(o => o.ID).ToList(); - - for (var i = 0; i < _species.Count; i++) - { - var name = Loc.GetString(_species[i].Name); - SpeciesButton.AddItem(name, i); - - if (Profile?.Species.Equals(_species[i].ID) == true) - { - SpeciesButton.SelectId(i); - } - } - - // If our species isn't available then reset it to default. - if (Profile != null) - { - if (!speciesIds.Contains(Profile.Species)) - { - SetSpecies(HumanoidCharacterProfile.DefaultSpecies); - } - } - } - - public void RefreshAntags() - { - AntagList.RemoveAllChildren(); - var items = new[] - { - ("humanoid-profile-editor-antag-preference-yes-button", 0), - ("humanoid-profile-editor-antag-preference-no-button", 1) - }; - - foreach (var antag in _prototypeManager.EnumeratePrototypes().OrderBy(a => Loc.GetString(a.Name))) - { - if (!antag.SetPreference) - continue; - - var antagContainer = new BoxContainer() - { - Orientation = LayoutOrientation.Horizontal, - }; - - var selector = new RequirementsSelector() - { - Margin = new Thickness(3f, 3f, 3f, 0f), - }; - selector.OnOpenGuidebook += OnOpenGuidebook; - - var title = Loc.GetString(antag.Name); - var description = Loc.GetString(antag.Objective); - selector.Setup(items, title, 250, description, guides: antag.Guides); - selector.Select(Profile?.AntagPreferences.Contains(antag.ID) == true ? 0 : 1); - - if (!_requirements.IsAllowed( - antag, - (HumanoidCharacterProfile?)_preferencesManager.Preferences?.SelectedCharacter, - out var reason)) - { - selector.LockRequirements(reason); - Profile = Profile?.WithAntagPreference(antag.ID, false); - SetDirty(); - } - else - { - selector.UnlockRequirements(); - } - - selector.OnSelected += preference => - { - Profile = Profile?.WithAntagPreference(antag.ID, preference == 0); - SetDirty(); - }; - - antagContainer.AddChild(selector); - - antagContainer.AddChild(new Button() - { - Disabled = true, - Text = Loc.GetString("loadout-window"), - HorizontalAlignment = HAlignment.Right, - Margin = new Thickness(3f, 0f, 0f, 0f), - }); - - AntagList.AddChild(antagContainer); - } - } - private void SetDirty() { // If it equals default then reset the button. @@ -599,14 +329,6 @@ namespace Content.Client.Lobby.UI IsDirty = true; } - /// - /// Refresh all loadouts. - /// - public void RefreshLoadouts() - { - _loadoutWindow?.Dispose(); - } - /// /// Reloads the entire dummy entity for preview. /// @@ -630,7 +352,7 @@ namespace Content.Client.Lobby.UI public void ResetToDefault() { SetProfile( - (HumanoidCharacterProfile?) _preferencesManager.Preferences?.SelectedCharacter, + (HumanoidCharacterProfile?)_preferencesManager.Preferences?.SelectedCharacter, _preferencesManager.Preferences?.SelectedCharacterIndex); } @@ -665,7 +387,7 @@ namespace Content.Client.Lobby.UI if (Profile != null) { - PreferenceUnavailableButton.SelectId((int) Profile.PreferenceUnavailable); + PreferenceUnavailableButton.SelectId((int)Profile.PreferenceUnavailable); } } @@ -683,336 +405,6 @@ namespace Content.Client.Lobby.UI SetDirty(); } - private void OnSpeciesInfoButtonPressed(BaseButton.ButtonEventArgs args) - { - // TODO GUIDEBOOK - // make the species guide book a field on the species prototype. - // I.e., do what jobs/antags do. - - var guidebookController = UserInterfaceManager.GetUIController(); - var species = Profile?.Species ?? HumanoidCharacterProfile.DefaultSpecies; - var page = DefaultSpeciesGuidebook; - if (_prototypeManager.HasIndex(species)) - page = new ProtoId(species.Id); // Gross. See above todo comment. - - if (_prototypeManager.Resolve(DefaultSpeciesGuidebook, out var guideRoot)) - { - var dict = new Dictionary, GuideEntry>(); - dict.Add(DefaultSpeciesGuidebook, guideRoot); - //TODO: Don't close the guidebook if its already open, just go to the correct page - guidebookController.OpenGuidebook(dict, includeChildren:true, selected: page); - } - } - - /// - /// Refreshes all job selectors. - /// - public void RefreshJobs() - { - JobList.RemoveAllChildren(); - _jobCategories.Clear(); - _jobPriorities.Clear(); - var firstCategory = true; - - // Get all displayed departments - var departments = new List(); - foreach (var department in _prototypeManager.EnumeratePrototypes()) - { - if (department.EditorHidden) - continue; - - departments.Add(department); - } - - departments.Sort(DepartmentUIComparer.Instance); - - var items = new[] - { - ("humanoid-profile-editor-job-priority-never-button", (int) JobPriority.Never), - ("humanoid-profile-editor-job-priority-low-button", (int) JobPriority.Low), - ("humanoid-profile-editor-job-priority-medium-button", (int) JobPriority.Medium), - ("humanoid-profile-editor-job-priority-high-button", (int) JobPriority.High), - }; - - foreach (var department in departments) - { - var departmentName = Loc.GetString(department.Name); - - if (!_jobCategories.TryGetValue(department.ID, out var category)) - { - category = new BoxContainer - { - Orientation = LayoutOrientation.Vertical, - Name = department.ID, - ToolTip = Loc.GetString("humanoid-profile-editor-jobs-amount-in-department-tooltip", - ("departmentName", departmentName)) - }; - - if (firstCategory) - { - firstCategory = false; - } - else - { - category.AddChild(new Control - { - MinSize = new Vector2(0, 23), - }); - } - - category.AddChild(new PanelContainer - { - PanelOverride = new StyleBoxFlat {BackgroundColor = Color.FromHex("#464966")}, - Children = - { - new Label - { - Text = Loc.GetString("humanoid-profile-editor-department-jobs-label", - ("departmentName", departmentName)), - Margin = new Thickness(5f, 0, 0, 0) - } - } - }); - - _jobCategories[department.ID] = category; - JobList.AddChild(category); - } - - var jobs = department.Roles.Select(jobId => _prototypeManager.Index(jobId)) - .Where(job => job.SetPreference) - .ToArray(); - - Array.Sort(jobs, JobUIComparer.Instance); - - foreach (var job in jobs) - { - var jobContainer = new BoxContainer() - { - Orientation = LayoutOrientation.Horizontal, - }; - - var selector = new RequirementsSelector() - { - Margin = new Thickness(3f, 3f, 3f, 0f), - }; - selector.OnOpenGuidebook += OnOpenGuidebook; - - var icon = new TextureRect - { - TextureScale = new Vector2(2, 2), - VerticalAlignment = VAlignment.Center - }; - var jobIcon = _prototypeManager.Index(job.Icon); - icon.Texture = _sprite.Frame0(jobIcon.Icon); - selector.Setup(items, job.LocalizedName, 200, job.LocalizedDescription, icon, job.Guides); - - if (!_requirements.IsAllowed(job, (HumanoidCharacterProfile?)_preferencesManager.Preferences?.SelectedCharacter, out var reason)) - { - selector.LockRequirements(reason); - } - else - { - selector.UnlockRequirements(); - } - - selector.OnSelected += selectedPrio => - { - var selectedJobPrio = (JobPriority) selectedPrio; - Profile = Profile?.WithJobPriority(job.ID, selectedJobPrio); - - foreach (var (jobId, other) in _jobPriorities) - { - // Sync other selectors with the same job in case of multiple department jobs - if (jobId == job.ID) - { - other.Select(selectedPrio); - continue; - } - - if (selectedJobPrio != JobPriority.High || (JobPriority) other.Selected != JobPriority.High) - continue; - - // Lower any other high priorities to medium. - other.Select((int)JobPriority.Medium); - Profile = Profile?.WithJobPriority(jobId, JobPriority.Medium); - } - - // TODO: Only reload on high change (either to or from). - ReloadPreview(); - - UpdateJobPriorities(); - SetDirty(); - }; - - var loadoutWindowBtn = new Button() - { - Text = Loc.GetString("loadout-window"), - HorizontalAlignment = HAlignment.Right, - VerticalAlignment = VAlignment.Center, - Margin = new Thickness(3f, 3f, 0f, 0f), - }; - - var collection = IoCManager.Instance!; - var protoManager = collection.Resolve(); - - // If no loadout found then disabled button - if (!protoManager.TryIndex(LoadoutSystem.GetJobPrototype(job.ID), out var roleLoadoutProto)) - { - loadoutWindowBtn.Disabled = true; - } - // else - else - { - loadoutWindowBtn.OnPressed += args => - { - RoleLoadout? loadout = null; - - // Clone so we don't modify the underlying loadout. - Profile?.Loadouts.TryGetValue(LoadoutSystem.GetJobPrototype(job.ID), out loadout); - loadout = loadout?.Clone(); - - if (loadout == null) - { - loadout = new RoleLoadout(roleLoadoutProto.ID); - loadout.SetDefault(Profile, _playerManager.LocalSession, _prototypeManager); - } - - OpenLoadout(job, loadout, roleLoadoutProto); - }; - } - - _jobPriorities.Add((job.ID, selector)); - jobContainer.AddChild(selector); - jobContainer.AddChild(loadoutWindowBtn); - category.AddChild(jobContainer); - } - } - - UpdateJobPriorities(); - } - - private void OpenLoadout(JobPrototype? jobProto, RoleLoadout roleLoadout, RoleLoadoutPrototype roleLoadoutProto) - { - _loadoutWindow?.Dispose(); - _loadoutWindow = null; - var collection = IoCManager.Instance; - - if (collection == null || _playerManager.LocalSession == null || Profile == null) - return; - - JobOverride = jobProto; - var session = _playerManager.LocalSession; - - _loadoutWindow = new LoadoutWindow(Profile, roleLoadout, roleLoadoutProto, _playerManager.LocalSession, collection) - { - Title = Loc.GetString("loadout-window-title-loadout", ("job", $"{jobProto?.LocalizedName}")), - }; - - // Refresh the buttons etc. - _loadoutWindow.RefreshLoadouts(roleLoadout, session, collection); - _loadoutWindow.OpenCenteredLeft(); - - _loadoutWindow.OnNameChanged += name => - { - roleLoadout.EntityName = name; - Profile = Profile.WithLoadout(roleLoadout); - SetDirty(); - }; - - _loadoutWindow.OnLoadoutPressed += (loadoutGroup, loadoutProto) => - { - roleLoadout.AddLoadout(loadoutGroup, loadoutProto, _prototypeManager); - _loadoutWindow.RefreshLoadouts(roleLoadout, session, collection); - Profile = Profile?.WithLoadout(roleLoadout); - ReloadPreview(); - }; - - _loadoutWindow.OnLoadoutUnpressed += (loadoutGroup, loadoutProto) => - { - roleLoadout.RemoveLoadout(loadoutGroup, loadoutProto, _prototypeManager); - _loadoutWindow.RefreshLoadouts(roleLoadout, session, collection); - Profile = Profile?.WithLoadout(roleLoadout); - ReloadPreview(); - }; - - JobOverride = jobProto; - ReloadPreview(); - - _loadoutWindow.OnClose += () => - { - JobOverride = null; - ReloadPreview(); - }; - - if (Profile is null) - return; - - UpdateJobPriorities(); - } - - private void OnFlavorTextChange(string content) - { - if (Profile is null) - return; - - Profile = Profile.WithFlavorText(content); - SetDirty(); - } - - private void OnMarkingChange() - { - if (Profile is null) - return; - - Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithMarkings(_markingsModel.Markings)); - ReloadProfilePreview(); - SetDirty(); - } - - private void OnSkinColorOnValueChanged() - { - if (Profile is null) return; - - var skin = _prototypeManager.Index(Profile.Species).SkinColoration; - var strategy = _prototypeManager.Index(skin).Strategy; - - switch (strategy.InputType) - { - case SkinColorationStrategyInput.Unary: - { - if (!Skin.Visible) - { - Skin.Visible = true; - RgbSkinColorContainer.Visible = false; - } - - var color = strategy.FromUnary(Skin.Value); - - _markingsModel.SetOrganSkinColor(color); - Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color)); - - break; - } - case SkinColorationStrategyInput.Color: - { - if (!RgbSkinColorContainer.Visible) - { - Skin.Visible = false; - RgbSkinColorContainer.Visible = true; - } - - var color = strategy.ClosestSkinColor(_rgbSkinColorSelector.Color); - - _markingsModel.SetOrganSkinColor(color); - Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color)); - - break; - } - } - - ReloadProfilePreview(); - } - protected override void Dispose(bool disposing) { base.Dispose(disposing); @@ -1029,248 +421,6 @@ namespace Content.Client.Lobby.UI ReloadPreview(); } - private void SetAge(int newAge) - { - Profile = Profile?.WithAge(newAge); - ReloadPreview(); - } - - private void SetSex(Sex newSex) - { - Profile = Profile?.WithSex(newSex); - // for convenience, default to most common gender when new sex is selected - switch (newSex) - { - case Sex.Male: - Profile = Profile?.WithGender(Gender.Male); - break; - case Sex.Female: - Profile = Profile?.WithGender(Gender.Female); - break; - default: - Profile = Profile?.WithGender(Gender.Epicene); - break; - } - - UpdateGenderControls(); - _markingsModel.SetOrganSexes(newSex); - ReloadPreview(); - } - - private void SetGender(Gender newGender) - { - Profile = Profile?.WithGender(newGender); - ReloadPreview(); - } - - private void SetSpecies(string newSpecies) - { - Profile = Profile?.WithSpecies(newSpecies); - OnSkinColorOnValueChanged(); // Species may have special color prefs, make sure to update it. - _markingsModel.OrganData = _markingManager.GetMarkingData(newSpecies); - _markingsModel.ValidateMarkings(); - // In case there's job restrictions for the species - RefreshJobs(); - // In case there's species restrictions for loadouts - RefreshLoadouts(); - UpdateSexControls(); // update sex for new species - UpdateSpeciesGuidebookIcon(); - ReloadPreview(); - } - - private void SetName(string newName) - { - Profile = Profile?.WithName(newName); - SetDirty(); - - if (!IsDirty) - return; - - SpriteView.SetName(newName); - } - - private void SetSpawnPriority(SpawnPriorityPreference newSpawnPriority) - { - Profile = Profile?.WithSpawnPriorityPreference(newSpawnPriority); - SetDirty(); - } - - public bool IsDirty - { - get => _isDirty; - set - { - if (_isDirty == value) - return; - - _isDirty = value; - UpdateSaveButton(); - } - } - - private void UpdateNameEdit() - { - NameEdit.Text = Profile?.Name ?? ""; - } - - private void UpdateFlavorTextEdit() - { - if (_flavorTextEdit != null) - { - _flavorTextEdit.TextRope = new Rope.Leaf(Profile?.FlavorText ?? ""); - } - } - - private void UpdateAgeEdit() - { - AgeEdit.Text = Profile?.Age.ToString() ?? ""; - } - - /// - /// Updates selected job priorities to the profile's. - /// - private void UpdateJobPriorities() - { - foreach (var (jobId, prioritySelector) in _jobPriorities) - { - var priority = Profile?.JobPriorities.GetValueOrDefault(jobId, JobPriority.Never) ?? JobPriority.Never; - prioritySelector.Select((int) priority); - } - } - - private void UpdateSexControls() - { - if (Profile == null) - return; - - SexButton.Clear(); - - var sexes = new List(); - - // add species sex options, default to just none if we are in bizzaro world and have no species - if (_prototypeManager.Resolve(Profile.Species, out var speciesProto)) - { - foreach (var sex in speciesProto.Sexes) - { - sexes.Add(sex); - } - } - else - { - sexes.Add(Sex.Unsexed); - } - - // add button for each sex - foreach (var sex in sexes) - { - SexButton.AddItem(Loc.GetString($"humanoid-profile-editor-sex-{sex.ToString().ToLower()}-text"), (int) sex); - } - - if (sexes.Contains(Profile.Sex)) - SexButton.SelectId((int) Profile.Sex); - else - SexButton.SelectId((int) sexes[0]); - } - - private void UpdateSkinColor() - { - if (Profile == null) - return; - - var skin = _prototypeManager.Index(Profile.Species).SkinColoration; - var strategy = _prototypeManager.Index(skin).Strategy; - - switch (strategy.InputType) - { - case SkinColorationStrategyInput.Unary: - { - if (!Skin.Visible) - { - Skin.Visible = true; - RgbSkinColorContainer.Visible = false; - } - - Skin.Value = strategy.ToUnary(Profile.Appearance.SkinColor); - - break; - } - case SkinColorationStrategyInput.Color: - { - if (!RgbSkinColorContainer.Visible) - { - Skin.Visible = false; - RgbSkinColorContainer.Visible = true; - } - - _rgbSkinColorSelector.Color = strategy.ClosestSkinColor(Profile.Appearance.SkinColor); - - break; - } - } - } - - public void UpdateSpeciesGuidebookIcon() - { - SpeciesInfoButton.StyleClasses.Clear(); - - var species = Profile?.Species; - if (species is null) - return; - - if (!_prototypeManager.Resolve(species, out var speciesProto)) - return; - - // Don't display the info button if no guide entry is found - if (!_prototypeManager.HasIndex(species)) - return; - - const string style = "SpeciesInfoDefault"; - SpeciesInfoButton.StyleIdentifier = style; - } - - private void UpdateMarkings() - { - if (Profile == null) - { - return; - } - - _markingsModel.OrganData = _markingManager.GetMarkingData(Profile.Species); - _markingsModel.OrganProfileData = _markingManager.GetProfileData(Profile.Species, Profile.Sex, Profile.Appearance.SkinColor, Profile.Appearance.EyeColor); - _markingsModel.Markings = Profile.Appearance.Markings; - } - - private void UpdateGenderControls() - { - if (Profile == null) - { - return; - } - - PronounsButton.SelectId((int) Profile.Gender); - } - - private void UpdateSpawnPriorityControls() - { - if (Profile == null) - { - return; - } - - SpawnPriorityButton.SelectId((int) Profile.SpawnPriority); - } - - private void UpdateEyePickers() - { - if (Profile == null) - { - return; - } - - _markingsModel.SetOrganEyeColor(Profile.Appearance.EyeColor); - EyeColorPicker.SetData(Profile.Appearance.EyeColor); - } - private void UpdateSaveButton() { SaveButton.Disabled = Profile is null || !IsDirty; @@ -1279,112 +429,7 @@ namespace Content.Client.Lobby.UI private void SetPreviewRotation(Direction direction) { - SpriteView.OverrideDirection = (Direction) ((int) direction % 4 * 2); - } - - private void RandomizeEverything() - { - Profile = HumanoidCharacterProfile.Random(); - SetProfile(Profile, CharacterSlot); - SetDirty(); - } - - private void RandomizeName() - { - if (Profile == null) return; - var name = HumanoidCharacterProfile.GetName(Profile.Species, Profile.Gender); - SetName(name); - UpdateNameEdit(); - } - - private async void ExportImage() - { - if (_imaging) - return; - - var dir = SpriteView.OverrideDirection ?? Direction.South; - - // I tried disabling the button but it looks sorta goofy as it only takes a frame or two to save - _imaging = true; - await _entManager.System().Export(SpriteView.PreviewDummy, dir, includeId: false); - _imaging = false; - } - - private async void ImportProfile() - { - if (_exporting || CharacterSlot == null || Profile == null) - return; - - StartExport(); - await using var file = await _dialogManager.OpenFile(new FileDialogFilters(new FileDialogFilters.Group("yml")), FileAccess.Read); - - if (file == null) - { - EndExport(); - return; - } - - try - { - var profile = HumanoidCharacterProfile.FromStream(file, _playerManager.LocalSession!); - var oldProfile = Profile; - SetProfile(profile, CharacterSlot); - - IsDirty = !profile.MemberwiseEquals(oldProfile); - } - catch (Exception exc) - { - _sawmill.Error($"Error when importing profile\n{exc.StackTrace}"); - } - finally - { - EndExport(); - } - } - - private async void ExportProfile() - { - if (Profile == null || _exporting) - return; - - StartExport(); - var file = await _dialogManager.SaveFile(new FileDialogFilters(new FileDialogFilters.Group("yml"))); - - if (file == null) - { - EndExport(); - return; - } - - try - { - var dataNode = Profile.ToDataNode(); - await using var writer = new StreamWriter(file.Value.fileStream); - dataNode.Write(writer); - } - catch (Exception exc) - { - _sawmill.Error($"Error when exporting profile\n{exc.StackTrace}"); - } - finally - { - EndExport(); - await file.Value.fileStream.DisposeAsync(); - } - } - - private void StartExport() - { - _exporting = true; - ImportButton.Disabled = true; - ExportButton.Disabled = true; - } - - private void EndExport() - { - _exporting = false; - ImportButton.Disabled = false; - ExportButton.Disabled = false; + SpriteView.OverrideDirection = (Direction)((int)direction % 4 * 2); } } }