423 lines
14 KiB
C#
423 lines
14 KiB
C#
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() { }
|
|
|
|
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<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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the active PostItNode for the given clue ID, or null if not found.
|
|
/// Used by ClueHandUI to retrieve the node after placement.
|
|
/// </summary>
|
|
/// <summary>Returns true if a node for this clue ID is already placed on the board.</summary>
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|