This commit is contained in:
2026-08-21 17:59:13 +03:00
parent b98901c366
commit f6a7547e84
951 changed files with 909007 additions and 70938 deletions
@@ -0,0 +1,388 @@
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();
}
}