using System.Collections; using System.Collections.Generic; using FishNet.Connection; using FishNet.Object; using UnityEngine; using Ashwild.Harvesting; using Ashwild.Inventory; namespace Ashwild.Network { /// /// Registry for Harvestables. On top of the shared WorldObjectRegistry plumbing (depleted ids, /// hide/show, catch-up) it owns the authoritative, server-side health per object, applies hits, /// grants the rolled loot to the hitting player, broadcasts the hit feedback to everyone, and /// schedules respawns. One per scene. Damage is computed client-side (client-authoritative, co-op). /// public class HarvestableRegistry : WorldObjectRegistry { #region Singleton /// /// The harvestable registry for the active game scene. /// public static HarvestableRegistry Instance { get; private set; } #endregion #region State /// /// Authoritative current health per harvestable id (server only, lazy-initialised to MaxHealth). /// private readonly Dictionary health = new Dictionary(); #endregion #region Lifecycle protected override void Awake() { base.Awake(); if (Instance != null && Instance != this) { Destroy(this); return; } Instance = this; } protected override void OnDestroy() { base.OnDestroy(); if (Instance == this) Instance = null; } #endregion #region Public API /// /// Owner-side entry: tells the server the local player hit a harvestable. The server applies /// the (client-computed) damage, grants loot, broadcasts feedback, and handles depletion. /// /// The two ways this can go wrong are reported back to the requesting player instead of being /// swallowed: an id the server does not track (a scene object with a missing or duplicate id) and /// a connection whose inventory cannot be resolved. Both used to end in a bare `return` — the /// player saw their hit land and gained nothing, with no trace anywhere on their machine. Losing /// the race against another player is the one case left unreported, since the object disappearing /// says it plainly enough. /// [ServerRpc(RequireOwnership = false)] public void RequestHitServerRpc(int id, float damage, ushort toolItemId, NetworkConnection conn = null) { if (IsInactive(id)) return; if (!TryGetObject(id, out WorldObject obj) || obj is not Harvestable harvestable) { ReportFailure(conn, id, "no harvestable is registered with this id on the server"); return; } PlayerInventory inventory = ResolveInventory(conn); if (inventory == null) { ReportFailure(conn, id, "the requesting player's inventory could not be resolved — the loot would be lost"); return; } if (!health.TryGetValue(id, out float hp)) hp = harvestable.MaxHealth; hp -= damage; health[id] = hp; ItemData tool = ItemDatabase.Instance != null ? ItemDatabase.Instance.GetItem(toolItemId) : null; foreach ((ItemData item, int quantity) in harvestable.RollDrops(tool)) inventory.GrantItemFromServer(item, quantity); // Feedback for everyone except the hitter (who already played it locally for responsiveness). int hitterClientId = conn != null ? conn.ClientId : -1; PlayHitObserversRpc(id, hitterClientId); if (hp <= 0f) { health.Remove(id); MarkInactive(id); if (harvestable.CanRespawn) StartCoroutine(RespawnRoutine(id, harvestable.RespawnDelay)); } } #endregion #region Network Feedback /// /// Plays the hit feedback on every client except the one who threw the hit. /// [ObserversRpc] private void PlayHitObserversRpc(int id, int hitterClientId) { if (LocalConnection != null && LocalConnection.ClientId == hitterClientId) return; if (TryGetObject(id, out WorldObject obj) && obj is Harvestable harvestable) harvestable.PlayHitEffect(); } #endregion #region Internal Helpers /// /// Brings a depleted harvestable back after its respawn delay (server-side). /// private IEnumerator RespawnRoutine(int id, float delay) { yield return new WaitForSeconds(delay); MarkActive(id); } #endregion } }