Files
SixShopSimulator/Assets/Scripts/Shop/Lieferkarton.cs
T
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

150 lines
5.9 KiB
C#

using AfterHours.Data;
using AfterHours.Interaction;
using AfterHours.Networking;
using Unity.Netcode;
using UnityEngine;
namespace AfterHours.Shop
{
[RequireComponent(typeof(NetworkObject))]
[RequireComponent(typeof(Carryable))]
public class Lieferkarton : NetworkBehaviour, IInteractable, IPourSource
{
[Header("Deckel Objekte")]
[SerializeField] private Transform deckelLinks;
[SerializeField] private Transform deckelRechts;
[Header("Rotations-Einstellungen")]
[SerializeField] private float oeffnungsWinkel = 110f; // Winkel in Grad nach außen
[SerializeField] private float geschwindigkeit = 5f; // Geschwindigkeit des Aufklappens
[Header("Inhalt")]
[SerializeField] private ProductCatalog _catalog;
[SerializeField] private int _productId = -1;
[SerializeField] private int _startingQuantity = 20;
[SerializeField] private Transform _contentsRoot;
[SerializeField] private int _maxVisiblePackages = 12;
[Header("Anzeige-Raster (sauber gestapelt, so wie Platz ist)")]
[SerializeField] private int _displayColumns = 3;
[SerializeField] private int _displayRows = 3;
[SerializeField] private Vector3 _displaySpacing = new(0.12f, 0.1f, 0.12f);
[SerializeField] private Vector3 _displayRotationEuler = Vector3.zero;
// Eine synchronisierte Variable, die den Zustand des Kartons speichert
private NetworkVariable<bool> istOffen = new NetworkVariable<bool>(
false,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server // Nur der Server darf den Zustand ändern
);
private readonly NetworkVariable<int> _quantity = new(
0,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server
);
public bool IsOpen => istOffen.Value;
public int ProductId => _productId;
public int Quantity => _quantity.Value;
public bool IsEmpty => _quantity.Value <= 0;
// Interne Variablen zum Speichern der Ziel-Rotationen
private Quaternion linksGeschlossen;
private Quaternion linksOffen;
private Quaternion rechtsGeschlossen;
private Quaternion rechtsOffen;
private Carryable _carryable;
private PackageDisplay _packageDisplay;
private void Awake()
{
_carryable = GetComponent<Carryable>();
_packageDisplay = new PackageDisplay(_contentsRoot, _displayColumns, _displayRows, _displaySpacing, _displayRotationEuler);
// Start-Rotationen (Geschlossen) direkt beim Laden sichern
if (deckelLinks != null) linksGeschlossen = deckelLinks.localRotation;
if (deckelRechts != null) rechtsGeschlossen = deckelRechts.localRotation;
// Ziel-Rotationen berechnen (X-Achse = Scharnierkante entlang der Längsseite)
linksOffen = linksGeschlossen * Quaternion.Euler(-oeffnungsWinkel, 0, 0);
rechtsOffen = rechtsGeschlossen * Quaternion.Euler(oeffnungsWinkel, 0, 0);
}
public override void OnNetworkSpawn()
{
if (IsServer) _quantity.Value = _startingQuantity;
_quantity.OnValueChanged += (_, _) => RefreshVisuals();
istOffen.OnValueChanged += (_, _) => RefreshVisuals();
RefreshVisuals();
}
private void Update()
{
// Beide Deckel weich auf jedem Client rotieren lassen basierend auf der NetworkVariable
if (deckelLinks != null)
{
Quaternion zielLinks = istOffen.Value ? linksOffen : linksGeschlossen;
deckelLinks.localRotation = Quaternion.Slerp(deckelLinks.localRotation, zielLinks, Time.deltaTime * geschwindigkeit);
}
if (deckelRechts != null)
{
Quaternion zielRechts = istOffen.Value ? rechtsOffen : rechtsGeschlossen;
deckelRechts.localRotation = Quaternion.Slerp(deckelRechts.localRotation, zielRechts, Time.deltaTime * geschwindigkeit);
}
}
/// <summary>
/// Wird von deinem Interaktionssystem aufgerufen (z. B. wenn "E" gedrückt wird).
/// Geschlossen -> öffnen. Offen und nicht getragen -> aufheben.
/// </summary>
public void Interact(NetworkObject actor)
{
if (!IsServer) return;
if (!istOffen.Value)
{
istOffen.Value = true;
return;
}
if (!_carryable.IsCarried && actor.TryGetComponent<PlayerCarry>(out var carry))
carry.TryPickUp(_carryable);
}
/// <summary>Server: nimmt ein Stück Ware aus dem Karton. Für das Ausschütten (siehe PlayerCarry).</summary>
public bool TryTakeOne()
{
if (!IsServer || IsEmpty) return false;
_quantity.Value--;
return true;
}
// Gibt den dynamischen Text für das UI-Prompt zurück
public string GetPrompt(ulong clientId)
{
if (!istOffen.Value) return "Karton öffnen";
var product = _catalog != null ? _catalog.Get(_productId) : null;
string name = product != null ? product.DisplayName : "?";
return IsEmpty ? $"{name} — leer" : $"{name} aufheben ({_quantity.Value} Stk.)";
}
/// <summary>Verpackungs-Prefab des aktuellen Produkts (auch für das Flug-Visual beim Ausschütten, siehe PlayerCarry).</summary>
public GameObject GetPackagePrefab()
{
var product = _catalog != null ? _catalog.Get(_productId) : null;
return product != null ? product.PackagePrefab : null;
}
private void RefreshVisuals()
{
int visibleCount = istOffen.Value ? _quantity.Value : 0;
_packageDisplay.SetTarget(GetPackagePrefab(), visibleCount, _maxVisiblePackages);
}
}
}