using DG.Tweening; using UnityEngine; using System.Collections; using System.Threading; // for Interlocked [RequireComponent(typeof(Renderer))] public class CubeClash_CubeLid : MonoBehaviour { [Header("Launch")] public float launchForce = 20f; [Header("Launch Direction (camera-relative)")] [Tooltip("x = camera right, y = camera forward")] public Vector2 camSpaceDir = new Vector2(0f, 1f); [Tooltip("Scale applied to the forward part relative to launchForce (final = launchForce * forwardScale)")] public float forwardScale = 10f; [Tooltip("Flatten the camera so direction is on XZ plane")] public bool flattenCamera = true; [Header("Cycle Timing")] [Tooltip("Full cycle: (rest) + (pre-blink) + (active-glow). Repeats.")] public float activeInterval = 8f; [Tooltip("Blink time BEFORE activation.")] public float preBlinkSeconds = 1.5f; [Tooltip("Time the cube stays steadily glowing AFTER activation.")] public float activeGlowSeconds = 2.0f; [Header("Blink / Glow")] [Tooltip("One full blink (up+down) duration.")] public float blinkPulseDuration = 0.25f; [Tooltip("Fade times for entering/exiting steady glow.")] public float glowFadeIn = 0.12f, glowFadeOut = 0.12f; [Header("Emission (last material slot)")] [Tooltip("Enable Emission on the last material of this renderer in the Inspector.")] public Color emissionBaseColor = Color.white; public float offEmission = 0f; public float blinkEmission = 2.0f; public float activeGlowIntensity = 2.0f; public bool usePropertyBlock = true; [Header("Detection Volume")] [Tooltip("If true, OverlapBox uses this BoxCollider; else uses Local Box below.")] public bool useColliderForVolume = true; public BoxCollider volumeSource; // auto-found if null public LayerMask detectMask = ~0; public QueryTriggerInteraction triggerQuery = QueryTriggerInteraction.Ignore; [Tooltip("Fallback volume if no BoxCollider is assigned.")] public Vector3 localBoxCenter = Vector3.zero; public Vector3 localBoxSize = new Vector3(1, 1, 1); [Header("Gizmos")] public bool drawGizmos = true; public Color gizmoFill = new Color(0f, 1f, 1f, 0.15f); public Color gizmoWire = new Color(0f, 1f, 1f, 1f); // ─────────────────────────── Global Coordination ─────────────────────────── [Header("Global Coordination")] [Tooltip("If true, lids coordinate so only one activates at a time.")] public bool coordinateGlobally = true; [Tooltip("Random extra wait added to each lid’s rest period to desync starts.")] public Vector2 restJitterRange = new Vector2(0.0f, 1.0f); [Tooltip("Random jitter while waiting for the global slot, to randomize who acquires it.")] public Vector2 acquireJitterRange = new Vector2(0.0f, 0.2f); [Tooltip("Minimum spacing between two different lids activating (seconds).")] public float minGlobalSeparation = 0.25f; private static int s_gateToken = 0; // 0 = free, 1 = held private static float s_nextAllowedTime = 0f; // spacing across lids private bool _holdingGate = false; // ─────────────────────────────────────────────────────────────────────────── // --- internals --- private Renderer _rend; private int _submeshIndex = -1; private Material _instancedMat; // used if not MPB private MaterialPropertyBlock _mpb; // used if MPB private float _emissionIntensity; private Tween _glowTween; // current tween private Coroutine _loopCo; private static readonly int EmissionColorID = Shader.PropertyToID("_EmissionColor"); private static readonly int EmissiveColorID = Shader.PropertyToID("_EmissiveColor"); void Awake() { _rend = GetComponentInChildren(); if (!volumeSource) volumeSource = GetComponent(); if (!volumeSource) volumeSource = GetComponentInChildren(); SetupGlowTarget(); ApplyEmission(offEmission); } void OnEnable() { StopTweens(); if (_loopCo != null) StopCoroutine(_loopCo); _loopCo = StartCoroutine(LidLoop()); } void OnDisable() { StopTweens(); if (_loopCo != null) StopCoroutine(_loopCo); _loopCo = null; // Release the gate if this lid was active and got disabled mid-cycle if (_holdingGate) { _holdingGate = false; Interlocked.Exchange(ref s_gateToken, 0); s_nextAllowedTime = Time.time + minGlobalSeparation; } } // ====== Main cycle ====== private IEnumerator LidLoop() { while (true) { // Rest until pre-blink window, with per-lid random jitter so they don't check the gate at the same moment float rest = Mathf.Max(0f, activeInterval - preBlinkSeconds - activeGlowSeconds); float jitter = (restJitterRange.y > 0f || restJitterRange.x > 0f) ? Random.Range(restJitterRange.x, restJitterRange.y) : 0f; if (rest + jitter > 0f) yield return new WaitForSeconds(rest + Mathf.Max(0f, jitter)); // Acquire global slot (only one lid runs blink+activate at a time) if (coordinateGlobally) yield return StartCoroutine(AcquireGlobalGate()); // Pre-blink (warning) yield return BlinkForSeconds(preBlinkSeconds); // Activate: launch & steady glow ActivateLid(); yield return SteadyGlow(activeGlowIntensity, activeGlowSeconds); // Deactivate: fade to OFF yield return FadeEmissionTo(offEmission, glowFadeOut); // Release global slot if (coordinateGlobally) { _holdingGate = false; Interlocked.Exchange(ref s_gateToken, 0); s_nextAllowedTime = Time.time + minGlobalSeparation; } } } private IEnumerator AcquireGlobalGate() { // Wait for spacing time first while (Time.time < s_nextAllowedTime) yield return null; // Try to acquire with small random jitter between attempts to randomize which lid wins while (true) { if (Interlocked.CompareExchange(ref s_gateToken, 1, 0) == 0) { _holdingGate = true; yield break; } float jitter = (acquireJitterRange.y > 0f || acquireJitterRange.x > 0f) ? Random.Range(acquireJitterRange.x, acquireJitterRange.y) : 0.05f; yield return new WaitForSeconds(Mathf.Max(0.01f, jitter)); } } // ====== Gameplay ====== [Header("Spring Animation")] [Tooltip("How high the cube jumps visually when launching a player.")] public float springHeight = 0.5f; [Tooltip("How long the up and down tween takes (seconds).")] public float springDuration = 0.2f; [ContextMenu("Activate Lid")] private void ActivateLid() { GetOverlapBoxWorld(out Vector3 center, out Vector3 halfExtents, out Quaternion orientation); Collider[] cols = Physics.OverlapBox(center, halfExtents, orientation, detectMask, triggerQuery); Vector3 worldForward = GetCameraRelativeDirection(camSpaceDir, flattenCamera); foreach (Collider col in cols) { if (!col) continue; if (col.CompareTag("Zibu") && col.attachedRigidbody != null) { Vector3 impulse = (Vector3.up * launchForce) + (worldForward * (launchForce * forwardScale)); col.attachedRigidbody.AddForce(impulse, ForceMode.Impulse); } } // --- NEW spring animation --- DoSpringAnimation(); } private void DoSpringAnimation() { // kill any existing spring DOTween.Kill("CubeSpring_" + GetInstanceID()); Vector3 startPos = transform.position; Vector3 upPos = startPos + Vector3.up * springHeight; Sequence seq = DOTween.Sequence(); seq.Append(transform.DOMoveY(upPos.y, springDuration).SetEase(Ease.OutQuad)); seq.Append(transform.DOMoveY(startPos.y, springDuration).SetEase(Ease.InQuad)); seq.SetId("CubeSpring_" + GetInstanceID()); } private static Vector3 GetCameraRelativeDirection(Vector2 camDir, bool flatten) { Transform cam = Camera.main ? Camera.main.transform : null; Vector3 camFwd = cam ? cam.forward : Vector3.forward; Vector3 camRight = cam ? cam.right : Vector3.right; if (flatten) { camFwd.y = 0f; camRight.y = 0f; if (camFwd.sqrMagnitude < 1e-6f) camFwd = Vector3.forward; if (camRight.sqrMagnitude < 1e-6f) camRight = Vector3.right; camFwd.Normalize(); camRight.Normalize(); } Vector3 dir = camRight * camDir.x + camFwd * camDir.y; if (dir.sqrMagnitude > 1f) dir.Normalize(); return dir.sqrMagnitude < 1e-6f ? (flatten ? Vector3.forward : (cam ? cam.forward : Vector3.forward)) : dir; } // ====== Blink / Glow visuals ====== private IEnumerator BlinkForSeconds(float seconds) { if (_rend == null || seconds <= 0f || blinkPulseDuration <= 0.02f) yield break; StopTweens(); ApplyEmission(offEmission); int blinks = Mathf.Max(1, Mathf.RoundToInt(seconds / blinkPulseDuration)); float half = blinkPulseDuration * 0.5f; _glowTween = DOTween .To(() => _emissionIntensity, v => { _emissionIntensity = v; ApplyEmission(v); }, blinkEmission, half) .SetEase(Ease.InOutSine) .SetLoops(blinks * 2, LoopType.Yoyo) .SetTarget(this); yield return _glowTween.WaitForCompletion(); _glowTween = null; } private IEnumerator SteadyGlow(float targetIntensity, float holdSeconds) { yield return FadeEmissionTo(targetIntensity, glowFadeIn); if (holdSeconds > 0f) yield return new WaitForSeconds(holdSeconds); } private IEnumerator FadeEmissionTo(float target, float duration) { StopTweens(); if (duration <= 0f) { ApplyEmission(target); yield break; } _glowTween = DOTween .To(() => _emissionIntensity, v => { _emissionIntensity = v; ApplyEmission(v); }, target, duration) .SetEase(target > _emissionIntensity ? Ease.OutSine : Ease.InSine) .SetTarget(this); yield return _glowTween.WaitForCompletion(); _glowTween = null; } private void StopTweens() { if (_glowTween != null && _glowTween.IsActive()) _glowTween.Kill(); _glowTween = null; } // ====== Emission plumbing ====== private Renderer RendererOrChild() { if (_rend) return _rend; _rend = GetComponentInChildren(); return _rend; } private void SetupGlowTarget() { if (RendererOrChild() == null) return; _submeshIndex = Mathf.Max(0, _rend.sharedMaterials.Length - 1); if (usePropertyBlock) { if (_mpb == null) _mpb = new MaterialPropertyBlock(); _rend.GetPropertyBlock(_mpb, _submeshIndex); // NOTE: Enable Emission on the material in the Inspector. } else { var mats = _rend.materials; if (mats != null && mats.Length > 0) { _instancedMat = mats[_submeshIndex]; if (_instancedMat != null) _instancedMat.EnableKeyword("_EMISSION"); } } } private void ApplyEmission(float intensity) { _emissionIntensity = Mathf.Max(0f, intensity); Color emissive = emissionBaseColor * _emissionIntensity; if (usePropertyBlock) { if (_mpb == null || _rend == null) return; _mpb.SetColor(EmissionColorID, emissive); _mpb.SetColor(EmissiveColorID, emissive); _rend.SetPropertyBlock(_mpb, _submeshIndex); } else { if (_instancedMat == null) return; _instancedMat.SetColor(EmissionColorID, emissive); _instancedMat.SetColor(EmissiveColorID, emissive); } } // ====== Overlap box helpers + gizmo ====== private static Vector3 Abs(Vector3 v) => new Vector3(Mathf.Abs(v.x), Mathf.Abs(v.y), Mathf.Abs(v.z)); private void GetOverlapBoxWorld(out Vector3 center, out Vector3 halfExtents, out Quaternion orientation) { if (useColliderForVolume && volumeSource != null) { center = volumeSource.transform.TransformPoint(volumeSource.center); Vector3 lossy = Abs(volumeSource.transform.lossyScale); halfExtents = Vector3.Scale(volumeSource.size * 0.5f, lossy); orientation = volumeSource.transform.rotation; } else { center = transform.TransformPoint(localBoxCenter); Vector3 lossy = Abs(transform.lossyScale); halfExtents = Vector3.Scale(localBoxSize * 0.5f, lossy); orientation = transform.rotation; } } private void OnDrawGizmosSelected() { if (!drawGizmos) return; GetOverlapBoxWorld(out Vector3 center, out Vector3 halfExtents, out Quaternion rotation); Matrix4x4 m = Matrix4x4.TRS(center, rotation, Vector3.one); Gizmos.matrix = m; Gizmos.color = gizmoFill; Gizmos.DrawCube(Vector3.zero, halfExtents * 2f); Gizmos.color = gizmoWire; Gizmos.DrawWireCube(Vector3.zero, halfExtents * 2f); } private void OnValidate() { activeInterval = Mathf.Max(0.1f, activeInterval); preBlinkSeconds = Mathf.Max(0f, preBlinkSeconds); activeGlowSeconds = Mathf.Max(0f, activeGlowSeconds); blinkPulseDuration = Mathf.Max(0.05f, blinkPulseDuration); glowFadeIn = Mathf.Max(0f, glowFadeIn); glowFadeOut = Mathf.Max(0f, glowFadeOut); offEmission = Mathf.Max(0f, offEmission); blinkEmission = Mathf.Max(offEmission, blinkEmission); activeGlowIntensity = Mathf.Max(offEmission, activeGlowIntensity); if (!volumeSource) volumeSource = GetComponent(); if (!volumeSource) volumeSource = GetComponentInChildren(); } }