mirror of
https://github.com/WWhiteDreamProject/wwdpublic.git
synced 2026-04-16 21:17:39 +03:00
* - tweak: update StyleSheetify * - add: flexbox * - fix: size of flexbox in launchergui * - tweak: Profile editor: start. * - add: categories * - tweak: help me please with this shi... loadouts * - fix: container path think * - tweak: thinks for optimisation * - add: group selection for loadoutpicker * - tweak: change position of preview * - add: reason text * - fix: Кролькины фиксы * - fix: кролькины фиксы ч.2 * - fix: кролькины фиксы ч.3 * - кролькины фиксы - финал * - fix: Ворчливого дедушкины фиксы, удаление старого барахла и пометка wwdp * - tweak: some ui change for LoadoutCategories and LoadoutEntry * - ворчливый дед фиксы ч.2 * - fix: очередные кролькины фиксы * - add: loadout prototype validation * - fix: description read from edit field
2427 lines
88 KiB
C#
2427 lines
88 KiB
C#
using System.IO;
|
|
using System.Linq;
|
|
using System.Numerics;
|
|
using Content.Client.Administration.UI;
|
|
using Content.Client.Humanoid;
|
|
using Content.Client.Message;
|
|
using Content.Client.Players.PlayTimeTracking;
|
|
using Content.Client.Roles;
|
|
using Content.Client.Sprite;
|
|
using Content.Client.Stylesheets;
|
|
using Content.Client.UserInterface.Controls;
|
|
using Content.Client.UserInterface.Systems.Guidebook;
|
|
using Content.Shared._EE.Contractors.Prototypes;
|
|
using Content.Shared._White.CCVar;
|
|
using Content.Shared._White.Humanoid.Prototypes;
|
|
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.Guidebook;
|
|
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.ContentPack;
|
|
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 IClientPreferencesManager _preferencesManager;
|
|
private readonly IConfigurationManager _cfgManager;
|
|
private readonly IEntityManager _entManager;
|
|
private readonly IFileDialogManager _dialogManager;
|
|
private readonly IPlayerManager _playerManager;
|
|
private readonly IPrototypeManager _prototypeManager;
|
|
private readonly IResourceManager _resManager;
|
|
private readonly IRobustRandom _random;
|
|
private readonly MarkingManager _markingManager;
|
|
private readonly JobRequirementsManager _requirements;
|
|
private readonly LobbyUIController _controller;
|
|
private readonly CharacterRequirementsSystem _characterRequirementsSystem;
|
|
private readonly RoleSystem _roleSystem;
|
|
|
|
private FlavorText.FlavorText? _flavorText;
|
|
private TextEdit? _flavorTextEdit;
|
|
|
|
private bool _exporting;
|
|
private bool _imaging;
|
|
|
|
/// <summary>
|
|
/// If we're attempting to save.
|
|
/// </summary>
|
|
public event Action? Save;
|
|
|
|
/// <summary>
|
|
/// Entity used for the profile editor preview
|
|
/// </summary>
|
|
public EntityUid PreviewDummy;
|
|
|
|
/// <summary>
|
|
/// Temporary override of their selected job, used to preview roles.
|
|
/// </summary>
|
|
public JobPrototype? JobOverride;
|
|
|
|
/// <summary>
|
|
/// The character slot for the current profile.
|
|
/// </summary>
|
|
public int? CharacterSlot;
|
|
|
|
/// <summary>
|
|
/// The work in progress profile being edited.
|
|
/// </summary>
|
|
public HumanoidCharacterProfile? Profile;
|
|
|
|
private List<SpeciesPrototype> _species = new();
|
|
private List<BodyTypePrototype> _bodyTypes = new(); // WD EDIT
|
|
// EE - Contractor System Changes Start
|
|
private List<NationalityPrototype> _nationalies = new();
|
|
private List<EmployerPrototype> _employers = new();
|
|
private List<LifepathPrototype> _lifepaths = new();
|
|
// EE - Contractor System Changes End
|
|
|
|
private Dictionary<Button, ConfirmationData> _confirmationData = new();
|
|
private List<TraitPreferenceSelector> _traitPreferences = new();
|
|
private int _traitCount;
|
|
|
|
private bool _customizePronouns;
|
|
private bool _customizeStationAiName;
|
|
private bool _customizeBorgName;
|
|
private bool _customizeClownName; // WD EDIT
|
|
private bool _customizeMimeName; // WD EDIT
|
|
|
|
private List<(string, RequirementsSelector)> _jobPriorities = new();
|
|
|
|
private readonly Dictionary<string, BoxContainer> _jobCategories;
|
|
|
|
private ColorSelectorSliders _rgbSkinColorSelector;
|
|
|
|
private bool _isDirty;
|
|
|
|
[ValidatePrototypeId<GuideEntryPrototype>]
|
|
private const string DefaultSpeciesGuidebook = "Species";
|
|
|
|
public event Action<List<ProtoId<GuideEntryPrototype>>>? OnOpenGuidebook;
|
|
|
|
public event Action<HumanoidCharacterProfile, int>? OnProfileChanged;
|
|
|
|
private ISawmill _sawmill;
|
|
|
|
[ValidatePrototypeId<LocalizedDatasetPrototype>]
|
|
private const string StationAiNames = "NamesAI";
|
|
|
|
[ValidatePrototypeId<DatasetPrototype>]
|
|
private const string CyborgNames = "names_borg";
|
|
|
|
// WD EDIT START
|
|
[ValidatePrototypeId<LocalizedDatasetPrototype>]
|
|
private const string ClownNames = "ClownNames";
|
|
|
|
[ValidatePrototypeId<LocalizedDatasetPrototype>]
|
|
private const string MimeNames = "MimeNames";
|
|
|
|
private const string Uncategorized = "Uncategorized";
|
|
|
|
public SpriteView? CharacterSpriteView;
|
|
// WD EDIT END
|
|
|
|
public HumanoidProfileEditor(
|
|
IClientPreferencesManager preferencesManager,
|
|
IConfigurationManager cfgManager,
|
|
IEntityManager entManager,
|
|
IFileDialogManager dialogManager,
|
|
ILogManager logManager,
|
|
IPlayerManager playerManager,
|
|
IPrototypeManager prototypeManager,
|
|
IResourceManager resManager,
|
|
JobRequirementsManager requirements,
|
|
MarkingManager markings,
|
|
IRobustRandom random
|
|
)
|
|
{
|
|
RobustXamlLoader.Load(this);
|
|
_sawmill = logManager.GetSawmill("profile.editor");
|
|
_cfgManager = cfgManager;
|
|
_entManager = entManager;
|
|
_dialogManager = dialogManager;
|
|
_playerManager = playerManager;
|
|
_prototypeManager = prototypeManager;
|
|
_markingManager = markings;
|
|
_preferencesManager = preferencesManager;
|
|
_resManager = resManager;
|
|
_requirements = requirements;
|
|
_random = random;
|
|
|
|
_roleSystem = _entManager.System<RoleSystem>();
|
|
_characterRequirementsSystem = _entManager.System<CharacterRequirementsSystem>();
|
|
_controller = UserInterfaceManager.GetUIController<LobbyUIController>();
|
|
|
|
ImportButton.OnPressed += args => { ImportProfile(); };
|
|
ExportButton.OnPressed += args => { ExportProfile(); };
|
|
|
|
ExportImageButton.OnPressed += args => { ExportImage(); };
|
|
OpenImagesButton.OnPressed += args => { _resManager.UserData.OpenOsWindow(ContentSpriteSystem.Exports); };
|
|
|
|
ResetButton.OnPressed += args =>
|
|
{
|
|
SetProfile(
|
|
(HumanoidCharacterProfile?) _preferencesManager.Preferences?.SelectedCharacter,
|
|
_preferencesManager.Preferences?.SelectedCharacterIndex);
|
|
};
|
|
|
|
SaveButton.OnPressed += args => { Save?.Invoke(); };
|
|
|
|
#region Left
|
|
|
|
#region Name
|
|
|
|
NameEdit.OnTextChanged += args => { SetName(args.Text); };
|
|
NameRandomize.OnPressed += _ => RandomizeName();
|
|
RandomizeEverything.OnPressed += _ => { RandomizeProfile(); };
|
|
|
|
#endregion Name
|
|
|
|
#region Custom Species Name
|
|
|
|
CCustomSpecieNameEdit.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();
|
|
InitializeBark();
|
|
|
|
#endregion
|
|
|
|
#region BodyType
|
|
|
|
CBodyTypesButton.OnItemSelected += args =>
|
|
{
|
|
CBodyTypesButton.SelectId(args.Id);
|
|
SetBodyType(_bodyTypes[args.Id].ID);
|
|
};
|
|
|
|
#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);
|
|
_customizeClownName = _cfgManager.GetCVar(WhiteCVars.AllowCustomClownName); // WD EDIT
|
|
_customizeMimeName = _cfgManager.GetCVar(WhiteCVars.AllowCustomMimeName); // WD EDIT
|
|
|
|
_cfgManager.OnValueChanged(CCVars.AllowCustomStationAiName, OnChangedStationAiNameCustomizationValue);
|
|
_cfgManager.OnValueChanged(CCVars.AllowCustomCyborgName, OnChangedCyborgNameCustomizationValue);
|
|
_cfgManager.OnValueChanged(WhiteCVars.AllowCustomClownName, OnChangedClownNameCustomizationValue); // WD EDIT
|
|
_cfgManager.OnValueChanged(WhiteCVars.AllowCustomMimeName, OnChangedMimeNameCustomizationValue); // WD EDIT
|
|
|
|
StationAINameEdit.OnTextChanged += args => { SetStationAiName(args.Text); };
|
|
CyborgNameEdit.OnTextChanged += args => { SetCyborgName(args.Text); };
|
|
ClownNameEdit.OnTextChanged += args => { SetClownName(args.Text); }; // WD EDIT
|
|
MimeNameEdit.OnTextChanged += args => { SetMimeName(args.Text); }; // WD EDIT
|
|
|
|
if (StationAiNameContainer.Visible != _customizeStationAiName)
|
|
StationAiNameContainer.Visible = _customizeStationAiName;
|
|
|
|
if (CyborgNameContainer.Visible != _customizeBorgName)
|
|
CyborgNameContainer.Visible = _customizeBorgName;
|
|
|
|
// WD EDIT START
|
|
if (ClownNameContainer.Visible != _customizeClownName)
|
|
ClownNameContainer.Visible = _customizeClownName;
|
|
|
|
if (MimeNameContainer.Visible != _customizeMimeName)
|
|
MimeNameContainer.Visible = _customizeMimeName;
|
|
// WD EDIT END
|
|
|
|
#endregion
|
|
|
|
#region Species
|
|
|
|
RefreshSpecies();
|
|
|
|
SpeciesButton.OnItemSelected += args =>
|
|
{
|
|
SpeciesButton.SelectId(args.Id);
|
|
SetSpecies(_species[args.Id].ID);
|
|
UpdateHairPickers();
|
|
OnSkinColorOnValueChanged();
|
|
UpdateCustomSpecieNameEdit();
|
|
UpdateHeightWidthSliders();
|
|
};
|
|
|
|
#endregion Species
|
|
|
|
#region Contractors
|
|
|
|
if(_cfgManager.GetCVar(CCVars.ContractorsEnabled))
|
|
{
|
|
Background.Orphan();
|
|
CTabContainer.AddTab(Background, Loc.GetString("humanoid-profile-editor-background-tab"));
|
|
|
|
RefreshNationalities();
|
|
RefreshEmployers();
|
|
RefreshLifepaths();
|
|
|
|
NationalityButton.OnItemSelected += args =>
|
|
{
|
|
NationalityButton.SelectId(args.Id);
|
|
SetNationality(_nationalies[args.Id].ID);
|
|
};
|
|
|
|
EmployerButton.OnItemSelected += args =>
|
|
{
|
|
EmployerButton.SelectId(args.Id);
|
|
SetEmployer(_employers[args.Id].ID);
|
|
};
|
|
|
|
LifepathButton.OnItemSelected += args =>
|
|
{
|
|
LifepathButton.SelectId(args.Id);
|
|
SetLifepath(_lifepaths[args.Id].ID);
|
|
};
|
|
}
|
|
else
|
|
{
|
|
Background.Visible = false;
|
|
}
|
|
|
|
#endregion Contractors
|
|
|
|
#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 Markings
|
|
|
|
Markings.OnMarkingAdded += OnMarkingChange;
|
|
Markings.OnMarkingRemoved += OnMarkingChange;
|
|
Markings.OnMarkingColorChange += OnMarkingChange;
|
|
Markings.OnMarkingRankChange += OnMarkingChange;
|
|
|
|
#endregion Markings
|
|
|
|
RefreshFlavorText();
|
|
|
|
#endregion Left
|
|
|
|
ShowClothes.OnToggled += _ => { SetProfile(Profile, CharacterSlot); };
|
|
ShowLoadouts.OnToggled += _ => { SetProfile(Profile, CharacterSlot); };
|
|
|
|
SpeciesInfoButton.OnPressed += OnSpeciesInfoButtonPressed;
|
|
UpdateSpeciesGuidebookIcon();
|
|
|
|
ReloadPreview();
|
|
InitializeCharacterMenu(); // WWDP EDIT
|
|
IsDirty = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Refreshes the flavor text editor status.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
// WD EDIT START
|
|
private void OnChangedClownNameCustomizationValue(bool newValue)
|
|
{
|
|
_customizeClownName = newValue;
|
|
UpdateClownControls();
|
|
}
|
|
|
|
private void OnChangedMimeNameCustomizationValue(bool newValue)
|
|
{
|
|
_customizeMimeName = newValue;
|
|
UpdateMimeControls();
|
|
}
|
|
// WD EDIT END
|
|
|
|
/// <summary>
|
|
/// Refreshes the species selector.
|
|
/// </summary>
|
|
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 then reset it to default.
|
|
if (Profile != null && !speciesIds.Contains(Profile.Species))
|
|
SetSpecies(SharedHumanoidAppearanceSystem.DefaultSpecies);
|
|
}
|
|
|
|
public void RefreshNationalities()
|
|
{
|
|
NationalityButton.Clear();
|
|
_nationalies.Clear();
|
|
|
|
_nationalies.AddRange(_prototypeManager.EnumeratePrototypes<NationalityPrototype>()
|
|
.Where(o => _characterRequirementsSystem.CheckRequirementsValid(o.Requirements,
|
|
_controller.GetPreferredJob(Profile ?? HumanoidCharacterProfile.DefaultWithSpecies()),
|
|
Profile ?? HumanoidCharacterProfile.DefaultWithSpecies(),
|
|
_requirements.GetRawPlayTimeTrackers(),
|
|
_requirements.IsWhitelisted(),
|
|
o,
|
|
_entManager,
|
|
_prototypeManager,
|
|
_cfgManager, out _)));
|
|
|
|
var nationalityIds = _nationalies.Select(o => o.ID).ToList();
|
|
|
|
for (var i = 0; i < _nationalies.Count; i++)
|
|
{
|
|
NationalityButton.AddItem(Loc.GetString(_nationalies[i].NameKey), i);
|
|
|
|
if (Profile?.Nationality == _nationalies[i].ID)
|
|
NationalityButton.SelectId(i);
|
|
}
|
|
|
|
// If our nationality isn't available, reset it to default
|
|
if (Profile != null && !nationalityIds.Contains(Profile.Nationality))
|
|
SetNationality(SharedHumanoidAppearanceSystem.DefaultNationality);
|
|
|
|
if(Profile != null)
|
|
UpdateNationalityDescription(Profile.Nationality);
|
|
}
|
|
|
|
public void RefreshEmployers()
|
|
{
|
|
EmployerButton.Clear();
|
|
_employers.Clear();
|
|
|
|
_employers.AddRange(_prototypeManager.EnumeratePrototypes<EmployerPrototype>()
|
|
.Where(o => _characterRequirementsSystem.CheckRequirementsValid(o.Requirements,
|
|
_controller.GetPreferredJob(Profile ?? HumanoidCharacterProfile.DefaultWithSpecies()),
|
|
Profile ?? HumanoidCharacterProfile.DefaultWithSpecies(),
|
|
_requirements.GetRawPlayTimeTrackers(),
|
|
_requirements.IsWhitelisted(),
|
|
o,
|
|
_entManager,
|
|
_prototypeManager,
|
|
_cfgManager, out _)));
|
|
|
|
var employerIds = _employers.Select(o => o.ID).ToList();
|
|
|
|
for (var i = 0; i < _employers.Count; i++)
|
|
{
|
|
EmployerButton.AddItem(Loc.GetString(_employers[i].NameKey), i);
|
|
|
|
if (Profile?.Employer == _employers[i].ID)
|
|
EmployerButton.SelectId(i);
|
|
}
|
|
|
|
// If our employer isn't available, reset it to default
|
|
if (Profile != null && !employerIds.Contains(Profile.Employer))
|
|
SetEmployer(SharedHumanoidAppearanceSystem.DefaultEmployer);
|
|
|
|
if(Profile != null)
|
|
UpdateEmployerDescription(Profile.Employer);
|
|
}
|
|
|
|
public void RefreshLifepaths()
|
|
{
|
|
LifepathButton.Clear();
|
|
_lifepaths.Clear();
|
|
|
|
_lifepaths.AddRange(_prototypeManager.EnumeratePrototypes<LifepathPrototype>()
|
|
.Where(o => _characterRequirementsSystem.CheckRequirementsValid(o.Requirements,
|
|
_controller.GetPreferredJob(Profile ?? HumanoidCharacterProfile.DefaultWithSpecies()),
|
|
Profile ?? HumanoidCharacterProfile.DefaultWithSpecies(),
|
|
_requirements.GetRawPlayTimeTrackers(),
|
|
_requirements.IsWhitelisted(),
|
|
o,
|
|
_entManager,
|
|
_prototypeManager,
|
|
_cfgManager, out _)));
|
|
|
|
var lifepathIds = _lifepaths.Select(o => o.ID).ToList();
|
|
|
|
for (var i = 0; i < _lifepaths.Count; i++)
|
|
{
|
|
LifepathButton.AddItem(Loc.GetString(_lifepaths[i].NameKey), i);
|
|
|
|
if (Profile?.Lifepath == _lifepaths[i].ID)
|
|
LifepathButton.SelectId(i);
|
|
}
|
|
|
|
// If our lifepath isn't available, reset it to default
|
|
if (Profile != null && !lifepathIds.Contains(Profile.Lifepath))
|
|
SetLifepath(SharedHumanoidAppearanceSystem.DefaultLifepath);
|
|
|
|
if(Profile != null)
|
|
UpdateLifepathDescription(Profile.Lifepath);
|
|
}
|
|
|
|
private void UpdateNationalityDescription(string nationality)
|
|
{
|
|
var prototype = _prototypeManager.Index<NationalityPrototype>(nationality);
|
|
NationalityDescriptionLabel.SetMessage(Loc.GetString(prototype.DescriptionKey));
|
|
}
|
|
|
|
private void UpdateLifepathDescription(string lifepath)
|
|
{
|
|
var prototype = _prototypeManager.Index<LifepathPrototype>(lifepath);
|
|
LifepathDescriptionLabel.SetMessage(Loc.GetString(prototype.DescriptionKey));
|
|
}
|
|
|
|
private void UpdateEmployerDescription(string employer)
|
|
{
|
|
var prototype = _prototypeManager.Index<EmployerPrototype>(employer);
|
|
EmployerDescriptionLabel.SetMessage(Loc.GetString(prototype.DescriptionKey));
|
|
}
|
|
|
|
public void RefreshAntags()
|
|
{
|
|
AntagList.DisposeAllChildren();
|
|
var items = new[]
|
|
{
|
|
("humanoid-profile-editor-antag-preference-yes-button", 0),
|
|
("humanoid-profile-editor-antag-preference-no-button", 1)
|
|
};
|
|
// Causes a weird error if I just replace AntagList so whatever, have a child
|
|
var alt = new AlternatingBGContainer { Orientation = LayoutOrientation.Vertical, };
|
|
AntagList.AddChild(alt);
|
|
|
|
foreach (var antag in _prototypeManager.EnumeratePrototypes<AntagPrototype>().OrderBy(a => Loc.GetString(a.Name)))
|
|
{
|
|
if (!antag.SetPreference)
|
|
continue;
|
|
|
|
var antagContainer = new BoxContainer()
|
|
{
|
|
Orientation = LayoutOrientation.Horizontal,
|
|
HorizontalExpand = true,
|
|
};
|
|
|
|
var selector = new RequirementsSelector()
|
|
{
|
|
Margin = new(3f, 3f, 3f, 0f),
|
|
HorizontalExpand = true,
|
|
};
|
|
selector.OnOpenGuidebook += OnOpenGuidebook;
|
|
|
|
var title = Loc.GetString(antag.Name);
|
|
var description = Loc.GetString(antag.Objective);
|
|
selector.Setup(items, title, 250, description, guides: antag.Guides);
|
|
selector.Select(Profile?.AntagPreferences.Contains(antag.ID) == true ? 0 : 1);
|
|
|
|
if (!_characterRequirementsSystem.CheckRequirementsValid(
|
|
_roleSystem.GetAntagRequirement(antag) ?? 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);
|
|
alt.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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reloads the entire dummy entity for preview.
|
|
/// </summary>
|
|
/// <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(Profile.Species))
|
|
return;
|
|
|
|
PreviewDummy = _controller.LoadProfileEntity(Profile, null, ShowClothes.Pressed, ShowLoadouts.Pressed);
|
|
CharacterSpriteView?.SetEntity(PreviewDummy); // WWDP EDIT
|
|
|
|
// Check and set the dirty flag to enable the save/reset buttons as appropriate.
|
|
SetDirty();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reloads the dummy entity's clothes for preview
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resets the profile to the defaults.
|
|
/// </summary>
|
|
public void ResetToDefault()
|
|
{
|
|
SetProfile(
|
|
(HumanoidCharacterProfile?) _preferencesManager.Preferences?.SelectedCharacter,
|
|
_preferencesManager.Preferences?.SelectedCharacterIndex);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the editor to the specified profile with the specified slot.
|
|
/// </summary>
|
|
public void SetProfile(HumanoidCharacterProfile? profile, int? slot)
|
|
{
|
|
Profile = profile?.Clone();
|
|
CharacterSlot = slot;
|
|
IsDirty = false;
|
|
JobOverride = null;
|
|
|
|
UpdateNameEdit();
|
|
UpdateFlavorTextEdit();
|
|
UpdateSexControls();
|
|
UpdateTTSVoicesControls(); // WD EDIT
|
|
UpdateBarksControl(); // WD EDIT
|
|
UpdateBodyTypes(); // WD EDIT
|
|
UpdateGenderControls();
|
|
UpdateDisplayPronounsControls();
|
|
UpdateStationAiControls();
|
|
UpdateCyborgControls();
|
|
UpdateClownControls(); // WD EDIT
|
|
UpdateMimeControls(); // WD EDIT
|
|
UpdateSkinColor();
|
|
UpdateSpawnPriorityControls();
|
|
UpdateFlavorTextEdit();
|
|
UpdateCustomSpecieNameEdit();
|
|
UpdateAgeEdit();
|
|
UpdateEyePickers();
|
|
UpdateSaveButton();
|
|
UpdateMarkings();
|
|
UpdateLoadouts(); // WD EDIT
|
|
CheckpointLoadouts(); // WD EDIT
|
|
UpdateHairPickers();
|
|
UpdateCMarkingsHair();
|
|
UpdateCMarkingsFacialHair();
|
|
UpdateHeightWidthSliders();
|
|
UpdateWeight();
|
|
UpdateCharacterRequired();
|
|
|
|
RefreshAntags();
|
|
RefreshJobs();
|
|
RefreshSpecies();
|
|
RefreshNationalities();
|
|
RefreshEmployers();
|
|
RefreshLifepaths();
|
|
RefreshFlavorText();
|
|
ReloadPreview();
|
|
|
|
if (Profile != null)
|
|
PreferenceUnavailableButton.SelectId((int) Profile.PreferenceUnavailable);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// A slim reload that only updates the entity itself and not any of the job entities, etc.
|
|
/// </summary>
|
|
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, false, false);
|
|
// Reapply the hidden layers set from clothing
|
|
appearanceSystem.SetLayersVisibility(PreviewDummy, hiddenLayers, false, humanoid: humanoid);
|
|
}
|
|
|
|
TraitsTabs.UpdateTabMerging();
|
|
|
|
// Check and set the dirty flag to enable the save/reset buttons as appropriate.
|
|
SetDirty();
|
|
}
|
|
|
|
private void LoadoutsChanged(bool enabled)
|
|
{
|
|
CTabContainer.SetTabVisible(4, enabled);
|
|
ShowLoadouts.Visible = enabled;
|
|
}
|
|
|
|
private void OnSpeciesInfoButtonPressed(BaseButton.ButtonEventArgs args)
|
|
{
|
|
// TODO GUIDEBOOK
|
|
// make the species guide book a field on the species prototype.
|
|
// I.e., do what jobs/antags do.
|
|
|
|
var guidebookController = UserInterfaceManager.GetUIController<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<ProtoId<GuideEntryPrototype>, GuideEntry>();
|
|
dict.Add(DefaultSpeciesGuidebook, guideRoot);
|
|
//TODO: Don't close the guidebook if its already open, just go to the correct page
|
|
guidebookController.OpenGuidebook(dict, includeChildren:true, selected: page);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Refreshes all job selectors.
|
|
/// </summary>
|
|
public void RefreshJobs()
|
|
{
|
|
JobList.DisposeAllChildren();
|
|
_jobCategories.Clear();
|
|
_jobPriorities.Clear();
|
|
|
|
// 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),
|
|
};
|
|
|
|
var firstCategory = true;
|
|
foreach (var department in departments)
|
|
{
|
|
var departmentName = Loc.GetString(department.Name);
|
|
|
|
if (!_jobCategories.TryGetValue(department.ID, out var category))
|
|
{
|
|
category = new AlternatingBGContainer
|
|
{
|
|
Orientation = LayoutOrientation.Vertical,
|
|
Name = department.ID,
|
|
ToolTip = Loc.GetString("humanoid-profile-editor-jobs-amount-in-department-tooltip",
|
|
("departmentName", departmentName)),
|
|
Margin = new(0, firstCategory ? 0 : 20, 0, 0),
|
|
Children =
|
|
{
|
|
new Label
|
|
{
|
|
Text = Loc.GetString("humanoid-profile-editor-department-jobs-label",
|
|
("departmentName", departmentName)),
|
|
StyleClasses = { StyleBase.StyleClassLabelHeading, },
|
|
Margin = new(5f, 0, 0, 0),
|
|
},
|
|
},
|
|
};
|
|
|
|
firstCategory = false;
|
|
_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, HorizontalExpand = true, };
|
|
var selector = new RequirementsSelector { Margin = new(3f, 3f, 3f, 0f), HorizontalExpand = true, };
|
|
selector.OnOpenGuidebook += OnOpenGuidebook;
|
|
|
|
var icon = new TextureRect
|
|
{
|
|
TextureScale = new(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, job.Guides);
|
|
|
|
if (!_requirements.CheckJobWhitelist(job, out var reason))
|
|
selector.LockRequirements(reason);
|
|
else if (!_characterRequirementsSystem.CheckRequirementsValid(
|
|
_roleSystem.GetJobRequirement(job) ?? 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);
|
|
continue;
|
|
}
|
|
|
|
if (selectedJobPrio != JobPriority.High || (JobPriority) other.Selected != JobPriority.High)
|
|
continue;
|
|
|
|
// Lower any other high priorities to medium.
|
|
other.Select((int)JobPriority.Medium);
|
|
Profile = Profile?.WithJobPriority(jobId, JobPriority.Medium);
|
|
}
|
|
|
|
// TODO: Only reload on high change (either to or from).
|
|
ReloadPreview();
|
|
|
|
UpdateJobPriorities();
|
|
SetDirty();
|
|
};
|
|
|
|
_jobPriorities.Add((job.ID, selector));
|
|
jobContainer.AddChild(selector);
|
|
category.AddChild(jobContainer);
|
|
}
|
|
}
|
|
|
|
UpdateJobPriorities();
|
|
}
|
|
|
|
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()));
|
|
ReloadProfilePreview();
|
|
}
|
|
|
|
private void OnSkinColorOnValueChanged()
|
|
{
|
|
if (Profile is null)
|
|
return;
|
|
|
|
var species = _prototypeManager.Index(Profile.Species);
|
|
|
|
switch (species.SkinColoration)
|
|
{
|
|
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:
|
|
{
|
|
if (!RgbSkinColorContainer.Visible)
|
|
{
|
|
Skin.Visible = false;
|
|
RgbSkinColorContainer.Visible = true;
|
|
}
|
|
|
|
var color = species.SkinColoration switch
|
|
{
|
|
HumanoidSkinColor.TintedHues => SkinColor.TintedHues(_rgbSkinColorSelector.Color),
|
|
HumanoidSkinColor.TintedHuesSkin => SkinColor.TintedHuesSkin(_rgbSkinColorSelector.Color, species.DefaultSkinTone),
|
|
_ => 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;
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
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
|
|
UpdateBodyTypes(); // WD EDIT
|
|
UpdateBarksControl(); // WD EDIT
|
|
ReloadProfilePreview();
|
|
}
|
|
|
|
// WD EDIT START
|
|
private void SetVoice(string newVoice)
|
|
{
|
|
Profile = Profile?.WithVoice(newVoice);
|
|
IsDirty = true;
|
|
}
|
|
|
|
private void SetBodyType(string newBodyType)
|
|
{
|
|
Profile = Profile?.WithBodyType(newBodyType);
|
|
ReloadPreview();
|
|
IsDirty = true;
|
|
}
|
|
// WD EDIT END
|
|
|
|
private void SetGender(Gender newGender)
|
|
{
|
|
Profile = Profile?.WithGender(newGender);
|
|
ReloadPreview();
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// WD EDIT START
|
|
private void SetClownName(string? clownName)
|
|
{
|
|
Profile = Profile?.WithClownName(clownName);
|
|
ReloadPreview();
|
|
IsDirty = true;
|
|
}
|
|
|
|
private void SetMimeName(string? mimeName)
|
|
{
|
|
Profile = Profile?.WithMimeName(mimeName);
|
|
IsDirty = true;
|
|
}
|
|
// WD EDIT END
|
|
|
|
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();
|
|
UpdateBodyTypes(); // WD EDIT
|
|
ReloadProfilePreview();
|
|
ReloadClothes(); // Species may have job-specific gear, reload the clothes
|
|
}
|
|
|
|
private void SetNationality(string newNationality)
|
|
{
|
|
Profile = Profile?.WithNationality(newNationality);
|
|
UpdateCharacterRequired();
|
|
IsDirty = true;
|
|
ReloadProfilePreview();
|
|
ReloadClothes(); // Nationalities may have specific gear, reload the clothes
|
|
UpdateNationalityDescription(newNationality);
|
|
}
|
|
|
|
private void SetEmployer(string newEmployer)
|
|
{
|
|
Profile = Profile?.WithEmployer(newEmployer);
|
|
UpdateCharacterRequired();
|
|
IsDirty = true;
|
|
ReloadProfilePreview();
|
|
ReloadClothes(); // Employers may have specific gear, reload the clothes
|
|
UpdateEmployerDescription(newEmployer);
|
|
}
|
|
|
|
private void SetLifepath(string newLifepath)
|
|
{
|
|
Profile = Profile?.WithLifepath(newLifepath);
|
|
UpdateCharacterRequired();
|
|
IsDirty = true;
|
|
ReloadProfilePreview();
|
|
ReloadClothes(); // Lifepaths may have specific gear, reload the clothes
|
|
UpdateLifepathDescription(newLifepath);
|
|
}
|
|
|
|
private void SetName(string newName)
|
|
{
|
|
Profile = Profile?.WithName(newName);
|
|
IsDirty = true;
|
|
_entManager.System<MetaDataSystem>().SetEntityName(PreviewDummy, newName);
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
public 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();
|
|
CCustomSpecieNameEdit.Text = string.IsNullOrEmpty(Profile?.Customspeciename) ? Loc.GetString(species.Name) : Profile.Customspeciename;
|
|
CCustomSpecieName.Visible = species.CustomName;
|
|
}
|
|
|
|
private void UpdateFlavorTextEdit()
|
|
{
|
|
if (_flavorTextEdit != null)
|
|
_flavorTextEdit.TextRope = new Rope.Leaf(Profile?.FlavorText ?? "");
|
|
}
|
|
|
|
private void UpdateAgeEdit()
|
|
{
|
|
AgeEdit.Text = Profile?.Age.ToString() ?? "";
|
|
}
|
|
|
|
// WD EDIT START
|
|
private void UpdateBodyTypes()
|
|
{
|
|
if (Profile is null)
|
|
return;
|
|
|
|
CBodyTypesButton.Clear();
|
|
var species = _prototypeManager.Index<SpeciesPrototype>(Profile.Species);
|
|
var sex = Profile.Sex;
|
|
_bodyTypes = species.BodyTypes.Select(protoId => _prototypeManager.Index<BodyTypePrototype>(protoId))
|
|
.Where(proto => !proto.SexRestrictions.Contains(sex.ToString()))
|
|
.ToList();
|
|
|
|
for (var i = 0; i < _bodyTypes.Count; i++)
|
|
CBodyTypesButton.AddItem(Loc.GetString(_bodyTypes[i].Name), i);
|
|
|
|
// If current body type is not valid.
|
|
if (!_bodyTypes.Select(proto => proto.ID).Contains(Profile.BodyType))
|
|
{
|
|
// Then replace it with a first valid body type.
|
|
SetBodyType(_bodyTypes.First().ID);
|
|
}
|
|
|
|
CBodyTypesButton.Select(_bodyTypes.FindIndex(x => x.ID == Profile.BodyType));
|
|
IsDirty = true;
|
|
}
|
|
// WD EDIT END
|
|
|
|
/// <summary>
|
|
/// Updates selected job priorities to the profile's.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
// WD EDIT START
|
|
private void UpdateClownControls()
|
|
{
|
|
if (Profile == null)
|
|
return;
|
|
|
|
ClownNameEdit.Text = Profile.ClownName ?? string.Empty;
|
|
|
|
if (ClownNameEdit.Text != string.Empty)
|
|
return;
|
|
|
|
var clownNames = _prototypeManager.Index<LocalizedDatasetPrototype>(ClownNames);
|
|
var randomName = _random.Pick(clownNames.Values);
|
|
ClownNameEdit.PlaceHolder = Loc.GetString(randomName);
|
|
}
|
|
|
|
private void UpdateMimeControls()
|
|
{
|
|
if (Profile == null)
|
|
return;
|
|
|
|
MimeNameEdit.Text = Profile.MimeName ?? string.Empty;
|
|
|
|
if (MimeNameEdit.Text != string.Empty)
|
|
return;
|
|
|
|
var mimeNames = _prototypeManager.Index<LocalizedDatasetPrototype>(MimeNames);
|
|
var randomName = _random.Pick(mimeNames.Values);
|
|
MimeNameEdit.PlaceHolder = Loc.GetString(randomName);
|
|
}
|
|
// WD EDIT END
|
|
|
|
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));
|
|
|
|
CharacterSpriteView?.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 RandomizeName()
|
|
{
|
|
if (Profile == null)
|
|
return;
|
|
|
|
var name = HumanoidCharacterProfile.GetName(Profile.Species, Profile.Gender);
|
|
SetName(name);
|
|
UpdateNameEdit();
|
|
}
|
|
|
|
private async void ExportImage()
|
|
{
|
|
if (_imaging)
|
|
return;
|
|
|
|
// I tried disabling the button but it looks sorta goofy as it only takes a frame or two to save
|
|
_imaging = true;
|
|
await _entManager.System<ContentSpriteSystem>().Export(PreviewDummy, includeId: false);
|
|
_imaging = false;
|
|
}
|
|
|
|
private async void ImportProfile()
|
|
{
|
|
if (_exporting || CharacterSlot == null || Profile == null)
|
|
return;
|
|
|
|
StartExport();
|
|
await using var file = await _dialogManager.OpenFile(new FileDialogFilters(new FileDialogFilters.Group("yml")));
|
|
|
|
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)
|
|
{
|
|
_sawmill.Error($"Error when importing profile\n{exc.StackTrace}");
|
|
}
|
|
finally
|
|
{
|
|
EndExport();
|
|
}
|
|
}
|
|
|
|
private async void ExportProfile()
|
|
{
|
|
if (Profile == null || _exporting)
|
|
return;
|
|
|
|
StartExport();
|
|
var file = await _dialogManager.SaveFile(new FileDialogFilters(new FileDialogFilters.Group("yml")));
|
|
|
|
if (file == null)
|
|
{
|
|
EndExport();
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
var dataNode = _entManager.System<HumanoidAppearanceSystem>().ToDataNode(Profile);
|
|
await using var writer = new StreamWriter(file.Value.fileStream);
|
|
dataNode.Write(writer);
|
|
}
|
|
catch (Exception exc)
|
|
{
|
|
_sawmill.Error($"Error when exporting profile\n{exc.StackTrace}");
|
|
}
|
|
finally
|
|
{
|
|
EndExport();
|
|
await file.Value.fileStream.DisposeAsync();
|
|
}
|
|
}
|
|
|
|
private void StartExport()
|
|
{
|
|
_exporting = true;
|
|
ImportButton.Disabled = true;
|
|
ExportButton.Disabled = true;
|
|
}
|
|
|
|
private void EndExport()
|
|
{
|
|
_exporting = false;
|
|
ImportButton.Disabled = false;
|
|
ExportButton.Disabled = false;
|
|
}
|
|
|
|
#region Traits
|
|
|
|
#region Updates
|
|
|
|
private void UpdateTraitPreferences()
|
|
{
|
|
var points = _cfgManager.GetCVar(CCVars.GameTraitsDefaultPoints);
|
|
var maxTraits = _cfgManager.GetCVar(CCVars.GameTraitsMax);
|
|
if (Profile is not null && _prototypeManager.TryIndex<SpeciesPrototype>(Profile.Species, out var speciesPrototype))
|
|
points += speciesPrototype.BonusTraitPoints;
|
|
|
|
_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 += preferenceSelector.Trait.Slots;
|
|
}
|
|
|
|
TraitPointsBar.Value = points;
|
|
TraitPointsLabel.Text = Loc.GetString("humanoid-profile-editor-traits-header",
|
|
("points", points), ("traits", _traitCount),
|
|
("maxTraits", maxTraits));
|
|
|
|
// 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 = 10; // WD EDIT
|
|
TraitPointsBar.Value = points;
|
|
|
|
// Reset the whole UI and delete caches
|
|
if (reload)
|
|
{
|
|
foreach (var tab in TraitsTabs.TakenIds)
|
|
TraitsTabs.RemoveTab(tab);
|
|
}
|
|
|
|
|
|
// 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 = trait.Enable && // WD EDIT
|
|
_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;
|
|
}
|
|
|
|
if (!TraitsTabs.TryFindTabByAlias(Uncategorized, out var id))
|
|
{
|
|
var uncategorizedA = 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,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
id = TraitsTabs.AddTab(uncategorizedA, Loc.GetString("trait-category-Uncategorized"));
|
|
TraitsTabs.SetTabAlias(id, Uncategorized);
|
|
}
|
|
|
|
var uncategorized = TraitsTabs.GetControl<BoxContainer>(id)!;
|
|
|
|
// 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.TryFindTabByAlias(key, out var _))
|
|
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,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
var catId = parent.AddTab(category, Loc.GetString($"trait-category-{key}"));
|
|
parent.SetTabAlias(catId, 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),
|
|
};
|
|
|
|
var catId =parent.AddTab(category, Loc.GetString($"trait-category-{key}"));
|
|
parent.SetTabAlias(catId, 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 = CheckSlots(preference ? selector.Trait.Slots : -selector.Trait.Slots, 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;
|
|
}
|
|
|
|
bool CheckSlots(int slots, bool preference)
|
|
{
|
|
var temp = _traitCount + slots;
|
|
var max = _cfgManager.GetCVar(CCVars.GameTraitsMax);
|
|
return preference ? !(temp > max) : temp > max;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Functions
|
|
|
|
private BoxContainer? FindCategory(string id, NeoTabContainer parent)
|
|
{
|
|
BoxContainer? match = null;
|
|
|
|
if(parent.TryFindTabByAlias(id, out var tabId))
|
|
match = parent.GetControl<BoxContainer>(tabId);
|
|
|
|
|
|
if (match != null)
|
|
return match;
|
|
|
|
foreach (var subcategory in parent.GetControls<NeoTabContainer>())
|
|
match ??= FindCategory(id, subcategory);
|
|
|
|
return match;
|
|
}
|
|
|
|
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)
|
|
{
|
|
// TODO: HIDE LOGIC LATER
|
|
}
|
|
|
|
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
|
|
|
|
|
|
private void UpdateCharacterRequired()
|
|
{
|
|
RefreshNationalities();
|
|
RefreshEmployers();
|
|
RefreshLifepaths();
|
|
RefreshJobs();
|
|
UpdateTraits(TraitsShowUnusableButton.Pressed);
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO: Rewrite this shitty code! This shit is repeat and repeat!
|