BallScript.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. using UnityEngine;
  2. using UnityEngine.InputSystem;
  3. [RequireComponent(typeof(Rigidbody2D))]
  4. [RequireComponent(typeof(Collider2D))]
  5. public class BallScript : MonoBehaviour
  6. {
  7. [Header("Launch")]
  8. [SerializeField] private float launchSpeed = 10f;
  9. [Tooltip("Maximum random angle (degrees) from straight up on launch.")]
  10. [SerializeField] private float maxLaunchAngle = 30f;
  11. [Tooltip("Base damage before ball effects modify it.")]
  12. [SerializeField] private float baseDamage = 1f;
  13. [Header("References")]
  14. [Tooltip("Assign the Paddle transform in the Inspector, or tag the paddle 'Paddle' for auto-find.")]
  15. [SerializeField] private Transform paddle;
  16. [Tooltip("Offset above the paddle centre where the ball sits before launch.")]
  17. [SerializeField] private Vector3 spawnOffset = new Vector3(0f, 0.7f, 0f);
  18. public float LaunchSpeed => launchSpeed;
  19. public float BaseDamage => baseDamage;
  20. public Rigidbody2D Body => _rb;
  21. public bool IsQueueManaged => _isQueueManaged;
  22. public bool IsInPlay => _rb != null && _rb.simulated && _collider != null && _collider.enabled;
  23. private Rigidbody2D _rb;
  24. private Collider2D _collider;
  25. private Renderer _renderer;
  26. private Material[] _baseMaterials;
  27. private bool _launched;
  28. private bool _followPaddleWhenUnlaunched = true;
  29. private bool _isQueueManaged;
  30. private BallEffectBase[] _effects;
  31. private Vector3 _initialScale;
  32. private float _damageMultiplier = 1f;
  33. private Vector2 _prePhysicsVelocity;
  34. void Awake()
  35. {
  36. _rb = GetComponent<Rigidbody2D>();
  37. _collider = GetComponent<Collider2D>();
  38. _renderer = GetComponentInChildren<Renderer>();
  39. _effects = GetComponents<BallEffectBase>();
  40. _rb.gravityScale = 0f;
  41. _rb.linearDamping = 0f;
  42. _rb.angularDamping = 0f;
  43. _rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
  44. _initialScale = transform.localScale;
  45. // Use physics to handle bouncing naturally - bounciness = 1 means perfect reflection.
  46. // Friction = 0 means no slowdown.
  47. var mat = new PhysicsMaterial2D { bounciness = 1f, friction = 0f };
  48. _collider.sharedMaterial = mat;
  49. if (_renderer != null)
  50. _baseMaterials = _renderer.sharedMaterials;
  51. }
  52. void Start()
  53. {
  54. if (paddle == null)
  55. paddle = GameObject.FindGameObjectWithTag("Paddle")?.transform;
  56. _launched = false;
  57. }
  58. void Update()
  59. {
  60. if (_launched || !_followPaddleWhenUnlaunched)
  61. return;
  62. // Follow the paddle until launched.
  63. if (paddle != null)
  64. transform.position = paddle.position + spawnOffset;
  65. bool spacePressed = Keyboard.current != null && Keyboard.current.spaceKey.wasPressedThisFrame;
  66. bool mousePressed = Mouse.current != null && Mouse.current.leftButton.wasPressedThisFrame;
  67. if (spacePressed || mousePressed)
  68. Launch();
  69. }
  70. void FixedUpdate()
  71. {
  72. if (!_launched)
  73. return;
  74. // Store incoming velocity for collision-response effects (e.g. piercing).
  75. _prePhysicsVelocity = _rb.linearVelocity;
  76. Vector2 velocity = _rb.linearVelocity;
  77. for (int i = 0; i < _effects.Length; i++)
  78. {
  79. BallEffectBase effect = _effects[i];
  80. if (effect != null && effect.isActiveAndEnabled)
  81. effect.ModifyVelocity(this, ref velocity, Time.fixedDeltaTime);
  82. }
  83. _rb.linearVelocity = velocity;
  84. }
  85. public void SetQueueManaged(bool value)
  86. {
  87. _isQueueManaged = value;
  88. }
  89. public void SetPaddleReference(Transform paddleTransform, Vector3 offset)
  90. {
  91. paddle = paddleTransform;
  92. spawnOffset = offset;
  93. }
  94. public void ParkAtPosition(Vector3 worldPosition)
  95. {
  96. _launched = false;
  97. _followPaddleWhenUnlaunched = false;
  98. _rb.linearVelocity = Vector2.zero;
  99. _rb.simulated = false;
  100. _collider.enabled = false;
  101. transform.position = worldPosition;
  102. }
  103. public void PrepareOnPaddle()
  104. {
  105. _launched = false;
  106. _followPaddleWhenUnlaunched = true;
  107. _rb.simulated = true;
  108. _collider.enabled = true;
  109. _rb.linearVelocity = Vector2.zero;
  110. _damageMultiplier = 1f;
  111. transform.localScale = _initialScale;
  112. if (paddle == null)
  113. paddle = GameObject.FindGameObjectWithTag("Paddle")?.transform;
  114. if (paddle != null)
  115. transform.position = paddle.position + spawnOffset;
  116. for (int i = 0; i < _effects.Length; i++)
  117. {
  118. BallEffectBase effect = _effects[i];
  119. if (effect != null && effect.isActiveAndEnabled)
  120. effect.OnBallReset(this);
  121. }
  122. }
  123. /// <summary>Called by DeathZoneScript for fallback non-queue behavior.</summary>
  124. public void ResetBall()
  125. {
  126. PrepareOnPaddle();
  127. }
  128. public void SetEffectDefinition(BallEffectDefinition effectDefinition)
  129. {
  130. // Disable all currently attached effects first.
  131. BallEffectBase[] allEffects = GetComponents<BallEffectBase>();
  132. for (int i = 0; i < allEffects.Length; i++)
  133. allEffects[i].enabled = false;
  134. ApplyOverlayMaterial(effectDefinition != null ? effectDefinition.OverlayMaterial : null);
  135. if (effectDefinition == null)
  136. {
  137. Debug.Log($"[{gameObject.name}] SetEffectDefinition: Received NULL definition (no effect assigned)");
  138. RebuildEffectsCache();
  139. return;
  140. }
  141. Debug.Log($"[{gameObject.name}] SetEffectDefinition: Assigning effect type: {effectDefinition.EffectType}");
  142. MonoBehaviour selected = null;
  143. switch (effectDefinition.EffectType)
  144. {
  145. case BallEffectType.TripleSplit:
  146. Debug.Log($"[{gameObject.name}] Matched TripleSplit case");
  147. selected = GetOrAddEffect<TripleSplitEffect>();
  148. break;
  149. case BallEffectType.Explosive:
  150. Debug.Log($"[{gameObject.name}] Matched Explosive case");
  151. selected = GetOrAddEffect<ExplosiveHitEffect>();
  152. break;
  153. case BallEffectType.Piercing:
  154. Debug.Log($"[{gameObject.name}] Matched Piercing case");
  155. selected = GetOrAddEffect<PiercingEffect>();
  156. break;
  157. case BallEffectType.Homing:
  158. Debug.Log($"[{gameObject.name}] Matched Homing case");
  159. selected = GetOrAddEffect<HomingEffect>();
  160. break;
  161. case BallEffectType.Teleport:
  162. Debug.Log($"[{gameObject.name}] Matched Teleport case");
  163. selected = GetOrAddEffect<TeleportEffect>();
  164. break;
  165. case BallEffectType.None:
  166. default:
  167. Debug.LogError($"[{gameObject.name}] ERROR: EffectType is None or unrecognized: {effectDefinition.EffectType}");
  168. break;
  169. }
  170. if (selected is BallEffectBase effectBase)
  171. {
  172. Debug.Log($"[{gameObject.name}] Successfully enabled effect: {selected.GetType().Name}");
  173. effectBase.SetDefinition(effectDefinition);
  174. selected.enabled = true;
  175. }
  176. else
  177. {
  178. Debug.LogError($"[{gameObject.name}] ERROR: selected component is not a BallEffectBase! Type: {selected?.GetType().Name ?? "NULL"}");
  179. }
  180. RebuildEffectsCache();
  181. }
  182. public void ApplyOverlayMaterial(Material overlayMaterial)
  183. {
  184. if (_renderer == null)
  185. return;
  186. if (overlayMaterial == null)
  187. {
  188. _renderer.sharedMaterials = _baseMaterials;
  189. return;
  190. }
  191. Material[] stacked = new Material[_baseMaterials.Length + 1];
  192. for (int i = 0; i < _baseMaterials.Length; i++)
  193. stacked[i] = _baseMaterials[i];
  194. stacked[stacked.Length - 1] = overlayMaterial;
  195. _renderer.sharedMaterials = stacked;
  196. }
  197. public float GetDamageAgainstBlock(blockScript block, Collision2D collision)
  198. {
  199. float damage = baseDamage * _damageMultiplier;
  200. for (int i = 0; i < _effects.Length; i++)
  201. {
  202. BallEffectBase effect = _effects[i];
  203. if (effect != null && effect.isActiveAndEnabled)
  204. damage = effect.ModifyBlockDamage(this, block, collision, damage);
  205. }
  206. return Mathf.Max(0f, damage);
  207. }
  208. public void NotifyHitBlock(blockScript block, Collision2D collision)
  209. {
  210. for (int i = 0; i < _effects.Length; i++)
  211. {
  212. BallEffectBase effect = _effects[i];
  213. if (effect != null && effect.isActiveAndEnabled)
  214. effect.OnHitBlock(this, block, collision);
  215. }
  216. }
  217. public void MultiplyDamage(float multiplier)
  218. {
  219. _damageMultiplier *= multiplier;
  220. }
  221. public void MultiplyScale(float scaleMultiplier)
  222. {
  223. transform.localScale *= scaleMultiplier;
  224. }
  225. public float GetCurrentSpeed()
  226. {
  227. float speed = _rb.linearVelocity.magnitude;
  228. return speed > 0.001f ? speed : launchSpeed;
  229. }
  230. public Vector2 GetPrePhysicsVelocity()
  231. {
  232. if (_prePhysicsVelocity.sqrMagnitude > 0.0001f)
  233. return _prePhysicsVelocity;
  234. return _rb.linearVelocity;
  235. }
  236. public void LaunchInDirection(Vector2 direction, float speed)
  237. {
  238. _launched = true;
  239. _followPaddleWhenUnlaunched = false;
  240. _rb.simulated = true;
  241. _collider.enabled = true;
  242. Vector2 dir = direction.sqrMagnitude > 0.0001f ? direction.normalized : Vector2.up;
  243. _rb.linearVelocity = dir * speed;
  244. }
  245. public BallScript SpawnClone(Vector2 direction, float speedMultiplier, float scaleMultiplier, float damageMultiplier)
  246. {
  247. GameObject cloneObject = Instantiate(gameObject, transform.position, Quaternion.identity);
  248. BallScript cloneBall = cloneObject.GetComponent<BallScript>();
  249. if (cloneBall == null)
  250. return null;
  251. // Clones are temporary gameplay balls and do not consume queue slots.
  252. cloneBall.SetQueueManaged(false);
  253. cloneBall.SetPaddleReference(paddle, spawnOffset);
  254. cloneBall.LaunchInDirection(direction, launchSpeed * speedMultiplier);
  255. cloneBall.MultiplyScale(scaleMultiplier);
  256. cloneBall.MultiplyDamage(damageMultiplier);
  257. return cloneBall;
  258. }
  259. private void Launch()
  260. {
  261. _launched = true;
  262. float angle = Random.Range(-maxLaunchAngle, maxLaunchAngle) * Mathf.Deg2Rad;
  263. _rb.linearVelocity = new Vector2(Mathf.Sin(angle), Mathf.Cos(angle)) * launchSpeed;
  264. for (int i = 0; i < _effects.Length; i++)
  265. {
  266. BallEffectBase effect = _effects[i];
  267. if (effect != null && effect.isActiveAndEnabled)
  268. effect.OnBallLaunched(this);
  269. }
  270. }
  271. private T GetOrAddEffect<T>() where T : MonoBehaviour
  272. {
  273. T existing = GetComponent<T>();
  274. if (existing != null)
  275. return existing;
  276. return gameObject.AddComponent<T>();
  277. }
  278. private void RebuildEffectsCache()
  279. {
  280. _effects = GetComponents<BallEffectBase>();
  281. }
  282. }