using UnityEngine; using UnityEngine.InputSystem; using System.Collections.Generic; using System.Linq; using Unheimlich.Data.DetectiveBoard; using Unheimlich.Managers; using Unheimlich.Core; namespace Unheimlich.UI.DetectiveBoard { public class DetectiveBoardCanvas : Singleton { [Header("References")] [SerializeField] private Canvas _canvas; [SerializeField] private RectTransform _boardArea; [SerializeField] private GameObject _postItNodePrefab; [SerializeField] private GameObject _connectionLinePrefab; [Header("World Space Setup")] [SerializeField] private Camera _targetCamera; [Header("Connection Settings")] [SerializeField] private Color _connectionColor = Color.white; [SerializeField] private bool _allowFreeConnections = false; [Header("Input")] [SerializeField] private Key _connectModeKey = Key.LeftCtrl; [SerializeField] private Key _eliminateKey = Key.X; private Dictionary _activeNodes = new Dictionary(); private List _activeConnections = new List(); private PostItNode _selectedNode; private bool _isConnectMode; private DetectiveBoardSaveData _saveData; public bool IsConnectMode => _isConnectMode; /// /// Called by DetectiveBoardInteraction when a node is left-clicked. /// Handles connection creation in connect mode, or selection in normal mode. /// public void OnNodeClicked(PostItNode node) { if (_isConnectMode) { HandleConnectionModeClick(node); } else { _selectedNode = node; UpdateAllNodeSelections(); } } /// /// Called by DetectiveBoardInteraction when a node is right-clicked. /// Toggles the eliminated state of the node. /// public void OnNodeRightClicked(PostItNode node) { ToggleNodeEliminated(node); } protected override void OnAwake() { if (_canvas == null) _canvas = GetComponent(); if (_canvas != null && _canvas.renderMode == RenderMode.WorldSpace) { 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(); HandleEliminateInput(); } private void HandleConnectModeInput() { if (Keyboard.current == null) return; bool wasConnectMode = _isConnectMode; _isConnectMode = Keyboard.current[_connectModeKey].isPressed; if (wasConnectMode && !_isConnectMode) { _selectedNode = null; UpdateAllNodeSelections(); } } 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)) 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 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); _saveData.AddOrUpdateNode(nodeData); } GameObject nodeObj = Instantiate(_postItNodePrefab, _boardArea); PostItNode node = nodeObj.GetComponent(); if (node == null) { Debug.LogError("[DetectiveBoardCanvas] PostItNode component not found on prefab"); Destroy(nodeObj); return; } node.Initialize(clueData, nodeData); _activeNodes.Add(clueData.ClueID, node); RestoreConnectionsForNode(node); } 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) { if (!CanConnect(nodeA, nodeB)) return; ConnectionLine existingConnection = _activeConnections.Find(c => c.ConnectsNodes(nodeA, nodeB)); if (existingConnection != null) { RemoveConnection(existingConnection); } else { CreateConnection(nodeA, nodeB); } } 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); lineObj.transform.SetAsFirstSibling(); ConnectionLine line = lineObj.GetComponent(); if (line == null) { Debug.LogError("[DetectiveBoardCanvas] ConnectionLine component not found on prefab"); Destroy(lineObj); return; } line.Initialize(startNode, endNode, _connectionColor); _activeConnections.Add(line); if (!startNode.NodeData.ConnectedNodeIDs.Contains(endNode.NodeID)) startNode.NodeData.ConnectedNodeIDs.Add(endNode.NodeID); if (!endNode.NodeData.ConnectedNodeIDs.Contains(startNode.NodeID)) endNode.NodeData.ConnectedNodeIDs.Add(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); } _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); } } /// /// Updates all connection lines attached to the specified node. /// Called during node dragging to keep connections visually updated. /// 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.ConnectedNodeIDs == null || node.NodeData.ConnectedNodeIDs.Count == 0) return; foreach (string connectedID in node.NodeData.ConnectedNodeIDs) { if (_activeNodes.TryGetValue(connectedID, 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.SetEliminated(nodeData.IsEliminated); } } _activeConnections.Clear(); foreach (var node in _activeNodes.Values) { RestoreConnectionsForNode(node); } } } }