Files
SixShopSimulator/Assets/Scripts/Interaction/PlayerInteractor.cs
T
svenm_adminandClaude Sonnet 5 a999b0182a feat(shop): add box pickup/carry, pour-into-shelf, and package flight visuals
- Carryable: generic pickup/carry component, cosmetic per-peer follow of
  the carrier's HandSocket instead of NGO reparenting (camera pitch isn't
  networked, so root-level reparenting wouldn't tilt with the view)
- PlayerCarry: mutual exclusion with PlayerHands, server-timed hold-to-pour
  tick (client only signals start/stop, never per-unit requests)
- IPourSource/IPourTarget: decouple PlayerInteractor from concrete Shop
  types, same pattern as IInteractable
- Lieferkarton: gains product data (productId/quantity), two-stage
  Interact() (open, then pick up), IPourSource
- ShelfSlot: implements IPourTarget, fills the previous RefreshVisuals TODO
- PackageDisplay: renders quantity as a proper column x row grid that
  stacks into further layers as space runs out, reused by both containers
- FlyingPackage: small non-networked cosmetic visual that arcs a package
  from the carried box to its exact landing slot in the shelf grid on
  every successful pour tick, broadcast via Rpc(SendTo.Everyone)
- StorageCrate: rejects pickup while the player is carrying a box

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 20:28:55 +02:00

135 lines
4.7 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using AfterHours.Networking;
using Unity.Netcode;
using UnityEngine;
namespace AfterHours.Interaction
{
/// <summary>
/// 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).
/// </summary>
[RequireComponent(typeof(NetworkObject))]
public sealed class PlayerInteractor : NetworkBehaviour
{
[SerializeField] private Transform _eyes;
[SerializeField] private float _reach = 2.5f;
[SerializeField] private LayerMask _interactableMask = ~0;
/// <summary>Toleranz, weil Client und Server nie exakt denselben Frame sehen.</summary>
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<NetworkObject>();
if (target != null) RequestInteractRpc(target);
}
else if (TryGetComponent<PlayerCarry>(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<IInteractable>(out var interactable))
{
_current = interactable;
CurrentPrompt = interactable.GetPrompt(OwnerClientId);
}
hit.collider.TryGetComponent<IPourTarget>(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.");
}
}
/// <summary>Maustaste halten, um ein IPourTarget im Blick Stück für Stück zu befüllen.</summary>
private void HandlePourInput()
{
if (Input.GetMouseButtonDown(0) && _currentPourTarget != null)
{
var targetNo = ((MonoBehaviour)_currentPourTarget).GetComponent<NetworkObject>();
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<IInteractable>(out var interactable))
interactable.Interact(NetworkObject);
}
[Rpc(SendTo.Server)]
private void RequestPourStartRpc(NetworkObjectReference targetRef)
{
if (!targetRef.TryGet(out var target) || !target.TryGetComponent<IPourTarget>(out var pourTarget)) return;
if (!IsInReach(target.transform.position)) return;
if (TryGetComponent<PlayerCarry>(out var carry)) carry.BeginPour(pourTarget);
}
[Rpc(SendTo.Server)]
private void RequestPourStopRpc()
{
if (TryGetComponent<PlayerCarry>(out var carry)) carry.StopPour();
}
/// <summary>Server-Validierung: ist der Spieler überhaupt in Reichweite?</summary>
private bool IsInReach(Vector3 point)
{
float distance = Vector3.Distance(transform.position, point);
return distance <= _reach + ServerReachTolerance;
}
}
}