411 lines
12 KiB
C#
411 lines
12 KiB
C#
using UnityEngine;
|
||
using System.Collections;
|
||
|
||
[RequireComponent(typeof(LineRenderer))]
|
||
public class LaserBeam : MonoBehaviour
|
||
{
|
||
[Header("Control")]
|
||
[Tooltip("If true, this beam will NOT run its own timers or intro; it only responds to the controller.")]
|
||
public bool externalControl = true;
|
||
|
||
[Header("Laser Settings")]
|
||
public float maxDistance = 20f;
|
||
public LayerMask collisionMask = ~0;
|
||
public float chargeDuration = 3f; // used only if externalControl == false
|
||
public float fireDuration = 1f; // used only if externalControl == false
|
||
|
||
[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 (ignored if externalControl)")]
|
||
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;
|
||
public float entryPush = 0.005f;
|
||
|
||
[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;
|
||
|
||
void Awake()
|
||
{
|
||
SetupLaserRenderer();
|
||
// Always start safe
|
||
currentPhase = LaserPhase.Idle;
|
||
hasTriggeredDeathThisBurst = false;
|
||
DisableLaser();
|
||
StopAllCoroutines();
|
||
}
|
||
|
||
void Start()
|
||
{
|
||
// Only self-run if not externally controlled
|
||
if (!externalControl && showIntro)
|
||
StartCoroutine(IntroShow());
|
||
}
|
||
|
||
void Update()
|
||
{
|
||
if (externalControl)
|
||
{
|
||
// Passive: only draw debug line if visible
|
||
if (debugDraw) Debug.DrawLine(laserStart, laserEnd, Color.cyan);
|
||
return;
|
||
}
|
||
|
||
// Legacy self-FSM (optional use)
|
||
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);
|
||
}
|
||
|
||
// LaserBeam.cs <20> replace SetLaserPhase with this version
|
||
public void SetLaserPhase(LaserPhase phase)
|
||
{
|
||
// If we<77>re externally controlled, cancel any internal coroutines immediately
|
||
if (externalControl)
|
||
{
|
||
StopAllCoroutines();
|
||
introRunning = false;
|
||
}
|
||
|
||
if (!line) SetupLaserRenderer(); // make sure 'line' exists even if GO was inactive
|
||
|
||
if (phase != currentPhase) 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);
|
||
hasTriggeredDeathThisBurst = false;
|
||
CheckHit();
|
||
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;
|
||
// Also clear endpoint for safety/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()
|
||
{
|
||
// HARD GATES: never kill unless we<77>re allowed to
|
||
if (!isActiveAndEnabled) return;
|
||
if (!gameObject.activeInHierarchy) return;
|
||
|
||
bool lethalNow = (currentPhase == LaserPhase.Firing) || (introRunning && introIsDeadly);
|
||
if (!lethalNow) return;
|
||
|
||
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;
|
||
Collider bestCol = null;
|
||
foreach (var h in hits)
|
||
{
|
||
if (h.collider && h.collider.GetComponentInParent<LaserBeam>() == this) continue;
|
||
if (h.distance < best) { best = h.distance; bestT = h.collider.transform; bestCol = h.collider; }
|
||
}
|
||
|
||
if (!bestT) return false;
|
||
|
||
if (bestT.CompareTag(playerTag) || (bestT.root && bestT.root.CompareTag(playerTag)))
|
||
{
|
||
hasTriggeredDeathThisBurst = true;
|
||
CrateEscapeGameManager.Instance?.OnPlayerHitByLaser();
|
||
return true;
|
||
}
|
||
|
||
if (bestCol && bestCol.CompareTag(boxTag))
|
||
{
|
||
var boxHealth = bestCol.GetComponent<LaserBoxHealth>();
|
||
if (boxHealth != null)
|
||
boxHealth.TakeLaserDamage();
|
||
}
|
||
|
||
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);
|
||
|
||
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<LaserBeam>() == 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);
|
||
|
||
if (!deflectFromBoxSides || !gotHit || !bestHit.collider.CompareTag(boxTag)) return;
|
||
|
||
var box = bestHit.collider.GetComponent<BoxCollider>();
|
||
if (!box) return;
|
||
|
||
if (_lastBox != bestHit.collider)
|
||
{
|
||
_lastBox = bestHit.collider;
|
||
_sideSign = (Random.value < 0.5f) ? -1 : 1;
|
||
}
|
||
|
||
Transform t = box.transform;
|
||
_entryPoint = bestHit.point + bestHit.normal * entryPush;
|
||
|
||
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);
|
||
|
||
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<LaserBeam>() == 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<LineRenderer>();
|
||
if (!line) line = gameObject.AddComponent<LineRenderer>();
|
||
|
||
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;
|
||
}
|
||
}
|