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:
@@ -10,7 +10,7 @@ namespace AfterHours.Shop
|
||||
/// Ein Regalfach: hält genau eine Produktsorte. Auffüllen geht über
|
||||
/// Interact(), Entnahme durch Kunden serverseitig über TakeOne().
|
||||
/// </summary>
|
||||
public sealed class ShelfSlot : NetworkBehaviour, IInteractable
|
||||
public sealed class ShelfSlot : NetworkBehaviour, IInteractable, IPourTarget
|
||||
{
|
||||
[SerializeField] private ProductCatalog _catalog;
|
||||
[SerializeField] private int _capacityUnits = 12;
|
||||
@@ -20,6 +20,11 @@ namespace AfterHours.Shop
|
||||
|
||||
[SerializeField] private Transform _displayRoot;
|
||||
|
||||
[Header("Anzeige-Raster (sauber in Reihen eingeräumt)")]
|
||||
[SerializeField] private int _displayColumns = 4;
|
||||
[SerializeField] private int _displayRows = 3;
|
||||
[SerializeField] private Vector3 _displaySpacing = new(0.1f, 0.1f, 0.1f);
|
||||
|
||||
private readonly NetworkVariable<int> _productId = new(-1);
|
||||
private readonly NetworkVariable<int> _quantity = new(0);
|
||||
private readonly NetworkVariable<float> _price = new(0f);
|
||||
@@ -29,6 +34,20 @@ namespace AfterHours.Shop
|
||||
public float Price => _price.Value;
|
||||
public bool ExposedToStreet => _exposedToStreet;
|
||||
public bool IsEmpty => _quantity.Value <= 0;
|
||||
public bool IsFull => _quantity.Value >= Capacity();
|
||||
|
||||
// Explizite Implementierung, weil "transform" (Component) den Namen schon belegt.
|
||||
Transform IPourTarget.Transform => transform;
|
||||
|
||||
private PackageDisplay _packageDisplay;
|
||||
|
||||
private void Awake() => _packageDisplay = new PackageDisplay(_displayRoot, _displayColumns, _displayRows, _displaySpacing);
|
||||
|
||||
/// <summary>Weltposition des Rasterplatzes mit gegebenem Index – Ziel fürs Flug-Visual (siehe PlayerCarry).</summary>
|
||||
public Vector3 GetSlotWorldPosition(int index)
|
||||
{
|
||||
return _displayRoot != null ? _displayRoot.TransformPoint(_packageDisplay.SlotLocalPosition(index)) : transform.position;
|
||||
}
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
@@ -99,6 +118,24 @@ namespace AfterHours.Shop
|
||||
Debug.Log($"[ShelfSlot] Eingeräumt: {moved} Stück, jetzt {_quantity.Value}/{Capacity()}.");
|
||||
}
|
||||
|
||||
/// <summary>Server: ein Stück aus einem IPourSource einräumen (siehe PlayerCarry).</summary>
|
||||
public bool TryAddOne(int productId)
|
||||
{
|
||||
if (!IsServer) return false;
|
||||
if (!IsEmpty && _productId.Value != productId) return false;
|
||||
if (_quantity.Value >= Capacity()) return false;
|
||||
|
||||
if (IsEmpty)
|
||||
{
|
||||
_productId.Value = productId;
|
||||
var def = _catalog.Get(productId);
|
||||
if (def != null && _price.Value <= 0f) _price.Value = def.RecommendedPrice;
|
||||
}
|
||||
|
||||
_quantity.Value++;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Server: Kunde entnimmt ein Stück.</summary>
|
||||
public bool TakeOne()
|
||||
{
|
||||
@@ -126,8 +163,9 @@ namespace AfterHours.Shop
|
||||
|
||||
private void RefreshVisuals()
|
||||
{
|
||||
// TODO: Verpackungs-Prefabs nach Menge ein-/ausblenden (Object Pool).
|
||||
// Läuft auf allen Peers, rein kosmetisch.
|
||||
var product = _catalog != null ? _catalog.Get(_productId.Value) : null;
|
||||
GameObject prefab = product != null ? product.PackagePrefab : null;
|
||||
_packageDisplay.SetTarget(prefab, _quantity.Value, Capacity());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user