diff --git a/Assets/Prefabs/Player.prefab b/Assets/Prefabs/Player.prefab
index bd812a5..f3684ca 100644
--- a/Assets/Prefabs/Player.prefab
+++ b/Assets/Prefabs/Player.prefab
@@ -15,6 +15,7 @@ GameObject:
- component: {fileID: 6222683508516611630}
- component: {fileID: 2738588686490111713}
- component: {fileID: 1819053832086648781}
+ - component: {fileID: 8888888888888888803}
m_Layer: 0
m_Name: Player
m_TagString: Untagged
@@ -215,7 +216,8 @@ Transform:
m_LocalPosition: {x: 0.07, y: 0.86, z: 0.14}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
- m_Children: []
+ m_Children:
+ - {fileID: 8888888888888888802}
m_Father: {fileID: 5481062863680931684}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!20 &6771695443112085325
@@ -321,6 +323,53 @@ MonoBehaviour:
m_VarianceClampScale: 0.9
m_ContrastAdaptiveSharpening: 0
m_Version: 2
+--- !u!1 &8888888888888888801
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 8888888888888888802}
+ m_Layer: 0
+ m_Name: HandSocket
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &8888888888888888802
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 8888888888888888801}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0.3, y: -0.2, z: 0.6}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 7530582357117073923}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!114 &8888888888888888803
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 2721740664708778080}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 6ed618ff659f6684f8c2ce58ba032c35, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: AfterHours.Runtime::AfterHours.Networking.PlayerCarry
+ ShowTopMostFoldoutHeaderGroup: 1
+ _handSocket: {fileID: 8888888888888888802}
+ _pourInterval: 0.4
+ _maxPourReach: 2.5
--- !u!1 &7157595194792186803
GameObject:
m_ObjectHideFlags: 0
diff --git a/Assets/Prefabs/ShelfSlot.prefab b/Assets/Prefabs/ShelfSlot.prefab
index 8328e9d..a8f78ce 100644
--- a/Assets/Prefabs/ShelfSlot.prefab
+++ b/Assets/Prefabs/ShelfSlot.prefab
@@ -98,7 +98,7 @@ MonoBehaviour:
_catalog: {fileID: 11400000, guid: e173fe210071dba4790dd003a88e4fcd, type: 2}
_capacityUnits: 12
_exposedToStreet: 0
- _displayRoot: {fileID: 0}
+ _displayRoot: {fileID: 5742035034345165094}
--- !u!1 &6000252374135812501
GameObject:
m_ObjectHideFlags: 0
diff --git a/Assets/Scenes/Shop.unity b/Assets/Scenes/Shop.unity
index 879baab..b833a84 100644
--- a/Assets/Scenes/Shop.unity
+++ b/Assets/Scenes/Shop.unity
@@ -450,6 +450,37 @@ MonoBehaviour:
m_SelectedFaces:
m_SelectedEdges: []
m_SelectedVertices:
+--- !u!1 &53478574
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 53478575}
+ m_Layer: 0
+ m_Name: ContentsRoot
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &53478575
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 53478574}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: -0.243, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 959439192}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &64083402
GameObject:
m_ObjectHideFlags: 0
@@ -3863,7 +3894,10 @@ PrefabInstance:
objectReference: {fileID: 0}
m_RemovedComponents: []
m_RemovedGameObjects: []
- m_AddedGameObjects: []
+ m_AddedGameObjects:
+ - targetCorrespondingSourceObject: {fileID: -8679921383154817045, guid: 280091748b47569469096b4ec9745729, type: 3}
+ insertIndex: -1
+ addedObject: {fileID: 53478575}
m_AddedComponents:
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 280091748b47569469096b4ec9745729, type: 3}
insertIndex: -1
@@ -3883,6 +3917,9 @@ PrefabInstance:
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 280091748b47569469096b4ec9745729, type: 3}
insertIndex: -1
addedObject: {fileID: 959439197}
+ - targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 280091748b47569469096b4ec9745729, type: 3}
+ insertIndex: -1
+ addedObject: {fileID: 959439200}
m_SourcePrefab: {fileID: 100100000, guid: 280091748b47569469096b4ec9745729, type: 3}
--- !u!1 &959439188 stripped
GameObject:
@@ -3925,6 +3962,11 @@ Transform:
m_CorrespondingSourceObject: {fileID: -7629218545453614500, guid: 280091748b47569469096b4ec9745729, type: 3}
m_PrefabInstance: {fileID: 959439187}
m_PrefabAsset: {fileID: 0}
+--- !u!4 &959439192 stripped
+Transform:
+ m_CorrespondingSourceObject: {fileID: -8679921383154817045, guid: 280091748b47569469096b4ec9745729, type: 3}
+ m_PrefabInstance: {fileID: 959439187}
+ m_PrefabAsset: {fileID: 0}
--- !u!114 &959439195
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -3942,6 +3984,11 @@ MonoBehaviour:
deckelRechts: {fileID: 959439190}
oeffnungsWinkel: 170
geschwindigkeit: 5
+ _catalog: {fileID: 11400000, guid: e173fe210071dba4790dd003a88e4fcd, type: 2}
+ _productId: 1
+ _startingQuantity: 12
+ _contentsRoot: {fileID: 53478575}
+ _maxVisiblePackages: 12
--- !u!65 &959439196
BoxCollider:
m_ObjectHideFlags: 0
@@ -4053,6 +4100,22 @@ Rigidbody:
m_Interpolate: 0
m_Constraints: 0
m_CollisionDetection: 0
+--- !u!114 &959439200
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 959439188}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: b73b32288703e1c4995e220e80249f35, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: AfterHours.Runtime::AfterHours.Interaction.Carryable
+ ShowTopMostFoldoutHeaderGroup: 1
+ _collidersToDisableWhileCarried: []
+ _networkTransform: {fileID: 0}
+ _rigidbody: {fileID: 0}
--- !u!1 &975800552
GameObject:
m_ObjectHideFlags: 0
@@ -5468,6 +5531,157 @@ Transform:
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1001 &1637614133
+PrefabInstance:
+ m_ObjectHideFlags: 0
+ serializedVersion: 2
+ m_Modification:
+ serializedVersion: 3
+ m_TransformParent: {fileID: 0}
+ m_Modifications:
+ - target: {fileID: -8679921383154817045, guid: 0d912f468f12f1a48b2df1314b991b38, type: 3}
+ propertyPath: m_LocalPosition.x
+ value: -4.018
+ objectReference: {fileID: 0}
+ - target: {fileID: -8679921383154817045, guid: 0d912f468f12f1a48b2df1314b991b38, type: 3}
+ propertyPath: m_LocalPosition.y
+ value: 1.047
+ objectReference: {fileID: 0}
+ - target: {fileID: -8679921383154817045, guid: 0d912f468f12f1a48b2df1314b991b38, type: 3}
+ propertyPath: m_LocalPosition.z
+ value: 0.603
+ objectReference: {fileID: 0}
+ - target: {fileID: -8679921383154817045, guid: 0d912f468f12f1a48b2df1314b991b38, type: 3}
+ propertyPath: m_LocalRotation.w
+ value: 0.7071067
+ objectReference: {fileID: 0}
+ - target: {fileID: -8679921383154817045, guid: 0d912f468f12f1a48b2df1314b991b38, type: 3}
+ propertyPath: m_LocalRotation.x
+ value: -0.7071068
+ objectReference: {fileID: 0}
+ - target: {fileID: -8679921383154817045, guid: 0d912f468f12f1a48b2df1314b991b38, type: 3}
+ propertyPath: m_LocalRotation.y
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: -8679921383154817045, guid: 0d912f468f12f1a48b2df1314b991b38, type: 3}
+ propertyPath: m_LocalRotation.z
+ value: -0
+ objectReference: {fileID: 0}
+ - target: {fileID: -8679921383154817045, guid: 0d912f468f12f1a48b2df1314b991b38, type: 3}
+ propertyPath: m_LocalEulerAnglesHint.x
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: -8679921383154817045, guid: 0d912f468f12f1a48b2df1314b991b38, type: 3}
+ propertyPath: m_LocalEulerAnglesHint.y
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: -8679921383154817045, guid: 0d912f468f12f1a48b2df1314b991b38, type: 3}
+ propertyPath: m_LocalEulerAnglesHint.z
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: -7511558181221131132, guid: 0d912f468f12f1a48b2df1314b991b38, type: 3}
+ propertyPath: 'm_Materials.Array.data[0]'
+ value:
+ objectReference: {fileID: 2100000, guid: 9ff3d1c8bfaace6428d25719c6afa121, type: 2}
+ - target: {fileID: 919132149155446097, guid: 0d912f468f12f1a48b2df1314b991b38, type: 3}
+ propertyPath: m_Name
+ value: Meshy_AI_wooden_shelf_3_compar_0814175655_image-to-3d-texture
+ objectReference: {fileID: 0}
+ m_RemovedComponents: []
+ m_RemovedGameObjects: []
+ m_AddedGameObjects: []
+ m_AddedComponents: []
+ m_SourcePrefab: {fileID: 100100000, guid: 0d912f468f12f1a48b2df1314b991b38, type: 3}
+--- !u!1 &1658583203
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 1658583206}
+ - component: {fileID: 1658583205}
+ - component: {fileID: 1658583204}
+ m_Layer: 0
+ m_Name: mesh_node
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!23 &1658583204
+MeshRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1658583203}
+ m_Enabled: 1
+ m_CastShadows: 1
+ m_ReceiveShadows: 1
+ m_DynamicOccludee: 1
+ m_StaticShadowCaster: 0
+ m_MotionVectors: 1
+ m_LightProbeUsage: 1
+ m_ReflectionProbeUsage: 1
+ m_RayTracingMode: 2
+ m_RayTraceProcedural: 0
+ m_RayTracingAccelStructBuildFlagsOverride: 0
+ m_RayTracingAccelStructBuildFlags: 1
+ m_SmallMeshCulling: 1
+ m_ForceMeshLod: -1
+ m_MeshLodSelectionBias: 0
+ m_RenderingLayerMask: 1
+ m_RendererPriority: 0
+ m_Materials:
+ - {fileID: 0}
+ m_StaticBatchInfo:
+ firstSubMesh: 0
+ subMeshCount: 0
+ m_StaticBatchRoot: {fileID: 0}
+ m_ProbeAnchor: {fileID: 0}
+ m_LightProbeVolumeOverride: {fileID: 0}
+ m_ScaleInLightmap: 1
+ m_ReceiveGI: 1
+ m_PreserveUVs: 0
+ m_IgnoreNormalsForChartDetection: 0
+ m_ImportantGI: 0
+ m_StitchLightmapSeams: 1
+ m_SelectedEditorRenderState: 3
+ m_MinimumChartSize: 4
+ m_AutoUVMaxDistance: 0.5
+ m_AutoUVMaxAngle: 89
+ m_LightmapParameters: {fileID: 0}
+ m_GlobalIlluminationMeshLod: 0
+ m_SortingLayerID: 0
+ m_SortingLayer: 0
+ m_SortingOrder: 0
+ m_MaskInteraction: 0
+ m_AdditionalVertexStreams: {fileID: 0}
+--- !u!33 &1658583205
+MeshFilter:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1658583203}
+ m_Mesh: {fileID: 7158796146257112550, guid: 0d912f468f12f1a48b2df1314b991b38, type: 3}
+--- !u!4 &1658583206
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1658583203}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: -4.069018, y: 0.00000036529013, z: 0.19831897}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!43 &1899577878
Mesh:
m_ObjectHideFlags: 0
@@ -6020,3 +6234,5 @@ SceneRoots:
- {fileID: 959439187}
- {fileID: 1091476159}
- {fileID: 1417349271}
+ - {fileID: 1637614133}
+ - {fileID: 1658583206}
diff --git a/Assets/Scripts/Interaction/Carryable.cs b/Assets/Scripts/Interaction/Carryable.cs
new file mode 100644
index 0000000..777d227
--- /dev/null
+++ b/Assets/Scripts/Interaction/Carryable.cs
@@ -0,0 +1,86 @@
+using AfterHours.Networking;
+using Unity.Netcode;
+using Unity.Netcode.Components;
+using UnityEngine;
+
+namespace AfterHours.Interaction
+{
+ ///
+ /// Macht ein Objekt aufheb- und tragbar. Reines Trage-Handwerkszeug ohne
+ /// Fachwissen über Inhalt/Produkte – das bleibt beim jeweiligen Objekt
+ /// (z. B. Lieferkarton). Positionierung beim Tragen ist rein kosmetisch
+ /// und läuft auf jedem Peer selbst (siehe ShelfSlot.RefreshVisuals-Muster),
+ /// kein NGO-Reparenting – siehe Plan-Begründung in Lieferkarton.cs.
+ ///
+ [RequireComponent(typeof(NetworkObject))]
+ public sealed class Carryable : NetworkBehaviour
+ {
+ [SerializeField] private Collider[] _collidersToDisableWhileCarried;
+ [SerializeField] private NetworkTransform _networkTransform;
+ [SerializeField] private Rigidbody _rigidbody;
+
+ private readonly NetworkVariable _isCarried = new(
+ false,
+ NetworkVariableReadPermission.Everyone,
+ NetworkVariableWritePermission.Server
+ );
+
+ private readonly NetworkVariable _carriedBy = new(
+ default,
+ NetworkVariableReadPermission.Everyone,
+ NetworkVariableWritePermission.Server
+ );
+
+ public bool IsCarried => _isCarried.Value;
+
+ public override void OnNetworkSpawn()
+ {
+ _isCarried.OnValueChanged += (_, current) => ApplyCarriedState(current);
+ ApplyCarriedState(_isCarried.Value);
+ }
+
+ /// Server: Träger übernimmt das Objekt. False, wenn schon getragen.
+ public bool TryPickUp(NetworkObject carrier)
+ {
+ if (!IsServer || _isCarried.Value) return false;
+
+ _carriedBy.Value = carrier;
+ _isCarried.Value = true;
+ return true;
+ }
+
+ /// Server: Objekt an fester Position/Rotation ablegen.
+ public void Drop(Vector3 position, Quaternion rotation)
+ {
+ if (!IsServer || !_isCarried.Value) return;
+
+ transform.SetPositionAndRotation(position, rotation);
+ _isCarried.Value = false;
+ _carriedBy.Value = default;
+ }
+
+ private void ApplyCarriedState(bool carried)
+ {
+ foreach (var collider in _collidersToDisableWhileCarried)
+ if (collider != null) collider.enabled = !carried;
+
+ if (_networkTransform != null) _networkTransform.enabled = !carried;
+
+ // Sonst kämpft die Physik gegen das kosmetische Tragen (LateUpdate unten).
+ if (_rigidbody != null)
+ {
+ _rigidbody.isKinematic = carried;
+ if (!carried) _rigidbody.linearVelocity = Vector3.zero;
+ }
+ }
+
+ private void LateUpdate()
+ {
+ if (!_isCarried.Value) return;
+ if (!_carriedBy.Value.TryGet(out var carrier)) return;
+ if (!carrier.TryGetComponent(out var carry) || carry.HandSocket == null) return;
+
+ transform.SetPositionAndRotation(carry.HandSocket.position, carry.HandSocket.rotation);
+ }
+ }
+}
diff --git a/Assets/Scripts/Interaction/Carryable.cs.meta b/Assets/Scripts/Interaction/Carryable.cs.meta
new file mode 100644
index 0000000..1dac910
--- /dev/null
+++ b/Assets/Scripts/Interaction/Carryable.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: b73b32288703e1c4995e220e80249f35
\ No newline at end of file
diff --git a/Assets/Scripts/Interaction/IPourSource.cs b/Assets/Scripts/Interaction/IPourSource.cs
new file mode 100644
index 0000000..afc8f70
--- /dev/null
+++ b/Assets/Scripts/Interaction/IPourSource.cs
@@ -0,0 +1,15 @@
+namespace AfterHours.Interaction
+{
+ ///
+ /// Alles, was per Klick-halten Stück für Stück ausgeschüttet werden kann
+ /// (z. B. Lieferkarton, später Lagerregal-Karton). TryTakeOne() läuft
+ /// serverseitig und muss selbst validieren, genau wie IPourTarget.TryAddOne().
+ ///
+ public interface IPourSource
+ {
+ bool IsOpen { get; }
+ int ProductId { get; }
+ bool IsEmpty { get; }
+ bool TryTakeOne();
+ }
+}
diff --git a/Assets/Scripts/Interaction/IPourSource.cs.meta b/Assets/Scripts/Interaction/IPourSource.cs.meta
new file mode 100644
index 0000000..e6c17d8
--- /dev/null
+++ b/Assets/Scripts/Interaction/IPourSource.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 7117f9f519059394889cc6edda07b9e1
\ No newline at end of file
diff --git a/Assets/Scripts/Interaction/IPourTarget.cs b/Assets/Scripts/Interaction/IPourTarget.cs
new file mode 100644
index 0000000..9353848
--- /dev/null
+++ b/Assets/Scripts/Interaction/IPourTarget.cs
@@ -0,0 +1,19 @@
+using UnityEngine;
+
+namespace AfterHours.Interaction
+{
+ ///
+ /// Alles, was per Klick-halten Stück für Stück befüllt werden kann
+ /// (z. B. ShelfSlot, später Lagerregal). TryAddOne() läuft serverseitig
+ /// und muss selbst validieren, genau wie IInteractable.Interact().
+ ///
+ public interface IPourTarget
+ {
+ Transform Transform { get; }
+ int ProductId { get; }
+ int Quantity { get; }
+ bool IsEmpty { get; }
+ bool IsFull { get; }
+ bool TryAddOne(int productId);
+ }
+}
diff --git a/Assets/Scripts/Interaction/IPourTarget.cs.meta b/Assets/Scripts/Interaction/IPourTarget.cs.meta
new file mode 100644
index 0000000..3d43d05
--- /dev/null
+++ b/Assets/Scripts/Interaction/IPourTarget.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 024acb3d9dadf024b9739a835debdbaa
\ No newline at end of file
diff --git a/Assets/Scripts/Interaction/PlayerInteractor.cs b/Assets/Scripts/Interaction/PlayerInteractor.cs
index 8522541..36848ee 100644
--- a/Assets/Scripts/Interaction/PlayerInteractor.cs
+++ b/Assets/Scripts/Interaction/PlayerInteractor.cs
@@ -1,3 +1,4 @@
+using AfterHours.Networking;
using Unity.Netcode;
using UnityEngine;
@@ -19,6 +20,8 @@ namespace AfterHours.Interaction
private const float ServerReachTolerance = 1.5f;
private IInteractable _current;
+ private IPourTarget _currentPourTarget;
+ private IPourTarget _pouringTarget;
public string CurrentPrompt { get; private set; }
private void Update()
@@ -27,24 +30,38 @@ namespace AfterHours.Interaction
RefreshTarget();
- if (_current != null && Input.GetKeyDown(KeyCode.E))
+ if (Input.GetKeyDown(KeyCode.E))
{
- var target = ((MonoBehaviour)_current).GetComponent();
- if (target != null) RequestInteractRpc(target);
+ if (_current != null)
+ {
+ var target = ((MonoBehaviour)_current).GetComponent();
+ if (target != null) RequestInteractRpc(target);
+ }
+ else if (TryGetComponent(out var carry) && carry.IsCarrying)
+ {
+ carry.RequestDropRpc();
+ }
}
+
+ HandlePourInput();
}
private void RefreshTarget()
{
var previous = _current;
_current = null;
+ _currentPourTarget = null;
CurrentPrompt = null;
- if (Physics.Raycast(_eyes.position, _eyes.forward, out var hit, _reach, _interactableMask)
- && hit.collider.TryGetComponent(out var interactable))
+ if (Physics.Raycast(_eyes.position, _eyes.forward, out var hit, _reach, _interactableMask))
{
- _current = interactable;
- CurrentPrompt = interactable.GetPrompt(OwnerClientId);
+ if (hit.collider.TryGetComponent(out var interactable))
+ {
+ _current = interactable;
+ CurrentPrompt = interactable.GetPrompt(OwnerClientId);
+ }
+
+ hit.collider.TryGetComponent(out _currentPourTarget);
}
// TODO: entfernen, sobald ein echter Prompt im UI angezeigt wird.
@@ -56,21 +73,62 @@ namespace AfterHours.Interaction
}
}
+ /// Maustaste halten, um ein IPourTarget im Blick Stück für Stück zu befüllen.
+ private void HandlePourInput()
+ {
+ if (Input.GetMouseButtonDown(0) && _currentPourTarget != null)
+ {
+ var targetNo = ((MonoBehaviour)_currentPourTarget).GetComponent();
+ if (targetNo != null)
+ {
+ _pouringTarget = _currentPourTarget;
+ RequestPourStartRpc(targetNo);
+ }
+ }
+
+ if (_pouringTarget != null && (Input.GetMouseButtonUp(0) || _currentPourTarget != _pouringTarget))
+ {
+ _pouringTarget = null;
+ RequestPourStopRpc();
+ }
+ }
+
[Rpc(SendTo.Server)]
private void RequestInteractRpc(NetworkObjectReference targetRef)
{
if (!targetRef.TryGet(out var target)) return;
// Server-Validierung: ist der Spieler überhaupt in Reichweite?
- float distance = Vector3.Distance(transform.position, target.transform.position);
- if (distance > _reach + ServerReachTolerance)
+ if (!IsInReach(target.transform.position))
{
- Debug.LogWarning($"[PlayerInteractor] Client {OwnerClientId} zu weit entfernt ({distance:F1} m).");
+ Debug.LogWarning($"[PlayerInteractor] Client {OwnerClientId} zu weit entfernt.");
return;
}
if (target.TryGetComponent(out var interactable))
interactable.Interact(NetworkObject);
}
+
+ [Rpc(SendTo.Server)]
+ private void RequestPourStartRpc(NetworkObjectReference targetRef)
+ {
+ if (!targetRef.TryGet(out var target) || !target.TryGetComponent(out var pourTarget)) return;
+ if (!IsInReach(target.transform.position)) return;
+
+ if (TryGetComponent(out var carry)) carry.BeginPour(pourTarget);
+ }
+
+ [Rpc(SendTo.Server)]
+ private void RequestPourStopRpc()
+ {
+ if (TryGetComponent(out var carry)) carry.StopPour();
+ }
+
+ /// Server-Validierung: ist der Spieler überhaupt in Reichweite?
+ private bool IsInReach(Vector3 point)
+ {
+ float distance = Vector3.Distance(transform.position, point);
+ return distance <= _reach + ServerReachTolerance;
+ }
}
}
diff --git a/Assets/Scripts/Networking/PlayerCarry.cs b/Assets/Scripts/Networking/PlayerCarry.cs
new file mode 100644
index 0000000..8d7b801
--- /dev/null
+++ b/Assets/Scripts/Networking/PlayerCarry.cs
@@ -0,0 +1,134 @@
+using AfterHours.Interaction;
+using AfterHours.Shop;
+using Unity.Netcode;
+using UnityEngine;
+
+namespace AfterHours.Networking
+{
+ ///
+ /// Was der Spieler gerade als physisches Objekt trägt (z. B. ein
+ /// Lieferkarton) sowie das serverseitig getaktete Ausschütten daraus in
+ /// ein IPourTarget. Getrennt von PlayerHands (abstrakte Stückzahl) –
+ /// beide sind gegenseitig exklusiv, siehe TryPickUp/StorageCrate.Interact.
+ ///
+ public sealed class PlayerCarry : NetworkBehaviour
+ {
+ [SerializeField] private Transform _handSocket;
+ [SerializeField] private float _pourInterval = 0.4f;
+ [SerializeField] private float _maxPourReach = 2.5f;
+ [SerializeField] private float _flightDuration = 0.35f;
+
+ private readonly NetworkVariable _isCarrying = new(
+ false,
+ NetworkVariableReadPermission.Everyone,
+ NetworkVariableWritePermission.Server
+ );
+
+ private readonly NetworkVariable _carriedObject = new(
+ default,
+ NetworkVariableReadPermission.Everyone,
+ NetworkVariableWritePermission.Server
+ );
+
+ public Transform HandSocket => _handSocket;
+ public bool IsCarrying => _isCarrying.Value;
+
+ private IPourTarget _pourTarget;
+ private IPourSource _pourSource;
+ private NetworkObjectReference _pourTargetRef;
+ private float _pourTimer;
+
+ /// Server: versucht, aufzuheben.
+ public bool TryPickUp(Carryable target)
+ {
+ if (!IsServer || _isCarrying.Value || target == null) return false;
+ if (TryGetComponent(out var hands) && !hands.IsEmpty) return false;
+ if (!target.TryPickUp(NetworkObject)) return false;
+
+ _carriedObject.Value = target.NetworkObject;
+ _isCarrying.Value = true;
+ return true;
+ }
+
+ /// Server: legt das getragene Objekt vor dem Spieler ab.
+ [Rpc(SendTo.Server)]
+ public void RequestDropRpc()
+ {
+ if (!_isCarrying.Value) return;
+
+ StopPour();
+
+ if (_carriedObject.Value.TryGet(out var carried) && carried.TryGetComponent(out var carryable))
+ {
+ Vector3 dropPosition = transform.position + transform.forward * 1f;
+ carryable.Drop(dropPosition, transform.rotation);
+ }
+
+ _isCarrying.Value = false;
+ _carriedObject.Value = default;
+ }
+
+ /// Server: startet das Ausschütten in , falls möglich.
+ public bool BeginPour(IPourTarget target)
+ {
+ if (!IsServer || target == null) return false;
+ if (!_isCarrying.Value || !_carriedObject.Value.TryGet(out var carried)) return false;
+ if (!carried.TryGetComponent(out var source)) return false;
+ if (!source.IsOpen || source.IsEmpty || target.IsFull) return false;
+ if (!target.IsEmpty && target.ProductId != source.ProductId) return false;
+ if (!((Component)target).TryGetComponent(out var targetNo)) return false;
+
+ _pourSource = source;
+ _pourTarget = target;
+ _pourTargetRef = targetNo;
+ _pourTimer = 0f; // erste Einheit sofort, danach im Takt von _pourInterval
+ return true;
+ }
+
+ public void StopPour()
+ {
+ _pourSource = null;
+ _pourTarget = null;
+ }
+
+ private void Update()
+ {
+ if (!IsServer || _pourSource == null || _pourTarget == null) return;
+
+ if (!_isCarrying.Value || _pourSource.IsEmpty || _pourTarget.IsFull)
+ {
+ StopPour();
+ return;
+ }
+
+ if (Vector3.Distance(transform.position, _pourTarget.Transform.position) > _maxPourReach)
+ {
+ StopPour();
+ return;
+ }
+
+ _pourTimer -= Time.deltaTime;
+ if (_pourTimer > 0f) return;
+ _pourTimer = _pourInterval;
+
+ int landingIndex = _pourTarget.Quantity;
+ int productId = _pourSource.ProductId;
+ if (_pourSource.TryTakeOne() && _pourTarget.TryAddOne(productId))
+ PlayFlightRpc(_carriedObject.Value, _pourTargetRef, landingIndex);
+ }
+
+ /// Läuft auf jedem Peer: rein kosmetisches Flug-Visual für die soeben transferierte Einheit.
+ [Rpc(SendTo.Everyone)]
+ private void PlayFlightRpc(NetworkObjectReference sourceRef, NetworkObjectReference targetRef, int landingIndex)
+ {
+ if (!sourceRef.TryGet(out var sourceObj) || !sourceObj.TryGetComponent(out var box)) return;
+ if (!targetRef.TryGet(out var targetObj) || !targetObj.TryGetComponent(out var shelf)) return;
+
+ GameObject prefab = box.GetPackagePrefab();
+ Vector3 start = box.transform.position + Vector3.up * 0.3f;
+ Vector3 end = shelf.GetSlotWorldPosition(landingIndex);
+
+ FlyingPackage.Spawn(prefab, start, end, shelf.transform.rotation, _flightDuration);
+ }
+ }
+}
diff --git a/Assets/Scripts/Networking/PlayerCarry.cs.meta b/Assets/Scripts/Networking/PlayerCarry.cs.meta
new file mode 100644
index 0000000..0794470
--- /dev/null
+++ b/Assets/Scripts/Networking/PlayerCarry.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 6ed618ff659f6684f8c2ce58ba032c35
\ No newline at end of file
diff --git a/Assets/Scripts/Shop/FlyingPackage.cs b/Assets/Scripts/Shop/FlyingPackage.cs
new file mode 100644
index 0000000..448516c
--- /dev/null
+++ b/Assets/Scripts/Shop/FlyingPackage.cs
@@ -0,0 +1,54 @@
+using UnityEngine;
+
+namespace AfterHours.Shop
+{
+ ///
+ /// Rein kosmetisches, nicht genetzwertes Flug-Visual für ein Stück Ware,
+ /// das vom Karton ins Regal wandert. Jeder Peer erzeugt seine eigene
+ /// Instanz (ausgelöst über PlayerCarry.PlayFlightRpc) – keine
+ /// Server-Autorität nötig, der eigentliche Bestand ist längst per
+ /// NetworkVariable synchron.
+ ///
+ public sealed class FlyingPackage : MonoBehaviour
+ {
+ private const float ArcHeight = 0.35f;
+
+ private Vector3 _start;
+ private Vector3 _end;
+ private Quaternion _startRotation;
+ private Quaternion _endRotation;
+ private float _duration;
+ private float _elapsed;
+
+ public static void Spawn(GameObject prefab, Vector3 start, Vector3 end, Quaternion endRotation, float duration)
+ {
+ if (prefab == null) return;
+
+ var instance = Instantiate(prefab, start, Quaternion.identity);
+ var flight = instance.AddComponent();
+ flight.Init(start, end, endRotation, duration);
+ }
+
+ private void Init(Vector3 start, Vector3 end, Quaternion endRotation, float duration)
+ {
+ _start = start;
+ _end = end;
+ _startRotation = transform.rotation;
+ _endRotation = endRotation;
+ _duration = Mathf.Max(0.05f, duration);
+ }
+
+ private void Update()
+ {
+ _elapsed += Time.deltaTime;
+ float t = Mathf.Clamp01(_elapsed / _duration);
+
+ Vector3 flat = Vector3.Lerp(_start, _end, t);
+ float arc = ArcHeight * Mathf.Sin(t * Mathf.PI);
+ transform.position = flat + Vector3.up * arc;
+ transform.rotation = Quaternion.Slerp(_startRotation, _endRotation, t);
+
+ if (t >= 1f) Destroy(gameObject);
+ }
+ }
+}
diff --git a/Assets/Scripts/Shop/FlyingPackage.cs.meta b/Assets/Scripts/Shop/FlyingPackage.cs.meta
new file mode 100644
index 0000000..caae1b2
--- /dev/null
+++ b/Assets/Scripts/Shop/FlyingPackage.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 3084ee344a724f9458cc369f42643325
\ No newline at end of file
diff --git a/Assets/Scripts/Shop/Lieferkarton.cs b/Assets/Scripts/Shop/Lieferkarton.cs
index c753b3e..e52b643 100644
--- a/Assets/Scripts/Shop/Lieferkarton.cs
+++ b/Assets/Scripts/Shop/Lieferkarton.cs
@@ -1,11 +1,14 @@
+using AfterHours.Data;
using AfterHours.Interaction;
+using AfterHours.Networking;
using Unity.Netcode;
using UnityEngine;
namespace AfterHours.Shop
{
[RequireComponent(typeof(NetworkObject))]
- public class Lieferkarton : NetworkBehaviour, IInteractable
+ [RequireComponent(typeof(Carryable))]
+ public class Lieferkarton : NetworkBehaviour, IInteractable, IPourSource
{
[Header("Deckel Objekte")]
[SerializeField] private Transform deckelLinks;
@@ -15,6 +18,18 @@ namespace AfterHours.Shop
[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);
+
// Eine synchronisierte Variable, die den Zustand des Kartons speichert
private NetworkVariable istOffen = new NetworkVariable(
false,
@@ -22,14 +37,31 @@ namespace AfterHours.Shop
NetworkVariableWritePermission.Server // Nur der Server darf den Zustand ändern
);
+ private readonly NetworkVariable _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();
+ _packageDisplay = new PackageDisplay(_contentsRoot, _displayColumns, _displayRows, _displaySpacing);
+
// Start-Rotationen (Geschlossen) direkt beim Laden sichern
if (deckelLinks != null) linksGeschlossen = deckelLinks.localRotation;
if (deckelRechts != null) rechtsGeschlossen = deckelRechts.localRotation;
@@ -39,6 +71,15 @@ namespace AfterHours.Shop
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
@@ -55,23 +96,53 @@ namespace AfterHours.Shop
}
}
- // Wird von deinem Interaktionssystem aufgerufen (z. B. wenn "E" gedrückt wird)
+ ///
+ /// Wird von deinem Interaktionssystem aufgerufen (z. B. wenn "E" gedrückt wird).
+ /// Geschlossen -> öffnen. Offen und nicht getragen -> aufheben.
+ ///
public void Interact(NetworkObject actor)
{
- ToggleKartonServerRpc();
+ if (!IsServer) return;
+
+ if (!istOffen.Value)
+ {
+ istOffen.Value = true;
+ return;
+ }
+
+ if (!_carryable.IsCarried && actor.TryGetComponent(out var carry))
+ carry.TryPickUp(_carryable);
}
- // Der Server verarbeitet den Aufruf und synchronisiert den Zustand im Netzwerk
- [Rpc(SendTo.Server)]
- private void ToggleKartonServerRpc()
+ /// Server: nimmt ein Stück Ware aus dem Karton. Für das Ausschütten (siehe PlayerCarry).
+ public bool TryTakeOne()
{
- istOffen.Value = !istOffen.Value;
+ 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)
{
- return istOffen.Value ? "Karton schließen" : "Karton öffnen";
+ 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.)";
+ }
+
+ /// Verpackungs-Prefab des aktuellen Produkts (auch für das Flug-Visual beim Ausschütten, siehe PlayerCarry).
+ 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);
}
}
}
diff --git a/Assets/Scripts/Shop/PackageDisplay.cs b/Assets/Scripts/Shop/PackageDisplay.cs
new file mode 100644
index 0000000..ddcbeb6
--- /dev/null
+++ b/Assets/Scripts/Shop/PackageDisplay.cs
@@ -0,0 +1,82 @@
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace AfterHours.Shop
+{
+ ///
+ /// Zeigt eine Stückzahl als einzelne Verpackungs-Prefabs unter einem
+ /// Wurzel-Transform an (Regalfach, Lieferkarton) – ordentlich in einem
+ /// Raster (Spalten x Reihen) gestapelt, weitere Lagen stapeln sich nach
+ /// oben. Rein kosmetisch, läuft auf jedem Peer. Baut bei Mengenänderung
+ /// nur das Delta neu, nicht die komplette Anzeige – wichtig beim
+ /// Stück-für-Stück-Ausschütten.
+ ///
+ public sealed class PackageDisplay
+ {
+ private readonly Transform _root;
+ private readonly int _columns;
+ private readonly int _rows;
+ private readonly Vector3 _spacing;
+ private readonly List _instances = new();
+ private GameObject _prefab;
+
+ public PackageDisplay(Transform root, int columns, int rows, Vector3 spacing)
+ {
+ _root = root;
+ _columns = Mathf.Max(1, columns);
+ _rows = Mathf.Max(1, rows);
+ _spacing = spacing;
+ }
+
+ /// Baut die Anzeige auf Instanzen an (gedeckelt bei maxVisible).
+ public void SetTarget(GameObject prefab, int count, int maxVisible)
+ {
+ if (_root == null) return;
+
+ if (prefab != _prefab)
+ {
+ Clear();
+ _prefab = prefab;
+ }
+
+ if (_prefab == null) return;
+
+ int target = Mathf.Clamp(count, 0, Mathf.Max(0, maxVisible));
+
+ while (_instances.Count < target)
+ {
+ var instance = Object.Instantiate(_prefab, _root);
+ instance.transform.localPosition = SlotLocalPosition(_instances.Count);
+ instance.transform.localRotation = Quaternion.identity;
+ _instances.Add(instance);
+ }
+
+ while (_instances.Count > target)
+ {
+ int last = _instances.Count - 1;
+ if (_instances[last] != null) Object.Destroy(_instances[last]);
+ _instances.RemoveAt(last);
+ }
+ }
+
+ /// Lokale Rasterposition für einen Index – auch als Flug-Ziel genutzt (siehe PlayerCarry).
+ public Vector3 SlotLocalPosition(int index)
+ {
+ int perLayer = _columns * _rows;
+ int layer = index / perLayer;
+ int rem = index % perLayer;
+ int row = rem / _columns;
+ int col = rem % _columns;
+ return new Vector3(col * _spacing.x, layer * _spacing.y, row * _spacing.z);
+ }
+
+ public void Clear()
+ {
+ foreach (var instance in _instances)
+ if (instance != null) Object.Destroy(instance);
+
+ _instances.Clear();
+ _prefab = null;
+ }
+ }
+}
diff --git a/Assets/Scripts/Shop/PackageDisplay.cs.meta b/Assets/Scripts/Shop/PackageDisplay.cs.meta
new file mode 100644
index 0000000..47f9daf
--- /dev/null
+++ b/Assets/Scripts/Shop/PackageDisplay.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: e0025e09fe99fd443aff717e718ffc70
\ No newline at end of file
diff --git a/Assets/Scripts/Shop/ShelfSlot.cs b/Assets/Scripts/Shop/ShelfSlot.cs
index 7a7cde3..ca268f4 100644
--- a/Assets/Scripts/Shop/ShelfSlot.cs
+++ b/Assets/Scripts/Shop/ShelfSlot.cs
@@ -10,7 +10,7 @@ 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
+ public sealed class ShelfSlot : NetworkBehaviour, IInteractable, IPourTarget
{
[SerializeField] private ProductCatalog _catalog;
[SerializeField] private int _capacityUnits = 12;
@@ -20,6 +20,11 @@ namespace AfterHours.Shop
[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);
+
private readonly NetworkVariable _productId = new(-1);
private readonly NetworkVariable _quantity = new(0);
private readonly NetworkVariable _price = new(0f);
@@ -29,6 +34,20 @@ namespace AfterHours.Shop
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);
+
+ /// 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;
+ }
public override void OnNetworkSpawn()
{
@@ -99,6 +118,24 @@ namespace AfterHours.Shop
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()
{
@@ -126,8 +163,9 @@ namespace AfterHours.Shop
private void RefreshVisuals()
{
- // TODO: Verpackungs-Prefabs nach Menge ein-/ausblenden (Object Pool).
- // Läuft auf allen Peers, rein kosmetisch.
+ var product = _catalog != null ? _catalog.Get(_productId.Value) : null;
+ GameObject prefab = product != null ? product.PackagePrefab : null;
+ _packageDisplay.SetTarget(prefab, _quantity.Value, Capacity());
}
}
}
diff --git a/Assets/Scripts/Shop/StorageCrate.cs b/Assets/Scripts/Shop/StorageCrate.cs
index e730dc7..d15173e 100644
--- a/Assets/Scripts/Shop/StorageCrate.cs
+++ b/Assets/Scripts/Shop/StorageCrate.cs
@@ -48,6 +48,7 @@ namespace AfterHours.Shop
{
if (!IsServer || IsEmpty) return;
if (!actor.TryGetComponent(out var hands)) return;
+ if (actor.TryGetComponent(out var carry) && carry.IsCarrying) return;
int want = Mathf.Min(_quantity.Value, PlayerHands.MaxCarry);
int taken = hands.TryTake(_productId, want);