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>
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6ed618ff659f6684f8c2ce58ba032c35
|
||||
Reference in New Issue
Block a user