Merge remote-tracking branch 'origin/feat/inport-props' into feat/craft-discovery
# Conflicts: # Assets/External/Animated PBR Chest Demo/Materials/WoodChest.mat # Packages/com.distantlands.cozy.core/Content/Integration/Import for BiRP.unitypackage.meta # Packages/com.distantlands.cozy.core/Content/Integration/Import for HDRP.unitypackage.meta # Packages/com.distantlands.cozy.core/Content/Integration/Import for URP.unitypackage.meta
This commit is contained in:
@@ -3,111 +3,225 @@ using Ashwild.Player;
|
||||
|
||||
namespace Ashwild.Inventory
|
||||
{
|
||||
/// <summary>
|
||||
/// Keeps the item in the player's hand in sync with the selected hotbar slot. It owns the *timing*
|
||||
/// of a swap, not its look: the put-away and draw animations live in the arms rig, and this
|
||||
/// controller simply waits for them.
|
||||
///
|
||||
/// The wait is what makes the swap read correctly. Destroying the old prefab the instant the
|
||||
/// selection changes makes the item vanish mid-motion, so instead the sequence is: ask for the
|
||||
/// put-away animation, wait for its Animation Event, then destroy, announce the new item (which
|
||||
/// lets the animation binder load its clips first), spawn it, and ask for the draw animation.
|
||||
///
|
||||
/// That wait is guarded by a timeout, because the completion event only exists if someone plays the
|
||||
/// clip. An item whose set has no put-away clip, a rig that was never wired, a disabled arms
|
||||
/// object — any of those would otherwise leave the player stuck holding an item he already
|
||||
/// switched away from, with no error to explain it. The timeout turns a silent deadlock into a
|
||||
/// slightly abrupt swap.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public class HotbarController : MonoBehaviour
|
||||
{
|
||||
#region Serialized Fields
|
||||
|
||||
[Header("References")]
|
||||
[SerializeField] private Transform toolHolder;
|
||||
[SerializeField] private PlayerToolHolder toolHolderAnim;
|
||||
[Tooltip("Attachment joint in the arms rig's right hand (RightHand_Holder_JTN). Held prefabs are parented here so they follow the animated hand.")]
|
||||
[SerializeField] private Transform handHolder;
|
||||
[Tooltip("Origin and forward of the aim ray handed to held items — normally the first-person camera.")]
|
||||
[SerializeField] private Transform raycastOrigin;
|
||||
|
||||
[Header("Swap")]
|
||||
[Tooltip("Seconds to wait for the put-away animation before swapping anyway. Safety net only — a wired rig always completes first.")]
|
||||
[SerializeField] private float stowTimeout = 0.5f;
|
||||
|
||||
#endregion
|
||||
|
||||
#region State
|
||||
|
||||
private PlayerInventory inventory;
|
||||
private GameObject currentHeldObject;
|
||||
private ItemData currentHeldItem;
|
||||
private bool isSwitching;
|
||||
private HeldItemContext heldContext;
|
||||
|
||||
private GameObject currentHeldObject;
|
||||
private ItemData currentHeldItem;
|
||||
|
||||
private bool isStowing;
|
||||
private ItemData pendingItem;
|
||||
private float stowDeadline;
|
||||
private bool stowTimeoutReported;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Unity Lifecycle
|
||||
|
||||
/// <summary>
|
||||
/// Binds to the local inventory and draws whatever is already selected, without animation —
|
||||
/// spawning into the world should not look like the player just swapped weapons.
|
||||
/// </summary>
|
||||
private void Start()
|
||||
{
|
||||
inventory = PlayerInventory.Instance;
|
||||
inventory.onSelectedSlotChanged.AddListener(OnSelectionChanged);
|
||||
inventory.onSlotChanged.AddListener(OnSlotChanged);
|
||||
if (inventory == null)
|
||||
{
|
||||
Debug.LogError("[HotbarController] No local PlayerInventory — the hand will stay empty.", this);
|
||||
return;
|
||||
}
|
||||
|
||||
inventory.onSelectedSlotChanged.AddListener(HandleSelectionChanged);
|
||||
inventory.onSlotChanged.AddListener(HandleSlotChanged);
|
||||
|
||||
heldContext = new HeldItemContext { RaycastOrigin = raycastOrigin };
|
||||
UpdateHeldItem(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Listens for the put-away animation finishing. Paired with OnDisable.
|
||||
/// </summary>
|
||||
private void OnEnable()
|
||||
{
|
||||
PlayerEvents.UnequipAnimComplete += HandleUnequipAnimComplete;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribes — mirrors OnEnable exactly.
|
||||
/// </summary>
|
||||
private void OnDisable()
|
||||
{
|
||||
PlayerEvents.UnequipAnimComplete -= HandleUnequipAnimComplete;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drops the inventory listeners; the bus subscription is already handled by OnDisable.
|
||||
/// </summary>
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (inventory != null)
|
||||
if (inventory == null) return;
|
||||
inventory.onSelectedSlotChanged.RemoveListener(HandleSelectionChanged);
|
||||
inventory.onSlotChanged.RemoveListener(HandleSlotChanged);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forces the swap through when the put-away animation never reports back. Runs only while a
|
||||
/// swap is actually pending, so an idle player costs nothing. The diagnostic is logged once per
|
||||
/// session rather than per swap: the cause is always a wiring or authoring gap that will repeat
|
||||
/// on every single hotbar change, and a console flooded with the same warning hides the rest.
|
||||
/// </summary>
|
||||
private void Update()
|
||||
{
|
||||
if (!isStowing) return;
|
||||
if (Time.time < stowDeadline) return;
|
||||
|
||||
if (!stowTimeoutReported)
|
||||
{
|
||||
inventory.onSelectedSlotChanged.RemoveListener(OnSelectionChanged);
|
||||
inventory.onSlotChanged.RemoveListener(OnSlotChanged);
|
||||
stowTimeoutReported = true;
|
||||
Debug.LogWarning("[HotbarController] The put-away animation never completed — swapping anyway " +
|
||||
"(logged once). Check that the arms rig has an ArmsAnimationEvents component and " +
|
||||
"that the Unequip clip carries an AnimUnequipComplete event. Until those clips " +
|
||||
"exist, lower Stow Timeout so the swap stays snappy.", this);
|
||||
}
|
||||
|
||||
CommitSwap();
|
||||
}
|
||||
|
||||
private void OnSelectionChanged(int index)
|
||||
#endregion
|
||||
|
||||
#region Event Handlers
|
||||
|
||||
private void HandleSelectionChanged(int index) => UpdateHeldItem(true);
|
||||
|
||||
/// <summary>
|
||||
/// The held item's own stack changed (it was consumed, worn out, or refilled) — redraw only when
|
||||
/// it is the selected slot.
|
||||
/// </summary>
|
||||
private void HandleSlotChanged(int index)
|
||||
{
|
||||
UpdateHeldItem(true);
|
||||
if (index == inventory.SelectedHotbarIndex) UpdateHeldItem(true);
|
||||
}
|
||||
|
||||
private void OnSlotChanged(int index)
|
||||
/// <summary>
|
||||
/// The arms finished putting the old item away, so the swap can complete.
|
||||
/// </summary>
|
||||
private void HandleUnequipAnimComplete()
|
||||
{
|
||||
if (index == inventory.SelectedHotbarIndex)
|
||||
UpdateHeldItem(true);
|
||||
if (isStowing) CommitSwap();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Swap Sequence
|
||||
|
||||
/// <summary>
|
||||
/// Entry point for every reason the hand might need to change. Swaps straight away when there is
|
||||
/// nothing to put away (empty hands, or the very first draw), otherwise starts the put-away
|
||||
/// animation and defers. A selection that changes again mid-stow only updates the target: the
|
||||
/// player who scrolls three slots quickly plays one put-away, not three.
|
||||
/// </summary>
|
||||
private void UpdateHeldItem(bool animate)
|
||||
{
|
||||
InventorySlot selected = inventory.GetSelectedSlot();
|
||||
ItemData newItem = selected.IsEmpty ? null : selected.ItemData;
|
||||
|
||||
if (newItem == currentHeldItem)
|
||||
if (newItem == currentHeldItem && !isStowing) return;
|
||||
|
||||
pendingItem = newItem;
|
||||
|
||||
if (isStowing) return;
|
||||
|
||||
if (!animate || currentHeldObject == null)
|
||||
{
|
||||
CommitSwap();
|
||||
return;
|
||||
|
||||
if (isSwitching)
|
||||
{
|
||||
if (currentHeldObject != null)
|
||||
{
|
||||
Destroy(currentHeldObject);
|
||||
currentHeldObject = null;
|
||||
}
|
||||
isSwitching = false;
|
||||
}
|
||||
|
||||
if (animate && currentHeldObject != null && toolHolderAnim != null)
|
||||
{
|
||||
isSwitching = true;
|
||||
toolHolderAnim.PlayUnequipAnimation(() =>
|
||||
{
|
||||
if (currentHeldObject != null)
|
||||
{
|
||||
Destroy(currentHeldObject);
|
||||
currentHeldObject = null;
|
||||
}
|
||||
currentHeldItem = newItem;
|
||||
SpawnItem(newItem);
|
||||
isSwitching = false;
|
||||
|
||||
if (currentHeldObject != null)
|
||||
toolHolderAnim.PlayEquipAnimation();
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
if (currentHeldObject != null)
|
||||
{
|
||||
Destroy(currentHeldObject);
|
||||
currentHeldObject = null;
|
||||
}
|
||||
currentHeldItem = newItem;
|
||||
SpawnItem(newItem);
|
||||
|
||||
if (animate && toolHolderAnim != null && currentHeldObject != null)
|
||||
toolHolderAnim.PlayEquipAnimation();
|
||||
}
|
||||
isStowing = true;
|
||||
stowDeadline = Time.time + stowTimeout;
|
||||
PlayerEvents.RaiseUnequipStarted();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs the actual exchange. HeldItemChanged is raised *before* the new prefab is spawned so
|
||||
/// the animation binder has already loaded the item's clips by the time the draw trigger fires —
|
||||
/// otherwise the first frame of the draw would play the previous item's animation.
|
||||
/// </summary>
|
||||
private void CommitSwap()
|
||||
{
|
||||
isStowing = false;
|
||||
|
||||
if (currentHeldObject != null)
|
||||
{
|
||||
Destroy(currentHeldObject);
|
||||
currentHeldObject = null;
|
||||
}
|
||||
|
||||
currentHeldItem = pendingItem;
|
||||
PlayerEvents.RaiseHeldItemChanged(currentHeldItem);
|
||||
|
||||
SpawnItem(currentHeldItem);
|
||||
|
||||
if (currentHeldItem != null) PlayerEvents.RaiseEquipStarted();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Instantiates the item's hand prefab on the rig's hand joint and links its behaviour to the
|
||||
/// player. Parenting to the joint rather than to a free-floating holder is what keeps a tool
|
||||
/// locked in the palm through every animation, with no code following the hand each frame.
|
||||
/// </summary>
|
||||
private void SpawnItem(ItemData item)
|
||||
{
|
||||
if (item == null || item.HandPrefab == null || toolHolder == null)
|
||||
if (item == null || item.HandPrefab == null) return;
|
||||
|
||||
if (handHolder == null)
|
||||
{
|
||||
Debug.LogError($"[HotbarController] No hand holder assigned — '{item.ItemName}' cannot be drawn. " +
|
||||
"Wire the arms rig's RightHand_Holder_JTN.", this);
|
||||
return;
|
||||
}
|
||||
|
||||
currentHeldObject = Instantiate(item.HandPrefab, toolHolder);
|
||||
currentHeldObject = Instantiate(item.HandPrefab, handHolder);
|
||||
|
||||
// Link the freshly spawned held item to the player so its behaviour
|
||||
// (tool, consumable, ...) can interact — only the equipped item is alive.
|
||||
IHeldItemBehaviour behaviour = currentHeldObject.GetComponentInChildren<IHeldItemBehaviour>(true);
|
||||
if (behaviour != null)
|
||||
behaviour.Setup(heldContext, item);
|
||||
behaviour?.Setup(heldContext, item);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user