209 lines
8.7 KiB
C#
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);
|
|
}
|
|
}
|
|
|