Files
Ipseity-Demo/Assets/_Content/Scripts/Features/DetectiveBoard/UI/ClueHandUI.cs
T

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);
}
}
}