using Core; using Core.Lifecycle; using Core.Settings; using UnityEngine; namespace Minigames.BirdPooper { /// /// Bird player controller with Flappy Bird-style flight mechanics. /// Responds to tap input to flap, with manual gravity simulation. /// public class BirdPlayerController : ManagedBehaviour, ITouchInputConsumer { [Header("Events")] public UnityEngine.Events.UnityEvent OnFlap; public UnityEngine.Events.UnityEvent OnPlayerDamaged; private Rigidbody2D rb; private IBirdPooperSettings settings; private float verticalVelocity = 0f; private bool isDead = false; private float fixedXPosition; // Store the initial X position from the scene internal override void OnManagedAwake() { base.OnManagedAwake(); // Initialize events if (OnFlap == null) OnFlap = new UnityEngine.Events.UnityEvent(); if (OnPlayerDamaged == null) OnPlayerDamaged = new UnityEngine.Events.UnityEvent(); // Load settings settings = GameManager.GetSettingsObject(); if (settings == null) { Debug.LogError("[BirdPlayerController] BirdPooperSettings not found!"); return; } // Get Rigidbody2D component (Dynamic with gravityScale = 0) rb = GetComponent(); if (rb != null) { rb.gravityScale = 0f; // Disable Unity physics gravity rb.bodyType = RigidbodyType2D.Kinematic; // Kinematic = manual movement, no physics forces // Store the initial X position from the scene fixedXPosition = rb.position.x; Debug.Log($"[BirdPlayerController] Fixed X position set to: {fixedXPosition}"); } else { Debug.LogError("[BirdPlayerController] Rigidbody2D component not found!"); return; } // Register as override consumer to capture ALL input (except UI button) // Register as override consumer to capture ALL input (except UI button) if (Input.InputManager.Instance != null) { Input.InputManager.Instance.RegisterOverrideConsumer(this); Debug.Log("[BirdPlayerController] Registered as override input consumer"); } else { Debug.LogError("[BirdPlayerController] InputManager instance not found!"); } } private void Update() { if (!isDead && settings != null && rb != null) { // Apply manual gravity verticalVelocity -= settings.Gravity * Time.deltaTime; // Cap fall speed (terminal velocity) if (verticalVelocity < -settings.MaxFallSpeed) verticalVelocity = -settings.MaxFallSpeed; // Update position manually Vector2 newPosition = rb.position; newPosition.y += verticalVelocity * Time.deltaTime; newPosition.x = fixedXPosition; // Keep X fixed at scene-configured position // Clamp Y position to bounds newPosition.y = Mathf.Clamp(newPosition.y, settings.MinY, settings.MaxY); rb.MovePosition(newPosition); // Update rotation based on velocity UpdateRotation(); } } #region ITouchInputConsumer Implementation public void OnTap(Vector2 tapPosition) { if (!isDead && settings != null) { verticalVelocity = settings.FlapForce; Debug.Log($"[BirdPlayerController] Flap! velocity = {verticalVelocity}"); // Emit flap event OnFlap?.Invoke(); } } public void OnHoldStart(Vector2 position) { } public void OnHoldMove(Vector2 position) { } public void OnHoldEnd(Vector2 position) { } #endregion #region Rotation /// /// Updates the bird's rotation based on vertical velocity. /// Bird tilts up when flapping, down when falling. /// private void UpdateRotation() { if (settings == null) return; // Map velocity to rotation angle // When falling at max speed (-MaxFallSpeed): -MaxRotationAngle (down) // When at flap velocity (+FlapForce): +MaxRotationAngle (up) float velocityPercent = Mathf.InverseLerp( -settings.MaxFallSpeed, settings.FlapForce, verticalVelocity ); float targetAngle = Mathf.Lerp( -settings.MaxRotationAngle, settings.MaxRotationAngle, velocityPercent ); // Get current angle (handle 0-360 wrapping to -180-180) float currentAngle = transform.rotation.eulerAngles.z; if (currentAngle > 180f) currentAngle -= 360f; // Smooth interpolation to target float smoothedAngle = Mathf.Lerp( currentAngle, targetAngle, settings.RotationSpeed * Time.deltaTime ); // Apply rotation to Z axis only (2D rotation) transform.rotation = Quaternion.Euler(0, 0, smoothedAngle); } #endregion #region Trigger-Based Collision Detection /// /// Called when a trigger collider enters this object's trigger. /// Used for detecting obstacles without physics interactions. /// private void OnTriggerEnter2D(Collider2D other) { // Check if the colliding object is tagged as an obstacle if (other.CompareTag("Obstacle")) { HandleDeath(); } } private void HandleDeath() { // Only process death once if (isDead) return; isDead = true; verticalVelocity = 0f; Debug.Log("[BirdPlayerController] Bird died!"); // Emit damage event - let the game manager handle UI OnPlayerDamaged?.Invoke(); } #endregion #region Public Properties public bool IsDead => isDead; #endregion } }