Hallucination system
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using Unheimlich.Features.DetectiveBoard.Data;
|
||||
|
||||
namespace Unheimlich.Features.DetectiveBoard.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Wraps a PostItNode to behave as a card in the bottom-screen fan hand.
|
||||
/// Handles hover pop animation and delegates click to ClueHandUI for board placement.
|
||||
/// Add this component alongside PostItNode on the same GameObject.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(PostItNode))]
|
||||
public class ClueHandCard : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler, IPointerClickHandler
|
||||
{
|
||||
[Header("Hover Settings")]
|
||||
[SerializeField] private float _hoverRiseAmount = 60f;
|
||||
[SerializeField] private float _hoverAnimSpeed = 8f;
|
||||
|
||||
private PostItNode _node;
|
||||
private ClueHandUI _handUI;
|
||||
|
||||
// Fan resting state assigned by ClueHandUI
|
||||
private Vector2 _restAnchoredPosition;
|
||||
private float _restRotation;
|
||||
|
||||
// Runtime animation state
|
||||
private Vector2 _currentAnchoredPosition;
|
||||
private float _currentRotation;
|
||||
private bool _isHovered;
|
||||
private bool _isPlaced;
|
||||
|
||||
public ClueData ClueData => _node != null ? _node.ClueData : null;
|
||||
public PostItNode Node => _node;
|
||||
public bool IsPlaced => _isPlaced;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (_node == null)
|
||||
_node = GetComponent<PostItNode>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Binds this card to its parent hand manager.
|
||||
/// </summary>
|
||||
public void Initialize(PostItNode node, ClueHandUI handUI)
|
||||
{
|
||||
_node = node;
|
||||
_handUI = handUI;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Assigns the resting fan slot (position + rotation) for this card.
|
||||
/// </summary>
|
||||
public void SetFanSlot(Vector2 anchoredPosition, float rotationZ)
|
||||
{
|
||||
_restAnchoredPosition = anchoredPosition;
|
||||
_restRotation = rotationZ;
|
||||
_currentAnchoredPosition = anchoredPosition;
|
||||
_currentRotation = rotationZ;
|
||||
|
||||
RectTransform rt = _node.RectTransform;
|
||||
rt.anchoredPosition = anchoredPosition;
|
||||
rt.localRotation = Quaternion.Euler(0f, 0f, rotationZ);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (_isPlaced)
|
||||
return;
|
||||
|
||||
RectTransform rt = _node.RectTransform;
|
||||
|
||||
Vector2 targetPos = _isHovered
|
||||
? _restAnchoredPosition + Vector2.up * _hoverRiseAmount
|
||||
: _restAnchoredPosition;
|
||||
|
||||
float targetRot = _isHovered ? 0f : _restRotation;
|
||||
|
||||
_currentAnchoredPosition = Vector2.Lerp(_currentAnchoredPosition, targetPos, Time.deltaTime * _hoverAnimSpeed);
|
||||
_currentRotation = Mathf.LerpAngle(_currentRotation, targetRot, Time.deltaTime * _hoverAnimSpeed);
|
||||
|
||||
rt.anchoredPosition = _currentAnchoredPosition;
|
||||
rt.localRotation = Quaternion.Euler(0f, 0f, _currentRotation);
|
||||
}
|
||||
|
||||
public void OnPointerEnter(PointerEventData eventData)
|
||||
{
|
||||
if (_isPlaced)
|
||||
return;
|
||||
|
||||
_isHovered = true;
|
||||
_handUI?.OnCardHovered(this);
|
||||
}
|
||||
|
||||
public void OnPointerExit(PointerEventData eventData)
|
||||
{
|
||||
if (_isPlaced)
|
||||
return;
|
||||
|
||||
_isHovered = false;
|
||||
_handUI?.OnCardUnhovered(this);
|
||||
}
|
||||
|
||||
public void OnPointerClick(PointerEventData eventData)
|
||||
{
|
||||
if (_isPlaced || eventData.button != PointerEventData.InputButton.Left)
|
||||
return;
|
||||
|
||||
_isHovered = false;
|
||||
_handUI?.OnCardSelected(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hides this card from the hand after placement on the board.
|
||||
/// </summary>
|
||||
public void MarkAsPlaced()
|
||||
{
|
||||
_isPlaced = true;
|
||||
gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dc7ce79e8bf16554fae5362980fec5b7
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ace0b6173dad68f47a97505680f54d4e
|
||||
@@ -131,45 +131,11 @@ namespace Unheimlich.Features.DetectiveBoard.UI
|
||||
_saveData = new DetectiveBoardSaveData();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
TrySubscribeToManager();
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
TrySubscribeToManager();
|
||||
}
|
||||
|
||||
private void TrySubscribeToManager()
|
||||
{
|
||||
if (DetectiveBoardManager.Instance != null)
|
||||
{
|
||||
DetectiveBoardManager.Instance.OnClueUnlocked -= OnClueUnlocked;
|
||||
DetectiveBoardManager.Instance.OnClueUnlocked += OnClueUnlocked;
|
||||
|
||||
foreach (var clue in DetectiveBoardManager.Instance.GetUnlockedClues())
|
||||
{
|
||||
if (!_activeNodes.ContainsKey(clue.ClueID))
|
||||
{
|
||||
AddClueNode(clue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (DetectiveBoardManager.Instance != null)
|
||||
{
|
||||
DetectiveBoardManager.Instance.OnClueUnlocked -= OnClueUnlocked;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnClueUnlocked(ClueData clueData)
|
||||
{
|
||||
AddClueNode(clueData);
|
||||
}
|
||||
private void OnEnable() { }
|
||||
|
||||
private void Start() { }
|
||||
|
||||
private void OnDisable() { }
|
||||
|
||||
private void Update()
|
||||
{
|
||||
@@ -413,6 +379,19 @@ namespace Unheimlich.Features.DetectiveBoard.UI
|
||||
}
|
||||
}
|
||||
|
||||
/// <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;
|
||||
|
||||
+39
-1
@@ -1,6 +1,7 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
using UnityEngine.UI;
|
||||
using Unheimlich.Core;
|
||||
using Unheimlich.Features.DetectiveBoard;
|
||||
using Unheimlich.Features.Player;
|
||||
using Unheimlich.World.CameraSystem;
|
||||
@@ -14,7 +15,7 @@ namespace Unheimlich.Features.DetectiveBoard.UI
|
||||
/// Manages hover, selection, dragging, and click forwarding for Post-It nodes.
|
||||
/// Interactions are only active when the board is in inspection mode.
|
||||
/// </summary>
|
||||
public class DetectiveBoardInteraction : MonoBehaviour
|
||||
public class DetectiveBoardInteraction : Singleton<DetectiveBoardInteraction>
|
||||
{
|
||||
[Header("Settings")]
|
||||
[SerializeField] private LayerMask _boardLayer;
|
||||
@@ -33,6 +34,8 @@ namespace Unheimlich.Features.DetectiveBoard.UI
|
||||
private Plane _canvasPlane;
|
||||
private Vector3 _dragOffset;
|
||||
|
||||
protected override void OnAwake() { }
|
||||
|
||||
private void Start()
|
||||
{
|
||||
_boardCanvas = GetComponent<Canvas>();
|
||||
@@ -349,6 +352,41 @@ namespace Unheimlich.Features.DetectiveBoard.UI
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Positions a freshly-created board node under the current mouse cursor
|
||||
/// and immediately enters drag mode on it, so the player can place it freely.
|
||||
/// Called by ClueHandUI after AddClueNode.
|
||||
/// </summary>
|
||||
public void SpawnNodeUnderMouseAndDrag(PostItNode node)
|
||||
{
|
||||
if (node == null || _camera == null)
|
||||
return;
|
||||
|
||||
UpdateCanvasPlane();
|
||||
|
||||
Vector2 mousePos = Mouse.current != null
|
||||
? Mouse.current.position.ReadValue()
|
||||
: new Vector2(Screen.width * 0.5f, Screen.height * 0.5f);
|
||||
|
||||
Ray ray = _camera.ScreenPointToRay(new Vector3(mousePos.x, mousePos.y, 0f));
|
||||
|
||||
if (_canvasPlane.Raycast(ray, out float enter))
|
||||
{
|
||||
Vector3 worldHitPoint = ray.GetPoint(enter);
|
||||
Vector3 localPos = _boardCanvas.transform.InverseTransformPoint(worldHitPoint);
|
||||
node.SetPosition(new Vector2(localPos.x, localPos.y));
|
||||
}
|
||||
|
||||
// Begin drag with zero offset so the node centre tracks the mouse exactly
|
||||
if (_selectedNode != null)
|
||||
_selectedNode.SetSelected(false);
|
||||
|
||||
_selectedNode = node;
|
||||
_selectedNode.SetSelected(true);
|
||||
_isDragging = true;
|
||||
_dragOffset = Vector3.zero;
|
||||
}
|
||||
|
||||
private PostItNode RaycastForNode()
|
||||
{
|
||||
if (_camera == null || Mouse.current == null)
|
||||
|
||||
Reference in New Issue
Block a user