chore: Project Restructuring & Theatre
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user