Crate Escape Completed

This commit is contained in:
Ali Sharoz 2025-09-09 16:46:17 +05:00
parent fc8f230187
commit 885657b910
11 changed files with 1223 additions and 220 deletions

View File

@ -2,13 +2,18 @@
%TAG !u! tag:unity3d.com,2011: %TAG !u! tag:unity3d.com,2011:
--- !u!21 &2100000 --- !u!21 &2100000
Material: Material:
serializedVersion: 6 serializedVersion: 8
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
m_PrefabParentObject: {fileID: 0} m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInternal: {fileID: 0} m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: em_clenchteeth2 m_Name: em_clenchteeth2
m_Shader: {fileID: 211, guid: 0000000000000000f000000000000000, type: 0} m_Shader: {fileID: 211, guid: 0000000000000000f000000000000000, type: 0}
m_ShaderKeywords: _ALPHABLEND_ON m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords:
- _ALPHABLEND_ON
m_InvalidKeywords: []
m_LightmapFlags: 0 m_LightmapFlags: 0
m_EnableInstancingVariants: 0 m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0 m_DoubleSidedGI: 0
@ -16,7 +21,8 @@ Material:
stringTagMap: stringTagMap:
RenderType: Transparent RenderType: Transparent
disabledShaderPasses: disabledShaderPasses:
- ALWAYS - GRABPASS
m_LockedProperties:
m_SavedProperties: m_SavedProperties:
serializedVersion: 3 serializedVersion: 3
m_TexEnvs: m_TexEnvs:
@ -32,6 +38,7 @@ Material:
m_Texture: {fileID: 2800000, guid: deb28cb372e1ee54fad618f6df43247f, type: 3} m_Texture: {fileID: 2800000, guid: deb28cb372e1ee54fad618f6df43247f, type: 3}
m_Scale: {x: 1, y: 1} m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0} m_Offset: {x: 0, y: 0}
m_Ints: []
m_Floats: m_Floats:
- _BlendOp: 0 - _BlendOp: 0
- _BumpScale: 1 - _BumpScale: 1
@ -63,3 +70,4 @@ Material:
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1} - _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
- _SoftParticleFadeParams: {r: 0, g: 0, b: 0, a: 0} - _SoftParticleFadeParams: {r: 0, g: 0, b: 0, a: 0}
- _TintColor: {r: 0.5, g: 0.5, b: 0.5, a: 0.5} - _TintColor: {r: 0.5, g: 0.5, b: 0.5, a: 0.5}
m_BuildTextureStacks: []

View File

@ -580,6 +580,32 @@ AnimatorStateTransition:
m_InterruptionSource: 0 m_InterruptionSource: 0
m_OrderedInterruption: 1 m_OrderedInterruption: 1
m_CanTransitionToSelf: 1 m_CanTransitionToSelf: 1
--- !u!1102 &-3331804382887163694
AnimatorState:
serializedVersion: 6
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: Celebration
m_Speed: 1
m_CycleOffset: 0
m_Transitions: []
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
m_WriteDefaultValues: 1
m_Mirror: 0
m_SpeedParameterActive: 0
m_MirrorParameterActive: 0
m_CycleOffsetParameterActive: 0
m_TimeParameterActive: 0
m_Motion: {fileID: 3060610079319413300, guid: f4cc7d9b87c59db4b83522e99e842364, type: 3}
m_Tag:
m_SpeedParameter:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:
--- !u!1101 &-2923408276037091438 --- !u!1101 &-2923408276037091438
AnimatorStateTransition: AnimatorStateTransition:
m_ObjectHideFlags: 1 m_ObjectHideFlags: 1
@ -956,6 +982,9 @@ AnimatorStateMachine:
- serializedVersion: 1 - serializedVersion: 1
m_State: {fileID: 7977088065826294681} m_State: {fileID: 7977088065826294681}
m_Position: {x: 1635, y: 2665, z: 0} m_Position: {x: 1635, y: 2665, z: 0}
- serializedVersion: 1
m_State: {fileID: -3331804382887163694}
m_Position: {x: 420, y: 400, z: 0}
m_ChildStateMachines: m_ChildStateMachines:
- serializedVersion: 1 - serializedVersion: 1
m_StateMachine: {fileID: 7036095546467220257} m_StateMachine: {fileID: 7036095546467220257}

File diff suppressed because it is too large Load Diff

View File

@ -1,64 +1,106 @@
using UnityEngine; using UnityEngine;
using UnityEngine.UI;
// Joystick Pack (Asset Store)
// Drag any Joystick (Fixed/Floating/Dynamic/Variable) into moveJoystick
[RequireComponent(typeof(Rigidbody))] [RequireComponent(typeof(Rigidbody))]
public class CrateEscapePlayerControllerJoystick : MonoBehaviour public class CrateEscapePlayerControllerJoystick : MonoBehaviour
{ {
[Header("Joystick")] [Header("Joystick")]
public Joystick moveJoystick; // assign from Joystick Pack public Joystick moveJoystick;
[Header("Movement")] [Header("Movement")]
public float moveSpeed = 4f; // base speed public float moveSpeed = 4f;
public float moveDamp = 12f; // velocity smoothing public float moveDamp = 12f; // smoothing for XZ
public float maxSpeedMultiplier = 3f; // full-stick multiplier public float maxSpeedMultiplier = 3f; // full-stick multiplier
[Range(0f, 0.5f)] public float inputDeadZone = 0.1f; [Range(0f, 0.5f)] public float inputDeadZone = 0.1f;
[Tooltip("If true, stick is interpreted relative to the camera view.")] [Tooltip("If true, stick is interpreted relative to the camera view.")]
public bool cameraRelative = true; public bool cameraRelative = true;
public Transform cameraTransform; // auto-fills with Camera.main if null public Transform cameraTransform;
[Header("Movement Plane")]
[Tooltip("If OFF, uses Static Up as the movement plane normal. If ON, raycasts to read ground normal under the character.")]
public bool useDynamicGroundNormal = false;
[Tooltip("Used when useDynamicGroundNormal = false. Set this to your level's 'up' direction (the plane normal).")]
public Vector3 staticUp = Vector3.up;
[Tooltip("Layer mask for reading ground normal when dynamic is ON.")]
public LayerMask groundForNormalMask = ~0;
[Tooltip("Ray length for sampling ground normal beneath groundCheck.")]
public float groundNormalRayLength = 3f;
[Header("Facing")] [Header("Facing")]
public bool faceMoveDirection = true; // rotate toward move direction public bool faceMoveDirection = true;
public float turnSpeed = 540f; // deg/sec rotation speed public float turnSpeed = 540f; // deg/sec
[Header("Grounding")] [Header("Grounding")]
public LayerMask groundMask = ~0; public LayerMask groundMask = ~0;
public float groundCheckRadius = 0.2f; public float groundCheckRadius = 0.2f;
public Transform groundCheck; public Transform groundCheck;
[Header("Jump")]
public float jumpForce = 7f; // upward impulse
public bool allowAirJump = false;
public Button jumpButton;
[Header("Optional Animator Driver")] [Header("Optional Animator Driver")]
public ZibuAnimDriver anim; public ZibuAnimDriver anim;
[Header("Input Locks")] [Header("Input Locks")]
public float initialLockSeconds = 4f; // joystick disabled for first N seconds public float initialLockSeconds = 0f; // keep 0 while testing
public bool useUnscaledForInitialLock = false; // set true if you pause time at start public bool useUnscaledForInitialLock = false;
public bool autoLockOnGameOver = true; // auto-freeze if GameManager reports isGameOver public bool autoLockOnGameOver = true;
[Header("Debug / Safety")]
public bool debugLogs = true; // show values in console
public bool drawMoveRay = true; // shows a cyan ray for move direction
public bool keyboardFallbackInEditor = true; // WASD/Arrow fallback when joystick is null or zero
public bool ignoreGameOverLock = false; // bypass GameManager lock (testing)
// --- runtime --- // --- runtime ---
Rigidbody _rb; Rigidbody _rb;
Vector3 _vel; // smoothed velocity Vector3 _velXZ; // horizontal velocity (XZ only)
Vector3 _moveDirWorld; // desired movement direction in world space Vector3 _moveDirWorld; // desired move dir on movement plane
float _stickMag; // 0..1 float _stickMag; // 0..1
bool _inputEnabled = false; // starts locked; enabled after initialLockSeconds bool _inputEnabled = false; // true when controls active
bool _jumpQueued;
bool _usedAirJump;
void Awake() void Awake()
{ {
_rb = GetComponent<Rigidbody>(); _rb = GetComponent<Rigidbody>();
// keep physics stable _rb.useGravity = true;
_rb.isKinematic = false;
// Lock only tilt, never lock Position X/Y/Z here
_rb.constraints = RigidbodyConstraints.FreezeRotationX | RigidbodyConstraints.FreezeRotationZ; _rb.constraints = RigidbodyConstraints.FreezeRotationX | RigidbodyConstraints.FreezeRotationZ;
// Good physics defaults
if (_rb.mass <= 0f) _rb.mass = 1f;
_rb.drag = 0f; // dont damp velocity away
_rb.angularDrag = 0.05f;
_rb.collisionDetectionMode = CollisionDetectionMode.Continuous;
if (!groundCheck) groundCheck = transform; if (!groundCheck) groundCheck = transform;
if (!anim) anim = GetComponent<ZibuAnimDriver>(); if (!anim) anim = GetComponent<ZibuAnimDriver>();
if (!cameraTransform && Camera.main) cameraTransform = Camera.main.transform; if (!cameraTransform && Camera.main) cameraTransform = Camera.main.transform;
} }
void OnValidate()
{
if (!cameraTransform && Camera.main) cameraTransform = Camera.main.transform;
if (!groundCheck) groundCheck = transform;
}
void OnEnable() void OnEnable()
{ {
// start locked, then enable after delay
_inputEnabled = false; _inputEnabled = false;
StopAllCoroutines(); StopAllCoroutines();
StartCoroutine(EnableAfterDelay(initialLockSeconds, useUnscaledForInitialLock)); StartCoroutine(EnableAfterDelay(initialLockSeconds, useUnscaledForInitialLock));
if (jumpButton) jumpButton.onClick.AddListener(OnJumpUIButton);
}
void OnDisable()
{
if (jumpButton) jumpButton.onClick.RemoveListener(OnJumpUIButton);
} }
System.Collections.IEnumerator EnableAfterDelay(float seconds, bool unscaled) System.Collections.IEnumerator EnableAfterDelay(float seconds, bool unscaled)
@ -69,6 +111,7 @@ public class CrateEscapePlayerControllerJoystick : MonoBehaviour
else yield return new WaitForSeconds(seconds); else yield return new WaitForSeconds(seconds);
} }
_inputEnabled = true; _inputEnabled = true;
if (debugLogs) Debug.Log($"[CrateEscape] Controls ENABLED (delay={seconds}) on {name}");
} }
void Update() void Update()
@ -76,35 +119,52 @@ public class CrateEscapePlayerControllerJoystick : MonoBehaviour
bool blocked = IsBlocked(); bool blocked = IsBlocked();
if (!blocked) if (!blocked)
ReadJoystick(); {
ReadMoveInput();
}
else else
{ {
// when blocked, zero input so the player stops + anim goes idle
_stickMag = 0f; _stickMag = 0f;
_moveDirWorld = Vector3.zero; _moveDirWorld = Vector3.zero;
} }
// animator hooks
if (anim) if (anim)
{ {
float speedUnits = Mathf.Lerp(0f, maxSpeedMultiplier, _stickMag); float speedUnits = Mathf.Lerp(0f, maxSpeedMultiplier, _stickMag);
anim.SetGrounded(IsGrounded()); anim.SetGrounded(IsGrounded());
anim.SetSpeed(speedUnits); anim.SetSpeed(speedUnits);
} }
// Reset air-jump on ground contact
if (IsGrounded()) _usedAirJump = false;
// Debug ray
if (drawMoveRay && _moveDirWorld.sqrMagnitude > 0.0001f)
Debug.DrawRay(transform.position + Vector3.up * 0.1f, _moveDirWorld.normalized, Color.cyan, 0f, false);
if (debugLogs)
{
if (Time.frameCount % 30 == 0)
{
Debug.Log($"[CrateEscape] {name} inputEnabled={_inputEnabled} blocked={blocked} stickMag={_stickMag:F2} dir={_moveDirWorld}");
}
}
} }
void FixedUpdate() void FixedUpdate()
{ {
bool blocked = IsBlocked(); bool blocked = IsBlocked();
Vector3 targetVel = Vector3.zero; Vector3 targetVelXZ = Vector3.zero;
if (!blocked && _stickMag > inputDeadZone) if (!blocked && _stickMag > inputDeadZone)
{ {
Vector3 dir = _moveDirWorld.normalized; Vector3 dir = _moveDirWorld.normalized;
float targetSpeed = moveSpeed * Mathf.Lerp(0f, maxSpeedMultiplier, _stickMag); float targetSpeed = moveSpeed * Mathf.Lerp(0f, maxSpeedMultiplier, _stickMag);
targetVel = dir * targetSpeed; targetVelXZ = dir * targetSpeed;
if (faceMoveDirection) if (faceMoveDirection && dir.sqrMagnitude > 0.0001f)
{ {
Quaternion targetRot = Quaternion.LookRotation(dir, Vector3.up); Quaternion targetRot = Quaternion.LookRotation(dir, Vector3.up);
_rb.MoveRotation(Quaternion.RotateTowards(_rb.rotation, targetRot, turnSpeed * Time.fixedDeltaTime)); _rb.MoveRotation(Quaternion.RotateTowards(_rb.rotation, targetRot, turnSpeed * Time.fixedDeltaTime));
@ -113,47 +173,131 @@ public class CrateEscapePlayerControllerJoystick : MonoBehaviour
if (blocked) if (blocked)
{ {
// hard-stop while locked // Stop horizontal movement but preserve Y (gravity / jump settle)
_vel = Vector3.zero; _velXZ = Vector3.zero;
_rb.velocity = Vector3.zero; _rb.velocity = new Vector3(0f, _rb.velocity.y, 0f);
return; // don't MovePosition this frame _jumpQueued = false;
return;
} }
// Smooth velocity and move // Smooth horizontal velocity (keep physics-driven Y)
_vel = Vector3.Lerp(_vel, targetVel, 1f - Mathf.Exp(-moveDamp * Time.fixedDeltaTime)); _velXZ = Vector3.Lerp(_velXZ, targetVelXZ, 1f - Mathf.Exp(-moveDamp * Time.fixedDeltaTime));
_rb.MovePosition(_rb.position + _vel * Time.fixedDeltaTime);
// Apply horizontal velocity while preserving current Y
_rb.velocity = new Vector3(_velXZ.x, _rb.velocity.y, _velXZ.z);
// Handle jump in physics step
if (_jumpQueued)
{
_jumpQueued = false;
DoJump();
}
} }
void ReadJoystick() // ---------- Input ----------
void ReadMoveInput()
{ {
if (!moveJoystick) { _stickMag = 0f; _moveDirWorld = Vector3.zero; return; } Vector2 stick = Vector2.zero;
float h = moveJoystick.Horizontal; // -1..1 // Prefer joystick if present and non-zero
float v = moveJoystick.Vertical; // -1..1 if (moveJoystick)
{
stick = new Vector2(moveJoystick.Horizontal, moveJoystick.Vertical);
}
#if UNITY_EDITOR
// Optional keyboard fallback (Editor/testing)
if (keyboardFallbackInEditor)
{
if (stick.sqrMagnitude <= 0.0001f)
{
float h = Input.GetAxisRaw("Horizontal"); // A/D or Left/Right
float v = Input.GetAxisRaw("Vertical"); // W/S or Up/Down
if (Mathf.Abs(h) > 0.01f || Mathf.Abs(v) > 0.01f)
stick = new Vector2(h, v);
}
}
#endif
Vector2 stick = new Vector2(h, v);
_stickMag = Mathf.Clamp01(stick.magnitude); _stickMag = Mathf.Clamp01(stick.magnitude);
if (_stickMag <= inputDeadZone) { _moveDirWorld = Vector3.zero; return; } if (_stickMag <= inputDeadZone)
{
_moveDirWorld = Vector3.zero;
return;
}
Vector2 n = stick.normalized; Vector2 n = stick.normalized;
if (cameraRelative && cameraTransform) if (cameraRelative && cameraTransform)
{ {
// Project camera forward to XZ and build a right vector // Plane-aware camera relative vectors
Vector3 camF = cameraTransform.forward; camF.y = 0f; Vector3 planeUp = GetMovementUp();
camF = camF.sqrMagnitude > 0.0001f ? camF.normalized : Vector3.forward;
Vector3 camR = new Vector3(camF.z, 0f, -camF.x); // Project camera forward onto the plane
Vector3 camF = Vector3.ProjectOnPlane(cameraTransform.forward, planeUp);
if (camF.sqrMagnitude < 0.0001f)
camF = Vector3.ProjectOnPlane(cameraTransform.up, planeUp);
camF.Normalize();
// Right is perpendicular on the plane
Vector3 camR = Vector3.Cross(planeUp, camF).normalized;
// Re-orthonormalize camF to be safe
camF = Vector3.Cross(camR, planeUp).normalized;
_moveDirWorld = camR * n.x + camF * n.y; _moveDirWorld = camR * n.x + camF * n.y;
} }
else else
{ {
// World-relative (X/Z) // World-relative, but still constrained to the plane
_moveDirWorld = new Vector3(n.x, 0f, n.y); Vector3 planeUp = GetMovementUp();
Vector3 worldX = Vector3.ProjectOnPlane(Vector3.right, planeUp).normalized;
Vector3 worldZ = Vector3.ProjectOnPlane(Vector3.forward, planeUp).normalized;
_moveDirWorld = (worldX * n.x + worldZ * n.y);
} }
} }
Vector3 GetMovementUp()
{
if (useDynamicGroundNormal && groundCheck)
{
// Cast along character's local down to find the surface normal
Ray ray = new Ray(groundCheck.position + Vector3.up * 0.05f, -transform.up);
if (Physics.Raycast(ray, out var hit, groundNormalRayLength, groundForNormalMask, QueryTriggerInteraction.Ignore))
{
return hit.normal.normalized;
}
}
// Fallback to static up
return staticUp.sqrMagnitude > 0.0001f ? staticUp.normalized : Vector3.up;
}
void DoJump()
{
bool grounded = IsGrounded();
if (!grounded)
{
if (!(allowAirJump && !_usedAirJump)) return;
_usedAirJump = true;
}
// Reset vertical velocity then apply impulse for consistent jumps
Vector3 v = _rb.velocity;
v.y = 0f;
_rb.velocity = v;
_rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse);
if (anim) anim.PlayAnimation(AnimationState.JumpStart);
if (debugLogs) Debug.Log($"[CrateEscape] {name} JUMP (grounded={grounded}, airUsed={_usedAirJump})");
}
// ---------- Ground / Gizmos ----------
bool IsGrounded() bool IsGrounded()
{ {
return Physics.CheckSphere(groundCheck.position, groundCheckRadius, groundMask, QueryTriggerInteraction.Ignore); return Physics.CheckSphere(groundCheck.position, groundCheckRadius, groundMask, QueryTriggerInteraction.Ignore);
@ -168,7 +312,6 @@ public class CrateEscapePlayerControllerJoystick : MonoBehaviour
// ---------- Public API ---------- // ---------- Public API ----------
/// <summary>Enable/disable all player input & movement immediately.</summary>
public void LockControls(bool locked) public void LockControls(bool locked)
{ {
_inputEnabled = !locked; _inputEnabled = !locked;
@ -176,25 +319,26 @@ public class CrateEscapePlayerControllerJoystick : MonoBehaviour
{ {
_stickMag = 0f; _stickMag = 0f;
_moveDirWorld = Vector3.zero; _moveDirWorld = Vector3.zero;
_vel = Vector3.zero; _velXZ = Vector3.zero;
if (_rb) _rb.velocity = Vector3.zero; if (_rb) _rb.velocity = new Vector3(0f, _rb.velocity.y, 0f);
if (anim) { anim.SetSpeed(0f); } if (anim) { anim.SetSpeed(0f); }
} }
} }
/// <summary>Enable controls now (e.g., if you want to skip the initial delay).</summary>
public void EnableControlsNow() public void EnableControlsNow()
{ {
StopAllCoroutines(); StopAllCoroutines();
_inputEnabled = true; _inputEnabled = true;
if (debugLogs) Debug.Log($"[CrateEscape] Controls ENABLED by call on {name}");
} }
// ---------- Helpers ---------- // ---------- Helpers ----------
bool IsBlocked() bool IsBlocked()
{ {
if (!_inputEnabled) return true; if (!_inputEnabled) return true;
if (autoLockOnGameOver) if (!ignoreGameOverLock && autoLockOnGameOver)
{ {
var gm = CrateEscapeGameManager.Instance; var gm = CrateEscapeGameManager.Instance;
if (gm != null && gm.isGameOver) return true; // freeze after game over if (gm != null && gm.isGameOver) return true; // freeze after game over
@ -203,6 +347,24 @@ public class CrateEscapePlayerControllerJoystick : MonoBehaviour
return false; return false;
} }
// For runtime hookup if you spawn the joystick UI
public void SetJoystick(Joystick j) => moveJoystick = j; public void SetJoystick(Joystick j) => moveJoystick = j;
// ---------- UI Button Hook ----------
/// <summary>Call this from a UI Button OnClick() to make the player jump.</summary>
public void OnJumpUIButton()
{
if (IsBlocked()) return;
if (IsGrounded() || (allowAirJump && !_usedAirJump))
_jumpQueued = true;
}
// ---------- Inspector Utilities ----------
[ContextMenu("Test/Nudge Forward")]
void NudgeForward()
{
// Small forward push to verify movement pipeline
_rb.velocity = new Vector3(2f, _rb.velocity.y, 0f);
if (debugLogs) Debug.Log($"[CrateEscape] {name} NudgeForward called.");
}
} }

View File

@ -4,11 +4,15 @@ using System.Collections;
[RequireComponent(typeof(LineRenderer))] [RequireComponent(typeof(LineRenderer))]
public class LaserBeam : MonoBehaviour 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")] [Header("Laser Settings")]
public float maxDistance = 20f; public float maxDistance = 20f;
public LayerMask collisionMask = ~0; public LayerMask collisionMask = ~0;
public float chargeDuration = 3f; public float chargeDuration = 3f; // used only if externalControl == false
public float fireDuration = 1f; public float fireDuration = 1f; // used only if externalControl == false
[Header("Laser Appearance")] [Header("Laser Appearance")]
public Color laserColor = Color.red; public Color laserColor = Color.red;
@ -22,7 +26,7 @@ public class LaserBeam : MonoBehaviour
public float hitRadius = 0.1f; public float hitRadius = 0.1f;
public QueryTriggerInteraction queryTriggerMode = QueryTriggerInteraction.Collide; public QueryTriggerInteraction queryTriggerMode = QueryTriggerInteraction.Collide;
[Header("Intro Show")] [Header("Intro Show (ignored if externalControl)")]
public bool showIntro = true; public bool showIntro = true;
public float initialShowDuration = 3f; public float initialShowDuration = 3f;
public bool introIsDeadly = true; public bool introIsDeadly = true;
@ -59,16 +63,30 @@ public class LaserBeam : MonoBehaviour
void Awake() void Awake()
{ {
SetupLaserRenderer(); SetupLaserRenderer();
// Always start safe
currentPhase = LaserPhase.Idle;
hasTriggeredDeathThisBurst = false;
DisableLaser();
StopAllCoroutines();
} }
void Start() void Start()
{ {
if (showIntro) // Only self-run if not externally controlled
if (!externalControl && showIntro)
StartCoroutine(IntroShow()); StartCoroutine(IntroShow());
} }
void Update() 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; if (introRunning) return;
timer += Time.deltaTime; timer += Time.deltaTime;
@ -113,8 +131,19 @@ public class LaserBeam : MonoBehaviour
Debug.DrawLine(laserStart, laserEnd, Color.cyan); Debug.DrawLine(laserStart, laserEnd, Color.cyan);
} }
// LaserBeam.cs — replace SetLaserPhase with this version
public void SetLaserPhase(LaserPhase phase) public void SetLaserPhase(LaserPhase phase)
{ {
// If were 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; currentPhase = phase;
switch (phase) switch (phase)
@ -131,6 +160,7 @@ public class LaserBeam : MonoBehaviour
case LaserPhase.Firing: case LaserPhase.Firing:
UpdateLaserPath(); UpdateLaserPath();
line.enabled = true;
SetLineColor(laserColor); SetLineColor(laserColor);
hasTriggeredDeathThisBurst = false; hasTriggeredDeathThisBurst = false;
CheckHit(); CheckHit();
@ -140,20 +170,16 @@ public class LaserBeam : MonoBehaviour
public void TickLaserDuringFiring() public void TickLaserDuringFiring()
{ {
if (currentPhase == LaserPhase.Firing) if (currentPhase != LaserPhase.Firing) return;
{ UpdateLaserPath();
UpdateLaserPath(); CheckHit();
CheckHit();
}
} }
public void TickLaserDuringCharging() public void TickLaserDuringCharging()
{ {
if (currentPhase == LaserPhase.Charging) if (currentPhase != LaserPhase.Charging) return;
{ BlinkWarning();
BlinkWarning(); UpdateLaserPath();
UpdateLaserPath();
}
} }
IEnumerator IntroShow() IEnumerator IntroShow()
@ -183,6 +209,9 @@ public class LaserBeam : MonoBehaviour
void DisableLaser() void DisableLaser()
{ {
line.enabled = false; line.enabled = false;
// Also clear endpoint for safety/debug
laserStart = transform.position;
laserEnd = laserStart;
} }
void BlinkWarning() void BlinkWarning()
@ -194,6 +223,13 @@ public class LaserBeam : MonoBehaviour
void CheckHit() void CheckHit()
{ {
// HARD GATES: never kill unless were allowed to
if (!isActiveAndEnabled) return;
if (!gameObject.activeInHierarchy) return;
bool lethalNow = (currentPhase == LaserPhase.Firing) || (introRunning && introIsDeadly);
if (!lethalNow) return;
if (hasTriggeredDeathThisBurst) return; if (hasTriggeredDeathThisBurst) return;
bool CheckSegment(Vector3 a, Vector3 b) bool CheckSegment(Vector3 a, Vector3 b)
@ -224,7 +260,7 @@ public class LaserBeam : MonoBehaviour
return true; return true;
} }
if (bestCol.CompareTag(boxTag)) if (bestCol && bestCol.CompareTag(boxTag))
{ {
var boxHealth = bestCol.GetComponent<LaserBoxHealth>(); var boxHealth = bestCol.GetComponent<LaserBoxHealth>();
if (boxHealth != null) if (boxHealth != null)

View File

@ -5,112 +5,133 @@ using static LaserBeam;
public class LaserBeamController : MonoBehaviour public class LaserBeamController : MonoBehaviour
{ {
public float interval = 20f; [Header("Cycle Durations")]
public float chargeDuration = 3f; public float interval = 20f; // how long each batch runs before switching
public float fireDuration = 1f; public float idleBeforeCharge = 3f; // Idle time before Charging
public float chargingWindup = 1f; // Charging duration (blink)
public float fireDuration = 1f; // Firing duration (lethal)
private List<LaserBeam> allLasers = new List<LaserBeam>(); [Header("Beams (auto-populated)")]
private List<List<LaserBeam>> batches = new List<List<LaserBeam>>(); public List<LaserBeam> allLasers = new List<LaserBeam>();
private List<LaserBeam> activeLasers = new List<LaserBeam>();
private float timer = 0f; private readonly List<List<LaserBeam>> batches = new();
private LaserPhase currentPhase = LaserPhase.Idle; private readonly List<LaserBeam> activeLasers = new();
void Start() void Start()
{ {
// Use active-only; if you need inactive too, use FindObjectsOfType<LaserBeam>(true)
allLasers.AddRange(FindObjectsOfType<LaserBeam>()); allLasers.AddRange(FindObjectsOfType<LaserBeam>());
foreach (var lb in allLasers)
{
if (!lb) continue;
lb.externalControl = true; // controller owns timing
lb.ResetCycle(); // Idle + line off
lb.enabled = false; // completely inert until selected
}
// Shuffle // Shuffle
for (int i = 0; i < allLasers.Count; i++) for (int i = 0; i < allLasers.Count; i++)
{ {
var temp = allLasers[i]; int r = Random.Range(i, allLasers.Count);
int rand = Random.Range(i, allLasers.Count); (allLasers[i], allLasers[r]) = (allLasers[r], allLasers[i]);
allLasers[i] = allLasers[rand];
allLasers[rand] = temp;
} }
// Split into 3 batches // Split into 3 batches
int batchSize = Mathf.CeilToInt(allLasers.Count / 3f); int batchSize = Mathf.CeilToInt(Mathf.Max(1, allLasers.Count) / 3f);
for (int i = 0; i < allLasers.Count; i += batchSize) for (int i = 0; i < allLasers.Count; i += batchSize)
{
batches.Add(allLasers.GetRange(i, Mathf.Min(batchSize, allLasers.Count - i))); batches.Add(allLasers.GetRange(i, Mathf.Min(batchSize, allLasers.Count - i)));
}
foreach (var lb in allLasers) StartCoroutine(RunBatches());
lb.enabled = false;
StartCoroutine(ActivateBatches());
} }
IEnumerator ActivateBatches() IEnumerator RunBatches()
{ {
for (int i = 0; i < batches.Count; i++) for (int bi = 0; bi < batches.Count; bi++)
{ {
activeLasers.AddRange(batches[i]); // Disable everything first
foreach (var lb in allLasers)
if (lb) { lb.enabled = false; lb.ResetCycle(); }
foreach (var laser in activeLasers) // Activate this batch
laser.enabled = true; activeLasers.Clear();
activeLasers.AddRange(batches[bi]);
foreach (var l in activeLasers)
{
if (!l) continue;
l.enabled = true;
l.ResetCycle();
l.SetLaserPhase(LaserPhase.Idle);
}
ResetAllLasers(); float batchEnd = Time.time + interval;
yield return new WaitForSeconds(interval); // Loop phases for the duration of this batch
while (Time.time < batchEnd)
{
// Compute runway required to complete a Charge->Fire sequence
float runway = Mathf.Max(0f, chargingWindup) + Mathf.Max(0f, fireDuration);
float timeLeft = batchEnd - Time.time;
// If not enough time to do Charging+Firing, just stay Idle until the batch ends.
if (timeLeft < runway)
{
yield return PhaseBlock(LaserPhase.Idle, timeLeft, batchEnd);
break;
}
// IDLE
float idleTime = Mathf.Max(0f, idleBeforeCharge);
// If idleTime itself would consume too much and leave < runway, trim idle to fit
if (idleTime > 0f && idleTime > timeLeft - runway)
idleTime = Mathf.Max(0f, timeLeft - runway);
if (idleTime > 0f)
{
yield return PhaseBlock(LaserPhase.Idle, idleTime, batchEnd);
if (Time.time >= batchEnd) break;
}
// CHARGING (blink)
if (chargingWindup > 0f)
{
yield return PhaseBlock(LaserPhase.Charging, chargingWindup, batchEnd);
// we guaranteed runway, so we should still have time left for Firing
}
// FIRING (lethal)
if (fireDuration > 0f)
{
yield return PhaseBlock(LaserPhase.Firing, fireDuration, batchEnd);
}
}
} }
} }
void Update() // Set phase for all active lasers and tick per-frame for 'duration' (clamped by batch end)
IEnumerator PhaseBlock(LaserPhase phase, float duration, float batchHardEnd)
{ {
if (activeLasers.Count == 0) return; SetPhaseForAll(phase);
timer += Time.deltaTime; float end = Mathf.Min(Time.time + duration, batchHardEnd);
while (Time.time < end)
switch (currentPhase)
{ {
case LaserPhase.Idle: if (phase == LaserPhase.Charging)
if (timer >= chargeDuration) {
{ foreach (var l in activeLasers) if (l && l.isActiveAndEnabled) l.TickLaserDuringCharging();
timer = 0f; }
currentPhase = LaserPhase.Charging; else if (phase == LaserPhase.Firing)
foreach (var laser in activeLasers) {
laser.SetLaserPhase(LaserPhase.Charging); foreach (var l in activeLasers) if (l && l.isActiveAndEnabled) l.TickLaserDuringFiring();
} }
break;
case LaserPhase.Charging: yield return null;
foreach (var laser in activeLasers)
laser.TickLaserDuringCharging();
if (timer >= 1f)
{
timer = 0f;
currentPhase = LaserPhase.Firing;
foreach (var laser in activeLasers)
laser.SetLaserPhase(LaserPhase.Firing);
}
break;
case LaserPhase.Firing:
foreach (var laser in activeLasers)
laser.TickLaserDuringFiring();
if (timer >= fireDuration)
{
timer = 0f;
currentPhase = LaserPhase.Idle;
foreach (var laser in activeLasers)
laser.SetLaserPhase(LaserPhase.Idle);
}
break;
} }
} }
void ResetAllLasers() void SetPhaseForAll(LaserPhase p)
{ {
timer = 0f; foreach (var l in activeLasers)
currentPhase = LaserPhase.Idle; if (l && l.isActiveAndEnabled) l.SetLaserPhase(p);
foreach (var laser in activeLasers)
{
laser.SetLaserPhase(LaserPhase.Idle);
}
} }
} }

View File

@ -4,7 +4,10 @@ using UnityEngine;
public class LevelCompleteDoorTrigger : MonoBehaviour public class LevelCompleteDoorTrigger : MonoBehaviour
{ {
public string playerTag = "Player"; public string playerTag = "Player";
private void Start()
{
}
void Reset() void Reset()
{ {
// Make sure the collider is a trigger // Make sure the collider is a trigger
@ -16,7 +19,7 @@ public class LevelCompleteDoorTrigger : MonoBehaviour
{ {
if (other.CompareTag(playerTag)) if (other.CompareTag(playerTag))
{ {
CrateEscapeGameManager.Instance?.OnLevelCompleteTriggered(); //CrateEscapeGameManager.Instance?.OnLevelCompleteTriggered();
} }
} }
} }

View File

@ -1,54 +1,134 @@
using UnityEngine; using UnityEngine;
using DG.Tweening; using DG.Tweening;
[RequireComponent(typeof(Collider))]
public class SmoothChildRotation : MonoBehaviour public class SmoothChildRotation : MonoBehaviour
{ {
[Header("References")] [Header("Door Parts")]
public Transform targetChild; // Child object to rotate public Transform targetChild; // door mesh child to rotate
public string playerTag = "Player"; // Tag for player detection public string playerTag = "Player";
[Header("Rotation Settings")] [Header("Door Rotation")]
public float rotationDuration = 0.5f; // Time in seconds for rotation public float rotationDuration = 0.5f;
public Ease rotationEase = Ease.OutQuad; // Easing type public Ease rotationEase = Ease.OutQuad;
public Vector3 targetRotationOnEnter = new Vector3(0, 0, -90); public Vector3 targetRotationOnEnter = new Vector3(0, 0, -90);
public Vector3 targetRotationOnExit = new Vector3(0, 0, 0); public Vector3 targetRotationOnExit = new Vector3(0, 0, 0);
private Tween currentTween; [Header("Cutscene: Exit Path")]
[Tooltip("If not set, we use (this.transform.position + this.transform.forward * exitForwardDistance).")]
public Transform exitPoint;
public float exitForwardDistance = 3f; // used when exitPoint is null
public float faceDuration = 0.2f; // how quickly the player faces exit
private void Start() [Header("Cutscene: Anim States")]
public string celebrationStateName = "Celebration";
public string walkStateName = "Run"; // or your locomotion state/BT name
[Header("Cutscene: Timing")]
public float celebrationDuration = 1.2f; // seconds to hold Celebration
public float walkMoveDuration = 1.5f; // seconds to walk to exit
public Ease walkMoveEase = Ease.Linear;
[Header("Behavior")]
public bool disableTriggerAfterUse = true; // prevents re-entrant triggers
private Tween _doorTween;
private bool _cutsceneRunning;
void Reset()
{ {
if (targetChild == null) var col = GetComponent<Collider>();
if (col) col.isTrigger = true;
}
void Start()
{
if (!targetChild)
{ {
Debug.LogError("Target child is not assigned!"); Debug.LogError("[SmoothChildRotation] Target child not assigned!", this);
enabled = false; enabled = false;
return;
} }
} }
private void OnTriggerEnter(Collider other) void OnTriggerEnter(Collider other)
{ {
if (other.CompareTag(playerTag)) if (_cutsceneRunning) return;
if (!other.CompareTag(playerTag)) return;
// Open the door
RotateTo(targetRotationOnEnter);
// Start the cutscene
StartCoroutine(RunDoorCutscene(other));
}
void OnTriggerExit(Collider other)
{
if (!other.CompareTag(playerTag)) return;
if (_cutsceneRunning) return; // dont close while cutscene is ongoing
RotateTo(targetRotationOnExit);
}
void RotateTo(Vector3 targetRot)
{
_doorTween?.Kill();
_doorTween = targetChild
.DOLocalRotate(targetRot, rotationDuration)
.SetEase(rotationEase);
}
System.Collections.IEnumerator RunDoorCutscene(Collider hit)
{
_cutsceneRunning = true;
if (disableTriggerAfterUse) { var c = GetComponent<Collider>(); if (c) c.enabled = false; }
// Find player components
Transform root = hit.attachedRigidbody ? hit.attachedRigidbody.transform : hit.transform.root;
var controller = root.GetComponentInChildren<CrateEscapePlayerControllerJoystick>();
var anim = root.GetComponentInChildren<ZibuAnimDriver>();
var rb = root.GetComponentInChildren<Rigidbody>();
// 1) Freeze player control & velocity
if (controller) controller.LockControls(true);
if (rb) { rb.velocity = Vector3.zero; rb.angularVelocity = Vector3.zero; }
// 2) Work out exit position / facing
Vector3 dst = exitPoint ? exitPoint.position : (transform.position + transform.forward * exitForwardDistance);
if (rb) dst.y = rb.position.y; else dst.y = root.position.y;
Vector3 faceDir = dst - root.position; faceDir.y = 0f;
if (faceDir.sqrMagnitude > 0.0001f)
{ {
RotateTo(targetRotationOnEnter); Quaternion look = Quaternion.LookRotation(faceDir.normalized, Vector3.up);
// quick face toward the door exit
root.DORotateQuaternion(look, faceDuration).SetEase(Ease.OutQuad);
} }
}
private void OnTriggerExit(Collider other) // 3) Celebration anim
{ if (anim && !string.IsNullOrEmpty(celebrationStateName))
if (other.CompareTag(playerTag)) anim.PlayState(celebrationStateName, 0.1f);
{
RotateTo(targetRotationOnExit);
}
}
private void RotateTo(Vector3 targetRot) yield return new WaitForSeconds(celebrationDuration);
{
// Kill any existing tween to avoid stacking
currentTween?.Kill();
currentTween = targetChild.DOLocalRotate( // 4) Walk/Run anim
targetRot, if (anim && !string.IsNullOrEmpty(walkStateName))
rotationDuration anim.PlayState(walkStateName, 0.1f);
).SetEase(rotationEase);
// 5) Move player outside the door
Tween moveT;
if (rb != null)
moveT = rb.DOMove(dst, walkMoveDuration).SetEase(walkMoveEase).SetUpdate(UpdateType.Fixed);
else
moveT = root.DOMove(dst, walkMoveDuration).SetEase(walkMoveEase);
yield return moveT.WaitForCompletion();
// 6) Level complete signal
CrateEscapeGameManager.Instance?.OnLevelCompleteTriggered();
// (Optional) keep controls locked while level completes. Remove next line if you want to give control back:
// if (controller) controller.LockControls(false);
_cutsceneRunning = false;
} }
} }

View File

@ -41,6 +41,11 @@ public class ZibuAnimDriver : MonoBehaviour
default: CrossFadeSafe(state.ToString(), xfade); break; default: CrossFadeSafe(state.ToString(), xfade); break;
} }
} }
public void PlayState(string stateName, float xfade = 0.1f)
{
if (string.IsNullOrEmpty(stateName)) return;
CrossFadeSafe(stateName, xfade);
}
void CrossFadeSafe(string stateName, float transition) void CrossFadeSafe(string stateName, float transition)
{ {

View File

@ -35,7 +35,7 @@ EditorBuildSettings:
- enabled: 0 - enabled: 0
path: Assets/Scenes/ChaseRun.unity path: Assets/Scenes/ChaseRun.unity
guid: be6c423b3d68dcb48bc49a7d2ed4957d guid: be6c423b3d68dcb48bc49a7d2ed4957d
- enabled: 0 - enabled: 1
path: Assets/Scenes/CrateEscape.unity path: Assets/Scenes/CrateEscape.unity
guid: af5c5d2a2d201e24f8c4913ae531addf guid: af5c5d2a2d201e24f8c4913ae531addf
- enabled: 0 - enabled: 0
@ -44,7 +44,7 @@ EditorBuildSettings:
- enabled: 0 - enabled: 0
path: Assets/Scenes/BlockDrop.unity path: Assets/Scenes/BlockDrop.unity
guid: a99fde5053c68e0478ef4665b9c7b510 guid: a99fde5053c68e0478ef4665b9c7b510
- enabled: 1 - enabled: 0
path: Assets/Scenes/SkyWalker.unity path: Assets/Scenes/SkyWalker.unity
guid: 71a02188e9a650b47a6c28ddde2142d6 guid: 71a02188e9a650b47a6c28ddde2142d6
- enabled: 0 - enabled: 0

View File

@ -44,10 +44,10 @@ PlayerSettings:
m_HolographicTrackingLossScreen: {fileID: 0} m_HolographicTrackingLossScreen: {fileID: 0}
defaultScreenWidth: 1920 defaultScreenWidth: 1920
defaultScreenHeight: 1080 defaultScreenHeight: 1080
defaultScreenWidthWeb: 960 defaultScreenWidthWeb: 600
defaultScreenHeightWeb: 600 defaultScreenHeightWeb: 960
m_StereoRenderingPath: 0 m_StereoRenderingPath: 0
m_ActiveColorSpace: 0 m_ActiveColorSpace: 1
unsupportedMSAAFallback: 0 unsupportedMSAAFallback: 0
m_SpriteBatchVertexThreshold: 300 m_SpriteBatchVertexThreshold: 300
m_MTRendering: 1 m_MTRendering: 1
@ -536,7 +536,7 @@ PlayerSettings:
m_Automatic: 1 m_Automatic: 1
- m_BuildTarget: WebGLSupport - m_BuildTarget: WebGLSupport
m_APIs: 0b000000 m_APIs: 0b000000
m_Automatic: 0 m_Automatic: 1
m_BuildTargetVRSettings: m_BuildTargetVRSettings:
- m_BuildTarget: Standalone - m_BuildTarget: Standalone
m_Enabled: 0 m_Enabled: 0
@ -828,7 +828,7 @@ PlayerSettings:
webGLWasmArithmeticExceptions: 0 webGLWasmArithmeticExceptions: 0
webGLLinkerTarget: 1 webGLLinkerTarget: 1
webGLThreadsSupport: 0 webGLThreadsSupport: 0
webGLDecompressionFallback: 1 webGLDecompressionFallback: 0
webGLInitialMemorySize: 32 webGLInitialMemorySize: 32
webGLMaximumMemorySize: 2048 webGLMaximumMemorySize: 2048
webGLMemoryGrowthMode: 2 webGLMemoryGrowthMode: 2