From c91a8a40114d699aa5162c7e9199dacb52318a6a Mon Sep 17 00:00:00 2001 From: Miguel Astor Date: Fri, 28 Aug 2026 18:23:35 -0400 Subject: [PATCH] TO SQUASH: Nuke ball explosion. --- .../Animations/Balls/BallExplosionDecal.anim | 36 ++- Assets/Prefabs/Ball Explosion.prefab | 13 + Assets/Prefabs/Nuclear Ball.prefab | 3 + Assets/Scenes/Game.unity | 100 +++++++ Assets/Scripts/Game/Balls/BallBase.cs | 5 + Assets/Scripts/Game/Balls/NukeBall.cs | 64 ++++- Assets/Scripts/Game/WorldFiller.cs | 252 +++++++++++++++++- 7 files changed, 456 insertions(+), 17 deletions(-) diff --git a/Assets/Animations/Balls/BallExplosionDecal.anim b/Assets/Animations/Balls/BallExplosionDecal.anim index 618c530..0411431 100644 --- a/Assets/Animations/Balls/BallExplosionDecal.anim +++ b/Assets/Animations/Balls/BallExplosionDecal.anim @@ -7,7 +7,7 @@ AnimationClip: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_Name: BallExplosionDecal - serializedVersion: 7 + serializedVersion: 8 m_Legacy: 0 m_Compressed: 0 m_UseHighQualityCurve: 1 @@ -22,23 +22,23 @@ AnimationClip: curve: - time: 0 value: {fileID: 7210558746331385846, guid: 7982a3f8e588f159dbb4a39dda4520a4, type: 3} - - time: 0.1 + - time: 0.16666667 value: {fileID: -5432075094247615553, guid: 7982a3f8e588f159dbb4a39dda4520a4, type: 3} - - time: 0.21666667 + - time: 0.35 value: {fileID: -6233756757657699445, guid: 7982a3f8e588f159dbb4a39dda4520a4, type: 3} - - time: 0.31666666 + - time: 0.5 value: {fileID: -9134259580595608040, guid: 7982a3f8e588f159dbb4a39dda4520a4, type: 3} - - time: 0.43333334 - value: {fileID: -6261058792273593043, guid: 7982a3f8e588f159dbb4a39dda4520a4, type: 3} - - time: 0.56666666 - value: {fileID: 7983266495651452045, guid: 7982a3f8e588f159dbb4a39dda4520a4, type: 3} - time: 0.68333334 - value: {fileID: 7271509617224618838, guid: 7982a3f8e588f159dbb4a39dda4520a4, type: 3} - - time: 0.78333336 - value: {fileID: -9122391734515957254, guid: 7982a3f8e588f159dbb4a39dda4520a4, type: 3} + value: {fileID: -6261058792273593043, guid: 7982a3f8e588f159dbb4a39dda4520a4, type: 3} - time: 0.9 + value: {fileID: 7983266495651452045, guid: 7982a3f8e588f159dbb4a39dda4520a4, type: 3} + - time: 1.0833334 + value: {fileID: 7271509617224618838, guid: 7982a3f8e588f159dbb4a39dda4520a4, type: 3} + - time: 1.25 + value: {fileID: -9122391734515957254, guid: 7982a3f8e588f159dbb4a39dda4520a4, type: 3} + - time: 1.4166666 value: {fileID: -6539983120607256907, guid: 7982a3f8e588f159dbb4a39dda4520a4, type: 3} - - time: 1 + - time: 1.5833334 value: {fileID: 1192567241846995084, guid: 7982a3f8e588f159dbb4a39dda4520a4, type: 3} attribute: m_Sprite path: @@ -61,6 +61,7 @@ AnimationClip: isPPtrCurve: 1 isIntCurve: 0 isSerializeReferenceCurve: 0 + metaData: 0 pptrCurveMapping: - {fileID: 7210558746331385846, guid: 7982a3f8e588f159dbb4a39dda4520a4, type: 3} - {fileID: -5432075094247615553, guid: 7982a3f8e588f159dbb4a39dda4520a4, type: 3} @@ -77,7 +78,7 @@ AnimationClip: m_AdditiveReferencePoseClip: {fileID: 0} m_AdditiveReferencePoseTime: 0 m_StartTime: 0 - m_StopTime: 1.0166667 + m_StopTime: 1.6 m_OrientationOffsetY: 0 m_Level: 0 m_CycleOffset: 0 @@ -96,4 +97,11 @@ AnimationClip: m_EulerEditorCurves: [] m_HasGenericRootTransform: 0 m_HasMotionFloatCurves: 0 - m_Events: [] + m_Events: + - time: 1.5833334 + functionName: DestroySpark + data: + objectReferenceParameter: {fileID: 0} + floatParameter: 0 + intParameter: 0 + messageOptions: 0 diff --git a/Assets/Prefabs/Ball Explosion.prefab b/Assets/Prefabs/Ball Explosion.prefab index f164ab2..8c5a3ab 100644 --- a/Assets/Prefabs/Ball Explosion.prefab +++ b/Assets/Prefabs/Ball Explosion.prefab @@ -11,6 +11,7 @@ GameObject: - component: {fileID: 1846621201000746085} - component: {fileID: 7402419950365427160} - component: {fileID: 4492134503030492876} + - component: {fileID: 7068514341266521413} m_Layer: 0 m_Name: Ball Explosion m_TagString: Untagged @@ -115,3 +116,15 @@ Animator: m_AllowConstantClipSamplingOptimization: 1 m_KeepAnimatorStateOnDisable: 0 m_WriteDefaultValuesOnDisable: 0 +--- !u!114 &7068514341266521413 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6718664185715355894} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: bc6262e498fa2d2eba7d1e3d1b58f1cb, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::SharkSpark diff --git a/Assets/Prefabs/Nuclear Ball.prefab b/Assets/Prefabs/Nuclear Ball.prefab index 040cc3c..55c6348 100644 --- a/Assets/Prefabs/Nuclear Ball.prefab +++ b/Assets/Prefabs/Nuclear Ball.prefab @@ -306,3 +306,6 @@ MonoBehaviour: hitSound: {fileID: 8300000, guid: a36177a84709d5c51ae647a599008d1d, type: 3} criticalSound: {fileID: 8300000, guid: 756fb91b052f83560a8f10a21d610eb1, type: 3} explosionSound: {fileID: 8300000, guid: 4b277a57224afa7df868a42f9005709e, type: 3} + destroySound: {fileID: 8300000, guid: f4dab77a6338f5d0ca6038bad0c87d87, type: 3} + explosionEffectPrefab: {fileID: 6718664185715355894, guid: 0fa4013691ac1bf6eae3a2ac6c7c35ff, type: 3} + worldFiller: {fileID: 0} diff --git a/Assets/Scenes/Game.unity b/Assets/Scenes/Game.unity index 837d34a..219a359 100644 --- a/Assets/Scenes/Game.unity +++ b/Assets/Scenes/Game.unity @@ -6287,6 +6287,7 @@ GameObject: m_Component: - component: {fileID: 2071167547} - component: {fileID: 2071167548} + - component: {fileID: 2071167549} m_Layer: 0 m_Name: WorldFiller m_TagString: WorldFiller @@ -6324,9 +6325,108 @@ MonoBehaviour: player: {fileID: 1002905792} gameBoardSpriteRenderer: {fileID: 297740151} wallPrefab: {fileID: 2746972694419659216, guid: 3fbc857440f3c649b9d49d40955db450, type: 3} + ballSpawner: {fileID: 240489636} + areaDestroyed: {fileID: 8300000, guid: f4dab77a6338f5d0ca6038bad0c87d87, type: 3} fallbackBarThicknessWorldUnits: 0.08 geometryEpsilon: 0.0001 wallPaddingPixelsPerSide: 2 +--- !u!82 &2071167549 +AudioSource: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2071167546} + m_Enabled: 1 + serializedVersion: 4 + OutputAudioMixerGroup: {fileID: 0} + m_audioClip: {fileID: 0} + m_Resource: {fileID: 0} + m_PlayOnAwake: 1 + m_Volume: 1 + m_Pitch: 1 + Loop: 0 + Mute: 0 + Spatialize: 0 + SpatializePostEffects: 0 + Priority: 128 + DopplerLevel: 1 + MinDistance: 1 + MaxDistance: 500 + Pan2D: 0 + rolloffMode: 0 + BypassEffects: 0 + BypassListenerEffects: 0 + BypassReverbZones: 0 + rolloffCustomCurve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 1 + inSlope: 0 + outSlope: 0 + tangentMode: 0 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 1 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 0 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + panLevelCustomCurve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 0 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + spreadCustomCurve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 0 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + reverbZoneMixCustomCurve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 1 + inSlope: 0 + outSlope: 0 + tangentMode: 0 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 --- !u!1 &2071745858 GameObject: m_ObjectHideFlags: 0 diff --git a/Assets/Scripts/Game/Balls/BallBase.cs b/Assets/Scripts/Game/Balls/BallBase.cs index 1c41833..6d0741a 100644 --- a/Assets/Scripts/Game/Balls/BallBase.cs +++ b/Assets/Scripts/Game/Balls/BallBase.cs @@ -199,4 +199,9 @@ public class BallBase : MonoBehaviour bounds = default; return false; } + + public void StopBall() + { + speed = new Vector2(0.0f, 0.0f); + } } diff --git a/Assets/Scripts/Game/Balls/NukeBall.cs b/Assets/Scripts/Game/Balls/NukeBall.cs index a99ede0..408ca9c 100644 --- a/Assets/Scripts/Game/Balls/NukeBall.cs +++ b/Assets/Scripts/Game/Balls/NukeBall.cs @@ -1,4 +1,6 @@ using UnityEngine; +using System.Collections; +using System.Collections.Generic; public class NukeBall : BallBase { @@ -6,6 +8,8 @@ public class NukeBall : BallBase public AudioClip explosionSound; + public GameObject explosionEffectPrefab; + public WorldFiller worldFiller; // Tracks escalating danger/critical phases for audio cues. @@ -30,6 +34,8 @@ public class NukeBall : BallBase 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() { @@ -55,6 +61,11 @@ public class NukeBall : BallBase // Update is called once per frame void FixedUpdate() { + if (exploding) + { + return; + } + AnimatorStateInfo stateInfo = animator.GetCurrentAnimatorStateInfo(0); if (stateInfo.IsName("NuclearBallBounce")) { @@ -100,14 +111,20 @@ public class NukeBall : BallBase { // Set critical state. var area = worldFiller != null ? worldFiller.GetAreaPercentOfFreeRegion(transform.position) : -1.0f; - criticalState = (area < 0.2f) ? 2 : (area < 0.4f) ? 1 : 0; + + 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) + if (criticalState == 1 || criticalState == 2) { criticalTimer += Time.deltaTime; @@ -119,6 +136,28 @@ public class NukeBall : BallBase 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; @@ -198,4 +237,25 @@ public class NukeBall : BallBase } } } + + 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); + } } diff --git a/Assets/Scripts/Game/WorldFiller.cs b/Assets/Scripts/Game/WorldFiller.cs index eb80fd5..b51af5b 100644 --- a/Assets/Scripts/Game/WorldFiller.cs +++ b/Assets/Scripts/Game/WorldFiller.cs @@ -9,6 +9,12 @@ public class WorldFiller : MonoBehaviour public GameObject wallPrefab; + public BallSpawner ballSpawner; + + public AudioClip areaDestroyed; + + private AudioSource audioSource; + [SerializeField] private float fallbackBarThicknessWorldUnits = 0.08f; @@ -33,6 +39,7 @@ public class WorldFiller : MonoBehaviour // Start is called once before the first execution of Update after the MonoBehaviour is created void Start() { + audioSource = GetComponent(); gameBoardBounds = gameBoardSpriteRenderer.bounds; InitializeFreeAreas(); ClearWalls(); @@ -299,7 +306,7 @@ public class WorldFiller : MonoBehaviour return false; } -public float GetAreaPercentOfFreeRegion(Vector2 position) + public float GetAreaPercentOfFreeRegion(Vector2 position) { if (freeAreas.Count == 0 && gameBoardSpriteRenderer != null) { @@ -317,6 +324,75 @@ public float GetAreaPercentOfFreeRegion(Vector2 position) return -1.0f; } + public Rect? GetFreeRegionOfPosition(Vector2 position) + { + if (freeAreas.Count == 0 && gameBoardSpriteRenderer != null) + { + InitializeFreeAreas(); + } + + for (int i = 0; i < freeAreas.Count; i++) + { + if (RectContainsWithTolerance(freeAreas[i], position)) + { + return freeAreas[i]; + } + } + + return null; + } + + public bool FillFreeRegion(Rect freeRegion) + { + if ((freeAreas.Count == 0 || boardArea <= geometryEpsilon) && gameBoardSpriteRenderer != null) + { + InitializeFreeAreas(); + } + + if (!TryGetMatchingFreeArea(freeRegion, out Rect regionToFill)) + { + Debug.LogWarning("Requested region is not currently a free area."); + return false; + } + + if (!TrySpawnWallInRect(regionToFill)) + { + return false; + } + + audioSource.PlayOneShot(areaDestroyed); + + return true; + } + + public Queue GetBallsInSameFreeRegion(Vector2 position) + { + Queue ballsInSameRegion = new Queue(); + + if (ballSpawner == null) + { + return ballsInSameRegion; + } + + var targetRegion = GetFreeRegionOfPosition(position); + + if (targetRegion == null) + { + return ballsInSameRegion; + } + + foreach (var ball in ballSpawner.GetBalls()) + { + var ballRegion = GetFreeRegionOfPosition(ball.transform.position); + if (ballRegion != null && ballRegion.Value == targetRegion.Value) + { + ballsInSameRegion.Enqueue(ball); + } + } + + return ballsInSameRegion; + } + public Vector2 GetRandomPositionInFreeAreas(Bounds? clearingArea = null) { if (freeAreas.Count == 0 && gameBoardSpriteRenderer != null) @@ -531,6 +607,180 @@ public float GetAreaPercentOfFreeRegion(Vector2 position) } } + private bool TryGetMatchingFreeArea(Rect requestedRegion, out Rect matchedRegion) + { + for (int i = 0; i < freeAreas.Count; i++) + { + if (RectsApproximatelyEqual(freeAreas[i], requestedRegion)) + { + matchedRegion = freeAreas[i]; + return true; + } + } + + matchedRegion = default; + return false; + } + + private bool RectsApproximatelyEqual(Rect a, Rect b) + { + return Mathf.Abs(a.xMin - b.xMin) <= geometryEpsilon + && Mathf.Abs(a.yMin - b.yMin) <= geometryEpsilon + && Mathf.Abs(a.xMax - b.xMax) <= geometryEpsilon + && Mathf.Abs(a.yMax - b.yMax) <= geometryEpsilon; + } + + private bool TrySpawnWallInRect(Rect targetRect) + { + if (wallPrefab == null) + { + Debug.LogError("Wall prefab is not assigned."); + return false; + } + + if (gameBoardSpriteRenderer == null) + { + Debug.LogError("Game board SpriteRenderer is not assigned."); + return false; + } + + gameBoardBounds = gameBoardSpriteRenderer.bounds; + + Rect boardRect = Rect.MinMaxRect( + gameBoardBounds.min.x, + gameBoardBounds.min.y, + gameBoardBounds.max.x, + gameBoardBounds.max.y + ); + + if (!TryGetRectIntersection(targetRect, boardRect, out Rect clampedRect)) + { + Debug.LogWarning("Target wall rectangle is outside the game board bounds."); + return false; + } + + GameObject wall = Instantiate(wallPrefab, Vector3.zero, Quaternion.identity); + var wallRenderer = wall.GetComponentInChildren(); + + if (wallRenderer == null) + { + Debug.LogError("Wall prefab is missing a SpriteRenderer in children."); + Destroy(wall); + return false; + } + + float wallPaddingWorldUnits = PixelsToWorldUnits(wallPaddingPixelsPerSide, wallRenderer); + Rect paddedWallRect = ExpandRectInsideBounds(clampedRect, wallPaddingWorldUnits, boardRect); + + ApplyWallTransform(wall.transform, wallRenderer, paddedWallRect); + spawnedWalls.Enqueue(wall); + + UpdateFreeAreas(paddedWallRect); + ResolveWallIntersections(wallRenderer.bounds); + DestroyBarSegments(); + + if (boardArea <= geometryEpsilon) + { + boardArea = boardRect.width * boardRect.height; + } + + int areaPercentage = boardArea > geometryEpsilon + ? Mathf.RoundToInt((paddedWallRect.width * paddedWallRect.height / boardArea) * 100.0f) + : 0; + + if (player != null) + { + player.OnWallSpawned(areaPercentage); + } + + return true; + } + + private void ResolveWallIntersections(Bounds wallBounds) + { + // Yummies and multipliers are awarded if intersected by the spawned wall. + var yummies = GameObject.FindGameObjectsWithTag("Yummy"); + var multipliers = GameObject.FindGameObjectsWithTag("Multiplier"); + var shark = GameObject.FindGameObjectWithTag("Shark"); + + foreach (var yummy in yummies) + { + if (yummy == null) + { + continue; + } + + var yummyRenderer = yummy.GetComponent(); + if (yummyRenderer == null || !wallBounds.Intersects(yummyRenderer.bounds)) + { + continue; + } + + YummyBase yummyBase = yummy.GetComponent(); + + if (yummyBase != null) + { + yummyBase.AwardToPlayer(player); + } + else + { + Debug.LogError("YummyBase component not found on yummy object."); + } + } + + foreach (var multiplier in multipliers) + { + if (multiplier == null) + { + continue; + } + + var multiplierRenderer = multiplier.GetComponent(); + if (multiplierRenderer == null || !wallBounds.Intersects(multiplierRenderer.bounds)) + { + continue; + } + + Multiplier multiplierComponent = multiplier.GetComponent(); + + if (multiplierComponent != null) + { + multiplierComponent.AwardToPlayer(player); + } + else + { + Debug.LogError("Multiplier component not found on multiplier object."); + } + } + + if (shark == null) + { + return; + } + + var sharkRenderer = shark.GetComponent(); + if (sharkRenderer == null || !wallBounds.Intersects(sharkRenderer.bounds)) + { + return; + } + + Shark sharkComponent = shark.GetComponent(); + if (sharkComponent != null) + { + sharkComponent.KillShark(); + } + } + + private void DestroyBarSegments() + { + GameObject[] barSegments = GameObject.FindGameObjectsWithTag("Bar"); + + foreach (GameObject barSegment in barSegments) + { + Destroy(barSegment); + } + } + private void ApplyWallTransform(Transform wallTransform, SpriteRenderer wallRenderer, Rect targetRect) { Vector2 baseSize = wallRenderer.bounds.size;