using OctoCore.Inputs; using UnityEngine; namespace OctoCore.Components { public class OctoPlayer : MonoBehaviour { public static OctoPlayer Instance { get; private set; } public enum PlayerModes { FirstPerson, ThirdPerson } public enum Orientations { World, Camera } [System.Serializable] private class References { public CharacterController Controller; } [System.Serializable] private class Settings { public PlayerModes Mode = PlayerModes.FirstPerson; public Orientations Orientation = Orientations.Camera; [Tooltip("Walk speed (km/h).")] public float WalkSpeed = 10; [Tooltip("Run speed (km/h).")] public float RunSpeed = 20; [Tooltip("Jump duration (seconds).")] public float JumpDuration = .4f; [Tooltip("Maximum jump height reached at the apex (meters).")] public float JumpHeight = .4f; [Tooltip("Maximum step height (meters).")] public float StepHeight = 0.3f; [Tooltip("Downward acceleration applied to the player (m/s²).")] public float Gravity = 9.8f; [Tooltip("Enable jump.")] public bool CanJump = true; [Tooltip("Enable sprint.")] public bool CanRun = true; } [System.Serializable] private class Mapping { public OctoActions MoveAction; public OctoActions LookAction; public OctoActions RunAction; public OctoActions JumpAction; } [System.Serializable] private class FirstPerson { [Header("References")] public Camera Camera; [Header("Settings")] [Tooltip("Horizontal sensitivity (Yaw).")] public float SensitivityX = 0.1f; [Tooltip("Vertical sensitivity (Pitch).")] public float SensitivityY = 0.1f; [Tooltip("Minimum pitch (looking down).")] public float MinPitch = -80f; [Tooltip("Maximum pitch (looking up).")] public float MaxPitch = 80f; } [SerializeField] private References _references; [SerializeField] private Settings _settings; [SerializeField] private Mapping _mapping; [SerializeField] private FirstPerson _firstPerson; private bool _isPaused; private bool _jumpInput; private bool _jumpping; private bool _runInput; private float _runInertia; private float _yaw; private float _pitch; private float _jumpLerp; private float _jumpTime; private float _jumpAltitude; private Vector2 _moveInput; private Vector2 _lookInput; private Vector3 _moveInertia; private Vector3 _moveDirection; public bool IsPaused => _isPaused; public float Radius => _references.Controller ? _references.Controller.radius : 0; /// True when the player is actively sprinting (inertia > 0.1 and CanRun enabled). public bool IsRunning => _settings.CanRun && _runInertia > 0.1f; // Public access to yaw and pitch for external aim assist systems public float Yaw { get => _yaw; set => _yaw = value; } public float Pitch { get => _pitch; set => _pitch = Mathf.Clamp(value, _firstPerson.MinPitch, _firstPerson.MaxPitch); } public bool CanJump { get => _settings.CanJump; set => _settings.CanJump = value; } public bool CanRun { get => _settings.CanRun; set => _settings.CanRun = value; } void OnEnable() { if (!Instance) Instance = this; else Destroy(gameObject); OctoCore.Instance.Inputs.SubscribeAction(_mapping.MoveAction, Move); OctoCore.Instance.Inputs.SubscribeAction(_mapping.LookAction, Look); OctoCore.Instance.Inputs.SubscribeAction(_mapping.RunAction, Run); OctoCore.Instance.Inputs.SubscribeAction(_mapping.JumpAction, Jump); _yaw = transform.eulerAngles.y; Pause(_isPaused); } void OnDisable() { OctoCore.Instance.Inputs.UnsubscribeAction(_mapping.MoveAction, Move); OctoCore.Instance.Inputs.UnsubscribeAction(_mapping.RunAction, Run); OctoCore.Instance.Inputs.UnsubscribeAction(_mapping.JumpAction, Jump); if (Instance == this) Instance = null; } void Update() { _moveInput = _isPaused ? Vector2.zero : Vector2.ClampMagnitude(_moveInput, 1); Camera cam = (_firstPerson.Camera && _firstPerson.Camera.enabled && _firstPerson.Camera.gameObject.activeInHierarchy) ? _firstPerson.Camera : Camera.main; bool accelerating = _moveInput.magnitude > .1f; bool isGrounded = _references.Controller ? _references.Controller.isGrounded : true; float t = Time.deltaTime / 3.6f; // ── Move ──────────────────────────────────────────────────────────── Vector3 move = new Vector3(_moveInput.x, 0, _moveInput.y); if (_settings.Orientation == Orientations.Camera && cam) { Vector3 camForward = cam.transform.forward; camForward.y = 0; camForward.Normalize(); Vector3 camRight = cam.transform.right; camRight.y = 0; camRight.Normalize(); move = camRight * move.x + camForward * move.z; } float deltaSmooth = t * (accelerating ? 30 : 15); _moveDirection = Vector3.Lerp(_moveDirection, move, deltaSmooth * 1.5f); deltaSmooth *= 1 - Vector3.Cross(_moveInertia, _moveDirection).magnitude; _moveInertia = Vector3.Lerp(_moveInertia, move, deltaSmooth); if (!accelerating && _moveInertia.magnitude < .01f) _moveInertia = Vector3.zero; // Sprint inertia — clamped to 0 if CanRun is off _runInertia = Mathf.Lerp( _runInertia, (_runInput && _settings.CanRun) ? 1f : 0f, t * (accelerating ? 5 : 3) ); Vector3 moveDelta = _moveInertia * t * Mathf.Lerp(_settings.WalkSpeed, _settings.RunSpeed, _runInertia); // ── Jump ──────────────────────────────────────────────────────────── if (isGrounded && _jumpInput && !_jumpping && _settings.CanJump) { _jumpInput = false; _jumpping = true; _jumpLerp = 0; _jumpTime = Time.timeSinceLevelLoad; _jumpAltitude = transform.position.y; } // Consume the input even if CanJump is off, so it doesn't queue up if (_jumpInput && !_settings.CanJump) _jumpInput = false; float jumpOffset = 0; if (_jumpping) { _jumpLerp = (Time.timeSinceLevelLoad - _jumpTime) / _settings.JumpDuration; // FIX: added 0.15s grace period before checking isGrounded to avoid immediate re-trigger bool landingAllowed = (Time.timeSinceLevelLoad - _jumpTime) > 0.15f; if (_jumpLerp >= 1 || (_jumpLerp > .5f && isGrounded && landingAllowed)) { _jumpping = false; _jumpLerp = 0; } else { float x = _jumpLerp * 2 - 1f; float y = x < 0 ? Mathf.Sqrt(1f - x * x) : Mathf.Cos(x * .5f * Mathf.PI); y = _jumpAltitude + y * _settings.JumpHeight; jumpOffset = y - transform.position.y; } } // ── Step-up ───────────────────────────────────────────────────────── if (_references.Controller && isGrounded && !_jumpping) { float feetY = transform.position.y - (_references.Controller.height * 0.5f); float castFromY = feetY + _settings.StepHeight; Vector3 stepOrigin = new Vector3(transform.position.x, castFromY, transform.position.z); Vector3 stepDir = new Vector3(moveDelta.x, 0, moveDelta.z).normalized; if (stepDir.magnitude > 0.1f) { float stepDist = _references.Controller.radius + 0.05f; if (!Physics.Raycast(stepOrigin, stepDir, stepDist)) { Vector3 probeOrigin = stepOrigin + stepDir * stepDist; if (Physics.Raycast(probeOrigin, Vector3.down, out RaycastHit hit, _settings.StepHeight)) { float stepUp = hit.point.y - feetY; if (stepUp > 0.05f) moveDelta.y = Mathf.Max(moveDelta.y, stepUp); } } } } // ── Apply ─────────────────────────────────────────────────────────── if (_jumpping) moveDelta.y = jumpOffset; else if (!isGrounded) moveDelta.y -= _settings.Gravity * Time.deltaTime; else moveDelta.y = -0.1f; if (_references.Controller) _references.Controller.Move(moveDelta); else transform.position += moveDelta; // ── Rotate ────────────────────────────────────────────────────────── switch (_settings.Mode) { case PlayerModes.FirstPerson: if (cam && !_isPaused) { float mouseX = _lookInput.x * _firstPerson.SensitivityX; float mouseY = _lookInput.y * _firstPerson.SensitivityY * -1; _yaw += mouseX; _pitch = Mathf.Clamp(_pitch + mouseY, _firstPerson.MinPitch, _firstPerson.MaxPitch); transform.rotation = Quaternion.Euler(0, _yaw, 0); cam.transform.localRotation = Quaternion.Euler(_pitch, 0, 0); } break; case PlayerModes.ThirdPerson: { Quaternion targetRot = transform.rotation; if (_moveInertia.magnitude > .002f) { Vector3 offset = _moveInertia - _moveDirection; offset *= _moveInertia.magnitude * _runInertia; offset = Vector3.ClampMagnitude(offset, 1f); Vector3 up = (Vector3.up - offset).normalized; Quaternion lookRot = Quaternion.LookRotation(_moveInertia, up); targetRot = Quaternion.Lerp(targetRot, lookRot, t * 30); } transform.rotation = targetRot; break; } } // ── Reset ─────────────────────────────────────────────────────────── _moveInput = Vector2.zero; _lookInput = Vector2.zero; _runInput = false; } public void Teleport(Vector3 position, Quaternion rotation) { _isPaused = _jumpInput = _jumpping = _runInput = false; _runInertia = _jumpLerp = _jumpTime = _jumpAltitude = 0; _moveInput = _lookInput = Vector2.zero; _moveInertia = _moveDirection = Vector3.zero; _yaw = rotation.eulerAngles.y; _pitch = 0; if (_references.Controller) _references.Controller.enabled = false; transform.position = position; transform.rotation = rotation; if (_references.Controller) _references.Controller.enabled = true; } public void Pause(bool pause) { _isPaused = pause; if (OctoCore.Instance) OctoCore.Instance.UI.SetCursor(); } // Convenience methods for external scripts public void SetCanJump(bool value) => _settings.CanJump = value; public void SetCanRun(bool value) => _settings.CanRun = value; private void Move(Vector2 value) { _moveInput = value; } private void Look(Vector2 value) { _lookInput = value; } private void Run() { _runInput = true; } private void Jump() { _jumpInput = true; } private void SetMode() { switch (_settings.Mode) { case PlayerModes.FirstPerson: break; case PlayerModes.ThirdPerson: break; } } } }