using UnityEngine; using System.Collections; using System.Collections.Generic; public class NukeBall : BallBase { public AudioClip criticalSound; public AudioClip explosionSound; public GameObject explosionEffectPrefab; public WorldFiller worldFiller; // Tracks escalating danger/critical phases for audio cues. private int criticalState = 0; private float criticalTime = 0.5f; private float criticalTimer = 0.0f; // Initial resting height the ball bounces back toward after a floor hit. private float height; private Collider2D selfCollider; private bool isBouncingToHeight = false; private float bounceStartY = 0.0f; private float fallTargetY = 0.0f; private float bounceTimer = 0.0f; private float bounceDuration = 1.00f; private bool exploding = false; // Start is called once before the first execution of Update after the MonoBehaviour is created void Start() { // NukeBall is not affected by magnets, unlike other balls. affectedByMagnet = false; // Cache frequently used components once. audioSource = GetComponent(); animator = GetComponent(); selfCollider = GetComponent(); // Start with a small random horizontal drift. speed = new Vector2(Random.Range(-0.09f, 0.09f), 0.0f); height = GetComponent().position.y; fallTargetY = height; // Find the closest valid floor point under the current position. UpdateFallTargetFromCurrentPosition(); audioSource.PlayOneShot(spawnSound); } // Update is called once per frame void FixedUpdate() { if (exploding) { return; } AnimatorStateInfo stateInfo = animator.GetCurrentAnimatorStateInfo(0); if (stateInfo.IsName("NuclearBallBounce")) { transform.Translate(speed); Vector3 currentPosition = transform.position; if (isBouncingToHeight) { bounceTimer += Time.fixedDeltaTime; float t = Mathf.Clamp01(bounceTimer / bounceDuration); // Ease-out ascent: fast start, gentle finish near target height. float easedT = 1.0f - Mathf.Pow(1.0f - t, 2.0f); currentPosition.y = Mathf.Lerp(bounceStartY, height, easedT); if (t >= 1.0f) { // Reached top of bounce arc; resume falling phase. isBouncingToHeight = false; bounceTimer = 0.0f; currentPosition.y = height; UpdateFallTargetFromCurrentPosition(); } } else { bounceTimer += Time.fixedDeltaTime; float t = Mathf.Clamp01(bounceTimer / bounceDuration); // Ease-in descent: gentle start, faster approach to floor. float easedT = t * t; currentPosition.y = Mathf.Lerp(height, fallTargetY, easedT); } transform.position = currentPosition; } } void Update() { // Set critical state. var area = worldFiller != null ? worldFiller.GetAreaPercentOfFreeRegion(transform.position) : -1.0f; if (area == -1.0f || exploding) { return; } criticalState = (area < 0.12f) ? 3 : (area < 0.2f) ? 2 : (area < 0.4f) ? 1 : 0; // State 1 beeps every 0.5s, state 2 beeps every 0.25s. criticalTime = (criticalState == 2) ? 0.5f : 1.0f; // If in a critical state, increment the critical timer and play the // critical sound if the timer exceeds the critical time. if (criticalState == 1 || criticalState == 2) { criticalTimer += Time.deltaTime; if (criticalTimer >= criticalTime) { audioSource.PlayOneShot(criticalSound); // Keep fractional overflow so cadence stays stable across frame rates. criticalTimer -= criticalTime; } } else if (criticalState == 3) { audioSource.PlayOneShot(explosionSound); var balls = worldFiller != null ? worldFiller.GetBallsInSameFreeRegion(transform.position) : new Queue(); foreach (var ball in balls) { ball.GetComponent()?.StopBall(); if (explosionEffectPrefab != null) { Instantiate(explosionEffectPrefab, ball.transform.position, Quaternion.identity); } } exploding = true; StartCoroutine(DestroyAndFillRegion(balls)); } else { criticalTimer = 0.0f; } } void OnCollisionEnter2D(Collision2D collision) { // NukeBall should bounce off walls only. It cannot collide with other balls, not even other NukeBalls. if (collision.gameObject.CompareTag("Wall")) { ContactPoint2D contact = collision.GetContact(0); // Reflect horizontal movement from wall surfaces. speed = Vector2.Reflect(speed, contact.normal); speed.y = 0.0f; audioSource.PlayOneShot(bounceSound); // A floor hit (normal pointing upward) starts an ease-out bounce back to initial height. if (contact.normal.y > 0.5f) { isBouncingToHeight = true; bounceTimer = 0.0f; // Use exact contact point as bounce origin so visual contact feels grounded. bounceStartY = contact.point.y; fallTargetY = bounceStartY; Vector3 currentPosition = transform.position; currentPosition.y = bounceStartY; transform.position = currentPosition; } else if (contact.normal.y <= -0.5f) { isBouncingToHeight = false; bounceTimer = 0.0f; height = contact.point.y - 0.01f; } } else { Physics2D.IgnoreCollision(collision.collider, GetComponent(), true); } } private void UpdateFallTargetFromCurrentPosition() { // Default to current y in case no valid floor is found. fallTargetY = transform.position.y; // Probe straight down and choose nearest upward-facing wall surface. RaycastHit2D[] hits = Physics2D.RaycastAll(transform.position, Vector2.down, 100.0f); float closestDistance = float.MaxValue; foreach (RaycastHit2D hit in hits) { if (hit.collider == null || hit.collider == selfCollider) { continue; } if (!hit.collider.CompareTag("Wall")) { continue; } if (hit.normal.y <= 0.5f) { continue; } if (hit.distance < closestDistance) { closestDistance = hit.distance; fallTargetY = hit.point.y; } } } private IEnumerator DestroyAndFillRegion(Queue balls) { yield return new WaitForSeconds(1.4f); foreach (var ball in balls) { Destroy(ball); } var freeRegion = worldFiller != null ? worldFiller.GetFreeRegionOfPosition(transform.position) : null; if (freeRegion != null) { worldFiller.FillFreeRegion(freeRegion.Value); } Destroy(gameObject); } }