You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

255 lines
6.7 KiB
C#

#if TOOLS
#nullable enable
using Godot;
using System;
using System.Linq;
namespace GodotInk;
[Tool]
public partial class InkDock : VBoxContainer
{
private static readonly StringName CHOICE_INDEX_META = "Index";
private Button loadButton = null!;
private Button startButton = null!;
private Button stopButton = null!;
private Button clearButton = null!;
private Label storyNameLabel = null!;
private VBoxContainer storyText = null!;
private VBoxContainer storyChoices = null!;
private ScrollContainer scroll = null!;
private EditorFileDialog fileDialog = null!;
private string storyPath = "";
private InkStory? story;
private bool storyStarted;
private string backupSave = "";
public override void _Ready()
{
base._Ready();
CustomMinimumSize = new Vector2(0.0f, 228.0f);
// Initialize file dialog.
fileDialog = new()
{
FileMode = EditorFileDialog.FileModeEnum.OpenFile,
Access = EditorFileDialog.AccessEnum.Resources,
};
fileDialog.AddFilter("*.ink", "Ink Stories");
fileDialog.FileSelected += LoadStory;
AddChild(fileDialog);
// Initialize top.
loadButton = GetNode<Button>("Container/Left/Top/LoadButton");
storyNameLabel = GetNode<Label>("Container/Left/Top/Label");
startButton = GetNode<Button>("Container/Left/Top/StartButton");
stopButton = GetNode<Button>("Container/Left/Top/StopButton");
clearButton = GetNode<Button>("Container/Left/Top/ClearButton");
// Connect UI events.
loadButton.Pressed += () => fileDialog.PopupCenteredClamped(new Vector2I(1050, 700), 0.8f);
startButton.Pressed += StartStory;
stopButton.Pressed += StopStory;
clearButton.Pressed += () => ClearStory(false);
// Initialize bottom.
storyText = GetNode<VBoxContainer>("Container/Left/Scroll/Margin/StoryText");
storyChoices = GetNode<VBoxContainer>("Container/Right/StoryChoices");
scroll = GetNode<ScrollContainer>("Container/Left/Scroll");
// Set icons.
loadButton.Icon = GetThemeIcon("Load", "EditorIcons");
startButton.Icon = GetThemeIcon("Play", "EditorIcons");
stopButton.Icon = GetThemeIcon("Stop", "EditorIcons");
clearButton.Icon = GetThemeIcon("Clear", "EditorIcons");
// Update UI.
UpdateTop();
}
private void UpdateTop()
{
bool hasStory = story != null;
storyNameLabel.Text = hasStory ? storyPath : string.Empty;
startButton.Visible = hasStory && !storyStarted;
stopButton.Visible = hasStory && storyStarted;
clearButton.Visible = hasStory;
clearButton.Disabled = storyText.GetChildCount() <= 0;
storyChoices.GetParent<Control>().Visible = hasStory;
}
private void LoadStory(string path)
{
try
{
storyPath = path;
story = ResourceLoader.Load<InkStory>(path, null, ResourceLoader.CacheMode.Ignore);
UpdateTop();
}
catch (InvalidCastException)
{
GD.PrintErr($"{path} is not a valid ink story. "
+ "Please make sure it was imported with `is_main_file` set to `true`.");
}
}
private void StartStory()
{
if (story == null) return;
storyStarted = true;
ContinueStory();
UpdateTop();
}
private void StopStory()
{
StopStory(false);
}
private void StopStory(bool setStoryToNull)
{
storyStarted = false;
try
{
story?.ResetState();
}
catch (ObjectDisposedException)
{
story = null;
}
if (setStoryToNull)
story = null;
ClearStory(true);
}
private void ClearStory(bool clearChoices)
{
RemoveAllStoryContent();
if (clearChoices)
RemoveAllChoices();
UpdateTop();
}
private void ContinueStory()
{
if (story == null) return;
if (!story.CanContinue) return;
string currentText = story.ContinueMaximally().Trim();
if (currentText.Length > 0)
{
Label newLine = new()
{
AutowrapMode = TextServer.AutowrapMode.WordSmart,
Text = currentText,
};
AddToStory(newLine);
if (story.CurrentTags.Count > 0)
{
newLine = new Label()
{
AutowrapMode = TextServer.AutowrapMode.WordSmart,
HorizontalAlignment = HorizontalAlignment.Center,
Text = $"# {string.Join(", ", story.CurrentTags)}",
};
newLine.AddThemeColorOverride("font_color", GetThemeColor("font_color_disabled", "Button"));
AddToStory(newLine);
}
}
foreach (InkChoice choice in story.CurrentChoices)
{
Button button = new() { Text = choice.Text };
button.SetMeta(CHOICE_INDEX_META, choice.Index);
button.Connect(Button.SignalName.Pressed, Callable.From(ClickChoice));
storyChoices.AddChild(button);
}
backupSave = storyStarted ? story.SaveState() : "";
}
private void ClickChoice()
{
if (storyChoices.GetChildren().OfType<Button>().First(button => button.ButtonPressed) is not Button button) return;
if (!button.HasMeta(CHOICE_INDEX_META)) return;
try
{
ClickChoice(button.GetMeta(CHOICE_INDEX_META).As<int>());
}
catch
{
story?.LoadState(backupSave);
try
{
ClickChoice(button.GetMeta(CHOICE_INDEX_META).As<int>());
}
catch
{
StopStory(true);
}
}
}
private void ClickChoice(int idx)
{
if (story == null) return;
story.ChooseChoiceIndex(idx);
RemoveAllChoices();
AddToStory(new HSeparator());
ContinueStory();
}
private async void AddToStory(CanvasItem item)
{
storyText.AddChild(item);
await ToSignal(GetTree(), "process_frame");
await ToSignal(GetTree(), "process_frame");
scroll.ScrollVertical = (int)scroll.GetVScrollBar().MaxValue;
}
private void RemoveAllStoryContent()
{
foreach (Node n in storyText.GetChildren())
n.QueueFree();
}
private void RemoveAllChoices()
{
foreach (Node n in storyChoices.GetChildren().OfType<Button>())
n.QueueFree();
}
public void WhenInkResourceReimported()
{
StopStory(true);
}
}
#endif