6
StarHorizon_Public/Content.Client/Xenoarchaeology/Ui/XenoArtifactGraphControl.xaml.cs
2025-08-13 15:03:01 +03:00

209 lines
8.7 KiB
C#

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<XenoArtifactComponent>? _artifact;
private Entity<XenoArtifactNodeComponent>? _hoveredNode;
private readonly Font _font;
public event Action<Entity<XenoArtifactNodeComponent>>? 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<XenoArtifactSystem>();
var fontResource = IoCManager.Resolve<IResourceCache>()
.GetResource<FontResource>("/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<XenoArtifactComponent>? 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();
}
/// <summary>
/// Renders artifact node graph control, consisting of nodes and edges connecting them.
/// </summary>
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<XenoArtifactNodeComponent> node, float ySpacing, List<List<Entity<XenoArtifactNodeComponent>>> 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<Entity<XenoArtifactNodeComponent>> nodes)
{
var num = _artifactSystem.GetDepthOrderedNodes(nodes)
.Max(p => p.Value.Count);
return (NodeDiameter * num) + MinXSpacing * (num - 1);
}
}