Files
Ipseity-Demo/Assets/Unheimlich_POC/Scripts/World/Environment/InfiniteStaircaseController.cs
T

411 lines
16 KiB
C#

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<int, GameObject> _segmentsBySlot = new();
// ─── Properties ──────────────────────────────────────────────────────
private float EffectiveSegmentHeight => _segmentHeight + _segmentHeightOffset;
/// <summary>Déplacement max dépend de la direction.</summary>
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();
}
/// <summary>
/// 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.
/// </summary>
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;
}
/// <summary>
/// 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.
/// </summary>
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<StaircaseTriggerZone>();
if (zone == null)
{
Debug.LogWarning("[InfiniteStaircase] No StaircaseTriggerZone found in children.", this);
return;
}
Collider col = zone.GetComponent<Collider>();
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<MeshRenderer>() != 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<Renderer>();
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)
);
}
}
}
}