diff --git a/Assets/Polyart/PolyartStudio/DreamscapeCastle/Scenes/Gameplay.unity b/Assets/Polyart/PolyartStudio/DreamscapeCastle/Scenes/Gameplay.unity index 69d39e7e..b7630e6a 100644 --- a/Assets/Polyart/PolyartStudio/DreamscapeCastle/Scenes/Gameplay.unity +++ b/Assets/Polyart/PolyartStudio/DreamscapeCastle/Scenes/Gameplay.unity @@ -191096,7 +191096,7 @@ Transform: m_GameObject: {fileID: 1236565186} serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 141.0367, y: 26.4137, z: 389.6348} + m_LocalPosition: {x: 140.95, y: 26.4137, z: 389.6348} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: @@ -198170,7 +198170,7 @@ Transform: m_GameObject: {fileID: 1282845310} serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalPosition: {x: 2.3, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: [] diff --git a/Assets/Scripts/Dialogues.meta b/Assets/Scripts/Dialogues.meta new file mode 100644 index 00000000..bdcd6530 --- /dev/null +++ b/Assets/Scripts/Dialogues.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8f4c15fc4e9fb7a40b4e9a3abced59cc +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Dialogues/DialogueAsset.cs b/Assets/Scripts/Dialogues/DialogueAsset.cs new file mode 100644 index 00000000..3967b813 --- /dev/null +++ b/Assets/Scripts/Dialogues/DialogueAsset.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.Events; + +[CreateAssetMenu(menuName = "Dialogue/Dialogue Asset", fileName = "NewDialogue")] +public class DialogueAsset : ScriptableObject +{ + [Tooltip("Start from this node id when dialogue begins")] + public int startNodeId = 0; + + public List nodes = new(); + + [Serializable] + public class Node + { + public int id = 0; + public string speakerName; + public Sprite speakerPortrait; + + [Tooltip("Shown in order. If more than one, player taps/presses to advance within the same node.")] + [TextArea(2, 4)] public List lines = new(); + + [Tooltip("Invoked when this node is entered (before showing text).")] + public UnityEvent onEnter; + + [Tooltip("If no choices and nextNodeId < 0, dialogue ends after this node.")] + public int nextNodeId = -1; + + public List choices = new(); + } + + [Serializable] + public class Choice + { + [TextArea(1, 2)] public string text; + + [Tooltip("Go to this node when chosen. If < 0, ends dialogue.")] + public int nextNodeId = -1; + + [Header("Conditions / Effects")] + [Tooltip("If non-empty, this choice is only visible when the flag is present in DialogueManager.")] + public string requireFlag; + + [Tooltip("If non-empty, set this flag on choose (stored in DialogueManager).")] + public string setFlag; + + [Tooltip("Optional SFX or other hook.")] + public UnityEvent onChoose; + } + + public bool TryGetNode(int id, out Node node) + { + node = nodes.Find(n => n.id == id); + return node != null; + } +} \ No newline at end of file diff --git a/Assets/Scripts/Dialogues/DialogueAsset.cs.meta b/Assets/Scripts/Dialogues/DialogueAsset.cs.meta new file mode 100644 index 00000000..d3a62043 --- /dev/null +++ b/Assets/Scripts/Dialogues/DialogueAsset.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8603d6a0b297c554fbd9be8adf7daecf +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Dialogues/DialogueManager.cs b/Assets/Scripts/Dialogues/DialogueManager.cs new file mode 100644 index 00000000..39ca1f19 --- /dev/null +++ b/Assets/Scripts/Dialogues/DialogueManager.cs @@ -0,0 +1,131 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +public class DialogueManager : MonoBehaviour +{ + public static DialogueManager Instance { get; private set; } + + [Header("Hook up the UI component here")] + public DialogueUI ui; + + [Header("Flags saved during play (simple global state)")] + public HashSet flags = new(); + + DialogueAsset _active; + DialogueAsset.Node _node; + int _lineIndex; + + bool _isTyping; + Coroutine _typeRoutine; + + void Awake() + { + if (Instance != null && Instance != this) { Destroy(gameObject); return; } + Instance = this; + DontDestroyOnLoad(gameObject); + } + + // Public API ------------- + public bool IsRunning => _active != null; + + public void StartDialogue(DialogueAsset asset) + { + if (asset == null) return; + _active = asset; + GoToNode(asset.startNodeId); + ui.Show(true); + } + + public void StopDialogue() + { + _active = null; + _node = null; + ui.Show(false); + } + + public void Next() // Called by UI "Continue" button / input + { + if (!IsRunning || _node == null) return; + + if (_isTyping) + { + ui.CompleteTypeThisFrame(); + _isTyping = false; + return; + } + + // Advance line within node + _lineIndex++; + if (_lineIndex < _node.lines.Count) + { + StartTyping(_node.lines[_lineIndex]); + } + else + { + // Lines finished -> show choices or auto-next / end + var visibleChoices = ui.BuildVisibleChoices(_node.choices, flags); + if (visibleChoices.Count > 0) + { + ui.ShowChoices(visibleChoices, OnChoicePicked); + } + else + { + if (_node.nextNodeId >= 0) GoToNode(_node.nextNodeId); + else StopDialogue(); + } + } + } + + // Internal ------------- + void GoToNode(int id) + { + if (!_active.TryGetNode(id, out _node)) + { + Debug.LogWarning($"Dialogue node {id} not found."); + StopDialogue(); + return; + } + + _lineIndex = 0; + _node.onEnter?.Invoke(); + + ui.BindSpeaker(_node.speakerName, _node.speakerPortrait); + + if (_node.lines.Count == 0) + { + // Empty node: immediately resolve choices/next + var visibleChoices = ui.BuildVisibleChoices(_node.choices, flags); + if (visibleChoices.Count > 0) ui.ShowChoices(visibleChoices, OnChoicePicked); + else if (_node.nextNodeId >= 0) GoToNode(_node.nextNodeId); + else StopDialogue(); + } + else + { + StartTyping(_node.lines[_lineIndex]); + } + } + + void StartTyping(string text) + { + ui.HideChoices(); + if (_typeRoutine != null) StopCoroutine(_typeRoutine); + _typeRoutine = StartCoroutine(TypeRoutine(text)); + } + + IEnumerator TypeRoutine(string text) + { + _isTyping = true; + yield return ui.TypeText(text); + _isTyping = false; + } + + void OnChoicePicked(DialogueAsset.Choice choice) + { + choice?.onChoose?.Invoke(); + if (!string.IsNullOrEmpty(choice.setFlag)) flags.Add(choice.setFlag); + + if (choice == null || choice.nextNodeId < 0) { StopDialogue(); return; } + GoToNode(choice.nextNodeId); + } +} diff --git a/Assets/Scripts/Dialogues/DialogueManager.cs.meta b/Assets/Scripts/Dialogues/DialogueManager.cs.meta new file mode 100644 index 00000000..fcbb3f45 --- /dev/null +++ b/Assets/Scripts/Dialogues/DialogueManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c11bb4d5be167a240beb9566d35772ae +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Dialogues/DialogueUI.cs b/Assets/Scripts/Dialogues/DialogueUI.cs new file mode 100644 index 00000000..4d13d1a2 --- /dev/null +++ b/Assets/Scripts/Dialogues/DialogueUI.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using TMPro; +using UnityEngine; +using UnityEngine.UI; + +public class DialogueUI : MonoBehaviour +{ + [Header("References")] + public CanvasGroup root; // Whole dialogue panel + public TMP_Text speakerNameText; + public Image speakerPortraitImage; + public TMP_Text bodyText; + + [Header("Choices")] + public Transform choicesParent; + public Button choiceButtonPrefab; + + [Header("Typing")] + [Range(0.005f, 0.1f)] public float charDelay = 0.02f; + + string _targetFull; + bool _completeThisFrame; + + void Reset() + { + root = GetComponent(); + } + + public void Show(bool on) + { + if (!root) return; + root.alpha = on ? 1f : 0f; + root.interactable = on; + root.blocksRaycasts = on; + + if (!on) + { + bodyText?.SetText(string.Empty); + HideChoices(); + } + } + + public void BindSpeaker(string speakerName, Sprite portrait) + { + if (speakerNameText) speakerNameText.text = speakerName ?? ""; + if (speakerPortraitImage) + { + speakerPortraitImage.sprite = portrait; + speakerPortraitImage.enabled = portrait != null; + } + } + + public IEnumerator TypeText(string text) + { + _targetFull = text ?? string.Empty; + bodyText.text = string.Empty; + _completeThisFrame = false; + + foreach (char c in _targetFull) + { + if (_completeThisFrame) break; + bodyText.text += c; + yield return new WaitForSeconds(charDelay); + } + + bodyText.text = _targetFull; + _completeThisFrame = false; + } + + public void CompleteTypeThisFrame() + { + _completeThisFrame = true; + } + + public List BuildVisibleChoices( + List all, + HashSet flags) + { + var list = new List(); + if (all == null) return list; + + foreach (var c in all) + { + if (string.IsNullOrEmpty(c.requireFlag) || flags.Contains(c.requireFlag)) + list.Add(c); + } + return list; + } + + public void ShowChoices( + List choices, + Action onPick) + { + HideChoices(); + + foreach (var c in choices) + { + var btn = Instantiate(choiceButtonPrefab, choicesParent); + var label = btn.GetComponentInChildren(); + if (label) label.text = c.text; + + btn.onClick.AddListener(() => onPick?.Invoke(c)); + } + } + + public void HideChoices() + { + if (!choicesParent) return; + for (int i = choicesParent.childCount - 1; i >= 0; i--) + Destroy(choicesParent.GetChild(i).gameObject); + } +} diff --git a/Assets/Scripts/Dialogues/DialogueUI.cs.meta b/Assets/Scripts/Dialogues/DialogueUI.cs.meta new file mode 100644 index 00000000..d51b3ec8 --- /dev/null +++ b/Assets/Scripts/Dialogues/DialogueUI.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 88612b9886a695c4faf4af1fa2800a2e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Dialogues/NPCDialogueTrigger.cs b/Assets/Scripts/Dialogues/NPCDialogueTrigger.cs new file mode 100644 index 00000000..f0f8acb6 --- /dev/null +++ b/Assets/Scripts/Dialogues/NPCDialogueTrigger.cs @@ -0,0 +1,36 @@ +using UnityEngine; + +public class NPCDialogueTrigger : MonoBehaviour +{ + public DialogueAsset dialogue; + public string interactKey = "e"; + public float interactRange = 2.2f; + + Transform _player; + + void Start() + { + var p = GameObject.FindGameObjectWithTag("Player"); + _player = p ? p.transform : null; + } + + void Update() + { + if (DialogueManager.Instance && DialogueManager.Instance.IsRunning) return; + if (_player == null) return; + + if (Vector3.Distance(transform.position, _player.position) <= interactRange) + { + if (Input.GetKeyDown(interactKey.ToLower())) + { + DialogueManager.Instance.StartDialogue(dialogue); + } + } + } + + void OnDrawGizmosSelected() + { + Gizmos.color = Color.yellow; + Gizmos.DrawWireSphere(transform.position, interactRange); + } +} \ No newline at end of file diff --git a/Assets/Scripts/Dialogues/NPCDialogueTrigger.cs.meta b/Assets/Scripts/Dialogues/NPCDialogueTrigger.cs.meta new file mode 100644 index 00000000..f6eb28c5 --- /dev/null +++ b/Assets/Scripts/Dialogues/NPCDialogueTrigger.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cafcb3d2d5194794089b630c916b6084 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: