Files
TopDown/Assets/Scripts/PlayerController.cs
2026-08-05 08:18:15 +01:00

290 lines
10 KiB
C#

using UnityEngine;
using UnityEngine.AI;
using UnityEngine.InputSystem;
[RequireComponent(typeof(CharacterController))]
[RequireComponent(typeof(CombatController))]
public class PlayerController : MonoBehaviour
{
[Header("Movement Settings")]
[SerializeField] private float moveSpeed = 6.0f;
[SerializeField] private float rotationSpeed = 12.0f;
[SerializeField] private float stoppingDistance = 0.2f;
[SerializeField] private LayerMask groundLayer = ~0; // Default to all layers
[Header("Interaction & Combat Settings")]
[SerializeField] private LayerMask interactableLayer = ~0;
[SerializeField] private float defaultInteractionRadius = 2.0f;
[Tooltip("If true, the player automatically turns to attack any enemy that damages them (RuneScape style).")]
[SerializeField] private bool autoRetaliate = true;
[Header("Visual Feedback Prefabs (Optional)")]
[SerializeField] private GameObject moveClickEffectPrefab;
[SerializeField] private GameObject interactClickEffectPrefab;
private Camera mainCamera;
private CharacterController characterController;
private NavMeshAgent navAgent;
private CombatController combatController;
private Vector3 targetPosition;
private IInteractable targetInteractable;
private bool isMovingToInteractable = false;
private bool isMoving = false;
private void Awake()
{
mainCamera = Camera.main;
characterController = GetComponent<CharacterController>();
navAgent = GetComponent<NavMeshAgent>();
combatController = GetComponent<CombatController>();
targetPosition = transform.position;
if (combatController != null && combatController.Stats != null)
{
combatController.Stats.onHealthChanged.AddListener(OnHealthChanged);
}
}
private void Update()
{
HandleInput();
ProcessMovement();
ProcessInteractionCheck();
ProcessCombatCheck();
}
private void HandleInput()
{
// Using New Unity Input System (UnityEngine.InputSystem)
Mouse currentMouse = Mouse.current;
if (currentMouse == null) return;
Vector2 screenMousePos = currentMouse.position.ReadValue();
// Left Click / Hold: Click or drag to move continuously
if (currentMouse.leftButton.isPressed)
{
bool isFirstPress = currentMouse.leftButton.wasPressedThisFrame;
HandleLeftClickOrDrag(screenMousePos, isFirstPress);
}
// Right Click: Interact with object or attack enemy
if (currentMouse.rightButton.wasPressedThisFrame)
{
HandleRightClick(screenMousePos);
}
}
private void HandleLeftClickOrDrag(Vector2 screenPosition, bool isFirstPress)
{
Ray ray = mainCamera.ScreenPointToRay(screenPosition);
if (Physics.Raycast(ray, out RaycastHit hit, 100f, groundLayer))
{
SetDestination(hit.point);
ClearTargetInteractable();
combatController.ClearTarget();
// Only spawn click effect on the initial click frame (not every frame while dragging)
if (isFirstPress)
{
SpawnEffect(moveClickEffectPrefab, hit.point);
}
}
}
private void HandleRightClick(Vector2 screenPosition)
{
Ray ray = mainCamera.ScreenPointToRay(screenPosition);
// Check for Raycast hit on Interactable or Enemy
if (Physics.Raycast(ray, out RaycastHit hit, 100f, interactableLayer))
{
// Check if object is an Enemy / CharacterStats for Combat
CharacterStats enemyStats = hit.collider.GetComponent<CharacterStats>() ?? hit.collider.GetComponentInParent<CharacterStats>() ?? hit.collider.GetComponentInChildren<CharacterStats>();
if (enemyStats != null && enemyStats != combatController.Stats && !enemyStats.IsDead)
{
ClearTargetInteractable();
combatController.SetTarget(enemyStats);
SetDestination(enemyStats.transform.position);
SpawnEffect(interactClickEffectPrefab, hit.point);
return;
}
// Check if object is an IInteractable
IInteractable interactable = hit.collider.GetComponent<IInteractable>() ?? hit.collider.GetComponentInParent<IInteractable>() ?? hit.collider.GetComponentInChildren<IInteractable>();
if (interactable != null)
{
combatController.ClearTarget();
targetInteractable = interactable;
isMovingToInteractable = true;
SetDestination(interactable.GetTransform().position);
SpawnEffect(interactClickEffectPrefab, hit.point);
return;
}
}
// Right-clicked ground with no interactable/enemy
if (Physics.Raycast(ray, out RaycastHit groundHit, 100f, groundLayer))
{
ClearTargetInteractable();
combatController.ClearTarget();
}
}
private void ProcessCombatCheck()
{
if (combatController.CurrentTarget == null) return;
if (combatController.CurrentTarget.IsDead)
{
combatController.ClearTarget();
return;
}
// Update destination to track moving target
targetPosition = combatController.CurrentTarget.transform.position;
if (navAgent != null && navAgent.enabled)
{
navAgent.stoppingDistance = combatController.Stats.AttackRange * 0.9f;
navAgent.SetDestination(targetPosition);
}
// Check if inside attack range
if (combatController.IsInAttackRange(combatController.CurrentTarget))
{
isMoving = false;
if (navAgent != null && navAgent.enabled && navAgent.hasPath)
{
navAgent.ResetPath();
}
combatController.TryExecuteAttack();
}
}
private void OnHealthChanged(float currentHealth, float maxHealth)
{
if (!autoRetaliate || combatController.CurrentTarget != null) return;
// Scan nearby hostile objects / enemies that are attacking the player
Collider[] hits = Physics.OverlapSphere(transform.position, 10.0f, interactableLayer);
foreach (var hit in hits)
{
CharacterStats attackerStats = hit.GetComponent<CharacterStats>() ?? hit.GetComponentInParent<CharacterStats>() ?? hit.GetComponentInChildren<CharacterStats>();
if (attackerStats != null && attackerStats != combatController.Stats && !attackerStats.IsDead)
{
combatController.SetTarget(attackerStats);
SetDestination(attackerStats.transform.position);
Debug.Log($"[PlayerController] Auto-retaliating against '{attackerStats.CharacterName}'!");
break;
}
}
}
private void SetDestination(Vector3 destination)
{
targetPosition = destination;
isMoving = true;
if (navAgent != null && navAgent.enabled)
{
navAgent.SetDestination(destination);
}
}
private void ProcessMovement()
{
if (!isMoving) return;
// If using NavMeshAgent
if (navAgent != null && navAgent.enabled)
{
float targetStoppingDistance = isMovingToInteractable && targetInteractable != null
? (targetInteractable.GetInteractionRadius() > 0 ? targetInteractable.GetInteractionRadius() : defaultInteractionRadius) * 0.9f
: stoppingDistance;
navAgent.stoppingDistance = targetStoppingDistance;
if (!navAgent.pathPending && navAgent.remainingDistance <= navAgent.stoppingDistance)
{
if (!navAgent.hasPath || navAgent.velocity.sqrMagnitude == 0f)
{
isMoving = false;
}
}
return;
}
// Fallback: Manual Transform/CharacterController movement
Vector3 direction = (targetPosition - transform.position);
direction.y = 0; // Keep movement top-down flat
float distance = direction.magnitude;
float currentStoppingDistance = isMovingToInteractable && targetInteractable != null
? (targetInteractable.GetInteractionRadius() > 0 ? targetInteractable.GetInteractionRadius() : defaultInteractionRadius) * 0.9f
: stoppingDistance;
if (distance > currentStoppingDistance)
{
// Smooth Rotation
Quaternion targetRotation = Quaternion.LookRotation(direction);
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotationSpeed * Time.deltaTime);
// Move
Vector3 moveVelocity = direction.normalized * moveSpeed * Time.deltaTime;
characterController.Move(moveVelocity);
}
else
{
isMoving = false;
}
}
private void ProcessInteractionCheck()
{
if (!isMovingToInteractable || targetInteractable == null) return;
Vector3 flatPlayerPos = new Vector3(transform.position.x, 0, transform.position.z);
Vector3 flatTargetPos = new Vector3(targetInteractable.GetTransform().position.x, 0, targetInteractable.GetTransform().position.z);
float distance = Vector3.Distance(flatPlayerPos, flatTargetPos);
float radius = targetInteractable.GetInteractionRadius() > 0 ? targetInteractable.GetInteractionRadius() : defaultInteractionRadius;
if (distance <= radius + 0.1f)
{
// Face target interactable
Vector3 dir = (targetInteractable.GetTransform().position - transform.position);
dir.y = 0;
if (dir != Vector3.zero)
{
transform.rotation = Quaternion.LookRotation(dir);
}
// Trigger interaction
targetInteractable.Interact(gameObject);
ClearTargetInteractable();
}
}
private void ClearTargetInteractable()
{
targetInteractable = null;
isMovingToInteractable = false;
}
private void SpawnEffect(GameObject effectPrefab, Vector3 position)
{
if (effectPrefab != null)
{
Instantiate(effectPrefab, position + Vector3.up * 0.05f, Quaternion.identity);
}
}
private void OnDrawGizmosSelected()
{
Gizmos.color = Color.green;
Gizmos.DrawWireSphere(targetPosition, 0.3f);
Gizmos.DrawLine(transform.position, targetPosition);
}
}