using System.Numerics; using Robust.Client.AutoGenerated; using Robust.Client.Graphics; using Robust.Client.ResourceManagement; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; using Robust.Shared.Input; using Robust.Shared.Prototypes; using Robust.Shared.Timing; namespace Content.Client.UserInterface.Controls; /// /// Handles generic grid-drawing data, with zoom and dragging. /// [GenerateTypedNameReferences] [Virtual] public partial class MapGridControl : LayoutContainer { [Dependency] protected readonly IEntityManager EntManager = default!; [Dependency] protected readonly IGameTiming Timing = default!; [Dependency] protected readonly IPrototypeManager PrototypeManager = default!; // Mono [Dependency] protected readonly IClyde DisplayManager = default!; // Mono protected static readonly Color BackingColor = new Color(0.08f, 0.08f, 0.08f); private Font _largerFont; /* Dragging */ protected virtual bool Draggable { get; } = false; /// /// Control offset from whatever is being tracked. /// public Vector2 Offset; /// /// If the control is being recentered what is the target offset to reach. /// public Vector2 TargetOffset; private bool _draggin; protected Vector2 StartDragPosition; protected bool Recentering; protected virtual float ScrollSensitivity => 8f; /// /// Zoom factor for consistent multiplicative zoom steps. /// Each scroll step zooms in/out by this factor. /// protected const float ZoomFactor = 3.0f; protected float RecenterMinimum = 0.05f; /// /// UI pixel radius. /// public const int UIDisplayRadius = 320; protected const int MinimapMargin = 4; protected float WorldMinRange; protected float WorldMaxRange; public float WorldRange; public Vector2 WorldRangeVector => new Vector2(WorldRange, WorldRange); /// /// We'll lerp between the radarrange and actual range /// protected float ActualRadarRange; protected float CornerRadarRange => MathF.Sqrt(ActualRadarRange * ActualRadarRange + ActualRadarRange * ActualRadarRange) * 1.1f; /// /// Controls the maximum distance that will display. /// public float MaxRadarRange { get; private set; } = 256f * 10f; public Vector2 MaxRadarRangeVector => new Vector2(MaxRadarRange, MaxRadarRange); protected Vector2 MidPointVector => new Vector2(MidPoint, MidPoint); protected int MidPoint => SizeFull / 2; protected int SizeFull => (int)((UIDisplayRadius + MinimapMargin) * 2 * UIScale); protected int ScaledMinimapRadius => (int)(UIDisplayRadius * UIScale); protected float MinimapScale => WorldRange != 0 ? ScaledMinimapRadius / WorldRange : 0f; public event Action? WorldRangeChanged; private readonly ShaderInstance _circleMaskShader; // Mono public MapGridControl() : this(32f, 32f, 32f) { } public MapGridControl(float minRange, float maxRange, float range) { RobustXamlLoader.Load(this); IoCManager.InjectDependencies(this); SetSize = new Vector2(SizeFull, SizeFull); RectClipContent = true; MouseFilter = MouseFilterMode.Stop; ActualRadarRange = WorldRange; WorldMinRange = minRange; WorldMaxRange = maxRange; WorldRange = range; ActualRadarRange = range; var cache = IoCManager.Resolve(); _largerFont = new VectorFont(cache.GetResource("/EngineFonts/NotoSans/NotoSans-Regular.ttf"), 16); _circleMaskShader = PrototypeManager.Index("CircleAlphaMask").InstanceUnique(); // Mono } public void ForceRecenter() { Recentering = true; } protected override void KeyBindDown(GUIBoundKeyEventArgs args) { base.KeyBindDown(args); if (!Draggable) return; if (args.Function == EngineKeyFunctions.UseSecondary) { StartDragPosition = args.PointerLocation.Position; _draggin = true; args.Handle(); } } protected override void KeyBindUp(GUIBoundKeyEventArgs args) { if (!Draggable) return; if (args.Function == EngineKeyFunctions.UseSecondary) { _draggin = false; args.Handle(); } } protected override void MouseMove(GUIMouseMoveEventArgs args) { base.MouseMove(args); if (!_draggin) return; Recentering = false; Offset -= new Vector2(args.Relative.X, -args.Relative.Y) / MidPoint * WorldRange; } protected override void MouseWheel(GUIMouseWheelEventArgs args) { base.MouseWheel(args); // Use multiplicative zoom for consistent zoom steps // Positive delta = zoom in (divide), negative delta = zoom out (multiply) var zoomDirection = -args.Delta.Y / ScrollSensitivity; if (zoomDirection > 0) { // Zoom out - multiply by zoom factor ActualRadarRange = Math.Clamp(ActualRadarRange * MathF.Pow(ZoomFactor, zoomDirection), WorldMinRange, WorldMaxRange); } else if (zoomDirection < 0) { // Zoom in - divide by zoom factor ActualRadarRange = Math.Clamp(ActualRadarRange * MathF.Pow(ZoomFactor, zoomDirection), WorldMinRange, WorldMaxRange); } } public void AddRadarRange(float value) { ActualRadarRange = Math.Clamp(ActualRadarRange + value, WorldMinRange, WorldMaxRange); } /// /// Converts map coordinates to the local control. /// protected Vector2 ScalePosition(Vector2 value) { return ScalePosition(value, MinimapScale, MidPointVector); } protected static Vector2 ScalePosition(Vector2 value, float minimapScale, Vector2 midpointVector) { return value * minimapScale + midpointVector; } /// /// Converts local coordinates on the control to map coordinates. /// protected Vector2 InverseMapPosition(Vector2 value) { var inversePos = (value - MidPointVector) / MinimapScale; inversePos = inversePos with { Y = -inversePos.Y }; inversePos = Vector2.Transform(inversePos, Matrix3Helpers.CreateTransform(Offset, Angle.Zero)); return inversePos; } /// /// Handles re-centering the control's offset. /// /// public bool DrawRecenter() { // Map re-centering if (Recentering) { var frameTime = Timing.FrameTime; var diff = (TargetOffset - Offset) * (float)frameTime.TotalSeconds; if (Offset.LengthSquared() < RecenterMinimum) { Offset = TargetOffset; Recentering = false; } else { Offset += diff * 5f; return false; } } return Offset == TargetOffset; } protected void DrawBacking(DrawingHandleScreen handle) { var backing = BackingColor; handle.DrawRect(PixelSizeBox, backing); } protected void DrawNoSignal(DrawingHandleScreen handle) { var greyColor = Color.FromHex("#474F52"); // Draw funny lines var lineCount = 4f; for (var i = 0; i < lineCount; i++) { var angle = Angle.FromDegrees(45 + i * 360f / lineCount); var distance = Width / 2f; var start = MidPointVector + angle.RotateVec(new Vector2(0f, 2.5f * distance / 4f)); var end = MidPointVector + angle.RotateVec(new Vector2(0f, 4f * distance / 4f)); handle.DrawLine(start, end, greyColor); } var signalText = Loc.GetString("shuttle-console-no-signal"); var dimensions = handle.GetDimensions(_largerFont, signalText, 1f); var position = MidPointVector - dimensions / 2f; handle.DrawString(_largerFont, position, Loc.GetString("shuttle-console-no-signal"), greyColor); } protected override void Draw(DrawingHandleScreen handle) { base.Draw(handle); if (!ActualRadarRange.Equals(WorldRange)) { var diff = ActualRadarRange - WorldRange; const float lerpRate = 10f; WorldRange += (float)Math.Clamp(diff, -lerpRate * MathF.Abs(diff) * Timing.FrameTime.TotalSeconds, lerpRate * MathF.Abs(diff) * Timing.FrameTime.TotalSeconds); WorldRangeChanged?.Invoke(WorldRange); } } #region Mono /// /// Masks everything drawn with the shader enabled by a circle. /// If you don't want it circular, don't use this. /// protected void UseCircleMaskShader(DrawingHandleScreen handle) { // Simple, just base the radius on the width. _circleMaskShader.SetParameter("radius", PixelWidth * 0.5f); // Not nearly as simple, we transform the coordinates from top-left origin (UI space) to bottom-left origin (shader fragment space) _circleMaskShader.SetParameter("center", new Vector2(GlobalPixelPosition.X + PixelWidth * 0.5f, DisplayManager.ScreenSize.Y - GlobalPixelPosition.Y - PixelHeight * 0.5f)); handle.UseShader(_circleMaskShader); } /// /// Verbose shortcut for handle.UseShader(null) /// protected void ClearShader(DrawingHandleScreen handle) { handle.UseShader(null); } #endregion Mono }