// Maeko Explodable: keeps a block structure perfectly still until it blows,
// blasts every block outward (optional slow motion), shrinks each block away
// once it lands, then optionally rebuilds the structure bottom to top for a seamless loop.
// Free addon by Maeko Studio, maeko.studio. Requires Unity 6 or newer.
// Put this file in Assets (NOT in an Editor folder).

using System.Collections;
using UnityEngine;

public class MaekoExplodable : MonoBehaviour
{
    [Header("Blast")]
    [Tooltip("Where the explosion starts. Low and inside the structure looks best.")]
    public Transform blastPoint;
    [Tooltip("Seconds after Play before it blows. Set to -1 to only trigger manually (right-click the component > Boom).")]
    public float delay = 1.5f;
    public float force = 700f;
    public float radius = 8f;
    [Tooltip("Lifts pieces up and out instead of just sideways.")]
    public float upward = 1.2f;
    [Range(0f, 0.5f)] public float randomness = 0.25f;

    [Header("Slow motion")]
    [Range(0.05f, 1f)] public float slowMo = 1f; // 1 = off
    [Tooltip("Real-world seconds the slow motion lasts.")]
    public float slowMoSeconds = 2.5f;

    [Header("Disappear after landing")]
    public bool disappear = true;
    [Tooltip("A block counts as landed once it moves slower than this.")]
    public float restSpeed = 0.3f;
    [Tooltip("No block vanishes before this many seconds after the blast.")]
    public float minAirTime = 0.8f;
    [Tooltip("Failsafe: every block vanishes by this time, even if still rolling.")]
    public float maxLifetime = 6f;
    public float shrinkTime = 0.35f;
    [Tooltip("Random wait before each landed block shrinks, so they don't all vanish at once.")]
    public Vector2 shrinkDelay = new Vector2(0f, 0.6f);

    [Header("Rebuild (seamless loops)")]
    [Tooltip("Once every block is gone, pop the structure back up from bottom to top.")]
    public bool rebuild = true;
    public float rebuildDelay = 0.4f;
    [Tooltip("Seconds for the rebuild wave to travel from the base to the tip.")]
    public float rebuildTime = 1.4f;
    public float popTime = 0.25f;
    [Tooltip("Blow it up again after rebuilding, forever.")]
    public bool loop = false;
    public float loopPause = 1f;

    /// <summary>Fires the moment the structure blows. Hook your own extras (sounds, camera shake) to this.</summary>
    public event System.Action Boomed;
    /// <summary>Fires once the structure has fully rebuilt.</summary>
    public event System.Action Rebuilt;

    Rigidbody[] bodies;
    Vector3[] startPos, startScale;
    Quaternion[] startRot;
    float[] shrinkAt;
    bool[] gone;
    bool blown, rebuilding;
    float blastTime, minY, maxY;

    void Awake()
    {
        bodies = GetComponentsInChildren<Rigidbody>();
        int n = bodies.Length;
        startPos = new Vector3[n]; startScale = new Vector3[n]; startRot = new Quaternion[n];
        shrinkAt = new float[n]; gone = new bool[n];
        minY = float.MaxValue; maxY = float.MinValue;

        for (int i = 0; i < n; i++)
        {
            var t = bodies[i].transform;
            startPos[i] = t.localPosition; startRot[i] = t.localRotation; startScale[i] = t.localScale;
            minY = Mathf.Min(minY, startPos[i].y); maxY = Mathf.Max(maxY, startPos[i].y);
            bodies[i].isKinematic = true; // stands perfectly until the blast
        }
        ResetState();
    }

    void Start()
    {
        if (delay >= 0f) Invoke(nameof(Boom), delay);
    }

    [ContextMenu("Boom")]
    public void Boom()
    {
        if (blown || rebuilding || bodies == null) return;
        blown = true;
        blastTime = Time.time;

        Vector3 p = blastPoint ? blastPoint.position : transform.position;
        foreach (var b in bodies)
        {
            b.isKinematic = false;
            float f = force * Random.Range(1f - randomness, 1f + randomness);
            b.AddExplosionForce(f, p, radius, upward);
            b.AddTorque(Random.insideUnitSphere * f * 0.02f);
        }

        if (slowMo < 1f) StartCoroutine(SlowMotion());
        Boomed?.Invoke();
    }

    void Update()
    {
        if (!blown || rebuilding || !disappear) return;

        float since = Time.time - blastTime;
        int goneCount = 0;

        for (int i = 0; i < bodies.Length; i++)
        {
            if (gone[i]) { goneCount++; continue; }
            var b = bodies[i];

            if (shrinkAt[i] < 0f)
            {
                bool landed = since > minAirTime && b.linearVelocity.magnitude < restSpeed;
                if (landed || since > maxLifetime)
                {
                    shrinkAt[i] = Time.time + Random.Range(shrinkDelay.x, shrinkDelay.y);
                    b.isKinematic = true; // freeze where it landed
                }
            }
            else if (Time.time >= shrinkAt[i])
            {
                float k = (Time.time - shrinkAt[i]) / shrinkTime;
                if (k >= 1f) { b.gameObject.SetActive(false); gone[i] = true; goneCount++; }
                else b.transform.localScale = startScale[i] * (1f - k * k); // ease in, then pop out
            }
        }

        if (goneCount == bodies.Length && rebuild) StartCoroutine(Rebuild());
    }

    IEnumerator Rebuild()
    {
        rebuilding = true;
        yield return new WaitForSeconds(rebuildDelay);

        for (int i = 0; i < bodies.Length; i++)
        {
            var t = bodies[i].transform;
            bodies[i].isKinematic = true;
            t.localPosition = startPos[i]; t.localRotation = startRot[i]; t.localScale = Vector3.zero;
            bodies[i].gameObject.SetActive(true);
        }

        float span = Mathf.Max(0.001f, maxY - minY);
        float timer = 0f;
        while (timer < rebuildTime + popTime)
        {
            timer += Time.deltaTime;
            for (int i = 0; i < bodies.Length; i++)
            {
                float begin = (startPos[i].y - minY) / span * rebuildTime; // base first, tip last
                float k = Mathf.Clamp01((timer - begin) / popTime);
                bodies[i].transform.localScale = startScale[i] * EaseOutBack(k);
            }
            yield return null;
        }

        for (int i = 0; i < bodies.Length; i++) bodies[i].transform.localScale = startScale[i];
        ResetState();
        blown = false;
        rebuilding = false;
        Rebuilt?.Invoke();

        if (loop) Invoke(nameof(Boom), loopPause);
    }

    void ResetState()
    {
        for (int i = 0; i < bodies.Length; i++) { shrinkAt[i] = -1f; gone[i] = false; }
    }

    IEnumerator SlowMotion()
    {
        float fixedDt = Time.fixedDeltaTime;
        Time.timeScale = slowMo;
        Time.fixedDeltaTime = fixedDt * slowMo; // keeps physics smooth while slowed
        yield return new WaitForSecondsRealtime(slowMoSeconds);
        Time.timeScale = 1f;
        Time.fixedDeltaTime = fixedDt;
    }

    static float EaseOutBack(float k)
    {
        const float c1 = 1.70158f, c3 = c1 + 1f;
        float x = k - 1f;
        return 1f + c3 * x * x * x + c1 * x * x;
    }
}
