127 lines
4.2 KiB
C#
127 lines
4.2 KiB
C#
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using FishNet.Connection;
|
|
using FishNet.Object;
|
|
using UnityEngine;
|
|
using Ashwild.Harvesting;
|
|
using Ashwild.Inventory;
|
|
|
|
namespace Ashwild.Network
|
|
{
|
|
/// <summary>
|
|
/// 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).
|
|
/// </summary>
|
|
public class HarvestableRegistry : WorldObjectRegistry
|
|
{
|
|
#region Singleton
|
|
|
|
/// <summary>
|
|
/// The harvestable registry for the active game scene.
|
|
/// </summary>
|
|
public static HarvestableRegistry Instance { get; private set; }
|
|
|
|
#endregion
|
|
|
|
#region State
|
|
|
|
/// <summary>
|
|
/// Authoritative current health per harvestable id (server only, lazy-initialised to MaxHealth).
|
|
/// </summary>
|
|
private readonly Dictionary<int, float> health = new Dictionary<int, float>();
|
|
|
|
#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
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
[ServerRpc(RequireOwnership = false)]
|
|
public void RequestHitServerRpc(int id, float damage, ushort toolItemId, Vector3 hitDirection, NetworkConnection conn = null)
|
|
{
|
|
if (IsInactive(id)) return;
|
|
if (!TryGetObject(id, out WorldObject obj) || obj is not Harvestable harvestable) return;
|
|
|
|
if (!health.TryGetValue(id, out float hp)) hp = harvestable.MaxHealth;
|
|
hp -= damage;
|
|
health[id] = hp;
|
|
|
|
// Grant the rolled loot to the hitting player.
|
|
ItemData tool = ItemDatabase.Instance != null ? ItemDatabase.Instance.GetItem(toolItemId) : null;
|
|
PlayerInventory inventory = ResolveInventory(conn);
|
|
if (inventory != 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, hitDirection, hitterClientId);
|
|
|
|
if (hp <= 0f)
|
|
{
|
|
health.Remove(id);
|
|
MarkInactive(id);
|
|
if (harvestable.CanRespawn)
|
|
StartCoroutine(RespawnRoutine(id, harvestable.RespawnDelay));
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Network Feedback
|
|
|
|
/// <summary>
|
|
/// Plays the hit feedback on every client except the one who threw the hit.
|
|
/// </summary>
|
|
[ObserversRpc]
|
|
private void PlayHitObserversRpc(int id, Vector3 hitDirection, int hitterClientId)
|
|
{
|
|
if (LocalConnection != null && LocalConnection.ClientId == hitterClientId) return;
|
|
if (TryGetObject(id, out WorldObject obj) && obj is Harvestable harvestable)
|
|
harvestable.PlayHitEffect(hitDirection);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Internal Helpers
|
|
|
|
/// <summary>
|
|
/// Brings a depleted harvestable back after its respawn delay (server-side).
|
|
/// </summary>
|
|
private IEnumerator RespawnRoutine(int id, float delay)
|
|
{
|
|
yield return new WaitForSeconds(delay);
|
|
MarkActive(id);
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|