using UnityEngine; using System.Collections; [RequireComponent(typeof(LineRenderer))] public class LaserBeam : MonoBehaviour { [Header("Laser Settings")] public float maxDistance = 20f; public LayerMask collisionMask = ~0; public float chargeDuration = 3f; public float fireDuration = 1f; [Header("Laser Appearance")] public Color laserColor = Color.red; public Color warningColor = new Color(1f, 0.5f, 0f); public float laserWidth = 0.05f; public float emissionStrength = 5f; public float scrollSpeed = 1f; [Header("Player Hit")] public string playerTag = "Player"; public float hitRadius = 0.1f; public QueryTriggerInteraction queryTriggerMode = QueryTriggerInteraction.Collide; [Header("Intro Show")] public bool showIntro = true; public float initialShowDuration = 3f; public bool introIsDeadly = true; [Header("Deflection (Box side center)")] public bool deflectFromBoxSides = true; public string boxTag = "Box"; public float outDistance = 12f; public float sideExitPush = 0.02f; // will be lifted to >= hitRadius + epsilon at runtime public float entryPush = 0.005f; // will be lifted to >= 0.25*hitRadius at runtime [Header("Debug")] public bool debugDraw = false; private LineRenderer line; private float timer = 0f; public enum LaserPhase { Idle, Charging, Firing } private LaserPhase currentPhase = LaserPhase.Idle; private Vector3 laserStart; private Vector3 laserEnd; private bool introRunning = false; private bool hasTriggeredDeathThisBurst = false; // Deflection private bool _routeViaSide; private Vector3 _entryPoint; private Vector3 _midPoint; private Collider _lastBox; private int _sideSign = 1; // External control hook (set by controller if used) public bool externalControl = true; void Awake() { SetupLaserRenderer(); } void Start() { if (showIntro) StartCoroutine(IntroShow()); } void Update() { if (introRunning) return; timer += Time.deltaTime; switch (currentPhase) { case LaserPhase.Idle: hasTriggeredDeathThisBurst = false; if (timer >= chargeDuration) { timer = 0f; SetLaserPhase(LaserPhase.Charging); } break; case LaserPhase.Charging: if (timer >= 1f) { timer = 0f; SetLaserPhase(LaserPhase.Firing); } else { TickLaserDuringCharging(); } break; case LaserPhase.Firing: if (timer >= fireDuration) { timer = 0f; SetLaserPhase(LaserPhase.Idle); } else { TickLaserDuringFiring(); } break; } if (debugDraw) Debug.DrawLine(laserStart, laserEnd, Color.cyan); } public void SetLaserPhase(LaserPhase phase) { // When controller is driving, cancel any self-cycling if (externalControl) { StopAllCoroutines(); introRunning = false; } if (!line) SetupLaserRenderer(); // Clear 1-kill gate whenever we enter lead-in or lethal phases if (phase == LaserPhase.Charging || phase == LaserPhase.Firing) hasTriggeredDeathThisBurst = false; currentPhase = phase; switch (phase) { case LaserPhase.Idle: DisableLaser(); break; case LaserPhase.Charging: UpdateLaserPath(); line.enabled = true; SetLineColor(warningColor); break; case LaserPhase.Firing: UpdateLaserPath(); line.enabled = true; SetLineColor(laserColor); CheckHit(); // immediate check on enter break; } } public void TickLaserDuringFiring() { if (currentPhase != LaserPhase.Firing) return; UpdateLaserPath(); CheckHit(); } public void TickLaserDuringCharging() { if (currentPhase != LaserPhase.Charging) return; BlinkWarning(); UpdateLaserPath(); } IEnumerator IntroShow() { introRunning = true; float t = 0f; line.enabled = true; SetLineColor(laserColor); hasTriggeredDeathThisBurst = false; while (t < initialShowDuration) { UpdateLaserPath(); if (introIsDeadly && !hasTriggeredDeathThisBurst) CheckHit(); t += Time.deltaTime; yield return null; } line.enabled = false; timer = 0f; SetLaserPhase(LaserPhase.Idle); introRunning = false; } void DisableLaser() { line.enabled = false; // Optional: zero endpoints for clarity in debug laserStart = transform.position; laserEnd = laserStart; } void BlinkWarning() { float blink = Mathf.PingPong(Time.time * 5f, 1f); Color blinkColor = Color.Lerp(Color.clear, warningColor, blink); SetLineColor(blinkColor); } void CheckHit() { // only lethal during Firing, or during Intro if flagged as deadly bool lethalNow = (currentPhase == LaserPhase.Firing) || (introRunning && introIsDeadly); if (!lethalNow || hasTriggeredDeathThisBurst) return; // local helper that can ignore a specific collider (e.g., the deflection box) bool CheckSegment(Vector3 a, Vector3 b, Collider ignoreCol = null) { Vector3 d = b - a; float len = d.magnitude; if (len <= 0.0001f) return false; d /= len; var hits = Physics.SphereCastAll(a, hitRadius, d, len, collisionMask, queryTriggerMode); if (hits == null || hits.Length == 0) return false; float best = float.MaxValue; Transform bestT = null; Collider bestCol = null; foreach (var h in hits) { if (!h.collider) continue; if (ignoreCol && h.collider == ignoreCol) continue; // NEW: ignore the deflection box if (h.collider.GetComponentInParent() == this) continue; if (h.distance < best) { best = h.distance; bestT = h.collider.transform; bestCol = h.collider; } } if (!bestT) return false; // Player? if (bestT.CompareTag(playerTag) || (bestT.root && bestT.root.CompareTag(playerTag))) { hasTriggeredDeathThisBurst = true; CrateEscapeGameManager.Instance?.OnPlayerHitByLaser(); return true; } // Box? (still allow box damage feedback) if (bestCol && bestCol.CompareTag(boxTag)) { var boxHealth = bestCol.GetComponent(); if (boxHealth != null) boxHealth.TakeLaserDamage(); } return false; } // segmented checks if we’re deflecting if (_routeViaSide) { if (CheckSegment(laserStart, _entryPoint)) return; if (CheckSegment(_entryPoint, _midPoint, _lastBox)) return; // ignore the box while exiting CheckSegment(_midPoint, laserEnd, _lastBox); // ignore it on the last leg too } else { CheckSegment(laserStart, laserEnd); } } void UpdateLaserPath() { laserStart = transform.position; Vector3 dir = transform.forward; float radius = Mathf.Max(0.0001f, hitRadius); RaycastHit[] hits = Physics.SphereCastAll(laserStart, radius, dir, maxDistance, collisionMask, queryTriggerMode); Vector3 straightEnd = laserStart + dir * maxDistance; RaycastHit bestHit = default; float bestDist = float.MaxValue; bool gotHit = false; if (hits != null) { foreach (var h in hits) { if (!h.collider) continue; if (h.collider.GetComponentInParent() == this) continue; if (h.distance < bestDist) { bestDist = h.distance; bestHit = h; gotHit = true; straightEnd = h.point; } } } _routeViaSide = false; laserEnd = straightEnd; line.positionCount = 2; line.SetPosition(0, laserStart); line.SetPosition(1, laserEnd); // No deflect? if (!deflectFromBoxSides || !gotHit || !bestHit.collider.CompareTag(boxTag)) return; var box = bestHit.collider.GetComponent(); if (!box) return; if (_lastBox != bestHit.collider) { _lastBox = bestHit.collider; _sideSign = (Random.value < 0.5f) ? -1 : 1; } Transform t = box.transform; // ensure we don’t start the entry segment inside the box float safeEntry = Mathf.Max(entryPush, hitRadius * 0.25f); _entryPoint = bestHit.point + bestHit.normal * safeEntry; // compute the midpoint on the box side we’re exiting from Vector3 centerW = t.TransformPoint(box.center); Vector3 half = Vector3.Scale(box.size * 0.5f, t.lossyScale); Vector3 rightW = t.right.normalized; Vector3 outDir = rightW * _sideSign; _midPoint = centerW + rightW * (_sideSign * half.x); // how much distance remains for leg 2 float traveled = bestHit.distance + Vector3.Distance(bestHit.point, _midPoint); float remain = Mathf.Max(0f, maxDistance - traveled); float leg = Mathf.Min(outDistance, remain); // Choose a second start that is definitely OUTSIDE the box surface float minExit = Mathf.Max(sideExitPush, radius + 0.02f); Vector3 secondStart = _midPoint + outDir * minExit; // If somehow still overlapping the box bounds, nudge out a bit more (guarded) for (int i = 0; i < 3; i++) { // AABBs are conservative; this is cheap and “good enough” if (box.bounds.Contains(secondStart)) secondStart += outDir * (radius + 0.02f); else break; } Vector3 secondEnd = secondStart + outDir * leg; // Trim second leg if it hits something early if (Physics.SphereCast(secondStart, radius, outDir, out var h2, leg, collisionMask, queryTriggerMode)) { if (!(h2.collider && h2.collider.GetComponentInParent() == this)) secondEnd = h2.point; } _routeViaSide = true; laserEnd = secondEnd; line.positionCount = 4; line.SetPosition(0, laserStart); line.SetPosition(1, _entryPoint); line.SetPosition(2, _midPoint); line.SetPosition(3, laserEnd); } public void ResetCycle() { StopAllCoroutines(); introRunning = false; timer = 0f; currentPhase = LaserPhase.Idle; hasTriggeredDeathThisBurst = false; DisableLaser(); } void SetLineColor(Color c) { if (line.material.HasProperty("_Color")) line.material.SetColor("_Color", c); line.startColor = c; line.endColor = c; } public void SetupLaserRenderer() { line = GetComponent(); if (!line) line = gameObject.AddComponent(); line.positionCount = 2; line.useWorldSpace = true; line.loop = false; line.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off; line.receiveShadows = false; line.widthMultiplier = laserWidth; Shader laserShader = Shader.Find("Custom/EmissiveLaser"); if (laserShader != null) { Material laserMat = new Material(laserShader); laserMat.SetColor("_Color", laserColor); laserMat.SetFloat("_Emission", emissionStrength); laserMat.SetFloat("_ScrollSpeed", scrollSpeed); line.material = laserMat; } else { line.material = new Material(Shader.Find("Sprites/Default")); line.material.color = laserColor; } line.enabled = false; } }