Add _displayRotationEuler to Lieferkarton/ShelfSlot so package prefabs with a non-upright export orientation can be corrected per-container without touching PackageDisplay code. Fixes a bug where flying packages landed with a different rotation than the resting shelf display (PlayerCarry used the raw shelf transform instead of ShelfSlot.GetSlotWorldRotation()). Also adds the pocoregal shelf model (Blender export) and its materials, places it in the Shop scene, registers ShelfSlot as a network prefab, and rescales EmptyBox. See docs/PACKAGE_DISPLAY_SETUP.md for the display-grid setup/troubleshooting reference. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
135 lines
5.2 KiB
C#
135 lines
5.2 KiB
C#
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.GetSlotWorldRotation(), _flightDuration);
|
||
}
|
||
}
|
||
}
|