Hallucination system
This commit is contained in:
+1
-1
@@ -158,7 +158,7 @@ MeshRenderer:
|
|||||||
m_RenderingLayerMask: 1
|
m_RenderingLayerMask: 1
|
||||||
m_RendererPriority: 0
|
m_RendererPriority: 0
|
||||||
m_Materials:
|
m_Materials:
|
||||||
- {fileID: 2100000, guid: 330dec4f7aa07334693179a8ae85c898, type: 2}
|
- {fileID: 2100000, guid: a4126e3e799efbc488e713f4ca63ab49, type: 2}
|
||||||
m_StaticBatchInfo:
|
m_StaticBatchInfo:
|
||||||
firstSubMesh: 0
|
firstSubMesh: 0
|
||||||
subMeshCount: 0
|
subMeshCount: 0
|
||||||
|
|||||||
-51
@@ -367,7 +367,6 @@ GameObject:
|
|||||||
m_Component:
|
m_Component:
|
||||||
- component: {fileID: 3867316379480422796}
|
- component: {fileID: 3867316379480422796}
|
||||||
- component: {fileID: 1942933376864229506}
|
- component: {fileID: 1942933376864229506}
|
||||||
- component: {fileID: 1306787889985282719}
|
|
||||||
- component: {fileID: 1381459755705258711}
|
- component: {fileID: 1381459755705258711}
|
||||||
m_Layer: 0
|
m_Layer: 0
|
||||||
m_Name: DetectiveBoardVirtualCamera
|
m_Name: DetectiveBoardVirtualCamera
|
||||||
@@ -442,56 +441,6 @@ Camera:
|
|||||||
m_OcclusionCulling: 1
|
m_OcclusionCulling: 1
|
||||||
m_StereoConvergence: 10
|
m_StereoConvergence: 10
|
||||||
m_StereoSeparation: 0.022
|
m_StereoSeparation: 0.022
|
||||||
--- !u!114 &1306787889985282719
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 6184255431701975184}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 0
|
|
||||||
m_Script: {fileID: 11500000, guid: df440cd4d3cc30d4e9f5b44ab4931a12, type: 3}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier: Assembly-CSharp::Unheimlich.Gameplay.CameraControl.DetectiveBoardVirtualCamera
|
|
||||||
_virtualCamera: {fileID: 0}
|
|
||||||
_transitionSpeed: 3
|
|
||||||
_transitionCurve:
|
|
||||||
serializedVersion: 2
|
|
||||||
m_Curve:
|
|
||||||
- serializedVersion: 3
|
|
||||||
time: 0
|
|
||||||
value: 0
|
|
||||||
inSlope: 0
|
|
||||||
outSlope: 0
|
|
||||||
tangentMode: 0
|
|
||||||
weightedMode: 0
|
|
||||||
inWeight: 0
|
|
||||||
outWeight: 0
|
|
||||||
- serializedVersion: 3
|
|
||||||
time: 1
|
|
||||||
value: 1
|
|
||||||
inSlope: 0
|
|
||||||
outSlope: 0
|
|
||||||
tangentMode: 0
|
|
||||||
weightedMode: 0
|
|
||||||
inWeight: 0
|
|
||||||
outWeight: 0
|
|
||||||
m_PreInfinity: 2
|
|
||||||
m_PostInfinity: 2
|
|
||||||
m_RotationOrder: 4
|
|
||||||
_initialDistance: 1.5
|
|
||||||
_panSpeed: 2
|
|
||||||
_panSmoothTime: 0.2
|
|
||||||
_edgeBorderSize: 0.1
|
|
||||||
_maxPanOffset: {x: 1.5, y: 0.5}
|
|
||||||
_useDynamicPanLimits: 1
|
|
||||||
_panMargin: 0.05
|
|
||||||
_minDistance: 0.5
|
|
||||||
_maxDistance: 3
|
|
||||||
_zoomSpeed: 2
|
|
||||||
_zoomSmoothTime: 0.15
|
|
||||||
_scrollSensitivity: 0.3
|
|
||||||
--- !u!114 &1381459755705258711
|
--- !u!114 &1381459755705258711
|
||||||
MonoBehaviour:
|
MonoBehaviour:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 3720c98e23174d8418e04d8b445faed1
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!114 &11400000
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 0}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: b4c627a455825134087ba8c6f8931aed, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier: Assembly-CSharp::Unheimlich.Features.Hallucination.HallucinationEvent
|
||||||
|
_label: 'RoomB Entry Vision '
|
||||||
|
_spawnGhostWord: 1
|
||||||
|
_ghostWordText: 'Ils savent '
|
||||||
|
_ghostWordSpawnOffset: {x: 0, y: 1.5, z: 2}
|
||||||
|
_ghostWordLifetime: 3
|
||||||
|
_sanityCost: 0.2
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 11ffb12bd28a98f40a40a63db49d7df3
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,167 @@
|
|||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!114 &-8730880457597845575
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 3
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 0}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: 0b2db86121404754db890f4c8dfe81b2, type: 3}
|
||||||
|
m_Name: Bloom
|
||||||
|
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.Bloom
|
||||||
|
active: 1
|
||||||
|
skipIterations:
|
||||||
|
m_OverrideState: 0
|
||||||
|
m_Value: 1
|
||||||
|
threshold:
|
||||||
|
m_OverrideState: 1
|
||||||
|
m_Value: 1
|
||||||
|
intensity:
|
||||||
|
m_OverrideState: 1
|
||||||
|
m_Value: 6.07
|
||||||
|
scatter:
|
||||||
|
m_OverrideState: 1
|
||||||
|
m_Value: 1
|
||||||
|
clamp:
|
||||||
|
m_OverrideState: 1
|
||||||
|
m_Value: 65472
|
||||||
|
tint:
|
||||||
|
m_OverrideState: 1
|
||||||
|
m_Value: {r: 0.27672952, g: 0.27672952, b: 0.27672952, a: 1}
|
||||||
|
highQualityFiltering:
|
||||||
|
m_OverrideState: 1
|
||||||
|
m_Value: 0
|
||||||
|
filter:
|
||||||
|
m_OverrideState: 1
|
||||||
|
m_Value: 1
|
||||||
|
downscale:
|
||||||
|
m_OverrideState: 0
|
||||||
|
m_Value: 0
|
||||||
|
maxIterations:
|
||||||
|
m_OverrideState: 0
|
||||||
|
m_Value: 6
|
||||||
|
dirtTexture:
|
||||||
|
m_OverrideState: 0
|
||||||
|
m_Value: {fileID: 0}
|
||||||
|
dimension: 1
|
||||||
|
dirtIntensity:
|
||||||
|
m_OverrideState: 0
|
||||||
|
m_Value: 0
|
||||||
|
--- !u!114 &-8301503217428336494
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 3
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 0}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: 29fa0085f50d5e54f8144f766051a691, type: 3}
|
||||||
|
m_Name: FilmGrain
|
||||||
|
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.FilmGrain
|
||||||
|
active: 1
|
||||||
|
type:
|
||||||
|
m_OverrideState: 1
|
||||||
|
m_Value: 9
|
||||||
|
intensity:
|
||||||
|
m_OverrideState: 1
|
||||||
|
m_Value: 1
|
||||||
|
response:
|
||||||
|
m_OverrideState: 1
|
||||||
|
m_Value: 0.8
|
||||||
|
texture:
|
||||||
|
m_OverrideState: 0
|
||||||
|
m_Value: {fileID: 0}
|
||||||
|
--- !u!114 &11400000
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 0}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: d7fd9488000d3734a9e00ee676215985, type: 3}
|
||||||
|
m_Name: Hallucinations Volume Profile
|
||||||
|
m_EditorClassIdentifier: Unity.RenderPipelines.Core.Runtime::UnityEngine.Rendering.VolumeProfile
|
||||||
|
components:
|
||||||
|
- {fileID: 5048160815353275902}
|
||||||
|
- {fileID: 2293496333911105441}
|
||||||
|
- {fileID: 7109066975785212743}
|
||||||
|
- {fileID: -8301503217428336494}
|
||||||
|
- {fileID: -8730880457597845575}
|
||||||
|
--- !u!114 &2293496333911105441
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 3
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 0}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: c5e1dc532bcb41949b58bc4f2abfbb7e, type: 3}
|
||||||
|
m_Name: LensDistortion
|
||||||
|
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.LensDistortion
|
||||||
|
active: 1
|
||||||
|
intensity:
|
||||||
|
m_OverrideState: 1
|
||||||
|
m_Value: -0.66
|
||||||
|
xMultiplier:
|
||||||
|
m_OverrideState: 0
|
||||||
|
m_Value: 1
|
||||||
|
yMultiplier:
|
||||||
|
m_OverrideState: 0
|
||||||
|
m_Value: 1
|
||||||
|
center:
|
||||||
|
m_OverrideState: 0
|
||||||
|
m_Value: {x: 0.5, y: 0.5}
|
||||||
|
scale:
|
||||||
|
m_OverrideState: 0
|
||||||
|
m_Value: 1
|
||||||
|
--- !u!114 &5048160815353275902
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 3
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 0}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: 81180773991d8724ab7f2d216912b564, type: 3}
|
||||||
|
m_Name: ChromaticAberration
|
||||||
|
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.ChromaticAberration
|
||||||
|
active: 1
|
||||||
|
intensity:
|
||||||
|
m_OverrideState: 1
|
||||||
|
m_Value: 1
|
||||||
|
--- !u!114 &7109066975785212743
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 3
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 0}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: 899c54efeace73346a0a16faa3afe726, type: 3}
|
||||||
|
m_Name: Vignette
|
||||||
|
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.Vignette
|
||||||
|
active: 1
|
||||||
|
color:
|
||||||
|
m_OverrideState: 1
|
||||||
|
m_Value: {r: 0, g: 0, b: 0, a: 1}
|
||||||
|
center:
|
||||||
|
m_OverrideState: 0
|
||||||
|
m_Value: {x: 0.5, y: 0.5}
|
||||||
|
intensity:
|
||||||
|
m_OverrideState: 1
|
||||||
|
m_Value: 0.14
|
||||||
|
smoothness:
|
||||||
|
m_OverrideState: 1
|
||||||
|
m_Value: 0.385
|
||||||
|
rounded:
|
||||||
|
m_OverrideState: 0
|
||||||
|
m_Value: 0
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 1205a1bec127aa14aa7c48f120b84eb7
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.Events;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Unheimlich.Core.Events
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Generic reusable trigger component. Handles the once-flag, optional delay,
|
||||||
|
/// OnStart/OnEnable auto-fire, and exposes a UnityEvent + C# event for downstream systems.
|
||||||
|
/// Compose this on any GameObject that needs to fire a game event from multiple sources.
|
||||||
|
/// </summary>
|
||||||
|
public class GameEventTrigger : MonoBehaviour
|
||||||
|
{
|
||||||
|
[Header("Auto-fire")]
|
||||||
|
[Tooltip("Fire automatically on Start()")]
|
||||||
|
[SerializeField] private bool _fireOnStart;
|
||||||
|
|
||||||
|
[Tooltip("Fire automatically when this GameObject is enabled")]
|
||||||
|
[SerializeField] private bool _fireOnEnable;
|
||||||
|
|
||||||
|
[Header("Behaviour")]
|
||||||
|
[Tooltip("Delay in seconds before the event fires")]
|
||||||
|
[SerializeField] private float _delay;
|
||||||
|
|
||||||
|
[Tooltip("Prevent this trigger from firing more than once per Play Mode session")]
|
||||||
|
[SerializeField] private bool _fireOnlyOnce = true;
|
||||||
|
|
||||||
|
[Header("Callbacks")]
|
||||||
|
[Tooltip("Called when the trigger fires (after the optional delay)")]
|
||||||
|
[SerializeField] private UnityEvent _onTriggered;
|
||||||
|
|
||||||
|
/// <summary>C# event raised when the trigger fires. Useful for code-only listeners.</summary>
|
||||||
|
public event Action OnTriggered;
|
||||||
|
|
||||||
|
private bool _hasFired;
|
||||||
|
|
||||||
|
private bool _startFired;
|
||||||
|
|
||||||
|
private void OnEnable()
|
||||||
|
{
|
||||||
|
// Reset per-session so the trigger can fire again when entering Play Mode
|
||||||
|
// without Domain Reload.
|
||||||
|
_hasFired = false;
|
||||||
|
_startFired = false;
|
||||||
|
|
||||||
|
// NOTE: _fireOnEnable is intentionally deferred to Start() so all Singleton
|
||||||
|
// Awake() calls across the scene complete before listeners access managers.
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Start()
|
||||||
|
{
|
||||||
|
if (_startFired)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_startFired = true;
|
||||||
|
|
||||||
|
// Fire once even if both flags are ticked
|
||||||
|
if (_fireOnStart || _fireOnEnable)
|
||||||
|
Fire();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fires the trigger. Respects the once-flag and optional delay.
|
||||||
|
/// Call this from any external script, UnityEvent, or animation event.
|
||||||
|
/// </summary>
|
||||||
|
public void Fire()
|
||||||
|
{
|
||||||
|
if (_fireOnlyOnce && _hasFired)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (_delay > 0f)
|
||||||
|
Invoke(nameof(ExecuteTrigger), _delay);
|
||||||
|
else
|
||||||
|
ExecuteTrigger();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resets the fired state so the trigger can fire again.
|
||||||
|
/// </summary>
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
_hasFired = false;
|
||||||
|
CancelInvoke(nameof(ExecuteTrigger));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ExecuteTrigger()
|
||||||
|
{
|
||||||
|
if (_fireOnlyOnce && _hasFired)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_hasFired = true;
|
||||||
|
_onTriggered?.Invoke();
|
||||||
|
OnTriggered?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 358c75d041ea30a4ab85380893077e70
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Unheimlich.Core.Events
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Abstract base for any physics-trigger zone that fires when the player enters.
|
||||||
|
/// Handles collider validation, player tag check, once-flag, retrigger, and Gizmos.
|
||||||
|
/// Subclasses implement OnPlayerEntered() and optionally OnPlayerExited().
|
||||||
|
/// </summary>
|
||||||
|
[RequireComponent(typeof(Collider))]
|
||||||
|
public abstract class TriggerZoneBase : MonoBehaviour
|
||||||
|
{
|
||||||
|
[Header("Zone Settings")]
|
||||||
|
[SerializeField] private string _playerTag = "Player";
|
||||||
|
|
||||||
|
[Header("Gizmo")]
|
||||||
|
[SerializeField] private Color _gizmoColor = new Color(0.8f, 0.5f, 0.0f, 0.35f);
|
||||||
|
|
||||||
|
protected bool HasFired { get; private set; }
|
||||||
|
|
||||||
|
private Collider _collider;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Override to control whether the zone can fire again after the first trigger.
|
||||||
|
/// Default is false (fires once). Subclasses expose their own retrigger field and return it here.
|
||||||
|
/// </summary>
|
||||||
|
protected virtual bool IsRetriggerable => false;
|
||||||
|
|
||||||
|
protected virtual void Awake()
|
||||||
|
{
|
||||||
|
_collider = GetComponent<Collider>();
|
||||||
|
|
||||||
|
if (!_collider.isTrigger)
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"[{GetType().Name}] Collider on '{gameObject.name}' is not a trigger. Forcing it.", this);
|
||||||
|
_collider.isTrigger = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTriggerEnter(Collider other)
|
||||||
|
{
|
||||||
|
if (!other.CompareTag(_playerTag))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (HasFired && !IsRetriggerable)
|
||||||
|
return;
|
||||||
|
|
||||||
|
HasFired = true;
|
||||||
|
OnPlayerEntered(other);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTriggerExit(Collider other)
|
||||||
|
{
|
||||||
|
if (!other.CompareTag(_playerTag))
|
||||||
|
return;
|
||||||
|
|
||||||
|
OnPlayerExited(other);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Called when the player enters the trigger zone.</summary>
|
||||||
|
protected abstract void OnPlayerEntered(Collider playerCollider);
|
||||||
|
|
||||||
|
/// <summary>Called when the player exits the trigger zone. Override if needed.</summary>
|
||||||
|
protected virtual void OnPlayerExited(Collider playerCollider) { }
|
||||||
|
|
||||||
|
/// <summary>Resets the fired state so the zone can fire again.</summary>
|
||||||
|
public void ResetZone()
|
||||||
|
{
|
||||||
|
HasFired = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDrawGizmosSelected()
|
||||||
|
{
|
||||||
|
Gizmos.color = _gizmoColor;
|
||||||
|
Gizmos.matrix = transform.localToWorldMatrix;
|
||||||
|
|
||||||
|
Collider col = GetComponent<Collider>();
|
||||||
|
if (col is BoxCollider box)
|
||||||
|
Gizmos.DrawCube(box.center, box.size);
|
||||||
|
else if (col is SphereCollider sphere)
|
||||||
|
Gizmos.DrawSphere(sphere.center, sphere.radius);
|
||||||
|
else if (col is CapsuleCollider capsule)
|
||||||
|
Gizmos.DrawWireSphere(capsule.center, capsule.radius);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 9fae9f4c26344344e93baeaee0281284
|
||||||
@@ -41,6 +41,9 @@ namespace Unheimlich.Core.Interaction
|
|||||||
private bool _isActive = false;
|
private bool _isActive = false;
|
||||||
private bool _isMerged = false;
|
private bool _isMerged = false;
|
||||||
private bool _hasBeenMergedOnce = false;
|
private bool _hasBeenMergedOnce = false;
|
||||||
|
|
||||||
|
// Persists across OnEnable/OnDisable — only cleared by explicit ResetMerge()
|
||||||
|
// or at the start of a new Play Mode session via RuntimeInitializeOnLoadMethod.
|
||||||
private bool _hasCompletedMergeOnce = false;
|
private bool _hasCompletedMergeOnce = false;
|
||||||
private Vector3[] _originalSpawnPositions;
|
private Vector3[] _originalSpawnPositions;
|
||||||
private Vector3[] _basePositions;
|
private Vector3[] _basePositions;
|
||||||
@@ -51,7 +54,20 @@ namespace Unheimlich.Core.Interaction
|
|||||||
|
|
||||||
void Start()
|
void Start()
|
||||||
{
|
{
|
||||||
_playerTransform = PlayerController.Instance.transform;
|
InitializeReferences();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnEnable()
|
||||||
|
{
|
||||||
|
// With Scene Reload disabled, Start() doesn't run again between Play Mode sessions.
|
||||||
|
// Re-fetch references and reset transient state so animations restart cleanly.
|
||||||
|
InitializeReferences();
|
||||||
|
ResetSessionState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeReferences()
|
||||||
|
{
|
||||||
|
_playerTransform = PlayerController.Instance != null ? PlayerController.Instance.transform : null;
|
||||||
_mainCamera = Camera.main;
|
_mainCamera = Camera.main;
|
||||||
|
|
||||||
if (_playerTransform == null)
|
if (_playerTransform == null)
|
||||||
@@ -67,6 +83,41 @@ namespace Unheimlich.Core.Interaction
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ResetSessionState()
|
||||||
|
{
|
||||||
|
// Force-despawn any leftover words from the previous session
|
||||||
|
// (their GOs were destroyed but _spawnedWords list may still have stale entries).
|
||||||
|
if (_isActive)
|
||||||
|
{
|
||||||
|
foreach (var word in _spawnedWords)
|
||||||
|
{
|
||||||
|
if (word != null) Destroy(word.gameObject);
|
||||||
|
}
|
||||||
|
_spawnedWords.Clear();
|
||||||
|
|
||||||
|
if (_wordsContainer != null) Destroy(_wordsContainer);
|
||||||
|
_wordsContainer = null;
|
||||||
|
_wordsRotator = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_isActive = false;
|
||||||
|
_isMerged = false;
|
||||||
|
_hasBeenMergedOnce = false;
|
||||||
|
// _hasCompletedMergeOnce intentionally NOT reset here — persists during session
|
||||||
|
// so a merged object stays merged when the player re-enters its activation range.
|
||||||
|
// It is only reset at the start of a new Play Mode session (see OnPlayModeReset).
|
||||||
|
_currentConvergenceRadius = 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resets _hasCompletedMergeOnce at the very start of each Play Mode session,
|
||||||
|
// before any scene loads, so objects start fresh without full Domain Reload.
|
||||||
|
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
|
||||||
|
private static void OnPlayModeReset()
|
||||||
|
{
|
||||||
|
// Static reset — instance fields will be re-initialized by Awake/Start naturally.
|
||||||
|
// No instance access needed here; ResetSessionState() handles the rest on OnEnable.
|
||||||
|
}
|
||||||
|
|
||||||
void Update()
|
void Update()
|
||||||
{
|
{
|
||||||
if (_playerTransform == null || _settings == null) return;
|
if (_playerTransform == null || _settings == null) return;
|
||||||
|
|||||||
@@ -1,16 +1,45 @@
|
|||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace Unheimlich.Core
|
namespace Unheimlich.Core
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Clears all Singleton static instances at the start of every Play Mode session.
|
||||||
|
/// Necessary when Domain Reload is disabled in Enter Play Mode settings.
|
||||||
|
/// </summary>
|
||||||
|
internal static class SingletonRegistry
|
||||||
|
{
|
||||||
|
private static readonly List<Action> _resetCallbacks = new List<Action>();
|
||||||
|
|
||||||
|
public static void Register(Action resetCallback)
|
||||||
|
{
|
||||||
|
if (!_resetCallbacks.Contains(resetCallback))
|
||||||
|
_resetCallbacks.Add(resetCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
|
||||||
|
private static void ResetAll()
|
||||||
|
{
|
||||||
|
foreach (Action reset in _resetCallbacks)
|
||||||
|
reset?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public abstract class Singleton<T> : MonoBehaviour where T : MonoBehaviour
|
public abstract class Singleton<T> : MonoBehaviour where T : MonoBehaviour
|
||||||
{
|
{
|
||||||
public static T Instance { get; private set; }
|
public static T Instance { get; private set; }
|
||||||
|
|
||||||
|
static Singleton()
|
||||||
|
{
|
||||||
|
SingletonRegistry.Register(() => Instance = null);
|
||||||
|
}
|
||||||
|
|
||||||
protected virtual void Awake()
|
protected virtual void Awake()
|
||||||
{
|
{
|
||||||
if (Instance != null && Instance != this)
|
if (Instance != null && Instance != this)
|
||||||
{
|
{
|
||||||
Debug.LogWarning($"[{typeof(T).Name}] Multiple instances detected! Destroying duplicate on GameObject '{gameObject.name}'.");
|
Debug.LogWarning($"[{typeof(T).Name}] Multiple instances detected! Destroying duplicate on '{gameObject.name}'.");
|
||||||
Destroy(this);
|
Destroy(this);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -18,6 +47,16 @@ namespace Unheimlich.Core
|
|||||||
OnAwake();
|
OnAwake();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Re-registers the instance when entering Play Mode with Scene Reload disabled.
|
||||||
|
// SubsystemRegistration clears Instance before OnEnable runs, so we restore it here.
|
||||||
|
protected virtual void OnEnable()
|
||||||
|
{
|
||||||
|
if (Instance == null)
|
||||||
|
Instance = this as T;
|
||||||
|
|
||||||
|
OnEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
protected virtual void OnDestroy()
|
protected virtual void OnDestroy()
|
||||||
{
|
{
|
||||||
if (Instance == this)
|
if (Instance == this)
|
||||||
@@ -28,6 +67,7 @@ namespace Unheimlich.Core
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected virtual void OnAwake() { }
|
protected virtual void OnAwake() { }
|
||||||
|
protected virtual void OnEnabled() { }
|
||||||
protected virtual void OnDestroyInstance() { }
|
protected virtual void OnDestroyInstance() { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Unheimlich.Core.Utils
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Resets third-party static state that doesn't handle Domain Reload being disabled.
|
||||||
|
/// Runs automatically at the start of every Play Mode session before the scene loads.
|
||||||
|
/// </summary>
|
||||||
|
internal static class GameSessionResetter
|
||||||
|
{
|
||||||
|
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
|
||||||
|
private static void OnSessionStart()
|
||||||
|
{
|
||||||
|
ResetLeanTween();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ResetLeanTween()
|
||||||
|
{
|
||||||
|
// LeanTween holds a static tween array that persists when Domain Reload is disabled.
|
||||||
|
// Stale tween IDs from the previous session cause callbacks to fire on wrong objects,
|
||||||
|
// which breaks animations like organic shake on floating words.
|
||||||
|
LeanTween.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 34edfe05577b2a24892a90d9901cef39
|
||||||
+46
-53
@@ -1,13 +1,13 @@
|
|||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
using Unheimlich.Core.Events;
|
||||||
using Unheimlich.Features.DetectiveBoard.Data;
|
using Unheimlich.Features.DetectiveBoard.Data;
|
||||||
using Unheimlich.Features.DetectiveBoard;
|
|
||||||
|
|
||||||
namespace Unheimlich.Features.DetectiveBoard
|
namespace Unheimlich.Features.DetectiveBoard
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Generic trigger to unlock clues on the detective board.
|
/// Unlocks a clue on the detective board.
|
||||||
/// Can be triggered by any game event: dialogue, object interaction, audio cue, hallucination, etc.
|
/// Can be driven by an optional GameEventTrigger on the same GameObject,
|
||||||
/// Simply call UnlockClue() when the trigger condition is met.
|
/// or called directly via UnlockClue() from any external script or UnityEvent.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ClueUnlockTrigger : MonoBehaviour
|
public class ClueUnlockTrigger : MonoBehaviour
|
||||||
{
|
{
|
||||||
@@ -15,91 +15,84 @@ namespace Unheimlich.Features.DetectiveBoard
|
|||||||
[Tooltip("The clue data to unlock when triggered")]
|
[Tooltip("The clue data to unlock when triggered")]
|
||||||
[SerializeField] private ClueData _clueToUnlock;
|
[SerializeField] private ClueData _clueToUnlock;
|
||||||
|
|
||||||
[Header("Trigger Settings")]
|
[Header("Auto Trigger")]
|
||||||
[Tooltip("Automatically unlock on Start()")]
|
[Tooltip("Unlock automatically on Start()")]
|
||||||
[SerializeField] private bool _unlockOnStart;
|
[SerializeField] private bool _unlockOnStart;
|
||||||
|
|
||||||
[Tooltip("Unlock when this GameObject is enabled")]
|
[Tooltip("Delay in seconds before unlocking (only with Unlock On Start)")]
|
||||||
[SerializeField] private bool _unlockOnEnable;
|
|
||||||
|
|
||||||
[Tooltip("Delay before unlocking (useful for sequencing)")]
|
|
||||||
[SerializeField] private float _unlockDelay;
|
[SerializeField] private float _unlockDelay;
|
||||||
|
|
||||||
[Tooltip("Show debug messages")]
|
[Tooltip("Prevent unlocking more than once")]
|
||||||
|
[SerializeField] private bool _unlockOnlyOnce = true;
|
||||||
|
|
||||||
|
[Header("Debug")]
|
||||||
[SerializeField] private bool _debugMode;
|
[SerializeField] private bool _debugMode;
|
||||||
|
|
||||||
|
private GameEventTrigger _eventTrigger;
|
||||||
private bool _hasUnlocked;
|
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()
|
private void Start()
|
||||||
{
|
{
|
||||||
if (_unlockOnStart)
|
if (_unlockOnStart)
|
||||||
{
|
{
|
||||||
UnlockClue();
|
if (_unlockDelay > 0f)
|
||||||
|
Invoke(nameof(UnlockClue), _unlockDelay);
|
||||||
|
else
|
||||||
|
UnlockClue();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnEnable()
|
private void OnDestroy()
|
||||||
{
|
{
|
||||||
if (_unlockOnEnable && !_hasUnlocked)
|
if (_eventTrigger != null)
|
||||||
{
|
_eventTrigger.OnTriggered -= UnlockClue;
|
||||||
UnlockClue();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Unlocks the associated clue. Can be called from external scripts, events, or animations.
|
/// Unlocks the associated clue. Can be called directly from external scripts or UnityEvents.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void UnlockClue()
|
public void UnlockClue()
|
||||||
{
|
{
|
||||||
if (_hasUnlocked)
|
if (_unlockOnlyOnce && _hasUnlocked)
|
||||||
{
|
|
||||||
if (_debugMode)
|
|
||||||
Debug.LogWarning($"[ClueUnlockTrigger] Clue '{_clueToUnlock?.Title}' already unlocked!");
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
if (_clueToUnlock == null)
|
if (_clueToUnlock == null)
|
||||||
{
|
{
|
||||||
Debug.LogError("[ClueUnlockTrigger] No clue data assigned!");
|
Debug.LogError("[ClueUnlockTrigger] No clue data assigned!", this);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_unlockDelay > 0f)
|
if (DetectiveBoardManager.Instance == null)
|
||||||
{
|
{
|
||||||
Invoke(nameof(UnlockClueImmediate), _unlockDelay);
|
Debug.LogError("[ClueUnlockTrigger] DetectiveBoardManager not found!", this);
|
||||||
}
|
return;
|
||||||
else
|
|
||||||
{
|
|
||||||
UnlockClueImmediate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DetectiveBoardManager.Instance.UnlockClue(_clueToUnlock);
|
||||||
|
_hasUnlocked = true;
|
||||||
|
|
||||||
|
if (_debugMode)
|
||||||
|
Debug.Log($"[ClueUnlockTrigger] Unlocked clue: {_clueToUnlock.Title}", this);
|
||||||
|
|
||||||
|
OnClueUnlocked?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UnlockClueImmediate()
|
/// <summary>Resets the unlock state so the clue can be unlocked again.</summary>
|
||||||
{
|
|
||||||
if (DetectiveBoardManager.Instance != null)
|
|
||||||
{
|
|
||||||
DetectiveBoardManager.Instance.UnlockClue(_clueToUnlock);
|
|
||||||
_hasUnlocked = true;
|
|
||||||
|
|
||||||
if (_debugMode)
|
|
||||||
Debug.Log($"[ClueUnlockTrigger] Unlocked clue: {_clueToUnlock.Title}");
|
|
||||||
|
|
||||||
OnClueUnlocked?.Invoke();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Debug.LogError("[ClueUnlockTrigger] DetectiveBoardManager not found!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Resets the unlock state (useful for testing).
|
|
||||||
/// </summary>
|
|
||||||
public void ResetUnlock()
|
public void ResetUnlock()
|
||||||
{
|
{
|
||||||
_hasUnlocked = false;
|
_hasUnlocked = false;
|
||||||
|
CancelInvoke(nameof(UnlockClue));
|
||||||
}
|
}
|
||||||
|
|
||||||
public event System.Action OnClueUnlocked;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
@@ -138,6 +138,10 @@ namespace Unheimlich.Features.DetectiveBoard
|
|||||||
DetectiveBoardCanvas.Instance.BoardRenderer
|
DetectiveBoardCanvas.Instance.BoardRenderer
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Show clue hand fan when board is ready
|
||||||
|
if (ClueHandUI.Instance != null)
|
||||||
|
ClueHandUI.Instance.SetHandVisible(true);
|
||||||
|
|
||||||
if (_playerAlignmentSettings != null)
|
if (_playerAlignmentSettings != null)
|
||||||
cameraPan.Settings = originalSettings;
|
cameraPan.Settings = originalSettings;
|
||||||
});
|
});
|
||||||
@@ -159,6 +163,10 @@ namespace Unheimlich.Features.DetectiveBoard
|
|||||||
|
|
||||||
_isBoardActive = false;
|
_isBoardActive = false;
|
||||||
|
|
||||||
|
// Hide clue hand fan
|
||||||
|
if (ClueHandUI.Instance != null)
|
||||||
|
ClueHandUI.Instance.SetHandVisible(false);
|
||||||
|
|
||||||
DetectiveBoardVirtualCamera.Instance.Deactivate(() =>
|
DetectiveBoardVirtualCamera.Instance.Deactivate(() =>
|
||||||
{
|
{
|
||||||
var cameraPan = PlayerController.Instance?.CameraPanController;
|
var cameraPan = PlayerController.Instance?.CameraPanController;
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.EventSystems;
|
||||||
|
using Unheimlich.Features.DetectiveBoard.Data;
|
||||||
|
|
||||||
|
namespace Unheimlich.Features.DetectiveBoard.UI
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Wraps a PostItNode to behave as a card in the bottom-screen fan hand.
|
||||||
|
/// Handles hover pop animation and delegates click to ClueHandUI for board placement.
|
||||||
|
/// Add this component alongside PostItNode on the same GameObject.
|
||||||
|
/// </summary>
|
||||||
|
[RequireComponent(typeof(PostItNode))]
|
||||||
|
public class ClueHandCard : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler, IPointerClickHandler
|
||||||
|
{
|
||||||
|
[Header("Hover Settings")]
|
||||||
|
[SerializeField] private float _hoverRiseAmount = 60f;
|
||||||
|
[SerializeField] private float _hoverAnimSpeed = 8f;
|
||||||
|
|
||||||
|
private PostItNode _node;
|
||||||
|
private ClueHandUI _handUI;
|
||||||
|
|
||||||
|
// Fan resting state assigned by ClueHandUI
|
||||||
|
private Vector2 _restAnchoredPosition;
|
||||||
|
private float _restRotation;
|
||||||
|
|
||||||
|
// Runtime animation state
|
||||||
|
private Vector2 _currentAnchoredPosition;
|
||||||
|
private float _currentRotation;
|
||||||
|
private bool _isHovered;
|
||||||
|
private bool _isPlaced;
|
||||||
|
|
||||||
|
public ClueData ClueData => _node != null ? _node.ClueData : null;
|
||||||
|
public PostItNode Node => _node;
|
||||||
|
public bool IsPlaced => _isPlaced;
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
if (_node == null)
|
||||||
|
_node = GetComponent<PostItNode>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Binds this card to its parent hand manager.
|
||||||
|
/// </summary>
|
||||||
|
public void Initialize(PostItNode node, ClueHandUI handUI)
|
||||||
|
{
|
||||||
|
_node = node;
|
||||||
|
_handUI = handUI;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Assigns the resting fan slot (position + rotation) for this card.
|
||||||
|
/// </summary>
|
||||||
|
public void SetFanSlot(Vector2 anchoredPosition, float rotationZ)
|
||||||
|
{
|
||||||
|
_restAnchoredPosition = anchoredPosition;
|
||||||
|
_restRotation = rotationZ;
|
||||||
|
_currentAnchoredPosition = anchoredPosition;
|
||||||
|
_currentRotation = rotationZ;
|
||||||
|
|
||||||
|
RectTransform rt = _node.RectTransform;
|
||||||
|
rt.anchoredPosition = anchoredPosition;
|
||||||
|
rt.localRotation = Quaternion.Euler(0f, 0f, rotationZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Update()
|
||||||
|
{
|
||||||
|
if (_isPlaced)
|
||||||
|
return;
|
||||||
|
|
||||||
|
RectTransform rt = _node.RectTransform;
|
||||||
|
|
||||||
|
Vector2 targetPos = _isHovered
|
||||||
|
? _restAnchoredPosition + Vector2.up * _hoverRiseAmount
|
||||||
|
: _restAnchoredPosition;
|
||||||
|
|
||||||
|
float targetRot = _isHovered ? 0f : _restRotation;
|
||||||
|
|
||||||
|
_currentAnchoredPosition = Vector2.Lerp(_currentAnchoredPosition, targetPos, Time.deltaTime * _hoverAnimSpeed);
|
||||||
|
_currentRotation = Mathf.LerpAngle(_currentRotation, targetRot, Time.deltaTime * _hoverAnimSpeed);
|
||||||
|
|
||||||
|
rt.anchoredPosition = _currentAnchoredPosition;
|
||||||
|
rt.localRotation = Quaternion.Euler(0f, 0f, _currentRotation);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnPointerEnter(PointerEventData eventData)
|
||||||
|
{
|
||||||
|
if (_isPlaced)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_isHovered = true;
|
||||||
|
_handUI?.OnCardHovered(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnPointerExit(PointerEventData eventData)
|
||||||
|
{
|
||||||
|
if (_isPlaced)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_isHovered = false;
|
||||||
|
_handUI?.OnCardUnhovered(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnPointerClick(PointerEventData eventData)
|
||||||
|
{
|
||||||
|
if (_isPlaced || eventData.button != PointerEventData.InputButton.Left)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_isHovered = false;
|
||||||
|
_handUI?.OnCardSelected(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Hides this card from the hand after placement on the board.
|
||||||
|
/// </summary>
|
||||||
|
public void MarkAsPlaced()
|
||||||
|
{
|
||||||
|
_isPlaced = true;
|
||||||
|
gameObject.SetActive(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: dc7ce79e8bf16554fae5362980fec5b7
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Unheimlich.Core;
|
||||||
|
using Unheimlich.Features.DetectiveBoard.Data;
|
||||||
|
|
||||||
|
namespace Unheimlich.Features.DetectiveBoard.UI
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Manages the clue hand fan displayed at the bottom of the screen (Screen Space canvas).
|
||||||
|
/// Reuses the PostItNode prefab for each card — ClueHandCard is added at runtime.
|
||||||
|
/// Hovering a card pops it up and straightens it; clicking starts a lerp-depth
|
||||||
|
/// placement animation then hands the clue to DetectiveBoardCanvas.
|
||||||
|
/// </summary>
|
||||||
|
public class ClueHandUI : Singleton<ClueHandUI>
|
||||||
|
{
|
||||||
|
[Header("References")]
|
||||||
|
[SerializeField] private GameObject _postItNodePrefab;
|
||||||
|
[SerializeField] private RectTransform _handContainer;
|
||||||
|
|
||||||
|
[Header("Fan Layout Settings")]
|
||||||
|
[SerializeField] private float _cardSpacingPixels = 80f;
|
||||||
|
[SerializeField] private float _fanArcRadius = 800f;
|
||||||
|
[SerializeField] private float _cardBaseY = -120f;
|
||||||
|
[SerializeField] private float _maxFanHalfAngle = 40f;
|
||||||
|
|
||||||
|
[Header("Placement Settings")]
|
||||||
|
[SerializeField] private float _placementLerpSpeed = 5f;
|
||||||
|
|
||||||
|
private readonly List<ClueHandCard> _handCards = new List<ClueHandCard>();
|
||||||
|
private bool _isPlacing; protected override void OnAwake() { }
|
||||||
|
|
||||||
|
protected override void OnEnabled()
|
||||||
|
{
|
||||||
|
// With Scene Reload disabled, stale card references from the previous session
|
||||||
|
// may point to destroyed GameObjects. Clear and rebuild from manager state.
|
||||||
|
if (_handCards.Count > 0)
|
||||||
|
{
|
||||||
|
foreach (ClueHandCard card in _handCards)
|
||||||
|
{
|
||||||
|
if (card != null && card.gameObject != null)
|
||||||
|
Destroy(card.gameObject);
|
||||||
|
}
|
||||||
|
_handCards.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DetectiveBoardManager.Instance != null)
|
||||||
|
{
|
||||||
|
foreach (var clue in DetectiveBoardManager.Instance.GetUnlockedClues())
|
||||||
|
{
|
||||||
|
// Skip clues already placed on the board — they must not reappear in the hand
|
||||||
|
if (DetectiveBoardCanvas.Instance != null && DetectiveBoardCanvas.Instance.HasNodeForClue(clue.ClueID))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
AddClueCard(clue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Start()
|
||||||
|
{
|
||||||
|
if (DetectiveBoardManager.Instance != null)
|
||||||
|
{
|
||||||
|
DetectiveBoardManager.Instance.OnClueUnlocked += OnClueUnlocked;
|
||||||
|
|
||||||
|
foreach (var clue in DetectiveBoardManager.Instance.GetUnlockedClues())
|
||||||
|
AddClueCard(clue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start hidden — shown when the board activates
|
||||||
|
SetHandVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDestroy()
|
||||||
|
{
|
||||||
|
if (DetectiveBoardManager.Instance != null)
|
||||||
|
DetectiveBoardManager.Instance.OnClueUnlocked -= OnClueUnlocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnClueUnlocked(ClueData clueData)
|
||||||
|
{
|
||||||
|
AddClueCard(clueData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────────────
|
||||||
|
// Card management
|
||||||
|
// ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Instantiates a PostItNode prefab, injects ClueHandCard, and adds it to the fan.
|
||||||
|
/// </summary>
|
||||||
|
public void AddClueCard(ClueData clueData)
|
||||||
|
{
|
||||||
|
if (_postItNodePrefab == null || _handContainer == null)
|
||||||
|
{
|
||||||
|
Debug.LogError("[ClueHandUI] Missing _postItNodePrefab or _handContainer reference.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_handCards.Exists(c => c.ClueData != null && c.ClueData.ClueID == clueData.ClueID))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Instantiate the PostItNode prefab inside the screen-space hand container
|
||||||
|
GameObject cardObj = Instantiate(_postItNodePrefab, _handContainer);
|
||||||
|
|
||||||
|
// PostItNode is nested one level inside "Canvas (Environment)" wrapper in the prefab
|
||||||
|
// — grab it directly
|
||||||
|
PostItNode node = cardObj.GetComponentInChildren<PostItNode>(true);
|
||||||
|
if (node == null)
|
||||||
|
{
|
||||||
|
Debug.LogError("[ClueHandUI] PostItNode not found on prefab.");
|
||||||
|
Destroy(cardObj);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a dummy NodeData for the hand display (position handled by fan layout)
|
||||||
|
PostItNodeData dummyData = new PostItNodeData(clueData.ClueID, Vector2.zero);
|
||||||
|
node.Initialize(clueData, dummyData);
|
||||||
|
|
||||||
|
// Add ClueHandCard wrapper on the PostItNode GameObject
|
||||||
|
ClueHandCard card = node.gameObject.AddComponent<ClueHandCard>();
|
||||||
|
card.Initialize(node, this);
|
||||||
|
|
||||||
|
// Move the PostItNode GameObject up to be a direct child of _handContainer
|
||||||
|
// so its RectTransform is in screen space
|
||||||
|
node.transform.SetParent(_handContainer, false);
|
||||||
|
|
||||||
|
// Destroy the now-empty prefab root (the "Canvas (Environment)" wrapper)
|
||||||
|
if (cardObj != node.gameObject)
|
||||||
|
Destroy(cardObj);
|
||||||
|
|
||||||
|
// Normalize scale — PostItNode prefab uses 0.5 scale for world space, reset for screen space
|
||||||
|
node.RectTransform.localScale = Vector3.one;
|
||||||
|
|
||||||
|
_handCards.Add(card);
|
||||||
|
RebuildFanLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Recomputes fan arc positions and rotations for all non-placed cards.
|
||||||
|
/// Uses fixed pixel spacing between cards, capped by a max half-angle.
|
||||||
|
/// </summary>
|
||||||
|
private void RebuildFanLayout()
|
||||||
|
{
|
||||||
|
List<ClueHandCard> visible = _handCards.FindAll(c => !c.IsPlaced);
|
||||||
|
int count = visible.Count;
|
||||||
|
|
||||||
|
if (count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Derive the angle step from pixel spacing on the arc
|
||||||
|
float angleStepDeg = (_cardSpacingPixels / _fanArcRadius) * Mathf.Rad2Deg;
|
||||||
|
|
||||||
|
// Total spread is (count-1) steps, capped so it never exceeds _maxFanHalfAngle on each side
|
||||||
|
float totalHalfAngle = angleStepDeg * (count - 1) * 0.5f;
|
||||||
|
float clampedHalfAngle = Mathf.Min(totalHalfAngle, _maxFanHalfAngle);
|
||||||
|
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
float t = count == 1 ? 0.5f : (float)i / (count - 1);
|
||||||
|
float angle = Mathf.Lerp(-clampedHalfAngle, clampedHalfAngle, t);
|
||||||
|
float angleRad = angle * Mathf.Deg2Rad;
|
||||||
|
|
||||||
|
float x = _fanArcRadius * Mathf.Sin(angleRad);
|
||||||
|
float y = _cardBaseY - _fanArcRadius * (1f - Mathf.Cos(angleRad));
|
||||||
|
|
||||||
|
visible[i].SetFanSlot(new Vector2(x, y), -angle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────────────
|
||||||
|
// Hover callbacks
|
||||||
|
// ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>Brings the hovered card in front of its siblings.</summary>
|
||||||
|
public void OnCardHovered(ClueHandCard card)
|
||||||
|
{
|
||||||
|
card.transform.SetAsLastSibling();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Restores natural sibling order after hover ends.</summary>
|
||||||
|
public void OnCardUnhovered(ClueHandCard card)
|
||||||
|
{
|
||||||
|
RebuildSiblingOrder();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RebuildSiblingOrder()
|
||||||
|
{
|
||||||
|
int index = 0;
|
||||||
|
foreach (ClueHandCard c in _handCards)
|
||||||
|
{
|
||||||
|
if (!c.IsPlaced)
|
||||||
|
c.transform.SetSiblingIndex(index++);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────────────
|
||||||
|
// Selection & placement
|
||||||
|
// ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when the player clicks a card. Creates the node on the board
|
||||||
|
/// directly under the mouse cursor and immediately starts drag mode.
|
||||||
|
/// </summary>
|
||||||
|
public void OnCardSelected(ClueHandCard card)
|
||||||
|
{
|
||||||
|
if (_isPlacing)
|
||||||
|
return;
|
||||||
|
|
||||||
|
DetectiveBoardCanvas boardCanvas = DetectiveBoardCanvas.Instance;
|
||||||
|
if (boardCanvas == null)
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[ClueHandUI] DetectiveBoardCanvas not found.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DetectiveBoardInteraction interaction = DetectiveBoardInteraction.Instance;
|
||||||
|
if (interaction == null)
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[ClueHandUI] DetectiveBoardInteraction not found.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
boardCanvas.AddClueNode(card.ClueData);
|
||||||
|
|
||||||
|
PostItNode boardNode = boardCanvas.GetNodeForClue(card.ClueData.ClueID);
|
||||||
|
if (boardNode == null)
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[ClueHandUI] Node not found on board after AddClueNode.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position node under the mouse on the board canvas plane, then start drag
|
||||||
|
interaction.SpawnNodeUnderMouseAndDrag(boardNode);
|
||||||
|
|
||||||
|
card.MarkAsPlaced();
|
||||||
|
RebuildFanLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────────────
|
||||||
|
// Visibility
|
||||||
|
// ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shows or hides the fan hand. Called by DetectiveBoardController.
|
||||||
|
/// </summary>
|
||||||
|
public void SetHandVisible(bool visible)
|
||||||
|
{
|
||||||
|
if (_handContainer != null)
|
||||||
|
_handContainer.gameObject.SetActive(visible);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ace0b6173dad68f47a97505680f54d4e
|
||||||
@@ -131,45 +131,11 @@ namespace Unheimlich.Features.DetectiveBoard.UI
|
|||||||
_saveData = new DetectiveBoardSaveData();
|
_saveData = new DetectiveBoardSaveData();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnEnable()
|
private void OnEnable() { }
|
||||||
{
|
|
||||||
TrySubscribeToManager();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Start()
|
private void Start() { }
|
||||||
{
|
|
||||||
TrySubscribeToManager();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void TrySubscribeToManager()
|
private void OnDisable() { }
|
||||||
{
|
|
||||||
if (DetectiveBoardManager.Instance != null)
|
|
||||||
{
|
|
||||||
DetectiveBoardManager.Instance.OnClueUnlocked -= OnClueUnlocked;
|
|
||||||
DetectiveBoardManager.Instance.OnClueUnlocked += OnClueUnlocked;
|
|
||||||
|
|
||||||
foreach (var clue in DetectiveBoardManager.Instance.GetUnlockedClues())
|
|
||||||
{
|
|
||||||
if (!_activeNodes.ContainsKey(clue.ClueID))
|
|
||||||
{
|
|
||||||
AddClueNode(clue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDisable()
|
|
||||||
{
|
|
||||||
if (DetectiveBoardManager.Instance != null)
|
|
||||||
{
|
|
||||||
DetectiveBoardManager.Instance.OnClueUnlocked -= OnClueUnlocked;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnClueUnlocked(ClueData clueData)
|
|
||||||
{
|
|
||||||
AddClueNode(clueData);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Update()
|
private void Update()
|
||||||
{
|
{
|
||||||
@@ -413,6 +379,19 @@ namespace Unheimlich.Features.DetectiveBoard.UI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the active PostItNode for the given clue ID, or null if not found.
|
||||||
|
/// Used by ClueHandUI to retrieve the node after placement.
|
||||||
|
/// </summary>
|
||||||
|
/// <summary>Returns true if a node for this clue ID is already placed on the board.</summary>
|
||||||
|
public bool HasNodeForClue(string clueID) => _activeNodes.ContainsKey(clueID);
|
||||||
|
|
||||||
|
public PostItNode GetNodeForClue(string clueID)
|
||||||
|
{
|
||||||
|
_activeNodes.TryGetValue(clueID, out PostItNode node);
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
public DetectiveBoardSaveData GetSaveData()
|
public DetectiveBoardSaveData GetSaveData()
|
||||||
{
|
{
|
||||||
return _saveData;
|
return _saveData;
|
||||||
|
|||||||
+39
-1
@@ -1,6 +1,7 @@
|
|||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.InputSystem;
|
using UnityEngine.InputSystem;
|
||||||
using UnityEngine.UI;
|
using UnityEngine.UI;
|
||||||
|
using Unheimlich.Core;
|
||||||
using Unheimlich.Features.DetectiveBoard;
|
using Unheimlich.Features.DetectiveBoard;
|
||||||
using Unheimlich.Features.Player;
|
using Unheimlich.Features.Player;
|
||||||
using Unheimlich.World.CameraSystem;
|
using Unheimlich.World.CameraSystem;
|
||||||
@@ -14,7 +15,7 @@ namespace Unheimlich.Features.DetectiveBoard.UI
|
|||||||
/// Manages hover, selection, dragging, and click forwarding for Post-It nodes.
|
/// Manages hover, selection, dragging, and click forwarding for Post-It nodes.
|
||||||
/// Interactions are only active when the board is in inspection mode.
|
/// Interactions are only active when the board is in inspection mode.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class DetectiveBoardInteraction : MonoBehaviour
|
public class DetectiveBoardInteraction : Singleton<DetectiveBoardInteraction>
|
||||||
{
|
{
|
||||||
[Header("Settings")]
|
[Header("Settings")]
|
||||||
[SerializeField] private LayerMask _boardLayer;
|
[SerializeField] private LayerMask _boardLayer;
|
||||||
@@ -33,6 +34,8 @@ namespace Unheimlich.Features.DetectiveBoard.UI
|
|||||||
private Plane _canvasPlane;
|
private Plane _canvasPlane;
|
||||||
private Vector3 _dragOffset;
|
private Vector3 _dragOffset;
|
||||||
|
|
||||||
|
protected override void OnAwake() { }
|
||||||
|
|
||||||
private void Start()
|
private void Start()
|
||||||
{
|
{
|
||||||
_boardCanvas = GetComponent<Canvas>();
|
_boardCanvas = GetComponent<Canvas>();
|
||||||
@@ -349,6 +352,41 @@ namespace Unheimlich.Features.DetectiveBoard.UI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Positions a freshly-created board node under the current mouse cursor
|
||||||
|
/// and immediately enters drag mode on it, so the player can place it freely.
|
||||||
|
/// Called by ClueHandUI after AddClueNode.
|
||||||
|
/// </summary>
|
||||||
|
public void SpawnNodeUnderMouseAndDrag(PostItNode node)
|
||||||
|
{
|
||||||
|
if (node == null || _camera == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
UpdateCanvasPlane();
|
||||||
|
|
||||||
|
Vector2 mousePos = Mouse.current != null
|
||||||
|
? Mouse.current.position.ReadValue()
|
||||||
|
: new Vector2(Screen.width * 0.5f, Screen.height * 0.5f);
|
||||||
|
|
||||||
|
Ray ray = _camera.ScreenPointToRay(new Vector3(mousePos.x, mousePos.y, 0f));
|
||||||
|
|
||||||
|
if (_canvasPlane.Raycast(ray, out float enter))
|
||||||
|
{
|
||||||
|
Vector3 worldHitPoint = ray.GetPoint(enter);
|
||||||
|
Vector3 localPos = _boardCanvas.transform.InverseTransformPoint(worldHitPoint);
|
||||||
|
node.SetPosition(new Vector2(localPos.x, localPos.y));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Begin drag with zero offset so the node centre tracks the mouse exactly
|
||||||
|
if (_selectedNode != null)
|
||||||
|
_selectedNode.SetSelected(false);
|
||||||
|
|
||||||
|
_selectedNode = node;
|
||||||
|
_selectedNode.SetSelected(true);
|
||||||
|
_isDragging = true;
|
||||||
|
_dragOffset = Vector3.zero;
|
||||||
|
}
|
||||||
|
|
||||||
private PostItNode RaycastForNode()
|
private PostItNode RaycastForNode()
|
||||||
{
|
{
|
||||||
if (_camera == null || Mouse.current == null)
|
if (_camera == null || Mouse.current == null)
|
||||||
|
|||||||
@@ -53,8 +53,35 @@ namespace Unheimlich.Features.Dialogue
|
|||||||
private float _autoCloseTimer = 0f;
|
private float _autoCloseTimer = 0f;
|
||||||
private bool _isWaitingForAutoClose = false;
|
private bool _isWaitingForAutoClose = false;
|
||||||
|
|
||||||
protected override void OnAwake()
|
protected override void OnAwake() { }
|
||||||
|
|
||||||
|
protected override void OnEnabled()
|
||||||
{
|
{
|
||||||
|
// With Scene Reload disabled, Start() doesn't run again each session.
|
||||||
|
// We recreate the focus system to clear any stale locked state (_isFocusLocked,
|
||||||
|
// _focusTimer, _currentFocusedHandler) that may have carried over.
|
||||||
|
if (_focusSystem != null)
|
||||||
|
{
|
||||||
|
_mainCamera = Camera.main;
|
||||||
|
_focusSystem = new DialogueFocusSystem(
|
||||||
|
this,
|
||||||
|
_mainCamera,
|
||||||
|
PlayerController.Instance?.CameraEffects,
|
||||||
|
_choiceLayer,
|
||||||
|
_focusRange,
|
||||||
|
_focusTime,
|
||||||
|
_raycastInterval,
|
||||||
|
_recoveryDuration,
|
||||||
|
_focusRetentionTime,
|
||||||
|
_fallbackShakeIntensity
|
||||||
|
);
|
||||||
|
_focusSystem.OnChoiceMade += (choice) => OnChoiceMade?.Invoke(choice);
|
||||||
|
|
||||||
|
// Also reset dialogue state in case we exited mid-dialogue
|
||||||
|
IsInDialogue = false;
|
||||||
|
_isWaitingForAutoClose = false;
|
||||||
|
_autoCloseTimer = 0f;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Start()
|
private void Start()
|
||||||
@@ -82,6 +109,13 @@ namespace Unheimlich.Features.Dialogue
|
|||||||
if (!IsInDialogue)
|
if (!IsInDialogue)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (_activeDialogueUI == null)
|
||||||
|
{
|
||||||
|
Debug.LogError("[DialogueManager] IsInDialogue is true but _activeDialogueUI is null. Forcing dialogue end.", this);
|
||||||
|
IsInDialogue = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!_activeDialogueUI.AreChoicesVisible())
|
if (!_activeDialogueUI.AreChoicesVisible())
|
||||||
{
|
{
|
||||||
if (_isWaitingForAutoClose && _currentNode != null && _currentNode.AutoCloseDelay > 0f)
|
if (_isWaitingForAutoClose && _currentNode != null && _currentNode.AutoCloseDelay > 0f)
|
||||||
@@ -142,6 +176,12 @@ namespace Unheimlich.Features.Dialogue
|
|||||||
_activeDialogueUI = customDialogueUI;
|
_activeDialogueUI = customDialogueUI;
|
||||||
CurrentOwner = owner;
|
CurrentOwner = owner;
|
||||||
|
|
||||||
|
if (_activeDialogueUI == null)
|
||||||
|
{
|
||||||
|
Debug.LogError("[DialogueManager] StartConversation called with no DialogueUI. Assign a Local Dialogue UI on the DialogueTriggerZone that triggered this conversation.", this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ShowCurrentNode();
|
ShowCurrentNode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using UnityEngine;
|
|||||||
using Unheimlich.Features.Dialogue.Data;
|
using Unheimlich.Features.Dialogue.Data;
|
||||||
using Unheimlich.Features.Dialogue;
|
using Unheimlich.Features.Dialogue;
|
||||||
using Unheimlich.Features.Dialogue.UI;
|
using Unheimlich.Features.Dialogue.UI;
|
||||||
|
using Unheimlich.Core.Events;
|
||||||
|
|
||||||
namespace Unheimlich.Features.Dialogue
|
namespace Unheimlich.Features.Dialogue
|
||||||
{
|
{
|
||||||
@@ -9,9 +10,8 @@ namespace Unheimlich.Features.Dialogue
|
|||||||
/// Trigger zone that starts a dialogue sequence when player enters.
|
/// Trigger zone that starts a dialogue sequence when player enters.
|
||||||
/// Supports pause/resume on exit/enter and optional one-time triggering.
|
/// Supports pause/resume on exit/enter and optional one-time triggering.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[RequireComponent(typeof(Collider))]
|
public class DialogueTriggerZone : TriggerZoneBase
|
||||||
public class DialogueTriggerZone : MonoBehaviour
|
{
|
||||||
{
|
|
||||||
[Header("Dialogue Reference")]
|
[Header("Dialogue Reference")]
|
||||||
[Tooltip("DialogueConversation to use for this trigger zone")]
|
[Tooltip("DialogueConversation to use for this trigger zone")]
|
||||||
[SerializeField] private DialogueConversation _conversation;
|
[SerializeField] private DialogueConversation _conversation;
|
||||||
@@ -19,8 +19,8 @@ namespace Unheimlich.Features.Dialogue
|
|||||||
[Header("Trigger Settings")]
|
[Header("Trigger Settings")]
|
||||||
[Tooltip("Can the dialogue be retriggered after completion")]
|
[Tooltip("Can the dialogue be retriggered after completion")]
|
||||||
[SerializeField] private bool _isRetriggerable = false;
|
[SerializeField] private bool _isRetriggerable = false;
|
||||||
[SerializeField] private bool _requirePlayerTag = true;
|
|
||||||
[SerializeField] private string _playerTag = "Player";
|
protected override bool IsRetriggerable => _isRetriggerable;
|
||||||
|
|
||||||
[Header("Distance Check")]
|
[Header("Distance Check")]
|
||||||
[Tooltip("Pause dialogue if player exits the trigger zone")]
|
[Tooltip("Pause dialogue if player exits the trigger zone")]
|
||||||
@@ -41,29 +41,19 @@ namespace Unheimlich.Features.Dialogue
|
|||||||
[SerializeField] private bool _enableBlurEffect = true;
|
[SerializeField] private bool _enableBlurEffect = true;
|
||||||
|
|
||||||
private bool _hasTriggered = false;
|
private bool _hasTriggered = false;
|
||||||
private Collider _triggerCollider;
|
|
||||||
private DialogueStateHandler _dialogueState;
|
private DialogueStateHandler _dialogueState;
|
||||||
|
|
||||||
private void Awake()
|
protected override void Awake()
|
||||||
{
|
{
|
||||||
_triggerCollider = GetComponent<Collider>();
|
base.Awake();
|
||||||
_dialogueState = new DialogueStateHandler(this);
|
_dialogueState = new DialogueStateHandler(this);
|
||||||
|
|
||||||
if (!_triggerCollider.isTrigger)
|
|
||||||
{
|
|
||||||
Debug.LogWarning($"{gameObject.name}: Collider should be marked as Trigger!", this);
|
|
||||||
_triggerCollider.isTrigger = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_floatingWordAnchor == null)
|
if (_floatingWordAnchor == null)
|
||||||
_floatingWordAnchor = transform;
|
_floatingWordAnchor = transform;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnTriggerEnter(Collider other)
|
protected override void OnPlayerEntered(Collider playerCollider)
|
||||||
{
|
{
|
||||||
if (_requirePlayerTag && !other.CompareTag(_playerTag))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (_dialogueState.IsCompleted && !_isRetriggerable)
|
if (_dialogueState.IsCompleted && !_isRetriggerable)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -80,11 +70,8 @@ namespace Unheimlich.Features.Dialogue
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnTriggerExit(Collider other)
|
protected override void OnPlayerExited(Collider playerCollider)
|
||||||
{
|
{
|
||||||
if (_requirePlayerTag && !other.CompareTag(_playerTag))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (_pauseOnExit && DialogueManager.Instance != null && DialogueManager.Instance.IsDialogueActive)
|
if (_pauseOnExit && DialogueManager.Instance != null && DialogueManager.Instance.IsDialogueActive)
|
||||||
{
|
{
|
||||||
if ((Object)DialogueManager.Instance.CurrentOwner == this)
|
if ((Object)DialogueManager.Instance.CurrentOwner == this)
|
||||||
@@ -153,6 +140,7 @@ namespace Unheimlich.Features.Dialogue
|
|||||||
{
|
{
|
||||||
_hasTriggered = false;
|
_hasTriggered = false;
|
||||||
_dialogueState.Reset();
|
_dialogueState.Reset();
|
||||||
|
ResetZone();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnDestroy()
|
private void OnDestroy()
|
||||||
@@ -161,20 +149,14 @@ namespace Unheimlich.Features.Dialogue
|
|||||||
DialogueManager.Instance.OnDialogueCompleted -= OnDialogueCompleted;
|
DialogueManager.Instance.OnDialogueCompleted -= OnDialogueCompleted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnValidate()
|
||||||
|
{
|
||||||
|
if (_localDialogueUI == null)
|
||||||
|
Debug.LogWarning($"[DialogueTriggerZone] '{gameObject.name}' has no Local Dialogue UI assigned. Assign the appropriate DialogueUI prefab variant (World, NPC, Room, Screen, etc.).", this);
|
||||||
|
}
|
||||||
|
|
||||||
private void OnDrawGizmosSelected()
|
private void OnDrawGizmosSelected()
|
||||||
{
|
{
|
||||||
Collider col = GetComponent<Collider>();
|
|
||||||
if (col != null)
|
|
||||||
{
|
|
||||||
Gizmos.color = _hasTriggered ? Color.gray : Color.yellow;
|
|
||||||
Gizmos.matrix = transform.localToWorldMatrix;
|
|
||||||
|
|
||||||
if (col is BoxCollider box)
|
|
||||||
Gizmos.DrawWireCube(box.center, box.size);
|
|
||||||
else if (col is SphereCollider sphere)
|
|
||||||
Gizmos.DrawWireSphere(sphere.center, sphere.radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_floatingWordAnchor != null)
|
if (_floatingWordAnchor != null)
|
||||||
{
|
{
|
||||||
Gizmos.color = Color.cyan;
|
Gizmos.color = Color.cyan;
|
||||||
|
|||||||
@@ -253,7 +253,8 @@ namespace Unheimlich.Features.Dialogue.UI
|
|||||||
{
|
{
|
||||||
if (!_positionCaptured)
|
if (!_positionCaptured)
|
||||||
{
|
{
|
||||||
_startLocalPos = transform.localPosition;
|
// Subtract shake offset to get the clean base position, not the shaken one
|
||||||
|
_startLocalPos = transform.localPosition - _shakeOffset;
|
||||||
_positionCaptured = true;
|
_positionCaptured = true;
|
||||||
|
|
||||||
LeanTween.cancel(gameObject);
|
LeanTween.cancel(gameObject);
|
||||||
@@ -516,13 +517,15 @@ namespace Unheimlich.Features.Dialogue.UI
|
|||||||
|
|
||||||
ResetSketchFade();
|
ResetSketchFade();
|
||||||
|
|
||||||
TweenHelper.MoveLocal(gameObject, _startLocalPos, duration, () => {
|
// Reset position capture and restart shake immediately — don't rely on a LeanTween
|
||||||
_positionCaptured = false;
|
// callback for this, since stale tween IDs (Domain Reload disabled) can silently
|
||||||
|
// drop callbacks, leaving some words permanently non-shaking.
|
||||||
|
_positionCaptured = false;
|
||||||
|
StartOrganicShake();
|
||||||
|
|
||||||
|
TweenHelper.MoveLocal(gameObject, _startLocalPos, duration, () => {
|
||||||
if (DialogueManager.Instance != null)
|
if (DialogueManager.Instance != null)
|
||||||
DialogueManager.Instance.UnlockFocus();
|
DialogueManager.Instance.UnlockFocus();
|
||||||
|
|
||||||
StartOrganicShake();
|
|
||||||
}, LeanTweenType.easeOutQuad);
|
}, LeanTweenType.easeOutQuad);
|
||||||
|
|
||||||
transform.localScale = Vector3.one * _originalScale;
|
transform.localScale = Vector3.one * _originalScale;
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 61bef91a8c6020442b3c036173f37714
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: f73b9ba084838014a956e0a0a4150ab7
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Unheimlich.Features.Hallucination
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// ScriptableObject describing a discrete hallucination event.
|
||||||
|
/// Configure in the inspector and pass to HallucinationManager.TriggerHallucinationEvent().
|
||||||
|
/// </summary>
|
||||||
|
[CreateAssetMenu(fileName = "HallucinationEvent", menuName = "Unheimlich/Hallucination/Hallucination Event")]
|
||||||
|
public class HallucinationEvent : ScriptableObject
|
||||||
|
{
|
||||||
|
[Header("Identity")]
|
||||||
|
[Tooltip("Human-readable label for this event (used in logs and the inspector)")]
|
||||||
|
[SerializeField] private string _label;
|
||||||
|
|
||||||
|
[Header("Ghost Word")]
|
||||||
|
[Tooltip("Spawn a ghost word that moves toward the player")]
|
||||||
|
[SerializeField] private bool _spawnGhostWord;
|
||||||
|
|
||||||
|
[Tooltip("Text displayed by the ghost word")]
|
||||||
|
[SerializeField] private string _ghostWordText;
|
||||||
|
|
||||||
|
[Tooltip("World-space spawn position offset from the anchor point")]
|
||||||
|
[SerializeField] private Vector3 _ghostWordSpawnOffset = new Vector3(0f, 1.5f, 2f);
|
||||||
|
|
||||||
|
[Tooltip("Duration before the ghost word disappears (seconds)")]
|
||||||
|
[SerializeField] private float _ghostWordLifetime = 3f;
|
||||||
|
|
||||||
|
[Header("Sanity Cost")]
|
||||||
|
[Tooltip("Sanity added to the player when this event fires (0 = no change)")]
|
||||||
|
[SerializeField, Range(0f, 1f)] private float _sanityCost = 0f;
|
||||||
|
|
||||||
|
// ─── Public Accessors ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public string Label => _label;
|
||||||
|
public bool SpawnGhostWord => _spawnGhostWord;
|
||||||
|
public string GhostWordText => _ghostWordText;
|
||||||
|
public Vector3 GhostWordSpawnOffset => _ghostWordSpawnOffset;
|
||||||
|
public float GhostWordLifetime => _ghostWordLifetime;
|
||||||
|
public float SanityCost => _sanityCost;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b4c627a455825134087ba8c6f8931aed
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 9bb36c8cb81b7fd49b9612c4fa8f8c80
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,272 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.Rendering;
|
||||||
|
using UnityEngine.Rendering.Universal;
|
||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using Unheimlich.Core;
|
||||||
|
|
||||||
|
namespace Unheimlich.Features.Hallucination
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Central manager for the hallucination system.
|
||||||
|
/// Tracks the player's sanity level (0 = sane, 1 = fully insane) and drives all
|
||||||
|
/// hallucination effects: individual URP post-process overrides (with lerp), word shake
|
||||||
|
/// intensity, and ghost-word visibility via the HallucinationWordSpawner.
|
||||||
|
///
|
||||||
|
/// Effects are applied directly on VolumeProfile overrides rather than on volume.weight,
|
||||||
|
/// so multiple volumes coexist without parameter conflicts.
|
||||||
|
/// </summary>
|
||||||
|
public class HallucinationManager : Singleton<HallucinationManager>
|
||||||
|
{
|
||||||
|
// ─── Constants ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private const float MinSanity = 0f;
|
||||||
|
private const float MaxSanity = 1f;
|
||||||
|
|
||||||
|
// ─── Inspector ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Header("Sanity")]
|
||||||
|
[Tooltip("Starting sanity level (0 = sane, 1 = fully insane)")]
|
||||||
|
[SerializeField, Range(0f, 1f)] private float _initialSanity = 0f;
|
||||||
|
|
||||||
|
[Tooltip("Sanity naturally recovers over time at this rate per second (0 = no recovery)")]
|
||||||
|
[SerializeField, Range(0f, 0.1f)] private float _sanityRecoveryRate = 0f;
|
||||||
|
|
||||||
|
[Header("Effect Lerp")]
|
||||||
|
[Tooltip("Speed at which visual effects lerp toward the target sanity value")]
|
||||||
|
[SerializeField] private float _effectLerpSpeed = 2f;
|
||||||
|
|
||||||
|
[Header("URP Post-Process Volume")]
|
||||||
|
[Tooltip("Volume containing the hallucination overrides (Chromatic Aberration, Lens Distortion, etc.)")]
|
||||||
|
[SerializeField] private Volume _hallucinationVolume;
|
||||||
|
|
||||||
|
[Header("Chromatic Aberration")]
|
||||||
|
[SerializeField] private bool _driveChromaticAberration = true;
|
||||||
|
[SerializeField, Range(0f, 1f)] private float _chromaticAberrationMin = 0f;
|
||||||
|
[SerializeField, Range(0f, 1f)] private float _chromaticAberrationMax = 0.8f;
|
||||||
|
|
||||||
|
[Header("Lens Distortion")]
|
||||||
|
[SerializeField] private bool _driveLensDistortion = true;
|
||||||
|
[SerializeField, Range(-1f, 1f)] private float _lensDistortionMin = 0f;
|
||||||
|
[SerializeField, Range(-1f, 1f)] private float _lensDistortionMax = -0.4f;
|
||||||
|
|
||||||
|
[Header("Vignette")]
|
||||||
|
[SerializeField] private bool _driveVignette = true;
|
||||||
|
[SerializeField, Range(0f, 1f)] private float _vignetteMin = 0.3f;
|
||||||
|
[SerializeField, Range(0f, 1f)] private float _vignetteMax = 0.7f;
|
||||||
|
|
||||||
|
[Header("Film Grain")]
|
||||||
|
[SerializeField] private bool _driveFilmGrain = true;
|
||||||
|
[SerializeField, Range(0f, 1f)] private float _filmGrainMin = 0f;
|
||||||
|
[SerializeField, Range(0f, 1f)] private float _filmGrainMax = 0.6f;
|
||||||
|
|
||||||
|
[Header("Bloom")]
|
||||||
|
[SerializeField] private bool _driveBloom = true;
|
||||||
|
[Tooltip("Bloom intensity at sanity = 0")]
|
||||||
|
[SerializeField, Min(0f)] private float _bloomIntensityMin = 0f;
|
||||||
|
[Tooltip("Bloom intensity at sanity = 1")]
|
||||||
|
[SerializeField, Min(0f)] private float _bloomIntensityMax = 1f;
|
||||||
|
[Tooltip("Bloom threshold at sanity = 0 — raise this to suppress any bleed at zero sanity")]
|
||||||
|
[SerializeField, Min(0f)] private float _bloomThresholdMin = 1f;
|
||||||
|
[Tooltip("Bloom threshold at sanity = 1")]
|
||||||
|
[SerializeField, Min(0f)] private float _bloomThresholdMax = 0.5f;
|
||||||
|
[Tooltip("Bloom scatter (spread) at sanity = 0")]
|
||||||
|
[SerializeField, Range(0f, 1f)] private float _bloomScatterMin = 0f;
|
||||||
|
[Tooltip("Bloom scatter (spread) at sanity = 1")]
|
||||||
|
[SerializeField, Range(0f, 1f)] private float _bloomScatterMax = 0.7f;
|
||||||
|
[Tooltip("Bloom dirt texture intensity at sanity = 0")]
|
||||||
|
[SerializeField, Min(0f)] private float _bloomDirtIntensityMin = 0f;
|
||||||
|
[Tooltip("Bloom dirt texture intensity at sanity = 1")]
|
||||||
|
[SerializeField, Min(0f)] private float _bloomDirtIntensityMax = 0f;
|
||||||
|
|
||||||
|
[Header("Word Shake")]
|
||||||
|
[Tooltip("Baseline shake multiplier at sanity = 0")]
|
||||||
|
[SerializeField] private float _shakeMultiplierAtZero = 1f;
|
||||||
|
|
||||||
|
[Tooltip("Max shake multiplier at sanity = 1")]
|
||||||
|
[SerializeField] private float _shakeMultiplierAtOne = 3f;
|
||||||
|
|
||||||
|
[Header("Debug")]
|
||||||
|
[SerializeField] private bool _debugMode;
|
||||||
|
|
||||||
|
// ─── State ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private float _currentSanity;
|
||||||
|
private float _displayedSanity; // lerped value used for visual effects
|
||||||
|
private Coroutine _lerpCoroutine;
|
||||||
|
|
||||||
|
// External systems can suppress the hallucination vignette (e.g. diapason listening mode).
|
||||||
|
private bool _vignetteExternallyOverridden;
|
||||||
|
|
||||||
|
// Cached volume overrides
|
||||||
|
private ChromaticAberration _chromaticAberration;
|
||||||
|
private LensDistortion _lensDistortion;
|
||||||
|
private Vignette _vignette;
|
||||||
|
private FilmGrain _filmGrain;
|
||||||
|
private Bloom _bloom;
|
||||||
|
|
||||||
|
// ─── Public API ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>Current logical sanity level in [0, 1]. 0 = sane, 1 = fully insane.</summary>
|
||||||
|
public float SanityLevel => _currentSanity;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Word shake multiplier computed from the lerped display sanity (smooth).
|
||||||
|
/// </summary>
|
||||||
|
public float WordShakeMultiplier =>
|
||||||
|
Mathf.Lerp(_shakeMultiplierAtZero, _shakeMultiplierAtOne, _displayedSanity);
|
||||||
|
|
||||||
|
/// <summary>Raised whenever the logical sanity level changes. Payload is the new value in [0, 1].</summary>
|
||||||
|
public event Action<float> OnSanityChanged;
|
||||||
|
|
||||||
|
/// <summary>Raised when a discrete hallucination event is triggered (e.g. ghost word appears).</summary>
|
||||||
|
public event Action<HallucinationEvent> OnHallucinationTriggered;
|
||||||
|
|
||||||
|
// ─── Lifecycle ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
protected override void OnAwake()
|
||||||
|
{
|
||||||
|
_currentSanity = Mathf.Clamp(_initialSanity, MinSanity, MaxSanity);
|
||||||
|
_displayedSanity = _currentSanity;
|
||||||
|
CacheVolumeOverrides();
|
||||||
|
ApplyEffectsImmediate(_currentSanity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Update()
|
||||||
|
{
|
||||||
|
if (_sanityRecoveryRate > 0f && _currentSanity > MinSanity)
|
||||||
|
SetSanity(_currentSanity - _sanityRecoveryRate * Time.deltaTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Public Methods ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Increases the sanity level by <paramref name="amount"/> (clamped to [0, 1]).
|
||||||
|
/// Effects lerp smoothly to the new value.
|
||||||
|
/// </summary>
|
||||||
|
public void AddInsanity(float amount)
|
||||||
|
{
|
||||||
|
SetSanity(_currentSanity + amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Directly sets the logical sanity level. Effects lerp smoothly to the new value.
|
||||||
|
/// </summary>
|
||||||
|
public void SetSanity(float value)
|
||||||
|
{
|
||||||
|
float clamped = Mathf.Clamp(value, MinSanity, MaxSanity);
|
||||||
|
|
||||||
|
if (Mathf.Approximately(clamped, _currentSanity))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_currentSanity = clamped;
|
||||||
|
OnSanityChanged?.Invoke(_currentSanity);
|
||||||
|
|
||||||
|
if (_debugMode)
|
||||||
|
Debug.Log($"[HallucinationManager] Sanity → {_currentSanity:F2}", this);
|
||||||
|
|
||||||
|
RestartLerp();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Suppresses the hallucination vignette so an external system (e.g. diapason) can
|
||||||
|
/// take full control of vignette without additive blending artefacts.
|
||||||
|
/// Call with <c>false</c> when the external system is done.
|
||||||
|
/// </summary>
|
||||||
|
public void SetVignetteOverriddenExternally(bool overridden)
|
||||||
|
{
|
||||||
|
_vignetteExternallyOverridden = overridden;
|
||||||
|
|
||||||
|
// Apply immediately so the vignette drops to zero without waiting for the lerp coroutine
|
||||||
|
if (_vignette != null)
|
||||||
|
_vignette.intensity.value = overridden ? 0f : Mathf.Lerp(_vignetteMin, _vignetteMax, _displayedSanity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Fires a discrete hallucination event (ghost word, vision flash, etc.).</summary>
|
||||||
|
public void TriggerHallucinationEvent(HallucinationEvent hallucinationEvent)
|
||||||
|
{
|
||||||
|
if (hallucinationEvent == null)
|
||||||
|
{
|
||||||
|
Debug.LogError("[HallucinationManager] Attempted to trigger a null HallucinationEvent!", this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hallucinationEvent.SanityCost > 0f)
|
||||||
|
AddInsanity(hallucinationEvent.SanityCost);
|
||||||
|
|
||||||
|
if (_debugMode)
|
||||||
|
Debug.Log($"[HallucinationManager] Event: {hallucinationEvent.name}", this);
|
||||||
|
|
||||||
|
OnHallucinationTriggered?.Invoke(hallucinationEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Lerp ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void RestartLerp()
|
||||||
|
{
|
||||||
|
if (_lerpCoroutine != null)
|
||||||
|
StopCoroutine(_lerpCoroutine);
|
||||||
|
|
||||||
|
_lerpCoroutine = StartCoroutine(LerpEffects());
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerator LerpEffects()
|
||||||
|
{
|
||||||
|
while (!Mathf.Approximately(_displayedSanity, _currentSanity))
|
||||||
|
{
|
||||||
|
_displayedSanity = Mathf.MoveTowards(_displayedSanity, _currentSanity, _effectLerpSpeed * Time.deltaTime);
|
||||||
|
ApplyEffectsImmediate(_displayedSanity);
|
||||||
|
yield return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_displayedSanity = _currentSanity;
|
||||||
|
ApplyEffectsImmediate(_displayedSanity);
|
||||||
|
_lerpCoroutine = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Effects ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void CacheVolumeOverrides()
|
||||||
|
{
|
||||||
|
if (_hallucinationVolume == null || _hallucinationVolume.profile == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_hallucinationVolume.profile.TryGet(out _chromaticAberration);
|
||||||
|
_hallucinationVolume.profile.TryGet(out _lensDistortion);
|
||||||
|
_hallucinationVolume.profile.TryGet(out _vignette);
|
||||||
|
_hallucinationVolume.profile.TryGet(out _filmGrain);
|
||||||
|
_hallucinationVolume.profile.TryGet(out _bloom);
|
||||||
|
|
||||||
|
// Keep volume weight at 1 permanently — we write individual override values directly.
|
||||||
|
// The hallucination volume must be on its OWN dedicated VolumeProfile, separate from
|
||||||
|
// the dialogue blur volume, to avoid weight conflicts between the two systems.
|
||||||
|
_hallucinationVolume.weight = 1f;
|
||||||
|
_hallucinationVolume.enabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyEffectsImmediate(float t)
|
||||||
|
{
|
||||||
|
if (_driveChromaticAberration && _chromaticAberration != null)
|
||||||
|
_chromaticAberration.intensity.value = Mathf.Lerp(_chromaticAberrationMin, _chromaticAberrationMax, t);
|
||||||
|
|
||||||
|
if (_driveLensDistortion && _lensDistortion != null)
|
||||||
|
_lensDistortion.intensity.value = Mathf.Lerp(_lensDistortionMin, _lensDistortionMax, t);
|
||||||
|
|
||||||
|
if (_driveVignette && _vignette != null && !_vignetteExternallyOverridden)
|
||||||
|
_vignette.intensity.value = Mathf.Lerp(_vignetteMin, _vignetteMax, t);
|
||||||
|
|
||||||
|
if (_driveFilmGrain && _filmGrain != null)
|
||||||
|
_filmGrain.intensity.value = Mathf.Lerp(_filmGrainMin, _filmGrainMax, t);
|
||||||
|
|
||||||
|
if (_driveBloom && _bloom != null)
|
||||||
|
{
|
||||||
|
// Drive all bloom parameters that can independently cause it to appear.
|
||||||
|
// intensity alone is not enough — threshold=0 or scatter>0 can bleed through.
|
||||||
|
_bloom.intensity.value = Mathf.Lerp(_bloomIntensityMin, _bloomIntensityMax, t);
|
||||||
|
_bloom.threshold.value = Mathf.Lerp(_bloomThresholdMin, _bloomThresholdMax, t);
|
||||||
|
_bloom.scatter.value = Mathf.Lerp(_bloomScatterMin, _bloomScatterMax, t);
|
||||||
|
_bloom.dirtIntensity.value = Mathf.Lerp(_bloomDirtIntensityMin, _bloomDirtIntensityMax, t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 6806feb984393b441ad0f1b20cf70333
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Unheimlich.Features.Hallucination
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Fires a hallucination trigger when a specific AudioSource plays, finishes, or reaches a timestamp.
|
||||||
|
/// Mirrors the pattern of ClueUnlockOnAudio but targets HallucinationManager.
|
||||||
|
/// </summary>
|
||||||
|
[RequireComponent(typeof(AudioSource))]
|
||||||
|
public class HallucinationOnAudio : MonoBehaviour
|
||||||
|
{
|
||||||
|
public enum FireCondition
|
||||||
|
{
|
||||||
|
OnPlay,
|
||||||
|
OnFinish,
|
||||||
|
OnTimeReached
|
||||||
|
}
|
||||||
|
|
||||||
|
[Header("Audio Trigger")]
|
||||||
|
[Tooltip("When to fire the hallucination effect")]
|
||||||
|
[SerializeField] private FireCondition _fireCondition = FireCondition.OnPlay;
|
||||||
|
|
||||||
|
[Tooltip("Playback time in seconds to reach before firing (only for OnTimeReached)")]
|
||||||
|
[SerializeField] private float _triggerTime = 1f;
|
||||||
|
|
||||||
|
[Header("Hallucination Payload")]
|
||||||
|
[Tooltip("Amount of sanity to add when this fires (0 = no change)")]
|
||||||
|
[SerializeField, Range(0f, 1f)] private float _insanityAmount = 0f;
|
||||||
|
|
||||||
|
[Tooltip("Optional discrete hallucination event to fire")]
|
||||||
|
[SerializeField] private HallucinationEvent _hallucinationEvent;
|
||||||
|
|
||||||
|
[Header("Behaviour")]
|
||||||
|
[Tooltip("Prevent firing more than once per Play Mode session")]
|
||||||
|
[SerializeField] private bool _fireOnlyOnce = true;
|
||||||
|
|
||||||
|
[Header("Debug")]
|
||||||
|
[SerializeField] private bool _debugMode;
|
||||||
|
|
||||||
|
private AudioSource _audioSource;
|
||||||
|
private bool _hasFired;
|
||||||
|
private bool _isMonitoring;
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
_audioSource = GetComponent<AudioSource>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnEnable()
|
||||||
|
{
|
||||||
|
_hasFired = false;
|
||||||
|
_isMonitoring = false;
|
||||||
|
|
||||||
|
if (_audioSource != null && _audioSource.isPlaying)
|
||||||
|
StartMonitoring();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Update()
|
||||||
|
{
|
||||||
|
if (!_isMonitoring || (_fireOnlyOnce && _hasFired))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (_audioSource == null || !_audioSource.isPlaying)
|
||||||
|
{
|
||||||
|
if (_fireCondition == FireCondition.OnFinish)
|
||||||
|
Execute();
|
||||||
|
|
||||||
|
_isMonitoring = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_fireCondition == FireCondition.OnTimeReached && _audioSource.time >= _triggerTime)
|
||||||
|
Execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Notify this component that the AudioSource has started playing.
|
||||||
|
/// Wire this to whatever code or event drives the AudioSource.
|
||||||
|
/// </summary>
|
||||||
|
public void OnAudioPlay()
|
||||||
|
{
|
||||||
|
StartMonitoring();
|
||||||
|
|
||||||
|
if (_fireCondition == FireCondition.OnPlay)
|
||||||
|
Execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resets the fired state so this can fire again.
|
||||||
|
/// </summary>
|
||||||
|
public void ResetFired()
|
||||||
|
{
|
||||||
|
_hasFired = false;
|
||||||
|
_isMonitoring = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StartMonitoring()
|
||||||
|
{
|
||||||
|
_isMonitoring = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Execute()
|
||||||
|
{
|
||||||
|
if (_fireOnlyOnce && _hasFired)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_hasFired = true;
|
||||||
|
_isMonitoring = false;
|
||||||
|
|
||||||
|
if (HallucinationManager.Instance == null)
|
||||||
|
{
|
||||||
|
Debug.LogError("[HallucinationOnAudio] HallucinationManager not found!", this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_insanityAmount > 0f)
|
||||||
|
HallucinationManager.Instance.AddInsanity(_insanityAmount);
|
||||||
|
|
||||||
|
if (_hallucinationEvent != null)
|
||||||
|
HallucinationManager.Instance.TriggerHallucinationEvent(_hallucinationEvent);
|
||||||
|
|
||||||
|
if (_debugMode)
|
||||||
|
Debug.Log($"[HallucinationOnAudio] Fired on '{_fireCondition}'. Sanity now: {HallucinationManager.Instance.SanityLevel:F2}", this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 57b4beb469df640499f96b38d4fa204b
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
using Unheimlich.Core.Events;
|
||||||
|
|
||||||
|
namespace Unheimlich.Features.Hallucination
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Drives the HallucinationManager when its GameEventTrigger fires.
|
||||||
|
/// Can increase sanity, fire a discrete HallucinationEvent, or both.
|
||||||
|
/// Compose a GameEventTrigger on the same GameObject to control the firing condition
|
||||||
|
/// (OnStart, OnEnable, delay, once-flag, external call via UnityEvent, etc.).
|
||||||
|
/// </summary>
|
||||||
|
[RequireComponent(typeof(GameEventTrigger))]
|
||||||
|
public class HallucinationTrigger : MonoBehaviour
|
||||||
|
{
|
||||||
|
[Header("Sanity")]
|
||||||
|
[Tooltip("Amount of sanity to add when this trigger fires (0 = no change)")]
|
||||||
|
[SerializeField, Range(0f, 1f)] private float _insanityAmount = 0f;
|
||||||
|
|
||||||
|
[Header("Hallucination Event")]
|
||||||
|
[Tooltip("Optional discrete hallucination event to fire (ghost word, vision flash, etc.)")]
|
||||||
|
[SerializeField] private HallucinationEvent _hallucinationEvent;
|
||||||
|
|
||||||
|
[Header("Debug")]
|
||||||
|
[SerializeField] private bool _debugMode;
|
||||||
|
|
||||||
|
private GameEventTrigger _eventTrigger;
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
_eventTrigger = GetComponent<GameEventTrigger>();
|
||||||
|
_eventTrigger.OnTriggered += Trigger;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDestroy()
|
||||||
|
{
|
||||||
|
if (_eventTrigger != null)
|
||||||
|
_eventTrigger.OnTriggered -= Trigger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fires the hallucination effect. Can be called directly from external scripts or UnityEvents.
|
||||||
|
/// </summary>
|
||||||
|
public void Trigger()
|
||||||
|
{
|
||||||
|
if (HallucinationManager.Instance == null)
|
||||||
|
{
|
||||||
|
Debug.LogError("[HallucinationTrigger] HallucinationManager not found in scene!", this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_insanityAmount > 0f)
|
||||||
|
{
|
||||||
|
HallucinationManager.Instance.AddInsanity(_insanityAmount);
|
||||||
|
|
||||||
|
if (_debugMode)
|
||||||
|
Debug.Log($"[HallucinationTrigger] Added {_insanityAmount:F2} insanity. New sanity: {HallucinationManager.Instance.SanityLevel:F2}", this);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_hallucinationEvent != null)
|
||||||
|
{
|
||||||
|
HallucinationManager.Instance.TriggerHallucinationEvent(_hallucinationEvent);
|
||||||
|
|
||||||
|
if (_debugMode)
|
||||||
|
Debug.Log($"[HallucinationTrigger] Fired event: {_hallucinationEvent.Label}", this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5e1119e5c406d0346b0350dbcc91845f
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
using Unheimlich.Core.Events;
|
||||||
|
|
||||||
|
namespace Unheimlich.Features.Hallucination
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Fires a GameEventTrigger (and therefore a HallucinationTrigger) when the player
|
||||||
|
/// enters a physics trigger collider. Pair with GameEventTrigger + HallucinationTrigger
|
||||||
|
/// on the same GameObject.
|
||||||
|
/// </summary>
|
||||||
|
[RequireComponent(typeof(GameEventTrigger))]
|
||||||
|
public class HallucinationTriggerZone : TriggerZoneBase
|
||||||
|
{
|
||||||
|
[Header("Zone Behaviour")]
|
||||||
|
[Tooltip("Allow the zone to fire again after the player exits and re-enters")]
|
||||||
|
[SerializeField] private bool _retriggerable = false;
|
||||||
|
|
||||||
|
protected override bool IsRetriggerable => _retriggerable;
|
||||||
|
|
||||||
|
private GameEventTrigger _eventTrigger;
|
||||||
|
|
||||||
|
protected override void Awake()
|
||||||
|
{
|
||||||
|
base.Awake();
|
||||||
|
_eventTrigger = GetComponent<GameEventTrigger>();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnPlayerEntered(Collider playerCollider)
|
||||||
|
{
|
||||||
|
_eventTrigger.Fire();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e8fd091b4319d48448d266a24e885dae
|
||||||
+248
@@ -0,0 +1,248 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Unheimlich.Features.Dialogue.Data;
|
||||||
|
using Unheimlich.Features.Dialogue.UI;
|
||||||
|
using Unheimlich.Features.Player;
|
||||||
|
using Unheimlich.Core.Utils;
|
||||||
|
using RotationAxis = Unheimlich.Features.Dialogue.Data.RotationAxis;
|
||||||
|
using ChoiceSpawnMode = Unheimlich.Features.Dialogue.Data.ChoiceSpawnMode;
|
||||||
|
|
||||||
|
namespace Unheimlich.Features.Hallucination
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Listens to HallucinationManager.OnHallucinationTriggered and spawns ghost words
|
||||||
|
/// that float in world space and drift toward the player.
|
||||||
|
/// Reuses the existing ChoiceFocusHandler prefab and its organic shake system.
|
||||||
|
/// Add this component to the HallucinationManager GameObject or any persistent scene object.
|
||||||
|
/// </summary>
|
||||||
|
public class HallucinationWordSpawner : MonoBehaviour
|
||||||
|
{
|
||||||
|
private const float CanvasLocalScale = 0.01f;
|
||||||
|
|
||||||
|
[Header("References")]
|
||||||
|
[Tooltip("Prefab used to render each ghost word (ChoiceFocusHandler)")]
|
||||||
|
[SerializeField] private ChoiceFocusHandler _wordPrefab;
|
||||||
|
|
||||||
|
[Header("Ghost Word Behaviour")]
|
||||||
|
[Tooltip("Speed at which the ghost word drifts toward the player")]
|
||||||
|
[SerializeField] private float _driftSpeed = 0.4f;
|
||||||
|
|
||||||
|
[Tooltip("Distance from the player at which the ghost word stops drifting")]
|
||||||
|
[SerializeField] private float _stopDistance = 1.5f;
|
||||||
|
|
||||||
|
[Tooltip("Fade-in duration when a ghost word appears")]
|
||||||
|
[SerializeField] private float _fadeInDuration = 0.5f;
|
||||||
|
|
||||||
|
[Tooltip("Fade-out duration before the ghost word is destroyed")]
|
||||||
|
[SerializeField] private float _fadeOutDuration = 0.8f;
|
||||||
|
|
||||||
|
[Header("Shake Settings")]
|
||||||
|
[Tooltip("Shake radius applied to ghost words (world units)")]
|
||||||
|
[SerializeField] private float _shakeRadius = 15f;
|
||||||
|
|
||||||
|
[Tooltip("Shake speed")]
|
||||||
|
[SerializeField] private float _shakeSpeed = 2f;
|
||||||
|
|
||||||
|
[Tooltip("Shake smoothness")]
|
||||||
|
[SerializeField] private float _shakeSmoothness = 5f;
|
||||||
|
|
||||||
|
private readonly List<GhostWordInstance> _activeWords = new List<GhostWordInstance>();
|
||||||
|
|
||||||
|
// ─── Lifecycle ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void OnEnable()
|
||||||
|
{
|
||||||
|
if (HallucinationManager.Instance != null)
|
||||||
|
HallucinationManager.Instance.OnHallucinationTriggered += OnHallucinationTriggered;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDisable()
|
||||||
|
{
|
||||||
|
if (HallucinationManager.Instance != null)
|
||||||
|
HallucinationManager.Instance.OnHallucinationTriggered -= OnHallucinationTriggered;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Update()
|
||||||
|
{
|
||||||
|
for (int i = _activeWords.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
GhostWordInstance instance = _activeWords[i];
|
||||||
|
|
||||||
|
if (instance.Handler == null)
|
||||||
|
{
|
||||||
|
_activeWords.RemoveAt(i);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
DriftTowardPlayer(instance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Event Handler ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void OnHallucinationTriggered(HallucinationEvent hallucinationEvent)
|
||||||
|
{
|
||||||
|
if (!hallucinationEvent.SpawnGhostWord || _wordPrefab == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
SpawnGhostWord(hallucinationEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Spawn ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void SpawnGhostWord(HallucinationEvent hallucinationEvent)
|
||||||
|
{
|
||||||
|
if (PlayerController.Instance == null)
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[HallucinationWordSpawner] PlayerController not found, cannot spawn ghost word.", this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// World-space canvas container — mirrors the pattern in FloatingWordsSpawner
|
||||||
|
GameObject container = new GameObject($"GhostWord_{hallucinationEvent.Label}");
|
||||||
|
Canvas canvas = container.AddComponent<Canvas>();
|
||||||
|
canvas.renderMode = RenderMode.WorldSpace;
|
||||||
|
container.transform.localScale = Vector3.one * CanvasLocalScale;
|
||||||
|
|
||||||
|
// Position relative to player + event offset
|
||||||
|
Vector3 spawnPosition = PlayerController.Instance.transform.position
|
||||||
|
+ PlayerController.Instance.transform.TransformDirection(hallucinationEvent.GhostWordSpawnOffset);
|
||||||
|
container.transform.position = spawnPosition;
|
||||||
|
container.transform.LookAt(PlayerController.Instance.transform.position);
|
||||||
|
|
||||||
|
// Spawn the word handler
|
||||||
|
ChoiceFocusHandler handler = Instantiate(_wordPrefab, container.transform);
|
||||||
|
handler.transform.localPosition = Vector3.zero;
|
||||||
|
handler.transform.localRotation = Quaternion.identity;
|
||||||
|
handler.transform.localScale = Vector3.one;
|
||||||
|
|
||||||
|
// Build a ghost-only DialogueChoiceData (no NextNode, no localization key)
|
||||||
|
DialogueChoiceData ghostData = new DialogueChoiceData
|
||||||
|
{
|
||||||
|
ChoiceTextOverride = hallucinationEvent.GhostWordText,
|
||||||
|
NextNode = null
|
||||||
|
};
|
||||||
|
|
||||||
|
FloatingWordsSpawner.SpawnConfig config = BuildGhostConfig();
|
||||||
|
handler.Setup(ghostData, config, disableFloatingEffects: false);
|
||||||
|
handler.StartOrganicShake();
|
||||||
|
|
||||||
|
// Fade-in via CanvasGroup
|
||||||
|
CanvasGroup canvasGroup = handler.GetComponent<CanvasGroup>();
|
||||||
|
if (canvasGroup == null)
|
||||||
|
canvasGroup = handler.gameObject.AddComponent<CanvasGroup>();
|
||||||
|
|
||||||
|
canvasGroup.alpha = 0f;
|
||||||
|
TweenHelper.FadeCanvasGroup(canvasGroup, 1f, _fadeInDuration, null, LeanTweenType.easeOutQuad);
|
||||||
|
|
||||||
|
GhostWordInstance instance = new GhostWordInstance
|
||||||
|
{
|
||||||
|
Handler = handler,
|
||||||
|
Container = container,
|
||||||
|
CanvasGroup = canvasGroup,
|
||||||
|
Lifetime = hallucinationEvent.GhostWordLifetime,
|
||||||
|
ElapsedTime = 0f,
|
||||||
|
IsFadingOut = false
|
||||||
|
};
|
||||||
|
|
||||||
|
_activeWords.Add(instance);
|
||||||
|
StartCoroutine(LifetimeRoutine(instance));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Drift ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void DriftTowardPlayer(GhostWordInstance instance)
|
||||||
|
{
|
||||||
|
if (PlayerController.Instance == null || instance.IsFadingOut)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Transform container = instance.Container.transform;
|
||||||
|
Vector3 playerPos = PlayerController.Instance.transform.position;
|
||||||
|
float distance = Vector3.Distance(container.position, playerPos);
|
||||||
|
|
||||||
|
if (distance > _stopDistance)
|
||||||
|
{
|
||||||
|
Vector3 direction = (playerPos - container.position).normalized;
|
||||||
|
container.position += direction * _driftSpeed * Time.deltaTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always face the player
|
||||||
|
container.LookAt(playerPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Lifetime ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private IEnumerator LifetimeRoutine(GhostWordInstance instance)
|
||||||
|
{
|
||||||
|
float aliveTime = Mathf.Max(0f, instance.Lifetime - _fadeOutDuration);
|
||||||
|
yield return new WaitForSeconds(aliveTime);
|
||||||
|
|
||||||
|
if (instance.Handler == null)
|
||||||
|
yield break;
|
||||||
|
|
||||||
|
instance.IsFadingOut = true;
|
||||||
|
instance.Handler.StopOrganicShake();
|
||||||
|
|
||||||
|
if (instance.CanvasGroup != null)
|
||||||
|
TweenHelper.FadeCanvasGroup(instance.CanvasGroup, 0f, _fadeOutDuration, null, LeanTweenType.easeInQuad);
|
||||||
|
|
||||||
|
yield return new WaitForSeconds(_fadeOutDuration);
|
||||||
|
|
||||||
|
_activeWords.Remove(instance);
|
||||||
|
|
||||||
|
if (instance.Container != null)
|
||||||
|
Destroy(instance.Container);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Config ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private FloatingWordsSpawner.SpawnConfig BuildGhostConfig()
|
||||||
|
{
|
||||||
|
float sanityMultiplier = HallucinationManager.Instance != null
|
||||||
|
? HallucinationManager.Instance.WordShakeMultiplier
|
||||||
|
: 1f;
|
||||||
|
|
||||||
|
return new FloatingWordsSpawner.SpawnConfig
|
||||||
|
{
|
||||||
|
SpawnRadius = 0f,
|
||||||
|
SpawnHeightOffset = 0f,
|
||||||
|
RotationAxis = RotationAxis.Up,
|
||||||
|
EnableOrganicShake = true,
|
||||||
|
ForwardOffset = 0f,
|
||||||
|
TiltAngle = 0f,
|
||||||
|
RotationSpeed = 0f,
|
||||||
|
EnableRotation = false,
|
||||||
|
SpawnMode = ChoiceSpawnMode.AtAnchorPosition,
|
||||||
|
EnableFloating = false,
|
||||||
|
MergeDuration = 0.3f,
|
||||||
|
MergeScalePulse = 1f,
|
||||||
|
EnableGhostTrail = true,
|
||||||
|
GhostTrailAlpha = 0.25f,
|
||||||
|
GhostTrailDelay = 0.08f,
|
||||||
|
ShakeRadius = _shakeRadius * sanityMultiplier,
|
||||||
|
ShakeSpeed = _shakeSpeed,
|
||||||
|
ShakeSmoothness = _shakeSmoothness,
|
||||||
|
MoveTowardPlayerOnFocus = false,
|
||||||
|
FocusDistanceFromPlayer = _stopDistance,
|
||||||
|
DisableFloatingEffects = false,
|
||||||
|
DefaultScaleMultiplier = 1f,
|
||||||
|
EnableScaleVariation = false,
|
||||||
|
MinScaleVariation = 1f,
|
||||||
|
MaxScaleVariation = 1f
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Internal ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private class GhostWordInstance
|
||||||
|
{
|
||||||
|
public ChoiceFocusHandler Handler;
|
||||||
|
public GameObject Container;
|
||||||
|
public CanvasGroup CanvasGroup;
|
||||||
|
public float Lifetime;
|
||||||
|
public float ElapsedTime;
|
||||||
|
public bool IsFadingOut;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 39db1c5f2a13bc04782e676a6ad133c8
|
||||||
@@ -2,6 +2,7 @@ using UnityEngine;
|
|||||||
using UnityEngine.Rendering;
|
using UnityEngine.Rendering;
|
||||||
using UnityEngine.Rendering.Universal;
|
using UnityEngine.Rendering.Universal;
|
||||||
using Unheimlich.Core;
|
using Unheimlich.Core;
|
||||||
|
using Unheimlich.Features.Hallucination;
|
||||||
|
|
||||||
namespace Unheimlich.Features.Player
|
namespace Unheimlich.Features.Player
|
||||||
{
|
{
|
||||||
@@ -75,6 +76,11 @@ namespace Unheimlich.Features.Player
|
|||||||
public void EnableVignette(bool enable)
|
public void EnableVignette(bool enable)
|
||||||
{
|
{
|
||||||
_isEnabled = 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)
|
public void SetIntensity(float intensity)
|
||||||
|
|||||||
@@ -222,36 +222,32 @@ namespace Unheimlich.World.CameraSystem
|
|||||||
if (Mouse.current != null)
|
if (Mouse.current != null)
|
||||||
{
|
{
|
||||||
Vector2 mousePos = Mouse.current.position.ReadValue();
|
Vector2 mousePos = Mouse.current.position.ReadValue();
|
||||||
bool mouseHasMoved = Vector2.Distance(mousePos, _lastMousePosition) > 0.1f;
|
|
||||||
_lastMousePosition = mousePos;
|
_lastMousePosition = mousePos;
|
||||||
|
|
||||||
if (mouseHasMoved)
|
float screenWidth = Screen.width;
|
||||||
|
float screenHeight = Screen.height;
|
||||||
|
float borderPixels = _edgeBorderSize * Mathf.Min(screenWidth, screenHeight);
|
||||||
|
|
||||||
|
if (mousePos.x >= 0f && mousePos.x < borderPixels)
|
||||||
{
|
{
|
||||||
float screenWidth = Screen.width;
|
panX = -(borderPixels - mousePos.x) / borderPixels;
|
||||||
float screenHeight = Screen.height;
|
hasInput = true;
|
||||||
float borderPixels = _edgeBorderSize * Mathf.Min(screenWidth, screenHeight);
|
}
|
||||||
|
else if (mousePos.x > screenWidth - borderPixels && mousePos.x <= screenWidth)
|
||||||
|
{
|
||||||
|
panX = (mousePos.x - (screenWidth - borderPixels)) / borderPixels;
|
||||||
|
hasInput = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (mousePos.x < borderPixels)
|
if (mousePos.y >= 0f && mousePos.y < borderPixels)
|
||||||
{
|
{
|
||||||
panX = -(borderPixels - mousePos.x) / borderPixels;
|
panY = -(borderPixels - mousePos.y) / borderPixels;
|
||||||
hasInput = true;
|
hasInput = true;
|
||||||
}
|
}
|
||||||
else if (mousePos.x > screenWidth - borderPixels)
|
else if (mousePos.y > screenHeight - borderPixels && mousePos.y <= screenHeight)
|
||||||
{
|
{
|
||||||
panX = (mousePos.x - (screenWidth - borderPixels)) / borderPixels;
|
panY = (mousePos.y - (screenHeight - borderPixels)) / borderPixels;
|
||||||
hasInput = true;
|
hasInput = true;
|
||||||
}
|
|
||||||
|
|
||||||
if (mousePos.y < borderPixels)
|
|
||||||
{
|
|
||||||
panY = -(borderPixels - mousePos.y) / borderPixels;
|
|
||||||
hasInput = true;
|
|
||||||
}
|
|
||||||
else if (mousePos.y > screenHeight - borderPixels)
|
|
||||||
{
|
|
||||||
panY = (mousePos.y - (screenHeight - borderPixels)) / borderPixels;
|
|
||||||
hasInput = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user