Added RocketAssemblyStation and ReadyRocket
This commit is contained in:
@@ -16,13 +16,12 @@ public abstract class NetworkHeldInteractable : NetworkInteractable, IWorkstatio
|
||||
|
||||
[Header("Holding Follow")]
|
||||
[SerializeField, Min(0.1f)] private float maximumFollowSpeed = 12f;
|
||||
[SerializeField, Min(0.1f)] private float followResponsiveness = 10f;
|
||||
[SerializeField, Min(0.1f)] private float maximumFollowAcceleration = 80f;
|
||||
[SerializeField, Min(0.5f)] private float maximumHoldDistance = 4.5f;
|
||||
[SerializeField, Min(0.1f)] private float rotationFollowSpeed = 14f;
|
||||
|
||||
[Header("Local Presentation")]
|
||||
[Tooltip("Removes the server round-trip delay for the player currently carrying the item.")]
|
||||
[SerializeField] private bool correctLocalHolderPresentation = true;
|
||||
[SerializeField, Min(0.1f)] private float maximumLocalCorrectionDistance = 2f;
|
||||
[SerializeField, Min(0.1f)] private float rotationResponsiveness = 10f;
|
||||
[SerializeField, Min(0.1f)] private float maximumAngularAcceleration = 80f;
|
||||
|
||||
[Header("Throw")]
|
||||
[SerializeField, Min(0f)] private float throwForce = 6f;
|
||||
@@ -41,10 +40,10 @@ public abstract class NetworkHeldInteractable : NetworkInteractable, IWorkstatio
|
||||
private readonly HashSet<Collider> ignoredPlayerColliders = new();
|
||||
private readonly HashSet<Collider> desiredIgnoredColliders = new();
|
||||
private readonly List<Collider> collidersToRestore = new();
|
||||
private readonly RaycastHit[] localCorrectionHits = new RaycastHit[24];
|
||||
private float nextCollisionRefreshTime;
|
||||
private bool initialUseGravity;
|
||||
private CollisionDetectionMode initialCollisionDetectionMode;
|
||||
private NetworkTransform networkTransform;
|
||||
|
||||
protected abstract Vector3 HoldingOffset { get; }
|
||||
protected abstract string DropPrompt { get; }
|
||||
@@ -80,6 +79,8 @@ public abstract class NetworkHeldInteractable : NetworkInteractable, IWorkstatio
|
||||
initialUseGravity = physicsBody.useGravity;
|
||||
initialCollisionDetectionMode = physicsBody.collisionDetectionMode;
|
||||
}
|
||||
|
||||
networkTransform = GetComponent<NetworkTransform>();
|
||||
}
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
@@ -87,6 +88,12 @@ public abstract class NetworkHeldInteractable : NetworkInteractable, IWorkstatio
|
||||
SpawnedHeldObjects.Add(this);
|
||||
holderClientId.OnValueChanged += HandleHolderChanged;
|
||||
isExternallyControlled.OnValueChanged += HandleExternalControlChanged;
|
||||
if (networkTransform != null)
|
||||
{
|
||||
networkTransform.Interpolate = true;
|
||||
networkTransform.UseUnreliableDeltas = true;
|
||||
}
|
||||
|
||||
ApplyHoldingState();
|
||||
}
|
||||
|
||||
@@ -129,28 +136,6 @@ public abstract class NetworkHeldInteractable : NetworkInteractable, IWorkstatio
|
||||
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
if (!correctLocalHolderPresentation || IsServer || !IsSpawned ||
|
||||
!IsHeldByLocalPlayer || IsExternallyControlled || physicsBody == null ||
|
||||
!TryGetPlayerObject(holderClientId.Value, out NetworkObject holder))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Transform anchor = ResolveHoldingAnchor(holder);
|
||||
Vector3 targetPosition = anchor.TransformPoint(HoldingOffset);
|
||||
Vector3 targetDelta = targetPosition - physicsBody.position;
|
||||
if (targetDelta.sqrMagnitude >
|
||||
maximumLocalCorrectionDistance * maximumLocalCorrectionDistance ||
|
||||
HasBlockingColliderForLocalCorrection(targetDelta))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
transform.SetPositionAndRotation(targetPosition, anchor.rotation);
|
||||
}
|
||||
|
||||
public void RequestThrow()
|
||||
{
|
||||
if (IsSpawned && IsHeldByLocalPlayer)
|
||||
@@ -292,10 +277,14 @@ public abstract class NetworkHeldInteractable : NetworkInteractable, IWorkstatio
|
||||
return false;
|
||||
}
|
||||
|
||||
Vector3 targetVelocity = targetDelta / Mathf.Max(Time.fixedDeltaTime, 0.001f);
|
||||
physicsBody.linearVelocity = Vector3.ClampMagnitude(
|
||||
targetVelocity,
|
||||
float fixedDeltaTime = Mathf.Max(Time.fixedDeltaTime, 0.001f);
|
||||
Vector3 targetVelocity = Vector3.ClampMagnitude(
|
||||
targetDelta * followResponsiveness,
|
||||
maximumFollowSpeed);
|
||||
physicsBody.linearVelocity = Vector3.MoveTowards(
|
||||
physicsBody.linearVelocity,
|
||||
targetVelocity,
|
||||
maximumFollowAcceleration * fixedDeltaTime);
|
||||
|
||||
DriveRotation(anchor.rotation);
|
||||
return true;
|
||||
@@ -306,57 +295,25 @@ public abstract class NetworkHeldInteractable : NetworkInteractable, IWorkstatio
|
||||
Quaternion rotationDelta = targetRotation * Quaternion.Inverse(physicsBody.rotation);
|
||||
rotationDelta.ToAngleAxis(out float angle, out Vector3 axis);
|
||||
if (axis.sqrMagnitude < 0.0001f || float.IsNaN(axis.x))
|
||||
{
|
||||
physicsBody.angularVelocity = Vector3.MoveTowards(
|
||||
physicsBody.angularVelocity,
|
||||
Vector3.zero,
|
||||
maximumAngularAcceleration * Mathf.Max(Time.fixedDeltaTime, 0.001f));
|
||||
return;
|
||||
}
|
||||
|
||||
if (angle > 180f)
|
||||
angle -= 360f;
|
||||
|
||||
Vector3 targetAngularVelocity = axis.normalized *
|
||||
(angle * Mathf.Deg2Rad / Mathf.Max(Time.fixedDeltaTime, 0.001f));
|
||||
physicsBody.angularVelocity = Vector3.ClampMagnitude(
|
||||
targetAngularVelocity,
|
||||
float fixedDeltaTime = Mathf.Max(Time.fixedDeltaTime, 0.001f);
|
||||
Vector3 targetAngularVelocity = Vector3.ClampMagnitude(
|
||||
axis.normalized * angle * Mathf.Deg2Rad * rotationResponsiveness,
|
||||
rotationFollowSpeed);
|
||||
}
|
||||
|
||||
private bool HasBlockingColliderForLocalCorrection(Vector3 targetDelta)
|
||||
{
|
||||
float distance = targetDelta.magnitude;
|
||||
if (distance <= 0.0001f)
|
||||
return false;
|
||||
|
||||
int hitCount = Physics.RaycastNonAlloc(
|
||||
physicsBody.position,
|
||||
targetDelta / distance,
|
||||
localCorrectionHits,
|
||||
distance,
|
||||
~0,
|
||||
QueryTriggerInteraction.Ignore);
|
||||
|
||||
for (int hitIndex = 0; hitIndex < hitCount; hitIndex++)
|
||||
{
|
||||
Collider hitCollider = localCorrectionHits[hitIndex].collider;
|
||||
if (hitCollider == null ||
|
||||
ignoredPlayerColliders.Contains(hitCollider) ||
|
||||
IsObjectCollider(hitCollider))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool IsObjectCollider(Collider candidate)
|
||||
{
|
||||
foreach (Collider objectCollider in objectColliders)
|
||||
{
|
||||
if (objectCollider == candidate)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
physicsBody.angularVelocity = Vector3.MoveTowards(
|
||||
physicsBody.angularVelocity,
|
||||
targetAngularVelocity,
|
||||
maximumAngularAcceleration * fixedDeltaTime);
|
||||
}
|
||||
|
||||
private void HandleHolderChanged(ulong _, ulong __) => ApplyHoldingState();
|
||||
@@ -369,6 +326,9 @@ public abstract class NetworkHeldInteractable : NetworkInteractable, IWorkstatio
|
||||
{
|
||||
bool isPhysicallyHeld = IsHeld && !IsExternallyControlled;
|
||||
bool shouldBeKinematic = IsExternallyControlled || !IsServer;
|
||||
physicsBody.interpolation = IsServer
|
||||
? RigidbodyInterpolation.Interpolate
|
||||
: RigidbodyInterpolation.None;
|
||||
|
||||
if (shouldBeKinematic)
|
||||
{
|
||||
|
||||
@@ -23,7 +23,7 @@ public sealed class ItemCatalog : ScriptableObject
|
||||
return false;
|
||||
}
|
||||
|
||||
return itemsById.TryGetValue(itemId.Trim().ToLowerInvariant(), out definition);
|
||||
return itemsById.TryGetValue(NormalizeId(itemId), out definition);
|
||||
}
|
||||
|
||||
private void RebuildIndex()
|
||||
@@ -39,4 +39,7 @@ public sealed class ItemCatalog : ScriptableObject
|
||||
Debug.LogError($"Duplicate item id '{definition.ItemId}' in {name}.", this);
|
||||
}
|
||||
}
|
||||
|
||||
private static string NormalizeId(string value) =>
|
||||
value.Trim().ToLowerInvariant().Replace(' ', '-');
|
||||
}
|
||||
|
||||
@@ -1,33 +1,23 @@
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
|
||||
public enum ItemCategory
|
||||
{
|
||||
Scrap,
|
||||
SmeltedMaterial,
|
||||
RocketPart,
|
||||
Tool,
|
||||
Consumable
|
||||
}
|
||||
|
||||
[CreateAssetMenu(fileName = "ItemDefinition", menuName = "FORT-BO/Items/Item Definition")]
|
||||
public sealed class ItemDefinition : ScriptableObject
|
||||
public class ItemDefinition : ScriptableObject
|
||||
{
|
||||
[SerializeField] private string itemId;
|
||||
[SerializeField] private string displayName;
|
||||
[SerializeField] private ItemCategory category;
|
||||
[SerializeField] private bool isAssemblyReady;
|
||||
[SerializeField] private NetworkObject worldPrefab;
|
||||
|
||||
public string ItemId => itemId;
|
||||
public string DisplayName => string.IsNullOrWhiteSpace(displayName) ? name : displayName;
|
||||
public ItemCategory Category => category;
|
||||
public bool IsAssemblyReady => isAssemblyReady;
|
||||
public NetworkObject WorldPrefab => worldPrefab;
|
||||
|
||||
private void OnValidate()
|
||||
protected virtual void OnValidate()
|
||||
{
|
||||
itemId = itemId?.Trim().ToLowerInvariant();
|
||||
itemId = NormalizeId(itemId);
|
||||
displayName = displayName?.Trim();
|
||||
}
|
||||
|
||||
protected static string NormalizeId(string value) =>
|
||||
value?.Trim().ToLowerInvariant().Replace(' ', '-');
|
||||
}
|
||||
|
||||
@@ -10,5 +10,11 @@ public sealed class NetworkItem : NetworkBehaviour
|
||||
public ItemDefinition Definition => definition;
|
||||
public string ItemId => definition != null ? definition.ItemId : string.Empty;
|
||||
public string DisplayName => definition != null ? definition.DisplayName : name;
|
||||
public ItemCategory Category => definition != null ? definition.Category : default;
|
||||
|
||||
public bool TryGetDefinition<TDefinition>(out TDefinition typedDefinition)
|
||||
where TDefinition : ItemDefinition
|
||||
{
|
||||
typedDefinition = definition as TDefinition;
|
||||
return typedDefinition != null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d1e6d4d81ab34d2e9ee3a1bfa201f100
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
using UnityEngine;
|
||||
|
||||
[CreateAssetMenu(
|
||||
fileName = "MaterialItemDefinition",
|
||||
menuName = "FORT-BO/Items/Definitions/Material")]
|
||||
public sealed class MaterialItemDefinition : ItemDefinition
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d1e6d4d81ab34d2e9ee3a1bfa201f101
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,29 @@
|
||||
using UnityEngine;
|
||||
|
||||
public enum PartProcessingState : byte
|
||||
{
|
||||
Raw,
|
||||
Ready
|
||||
}
|
||||
|
||||
[CreateAssetMenu(
|
||||
fileName = "PartItemDefinition",
|
||||
menuName = "FORT-BO/Items/Definitions/Part")]
|
||||
public sealed class PartItemDefinition : ItemDefinition
|
||||
{
|
||||
[Tooltip("Data-driven part family id, for example rocket-engine or rocket-cone-standard.")]
|
||||
[SerializeField] private string partTypeId;
|
||||
[SerializeField] private PartProcessingState processingState;
|
||||
|
||||
public string PartTypeId => string.IsNullOrWhiteSpace(partTypeId)
|
||||
? ItemId
|
||||
: partTypeId;
|
||||
public PartProcessingState ProcessingState => processingState;
|
||||
public bool IsReadyForAssembly => processingState == PartProcessingState.Ready;
|
||||
|
||||
protected override void OnValidate()
|
||||
{
|
||||
base.OnValidate();
|
||||
partTypeId = NormalizeId(partTypeId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d1e6d4d81ab34d2e9ee3a1bfa201f102
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,22 @@
|
||||
using UnityEngine;
|
||||
|
||||
[CreateAssetMenu(
|
||||
fileName = "RocketItemDefinition",
|
||||
menuName = "FORT-BO/Items/Definitions/Rocket")]
|
||||
public sealed class RocketItemDefinition : ItemDefinition
|
||||
{
|
||||
[Tooltip("Stable rocket family id used by launchers and orders.")]
|
||||
[SerializeField] private string rocketTypeId;
|
||||
[SerializeField, Min(0.1f)] private float criticalImpactSpeed = 7f;
|
||||
|
||||
public string RocketTypeId => string.IsNullOrWhiteSpace(rocketTypeId)
|
||||
? ItemId
|
||||
: rocketTypeId;
|
||||
public float CriticalImpactSpeed => criticalImpactSpeed;
|
||||
|
||||
protected override void OnValidate()
|
||||
{
|
||||
base.OnValidate();
|
||||
rocketTypeId = NormalizeId(rocketTypeId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d1e6d4d81ab34d2e9ee3a1bfa201f103
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
using UnityEngine;
|
||||
|
||||
[CreateAssetMenu(
|
||||
fileName = "UsableItemDefinition",
|
||||
menuName = "FORT-BO/Items/Definitions/Usable")]
|
||||
public sealed class UsableItemDefinition : ItemDefinition
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d1e6d4d81ab34d2e9ee3a1bfa201f104
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d1e6d4d81ab34d2e9ee3a1bfa201f110
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,168 @@
|
||||
using System;
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
|
||||
public enum RocketAssemblyQuality : byte
|
||||
{
|
||||
Normal,
|
||||
Defective
|
||||
}
|
||||
|
||||
public struct RocketCriticalImpactState : INetworkSerializable,
|
||||
IEquatable<RocketCriticalImpactState>
|
||||
{
|
||||
public uint Sequence;
|
||||
public Vector3 Point;
|
||||
public float Speed;
|
||||
|
||||
public RocketCriticalImpactState(uint sequence, Vector3 point, float speed)
|
||||
{
|
||||
Sequence = sequence;
|
||||
Point = point;
|
||||
Speed = speed;
|
||||
}
|
||||
|
||||
public void NetworkSerialize<T>(BufferSerializer<T> serializer)
|
||||
where T : IReaderWriter
|
||||
{
|
||||
serializer.SerializeValue(ref Sequence);
|
||||
serializer.SerializeValue(ref Point);
|
||||
serializer.SerializeValue(ref Speed);
|
||||
}
|
||||
|
||||
public bool Equals(RocketCriticalImpactState other) =>
|
||||
Sequence == other.Sequence && Point == other.Point && Speed.Equals(other.Speed);
|
||||
}
|
||||
|
||||
[DisallowMultipleComponent]
|
||||
[RequireComponent(typeof(NetworkItem), typeof(Rigidbody))]
|
||||
public sealed class NetworkRocketItem : NetworkBehaviour
|
||||
{
|
||||
[SerializeField] private NetworkItem networkItem;
|
||||
[SerializeField] private Rigidbody physicsBody;
|
||||
|
||||
private readonly NetworkVariable<RocketAssemblyQuality> assemblyQuality = new(
|
||||
RocketAssemblyQuality.Normal,
|
||||
NetworkVariableReadPermission.Everyone,
|
||||
NetworkVariableWritePermission.Server);
|
||||
|
||||
private readonly NetworkVariable<RocketCriticalImpactState> criticalImpactState = new(
|
||||
default,
|
||||
NetworkVariableReadPermission.Everyone,
|
||||
NetworkVariableWritePermission.Server);
|
||||
|
||||
public RocketItemDefinition Definition =>
|
||||
networkItem != null && networkItem.TryGetDefinition(out RocketItemDefinition definition)
|
||||
? definition
|
||||
: null;
|
||||
public string RocketTypeId => Definition != null ? Definition.RocketTypeId : string.Empty;
|
||||
public RocketAssemblyQuality AssemblyQuality => assemblyQuality.Value;
|
||||
public bool IsDefective => AssemblyQuality == RocketAssemblyQuality.Defective;
|
||||
public bool IsCriticallyDamaged => criticalImpactState.Value.Sequence > 0;
|
||||
public uint CriticalImpactSequence => criticalImpactState.Value.Sequence;
|
||||
public Vector3 LastCriticalImpactPoint => criticalImpactState.Value.Point;
|
||||
public float LastCriticalImpactSpeed => criticalImpactState.Value.Speed;
|
||||
|
||||
public event Action Changed;
|
||||
public event Action CriticalImpact;
|
||||
|
||||
private void Reset() => ResolveReferences();
|
||||
|
||||
private void Awake() => ResolveReferences();
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
assemblyQuality.OnValueChanged += HandleAssemblyQualityChanged;
|
||||
criticalImpactState.OnValueChanged += HandleCriticalImpactChanged;
|
||||
|
||||
if (Definition == null)
|
||||
{
|
||||
Debug.LogError(
|
||||
$"{name} has NetworkRocketItem but its NetworkItem does not reference " +
|
||||
"a RocketItemDefinition.",
|
||||
this);
|
||||
}
|
||||
|
||||
Changed?.Invoke();
|
||||
}
|
||||
|
||||
public override void OnNetworkDespawn()
|
||||
{
|
||||
assemblyQuality.OnValueChanged -= HandleAssemblyQualityChanged;
|
||||
criticalImpactState.OnValueChanged -= HandleCriticalImpactChanged;
|
||||
}
|
||||
|
||||
private void OnCollisionEnter(Collision collision)
|
||||
{
|
||||
if (!IsServer || !IsSpawned || IsCriticallyDamaged || Definition == null)
|
||||
return;
|
||||
|
||||
float impactSpeed = collision.relativeVelocity.magnitude;
|
||||
if (impactSpeed < Definition.CriticalImpactSpeed)
|
||||
return;
|
||||
|
||||
Vector3 impactPoint = collision.contactCount > 0
|
||||
? collision.GetContact(0).point
|
||||
: transform.position;
|
||||
TryTriggerCriticalImpact(impactPoint, impactSpeed);
|
||||
}
|
||||
|
||||
public bool TryTriggerCriticalImpact(Vector3 impactPoint, float impactSpeed)
|
||||
{
|
||||
if (!IsServer || !IsSpawned || IsCriticallyDamaged || Definition == null ||
|
||||
impactSpeed < Definition.CriticalImpactSpeed)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
criticalImpactState.Value = new RocketCriticalImpactState(
|
||||
criticalImpactState.Value.Sequence + 1,
|
||||
impactPoint,
|
||||
impactSpeed);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryInitializeAssemblyQuality(bool isDefectiveResult)
|
||||
{
|
||||
if (!IsServer || !IsSpawned)
|
||||
return false;
|
||||
|
||||
assemblyQuality.Value = isDefectiveResult
|
||||
? RocketAssemblyQuality.Defective
|
||||
: RocketAssemblyQuality.Normal;
|
||||
return true;
|
||||
}
|
||||
|
||||
[ContextMenu("Debug/Trigger Critical Impact (Server Play Mode)")]
|
||||
private void TriggerCriticalImpactFromContextMenu()
|
||||
{
|
||||
if (Application.isPlaying && IsServer && IsSpawned && Definition != null)
|
||||
{
|
||||
TryTriggerCriticalImpact(
|
||||
transform.position,
|
||||
Definition.CriticalImpactSpeed);
|
||||
}
|
||||
}
|
||||
|
||||
private void ResolveReferences()
|
||||
{
|
||||
if (networkItem == null)
|
||||
networkItem = GetComponent<NetworkItem>();
|
||||
|
||||
if (physicsBody == null)
|
||||
physicsBody = GetComponent<Rigidbody>();
|
||||
}
|
||||
|
||||
private void HandleAssemblyQualityChanged(
|
||||
RocketAssemblyQuality _,
|
||||
RocketAssemblyQuality __) => Changed?.Invoke();
|
||||
|
||||
private void HandleCriticalImpactChanged(
|
||||
RocketCriticalImpactState previous,
|
||||
RocketCriticalImpactState current)
|
||||
{
|
||||
Changed?.Invoke();
|
||||
if (current.Sequence != previous.Sequence)
|
||||
CriticalImpact?.Invoke();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d1e6d4d81ab34d2e9ee3a1bfa201f111
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,102 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
|
||||
[DisallowMultipleComponent]
|
||||
public sealed class RocketItemView : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private NetworkRocketItem rocket;
|
||||
|
||||
[Header("State")]
|
||||
[SerializeField] private UnityEvent onNormalQualityApplied;
|
||||
[SerializeField] private UnityEvent onDefectiveQualityApplied;
|
||||
[SerializeField] private UnityEvent onDamagedStateApplied;
|
||||
|
||||
[Header("Critical Impact")]
|
||||
[SerializeField] private UnityEvent onCriticalImpact;
|
||||
[SerializeField] private UnityEvent<Vector3> onCriticalImpactPoint;
|
||||
[SerializeField] private UnityEvent<float> onCriticalImpactSpeed;
|
||||
|
||||
private uint observedImpactSequence;
|
||||
private RocketAssemblyQuality observedQuality;
|
||||
private bool hasAppliedQuality;
|
||||
private bool hasAppliedDamagedState;
|
||||
|
||||
private void Reset() => rocket = GetComponentInParent<NetworkRocketItem>();
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (rocket == null)
|
||||
rocket = GetComponentInParent<NetworkRocketItem>();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (rocket == null)
|
||||
return;
|
||||
|
||||
observedImpactSequence = rocket.CriticalImpactSequence;
|
||||
observedQuality = rocket.AssemblyQuality;
|
||||
hasAppliedQuality = false;
|
||||
hasAppliedDamagedState = false;
|
||||
rocket.Changed += ApplyState;
|
||||
rocket.CriticalImpact += HandleCriticalImpact;
|
||||
ApplyState();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (rocket == null)
|
||||
return;
|
||||
|
||||
rocket.Changed -= ApplyState;
|
||||
rocket.CriticalImpact -= HandleCriticalImpact;
|
||||
}
|
||||
|
||||
[ContextMenu("Preview/Critical Impact")]
|
||||
private void PreviewCriticalImpact()
|
||||
{
|
||||
onCriticalImpactPoint?.Invoke(transform.position);
|
||||
onCriticalImpactSpeed?.Invoke(7f);
|
||||
onCriticalImpact?.Invoke();
|
||||
onDamagedStateApplied?.Invoke();
|
||||
}
|
||||
|
||||
[ContextMenu("Preview/Normal Quality")]
|
||||
private void PreviewNormalQuality() => onNormalQualityApplied?.Invoke();
|
||||
|
||||
[ContextMenu("Preview/Defective Quality")]
|
||||
private void PreviewDefectiveQuality() => onDefectiveQualityApplied?.Invoke();
|
||||
|
||||
private void ApplyState()
|
||||
{
|
||||
if (rocket == null)
|
||||
return;
|
||||
|
||||
if (!hasAppliedQuality || observedQuality != rocket.AssemblyQuality)
|
||||
{
|
||||
observedQuality = rocket.AssemblyQuality;
|
||||
hasAppliedQuality = true;
|
||||
if (observedQuality == RocketAssemblyQuality.Defective)
|
||||
onDefectiveQualityApplied?.Invoke();
|
||||
else
|
||||
onNormalQualityApplied?.Invoke();
|
||||
}
|
||||
|
||||
if (rocket.IsCriticallyDamaged && !hasAppliedDamagedState)
|
||||
{
|
||||
hasAppliedDamagedState = true;
|
||||
onDamagedStateApplied?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleCriticalImpact()
|
||||
{
|
||||
if (rocket == null || rocket.CriticalImpactSequence == observedImpactSequence)
|
||||
return;
|
||||
|
||||
observedImpactSequence = rocket.CriticalImpactSequence;
|
||||
onCriticalImpactPoint?.Invoke(rocket.LastCriticalImpactPoint);
|
||||
onCriticalImpactSpeed?.Invoke(rocket.LastCriticalImpactSpeed);
|
||||
onCriticalImpact?.Invoke();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d1e6d4d81ab34d2e9ee3a1bfa201f112
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d1e6d4d81ab34d2e9ee3a1bfa201f130
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,6 @@
|
||||
using Unity.Netcode;
|
||||
|
||||
public interface IHandItemUseAction
|
||||
{
|
||||
bool TryUseOnServer(NetworkObject playerObject);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d1e6d4d81ab34d2e9ee3a1bfa201f131
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,49 @@
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
|
||||
[DisallowMultipleComponent]
|
||||
public sealed class NetworkUsableHandItemInteractable : NetworkHandItemInteractable
|
||||
{
|
||||
[Tooltip("Component on this prefab implementing IHandItemUseAction.")]
|
||||
[SerializeField] private MonoBehaviour useActionComponent;
|
||||
|
||||
private IHandItemUseAction UseAction => useActionComponent as IHandItemUseAction;
|
||||
|
||||
protected override void Awake()
|
||||
{
|
||||
base.Awake();
|
||||
ResolveUseAction();
|
||||
}
|
||||
|
||||
private void OnValidate() => ResolveUseAction();
|
||||
|
||||
protected override void UseOnServer(NetworkObject playerObject)
|
||||
{
|
||||
if (UseAction == null)
|
||||
{
|
||||
Debug.LogWarning(
|
||||
$"{name} was used but has no IHandItemUseAction component assigned.",
|
||||
this);
|
||||
return;
|
||||
}
|
||||
|
||||
UseAction.TryUseOnServer(playerObject);
|
||||
}
|
||||
|
||||
private void ResolveUseAction()
|
||||
{
|
||||
if (useActionComponent is IHandItemUseAction)
|
||||
return;
|
||||
|
||||
foreach (MonoBehaviour component in GetComponents<MonoBehaviour>())
|
||||
{
|
||||
if (component is IHandItemUseAction)
|
||||
{
|
||||
useActionComponent = component;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
useActionComponent = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d1e6d4d81ab34d2e9ee3a1bfa201f132
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -4,6 +4,7 @@ using UnityEngine;
|
||||
public sealed class FirstPersonCameraController : NetworkBehaviour, IPlayerSystem
|
||||
{
|
||||
[SerializeField, Range(1f, 89f)] private float verticalLookLimit = 80f;
|
||||
[SerializeField, Min(0.1f)] private float remotePitchFollowSpeed = 20f;
|
||||
|
||||
private readonly NetworkVariable<float> synchronizedPitch = new(
|
||||
0f,
|
||||
@@ -14,6 +15,7 @@ public sealed class FirstPersonCameraController : NetworkBehaviour, IPlayerSyste
|
||||
private AudioListener audioListener;
|
||||
private MeshRenderer playerRenderer;
|
||||
private float pitch;
|
||||
private float targetPitch;
|
||||
private bool isInitialized;
|
||||
|
||||
public Camera PlayerCamera => playerCamera;
|
||||
@@ -34,7 +36,10 @@ public sealed class FirstPersonCameraController : NetworkBehaviour, IPlayerSyste
|
||||
playerCamera.gameObject.SetActive(isLocalPlayer);
|
||||
|
||||
if (!isLocalPlayer)
|
||||
ApplyCameraPitch(synchronizedPitch.Value);
|
||||
{
|
||||
targetPitch = synchronizedPitch.Value;
|
||||
ApplyCameraPitch(targetPitch);
|
||||
}
|
||||
|
||||
if (!isLocalPlayer)
|
||||
return;
|
||||
@@ -62,7 +67,10 @@ public sealed class FirstPersonCameraController : NetworkBehaviour, IPlayerSyste
|
||||
synchronizedPitch.OnValueChanged += HandleSynchronizedPitchChanged;
|
||||
|
||||
if (isInitialized && !IsOwner)
|
||||
ApplyCameraPitch(synchronizedPitch.Value);
|
||||
{
|
||||
targetPitch = synchronizedPitch.Value;
|
||||
ApplyCameraPitch(targetPitch);
|
||||
}
|
||||
|
||||
if (isInitialized && IsOwner)
|
||||
CaptureCursor();
|
||||
@@ -70,7 +78,17 @@ public sealed class FirstPersonCameraController : NetworkBehaviour, IPlayerSyste
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
if (!isInitialized || !IsOwner || !Application.isFocused)
|
||||
if (!isInitialized)
|
||||
return;
|
||||
|
||||
if (!IsOwner)
|
||||
{
|
||||
float interpolation = 1f - Mathf.Exp(-remotePitchFollowSpeed * Time.deltaTime);
|
||||
ApplyCameraPitch(Mathf.LerpAngle(pitch, targetPitch, interpolation));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Application.isFocused)
|
||||
return;
|
||||
|
||||
if (Cursor.lockState != CursorLockMode.Locked || Cursor.visible)
|
||||
@@ -89,6 +107,7 @@ public sealed class FirstPersonCameraController : NetworkBehaviour, IPlayerSyste
|
||||
return;
|
||||
|
||||
pitch = Mathf.Clamp(pitch - pitchDelta, -verticalLookLimit, verticalLookLimit);
|
||||
targetPitch = pitch;
|
||||
ApplyCameraPitch(pitch);
|
||||
|
||||
if (IsSpawned)
|
||||
@@ -110,7 +129,7 @@ public sealed class FirstPersonCameraController : NetworkBehaviour, IPlayerSyste
|
||||
private void HandleSynchronizedPitchChanged(float _, float currentPitch)
|
||||
{
|
||||
if (!IsOwner)
|
||||
ApplyCameraPitch(currentPitch);
|
||||
targetPitch = currentPitch;
|
||||
}
|
||||
|
||||
private void ApplyCameraPitch(float currentPitch)
|
||||
|
||||
+65
-19
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user