chore: Project Restructuring & Theatre
This commit is contained in:
@@ -0,0 +1,252 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user