using Unity.Netcode;
using UnityEngine;
using UnityEngine.InputSystem;
namespace Jankenbots.Prototype
{
///
/// JANKENBOTS — networked run-around PLAYER avatar (the rigged low-poly
/// character). This is the foundation for the timed junkyard "gather parts"
/// phase: each player controls one of these, runs around, and (later) hauls
/// scrap back to build the bot. Driving the finished bot happens from player
/// stands, so the avatar and the tread controls are separate systems.
///
/// NETCODE: OWNER-authoritative. Only the owning client reads input and moves
/// its CharacterController; an on the
/// same object replicates the resulting motion to everyone. No host round-trip
/// on your own movement → it feels responsive.
///
/// ANIMATION: derived LOCALLY on every client from the avatar's measured
/// horizontal speed (position delta / dt), fed into the Animator's "Speed"
/// float, which drives an Idle→Walk→Run 1-D blend tree. Because observers
/// measure the same replicated motion the owner produces, the animation stays
/// in sync with zero extra network traffic — no NetworkAnimator needed.
///
/// NOTE: reads WASD, same keys as the bot's TreadPart — fine while testing the
/// avatar in isolation; the real game separates "running" from "driving" via
/// the (future) player-stand flow, so a player is never doing both at once.
///
[RequireComponent(typeof(CharacterController))]
public class PlayerAvatar : NetworkBehaviour
{
[Header("Movement")]
[Tooltip("Run speed in m/s.")]
public float moveSpeed = 5f;
[Tooltip("How fast the avatar ramps UP to full speed (m/s^2). Lower = floatier start.")]
public float acceleration = 45f;
[Tooltip("How fast the avatar ramps DOWN to a stop (m/s^2). Higher = snappier stop.")]
public float deceleration = 60f;
[Tooltip("How fast the avatar turns to face its movement direction (deg/s).")]
public float turnSpeed = 720f;
[Tooltip("Gravity (m/s^2, negative). Keeps the CharacterController grounded.")]
public float gravity = -20f;
[Header("Animation")]
[Tooltip("Speed value the Animator treats as full-run, for normalising the blend.")]
public float runAnimSpeed = 5f;
CharacterController _cc;
Animator _anim;
float _vy; // vertical velocity (gravity)
Vector3 _horizVel; // smoothed horizontal velocity (ramped, not instant)
Vector3 _lastPos;
float _animSpeed;
static readonly int SpeedHash = Animator.StringToHash("Speed");
void Awake()
{
_cc = GetComponent();
_anim = GetComponentInChildren();
_lastPos = transform.position;
}
public override void OnNetworkSpawn()
{
_lastPos = transform.position;
// Fan owners out a little so multiple players don't stack on the origin.
if (IsOwner)
{
float a = OwnerClientId * 1.3f;
transform.position += new Vector3(Mathf.Cos(a), 0f, Mathf.Sin(a)) * (2f + OwnerClientId);
_lastPos = transform.position;
}
}
void Update()
{
if (!IsSpawned) return;
if (IsOwner) OwnerMove();
// Animation on EVERY client, from measured horizontal speed.
Vector3 delta = transform.position - _lastPos;
delta.y = 0f;
float measured = delta.magnitude / Mathf.Max(Time.deltaTime, 1e-4f);
_lastPos = transform.position;
_animSpeed = Mathf.Lerp(_animSpeed, measured, 12f * Time.deltaTime);
if (_anim != null) _anim.SetFloat(SpeedHash, _animSpeed);
}
void OwnerMove()
{
var kb = Keyboard.current;
float h = 0f, v = 0f;
if (kb != null)
{
if (kb.aKey.isPressed || kb.leftArrowKey.isPressed) h -= 1f;
if (kb.dKey.isPressed || kb.rightArrowKey.isPressed) h += 1f;
if (kb.wKey.isPressed || kb.upArrowKey.isPressed) v += 1f;
if (kb.sKey.isPressed || kb.downArrowKey.isPressed) v -= 1f;
}
Vector3 input = new Vector3(h, 0f, v);
if (input.sqrMagnitude > 1f) input.Normalize();
Vector3 desired = input * moveSpeed;
// Ramp velocity toward the target instead of snapping — this is what kills
// the "jerky/jarring" feel and lets the anim blend Idle→Walk→Run smoothly.
float rate = input.sqrMagnitude > 0.001f ? acceleration : deceleration;
_horizVel = Vector3.MoveTowards(_horizVel, desired, rate * Time.deltaTime);
if (_cc.isGrounded && _vy < 0f) _vy = -2f;
_vy += gravity * Time.deltaTime;
_cc.Move((_horizVel + new Vector3(0f, _vy, 0f)) * Time.deltaTime);
// Face the way we're actually moving (uses the smoothed velocity, so turns
// ease too rather than snapping on a key change).
if (_horizVel.sqrMagnitude > 0.05f)
{
Quaternion target = Quaternion.LookRotation(new Vector3(_horizVel.x, 0f, _horizVel.z));
transform.rotation = Quaternion.RotateTowards(transform.rotation, target, turnSpeed * Time.deltaTime);
}
}
}
}