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; [Header("Deflection")] public bool enableDeflection = true; public string deflectTag = "Box"; [Tooltip("How far the beam travels after deflection.")] public float deflectDistance = 10f; [Tooltip("Push along the deflected direction to avoid re-hitting the same face.")] public float deflectEdgePush = 0.02f; // runtime cache so the left/right choice doesn't flicker Collider _currentDeflectCol; int _currentDeflectSign = 1; // +1 = right, -1 = left bool _isDeflecting; Vector3 _bouncePoint; Vector3 _bounceEnd; [Header("Deflection (Box side center)")] [Tooltip("Tiny push so the second segment isn't inside the face.")] // runtime bool _segmented; [Header("Deflection (Box side center)")] public bool deflectFromBoxSides = true; public string boxTag = "Box"; public float outDistance = 12f; public float sideExitPush = 0.02f; // push outward from side face public float entryPush = 0.005f; // push outward from front face so it shows // runtime bool _routeViaSide; Vector3 _entryPoint; // front-face hit Vector3 _midPoint; // side-face center Collider _lastBox; int _sideSign = 1; // -1 = left, +1 = right (box local right axis) 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 CheckHit() { if (hasTriggeredDeathThisBurst) return; bool CheckSegment(Vector3 a, Vector3 b) { 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; foreach (var h in hits) { if (h.collider && h.collider.GetComponentInParent() == this) continue; if (h.distance < best) { best = h.distance; bestT = h.collider.transform; } } if (!bestT) return false; bool isPlayer = bestT.CompareTag(playerTag) || (bestT.root && bestT.root.CompareTag(playerTag)); if (isPlayer) { hasTriggeredDeathThisBurst = true; CrateEscapeGameManager.Instance?.OnPlayerHitByLaser(); return true; } return false; } if (_routeViaSide) { if (CheckSegment(laserStart, _entryPoint)) return; if (CheckSegment(_entryPoint, _midPoint)) return; CheckSegment(_midPoint, laserEnd); } else { CheckSegment(laserStart, laserEnd); } } void UpdateLaserPath() { laserStart = transform.position; Vector3 dir = transform.forward; float radius = Mathf.Max(0.0001f, hitRadius); // first: straight cast to find the first collider 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; } } } // default: straight line _routeViaSide = false; laserEnd = straightEnd; line.positionCount = 2; line.SetPosition(0, laserStart); line.SetPosition(1, laserEnd); // route via side center only for tagged Box with a BoxCollider if (!deflectFromBoxSides || !gotHit || !bestHit.collider.CompareTag(boxTag)) return; var box = bestHit.collider.GetComponent(); if (!box) return; // choose/stabilize left/right per box if (_lastBox != bestHit.collider) { _lastBox = bestHit.collider; _sideSign = (Random.value < 0.5f) ? -1 : 1; } Transform t = box.transform; // 1) ENTRY POINT on the FRONT face (push a hair outward so it's visible) _entryPoint = bestHit.point + bestHit.normal * entryPush; // 2) SIDE-CENTER (left/right in box local space), and outward direction 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); // 3) SECOND LEG: from side-center outward // remaining distance after traveling to entry + to side-center float traveled = bestHit.distance + Vector3.Distance(bestHit.point, _midPoint); float remain = Mathf.Max(0f, maxDistance - traveled); float leg = Mathf.Min(outDistance, remain); Vector3 secondStart = _midPoint + outDir * sideExitPush; Vector3 secondEnd = secondStart + outDir * leg; if (Physics.SphereCast(secondStart, radius, outDir, out var h2, leg, collisionMask, queryTriggerMode)) { if (!(h2.collider && h2.collider.GetComponentInParent() == this)) secondEnd = h2.point; } // draw the full polyline: START -> ENTRY -> SIDE-CENTER -> OUT _routeViaSide = true; laserEnd = secondEnd; line.positionCount = 4; line.SetPosition(0, laserStart); line.SetPosition(1, _entryPoint); line.SetPosition(2, _midPoint); line.SetPosition(3, laserEnd); } //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); } }