using Content.Client.Parallax; using Content.Client.Research; using Content.Client.UserInterface.Controls; using Content.Shared._NF.Research; using Content.Shared.Access.Systems; using Content.Shared.Research.Components; using Content.Shared.Research.Prototypes; using Robust.Client.AutoGenerated; using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Client.Player; 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.Utility; using System.Numerics; namespace Content.Client._NF.Research.UI; [GenerateTypedNameReferences] public sealed partial class FancyResearchConsoleMenu : FancyWindow { public Action? OnTechnologyCardPressed; public Action? OnServerButtonPressed; [Dependency] private readonly IEntityManager _entity = default!; [Dependency] private readonly IPrototypeManager _prototype = default!; [Dependency] private readonly IPlayerManager _player = default!; private readonly ResearchSystem _research; private readonly SpriteSystem _sprite; private readonly AccessReaderSystem _accessReader; private ParallaxControl _parallaxControl; /// /// The parallax prototype to use for the background. Configurable. /// public string ParallaxPrototype { get; set; } = "Default"; /// /// Updates the parallax background to use a different prototype /// /// The new parallax prototype to use public void SetParallaxPrototype(string parallaxPrototype) { ParallaxPrototype = parallaxPrototype; if (_parallaxControl != null) { _parallaxControl.ParallaxPrototype = parallaxPrototype; } } /// /// Console entity /// public EntityUid Entity; /// /// Currently selected tech /// Exists for better UI refreshing /// public ProtoId? CurrentTech; /// /// All technologies and their availability /// public Dictionary List = new(); /// /// Cached research points /// public int Points = 0; /// /// Is tech currently being dragged /// private bool _draggin; /// /// the distance between elements on the grid. /// private const int GridSize = 90; /// /// technology cards size. /// private const int CardSize = 64; /// /// the origin point of the grid. /// private static readonly Vector2i DefaultPosition = Vector2i.Zero; private Box2i _bounds = new(DefaultPosition, DefaultPosition); public FancyResearchConsoleMenu() { RobustXamlLoader.Load(this); IoCManager.InjectDependencies(this); _research = _entity.System(); _sprite = _entity.System(); _accessReader = _entity.System(); // Set up scroll container properties TechScrollContainer.ScrollSpeedX = 100; TechScrollContainer.ScrollSpeedY = 100; TechScrollContainer.HScrollEnabled = false; TechScrollContainer.VScrollEnabled = true; // Frontier: Initialize parallax background _parallaxControl = new ParallaxControl { ParallaxPrototype = ParallaxPrototype, HorizontalExpand = true, VerticalExpand = true, }; // Add the parallax control to the ResearchesContainer at the beginning (bottom layer) ResearchesContainer.AddChild(_parallaxControl); // Set the proper rendering order by adjusting positions in the parent's child list // Controls with a higher position value are drawn on top (foreground) // Make sure the parallax is at the bottom of the z-order (drawn first) _parallaxControl.SetPositionInParent(0); // The drag container should be in the middle TechScrollContainer.SetPositionInParent(1); // Apply scrollbar styling ApplyScrollbarStyling(); // Set up event handlers ServerButton.OnPressed += _ => OnServerButtonPressed?.Invoke(); DragContainer.OnKeyBindDown += OnKeybindDown; DragContainer.OnKeyBindUp += OnKeybindUp; RecenterButton.OnPressed += _ => Recenter(); // Empty initialization UpdatePanels(List); } /// /// Apply scrollbar styling using centralized colors and utilities /// private void ApplyScrollbarStyling() { var scrollBarNormal = ResearchUIHelpers.CreateScrollbarStyleBox("normal"); var scrollBarHovered = ResearchUIHelpers.CreateScrollbarStyleBox("hovered"); var scrollBarGrabbed = ResearchUIHelpers.CreateScrollbarStyleBox("grabbed"); } public void SetEntity(EntityUid entity) => Entity = entity; public void UpdatePanels(Dictionary dict) { // Clear existing items DragContainer.RemoveAllChildren(); List = dict; var bounds = new Box2i(); var boundsSet = false; // Calculate bounds foreach (var tech in List) { var proto = _prototype.Index(tech.Key); var position = DefaultPosition + (GridSize * proto.Position.X, GridSize * proto.Position.Y); if (!boundsSet) { bounds.BottomLeft = position; bounds.TopRight = position; boundsSet = true; } else { bounds.Left = int.Min(position.X, bounds.Left); bounds.Bottom = int.Min(position.Y, bounds.Bottom); bounds.Right = int.Max(position.X, bounds.Right); bounds.Top = int.Max(position.Y, bounds.Top); } } if (boundsSet) { _bounds = bounds; // Set container size with padding var padding = 200; var totalWidth = _bounds.Width + CardSize + padding; var totalHeight = _bounds.Height + CardSize + padding; DragContainer.SetWidth = Math.Max(totalWidth, 1000); DragContainer.SetHeight = Math.Max(totalHeight, 1000); } // Add tech items foreach (var tech in List) { var proto = _prototype.Index(tech.Key); var control = new FancyResearchConsoleItem(proto, _sprite, tech.Value); DragContainer.AddChild(control); // Position the tech item var leftPadding = 20; var topPadding = 20; var uiPosition = new Vector2( proto.Position.X * GridSize - _bounds.Left + leftPadding, proto.Position.Y * GridSize - _bounds.Bottom + topPadding ); LayoutContainer.SetPosition(control, uiPosition); control.SelectAction += SelectTech; control.IsSelected = tech.Key == CurrentTech; } } public void UpdateInformationPanel(int points) { Points = points; var amountMsg = new FormattedMessage(); amountMsg.AddMarkupOrThrow(Loc.GetString("research-console-menu-research-points-text", ("points", points))); ResearchAmountLabel.SetMessage(amountMsg); if (!_entity.TryGetComponent(Entity, out TechnologyDatabaseComponent? database)) return; TierDisplayContainer.RemoveAllChildren(); foreach (var disciplineId in database.SupportedDisciplines) { var discipline = _prototype.Index(disciplineId); var tier = _research.GetTierCompletionPercentage(database, discipline, _prototype); var texture = new TextureRect { TextureScale = new Vector2(2, 2), VerticalAlignment = VAlignment.Center }; var label = new RichTextLabel(); texture.Texture = _sprite.Frame0(discipline.Icon); label.SetMessage(Loc.GetString("research-console-tier-percentage", ("perc", tier))); var control = new BoxContainer { Children = { texture, label, new Control { MinWidth = 10 } } }; TierDisplayContainer.AddChild(control); } } #region Drag handle protected override void MouseMove(GUIMouseMoveEventArgs args) { base.MouseMove(args); if (!_draggin) return; // Adjust scroll position with drag var scrollSpeed = 2.0f; TechScrollContainer.VScrollTarget -= args.Relative.Y * scrollSpeed; } /// /// Raised when LMB is pressed at /// private void OnKeybindDown(GUIBoundKeyEventArgs args) { if (args.Function == EngineKeyFunctions.Use) _draggin = true; } /// /// Raised when LMB is unpressed at /// private void OnKeybindUp(GUIBoundKeyEventArgs args) { if (args.Function == EngineKeyFunctions.Use) _draggin = false; } protected override DragMode GetDragModeFor(Vector2 relativeMousePos) => _draggin ? DragMode.None : base.GetDragModeFor(relativeMousePos); #endregion /// /// Selects a tech prototype and opens info panel /// /// Tech proto /// Tech availability public void SelectTech(TechnologyPrototype proto, ResearchAvailability availability) { InfoContainer.RemoveAllChildren(); if (!_player.LocalEntity.HasValue) return; // Update selection CurrentTech = proto.ID; // Update visual selection state foreach (var child in DragContainer.Children) { if (child is FancyResearchConsoleItem techItem) { techItem.IsSelected = techItem.Prototype.ID == CurrentTech; } } // Create and add info panel var control = new FancyTechnologyInfoPanel(proto, _accessReader.IsAllowed(_player.LocalEntity.Value, Entity), availability, _sprite); control.BuyAction += args => OnTechnologyCardPressed?.Invoke(args.ID); InfoContainer.AddChild(control); } public void Recenter() { // Reset scroll position TechScrollContainer.VScrollTarget = 0; } public override void Close() { base.Close(); DragContainer.RemoveAllChildren(); InfoContainer.RemoveAllChildren(); } private sealed partial class DisciplineButton(TechDisciplinePrototype proto) : Button { public TechDisciplinePrototype Proto = proto; } protected override void MouseWheel(GUIMouseWheelEventArgs args) { base.MouseWheel(args); } }