From f41e14971aa081a2ae99d4fc1eda195e9a26e438 Mon Sep 17 00:00:00 2001 From: AsanoRema Date: Thu, 13 Aug 2026 08:33:12 +0300 Subject: [PATCH] Added new stanok --- .../Game/Scripts/Workstations/Processing.meta | 8 + .../Processing/ItemProcessingRecipe.cs | 29 +++ .../Processing/ItemProcessingRecipe.cs.meta | 11 + .../ItemProcessingWorkstationView.cs | 124 +++++++++ .../ItemProcessingWorkstationView.cs.meta | 11 + .../Processing/NetworkItemInputSlot.cs | 79 ++++++ .../Processing/NetworkItemInputSlot.cs.meta | 11 + .../NetworkItemProcessingWorkstation.cs | 236 ++++++++++++++++++ .../NetworkItemProcessingWorkstation.cs.meta | 11 + 9 files changed, 520 insertions(+) create mode 100644 Assets/CONTENT/Game/Scripts/Workstations/Processing.meta create mode 100644 Assets/CONTENT/Game/Scripts/Workstations/Processing/ItemProcessingRecipe.cs create mode 100644 Assets/CONTENT/Game/Scripts/Workstations/Processing/ItemProcessingRecipe.cs.meta create mode 100644 Assets/CONTENT/Game/Scripts/Workstations/Processing/ItemProcessingWorkstationView.cs create mode 100644 Assets/CONTENT/Game/Scripts/Workstations/Processing/ItemProcessingWorkstationView.cs.meta create mode 100644 Assets/CONTENT/Game/Scripts/Workstations/Processing/NetworkItemInputSlot.cs create mode 100644 Assets/CONTENT/Game/Scripts/Workstations/Processing/NetworkItemInputSlot.cs.meta create mode 100644 Assets/CONTENT/Game/Scripts/Workstations/Processing/NetworkItemProcessingWorkstation.cs create mode 100644 Assets/CONTENT/Game/Scripts/Workstations/Processing/NetworkItemProcessingWorkstation.cs.meta diff --git a/Assets/CONTENT/Game/Scripts/Workstations/Processing.meta b/Assets/CONTENT/Game/Scripts/Workstations/Processing.meta new file mode 100644 index 0000000..643fcd8 --- /dev/null +++ b/Assets/CONTENT/Game/Scripts/Workstations/Processing.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1d3dd768e3784bc6bc270a1ce57ce345 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/CONTENT/Game/Scripts/Workstations/Processing/ItemProcessingRecipe.cs b/Assets/CONTENT/Game/Scripts/Workstations/Processing/ItemProcessingRecipe.cs new file mode 100644 index 0000000..2df85dc --- /dev/null +++ b/Assets/CONTENT/Game/Scripts/Workstations/Processing/ItemProcessingRecipe.cs @@ -0,0 +1,29 @@ +using UnityEngine; + +[CreateAssetMenu( + fileName = "ItemProcessingRecipe", + menuName = "FORT-BO/Workstations/Item Processing Recipe")] +public sealed class ItemProcessingRecipe : ScriptableObject +{ + [SerializeField] private string recipeId; + [SerializeField] private ItemDefinition inputItem; + [SerializeField] private ItemDefinition outputItem; + [SerializeField, Min(0.05f)] private float processingDuration = 3f; + + public string RecipeId => recipeId; + public ItemDefinition InputItem => inputItem; + public ItemDefinition OutputItem => outputItem; + public float ProcessingDuration => processingDuration; + + public bool Accepts(NetworkItem item) + { + if (item == null || item.Definition == null || inputItem == null) + return false; + + return item.Definition == inputItem || + (!string.IsNullOrEmpty(inputItem.ItemId) && item.ItemId == inputItem.ItemId); + } + + private void OnValidate() => + recipeId = recipeId?.Trim().ToLowerInvariant(); +} diff --git a/Assets/CONTENT/Game/Scripts/Workstations/Processing/ItemProcessingRecipe.cs.meta b/Assets/CONTENT/Game/Scripts/Workstations/Processing/ItemProcessingRecipe.cs.meta new file mode 100644 index 0000000..d5fbc24 --- /dev/null +++ b/Assets/CONTENT/Game/Scripts/Workstations/Processing/ItemProcessingRecipe.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ea07d1502f6f4abc96ea949c7e688857 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/CONTENT/Game/Scripts/Workstations/Processing/ItemProcessingWorkstationView.cs b/Assets/CONTENT/Game/Scripts/Workstations/Processing/ItemProcessingWorkstationView.cs new file mode 100644 index 0000000..60b055d --- /dev/null +++ b/Assets/CONTENT/Game/Scripts/Workstations/Processing/ItemProcessingWorkstationView.cs @@ -0,0 +1,124 @@ +using UnityEngine; +using UnityEngine.Events; + +[DisallowMultipleComponent] +public class ItemProcessingWorkstationView : MonoBehaviour +{ + [SerializeField] private NetworkItemProcessingWorkstation workstation; + + [Header("State Events")] + [SerializeField] private UnityEvent onEmpty; + [SerializeField] private UnityEvent onProcessingStarted; + [SerializeField] private UnityEvent onOutputReady; + + [Header("Continuous Progress")] + [SerializeField] private UnityEvent onProcessingProgress; + + private ItemProcessingWorkstationState previousState; + private bool hasAppliedState; + + public NetworkItemProcessingWorkstation Workstation => workstation; + + protected virtual void Reset() => + workstation = GetComponentInParent(); + + protected virtual void Awake() + { + if (workstation == null) + workstation = GetComponentInParent(); + } + + protected virtual void OnEnable() + { + if (workstation != null) + { + workstation.Changed += HandleWorkstationChanged; + ApplyState(); + } + } + + protected virtual void Start() => ApplyState(); + + protected virtual void OnDisable() + { + if (workstation != null) + workstation.Changed -= HandleWorkstationChanged; + + hasAppliedState = false; + } + + protected virtual void Update() + { + if (workstation != null && + workstation.State == ItemProcessingWorkstationState.Processing) + { + onProcessingProgress?.Invoke(workstation.ProcessProgress); + } + } + + protected virtual void HandleStateApplied(ItemProcessingWorkstationState state) + { + } + + [ContextMenu("Preview/Empty")] + private void PreviewEmpty() + { + onProcessingProgress?.Invoke(0f); + onEmpty?.Invoke(); + HandleStateApplied(ItemProcessingWorkstationState.Empty); + } + + [ContextMenu("Preview/Processing Started")] + private void PreviewProcessingStarted() + { + onProcessingProgress?.Invoke(0f); + onProcessingStarted?.Invoke(); + HandleStateApplied(ItemProcessingWorkstationState.Processing); + } + + [ContextMenu("Preview/Processing 50 Percent")] + private void PreviewProcessingHalfway() => onProcessingProgress?.Invoke(0.5f); + + [ContextMenu("Preview/Output Ready")] + private void PreviewOutputReady() + { + onProcessingProgress?.Invoke(1f); + onOutputReady?.Invoke(); + HandleStateApplied(ItemProcessingWorkstationState.OutputReady); + } + + private void HandleWorkstationChanged() => ApplyState(); + + private void ApplyState() + { + if (workstation == null || !workstation.IsSpawned) + return; + + ItemProcessingWorkstationState currentState = workstation.State; + if (hasAppliedState && currentState == previousState) + return; + + previousState = currentState; + hasAppliedState = true; + + switch (currentState) + { + case ItemProcessingWorkstationState.Processing: + onProcessingStarted?.Invoke(); + onProcessingProgress?.Invoke(workstation.ProcessProgress); + break; + + case ItemProcessingWorkstationState.OutputReady: + onProcessingProgress?.Invoke(1f); + onOutputReady?.Invoke(); + break; + + default: + onProcessingProgress?.Invoke(0f); + onEmpty?.Invoke(); + break; + } + + HandleStateApplied(currentState); + } +} diff --git a/Assets/CONTENT/Game/Scripts/Workstations/Processing/ItemProcessingWorkstationView.cs.meta b/Assets/CONTENT/Game/Scripts/Workstations/Processing/ItemProcessingWorkstationView.cs.meta new file mode 100644 index 0000000..05e899b --- /dev/null +++ b/Assets/CONTENT/Game/Scripts/Workstations/Processing/ItemProcessingWorkstationView.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fddbd9f473234f27b85054ee55b89f69 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/CONTENT/Game/Scripts/Workstations/Processing/NetworkItemInputSlot.cs b/Assets/CONTENT/Game/Scripts/Workstations/Processing/NetworkItemInputSlot.cs new file mode 100644 index 0000000..72e775a --- /dev/null +++ b/Assets/CONTENT/Game/Scripts/Workstations/Processing/NetworkItemInputSlot.cs @@ -0,0 +1,79 @@ +using System.Collections.Generic; +using UnityEngine; + +[DisallowMultipleComponent] +[RequireComponent(typeof(Collider))] +public sealed class NetworkItemInputSlot : MonoBehaviour +{ + [SerializeField] private NetworkItemProcessingWorkstation workstation; + + private readonly Dictionary> overlaps = new(); + + private void Reset() + { + workstation = GetComponentInParent(); + GetComponent().isTrigger = true; + } + + private void Awake() + { + if (workstation == null) + workstation = GetComponentInParent(); + } + + private void OnDisable() => overlaps.Clear(); + + private void OnTriggerEnter(Collider other) + { + RegisterOverlap(other); + TryAccept(other); + } + + private void OnTriggerStay(Collider other) + { + RegisterOverlap(other); + TryAccept(other); + } + + private void OnTriggerExit(Collider other) + { + if (workstation == null || !workstation.IsServer) + return; + + NetworkItem item = other.GetComponentInParent(); + if (item == null || !overlaps.TryGetValue(item, out HashSet colliders)) + return; + + colliders.Remove(other); + if (colliders.Count > 0) + return; + + overlaps.Remove(item); + workstation.NotifyItemExited(item); + } + + private void TryAccept(Collider other) + { + if (workstation == null || !workstation.IsServer || !workstation.IsSpawned) + return; + + NetworkItem item = other.GetComponentInParent(); + if (item != null) + workstation.TryAcceptItem(item); + } + + private void RegisterOverlap(Collider other) + { + NetworkItem item = other.GetComponentInParent(); + if (item == null) + return; + + if (!overlaps.TryGetValue(item, out HashSet colliders)) + { + colliders = new HashSet(); + overlaps.Add(item, colliders); + } + + colliders.Add(other); + } +} diff --git a/Assets/CONTENT/Game/Scripts/Workstations/Processing/NetworkItemInputSlot.cs.meta b/Assets/CONTENT/Game/Scripts/Workstations/Processing/NetworkItemInputSlot.cs.meta new file mode 100644 index 0000000..8a5b15e --- /dev/null +++ b/Assets/CONTENT/Game/Scripts/Workstations/Processing/NetworkItemInputSlot.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 67504197a13e4569ab17c8dcefaf8293 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/CONTENT/Game/Scripts/Workstations/Processing/NetworkItemProcessingWorkstation.cs b/Assets/CONTENT/Game/Scripts/Workstations/Processing/NetworkItemProcessingWorkstation.cs new file mode 100644 index 0000000..fe408a6 --- /dev/null +++ b/Assets/CONTENT/Game/Scripts/Workstations/Processing/NetworkItemProcessingWorkstation.cs @@ -0,0 +1,236 @@ +using System; +using Unity.Netcode; +using UnityEngine; + +public enum ItemProcessingWorkstationState : byte +{ + Empty, + Processing, + OutputReady +} + +public sealed class NetworkItemProcessingWorkstation : NetworkWorkstationController +{ + [Header("Recipe")] + [SerializeField] private ItemProcessingRecipe recipe; + + [Header("Slot")] + [SerializeField] private Transform inputPoint; + + private readonly NetworkVariable state = new( + ItemProcessingWorkstationState.Empty, + NetworkVariableReadPermission.Everyone, + NetworkVariableWritePermission.Server); + + private readonly NetworkVariable processStartedAt = new( + 0d, + NetworkVariableReadPermission.Everyone, + NetworkVariableWritePermission.Server); + + private readonly NetworkVariable processEndsAt = new( + 0d, + NetworkVariableReadPermission.Everyone, + NetworkVariableWritePermission.Server); + + private NetworkItem currentItem; + + public override bool IsOperating => State == ItemProcessingWorkstationState.Processing; + public ItemProcessingWorkstationState State => state.Value; + public ItemProcessingRecipe Recipe => recipe; + public NetworkItem CurrentItem => currentItem; + public bool CanAcceptItem => IsServer && IsSpawned && + State == ItemProcessingWorkstationState.Empty && + recipe != null && recipe.InputItem != null && + recipe.OutputItem != null && recipe.OutputItem.WorldPrefab != null; + + public float ProcessProgress + { + get + { + if (State == ItemProcessingWorkstationState.OutputReady) + return 1f; + + if (!IsOperating) + return 0f; + + double duration = processEndsAt.Value - processStartedAt.Value; + if (duration <= 0d) + return 1f; + + return Mathf.Clamp01((float)((GetNetworkTime() - processStartedAt.Value) / duration)); + } + } + + public float RemainingSeconds => !IsOperating + ? 0f + : Mathf.Max(0f, (float)(processEndsAt.Value - GetNetworkTime())); + + public event Action Changed; + + public override void OnNetworkSpawn() + { + state.OnValueChanged += HandleStateChanged; + processStartedAt.OnValueChanged += HandleProcessTimeChanged; + processEndsAt.OnValueChanged += HandleProcessTimeChanged; + + if (IsServer) + ResetWorkstationOnServer(); + + Changed?.Invoke(); + } + + public override void OnNetworkDespawn() + { + state.OnValueChanged -= HandleStateChanged; + processStartedAt.OnValueChanged -= HandleProcessTimeChanged; + processEndsAt.OnValueChanged -= HandleProcessTimeChanged; + } + + private void Update() + { + if (!IsServer || !IsSpawned) + return; + + if (State == ItemProcessingWorkstationState.Processing) + { + if (currentItem == null || !currentItem.IsSpawned) + { + ResetWorkstationOnServer(); + return; + } + + if (GetNetworkTime() >= processEndsAt.Value) + CompleteProcessingOnServer(); + } + else if (State == ItemProcessingWorkstationState.OutputReady && + (currentItem == null || !currentItem.IsSpawned)) + { + ResetWorkstationOnServer(); + } + } + + private void LateUpdate() + { + if (!IsServer || !IsSpawned || + State != ItemProcessingWorkstationState.Processing || + currentItem == null || inputPoint == null) + { + return; + } + + SnapToInputPoint(currentItem.transform); + } + + public bool TryAcceptItem(NetworkItem item) + { + if (!CanAcceptItem || item == null || !item.IsSpawned || !recipe.Accepts(item)) + return false; + + NetworkCarryableInteractable carryable = + item.GetComponent(); + if (carryable == null || !carryable.TryAcquireExternalControl()) + return false; + + currentItem = item; + SnapToInputPoint(item.transform); + + double now = GetNetworkTime(); + processStartedAt.Value = now; + processEndsAt.Value = now + recipe.ProcessingDuration; + state.Value = ItemProcessingWorkstationState.Processing; + Changed?.Invoke(); + return true; + } + + public void NotifyItemExited(NetworkItem item) + { + if (!IsServer || !IsSpawned || + State != ItemProcessingWorkstationState.OutputReady || + item == null || item != currentItem) + { + return; + } + + ResetWorkstationOnServer(); + } + + private void CompleteProcessingOnServer() + { + if (currentItem == null || recipe == null || + recipe.OutputItem == null || recipe.OutputItem.WorldPrefab == null) + { + ResetWorkstationOnServer(); + return; + } + + Transform slot = inputPoint != null ? inputPoint : transform; + Vector3 outputPosition = slot.position; + Quaternion outputRotation = slot.rotation; + + NetworkObject inputObject = currentItem.NetworkObject; + currentItem = null; + + if (inputObject != null && inputObject.IsSpawned) + inputObject.Despawn(true); + + NetworkObject outputObject = Instantiate( + recipe.OutputItem.WorldPrefab, + outputPosition, + outputRotation); + outputObject.Spawn(); + + currentItem = outputObject.GetComponent(); + if (currentItem == null || + outputObject.GetComponent() == null) + { + Debug.LogError( + $"{name} cannot produce '{recipe.OutputItem.DisplayName}': " + + "the output prefab must contain NetworkItem and " + + "NetworkCarryableInteractable.", + this); + outputObject.Despawn(true); + currentItem = null; + ResetWorkstationOnServer(); + return; + } + + processStartedAt.Value = 0d; + processEndsAt.Value = 0d; + state.Value = ItemProcessingWorkstationState.OutputReady; + Changed?.Invoke(); + } + + private void ResetWorkstationOnServer() + { + if (!IsServer) + return; + + if (State == ItemProcessingWorkstationState.Processing && currentItem != null && + currentItem.TryGetComponent(out NetworkCarryableInteractable carryable)) + { + carryable.TryReleaseExternalControl(); + } + + currentItem = null; + processStartedAt.Value = 0d; + processEndsAt.Value = 0d; + state.Value = ItemProcessingWorkstationState.Empty; + Changed?.Invoke(); + } + + private void SnapToInputPoint(Transform itemTransform) + { + Transform slot = inputPoint != null ? inputPoint : transform; + itemTransform.SetPositionAndRotation(slot.position, slot.rotation); + } + + private double GetNetworkTime() => NetworkManager != null + ? NetworkManager.ServerTime.Time + : Time.timeAsDouble; + + private void HandleStateChanged( + ItemProcessingWorkstationState _, + ItemProcessingWorkstationState __) => Changed?.Invoke(); + + private void HandleProcessTimeChanged(double _, double __) => Changed?.Invoke(); +} diff --git a/Assets/CONTENT/Game/Scripts/Workstations/Processing/NetworkItemProcessingWorkstation.cs.meta b/Assets/CONTENT/Game/Scripts/Workstations/Processing/NetworkItemProcessingWorkstation.cs.meta new file mode 100644 index 0000000..129fcd0 --- /dev/null +++ b/Assets/CONTENT/Game/Scripts/Workstations/Processing/NetworkItemProcessingWorkstation.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 90c3cd35b32140ac86dec106c4bb5035 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: