using System.Collections.Generic; using UnityEngine; using Unheimlich.Features.Player; namespace Unheimlich.World.Environment { public class InfiniteStaircaseController : MonoBehaviour { // ─── Prefabs ───────────────────────────────────────────────────────── [Header("Stair Prefabs")] [Tooltip("Segment standard utilisé pour remplir la tour.")] [SerializeField] private GameObject _stairPrefab; [Tooltip("Segment toujours placé tout en HAUT de la tour (slot +maxIterationsUp).")] [SerializeField] private GameObject _stairTopPrefab; [Tooltip("Segment toujours placé tout en BAS de la tour (slot -maxIterationsDown).")] [SerializeField] private GameObject _stairBottomPrefab; [Tooltip("Si true, le premier enfant avec renderers est réutilisé comme slot 0.")] [SerializeField] private bool _useExistingChildAsBase = true; // ─── Iterations ────────────────────────────────────────────────────── [Header("Iterations")] [Tooltip("Nombre de segments au-dessus du sol (montée).")] [SerializeField] private int _maxIterationsUp = 4; [Tooltip("Nombre de segments en-dessous du sol (descente).")] [SerializeField] private int _maxIterationsDown = 4; // ─── Segment Bounds ────────────────────────────────────────────────── [Header("Segment Bounds — Auto-Detection")] [SerializeField] private Renderer _centralColumnRenderer; [SerializeField] private float _segmentHeight = 4.82f; [SerializeField] private float _segmentHeightOffset = 0f; // ─── Scroll Control ─────────────────────────────────────────────────── [Header("Scroll Speed Control")] [SerializeField] private float _correctionGain = 4f; [SerializeField] private float _maxScrollSpeed = 8f; [SerializeField] private float _scrollSmoothTime = 0.1f; // ─── Rotation Offset ───────────────────────────────────────────────── [Header("Rotation Offset")] [SerializeField] private float _segmentRotationOffsetY = 0f; // ─── Debug ─────────────────────────────────────────────────────────── [Header("Debug")] [SerializeField] private bool _drawGizmos = true; // ─── State ─────────────────────────────────────────────────────────── private enum StairState { Idle, Active, Stopping, Done } private StairState _currentState = StairState.Idle; private Transform _playerTransform; private Transform _towerRoot; private float _triggerCenterWorldY; private bool _enteredFromBelow; private float _limitLocalY; private float _scrollSpeed; private float _scrollSpeedVelocity; private readonly Dictionary _segmentsBySlot = new(); // ─── Properties ────────────────────────────────────────────────────── private float EffectiveSegmentHeight => _segmentHeight + _segmentHeightOffset; /// Déplacement max dépend de la direction. private float MaxTowerDisplacementUp => _maxIterationsUp * EffectiveSegmentHeight; private float MaxTowerDisplacementDown => _maxIterationsDown * EffectiveSegmentHeight; // ─── Lifecycle ─────────────────────────────────────────────────────── private void Awake() { AutoDetectBoundsFromColumn(); CacheTriggerCenterY(); } private void Update() { EnsurePlayerCached(); switch (_currentState) { case StairState.Active: UpdateScrollSpeed(); ApplyScrollToTower(); CheckLimitReached(); break; case StairState.Stopping: DecelerateAndFinish(); break; } } // ─── Trigger Events ─────────────────────────────────────────────────── public void OnPlayerEnteredTrigger() { if (_currentState != StairState.Idle && _currentState != StairState.Done) return; EnsurePlayerCached(); if (_playerTransform == null) return; _enteredFromBelow = _playerTransform.position.y < _triggerCenterWorldY; _scrollSpeed = 0f; _scrollSpeedVelocity = 0f; BuildTower(); _currentState = StairState.Active; } public void OnPlayerExitedTrigger() { } // ─── Tower Construction ─────────────────────────────────────────────── private void BuildTower() { DestroyTower(); _towerRoot = new GameObject("_TowerRoot").transform; _towerRoot.SetParent(transform, false); _towerRoot.localPosition = Vector3.zero; _towerRoot.localRotation = Quaternion.identity; _towerRoot.localScale = Vector3.one; if (_enteredFromBelow) { // Slot 0 → +maxIterationsUp for (int i = 0; i <= _maxIterationsUp; i++) SpawnSegmentAtSlot(i); _limitLocalY = -MaxTowerDisplacementUp; } else { // Slot 0 → -maxIterationsDown for (int i = 0; i >= -_maxIterationsDown; i--) SpawnSegmentAtSlot(i); _limitLocalY = MaxTowerDisplacementDown; } } private void DestroyTower() { if (_towerRoot != null) { Destroy(_towerRoot.gameObject); _towerRoot = null; } _segmentsBySlot.Clear(); } /// /// Choisit le bon prefab selon le slot : /// - slot == +maxIterationsUp → _stairTopPrefab /// - slot == -maxIterationsDown → _stairBottomPrefab /// - tout le reste → _stairPrefab /// Le slot 0 peut aussi réutiliser l'enfant existant si l'option est activée. /// private void SpawnSegmentAtSlot(int slotIndex) { if (_segmentsBySlot.ContainsKey(slotIndex)) return; float localY = slotIndex * EffectiveSegmentHeight; GameObject segment; // ── Slot 0 : réutilisation de l'enfant existant ────────────────── if (slotIndex == 0 && _useExistingChildAsBase) { Transform existing = FindExistingStairChild(); if (existing != null) { segment = existing.gameObject; segment.transform.SetParent(_towerRoot, false); segment.transform.localPosition = new Vector3(0f, localY, 0f); segment.transform.localRotation = Quaternion.identity; ApplyRotationOffsetFromCenter(segment); _segmentsBySlot[slotIndex] = segment; return; } } // ── Choix du prefab ─────────────────────────────────────────────── GameObject prefabToUse = PickPrefabForSlot(slotIndex); if (prefabToUse == null) return; segment = Instantiate(prefabToUse, _towerRoot); segment.transform.localPosition = new Vector3(0f, localY, 0f); segment.transform.localRotation = Quaternion.identity; ApplyRotationOffsetFromCenter(segment); _segmentsBySlot[slotIndex] = segment; } /// /// Renvoie le prefab approprié pour un slot donné. /// Top = slot +maxIterationsUp (sommet absolu de la tour montante) /// Bottom = slot -maxIterationsDown (fond absolu de la tour descendante) /// Sinon = prefab standard. /// private GameObject PickPrefabForSlot(int slotIndex) { if (slotIndex == _maxIterationsUp && _stairTopPrefab != null) return _stairTopPrefab; if (slotIndex == -_maxIterationsDown && _stairBottomPrefab != null) return _stairBottomPrefab; return _stairPrefab; } // ─── Scroll ─────────────────────────────────────────────────────────── private void UpdateScrollSpeed() { if (_playerTransform == null) return; float error = _playerTransform.position.y - _triggerCenterWorldY; float rawTarget = Mathf.Clamp(error * _correctionGain, -_maxScrollSpeed, _maxScrollSpeed); bool towerHasMoved = _enteredFromBelow ? _towerRoot != null && _towerRoot.localPosition.y < -0.05f : _towerRoot != null && _towerRoot.localPosition.y > 0.05f; float targetSpeed = towerHasMoved ? rawTarget : (_enteredFromBelow ? Mathf.Max(rawTarget, 0f) : Mathf.Min(rawTarget, 0f)); _scrollSpeed = Mathf.SmoothDamp(_scrollSpeed, targetSpeed, ref _scrollSpeedVelocity, _scrollSmoothTime); } private void ApplyScrollToTower() { if (_towerRoot == null) return; Vector3 pos = _towerRoot.localPosition; pos.y -= _scrollSpeed * Time.deltaTime; pos.y = _enteredFromBelow ? Mathf.Max(pos.y, _limitLocalY) : Mathf.Min(pos.y, _limitLocalY); _towerRoot.localPosition = pos; } private void CheckLimitReached() { if (_towerRoot == null) return; bool reached = _enteredFromBelow ? _towerRoot.localPosition.y <= _limitLocalY : _towerRoot.localPosition.y >= _limitLocalY; if (reached) _currentState = StairState.Stopping; } private void DecelerateAndFinish() { _scrollSpeed = Mathf.SmoothDamp(_scrollSpeed, 0f, ref _scrollSpeedVelocity, _scrollSmoothTime); ApplyScrollToTower(); if (Mathf.Abs(_scrollSpeed) < 0.02f) FinishAndCleanup(); } // ─── Cleanup ───────────────────────────────────────────────────────── private void FinishAndCleanup() { _currentState = StairState.Done; if (_towerRoot == null) return; // Le segment qui doit rester visible après la fin = l'extrémité atteinte int keepSlot = _enteredFromBelow ? _maxIterationsUp : -_maxIterationsDown; _segmentsBySlot.TryGetValue(keepSlot, out GameObject keepSegment); if (keepSegment != null) { keepSegment.transform.SetParent(transform, true); keepSegment.transform.position = transform.position; } DestroyTower(); if (keepSegment != null) _segmentsBySlot[0] = keepSegment; _scrollSpeed = 0f; _scrollSpeedVelocity = 0f; _playerTransform = null; } // ─── Helpers ───────────────────────────────────────────────────────── private void AutoDetectBoundsFromColumn() { if (_centralColumnRenderer == null) return; _segmentHeight = _centralColumnRenderer.bounds.size.y; } private void CacheTriggerCenterY() { StaircaseTriggerZone zone = GetComponentInChildren(); if (zone == null) { Debug.LogWarning("[InfiniteStaircase] No StaircaseTriggerZone found in children.", this); return; } Collider col = zone.GetComponent(); if (col != null) _triggerCenterWorldY = col.bounds.center.y; } private void EnsurePlayerCached() { if (_playerTransform != null) return; if (PlayerController.Instance != null) _playerTransform = PlayerController.Instance.transform; } private Transform FindExistingStairChild() { foreach (Transform child in transform) { if (child == _towerRoot) continue; if (child.GetComponentInChildren() != null) return child; } return null; } private void ApplyRotationOffsetFromCenter(GameObject segment) { if (Mathf.Approximately(_segmentRotationOffsetY, 0f)) return; segment.transform.RotateAround(ComputeWorldBoundsCenter(segment), Vector3.up, _segmentRotationOffsetY); } private Vector3 ComputeWorldBoundsCenter(GameObject segment) { Renderer[] renderers = segment.GetComponentsInChildren(); if (renderers.Length == 0) return segment.transform.position + Vector3.up * (EffectiveSegmentHeight * 0.5f); Bounds combined = renderers[0].bounds; for (int i = 1; i < renderers.Length; i++) combined.Encapsulate(renderers[i].bounds); return combined.center; } // ─── Gizmos ────────────────────────────────────────────────────────── private void OnDrawGizmosSelected() { if (!_drawGizmos) return; float h = EffectiveSegmentHeight; Vector3 o = transform.position; // Tour montante — vert Gizmos.color = new Color(0f, 1f, 0.5f, 0.35f); for (int i = 1; i <= _maxIterationsUp; i++) Gizmos.DrawWireCube(new Vector3(o.x, o.y + i * h + h * 0.5f, o.z), new Vector3(1.8f, h * 0.95f, 1.8f)); // Tour descendante — orange Gizmos.color = new Color(1f, 0.6f, 0f, 0.25f); for (int i = 1; i <= _maxIterationsDown; i++) Gizmos.DrawWireCube(new Vector3(o.x, o.y - i * h + h * 0.5f, o.z), new Vector3(1.8f, h * 0.95f, 1.8f)); // Ligne du centre du trigger — cyan Gizmos.color = Color.cyan; Gizmos.DrawLine( new Vector3(o.x - 2f, _triggerCenterWorldY, o.z), new Vector3(o.x + 2f, _triggerCenterWorldY, o.z) ); // Slot top — magenta if (_stairTopPrefab != null) { Gizmos.color = Color.magenta; Gizmos.DrawWireCube( new Vector3(o.x, o.y + _maxIterationsUp * h + h * 0.5f, o.z), new Vector3(2f, h, 2f) ); } // Slot bottom — rouge if (_stairBottomPrefab != null) { Gizmos.color = Color.red; Gizmos.DrawWireCube( new Vector3(o.x, o.y - _maxIterationsDown * h + h * 0.5f, o.z), new Vector3(2f, h, 2f) ); } } } }