PuckController.cs 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. using UnityEngine;
  2. public class PuckController : MonoBehaviour
  3. {
  4. [Header("Physics Settings")]
  5. [SerializeField] private float mass = 0.17f; // ~170g (regulation puck)
  6. [SerializeField] private float drag = 0.5f;
  7. [SerializeField] private float angularDrag = 0.5f;
  8. [SerializeField] private float friction = 0.3f;
  9. [Header("Possession Settings")]
  10. [SerializeField] private float possessionRadius = 1.5f; // Distance to consider "controlling" the puck
  11. [SerializeField] private float carrierOffset = 1.0f; // How far in front of player when carrying
  12. [Header("Visual Feedback")]
  13. [SerializeField] private Material normalMaterial;
  14. [SerializeField] private Material possessedMaterial;
  15. [Header("Interception Settings")]
  16. [SerializeField] private bool canBeIntercepted = true;
  17. [SerializeField] private float interceptionCheckRadius = 0.5f;
  18. [SerializeField] private LayerMask playerLayer;
  19. private bool isPassActive = false;
  20. private float passStartTime = 0f;
  21. private float passProtectionTime = 0.1f; // Short window where pass can't be intercepted
  22. private Rigidbody rb;
  23. private MeshRenderer meshRenderer;
  24. private PlayerController currentCarrier;
  25. private bool isBeingCarried = false;
  26. private Vector3 targetCarryPosition;
  27. public PlayerController CurrentCarrier => currentCarrier;
  28. public bool IsLoose => currentCarrier == null;
  29. void Awake()
  30. {
  31. SetupPhysics();
  32. SetupVisuals();
  33. }
  34. void SetupPhysics()
  35. {
  36. rb = GetComponent<Rigidbody>();
  37. if (rb == null)
  38. {
  39. rb = gameObject.AddComponent<Rigidbody>();
  40. }
  41. rb.mass = mass;
  42. rb.linearDamping = drag;
  43. rb.angularDamping = angularDrag;
  44. rb.collisionDetectionMode = CollisionDetectionMode.Continuous;
  45. rb.interpolation = RigidbodyInterpolation.Interpolate;
  46. // IMPORTANT: Make sure puck is NOT kinematic at start
  47. rb.isKinematic = false;
  48. // Freeze rotation on X and Z to keep puck flat
  49. rb.constraints = RigidbodyConstraints.FreezeRotationX | RigidbodyConstraints.FreezeRotationZ;
  50. // Set up collider if not present
  51. SphereCollider collider = GetComponent<SphereCollider>();
  52. if (collider == null)
  53. {
  54. collider = gameObject.AddComponent<SphereCollider>();
  55. }
  56. collider.radius = 0.038f; // ~76mm diameter regulation puck
  57. collider.isTrigger = false; // MUST be false for physics collisions
  58. // Create physics material for ice-like behavior
  59. PhysicsMaterial puckPhysicsMaterial = new PhysicsMaterial("PuckPhysics");
  60. puckPhysicsMaterial.dynamicFriction = friction;
  61. puckPhysicsMaterial.staticFriction = friction;
  62. puckPhysicsMaterial.bounciness = 0.1f; // Reduced bounce
  63. puckPhysicsMaterial.frictionCombine = PhysicsMaterialCombine.Minimum;
  64. puckPhysicsMaterial.bounceCombine = PhysicsMaterialCombine.Minimum; // Changed to Minimum
  65. collider.material = puckPhysicsMaterial;
  66. }
  67. void SetupVisuals()
  68. {
  69. meshRenderer = GetComponent<MeshRenderer>();
  70. if (meshRenderer == null)
  71. {
  72. // Create a basic cylinder for the puck
  73. GameObject cylinder = GameObject.CreatePrimitive(PrimitiveType.Cylinder);
  74. cylinder.transform.SetParent(transform);
  75. cylinder.transform.localPosition = Vector3.zero;
  76. cylinder.transform.localScale = new Vector3(0.076f, 0.0125f, 0.076f); // Regulation size
  77. meshRenderer = cylinder.GetComponent<MeshRenderer>();
  78. // Destroy the collider from the primitive (we have our own)
  79. Destroy(cylinder.GetComponent<Collider>());
  80. }
  81. }
  82. void FixedUpdate()
  83. {
  84. if (isBeingCarried && currentCarrier != null)
  85. {
  86. // Keep puck attached to player
  87. Vector3 carrierForward = currentCarrier.transform.forward;
  88. targetCarryPosition = currentCarrier.transform.position + carrierForward * carrierOffset;
  89. targetCarryPosition.y = 0.025f;
  90. transform.position = targetCarryPosition;
  91. if (rb != null)
  92. {
  93. rb.linearVelocity = Vector3.zero;
  94. rb.angularVelocity = Vector3.zero;
  95. }
  96. }
  97. else
  98. {
  99. // Check for interceptions during passes
  100. if (isPassActive && canBeIntercepted)
  101. {
  102. CheckForInterception();
  103. }
  104. // Keep puck at proper height on ice when loose
  105. if (transform.position.y < 0.025f)
  106. {
  107. Vector3 pos = transform.position;
  108. pos.y = 0.025f;
  109. transform.position = pos;
  110. if (rb != null)
  111. {
  112. Vector3 vel = rb.linearVelocity;
  113. if (vel.y < 0)
  114. {
  115. vel.y = 0;
  116. rb.linearVelocity = vel;
  117. }
  118. if (rb.angularVelocity.magnitude > 10f)
  119. {
  120. rb.angularVelocity *= 0.9f;
  121. }
  122. }
  123. }
  124. // End pass if puck has slowed down
  125. if (isPassActive && rb != null && rb.linearVelocity.magnitude < 1f)
  126. {
  127. isPassActive = false;
  128. }
  129. }
  130. }
  131. private void CheckForInterception()
  132. {
  133. // Skip interception check during protection window
  134. if (Time.time - passStartTime < passProtectionTime)
  135. return;
  136. // Find nearby players
  137. Collider[] nearbyColliders = Physics.OverlapSphere(transform.position, interceptionCheckRadius, playerLayer);
  138. foreach (var col in nearbyColliders)
  139. {
  140. PlayerController player = col.GetComponent<PlayerController>();
  141. if (player != null && player.CanPickupPuck())
  142. {
  143. // Calculate interception chance based on player stats and positioning
  144. float interceptChance = CalculateInterceptionChance(player);
  145. if (Random.value < interceptChance)
  146. {
  147. Debug.Log($"{player.stats.playerName} intercepts the pass!");
  148. AssignToPlayer(player);
  149. isPassActive = false;
  150. break;
  151. }
  152. }
  153. }
  154. }
  155. private float CalculateInterceptionChance(PlayerController player)
  156. {
  157. // Base chance very low (5%)
  158. float baseChance = 0.05f;
  159. // Increase based on awareness and stick handling
  160. float statBonus = (player.stats.awareness + player.stats.stickHandling) / 200f * 0.1f;
  161. // Increase if player is facing the puck
  162. Vector3 toPuck = (transform.position - player.transform.position).normalized;
  163. float facingBonus = Vector3.Dot(player.transform.forward, toPuck) * 0.05f;
  164. return Mathf.Clamp01(baseChance + statBonus + facingBonus);
  165. }
  166. public void AssignToPlayer(PlayerController player)
  167. {
  168. if (currentCarrier != null)
  169. {
  170. currentCarrier.ReleasePuck();
  171. }
  172. currentCarrier = player;
  173. isBeingCarried = true;
  174. if (player != null)
  175. {
  176. // IMPORTANT: Stop all physics movement immediately
  177. if (rb != null)
  178. {
  179. rb.linearVelocity = Vector3.zero;
  180. rb.angularVelocity = Vector3.zero;
  181. rb.isKinematic = true; // Make kinematic while carried
  182. }
  183. // Position puck at player's feet immediately to prevent bouncing
  184. Vector3 playerForward = player.transform.forward;
  185. Vector3 targetPos = player.transform.position + playerForward * carrierOffset;
  186. targetPos.y = 0.025f; // Puck height
  187. transform.position = targetPos;
  188. player.GainPuck(this);
  189. UpdateVisualState(true);
  190. }
  191. }
  192. public void Release()
  193. {
  194. if (currentCarrier != null)
  195. {
  196. currentCarrier.ReleasePuck();
  197. }
  198. currentCarrier = null;
  199. isBeingCarried = false;
  200. // Re-enable physics
  201. if (rb != null)
  202. {
  203. rb.isKinematic = false;
  204. }
  205. UpdateVisualState(false);
  206. }
  207. public void ApplyForce(Vector3 force, ForceMode mode = ForceMode.Impulse)
  208. {
  209. Release();
  210. rb.AddForce(force, mode);
  211. // Mark as active pass
  212. isPassActive = true;
  213. passStartTime = Time.time;
  214. }
  215. public void SetVelocity(Vector3 velocity)
  216. {
  217. Release();
  218. rb.linearVelocity = velocity;
  219. }
  220. public bool IsWithinPossessionRange(Vector3 playerPosition)
  221. {
  222. return Vector3.Distance(transform.position, playerPosition) <= possessionRadius;
  223. }
  224. private void UpdateVisualState(bool possessed)
  225. {
  226. if (meshRenderer != null)
  227. {
  228. if (possessed && possessedMaterial != null)
  229. {
  230. meshRenderer.material = possessedMaterial;
  231. }
  232. else if (normalMaterial != null)
  233. {
  234. meshRenderer.material = normalMaterial;
  235. }
  236. }
  237. }
  238. // Physics collision - check if any player can pick up the puck
  239. void OnTriggerStay(Collider other)
  240. {
  241. // Only auto-pickup if puck is NOT in active pass and moving slowly
  242. if (IsLoose && !isPassActive && rb != null && rb.linearVelocity.magnitude < 2f)
  243. {
  244. PlayerController player = other.GetComponent<PlayerController>();
  245. if (player != null && player.CanPickupPuck())
  246. {
  247. AssignToPlayer(player);
  248. }
  249. }
  250. }
  251. public float GetSpeed()
  252. {
  253. return rb.linearVelocity.magnitude;
  254. }
  255. public Vector3 GetVelocity()
  256. {
  257. return rb.linearVelocity;
  258. }
  259. }