Added Furnance and smelting

This commit is contained in:
2026-08-11 17:12:34 +03:00
parent 2a30d4d8cf
commit 67e0bcfaaf
82 changed files with 25148 additions and 7186 deletions
@@ -0,0 +1,291 @@
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<SmeltingRecipe> recipes = new();
[SerializeField, Min(0f)] private float outputSpacing = 0.35f;
private readonly NetworkVariable<float> storedMetal = new(
0f,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private readonly NetworkVariable<int> selectedRecipeIndex = new(
-1,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private readonly NetworkVariable<FurnaceState> furnaceState = new(
FurnaceState.Idle,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private readonly NetworkVariable<double> processStartedAt = new(
0d,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private readonly NetworkVariable<double> 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<SmeltingRecipe> 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<NetworkItem>();
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();
}