chore: Project Restructuring & Theatre
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unheimlich.Features.Dialogue.Data
|
||||
{
|
||||
/// <summary>
|
||||
/// ScriptableObject configuration for dialogue choice spawn behavior and animations.
|
||||
/// Defines spawn patterns, floating effects, rotation, distance scaling, and focus behavior.
|
||||
/// </summary>
|
||||
[CreateAssetMenu(fileName = "DialogueAnimationSettings", menuName = "Unheimlich/Animation/Dialogue Animation Settings")]
|
||||
public class DialogueAnimationSettings : ScriptableObject
|
||||
{
|
||||
[Header("Choice Spawn Animation")]
|
||||
[Tooltip("How choices appear in the world")]
|
||||
public ChoiceSpawnMode SpawnMode = ChoiceSpawnMode.AroundAnchor;
|
||||
|
||||
[Tooltip("Default scale multiplier for spawned choices (1.0 = normal size)")]
|
||||
public float DefaultScaleMultiplier = 1.0f;
|
||||
|
||||
[Tooltip("Minimum scale variation multiplier (e.g., 0.5 = 50% of base scale)")]
|
||||
[Range(0.1f, 2f)]
|
||||
public float MinScaleVariation = 0.5f;
|
||||
|
||||
[Tooltip("Maximum scale variation multiplier (e.g., 1.5 = 150% of base scale)")]
|
||||
[Range(0.1f, 2f)]
|
||||
public float MaxScaleVariation = 1.5f;
|
||||
|
||||
[Tooltip("Enable random scale variation for each word")]
|
||||
public bool EnableScaleVariation = false;
|
||||
|
||||
[Header("Spawn Settings")]
|
||||
[Tooltip("Radius around anchor where choices spawn")]
|
||||
public float SpawnRadius = 2f;
|
||||
|
||||
[Tooltip("Height offset from anchor")]
|
||||
public float SpawnHeightOffset = 0f;
|
||||
|
||||
[Tooltip("Should choices face the player?")]
|
||||
public bool FacePlayer = true;
|
||||
|
||||
[Header("Animation")]
|
||||
[Tooltip("Duration of spawn animation")]
|
||||
public float SpawnAnimationDuration = 0.5f;
|
||||
|
||||
[Tooltip("Spawn animation curve")]
|
||||
public AnimationCurve SpawnCurve = AnimationCurve.EaseInOut(0, 0, 1, 1);
|
||||
|
||||
[Header("Idle Behavior")]
|
||||
[Tooltip("Should choices float vertically?")]
|
||||
public bool EnableFloating = true;
|
||||
|
||||
[Tooltip("Floating amplitude")]
|
||||
public float FloatingAmplitude = 0.2f;
|
||||
|
||||
[Tooltip("Floating duration (full cycle)")]
|
||||
public float FloatingDuration = 3f;
|
||||
|
||||
[Header("Organic Shake")]
|
||||
[Tooltip("Enable organic shake/drift movement around center point (Heavy Rain style)")]
|
||||
public bool EnableOrganicShake = false;
|
||||
|
||||
[Tooltip("Radius around center point where choices can drift")]
|
||||
public float ShakeRadius = 0.15f;
|
||||
|
||||
[Tooltip("Speed of the shake movement")]
|
||||
public float ShakeSpeed = 1.5f;
|
||||
|
||||
[Tooltip("Smoothness of shake transitions (higher = smoother)")]
|
||||
public float ShakeSmoothness = 2f;
|
||||
|
||||
[Tooltip("Shake speed multiplier for unfocused choices when one is focused (0.3 = 30% speed)")]
|
||||
[Range(0f, 1f)]
|
||||
public float UnfocusedShakeSpeedMultiplier = 0.3f;
|
||||
|
||||
[Header("Ghost Trail Effect")]
|
||||
[Tooltip("Enable ghost/shadow trail effect behind text (Heavy Rain style)")]
|
||||
public bool EnableGhostTrail = false;
|
||||
|
||||
[Tooltip("Time delay for ghost trail in seconds (how far behind the ghost follows)")]
|
||||
public float GhostTrailDelay = 0.15f;
|
||||
|
||||
[Tooltip("Transparency of the ghost trail (0 = invisible, 1 = opaque)")]
|
||||
[Range(0f, 1f)]
|
||||
public float GhostTrailAlpha = 0.3f;
|
||||
|
||||
[Tooltip("Should container rotate continuously?")]
|
||||
public bool EnableRotation = true;
|
||||
|
||||
[Tooltip("Axis of rotation (Forward = Z, Up = Y, Right = X)")]
|
||||
public RotationAxis RotationAxis = RotationAxis.Forward;
|
||||
|
||||
[Tooltip("Normal rotation speed when not focused (degrees per second). Higher = faster rotation.")]
|
||||
public float NormalRotationSpeed = 36f;
|
||||
|
||||
[Header("Follow Player")]
|
||||
[Tooltip("Should container follow player position?")]
|
||||
public bool FollowPlayerPosition = false;
|
||||
|
||||
[Tooltip("Should container follow player Y rotation? (keeps choices in front of player)")]
|
||||
public bool FollowPlayerRotation = false;
|
||||
|
||||
[Header("Distance-Based Scaling")]
|
||||
[Tooltip("Should scale and radius adjust based on distance to player?")]
|
||||
public bool EnableDistanceScaling = false;
|
||||
|
||||
[Tooltip("Minimum distance for scaling calculation")]
|
||||
public float MinDistance = 2f;
|
||||
|
||||
[Tooltip("Maximum distance for scaling calculation")]
|
||||
public float MaxDistance = 10f;
|
||||
|
||||
[Tooltip("Scale multiplier at minimum distance")]
|
||||
public float MinScaleMultiplier = 1f;
|
||||
|
||||
[Tooltip("Scale multiplier at maximum distance")]
|
||||
public float MaxScaleMultiplier = 3f;
|
||||
|
||||
[Header("Focus Animation")]
|
||||
[Tooltip("Should choices move toward player when focused?")]
|
||||
public bool MoveTowardPlayerOnFocus = true;
|
||||
|
||||
[Tooltip("Distance from player when focused")]
|
||||
public float FocusDistanceFromPlayer = 1.5f;
|
||||
|
||||
[Tooltip("Rotation speed when a choice is focused (degrees per second). Higher = faster rotation.")]
|
||||
public float FocusRotationSpeed = 60f;
|
||||
|
||||
[Tooltip("Enable camera shake when focusing on a choice")]
|
||||
public bool EnableCameraShakeOnFocus = true;
|
||||
|
||||
[Tooltip("Camera shake intensity when focusing (higher = less shake, lower = more shake)")]
|
||||
public float CameraShakeIntensity = 30f;
|
||||
|
||||
[Tooltip("Camera shake smoothness (0 = instant/jarring, higher = smoother). Controls how fast the camera moves to shake position.")]
|
||||
public float CameraShakeSmoothness = 10f;
|
||||
|
||||
[Tooltip("Scale down multiplier for the FOCUSED choice text (0.9 = 90% size)")]
|
||||
[Range(0f, 1f)]
|
||||
public float FocusedTextScaleMultiplier = 0.9f;
|
||||
|
||||
[Tooltip("Duration of the text scale animation when focusing (in seconds)")]
|
||||
public float FocusedTextScaleDuration = 0.3f;
|
||||
|
||||
[Tooltip("Duration of the text scale reset animation when unfocusing (in seconds)")]
|
||||
public float UnfocusedTextScaleDuration = 0.3f;
|
||||
|
||||
[Header("Sketch & Fade on Selection")]
|
||||
[Tooltip("Enable sketch and fade effect on non-selected words when a choice is focused")]
|
||||
public bool EnableSketchFadeOnFocus = false;
|
||||
|
||||
[Tooltip("Material with sketch shader for TextMeshPro")]
|
||||
public Material SketchMaterial;
|
||||
|
||||
[Tooltip("Duration of the sketch effect before fade (in seconds)")]
|
||||
public float SketchDuration = 0.3f;
|
||||
|
||||
[Tooltip("Duration of the fade out after sketch (in seconds)")]
|
||||
public float FadeOutDuration = 0.5f;
|
||||
|
||||
[Tooltip("Intensity of the sketch effect (0 = no sketch, 1 = full sketch)")]
|
||||
[Range(0f, 1f)]
|
||||
public float SketchIntensity = 0.8f;
|
||||
|
||||
[Tooltip("Final alpha value after fade (0 = invisible, 1 = opaque)")]
|
||||
[Range(0f, 1f)]
|
||||
public float FinalFadeAlpha = 0.2f;
|
||||
}
|
||||
|
||||
public enum ChoiceSpawnMode
|
||||
{
|
||||
AroundAnchor,
|
||||
AtAnchorPosition,
|
||||
InFrontOfPlayer,
|
||||
CircleAroundPlayer
|
||||
}
|
||||
|
||||
public enum RotationAxis
|
||||
{
|
||||
Forward,
|
||||
Up,
|
||||
Right
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1aec9b5da9003c646be8289148a4a405
|
||||
@@ -0,0 +1,40 @@
|
||||
using UnityEngine;
|
||||
using OctoCore.Localization;
|
||||
|
||||
namespace Unheimlich.Features.Dialogue.Data
|
||||
{
|
||||
[System.Serializable]
|
||||
public class DialogueChoiceData
|
||||
{
|
||||
[Header("Choice Content")]
|
||||
[Tooltip("Localized text displayed on the choice button")]
|
||||
public OctoLocalizedTextData ChoiceText;
|
||||
|
||||
[HideInInspector]
|
||||
public string ChoiceTextOverride;
|
||||
|
||||
[Header("Flow")]
|
||||
[Tooltip("Direct reference to the next DialogueNode")]
|
||||
public DialogueNode NextNode;
|
||||
|
||||
[Header("Conditions (Optional)")]
|
||||
[Tooltip("Required condition to display this choice (optional, for future use)")]
|
||||
public string RequiredCondition;
|
||||
|
||||
public string GetLocalizedChoiceText()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(ChoiceTextOverride))
|
||||
return ChoiceTextOverride;
|
||||
|
||||
if (ChoiceText != null)
|
||||
return OctoLocalization.GetLocalizedText(ChoiceText);
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public bool IsValid()
|
||||
{
|
||||
return ChoiceText != null && NextNode != null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6a3b0ea4c9c5bec419eac7026710bfc9
|
||||
@@ -0,0 +1,35 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using OctoCore.Localization;
|
||||
|
||||
namespace Unheimlich.Features.Dialogue.Data
|
||||
{
|
||||
[CreateAssetMenu(fileName = "NewConversation", menuName = "Unheimlich/Dialogue/Conversation")]
|
||||
public class DialogueConversation : ScriptableObject
|
||||
{
|
||||
[Header("Speaker Information")]
|
||||
[Tooltip("Localized name of the speaker (common to all texts in this conversation)")]
|
||||
public OctoLocalizedTextData SpeakerName;
|
||||
|
||||
[Header("Display Mode")]
|
||||
[Tooltip("Choose how to display dialogue text: World (3D space) or Screen (Canvas UI)")]
|
||||
public DialogueDisplayMode DisplayMode = DialogueDisplayMode.World;
|
||||
|
||||
[Header("Dialogue Flow")]
|
||||
[Tooltip("Ordered list of dialogue nodes for this conversation")]
|
||||
public List<DialogueNode> DialogueNodes = new List<DialogueNode>();
|
||||
|
||||
public string GetLocalizedSpeakerName()
|
||||
{
|
||||
if (SpeakerName != null)
|
||||
return OctoLocalization.GetLocalizedText(SpeakerName);
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public DialogueNode GetFirstNode()
|
||||
{
|
||||
return DialogueNodes != null && DialogueNodes.Count > 0 ? DialogueNodes[0] : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 83cc2feccc2da50418d37dfef21a2e99
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 863ff46c7eaea694e97b5e51782b7119
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Unheimlich.Features.Dialogue.Data
|
||||
{
|
||||
public enum DialogueDisplayMode
|
||||
{
|
||||
World,
|
||||
Screen
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b10588959f3745441b157488d7a8f6e9
|
||||
@@ -0,0 +1,54 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using OctoCore.Localization;
|
||||
using Unheimlich.Features.DetectiveBoard.Data;
|
||||
|
||||
namespace Unheimlich.Features.Dialogue.Data
|
||||
{
|
||||
[CreateAssetMenu(fileName = "NewDialogueNode", menuName = "Unheimlich/Dialogue/Dialogue Node")]
|
||||
public class DialogueNode : ScriptableObject
|
||||
{
|
||||
[Header("Content")]
|
||||
[Tooltip("Localized text displayed for this dialogue node")]
|
||||
public OctoLocalizedTextData DialogueText;
|
||||
|
||||
[Header("Choices")]
|
||||
[Tooltip("List of available choices for this dialogue. If empty, this is an end node")]
|
||||
public List<DialogueChoiceData> Choices = new List<DialogueChoiceData>();
|
||||
|
||||
[Header("End Node Settings")]
|
||||
[Tooltip("For end nodes (no choices), delay in seconds before automatic close. 0 = requires player interaction to close")]
|
||||
public float AutoCloseDelay = 3f;
|
||||
|
||||
[Header("Detective Board Integration")]
|
||||
[Tooltip("Clue to unlock when this node is reached. Useful for end nodes to unlock evidence after a conversation")]
|
||||
public ClueData ClueToUnlock;
|
||||
|
||||
public string GetLocalizedDialogueText()
|
||||
{
|
||||
if (DialogueText != null)
|
||||
return OctoLocalization.GetLocalizedText(DialogueText);
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public bool IsEndNode()
|
||||
{
|
||||
return Choices == null || Choices.Count == 0;
|
||||
}
|
||||
|
||||
public bool HasValidChoices()
|
||||
{
|
||||
if (Choices == null || Choices.Count == 0)
|
||||
return false;
|
||||
|
||||
foreach (var choice in Choices)
|
||||
{
|
||||
if (choice != null && choice.NextNode != null)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c5adec41ffe2e0f4d83e2e75e31781d6
|
||||
@@ -0,0 +1,123 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unheimlich.Features.Dialogue.Data
|
||||
{
|
||||
[CreateAssetMenu(fileName = "NewObjectDescriptionSettings", menuName = "Unheimlich/Animation/Object Description Settings")]
|
||||
public class ObjectDescriptionSettings : ScriptableObject
|
||||
{
|
||||
[Header("Spawn Settings")]
|
||||
[Tooltip("Spawn mode for floating words")]
|
||||
public ChoiceSpawnMode SpawnMode = ChoiceSpawnMode.AroundAnchor;
|
||||
|
||||
[Tooltip("Number of floating words to spawn around the object")]
|
||||
[Range(1, 10)]
|
||||
public int WordCount = 3;
|
||||
|
||||
[Tooltip("Radius around anchor where words spawn")]
|
||||
public float SpawnRadius = 1.5f;
|
||||
|
||||
[Tooltip("Height offset from anchor")]
|
||||
public float SpawnHeightOffset = 0f;
|
||||
|
||||
[Tooltip("Forward offset from anchor (to avoid clipping with object)")]
|
||||
public float ForwardOffset = 0.5f;
|
||||
|
||||
[Tooltip("Tilt angle in degrees (rotation around Right axis for height adjustment)")]
|
||||
[Range(-90f, 90f)]
|
||||
public float TiltAngle = 0f;
|
||||
|
||||
[Tooltip("Should words face the player?")]
|
||||
public bool FacePlayer = true;
|
||||
|
||||
[Tooltip("Duration of spawn animation")]
|
||||
public float SpawnAnimationDuration = 1f;
|
||||
|
||||
[Tooltip("Spawn animation curve")]
|
||||
public AnimationCurve SpawnCurve = AnimationCurve.EaseInOut(0, 0, 1, 1);
|
||||
|
||||
[Header("Idle Behavior")]
|
||||
[Tooltip("Should words float vertically?")]
|
||||
public bool EnableFloating = true;
|
||||
|
||||
[Tooltip("Floating amplitude")]
|
||||
public float FloatingAmplitude = 0.2f;
|
||||
|
||||
[Tooltip("Floating duration (full cycle)")]
|
||||
public float FloatingDuration = 3f;
|
||||
|
||||
[Tooltip("Enable organic shake/drift movement around center point")]
|
||||
public bool EnableOrganicShake = true;
|
||||
|
||||
[Tooltip("Radius around center point where words can drift")]
|
||||
public float ShakeRadius = 0.15f;
|
||||
|
||||
[Tooltip("Speed of the shake movement")]
|
||||
public float ShakeSpeed = 1.5f;
|
||||
|
||||
[Tooltip("Smoothness of shake transitions (higher = smoother)")]
|
||||
public float ShakeSmoothness = 2f;
|
||||
|
||||
[Header("Ghost Trail Effect")]
|
||||
[Tooltip("Enable ghost/shadow trail effect behind text")]
|
||||
public bool EnableGhostTrail = false;
|
||||
|
||||
[Tooltip("Time delay for ghost trail in seconds")]
|
||||
public float GhostTrailDelay = 0.4f;
|
||||
|
||||
[Tooltip("Transparency of the ghost trail (0 = invisible, 1 = opaque)")]
|
||||
[Range(0f, 1f)]
|
||||
public float GhostTrailAlpha = 0.5f;
|
||||
|
||||
[Header("Rotation")]
|
||||
[Tooltip("Should container rotate continuously?")]
|
||||
public bool EnableRotation = true;
|
||||
|
||||
[Tooltip("Axis of rotation")]
|
||||
public RotationAxis RotationAxis = RotationAxis.Up;
|
||||
|
||||
[Tooltip("Rotation speed (in seconds for a full rotation)")]
|
||||
public float RotationSpeed = 10f;
|
||||
|
||||
[Header("Convergence Behavior")]
|
||||
[Tooltip("Minimum radius when cursor is on object (words converge here)")]
|
||||
public float MinConvergenceRadius = 0.1f;
|
||||
|
||||
[Tooltip("Speed at which words converge to center")]
|
||||
public float ConvergenceSpeed = 5f;
|
||||
|
||||
[Tooltip("Duration of the merge animation when words fuse together")]
|
||||
public float MergeDuration = 0.3f;
|
||||
|
||||
[Tooltip("Scale multiplier during merge animation")]
|
||||
public float MergeScalePulse = 1.2f;
|
||||
|
||||
[Tooltip("If true, merge only happens once and words stay merged. If false, words can merge/unmerge repeatedly.")]
|
||||
public bool MergeOnce = false;
|
||||
|
||||
[Tooltip("Duration in seconds the merged word stays visible before disappearing (only used if mergeOnce is true)")]
|
||||
public float MergeOnceDisplayDuration = 3f;
|
||||
|
||||
[Header("Player Distance")]
|
||||
[Tooltip("Distance from object at which words appear")]
|
||||
public float ActivationDistance = 5f;
|
||||
|
||||
[Tooltip("Distance beyond which words disappear")]
|
||||
public float DeactivationDistance = 7f;
|
||||
|
||||
[Header("Visual Settings")]
|
||||
[Tooltip("Enable distance-based scaling")]
|
||||
public bool EnableDistanceScaling = true;
|
||||
|
||||
[Tooltip("Minimum distance for scaling calculation")]
|
||||
public float MinDistance = 2f;
|
||||
|
||||
[Tooltip("Maximum distance for scaling calculation")]
|
||||
public float MaxDistance = 10f;
|
||||
|
||||
[Tooltip("Scale multiplier at minimum distance")]
|
||||
public float MinScaleMultiplier = 1f;
|
||||
|
||||
[Tooltip("Scale multiplier at maximum distance")]
|
||||
public float MaxScaleMultiplier = 2.5f;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dfa0e8eb3803d1c47bbde0fe4fb30a37
|
||||
Reference in New Issue
Block a user