clean : deleted all useless scripts

This commit is contained in:
2026-05-26 15:57:53 +01:00
parent a6d3ab2f8a
commit 32172f4437
121 changed files with 48 additions and 8108 deletions
@@ -1,135 +0,0 @@
using UnityEngine;
using Unheimlich.Features.Player;
using Unheimlich.Features.DetectiveBoard.UI;
namespace Unheimlich.NonEuclidean
{
public class DetectiveBoardPortalHider : MonoBehaviour
{
[SerializeField] private float _checkInterval = 0.1f;
[SerializeField] private float _hideDistance = 5f;
[SerializeField] private bool _enableDebugLogs = true;
private Canvas _detectiveBoardCanvas;
private NonEuclideanManager _manager;
private float _checkTimer;
private void Start()
{
_manager = NonEuclideanManager.Instance;
if (DetectiveBoardCanvas.Instance != null)
{
_detectiveBoardCanvas = DetectiveBoardCanvas.Instance.GetComponent<Canvas>();
}
if (_enableDebugLogs)
{
Debug.Log($"[DetectiveBoardPortalHider] Initialized - Manager: {_manager != null}, Canvas: {_detectiveBoardCanvas != null}");
}
}
private void Update()
{
if (_detectiveBoardCanvas == null || _manager == null || PlayerController.Instance == null)
{
if (_enableDebugLogs && Time.frameCount % 300 == 0)
{
Debug.LogWarning($"[DetectiveBoardPortalHider] Missing references - Canvas: {_detectiveBoardCanvas != null}, Manager: {_manager != null}, Player: {PlayerController.Instance != null}");
}
return;
}
_checkTimer += Time.deltaTime;
if (_checkTimer >= _checkInterval)
{
_checkTimer = 0f;
CheckAndToggleCanvas();
}
}
private void CheckAndToggleCanvas()
{
Vector3 playerPos = PlayerController.Instance.transform.position;
NonEuclideanRoom currentRoom = _manager.ActiveRoom;
if (currentRoom == null)
{
if (_enableDebugLogs)
Debug.LogWarning("[DetectiveBoardPortalHider] No active room");
return;
}
NonEuclideanRoom boardRoom = GetDetectiveBoardRoom();
if (boardRoom == null)
{
if (_enableDebugLogs && Time.frameCount % 300 == 0)
Debug.LogWarning("[DetectiveBoardPortalHider] Could not find board room");
_detectiveBoardCanvas.enabled = true;
return;
}
bool isInBoardRoom = (currentRoom == boardRoom);
if (_enableDebugLogs)
Debug.Log($"[DetectiveBoardPortalHider] Current room: {currentRoom.name}, Board room: {boardRoom.name}, In board room: {isInBoardRoom}, Portals count: {currentRoom.Portals.Length}");
foreach (var portal in currentRoom.Portals)
{
float distance = Vector3.Distance(playerPos, portal.transform.position);
bool shouldHide = false;
if (isInBoardRoom)
{
shouldHide = (distance < _hideDistance);
if (_enableDebugLogs && shouldHide)
Debug.Log($"[DetectiveBoardPortalHider] In board room, close to portal '{portal.name}' (distance: {distance:F2}m) - hiding canvas");
}
else if (portal.TargetRoom == boardRoom)
{
shouldHide = (distance < _hideDistance);
if (_enableDebugLogs && shouldHide)
Debug.Log($"[DetectiveBoardPortalHider] Close to portal '{portal.name}' leading to board room (distance: {distance:F2}m) - hiding canvas");
}
if (shouldHide)
{
_detectiveBoardCanvas.enabled = false;
return;
}
}
if (_enableDebugLogs)
Debug.Log("[DetectiveBoardPortalHider] Not close to any relevant portal - enabling canvas");
_detectiveBoardCanvas.enabled = true;
}
private NonEuclideanRoom GetDetectiveBoardRoom()
{
if (_detectiveBoardCanvas == null)
return null;
Transform current = _detectiveBoardCanvas.transform;
while (current != null)
{
NonEuclideanRoom room = current.GetComponent<NonEuclideanRoom>();
if (room != null)
{
if (_enableDebugLogs && Time.frameCount % 300 == 0)
Debug.Log($"[DetectiveBoardPortalHider] Found board room: {room.name}");
return room;
}
current = current.parent;
}
return null;
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 16e1f1371a69f6d4db490054110293b7
@@ -1,38 +0,0 @@
using UnityEngine;
namespace Unheimlich.Core.Audio
{
[CreateAssetMenu(fileName = "AudioEmitterData", menuName = "Unheimlich/Audio/Audio Emitter Data")]
public class AudioEmitterData : ScriptableObject
{
[Header("Emitter Info")]
public AudioEmitterType EmitterType = AudioEmitterType.Generic;
public string DisplayName = "Generic";
[Header("Procedural Audio Settings")]
public float BaseFrequency = 440f;
public float FrequencyVariation = 50f;
public float VolumeMin = 0.3f;
public float VolumeMax = 0.7f;
public float DurationMin = 0.5f;
public float DurationMax = 2f;
[Header("Harmonics (for complex sounds)")]
public float[] HarmonicMultipliers = { 1f };
public float[] HarmonicAmplitudes = { 1f };
[Header("Noise Settings")]
[Range(0f, 1f)]
public float NoiseAmount = 0f;
[Header("Vibration Settings")]
public bool EnableVibration = true;
public float VibrationIntensityMin = 0.1f;
public float VibrationIntensityMax = 0.5f;
public float VibrationFrequency = 10f;
[Header("Distance Settings")]
public float MaxDistance = 10f;
public AnimationCurve DistanceAttenuation = AnimationCurve.EaseInOut(0f, 1f, 1f, 0f);
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: c20752cb7065e844aaaedf606c4df2db
@@ -1,434 +0,0 @@
using System.Collections;
using UnityEngine;
using UnityEngine.InputSystem;
using Unheimlich.Core.Utils;
using Unheimlich.World.CameraSystem;
namespace Unheimlich.Core.Audio
{
public enum AudioEmitterType
{
Generic,
Wall,
Wood,
Metal,
Glass,
Water,
Stone,
Mechanical,
Electrical,
Organic
}
public class AudioListenable : MonoBehaviour
{
// String constants
private const string CONTINUOUS_AUDIO_SOURCE_NAME = "Continuous Audio Source";
// Volume multipliers for concentration system
private const float BASELINE_VOLUME_MULTIPLIER = 0.1f;
private const float FULL_VOLUME_MULTIPLIER = 1.5f;
private const float CONCENTRATION_FADE_DURATION = 1f;
private const float RELEASE_FADE_DURATION = 0.5f;
[Header("Emitter Settings")]
[SerializeField] private AudioEmitterType _emitterType = AudioEmitterType.Generic;
[SerializeField] private bool _useProceduralAudio = true;
[Header("Continuous Emission")]
[SerializeField] private bool _emitContinuously = true;
[SerializeField] private float _continuousBaseVolume = 0.1f;
[SerializeField] private float _hearingRangeMin = 4f;
[SerializeField] private float _hearingRangeMax = 8f;
[Header("Listening Enhancement")]
[SerializeField] private float _reverbWetMix = 0.5f;
[Header("Static Audio")]
[SerializeField] private AudioClip _staticAudioClip;
[SerializeField] private bool _loopStaticAudio = true;
[Header("Procedural Settings")]
[SerializeField] private float _baseFrequency = 440f;
[SerializeField] private float _frequencyVariation = 50f;
[SerializeField] private float _volumeMin = 0.3f;
[SerializeField] private float _volumeMax = 0.7f;
[SerializeField] private float _durationMin = 0.5f;
[SerializeField] private float _durationMax = 2f;
[Header("Distance Attenuation")]
[SerializeField] private float _maxDistance = 10f;
[SerializeField] private AnimationCurve _distanceAttenuation = AnimationCurve.EaseInOut(0f, 1f, 1f, 0f);
[Header("Visual Feedback")]
[SerializeField] private ParticleSystem _emitterParticles;
[Header("Camera Settings")]
[SerializeField] private CameraPanSettings _cameraPanSettings;
private AudioSource _audioSource;
private AudioSource _continuousAudioSource;
private AudioReverbFilter _reverbFilter;
private bool _isCurrentlyListening;
private float _listeningTimer;
private float _nextProceduralEventTime;
private Vector3 _listenerPosition;
private bool _hasBeenTapped;
private AudioClip _continuousClip;
private Coroutine _volumeFadeCoroutine;
private Coroutine _startListeningCoroutine;
private int _concentrationFadeTweenId = -1;
private int _crossfadeTweenId = -1;
/// <summary>
/// Returns true if this emitter has been tapped during the current listening session.
/// Used to prevent multiple taps on the same emitter.
/// </summary>
public bool HasBeenTapped => _hasBeenTapped;
/// <summary>
/// Returns the camera pan settings for this emitter, or null if not set.
/// </summary>
public CameraPanSettings CameraPanSettings => _cameraPanSettings;
private void Awake()
{
_audioSource = GetComponent<AudioSource>();
if (_audioSource == null)
_audioSource = gameObject.AddComponent<AudioSource>();
_audioSource.spatialBlend = 1f;
_audioSource.maxDistance = _maxDistance;
_audioSource.rolloffMode = AudioRolloffMode.Custom;
_audioSource.playOnAwake = false;
_reverbFilter = gameObject.AddComponent<AudioReverbFilter>();
_reverbFilter.reverbPreset = AudioReverbPreset.Off;
_reverbFilter.dryLevel = 0f;
_reverbFilter.room = -10000;
GameObject continuousSourceObject = new GameObject(CONTINUOUS_AUDIO_SOURCE_NAME);
continuousSourceObject.transform.SetParent(transform);
continuousSourceObject.transform.localPosition = Vector3.zero;
_continuousAudioSource = continuousSourceObject.AddComponent<AudioSource>();
_continuousAudioSource.spatialBlend = 1f;
_continuousAudioSource.maxDistance = _hearingRangeMax;
_continuousAudioSource.minDistance = _hearingRangeMin;
_continuousAudioSource.rolloffMode = AudioRolloffMode.Linear;
_continuousAudioSource.playOnAwake = false;
_continuousAudioSource.loop = true;
if (_emitterParticles != null)
{
_emitterParticles.Stop();
var emission = _emitterParticles.emission;
emission.enabled = false;
}
}
private void Start()
{
if (_emitContinuously && _useProceduralAudio)
{
StartContinuousEmission();
}
}
private void StartContinuousEmission()
{
float frequency = _baseFrequency + Random.Range(-_frequencyVariation * 0.5f, _frequencyVariation * 0.5f);
_continuousClip = GenerateProceduralAudioClip(frequency, 10f);
_continuousAudioSource.clip = _continuousClip;
_continuousAudioSource.volume = _continuousBaseVolume * BASELINE_VOLUME_MULTIPLIER;
_continuousAudioSource.loop = true;
_continuousAudioSource.Play();
}
/// <summary>
/// Sets the concentration state and fades the emitter volume accordingly.
/// When concentrating: fades from 30% to 100% over 1 second.
/// When releasing: fades from 100% to 30% over 0.5 seconds.
/// </summary>
public void SetConcentrating(bool concentrating)
{
TweenHelper.CancelTween(ref _concentrationFadeTweenId);
if (_continuousAudioSource == null)
return;
float baseVolume = _continuousBaseVolume;
float fromMultiplier = concentrating ? BASELINE_VOLUME_MULTIPLIER : FULL_VOLUME_MULTIPLIER;
float toMultiplier = concentrating ? FULL_VOLUME_MULTIPLIER : BASELINE_VOLUME_MULTIPLIER;
float duration = concentrating ? CONCENTRATION_FADE_DURATION : RELEASE_FADE_DURATION;
float fromVolume = baseVolume * fromMultiplier;
float toVolume = baseVolume * toMultiplier;
_concentrationFadeTweenId = TweenHelper.FadeVolume(
_continuousAudioSource,
toVolume,
duration);
}
/// <summary>
/// Starts the listening sequence for this emitter.
/// Blocked if the emitter has already been tapped during this session.
/// </summary>
public void StartListening(Vector3 listenerPosition)
{
// Prevent multiple taps on the same emitter
if (_hasBeenTapped)
return;
if (_startListeningCoroutine != null)
StopCoroutine(_startListeningCoroutine);
_startListeningCoroutine = StartCoroutine(StartListeningSequence(listenerPosition));
}
/// <summary>
/// Coroutine that handles the full listening sequence:
/// - Crossfades from continuous to procedural audio
/// - Activates particles
/// - Marks emitter as tapped (prevents re-triggering)
/// - Starts vibration
/// </summary>
private IEnumerator StartListeningSequence(Vector3 listenerPosition)
{
_isCurrentlyListening = true;
_listenerPosition = listenerPosition;
_listeningTimer = 0f;
_nextProceduralEventTime = 0f;
// Mark as tapped to prevent re-triggering
_hasBeenTapped = true;
if (_volumeFadeCoroutine != null)
StopCoroutine(_volumeFadeCoroutine);
TweenHelper.CancelTween(ref _crossfadeTweenId);
float crossfadeDuration = 1f;
if (_continuousAudioSource != null)
{
_crossfadeTweenId = TweenHelper.FadeVolume(
_continuousAudioSource,
0f,
crossfadeDuration,
() =>
{
if (_continuousAudioSource != null)
_continuousAudioSource.Stop();
});
}
if (_emitterParticles != null)
{
var emission = _emitterParticles.emission;
emission.enabled = true;
_emitterParticles.Play();
}
EnableReverb(true);
yield return new WaitForSeconds(crossfadeDuration);
if (_useProceduralAudio)
{
PlayProceduralSound();
}
else if (_staticAudioClip != null)
{
_audioSource.clip = _staticAudioClip;
_audioSource.loop = _loopStaticAudio;
_audioSource.Play();
}
}
/// <summary>
/// Stops the listening mode and resets the emitter state.
/// This is the only place where _hasBeenTapped is reset to false,
/// allowing the emitter to be used again in a future listening session.
/// </summary>
public void StopListening(bool cancelConcentrationFade = true)
{
bool wasBeenTapped = _hasBeenTapped;
_isCurrentlyListening = false;
_hasBeenTapped = false;
if (_volumeFadeCoroutine != null)
StopCoroutine(_volumeFadeCoroutine);
if (_startListeningCoroutine != null)
StopCoroutine(_startListeningCoroutine);
if (cancelConcentrationFade)
TweenHelper.CancelTween(ref _concentrationFadeTweenId);
if (_audioSource != null && _audioSource.isPlaying)
_audioSource.Stop();
if (_emitterParticles != null && wasBeenTapped)
{
_emitterParticles.Stop();
var emission = _emitterParticles.emission;
emission.enabled = false;
}
if (_continuousAudioSource != null && !_continuousAudioSource.isPlaying && gameObject.activeInHierarchy)
{
StartContinuousEmission();
}
EnableReverb(false);
}
private void Update()
{
if (!_isCurrentlyListening)
return;
_listeningTimer += Time.deltaTime;
if (_useProceduralAudio && _listeningTimer >= _nextProceduralEventTime)
{
PlayProceduralSound();
_nextProceduralEventTime = _listeningTimer + Random.Range(_durationMin, _durationMax);
}
}
private void EnableReverb(bool enable)
{
if (_reverbFilter == null)
return;
if (enable)
{
_reverbFilter.reverbPreset = AudioReverbPreset.Room;
_reverbFilter.dryLevel = Mathf.Lerp(0f, -1000f, 1f - _reverbWetMix);
_reverbFilter.room = Mathf.Lerp(-1000f, -100f, _reverbWetMix);
}
else
{
_reverbFilter.reverbPreset = AudioReverbPreset.Off;
_reverbFilter.dryLevel = 0f;
_reverbFilter.room = -10000;
}
}
private void PlayProceduralSound()
{
float distance = Vector3.Distance(_listenerPosition, transform.position);
float distanceFactor = Mathf.Clamp01(1f - (distance / _maxDistance));
float attenuation = _distanceAttenuation.Evaluate(distanceFactor);
float frequency = _baseFrequency + Random.Range(-_frequencyVariation, _frequencyVariation);
float volume = Mathf.Lerp(_volumeMin, _volumeMax, Random.value) * attenuation;
AudioClip proceduralClip = GenerateProceduralAudioClip(frequency, Random.Range(_durationMin, _durationMax));
_audioSource.clip = proceduralClip;
_audioSource.volume = volume;
_audioSource.loop = false;
_audioSource.Play();
}
private AudioClip GenerateProceduralAudioClip(float frequency, float duration)
{
int sampleRate = 44100;
int sampleCount = Mathf.RoundToInt(sampleRate * duration);
AudioClip clip = AudioClip.Create("ProceduralSound", sampleCount, 1, sampleRate, false);
float[] samples = new float[sampleCount];
for (int i = 0; i < sampleCount; i++)
{
float t = (float)i / sampleRate;
float envelope = Mathf.Sin(Mathf.PI * t / duration);
samples[i] = GenerateSampleForType(frequency, t, _emitterType) * envelope;
}
clip.SetData(samples, 0);
return clip;
}
private float GenerateSampleForType(float frequency, float time, AudioEmitterType type)
{
switch (type)
{
case AudioEmitterType.Metal:
return Mathf.Sin(2f * Mathf.PI * frequency * time) * 0.5f +
Mathf.Sin(2f * Mathf.PI * frequency * 2.1f * time) * 0.3f +
Mathf.Sin(2f * Mathf.PI * frequency * 3.8f * time) * 0.2f;
case AudioEmitterType.Wood:
return Mathf.Sin(2f * Mathf.PI * frequency * time) * 0.6f +
(Random.value - 0.5f) * 0.1f;
case AudioEmitterType.Glass:
return Mathf.Sin(2f * Mathf.PI * frequency * 1.5f * time) * 0.4f +
Mathf.Sin(2f * Mathf.PI * frequency * 2.7f * time) * 0.3f +
Mathf.Sin(2f * Mathf.PI * frequency * 4.2f * time) * 0.3f;
case AudioEmitterType.Stone:
return Mathf.Sin(2f * Mathf.PI * frequency * 0.8f * time) * 0.5f +
(Random.value - 0.5f) * 0.3f;
case AudioEmitterType.Water:
return (Random.value - 0.5f) * 0.5f +
Mathf.Sin(2f * Mathf.PI * frequency * 0.5f * time) * 0.3f;
case AudioEmitterType.Mechanical:
return Mathf.Sin(2f * Mathf.PI * frequency * time) * 0.4f +
Mathf.Sign(Mathf.Sin(2f * Mathf.PI * frequency * 0.3f * time)) * 0.3f;
case AudioEmitterType.Electrical:
return (Random.value - 0.5f) * 0.6f +
Mathf.Sin(2f * Mathf.PI * frequency * 50f * time) * 0.2f;
case AudioEmitterType.Organic:
return Mathf.Sin(2f * Mathf.PI * frequency * 0.7f * time) * 0.5f +
Mathf.Sin(2f * Mathf.PI * frequency * 1.3f * time) * 0.3f +
(Random.value - 0.5f) * 0.2f;
case AudioEmitterType.Wall:
return Mathf.Sin(2f * Mathf.PI * frequency * 0.5f * time) * 0.3f +
(Random.value - 0.5f) * 0.1f;
default:
return Mathf.Sin(2f * Mathf.PI * frequency * time);
}
}
private void OnDisable()
{
StopListening();
}
private void OnEnable()
{
if (_continuousAudioSource != null && !_continuousAudioSource.isPlaying && _emitContinuously)
{
StartContinuousEmission();
}
}
private void OnDrawGizmosSelected()
{
Gizmos.color = Color.cyan;
Gizmos.DrawWireSphere(transform.position, _maxDistance);
if (_isCurrentlyListening)
{
Gizmos.color = Color.green;
Gizmos.DrawLine(transform.position, _listenerPosition);
}
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: e15490f7adacb594cb114f535e71d6b6
@@ -1,67 +0,0 @@
using UnityEngine;
using Unheimlich.Core;
using Unheimlich.Core.Utils;
using InputManager = Unheimlich.Core.Input.InputManager;
namespace Unheimlich.Core.Interaction
{
public class InteractionHandler : Singleton<InteractionHandler>
{
[Header("Settings")]
[SerializeField] private float _interactionRange = 1f;
[SerializeField] private LayerMask _interactableLayers;
[SerializeField] private float _raycastInterval = 0.1f;
private InteractableBase _currentInteractable;
private Camera _playerCamera;
private float _raycastTimer;
protected override void OnAwake()
{
_playerCamera = GetComponent<Camera>();
}
private bool _didHit;
private void Update()
{
_didHit = RaycastHelper.ThrottledRaycastComponent(
ref _raycastTimer,
_raycastInterval,
_playerCamera,
_interactionRange,
_interactableLayers,
out InteractableBase interactable);
if (_didHit)
_currentInteractable = interactable;
else if (!_didHit && _raycastTimer <= 0f)
_currentInteractable = null;
HandleInteractionInput();
}
private void HandleInteractionInput()
{
var interactAction = InputManager.GetInteractAction();
if (interactAction != null && interactAction.WasPressedThisFrame())
{
if (_currentInteractable != null)
_currentInteractable.Interact();
}
}
private void OnDrawGizmosSelected()
{
if (_playerCamera != null)
{
Gizmos.color = _currentInteractable != null ? Color.green : Color.red;
Vector3 viewportCenter = new Vector3(0.5f, 0.5f, 0f);
Ray ray = _playerCamera.ViewportPointToRay(viewportCenter);
Gizmos.DrawRay(ray.origin, ray.direction * _interactionRange);
}
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: d96ceb0ee513b1e4197e55e9b926269b
@@ -1,17 +0,0 @@
using UnityEngine;
namespace Unheimlich.Core.Interaction
{
/// <summary>
/// Base class for all interactable objects in the game.
/// Derived classes must implement Interact() to define interaction behavior.
/// </summary>
public abstract class InteractableBase : MonoBehaviour
{
[Header("Interaction Settings")]
[Tooltip("Text displayed when the player looks at the object")]
public string InteractionText = "Interact";
public abstract void Interact();
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: c2bd6cb9667f8674294172efbdf8afd3
@@ -1,8 +1,6 @@
using UnityEngine;
using System.Collections.Generic;
using Unheimlich.Features.Dialogue.Data;
using Unheimlich.Features.DetectiveBoard.Data;
using Unheimlich.Features.DetectiveBoard;
using Unheimlich.Features.Player;
using Unheimlich.Features.Dialogue;
using Unheimlich.Features.Dialogue.UI;
@@ -29,10 +27,6 @@ namespace Unheimlich.Core.Interaction
[Header("Prefab")]
[SerializeField] private ChoiceFocusHandler _wordPrefab;
[Header("Detective Board Integration")]
[Tooltip("Clue to unlock when words are fully merged (optional)")]
[SerializeField] private ClueData _clueToUnlock;
private Transform _playerTransform;
private Camera _mainCamera;
private List<ChoiceFocusHandler> _spawnedWords = new List<ChoiceFocusHandler>();
@@ -327,21 +321,12 @@ namespace Unheimlich.Core.Interaction
.setOnComplete(() =>
{
_hasCompletedMergeOnce = true;
UnlockClue();
DespawnWords();
});
}
}
}
private void UnlockClue()
{
if (_clueToUnlock != null && DetectiveBoardManager.Instance != null)
{
DetectiveBoardManager.Instance.UnlockClue(_clueToUnlock);
Debug.Log($"[ObjectDescription] Unlocked clue: {_clueToUnlock.name} from description merge");
}
}
private void UnmergeWords()
{
@@ -1,173 +0,0 @@
using System.Collections;
using UnityEngine;
using UnityEngine.InputSystem;
using Unheimlich.Core;
using Unheimlich.Features.Items;
using Unheimlich.Features.Player;
using InputManager = Unheimlich.Core.Input.InputManager;
namespace Unheimlich.Core.Interaction
{
/// <summary>
/// Handles player item inspection mode.
/// </summary>
public class PlayerInspectionHandler : Singleton<PlayerInspectionHandler>
{
[Header("Components")]
[SerializeField] private Transform _inspectionHoldPoint;
[Header("State")]
public bool IsInspecting { get; private set; } = false;
private InspectableItem _currentInspectedItem;
private Coroutine _interactionListener;
private bool _isUsingGamepad = false;
public void ToggleInspection(InspectableItem item)
{
if (!IsInspecting)
{
StartInspection(item);
}
else
{
if (_interactionListener != null)
StopInspection();
}
}
private void StartInspection(InspectableItem item)
{
IsInspecting = true;
_currentInspectedItem = item;
// Set Inspection context (handles cursor automatically)
InputManager.SetContext(InputManager.InputContext.Inspection);
// Disable OctoPlayer to prevent movement and look
if (PlayerController.Instance?.Movement != null)
{
PlayerController.Instance.Movement.enabled = false;
}
item.PrepareForInspection(_inspectionHoldPoint);
if (_interactionListener == null)
_interactionListener = StartCoroutine(ListenForInteraction());
}
private void StopInspection()
{
if (_interactionListener != null)
{
StopCoroutine(_interactionListener);
_interactionListener = null;
}
StopInspectionInternal();
}
private void StopInspectionInternal()
{
if (_currentInspectedItem != null)
{
_currentInspectedItem.Release();
_currentInspectedItem = null;
StartCoroutine(DelayedToggleInput(true));
}
IsInspecting = false;
}
private IEnumerator DelayedToggleInput(bool enable)
{
yield return new WaitForEndOfFrame();
// Re-enable OctoPlayer
if (PlayerController.Instance?.Movement != null)
{
PlayerController.Instance.Movement.enabled = true;
}
// Restore Normal context (handles cursor automatically)
InputManager.SetContext(InputManager.InputContext.Normal);
yield break;
}
private IEnumerator ListenForInteraction()
{
yield return null;
while (IsInspecting)
{
var interactAction = InputManager.GetInteractAction();
if (interactAction != null && interactAction.WasPressedThisFrame())
{
StopInspectionInternal();
yield break;
}
yield return null;
}
_interactionListener = null;
}
private void Update()
{
if (IsInspecting && _currentInspectedItem != null)
{
HandleObjectRotation();
}
}
private void HandleObjectRotation()
{
if (_currentInspectedItem == null) return;
Vector2 lookInput = Vector2.zero;
bool hasInput = false;
// Read input directly (not cached)
if (Gamepad.current != null)
{
Vector2 stickInput = Gamepad.current.rightStick.ReadValue();
if (stickInput.sqrMagnitude > 0.01f)
{
lookInput = stickInput;
hasInput = true;
_isUsingGamepad = true;
}
}
if (!hasInput && Mouse.current != null)
{
Vector2 mouseDelta = Mouse.current.delta.ReadValue();
if (mouseDelta.sqrMagnitude > 0.01f)
{
lookInput = mouseDelta;
hasInput = true;
_isUsingGamepad = false;
}
}
// Only rotate if there's actual input
if (hasInput)
{
// Normalize to get constant rotation speed regardless of input magnitude
Vector2 direction = lookInput.normalized;
float rotationMultiplier = _isUsingGamepad ? Time.deltaTime : 0.02f;
float rotX = direction.y * _currentInspectedItem.RotationSpeed * rotationMultiplier;
float rotY = direction.x * _currentInspectedItem.RotationSpeed * rotationMultiplier;
_currentInspectedItem.transform.Rotate(transform.up, -rotY, Space.World);
_currentInspectedItem.transform.Rotate(transform.right, rotX, Space.World);
}
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 464960797e705e84a9eb87ecce4e9311
@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 494acbc99a38a1f4dbbe46579e1a4d2c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,129 +0,0 @@
#if UNITY_EDITOR || DEVELOPMENT_BUILD
using UnityEngine;
using Unheimlich.Features.Player;
namespace Unheimlich.Dev
{
public class AudioSourceDemo : MonoBehaviour
{
[Header("Demo Settings")]
[SerializeField] private AudioClip _clip;
[SerializeField] private bool _playOnStart = true;
[SerializeField] private bool _loop = true;
[SerializeField] private float _volume = 0.5f;
[SerializeField] private float _sourceMinDistance = 1f;
[SerializeField] private float _sourceMaxDistance = 20;
[Header("Distance Culling")]
[SerializeField] private bool _enableDistanceCulling = true;
[SerializeField] private float _cullingDistance = 50f;
[SerializeField] private float _fadeStartDistance = 40f;
private AudioSource _source;
private bool _hasStarted = false;
private float _savedTime = 0f;
private bool _wasPlaying = false;
private bool _isCulled = false;
private Transform _player;
public float OriginalVolume => _volume;
public float GetDistanceFadeMultiplier(Vector3 listenerPosition)
{
if (!_enableDistanceCulling)
return 1f;
float distance = Vector3.Distance(transform.position, listenerPosition);
if (distance > _cullingDistance)
return 0f;
if (distance > _fadeStartDistance)
{
float fadeRange = _cullingDistance - _fadeStartDistance;
float fadeAmount = (distance - _fadeStartDistance) / fadeRange;
return 1f - Mathf.Clamp01(fadeAmount);
}
return 1f;
}
private void Awake()
{
_source = GetComponent<AudioSource>();
if (_source == null)
_source = gameObject.AddComponent<AudioSource>();
_source.clip = _clip;
_source.loop = _loop;
_source.volume = _volume;
_source.playOnAwake = false;
_source.spatialBlend = 1f;
_source.rolloffMode = AudioRolloffMode.Logarithmic;
_source.minDistance = _sourceMinDistance;
_source.maxDistance = _sourceMaxDistance;
}
private void OnEnable()
{
if (_wasPlaying && _savedTime > 0f)
{
_source.time = _savedTime;
_source.Play();
}
}
private void Start()
{
if (_playOnStart && _clip != null && !_hasStarted && !_source.isPlaying)
{
_source.Play();
_hasStarted = true;
}
PlayerController playerObj = PlayerController.Instance;
if (playerObj != null)
_player = playerObj.transform;
}
private void Update()
{
if (_enableDistanceCulling && _player != null && _source != null)
{
float distance = Vector3.Distance(transform.position, _player.position);
if (distance > _cullingDistance)
{
if (!_isCulled)
{
_source.volume = 0f;
_isCulled = true;
}
}
else if (distance > _fadeStartDistance)
{
float fadeRange = _cullingDistance - _fadeStartDistance;
float fadeAmount = (distance - _fadeStartDistance) / fadeRange;
fadeAmount = Mathf.Clamp01(fadeAmount);
_source.volume = _volume * (1f - fadeAmount);
_isCulled = false;
}
else
{
if (_isCulled || _source.volume != _volume)
{
_source.volume = _volume;
_isCulled = false;
}
}
}
}
private void OnDisable()
{
_wasPlaying = _source.isPlaying;
_savedTime = _source.time;
}
}
}
#endif
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: f399329860533c14fba15187d7e340db
@@ -1,124 +0,0 @@
#if UNITY_EDITOR || DEVELOPMENT_BUILD
using UnityEngine;
using Unheimlich.Features.Player;
using Unheimlich.Features.Diapason;
using Unheimlich.Core.Audio;
namespace Unheimlich.Dev
{
public class DiapasonDebugger : MonoBehaviour
{
[Header("Debug Display")]
[SerializeField] private bool _showDebugInfo = true;
[SerializeField] private bool _showGizmos = true;
[SerializeField] private Color _listeningRangeColor = Color.cyan;
[SerializeField] private Color _activeTargetColor = Color.green;
private DiapasonListeningHandlerV2 _listeningHandler;
private AudioListenable[] _allListenables;
private void Awake()
{
_listeningHandler = DiapasonListeningHandlerV2.Instance;
}
private void Start()
{
RefreshListenablesList();
}
private void OnGUI()
{
if (!_showDebugInfo || _listeningHandler == null)
return;
GUILayout.BeginArea(new Rect(10, 10, 300, 200));
GUILayout.Box("Diapason System Debug");
GUILayout.Label($"Is Listening: {_listeningHandler.IsListening}");
GUILayout.Label($"Is Concentrating: {_listeningHandler.IsConcentrating}");
if (_allListenables != null)
GUILayout.Label($"Total Listenables: {_allListenables.Length}");
if (GUILayout.Button("Refresh Listenables List"))
RefreshListenablesList();
if (GUILayout.Button("Toggle Debug Gizmos"))
_showGizmos = !_showGizmos;
GUILayout.EndArea();
}
private void RefreshListenablesList()
{
_allListenables = FindObjectsByType<AudioListenable>(FindObjectsSortMode.None);
if (!_showDebugInfo)
return;
Debug.Log($"Found {_allListenables.Length} AudioListenable objects in scene");
}
private void OnDrawGizmos()
{
if (!_showGizmos)
return;
if (_allListenables == null)
_allListenables = FindObjectsByType<AudioListenable>(FindObjectsSortMode.None);
foreach (AudioListenable listenable in _allListenables)
{
if (listenable == null)
continue;
Gizmos.color = _listeningRangeColor;
Gizmos.DrawIcon(listenable.transform.position, "sv_icon_dot3_pix16_gizmo", true);
Gizmos.color = new Color(_listeningRangeColor.r, _listeningRangeColor.g, _listeningRangeColor.b, 0.1f);
Gizmos.DrawSphere(listenable.transform.position, 0.2f);
}
if (_listeningHandler != null && _listeningHandler.IsListening)
{
Gizmos.color = _activeTargetColor;
}
}
[ContextMenu("List All Listenables")]
private void ListAllListenables()
{
RefreshListenablesList();
Debug.Log("=== Audio Listenables in Scene ===");
foreach (AudioListenable listenable in _allListenables)
{
if (listenable != null)
Debug.Log($"- {listenable.gameObject.name} (Type: {listenable.GetType().Name})");
}
}
[ContextMenu("Enable All Listenables")]
private void EnableAllListenables()
{
RefreshListenablesList();
foreach (AudioListenable listenable in _allListenables)
{
if (listenable != null)
listenable.enabled = true;
}
Debug.Log("Enabled all AudioListenable components");
}
[ContextMenu("Disable All Listenables")]
private void DisableAllListenables()
{
RefreshListenablesList();
foreach (AudioListenable listenable in _allListenables)
{
if (listenable != null)
listenable.enabled = false;
}
Debug.Log("Disabled all AudioListenable components");
}
}
}
#endif
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 7093688fd39800645bbe00b6d9a3f27b
@@ -1,183 +0,0 @@
#if UNITY_EDITOR || DEVELOPMENT_BUILD
using UnityEngine;
using Unheimlich.Features.OctoArms;
namespace Unheimlich.Dev
{
[ExecuteInEditMode]
public class DiapasonPoseTweaker : MonoBehaviour
{
[Header("References")]
[SerializeField] private OctoIkArmsImproved _armsSystem;
// [SerializeField] private int _armIndex = 1;
[Header("Quick Presets")]
[SerializeField] private bool _applyPreset1 = false;
[SerializeField] private bool _applyPreset2 = false;
[SerializeField] private bool _applyPreset3 = false;
[Header("Live Tweaking (Runtime Only)")]
[SerializeField] private Vector3 _shoulderRotation = new Vector3(20, 0, -10);
[SerializeField] private Vector3 _elbowRotation = new Vector3(0, 0, 0);
[SerializeField] private Vector3 _wristRotation = new Vector3(0, 0, 90);
[Header("Preview")]
[SerializeField] private bool _previewPose = false;
private ArmEquipment _equipment;
private bool _wasPreviewActive = false;
void Update()
{
if (!Application.isPlaying)
return;
if (_armsSystem == null)
return;
if (_equipment == null)
{
var armRigField = typeof(OctoIkArmsImproved).GetField("_rightArm",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
if (armRigField != null)
{
var armRig = armRigField.GetValue(_armsSystem);
var equipmentField = armRig.GetType().GetField("Equipment");
if (equipmentField != null)
{
_equipment = (ArmEquipment)equipmentField.GetValue(armRig);
}
}
}
if (_equipment == null)
return;
if (_previewPose && _equipment.State == ArmState.Environmental)
{
ApplyRotations();
_wasPreviewActive = true;
}
else if (_wasPreviewActive && !_previewPose)
{
ResetRotations();
_wasPreviewActive = false;
}
if (_equipment.State == ArmState.Equipped)
{
_equipment.ShoulderRotationOffset = _shoulderRotation;
_equipment.ElbowRotationOffset = _elbowRotation;
_equipment.WristRotationOffset = _wristRotation;
}
}
void OnValidate()
{
if (_applyPreset1)
{
_applyPreset1 = false;
ApplyPreset1();
}
if (_applyPreset2)
{
_applyPreset2 = false;
ApplyPreset2();
}
if (_applyPreset3)
{
_applyPreset3 = false;
ApplyPreset3();
}
}
private void ApplyPreset1()
{
_shoulderRotation = new Vector3(30, 0, -15);
_elbowRotation = new Vector3(0, 10, 0);
_wristRotation = new Vector3(0, 0, 90);
Debug.Log("[DiapasonPose] Applied Preset 1: Close to body, slight inward");
}
private void ApplyPreset2()
{
_shoulderRotation = new Vector3(20, -10, -20);
_elbowRotation = new Vector3(0, 0, 0);
_wristRotation = new Vector3(0, -15, 90);
Debug.Log("[DiapasonPose] Applied Preset 2: Further from left hand, angled");
}
private void ApplyPreset3()
{
_shoulderRotation = new Vector3(15, 0, -25);
_elbowRotation = new Vector3(0, 15, 0);
_wristRotation = new Vector3(0, 0, 85);
Debug.Log("[DiapasonPose] Applied Preset 3: Natural grip, centered");
}
private void ApplyRotations()
{
if (_equipment.ShoulderBone != null)
{
_equipment.ShoulderBone.localRotation = _equipment.OriginalShoulderRotation * Quaternion.Euler(_shoulderRotation);
}
if (_equipment.ElbowBone != null)
{
_equipment.ElbowBone.localRotation = _equipment.OriginalElbowRotation * Quaternion.Euler(_elbowRotation);
}
if (_equipment.WristBone != null)
{
_equipment.WristBone.localRotation = _equipment.OriginalWristRotation * Quaternion.Euler(_wristRotation);
}
}
private void ResetRotations()
{
if (_equipment.ShoulderBone != null)
{
_equipment.ShoulderBone.localRotation = _equipment.OriginalShoulderRotation;
}
if (_equipment.ElbowBone != null)
{
_equipment.ElbowBone.localRotation = _equipment.OriginalElbowRotation;
}
if (_equipment.WristBone != null)
{
_equipment.WristBone.localRotation = _equipment.OriginalWristRotation;
}
}
void OnGUI()
{
if (!Application.isPlaying)
return;
GUILayout.BeginArea(new Rect(10, 150, 350, 250));
GUILayout.Label("=== Diapason Pose Tweaker ===");
GUILayout.Label($"Shoulder: {_shoulderRotation}");
GUILayout.Label($"Elbow: {_elbowRotation}");
GUILayout.Label($"Wrist: {_wristRotation}");
GUILayout.Space(10);
GUILayout.Label("Tips:");
GUILayout.Label("• Shoulder X: + = down, - = up");
GUILayout.Label("• Shoulder Y: + = inward, - = outward");
GUILayout.Label("• Shoulder Z: + = twist out, - = twist in");
GUILayout.Label("• Elbow Y: + = bend out, - = bend in");
GUILayout.Label("• Wrist Z: rotation du diapason");
GUILayout.EndArea();
}
}
}
#endif
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 8d1a1f500877b404fa6d23dff3bfa8c8
@@ -1,49 +0,0 @@
#if UNITY_EDITOR || DEVELOPMENT_BUILD
using UnityEngine;
using UnityEngine.InputSystem;
using Unheimlich.Features.Diapason;
namespace Unheimlich.Dev
{
public class DiapasonEquipmentTester : MonoBehaviour
{
[Header("References")]
[SerializeField] private DiapasonEquipmentManager _equipmentManager;
[Header("Input")]
[SerializeField] private Key _equipKey = Key.E;
[SerializeField] private Key _unequipKey = Key.R;
void Update()
{
if (_equipmentManager == null)
return;
if (Keyboard.current[_equipKey].wasPressedThisFrame)
{
_equipmentManager.EquipDiapason();
}
if (Keyboard.current[_unequipKey].wasPressedThisFrame)
{
_equipmentManager.UnequipDiapason();
}
}
void OnGUI()
{
GUILayout.BeginArea(new Rect(10, 10, 350, 150));
GUILayout.Label("=== Diapason Equipment System ===");
GUILayout.Label($"Press {_equipKey} to Equip Diapason");
GUILayout.Label($"Press {_unequipKey} to Unequip Diapason");
GUILayout.Label("");
GUILayout.Label("When equipped:");
GUILayout.Label("• Move LEFT hand to touch AudioListenable");
GUILayout.Label("• LEFT CLICK to tap and activate");
GUILayout.EndArea();
}
}
}
#endif
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 9279e0fb73118d14ba6f2cf05170a660
@@ -1,106 +0,0 @@
using UnityEngine;
using UnityEditor;
using Unheimlich.Core.Audio;
namespace Unheimlich.Editor
{
[CustomEditor(typeof(AudioListenable))]
public class AudioListenableEditor : UnityEditor.Editor
{
public override void OnInspectorGUI()
{
DrawDefaultInspector();
AudioListenable listenable = (AudioListenable)target;
EditorGUILayout.Space(10);
EditorGUILayout.LabelField("Quick Setup", EditorStyles.boldLabel);
if (GUILayout.Button("Setup as Wall"))
SetupAsType(listenable, AudioEmitterType.Wall, 200f, 20f, true);
if (GUILayout.Button("Setup as Metal Object"))
SetupAsType(listenable, AudioEmitterType.Metal, 800f, 100f, false);
if (GUILayout.Button("Setup as Wood Object"))
SetupAsType(listenable, AudioEmitterType.Wood, 300f, 50f, false);
if (GUILayout.Button("Setup as Glass"))
SetupAsType(listenable, AudioEmitterType.Glass, 1200f, 150f, false);
if (GUILayout.Button("Setup as Mechanical"))
SetupAsType(listenable, AudioEmitterType.Mechanical, 500f, 80f, false);
EditorGUILayout.Space(5);
if (GUILayout.Button("Test Sound (Play Mode Only)", GUILayout.Height(30)))
{
if (Application.isPlaying)
{
listenable.StartListening(Camera.main != null ? Camera.main.transform.position : Vector3.zero);
}
else
{
EditorGUILayout.HelpBox("Enter Play Mode to test sounds", MessageType.Info);
}
}
}
private void SetupAsType(AudioListenable listenable, AudioEmitterType type, float baseFreq, float freqVar, bool isGeneric)
{
SerializedObject so = new SerializedObject(listenable);
so.FindProperty("_emitterType").enumValueIndex = (int)type;
so.FindProperty("_useProceduralAudio").boolValue = true;
so.FindProperty("_baseFrequency").floatValue = baseFreq;
so.FindProperty("_frequencyVariation").floatValue = freqVar;
if (isGeneric)
{
so.FindProperty("_volumeMin").floatValue = 0.2f;
so.FindProperty("_volumeMax").floatValue = 0.4f;
}
so.ApplyModifiedProperties();
EditorUtility.SetDirty(listenable);
}
}
[CanEditMultipleObjects]
[CustomEditor(typeof(Transform))]
public class TransformEditorExtension : UnityEditor.Editor
{
private UnityEditor.Editor defaultEditor;
private void OnEnable()
{
defaultEditor = CreateEditor(targets, System.Type.GetType("UnityEditor.TransformInspector, UnityEditor"));
}
private void OnDisable()
{
if (defaultEditor != null)
DestroyImmediate(defaultEditor);
}
public override void OnInspectorGUI()
{
if (defaultEditor != null)
defaultEditor.OnInspectorGUI();
EditorGUILayout.Space(10);
if (targets.Length == 1)
{
Transform t = (Transform)target;
if (t.GetComponent<AudioListenable>() == null)
{
if (GUILayout.Button("Add Audio Listenable", GUILayout.Height(25)))
{
Undo.AddComponent<AudioListenable>(t.gameObject);
}
}
}
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: b606ceac9567f9746b716e63b0e644ee
@@ -1,63 +0,0 @@
using UnityEngine;
namespace Unheimlich.Audio
{
public class AudioSourceInspector : MonoBehaviour
{
[Header("Debug Settings")]
[SerializeField] private float _logInterval = 1f;
private AudioSource _source;
private float _lastLogTime;
private void Awake()
{
_source = GetComponent<AudioSource>();
}
private void Update()
{
if (_source == null) return;
if (Time.time - _lastLogTime >= _logInterval)
{
LogAudioSourceState();
_lastLogTime = Time.time;
}
}
private void OnEnable()
{
Debug.Log($"<color=cyan>[AudioInspector] OnEnable called on {gameObject.name}</color>");
if (_source != null)
LogAudioSourceState();
}
private void OnDisable()
{
Debug.Log($"<color=magenta>[AudioInspector] OnDisable called on {gameObject.name}</color>");
if (_source != null)
LogAudioSourceState();
}
private void LogAudioSourceState()
{
if (_source == null)
{
Debug.LogWarning($"[AudioInspector] No AudioSource on {gameObject.name}");
return;
}
string clipName = _source.clip != null ? _source.clip.name : "NULL";
Debug.Log($"<color=yellow>[AudioInspector] {gameObject.name} | " +
$"Clip: {clipName} | " +
$"Time: {_source.time:F2}s | " +
$"isPlaying: {_source.isPlaying} | " +
$"enabled: {_source.enabled} | " +
$"volume: {_source.volume:F2} | " +
$"mute: {_source.mute} | " +
$"loop: {_source.loop} | " +
$"Active: {gameObject.activeInHierarchy}</color>");
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 65fea0a39ddb811419ec546ea98e5917
@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 3e30b918248fbaa45bd741cc5a8bbeac
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 41ce6b2bff04d1e4d9eb45ac2c7dd0d9
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,30 +0,0 @@
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;
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 166ae120db91b064c9b98086267b9783
@@ -1,75 +0,0 @@
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);
}
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: f2344709e137fdf46aaa5445a330db2f
@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: a7317abe29dcd654f99d8d2ad8e1d179
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,39 +0,0 @@
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!");
}
}
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 184cbe5b79ee0a84ba29da016ec76159
@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 81f81f88e30714043a121630f7bf7cb9
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,40 +0,0 @@
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();
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: fcedba870c1ea9642a272a53148bb933
@@ -1,60 +0,0 @@
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();
}
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: cca584f48d1f0ab429416470fd01d543
@@ -1,98 +0,0 @@
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));
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 61b90106a531ea840b670f368e2c8aa9
@@ -1,219 +0,0 @@
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);
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 9f9f72364623a2a4bb50f02537fd2e72
@@ -1,191 +0,0 @@
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;
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 4b187cdccaa498441a752e8f49ff4290
@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 466fa804543e82c4483a22466a9bb484
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,122 +0,0 @@
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);
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: dc7ce79e8bf16554fae5362980fec5b7
@@ -1,252 +0,0 @@
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);
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: ace0b6173dad68f47a97505680f54d4e
@@ -1,170 +0,0 @@
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);
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: d38fc1a64b1949443b49ec6506174085
@@ -1,90 +0,0 @@
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;
}
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 8d829d9811842b241b980bd64ab73bff
@@ -1,422 +0,0 @@
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);
}
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: edfcd01879d2cb34495302ef9882965b
@@ -1,430 +0,0 @@
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;
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 17bf00a333a0bc9409c21009e3ac6aea
@@ -1,132 +0,0 @@
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;
}
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: a398093f79b17c04e81fe88b509b1319
@@ -1,135 +0,0 @@
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);
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 009173206d77d184d86915685410c7d8
@@ -1,7 +1,6 @@
using System.Collections.Generic;
using UnityEngine;
using OctoCore.Localization;
using Unheimlich.Features.DetectiveBoard.Data;
namespace Unheimlich.Features.Dialogue.Data
{
@@ -20,9 +19,6 @@ namespace Unheimlich.Features.Dialogue.Data
[Tooltip("For end nodes (no choices), delay in seconds before automatic close. 0 = requires player interaction to close")]
public float AutoCloseDelay = 3f;
[Header("Detective Board Integration")]
[Tooltip("Clue to unlock when this node is reached. Useful for end nodes to unlock evidence after a conversation")]
public ClueData ClueToUnlock;
public string GetLocalizedDialogueText()
{
@@ -2,8 +2,6 @@ using UnityEngine;
using System;
using Unheimlich.Core;
using Unheimlich.Features.Dialogue.Data;
using Unheimlich.Features.DetectiveBoard.Data;
using Unheimlich.Features.DetectiveBoard;
using Unheimlich.Features.Player;
using Unheimlich.Features.Dialogue.UI;
using Unheimlich.Features.Dialogue;
@@ -194,17 +192,6 @@ namespace Unheimlich.Features.Dialogue
UnlockFocus();
IsInDialogue = true;
if (_currentNode.ClueToUnlock != null)
{
if (DetectiveBoardManager.Instance != null)
{
DetectiveBoardManager.Instance.UnlockClue(_currentNode.ClueToUnlock);
}
else
{
Debug.LogWarning($"[DialogueManager] Cannot unlock clue '{_currentNode.ClueToUnlock.ClueID}': DetectiveBoardManager not found");
}
}
if(_currentChoiceAnchor == null)
{
@@ -1,142 +0,0 @@
using UnityEngine;
using Unheimlich.Core.Interaction;
using Unheimlich.Features.Dialogue.Data;
using Unheimlich.Features.Dialogue;
using Unheimlich.Features.Player;
using Unheimlich.Features.Dialogue.UI;
namespace Unheimlich.Features.Dialogue
{
/// <summary>
/// NPC interactable that starts dialogue conversations.
/// Rotates to face player during dialogue and pauses if player moves too far away.
/// </summary>
public class NPCDialogueTrigger : InteractableBase
{
[Header("Dialogue Reference")]
[Tooltip("DialogueConversation to use for this NPC")]
public DialogueConversation Conversation;
[Header("Trigger Settings")]
[Tooltip("Can the dialogue be retriggered after completion")]
[SerializeField] private bool _isRetriggerable = true;
[Header("Floating Word Setting")]
[SerializeField] private Transform _floatingWordAnchor;
[Header("Animation Settings")]
[SerializeField] private DialogueAnimationSettings _animationSettings;
[Header("UI Reference")]
[Tooltip("Local DialogueUI for this NPC. Uses DialogueManager's UI if null")]
[SerializeField] private DialogueUI _localDialogueUI;
[Header("Distance Settings")]
[Tooltip("Maximum distance before dialogue pauses")]
[SerializeField] private float _maxDialogueDistance = 5f;
[Header("Blur Effect")]
[Tooltip("Enable blur effect when dialogue starts")]
[SerializeField] private bool _enableBlurEffect = false;
public Transform FloatingWordAnchor => _floatingWordAnchor;
private bool _isDialogueActive = false;
private DialogueStateHandler _dialogueState;
private void Awake()
{
_dialogueState = new DialogueStateHandler(this);
}
private void Start()
{
InteractionText = "Talk";
}
private void Update()
{
if (_isDialogueActive && PlayerController.Instance != null)
{
float distance = Vector3.Distance(transform.position, PlayerController.Instance.transform.position);
if (distance > _maxDialogueDistance && DialogueManager.Instance != null && DialogueManager.Instance.IsDialogueActive)
{
if ((Object)DialogueManager.Instance.CurrentOwner == this)
{
_dialogueState.PauseDialogue();
_isDialogueActive = false;
}
}
else
LookAtPlayer();
}
}
public override void Interact()
{
if (DialogueManager.Instance == null)
return;
if (_dialogueState.IsCompleted && !_isRetriggerable)
return;
if (_dialogueState.HasPausedDialogue)
{
_dialogueState.ResumeDialogue();
DialogueManager.Instance.OnDialogueCompleted += OnDialogueCompleted;
_isDialogueActive = true;
}
else
{
if (Conversation == null)
{
Debug.LogWarning($"NPC {gameObject.name} has no conversation configured!");
return;
}
DialogueManager.Instance.StartConversation(
Conversation,
_floatingWordAnchor,
_animationSettings,
_localDialogueUI,
this,
_enableBlurEffect
);
DialogueManager.Instance.OnDialogueCompleted += OnDialogueCompleted;
_isDialogueActive = true;
}
}
private void OnDialogueCompleted(object owner)
{
if ((Object)owner == this)
{
_dialogueState.MarkAsCompleted();
if (_isRetriggerable)
_dialogueState.AllowRetrigger();
_isDialogueActive = false;
}
if (DialogueManager.Instance != null)
DialogueManager.Instance.OnDialogueCompleted -= OnDialogueCompleted;
}
private void OnDestroy()
{
if (DialogueManager.Instance != null)
DialogueManager.Instance.OnDialogueCompleted -= OnDialogueCompleted;
}
public void LookAtPlayer()
{
if (PlayerController.Instance == null) return;
Vector3 lookAtPosition = PlayerController.Instance.transform.position;
lookAtPosition.y = transform.position.y;
transform.LookAt(lookAtPosition);
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 8696519b7eabaab4dbe80f2a4744cea0
@@ -1,32 +0,0 @@
using UnityEngine;
namespace Unheimlich.Features.Dialogue.UI
{
/// <summary>
/// Makes UI element always face the main camera.
/// Commonly used for world-space UI elements like floating words.
/// </summary>
public class BillboardUI : MonoBehaviour
{
private Transform _cameraTransform;
private void Awake()
{
_cameraTransform = Camera.main != null ? Camera.main.transform : null;
if (_cameraTransform == null)
Debug.LogWarning($"BillboardUI on {gameObject.name}: Main camera not found!");
}
private void Start() { }
private void LateUpdate()
{
if (_cameraTransform != null)
{
transform.LookAt(transform.position + _cameraTransform.rotation * Vector3.forward,
_cameraTransform.rotation * Vector3.up);
}
}
}
}
@@ -3,6 +3,7 @@ using TMPro;
using System.Collections;
using System.Collections.Generic;
using Unheimlich.Features.Hallucination;
using Unheimlich.Features.Shared;
namespace Unheimlich.Features.Dialogue.UI
{
@@ -495,7 +496,7 @@ namespace Unheimlich.Features.Dialogue.UI
ghostRenderer.sortingOrder = mainRenderer.sortingOrder - 1;
}
ghostObj.AddComponent<BillboardUI>();
ghostObj.AddComponent<FacePlayer>();
// Start hidden — the manager or an override will enable it when needed
_ghostText.enabled = _liveGhostEnabled;
@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 8a26921d06d64544184c5cbcf41b0b7d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 6e871b828a2e9ca4ba7cc5e025373372
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,37 +0,0 @@
using UnityEngine;
namespace Unheimlich.Features.Diapason
{
[System.Serializable]
public class TapAnimationSettings
{
[Header("Tap Motion")]
[Tooltip("Elbow rotation during windup (degrees)")]
public float WindupRotation = 15f;
[Tooltip("Duration of windup (pull back)")]
public float WindupDuration = 0.1f;
[Tooltip("Duration of strike (forward snap)")]
public float StrikeDuration = 0.05f;
[Tooltip("Wrist snap rotation during strike (degrees)")]
public float WristSnapRotation = 10f;
[Header("Vibration Shake")]
[Tooltip("Shake intensity during vibration")]
public float ShakeIntensity = 2f;
[Tooltip("Shake speed (frequency)")]
public float ShakeSpeed = 30f;
}
public class EquippableTapSettings : MonoBehaviour
{
[SerializeField] private TapAnimationSettings _tapSettings = new TapAnimationSettings();
public TapAnimationSettings TapSettings => _tapSettings;
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: b46af809fa8f34d49b3dec606a505c5e
@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: adbe6317f6c8e9e469b36e06ab2abbfb
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,379 +0,0 @@
using UnityEngine;
using Unheimlich.Core;
using Unheimlich.Core.Audio;
using Unheimlich.Features.OctoArms;
using InputManager = Unheimlich.Core.Input.InputManager;
using System;
using System.Collections;
namespace Unheimlich.Features.Diapason
{
public class DiapasonEquipmentManager : MonoBehaviour
{
[Header("References")]
[SerializeField] private OctoIkArmsImproved _armsSystem;
[SerializeField] private GameObject _diapasonPrefab;
[SerializeField] private int _equipmentArmIndex = 1;
[Header("Detection Settings")]
[SerializeField] private LayerMask _soundEmitterLayer;
[SerializeField] private float _detectionRadius = 0.15f;
[SerializeField] private Transform[] _detectionPoints;
[Header("Visual Feedback")]
[SerializeField] private GameObject _contactFeedbackPrefab;
[Header("Diapason Animation")]
[SerializeField] private string _vibratingBoolParam = "IsVibrating";
[SerializeField] private string _tapTriggerParam = "Tap";
[Header("Tap Settings")]
[SerializeField] private AudioClip _tapSound;
[SerializeField] [Range(0f, 1f)] private float _tapVolume = 1f;
[SerializeField] private float _vibrationDuration = 15f;
public bool IsEquipped => _isEquipped;
public AudioListenable CurrentDetectedEmitter => _currentDetectedEmitter;
public bool CanTap => _isEquipped && _currentDetectedEmitter != null && !_isVibrating && !_currentDetectedEmitter.HasBeenTapped;
public event Action<AudioListenable> OnEmitterDetected;
public event Action<AudioListenable> OnEmitterLost;
private GameObject _spawnedDiapason;
private bool _isEquipped = false;
private AudioListenable _currentDetectedEmitter;
private GameObject _contactFeedback;
private bool _isVibrating = false;
private AudioSource _audioSource;
private Coroutine _vibrationCoroutine;
private Animator _diapasonAnimator;
private ParticleSystem _diapasonParticles;
void Awake()
{
if (_armsSystem == null)
{
_armsSystem = GetComponent<OctoIkArmsImproved>();
if (_armsSystem == null)
{
Debug.LogError("[DiapasonEquipment] OctoIkArmsImproved not found!");
}
}
_audioSource = GetComponent<AudioSource>();
if (_audioSource == null)
{
_audioSource = gameObject.AddComponent<AudioSource>();
}
}
void Update()
{
if (_isEquipped && _armsSystem.GetArmState(_equipmentArmIndex) == ArmState.Equipped)
{
CheckForSoundEmitters();
var fireAction = InputManager.GetFireAction();
if (fireAction != null && fireAction.WasPressedThisFrame())
{
if (CanTap)
TapDiapason();
}
}
}
public void EquipDiapason()
{
if (_isEquipped)
{
Debug.LogWarning("[DiapasonEquipment] Diapason already equipped!");
return;
}
if (_diapasonPrefab == null)
{
Debug.LogError("[DiapasonEquipment] Diapason prefab not assigned!");
return;
}
_spawnedDiapason = Instantiate(_diapasonPrefab);
_spawnedDiapason.SetActive(true);
_diapasonAnimator = _spawnedDiapason.GetComponentInChildren<Animator>(true);
if (_diapasonAnimator == null)
{
Debug.LogWarning("[DiapasonEquipment] No Animator found on spawned diapason");
}
else
{
_diapasonAnimator.enabled = false;
}
_diapasonParticles = _spawnedDiapason.GetComponentInChildren<ParticleSystem>(true);
if (_diapasonParticles == null)
{
Debug.LogWarning("[DiapasonEquipment] No ParticleSystem found on spawned diapason");
}
_armsSystem.EquipObjectOnArm(_equipmentArmIndex, _spawnedDiapason, () => {
_isEquipped = true;
OnDiapasonEquipped();
});
}
public void UnequipDiapason()
{
if (!_isEquipped)
{
Debug.LogWarning("[DiapasonEquipment] Diapason not equipped!");
return;
}
if (_vibrationCoroutine != null)
{
StopCoroutine(_vibrationCoroutine);
_vibrationCoroutine = null;
}
if (_currentDetectedEmitter != null)
{
_currentDetectedEmitter.SetConcentrating(false);
_currentDetectedEmitter.StopListening(cancelConcentrationFade: false);
_currentDetectedEmitter = null;
}
_armsSystem.UnequipObjectFromArm(_equipmentArmIndex, () => {
_isEquipped = false;
_spawnedDiapason = null;
OnDiapasonUnequipped();
});
}
private void CheckForSoundEmitters()
{
if (_detectionPoints == null || _detectionPoints.Length == 0)
return;
AudioListenable closestEmitter = null;
float closestDistance = float.MaxValue;
foreach (Transform detectionPoint in _detectionPoints)
{
if (detectionPoint == null)
continue;
Collider[] hits = Physics.OverlapSphere(detectionPoint.position, _detectionRadius, _soundEmitterLayer);
foreach (Collider hit in hits)
{
AudioListenable emitter = hit.GetComponent<AudioListenable>();
if (emitter != null)
{
float distance = Vector3.Distance(detectionPoint.position, hit.transform.position);
if (distance < closestDistance)
{
closestDistance = distance;
closestEmitter = emitter;
}
}
}
}
if (closestEmitter != _currentDetectedEmitter)
{
if (_currentDetectedEmitter != null)
{
OnEmitterExited(_currentDetectedEmitter);
}
_currentDetectedEmitter = closestEmitter;
if (_currentDetectedEmitter != null)
{
OnEmitterEntered(_currentDetectedEmitter);
}
}
}
private void OnDiapasonEquipped()
{
PlayerEvents.TriggerDiapasonEquip();
if (_diapasonAnimator != null)
{
_diapasonAnimator.enabled = true;
_diapasonAnimator.SetBool(_vibratingBoolParam, false);
}
if (_diapasonParticles != null)
{
_diapasonParticles.Stop();
var emission = _diapasonParticles.emission;
emission.enabled = false;
}
}
private void OnDiapasonUnequipped()
{
PlayerEvents.TriggerDiapasonUnequip();
_isVibrating = false;
_diapasonAnimator = null;
_diapasonParticles = null;
}
public void TapDiapason()
{
if (!CanTap)
{
Debug.LogWarning("[DiapasonEquipment] Cannot tap: not equipped, no contact, or already tapped");
return;
}
TapAnimationSettings tapSettings = null;
if (_spawnedDiapason != null)
{
EquippableTapSettings equippable = _spawnedDiapason.GetComponent<EquippableTapSettings>();
if (equippable != null)
{
tapSettings = equippable.TapSettings;
}
}
ArmEquipment equipment = _armsSystem.GetArmEquipment(_equipmentArmIndex);
if (equipment != null && tapSettings != null)
{
equipment.PlayTapAnimation(tapSettings);
equipment.StartShake(tapSettings);
}
if (_tapSound != null && _audioSource != null)
{
_audioSource.PlayOneShot(_tapSound, _tapVolume);
}
if (_diapasonAnimator != null)
{
_diapasonAnimator.SetTrigger(_tapTriggerParam);
_diapasonAnimator.SetBool(_vibratingBoolParam, true);
}
if (_diapasonParticles != null)
{
var emission = _diapasonParticles.emission;
emission.enabled = true;
_diapasonParticles.Play();
}
_isVibrating = true;
if (_currentDetectedEmitter != null)
{
_currentDetectedEmitter.StartListening(transform.position);
}
PlayerEvents.TriggerDiapasonTap();
if (_vibrationCoroutine != null)
StopCoroutine(_vibrationCoroutine);
_vibrationCoroutine = StartCoroutine(StopVibrationAfterDuration());
}
private IEnumerator StopVibrationAfterDuration()
{
yield return new WaitForSeconds(_vibrationDuration);
if (_diapasonAnimator != null)
{
_diapasonAnimator.SetBool(_vibratingBoolParam, false);
}
if (_diapasonParticles != null)
{
_diapasonParticles.Stop();
var emission = _diapasonParticles.emission;
emission.enabled = false;
}
if (_currentDetectedEmitter != null)
{
_currentDetectedEmitter.StopListening();
}
ArmEquipment equipment = _armsSystem.GetArmEquipment(_equipmentArmIndex);
if (equipment != null)
{
equipment.StopShake();
}
_isVibrating = false;
_vibrationCoroutine = null;
}
private void OnEmitterEntered(AudioListenable emitter)
{
emitter.SetConcentrating(true);
if (_contactFeedbackPrefab != null && _contactFeedback == null)
{
_contactFeedback = Instantiate(_contactFeedbackPrefab, emitter.transform);
_contactFeedback.transform.localPosition = Vector3.zero;
}
OnEmitterDetected?.Invoke(emitter);
}
private void OnEmitterExited(AudioListenable emitter)
{
emitter.SetConcentrating(false);
if (!emitter.HasBeenTapped)
{
emitter.StopListening();
}
if (_contactFeedback != null)
{
Destroy(_contactFeedback);
_contactFeedback = null;
}
if (_diapasonAnimator != null)
{
_diapasonAnimator.SetBool(_vibratingBoolParam, false);
}
if (_diapasonParticles != null)
{
_diapasonParticles.Stop();
var emission = _diapasonParticles.emission;
emission.enabled = false;
}
_isVibrating = false;
OnEmitterLost?.Invoke(emitter);
}
void OnDrawGizmosSelected()
{
if (_detectionPoints == null || !_isEquipped)
return;
Gizmos.color = Color.yellow;
foreach (Transform detectionPoint in _detectionPoints)
{
if (detectionPoint != null)
{
Gizmos.DrawWireSphere(detectionPoint.position, _detectionRadius);
}
}
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 38b80731eb88f834ab3d79a566e40b48
@@ -1,193 +0,0 @@
using System.Collections;
using UnityEngine;
using Unheimlich.Core.Audio;
using Unheimlich.Core;
using Unheimlich.Core.Utils;
using Unheimlich.Features.Player;
using Unheimlich.World.CameraSystem;
using InputManager = Unheimlich.Core.Input.InputManager;
namespace Unheimlich.Features.Diapason
{
public class DiapasonListeningHandlerV2 : Singleton<DiapasonListeningHandlerV2>
{
[Header("Settings")]
[SerializeField] private bool _disablePlayerMovementDuringListen = true;
[SerializeField] private bool _enablePanMode = true;
[Header("Visual Feedback")]
[Tooltip("Vignette intensity during listening (0-1)")]
[SerializeField] [Range(0f, 1f)] private float _vignetteIntensity = 0.3f;
[Header("Audio")]
[SerializeField] private AudioClip _breathInClip;
[SerializeField] [Range(0f, 1f)] private float _breathInVolume = 1f;
[SerializeField] private float _concentrationDuration = 1f;
[SerializeField] private float _breathInDuration = 0.5f;
public bool IsListening { get; private set; }
public bool IsConcentrating { get; private set; }
private AudioSource _playerAudioSource;
private Coroutine _listeningCoroutine;
private CameraHeadBob _cameraHeadBob;
private CameraPanSettings _originalCameraSettings;
private AudioListenable _lastDetectedEmitter;
private bool _isFadingOut;
protected override void OnAwake()
{
_playerAudioSource = GetComponent<AudioSource>();
if (_playerAudioSource == null)
_playerAudioSource = gameObject.AddComponent<AudioSource>();
_cameraHeadBob = GetComponentInChildren<CameraHeadBob>();
}
void Update()
{
var diapasonAction = InputManager.GetDiapasonAction();
if (diapasonAction != null && diapasonAction.WasPressedThisFrame())
{
if (!IsListening && !IsConcentrating)
{
StartListeningMode();
}
else if (IsListening || IsConcentrating)
{
StopListeningMode();
}
}
if (IsListening && PlayerController.Instance?.CameraPanController != null)
{
PlayerController.Instance.CameraPanController.UpdatePan();
}
}
private void StartListeningMode()
{
if (_isFadingOut || PlayerController.Instance?.DiapasonEquipment == null)
return;
if (_listeningCoroutine != null)
StopCoroutine(_listeningCoroutine);
PlayerEvents.TriggerEnterListening();
_listeningCoroutine = StartCoroutine(ListeningSequence());
}
private IEnumerator ListeningSequence()
{
IsConcentrating = true;
InputManager.SetContext(InputManager.InputContext.Listening);
if (_disablePlayerMovementDuringListen && PlayerController.Instance?.Movement != null)
{
PlayerController.Instance.Movement.enabled = false;
}
PlayerController.Instance.DiapasonEquipment.EquipDiapason();
PlayBreathIn();
yield return new WaitForSeconds(_breathInDuration);
if (_cameraHeadBob != null)
{
float resetDuration = _concentrationDuration - _breathInDuration;
_cameraHeadBob.LerpToZero(resetDuration);
}
yield return new WaitForSeconds(_concentrationDuration - _breathInDuration);
IsConcentrating = false;
IsListening = true;
EnableVignette(true);
if (_enablePanMode && PlayerController.Instance?.CameraPanController != null)
{
PlayerController.Instance.CameraPanController.SaveOriginalState();
PlayerController.Instance.CameraPanController.StartPan();
}
Cursor.lockState = CursorLockMode.Confined;
Cursor.visible = false;
}
private void StopListeningMode()
{
if (_listeningCoroutine != null)
{
StopCoroutine(_listeningCoroutine);
_listeningCoroutine = null;
}
IsListening = false;
IsConcentrating = false;
_isFadingOut = true;
PlayerController.Instance.DiapasonEquipment.UnequipDiapason();
EnableVignette(false);
if (_enablePanMode && PlayerController.Instance?.CameraPanController != null)
{
PlayerController.Instance.CameraPanController.StopPan();
PlayerController.Instance.CameraPanController.ReturnToOriginalPosition(() =>
{
if (_originalCameraSettings != null && PlayerController.Instance?.CameraPanController != null)
{
PlayerController.Instance.CameraPanController.Settings = _originalCameraSettings;
_originalCameraSettings = null;
}
if (_disablePlayerMovementDuringListen && PlayerController.Instance?.Movement != null)
{
PlayerController.Instance.Movement.enabled = true;
}
_isFadingOut = false;
});
}
else
{
if (_disablePlayerMovementDuringListen && PlayerController.Instance?.Movement != null)
{
PlayerController.Instance.Movement.enabled = true;
}
_isFadingOut = false;
}
InputManager.SetContext(InputManager.InputContext.Normal);
PlayerEvents.TriggerExitListening();
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
}
private void PlayBreathIn()
{
if (_breathInClip != null && _playerAudioSource != null)
{
_playerAudioSource.PlayOneShot(_breathInClip, _breathInVolume);
}
}
private void EnableVignette(bool enable)
{
if (ListeningVignetteEffect.Instance == null)
return;
if (enable)
{
ListeningVignetteEffect.Instance.EnableVignetteWithIntensity(_vignetteIntensity);
}
else
{
ListeningVignetteEffect.Instance.EnableVignette(false);
}
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 46c1f473fbd3cac4397aabaa76d18357
@@ -1,109 +0,0 @@
using UnityEngine;
using Unheimlich.Core.Interaction;
using Unheimlich.Features.Dialogue.UI;
namespace Unheimlich.Features.Items
{
/// <summary>
/// Interactable item that can be picked up and examined closely by the player.
/// Temporarily moves to inspection position and allows rotation via look input.
/// </summary>
[RequireComponent(typeof(Collider), typeof(Rigidbody))]
public class InspectableItem : InteractableBase
{
[Header("Inspection Settings")]
[SerializeField] private float _inspectionDistance = 1f;
[SerializeField] private float _rotationSpeed = 100f;
[Header("Animation Settings")]
[SerializeField] private bool _blockInteractionDuringAnimation = true;
private Vector3 _originalPosition;
private Quaternion _originalRotation;
private Transform _originalParent;
private Rigidbody _rigidbody;
private bool _isAnimationPlaying = false;
private ObjectDescriptionTrigger _objectDescriptionTrigger;
private LevitatingObject _levitatingObject;
public bool IsAnimationPlaying => _isAnimationPlaying;
private void Start()
{
_rigidbody = GetComponent<Rigidbody>();
_rigidbody.isKinematic = true;
_originalPosition = transform.position;
_originalRotation = transform.rotation;
_originalParent = transform.parent;
_objectDescriptionTrigger = GetComponent<ObjectDescriptionTrigger>();
_levitatingObject = GetComponent<LevitatingObject>();
}
public override void Interact()
{
if (_blockInteractionDuringAnimation)
{
if (_isAnimationPlaying)
return;
if (_objectDescriptionTrigger != null &&
_objectDescriptionTrigger.IsInMergeOnceMode &&
_objectDescriptionTrigger.IsAnimationActive)
{
return;
}
}
if (PlayerInspectionHandler.Instance != null)
PlayerInspectionHandler.Instance.ToggleInspection(this);
}
public void SetAnimationState(bool isPlaying)
{
_isAnimationPlaying = isPlaying;
}
/// <summary>
/// Prepares the item for inspection by parenting it to the hold point and making it kinematic.
/// </summary>
public void PrepareForInspection(Transform holdPoint)
{
_originalPosition = transform.position;
_originalRotation = transform.rotation;
_originalParent = transform.parent;
_rigidbody.isKinematic = true;
transform.SetParent(holdPoint);
transform.localPosition = Vector3.forward * _inspectionDistance;
transform.localRotation = Quaternion.identity;
if (_levitatingObject != null)
{
_levitatingObject.UpdateBasePosition();
}
}
public void Release()
{
transform.SetParent(_originalParent);
transform.position = _originalPosition;
transform.rotation = _originalRotation;
_rigidbody.isKinematic = true;
if (_levitatingObject != null)
{
_levitatingObject.UpdateBasePosition();
}
}
public float RotationSpeed => _rotationSpeed;
private void OnDestroy()
{
LeanTween.cancel(gameObject);
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: cd19509bd29a28648a08eaf7f6146ef1
@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 649c28cf347c39f4da922da29b186810
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 68bed47480dd3d34cb3ab7611ce22c1d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,451 +0,0 @@
using UnityEngine;
using Unheimlich.Features.Diapason;
namespace Unheimlich.Features.OctoArms
{
public enum ArmState
{
Environmental,
TransitionToEquipped,
Equipped,
TransitionToEnvironmental
}
[System.Serializable]
public class ArmEquipment
{
[Header("Arm References")]
public OctoIkLimb IkLimb;
public Transform HandSocket;
[Header("Bone References (for procedural animation)")]
[Tooltip("Shoulder bone (optional)")]
public Transform ShoulderBone;
[Tooltip("Elbow bone (optional)")]
public Transform ElbowBone;
[Tooltip("Wrist bone (required for rotation)")]
public Transform WristBone;
[Header("Equipped Pose Settings")]
[Tooltip("Rotation to apply to shoulder when equipped")]
public Vector3 ShoulderRotationOffset = Vector3.zero;
[Tooltip("Rotation to apply to elbow when equipped")]
public Vector3 ElbowRotationOffset = Vector3.zero;
[Tooltip("Rotation to apply to wrist when equipped")]
public Vector3 WristRotationOffset = new Vector3(0, 0, 90);
[Tooltip("Speed of rotation lerp")]
[Range(1f, 20f)]
public float RotationSpeed = 10f;
[Tooltip("Duration of IK blend after unequip transition")]
[Range(0.1f, 1f)]
public float IkBlendDuration = 0.3f;
[Header("Transition Settings")]
[Tooltip("Position offset where arm exits camera view")]
public Vector3 TransitionOffset = new Vector3(0.5f, -0.3f, 0.2f);
[Tooltip("Duration for arm to move out of view")]
public float TransitionOutDuration = 0.3f;
[Tooltip("Duration for arm to return to view")]
public float TransitionInDuration = 0.3f;
[Tooltip("Curve for transition movement")]
public AnimationCurve TransitionCurve = AnimationCurve.EaseInOut(0, 0, 1, 1);
[HideInInspector] public ArmState State = ArmState.Environmental;
[HideInInspector] public Vector3 OriginalTargetPosition;
[HideInInspector] public Quaternion OriginalTargetRotation;
[HideInInspector] public GameObject EquippedObject;
[HideInInspector] public float IkWeight = 1f;
[HideInInspector] public Quaternion OriginalShoulderRotation;
[HideInInspector] public Quaternion OriginalElbowRotation;
[HideInInspector] public Quaternion OriginalWristRotation;
private int _transitionTweenId = -1;
private int _ikWeightTweenId = -1;
private int _ikBlendTweenId = -1;
private bool _isBlendingToIk = false;
private int _tapTweenId = -1;
private bool _isShaking = false;
private TapAnimationSettings _currentShakeSettings;
private Quaternion _transitionStartShoulder;
private Quaternion _transitionStartElbow;
private Quaternion _transitionStartWrist;
public void Initialize()
{
if (IkLimb != null && IkLimb.Target != null)
{
OriginalTargetPosition = IkLimb.Target.localPosition;
OriginalTargetRotation = IkLimb.Target.localRotation;
}
if (ShoulderBone != null)
OriginalShoulderRotation = ShoulderBone.localRotation;
if (ElbowBone != null)
OriginalElbowRotation = ElbowBone.localRotation;
if (WristBone != null)
OriginalWristRotation = WristBone.localRotation;
State = ArmState.Environmental;
IkWeight = 1f;
}
public void StartTransitionToEquipped(GameObject objectToEquip, System.Action onTransitionComplete)
{
if (State != ArmState.Environmental)
return;
State = ArmState.TransitionToEquipped;
EquippedObject = objectToEquip;
CancelActiveTweens();
if (IkLimb == null || IkLimb.Target == null)
{
CompleteEquip(onTransitionComplete);
return;
}
Vector3 transitionTarget = OriginalTargetPosition + TransitionOffset;
_transitionTweenId = LeanTween.value(IkLimb.gameObject, 0f, 1f, TransitionOutDuration)
.setEase(TransitionCurve)
.setOnUpdate((float t) => {
if (IkLimb != null && IkLimb.Target != null)
{
IkLimb.Target.localPosition = Vector3.Lerp(IkLimb.Target.localPosition, transitionTarget, t);
}
})
.setOnComplete(() => {
_transitionTweenId = -1;
CompleteEquip(onTransitionComplete);
}).id;
_ikWeightTweenId = LeanTween.value(IkLimb.gameObject, IkWeight, 0f, TransitionOutDuration * 0.5f)
.setOnUpdate((float val) => {
IkWeight = val;
})
.setOnComplete(() => {
_ikWeightTweenId = -1;
}).id;
}
private void CompleteEquip(System.Action onComplete)
{
State = ArmState.Equipped;
if (IkLimb != null)
{
IkLimb.enabled = false;
}
if (EquippedObject != null && HandSocket != null)
{
EquippedObject.transform.SetParent(HandSocket, false);
EquippedObject.transform.localPosition = Vector3.zero;
EquippedObject.transform.localRotation = Quaternion.identity;
}
onComplete?.Invoke();
}
public void UpdateEquippedPose()
{
if (State != ArmState.Equipped && !_isBlendingToIk)
return;
if (_isShaking)
{
UpdateShake();
return;
}
if (State == ArmState.Equipped)
{
float deltaTime = UnityEngine.Time.deltaTime;
float lerpSpeed = 1f - Mathf.Exp(-RotationSpeed * deltaTime);
if (ShoulderBone != null)
{
Quaternion targetRot = OriginalShoulderRotation * Quaternion.Euler(ShoulderRotationOffset);
ShoulderBone.localRotation = Quaternion.Slerp(ShoulderBone.localRotation, targetRot, lerpSpeed);
}
if (ElbowBone != null)
{
Quaternion targetRot = OriginalElbowRotation * Quaternion.Euler(ElbowRotationOffset);
ElbowBone.localRotation = Quaternion.Slerp(ElbowBone.localRotation, targetRot, lerpSpeed);
}
if (WristBone != null)
{
Quaternion targetRot = OriginalWristRotation * Quaternion.Euler(WristRotationOffset);
WristBone.localRotation = Quaternion.Slerp(WristBone.localRotation, targetRot, lerpSpeed);
}
}
}
public void StartTransitionToEnvironmental(System.Action onTransitionComplete)
{
if (State != ArmState.Equipped)
return;
State = ArmState.TransitionToEnvironmental;
CancelActiveTweens();
_transitionStartShoulder = ShoulderBone != null ? ShoulderBone.localRotation : Quaternion.identity;
_transitionStartElbow = ElbowBone != null ? ElbowBone.localRotation : Quaternion.identity;
_transitionStartWrist = WristBone != null ? WristBone.localRotation : Quaternion.identity;
if (IkLimb == null || IkLimb.Target == null)
{
CompleteUnequip(onTransitionComplete);
return;
}
Vector3 currentPosition = IkLimb.Target.localPosition;
Vector3 transitionTarget = OriginalTargetPosition + TransitionOffset;
_ikWeightTweenId = LeanTween.value(IkLimb.gameObject, IkWeight, 0f, TransitionInDuration * 0.5f)
.setOnUpdate((float val) => {
IkWeight = val;
})
.setOnComplete(() => {
_ikWeightTweenId = -1;
}).id;
_transitionTweenId = LeanTween.value(IkLimb.gameObject, 0f, 1f, TransitionInDuration)
.setEase(TransitionCurve)
.setOnUpdate((float t) => {
if (IkLimb != null && IkLimb.Target != null)
{
IkLimb.Target.localPosition = Vector3.Lerp(currentPosition, transitionTarget, t);
}
if (ShoulderBone != null)
ShoulderBone.localRotation = Quaternion.Slerp(_transitionStartShoulder, OriginalShoulderRotation, t);
if (ElbowBone != null)
ElbowBone.localRotation = Quaternion.Slerp(_transitionStartElbow, OriginalElbowRotation, t);
if (WristBone != null)
WristBone.localRotation = Quaternion.Slerp(_transitionStartWrist, OriginalWristRotation, t);
})
.setOnComplete(() => {
_transitionTweenId = -1;
StartIkBlend(onTransitionComplete);
}).id;
}
private void StartIkBlend(System.Action onTransitionComplete)
{
if (IkLimb != null)
{
IkLimb.enabled = true;
}
if (EquippedObject != null)
{
if (HandSocket != null)
EquippedObject.transform.SetParent(null);
UnityEngine.Object.Destroy(EquippedObject);
EquippedObject = null;
}
_isBlendingToIk = true;
Quaternion startShoulder = ShoulderBone != null ? ShoulderBone.localRotation : Quaternion.identity;
Quaternion startElbow = ElbowBone != null ? ElbowBone.localRotation : Quaternion.identity;
Quaternion startWrist = WristBone != null ? WristBone.localRotation : Quaternion.identity;
Vector3 startTargetPos = IkLimb != null && IkLimb.Target != null ? IkLimb.Target.localPosition : Vector3.zero;
Quaternion startTargetRot = IkLimb != null && IkLimb.Target != null ? IkLimb.Target.localRotation : Quaternion.identity;
if (IkLimb != null && IkLimb.gameObject != null)
{
_ikBlendTweenId = LeanTween.value(IkLimb.gameObject, 0f, 1f, IkBlendDuration)
.setEase(LeanTweenType.easeOutQuad)
.setOnUpdate((float t) => {
if (ShoulderBone != null)
ShoulderBone.localRotation = Quaternion.Slerp(startShoulder, OriginalShoulderRotation, t);
if (ElbowBone != null)
ElbowBone.localRotation = Quaternion.Slerp(startElbow, OriginalElbowRotation, t);
if (WristBone != null)
WristBone.localRotation = Quaternion.Slerp(startWrist, OriginalWristRotation, t);
if (IkLimb != null && IkLimb.Target != null)
{
IkLimb.Target.localPosition = Vector3.Lerp(startTargetPos, OriginalTargetPosition, t);
IkLimb.Target.localRotation = Quaternion.Slerp(startTargetRot, OriginalTargetRotation, t);
}
IkWeight = t;
})
.setOnComplete(() => {
_ikBlendTweenId = -1;
CompleteUnequip(onTransitionComplete);
}).id;
}
else
{
CompleteUnequip(onTransitionComplete);
}
}
private void CompleteUnequip(System.Action onComplete)
{
State = ArmState.Environmental;
_isBlendingToIk = false;
if (IkLimb != null && IkLimb.Target != null)
{
IkLimb.Target.localPosition = OriginalTargetPosition;
IkLimb.Target.localRotation = OriginalTargetRotation;
}
if (ShoulderBone != null)
ShoulderBone.localRotation = OriginalShoulderRotation;
if (ElbowBone != null)
ElbowBone.localRotation = OriginalElbowRotation;
if (WristBone != null)
WristBone.localRotation = OriginalWristRotation;
onComplete?.Invoke();
}
public void CancelActiveTweens()
{
if (_transitionTweenId != -1)
{
LeanTween.cancel(_transitionTweenId);
_transitionTweenId = -1;
}
if (_ikWeightTweenId != -1)
{
LeanTween.cancel(_ikWeightTweenId);
_ikWeightTweenId = -1;
}
if (_ikBlendTweenId != -1)
{
LeanTween.cancel(_ikBlendTweenId);
_ikBlendTweenId = -1;
}
if (_tapTweenId != -1)
{
LeanTween.cancel(_tapTweenId);
_tapTweenId = -1;
}
_isBlendingToIk = false;
_isShaking = false;
}
public void PlayTapAnimation(TapAnimationSettings settings)
{
if (State != ArmState.Equipped || settings == null)
return;
if (_tapTweenId != -1)
{
LeanTween.cancel(_tapTweenId);
}
Quaternion elbowStart = ElbowBone != null ? ElbowBone.localRotation : Quaternion.identity;
Quaternion wristStart = WristBone != null ? WristBone.localRotation : Quaternion.identity;
Quaternion elbowWindup = elbowStart * Quaternion.Euler(0, 0, -settings.WindupRotation);
Quaternion elbowStrike = elbowStart * Quaternion.Euler(0, 0, settings.WindupRotation * 0.3f);
Quaternion wristWindup = wristStart;
Quaternion wristStrike = wristStart * Quaternion.Euler(0, 0, -settings.WristSnapRotation);
_tapTweenId = LeanTween.value(IkLimb.gameObject, 0f, 1f, settings.WindupDuration)
.setOnUpdate((float t) => {
if (ElbowBone != null)
{
ElbowBone.localRotation = Quaternion.Slerp(elbowStart, elbowWindup, t);
}
if (WristBone != null)
{
WristBone.localRotation = Quaternion.Slerp(wristStart, wristWindup, t);
}
})
.setOnComplete(() => {
_tapTweenId = LeanTween.value(IkLimb.gameObject, 0f, 1f, settings.StrikeDuration)
.setOnUpdate((float t) => {
if (ElbowBone != null)
{
ElbowBone.localRotation = Quaternion.Slerp(elbowWindup, elbowStrike, t);
}
if (WristBone != null)
{
WristBone.localRotation = Quaternion.Slerp(wristWindup, wristStrike, t);
}
})
.setOnComplete(() => {
_tapTweenId = LeanTween.value(IkLimb.gameObject, 0f, 1f, 0.1f)
.setOnUpdate((float t) => {
if (ElbowBone != null)
{
ElbowBone.localRotation = Quaternion.Slerp(elbowStrike, elbowStart, t);
}
if (WristBone != null)
{
WristBone.localRotation = Quaternion.Slerp(wristStrike, wristStart, t);
}
})
.setOnComplete(() => {
_tapTweenId = -1;
}).id;
}).id;
}).id;
}
public void StartShake(TapAnimationSettings settings)
{
_isShaking = true;
_currentShakeSettings = settings;
}
public void StopShake()
{
_isShaking = false;
_currentShakeSettings = null;
}
public void UpdateShake()
{
if (!_isShaking || State != ArmState.Equipped || WristBone == null || _currentShakeSettings == null)
return;
float shake = Mathf.Sin(UnityEngine.Time.time * _currentShakeSettings.ShakeSpeed) * _currentShakeSettings.ShakeIntensity;
Quaternion shakeRotation = Quaternion.Euler(shake, shake * 0.5f, 0);
Quaternion targetRot = OriginalWristRotation * Quaternion.Euler(WristRotationOffset) * shakeRotation;
WristBone.localRotation = targetRot;
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 80d59ec0ac09852429c56ca6c42464c2
@@ -1,519 +0,0 @@
using UnityEngine;
namespace Unheimlich.Features.OctoArms
{
public class OctoIkArmsImproved : MonoBehaviour
{
// Stores raycast hit information
private class RaycastInfos
{
public bool HasHit;
public float Distance;
public float MaxDistance;
public Vector3 Position;
public Vector3 Normal;
public Transform Transform;
}
[System.Serializable]
private class RaycastRig
{
public Transform Root;
public float Distance = 1f;
}
[System.Serializable]
private class ArmRig
{
public bool Enabled = true;
public OctoIkLimb Arm;
public Vector3 RotationOffset;
public RaycastRig[] Raycasts;
[Header("Equipment System")]
public ArmEquipment Equipment;
[HideInInspector] public Vector3 TargetOriginPos;
[HideInInspector] public Quaternion TargetOriginRot;
[HideInInspector] public Vector3 PoleOriginPos;
public void Init()
{
if (Arm)
{
if (Arm.Target)
{
TargetOriginPos = Arm.Target.localPosition;
TargetOriginRot = Arm.Target.localRotation;
}
if (Arm.Pole)
{
PoleOriginPos = Arm.Pole.localPosition;
}
}
if (Equipment != null)
{
Equipment.Initialize();
}
}
public bool IsEnvironmentalIkActive()
{
if (Equipment == null)
return Enabled;
return Enabled && Equipment.State == ArmState.Environmental;
}
public float GetEnvironmentalIkWeight()
{
if (Equipment == null)
return 1f;
return Equipment.IkWeight;
}
}
[System.Serializable]
private class Settings
{
[Header("Raycast")]
public LayerMask LayerMask;
[Header("Stickiness")]
[Tooltip("Distance to start sticking (meters)")]
[Range(0.1f, 2f)]
public float StickStartDistance = 0.8f;
[Tooltip("Distance to fully release (meters)")]
[Range(0.5f, 3f)]
public float StickReleaseDistance = 1.2f;
[Header("Smoothing")]
[Tooltip("Stick speed (units/sec)")]
[Range(1f, 20f)]
public float StickSpeed = 10f;
[Tooltip("Release speed (units/sec)")]
[Range(1f, 20f)]
public float ReleaseSpeed = 6f;
[Tooltip("Rotation speed (deg/sec)")]
[Range(90f, 720f)]
public float RotationSpeed = 360f;
[Header("Arm Selection")]
[Tooltip("Can both arms stick at the same time?")]
public bool AllowBothArms = true;
[Tooltip("Max surface angle to accept sticking (degrees)")]
[Range(0f, 90f)]
public float MaxSurfaceAngle = 60f;
[Header("Temporal Smoothing")]
[Tooltip("Frames before releasing (prevents flickering)")]
[Range(0, 10)]
public int StabilityFrames = 3;
}
[SerializeField] private ArmRig _leftArm;
[SerializeField] private ArmRig _rightArm;
[SerializeField] private Settings _settings;
private ArmRig[] _arms;
private RaycastInfos[][] _raycasts = new RaycastInfos[2][];
private RaycastInfos[] _nearestHitRaycasts = new RaycastInfos[2];
private int[] _noHitFrameCount = new int[2];
private float[] _currentStickiness = new float[2];
void Awake()
{
_arms = new ArmRig[2] { _leftArm, _rightArm };
foreach (ArmRig armRig in _arms)
armRig.Init();
// Initialize raycast arrays for both arms
_raycasts = new RaycastInfos[_arms.Length][];
for (int i = 0; i < _arms.Length; i++)
{
_raycasts[i] = new RaycastInfos[_arms[i].Raycasts.Length];
for (int u = 0; u < _raycasts[i].Length; u++)
_raycasts[i][u] = new RaycastInfos() { MaxDistance = 1 };
}
_nearestHitRaycasts = new RaycastInfos[_arms.Length];
for (int i = 0; i < _nearestHitRaycasts.Length; i++)
_nearestHitRaycasts[i] = new RaycastInfos() { MaxDistance = 1 };
_noHitFrameCount = new int[_arms.Length];
_currentStickiness = new float[_arms.Length];
}
void OnDrawGizmos()
{
if (Application.isPlaying || !enabled) return;
foreach (ArmRig armRig in new ArmRig[2] { _leftArm, _rightArm })
{
if (armRig.Raycasts == null) continue;
for (int i = 0; i < armRig.Raycasts.Length; i++)
{
RaycastRig raycastRig = armRig.Raycasts[i];
if (raycastRig.Root == null) continue;
Debug.DrawRay(raycastRig.Root.position, raycastRig.Root.TransformDirection(Vector3.forward) * raycastRig.Distance, Color.cyan);
}
}
}
void Update()
{
PerformRaycasts();
FindNearestHits();
UpdateArmPositions();
UpdateEquippedPoses();
UpdateHideShowAnimation();
if (_keepHidden && !_isShowing)
{
_leftArm.Arm.Target.localPosition = _originalLeftTargetPos + _hideOffset;
_rightArm.Arm.Target.localPosition = _originalRightTargetPos + _hideOffset;
}
}
private void UpdateEquippedPoses()
{
for (int i = 0; i < _arms.Length; i++)
{
ArmRig armRig = _arms[i];
if (armRig.Equipment != null)
{
armRig.Equipment.UpdateEquippedPose();
}
}
}
// Cast all configured raycasts and store results
private void PerformRaycasts()
{
for (int i = 0; i < _arms.Length; i++)
{
ArmRig armRig = _arms[i];
if (!armRig.IsEnvironmentalIkActive() || armRig.Raycasts == null)
continue;
for (int u = 0; u < armRig.Raycasts.Length; u++)
{
RaycastRig raycastRig = armRig.Raycasts[u];
if (raycastRig.Root == null) continue;
RaycastInfos raycastInfos = _raycasts[i][u];
RaycastHit hit;
Vector3 origin = raycastRig.Root.position;
Vector3 direction = raycastRig.Root.TransformDirection(Vector3.forward);
if (Physics.Raycast(origin, direction, out hit, raycastRig.Distance, _settings.LayerMask))
{
Debug.DrawRay(origin, direction * hit.distance, Color.green);
raycastInfos.HasHit = true;
raycastInfos.Distance = hit.distance;
raycastInfos.MaxDistance = raycastRig.Distance;
raycastInfos.Position = hit.point;
raycastInfos.Normal = hit.normal;
raycastInfos.Transform = hit.transform;
}
else
{
Debug.DrawRay(origin, direction * raycastRig.Distance, Color.red);
raycastInfos.HasHit = false;
raycastInfos.Distance = raycastRig.Distance;
raycastInfos.MaxDistance = raycastRig.Distance;
raycastInfos.Position = Vector3.zero;
raycastInfos.Normal = Vector3.zero;
raycastInfos.Transform = null;
}
}
}
}
// Find the closest raycast hit for each arm
private void FindNearestHits()
{
for (int i = 0; i < _arms.Length; i++)
{
ArmRig armRig = _arms[i];
RaycastInfos nearest = _nearestHitRaycasts[i];
nearest.HasHit = false;
float closestDistance = float.MaxValue;
if (armRig.Raycasts == null) continue;
// Find the closest hit among all raycasts for this arm
for (int u = 0; u < armRig.Raycasts.Length; u++)
{
RaycastInfos raycastInfos = _raycasts[i][u];
if (raycastInfos.HasHit && raycastInfos.Distance < closestDistance)
{
closestDistance = raycastInfos.Distance;
nearest.HasHit = true;
nearest.Distance = raycastInfos.Distance;
nearest.MaxDistance = raycastInfos.MaxDistance;
nearest.Position = raycastInfos.Position;
nearest.Normal = raycastInfos.Normal;
nearest.Transform = raycastInfos.Transform;
}
}
// Track frames without hits for temporal smoothing
if (!nearest.HasHit)
{
_noHitFrameCount[i]++;
}
else
{
_noHitFrameCount[i] = 0;
}
}
}
// Update arm positions based on raycast hits
private void UpdateArmPositions()
{
for (int i = 0; i < _arms.Length; i++)
{
ArmRig armRig = _arms[i];
if (armRig.Arm == null || armRig.Arm.Target == null) continue;
if (!armRig.IsEnvironmentalIkActive())
continue;
RaycastInfos raycastInfos = _nearestHitRaycasts[i];
Vector3 targetPos = armRig.TargetOriginPos;
Quaternion targetRot = armRig.TargetOriginRot;
bool shouldStick = false;
if (raycastInfos.HasHit && _noHitFrameCount[i] < _settings.StabilityFrames)
{
if (IsValidSurface(raycastInfos, i))
{
targetPos = armRig.Arm.Target.parent.InverseTransformPoint(raycastInfos.Position);
targetRot = Quaternion.LookRotation(
-armRig.Arm.Target.parent.InverseTransformDirection(raycastInfos.Normal),
transform.up
) * Quaternion.Euler(armRig.RotationOffset);
shouldStick = true;
}
}
float targetStickiness = shouldStick ? 1f : 0f;
float stickinessSpeed = shouldStick ? _settings.StickSpeed : _settings.ReleaseSpeed;
_currentStickiness[i] = Mathf.MoveTowards(_currentStickiness[i], targetStickiness, stickinessSpeed * Time.deltaTime);
float distanceFactor = 0f;
if (shouldStick && raycastInfos.HasHit)
{
distanceFactor = 1f - Mathf.InverseLerp(_settings.StickStartDistance, _settings.StickReleaseDistance, raycastInfos.Distance);
distanceFactor = Mathf.Clamp01(distanceFactor);
}
float finalStickiness = _currentStickiness[i] * distanceFactor * armRig.GetEnvironmentalIkWeight();
float posSpeed = Mathf.Lerp(_settings.ReleaseSpeed, _settings.StickSpeed, finalStickiness);
float rotSpeed = _settings.RotationSpeed * Mathf.Deg2Rad;
float tPos = 1f - Mathf.Exp(-posSpeed * Time.deltaTime);
float tRot = 1f - Mathf.Exp(-rotSpeed * Time.deltaTime);
armRig.Arm.Target.localPosition = Vector3.Lerp(armRig.Arm.Target.localPosition, targetPos, tPos);
armRig.Arm.Target.localRotation = Quaternion.Slerp(armRig.Arm.Target.localRotation, targetRot, tRot);
}
}
// Check if the surface is valid for sticking
private bool IsValidSurface(RaycastInfos raycast, int armIndex)
{
if (raycast.Transform == null)
return false;
// Check surface angle (reject surfaces too angled)
float surfaceAngle = Vector3.Angle(-raycast.Normal, transform.forward);
if (surfaceAngle > _settings.MaxSurfaceAngle)
return false;
// If not allowing both arms, check if arm is on correct side
if (!_settings.AllowBothArms)
{
Vector3 hitLocalPos = transform.InverseTransformPoint(raycast.Position);
bool isLeftSide = hitLocalPos.x < 0;
bool isLeftArm = armIndex == 0;
if (isLeftSide != isLeftArm)
return false;
}
return true;
}
public void EquipObjectOnArm(int armIndex, GameObject objectToEquip, System.Action onComplete = null)
{
if (armIndex < 0 || armIndex >= _arms.Length)
{
Debug.LogError($"[OctoIkArms] Invalid arm index: {armIndex}");
return;
}
ArmRig armRig = _arms[armIndex];
if (armRig.Equipment == null)
{
Debug.LogError($"[OctoIkArms] Arm {armIndex} has no Equipment system configured!");
return;
}
armRig.Equipment.StartTransitionToEquipped(objectToEquip, onComplete);
}
public void UnequipObjectFromArm(int armIndex, System.Action onComplete = null)
{
if (armIndex < 0 || armIndex >= _arms.Length)
{
Debug.LogError($"[OctoIkArms] Invalid arm index: {armIndex}");
return;
}
ArmRig armRig = _arms[armIndex];
if (armRig.Equipment == null)
{
Debug.LogError($"[OctoIkArms] Arm {armIndex} has no Equipment system configured!");
return;
}
armRig.Equipment.StartTransitionToEnvironmental(onComplete);
}
public ArmState GetArmState(int armIndex)
{
if (armIndex < 0 || armIndex >= _arms.Length)
return ArmState.Environmental;
if (_arms[armIndex].Equipment == null)
return ArmState.Environmental;
return _arms[armIndex].Equipment.State;
}
public bool IsArmInEnvironmentalMode(int armIndex)
{
return GetArmState(armIndex) == ArmState.Environmental;
}
public ArmEquipment GetArmEquipment(int armIndex)
{
if (armIndex < 0 || armIndex >= _arms.Length)
return null;
return _arms[armIndex].Equipment;
}
private SkinnedMeshRenderer _armsRenderer;
private bool _armsHidden = false;
private Vector3 _originalLeftTargetPos;
private Vector3 _originalRightTargetPos;
private float _hideAnimationProgress = 0f;
private bool _isHiding = false;
private bool _isShowing = false;
private bool _keepHidden = false;
private float _hideShowSpeed = 2f;
private Vector3 _hideOffset = new Vector3(0, -0.5f, 0);
public void HideArms(float speed = 2f)
{
if (_armsHidden)
return;
if (_armsRenderer == null)
_armsRenderer = GetComponentInChildren<SkinnedMeshRenderer>();
_originalLeftTargetPos = _leftArm.Arm.Target.localPosition;
_originalRightTargetPos = _rightArm.Arm.Target.localPosition;
_hideShowSpeed = speed;
_hideAnimationProgress = 0f;
_isHiding = true;
_isShowing = false;
_armsHidden = true;
_keepHidden = false;
}
public void ShowArms(float speed = 2f)
{
if (!_armsHidden && !_isHiding)
return;
if (_armsRenderer == null)
_armsRenderer = GetComponentInChildren<SkinnedMeshRenderer>();
if (_armsRenderer != null)
_armsRenderer.enabled = true;
_hideShowSpeed = speed;
_hideAnimationProgress = 0f;
_isShowing = true;
_isHiding = false;
}
private void UpdateHideShowAnimation()
{
if (!_isHiding && !_isShowing)
return;
_hideAnimationProgress += Time.deltaTime * _hideShowSpeed;
float t = Mathf.Clamp01(_hideAnimationProgress);
if (_isHiding)
{
_leftArm.Arm.Target.localPosition = Vector3.Lerp(_originalLeftTargetPos, _originalLeftTargetPos + _hideOffset, t);
_rightArm.Arm.Target.localPosition = Vector3.Lerp(_originalRightTargetPos, _originalRightTargetPos + _hideOffset, t);
if (t >= 1f)
{
_isHiding = false;
_keepHidden = true;
if (_armsRenderer != null)
_armsRenderer.enabled = false;
}
}
else if (_isShowing)
{
_leftArm.Arm.Target.localPosition = Vector3.Lerp(_originalLeftTargetPos + _hideOffset, _originalLeftTargetPos, t);
_rightArm.Arm.Target.localPosition = Vector3.Lerp(_originalRightTargetPos + _hideOffset, _originalRightTargetPos, t);
if (t >= 1f)
{
_isShowing = false;
_armsHidden = false;
_keepHidden = false;
_leftArm.Arm.Target.localPosition = _originalLeftTargetPos;
_rightArm.Arm.Target.localPosition = _originalRightTargetPos;
}
}
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 97777a4e863d974428be79c0d8a40883
@@ -36,6 +36,14 @@ namespace Unheimlich.Features.Player
public bool HasActiveLookInput { get; private set; }
public bool IsUsingGamepad { get; private set; }
//Cam headbob mods
public enum HeadBobMode
{
Normal,
Idle,
Disabled
}
private void Start()
{
if (Settings != null)
@@ -24,8 +24,8 @@ namespace Unheimlich.Features.Player
// Strafe tilt (Z roll applied in LateUpdate, after OctoPlayer sets camera pitch).
private float _currentTilt = 0f;
private HeadBobMode _overrideMode = HeadBobMode.Normal;
private bool _hasOverride = false;
private CameraEffects.HeadBobMode _overrideMode = CameraEffects.HeadBobMode.Normal;
private bool _hasOverride = false;
private Coroutine _resetCoroutine;
private void Start()
@@ -41,17 +41,16 @@ namespace Unheimlich.Features.Player
var settings = PlayerController.Instance.CameraEffects?.Settings;
if (settings == null) return;
HeadBobMode currentMode = _hasOverride ? _overrideMode : HeadBobMode.Normal;
CameraEffects.HeadBobMode currentMode = _hasOverride ? _overrideMode : CameraEffects.HeadBobMode.Normal;
if (currentMode == HeadBobMode.Disabled)
if (currentMode == CameraEffects.HeadBobMode.Disabled)
{
SetBobDirect(settings.idleBobSpeed, 0f);
ApplySmoothedBob(settings.bobRollAmplitude);
return;
}
bool forceIdle = currentMode == HeadBobMode.Idle
|| (PlayerInspectionHandler.Instance != null && PlayerInspectionHandler.Instance.IsInspecting);
bool forceIdle = currentMode == CameraEffects.HeadBobMode.Idle;
if (forceIdle)
{
@@ -119,7 +118,7 @@ namespace Unheimlich.Features.Player
// ── Public API ────────────────────────────────────────────────────────────
/// <summary>Overrides the head bob mode (Idle, Normal, Disabled).</summary>
public void SetMode(HeadBobMode mode)
public void SetMode(CameraEffects.HeadBobMode mode)
{
_overrideMode = mode;
_hasOverride = true;
@@ -1,42 +0,0 @@
using UnityEngine;
using UnityEngine.UI;
namespace Unheimlich.Features.Player
{
public class HandTouchFeedback : MonoBehaviour
{
[Header("Visual Settings")]
[SerializeField] private Image _feedbackImage;
[SerializeField] private Color _readyColor = Color.green;
[SerializeField] private float _pulseSpeed = 2f;
[SerializeField] private float _pulseIntensity = 0.3f;
private float _pulseTimer;
private Color _originalColor;
private void Awake()
{
if (_feedbackImage != null)
{
_originalColor = _feedbackImage.color;
}
}
private void OnEnable()
{
_pulseTimer = 0f;
}
private void Update()
{
if (_feedbackImage == null)
return;
_pulseTimer += Time.deltaTime * _pulseSpeed;
float pulse = (Mathf.Sin(_pulseTimer) + 1f) * 0.5f;
Color targetColor = Color.Lerp(_originalColor, _readyColor, pulse * _pulseIntensity);
_feedbackImage.color = targetColor;
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 39b78978d656e894ea455342398ef8ba
@@ -1,105 +0,0 @@
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using Unheimlich.Core;
using Unheimlich.Features.Hallucination;
namespace Unheimlich.Features.Player
{
public class ListeningVignetteEffect : Singleton<ListeningVignetteEffect>
{
[Header("References")]
[Tooltip("Dedicated Volume for listening mode (not shared with dialogue system)")]
[SerializeField] private Volume _listeningVolume;
[Header("Settings")]
[Tooltip("Target intensity when listening (0-1)")]
[SerializeField] [Range(0f, 1f)] private float _targetIntensity = 0.3f;
[Tooltip("Speed of vignette fade in/out")]
[SerializeField] private float _transitionSpeed = 2f;
private Vignette _vignette;
private bool _isEnabled;
private VolumeProfile _volumeProfile;
protected override void OnAwake()
{
if (_listeningVolume == null)
{
GameObject volumeObject = new GameObject("Listening Mode Volume");
volumeObject.transform.SetParent(transform);
_listeningVolume = volumeObject.AddComponent<Volume>();
_listeningVolume.isGlobal = true;
_listeningVolume.priority = 1;
_listeningVolume.weight = 0f;
}
if (_listeningVolume.profile == null)
{
_volumeProfile = ScriptableObject.CreateInstance<VolumeProfile>();
_listeningVolume.profile = _volumeProfile;
_vignette = _volumeProfile.Add<Vignette>();
_vignette.active = true;
_vignette.intensity.Override(0f);
_vignette.smoothness.Override(0.4f);
_vignette.rounded.Override(true);
}
else
{
if (!_listeningVolume.profile.TryGet(out _vignette))
{
_vignette = _listeningVolume.profile.Add<Vignette>();
_vignette.active = true;
_vignette.intensity.Override(0f);
_vignette.smoothness.Override(0.4f);
_vignette.rounded.Override(true);
}
}
_listeningVolume.weight = 0f;
}
private void Update()
{
if (_vignette == null || _listeningVolume == null)
return;
float targetWeight = _isEnabled ? 1f : 0f;
_listeningVolume.weight = Mathf.Lerp(_listeningVolume.weight, targetWeight, Time.deltaTime * _transitionSpeed);
if (_isEnabled)
_vignette.intensity.value = _targetIntensity;
}
public void EnableVignette(bool enable)
{
_isEnabled = enable;
// Suppress the hallucination vignette while diapason is active to avoid additive blending.
// Restores hallucination vignette control when diapason listening ends.
if (HallucinationManager.Instance != null)
HallucinationManager.Instance.SetVignetteOverriddenExternally(enable);
}
public void SetIntensity(float intensity)
{
_targetIntensity = Mathf.Clamp01(intensity);
if (_vignette != null)
_vignette.intensity.value = _targetIntensity;
}
public void EnableVignetteWithIntensity(float intensity)
{
SetIntensity(intensity);
EnableVignette(true);
}
private void OnDestroy()
{
if (_volumeProfile != null)
Destroy(_volumeProfile);
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 878ebc270c13e5c49a3813b1848f6ea5
@@ -4,9 +4,6 @@ using Unheimlich.Core;
using Unheimlich.Features.Player.Data;
using Unheimlich.Features.Dialogue;
using Unheimlich.Core.Interaction;
using Unheimlich.Features.DetectiveBoard;
using Unheimlich.Features.Diapason;
using Unheimlich.Features.OctoArms;
using Unheimlich.World.CameraSystem;
using InputManager = Unheimlich.Core.Input.InputManager;
@@ -27,13 +24,6 @@ namespace Unheimlich.Features.Player
public Transform CameraTransform { get; private set; }
public Transform CameraPivot { get; private set; }
public CameraEffects CameraEffects { get; private set; }
public CameraPanController CameraPanController { get; private set; }
public DiapasonListeningHandlerV2 DiapasonHandler { get; private set; }
public DiapasonEquipmentManager DiapasonEquipment { get; private set; }
public PlayerInspectionHandler InspectionHandler { get; private set; }
public DetectiveBoardController DetectiveBoardController { get; private set; }
public OctoIkArmsImproved ArmsSystem { get; private set; }
public InputManager Inputs => InputManager.Instance;
// Cached sprint InputAction — resolved once in OnAwake to avoid per-frame foreach.
@@ -56,14 +46,8 @@ namespace Unheimlich.Features.Player
Movement = GetComponent<OctoPlayer>();
CharController = GetComponent<CharacterController>();
CameraEffects = GetComponent<CameraEffects>();
CameraPanController = GetComponent<CameraPanController>();
DiapasonHandler = GetComponent<DiapasonListeningHandlerV2>();
DiapasonEquipment = GetComponent<DiapasonEquipmentManager>();
InspectionHandler = GetComponent<PlayerInspectionHandler>();
DetectiveBoardController = GetComponent<DetectiveBoardController>();
PlayerCamera = GetComponentInChildren<Camera>();
ArmsSystem = GetComponentInChildren<OctoIkArmsImproved>();
if (PlayerCamera != null)
{
@@ -0,0 +1,31 @@
using UnityEngine;
namespace Unheimlich.Features.Shared
{
/// <summary>
/// Makes element always face the main camera.
/// </summary>
public class FacePlayer : MonoBehaviour
{
private Transform _cameraTransform;
private void Awake()
{
_cameraTransform = Camera.main != null ? Camera.main.transform : null;
if (_cameraTransform == null)
Debug.LogWarning($"FacePlayer on {gameObject.name}: Main camera not found!");
}
private void Start() { }
private void LateUpdate()
{
if (_cameraTransform != null)
{
transform.LookAt(transform.position + _cameraTransform.rotation * Vector3.forward,
_cameraTransform.rotation * Vector3.up);
}
}
}
}
-8
View File
@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: bfc84dc662a6af7438b306b5c2c0cad4
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: a8de5fd3bd003994aaaf0ed9c2f5c44d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 2ee4955d2b51b5e4abe0e243cab7267e
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,80 +0,0 @@
using UnityEngine;
namespace Unheimlich.World.CameraSystem
{
/// <summary>
/// Camera pan mode.
/// </summary>
public enum CameraPanMode
{
Pan,
FreeLook
}
/// <summary>
/// Head bob mode for camera pan.
/// </summary>
public enum HeadBobMode
{
Normal,
Idle,
Disabled
}
/// <summary>
/// ScriptableObject configuration for camera pan and player alignment behavior.
/// Defines pan speed, borders, angles, alignment parameters, and lerp smoothness.
/// </summary>
[CreateAssetMenu(fileName = "CameraPanSettings", menuName = "Unheimlich/Camera/Camera Pan Settings")]
public class CameraPanSettings : ScriptableObject
{
[Header("Pan Mode")]
[Tooltip("Pan: Smooth edge-based panning for object inspection | FreeLook: Direct mouse/stick control for detective board")]
public CameraPanMode PanMode = CameraPanMode.Pan;
[Header("Camera Pan")]
[Tooltip("Camera pan speed when moving the cursor/look stick")]
public float PanSpeed = 2f;
[Tooltip("Size of the border area for mouse pan (0-1, screen percentage)")]
[Range(0f, 0.5f)]
public float PanBorderSize = 0.1f;
[Tooltip("Maximum horizontal angle the camera can pan from center (left/right)")]
public float MaxPanAngleX = 15f;
[Tooltip("Maximum vertical angle the camera can pan from center (up/down)")]
public float MaxPanAngleY = 15f;
[Header("Recentering")]
[Tooltip("Enable automatic return to center when idle")]
public bool EnableRecentering = true;
[Tooltip("Speed of automatic recentering (higher = faster return to center)")]
public float RecenterSpeed = 2f;
[Tooltip("Delay before recentering starts (in seconds)")]
public float RecenterDelay = 0.1f;
[Header("Player Alignment")]
[Tooltip("Speed of player/camera alignment to target")]
public float AlignmentSpeed = 3f;
[Tooltip("Threshold for alignment completion")]
public float AlignmentThreshold = 0.5f;
[Tooltip("Fixed distance from the player to the target when aligning (in meters)")]
public float FixedDistanceFromTarget = 1.5f;
[Header("Head Bob Control")]
[Tooltip("Head bob mode: Normal (default behavior), Idle (force subtle breathing), Disabled (no movement)")]
public HeadBobMode HeadBobMode = HeadBobMode.Idle;
[Header("Lerp Smoothing")]
[Tooltip("Use SmoothStep for smoother interpolation (true) or linear lerp (false)")]
public bool UseSmoothStep = true;
[Tooltip("Custom animation curve for alignment (overrides SmoothStep if set)")]
public AnimationCurve AlignmentCurve = null;
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 3e231c75dddecc0469543c190a9234b1

Some files were not shown because too many files have changed in this diff Show More