Files
FORT-BO/Assets/CONTENT/FEATURES/PipeJunk/ItemPipe.cs
T

292 lines
8.5 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using Unity.Netcode;
using UnityEngine;
[RequireComponent(typeof(NetworkObject))]
public sealed class ItemPipe : NetworkBehaviour
{
[Header("Pipe")]
[SerializeField] private Transform spawnPoint;
[Header("Configuration")]
[SerializeField] private ItemPipeConfig config;
private readonly NetworkVariable<bool> spawnSequenceActive = new(
false,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private readonly NetworkVariable<double> cooldownEndsAt = new(
0d,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private Coroutine spawnRoutine;
public float CooldownDuration => config != null ? config.SpawnCooldown : 0f;
public float CooldownRemaining
{
get
{
double now = NetworkManager != null
? NetworkManager.ServerTime.Time
: Time.timeAsDouble;
return Mathf.Max(0f, (float)(cooldownEndsAt.Value - now));
}
}
public int CooldownSecondsRemaining => Mathf.CeilToInt(CooldownRemaining);
public bool IsSpawning => spawnSequenceActive.Value;
public bool CanSpawnItems =>
IsSpawned &&
config != null &&
!spawnSequenceActive.Value &&
CooldownRemaining <= 0f;
public event Action Changed;
private void Reset() => spawnPoint = transform;
public override void OnNetworkSpawn()
{
spawnSequenceActive.OnValueChanged += HandleSpawnStateChanged;
cooldownEndsAt.OnValueChanged += HandleCooldownChanged;
if (IsServer)
{
spawnSequenceActive.Value = false;
cooldownEndsAt.Value = 0d;
}
Changed?.Invoke();
}
public override void OnNetworkDespawn()
{
spawnSequenceActive.OnValueChanged -= HandleSpawnStateChanged;
cooldownEndsAt.OnValueChanged -= HandleCooldownChanged;
if (spawnRoutine != null)
{
StopCoroutine(spawnRoutine);
spawnRoutine = null;
}
}
/// <summary>
/// Starts one configured batch. Safe to connect directly to a Button or UnityEvent.
/// Calls made on a client are forwarded to the server.
/// </summary>
public void SpawnItems()
{
if (!IsSpawned)
return;
if (IsServer)
TryStartSpawnSequenceOnServer();
else
RequestSpawnItemsRpc();
}
/// <summary>
/// UnityEvent&lt;bool&gt; adapter for a lever. Only the true edge starts a batch;
/// returning a momentary lever to false does not cancel the current batch.
/// </summary>
public void SpawnItems(bool activated)
{
if (activated)
SpawnItems();
}
/// <summary>
/// Starts while true and cancels the current batch while false. Use this when
/// the lever position must control whether the pipe is allowed to keep running.
/// </summary>
public void SetSpawning(bool shouldSpawn)
{
if (shouldSpawn)
SpawnItems();
else
StopSpawning();
}
public void StopSpawning()
{
if (!IsSpawned)
return;
if (IsServer)
StopSpawnSequenceOnServer(applyCooldown: true);
else
RequestStopSpawningRpc();
}
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
private void RequestSpawnItemsRpc() => TryStartSpawnSequenceOnServer();
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
private void RequestStopSpawningRpc() =>
StopSpawnSequenceOnServer(applyCooldown: true);
private bool TryStartSpawnSequenceOnServer()
{
if (!IsServer || !CanSpawnItems)
return false;
spawnSequenceActive.Value = true;
spawnRoutine = StartCoroutine(SpawnSequenceOnServer(config));
return true;
}
private IEnumerator SpawnSequenceOnServer(ItemPipeConfig activeConfig)
{
List<GameObject> spawnQueue = BuildSpawnQueue(activeConfig);
for (int index = 0; index < spawnQueue.Count; index++)
{
SpawnNetworkItemOnServer(spawnQueue[index], activeConfig);
if (index < spawnQueue.Count - 1)
yield return new WaitForSeconds(activeConfig.SpawnInterval);
}
spawnRoutine = null;
cooldownEndsAt.Value =
NetworkManager.ServerTime.Time + activeConfig.SpawnCooldown;
spawnSequenceActive.Value = false;
}
private void StopSpawnSequenceOnServer(bool applyCooldown)
{
if (!IsServer || !spawnSequenceActive.Value)
return;
if (spawnRoutine != null)
{
StopCoroutine(spawnRoutine);
spawnRoutine = null;
}
cooldownEndsAt.Value = applyCooldown && config != null
? NetworkManager.ServerTime.Time + config.SpawnCooldown
: 0d;
spawnSequenceActive.Value = false;
}
private static List<GameObject> BuildSpawnQueue(ItemPipeConfig activeConfig)
{
List<GameObject> queue = new();
foreach (ItemPipeConfig.GuaranteedItem item in activeConfig.GuaranteedItems)
{
if (item.prefab == null)
continue;
for (int index = 0; index < item.amount; index++)
queue.Add(item.prefab);
}
for (int index = 0; index < activeConfig.RandomItemCount; index++)
{
GameObject selectedPrefab = ChooseWeightedRandom(activeConfig);
if (selectedPrefab != null)
queue.Add(selectedPrefab);
}
Shuffle(queue);
return queue;
}
private static GameObject ChooseWeightedRandom(ItemPipeConfig activeConfig)
{
float totalWeight = 0f;
foreach (ItemPipeConfig.RandomItem item in activeConfig.RandomItems)
{
if (item.prefab != null && item.weight > 0f)
totalWeight += item.weight;
}
if (totalWeight <= 0f)
return null;
float roll = UnityEngine.Random.value * totalWeight;
float accumulatedWeight = 0f;
foreach (ItemPipeConfig.RandomItem item in activeConfig.RandomItems)
{
if (item.prefab == null || item.weight <= 0f)
continue;
accumulatedWeight += item.weight;
if (roll < accumulatedWeight)
return item.prefab;
}
return null;
}
private static void Shuffle(List<GameObject> queue)
{
for (int index = queue.Count - 1; index > 0; index--)
{
int randomIndex = UnityEngine.Random.Range(0, index + 1);
(queue[index], queue[randomIndex]) =
(queue[randomIndex], queue[index]);
}
}
private void SpawnNetworkItemOnServer(
GameObject prefab,
ItemPipeConfig activeConfig)
{
Transform activeSpawnPoint = spawnPoint != null ? spawnPoint : transform;
if (prefab == null)
return;
float offsetX = UnityEngine.Random.Range(
-activeConfig.RandomSpawnX,
activeConfig.RandomSpawnX);
float offsetZ = UnityEngine.Random.Range(
-activeConfig.RandomSpawnZ,
activeConfig.RandomSpawnZ);
Vector3 spawnPosition =
activeSpawnPoint.position +
activeSpawnPoint.right * offsetX +
activeSpawnPoint.forward * offsetZ;
GameObject item = Instantiate(
prefab,
spawnPosition,
UnityEngine.Random.rotationUniform);
if (!item.TryGetComponent(out NetworkObject networkObject))
{
Debug.LogError(
$"{name} cannot spawn '{prefab.name}': the prefab has no NetworkObject.",
this);
Destroy(item);
return;
}
networkObject.Spawn();
if (item.TryGetComponent(out Rigidbody body))
{
Vector3 worldVelocityChange =
activeSpawnPoint.TransformDirection(activeConfig.LocalVelocityChange);
body.AddForce(worldVelocityChange, ForceMode.VelocityChange);
}
}
private void HandleSpawnStateChanged(bool _, bool __) => Changed?.Invoke();
private void HandleCooldownChanged(double _, double __) => Changed?.Invoke();
[ContextMenu("Debug/Spawn Batch (Network Play Mode)")]
private void DebugSpawnBatch() => SpawnItems();
[ContextMenu("Debug/Stop Batch (Network Play Mode)")]
private void DebugStopBatch() => StopSpawning();
}