using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.UI; using Unheimlich.Core; using Unheimlich.Features.DetectiveBoard; using Unheimlich.Features.Player; using Unheimlich.World.CameraSystem; using System.Collections.Generic; using UnityEngine.EventSystems; namespace Unheimlich.Features.DetectiveBoard.UI { /// /// 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. /// public class DetectiveBoardInteraction : Singleton { [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; protected override void OnAwake() { } private void Start() { _boardCanvas = GetComponent(); _canvasManager = GetComponent(); _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(); if (raycaster != null && EventSystem.current != null) { var pointerEventData = new PointerEventData(EventSystem.current) { position = mousePos }; var results = new List(); raycaster.Raycast(pointerEventData, results); foreach (var result in results) { ConnectionLine line = result.gameObject.GetComponentInParent(); 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; } } /// /// Positions a freshly-created board node under the current mouse cursor /// and immediately enters drag mode on it, so the player can place it freely. /// Called by ClueHandUI after AddClueNode. /// public void SpawnNodeUnderMouseAndDrag(PostItNode node) { if (node == null || _camera == null) return; UpdateCanvasPlane(); Vector2 mousePos = Mouse.current != null ? Mouse.current.position.ReadValue() : new Vector2(Screen.width * 0.5f, Screen.height * 0.5f); Ray ray = _camera.ScreenPointToRay(new Vector3(mousePos.x, mousePos.y, 0f)); if (_canvasPlane.Raycast(ray, out float enter)) { Vector3 worldHitPoint = ray.GetPoint(enter); Vector3 localPos = _boardCanvas.transform.InverseTransformPoint(worldHitPoint); node.SetPosition(new Vector2(localPos.x, localPos.y)); } // Begin drag with zero offset so the node centre tracks the mouse exactly if (_selectedNode != null) _selectedNode.SetSelected(false); _selectedNode = node; _selectedNode.SetSelected(true); _isDragging = true; _dragOffset = Vector3.zero; } private PostItNode RaycastForNode() { if (_camera == null || Mouse.current == null) return null; Vector2 mousePos = Mouse.current.position.ReadValue(); GraphicRaycaster raycaster = _boardCanvas?.GetComponent(); if (raycaster != null && EventSystem.current != null) { var pointerEventData = new PointerEventData(EventSystem.current) { position = mousePos }; var results = new List(); raycaster.Raycast(pointerEventData, results); foreach (var result in results) { PostItNode node = result.gameObject.GetComponentInParent(); 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(); } } return null; } } }