chore: Project Restructuring & Theatre

This commit is contained in:
Alexandre Coirier
2026-05-13 12:24:48 +02:00
parent 4070916c1f
commit e96d0bf6e6
2145 changed files with 20957 additions and 44792 deletions
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 0e774c6c7706766428a1bd334a02cba8
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,176 @@
using UnityEngine;
using UnityEngine.InputSystem;
using Unheimlich.Core;
using Unheimlich.Features.Player.Data;
using Unheimlich.Features.Dialogue;
using InputManager = Unheimlich.Core.Input.InputManager;
using OctoCore.Components;
namespace Unheimlich.Features.Player
{
/// <summary>
/// Handles camera effects: aim assist, shake, and FOV kick on sprint.
/// </summary>
public class CameraEffects : MonoBehaviour
{
[Header("Settings")]
public CameraEffectsSettings Settings;
private float _shakeTimer = 0f;
private float _shakeInterval;
private Vector3 _shakeTarget;
private Vector3 _shakeOriginalPos;
private float _shakeSmoothness;
private Vector3 _pendingAimAssistTarget;
private bool _hasAimAssistTarget;
// FOV kick
private float _baseFov = -1f;
private float _currentFovOffset = 0f;
public bool IsAimLocked { get; private set; }
public bool HasActiveLookInput { get; private set; }
public bool IsUsingGamepad { get; private set; }
private void Start()
{
if (Settings != null)
_shakeInterval = 1f / Settings.shakeFrequency;
if (PlayerController.Instance?.PlayerCamera != null)
_baseFov = PlayerController.Instance.PlayerCamera.fieldOfView;
}
private void Update()
{
DetectInputDevice();
UpdateFovKick();
}
private void LateUpdate()
{
if (_hasAimAssistTarget)
ApplyAimAssistInternal(_pendingAimAssistTarget);
}
// ── FOV Kick ─────────────────────────────────────────────────────────────
private void UpdateFovKick()
{
if (Settings == null || PlayerController.Instance?.PlayerCamera == null) return;
// Capture base FOV once (handles cases where FOV is changed externally before Start).
if (_baseFov < 0f)
_baseFov = PlayerController.Instance.PlayerCamera.fieldOfView;
float targetOffset = PlayerController.Instance.IsRunning ? Settings.fovKickAmount : 0f;
_currentFovOffset = Mathf.Lerp(_currentFovOffset, targetOffset, Time.deltaTime * Settings.fovKickSpeed);
PlayerController.Instance.PlayerCamera.fieldOfView = _baseFov + _currentFovOffset;
}
/// <summary>
/// Resets the base FOV reference. Call this if the camera FOV is changed intentionally at runtime.
/// </summary>
public void ResetBaseFov()
{
if (PlayerController.Instance?.PlayerCamera != null)
_baseFov = PlayerController.Instance.PlayerCamera.fieldOfView;
}
// ── Input device detection ────────────────────────────────────────────────
private void DetectInputDevice()
{
Vector2 lookInput = InputManager.GetLookInput();
HasActiveLookInput = lookInput.magnitude > 0.1f;
if (Gamepad.current != null && Gamepad.current.rightStick.ReadValue().magnitude > 0.1f)
IsUsingGamepad = true;
else if (Mouse.current != null && Mouse.current.delta.ReadValue().magnitude > 0.1f)
IsUsingGamepad = false;
}
// ── Camera Shake ──────────────────────────────────────────────────────────
public void ShakeCamera(float amplitude, float smoothness = 0f)
{
if (PlayerController.Instance == null || PlayerController.Instance.CameraPivot == null || Settings == null)
return;
PlayerEvents.TriggerCameraShake(amplitude);
_shakeSmoothness = smoothness;
_shakeTimer -= Time.deltaTime;
if (_shakeTimer <= 0f)
{
_shakeTimer = _shakeInterval * amplitude;
_shakeOriginalPos = PlayerController.Instance.CameraPivot.position;
float x = Random.Range(-1f, 1f) * amplitude;
float y = Random.Range(-1f, 1f) * amplitude;
_shakeTarget = _shakeOriginalPos + new Vector3(x, y, 0f);
}
if (_shakeSmoothness > 0f)
{
PlayerController.Instance.CameraPivot.position = Vector3.Lerp(
PlayerController.Instance.CameraPivot.position,
_shakeTarget,
Time.deltaTime * _shakeSmoothness);
}
else
{
PlayerController.Instance.CameraPivot.position = _shakeTarget;
}
}
// ── Aim Assist ────────────────────────────────────────────────────────────
public void ApplyAimAssist(Vector3 targetCenter)
{
_pendingAimAssistTarget = targetCenter;
_hasAimAssistTarget = true;
}
private void ApplyAimAssistInternal(Vector3 targetCenter)
{
if (PlayerController.Instance == null || PlayerController.Instance.CameraTransform == null || Settings == null)
return;
if (PlayerController.Instance.Movement == null)
return;
if (IsAimLocked && HasActiveLookInput)
IsAimLocked = false;
Vector3 directionToTarget = (targetCenter - PlayerController.Instance.CameraTransform.position).normalized;
float angleError = Vector3.Angle(PlayerController.Instance.CameraTransform.forward, directionToTarget);
if (IsAimLocked || angleError < Settings.aimAssistMaxAngle)
{
Vector3 targetDirection = directionToTarget;
float targetYaw = Mathf.Atan2(targetDirection.x, targetDirection.z) * Mathf.Rad2Deg;
float targetPitch = -Mathf.Asin(targetDirection.y) * Mathf.Rad2Deg;
float currentYaw = OctoPlayer.Instance.State.Yaw;
float currentPitch = OctoPlayer.Instance.State.Pitch;
float newYaw = Mathf.LerpAngle(currentYaw, targetYaw, Settings.aimAssistStrength * Time.deltaTime);
float newPitch = Mathf.Lerp(currentPitch, targetPitch, Settings.aimAssistStrength * Time.deltaTime);
OctoPlayer.Instance.SetYaw(newYaw);
OctoPlayer.Instance.SetPitch(newPitch);
}
_hasAimAssistTarget = false;
}
public void LockAim(bool locked)
{
IsAimLocked = locked;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e05e5ee4f09d55d4a93324fe0eb85d6e
@@ -0,0 +1,190 @@
using UnityEngine;
using Unheimlich.Core.Interaction;
using Unheimlich.World.CameraSystem;
namespace Unheimlich.Features.Player
{
/// <summary>
/// Simulates camera head bobbing motion during player movement and idle states.
/// Smoothly transitions between idle, walk and run bob parameters.
/// Adds a subtle Z-axis roll to the bob and a strafe tilt applied in LateUpdate.
///
/// Sprint bob is automatically disabled when OctoPlayer.CanRun is false,
/// since IsRunning already returns false in that case.
/// </summary>
public class CameraHeadBob : MonoBehaviour
{
private float _timer = 0f;
private Vector3 _startLocalPos;
// Smoothly interpolated bob parameters to avoid brutal state switches.
private float _currentBobSpeed = 0f;
private float _currentBobAmplitude = 0f;
// Strafe tilt (Z roll applied in LateUpdate, after OctoPlayer sets camera pitch).
private float _currentTilt = 0f;
private HeadBobMode _overrideMode = HeadBobMode.Normal;
private bool _hasOverride = false;
private Coroutine _resetCoroutine;
private void Start()
{
_startLocalPos = transform.localPosition;
}
private void Update()
{
if (OctoCore.Components.OctoPlayer.Instance == null || PlayerController.Instance == null || PlayerController.Instance.CharController == null)
return;
var settings = PlayerController.Instance.CameraEffects?.Settings;
if (settings == null) return;
HeadBobMode currentMode = _hasOverride ? _overrideMode : HeadBobMode.Normal;
if (currentMode == HeadBobMode.Disabled)
{
SetBobDirect(settings.idleBobSpeed, 0f);
ApplySmoothedBob(settings.bobRollAmplitude);
return;
}
bool forceIdle = currentMode == HeadBobMode.Idle
|| (PlayerInspectionHandler.Instance != null && PlayerInspectionHandler.Instance.IsInspecting);
if (forceIdle)
{
SetBobDirect(settings.idleBobSpeed, settings.idleBobAmplitude);
ApplySmoothedBob(settings.bobRollAmplitude);
return;
}
bool isGrounded = PlayerController.Instance.CharController.isGrounded;
if (!isGrounded)
{
SetBobDirect(settings.idleBobSpeed, 0f);
ApplySmoothedBob(settings.bobRollAmplitude);
return;
}
// Drive bob parameters directly from the actual physics velocity.
float horizontalSpeed = new Vector3(
PlayerController.Instance.CharController.velocity.x,
0f,
PlayerController.Instance.CharController.velocity.z
).magnitude;
// IsRunning already returns false when OctoPlayer.CanRun is off —
// so the bob falls back to walk params automatically.
bool isRunning = OctoCore.Components.OctoPlayer.Instance.State.IsRunning;
const float WalkSpeedMs = 10f / 3.6f;
const float RunSpeedMs = 20f / 3.6f;
float maxSpeed = isRunning ? RunSpeedMs : WalkSpeedMs;
float t = Mathf.Clamp01(horizontalSpeed / maxSpeed);
float targetSpeed = Mathf.Lerp(settings.idleBobSpeed, isRunning ? settings.runBobSpeed : settings.walkBobSpeed, t);
float targetAmplitude = Mathf.Lerp(settings.idleBobAmplitude, isRunning ? settings.runBobAmplitude : settings.walkBobAmplitude, t);
SetBobDirect(targetSpeed, targetAmplitude);
ApplySmoothedBob(settings.bobRollAmplitude);
}
/// <summary>
/// Applies strafe tilt in LateUpdate so it composites correctly over OctoPlayer's pitch rotation.
/// </summary>
private void LateUpdate()
{
if (PlayerController.Instance == null || PlayerController.Instance.CharController == null)
return;
var settings = PlayerController.Instance.CameraEffects?.Settings;
if (settings == null) return;
Vector3 velocity = PlayerController.Instance.CharController.velocity;
float strafeAmount = Vector3.Dot(velocity, PlayerController.Instance.transform.right);
float runSpeedMs = PlayerController.Instance.Movement != null ? 20f / 3.6f : 5.5f;
float strafeFactor = Mathf.Clamp(strafeAmount / runSpeedMs, -1f, 1f);
float targetTilt = -strafeFactor * settings.strafeTiltAngle;
_currentTilt = Mathf.Lerp(_currentTilt, targetTilt, Time.deltaTime * settings.strafeTiltSpeed);
Quaternion baseRot = transform.localRotation;
transform.localRotation = baseRot * Quaternion.Euler(0f, 0f, _currentTilt);
}
// ── Public API ────────────────────────────────────────────────────────────
/// <summary>Overrides the head bob mode (Idle, Normal, Disabled).</summary>
public void SetMode(HeadBobMode mode)
{
_overrideMode = mode;
_hasOverride = true;
}
/// <summary>Clears any active mode override and returns to Normal.</summary>
public void ClearOverride()
{
_hasOverride = false;
if (_resetCoroutine != null)
{
StopCoroutine(_resetCoroutine);
_resetCoroutine = null;
}
}
/// <summary>Lerps the camera local position back to origin over <paramref name="duration"/> seconds.</summary>
public void LerpToZero(float duration)
{
if (_resetCoroutine != null)
StopCoroutine(_resetCoroutine);
_resetCoroutine = StartCoroutine(LerpToZeroCoroutine(duration));
}
// ── Private helpers ───────────────────────────────────────────────────────
private void SetBobDirect(float speed, float amplitude)
{
_currentBobSpeed = speed;
_currentBobAmplitude = amplitude;
}
private void ApplySmoothedBob(float rollAmplitude)
{
_timer += Time.deltaTime * _currentBobSpeed;
float x = Mathf.Sin(_timer * 0.5f) * _currentBobAmplitude * 0.6f;
float y = Mathf.Cos(_timer) * _currentBobAmplitude;
float roll = Mathf.Sin(_timer * 0.5f + Mathf.PI * 0.25f) * rollAmplitude * (_currentBobAmplitude / Mathf.Max(0.001f, 0.03f));
Vector3 newPos = _startLocalPos;
newPos.x += x;
newPos.y += y;
transform.localPosition = newPos;
transform.localRotation = Quaternion.Euler(0f, 0f, roll);
}
private System.Collections.IEnumerator LerpToZeroCoroutine(float duration)
{
Vector3 startPos = transform.localPosition;
float elapsed = 0f;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
float t = Mathf.Clamp01(elapsed / duration);
transform.localPosition = Vector3.Lerp(startPos, _startLocalPos, t);
yield return null;
}
transform.localPosition = _startLocalPos;
_resetCoroutine = null;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a2e180d338cc26242a3a9d962eea7f7c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: -200
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,379 @@
using System.Collections;
using UnityEngine;
namespace Unheimlich.World.CameraSystem
{
/// <summary>
/// Overlays a "lost footage / handheld camcorder" shake on a dedicated pivot
/// sitting between CameraPivot and Camera Player.
///
/// Layered in three passes:
/// 1. Continuous Perlin drift — organic idle tremor, scales with movement and stress.
/// 2. Micro-jolts — randomised snap/recoil spikes, only fire when the camera
/// is steady (attenuated during active mouse look).
/// 3. Inertia swinging — the camera "lags" behind head rotation on all axes,
/// simulating the physical weight of a handheld camcorder.
///
/// Hierarchy expected:
/// OctoPlayerFP (mouse-look root)
/// └── CameraPivot (CameraHeadBob lives here)
/// └── LostFootagePivot ← this component
/// └── Camera Player
///
/// Mouse-look speed is sampled on OctoPlayerFP (grandparent) to avoid being
/// polluted by the continuous rotations CameraHeadBob writes on CameraPivot.
/// Movement blend (moveT) is smoothed to avoid noise amplitude spikes on
/// direction changes.
/// </summary>
public class LostFootageRig : MonoBehaviour
{
// ── Inspector — Noise ─────────────────────────────────────────────────────
[Header("Noise — idle")]
[SerializeField] private float _idleAmplitude = 1f;
[SerializeField] private float _idleNoiseSpeed = 0.3f;
[Header("Noise — walk")]
[SerializeField] private float _walkAmplitude = 3f;
[SerializeField] private float _walkNoiseSpeed = 0.4f;
[Header("Noise — run")]
[SerializeField] private float _runAmplitude = 5f;
[SerializeField] private float _runNoiseSpeed = 0.6f;
[Header("Noise — stress scale")]
[SerializeField] private float _stressAmplitudeMul = 2.5f;
[SerializeField] private float _stressSpeedMul = 1f;
[Header("Noise — smoothing")]
[Tooltip("How fast the noise amplitude blends between idle/walk/run. Lower = smoother transitions.")]
[SerializeField] private float _moveTSmoothSpeed = 4f;
// ── Inspector — Micro-jolts ───────────────────────────────────────────────
[Header("Micro-jolts — idle")]
[SerializeField] private float _idleJoltAmplitude = 1.5f;
[SerializeField] private Vector2 _idleJoltIntervalRange = new Vector2(0.5f, 1.5f);
[Header("Micro-jolts — walk")]
[SerializeField] private float _walkJoltAmplitude = 3f;
[SerializeField] private Vector2 _walkJoltIntervalRange = new Vector2(2f, 4f);
[Header("Micro-jolts — run")]
[SerializeField] private float _runJoltAmplitude = 5f;
[SerializeField] private Vector2 _runJoltIntervalRange = new Vector2(1f, 2f);
[Header("Micro-jolts — shared")]
[Tooltip("How fast the jolt snaps to its target (fraction of interval).")]
[SerializeField] private float _joltSnapFraction = 0.1f;
[Tooltip("Jolt interval is divided by this value when stress = 1.")]
[SerializeField] private float _stressJoltFreqMul = 2f;
[Tooltip("Jolt amplitude is multiplied by this value when stress = 1.")]
[SerializeField] private float _stressJoltAmpMul = 1.4f;
[Tooltip("Look speed (deg/s) above which jolts are fully suppressed. Sampled on OctoPlayerFP to bypass CameraHeadBob noise.")]
[SerializeField] private float _joltLookSuppressSpeed = 5f;
// ── Inspector — Inertia swing ─────────────────────────────────────────────
[Header("Inertia swing")]
[Tooltip("How strongly the camera lags behind movement. Higher = more swing.")]
[SerializeField] private float _inertiaStrength = 4f;
[Tooltip("How fast the inertia offset returns to zero. Lower = longer swing tail.")]
[SerializeField] private float _inertiaDamping = 6f;
[Tooltip("Maximum swing angle (degrees) on each axis to avoid excessive drift.")]
[SerializeField] private float _inertiaMaxAngle = 8f;
// ── Inspector — Fade ──────────────────────────────────────────────────────
[Header("Fade")]
[Tooltip("Duration (seconds) of the fade-in / fade-out when toggling the rig.")]
[SerializeField] private float _fadeDuration = 0.6f;
// ── Inspector — Debug ─────────────────────────────────────────────────────
[Header("Debug")]
[SerializeField] private bool _debugSustainStress;
[SerializeField] [Range(0f, 1f)] private float _debugStressValue;
// ── Runtime state ─────────────────────────────────────────────────────────
/// <summary>Current stress level in [0, 1]. Driven by <see cref="StressController"/>.</summary>
public float StressLevel { get; set; }
/// <summary>Whether the rig is currently active (or fading in/out).</summary>
public bool IsActive { get; private set; }
private float _weight;
private float _noiseTimer;
private Vector3 _noiseOffset;
private Vector3 _joltRotation;
private Vector3 _inertiaOffset;
// Smoothed movement blend — avoids amplitude spikes on direction changes.
private float _smoothMoveT;
// Mouse-look speed sampled on OctoPlayerFP (grandparent) — used to gate
// micro-jolts so they only fire when the player is not actively looking around.
private Quaternion _lastLookRootRotation;
private float _lookSpeed;
private Coroutine _fadeCoroutine;
private Coroutine _joltCoroutine;
// ── Unity lifecycle ───────────────────────────────────────────────────────
private void Awake()
{
_noiseOffset = new Vector3(
Random.Range(0f, 100f),
Random.Range(0f, 100f),
Random.Range(0f, 100f)
);
}
private void Start()
{
Transform lookRoot = transform.parent?.parent;
_lastLookRootRotation = lookRoot != null
? lookRoot.localRotation
: Quaternion.identity;
}
private void Update()
{
if (_debugSustainStress)
StressLevel = _debugStressValue;
if (_weight <= 0f)
{
UpdateInertia();
return;
}
float stress = Mathf.Clamp01(StressLevel);
// ── 1. Resolve movement state ─────────────────────────────────────────
float horizontalSpeed = 0f;
bool isRunning = false;
if (Unheimlich.Features.Player.PlayerController.Instance?.CharController != null)
{
var vel = Unheimlich.Features.Player.PlayerController.Instance.CharController.velocity;
horizontalSpeed = new Vector3(vel.x, 0f, vel.z).magnitude;
isRunning = OctoCore.Components.OctoPlayer.Instance != null
&& OctoCore.Components.OctoPlayer.Instance.State.IsRunning;
}
const float WalkSpeedMs = 10f / 3.6f;
const float RunSpeedMs = 20f / 3.6f;
float maxSpeed = isRunning ? RunSpeedMs : WalkSpeedMs;
float rawMoveT = Mathf.Clamp01(horizontalSpeed / maxSpeed);
// Smooth moveT to avoid abrupt amplitude jumps on direction changes.
_smoothMoveT = Mathf.Lerp(_smoothMoveT, rawMoveT, Time.deltaTime * _moveTSmoothSpeed);
// ── 2. Interpolate noise parameters from movement state ───────────────
float targetAmp = isRunning
? Mathf.Lerp(_walkAmplitude, _runAmplitude, _smoothMoveT)
: Mathf.Lerp(_idleAmplitude, _walkAmplitude, _smoothMoveT);
float targetSpeed = isRunning
? Mathf.Lerp(_walkNoiseSpeed, _runNoiseSpeed, _smoothMoveT)
: Mathf.Lerp(_idleNoiseSpeed, _walkNoiseSpeed, _smoothMoveT);
float amp = (targetAmp + stress * _stressAmplitudeMul) * _weight;
float spd = (targetSpeed + stress * _stressSpeedMul);
_noiseTimer += Time.deltaTime * spd;
if (_noiseTimer > 1000f) _noiseTimer -= 1000f; // wrap to avoid float precision loss
float t = _noiseTimer;
// ── 3. Perlin drift ───────────────────────────────────────────────────
float rx = (Mathf.PerlinNoise(_noiseOffset.x + t, 0f) - 0.5f) * amp * 2.0f;
float ry = (Mathf.PerlinNoise(_noiseOffset.y + t, 0.5f) - 0.5f) * amp * 1.4f;
float rz = (Mathf.PerlinNoise(_noiseOffset.z + t, 1.0f) - 0.5f) * amp * 0.6f;
// ── 4. Inertia swing ──────────────────────────────────────────────────
UpdateInertia();
Vector3 swing = _inertiaOffset * _weight;
// ── 5. Compose and apply ──────────────────────────────────────────────
transform.localRotation = Quaternion.Euler(
rx + _joltRotation.x + swing.x,
ry + _joltRotation.y + swing.y,
rz + _joltRotation.z + swing.z
);
}
// ── Public API ────────────────────────────────────────────────────────────
/// <summary>
/// Enables or disables the lost-footage effect with a smooth fade.
/// Safe to call multiple times — an in-progress fade is cancelled and reversed.
/// </summary>
public void SetActive(bool active)
{
if (IsActive == active)
return;
IsActive = active;
if (_fadeCoroutine != null)
StopCoroutine(_fadeCoroutine);
_fadeCoroutine = StartCoroutine(FadeCoroutine(active));
if (active)
{
if (_joltCoroutine != null)
StopCoroutine(_joltCoroutine);
_joltCoroutine = StartCoroutine(JoltCoroutine());
}
}
// ── Private helpers ───────────────────────────────────────────────────────
/// <summary>
/// Drives inertia swing from CharController velocity projected into local camera space,
/// avoiding the rotation-delta approach which caused spikes whenever CameraHeadBob
/// changed phase. Also samples OctoPlayerFP (grandparent) rotation delta to measure
/// true mouse-look speed for jolt gating.
/// </summary>
private void UpdateInertia()
{
// ── Inertia from CharController velocity ──────────────────────────────
Vector3 velocity = Vector3.zero;
if (Unheimlich.Features.Player.PlayerController.Instance?.CharController != null)
velocity = Unheimlich.Features.Player.PlayerController.Instance.CharController.velocity;
// Project velocity into local camera space so the swing feels attached to the view.
Vector3 localVel = transform.parent != null
? transform.parent.InverseTransformDirection(velocity)
: velocity;
Vector3 targetInertia = new Vector3(-localVel.z, 0f, localVel.x) * (_inertiaStrength * 0.01f);
targetInertia = Vector3.ClampMagnitude(targetInertia, _inertiaMaxAngle);
_inertiaOffset = Vector3.Lerp(_inertiaOffset, targetInertia, _inertiaDamping * Time.deltaTime);
// ── Mouse-look speed from OctoPlayerFP (grandparent) ─────────────────
Transform lookRoot = transform.parent?.parent;
Quaternion lookRot = lookRoot != null ? lookRoot.localRotation : Quaternion.identity;
Quaternion lookDelta = lookRot * Quaternion.Inverse(_lastLookRootRotation);
lookDelta.ToAngleAxis(out float lookAngle, out _);
if (lookAngle > 180f) lookAngle -= 360f;
_lookSpeed = Mathf.Abs(lookAngle) / Mathf.Max(Time.deltaTime, 0.0001f);
_lastLookRootRotation = lookRot;
}
private IEnumerator FadeCoroutine(bool fadeIn)
{
float start = _weight;
float target = fadeIn ? 1f : 0f;
float elapsed = 0f;
while (elapsed < _fadeDuration)
{
elapsed += Time.deltaTime;
_weight = Mathf.Lerp(start, target, elapsed / _fadeDuration);
yield return null;
}
_weight = target;
_fadeCoroutine = null;
if (!fadeIn && _joltCoroutine != null)
{
StopCoroutine(_joltCoroutine);
_joltCoroutine = null;
}
}
private IEnumerator JoltCoroutine()
{
while (true)
{
float stress = Mathf.Clamp01(StressLevel);
// Use the same smoothed moveT as Update to keep jolt parameters consistent.
float joltAmp = isRunning()
? Mathf.Lerp(_walkJoltAmplitude, _runJoltAmplitude, _smoothMoveT)
: Mathf.Lerp(_idleJoltAmplitude, _walkJoltAmplitude, _smoothMoveT);
Vector2 intervalRange = isRunning()
? Vector2.Lerp(_walkJoltIntervalRange, _runJoltIntervalRange, _smoothMoveT)
: Vector2.Lerp(_idleJoltIntervalRange, _walkJoltIntervalRange, _smoothMoveT);
float interval = Random.Range(intervalRange.x, intervalRange.y);
interval /= Mathf.Lerp(1f, _stressJoltFreqMul, stress);
yield return new WaitForSeconds(interval);
// Jolts only fire when the camera is steady — steadyFactor is 1 when still,
// 0 when the player is actively looking around.
float steadyFactor = 1f - Mathf.Clamp01(_lookSpeed / _joltLookSuppressSpeed);
float ampScale = (1f + stress * _stressJoltAmpMul) * _weight * steadyFactor;
Vector3 target = new Vector3(
Random.Range(-1f, 1f) * joltAmp * ampScale,
Random.Range(-1f, 1f) * joltAmp * ampScale,
Random.Range(-0.3f, 0.3f) * joltAmp * ampScale
);
yield return StartCoroutine(ApplyJolt(target, interval));
}
}
/// <summary>Returns true if the player is currently running.</summary>
private bool isRunning()
{
return OctoCore.Components.OctoPlayer.Instance != null
&& OctoCore.Components.OctoPlayer.Instance.State.IsRunning;
}
/// <summary>
/// Snaps to <paramref name="target"/> in the first fraction of the interval,
/// then eases back to zero for the remainder — simulating a hand recoil.
/// </summary>
private IEnumerator ApplyJolt(Vector3 target, float totalDuration)
{
float snapDur = totalDuration * _joltSnapFraction;
float easeDur = totalDuration * (1f - _joltSnapFraction);
float elapsed = 0f;
Vector3 start = _joltRotation;
while (elapsed < snapDur)
{
elapsed += Time.deltaTime;
float t = elapsed / snapDur;
_joltRotation = Vector3.Lerp(start, target, t * t);
yield return null;
}
elapsed = 0f;
start = _joltRotation;
while (elapsed < easeDur)
{
elapsed += Time.deltaTime;
float t = elapsed / easeDur;
_joltRotation = Vector3.Lerp(start, Vector3.zero, t);
yield return null;
}
_joltRotation = Vector3.zero;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 320cde26f18d40b40a31c85e85ee4e59
@@ -0,0 +1,95 @@
using UnityEngine;
namespace Unheimlich.Features.Player
{
/// <summary>
/// Manages the player's stress level and forwards it to <see cref="Unheimlich.World.CameraSystem.LostFootageRig"/>.
/// Stress rises instantly to the requested target and decays back to zero over time when not sustained.
///
/// Intended to sit on <c>OctoPlayerFP</c> alongside other player-level components.
/// Call <see cref="AddStress"/>, <see cref="SetStress"/>, or <see cref="CalmDown"/> from
/// gameplay systems (proximity triggers, jump-scares, scripted events, etc.).
/// </summary>
public class StressController : MonoBehaviour
{
// ── Inspector ─────────────────────────────────────────────────────────────
[Header("References")]
[Tooltip("The LostFootageRig component on CameraPivot.")]
[SerializeField] private Unheimlich.World.CameraSystem.LostFootageRig _rig;
[Header("Decay")]
[Tooltip("Rate at which stress falls back to zero per second when not sustained.")]
[SerializeField] private float _decaySpeed = 0.3f;
[Tooltip("Stress must stay below this threshold for the rig to auto-deactivate.")]
[SerializeField] private float _deactivateThreshold = 0.05f;
[Header("Debug")]
[SerializeField] private bool _debugSustainStress;
[SerializeField] [Range(0f, 1f)] private float _debugStressValue;
// ── Runtime state ─────────────────────────────────────────────────────────
private float _currentStress;
private float _targetStress;
private bool _sustained; // true while an external system holds a target this frame
// ── Unity lifecycle ───────────────────────────────────────────────────────
private void Update()
{
if (_debugSustainStress)
SetStress(_debugStressValue);
_sustained = false;
if (_currentStress > _targetStress)
_currentStress = Mathf.MoveTowards(_currentStress, _targetStress, _decaySpeed * Time.deltaTime);
else
_currentStress = _targetStress;
_targetStress = 0f; // reset each frame; callers must re-assert every Update if sustained
if (_rig == null)
return;
_rig.StressLevel = _currentStress;
// Auto-toggle the rig based on stress level.
if (!_rig.IsActive && _currentStress > _deactivateThreshold)
_rig.SetActive(true);
else if (_rig.IsActive && _currentStress <= _deactivateThreshold)
_rig.SetActive(false);
}
// ── Public API ────────────────────────────────────────────────────────────
/// <summary>
/// Sets a one-shot stress spike that will decay naturally next frame.
/// For sustained stress, call this every frame from a trigger zone or event.
/// </summary>
public void SetStress(float value)
{
_targetStress = Mathf.Max(_targetStress, Mathf.Clamp01(value));
_currentStress = Mathf.Max(_currentStress, _targetStress);
}
/// <summary>Adds <paramref name="delta"/> on top of the current target stress.</summary>
public void AddStress(float delta) => SetStress(_targetStress + delta);
/// <summary>Immediately zeroes stress and deactivates the rig.</summary>
public void CalmDown()
{
_currentStress = 0f;
_targetStress = 0f;
_rig?.SetActive(false);
}
/// <summary>
/// Directly activates or deactivates the rig regardless of stress level.
/// Useful for scripted sequences where you want the effect without the stress system.
/// </summary>
public void ForceRig(bool active) => _rig?.SetActive(active);
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b711f35d53cba9d43b8b6f55a40cedbc
@@ -0,0 +1,42 @@
using UnityEngine;
using UnityEngine.UI;
namespace Unheimlich.Features.Player
{
public class HandTouchFeedback : MonoBehaviour
{
[Header("Visual Settings")]
[SerializeField] private Image _feedbackImage;
[SerializeField] private Color _readyColor = Color.green;
[SerializeField] private float _pulseSpeed = 2f;
[SerializeField] private float _pulseIntensity = 0.3f;
private float _pulseTimer;
private Color _originalColor;
private void Awake()
{
if (_feedbackImage != null)
{
_originalColor = _feedbackImage.color;
}
}
private void OnEnable()
{
_pulseTimer = 0f;
}
private void Update()
{
if (_feedbackImage == null)
return;
_pulseTimer += Time.deltaTime * _pulseSpeed;
float pulse = (Mathf.Sin(_pulseTimer) + 1f) * 0.5f;
Color targetColor = Color.Lerp(_originalColor, _readyColor, pulse * _pulseIntensity);
_feedbackImage.color = targetColor;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 39b78978d656e894ea455342398ef8ba
@@ -0,0 +1,105 @@
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using Unheimlich.Core;
using Unheimlich.Features.Hallucination;
namespace Unheimlich.Features.Player
{
public class ListeningVignetteEffect : Singleton<ListeningVignetteEffect>
{
[Header("References")]
[Tooltip("Dedicated Volume for listening mode (not shared with dialogue system)")]
[SerializeField] private Volume _listeningVolume;
[Header("Settings")]
[Tooltip("Target intensity when listening (0-1)")]
[SerializeField] [Range(0f, 1f)] private float _targetIntensity = 0.3f;
[Tooltip("Speed of vignette fade in/out")]
[SerializeField] private float _transitionSpeed = 2f;
private Vignette _vignette;
private bool _isEnabled;
private VolumeProfile _volumeProfile;
protected override void OnAwake()
{
if (_listeningVolume == null)
{
GameObject volumeObject = new GameObject("Listening Mode Volume");
volumeObject.transform.SetParent(transform);
_listeningVolume = volumeObject.AddComponent<Volume>();
_listeningVolume.isGlobal = true;
_listeningVolume.priority = 1;
_listeningVolume.weight = 0f;
}
if (_listeningVolume.profile == null)
{
_volumeProfile = ScriptableObject.CreateInstance<VolumeProfile>();
_listeningVolume.profile = _volumeProfile;
_vignette = _volumeProfile.Add<Vignette>();
_vignette.active = true;
_vignette.intensity.Override(0f);
_vignette.smoothness.Override(0.4f);
_vignette.rounded.Override(true);
}
else
{
if (!_listeningVolume.profile.TryGet(out _vignette))
{
_vignette = _listeningVolume.profile.Add<Vignette>();
_vignette.active = true;
_vignette.intensity.Override(0f);
_vignette.smoothness.Override(0.4f);
_vignette.rounded.Override(true);
}
}
_listeningVolume.weight = 0f;
}
private void Update()
{
if (_vignette == null || _listeningVolume == null)
return;
float targetWeight = _isEnabled ? 1f : 0f;
_listeningVolume.weight = Mathf.Lerp(_listeningVolume.weight, targetWeight, Time.deltaTime * _transitionSpeed);
if (_isEnabled)
_vignette.intensity.value = _targetIntensity;
}
public void EnableVignette(bool enable)
{
_isEnabled = enable;
// Suppress the hallucination vignette while diapason is active to avoid additive blending.
// Restores hallucination vignette control when diapason listening ends.
if (HallucinationManager.Instance != null)
HallucinationManager.Instance.SetVignetteOverriddenExternally(enable);
}
public void SetIntensity(float intensity)
{
_targetIntensity = Mathf.Clamp01(intensity);
if (_vignette != null)
_vignette.intensity.value = _targetIntensity;
}
public void EnableVignetteWithIntensity(float intensity)
{
SetIntensity(intensity);
EnableVignette(true);
}
private void OnDestroy()
{
if (_volumeProfile != null)
Destroy(_volumeProfile);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 878ebc270c13e5c49a3813b1848f6ea5
@@ -0,0 +1,120 @@
using UnityEngine;
using OctoCore.Components;
using Unheimlich.Core;
using Unheimlich.Features.Player.Data;
using Unheimlich.Features.Dialogue;
using Unheimlich.Core.Interaction;
using Unheimlich.Features.DetectiveBoard;
using Unheimlich.Features.Diapason;
using Unheimlich.Features.OctoArms;
using Unheimlich.World.CameraSystem;
using InputManager = Unheimlich.Core.Input.InputManager;
namespace Unheimlich.Features.Player
{
/// <summary>
/// Central hub for all player systems.
/// Exposes components and settings with zero Inspector refs needed for other scripts.
/// </summary>
[RequireComponent(typeof(OctoPlayer))]
[RequireComponent(typeof(CharacterController))]
public class PlayerController : Singleton<PlayerController>
{
// Auto-found components (exposed publicly, found once in Awake)
public OctoPlayer Movement { get; private set; }
public CharacterController CharController { get; private set; }
public Camera PlayerCamera { get; private set; }
public Transform CameraTransform { get; private set; }
public Transform CameraPivot { get; private set; }
public CameraEffects CameraEffects { get; private set; }
public CameraPanController CameraPanController { get; private set; }
public DiapasonListeningHandlerV2 DiapasonHandler { get; private set; }
public DiapasonEquipmentManager DiapasonEquipment { get; private set; }
public PlayerInspectionHandler InspectionHandler { get; private set; }
public DetectiveBoardController DetectiveBoardController { get; private set; }
public OctoIkArmsImproved ArmsSystem { get; private set; }
public InputManager Inputs => InputManager.Instance;
// Cached sprint InputAction — resolved once in OnAwake to avoid per-frame foreach.
private UnityEngine.InputSystem.InputAction _sprintAction;
/// <summary>
/// True when the character is physically moving (based on CharacterController velocity,
/// not raw input — correctly synced with OctoPlayer's inertia).
/// </summary>
public bool IsMoving => CharController != null && CharController.velocity.magnitude > 0.1f;
/// <summary>
/// True while the sprint input is held and the player is moving.
/// </summary>
public bool IsRunning => _sprintAction != null && _sprintAction.IsPressed() && IsMoving;
protected override void OnAwake()
{
// Auto-find all components once
Movement = GetComponent<OctoPlayer>();
CharController = GetComponent<CharacterController>();
CameraEffects = GetComponent<CameraEffects>();
CameraPanController = GetComponent<CameraPanController>();
DiapasonHandler = GetComponent<DiapasonListeningHandlerV2>();
DiapasonEquipment = GetComponent<DiapasonEquipmentManager>();
InspectionHandler = GetComponent<PlayerInspectionHandler>();
DetectiveBoardController = GetComponent<DetectiveBoardController>();
PlayerCamera = GetComponentInChildren<Camera>();
ArmsSystem = GetComponentInChildren<OctoIkArmsImproved>();
if (PlayerCamera != null)
{
CameraTransform = PlayerCamera.transform;
CameraPivot = CameraTransform.parent;
}
CacheSprintAction();
}
/// <summary>
/// Resolves the Sprint InputAction once from OctoCore to avoid per-frame foreach iteration.
/// </summary>
private void CacheSprintAction()
{
if (OctoCore.OctoCore.Instance?.Inputs?.Actions == null) return;
foreach (var actionInfo in OctoCore.OctoCore.Instance.Inputs.Actions)
{
if (actionInfo.Name == "Sprint" && actionInfo.Reference != null)
{
_sprintAction = actionInfo.Reference.action;
break;
}
}
}
// Teleport
public void Teleport(Vector3 position, Quaternion rotation)
{
Movement.Teleport(position, rotation);
PlayerEvents.TriggerTeleport(position, rotation);
}
// Helper methods
public Vector3 GetForward()
{
if (PlayerCamera == null) return transform.forward;
Vector3 forward = PlayerCamera.transform.forward;
forward.y = 0;
return forward.normalized;
}
public Vector3 GetRight()
{
if (PlayerCamera == null) return transform.right;
Vector3 right = PlayerCamera.transform.right;
right.y = 0;
return right.normalized;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 2d192ca4042c25c4ea16f024d0e262c1