using System.Linq; using System.Numerics; using Content.Client.Xenoarchaeology.Artifact; using Content.Shared.Xenoarchaeology.Artifact.Components; 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; namespace Content.Client.Xenoarchaeology.Ui; [GenerateTypedNameReferences] public sealed partial class XenoArtifactGraphControl : BoxContainer { [Dependency] private readonly IEntityManager _entityManager = default!; private readonly XenoArtifactSystem _artifactSystem; private Entity? _artifact; private Entity? _hoveredNode; private readonly Font _font; public event Action>? OnNodeSelected; private float NodeRadius => 25 * UIScale; private float NodeDiameter => NodeRadius * 2; private float MinYSpacing => NodeDiameter * 0.75f; private float MaxYSpacing => NodeDiameter * 1.5f; private float MinXSpacing => NodeDiameter * 0.33f; private float MaxXSpacing => NodeDiameter * 1f; private float MinXSegmentSpacing => NodeDiameter * 0.5f; private float MaxXSegmentSpacing => NodeDiameter * 3f; public XenoArtifactGraphControl() { IoCManager.InjectDependencies(this); RobustXamlLoader.Load(this); _artifactSystem = _entityManager.System(); var fontResource = IoCManager.Resolve() .GetResource("/EngineFonts/NotoSans/NotoSansMono-Regular.ttf"); _font = new VectorFont(fontResource, 16); } public Color LockedNodeColor { get; set; } = Color.FromHex("#777777"); public Color ActiveNodeColor { get; set; } = Color.Plum; public Color UnlockedNodeColor { get; set; } = Color.White; public Color HoveredNodeColor { get; set; } = Color.DimGray; public Color UnlockableNodeColor { get; set; } = Color.LightSlateGray; public void SetArtifact(Entity? artifact) { _artifact = artifact; } protected override void KeyBindDown(GUIBoundKeyEventArgs args) { base.KeyBindDown(args); if (args.Handled || args.Function != EngineKeyFunctions.UIClick) return; if (_hoveredNode == null) return; OnNodeSelected?.Invoke(_hoveredNode.Value); UserInterfaceManager.ClickSound(); } /// /// Renders artifact node graph control, consisting of nodes and edges connecting them. /// protected override void Draw(DrawingHandleScreen handle) { base.Draw(handle); _hoveredNode = null; if (_artifact == null) return; var artifact = _artifact.Value; var maxDepth = _artifactSystem.GetAllNodes(artifact) .Max(s => s.Comp.Depth); var segments = _artifactSystem.GetSegments(artifact); var bottomLeft = Position // the position + new Vector2(0, Size.Y * UIScale) // the scaled height of the control + new Vector2(NodeRadius, -NodeRadius); // offset half a node so we don't render off screen var controlHeight = bottomLeft.Y; var controlWidth = Size.X * UIScale - NodeRadius; // select y spacing based on max number of nodes we have on Y axis - that is max depth of artifact graph node var ySpacing = 0f; if (maxDepth != 0) ySpacing = Math.Clamp((controlHeight - ((maxDepth + 1) * NodeDiameter)) / maxDepth, MinYSpacing, MaxYSpacing); // gets settings for visualizing segments (groups of interconnected nodes - there may be 1 or more per artifact). var segmentWidths = segments.Sum(GetBiggestWidth); var segmentSpacing = Math.Clamp((controlWidth - segmentWidths) / (segments.Count - 1), MinXSegmentSpacing, MaxXSegmentSpacing); var segmentOffset = Math.Max((controlWidth - (segmentWidths) - (segmentSpacing * (segments.Count - 1))) / 2, 0); bottomLeft.X += segmentOffset; bottomLeft.Y -= (controlHeight - (ySpacing * maxDepth) - (NodeDiameter * (maxDepth + 1))) / 2; var cursor = (UserInterfaceManager.MousePositionScaled.Position * UIScale) - GlobalPixelPosition; foreach (var segment in segments) { // For each segment we draw nodes in order of depth. Method returns List of nodes for each depth level. var orderedNodes = _artifactSystem.GetDepthOrderedNodes(segment); foreach (var (_, nodes) in orderedNodes) { for (var i = 0; i < nodes.Count; i++) { // selecting color for node based on its state var node = nodes[i]; var color = LockedNodeColor; if (_artifactSystem.IsNodeActive(artifact, node)) { color = ActiveNodeColor; } else if (!node.Comp.Locked) { color = UnlockedNodeColor; } else { var directPredecessorNodes = _artifactSystem.GetDirectPredecessorNodes((artifact, artifact), node); if (directPredecessorNodes.Count == 0 || directPredecessorNodes.All(x => !x.Comp.Locked)) { color = UnlockableNodeColor; } } var pos = GetNodePos(node, ySpacing, segments, ref bottomLeft); var hovered = (cursor - pos).LengthSquared() <= NodeRadius * NodeRadius; if (hovered) { // render hovered node if we have one _hoveredNode = node; handle.DrawCircle(pos, NodeRadius, HoveredNodeColor); } // render circle and text with node id inside handle.DrawCircle(pos, NodeRadius, Color.ToSrgb(color), false); var text = _artifactSystem.GetNodeId(node); var dimensions = handle.GetDimensions(_font, text, 1); handle.DrawString(_font, pos - new Vector2(dimensions.X / 2, dimensions.Y / 2), text, color); } } // draw edges for each segment and each node that have successors foreach (var node in segment) { var fromNode = GetNodePos(node, ySpacing, segments, ref bottomLeft) + new Vector2(0, -NodeRadius); var successorNodes = _artifactSystem.GetDirectSuccessorNodes((artifact, artifact), node); foreach (var successorNode in successorNodes) { var color = node.Comp.Locked ? LockedNodeColor : UnlockedNodeColor; var toNode = GetNodePos(successorNode, ySpacing, segments, ref bottomLeft) + new Vector2(0, NodeRadius); handle.DrawLine(fromNode, toNode, color); } } bottomLeft.X += GetBiggestWidth(segment) + segmentSpacing; } } private Vector2 GetNodePos(Entity node, float ySpacing, List>> segments, ref Vector2 bottomLeft) { var yPos = -(NodeDiameter + ySpacing) * node.Comp.Depth; var segment = segments.First(s => s.Contains(node)); var depthOrderedNodes = _artifactSystem.GetDepthOrderedNodes(segment); var biggestTier = depthOrderedNodes.Max(s => s.Value.Count); var nodesInLayer = depthOrderedNodes.GetValueOrDefault(node.Comp.Depth)!.Count; var biggestWidth = (NodeDiameter + MinXSpacing) * biggestTier; var xSpacing = Math.Clamp((biggestWidth - (NodeDiameter * nodesInLayer)) / (nodesInLayer - 1), MinXSpacing, MaxXSpacing); var layerXOffset = (biggestWidth - (xSpacing * (nodesInLayer - 1)) - (NodeDiameter * nodesInLayer)) / 2; // get index of node in current segment's row (row per depth level) var index = depthOrderedNodes.GetValueOrDefault(node.Comp.Depth)!.IndexOf(node); var xPos = NodeDiameter * index + (xSpacing * index) + layerXOffset; return bottomLeft + new Vector2(xPos, yPos); } private float GetBiggestWidth(List> nodes) { var num = _artifactSystem.GetDepthOrderedNodes(nodes) .Max(p => p.Value.Count); return (NodeDiameter * num) + MinXSpacing * (num - 1); } }