chore: Project Restructuring & Theatre
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 41ce6b2bff04d1e4d9eb45ac2c7dd0d9
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,30 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unheimlich.Features.DetectiveBoard.Data
|
||||
{
|
||||
[CreateAssetMenu(fileName = "ClueData", menuName = "Unheimlich/Detective Board/Clue Data")]
|
||||
public class ClueData : ScriptableObject
|
||||
{
|
||||
[Header("Clue Identity")]
|
||||
[Tooltip("Unique identifier for this clue")]
|
||||
public string ClueID;
|
||||
|
||||
[Tooltip("Display title on the board")]
|
||||
public string Title;
|
||||
|
||||
[Tooltip("Detailed description of the clue")]
|
||||
[TextArea(3, 6)]
|
||||
public string Description;
|
||||
|
||||
[Header("Visual Representation")]
|
||||
[Tooltip("Icon or image for the node (post-it/card visual)")]
|
||||
public Sprite Icon;
|
||||
|
||||
[Tooltip("Color tint for the node")]
|
||||
public Color NodeColor = Color.white;
|
||||
|
||||
[Header("Meta")]
|
||||
[Tooltip("Category for organization (e.g., 'Evidence', 'Witness', 'Location')")]
|
||||
public string Category;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 166ae120db91b064c9b98086267b9783
|
||||
@@ -0,0 +1,75 @@
|
||||
using UnityEngine;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Unheimlich.Features.DetectiveBoard.UI;
|
||||
|
||||
namespace Unheimlich.Features.DetectiveBoard.Data
|
||||
{
|
||||
[Serializable]
|
||||
public class PostItNodeData
|
||||
{
|
||||
public string NodeID;
|
||||
public Vector2 Position;
|
||||
public float Rotation;
|
||||
public bool IsEliminated;
|
||||
public List<ConnectionData> Connections = new List<ConnectionData>();
|
||||
|
||||
public PostItNodeData(string id, Vector2 position)
|
||||
{
|
||||
NodeID = id;
|
||||
Position = position;
|
||||
Rotation = 0f;
|
||||
IsEliminated = false;
|
||||
}
|
||||
|
||||
public List<string> ConnectedNodeIDs
|
||||
{
|
||||
get
|
||||
{
|
||||
List<string> ids = new List<string>();
|
||||
foreach (var conn in Connections)
|
||||
{
|
||||
ids.Add(conn.TargetNodeID);
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class ConnectionData
|
||||
{
|
||||
public string TargetNodeID;
|
||||
public LineColorState ColorState;
|
||||
|
||||
public ConnectionData(string targetId, LineColorState colorState = LineColorState.Green)
|
||||
{
|
||||
TargetNodeID = targetId;
|
||||
ColorState = colorState;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class DetectiveBoardSaveData
|
||||
{
|
||||
public List<PostItNodeData> Nodes = new List<PostItNodeData>();
|
||||
|
||||
public PostItNodeData GetNodeData(string nodeID)
|
||||
{
|
||||
return Nodes.Find(n => n.NodeID == nodeID);
|
||||
}
|
||||
|
||||
public void AddOrUpdateNode(PostItNodeData nodeData)
|
||||
{
|
||||
int index = Nodes.FindIndex(n => n.NodeID == nodeData.NodeID);
|
||||
if (index >= 0)
|
||||
{
|
||||
Nodes[index] = nodeData;
|
||||
}
|
||||
else
|
||||
{
|
||||
Nodes.Add(nodeData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f2344709e137fdf46aaa5445a330db2f
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a7317abe29dcd654f99d8d2ad8e1d179
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,39 @@
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
using Unheimlich.Features.DetectiveBoard.Data;
|
||||
using Unheimlich.Features.DetectiveBoard;
|
||||
|
||||
namespace Unheimlich.Editor
|
||||
{
|
||||
[CustomEditor(typeof(ClueData))]
|
||||
public class ClueDataEditor : UnityEditor.Editor
|
||||
{
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
DrawDefaultInspector();
|
||||
|
||||
ClueData clueData = (ClueData)target;
|
||||
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.LabelField("Quick Actions", EditorStyles.boldLabel);
|
||||
|
||||
if (GUILayout.Button("Generate Unique ID"))
|
||||
{
|
||||
clueData.ClueID = System.Guid.NewGuid().ToString();
|
||||
EditorUtility.SetDirty(clueData);
|
||||
}
|
||||
|
||||
if (GUILayout.Button("Test Unlock Clue (Play Mode Only)"))
|
||||
{
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
DetectiveBoardManager.Instance?.UnlockClue(clueData);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("This button only works in Play Mode!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 184cbe5b79ee0a84ba29da016ec76159
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 81f81f88e30714043a121630f7bf7cb9
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,40 @@
|
||||
using UnityEngine;
|
||||
using Unheimlich.Core.Audio;
|
||||
using Unheimlich.Features.DetectiveBoard.Data;
|
||||
|
||||
namespace Unheimlich.Features.DetectiveBoard
|
||||
{
|
||||
/// <summary>
|
||||
/// Unlocks a clue when a specific AudioSource plays, finishes, or reaches a timestamp.
|
||||
/// Call OnAudioPlay() from whatever drives the AudioSource to start monitoring.
|
||||
/// </summary>
|
||||
public class ClueUnlockOnAudio : AudioTriggerBase
|
||||
{
|
||||
[Header("Clue Configuration")]
|
||||
[Tooltip("The clue to unlock")]
|
||||
[SerializeField] private ClueData _clueToUnlock;
|
||||
|
||||
protected override void Execute()
|
||||
{
|
||||
if (_clueToUnlock == null)
|
||||
{
|
||||
Debug.LogError("[ClueUnlockOnAudio] No ClueData assigned!", this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (DetectiveBoardManager.Instance == null)
|
||||
{
|
||||
Debug.LogError("[ClueUnlockOnAudio] DetectiveBoardManager not found!", this);
|
||||
return;
|
||||
}
|
||||
|
||||
DetectiveBoardManager.Instance.UnlockClue(_clueToUnlock);
|
||||
|
||||
if (_debugMode)
|
||||
Debug.Log($"[ClueUnlockOnAudio] Unlocked clue '{_clueToUnlock.Title}' on '{_fireCondition}'.", this);
|
||||
}
|
||||
|
||||
/// <summary>Resets the unlock state so the clue can be unlocked again.</summary>
|
||||
public void ResetUnlock() => ResetFired();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fcedba870c1ea9642a272a53148bb933
|
||||
@@ -0,0 +1,60 @@
|
||||
using UnityEngine;
|
||||
using Unheimlich.Features.DetectiveBoard.Data;
|
||||
using Unheimlich.Core.Interaction;
|
||||
using Unheimlich.Features.DetectiveBoard;
|
||||
using Unheimlich.Features.Items;
|
||||
|
||||
namespace Unheimlich.Features.DetectiveBoard
|
||||
{
|
||||
/// <summary>
|
||||
/// Unlocks a clue when the player interacts with this object.
|
||||
/// Can work standalone (extends InteractableBase) or piggyback on an existing InspectableItem.
|
||||
/// </summary>
|
||||
public class ClueUnlockOnInteraction : InteractableBase
|
||||
{
|
||||
[Header("Clue Configuration")]
|
||||
[Tooltip("The clue to unlock when player interacts")]
|
||||
[SerializeField] private ClueData _clueToUnlock;
|
||||
|
||||
[Tooltip("Only unlock once")]
|
||||
[SerializeField] private bool _unlockOnlyOnce = true;
|
||||
|
||||
private bool _hasUnlocked;
|
||||
private InspectableItem _inspectableItem;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_inspectableItem = GetComponent<InspectableItem>();
|
||||
}
|
||||
|
||||
public override void Interact()
|
||||
{
|
||||
if (_unlockOnlyOnce && _hasUnlocked)
|
||||
{
|
||||
if (_inspectableItem != null)
|
||||
_inspectableItem.Interact();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_clueToUnlock == null)
|
||||
{
|
||||
Debug.LogError("[ClueUnlockOnInteraction] No clue data assigned!");
|
||||
if (_inspectableItem != null)
|
||||
_inspectableItem.Interact();
|
||||
return;
|
||||
}
|
||||
|
||||
if (DetectiveBoardManager.Instance != null)
|
||||
{
|
||||
DetectiveBoardManager.Instance.UnlockClue(_clueToUnlock);
|
||||
_hasUnlocked = true;
|
||||
|
||||
}
|
||||
|
||||
if (_inspectableItem != null)
|
||||
{
|
||||
_inspectableItem.Interact();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cca584f48d1f0ab429416470fd01d543
|
||||
@@ -0,0 +1,98 @@
|
||||
using UnityEngine;
|
||||
using Unheimlich.Core.Events;
|
||||
using Unheimlich.Features.DetectiveBoard.Data;
|
||||
|
||||
namespace Unheimlich.Features.DetectiveBoard
|
||||
{
|
||||
/// <summary>
|
||||
/// Unlocks a clue on the detective board.
|
||||
/// Can be driven by an optional GameEventTrigger on the same GameObject,
|
||||
/// or called directly via UnlockClue() from any external script or UnityEvent.
|
||||
/// </summary>
|
||||
public class ClueUnlockTrigger : MonoBehaviour
|
||||
{
|
||||
[Header("Clue Configuration")]
|
||||
[Tooltip("The clue data to unlock when triggered")]
|
||||
[SerializeField] private ClueData _clueToUnlock;
|
||||
|
||||
[Header("Auto Trigger")]
|
||||
[Tooltip("Unlock automatically on Start()")]
|
||||
[SerializeField] private bool _unlockOnStart;
|
||||
|
||||
[Tooltip("Delay in seconds before unlocking (only with Unlock On Start)")]
|
||||
[SerializeField] private float _unlockDelay;
|
||||
|
||||
[Tooltip("Prevent unlocking more than once")]
|
||||
[SerializeField] private bool _unlockOnlyOnce = true;
|
||||
|
||||
[Header("Debug")]
|
||||
[SerializeField] private bool _debugMode;
|
||||
|
||||
private GameEventTrigger _eventTrigger;
|
||||
private bool _hasUnlocked;
|
||||
|
||||
/// <summary>Raised after the clue has been successfully unlocked.</summary>
|
||||
public event System.Action OnClueUnlocked;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
// Optional composition with GameEventTrigger
|
||||
_eventTrigger = GetComponent<GameEventTrigger>();
|
||||
if (_eventTrigger != null)
|
||||
_eventTrigger.OnTriggered += UnlockClue;
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (_unlockOnStart)
|
||||
{
|
||||
if (_unlockDelay > 0f)
|
||||
Invoke(nameof(UnlockClue), _unlockDelay);
|
||||
else
|
||||
UnlockClue();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (_eventTrigger != null)
|
||||
_eventTrigger.OnTriggered -= UnlockClue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unlocks the associated clue. Can be called directly from external scripts or UnityEvents.
|
||||
/// </summary>
|
||||
public void UnlockClue()
|
||||
{
|
||||
if (_unlockOnlyOnce && _hasUnlocked)
|
||||
return;
|
||||
|
||||
if (_clueToUnlock == null)
|
||||
{
|
||||
Debug.LogError("[ClueUnlockTrigger] No clue data assigned!", this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (DetectiveBoardManager.Instance == null)
|
||||
{
|
||||
Debug.LogError("[ClueUnlockTrigger] DetectiveBoardManager not found!", this);
|
||||
return;
|
||||
}
|
||||
|
||||
DetectiveBoardManager.Instance.UnlockClue(_clueToUnlock);
|
||||
_hasUnlocked = true;
|
||||
|
||||
if (_debugMode)
|
||||
Debug.Log($"[ClueUnlockTrigger] Unlocked clue: {_clueToUnlock.Title}", this);
|
||||
|
||||
OnClueUnlocked?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>Resets the unlock state so the clue can be unlocked again.</summary>
|
||||
public void ResetUnlock()
|
||||
{
|
||||
_hasUnlocked = false;
|
||||
CancelInvoke(nameof(UnlockClue));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 61b90106a531ea840b670f368e2c8aa9
|
||||
@@ -0,0 +1,219 @@
|
||||
using UnityEngine;
|
||||
using Unheimlich.Core;
|
||||
using Unheimlich.World.CameraSystem;
|
||||
using Unheimlich.Features.Player;
|
||||
using Unheimlich.Features.OctoArms;
|
||||
using InputManager = Unheimlich.Core.Input.InputManager;
|
||||
using Unheimlich.Features.DetectiveBoard.UI;
|
||||
using Unheimlich.Features.Dialogue.UI;
|
||||
|
||||
namespace Unheimlich.Features.DetectiveBoard
|
||||
{
|
||||
/// <summary>
|
||||
/// Controls player interaction with the detective board.
|
||||
/// </summary>
|
||||
public class DetectiveBoardController : Singleton<DetectiveBoardController>
|
||||
{
|
||||
[Header("Settings")]
|
||||
[SerializeField] private float _activationRange = 2f;
|
||||
[SerializeField] private LayerMask _boardLayer;
|
||||
[SerializeField] private CameraPanSettings _playerAlignmentSettings;
|
||||
|
||||
[Header("Virtual Camera")]
|
||||
[SerializeField] private DetectiveBoardVirtualCamera _virtualCamera;
|
||||
|
||||
private bool _isBoardActive;
|
||||
private CameraHeadBob _cameraHeadBob;
|
||||
|
||||
public bool IsBoardActive => _isBoardActive;
|
||||
|
||||
protected override void OnAwake()
|
||||
{
|
||||
_cameraHeadBob = FindFirstObjectByType<CameraHeadBob>();
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
if(_virtualCamera == null)
|
||||
_virtualCamera = DetectiveBoardVirtualCamera.Instance;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (_isBoardActive)
|
||||
{
|
||||
HandleBoardInteraction();
|
||||
}
|
||||
else
|
||||
{
|
||||
CheckForBoardActivation();
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckForBoardActivation()
|
||||
{
|
||||
if (PlayerController.Instance == null ||
|
||||
PlayerController.Instance.PlayerCamera == null ||
|
||||
DetectiveBoardCanvas.Instance == null)
|
||||
return;
|
||||
|
||||
var camera = PlayerController.Instance.PlayerCamera;
|
||||
var boardTransform = DetectiveBoardCanvas.Instance.transform;
|
||||
|
||||
float distanceToBoard = Vector3.Distance(camera.transform.position, boardTransform.position);
|
||||
if (distanceToBoard > _activationRange)
|
||||
return;
|
||||
|
||||
Ray ray = new Ray(camera.transform.position, camera.transform.forward);
|
||||
if (Physics.Raycast(ray, out RaycastHit hit, _activationRange, _boardLayer))
|
||||
{
|
||||
bool isLookingAtBoard = (hit.transform == boardTransform) ||
|
||||
(boardTransform.IsChildOf(hit.transform)) ||
|
||||
(hit.transform.IsChildOf(boardTransform));
|
||||
|
||||
if (isLookingAtBoard)
|
||||
{
|
||||
var interactAction = InputManager.GetInteractAction();
|
||||
if (interactAction != null && interactAction.WasPressedThisFrame())
|
||||
{
|
||||
ActivateBoard();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ActivateBoard()
|
||||
{
|
||||
if (_isBoardActive ||
|
||||
PlayerController.Instance?.PlayerCamera == null ||
|
||||
PlayerController.Instance?.CameraPanController == null ||
|
||||
DetectiveBoardCanvas.Instance == null ||
|
||||
DetectiveBoardVirtualCamera.Instance == null)
|
||||
{
|
||||
Debug.LogError("[DetectiveBoardController] Missing required references!");
|
||||
return;
|
||||
}
|
||||
|
||||
_isBoardActive = true;
|
||||
|
||||
if (PlayerController.Instance.Movement != null)
|
||||
{
|
||||
PlayerController.Instance.Movement.enabled = false;
|
||||
}
|
||||
|
||||
OctoIkArmsImproved arms = PlayerController.Instance.GetComponentInChildren<OctoIkArmsImproved>();
|
||||
if (arms != null)
|
||||
{
|
||||
arms.HideArms(3f);
|
||||
}
|
||||
|
||||
InputManager.SetContext(InputManager.InputContext.DetectiveBoard);
|
||||
|
||||
if (_cameraHeadBob != null)
|
||||
{
|
||||
_cameraHeadBob.SetMode(HeadBobMode.Disabled);
|
||||
}
|
||||
|
||||
if (CrosshairController.Instance != null)
|
||||
{
|
||||
CrosshairController.Instance.Hide();
|
||||
}
|
||||
|
||||
var cameraPan = PlayerController.Instance.CameraPanController;
|
||||
var originalSettings = cameraPan.Settings;
|
||||
|
||||
if (_playerAlignmentSettings != null)
|
||||
cameraPan.Settings = _playerAlignmentSettings;
|
||||
|
||||
cameraPan.SaveOriginalState();
|
||||
|
||||
cameraPan.AlignToTarget(DetectiveBoardCanvas.Instance.transform, () =>
|
||||
{
|
||||
DetectiveBoardVirtualCamera.Instance.ActivateFromPlayerPosition(
|
||||
PlayerController.Instance.PlayerCamera,
|
||||
DetectiveBoardCanvas.Instance.transform,
|
||||
DetectiveBoardCanvas.Instance.BoardRenderer
|
||||
);
|
||||
|
||||
// Show clue hand fan when board is ready
|
||||
if (ClueHandUI.Instance != null)
|
||||
ClueHandUI.Instance.SetHandVisible(true);
|
||||
|
||||
if (_playerAlignmentSettings != null)
|
||||
cameraPan.Settings = originalSettings;
|
||||
});
|
||||
}
|
||||
|
||||
private void HandleBoardInteraction()
|
||||
{
|
||||
var exitAction = InputManager.GetExitAction();
|
||||
if (exitAction != null && exitAction.WasPressedThisFrame())
|
||||
{
|
||||
DeactivateBoard();
|
||||
}
|
||||
}
|
||||
|
||||
public void DeactivateBoard()
|
||||
{
|
||||
if (!_isBoardActive || DetectiveBoardVirtualCamera.Instance == null)
|
||||
return;
|
||||
|
||||
_isBoardActive = false;
|
||||
|
||||
// Hide clue hand fan
|
||||
if (ClueHandUI.Instance != null)
|
||||
ClueHandUI.Instance.SetHandVisible(false);
|
||||
|
||||
DetectiveBoardVirtualCamera.Instance.Deactivate(() =>
|
||||
{
|
||||
var cameraPan = PlayerController.Instance?.CameraPanController;
|
||||
if (cameraPan != null)
|
||||
{
|
||||
OctoIkArmsImproved arms = PlayerController.Instance.GetComponentInChildren<OctoIkArmsImproved>();
|
||||
if (arms != null)
|
||||
{
|
||||
arms.ShowArms(3f);
|
||||
}
|
||||
|
||||
cameraPan.ReturnToOriginalPosition(() =>
|
||||
{
|
||||
if (PlayerController.Instance?.Movement != null)
|
||||
{
|
||||
PlayerController.Instance.Movement.enabled = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
else if (PlayerController.Instance?.Movement != null)
|
||||
{
|
||||
PlayerController.Instance.Movement.enabled = true;
|
||||
}
|
||||
});
|
||||
|
||||
InputManager.SetContext(InputManager.InputContext.Normal);
|
||||
|
||||
RestoreHeadBob();
|
||||
|
||||
if (CrosshairController.Instance != null)
|
||||
{
|
||||
CrosshairController.Instance.Show();
|
||||
}
|
||||
}
|
||||
|
||||
private void RestoreHeadBob()
|
||||
{
|
||||
if (_cameraHeadBob == null)
|
||||
return;
|
||||
|
||||
_cameraHeadBob.ClearOverride();
|
||||
}
|
||||
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
if (DetectiveBoardCanvas.Instance == null)
|
||||
return;
|
||||
|
||||
Gizmos.color = _isBoardActive ? Color.green : Color.yellow;
|
||||
Gizmos.DrawWireSphere(DetectiveBoardCanvas.Instance.transform.position, _activationRange);
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9f9f72364623a2a4bb50f02537fd2e72
|
||||
@@ -0,0 +1,191 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using Unheimlich.Core;
|
||||
using Unheimlich.Features.DetectiveBoard.Data;
|
||||
|
||||
namespace Unheimlich.Features.DetectiveBoard
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages the detective board state and clue unlocking system.
|
||||
/// Tracks unlocked clues, connections, certainty levels, and eliminated hypotheses.
|
||||
/// Does NOT affect game progression - purely interpretive tool for the player.
|
||||
/// </summary>
|
||||
public class DetectiveBoardManager : Singleton<DetectiveBoardManager>
|
||||
{
|
||||
public enum CertaintyLevel
|
||||
{
|
||||
Uncertain,
|
||||
Possible,
|
||||
Confident,
|
||||
Certain
|
||||
}
|
||||
|
||||
public class ClueState
|
||||
{
|
||||
public ClueData Data;
|
||||
public bool IsUnlocked;
|
||||
public bool IsEliminated;
|
||||
}
|
||||
|
||||
public class Connection
|
||||
{
|
||||
public string FromClueID;
|
||||
public string ToClueID;
|
||||
public CertaintyLevel Certainty;
|
||||
public bool IsEliminated;
|
||||
}
|
||||
|
||||
[Header("References")]
|
||||
|
||||
private Dictionary<string, ClueState> _clueStates = new Dictionary<string, ClueState>();
|
||||
private List<Connection> _connections = new List<Connection>();
|
||||
|
||||
public IReadOnlyDictionary<string, ClueState> ClueStates => _clueStates;
|
||||
public IReadOnlyList<Connection> Connections => _connections;
|
||||
|
||||
/// <summary>
|
||||
/// Unlocks a clue on the detective board. Can be triggered by any in-game event.
|
||||
/// </summary>
|
||||
public void UnlockClue(ClueData clueData)
|
||||
{
|
||||
if (clueData == null)
|
||||
{
|
||||
Debug.LogError("[DetectiveBoardManager] Attempted to unlock null clue!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_clueStates.ContainsKey(clueData.ClueID))
|
||||
{
|
||||
_clueStates[clueData.ClueID] = new ClueState
|
||||
{
|
||||
Data = clueData,
|
||||
IsUnlocked = true,
|
||||
IsEliminated = false
|
||||
};
|
||||
|
||||
OnClueUnlocked?.Invoke(clueData);
|
||||
}
|
||||
else if (!_clueStates[clueData.ClueID].IsUnlocked)
|
||||
{
|
||||
_clueStates[clueData.ClueID].IsUnlocked = true;
|
||||
OnClueUnlocked?.Invoke(clueData);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a list of all currently unlocked clues.
|
||||
/// Used by DetectiveBoardCanvas to display already-unlocked clues on initialization.
|
||||
/// </summary>
|
||||
public List<ClueData> GetUnlockedClues()
|
||||
{
|
||||
List<ClueData> unlockedClues = new List<ClueData>();
|
||||
foreach (var state in _clueStates.Values)
|
||||
{
|
||||
if (state.IsUnlocked)
|
||||
{
|
||||
unlockedClues.Add(state.Data);
|
||||
}
|
||||
}
|
||||
return unlockedClues;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unlocks a clue by its ID string.
|
||||
/// </summary>
|
||||
public void UnlockClueByID(string clueID)
|
||||
{
|
||||
if (_clueStates.TryGetValue(clueID, out ClueState state))
|
||||
{
|
||||
if (!state.IsUnlocked)
|
||||
{
|
||||
state.IsUnlocked = true;
|
||||
OnClueUnlocked?.Invoke(state.Data);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"[DetectiveBoardManager] Clue ID '{clueID}' not found!");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a clue in the system without unlocking it.
|
||||
/// </summary>
|
||||
public void RegisterClue(ClueData clueData)
|
||||
{
|
||||
if (clueData == null || _clueStates.ContainsKey(clueData.ClueID))
|
||||
return;
|
||||
|
||||
_clueStates[clueData.ClueID] = new ClueState
|
||||
{
|
||||
Data = clueData,
|
||||
IsUnlocked = false,
|
||||
IsEliminated = false
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a connection between two clues.
|
||||
/// </summary>
|
||||
public void CreateConnection(string fromClueID, string toClueID, CertaintyLevel certainty = CertaintyLevel.Uncertain)
|
||||
{
|
||||
if (!_clueStates.ContainsKey(fromClueID) || !_clueStates.ContainsKey(toClueID))
|
||||
{
|
||||
Debug.LogWarning($"[DetectiveBoardManager] Cannot create connection - one or both clues not found!");
|
||||
return;
|
||||
}
|
||||
|
||||
Connection existing = _connections.Find(c =>
|
||||
(c.FromClueID == fromClueID && c.ToClueID == toClueID) ||
|
||||
(c.FromClueID == toClueID && c.ToClueID == fromClueID));
|
||||
|
||||
if (existing != null)
|
||||
{
|
||||
existing.Certainty = certainty;
|
||||
}
|
||||
else
|
||||
{
|
||||
_connections.Add(new Connection
|
||||
{
|
||||
FromClueID = fromClueID,
|
||||
ToClueID = toClueID,
|
||||
Certainty = certainty,
|
||||
IsEliminated = false
|
||||
});
|
||||
|
||||
OnConnectionCreated?.Invoke(fromClueID, toClueID);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks a clue as eliminated (incorrect hypothesis).
|
||||
/// </summary>
|
||||
public void EliminateClue(string clueID)
|
||||
{
|
||||
if (_clueStates.TryGetValue(clueID, out ClueState state))
|
||||
{
|
||||
state.IsEliminated = true;
|
||||
OnClueEliminated?.Invoke(clueID);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks a connection as eliminated.
|
||||
/// </summary>
|
||||
public void EliminateConnection(string fromClueID, string toClueID)
|
||||
{
|
||||
Connection connection = _connections.Find(c =>
|
||||
(c.FromClueID == fromClueID && c.ToClueID == toClueID) ||
|
||||
(c.FromClueID == toClueID && c.ToClueID == fromClueID));
|
||||
|
||||
if (connection != null)
|
||||
{
|
||||
connection.IsEliminated = true;
|
||||
}
|
||||
}
|
||||
|
||||
public event System.Action<ClueData> OnClueUnlocked;
|
||||
public event System.Action<string, string> OnConnectionCreated;
|
||||
public event System.Action<string> OnClueEliminated;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4b187cdccaa498441a752e8f49ff4290
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 466fa804543e82c4483a22466a9bb484
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,170 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Unheimlich.Features.DetectiveBoard.UI
|
||||
{
|
||||
public enum LineColorState
|
||||
{
|
||||
Green = 0,
|
||||
Orange = 1,
|
||||
Red = 2
|
||||
}
|
||||
|
||||
[RequireComponent(typeof(RectTransform))]
|
||||
public class ConnectionLine : MonoBehaviour
|
||||
{
|
||||
[Header("References")]
|
||||
[SerializeField] private Image _lineImage;
|
||||
[SerializeField] private Image _outlineImage;
|
||||
[SerializeField] private RectTransform _rectTransform;
|
||||
|
||||
private RectTransform _lineRectTransform;
|
||||
private RectTransform _outlineRectTransform;
|
||||
|
||||
[Header("Settings")]
|
||||
[SerializeField] private float _lineThickness = 3f;
|
||||
[SerializeField] private Color _outlineColor = Color.yellow;
|
||||
|
||||
[Header("Color States")]
|
||||
[SerializeField] private Color _greenColor = Color.green;
|
||||
[SerializeField] private Color _orangeColor = new Color(1f, 0.5f, 0f);
|
||||
[SerializeField] private Color _redColor = Color.red;
|
||||
|
||||
private PostItNode _startNode;
|
||||
private PostItNode _endNode;
|
||||
private LineColorState _colorState = LineColorState.Green;
|
||||
private bool _isHovered;
|
||||
|
||||
public PostItNode StartNode => _startNode;
|
||||
public PostItNode EndNode => _endNode;
|
||||
public LineColorState ColorState => _colorState;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (_rectTransform == null)
|
||||
_rectTransform = GetComponent<RectTransform>();
|
||||
|
||||
if (_lineImage == null)
|
||||
_lineImage = GetComponentInChildren<Image>();
|
||||
|
||||
if (_lineImage != null)
|
||||
{
|
||||
_lineRectTransform = _lineImage.GetComponent<RectTransform>();
|
||||
_lineImage.raycastTarget = true;
|
||||
}
|
||||
|
||||
if (_rectTransform != null)
|
||||
{
|
||||
_rectTransform.anchorMin = new Vector2(0.5f, 0.5f);
|
||||
_rectTransform.anchorMax = new Vector2(0.5f, 0.5f);
|
||||
_rectTransform.pivot = new Vector2(0f, 0.5f);
|
||||
}
|
||||
|
||||
if (_lineRectTransform != null)
|
||||
{
|
||||
_lineRectTransform.anchorMin = new Vector2(0f, 0.5f);
|
||||
_lineRectTransform.anchorMax = new Vector2(0f, 0.5f);
|
||||
_lineRectTransform.pivot = new Vector2(0f, 0.5f);
|
||||
}
|
||||
|
||||
if (_outlineImage == null)
|
||||
{
|
||||
_outlineImage = transform.Find("Outline")?.GetComponent<Image>();
|
||||
}
|
||||
|
||||
if (_outlineImage != null)
|
||||
{
|
||||
_outlineRectTransform = _outlineImage.GetComponent<RectTransform>();
|
||||
if (_outlineRectTransform != null)
|
||||
{
|
||||
_outlineRectTransform.anchorMin = new Vector2(0f, 0.5f);
|
||||
_outlineRectTransform.anchorMax = new Vector2(0f, 0.5f);
|
||||
_outlineRectTransform.pivot = new Vector2(0f, 0.5f);
|
||||
}
|
||||
_outlineImage.raycastTarget = false;
|
||||
_outlineImage.gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
public void Initialize(PostItNode startNode, PostItNode endNode)
|
||||
{
|
||||
_startNode = startNode;
|
||||
_endNode = endNode;
|
||||
_colorState = LineColorState.Green;
|
||||
|
||||
UpdateColorState();
|
||||
UpdateLine();
|
||||
}
|
||||
|
||||
public void UpdateLine()
|
||||
{
|
||||
if (_startNode == null || _endNode == null || _rectTransform == null)
|
||||
return;
|
||||
|
||||
Vector2 startPos = _startNode.ConnectionPoint.anchoredPosition;
|
||||
Vector2 endPos = _endNode.ConnectionPoint.anchoredPosition;
|
||||
|
||||
Vector2 direction = endPos - startPos;
|
||||
float distance = direction.magnitude;
|
||||
|
||||
_rectTransform.anchoredPosition = startPos;
|
||||
|
||||
float angle = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg;
|
||||
_rectTransform.localRotation = Quaternion.Euler(0, 0, angle);
|
||||
|
||||
if (_lineRectTransform != null)
|
||||
{
|
||||
_lineRectTransform.sizeDelta = new Vector2(distance, _lineThickness);
|
||||
}
|
||||
|
||||
if (_outlineRectTransform != null)
|
||||
{
|
||||
_outlineRectTransform.sizeDelta = new Vector2(distance, _lineThickness + 3f);
|
||||
}
|
||||
}
|
||||
|
||||
public void CycleColorState()
|
||||
{
|
||||
_colorState = (LineColorState)(((int)_colorState + 1) % 3);
|
||||
UpdateColorState();
|
||||
}
|
||||
|
||||
public void SetColorState(LineColorState state)
|
||||
{
|
||||
_colorState = state;
|
||||
UpdateColorState();
|
||||
}
|
||||
|
||||
private void UpdateColorState()
|
||||
{
|
||||
if (_lineImage == null)
|
||||
return;
|
||||
|
||||
Color targetColor = _colorState switch
|
||||
{
|
||||
LineColorState.Green => _greenColor,
|
||||
LineColorState.Orange => _orangeColor,
|
||||
LineColorState.Red => _redColor,
|
||||
_ => _greenColor
|
||||
};
|
||||
|
||||
_lineImage.color = targetColor;
|
||||
}
|
||||
|
||||
public void SetHovered(bool hovered)
|
||||
{
|
||||
_isHovered = hovered;
|
||||
|
||||
if (_outlineImage != null)
|
||||
{
|
||||
_outlineImage.gameObject.SetActive(hovered);
|
||||
}
|
||||
}
|
||||
|
||||
public bool ConnectsNodes(PostItNode nodeA, PostItNode nodeB)
|
||||
{
|
||||
return (_startNode == nodeA && _endNode == nodeB) ||
|
||||
(_startNode == nodeB && _endNode == nodeA);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d38fc1a64b1949443b49ec6506174085
|
||||
@@ -0,0 +1,90 @@
|
||||
using UnityEngine;
|
||||
using Unheimlich.Features.DetectiveBoard;
|
||||
|
||||
namespace Unheimlich.Features.DetectiveBoard.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Renders a connection line between two clue nodes on the detective board.
|
||||
/// Visual style changes based on certainty level and elimination state.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(LineRenderer))]
|
||||
public class ConnectionLineRenderer : MonoBehaviour
|
||||
{
|
||||
private LineRenderer _lineRenderer;
|
||||
private string _fromClueID;
|
||||
private string _toClueID;
|
||||
private DetectiveBoardManager.CertaintyLevel _certainty;
|
||||
private bool _isEliminated;
|
||||
|
||||
public string FromClueID => _fromClueID;
|
||||
public string ToClueID => _toClueID;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_lineRenderer = GetComponent<LineRenderer>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the connection line between two nodes.
|
||||
/// </summary>
|
||||
public void Initialize(string fromClueID, string toClueID,
|
||||
DetectiveBoardManager.CertaintyLevel certainty, Color color, float width)
|
||||
{
|
||||
_fromClueID = fromClueID;
|
||||
_toClueID = toClueID;
|
||||
_certainty = certainty;
|
||||
|
||||
if (_lineRenderer != null)
|
||||
{
|
||||
_lineRenderer.startWidth = width;
|
||||
_lineRenderer.endWidth = width;
|
||||
_lineRenderer.material = new Material(Shader.Find("Sprites/Default"));
|
||||
_lineRenderer.startColor = color;
|
||||
_lineRenderer.endColor = color;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the line positions based on node transforms.
|
||||
/// </summary>
|
||||
public void UpdateLine(Vector3 startPos, Vector3 endPos)
|
||||
{
|
||||
if (_lineRenderer != null)
|
||||
{
|
||||
_lineRenderer.SetPosition(0, startPos);
|
||||
_lineRenderer.SetPosition(1, endPos);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the certainty level and visual appearance.
|
||||
/// </summary>
|
||||
public void SetCertainty(DetectiveBoardManager.CertaintyLevel certainty, Color color)
|
||||
{
|
||||
_certainty = certainty;
|
||||
|
||||
if (_lineRenderer != null)
|
||||
{
|
||||
_lineRenderer.startColor = color;
|
||||
_lineRenderer.endColor = color;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks this connection as eliminated.
|
||||
/// </summary>
|
||||
public void SetEliminated(bool eliminated)
|
||||
{
|
||||
_isEliminated = eliminated;
|
||||
|
||||
if (_lineRenderer != null)
|
||||
{
|
||||
float alpha = eliminated ? 0.3f : 1f;
|
||||
Color color = _lineRenderer.startColor;
|
||||
color.a = alpha;
|
||||
_lineRenderer.startColor = color;
|
||||
_lineRenderer.endColor = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8d829d9811842b241b980bd64ab73bff
|
||||
@@ -0,0 +1,422 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: edfcd01879d2cb34495302ef9882965b
|
||||
@@ -0,0 +1,430 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
using UnityEngine.UI;
|
||||
using Unheimlich.Core;
|
||||
using Unheimlich.Features.DetectiveBoard;
|
||||
using Unheimlich.Features.Player;
|
||||
using Unheimlich.World.CameraSystem;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine.EventSystems;
|
||||
|
||||
namespace Unheimlich.Features.DetectiveBoard.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles mouse interaction for the detective board World Space canvas.
|
||||
/// 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 : Singleton<DetectiveBoardInteraction>
|
||||
{
|
||||
[Header("Settings")]
|
||||
[SerializeField] private LayerMask _boardLayer;
|
||||
[SerializeField] private float _minInteractionDistance = 0.1f;
|
||||
[SerializeField] private float _maxInteractionDistance = 10f;
|
||||
|
||||
private Camera _camera;
|
||||
private Canvas _boardCanvas;
|
||||
private RectTransform _boardArea;
|
||||
private DetectiveBoardController _boardController;
|
||||
private DetectiveBoardCanvas _canvasManager;
|
||||
private PostItNode _hoveredNode;
|
||||
private PostItNode _selectedNode;
|
||||
private ConnectionLine _hoveredLine;
|
||||
private bool _isDragging;
|
||||
private Plane _canvasPlane;
|
||||
private Vector3 _dragOffset;
|
||||
|
||||
protected override void OnAwake() { }
|
||||
|
||||
private void Start()
|
||||
{
|
||||
_boardCanvas = GetComponent<Canvas>();
|
||||
_canvasManager = GetComponent<DetectiveBoardCanvas>();
|
||||
_boardController = DetectiveBoardController.Instance;
|
||||
|
||||
if (_canvasManager != null && _canvasManager.BoardArea != null)
|
||||
_boardArea = _canvasManager.BoardArea;
|
||||
|
||||
if (PlayerController.Instance != null)
|
||||
_camera = PlayerController.Instance.PlayerCamera;
|
||||
|
||||
if (_camera == null)
|
||||
_camera = Camera.main;
|
||||
|
||||
UpdateCanvasPlane();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!IsInBoardInteractionMode())
|
||||
{
|
||||
ClearInteractionState();
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateCameraReference();
|
||||
UpdateCanvasPlane();
|
||||
|
||||
if (_isDragging)
|
||||
{
|
||||
HandleDrag();
|
||||
}
|
||||
else
|
||||
{
|
||||
HandleHover();
|
||||
HandleLineHover();
|
||||
}
|
||||
|
||||
HandleClick();
|
||||
}
|
||||
|
||||
private void UpdateCameraReference()
|
||||
{
|
||||
if (DetectiveBoardVirtualCamera.Instance != null && DetectiveBoardVirtualCamera.Instance.IsActive)
|
||||
{
|
||||
_camera = DetectiveBoardVirtualCamera.Instance.VirtualCamera;
|
||||
}
|
||||
else if (PlayerController.Instance != null)
|
||||
{
|
||||
_camera = PlayerController.Instance.PlayerCamera;
|
||||
}
|
||||
else if (_camera == null)
|
||||
{
|
||||
_camera = Camera.main;
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsInBoardInteractionMode()
|
||||
{
|
||||
bool isActive = _boardController != null && _boardController.IsBoardActive;
|
||||
|
||||
return isActive;
|
||||
}
|
||||
|
||||
private void ClearInteractionState()
|
||||
{
|
||||
if (_hoveredNode != null)
|
||||
{
|
||||
_hoveredNode.SetHovered(false);
|
||||
_hoveredNode = null;
|
||||
}
|
||||
|
||||
if (_hoveredLine != null && _canvasManager != null)
|
||||
{
|
||||
_canvasManager.SetHoveredLine(null);
|
||||
_hoveredLine = null;
|
||||
}
|
||||
|
||||
if (_selectedNode != null && _isDragging)
|
||||
{
|
||||
_selectedNode.SetSelected(false);
|
||||
_selectedNode = null;
|
||||
_isDragging = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateCanvasPlane()
|
||||
{
|
||||
if (_boardCanvas != null)
|
||||
{
|
||||
Vector3 planeNormal = -_boardCanvas.transform.forward;
|
||||
Vector3 planePosition = _boardCanvas.transform.position;
|
||||
_canvasPlane = new Plane(planeNormal, planePosition);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleHover()
|
||||
{
|
||||
PostItNode hitNode = RaycastForNode();
|
||||
|
||||
if (hitNode != _hoveredNode)
|
||||
{
|
||||
if (_hoveredNode != null)
|
||||
{
|
||||
_hoveredNode.SetHovered(false);
|
||||
}
|
||||
|
||||
_hoveredNode = hitNode;
|
||||
|
||||
if (_hoveredNode != null)
|
||||
{
|
||||
_hoveredNode.SetHovered(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleLineHover()
|
||||
{
|
||||
if (_canvasManager == null || Mouse.current == null)
|
||||
return;
|
||||
|
||||
if (_hoveredNode != null)
|
||||
{
|
||||
if (_hoveredLine != null)
|
||||
{
|
||||
_canvasManager.SetHoveredLine(null);
|
||||
_hoveredLine = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
ConnectionLine hitLine = RaycastForLine();
|
||||
|
||||
if (hitLine != _hoveredLine)
|
||||
{
|
||||
_canvasManager.SetHoveredLine(hitLine);
|
||||
_hoveredLine = hitLine;
|
||||
}
|
||||
}
|
||||
|
||||
private ConnectionLine RaycastForLine()
|
||||
{
|
||||
if (_camera == null || Mouse.current == null)
|
||||
return null;
|
||||
|
||||
Vector2 mousePos = Mouse.current.position.ReadValue();
|
||||
|
||||
GraphicRaycaster raycaster = _boardCanvas?.GetComponent<GraphicRaycaster>();
|
||||
if (raycaster != null && EventSystem.current != null)
|
||||
{
|
||||
var pointerEventData = new PointerEventData(EventSystem.current)
|
||||
{
|
||||
position = mousePos
|
||||
};
|
||||
|
||||
var results = new List<RaycastResult>();
|
||||
raycaster.Raycast(pointerEventData, results);
|
||||
|
||||
foreach (var result in results)
|
||||
{
|
||||
ConnectionLine line = result.gameObject.GetComponentInParent<ConnectionLine>();
|
||||
if (line != null)
|
||||
return line;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void HandleClick()
|
||||
{
|
||||
if (Mouse.current == null)
|
||||
return;
|
||||
|
||||
PostItNode hitNode = RaycastForNode();
|
||||
|
||||
if (Mouse.current.leftButton.wasPressedThisFrame)
|
||||
{
|
||||
if (hitNode != null)
|
||||
{
|
||||
HandleLeftClick(hitNode);
|
||||
}
|
||||
else
|
||||
{
|
||||
ConnectionLine hitLine = RaycastForLine();
|
||||
if (hitLine != null && _canvasManager != null)
|
||||
{
|
||||
_canvasManager.OnLineClicked(hitLine);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (Mouse.current.rightButton.wasPressedThisFrame && hitNode != null && _canvasManager != null)
|
||||
{
|
||||
_canvasManager.OnNodeRightClicked(hitNode);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleLeftClick(PostItNode hitNode)
|
||||
{
|
||||
if (_canvasManager != null && _canvasManager.IsConnectMode)
|
||||
{
|
||||
if (hitNode != null)
|
||||
{
|
||||
_canvasManager.OnNodeClicked(hitNode);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (_isDragging)
|
||||
{
|
||||
_isDragging = false;
|
||||
|
||||
if (_selectedNode != null && hitNode == _selectedNode)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_selectedNode != null)
|
||||
{
|
||||
_selectedNode.SetSelected(false);
|
||||
}
|
||||
|
||||
if (hitNode != null)
|
||||
{
|
||||
_selectedNode = hitNode;
|
||||
_selectedNode.SetSelected(true);
|
||||
_isDragging = true;
|
||||
CalculateDragOffset();
|
||||
}
|
||||
else
|
||||
{
|
||||
_selectedNode = null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (hitNode != null)
|
||||
{
|
||||
if (_selectedNode != null && _selectedNode != hitNode)
|
||||
{
|
||||
_selectedNode.SetSelected(false);
|
||||
}
|
||||
|
||||
_selectedNode = hitNode;
|
||||
_selectedNode.SetSelected(true);
|
||||
_isDragging = true;
|
||||
CalculateDragOffset();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_selectedNode != null)
|
||||
{
|
||||
_selectedNode.SetSelected(false);
|
||||
_selectedNode = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleDrag()
|
||||
{
|
||||
if (_selectedNode == null || _camera == null)
|
||||
return;
|
||||
|
||||
Vector2 mousePos = Mouse.current.position.ReadValue();
|
||||
Ray ray = _camera.ScreenPointToRay(new Vector3(mousePos.x, mousePos.y, 0));
|
||||
|
||||
if (_canvasPlane.Raycast(ray, out float enter))
|
||||
{
|
||||
Vector3 worldHitPoint = ray.GetPoint(enter);
|
||||
Vector3 newWorldPos = worldHitPoint + _dragOffset;
|
||||
|
||||
Vector3 localPos = _boardCanvas.transform.InverseTransformPoint(newWorldPos);
|
||||
Vector2 clampedPos = ClampPositionToBoard(new Vector2(localPos.x, localPos.y));
|
||||
|
||||
_selectedNode.SetPosition(clampedPos);
|
||||
|
||||
if (_canvasManager != null)
|
||||
{
|
||||
_canvasManager.UpdateConnectionsForNode(_selectedNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Vector2 ClampPositionToBoard(Vector2 position)
|
||||
{
|
||||
if (_boardArea == null)
|
||||
return position;
|
||||
|
||||
Rect boardRect = _boardArea.rect;
|
||||
float halfWidth = boardRect.width * 0.5f;
|
||||
float halfHeight = boardRect.height * 0.5f;
|
||||
|
||||
float clampedX = Mathf.Clamp(position.x, -halfWidth, halfWidth);
|
||||
float clampedY = Mathf.Clamp(position.y, -halfHeight, halfHeight);
|
||||
|
||||
return new Vector2(clampedX, clampedY);
|
||||
}
|
||||
|
||||
private void CalculateDragOffset()
|
||||
{
|
||||
if (_selectedNode == null || _camera == null)
|
||||
return;
|
||||
|
||||
Vector2 mousePos = Mouse.current.position.ReadValue();
|
||||
Ray ray = _camera.ScreenPointToRay(new Vector3(mousePos.x, mousePos.y, 0));
|
||||
|
||||
if (_canvasPlane.Raycast(ray, out float enter))
|
||||
{
|
||||
Vector3 worldHitPoint = ray.GetPoint(enter);
|
||||
Vector3 currentWorldPos = _selectedNode.transform.position;
|
||||
_dragOffset = currentWorldPos - worldHitPoint;
|
||||
}
|
||||
}
|
||||
|
||||
/// <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)
|
||||
return null;
|
||||
|
||||
Vector2 mousePos = Mouse.current.position.ReadValue();
|
||||
|
||||
GraphicRaycaster raycaster = _boardCanvas?.GetComponent<GraphicRaycaster>();
|
||||
if (raycaster != null && EventSystem.current != null)
|
||||
{
|
||||
var pointerEventData = new PointerEventData(EventSystem.current)
|
||||
{
|
||||
position = mousePos
|
||||
};
|
||||
|
||||
var results = new List<RaycastResult>();
|
||||
raycaster.Raycast(pointerEventData, results);
|
||||
|
||||
foreach (var result in results)
|
||||
{
|
||||
PostItNode node = result.gameObject.GetComponentInParent<PostItNode>();
|
||||
if (node != null)
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
Ray ray = _camera.ScreenPointToRay(new Vector3(mousePos.x, mousePos.y, 0));
|
||||
|
||||
if (Physics.Raycast(ray, out RaycastHit hit, _maxInteractionDistance, _boardLayer))
|
||||
{
|
||||
float distance = hit.distance;
|
||||
if (distance >= _minInteractionDistance)
|
||||
{
|
||||
return hit.collider.GetComponentInParent<PostItNode>();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 17bf00a333a0bc9409c21009e3ac6aea
|
||||
@@ -0,0 +1,132 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
using Unheimlich.Features.DetectiveBoard.Data;
|
||||
|
||||
namespace Unheimlich.Features.DetectiveBoard.UI
|
||||
{
|
||||
public class PostItNode : MonoBehaviour
|
||||
{
|
||||
[Header("UI References")]
|
||||
[SerializeField] private RectTransform _rectTransform;
|
||||
[SerializeField] private Image _background;
|
||||
[SerializeField] private TextMeshProUGUI _titleText;
|
||||
[SerializeField] private TextMeshProUGUI _descriptionText;
|
||||
[SerializeField] private Image _iconImage;
|
||||
[SerializeField] private Image _selectionOutline;
|
||||
|
||||
[Header("Effects")]
|
||||
[SerializeField] private PostItScribbleEffect _scribbleEffect;
|
||||
|
||||
[Header("Connection Points")]
|
||||
[SerializeField] private RectTransform _connectionPoint;
|
||||
|
||||
private ClueData _clueData;
|
||||
private PostItNodeData _nodeData;
|
||||
private bool _isEliminated;
|
||||
private bool _isSelected;
|
||||
private Color _originalColor;
|
||||
|
||||
public string NodeID => _clueData?.ClueID;
|
||||
public RectTransform RectTransform => _rectTransform;
|
||||
public RectTransform ConnectionPoint => _rectTransform;
|
||||
public ClueData ClueData => _clueData;
|
||||
public PostItNodeData NodeData => _nodeData;
|
||||
public bool IsEliminated => _isEliminated;
|
||||
public bool IsSelected => _isSelected;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (_rectTransform == null)
|
||||
_rectTransform = GetComponent<RectTransform>();
|
||||
|
||||
if (_scribbleEffect == null)
|
||||
_scribbleEffect = GetComponent<PostItScribbleEffect>();
|
||||
|
||||
if (_selectionOutline != null)
|
||||
_selectionOutline.enabled = false;
|
||||
}
|
||||
|
||||
public void Initialize(ClueData clueData, PostItNodeData nodeData)
|
||||
{
|
||||
_clueData = clueData;
|
||||
_nodeData = nodeData;
|
||||
|
||||
if (_titleText != null)
|
||||
_titleText.text = clueData.Title;
|
||||
|
||||
if (_descriptionText != null)
|
||||
_descriptionText.text = clueData.Description;
|
||||
|
||||
if (_iconImage != null && clueData.Icon != null)
|
||||
{
|
||||
_iconImage.sprite = clueData.Icon;
|
||||
_iconImage.enabled = true;
|
||||
}
|
||||
else if (_iconImage != null)
|
||||
{
|
||||
_iconImage.enabled = false;
|
||||
}
|
||||
|
||||
if (_background != null)
|
||||
{
|
||||
_originalColor = clueData.NodeColor;
|
||||
_background.color = _originalColor;
|
||||
}
|
||||
|
||||
SetPosition(nodeData.Position);
|
||||
SetEliminated(nodeData.IsEliminated);
|
||||
}
|
||||
|
||||
public void SetPosition(Vector2 position)
|
||||
{
|
||||
if (_rectTransform != null)
|
||||
{
|
||||
_rectTransform.anchoredPosition = position;
|
||||
if (_nodeData != null)
|
||||
_nodeData.Position = position;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetRotation(float rotation)
|
||||
{
|
||||
if (_rectTransform != null)
|
||||
{
|
||||
_rectTransform.localRotation = Quaternion.Euler(0, 0, rotation);
|
||||
if (_nodeData != null)
|
||||
_nodeData.Rotation = rotation;
|
||||
}
|
||||
}
|
||||
|
||||
public float GetRotation()
|
||||
{
|
||||
return _rectTransform != null ? _rectTransform.localEulerAngles.z : 0f;
|
||||
}
|
||||
|
||||
public void SetEliminated(bool eliminated)
|
||||
{
|
||||
_isEliminated = eliminated;
|
||||
|
||||
if (_scribbleEffect != null)
|
||||
{
|
||||
_scribbleEffect.SetEliminated(eliminated);
|
||||
}
|
||||
|
||||
if (_nodeData != null)
|
||||
_nodeData.IsEliminated = eliminated;
|
||||
}
|
||||
|
||||
public void SetSelected(bool selected)
|
||||
{
|
||||
_isSelected = selected;
|
||||
}
|
||||
|
||||
public void SetHovered(bool hovered)
|
||||
{
|
||||
if (_selectionOutline != null)
|
||||
{
|
||||
_selectionOutline.enabled = hovered;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a398093f79b17c04e81fe88b509b1319
|
||||
@@ -0,0 +1,135 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using Unheimlich.Core.Utils;
|
||||
|
||||
namespace Unheimlich.Features.DetectiveBoard.UI
|
||||
{
|
||||
public class PostItScribbleEffect : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private Material _scribbleMaterialTemplate;
|
||||
[SerializeField] private RectTransform _targetRectTransform;
|
||||
|
||||
private Image _scribbleOverlay;
|
||||
private Material _scribbleMaterialInstance;
|
||||
private bool _overlayCreated = false;
|
||||
private bool _isEliminated = false;
|
||||
|
||||
private static readonly int ProgressID = Shader.PropertyToID("_Progress");
|
||||
private int _scribbleTweenId = -1;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (_targetRectTransform == null)
|
||||
_targetRectTransform = GetComponent<RectTransform>();
|
||||
}
|
||||
|
||||
private void CreateScribbleOverlay()
|
||||
{
|
||||
if (_overlayCreated || _scribbleMaterialTemplate == null || _targetRectTransform == null)
|
||||
return;
|
||||
|
||||
GameObject overlayObject = new GameObject("ScribbleOverlay");
|
||||
overlayObject.transform.SetParent(_targetRectTransform, false);
|
||||
|
||||
_scribbleOverlay = overlayObject.AddComponent<Image>();
|
||||
_scribbleOverlay.raycastTarget = false;
|
||||
|
||||
RectTransform overlayRect = overlayObject.GetComponent<RectTransform>();
|
||||
|
||||
overlayRect.anchorMin = Vector2.zero;
|
||||
overlayRect.anchorMax = Vector2.one;
|
||||
overlayRect.sizeDelta = Vector2.zero;
|
||||
overlayRect.anchoredPosition = Vector2.zero;
|
||||
|
||||
_scribbleMaterialInstance = new Material(_scribbleMaterialTemplate);
|
||||
_scribbleMaterialInstance.SetFloat(ProgressID, 0f);
|
||||
_scribbleOverlay.material = _scribbleMaterialInstance;
|
||||
|
||||
_scribbleOverlay.gameObject.SetActive(false);
|
||||
|
||||
_overlayCreated = true;
|
||||
}
|
||||
|
||||
public void SetEliminated(bool eliminated)
|
||||
{
|
||||
if (_isEliminated == eliminated)
|
||||
return;
|
||||
|
||||
_isEliminated = eliminated;
|
||||
|
||||
if (eliminated)
|
||||
{
|
||||
ApplyScribble();
|
||||
}
|
||||
else
|
||||
{
|
||||
RemoveScribble();
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyScribble()
|
||||
{
|
||||
CancelActiveTweens();
|
||||
CreateScribbleOverlay();
|
||||
|
||||
if (_scribbleOverlay == null || _scribbleMaterialInstance == null)
|
||||
return;
|
||||
|
||||
_scribbleOverlay.gameObject.SetActive(true);
|
||||
|
||||
_scribbleTweenId = LeanTween.value(gameObject, 0f, 1f, 0.4f)
|
||||
.setEase(LeanTweenType.easeInOutQuad)
|
||||
.setOnUpdate((float val) => {
|
||||
if (_scribbleMaterialInstance != null)
|
||||
_scribbleMaterialInstance.SetFloat(ProgressID, val);
|
||||
})
|
||||
.setOnComplete(() => {
|
||||
_scribbleTweenId = -1;
|
||||
}).id;
|
||||
}
|
||||
|
||||
private void RemoveScribble()
|
||||
{
|
||||
if (!_overlayCreated || _scribbleOverlay == null)
|
||||
return;
|
||||
|
||||
if (!_scribbleOverlay.gameObject.activeSelf)
|
||||
return;
|
||||
|
||||
CancelActiveTweens();
|
||||
|
||||
if (_scribbleMaterialInstance != null)
|
||||
{
|
||||
float currentProgress = _scribbleMaterialInstance.GetFloat(ProgressID);
|
||||
|
||||
_scribbleTweenId = LeanTween.value(gameObject, currentProgress, 0f, 0.3f)
|
||||
.setEase(LeanTweenType.easeOutQuad)
|
||||
.setOnUpdate((float val) => {
|
||||
if (_scribbleMaterialInstance != null)
|
||||
_scribbleMaterialInstance.SetFloat(ProgressID, val);
|
||||
})
|
||||
.setOnComplete(() => {
|
||||
_scribbleTweenId = -1;
|
||||
if (_scribbleOverlay != null)
|
||||
_scribbleOverlay.gameObject.SetActive(false);
|
||||
}).id;
|
||||
}
|
||||
}
|
||||
|
||||
private void CancelActiveTweens()
|
||||
{
|
||||
TweenHelper.CancelTween(ref _scribbleTweenId);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
CancelActiveTweens();
|
||||
|
||||
if (_scribbleMaterialInstance != null)
|
||||
Destroy(_scribbleMaterialInstance);
|
||||
|
||||
if (_scribbleOverlay != null)
|
||||
Destroy(_scribbleOverlay.gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 009173206d77d184d86915685410c7d8
|
||||
Reference in New Issue
Block a user