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 { [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 _activeNodes = new Dictionary(); private List _activeConnections = new List(); 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); } } /// /// 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() { _canvas = GetComponent(); 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() { } private void Start() { } private void OnDisable() { } 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(); 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(); 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); } } /// /// 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.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); } } /// /// Returns the active PostItNode for the given clue ID, or null if not found. /// Used by ClueHandUI to retrieve the node after placement. /// /// Returns true if a node for this clue ID is already placed on the board. public bool HasNodeForClue(string clueID) => _activeNodes.ContainsKey(clueID); public PostItNode GetNodeForClue(string clueID) { _activeNodes.TryGetValue(clueID, out PostItNode node); return node; } 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); } } } }