From 8ea10a1d94e3239dbe80bffcab22c72b14de34ff Mon Sep 17 00:00:00 2001
From: Cinkafox <70429757+Cinkafox@users.noreply.github.com>
Date: Tue, 6 Jan 2026 16:57:28 +0300
Subject: [PATCH] [Add] Species selection menu (#1009)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* - add: species selection menu
* - fix: Кролькины фиксы ч.1
* - fix: кролькины фиксы ч.2
---
.../Lobby/UI/HumanoidProfileEditor.xaml | 5 +-
.../Lobby/UI/HumanoidProfileEditor.xaml.cs | 1 +
.../HumanoidProfileEditor.SpeciesSelection.cs | 50 ++++++
.../Controls/SpeciesGroupContainer.xaml | 18 +++
.../Controls/SpeciesGroupContainer.xaml.cs | 70 ++++++++
.../Windows/SpeciesSelectWindow.xaml | 40 +++++
.../Windows/SpeciesSelectWindow.xaml.cs | 152 ++++++++++++++++++
.../SpeciesDictionaryPrototype.cs | 23 +++
.../en-US/_white/species/speciesgroup.ftl | 8 +
.../ru-RU/_white/species/speciesgroup.ftl | 8 +
Resources/Prototypes/Guidebook/species.yml | 16 ++
.../_White/SpeciesDictionary/groups.yml | 7 +
.../_White/SpeciesDictionary/species.yml | 79 +++++++++
.../ServerInfo/Guidebook/Mobs/Felinid.xml | 3 +
.../ServerInfo/Guidebook/Mobs/Vulpkanin.xml | 3 +
15 files changed, 482 insertions(+), 1 deletion(-)
create mode 100644 Content.Client/Lobby/UI/ProfileEditor/HumanoidProfileEditor.SpeciesSelection.cs
create mode 100644 Content.Client/_White/UserInterface/Controls/SpeciesGroupContainer.xaml
create mode 100644 Content.Client/_White/UserInterface/Controls/SpeciesGroupContainer.xaml.cs
create mode 100644 Content.Client/_White/UserInterface/Windows/SpeciesSelectWindow.xaml
create mode 100644 Content.Client/_White/UserInterface/Windows/SpeciesSelectWindow.xaml.cs
create mode 100644 Content.Shared/_White/SpeciesDictionary/SpeciesDictionaryPrototype.cs
create mode 100644 Resources/Locale/en-US/_white/species/speciesgroup.ftl
create mode 100644 Resources/Locale/ru-RU/_white/species/speciesgroup.ftl
create mode 100644 Resources/Prototypes/_White/SpeciesDictionary/groups.yml
create mode 100644 Resources/Prototypes/_White/SpeciesDictionary/species.yml
create mode 100644 Resources/ServerInfo/Guidebook/Mobs/Felinid.xml
create mode 100644 Resources/ServerInfo/Guidebook/Mobs/Vulpkanin.xml
diff --git a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml
index 97cd590bcf..937981ebbd 100644
--- a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml
+++ b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml
@@ -41,7 +41,10 @@
-
+
+
+
+
diff --git a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs
index a88a7aff18..10c7135459 100644
--- a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs
+++ b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs
@@ -619,6 +619,7 @@ namespace Content.Client.Lobby.UI
ReloadPreview();
InitializeCharacterMenu(); // WWDP EDIT
+ InitializeSpeciesSelection(); // WWDP EDIT;
IsDirty = false;
}
diff --git a/Content.Client/Lobby/UI/ProfileEditor/HumanoidProfileEditor.SpeciesSelection.cs b/Content.Client/Lobby/UI/ProfileEditor/HumanoidProfileEditor.SpeciesSelection.cs
new file mode 100644
index 0000000000..a2116b1723
--- /dev/null
+++ b/Content.Client/Lobby/UI/ProfileEditor/HumanoidProfileEditor.SpeciesSelection.cs
@@ -0,0 +1,50 @@
+using Content.Client._White.UserInterface.Windows;
+using Content.Shared.Humanoid.Prototypes;
+using Robust.Client.UserInterface.Controls;
+using Robust.Shared.Prototypes;
+
+
+namespace Content.Client.Lobby.UI;
+
+// WWDP PARTIAL CLASS
+public sealed partial class HumanoidProfileEditor
+{
+ private SpeciesSelectWindow? _currentWindow;
+
+ public void InitializeSpeciesSelection()
+ {
+ OpenSpeciesWindow.OnPressed += OpenSpeciesWindowPressed;
+ }
+
+ private void OpenSpeciesWindowPressed(BaseButton.ButtonEventArgs obj)
+ {
+ if(Profile is null)
+ return;
+
+ OpenSpeciesWindow.Disabled = true;
+ _currentWindow = UserInterfaceManager.CreateWindow();
+ _currentWindow.Initialize(Profile);
+ _currentWindow.OnClose += CurrentWindowClosed;
+ _currentWindow.OnSpeciesSelected += OnSpeciesSelected;
+ _currentWindow.OpenCentered();
+ }
+
+ private void OnSpeciesSelected(ProtoId proto)
+ {
+ SetSpecies(proto);
+ UpdateHairPickers();
+ OnSkinColorOnValueChanged();
+ UpdateCustomSpecieNameEdit();
+ UpdateHeightWidthSliders();
+ _currentWindow?.Close();
+ }
+
+ private void CurrentWindowClosed()
+ {
+ if(_currentWindow == null)
+ return;
+ _currentWindow.OnClose -= CurrentWindowClosed;
+ _currentWindow.OnSpeciesSelected -= OnSpeciesSelected;
+ OpenSpeciesWindow.Disabled = false;
+ }
+}
diff --git a/Content.Client/_White/UserInterface/Controls/SpeciesGroupContainer.xaml b/Content.Client/_White/UserInterface/Controls/SpeciesGroupContainer.xaml
new file mode 100644
index 0000000000..01b0894c99
--- /dev/null
+++ b/Content.Client/_White/UserInterface/Controls/SpeciesGroupContainer.xaml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_White/UserInterface/Controls/SpeciesGroupContainer.xaml.cs b/Content.Client/_White/UserInterface/Controls/SpeciesGroupContainer.xaml.cs
new file mode 100644
index 0000000000..ffcb221646
--- /dev/null
+++ b/Content.Client/_White/UserInterface/Controls/SpeciesGroupContainer.xaml.cs
@@ -0,0 +1,70 @@
+using Content.Shared._White.SpeciesDictionary;
+using Content.Shared.Humanoid.Prototypes;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+
+
+namespace Content.Client._White.UserInterface.Controls;
+
+
+[GenerateTypedNameReferences]
+public sealed partial class SpeciesGroupContainer : Control
+{
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+
+ private readonly List<(Button, Action)> _buttonsEvents = new();
+
+ public Action>? OnSpeciesSelected { get; set; }
+
+ public SpeciesGroupContainer()
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+ }
+
+ public void Initialize(SpeciesDictionaryGroupPrototype group)
+ {
+ Clear();
+
+ GroupLabel.Text = Loc.GetString($"species-window-group-{group.ID.ToLower()}");
+ foreach (var species in _prototypeManager.EnumeratePrototypes())
+ {
+ if(species.GroupPrototype != group.ID)
+ continue;
+
+ if(!_prototypeManager.TryIndex(species.ID, out var speciesPrototype) || !speciesPrototype.RoundStart)
+ continue;
+
+ var button = new Button()
+ {
+ Text = Loc.GetString($"species-name-{species.ID.ToLower()}"),
+ HorizontalAlignment = HAlignment.Stretch
+ };
+
+ if (group.Color.HasValue)
+ {
+ button.Modulate = group.Color.Value;
+ }
+
+ Action onSelected = _ => OnSpeciesSelected?.Invoke(new(species.ID));
+ button.OnPressed += onSelected;
+ _buttonsEvents.Add((button, onSelected));
+ SpeciesButtonContainer.AddChild(button);
+ }
+ }
+
+ public void Clear()
+ {
+ foreach (var (button, eventInternal) in _buttonsEvents)
+ {
+ button.OnPressed -= eventInternal;
+ }
+ _buttonsEvents.Clear();
+
+ SpeciesButtonContainer.Children.Clear();
+ }
+}
+
diff --git a/Content.Client/_White/UserInterface/Windows/SpeciesSelectWindow.xaml b/Content.Client/_White/UserInterface/Windows/SpeciesSelectWindow.xaml
new file mode 100644
index 0000000000..f7f64b55bc
--- /dev/null
+++ b/Content.Client/_White/UserInterface/Windows/SpeciesSelectWindow.xaml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_White/UserInterface/Windows/SpeciesSelectWindow.xaml.cs b/Content.Client/_White/UserInterface/Windows/SpeciesSelectWindow.xaml.cs
new file mode 100644
index 0000000000..d24ac5a179
--- /dev/null
+++ b/Content.Client/_White/UserInterface/Windows/SpeciesSelectWindow.xaml.cs
@@ -0,0 +1,152 @@
+using System.Linq;
+using Content.Client._White.UserInterface.Controls;
+using Content.Client.Guidebook;
+using Content.Client.Humanoid;
+using Content.Shared._White.SpeciesDictionary;
+using Content.Shared.Humanoid;
+using Content.Shared.Humanoid.Prototypes;
+using Content.Shared.Preferences;
+using Robust.Client.AutoGenerated;
+using Robust.Client.Player;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Map;
+using Robust.Shared.Prototypes;
+
+
+namespace Content.Client._White.UserInterface.Windows;
+
+
+[GenerateTypedNameReferences]
+public sealed partial class SpeciesSelectWindow : DefaultWindow
+{
+ [Dependency] private readonly IEntityManager _entityManager = default!;
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly DocumentParsingManager _documentParsingManager = default!;
+ [Dependency] private readonly IPlayerManager _playerManager = default!;
+
+ public event Action>? OnSpeciesSelected;
+
+ private EntityUid _dummyUid;
+
+ private HumanoidCharacterProfile? _dummyProfile;
+ private Dictionary, SpeciesDictionaryPrototype> _dictionaryCache = [];
+
+ private ProtoId? _selectedSpecies;
+
+ public ProtoId? SelectedSpecies
+ {
+ get => _selectedSpecies;
+ set
+ {
+ if(!_prototypeManager.TryIndex(value, out var prototype) || _playerManager.LocalSession is null)
+ return;
+
+ InfoContainer.Children.Clear();
+ _selectedSpecies = value;
+
+ _entityManager.DeleteEntity(_dummyUid);
+ SpeciesVisualContainer.Visible = value != null;
+
+ if (_selectedSpecies == null)
+ return;
+
+ SpeciesNameLabel.Text = Loc.GetString($"species-name-{_selectedSpecies.Value.Id.ToLower()}");
+
+ if (_dummyProfile != null)
+ {
+ _dummyProfile = _dummyProfile.WithSpecies(_selectedSpecies.Value);
+
+ _dummyProfile.EnsureValid(_playerManager.LocalSession, IoCManager.Instance!);
+ }
+
+ _dummyUid = _entityManager.SpawnEntity(prototype.DollPrototype, MapCoordinates.Nullspace);
+
+ if (_entityManager.TryGetComponent(_dummyUid, out var humanoid))
+ {
+ var hiddenLayers = humanoid.HiddenLayers;
+ var appearanceSystem = _entityManager.System();
+ appearanceSystem.LoadProfile(_dummyUid, _dummyProfile, humanoid, false, false);
+ // Reapply the hidden layers set from clothing
+ appearanceSystem.SetLayersVisibility(_dummyUid, hiddenLayers, false, humanoid: humanoid);
+ }
+
+ EntityFrontView.SetEntity(_dummyUid);
+ EntityRightView.SetEntity(_dummyUid);
+
+ if(!_dictionaryCache.TryGetValue(_selectedSpecies.Value, out var dictionary))
+ return;
+
+ _documentParsingManager.TryAddMarkup(InfoContainer, dictionary.GuidePrototype);
+ }
+ }
+
+
+ private List _groupContainers = new();
+
+ public SpeciesSelectWindow()
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+
+ SelectButton.OnPressed += SelectButtonOnOnPressed;
+ }
+
+ public void Initialize(HumanoidCharacterProfile characterProfile)
+ {
+ DisposeContainers();
+ _dummyProfile = characterProfile.Clone();
+
+ var groups =
+ _prototypeManager.EnumeratePrototypes().ToList();
+ groups.Sort((a, b) => a.Weight.CompareTo(b.Weight));
+
+ foreach (var group in groups)
+ {
+ var groupContainer = new SpeciesGroupContainer();
+ groupContainer.Initialize(group);
+ groupContainer.OnSpeciesSelected += OnSpeciesSelectedForView;
+ _groupContainers.Add(groupContainer);
+ RaceGroupContainer.AddChild(groupContainer);
+ }
+
+ foreach (var dictionary in _prototypeManager.EnumeratePrototypes())
+ _dictionaryCache[dictionary.ID] = dictionary;
+
+ SelectedSpecies = characterProfile.Species;
+ }
+
+ private void OnSpeciesSelectedForView(ProtoId species)
+ {
+ SelectedSpecies = species;
+ }
+
+ private void SelectButtonOnOnPressed(BaseButton.ButtonEventArgs obj)
+ {
+ if(_selectedSpecies == null)
+ return;
+ OnSpeciesSelected?.Invoke(_selectedSpecies.Value);
+ }
+
+ private void DisposeContainers()
+ {
+ foreach (var container in _groupContainers)
+ {
+ container.OnSpeciesSelected -= OnSpeciesSelectedForView;
+ container.Clear();
+ }
+ RaceGroupContainer.Children.Clear();
+ _dictionaryCache.Clear();
+ }
+
+ public override void Close()
+ {
+ base.Close();
+ _entityManager.DeleteEntity(_dummyUid);
+ SelectButton.OnPressed -= SelectButtonOnOnPressed;
+ DisposeContainers();
+ }
+}
+
diff --git a/Content.Shared/_White/SpeciesDictionary/SpeciesDictionaryPrototype.cs b/Content.Shared/_White/SpeciesDictionary/SpeciesDictionaryPrototype.cs
new file mode 100644
index 0000000000..39851b1caf
--- /dev/null
+++ b/Content.Shared/_White/SpeciesDictionary/SpeciesDictionaryPrototype.cs
@@ -0,0 +1,23 @@
+using Content.Shared.Guidebook;
+using Robust.Shared.Prototypes;
+
+
+namespace Content.Shared._White.SpeciesDictionary;
+
+
+[Prototype]
+public sealed class SpeciesDictionaryPrototype : IPrototype
+{
+ [IdDataField] public string ID { get; private set; } = default!;
+
+ [DataField] public ProtoId GuidePrototype { get; private set; } = default!;
+ [DataField] public ProtoId GroupPrototype { get; private set; } = default!;
+}
+
+[Prototype]
+public sealed class SpeciesDictionaryGroupPrototype : IPrototype
+{
+ [IdDataField] public string ID { get; private set; } = default!;
+ [DataField] public Color? Color { get; private set; }
+ [DataField] public int Weight { get; private set; }
+}
diff --git a/Resources/Locale/en-US/_white/species/speciesgroup.ftl b/Resources/Locale/en-US/_white/species/speciesgroup.ftl
new file mode 100644
index 0000000000..ccca416acd
--- /dev/null
+++ b/Resources/Locale/en-US/_white/species/speciesgroup.ftl
@@ -0,0 +1,8 @@
+species-window-select = Select
+species-window-group-humanoid = Humanoids
+species-window-group-other = Other
+species-window-open = Open
+species-window-label = Species Selection Menu
+
+
+species-name-slimeperson = Slime person
diff --git a/Resources/Locale/ru-RU/_white/species/speciesgroup.ftl b/Resources/Locale/ru-RU/_white/species/speciesgroup.ftl
new file mode 100644
index 0000000000..76b7f21d1d
--- /dev/null
+++ b/Resources/Locale/ru-RU/_white/species/speciesgroup.ftl
@@ -0,0 +1,8 @@
+species-window-select = Выбрать
+species-window-group-humanoid = Гуманоиды
+species-window-group-other = Другие
+species-window-open = Открыть
+species-window-label = Меню выбора рас
+
+
+species-name-slimeperson = Слаймолюд
diff --git a/Resources/Prototypes/Guidebook/species.yml b/Resources/Prototypes/Guidebook/species.yml
index 9a2365e317..4d472744be 100644
--- a/Resources/Prototypes/Guidebook/species.yml
+++ b/Resources/Prototypes/Guidebook/species.yml
@@ -5,6 +5,7 @@
children:
- Arachnid
- Diona
+ - Felinid # WWDP EDIT
- Human
- Moth
- Oni
@@ -16,6 +17,7 @@
- Plasmaman
# - Chitinid
- Tajaran
+ - Vulpkanin # WWDP EDIT
- Xelthia
# ImpStation additions
- Thaven
@@ -84,3 +86,17 @@
id: Xelthia
name: species-name-xelthia
text: "/ServerInfo/_EE/Guidebook/Mobs/Xelthia.xml"
+
+# WWDP EDIT START
+
+- type: guideEntry
+ id: Felinid
+ name: species-name-felinid
+ text: "/ServerInfo/Guidebook/Mobs/Felinid.xml"
+
+- type: guideEntry
+ id: Vulpkanin
+ name: species-name-vulpkanin
+ text: "/ServerInfo/Guidebook/Mobs/Vulpkanin.xml"
+
+# WWDP EDIT END
diff --git a/Resources/Prototypes/_White/SpeciesDictionary/groups.yml b/Resources/Prototypes/_White/SpeciesDictionary/groups.yml
new file mode 100644
index 0000000000..94987d22b7
--- /dev/null
+++ b/Resources/Prototypes/_White/SpeciesDictionary/groups.yml
@@ -0,0 +1,7 @@
+- type: speciesDictionaryGroup
+ id: Other
+ weight: 3
+
+- type: speciesDictionaryGroup
+ id: Humanoid
+ weight: 0
diff --git a/Resources/Prototypes/_White/SpeciesDictionary/species.yml b/Resources/Prototypes/_White/SpeciesDictionary/species.yml
new file mode 100644
index 0000000000..efad1bf66f
--- /dev/null
+++ b/Resources/Prototypes/_White/SpeciesDictionary/species.yml
@@ -0,0 +1,79 @@
+- type: speciesDictionary
+ id: Human
+ guidePrototype: Human
+ groupPrototype: Humanoid
+
+- type: speciesDictionary
+ id: Reptilian
+ guidePrototype: Reptilian
+ groupPrototype: Other
+
+- type: speciesDictionary
+ id: SlimePerson
+ guidePrototype: SlimePerson
+ groupPrototype: Humanoid
+
+- type: speciesDictionary
+ id: Oni
+ guidePrototype: Oni
+ groupPrototype: Other
+
+- type: speciesDictionary
+ id: Felinid
+ guidePrototype: Felinid
+ groupPrototype: Humanoid
+
+- type: speciesDictionary
+ id: Vulpkanin
+ guidePrototype: Vulpkanin
+ groupPrototype: Other
+
+- type: speciesDictionary
+ id: Diona
+ guidePrototype: Diona
+ groupPrototype: Other
+
+- type: speciesDictionary
+ id: Shadowkin
+ guidePrototype: Shadowkin
+ groupPrototype: Other
+
+- type: speciesDictionary
+ id: Tajaran
+ guidePrototype: Tajaran
+ groupPrototype: Other
+
+- type: speciesDictionary
+ id: Xelthia
+ guidePrototype: Xelthia
+ groupPrototype: Other
+
+- type: speciesDictionary
+ id: Arachnid
+ guidePrototype: Arachnid
+ groupPrototype: Other
+
+- type: speciesDictionary
+ id: Moth
+ guidePrototype: Moth
+ groupPrototype: Other
+
+- type: speciesDictionary
+ id: IPC
+ guidePrototype: IPC
+ groupPrototype: Other
+
+- type: speciesDictionary
+ id: Harpy
+ guidePrototype: Harpy
+ groupPrototype: Humanoid
+
+- type: speciesDictionary
+ id: Plasmaman
+ guidePrototype: Plasmaman
+ groupPrototype: Other
+
+- type: speciesDictionary
+ id: Thaven
+ guidePrototype: Thaven
+ groupPrototype: Other
diff --git a/Resources/ServerInfo/Guidebook/Mobs/Felinid.xml b/Resources/ServerInfo/Guidebook/Mobs/Felinid.xml
new file mode 100644
index 0000000000..0d46710eab
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/Mobs/Felinid.xml
@@ -0,0 +1,3 @@
+
+ Мяу
+
diff --git a/Resources/ServerInfo/Guidebook/Mobs/Vulpkanin.xml b/Resources/ServerInfo/Guidebook/Mobs/Vulpkanin.xml
new file mode 100644
index 0000000000..f60175aeab
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/Mobs/Vulpkanin.xml
@@ -0,0 +1,3 @@
+
+ Гав
+