This commit is contained in:
cthangover committed 2026-09-10 10:59:40 +03:00
commit 8e6ad52ee4
44 files changed
+6842

No files matched your search

+297
View File
@@ -0,0 +1,297 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Cthangover.Tools.Services;
using Cthangover.Core.Utils;
using Godot;
namespace Cthangover.Core.UI.Tool.ScenarioEditor
{
public sealed class TabData
{
public string FilePath;
public string FileName;
public string SavedText;
public bool IsDirty;
}
public partial class ScenarioEditTabs : VBoxContainer
{
private const int GraphTabIndex = 0;
private TabBar _tabBar;
private TextEdit _textEdit;
private ScenarioGraphView _graphView;
private Font _monospaceFont;
private readonly List<TabData> _tabs = new();
private int _activeTabIndex = -1;
private bool _suppressTextChanged;
public TextEdit CodeEditor => _textEdit;
public string CurrentText => _textEdit.Text;
public string CurrentAbsolutePath =>
_activeTabIndex >= 0 && _activeTabIndex < _tabs.Count ? _tabs[_activeTabIndex].FilePath : null;
public bool HasActiveFile => _activeTabIndex >= 0 && _activeTabIndex < _tabs.Count;
public bool IsCurrentDirty =>
_activeTabIndex >= 0 && _activeTabIndex < _tabs.Count && _tabs[_activeTabIndex].IsDirty;
public event Action<string> TextTabSelected;
public event Action GraphTabSelected;
public event Action<bool> DirtyChanged;
public event Action<string> GraphFileRequested;
public ScenarioEditTabs(ScenarioGraphView graphView, Font monospaceFont)
{
_graphView = graphView;
_monospaceFont = monospaceFont;
SizeFlagsHorizontal = SizeFlags.ExpandFill;
SizeFlagsVertical = SizeFlags.ExpandFill;
_tabBar = new TabBar();
_tabBar.TabCloseDisplayPolicy = TabBar.CloseButtonDisplayPolicy.ShowActiveOnly;
_tabBar.TabSelected += OnTabSelected;
_tabBar.TabClosePressed += OnTabClosePressed;
_tabBar.AddTab(TranslationServer.Translate("tools/scenario_editor/tab_graph"));
AddChild(_tabBar);
_textEdit = new TextEdit
{
SizeFlagsHorizontal = SizeFlags.ExpandFill,
SizeFlagsVertical = SizeFlags.ExpandFill,
Editable = true,
SyntaxHighlighter = ScenarioSyntaxService.CreateHighlighter(),
WrapMode = TextEdit.LineWrappingMode.None
};
_textEdit.AddThemeFontOverride("font", _monospaceFont);
_textEdit.TextChanged += OnTextChanged;
AddChild(_textEdit);
_graphView.ScenarioFileRequested += OnGraphScenarioFileRequested;
AddChild(_graphView);
}
public void OpenFile(string absolutePath)
{
for (int i = 0; i < _tabs.Count; i++)
{
if (_tabs[i].FilePath == absolutePath)
{
SwitchToTextTab(i);
return;
}
}
try
{
var text = ScenarioFileService.ReadAllText(absolutePath);
var tab = new TabData
{
FilePath = absolutePath,
FileName = Path.GetFileNameWithoutExtension(absolutePath),
SavedText = text,
IsDirty = false
};
_tabs.Add(tab);
_tabBar.AddTab(tab.FileName);
SwitchToTextTab(_tabs.Count - 1);
}
catch (Exception ex)
{
GameLogger.Log("SCENARIO_EDITOR", $"Failed to load file: {ex.Message}", LogLevel.Error);
}
}
public void SaveCurrentFile()
{
if (_activeTabIndex < 0 || _activeTabIndex >= _tabs.Count)
return;
var tab = _tabs[_activeTabIndex];
tab.SavedText = _textEdit.Text;
if (tab.IsDirty)
{
tab.IsDirty = false;
UpdateTabTitle(_activeTabIndex);
UpdateGlobalDirty();
}
}
public void SetTextSilent(string text)
{
_suppressTextChanged = true;
_textEdit.Text = text;
_suppressTextChanged = false;
}
public void SetViewToGraph()
{
if (_tabs.Count > 0)
return;
_tabBar.CurrentTab = GraphTabIndex;
_textEdit.Visible = false;
_graphView.Visible = true;
GraphTabSelected?.Invoke();
}
public void RefreshGraph()
{
if (_graphView.Visible)
{
var data = ScenarioFileService.BuildSceneGraph();
_graphView.Populate(data);
}
}
public bool ConfirmCloseAll()
{
for (int i = _tabs.Count - 1; i >= 0; i--)
{
if (_tabs[i].IsDirty)
return false;
}
return true;
}
private void OnTabSelected(long index)
{
if (index == GraphTabIndex)
{
SaveActiveTextEditContent();
_activeTabIndex = -1;
_textEdit.Visible = false;
_graphView.Visible = true;
GraphTabSelected?.Invoke();
}
else
{
_graphView.Visible = false;
_textEdit.Visible = true;
SwitchToTextTab((int)index - 1);
}
}
private void OnTabClosePressed(long index)
{
if (index == GraphTabIndex)
return;
CloseTextTab((int)index - 1);
}
private void SwitchToTextTab(int index)
{
if (index < 0 || index >= _tabs.Count || index == _activeTabIndex)
return;
if (_activeTabIndex >= 0 && _activeTabIndex < _tabs.Count)
_tabs[_activeTabIndex].SavedText = _textEdit.Text;
_activeTabIndex = index;
_tabBar.CurrentTab = index + 1;
var tab = _tabs[index];
SetTextSilent(tab.SavedText);
TextTabSelected?.Invoke(tab.SavedText);
}
private void CloseTextTab(int index)
{
if (index < 0 || index >= _tabs.Count)
return;
if (_tabs[index].IsDirty)
{
var dialog = new ConfirmationDialog();
dialog.Title = TranslationServer.Translate("tools/scenario_editor/unsaved_title");
dialog.DialogText = TranslationServer.Translate("tools/scenario_editor/unsaved_switch_text");
int capturedIndex = index;
dialog.Confirmed += () =>
{
dialog.QueueFree();
DoCloseTextTab(capturedIndex);
};
dialog.Canceled += () => dialog.QueueFree();
AddChild(dialog);
dialog.PopupCentered();
return;
}
DoCloseTextTab(index);
}
private void DoCloseTextTab(int index)
{
_tabs.RemoveAt(index);
_tabBar.RemoveTab(index + 1);
if (_tabs.Count == 0)
{
_activeTabIndex = -1;
SetTextSilent("");
TextTabSelected?.Invoke("");
UpdateGlobalDirty();
return;
}
if (index == _activeTabIndex)
{
var newIndex = Math.Min(index, _tabs.Count - 1);
_activeTabIndex = -1;
SwitchToTextTab(newIndex);
}
else if (index < _activeTabIndex)
{
_activeTabIndex--;
_tabBar.CurrentTab = _activeTabIndex + 1;
}
UpdateGlobalDirty();
}
private void OnTextChanged()
{
if (_suppressTextChanged || _activeTabIndex < 0 || _activeTabIndex >= _tabs.Count)
return;
var tab = _tabs[_activeTabIndex];
if (!tab.IsDirty)
{
tab.IsDirty = true;
UpdateTabTitle(_activeTabIndex);
UpdateGlobalDirty();
}
}
private void UpdateTabTitle(int index)
{
if (index < 0 || index >= _tabs.Count)
return;
var tab = _tabs[index];
_tabBar.SetTabTitle(index + 1, tab.IsDirty ? tab.FileName + " *" : tab.FileName);
}
private void UpdateGlobalDirty()
{
bool anyDirty = _tabs.Any(t => t.IsDirty);
DirtyChanged?.Invoke(anyDirty);
}
private void SaveActiveTextEditContent()
{
if (_activeTabIndex >= 0 && _activeTabIndex < _tabs.Count)
_tabs[_activeTabIndex].SavedText = _textEdit.Text;
}
private void OnGraphScenarioFileRequested(string filePath)
{
GraphFileRequested?.Invoke(filePath);
}
}
}
@@ -0,0 +1,16 @@
using Cthangover.Core.UI.Tool;
using Godot;
namespace Cthangover.Core.UI.Tool.ScenarioEditor
{
/// <summary>Registers the scenario editor as a tool window. Creates <see cref="ScenarioEditorWindow"/> instances.</summary>
public class ScenarioEditorToolProvider : IToolProvider
{
/// <summary>Unique tool identifier.</summary>
public string Id => "scenario_editor";
/// <summary>Translation key for the tool's display name.</summary>
public string LocaleKey => "tools/scenario_editor/title";
/// <summary>Creates the editor window instance.</summary>
public Window CreateWindow() => new ScenarioEditorWindow();
}
}
+354
View File
@@ -0,0 +1,354 @@
using System;
using Cthangover.Core.Scenes;
using Cthangover.Core.Utils;
using Cthangover.Tools.Services;
using Godot;
namespace Cthangover.Core.UI.Tool.ScenarioEditor
{
public partial class ScenarioEditorWindow : ToolWindow
{
private Control _editLayout;
private Control _playLayout;
private ScenarioFileTreePanel _fileTreePanel;
private ScenarioEditTabs _editTabs;
private ScenarioInfoPanel _infoPanel;
private ScenarioThumbnailPanel _thumbPanel;
private Button _saveBtn;
private Button _openFileBtn;
private Button _playBtn;
private Label _filePathLabel;
private Label _statusLabel;
private bool _playActive;
private CanvasLayer _stopOverlay;
public ScenarioEditorWindow() : base("tools/scenario_editor/title")
{
BuildUI();
}
public override void _Ready()
{
_editTabs.SetViewToGraph();
}
protected override void Cleanup()
{
}
public override void _Process(double delta)
{
if (_playActive && !SceneManager.IsTestPlayActive)
ReturnFromPlay();
}
private void BuildUI()
{
_editLayout = new Control
{
AnchorRight = 1f,
AnchorBottom = 1f,
Visible = true
};
AddChild(_editLayout);
var outerVBox = CreateFillContainer();
_editLayout.AddChild(outerVBox);
var toolbar = CreateToolbar();
outerVBox.AddChild(toolbar);
_saveBtn = new Button
{
Text = TranslationServer.Translate("tools/scenario_editor/save"),
Disabled = true
};
_saveBtn.Pressed += OnSavePressed;
toolbar.AddChild(_saveBtn);
_openFileBtn = new Button
{
Text = TranslationServer.Translate("tools/scenario_editor/open_file"),
Disabled = true
};
_openFileBtn.Pressed += OnOpenFilePressed;
toolbar.AddChild(_openFileBtn);
_playBtn = new Button
{
Text = TranslationServer.Translate("tools/scenario_editor/play"),
TooltipText = TranslationServer.Translate("tools/scenario_editor/play_tooltip"),
Disabled = true
};
_playBtn.Pressed += OnPlayPressed;
toolbar.AddChild(_playBtn);
_filePathLabel = new Label { Text = "" };
_filePathLabel.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
toolbar.AddChild(_filePathLabel);
var mainSplit = new HSplitContainer();
mainSplit.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
mainSplit.SizeFlagsVertical = Control.SizeFlags.ExpandFill;
outerVBox.AddChild(mainSplit);
var leftPanel = new PanelContainer
{
CustomMinimumSize = new Vector2(340, 0),
SizeFlagsVertical = Control.SizeFlags.ExpandFill
};
mainSplit.AddChild(leftPanel);
var leftVBox = new VBoxContainer();
leftPanel.AddChild(leftVBox);
var tabsContainer = new TabContainer();
tabsContainer.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
tabsContainer.SizeFlagsVertical = Control.SizeFlags.ExpandFill;
leftVBox.AddChild(tabsContainer);
_fileTreePanel = (ScenarioFileTreePanel)TscnScenes.LoadAndBuild("scenes/scenario_file_tree_panel.tscn");
_fileTreePanel.FileSelected += OnFileSelected;
tabsContainer.AddChild(_fileTreePanel);
tabsContainer.SetTabTitle(0, TranslationServer.Translate("tools/scenario_editor/tab_files"));
_infoPanel = (ScenarioInfoPanel)TscnScenes.LoadAndBuild("scenes/scenario_info_panel.tscn");
tabsContainer.AddChild(_infoPanel);
tabsContainer.SetTabTitle(1, TranslationServer.Translate("tools/scenario_editor/tab_info"));
leftVBox.AddChild(new Label { Text = TranslationServer.Translate("tools/scenario_editor/bg_previews") + ":" });
_thumbPanel = (ScenarioThumbnailPanel)TscnScenes.LoadAndBuild("scenes/scenario_thumbnail_panel.tscn");
leftVBox.AddChild(_thumbPanel);
var rightPanel = new PanelContainer
{
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
SizeFlagsVertical = Control.SizeFlags.ExpandFill
};
mainSplit.AddChild(rightPanel);
_editTabs = new ScenarioEditTabs(new ScenarioGraphView(), GetMonospaceFont());
_editTabs.TextTabSelected += OnTextTabSelected;
_editTabs.GraphTabSelected += OnGraphTabSelected;
_editTabs.DirtyChanged += OnDirtyChanged;
_editTabs.GraphFileRequested += OnGraphFileRequested;
rightPanel.AddChild(_editTabs);
var statusBar = new HBoxContainer();
outerVBox.AddChild(statusBar);
_statusLabel = new Label { Text = "" };
statusBar.AddChild(_statusLabel);
_playLayout = new Control
{
AnchorRight = 1f,
AnchorBottom = 1f,
Visible = false
};
AddChild(_playLayout);
var stopContainer = new HBoxContainer();
stopContainer.Alignment = BoxContainer.AlignmentMode.Center;
stopContainer.AnchorRight = 0.5f;
stopContainer.AnchorLeft = 0.5f;
stopContainer.OffsetTop = 8;
_playLayout.AddChild(stopContainer);
var stopBtn = new Button
{
Text = TranslationServer.Translate("tools/scenario_editor/stop"),
CustomMinimumSize = new Vector2(120, 40)
};
stopBtn.Pressed += OnStopPressed;
stopContainer.AddChild(stopBtn);
}
private void OnFileSelected(string filePath)
{
_editTabs.OpenFile(filePath);
}
private void OnGraphFileRequested(string filePath)
{
_fileTreePanel.SelectFile(filePath);
}
private void OnTextTabSelected(string text)
{
_filePathLabel.Text = _editTabs.CurrentAbsolutePath != null
? ScenarioFileService.GetRelativePath(_editTabs.CurrentAbsolutePath) : "";
_saveBtn.Disabled = false;
_openFileBtn.Disabled = false;
_playBtn.Disabled = false;
_statusLabel.Text = _editTabs.IsCurrentDirty
? TranslationServer.Translate("tools/scenario_editor/status_modified") : "";
_infoPanel.Update(text);
_thumbPanel.Load(text);
}
private void OnGraphTabSelected()
{
_filePathLabel.Text = "";
_saveBtn.Disabled = true;
_openFileBtn.Disabled = true;
_playBtn.Disabled = true;
_statusLabel.Text = "";
_infoPanel.Clear();
_thumbPanel.Clear();
_editTabs.RefreshGraph();
}
private void OnDirtyChanged(bool anyDirty)
{
if (anyDirty)
{
SetDirty();
_statusLabel.Text = TranslationServer.Translate("tools/scenario_editor/status_modified");
}
else
{
MarkClean();
}
}
private void OnSavePressed()
{
if (!_editTabs.HasActiveFile)
return;
var path = _editTabs.CurrentAbsolutePath;
var text = _editTabs.CurrentText;
try
{
ScenarioFileService.WriteAllText(path, text);
_editTabs.SaveCurrentFile();
_statusLabel.Text = TranslationServer.Translate("tools/scenario_editor/status_saved");
_infoPanel.Update(text);
_thumbPanel.Load(text);
_editTabs.RefreshGraph();
GameLogger.Log("SCENARIO_EDITOR", $"Saved: {path}");
}
catch (Exception ex)
{
GameLogger.Log("SCENARIO_EDITOR", $"Save failed: {ex.Message}", LogLevel.Error);
_statusLabel.Text = string.Format(
TranslationServer.Translate("tools/scenario_editor/save_error"), ex.Message);
}
}
private void OnOpenFilePressed()
{
var path = _editTabs.CurrentAbsolutePath;
if (path == null)
return;
GD.Print($"[ScenarioEditor] Opening: {path}");
var err = OS.ShellOpen(path);
if (err != Error.Ok)
GameLogger.Log("SCENARIO_EDITOR", $"ShellOpen error: {err}", LogLevel.Error);
}
private void OnPlayPressed()
{
if (!_editTabs.HasActiveFile)
return;
var text = _editTabs.CurrentText;
var metadata = ScenarioFileService.ParseMetadata(text);
if (metadata.Scene == "-")
{
_statusLabel.Text = TranslationServer.Translate("tools/scenario_editor/no_scene_error");
return;
}
var path = _editTabs.CurrentAbsolutePath;
try
{
ScenarioFileService.WriteAllText(path, text);
}
catch (Exception ex)
{
_statusLabel.Text = string.Format(
TranslationServer.Translate("tools/scenario_editor/save_error"), ex.Message);
return;
}
_editTabs.SaveCurrentFile();
_editLayout.Visible = false;
_playLayout.Visible = true;
SceneManager.IsTestPlayActive = true;
SceneManager.TestScenarioText = text;
GameLogger.Log("SCENARIO_EDITOR", $"Test play START: scene='{metadata.Scene}', queue={text.Split('\n').Length} lines");
Visible = false;
_stopOverlay = CreateStopOverlay();
var tree = Godot.Engine.GetMainLoop() as SceneTree;
tree?.Root.AddChild(_stopOverlay);
var sceneManager = GetNodeOrNull<SceneManager>("/root/SceneManager");
if (sceneManager != null)
{
sceneManager.Initialize();
sceneManager.PendingSceneName = metadata.Scene;
}
var sceneService = GetNodeOrNull<GodotSceneService>("/root/GodotSceneService");
sceneService?.LoadScene("res://scenes/ui/base_scene.tscn");
_playActive = true;
}
private void OnStopPressed()
{
SceneManager.IsTestPlayActive = false;
ReturnFromPlay();
}
private void ReturnFromPlay()
{
_playActive = false;
_playLayout.Visible = false;
_editLayout.Visible = true;
Visible = true;
if (_stopOverlay != null)
{
_stopOverlay.QueueFree();
_stopOverlay = null;
}
var sceneService = GetNodeOrNull<GodotSceneService>("/root/GodotSceneService");
sceneService?.SwitchToMenu();
}
private CanvasLayer CreateStopOverlay()
{
var layer = new CanvasLayer { Layer = 128 };
var btn = new Button
{
Text = TranslationServer.Translate("tools/scenario_editor/stop"),
CustomMinimumSize = new Vector2(120, 40)
};
btn.Pressed += OnStopPressed;
var margin = new MarginContainer();
margin.AddThemeConstantOverride("margin_top", 8);
margin.AddThemeConstantOverride("margin_left", 8);
margin.AddChild(btn);
layer.AddChild(margin);
return layer;
}
}
}
+103
View File
@@ -0,0 +1,103 @@
using System;
using System.Collections.Generic;
using Cthangover.Tools.Services;
using Godot;
namespace Cthangover.Core.UI.Tool.ScenarioEditor
{
public partial class ScenarioFileTreePanel : VBoxContainer
{
private Tree _fileTree;
private readonly Dictionary<string, string> _fileItemToPath = new();
private readonly Dictionary<string, TreeItem> _pathToTreeItem = new();
private bool _isProgrammaticSelection;
public event Action<string> FileSelected;
public ScenarioFileTreePanel()
{
_fileTree = GetNode<Tree>("FileTree");
_fileTree.CellSelected += OnCellSelected;
Populate();
}
private void Populate()
{
_fileTree.Clear();
_fileItemToPath.Clear();
var root = _fileTree.CreateItem();
var mods = ScenarioFileService.ScanScenarioFiles();
foreach (var mod in mods)
{
var modItem = _fileTree.CreateItem(root);
modItem.SetText(0, $"[{mod.ModId}]");
modItem.SetSelectable(0, false);
modItem.SetCustomColor(0, new Color(0.5f, 0.75f, 1f));
foreach (var group in mod.Groups)
{
TreeItem parentItem;
if (string.IsNullOrEmpty(group.DirectoryPath))
{
parentItem = modItem;
}
else
{
parentItem = _fileTree.CreateItem(modItem);
parentItem.SetText(0, group.DirectoryPath + "/");
parentItem.SetSelectable(0, false);
}
foreach (var file in group.Files)
{
var fileItem = _fileTree.CreateItem(parentItem);
fileItem.SetText(0, file.Name);
fileItem.SetMetadata(0, Variant.From("scenario"));
_fileItemToPath[GetItemKey(fileItem)] = file.AbsolutePath;
_pathToTreeItem[file.AbsolutePath] = fileItem;
}
}
}
}
private static string GetItemKey(TreeItem item)
{
return item.GetInstanceId().ToString();
}
public void SelectFile(string absolutePath)
{
if (!_pathToTreeItem.TryGetValue(absolutePath, out var item))
return;
_isProgrammaticSelection = true;
item.Select(0);
_isProgrammaticSelection = false;
FileSelected?.Invoke(absolutePath);
}
private void OnCellSelected()
{
if (_isProgrammaticSelection)
return;
var item = _fileTree.GetSelected();
if (item == null)
return;
var meta = item.GetMetadata(0);
if (meta.VariantType != Variant.Type.String || meta.AsString() != "scenario")
return;
var key = GetItemKey(item);
if (!_fileItemToPath.TryGetValue(key, out var filePath))
return;
FileSelected?.Invoke(filePath);
}
}
}
+211
View File
@@ -0,0 +1,211 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Cthangover.Tools.Services;
using Godot;
namespace Cthangover.Core.UI.Tool.ScenarioEditor
{
public partial class ScenarioGraphView : HSplitContainer
{
private GraphEdit _graph;
private RichTextLabel _infoLabel;
private Dictionary<string, GraphNode> _nodes;
private Dictionary<string, SceneGraphNode> _lookup;
public event Action<string> 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(250, 0) };
panel.SizeFlagsVertical = SizeFlags.ExpandFill;
AddChild(panel);
panel.AddChild(new Label
{
Text = TranslationServer.Translate("tools/scenario_editor/graph_select_node"),
AutowrapMode = TextServer.AutowrapMode.WordSmart
});
_infoLabel = new RichTextLabel
{
BbcodeEnabled = true,
FitContent = true,
ScrollFollowing = true,
SizeFlagsVertical = SizeFlags.ExpandFill
};
_infoLabel.MetaClicked += OnLinkClicked;
panel.AddChild(_infoLabel);
}
public void Populate(SceneGraphData data)
{
var savedPositions = SaveNodePositions();
ClearGraph();
_nodes = new Dictionary<string, GraphNode>();
_lookup = new Dictionary<string, SceneGraphNode>();
int x = 40, y = 40;
foreach (var pos in savedPositions.Values)
{
int bottom = (int)pos.Y + 160 + 20;
if (bottom > y)
y = bottom;
}
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
: new Vector2(x, y);
var gn = new GraphNode
{
Title = nodeData.SceneName,
Size = new Vector2(240, 90),
PositionOffset = position
};
gn.AddChild(new Label
{
Text = $"Mod: {nodeData.ModId}\nScenarios: {nodeData.ScenarioCount}" +
(bgCount > 0 ? $"\nBg: {firstBg}" +
(bgCount > 1 ? $" +{bgCount - 1}" : "") : "")
});
var color = Colors.White;
if (nodeData.IsStartScene)
{
color = new Color(0.3f, 1f, 0.3f);
gn.AddThemeStyleboxOverride("titlebar", CreateTitlebarStyle(new Color(0.2f, 0.5f, 0.15f)));
}
else if (nodeData.HasErrors)
{
color = new Color(1f, 0.3f, 0.3f);
gn.AddThemeStyleboxOverride("titlebar", CreateTitlebarStyle(new Color(0.5f, 0.1f, 0.1f)));
}
gn.SetSlot(0, true, 0, Colors.White, true, 0, color);
_graph.AddChild(gn);
_nodes[key] = gn;
_lookup[key] = nodeData;
if (!savedPositions.ContainsKey(key))
{
x += 280;
if (x > 1000) { x = 40; y += 160; }
}
}
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<string, Vector2> SaveNodePositions()
{
var positions = new Dictionary<string, Vector2>();
if (_nodes == null)
return positions;
foreach (var kv in _nodes)
positions[kv.Key] = kv.Value.PositionOffset;
return positions;
}
private static StyleBoxFlat CreateTitlebarStyle(Color bgColor)
{
return new StyleBoxFlat
{
BgColor = bgColor,
CornerRadiusTopLeft = 6,
CornerRadiusTopRight = 6,
CornerRadiusBottomLeft = 0,
CornerRadiusBottomRight = 0
};
}
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($"Scene: [b]{nodeData.SceneName}[/b]");
sb.AppendLine($"Mod: {nodeData.ModId}");
if (nodeData.Backgrounds.Count > 0)
sb.AppendLine($"Backgrounds: {string.Join(", ", nodeData.Backgrounds)}");
sb.AppendLine();
sb.AppendLine($"Scenarios ({nodeData.Scenarios.Count}):");
foreach (var sc in nodeData.Scenarios.OrderBy(s => s.Priority))
{
var url = Uri.EscapeDataString(sc.FilePath);
sb.AppendLine($" [{sc.Priority}] [url={url}]{sc.Name}[/url]");
}
_infoLabel.Text = sb.ToString();
}
private void OnNodeDeselected(Node n)
{
_infoLabel.Text = "";
}
private void OnLinkClicked(Variant meta)
{
var path = Uri.UnescapeDataString(meta.AsString());
ScenarioFileRequested?.Invoke(path);
}
}
}
+62
View File
@@ -0,0 +1,62 @@
using System.Linq;
using Cthangover.Tools.Services;
using Godot;
namespace Cthangover.Core.UI.Tool.ScenarioEditor
{
public partial class ScenarioInfoPanel : VBoxContainer
{
private Label _infoScene;
private Label _infoPriority;
private Label _infoCondition;
private Label _infoSwitchTargets;
private Label _infoLocaleKeys;
private Label _infoQuestRefs;
public ScenarioInfoPanel()
{
GetNode<Label>("Scroll/VBox/HintScene").Text = TranslationServer.Translate("tools/scenario_editor/info_scene") + ":";
GetNode<Label>("Scroll/VBox/HintPriority").Text = TranslationServer.Translate("tools/scenario_editor/info_priority") + ":";
GetNode<Label>("Scroll/VBox/HintCondition").Text = TranslationServer.Translate("tools/scenario_editor/info_condition") + ":";
GetNode<Label>("Scroll/VBox/HintSwitchTargets").Text = TranslationServer.Translate("tools/scenario_editor/info_switch_targets") + ":";
GetNode<Label>("Scroll/VBox/HintLocaleKeys").Text = TranslationServer.Translate("tools/scenario_editor/info_locale_keys") + ":";
GetNode<Label>("Scroll/VBox/HintQuestRefs").Text = TranslationServer.Translate("tools/scenario_editor/info_quest_refs") + ":";
_infoScene = GetNode<Label>("Scroll/VBox/InfoScene");
_infoPriority = GetNode<Label>("Scroll/VBox/InfoPriority");
_infoCondition = GetNode<Label>("Scroll/VBox/InfoCondition");
_infoSwitchTargets = GetNode<Label>("Scroll/VBox/InfoSwitchTargets");
_infoLocaleKeys = GetNode<Label>("Scroll/VBox/InfoLocaleKeys");
_infoQuestRefs = GetNode<Label>("Scroll/VBox/InfoQuestRefs");
}
public void Update(string text)
{
if (string.IsNullOrEmpty(text))
{
Clear();
return;
}
var metadata = ScenarioFileService.ParseMetadata(text);
var refs = ScenarioFileService.ExtractReferences(text);
_infoScene.Text = metadata.Scene;
_infoPriority.Text = metadata.Priority;
_infoCondition.Text = metadata.Condition;
_infoSwitchTargets.Text = refs.SwitchTargets.Count > 0 ? string.Join(", ", refs.SwitchTargets.Distinct()) : "-";
_infoLocaleKeys.Text = refs.LocaleKeys.Count > 0 ? string.Join(", ", refs.LocaleKeys.Distinct()) : "-";
_infoQuestRefs.Text = refs.QuestRefs.Count > 0 ? string.Join(", ", refs.QuestRefs.Distinct()) : "-";
}
public void Clear()
{
_infoScene.Text = "-";
_infoPriority.Text = "-";
_infoCondition.Text = "-";
_infoSwitchTargets.Text = "-";
_infoLocaleKeys.Text = "-";
_infoQuestRefs.Text = "-";
}
}
}
@@ -0,0 +1,66 @@
using System.Linq;
using Cthangover.Tools.Services;
using Godot;
namespace Cthangover.Core.UI.Tool.ScenarioEditor
{
public partial class ScenarioThumbnailPanel : ScrollContainer
{
private HBoxContainer _thumbContainer;
public ScenarioThumbnailPanel()
{
_thumbContainer = GetNode<HBoxContainer>("ThumbContainer");
}
public void Load(string text)
{
Clear();
if (string.IsNullOrEmpty(text))
return;
var refs = ScenarioFileService.ExtractReferences(text);
if (refs.BackgroundRefs.Count == 0)
{
_thumbContainer.AddChild(new Label
{
Text = TranslationServer.Translate("tools/scenario_editor/no_backgrounds")
});
return;
}
foreach (var id in refs.BackgroundRefs.Distinct())
{
var tex = ModResourceService.LoadBackgroundTexture(id);
if (tex != null)
{
_thumbContainer.AddChild(new TextureRect
{
Texture = tex,
ExpandMode = TextureRect.ExpandModeEnum.IgnoreSize,
StretchMode = TextureRect.StretchModeEnum.KeepAspectCentered,
CustomMinimumSize = new Vector2(120, 90),
TooltipText = id
});
}
else
{
_thumbContainer.AddChild(new Label
{
Text = $"[? {id}]",
TooltipText = $"Not found: {id}"
});
}
}
}
public new void Clear()
{
foreach (var c in _thumbContainer.GetChildren())
c.QueueFree();
}
}
}