Added new Rocket-Processing
This commit is contained in:
@@ -66,11 +66,16 @@ public sealed class NetworkDraggableInteractable : NetworkInteractable, IWorksta
|
||||
NetworkVariableWritePermission.Server);
|
||||
|
||||
private readonly HashSet<Collider> ignoredPlayerColliders = new();
|
||||
private readonly HashSet<Collider> desiredIgnoredColliders = new();
|
||||
private readonly List<Collider> collidersToRestore = new();
|
||||
private readonly RaycastHit[] groundHits = new RaycastHit[16];
|
||||
private float nextCollisionRefreshTime;
|
||||
private Vector3 localGroundContactPoint;
|
||||
private Vector3 smoothedGroundNormal = Vector3.up;
|
||||
private Vector3 dragVelocity;
|
||||
private float dragWorldHeight;
|
||||
private bool hasDragReferenceHeight;
|
||||
private bool initialUseGravity;
|
||||
private CollisionDetectionMode initialCollisionDetectionMode;
|
||||
|
||||
public float Weight => weight;
|
||||
public int ActiveDraggerCount
|
||||
@@ -125,6 +130,12 @@ public sealed class NetworkDraggableInteractable : NetworkInteractable, IWorksta
|
||||
{
|
||||
ResolvePhysicsReferences();
|
||||
ApplyWeight();
|
||||
|
||||
if (physicsBody != null)
|
||||
{
|
||||
initialUseGravity = physicsBody.useGravity;
|
||||
initialCollisionDetectionMode = physicsBody.collisionDetectionMode;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnValidate()
|
||||
@@ -157,7 +168,7 @@ public sealed class NetworkDraggableInteractable : NetworkInteractable, IWorksta
|
||||
return;
|
||||
|
||||
nextCollisionRefreshTime = Time.time + collisionRefreshInterval;
|
||||
IgnoreCollisionsWithPlayers();
|
||||
RefreshDraggerCollisions();
|
||||
}
|
||||
|
||||
private void FixedUpdate()
|
||||
@@ -168,10 +179,7 @@ public sealed class NetworkDraggableInteractable : NetworkInteractable, IWorksta
|
||||
RemoveInvalidDraggers();
|
||||
if (!HasEnoughPullStrength ||
|
||||
!TryGetCombinedDragTarget(out Vector3 target, out Vector3 forward))
|
||||
{
|
||||
dragVelocity = Vector3.zero;
|
||||
return;
|
||||
}
|
||||
|
||||
float totalStrength = ActiveDraggerCount * pullStrengthPerPlayer;
|
||||
float loadRatio = Mathf.Clamp01(weight / totalStrength);
|
||||
@@ -181,7 +189,7 @@ public sealed class NetworkDraggableInteractable : NetworkInteractable, IWorksta
|
||||
switch (movementMode)
|
||||
{
|
||||
case DraggableMovementMode.KeepWorldHeight:
|
||||
target.y = physicsBody.position.y;
|
||||
target.y = dragWorldHeight;
|
||||
FollowDragTarget(target, forward, movementSpeed, Vector3.up);
|
||||
break;
|
||||
|
||||
@@ -360,6 +368,21 @@ public sealed class NetworkDraggableInteractable : NetworkInteractable, IWorksta
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryReleaseExternalControlToPlayer(ulong clientId)
|
||||
{
|
||||
if (!IsServer || !IsSpawned || !IsExternallyControlled ||
|
||||
TryGetDraggedObject(clientId, this, out _) ||
|
||||
!TryGetPlayerObject(clientId, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
isExternallyControlled.Value = false;
|
||||
AddDragger(clientId);
|
||||
ApplyDraggingState();
|
||||
return IsDragger(clientId);
|
||||
}
|
||||
|
||||
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
|
||||
private void RequestReleaseRpc(RpcParams rpcParams = default)
|
||||
{
|
||||
@@ -379,34 +402,52 @@ public sealed class NetworkDraggableInteractable : NetworkInteractable, IWorksta
|
||||
{
|
||||
if (physicsBody != null)
|
||||
{
|
||||
bool shouldBeKinematic = HasAnyDragger || IsExternallyControlled || !IsServer;
|
||||
bool shouldBeKinematic = IsExternallyControlled || !IsServer;
|
||||
|
||||
if (physicsBody.isKinematic && !shouldBeKinematic)
|
||||
if (shouldBeKinematic)
|
||||
{
|
||||
physicsBody.collisionDetectionMode = CollisionDetectionMode.ContinuousSpeculative;
|
||||
physicsBody.isKinematic = true;
|
||||
physicsBody.useGravity = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
physicsBody.isKinematic = false;
|
||||
physicsBody.useGravity = initialUseGravity;
|
||||
physicsBody.collisionDetectionMode = ResolveDynamicCollisionDetectionMode();
|
||||
}
|
||||
|
||||
if (!physicsBody.isKinematic)
|
||||
if (IsExternallyControlled)
|
||||
{
|
||||
physicsBody.linearVelocity = Vector3.zero;
|
||||
physicsBody.angularVelocity = Vector3.zero;
|
||||
}
|
||||
|
||||
physicsBody.isKinematic = shouldBeKinematic;
|
||||
}
|
||||
|
||||
if (HasAnyDragger || IsExternallyControlled)
|
||||
if (HasAnyDragger)
|
||||
{
|
||||
dragVelocity = Vector3.zero;
|
||||
if (!hasDragReferenceHeight && physicsBody != null)
|
||||
{
|
||||
dragWorldHeight = physicsBody.position.y;
|
||||
hasDragReferenceHeight = true;
|
||||
}
|
||||
|
||||
CacheGroundContactPoint();
|
||||
smoothedGroundNormal = physicsBody != null ? physicsBody.transform.up : Vector3.up;
|
||||
IgnoreCollisionsWithPlayers();
|
||||
RefreshDraggerCollisions();
|
||||
}
|
||||
else
|
||||
{
|
||||
dragVelocity = Vector3.zero;
|
||||
hasDragReferenceHeight = false;
|
||||
RestorePlayerCollisions();
|
||||
}
|
||||
}
|
||||
|
||||
private CollisionDetectionMode ResolveDynamicCollisionDetectionMode() =>
|
||||
initialCollisionDetectionMode == CollisionDetectionMode.Discrete
|
||||
? CollisionDetectionMode.ContinuousDynamic
|
||||
: initialCollisionDetectionMode;
|
||||
|
||||
private void ResolvePhysicsReferences()
|
||||
{
|
||||
if (physicsBody == null)
|
||||
@@ -430,16 +471,11 @@ public sealed class NetworkDraggableInteractable : NetworkInteractable, IWorksta
|
||||
float movementSpeed,
|
||||
Vector3 surfaceNormal)
|
||||
{
|
||||
Vector3 nextPosition = Vector3.SmoothDamp(
|
||||
physicsBody.position,
|
||||
DrivePhysicsBody(
|
||||
target,
|
||||
ref dragVelocity,
|
||||
positionSmoothTime,
|
||||
CalculateDragRotation(forward, surfaceNormal),
|
||||
movementSpeed,
|
||||
Time.fixedDeltaTime);
|
||||
|
||||
physicsBody.MovePosition(nextPosition);
|
||||
physicsBody.MoveRotation(CalculateDragRotation(forward, surfaceNormal));
|
||||
movementSpeed);
|
||||
}
|
||||
|
||||
private void MoveAlongGround(
|
||||
@@ -449,18 +485,10 @@ public sealed class NetworkDraggableInteractable : NetworkInteractable, IWorksta
|
||||
{
|
||||
Vector3 currentPosition = physicsBody.position;
|
||||
Vector3 planarTarget = new(dragTarget.x, currentPosition.y, dragTarget.z);
|
||||
Vector3 planarVelocity = new(dragVelocity.x, 0f, dragVelocity.z);
|
||||
Vector3 nextPlanarPosition = Vector3.SmoothDamp(
|
||||
currentPosition,
|
||||
planarTarget,
|
||||
ref planarVelocity,
|
||||
positionSmoothTime,
|
||||
movementSpeed,
|
||||
Time.fixedDeltaTime);
|
||||
nextPlanarPosition.y = currentPosition.y;
|
||||
dragVelocity = new Vector3(planarVelocity.x, dragVelocity.y, planarVelocity.z);
|
||||
|
||||
Vector3 probePosition = nextPlanarPosition;
|
||||
Vector3 planarStep = Vector3.ClampMagnitude(
|
||||
planarTarget - currentPosition,
|
||||
movementSpeed * Time.fixedDeltaTime);
|
||||
Vector3 probePosition = currentPosition + planarStep;
|
||||
|
||||
if (!TryGetGroundSurface(probePosition, out Vector3 groundPoint, out Vector3 groundNormal))
|
||||
return;
|
||||
@@ -469,14 +497,55 @@ public sealed class NetworkDraggableInteractable : NetworkInteractable, IWorksta
|
||||
Vector3 scaledContactPoint = Vector3.Scale(localGroundContactPoint, physicsBody.transform.lossyScale);
|
||||
float contactOffsetY = (targetRotation * scaledContactPoint).y;
|
||||
float targetHeight = groundPoint.y - contactOffsetY + groundClearance;
|
||||
float nextHeight = Mathf.MoveTowards(
|
||||
currentPosition.y,
|
||||
targetHeight,
|
||||
maximumGroundVerticalSpeed * Time.fixedDeltaTime);
|
||||
DrivePhysicsBody(
|
||||
new Vector3(dragTarget.x, targetHeight, dragTarget.z),
|
||||
targetRotation,
|
||||
movementSpeed,
|
||||
maximumGroundVerticalSpeed);
|
||||
}
|
||||
|
||||
physicsBody.MovePosition(new Vector3(nextPlanarPosition.x, nextHeight, nextPlanarPosition.z));
|
||||
private void DrivePhysicsBody(
|
||||
Vector3 targetPosition,
|
||||
Quaternion targetRotation,
|
||||
float maximumPlanarSpeed,
|
||||
float maximumVerticalSpeed)
|
||||
{
|
||||
Vector3 positionDelta = targetPosition - physicsBody.position;
|
||||
float responseTime = Mathf.Max(positionSmoothTime, 0.01f);
|
||||
Vector3 desiredVelocity = positionDelta / responseTime;
|
||||
Vector3 desiredPlanarVelocity = Vector3.ClampMagnitude(
|
||||
new Vector3(desiredVelocity.x, 0f, desiredVelocity.z),
|
||||
maximumPlanarSpeed);
|
||||
desiredVelocity = new Vector3(
|
||||
desiredPlanarVelocity.x,
|
||||
Mathf.Clamp(desiredVelocity.y, -maximumVerticalSpeed, maximumVerticalSpeed),
|
||||
desiredPlanarVelocity.z);
|
||||
|
||||
physicsBody.MoveRotation(targetRotation);
|
||||
float maximumAcceleration = Mathf.Max(
|
||||
maximumPlanarSpeed,
|
||||
maximumVerticalSpeed) / responseTime;
|
||||
Vector3 velocityChange = Vector3.ClampMagnitude(
|
||||
desiredVelocity - physicsBody.linearVelocity,
|
||||
maximumAcceleration * Time.fixedDeltaTime);
|
||||
physicsBody.AddForce(velocityChange, ForceMode.VelocityChange);
|
||||
|
||||
Quaternion rotationDelta = targetRotation * Quaternion.Inverse(physicsBody.rotation);
|
||||
rotationDelta.ToAngleAxis(out float angle, out Vector3 axis);
|
||||
if (axis.sqrMagnitude < 0.0001f || float.IsNaN(axis.x))
|
||||
return;
|
||||
|
||||
if (angle > 180f)
|
||||
angle -= 360f;
|
||||
|
||||
Vector3 desiredAngularVelocity = axis.normalized *
|
||||
(angle * Mathf.Deg2Rad * rotationFollowSpeed);
|
||||
desiredAngularVelocity = Vector3.ClampMagnitude(
|
||||
desiredAngularVelocity,
|
||||
rotationFollowSpeed);
|
||||
Vector3 angularVelocityChange = Vector3.ClampMagnitude(
|
||||
desiredAngularVelocity - physicsBody.angularVelocity,
|
||||
rotationFollowSpeed * 4f * Time.fixedDeltaTime);
|
||||
physicsBody.AddTorque(angularVelocityChange, ForceMode.VelocityChange);
|
||||
}
|
||||
|
||||
private bool TryGetGroundSurface(
|
||||
@@ -538,8 +607,7 @@ public sealed class NetworkDraggableInteractable : NetworkInteractable, IWorksta
|
||||
Quaternion desiredRotation = Quaternion.LookRotation(
|
||||
surfaceForward.normalized,
|
||||
smoothedGroundNormal) * Quaternion.Euler(rotationOffset);
|
||||
float rotationBlend = 1f - Mathf.Exp(-rotationFollowSpeed * Time.fixedDeltaTime);
|
||||
return Quaternion.Slerp(physicsBody.rotation, desiredRotation, rotationBlend);
|
||||
return desiredRotation;
|
||||
}
|
||||
|
||||
private void CacheGroundContactPoint()
|
||||
@@ -596,19 +664,50 @@ public sealed class NetworkDraggableInteractable : NetworkInteractable, IWorksta
|
||||
return false;
|
||||
}
|
||||
|
||||
private void IgnoreCollisionsWithPlayers()
|
||||
private void RefreshDraggerCollisions()
|
||||
{
|
||||
foreach (CharacterController playerCollider in FindObjectsByType<CharacterController>(
|
||||
FindObjectsInactive.Exclude,
|
||||
FindObjectsSortMode.None))
|
||||
{
|
||||
foreach (Collider objectCollider in objectColliders)
|
||||
{
|
||||
if (objectCollider != null && playerCollider != null)
|
||||
Physics.IgnoreCollision(objectCollider, playerCollider, true);
|
||||
}
|
||||
desiredIgnoredColliders.Clear();
|
||||
AddDraggerColliders(firstDraggerClientId.Value);
|
||||
AddDraggerColliders(secondDraggerClientId.Value);
|
||||
|
||||
ignoredPlayerColliders.Add(playerCollider);
|
||||
collidersToRestore.Clear();
|
||||
foreach (Collider ignoredCollider in ignoredPlayerColliders)
|
||||
{
|
||||
if (ignoredCollider == null || !desiredIgnoredColliders.Contains(ignoredCollider))
|
||||
collidersToRestore.Add(ignoredCollider);
|
||||
}
|
||||
|
||||
foreach (Collider colliderToRestore in collidersToRestore)
|
||||
{
|
||||
SetCollisionIgnored(colliderToRestore, false);
|
||||
ignoredPlayerColliders.Remove(colliderToRestore);
|
||||
}
|
||||
|
||||
foreach (Collider desiredCollider in desiredIgnoredColliders)
|
||||
{
|
||||
if (desiredCollider != null && ignoredPlayerColliders.Add(desiredCollider))
|
||||
SetCollisionIgnored(desiredCollider, true);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddDraggerColliders(ulong clientId)
|
||||
{
|
||||
if (clientId == NoDragger || !TryGetPlayerObject(clientId, out NetworkObject playerObject))
|
||||
return;
|
||||
|
||||
foreach (Collider playerCollider in playerObject.GetComponentsInChildren<Collider>(true))
|
||||
desiredIgnoredColliders.Add(playerCollider);
|
||||
}
|
||||
|
||||
private void SetCollisionIgnored(Collider playerCollider, bool ignored)
|
||||
{
|
||||
if (playerCollider == null)
|
||||
return;
|
||||
|
||||
foreach (Collider objectCollider in objectColliders)
|
||||
{
|
||||
if (objectCollider != null)
|
||||
Physics.IgnoreCollision(objectCollider, playerCollider, ignored);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -619,13 +718,11 @@ public sealed class NetworkDraggableInteractable : NetworkInteractable, IWorksta
|
||||
if (playerCollider == null)
|
||||
continue;
|
||||
|
||||
foreach (Collider objectCollider in objectColliders)
|
||||
{
|
||||
if (objectCollider != null)
|
||||
Physics.IgnoreCollision(objectCollider, playerCollider, false);
|
||||
}
|
||||
SetCollisionIgnored(playerCollider, false);
|
||||
}
|
||||
|
||||
ignoredPlayerColliders.Clear();
|
||||
desiredIgnoredColliders.Clear();
|
||||
collidersToRestore.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ public abstract class NetworkHeldInteractable : NetworkInteractable, IWorkstatio
|
||||
[SerializeField, Min(0.01f)] private float positionSmoothTime = 0.12f;
|
||||
[SerializeField, Min(0.1f)] private float maximumFollowSpeed = 12f;
|
||||
[SerializeField, Min(0.1f)] private float maximumFollowDistance = 1.25f;
|
||||
[SerializeField, Min(0.5f)] private float maximumHoldDistance = 4.5f;
|
||||
[SerializeField, Min(0.1f)] private float rotationFollowSpeed = 14f;
|
||||
|
||||
[Header("Throw")]
|
||||
@@ -35,10 +36,11 @@ public abstract class NetworkHeldInteractable : NetworkInteractable, IWorkstatio
|
||||
NetworkVariableWritePermission.Server);
|
||||
|
||||
private readonly HashSet<Collider> ignoredPlayerColliders = new();
|
||||
private readonly HashSet<Collider> desiredIgnoredColliders = new();
|
||||
private readonly List<Collider> collidersToRestore = new();
|
||||
private float nextCollisionRefreshTime;
|
||||
private Vector3 followVelocity;
|
||||
private Vector3 previousHolderPosition;
|
||||
private bool hasPreviousHolderPosition;
|
||||
private bool initialUseGravity;
|
||||
private CollisionDetectionMode initialCollisionDetectionMode;
|
||||
|
||||
protected abstract Vector3 HoldingOffset { get; }
|
||||
protected abstract string DropPrompt { get; }
|
||||
@@ -68,6 +70,12 @@ public abstract class NetworkHeldInteractable : NetworkInteractable, IWorkstatio
|
||||
|
||||
if (objectColliders == null || objectColliders.Length == 0)
|
||||
objectColliders = GetComponentsInChildren<Collider>(true);
|
||||
|
||||
if (physicsBody != null)
|
||||
{
|
||||
initialUseGravity = physicsBody.useGravity;
|
||||
initialCollisionDetectionMode = physicsBody.collisionDetectionMode;
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
@@ -75,7 +83,7 @@ public abstract class NetworkHeldInteractable : NetworkInteractable, IWorkstatio
|
||||
SpawnedHeldObjects.Add(this);
|
||||
holderClientId.OnValueChanged += HandleHolderChanged;
|
||||
isExternallyControlled.OnValueChanged += HandleExternalControlChanged;
|
||||
ApplyHoldingState(IsHeld || IsExternallyControlled);
|
||||
ApplyHoldingState();
|
||||
}
|
||||
|
||||
public override void OnNetworkDespawn()
|
||||
@@ -83,12 +91,24 @@ public abstract class NetworkHeldInteractable : NetworkInteractable, IWorkstatio
|
||||
holderClientId.OnValueChanged -= HandleHolderChanged;
|
||||
isExternallyControlled.OnValueChanged -= HandleExternalControlChanged;
|
||||
SpawnedHeldObjects.Remove(this);
|
||||
ApplyHoldingState(false);
|
||||
RestorePlayerCollisions();
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
private void Update()
|
||||
{
|
||||
if (!IsSpawned || !IsHeld)
|
||||
if (!IsSpawned || !IsHeld || IsExternallyControlled ||
|
||||
Time.time < nextCollisionRefreshTime)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
nextCollisionRefreshTime = Time.time + collisionRefreshInterval;
|
||||
RefreshHolderCollisions();
|
||||
}
|
||||
|
||||
private void FixedUpdate()
|
||||
{
|
||||
if (!IsServer || !IsSpawned || !IsHeld || IsExternallyControlled || physicsBody == null)
|
||||
return;
|
||||
|
||||
if (!TryGetPlayerObject(holderClientId.Value, out NetworkObject holder))
|
||||
@@ -99,15 +119,10 @@ public abstract class NetworkHeldInteractable : NetworkInteractable, IWorkstatio
|
||||
return;
|
||||
}
|
||||
|
||||
FollowHolderTranslation(holder.transform.position);
|
||||
Transform anchor = ResolveHoldingAnchor(holder);
|
||||
FollowHoldingAnchor(anchor);
|
||||
if (!DriveTowardAnchor(anchor))
|
||||
return;
|
||||
|
||||
if (Time.time >= nextCollisionRefreshTime)
|
||||
{
|
||||
nextCollisionRefreshTime = Time.time + collisionRefreshInterval;
|
||||
IgnoreCollisionsWithPlayers();
|
||||
}
|
||||
}
|
||||
|
||||
public void RequestThrow()
|
||||
@@ -126,7 +141,7 @@ public abstract class NetworkHeldInteractable : NetworkInteractable, IWorkstatio
|
||||
if (IsHeld)
|
||||
holderClientId.Value = NoHolder;
|
||||
|
||||
ApplyHoldingState(true);
|
||||
ApplyHoldingState();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -136,7 +151,23 @@ public abstract class NetworkHeldInteractable : NetworkInteractable, IWorkstatio
|
||||
return false;
|
||||
|
||||
isExternallyControlled.Value = false;
|
||||
ApplyHoldingState(IsHeld);
|
||||
ApplyHoldingState();
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryReleaseExternalControlToPlayer(ulong clientId)
|
||||
{
|
||||
if (!IsServer || !IsSpawned || !IsExternallyControlled ||
|
||||
TryGetHeldObject(clientId, out _) ||
|
||||
NetworkHandItemInteractable.TryGetHeldItem(clientId, out _) ||
|
||||
!TryGetPlayerObject(clientId, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
isExternallyControlled.Value = false;
|
||||
holderClientId.Value = clientId;
|
||||
ApplyHoldingState();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -208,13 +239,13 @@ public abstract class NetworkHeldInteractable : NetworkInteractable, IWorkstatio
|
||||
return;
|
||||
|
||||
holderClientId.Value = clientId;
|
||||
ApplyHoldingState(true);
|
||||
ApplyHoldingState();
|
||||
}
|
||||
|
||||
private void StopHolding()
|
||||
{
|
||||
holderClientId.Value = NoHolder;
|
||||
ApplyHoldingState(IsExternallyControlled);
|
||||
ApplyHoldingState();
|
||||
}
|
||||
|
||||
private Transform ResolveHoldingAnchor(NetworkObject holder)
|
||||
@@ -224,94 +255,134 @@ public abstract class NetworkHeldInteractable : NetworkInteractable, IWorkstatio
|
||||
return cameraController != null ? cameraController.AimTransform : holder.transform;
|
||||
}
|
||||
|
||||
private void FollowHoldingAnchor(Transform anchor)
|
||||
private bool DriveTowardAnchor(Transform anchor)
|
||||
{
|
||||
Vector3 targetPosition = anchor.TransformPoint(HoldingOffset);
|
||||
Vector3 targetDelta = targetPosition - transform.position;
|
||||
Vector3 targetDelta = targetPosition - physicsBody.position;
|
||||
|
||||
if (targetDelta.sqrMagnitude > maximumFollowDistance * maximumFollowDistance)
|
||||
if (targetDelta.sqrMagnitude > maximumHoldDistance * maximumHoldDistance)
|
||||
{
|
||||
transform.position = targetPosition -
|
||||
targetDelta.normalized * maximumFollowDistance;
|
||||
followVelocity = Vector3.zero;
|
||||
StopHolding();
|
||||
return false;
|
||||
}
|
||||
|
||||
transform.position = Vector3.SmoothDamp(
|
||||
transform.position,
|
||||
targetPosition,
|
||||
ref followVelocity,
|
||||
positionSmoothTime,
|
||||
maximumFollowSpeed,
|
||||
Time.deltaTime);
|
||||
float distance = targetDelta.magnitude;
|
||||
float speedDistance = Mathf.Max(maximumFollowDistance, 0.01f);
|
||||
float desiredSpeed = maximumFollowSpeed * Mathf.Clamp01(distance / speedDistance);
|
||||
Vector3 desiredVelocity = distance > 0.0001f
|
||||
? targetDelta / distance * desiredSpeed
|
||||
: Vector3.zero;
|
||||
float maximumAcceleration = maximumFollowSpeed / Mathf.Max(positionSmoothTime, 0.01f);
|
||||
Vector3 velocityChange = Vector3.ClampMagnitude(
|
||||
desiredVelocity - physicsBody.linearVelocity,
|
||||
maximumAcceleration * Time.fixedDeltaTime);
|
||||
physicsBody.AddForce(velocityChange, ForceMode.VelocityChange);
|
||||
|
||||
float rotationBlend = 1f - Mathf.Exp(-rotationFollowSpeed * Time.deltaTime);
|
||||
transform.rotation = Quaternion.Slerp(
|
||||
transform.rotation,
|
||||
anchor.rotation,
|
||||
rotationBlend);
|
||||
DriveRotation(anchor.rotation);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void FollowHolderTranslation(Vector3 holderPosition)
|
||||
private void DriveRotation(Quaternion targetRotation)
|
||||
{
|
||||
if (!hasPreviousHolderPosition)
|
||||
{
|
||||
previousHolderPosition = holderPosition;
|
||||
hasPreviousHolderPosition = true;
|
||||
Quaternion rotationDelta = targetRotation * Quaternion.Inverse(physicsBody.rotation);
|
||||
rotationDelta.ToAngleAxis(out float angle, out Vector3 axis);
|
||||
if (axis.sqrMagnitude < 0.0001f || float.IsNaN(axis.x))
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3 holderDelta = holderPosition - previousHolderPosition;
|
||||
previousHolderPosition = holderPosition;
|
||||
if (angle > 180f)
|
||||
angle -= 360f;
|
||||
|
||||
transform.position += holderDelta;
|
||||
Vector3 desiredAngularVelocity = axis.normalized *
|
||||
(angle * Mathf.Deg2Rad * rotationFollowSpeed);
|
||||
desiredAngularVelocity = Vector3.ClampMagnitude(
|
||||
desiredAngularVelocity,
|
||||
rotationFollowSpeed);
|
||||
Vector3 angularVelocityChange = Vector3.ClampMagnitude(
|
||||
desiredAngularVelocity - physicsBody.angularVelocity,
|
||||
rotationFollowSpeed * 4f * Time.fixedDeltaTime);
|
||||
physicsBody.AddTorque(angularVelocityChange, ForceMode.VelocityChange);
|
||||
}
|
||||
|
||||
private void HandleHolderChanged(ulong _, ulong currentHolder) =>
|
||||
ApplyHoldingState(currentHolder != NoHolder || IsExternallyControlled);
|
||||
private void HandleHolderChanged(ulong _, ulong __) => ApplyHoldingState();
|
||||
|
||||
private void HandleExternalControlChanged(bool _, bool isControlled) =>
|
||||
ApplyHoldingState(IsHeld || isControlled);
|
||||
private void HandleExternalControlChanged(bool _, bool __) => ApplyHoldingState();
|
||||
|
||||
private void ApplyHoldingState(bool isHeld)
|
||||
private void ApplyHoldingState()
|
||||
{
|
||||
followVelocity = Vector3.zero;
|
||||
hasPreviousHolderPosition = false;
|
||||
|
||||
if (physicsBody != null)
|
||||
{
|
||||
bool shouldBeKinematic = isHeld || !IsServer;
|
||||
bool isPhysicallyHeld = IsHeld && !IsExternallyControlled;
|
||||
bool shouldBeKinematic = IsExternallyControlled || !IsServer;
|
||||
|
||||
if (physicsBody.isKinematic && !shouldBeKinematic)
|
||||
if (shouldBeKinematic)
|
||||
{
|
||||
physicsBody.collisionDetectionMode = CollisionDetectionMode.ContinuousSpeculative;
|
||||
physicsBody.isKinematic = true;
|
||||
physicsBody.useGravity = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
physicsBody.isKinematic = false;
|
||||
physicsBody.useGravity = isPhysicallyHeld ? false : initialUseGravity;
|
||||
physicsBody.collisionDetectionMode = ResolveDynamicCollisionDetectionMode();
|
||||
}
|
||||
|
||||
if (!physicsBody.isKinematic)
|
||||
if (!isPhysicallyHeld)
|
||||
{
|
||||
physicsBody.linearVelocity = Vector3.zero;
|
||||
physicsBody.angularVelocity = Vector3.zero;
|
||||
}
|
||||
|
||||
physicsBody.isKinematic = shouldBeKinematic;
|
||||
}
|
||||
|
||||
if (isHeld)
|
||||
IgnoreCollisionsWithPlayers();
|
||||
if (IsHeld && !IsExternallyControlled)
|
||||
RefreshHolderCollisions();
|
||||
else
|
||||
RestorePlayerCollisions();
|
||||
}
|
||||
|
||||
private void IgnoreCollisionsWithPlayers()
|
||||
{
|
||||
foreach (CharacterController playerCollider in FindObjectsByType<CharacterController>(
|
||||
FindObjectsInactive.Exclude,
|
||||
FindObjectsSortMode.None))
|
||||
{
|
||||
foreach (Collider objectCollider in objectColliders)
|
||||
{
|
||||
if (objectCollider != null && playerCollider != null)
|
||||
Physics.IgnoreCollision(objectCollider, playerCollider, true);
|
||||
}
|
||||
private CollisionDetectionMode ResolveDynamicCollisionDetectionMode() =>
|
||||
initialCollisionDetectionMode == CollisionDetectionMode.Discrete
|
||||
? CollisionDetectionMode.ContinuousDynamic
|
||||
: initialCollisionDetectionMode;
|
||||
|
||||
ignoredPlayerColliders.Add(playerCollider);
|
||||
private void RefreshHolderCollisions()
|
||||
{
|
||||
desiredIgnoredColliders.Clear();
|
||||
if (TryGetPlayerObject(holderClientId.Value, out NetworkObject holder))
|
||||
{
|
||||
foreach (Collider holderCollider in holder.GetComponentsInChildren<Collider>(true))
|
||||
desiredIgnoredColliders.Add(holderCollider);
|
||||
}
|
||||
|
||||
collidersToRestore.Clear();
|
||||
foreach (Collider ignoredCollider in ignoredPlayerColliders)
|
||||
{
|
||||
if (ignoredCollider == null || !desiredIgnoredColliders.Contains(ignoredCollider))
|
||||
collidersToRestore.Add(ignoredCollider);
|
||||
}
|
||||
|
||||
foreach (Collider colliderToRestore in collidersToRestore)
|
||||
{
|
||||
SetCollisionIgnored(colliderToRestore, false);
|
||||
ignoredPlayerColliders.Remove(colliderToRestore);
|
||||
}
|
||||
|
||||
foreach (Collider desiredCollider in desiredIgnoredColliders)
|
||||
{
|
||||
if (desiredCollider != null && ignoredPlayerColliders.Add(desiredCollider))
|
||||
SetCollisionIgnored(desiredCollider, true);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetCollisionIgnored(Collider playerCollider, bool ignored)
|
||||
{
|
||||
if (playerCollider == null)
|
||||
return;
|
||||
|
||||
foreach (Collider objectCollider in objectColliders)
|
||||
{
|
||||
if (objectCollider != null)
|
||||
Physics.IgnoreCollision(objectCollider, playerCollider, ignored);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,13 +393,11 @@ public abstract class NetworkHeldInteractable : NetworkInteractable, IWorkstatio
|
||||
if (playerCollider == null)
|
||||
continue;
|
||||
|
||||
foreach (Collider objectCollider in objectColliders)
|
||||
{
|
||||
if (objectCollider != null)
|
||||
Physics.IgnoreCollision(objectCollider, playerCollider, false);
|
||||
}
|
||||
SetCollisionIgnored(playerCollider, false);
|
||||
}
|
||||
|
||||
ignoredPlayerColliders.Clear();
|
||||
desiredIgnoredColliders.Clear();
|
||||
collidersToRestore.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,31 @@
|
||||
using UnityEngine;
|
||||
|
||||
public interface IWorkstationItemControl
|
||||
{
|
||||
bool TryAcquireExternalControl();
|
||||
bool TryReleaseExternalControl();
|
||||
bool TryReleaseExternalControlToPlayer(ulong clientId);
|
||||
}
|
||||
|
||||
public static class WorkstationItemControlUtility
|
||||
{
|
||||
public static bool TryGetControl(
|
||||
NetworkItem item,
|
||||
out IWorkstationItemControl itemControl)
|
||||
{
|
||||
if (item != null)
|
||||
{
|
||||
foreach (MonoBehaviour component in item.GetComponents<MonoBehaviour>())
|
||||
{
|
||||
if (component is IWorkstationItemControl candidate)
|
||||
{
|
||||
itemControl = candidate;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
itemControl = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,11 +16,13 @@ public sealed 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()
|
||||
|
||||
@@ -10,6 +10,7 @@ public sealed class PlayerInteractionController : NetworkBehaviour, IPlayerSyste
|
||||
private Camera playerCamera;
|
||||
private IInteractable focusedInteractable;
|
||||
private IPlayerInteractionSession activeSession;
|
||||
private readonly RaycastHit[] interactionHits = new RaycastHit[32];
|
||||
private bool activeSessionReleaseRequested;
|
||||
private bool isInitialized;
|
||||
|
||||
@@ -54,10 +55,57 @@ public sealed class PlayerInteractionController : NetworkBehaviour, IPlayerSyste
|
||||
private IInteractable FindFocusedInteractable()
|
||||
{
|
||||
Ray ray = new(playerCamera.transform.position, playerCamera.transform.forward);
|
||||
if (!Physics.Raycast(ray, out RaycastHit hit, interactionDistance, interactionMask, QueryTriggerInteraction.Collide))
|
||||
int hitCount = Physics.RaycastNonAlloc(
|
||||
ray,
|
||||
interactionHits,
|
||||
interactionDistance,
|
||||
interactionMask,
|
||||
QueryTriggerInteraction.Collide);
|
||||
if (hitCount <= 0)
|
||||
return null;
|
||||
|
||||
foreach (MonoBehaviour component in hit.collider.GetComponentsInParent<MonoBehaviour>())
|
||||
IInteractable closestAvailable = null;
|
||||
float closestAvailableDistance = float.PositiveInfinity;
|
||||
IInteractable closestUnavailable = null;
|
||||
float closestUnavailableDistance = float.PositiveInfinity;
|
||||
float closestBlockingDistance = float.PositiveInfinity;
|
||||
|
||||
for (int hitIndex = 0; hitIndex < hitCount; hitIndex++)
|
||||
{
|
||||
RaycastHit hit = interactionHits[hitIndex];
|
||||
IInteractable interactable = FindInteractable(hit.collider);
|
||||
if (interactable == null)
|
||||
{
|
||||
closestBlockingDistance = Mathf.Min(closestBlockingDistance, hit.distance);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (interactable.IsInteractionAvailable)
|
||||
{
|
||||
if (hit.distance < closestAvailableDistance)
|
||||
{
|
||||
closestAvailable = interactable;
|
||||
closestAvailableDistance = hit.distance;
|
||||
}
|
||||
}
|
||||
else if (hit.distance < closestUnavailableDistance)
|
||||
{
|
||||
closestUnavailable = interactable;
|
||||
closestUnavailableDistance = hit.distance;
|
||||
}
|
||||
}
|
||||
|
||||
if (closestAvailable != null && closestAvailableDistance <= closestBlockingDistance)
|
||||
return closestAvailable;
|
||||
|
||||
return closestUnavailable != null && closestUnavailableDistance <= closestBlockingDistance
|
||||
? closestUnavailable
|
||||
: null;
|
||||
}
|
||||
|
||||
private static IInteractable FindInteractable(Collider collider)
|
||||
{
|
||||
foreach (MonoBehaviour component in collider.GetComponentsInParent<MonoBehaviour>())
|
||||
{
|
||||
if (component is IInteractable interactable)
|
||||
return interactable;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e48d638d2d22411db1b67c8276c6f6c9
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+609
@@ -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();
|
||||
}
|
||||
+11
@@ -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);
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 42a6ab0b475f46aebda93f3c671b670a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+5
-18
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user