chore: Project Restructuring & Theatre
This commit is contained in:
@@ -0,0 +1,255 @@
|
||||
using UnityEngine;
|
||||
using System;
|
||||
using Unheimlich.Features.Player;
|
||||
using Unheimlich.Features.Dialogue;
|
||||
using Unheimlich.Features.Dialogue.UI;
|
||||
using Unheimlich.Features.Dialogue.Data;
|
||||
using Unheimlich.Core.Utils;
|
||||
|
||||
namespace Unheimlich.Features.Dialogue
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages gaze-based focus detection for dialogue choices using raycasts.
|
||||
/// </summary>
|
||||
public class DialogueFocusSystem
|
||||
{
|
||||
private readonly DialogueManager _manager;
|
||||
private readonly Camera _mainCamera;
|
||||
private readonly CameraEffects _cameraEffects;
|
||||
|
||||
private ChoiceFocusHandler _currentFocusedHandler;
|
||||
private float _focusTimer = 0f;
|
||||
private float _retentionTimer = 0f;
|
||||
private float _raycastTimer = 0f;
|
||||
private bool _isFocusLocked = false;
|
||||
|
||||
private readonly LayerMask _choiceLayer;
|
||||
private readonly float _focusRange;
|
||||
private readonly float _focusTime;
|
||||
private readonly float _raycastInterval;
|
||||
private readonly float _recoveryDuration;
|
||||
private readonly float _focusRetentionTime;
|
||||
private readonly float _fallbackShakeIntensity;
|
||||
|
||||
public event Action<DialogueChoiceData> OnChoiceMade;
|
||||
|
||||
public DialogueFocusSystem(
|
||||
DialogueManager manager,
|
||||
Camera mainCamera,
|
||||
CameraEffects cameraEffects,
|
||||
LayerMask choiceLayer,
|
||||
float focusRange,
|
||||
float focusTime,
|
||||
float raycastInterval,
|
||||
float recoveryDuration,
|
||||
float focusRetentionTime,
|
||||
float fallbackShakeIntensity)
|
||||
{
|
||||
_manager = manager;
|
||||
_mainCamera = mainCamera;
|
||||
_cameraEffects = cameraEffects;
|
||||
_choiceLayer = choiceLayer;
|
||||
_focusRange = focusRange;
|
||||
_focusTime = focusTime;
|
||||
_raycastInterval = raycastInterval;
|
||||
_recoveryDuration = recoveryDuration;
|
||||
_focusRetentionTime = focusRetentionTime;
|
||||
_fallbackShakeIntensity = fallbackShakeIntensity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the focus detection system. Should be called every frame during active dialogue.
|
||||
/// Performs raycasts at configured intervals to detect choice focus.
|
||||
/// </summary>
|
||||
public void Update()
|
||||
{
|
||||
if (!_manager.IsDialogueActive || !_manager.CurrentDialogueUI.AreChoicesVisible())
|
||||
return;
|
||||
|
||||
_raycastTimer += Time.deltaTime;
|
||||
if (_raycastTimer >= _raycastInterval)
|
||||
{
|
||||
CheckForChoiceFocus();
|
||||
_raycastTimer = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs raycast from camera center to detect focused dialogue choices.
|
||||
/// Manages focus state transitions, retention timers, visual feedback, and choice selection.
|
||||
/// Implements stabilization to prevent rapid focus switching between choices.
|
||||
/// </summary>
|
||||
private void CheckForChoiceFocus()
|
||||
{
|
||||
if (_isFocusLocked)
|
||||
{
|
||||
if (_retentionTimer > 0f)
|
||||
_retentionTimer -= Time.deltaTime;
|
||||
else
|
||||
_isFocusLocked = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_mainCamera == null)
|
||||
return;
|
||||
|
||||
ChoiceFocusHandler hitHandler = null;
|
||||
|
||||
if (RaycastHelper.RaycastFromCameraCenter(_mainCamera, _focusRange, _choiceLayer, out RaycastHit hit))
|
||||
{
|
||||
hitHandler = hit.collider.GetComponent<ChoiceFocusHandler>();
|
||||
|
||||
if (_cameraEffects != null && hitHandler != null)
|
||||
_cameraEffects.ApplyAimAssist(hit.collider.bounds.center);
|
||||
}
|
||||
|
||||
if (hitHandler != _currentFocusedHandler)
|
||||
{
|
||||
_retentionTimer -= Time.deltaTime;
|
||||
|
||||
if (hitHandler != null && _retentionTimer <= 0f)
|
||||
{
|
||||
if (_currentFocusedHandler != null)
|
||||
{
|
||||
_currentFocusedHandler.ResetFeedback(_recoveryDuration);
|
||||
ResetAllUnfocusEffects();
|
||||
}
|
||||
|
||||
_currentFocusedHandler = hitHandler;
|
||||
_focusTimer = 0f;
|
||||
_retentionTimer = _focusRetentionTime;
|
||||
|
||||
if (_cameraEffects != null)
|
||||
_cameraEffects.LockAim(true);
|
||||
|
||||
ApplyUnfocusEffectToOthers(hitHandler);
|
||||
|
||||
DialogueAnimationSettings settings = _manager.CurrentAnimationSettings;
|
||||
bool shouldRotate = settings == null || settings.EnableRotation;
|
||||
if (shouldRotate && _manager.CurrentDialogueUI != null)
|
||||
{
|
||||
float focusSpeed = settings != null ? settings.FocusRotationSpeed : 60f;
|
||||
_manager.CurrentDialogueUI.RotationAnimation(focusSpeed);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (hitHandler != null)
|
||||
{
|
||||
_retentionTimer = _focusRetentionTime;
|
||||
}
|
||||
|
||||
bool isFocusActive = _currentFocusedHandler != null && (_retentionTimer > 0f);
|
||||
|
||||
if (isFocusActive)
|
||||
{
|
||||
_focusTimer += Time.deltaTime;
|
||||
float progress = _focusTimer / _focusTime;
|
||||
|
||||
_currentFocusedHandler.ApplyFocusFeedback(progress);
|
||||
|
||||
DialogueAnimationSettings currentSettings = _manager.CurrentAnimationSettings;
|
||||
bool shouldShake = currentSettings == null || currentSettings.EnableCameraShakeOnFocus;
|
||||
|
||||
if (shouldShake && _cameraEffects != null)
|
||||
{
|
||||
float shakeIntensity = currentSettings != null ? currentSettings.CameraShakeIntensity : _fallbackShakeIntensity;
|
||||
float shakeSmoothness = currentSettings != null ? currentSettings.CameraShakeSmoothness : 10f;
|
||||
_cameraEffects.ShakeCamera(progress / shakeIntensity, shakeSmoothness);
|
||||
}
|
||||
|
||||
if (_focusTimer >= _focusTime)
|
||||
{
|
||||
ChoiceFocusHandler selectedHandler = _currentFocusedHandler;
|
||||
|
||||
if (selectedHandler.DialogueData != null)
|
||||
{
|
||||
OnChoiceMade?.Invoke(selectedHandler.DialogueData);
|
||||
DialogueNode nextNode = selectedHandler.GetNextNode();
|
||||
_manager.GoToNextNode(nextNode);
|
||||
}
|
||||
|
||||
DialogueAnimationSettings settings = _manager.CurrentAnimationSettings;
|
||||
bool shouldRotate = settings == null || settings.EnableRotation;
|
||||
if (shouldRotate && _manager.CurrentDialogueUI != null)
|
||||
{
|
||||
float normalSpeed = settings != null ? settings.NormalRotationSpeed : 36f;
|
||||
_manager.CurrentDialogueUI.RotationAnimation(normalSpeed);
|
||||
}
|
||||
|
||||
ResetAnimationTimers();
|
||||
ResetAllUnfocusEffects();
|
||||
|
||||
selectedHandler.ResetFeedback(_recoveryDuration);
|
||||
|
||||
if (_cameraEffects != null)
|
||||
_cameraEffects.LockAim(false);
|
||||
}
|
||||
}
|
||||
else if (_currentFocusedHandler != null && _retentionTimer <= 0f)
|
||||
{
|
||||
_currentFocusedHandler.ResetFeedback(_recoveryDuration);
|
||||
ResetAllUnfocusEffects();
|
||||
|
||||
DialogueAnimationSettings settings = _manager.CurrentAnimationSettings;
|
||||
bool shouldRotate = settings == null || settings.EnableRotation;
|
||||
if (shouldRotate && _manager.CurrentDialogueUI != null)
|
||||
{
|
||||
float normalSpeed = settings != null ? settings.NormalRotationSpeed : 36f;
|
||||
_manager.CurrentDialogueUI.RotationAnimation(normalSpeed);
|
||||
}
|
||||
|
||||
ResetAnimationTimers();
|
||||
|
||||
if (_cameraEffects != null)
|
||||
_cameraEffects.LockAim(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyUnfocusEffectToOthers(ChoiceFocusHandler focusedHandler)
|
||||
{
|
||||
if (_manager.CurrentDialogueUI == null || _manager.CurrentDialogueUI.ChoiceContainer == null)
|
||||
return;
|
||||
|
||||
DialogueAnimationSettings settings = _manager.CurrentAnimationSettings;
|
||||
float shakeSlowdown = settings != null ? settings.UnfocusedShakeSpeedMultiplier : 0.3f;
|
||||
|
||||
foreach (Transform child in _manager.CurrentDialogueUI.ChoiceContainer.transform)
|
||||
{
|
||||
ChoiceFocusHandler handler = child.GetComponent<ChoiceFocusHandler>();
|
||||
if (handler != null && handler != focusedHandler)
|
||||
{
|
||||
handler.SetShakeSpeedMultiplier(shakeSlowdown);
|
||||
handler.ApplySketchFade();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetAllUnfocusEffects()
|
||||
{
|
||||
if (_manager.CurrentDialogueUI == null || _manager.CurrentDialogueUI.ChoiceContainer == null)
|
||||
return;
|
||||
|
||||
foreach (Transform child in _manager.CurrentDialogueUI.ChoiceContainer.transform)
|
||||
{
|
||||
ChoiceFocusHandler handler = child.GetComponent<ChoiceFocusHandler>();
|
||||
if (handler != null)
|
||||
{
|
||||
handler.SetShakeSpeedMultiplier(1f);
|
||||
handler.ResetSketchFade();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetAnimationTimers()
|
||||
{
|
||||
_currentFocusedHandler = null;
|
||||
_focusTimer = 0f;
|
||||
_retentionTimer = 0f;
|
||||
}
|
||||
|
||||
public void UnlockFocus()
|
||||
{
|
||||
_isFocusLocked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 22514e9ce0f2e6c4b9f5f718ae139b09
|
||||
@@ -0,0 +1,293 @@
|
||||
using UnityEngine;
|
||||
using System;
|
||||
using Unheimlich.Core;
|
||||
using Unheimlich.Features.Dialogue.Data;
|
||||
using Unheimlich.Features.DetectiveBoard.Data;
|
||||
using Unheimlich.Features.DetectiveBoard;
|
||||
using Unheimlich.Features.Player;
|
||||
using Unheimlich.Features.Dialogue.UI;
|
||||
using Unheimlich.Features.Dialogue;
|
||||
using System.Collections.Generic;
|
||||
using InputManager = Unheimlich.Core.Input.InputManager;
|
||||
|
||||
namespace Unheimlich.Features.Dialogue
|
||||
{
|
||||
public class DialogueManager : Singleton<DialogueManager>
|
||||
{
|
||||
|
||||
[Header("State")]
|
||||
[SerializeField] private bool IsInDialogue = false;
|
||||
|
||||
[Header("Focus Settings")]
|
||||
[SerializeField] private LayerMask _choiceLayer;
|
||||
[SerializeField] private float _focusRange = 5f;
|
||||
[SerializeField] private float _focusTime = 1.5f;
|
||||
[SerializeField] private float _raycastInterval = 0.05f;
|
||||
|
||||
[Header("Focus Stabilization")]
|
||||
[SerializeField] private float _recoveryDuration = 0.5f;
|
||||
[SerializeField] private float _focusRetentionTime = 0.1f;
|
||||
|
||||
[Tooltip("Fallback camera shake intensity if not set in DialogueAnimationSettings")]
|
||||
[SerializeField] private float _fallbackShakeIntensity = 30f;
|
||||
|
||||
public DialogueAnimationSettings CurrentAnimationSettings => _currentAnimationSettings;
|
||||
public bool IsDialogueActive => IsInDialogue;
|
||||
public Transform CurrentChoiceAnchor => _currentChoiceAnchor;
|
||||
public DialogueUI CurrentDialogueUI => _activeDialogueUI;
|
||||
public object CurrentOwner { get; private set; }
|
||||
public DialogueConversation CurrentConversation => _currentConversation;
|
||||
public DialogueNode CurrentNode => _currentNode;
|
||||
|
||||
private bool _blurEnabled;
|
||||
|
||||
public event Action<DialogueChoiceData> OnChoiceMade;
|
||||
public event Action<object> OnDialogueCompleted;
|
||||
|
||||
private DialogueFocusSystem _focusSystem;
|
||||
private Camera _mainCamera;
|
||||
private DialogueAnimationSettings _currentAnimationSettings;
|
||||
private DialogueUI _activeDialogueUI;
|
||||
private Transform _currentChoiceAnchor;
|
||||
|
||||
private DialogueConversation _currentConversation;
|
||||
private DialogueNode _currentNode;
|
||||
private float _autoCloseTimer = 0f;
|
||||
private bool _isWaitingForAutoClose = false;
|
||||
|
||||
protected override void OnAwake() { }
|
||||
|
||||
protected override void OnEnabled()
|
||||
{
|
||||
// With Scene Reload disabled, Start() doesn't run again each session.
|
||||
// We recreate the focus system to clear any stale locked state (_isFocusLocked,
|
||||
// _focusTimer, _currentFocusedHandler) that may have carried over.
|
||||
if (_focusSystem != null)
|
||||
{
|
||||
_mainCamera = Camera.main;
|
||||
InitFocusSystem();
|
||||
|
||||
// Also reset dialogue state in case we exited mid-dialogue
|
||||
IsInDialogue = false;
|
||||
_isWaitingForAutoClose = false;
|
||||
_autoCloseTimer = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
_mainCamera = Camera.main;
|
||||
InitFocusSystem();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates or recreates the DialogueFocusSystem with the current inspector settings.
|
||||
/// Called from Start and OnEnabled (domain-reload-less re-entry).
|
||||
/// </summary>
|
||||
private void InitFocusSystem()
|
||||
{
|
||||
_focusSystem = new DialogueFocusSystem(
|
||||
this,
|
||||
_mainCamera,
|
||||
PlayerController.Instance?.CameraEffects,
|
||||
_choiceLayer,
|
||||
_focusRange,
|
||||
_focusTime,
|
||||
_raycastInterval,
|
||||
_recoveryDuration,
|
||||
_focusRetentionTime,
|
||||
_fallbackShakeIntensity
|
||||
);
|
||||
|
||||
_focusSystem.OnChoiceMade += (choice) => OnChoiceMade?.Invoke(choice);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!IsInDialogue)
|
||||
return;
|
||||
|
||||
if (_activeDialogueUI == null)
|
||||
{
|
||||
Debug.LogError("[DialogueManager] IsInDialogue is true but _activeDialogueUI is null. Forcing dialogue end.", this);
|
||||
IsInDialogue = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_activeDialogueUI.AreChoicesVisible())
|
||||
{
|
||||
if (_isWaitingForAutoClose && _currentNode != null && _currentNode.AutoCloseDelay > 0f)
|
||||
{
|
||||
_autoCloseTimer += Time.deltaTime;
|
||||
|
||||
if (_autoCloseTimer >= _currentNode.AutoCloseDelay)
|
||||
{
|
||||
_activeDialogueUI.FadeOutAndHide(() =>
|
||||
{
|
||||
OnDialogueCompleted?.Invoke(CurrentOwner);
|
||||
CleanupDialogue();
|
||||
});
|
||||
|
||||
IsInDialogue = false;
|
||||
_isWaitingForAutoClose = false;
|
||||
|
||||
if (_blurEnabled && DialogueBlurEffectURP.Instance != null)
|
||||
DialogueBlurEffectURP.Instance.HideBlur();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var interactAction = InputManager.GetInteractAction();
|
||||
if (interactAction != null && interactAction.WasPressedThisFrame())
|
||||
{
|
||||
EndDialogue();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_focusSystem?.Update();
|
||||
}
|
||||
}
|
||||
|
||||
public void StartConversation(DialogueConversation conversation, Transform choiceAnchor = null, DialogueAnimationSettings animationSettings = null, DialogueUI customDialogueUI = null, object owner = null, bool showBlur = false)
|
||||
{
|
||||
if (conversation == null)
|
||||
{
|
||||
Debug.LogError("Cannot start conversation: conversation is null");
|
||||
return;
|
||||
}
|
||||
|
||||
DialogueNode firstNode = conversation.GetFirstNode();
|
||||
if (firstNode == null)
|
||||
{
|
||||
Debug.LogError($"Cannot start conversation {conversation.name}: no dialogue nodes found");
|
||||
return;
|
||||
}
|
||||
|
||||
_currentConversation = conversation;
|
||||
_currentNode = firstNode;
|
||||
_currentChoiceAnchor = choiceAnchor;
|
||||
_currentAnimationSettings = animationSettings;
|
||||
_activeDialogueUI = customDialogueUI;
|
||||
CurrentOwner = owner;
|
||||
_blurEnabled = showBlur;
|
||||
|
||||
if (_activeDialogueUI == null)
|
||||
{
|
||||
Debug.LogError("[DialogueManager] StartConversation called with no DialogueUI. Assign a Local Dialogue UI on the DialogueTriggerZone that triggered this conversation.", this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_blurEnabled && DialogueBlurEffectURP.Instance != null)
|
||||
DialogueBlurEffectURP.Instance.ShowBlur();
|
||||
|
||||
PlayerEvents.TriggerEnterDialogue();
|
||||
ShowCurrentNode();
|
||||
}
|
||||
|
||||
private void ShowCurrentNode()
|
||||
{
|
||||
if (_currentNode == null || _currentConversation == null)
|
||||
return;
|
||||
|
||||
UnlockFocus();
|
||||
IsInDialogue = true;
|
||||
|
||||
if (_currentNode.ClueToUnlock != null)
|
||||
{
|
||||
if (DetectiveBoardManager.Instance != null)
|
||||
{
|
||||
DetectiveBoardManager.Instance.UnlockClue(_currentNode.ClueToUnlock);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"[DialogueManager] Cannot unlock clue '{_currentNode.ClueToUnlock.ClueID}': DetectiveBoardManager not found");
|
||||
}
|
||||
}
|
||||
|
||||
if(_currentChoiceAnchor == null)
|
||||
{
|
||||
Debug.LogWarning("No choice anchor found!");
|
||||
return;
|
||||
}
|
||||
Transform anchor = _currentChoiceAnchor;
|
||||
|
||||
string speakerName = _currentConversation.GetLocalizedSpeakerName();
|
||||
string dialogueText = _currentNode.GetLocalizedDialogueText();
|
||||
DialogueDisplayMode displayMode = _currentConversation.DisplayMode;
|
||||
|
||||
_activeDialogueUI.ShowDialogueBox(speakerName, dialogueText, displayMode);
|
||||
|
||||
_autoCloseTimer = 0f;
|
||||
_isWaitingForAutoClose = false;
|
||||
|
||||
if (_currentNode.Choices != null && _currentNode.Choices.Count > 0)
|
||||
{
|
||||
List<DialogueChoiceData> validChoices = new List<DialogueChoiceData>();
|
||||
|
||||
foreach (var choiceData in _currentNode.Choices)
|
||||
{
|
||||
if (choiceData != null && choiceData.IsValid())
|
||||
validChoices.Add(choiceData);
|
||||
}
|
||||
|
||||
if (validChoices.Count > 0)
|
||||
_activeDialogueUI.ShowChoices(validChoices, anchor, _currentAnimationSettings);
|
||||
}
|
||||
else
|
||||
_isWaitingForAutoClose = true;
|
||||
}
|
||||
|
||||
public void GoToNextNode(DialogueNode nextNode)
|
||||
{
|
||||
if (nextNode == null)
|
||||
{
|
||||
OnDialogueCompleted?.Invoke(CurrentOwner);
|
||||
EndDialogue();
|
||||
return;
|
||||
}
|
||||
|
||||
UnlockFocus();
|
||||
|
||||
if (_activeDialogueUI != null)
|
||||
_activeDialogueUI.HideChoices();
|
||||
|
||||
_currentNode = nextNode;
|
||||
ShowCurrentNode();
|
||||
}
|
||||
|
||||
public void EndDialogue()
|
||||
{
|
||||
IsInDialogue = false;
|
||||
|
||||
if (_activeDialogueUI != null)
|
||||
_activeDialogueUI.HideDialogueBox();
|
||||
|
||||
if (_blurEnabled && DialogueBlurEffectURP.Instance != null)
|
||||
DialogueBlurEffectURP.Instance.HideBlur();
|
||||
|
||||
PlayerEvents.TriggerExitDialogue();
|
||||
CleanupDialogue();
|
||||
}
|
||||
|
||||
private void CleanupDialogue()
|
||||
{
|
||||
_currentChoiceAnchor = null;
|
||||
_currentAnimationSettings = null;
|
||||
_activeDialogueUI = null;
|
||||
CurrentOwner = null;
|
||||
_blurEnabled = false;
|
||||
|
||||
_currentConversation = null;
|
||||
_currentNode = null;
|
||||
_autoCloseTimer = 0f;
|
||||
_isWaitingForAutoClose = false;
|
||||
}
|
||||
|
||||
public void UnlockFocus()
|
||||
{
|
||||
_focusSystem?.UnlockFocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bd83d1e8e266cff41b8a5c2928f1f80f
|
||||
@@ -0,0 +1,84 @@
|
||||
using UnityEngine;
|
||||
using Unheimlich.Features.Dialogue.Data;
|
||||
using Unheimlich.Features.Dialogue;
|
||||
using Unheimlich.Features.Dialogue.UI;
|
||||
|
||||
namespace Unheimlich.Features.Dialogue
|
||||
{
|
||||
public class DialogueStateHandler
|
||||
{
|
||||
public bool IsCompleted { get; private set; }
|
||||
public bool HasPausedDialogue => _pausedConversation != null && _pausedNode != null;
|
||||
|
||||
private DialogueConversation _pausedConversation;
|
||||
private DialogueNode _pausedNode;
|
||||
private Transform _pausedChoiceAnchor;
|
||||
private DialogueAnimationSettings _pausedAnimationSettings;
|
||||
private DialogueUI _pausedDialogueUI;
|
||||
private object _owner;
|
||||
|
||||
public DialogueStateHandler(object owner)
|
||||
{
|
||||
_owner = owner;
|
||||
}
|
||||
|
||||
public void PauseDialogue()
|
||||
{
|
||||
if (DialogueManager.Instance == null || !DialogueManager.Instance.IsDialogueActive)
|
||||
return;
|
||||
|
||||
DialogueManager dialogueManager = DialogueManager.Instance;
|
||||
|
||||
_pausedConversation = dialogueManager.CurrentConversation;
|
||||
_pausedNode = dialogueManager.CurrentNode;
|
||||
_pausedChoiceAnchor = dialogueManager.CurrentChoiceAnchor;
|
||||
_pausedAnimationSettings = dialogueManager.CurrentAnimationSettings;
|
||||
_pausedDialogueUI = dialogueManager.CurrentDialogueUI;
|
||||
|
||||
DialogueManager.Instance.EndDialogue();
|
||||
}
|
||||
|
||||
public void ResumeDialogue()
|
||||
{
|
||||
if (!HasPausedDialogue || DialogueManager.Instance == null)
|
||||
return;
|
||||
|
||||
DialogueManager.Instance.StartConversation(
|
||||
_pausedConversation,
|
||||
_pausedChoiceAnchor,
|
||||
_pausedAnimationSettings,
|
||||
_pausedDialogueUI,
|
||||
_owner
|
||||
);
|
||||
|
||||
ClearPausedDialogue();
|
||||
}
|
||||
|
||||
public void MarkAsCompleted()
|
||||
{
|
||||
IsCompleted = true;
|
||||
ClearPausedDialogue();
|
||||
}
|
||||
|
||||
public void AllowRetrigger()
|
||||
{
|
||||
IsCompleted = false;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
IsCompleted = false;
|
||||
ClearPausedDialogue();
|
||||
}
|
||||
|
||||
private void ClearPausedDialogue()
|
||||
{
|
||||
_pausedConversation = null;
|
||||
_pausedNode = null;
|
||||
_pausedChoiceAnchor = null;
|
||||
_pausedAnimationSettings = null;
|
||||
_pausedDialogueUI = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b328dc63103543940898cfa2dbd4ec36
|
||||
@@ -0,0 +1,152 @@
|
||||
using UnityEngine;
|
||||
using Unheimlich.Features.Dialogue.Data;
|
||||
using Unheimlich.Features.Dialogue;
|
||||
using Unheimlich.Features.Dialogue.UI;
|
||||
using Unheimlich.Core.Events;
|
||||
|
||||
namespace Unheimlich.Features.Dialogue
|
||||
{
|
||||
/// <summary>
|
||||
/// Trigger zone that starts a dialogue sequence when player enters.
|
||||
/// Supports pause/resume on exit/enter and optional one-time triggering.
|
||||
/// </summary>
|
||||
public class DialogueTriggerZone : TriggerZoneBase
|
||||
{
|
||||
[Header("Dialogue Reference")]
|
||||
[Tooltip("DialogueConversation to use for this trigger zone")]
|
||||
[SerializeField] private DialogueConversation _conversation;
|
||||
|
||||
[Header("Trigger Settings")]
|
||||
[Tooltip("Can the dialogue be retriggered after completion")]
|
||||
[SerializeField] private bool _isRetriggerable = false;
|
||||
|
||||
protected override bool IsRetriggerable => _isRetriggerable;
|
||||
|
||||
[Header("Distance Check")]
|
||||
[Tooltip("Pause dialogue if player exits the trigger zone")]
|
||||
[SerializeField] private bool _pauseOnExit = true;
|
||||
|
||||
[Header("Floating Word Anchor")]
|
||||
[SerializeField] private Transform _floatingWordAnchor;
|
||||
|
||||
[Header("Animation Settings")]
|
||||
[SerializeField] private DialogueAnimationSettings _animationSettings;
|
||||
|
||||
[Header("UI Reference")]
|
||||
[Tooltip("Local DialogueUI for this trigger zone. Uses DialogueManager's UI if null")]
|
||||
[SerializeField] private DialogueUI _localDialogueUI;
|
||||
|
||||
[Header("Blur Effect")]
|
||||
[Tooltip("Enable blur effect when dialogue starts")]
|
||||
[SerializeField] private bool _enableBlurEffect = true;
|
||||
|
||||
private bool _hasTriggered = false;
|
||||
private DialogueStateHandler _dialogueState;
|
||||
|
||||
protected override void Awake()
|
||||
{
|
||||
base.Awake();
|
||||
_dialogueState = new DialogueStateHandler(this);
|
||||
|
||||
if (_floatingWordAnchor == null)
|
||||
_floatingWordAnchor = transform;
|
||||
}
|
||||
|
||||
protected override void OnPlayerEntered(Collider playerCollider)
|
||||
{
|
||||
if (_dialogueState.IsCompleted && !_isRetriggerable)
|
||||
return;
|
||||
|
||||
if (_dialogueState.HasPausedDialogue)
|
||||
{
|
||||
_dialogueState.ResumeDialogue();
|
||||
}
|
||||
else if (!_hasTriggered || (_isRetriggerable && _dialogueState.IsCompleted))
|
||||
{
|
||||
TriggerDialogue();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnPlayerExited(Collider playerCollider)
|
||||
{
|
||||
if (_pauseOnExit && DialogueManager.Instance != null && DialogueManager.Instance.IsDialogueActive)
|
||||
{
|
||||
if ((Object)DialogueManager.Instance.CurrentOwner == this)
|
||||
_dialogueState.PauseDialogue();
|
||||
}
|
||||
}
|
||||
|
||||
private void TriggerDialogue()
|
||||
{
|
||||
if (DialogueManager.Instance == null)
|
||||
{
|
||||
Debug.LogError("DialogueManager not found in scene!", this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_conversation == null)
|
||||
{
|
||||
Debug.LogError($"{gameObject.name}: No conversation assigned!", this);
|
||||
return;
|
||||
}
|
||||
|
||||
_hasTriggered = true;
|
||||
|
||||
DialogueManager.Instance.StartConversation(
|
||||
_conversation,
|
||||
_floatingWordAnchor,
|
||||
_animationSettings,
|
||||
_localDialogueUI,
|
||||
this,
|
||||
_enableBlurEffect
|
||||
);
|
||||
|
||||
DialogueManager.Instance.OnDialogueCompleted += OnDialogueCompleted;
|
||||
}
|
||||
|
||||
private void OnDialogueCompleted(object owner)
|
||||
{
|
||||
if ((Object)owner == this)
|
||||
{
|
||||
_dialogueState.MarkAsCompleted();
|
||||
|
||||
if (_isRetriggerable)
|
||||
{
|
||||
_hasTriggered = false;
|
||||
_dialogueState.AllowRetrigger();
|
||||
}
|
||||
}
|
||||
|
||||
if (DialogueManager.Instance != null)
|
||||
DialogueManager.Instance.OnDialogueCompleted -= OnDialogueCompleted;
|
||||
}
|
||||
|
||||
public void ResetTrigger()
|
||||
{
|
||||
_hasTriggered = false;
|
||||
_dialogueState.Reset();
|
||||
ResetZone();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (DialogueManager.Instance != null)
|
||||
DialogueManager.Instance.OnDialogueCompleted -= OnDialogueCompleted;
|
||||
}
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
if (_localDialogueUI == null)
|
||||
Debug.LogWarning($"[DialogueTriggerZone] '{gameObject.name}' has no Local Dialogue UI assigned. Assign the appropriate DialogueUI prefab variant (World, NPC, Room, Screen, etc.).", this);
|
||||
}
|
||||
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
if (_floatingWordAnchor != null)
|
||||
{
|
||||
Gizmos.color = Color.cyan;
|
||||
Gizmos.DrawWireSphere(_floatingWordAnchor.position, 0.2f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f33be20afd5d4c843a800ba1d8dd8437
|
||||
@@ -0,0 +1,142 @@
|
||||
using UnityEngine;
|
||||
using Unheimlich.Core.Interaction;
|
||||
using Unheimlich.Features.Dialogue.Data;
|
||||
using Unheimlich.Features.Dialogue;
|
||||
using Unheimlich.Features.Player;
|
||||
using Unheimlich.Features.Dialogue.UI;
|
||||
|
||||
namespace Unheimlich.Features.Dialogue
|
||||
{
|
||||
/// <summary>
|
||||
/// NPC interactable that starts dialogue conversations.
|
||||
/// Rotates to face player during dialogue and pauses if player moves too far away.
|
||||
/// </summary>
|
||||
public class NPCDialogueTrigger : InteractableBase
|
||||
{
|
||||
[Header("Dialogue Reference")]
|
||||
[Tooltip("DialogueConversation to use for this NPC")]
|
||||
public DialogueConversation Conversation;
|
||||
|
||||
[Header("Trigger Settings")]
|
||||
[Tooltip("Can the dialogue be retriggered after completion")]
|
||||
[SerializeField] private bool _isRetriggerable = true;
|
||||
|
||||
[Header("Floating Word Setting")]
|
||||
[SerializeField] private Transform _floatingWordAnchor;
|
||||
|
||||
[Header("Animation Settings")]
|
||||
[SerializeField] private DialogueAnimationSettings _animationSettings;
|
||||
|
||||
[Header("UI Reference")]
|
||||
[Tooltip("Local DialogueUI for this NPC. Uses DialogueManager's UI if null")]
|
||||
[SerializeField] private DialogueUI _localDialogueUI;
|
||||
|
||||
[Header("Distance Settings")]
|
||||
[Tooltip("Maximum distance before dialogue pauses")]
|
||||
[SerializeField] private float _maxDialogueDistance = 5f;
|
||||
|
||||
[Header("Blur Effect")]
|
||||
[Tooltip("Enable blur effect when dialogue starts")]
|
||||
[SerializeField] private bool _enableBlurEffect = false;
|
||||
|
||||
public Transform FloatingWordAnchor => _floatingWordAnchor;
|
||||
|
||||
private bool _isDialogueActive = false;
|
||||
private DialogueStateHandler _dialogueState;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_dialogueState = new DialogueStateHandler(this);
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
InteractionText = "Talk";
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (_isDialogueActive && PlayerController.Instance != null)
|
||||
{
|
||||
float distance = Vector3.Distance(transform.position, PlayerController.Instance.transform.position);
|
||||
|
||||
if (distance > _maxDialogueDistance && DialogueManager.Instance != null && DialogueManager.Instance.IsDialogueActive)
|
||||
{
|
||||
if ((Object)DialogueManager.Instance.CurrentOwner == this)
|
||||
{
|
||||
_dialogueState.PauseDialogue();
|
||||
_isDialogueActive = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
LookAtPlayer();
|
||||
}
|
||||
}
|
||||
|
||||
public override void Interact()
|
||||
{
|
||||
if (DialogueManager.Instance == null)
|
||||
return;
|
||||
|
||||
if (_dialogueState.IsCompleted && !_isRetriggerable)
|
||||
return;
|
||||
|
||||
if (_dialogueState.HasPausedDialogue)
|
||||
{
|
||||
_dialogueState.ResumeDialogue();
|
||||
DialogueManager.Instance.OnDialogueCompleted += OnDialogueCompleted;
|
||||
_isDialogueActive = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Conversation == null)
|
||||
{
|
||||
Debug.LogWarning($"NPC {gameObject.name} has no conversation configured!");
|
||||
return;
|
||||
}
|
||||
|
||||
DialogueManager.Instance.StartConversation(
|
||||
Conversation,
|
||||
_floatingWordAnchor,
|
||||
_animationSettings,
|
||||
_localDialogueUI,
|
||||
this,
|
||||
_enableBlurEffect
|
||||
);
|
||||
DialogueManager.Instance.OnDialogueCompleted += OnDialogueCompleted;
|
||||
_isDialogueActive = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDialogueCompleted(object owner)
|
||||
{
|
||||
if ((Object)owner == this)
|
||||
{
|
||||
_dialogueState.MarkAsCompleted();
|
||||
|
||||
if (_isRetriggerable)
|
||||
_dialogueState.AllowRetrigger();
|
||||
|
||||
_isDialogueActive = false;
|
||||
}
|
||||
|
||||
if (DialogueManager.Instance != null)
|
||||
DialogueManager.Instance.OnDialogueCompleted -= OnDialogueCompleted;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (DialogueManager.Instance != null)
|
||||
DialogueManager.Instance.OnDialogueCompleted -= OnDialogueCompleted;
|
||||
}
|
||||
|
||||
public void LookAtPlayer()
|
||||
{
|
||||
if (PlayerController.Instance == null) return;
|
||||
Vector3 lookAtPosition = PlayerController.Instance.transform.position;
|
||||
lookAtPosition.y = transform.position.y;
|
||||
|
||||
transform.LookAt(lookAtPosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8696519b7eabaab4dbe80f2a4744cea0
|
||||
Reference in New Issue
Block a user