Hallucination system

This commit is contained in:
Ferlat Thibaud
2026-03-16 17:06:47 +01:00
parent 10faf74405
commit 0d968f75ab
44 changed files with 2860 additions and 909 deletions
@@ -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 _isMerged = 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 Vector3[] _originalSpawnPositions;
private Vector3[] _basePositions;
@@ -51,21 +54,69 @@ namespace Unheimlich.Core.Interaction
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;
if (_playerTransform == null)
Debug.LogWarning($"[ObjectDescription] No player found for {gameObject.name}");
if (_descriptionAnchor == null)
_descriptionAnchor = transform;
if (_settings == null)
{
Debug.LogError($"[ObjectDescription] No settings assigned for {gameObject.name}");
enabled = false;
}
}
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()
{
@@ -1,23 +1,62 @@
using UnityEngine;
using System;
using System.Collections.Generic;
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 static T Instance { get; private set; }
static Singleton()
{
SingletonRegistry.Register(() => Instance = null);
}
protected virtual void Awake()
{
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);
return;
}
Instance = this as T;
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()
{
if (Instance == this)
@@ -26,8 +65,9 @@ namespace Unheimlich.Core
OnDestroyInstance();
}
}
protected virtual void OnAwake() { }
protected virtual void OnEnabled() { }
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