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:
2026-08-14 20:28:55 +02:00
co-authored by Claude Sonnet 5
parent 2fd424b2bf
commit a999b0182a
19 changed files with 859 additions and 24 deletions
+54
View File
@@ -0,0 +1,54 @@
using UnityEngine;
namespace AfterHours.Shop
{
/// <summary>
/// Rein kosmetisches, nicht genetzwertes Flug-Visual für ein Stück Ware,
/// das vom Karton ins Regal wandert. Jeder Peer erzeugt seine eigene
/// Instanz (ausgelöst über PlayerCarry.PlayFlightRpc) keine
/// Server-Autorität nötig, der eigentliche Bestand ist längst per
/// NetworkVariable synchron.
/// </summary>
public sealed class FlyingPackage : MonoBehaviour
{
private const float ArcHeight = 0.35f;
private Vector3 _start;
private Vector3 _end;
private Quaternion _startRotation;
private Quaternion _endRotation;
private float _duration;
private float _elapsed;
public static void Spawn(GameObject prefab, Vector3 start, Vector3 end, Quaternion endRotation, float duration)
{
if (prefab == null) return;
var instance = Instantiate(prefab, start, Quaternion.identity);
var flight = instance.AddComponent<FlyingPackage>();
flight.Init(start, end, endRotation, duration);
}
private void Init(Vector3 start, Vector3 end, Quaternion endRotation, float duration)
{
_start = start;
_end = end;
_startRotation = transform.rotation;
_endRotation = endRotation;
_duration = Mathf.Max(0.05f, duration);
}
private void Update()
{
_elapsed += Time.deltaTime;
float t = Mathf.Clamp01(_elapsed / _duration);
Vector3 flat = Vector3.Lerp(_start, _end, t);
float arc = ArcHeight * Mathf.Sin(t * Mathf.PI);
transform.position = flat + Vector3.up * arc;
transform.rotation = Quaternion.Slerp(_startRotation, _endRotation, t);
if (t >= 1f) Destroy(gameObject);
}
}
}