mirror of
https://github.com/WWhiteDreamProject/wwdpublic.git
synced 2026-04-18 05:59:03 +03:00
# Description
~~On user end Felinids are removed and replaced with Tajara, mostly
inspired by the ParadiseStation sprites.~~
~~Most things from Felinids are ported over, either being untouched (in
which case they're left in the nyanotrasen directory), or somewhat
modified and copied to tajara directories. This is done because both
shadowkin and humans reference these files quite a bit, so we don't want
to break anything forwards/backwards,~~
~~Under the hood, felinid components are untouched, just simply set to
`abstract: true` and `roundstart: false`, once again to prevent any
breakage.~~
There's been like 50 changes all the ways, but basically, felinids are
staying, tajara get a few languages, they also are more "specialized"
than felinids due to big upsides and downsides (funny :)
---
# TODO
- [X] Add tajara
~~- [X] Hide felinids~~
~~- [x] Analyze and find now-orphaned felinid
protorypes/textures/references and remove them (optional)~~
- [x] Fix graphical bugs (if any)
- [x] Ensure the code structure is compliant with repo practices
---
<details><summary><h1>Media</h1></summary>
<p>
In captain attire

Testing emotes and languages

Emotes still working, same with hairballs

Few character selector pictures
- The previously shown off tajaran

- Markingless male body


- Markingless female body


- Traits are updated, as is the case with other texts

~~- And a neon colored specimen to show off some markings~~ No longer
the case, fur hue is clamped to some reasonable colors

</p>
</details>
---
# Changelog
🆑
- add: Added Tajara and related content
---------
Signed-off-by: SX-7 <92227810+SX-7@users.noreply.github.com>
Signed-off-by: VMSolidus <evilexecutive@gmail.com>
Co-authored-by: sleepyyapril <123355664+sleepyyapril@users.noreply.github.com>
Co-authored-by: VMSolidus <evilexecutive@gmail.com>
(cherry picked from commit b7660b95566c8e138cb458a3d70732c1d3ea4602)
2577 lines
98 KiB
C#
2577 lines
98 KiB
C#
using System.IO;
|
|
using System.Linq;
|
|
using System.Numerics;
|
|
using Content.Client.Administration.UI;
|
|
using Content.Client.Guidebook;
|
|
using Content.Client.Humanoid;
|
|
using Content.Client.Message;
|
|
using Content.Client.Players.PlayTimeTracking;
|
|
using Content.Client.UserInterface.Controls;
|
|
using Content.Client.UserInterface.Systems.Guidebook;
|
|
using Content.Shared.CCVar;
|
|
using Content.Shared.Clothing.Components;
|
|
using Content.Shared.Clothing.Loadouts.Prototypes;
|
|
using Content.Shared.Clothing.Loadouts.Systems;
|
|
using Content.Shared.Customization.Systems;
|
|
using Content.Shared.Dataset;
|
|
using Content.Shared.GameTicking;
|
|
using Content.Shared.Humanoid;
|
|
using Content.Shared.Humanoid.Markings;
|
|
using Content.Shared.Humanoid.Prototypes;
|
|
using Content.Shared.Preferences;
|
|
using Content.Shared.Roles;
|
|
using Content.Shared.StatusIcon;
|
|
using Content.Shared.Traits;
|
|
using Robust.Client.AutoGenerated;
|
|
using Robust.Client.Graphics;
|
|
using Robust.Client.UserInterface;
|
|
using Robust.Client.UserInterface.Controls;
|
|
using Robust.Client.UserInterface.XAML;
|
|
using Robust.Client.Utility;
|
|
using Robust.Client.Player;
|
|
using Robust.Shared.Configuration;
|
|
using Robust.Shared.Enums;
|
|
using Robust.Shared.Map;
|
|
using Robust.Shared.Physics;
|
|
using Robust.Shared.Prototypes;
|
|
using Robust.Shared.Random;
|
|
using Robust.Shared.Utility;
|
|
using Direction = Robust.Shared.Maths.Direction;
|
|
|
|
namespace Content.Client.Lobby.UI
|
|
{
|
|
[GenerateTypedNameReferences]
|
|
public sealed partial class HumanoidProfileEditor : BoxContainer
|
|
{
|
|
private readonly IConfigurationManager _cfgManager;
|
|
private readonly IEntityManager _entManager;
|
|
private readonly IFileDialogManager _dialogManager;
|
|
private readonly IPlayerManager _playerManager;
|
|
private readonly IPrototypeManager _prototypeManager;
|
|
private readonly IClientPreferencesManager _preferencesManager;
|
|
private readonly MarkingManager _markingManager;
|
|
private readonly JobRequirementsManager _requirements;
|
|
private readonly CharacterRequirementsSystem _characterRequirementsSystem;
|
|
private readonly LobbyUIController _controller;
|
|
private readonly IRobustRandom _random;
|
|
|
|
private FlavorText.FlavorText? _flavorText;
|
|
private BoxContainer _ccustomspecienamecontainerEdit => CCustomSpecieName;
|
|
private LineEdit _customspecienameEdit => CCustomSpecieNameEdit;
|
|
private TextEdit? _flavorTextEdit;
|
|
|
|
/// If we're attempting to save
|
|
public event Action? Save;
|
|
private bool _exporting;
|
|
private bool _isDirty;
|
|
|
|
/// The character slot for the current profile
|
|
public int? CharacterSlot;
|
|
/// The work in progress profile being edited
|
|
public HumanoidCharacterProfile? Profile;
|
|
/// Entity Used for the profile editor preview
|
|
public EntityUid PreviewDummy;
|
|
/// Temporary override of their selected job, used to preview roles
|
|
public JobPrototype? JobOverride;
|
|
|
|
private List<SpeciesPrototype> _species = new();
|
|
private List<(string, RequirementsSelector)> _jobPriorities = new();
|
|
private readonly Dictionary<string, BoxContainer> _jobCategories;
|
|
|
|
private Dictionary<Button, ConfirmationData> _confirmationData = new();
|
|
private List<TraitPreferenceSelector> _traitPreferences = new();
|
|
private int _traitCount;
|
|
private HashSet<LoadoutPreferenceSelector> _loadoutPreferences = new();
|
|
|
|
private Direction _previewRotation = Direction.North;
|
|
private ColorSelectorSliders _rgbSkinColorSelector;
|
|
|
|
private bool _customizePronouns;
|
|
private bool _customizeStationAiName;
|
|
private bool _customizeBorgName;
|
|
|
|
public event Action<HumanoidCharacterProfile, int>? OnProfileChanged;
|
|
|
|
[ValidatePrototypeId<GuideEntryPrototype>]
|
|
private const string DefaultSpeciesGuidebook = "Species";
|
|
|
|
[ValidatePrototypeId<LocalizedDatasetPrototype>]
|
|
private const string StationAiNames = "NamesAI";
|
|
|
|
[ValidatePrototypeId<DatasetPrototype>]
|
|
private const string CyborgNames = "names_borg";
|
|
|
|
public HumanoidProfileEditor(
|
|
IClientPreferencesManager preferencesManager,
|
|
IConfigurationManager cfgManager,
|
|
IEntityManager entManager,
|
|
IFileDialogManager dialogManager,
|
|
IPlayerManager playerManager,
|
|
IPrototypeManager prototypeManager,
|
|
JobRequirementsManager requirements,
|
|
MarkingManager markings,
|
|
IRobustRandom random
|
|
)
|
|
{
|
|
RobustXamlLoader.Load(this);
|
|
_cfgManager = cfgManager;
|
|
_entManager = entManager;
|
|
_dialogManager = dialogManager;
|
|
_playerManager = playerManager;
|
|
_prototypeManager = prototypeManager;
|
|
_markingManager = markings;
|
|
_preferencesManager = preferencesManager;
|
|
_requirements = requirements;
|
|
_random = random;
|
|
|
|
_characterRequirementsSystem = _entManager.System<CharacterRequirementsSystem>();
|
|
_controller = UserInterfaceManager.GetUIController<LobbyUIController>();
|
|
|
|
ImportButton.OnPressed += args => { ImportProfile(); };
|
|
ExportButton.OnPressed += args => { ExportProfile(); };
|
|
SaveButton.OnPressed += args => { Save?.Invoke(); };
|
|
ResetButton.OnPressed += args =>
|
|
{
|
|
SetProfile(
|
|
(HumanoidCharacterProfile?) _preferencesManager.Preferences?.SelectedCharacter,
|
|
_preferencesManager.Preferences?.SelectedCharacterIndex);
|
|
};
|
|
|
|
#region Left
|
|
|
|
#region Name
|
|
|
|
NameEdit.OnTextChanged += args => { SetName(args.Text); };
|
|
NameRandomize.OnPressed += _ => RandomizeName();
|
|
RandomizeEverything.OnPressed += _ => { RandomizeProfile(); };
|
|
WarningLabel.SetMarkup($"[color=red]{Loc.GetString("humanoid-profile-editor-naming-rules-warning")}[/color]");
|
|
|
|
#endregion Name
|
|
|
|
#region Custom Species Name
|
|
|
|
_customspecienameEdit.OnTextChanged += args => { SetCustomSpecieName(args.Text); };
|
|
|
|
#endregion Custom Species Name
|
|
|
|
#region Appearance
|
|
|
|
Appearance.Orphan();
|
|
CTabContainer.AddTab(Appearance, Loc.GetString("humanoid-profile-editor-appearance-tab"));
|
|
|
|
#region Sex
|
|
|
|
SexButton.OnItemSelected += args =>
|
|
{
|
|
SexButton.SelectId(args.Id);
|
|
SetSex((Sex) args.Id);
|
|
};
|
|
|
|
#endregion Sex
|
|
|
|
// WD EDIT START
|
|
#region Voice
|
|
|
|
InitializeVoice();
|
|
|
|
#endregion
|
|
// WD EDIT END
|
|
|
|
#region Age
|
|
|
|
AgeEdit.OnTextChanged += args =>
|
|
{
|
|
if (!int.TryParse(args.Text, out var newAge))
|
|
return;
|
|
|
|
SetAge(newAge);
|
|
};
|
|
|
|
#endregion Age
|
|
|
|
#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.OnItemSelected += args =>
|
|
{
|
|
PronounsButton.SelectId(args.Id);
|
|
SetGender((Gender) args.Id);
|
|
|
|
if (Profile?.DisplayPronouns == null)
|
|
UpdateDisplayPronounsControls();
|
|
};
|
|
|
|
#endregion Gender
|
|
|
|
#region Cosmetic Pronouns
|
|
|
|
_customizePronouns = _cfgManager.GetCVar(CCVars.AllowCosmeticPronouns);
|
|
_cfgManager.OnValueChanged(CCVars.AllowCosmeticPronouns, OnCosmeticPronounsValueChanged);
|
|
|
|
CosmeticPronounsNameEdit.OnTextChanged += args => { SetDisplayPronouns(args.Text); };
|
|
|
|
if (CosmeticPronousContainer.Visible != _customizePronouns)
|
|
CosmeticPronousContainer.Visible = _customizePronouns;
|
|
|
|
#endregion Cosmetic Pronouns
|
|
|
|
#region Custom Names
|
|
|
|
_customizeStationAiName = _cfgManager.GetCVar(CCVars.AllowCustomStationAiName);
|
|
_customizeBorgName = _cfgManager.GetCVar(CCVars.AllowCustomCyborgName);
|
|
|
|
_cfgManager.OnValueChanged(CCVars.AllowCustomStationAiName, OnChangedStationAiNameCustomizationValue);
|
|
_cfgManager.OnValueChanged(CCVars.AllowCustomCyborgName, OnChangedCyborgNameCustomizationValue);
|
|
|
|
StationAINameEdit.OnTextChanged += args => { SetStationAiName(args.Text); };
|
|
CyborgNameEdit.OnTextChanged += args => { SetCyborgName(args.Text); };
|
|
|
|
if (StationAiNameContainer.Visible != _customizeStationAiName)
|
|
StationAiNameContainer.Visible = _customizeStationAiName;
|
|
|
|
if (CyborgNameContainer.Visible != _customizeBorgName)
|
|
CyborgNameContainer.Visible = _customizeBorgName;
|
|
|
|
#endregion
|
|
|
|
#region Species
|
|
|
|
RefreshSpecies();
|
|
|
|
SpeciesButton.OnItemSelected += args =>
|
|
{
|
|
SpeciesButton.SelectId(args.Id);
|
|
SetSpecies(_species[args.Id].ID);
|
|
UpdateHairPickers();
|
|
OnSkinColorOnValueChanged();
|
|
UpdateCustomSpecieNameEdit();
|
|
UpdateHeightWidthSliders();
|
|
};
|
|
|
|
#endregion Species
|
|
|
|
#region Height and Width
|
|
|
|
var prototype = _species.Find(x => x.ID == Profile?.Species) ?? _species.First();
|
|
|
|
UpdateHeightWidthSliders();
|
|
|
|
HeightSlider.OnValueChanged += _ => UpdateDimensions(SliderUpdate.Height);
|
|
WidthSlider.OnValueChanged += _ => UpdateDimensions(SliderUpdate.Width);
|
|
|
|
HeightReset.OnPressed += _ =>
|
|
{
|
|
HeightSlider.Value = prototype.DefaultHeight;
|
|
UpdateDimensions(SliderUpdate.Height);
|
|
};
|
|
|
|
WidthReset.OnPressed += _ =>
|
|
{
|
|
WidthSlider.Value = prototype.DefaultWidth;
|
|
UpdateDimensions(SliderUpdate.Width);
|
|
};
|
|
|
|
#endregion Height
|
|
|
|
#region Skin
|
|
|
|
|
|
Skin.OnValueChanged += _ => { OnSkinColorOnValueChanged(); };
|
|
RgbSkinColorContainer.AddChild(_rgbSkinColorSelector = new());
|
|
_rgbSkinColorSelector.OnColorChanged += _ => { OnSkinColorOnValueChanged(); };
|
|
|
|
#endregion
|
|
|
|
#region Hair
|
|
|
|
HairStylePicker.OnMarkingSelect += newStyle =>
|
|
{
|
|
if (Profile is null)
|
|
return;
|
|
Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithHairStyleName(newStyle.id));
|
|
IsDirty = true;
|
|
ReloadProfilePreview();
|
|
};
|
|
|
|
HairStylePicker.OnColorChanged += newColor =>
|
|
{
|
|
if (Profile is null)
|
|
return;
|
|
Profile = Profile.WithCharacterAppearance(
|
|
Profile.Appearance.WithHairColor(newColor.marking.MarkingColors[0]));
|
|
UpdateCMarkingsHair();
|
|
IsDirty = true;
|
|
ReloadProfilePreview();
|
|
};
|
|
|
|
FacialHairPicker.OnMarkingSelect += newStyle =>
|
|
{
|
|
if (Profile is null)
|
|
return;
|
|
Profile = Profile.WithCharacterAppearance(
|
|
Profile.Appearance.WithFacialHairStyleName(newStyle.id));
|
|
IsDirty = true;
|
|
ReloadProfilePreview();
|
|
};
|
|
|
|
FacialHairPicker.OnColorChanged += newColor =>
|
|
{
|
|
if (Profile is null)
|
|
return;
|
|
Profile = Profile.WithCharacterAppearance(
|
|
Profile.Appearance.WithFacialHairColor(newColor.marking.MarkingColors[0]));
|
|
UpdateCMarkingsFacialHair();
|
|
IsDirty = true;
|
|
ReloadProfilePreview();
|
|
};
|
|
|
|
HairStylePicker.OnSlotRemove += _ =>
|
|
{
|
|
if (Profile is null)
|
|
return;
|
|
Profile = Profile.WithCharacterAppearance(
|
|
Profile.Appearance.WithHairStyleName(HairStyles.DefaultHairStyle)
|
|
);
|
|
UpdateHairPickers();
|
|
UpdateCMarkingsHair();
|
|
IsDirty = true;
|
|
ReloadProfilePreview();
|
|
};
|
|
|
|
FacialHairPicker.OnSlotRemove += _ =>
|
|
{
|
|
if (Profile is null)
|
|
return;
|
|
Profile = Profile.WithCharacterAppearance(
|
|
Profile.Appearance.WithFacialHairStyleName(HairStyles.DefaultFacialHairStyle)
|
|
);
|
|
UpdateHairPickers();
|
|
UpdateCMarkingsFacialHair();
|
|
IsDirty = true;
|
|
ReloadProfilePreview();
|
|
};
|
|
|
|
HairStylePicker.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;
|
|
ReloadProfilePreview();
|
|
};
|
|
|
|
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;
|
|
ReloadProfilePreview();
|
|
};
|
|
|
|
#endregion Hair
|
|
|
|
#region SpawnPriority
|
|
|
|
foreach (var value in Enum.GetValues<SpawnPriorityPreference>())
|
|
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);
|
|
};
|
|
|
|
#endregion SpawnPriority
|
|
|
|
#region Eyes
|
|
|
|
EyeColorPicker.OnEyeColorPicked += newColor =>
|
|
{
|
|
if (Profile is null)
|
|
return;
|
|
Profile = Profile.WithCharacterAppearance(
|
|
Profile.Appearance.WithEyeColor(newColor));
|
|
Markings.CurrentEyeColor = Profile.Appearance.EyeColor;
|
|
IsDirty = true;
|
|
ReloadProfilePreview();
|
|
};
|
|
|
|
#endregion Eyes
|
|
|
|
#endregion Appearance
|
|
|
|
#region Jobs
|
|
|
|
Jobs.Orphan();
|
|
CTabContainer.AddTab(Jobs, 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;
|
|
};
|
|
|
|
_jobCategories = new Dictionary<string, BoxContainer>();
|
|
|
|
#endregion Jobs
|
|
|
|
#region Antags
|
|
|
|
Antags.Orphan();
|
|
CTabContainer.AddTab(Antags, Loc.GetString("humanoid-profile-editor-antags-tab"));
|
|
|
|
#endregion Antags
|
|
|
|
#region Traits
|
|
|
|
// Set up the traits tab
|
|
TraitsTab.Orphan();
|
|
CTabContainer.AddTab(TraitsTab, Loc.GetString("humanoid-profile-editor-traits-tab"));
|
|
_traitPreferences = new List<TraitPreferenceSelector>();
|
|
|
|
// Show/Hide the traits tab if they ever get enabled/disabled
|
|
var traitsEnabled = cfgManager.GetCVar(CCVars.GameTraitsEnabled);
|
|
CTabContainer.SetTabVisible(3, traitsEnabled);
|
|
cfgManager.OnValueChanged(CCVars.GameTraitsEnabled,
|
|
enabled => CTabContainer.SetTabVisible(3, enabled));
|
|
|
|
TraitsShowUnusableButton.OnToggled += args => UpdateTraits(args.Pressed);
|
|
TraitsRemoveUnusableButton.OnPressed += _ => TryRemoveUnusableTraits();
|
|
|
|
UpdateTraits(false);
|
|
|
|
#endregion
|
|
|
|
#region Loadouts
|
|
|
|
// Set up the loadouts tab
|
|
LoadoutsTab.Orphan();
|
|
CTabContainer.AddTab(LoadoutsTab, Loc.GetString("humanoid-profile-editor-loadouts-tab"));
|
|
_loadoutPreferences = new();
|
|
|
|
// Show/Hide the loadouts tab if they ever get enabled/disabled
|
|
var loadoutsEnabled = cfgManager.GetCVar(CCVars.GameLoadoutsEnabled);
|
|
CTabContainer.SetTabVisible(4, loadoutsEnabled);
|
|
ShowLoadouts.Visible = loadoutsEnabled;
|
|
cfgManager.OnValueChanged(CCVars.GameLoadoutsEnabled, LoadoutsChanged);
|
|
|
|
LoadoutsShowUnusableButton.OnToggled += args => UpdateLoadouts(args.Pressed);
|
|
LoadoutsRemoveUnusableButton.OnPressed += _ => TryRemoveUnusableLoadouts();
|
|
|
|
UpdateLoadouts(false);
|
|
|
|
#endregion
|
|
|
|
#region Markings
|
|
|
|
MarkingsTab.Orphan();
|
|
CTabContainer.AddTab(MarkingsTab, Loc.GetString("humanoid-profile-editor-markings-tab"));
|
|
|
|
Markings.OnMarkingAdded += OnMarkingChange;
|
|
Markings.OnMarkingRemoved += OnMarkingChange;
|
|
Markings.OnMarkingColorChange += OnMarkingChange;
|
|
Markings.OnMarkingRankChange += OnMarkingChange;
|
|
|
|
#endregion Markings
|
|
|
|
RefreshFlavorText();
|
|
|
|
#region Dummy
|
|
|
|
SpriteRotateLeft.OnPressed += _ =>
|
|
{
|
|
_previewRotation = _previewRotation.TurnCw();
|
|
SetPreviewRotation(_previewRotation);
|
|
};
|
|
SpriteRotateRight.OnPressed += _ =>
|
|
{
|
|
_previewRotation = _previewRotation.TurnCcw();
|
|
SetPreviewRotation(_previewRotation);
|
|
};
|
|
|
|
#endregion Dummy
|
|
|
|
#endregion Left
|
|
|
|
ShowClothes.OnToggled += _ => { SetProfile(Profile, CharacterSlot); };
|
|
ShowLoadouts.OnToggled += _ => { SetProfile(Profile, CharacterSlot); };
|
|
|
|
SpeciesInfoButton.OnPressed += OnSpeciesInfoButtonPressed;
|
|
UpdateSpeciesGuidebookIcon();
|
|
|
|
ReloadPreview();
|
|
IsDirty = false;
|
|
}
|
|
|
|
/// Refreshes the flavor text editor status
|
|
public void RefreshFlavorText()
|
|
{
|
|
if (_cfgManager.GetCVar(CCVars.FlavorText))
|
|
{
|
|
if (_flavorText != null)
|
|
return;
|
|
|
|
_flavorText = new();
|
|
_flavorText.OnFlavorTextChanged += OnFlavorTextChange;
|
|
_flavorTextEdit = _flavorText.CFlavorTextInput;
|
|
CTabContainer.AddTab(_flavorText, Loc.GetString("humanoid-profile-editor-flavortext-tab"));
|
|
}
|
|
else
|
|
{
|
|
if (_flavorText == null)
|
|
return;
|
|
|
|
CTabContainer.RemoveChild(_flavorText);
|
|
_flavorText.OnFlavorTextChanged -= OnFlavorTextChange;
|
|
_flavorText.Dispose();
|
|
_flavorText = null;
|
|
_flavorTextEdit?.Dispose();
|
|
_flavorTextEdit = null;
|
|
}
|
|
}
|
|
|
|
private void OnCosmeticPronounsValueChanged(bool newValue)
|
|
{
|
|
_customizePronouns = newValue;
|
|
CosmeticPronousContainer.Visible = newValue;
|
|
}
|
|
|
|
private void OnChangedStationAiNameCustomizationValue(bool newValue)
|
|
{
|
|
_customizeStationAiName = newValue;
|
|
StationAiNameContainer.Visible = newValue;
|
|
}
|
|
|
|
private void OnChangedCyborgNameCustomizationValue(bool newValue)
|
|
{
|
|
_customizeBorgName = newValue;
|
|
CyborgNameContainer.Visible = newValue;
|
|
}
|
|
|
|
/// Refreshes the species selector
|
|
public void RefreshSpecies()
|
|
{
|
|
SpeciesButton.Clear();
|
|
_species.Clear();
|
|
|
|
_species.AddRange(_prototypeManager.EnumeratePrototypes<SpeciesPrototype>().Where(o => o.RoundStart));
|
|
var speciesIds = _species.Select(o => o.ID).ToList();
|
|
|
|
for (var i = 0; i < _species.Count; i++)
|
|
{
|
|
SpeciesButton.AddItem(Loc.GetString(_species[i].Name), i);
|
|
|
|
if (Profile?.Species.Equals(_species[i].ID) == true)
|
|
SpeciesButton.SelectId(i);
|
|
}
|
|
|
|
// If our species isn't available, reset it to default
|
|
if (Profile != null && !speciesIds.Contains(Profile.Species))
|
|
SetSpecies(SharedHumanoidAppearanceSystem.DefaultSpecies);
|
|
}
|
|
|
|
public void RefreshAntags()
|
|
{
|
|
AntagList.DisposeAllChildren();
|
|
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<AntagPrototype>().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),
|
|
};
|
|
|
|
var title = Loc.GetString(antag.Name);
|
|
var description = Loc.GetString(antag.Objective);
|
|
selector.Setup(items, title, 250, description);
|
|
selector.Select(Profile?.AntagPreferences.Contains(antag.ID) == true ? 0 : 1);
|
|
|
|
if (!_characterRequirementsSystem.CheckRequirementsValid(
|
|
antag.Requirements ?? new(),
|
|
_controller.GetPreferredJob(Profile ?? HumanoidCharacterProfile.DefaultWithSpecies()),
|
|
Profile ?? HumanoidCharacterProfile.DefaultWithSpecies(),
|
|
_requirements.GetRawPlayTimeTrackers(),
|
|
_requirements.IsWhitelisted(),
|
|
antag,
|
|
_entManager,
|
|
_prototypeManager,
|
|
_cfgManager,
|
|
out var reasons))
|
|
{
|
|
var reason = _characterRequirementsSystem.GetRequirementsText(reasons);
|
|
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);
|
|
AntagList.AddChild(antagContainer);
|
|
}
|
|
}
|
|
|
|
private void SetDirty()
|
|
{
|
|
// If it equals default then reset the button.
|
|
if (Profile == null || _preferencesManager.Preferences?.SelectedCharacter.MemberwiseEquals(Profile) == true)
|
|
{
|
|
IsDirty = false;
|
|
return;
|
|
}
|
|
|
|
//TODO: Check if profile matches default
|
|
IsDirty = true;
|
|
}
|
|
|
|
/// Reloads the entire dummy entity for preview
|
|
/// <remarks>This is expensive so not recommended to run if you have a slider</remarks>
|
|
private void ReloadPreview()
|
|
{
|
|
_entManager.DeleteEntity(PreviewDummy);
|
|
PreviewDummy = EntityUid.Invalid;
|
|
|
|
if (Profile == null || !_prototypeManager.HasIndex<SpeciesPrototype>(Profile.Species))
|
|
return;
|
|
|
|
PreviewDummy = _controller.LoadProfileEntity(Profile, ShowClothes.Pressed, ShowLoadouts.Pressed);
|
|
SpriteView.SetEntity(PreviewDummy);
|
|
}
|
|
|
|
/// Reloads the dummy entity's clothes for preview
|
|
private void ReloadClothes()
|
|
{
|
|
if (Profile == null)
|
|
return;
|
|
|
|
_controller.RemoveDummyClothes(PreviewDummy);
|
|
var job = _controller.GetPreferredJob(Profile);
|
|
if (ShowClothes.Pressed)
|
|
_controller.GiveDummyJobClothes(PreviewDummy, job, Profile);
|
|
if (ShowLoadouts.Pressed)
|
|
_controller.GiveDummyLoadout(PreviewDummy, job, Profile);
|
|
}
|
|
|
|
/// Resets the profile to the defaults
|
|
public void ResetToDefault()
|
|
{
|
|
SetProfile(
|
|
(HumanoidCharacterProfile?) _preferencesManager.Preferences?.SelectedCharacter,
|
|
_preferencesManager.Preferences?.SelectedCharacterIndex);
|
|
}
|
|
|
|
/// Sets the editor to the specified profile with the specified slot
|
|
public void SetProfile(HumanoidCharacterProfile? profile, int? slot)
|
|
{
|
|
Profile = profile?.Clone();
|
|
CharacterSlot = slot;
|
|
IsDirty = false;
|
|
JobOverride = null;
|
|
|
|
UpdateNameEdit();
|
|
UpdateSexControls();
|
|
UpdateTTSVoicesControls(); // WD EDIT
|
|
UpdateGenderControls();
|
|
UpdateDisplayPronounsControls();
|
|
UpdateStationAiControls();
|
|
UpdateCyborgControls();
|
|
UpdateSkinColor();
|
|
UpdateSpawnPriorityControls();
|
|
UpdateFlavorTextEdit();
|
|
UpdateCustomSpecieNameEdit();
|
|
UpdateAgeEdit();
|
|
UpdateEyePickers();
|
|
UpdateSaveButton();
|
|
UpdateMarkings();
|
|
UpdateHairPickers();
|
|
UpdateCMarkingsHair();
|
|
UpdateCMarkingsFacialHair();
|
|
UpdateHeightWidthSliders();
|
|
UpdateWeight();
|
|
UpdateCharacterRequired();
|
|
|
|
RefreshAntags();
|
|
RefreshJobs();
|
|
RefreshSpecies();
|
|
RefreshFlavorText();
|
|
ReloadPreview();
|
|
|
|
if (Profile != null)
|
|
PreferenceUnavailableButton.SelectId((int) Profile.PreferenceUnavailable);
|
|
}
|
|
|
|
/// A slim reload that only updates the entity itself and not any of the job entities, etc
|
|
private void ReloadProfilePreview()
|
|
{
|
|
if (Profile == null || !_entManager.EntityExists(PreviewDummy))
|
|
return;
|
|
|
|
if (_entManager.TryGetComponent<HumanoidAppearanceComponent>(PreviewDummy, out var humanoid))
|
|
{
|
|
var hiddenLayers = humanoid.HiddenLayers;
|
|
var appearanceSystem = _entManager.System<HumanoidAppearanceSystem>();
|
|
appearanceSystem.LoadProfile(PreviewDummy, Profile, humanoid);
|
|
// Reapply the hidden layers set from clothing
|
|
appearanceSystem.SetLayersVisibility(PreviewDummy, hiddenLayers, false, humanoid: humanoid);
|
|
}
|
|
|
|
SetPreviewRotation(_previewRotation);
|
|
TraitsTabs.UpdateTabMerging();
|
|
LoadoutsTabs.UpdateTabMerging();
|
|
}
|
|
|
|
private void LoadoutsChanged(bool enabled)
|
|
{
|
|
CTabContainer.SetTabVisible(4, enabled);
|
|
ShowLoadouts.Visible = enabled;
|
|
}
|
|
|
|
private void OnSpeciesInfoButtonPressed(BaseButton.ButtonEventArgs args)
|
|
{
|
|
var guidebookController = UserInterfaceManager.GetUIController<GuidebookUIController>();
|
|
var species = Profile?.Species ?? SharedHumanoidAppearanceSystem.DefaultSpecies;
|
|
var page = DefaultSpeciesGuidebook;
|
|
if (_prototypeManager.HasIndex<GuideEntryPrototype>(species))
|
|
page = species;
|
|
|
|
if (_prototypeManager.TryIndex<GuideEntryPrototype>(DefaultSpeciesGuidebook, out var guideRoot))
|
|
{
|
|
var dict = new Dictionary<string, GuideEntry> { { DefaultSpeciesGuidebook, guideRoot } };
|
|
//TODO: Don't close the guidebook if its already open, just go to the correct page
|
|
guidebookController.ToggleGuidebook(dict, includeChildren:true, selected: page);
|
|
}
|
|
}
|
|
|
|
/// Refreshes all job selectors
|
|
public void RefreshJobs()
|
|
{
|
|
JobList.DisposeAllChildren();
|
|
_jobCategories.Clear();
|
|
_jobPriorities.Clear();
|
|
var firstCategory = true;
|
|
|
|
// Get all displayed departments
|
|
var departments = new List<DepartmentPrototype>();
|
|
foreach (var department in _prototypeManager.EnumeratePrototypes<DepartmentPrototype>())
|
|
{
|
|
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-{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(jobId => _prototypeManager.Index<JobPrototype>(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(3f, 3f, 3f, 0f) };
|
|
|
|
var icon = new TextureRect
|
|
{
|
|
TextureScale = new(2, 2),
|
|
VerticalAlignment = VAlignment.Center
|
|
};
|
|
var jobIcon = _prototypeManager.Index<JobIconPrototype>(job.Icon);
|
|
icon.Texture = jobIcon.Icon.Frame0();
|
|
selector.Setup(items, job.LocalizedName, 200, job.LocalizedDescription, icon);
|
|
|
|
if (!_requirements.CheckJobWhitelist(job, out var reason))
|
|
selector.LockRequirements(reason);
|
|
else if (!_characterRequirementsSystem.CheckRequirementsValid(
|
|
job.Requirements ?? new(),
|
|
job,
|
|
Profile ?? HumanoidCharacterProfile.DefaultWithSpecies(),
|
|
_requirements.GetRawPlayTimeTrackers(),
|
|
_requirements.IsWhitelisted(),
|
|
job,
|
|
_entManager,
|
|
_prototypeManager,
|
|
_cfgManager,
|
|
out var reasons))
|
|
selector.LockRequirements(_characterRequirementsSystem.GetRequirementsText(reasons));
|
|
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);
|
|
else if (selectedJobPrio == JobPriority.High &&
|
|
(JobPriority) other.Selected == JobPriority.High)
|
|
{
|
|
// 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();
|
|
};
|
|
|
|
_jobPriorities.Add((job.ID, selector));
|
|
jobContainer.AddChild(selector);
|
|
category.AddChild(jobContainer);
|
|
}
|
|
}
|
|
|
|
UpdateJobPriorities();
|
|
}
|
|
|
|
private void UpdateRoleRequirements()
|
|
{
|
|
JobList.DisposeAllChildren();
|
|
_jobCategories.Clear();
|
|
_jobPriorities.Clear();
|
|
var firstCategory = true;
|
|
|
|
var departments = _prototypeManager.EnumeratePrototypes<DepartmentPrototype>().ToArray();
|
|
Array.Sort(departments, 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-{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("#232323")}, // WD EDIT
|
|
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), };
|
|
|
|
var icon = new TextureRect
|
|
{
|
|
TextureScale = new Vector2(2, 2),
|
|
VerticalAlignment = VAlignment.Center
|
|
};
|
|
var jobIcon = _prototypeManager.Index(job.Icon);
|
|
icon.Texture = jobIcon.Icon.Frame0();
|
|
selector.Setup(items, job.LocalizedName, 200, job.LocalizedDescription, icon);
|
|
|
|
if (!_requirements.CheckJobWhitelist(job, out var reason))
|
|
selector.LockRequirements(reason);
|
|
else if (!_characterRequirementsSystem.CheckRequirementsValid(
|
|
job.Requirements ?? new(),
|
|
job,
|
|
Profile ?? HumanoidCharacterProfile.DefaultWithSpecies(),
|
|
_requirements.GetRawPlayTimeTrackers(),
|
|
_requirements.IsWhitelisted(),
|
|
job,
|
|
_entManager,
|
|
_prototypeManager,
|
|
_cfgManager,
|
|
out var reasons))
|
|
selector.LockRequirements(_characterRequirementsSystem.GetRequirementsText(reasons));
|
|
else
|
|
selector.UnlockRequirements();
|
|
|
|
category.AddChild(selector);
|
|
_jobPriorities.Add((job.ID, selector));
|
|
EnsureJobRequirementsValid(); // DeltaV
|
|
|
|
selector.OnSelected += priority =>
|
|
{
|
|
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(other.Selected);
|
|
else if ((JobPriority) other.Selected == JobPriority.High && (JobPriority) other.Selected == JobPriority.High)
|
|
{
|
|
// Lower any other high priorities to medium.
|
|
other.Select((int) JobPriority.Medium);
|
|
Profile = Profile?.WithJobPriority(jobId, JobPriority.Medium);
|
|
}
|
|
}
|
|
|
|
Profile = Profile?.WithJobPriority(job.ID, (JobPriority) priority);
|
|
ReloadPreview();
|
|
SetDirty();
|
|
SetProfile(Profile, CharacterSlot);
|
|
};
|
|
|
|
_jobPriorities.Add((job.ID, selector));
|
|
category.AddChild(jobContainer);
|
|
}
|
|
}
|
|
|
|
if (Profile is not null)
|
|
UpdateJobPriorities();
|
|
}
|
|
|
|
/// DeltaV - Make sure that no invalid job priorities get through
|
|
private void EnsureJobRequirementsValid()
|
|
{
|
|
foreach (var (jobId, selector) in _jobPriorities)
|
|
{
|
|
var proto = _prototypeManager.Index<JobPrototype>(jobId);
|
|
if ((JobPriority) selector.Selected == JobPriority.Never
|
|
|| _requirements.CheckJobWhitelist(proto, out _)
|
|
|| _characterRequirementsSystem.CheckRequirementsValid(
|
|
proto.Requirements ?? new(),
|
|
proto,
|
|
Profile ?? HumanoidCharacterProfile.DefaultWithSpecies(),
|
|
_requirements.GetRawPlayTimeTrackers(),
|
|
_requirements.IsWhitelisted(),
|
|
proto,
|
|
_entManager,
|
|
_prototypeManager,
|
|
_cfgManager,
|
|
out _))
|
|
continue;
|
|
|
|
selector.Select((int) JobPriority.Never);
|
|
Profile = Profile?.WithJobPriority(proto.ID, JobPriority.Never);
|
|
}
|
|
}
|
|
|
|
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()));
|
|
SetDirty();
|
|
ReloadProfilePreview();
|
|
}
|
|
|
|
private void OnSkinColorOnValueChanged()
|
|
{
|
|
if (Profile is null)
|
|
return;
|
|
|
|
var skin = _prototypeManager.Index<SpeciesPrototype>(Profile.Species).SkinColoration;
|
|
var skinColor = _prototypeManager.Index<SpeciesPrototype>(Profile.Species).DefaultSkinTone;
|
|
|
|
switch (skin)
|
|
{
|
|
case HumanoidSkinColor.HumanToned:
|
|
{
|
|
if (!Skin.Visible)
|
|
{
|
|
Skin.Visible = true;
|
|
RgbSkinColorContainer.Visible = false;
|
|
}
|
|
|
|
var color = SkinColor.HumanSkinTone((int) Skin.Value);
|
|
|
|
Markings.CurrentSkinColor = color;
|
|
Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color));//
|
|
break;
|
|
}
|
|
case HumanoidSkinColor.Hues:
|
|
{
|
|
if (!RgbSkinColorContainer.Visible)
|
|
{
|
|
Skin.Visible = false;
|
|
RgbSkinColorContainer.Visible = true;
|
|
}
|
|
|
|
Markings.CurrentSkinColor = _rgbSkinColorSelector.Color;
|
|
Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(_rgbSkinColorSelector.Color));
|
|
break;
|
|
}
|
|
case HumanoidSkinColor.TintedHues:
|
|
case HumanoidSkinColor.TintedHuesSkin: // DeltaV - Tone blending
|
|
{
|
|
if (!RgbSkinColorContainer.Visible)
|
|
{
|
|
Skin.Visible = false;
|
|
RgbSkinColorContainer.Visible = true;
|
|
}
|
|
|
|
var color = skin switch // DeltaV - Tone blending
|
|
{
|
|
HumanoidSkinColor.TintedHues => SkinColor.TintedHues(_rgbSkinColorSelector.Color),
|
|
HumanoidSkinColor.TintedHuesSkin => SkinColor.TintedHuesSkin(_rgbSkinColorSelector.Color, skinColor),
|
|
_ => Color.White
|
|
};
|
|
|
|
Markings.CurrentSkinColor = color;
|
|
Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color));
|
|
break;
|
|
}
|
|
case HumanoidSkinColor.VoxFeathers:
|
|
{
|
|
if (!RgbSkinColorContainer.Visible)
|
|
{
|
|
Skin.Visible = false;
|
|
RgbSkinColorContainer.Visible = true;
|
|
}
|
|
|
|
var color = SkinColor.ClosestVoxColor(_rgbSkinColorSelector.Color);
|
|
|
|
Markings.CurrentSkinColor = color;
|
|
Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color));
|
|
break;
|
|
}
|
|
case HumanoidSkinColor.AnimalFur: // Einstein Engines - Tajaran
|
|
{
|
|
if (!RgbSkinColorContainer.Visible)
|
|
{
|
|
Skin.Visible = false;
|
|
RgbSkinColorContainer.Visible = true;
|
|
}
|
|
|
|
var color = SkinColor.ClosestAnimalFurColor(_rgbSkinColorSelector.Color);
|
|
|
|
Markings.CurrentSkinColor = color;
|
|
Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color));
|
|
break;
|
|
}
|
|
}
|
|
|
|
SetDirty();
|
|
ReloadProfilePreview();
|
|
}
|
|
|
|
protected override void Dispose(bool disposing)
|
|
{
|
|
base.Dispose(disposing);
|
|
if (!disposing)
|
|
return;
|
|
|
|
_entManager.DeleteEntity(PreviewDummy);
|
|
PreviewDummy = EntityUid.Invalid;
|
|
|
|
_cfgManager.UnsubValueChanged(CCVars.GameLoadoutsEnabled, LoadoutsChanged);
|
|
}
|
|
|
|
private void SetAge(int newAge)
|
|
{
|
|
Profile = Profile?.WithAge(newAge);
|
|
ReloadPreview();
|
|
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();
|
|
Markings.SetSex(newSex);
|
|
UpdateTTSVoicesControls(); // WD EDIT
|
|
ReloadProfilePreview();
|
|
SetDirty();
|
|
}
|
|
|
|
// WD EDIT START
|
|
private void SetVoice(string newVoice)
|
|
{
|
|
Profile = Profile?.WithVoice(newVoice);
|
|
IsDirty = true;
|
|
}
|
|
// WD EDIT END
|
|
|
|
private void SetGender(Gender newGender)
|
|
{
|
|
Profile = Profile?.WithGender(newGender);
|
|
ReloadPreview();
|
|
IsDirty = true;
|
|
}
|
|
|
|
private void SetDisplayPronouns(string? displayPronouns)
|
|
{
|
|
if (displayPronouns == GetFormattedPronounsFromGender())
|
|
displayPronouns = null;
|
|
|
|
Profile = Profile?.WithDisplayPronouns(displayPronouns);
|
|
ReloadPreview();
|
|
IsDirty = true;
|
|
}
|
|
|
|
private void SetStationAiName(string? stationAiName)
|
|
{
|
|
Profile = Profile?.WithStationAiName(stationAiName);
|
|
ReloadPreview();
|
|
IsDirty = true;
|
|
}
|
|
|
|
private void SetCyborgName(string? cyborgName)
|
|
{
|
|
Profile = Profile?.WithCyborgName(cyborgName);
|
|
ReloadPreview();
|
|
IsDirty = true;
|
|
}
|
|
|
|
private string GetFormattedPronounsFromGender()
|
|
{
|
|
if (Profile == null)
|
|
return "they/them";
|
|
|
|
var genderName = Enum.GetName(typeof(Gender), Profile.Gender) ?? "Epicene";
|
|
var label = Loc.GetString($"humanoid-profile-editor-pronouns-{genderName.ToLower()}-text");
|
|
return label.Replace(" ", string.Empty).ToLower();
|
|
}
|
|
|
|
private void SetSpecies(string newSpecies)
|
|
{
|
|
Profile = Profile?.WithSpecies(newSpecies);
|
|
OnSkinColorOnValueChanged(); // Species may have special color prefs, make sure to update it.
|
|
Markings.SetSpecies(newSpecies); // Repopulate the markings tab as well.
|
|
UpdateSexControls(); // Update sex for new species
|
|
UpdateCharacterRequired();
|
|
// Changing species provides inaccurate sliders without these
|
|
UpdateHeightWidthSliders();
|
|
UpdateWeight();
|
|
UpdateSpeciesGuidebookIcon();
|
|
IsDirty = true;
|
|
ReloadProfilePreview();
|
|
ReloadClothes(); // Species may have job-specific gear, reload the clothes
|
|
}
|
|
|
|
private void SetName(string newName)
|
|
{
|
|
Profile = Profile?.WithName(newName);
|
|
IsDirty = true;
|
|
}
|
|
|
|
private void SetCustomSpecieName(string customname)
|
|
{
|
|
Profile = Profile?.WithCustomSpeciesName(customname);
|
|
IsDirty = true;
|
|
}
|
|
|
|
private void SetSpawnPriority(SpawnPriorityPreference newSpawnPriority)
|
|
{
|
|
Profile = Profile?.WithSpawnPriorityPreference(newSpawnPriority);
|
|
IsDirty = true;
|
|
}
|
|
|
|
private void SetProfileHeight(float height)
|
|
{
|
|
Profile = Profile?.WithHeight(height);
|
|
IsDirty = true;
|
|
ReloadProfilePreview();
|
|
}
|
|
|
|
private void SetProfileWidth(float width)
|
|
{
|
|
Profile = Profile?.WithWidth(width);
|
|
IsDirty = true;
|
|
ReloadProfilePreview();
|
|
}
|
|
|
|
private bool IsDirty
|
|
{
|
|
get => _isDirty;
|
|
set
|
|
{
|
|
if (_isDirty == value)
|
|
return;
|
|
|
|
_isDirty = value;
|
|
UpdateSaveButton();
|
|
}
|
|
}
|
|
|
|
private void UpdateNameEdit()
|
|
{
|
|
NameEdit.Text = Profile?.Name ?? "";
|
|
}
|
|
|
|
private void UpdateCustomSpecieNameEdit()
|
|
{
|
|
var species = _species.Find(x => x.ID == Profile?.Species) ?? _species.First();
|
|
_customspecienameEdit.Text = string.IsNullOrEmpty(Profile?.Customspeciename) ? Loc.GetString(species.Name) : Profile.Customspeciename;
|
|
_ccustomspecienamecontainerEdit.Visible = species.CustomName;
|
|
}
|
|
|
|
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<Sex>();
|
|
|
|
// Add species sex options, default to just none if we are in bizzaro world and have no species
|
|
if (_prototypeManager.TryIndex<SpeciesPrototype>(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<SpeciesPrototype>(Profile.Species).SkinColoration;
|
|
|
|
switch (skin)
|
|
{
|
|
case HumanoidSkinColor.HumanToned:
|
|
{
|
|
if (!Skin.Visible)
|
|
{
|
|
Skin.Visible = true;
|
|
RgbSkinColorContainer.Visible = false;
|
|
}
|
|
|
|
Skin.Value = SkinColor.HumanSkinToneFromColor(Profile.Appearance.SkinColor);
|
|
break;
|
|
}
|
|
case HumanoidSkinColor.Hues:
|
|
{
|
|
if (!RgbSkinColorContainer.Visible)
|
|
{
|
|
Skin.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)
|
|
{
|
|
Skin.Visible = false;
|
|
RgbSkinColorContainer.Visible = true;
|
|
}
|
|
|
|
// Set the RGB values to the direct values otherwise
|
|
_rgbSkinColorSelector.Color = Profile.Appearance.SkinColor;
|
|
break;
|
|
}
|
|
case HumanoidSkinColor.VoxFeathers:
|
|
{
|
|
if (!RgbSkinColorContainer.Visible)
|
|
{
|
|
Skin.Visible = false;
|
|
RgbSkinColorContainer.Visible = true;
|
|
}
|
|
|
|
_rgbSkinColorSelector.Color = SkinColor.ClosestVoxColor(Profile.Appearance.SkinColor);
|
|
|
|
break;
|
|
}
|
|
case HumanoidSkinColor.AnimalFur: // Einstein Engines - Tajaran
|
|
{
|
|
if (!RgbSkinColorContainer.Visible)
|
|
{
|
|
Skin.Visible = false;
|
|
RgbSkinColorContainer.Visible = true;
|
|
}
|
|
|
|
_rgbSkinColorSelector.Color = SkinColor.ClosestAnimalFurColor(Profile.Appearance.SkinColor);
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void UpdateSpeciesGuidebookIcon()
|
|
{
|
|
SpeciesInfoButton.StyleClasses.Clear();
|
|
|
|
var species = Profile?.Species;
|
|
if (species is null
|
|
|| !_prototypeManager.TryIndex<SpeciesPrototype>(species, out var speciesProto)
|
|
|| !_prototypeManager.HasIndex<GuideEntryPrototype>(species))
|
|
return;
|
|
|
|
const string style = "SpeciesInfoDefault";
|
|
SpeciesInfoButton.StyleClasses.Add(style);
|
|
}
|
|
|
|
private void UpdateMarkings()
|
|
{
|
|
if (Profile == null)
|
|
return;
|
|
|
|
Markings.SetData(Profile.Appearance.Markings, Profile.Species, Profile.Sex, Profile.Appearance.SkinColor,
|
|
Profile.Appearance.EyeColor);
|
|
}
|
|
|
|
private void UpdateGenderControls()
|
|
{
|
|
if (Profile == null)
|
|
return;
|
|
|
|
PronounsButton.SelectId((int) Profile.Gender);
|
|
}
|
|
|
|
private void UpdateDisplayPronounsControls()
|
|
{
|
|
if (Profile == null)
|
|
return;
|
|
|
|
var label = GetFormattedPronounsFromGender();
|
|
CosmeticPronounsNameEdit.PlaceHolder = label;
|
|
|
|
if (Profile.DisplayPronouns == null)
|
|
CosmeticPronounsNameEdit.Text = string.Empty;
|
|
else
|
|
CosmeticPronounsNameEdit.Text = Profile.DisplayPronouns;
|
|
}
|
|
|
|
private void UpdateStationAiControls()
|
|
{
|
|
if (Profile == null)
|
|
return;
|
|
|
|
StationAINameEdit.Text = Profile.StationAiName ?? string.Empty;
|
|
|
|
if (StationAINameEdit.Text != string.Empty)
|
|
return;
|
|
|
|
var stationAiNames = _prototypeManager.Index<LocalizedDatasetPrototype>(StationAiNames);
|
|
var randomName = _random.Pick(stationAiNames.Values);
|
|
StationAINameEdit.PlaceHolder = Loc.GetString(randomName);
|
|
}
|
|
|
|
private void UpdateCyborgControls()
|
|
{
|
|
if (Profile == null)
|
|
return;
|
|
|
|
CyborgNameEdit.Text = Profile.CyborgName ?? string.Empty;
|
|
|
|
if (CyborgNameEdit.Text != string.Empty)
|
|
return;
|
|
|
|
var borgNames = _prototypeManager.Index<DatasetPrototype>(CyborgNames);
|
|
var randomName = _random.Pick(borgNames.Values);
|
|
CyborgNameEdit.PlaceHolder = Loc.GetString(randomName);
|
|
}
|
|
|
|
private void UpdateSpawnPriorityControls()
|
|
{
|
|
if (Profile == null)
|
|
return;
|
|
|
|
SpawnPriorityButton.SelectId((int) Profile.SpawnPriority);
|
|
}
|
|
|
|
private void UpdateHeightWidthSliders()
|
|
{
|
|
if (Profile is null)
|
|
return;
|
|
|
|
var species = _species.Find(x => x.ID == Profile?.Species) ?? _species.First();
|
|
|
|
HeightSlider.MinValue = species.MinHeight;
|
|
HeightSlider.MaxValue = species.MaxHeight;
|
|
HeightSlider.SetValueWithoutEvent(Profile?.Height ?? species.DefaultHeight);
|
|
|
|
WidthSlider.MinValue = species.MinWidth;
|
|
WidthSlider.MaxValue = species.MaxWidth;
|
|
WidthSlider.SetValueWithoutEvent(Profile?.Width ?? species.DefaultWidth);
|
|
|
|
var height = MathF.Round(species.AverageHeight * HeightSlider.Value);
|
|
HeightLabel.Text = Loc.GetString("humanoid-profile-editor-height-label", ("height", (int) height));
|
|
|
|
var width = MathF.Round(species.AverageWidth * WidthSlider.Value);
|
|
WidthLabel.Text = Loc.GetString("humanoid-profile-editor-width-label", ("width", (int) width));
|
|
|
|
UpdateDimensions(SliderUpdate.Both);
|
|
}
|
|
|
|
private enum SliderUpdate
|
|
{
|
|
Height,
|
|
Width,
|
|
Both
|
|
}
|
|
|
|
private void UpdateDimensions(SliderUpdate updateType)
|
|
{
|
|
if (Profile == null)
|
|
return;
|
|
|
|
var species = _species.Find(x => x.ID == Profile?.Species) ?? _species.First();
|
|
|
|
var heightValue = Math.Clamp(HeightSlider.Value, species.MinHeight, species.MaxHeight);
|
|
var widthValue = Math.Clamp(WidthSlider.Value, species.MinWidth, species.MaxWidth);
|
|
var sizeRatio = species.SizeRatio;
|
|
var ratio = heightValue / widthValue;
|
|
|
|
if (updateType == SliderUpdate.Height || updateType == SliderUpdate.Both)
|
|
if (ratio < 1 / sizeRatio || ratio > sizeRatio)
|
|
widthValue = heightValue / (ratio < 1 / sizeRatio ? (1 / sizeRatio) : sizeRatio);
|
|
|
|
if (updateType == SliderUpdate.Width || updateType == SliderUpdate.Both)
|
|
if (ratio < 1 / sizeRatio || ratio > sizeRatio)
|
|
heightValue = widthValue * (ratio < 1 / sizeRatio ? (1 / sizeRatio) : sizeRatio);
|
|
|
|
heightValue = Math.Clamp(heightValue, species.MinHeight, species.MaxHeight);
|
|
widthValue = Math.Clamp(widthValue, species.MinWidth, species.MaxWidth);
|
|
|
|
HeightSlider.Value = heightValue;
|
|
WidthSlider.Value = widthValue;
|
|
|
|
SetProfileHeight(heightValue);
|
|
SetProfileWidth(widthValue);
|
|
|
|
var height = MathF.Round(species.AverageHeight * HeightSlider.Value);
|
|
HeightLabel.Text = Loc.GetString("humanoid-profile-editor-height-label", ("height", (int) height));
|
|
|
|
var width = MathF.Round(species.AverageWidth * WidthSlider.Value);
|
|
WidthLabel.Text = Loc.GetString("humanoid-profile-editor-width-label", ("width", (int) width));
|
|
|
|
UpdateWeight();
|
|
}
|
|
|
|
private void UpdateWeight()
|
|
{
|
|
if (Profile == null)
|
|
return;
|
|
|
|
var species = _species.Find(x => x.ID == Profile.Species) ?? _species.First();
|
|
_prototypeManager.Index(species.Prototype).TryGetComponent<FixturesComponent>(out var fixture);
|
|
|
|
if (fixture != null)
|
|
{
|
|
var radius = fixture.Fixtures["fix1"].Shape.Radius;
|
|
var density = fixture.Fixtures["fix1"].Density;
|
|
var avg = (Profile.Width + Profile.Height) / 2;
|
|
var weight = MathF.Round(MathF.PI * MathF.Pow(radius * avg, 2) * density);
|
|
WeightLabel.Text = Loc.GetString("humanoid-profile-editor-weight-label", ("weight", (int) weight));
|
|
}
|
|
else // Whelp, the fixture doesn't exist, guesstimate it instead
|
|
WeightLabel.Text = Loc.GetString("humanoid-profile-editor-weight-label", ("weight", (int) 71));
|
|
|
|
SpriteView.InvalidateMeasure();
|
|
}
|
|
|
|
private void UpdateHairPickers()
|
|
{
|
|
if (Profile == null)
|
|
return;
|
|
|
|
var hairMarking = Profile.Appearance.HairStyleId switch
|
|
{
|
|
HairStyles.DefaultHairStyle => new List<Marking>(),
|
|
_ => new() { new(Profile.Appearance.HairStyleId, new List<Color>() { Profile.Appearance.HairColor }) },
|
|
};
|
|
|
|
var facialHairMarking = Profile.Appearance.FacialHairStyleId switch
|
|
{
|
|
HairStyles.DefaultFacialHairStyle => new List<Marking>(),
|
|
_ => new() { new(Profile.Appearance.FacialHairStyleId, new List<Color>() { Profile.Appearance.FacialHairColor }) },
|
|
};
|
|
|
|
HairStylePicker.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, Profile.Sex, hairProto, _prototypeManager))
|
|
{
|
|
hairColor = _markingManager.MustMatchSkin(Profile.Species, HumanoidVisualLayers.Hair, out _, _prototypeManager)
|
|
? Profile.Appearance.SkinColor
|
|
: Profile.Appearance.HairColor;
|
|
}
|
|
}
|
|
|
|
if (hairColor != null)
|
|
Markings.HairMarking = new(Profile.Appearance.HairStyleId, new List<Color> { hairColor.Value });
|
|
else
|
|
Markings.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, Profile.Sex, facialHairProto, _prototypeManager))
|
|
{
|
|
facialHairColor = _markingManager.MustMatchSkin(Profile.Species, HumanoidVisualLayers.Hair, out _, _prototypeManager)
|
|
? Profile.Appearance.SkinColor
|
|
: Profile.Appearance.FacialHairColor;
|
|
}
|
|
}
|
|
|
|
if (facialHairColor != null)
|
|
Markings.FacialHairMarking = new(Profile.Appearance.FacialHairStyleId, new List<Color> { facialHairColor.Value });
|
|
else
|
|
Markings.FacialHairMarking = null;
|
|
}
|
|
|
|
private void UpdateEyePickers()
|
|
{
|
|
if (Profile == null)
|
|
return;
|
|
|
|
Markings.CurrentEyeColor = Profile.Appearance.EyeColor;
|
|
EyeColorPicker.SetData(Profile.Appearance.EyeColor);
|
|
}
|
|
|
|
private void UpdateSaveButton()
|
|
{
|
|
SaveButton.Disabled = Profile is null || !IsDirty;
|
|
ResetButton.Disabled = Profile is null || !IsDirty;
|
|
}
|
|
|
|
private void RandomizeProfile()
|
|
{
|
|
Profile = HumanoidCharacterProfile.Random();
|
|
SetProfile(Profile, CharacterSlot);
|
|
SetDirty();
|
|
}
|
|
|
|
private void SetPreviewRotation(Direction direction)
|
|
{
|
|
SpriteView.OverrideDirection = (Direction) ((int) direction % 4 * 2);
|
|
}
|
|
|
|
private void RandomizeName()
|
|
{
|
|
if (Profile == null)
|
|
return;
|
|
var name = HumanoidCharacterProfile.GetName(Profile.Species, Profile.Gender);
|
|
SetName(name);
|
|
UpdateNameEdit();
|
|
}
|
|
|
|
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")));
|
|
|
|
if (file == null)
|
|
{
|
|
EndExport();
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
var profile = _entManager.System<HumanoidAppearanceSystem>().FromStream(file, _playerManager.LocalSession!);
|
|
var oldProfile = Profile;
|
|
SetProfile(profile, CharacterSlot);
|
|
|
|
IsDirty = !profile.MemberwiseEquals(oldProfile);
|
|
}
|
|
catch (Exception exc)
|
|
{
|
|
Logger.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 = _entManager.System<HumanoidAppearanceSystem>().ToDataNode(Profile);
|
|
await using var writer = new StreamWriter(file.Value.fileStream);
|
|
dataNode.Write(writer);
|
|
}
|
|
catch (Exception exc)
|
|
{
|
|
Logger.Error($"Error when exporting profile: {exc.Message}\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;
|
|
}
|
|
|
|
#region Traits
|
|
|
|
#region Updates
|
|
|
|
private void UpdateTraitPreferences()
|
|
{
|
|
var points = _cfgManager.GetCVar(CCVars.GameTraitsDefaultPoints);
|
|
_traitCount = 0;
|
|
|
|
foreach (var preferenceSelector in _traitPreferences)
|
|
{
|
|
var traitId = preferenceSelector.Trait.ID;
|
|
var preference = Profile?.TraitPreferences.Contains(traitId) ?? false;
|
|
|
|
preferenceSelector.Preference = preference;
|
|
|
|
if (!preference)
|
|
continue;
|
|
|
|
points += preferenceSelector.Trait.Points;
|
|
_traitCount += 1;
|
|
}
|
|
|
|
TraitPointsBar.Value = points;
|
|
TraitPointsLabel.Text = Loc.GetString("humanoid-profile-editor-traits-header",
|
|
("points", points), ("traits", _traitCount),
|
|
("maxTraits", _cfgManager.GetCVar(CCVars.GameTraitsMax)));
|
|
|
|
// Set the remove unusable button's label to have the correct amount of unusable traits
|
|
TraitsRemoveUnusableButton.Text = Loc.GetString("humanoid-profile-editor-traits-remove-unusable-button",
|
|
("count", _traits
|
|
.Where(t => _traitPreferences
|
|
.Where(tps => tps.Preference).Select(tps => tps.Trait).Contains(t.Key))
|
|
.Count(t => !t.Value)));
|
|
AdminUIHelpers.RemoveConfirm(TraitsRemoveUnusableButton, _confirmationData);
|
|
|
|
IsDirty = true;
|
|
ReloadProfilePreview();
|
|
}
|
|
|
|
// Yeah this is mostly just copied from UpdateLoadouts
|
|
// This whole file is bad though and a lot of loadout code came from traits originally
|
|
//TODO Make this file not hell
|
|
private Dictionary<TraitPrototype, bool> _traits = new();
|
|
public void UpdateTraits(bool? showUnusable = null, bool reload = false)
|
|
{
|
|
showUnusable ??= TraitsShowUnusableButton.Pressed;
|
|
|
|
// Reset trait points so you don't get -14 points or something for no reason
|
|
var points = _cfgManager.GetCVar(CCVars.GameTraitsDefaultPoints);
|
|
TraitPointsLabel.Text = Loc.GetString("humanoid-profile-editor-traits-points-label", ("points", points), ("max", points));
|
|
TraitPointsBar.MaxValue = points;
|
|
TraitPointsBar.Value = points;
|
|
|
|
// Reset the whole UI and delete caches
|
|
if (reload)
|
|
{
|
|
foreach (var tab in TraitsTabs.Tabs)
|
|
TraitsTabs.RemoveTab(tab);
|
|
_loadoutPreferences.Clear();
|
|
}
|
|
|
|
|
|
// Get the highest priority job to use for trait filtering
|
|
var highJob = _controller.GetPreferredJob(Profile ?? HumanoidCharacterProfile.DefaultWithSpecies());
|
|
|
|
_traits.Clear();
|
|
foreach (var trait in _prototypeManager.EnumeratePrototypes<TraitPrototype>())
|
|
{
|
|
var usable = _characterRequirementsSystem.CheckRequirementsValid(
|
|
trait.Requirements,
|
|
highJob,
|
|
Profile ?? HumanoidCharacterProfile.DefaultWithSpecies(),
|
|
_requirements.GetRawPlayTimeTrackers(),
|
|
_requirements.IsWhitelisted(),
|
|
trait,
|
|
_entManager,
|
|
_prototypeManager,
|
|
_cfgManager,
|
|
out _
|
|
);
|
|
_traits.Add(trait, usable);
|
|
|
|
if (_traitPreferences.FindIndex(lps => lps.Trait.ID == trait.ID) is not (not -1 and var i))
|
|
continue;
|
|
|
|
var selector = _traitPreferences[i];
|
|
selector.Valid = usable;
|
|
selector.ShowUnusable = showUnusable.Value;
|
|
}
|
|
|
|
if (_traits.Count == 0)
|
|
{
|
|
TraitsTabs.AddTab(new Label { Text = Loc.GetString("humanoid-profile-editor-traits-no-traits") },
|
|
Loc.GetString("trait-category-Uncategorized"));
|
|
return;
|
|
}
|
|
|
|
|
|
var uncategorized = TraitsTabs.Contents.FirstOrDefault(c => c.Name == "Uncategorized");
|
|
if (uncategorized == null)
|
|
{
|
|
uncategorized = new BoxContainer
|
|
{
|
|
Name = "Uncategorized",
|
|
Orientation = LayoutOrientation.Vertical,
|
|
HorizontalExpand = true,
|
|
VerticalExpand = true,
|
|
// I hate ScrollContainers
|
|
Children =
|
|
{
|
|
new ScrollContainer
|
|
{
|
|
HScrollEnabled = false,
|
|
HorizontalExpand = true,
|
|
VerticalExpand = true,
|
|
Children =
|
|
{
|
|
new BoxContainer
|
|
{
|
|
Orientation = LayoutOrientation.Vertical,
|
|
HorizontalExpand = true,
|
|
VerticalExpand = true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
TraitsTabs.AddTab(uncategorized, Loc.GetString("trait-category-Uncategorized"));
|
|
}
|
|
|
|
// Create a Dictionary/tree of categories and subcategories
|
|
var cats = CreateTree(_prototypeManager.EnumeratePrototypes<TraitCategoryPrototype>()
|
|
.Where(c => c.Root)
|
|
.OrderBy(c => Loc.GetString($"trait-category-{c.ID}"))
|
|
.ToList());
|
|
var categories = new Dictionary<string, object>();
|
|
foreach (var (key, value) in cats)
|
|
categories.Add(key, value);
|
|
|
|
// Create the UI elements for the category tree
|
|
CreateCategoryUI(categories, TraitsTabs);
|
|
|
|
// Fill categories with traits
|
|
foreach (var (trait, usable) in _traits
|
|
.OrderBy(l => -l.Key.Points)
|
|
.ThenBy(l => l.Key.ID)
|
|
.ThenBy(l => Loc.GetString($"trait-name-{l.Key.ID}")))
|
|
{
|
|
if (_traitPreferences.Select(lps => lps.Trait.ID).Contains(trait.ID))
|
|
{
|
|
var first = _traitPreferences.First(lps => lps.Trait.ID == trait.ID);
|
|
first.Valid = usable;
|
|
first.ShowUnusable = showUnusable.Value;
|
|
continue;
|
|
}
|
|
|
|
var selector = new TraitPreferenceSelector(
|
|
trait, highJob, Profile ?? HumanoidCharacterProfile.DefaultWithSpecies(),
|
|
_entManager, _prototypeManager, _cfgManager, _characterRequirementsSystem, _requirements);
|
|
selector.Valid = usable;
|
|
selector.ShowUnusable = showUnusable.Value;
|
|
AddSelector(selector);
|
|
|
|
// Look for an existing category tab
|
|
var match = FindCategory(trait.Category, TraitsTabs);
|
|
|
|
// If there is no category put it in Uncategorized (this shouldn't happen)
|
|
(match ?? uncategorized).Children.First().Children.First().AddChild(selector);
|
|
}
|
|
|
|
// Hide any empty tabs
|
|
HideEmptyTabs(_prototypeManager.EnumeratePrototypes<TraitCategoryPrototype>().ToList());
|
|
|
|
UpdateTraitPreferences();
|
|
return;
|
|
|
|
|
|
void CreateCategoryUI(Dictionary<string, object> tree, NeoTabContainer parent)
|
|
{
|
|
foreach (var (key, value) in tree)
|
|
{
|
|
// If the category's container exists already, ignore it
|
|
if (parent.Contents.Any(c => c.Name == key))
|
|
continue;
|
|
|
|
// If the value is a list of TraitPrototypes, create a final tab for them
|
|
if (value is List<TraitPrototype>)
|
|
{
|
|
var category = new BoxContainer
|
|
{
|
|
Name = key,
|
|
Orientation = LayoutOrientation.Vertical,
|
|
HorizontalExpand = true,
|
|
VerticalExpand = true,
|
|
Children =
|
|
{
|
|
new ScrollContainer
|
|
{
|
|
HScrollEnabled = false,
|
|
HorizontalExpand = true,
|
|
VerticalExpand = true,
|
|
Children =
|
|
{
|
|
new BoxContainer
|
|
{
|
|
Orientation = LayoutOrientation.Vertical,
|
|
HorizontalExpand = true,
|
|
VerticalExpand = true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
parent.AddTab(category, Loc.GetString($"trait-category-{key}"));
|
|
}
|
|
// If the value is a dictionary, create a new tab for it and recursively call this function to fill it
|
|
else
|
|
{
|
|
var category = new NeoTabContainer
|
|
{
|
|
Name = key,
|
|
HorizontalExpand = true,
|
|
VerticalExpand = true,
|
|
SeparatorMargin = new Thickness(0),
|
|
};
|
|
|
|
parent.AddTab(category, Loc.GetString($"trait-category-{key}"));
|
|
CreateCategoryUI((Dictionary<string, object>) value, category);
|
|
}
|
|
}
|
|
}
|
|
|
|
void AddSelector(TraitPreferenceSelector selector)
|
|
{
|
|
_traitPreferences.Add(selector);
|
|
selector.PreferenceChanged += preference =>
|
|
{
|
|
// Make sure they have enough trait points
|
|
preference = CheckPoints(preference ? selector.Trait.Points : -selector.Trait.Points, preference);
|
|
// Make sure they have enough trait slots
|
|
preference = preference ? _traitCount < _cfgManager.GetCVar(CCVars.GameTraitsMax) : preference;
|
|
|
|
// Update Preferences
|
|
Profile = Profile?.WithTraitPreference(selector.Trait.ID, preference);
|
|
IsDirty = true;
|
|
UpdateTraitPreferences();
|
|
SetProfile(Profile, CharacterSlot);
|
|
};
|
|
}
|
|
|
|
bool CheckPoints(int points, bool preference)
|
|
{
|
|
var temp = TraitPointsBar.Value + points;
|
|
return preference ? !(temp < 0) : temp < 0;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Functions
|
|
|
|
private Dictionary<string, object> CreateTree(List<TraitCategoryPrototype> cats)
|
|
{
|
|
var tree = new Dictionary<string, object>();
|
|
foreach (var category in cats)
|
|
{
|
|
// If the category is already in the tree, ignore it
|
|
if (tree.ContainsKey(category.ID))
|
|
continue;
|
|
|
|
// Categories don't have a Parent field, so we need to instead check the SubCategories of every Category
|
|
var subCategories = category.SubCategories.Where(subCategory => !tree.ContainsKey(subCategory)).ToList();
|
|
// If there are no subcategories, add a loadout spot to the dictionary
|
|
if (subCategories.Count == 0)
|
|
{
|
|
tree.Add(category.ID, new List<TraitPrototype>());
|
|
continue;
|
|
}
|
|
|
|
// If there are subcategories, we need to add them to the dictionary as well
|
|
var subCategoryTree = CreateTree(subCategories.Select(c => _prototypeManager.Index(c)).ToList());
|
|
tree.Add(category.ID, subCategoryTree);
|
|
}
|
|
|
|
return tree;
|
|
}
|
|
|
|
private void HideEmptyTabs(List<TraitCategoryPrototype> cats)
|
|
{
|
|
foreach (var tab in cats.Select(category => FindCategory(category.ID, TraitsTabs)))
|
|
{
|
|
// If it's empty, hide it
|
|
if (tab != null)
|
|
((NeoTabContainer) tab.Parent!.Parent!.Parent!.Parent!).SetTabVisible(tab, tab.Children.First().Children.First().Children.Any());
|
|
|
|
// If it has a parent tab container, hide it if it's empty
|
|
if (tab?.Parent?.Parent is NeoTabContainer parent)
|
|
{
|
|
var parentCats = parent.Contents.Select(c => _prototypeManager.Index<TraitCategoryPrototype>(c.Name!)).ToList();
|
|
HideEmptyTabs(parentCats);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void TryRemoveUnusableTraits()
|
|
{
|
|
// Confirm the user wants to remove unusable loadouts
|
|
if (!AdminUIHelpers.TryConfirm(TraitsRemoveUnusableButton, _confirmationData))
|
|
return;
|
|
|
|
// Remove unusable traits
|
|
foreach (var (trait, _) in _traits.Where(l => !l.Value).ToList())
|
|
Profile = Profile?.WithTraitPreference(trait.ID, false);
|
|
UpdateCharacterRequired();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#endregion
|
|
|
|
#region Loadouts
|
|
|
|
#region Updates
|
|
|
|
private void UpdateLoadoutPreferences()
|
|
{
|
|
var points = _cfgManager.GetCVar(CCVars.GameLoadoutsPoints);
|
|
LoadoutPointsBar.Value = points;
|
|
LoadoutPointsLabel.Text = Loc.GetString("humanoid-profile-editor-loadouts-points-label", ("points", points), ("max", points));
|
|
|
|
foreach (var preferenceSelector in _loadoutPreferences)
|
|
{
|
|
var loadoutId = preferenceSelector.Loadout.ID;
|
|
var loadoutPreference = Profile?.LoadoutPreferences.FirstOrDefault(l => l.LoadoutName == loadoutId) ?? preferenceSelector.Preference;
|
|
var preference = new LoadoutPreference(
|
|
loadoutPreference.LoadoutName,
|
|
loadoutPreference.CustomName,
|
|
loadoutPreference.CustomDescription,
|
|
loadoutPreference.CustomColorTint,
|
|
loadoutPreference.CustomHeirloom)
|
|
{ Selected = loadoutPreference.Selected };
|
|
|
|
preferenceSelector.Preference = preference;
|
|
|
|
if (preference.Selected)
|
|
{
|
|
points -= preferenceSelector.Loadout.Cost;
|
|
LoadoutPointsBar.Value = points;
|
|
LoadoutPointsLabel.Text = Loc.GetString("humanoid-profile-editor-loadouts-points-label", ("points", points), ("max", LoadoutPointsBar.MaxValue));
|
|
}
|
|
}
|
|
|
|
// Set the remove unusable button's label to have the correct amount of unusable loadouts
|
|
LoadoutsRemoveUnusableButton.Text = Loc.GetString("humanoid-profile-editor-loadouts-remove-unusable-button",
|
|
("count", _loadouts
|
|
.Where(l => _loadoutPreferences
|
|
.Where(lps => lps.Preference.Selected).Select(lps => lps.Loadout).Contains(l.Key))
|
|
.Count(l => !l.Value
|
|
|| !_loadoutPreferences.First(lps => lps.Loadout == l.Key).Wearable)));
|
|
AdminUIHelpers.RemoveConfirm(LoadoutsRemoveUnusableButton, _confirmationData);
|
|
|
|
IsDirty = true;
|
|
ReloadProfilePreview();
|
|
}
|
|
|
|
private Dictionary<LoadoutPrototype, bool> _loadouts = new();
|
|
private Dictionary<string, EntityUid> _dummyLoadouts = new();
|
|
public void UpdateLoadouts(bool? showUnusable = null, bool reload = false)
|
|
{
|
|
showUnusable ??= LoadoutsShowUnusableButton.Pressed;
|
|
|
|
// Reset loadout points so you don't get -14 points or something for no reason
|
|
var points = _cfgManager.GetCVar(CCVars.GameLoadoutsPoints);
|
|
LoadoutPointsLabel.Text = Loc.GetString("humanoid-profile-editor-loadouts-points-label", ("points", points), ("max", points));
|
|
LoadoutPointsBar.MaxValue = points;
|
|
LoadoutPointsBar.Value = points;
|
|
|
|
// Reset the whole UI and delete caches
|
|
if (reload)
|
|
{
|
|
foreach (var tab in LoadoutsTabs.Tabs)
|
|
LoadoutsTabs.RemoveTab(tab);
|
|
foreach (var uid in _dummyLoadouts)
|
|
_entManager.QueueDeleteEntity(uid.Value);
|
|
_loadoutPreferences.Clear();
|
|
}
|
|
|
|
|
|
// Get the highest priority job to use for loadout filtering
|
|
var highJob = _controller.GetPreferredJob(Profile ?? HumanoidCharacterProfile.DefaultWithSpecies());
|
|
|
|
_loadouts.Clear();
|
|
foreach (var loadout in _prototypeManager.EnumeratePrototypes<LoadoutPrototype>())
|
|
{
|
|
var usable = _characterRequirementsSystem.CheckRequirementsValid(
|
|
loadout.Requirements,
|
|
highJob ?? new JobPrototype(),
|
|
Profile ?? HumanoidCharacterProfile.DefaultWithSpecies(),
|
|
_requirements.GetRawPlayTimeTrackers(),
|
|
_requirements.IsWhitelisted(),
|
|
loadout,
|
|
_entManager,
|
|
_prototypeManager,
|
|
_cfgManager,
|
|
out _
|
|
);
|
|
_loadouts.Add(loadout, usable);
|
|
|
|
var list = _loadoutPreferences.ToList();
|
|
if (list.FindIndex(lps => lps.Loadout.ID == loadout.ID) is not (not -1 and var i))
|
|
continue;
|
|
|
|
var selector = list[i];
|
|
UpdateSelector(selector, usable);
|
|
}
|
|
|
|
if (_loadouts.Count == 0)
|
|
{
|
|
LoadoutsTabs.AddTab(new Label { Text = Loc.GetString("humanoid-profile-editor-loadouts-no-loadouts") },
|
|
Loc.GetString("loadout-category-Uncategorized"));
|
|
return;
|
|
}
|
|
|
|
|
|
var uncategorized = LoadoutsTabs.Contents.FirstOrDefault(c => c.Name == "Uncategorized");
|
|
if (uncategorized == null)
|
|
{
|
|
uncategorized = new BoxContainer
|
|
{
|
|
Name = "Uncategorized",
|
|
Orientation = LayoutOrientation.Vertical,
|
|
HorizontalExpand = true,
|
|
VerticalExpand = true,
|
|
// I hate ScrollContainers
|
|
Children =
|
|
{
|
|
new ScrollContainer
|
|
{
|
|
HScrollEnabled = false,
|
|
HorizontalExpand = true,
|
|
VerticalExpand = true,
|
|
Children =
|
|
{
|
|
new BoxContainer
|
|
{
|
|
Orientation = LayoutOrientation.Vertical,
|
|
HorizontalExpand = true,
|
|
VerticalExpand = true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
LoadoutsTabs.AddTab(uncategorized, Loc.GetString("loadout-category-Uncategorized"));
|
|
}
|
|
|
|
// Create a Dictionary/tree of categories and subcategories
|
|
var cats = CreateTree(_prototypeManager.EnumeratePrototypes<LoadoutCategoryPrototype>()
|
|
.Where(c => c.Root)
|
|
.OrderBy(c => Loc.GetString($"loadout-category-{c.ID}"))
|
|
.ToList());
|
|
var categories = new Dictionary<string, object>();
|
|
foreach (var (key, value) in cats)
|
|
categories.Add(key, value);
|
|
|
|
// Create the UI elements for the category tree
|
|
CreateCategoryUI(categories, LoadoutsTabs);
|
|
|
|
// Fill categories with loadouts
|
|
foreach (var (loadout, usable) in _loadouts
|
|
.OrderBy(l => l.Key.ID)
|
|
.ThenBy(l => Loc.GetString($"loadout-name-{l.Key.ID}"))
|
|
.ThenBy(l => l.Key.Cost))
|
|
{
|
|
if (_loadoutPreferences.Select(lps => lps.Loadout.ID).Contains(loadout.ID))
|
|
{
|
|
var first = _loadoutPreferences.First(lps => lps.Loadout.ID == loadout.ID);
|
|
var prof = Profile?.LoadoutPreferences.FirstOrDefault(lp => lp.LoadoutName == loadout.ID);
|
|
first.Preference = new(loadout.ID, prof?.CustomName, prof?.CustomDescription, prof?.CustomColorTint, prof?.CustomHeirloom);
|
|
UpdateSelector(first, usable);
|
|
continue;
|
|
}
|
|
|
|
var selector = new LoadoutPreferenceSelector(
|
|
loadout, highJob ?? new JobPrototype(),
|
|
Profile ?? HumanoidCharacterProfile.DefaultWithSpecies(), ref _dummyLoadouts,
|
|
_entManager, _prototypeManager, _cfgManager, _characterRequirementsSystem, _requirements)
|
|
{ Preference = new(loadout.ID) };
|
|
UpdateSelector(selector, usable);
|
|
AddSelector(selector);
|
|
|
|
// Look for an existing category tab
|
|
var match = FindCategory(loadout.Category, LoadoutsTabs);
|
|
|
|
// If there is no category put it in Uncategorized (this shouldn't happen)
|
|
(match ?? uncategorized).Children.First().Children.First().AddChild(selector);
|
|
}
|
|
|
|
// Hide any empty tabs
|
|
HideEmptyTabs(_prototypeManager.EnumeratePrototypes<LoadoutCategoryPrototype>().ToList());
|
|
|
|
UpdateLoadoutPreferences();
|
|
return;
|
|
|
|
|
|
void UpdateSelector(LoadoutPreferenceSelector selector, bool usable)
|
|
{
|
|
selector.Valid = usable;
|
|
selector.ShowUnusable = showUnusable.Value;
|
|
|
|
foreach (var item in selector.Loadout.Items)
|
|
{
|
|
if (_dummyLoadouts.TryGetValue(selector.Loadout.ID + selector.Loadout.Items.IndexOf(item), out var entity)
|
|
&& _entManager.GetComponent<MetaDataComponent>(entity).EntityPrototype!.ID == item)
|
|
{
|
|
if (!_entManager.HasComponent<ClothingComponent>(entity))
|
|
{
|
|
selector.Wearable = true;
|
|
continue;
|
|
}
|
|
selector.Wearable = _characterRequirementsSystem.CanEntityWearItem(PreviewDummy, entity);
|
|
continue;
|
|
}
|
|
|
|
entity = _entManager.SpawnEntity(item, MapCoordinates.Nullspace);
|
|
_dummyLoadouts[selector.Loadout.ID + selector.Loadout.Items.IndexOf(item)] = entity;
|
|
|
|
if (!_entManager.HasComponent<ClothingComponent>(entity))
|
|
{
|
|
selector.Wearable = true;
|
|
continue;
|
|
}
|
|
selector.Wearable = _characterRequirementsSystem.CanEntityWearItem(PreviewDummy, entity);
|
|
}
|
|
}
|
|
|
|
void CreateCategoryUI(Dictionary<string, object> tree, NeoTabContainer parent)
|
|
{
|
|
foreach (var (key, value) in tree)
|
|
{
|
|
// If the category's container exists already, ignore it
|
|
if (parent.Contents.Any(c => c.Name == key))
|
|
continue;
|
|
|
|
// If the value is a list of LoadoutPrototypes, create a final tab for them
|
|
if (value is List<LoadoutPrototype>)
|
|
{
|
|
var category = new BoxContainer
|
|
{
|
|
Name = key,
|
|
Orientation = LayoutOrientation.Vertical,
|
|
HorizontalExpand = true,
|
|
VerticalExpand = true,
|
|
Children =
|
|
{
|
|
new ScrollContainer
|
|
{
|
|
HScrollEnabled = false,
|
|
HorizontalExpand = true,
|
|
VerticalExpand = true,
|
|
Children =
|
|
{
|
|
new BoxContainer
|
|
{
|
|
Orientation = LayoutOrientation.Vertical,
|
|
HorizontalExpand = true,
|
|
VerticalExpand = true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
parent.AddTab(category, Loc.GetString($"loadout-category-{key}"));
|
|
}
|
|
// If the value is a dictionary, create a new tab for it and recursively call this function to fill it
|
|
else
|
|
{
|
|
var category = new NeoTabContainer
|
|
{
|
|
Name = key,
|
|
HorizontalExpand = true,
|
|
VerticalExpand = true,
|
|
SeparatorMargin = new Thickness(0),
|
|
};
|
|
|
|
parent.AddTab(category, Loc.GetString($"loadout-category-{key}"));
|
|
CreateCategoryUI((Dictionary<string, object>) value, category);
|
|
}
|
|
}
|
|
}
|
|
|
|
void AddSelector(LoadoutPreferenceSelector selector)
|
|
{
|
|
_loadoutPreferences.Add(selector);
|
|
selector.PreferenceChanged += preference =>
|
|
{
|
|
// Make sure they have enough loadout points
|
|
var selected = preference.Selected
|
|
? CheckPoints(-selector.Loadout.Cost, preference.Selected)
|
|
: CheckPoints(selector.Loadout.Cost, preference.Selected);
|
|
|
|
// Update Preferences
|
|
Profile = Profile?.WithLoadoutPreference(
|
|
selector.Loadout.ID,
|
|
selected,
|
|
preference.CustomName,
|
|
preference.CustomDescription,
|
|
preference.CustomColorTint,
|
|
preference.CustomHeirloom);
|
|
IsDirty = true;
|
|
UpdateLoadoutPreferences();
|
|
SetProfile(Profile, CharacterSlot);
|
|
};
|
|
}
|
|
|
|
bool CheckPoints(int points, bool preference)
|
|
{
|
|
var temp = LoadoutPointsBar.Value + points;
|
|
return preference ? !(temp < 0) : temp < 0;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Functions
|
|
|
|
private Dictionary<string, object> CreateTree(List<LoadoutCategoryPrototype> cats)
|
|
{
|
|
var tree = new Dictionary<string, object>();
|
|
foreach (var category in cats)
|
|
{
|
|
// If the category is already in the tree, ignore it
|
|
if (tree.ContainsKey(category.ID))
|
|
continue;
|
|
|
|
// Categories don't have a Parent field, so we need to instead check the SubCategories of every Category
|
|
var subCategories = category.SubCategories.Where(subCategory => !tree.ContainsKey(subCategory)).ToList();
|
|
// If there are no subcategories, add a loadout spot to the dictionary
|
|
if (subCategories.Count == 0)
|
|
{
|
|
tree.Add(category.ID, new List<LoadoutPrototype>());
|
|
continue;
|
|
}
|
|
|
|
// If there are subcategories, we need to add them to the dictionary as well
|
|
var subCategoryTree = CreateTree(subCategories.Select(c => _prototypeManager.Index(c)).ToList());
|
|
tree.Add(category.ID, subCategoryTree);
|
|
}
|
|
|
|
return tree;
|
|
}
|
|
|
|
private BoxContainer? FindCategory(string id, NeoTabContainer parent)
|
|
{
|
|
BoxContainer? match = null;
|
|
foreach (var child in parent.Contents)
|
|
{
|
|
if (string.IsNullOrEmpty(child.Name))
|
|
continue;
|
|
|
|
if (child.Name == id)
|
|
match = (BoxContainer?) child;
|
|
}
|
|
|
|
if (match != null)
|
|
return match;
|
|
|
|
foreach (var subcategory in parent.Contents.Where(c => c is NeoTabContainer).Cast<NeoTabContainer>())
|
|
match ??= FindCategory(id, subcategory);
|
|
|
|
return match;
|
|
}
|
|
|
|
private void HideEmptyTabs(List<LoadoutCategoryPrototype> cats)
|
|
{
|
|
foreach (var tab in cats.Select(category => FindCategory(category.ID, LoadoutsTabs)))
|
|
{
|
|
// If it's empty, hide it
|
|
if (tab != null)
|
|
((NeoTabContainer) tab.Parent!.Parent!.Parent!.Parent!).SetTabVisible(tab, tab.Children.First().Children.First().Children.Any());
|
|
|
|
// If it has a parent tab container, hide it if it's empty
|
|
if (tab?.Parent?.Parent is NeoTabContainer parent)
|
|
{
|
|
var parentCats = parent.Contents.Select(c => _prototypeManager.Index<LoadoutCategoryPrototype>(c.Name!)).ToList();
|
|
HideEmptyTabs(parentCats);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void TryRemoveUnusableLoadouts()
|
|
{
|
|
// Confirm the user wants to remove unusable loadouts
|
|
if (!AdminUIHelpers.TryConfirm(LoadoutsRemoveUnusableButton, _confirmationData))
|
|
return;
|
|
|
|
// Remove unusable and unwearable loadouts
|
|
foreach (var (loadout, _) in
|
|
_loadouts.Where(l =>
|
|
!l.Value || !_loadoutPreferences.First(lps => lps.Loadout.ID == l.Key.ID).Wearable).ToList())
|
|
Profile = Profile?.WithLoadoutPreference(loadout.ID, false);
|
|
UpdateCharacterRequired();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#endregion
|
|
|
|
private void UpdateCharacterRequired()
|
|
{
|
|
UpdateRoleRequirements();
|
|
UpdateTraits(TraitsShowUnusableButton.Pressed);
|
|
UpdateLoadouts(LoadoutsShowUnusableButton.Pressed);
|
|
}
|
|
}
|
|
}
|