feat(shop): add box pickup/carry, pour-into-shelf, and package flight visuals
- Carryable: generic pickup/carry component, cosmetic per-peer follow of the carrier's HandSocket instead of NGO reparenting (camera pitch isn't networked, so root-level reparenting wouldn't tilt with the view) - PlayerCarry: mutual exclusion with PlayerHands, server-timed hold-to-pour tick (client only signals start/stop, never per-unit requests) - IPourSource/IPourTarget: decouple PlayerInteractor from concrete Shop types, same pattern as IInteractable - Lieferkarton: gains product data (productId/quantity), two-stage Interact() (open, then pick up), IPourSource - ShelfSlot: implements IPourTarget, fills the previous RefreshVisuals TODO - PackageDisplay: renders quantity as a proper column x row grid that stacks into further layers as space runs out, reused by both containers - FlyingPackage: small non-networked cosmetic visual that arcs a package from the carried box to its exact landing slot in the shelf grid on every successful pour tick, broadcast via Rpc(SendTo.Everyone) - StorageCrate: rejects pickup while the player is carrying a box Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
using AfterHours.Networking;
|
||||
using Unity.Netcode;
|
||||
using Unity.Netcode.Components;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AfterHours.Interaction
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(NetworkObject))]
|
||||
public sealed class Carryable : NetworkBehaviour
|
||||
{
|
||||
[SerializeField] private Collider[] _collidersToDisableWhileCarried;
|
||||
[SerializeField] private NetworkTransform _networkTransform;
|
||||
[SerializeField] private Rigidbody _rigidbody;
|
||||
|
||||
private readonly NetworkVariable<bool> _isCarried = new(
|
||||
false,
|
||||
NetworkVariableReadPermission.Everyone,
|
||||
NetworkVariableWritePermission.Server
|
||||
);
|
||||
|
||||
private readonly NetworkVariable<NetworkObjectReference> _carriedBy = new(
|
||||
default,
|
||||
NetworkVariableReadPermission.Everyone,
|
||||
NetworkVariableWritePermission.Server
|
||||
);
|
||||
|
||||
public bool IsCarried => _isCarried.Value;
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
_isCarried.OnValueChanged += (_, current) => ApplyCarriedState(current);
|
||||
ApplyCarriedState(_isCarried.Value);
|
||||
}
|
||||
|
||||
/// <summary>Server: Träger übernimmt das Objekt. False, wenn schon getragen.</summary>
|
||||
public bool TryPickUp(NetworkObject carrier)
|
||||
{
|
||||
if (!IsServer || _isCarried.Value) return false;
|
||||
|
||||
_carriedBy.Value = carrier;
|
||||
_isCarried.Value = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Server: Objekt an fester Position/Rotation ablegen.</summary>
|
||||
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<PlayerCarry>(out var carry) || carry.HandSocket == null) return;
|
||||
|
||||
transform.SetPositionAndRotation(carry.HandSocket.position, carry.HandSocket.rotation);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user