Files
AppleHillsProduction/Assets/Scripts/Input/BasePlayerMovementController.cs
2025-12-15 11:59:40 +01:00

431 lines
16 KiB
C#

using UnityEngine;
using Pathfinding;
using AppleHills.Core.Settings;
using Core;
using Core.Lifecycle;
using Core.Settings;
namespace Input
{
/// <summary>
/// Base class for player movement controllers.
/// Handles tap-to-move and hold-to-move input with pathfinding or direct movement.
/// Implements IInteractingCharacter to enable interaction with items.
/// Derived classes can override to add specialized behavior (e.g., shader updates).
/// </summary>
public abstract class BasePlayerMovementController : ManagedBehaviour, ITouchInputConsumer, IInteractingCharacter
{
[Header("Movement")]
[SerializeField] protected float moveSpeed = 5f;
[Header("Collision Simulation")]
[SerializeField] protected LayerMask obstacleMask;
[SerializeField] protected float colliderRadius = 0.5f;
// Movement state
protected Vector3 _targetPosition;
protected Vector3 _directMoveVelocity;
protected bool _isHolding;
protected Vector2 _lastHoldPosition;
protected Coroutine _pathfindingDragCoroutine;
protected float _pathfindingDragUpdateInterval = 0.1f;
// Settings reference (populated by derived classes in LoadSettings)
protected IPlayerMovementSettings _movementSettings;
protected IPlayerMovementSettings Settings => _movementSettings;
// Abstract method for derived classes to load their specific settings
protected abstract void LoadSettings();
// Movement tracking
protected bool _isMoving;
public bool IsMoving => _isMoving;
public bool IsHolding => _isHolding;
public event System.Action OnMovementStarted;
public event System.Action OnMovementStopped;
// IInteractingCharacter implementation - scripted movement for interactions
private Coroutine _moveToCoroutine;
private bool _interruptMoveTo;
public event System.Action OnArrivedAtTarget;
public event System.Action OnMoveToCancelled;
// Components
protected AIPath _aiPath;
protected Animator _animator;
protected Transform _artTransform;
protected SpriteRenderer _spriteRenderer;
// Animation tracking
protected Vector3 _lastDirectMoveDir = Vector3.right;
public Vector3 LastDirectMoveDir => _lastDirectMoveDir;
protected float _lastDirX;
protected float _lastDirY = -1f;
protected LogVerbosity _logVerbosity = LogVerbosity.Warning;
internal override void OnManagedAwake()
{
base.OnManagedAwake();
LoadSettings(); // Let derived class load appropriate settings
InitializeComponents();
}
internal override void OnManagedStart()
{
base.OnManagedStart();
_logVerbosity = DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>().inputLogVerbosity;
}
protected virtual void InitializeComponents()
{
_aiPath = GetComponent<AIPath>();
_artTransform = transform.Find("CharacterArt");
if (_artTransform != null)
_animator = _artTransform.GetComponent<Animator>();
else
_animator = GetComponentInChildren<Animator>();
if (_artTransform != null)
_spriteRenderer = _artTransform.GetComponent<SpriteRenderer>();
if (_spriteRenderer == null)
_spriteRenderer = GetComponentInChildren<SpriteRenderer>();
}
protected virtual void Update()
{
UpdateMovementState();
UpdateAnimation();
}
#region ITouchInputConsumer Implementation
public virtual void OnTap(Vector2 worldPosition)
{
InterruptMoveTo(); // Cancel any scripted movement
Logging.Debug($"[{GetType().Name}] OnTap at {worldPosition}");
if (_aiPath != null)
{
_aiPath.enabled = true;
_aiPath.canMove = true;
_aiPath.isStopped = false;
SetTargetPosition(worldPosition);
_directMoveVelocity = Vector3.zero;
_isHolding = false;
}
}
public virtual void OnHoldStart(Vector2 worldPosition)
{
InterruptMoveTo(); // Cancel any scripted movement
Logging.Debug($"[{GetType().Name}] OnHoldStart at {worldPosition}");
_lastHoldPosition = worldPosition;
_isHolding = true;
if (Settings.DefaultHoldMovementMode == HoldMovementMode.Pathfinding && _aiPath != null)
{
_aiPath.enabled = true;
if (_pathfindingDragCoroutine != null) StopCoroutine(_pathfindingDragCoroutine);
_pathfindingDragCoroutine = StartCoroutine(PathfindingDragUpdateCoroutine());
}
else // Direct movement
{
if (_aiPath != null) _aiPath.enabled = false;
_directMoveVelocity = Vector3.zero;
}
}
public virtual void OnHoldUpdate(Vector2 worldPosition)
{
if (!_isHolding) return;
_lastHoldPosition = worldPosition;
if (Settings.DefaultHoldMovementMode == HoldMovementMode.Direct)
{
if (_aiPath != null && _aiPath.enabled) _aiPath.enabled = false;
MoveDirectlyTo(worldPosition);
}
}
public virtual void OnHoldMove(Vector2 worldPosition)
{
// Alias for OnHoldUpdate for interface compatibility
OnHoldUpdate(worldPosition);
}
public virtual void OnHoldEnd(Vector2 worldPosition)
{
Logging.Debug($"[{GetType().Name}] OnHoldEnd at {worldPosition}");
_isHolding = false;
_directMoveVelocity = Vector3.zero;
if (_aiPath != null && Settings.DefaultHoldMovementMode == HoldMovementMode.Pathfinding)
{
if (_pathfindingDragCoroutine != null)
{
StopCoroutine(_pathfindingDragCoroutine);
_pathfindingDragCoroutine = null;
}
}
if (_aiPath != null && Settings.DefaultHoldMovementMode == HoldMovementMode.Direct)
{
_aiPath.enabled = false;
}
}
#endregion
#region Movement Methods
protected virtual void SetTargetPosition(Vector2 worldPosition)
{
if (_aiPath != null)
{
_aiPath.destination = worldPosition;
_aiPath.maxSpeed = Settings.MoveSpeed;
_aiPath.maxAcceleration = Settings.MaxAcceleration;
_aiPath.canMove = true;
_aiPath.isStopped = false;
}
}
protected virtual void MoveDirectlyTo(Vector2 worldPosition)
{
if (_aiPath == null) return;
Vector3 current = transform.position;
Vector3 target = new Vector3(worldPosition.x, worldPosition.y, current.z);
Vector3 toTarget = (target - current);
Vector3 direction = toTarget.normalized;
float maxSpeed = Settings.MoveSpeed;
float acceleration = Settings.MaxAcceleration;
_directMoveVelocity = Vector3.MoveTowards(_directMoveVelocity, direction * maxSpeed, acceleration * Time.deltaTime);
if (_directMoveVelocity.magnitude > maxSpeed)
{
_directMoveVelocity = _directMoveVelocity.normalized * maxSpeed;
}
Vector3 move = _directMoveVelocity * Time.deltaTime;
if (move.magnitude > toTarget.magnitude)
{
move = toTarget;
}
// Collision simulation
Vector3 adjustedVelocity = AdjustVelocityForObstacles(current, _directMoveVelocity);
Vector3 adjustedMove = adjustedVelocity * Time.deltaTime;
if (adjustedMove.magnitude > toTarget.magnitude)
{
adjustedMove = toTarget;
}
transform.position += adjustedMove;
_lastDirectMoveDir = _directMoveVelocity.normalized;
}
protected virtual Vector3 AdjustVelocityForObstacles(Vector3 position, Vector3 velocity)
{
if (velocity.sqrMagnitude < 0.0001f)
return velocity;
float moveDistance = velocity.magnitude * Time.deltaTime;
Vector2 origin = new Vector2(position.x, position.y);
Vector2 dir = velocity.normalized;
float rayLength = colliderRadius + moveDistance;
RaycastHit2D hit = Physics2D.Raycast(origin, dir, rayLength, obstacleMask);
if (hit.collider != null)
{
Vector2 tangent = new Vector2(-hit.normal.y, hit.normal.x);
float slideAmount = Vector2.Dot(velocity, tangent);
Vector3 slideVelocity = tangent * slideAmount;
return slideVelocity;
}
return velocity;
}
protected virtual System.Collections.IEnumerator PathfindingDragUpdateCoroutine()
{
while (_isHolding && _aiPath != null)
{
SetTargetPosition(_lastHoldPosition);
yield return new WaitForSeconds(_pathfindingDragUpdateInterval);
}
}
#endregion
#region State and Animation
protected virtual void UpdateMovementState()
{
bool isCurrentlyMoving = false;
if (_isHolding && Settings.DefaultHoldMovementMode == HoldMovementMode.Direct)
{
isCurrentlyMoving = _directMoveVelocity.sqrMagnitude > 0.001f;
}
else if (_aiPath != null && _aiPath.enabled)
{
isCurrentlyMoving = _aiPath.velocity.sqrMagnitude > 0.001f;
}
if (isCurrentlyMoving && !_isMoving)
{
_isMoving = true;
OnMovementStarted?.Invoke();
Logging.Debug($"[{GetType().Name}] Movement started");
}
else if (!isCurrentlyMoving && _isMoving)
{
_isMoving = false;
OnMovementStopped?.Invoke();
Logging.Debug($"[{GetType().Name}] Movement stopped");
}
}
protected virtual void UpdateAnimation()
{
if (_animator == null || _aiPath == null) return;
float normalizedSpeed = 0f;
Vector3 velocity = Vector3.zero;
float maxSpeed = Settings.MoveSpeed;
if (_isHolding && Settings.DefaultHoldMovementMode == HoldMovementMode.Direct)
{
normalizedSpeed = _directMoveVelocity.magnitude / maxSpeed;
velocity = _directMoveVelocity;
}
else if (_aiPath.enabled)
{
normalizedSpeed = _aiPath.velocity.magnitude / maxSpeed;
velocity = _aiPath.velocity;
}
_animator.SetFloat("Speed", Mathf.Clamp01(normalizedSpeed));
if (velocity.sqrMagnitude > 0.01f)
{
Vector3 normalizedVelocity = velocity.normalized;
_lastDirX = normalizedVelocity.x;
_lastDirY = normalizedVelocity.y;
_animator.SetFloat("DirX", _lastDirX);
_animator.SetFloat("DirY", _lastDirY);
}
else
{
_animator.SetFloat("DirX", _lastDirX);
_animator.SetFloat("DirY", _lastDirY);
}
}
#endregion
#region IInteractingCharacter Implementation
/// <summary>
/// Controller-driven interaction movement. Base implementation moves this controller to the interactable.
/// Override in derived classes for custom behavior (e.g., PlayerTouchController handles follower dispatch).
/// </summary>
public virtual async System.Threading.Tasks.Task<bool> MoveToInteractableAsync(Interactions.InteractableBase interactable)
{
// Default behavior: move self to interactable position
Vector3 targetPosition = interactable.transform.position;
// Check for custom CharacterMoveToTarget
var moveTargets = interactable.GetComponentsInChildren<Interactions.CharacterMoveToTarget>();
foreach (var target in moveTargets)
{
if (target.characterType == Interactions.CharacterToInteract.Trafalgar ||
target.characterType == Interactions.CharacterToInteract.Both)
{
targetPosition = target.GetTargetPosition();
break;
}
}
// Use MovementUtilities to handle movement
return await Utils.MovementUtilities.MoveToPositionAsync(this, targetPosition);
}
/// <summary>
/// Moves the character to a specific target position and notifies via events when arrived or cancelled.
/// This is used by systems like interactions to orchestrate scripted movement.
/// </summary>
public virtual void MoveToAndNotify(Vector3 target)
{
// Cancel any previous move-to coroutine
if (_moveToCoroutine != null)
{
StopCoroutine(_moveToCoroutine);
}
_interruptMoveTo = false;
// Ensure pathfinding is enabled for MoveToAndNotify
if (_aiPath != null)
{
_aiPath.enabled = true;
_aiPath.canMove = true;
_aiPath.isStopped = false;
}
_moveToCoroutine = StartCoroutine(MoveToTargetCoroutine(target));
}
/// <summary>
/// Cancels any in-progress MoveToAndNotify operation and fires the cancellation event.
/// </summary>
public virtual void InterruptMoveTo()
{
_interruptMoveTo = true;
_isHolding = false;
_directMoveVelocity = Vector3.zero;
if (Settings != null && Settings.DefaultHoldMovementMode == HoldMovementMode.Direct && _aiPath != null)
_aiPath.enabled = false;
OnMoveToCancelled?.Invoke();
}
/// <summary>
/// Coroutine for moving the character to a target position and firing arrival/cancel events.
/// </summary>
protected virtual System.Collections.IEnumerator MoveToTargetCoroutine(Vector3 target)
{
if (_aiPath != null)
{
_aiPath.destination = target;
_aiPath.maxSpeed = Settings.MoveSpeed;
_aiPath.maxAcceleration = Settings.MaxAcceleration;
}
while (!_interruptMoveTo)
{
Vector2 current2D = new Vector2(transform.position.x, transform.position.y);
Vector2 target2D = new Vector2(target.x, target.y);
float dist = Vector2.Distance(current2D, target2D);
if (dist <= Settings.StopDistance + 0.2f)
{
break;
}
yield return null;
}
_moveToCoroutine = null;
if (!_interruptMoveTo)
{
OnArrivedAtTarget?.Invoke();
}
}
#endregion
}
}