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; } } }