using AppleHills.Core.Interfaces; using AppleHills.Core.Settings; using AppleHillsCamera; using Core; using Input; using UnityEngine; namespace Minigames.DivingForPictures.Player { /// /// Handles endless descender movement in response to tap and hold input events. /// Moves the character horizontally to follow the finger or tap position. /// public class PlayerController : MonoBehaviour, ITouchInputConsumer, IPausable { [Tooltip("Reference to the edge anchor that this player should follow for Y position")] [SerializeField] private EdgeAnchor edgeAnchor; // Settings reference private IDivingMinigameSettings settings; private float targetFingerX; private bool isTouchActive; private float originY; // Tap impulse system variables private float tapImpulseStrength = 0f; private float tapDirection = 0f; // Initialization flag private bool isInitialized = false; void Awake() { originY = transform.position.y; // Get settings from GameManager settings = GameManager.GetSettingsObject(); if (settings == null) { Debug.LogError("[PlayerController] Failed to load diving minigame settings!"); } } void OnEnable() { // Register as a pausable component with DivingGameManager DivingGameManager.Instance.RegisterPausableComponent(this); } void Start() { // Initialize target to current position targetFingerX = transform.position.x; isTouchActive = false; // Try to find edge anchor if not assigned if (edgeAnchor == null) { // First try to find edge anchor on the same object or parent edgeAnchor = GetComponentInParent(); // If not found, find any edge anchor in the scene if (edgeAnchor == null) { edgeAnchor = FindFirstObjectByType(); if (edgeAnchor == null) { Logging.Warning("[PlayerController] No EdgeAnchor found in scene. Origin Y position won't update with camera changes."); } else { Logging.Debug($"[PlayerController] Auto-connected to EdgeAnchor on {edgeAnchor.gameObject.name}"); } } } // Subscribe to edge anchor events if it exists if (edgeAnchor != null) { // Unsubscribe first to prevent duplicate subscriptions edgeAnchor.OnPositionUpdated -= UpdateOriginYFromAnchor; edgeAnchor.OnPositionUpdated += UpdateOriginYFromAnchor; // Update origin Y based on current anchor position UpdateOriginYFromAnchor(); } DivingGameManager.Instance.OnGameInitialized += Initialize; // If game is already initialized, initialize immediately if (DivingGameManager.Instance.GetType().GetField("_isGameInitialized", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?.GetValue(DivingGameManager.Instance) is bool isInitialized && isInitialized) { Initialize(); } } /// /// Initializes the player controller when triggered by DivingGameManager /// private void Initialize() { if (isInitialized) return; // Register as default consumer for input InputManager.Instance?.SetDefaultConsumer(this); isInitialized = true; Logging.Debug("[PlayerController] Initialized"); } private void OnDestroy() { DivingGameManager.Instance.OnGameInitialized -= Initialize; // Unregister as a pausable component DivingGameManager.Instance.UnregisterPausableComponent(this); // Unsubscribe from edge anchor events if (edgeAnchor != null) { edgeAnchor.OnPositionUpdated -= UpdateOriginYFromAnchor; } } /// /// Handles tap input. Applies an impulse in the tapped direction. /// public void OnTap(Vector2 worldPosition) { // Ignore input when paused if (isPaused) return; // Logging.Debug($"[EndlessDescenderController] OnTap at {worldPosition}"); float targetX = Mathf.Clamp(worldPosition.x, settings.ClampXMin, settings.ClampXMax); // Calculate tap direction (+1 for right, -1 for left) tapDirection = Mathf.Sign(targetX - transform.position.x); // Set impulse strength to full tapImpulseStrength = 1.0f; // Store target X for animation purposes targetFingerX = targetX; // Do not set _isTouchActive for taps anymore // _isTouchActive = true; - Removed to prevent continuous movement } /// /// Handles the start of a hold input. Begins tracking the finger. /// public void OnHoldStart(Vector2 worldPosition) { // Ignore input when paused if (isPaused) return; // Logging.Debug($"[EndlessDescenderController] OnHoldStart at {worldPosition}"); targetFingerX = Mathf.Clamp(worldPosition.x, settings.ClampXMin, settings.ClampXMax); isTouchActive = true; } /// /// Handles hold move input. Updates the target X position as the finger moves. /// public void OnHoldMove(Vector2 worldPosition) { // Ignore input when paused if (isPaused) return; // Logging.Debug($"[EndlessDescenderController] OnHoldMove at {worldPosition}"); targetFingerX = Mathf.Clamp(worldPosition.x, settings.ClampXMin, settings.ClampXMax); } /// /// Handles the end of a hold input. Stops tracking. /// public void OnHoldEnd(Vector2 worldPosition) { // Ignore input when paused if (isPaused) return; // Logging.Debug($"[EndlessDescenderController] OnHoldEnd at {worldPosition}"); isTouchActive = false; } void Update() { // Skip movement processing if paused if (isPaused) return; // Handle hold movement if (isTouchActive) { float currentX = transform.position.x; float lerpSpeed = settings.LerpSpeed; float maxOffset = settings.MaxOffset; float exponent = settings.SpeedExponent; float targetX = targetFingerX; float offset = targetX - currentX; offset = Mathf.Clamp(offset, -maxOffset, maxOffset); float absOffset = Mathf.Abs(offset); float t = Mathf.Pow(absOffset / maxOffset, exponent); // Non-linear drop-off float moveStep = Mathf.Sign(offset) * maxOffset * t * Time.deltaTime * lerpSpeed; // Prevent overshooting moveStep = Mathf.Clamp(moveStep, -absOffset, absOffset); float newX = currentX + moveStep; newX = Mathf.Clamp(newX, settings.ClampXMin, settings.ClampXMax); UpdatePosition(newX); } // Handle tap impulse movement else if (tapImpulseStrength > 0) { float currentX = transform.position.x; float maxOffset = settings.MaxOffset; float lerpSpeed = settings.LerpSpeed; // Calculate move distance based on impulse strength float moveDistance = maxOffset * tapImpulseStrength * Time.deltaTime * lerpSpeed; // Limit total movement from single tap moveDistance = Mathf.Min(moveDistance, settings.TapMaxDistance * tapImpulseStrength); // Apply movement in tap direction float newX = currentX + (moveDistance * tapDirection); newX = Mathf.Clamp(newX, settings.ClampXMin, settings.ClampXMax); // Reduce impulse strength over time tapImpulseStrength -= Time.deltaTime * settings.TapDecelerationRate; if (tapImpulseStrength < 0.01f) { tapImpulseStrength = 0f; } UpdatePosition(newX); } } /// /// Updates the player's position with the given X coordinate /// private void UpdatePosition(float newX) { float newY = originY; // Add vertical offset from WobbleBehavior if present WobbleBehavior wobble = GetComponent(); if (wobble != null) { newY += wobble.VerticalOffset; } transform.position = new Vector3(newX, newY, transform.position.z); } /// /// Updates the origin Y position based on camera adjustments /// public void UpdateOriginY(float newOriginY) { originY = newOriginY; } /// /// Updates the origin Y position based on the current position of the player /// This method is intended to be called by the camera adapter when the camera is adjusted. /// private void UpdateOriginYFromCurrentPosition() { originY = transform.position.y; } /// /// Updates the origin Y position based on the current position of the edge anchor /// This method is intended to be called by the edge anchor when its position is updated. /// private void UpdateOriginYFromAnchor() { originY = edgeAnchor.transform.position.y; } #region IPausable Implementation private bool isPaused = false; /// /// Pauses the player controller, blocking all input processing /// public void Pause() { if (isPaused) return; isPaused = true; // If we're being paused, stop any active touch and tap impulse isTouchActive = false; tapImpulseStrength = 0f; Logging.Debug("[PlayerController] Paused"); } /// /// Resumes the player controller, allowing input processing again /// public void DoResume() { if (!isPaused) return; isPaused = false; Logging.Debug("[PlayerController] Resumed"); } /// /// Returns whether the player controller is currently paused /// public bool IsPaused => isPaused; #endregion } }