using System; using System.Collections.Generic; using Unity.Netcode; using UnityEngine; public enum FurnaceState : byte { Idle, Smelting } public sealed class FurnaceController : NetworkWorkstationController { [Header("Storage")] [SerializeField, Min(1f)] private float metalCapacity = 100f; [Header("Production")] [SerializeField] private Transform outputPoint; [SerializeField] private List recipes = new(); [SerializeField, Min(0f)] private float outputSpacing = 0.35f; private readonly NetworkVariable storedMetal = new( 0f, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server); private readonly NetworkVariable selectedRecipeIndex = new( -1, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server); private readonly NetworkVariable furnaceState = new( FurnaceState.Idle, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server); private readonly NetworkVariable processStartedAt = new( 0d, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server); private readonly NetworkVariable processEndsAt = new( 0d, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server); public override bool IsOperating => furnaceState.Value == FurnaceState.Smelting; public FurnaceState State => furnaceState.Value; public float StoredMetal => storedMetal.Value; public float MetalCapacity => metalCapacity; public float MetalNormalized => metalCapacity > 0f ? storedMetal.Value / metalCapacity : 0f; public int SelectedRecipeIndex => selectedRecipeIndex.Value; public IReadOnlyList Recipes => recipes; public SmeltingRecipe SelectedRecipe => TryGetRecipe(selectedRecipeIndex.Value, out SmeltingRecipe recipe) ? recipe : null; public bool CanStartSelectedRecipe => !IsOperating && SelectedRecipe != null && SelectedRecipe.OutputItem != null && SelectedRecipe.OutputItem.WorldPrefab != null && storedMetal.Value >= SelectedRecipe.MetalCost; public float ProcessProgress { get { if (!IsOperating) return 0f; double duration = processEndsAt.Value - processStartedAt.Value; if (duration <= 0d) return 1f; double now = NetworkManager != null ? NetworkManager.ServerTime.Time : Time.timeAsDouble; return Mathf.Clamp01((float)((now - processStartedAt.Value) / duration)); } } public float RemainingSeconds { get { if (!IsOperating) return 0f; double now = NetworkManager != null ? NetworkManager.ServerTime.Time : Time.timeAsDouble; return Mathf.Max(0f, (float)(processEndsAt.Value - now)); } } public event Action Changed; public override void OnNetworkSpawn() { storedMetal.OnValueChanged += HandleStoredMetalChanged; selectedRecipeIndex.OnValueChanged += HandleSelectedRecipeChanged; furnaceState.OnValueChanged += HandleStateChanged; processStartedAt.OnValueChanged += HandleProcessTimeChanged; processEndsAt.OnValueChanged += HandleProcessTimeChanged; if (IsServer) { storedMetal.Value = Mathf.Clamp(storedMetal.Value, 0f, metalCapacity); selectedRecipeIndex.Value = recipes.Count > 0 ? 0 : -1; furnaceState.Value = FurnaceState.Idle; processStartedAt.Value = 0d; processEndsAt.Value = 0d; } Changed?.Invoke(); } public override void OnNetworkDespawn() { storedMetal.OnValueChanged -= HandleStoredMetalChanged; selectedRecipeIndex.OnValueChanged -= HandleSelectedRecipeChanged; furnaceState.OnValueChanged -= HandleStateChanged; processStartedAt.OnValueChanged -= HandleProcessTimeChanged; processEndsAt.OnValueChanged -= HandleProcessTimeChanged; } private void Update() { if (!IsServer || !IsSpawned || !IsOperating) return; if (NetworkManager.ServerTime.Time >= processEndsAt.Value) CompleteSmeltingOnServer(); } public bool TryConsumeMetalSource(FurnaceMetalSource source) { if (!IsServer || !IsSpawned || source == null || storedMetal.Value >= metalCapacity) return false; NetworkItem item = source.GetComponent(); if (item == null || !item.IsSpawned || item.NetworkObject == null) return false; if (source.TryGetComponent(out NetworkHeldInteractable heldObject) && heldObject.IsHeld) return false; if (source.TryGetComponent(out NetworkHandItemInteractable handItem) && handItem.IsHeld) return false; if (!source.TryMarkConsumed()) return false; storedMetal.Value = Mathf.Min(metalCapacity, storedMetal.Value + source.MetalAmount); item.NetworkObject.Despawn(true); return true; } public bool TryAddMetal(float amount) { if (!IsServer || !IsSpawned || amount <= 0f || storedMetal.Value >= metalCapacity) return false; storedMetal.Value = Mathf.Min(metalCapacity, storedMetal.Value + amount); return true; } public void SelectRecipe(int recipeIndex) { if (!IsSpawned) return; if (IsServer) SelectRecipeOnServer(recipeIndex); else RequestSelectRecipeRpc(recipeIndex); } public void StartSmelting() { if (!IsSpawned) return; if (IsServer) TryStartSmeltingOnServer(); else RequestStartSmeltingRpc(); } public void SelectAndStartRecipe(int recipeIndex) { if (!IsSpawned) return; if (IsServer) { if (SelectRecipeOnServer(recipeIndex)) TryStartSmeltingOnServer(); } else { RequestSelectAndStartRecipeRpc(recipeIndex); } } [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)] private void RequestSelectRecipeRpc(int recipeIndex) => SelectRecipeOnServer(recipeIndex); [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)] private void RequestStartSmeltingRpc() => TryStartSmeltingOnServer(); [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)] private void RequestSelectAndStartRecipeRpc(int recipeIndex) { if (SelectRecipeOnServer(recipeIndex)) TryStartSmeltingOnServer(); } private bool SelectRecipeOnServer(int recipeIndex) { if (!IsServer || IsOperating || !TryGetRecipe(recipeIndex, out _)) return false; selectedRecipeIndex.Value = recipeIndex; return true; } private bool TryStartSmeltingOnServer() { if (!IsServer || !CanStartSelectedRecipe) return false; SmeltingRecipe recipe = SelectedRecipe; storedMetal.Value -= recipe.MetalCost; double now = NetworkManager.ServerTime.Time; processStartedAt.Value = now; processEndsAt.Value = now + recipe.ProcessingDuration; furnaceState.Value = FurnaceState.Smelting; return true; } private void CompleteSmeltingOnServer() { if (!TryGetRecipe(selectedRecipeIndex.Value, out SmeltingRecipe recipe) || recipe.OutputItem == null || recipe.OutputItem.WorldPrefab == null) { Debug.LogError($"{name} cannot complete smelting: output prefab is missing.", this); StopProcessingOnServer(); return; } Transform spawnTransform = outputPoint != null ? outputPoint : transform; for (int index = 0; index < recipe.OutputAmount; index++) { Vector3 sideOffset = spawnTransform.right * outputSpacing * index; NetworkObject output = Instantiate( recipe.OutputItem.WorldPrefab, spawnTransform.position + sideOffset, spawnTransform.rotation); output.Spawn(); } StopProcessingOnServer(); } private void StopProcessingOnServer() { furnaceState.Value = FurnaceState.Idle; processStartedAt.Value = 0d; processEndsAt.Value = 0d; } private bool TryGetRecipe(int index, out SmeltingRecipe recipe) { bool isValid = index >= 0 && index < recipes.Count && recipes[index] != null; recipe = isValid ? recipes[index] : null; return isValid; } private void HandleStoredMetalChanged(float _, float __) => Changed?.Invoke(); private void HandleSelectedRecipeChanged(int _, int __) => Changed?.Invoke(); private void HandleStateChanged(FurnaceState _, FurnaceState __) => Changed?.Invoke(); private void HandleProcessTimeChanged(double _, double __) => Changed?.Invoke(); [ContextMenu("Debug/Add 25 Metal (Server Play Mode)")] private void DebugAddMetal() => TryAddMetal(25f); [ContextMenu("Debug/Start Selected Recipe (Server Play Mode)")] private void DebugStartSelectedRecipe() => StartSmelting(); }