using System.Linq; using System.Numerics; using Content.Client.Humanoid; using Content.Client.Lobby.UI; using Content.Client.Message; using Content.Client.Players.PlayTimeTracking; using Content.Client.Stylesheets; using Content.Client.UserInterface.Controls; using Content.Shared.CCVar; using Content.Shared.GameTicking; using Content.Shared.Humanoid; using Content.Shared.Humanoid.Markings; using Content.Shared.Humanoid.Prototypes; using Content.Shared.Inventory; using Content.Shared.Preferences; using Content.Shared.Roles; using Content.Shared.StatusIcon; using Content.Shared.Traits; using Robust.Client.AutoGenerated; using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; using Robust.Client.Utility; using Robust.Shared.Configuration; using Robust.Shared.Enums; using Robust.Shared.Map; using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Timing; using Robust.Shared.Utility; using static Robust.Client.UserInterface.Controls.BoxContainer; namespace Content.Client.Preferences.UI { public sealed class HighlightedContainer : PanelContainer { public HighlightedContainer() { PanelOverride = new StyleBoxFlat() { BackgroundColor = new Color(47, 47, 53), ContentMarginTopOverride = 10, ContentMarginBottomOverride = 10, ContentMarginLeftOverride = 10, ContentMarginRightOverride = 10 }; } } [GenerateTypedNameReferences] public sealed partial class HumanoidProfileEditor : Control { private readonly IClientPreferencesManager _preferencesManager; private readonly IEntityManager _entMan; private readonly IConfigurationManager _configurationManager; private readonly MarkingManager _markingManager; private readonly JobRequirementsManager _requirements; private LineEdit _ageEdit => CAgeEdit; private LineEdit _nameEdit => CNameEdit; private TextEdit _flavorTextEdit = null!; private Button _nameRandomButton => CNameRandomize; private Button _randomizeEverythingButton => CRandomizeEverything; private RichTextLabel _warningLabel => CWarningLabel; private Button _saveButton => CSaveButton; private OptionButton _sexButton => CSexButton; private OptionButton _genderButton => CPronounsButton; private Slider _skinColor => CSkin; private OptionButton _clothingButton => CClothingButton; private OptionButton _backpackButton => CBackpackButton; private SingleMarkingPicker _hairPicker => CHairStylePicker; private SingleMarkingPicker _facialHairPicker => CFacialHairPicker; private EyeColorPicker _eyesPicker => CEyeColorPicker; private TabContainer _tabContainer => CTabContainer; private BoxContainer _jobList => CJobList; private BoxContainer _antagList => CAntagList; private BoxContainer _traitsList => CTraitsList; private readonly List _jobPriorities; private OptionButton _preferenceUnavailableButton => CPreferenceUnavailableButton; private readonly Dictionary _jobCategories; // Mildly hacky, as I don't trust prototype order to stay consistent and don't want the UI to break should a new one get added mid-edit. --moony private readonly List _speciesList; private readonly List _antagPreferences; private readonly List _traitPreferences; private Control _previewSpriteControl => CSpriteViewFront; private Control _previewSpriteSideControl => CSpriteViewSide; private EntityUid? _previewDummy; /// /// Used to avoid unnecessarily re-creating the entity. /// private string? _lastSpecies; private SpriteView? _previewSprite; private SpriteView? _previewSpriteSide; private BoxContainer _rgbSkinColorContainer => CRgbSkinColorContainer; private ColorSelectorSliders _rgbSkinColorSelector; private bool _isDirty; private bool _needUpdatePreview; public int CharacterSlot; public HumanoidCharacterProfile? Profile; private MarkingSet _markingSet = new(); // storing this here feels iffy but a few things need it this high up public event Action? OnProfileChanged; public HumanoidProfileEditor(IClientPreferencesManager preferencesManager, IPrototypeManager prototypeManager, IEntityManager entityManager, IConfigurationManager configurationManager) { RobustXamlLoader.Load(this); _prototypeManager = prototypeManager; _entMan = entityManager; _preferencesManager = preferencesManager; _configurationManager = configurationManager; _markingManager = IoCManager.Resolve(); #region Left #region Randomize #endregion Randomize #region Name _nameEdit.OnTextChanged += args => { SetName(args.Text); }; _nameRandomButton.OnPressed += args => RandomizeName(); _randomizeEverythingButton.OnPressed += args => { RandomizeEverything(); }; _warningLabel.SetMarkup($"[color=red]{Loc.GetString("humanoid-profile-editor-naming-rules-warning")}[/color]"); #endregion Name #region Appearance _tabContainer.SetTabTitle(0, Loc.GetString("humanoid-profile-editor-appearance-tab")); ShowClothes.OnPressed += ToggleClothes; #region Sex _sexButton.OnItemSelected += args => { _sexButton.SelectId(args.Id); SetSex((Sex) args.Id); }; #endregion Sex #region Age _ageEdit.OnTextChanged += args => { if (!int.TryParse(args.Text, out var newAge)) return; SetAge(newAge); }; #endregion Age #region Gender _genderButton.AddItem(Loc.GetString("humanoid-profile-editor-pronouns-male-text"), (int) Gender.Male); _genderButton.AddItem(Loc.GetString("humanoid-profile-editor-pronouns-female-text"), (int) Gender.Female); _genderButton.AddItem(Loc.GetString("humanoid-profile-editor-pronouns-epicene-text"), (int) Gender.Epicene); _genderButton.AddItem(Loc.GetString("humanoid-profile-editor-pronouns-neuter-text"), (int) Gender.Neuter); _genderButton.OnItemSelected += args => { _genderButton.SelectId(args.Id); SetGender((Gender) args.Id); }; #endregion Gender #region Species _speciesList = prototypeManager.EnumeratePrototypes().Where(o => o.RoundStart).ToList(); for (var i = 0; i < _speciesList.Count; i++) { var name = Loc.GetString(_speciesList[i].Name); CSpeciesButton.AddItem(name, i); } CSpeciesButton.OnItemSelected += args => { CSpeciesButton.SelectId(args.Id); SetSpecies(_speciesList[args.Id].ID); UpdateHairPickers(); OnSkinColorOnValueChanged(); }; #endregion Species #region Skin _skinColor.OnValueChanged += _ => { OnSkinColorOnValueChanged(); }; _rgbSkinColorContainer.AddChild(_rgbSkinColorSelector = new ColorSelectorSliders()); _rgbSkinColorSelector.OnColorChanged += _ => { OnSkinColorOnValueChanged(); }; #endregion #region Hair _hairPicker.OnMarkingSelect += newStyle => { if (Profile is null) return; Profile = Profile.WithCharacterAppearance( Profile.Appearance.WithHairStyleName(newStyle.id)); IsDirty = true; }; _hairPicker.OnColorChanged += newColor => { if (Profile is null) return; Profile = Profile.WithCharacterAppearance( Profile.Appearance.WithHairColor(newColor.marking.MarkingColors[0])); UpdateCMarkingsHair(); IsDirty = true; }; _facialHairPicker.OnMarkingSelect += newStyle => { if (Profile is null) return; Profile = Profile.WithCharacterAppearance( Profile.Appearance.WithFacialHairStyleName(newStyle.id)); IsDirty = true; }; _facialHairPicker.OnColorChanged += newColor => { if (Profile is null) return; Profile = Profile.WithCharacterAppearance( Profile.Appearance.WithFacialHairColor(newColor.marking.MarkingColors[0])); UpdateCMarkingsFacialHair(); IsDirty = true; }; _hairPicker.OnSlotRemove += _ => { if (Profile is null) return; Profile = Profile.WithCharacterAppearance( Profile.Appearance.WithHairStyleName(HairStyles.DefaultHairStyle) ); UpdateHairPickers(); UpdateCMarkingsHair(); IsDirty = true; }; _facialHairPicker.OnSlotRemove += _ => { if (Profile is null) return; Profile = Profile.WithCharacterAppearance( Profile.Appearance.WithFacialHairStyleName(HairStyles.DefaultFacialHairStyle) ); UpdateHairPickers(); UpdateCMarkingsFacialHair(); IsDirty = true; }; _hairPicker.OnSlotAdd += delegate() { if (Profile is null) return; var hair = _markingManager.MarkingsByCategoryAndSpecies(MarkingCategories.Hair, Profile.Species).Keys .FirstOrDefault(); if (string.IsNullOrEmpty(hair)) return; Profile = Profile.WithCharacterAppearance( Profile.Appearance.WithHairStyleName(hair) ); UpdateHairPickers(); UpdateCMarkingsHair(); IsDirty = true; }; _facialHairPicker.OnSlotAdd += delegate() { if (Profile is null) return; var hair = _markingManager.MarkingsByCategoryAndSpecies(MarkingCategories.FacialHair, Profile.Species).Keys .FirstOrDefault(); if (string.IsNullOrEmpty(hair)) return; Profile = Profile.WithCharacterAppearance( Profile.Appearance.WithFacialHairStyleName(hair) ); UpdateHairPickers(); UpdateCMarkingsFacialHair(); IsDirty = true; }; #endregion Hair #region Clothing _clothingButton.AddItem(Loc.GetString("humanoid-profile-editor-preference-jumpsuit"), (int) ClothingPreference.Jumpsuit); _clothingButton.AddItem(Loc.GetString("humanoid-profile-editor-preference-jumpskirt"), (int) ClothingPreference.Jumpskirt); _clothingButton.OnItemSelected += args => { _clothingButton.SelectId(args.Id); SetClothing((ClothingPreference) args.Id); }; #endregion Clothing #region Backpack _backpackButton.AddItem(Loc.GetString("humanoid-profile-editor-preference-backpack"), (int) BackpackPreference.Backpack); _backpackButton.AddItem(Loc.GetString("humanoid-profile-editor-preference-satchel"), (int) BackpackPreference.Satchel); _backpackButton.AddItem(Loc.GetString("humanoid-profile-editor-preference-duffelbag"), (int) BackpackPreference.Duffelbag); _backpackButton.OnItemSelected += args => { _backpackButton.SelectId(args.Id); SetBackpack((BackpackPreference) args.Id); }; #endregion Backpack #region Eyes _eyesPicker.OnEyeColorPicked += newColor => { if (Profile is null) return; Profile = Profile.WithCharacterAppearance( Profile.Appearance.WithEyeColor(newColor)); CMarkings.CurrentEyeColor = Profile.Appearance.EyeColor; IsDirty = true; }; #endregion Eyes #endregion Appearance #region Jobs _tabContainer.SetTabTitle(1, Loc.GetString("humanoid-profile-editor-jobs-tab")); _preferenceUnavailableButton.AddItem( Loc.GetString("humanoid-profile-editor-preference-unavailable-stay-in-lobby-button"), (int) PreferenceUnavailableMode.StayInLobby); _preferenceUnavailableButton.AddItem( Loc.GetString("humanoid-profile-editor-preference-unavailable-spawn-as-overflow-button", ("overflowJob", Loc.GetString(SharedGameTicker.FallbackOverflowJobName))), (int) PreferenceUnavailableMode.SpawnAsOverflow); _preferenceUnavailableButton.OnItemSelected += args => { _preferenceUnavailableButton.SelectId(args.Id); Profile = Profile?.WithPreferenceUnavailable((PreferenceUnavailableMode) args.Id); IsDirty = true; }; _jobPriorities = new List(); _jobCategories = new Dictionary(); _requirements = IoCManager.Resolve(); _requirements.Updated += UpdateRoleRequirements; UpdateRoleRequirements(); #endregion Jobs #region Antags _tabContainer.SetTabTitle(2, Loc.GetString("humanoid-profile-editor-antags-tab")); _antagPreferences = new List(); foreach (var antag in prototypeManager.EnumeratePrototypes().OrderBy(a => Loc.GetString(a.Name))) { if (!antag.SetPreference) { continue; } var selector = new AntagPreferenceSelector(antag); _antagList.AddChild(selector); _antagPreferences.Add(selector); selector.PreferenceChanged += preference => { Profile = Profile?.WithAntagPreference(antag.ID, preference); IsDirty = true; }; } #endregion Antags #region Traits var traits = prototypeManager.EnumeratePrototypes().OrderBy(t => Loc.GetString(t.Name)).ToList(); _traitPreferences = new List(); _tabContainer.SetTabTitle(3, Loc.GetString("humanoid-profile-editor-traits-tab")); if (traits.Count > 0) { foreach (var trait in traits) { var selector = new TraitPreferenceSelector(trait); _traitsList.AddChild(selector); _traitPreferences.Add(selector); selector.PreferenceChanged += preference => { Profile = Profile?.WithTraitPreference(trait.ID, preference); IsDirty = true; }; } } else { _traitsList.AddChild(new Label { Text = "No traits available :(", FontColorOverride = Color.Gray, }); } #endregion #region Save _saveButton.OnPressed += _ => { Save(); }; #endregion Save #region Markings _tabContainer.SetTabTitle(4, Loc.GetString("humanoid-profile-editor-markings-tab")); CMarkings.OnMarkingAdded += OnMarkingChange; CMarkings.OnMarkingRemoved += OnMarkingChange; CMarkings.OnMarkingColorChange += OnMarkingChange; CMarkings.OnMarkingRankChange += OnMarkingChange; #endregion Markings #region FlavorText if (_configurationManager.GetCVar(CCVars.FlavorText)) { var 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; } #endregion FlavorText #region Dummy var species = Profile?.Species ?? SharedHumanoidAppearanceSystem.DefaultSpecies; var dollProto = _prototypeManager.Index(species).DollPrototype; if (_previewDummy != null) _entMan.DeleteEntity(_previewDummy!.Value); _previewDummy = _entMan.SpawnEntity(dollProto, MapCoordinates.Nullspace); _lastSpecies = species; _previewSprite = new SpriteView { Scale = new Vector2(6, 6), OverrideDirection = Direction.South, VerticalAlignment = VAlignment.Center, SizeFlagsStretchRatio = 1 }; _previewSprite.SetEntity(_previewDummy.Value); _previewSpriteControl.AddChild(_previewSprite); _previewSpriteSide = new SpriteView { Scale = new Vector2(6, 6), OverrideDirection = Direction.East, VerticalAlignment = VAlignment.Center, SizeFlagsStretchRatio = 1 }; _previewSpriteSide.SetEntity(_previewDummy.Value); _previewSpriteSideControl.AddChild(_previewSpriteSide); #endregion Dummy #endregion Left if (preferencesManager.ServerDataLoaded) { LoadServerData(); } preferencesManager.OnServerDataLoaded += LoadServerData; IsDirty = false; } private void ToggleClothes(BaseButton.ButtonEventArgs obj) { RebuildSpriteView(); } private void UpdateRoleRequirements() { _jobList.DisposeAllChildren(); _jobPriorities.Clear(); _jobCategories.Clear(); var firstCategory = true; foreach (var department in _prototypeManager.EnumeratePrototypes()) { var departmentName = Loc.GetString($"department-{department.ID}"); 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(o => _prototypeManager.Index(o)).Where(o => o.SetPreference).ToList(); jobs.Sort((x, y) => -string.Compare(x.LocalizedName, y.LocalizedName, StringComparison.CurrentCultureIgnoreCase)); foreach (var job in jobs) { var selector = new JobPrioritySelector(job, _prototypeManager); if (!_requirements.IsAllowed(job, out var reason)) { selector.LockRequirements(reason); } category.AddChild(selector); _jobPriorities.Add(selector); selector.PriorityChanged += priority => { Profile = Profile?.WithJobPriority(job.ID, priority); IsDirty = true; foreach (var jobSelector in _jobPriorities) { // Sync other selectors with the same job in case of multiple department jobs if (jobSelector.Job == selector.Job) { jobSelector.Priority = priority; } // Lower any other high priorities to medium. if (priority == JobPriority.High) { if (jobSelector.Job != selector.Job && jobSelector.Priority == JobPriority.High) { jobSelector.Priority = JobPriority.Medium; Profile = Profile?.WithJobPriority(jobSelector.Job.ID, JobPriority.Medium); } } } }; } } } private void OnFlavorTextChange(string content) { if (Profile is null) return; Profile = Profile.WithFlavorText(content); IsDirty = true; } private void OnMarkingChange(MarkingSet markings) { if (Profile is null) return; Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithMarkings(markings.GetForwardEnumerator().ToList())); _needUpdatePreview = true; IsDirty = true; } private void OnMarkingColorChange(List markings) { if (Profile is null) return; Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithMarkings(markings)); IsDirty = true; } private void OnSkinColorOnValueChanged() { if (Profile is null) return; var skin = _prototypeManager.Index(Profile.Species).SkinColoration; switch (skin) { case HumanoidSkinColor.HumanToned: { if (!_skinColor.Visible) { _skinColor.Visible = true; _rgbSkinColorContainer.Visible = false; } var color = SkinColor.HumanSkinTone((int) _skinColor.Value); CMarkings.CurrentSkinColor = color; Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color));// break; } case HumanoidSkinColor.Hues: { if (!_rgbSkinColorContainer.Visible) { _skinColor.Visible = false; _rgbSkinColorContainer.Visible = true; } CMarkings.CurrentSkinColor = _rgbSkinColorSelector.Color; Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(_rgbSkinColorSelector.Color)); break; } case HumanoidSkinColor.TintedHues: { if (!_rgbSkinColorContainer.Visible) { _skinColor.Visible = false; _rgbSkinColorContainer.Visible = true; } var color = SkinColor.TintedHues(_rgbSkinColorSelector.Color); CMarkings.CurrentSkinColor = color; Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color)); break; } } IsDirty = true; } protected override void Dispose(bool disposing) { base.Dispose(disposing); if (!disposing) return; if (_previewDummy != null) _entMan.DeleteEntity(_previewDummy.Value); _requirements.Updated -= UpdateRoleRequirements; _preferencesManager.OnServerDataLoaded -= LoadServerData; } private void RebuildSpriteView() { var species = Profile?.Species ?? SharedHumanoidAppearanceSystem.DefaultSpecies; var dollProto = _prototypeManager.Index(species).DollPrototype; if (_previewDummy != null) _entMan.DeleteEntity(_previewDummy!.Value); _previewDummy = _entMan.SpawnEntity(dollProto, MapCoordinates.Nullspace); _lastSpecies = species; if (_previewSprite == null) { // Front _previewSprite = new SpriteView { Scale = new Vector2(6, 6), OverrideDirection = Direction.South, VerticalAlignment = VAlignment.Center, SizeFlagsStretchRatio = 1 }; _previewSpriteControl.AddChild(_previewSprite); } _previewSprite.SetEntity(_previewDummy.Value); if (_previewSpriteSide == null) { _previewSpriteSide = new SpriteView { Scale = new Vector2(6, 6), OverrideDirection = Direction.East, VerticalAlignment = VAlignment.Center, SizeFlagsStretchRatio = 1 }; _previewSpriteSideControl.AddChild(_previewSpriteSide); } _previewSpriteSide.SetEntity(_previewDummy.Value); _needUpdatePreview = true; } private void LoadServerData() { Profile = (HumanoidCharacterProfile) _preferencesManager.Preferences!.SelectedCharacter; CharacterSlot = _preferencesManager.Preferences.SelectedCharacterIndex; UpdateControls(); _needUpdatePreview = true; } private void SetAge(int newAge) { Profile = Profile?.WithAge(newAge); IsDirty = true; } 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(); IsDirty = true; } private void SetGender(Gender newGender) { Profile = Profile?.WithGender(newGender); IsDirty = true; } private void SetSpecies(string newSpecies) { Profile = Profile?.WithSpecies(newSpecies); OnSkinColorOnValueChanged(); // Species may have special color prefs, make sure to update it. CMarkings.SetSpecies(newSpecies); // Repopulate the markings tab as well. UpdateSexControls(); // update sex for new species RebuildSpriteView(); // they might have different inv so we need a new dummy IsDirty = true; _needUpdatePreview = true; } private void SetName(string newName) { Profile = Profile?.WithName(newName); IsDirty = true; } private void SetClothing(ClothingPreference newClothing) { Profile = Profile?.WithClothingPreference(newClothing); IsDirty = true; } private void SetBackpack(BackpackPreference newBackpack) { Profile = Profile?.WithBackpackPreference(newBackpack); IsDirty = true; } public void Save() { IsDirty = false; if (Profile != null) { _preferencesManager.UpdateCharacter(Profile, CharacterSlot); OnProfileChanged?.Invoke(Profile, CharacterSlot); _needUpdatePreview = true; } } private bool IsDirty { get => _isDirty; set { _isDirty = value; _needUpdatePreview = true; 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() ?? ""; } 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.TryIndex(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; switch (skin) { case HumanoidSkinColor.HumanToned: { if (!_skinColor.Visible) { _skinColor.Visible = true; _rgbSkinColorContainer.Visible = false; } _skinColor.Value = SkinColor.HumanSkinToneFromColor(Profile.Appearance.SkinColor); break; } case HumanoidSkinColor.Hues: { if (!_rgbSkinColorContainer.Visible) { _skinColor.Visible = false; _rgbSkinColorContainer.Visible = true; } // set the RGB values to the direct values otherwise _rgbSkinColorSelector.Color = Profile.Appearance.SkinColor; break; } case HumanoidSkinColor.TintedHues: { if (!_rgbSkinColorContainer.Visible) { _skinColor.Visible = false; _rgbSkinColorContainer.Visible = true; } // set the RGB values to the direct values otherwise _rgbSkinColorSelector.Color = Profile.Appearance.SkinColor; break; } } } private void UpdateMarkings() { if (Profile == null) { return; } CMarkings.SetData(Profile.Appearance.Markings, Profile.Species, Profile.Appearance.SkinColor, Profile.Appearance.EyeColor ); } private void UpdateSpecies() { if (Profile == null) { return; } CSpeciesButton.Select(_speciesList.FindIndex(x => x.ID == Profile.Species)); } private void UpdateGenderControls() { if (Profile == null) { return; } _genderButton.SelectId((int) Profile.Gender); } private void UpdateClothingControls() { if (Profile == null) { return; } _clothingButton.SelectId((int) Profile.Clothing); } private void UpdateBackpackControls() { if (Profile == null) { return; } _backpackButton.SelectId((int) Profile.Backpack); } private void UpdateHairPickers() { if (Profile == null) { return; } var hairMarking = Profile.Appearance.HairStyleId switch { HairStyles.DefaultHairStyle => new List(), _ => new() { new(Profile.Appearance.HairStyleId, new List() { Profile.Appearance.HairColor }) }, }; var facialHairMarking = Profile.Appearance.FacialHairStyleId switch { HairStyles.DefaultFacialHairStyle => new List(), _ => new() { new(Profile.Appearance.FacialHairStyleId, new List() { Profile.Appearance.FacialHairColor }) }, }; _hairPicker.UpdateData( hairMarking, Profile.Species, 1); _facialHairPicker.UpdateData( facialHairMarking, Profile.Species, 1); } private void UpdateCMarkingsHair() { if (Profile == null) { return; } // hair color Color? hairColor = null; if ( Profile.Appearance.HairStyleId != HairStyles.DefaultHairStyle && _markingManager.Markings.TryGetValue(Profile.Appearance.HairStyleId, out var hairProto) ) { if (_markingManager.CanBeApplied(Profile.Species, hairProto, _prototypeManager)) { if (_markingManager.MustMatchSkin(Profile.Species, HumanoidVisualLayers.Hair, out var _, _prototypeManager)) { hairColor = Profile.Appearance.SkinColor; } else { hairColor = Profile.Appearance.HairColor; } } } if (hairColor != null) { CMarkings.HairMarking = new (Profile.Appearance.HairStyleId, new List() { hairColor.Value }); } else { CMarkings.HairMarking = null; } } private void UpdateCMarkingsFacialHair() { if (Profile == null) { return; } // facial hair color Color? facialHairColor = null; if ( Profile.Appearance.FacialHairStyleId != HairStyles.DefaultFacialHairStyle && _markingManager.Markings.TryGetValue(Profile.Appearance.FacialHairStyleId, out var facialHairProto) ) { if (_markingManager.CanBeApplied(Profile.Species, facialHairProto, _prototypeManager)) { if (_markingManager.MustMatchSkin(Profile.Species, HumanoidVisualLayers.Hair, out var _, _prototypeManager)) { facialHairColor = Profile.Appearance.SkinColor; } else { facialHairColor = Profile.Appearance.FacialHairColor; } } } if (facialHairColor != null) { CMarkings.FacialHairMarking = new (Profile.Appearance.FacialHairStyleId, new List() { facialHairColor.Value }); } else { CMarkings.FacialHairMarking = null; } } private void UpdateEyePickers() { if (Profile == null) { return; } CMarkings.CurrentEyeColor = Profile.Appearance.EyeColor; _eyesPicker.SetData(Profile.Appearance.EyeColor); } private void UpdateSaveButton() { _saveButton.Disabled = Profile is null || !IsDirty; } private void UpdatePreview() { if (Profile is null) return; var humanoid = _entMan.System(); humanoid.LoadProfile(_previewDummy!.Value, Profile); if (ShowClothes.Pressed) LobbyCharacterPreviewPanel.GiveDummyJobClothes(_previewDummy!.Value, Profile); } public void UpdateControls() { if (Profile is null) return; UpdateNameEdit(); UpdateFlavorTextEdit(); UpdateSexControls(); UpdateGenderControls(); UpdateSkinColor(); UpdateSpecies(); UpdateClothingControls(); UpdateBackpackControls(); UpdateAgeEdit(); UpdateEyePickers(); UpdateSaveButton(); UpdateJobPriorities(); UpdateAntagPreferences(); UpdateTraitPreferences(); UpdateMarkings(); RebuildSpriteView(); UpdateHairPickers(); UpdateCMarkingsHair(); UpdateCMarkingsFacialHair(); _preferenceUnavailableButton.SelectId((int) Profile.PreferenceUnavailable); } protected override void FrameUpdate(FrameEventArgs args) { base.FrameUpdate(args); if (_needUpdatePreview) { UpdatePreview(); _needUpdatePreview = false; } } private void UpdateJobPriorities() { foreach (var prioritySelector in _jobPriorities) { var jobId = prioritySelector.Job.ID; var priority = Profile?.JobPriorities.GetValueOrDefault(jobId, JobPriority.Never) ?? JobPriority.Never; prioritySelector.Priority = priority; } } private sealed class JobPrioritySelector : Control { public JobPrototype Job { get; } private readonly RadioOptions _optionButton; public JobPriority Priority { get => (JobPriority) _optionButton.SelectedValue; set => _optionButton.SelectByValue((int) value); } public event Action? PriorityChanged; private StripeBack _lockStripe; private Label _requirementsLabel; private Label _jobTitle; public JobPrioritySelector(JobPrototype job, IPrototypeManager prototypeManager) { Job = job; _optionButton = new RadioOptions(RadioOptionsLayout.Horizontal) { FirstButtonStyle = StyleBase.ButtonOpenRight, ButtonStyle = StyleBase.ButtonOpenBoth, LastButtonStyle = StyleBase.ButtonOpenLeft }; //Override default radio option button width _optionButton.GenerateItem = GenerateButton; // Text, Value _optionButton.AddItem(Loc.GetString("humanoid-profile-editor-job-priority-high-button"), (int) JobPriority.High); _optionButton.AddItem(Loc.GetString("humanoid-profile-editor-job-priority-medium-button"), (int) JobPriority.Medium); _optionButton.AddItem(Loc.GetString("humanoid-profile-editor-job-priority-low-button"), (int) JobPriority.Low); _optionButton.AddItem(Loc.GetString("humanoid-profile-editor-job-priority-never-button"), (int) JobPriority.Never); _optionButton.OnItemSelected += args => { _optionButton.Select(args.Id); PriorityChanged?.Invoke(Priority); }; var icon = new TextureRect { TextureScale = new Vector2(2, 2), Stretch = TextureRect.StretchMode.KeepCentered }; var jobIcon = prototypeManager.Index(job.Icon); icon.Texture = jobIcon.Icon.Frame0(); _requirementsLabel = new Label() { Text = Loc.GetString("role-timer-locked"), Visible = true, HorizontalAlignment = HAlignment.Center, StyleClasses = {StyleBase.StyleClassLabelSubText}, }; _lockStripe = new StripeBack() { Visible = false, HorizontalExpand = true, MouseFilter = MouseFilterMode.Stop, Children = { _requirementsLabel } }; _jobTitle = new Label() { Margin = new Thickness(5f,0,5f,0), Text = job.LocalizedName, MinSize = new Vector2(200, 0), MouseFilter = MouseFilterMode.Stop }; if (job.LocalizedDescription != null) { _jobTitle.ToolTip = job.LocalizedDescription; } AddChild(new BoxContainer { Orientation = LayoutOrientation.Horizontal, Children = { icon, _jobTitle, _optionButton, _lockStripe, } }); } public void LockRequirements(string requirements) { _lockStripe.ToolTip = requirements; _lockStripe.Visible = true; _optionButton.Visible = false; } // TODO: Subscribe to roletimers event. I am too lazy to do this RN But I doubt most people will notice fn public void UnlockRequirements() { _requirementsLabel.Visible = false; _lockStripe.Visible = false; _optionButton.Visible = true; } private Button GenerateButton(string text, int value) { var btn = new Button { Text = text, MinWidth = 90 }; return btn; } } private void UpdateAntagPreferences() { foreach (var preferenceSelector in _antagPreferences) { var antagId = preferenceSelector.Antag.ID; var preference = Profile?.AntagPreferences.Contains(antagId) ?? false; preferenceSelector.Preference = preference; } } private void UpdateTraitPreferences() { foreach (var preferenceSelector in _traitPreferences) { var traitId = preferenceSelector.Trait.ID; var preference = Profile?.TraitPreferences.Contains(traitId) ?? false; preferenceSelector.Preference = preference; } } private sealed class AntagPreferenceSelector : Control { public AntagPrototype Antag { get; } private readonly CheckBox _checkBox; public bool Preference { get => _checkBox.Pressed; set => _checkBox.Pressed = value; } public event Action? PreferenceChanged; public AntagPreferenceSelector(AntagPrototype antag) { Antag = antag; _checkBox = new CheckBox {Text = Loc.GetString(antag.Name)}; _checkBox.OnToggled += OnCheckBoxToggled; if (antag.Description != null) { _checkBox.ToolTip = Loc.GetString(antag.Description); } AddChild(new BoxContainer { Orientation = LayoutOrientation.Horizontal, Children = { _checkBox } }); } private void OnCheckBoxToggled(BaseButton.ButtonToggledEventArgs args) { PreferenceChanged?.Invoke(Preference); } } private sealed class TraitPreferenceSelector : Control { public TraitPrototype Trait { get; } private readonly CheckBox _checkBox; public bool Preference { get => _checkBox.Pressed; set => _checkBox.Pressed = value; } public event Action? PreferenceChanged; public TraitPreferenceSelector(TraitPrototype trait) { Trait = trait; _checkBox = new CheckBox {Text = Loc.GetString(trait.Name)}; _checkBox.OnToggled += OnCheckBoxToggled; if (trait.Description is { } desc) { _checkBox.ToolTip = Loc.GetString(desc); } AddChild(new BoxContainer { Orientation = LayoutOrientation.Horizontal, Children = { _checkBox }, }); } private void OnCheckBoxToggled(BaseButton.ButtonToggledEventArgs args) { PreferenceChanged?.Invoke(Preference); } } } }