From a4e369e6299d6a2a2e4f2803eb33922ee1fd82c8 Mon Sep 17 00:00:00 2001 From: DamianX Date: Sat, 18 Jan 2020 01:54:13 +0100 Subject: [PATCH] added Character Setup (#511) * added Character Setup * whoops * reverted unrelated changes * Made everything work post-rebase * Removed unused PreferencesChanged event * nope, don't need this * HumanoidProfileEditorPanel -> HumanoidProfileEditor * Set initial data for hair pickers * Fixed nullable warning * Renamed LooksComponent -> HumanoidAppearanceComponent * Renamed LooksComponentState -> HumanoidAppearanceComponentState * Final renaming maybe * Use a human-like dummy instead of a real human * Change preferences structs back to classes --- Content.Client/ClientPreferencesManager.cs | 36 +- Content.Client/EntryPoint.cs | 3 + .../MagicMirrorBoundUserInterface.cs | 3 +- .../Components/Mobs/HairComponent.cs | 72 ---- .../Mobs/HumanoidAppearanceComponent.cs | 62 ++++ .../GameTicking/ClientGameTicker.cs | 29 +- .../Interfaces/IClientPreferencesManager.cs | 6 +- .../UserInterface/CharacterSetupGui.cs | 268 ++++++++++++++ .../UserInterface/HumanoidProfileEditor.cs | 347 ++++++++++++++++++ .../LobbyCharacterPreviewPanel.cs | 101 +++++ Content.Client/UserInterface/LobbyGui.cs | 21 +- .../Content.Server.Database.csproj | 10 +- .../20200111103836_InitialCreate.Designer.cs | 109 ------ .../20200111103836_InitialCreate.cs | 71 ---- .../PreferencesDbContextModelSnapshot.cs | 105 ------ Content.Server.Database/Model.cs | 3 + Content.Server.Database/PrefsDb.cs | 6 +- Content.Server/Content.Server.csproj | 2 +- .../Components/MagicMirrorComponent.cs | 16 +- ...nent.cs => HumanoidAppearanceComponent.cs} | 2 +- Content.Server/GameTicking/GameTicker.cs | 195 ++++------ .../Preferences/PreferencesDatabase.cs | 41 ++- .../Components/Mobs/SharedHairComponent.cs | 109 ------ .../Mobs/SharedHumanoidAppearanceComponent.cs | 73 ++++ Content.Shared/GameObjects/ContentNetIDs.cs | 2 +- .../Appearance/HumanoidCharacterAppearance.cs | 4 +- .../HumanoidCharacterAppearance.cs | 73 +++- .../Preferences/HumanoidCharacterProfile.cs | 56 ++- .../Preferences/ICharacterProfile.cs | 2 + .../Preferences/PlayerPreferences.cs | 67 ++-- .../Preferences/PreferencesDatabaseTests.cs | 37 +- Resources/Prototypes/Entities/mobs/human.yml | 72 +++- Resources/Textures/Mob/human.rsi/fat_husk.png | Bin 0 -> 1905 bytes Resources/Textures/Mob/human.rsi/female.png | Bin 0 -> 1842 bytes .../Textures/Mob/human.rsi/female_fat.png | Bin 0 -> 1718 bytes .../Textures/Mob/human.rsi/female_slim.png | Bin 0 -> 1492 bytes Resources/Textures/Mob/human.rsi/husk.png | Bin 0 -> 1351 bytes Resources/Textures/Mob/human.rsi/male.png | Bin 1705 -> 1699 bytes Resources/Textures/Mob/human.rsi/male_fat.png | Bin 0 -> 1694 bytes .../Textures/Mob/human.rsi/male_slim.png | Bin 0 -> 1413 bytes Resources/Textures/Mob/human.rsi/meta.json | 176 +++++++-- 41 files changed, 1423 insertions(+), 756 deletions(-) delete mode 100644 Content.Client/GameObjects/Components/Mobs/HairComponent.cs create mode 100644 Content.Client/GameObjects/Components/Mobs/HumanoidAppearanceComponent.cs create mode 100644 Content.Client/UserInterface/CharacterSetupGui.cs create mode 100644 Content.Client/UserInterface/HumanoidProfileEditor.cs create mode 100644 Content.Client/UserInterface/LobbyCharacterPreviewPanel.cs delete mode 100644 Content.Server.Database/Migrations/20200111103836_InitialCreate.Designer.cs delete mode 100644 Content.Server.Database/Migrations/20200111103836_InitialCreate.cs delete mode 100644 Content.Server.Database/Migrations/PreferencesDbContextModelSnapshot.cs rename Content.Server/GameObjects/Components/Mobs/{HairComponent.cs => HumanoidAppearanceComponent.cs} (66%) create mode 100644 Content.Shared/GameObjects/Components/Mobs/SharedHumanoidAppearanceComponent.cs create mode 100644 Resources/Textures/Mob/human.rsi/fat_husk.png create mode 100644 Resources/Textures/Mob/human.rsi/female.png create mode 100644 Resources/Textures/Mob/human.rsi/female_fat.png create mode 100644 Resources/Textures/Mob/human.rsi/female_slim.png create mode 100644 Resources/Textures/Mob/human.rsi/husk.png create mode 100644 Resources/Textures/Mob/human.rsi/male_fat.png create mode 100644 Resources/Textures/Mob/human.rsi/male_slim.png diff --git a/Content.Client/ClientPreferencesManager.cs b/Content.Client/ClientPreferencesManager.cs index 3b338ba70c..35b1032fdd 100644 --- a/Content.Client/ClientPreferencesManager.cs +++ b/Content.Client/ClientPreferencesManager.cs @@ -1,3 +1,4 @@ +using System.Linq; using Content.Client.Interfaces; using Content.Shared.Preferences; using Robust.Shared.Interfaces.Network; @@ -6,8 +7,9 @@ using Robust.Shared.IoC; namespace Content.Client { /// - /// Receives and from the server during the initial connection. - /// Stores preferences on the server through and . + /// Receives and from the server during the initial + /// connection. + /// Stores preferences on the server through and . /// public class ClientPreferencesManager : SharedPreferencesManager, IClientPreferencesManager { @@ -24,14 +26,14 @@ namespace Content.Client HandlePreferencesAndSettings); } - private void HandlePreferencesAndSettings(MsgPreferencesAndSettings message) + public void SelectCharacter(ICharacterProfile profile) { - Preferences = message.Preferences; - Settings = message.Settings; + SelectCharacter(Preferences.IndexOfCharacter(profile)); } public void SelectCharacter(int slot) { + Preferences = new PlayerPreferences(Preferences.Characters, slot); var msg = _netManager.CreateNetMessage(); msg.SelectedCharacterIndex = slot; _netManager.ClientSendMessage(msg); @@ -39,10 +41,34 @@ namespace Content.Client public void UpdateCharacter(ICharacterProfile profile, int slot) { + var characters = Preferences.Characters.ToArray(); + characters[slot] = profile; + Preferences = new PlayerPreferences(characters, Preferences.SelectedCharacterIndex); var msg = _netManager.CreateNetMessage(); msg.Profile = profile; msg.Slot = slot; _netManager.ClientSendMessage(msg); } + + public void CreateCharacter(ICharacterProfile profile) + { + UpdateCharacter(profile, Preferences.FirstEmptySlot); + } + + public void DeleteCharacter(ICharacterProfile profile) + { + DeleteCharacter(Preferences.IndexOfCharacter(profile)); + } + + public void DeleteCharacter(int slot) + { + UpdateCharacter(null, slot); + } + + private void HandlePreferencesAndSettings(MsgPreferencesAndSettings message) + { + Preferences = message.Preferences; + Settings = message.Settings; + } } } diff --git a/Content.Client/EntryPoint.cs b/Content.Client/EntryPoint.cs index 577b7861d4..478c76e952 100644 --- a/Content.Client/EntryPoint.cs +++ b/Content.Client/EntryPoint.cs @@ -22,7 +22,9 @@ using Robust.Client.Interfaces.UserInterface; using Robust.Client.Player; using Robust.Shared.ContentPack; using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Interfaces.Map; using Robust.Shared.IoC; +using Robust.Shared.Map; using Robust.Shared.Prototypes; using Robust.Shared.Timing; @@ -215,6 +217,7 @@ namespace Content.Client IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); + IoCManager.Resolve().CreateNewMapEntity(MapId.Nullspace); IoCManager.Resolve().Initialize(); } diff --git a/Content.Client/GameObjects/Components/MagicMirrorBoundUserInterface.cs b/Content.Client/GameObjects/Components/MagicMirrorBoundUserInterface.cs index 4ce94dd235..93eb3d6184 100644 --- a/Content.Client/GameObjects/Components/MagicMirrorBoundUserInterface.cs +++ b/Content.Client/GameObjects/Components/MagicMirrorBoundUserInterface.cs @@ -77,7 +77,7 @@ namespace Content.Client.GameObjects.Components var styles = HairStyles.FacialHairStylesMap.ToList(); styles.Sort(HairStyles.FacialHairStyleComparer); - foreach (var (styleName, styleState) in styles) + foreach (var (styleName, styleState) in HairStyles.FacialHairStylesMap) { Items.AddItem(styleName, humanFacialHairRSI[styleState].Frame0); } @@ -141,6 +141,7 @@ namespace Content.Client.GameObjects.Components Items = new ItemList { SizeFlagsVertical = SizeFlags.FillExpand, + CustomMinimumSize = (300, 250) }; vBox.AddChild(Items); Items.OnItemSelected += ItemSelected; diff --git a/Content.Client/GameObjects/Components/Mobs/HairComponent.cs b/Content.Client/GameObjects/Components/Mobs/HairComponent.cs deleted file mode 100644 index 3940f40c44..0000000000 --- a/Content.Client/GameObjects/Components/Mobs/HairComponent.cs +++ /dev/null @@ -1,72 +0,0 @@ -using Content.Shared.GameObjects.Components.Mobs; -using Content.Shared.Preferences.Appearance; -using Robust.Client.GameObjects; -using Robust.Shared.GameObjects; -using Robust.Shared.Maths; - -namespace Content.Client.GameObjects.Components.Mobs -{ - [RegisterComponent] - public sealed class HairComponent : SharedHairComponent - { - protected override void Startup() - { - base.Startup(); - - UpdateHairStyle(); - } - - public override string FacialHairStyleName - { - get => base.FacialHairStyleName; - set - { - base.FacialHairStyleName = value; - UpdateHairStyle(); - } - } - - public override string HairStyleName - { - get => base.HairStyleName; - set - { - base.HairStyleName = value; - UpdateHairStyle(); - } - } - - public override Color HairColor - { - get => base.HairColor; - set - { - base.HairColor = value; - UpdateHairStyle(); - } - } - - public override Color FacialHairColor - { - get => base.FacialHairColor; - set - { - base.FacialHairColor = value; - UpdateHairStyle(); - } - } - - private void UpdateHairStyle() - { - var sprite = Owner.GetComponent(); - - sprite.LayerSetColor(HumanoidVisualLayers.Hair, HairColor); - sprite.LayerSetColor(HumanoidVisualLayers.FacialHair, FacialHairColor); - - sprite.LayerSetState(HumanoidVisualLayers.Hair, - HairStyles.HairStylesMap[HairStyleName ?? HairStyles.DefaultHairStyle]); - sprite.LayerSetState(HumanoidVisualLayers.FacialHair, - HairStyles.FacialHairStylesMap[FacialHairStyleName ?? HairStyles.DefaultFacialHairStyle]); - } - } -} diff --git a/Content.Client/GameObjects/Components/Mobs/HumanoidAppearanceComponent.cs b/Content.Client/GameObjects/Components/Mobs/HumanoidAppearanceComponent.cs new file mode 100644 index 0000000000..c7c4cd182e --- /dev/null +++ b/Content.Client/GameObjects/Components/Mobs/HumanoidAppearanceComponent.cs @@ -0,0 +1,62 @@ +using Content.Shared.GameObjects.Components.Mobs; +using Content.Shared.Preferences; +using Content.Shared.Preferences.Appearance; +using Robust.Client.GameObjects; +using Robust.Shared.GameObjects; + +namespace Content.Client.GameObjects.Components.Mobs +{ + [RegisterComponent] + public sealed class HumanoidAppearanceComponent : SharedHumanoidAppearanceComponent + { + public override HumanoidCharacterAppearance Appearance + { + get => base.Appearance; + set + { + base.Appearance = value; + UpdateLooks(); + } + } + + public override Sex Sex + { + get => base.Sex; + set + { + base.Sex = value; + UpdateLooks(); + } + } + + protected override void Startup() + { + base.Startup(); + + UpdateLooks(); + } + + private void UpdateLooks() + { + if (Appearance is null) return; + var sprite = Owner.GetComponent(); + + sprite.LayerSetColor(HumanoidVisualLayers.Hair, Appearance.HairColor); + sprite.LayerSetColor(HumanoidVisualLayers.FacialHair, Appearance.FacialHairColor); + + sprite.LayerSetState(HumanoidVisualLayers.Body, Sex == Sex.Male ? "male" : "female"); + + var hairStyle = Appearance.HairStyleName; + if (string.IsNullOrWhiteSpace(hairStyle) || !HairStyles.HairStylesMap.ContainsKey(hairStyle)) + hairStyle = HairStyles.DefaultHairStyle; + sprite.LayerSetState(HumanoidVisualLayers.Hair, + HairStyles.HairStylesMap[hairStyle]); + + var facialHairStyle = Appearance.FacialHairStyleName; + if (string.IsNullOrWhiteSpace(facialHairStyle) || !HairStyles.FacialHairStylesMap.ContainsKey(facialHairStyle)) + facialHairStyle = HairStyles.DefaultFacialHairStyle; + sprite.LayerSetState(HumanoidVisualLayers.FacialHair, + HairStyles.FacialHairStylesMap[facialHairStyle]); + } + } +} diff --git a/Content.Client/GameTicking/ClientGameTicker.cs b/Content.Client/GameTicking/ClientGameTicker.cs index 2e60f19f53..8dcfd10a3a 100644 --- a/Content.Client/GameTicking/ClientGameTicker.cs +++ b/Content.Client/GameTicking/ClientGameTicker.cs @@ -13,9 +13,9 @@ using Robust.Client.Interfaces.Input; using Robust.Client.Interfaces.ResourceManagement; using Robust.Client.Interfaces.UserInterface; using Robust.Client.Player; -using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Shared.Input; +using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.Network; using Robust.Shared.IoC; using Robust.Shared.Localization; @@ -38,16 +38,19 @@ namespace Content.Client.GameTicking [Dependency] private IResourceCache _resourceCache; [Dependency] private IPlayerManager _playerManager; [Dependency] private IGameHud _gameHud; + [Dependency] private IEntityManager _entityManager; + [Dependency] private IClientPreferencesManager _preferencesManager; #pragma warning restore 649 [ViewVariables] private bool _areWeReady; - [ViewVariables] private bool _initialized; - [ViewVariables] private TickerState _tickerState; + [ViewVariables] private CharacterSetupGui _characterSetup; [ViewVariables] private ChatBox _gameChat; - [ViewVariables] private LobbyGui _lobby; [ViewVariables] private bool _gameStarted; - [ViewVariables] private DateTime _startTime; + [ViewVariables] private bool _initialized; + [ViewVariables] private LobbyGui _lobby; [ViewVariables] private string _serverInfoBlob; + [ViewVariables] private DateTime _startTime; + [ViewVariables] private TickerState _tickerState; public void Initialize() { @@ -189,7 +192,15 @@ namespace Content.Client.GameTicking _tickerState = TickerState.InLobby; - _lobby = new LobbyGui(_localization, _resourceCache); + _characterSetup = new CharacterSetupGui(_entityManager, _localization, _resourceCache, _preferencesManager); + LayoutContainer.SetAnchorPreset(_characterSetup, LayoutContainer.LayoutPreset.Wide); + _characterSetup.CloseButton.OnPressed += args => + { + _lobby.CharacterPreview.UpdateUI(); + _userInterfaceManager.StateRoot.AddChild(_lobby); + _userInterfaceManager.StateRoot.RemoveChild(_characterSetup); + }; + _lobby = new LobbyGui(_entityManager, _localization, _resourceCache, _preferencesManager); _userInterfaceManager.StateRoot.AddChild(_lobby); LayoutContainer.SetAnchorPreset(_lobby, LayoutContainer.LayoutPreset.Wide); @@ -204,6 +215,12 @@ namespace Content.Client.GameTicking _updateLobbyUi(); + _lobby.CharacterPreview.CharacterSetupButton.OnPressed += args => + { + _userInterfaceManager.StateRoot.RemoveChild(_lobby); + _userInterfaceManager.StateRoot.AddChild(_characterSetup); + }; + _lobby.ObserveButton.OnPressed += args => _console.ProcessCommand("observe"); _lobby.ReadyButton.OnPressed += args => { diff --git a/Content.Client/Interfaces/IClientPreferencesManager.cs b/Content.Client/Interfaces/IClientPreferencesManager.cs index c983c4f933..d64f019e7e 100644 --- a/Content.Client/Interfaces/IClientPreferencesManager.cs +++ b/Content.Client/Interfaces/IClientPreferencesManager.cs @@ -4,10 +4,14 @@ namespace Content.Client.Interfaces { public interface IClientPreferencesManager { - void Initialize(); GameSettings Settings { get; } PlayerPreferences Preferences { get; } + void Initialize(); + void SelectCharacter(ICharacterProfile profile); void SelectCharacter(int slot); void UpdateCharacter(ICharacterProfile profile, int slot); + void CreateCharacter(ICharacterProfile profile); + void DeleteCharacter(ICharacterProfile profile); + void DeleteCharacter(int slot); } } diff --git a/Content.Client/UserInterface/CharacterSetupGui.cs b/Content.Client/UserInterface/CharacterSetupGui.cs new file mode 100644 index 0000000000..b647c5b26f --- /dev/null +++ b/Content.Client/UserInterface/CharacterSetupGui.cs @@ -0,0 +1,268 @@ +using Content.Client.GameObjects.Components.Mobs; +using Content.Client.Interfaces; +using Content.Client.Utility; +using Content.Shared.Preferences; +using Robust.Client.GameObjects; +using Robust.Client.Graphics.Drawing; +using Robust.Client.Interfaces.ResourceManagement; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Localization; +using Robust.Shared.Map; +using Robust.Shared.Maths; + +namespace Content.Client.UserInterface +{ + public class CharacterSetupGui : Control + { + private readonly VBoxContainer _charactersVBox; + private readonly Button _createNewCharacterButton; + private readonly IEntityManager _entityManager; + private readonly HumanoidProfileEditor _humanoidProfileEditor; + private readonly IClientPreferencesManager _preferencesManager; + public readonly Button CloseButton; + + public CharacterSetupGui(IEntityManager entityManager, + ILocalizationManager localization, + IResourceCache resourceCache, + IClientPreferencesManager preferencesManager) + { + _entityManager = entityManager; + _preferencesManager = preferencesManager; + var margin = new MarginContainer + { + MarginBottomOverride = 20, + MarginLeftOverride = 20, + MarginRightOverride = 20, + MarginTopOverride = 20 + }; + + AddChild(margin); + + var panelTex = resourceCache.GetTexture("/Nano/button.svg.96dpi.png"); + var back = new StyleBoxTexture + { + Texture = panelTex, + Modulate = new Color(37, 37, 42) + }; + back.SetPatchMargin(StyleBox.Margin.All, 10); + + var panel = new PanelContainer + { + PanelOverride = back + }; + + margin.AddChild(panel); + + var vBox = new VBoxContainer {SeparationOverride = 0}; + + margin.AddChild(vBox); + + CloseButton = new Button + { + SizeFlagsHorizontal = SizeFlags.Expand | SizeFlags.ShrinkEnd, + Text = localization.GetString("Save and close"), + StyleClasses = {NanoStyle.StyleClassButtonBig} + }; + + var topHBox = new HBoxContainer + { + CustomMinimumSize = (0, 40), + Children = + { + new MarginContainer + { + MarginLeftOverride = 8, + Children = + { + new Label + { + Text = localization.GetString("Character Setup"), + StyleClasses = {NanoStyle.StyleClassLabelHeadingBigger}, + VAlign = Label.VAlignMode.Center, + SizeFlagsHorizontal = SizeFlags.Expand | SizeFlags.ShrinkCenter + } + } + }, + CloseButton + } + }; + + vBox.AddChild(topHBox); + + vBox.AddChild(new PanelContainer + { + PanelOverride = new StyleBoxFlat + { + BackgroundColor = NanoStyle.NanoGold, + ContentMarginTopOverride = 2 + } + }); + + var hBox = new HBoxContainer + { + SizeFlagsVertical = SizeFlags.FillExpand, + SeparationOverride = 0 + }; + vBox.AddChild(hBox); + + _charactersVBox = new VBoxContainer(); + + hBox.AddChild(new MarginContainer + { + CustomMinimumSize = (420, 0), + SizeFlagsHorizontal = SizeFlags.Fill, + MarginTopOverride = 5, + MarginLeftOverride = 5, + Children = + { + new ScrollContainer + { + SizeFlagsVertical = SizeFlags.FillExpand, + Children = + { + _charactersVBox + } + } + } + }); + + _createNewCharacterButton = new Button + { + Text = "Create new slot...", + ToolTip = $"A maximum of {preferencesManager.Settings.MaxCharacterSlots} characters are allowed." + }; + _createNewCharacterButton.OnPressed += args => + { + preferencesManager.CreateCharacter(HumanoidCharacterProfile.Default()); + UpdateUI(); + }; + + hBox.AddChild(new PanelContainer + { + PanelOverride = new StyleBoxFlat {BackgroundColor = NanoStyle.NanoGold}, + CustomMinimumSize = (2, 0) + }); + _humanoidProfileEditor = new HumanoidProfileEditor(localization, preferencesManager); + _humanoidProfileEditor.OnProfileChanged += newProfile => { UpdateUI(); }; + hBox.AddChild(_humanoidProfileEditor); + + UpdateUI(); + } + + private void UpdateUI() + { + var numberOfFullSlots = 0; + var characterButtonsGroup = new ButtonGroup(); + _charactersVBox.RemoveAllChildren(); + var characterIndex = 0; + foreach (var character in _preferencesManager.Preferences.Characters) + { + if (character is null) + { + characterIndex++; + continue; + } + + numberOfFullSlots++; + var characterPickerButton = new CharacterPickerButton(_entityManager, + _preferencesManager, + characterButtonsGroup, + character); + _charactersVBox.AddChild(characterPickerButton); + + var characterIndexCopy = characterIndex; + characterPickerButton.ActualButton.OnPressed += args => + { + _humanoidProfileEditor.Profile = (HumanoidCharacterProfile) character; + _humanoidProfileEditor.CharacterSlot = characterIndexCopy; + _humanoidProfileEditor.UpdateControls(); + _preferencesManager.SelectCharacter(character); + }; + characterIndex++; + } + + _createNewCharacterButton.Disabled = + numberOfFullSlots >= _preferencesManager.Settings.MaxCharacterSlots; + _charactersVBox.AddChild(_createNewCharacterButton); + } + + private class CharacterPickerButton : Control + { + public readonly Button ActualButton; + private IEntity _previewDummy; + + public CharacterPickerButton( + IEntityManager entityManager, + IClientPreferencesManager preferencesManager, + ButtonGroup group, + ICharacterProfile profile) + { + _previewDummy = entityManager.SpawnEntityAt("HumanMob_Dummy", + new MapCoordinates(Vector2.Zero, MapId.Nullspace)); + _previewDummy.GetComponent().UpdateFromProfile(profile); + + var isSelectedCharacter = profile == preferencesManager.Preferences.SelectedCharacter; + + ActualButton = new Button + { + SizeFlagsHorizontal = SizeFlags.FillExpand, + SizeFlagsVertical = SizeFlags.FillExpand, + ToggleMode = true, + Group = group + }; + if (isSelectedCharacter) + ActualButton.Pressed = true; + AddChild(ActualButton); + + var view = new SpriteView + { + Sprite = _previewDummy.GetComponent(), + Scale = (2, 2), + MouseFilter = MouseFilterMode.Ignore, + OverrideDirection = Direction.South + }; + + var descriptionLabel = new Label + { + Text = $"{profile.Name}\nAssistant" //TODO implement job selection + }; + var deleteButton = new Button + { + Text = "Delete", + Visible = !isSelectedCharacter, + SizeFlagsHorizontal = SizeFlags.ShrinkEnd + }; + deleteButton.OnPressed += args => + { + Parent.RemoveChild(this); + preferencesManager.DeleteCharacter(profile); + }; + + var internalHBox = new HBoxContainer + { + SizeFlagsHorizontal = SizeFlags.FillExpand, + MouseFilter = MouseFilterMode.Ignore, + SeparationOverride = 0, + Children = + { + view, + descriptionLabel, + deleteButton + } + }; + + AddChild(internalHBox); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (!disposing) return; + _previewDummy.Delete(); + _previewDummy = null; + } + } + } +} diff --git a/Content.Client/UserInterface/HumanoidProfileEditor.cs b/Content.Client/UserInterface/HumanoidProfileEditor.cs new file mode 100644 index 0000000000..88e477dbd0 --- /dev/null +++ b/Content.Client/UserInterface/HumanoidProfileEditor.cs @@ -0,0 +1,347 @@ +using System; +using Content.Client.GameObjects.Components; +using Content.Client.Interfaces; +using Content.Shared.Preferences; +using Robust.Client.Graphics.Drawing; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Shared.Localization; +using Robust.Shared.Maths; + +namespace Content.Client.UserInterface +{ + public class HumanoidProfileEditor : Control + { + private static readonly StyleBoxFlat HighlightedStyle = new StyleBoxFlat + { + BackgroundColor = new Color(47, 47, 53), + ContentMarginTopOverride = 10, + ContentMarginBottomOverride = 10, + ContentMarginLeftOverride = 10, + ContentMarginRightOverride = 10 + }; + + private readonly LineEdit _ageEdit; + + private readonly LineEdit _nameEdit; + private readonly IClientPreferencesManager _preferencesManager; + private readonly Button _saveButton; + private readonly Button _sexFemaleButton; + private readonly Button _sexMaleButton; + private readonly HairStylePicker _hairPicker; + private readonly FacialHairStylePicker _facialHairPicker; + + private bool _isDirty; + public int CharacterSlot; + public HumanoidCharacterProfile Profile; + public event Action OnProfileChanged; + + public HumanoidProfileEditor(ILocalizationManager localization, + IClientPreferencesManager preferencesManager) + { + Profile = (HumanoidCharacterProfile) preferencesManager.Preferences.SelectedCharacter; + CharacterSlot = preferencesManager.Preferences.SelectedCharacterIndex; + _preferencesManager = preferencesManager; + + var margin = new MarginContainer + { + MarginTopOverride = 10, + MarginBottomOverride = 10, + MarginLeftOverride = 10, + MarginRightOverride = 10 + }; + AddChild(margin); + + var vBox = new VBoxContainer(); + margin.AddChild(vBox); + + var middleContainer = new HBoxContainer + { + SeparationOverride = 10 + }; + vBox.AddChild(middleContainer); + + var leftColumn = new VBoxContainer(); + middleContainer.AddChild(leftColumn); + + #region Randomize + + { + var panel = HighlightedContainer(); + var randomizeEverythingButton = new Button + { + Text = localization.GetString("Randomize everything"), + Disabled = true, + ToolTip = "Not yet implemented!" + }; + panel.AddChild(randomizeEverythingButton); + leftColumn.AddChild(panel); + } + + #endregion Randomize + + var middleColumn = new VBoxContainer(); + leftColumn.AddChild(middleColumn); + + #region Name + + { + var panel = HighlightedContainer(); + var hBox = new HBoxContainer + { + SizeFlagsVertical = SizeFlags.FillExpand + }; + var nameLabel = new Label {Text = localization.GetString("Name:")}; + _nameEdit = new LineEdit + { + CustomMinimumSize = (270, 0), + SizeFlagsVertical = SizeFlags.ShrinkCenter + }; + _nameEdit.OnTextChanged += args => + { + Profile = Profile?.WithName(args.Text); + IsDirty = true; + }; + var nameRandomButton = new Button + { + Text = localization.GetString("Randomize"), + Disabled = true, + ToolTip = "Not implemented yet!" + }; + hBox.AddChild(nameLabel); + hBox.AddChild(_nameEdit); + hBox.AddChild(nameRandomButton); + panel.AddChild(hBox); + middleColumn.AddChild(panel); + } + + #endregion Name + + var sexAndAgeRow = new HBoxContainer + { + SeparationOverride = 10 + }; + middleColumn.AddChild(sexAndAgeRow); + + #region Sex + + { + var panel = HighlightedContainer(); + var hBox = new HBoxContainer(); + var sexLabel = new Label {Text = localization.GetString("Sex:")}; + + var sexButtonGroup = new ButtonGroup(); + + _sexMaleButton = new Button + { + Text = localization.GetString("Male"), + Group = sexButtonGroup + }; + _sexMaleButton.OnPressed += args => + { + Profile = Profile?.WithSex(Sex.Male); + IsDirty = true; + }; + _sexFemaleButton = new Button + { + Text = localization.GetString("Female"), + Group = sexButtonGroup + }; + _sexFemaleButton.OnPressed += args => + { + Profile = Profile?.WithSex(Sex.Female); + IsDirty = true; + }; + hBox.AddChild(sexLabel); + hBox.AddChild(_sexMaleButton); + hBox.AddChild(_sexFemaleButton); + panel.AddChild(hBox); + sexAndAgeRow.AddChild(panel); + } + + #endregion Sex + + #region Age + + { + var panel = HighlightedContainer(); + var hBox = new HBoxContainer(); + var ageLabel = new Label {Text = localization.GetString("Age:")}; + _ageEdit = new LineEdit {CustomMinimumSize = (40, 0)}; + _ageEdit.OnTextChanged += args => + { + if (!int.TryParse(args.Text, out var newAge)) + return; + Profile = Profile?.WithAge(newAge); + IsDirty = true; + }; + hBox.AddChild(ageLabel); + hBox.AddChild(_ageEdit); + panel.AddChild(hBox); + sexAndAgeRow.AddChild(panel); + } + + #endregion Age + + var rightColumn = new VBoxContainer(); + middleContainer.AddChild(rightColumn); + + #region Import/Export + + { + var panelContainer = HighlightedContainer(); + var hBox = new HBoxContainer(); + var importButton = new Button + { + Text = localization.GetString("Import"), + Disabled = true, + ToolTip = "Not yet implemented!" + }; + var exportButton = new Button + { + Text = localization.GetString("Export"), + Disabled = true, + ToolTip = "Not yet implemented!" + }; + hBox.AddChild(importButton); + hBox.AddChild(exportButton); + panelContainer.AddChild(hBox); + rightColumn.AddChild(panelContainer); + } + + #endregion Import/Export + + #region Save + + { + var panel = HighlightedContainer(); + _saveButton = new Button + { + Text = localization.GetString("Save"), + SizeFlagsHorizontal = SizeFlags.ShrinkCenter + }; + _saveButton.OnPressed += args => + { + IsDirty = false; + _preferencesManager.UpdateCharacter(Profile, CharacterSlot); + OnProfileChanged?.Invoke(Profile); + }; + panel.AddChild(_saveButton); + rightColumn.AddChild(panel); + } + + #endregion Save + + #region Hair + + { + var panel = HighlightedContainer(); + panel.SizeFlagsHorizontal = SizeFlags.None; + var hairHBox = new HBoxContainer(); + + _hairPicker = new HairStylePicker(); + _hairPicker.Populate(); + + _hairPicker.OnHairStylePicked += newStyle => + { + if (Profile is null) + return; + Profile = Profile.WithCharacterAppearance( + Profile.Appearance.WithHairStyleName(newStyle)); + IsDirty = true; + }; + + _hairPicker.OnHairColorPicked += newColor => + { + if (Profile is null) + return; + Profile = Profile.WithCharacterAppearance( + Profile.Appearance.WithHairColor(newColor)); + IsDirty = true; + }; + + _facialHairPicker = new FacialHairStylePicker(); + _facialHairPicker.Populate(); + + _facialHairPicker.OnHairStylePicked += newStyle => + { + if (Profile is null) + return; + Profile = Profile.WithCharacterAppearance( + Profile.Appearance.WithFacialHairStyleName(newStyle)); + IsDirty = true; + }; + + _facialHairPicker.OnHairColorPicked += newColor => + { + if (Profile is null) + return; + Profile = Profile.WithCharacterAppearance( + Profile.Appearance.WithFacialHairColor(newColor)); + IsDirty = true; + }; + + hairHBox.AddChild(_hairPicker); + hairHBox.AddChild(_facialHairPicker); + + panel.AddChild(hairHBox); + vBox.AddChild(panel); + } + + #endregion Hair + + UpdateControls(); + } + + private bool IsDirty + { + get => _isDirty; + set + { + _isDirty = value; + UpdateSaveButton(); + } + } + + private static Control HighlightedContainer() + { + return new PanelContainer + { + PanelOverride = HighlightedStyle + }; + } + + private void UpdateSexControls() + { + if (Profile.Sex == Sex.Male) + _sexMaleButton.Pressed = true; + else + _sexFemaleButton.Pressed = true; + } + + private void UpdateHairPickers() + { + _hairPicker.SetInitialData( + Profile.Appearance.HairColor, + Profile.Appearance.HairStyleName); + _facialHairPicker.SetInitialData( + Profile.Appearance.FacialHairColor, + Profile.Appearance.FacialHairStyleName); + } + + private void UpdateSaveButton() + { + _saveButton.Disabled = !(Profile is null) || !IsDirty; + } + + public void UpdateControls() + { + if (Profile is null) return; + _nameEdit.Text = Profile?.Name; + UpdateSexControls(); + _ageEdit.Text = Profile?.Age.ToString(); + UpdateHairPickers(); + UpdateSaveButton(); + } + } +} diff --git a/Content.Client/UserInterface/LobbyCharacterPreviewPanel.cs b/Content.Client/UserInterface/LobbyCharacterPreviewPanel.cs new file mode 100644 index 0000000000..42935e3aee --- /dev/null +++ b/Content.Client/UserInterface/LobbyCharacterPreviewPanel.cs @@ -0,0 +1,101 @@ +using Content.Client.GameObjects.Components.Mobs; +using Content.Client.Interfaces; +using Content.Shared.Preferences; +using Robust.Client.Interfaces.GameObjects.Components; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Localization; +using Robust.Shared.Map; +using Robust.Shared.Maths; + +namespace Content.Client.UserInterface +{ + public class LobbyCharacterPreviewPanel : Control + { + private readonly IClientPreferencesManager _preferencesManager; + private IEntity _previewDummy; + private readonly Label _summaryLabel; + + public LobbyCharacterPreviewPanel(IEntityManager entityManager, + ILocalizationManager localization, + IClientPreferencesManager preferencesManager) + { + _preferencesManager = preferencesManager; + _previewDummy = entityManager.SpawnEntityAt("HumanMob_Dummy", + new MapCoordinates(Vector2.Zero, MapId.Nullspace)); + + var header = new NanoHeading + { + Text = localization.GetString("Character setup") + }; + + CharacterSetupButton = new Button + { + Text = localization.GetString("Customize"), + SizeFlagsHorizontal = SizeFlags.None + }; + + _summaryLabel = new Label(); + + var viewSouth = MakeSpriteView(_previewDummy, Direction.South); + var viewNorth = MakeSpriteView(_previewDummy, Direction.North); + var viewWest = MakeSpriteView(_previewDummy, Direction.West); + var viewEast = MakeSpriteView(_previewDummy, Direction.East); + + var vBox = new VBoxContainer(); + + vBox.AddChild(header); + vBox.AddChild(CharacterSetupButton); + + vBox.AddChild(_summaryLabel); + + var hBox = new HBoxContainer(); + hBox.AddChild(viewSouth); + hBox.AddChild(viewNorth); + hBox.AddChild(viewWest); + hBox.AddChild(viewEast); + + vBox.AddChild(hBox); + + AddChild(vBox); + + UpdateUI(); + } + + public Button CharacterSetupButton { get; } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (!disposing) return; + _previewDummy.Delete(); + _previewDummy = null; + } + + private static SpriteView MakeSpriteView(IEntity entity, Direction direction) + { + return new SpriteView + { + Sprite = entity.GetComponent(), + OverrideDirection = direction, + Scale = (2, 2) + }; + } + + public void UpdateUI() + { + if (!(_preferencesManager.Preferences.SelectedCharacter is HumanoidCharacterProfile selectedCharacter)) + { + _summaryLabel.Text = string.Empty; + } + else + { + _summaryLabel.Text = selectedCharacter.Summary; + _previewDummy + .GetComponent() + .Appearance = (HumanoidCharacterAppearance) selectedCharacter.CharacterAppearance; + } + } + } +} diff --git a/Content.Client/UserInterface/LobbyGui.cs b/Content.Client/UserInterface/LobbyGui.cs index 72e39a8301..5acd10f365 100644 --- a/Content.Client/UserInterface/LobbyGui.cs +++ b/Content.Client/UserInterface/LobbyGui.cs @@ -1,9 +1,11 @@ using Content.Client.Chat; +using Content.Client.Interfaces; using Content.Client.Utility; using Robust.Client.Graphics.Drawing; using Robust.Client.Interfaces.ResourceManagement; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; +using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Localization; using Robust.Shared.Maths; @@ -19,8 +21,12 @@ namespace Content.Client.UserInterface public ChatBox Chat { get; } public ItemList OnlinePlayerItemList { get; } public ServerInfo ServerInfo { get; } + public LobbyCharacterPreviewPanel CharacterPreview { get; } - public LobbyGui(ILocalizationManager localization, IResourceCache resourceCache) + public LobbyGui(IEntityManager entityManager, + ILocalizationManager localization, + IResourceCache resourceCache, + IClientPreferencesManager preferencesManager) { var margin = new MarginContainer { @@ -107,17 +113,20 @@ namespace Content.Client.UserInterface }; vBox.AddChild(hBox); + CharacterPreview = new LobbyCharacterPreviewPanel( + entityManager, + localization, + preferencesManager) + { + SizeFlagsHorizontal = SizeFlags.None + }; hBox.AddChild(new VBoxContainer { SizeFlagsHorizontal = SizeFlags.FillExpand, SeparationOverride = 0, Children = { - new Placeholder(resourceCache) - { - SizeFlagsVertical = SizeFlags.FillExpand, - PlaceholderText = localization.GetString("Character UI\nPlaceholder") - }, + CharacterPreview, new StripeBack { diff --git a/Content.Server.Database/Content.Server.Database.csproj b/Content.Server.Database/Content.Server.Database.csproj index 469168a8e8..8123f496ed 100644 --- a/Content.Server.Database/Content.Server.Database.csproj +++ b/Content.Server.Database/Content.Server.Database.csproj @@ -1,5 +1,5 @@  - + $(TargetFramework) @@ -11,18 +11,14 @@ true enable - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - - + diff --git a/Content.Server.Database/Migrations/20200111103836_InitialCreate.Designer.cs b/Content.Server.Database/Migrations/20200111103836_InitialCreate.Designer.cs deleted file mode 100644 index 3d5f2d5b1d..0000000000 --- a/Content.Server.Database/Migrations/20200111103836_InitialCreate.Designer.cs +++ /dev/null @@ -1,109 +0,0 @@ -// -using System; -using Content.Server.Database; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -namespace Content.Server.Database.Migrations -{ - [DbContext(typeof(PreferencesDbContext))] - [Migration("20200111103836_InitialCreate")] - partial class InitialCreate - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "3.1.0"); - - modelBuilder.Entity("Content.Server.Database.HumanoidProfile", b => - { - b.Property("HumanoidProfileId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Age") - .HasColumnType("INTEGER"); - - b.Property("CharacterName") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("EyeColor") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("FacialHairColor") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("FacialHairName") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("HairColor") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("HairName") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("PrefsId") - .HasColumnType("INTEGER"); - - b.Property("Sex") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("SkinColor") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Slot") - .HasColumnType("INTEGER"); - - b.Property("SlotName") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("HumanoidProfileId"); - - b.HasIndex("PrefsId"); - - b.ToTable("HumanoidProfile"); - }); - - modelBuilder.Entity("Content.Server.Database.Prefs", b => - { - b.Property("PrefsId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("SelectedCharacterSlot") - .HasColumnType("INTEGER"); - - b.Property("Username") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("PrefsId"); - - b.HasIndex("Username") - .IsUnique(); - - b.ToTable("Preferences"); - }); - - modelBuilder.Entity("Content.Server.Database.HumanoidProfile", b => - { - b.HasOne("Content.Server.Database.Prefs", null) - .WithMany("HumanoidProfiles") - .HasForeignKey("PrefsId"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Content.Server.Database/Migrations/20200111103836_InitialCreate.cs b/Content.Server.Database/Migrations/20200111103836_InitialCreate.cs deleted file mode 100644 index 1324d3ca88..0000000000 --- a/Content.Server.Database/Migrations/20200111103836_InitialCreate.cs +++ /dev/null @@ -1,71 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -namespace Content.Server.Database.Migrations -{ - public partial class InitialCreate : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - "Preferences", - table => new - { - PrefsId = table.Column() - .Annotation("Sqlite:Autoincrement", true), - Username = table.Column(), - SelectedCharacterSlot = table.Column() - }, - constraints: table => { table.PrimaryKey("PK_Preferences", x => x.PrefsId); }); - - migrationBuilder.CreateTable( - "HumanoidProfile", - table => new - { - HumanoidProfileId = table.Column() - .Annotation("Sqlite:Autoincrement", true), - Slot = table.Column(), - SlotName = table.Column(), - CharacterName = table.Column(), - Age = table.Column(), - Sex = table.Column(), - HairName = table.Column(), - HairColor = table.Column(), - FacialHairName = table.Column(), - FacialHairColor = table.Column(), - EyeColor = table.Column(), - SkinColor = table.Column(), - PrefsId = table.Column(nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_HumanoidProfile", x => x.HumanoidProfileId); - table.ForeignKey( - "FK_HumanoidProfile_Preferences_PrefsId", - x => x.PrefsId, - "Preferences", - "PrefsId", - onDelete: ReferentialAction.Restrict); - }); - - migrationBuilder.CreateIndex( - "IX_HumanoidProfile_PrefsId", - "HumanoidProfile", - "PrefsId"); - - migrationBuilder.CreateIndex( - "IX_Preferences_Username", - "Preferences", - "Username", - unique: true); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - "HumanoidProfile"); - - migrationBuilder.DropTable( - "Preferences"); - } - } -} diff --git a/Content.Server.Database/Migrations/PreferencesDbContextModelSnapshot.cs b/Content.Server.Database/Migrations/PreferencesDbContextModelSnapshot.cs deleted file mode 100644 index 1cdebf3e2e..0000000000 --- a/Content.Server.Database/Migrations/PreferencesDbContextModelSnapshot.cs +++ /dev/null @@ -1,105 +0,0 @@ -// - -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; - -namespace Content.Server.Database.Migrations -{ - [DbContext(typeof(PreferencesDbContext))] - internal class PreferencesDbContextModelSnapshot : ModelSnapshot - { - protected override void BuildModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "3.1.0"); - - modelBuilder.Entity("Content.Server.Database.HumanoidProfile", b => - { - b.Property("HumanoidProfileId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Age") - .HasColumnType("INTEGER"); - - b.Property("CharacterName") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("EyeColor") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("FacialHairColor") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("FacialHairName") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("HairColor") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("HairName") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("PrefsId") - .HasColumnType("INTEGER"); - - b.Property("Sex") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("SkinColor") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Slot") - .HasColumnType("INTEGER"); - - b.Property("SlotName") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("HumanoidProfileId"); - - b.HasIndex("PrefsId"); - - b.ToTable("HumanoidProfile"); - }); - - modelBuilder.Entity("Content.Server.Database.Prefs", b => - { - b.Property("PrefsId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("SelectedCharacterSlot") - .HasColumnType("INTEGER"); - - b.Property("Username") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("PrefsId"); - - b.HasIndex("Username") - .IsUnique(); - - b.ToTable("Preferences"); - }); - - modelBuilder.Entity("Content.Server.Database.HumanoidProfile", b => - { - b.HasOne("Content.Server.Database.Prefs", null) - .WithMany("HumanoidProfiles") - .HasForeignKey("PrefsId"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Content.Server.Database/Model.cs b/Content.Server.Database/Model.cs index 54b4fce67a..8c1dc07789 100644 --- a/Content.Server.Database/Model.cs +++ b/Content.Server.Database/Model.cs @@ -47,5 +47,8 @@ namespace Content.Server.Database public string FacialHairColor { get; set; } = null!; public string EyeColor { get; set; } = null!; public string SkinColor { get; set; } = null!; + + public int PrefsId { get; set; } + public Prefs Prefs { get; set; } = null!; } } diff --git a/Content.Server.Database/PrefsDb.cs b/Content.Server.Database/PrefsDb.cs index 506686dccc..622b85d07f 100644 --- a/Content.Server.Database/PrefsDb.cs +++ b/Content.Server.Database/PrefsDb.cs @@ -18,7 +18,10 @@ namespace Content.Server.Database public Prefs GetPlayerPreferences(string username) { - return _prefsCtx.Preferences.SingleOrDefault(p => p.Username == username); + return _prefsCtx + .Preferences + .Include(p => p.HumanoidProfiles) + .SingleOrDefault(p => p.Username == username); } public void SaveSelectedCharacterIndex(string username, int slot) @@ -45,6 +48,7 @@ namespace Content.Server.Database .SingleOrDefault(h => h.Slot == newProfile.Slot); if (!(oldProfile is null)) prefs.HumanoidProfiles.Remove(oldProfile); prefs.HumanoidProfiles.Add(newProfile); + _prefsCtx.SaveChanges(); } public void DeleteCharacterSlot(string username, int slot) diff --git a/Content.Server/Content.Server.csproj b/Content.Server/Content.Server.csproj index 948ae862a2..83dada1c9e 100644 --- a/Content.Server/Content.Server.csproj +++ b/Content.Server/Content.Server.csproj @@ -16,7 +16,7 @@ - + false diff --git a/Content.Server/GameObjects/Components/MagicMirrorComponent.cs b/Content.Server/GameObjects/Components/MagicMirrorComponent.cs index 2f827cd91f..c245ef6aa3 100644 --- a/Content.Server/GameObjects/Components/MagicMirrorComponent.cs +++ b/Content.Server/GameObjects/Components/MagicMirrorComponent.cs @@ -27,7 +27,7 @@ namespace Content.Server.GameObjects.Components private static void OnUiReceiveMessage(ServerBoundUserInterfaceMessage obj) { - if (!obj.Session.AttachedEntity.TryGetComponent(out HairComponent hair)) + if (!obj.Session.AttachedEntity.TryGetComponent(out HumanoidAppearanceComponent looks)) { return; } @@ -42,11 +42,11 @@ namespace Content.Server.GameObjects.Components if (msg.IsFacialHair) { - hair.FacialHairStyleName = msg.HairName; + looks.Appearance = looks.Appearance.WithFacialHairStyleName(msg.HairName); } else { - hair.HairStyleName = msg.HairName; + looks.Appearance = looks.Appearance.WithHairStyleName(msg.HairName); } break; @@ -57,11 +57,11 @@ namespace Content.Server.GameObjects.Components if (msg.IsFacialHair) { - hair.FacialHairColor = color; + looks.Appearance = looks.Appearance.WithFacialHairColor(color); } else { - hair.HairColor = color; + looks.Appearance = looks.Appearance.WithHairColor(color); } break; @@ -75,7 +75,7 @@ namespace Content.Server.GameObjects.Components return; } - if (!eventArgs.User.TryGetComponent(out HairComponent hair)) + if (!eventArgs.User.TryGetComponent(out HumanoidAppearanceComponent looks)) { Owner.PopupMessage(eventArgs.User, Loc.GetString("You can't have any hair!")); return; @@ -83,8 +83,8 @@ namespace Content.Server.GameObjects.Components _userInterface.Open(actor.playerSession); - var msg = new MagicMirrorInitialDataMessage(hair.HairColor, hair.FacialHairColor, hair.HairStyleName, - hair.FacialHairStyleName); + var msg = new MagicMirrorInitialDataMessage(looks.Appearance.HairColor, looks.Appearance.FacialHairColor, looks.Appearance.HairStyleName, + looks.Appearance.FacialHairStyleName); _userInterface.SendMessage(msg, actor.playerSession); } diff --git a/Content.Server/GameObjects/Components/Mobs/HairComponent.cs b/Content.Server/GameObjects/Components/Mobs/HumanoidAppearanceComponent.cs similarity index 66% rename from Content.Server/GameObjects/Components/Mobs/HairComponent.cs rename to Content.Server/GameObjects/Components/Mobs/HumanoidAppearanceComponent.cs index fc1ec345e1..0eb9196a32 100644 --- a/Content.Server/GameObjects/Components/Mobs/HairComponent.cs +++ b/Content.Server/GameObjects/Components/Mobs/HumanoidAppearanceComponent.cs @@ -4,7 +4,7 @@ using Robust.Shared.GameObjects; namespace Content.Server.GameObjects.Components.Mobs { [RegisterComponent] - public sealed class HairComponent : SharedHairComponent + public sealed class HumanoidAppearanceComponent : SharedHumanoidAppearanceComponent { } diff --git a/Content.Server/GameTicking/GameTicker.cs b/Content.Server/GameTicking/GameTicker.cs index 033b884bd0..a4749c4da6 100644 --- a/Content.Server/GameTicking/GameTicker.cs +++ b/Content.Server/GameTicking/GameTicker.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using Content.Server.GameObjects; using Content.Server.GameObjects.Components.Markers; +using Content.Server.GameObjects.Components.Mobs; using Content.Server.GameTicking.GamePresets; using Content.Server.Interfaces; using Content.Server.Interfaces.Chat; @@ -13,6 +14,7 @@ using Content.Server.Players; using Content.Shared; using Content.Shared.GameObjects.Components.Inventory; using Content.Shared.Jobs; +using Content.Shared.Preferences; using Robust.Server.Interfaces.Maps; using Robust.Server.Interfaces.Player; using Robust.Server.Player; @@ -41,16 +43,37 @@ namespace Content.Server.GameTicking { public class GameTicker : SharedGameTicker, IGameTicker { + private const string PlayerPrototypeName = "HumanMob_Content"; + private const string ObserverPrototypeName = "MobObserver"; + private const string MapFile = "Maps/stationstation.yml"; + + // Seconds. + private const float LobbyDuration = 20; + + [ViewVariables] private readonly List _gameRules = new List(); + + // Value is whether they're ready. + [ViewVariables] + private readonly Dictionary _playersInLobby = new Dictionary(); + + [ViewVariables] private bool _initialized; + + [ViewVariables] private Type _presetType; + + [ViewVariables] private bool _roundStartCountdownHasNotStartedYetDueToNoPlayers; + private DateTime _roundStartTimeUtc; + [ViewVariables] private GameRunLevel _runLevel; + [ViewVariables(VVAccess.ReadWrite)] private GridCoordinates _spawnPoint; + + [ViewVariables] private bool LobbyEnabled => _configurationManager.GetCVar("game.lobbyenabled"); + [ViewVariables] public GameRunLevel RunLevel { get => _runLevel; private set { - if (_runLevel == value) - { - return; - } + if (_runLevel == value) return; var old = _runLevel; _runLevel = value; @@ -61,46 +84,6 @@ namespace Content.Server.GameTicking public event Action OnRunLevelChanged; - private const string PlayerPrototypeName = "HumanMob_Content"; - private const string ObserverPrototypeName = "MobObserver"; - private const string MapFile = "Maps/stationstation.yml"; - - // Seconds. - private const float LobbyDuration = 20; - - [ViewVariables] private bool _initialized; - [ViewVariables(VVAccess.ReadWrite)] private GridCoordinates _spawnPoint; - [ViewVariables] private GameRunLevel _runLevel; - - [ViewVariables] private bool LobbyEnabled => _configurationManager.GetCVar("game.lobbyenabled"); - - // Value is whether they're ready. - [ViewVariables] - private readonly Dictionary _playersInLobby = new Dictionary(); - - [ViewVariables] private bool _roundStartCountdownHasNotStartedYetDueToNoPlayers; - private DateTime _roundStartTimeUtc; - - [ViewVariables] private readonly List _gameRules = new List(); - - [ViewVariables] private Type _presetType; - -#pragma warning disable 649 - [Dependency] private IEntityManager _entityManager; - [Dependency] private IMapManager _mapManager; - [Dependency] private IMapLoader _mapLoader; - [Dependency] private IGameTiming _gameTiming; - [Dependency] private IConfigurationManager _configurationManager; - [Dependency] private IPlayerManager _playerManager; - [Dependency] private IChatManager _chatManager; - [Dependency] private IServerNetManager _netManager; - [Dependency] private IDynamicTypeFactory _dynamicTypeFactory; - [Dependency] private IPrototypeManager _prototypeManager; - [Dependency] private readonly ILocalizationManager _localization; - [Dependency] private readonly IRobustRandom _robustRandom; - [Dependency] private readonly IServerPreferencesManager _prefsManager; -#pragma warning restore 649 - public void Initialize() { DebugTools.Assert(!_initialized); @@ -122,9 +105,7 @@ namespace Content.Server.GameTicking { if (RunLevel != GameRunLevel.PreRoundLobby || _roundStartTimeUtc > DateTime.UtcNow || _roundStartCountdownHasNotStartedYetDueToNoPlayers) - { return; - } StartRound(); } @@ -144,13 +125,9 @@ namespace Content.Server.GameTicking else { if (_playerManager.PlayerCount == 0) - { _roundStartCountdownHasNotStartedYetDueToNoPlayers = true; - } else - { _roundStartTimeUtc = DateTime.UtcNow + TimeSpan.FromSeconds(LobbyDuration); - } _sendStatusToAll(); } @@ -168,10 +145,7 @@ namespace Content.Server.GameTicking foreach (var (playerSession, ready) in _playersInLobby.ToList()) { - if (LobbyEnabled && !ready) - { - continue; - } + if (LobbyEnabled && !ready) continue; _spawnPlayer(playerSession); } @@ -192,41 +166,28 @@ namespace Content.Server.GameTicking targetPlayer.ContentData().WipeMind(); if (LobbyEnabled) - { _playerJoinLobby(targetPlayer); - } else - { _spawnPlayer(targetPlayer); - } } public void MakeObserve(IPlayerSession player) { - if (!_playersInLobby.ContainsKey(player)) - { - return; - } + if (!_playersInLobby.ContainsKey(player)) return; _spawnObserver(player); } public void MakeJoinGame(IPlayerSession player) { - if (!_playersInLobby.ContainsKey(player)) - { - return; - } + if (!_playersInLobby.ContainsKey(player)) return; _spawnPlayer(player); } public void ToggleReady(IPlayerSession player, bool ready) { - if (!_playersInLobby.ContainsKey(player)) - { - return; - } + if (!_playersInLobby.ContainsKey(player)) return; _playersInLobby[player] = ready; _netManager.ServerSendMessage(_getStatusMsg(player), player.ConnectedClient); @@ -244,10 +205,7 @@ namespace Content.Server.GameTicking public void RemoveGameRule(GameRule rule) { - if (_gameRules.Contains(rule)) - { - return; - } + if (_gameRules.Contains(rule)) return; rule.Removed(); @@ -258,10 +216,8 @@ namespace Content.Server.GameTicking public void SetStartPreset(Type type) { - if (!typeof(GamePreset).IsAssignableFrom(type)) - { - throw new ArgumentException("type must inherit GamePreset"); - } + if (!typeof(GamePreset).IsAssignableFrom(type)) throw new ArgumentException("type must inherit GamePreset"); + _presetType = type; UpdateInfoText(); } @@ -280,6 +236,7 @@ namespace Content.Server.GameTicking Logger.Error("{0} is an invalid equipment slot.", slotStr); continue; } + var equipmentEntity = _entityManager.SpawnEntity(equipmentStr, entity.Transform.GridPosition); inventory.Equip(slot, equipmentEntity.GetComponent()); } @@ -288,6 +245,14 @@ namespace Content.Server.GameTicking return entity; } + private void ApplyCharacterProfile(IEntity entity, ICharacterProfile profile) + { + if (profile is null) + return; + entity.GetComponent().UpdateFromProfile(profile); + entity.Name = profile.Name; + } + private IEntity _spawnObserverMob() { return _entityManager.SpawnEntityAt(ObserverPrototypeName, _getLateJoinSpawnPoint()); @@ -301,16 +266,10 @@ namespace Content.Server.GameTicking foreach (var entity in _entityManager.GetEntities(new TypeEntityQuery(typeof(SpawnPointComponent)))) { var point = entity.GetComponent(); - if (point.SpawnType == SpawnPointType.LateJoin) - { - possiblePoints.Add(entity.Transform.GridPosition); - } + if (point.SpawnType == SpawnPointType.LateJoin) possiblePoints.Add(entity.Transform.GridPosition); } - if (possiblePoints.Count != 0) - { - location = _robustRandom.Pick(possiblePoints); - } + if (possiblePoints.Count != 0) location = _robustRandom.Pick(possiblePoints); return location; } @@ -323,44 +282,29 @@ namespace Content.Server.GameTicking { // Delete all entities. foreach (var entity in _entityManager.GetEntities().ToList()) - { // TODO: Maybe something less naive here? // FIXME: Actually, definitely. entity.Delete(); - } // Delete all maps outside of nullspace. foreach (var mapId in _mapManager.GetAllMapIds().ToList()) - { // TODO: Maybe something less naive here? if (mapId != MapId.Nullspace) - { _mapManager.DeleteMap(mapId); - } - } // Delete the minds of everybody. // TODO: Maybe move this into a separate manager? - foreach (var unCastData in _playerManager.GetAllPlayerData()) - { - unCastData.ContentData().WipeMind(); - } + foreach (var unCastData in _playerManager.GetAllPlayerData()) unCastData.ContentData().WipeMind(); // Clear up any game rules. - foreach (var rule in _gameRules) - { - rule.Removed(); - } + foreach (var rule in _gameRules) rule.Removed(); _gameRules.Clear(); // Move everybody currently in the server to lobby. foreach (var player in _playerManager.GetAllPlayers()) { - if (_playersInLobby.ContainsKey(player)) - { - continue; - } + if (_playersInLobby.ContainsKey(player)) continue; _playerJoinLobby(player); } @@ -388,9 +332,7 @@ namespace Content.Server.GameTicking { // Always make sure the client has player data. Mind gets assigned on spawn. if (session.Data.ContentDataUncast == null) - { session.Data.ContentDataUncast = new PlayerData(session.SessionId); - } // timer time must be > tick length Timer.Spawn(0, args.Session.JoinGame); @@ -437,10 +379,7 @@ namespace Content.Server.GameTicking case SessionStatus.Disconnected: { - if (_playersInLobby.ContainsKey(session)) - { - _playersInLobby.Remove(session); - } + if (_playersInLobby.ContainsKey(session)) _playersInLobby.Remove(session); _chatManager.DispatchServerAnnouncement($"Player {args.Session.SessionId} left server!"); break; @@ -460,6 +399,10 @@ namespace Content.Server.GameTicking var mob = _spawnPlayerMob(job); data.Mind.TransferTo(mob); + var character = _prefsManager + .GetPreferences(session.SessionId.Username) + .SelectedCharacter; + ApplyCharacterProfile(mob, character); } private void _spawnObserver(IPlayerSession session) @@ -485,11 +428,9 @@ namespace Content.Server.GameTicking private void _playerJoinGame(IPlayerSession session) { - _chatManager.DispatchServerMessage(session, $"Welcome to Space Station 14! If this is your first time checking out the game, be sure to check out the tutorial in the top left!"); - if (_playersInLobby.ContainsKey(session)) - { - _playersInLobby.Remove(session); - } + _chatManager.DispatchServerMessage(session, + "Welcome to Space Station 14! If this is your first time checking out the game, be sure to check out the tutorial in the top left!"); + if (_playersInLobby.ContainsKey(session)) _playersInLobby.Remove(session); _netManager.ServerSendMessage(_netManager.CreateNetMessage(), session.ConnectedClient); } @@ -514,9 +455,7 @@ namespace Content.Server.GameTicking private void _sendStatusToAll() { foreach (var player in _playersInLobby.Keys) - { _netManager.ServerSendMessage(_getStatusMsg(player), player.ConnectedClient); - } } private string GetInfoText() @@ -538,6 +477,22 @@ The current game mode is [color=white]{0}[/color]", gameMode); { return _dynamicTypeFactory.CreateInstance(_presetType ?? typeof(PresetSandbox)); } + +#pragma warning disable 649 + [Dependency] private IEntityManager _entityManager; + [Dependency] private IMapManager _mapManager; + [Dependency] private IMapLoader _mapLoader; + [Dependency] private IGameTiming _gameTiming; + [Dependency] private IConfigurationManager _configurationManager; + [Dependency] private IPlayerManager _playerManager; + [Dependency] private IChatManager _chatManager; + [Dependency] private IServerNetManager _netManager; + [Dependency] private IDynamicTypeFactory _dynamicTypeFactory; + [Dependency] private IPrototypeManager _prototypeManager; + [Dependency] private readonly ILocalizationManager _localization; + [Dependency] private readonly IRobustRandom _robustRandom; + [Dependency] private readonly IServerPreferencesManager _prefsManager; +#pragma warning restore 649 } public enum GameRunLevel @@ -549,13 +504,13 @@ The current game mode is [color=white]{0}[/color]", gameMode); public class GameRunLevelChangedEventArgs : EventArgs { - public GameRunLevel OldRunLevel { get; } - public GameRunLevel NewRunLevel { get; } - public GameRunLevelChangedEventArgs(GameRunLevel oldRunLevel, GameRunLevel newRunLevel) { OldRunLevel = oldRunLevel; NewRunLevel = newRunLevel; } + + public GameRunLevel OldRunLevel { get; } + public GameRunLevel NewRunLevel { get; } } } diff --git a/Content.Server/Preferences/PreferencesDatabase.cs b/Content.Server/Preferences/PreferencesDatabase.cs index e806fa27f9..06452e27b2 100644 --- a/Content.Server/Preferences/PreferencesDatabase.cs +++ b/Content.Server/Preferences/PreferencesDatabase.cs @@ -29,27 +29,28 @@ namespace Content.Server.Preferences var profiles = new ICharacterProfile[_maxCharacterSlots]; foreach (var profile in prefs.HumanoidProfiles) - profiles[profile.Slot] = new HumanoidCharacterProfile - { - Name = profile.CharacterName, - Age = profile.Age, - Sex = profile.Sex == "Male" ? Male : Female, - CharacterAppearance = new HumanoidCharacterAppearance - { - HairStyleName = profile.HairName, - HairColor = Color.FromHex(profile.HairColor), - FacialHairStyleName = profile.FacialHairName, - FacialHairColor = Color.FromHex(profile.FacialHairColor), - EyeColor = Color.FromHex(profile.EyeColor), - SkinColor = Color.FromHex(profile.SkinColor) - } - }; + { + profiles[profile.Slot] = new HumanoidCharacterProfile( + profile.CharacterName, + profile.Age, + profile.Sex == "Male" ? Male : Female, + new HumanoidCharacterAppearance + ( + profile.HairName, + Color.FromHex(profile.HairColor), + profile.FacialHairName, + Color.FromHex(profile.FacialHairColor), + Color.FromHex(profile.EyeColor), + Color.FromHex(profile.SkinColor) + ) + ); + } return new PlayerPreferences - { - SelectedCharacterIndex = prefs.SelectedCharacterSlot, - Characters = profiles.ToList() - }; + ( + profiles, + prefs.SelectedCharacterSlot + ); } public void SaveSelectedCharacterIndex(string username, int index) @@ -71,10 +72,10 @@ namespace Content.Server.Preferences if (!(profile is HumanoidCharacterProfile humanoid)) // TODO: Handle other ICharacterProfile implementations properly throw new NotImplementedException(); - var appearance = (HumanoidCharacterAppearance) humanoid.CharacterAppearance; _prefsDb.SaveCharacterSlot(username, new HumanoidProfile { + SlotName = humanoid.Name, CharacterName = humanoid.Name, Age = humanoid.Age, Sex = humanoid.Sex.ToString(), diff --git a/Content.Shared/GameObjects/Components/Mobs/SharedHairComponent.cs b/Content.Shared/GameObjects/Components/Mobs/SharedHairComponent.cs index 2769ddb41f..e69de29bb2 100644 --- a/Content.Shared/GameObjects/Components/Mobs/SharedHairComponent.cs +++ b/Content.Shared/GameObjects/Components/Mobs/SharedHairComponent.cs @@ -1,109 +0,0 @@ -using System; -using Content.Shared.Preferences.Appearance; -using Robust.Shared.GameObjects; -using Robust.Shared.Maths; -using Robust.Shared.Serialization; -using Robust.Shared.ViewVariables; - -namespace Content.Shared.GameObjects.Components.Mobs -{ - public abstract class SharedHairComponent : Component - { - private static readonly Color DefaultHairColor = Color.FromHex("#232323"); - - private string _facialHairStyleName = HairStyles.DefaultFacialHairStyle; - private string _hairStyleName = HairStyles.DefaultHairStyle; - private Color _hairColor = DefaultHairColor; - private Color _facialHairColor = DefaultHairColor; - - public sealed override string Name => "Hair"; - public sealed override uint? NetID => ContentNetIDs.HAIR; - public sealed override Type StateType => typeof(HairComponentState); - - [ViewVariables(VVAccess.ReadWrite)] - public virtual string HairStyleName - { - get => _hairStyleName; - set - { - _hairStyleName = value; - Dirty(); - } - } - - [ViewVariables(VVAccess.ReadWrite)] - public virtual string FacialHairStyleName - { - get => _facialHairStyleName; - set - { - _facialHairStyleName = value; - Dirty(); - } - } - - [ViewVariables(VVAccess.ReadWrite)] - public virtual Color HairColor - { - get => _hairColor; - set - { - _hairColor = value; - Dirty(); - } - } - - [ViewVariables(VVAccess.ReadWrite)] - public virtual Color FacialHairColor - { - get => _facialHairColor; - set - { - _facialHairColor = value; - Dirty(); - } - } - - public override ComponentState GetComponentState() - { - return new HairComponentState(HairStyleName, FacialHairStyleName, HairColor, FacialHairColor); - } - - public override void HandleComponentState(ComponentState curState, ComponentState nextState) - { - var cast = (HairComponentState) curState; - - HairStyleName = cast.HairStyleName; - FacialHairStyleName = cast.FacialHairStyleName; - HairColor = cast.HairColor; - FacialHairColor = cast.FacialHairColor; - } - - public override void ExposeData(ObjectSerializer serializer) - { - base.ExposeData(serializer); - - serializer.DataField(ref _hairColor, "hairColor", DefaultHairColor); - serializer.DataField(ref _facialHairColor, "facialHairColor", DefaultHairColor); - serializer.DataField(ref _hairStyleName, "hairStyle", HairStyles.DefaultHairStyle); - serializer.DataField(ref _facialHairStyleName, "facialHairStyle", HairStyles.DefaultFacialHairStyle); - } - - [Serializable, NetSerializable] - private sealed class HairComponentState : ComponentState - { - public string HairStyleName { get; } - public string FacialHairStyleName { get; } - public Color HairColor { get; } - public Color FacialHairColor { get; } - - public HairComponentState(string hairStyleName, string facialHairStyleName, Color hairColor, Color facialHairColor) : base(ContentNetIDs.HAIR) - { - HairStyleName = hairStyleName; - FacialHairStyleName = facialHairStyleName; - HairColor = hairColor; - FacialHairColor = facialHairColor; - } - } - } -} diff --git a/Content.Shared/GameObjects/Components/Mobs/SharedHumanoidAppearanceComponent.cs b/Content.Shared/GameObjects/Components/Mobs/SharedHumanoidAppearanceComponent.cs new file mode 100644 index 0000000000..cc3871790e --- /dev/null +++ b/Content.Shared/GameObjects/Components/Mobs/SharedHumanoidAppearanceComponent.cs @@ -0,0 +1,73 @@ +using System; +using Content.Shared.Preferences; +using Robust.Shared.GameObjects; +using Robust.Shared.Serialization; +using Robust.Shared.ViewVariables; + +namespace Content.Shared.GameObjects.Components.Mobs +{ + public abstract class SharedHumanoidAppearanceComponent : Component + { + private HumanoidCharacterAppearance _appearance; + private Sex _sex; + + public sealed override string Name => "HumanoidAppearance"; + public sealed override uint? NetID => ContentNetIDs.HUMANOID_APPEARANCE; + public sealed override Type StateType => typeof(HumanoidAppearanceComponentState); + + [ViewVariables(VVAccess.ReadWrite)] + public virtual HumanoidCharacterAppearance Appearance + { + get => _appearance; + set + { + _appearance = value; + Dirty(); + } + } + + [ViewVariables(VVAccess.ReadWrite)] + public virtual Sex Sex + { + get => _sex; + set + { + _sex = value; + Dirty(); + } + } + + public override ComponentState GetComponentState() + { + return new HumanoidAppearanceComponentState(Appearance, Sex); + } + + public override void HandleComponentState(ComponentState curState, ComponentState nextState) + { + var cast = (HumanoidAppearanceComponentState) curState; + Appearance = cast.Appearance; + Sex = cast.Sex; + } + + public void UpdateFromProfile(ICharacterProfile profile) + { + var humanoid = (HumanoidCharacterProfile) profile; + Appearance = (HumanoidCharacterAppearance) humanoid.CharacterAppearance; + Sex = humanoid.Sex; + } + + [Serializable] + [NetSerializable] + private sealed class HumanoidAppearanceComponentState : ComponentState + { + public HumanoidAppearanceComponentState(HumanoidCharacterAppearance appearance, Sex sex) : base(ContentNetIDs.HUMANOID_APPEARANCE) + { + Appearance = appearance; + Sex = sex; + } + + public HumanoidCharacterAppearance Appearance { get; } + public Sex Sex { get; } + } + } +} diff --git a/Content.Shared/GameObjects/ContentNetIDs.cs b/Content.Shared/GameObjects/ContentNetIDs.cs index 599c7a3125..c6482cdf62 100644 --- a/Content.Shared/GameObjects/ContentNetIDs.cs +++ b/Content.Shared/GameObjects/ContentNetIDs.cs @@ -34,7 +34,7 @@ public const uint ITEMCOOLDOWN = 1029; public const uint CARGO_ORDER_DATABASE = 1030; public const uint GALACTIC_MARKET = 1031; - public const uint HAIR = 1032; + public const uint HUMANOID_APPEARANCE = 1032; public const uint INSTRUMENTS = 1033; public const uint WELDER = 1034; public const uint STACK = 1035; diff --git a/Content.Shared/Preferences/Appearance/HumanoidCharacterAppearance.cs b/Content.Shared/Preferences/Appearance/HumanoidCharacterAppearance.cs index f7defbecc2..31ede8b7c1 100644 --- a/Content.Shared/Preferences/Appearance/HumanoidCharacterAppearance.cs +++ b/Content.Shared/Preferences/Appearance/HumanoidCharacterAppearance.cs @@ -1,11 +1,9 @@ -using System; -using Robust.Shared.Serialization; - namespace Content.Shared.Preferences.Appearance { public enum HumanoidVisualLayers { Hair, FacialHair, + Body } } diff --git a/Content.Shared/Preferences/HumanoidCharacterAppearance.cs b/Content.Shared/Preferences/HumanoidCharacterAppearance.cs index cf7a7b69df..a2139a91c5 100644 --- a/Content.Shared/Preferences/HumanoidCharacterAppearance.cs +++ b/Content.Shared/Preferences/HumanoidCharacterAppearance.cs @@ -7,24 +7,69 @@ namespace Content.Shared.Preferences [Serializable, NetSerializable] public class HumanoidCharacterAppearance : ICharacterAppearance { - public string HairStyleName { get; set; } - public Color HairColor { get; set; } - public string FacialHairStyleName { get; set; } - public Color FacialHairColor { get; set; } - public Color EyeColor { get; set; } - public Color SkinColor { get; set; } + public HumanoidCharacterAppearance(string hairStyleName, + Color hairColor, + string facialHairStyleName, + Color facialHairColor, + Color eyeColor, + Color skinColor) + { + HairStyleName = hairStyleName; + HairColor = hairColor; + FacialHairStyleName = facialHairStyleName; + FacialHairColor = facialHairColor; + EyeColor = eyeColor; + SkinColor = skinColor; + } + + public string HairStyleName { get; } + public Color HairColor { get; } + public string FacialHairStyleName { get; } + public Color FacialHairColor { get; } + public Color EyeColor { get; } + public Color SkinColor { get; } + + public HumanoidCharacterAppearance WithHairStyleName(string newName) + { + return new HumanoidCharacterAppearance(newName, HairColor, FacialHairStyleName, FacialHairColor, EyeColor, SkinColor); + } + + public HumanoidCharacterAppearance WithHairColor(Color newColor) + { + return new HumanoidCharacterAppearance(HairStyleName, newColor, FacialHairStyleName, FacialHairColor, EyeColor, SkinColor); + } + + public HumanoidCharacterAppearance WithFacialHairStyleName(string newName) + { + return new HumanoidCharacterAppearance(HairStyleName, HairColor, newName, FacialHairColor, EyeColor, SkinColor); + } + + public HumanoidCharacterAppearance WithFacialHairColor(Color newColor) + { + return new HumanoidCharacterAppearance(HairStyleName, HairColor, FacialHairStyleName, newColor, EyeColor, SkinColor); + } + + public HumanoidCharacterAppearance WithEyeColor(Color newColor) + { + return new HumanoidCharacterAppearance(HairStyleName, HairColor, FacialHairStyleName, FacialHairColor, newColor, SkinColor); + } + + public HumanoidCharacterAppearance WithSkinColor(Color newColor) + { + return new HumanoidCharacterAppearance(HairStyleName, HairColor, FacialHairStyleName, FacialHairColor, EyeColor, newColor); + } public static HumanoidCharacterAppearance Default() { return new HumanoidCharacterAppearance - { - HairStyleName = "Bald", - HairColor = Color.Black, - FacialHairStyleName = "Shaved", - FacialHairColor = Color.Black, - EyeColor = Color.Black, - SkinColor = Color.Black - }; + ( + "Bald", + Color.Black, + "Shaved", + Color.Black, + Color.Black, + Color.Black + ); } public bool MemberwiseEquals(ICharacterAppearance maybeOther) diff --git a/Content.Shared/Preferences/HumanoidCharacterProfile.cs b/Content.Shared/Preferences/HumanoidCharacterProfile.cs index 8dd87dfe91..dc97c2c914 100644 --- a/Content.Shared/Preferences/HumanoidCharacterProfile.cs +++ b/Content.Shared/Preferences/HumanoidCharacterProfile.cs @@ -6,21 +6,49 @@ namespace Content.Shared.Preferences [Serializable, NetSerializable] public class HumanoidCharacterProfile : ICharacterProfile { - public static HumanoidCharacterProfile Default() + public HumanoidCharacterProfile(string name, + int age, + Sex sex, + HumanoidCharacterAppearance appearance) { - return new HumanoidCharacterProfile - { - Name = "John Doe", - Age = 18, - Sex = Sex.Male, - CharacterAppearance = HumanoidCharacterAppearance.Default() - }; + Name = name; + Age = age; + Sex = sex; + Appearance = appearance; } - public string Name { get; set; } - public int Age { get; set; } - public Sex Sex { get; set; } - public ICharacterAppearance CharacterAppearance { get; set; } + public static HumanoidCharacterProfile Default() + { + return new HumanoidCharacterProfile("John Doe", 18, Sex.Male, HumanoidCharacterAppearance.Default()); + } + + public string Name { get; } + public int Age { get; } + public Sex Sex { get; } + public ICharacterAppearance CharacterAppearance => Appearance; + public HumanoidCharacterAppearance Appearance { get; } + + public HumanoidCharacterProfile WithName(string name) + { + return new HumanoidCharacterProfile(name, Age, Sex, Appearance); + } + + public HumanoidCharacterProfile WithAge(int age) + { + return new HumanoidCharacterProfile(Name, age, Sex, Appearance); + } + + public HumanoidCharacterProfile WithSex(Sex sex) + { + return new HumanoidCharacterProfile(Name, Age, sex, Appearance); + } + + public HumanoidCharacterProfile WithCharacterAppearance(HumanoidCharacterAppearance appearance) + { + return new HumanoidCharacterProfile(Name, Age, Sex, appearance); + } + + public string Summary => $"{Name}, {Age} years old {Sex.ToString().ToLower()} human.\nOccupation: to be implemented."; public bool MemberwiseEquals(ICharacterProfile maybeOther) { @@ -28,9 +56,7 @@ namespace Content.Shared.Preferences if (Name != other.Name) return false; if (Age != other.Age) return false; if (Sex != other.Sex) return false; - if (CharacterAppearance is null) - return other.CharacterAppearance is null; - return CharacterAppearance.MemberwiseEquals(other.CharacterAppearance); + return Appearance.MemberwiseEquals(other.Appearance); } } } diff --git a/Content.Shared/Preferences/ICharacterProfile.cs b/Content.Shared/Preferences/ICharacterProfile.cs index abd73ec7a0..ab0b7c49b6 100644 --- a/Content.Shared/Preferences/ICharacterProfile.cs +++ b/Content.Shared/Preferences/ICharacterProfile.cs @@ -2,6 +2,8 @@ namespace Content.Shared.Preferences { public interface ICharacterProfile { + string Name { get; } + ICharacterAppearance CharacterAppearance { get; } bool MemberwiseEquals(ICharacterProfile other); } } diff --git a/Content.Shared/Preferences/PlayerPreferences.cs b/Content.Shared/Preferences/PlayerPreferences.cs index 6472da8c6d..25a0adb811 100644 --- a/Content.Shared/Preferences/PlayerPreferences.cs +++ b/Content.Shared/Preferences/PlayerPreferences.cs @@ -1,54 +1,55 @@ using System; using System.Collections.Generic; using System.Linq; -using Robust.Shared.Interfaces.Serialization; using Robust.Shared.Serialization; namespace Content.Shared.Preferences { /// - /// Contains all player characters and the index of the currently selected character. - /// Serialized both over the network and to disk. + /// Contains all player characters and the index of the currently selected character. + /// Serialized both over the network and to disk. /// - [Serializable, NetSerializable] - public class PlayerPreferences + [Serializable] + [NetSerializable] + public sealed class PlayerPreferences { + private List _characters; + + public PlayerPreferences(IEnumerable characters, int selectedCharacterIndex) + { + _characters = characters.ToList(); + SelectedCharacterIndex = selectedCharacterIndex; + } + + /// + /// All player characters. + /// + public IEnumerable Characters => _characters.AsEnumerable(); + + /// + /// Index of the currently selected character. + /// + public int SelectedCharacterIndex { get; } + + /// + /// The currently selected character. + /// + public ICharacterProfile SelectedCharacter => Characters.ElementAtOrDefault(SelectedCharacterIndex); + + public int FirstEmptySlot => IndexOfCharacter(null); + public static PlayerPreferences Default() { - return new PlayerPreferences - { - Characters = new List + return new PlayerPreferences(new List { HumanoidCharacterProfile.Default() }, - SelectedCharacterIndex = 0 - }; + 0); } - private List _characters; - private int _selectedCharacterIndex; - - /// - /// All player characters. - /// - public List Characters + public int IndexOfCharacter(ICharacterProfile profile) { - get => _characters; - set => _characters = value; + return _characters.FindIndex(x => x == profile); } - - /// - /// Index of the currently selected character. - /// - public int SelectedCharacterIndex - { - get => _selectedCharacterIndex; - set => _selectedCharacterIndex = value; - } - - /// - /// Retrieves the currently selected character. - /// - public ICharacterProfile SelectedCharacter => Characters.ElementAtOrDefault(SelectedCharacterIndex); } } diff --git a/Content.Tests/Server/Preferences/PreferencesDatabaseTests.cs b/Content.Tests/Server/Preferences/PreferencesDatabaseTests.cs index 66b82005bd..13bdb16c54 100644 --- a/Content.Tests/Server/Preferences/PreferencesDatabaseTests.cs +++ b/Content.Tests/Server/Preferences/PreferencesDatabaseTests.cs @@ -1,4 +1,5 @@ using System.IO; +using System.Linq; using Content.Server.Preferences; using Content.Shared.Preferences; using NUnit.Framework; @@ -12,23 +13,21 @@ namespace Content.Tests.Server.Preferences { private const int MaxCharacterSlots = 10; - private static ICharacterProfile CharlieCharlieson() + private static HumanoidCharacterProfile CharlieCharlieson() { - return new HumanoidCharacterProfile - { - Name = "Charlie Charlieson", - Age = 21, - Sex = Sex.Male, - CharacterAppearance = new HumanoidCharacterAppearance() - { - HairStyleName = "Afro", - HairColor = Color.Aqua, - FacialHairStyleName = "Shaved", - FacialHairColor = Color.Aquamarine, - EyeColor = Color.Azure, - SkinColor = Color.Beige - } - }; + return new HumanoidCharacterProfile( + "Charlie Charlieson", + 21, + Sex.Male, + new HumanoidCharacterAppearance( + "Afro", + Color.Aqua, + "Shaved", + Color.Aquamarine, + Color.Azure, + Color.Beige + ) + ); } private static PreferencesDatabase GetDb() @@ -52,7 +51,7 @@ namespace Content.Tests.Server.Preferences var prefs = db.GetPlayerPreferences(username); Assert.NotNull(prefs); Assert.Zero(prefs.SelectedCharacterIndex); - Assert.That(prefs.Characters.TrueForAll(character => character is null)); + Assert.That(prefs.Characters.ToList().TrueForAll(character => character is null)); } [Test] @@ -65,7 +64,7 @@ namespace Content.Tests.Server.Preferences db.SaveSelectedCharacterIndex(username, slot); db.SaveCharacterSlot(username, originalProfile, slot); var prefs = db.GetPlayerPreferences(username); - Assert.That(prefs.Characters[slot].MemberwiseEquals(originalProfile)); + Assert.That(prefs.Characters.ElementAt(slot).MemberwiseEquals(originalProfile)); } [Test] @@ -78,7 +77,7 @@ namespace Content.Tests.Server.Preferences db.SaveCharacterSlot(username, CharlieCharlieson(), slot); db.SaveCharacterSlot(username, null, slot); var prefs = db.GetPlayerPreferences(username); - Assert.That(prefs.Characters.TrueForAll(character => character is null)); + Assert.That(prefs.Characters.ToList().TrueForAll(character => character is null)); } [Test] diff --git a/Resources/Prototypes/Entities/mobs/human.yml b/Resources/Prototypes/Entities/mobs/human.yml index 70b8dbbcd7..78a55e8c66 100644 --- a/Resources/Prototypes/Entities/mobs/human.yml +++ b/Resources/Prototypes/Entities/mobs/human.yml @@ -22,7 +22,8 @@ drawdepth: Mobs layers: - - sprite: Mob/human.rsi + - map: ["enum.HumanoidVisualLayers.Body"] + sprite: Mob/human.rsi state: male - map: ["enum.Slots.INNERCLOTHING"] - map: ["enum.Slots.IDCARD"] @@ -82,4 +83,71 @@ - type: Examiner - type: CharacterInfo - type: FootstepSound - - type: Hair + - type: HumanoidAppearance + +- type: entity + save: false + name: Urist McHands + id: HumanMob_Dummy + description: A dummy human meant to be used in character setup + components: + - type: Hands + hands: + - left + - right + # Organs + - type: Stomach + + - type: Inventory + - type: Sprite + netsync: false + drawdepth: Mobs + + layers: + - map: ["enum.HumanoidVisualLayers.Body"] + sprite: Mob/human.rsi + state: male + - map: ["enum.Slots.INNERCLOTHING"] + - map: ["enum.Slots.IDCARD"] + - map: ["enum.Slots.GLOVES"] + - map: ["enum.Slots.SHOES"] + - map: ["enum.Slots.EARS"] + - map: ["enum.Slots.OUTERCLOTHING"] + - map: ["enum.Slots.EYES"] + - map: ["enum.Slots.BELT"] + - map: ["enum.Slots.BACKPACK"] + - map: ["enum.HumanoidVisualLayers.FacialHair"] + state: shaved + sprite: Mob/human_facial_hair.rsi + - map: ["enum.HumanoidVisualLayers.Hair"] + state: bald + sprite: Mob/human_hair.rsi + - map: ["enum.Slots.MASK"] + - map: ["enum.Slots.HEAD"] + - map: ["hand-left"] + - map: ["hand-right"] + + - type: Icon + sprite: Mob/human.rsi + state: male + + - type: Physics + mass: 85 + + - type: Collidable + shapes: + - !type:PhysShapeAabb + bounds: "-0.35,-0.35,0.35,0.35" + mask: 30 + layer: 4 + + - type: Species + Template: Human + HeatResistance: 323 + - type: Damageable + + - type: Appearance + visuals: + - type: SpeciesVisualizer2D + + - type: HumanoidAppearance diff --git a/Resources/Textures/Mob/human.rsi/fat_husk.png b/Resources/Textures/Mob/human.rsi/fat_husk.png new file mode 100644 index 0000000000000000000000000000000000000000..24eec54b4075b751abd121091a5dd2dbbc601ba6 GIT binary patch literal 1905 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 literal 0 HcmV?d00001 diff --git a/Resources/Textures/Mob/human.rsi/female_fat.png b/Resources/Textures/Mob/human.rsi/female_fat.png new file mode 100644 index 0000000000000000000000000000000000000000..19b3235c02f73b9d9eacefd8f277b1d00de67477 GIT binary patch literal 1718 zcmV;n21)seP)d@3gIK$2h_;}Ky~A`v?9_(eP`2O-?$;Qt#90rR)h_#1n}37-}wO0J$Q?i{hPqw zb6DBGiSEH$sXr}9?>{*le{}tZu-XF02ftzdvt<;oIM{cd0RZ+Fp8*%M0G4rl@XD6o z3cXGnPA-f7(Ex5CAGG(625@p&^g3-@VUZ@Xc3v_dWxBIK^4_ z6&4>nO;rzwdZvFg2tK-gQ?Lg7`r9x4^70qMfBwTMIxl}teexn&wJm?4T!CB2qkl9& zuhV8*BIAcdC0AWtlO*pJ^6;9C;5rS9Ao4GiE1`aI`P=YMy^)Bkt82rCoHOn`ISgL| zfiRK~gvzf9UcI*EpLm00@&s)=6(gZ|^}Sd^n8vRvLk@qW5>)NI*KA1A)+wR>(LmY? zar^_7U*r+5quNdRCtHD$o%QN_SSVNE7E6J2-9kS20043h++qm} zvh`FJyO5sd28exX~hBI zB~PGsaYdT&Pu~I?BNd zg^9##HbRpnTdZsMnvIaJz=2N7t9xFj1EQ{V&FtNYw7meB9M z36Xz%;UcQ5YiQN%j>}Q}T9YS^L+}es`6IW$@!%Af^1}lLxt(sY6shE>&n@J!P_CfY zY2#8pi{ruR^cfrS2m3nbz-u<(7E433E4174NB%3S>#3V~zxyV#o$H#;nmGTCm-iNgR;+)PV@v90?+8#=~DZg<*gvh38 zR!&gDlpmT?ARuddTYh5!kYY*dK3hb+S8cQ9Hx__f$cO#`S{3SjJ!5)vq{VM^7FgQe z&3Mg*eE87}kesHZ10uvvJW>1rtoe-uK#mCz?VZs+96BD!hRBJZc%t~-Lf)ExssMtQ z362JlhQ5UZNK!(e_lW*KFgC%#w{k8p<)5enLP?NO2uyT)gHu?s6!g(h)d$pf zHvP)-b?G!1Cr8iW7wZH1-8ZQ3@c6Ck19X!Ifb~0f**SV!esS`^&;(bOue12A1rR_8 zI#RvB)%nZp*O}t#{AH;oA#VP`S^%Lu{K+ATP8Khm$0)B{!|O-)Q(FbEAKmxME7$PC zd5ofyl_pNL1GZqTfa>bn&;gkn`{YF@3$NM0;=?DY>4C+EPcmM!F`PVME+0gpIj~lO zOWV7dRt-ZJ^=u#s;nSDDvIb#;>Xn?tiL*fvis!i9EGr-uCjde;X8CjAhhI3aG=}%* z2^KziE4Arj1*WhTK&!Up)1kyw`rM7%sg}$Ihci{_jmBbf_)%e{1A@2!oygSO+sR>S zC3BM|Pv4waguN0}CM^X?M}0{PArQPW`A~qCk&NZ}0ZQIJ$j+pQ_V0gaIpWqL*b6{I zq$WtyK&4uhGR4~PJVOX9D*zyrw@1oCpm(c@kRFYNxdM0s7!i^~L{5kpzDhbmBsw}` z+CIs!C~AAoo{Pyqszj)(TJ+QvYZ$+wwReH^Dsx5?BJD zqA4jauha-7RU@#py_;zPzM2FTK{Wv!VX#YA)B&kxFX;o3>RLzf+w!soDJDw^aqISN zxOIAGrtttEYjHEzsa3E+ECGlENTV)52X*_!6+-M403emR z03Fm?g(Se%0pdP%M}tbLE)aP<#c0HQZIE9Dn;+>XQl4^>PJADPZ&6951J M07*qoM6N<$g2-V$)Bpeg literal 0 HcmV?d00001 diff --git a/Resources/Textures/Mob/human.rsi/female_slim.png b/Resources/Textures/Mob/human.rsi/female_slim.png new file mode 100644 index 0000000000000000000000000000000000000000..14450f6e23d4b6579a6f2d63a9c6b8bce8aafb93 GIT binary patch literal 1492 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 JPDHLkV1iQEc8UN1 literal 0 HcmV?d00001 diff --git a/Resources/Textures/Mob/human.rsi/male.png b/Resources/Textures/Mob/human.rsi/male.png index b501089e4a5f3d4f985b741f065a9da9af613729..5d021234c8089b63e8dfea639e1103cdce0ccdef 100644 GIT binary patch delta 1683 zcmV;E25kAM4WkW^B!6m2L_t(|ob6jpXd6cq{#0x#q-{myrNRn{0)@&@5RqhpW606U zE#N@#AtxUSIVZUopMx$vv0;1I zozc!}E$z%|WoZ0?vG-@@oA-9+&3p5nfm58~i$+YeVWYZek$=BSOEuwsM4>D%Q2xrj zC&K(=B8?i=MXOku?_LAeD3zcrFIaQeZ+5Q%OH>J8@iy=Z4*z%NXxncwy?zZiieq~H8no@V%=4(g^luv7AHU}T!L;K(^54%f;O{@|--jP@puNJ>?MIQ?0k@se_d4{)?|D!N4fyphKUw+mW&6t? ze#F+x=aCEVMx(lDW%C7OvS*>UcG23}5Z;Nn8WBTRoPWK-Xg)KMf?8h#5bc&o_=)fs zj{}DXR0$&dfnRP^7p>y#75hSxdtY#jky>AiL-4vKd_4StW`M6@k!0HddF(=I zsTNa}l_1>)hMWKJ=m_all6js^C2@FkB*Z^h0e_z9lujjkjiDs>3~Ff0pie(NoC5rT zW)<6~n~_;e~sl`uXwCd5Bb0a79k0N8l>CjcOQHb;fc zQ6rs7qP?quY;~%#LY*QJ@GHPI1sP2TiGLG@1R^Zq7Zn~5JQO`q_$fMs^7&_g$LIkb zqZl!cTW+GuP|OB+BEz7O;xIsL0K-308}L6vU5#ks31YNm#QZH~meMY#upg5mC!_*{rYXo?~t$?09 zylEgOC-78$fc*3&yuN=cvRUx@{(mhiKYa;Lg2}W>IJHQK}89+{g5bqW#XnKlM=&; zgATI!f^b4s_&_8xk#dfnyT{M1ccy4}fuX)Lk^S~<&sbW5{c&sTxKuT7UVlCej>{N} z%mOYOK#6@KvQd@F`4vh4=q5xt#~{27>U*7$9fS3SX@tr`i7CJ|(AwIdN}wsS5kl9R zRQT4`hVaJ_IuvCFLTdI#IU7vFWrU9(TM&rN0GAD*1&brh3lJ`Mv7{)!@DlJF&`9Cs z9K{kSE!AQq9agN&ckO@+tABTIM79%n`zGt3>2yEm+BKmQi#c1VP$ydlGIVFh^iUHKdFQZ*s| dA=L!@2XixF13`R1&I|wm002ovPDHLkV1gebHE#d_ delta 1689 zcmV;K24?xA4XF)~B!6&8L_t(|ob6jrXcJi&|7B^}kYov_VS^QoAZ)rU1e1u=g2%?2 zP(ga|)Wd>j^%8oPT=vh!gCK0qvS`6Q2)QjnT?8{2=&lih9ZRDl(_q-CR9l!m%)FO* zlW8;Wn@kF8e-M&4Gw=Q0`{upxec$`O07p2&7mWzDVxzibvVY&jN{!qO74_vs&R@QL zpUgi&snn=0nT7H~zYLh;6heJ@(VV|>tzQOAst~@Ct>7L!02Im#!i)$L_2or#zI;iz zHebFZ%m^7s3Ej44Aob zFS4UrgZ%e%1o-C<>wEVjcGMS`xp6O4J>b+c+E$N$yzcu2slzXS`O(akE?94V_aj=* zpN2koCmPiyb2^ttX8HuQ&L%poCV3~4s>BpsVeX$B6T7wTvO^RQvALi-M&R-JOB!GW`B_0Yitn)6>48p-Y#AKT&HyKY-`5P7 z&gFeO8};Qyb2^vzMeY{^{9h-4PllxvJ{eY-$Ujs7+Ex#VSQPovEC5^cx|AQ&2LO;S z%_0$t3Wib;xcMh1Civh}$v79ncKprM1HcjrGf3ET ztbblm4Dj1aknu4+#Fm3B|A7K1R%($~$}_xDo(ZQesbsvr#tdCpi{;z*d7YrfkSG6s z#+ZFwmVc-_JkW@3eJS-76y`2MS$%>+c|lM*qqG9uZ8^wj~f z-+@uP-$yvY7X-4RavzWciP0Y>KgkrXFE5(;(kwp@0D{HnDg4g-08MS9zD(nnHiWze z4*>I5u90K(vi#1$1J=PzzBEhXr(1%K*gVS@IF&j^Zq6@GrH%B@H}6L9TCunoqE=*@#||YStptmeTBK2hb@aT`6NI3=HAtP1f%4Ws;rl)( zc6yl3<;ekA@_|TZDrp}*2g}fqMX^$gynC1zmRV*hi4L(nT9%kS-7?uT{C`8q&!M8z zY6{$dwJa%~sFVP>SLor9jVo2oD^UVKKOidE2l6(kZS}@>40a$4Jx~;iCICZ6r`6;_ z;32XxLQ~s(@SRqZ{4z*~qC!Vl&fYj@gP}W=@cw=C1KJF5=m4IuIL5dD<8mfTQu)bC zz^g-Jg`0C6OQ2Y(MOZkjP=8+N>j4KI-MSjuPT+2vt$(&FP>i8K&XCu3bbfA*+~hEL zwe1ef%7U%~>Ojl>hEYP2d;Hy|m0IL;r!7o-p?^;jIEoDhs+~w< z<7pj_mGPUxkCiqyp4J1~AmsA^ZL5cj{rmW_#DUL-0nT|S&JbK?=piFWUygJ@pxXBO zJ9|14stN4Iqgz+av!^pcAE*eHBxUG0d7*3<>UebPs(JaR-$UIF+2A>EKsTH$0QbI| j{)V2aM&ch)PQZW7x)NcOuPBwm00000NkvXXu0mjfDiAmU diff --git a/Resources/Textures/Mob/human.rsi/male_fat.png b/Resources/Textures/Mob/human.rsi/male_fat.png new file mode 100644 index 0000000000000000000000000000000000000000..507369263f76aebd108da48f4939288147349778 GIT binary patch literal 1694 zcmV;P24VS$P)&d8vN5=Q`z`@jc+qQV5lUU3dH0^YK04 zQibr1=?m)Q0l+EmNGl>qR1S9C?eY_;ZM*zLS`j8N62PCo{Nw^a`{EKCCr^QY7O-*h z6zz*k>3LF+`XBbjAKmkaFxmpw7r$fa+jSI{Eu2{I0RShf?}7O=fOTA7d@|)XLZ{V) zl}V%9AHdFKd3(1%fR#z3(`p*`X|w`XU%pG-wnvlv>+cls$H&q2%MEVZpRoG!U7~tG z)HB`wfPZw)qk=Tx(;vTgi|gNw{`oIAX#MtU;*%FqKiqTk#S-jX7Tx{;omP`H`;japTbwoVCk`vYk!#PJVQevwDK z4{DFfA8rLaJKLxngDD*bJD24ju`ORLp;0;Z*=Q03e!#!{#jE(-gg>$wv-@HhD%WLfwP; zVhN2})#ocvpwsdypH2q^-Rq#;0`>XHc7Kmf)Y{rp*aNtvbHznk1PPDSf=$mQv~&^wwdxr z7J!}0`u+j7D%9U~BGX4lQv8w50&Dw6sYb2pJN&4`%xRi-K!ou#o*;ez#{7{4z#J1G z*gIqUkmC4651SX#N_`#&G!k@Eu;%1CQ^9w+4^M$aSjq!Pey+4V6q7r1mh#)YHo2dC?-!;$0 z2-F?!a0g?M6_lEdjgzDQn*5~tfXczHTUviCod)CN=qdbSeL%N;j>-Xz-?%M1cv;ssW#}gK_ERPd%6LA$TNdT;9N-}|Bc^sx!Be1rAl&S-+u0Wb|d*+uS z407p;Iv~~lPy0ZmT9hDuQ(n>_#bhZ%+`4@eZk^tlX*>Y%o1LWU0(8(7NK!!$ih4-0 zE-^%Nz?`Cpl-joLWsQr03=cupo3bgFb$A(fVdBBZ&1^!3(!GVs~{Fl oQ}qv70HQZIE9Dnds}P6(U#3b4#D1!o$N&HU07*qoM6N<$g6EhZNB{r; literal 0 HcmV?d00001 diff --git a/Resources/Textures/Mob/human.rsi/male_slim.png b/Resources/Textures/Mob/human.rsi/male_slim.png new file mode 100644 index 0000000000000000000000000000000000000000..db0d08b4836c4b79eedc401f86ee36df6d314066 GIT binary patch literal 1413 zcmV;01$z34P)BSZGOAl9W(K#_I~^Cgrto`C$Av-UCuslxbU$Z+L(9LW0H@>fj|@noH>(w|u)2os=L~W)9;`+k z#jWkcBw`5(gvJko8QwH^RQ|=i3owc{OA&(3PI=(Y~}#~ zsxf>pFhrBqgb;uB4xIN#(7#;*`fW0Q8Hf8_3=nUI(!Dc7^yO89^@jUJE&87P*BbXPapNfBAkJ*4{1(56VLPLSDdX)X{a%acaNuXC6Se|H*H==dc=ek?zDW zcyXuW^6%UZ^xH#kzyuT#R|#PVF~X$bZM%%txPm_!xNrB>sR4Bs}1@MCHk`eGjFxBctXkWZBVN&zs#6AUt#%VQt5 z1eg($2~DH$GmP#7DEyI@z_`w=MjeIKHGaM9x3UoFQ3k5<}ZRf&0`@n=f=C_l8ePD*J04AG+xy3DRKCW$jKwM+E zmh}N~j^WzX2gE&wSoQ)d%`*9TngT=3^7@3YjVUUG1z@OI-}eSmV$J)tiJS_M14xB%KA^C=#%DgQY)}w<45~r$MB*Bt8n9|RV0AjDXC2`#;=<`!2fC&pzg+aU z>V)jHbV}^xpMBXbQsovGz(~XuELTe!7q|)0ps)Z~48XE7vTjHQjN3jiF9v53AtZpH zi^(q+eZn+Pg;WDpqaMlv;X2be){FNRQ3XY{k-Yjz4s#kdc`