389 lines
13 KiB
C#
389 lines
13 KiB
C#
using System;
|
|
using Unity.Netcode;
|
|
using UnityEngine;
|
|
|
|
public enum CannonState : byte
|
|
{
|
|
Empty,
|
|
Loaded,
|
|
Countdown
|
|
}
|
|
|
|
public struct CannonShotState : INetworkSerializable, IEquatable<CannonShotState>
|
|
{
|
|
public uint Sequence;
|
|
public MissileType MissileType;
|
|
|
|
public CannonShotState(uint sequence, MissileType missileType)
|
|
{
|
|
Sequence = sequence;
|
|
MissileType = missileType;
|
|
}
|
|
|
|
public void NetworkSerialize<T>(BufferSerializer<T> serializer)
|
|
where T : IReaderWriter
|
|
{
|
|
serializer.SerializeValue(ref Sequence);
|
|
serializer.SerializeValue(ref MissileType);
|
|
}
|
|
|
|
public bool Equals(CannonShotState other) =>
|
|
Sequence == other.Sequence && MissileType == other.MissileType;
|
|
}
|
|
|
|
[DisallowMultipleComponent]
|
|
[RequireComponent(typeof(NetworkObject))]
|
|
public sealed class NetworkCannonController : NetworkBehaviour
|
|
{
|
|
private const ulong EmptyRocketId = ulong.MaxValue;
|
|
|
|
[Header("Configuration")]
|
|
[SerializeField] private CannonConfig config;
|
|
|
|
[Header("Rocket Slot")]
|
|
[SerializeField] private CannonRocketSlot rocketSlot;
|
|
[SerializeField] private Transform rocketSnapPoint;
|
|
[SerializeField, Min(0.5f)] private float maximumTakeDistance = 4f;
|
|
[SerializeField, Min(0f)] private float minimumReinsertBlockDuration = 0.1f;
|
|
|
|
[Header("Output")]
|
|
[SerializeField] private MapController mapController;
|
|
[SerializeField] private NetworkShockwaveEmitter shotShockwave;
|
|
|
|
private readonly NetworkVariable<CannonState> state = new(
|
|
CannonState.Empty,
|
|
NetworkVariableReadPermission.Everyone,
|
|
NetworkVariableWritePermission.Server);
|
|
private readonly NetworkVariable<ulong> loadedRocketId = new(
|
|
EmptyRocketId,
|
|
NetworkVariableReadPermission.Everyone,
|
|
NetworkVariableWritePermission.Server);
|
|
private readonly NetworkVariable<float> selectedDelay = new(
|
|
5f,
|
|
NetworkVariableReadPermission.Everyone,
|
|
NetworkVariableWritePermission.Server);
|
|
private readonly NetworkVariable<double> fireAt = new(
|
|
0d,
|
|
NetworkVariableReadPermission.Everyone,
|
|
NetworkVariableWritePermission.Server);
|
|
private readonly NetworkVariable<CannonShotState> lastShot = new(
|
|
default,
|
|
NetworkVariableReadPermission.Everyone,
|
|
NetworkVariableWritePermission.Server);
|
|
|
|
private NetworkRocketItem loadedRocket;
|
|
private IWorkstationItemControl loadedRocketControl;
|
|
private ulong blockedRocketId = EmptyRocketId;
|
|
private Collider[] blockedRocketColliders;
|
|
private double unblockAfter;
|
|
|
|
public CannonState State => state.Value;
|
|
public CannonConfig Config => config;
|
|
public NetworkRocketItem LoadedRocket => loadedRocket != null
|
|
? loadedRocket
|
|
: ResolveLoadedRocket();
|
|
public float SelectedDelay => selectedDelay.Value;
|
|
public double FireAt => fireAt.Value;
|
|
public float RemainingSeconds => State == CannonState.Countdown && NetworkManager != null
|
|
? Mathf.Max(0f, (float)(fireAt.Value - NetworkManager.ServerTime.Time))
|
|
: 0f;
|
|
public bool CanTakeRocket => IsSpawned && State == CannonState.Loaded &&
|
|
loadedRocketId.Value != EmptyRocketId;
|
|
public CannonShotState LastShot => lastShot.Value;
|
|
|
|
public event Action Changed;
|
|
public event Action<CannonShotState> ShotFired;
|
|
|
|
private void Awake()
|
|
{
|
|
if (rocketSnapPoint == null && rocketSlot != null)
|
|
rocketSnapPoint = rocketSlot.SnapPoint;
|
|
rocketSlot?.Configure(this);
|
|
}
|
|
|
|
public override void OnNetworkSpawn()
|
|
{
|
|
state.OnValueChanged += HandleStateChanged;
|
|
loadedRocketId.OnValueChanged += HandleRocketIdChanged;
|
|
selectedDelay.OnValueChanged += HandleDelayChanged;
|
|
fireAt.OnValueChanged += HandleFireAtChanged;
|
|
lastShot.OnValueChanged += HandleShotChanged;
|
|
|
|
rocketSlot?.Configure(this);
|
|
if (IsServer)
|
|
{
|
|
state.Value = CannonState.Empty;
|
|
loadedRocketId.Value = EmptyRocketId;
|
|
selectedDelay.Value = config != null ? config.DefaultDelay : 5f;
|
|
fireAt.Value = 0d;
|
|
}
|
|
Changed?.Invoke();
|
|
}
|
|
|
|
public override void OnNetworkDespawn()
|
|
{
|
|
state.OnValueChanged -= HandleStateChanged;
|
|
loadedRocketId.OnValueChanged -= HandleRocketIdChanged;
|
|
selectedDelay.OnValueChanged -= HandleDelayChanged;
|
|
fireAt.OnValueChanged -= HandleFireAtChanged;
|
|
lastShot.OnValueChanged -= HandleShotChanged;
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
if (!IsServer || !IsSpawned)
|
|
return;
|
|
|
|
UpdateReinsertBlock();
|
|
if (State == CannonState.Countdown &&
|
|
NetworkManager != null && NetworkManager.ServerTime.Time >= fireAt.Value)
|
|
{
|
|
FireOnServer();
|
|
}
|
|
}
|
|
|
|
private void LateUpdate()
|
|
{
|
|
if (!IsServer || !IsSpawned || LoadedRocket == null ||
|
|
rocketSnapPoint == null)
|
|
return;
|
|
|
|
loadedRocket.transform.SetPositionAndRotation(
|
|
rocketSnapPoint.position,
|
|
rocketSnapPoint.rotation);
|
|
}
|
|
|
|
public bool TryInsertRocket(NetworkRocketItem rocket)
|
|
{
|
|
if (!IsServer || !IsSpawned || State != CannonState.Empty ||
|
|
rocket == null || !rocket.IsSpawned || rocket.Definition == null ||
|
|
rocket.NetworkObjectId == blockedRocketId ||
|
|
(config != null && !config.AcceptDefectiveRockets && rocket.IsDefective) ||
|
|
(config != null && !config.AcceptCriticallyDamagedRockets &&
|
|
rocket.IsCriticallyDamaged) ||
|
|
!WorkstationItemControlUtility.TryGetControl(
|
|
rocket.GetComponent<NetworkItem>(),
|
|
out IWorkstationItemControl control) ||
|
|
!control.TryAcquireExternalControl())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
loadedRocket = rocket;
|
|
loadedRocketControl = control;
|
|
loadedRocketId.Value = rocket.NetworkObjectId;
|
|
state.Value = CannonState.Loaded;
|
|
SnapLoadedRocket();
|
|
return true;
|
|
}
|
|
|
|
public void RequestTakeRocket()
|
|
{
|
|
if (CanTakeRocket)
|
|
RequestTakeRocketRpc();
|
|
}
|
|
|
|
public void RequestSetShotDelay(float seconds)
|
|
{
|
|
if (!IsSpawned)
|
|
return;
|
|
if (IsServer)
|
|
SetShotDelayOnServer(seconds);
|
|
else
|
|
RequestSetShotDelayRpc(seconds);
|
|
}
|
|
|
|
public void RequestAdjustShotDelay(float delta)
|
|
{
|
|
RequestSetShotDelay(SelectedDelay + delta);
|
|
}
|
|
|
|
public void RequestIncreaseShotDelay()
|
|
{
|
|
RequestAdjustShotDelay(config != null ? config.AdjustmentStep : 1f);
|
|
}
|
|
|
|
public void RequestDecreaseShotDelay()
|
|
{
|
|
RequestAdjustShotDelay(-(config != null ? config.AdjustmentStep : 1f));
|
|
}
|
|
|
|
public void RequestStartCountdown()
|
|
{
|
|
if (!IsSpawned)
|
|
return;
|
|
if (IsServer)
|
|
TryStartCountdownOnServer();
|
|
else
|
|
RequestStartCountdownRpc();
|
|
}
|
|
|
|
public void SetFireRequested(bool isActivated)
|
|
{
|
|
if (isActivated && IsServer && IsSpawned)
|
|
TryStartCountdownOnServer();
|
|
}
|
|
|
|
[ContextMenu("Debug/Start Countdown (Server Play Mode)")]
|
|
private void StartCountdownFromContextMenu()
|
|
{
|
|
if (Application.isPlaying)
|
|
TryStartCountdownOnServer();
|
|
}
|
|
|
|
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
|
|
private void RequestSetShotDelayRpc(float seconds) => SetShotDelayOnServer(seconds);
|
|
|
|
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
|
|
private void RequestStartCountdownRpc() => TryStartCountdownOnServer();
|
|
|
|
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
|
|
private void RequestTakeRocketRpc(RpcParams rpcParams = default)
|
|
{
|
|
if (!CanTakeRocket || rocketSlot == null ||
|
|
!TryGetPlayerObject(rpcParams.Receive.SenderClientId, out NetworkObject player))
|
|
return;
|
|
|
|
float maxDistanceSquared = maximumTakeDistance * maximumTakeDistance;
|
|
if ((player.transform.position - rocketSlot.InteractionPosition).sqrMagnitude >
|
|
maxDistanceSquared)
|
|
return;
|
|
|
|
NetworkRocketItem rocket = LoadedRocket;
|
|
IWorkstationItemControl control = loadedRocketControl ?? ResolveLoadedControl();
|
|
if (rocket == null || control == null ||
|
|
!control.TryReleaseExternalControlToPlayer(rpcParams.Receive.SenderClientId))
|
|
return;
|
|
|
|
blockedRocketId = rocket.NetworkObjectId;
|
|
blockedRocketColliders = rocket.GetComponentsInChildren<Collider>(true);
|
|
unblockAfter = GetNetworkTime() + minimumReinsertBlockDuration;
|
|
ClearRocketSlot();
|
|
}
|
|
|
|
private void SetShotDelayOnServer(float seconds)
|
|
{
|
|
if (!IsServer || State == CannonState.Countdown)
|
|
return;
|
|
selectedDelay.Value = config != null
|
|
? config.ClampDelay(seconds)
|
|
: Mathf.Clamp(seconds, 0f, 60f);
|
|
}
|
|
|
|
private bool TryStartCountdownOnServer()
|
|
{
|
|
if (!IsServer || !IsSpawned || State != CannonState.Loaded ||
|
|
LoadedRocket == null || NetworkManager == null)
|
|
return false;
|
|
|
|
fireAt.Value = NetworkManager.ServerTime.Time + selectedDelay.Value;
|
|
state.Value = CannonState.Countdown;
|
|
return true;
|
|
}
|
|
|
|
private void FireOnServer()
|
|
{
|
|
NetworkRocketItem rocket = LoadedRocket;
|
|
if (rocket == null || rocket.Definition == null)
|
|
{
|
|
ClearRocketSlot();
|
|
return;
|
|
}
|
|
|
|
MissileType missileType = rocket.Definition.MissileType;
|
|
mapController?.RequestShot(missileType);
|
|
shotShockwave?.EmitShockwave();
|
|
lastShot.Value = new CannonShotState(lastShot.Value.Sequence + 1, missileType);
|
|
|
|
NetworkObject rocketObject = rocket.NetworkObject;
|
|
loadedRocket = null;
|
|
loadedRocketControl = null;
|
|
loadedRocketId.Value = EmptyRocketId;
|
|
fireAt.Value = 0d;
|
|
state.Value = CannonState.Empty;
|
|
|
|
if (rocketObject != null && rocketObject.IsSpawned)
|
|
rocketObject.Despawn(true);
|
|
}
|
|
|
|
private void ClearRocketSlot()
|
|
{
|
|
loadedRocket = null;
|
|
loadedRocketControl = null;
|
|
loadedRocketId.Value = EmptyRocketId;
|
|
fireAt.Value = 0d;
|
|
state.Value = CannonState.Empty;
|
|
}
|
|
|
|
private void SnapLoadedRocket()
|
|
{
|
|
if (loadedRocket != null && rocketSnapPoint != null)
|
|
{
|
|
loadedRocket.transform.SetPositionAndRotation(
|
|
rocketSnapPoint.position,
|
|
rocketSnapPoint.rotation);
|
|
}
|
|
}
|
|
|
|
private NetworkRocketItem ResolveLoadedRocket()
|
|
{
|
|
if (loadedRocketId.Value == EmptyRocketId || NetworkManager == null ||
|
|
NetworkManager.SpawnManager == null ||
|
|
!NetworkManager.SpawnManager.SpawnedObjects.TryGetValue(
|
|
loadedRocketId.Value, out NetworkObject networkObject))
|
|
return null;
|
|
|
|
loadedRocket = networkObject.GetComponent<NetworkRocketItem>();
|
|
return loadedRocket;
|
|
}
|
|
|
|
private IWorkstationItemControl ResolveLoadedControl()
|
|
{
|
|
NetworkRocketItem rocket = LoadedRocket;
|
|
WorkstationItemControlUtility.TryGetControl(
|
|
rocket != null ? rocket.GetComponent<NetworkItem>() : null,
|
|
out loadedRocketControl);
|
|
return loadedRocketControl;
|
|
}
|
|
|
|
private bool TryGetPlayerObject(ulong clientId, out NetworkObject player)
|
|
{
|
|
player = null;
|
|
return NetworkManager != null &&
|
|
NetworkManager.ConnectedClients.TryGetValue(clientId, out NetworkClient client) &&
|
|
(player = client.PlayerObject) != null;
|
|
}
|
|
|
|
private void UpdateReinsertBlock()
|
|
{
|
|
if (blockedRocketId == EmptyRocketId || rocketSlot == null ||
|
|
GetNetworkTime() < unblockAfter ||
|
|
rocketSlot.OverlapsAny(blockedRocketColliders, 0.05f))
|
|
return;
|
|
|
|
blockedRocketId = EmptyRocketId;
|
|
blockedRocketColliders = null;
|
|
}
|
|
|
|
private double GetNetworkTime() => NetworkManager != null
|
|
? NetworkManager.ServerTime.Time
|
|
: Time.timeAsDouble;
|
|
|
|
private void HandleStateChanged(CannonState _, CannonState __) => Changed?.Invoke();
|
|
private void HandleRocketIdChanged(ulong _, ulong __)
|
|
{
|
|
loadedRocket = null;
|
|
loadedRocketControl = null;
|
|
Changed?.Invoke();
|
|
}
|
|
private void HandleDelayChanged(float _, float __) => Changed?.Invoke();
|
|
private void HandleFireAtChanged(double _, double __) => Changed?.Invoke();
|
|
private void HandleShotChanged(CannonShotState previous, CannonShotState current)
|
|
{
|
|
if (current.Sequence == previous.Sequence)
|
|
return;
|
|
ShotFired?.Invoke(current);
|
|
Changed?.Invoke();
|
|
}
|
|
}
|