using System; using System.Collections.Generic; using UnityEngine.Events; using UnityEngine.InputSystem; using UnityEngine; [RequireComponent(typeof(CharacterVoiceObject))] public class DialogPlayer : MonoBehaviour { // 대화 후보 하나. 리스트에서 위에 있을수록 우선순위가 높다. // (스토리 대화를 위에, 조건 없는 기본 잡담을 맨 아래에 두는 것을 권장) [System.Serializable] public struct DialogEntry { public DialogGroup Group; public DialogCondition Condition; [Tooltip("켜면 완료 후에도 반복 재생 가능(잡담용). 끄면 1회성(스토리 대화)")] public bool Repeatable; [Tooltip("이 대화를 처음 완료하면 메인 진행도 +N (필수 대화가 아니면 0)")] [Min(0)] public int ProgressOnComplete; } // 노드의 EventKey ↔ 그 노드 재생 시 호출할 이벤트. [System.Serializable] public struct NodeEvent { public string Key; public UnityEvent Event; } [Tooltip("이 NPC의 대화 후보들. 위에서부터 조건을 검사해 첫 번째로 만족하는 대화를 재생")] [SerializeField] private List _dialogs = new(); [Header("Dialog HUD Placement")] // 씬에서 캐릭터 위치/주변(벽 등)에 맞춰 조절 [SerializeField] private float _hudChestHeight = 1.2f; // 화자 발 기준 가슴 높이 [SerializeField] private float _hudForwardOffset = 0.5f; // 화자→플레이어 방향으로 띄울 거리 [SerializeField] private float _hudLateralOffset = 0f; // 좌우 오프셋 (+ 플레이어 시점 오른쪽) [Header("Dialog Events")] [Tooltip("노드의 Event Key와 같은 Key가 그 노드 재생 시 호출됨")] [SerializeField] private List _nodeEvents = new(); private CharacterVoiceObject _voice; // 이 NPC의 캐릭터 정보 (호감도 조건 대상) private Animator _animator; private int _initialGestureHash; private int _initialExpressionHash; private bool _hasInitialExpression; private readonly Dictionary _originalRotations = new(); public bool IsPlaying { get; private set; } private void Awake() { _voice = GetComponent(); _animator = GetComponentInChildren(); if (_animator != null) { _initialGestureHash = _animator.GetCurrentAnimatorStateInfo(0).fullPathHash; if (_animator.layerCount > 1) { _initialExpressionHash = _animator.GetCurrentAnimatorStateInfo(1).fullPathHash; _hasInitialExpression = true; } } } public async Awaitable Play() { if (IsPlaying) return; int index = FindPlayableIndex(); if (index < 0) { Debug.Log($"[DialogPlayer] 조건에 맞는 대화가 없음: {name}"); return; } await PlayEntry(_dialogs[index]); } // 리스트 순서 = 우선순위. 조건을 만족하고 (반복 가능하거나 아직 안 한) 첫 대화. private int FindPlayableIndex() { for (int i = 0; i < _dialogs.Count; i++) { var entry = _dialogs[i]; if (entry.Group == null) continue; if (!entry.Repeatable && StoryState.IsDialogCompleted(entry.Group.name)) continue; if (entry.Condition != null && !entry.Condition.IsMet(_voice.Character)) continue; return i; } return -1; } private async Awaitable PlayEntry(DialogEntry entry) { IsPlaying = true; try { var node = entry.Group.StartNode; while (node != null) { await PlayNode(node); if (node.Choices != null && node.Choices.Count > 0) { int picked = await WaitForChoice(node); RecordChoice(node, picked); node = node.Choices[picked].DestinationNode; } else { node = node.Next; } } // 여기까지 왔으면 자연 종료(끝까지 재생) — 이때만 완료로 기록한다. // (중간에 오브젝트 파괴 등으로 끊기면 예외로 빠져나가 기록되지 않음) bool firstTime = StoryState.MarkDialogCompleted(entry.Group.name); if (firstTime && entry.ProgressOnComplete > 0) StoryState.MainProgress += entry.ProgressOnComplete; StoryState.Save(); Debug.Log($"[DialogPlayer] 대화 종료: {entry.Group.name}"); } finally { IsPlaying = false; if (DialogHud.Instance != null) DialogHud.Instance.Hide(); RestoreDefaultAnimations(); RestoreRotations(); } } private void RestoreDefaultAnimations() { if (_animator == null) return; _animator.CrossFade(_initialGestureHash, 0.3f, 0, normalizedTimeOffset: 0f); if (_hasInitialExpression) _animator.CrossFade(_initialExpressionHash, 0.3f, 1, normalizedTimeOffset: 0f); } private async Awaitable RotateTowardPlayer(Transform target) { if (Camera.main == null) return; var playerCam = Camera.main.transform; float duration = 0.5f; float elapsed = 0f; while (elapsed < duration) { Vector3 dir = playerCam.position - target.position; dir.y = 0f; if (dir.sqrMagnitude > 0.0001f) { var targetRot = Quaternion.LookRotation(dir); target.rotation = Quaternion.Slerp(target.rotation, targetRot, 10f * Time.deltaTime); } elapsed += Time.deltaTime; await Awaitable.NextFrameAsync(); } } private void RestoreRotations() { foreach (var kvp in _originalRotations) { if (kvp.Key != null) _ = RotateToRotation(kvp.Key, kvp.Value); } _originalRotations.Clear(); } private async Awaitable RotateToRotation(Transform target, Quaternion targetRotation) { float duration = 0.5f; float elapsed = 0f; while (elapsed < duration) { if (target == null) return; target.rotation = Quaternion.Slerp(target.rotation, targetRotation, 10f * Time.deltaTime); elapsed += Time.deltaTime; await Awaitable.NextFrameAsync(); } } private async Awaitable PlayNode(DialogNode node) { // 화자 옆 DialogHud에 대사 표시 (배치 오프셋은 이 NPC의 설정값 사용) if (DialogHud.Instance != null) DialogHud.Instance.Show(node.Speaker, node.TalkText, _hudChestHeight, _hudForwardOffset, _hudLateralOffset); RaiseNodeEvent(node.EventKey); // EventKey 있으면 매칭 이벤트 호출 // 보이스 재생 if (node.Voice != null && node.Speaker != null) { var voiceObj = CharacterVoiceObject.Find(node.Speaker); if (voiceObj != null && node.Voice.Clip != null) voiceObj.Play(node.Voice.Clip); } // 플레이어 향해 회전 if (node.LookAtPlayer && node.Speaker != null) { var voiceObj = CharacterVoiceObject.Find(node.Speaker); if (voiceObj != null) { _originalRotations.TryAdd(voiceObj.transform, voiceObj.transform.rotation); _ = RotateTowardPlayer(voiceObj.transform); } } if (node.Gesture != null) _animator.CrossFade(node.Gesture.StateName, node.Gesture.CrossFadeDuration, node.Gesture.AnimationLayer); if (node.Expression != null) _animator.CrossFade(node.Expression.StateName, node.Expression.CrossFadeDuration, node.Expression.AnimationLayer); // 진행 방식 결정 if (node.WaitForInput) { await WaitForAdvanceInput(); // B버튼 입력이 있어야만 다음으로 } else { float wait = (node.Voice != null && node.Voice.Clip != null) ? node.Voice.Clip.length : node.LineDuration; if (wait > 0f) await Awaitable.WaitForSecondsAsync(wait); else await WaitForAdvanceInput(); // 지정 시간이 없으면 입력으로 진행 } } // 노드의 EventKey와 같은 Key를 가진 이벤트들을 호출 private void RaiseNodeEvent(string key) { if (string.IsNullOrEmpty(key)) return; foreach (var e in _nodeEvents) if (e.Key == key) e.Event?.Invoke(); } // 선택 기록: 선택지 Code를 StoryState에 영구 기록 (대화 활성화 조건 판정에 사용) private void RecordChoice(DialogNode node, int index) { string code = (node.Choices != null && index >= 0 && index < node.Choices.Count) ? node.Choices[index].Code : null; code = DialogVariables.Format(code); // {token} 치환 → 동적으로 생성된 코드 반영 if (!string.IsNullOrEmpty(code)) StoryState.RecordChoice(code); } private async Awaitable WaitForChoice(DialogNode node) { //선택을 기다리는 함수 수정해서 사용할것 if (ChoiceHud.Instance == null) { Debug.LogWarning("[DialogPlayer] ChoiceHud 없음 — 0번 자동 선택"); return 0; } return await ChoiceHud.Instance.Show(node.ChoiceQuestion, node.Choices); } // 대화 진행 입력(OnDialogNext = VR B버튼) 한 번을 대기 private async Awaitable WaitForAdvanceInput() { var im = InputManager.Instance; if (im == null) { // 입력 매니저 없으면 안전하게 잠깐 대기 후 진행 await Awaitable.WaitForSecondsAsync(1f); return; } bool pressed = false; void Handler() => pressed = true; im.OnDialogNext_Event += Handler; try { while (!pressed) await Awaitable.NextFrameAsync(destroyCancellationToken); } catch (OperationCanceledException) { // 대기 중 오브젝트 파괴 시 조용히 종료 } finally { im.OnDialogNext_Event -= Handler; } } //테스트용 private void Update() { if (Mouse.current == null) return; if (!Mouse.current.leftButton.wasPressedThisFrame) return; if (Camera.main == null) return; var ray = Camera.main.ScreenPointToRay(Mouse.current.position.ReadValue()); if (Physics.Raycast(ray, out var hit) && hit.transform.IsChildOf(transform)) { Debug.Log("캐릭터 클릭"); Play(); } } }