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:
@@ -0,0 +1,43 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Ashwild.Player
|
||||
{
|
||||
/// <summary>
|
||||
/// Turns the arms rig's Animation Events into bus events, so gameplay reacts on the frame the
|
||||
/// animation says it should instead of on the frame the input arrived. That distinction is what
|
||||
/// makes a swing read as a swing: without it the axe deals its damage while it is still travelling
|
||||
/// backwards, and the tree loses a chunk before the blade has touched it.
|
||||
///
|
||||
/// Must sit on the same GameObject as the Animator — Unity resolves Animation Events by method name
|
||||
/// against the components of the animated object. Every method is prefixed "Anim" on purpose: the
|
||||
/// PlayerInput component uses Send Messages, so any method named after an input action would be
|
||||
/// invoked by the Input System too and crash on the signature mismatch (see CLAUDE.md §5.6).
|
||||
///
|
||||
/// This is owner-only local feedback — the arms exist only on the owning client — so it belongs in
|
||||
/// PlayerNetworkController.ownerOnlyBehaviours alongside the rest of the arms rig.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public class ArmsAnimationEvents : MonoBehaviour
|
||||
{
|
||||
#region Animation Events
|
||||
|
||||
/// <summary>
|
||||
/// The frame the held tool or weapon actually connects. Place it on the contact pose of every
|
||||
/// attack clip; the swinging item resolves its hit here.
|
||||
/// </summary>
|
||||
public void AnimAttackImpact() => PlayerEvents.RaiseAttackImpact();
|
||||
|
||||
/// <summary>
|
||||
/// The draw animation has finished and the item is fully in hand.
|
||||
/// </summary>
|
||||
public void AnimEquipComplete() => PlayerEvents.RaiseEquipAnimComplete();
|
||||
|
||||
/// <summary>
|
||||
/// The put-away animation has finished and the item has left the frame — the cue for
|
||||
/// HotbarController to actually destroy the old prefab and draw the next one.
|
||||
/// </summary>
|
||||
public void AnimUnequipComplete() => PlayerEvents.RaiseUnequipAnimComplete();
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4f541ac054ea4fc40976871e6e445941
|
||||
@@ -0,0 +1,127 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Ashwild.Player
|
||||
{
|
||||
/// <summary>
|
||||
/// Which of the player's two rigs a set of clips is authored for. The same item needs two
|
||||
/// different sets: the owner's first-person arms play hand-only clips, while the third-person
|
||||
/// body everyone else sees plays full-body clips with legs and torso.
|
||||
/// </summary>
|
||||
public enum PlayerAnimationRig
|
||||
{
|
||||
Arms,
|
||||
Body
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The clips one rig plays for one family of held item (bare hands, axe, pickaxe, sword…).
|
||||
/// This is the asset that makes the animation system extensible without code: the master
|
||||
/// Animator Controller authors the state machine once with placeholder clips, and each set
|
||||
/// merely swaps which clip sits in each slot. Adding a new weapon is two assets and one field
|
||||
/// on its ItemData — never a new controller, never a new state, never a new script.
|
||||
///
|
||||
/// Slots are matched by the *name* of the placeholder clip in the master controller, so an
|
||||
/// authored state named "Run" pulls this set's run clip. A slot left empty falls back to the
|
||||
/// binder's default set and then to the master's own placeholder, which is why a minimal set
|
||||
/// (a torch with only an idle and a run) is perfectly valid and never leaves a rig in T-pose.
|
||||
/// </summary>
|
||||
[CreateAssetMenu(fileName = "NewAnimationSet", menuName = "Ashwild/Player Animation Set")]
|
||||
public class PlayerAnimationSet : ScriptableObject
|
||||
{
|
||||
#region Slot Names
|
||||
|
||||
public const string SlotIdle = "Idle";
|
||||
public const string SlotWalk = "Walk";
|
||||
public const string SlotRun = "Run";
|
||||
public const string SlotCrouchIdle = "CrouchIdle";
|
||||
public const string SlotCrouchWalk = "CrouchWalk";
|
||||
public const string SlotJumpStart = "JumpStart";
|
||||
public const string SlotJumpLoop = "JumpLoop";
|
||||
public const string SlotJumpLand = "JumpLand";
|
||||
public const string SlotGrab = "Grab";
|
||||
public const string SlotEquip = "Equip";
|
||||
public const string SlotUnequip = "Unequip";
|
||||
|
||||
/// <summary>
|
||||
/// Prefix of the attack slots ("Attack1", "Attack2", …). Numbered rather than enumerated so a
|
||||
/// three-hit sword combo and a single pickaxe swing share the exact same master controller.
|
||||
/// </summary>
|
||||
public const string SlotAttackPrefix = "Attack";
|
||||
|
||||
#endregion
|
||||
|
||||
#region Serialized Fields
|
||||
|
||||
[Header("Locomotion")]
|
||||
[SerializeField] private AnimationClip idle;
|
||||
[SerializeField] private AnimationClip walk;
|
||||
[SerializeField] private AnimationClip run;
|
||||
[SerializeField] private AnimationClip crouchIdle;
|
||||
[SerializeField] private AnimationClip crouchWalk;
|
||||
|
||||
[Header("Air")]
|
||||
[SerializeField] private AnimationClip jumpStart;
|
||||
[SerializeField] private AnimationClip jumpLoop;
|
||||
[SerializeField] private AnimationClip jumpLand;
|
||||
|
||||
[Header("Actions")]
|
||||
[SerializeField] private AnimationClip grab;
|
||||
[SerializeField] private AnimationClip equip;
|
||||
[SerializeField] private AnimationClip unequip;
|
||||
|
||||
[Header("Attack")]
|
||||
[Tooltip("Swing variants, in combo order. One entry for a plain tool, several for a weapon combo.")]
|
||||
[SerializeField] private AnimationClip[] attackVariants;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public API
|
||||
|
||||
/// <summary>
|
||||
/// How many swing variants this set offers, so the swinging item can cycle through a combo
|
||||
/// without hard-coding a count it cannot see.
|
||||
/// </summary>
|
||||
public int AttackVariantCount => attackVariants != null ? attackVariants.Length : 0;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the clip authored for a slot, or null when this set does not cover it (the caller
|
||||
/// then falls back). Attack slots are resolved by their trailing number, which is 1-based in the
|
||||
/// controller ("Attack1" is the first variant) so designers read the states in combo order.
|
||||
/// </summary>
|
||||
public AnimationClip GetClip(string slotName)
|
||||
{
|
||||
switch (slotName)
|
||||
{
|
||||
case SlotIdle: return idle;
|
||||
case SlotWalk: return walk;
|
||||
case SlotRun: return run;
|
||||
case SlotCrouchIdle: return crouchIdle;
|
||||
case SlotCrouchWalk: return crouchWalk;
|
||||
case SlotJumpStart: return jumpStart;
|
||||
case SlotJumpLoop: return jumpLoop;
|
||||
case SlotJumpLand: return jumpLand;
|
||||
case SlotGrab: return grab;
|
||||
case SlotEquip: return equip;
|
||||
case SlotUnequip: return unequip;
|
||||
}
|
||||
|
||||
if (slotName != null && slotName.StartsWith(SlotAttackPrefix) &&
|
||||
int.TryParse(slotName.Substring(SlotAttackPrefix.Length), out int variant))
|
||||
return GetAttackVariant(variant - 1);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The swing clip at a combo index, or null when out of range — an unauthored variant simply
|
||||
/// falls back instead of throwing mid-fight.
|
||||
/// </summary>
|
||||
public AnimationClip GetAttackVariant(int index)
|
||||
{
|
||||
if (attackVariants == null || index < 0 || index >= attackVariants.Length) return null;
|
||||
return attackVariants[index];
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dc23e353ca2e8fb45ae7cbad52c55d9b
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1efb301f6b6f0674c94758684e992e0b
|
||||
@@ -30,8 +30,8 @@ namespace Ashwild.Player
|
||||
[SerializeField] private string speedParam = "Speed";
|
||||
[SerializeField] private string moveXParam = "MoveX";
|
||||
[SerializeField] private string moveYParam = "MoveY";
|
||||
[Tooltip("Planar speed (m/s) that maps to 1 in the blend tree. 0 feeds raw speed unscaled.")]
|
||||
[SerializeField] private float normalizeSpeed = 6f;
|
||||
[Tooltip("Planar speed (m/s) above which the player counts as moving rather than idle.")]
|
||||
[SerializeField] private float moveThreshold = 0.15f;
|
||||
[Tooltip("Damping applied to float parameters for smooth blends.")]
|
||||
[SerializeField] private float floatDampTime = 0.1f;
|
||||
|
||||
@@ -40,20 +40,46 @@ namespace Ashwild.Player
|
||||
[SerializeField] private string crouchingParam = "Crouching";
|
||||
[SerializeField] private string sprintingParam = "Sprinting";
|
||||
|
||||
[Header("Int parameters (leave blank to skip)")]
|
||||
[Tooltip("Selects which swing variant the attack state plays — 1-based, matching the Attack1/Attack2… slots.")]
|
||||
[SerializeField] private string attackIndexParam = "AttackIndex";
|
||||
|
||||
[Header("Trigger parameters (leave blank to skip)")]
|
||||
[SerializeField] private string jumpParam = "Jump";
|
||||
[SerializeField] private string attackParam = "Attack";
|
||||
[SerializeField] private string landParam = "Land";
|
||||
[SerializeField] private string grabParam = "Grab";
|
||||
[SerializeField] private string equipParam = "Equip";
|
||||
[SerializeField] private string unequipParam = "Unequip";
|
||||
|
||||
#endregion
|
||||
|
||||
#region Locomotion Levels
|
||||
|
||||
/// <summary>
|
||||
/// The discrete gait the Speed parameter reports, and the thresholds the blend tree is built on.
|
||||
/// Deliberately not a measured velocity: a metre-per-second reading makes the animation hostage to
|
||||
/// movement tuning (retune sprintSpeed and every rig silently blends wrong), and it lies whenever
|
||||
/// the player's actual speed drops without his intent changing — pushing uphill, against a wall,
|
||||
/// through a slowing effect — where a sprint would visibly decay into a walk. Reporting intent
|
||||
/// instead keeps the gait stable, and the parameter damping still eases each change.
|
||||
/// </summary>
|
||||
private const float IdleLevel = 0f;
|
||||
private const float WalkLevel = 1f;
|
||||
private const float RunLevel = 2f;
|
||||
|
||||
#endregion
|
||||
|
||||
#region State
|
||||
|
||||
private Vector3 planarVelocity;
|
||||
private bool isSprinting;
|
||||
|
||||
private int speedId, moveXId, moveYId;
|
||||
private int groundedId, crouchingId, sprintingId;
|
||||
private int attackIndexId;
|
||||
private int jumpId, attackId, landId;
|
||||
private int grabId, equipId, unequipId;
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -71,9 +97,13 @@ namespace Ashwild.Player
|
||||
groundedId = Hash(groundedParam);
|
||||
crouchingId = Hash(crouchingParam);
|
||||
sprintingId = Hash(sprintingParam);
|
||||
attackIndexId = Hash(attackIndexParam);
|
||||
jumpId = Hash(jumpParam);
|
||||
attackId = Hash(attackParam);
|
||||
landId = Hash(landParam);
|
||||
grabId = Hash(grabParam);
|
||||
equipId = Hash(equipParam);
|
||||
unequipId = Hash(unequipParam);
|
||||
|
||||
if (orientation == null && animator != null)
|
||||
orientation = animator.transform;
|
||||
@@ -92,10 +122,14 @@ namespace Ashwild.Player
|
||||
PlayerEvents.Jumped += HandleJumped;
|
||||
PlayerEvents.AttackSwung += HandleAttackSwung;
|
||||
PlayerEvents.LandingStunStarted += HandleLandingStunStarted;
|
||||
PlayerEvents.GrabPerformed += HandleGrabPerformed;
|
||||
PlayerEvents.EquipStarted += HandleEquipStarted;
|
||||
PlayerEvents.UnequipStarted += HandleUnequipStarted;
|
||||
|
||||
isSprinting = PlayerEvents.IsSprinting;
|
||||
SetBool(groundedId, PlayerEvents.IsGrounded);
|
||||
SetBool(crouchingId, PlayerEvents.IsCrouching);
|
||||
SetBool(sprintingId, PlayerEvents.IsSprinting);
|
||||
SetBool(sprintingId, isSprinting);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -110,26 +144,31 @@ namespace Ashwild.Player
|
||||
PlayerEvents.Jumped -= HandleJumped;
|
||||
PlayerEvents.AttackSwung -= HandleAttackSwung;
|
||||
PlayerEvents.LandingStunStarted -= HandleLandingStunStarted;
|
||||
PlayerEvents.GrabPerformed -= HandleGrabPerformed;
|
||||
PlayerEvents.EquipStarted -= HandleEquipStarted;
|
||||
PlayerEvents.UnequipStarted -= HandleUnequipStarted;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pushes the smoothed movement floats every frame so blend trees ease between poses
|
||||
/// instead of snapping when velocity changes.
|
||||
/// Pushes the smoothed movement floats every frame. The gait itself is a step function, but the
|
||||
/// damping on the way to the Animator is what turns idle → walk → run into a blend instead of a
|
||||
/// snap, so the discrete parameter still reads as continuous motion.
|
||||
/// </summary>
|
||||
private void Update()
|
||||
{
|
||||
if (animator == null) return;
|
||||
|
||||
float scale = normalizeSpeed > 0f ? 1f / normalizeSpeed : 1f;
|
||||
SetFloat(speedId, planarVelocity.magnitude * scale);
|
||||
float level = ComputeLocomotionLevel();
|
||||
SetFloat(speedId, level);
|
||||
|
||||
if (moveXId != 0 || moveYId != 0)
|
||||
{
|
||||
Vector3 direction = planarVelocity.sqrMagnitude > 0.0001f ? planarVelocity.normalized : Vector3.zero;
|
||||
Vector3 local = orientation != null
|
||||
? orientation.InverseTransformDirection(planarVelocity)
|
||||
: planarVelocity;
|
||||
SetFloat(moveXId, local.x * scale);
|
||||
SetFloat(moveYId, local.z * scale);
|
||||
? orientation.InverseTransformDirection(direction)
|
||||
: direction;
|
||||
SetFloat(moveXId, local.x * level);
|
||||
SetFloat(moveYId, local.z * level);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,19 +186,54 @@ namespace Ashwild.Player
|
||||
|
||||
private void HandleGroundedChanged(bool grounded, float impactSpeed) => SetBool(groundedId, grounded);
|
||||
private void HandleCrouchChanged(bool crouching) => SetBool(crouchingId, crouching);
|
||||
private void HandleSprintChanged(bool sprinting) => SetBool(sprintingId, sprinting);
|
||||
|
||||
/// <summary>
|
||||
/// Caches the sprint intent as well as setting the bool, because the gait reported through Speed
|
||||
/// is chosen from the player's intent rather than from how fast he happens to be travelling.
|
||||
/// </summary>
|
||||
private void HandleSprintChanged(bool sprinting)
|
||||
{
|
||||
isSprinting = sprinting;
|
||||
SetBool(sprintingId, sprinting);
|
||||
}
|
||||
private void HandleJumped() => SetTrigger(jumpId);
|
||||
private void HandleAttackSwung() => SetTrigger(attackId);
|
||||
|
||||
/// <summary>
|
||||
/// Selects the combo variant before firing the swing, so the attack state reads an index that is
|
||||
/// already correct on the frame it is entered. The parameter is 1-based to match the Attack1/
|
||||
/// Attack2… clip slots a designer sees in the controller.
|
||||
/// </summary>
|
||||
private void HandleAttackSwung(int variant)
|
||||
{
|
||||
SetInt(attackIndexId, variant + 1);
|
||||
SetTrigger(attackId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A landing stun begins on a hard touchdown — the cue for a landing animation.
|
||||
/// </summary>
|
||||
private void HandleLandingStunStarted(float duration) => SetTrigger(landId);
|
||||
|
||||
private void HandleGrabPerformed() => SetTrigger(grabId);
|
||||
private void HandleEquipStarted() => SetTrigger(equipId);
|
||||
private void HandleUnequipStarted() => SetTrigger(unequipId);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Internal Helpers
|
||||
|
||||
/// <summary>
|
||||
/// Picks the gait to report. Velocity is consulted only to answer "is he moving at all" — the
|
||||
/// distinction between walking and running comes from the sprint intent, never from a speed
|
||||
/// reading, so the two never disagree. Crouching is not a level of its own: it is a separate bool
|
||||
/// so a crouch tree can reuse the same idle/walk distinction underneath it.
|
||||
/// </summary>
|
||||
private float ComputeLocomotionLevel()
|
||||
{
|
||||
if (planarVelocity.magnitude < moveThreshold) return IdleLevel;
|
||||
return isSprinting ? RunLevel : WalkLevel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hashes a parameter name, returning 0 for a blank name so the setters can skip it.
|
||||
/// </summary>
|
||||
@@ -175,6 +249,11 @@ namespace Ashwild.Player
|
||||
if (id != 0 && animator != null) animator.SetBool(id, value);
|
||||
}
|
||||
|
||||
private void SetInt(int id, int value)
|
||||
{
|
||||
if (id != 0 && animator != null) animator.SetInteger(id, value);
|
||||
}
|
||||
|
||||
private void SetTrigger(int id)
|
||||
{
|
||||
if (id != 0 && animator != null) animator.SetTrigger(id);
|
||||
|
||||
@@ -127,13 +127,24 @@ namespace Ashwild.Player
|
||||
// ============================================================
|
||||
|
||||
public static event Action<IInteractable> InteractableHoverChanged; // null = nothing hovered
|
||||
// A world interaction (pickup, harvest) could not be carried out — payload is a short, player-facing
|
||||
// reason such as "Inventory full". Raised whenever an interaction is refused, so a refusal is never
|
||||
// silent: without it a rejected harvest looks exactly like a successful one that dropped nothing.
|
||||
public static event Action<string> InteractionRefused;
|
||||
// The local player just reached out to grab something off the ground. Raised the moment the
|
||||
// gesture is committed (all checks passed, request sent) rather than when the item lands in the
|
||||
// inventory, so the grab animation plays instantly instead of after a server round-trip.
|
||||
public static event Action GrabPerformed;
|
||||
|
||||
// ============================================================
|
||||
// Tools events
|
||||
// ============================================================
|
||||
|
||||
public static event Action AttackSwung;
|
||||
public static event Action<int> AttackSwung; // the swing started — payload is the combo variant index
|
||||
public static event Action AttackImpact; // the swing's contact frame (Animation Event) — resolve the hit here
|
||||
public static event Action<Harvestable, float> HarvestableHit;
|
||||
public static event Action EquipStarted; // play the draw animation for the item now in hand
|
||||
public static event Action UnequipStarted; // play the put-away animation for the item leaving the hand
|
||||
public static event Action EquipAnimComplete;
|
||||
public static event Action UnequipAnimComplete;
|
||||
|
||||
@@ -299,8 +310,39 @@ namespace Ashwild.Player
|
||||
InteractableHoverChanged?.Invoke(target);
|
||||
}
|
||||
|
||||
public static void RaiseAttackSwung() { Log(nameof(AttackSwung)); AttackSwung?.Invoke(); }
|
||||
/// <summary>
|
||||
/// Announces that a world interaction was refused, with a short player-facing reason. The HUD turns
|
||||
/// it into a notification so the player always learns why nothing was gained.
|
||||
/// </summary>
|
||||
public static void RaiseInteractionRefused(string reason)
|
||||
{
|
||||
if (string.IsNullOrEmpty(reason)) return;
|
||||
Log(nameof(InteractionRefused));
|
||||
InteractionRefused?.Invoke(reason);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Announces a swing, carrying which combo variant to play. The variant defaults to 0 so a plain
|
||||
/// one-swing tool never has to think about combos, while a weapon can cycle its own variants.
|
||||
/// </summary>
|
||||
public static void RaiseAttackSwung(int variant = 0) { Log(nameof(AttackSwung)); AttackSwung?.Invoke(variant); }
|
||||
|
||||
/// <summary>
|
||||
/// Announces the local player's grab gesture so the arms play their pickup animation. Owner-side
|
||||
/// only, and deliberately not tied to the item actually arriving: the server still has the final
|
||||
/// say on the pickup, but the hand should reach out the instant the player presses.
|
||||
/// </summary>
|
||||
public static void RaiseGrabPerformed() { Log(nameof(GrabPerformed)); GrabPerformed?.Invoke(); }
|
||||
|
||||
/// <summary>
|
||||
/// Announces the contact frame of a swing, raised from the attack clip's Animation Event. Items
|
||||
/// resolve their hit here rather than on the input press so the damage lands when the blade does.
|
||||
/// </summary>
|
||||
public static void RaiseAttackImpact() { Log(nameof(AttackImpact)); AttackImpact?.Invoke(); }
|
||||
|
||||
public static void RaiseHarvestableHit(Harvestable h, float d) { Log(nameof(HarvestableHit)); HarvestableHit?.Invoke(h, d); }
|
||||
public static void RaiseEquipStarted() { Log(nameof(EquipStarted)); EquipStarted?.Invoke(); }
|
||||
public static void RaiseUnequipStarted() { Log(nameof(UnequipStarted)); UnequipStarted?.Invoke(); }
|
||||
public static void RaiseEquipAnimComplete() { EquipAnimComplete?.Invoke(); }
|
||||
public static void RaiseUnequipAnimComplete() { UnequipAnimComplete?.Invoke(); }
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using UnityEngine;
|
||||
using Ashwild.Harvesting;
|
||||
using Ashwild.Inventory;
|
||||
using Ashwild.Network;
|
||||
|
||||
namespace Ashwild.Player
|
||||
{
|
||||
@@ -44,6 +43,13 @@ namespace Ashwild.Player
|
||||
/// </summary>
|
||||
[SerializeField] private float swingCooldown = 0.5f;
|
||||
|
||||
[Header("Impact")]
|
||||
/// <summary>
|
||||
/// Delay after which a swing resolves on its own when the animation never reports its contact
|
||||
/// frame. Safety net for an item whose attack clip carries no AnimAttackImpact event.
|
||||
/// </summary>
|
||||
[SerializeField] private float impactFallbackDelay = 0.2f;
|
||||
|
||||
#endregion
|
||||
|
||||
#region State
|
||||
@@ -63,16 +69,27 @@ namespace Ashwild.Player
|
||||
/// </summary>
|
||||
private float nextSwingTime;
|
||||
|
||||
/// <summary>
|
||||
/// True between a swing starting and its hit being resolved.
|
||||
/// </summary>
|
||||
private bool swingPending;
|
||||
|
||||
/// <summary>
|
||||
/// Time at which a pending swing resolves itself without an impact event.
|
||||
/// </summary>
|
||||
private float impactDeadline;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Unity Lifecycle
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes to the use input for as long as this item is held.
|
||||
/// Subscribes to the use input and to the animation's contact frame for as long as this item is held.
|
||||
/// </summary>
|
||||
private void OnEnable()
|
||||
{
|
||||
PlayerEvents.AttackPressed += HandleAttackPressed;
|
||||
PlayerEvents.AttackImpact += HandleAttackImpact;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -81,6 +98,19 @@ namespace Ashwild.Player
|
||||
private void OnDisable()
|
||||
{
|
||||
PlayerEvents.AttackPressed -= HandleAttackPressed;
|
||||
PlayerEvents.AttackImpact -= HandleAttackImpact;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a swing whose animation never announced its contact frame, so an item with no impact
|
||||
/// event still harvests instead of swinging forever without effect. Runs only while a swing is in
|
||||
/// flight.
|
||||
/// </summary>
|
||||
private void Update()
|
||||
{
|
||||
if (!swingPending) return;
|
||||
if (Time.time < impactDeadline) return;
|
||||
ResolveSwing();
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -101,9 +131,10 @@ namespace Ashwild.Player
|
||||
#region Event Handlers
|
||||
|
||||
/// <summary>
|
||||
/// Swings on use input (gated by lock, setup and cooldown) and harvests the
|
||||
/// aimed target. A depleted but non-destroyed tool (a worn-out repairable axe) is
|
||||
/// unusable: the swing is blocked until it is repaired.
|
||||
/// Starts a swing on use input (gated by lock, setup and cooldown). The hit is deliberately not
|
||||
/// resolved here: it waits for the animation's contact frame, so the tree only loses a chunk once
|
||||
/// the blade has actually reached it. A depleted but non-destroyed tool (a worn-out repairable
|
||||
/// axe) is unusable: the swing is blocked until it is repaired.
|
||||
/// </summary>
|
||||
private void HandleAttackPressed()
|
||||
{
|
||||
@@ -113,8 +144,33 @@ namespace Ashwild.Player
|
||||
if (Time.time < nextSwingTime) return;
|
||||
|
||||
nextSwingTime = Time.time + swingCooldown;
|
||||
swingPending = true;
|
||||
impactDeadline = Time.time + impactFallbackDelay;
|
||||
|
||||
PlayerEvents.RaiseAttackSwung();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The swing animation reached its contact frame. Ignored when no swing of ours is in flight, so
|
||||
/// an impact event belonging to a previous item cannot make a freshly drawn tool hit for free.
|
||||
/// </summary>
|
||||
private void HandleAttackImpact()
|
||||
{
|
||||
if (!swingPending) return;
|
||||
ResolveSwing();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Swing Resolution
|
||||
|
||||
/// <summary>
|
||||
/// Closes an in-flight swing and applies its hit. Clearing the pending flag first makes the swing
|
||||
/// resolve exactly once whether the impact event or the fallback timer got here first.
|
||||
/// </summary>
|
||||
private void ResolveSwing()
|
||||
{
|
||||
swingPending = false;
|
||||
TryHarvest();
|
||||
}
|
||||
|
||||
@@ -124,47 +180,31 @@ namespace Ashwild.Player
|
||||
|
||||
/// <summary>
|
||||
/// Raycasts forward and applies a hit to the harvestable found, or a blocked
|
||||
/// "clunk" when this tool deals no damage to that target type.
|
||||
/// "clunk" when this tool deals no damage to that target type. The hit itself goes through
|
||||
/// Harvestable.ApplyHit (the shared path with bare-hand gathering); the tool is only worn when
|
||||
/// that hit actually connects, never on a swing into the air or a blocked clunk. Triggers are
|
||||
/// included so walkable harvestables (e.g. bushes with a trigger collider) can still be hit.
|
||||
/// </summary>
|
||||
private void TryHarvest()
|
||||
{
|
||||
if (!Physics.Raycast(raycastOrigin.position, raycastOrigin.forward, out RaycastHit hit, harvestRange, harvestMask))
|
||||
if (!Physics.Raycast(raycastOrigin.position, raycastOrigin.forward, out RaycastHit hit,
|
||||
harvestRange, harvestMask, QueryTriggerInteraction.Collide))
|
||||
return;
|
||||
|
||||
Harvestable harvestable = hit.collider.GetComponentInParent<Harvestable>();
|
||||
if (harvestable == null) return;
|
||||
|
||||
Vector3 hitDirection = raycastOrigin.forward;
|
||||
float damage = CalculateDamage(harvestable.HarvestType);
|
||||
|
||||
// No proper tool match: the hit is blocked — no resources, no damage,
|
||||
// just a small "clunk" so it reads as "you need a tool for this".
|
||||
if (damage <= 0f)
|
||||
{
|
||||
harvestable.PlayBlockedFeedback(hitDirection);
|
||||
harvestable.PlayBlockedFeedback();
|
||||
return;
|
||||
}
|
||||
|
||||
if (HarvestableRegistry.Instance == null)
|
||||
{
|
||||
Debug.LogError("[ToolBehaviour] No HarvestableRegistry in the scene — cannot harvest.", this);
|
||||
return;
|
||||
}
|
||||
|
||||
// Already depleted (e.g. someone else just felled it): ignore.
|
||||
if (HarvestableRegistry.Instance.IsInactive(harvestable.Id)) return;
|
||||
|
||||
// Play the hit feedback locally for instant response; the server replays it to others.
|
||||
harvestable.PlayHitEffect(hitDirection);
|
||||
|
||||
// Server applies the damage authoritatively, grants loot, and handles depletion/respawn.
|
||||
ushort toolId = ItemDatabase.Instance != null ? ItemDatabase.Instance.GetId(item) : (ushort)0;
|
||||
HarvestableRegistry.Instance.RequestHitServerRpc(harvestable.Id, damage, toolId, hitDirection);
|
||||
|
||||
PlayerEvents.RaiseHarvestableHit(harvestable, damage);
|
||||
|
||||
// Wear the tool only on a real connecting hit, never on a swing into the air or a blocked clunk.
|
||||
if (PlayerInventory.Instance != null)
|
||||
if (harvestable.ApplyHit(item, damage) && PlayerInventory.Instance != null)
|
||||
PlayerInventory.Instance.ConsumeSelectedToolUse(1);
|
||||
}
|
||||
|
||||
|
||||
@@ -102,13 +102,7 @@ namespace Ashwild.Player
|
||||
return;
|
||||
}
|
||||
|
||||
IInteractable raw = null;
|
||||
if (raycastOrigin != null
|
||||
&& Physics.Raycast(raycastOrigin.position, raycastOrigin.forward, out RaycastHit hit,
|
||||
interactRange, interactMask, QueryTriggerInteraction.Collide))
|
||||
{
|
||||
hit.collider.TryGetComponent(out raw);
|
||||
}
|
||||
IInteractable raw = RaycastInteractable(out _);
|
||||
|
||||
// Restart the stability timer whenever the seen target changes.
|
||||
if (!ReferenceEquals(raw, candidateHover))
|
||||
@@ -152,14 +146,40 @@ namespace Ashwild.Player
|
||||
/// </summary>
|
||||
private void OnInteractPressed()
|
||||
{
|
||||
if (PlayerEvents.InputLocked || raycastOrigin == null) return;
|
||||
if (PlayerEvents.InputLocked) return;
|
||||
|
||||
if (Physics.Raycast(raycastOrigin.position, raycastOrigin.forward, out RaycastHit hit,
|
||||
interactRange, interactMask, QueryTriggerInteraction.Collide)
|
||||
&& hit.collider.TryGetComponent(out IInteractable interactable))
|
||||
IInteractable interactable = RaycastInteractable(out _);
|
||||
if (interactable != null) interactable.Interact();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Interactable Detection
|
||||
|
||||
/// <summary>
|
||||
/// One raycast forward for an interaction target. Returns the first IInteractable under the aim
|
||||
/// ray that actually offers an interaction — a component whose InteractionPrompt is null/empty is
|
||||
/// treated as inert and skipped, so IInteractables that opt out (e.g. a Harvestable with no
|
||||
/// bare-hand gathering) never register as a hover or a valid interact. Triggers are included so
|
||||
/// walkable interactables (bushes with a trigger collider) still respond.
|
||||
///
|
||||
/// The lookup walks up from the collider (as the tool swing does) rather than reading the collider
|
||||
/// alone: an object whose colliders sit on child meshes is perfectly valid, and matching only the
|
||||
/// exact hit object made such an object silently unusable — no prompt, no interaction, no error.
|
||||
/// </summary>
|
||||
private IInteractable RaycastInteractable(out RaycastHit hit)
|
||||
{
|
||||
hit = default;
|
||||
if (raycastOrigin == null) return null;
|
||||
|
||||
if (Physics.Raycast(raycastOrigin.position, raycastOrigin.forward, out hit,
|
||||
interactRange, interactMask, QueryTriggerInteraction.Collide))
|
||||
{
|
||||
interactable.Interact();
|
||||
IInteractable interactable = hit.collider.GetComponentInParent<IInteractable>();
|
||||
if (interactable != null && !string.IsNullOrEmpty(interactable.InteractionPrompt))
|
||||
return interactable;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -175,9 +195,7 @@ namespace Ashwild.Player
|
||||
{
|
||||
Transform o = raycastOrigin != null ? raycastOrigin : transform;
|
||||
|
||||
bool hitInteractable = Physics.Raycast(o.position, o.forward, out RaycastHit hit,
|
||||
interactRange, interactMask, QueryTriggerInteraction.Collide)
|
||||
&& hit.collider.TryGetComponent(out IInteractable _);
|
||||
bool hitInteractable = RaycastInteractable(out RaycastHit hit) != null;
|
||||
|
||||
float length = hitInteractable ? hit.distance : interactRange;
|
||||
Vector3 endPoint = o.position + o.forward * length;
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c6d6c4a9e9b879644a606de6561f30ac
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,254 +0,0 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using Ashwild.Inventory;
|
||||
|
||||
namespace Ashwild.Player
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
public class PlayerToolHolder : MonoBehaviour
|
||||
{
|
||||
// ============================================================
|
||||
// Tunables
|
||||
// ============================================================
|
||||
|
||||
[Header("References")]
|
||||
[SerializeField] private Transform toolHolder;
|
||||
|
||||
[Header("Weapon bob")]
|
||||
[SerializeField] private bool enableWeaponBob = true;
|
||||
[SerializeField] private float bobFrequency = 8f;
|
||||
[SerializeField] private float bobAmplitudeY = 0.04f;
|
||||
[SerializeField] private float bobAmplitudeX = 0.03f;
|
||||
[SerializeField] private float bobSmooth = 8f;
|
||||
[SerializeField] private float sprintBobMultiplier = 1.4f;
|
||||
[SerializeField] private float crouchBobMultiplier = 0.5f;
|
||||
|
||||
[Header("Weapon sway")]
|
||||
[SerializeField] private bool enableWeaponSway = true;
|
||||
[SerializeField] private float swayAmount = 0.02f;
|
||||
[SerializeField] private float swaySmooth = 6f;
|
||||
[SerializeField] private float swayMaxAmount = 0.06f;
|
||||
|
||||
[Header("Idle tilt")]
|
||||
[SerializeField] private bool enableIdleTilt = true;
|
||||
[SerializeField] private float idleTiltFrequency = 0.4f;
|
||||
[SerializeField] private float idleTiltAmplitudeZ = 1.5f;
|
||||
[SerializeField] private float idleTiltAmplitudeX = 0.8f;
|
||||
[SerializeField] private float idleTiltSmooth = 4f;
|
||||
|
||||
[Header("Equip / Unequip")]
|
||||
[SerializeField] private float equipDropOffset = -0.5f;
|
||||
[SerializeField] private float equipTiltAngle = -15f;
|
||||
[SerializeField] private float equipDuration = 0.25f;
|
||||
[SerializeField] private float unequipDuration = 0.12f;
|
||||
|
||||
// ============================================================
|
||||
// Wiring
|
||||
// ============================================================
|
||||
|
||||
// ============================================================
|
||||
// Runtime
|
||||
// ============================================================
|
||||
|
||||
private Vector2 lookInput;
|
||||
private float currentSpeed;
|
||||
private bool isSprinting;
|
||||
private bool isCrouching;
|
||||
private bool isGrounded = true;
|
||||
|
||||
private Vector3 toolHolderBasePos;
|
||||
private Quaternion toolHolderBaseRot;
|
||||
private Vector3 currentSway;
|
||||
private float bobTimer;
|
||||
private float idleTimer;
|
||||
private float currentBobIntensity;
|
||||
|
||||
private float equipAnimProgress = 1f;
|
||||
private float equipAnimTimer;
|
||||
private bool isUnequipping;
|
||||
private float unequipAnimProgress = 1f;
|
||||
private float unequipAnimTimer;
|
||||
private Action onUnequipComplete;
|
||||
|
||||
// ============================================================
|
||||
// Public API (consumed by HotbarController)
|
||||
// ============================================================
|
||||
|
||||
public void PlayEquipAnimation()
|
||||
{
|
||||
equipAnimProgress = 0f;
|
||||
equipAnimTimer = 0f;
|
||||
}
|
||||
|
||||
public void PlayUnequipAnimation(Action onComplete)
|
||||
{
|
||||
isUnequipping = true;
|
||||
unequipAnimProgress = 0f;
|
||||
unequipAnimTimer = 0f;
|
||||
onUnequipComplete = onComplete;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Lifecycle
|
||||
// ============================================================
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (toolHolder != null)
|
||||
{
|
||||
toolHolderBasePos = toolHolder.localPosition;
|
||||
toolHolderBaseRot = toolHolder.localRotation;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
PlayerEvents.LookInput += OnLookInput;
|
||||
PlayerEvents.VelocityChanged += OnVelocityChanged;
|
||||
PlayerEvents.SprintChanged += OnSprintChanged;
|
||||
PlayerEvents.CrouchChanged += OnCrouchChanged;
|
||||
PlayerEvents.GroundedChanged += OnGroundedChanged;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
PlayerEvents.LookInput -= OnLookInput;
|
||||
PlayerEvents.VelocityChanged -= OnVelocityChanged;
|
||||
PlayerEvents.SprintChanged -= OnSprintChanged;
|
||||
PlayerEvents.CrouchChanged -= OnCrouchChanged;
|
||||
PlayerEvents.GroundedChanged -= OnGroundedChanged;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Event handlers
|
||||
// ============================================================
|
||||
|
||||
private void OnLookInput(Vector2 v) => lookInput = v;
|
||||
private void OnVelocityChanged(Vector3 v) => currentSpeed = v.magnitude;
|
||||
private void OnSprintChanged(bool b) => isSprinting = b;
|
||||
private void OnCrouchChanged(bool b) => isCrouching = b;
|
||||
private void OnGroundedChanged(bool grounded, float _) => isGrounded = grounded;
|
||||
|
||||
// ============================================================
|
||||
// Main loop
|
||||
// ============================================================
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
if (toolHolder == null) return;
|
||||
|
||||
bool dead = PlayerEvents.IsDead;
|
||||
if (dead)
|
||||
{
|
||||
currentBobIntensity = 0f;
|
||||
currentSway = Vector3.zero;
|
||||
toolHolder.localPosition = Vector3.Lerp(toolHolder.localPosition, toolHolderBasePos, Time.deltaTime * bobSmooth);
|
||||
toolHolder.localRotation = Quaternion.Slerp(toolHolder.localRotation, toolHolderBaseRot, Time.deltaTime * idleTiltSmooth);
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateBobTimers();
|
||||
UpdateSway();
|
||||
UpdateEquipAnimations();
|
||||
ApplyToolHolderPose();
|
||||
}
|
||||
|
||||
private void UpdateBobTimers()
|
||||
{
|
||||
float dt = Time.deltaTime;
|
||||
float target = (isGrounded && currentSpeed > 0.1f) ? 1f : 0f;
|
||||
currentBobIntensity = Mathf.Lerp(currentBobIntensity, target, dt * 4f);
|
||||
|
||||
if (isGrounded && currentSpeed > 0.1f)
|
||||
bobTimer += dt * bobFrequency * (currentSpeed / 5f);
|
||||
|
||||
if (currentBobIntensity < 0.05f) idleTimer += dt;
|
||||
else idleTimer = 0f;
|
||||
}
|
||||
|
||||
private void UpdateSway()
|
||||
{
|
||||
if (enableWeaponSway)
|
||||
{
|
||||
Vector3 target = new Vector3(-lookInput.x * swayAmount, -lookInput.y * swayAmount, 0f);
|
||||
target.x = Mathf.Clamp(target.x, -swayMaxAmount, swayMaxAmount);
|
||||
target.y = Mathf.Clamp(target.y, -swayMaxAmount, swayMaxAmount);
|
||||
currentSway = Vector3.Lerp(currentSway, target, Time.deltaTime * swaySmooth);
|
||||
}
|
||||
else
|
||||
{
|
||||
currentSway = Vector3.Lerp(currentSway, Vector3.zero, Time.deltaTime * swaySmooth);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateEquipAnimations()
|
||||
{
|
||||
if (isUnequipping)
|
||||
{
|
||||
unequipAnimTimer += Time.deltaTime;
|
||||
float t = Mathf.Clamp01(unequipAnimTimer / unequipDuration);
|
||||
unequipAnimProgress = t * t;
|
||||
if (t >= 1f)
|
||||
{
|
||||
isUnequipping = false;
|
||||
PlayerEvents.RaiseUnequipAnimComplete();
|
||||
onUnequipComplete?.Invoke();
|
||||
onUnequipComplete = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (equipAnimTimer < equipDuration)
|
||||
{
|
||||
equipAnimTimer += Time.deltaTime;
|
||||
float t = Mathf.Clamp01(equipAnimTimer / equipDuration);
|
||||
const float c1 = 1.70158f;
|
||||
const float c3 = c1 + 1f;
|
||||
equipAnimProgress = 1f + c3 * Mathf.Pow(t - 1f, 3f) + c1 * Mathf.Pow(t - 1f, 2f);
|
||||
if (t >= 1f) PlayerEvents.RaiseEquipAnimComplete();
|
||||
}
|
||||
else
|
||||
{
|
||||
equipAnimProgress = 1f;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyToolHolderPose()
|
||||
{
|
||||
Vector3 targetPos = toolHolderBasePos;
|
||||
float mult = isCrouching ? crouchBobMultiplier : (isSprinting ? sprintBobMultiplier : 1f);
|
||||
|
||||
if (enableWeaponBob)
|
||||
{
|
||||
targetPos.y += Mathf.Sin(bobTimer) * bobAmplitudeY * mult * currentBobIntensity;
|
||||
targetPos.x += Mathf.Cos(bobTimer * 0.5f) * bobAmplitudeX * mult * currentBobIntensity;
|
||||
}
|
||||
targetPos += currentSway;
|
||||
|
||||
bool isAnimating = isUnequipping || equipAnimProgress < 1f;
|
||||
float animFactor = isUnequipping ? unequipAnimProgress : (1f - equipAnimProgress);
|
||||
|
||||
if (isAnimating)
|
||||
{
|
||||
targetPos.y += animFactor * equipDropOffset;
|
||||
toolHolder.localPosition = targetPos;
|
||||
}
|
||||
else
|
||||
{
|
||||
toolHolder.localPosition = Vector3.Lerp(toolHolder.localPosition, targetPos, Time.deltaTime * bobSmooth);
|
||||
}
|
||||
|
||||
if (enableIdleTilt || isAnimating)
|
||||
{
|
||||
float idleWeight = 1f - currentBobIntensity;
|
||||
float phase = idleTimer * idleTiltFrequency * Mathf.PI * 2f;
|
||||
float tiltZ = Mathf.Sin(phase) * idleTiltAmplitudeZ * idleWeight;
|
||||
float tiltX = Mathf.Cos(phase * 0.6f) * idleTiltAmplitudeX * idleWeight;
|
||||
if (isAnimating) tiltZ += animFactor * equipTiltAngle;
|
||||
|
||||
Quaternion targetTilt = toolHolderBaseRot * Quaternion.Euler(tiltX, 0f, tiltZ);
|
||||
if (isAnimating) toolHolder.localRotation = targetTilt;
|
||||
else toolHolder.localRotation = Quaternion.Slerp(toolHolder.localRotation, targetTilt, Time.deltaTime * idleTiltSmooth);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b8c9d0e1f20314255567890123456abc
|
||||
Reference in New Issue
Block a user