initial
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
using System.Collections.Generic;
|
||||
using AfterHours.Core;
|
||||
using AfterHours.Data;
|
||||
using AfterHours.Interaction;
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AfterHours.Shop
|
||||
{
|
||||
/// <summary>
|
||||
/// Kasse. Kunden reihen sich ein; ein Spieler scannt Position für Position
|
||||
/// und schließt ab. Sämtliche Geldbewegung passiert hier auf dem Server.
|
||||
/// </summary>
|
||||
public sealed class CashRegister : NetworkBehaviour, IInteractable
|
||||
{
|
||||
[SerializeField] private ProductCatalog _catalog;
|
||||
[SerializeField] private Transform _queueStart;
|
||||
[SerializeField] private float _queueSpacing = 0.8f;
|
||||
|
||||
/// <summary>Server-only. Clients sehen die Schlange über die NPC-Positionen.</summary>
|
||||
private readonly List<Customers.CustomerAgent> _queue = new();
|
||||
|
||||
private readonly NetworkVariable<float> _pendingTotal = new(0f);
|
||||
private readonly NetworkVariable<int> _scannedItems = new(0);
|
||||
|
||||
public float PendingTotal => _pendingTotal.Value;
|
||||
public int QueueLength => _queue.Count;
|
||||
|
||||
public string GetPrompt(ulong clientId)
|
||||
{
|
||||
if (_queue.Count == 0) return "Kasse (frei)";
|
||||
return _scannedItems.Value > 0
|
||||
? $"Kassieren — {_pendingTotal.Value:0.00} €"
|
||||
: "Artikel scannen";
|
||||
}
|
||||
|
||||
/// <summary>Server: ein Klick = ein Artikel scannen, oder abschließen wenn fertig.</summary>
|
||||
public void Interact(NetworkObject actor)
|
||||
{
|
||||
if (!IsServer || _queue.Count == 0) return;
|
||||
|
||||
var customer = _queue[0];
|
||||
if (customer == null) { _queue.RemoveAt(0); return; }
|
||||
|
||||
if (_scannedItems.Value < customer.BasketCount)
|
||||
{
|
||||
ScanNext(customer);
|
||||
return;
|
||||
}
|
||||
|
||||
Finalize(customer);
|
||||
}
|
||||
|
||||
private void ScanNext(Customers.CustomerAgent customer)
|
||||
{
|
||||
var line = customer.BasketAt(_scannedItems.Value);
|
||||
_pendingTotal.Value += line.Price;
|
||||
_scannedItems.Value++;
|
||||
}
|
||||
|
||||
private void Finalize(Customers.CustomerAgent customer)
|
||||
{
|
||||
ShopEconomy.Instance.Earn(_pendingTotal.Value);
|
||||
ShopEconomy.Instance.RegisterSale(customer.Comfort);
|
||||
|
||||
customer.OnPaid();
|
||||
_queue.RemoveAt(0);
|
||||
|
||||
_pendingTotal.Value = 0f;
|
||||
_scannedItems.Value = 0;
|
||||
|
||||
ReflowQueue();
|
||||
}
|
||||
|
||||
/// <summary>Server: Kunde stellt sich an. Gibt die Zielposition zurück.</summary>
|
||||
public Vector3 Enqueue(Customers.CustomerAgent customer)
|
||||
{
|
||||
if (!IsServer) return _queueStart.position;
|
||||
|
||||
if (!_queue.Contains(customer)) _queue.Add(customer);
|
||||
return SlotPosition(_queue.IndexOf(customer));
|
||||
}
|
||||
|
||||
/// <summary>Server: Kunde verlässt die Schlange (z. B. weil er aufgibt).</summary>
|
||||
public void Remove(Customers.CustomerAgent customer)
|
||||
{
|
||||
if (!IsServer) return;
|
||||
if (_queue.Remove(customer)) ReflowQueue();
|
||||
}
|
||||
|
||||
private void ReflowQueue()
|
||||
{
|
||||
for (int i = 0; i < _queue.Count; i++)
|
||||
if (_queue[i] != null) _queue[i].MoveTo(SlotPosition(i));
|
||||
}
|
||||
|
||||
private Vector3 SlotPosition(int index)
|
||||
{
|
||||
return _queueStart.position + _queueStart.forward * (-_queueSpacing * index);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 87517e4a128619c469c629b6248f1daa
|
||||
@@ -0,0 +1,22 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace AfterHours.Shop
|
||||
{
|
||||
/// <summary>
|
||||
/// Serverseitige Liste aller aktiven Regalfächer. Damit Kunden ein
|
||||
/// passendes Fach finden können, ohne FindObjectOfType im Frame-Loop
|
||||
/// aufzurufen (siehe CLAUDE.md §5).
|
||||
/// </summary>
|
||||
public sealed class ShelfRegistry
|
||||
{
|
||||
public static readonly ShelfRegistry Instance = new();
|
||||
|
||||
private readonly List<ShelfSlot> _slots = new();
|
||||
|
||||
public IReadOnlyList<ShelfSlot> Slots => _slots;
|
||||
|
||||
public void Register(ShelfSlot slot) => _slots.Add(slot);
|
||||
|
||||
public void Unregister(ShelfSlot slot) => _slots.Remove(slot);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 36930243258ac6f4f816f2cfc254a52f
|
||||
@@ -0,0 +1,112 @@
|
||||
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
|
||||
{
|
||||
[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;
|
||||
|
||||
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 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;
|
||||
if (!actor.TryGetComponent<PlayerHands>(out var hands) || hands.IsEmpty) return;
|
||||
|
||||
// Fach ist mit anderer Ware belegt -> nichts tun.
|
||||
if (!IsEmpty && _productId.Value != hands.ProductId) return;
|
||||
|
||||
int free = Capacity() - _quantity.Value;
|
||||
if (free <= 0) return;
|
||||
|
||||
int moved = hands.TryGive(Mathf.Min(free, hands.Quantity));
|
||||
if (moved <= 0) return;
|
||||
|
||||
if (IsEmpty)
|
||||
{
|
||||
_productId.Value = hands.ProductId >= 0 ? hands.ProductId : _productId.Value;
|
||||
var def = _catalog.Get(_productId.Value);
|
||||
if (def != null && _price.Value <= 0f) _price.Value = def.RecommendedPrice;
|
||||
}
|
||||
|
||||
_quantity.Value += moved;
|
||||
}
|
||||
|
||||
/// <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()
|
||||
{
|
||||
// TODO: Verpackungs-Prefabs nach Menge ein-/ausblenden (Object Pool).
|
||||
// Läuft auf allen Peers, rein kosmetisch.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2bf1c5efe2d53984e90da832661184d8
|
||||
@@ -0,0 +1,49 @@
|
||||
using AfterHours.Data;
|
||||
using AfterHours.Interaction;
|
||||
using AfterHours.Networking;
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AfterHours.Shop
|
||||
{
|
||||
/// <summary>
|
||||
/// Lagerkiste im Backroom: hält Nachschub einer einzigen Produktsorte.
|
||||
/// Spieler nehmen daraus in die Hände auf und tragen die Ware zum Regal
|
||||
/// (siehe ShelfSlot.Interact).
|
||||
/// </summary>
|
||||
public sealed class StorageCrate : NetworkBehaviour, IInteractable
|
||||
{
|
||||
[SerializeField] private ProductCatalog _catalog;
|
||||
[SerializeField] private int _productId = -1;
|
||||
[SerializeField] private int _startingQuantity = 200;
|
||||
|
||||
private readonly NetworkVariable<int> _quantity = new(0);
|
||||
|
||||
public int ProductId => _productId;
|
||||
public int Quantity => _quantity.Value;
|
||||
public bool IsEmpty => _quantity.Value <= 0;
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
if (IsServer) _quantity.Value = _startingQuantity;
|
||||
}
|
||||
|
||||
public string GetPrompt(ulong clientId)
|
||||
{
|
||||
var product = _catalog.Get(_productId);
|
||||
string name = product != null ? product.DisplayName : "?";
|
||||
return IsEmpty ? $"{name} — Lager leer" : $"{name} nehmen — {_quantity.Value} im Lager";
|
||||
}
|
||||
|
||||
/// <summary>Server: Spieler nimmt Ware in die Hände auf.</summary>
|
||||
public void Interact(NetworkObject actor)
|
||||
{
|
||||
if (!IsServer || IsEmpty) return;
|
||||
if (!actor.TryGetComponent<PlayerHands>(out var hands)) return;
|
||||
|
||||
int want = Mathf.Min(_quantity.Value, PlayerHands.MaxCarry);
|
||||
int taken = hands.TryTake(_productId, want);
|
||||
_quantity.Value -= taken;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2b8651b0bcbd9a442bc9688879ce2605
|
||||
Reference in New Issue
Block a user