Added new Rocket-Processing

This commit is contained in:
2026-08-14 16:54:09 +03:00
parent 3c490bdae8
commit b5eadea728
36 changed files with 17653 additions and 1984 deletions
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e48d638d2d22411db1b67c8276c6f6c9
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,609 @@
using System;
using System.Collections.Generic;
using Unity.Netcode;
using UnityEngine;
public enum RocketAssemblyWorkstationState : byte
{
Loading,
Assembling,
OutputReady
}
public sealed class NetworkRocketAssemblyWorkstation : NetworkWorkstationController
{
public const int MaximumSlotCount = 9;
private const ulong EmptySlot = ulong.MaxValue;
[Header("Recipes")]
[SerializeField] private List<RocketAssemblyRecipe> recipes = new();
[Header("Slots (bottom to top)")]
[Tooltip("Element 0 is the lowest physical slot. Only the first 9 entries are used.")]
[SerializeField] private List<RocketAssemblyInputSlot> slotsBottomToTop = new();
[Header("Output")]
[SerializeField] private Transform outputPoint;
[SerializeField, Min(0.1f)] private float outputReleaseDistance = 1.5f;
[Header("Interaction")]
[SerializeField, Min(0.1f)] private float maximumTakeDistance = 3.5f;
[SerializeField, Min(0.05f)] private float reinsertCooldownAfterTake = 0.5f;
private readonly NetworkVariable<RocketAssemblyWorkstationState> state = new(
RocketAssemblyWorkstationState.Loading,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private readonly NetworkVariable<int> selectedRecipeIndex = new(
-1,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private readonly NetworkVariable<double> assemblyStartedAt = new(
0d,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private readonly NetworkVariable<double> assemblyEndsAt = new(
0d,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private readonly NetworkVariable<bool> assemblyWillBeDefective = new(
false,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private readonly NetworkList<ulong> slotItemNetworkObjectIds = new(
default,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private readonly NetworkItem[] slottedItems = new NetworkItem[MaximumSlotCount];
private readonly IWorkstationItemControl[] slottedItemControls =
new IWorkstationItemControl[MaximumSlotCount];
private readonly Dictionary<ulong, double> reinsertBlockedUntil = new();
private NetworkItem currentOutput;
public override bool IsOperating => State == RocketAssemblyWorkstationState.Assembling;
public RocketAssemblyWorkstationState State => state.Value;
public IReadOnlyList<RocketAssemblyRecipe> Recipes => recipes;
public IReadOnlyList<NetworkItem> SlottedItems => slottedItems;
public int SlotCount => Mathf.Min(slotsBottomToTop.Count, MaximumSlotCount);
public int SelectedRecipeIndex => selectedRecipeIndex.Value;
public RocketAssemblyRecipe SelectedRecipe => TryGetRecipe(
selectedRecipeIndex.Value,
out RocketAssemblyRecipe recipe)
? recipe
: null;
public bool AssemblyWillBeDefective => assemblyWillBeDefective.Value;
public bool LastCompletedAssemblyWasDefective =>
State == RocketAssemblyWorkstationState.OutputReady &&
assemblyWillBeDefective.Value;
public NetworkItem CurrentOutput => currentOutput;
public int OccupiedSlotCount
{
get
{
int count = 0;
int synchronizedSlotCount = Mathf.Min(slotItemNetworkObjectIds.Count, SlotCount);
for (int index = 0; index < synchronizedSlotCount; index++)
{
if (slotItemNetworkObjectIds[index] != EmptySlot)
count++;
}
return count;
}
}
public bool CanStartAssembly =>
State == RocketAssemblyWorkstationState.Loading &&
SelectedRecipe != null &&
OccupiedSlotCount >= SelectedRecipe.RequiredPartCount;
public float AssemblyProgress
{
get
{
if (State == RocketAssemblyWorkstationState.OutputReady)
return 1f;
if (!IsOperating)
return 0f;
double duration = assemblyEndsAt.Value - assemblyStartedAt.Value;
return duration <= 0d
? 1f
: Mathf.Clamp01((float)((GetNetworkTime() - assemblyStartedAt.Value) / duration));
}
}
public float RemainingSeconds => !IsOperating
? 0f
: Mathf.Max(0f, (float)(assemblyEndsAt.Value - GetNetworkTime()));
public event Action Changed;
private void Awake() => ConfigureSlots();
private void OnValidate()
{
if (slotsBottomToTop.Count > MaximumSlotCount)
slotsBottomToTop.RemoveRange(
MaximumSlotCount,
slotsBottomToTop.Count - MaximumSlotCount);
ConfigureSlots();
}
public override void OnNetworkSpawn()
{
state.OnValueChanged += HandleStateChanged;
selectedRecipeIndex.OnValueChanged += HandleSelectedRecipeChanged;
assemblyStartedAt.OnValueChanged += HandleAssemblyTimeChanged;
assemblyEndsAt.OnValueChanged += HandleAssemblyTimeChanged;
assemblyWillBeDefective.OnValueChanged += HandleDefectiveStateChanged;
slotItemNetworkObjectIds.OnListChanged += HandleSlotListChanged;
if (IsServer)
InitializeOnServer();
Changed?.Invoke();
}
public override void OnNetworkDespawn()
{
state.OnValueChanged -= HandleStateChanged;
selectedRecipeIndex.OnValueChanged -= HandleSelectedRecipeChanged;
assemblyStartedAt.OnValueChanged -= HandleAssemblyTimeChanged;
assemblyEndsAt.OnValueChanged -= HandleAssemblyTimeChanged;
assemblyWillBeDefective.OnValueChanged -= HandleDefectiveStateChanged;
slotItemNetworkObjectIds.OnListChanged -= HandleSlotListChanged;
}
private void Update()
{
if (!IsServer || !IsSpawned)
return;
RemoveMissingSlottedItems();
if (State == RocketAssemblyWorkstationState.Assembling &&
GetNetworkTime() >= assemblyEndsAt.Value)
{
CompleteAssemblyOnServer();
}
else if (State == RocketAssemblyWorkstationState.OutputReady &&
HasOutputLeftWorkstation())
{
ResetToLoadingOnServer();
}
}
private void LateUpdate()
{
if (!IsServer || !IsSpawned ||
State == RocketAssemblyWorkstationState.OutputReady)
{
return;
}
for (int index = 0; index < SlotCount; index++)
{
if (slottedItems[index] != null)
SnapToSlot(index, slottedItems[index].transform);
}
}
public bool TryInsertItem(int slotIndex, NetworkItem item)
{
if (!IsServer || !IsSpawned ||
State != RocketAssemblyWorkstationState.Loading ||
!IsValidSlotIndex(slotIndex) ||
slottedItems[slotIndex] != null ||
!IsReadyRocketPart(item) ||
IsTemporarilyBlockedFromReinsert(item) ||
IsAlreadySlotted(item) ||
!WorkstationItemControlUtility.TryGetControl(
item,
out IWorkstationItemControl itemControl) ||
!itemControl.TryAcquireExternalControl())
{
return false;
}
slottedItems[slotIndex] = item;
slottedItemControls[slotIndex] = itemControl;
slotItemNetworkObjectIds[slotIndex] = item.NetworkObjectId;
SnapToSlot(slotIndex, item.transform);
Changed?.Invoke();
return true;
}
public bool CanTakeSlotItem(int slotIndex) =>
IsSpawned &&
State == RocketAssemblyWorkstationState.Loading &&
IsValidSlotIndex(slotIndex) &&
slotIndex < slotItemNetworkObjectIds.Count &&
slotItemNetworkObjectIds[slotIndex] != EmptySlot;
public string GetTakePrompt(int slotIndex)
{
if (!CanTakeSlotItem(slotIndex))
return "Take part";
NetworkItem item = IsServer ? slottedItems[slotIndex] : ResolveSynchronizedItem(slotIndex);
return item != null ? $"Take {item.DisplayName}" : "Take part";
}
public void RequestTakeSlotItem(int slotIndex)
{
if (!CanTakeSlotItem(slotIndex))
return;
RequestTakeSlotItemServerRpc(slotIndex);
}
public void RequestSelectRecipe(int recipeIndex)
{
if (!IsSpawned)
return;
if (IsServer)
SelectRecipeOnServer(recipeIndex);
else
RequestSelectRecipeServerRpc(recipeIndex);
}
public void RequestStartAssembly()
{
if (!IsSpawned)
return;
if (IsServer)
TryStartAssemblyOnServer();
else
RequestStartAssemblyServerRpc();
}
public void SetAssemblyRequested(bool isActivated)
{
if (isActivated && IsServer && IsSpawned)
TryStartAssemblyOnServer();
}
[ContextMenu("Debug/Start Assembly (Server Play Mode)")]
private void StartAssemblyFromContextMenu()
{
if (Application.isPlaying && IsServer && IsSpawned)
TryStartAssemblyOnServer();
}
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
private void RequestTakeSlotItemServerRpc(
int slotIndex,
RpcParams rpcParams = default)
{
if (!CanTakeSlotItem(slotIndex) ||
!TryGetPlayerObject(rpcParams.Receive.SenderClientId, out NetworkObject playerObject))
{
return;
}
float maximumDistanceSquared = maximumTakeDistance * maximumTakeDistance;
if ((playerObject.transform.position -
slotsBottomToTop[slotIndex].InteractionPosition).sqrMagnitude > maximumDistanceSquared)
{
return;
}
IWorkstationItemControl itemControl = slottedItemControls[slotIndex];
if (itemControl == null ||
!itemControl.TryReleaseExternalControlToPlayer(rpcParams.Receive.SenderClientId))
{
return;
}
NetworkItem item = slottedItems[slotIndex];
if (item != null)
{
reinsertBlockedUntil[item.NetworkObjectId] =
GetNetworkTime() + reinsertCooldownAfterTake;
}
ClearSlot(slotIndex);
}
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
private void RequestSelectRecipeServerRpc(int recipeIndex) =>
SelectRecipeOnServer(recipeIndex);
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
private void RequestStartAssemblyServerRpc() => TryStartAssemblyOnServer();
private void InitializeOnServer()
{
slotItemNetworkObjectIds.Clear();
for (int index = 0; index < SlotCount; index++)
slotItemNetworkObjectIds.Add(EmptySlot);
Array.Clear(slottedItems, 0, slottedItems.Length);
Array.Clear(slottedItemControls, 0, slottedItemControls.Length);
reinsertBlockedUntil.Clear();
currentOutput = null;
selectedRecipeIndex.Value = FindFirstValidRecipeIndex();
assemblyStartedAt.Value = 0d;
assemblyEndsAt.Value = 0d;
assemblyWillBeDefective.Value = false;
state.Value = RocketAssemblyWorkstationState.Loading;
}
private void SelectRecipeOnServer(int recipeIndex)
{
if (!IsServer || !IsSpawned ||
State != RocketAssemblyWorkstationState.Loading ||
!TryGetRecipe(recipeIndex, out _))
{
return;
}
selectedRecipeIndex.Value = recipeIndex;
Changed?.Invoke();
}
private bool TryStartAssemblyOnServer()
{
RocketAssemblyRecipe recipe = SelectedRecipe;
if (!IsServer || !IsSpawned || !CanStartAssembly || recipe == null)
return false;
assemblyWillBeDefective.Value = !recipe.Matches(slottedItems);
double now = GetNetworkTime();
assemblyStartedAt.Value = now;
assemblyEndsAt.Value = now + recipe.AssemblyDuration;
state.Value = RocketAssemblyWorkstationState.Assembling;
Changed?.Invoke();
return true;
}
private void CompleteAssemblyOnServer()
{
RocketAssemblyRecipe recipe = SelectedRecipe;
if (recipe == null)
{
CancelAssemblyOnServer();
return;
}
bool defective = assemblyWillBeDefective.Value;
ItemDefinition outputDefinition = defective
? recipe.DefectiveOutputItem
: recipe.OutputItem;
if (outputDefinition == null || outputDefinition.WorldPrefab == null)
{
CancelAssemblyOnServer();
return;
}
for (int index = 0; index < SlotCount; index++)
{
NetworkItem item = slottedItems[index];
ClearSlotRuntime(index);
if (item != null && item.NetworkObject != null && item.IsSpawned)
item.NetworkObject.Despawn(true);
slotItemNetworkObjectIds[index] = EmptySlot;
}
Transform spawnPoint = outputPoint != null ? outputPoint : transform;
NetworkObject outputObject = Instantiate(
outputDefinition.WorldPrefab,
spawnPoint.position,
spawnPoint.rotation);
outputObject.Spawn();
currentOutput = outputObject.GetComponent<NetworkItem>();
if (currentOutput == null ||
!WorkstationItemControlUtility.TryGetControl(currentOutput, out _))
{
Debug.LogError(
$"{name} cannot produce '{outputDefinition.DisplayName}': the output prefab " +
"must contain NetworkItem and a supported Carry or Drag interaction component.",
this);
outputObject.Despawn(true);
currentOutput = null;
ResetToLoadingOnServer();
return;
}
assemblyStartedAt.Value = 0d;
assemblyEndsAt.Value = 0d;
state.Value = RocketAssemblyWorkstationState.OutputReady;
Changed?.Invoke();
}
private void CancelAssemblyOnServer()
{
assemblyStartedAt.Value = 0d;
assemblyEndsAt.Value = 0d;
assemblyWillBeDefective.Value = false;
state.Value = RocketAssemblyWorkstationState.Loading;
Changed?.Invoke();
}
private void ResetToLoadingOnServer()
{
currentOutput = null;
assemblyStartedAt.Value = 0d;
assemblyEndsAt.Value = 0d;
assemblyWillBeDefective.Value = false;
state.Value = RocketAssemblyWorkstationState.Loading;
Changed?.Invoke();
}
private void RemoveMissingSlottedItems()
{
if (State == RocketAssemblyWorkstationState.Assembling)
{
for (int index = 0; index < SlotCount; index++)
{
if (slotItemNetworkObjectIds[index] != EmptySlot &&
(slottedItems[index] == null || !slottedItems[index].IsSpawned))
{
CancelAssemblyOnServer();
return;
}
}
}
for (int index = 0; index < SlotCount; index++)
{
if (slotItemNetworkObjectIds[index] != EmptySlot &&
(slottedItems[index] == null || !slottedItems[index].IsSpawned))
{
ClearSlot(index);
}
}
}
private bool HasOutputLeftWorkstation()
{
if (currentOutput == null || !currentOutput.IsSpawned)
return true;
Transform spawnPoint = outputPoint != null ? outputPoint : transform;
return (currentOutput.transform.position - spawnPoint.position).sqrMagnitude >
outputReleaseDistance * outputReleaseDistance;
}
private void ClearSlot(int slotIndex)
{
ClearSlotRuntime(slotIndex);
slotItemNetworkObjectIds[slotIndex] = EmptySlot;
Changed?.Invoke();
}
private void ClearSlotRuntime(int slotIndex)
{
slottedItems[slotIndex] = null;
slottedItemControls[slotIndex] = null;
}
private void SnapToSlot(int slotIndex, Transform itemTransform)
{
Transform snapPoint = slotsBottomToTop[slotIndex].SnapPoint;
itemTransform.SetPositionAndRotation(snapPoint.position, snapPoint.rotation);
}
private bool IsAlreadySlotted(NetworkItem item)
{
for (int index = 0; index < SlotCount; index++)
{
if (slottedItems[index] == item)
return true;
}
return false;
}
private bool IsTemporarilyBlockedFromReinsert(NetworkItem item)
{
if (item == null ||
!reinsertBlockedUntil.TryGetValue(item.NetworkObjectId, out double blockedUntil))
{
return false;
}
if (GetNetworkTime() < blockedUntil)
return true;
reinsertBlockedUntil.Remove(item.NetworkObjectId);
return false;
}
private NetworkItem ResolveSynchronizedItem(int slotIndex)
{
if (!IsValidSlotIndex(slotIndex) ||
slotIndex >= slotItemNetworkObjectIds.Count ||
slotItemNetworkObjectIds[slotIndex] == EmptySlot ||
NetworkManager == null)
{
return null;
}
return NetworkManager.SpawnManager.SpawnedObjects.TryGetValue(
slotItemNetworkObjectIds[slotIndex],
out NetworkObject networkObject)
? networkObject.GetComponent<NetworkItem>()
: null;
}
private bool TryGetPlayerObject(ulong clientId, out NetworkObject playerObject)
{
playerObject = NetworkManager != null
? NetworkManager.SpawnManager.GetPlayerNetworkObject(clientId)
: null;
return playerObject != null;
}
private void ConfigureSlots()
{
int count = Mathf.Min(slotsBottomToTop.Count, MaximumSlotCount);
for (int index = 0; index < count; index++)
{
if (slotsBottomToTop[index] != null)
slotsBottomToTop[index].Configure(this, index);
}
}
private int FindFirstValidRecipeIndex()
{
for (int index = 0; index < recipes.Count; index++)
{
if (IsRecipeValid(recipes[index]))
return index;
}
return -1;
}
private bool TryGetRecipe(int index, out RocketAssemblyRecipe recipe)
{
if (index >= 0 && index < recipes.Count && IsRecipeValid(recipes[index]))
{
recipe = recipes[index];
return true;
}
recipe = null;
return false;
}
private bool IsValidSlotIndex(int slotIndex) =>
slotIndex >= 0 && slotIndex < SlotCount && slotsBottomToTop[slotIndex] != null;
private static bool IsReadyRocketPart(NetworkItem item) =>
item != null && item.IsSpawned && item.Definition != null &&
item.Category == ItemCategory.RocketPart && item.Definition.IsAssemblyReady;
private static bool IsRecipeValid(RocketAssemblyRecipe recipe) =>
recipe != null && recipe.IsConfigured;
private double GetNetworkTime() => NetworkManager != null
? NetworkManager.ServerTime.Time
: Time.timeAsDouble;
private void HandleStateChanged(
RocketAssemblyWorkstationState _,
RocketAssemblyWorkstationState __) => Changed?.Invoke();
private void HandleSelectedRecipeChanged(int _, int __) => Changed?.Invoke();
private void HandleAssemblyTimeChanged(double _, double __) => Changed?.Invoke();
private void HandleDefectiveStateChanged(bool _, bool __) => Changed?.Invoke();
private void HandleSlotListChanged(NetworkListEvent<ulong> _) => Changed?.Invoke();
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 43801e0a52c2499bbcf2c7e39871ce80
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,54 @@
using Unity.Netcode;
using UnityEngine;
[DisallowMultipleComponent]
[RequireComponent(typeof(Collider))]
public sealed class RocketAssemblyInputSlot : MonoBehaviour, IInteractable
{
[SerializeField] private Transform snapPoint;
private NetworkRocketAssemblyWorkstation workstation;
private int slotIndex = -1;
public Transform SnapPoint => snapPoint != null ? snapPoint : transform;
public Vector3 InteractionPosition => SnapPoint.position;
public string InteractionPrompt => workstation != null
? workstation.GetTakePrompt(slotIndex)
: "Take part";
public bool IsInteractionAvailable =>
workstation != null && workstation.CanTakeSlotItem(slotIndex);
private void Reset()
{
snapPoint = transform;
GetComponent<Collider>().isTrigger = true;
}
private void OnValidate() => GetComponent<Collider>().isTrigger = true;
public void Configure(NetworkRocketAssemblyWorkstation owner, int index)
{
workstation = owner;
slotIndex = index;
}
public void RequestInteraction()
{
if (IsInteractionAvailable)
workstation.RequestTakeSlotItem(slotIndex);
}
private void OnTriggerEnter(Collider other) => TryInsert(other);
private void OnTriggerStay(Collider other) => TryInsert(other);
private void TryInsert(Collider other)
{
if (workstation == null || !workstation.IsServer || !workstation.IsSpawned)
return;
NetworkItem item = other.GetComponentInParent<NetworkItem>();
if (item != null)
workstation.TryInsertItem(slotIndex, item);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 54737cf1ab274fdb851b89457fb3412f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,89 @@
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(
fileName = "RocketAssemblyRecipe",
menuName = "FORT-BO/Workstations/Rocket Assembly Recipe")]
public sealed class RocketAssemblyRecipe : ScriptableObject
{
public const int MaximumPartCount = 9;
[Header("Recipe")]
[SerializeField] private string recipeId;
[SerializeField] private string displayName;
[Tooltip("Exact assembly order. Element 0 is the lowest rocket part.")]
[SerializeField] private List<ItemDefinition> requiredPartsBottomToTop = new();
[SerializeField] private ItemDefinition outputItem;
[SerializeField] private ItemDefinition defectiveOutputItem;
[SerializeField, Min(0.05f)] private float assemblyDuration = 5f;
[Header("Blueprint")]
[SerializeField] private string blueprintTitle;
[SerializeField] private Sprite blueprintImage;
public string RecipeId => recipeId;
public string DisplayName => string.IsNullOrWhiteSpace(displayName) ? name : displayName;
public IReadOnlyList<ItemDefinition> RequiredPartsBottomToTop => requiredPartsBottomToTop;
public int RequiredPartCount => requiredPartsBottomToTop.Count;
public ItemDefinition OutputItem => outputItem;
public ItemDefinition DefectiveOutputItem => defectiveOutputItem;
public float AssemblyDuration => assemblyDuration;
public string BlueprintTitle => string.IsNullOrWhiteSpace(blueprintTitle)
? DisplayName
: blueprintTitle;
public Sprite BlueprintImage => blueprintImage;
public bool IsConfigured =>
requiredPartsBottomToTop.Count is > 0 and <= MaximumPartCount &&
requiredPartsBottomToTop.TrueForAll(IsReadyRocketPart) &&
IsSpawnable(outputItem) &&
IsSpawnable(defectiveOutputItem);
public bool Matches(IReadOnlyList<NetworkItem> slottedItems)
{
if (!IsConfigured || slottedItems == null ||
slottedItems.Count < requiredPartsBottomToTop.Count)
{
return false;
}
for (int index = 0; index < requiredPartsBottomToTop.Count; index++)
{
NetworkItem item = slottedItems[index];
if (item == null || !MatchesDefinition(item, requiredPartsBottomToTop[index]))
return false;
}
for (int index = requiredPartsBottomToTop.Count; index < slottedItems.Count; index++)
{
if (slottedItems[index] != null)
return false;
}
return true;
}
private void OnValidate()
{
recipeId = recipeId?.Trim().ToLowerInvariant();
displayName = displayName?.Trim();
blueprintTitle = blueprintTitle?.Trim();
if (requiredPartsBottomToTop.Count > MaximumPartCount)
requiredPartsBottomToTop.RemoveRange(
MaximumPartCount,
requiredPartsBottomToTop.Count - MaximumPartCount);
}
private static bool MatchesDefinition(NetworkItem item, ItemDefinition definition) =>
item.Definition == definition ||
(!string.IsNullOrEmpty(definition.ItemId) && item.ItemId == definition.ItemId);
private static bool IsReadyRocketPart(ItemDefinition definition) =>
definition != null &&
definition.Category == ItemCategory.RocketPart &&
definition.IsAssemblyReady;
private static bool IsSpawnable(ItemDefinition definition) =>
definition != null && definition.WorldPrefab != null;
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 04f3334793c14ee791f78059d0c2854c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,148 @@
using UnityEngine;
using UnityEngine.Events;
[DisallowMultipleComponent]
public sealed class RocketAssemblyWorkstationView : MonoBehaviour
{
[SerializeField] private NetworkRocketAssemblyWorkstation workstation;
[Header("State Events")]
[SerializeField] private UnityEvent onLoading;
[SerializeField] private UnityEvent onAssemblyStarted;
[SerializeField] private UnityEvent onValidOutputReady;
[SerializeField] private UnityEvent onDefectiveOutputReady;
[Header("Blueprint and Slot Events")]
[SerializeField] private UnityEvent<string> onRecipeNameChanged;
[SerializeField] private UnityEvent<string> onBlueprintTitleChanged;
[SerializeField] private UnityEvent<Sprite> onBlueprintImageChanged;
[SerializeField] private UnityEvent<int> onRequiredPartCountChanged;
[SerializeField] private UnityEvent<int> onOccupiedSlotCountChanged;
[SerializeField] private UnityEvent<bool> onCanStartChanged;
[Header("Continuous Progress")]
[SerializeField] private UnityEvent<float> onAssemblyProgress;
private RocketAssemblyWorkstationState previousState;
private bool previousDefectiveResult;
private bool hasAppliedState;
public NetworkRocketAssemblyWorkstation Workstation => workstation;
private void Reset() =>
workstation = GetComponentInParent<NetworkRocketAssemblyWorkstation>();
private void Awake()
{
if (workstation == null)
workstation = GetComponentInParent<NetworkRocketAssemblyWorkstation>();
}
private void OnEnable()
{
if (workstation != null)
{
workstation.Changed += Apply;
Apply();
}
}
private void Start() => Apply();
private void OnDisable()
{
if (workstation != null)
workstation.Changed -= Apply;
hasAppliedState = false;
}
private void Update()
{
if (workstation != null && workstation.IsOperating)
onAssemblyProgress?.Invoke(workstation.AssemblyProgress);
}
public void Apply()
{
if (workstation == null || !workstation.IsSpawned)
return;
ApplyRecipeAndSlots();
RocketAssemblyWorkstationState currentState = workstation.State;
bool defectiveResult = workstation.LastCompletedAssemblyWasDefective;
if (hasAppliedState && currentState == previousState &&
defectiveResult == previousDefectiveResult)
{
return;
}
previousState = currentState;
previousDefectiveResult = defectiveResult;
hasAppliedState = true;
switch (currentState)
{
case RocketAssemblyWorkstationState.Assembling:
onAssemblyStarted?.Invoke();
onAssemblyProgress?.Invoke(workstation.AssemblyProgress);
break;
case RocketAssemblyWorkstationState.OutputReady:
onAssemblyProgress?.Invoke(1f);
if (defectiveResult)
onDefectiveOutputReady?.Invoke();
else
onValidOutputReady?.Invoke();
break;
default:
onAssemblyProgress?.Invoke(0f);
onLoading?.Invoke();
break;
}
}
[ContextMenu("Preview/Loading")]
private void PreviewLoading()
{
onAssemblyProgress?.Invoke(0f);
onLoading?.Invoke();
}
[ContextMenu("Preview/Assembly Started")]
private void PreviewAssemblyStarted()
{
onAssemblyProgress?.Invoke(0f);
onAssemblyStarted?.Invoke();
}
[ContextMenu("Preview/Assembly 50 Percent")]
private void PreviewAssemblyHalfway() => onAssemblyProgress?.Invoke(0.5f);
[ContextMenu("Preview/Valid Output")]
private void PreviewValidOutput()
{
onAssemblyProgress?.Invoke(1f);
onValidOutputReady?.Invoke();
}
[ContextMenu("Preview/Defective Output")]
private void PreviewDefectiveOutput()
{
onAssemblyProgress?.Invoke(1f);
onDefectiveOutputReady?.Invoke();
}
private void ApplyRecipeAndSlots()
{
RocketAssemblyRecipe recipe = workstation.SelectedRecipe;
onRecipeNameChanged?.Invoke(recipe != null ? recipe.DisplayName : string.Empty);
onBlueprintTitleChanged?.Invoke(recipe != null ? recipe.BlueprintTitle : string.Empty);
onBlueprintImageChanged?.Invoke(recipe != null ? recipe.BlueprintImage : null);
onRequiredPartCountChanged?.Invoke(recipe != null ? recipe.RequiredPartCount : 0);
onOccupiedSlotCountChanged?.Invoke(workstation.OccupiedSlotCount);
onCanStartChanged?.Invoke(workstation.CanStartAssembly);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 42a6ab0b475f46aebda93f3c671b670a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -139,7 +139,9 @@ public sealed class NetworkItemProcessingWorkstation : NetworkWorkstationControl
{
if (!CanAcceptItem || item == null || !item.IsSpawned ||
!TryFindRecipe(item, out int recipeIndex, out ItemProcessingRecipe recipe) ||
!TryGetItemControl(item, out IWorkstationItemControl itemControl))
!WorkstationItemControlUtility.TryGetControl(
item,
out IWorkstationItemControl itemControl))
{
return false;
}
@@ -200,7 +202,8 @@ public sealed class NetworkItemProcessingWorkstation : NetworkWorkstationControl
outputObject.Spawn();
currentItem = outputObject.GetComponent<NetworkItem>();
if (currentItem == null || !TryGetItemControl(currentItem, out _))
if (currentItem == null ||
!WorkstationItemControlUtility.TryGetControl(currentItem, out _))
{
Debug.LogError(
$"{name} cannot produce '{recipe.OutputItem.DisplayName}': " +
@@ -291,20 +294,4 @@ public sealed class NetworkItemProcessingWorkstation : NetworkWorkstationControl
recipe != null && recipe.InputItem != null &&
recipe.OutputItem != null && recipe.OutputItem.WorldPrefab != null;
private static bool TryGetItemControl(
NetworkItem item,
out IWorkstationItemControl itemControl)
{
foreach (MonoBehaviour component in item.GetComponents<MonoBehaviour>())
{
if (component is IWorkstationItemControl candidate)
{
itemControl = candidate;
return true;
}
}
itemControl = null;
return false;
}
}