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
@@ -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);
}