Files
SixShopSimulator/Assets/Scripts/Shop/ShelfSlot.cs
T
svenm_adminandClaude Sonnet 5 a999b0182a 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>
2026-08-14 20:28:55 +02:00

172 lines
6.3 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using AfterHours.Data;
using AfterHours.Interaction;
using AfterHours.Networking;
using Unity.Netcode;
using UnityEngine;
namespace AfterHours.Shop
{
/// <summary>
/// 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, IPourTarget
{
[SerializeField] private ProductCatalog _catalog;
[SerializeField] private int _capacityUnits = 12;
[Tooltip("Ist dieses Fach vom Schaufenster aus einsehbar? Setzt der Level-Designer.")]
[SerializeField] private bool _exposedToStreet;
[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);
public int ProductId => _productId.Value;
public int Quantity => _quantity.Value;
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()
{
_quantity.OnValueChanged += (_, _) => RefreshVisuals();
_productId.OnValueChanged += (_, _) => RefreshVisuals();
RefreshVisuals();
if (IsServer) ShelfRegistry.Instance.Register(this);
}
public override void OnNetworkDespawn()
{
if (IsServer) ShelfRegistry.Instance.Unregister(this);
}
public string GetPrompt(ulong clientId)
{
if (IsEmpty) return "Regal befüllen";
var product = _catalog.Get(_productId.Value);
string name = product != null ? product.DisplayName : "?";
return $"{name} — {_quantity.Value}/{Capacity()} — {_price.Value:0.00} €";
}
/// <summary>Server: Spieler füllt aus der Hand auf.</summary>
public void Interact(NetworkObject actor)
{
if (!IsServer) return;
// TODO: Debug.Log-Zeilen entfernen, sobald Regal-Befüllen zuverlässig läuft.
if (!actor.TryGetComponent<PlayerHands>(out var hands) || hands.IsEmpty)
{
Debug.Log("[ShelfSlot] Abbruch: keine PlayerHands am Actor oder Hände leer.");
return;
}
int incomingProductId = hands.ProductId;
// Fach ist mit anderer Ware belegt -> nichts tun.
if (!IsEmpty && _productId.Value != incomingProductId)
{
Debug.Log($"[ShelfSlot] Abbruch: Fach belegt mit ProductId {_productId.Value}, Hand trägt {incomingProductId}.");
return;
}
int free = Capacity() - _quantity.Value;
if (free <= 0)
{
Debug.Log($"[ShelfSlot] Abbruch: Fach voll ({_quantity.Value}/{Capacity()}).");
return;
}
int moved = hands.TryGive(Mathf.Min(free, hands.Quantity));
if (moved <= 0)
{
Debug.Log("[ShelfSlot] Abbruch: TryGive() lieferte 0 Stück.");
return;
}
if (IsEmpty)
{
_productId.Value = incomingProductId;
var def = _catalog.Get(_productId.Value);
if (def != null && _price.Value <= 0f) _price.Value = def.RecommendedPrice;
}
_quantity.Value += moved;
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()
{
if (!IsServer || IsEmpty) return false;
_quantity.Value--;
if (_quantity.Value == 0) _productId.Value = -1;
return true;
}
/// <summary>Server: Preis setzen (vom Preis-UI aus).</summary>
public void SetPrice(float price)
{
if (!IsServer) return;
_price.Value = Mathf.Max(0.01f, price);
}
/// <summary>Kapazität in Stück, abhängig von der Produktgröße.</summary>
private int Capacity()
{
var product = _catalog.Get(_productId.Value);
int size = product != null ? (int)product.ShelfSize : 1;
return Mathf.Max(1, _capacityUnits / size);
}
private void RefreshVisuals()
{
var product = _catalog != null ? _catalog.Get(_productId.Value) : null;
GameObject prefab = product != null ? product.PackagePrefab : null;
_packageDisplay.SetTarget(prefab, _quantity.Value, Capacity());
}
}
}