using Unity.Netcode; using Unity.Netcode.Components; using UnityEngine; using UnityEngine.InputSystem; namespace Jankenbots.Prototype { /// /// JANKENBOTS — networked run-around PLAYER avatar (the rigged low-poly /// character), PLUS the on-foot ↔ driving control handoff. /// /// CONTROL MODEL (the spine): a player is in exactly one mode, derived from a /// single fact — do I currently hold a bot seat? (). /// • ON FOOT (no seat) → WASD drives THIS avatar; free-roaming. /// • DRIVING (holds a seat)→ avatar TELEPORTS to its player stand and FREEZES; /// it refuses all input, so control transfers and /// locks to the claimed tread (TreadPart already only /// listens when you hold its seat). Release the seat → /// teleport back and resume on-foot control. /// This is what resolves the WASD overlap: PlayerAvatar and TreadPart become /// mutually exclusive off one flag — never both live at once. /// /// NETCODE: OWNER-authoritative movement (see ). /// Teleports use NetworkTransform.Teleport so observers don't see a slide. /// /// ANIMATION: derived locally on every client from measured horizontal speed → /// Animator "Speed" → Idle/Walk/Run blend (stays in sync with no extra traffic). /// [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; CharacterController _cc; Animator _anim; NetworkTransform _nt; SeatManager _seats; Transform _standLeft, _standRight; // where each pilot teleports (found by name) float _vy; // vertical velocity (gravity) Vector3 _horizVel; // smoothed horizontal velocity (ramped, not instant) Vector3 _lastPos; float _animSpeed; bool _seated; // am I (owner) currently piloting a tread? Vector3 _returnPos; // where to drop me back when I leave the seat bool _hasReturn; static readonly int SpeedHash = Animator.StringToHash("Speed"); /// True while the LOCAL owner is seated/driving — read by the camera director. public bool IsSeatedLocal => _seated; void Awake() { _cc = GetComponent(); _anim = GetComponentInChildren(); _nt = GetComponent(); _lastPos = transform.position; } public override void OnNetworkSpawn() { _lastPos = transform.position; _seats = FindFirstObjectByType(); var sl = GameObject.Find("StandPoint_0"); if (sl != null) _standLeft = sl.transform; var sr = GameObject.Find("StandPoint_1"); if (sr != null) _standRight = sr.transform; // 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) { // Derive mode from seat ownership and handle the transition. SeatManager.Seat? seat = LocalSeat(); bool wantSeated = seat.HasValue; if (wantSeated != _seated) { if (wantSeated) EnterSeated(seat.Value); else ExitSeated(); } // Claim / leave a tread by KEY. On-foot mouse-look locks the cursor, so // the OnGUI Claim/Leave buttons aren't clickable — E claims, Q leaves. var kb = Keyboard.current; if (_seats != null && kb != null) { if (!_seated && kb.eKey.wasPressedThisFrame) _seats.ClaimSeatRpc(); else if (_seated && kb.qKey.wasPressedThisFrame) _seats.ReleaseSeatRpc(); } // On foot → drive the avatar. Seated → do nothing (control is on the tread). if (!_seated) 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); } // Which seat (if any) the local player holds — the single source of truth for mode. SeatManager.Seat? LocalSeat() { if (_seats == null || NetworkManager.Singleton == null) return null; ulong me = NetworkManager.Singleton.LocalClientId; if (_seats.PilotOf(SeatManager.Seat.Left) == me) return SeatManager.Seat.Left; if (_seats.PilotOf(SeatManager.Seat.Right) == me) return SeatManager.Seat.Right; return null; } void EnterSeated(SeatManager.Seat seat) { _seated = true; _returnPos = transform.position; // remember where I left the arena _hasReturn = true; _horizVel = Vector3.zero; _vy = 0f; Transform sp = seat == SeatManager.Seat.Left ? _standLeft : _standRight; if (sp != null) TeleportTo(sp.position, sp.rotation); } void ExitSeated() { _seated = false; _horizVel = Vector3.zero; _vy = 0f; if (_hasReturn) TeleportTo(_returnPos + Vector3.up * 0.2f, transform.rotation); } // Interpolation-free teleport (CharacterController must be off to move it directly; // NetworkTransform.Teleport keeps observers from seeing a slide across the map). void TeleportTo(Vector3 pos, Quaternion rot) { bool ccWas = _cc.enabled; _cc.enabled = false; transform.SetPositionAndRotation(pos, rot); _cc.enabled = ccWas; _lastPos = pos; if (_nt != null) _nt.Teleport(pos, rot, transform.localScale); } 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; } // CAMERA-RELATIVE movement: W goes the way the camera (mouse) is looking, // not world +Z. Basis is the active game camera flattened onto the ground. Vector3 camF = Vector3.forward, camR = Vector3.right; var cam = Camera.main; if (cam != null) { camF = Vector3.ProjectOnPlane(cam.transform.forward, Vector3.up); if (camF.sqrMagnitude < 1e-4f) camF = Vector3.ProjectOnPlane(cam.transform.up, Vector3.up); // looking straight down camF.Normalize(); camR = Vector3.ProjectOnPlane(cam.transform.right, Vector3.up).normalized; } Vector3 input = camR * h + camF * v; if (input.sqrMagnitude > 1f) input.Normalize(); Vector3 desired = input * moveSpeed; // Ramp velocity toward the target instead of snapping (smooth start/stop). 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); 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); } } } }