using System; using System.Collections.Generic; using System.Linq; using Cthangover.Core.UI.Theming; using Cthangover.Tools.Services; using Godot; using Cthangover.Mods.Interface.Theming; namespace Cthangover.Core.UI.Tool.ScenarioEditor { /// Cross-scene graph of all scenario files: a GraphEdit of scene nodes linked by switch targets, with a detail panel for the selected scenario. public partial class ScenarioGraphView : HSplitContainer { private GraphEdit _graph; private RichTextLabel _infoLabel; private Dictionary _nodes; private Dictionary _lookup; public event Action ScenarioFileRequested; public ScenarioGraphView() { SizeFlagsHorizontal = SizeFlags.ExpandFill; SizeFlagsVertical = SizeFlags.ExpandFill; _graph = new GraphEdit { SizeFlagsHorizontal = SizeFlags.ExpandFill, SizeFlagsVertical = SizeFlags.ExpandFill, RightDisconnects = false, ConnectionLinesThickness = 2f, ConnectionLinesAntialiased = true, MinimapEnabled = true, ShowGrid = true, SnappingEnabled = true, SnappingDistance = 20 }; _graph.ConnectionRequest += OnConnectionRequest; _graph.DisconnectionRequest += OnDisconnectionRequest; _graph.NodeSelected += OnNodeSelected; _graph.NodeDeselected += OnNodeDeselected; AddChild(_graph); var panel = new VBoxContainer { CustomMinimumSize = new Vector2(260, 0) }; panel.SizeFlagsVertical = SizeFlags.ExpandFill; AddChild(panel); var hint = UiKit.Hint(TranslationServer.Translate("tools/scenario_editor/graph_select_node")); hint.AutowrapMode = TextServer.AutowrapMode.WordSmart; panel.AddChild(hint); _infoLabel = new RichTextLabel { BbcodeEnabled = true, SizeFlagsVertical = SizeFlags.ExpandFill }; _infoLabel.AddThemeFontSizeOverride("normal_font_size", UiTokens.FontBody); _infoLabel.AddThemeFontSizeOverride("bold_font_size", UiTokens.FontBodyLarge); _infoLabel.AddThemeColorOverride("default_color", UiTokens.Body); _infoLabel.MetaClicked += OnLinkClicked; panel.AddChild(_infoLabel); } /// Rebuilds the graph nodes and edges, preserving the nodes' saved canvas positions. public void Populate(SceneGraphData data) { var savedPositions = SaveNodePositions(); ClearGraph(); _nodes = new Dictionary(); _lookup = new Dictionary(); var layout = ScenarioGraphLayout.Compute(data); foreach (var nodeData in data.Nodes) { var key = nodeData.SceneName.ToLowerInvariant(); if (_nodes.ContainsKey(key)) continue; var bgCount = nodeData.Backgrounds?.Count ?? 0; var firstBg = bgCount > 0 ? nodeData.Backgrounds[0] : ""; var position = savedPositions.TryGetValue(key, out var saved) ? saved : layout[key]; var gn = new GraphNode { Title = nodeData.SceneName, Size = new Vector2(240, 90), PositionOffset = position }; var nodeLabel = new Label { Text = string.Format(Tr("tools/scenario_editor/graph_node"), nodeData.ModId, nodeData.ScenarioCount) + (bgCount > 0 ? "\n" + string.Format(Tr("tools/scenario_editor/graph_node_bg"), firstBg) + (bgCount > 1 ? $" +{bgCount - 1}" : "") : "") }; nodeLabel.AddThemeColorOverride("font_color", UiTokens.Body); nodeLabel.AddThemeFontSizeOverride("font_size", UiTokens.FontBodySmall); gn.AddChild(nodeLabel); var color = UiTokens.White; if (nodeData.IsStartScene) { color = UiTokens.Success; gn.AddThemeStyleboxOverride("titlebar", CreateTitlebarStyle(UiTokens.Alpha(UiTokens.Success, 0.3f))); } else if (nodeData.HasErrors) { color = UiTokens.DangerText; gn.AddThemeStyleboxOverride("titlebar", CreateTitlebarStyle(UiTokens.Alpha(UiTokens.Danger, 0.5f))); } gn.SetSlot(0, true, 0, UiTokens.White, true, 0, color); _graph.AddChild(gn); _nodes[key] = gn; _lookup[key] = nodeData; } foreach (var edge in data.Edges) { var srcK = edge.FromScene.ToLowerInvariant(); var dstK = edge.ToScene.ToLowerInvariant(); if (!_nodes.ContainsKey(srcK) || !_nodes.ContainsKey(dstK)) continue; _graph.ConnectNode(_nodes[srcK].Name, 0, _nodes[dstK].Name, 0); } } private Dictionary SaveNodePositions() { var positions = new Dictionary(); if (_nodes == null) return positions; foreach (var kv in _nodes) positions[kv.Key] = kv.Value.PositionOffset; return positions; } private static StyleBoxFlat CreateTitlebarStyle(Color bgColor) { var style = UiKit.Frame(UiTokens.RadiusMd, bgColor); style.CornerRadiusBottomLeft = 0; style.CornerRadiusBottomRight = 0; return style; } private void ClearGraph() { foreach (var c in _graph.GetChildren()) if (c is GraphNode gn) gn.QueueFree(); _infoLabel.Text = ""; _nodes?.Clear(); _lookup?.Clear(); } private void OnConnectionRequest(StringName fromNode, long fromPort, StringName toNode, long toPort) { } private void OnDisconnectionRequest(StringName fromNode, long fromPort, StringName toNode, long toPort) { } private void OnNodeSelected(Node n) { if (n is not GraphNode gn) return; var key = gn.Title.ToString().ToLowerInvariant(); if (!_lookup.TryGetValue(key, out var nodeData)) return; var sb = new System.Text.StringBuilder(); sb.AppendLine($"[b]{nodeData.SceneName}[/b]"); sb.AppendLine(string.Format(Tr("tools/scenario_editor/graph_detail_mod"), nodeData.ModId)); if (nodeData.Backgrounds.Count > 0) sb.AppendLine(string.Format(Tr("tools/scenario_editor/graph_node_bg"), string.Join(", ", nodeData.Backgrounds))); sb.AppendLine(); sb.AppendLine(string.Format(Tr("tools/scenario_editor/graph_detail_scenarios"), nodeData.Scenarios.Count)); foreach (var sc in nodeData.Scenarios.OrderBy(s => s.Priority)) { var url = Uri.EscapeDataString(sc.Key); sb.AppendLine($" [{sc.Priority}] [url={url}]{sc.Name}[/url]"); } _infoLabel.Text = sb.ToString(); } private void OnNodeDeselected(Node n) { _infoLabel.Text = ""; } private static string Tr(string key) => TranslationServer.Translate(key); private void OnLinkClicked(Variant meta) { var path = Uri.UnescapeDataString(meta.AsString()); ScenarioFileRequested?.Invoke(path); } } }