using AfterHours.Data; using AfterHours.Interaction; using AfterHours.Networking; using Unity.Netcode; using UnityEngine; namespace AfterHours.Shop { /// /// Ein Regalfach: hält genau eine Produktsorte. Auffüllen geht über /// Interact(), Entnahme durch Kunden serverseitig über TakeOne(). /// 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 _productId = new(-1); private readonly NetworkVariable _quantity = new(0); private readonly NetworkVariable _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); /// Weltposition des Rasterplatzes mit gegebenem Index – Ziel fürs Flug-Visual (siehe PlayerCarry). public Vector3 GetSlotWorldPosition(int index) { return _displayRoot != null ? _displayRoot.TransformPoint(_packageDisplay.SlotLocalPosition(index)) : transform.position; } /// Endrotation eines Pakets im Regal – muss zur ruhenden Anzeige passen, siehe PlayerCarry. 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} €"; } /// Server: Spieler füllt aus der Hand auf. public void Interact(NetworkObject actor) { if (!IsServer) return; // TODO: Debug.Log-Zeilen entfernen, sobald Regal-Befüllen zuverlässig läuft. if (!actor.TryGetComponent(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()}."); } /// Server: ein Stück aus einem IPourSource einräumen (siehe PlayerCarry). 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; } /// Server: Kunde entnimmt ein Stück. public bool TakeOne() { if (!IsServer || IsEmpty) return false; _quantity.Value--; if (_quantity.Value == 0) _productId.Value = -1; return true; } /// Server: Preis setzen (vom Preis-UI aus). public void SetPrice(float price) { if (!IsServer) return; _price.Value = Mathf.Max(0.01f, price); } /// Kapazität in Stück, abhängig von der Produktgröße. 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()); } } }