From 6b0c4e8ca892bf3c30611e6b3bc3a84c391b81d0 Mon Sep 17 00:00:00 2001 From: TryHardo7 Date: Sat, 13 Jun 2026 22:41:58 +0300 Subject: [PATCH 1/7] Hello world! --- Content.Client/Decals/DecalPlacementSystem.cs | 94 +++++++++++++++++++ .../Decals/Overlays/DecalPlacementOverlay.cs | 38 ++++++++ .../Decals/UI/DecalPlacerWindow.xaml | 2 + .../Decals/UI/DecalPlacerWindow.xaml.cs | 35 +++++++ .../en-US/_Exodus/decals/decal-window.ftl | 1 + .../ru-RU/_Exodus/decals/decal-window.ftl | 1 + 6 files changed, 171 insertions(+) create mode 100644 Resources/Locale/en-US/_Exodus/decals/decal-window.ftl create mode 100644 Resources/Locale/ru-RU/_Exodus/decals/decal-window.ftl diff --git a/Content.Client/Decals/DecalPlacementSystem.cs b/Content.Client/Decals/DecalPlacementSystem.cs index ac39182ac68..dfb4a16a4c7 100644 --- a/Content.Client/Decals/DecalPlacementSystem.cs +++ b/Content.Client/Decals/DecalPlacementSystem.cs @@ -8,6 +8,7 @@ using Robust.Client.Input; using Robust.Shared.Input; using Robust.Shared.Input.Binding; +using Robust.Shared.Map; // Exodus using Robust.Shared.Prototypes; namespace Content.Client.Decals; @@ -17,6 +18,7 @@ namespace Content.Client.Decals; public sealed partial class DecalPlacementSystem : EntitySystem { [Dependency] private IInputManager _inputManager = default!; + [Dependency] private IMapManager _mapManager = default!; // Exodus [Dependency] private IOverlayManager _overlay = default!; [Dependency] private IPrototypeManager _protoMan = default!; [Dependency] private InputSystem _inputSystem = default!; @@ -35,6 +37,25 @@ public sealed partial class DecalPlacementSystem : EntitySystem private bool _placing; private bool _erasing; + // Exodus-Start: eyedropper tool that copies the color of an existing decal on the map. + private bool _eyedropper; + + /// + /// Whether the eyedropper (color picker) tool is currently selected. + /// + public bool EyedropperActive => _eyedropper; + + /// + /// Raised when the eyedropper successfully copies a color from a decal on the map. + /// + public event Action? EyedropperPicked; + + public void SetEyedropper(bool active) + { + _eyedropper = active && _active; + } + // Exodus-End + public (DecalPrototype? Decal, bool Snap, Angle Angle, Color Color) GetActiveDecal() { return _active && _decalId != null ? @@ -50,6 +71,18 @@ public override void Initialize() CommandBinds.Builder.Bind(EngineKeyFunctions.EditorPlaceObject, new PointerStateInputCmdHandler( (session, coords, uid) => { + // Exodus-Start: left click while the eyedropper is active copies a color instead of placing. + if (_eyedropper) + { + _eyedropper = false; + + if (TryPickDecalColor(coords, out var picked)) + EyedropperPicked?.Invoke(picked); + + return true; + } + // Exodus-End + if (!_active || _placing || _decalId == null) return false; @@ -85,6 +118,14 @@ public override void Initialize() .Bind(EngineKeyFunctions.EditorCancelPlace, new PointerStateInputCmdHandler( (session, coords, uid) => { + // Exodus-Start: right click cancels the eyedropper instead of erasing. + if (_eyedropper) + { + _eyedropper = false; + return true; + } + // Exodus-End + if (!_active || _erasing) return false; @@ -189,12 +230,65 @@ public void UpdateDecalInfo(string id, Color color, float rotation, bool snap, i _cleanable = cleanable; } + // Exodus: clear the active decal so it stops following the cursor when deselected. + public void ClearDecal() + { + _decalId = null; + } + public void SetActive(bool active) { _active = active; + _eyedropper = false; // Exodus: arming is always an explicit user action. if (_active) _inputManager.Contexts.SetActiveContext("editor"); else _inputSystem.SetEntityContextActive(); } + + // Exodus-Start: find the topmost decal under the given coordinates and return its color. + private bool TryPickDecalColor(EntityCoordinates coords, out Color color) + { + color = Color.White; + + var mapPos = _transform.ToMapCoordinates(coords); + if (!_mapManager.TryFindGridAt(mapPos, out var gridUid, out _)) + return false; + + if (!TryComp(gridUid, out var decalGrid)) + return false; + + var localPos = Vector2.Transform(mapPos.Position, _transform.GetInvWorldMatrix(gridUid)); + var chunkIndices = SharedDecalSystem.GetChunkIndices(localPos); + + if (!decalGrid.ChunkCollection.ChunkCollection.TryGetValue(chunkIndices, out var chunk)) + return false; + + Decal? best = null; + var bestZ = int.MinValue; + var bestId = 0u; + + foreach (var (id, decal) in chunk.Decals) + { + // Decals are drawn as a 1x1 tile with their bottom-left corner at Coordinates. + if (localPos.X < decal.Coordinates.X || localPos.X >= decal.Coordinates.X + 1f || + localPos.Y < decal.Coordinates.Y || localPos.Y >= decal.Coordinates.Y + 1f) + continue; + + // Match the overlay's draw order: highest ZIndex, then highest id, is on top. + if (best != null && (decal.ZIndex < bestZ || decal.ZIndex == bestZ && id < bestId)) + continue; + + best = decal; + bestZ = decal.ZIndex; + bestId = id; + } + + if (best == null) + return false; + + color = best.Color ?? Color.White; + return true; + } + // Exodus-End } diff --git a/Content.Client/Decals/Overlays/DecalPlacementOverlay.cs b/Content.Client/Decals/Overlays/DecalPlacementOverlay.cs index e0e73af5f79..40bd026ff91 100644 --- a/Content.Client/Decals/Overlays/DecalPlacementOverlay.cs +++ b/Content.Client/Decals/Overlays/DecalPlacementOverlay.cs @@ -30,6 +30,14 @@ public DecalPlacementOverlay(DecalPlacementSystem placement, SharedTransformSyst protected override void Draw(in OverlayDrawArgs args) { + // Exodus-Start: draw the eyedropper crosshair instead of a decal preview while picking a color. + if (_placement.EyedropperActive) + { + DrawEyedropper(args); + return; + } + // Exodus-End + var (decal, snap, rotation, color) = _placement.GetActiveDecal(); if (decal == null) @@ -67,4 +75,34 @@ protected override void Draw(in OverlayDrawArgs args) handle.DrawTextureRect(_sprite.Frame0(decal.Sprite), box, color); handle.SetTransform(Matrix3x2.Identity); } + + // Exodus-Start: crosshair shown at the cursor marking the sample point of the eyedropper tool. + private void DrawEyedropper(in OverlayDrawArgs args) + { + var mouseScreenPos = _inputManager.MouseScreenPosition; + var mousePos = _eyeManager.PixelToMap(mouseScreenPos); + + if (mousePos.MapId != args.MapId) + return; + + // The handle is already in world space here (we never set a transform), so draw directly. + var handle = args.WorldHandle; + var pos = mousePos.Position; + const float arm = 0.35f; + const float gap = 0.08f; + + // Draw a dark outline first so the crosshair stays visible over any decal color. + DrawCross(handle, pos, arm, gap, 0.04f, Color.Black.WithAlpha(0.6f)); + DrawCross(handle, pos, arm, gap, 0f, Color.White); + handle.DrawCircle(pos, gap, Color.White, false); + } + + private static void DrawCross(DrawingHandleWorld handle, Vector2 pos, float arm, float gap, float pad, Color color) + { + handle.DrawLine(pos + new Vector2(-arm - pad, 0f), pos + new Vector2(-gap + pad, 0f), color); + handle.DrawLine(pos + new Vector2(gap - pad, 0f), pos + new Vector2(arm + pad, 0f), color); + handle.DrawLine(pos + new Vector2(0f, -arm - pad), pos + new Vector2(0f, -gap + pad), color); + handle.DrawLine(pos + new Vector2(0f, gap - pad), pos + new Vector2(0f, arm + pad), color); + } + // Exodus-End } diff --git a/Content.Client/Decals/UI/DecalPlacerWindow.xaml b/Content.Client/Decals/UI/DecalPlacerWindow.xaml index 0a4f2d67a6b..f9e8c8b58be 100644 --- a/Content.Client/Decals/UI/DecalPlacerWindow.xaml +++ b/Content.Client/Decals/UI/DecalPlacerWindow.xaml @@ -13,6 +13,8 @@