253 lines
11 KiB
C#
253 lines
11 KiB
C#
using UnityEngine;
|
|
using System.Collections.Generic;
|
|
using Unheimlich.Core;
|
|
using Unheimlich.Features.DetectiveBoard.Data;
|
|
|
|
namespace Unheimlich.Features.DetectiveBoard.UI
|
|
{
|
|
/// <summary>
|
|
/// Manages the clue hand fan displayed at the bottom of the screen (Screen Space canvas).
|
|
/// Reuses the PostItNode prefab for each card — ClueHandCard is added at runtime.
|
|
/// Hovering a card pops it up and straightens it; clicking starts a lerp-depth
|
|
/// placement animation then hands the clue to DetectiveBoardCanvas.
|
|
/// </summary>
|
|
public class ClueHandUI : Singleton<ClueHandUI>
|
|
{
|
|
[Header("References")]
|
|
[SerializeField] private GameObject _postItNodePrefab;
|
|
[SerializeField] private RectTransform _handContainer;
|
|
|
|
[Header("Fan Layout Settings")]
|
|
[SerializeField] private float _cardSpacingPixels = 80f;
|
|
[SerializeField] private float _fanArcRadius = 800f;
|
|
[SerializeField] private float _cardBaseY = -120f;
|
|
[SerializeField] private float _maxFanHalfAngle = 40f;
|
|
|
|
[Header("Placement Settings")]
|
|
[SerializeField] private float _placementLerpSpeed = 5f;
|
|
|
|
private readonly List<ClueHandCard> _handCards = new List<ClueHandCard>();
|
|
private bool _isPlacing; protected override void OnAwake() { }
|
|
|
|
protected override void OnEnabled()
|
|
{
|
|
// With Scene Reload disabled, stale card references from the previous session
|
|
// may point to destroyed GameObjects. Clear and rebuild from manager state.
|
|
if (_handCards.Count > 0)
|
|
{
|
|
foreach (ClueHandCard card in _handCards)
|
|
{
|
|
if (card != null && card.gameObject != null)
|
|
Destroy(card.gameObject);
|
|
}
|
|
_handCards.Clear();
|
|
}
|
|
|
|
if (DetectiveBoardManager.Instance != null)
|
|
{
|
|
foreach (var clue in DetectiveBoardManager.Instance.GetUnlockedClues())
|
|
{
|
|
// Skip clues already placed on the board — they must not reappear in the hand
|
|
if (DetectiveBoardCanvas.Instance != null && DetectiveBoardCanvas.Instance.HasNodeForClue(clue.ClueID))
|
|
continue;
|
|
|
|
AddClueCard(clue);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void Start()
|
|
{
|
|
if (DetectiveBoardManager.Instance != null)
|
|
{
|
|
DetectiveBoardManager.Instance.OnClueUnlocked += OnClueUnlocked;
|
|
|
|
foreach (var clue in DetectiveBoardManager.Instance.GetUnlockedClues())
|
|
AddClueCard(clue);
|
|
}
|
|
|
|
// Start hidden — shown when the board activates
|
|
SetHandVisible(false);
|
|
}
|
|
|
|
private void OnDestroy()
|
|
{
|
|
if (DetectiveBoardManager.Instance != null)
|
|
DetectiveBoardManager.Instance.OnClueUnlocked -= OnClueUnlocked;
|
|
}
|
|
|
|
private void OnClueUnlocked(ClueData clueData)
|
|
{
|
|
AddClueCard(clueData);
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────────────────────────
|
|
// Card management
|
|
// ──────────────────────────────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Instantiates a PostItNode prefab, injects ClueHandCard, and adds it to the fan.
|
|
/// </summary>
|
|
public void AddClueCard(ClueData clueData)
|
|
{
|
|
if (_postItNodePrefab == null || _handContainer == null)
|
|
{
|
|
Debug.LogError("[ClueHandUI] Missing _postItNodePrefab or _handContainer reference.");
|
|
return;
|
|
}
|
|
|
|
if (_handCards.Exists(c => c.ClueData != null && c.ClueData.ClueID == clueData.ClueID))
|
|
return;
|
|
|
|
// Instantiate the PostItNode prefab inside the screen-space hand container
|
|
GameObject cardObj = Instantiate(_postItNodePrefab, _handContainer);
|
|
|
|
// PostItNode is nested one level inside "Canvas (Environment)" wrapper in the prefab
|
|
// — grab it directly
|
|
PostItNode node = cardObj.GetComponentInChildren<PostItNode>(true);
|
|
if (node == null)
|
|
{
|
|
Debug.LogError("[ClueHandUI] PostItNode not found on prefab.");
|
|
Destroy(cardObj);
|
|
return;
|
|
}
|
|
|
|
// Create a dummy NodeData for the hand display (position handled by fan layout)
|
|
PostItNodeData dummyData = new PostItNodeData(clueData.ClueID, Vector2.zero);
|
|
node.Initialize(clueData, dummyData);
|
|
|
|
// Add ClueHandCard wrapper on the PostItNode GameObject
|
|
ClueHandCard card = node.gameObject.AddComponent<ClueHandCard>();
|
|
card.Initialize(node, this);
|
|
|
|
// Move the PostItNode GameObject up to be a direct child of _handContainer
|
|
// so its RectTransform is in screen space
|
|
node.transform.SetParent(_handContainer, false);
|
|
|
|
// Destroy the now-empty prefab root (the "Canvas (Environment)" wrapper)
|
|
if (cardObj != node.gameObject)
|
|
Destroy(cardObj);
|
|
|
|
// Normalize scale — PostItNode prefab uses 0.5 scale for world space, reset for screen space
|
|
node.RectTransform.localScale = Vector3.one;
|
|
|
|
_handCards.Add(card);
|
|
RebuildFanLayout();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Recomputes fan arc positions and rotations for all non-placed cards.
|
|
/// Uses fixed pixel spacing between cards, capped by a max half-angle.
|
|
/// </summary>
|
|
private void RebuildFanLayout()
|
|
{
|
|
List<ClueHandCard> visible = _handCards.FindAll(c => !c.IsPlaced);
|
|
int count = visible.Count;
|
|
|
|
if (count == 0)
|
|
return;
|
|
|
|
// Derive the angle step from pixel spacing on the arc
|
|
float angleStepDeg = (_cardSpacingPixels / _fanArcRadius) * Mathf.Rad2Deg;
|
|
|
|
// Total spread is (count-1) steps, capped so it never exceeds _maxFanHalfAngle on each side
|
|
float totalHalfAngle = angleStepDeg * (count - 1) * 0.5f;
|
|
float clampedHalfAngle = Mathf.Min(totalHalfAngle, _maxFanHalfAngle);
|
|
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
float t = count == 1 ? 0.5f : (float)i / (count - 1);
|
|
float angle = Mathf.Lerp(-clampedHalfAngle, clampedHalfAngle, t);
|
|
float angleRad = angle * Mathf.Deg2Rad;
|
|
|
|
float x = _fanArcRadius * Mathf.Sin(angleRad);
|
|
float y = _cardBaseY - _fanArcRadius * (1f - Mathf.Cos(angleRad));
|
|
|
|
visible[i].SetFanSlot(new Vector2(x, y), -angle);
|
|
}
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────────────────────────
|
|
// Hover callbacks
|
|
// ──────────────────────────────────────────────────────────────────────
|
|
|
|
/// <summary>Brings the hovered card in front of its siblings.</summary>
|
|
public void OnCardHovered(ClueHandCard card)
|
|
{
|
|
card.transform.SetAsLastSibling();
|
|
}
|
|
|
|
/// <summary>Restores natural sibling order after hover ends.</summary>
|
|
public void OnCardUnhovered(ClueHandCard card)
|
|
{
|
|
RebuildSiblingOrder();
|
|
}
|
|
|
|
private void RebuildSiblingOrder()
|
|
{
|
|
int index = 0;
|
|
foreach (ClueHandCard c in _handCards)
|
|
{
|
|
if (!c.IsPlaced)
|
|
c.transform.SetSiblingIndex(index++);
|
|
}
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────────────────────────
|
|
// Selection & placement
|
|
// ──────────────────────────────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Called when the player clicks a card. Creates the node on the board
|
|
/// directly under the mouse cursor and immediately starts drag mode.
|
|
/// </summary>
|
|
public void OnCardSelected(ClueHandCard card)
|
|
{
|
|
if (_isPlacing)
|
|
return;
|
|
|
|
DetectiveBoardCanvas boardCanvas = DetectiveBoardCanvas.Instance;
|
|
if (boardCanvas == null)
|
|
{
|
|
Debug.LogWarning("[ClueHandUI] DetectiveBoardCanvas not found.");
|
|
return;
|
|
}
|
|
|
|
DetectiveBoardInteraction interaction = DetectiveBoardInteraction.Instance;
|
|
if (interaction == null)
|
|
{
|
|
Debug.LogWarning("[ClueHandUI] DetectiveBoardInteraction not found.");
|
|
return;
|
|
}
|
|
|
|
boardCanvas.AddClueNode(card.ClueData);
|
|
|
|
PostItNode boardNode = boardCanvas.GetNodeForClue(card.ClueData.ClueID);
|
|
if (boardNode == null)
|
|
{
|
|
Debug.LogWarning("[ClueHandUI] Node not found on board after AddClueNode.");
|
|
return;
|
|
}
|
|
|
|
// Position node under the mouse on the board canvas plane, then start drag
|
|
interaction.SpawnNodeUnderMouseAndDrag(boardNode);
|
|
|
|
card.MarkAsPlaced();
|
|
RebuildFanLayout();
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────────────────────────
|
|
// Visibility
|
|
// ──────────────────────────────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Shows or hides the fan hand. Called by DetectiveBoardController.
|
|
/// </summary>
|
|
public void SetHandVisible(bool visible)
|
|
{
|
|
if (_handContainer != null)
|
|
_handContainer.gameObject.SetActive(visible);
|
|
}
|
|
}
|
|
}
|