Add _displayRotationEuler to Lieferkarton/ShelfSlot so package prefabs with a non-upright export orientation can be corrected per-container without touching PackageDisplay code. Fixes a bug where flying packages landed with a different rotation than the resting shelf display (PlayerCarry used the raw shelf transform instead of ShelfSlot.GetSlotWorldRotation()). Also adds the pocoregal shelf model (Blender export) and its materials, places it in the Shop scene, registers ShelfSlot as a network prefab, and rescales EmptyBox. See docs/PACKAGE_DISPLAY_SETUP.md for the display-grid setup/troubleshooting reference. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
180 lines
6.8 KiB
C#
180 lines
6.8 KiB
C#
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);
|
||
[SerializeField] private Vector3 _displayRotationEuler = Vector3.zero;
|
||
|
||
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, _displayRotationEuler);
|
||
|
||
/// <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;
|
||
}
|
||
|
||
/// <summary>Endrotation eines Pakets im Regal – muss zur ruhenden Anzeige passen, siehe PlayerCarry.</summary>
|
||
public Quaternion GetSlotWorldRotation()
|
||
{
|
||
Quaternion baseRotation = _displayRoot != null ? _displayRoot.rotation : transform.rotation;
|
||
return baseRotation * Quaternion.Euler(_displayRotationEuler);
|
||
}
|
||
|
||
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());
|
||
}
|
||
}
|
||
}
|