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:
2026-07-25 19:42:26 +02:00
954 changed files with 999772 additions and 26193 deletions
@@ -0,0 +1,152 @@
using System.Collections.Generic;
using UnityEngine;
using Ashwild.Inventory;
namespace Ashwild.Player
{
/// <summary>
/// Keeps a rig's Animator loaded with the clips of whatever item the player is holding. It owns
/// exactly one concern — *which clips are in the controller right now* — while PlayerAnimatorDriver
/// owns *which parameters are set*. Splitting them is what lets a new weapon ship without touching
/// either: the driver keeps firing the same "Attack" trigger, the binder just points that slot at a
/// different clip.
///
/// It never assigns a fresh controller per item. Assigning runtimeAnimatorController rebinds the
/// Animator, which resets the state machine to Entry and wipes the parameters — the player would
/// visibly snap to idle every time he scrolled the hotbar. Instead one AnimatorOverrideController is
/// built in Awake and only its overrides are mutated afterwards, which leaves the current state and
/// all parameters untouched. That same property is what keeps FishNet's NetworkAnimator working on
/// the third-person body: an override controller preserves the base controller's parameter list, so
/// replication never sees the swap.
/// </summary>
[DisallowMultipleComponent]
public class PlayerAnimationSetBinder : MonoBehaviour
{
#region Serialized Fields
[Header("Target")]
[SerializeField] private Animator animator;
[Tooltip("Which rig this binder drives — it picks the matching set off the held ItemData.")]
[SerializeField] private PlayerAnimationRig rig = PlayerAnimationRig.Arms;
[Header("Sets")]
[Tooltip("Set used with bare hands, and as the fallback for any slot the held item leaves empty.")]
[SerializeField] private PlayerAnimationSet defaultSet;
#endregion
#region State
private AnimatorOverrideController overrideController;
private AnimationClip[] placeholders;
private readonly List<KeyValuePair<AnimationClip, AnimationClip>> overrides = new List<KeyValuePair<AnimationClip, AnimationClip>>();
private PlayerAnimationSet currentSet;
private bool ready;
#endregion
#region Unity Lifecycle
/// <summary>
/// Wraps the authored controller in a single override controller and caches its placeholder
/// clips, whose names are the slot keys every set is matched against. Done in Awake so the
/// rebind happens once, before any driver seeds a parameter.
/// </summary>
private void Awake()
{
if (animator == null)
{
Debug.LogError($"[PlayerAnimationSetBinder] '{name}' has no Animator assigned — clips will never swap.", this);
return;
}
RuntimeAnimatorController master = animator.runtimeAnimatorController;
if (master == null)
{
Debug.LogError($"[PlayerAnimationSetBinder] '{name}' — the Animator has no controller assigned.", this);
return;
}
overrideController = new AnimatorOverrideController(master) { name = $"{master.name} (Runtime)" };
overrideController.GetOverrides(overrides);
placeholders = new AnimationClip[overrides.Count];
for (int i = 0; i < overrides.Count; i++)
placeholders[i] = overrides[i].Key;
animator.runtimeAnimatorController = overrideController;
ready = true;
ApplySet(defaultSet);
}
/// <summary>
/// Subscribes to the held-item bus. HotbarController re-raises HeldItemChanged on its own first
/// resolve, so a rig enabled after the player already drew an item still lands on the right set.
/// </summary>
private void OnEnable()
{
PlayerEvents.HeldItemChanged += HandleHeldItemChanged;
}
/// <summary>
/// Unsubscribes — mirrors OnEnable exactly.
/// </summary>
private void OnDisable()
{
PlayerEvents.HeldItemChanged -= HandleHeldItemChanged;
}
#endregion
#region Event Handlers
/// <summary>
/// Loads the set the newly drawn item authors for this rig (or the bare-hand set when the player
/// empties his hands).
/// </summary>
private void HandleHeldItemChanged(ItemData item)
{
ApplySet(item != null ? item.GetAnimationSet(rig) : defaultSet);
}
#endregion
#region Internal Helpers
/// <summary>
/// Rewrites every slot in one ApplyOverrides call rather than one indexer assignment per clip,
/// because each individual assignment forces the controller to regenerate. Skips redundant work
/// when the set has not actually changed — several items can legitimately share one set.
/// </summary>
private void ApplySet(PlayerAnimationSet set)
{
if (!ready) return;
if (set == currentSet) return;
currentSet = set;
for (int i = 0; i < placeholders.Length; i++)
{
AnimationClip placeholder = placeholders[i];
overrides[i] = new KeyValuePair<AnimationClip, AnimationClip>(placeholder, Resolve(placeholder.name, set));
}
overrideController.ApplyOverrides(overrides);
}
/// <summary>
/// Picks the clip for a slot: the held item's set first, then the default set, then null — which
/// means "no override" and leaves the master controller's own placeholder playing. That last
/// step is the safety net: a slot nobody authored still animates instead of freezing the rig.
/// </summary>
private AnimationClip Resolve(string slotName, PlayerAnimationSet set)
{
AnimationClip clip = set != null ? set.GetClip(slotName) : null;
if (clip == null && defaultSet != null) clip = defaultSet.GetClip(slotName);
return clip;
}
#endregion
}
}