using System; using Core.Domain.Status; using Core.Domain.Status.Effects; using KBCore.Refs; using UnityEngine; using UnityEngine.InputSystem; namespace Infrastructure.Unity { [RequireComponent(typeof(Rigidbody))] public class PlayerController : MonoBehaviour { [Header("Movement Settings")] [SerializeField] private float moveSpeed = 8f; [SerializeField] private float maxVelocityChange = 10f; [SerializeField] private float snapForce = 15f; [Header("Jump Settings")] [SerializeField] private float jumpForce = 12f; // Adjusted for snappy feel [SerializeField] private float coyoteTime = 0.1f; // Grace period after falling [SerializeField] private float gravityMultiplier = 2.5f; // Stronger gravity for less "floaty" feel [Header("Controls")] [SerializeField] private bool useCameraRelativeMovement = true; [Header("Interaction")] [SerializeField] private LayerMask tileLayer; [SerializeField] private float groundCheckDistance = 1.1f; // Slightly more than half player height [Self][SerializeField] private Rigidbody rb; [Self][SerializeField] private MeshRenderer meshRenderer; private InputSystem_Actions _actions; private Vector2 _moveInput; private Transform _camTransform; private MaterialPropertyBlock _propBlock; private static readonly int ColorProperty = Shader.PropertyToID("_BaseColor"); private static readonly int EmissionColorProperty = Shader.PropertyToID("_EmissionColor"); private Color _defaultColor = Color.white; private bool _isGrounded; private float _coyoteTimeCounter; private bool _jumpPressed; private Vector3 _currentTileCentroid; public Rigidbody Rigidbody => rb; public StatusManager Status { get; private set; } private void OnEnable() { _actions.Player.Enable(); _actions.Player.Move.performed += OnMovePerformed; _actions.Player.Move.canceled += OnMoveCanceled; _actions.Player.Jump.performed += OnJumpPerformed; } private void OnDisable() { _actions.Player.Move.performed -= OnMovePerformed; _actions.Player.Move.canceled -= OnMoveCanceled; _actions.Player.Jump.performed -= OnJumpPerformed; _actions.Player.Disable(); } private void Awake() { _actions = new InputSystem_Actions(); Status = new StatusManager(); if (Camera.main) { _camTransform = Camera.main.transform; } rb.freezeRotation = true; rb.useGravity = false; _propBlock = new MaterialPropertyBlock(); if (meshRenderer.material.HasProperty(ColorProperty)) { _defaultColor = meshRenderer.material.GetColor(ColorProperty); } } private void Update() { Status.Tick(Time.deltaTime); UpdateVisuals(); if (_isGrounded) { _coyoteTimeCounter = coyoteTime; } else { _coyoteTimeCounter -= Time.deltaTime; } if (_jumpPressed) { _jumpPressed = false; if (_coyoteTimeCounter > 0f || Status.CurrentCapabilities.CanHover) { PerformJump(); } } } private void FixedUpdate() { CheckGround(); HandleMovement(); ApplyGravity(); // Interaction with tiles (Decay Logic) if (_isGrounded && Status.CurrentCapabilities.CanTriggerDecay) { InteractWithGround(); } } private void CheckGround() { if (Physics.SphereCast(transform.position, 0.3f, Vector3.down, out var hit, groundCheckDistance, tileLayer)) { _isGrounded = true; _currentTileCentroid = hit.collider.transform.position; } else { _isGrounded = false; } } private void PerformJump() { var vel = rb.linearVelocity; vel.y = 0; rb.linearVelocity = vel; rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse); _coyoteTimeCounter = 0f; _isGrounded = false; } private void ApplyGravity() { if (Status.CurrentCapabilities.CanHover) return; var gravity = Physics.gravity.y * gravityMultiplier; rb.AddForce(Vector3.up * gravity, ForceMode.Acceleration); } private void HandleMovement() { var currentSpeed = moveSpeed * Status.CurrentCapabilities.SpeedMultiplier; var targetVelocity = Vector3.zero; var snapAxis = Vector3.zero; Vector3 desiredDirection; if (_moveInput.sqrMagnitude < 0.01f) { desiredDirection = Vector3.zero; } else if (useCameraRelativeMovement && _camTransform) { var camForward = _camTransform.forward; var camRight = _camTransform.right; camForward.y = 0; camRight.y = 0; camForward.Normalize(); camRight.Normalize(); desiredDirection = (camForward * _moveInput.y + camRight * _moveInput.x).normalized; } else { desiredDirection = new Vector3(_moveInput.x, 0, _moveInput.y).normalized; } if (desiredDirection.sqrMagnitude > 0.01f) { if (Mathf.Abs(desiredDirection.x) > Mathf.Abs(desiredDirection.z)) { targetVelocity = new Vector3(Mathf.Sign(desiredDirection.x) * currentSpeed, 0, 0); snapAxis = Vector3.forward; } else { targetVelocity = new Vector3(0, 0, Mathf.Sign(desiredDirection.z) * currentSpeed); snapAxis = Vector3.right; } } var velocity = rb.linearVelocity; var velocityChange = (targetVelocity - velocity); velocityChange.x = Mathf.Clamp(velocityChange.x, -maxVelocityChange, maxVelocityChange); velocityChange.z = Mathf.Clamp(velocityChange.z, -maxVelocityChange, maxVelocityChange); velocityChange.y = 0f; rb.AddForce(velocityChange, ForceMode.VelocityChange); if (snapAxis != Vector3.zero) { ApplySnapping(snapAxis); } else { ApplySnapping(Vector3.right); ApplySnapping(Vector3.forward); } } private void ApplySnapping(Vector3 axis) { if (!_isGrounded) return; var targetPos = Vector3.Dot(_currentTileCentroid, axis); var currentPos = Vector3.Dot(transform.position, axis); var diff = targetPos - currentPos; var currentVelOnAxis = Vector3.Dot(rb.linearVelocity, axis); var targetVel = diff * snapForce; var correction = axis * (targetVel - currentVelOnAxis); rb.AddForce(correction, ForceMode.VelocityChange); } private void InteractWithGround() { if (Physics.SphereCast(transform.position, 0.3f, Vector3.down, out var hit, groundCheckDistance, tileLayer)) { if (hit.collider.TryGetComponent(out var tileAdapter)) { tileAdapter.OnPlayerStep(); } } } private void OnMovePerformed(InputAction.CallbackContext ctx) { _moveInput = ctx.ReadValue(); } private void OnMoveCanceled(InputAction.CallbackContext ctx) { _moveInput = Vector2.zero; } private void OnJumpPerformed(InputAction.CallbackContext ctx) { if (ctx.performed) _jumpPressed = true; } private void UpdateVisuals() { var targetColor = _defaultColor; var emissionColor = Color.black; var caps = Status.CurrentCapabilities; if (caps.CanHover) { targetColor = EffectColors.HoverColor; emissionColor = EffectColors.HoverColor * 0.5f; } else if (!caps.CanTriggerDecay) { targetColor = EffectColors.LightFootedColor; } else if (caps.SpeedMultiplier > 1.2f) { targetColor = EffectColors.SpeedBoostColor; emissionColor = EffectColors.SpeedBoostColor * 0.5f; } meshRenderer.GetPropertyBlock(_propBlock); var currentColor = _propBlock.GetColor(ColorProperty); if (currentColor.a == 0) currentColor = _defaultColor; var newColor = Color.Lerp(currentColor, targetColor, Time.deltaTime * 5f); _propBlock.SetColor(ColorProperty, newColor); _propBlock.SetColor(EmissionColorProperty, emissionColor); meshRenderer.SetPropertyBlock(_propBlock); } } }