diff --git a/Content.Client/CharacterAppearance/MagicMirrorBoundUserInterface.cs b/Content.Client/CharacterAppearance/MagicMirrorBoundUserInterface.cs index 924f4093cb..e69de29bb2 100644 --- a/Content.Client/CharacterAppearance/MagicMirrorBoundUserInterface.cs +++ b/Content.Client/CharacterAppearance/MagicMirrorBoundUserInterface.cs @@ -1,269 +0,0 @@ -using System; -using System.Linq; -using Content.Client.Stylesheets; -using Content.Shared.CharacterAppearance; -using JetBrains.Annotations; -using Robust.Client.GameObjects; -using Robust.Client.Graphics; -using Robust.Client.UserInterface; -using Robust.Client.UserInterface.Controls; -using Robust.Client.UserInterface.CustomControls; -using Robust.Client.Utility; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Localization; -using Robust.Shared.Maths; -using static Content.Shared.CharacterAppearance.Components.SharedMagicMirrorComponent; -using static Robust.Client.UserInterface.Controls.BoxContainer; - -namespace Content.Client.CharacterAppearance -{ - [UsedImplicitly] - public sealed class MagicMirrorBoundUserInterface : BoundUserInterface - { - private MagicMirrorWindow? _window; - - public MagicMirrorBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey) - { - } - - protected override void Open() - { - base.Open(); - - _window = new MagicMirrorWindow(this); - _window.OnClose += Close; - _window.Open(); - } - - protected override void ReceiveMessage(BoundUserInterfaceMessage message) - { - switch (message) - { - case MagicMirrorInitialDataMessage initialData: - _window?.SetInitialData(initialData); - break; - } - } - - internal void HairSelected(string name, bool isFacialHair) - { - SendMessage(new HairSelectedMessage(name, isFacialHair)); - } - - internal void HairColorSelected(Color color, bool isFacialHair) - { - SendMessage(new HairColorSelectedMessage((color.RByte, color.GByte, color.BByte), - isFacialHair)); - } - - internal void EyeColorSelected(Color color) - { - SendMessage(new EyeColorSelectedMessage((color.RByte, color.GByte, color.BByte))); - } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - - if (disposing) - { - _window?.Dispose(); - } - } - } - - public sealed class HairStylePicker : Control - { - [Dependency] private readonly SpriteAccessoryManager _spriteAccessoryManager = default!; - - public event Action? OnHairColorPicked; - public event Action? OnHairStylePicked; - - private readonly ItemList _items; - - private readonly Control _colorContainer; - private readonly ColorSelectorSliders _colorSelectors; - private Color _lastColor; - private SpriteAccessoryCategories _categories; - - public void SetData(Color color, string styleId, SpriteAccessoryCategories categories, bool canColor) - { - if (_categories != categories) - { - _categories = categories; - Populate(); - } - - _colorContainer.Visible = canColor; - _lastColor = color; - - _colorSelectors.Color = color; - - foreach (var item in _items) - { - var prototype = (SpriteAccessoryPrototype) item.Metadata!; - item.Selected = prototype.ID == styleId; - } - - UpdateStylePickerColor(); - } - - private void UpdateStylePickerColor() - { - foreach (var item in _items) - { - item.IconModulate = _lastColor; - } - } - - public HairStylePicker() - { - IoCManager.InjectDependencies(this); - - var vBox = new BoxContainer - { - Orientation = LayoutOrientation.Vertical - }; - AddChild(vBox); - - _colorContainer = new BoxContainer - { - Orientation = LayoutOrientation.Vertical - }; - vBox.AddChild(_colorContainer); - _colorContainer.AddChild(_colorSelectors = new ()); - _colorSelectors.OnColorChanged += color => ColorValueChanged(color); - - _items = new ItemList - { - VerticalExpand = true, - MinSize = (300, 250) - }; - vBox.AddChild(_items); - _items.OnItemSelected += ItemSelected; - } - - private void ColorValueChanged(Color newColor) - { - OnHairColorPicked?.Invoke(newColor); - _lastColor = newColor; - UpdateStylePickerColor(); - } - - public void Populate() - { - var styles = _spriteAccessoryManager - .AccessoriesForCategory(_categories) - .ToList(); - styles.Sort(HairStyles.SpriteAccessoryComparer); - - foreach (var style in styles) - { - var item = _items.AddItem(style.Name, style.Sprite.Frame0()); - item.Metadata = style; - } - } - - private void ItemSelected(ItemList.ItemListSelectedEventArgs args) - { - var prototype = (SpriteAccessoryPrototype?) _items[args.ItemIndex].Metadata; - var style = prototype?.ID; - - if (style != null) - { - OnHairStylePicked?.Invoke(style); - } - } - - // ColorSlider - } - - public sealed class EyeColorPicker : Control - { - public event Action? OnEyeColorPicked; - - private readonly ColorSelectorSliders _colorSelectors; - - private Color _lastColor; - - public void SetData(Color color) - { - _lastColor = color; - - _colorSelectors.Color = color; - } - - public EyeColorPicker() - { - var vBox = new BoxContainer - { - Orientation = LayoutOrientation.Vertical - }; - AddChild(vBox); - - vBox.AddChild(_colorSelectors = new ColorSelectorSliders()); - - _colorSelectors.OnColorChanged += ColorValueChanged; - } - - private void ColorValueChanged(Color newColor) - { - OnEyeColorPicked?.Invoke(newColor); - - _lastColor = newColor; - } - - // ColorSlider - } - - public sealed class MagicMirrorWindow : DefaultWindow - { - private readonly HairStylePicker _hairStylePicker; - private readonly HairStylePicker _facialHairStylePicker; - private readonly EyeColorPicker _eyeColorPicker; - - public MagicMirrorWindow(MagicMirrorBoundUserInterface owner) - { - SetSize = MinSize = (500, 360); - Title = Loc.GetString("magic-mirror-window-title"); - - _hairStylePicker = new HairStylePicker {HorizontalExpand = true}; - _hairStylePicker.OnHairStylePicked += newStyle => owner.HairSelected(newStyle, false); - _hairStylePicker.OnHairColorPicked += newColor => owner.HairColorSelected(newColor, false); - - _facialHairStylePicker = new HairStylePicker {HorizontalExpand = true}; - _facialHairStylePicker.OnHairStylePicked += newStyle => owner.HairSelected(newStyle, true); - _facialHairStylePicker.OnHairColorPicked += newColor => owner.HairColorSelected(newColor, true); - - _eyeColorPicker = new EyeColorPicker { HorizontalExpand = true }; - _eyeColorPicker.OnEyeColorPicked += newColor => owner.EyeColorSelected(newColor); - - Contents.AddChild(new BoxContainer - { - Orientation = LayoutOrientation.Horizontal, - SeparationOverride = 8, - Children = {_hairStylePicker, _facialHairStylePicker, _eyeColorPicker} - }); - } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - - if (disposing) - { - _hairStylePicker.Dispose(); - _facialHairStylePicker.Dispose(); - _eyeColorPicker.Dispose(); - } - } - - public void SetInitialData(MagicMirrorInitialDataMessage initialData) - { - _facialHairStylePicker.SetData(initialData.FacialHairColor, initialData.FacialHairId, initialData.CategoriesFacialHair, initialData.CanColorFacialHair); - _hairStylePicker.SetData(initialData.HairColor, initialData.HairId, initialData.CategoriesHair, initialData.CanColorHair); - _eyeColorPicker.SetData(initialData.EyeColor); - } - } -} diff --git a/Content.Client/CharacterAppearance/Systems/HumanoidAppearanceSystem.cs b/Content.Client/CharacterAppearance/Systems/HumanoidAppearanceSystem.cs deleted file mode 100644 index 328d4f0e62..0000000000 --- a/Content.Client/CharacterAppearance/Systems/HumanoidAppearanceSystem.cs +++ /dev/null @@ -1,165 +0,0 @@ -using Content.Client.Cuffs.Components; -using Content.Shared.Body.Components; -using Content.Shared.CharacterAppearance; -using Content.Shared.CharacterAppearance.Components; -using Content.Shared.CharacterAppearance.Systems; -using Robust.Client.GameObjects; -using Robust.Shared.Prototypes; - -namespace Content.Client.CharacterAppearance.Systems -{ - public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem - { - [Dependency] private readonly SpriteAccessoryManager _accessoryManager = default!; - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(UpdateLooks); - SubscribeLocalEvent(BodyPartAdded); - SubscribeLocalEvent(BodyPartRemoved); - } - - public readonly static HumanoidVisualLayers[] BodyPartLayers = { - HumanoidVisualLayers.Chest, - HumanoidVisualLayers.Head, - HumanoidVisualLayers.Snout, - HumanoidVisualLayers.HeadTop, - HumanoidVisualLayers.HeadSide, - HumanoidVisualLayers.Tail, - HumanoidVisualLayers.Eyes, - HumanoidVisualLayers.RArm, - HumanoidVisualLayers.LArm, - HumanoidVisualLayers.RHand, - HumanoidVisualLayers.LHand, - HumanoidVisualLayers.RLeg, - HumanoidVisualLayers.LLeg, - HumanoidVisualLayers.RFoot, - HumanoidVisualLayers.LFoot - }; - - private void UpdateLooks(EntityUid uid, HumanoidAppearanceComponent component, - ChangedHumanoidAppearanceEvent args) - { - var spriteQuery = EntityManager.GetEntityQuery(); - - if (!spriteQuery.TryGetComponent(uid, out var sprite)) - return; - - if (EntityManager.TryGetComponent(uid, out SharedBodyComponent? body)) - { - foreach (var (part, _) in body.Parts) - { - if (spriteQuery.TryGetComponent(part.Owner, out var partSprite)) - { - partSprite.Color = component.Appearance.SkinColor; - } - } - } - - // Like body parts some stuff may not have hair. - if (sprite.LayerMapTryGet(HumanoidVisualLayers.Hair, out var hairLayer)) - { - var hairColor = component.CanColorHair ? component.Appearance.HairColor : Color.White; - hairColor = component.HairMatchesSkin ? component.Appearance.SkinColor : hairColor; - sprite.LayerSetColor(hairLayer, hairColor.WithAlpha(component.HairAlpha)); - - var hairStyle = component.Appearance.HairStyleId; - if (string.IsNullOrWhiteSpace(hairStyle) || - !_accessoryManager.IsValidAccessoryInCategory(hairStyle, component.CategoriesHair)) - { - hairStyle = HairStyles.DefaultHairStyle; - } - - var hairPrototype = _prototypeManager.Index(hairStyle); - sprite.LayerSetSprite(hairLayer, hairPrototype.Sprite); - } - - if (sprite.LayerMapTryGet(HumanoidVisualLayers.FacialHair, out var facialLayer)) - { - var facialHairColor = component.CanColorHair ? component.Appearance.FacialHairColor : Color.White; - facialHairColor = component.HairMatchesSkin ? component.Appearance.SkinColor : facialHairColor; - sprite.LayerSetColor(facialLayer, facialHairColor.WithAlpha(component.HairAlpha)); - - var facialHairStyle = component.Appearance.FacialHairStyleId; - if (string.IsNullOrWhiteSpace(facialHairStyle) || - !_accessoryManager.IsValidAccessoryInCategory(facialHairStyle, component.CategoriesFacialHair)) - { - facialHairStyle = HairStyles.DefaultFacialHairStyle; - } - - var facialHairPrototype = _prototypeManager.Index(facialHairStyle); - sprite.LayerSetSprite(facialLayer, facialHairPrototype.Sprite); - } - - foreach (var layer in BodyPartLayers) - { - // Not every mob may have the furry layers hence we just skip it. - if (!sprite.LayerMapTryGet(layer, out var actualLayer)) continue; - if (!sprite[actualLayer].Visible) continue; - - sprite.LayerSetColor(actualLayer, component.Appearance.SkinColor); - } - - sprite.LayerSetColor(HumanoidVisualLayers.Eyes, component.Appearance.EyeColor); - sprite.LayerSetState(HumanoidVisualLayers.Chest, component.Sex == Sex.Male ? "torso_m" : "torso_f"); - sprite.LayerSetState(HumanoidVisualLayers.Head, component.Sex == Sex.Male ? "head_m" : "head_f"); - - if (sprite.LayerMapTryGet(HumanoidVisualLayers.StencilMask, out _)) - sprite.LayerSetVisible(HumanoidVisualLayers.StencilMask, component.Sex == Sex.Female); - - if (EntityManager.TryGetComponent(uid, out var cuffed)) - { - sprite.LayerSetVisible(HumanoidVisualLayers.Handcuffs, !cuffed.CanStillInteract); - } - else - { - sprite.LayerSetVisible(HumanoidVisualLayers.Handcuffs, false); - } - } - - // Scaffolding until Body is moved to ECS. - private void BodyPartAdded(HumanoidAppearanceBodyPartAddedEvent args) - { - if (!EntityManager.TryGetComponent(args.Uid, out SpriteComponent? sprite)) - { - return; - } - - if (!EntityManager.HasComponent(args.Args.Part.Owner)) - { - return; - } - - var layers = args.Args.Part.ToHumanoidLayers(); - // TODO BODY Layer color, sprite and state - foreach (var layer in layers) - { - if (!sprite.LayerMapTryGet(layer, out _)) - continue; - - sprite.LayerSetVisible(layer, true); - } - } - - private void BodyPartRemoved(HumanoidAppearanceBodyPartRemovedEvent args) - { - if (!EntityManager.TryGetComponent(args.Uid, out SpriteComponent? sprite)) - { - return; - } - - if (!EntityManager.HasComponent(args.Args.Part.Owner)) - { - return; - } - - var layers = args.Args.Part.ToHumanoidLayers(); - // TODO BODY Layer color, sprite and state - foreach (var layer in layers) - sprite.LayerSetVisible(layer, false); - } - } -} diff --git a/Content.Client/Clothing/ClothingVisualsSystem.cs b/Content.Client/Clothing/ClothingVisualsSystem.cs index 967a96d34d..aecf94eabd 100644 --- a/Content.Client/Clothing/ClothingVisualsSystem.cs +++ b/Content.Client/Clothing/ClothingVisualsSystem.cs @@ -2,9 +2,9 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Client.Inventory; -using Content.Shared.CharacterAppearance; using Content.Shared.Clothing; using Content.Shared.Clothing.Components; +using Content.Shared.Humanoid; using Content.Shared.Inventory; using Content.Shared.Inventory.Events; using Content.Shared.Item; @@ -150,17 +150,6 @@ public sealed class ClothingVisualsSystem : EntitySystem private void OnGotUnequipped(EntityUid uid, ClothingComponent component, GotUnequippedEvent args) { - if (component.InSlot == "head" - && _tagSystem.HasTag(uid, "HidesHair") - && TryComp(args.Equipee, out SpriteComponent? sprite)) - { - if (sprite.LayerMapTryGet(HumanoidVisualLayers.FacialHair, out var facial)) - sprite[facial].Visible = true; - - if (sprite.LayerMapTryGet(HumanoidVisualLayers.Hair, out var hair)) - sprite[hair].Visible = true; - } - component.InSlot = null; } @@ -198,17 +187,6 @@ public sealed class ClothingVisualsSystem : EntitySystem { component.InSlot = args.Slot; - if (args.Slot == "head" - && _tagSystem.HasTag(uid, "HidesHair") - && TryComp(args.Equipee, out SpriteComponent? sprite)) - { - if (sprite.LayerMapTryGet(HumanoidVisualLayers.FacialHair, out var facial)) - sprite[facial].Visible = false; - - if (sprite.LayerMapTryGet(HumanoidVisualLayers.Hair, out var hair)) - sprite[hair].Visible = false; - } - RenderEquipment(args.Equipee, uid, args.Slot, clothingComponent: component); } diff --git a/Content.Client/Cuffs/Components/CuffableComponent.cs b/Content.Client/Cuffs/Components/CuffableComponent.cs index b923491377..a074c044fb 100644 --- a/Content.Client/Cuffs/Components/CuffableComponent.cs +++ b/Content.Client/Cuffs/Components/CuffableComponent.cs @@ -1,6 +1,6 @@ using Content.Shared.ActionBlocker; -using Content.Shared.CharacterAppearance; using Content.Shared.Cuffs.Components; +using Content.Shared.Humanoid; using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Shared.GameObjects; diff --git a/Content.Client/Damage/DamageVisualsSystem.cs b/Content.Client/Damage/DamageVisualsSystem.cs index adf245e4a5..9e24a90e2a 100644 --- a/Content.Client/Damage/DamageVisualsSystem.cs +++ b/Content.Client/Damage/DamageVisualsSystem.cs @@ -230,8 +230,7 @@ public sealed class DamageVisualsSystem : VisualizerSystem? OnEyeColorPicked; + + private readonly ColorSelectorSliders _colorSelectors; + + private Color _lastColor; + + public void SetData(Color color) + { + _lastColor = color; + + _colorSelectors.Color = color; + } + + public EyeColorPicker() + { + var vBox = new BoxContainer + { + Orientation = BoxContainer.LayoutOrientation.Vertical + }; + AddChild(vBox); + + vBox.AddChild(_colorSelectors = new ColorSelectorSliders()); + + _colorSelectors.OnColorChanged += ColorValueChanged; + } + + private void ColorValueChanged(Color newColor) + { + OnEyeColorPicked?.Invoke(newColor); + + _lastColor = newColor; + } +} diff --git a/Content.Client/Humanoid/HumanoidComponent.cs b/Content.Client/Humanoid/HumanoidComponent.cs new file mode 100644 index 0000000000..d433013c9d --- /dev/null +++ b/Content.Client/Humanoid/HumanoidComponent.cs @@ -0,0 +1,15 @@ +using Content.Shared.Humanoid; +using Content.Shared.Humanoid.Markings; +using Content.Shared.Humanoid.Prototypes; + +namespace Content.Client.Humanoid; + +[RegisterComponent] +public sealed class HumanoidComponent : SharedHumanoidComponent +{ + [ViewVariables] public List CurrentMarkings = new(); + + public Dictionary BaseLayers = new(); + + public string LastSpecies = default!; +} diff --git a/Content.Client/Humanoid/HumanoidMarkingModifierBoundUserInterface.cs b/Content.Client/Humanoid/HumanoidMarkingModifierBoundUserInterface.cs new file mode 100644 index 0000000000..75942ba56d --- /dev/null +++ b/Content.Client/Humanoid/HumanoidMarkingModifierBoundUserInterface.cs @@ -0,0 +1,62 @@ +using Content.Shared.Humanoid; +using Content.Shared.Humanoid.Markings; +using Robust.Client.GameObjects; + +namespace Content.Client.Humanoid; + +// Marking BUI. +// Do not use this in any non-privileged instance. This just replaces an entire marking set +// with the set sent over. + +public sealed class HumanoidMarkingModifierBoundUserInterface : BoundUserInterface +{ + public HumanoidMarkingModifierBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey) + { + } + + private HumanoidMarkingModifierWindow? _window; + + protected override void Open() + { + base.Open(); + + _window = new(); + _window.OnClose += Close; + _window.OnMarkingAdded += SendMarkingSet; + _window.OnMarkingRemoved += SendMarkingSet; + _window.OnMarkingColorChange += SendMarkingSetNoResend; + _window.OnMarkingRankChange += SendMarkingSet; + _window.OnLayerInfoModified += SendBaseLayer; + + _window.OpenCenteredLeft(); + } + + protected override void UpdateState(BoundUserInterfaceState state) + { + base.UpdateState(state); + + if (_window == null || state is not HumanoidMarkingModifierState cast) + { + return; + } + + _window.SetState(cast.MarkingSet, cast.Species, cast.SkinColor, cast.CustomBaseLayers); + } + + private void SendMarkingSet(MarkingSet set) + { + SendMessage(new HumanoidMarkingModifierMarkingSetMessage(set, true)); + } + + private void SendMarkingSetNoResend(MarkingSet set) + { + SendMessage(new HumanoidMarkingModifierMarkingSetMessage(set, false)); + } + + private void SendBaseLayer(HumanoidVisualLayers layer, CustomBaseLayerInfo? info) + { + SendMessage(new HumanoidMarkingModifierBaseLayersSetMessage(layer, info, true)); + } +} + + diff --git a/Content.Client/Humanoid/HumanoidMarkingModifierWindow.xaml b/Content.Client/Humanoid/HumanoidMarkingModifierWindow.xaml new file mode 100644 index 0000000000..d32d3ba2cf --- /dev/null +++ b/Content.Client/Humanoid/HumanoidMarkingModifierWindow.xaml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + diff --git a/Content.Client/Humanoid/HumanoidMarkingModifierWindow.xaml.cs b/Content.Client/Humanoid/HumanoidMarkingModifierWindow.xaml.cs new file mode 100644 index 0000000000..2f6c396096 --- /dev/null +++ b/Content.Client/Humanoid/HumanoidMarkingModifierWindow.xaml.cs @@ -0,0 +1,138 @@ +using Content.Shared.Humanoid; +using Content.Shared.Humanoid.Markings; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.CustomControls; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client.Humanoid; + +// hack for a panel that modifies an entity's markings on demand + +[GenerateTypedNameReferences] +public sealed partial class HumanoidMarkingModifierWindow : DefaultWindow +{ + public Action? OnMarkingAdded; + public Action? OnMarkingRemoved; + public Action? OnMarkingColorChange; + public Action? OnMarkingRankChange; + public Action? OnLayerInfoModified; + + private readonly Dictionary _modifiers = new(); + + public HumanoidMarkingModifierWindow() + { + RobustXamlLoader.Load(this); + + foreach (var layer in Enum.GetValues()) + { + var modifier = new HumanoidBaseLayerModifier(layer); + BaseLayersContainer.AddChild(modifier); + _modifiers.Add(layer, modifier); + + modifier.OnStateChanged += delegate + { + OnLayerInfoModified!( + layer, + modifier.Enabled + ? new CustomBaseLayerInfo(modifier.State, modifier.Color) + : null); + }; + } + + MarkingPickerWidget.OnMarkingAdded += set => OnMarkingAdded!(set); + MarkingPickerWidget.OnMarkingRemoved += set => OnMarkingRemoved!(set); + MarkingPickerWidget.OnMarkingColorChange += set => OnMarkingColorChange!(set); + MarkingPickerWidget.OnMarkingRankChange += set => OnMarkingRankChange!(set); + MarkingForced.OnToggled += args => MarkingPickerWidget.Forced = args.Pressed; + MarkingIgnoreSpecies.OnToggled += args => MarkingPickerWidget.Forced = args.Pressed; + + MarkingPickerWidget.Forced = MarkingForced.Pressed; + MarkingPickerWidget.IgnoreSpecies = MarkingForced.Pressed; + } + + public void SetState(MarkingSet markings, string species, Color skinColor, Dictionary info) + { + MarkingPickerWidget.SetData(markings, species, skinColor); + + foreach (var (layer, modifier) in _modifiers) + { + if (!info.TryGetValue(layer, out var layerInfo)) + { + modifier.SetState(false, string.Empty, Color.White); + continue; + } + + modifier.SetState(true, layerInfo.ID, layerInfo.Color); + } + } + + private sealed class HumanoidBaseLayerModifier : BoxContainer + { + private CheckBox _enable; + private LineEdit _lineEdit; + private ColorSelectorSliders _colorSliders; + private BoxContainer _infoBox; + + public bool Enabled => _enable.Pressed; + public string State => _lineEdit.Text; + public Color Color => _colorSliders.Color; + + public Action? OnStateChanged; + + public HumanoidBaseLayerModifier(HumanoidVisualLayers layer) + { + HorizontalExpand = true; + Orientation = LayoutOrientation.Vertical; + var labelBox = new BoxContainer + { + MinWidth = 250, + HorizontalExpand = true + }; + AddChild(labelBox); + + labelBox.AddChild(new Label + { + HorizontalExpand = true, + Text = layer.ToString() + }); + _enable = new CheckBox + { + Text = "Enable", + HorizontalAlignment = HAlignment.Right + }; + + labelBox.AddChild(_enable); + _infoBox = new BoxContainer + { + Orientation = LayoutOrientation.Vertical, + Visible = false + }; + _enable.OnToggled += args => + { + _infoBox.Visible = args.Pressed; + OnStateChanged!(); + }; + + var lineEditBox = new BoxContainer(); + lineEditBox.AddChild(new Label { Text = "Prototype id: "}); + _lineEdit = new(); + _lineEdit.OnTextEntered += args => OnStateChanged!(); + lineEditBox.AddChild(_lineEdit); + _infoBox.AddChild(lineEditBox); + + _colorSliders = new(); + _colorSliders.OnColorChanged += color => OnStateChanged!(); + _infoBox.AddChild(_colorSliders); + AddChild(_infoBox); + } + + public void SetState(bool enabled, string state, Color color) + { + _enable.Pressed = enabled; + _infoBox.Visible = enabled; + _lineEdit.Text = state; + _colorSliders.Color = color; + } + } +} diff --git a/Content.Client/Humanoid/HumanoidSystem.cs b/Content.Client/Humanoid/HumanoidSystem.cs new file mode 100644 index 0000000000..7051b8711f --- /dev/null +++ b/Content.Client/Humanoid/HumanoidSystem.cs @@ -0,0 +1,63 @@ +using System.Linq; +using Content.Shared.Humanoid; +using Content.Shared.Humanoid.Markings; +using Content.Shared.Humanoid.Prototypes; +using Content.Shared.Preferences; +using Robust.Shared.Prototypes; + +namespace Content.Client.Humanoid; + +public sealed class HumanoidSystem : SharedHumanoidSystem +{ + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly MarkingManager _markingManager = default!; + + /// + /// Loads a profile directly into a humanoid. + /// + /// The humanoid entity's UID + /// The profile to load. + /// The humanoid entity's humanoid component. + /// + /// This should not be used if the entity is owned by the server. The server will otherwise + /// override this with the appearance data it sends over. + /// + public void LoadProfile(EntityUid uid, HumanoidCharacterProfile profile, HumanoidComponent? humanoid = null) + { + if (!Resolve(uid, ref humanoid)) + { + return; + } + + humanoid.Species = profile.Species; + var customBaseLayers = new Dictionary + { + [HumanoidVisualLayers.Eyes] = new CustomBaseLayerInfo(string.Empty, profile.Appearance.EyeColor) + }; + + var speciesPrototype = _prototypeManager.Index(profile.Species); + var markings = new MarkingSet(profile.Appearance.Markings, speciesPrototype.MarkingPoints, _markingManager, + _prototypeManager); + markings.EnsureDefault(profile.Appearance.SkinColor, _markingManager); + + // legacy: remove in the future? + markings.RemoveCategory(MarkingCategories.Hair); + markings.RemoveCategory(MarkingCategories.FacialHair); + + var hair = new Marking(profile.Appearance.HairStyleId, new[] { profile.Appearance.HairColor }); + markings.AddBack(MarkingCategories.Hair, hair); + + var facialHair = new Marking(profile.Appearance.FacialHairStyleId, + new[] { profile.Appearance.FacialHairColor }); + markings.AddBack(MarkingCategories.FacialHair, facialHair); + + markings.FilterSpecies(profile.Species, _markingManager, _prototypeManager); + + SetAppearance(uid, + profile.Species, + customBaseLayers, + profile.Appearance.SkinColor, + new(), // doesn't exist yet + markings.GetForwardEnumerator().ToList()); + } +} diff --git a/Content.Client/Humanoid/HumanoidVisualizerSystem.cs b/Content.Client/Humanoid/HumanoidVisualizerSystem.cs new file mode 100644 index 0000000000..f6152d3a06 --- /dev/null +++ b/Content.Client/Humanoid/HumanoidVisualizerSystem.cs @@ -0,0 +1,447 @@ +using System.Linq; +using Content.Shared.Humanoid; +using Content.Shared.Humanoid.Markings; +using Content.Shared.Humanoid.Prototypes; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; + +namespace Content.Client.Humanoid; + +public sealed class HumanoidVisualizerSystem : VisualizerSystem +{ + [Dependency] private IPrototypeManager _prototypeManager = default!; + [Dependency] private MarkingManager _markingManager = default!; + + protected override void OnAppearanceChange(EntityUid uid, HumanoidComponent component, ref AppearanceChangeEvent args) + { + base.OnAppearanceChange(uid, component, ref args); + + if (args.Sprite == null) + { + return; + } + + if (!args.AppearanceData.TryGetValue(HumanoidVisualizerKey.Key, out var dataRaw) + || dataRaw is not HumanoidVisualizerData data) + { + return; + } + + if (!_prototypeManager.TryIndex(data.Species, out SpeciesPrototype? speciesProto) + || !_prototypeManager.TryIndex(speciesProto.SpriteSet, out HumanoidSpeciesBaseSpritesPrototype? baseSprites)) + { + return; + } + + bool dirty; + if (data.CustomBaseLayerInfo.Count != 0) + { + dirty = MergeCustomBaseSprites(uid, baseSprites.Sprites, data.CustomBaseLayerInfo, component); + } + else + { + dirty = MergeCustomBaseSprites(uid, baseSprites.Sprites, null, component); + } + + if (dirty) + { + ApplyBaseSprites(uid, component, args.Sprite); + ApplySkinColor(uid, data.SkinColor, component, args.Sprite); + } + + if (data.CustomBaseLayerInfo.Count != 0) + { + foreach (var (layer, info) in data.CustomBaseLayerInfo) + { + SetBaseLayerColor(uid, layer, info.Color, args.Sprite); + } + } + + var layerVis = data.LayerVisibility.ToHashSet(); + dirty |= ReplaceHiddenLayers(uid, layerVis, component, args.Sprite); + + DiffAndApplyMarkings(uid, data.Markings, dirty, component, args.Sprite); + } + + private bool ReplaceHiddenLayers(EntityUid uid, HashSet hiddenLayers, + HumanoidComponent humanoid, SpriteComponent sprite) + { + if (hiddenLayers.SetEquals(humanoid.HiddenLayers)) + { + return false; + } + + SetSpriteVisibility(uid, hiddenLayers, false, sprite); + + humanoid.HiddenLayers.ExceptWith(hiddenLayers); + + SetSpriteVisibility(uid, humanoid.HiddenLayers, true, sprite); + + humanoid.HiddenLayers.Clear(); + humanoid.HiddenLayers.UnionWith(hiddenLayers); + + return true; + } + + private void SetSpriteVisibility(EntityUid uid, HashSet layers, bool visibility, SpriteComponent sprite) + { + foreach (var layer in layers) + { + if (!sprite.LayerMapTryGet(layer, out var index)) + { + continue; + } + + sprite[index].Visible = visibility; + } + } + + private void DiffAndApplyMarkings(EntityUid uid, + List newMarkings, + bool layersDirty, + HumanoidComponent humanoid, + SpriteComponent sprite) + { + // skip this entire thing if both sets are empty + if (humanoid.CurrentMarkings.Count == 0 && newMarkings.Count == 0) + { + return; + } + + var dirtyMarkings = new List(); + var dirtyRangeStart = humanoid.CurrentMarkings.Count == 0 ? 0 : -1; + + // edge cases: + // humanoid.CurrentMarkings < newMarkings.Count + // - check if count matches this condition before diffing + // - if count is unequal, set dirty range to start from humanoid.CurrentMarkings.Count + // humanoid.CurrentMarkings > newMarkings.Count, no dirty markings + // - break count upon meeting this condition + // - clear markings from newMarkings.Count to humanoid.CurrentMarkings.Count - newMarkings.Count + + for (var i = 0; i < humanoid.CurrentMarkings.Count; i++) + { + // if we've reached the end of the new set of markings, + // then that means it's time to finish + if (newMarkings.Count == i) + { + break; + } + + // if the marking is different here, set the range start to i and break, we need + // to rebuild all markings starting from i + if (humanoid.CurrentMarkings[i].MarkingId != newMarkings[i].MarkingId) + { + dirtyRangeStart = i; + break; + } + + // otherwise, we add the current marking to dirtyMarkings if it has different + // settings + // however: if the hidden layers are set to dirty, then we need to + // instead just add every single marking, since we don't know ahead of time + // where these markings go + if (humanoid.CurrentMarkings[i] != newMarkings[i] || layersDirty) + { + dirtyMarkings.Add(i); + } + } + + foreach (var i in dirtyMarkings) + { + if (!_markingManager.TryGetMarking(newMarkings[i], out var dirtyMarking)) + { + continue; + } + + ApplyMarking(uid, dirtyMarking, newMarkings[i].MarkingColors, newMarkings[i].Visible, humanoid, sprite); + } + + if (humanoid.CurrentMarkings.Count < newMarkings.Count && dirtyRangeStart < 0) + { + dirtyRangeStart = humanoid.CurrentMarkings.Count; + } + + if (dirtyRangeStart >= 0) + { + var range = newMarkings.GetRange(dirtyRangeStart, newMarkings.Count - dirtyRangeStart); + + if (humanoid.CurrentMarkings.Count > 0) + { + var oldRange = humanoid.CurrentMarkings.GetRange(dirtyRangeStart, humanoid.CurrentMarkings.Count - dirtyRangeStart); + ClearMarkings(uid, oldRange, humanoid, sprite); + } + + ApplyMarkings(uid, range, humanoid, sprite); + } + else if (humanoid.CurrentMarkings.Count != newMarkings.Count) + { + if (newMarkings.Count == 0) + { + ClearAllMarkings(uid, humanoid, sprite); + } + else if (humanoid.CurrentMarkings.Count > newMarkings.Count) + { + var rangeStart = newMarkings.Count; + var rangeCount = humanoid.CurrentMarkings.Count - newMarkings.Count; + var range = humanoid.CurrentMarkings.GetRange(rangeStart, rangeCount); + + ClearMarkings(uid, range, humanoid, sprite); + } + } + + if (dirtyMarkings.Count > 0 || dirtyRangeStart >= 0 || humanoid.CurrentMarkings.Count != newMarkings.Count) + { + humanoid.CurrentMarkings = newMarkings; + } + } + + private void ClearAllMarkings(EntityUid uid, HumanoidComponent humanoid, + SpriteComponent spriteComp) + { + ClearMarkings(uid, humanoid.CurrentMarkings, humanoid, spriteComp); + } + + private void ClearMarkings(EntityUid uid, List markings, HumanoidComponent humanoid, + SpriteComponent spriteComp) + { + foreach (var marking in markings) + { + RemoveMarking(uid, marking, spriteComp); + } + } + + private void RemoveMarking(EntityUid uid, Marking marking, + SpriteComponent spriteComp) + { + if (!_markingManager.TryGetMarking(marking, out var prototype)) + { + return; + } + + foreach (var sprite in prototype.Sprites) + { + if (sprite is not SpriteSpecifier.Rsi rsi) + { + continue; + } + + var layerId = $"{marking.MarkingId}-{rsi.RsiState}"; + if (!spriteComp.LayerMapTryGet(layerId, out var index)) + { + continue; + } + + spriteComp.LayerMapRemove(layerId); + spriteComp.RemoveLayer(index); + } + } + + private void ApplyMarkings(EntityUid uid, + List markings, + HumanoidComponent humanoid, + SpriteComponent spriteComp) + { + foreach (var marking in new ReverseMarkingEnumerator(markings)) + { + if (!_markingManager.TryGetMarking(marking, out var markingPrototype)) + { + continue; + } + + ApplyMarking(uid, markingPrototype, marking.MarkingColors, marking.Visible, humanoid, spriteComp); + } + } + + private void ApplyMarking(EntityUid uid, + MarkingPrototype markingPrototype, + IReadOnlyList? colors, + bool visible, + HumanoidComponent humanoid, + SpriteComponent sprite) + { + if (!sprite.LayerMapTryGet(markingPrototype.BodyPart, out int targetLayer)) + { + return; + } + + visible &= !humanoid.HiddenLayers.Contains(markingPrototype.BodyPart); + visible &= humanoid.BaseLayers.TryGetValue(markingPrototype.BodyPart, out var setting) + && setting.AllowsMarkings; + + for (var j = 0; j < markingPrototype.Sprites.Count; j++) + { + if (markingPrototype.Sprites[j] is not SpriteSpecifier.Rsi rsi) + { + continue; + } + + var layerId = $"{markingPrototype.ID}-{rsi.RsiState}"; + + if (!sprite.LayerMapTryGet(layerId, out _)) + { + var layer = sprite.AddLayer(markingPrototype.Sprites[j], targetLayer + j + 1); + sprite.LayerMapSet(layerId, layer); + sprite.LayerSetSprite(layerId, rsi); + } + + sprite.LayerSetVisible(layerId, visible); + + if (!visible || setting == null) // this is kinda implied + { + continue; + } + + if (markingPrototype.FollowSkinColor || colors == null || setting.MarkingsMatchSkin) + { + var skinColor = humanoid.SkinColor; + skinColor.A = setting.LayerAlpha; + + sprite.LayerSetColor(layerId, skinColor); + } + else + { + sprite.LayerSetColor(layerId, colors[j]); + } + } + } + + private void ApplySkinColor(EntityUid uid, + Color skinColor, + HumanoidComponent humanoid, + SpriteComponent spriteComp) + { + humanoid.SkinColor = skinColor; + + foreach (var (layer, spriteInfo) in humanoid.BaseLayers) + { + if (!spriteInfo.MatchSkin) + { + continue; + } + + var color = skinColor; + color.A = spriteInfo.LayerAlpha; + + SetBaseLayerColor(uid, layer, color, spriteComp); + } + } + + private void SetBaseLayerColor(EntityUid uid, HumanoidVisualLayers layer, Color color, + SpriteComponent sprite) + { + if (!sprite.LayerMapTryGet(layer, out var index)) + { + return; + } + + sprite[index].Color = color; + } + + private bool MergeCustomBaseSprites(EntityUid uid, Dictionary baseSprites, + Dictionary? customBaseSprites, + HumanoidComponent humanoid) + { + var newBaseLayers = new Dictionary(); + + foreach (var (key, id) in baseSprites) + { + var sexMorph = humanoid.Sex switch + { + Sex.Male when HumanoidVisualLayersExtension.HasSexMorph(key) => $"{id}Male", + Sex.Female when HumanoidVisualLayersExtension.HasSexMorph(key) => $"{id}Female", + _ => id + }; + + if (!_prototypeManager.TryIndex(sexMorph, out HumanoidSpeciesSpriteLayer? baseLayer)) + { + continue; + } + + if (!newBaseLayers.TryAdd(key, baseLayer)) + { + newBaseLayers[key] = baseLayer; + } + } + + if (customBaseSprites == null) + { + return IsDirty(newBaseLayers); + } + + foreach (var (key, info) in customBaseSprites) + { + if (!_prototypeManager.TryIndex(info.ID, out HumanoidSpeciesSpriteLayer? baseLayer)) + { + continue; + } + + if (!newBaseLayers.TryAdd(key, baseLayer)) + { + newBaseLayers[key] = baseLayer; + } + } + + bool IsDirty(Dictionary newBaseLayers) + { + var dirty = false; + if (humanoid.BaseLayers.Count != newBaseLayers.Count) + { + dirty = true; + humanoid.BaseLayers = newBaseLayers; + return dirty; + } + + foreach (var (key, info) in humanoid.BaseLayers) + { + if (!newBaseLayers.TryGetValue(key, out var newInfo)) + { + dirty = true; + break; + } + + if (info.ID != newInfo.ID) + { + dirty = true; + break; + } + } + + if (dirty) + { + humanoid.BaseLayers = newBaseLayers; + } + + return dirty; + } + + return IsDirty(newBaseLayers); + } + + private void ApplyBaseSprites(EntityUid uid, + HumanoidComponent humanoid, + SpriteComponent spriteComp) + { + foreach (var (layer, spriteInfo) in humanoid.BaseLayers) + { + if (spriteInfo.BaseSprite != null && spriteComp.LayerMapTryGet(layer, out var index)) + { + switch (spriteInfo.BaseSprite) + { + case SpriteSpecifier.Rsi rsi: + spriteComp.LayerSetRSI(index, rsi.RsiPath); + spriteComp.LayerSetState(index, rsi.RsiState); + break; + case SpriteSpecifier.Texture texture: + spriteComp.LayerSetTexture(index, texture.TexturePath); + break; + } + } + } + } + + +} diff --git a/Content.Client/Markings/MarkingPicker.xaml b/Content.Client/Humanoid/MarkingPicker.xaml similarity index 100% rename from Content.Client/Markings/MarkingPicker.xaml rename to Content.Client/Humanoid/MarkingPicker.xaml diff --git a/Content.Client/Humanoid/MarkingPicker.xaml.cs b/Content.Client/Humanoid/MarkingPicker.xaml.cs new file mode 100644 index 0000000000..5d9178ba6a --- /dev/null +++ b/Content.Client/Humanoid/MarkingPicker.xaml.cs @@ -0,0 +1,463 @@ +using System.Linq; +using Content.Shared.Humanoid; +using Content.Shared.Humanoid.Markings; +using Content.Shared.Humanoid.Prototypes; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; +using Robust.Client.Utility; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; +using static Robust.Client.UserInterface.Controls.BoxContainer; + +namespace Content.Client.Humanoid; + +[GenerateTypedNameReferences] +public sealed partial class MarkingPicker : Control +{ + [Dependency] private readonly MarkingManager _markingManager = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + + public Action? OnMarkingAdded; + public Action? OnMarkingRemoved; + public Action? OnMarkingColorChange; + public Action? OnMarkingRankChange; + + private List _currentMarkingColors = new(); + + private ItemList.Item? _selectedMarking; + private ItemList.Item? _selectedUnusedMarking; + private MarkingCategories _selectedMarkingCategory = MarkingCategories.Chest; + + private MarkingSet _currentMarkings = new(); + + private List _markingCategories = Enum.GetValues().ToList(); + + private string _currentSpecies = SharedHumanoidSystem.DefaultSpecies; + public Color CurrentSkinColor = Color.White; + + private readonly HashSet _ignoreCategories = new(); + + public string IgnoreCategories + { + get => string.Join(',', _ignoreCategories); + set + { + _ignoreCategories.Clear(); + var split = value.Split(','); + foreach (var category in split) + { + if (!Enum.TryParse(category, out MarkingCategories categoryParse)) + { + continue; + } + + _ignoreCategories.Add(categoryParse); + } + + SetupCategoryButtons(); + } + } + + public bool Forced { get; set; } + + private bool _ignoreSpecies; + + public bool IgnoreSpecies + { + get => _ignoreSpecies; + set + { + _ignoreSpecies = value; + Populate(); + } + } + + public void SetData(List newMarkings, string species, Color skinColor) + { + var pointsProto = _prototypeManager + .Index(species).MarkingPoints; + _currentMarkings = new(newMarkings, pointsProto, _markingManager); + + if (!IgnoreSpecies) + { + _currentMarkings.FilterSpecies(species); // should be validated server-side but it can't hurt + } + + _currentSpecies = species; + CurrentSkinColor = skinColor; + + Populate(); + PopulateUsed(); + } + + public void SetData(MarkingSet set, string species, Color skinColor) + { + _currentMarkings = set; + + if (!IgnoreSpecies) + { + _currentMarkings.FilterSpecies(species); // should be validated server-side but it can't hurt + } + + _currentSpecies = species; + CurrentSkinColor = skinColor; + + Populate(); + PopulateUsed(); + } + + public void SetSkinColor(Color color) => CurrentSkinColor = color; + + public MarkingPicker() + { + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + + SetupCategoryButtons(); + CMarkingCategoryButton.OnItemSelected += OnCategoryChange; + CMarkingsUnused.OnItemSelected += item => + _selectedUnusedMarking = CMarkingsUnused[item.ItemIndex]; + + CMarkingAdd.OnPressed += args => + MarkingAdd(); + + CMarkingsUsed.OnItemSelected += OnUsedMarkingSelected; + + CMarkingRemove.OnPressed += args => + MarkingRemove(); + + CMarkingRankUp.OnPressed += _ => SwapMarkingUp(); + CMarkingRankDown.OnPressed += _ => SwapMarkingDown(); + } + + private void SetupCategoryButtons() + { + CMarkingCategoryButton.Clear(); + for (var i = 0; i < _markingCategories.Count; i++) + { + if (_ignoreCategories.Contains(_markingCategories[i])) + { + continue; + } + + CMarkingCategoryButton.AddItem(Loc.GetString($"markings-category-{_markingCategories[i].ToString()}"), i); + } + CMarkingCategoryButton.SelectId(_markingCategories.IndexOf(_selectedMarkingCategory)); + } + + private string GetMarkingName(MarkingPrototype marking) => Loc.GetString($"marking-{marking.ID}"); + + private List GetMarkingStateNames(MarkingPrototype marking) + { + List result = new(); + foreach (var markingState in marking.Sprites) + { + switch (markingState) + { + case SpriteSpecifier.Rsi rsi: + result.Add(Loc.GetString($"marking-{marking.ID}-{rsi.RsiState}")); + break; + case SpriteSpecifier.Texture texture: + result.Add(Loc.GetString($"marking-{marking.ID}-{texture.TexturePath.Filename}")); + break; + } + } + + return result; + } + + public void Populate() + { + CMarkingsUnused.Clear(); + _selectedUnusedMarking = null; + + var markings = IgnoreSpecies + ? _markingManager.MarkingsByCategory(_selectedMarkingCategory) + : _markingManager.MarkingsByCategoryAndSpecies(_selectedMarkingCategory, _currentSpecies); + + foreach (var marking in markings.Values) + { + if (_currentMarkings.TryGetCategory(_selectedMarkingCategory, out var listing) + && listing.Contains(marking.AsMarking())) + { + continue; + } + + var item = CMarkingsUnused.AddItem($"{GetMarkingName(marking)}", marking.Sprites[0].Frame0()); + item.Metadata = marking; + } + + CMarkingPoints.Visible = _currentMarkings.PointsLeft(_selectedMarkingCategory) != -1; + } + + // Populate the used marking list. Returns a list of markings that weren't + // valid to add to the marking list. + public void PopulateUsed() + { + CMarkingsUsed.Clear(); + CMarkingColors.Visible = false; + _selectedMarking = null; + + if (!IgnoreSpecies) + { + _currentMarkings.FilterSpecies(_currentSpecies, _markingManager); + } + + // walk backwards through the list for visual purposes + foreach (var marking in _currentMarkings.GetReverseEnumerator(_selectedMarkingCategory)) + { + if (!_markingManager.TryGetMarking(marking, out var newMarking)) + { + continue; + } + + var text = Loc.GetString(marking.Forced ? "marking-used-forced" : "marking-used", ("marking-name", $"{GetMarkingName(newMarking)}"), + ("marking-category", Loc.GetString($"markings-category-{newMarking.MarkingCategory}"))); + + var _item = new ItemList.Item(CMarkingsUsed) + { + Text = text, + Icon = newMarking.Sprites[0].Frame0(), + Selectable = true, + Metadata = newMarking, + IconModulate = marking.MarkingColors[0] + }; + + CMarkingsUsed.Add(_item); + } + + // since all the points have been processed, update the points visually + UpdatePoints(); + } + + private void SwapMarkingUp() + { + if (_selectedMarking == null) + { + return; + } + + var i = CMarkingsUsed.IndexOf(_selectedMarking); + if (ShiftMarkingRank(i, -1)) + { + OnMarkingRankChange?.Invoke(_currentMarkings); + } + } + + private void SwapMarkingDown() + { + if (_selectedMarking == null) + { + return; + } + + var i = CMarkingsUsed.IndexOf(_selectedMarking); + if (ShiftMarkingRank(i, 1)) + { + OnMarkingRankChange?.Invoke(_currentMarkings); + } + } + + private bool ShiftMarkingRank(int src, int places) + { + if (src + places >= CMarkingsUsed.Count || src + places < 0) + { + return false; + } + + var visualDest = src + places; // what it would visually look like + var visualTemp = CMarkingsUsed[visualDest]; + CMarkingsUsed[visualDest] = CMarkingsUsed[src]; + CMarkingsUsed[src] = visualTemp; + + switch (places) + { + // i.e., we're going down in rank + case < 0: + _currentMarkings.ShiftRankDownFromEnd(_selectedMarkingCategory, src); + break; + // i.e., we're going up in rank + case > 0: + _currentMarkings.ShiftRankUpFromEnd(_selectedMarkingCategory, src); + break; + // do nothing? + default: + break; + } + + return true; + } + + + + // repopulate in case markings are restricted, + // and also filter out any markings that are now invalid + // attempt to preserve any existing markings as well: + // it would be frustrating to otherwise have all markings + // cleared, imo + public void SetSpecies(string species) + { + _currentSpecies = species; + var markingList = _currentMarkings.GetForwardEnumerator().ToList(); + + var speciesPrototype = _prototypeManager.Index(species); + + _currentMarkings = new(markingList, speciesPrototype.MarkingPoints, _markingManager, _prototypeManager); + _currentMarkings.FilterSpecies(species); + + Populate(); + PopulateUsed(); + } + + private void UpdatePoints() + { + var count = _currentMarkings.PointsLeft(_selectedMarkingCategory); + if (count > -1) + { + CMarkingPoints.Text = Loc.GetString("marking-points-remaining", ("points", count)); + } + } + + private void OnCategoryChange(OptionButton.ItemSelectedEventArgs category) + { + CMarkingCategoryButton.SelectId(category.Id); + _selectedMarkingCategory = _markingCategories[category.Id]; + Populate(); + PopulateUsed(); + UpdatePoints(); + } + + // TODO: This should be using ColorSelectorSliders once that's merged, so + private void OnUsedMarkingSelected(ItemList.ItemListSelectedEventArgs item) + { + _selectedMarking = CMarkingsUsed[item.ItemIndex]; + var prototype = (MarkingPrototype) _selectedMarking.Metadata!; + + if (prototype.FollowSkinColor) + { + CMarkingColors.Visible = false; + + return; + } + + var stateNames = GetMarkingStateNames(prototype); + _currentMarkingColors.Clear(); + CMarkingColors.DisposeAllChildren(); + List colorSliders = new(); + for (int i = 0; i < prototype.Sprites.Count; i++) + { + var colorContainer = new BoxContainer + { + Orientation = LayoutOrientation.Vertical, + }; + + CMarkingColors.AddChild(colorContainer); + + ColorSelectorSliders colorSelector = new ColorSelectorSliders(); + colorSliders.Add(colorSelector); + + colorContainer.AddChild(new Label { Text = $"{stateNames[i]} color:" }); + colorContainer.AddChild(colorSelector); + + var listing = _currentMarkings[_selectedMarkingCategory]; + + var currentColor = new Color( + listing[listing.Count - 1 - item.ItemIndex].MarkingColors[i].RByte, + listing[listing.Count - 1 - item.ItemIndex].MarkingColors[i].GByte, + listing[listing.Count - 1 - item.ItemIndex].MarkingColors[i].BByte + ); + colorSelector.Color = currentColor; + _currentMarkingColors.Add(currentColor); + var colorIndex = _currentMarkingColors.Count - 1; + + Action colorChanged = _ => + { + _currentMarkingColors[colorIndex] = colorSelector.Color; + + ColorChanged(colorIndex); + }; + colorSelector.OnColorChanged += colorChanged; + } + + CMarkingColors.Visible = true; + } + + private void ColorChanged(int colorIndex) + { + if (_selectedMarking is null) return; + var markingPrototype = (MarkingPrototype) _selectedMarking.Metadata!; + int markingIndex = _currentMarkings.FindIndexOf(_selectedMarkingCategory, markingPrototype.ID); + + if (markingIndex < 0) return; + + _selectedMarking.IconModulate = _currentMarkingColors[colorIndex]; + + var marking = new Marking(_currentMarkings[_selectedMarkingCategory][markingIndex]); + marking.SetColor(colorIndex, _currentMarkingColors[colorIndex]); + _currentMarkings.Replace(_selectedMarkingCategory, markingIndex, marking); + + OnMarkingColorChange?.Invoke(_currentMarkings); + } + + private void MarkingAdd() + { + if (_selectedUnusedMarking is null) return; + + if (_currentMarkings.PointsLeft(_selectedMarkingCategory) == 0 && !Forced) + { + return; + } + + var marking = (MarkingPrototype) _selectedUnusedMarking.Metadata!; + + + var markingObject = marking.AsMarking(); + for (var i = 0; i < markingObject.MarkingColors.Count; i++) + { + markingObject.SetColor(i, CurrentSkinColor); + } + + markingObject.Forced = Forced; + + _currentMarkings.AddBack(_selectedMarkingCategory, markingObject); + + UpdatePoints(); + + CMarkingsUnused.Remove(_selectedUnusedMarking); + var item = new ItemList.Item(CMarkingsUsed) + { + Text = Loc.GetString("marking-used", ("marking-name", $"{GetMarkingName(marking)}"), ("marking-category", Loc.GetString($"markings-category-{marking.MarkingCategory}"))), + Icon = marking.Sprites[0].Frame0(), + Selectable = true, + Metadata = marking, + }; + CMarkingsUsed.Insert(0, item); + + _selectedUnusedMarking = null; + OnMarkingAdded?.Invoke(_currentMarkings); + } + + private void MarkingRemove() + { + if (_selectedMarking is null) return; + + var marking = (MarkingPrototype) _selectedMarking.Metadata!; + + _currentMarkings.Remove(_selectedMarkingCategory, marking.ID); + + UpdatePoints(); + + CMarkingsUsed.Remove(_selectedMarking); + + if (marking.MarkingCategory == _selectedMarkingCategory) + { + var item = CMarkingsUnused.AddItem($"{GetMarkingName(marking)}", marking.Sprites[0].Frame0()); + item.Metadata = marking; + } + _selectedMarking = null; + CMarkingColors.Visible = false; + OnMarkingRemoved?.Invoke(_currentMarkings); + } +} diff --git a/Content.Client/Humanoid/SingleMarkingPicker.xaml b/Content.Client/Humanoid/SingleMarkingPicker.xaml new file mode 100644 index 0000000000..2dfa661f58 --- /dev/null +++ b/Content.Client/Humanoid/SingleMarkingPicker.xaml @@ -0,0 +1,22 @@ + + +