Added RocketAssemblyStation and ReadyRocket

This commit is contained in:
2026-08-18 16:12:00 +03:00
parent 7a0ee6b519
commit 63d2836dc9
85 changed files with 15087 additions and 3562 deletions
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using Unity.Netcode;
using UnityEngine;
using UnityEngine.Serialization;
public enum RocketAssemblyWorkstationState : byte
{
@@ -28,7 +29,9 @@ public sealed class NetworkRocketAssemblyWorkstation : NetworkWorkstationControl
[Header("Interaction")]
[SerializeField, Min(0.1f)] private float maximumTakeDistance = 3.5f;
[SerializeField, Min(0.05f)] private float reinsertCooldownAfterTake = 0.5f;
[FormerlySerializedAs("reinsertCooldownAfterTake")]
[SerializeField, Min(0f)] private float minimumReinsertBlockDuration = 0.1f;
[SerializeField, Min(0f)] private float reinsertClearance = 0.1f;
private readonly NetworkVariable<RocketAssemblyWorkstationState> state = new(
RocketAssemblyWorkstationState.Loading,
@@ -63,7 +66,8 @@ public sealed class NetworkRocketAssemblyWorkstation : NetworkWorkstationControl
private readonly NetworkItem[] slottedItems = new NetworkItem[MaximumSlotCount];
private readonly IWorkstationItemControl[] slottedItemControls =
new IWorkstationItemControl[MaximumSlotCount];
private readonly Dictionary<ulong, double> reinsertBlockedUntil = new();
private readonly Dictionary<ulong, ReinsertBlock> reinsertBlocks = new();
private readonly List<ulong> clearedReinsertBlockIds = new();
private NetworkItem currentOutput;
@@ -170,6 +174,7 @@ public sealed class NetworkRocketAssemblyWorkstation : NetworkWorkstationControl
if (!IsServer || !IsSpawned)
return;
UpdateReinsertBlocks();
RemoveMissingSlottedItems();
if (State == RocketAssemblyWorkstationState.Assembling &&
@@ -311,8 +316,11 @@ public sealed class NetworkRocketAssemblyWorkstation : NetworkWorkstationControl
NetworkItem item = slottedItems[slotIndex];
if (item != null)
{
reinsertBlockedUntil[item.NetworkObjectId] =
GetNetworkTime() + reinsertCooldownAfterTake;
reinsertBlocks[item.NetworkObjectId] = new ReinsertBlock(
item,
slotIndex,
item.GetComponentsInChildren<Collider>(true),
GetNetworkTime() + minimumReinsertBlockDuration);
}
ClearSlot(slotIndex);
@@ -333,7 +341,8 @@ public sealed class NetworkRocketAssemblyWorkstation : NetworkWorkstationControl
Array.Clear(slottedItems, 0, slottedItems.Length);
Array.Clear(slottedItemControls, 0, slottedItemControls.Length);
reinsertBlockedUntil.Clear();
reinsertBlocks.Clear();
clearedReinsertBlockIds.Clear();
currentOutput = null;
selectedRecipeIndex.Value = FindFirstValidRecipeIndex();
assemblyStartedAt.Value = 0d;
@@ -380,9 +389,7 @@ public sealed class NetworkRocketAssemblyWorkstation : NetworkWorkstationControl
}
bool defective = assemblyWillBeDefective.Value;
ItemDefinition outputDefinition = defective
? recipe.DefectiveOutputItem
: recipe.OutputItem;
RocketItemDefinition outputDefinition = recipe.OutputItem;
if (outputDefinition == null || outputDefinition.WorldPrefab == null)
{
CancelAssemblyOnServer();
@@ -408,12 +415,14 @@ public sealed class NetworkRocketAssemblyWorkstation : NetworkWorkstationControl
outputObject.Spawn();
currentOutput = outputObject.GetComponent<NetworkItem>();
if (currentOutput == null ||
NetworkRocketItem rocketItem = outputObject.GetComponent<NetworkRocketItem>();
if (currentOutput == null || rocketItem == 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.",
"must contain NetworkItem, NetworkRocketItem and a supported Carry or Drag " +
"interaction component.",
this);
outputObject.Despawn(true);
currentOutput = null;
@@ -421,6 +430,8 @@ public sealed class NetworkRocketAssemblyWorkstation : NetworkWorkstationControl
return;
}
rocketItem.TryInitializeAssemblyQuality(defective);
assemblyStartedAt.Value = 0d;
assemblyEndsAt.Value = 0d;
state.Value = RocketAssemblyWorkstationState.OutputReady;
@@ -513,17 +524,51 @@ public sealed class NetworkRocketAssemblyWorkstation : NetworkWorkstationControl
private bool IsTemporarilyBlockedFromReinsert(NetworkItem item)
{
if (item == null ||
!reinsertBlockedUntil.TryGetValue(item.NetworkObjectId, out double blockedUntil))
return item != null && reinsertBlocks.ContainsKey(item.NetworkObjectId);
}
private void UpdateReinsertBlocks()
{
if (reinsertBlocks.Count == 0)
return;
double now = GetNetworkTime();
clearedReinsertBlockIds.Clear();
foreach (KeyValuePair<ulong, ReinsertBlock> pair in reinsertBlocks)
{
return false;
ReinsertBlock block = pair.Value;
bool canReleaseBlock = block.Item == null || !block.Item.IsSpawned ||
(now >= block.MinimumBlockedUntil &&
(!IsValidSlotIndex(block.SourceSlotIndex) ||
!slotsBottomToTop[block.SourceSlotIndex].OverlapsAny(
block.ItemColliders,
reinsertClearance)));
if (canReleaseBlock)
clearedReinsertBlockIds.Add(pair.Key);
}
if (GetNetworkTime() < blockedUntil)
return true;
foreach (ulong networkObjectId in clearedReinsertBlockIds)
reinsertBlocks.Remove(networkObjectId);
}
reinsertBlockedUntil.Remove(item.NetworkObjectId);
return false;
private readonly struct ReinsertBlock
{
public ReinsertBlock(
NetworkItem item,
int sourceSlotIndex,
Collider[] itemColliders,
double minimumBlockedUntil)
{
Item = item;
SourceSlotIndex = sourceSlotIndex;
ItemColliders = itemColliders;
MinimumBlockedUntil = minimumBlockedUntil;
}
public NetworkItem Item { get; }
public int SourceSlotIndex { get; }
public Collider[] ItemColliders { get; }
public double MinimumBlockedUntil { get; }
}
private NetworkItem ResolveSynchronizedItem(int slotIndex)
@@ -588,8 +633,9 @@ public sealed class NetworkRocketAssemblyWorkstation : NetworkWorkstationControl
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;
item != null && item.IsSpawned &&
item.TryGetDefinition(out PartItemDefinition definition) &&
definition.IsReadyForAssembly;
private static bool IsRecipeValid(RocketAssemblyRecipe recipe) =>
recipe != null && recipe.IsConfigured;
@@ -8,6 +8,7 @@ public sealed class RocketAssemblyInputSlot : MonoBehaviour, IInteractable
[SerializeField] private Transform snapPoint;
private NetworkRocketAssemblyWorkstation workstation;
private Collider triggerCollider;
private int slotIndex = -1;
public Transform SnapPoint => snapPoint != null ? snapPoint : transform;
@@ -21,10 +22,17 @@ public sealed class RocketAssemblyInputSlot : MonoBehaviour, IInteractable
private void Reset()
{
snapPoint = transform;
GetComponent<Collider>().isTrigger = true;
ResolveTriggerCollider();
triggerCollider.isTrigger = true;
}
private void OnValidate() => GetComponent<Collider>().isTrigger = true;
private void Awake() => ResolveTriggerCollider();
private void OnValidate()
{
ResolveTriggerCollider();
triggerCollider.isTrigger = true;
}
public void Configure(NetworkRocketAssemblyWorkstation owner, int index)
{
@@ -51,4 +59,30 @@ public sealed class RocketAssemblyInputSlot : MonoBehaviour, IInteractable
if (item != null)
workstation.TryInsertItem(slotIndex, item);
}
public bool OverlapsAny(Collider[] itemColliders, float clearance)
{
ResolveTriggerCollider();
if (triggerCollider == null || !triggerCollider.enabled || itemColliders == null)
return false;
Bounds triggerBounds = triggerCollider.bounds;
triggerBounds.Expand(Mathf.Max(0f, clearance) * 2f);
foreach (Collider itemCollider in itemColliders)
{
if (itemCollider != null && itemCollider.enabled &&
triggerBounds.Intersects(itemCollider.bounds))
{
return true;
}
}
return false;
}
private void ResolveTriggerCollider()
{
if (triggerCollider == null)
triggerCollider = GetComponent<Collider>();
}
}
@@ -12,9 +12,8 @@ public sealed class RocketAssemblyRecipe : ScriptableObject
[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] private List<PartItemDefinition> requiredPartsBottomToTop = new();
[SerializeField] private RocketItemDefinition outputItem;
[SerializeField, Min(0.05f)] private float assemblyDuration = 5f;
[Header("Blueprint")]
@@ -23,10 +22,9 @@ public sealed class RocketAssemblyRecipe : ScriptableObject
public string RecipeId => recipeId;
public string DisplayName => string.IsNullOrWhiteSpace(displayName) ? name : displayName;
public IReadOnlyList<ItemDefinition> RequiredPartsBottomToTop => requiredPartsBottomToTop;
public IReadOnlyList<PartItemDefinition> RequiredPartsBottomToTop => requiredPartsBottomToTop;
public int RequiredPartCount => requiredPartsBottomToTop.Count;
public ItemDefinition OutputItem => outputItem;
public ItemDefinition DefectiveOutputItem => defectiveOutputItem;
public RocketItemDefinition OutputItem => outputItem;
public float AssemblyDuration => assemblyDuration;
public string BlueprintTitle => string.IsNullOrWhiteSpace(blueprintTitle)
? DisplayName
@@ -36,8 +34,7 @@ public sealed class RocketAssemblyRecipe : ScriptableObject
public bool IsConfigured =>
requiredPartsBottomToTop.Count is > 0 and <= MaximumPartCount &&
requiredPartsBottomToTop.TrueForAll(IsReadyRocketPart) &&
IsSpawnable(outputItem) &&
IsSpawnable(defectiveOutputItem);
IsSpawnable(outputItem);
public bool Matches(IReadOnlyList<NetworkItem> slottedItems)
{
@@ -75,14 +72,12 @@ public sealed class RocketAssemblyRecipe : ScriptableObject
requiredPartsBottomToTop.Count - MaximumPartCount);
}
private static bool MatchesDefinition(NetworkItem item, ItemDefinition definition) =>
private static bool MatchesDefinition(NetworkItem item, PartItemDefinition 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 IsReadyRocketPart(PartItemDefinition definition) =>
definition != null && definition.IsReadyForAssembly;
private static bool IsSpawnable(ItemDefinition definition) =>
definition != null && definition.WorldPrefab != null;