using UnityEngine; [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); // Orange public float laserWidth = 0.05f; public float emissionStrength = 5f; public float scrollSpeed = 1f; [Header("Player Hit")] public string playerTag = "Player"; [Tooltip("Approximate beam thickness for hit tests.")] public float hitRadius = 0.1f; [Tooltip("Should raycasts consider trigger colliders? (Player often uses triggers)")] public QueryTriggerInteraction queryTriggerMode = QueryTriggerInteraction.Collide; [Header("Intro Show")] public bool showIntro = true; public float initialShowDuration = 3f; [Tooltip("If true, the intro beam can kill the player too.")] public bool introIsDeadly = true; [Header("Debug")] public bool debugDraw = false; private LineRenderer line; private float timer = 0f; private enum LaserState { Idle, Charging, Firing } private LaserState currentState = LaserState.Idle; private Vector3 laserStart; private Vector3 laserEnd; private bool introRunning = false; private bool hasTriggeredDeathThisBurst = false; // cache root to ignore self-hits Transform _selfRoot; void Awake() { _selfRoot = transform.root; SetupLaserRenderer(); } void Start() { if (showIntro) StartCoroutine(IntroShow()); } void Update() { if (introRunning) return; timer += Time.deltaTime; switch (currentState) { case LaserState.Idle: hasTriggeredDeathThisBurst = false; // reset per cycle if (timer >= chargeDuration) { timer = 0f; currentState = LaserState.Charging; StartCharging(); } break; case LaserState.Charging: if (timer >= 1f) { timer = 0f; currentState = LaserState.Firing; FireLaser(); } else { BlinkWarning(); } break; case LaserState.Firing: if (timer >= fireDuration) { timer = 0f; currentState = LaserState.Idle; DisableLaser(); } else { UpdateLaserPath(); CheckHit(); } break; } if (debugDraw) { Debug.DrawLine(laserStart, laserEnd, Color.cyan, 0f, false); } } // ---------------- Intro show ---------------- System.Collections.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; currentState = LaserState.Idle; introRunning = false; } // ---------------- States ---------------- void StartCharging() { UpdateLaserPath(); line.enabled = true; SetLineColor(warningColor); } void BlinkWarning() { float blink = Mathf.PingPong(Time.time * 5f, 1f); Color blinkColor = Color.Lerp(Color.clear, warningColor, blink); SetLineColor(blinkColor); } void FireLaser() { UpdateLaserPath(); SetLineColor(laserColor); hasTriggeredDeathThisBurst = false; CheckHit(); // initial frame } void DisableLaser() { line.enabled = false; } // ---------------- Hit Detection (robust) ---------------- void CheckHit() { if (hasTriggeredDeathThisBurst) return; Vector3 dir = (laserEnd - laserStart).normalized; float distToEnd = Vector3.Distance(laserStart, laserEnd); // Cast along visible beam, then pick the nearest valid hit RaycastHit[] hits = Physics.SphereCastAll( laserStart, hitRadius, dir, distToEnd, collisionMask, queryTriggerMode ); if (hits == null || hits.Length == 0) return; float bestDist = float.MaxValue; Transform best = null; foreach (var h in hits) { // Ignore self (any collider in our own hierarchy) if (h.collider && h.collider.GetComponentInParent() == this) continue; // Keep the nearest hit under our visible segment if (h.distance < bestDist) { bestDist = h.distance; best = h.collider.transform; } } if (best == null) return; // Player detection: check both object and its root bool isPlayer = best.CompareTag(playerTag) || (best.root != null && best.root.CompareTag(playerTag)); if (isPlayer) { Debug.Log("Laser hit player: " + best.name); hasTriggeredDeathThisBurst = true; CrateEscapeGameManager.Instance?.OnPlayerHitByLaser(); } } void UpdateLaserPath() { laserStart = transform.position; Vector3 dir = transform.forward; // SphereCast to find the FIRST valid hit along the visible beam, // ignoring ONLY this laser's own colliders. float radius = Mathf.Max(0.0001f, hitRadius); RaycastHit[] hits = Physics.SphereCastAll( laserStart, radius, dir, maxDistance, collisionMask, queryTriggerMode ); Vector3 end = laserStart + dir * maxDistance; float bestDist = float.MaxValue; if (hits != null && hits.Length > 0) { foreach (var h in hits) { if (!h.collider) continue; // Ignore ONLY this LaserBeam's colliders (not the whole root/level) if (h.collider.GetComponentInParent() == this) continue; if (h.distance < bestDist) { bestDist = h.distance; end = h.point; } } } laserEnd = end; line.SetPosition(0, laserStart); line.SetPosition(1, laserEnd); } // ---------------- Utils ---------------- void SetLineColor(Color c) { line.material.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 { Debug.LogWarning("Custom/EmissiveLaser shader not found. Using fallback."); line.material = new Material(Shader.Find("Sprites/Default")); line.material.color = laserColor; } line.enabled = false; } void OnDrawGizmosSelected() { if (!debugDraw) return; Gizmos.color = Color.magenta; Gizmos.DrawWireSphere(transform.position, hitRadius); } }