Mini demo with all features

This commit is contained in:
2026-02-23 14:10:46 +01:00
parent 5b1bdaeb90
commit d9326e48cc
176 changed files with 32668 additions and 564 deletions
@@ -411,6 +411,14 @@ namespace Unheimlich.Audio
StopListening();
}
private void OnEnable()
{
if (_continuousAudioSource != null && !_continuousAudioSource.isPlaying && _emitContinuously)
{
StartContinuousEmission();
}
}
private void OnDrawGizmosSelected()
{
Gizmos.color = Color.cyan;
@@ -2,10 +2,6 @@ using UnityEngine;
namespace Unheimlich.Data.DetectiveBoard
{
/// <summary>
/// Represents a single clue/evidence node on the detective board.
/// Unlocked through various triggers (audio, objects, dialogues, hallucinations, etc.).
/// </summary>
[CreateAssetMenu(fileName = "ClueData", menuName = "Unheimlich/Detective Board/Clue Data")]
public class ClueData : ScriptableObject
{
@@ -27,15 +23,8 @@ namespace Unheimlich.Data.DetectiveBoard
[Tooltip("Color tint for the node")]
public Color NodeColor = Color.white;
[Header("Connection Rules")]
[Tooltip("IDs of clues this can connect to")]
public string[] PossibleConnections;
[Header("Meta")]
[Tooltip("Category for organization (e.g., 'Evidence', 'Witness', 'Location')")]
public string Category;
[Tooltip("Initial position on the board (normalized 0-1)")]
public Vector2 InitialPosition = new Vector2(0.5f, 0.5f);
}
}
@@ -1,42 +0,0 @@
using UnityEngine;
namespace Unheimlich.Data.DetectiveBoard
{
/// <summary>
/// Configuration for detective board UI behavior and appearance.
/// Defines node visuals, connection rules, and interaction parameters.
/// </summary>
[CreateAssetMenu(fileName = "DetectiveBoardSettings", menuName = "Unheimlich/Detective Board/Board Settings")]
public class DetectiveBoardSettings : ScriptableObject
{
[Header("Node Visuals")]
[Tooltip("Default sprite for nodes (post-it/card)")]
public Sprite DefaultNodeSprite;
[Tooltip("Scale of node objects on the board")]
public float NodeScale = 1f;
[Tooltip("Spacing between nodes when auto-arranged")]
public float NodeSpacing = 0.5f;
[Header("Connection Visuals")]
[Tooltip("Line width for connections")]
public float ConnectionLineWidth = 0.05f;
[Tooltip("Color for confirmed connections")]
public Color ConfirmedConnectionColor = Color.green;
[Tooltip("Color for uncertain connections")]
public Color UncertainConnectionColor = Color.yellow;
[Tooltip("Color for eliminated connections")]
public Color EliminatedConnectionColor = Color.red;
[Header("Interaction")]
[Tooltip("Distance at which nodes can be interacted with")]
public float InteractionRange = 2f;
[Tooltip("Highlight color when hovering over a node")]
public Color HoverHighlightColor = new Color(1f, 1f, 0.5f, 1f);
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: f6b20768fe570fb429fcf6a348431232
@@ -1,6 +1,7 @@
using UnityEngine;
using System;
using System.Collections.Generic;
using Unheimlich.UI.DetectiveBoard;
namespace Unheimlich.Data.DetectiveBoard
{
@@ -9,15 +10,43 @@ namespace Unheimlich.Data.DetectiveBoard
{
public string NodeID;
public Vector2 Position;
public float Rotation;
public bool IsEliminated;
public List<string> ConnectedNodeIDs = new List<string>();
public List<ConnectionData> Connections = new List<ConnectionData>();
public PostItNodeData(string id, Vector2 position)
{
NodeID = id;
Position = position;
Rotation = 0f;
IsEliminated = false;
}
public List<string> ConnectedNodeIDs
{
get
{
List<string> ids = new List<string>();
foreach (var conn in Connections)
{
ids.Add(conn.TargetNodeID);
}
return ids;
}
}
}
[Serializable]
public class ConnectionData
{
public string TargetNodeID;
public LineColorState ColorState;
public ConnectionData(string targetId, LineColorState colorState = LineColorState.Green)
{
TargetNodeID = targetId;
ColorState = colorState;
}
}
[Serializable]
@@ -16,6 +16,17 @@ namespace Unheimlich.Data.Dialogues
[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;
@@ -9,6 +9,9 @@ namespace Unheimlich.Data.Dialogues
[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")]
@@ -20,6 +23,9 @@ namespace Unheimlich.Data.Dialogues
public string GetLocalizedChoiceText()
{
if (!string.IsNullOrEmpty(ChoiceTextOverride))
return ChoiceTextOverride;
if (ChoiceText != null)
return OctoLocalization.GetLocalizedText(ChoiceText);
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using UnityEngine;
using OctoCore.Localization;
using Unheimlich.Data.DetectiveBoard;
namespace Unheimlich.Data.Dialogues
{
@@ -19,6 +20,10 @@ namespace Unheimlich.Data.Dialogues
[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)
@@ -8,12 +8,14 @@ public class DialogueNodeEditor : Editor
private SerializedProperty _dialogueTextProp;
private SerializedProperty _choicesProp;
private SerializedProperty _autoCloseDelayProp;
private SerializedProperty _clueToUnlockProp;
private void OnEnable()
{
_dialogueTextProp = serializedObject.FindProperty("DialogueText");
_choicesProp = serializedObject.FindProperty("Choices");
_autoCloseDelayProp = serializedObject.FindProperty("AutoCloseDelay");
_clueToUnlockProp = serializedObject.FindProperty("ClueToUnlock");
}
public override void OnInspectorGUI()
@@ -52,6 +54,20 @@ public class DialogueNodeEditor : Editor
EditorGUILayout.PropertyField(_choicesProp, true);
EditorGUILayout.Space(10);
EditorGUILayout.LabelField("Detective Board Integration", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(_clueToUnlockProp, new GUIContent("Clue To Unlock"));
if (_clueToUnlockProp.objectReferenceValue != null)
{
EditorGUILayout.HelpBox("This clue will be unlocked on the Detective Board when this dialogue node is reached.", MessageType.Info);
}
else
{
EditorGUILayout.HelpBox("Optional: Assign a ClueData to unlock an evidence on the Detective Board when reaching this node.", MessageType.None);
}
EditorGUILayout.Space(10);
EditorGUILayout.BeginHorizontal();
@@ -12,17 +12,13 @@ namespace Unheimlich.Gameplay.Player
{
public class DiapasonListeningHandlerV2 : Singleton<DiapasonListeningHandlerV2>
{
[Header("Systems")]
[SerializeField] private DiapasonEquipmentManager _equipmentManager;
[SerializeField] private CameraPanController _cameraPanController;
[Header("Settings")]
[SerializeField] private bool _disablePlayerMovementDuringListen = true;
[SerializeField] private bool _enablePanMode = true;
[Header("Visual Feedback")]
[SerializeField] private float _vignetteIntensity = 0.5f;
[SerializeField] private ListeningVignetteEffect _vignetteEffect;
[Tooltip("Vignette intensity during listening (0-1)")]
[SerializeField] [Range(0f, 1f)] private float _vignetteIntensity = 0.3f;
[Header("Audio")]
[SerializeField] private AudioClip _breathInClip;
@@ -46,23 +42,9 @@ namespace Unheimlich.Gameplay.Player
if (_playerAudioSource == null)
_playerAudioSource = gameObject.AddComponent<AudioSource>();
if (_cameraPanController == null)
{
_cameraPanController = GetComponent<CameraPanController>();
if (_cameraPanController == null)
{
Debug.LogWarning("[DiapasonV2] CameraPanController not found!");
}
}
if (_vignetteEffect == null)
{
_vignetteEffect = GetComponentInChildren<ListeningVignetteEffect>();
}
_cameraHeadBob = GetComponentInChildren<CameraHeadBob>();
if (_equipmentManager != null)
if (PlayerController.Instance != null)
{
SubscribeToEquipmentEvents();
}
@@ -70,7 +52,7 @@ namespace Unheimlich.Gameplay.Player
void OnDestroy()
{
if (_equipmentManager != null)
if (PlayerController.Instance != null && PlayerController.Instance.DiapasonEquipment != null)
{
UnsubscribeFromEquipmentEvents();
}
@@ -99,15 +81,15 @@ namespace Unheimlich.Gameplay.Player
}
}
if (IsListening && _cameraPanController != null)
if (IsListening && PlayerController.Instance?.CameraPanController != null)
{
_cameraPanController.UpdatePan();
PlayerController.Instance.CameraPanController.UpdatePan();
}
}
private void StartListeningMode()
{
if (_isFadingOut || _equipmentManager == null)
if (_isFadingOut || PlayerController.Instance?.DiapasonEquipment == null)
return;
if (_listeningCoroutine != null)
@@ -126,7 +108,7 @@ namespace Unheimlich.Gameplay.Player
PlayerController.Instance.Movement.enabled = false;
}
_equipmentManager.EquipDiapason();
PlayerController.Instance.DiapasonEquipment.EquipDiapason();
PlayBreathIn();
yield return new WaitForSeconds(_breathInDuration);
@@ -144,10 +126,10 @@ namespace Unheimlich.Gameplay.Player
EnableVignette(true);
if (_enablePanMode && _cameraPanController != null)
if (_enablePanMode && PlayerController.Instance?.CameraPanController != null)
{
_cameraPanController.SaveOriginalState();
_cameraPanController.StartPan();
PlayerController.Instance.CameraPanController.SaveOriginalState();
PlayerController.Instance.CameraPanController.StartPan();
}
Cursor.lockState = CursorLockMode.Confined;
@@ -166,18 +148,18 @@ namespace Unheimlich.Gameplay.Player
IsConcentrating = false;
_isFadingOut = true;
_equipmentManager.UnequipDiapason();
PlayerController.Instance.DiapasonEquipment.UnequipDiapason();
EnableVignette(false);
if (_enablePanMode && _cameraPanController != null)
if (_enablePanMode && PlayerController.Instance?.CameraPanController != null)
{
_cameraPanController.StopPan();
_cameraPanController.ReturnToOriginalPosition(() =>
PlayerController.Instance.CameraPanController.StopPan();
PlayerController.Instance.CameraPanController.ReturnToOriginalPosition(() =>
{
if (_originalCameraSettings != null)
if (_originalCameraSettings != null && PlayerController.Instance?.CameraPanController != null)
{
_cameraPanController.Settings = _originalCameraSettings;
PlayerController.Instance.CameraPanController.Settings = _originalCameraSettings;
_originalCameraSettings = null;
}
@@ -215,15 +197,17 @@ namespace Unheimlich.Gameplay.Player
private void EnableVignette(bool enable)
{
if (_vignetteEffect == null)
if (ListeningVignetteEffect.Instance == null)
return;
if (enable)
{
_vignetteEffect.SetIntensity(_vignetteIntensity);
ListeningVignetteEffect.Instance.EnableVignetteWithIntensity(_vignetteIntensity);
}
else
{
ListeningVignetteEffect.Instance.EnableVignette(false);
}
_vignetteEffect.EnableVignette(enable);
}
}
}
@@ -1,24 +1,28 @@
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using Unheimlich.Core;
namespace Unheimlich.Gameplay.Player
{
public class ListeningVignetteEffect : MonoBehaviour
public class ListeningVignetteEffect : Singleton<ListeningVignetteEffect>
{
[Header("References")]
[Tooltip("Dedicated Volume for listening mode (not shared with dialogue system)")]
[SerializeField] private Volume _listeningVolume;
[Header("Settings")]
[SerializeField] private float _targetIntensity = 0.5f;
[Tooltip("Target intensity when listening (0-1)")]
[SerializeField] [Range(0f, 1f)] private float _targetIntensity = 0.3f;
[Tooltip("Speed of vignette fade in/out")]
[SerializeField] private float _transitionSpeed = 2f;
private Vignette _vignette;
private bool _isEnabled;
private VolumeProfile _volumeProfile;
private void Awake()
protected override void OnAwake()
{
if (_listeningVolume == null)
{
@@ -79,6 +83,12 @@ namespace Unheimlich.Gameplay.Player
if (_vignette != null)
_vignette.intensity.value = _targetIntensity;
}
public void EnableVignetteWithIntensity(float intensity)
{
SetIntensity(intensity);
EnableVignette(true);
}
private void OnDestroy()
{
@@ -4,6 +4,8 @@ using Unheimlich.Core;
using Unheimlich.Data;
using Unheimlich.Managers;
using Unheimlich.Gameplay.CameraControl;
using Unheimlich.Interactions.Core;
using Unheimlich.Gameplay.DetectiveBoard;
namespace Unheimlich.Gameplay.Player
{
@@ -23,6 +25,12 @@ namespace Unheimlich.Gameplay.Player
public Transform CameraPivot { get; private set; }
public CameraEffects CameraEffects { get; private set; }
public CameraPanController CameraPanController { get; private set; }
public DiapasonListeningHandlerV2 DiapasonHandler { get; private set; }
public DiapasonEquipmentManager DiapasonEquipment { get; private set; }
public PlayerInspectionHandler InspectionHandler { get; private set; }
public DetectiveBoardController DetectiveBoardController { get; private set; }
public OctoIkArmsImproved ArmsSystem { get; private set; }
public InputManager Inputs => InputManager.Instance;
public bool IsMoving
@@ -61,7 +69,13 @@ namespace Unheimlich.Gameplay.Player
CharController = GetComponent<CharacterController>();
CameraEffects = GetComponent<CameraEffects>();
CameraPanController = GetComponent<CameraPanController>();
DiapasonHandler = GetComponent<DiapasonListeningHandlerV2>();
DiapasonEquipment = GetComponent<DiapasonEquipmentManager>();
InspectionHandler = GetComponent<PlayerInspectionHandler>();
DetectiveBoardController = GetComponent<DetectiveBoardController>();
PlayerCamera = GetComponentInChildren<Camera>();
ArmsSystem = GetComponentInChildren<OctoIkArmsImproved>();
if (PlayerCamera != null)
{
@@ -56,9 +56,9 @@ namespace Unheimlich.Interactions.Dialogues
private void Update()
{
if (_isDialogueActive && Player.Instance != null)
if (_isDialogueActive && PlayerController.Instance != null)
{
float distance = Vector3.Distance(transform.position, Player.Instance.transform.position);
float distance = Vector3.Distance(transform.position, PlayerController.Instance.transform.position);
if (distance > _maxDialogueDistance && DialogueManager.Instance != null && DialogueManager.Instance.IsDialogueActive)
{
@@ -131,9 +131,8 @@ namespace Unheimlich.Interactions.Dialogues
public void LookAtPlayer()
{
if (Player.Instance == null) return;
Vector3 lookAtPosition = Player.Instance.transform.position;
if (PlayerController.Instance == null) return;
Vector3 lookAtPosition = PlayerController.Instance.transform.position;
lookAtPosition.y = transform.position.y;
transform.LookAt(lookAtPosition);
@@ -42,7 +42,7 @@ namespace Unheimlich.Interactions.FloatingWords
[Tooltip("Maximum width for short texts (1-3 words)")]
[SerializeField] private float _shortTextMaxWidth = 80f;
[Tooltip("Maximum width for medium texts (4-5 words)")]
[SerializeField] private float _mediumTextMaxWidth = 150f;
[SerializeField] private float _mediumTextMaxWidth = 120f;
[Tooltip("Maximum width for long texts (6-10 words)")]
[SerializeField] private float _longTextMaxWidth = 200f;
[Tooltip("Maximum width for very long texts (11+ words)")]
@@ -123,10 +123,6 @@ namespace Unheimlich.Interactions.FloatingWords
ghostColor.a = GetGhostTrailAlpha();
_ghostTextComponent.color = ghostColor;
Canvas ghostCanvas = ghostObj.AddComponent<Canvas>();
ghostCanvas.overrideSorting = true;
ghostCanvas.sortingOrder = -1;
_positionHistory.Clear();
}
}
@@ -266,18 +262,18 @@ namespace Unheimlich.Interactions.FloatingWords
DialogueAnimationSettings settings = GetAnimationSettings();
float textScaleMultiplier = settings != null ? settings.FocusedTextScaleMultiplier : 0.9f;
float textScaleDuration = settings != null ? settings.FocusedTextScaleDuration : 0.3f;
SetTextScale(textScaleMultiplier, textScaleDuration);
SetTextScale(_originalTextScale * textScaleMultiplier, textScaleDuration);
}
transform.localScale = Vector3.one * Mathf.Lerp(_originalScale, MAX_FOCUS_SCALE, progress);
// transform.localScale = Vector3.one * Mathf.Lerp(_originalScale, MAX_FOCUS_SCALE, progress);
bool shouldMoveToPlayer = GetMoveTowardPlayerOnFocus();
if (shouldMoveToPlayer && Player.Instance != null)
if (shouldMoveToPlayer && PlayerController.Instance != null)
{
float distance = GetFocusDistanceFromPlayer();
Vector3 targetPos = Player.Instance.transform.position
+ Player.Instance.transform.forward * distance;
Vector3 targetPos = PlayerController.Instance.transform.position
+ PlayerController.Instance.transform.forward * distance;
transform.position = Vector3.Lerp(transform.position, targetPos, Time.deltaTime * _focusLerpSpeed);
}
@@ -38,6 +38,9 @@ namespace Unheimlich.Interactions.FloatingWords
public float FocusDistanceFromPlayer;
public bool DisableFloatingEffects;
public float DefaultScaleMultiplier;
public bool EnableScaleVariation;
public float MinScaleVariation;
public float MaxScaleVariation;
}
private static SpawnConfig GetConfig(ObjectDescriptionSettings settings)
@@ -96,7 +99,10 @@ namespace Unheimlich.Interactions.FloatingWords
MoveTowardPlayerOnFocus = settings.MoveTowardPlayerOnFocus,
FocusDistanceFromPlayer = settings.FocusDistanceFromPlayer,
DisableFloatingEffects = false,
DefaultScaleMultiplier = settings.DefaultScaleMultiplier
DefaultScaleMultiplier = settings.DefaultScaleMultiplier,
EnableScaleVariation = settings.EnableScaleVariation,
MinScaleVariation = settings.MinScaleVariation,
MaxScaleVariation = settings.MaxScaleVariation
};
}
@@ -144,7 +150,13 @@ namespace Unheimlich.Interactions.FloatingWords
ChoiceFocusHandler word = Object.Instantiate(wordPrefab, parent);
word.transform.localPosition = localOffset;
word.transform.localRotation = Quaternion.identity;
word.transform.localScale = Vector3.one * config.DefaultScaleMultiplier;
float scaleMultiplier = config.DefaultScaleMultiplier;
if (config.EnableScaleVariation)
{
scaleMultiplier *= Random.Range(config.MinScaleVariation, config.MaxScaleVariation);
}
word.transform.localScale = Vector3.one * scaleMultiplier;
CanvasGroup canvasGroup = word.GetComponent<CanvasGroup>();
if (canvasGroup == null)
@@ -170,7 +182,13 @@ namespace Unheimlich.Interactions.FloatingWords
ChoiceFocusHandler word = Object.Instantiate(prefab, parent);
word.transform.localPosition = Vector3.up * i * 50f;
word.transform.localRotation = Quaternion.identity;
word.transform.localScale = Vector3.one * config.DefaultScaleMultiplier;
float scaleMultiplier = config.DefaultScaleMultiplier;
if (config.EnableScaleVariation)
{
scaleMultiplier *= Random.Range(config.MinScaleVariation, config.MaxScaleVariation);
}
word.transform.localScale = Vector3.one * scaleMultiplier;
word.Setup(choices[i], config);
words.Add(word);
}
@@ -180,16 +198,22 @@ namespace Unheimlich.Interactions.FloatingWords
private static List<ChoiceFocusHandler> SpawnInFrontOfPlayer(List<DialogueChoiceData> choices, ChoiceFocusHandler prefab, Transform parent, SpawnConfig config)
{
List<ChoiceFocusHandler> words = new List<ChoiceFocusHandler>();
if (Player.Instance == null) return words;
if (PlayerController.Instance == null) return words;
Vector3 playerForward = Player.Instance.transform.forward;
Vector3 playerForward = PlayerController.Instance.transform.forward;
for (int i = 0; i < choices.Count; i++)
{
ChoiceFocusHandler word = Object.Instantiate(prefab, parent);
Vector3 offset = playerForward * 2f + Vector3.up * (i - choices.Count / 2f) * (config.SpawnRadius / choices.Count / 100f);
word.transform.position = Player.Instance.transform.position + offset;
word.transform.LookAt(Player.Instance.transform);
word.transform.localScale = Vector3.one * config.DefaultScaleMultiplier;
word.transform.position = PlayerController.Instance.transform.position + offset;
word.transform.LookAt(PlayerController.Instance.transform);
float scaleMultiplier = config.DefaultScaleMultiplier;
if (config.EnableScaleVariation)
{
scaleMultiplier *= Random.Range(config.MinScaleVariation, config.MaxScaleVariation);
}
word.transform.localScale = Vector3.one * scaleMultiplier;
word.Setup(choices[i], config);
words.Add(word);
}
@@ -200,9 +224,9 @@ namespace Unheimlich.Interactions.FloatingWords
{
List<ChoiceFocusHandler> words = new List<ChoiceFocusHandler>();
originalPositions = new Vector3[choices.Count];
if (Player.Instance == null) return words;
if (PlayerController.Instance == null) return words;
parent.position = Player.Instance.transform.position;
parent.position = PlayerController.Instance.transform.position;
parent.rotation = Quaternion.identity;
float angleStep = 360f / choices.Count;
@@ -214,8 +238,15 @@ namespace Unheimlich.Interactions.FloatingWords
ChoiceFocusHandler word = Object.Instantiate(prefab, parent);
word.transform.localPosition = localOffset;
word.transform.LookAt(Player.Instance.transform);
word.transform.localScale = Vector3.one * config.DefaultScaleMultiplier;
word.transform.LookAt(PlayerController.Instance.transform);
float scaleMultiplier = config.DefaultScaleMultiplier;
if (config.EnableScaleVariation)
{
scaleMultiplier *= Random.Range(config.MinScaleVariation, config.MaxScaleVariation);
}
word.transform.localScale = Vector3.one * scaleMultiplier;
word.Setup(choices[i], config);
words.Add(word);
}
@@ -1,7 +1,9 @@
using UnityEngine;
using System.Collections.Generic;
using Unheimlich.Data.Dialogues;
using Unheimlich.Data.DetectiveBoard;
using Unheimlich.Gameplay.Player;
using Unheimlich.Managers;
using OctoCore.Localization;
namespace Unheimlich.Interactions.FloatingWords
@@ -17,6 +19,7 @@ namespace Unheimlich.Interactions.FloatingWords
[Header("Settings")]
[SerializeField] private ObjectDescriptionSettings _settings;
[SerializeField] private float _raycastDistance = 100f;
[Header("Anchor")]
[SerializeField] private Transform _descriptionAnchor;
@@ -24,6 +27,10 @@ namespace Unheimlich.Interactions.FloatingWords
[Header("Prefab")]
[SerializeField] private ChoiceFocusHandler _wordPrefab;
[Header("Detective Board Integration")]
[Tooltip("Clue to unlock when words are fully merged (optional)")]
[SerializeField] private ClueData _clueToUnlock;
private Transform _playerTransform;
private Camera _mainCamera;
private List<ChoiceFocusHandler> _spawnedWords = new List<ChoiceFocusHandler>();
@@ -42,7 +49,7 @@ namespace Unheimlich.Interactions.FloatingWords
void Start()
{
_playerTransform = Player.Instance.transform;
_playerTransform = PlayerController.Instance.transform;
_mainCamera = Camera.main;
if (_playerTransform == null)
@@ -113,7 +120,8 @@ namespace Unheimlich.Interactions.FloatingWords
{
dummyChoices.Add(new DialogueChoiceData
{
ChoiceText = _descriptionTextLocalized,
ChoiceText = null,
ChoiceTextOverride = descText,
NextNode = null
});
}
@@ -162,7 +170,7 @@ namespace Unheimlich.Interactions.FloatingWords
bool isHoveringObject = false;
if (Physics.Raycast(ray, out hit, 100f, layerMask))
if (Physics.Raycast(ray, out hit, _raycastDistance, layerMask))
{
if (hit.collider.gameObject == gameObject || hit.collider.transform.IsChildOf(transform))
isHoveringObject = true;
@@ -266,12 +274,22 @@ namespace Unheimlich.Interactions.FloatingWords
.setOnComplete(() =>
{
_hasCompletedMergeOnce = true;
UnlockClue();
DespawnWords();
});
}
}
}
private void UnlockClue()
{
if (_clueToUnlock != null && DetectiveBoardManager.Instance != null)
{
DetectiveBoardManager.Instance.UnlockClue(_clueToUnlock);
Debug.Log($"[ObjectDescription] Unlocked clue: {_clueToUnlock.name} from description merge");
}
}
private void UnmergeWords()
{
if (!_isMerged) return;
@@ -25,7 +25,6 @@ namespace Unheimlich.Managers
public ClueData Data;
public bool IsUnlocked;
public bool IsEliminated;
public Vector2 BoardPosition;
}
public class Connection
@@ -37,12 +36,10 @@ namespace Unheimlich.Managers
}
[Header("References")]
[SerializeField] private DetectiveBoardSettings _settings;
private Dictionary<string, ClueState> _clueStates = new Dictionary<string, ClueState>();
private List<Connection> _connections = new List<Connection>();
public DetectiveBoardSettings Settings => _settings;
public IReadOnlyDictionary<string, ClueState> ClueStates => _clueStates;
public IReadOnlyList<Connection> Connections => _connections;
@@ -63,8 +60,7 @@ namespace Unheimlich.Managers
{
Data = clueData,
IsUnlocked = true,
IsEliminated = false,
BoardPosition = clueData.InitialPosition
IsEliminated = false
};
OnClueUnlocked?.Invoke(clueData);
@@ -124,8 +120,7 @@ namespace Unheimlich.Managers
{
Data = clueData,
IsUnlocked = false,
IsEliminated = false,
BoardPosition = clueData.InitialPosition
IsEliminated = false
};
}
@@ -189,17 +184,6 @@ namespace Unheimlich.Managers
}
}
/// <summary>
/// Updates a clue's position on the board.
/// </summary>
public void UpdateCluePosition(string clueID, Vector2 newPosition)
{
if (_clueStates.TryGetValue(clueID, out ClueState state))
{
state.BoardPosition = newPosition;
}
}
public event System.Action<ClueData> OnClueUnlocked;
public event System.Action<string, string> OnConnectionCreated;
public event System.Action<string> OnClueEliminated;
@@ -2,6 +2,7 @@ using UnityEngine;
using System;
using Unheimlich.Core;
using Unheimlich.Data.Dialogues;
using Unheimlich.Data.DetectiveBoard;
using Unheimlich.Gameplay.Player;
using Unheimlich.UI;
using Unheimlich.Interactions.Dialogues;
@@ -150,6 +151,18 @@ namespace Unheimlich.Managers
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)
{
@@ -3,22 +3,41 @@ using UnityEngine.UI;
namespace Unheimlich.UI.DetectiveBoard
{
public enum LineColorState
{
Green = 0,
Orange = 1,
Red = 2
}
[RequireComponent(typeof(RectTransform))]
public class ConnectionLine : MonoBehaviour
{
[Header("References")]
[SerializeField] private Image _lineImage;
[SerializeField] private Image _outlineImage;
[SerializeField] private RectTransform _rectTransform;
private RectTransform _lineRectTransform;
private RectTransform _outlineRectTransform;
[Header("Settings")]
[SerializeField] private float _lineThickness = 3f;
[SerializeField] private Color _lineColor = Color.white;
[SerializeField] private Color _outlineColor = Color.yellow;
[Header("Color States")]
[SerializeField] private Color _greenColor = Color.green;
[SerializeField] private Color _orangeColor = new Color(1f, 0.5f, 0f);
[SerializeField] private Color _redColor = Color.red;
private PostItNode _startNode;
private PostItNode _endNode;
private LineColorState _colorState = LineColorState.Green;
private bool _isHovered;
public PostItNode StartNode => _startNode;
public PostItNode EndNode => _endNode;
public LineColorState ColorState => _colorState;
private void Awake()
{
@@ -26,20 +45,54 @@ namespace Unheimlich.UI.DetectiveBoard
_rectTransform = GetComponent<RectTransform>();
if (_lineImage == null)
_lineImage = GetComponent<Image>();
_lineImage = GetComponentInChildren<Image>();
if (_lineImage != null)
{
_lineRectTransform = _lineImage.GetComponent<RectTransform>();
_lineImage.raycastTarget = true;
}
if (_rectTransform != null)
{
_rectTransform.anchorMin = new Vector2(0.5f, 0.5f);
_rectTransform.anchorMax = new Vector2(0.5f, 0.5f);
_rectTransform.pivot = new Vector2(0f, 0.5f);
}
if (_lineRectTransform != null)
{
_lineRectTransform.anchorMin = new Vector2(0f, 0.5f);
_lineRectTransform.anchorMax = new Vector2(0f, 0.5f);
_lineRectTransform.pivot = new Vector2(0f, 0.5f);
}
if (_outlineImage == null)
{
_outlineImage = transform.Find("Outline")?.GetComponent<Image>();
}
if (_outlineImage != null)
{
_outlineRectTransform = _outlineImage.GetComponent<RectTransform>();
if (_outlineRectTransform != null)
{
_outlineRectTransform.anchorMin = new Vector2(0f, 0.5f);
_outlineRectTransform.anchorMax = new Vector2(0f, 0.5f);
_outlineRectTransform.pivot = new Vector2(0f, 0.5f);
}
_outlineImage.raycastTarget = false;
_outlineImage.gameObject.SetActive(false);
}
}
public void Initialize(PostItNode startNode, PostItNode endNode, Color? color = null)
public void Initialize(PostItNode startNode, PostItNode endNode)
{
_startNode = startNode;
_endNode = endNode;
_colorState = LineColorState.Green;
if (color.HasValue)
_lineColor = color.Value;
if (_lineImage != null)
_lineImage.color = _lineColor;
UpdateColorState();
UpdateLine();
}
@@ -55,19 +108,57 @@ namespace Unheimlich.UI.DetectiveBoard
float distance = direction.magnitude;
_rectTransform.anchoredPosition = startPos;
_rectTransform.sizeDelta = new Vector2(distance, _lineThickness);
float angle = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg;
_rectTransform.rotation = Quaternion.Euler(0, 0, angle);
_rectTransform.localRotation = Quaternion.Euler(0, 0, angle);
_rectTransform.pivot = new Vector2(0, 0.5f);
if (_lineRectTransform != null)
{
_lineRectTransform.sizeDelta = new Vector2(distance, _lineThickness);
}
if (_outlineRectTransform != null)
{
_outlineRectTransform.sizeDelta = new Vector2(distance, _lineThickness + 3f);
}
}
public void SetColor(Color color)
public void CycleColorState()
{
_lineColor = color;
if (_lineImage != null)
_lineImage.color = color;
_colorState = (LineColorState)(((int)_colorState + 1) % 3);
UpdateColorState();
}
public void SetColorState(LineColorState state)
{
_colorState = state;
UpdateColorState();
}
private void UpdateColorState()
{
if (_lineImage == null)
return;
Color targetColor = _colorState switch
{
LineColorState.Green => _greenColor,
LineColorState.Orange => _orangeColor,
LineColorState.Red => _redColor,
_ => _greenColor
};
_lineImage.color = targetColor;
}
public void SetHovered(bool hovered)
{
_isHovered = hovered;
if (_outlineImage != null)
{
_outlineImage.gameObject.SetActive(hovered);
}
}
public bool ConnectsNodes(PostItNode nodeA, PostItNode nodeB)
@@ -5,35 +5,85 @@ using System.Linq;
using Unheimlich.Data.DetectiveBoard;
using Unheimlich.Managers;
using Unheimlich.Core;
using Unheimlich.Gameplay.Player;
namespace Unheimlich.UI.DetectiveBoard
{
public class DetectiveBoardCanvas : Singleton<DetectiveBoardCanvas>
{
[Header("References")]
[SerializeField] private Canvas _canvas;
[SerializeField] private RectTransform _boardArea;
[Header("Prefabs")]
[SerializeField] private GameObject _postItNodePrefab;
[SerializeField] private GameObject _connectionLinePrefab;
[Header("World Space Setup")]
[SerializeField] private Camera _targetCamera;
[Header("Layout References")]
[SerializeField] private RectTransform _boardArea;
[Header("Connection Settings")]
[SerializeField] private Color _connectionColor = Color.white;
[SerializeField] private bool _allowFreeConnections = false;
[Header("Auto-Placement Settings")]
[SerializeField] private float _leftMargin = 50f;
[SerializeField] private float _topMargin = 50f;
[SerializeField] private float _nodeSpacing = 20f;
[SerializeField] private float _randomRotationRange = 10f;
[Header("Input")]
[SerializeField] private Key _connectModeKey = Key.LeftCtrl;
[SerializeField] private Key _eliminateKey = Key.X;
private Canvas _canvas;
private Camera _targetCamera;
private Dictionary<string, PostItNode> _activeNodes = new Dictionary<string, PostItNode>();
private List<ConnectionLine> _activeConnections = new List<ConnectionLine>();
private PostItNode _selectedNode;
private ConnectionLine _hoveredLine;
private bool _isConnectMode;
private DetectiveBoardSaveData _saveData;
private float _currentColumnYOffset = 0f;
public bool IsConnectMode => _isConnectMode;
public RectTransform BoardArea => _boardArea;
public void OnLineClicked(ConnectionLine line)
{
if (line != null)
{
line.CycleColorState();
SaveConnectionColorState(line);
}
}
private void SaveConnectionColorState(ConnectionLine line)
{
if (line.StartNode == null || line.EndNode == null)
return;
ConnectionData startNodeConnection = line.StartNode.NodeData.Connections.Find(c => c.TargetNodeID == line.EndNode.NodeID);
if (startNodeConnection != null)
{
startNodeConnection.ColorState = line.ColorState;
}
ConnectionData endNodeConnection = line.EndNode.NodeData.Connections.Find(c => c.TargetNodeID == line.StartNode.NodeID);
if (endNodeConnection != null)
{
endNodeConnection.ColorState = line.ColorState;
}
}
public void SetHoveredLine(ConnectionLine line)
{
if (_hoveredLine == line)
return;
if (_hoveredLine != null)
{
_hoveredLine.SetHovered(false);
}
_hoveredLine = line;
if (_hoveredLine != null)
{
_hoveredLine.SetHovered(true);
}
}
/// <summary>
/// Called by DetectiveBoardInteraction when a node is left-clicked.
@@ -63,11 +113,13 @@ namespace Unheimlich.UI.DetectiveBoard
protected override void OnAwake()
{
if (_canvas == null)
_canvas = GetComponent<Canvas>();
_canvas = GetComponent<Canvas>();
if (_canvas != null && _canvas.renderMode == RenderMode.WorldSpace)
{
if (PlayerController.Instance != null)
_targetCamera = PlayerController.Instance.PlayerCamera;
if (_targetCamera == null)
_targetCamera = Camera.main;
@@ -120,7 +172,6 @@ namespace Unheimlich.UI.DetectiveBoard
private void Update()
{
HandleConnectModeInput();
HandleEliminateInput();
}
private void HandleConnectModeInput()
@@ -138,17 +189,6 @@ namespace Unheimlich.UI.DetectiveBoard
}
}
private void HandleEliminateInput()
{
if (Keyboard.current == null)
return;
if (Keyboard.current[_eliminateKey].wasPressedThisFrame && _selectedNode != null)
{
ToggleNodeEliminated(_selectedNode);
}
}
public void AddClueNode(ClueData clueData)
{
if (_activeNodes.ContainsKey(clueData.ClueID))
@@ -169,12 +209,8 @@ namespace Unheimlich.UI.DetectiveBoard
PostItNodeData nodeData = _saveData.GetNodeData(clueData.ClueID);
if (nodeData == null)
{
Vector2 canvasSize = _boardArea.rect.size;
Vector2 randomPos = new Vector2(
Random.Range(-canvasSize.x * 0.4f, canvasSize.x * 0.4f),
Random.Range(-canvasSize.y * 0.4f, canvasSize.y * 0.4f)
);
nodeData = new PostItNodeData(clueData.ClueID, randomPos);
Vector2 autoPlacedPos = CalculateAutoPlacementPosition();
nodeData = new PostItNodeData(clueData.ClueID, autoPlacedPos);
_saveData.AddOrUpdateNode(nodeData);
}
@@ -191,11 +227,40 @@ namespace Unheimlich.UI.DetectiveBoard
node.Initialize(clueData, nodeData);
float savedRotation = nodeData.Rotation;
if (savedRotation == 0f)
{
savedRotation = Random.Range(-_randomRotationRange, _randomRotationRange);
nodeData.Rotation = savedRotation;
}
node.SetRotation(savedRotation);
_activeNodes.Add(clueData.ClueID, node);
RestoreConnectionsForNode(node);
}
private Vector2 CalculateAutoPlacementPosition()
{
Vector2 canvasSize = _boardArea.rect.size;
float startX = -canvasSize.x * 0.5f + _leftMargin;
float startY = canvasSize.y * 0.5f - _topMargin;
float estimatedNodeHeight = 150f;
Vector2 position = new Vector2(startX, startY - _currentColumnYOffset);
_currentColumnYOffset += estimatedNodeHeight + _nodeSpacing;
if (_currentColumnYOffset > canvasSize.y - _topMargin * 2)
{
_currentColumnYOffset = 0f;
}
return position;
}
public void RemoveClueNode(string clueID)
{
if (!_activeNodes.TryGetValue(clueID, out PostItNode node))
@@ -229,9 +294,6 @@ namespace Unheimlich.UI.DetectiveBoard
private void ToggleConnection(PostItNode nodeA, PostItNode nodeB)
{
if (!CanConnect(nodeA, nodeB))
return;
ConnectionLine existingConnection = _activeConnections.Find(c => c.ConnectsNodes(nodeA, nodeB));
if (existingConnection != null)
@@ -244,20 +306,6 @@ namespace Unheimlich.UI.DetectiveBoard
}
}
private bool CanConnect(PostItNode nodeA, PostItNode nodeB)
{
if (nodeA == null || nodeB == null)
return false;
if (_allowFreeConnections)
return true;
if (nodeA.ClueData.PossibleConnections == null || nodeA.ClueData.PossibleConnections.Length == 0)
return true;
return nodeA.ClueData.PossibleConnections.Contains(nodeB.NodeID);
}
private void CreateConnection(PostItNode startNode, PostItNode endNode)
{
GameObject lineObj = Instantiate(_connectionLinePrefab, _boardArea);
@@ -271,22 +319,34 @@ namespace Unheimlich.UI.DetectiveBoard
return;
}
line.Initialize(startNode, endNode, _connectionColor);
line.Initialize(startNode, endNode);
ConnectionData existingConnectionData = startNode.NodeData.Connections.Find(c => c.TargetNodeID == endNode.NodeID);
if (existingConnectionData != null)
{
line.SetColorState(existingConnectionData.ColorState);
}
_activeConnections.Add(line);
if (!startNode.NodeData.ConnectedNodeIDs.Contains(endNode.NodeID))
startNode.NodeData.ConnectedNodeIDs.Add(endNode.NodeID);
if (existingConnectionData == null)
{
startNode.NodeData.Connections.Add(new ConnectionData(endNode.NodeID));
}
if (!endNode.NodeData.ConnectedNodeIDs.Contains(startNode.NodeID))
endNode.NodeData.ConnectedNodeIDs.Add(startNode.NodeID);
ConnectionData reverseConnectionData = endNode.NodeData.Connections.Find(c => c.TargetNodeID == startNode.NodeID);
if (reverseConnectionData == null)
{
endNode.NodeData.Connections.Add(new ConnectionData(startNode.NodeID));
}
}
private void RemoveConnection(ConnectionLine connection)
{
if (connection.StartNode != null && connection.EndNode != null)
{
connection.StartNode.NodeData.ConnectedNodeIDs.Remove(connection.EndNode.NodeID);
connection.EndNode.NodeData.ConnectedNodeIDs.Remove(connection.StartNode.NodeID);
connection.StartNode.NodeData.Connections.RemoveAll(c => c.TargetNodeID == connection.EndNode.NodeID);
connection.EndNode.NodeData.Connections.RemoveAll(c => c.TargetNodeID == connection.StartNode.NodeID);
}
_activeConnections.Remove(connection);
@@ -322,12 +382,12 @@ namespace Unheimlich.UI.DetectiveBoard
private void RestoreConnectionsForNode(PostItNode node)
{
if (node.NodeData.ConnectedNodeIDs == null || node.NodeData.ConnectedNodeIDs.Count == 0)
if (node.NodeData.Connections == null || node.NodeData.Connections.Count == 0)
return;
foreach (string connectedID in node.NodeData.ConnectedNodeIDs)
foreach (ConnectionData connectionData in node.NodeData.Connections)
{
if (_activeNodes.TryGetValue(connectedID, out PostItNode otherNode))
if (_activeNodes.TryGetValue(connectionData.TargetNodeID, out PostItNode otherNode))
{
bool connectionExists = _activeConnections.Any(c => c.ConnectsNodes(node, otherNode));
if (!connectionExists)
@@ -366,6 +426,7 @@ namespace Unheimlich.UI.DetectiveBoard
if (nodeData != null)
{
node.SetPosition(nodeData.Position);
node.SetRotation(nodeData.Rotation);
node.SetEliminated(nodeData.IsEliminated);
}
}
@@ -2,6 +2,7 @@ using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UI;
using Unheimlich.Gameplay.DetectiveBoard;
using Unheimlich.Gameplay.Player;
using System.Collections.Generic;
using UnityEngine.EventSystems;
@@ -14,35 +15,38 @@ namespace Unheimlich.UI.DetectiveBoard
/// </summary>
public class DetectiveBoardInteraction : MonoBehaviour
{
[Header("References")]
[SerializeField] private Camera _camera;
[SerializeField] private Canvas _boardCanvas;
[SerializeField] private RectTransform _boardArea;
[SerializeField] private LayerMask _boardLayer;
[SerializeField] private DetectiveBoardController _boardController;
[Header("Settings")]
[SerializeField] private LayerMask _boardLayer;
[SerializeField] private float _minInteractionDistance = 0.1f;
[SerializeField] private float _maxInteractionDistance = 10f;
private Camera _camera;
private Canvas _boardCanvas;
private RectTransform _boardArea;
private DetectiveBoardController _boardController;
private DetectiveBoardCanvas _canvasManager;
private PostItNode _hoveredNode;
private PostItNode _selectedNode;
private ConnectionLine _hoveredLine;
private bool _isDragging;
private Plane _canvasPlane;
private Vector3 _dragOffset;
private void Start()
{
_boardCanvas = GetComponent<Canvas>();
_canvasManager = GetComponent<DetectiveBoardCanvas>();
_boardController = DetectiveBoardController.Instance;
if (_canvasManager != null && _canvasManager.BoardArea != null)
_boardArea = _canvasManager.BoardArea;
if (PlayerController.Instance != null)
_camera = PlayerController.Instance.PlayerCamera;
if (_camera == null)
_camera = Camera.main;
if (_boardController == null)
_boardController = FindFirstObjectByType<DetectiveBoardController>();
if (_canvasManager == null)
_canvasManager = GetComponent<DetectiveBoardCanvas>();
UpdateCanvasPlane();
}
@@ -63,6 +67,7 @@ namespace Unheimlich.UI.DetectiveBoard
else
{
HandleHover();
HandleLineHover();
}
HandleClick();
@@ -70,7 +75,9 @@ namespace Unheimlich.UI.DetectiveBoard
private bool IsInBoardInteractionMode()
{
return _boardController != null && _boardController.IsBoardActive;
bool isActive = _boardController != null && _boardController.IsBoardActive;
return isActive;
}
private void ClearInteractionState()
@@ -81,6 +88,12 @@ namespace Unheimlich.UI.DetectiveBoard
_hoveredNode = null;
}
if (_hoveredLine != null && _canvasManager != null)
{
_canvasManager.SetHoveredLine(null);
_hoveredLine = null;
}
if (_selectedNode != null && _isDragging)
{
_selectedNode.SetSelected(false);
@@ -119,6 +132,59 @@ namespace Unheimlich.UI.DetectiveBoard
}
}
private void HandleLineHover()
{
if (_canvasManager == null || Mouse.current == null)
return;
if (_hoveredNode != null)
{
if (_hoveredLine != null)
{
_canvasManager.SetHoveredLine(null);
_hoveredLine = null;
}
return;
}
ConnectionLine hitLine = RaycastForLine();
if (hitLine != _hoveredLine)
{
_canvasManager.SetHoveredLine(hitLine);
_hoveredLine = hitLine;
}
}
private ConnectionLine RaycastForLine()
{
if (_camera == null || Mouse.current == null)
return null;
Vector2 mousePos = Mouse.current.position.ReadValue();
GraphicRaycaster raycaster = _boardCanvas?.GetComponent<GraphicRaycaster>();
if (raycaster != null && EventSystem.current != null)
{
var pointerEventData = new PointerEventData(EventSystem.current)
{
position = mousePos
};
var results = new List<RaycastResult>();
raycaster.Raycast(pointerEventData, results);
foreach (var result in results)
{
ConnectionLine line = result.gameObject.GetComponentInParent<ConnectionLine>();
if (line != null)
return line;
}
}
return null;
}
private void HandleClick()
{
if (Mouse.current == null)
@@ -128,7 +194,18 @@ namespace Unheimlich.UI.DetectiveBoard
if (Mouse.current.leftButton.wasPressedThisFrame)
{
HandleLeftClick(hitNode);
if (hitNode != null)
{
HandleLeftClick(hitNode);
}
else
{
ConnectionLine hitLine = RaycastForLine();
if (hitLine != null && _canvasManager != null)
{
_canvasManager.OnLineClicked(hitLine);
}
}
}
else if (Mouse.current.rightButton.wasPressedThisFrame && hitNode != null && _canvasManager != null)
{
@@ -13,9 +13,11 @@ namespace Unheimlich.UI.DetectiveBoard
[SerializeField] private TextMeshProUGUI _titleText;
[SerializeField] private TextMeshProUGUI _descriptionText;
[SerializeField] private Image _iconImage;
[SerializeField] private GameObject _strikethroughLine;
[SerializeField] private Image _selectionOutline;
[Header("Effects")]
[SerializeField] private PostItScribbleEffect _scribbleEffect;
[Header("Connection Points")]
[SerializeField] private RectTransform _connectionPoint;
@@ -27,7 +29,7 @@ namespace Unheimlich.UI.DetectiveBoard
public string NodeID => _clueData?.ClueID;
public RectTransform RectTransform => _rectTransform;
public RectTransform ConnectionPoint => _connectionPoint;
public RectTransform ConnectionPoint => _rectTransform;
public ClueData ClueData => _clueData;
public PostItNodeData NodeData => _nodeData;
public bool IsEliminated => _isEliminated;
@@ -38,8 +40,8 @@ namespace Unheimlich.UI.DetectiveBoard
if (_rectTransform == null)
_rectTransform = GetComponent<RectTransform>();
if (_strikethroughLine != null)
_strikethroughLine.SetActive(false);
if (_scribbleEffect == null)
_scribbleEffect = GetComponent<PostItScribbleEffect>();
if (_selectionOutline != null)
_selectionOutline.enabled = false;
@@ -72,11 +74,6 @@ namespace Unheimlich.UI.DetectiveBoard
_background.color = _originalColor;
}
if (_connectionPoint == null)
{
_connectionPoint = _rectTransform;
}
SetPosition(nodeData.Position);
SetEliminated(nodeData.IsEliminated);
}
@@ -91,16 +88,28 @@ namespace Unheimlich.UI.DetectiveBoard
}
}
public void SetRotation(float rotation)
{
if (_rectTransform != null)
{
_rectTransform.localRotation = Quaternion.Euler(0, 0, rotation);
if (_nodeData != null)
_nodeData.Rotation = rotation;
}
}
public float GetRotation()
{
return _rectTransform != null ? _rectTransform.localEulerAngles.z : 0f;
}
public void SetEliminated(bool eliminated)
{
_isEliminated = eliminated;
if (_strikethroughLine != null)
_strikethroughLine.SetActive(eliminated);
if (_background != null)
if (_scribbleEffect != null)
{
_background.color = eliminated ? new Color(0.5f, 0.5f, 0.5f, 0.8f) : _originalColor;
_scribbleEffect.SetEliminated(eliminated);
}
if (_nodeData != null)
@@ -110,18 +119,13 @@ namespace Unheimlich.UI.DetectiveBoard
public void SetSelected(bool selected)
{
_isSelected = selected;
if (_selectionOutline != null)
{
_selectionOutline.enabled = selected;
}
}
public void SetHovered(bool hovered)
{
if (_selectionOutline != null)
{
_selectionOutline.enabled = hovered || _isSelected;
_selectionOutline.enabled = hovered;
}
}
}
@@ -0,0 +1,135 @@
using UnityEngine;
using UnityEngine.UI;
using Unheimlich.Utility;
namespace Unheimlich.UI.DetectiveBoard
{
public class PostItScribbleEffect : MonoBehaviour
{
[SerializeField] private Material _scribbleMaterialTemplate;
[SerializeField] private RectTransform _targetRectTransform;
private Image _scribbleOverlay;
private Material _scribbleMaterialInstance;
private bool _overlayCreated = false;
private bool _isEliminated = false;
private static readonly int ProgressID = Shader.PropertyToID("_Progress");
private int _scribbleTweenId = -1;
private void Awake()
{
if (_targetRectTransform == null)
_targetRectTransform = GetComponent<RectTransform>();
}
private void CreateScribbleOverlay()
{
if (_overlayCreated || _scribbleMaterialTemplate == null || _targetRectTransform == null)
return;
GameObject overlayObject = new GameObject("ScribbleOverlay");
overlayObject.transform.SetParent(_targetRectTransform, false);
_scribbleOverlay = overlayObject.AddComponent<Image>();
_scribbleOverlay.raycastTarget = false;
RectTransform overlayRect = overlayObject.GetComponent<RectTransform>();
overlayRect.anchorMin = Vector2.zero;
overlayRect.anchorMax = Vector2.one;
overlayRect.sizeDelta = Vector2.zero;
overlayRect.anchoredPosition = Vector2.zero;
_scribbleMaterialInstance = new Material(_scribbleMaterialTemplate);
_scribbleMaterialInstance.SetFloat(ProgressID, 0f);
_scribbleOverlay.material = _scribbleMaterialInstance;
_scribbleOverlay.gameObject.SetActive(false);
_overlayCreated = true;
}
public void SetEliminated(bool eliminated)
{
if (_isEliminated == eliminated)
return;
_isEliminated = eliminated;
if (eliminated)
{
ApplyScribble();
}
else
{
RemoveScribble();
}
}
private void ApplyScribble()
{
CancelActiveTweens();
CreateScribbleOverlay();
if (_scribbleOverlay == null || _scribbleMaterialInstance == null)
return;
_scribbleOverlay.gameObject.SetActive(true);
_scribbleTweenId = LeanTween.value(gameObject, 0f, 1f, 0.4f)
.setEase(LeanTweenType.easeInOutQuad)
.setOnUpdate((float val) => {
if (_scribbleMaterialInstance != null)
_scribbleMaterialInstance.SetFloat(ProgressID, val);
})
.setOnComplete(() => {
_scribbleTweenId = -1;
}).id;
}
private void RemoveScribble()
{
if (!_overlayCreated || _scribbleOverlay == null)
return;
if (!_scribbleOverlay.gameObject.activeSelf)
return;
CancelActiveTweens();
if (_scribbleMaterialInstance != null)
{
float currentProgress = _scribbleMaterialInstance.GetFloat(ProgressID);
_scribbleTweenId = LeanTween.value(gameObject, currentProgress, 0f, 0.3f)
.setEase(LeanTweenType.easeOutQuad)
.setOnUpdate((float val) => {
if (_scribbleMaterialInstance != null)
_scribbleMaterialInstance.SetFloat(ProgressID, val);
})
.setOnComplete(() => {
_scribbleTweenId = -1;
if (_scribbleOverlay != null)
_scribbleOverlay.gameObject.SetActive(false);
}).id;
}
}
private void CancelActiveTweens()
{
TweenHelper.CancelTween(ref _scribbleTweenId);
}
private void OnDestroy()
{
CancelActiveTweens();
if (_scribbleMaterialInstance != null)
Destroy(_scribbleMaterialInstance);
if (_scribbleOverlay != null)
Destroy(_scribbleOverlay.gameObject);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 009173206d77d184d86915685410c7d8
@@ -2,11 +2,11 @@ using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using Unheimlich.Core;
using Unheimlich.Gameplay.Player;
namespace Unheimlich.UI
{
[RequireComponent(typeof(Camera))]
public class DialogueBlurEffectURP : Singleton<DialogueBlurEffectURP>
public class DialogueBlurEffectURP : Singleton<DialogueBlurEffectURP>
{
[Header("Volume Settings")]
@@ -29,6 +29,11 @@ namespace Unheimlich.UI
[Tooltip("Layer(s) for dialogue (World Space UI) to render without blur")]
[SerializeField] private LayerMask _dialogueLayers;
[SerializeField] private bool _autoCreateDialogueCamera = true;
[Header("Hands Configuration")]
[Tooltip("Layer for player hands to render on top of dialogue")]
[SerializeField] private LayerMask _handsLayer;
[SerializeField] private bool _autoCreateHandsCamera = true;
private DepthOfField _dof;
private ColorAdjustments _color;
@@ -42,10 +47,11 @@ namespace Unheimlich.UI
private Camera _baseCam;
private Camera _dialogueCam;
private Camera _handsCam;
protected override void OnAwake()
{
_baseCam = GetComponent<Camera>();
if (_fadeCurve == null || _fadeCurve.keys.Length == 0)
_fadeCurve = AnimationCurve.EaseInOut(0, 0, 1, 1);
@@ -96,8 +102,11 @@ namespace Unheimlich.UI
void Start()
{
_baseCam = PlayerController.Instance.PlayerCamera;
if (_autoCreateDialogueCamera)
SetupDialogueOverlayCamera();
if (_autoCreateHandsCamera)
SetupHandsOverlayCamera();
}
void SetupDialogueOverlayCamera()
@@ -113,6 +122,8 @@ namespace Unheimlich.UI
Debug.LogWarning("[DialogueBlur] WARNING: dialogueLayers is EMPTY! Ghost words will be blurred!");
_baseCam.cullingMask &= ~_dialogueLayers;
if (_handsLayer.value != 0)
_baseCam.cullingMask &= ~_handsLayer;
var go = new GameObject("Dialogue Camera (Overlay)");
go.transform.SetParent(_baseCam.transform);
@@ -131,6 +142,38 @@ namespace Unheimlich.UI
baseData.cameraStack.Add(_dialogueCam);
}
void SetupHandsOverlayCamera()
{
if (_handsLayer.value == 0)
{
Debug.LogWarning("[DialogueBlur] WARNING: handsLayer is EMPTY! Hands camera will not be created.");
return;
}
var baseData = _baseCam.GetComponent<UniversalAdditionalCameraData>();
if (baseData == null)
{
Debug.LogError("[DialogueBlur] ERROR: Base camera has no UniversalAdditionalCameraData!");
return;
}
var go = new GameObject("Hands Camera (Overlay)");
go.transform.SetParent(_baseCam.transform);
go.transform.localPosition = Vector3.zero;
go.transform.localRotation = Quaternion.identity;
_handsCam = go.AddComponent<Camera>();
_handsCam.CopyFrom(_baseCam);
_handsCam.clearFlags = CameraClearFlags.Depth;
_handsCam.cullingMask = _handsLayer;
var overlayData = go.AddComponent<UniversalAdditionalCameraData>();
overlayData.renderType = CameraRenderType.Overlay;
overlayData.renderPostProcessing = false;
baseData.cameraStack.Add(_handsCam);
}
void Update()
{
if (!_fading) return;
@@ -186,6 +229,21 @@ namespace Unheimlich.UI
public void ShowBlur()
{
if (_dialogueCam == null && _autoCreateDialogueCamera)
{
if (_baseCam == null && PlayerController.Instance != null)
_baseCam = PlayerController.Instance.PlayerCamera;
if (_baseCam != null)
{
SetupDialogueOverlayCamera();
if (_autoCreateHandsCamera)
SetupHandsOverlayCamera();
}
else
Debug.LogWarning("[DialogueBlur] WARNING: Cannot create dialogue camera - PlayerCamera not ready!");
}
if (_fading && _target == 1f) return;
_start = _current;
+36 -10
View File
@@ -38,6 +38,7 @@ namespace Unheimlich.UI
private bool _shouldFollowPlayerRotation = false;
private Transform _currentAnchor;
private Vector3 _baseScale;
private Vector3 _basePanelScale;
private List<ChoiceFocusHandler> _activeChoices = new List<ChoiceFocusHandler>();
private float _sortTimer = 0f;
private const float SORT_INTERVAL = 0.1f;
@@ -59,24 +60,32 @@ namespace Unheimlich.UI
if (_dialogueCanvasGroup == null)
_dialogueCanvasGroup = _dialoguePanel.AddComponent<CanvasGroup>();
}
if (_dialoguePanel != null)
{
_basePanelScale = _dialoguePanel.transform.localScale;
}
}
private void Update()
{
if (Player.Instance == null || !ChoiceContainer.activeInHierarchy)
if (PlayerController.Instance == null || !ChoiceContainer.activeInHierarchy)
return;
if (_shouldFollowPlayerPosition)
ChoiceContainer.transform.position = Player.Instance.transform.position;
ChoiceContainer.transform.position = PlayerController.Instance.transform.position;
if (_shouldFollowPlayerRotation)
{
float playerYRotation = Player.Instance.transform.eulerAngles.y;
float playerYRotation = PlayerController.Instance.transform.eulerAngles.y;
ChoiceContainer.transform.rotation = Quaternion.Euler(0f, playerYRotation, 0f);
}
if (_currentAnimationSettings != null && _currentAnimationSettings.EnableDistanceScaling && !_shouldFollowPlayerPosition)
{
ApplyDistanceScaling();
ApplyDistanceScalingToPanel();
}
SortChoicesByDistance();
}
@@ -188,17 +197,17 @@ namespace Unheimlich.UI
_shouldFollowPlayerPosition = followPosition;
_shouldFollowPlayerRotation = followRotation;
if (followPosition && Player.Instance != null)
if (followPosition && PlayerController.Instance != null)
{
Vector3 playerWorldPos = Player.Instance.transform.position;
Vector3 playerWorldPos = PlayerController.Instance.transform.position;
ChoiceContainer.transform.position = playerWorldPos;
}
else
ChoiceContainer.transform.localPosition = Vector3.zero;
if (followRotation && Player.Instance != null)
if (followRotation && PlayerController.Instance != null)
{
float playerYRotation = Player.Instance.transform.eulerAngles.y;
float playerYRotation = PlayerController.Instance.transform.eulerAngles.y;
ChoiceContainer.transform.rotation = Quaternion.Euler(0f, playerYRotation, 0f);
}
else
@@ -215,7 +224,10 @@ namespace Unheimlich.UI
SpawnChoices(nodeChoices, animationSettings);
if (animationSettings != null && animationSettings.EnableDistanceScaling && !_shouldFollowPlayerPosition)
{
ApplyDistanceScaling();
ApplyDistanceScalingToPanel();
}
bool shouldRotate = animationSettings == null || animationSettings.EnableRotation;
if (shouldRotate)
@@ -275,10 +287,10 @@ namespace Unheimlich.UI
/// </summary>
private void ApplyDistanceScaling()
{
if (_currentAnchor == null || Player.Instance == null)
if (_currentAnchor == null || PlayerController.Instance == null)
return;
float distance = Vector3.Distance(_currentAnchor.position, Player.Instance.transform.position);
float distance = Vector3.Distance(_currentAnchor.position, PlayerController.Instance.transform.position);
float normalizedDistance = Mathf.InverseLerp(_currentAnimationSettings.MinDistance, _currentAnimationSettings.MaxDistance, distance);
@@ -286,6 +298,20 @@ namespace Unheimlich.UI
ChoiceContainer.transform.localScale = _baseScale * finalScale;
}
private void ApplyDistanceScalingToPanel()
{
if (_currentAnchor == null || PlayerController.Instance == null || _dialoguePanel == null)
return;
float distance = Vector3.Distance(_currentAnchor.position, PlayerController.Instance.transform.position);
float normalizedDistance = Mathf.InverseLerp(_currentAnimationSettings.MinDistance, _currentAnimationSettings.MaxDistance, distance);
float finalScale = Mathf.Lerp(_currentAnimationSettings.MinScaleMultiplier, _currentAnimationSettings.MaxScaleMultiplier, normalizedDistance);
_dialoguePanel.transform.localScale = _basePanelScale * finalScale;
}
private Vector3 GetRotationAxisVector(RotationAxis axis)
{
@@ -331,7 +357,7 @@ namespace Unheimlich.UI
/// </summary>
private void SortChoicesByDistance()
{
if (_activeChoices.Count <= 1 || Player.Instance == null)
if (_activeChoices.Count <= 1 || PlayerController.Instance == null)
return;
_sortTimer += Time.deltaTime;