using AfterHours.Networking;
using Unity.Netcode;
using UnityEngine;
namespace AfterHours.Interaction
{
///
/// Raycast auf dem Owner-Client für den Prompt, Ausführung auf dem Server.
/// Der Server vertraut dem Client die Zielauswahl NICHT blind – er prüft
/// Distanz und Sichtbarkeit erneut (siehe CLAUDE.md §3).
///
[RequireComponent(typeof(NetworkObject))]
public sealed class PlayerInteractor : NetworkBehaviour
{
[SerializeField] private Transform _eyes;
[SerializeField] private float _reach = 2.5f;
[SerializeField] private LayerMask _interactableMask = ~0;
/// Toleranz, weil Client und Server nie exakt denselben Frame sehen.
private const float ServerReachTolerance = 1.5f;
private IInteractable _current;
private IPourTarget _currentPourTarget;
private IPourTarget _pouringTarget;
public string CurrentPrompt { get; private set; }
private void Update()
{
if (!IsOwner) return;
RefreshTarget();
if (Input.GetKeyDown(KeyCode.E))
{
if (_current != null)
{
var target = ((MonoBehaviour)_current).GetComponent();
if (target != null) RequestInteractRpc(target);
}
else if (TryGetComponent(out var carry) && carry.IsCarrying)
{
carry.RequestDropRpc();
}
}
HandlePourInput();
}
private void RefreshTarget()
{
var previous = _current;
_current = null;
_currentPourTarget = null;
CurrentPrompt = null;
if (Physics.Raycast(_eyes.position, _eyes.forward, out var hit, _reach, _interactableMask))
{
if (hit.collider.TryGetComponent(out var interactable))
{
_current = interactable;
CurrentPrompt = interactable.GetPrompt(OwnerClientId);
}
hit.collider.TryGetComponent(out _currentPourTarget);
}
// TODO: entfernen, sobald ein echter Prompt im UI angezeigt wird.
if (_current != previous)
{
Debug.Log(_current != null
? $"[PlayerInteractor] Ziel im Blick: {CurrentPrompt}"
: "[PlayerInteractor] Kein Ziel mehr im Blick.");
}
}
/// Maustaste halten, um ein IPourTarget im Blick Stück für Stück zu befüllen.
private void HandlePourInput()
{
if (Input.GetMouseButtonDown(0) && _currentPourTarget != null)
{
var targetNo = ((MonoBehaviour)_currentPourTarget).GetComponent();
if (targetNo != null)
{
_pouringTarget = _currentPourTarget;
RequestPourStartRpc(targetNo);
}
}
if (_pouringTarget != null && (Input.GetMouseButtonUp(0) || _currentPourTarget != _pouringTarget))
{
_pouringTarget = null;
RequestPourStopRpc();
}
}
[Rpc(SendTo.Server)]
private void RequestInteractRpc(NetworkObjectReference targetRef)
{
if (!targetRef.TryGet(out var target)) return;
// Server-Validierung: ist der Spieler überhaupt in Reichweite?
if (!IsInReach(target.transform.position))
{
Debug.LogWarning($"[PlayerInteractor] Client {OwnerClientId} zu weit entfernt.");
return;
}
if (target.TryGetComponent(out var interactable))
interactable.Interact(NetworkObject);
}
[Rpc(SendTo.Server)]
private void RequestPourStartRpc(NetworkObjectReference targetRef)
{
if (!targetRef.TryGet(out var target) || !target.TryGetComponent(out var pourTarget)) return;
if (!IsInReach(target.transform.position)) return;
if (TryGetComponent(out var carry)) carry.BeginPour(pourTarget);
}
[Rpc(SendTo.Server)]
private void RequestPourStopRpc()
{
if (TryGetComponent(out var carry)) carry.StopPour();
}
/// Server-Validierung: ist der Spieler überhaupt in Reichweite?
private bool IsInReach(Vector3 point)
{
float distance = Vector3.Distance(transform.position, point);
return distance <= _reach + ServerReachTolerance;
}
}
}