From e9be97dee0a327edee22b1f2e5f7bea76f63e244 Mon Sep 17 00:00:00 2001 From: SabreML <57483089+SabreML@users.noreply.github.com> Date: Thu, 22 Jan 2026 17:33:21 +0000 Subject: [PATCH] Colour picker, palettes, & other spraypainter stuff (#41943) * The stuff * Valid check * Spraypaintable decals don't actually seem to use `ZIndex` * Don't need this * datafield fix and button swap --------- Co-authored-by: Janet Blackquill --- .../UI/SprayPainterBoundUserInterface.cs | 7 +++ .../SprayPainter/UI/SprayPainterDecals.xaml | 23 ++++++++-- .../UI/SprayPainterDecals.xaml.cs | 46 +++++++++++++++++-- .../UI/SprayPainterWindow.xaml.cs | 9 +++- .../SprayPainter/SprayPainterSystem.cs | 43 ++++++++++++++--- .../Components/SprayPainterComponent.cs | 6 +++ .../SprayPainter/SharedSprayPainterSystem.cs | 11 +++++ .../SprayPainter/SprayPainterEvents.cs | 6 +++ .../en-US/spray-painter/spray-painter.ftl | 2 + 9 files changed, 139 insertions(+), 14 deletions(-) diff --git a/Content.Client/SprayPainter/UI/SprayPainterBoundUserInterface.cs b/Content.Client/SprayPainter/UI/SprayPainterBoundUserInterface.cs index 701ec80bac..4eb3a43d25 100644 --- a/Content.Client/SprayPainter/UI/SprayPainterBoundUserInterface.cs +++ b/Content.Client/SprayPainter/UI/SprayPainterBoundUserInterface.cs @@ -30,6 +30,7 @@ public sealed class SprayPainterBoundUserInterface(EntityUid owner, Enum uiKey) _window.OnDecalColorChanged += OnDecalColorChanged; _window.OnDecalAngleChanged += OnDecalAngleChanged; _window.OnDecalSnapChanged += OnDecalSnapChanged; + _window.OnDecalColorPickerToggled += OnDecalColorPickerToggled; } var sprayPainter = EntMan.System(); @@ -56,6 +57,7 @@ public sealed class SprayPainterBoundUserInterface(EntityUid owner, Enum uiKey) _window.SetDecalAngle(sprayPainter.SelectedDecalAngle); _window.SetDecalColor(sprayPainter.SelectedDecalColor); _window.SetDecalSnap(sprayPainter.SnapDecals); + _window.SetDecalColorPicker(sprayPainter.ColorPickerEnabled); } private void OnDecalSnapChanged(bool snap) @@ -93,4 +95,9 @@ public sealed class SprayPainterBoundUserInterface(EntityUid owner, Enum uiKey) var key = _window?.IndexToColorKey(args.ItemIndex); SendPredictedMessage(new SprayPainterSetPipeColorMessage(key)); } + + private void OnDecalColorPickerToggled(bool toggle) + { + SendPredictedMessage(new SprayPainterSetDecalColorPickerMessage(toggle)); + } } diff --git a/Content.Client/SprayPainter/UI/SprayPainterDecals.xaml b/Content.Client/SprayPainter/UI/SprayPainterDecals.xaml index 0d5c8e4f16..ef4379e6cb 100644 --- a/Content.Client/SprayPainter/UI/SprayPainterDecals.xaml +++ b/Content.Client/SprayPainter/UI/SprayPainterDecals.xaml @@ -2,15 +2,26 @@ xmlns="https://spacestation14.io" xmlns:controls="clr-namespace:Content.Client.SprayPainter.UI"> - diff --git a/Content.Client/SprayPainter/UI/SprayPainterDecals.xaml.cs b/Content.Client/SprayPainter/UI/SprayPainterDecals.xaml.cs index 64d1f78d3c..66530eba91 100644 --- a/Content.Client/SprayPainter/UI/SprayPainterDecals.xaml.cs +++ b/Content.Client/SprayPainter/UI/SprayPainterDecals.xaml.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using Content.Client.Decals.UI; using Content.Client.Stylesheets; using Content.Shared.Decals; using Robust.Client.AutoGenerated; @@ -8,6 +8,8 @@ using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; using Robust.Shared.Prototypes; +using System.Linq; +using System.Numerics; namespace Content.Client.SprayPainter.UI; @@ -21,6 +23,9 @@ public sealed partial class SprayPainterDecals : Control public Action? OnColorChanged; public Action? OnAngleChanged; public Action? OnSnapChanged; + public Action? OnColorPickerToggled; + + private PaletteColorPicker? _palette; private SpriteSystem? _sprite; private string _selectedDecal = string.Empty; @@ -30,14 +35,17 @@ public sealed partial class SprayPainterDecals : Control { RobustXamlLoader.Load(this); - AddAngleButton.OnButtonUp += _ => AngleSpinBox.Value += 90; - SubAngleButton.OnButtonUp += _ => AngleSpinBox.Value -= 90; + AddAngleButton.OnButtonUp += _ => AngleSpinBox.Value = (AngleSpinBox.Value + 90) % 360; + SubAngleButton.OnButtonUp += _ => AngleSpinBox.Value = (AngleSpinBox.Value - 90) % 360; SetZeroAngleButton.OnButtonUp += _ => AngleSpinBox.Value = 0; AngleSpinBox.ValueChanged += args => OnAngleChanged?.Invoke(args.Value); UseCustomColorCheckBox.OnPressed += UseCustomColorCheckBoxOnOnPressed; SnapToTileCheckBox.OnPressed += SnapToTileCheckBoxOnOnPressed; ColorSelector.OnColorChanged += OnColorSelected; + + ColorPalette.OnPressed += ColorPaletteOnPressed; + ColorPicker.OnPressed += args => OnColorPickerToggled?.Invoke(args.Button.Pressed); } private void UseCustomColorCheckBoxOnOnPressed(BaseButton.ButtonEventArgs _) @@ -147,6 +155,7 @@ public sealed partial class SprayPainterDecals : Control public void SetSelectedDecal(string name) { _selectedDecal = name; + SelectedDecalName.Text = name; if (_sprite is null) return; @@ -171,4 +180,35 @@ public sealed partial class SprayPainterDecals : Control { SnapToTileCheckBox.Pressed = snap; } + + private void ColorPaletteOnPressed(BaseButton.ButtonEventArgs _) + { + // Code copied from other implementations of `PaletteColorPicker`. + if (_palette is null) + { + _palette = new PaletteColorPicker(); + _palette.OpenCenteredLeft(); + _palette.PaletteList.OnItemSelected += args => + { + var color = (args.ItemList.GetSelected().First().Metadata as Color?)!.Value; + ColorSelector.Color = color; + OnColorSelected(color); + }; + return; + } + + if (_palette.IsOpen) + { + _palette.Close(); + } + else + { + _palette.Open(); + } + } + + public void SetColorPicker(bool enabled) + { + ColorPicker.Pressed = enabled; + } } diff --git a/Content.Client/SprayPainter/UI/SprayPainterWindow.xaml.cs b/Content.Client/SprayPainter/UI/SprayPainterWindow.xaml.cs index eb1218ad67..2f72796043 100644 --- a/Content.Client/SprayPainter/UI/SprayPainterWindow.xaml.cs +++ b/Content.Client/SprayPainter/UI/SprayPainterWindow.xaml.cs @@ -30,6 +30,7 @@ public sealed partial class SprayPainterWindow : DefaultWindow public event Action? OnDecalColorChanged; public event Action? OnDecalAngleChanged; public event Action? OnDecalSnapChanged; + public event Action? OnDecalColorPickerToggled; // Pipe color data private ItemList _colorList = default!; @@ -195,6 +196,7 @@ public sealed partial class SprayPainterWindow : DefaultWindow _sprayPainterDecals.OnColorChanged += color => OnDecalColorChanged?.Invoke(color); _sprayPainterDecals.OnAngleChanged += angle => OnDecalAngleChanged?.Invoke(angle); _sprayPainterDecals.OnSnapChanged += snap => OnDecalSnapChanged?.Invoke(snap); + _sprayPainterDecals.OnColorPickerToggled += toggle => OnDecalColorPickerToggled?.Invoke(toggle); Tabs.AddChild(_sprayPainterDecals); TabContainer.SetTabTitle(_sprayPainterDecals, Loc.GetString("spray-painter-tab-category-decals")); @@ -298,7 +300,12 @@ public sealed partial class SprayPainterWindow : DefaultWindow if (_sprayPainterDecals != null) _sprayPainterDecals.SetSnap(snap); } - # endregion + + public void SetDecalColorPicker(bool colorPickerEnabled) + { + _sprayPainterDecals?.SetColorPicker(colorPickerEnabled); + } + #endregion } public record SpriteListData(string Group, string Style, EntProtoId Prototype, int SelectedIndex) : ListData; diff --git a/Content.Server/SprayPainter/SprayPainterSystem.cs b/Content.Server/SprayPainter/SprayPainterSystem.cs index f00ae1d7dd..a4c631db41 100644 --- a/Content.Server/SprayPainter/SprayPainterSystem.cs +++ b/Content.Server/SprayPainter/SprayPainterSystem.cs @@ -15,7 +15,8 @@ using Content.Shared.SprayPainter; using Content.Shared.SprayPainter.Components; using Robust.Server.Audio; using Robust.Server.GameObjects; -using Robust.Shared.Prototypes; +using System.Linq; +using System.Numerics; namespace Content.Server.SprayPainter; @@ -48,7 +49,16 @@ public sealed class SprayPainterSystem : SharedSprayPainterSystem /// private void OnFloorAfterInteract(Entity ent, ref AfterInteractEvent args) { - if (args.Handled || !args.CanReach || args.Target != null) + if (args.Handled || args.Target != null) + return; + + if (ent.Comp.ColorPickerEnabled) + { + PickColor(ent, ref args); + return; + } + + if (!args.CanReach) return; // Includes both off and all other don't cares @@ -83,7 +93,7 @@ public sealed class SprayPainterSystem : SharedSprayPainterSystem return; } - var decals = _decals.GetDecalsInRange(grid, position.Position, validDelegate: IsDecalRemovable); + var decals = _decals.GetDecalsInRange(grid, position.Position, validDelegate: IsDecalValid); if (decals.Count <= 0) { _popup.PopupEntity(Loc.GetString("spray-painter-interact-nothing-to-remove"), args.User, args.User); @@ -104,10 +114,9 @@ public sealed class SprayPainterSystem : SharedSprayPainterSystem } /// - /// Handles drawing decals when a spray painter is used to interact with the floor. - /// Spray painter must have decal painting enabled and enough charges of paint to paint on the floor. + /// Returns whether is valid to interact with when a spray painter is used to interact with the floor. /// - private bool IsDecalRemovable(Decal decal) + private bool IsDecalValid(Decal decal) { if (!Proto.TryIndex(decal.Id, out var decalProto)) return false; @@ -189,4 +198,26 @@ public sealed class SprayPainterSystem : SharedSprayPainterSystem args.Handled = DoAfter.TryStartDoAfter(doAfterEventArgs); } + + private void PickColor(Entity ent, ref AfterInteractEvent args) + { + if (!args.ClickLocation.IsValid(EntityManager) || _transform.GetGrid(args.ClickLocation) is not { } grid) + return; + + var clickPos = args.ClickLocation.Position; + var decals = _decals.GetDecalsInRange(grid, clickPos, validDelegate: IsDecalValid); + if (decals.Count == 0) + { + _popup.PopupEntity(Loc.GetString("spray-painter-interact-no-color-pick"), args.User, args.User); + return; + } + + var closestDecal = decals.MinBy(d => Vector2.Distance(d.Decal.Coordinates, clickPos)).Decal; + + _popup.PopupEntity(Loc.GetString("spray-painter-interact-color-picked", ("id", closestDecal.Id)), args.User, args.User); + + ent.Comp.SelectedDecalColor = closestDecal.Color; + ent.Comp.ColorPickerEnabled = false; + Dirty(ent); + } } diff --git a/Content.Shared/SprayPainter/Components/SprayPainterComponent.cs b/Content.Shared/SprayPainter/Components/SprayPainterComponent.cs index b9a7057347..6b3e36bef2 100644 --- a/Content.Shared/SprayPainter/Components/SprayPainterComponent.cs +++ b/Content.Shared/SprayPainter/Components/SprayPainterComponent.cs @@ -105,6 +105,12 @@ public sealed partial class SprayPainterComponent : Component /// [DataField] public SoundSpecifier SoundSwitchDecalMode = new SoundPathSpecifier("/Audio/Machines/quickbeep.ogg", AudioParams.Default.WithVolume(1.5f)); + + /// + /// Whether the decal color picker is currently active. + /// + [DataField, AutoNetworkedField] + public bool ColorPickerEnabled = false; } /// diff --git a/Content.Shared/SprayPainter/SharedSprayPainterSystem.cs b/Content.Shared/SprayPainter/SharedSprayPainterSystem.cs index d1f19d0c25..a3eb26a892 100644 --- a/Content.Shared/SprayPainter/SharedSprayPainterSystem.cs +++ b/Content.Shared/SprayPainter/SharedSprayPainterSystem.cs @@ -53,6 +53,7 @@ public abstract class SharedSprayPainterSystem : EntitySystem subs.Event(OnSetDecalColor); subs.Event(OnSetDecalAngle); subs.Event(OnSetDecalSnap); + subs.Event(OnSetDecalColorPicker); }); } @@ -300,6 +301,16 @@ public abstract class SharedSprayPainterSystem : EntitySystem UpdateUi(ent); } + /// + /// Enables or disables the decal colour picker. + /// + private void OnSetDecalColorPicker(Entity ent, ref SprayPainterSetDecalColorPickerMessage args) + { + ent.Comp.ColorPickerEnabled = args.Toggle; + Dirty(ent); + UpdateUi(ent); + } + /// /// Sets the decal to paint on the ground. /// diff --git a/Content.Shared/SprayPainter/SprayPainterEvents.cs b/Content.Shared/SprayPainter/SprayPainterEvents.cs index db9de9c278..806cfd2888 100644 --- a/Content.Shared/SprayPainter/SprayPainterEvents.cs +++ b/Content.Shared/SprayPainter/SprayPainterEvents.cs @@ -56,6 +56,12 @@ public sealed class SprayPainterSetPipeColorMessage(string? key) : BoundUserInte public readonly string? Key = key; } +[Serializable, NetSerializable] +public sealed class SprayPainterSetDecalColorPickerMessage(bool toggle) : BoundUserInterfaceMessage +{ + public bool Toggle = toggle; +} + [Serializable, NetSerializable] public sealed partial class SprayPainterDoAfterEvent : DoAfterEvent { diff --git a/Resources/Locale/en-US/spray-painter/spray-painter.ftl b/Resources/Locale/en-US/spray-painter/spray-painter.ftl index dc54c5c8b8..18c8c36f90 100644 --- a/Resources/Locale/en-US/spray-painter/spray-painter.ftl +++ b/Resources/Locale/en-US/spray-painter/spray-painter.ftl @@ -5,6 +5,8 @@ spray-painter-ammo-after-interact-refilled = You refill the spray painter. spray-painter-interact-no-charges = Not enough paint left. spray-painter-interact-nothing-to-remove = Nothing to remove! +spray-painter-interact-no-color-pick = Can't find a color to pick! +spray-painter-interact-color-picked = Picked color from '{$id}'. spray-painter-on-examined-painted-message = It seems to have been freshly painted. spray-painter-style-not-available = Cannot apply the selected style to this object. -- 2.52.0