using UnityEngine; using UnityEngine.InputSystem; [RequireComponent(typeof(Rigidbody2D))] [RequireComponent(typeof(Collider2D))] public class BallScript : MonoBehaviour { [Header("Launch")] [SerializeField] private float launchSpeed = 10f; [Tooltip("Maximum random angle (degrees) from straight up on launch.")] [SerializeField] private float maxLaunchAngle = 30f; [Tooltip("Base damage before ball effects modify it.")] [SerializeField] private float baseDamage = 1f; [Header("References")] [Tooltip("Assign the Paddle transform in the Inspector, or tag the paddle 'Paddle' for auto-find.")] [SerializeField] private Transform paddle; [Tooltip("Offset above the paddle centre where the ball sits before launch.")] [SerializeField] private Vector3 spawnOffset = new Vector3(0f, 0.7f, 0f); public float LaunchSpeed => launchSpeed; public float BaseDamage => baseDamage; public Rigidbody2D Body => _rb; public bool IsQueueManaged => _isQueueManaged; public bool IsInPlay => _rb != null && _rb.simulated && _collider != null && _collider.enabled; private Rigidbody2D _rb; private Collider2D _collider; private Renderer _renderer; private Material[] _baseMaterials; private bool _launched; private bool _followPaddleWhenUnlaunched = true; private bool _isQueueManaged; private BallEffectBase[] _effects; private Vector3 _initialScale; private float _damageMultiplier = 1f; private Vector2 _prePhysicsVelocity; void Awake() { _rb = GetComponent(); _collider = GetComponent(); _renderer = GetComponentInChildren(); _effects = GetComponents(); _rb.gravityScale = 0f; _rb.linearDamping = 0f; _rb.angularDamping = 0f; _rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous; _initialScale = transform.localScale; // Use physics to handle bouncing naturally - bounciness = 1 means perfect reflection. // Friction = 0 means no slowdown. var mat = new PhysicsMaterial2D { bounciness = 1f, friction = 0f }; _collider.sharedMaterial = mat; if (_renderer != null) _baseMaterials = _renderer.sharedMaterials; } void Start() { if (paddle == null) paddle = GameObject.FindGameObjectWithTag("Paddle")?.transform; _launched = false; } void Update() { if (_launched || !_followPaddleWhenUnlaunched) return; // Follow the paddle until launched. if (paddle != null) transform.position = paddle.position + spawnOffset; bool spacePressed = Keyboard.current != null && Keyboard.current.spaceKey.wasPressedThisFrame; bool mousePressed = Mouse.current != null && Mouse.current.leftButton.wasPressedThisFrame; if (spacePressed || mousePressed) Launch(); } void FixedUpdate() { if (!_launched) return; // Store incoming velocity for collision-response effects (e.g. piercing). _prePhysicsVelocity = _rb.linearVelocity; Vector2 velocity = _rb.linearVelocity; for (int i = 0; i < _effects.Length; i++) { BallEffectBase effect = _effects[i]; if (effect != null && effect.isActiveAndEnabled) effect.ModifyVelocity(this, ref velocity, Time.fixedDeltaTime); } _rb.linearVelocity = velocity; } public void SetQueueManaged(bool value) { _isQueueManaged = value; } public void SetPaddleReference(Transform paddleTransform, Vector3 offset) { paddle = paddleTransform; spawnOffset = offset; } public void ParkAtPosition(Vector3 worldPosition) { _launched = false; _followPaddleWhenUnlaunched = false; _rb.linearVelocity = Vector2.zero; _rb.simulated = false; _collider.enabled = false; transform.position = worldPosition; } public void PrepareOnPaddle() { _launched = false; _followPaddleWhenUnlaunched = true; _rb.simulated = true; _collider.enabled = true; _rb.linearVelocity = Vector2.zero; _damageMultiplier = 1f; transform.localScale = _initialScale; if (paddle == null) paddle = GameObject.FindGameObjectWithTag("Paddle")?.transform; if (paddle != null) transform.position = paddle.position + spawnOffset; for (int i = 0; i < _effects.Length; i++) { BallEffectBase effect = _effects[i]; if (effect != null && effect.isActiveAndEnabled) effect.OnBallReset(this); } } /// Called by DeathZoneScript for fallback non-queue behavior. public void ResetBall() { PrepareOnPaddle(); } public void SetEffectDefinition(BallEffectDefinition effectDefinition) { // Disable all currently attached effects first. BallEffectBase[] allEffects = GetComponents(); for (int i = 0; i < allEffects.Length; i++) allEffects[i].enabled = false; ApplyOverlayMaterial(effectDefinition != null ? effectDefinition.OverlayMaterial : null); if (effectDefinition == null) { Debug.Log($"[{gameObject.name}] SetEffectDefinition: Received NULL definition (no effect assigned)"); RebuildEffectsCache(); return; } Debug.Log($"[{gameObject.name}] SetEffectDefinition: Assigning effect type: {effectDefinition.EffectType}"); MonoBehaviour selected = null; switch (effectDefinition.EffectType) { case BallEffectType.TripleSplit: Debug.Log($"[{gameObject.name}] Matched TripleSplit case"); selected = GetOrAddEffect(); break; case BallEffectType.Explosive: Debug.Log($"[{gameObject.name}] Matched Explosive case"); selected = GetOrAddEffect(); break; case BallEffectType.Piercing: Debug.Log($"[{gameObject.name}] Matched Piercing case"); selected = GetOrAddEffect(); break; case BallEffectType.Homing: Debug.Log($"[{gameObject.name}] Matched Homing case"); selected = GetOrAddEffect(); break; case BallEffectType.Teleport: Debug.Log($"[{gameObject.name}] Matched Teleport case"); selected = GetOrAddEffect(); break; case BallEffectType.None: default: Debug.LogError($"[{gameObject.name}] ERROR: EffectType is None or unrecognized: {effectDefinition.EffectType}"); break; } if (selected is BallEffectBase effectBase) { Debug.Log($"[{gameObject.name}] Successfully enabled effect: {selected.GetType().Name}"); effectBase.SetDefinition(effectDefinition); selected.enabled = true; } else { Debug.LogError($"[{gameObject.name}] ERROR: selected component is not a BallEffectBase! Type: {selected?.GetType().Name ?? "NULL"}"); } RebuildEffectsCache(); } public void ApplyOverlayMaterial(Material overlayMaterial) { if (_renderer == null) return; if (overlayMaterial == null) { _renderer.sharedMaterials = _baseMaterials; return; } Material[] stacked = new Material[_baseMaterials.Length + 1]; for (int i = 0; i < _baseMaterials.Length; i++) stacked[i] = _baseMaterials[i]; stacked[stacked.Length - 1] = overlayMaterial; _renderer.sharedMaterials = stacked; } public float GetDamageAgainstBlock(blockScript block, Collision2D collision) { float damage = baseDamage * _damageMultiplier; for (int i = 0; i < _effects.Length; i++) { BallEffectBase effect = _effects[i]; if (effect != null && effect.isActiveAndEnabled) damage = effect.ModifyBlockDamage(this, block, collision, damage); } return Mathf.Max(0f, damage); } public void NotifyHitBlock(blockScript block, Collision2D collision) { for (int i = 0; i < _effects.Length; i++) { BallEffectBase effect = _effects[i]; if (effect != null && effect.isActiveAndEnabled) effect.OnHitBlock(this, block, collision); } } public void MultiplyDamage(float multiplier) { _damageMultiplier *= multiplier; } public void MultiplyScale(float scaleMultiplier) { transform.localScale *= scaleMultiplier; } public float GetCurrentSpeed() { float speed = _rb.linearVelocity.magnitude; return speed > 0.001f ? speed : launchSpeed; } public Vector2 GetPrePhysicsVelocity() { if (_prePhysicsVelocity.sqrMagnitude > 0.0001f) return _prePhysicsVelocity; return _rb.linearVelocity; } public void LaunchInDirection(Vector2 direction, float speed) { _launched = true; _followPaddleWhenUnlaunched = false; _rb.simulated = true; _collider.enabled = true; Vector2 dir = direction.sqrMagnitude > 0.0001f ? direction.normalized : Vector2.up; _rb.linearVelocity = dir * speed; } public BallScript SpawnClone(Vector2 direction, float speedMultiplier, float scaleMultiplier, float damageMultiplier) { GameObject cloneObject = Instantiate(gameObject, transform.position, Quaternion.identity); BallScript cloneBall = cloneObject.GetComponent(); if (cloneBall == null) return null; // Clones are temporary gameplay balls and do not consume queue slots. cloneBall.SetQueueManaged(false); cloneBall.SetPaddleReference(paddle, spawnOffset); cloneBall.LaunchInDirection(direction, launchSpeed * speedMultiplier); cloneBall.MultiplyScale(scaleMultiplier); cloneBall.MultiplyDamage(damageMultiplier); return cloneBall; } private void Launch() { _launched = true; float angle = Random.Range(-maxLaunchAngle, maxLaunchAngle) * Mathf.Deg2Rad; _rb.linearVelocity = new Vector2(Mathf.Sin(angle), Mathf.Cos(angle)) * launchSpeed; for (int i = 0; i < _effects.Length; i++) { BallEffectBase effect = _effects[i]; if (effect != null && effect.isActiveAndEnabled) effect.OnBallLaunched(this); } } private T GetOrAddEffect() where T : MonoBehaviour { T existing = GetComponent(); if (existing != null) return existing; return gameObject.AddComponent(); } private void RebuildEffectsCache() { _effects = GetComponents(); } }