chore: Project Restructuring & Theatre
This commit is contained in:
@@ -0,0 +1,367 @@
|
||||
using UnityEngine;
|
||||
using System.Collections.Generic;
|
||||
using OctoCore.Localization;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
using System.IO;
|
||||
#endif
|
||||
|
||||
namespace Unheimlich.Features.Dialogue.Data
|
||||
{
|
||||
public static class DialogueConversationBuilder
|
||||
{
|
||||
// Helper methods kept for compatibility but now create localized text data
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
public class DialogueCreatorWindow : EditorWindow
|
||||
{
|
||||
private string _speakerName = "NPC";
|
||||
private string _savePath = "Assets/Unheimlich_POC/Datas/DialogueData";
|
||||
private string _conversationName = "NewConversation";
|
||||
|
||||
private List<NodeTemplate> _nodes = new List<NodeTemplate>();
|
||||
private Vector2 _scrollPosition;
|
||||
|
||||
[System.Serializable]
|
||||
private class NodeTemplate
|
||||
{
|
||||
public string NodeText = "Dialogue text";
|
||||
public List<ChoiceTemplate> Choices = new List<ChoiceTemplate>
|
||||
{
|
||||
new ChoiceTemplate { Text = "Choice 1", TargetNodeIndex = 1 },
|
||||
new ChoiceTemplate { Text = "Choice 2", TargetNodeIndex = 1 }
|
||||
};
|
||||
public bool IsEndNode = false;
|
||||
public bool Foldout = true;
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
private class ChoiceTemplate
|
||||
{
|
||||
public string Text = "New choice";
|
||||
public int TargetNodeIndex = 1;
|
||||
}
|
||||
|
||||
[MenuItem("Unheimlich/Tools/Dialogue/Dialogue Creator")]
|
||||
public static void ShowWindow()
|
||||
{
|
||||
var window = GetWindow<DialogueCreatorWindow>("Dialogue Creator");
|
||||
window.minSize = new Vector2(500, 600);
|
||||
window.Show();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (_nodes.Count == 0)
|
||||
{
|
||||
_nodes.Add(new NodeTemplate
|
||||
{
|
||||
NodeText = "Hello! Starting dialogue",
|
||||
Choices = new List<ChoiceTemplate>
|
||||
{
|
||||
new ChoiceTemplate { Text = "Option 1", TargetNodeIndex = 1 },
|
||||
new ChoiceTemplate { Text = "Option 2", TargetNodeIndex = 1 }
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
EditorGUILayout.Space(10);
|
||||
EditorGUILayout.LabelField("Dialogue Creator", EditorStyles.boldLabel);
|
||||
EditorGUILayout.HelpBox("Quickly create a complete conversation with all nodes pre-linked!\nTexts will be created as localized assets (you can edit FR/EN later in Inspector).", MessageType.Info);
|
||||
|
||||
EditorGUILayout.Space(10);
|
||||
|
||||
EditorGUILayout.BeginVertical("box");
|
||||
EditorGUILayout.LabelField("General Configuration", EditorStyles.boldLabel);
|
||||
|
||||
_speakerName = EditorGUILayout.TextField("Speaker Name", _speakerName);
|
||||
_conversationName = EditorGUILayout.TextField("Conversation Name", _conversationName);
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
EditorGUILayout.LabelField("Save Path", GUILayout.Width(150));
|
||||
_savePath = EditorGUILayout.TextField(_savePath);
|
||||
if (GUILayout.Button("...", GUILayout.Width(30)))
|
||||
{
|
||||
string newPath = EditorUtility.OpenFolderPanel("Choose Folder", _savePath, "");
|
||||
if (!string.IsNullOrEmpty(newPath))
|
||||
{
|
||||
if (newPath.StartsWith(Application.dataPath))
|
||||
_savePath = "Assets" + newPath.Substring(Application.dataPath.Length);
|
||||
}
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
EditorGUILayout.EndVertical();
|
||||
|
||||
EditorGUILayout.Space(10);
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
EditorGUILayout.LabelField("Dialogue Structure", EditorStyles.boldLabel);
|
||||
if (GUILayout.Button("+ Add Node", GUILayout.Width(150)))
|
||||
{
|
||||
NodeTemplate newNode = new NodeTemplate
|
||||
{
|
||||
NodeText = $"Node {_nodes.Count + 1}",
|
||||
Choices = new List<ChoiceTemplate>
|
||||
{
|
||||
new ChoiceTemplate
|
||||
{
|
||||
Text = "Choice 1",
|
||||
TargetNodeIndex = Mathf.Min(_nodes.Count + 1, _nodes.Count)
|
||||
}
|
||||
}
|
||||
};
|
||||
_nodes.Add(newNode);
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
EditorGUILayout.Space(5);
|
||||
|
||||
_scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition, GUILayout.ExpandHeight(true));
|
||||
|
||||
for (int i = 0; i < _nodes.Count; i++)
|
||||
{
|
||||
DrawNodeEditor(i);
|
||||
}
|
||||
|
||||
EditorGUILayout.EndScrollView();
|
||||
|
||||
EditorGUILayout.Space(10);
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.FlexibleSpace();
|
||||
|
||||
if (GUILayout.Button("Create Conversation", GUILayout.Height(40), GUILayout.Width(200)))
|
||||
{
|
||||
CreateDialogue();
|
||||
}
|
||||
|
||||
GUILayout.FlexibleSpace();
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
EditorGUILayout.Space(10);
|
||||
}
|
||||
|
||||
private void DrawNodeEditor(int index)
|
||||
{
|
||||
NodeTemplate node = _nodes[index];
|
||||
|
||||
EditorGUILayout.BeginVertical("box");
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
node.Foldout = EditorGUILayout.Foldout(node.Foldout, $"Node {index + 1}" + (index == 0 ? " (START)" : ""), true);
|
||||
|
||||
GUILayout.FlexibleSpace();
|
||||
|
||||
if (_nodes.Count > 1 && GUILayout.Button("X", GUILayout.Width(25)))
|
||||
{
|
||||
_nodes.RemoveAt(index);
|
||||
return;
|
||||
}
|
||||
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
if (node.Foldout)
|
||||
{
|
||||
EditorGUI.indentLevel++;
|
||||
|
||||
EditorGUILayout.LabelField("Dialogue Text", EditorStyles.miniBoldLabel);
|
||||
node.NodeText = EditorGUILayout.TextArea(node.NodeText, GUILayout.Height(60));
|
||||
|
||||
EditorGUILayout.Space(5);
|
||||
|
||||
bool isLastNode = index == _nodes.Count - 1;
|
||||
|
||||
if (isLastNode)
|
||||
{
|
||||
EditorGUILayout.HelpBox("Last node = End of conversation (no choices)", MessageType.Info);
|
||||
node.IsEndNode = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorGUILayout.LabelField("Available Choices", EditorStyles.miniBoldLabel);
|
||||
|
||||
for (int c = 0; c < node.Choices.Count; c++)
|
||||
{
|
||||
ChoiceTemplate choice = node.Choices[c];
|
||||
|
||||
EditorGUILayout.BeginVertical("box");
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
EditorGUILayout.LabelField($"Choice {c + 1}", EditorStyles.boldLabel, GUILayout.Width(60));
|
||||
|
||||
if (GUILayout.Button("-", GUILayout.Width(25)))
|
||||
{
|
||||
node.Choices.RemoveAt(c);
|
||||
break;
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
EditorGUILayout.LabelField("Choice Text:", GUILayout.Width(100));
|
||||
choice.Text = EditorGUILayout.TextField(choice.Text);
|
||||
|
||||
EditorGUILayout.Space(3);
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
EditorGUILayout.LabelField("Target:", GUILayout.Width(100));
|
||||
|
||||
string[] nodeOptions = new string[_nodes.Count];
|
||||
for (int n = 0; n < _nodes.Count; n++)
|
||||
{
|
||||
if (n == index)
|
||||
nodeOptions[n] = $"Node {n + 1} (SELF - Loop)";
|
||||
else if (n == _nodes.Count - 1)
|
||||
nodeOptions[n] = $"Node {n + 1} (END)";
|
||||
else
|
||||
nodeOptions[n] = $"Node {n + 1}";
|
||||
}
|
||||
|
||||
choice.TargetNodeIndex = Mathf.Clamp(choice.TargetNodeIndex, 0, _nodes.Count - 1);
|
||||
|
||||
int selectedIndex = EditorGUILayout.Popup(choice.TargetNodeIndex, nodeOptions);
|
||||
choice.TargetNodeIndex = selectedIndex;
|
||||
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
if (choice.TargetNodeIndex == index)
|
||||
EditorGUILayout.HelpBox("! Loop detected: this choice returns to the same node", MessageType.Warning);
|
||||
else if (choice.TargetNodeIndex < index)
|
||||
EditorGUILayout.HelpBox("↩ Going back to Node " + (choice.TargetNodeIndex + 1), MessageType.Info);
|
||||
|
||||
EditorGUILayout.EndVertical();
|
||||
EditorGUILayout.Space(3);
|
||||
}
|
||||
|
||||
if (GUILayout.Button("+ Add Choice"))
|
||||
{
|
||||
node.Choices.Add(new ChoiceTemplate
|
||||
{
|
||||
Text = $"New choice {node.Choices.Count + 1}",
|
||||
TargetNodeIndex = Mathf.Min(index + 1, _nodes.Count - 1)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
|
||||
EditorGUILayout.EndVertical();
|
||||
EditorGUILayout.Space(5);
|
||||
}
|
||||
|
||||
private OctoLocalizedTextData CreateLocalizedTextData(string text, string basePath, string suffix)
|
||||
{
|
||||
OctoLocalizedTextData textData = ScriptableObject.CreateInstance<OctoLocalizedTextData>();
|
||||
textData.Init();
|
||||
|
||||
// Set English text by default
|
||||
textData.Languages.Dictionary["English"] = text;
|
||||
textData.Languages.Dictionary["French"] = ""; // Empty, user will fill later
|
||||
|
||||
string textDataPath = AssetDatabase.GenerateUniqueAssetPath($"{basePath}/text_{suffix}.asset");
|
||||
AssetDatabase.CreateAsset(textData, textDataPath);
|
||||
|
||||
return textData;
|
||||
}
|
||||
|
||||
private void CreateDialogue()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_speakerName) || string.IsNullOrEmpty(_conversationName))
|
||||
{
|
||||
EditorUtility.DisplayDialog("Error", "Speaker name and conversation name are required!", "OK");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!AssetDatabase.IsValidFolder(_savePath))
|
||||
{
|
||||
EditorUtility.DisplayDialog("Error", "Save path is not valid!", "OK");
|
||||
return;
|
||||
}
|
||||
|
||||
string folderPath = $"{_savePath}/{_conversationName}";
|
||||
if (!AssetDatabase.IsValidFolder(folderPath))
|
||||
{
|
||||
string parentFolder = _savePath;
|
||||
AssetDatabase.CreateFolder(parentFolder, _conversationName);
|
||||
}
|
||||
|
||||
// Create speaker name localized text
|
||||
OctoLocalizedTextData speakerNameData = CreateLocalizedTextData(_speakerName, folderPath, "SpeakerName");
|
||||
|
||||
List<DialogueNode> createdNodes = new List<DialogueNode>();
|
||||
|
||||
// Create nodes
|
||||
for (int i = 0; i < _nodes.Count; i++)
|
||||
{
|
||||
NodeTemplate template = _nodes[i];
|
||||
DialogueNode node = CreateInstance<DialogueNode>();
|
||||
node.name = $"Node_{i + 1}";
|
||||
|
||||
// Create localized text for dialogue
|
||||
node.DialogueText = CreateLocalizedTextData(template.NodeText, folderPath, $"Node{i + 1}_Text");
|
||||
|
||||
string nodePath = $"{folderPath}/{node.name}.asset";
|
||||
AssetDatabase.CreateAsset(node, nodePath);
|
||||
createdNodes.Add(node);
|
||||
}
|
||||
|
||||
// Link choices
|
||||
for (int i = 0; i < _nodes.Count; i++)
|
||||
{
|
||||
NodeTemplate template = _nodes[i];
|
||||
DialogueNode currentNode = createdNodes[i];
|
||||
|
||||
foreach (ChoiceTemplate choiceTemplate in template.Choices)
|
||||
{
|
||||
int targetIndex = Mathf.Clamp(choiceTemplate.TargetNodeIndex, 0, createdNodes.Count - 1);
|
||||
|
||||
// Create localized text for choice
|
||||
OctoLocalizedTextData choiceTextData = CreateLocalizedTextData(
|
||||
choiceTemplate.Text,
|
||||
folderPath,
|
||||
$"Node{i + 1}_Choice{template.Choices.IndexOf(choiceTemplate)}"
|
||||
);
|
||||
|
||||
DialogueChoiceData choice = new DialogueChoiceData
|
||||
{
|
||||
ChoiceText = choiceTextData,
|
||||
NextNode = createdNodes[targetIndex]
|
||||
};
|
||||
currentNode.Choices.Add(choice);
|
||||
}
|
||||
|
||||
EditorUtility.SetDirty(currentNode);
|
||||
}
|
||||
|
||||
// Create conversation
|
||||
DialogueConversation conversation = CreateInstance<DialogueConversation>();
|
||||
conversation.SpeakerName = speakerNameData;
|
||||
conversation.name = _conversationName;
|
||||
conversation.DialogueNodes = createdNodes;
|
||||
|
||||
string conversationPath = $"{folderPath}/{_conversationName}.asset";
|
||||
AssetDatabase.CreateAsset(conversation, conversationPath);
|
||||
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
|
||||
EditorGUIUtility.PingObject(conversation);
|
||||
Selection.activeObject = conversation;
|
||||
|
||||
EditorUtility.DisplayDialog(
|
||||
"Success",
|
||||
$"Conversation '{_conversationName}' created with {_nodes.Count} nodes!\n\n" +
|
||||
$"Path: {folderPath}\n\n" +
|
||||
$"📝 Note: All texts are in English by default.\n" +
|
||||
$"Open each DialogueNode in the Inspector to add French translations!",
|
||||
"OK"
|
||||
);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
Reference in New Issue
Block a user