clean : deleted all useless scripts
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-2
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
-2
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user