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
+1
View File
@@ -48,6 +48,7 @@ namespace AfterHours.Shop
{
if (!IsServer || IsEmpty) return;
if (!actor.TryGetComponent<PlayerHands>(out var hands)) return;
if (actor.TryGetComponent<PlayerCarry>(out var carry) && carry.IsCarrying) return;
int want = Mathf.Min(_quantity.Value, PlayerHands.MaxCarry);
int taken = hands.TryTake(_productId, want);