From f59765c072e4c194c74524cc85a02c7ee8ca33b2 Mon Sep 17 00:00:00 2001 From: Spatison <137375981+Spatison@users.noreply.github.com> Date: Tue, 16 Jul 2024 18:31:28 +0300 Subject: [PATCH] =?UTF-8?q?=D0=92=D0=BD=D0=B5=D1=88=D0=BD=D0=B8=D0=B9=20?= =?UTF-8?q?=D0=B2=D0=B8=D0=B4=20=D0=BC=D0=B0=D0=B3=D0=B0=20(#444)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add: Magic mirror * add: Wizard appearance --- Content.Client/Content.Client.csproj | 5 + .../Mirror/WizardMirrorBoundUserInterface.cs | 53 ++ .../Wizard/Mirror/WizardMirrorWindow.cs | 692 ++++++++++++++++++ .../Wizard/Mirror/WizardMirrorWindow.xaml | 85 +++ .../MagicMirror/MagicMirrorSystem.cs | 2 +- .../Wizard/Mirror/WizardMirrorSystem.cs | 137 ++++ .../_White/Wizard/WizardComponent.cs | 18 + .../_White/Wizard/WizardRuleSystem.cs | 56 +- .../Wizard/Mirror/SharedWizardMirrorSystem.cs | 29 + .../Wizard/Mirror/WizardMirrorComponent.cs | 8 + .../Locale/ru-RU/_white/wizard/mirror.ftl | 2 + Resources/Maps/White/Shuttles/wizard.yml | 48 +- .../Entities/Structures/Wallmounts/mirror.yml | 21 + .../_White/Wizard/Appearance/facial_hair.yml | 13 + .../_White/Wizard/Appearance/hair.yml | 14 + .../_White/Wizard/Appearance/hair_color.yml | 13 + .../_White/Wizard/Appearance/names.yml | 13 + 17 files changed, 1155 insertions(+), 54 deletions(-) create mode 100644 Content.Client/_White/Wizard/Mirror/WizardMirrorBoundUserInterface.cs create mode 100644 Content.Client/_White/Wizard/Mirror/WizardMirrorWindow.cs create mode 100644 Content.Client/_White/Wizard/Mirror/WizardMirrorWindow.xaml create mode 100644 Content.Server/_White/Wizard/Mirror/WizardMirrorSystem.cs create mode 100644 Content.Shared/_White/Wizard/Mirror/SharedWizardMirrorSystem.cs create mode 100644 Content.Shared/_White/Wizard/Mirror/WizardMirrorComponent.cs create mode 100644 Resources/Locale/ru-RU/_white/wizard/mirror.ftl create mode 100644 Resources/Prototypes/_White/Entities/Structures/Wallmounts/mirror.yml create mode 100644 Resources/Prototypes/_White/Wizard/Appearance/facial_hair.yml create mode 100644 Resources/Prototypes/_White/Wizard/Appearance/hair.yml create mode 100644 Resources/Prototypes/_White/Wizard/Appearance/hair_color.yml create mode 100644 Resources/Prototypes/_White/Wizard/Appearance/names.yml diff --git a/Content.Client/Content.Client.csproj b/Content.Client/Content.Client.csproj index 43fd8df2e1..62ab76ee21 100644 --- a/Content.Client/Content.Client.csproj +++ b/Content.Client/Content.Client.csproj @@ -23,6 +23,11 @@ + + + WizardMirrorWindow.xaml + + diff --git a/Content.Client/_White/Wizard/Mirror/WizardMirrorBoundUserInterface.cs b/Content.Client/_White/Wizard/Mirror/WizardMirrorBoundUserInterface.cs new file mode 100644 index 0000000000..e7cdf5356a --- /dev/null +++ b/Content.Client/_White/Wizard/Mirror/WizardMirrorBoundUserInterface.cs @@ -0,0 +1,53 @@ +using Content.Shared._White.Wizard.Mirror; +using Content.Shared.Preferences; +using Robust.Shared.Prototypes; + +namespace Content.Client._White.Wizard.Mirror; + +public sealed class WizardMirrorBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey) +{ + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + + [ViewVariables] + private WizardMirrorWindow? _window; + + protected override void Open() + { + base.Open(); + + _window = new(_prototypeManager); + + _window.OnSave += Save; + + _window.OnClose += Close; + _window.OpenCentered(); + } + + private void Save(HumanoidCharacterProfile profile) + { + SendMessage(new WizardMirrorSave(profile)); + } + + protected override void UpdateState(BoundUserInterfaceState state) + { + base.UpdateState(state); + + if (state is not WizardMirrorUiState data || _window == null) + return; + + _window.UpdateState(data); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (!disposing) + return; + + if (_window != null) + _window.OnClose -= Close; + + _window?.Dispose(); + } +} + diff --git a/Content.Client/_White/Wizard/Mirror/WizardMirrorWindow.cs b/Content.Client/_White/Wizard/Mirror/WizardMirrorWindow.cs new file mode 100644 index 0000000000..5053205b2b --- /dev/null +++ b/Content.Client/_White/Wizard/Mirror/WizardMirrorWindow.cs @@ -0,0 +1,692 @@ +using System.Linq; +using Content.Client._White.Sponsors; +using Content.Client.Humanoid; +using Content.Shared._White.Wizard.Mirror; +using Content.Shared.Humanoid; +using Content.Shared.Humanoid.Markings; +using Content.Shared.Preferences; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.CustomControls; +using Robust.Client.UserInterface.XAML; +using Content.Shared._White.TTS; +using Content.Shared.Humanoid.Prototypes; +using Robust.Shared.Enums; +using Robust.Shared.Prototypes; + +namespace Content.Client._White.Wizard.Mirror; + +[GenerateTypedNameReferences] +public sealed partial class WizardMirrorWindow : DefaultWindow +{ + private LineEdit NameEdit => CNameEdit; + + private Button SaveButton => CSaveButton; + public Action? OnSave; + + private OptionButton SexButton => CSexButton; + + private LineEdit AgeEdit => CAgeEdit; + + private OptionButton GenderButton => CPronounsButton; + + private OptionButton VoiceButton => CVoiceButton; + + private OptionButton BodyTypesButton => CBodyTypesButton; + + private OptionButton SpeciesButton => CSpeciesButton; + + private Slider SkinColorSlider => CSkin; + private BoxContainer RgbSkinColorContainer => CRgbSkinColorContainer; + private ColorSelectorSliders _rgbSkinColorSelector; + + // Hair + private SingleMarkingPicker HairPicker => CHairStylePicker; + private SingleMarkingPicker FacialHairPicker => CFacialHairPicker; + + private EyeColorPicker EyesPicker => CEyeColorPicker; + + private bool _isDirty; + public HumanoidCharacterProfile? Profile; + + private readonly MarkingManager _markingManager; + private readonly IPrototypeManager _prototypeManager; + + private List _bodyTypesList = new(); + + private readonly List _speciesList; + + private List _voiceList = default!; + private const string AnySexVoiceProto = "SponsorAnySexVoices"; + + public WizardMirrorWindow(IPrototypeManager prototypeManager) + { + RobustXamlLoader.Load(this); + + _markingManager = IoCManager.Resolve(); + _prototypeManager = prototypeManager; + + Profile ??= HumanoidCharacterProfile.RandomWithSpecies(); + + SaveButton.OnPressed += _ => OnSave!(Profile); + + _voiceList = _prototypeManager.EnumeratePrototypes().Where(o => o.RoundStart).ToList(); + + #region Voice + + VoiceButton.OnItemSelected += args => + { + VoiceButton.SelectId(args.Id); + SetVoice(_voiceList[args.Id].ID); + }; + + #endregion + + #region Name + + NameEdit.OnTextChanged += args => { SetName(args.Text); }; + + #endregion + + #region Age + + AgeEdit.OnTextChanged += args => + { + if (!int.TryParse(args.Text, out var newAge)) + return; + SetAge(newAge); + }; + + #endregion Age + + #region Sex + + SexButton.OnItemSelected += args => + { + SexButton.SelectId(args.Id); + SetSex((Sex) args.Id); + }; + + #endregion Sex + + #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 Body Type + + BodyTypesButton.OnItemSelected += OnBodyTypeSelected; + + UpdateBodyTypes(); + + #endregion Body Type + + #region Skin + + + SkinColorSlider.OnValueChanged += _ => + { + OnSkinColorOnValueChanged(); + }; + + RgbSkinColorContainer.AddChild(_rgbSkinColorSelector = new ColorSelectorSliders()); + _rgbSkinColorSelector.OnColorChanged += _ => + { + OnSkinColorOnValueChanged(); + }; + + #endregion + + #region Species + + _speciesList = prototypeManager.EnumeratePrototypes().Where(o => o.RoundStart).ToList(); + + for (var i = 0; i < _speciesList.Count; i++) + { + var specie = _speciesList[i]; + var name = Loc.GetString(specie.Name); + + SpeciesButton.AddItem(name, i); + } + + SpeciesButton.OnItemSelected += args => + { + SpeciesButton.SelectId(args.Id); + SetSpecies(_speciesList[args.Id].ID); + UpdateHairPickers(); + OnSkinColorOnValueChanged(); + }; + + #endregion Species + + #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 Eyes + + EyesPicker.OnEyeColorPicked += newColor => + { + if (Profile is null) + return; + Profile = Profile.WithCharacterAppearance( + Profile.Appearance.WithEyeColor(newColor)); + IsDirty = true; + }; + + #endregion Eyes + } + + #region Set + + private void SetAge(int newAge) + { + Profile = Profile?.WithAge(newAge); + IsDirty = true; + } + + private void SetSex(Sex newSex) + { + Profile = Profile?.WithSex(newSex); + 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(); + UpdateTtsVoicesControls(); + IsDirty = true; + } + + private void SetVoice(string newVoice) + { + Profile = Profile?.WithVoice(newVoice); + IsDirty = true; + } + + private void SetGender(Gender newGender) + { + Profile = Profile?.WithGender(newGender); + IsDirty = true; + } + + private void SetSpecies(string newSpecies) + { + Profile = Profile?.WithSpecies(newSpecies); + OnSkinColorOnValueChanged(); + UpdateSexControls(); + UpdateBodyTypes(); + IsDirty = true; + } + + private void SetName(string newName) + { + Profile = Profile?.WithName(newName); + IsDirty = true; + } + + private void SetBodyType(string newBodyType) + { + Profile = Profile?.WithBodyType(newBodyType); + IsDirty = true; + } + + #endregion + + #region Update + + private void UpdateSaveButton() + { + SaveButton.Disabled = Profile is null || !IsDirty; + } + + private void UpdateNamesEdit() + { + NameEdit.Text = Profile?.Name ?? ""; + } + + private void UpdateGenderControls() + { + if (Profile == null) + return; + + GenderButton.SelectId((int) Profile.Gender); + } + + private void UpdateTtsVoicesControls() + { + if (Profile is null) + return; + + var sponsorsManager = IoCManager.Resolve(); + + VoiceButton.Clear(); + + var firstVoiceChoiceId = 1; + for (var i = 0; i < _voiceList.Count; i++) + { + var voice = _voiceList[i]; + if (!HumanoidCharacterProfile.CanHaveVoice(voice, Profile.Sex)) + { + if (!sponsorsManager.TryGetInfo(out var sponsorInfo) + || !sponsorInfo.AllowedMarkings.Contains(AnySexVoiceProto)) + continue; + } + + var name = Loc.GetString(voice.Name); + VoiceButton.AddItem(name, i); + + if (firstVoiceChoiceId == 1) + firstVoiceChoiceId = i; + + if (voice.SponsorOnly && + sponsorsManager.TryGetInfo(out var sponsor) && + !sponsor.AllowedMarkings.Contains(voice.ID)) + { + VoiceButton.SetItemDisabled(i, true); + } + } + + var voiceChoiceId = _voiceList.FindIndex(x => x.ID == Profile.Voice); + if (!VoiceButton.TrySelectId(voiceChoiceId) && + VoiceButton.TrySelectId(firstVoiceChoiceId)) + { + SetVoice(_voiceList[firstVoiceChoiceId].ID); + } + } + + private void UpdateSexControls() + { + if (Profile == null) + return; + + SexButton.Clear(); + + var sexes = new List(); + + if (!_prototypeManager.TryIndex(Profile.Species, out var speciesProto)) + sexes.Add(Sex.Unsexed); + else + sexes.AddRange(speciesProto.Sexes); + + 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 UpdateBodyTypes() + { + if (Profile is null) + return; + + BodyTypesButton.Clear(); + var species = _prototypeManager.Index(Profile.Species); + var sex = Profile.Sex; + _bodyTypesList = EntitySystem.Get().GetValidBodyTypes(species, sex); + + for (var i = 0; i < _bodyTypesList.Count; i++) + { + BodyTypesButton.AddItem(Loc.GetString(_bodyTypesList[i].Name), i); + } + + if (!_bodyTypesList.Select(proto => proto.ID).Contains(Profile.BodyType.Id)) + SetBodyType(_bodyTypesList.First().ID); + + BodyTypesButton.Select(_bodyTypesList.FindIndex(x => x.ID == Profile.BodyType)); + IsDirty = true; + } + + private void UpdateHairPickers() + { + if (Profile == null) + return; + + var hairMarking = Profile.Appearance.HairStyleId switch + { + HairStyles.DefaultHairStyle => new List(), + _ => new List { new(Profile.Appearance.HairStyleId, new List { Profile.Appearance.HairColor }) }, + }; + + var facialHairMarking = Profile.Appearance.FacialHairStyleId switch + { + HairStyles.DefaultFacialHairStyle => new List(), + _ => new List { new(Profile.Appearance.FacialHairStyleId, new List { Profile.Appearance.FacialHairColor }) }, + }; + + HairPicker.UpdateData(hairMarking, Profile.Species, 1); + FacialHairPicker.UpdateData(facialHairMarking, Profile.Species, 1); + } + + private void UpdateCMarkingsFacialHair() + { + if (Profile == null) + return; + + Color? facialHairColor = null; + if ( Profile.Appearance.FacialHairStyleId != HairStyles.DefaultFacialHairStyle && + _markingManager.Markings.TryGetValue(Profile.Appearance.FacialHairStyleId, out var facialHairProto) + ) + { + if (_markingManager.CanBeApplied(Profile.Species, Profile.Sex, facialHairProto, _prototypeManager)) + { + facialHairColor = _markingManager.MustMatchSkin(Profile.BodyType, HumanoidVisualLayers.Hair, out _, _prototypeManager) ? Profile.Appearance.SkinColor : Profile.Appearance.FacialHairColor; + } + } + } + + 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, Profile.Sex, hairProto, _prototypeManager)) + { + hairColor = _markingManager.MustMatchSkin(Profile.BodyType, HumanoidVisualLayers.Hair, out _, _prototypeManager) + ? Profile.Appearance.SkinColor + : Profile.Appearance.HairColor; + } + } + } + + private void UpdateSkinColor() + { + if (Profile == null) + return; + + var skin = _prototypeManager.Index(Profile.Species).SkinColoration; + + switch (skin) + { + case HumanoidSkinColor.HumanToned: + { + if (!SkinColorSlider.Visible) + { + SkinColorSlider.Visible = true; + _rgbSkinColorSelector.Visible = false; + } + + SkinColorSlider.Value = SkinColor.HumanSkinToneFromColor(Profile.Appearance.SkinColor); + + break; + } + case HumanoidSkinColor.Hues: + { + if (!_rgbSkinColorSelector.Visible) + { + SkinColorSlider.Visible = false; + _rgbSkinColorSelector.Visible = true; + } + + // set the RGB values to the direct values otherwise + _rgbSkinColorSelector.Color = Profile.Appearance.SkinColor; + break; + } + case HumanoidSkinColor.TintedHues: + { + if (!_rgbSkinColorSelector.Visible) + { + SkinColorSlider.Visible = false; + _rgbSkinColorSelector.Visible = true; + } + + // set the RGB values to the direct values otherwise + _rgbSkinColorSelector.Color = Profile.Appearance.SkinColor; + break; + } + } + } + + private void UpdateSpecies() + { + if (Profile == null) + { + return; + } + + if (!_speciesList.Exists(x => x.ID == Profile.Species)) + { + SpeciesButton.Select(0); + return; + } + + SpeciesButton.Select(_speciesList.FindIndex(x => x.ID == Profile.Species)); + } + + private void UpdateAgeEdit() + { + AgeEdit.Text = Profile?.Age.ToString() ?? ""; + } + + private void UpdateEyePickers() + { + if (Profile == null) + { + return; + } + + EyesPicker.SetData(Profile.Appearance.EyeColor); + } + + #endregion + + private void OnSkinColorOnValueChanged() + { + if (Profile is null) + return; + + var skin = _prototypeManager.Index(Profile.Species).SkinColoration; + + switch (skin) + { + case HumanoidSkinColor.HumanToned: + { + if (!SkinColorSlider.Visible) + { + SkinColorSlider.Visible = true; + RgbSkinColorContainer.Visible = false; + } + + var color = SkinColor.HumanSkinTone((int) SkinColorSlider.Value); + + Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color));// + break; + } + case HumanoidSkinColor.Hues: + { + if (!RgbSkinColorContainer.Visible) + { + SkinColorSlider.Visible = false; + RgbSkinColorContainer.Visible = true; + } + + Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(_rgbSkinColorSelector.Color)); + break; + } + case HumanoidSkinColor.TintedHues: + { + if (!RgbSkinColorContainer.Visible) + { + SkinColorSlider.Visible = false; + RgbSkinColorContainer.Visible = true; + } + + var color = SkinColor.TintedHues(_rgbSkinColorSelector.Color); + + Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color)); + break; + } + } + + IsDirty = true; + } + + private void OnBodyTypeSelected(OptionButton.ItemSelectedEventArgs args) + { + args.Button.SelectId(args.Id); + SetBodyType(_bodyTypesList[args.Id].ID); + } + + private bool IsDirty + { + get => _isDirty; + set + { + _isDirty = value; + UpdateSaveButton(); + } + } + + public void UpdateState(WizardMirrorUiState state) + { + Profile = state.Profile; + + UpdateNamesEdit(); + UpdateSexControls(); + UpdateGenderControls(); + UpdateSkinColor(); + UpdateSpecies(); + UpdateAgeEdit(); + UpdateEyePickers(); + UpdateSaveButton(); + UpdateHairPickers(); + UpdateCMarkingsHair(); + UpdateCMarkingsFacialHair(); + UpdateTtsVoicesControls(); + UpdateBodyTypes(); + } +} diff --git a/Content.Client/_White/Wizard/Mirror/WizardMirrorWindow.xaml b/Content.Client/_White/Wizard/Mirror/WizardMirrorWindow.xaml new file mode 100644 index 0000000000..64258d2f19 --- /dev/null +++ b/Content.Client/_White/Wizard/Mirror/WizardMirrorWindow.xaml @@ -0,0 +1,85 @@ + + + + +