init
This commit is contained in:
commit
8e6ad52ee4
44 files changed
+6842
No files matched your search
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user