using OctoCore.Inputs; using System.Globalization; using UnityEngine; using UnityEngine.InputSystem; 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] public class StateMachine { public bool IsFocussed; public bool IsPaused; public bool IsRunning; public float Yaw; public float Pitch; public OctoPlayerDriver Driver; } [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] public 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; } [OctoReadOnly] public StateMachine State; [SerializeField] private References _references; [SerializeField] private Settings _settings; [SerializeField] private Mapping _mapping; [SerializeField] private FirstPerson _firstPerson; private bool _wasPausedBeforeUnfocuss; private bool _jumpInput; private bool _jumpping; private bool _runInput; private float _runInertia; private float _jumpLerp; private float _jumpTime; private float _jumpAltitude; private Vector2 _moveInput; private Vector2 _lookInput; private Vector3 _moveInertia; private Vector3 _moveDirection; public bool IsPaused => State.IsPaused; public float Radius => _references.Controller ? _references.Controller.radius : 0; public FirstPerson _FirstPerson => _firstPerson; /// 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 => State.Yaw; set => State.Yaw = value; } public float Pitch { get => State.Pitch; set => State.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); State.Yaw = transform.eulerAngles.y; State.IsFocussed = Application.isFocused; Pause(State.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() { HandleFocus(); HandleMovement(); } public void Teleport(string positionAndYaw) { string[] s = positionAndYaw.Replace(" ", string.Empty).Split(','); float x = float.Parse(s[0], CultureInfo.InvariantCulture.NumberFormat); float y = float.Parse(s[1], CultureInfo.InvariantCulture.NumberFormat); float z = float.Parse(s[2], CultureInfo.InvariantCulture.NumberFormat); float a = float.Parse(s[3], CultureInfo.InvariantCulture.NumberFormat); Teleport(new Vector3(x, y, z), Quaternion.Euler(0, a, 0)); } public void Teleport(Vector3 position, Quaternion rotation) { State.IsPaused = _jumpInput = _jumpping = _runInput = false; _runInertia = _jumpLerp = _jumpTime = _jumpAltitude = 0; _moveInput = _lookInput = Vector2.zero; _moveInertia = _moveDirection = Vector3.zero; State.Yaw = rotation.eulerAngles.y; State.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) { State.IsPaused = pause; if (OctoCore.Instance) OctoCore.Instance.UI.SetCursor(); } public void Drive(OctoPlayerDriver driver) { State.Driver = driver; } public void ReleaseDrive() { State.Driver = null; } public void SetYaw(float yaw) { State.Yaw = yaw; } public void SetPitch(float pitch) { State.Pitch = pitch; } // 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 HandleFocus() { bool escapePressed = false; bool mouseClicked = false; #if ENABLE_INPUT_SYSTEM escapePressed = Keyboard.current != null && Keyboard.current.escapeKey.wasPressedThisFrame; mouseClicked = Mouse.current != null && ( Mouse.current.leftButton.wasPressedThisFrame || Mouse.current.rightButton.wasPressedThisFrame || Mouse.current.middleButton.wasPressedThisFrame || Mouse.current.forwardButton.wasPressedThisFrame || Mouse.current.backButton.wasPressedThisFrame ); #else escapePressed = Input.GetKeyDown(KeyCode.Escape); mouseClicked = Input.GetMouseButtonDown(0) || Input.GetMouseButtonDown(1) || Input.GetMouseButtonDown(2); #endif if (!Application.isFocused || escapePressed) { if (State.IsFocussed) { State.IsFocussed = false; _wasPausedBeforeUnfocuss = State.IsPaused; Pause(true); } } else if (!State.IsFocussed && mouseClicked) { State.IsFocussed = true; if (!_wasPausedBeforeUnfocuss) Pause(false); } } private void HandleMovement() { _moveInput = (State.IsPaused || !State.IsFocussed) ? 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; } if (State.Driver) State.Driver.ApplyMoveInfluence(move, move.magnitude * t, out move); 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) ); State.IsRunning = IsRunning; 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) { _jumpInput = false; _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.enabled) _references.Controller.Move(moveDelta); else transform.position += moveDelta; // ── Rotate ────────────────────────────────────────────────────────── switch (_settings.Mode) { case PlayerModes.FirstPerson: if (cam && !State.IsPaused) { Vector2 delta = (State.IsPaused || !State.IsFocussed) ? Vector2.zero : _lookInput; float mouseX = delta.x * _firstPerson.SensitivityX; float mouseY = delta.y * _firstPerson.SensitivityY * -1; State.Yaw += mouseX; State.Pitch = Mathf.Clamp(State.Pitch + mouseY, _firstPerson.MinPitch, _firstPerson.MaxPitch); if (State.Driver) State.Driver.ApplyLookInfluence(State.Yaw, State.Pitch, Time.deltaTime, out State.Yaw, out State.Pitch); transform.rotation = Quaternion.Euler(0, State.Yaw, 0); cam.transform.localRotation = Quaternion.Euler(State.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; } private void SetMode() { switch (_settings.Mode) { case PlayerModes.FirstPerson: // Activate First Person Camera break; case PlayerModes.ThirdPerson: // Activate Third Person Camera break; } } } }