Added Furnance and smelting
This commit is contained in:
@@ -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();
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 41af49db79eeaf84ea8bc7c3dd75ae0f
|
||||
@@ -0,0 +1,88 @@
|
||||
using UnityEngine;
|
||||
|
||||
public sealed class FurnaceMetalLevelView : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private FurnaceController furnace;
|
||||
[SerializeField] private Transform metalVisual;
|
||||
[SerializeField] private Transform emptyLocalPosition;
|
||||
[SerializeField] private Transform fullLocalPosition;
|
||||
[SerializeField, Min(0f)] private float movementSpeed = 1.5f;
|
||||
|
||||
private Vector3 targetLocalPosition;
|
||||
|
||||
private void Reset()
|
||||
{
|
||||
furnace = GetComponentInParent<FurnaceController>();
|
||||
metalVisual = transform;
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (furnace == null)
|
||||
furnace = GetComponentInParent<FurnaceController>();
|
||||
|
||||
if (furnace != null)
|
||||
furnace.Changed += RefreshTarget;
|
||||
|
||||
RefreshTarget();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (furnace != null)
|
||||
furnace.Changed -= RefreshTarget;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (metalVisual == null)
|
||||
return;
|
||||
|
||||
metalVisual.localPosition = movementSpeed <= 0f
|
||||
? targetLocalPosition
|
||||
: Vector3.MoveTowards(
|
||||
metalVisual.localPosition,
|
||||
targetLocalPosition,
|
||||
movementSpeed * Time.deltaTime);
|
||||
}
|
||||
|
||||
private void RefreshTarget()
|
||||
{
|
||||
float normalized = furnace != null ? furnace.MetalNormalized : 0f;
|
||||
targetLocalPosition = Vector3.Lerp(
|
||||
GetLocalPoint(emptyLocalPosition),
|
||||
GetLocalPoint(fullLocalPosition),
|
||||
normalized);
|
||||
}
|
||||
|
||||
private Vector3 GetLocalPoint(Transform point)
|
||||
{
|
||||
if (point == null || metalVisual == null)
|
||||
return metalVisual != null ? metalVisual.localPosition : Vector3.zero;
|
||||
|
||||
Transform space = metalVisual.parent;
|
||||
return space != null
|
||||
? space.InverseTransformPoint(point.position)
|
||||
: point.position;
|
||||
}
|
||||
|
||||
[ContextMenu("Preview/Empty")]
|
||||
private void PreviewEmpty() => ApplyPreview(0f);
|
||||
|
||||
[ContextMenu("Preview/Half")]
|
||||
private void PreviewHalf() => ApplyPreview(0.5f);
|
||||
|
||||
[ContextMenu("Preview/Full")]
|
||||
private void PreviewFull() => ApplyPreview(1f);
|
||||
|
||||
private void ApplyPreview(float normalized)
|
||||
{
|
||||
if (metalVisual == null)
|
||||
return;
|
||||
|
||||
metalVisual.localPosition = Vector3.Lerp(
|
||||
GetLocalPoint(emptyLocalPosition),
|
||||
GetLocalPoint(fullLocalPosition),
|
||||
normalized);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d51e4e087bfc8f14d8df1b4da69a6d56
|
||||
@@ -0,0 +1,31 @@
|
||||
using UnityEngine;
|
||||
|
||||
[DisallowMultipleComponent]
|
||||
[RequireComponent(typeof(Collider))]
|
||||
public sealed class FurnaceScrapReceiver : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private FurnaceController furnace;
|
||||
|
||||
private void Reset()
|
||||
{
|
||||
furnace = GetComponentInParent<FurnaceController>();
|
||||
Collider receiverCollider = GetComponent<Collider>();
|
||||
receiverCollider.isTrigger = true;
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (furnace == null)
|
||||
furnace = GetComponentInParent<FurnaceController>();
|
||||
}
|
||||
|
||||
private void OnTriggerEnter(Collider other)
|
||||
{
|
||||
if (furnace == null || !furnace.IsServer || !furnace.IsSpawned)
|
||||
return;
|
||||
|
||||
FurnaceMetalSource source = other.GetComponentInParent<FurnaceMetalSource>();
|
||||
if (source != null)
|
||||
furnace.TryConsumeMetalSource(source);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7a72230fcd966f84d967ab39b124447d
|
||||
@@ -0,0 +1,25 @@
|
||||
using UnityEngine;
|
||||
|
||||
[CreateAssetMenu(fileName = "SmeltingRecipe", menuName = "FORT-BO/Workstations/Smelting Recipe")]
|
||||
public sealed class SmeltingRecipe : ScriptableObject
|
||||
{
|
||||
[SerializeField] private string recipeId;
|
||||
[SerializeField] private string displayName;
|
||||
[SerializeField] private ItemDefinition outputItem;
|
||||
[SerializeField, Min(1)] private int outputAmount = 1;
|
||||
[SerializeField, Min(0.01f)] private float metalCost = 25f;
|
||||
[SerializeField, Min(0.05f)] private float processingDuration = 3f;
|
||||
|
||||
public string RecipeId => recipeId;
|
||||
public string DisplayName => string.IsNullOrWhiteSpace(displayName) ? name : displayName;
|
||||
public ItemDefinition OutputItem => outputItem;
|
||||
public int OutputAmount => outputAmount;
|
||||
public float MetalCost => metalCost;
|
||||
public float ProcessingDuration => processingDuration;
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
recipeId = recipeId?.Trim().ToLowerInvariant();
|
||||
displayName = displayName?.Trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dbdeccc1573d9204f858f7c114972d91
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 46d9fb1e1a0fed4479d8298a3ccff544
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,72 @@
|
||||
using UnityEngine;
|
||||
|
||||
public sealed class FurnacePanelUIController : MonoBehaviour, IGlobalUISectionController
|
||||
{
|
||||
[SerializeField] private FurnaceController furnace;
|
||||
[SerializeField] private FurnacePanelUIView view;
|
||||
|
||||
private void Reset()
|
||||
{
|
||||
furnace = GetComponentInParent<FurnaceController>();
|
||||
view = GetComponent<FurnacePanelUIView>();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (furnace == null)
|
||||
furnace = GetComponentInParent<FurnaceController>();
|
||||
if (view == null)
|
||||
view = GetComponent<FurnacePanelUIView>();
|
||||
|
||||
if (furnace != null)
|
||||
furnace.Changed += Refresh;
|
||||
|
||||
GlobalUIController.Instance?.Register(this);
|
||||
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (furnace != null)
|
||||
furnace.Changed -= Refresh;
|
||||
|
||||
GlobalUIController.Instance?.Unregister(this);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (furnace != null && furnace.IsOperating)
|
||||
Refresh();
|
||||
}
|
||||
|
||||
public void SelectRecipe(int recipeIndex)
|
||||
{
|
||||
furnace?.SelectRecipe(recipeIndex);
|
||||
}
|
||||
|
||||
public void SelectAndStartRecipe(int recipeIndex)
|
||||
{
|
||||
furnace?.SelectAndStartRecipe(recipeIndex);
|
||||
}
|
||||
|
||||
public void StartSmelting()
|
||||
{
|
||||
furnace?.StartSmelting();
|
||||
}
|
||||
|
||||
public void Refresh()
|
||||
{
|
||||
if (furnace == null || view == null)
|
||||
return;
|
||||
|
||||
view.Render(
|
||||
furnace.StoredMetal,
|
||||
furnace.MetalCapacity,
|
||||
furnace.SelectedRecipe,
|
||||
furnace.State,
|
||||
furnace.ProcessProgress,
|
||||
furnace.RemainingSeconds,
|
||||
furnace.CanStartSelectedRecipe);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6cce45d5e80938340961575a1e91eb41
|
||||
@@ -0,0 +1,51 @@
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
public sealed class FurnacePanelUIView : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private TMP_Text metalText;
|
||||
[SerializeField] private TMP_Text selectedRecipeText;
|
||||
[SerializeField] private TMP_Text statusText;
|
||||
[SerializeField] private TMP_Text remainingTimeText;
|
||||
[SerializeField] private Image metalImage;
|
||||
[SerializeField] private Image progressImage;
|
||||
[SerializeField] private Button startButton;
|
||||
|
||||
public void Render(
|
||||
float storedMetal,
|
||||
float metalCapacity,
|
||||
SmeltingRecipe selectedRecipe,
|
||||
FurnaceState state,
|
||||
float processProgress,
|
||||
float remainingSeconds,
|
||||
bool canStart)
|
||||
{
|
||||
if (metalText != null)
|
||||
metalText.text = $"METAL {storedMetal:0.#} / {metalCapacity:0.#}";
|
||||
|
||||
if (metalImage != null)
|
||||
metalImage.fillAmount = metalCapacity > 0f ? storedMetal / metalCapacity : 0f;
|
||||
|
||||
if (selectedRecipeText != null)
|
||||
{
|
||||
selectedRecipeText.text = selectedRecipe != null
|
||||
? $"{selectedRecipe.DisplayName} | {selectedRecipe.MetalCost:0.#} metal"
|
||||
: "NO RECIPE";
|
||||
}
|
||||
|
||||
if (statusText != null)
|
||||
statusText.text = state == FurnaceState.Smelting ? "SMELTING" : "READY";
|
||||
|
||||
if (progressImage != null)
|
||||
progressImage.fillAmount = processProgress;
|
||||
|
||||
if (remainingTimeText != null)
|
||||
remainingTimeText.text = state == FurnaceState.Smelting
|
||||
? $"{remainingSeconds:0.0}s"
|
||||
: string.Empty;
|
||||
|
||||
if (startButton != null)
|
||||
startButton.interactable = canStart;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 871599a5b7d016b45a74a2005a6e22e5
|
||||
Reference in New Issue
Block a user