using Unity.Netcode;
using Unity.Netcode.Components;
using UnityEngine;
using UnityEngine.InputSystem;
namespace Northbound
{
///
/// 플레이어가 월드의 오브젝트와 상호작용하는 시스템
///
public class PlayerInteraction : NetworkBehaviour
{
[Header("Interaction Settings")]
public float interactionRange = 3f;
public LayerMask interactableLayer = ~0;
public float workPower = 10f;
[Header("Detection")]
public Transform rayOrigin;
public bool useForwardDirection = true;
[Header("Animation")]
public bool playAnimations = true;
public bool useAnimationEvents = true;
public bool blockDuringAnimation = true;
[Header("Equipment")]
public bool useEquipment = true;
[Header("Debug")]
public bool showDebugRay = true;
[Header("Worker")]
public Worker assignedWorker;
private PlayerInputActions _inputActions;
private IInteractable _currentInteractable;
private IInteractable _unavailableInteractable;
private Camera _mainCamera;
private Animator _animator;
private NetworkAnimator _networkAnimator;
private EquipmentSocket _equipmentSocket;
private EquipmentData _pendingEquipmentData;
private string _currentEquipmentSocket;
private bool _isInteracting = false;
private Coroutine _interactionTimeoutCoroutine;
private NetworkPlayerController _networkPlayerController;
public bool IsInteracting => _isInteracting;
public float WorkPower => workPower;
public IInteractable CurrentUnavailableInteractable => _unavailableInteractable;
// 로컬 플레이어인지 확인
private bool IsLocalPlayer => _networkPlayerController != null && _networkPlayerController.IsLocalPlayer;
private ulong LocalPlayerId => _networkPlayerController != null ? _networkPlayerController.OwnerPlayerId : OwnerClientId;
private void Awake()
{
_networkPlayerController = GetComponent();
_networkAnimator = GetComponent();
}
public override void OnNetworkSpawn()
{
_animator = GetComponent();
_equipmentSocket = GetComponent();
if (rayOrigin == null)
rayOrigin = transform;
// _ownerPlayerId 변경 이벤트 구독
if (_networkPlayerController != null)
{
_networkPlayerController.OnOwnerChanged += OnOwnerPlayerIdChanged;
}
// 이미 로컬 플레이어면 입력 초기화
TryInitializeInput();
}
private void OnOwnerPlayerIdChanged(ulong newOwnerId)
{
TryInitializeInput();
}
private void TryInitializeInput()
{
if (!IsLocalPlayer) return;
if (_inputActions != null) return;
_mainCamera = Camera.main;
_inputActions = new PlayerInputActions();
_inputActions.Player.Interact.performed += OnInteract;
_inputActions.Enable();
}
public override void OnNetworkDespawn()
{
if (_networkPlayerController != null)
{
_networkPlayerController.OnOwnerChanged -= OnOwnerPlayerIdChanged;
}
if (_inputActions != null)
{
_inputActions.Player.Interact.performed -= OnInteract;
_inputActions.Disable();
_inputActions.Dispose();
}
}
private void Update()
{
if (!IsLocalPlayer) return;
if (_isInteracting && _currentInteractable != null)
{
if (!_currentInteractable.CanInteract(LocalPlayerId))
{
EndInteraction();
}
}
DetectInteractable();
}
private void DetectInteractable()
{
// 카메라가 없으면 다시 시도
if (_mainCamera == null)
{
_mainCamera = Camera.main;
if (_mainCamera == null) return;
}
Vector3 origin = rayOrigin.position;
Vector3 direction = useForwardDirection ? transform.forward : _mainCamera.transform.forward;
Ray ray = new Ray(origin, direction);
if (showDebugRay)
{
Debug.DrawRay(ray.origin, ray.direction * interactionRange,
_currentInteractable != null ? Color.green : Color.yellow);
}
if (Physics.Raycast(ray, out RaycastHit hit, interactionRange, interactableLayer))
{
IInteractable interactable = hit.collider.GetComponent();
if (interactable == null)
interactable = hit.collider.GetComponentInParent();
if (interactable != null)
{
if (interactable.CanInteract(LocalPlayerId))
{
_currentInteractable = interactable;
_unavailableInteractable = null;
return;
}
else
{
_currentInteractable = null;
_unavailableInteractable = interactable;
return;
}
}
}
_currentInteractable = null;
_unavailableInteractable = null;
}
private void OnInteract(InputAction.CallbackContext context)
{
if (blockDuringAnimation && _isInteracting)
return;
if (_currentInteractable != null)
{
// 상호작용 가능 여부 다시 확인 (NetworkVariable 동기화 지연 대응)
if (!_currentInteractable.CanInteract(LocalPlayerId))
{
return;
}
bool isResource = _currentInteractable is Resource;
bool hasWorker = assignedWorker != null && assignedWorker.OwnerPlayerId == LocalPlayerId;
bool isWorkerFollowing = hasWorker && (int)assignedWorker.CurrentState == 1;
bool shouldPlayAnimation = !isResource || !hasWorker || !isWorkerFollowing;
_isInteracting = true;
_pendingEquipmentData = _currentInteractable.GetEquipmentData();
string animTrigger = _currentInteractable.GetInteractionAnimation();
bool hasAnimation = !string.IsNullOrEmpty(animTrigger);
if (playAnimations && hasAnimation && shouldPlayAnimation)
{
// 서버에서 애니메이션 실행 (동기화를 위해)
PlayAnimationServerRpc(animTrigger);
_interactionTimeoutCoroutine = StartCoroutine(InteractionTimeout(3f));
}
else
{
_isInteracting = false;
}
if (shouldPlayAnimation && !useAnimationEvents && useEquipment && _equipmentSocket != null && _pendingEquipmentData != null)
{
if (_pendingEquipmentData.attachOnStart && _pendingEquipmentData.equipmentPrefab != null)
{
AttachEquipment();
if (_pendingEquipmentData.detachOnEnd)
{
StartCoroutine(DetachEquipmentAfterDelay(2f));
}
}
}
_currentInteractable.Interact(LocalPlayerId);
}
}
[Rpc(SendTo.Server)]
private void PlayAnimationServerRpc(string animTrigger)
{
if (_networkAnimator != null && !string.IsNullOrEmpty(animTrigger))
{
_networkAnimator.SetTrigger(animTrigger);
}
}
public void OnEquipTool()
{
if (!useAnimationEvents || !useEquipment) return;
AttachEquipment();
}
public void OnEquipTool(string socketName)
{
if (!useAnimationEvents || !useEquipment) return;
AttachEquipment(socketName);
}
public void OnUnequipTool()
{
if (!useAnimationEvents || !useEquipment) return;
DetachEquipment();
}
public void OnUnequipTool(string socketName)
{
if (!useAnimationEvents || !useEquipment) return;
DetachEquipment(socketName);
}
public void OnInteractionComplete()
{
if (!IsLocalPlayer) return;
if (_interactionTimeoutCoroutine != null)
{
StopCoroutine(_interactionTimeoutCoroutine);
_interactionTimeoutCoroutine = null;
}
_isInteracting = false;
}
private void AttachEquipment(string socketName = null)
{
if (_equipmentSocket == null || _pendingEquipmentData == null)
return;
if (_pendingEquipmentData.equipmentPrefab == null)
return;
string socket = socketName ?? _pendingEquipmentData.socketName;
_equipmentSocket.AttachToSocket(socket, _pendingEquipmentData.equipmentPrefab);
_currentEquipmentSocket = socket;
}
private void DetachEquipment(string socketName = null)
{
if (_equipmentSocket == null)
return;
string socket = socketName ?? _currentEquipmentSocket;
if (!string.IsNullOrEmpty(socket))
{
_equipmentSocket.DetachFromSocket(socket);
if (socket == _currentEquipmentSocket)
_currentEquipmentSocket = null;
}
}
private System.Collections.IEnumerator DetachEquipmentAfterDelay(float delay)
{
yield return new WaitForSeconds(delay);
DetachEquipment();
if (!useAnimationEvents)
{
_isInteracting = false;
}
}
private System.Collections.IEnumerator InteractionTimeout(float timeout)
{
yield return new WaitForSeconds(timeout);
if (_isInteracting)
{
_isInteracting = false;
_interactionTimeoutCoroutine = null;
}
}
private void EndInteraction()
{
_isInteracting = false;
if (_interactionTimeoutCoroutine != null)
{
StopCoroutine(_interactionTimeoutCoroutine);
_interactionTimeoutCoroutine = null;
}
}
private void OnGUI()
{
if (!IsLocalPlayer) return;
// 디버그: _mainCamera 상태 확인
if (_mainCamera == null)
{
_mainCamera = Camera.main;
}
GUIStyle style = new GUIStyle(GUI.skin.label)
{
fontSize = 36,
alignment = TextAnchor.MiddleCenter
};
style.normal.textColor = Color.black;
string prompt = "";
if (_currentInteractable != null)
{
prompt = _currentInteractable.GetInteractionPrompt();
bool isResource = _currentInteractable is Resource;
if (isResource && assignedWorker != null && assignedWorker.OwnerPlayerId == LocalPlayerId)
{
prompt += " (워커 할당 가능)";
}
if (_isInteracting)
{
prompt += " (진행 중...)";
style.normal.textColor = Color.red;
}
}
if (assignedWorker != null && assignedWorker.OwnerPlayerId == LocalPlayerId)
{
string workerStatus = assignedWorker.CurrentState switch
{
WorkerState.Following => "따라가는 중",
WorkerState.Mining => "채굴 중",
WorkerState.Returning => "반환 중",
_ => "대기 중"
};
GUI.Label(new Rect(Screen.width / 2 - 200, Screen.height - 150, 800, 100),
$"워커: {workerStatus} (가방: {assignedWorker.CurrentBagResources}/{assignedWorker.maxBagCapacity})",
style);
}
else
{
GUI.Label(new Rect(Screen.width / 2 - 200, Screen.height - 150, 800, 100),
$"워커: 미할당",
style);
}
if (_currentInteractable != null)
{
GUI.Label(new Rect(Screen.width / 2 - 200, Screen.height - 100, 800, 100), prompt, style);
}
}
public override void OnDestroy()
{
if (_inputActions != null)
{
_inputActions.Dispose();
}
}
}
}