Files
SixShopSimulator/Assets/Scripts/Networking/PlayerCarry.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
5.2 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.Interaction;
using AfterHours.Shop;
using Unity.Netcode;
using UnityEngine;
namespace AfterHours.Networking
{
/// <summary>
/// Was der Spieler gerade als physisches Objekt trägt (z. B. ein
/// Lieferkarton) sowie das serverseitig getaktete Ausschütten daraus in
/// ein IPourTarget. Getrennt von PlayerHands (abstrakte Stückzahl)
/// beide sind gegenseitig exklusiv, siehe TryPickUp/StorageCrate.Interact.
/// </summary>
public sealed class PlayerCarry : NetworkBehaviour
{
[SerializeField] private Transform _handSocket;
[SerializeField] private float _pourInterval = 0.4f;
[SerializeField] private float _maxPourReach = 2.5f;
[SerializeField] private float _flightDuration = 0.35f;
private readonly NetworkVariable<bool> _isCarrying = new(
false,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server
);
private readonly NetworkVariable<NetworkObjectReference> _carriedObject = new(
default,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server
);
public Transform HandSocket => _handSocket;
public bool IsCarrying => _isCarrying.Value;
private IPourTarget _pourTarget;
private IPourSource _pourSource;
private NetworkObjectReference _pourTargetRef;
private float _pourTimer;
/// <summary>Server: versucht, <paramref name="target"/> aufzuheben.</summary>
public bool TryPickUp(Carryable target)
{
if (!IsServer || _isCarrying.Value || target == null) return false;
if (TryGetComponent<PlayerHands>(out var hands) && !hands.IsEmpty) return false;
if (!target.TryPickUp(NetworkObject)) return false;
_carriedObject.Value = target.NetworkObject;
_isCarrying.Value = true;
return true;
}
/// <summary>Server: legt das getragene Objekt vor dem Spieler ab.</summary>
[Rpc(SendTo.Server)]
public void RequestDropRpc()
{
if (!_isCarrying.Value) return;
StopPour();
if (_carriedObject.Value.TryGet(out var carried) && carried.TryGetComponent<Carryable>(out var carryable))
{
Vector3 dropPosition = transform.position + transform.forward * 1f;
carryable.Drop(dropPosition, transform.rotation);
}
_isCarrying.Value = false;
_carriedObject.Value = default;
}
/// <summary>Server: startet das Ausschütten in <paramref name="target"/>, falls möglich.</summary>
public bool BeginPour(IPourTarget target)
{
if (!IsServer || target == null) return false;
if (!_isCarrying.Value || !_carriedObject.Value.TryGet(out var carried)) return false;
if (!carried.TryGetComponent<IPourSource>(out var source)) return false;
if (!source.IsOpen || source.IsEmpty || target.IsFull) return false;
if (!target.IsEmpty && target.ProductId != source.ProductId) return false;
if (!((Component)target).TryGetComponent<NetworkObject>(out var targetNo)) return false;
_pourSource = source;
_pourTarget = target;
_pourTargetRef = targetNo;
_pourTimer = 0f; // erste Einheit sofort, danach im Takt von _pourInterval
return true;
}
public void StopPour()
{
_pourSource = null;
_pourTarget = null;
}
private void Update()
{
if (!IsServer || _pourSource == null || _pourTarget == null) return;
if (!_isCarrying.Value || _pourSource.IsEmpty || _pourTarget.IsFull)
{
StopPour();
return;
}
if (Vector3.Distance(transform.position, _pourTarget.Transform.position) > _maxPourReach)
{
StopPour();
return;
}
_pourTimer -= Time.deltaTime;
if (_pourTimer > 0f) return;
_pourTimer = _pourInterval;
int landingIndex = _pourTarget.Quantity;
int productId = _pourSource.ProductId;
if (_pourSource.TryTakeOne() && _pourTarget.TryAddOne(productId))
PlayFlightRpc(_carriedObject.Value, _pourTargetRef, landingIndex);
}
/// <summary>Läuft auf jedem Peer: rein kosmetisches Flug-Visual für die soeben transferierte Einheit.</summary>
[Rpc(SendTo.Everyone)]
private void PlayFlightRpc(NetworkObjectReference sourceRef, NetworkObjectReference targetRef, int landingIndex)
{
if (!sourceRef.TryGet(out var sourceObj) || !sourceObj.TryGetComponent<Lieferkarton>(out var box)) return;
if (!targetRef.TryGet(out var targetObj) || !targetObj.TryGetComponent<ShelfSlot>(out var shelf)) return;
GameObject prefab = box.GetPackagePrefab();
Vector3 start = box.transform.position + Vector3.up * 0.3f;
Vector3 end = shelf.GetSlotWorldPosition(landingIndex);
FlyingPackage.Spawn(prefab, start, end, shelf.transform.rotation, _flightDuration);
}
}
}