Files
svenm_adminandClaude Sonnet 5 9eb588e491 feat(shop): configurable package display rotation, pocoregal shelf model
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>
2026-08-16 16:13:32 +02:00

180 lines
6.8 KiB
C#
Raw Permalink 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);
[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());
}
}
}