Complete reorga

This commit is contained in:
2026-03-09 17:55:23 +01:00
parent d9326e48cc
commit 10faf74405
703 changed files with 13040 additions and 1533 deletions
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 41ce6b2bff04d1e4d9eb45ac2c7dd0d9
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,30 @@
using UnityEngine;
namespace Unheimlich.Features.DetectiveBoard.Data
{
[CreateAssetMenu(fileName = "ClueData", menuName = "Unheimlich/Detective Board/Clue Data")]
public class ClueData : ScriptableObject
{
[Header("Clue Identity")]
[Tooltip("Unique identifier for this clue")]
public string ClueID;
[Tooltip("Display title on the board")]
public string Title;
[Tooltip("Detailed description of the clue")]
[TextArea(3, 6)]
public string Description;
[Header("Visual Representation")]
[Tooltip("Icon or image for the node (post-it/card visual)")]
public Sprite Icon;
[Tooltip("Color tint for the node")]
public Color NodeColor = Color.white;
[Header("Meta")]
[Tooltip("Category for organization (e.g., 'Evidence', 'Witness', 'Location')")]
public string Category;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 166ae120db91b064c9b98086267b9783
@@ -0,0 +1,75 @@
using UnityEngine;
using System;
using System.Collections.Generic;
using Unheimlich.Features.DetectiveBoard.UI;
namespace Unheimlich.Features.DetectiveBoard.Data
{
[Serializable]
public class PostItNodeData
{
public string NodeID;
public Vector2 Position;
public float Rotation;
public bool IsEliminated;
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]
public class DetectiveBoardSaveData
{
public List<PostItNodeData> Nodes = new List<PostItNodeData>();
public PostItNodeData GetNodeData(string nodeID)
{
return Nodes.Find(n => n.NodeID == nodeID);
}
public void AddOrUpdateNode(PostItNodeData nodeData)
{
int index = Nodes.FindIndex(n => n.NodeID == nodeData.NodeID);
if (index >= 0)
{
Nodes[index] = nodeData;
}
else
{
Nodes.Add(nodeData);
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f2344709e137fdf46aaa5445a330db2f
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a7317abe29dcd654f99d8d2ad8e1d179
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,39 @@
using UnityEngine;
using UnityEditor;
using Unheimlich.Features.DetectiveBoard.Data;
using Unheimlich.Features.DetectiveBoard;
namespace Unheimlich.Editor
{
[CustomEditor(typeof(ClueData))]
public class ClueDataEditor : UnityEditor.Editor
{
public override void OnInspectorGUI()
{
DrawDefaultInspector();
ClueData clueData = (ClueData)target;
EditorGUILayout.Space();
EditorGUILayout.LabelField("Quick Actions", EditorStyles.boldLabel);
if (GUILayout.Button("Generate Unique ID"))
{
clueData.ClueID = System.Guid.NewGuid().ToString();
EditorUtility.SetDirty(clueData);
}
if (GUILayout.Button("Test Unlock Clue (Play Mode Only)"))
{
if (Application.isPlaying)
{
DetectiveBoardManager.Instance?.UnlockClue(clueData);
}
else
{
Debug.LogWarning("This button only works in Play Mode!");
}
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 184cbe5b79ee0a84ba29da016ec76159
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 81f81f88e30714043a121630f7bf7cb9
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,115 @@
using UnityEngine;
using Unheimlich.Features.DetectiveBoard.Data;
using Unheimlich.Features.DetectiveBoard;
namespace Unheimlich.Features.DetectiveBoard
{
/// <summary>
/// Unlocks a clue when a specific audio source plays or finishes playing.
/// Useful for triggering clues based on environmental sounds, dialogue, or hallucinations.
/// </summary>
[RequireComponent(typeof(AudioSource))]
public class ClueUnlockOnAudio : MonoBehaviour
{
public enum UnlockTrigger
{
OnPlay,
OnFinish,
OnTimeReached
}
[Header("Clue Configuration")]
[Tooltip("The clue to unlock")]
[SerializeField] private ClueData _clueToUnlock;
[Header("Audio Trigger")]
[Tooltip("When to unlock the clue")]
[SerializeField] private UnlockTrigger _trigger = UnlockTrigger.OnPlay;
[Tooltip("Time in seconds to wait before unlocking (only for OnTimeReached)")]
[SerializeField] private float _unlockTime = 1f;
private AudioSource _audioSource;
private bool _hasUnlocked;
private bool _isMonitoring;
private float _currentTime;
private void Awake()
{
_audioSource = GetComponent<AudioSource>();
}
private void OnEnable()
{
if (_audioSource != null && _audioSource.isPlaying)
{
StartMonitoring();
}
}
private void Update()
{
if (!_isMonitoring || _hasUnlocked)
return;
if (_audioSource == null || !_audioSource.isPlaying)
{
if (_trigger == UnlockTrigger.OnFinish && _isMonitoring)
{
UnlockClue();
}
_isMonitoring = false;
return;
}
_currentTime = _audioSource.time;
if (_trigger == UnlockTrigger.OnTimeReached && _currentTime >= _unlockTime)
{
UnlockClue();
}
}
/// <summary>
/// Call this when the audio starts playing.
/// </summary>
public void OnAudioPlay()
{
StartMonitoring();
if (_trigger == UnlockTrigger.OnPlay)
{
UnlockClue();
}
}
private void StartMonitoring()
{
_isMonitoring = true;
_currentTime = 0f;
}
private void UnlockClue()
{
if (_hasUnlocked || _clueToUnlock == null)
return;
if (DetectiveBoardManager.Instance != null)
{
DetectiveBoardManager.Instance.UnlockClue(_clueToUnlock);
_hasUnlocked = true;
Debug.Log($"[ClueUnlockOnAudio] Unlocked clue: {_clueToUnlock.Title} via audio trigger");
}
}
/// <summary>
/// Resets the unlock state.
/// </summary>
public void ResetUnlock()
{
_hasUnlocked = false;
_isMonitoring = false;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: fcedba870c1ea9642a272a53148bb933
@@ -0,0 +1,60 @@
using UnityEngine;
using Unheimlich.Features.DetectiveBoard.Data;
using Unheimlich.Core.Interaction;
using Unheimlich.Features.DetectiveBoard;
using Unheimlich.Features.Items;
namespace Unheimlich.Features.DetectiveBoard
{
/// <summary>
/// Unlocks a clue when the player interacts with this object.
/// Can work standalone (extends InteractableBase) or piggyback on an existing InspectableItem.
/// </summary>
public class ClueUnlockOnInteraction : InteractableBase
{
[Header("Clue Configuration")]
[Tooltip("The clue to unlock when player interacts")]
[SerializeField] private ClueData _clueToUnlock;
[Tooltip("Only unlock once")]
[SerializeField] private bool _unlockOnlyOnce = true;
private bool _hasUnlocked;
private InspectableItem _inspectableItem;
private void Awake()
{
_inspectableItem = GetComponent<InspectableItem>();
}
public override void Interact()
{
if (_unlockOnlyOnce && _hasUnlocked)
{
if (_inspectableItem != null)
_inspectableItem.Interact();
return;
}
if (_clueToUnlock == null)
{
Debug.LogError("[ClueUnlockOnInteraction] No clue data assigned!");
if (_inspectableItem != null)
_inspectableItem.Interact();
return;
}
if (DetectiveBoardManager.Instance != null)
{
DetectiveBoardManager.Instance.UnlockClue(_clueToUnlock);
_hasUnlocked = true;
}
if (_inspectableItem != null)
{
_inspectableItem.Interact();
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: cca584f48d1f0ab429416470fd01d543
@@ -0,0 +1,105 @@
using UnityEngine;
using Unheimlich.Features.DetectiveBoard.Data;
using Unheimlich.Features.DetectiveBoard;
namespace Unheimlich.Features.DetectiveBoard
{
/// <summary>
/// Generic trigger to unlock clues on the detective board.
/// Can be triggered by any game event: dialogue, object interaction, audio cue, hallucination, etc.
/// Simply call UnlockClue() when the trigger condition is met.
/// </summary>
public class ClueUnlockTrigger : MonoBehaviour
{
[Header("Clue Configuration")]
[Tooltip("The clue data to unlock when triggered")]
[SerializeField] private ClueData _clueToUnlock;
[Header("Trigger Settings")]
[Tooltip("Automatically unlock on Start()")]
[SerializeField] private bool _unlockOnStart;
[Tooltip("Unlock when this GameObject is enabled")]
[SerializeField] private bool _unlockOnEnable;
[Tooltip("Delay before unlocking (useful for sequencing)")]
[SerializeField] private float _unlockDelay;
[Tooltip("Show debug messages")]
[SerializeField] private bool _debugMode;
private bool _hasUnlocked;
private void Start()
{
if (_unlockOnStart)
{
UnlockClue();
}
}
private void OnEnable()
{
if (_unlockOnEnable && !_hasUnlocked)
{
UnlockClue();
}
}
/// <summary>
/// Unlocks the associated clue. Can be called from external scripts, events, or animations.
/// </summary>
public void UnlockClue()
{
if (_hasUnlocked)
{
if (_debugMode)
Debug.LogWarning($"[ClueUnlockTrigger] Clue '{_clueToUnlock?.Title}' already unlocked!");
return;
}
if (_clueToUnlock == null)
{
Debug.LogError("[ClueUnlockTrigger] No clue data assigned!");
return;
}
if (_unlockDelay > 0f)
{
Invoke(nameof(UnlockClueImmediate), _unlockDelay);
}
else
{
UnlockClueImmediate();
}
}
private void UnlockClueImmediate()
{
if (DetectiveBoardManager.Instance != null)
{
DetectiveBoardManager.Instance.UnlockClue(_clueToUnlock);
_hasUnlocked = true;
if (_debugMode)
Debug.Log($"[ClueUnlockTrigger] Unlocked clue: {_clueToUnlock.Title}");
OnClueUnlocked?.Invoke();
}
else
{
Debug.LogError("[ClueUnlockTrigger] DetectiveBoardManager not found!");
}
}
/// <summary>
/// Resets the unlock state (useful for testing).
/// </summary>
public void ResetUnlock()
{
_hasUnlocked = false;
}
public event System.Action OnClueUnlocked;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 61b90106a531ea840b670f368e2c8aa9
@@ -0,0 +1,217 @@
using UnityEngine;
using Unheimlich.Core;
using Unheimlich.World.CameraSystem;
using Unheimlich.Features.Player;
using Unheimlich.Features.OctoArms;
using InputManager = Unheimlich.Core.Input.InputManager;
using Unheimlich.Features.DetectiveBoard.UI;
using Unheimlich.Features.Dialogue.UI;
namespace Unheimlich.Features.DetectiveBoard
{
/// <summary>
/// Controls player interaction with the detective board.
/// </summary>
public class DetectiveBoardController : Singleton<DetectiveBoardController>
{
[Header("Settings")]
[SerializeField] private float _activationRange = 2f;
[SerializeField] private LayerMask _boardLayer;
[SerializeField] private CameraPanSettings _playerAlignmentSettings;
[Header("Virtual Camera")]
[SerializeField] private DetectiveBoardVirtualCamera _virtualCamera;
private bool _isBoardActive;
private CameraHeadBob _cameraHeadBob;
public bool IsBoardActive => _isBoardActive;
protected override void OnAwake()
{
_cameraHeadBob = FindFirstObjectByType<CameraHeadBob>();
}
void Start()
{
if(_virtualCamera == null)
_virtualCamera = DetectiveBoardVirtualCamera.Instance;
}
private void Update()
{
if (_isBoardActive)
{
HandleBoardInteraction();
}
else
{
CheckForBoardActivation();
}
}
private void CheckForBoardActivation()
{
if (PlayerController.Instance == null ||
PlayerController.Instance.PlayerCamera == null ||
DetectiveBoardCanvas.Instance == null)
return;
var camera = PlayerController.Instance.PlayerCamera;
var boardTransform = DetectiveBoardCanvas.Instance.transform;
float distanceToBoard = Vector3.Distance(camera.transform.position, boardTransform.position);
if (distanceToBoard > _activationRange)
return;
Ray ray = new Ray(camera.transform.position, camera.transform.forward);
if (Physics.Raycast(ray, out RaycastHit hit, _activationRange, _boardLayer))
{
bool isLookingAtBoard = (hit.transform == boardTransform) ||
(boardTransform.IsChildOf(hit.transform)) ||
(hit.transform.IsChildOf(boardTransform));
if (isLookingAtBoard)
{
var interactAction = InputManager.GetInteractAction();
if (interactAction != null && interactAction.WasPressedThisFrame())
{
ActivateBoard();
}
}
}
}
public void ActivateBoard()
{
if (_isBoardActive ||
PlayerController.Instance?.PlayerCamera == null ||
PlayerController.Instance?.CameraPanController == null ||
DetectiveBoardCanvas.Instance == null ||
DetectiveBoardVirtualCamera.Instance == null)
{
Debug.LogError("[DetectiveBoardController] Missing required references!");
return;
}
_isBoardActive = true;
if (PlayerController.Instance.Movement != null)
{
PlayerController.Instance.Movement.enabled = false;
}
OctoIkArmsImproved arms = PlayerController.Instance.GetComponentInChildren<OctoIkArmsImproved>();
if (arms != null)
{
arms.HideArms(3f);
}
InputManager.SetContext(InputManager.InputContext.Listening);
if (_cameraHeadBob != null)
{
_cameraHeadBob.SetMode(HeadBobMode.Disabled);
}
if (CrosshairController.Instance != null)
{
CrosshairController.Instance.Hide();
}
Cursor.lockState = CursorLockMode.None;
Cursor.visible = true;
var cameraPan = PlayerController.Instance.CameraPanController;
var originalSettings = cameraPan.Settings;
if (_playerAlignmentSettings != null)
cameraPan.Settings = _playerAlignmentSettings;
cameraPan.SaveOriginalState();
cameraPan.AlignToTarget(DetectiveBoardCanvas.Instance.transform, () =>
{
DetectiveBoardVirtualCamera.Instance.ActivateFromPlayerPosition(
PlayerController.Instance.PlayerCamera,
DetectiveBoardCanvas.Instance.transform,
DetectiveBoardCanvas.Instance.BoardRenderer
);
if (_playerAlignmentSettings != null)
cameraPan.Settings = originalSettings;
});
}
private void HandleBoardInteraction()
{
var exitAction = InputManager.GetExitAction();
if (exitAction != null && exitAction.WasPressedThisFrame())
{
DeactivateBoard();
}
}
public void DeactivateBoard()
{
if (!_isBoardActive || DetectiveBoardVirtualCamera.Instance == null)
return;
_isBoardActive = false;
DetectiveBoardVirtualCamera.Instance.Deactivate(() =>
{
var cameraPan = PlayerController.Instance?.CameraPanController;
if (cameraPan != null)
{
OctoIkArmsImproved arms = PlayerController.Instance.GetComponentInChildren<OctoIkArmsImproved>();
if (arms != null)
{
arms.ShowArms(3f);
}
cameraPan.ReturnToOriginalPosition(() =>
{
if (PlayerController.Instance?.Movement != null)
{
PlayerController.Instance.Movement.enabled = true;
}
});
}
else if (PlayerController.Instance?.Movement != null)
{
PlayerController.Instance.Movement.enabled = true;
}
});
InputManager.SetContext(InputManager.InputContext.Normal);
RestoreHeadBob();
if (CrosshairController.Instance != null)
{
CrosshairController.Instance.Show();
}
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
}
private void RestoreHeadBob()
{
if (_cameraHeadBob == null)
return;
_cameraHeadBob.ClearOverride();
}
private void OnDrawGizmosSelected()
{
if (DetectiveBoardCanvas.Instance == null)
return;
Gizmos.color = _isBoardActive ? Color.green : Color.yellow;
Gizmos.DrawWireSphere(DetectiveBoardCanvas.Instance.transform.position, _activationRange);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9f9f72364623a2a4bb50f02537fd2e72
@@ -0,0 +1,191 @@
using System.Collections.Generic;
using UnityEngine;
using Unheimlich.Core;
using Unheimlich.Features.DetectiveBoard.Data;
namespace Unheimlich.Features.DetectiveBoard
{
/// <summary>
/// Manages the detective board state and clue unlocking system.
/// Tracks unlocked clues, connections, certainty levels, and eliminated hypotheses.
/// Does NOT affect game progression - purely interpretive tool for the player.
/// </summary>
public class DetectiveBoardManager : Singleton<DetectiveBoardManager>
{
public enum CertaintyLevel
{
Uncertain,
Possible,
Confident,
Certain
}
public class ClueState
{
public ClueData Data;
public bool IsUnlocked;
public bool IsEliminated;
}
public class Connection
{
public string FromClueID;
public string ToClueID;
public CertaintyLevel Certainty;
public bool IsEliminated;
}
[Header("References")]
private Dictionary<string, ClueState> _clueStates = new Dictionary<string, ClueState>();
private List<Connection> _connections = new List<Connection>();
public IReadOnlyDictionary<string, ClueState> ClueStates => _clueStates;
public IReadOnlyList<Connection> Connections => _connections;
/// <summary>
/// Unlocks a clue on the detective board. Can be triggered by any in-game event.
/// </summary>
public void UnlockClue(ClueData clueData)
{
if (clueData == null)
{
Debug.LogError("[DetectiveBoardManager] Attempted to unlock null clue!");
return;
}
if (!_clueStates.ContainsKey(clueData.ClueID))
{
_clueStates[clueData.ClueID] = new ClueState
{
Data = clueData,
IsUnlocked = true,
IsEliminated = false
};
OnClueUnlocked?.Invoke(clueData);
}
else if (!_clueStates[clueData.ClueID].IsUnlocked)
{
_clueStates[clueData.ClueID].IsUnlocked = true;
OnClueUnlocked?.Invoke(clueData);
}
}
/// <summary>
/// Returns a list of all currently unlocked clues.
/// Used by DetectiveBoardCanvas to display already-unlocked clues on initialization.
/// </summary>
public List<ClueData> GetUnlockedClues()
{
List<ClueData> unlockedClues = new List<ClueData>();
foreach (var state in _clueStates.Values)
{
if (state.IsUnlocked)
{
unlockedClues.Add(state.Data);
}
}
return unlockedClues;
}
/// <summary>
/// Unlocks a clue by its ID string.
/// </summary>
public void UnlockClueByID(string clueID)
{
if (_clueStates.TryGetValue(clueID, out ClueState state))
{
if (!state.IsUnlocked)
{
state.IsUnlocked = true;
OnClueUnlocked?.Invoke(state.Data);
}
}
else
{
Debug.LogWarning($"[DetectiveBoardManager] Clue ID '{clueID}' not found!");
}
}
/// <summary>
/// Registers a clue in the system without unlocking it.
/// </summary>
public void RegisterClue(ClueData clueData)
{
if (clueData == null || _clueStates.ContainsKey(clueData.ClueID))
return;
_clueStates[clueData.ClueID] = new ClueState
{
Data = clueData,
IsUnlocked = false,
IsEliminated = false
};
}
/// <summary>
/// Creates a connection between two clues.
/// </summary>
public void CreateConnection(string fromClueID, string toClueID, CertaintyLevel certainty = CertaintyLevel.Uncertain)
{
if (!_clueStates.ContainsKey(fromClueID) || !_clueStates.ContainsKey(toClueID))
{
Debug.LogWarning($"[DetectiveBoardManager] Cannot create connection - one or both clues not found!");
return;
}
Connection existing = _connections.Find(c =>
(c.FromClueID == fromClueID && c.ToClueID == toClueID) ||
(c.FromClueID == toClueID && c.ToClueID == fromClueID));
if (existing != null)
{
existing.Certainty = certainty;
}
else
{
_connections.Add(new Connection
{
FromClueID = fromClueID,
ToClueID = toClueID,
Certainty = certainty,
IsEliminated = false
});
OnConnectionCreated?.Invoke(fromClueID, toClueID);
}
}
/// <summary>
/// Marks a clue as eliminated (incorrect hypothesis).
/// </summary>
public void EliminateClue(string clueID)
{
if (_clueStates.TryGetValue(clueID, out ClueState state))
{
state.IsEliminated = true;
OnClueEliminated?.Invoke(clueID);
}
}
/// <summary>
/// Marks a connection as eliminated.
/// </summary>
public void EliminateConnection(string fromClueID, string toClueID)
{
Connection connection = _connections.Find(c =>
(c.FromClueID == fromClueID && c.ToClueID == toClueID) ||
(c.FromClueID == toClueID && c.ToClueID == fromClueID));
if (connection != null)
{
connection.IsEliminated = true;
}
}
public event System.Action<ClueData> OnClueUnlocked;
public event System.Action<string, string> OnConnectionCreated;
public event System.Action<string> OnClueEliminated;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4b187cdccaa498441a752e8f49ff4290
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 466fa804543e82c4483a22466a9bb484
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,170 @@
using UnityEngine;
using UnityEngine.UI;
namespace Unheimlich.Features.DetectiveBoard.UI
{
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 _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()
{
if (_rectTransform == null)
_rectTransform = GetComponent<RectTransform>();
if (_lineImage == null)
_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)
{
_startNode = startNode;
_endNode = endNode;
_colorState = LineColorState.Green;
UpdateColorState();
UpdateLine();
}
public void UpdateLine()
{
if (_startNode == null || _endNode == null || _rectTransform == null)
return;
Vector2 startPos = _startNode.ConnectionPoint.anchoredPosition;
Vector2 endPos = _endNode.ConnectionPoint.anchoredPosition;
Vector2 direction = endPos - startPos;
float distance = direction.magnitude;
_rectTransform.anchoredPosition = startPos;
float angle = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg;
_rectTransform.localRotation = Quaternion.Euler(0, 0, angle);
if (_lineRectTransform != null)
{
_lineRectTransform.sizeDelta = new Vector2(distance, _lineThickness);
}
if (_outlineRectTransform != null)
{
_outlineRectTransform.sizeDelta = new Vector2(distance, _lineThickness + 3f);
}
}
public void CycleColorState()
{
_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)
{
return (_startNode == nodeA && _endNode == nodeB) ||
(_startNode == nodeB && _endNode == nodeA);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d38fc1a64b1949443b49ec6506174085
@@ -0,0 +1,90 @@
using UnityEngine;
using Unheimlich.Features.DetectiveBoard;
namespace Unheimlich.Features.DetectiveBoard.UI
{
/// <summary>
/// Renders a connection line between two clue nodes on the detective board.
/// Visual style changes based on certainty level and elimination state.
/// </summary>
[RequireComponent(typeof(LineRenderer))]
public class ConnectionLineRenderer : MonoBehaviour
{
private LineRenderer _lineRenderer;
private string _fromClueID;
private string _toClueID;
private DetectiveBoardManager.CertaintyLevel _certainty;
private bool _isEliminated;
public string FromClueID => _fromClueID;
public string ToClueID => _toClueID;
private void Awake()
{
_lineRenderer = GetComponent<LineRenderer>();
}
/// <summary>
/// Initializes the connection line between two nodes.
/// </summary>
public void Initialize(string fromClueID, string toClueID,
DetectiveBoardManager.CertaintyLevel certainty, Color color, float width)
{
_fromClueID = fromClueID;
_toClueID = toClueID;
_certainty = certainty;
if (_lineRenderer != null)
{
_lineRenderer.startWidth = width;
_lineRenderer.endWidth = width;
_lineRenderer.material = new Material(Shader.Find("Sprites/Default"));
_lineRenderer.startColor = color;
_lineRenderer.endColor = color;
}
}
/// <summary>
/// Updates the line positions based on node transforms.
/// </summary>
public void UpdateLine(Vector3 startPos, Vector3 endPos)
{
if (_lineRenderer != null)
{
_lineRenderer.SetPosition(0, startPos);
_lineRenderer.SetPosition(1, endPos);
}
}
/// <summary>
/// Updates the certainty level and visual appearance.
/// </summary>
public void SetCertainty(DetectiveBoardManager.CertaintyLevel certainty, Color color)
{
_certainty = certainty;
if (_lineRenderer != null)
{
_lineRenderer.startColor = color;
_lineRenderer.endColor = color;
}
}
/// <summary>
/// Marks this connection as eliminated.
/// </summary>
public void SetEliminated(bool eliminated)
{
_isEliminated = eliminated;
if (_lineRenderer != null)
{
float alpha = eliminated ? 0.3f : 1f;
Color color = _lineRenderer.startColor;
color.a = alpha;
_lineRenderer.startColor = color;
_lineRenderer.endColor = color;
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8d829d9811842b241b980bd64ab73bff
@@ -0,0 +1,443 @@
using UnityEngine;
using UnityEngine.InputSystem;
using System.Collections.Generic;
using System.Linq;
using Unheimlich.Features.DetectiveBoard.Data;
using Unheimlich.Features.DetectiveBoard;
using Unheimlich.Core;
using Unheimlich.Features.Player;
namespace Unheimlich.Features.DetectiveBoard.UI
{
public class DetectiveBoardCanvas : Singleton<DetectiveBoardCanvas>
{
[Header("Prefabs")]
[SerializeField] private GameObject _postItNodePrefab;
[SerializeField] private GameObject _connectionLinePrefab;
[Header("Layout References")]
[SerializeField] private RectTransform _boardArea;
[SerializeField] private Renderer _boardRenderer;
[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;
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 Renderer BoardRenderer => _boardRenderer;
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.
/// Handles connection creation in connect mode, or selection in normal mode.
/// </summary>
public void OnNodeClicked(PostItNode node)
{
if (_isConnectMode)
{
HandleConnectionModeClick(node);
}
else
{
_selectedNode = node;
UpdateAllNodeSelections();
}
}
/// <summary>
/// Called by DetectiveBoardInteraction when a node is right-clicked.
/// Toggles the eliminated state of the node.
/// </summary>
public void OnNodeRightClicked(PostItNode node)
{
ToggleNodeEliminated(node);
}
protected override void OnAwake()
{
_canvas = GetComponent<Canvas>();
if (_canvas != null && _canvas.renderMode == RenderMode.WorldSpace)
{
if (PlayerController.Instance != null)
_targetCamera = PlayerController.Instance.PlayerCamera;
if (_targetCamera == null)
_targetCamera = Camera.main;
_canvas.worldCamera = _targetCamera;
}
_saveData = new DetectiveBoardSaveData();
}
private void OnEnable()
{
TrySubscribeToManager();
}
private void Start()
{
TrySubscribeToManager();
}
private void TrySubscribeToManager()
{
if (DetectiveBoardManager.Instance != null)
{
DetectiveBoardManager.Instance.OnClueUnlocked -= OnClueUnlocked;
DetectiveBoardManager.Instance.OnClueUnlocked += OnClueUnlocked;
foreach (var clue in DetectiveBoardManager.Instance.GetUnlockedClues())
{
if (!_activeNodes.ContainsKey(clue.ClueID))
{
AddClueNode(clue);
}
}
}
}
private void OnDisable()
{
if (DetectiveBoardManager.Instance != null)
{
DetectiveBoardManager.Instance.OnClueUnlocked -= OnClueUnlocked;
}
}
private void OnClueUnlocked(ClueData clueData)
{
AddClueNode(clueData);
}
private void Update()
{
HandleConnectModeInput();
}
private void HandleConnectModeInput()
{
if (Keyboard.current == null)
return;
bool wasConnectMode = _isConnectMode;
_isConnectMode = Keyboard.current[_connectModeKey].isPressed;
if (wasConnectMode && !_isConnectMode)
{
_selectedNode = null;
UpdateAllNodeSelections();
}
}
public void AddClueNode(ClueData clueData)
{
if (_activeNodes.ContainsKey(clueData.ClueID))
return;
if (_postItNodePrefab == null)
{
Debug.LogError("[DetectiveBoardCanvas] _postItNodePrefab is null! Assign the prefab in the inspector.");
return;
}
if (_boardArea == null)
{
Debug.LogError("[DetectiveBoardCanvas] _boardArea is null! Assign NodesContainer in the inspector.");
return;
}
PostItNodeData nodeData = _saveData.GetNodeData(clueData.ClueID);
if (nodeData == null)
{
Vector2 autoPlacedPos = CalculateAutoPlacementPosition();
nodeData = new PostItNodeData(clueData.ClueID, autoPlacedPos);
_saveData.AddOrUpdateNode(nodeData);
}
GameObject nodeObj = Instantiate(_postItNodePrefab, _boardArea);
PostItNode node = nodeObj.GetComponent<PostItNode>();
if (node == null)
{
Debug.LogError("[DetectiveBoardCanvas] PostItNode component not found on prefab");
Destroy(nodeObj);
return;
}
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))
return;
RemoveConnectionsForNode(node);
_activeNodes.Remove(clueID);
Destroy(node.gameObject);
}
private void HandleConnectionModeClick(PostItNode clickedNode)
{
if (_selectedNode == null)
{
_selectedNode = clickedNode;
UpdateAllNodeSelections();
}
else if (_selectedNode == clickedNode)
{
_selectedNode = null;
UpdateAllNodeSelections();
}
else
{
ToggleConnection(_selectedNode, clickedNode);
_selectedNode = null;
UpdateAllNodeSelections();
}
}
private void ToggleConnection(PostItNode nodeA, PostItNode nodeB)
{
ConnectionLine existingConnection = _activeConnections.Find(c => c.ConnectsNodes(nodeA, nodeB));
if (existingConnection != null)
{
RemoveConnection(existingConnection);
}
else
{
CreateConnection(nodeA, nodeB);
}
}
private void CreateConnection(PostItNode startNode, PostItNode endNode)
{
GameObject lineObj = Instantiate(_connectionLinePrefab, _boardArea);
lineObj.transform.SetAsFirstSibling();
ConnectionLine line = lineObj.GetComponent<ConnectionLine>();
if (line == null)
{
Debug.LogError("[DetectiveBoardCanvas] ConnectionLine component not found on prefab");
Destroy(lineObj);
return;
}
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 (existingConnectionData == null)
{
startNode.NodeData.Connections.Add(new ConnectionData(endNode.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.Connections.RemoveAll(c => c.TargetNodeID == connection.EndNode.NodeID);
connection.EndNode.NodeData.Connections.RemoveAll(c => c.TargetNodeID == connection.StartNode.NodeID);
}
_activeConnections.Remove(connection);
Destroy(connection.gameObject);
}
private void RemoveConnectionsForNode(PostItNode node)
{
var connectionsToRemove = _activeConnections
.Where(c => c.StartNode == node || c.EndNode == node)
.ToList();
foreach (var connection in connectionsToRemove)
{
RemoveConnection(connection);
}
}
/// <summary>
/// Updates all connection lines attached to the specified node.
/// Called during node dragging to keep connections visually updated.
/// </summary>
public void UpdateConnectionsForNode(PostItNode node)
{
foreach (var connection in _activeConnections)
{
if (connection.StartNode == node || connection.EndNode == node)
{
connection.UpdateLine();
}
}
}
private void RestoreConnectionsForNode(PostItNode node)
{
if (node.NodeData.Connections == null || node.NodeData.Connections.Count == 0)
return;
foreach (ConnectionData connectionData in node.NodeData.Connections)
{
if (_activeNodes.TryGetValue(connectionData.TargetNodeID, out PostItNode otherNode))
{
bool connectionExists = _activeConnections.Any(c => c.ConnectsNodes(node, otherNode));
if (!connectionExists)
{
CreateConnection(node, otherNode);
}
}
}
}
private void ToggleNodeEliminated(PostItNode node)
{
node.SetEliminated(!node.IsEliminated);
}
private void UpdateAllNodeSelections()
{
foreach (var node in _activeNodes.Values)
{
node.SetSelected(node == _selectedNode);
}
}
public DetectiveBoardSaveData GetSaveData()
{
return _saveData;
}
public void LoadSaveData(DetectiveBoardSaveData saveData)
{
_saveData = saveData;
foreach (var node in _activeNodes.Values)
{
PostItNodeData nodeData = _saveData.GetNodeData(node.NodeID);
if (nodeData != null)
{
node.SetPosition(nodeData.Position);
node.SetRotation(nodeData.Rotation);
node.SetEliminated(nodeData.IsEliminated);
}
}
_activeConnections.Clear();
foreach (var node in _activeNodes.Values)
{
RestoreConnectionsForNode(node);
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: edfcd01879d2cb34495302ef9882965b
@@ -0,0 +1,392 @@
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UI;
using Unheimlich.Features.DetectiveBoard;
using Unheimlich.Features.Player;
using Unheimlich.World.CameraSystem;
using System.Collections.Generic;
using UnityEngine.EventSystems;
namespace Unheimlich.Features.DetectiveBoard.UI
{
/// <summary>
/// Handles mouse interaction for the detective board World Space canvas.
/// Manages hover, selection, dragging, and click forwarding for Post-It nodes.
/// Interactions are only active when the board is in inspection mode.
/// </summary>
public class DetectiveBoardInteraction : MonoBehaviour
{
[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;
UpdateCanvasPlane();
}
private void Update()
{
if (!IsInBoardInteractionMode())
{
ClearInteractionState();
return;
}
UpdateCameraReference();
UpdateCanvasPlane();
if (_isDragging)
{
HandleDrag();
}
else
{
HandleHover();
HandleLineHover();
}
HandleClick();
}
private void UpdateCameraReference()
{
if (DetectiveBoardVirtualCamera.Instance != null && DetectiveBoardVirtualCamera.Instance.IsActive)
{
_camera = DetectiveBoardVirtualCamera.Instance.VirtualCamera;
}
else if (PlayerController.Instance != null)
{
_camera = PlayerController.Instance.PlayerCamera;
}
else if (_camera == null)
{
_camera = Camera.main;
}
}
private bool IsInBoardInteractionMode()
{
bool isActive = _boardController != null && _boardController.IsBoardActive;
return isActive;
}
private void ClearInteractionState()
{
if (_hoveredNode != null)
{
_hoveredNode.SetHovered(false);
_hoveredNode = null;
}
if (_hoveredLine != null && _canvasManager != null)
{
_canvasManager.SetHoveredLine(null);
_hoveredLine = null;
}
if (_selectedNode != null && _isDragging)
{
_selectedNode.SetSelected(false);
_selectedNode = null;
_isDragging = false;
}
}
private void UpdateCanvasPlane()
{
if (_boardCanvas != null)
{
Vector3 planeNormal = -_boardCanvas.transform.forward;
Vector3 planePosition = _boardCanvas.transform.position;
_canvasPlane = new Plane(planeNormal, planePosition);
}
}
private void HandleHover()
{
PostItNode hitNode = RaycastForNode();
if (hitNode != _hoveredNode)
{
if (_hoveredNode != null)
{
_hoveredNode.SetHovered(false);
}
_hoveredNode = hitNode;
if (_hoveredNode != null)
{
_hoveredNode.SetHovered(true);
}
}
}
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)
return;
PostItNode hitNode = RaycastForNode();
if (Mouse.current.leftButton.wasPressedThisFrame)
{
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)
{
_canvasManager.OnNodeRightClicked(hitNode);
}
}
private void HandleLeftClick(PostItNode hitNode)
{
if (_canvasManager != null && _canvasManager.IsConnectMode)
{
if (hitNode != null)
{
_canvasManager.OnNodeClicked(hitNode);
}
return;
}
if (_isDragging)
{
_isDragging = false;
if (_selectedNode != null && hitNode == _selectedNode)
{
return;
}
if (_selectedNode != null)
{
_selectedNode.SetSelected(false);
}
if (hitNode != null)
{
_selectedNode = hitNode;
_selectedNode.SetSelected(true);
_isDragging = true;
CalculateDragOffset();
}
else
{
_selectedNode = null;
}
}
else
{
if (hitNode != null)
{
if (_selectedNode != null && _selectedNode != hitNode)
{
_selectedNode.SetSelected(false);
}
_selectedNode = hitNode;
_selectedNode.SetSelected(true);
_isDragging = true;
CalculateDragOffset();
}
else
{
if (_selectedNode != null)
{
_selectedNode.SetSelected(false);
_selectedNode = null;
}
}
}
}
private void HandleDrag()
{
if (_selectedNode == null || _camera == null)
return;
Vector2 mousePos = Mouse.current.position.ReadValue();
Ray ray = _camera.ScreenPointToRay(new Vector3(mousePos.x, mousePos.y, 0));
if (_canvasPlane.Raycast(ray, out float enter))
{
Vector3 worldHitPoint = ray.GetPoint(enter);
Vector3 newWorldPos = worldHitPoint + _dragOffset;
Vector3 localPos = _boardCanvas.transform.InverseTransformPoint(newWorldPos);
Vector2 clampedPos = ClampPositionToBoard(new Vector2(localPos.x, localPos.y));
_selectedNode.SetPosition(clampedPos);
if (_canvasManager != null)
{
_canvasManager.UpdateConnectionsForNode(_selectedNode);
}
}
}
private Vector2 ClampPositionToBoard(Vector2 position)
{
if (_boardArea == null)
return position;
Rect boardRect = _boardArea.rect;
float halfWidth = boardRect.width * 0.5f;
float halfHeight = boardRect.height * 0.5f;
float clampedX = Mathf.Clamp(position.x, -halfWidth, halfWidth);
float clampedY = Mathf.Clamp(position.y, -halfHeight, halfHeight);
return new Vector2(clampedX, clampedY);
}
private void CalculateDragOffset()
{
if (_selectedNode == null || _camera == null)
return;
Vector2 mousePos = Mouse.current.position.ReadValue();
Ray ray = _camera.ScreenPointToRay(new Vector3(mousePos.x, mousePos.y, 0));
if (_canvasPlane.Raycast(ray, out float enter))
{
Vector3 worldHitPoint = ray.GetPoint(enter);
Vector3 currentWorldPos = _selectedNode.transform.position;
_dragOffset = currentWorldPos - worldHitPoint;
}
}
private PostItNode RaycastForNode()
{
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)
{
PostItNode node = result.gameObject.GetComponentInParent<PostItNode>();
if (node != null)
return node;
}
}
Ray ray = _camera.ScreenPointToRay(new Vector3(mousePos.x, mousePos.y, 0));
if (Physics.Raycast(ray, out RaycastHit hit, _maxInteractionDistance, _boardLayer))
{
float distance = hit.distance;
if (distance >= _minInteractionDistance)
{
return hit.collider.GetComponentInParent<PostItNode>();
}
}
return null;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 17bf00a333a0bc9409c21009e3ac6aea
@@ -0,0 +1,132 @@
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using Unheimlich.Features.DetectiveBoard.Data;
namespace Unheimlich.Features.DetectiveBoard.UI
{
public class PostItNode : MonoBehaviour
{
[Header("UI References")]
[SerializeField] private RectTransform _rectTransform;
[SerializeField] private Image _background;
[SerializeField] private TextMeshProUGUI _titleText;
[SerializeField] private TextMeshProUGUI _descriptionText;
[SerializeField] private Image _iconImage;
[SerializeField] private Image _selectionOutline;
[Header("Effects")]
[SerializeField] private PostItScribbleEffect _scribbleEffect;
[Header("Connection Points")]
[SerializeField] private RectTransform _connectionPoint;
private ClueData _clueData;
private PostItNodeData _nodeData;
private bool _isEliminated;
private bool _isSelected;
private Color _originalColor;
public string NodeID => _clueData?.ClueID;
public RectTransform RectTransform => _rectTransform;
public RectTransform ConnectionPoint => _rectTransform;
public ClueData ClueData => _clueData;
public PostItNodeData NodeData => _nodeData;
public bool IsEliminated => _isEliminated;
public bool IsSelected => _isSelected;
private void Awake()
{
if (_rectTransform == null)
_rectTransform = GetComponent<RectTransform>();
if (_scribbleEffect == null)
_scribbleEffect = GetComponent<PostItScribbleEffect>();
if (_selectionOutline != null)
_selectionOutline.enabled = false;
}
public void Initialize(ClueData clueData, PostItNodeData nodeData)
{
_clueData = clueData;
_nodeData = nodeData;
if (_titleText != null)
_titleText.text = clueData.Title;
if (_descriptionText != null)
_descriptionText.text = clueData.Description;
if (_iconImage != null && clueData.Icon != null)
{
_iconImage.sprite = clueData.Icon;
_iconImage.enabled = true;
}
else if (_iconImage != null)
{
_iconImage.enabled = false;
}
if (_background != null)
{
_originalColor = clueData.NodeColor;
_background.color = _originalColor;
}
SetPosition(nodeData.Position);
SetEliminated(nodeData.IsEliminated);
}
public void SetPosition(Vector2 position)
{
if (_rectTransform != null)
{
_rectTransform.anchoredPosition = position;
if (_nodeData != null)
_nodeData.Position = position;
}
}
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 (_scribbleEffect != null)
{
_scribbleEffect.SetEliminated(eliminated);
}
if (_nodeData != null)
_nodeData.IsEliminated = eliminated;
}
public void SetSelected(bool selected)
{
_isSelected = selected;
}
public void SetHovered(bool hovered)
{
if (_selectionOutline != null)
{
_selectionOutline.enabled = hovered;
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a398093f79b17c04e81fe88b509b1319
@@ -0,0 +1,135 @@
using UnityEngine;
using UnityEngine.UI;
using Unheimlich.Core.Utils;
namespace Unheimlich.Features.DetectiveBoard.UI
{
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