Squashed commit of the following:
commit428243f4a6Author: AsanoRema <asanorema2036@gmail.com> Date: Sat Aug 29 13:43:13 2026 +0300 Squashed commit of the following: commit68e78fa464Author: AsanoRema <asanorema2036@gmail.com> Date: Sat Aug 29 13:41:49 2026 +0300 Updated .gitignore commit9f87122a34Author: AsanoRema <asanorema2036@gmail.com> Date: Sat Aug 29 13:34:57 2026 +0300 Added Dohya commitf6a7547e84Author: AsanoRema <asanorema2036@gmail.com> Date: Fri Aug 21 17:59:13 2026 +0300 Fixed commitb98901c366Author: AsanoRema <asanorema2036@gmail.com> Date: Tue Aug 18 17:37:22 2026 +0300 Added new particle Package commitbb2f4a3b16Author: AsanoRema <asanorema2036@gmail.com> Date: Tue Aug 18 17:17:48 2026 +0300 Map mehanic ready to multiplayer commitc01ff800f6Author: AsanoRema <asanorema2036@gmail.com> Date: Tue Aug 18 16:16:19 2026 +0300 Squashed commit of the following: commit173bbb165cAuthor: henjkey <henjkey@gmail.com> Date: Fri Aug 14 18:51:23 2026 +0500 Mappp commitd559ab338aAuthor: henjkey <henjkey@gmail.com> Date: Thu Aug 13 18:42:41 2026 +0500 Map Controller Coordinates Target System Crosshair System Radar System
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2dab561baa00bfb46b838b978cb45129
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,32 @@
|
||||
using UnityEngine;
|
||||
|
||||
[CreateAssetMenu(
|
||||
fileName = "CannonConfig",
|
||||
menuName = "FORT-BO/Cannon/Cannon Config")]
|
||||
public sealed class CannonConfig : ScriptableObject
|
||||
{
|
||||
[SerializeField, Min(0f)] private float minimumDelay = 0f;
|
||||
[SerializeField, Min(0.1f)] private float maximumDelay = 60f;
|
||||
[SerializeField, Min(0f)] private float defaultDelay = 5f;
|
||||
[SerializeField, Min(0.1f)] private float adjustmentStep = 1f;
|
||||
[SerializeField] private bool acceptDefectiveRockets = true;
|
||||
[SerializeField] private bool acceptCriticallyDamagedRockets;
|
||||
|
||||
public float MinimumDelay => minimumDelay;
|
||||
public float MaximumDelay => maximumDelay;
|
||||
public float DefaultDelay => ClampDelay(defaultDelay);
|
||||
public float AdjustmentStep => adjustmentStep;
|
||||
public bool AcceptDefectiveRockets => acceptDefectiveRockets;
|
||||
public bool AcceptCriticallyDamagedRockets => acceptCriticallyDamagedRockets;
|
||||
|
||||
public float ClampDelay(float value) =>
|
||||
Mathf.Clamp(value, minimumDelay, Mathf.Max(minimumDelay, maximumDelay));
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
minimumDelay = Mathf.Max(0f, minimumDelay);
|
||||
maximumDelay = Mathf.Max(minimumDelay, maximumDelay);
|
||||
defaultDelay = ClampDelay(defaultDelay);
|
||||
adjustmentStep = Mathf.Max(0.1f, adjustmentStep);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e069cd58c327cc94e9c2598c9e4c449f
|
||||
@@ -0,0 +1,51 @@
|
||||
using UnityEngine;
|
||||
|
||||
[DisallowMultipleComponent]
|
||||
public sealed class CannonControlBinder : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private NetworkCannonController cannon;
|
||||
[SerializeField] private NetworkPhysicalLeverInteractable fireLever;
|
||||
[SerializeField] private NetworkButtonInteractable decreaseTimerButton;
|
||||
[SerializeField] private NetworkButtonInteractable increaseTimerButton;
|
||||
|
||||
private bool bound;
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!bound)
|
||||
TryBind();
|
||||
}
|
||||
|
||||
private void OnDisable() => Unbind();
|
||||
|
||||
private void TryBind()
|
||||
{
|
||||
if (cannon == null || !cannon.IsSpawned || !cannon.IsServer)
|
||||
return;
|
||||
|
||||
if (fireLever != null)
|
||||
fireLever.MaximumReached += HandleFireLever;
|
||||
if (decreaseTimerButton != null)
|
||||
decreaseTimerButton.Pressed += HandleDecreaseTimer;
|
||||
if (increaseTimerButton != null)
|
||||
increaseTimerButton.Pressed += HandleIncreaseTimer;
|
||||
bound = true;
|
||||
}
|
||||
|
||||
private void Unbind()
|
||||
{
|
||||
if (!bound)
|
||||
return;
|
||||
if (fireLever != null)
|
||||
fireLever.MaximumReached -= HandleFireLever;
|
||||
if (decreaseTimerButton != null)
|
||||
decreaseTimerButton.Pressed -= HandleDecreaseTimer;
|
||||
if (increaseTimerButton != null)
|
||||
increaseTimerButton.Pressed -= HandleIncreaseTimer;
|
||||
bound = false;
|
||||
}
|
||||
|
||||
private void HandleFireLever() => cannon.RequestStartCountdown();
|
||||
private void HandleDecreaseTimer() => cannon.RequestDecreaseShotDelay();
|
||||
private void HandleIncreaseTimer() => cannon.RequestIncreaseShotDelay();
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 24a208cb322bf904bb12481f5fc58a85
|
||||
@@ -0,0 +1,81 @@
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
|
||||
[DisallowMultipleComponent]
|
||||
[RequireComponent(typeof(Collider))]
|
||||
public sealed class CannonRocketSlot : MonoBehaviour, IInteractable
|
||||
{
|
||||
[SerializeField] private Transform snapPoint;
|
||||
|
||||
private NetworkCannonController cannon;
|
||||
private Collider triggerCollider;
|
||||
|
||||
public Transform SnapPoint => snapPoint != null ? snapPoint : transform;
|
||||
public Vector3 InteractionPosition => SnapPoint.position;
|
||||
public string InteractionPrompt => cannon != null
|
||||
? cannon.LoadedRocket != null
|
||||
? $"Take {cannon.LoadedRocket.RocketTypeId} rocket"
|
||||
: "Rocket slot"
|
||||
: "Rocket slot";
|
||||
public bool IsInteractionAvailable => cannon != null && cannon.CanTakeRocket;
|
||||
|
||||
private void Reset()
|
||||
{
|
||||
snapPoint = transform;
|
||||
ResolveCollider();
|
||||
triggerCollider.isTrigger = true;
|
||||
}
|
||||
|
||||
private void Awake() => ResolveCollider();
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
ResolveCollider();
|
||||
if (triggerCollider != null)
|
||||
triggerCollider.isTrigger = true;
|
||||
}
|
||||
|
||||
public void Configure(NetworkCannonController owner) => cannon = owner;
|
||||
|
||||
public void RequestInteraction()
|
||||
{
|
||||
if (IsInteractionAvailable)
|
||||
cannon.RequestTakeRocket();
|
||||
}
|
||||
|
||||
private void OnTriggerEnter(Collider other) => TryInsert(other);
|
||||
private void OnTriggerStay(Collider other) => TryInsert(other);
|
||||
|
||||
private void TryInsert(Collider other)
|
||||
{
|
||||
if (cannon == null || !cannon.IsServer || !cannon.IsSpawned)
|
||||
return;
|
||||
|
||||
NetworkRocketItem rocket = other.GetComponentInParent<NetworkRocketItem>();
|
||||
if (rocket != null)
|
||||
cannon.TryInsertRocket(rocket);
|
||||
}
|
||||
|
||||
public bool OverlapsAny(Collider[] itemColliders, float clearance)
|
||||
{
|
||||
ResolveCollider();
|
||||
if (triggerCollider == null || itemColliders == null)
|
||||
return false;
|
||||
|
||||
Bounds bounds = triggerCollider.bounds;
|
||||
bounds.Expand(Mathf.Max(0f, clearance) * 2f);
|
||||
foreach (Collider itemCollider in itemColliders)
|
||||
{
|
||||
if (itemCollider != null && itemCollider.enabled &&
|
||||
bounds.Intersects(itemCollider.bounds))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void ResolveCollider()
|
||||
{
|
||||
if (triggerCollider == null)
|
||||
triggerCollider = GetComponent<Collider>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7ad0f04c740f9b343b2ed4f083c407a6
|
||||
@@ -0,0 +1,46 @@
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
[DisallowMultipleComponent]
|
||||
public sealed class CannonTimerView : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private NetworkCannonController controller;
|
||||
[SerializeField] private TMP_Text timerText;
|
||||
[SerializeField] private string emptyText = "INSERT ROCKET";
|
||||
[SerializeField] private string loadedFormat = "DELAY {0:0}s";
|
||||
[SerializeField] private string countdownFormat = "FIRE IN {0:0.0}s";
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (controller != null)
|
||||
controller.Changed += Refresh;
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (controller != null)
|
||||
controller.Changed -= Refresh;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (controller != null && controller.State == CannonState.Countdown)
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private void Refresh()
|
||||
{
|
||||
if (timerText == null || controller == null)
|
||||
return;
|
||||
|
||||
timerText.text = controller.State switch
|
||||
{
|
||||
CannonState.Empty => emptyText,
|
||||
CannonState.Loaded => string.Format(loadedFormat, controller.SelectedDelay),
|
||||
CannonState.Countdown => string.Format(
|
||||
countdownFormat, controller.RemainingSeconds),
|
||||
_ => emptyText
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 451ab5135cfc56f489740516a23e7834
|
||||
@@ -0,0 +1,90 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
|
||||
[DisallowMultipleComponent]
|
||||
public sealed class CannonView : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private NetworkCannonController controller;
|
||||
[SerializeField] private Transform recoilVisual;
|
||||
[SerializeField] private Vector3 localRecoilOffset = new(0f, 0f, -0.75f);
|
||||
[SerializeField, Min(0.01f)] private float recoilOutDuration = 0.08f;
|
||||
[SerializeField, Min(0.01f)] private float recoilReturnDuration = 0.45f;
|
||||
[SerializeField] private UnityEvent shotFired;
|
||||
[SerializeField] private UnityEvent<MissileType> missileFired;
|
||||
|
||||
private Vector3 baseLocalPosition;
|
||||
private Coroutine recoilRoutine;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (recoilVisual != null)
|
||||
baseLocalPosition = recoilVisual.localPosition;
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (controller != null)
|
||||
controller.ShotFired += HandleShotFired;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (controller != null)
|
||||
controller.ShotFired -= HandleShotFired;
|
||||
if (recoilRoutine != null)
|
||||
StopCoroutine(recoilRoutine);
|
||||
if (recoilVisual != null)
|
||||
recoilVisual.localPosition = baseLocalPosition;
|
||||
}
|
||||
|
||||
private void HandleShotFired(CannonShotState shot)
|
||||
{
|
||||
if (recoilRoutine != null)
|
||||
StopCoroutine(recoilRoutine);
|
||||
recoilRoutine = StartCoroutine(PlayRecoil());
|
||||
shotFired?.Invoke();
|
||||
missileFired?.Invoke(shot.MissileType);
|
||||
}
|
||||
|
||||
private IEnumerator PlayRecoil()
|
||||
{
|
||||
if (recoilVisual == null)
|
||||
yield break;
|
||||
|
||||
Vector3 recoilPosition = baseLocalPosition + localRecoilOffset;
|
||||
yield return Move(recoilVisual.localPosition, recoilPosition, recoilOutDuration);
|
||||
yield return Move(recoilVisual.localPosition, baseLocalPosition, recoilReturnDuration);
|
||||
recoilRoutine = null;
|
||||
}
|
||||
|
||||
private IEnumerator Move(Vector3 from, Vector3 to, float duration)
|
||||
{
|
||||
float elapsed = 0f;
|
||||
while (elapsed < duration)
|
||||
{
|
||||
elapsed += Time.deltaTime;
|
||||
float t = Mathf.Clamp01(elapsed / duration);
|
||||
recoilVisual.localPosition = Vector3.LerpUnclamped(
|
||||
from, to, 1f - Mathf.Pow(1f - t, 3f));
|
||||
yield return null;
|
||||
}
|
||||
recoilVisual.localPosition = to;
|
||||
}
|
||||
|
||||
[ContextMenu("Preview/Recoil")]
|
||||
private void PreviewRecoil()
|
||||
{
|
||||
if (Application.isPlaying)
|
||||
HandleShotFired(new CannonShotState(0, MissileType.Standard));
|
||||
else if (recoilVisual != null)
|
||||
recoilVisual.localPosition = baseLocalPosition + localRecoilOffset;
|
||||
}
|
||||
|
||||
[ContextMenu("Preview/Reset")]
|
||||
private void ResetPreview()
|
||||
{
|
||||
if (recoilVisual != null)
|
||||
recoilVisual.localPosition = baseLocalPosition;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f174c161f1871f44bba1b7e09ce03e61
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4c3f0760c12468543bab2cf3f99294f6
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,458 @@
|
||||
using TMPro;
|
||||
using Unity.Netcode;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
|
||||
public static class CannonPrototypeSetup
|
||||
{
|
||||
private const string DataFolder = "Assets/CONTENT/Game/Data/Cannon";
|
||||
private const string CannonConfigPath = DataFolder + "/CannonConfig.asset";
|
||||
private const string CannonShockwavePath = DataFolder + "/CannonShockwave.asset";
|
||||
private const string RocketShockwavePath = DataFolder + "/RocketImpactShockwave.asset";
|
||||
private const string PlayerPrefabPath =
|
||||
"Assets/CONTENT/Friendslop/Prefabs/NetworkPlayer.prefab";
|
||||
private const string RocketPrefabPath =
|
||||
"Assets/CONTENT/Game/Prefabs/Items/Ready-Rocket-Standart.prefab";
|
||||
private const string StandardRocketDefinitionPath =
|
||||
"Assets/CONTENT/Game/Data/Items/Rocket-Ready/Ready-Rocket-Standart.asset";
|
||||
private const string LeverPrefabPath =
|
||||
"Assets/CONTENT/Game/Prefabs/Interaction/PhysicalLever.prefab";
|
||||
|
||||
[MenuItem("FORT-BO/Cannon/Configure Network Prototype")]
|
||||
public static void ConfigureNetworkPrototype()
|
||||
{
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
Debug.LogWarning("Stop Play Mode before configuring the cannon prototype.");
|
||||
return;
|
||||
}
|
||||
|
||||
EnsureDataFolder();
|
||||
CannonConfig config = GetOrCreateCannonConfig();
|
||||
ShockwaveProfile cannonShockwave = GetOrCreateProfile(
|
||||
CannonShockwavePath,
|
||||
new ShockwaveZone
|
||||
{
|
||||
Radius = 8f,
|
||||
PlayerImpulse = 12f,
|
||||
RigidbodyImpulse = 18f,
|
||||
UpwardBias = 0.35f,
|
||||
CameraShakeAmplitude = 0.22f,
|
||||
CameraShakeDuration = 0.9f,
|
||||
CameraShakeFrequency = 28f
|
||||
},
|
||||
new ShockwaveZone
|
||||
{
|
||||
Radius = 18f,
|
||||
PlayerImpulse = 5f,
|
||||
RigidbodyImpulse = 8f,
|
||||
UpwardBias = 0.25f,
|
||||
CameraShakeAmplitude = 0.12f,
|
||||
CameraShakeDuration = 0.75f,
|
||||
CameraShakeFrequency = 24f
|
||||
},
|
||||
new ShockwaveZone
|
||||
{
|
||||
Radius = 35f,
|
||||
PlayerImpulse = 0f,
|
||||
RigidbodyImpulse = 1.5f,
|
||||
UpwardBias = 0.1f,
|
||||
CameraShakeAmplitude = 0.055f,
|
||||
CameraShakeDuration = 0.55f,
|
||||
CameraShakeFrequency = 20f
|
||||
});
|
||||
ShockwaveProfile rocketShockwave = GetOrCreateProfile(
|
||||
RocketShockwavePath,
|
||||
new ShockwaveZone
|
||||
{
|
||||
Radius = 4f,
|
||||
PlayerImpulse = 8f,
|
||||
RigidbodyImpulse = 12f,
|
||||
UpwardBias = 0.4f,
|
||||
CameraShakeAmplitude = 0.18f,
|
||||
CameraShakeDuration = 0.7f,
|
||||
CameraShakeFrequency = 27f
|
||||
},
|
||||
new ShockwaveZone
|
||||
{
|
||||
Radius = 8f,
|
||||
PlayerImpulse = 3f,
|
||||
RigidbodyImpulse = 5f,
|
||||
UpwardBias = 0.3f,
|
||||
CameraShakeAmplitude = 0.09f,
|
||||
CameraShakeDuration = 0.55f,
|
||||
CameraShakeFrequency = 23f
|
||||
},
|
||||
new ShockwaveZone
|
||||
{
|
||||
Radius = 14f,
|
||||
PlayerImpulse = 0f,
|
||||
RigidbodyImpulse = 0.5f,
|
||||
UpwardBias = 0.1f,
|
||||
CameraShakeAmplitude = 0.04f,
|
||||
CameraShakeDuration = 0.4f,
|
||||
CameraShakeFrequency = 18f
|
||||
});
|
||||
|
||||
ConfigurePlayerPrefab();
|
||||
ConfigureRocketPrefab(rocketShockwave);
|
||||
ConfigureStandardRocketDefinition();
|
||||
NetworkCannonController cannon = ConfigureCannonScene(config, cannonShockwave);
|
||||
ConfigureMapMarker();
|
||||
|
||||
AssetDatabase.SaveAssets();
|
||||
EditorSceneManager.MarkSceneDirty(cannon.gameObject.scene);
|
||||
EditorSceneManager.SaveOpenScenes();
|
||||
Selection.activeObject = cannon.gameObject;
|
||||
Debug.Log(
|
||||
"[Cannon] Network prototype configured. Insert a ready rocket, " +
|
||||
"set delay with -/+ buttons and pull the fire lever.", cannon);
|
||||
}
|
||||
|
||||
private static CannonConfig GetOrCreateCannonConfig()
|
||||
{
|
||||
CannonConfig config = AssetDatabase.LoadAssetAtPath<CannonConfig>(CannonConfigPath);
|
||||
if (config == null)
|
||||
{
|
||||
config = ScriptableObject.CreateInstance<CannonConfig>();
|
||||
AssetDatabase.CreateAsset(config, CannonConfigPath);
|
||||
}
|
||||
|
||||
SerializedObject serialized = new(config);
|
||||
SetFloat(serialized, "minimumDelay", 0f);
|
||||
SetFloat(serialized, "maximumDelay", 30f);
|
||||
SetFloat(serialized, "defaultDelay", 5f);
|
||||
SetFloat(serialized, "adjustmentStep", 1f);
|
||||
serialized.ApplyModifiedPropertiesWithoutUndo();
|
||||
EditorUtility.SetDirty(config);
|
||||
return config;
|
||||
}
|
||||
|
||||
private static ShockwaveProfile GetOrCreateProfile(
|
||||
string path,
|
||||
params ShockwaveZone[] zones)
|
||||
{
|
||||
ShockwaveProfile profile = AssetDatabase.LoadAssetAtPath<ShockwaveProfile>(path);
|
||||
if (profile == null)
|
||||
{
|
||||
profile = ScriptableObject.CreateInstance<ShockwaveProfile>();
|
||||
AssetDatabase.CreateAsset(profile, path);
|
||||
}
|
||||
|
||||
SerializedObject serialized = new(profile);
|
||||
SerializedProperty array = serialized.FindProperty("zones");
|
||||
array.arraySize = zones.Length;
|
||||
for (int index = 0; index < zones.Length; index++)
|
||||
{
|
||||
SerializedProperty element = array.GetArrayElementAtIndex(index);
|
||||
ShockwaveZone zone = zones[index];
|
||||
element.FindPropertyRelative("Radius").floatValue = zone.Radius;
|
||||
element.FindPropertyRelative("PlayerImpulse").floatValue = zone.PlayerImpulse;
|
||||
element.FindPropertyRelative("RigidbodyImpulse").floatValue = zone.RigidbodyImpulse;
|
||||
element.FindPropertyRelative("UpwardBias").floatValue = zone.UpwardBias;
|
||||
element.FindPropertyRelative("CameraShakeAmplitude").floatValue =
|
||||
zone.CameraShakeAmplitude;
|
||||
element.FindPropertyRelative("CameraShakeDuration").floatValue =
|
||||
zone.CameraShakeDuration;
|
||||
element.FindPropertyRelative("CameraShakeFrequency").floatValue =
|
||||
zone.CameraShakeFrequency;
|
||||
}
|
||||
serialized.ApplyModifiedPropertiesWithoutUndo();
|
||||
EditorUtility.SetDirty(profile);
|
||||
return profile;
|
||||
}
|
||||
|
||||
private static void ConfigurePlayerPrefab()
|
||||
{
|
||||
GameObject root = PrefabUtility.LoadPrefabContents(PlayerPrefabPath);
|
||||
try
|
||||
{
|
||||
GetOrAdd<PlayerCameraShakeController>(root);
|
||||
GetOrAdd<PlayerImpactController>(root);
|
||||
PrefabUtility.SaveAsPrefabAsset(root, PlayerPrefabPath);
|
||||
}
|
||||
finally
|
||||
{
|
||||
PrefabUtility.UnloadPrefabContents(root);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ConfigureRocketPrefab(ShockwaveProfile rocketShockwave)
|
||||
{
|
||||
GameObject root = PrefabUtility.LoadPrefabContents(RocketPrefabPath);
|
||||
try
|
||||
{
|
||||
NetworkRocketItem rocket = root.GetComponent<NetworkRocketItem>();
|
||||
NetworkShockwaveEmitter emitter = GetOrAdd<NetworkShockwaveEmitter>(root);
|
||||
RocketImpactExplosion explosion = GetOrAdd<RocketImpactExplosion>(root);
|
||||
|
||||
SerializedObject serializedEmitter = new(emitter);
|
||||
SetObject(serializedEmitter, "profile", rocketShockwave);
|
||||
SetObject(serializedEmitter, "origin", root.transform);
|
||||
serializedEmitter.ApplyModifiedPropertiesWithoutUndo();
|
||||
|
||||
SerializedObject serializedExplosion = new(explosion);
|
||||
SetObject(serializedExplosion, "rocket", rocket);
|
||||
SetObject(serializedExplosion, "shockwave", emitter);
|
||||
serializedExplosion.ApplyModifiedPropertiesWithoutUndo();
|
||||
PrefabUtility.SaveAsPrefabAsset(root, RocketPrefabPath);
|
||||
}
|
||||
finally
|
||||
{
|
||||
PrefabUtility.UnloadPrefabContents(root);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ConfigureStandardRocketDefinition()
|
||||
{
|
||||
RocketItemDefinition definition =
|
||||
AssetDatabase.LoadAssetAtPath<RocketItemDefinition>(StandardRocketDefinitionPath);
|
||||
if (definition == null)
|
||||
return;
|
||||
|
||||
SerializedObject serialized = new(definition);
|
||||
serialized.FindProperty("missileType").enumValueIndex = (int)MissileType.Standard;
|
||||
serialized.ApplyModifiedPropertiesWithoutUndo();
|
||||
EditorUtility.SetDirty(definition);
|
||||
}
|
||||
|
||||
private static NetworkCannonController ConfigureCannonScene(
|
||||
CannonConfig config,
|
||||
ShockwaveProfile cannonShockwave)
|
||||
{
|
||||
GameObject root = GameObject.Find("Cannon Prototype");
|
||||
bool created = root == null;
|
||||
if (created)
|
||||
{
|
||||
root = new GameObject("Cannon Prototype");
|
||||
root.transform.SetPositionAndRotation(
|
||||
new Vector3(132f, 2.55f, 24.3f),
|
||||
Quaternion.Euler(0f, 90f, 0f));
|
||||
Undo.RegisterCreatedObjectUndo(root, "Create cannon prototype");
|
||||
}
|
||||
|
||||
GetOrAdd<NetworkObject>(root);
|
||||
NetworkCannonController controller = GetOrAdd<NetworkCannonController>(root);
|
||||
NetworkShockwaveEmitter emitter = GetOrAdd<NetworkShockwaveEmitter>(root);
|
||||
CannonControlBinder binder = GetOrAdd<CannonControlBinder>(root);
|
||||
CannonView view = GetOrAdd<CannonView>(root);
|
||||
|
||||
Transform baseVisual = GetOrCreatePrimitive(
|
||||
root.transform, "Cannon Base", PrimitiveType.Cube);
|
||||
baseVisual.localPosition = new Vector3(0f, 0.75f, 0f);
|
||||
baseVisual.localRotation = Quaternion.identity;
|
||||
baseVisual.localScale = new Vector3(2.2f, 1.5f, 2.2f);
|
||||
|
||||
Transform recoil = GetOrCreateChild(root.transform, "Barrel Recoil");
|
||||
recoil.localPosition = new Vector3(0f, 2.1f, 0f);
|
||||
recoil.localRotation = Quaternion.identity;
|
||||
recoil.localScale = Vector3.one;
|
||||
Transform barrel = GetOrCreatePrimitive(recoil, "Barrel", PrimitiveType.Cube);
|
||||
barrel.localPosition = new Vector3(0f, 0f, 2.1f);
|
||||
barrel.localRotation = Quaternion.identity;
|
||||
barrel.localScale = new Vector3(0.8f, 0.8f, 4.2f);
|
||||
Transform muzzle = GetOrCreateChild(recoil, "Muzzle");
|
||||
muzzle.localPosition = new Vector3(0f, 0f, 4.25f);
|
||||
|
||||
Transform slotObject = GetOrCreateChild(root.transform, "Rocket Slot");
|
||||
slotObject.localPosition = new Vector3(-2.2f, 1.25f, 0f);
|
||||
slotObject.localRotation = Quaternion.Euler(0f, 90f, 0f);
|
||||
slotObject.localScale = Vector3.one;
|
||||
BoxCollider slotCollider = GetOrAdd<BoxCollider>(slotObject.gameObject);
|
||||
slotCollider.isTrigger = true;
|
||||
slotCollider.size = new Vector3(1.4f, 1.4f, 3.2f);
|
||||
CannonRocketSlot slot = GetOrAdd<CannonRocketSlot>(slotObject.gameObject);
|
||||
Transform snapPoint = GetOrCreateChild(slotObject, "Rocket Snap Point");
|
||||
snapPoint.localPosition = Vector3.zero;
|
||||
snapPoint.localRotation = Quaternion.identity;
|
||||
|
||||
Transform controls = GetOrCreateChild(root.transform, "Controls");
|
||||
controls.localPosition = Vector3.zero;
|
||||
controls.localRotation = Quaternion.identity;
|
||||
controls.localScale = Vector3.one;
|
||||
NetworkPhysicalLeverInteractable lever = CreateLever(controls);
|
||||
NetworkButtonInteractable minus = CreateButton(
|
||||
controls, "Timer Minus", new Vector3(1.8f, 1.4f, -1.4f), "Timer -");
|
||||
NetworkButtonInteractable plus = CreateButton(
|
||||
controls, "Timer Plus", new Vector3(3.1f, 1.4f, -1.4f), "Timer +");
|
||||
TMP_Text timerText = CreateTimerText(controls);
|
||||
|
||||
SerializedObject serializedEmitter = new(emitter);
|
||||
SetObject(serializedEmitter, "profile", cannonShockwave);
|
||||
SetObject(serializedEmitter, "origin", muzzle);
|
||||
serializedEmitter.ApplyModifiedPropertiesWithoutUndo();
|
||||
|
||||
SerializedObject serializedController = new(controller);
|
||||
SetObject(serializedController, "config", config);
|
||||
SetObject(serializedController, "rocketSlot", slot);
|
||||
SetObject(serializedController, "rocketSnapPoint", snapPoint);
|
||||
SetObject(serializedController, "mapController",
|
||||
Object.FindFirstObjectByType<MapController>(FindObjectsInactive.Include));
|
||||
SetObject(serializedController, "shotShockwave", emitter);
|
||||
serializedController.ApplyModifiedPropertiesWithoutUndo();
|
||||
|
||||
SerializedObject serializedSlot = new(slot);
|
||||
SetObject(serializedSlot, "snapPoint", snapPoint);
|
||||
serializedSlot.ApplyModifiedPropertiesWithoutUndo();
|
||||
|
||||
SerializedObject serializedBinder = new(binder);
|
||||
SetObject(serializedBinder, "cannon", controller);
|
||||
SetObject(serializedBinder, "fireLever", lever);
|
||||
SetObject(serializedBinder, "decreaseTimerButton", minus);
|
||||
SetObject(serializedBinder, "increaseTimerButton", plus);
|
||||
serializedBinder.ApplyModifiedPropertiesWithoutUndo();
|
||||
|
||||
SerializedObject serializedView = new(view);
|
||||
SetObject(serializedView, "controller", controller);
|
||||
SetObject(serializedView, "recoilVisual", recoil);
|
||||
serializedView.ApplyModifiedPropertiesWithoutUndo();
|
||||
|
||||
CannonTimerView timerView = GetOrAdd<CannonTimerView>(timerText.gameObject);
|
||||
SerializedObject serializedTimer = new(timerView);
|
||||
SetObject(serializedTimer, "controller", controller);
|
||||
SetObject(serializedTimer, "timerText", timerText);
|
||||
serializedTimer.ApplyModifiedPropertiesWithoutUndo();
|
||||
|
||||
slot.Configure(controller);
|
||||
EditorUtility.SetDirty(root);
|
||||
return controller;
|
||||
}
|
||||
|
||||
private static NetworkPhysicalLeverInteractable CreateLever(Transform parent)
|
||||
{
|
||||
Transform existing = parent.Find("Fire Lever");
|
||||
GameObject root;
|
||||
if (existing != null)
|
||||
{
|
||||
root = existing.gameObject;
|
||||
}
|
||||
else
|
||||
{
|
||||
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(LeverPrefabPath);
|
||||
root = (GameObject)PrefabUtility.InstantiatePrefab(prefab, parent);
|
||||
root.name = "Fire Lever";
|
||||
}
|
||||
|
||||
root.transform.localPosition = new Vector3(4.5f, 1.25f, -1.2f);
|
||||
root.transform.localRotation = Quaternion.Euler(0f, -90f, 0f);
|
||||
root.transform.localScale = Vector3.one * 0.65f;
|
||||
NetworkPhysicalLeverInteractable lever =
|
||||
root.GetComponent<NetworkPhysicalLeverInteractable>();
|
||||
SerializedObject serialized = new(lever);
|
||||
SetString(serialized, "interactionLabel", "Fire cannon");
|
||||
SetBool(serialized, "initialPositionIsMaximum", false);
|
||||
SetBool(serialized, "returnToInitialPositionAfterSwitch", true);
|
||||
SetFloat(serialized, "automaticReturnDelay", 0.2f);
|
||||
serialized.ApplyModifiedPropertiesWithoutUndo();
|
||||
return lever;
|
||||
}
|
||||
|
||||
private static NetworkButtonInteractable CreateButton(
|
||||
Transform parent,
|
||||
string name,
|
||||
Vector3 localPosition,
|
||||
string label)
|
||||
{
|
||||
Transform root = GetOrCreateChild(parent, name);
|
||||
root.localPosition = localPosition;
|
||||
root.localRotation = Quaternion.identity;
|
||||
root.localScale = Vector3.one;
|
||||
GetOrAdd<NetworkObject>(root.gameObject);
|
||||
|
||||
Transform visual = GetOrCreatePrimitive(root, "Button Visual", PrimitiveType.Cube);
|
||||
visual.localPosition = Vector3.zero;
|
||||
visual.localRotation = Quaternion.identity;
|
||||
visual.localScale = new Vector3(0.8f, 0.8f, 0.3f);
|
||||
NetworkButtonInteractable button = GetOrAdd<NetworkButtonInteractable>(root.gameObject);
|
||||
SerializedObject serialized = new(button);
|
||||
SetString(serialized, "interactionLabel", label);
|
||||
SetFloat(serialized, "maximumInteractionDistance", 3.5f);
|
||||
serialized.ApplyModifiedPropertiesWithoutUndo();
|
||||
return button;
|
||||
}
|
||||
|
||||
private static TMP_Text CreateTimerText(Transform parent)
|
||||
{
|
||||
Transform root = GetOrCreateChild(parent, "Timer Display");
|
||||
root.localPosition = new Vector3(2.45f, 2.25f, -1.35f);
|
||||
root.localRotation = Quaternion.Euler(0f, 180f, 0f);
|
||||
root.localScale = Vector3.one * 0.12f;
|
||||
TextMeshPro text = GetOrAdd<TextMeshPro>(root.gameObject);
|
||||
text.alignment = TextAlignmentOptions.Center;
|
||||
text.fontSize = 4f;
|
||||
text.text = "INSERT ROCKET";
|
||||
text.rectTransform.sizeDelta = new Vector2(12f, 3f);
|
||||
return text;
|
||||
}
|
||||
|
||||
private static void ConfigureMapMarker()
|
||||
{
|
||||
MapController map = Object.FindFirstObjectByType<MapController>(FindObjectsInactive.Include);
|
||||
if (map == null)
|
||||
return;
|
||||
|
||||
Transform root = map.transform.Find("Impact Markers");
|
||||
if (root == null)
|
||||
{
|
||||
root = new GameObject("Impact Markers").transform;
|
||||
root.SetParent(map.transform, false);
|
||||
}
|
||||
MapImpactMarkerView view = GetOrAdd<MapImpactMarkerView>(root.gameObject);
|
||||
view.Configure(map.CoordinateSystem);
|
||||
|
||||
SerializedObject serializedMap = new(map);
|
||||
SetObject(serializedMap, "impactMarkerView", view);
|
||||
serializedMap.ApplyModifiedPropertiesWithoutUndo();
|
||||
EditorUtility.SetDirty(map);
|
||||
}
|
||||
|
||||
private static void EnsureDataFolder()
|
||||
{
|
||||
if (!AssetDatabase.IsValidFolder("Assets/CONTENT/Game/Data"))
|
||||
AssetDatabase.CreateFolder("Assets/CONTENT/Game", "Data");
|
||||
if (!AssetDatabase.IsValidFolder(DataFolder))
|
||||
AssetDatabase.CreateFolder("Assets/CONTENT/Game/Data", "Cannon");
|
||||
}
|
||||
|
||||
private static Transform GetOrCreateChild(Transform parent, string name)
|
||||
{
|
||||
Transform existing = parent.Find(name);
|
||||
if (existing != null)
|
||||
return existing;
|
||||
GameObject child = new(name);
|
||||
child.transform.SetParent(parent, false);
|
||||
Undo.RegisterCreatedObjectUndo(child, $"Create {name}");
|
||||
return child.transform;
|
||||
}
|
||||
|
||||
private static Transform GetOrCreatePrimitive(
|
||||
Transform parent,
|
||||
string name,
|
||||
PrimitiveType primitiveType)
|
||||
{
|
||||
Transform existing = parent.Find(name);
|
||||
if (existing != null)
|
||||
return existing;
|
||||
GameObject primitive = GameObject.CreatePrimitive(primitiveType);
|
||||
primitive.name = name;
|
||||
primitive.transform.SetParent(parent, false);
|
||||
Undo.RegisterCreatedObjectUndo(primitive, $"Create {name}");
|
||||
return primitive.transform;
|
||||
}
|
||||
|
||||
private static T GetOrAdd<T>(GameObject target) where T : Component
|
||||
{
|
||||
T component = target.GetComponent<T>();
|
||||
return component != null ? component : Undo.AddComponent<T>(target);
|
||||
}
|
||||
|
||||
private static void SetObject(SerializedObject target, string property, Object value) =>
|
||||
target.FindProperty(property).objectReferenceValue = value;
|
||||
private static void SetFloat(SerializedObject target, string property, float value) =>
|
||||
target.FindProperty(property).floatValue = value;
|
||||
private static void SetBool(SerializedObject target, string property, bool value) =>
|
||||
target.FindProperty(property).boolValue = value;
|
||||
private static void SetString(SerializedObject target, string property, string value) =>
|
||||
target.FindProperty(property).stringValue = value;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fe3002dfbec1fe1458dcd6058b1486c3
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8382605c6a901154783b2faf23067629
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c09a47e212ae515468d691409560c5d3
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 03ef14f0e5db05941afa0b0132e3bfb3
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,126 @@
|
||||
using System.Collections.Generic;
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
|
||||
[DisallowMultipleComponent]
|
||||
public sealed class NetworkShockwaveEmitter : NetworkBehaviour
|
||||
{
|
||||
private const int MaximumOverlaps = 256;
|
||||
|
||||
[SerializeField] private ShockwaveProfile profile;
|
||||
[SerializeField] private Transform origin;
|
||||
[SerializeField] private LayerMask physicsLayers = ~0;
|
||||
[SerializeField] private UnityEvent shockwaveEmitted;
|
||||
|
||||
private readonly NetworkVariable<uint> sequence = new(
|
||||
0,
|
||||
NetworkVariableReadPermission.Everyone,
|
||||
NetworkVariableWritePermission.Server);
|
||||
|
||||
private readonly Collider[] overlapBuffer = new Collider[MaximumOverlaps];
|
||||
private readonly HashSet<Rigidbody> affectedBodies = new();
|
||||
|
||||
public ShockwaveProfile Profile => profile;
|
||||
public Vector3 Origin => origin != null ? origin.position : transform.position;
|
||||
public uint Sequence => sequence.Value;
|
||||
|
||||
public event System.Action Emitted;
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
sequence.OnValueChanged += HandleSequenceChanged;
|
||||
}
|
||||
|
||||
public override void OnNetworkDespawn()
|
||||
{
|
||||
sequence.OnValueChanged -= HandleSequenceChanged;
|
||||
}
|
||||
|
||||
public bool EmitShockwave() => EmitShockwave(Origin);
|
||||
|
||||
public bool EmitShockwave(Vector3 worldOrigin)
|
||||
{
|
||||
if (!IsServer || !IsSpawned || profile == null)
|
||||
return false;
|
||||
|
||||
ApplyToPlayers(worldOrigin);
|
||||
ApplyToPhysics(worldOrigin);
|
||||
sequence.Value++;
|
||||
return true;
|
||||
}
|
||||
|
||||
[ContextMenu("Debug/Emit Shockwave (Server Play Mode)")]
|
||||
private void EmitFromContextMenu()
|
||||
{
|
||||
if (Application.isPlaying)
|
||||
EmitShockwave();
|
||||
}
|
||||
|
||||
private void ApplyToPlayers(Vector3 worldOrigin)
|
||||
{
|
||||
foreach (PlayerImpactController player in
|
||||
FindObjectsByType<PlayerImpactController>(FindObjectsSortMode.None))
|
||||
{
|
||||
if (!player.IsSpawned)
|
||||
continue;
|
||||
|
||||
Vector3 offset = player.transform.position - worldOrigin;
|
||||
float distance = offset.magnitude;
|
||||
if (!profile.TryEvaluate(distance, out ShockwaveZone zone))
|
||||
continue;
|
||||
|
||||
Vector3 direction = CreateDirection(offset, zone.UpwardBias);
|
||||
player.ApplyImpactOnServer(direction * zone.PlayerImpulse, zone);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyToPhysics(Vector3 worldOrigin)
|
||||
{
|
||||
int count = Physics.OverlapSphereNonAlloc(
|
||||
worldOrigin,
|
||||
profile.MaximumRadius,
|
||||
overlapBuffer,
|
||||
physicsLayers,
|
||||
QueryTriggerInteraction.Ignore);
|
||||
|
||||
affectedBodies.Clear();
|
||||
for (int index = 0; index < count; index++)
|
||||
{
|
||||
Collider hit = overlapBuffer[index];
|
||||
Rigidbody body = hit != null ? hit.attachedRigidbody : null;
|
||||
if (body == null || body.isKinematic || !affectedBodies.Add(body) ||
|
||||
body.GetComponentInParent<PlayerImpactController>() != null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Vector3 offset = body.worldCenterOfMass - worldOrigin;
|
||||
if (!profile.TryEvaluate(offset.magnitude, out ShockwaveZone zone) ||
|
||||
zone.RigidbodyImpulse <= 0f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
body.AddForce(
|
||||
CreateDirection(offset, zone.UpwardBias) * zone.RigidbodyImpulse,
|
||||
ForceMode.Impulse);
|
||||
}
|
||||
}
|
||||
|
||||
private static Vector3 CreateDirection(Vector3 offset, float upwardBias)
|
||||
{
|
||||
if (offset.sqrMagnitude < 0.0001f)
|
||||
offset = Vector3.up;
|
||||
offset.Normalize();
|
||||
return (offset + Vector3.up * Mathf.Clamp01(upwardBias)).normalized;
|
||||
}
|
||||
|
||||
private void HandleSequenceChanged(uint previous, uint current)
|
||||
{
|
||||
if (current == previous)
|
||||
return;
|
||||
Emitted?.Invoke();
|
||||
shockwaveEmitted?.Invoke();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 43f56ec799f9aec4ca1bf372c88592cd
|
||||
@@ -0,0 +1,110 @@
|
||||
using System;
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
|
||||
public struct PlayerImpactState : INetworkSerializable, IEquatable<PlayerImpactState>
|
||||
{
|
||||
public uint Sequence;
|
||||
public Vector3 Impulse;
|
||||
public float ShakeAmplitude;
|
||||
public float ShakeDuration;
|
||||
public float ShakeFrequency;
|
||||
public float ShakeSeed;
|
||||
|
||||
public PlayerImpactState(
|
||||
uint sequence,
|
||||
Vector3 impulse,
|
||||
float shakeAmplitude,
|
||||
float shakeDuration,
|
||||
float shakeFrequency,
|
||||
float shakeSeed)
|
||||
{
|
||||
Sequence = sequence;
|
||||
Impulse = impulse;
|
||||
ShakeAmplitude = shakeAmplitude;
|
||||
ShakeDuration = shakeDuration;
|
||||
ShakeFrequency = shakeFrequency;
|
||||
ShakeSeed = shakeSeed;
|
||||
}
|
||||
|
||||
public void NetworkSerialize<T>(BufferSerializer<T> serializer)
|
||||
where T : IReaderWriter
|
||||
{
|
||||
serializer.SerializeValue(ref Sequence);
|
||||
serializer.SerializeValue(ref Impulse);
|
||||
serializer.SerializeValue(ref ShakeAmplitude);
|
||||
serializer.SerializeValue(ref ShakeDuration);
|
||||
serializer.SerializeValue(ref ShakeFrequency);
|
||||
serializer.SerializeValue(ref ShakeSeed);
|
||||
}
|
||||
|
||||
public bool Equals(PlayerImpactState other) =>
|
||||
Sequence == other.Sequence && Impulse == other.Impulse &&
|
||||
ShakeAmplitude.Equals(other.ShakeAmplitude) &&
|
||||
ShakeDuration.Equals(other.ShakeDuration) &&
|
||||
ShakeFrequency.Equals(other.ShakeFrequency) &&
|
||||
ShakeSeed.Equals(other.ShakeSeed);
|
||||
}
|
||||
|
||||
[DisallowMultipleComponent]
|
||||
public sealed class PlayerImpactController : NetworkBehaviour, IPlayerSystem
|
||||
{
|
||||
private readonly NetworkVariable<PlayerImpactState> impactState = new(
|
||||
default,
|
||||
NetworkVariableReadPermission.Everyone,
|
||||
NetworkVariableWritePermission.Server);
|
||||
|
||||
private PlayerController movement;
|
||||
private PlayerCameraShakeController cameraShake;
|
||||
|
||||
public uint LastImpactSequence => impactState.Value.Sequence;
|
||||
public event Action<PlayerImpactState> ImpactApplied;
|
||||
|
||||
public void Initialize(PlayerFacade facade)
|
||||
{
|
||||
movement = GetComponent<PlayerController>();
|
||||
cameraShake = GetComponent<PlayerCameraShakeController>();
|
||||
}
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
impactState.OnValueChanged += HandleImpactChanged;
|
||||
}
|
||||
|
||||
public override void OnNetworkDespawn()
|
||||
{
|
||||
impactState.OnValueChanged -= HandleImpactChanged;
|
||||
}
|
||||
|
||||
public bool ApplyImpactOnServer(Vector3 impulse, ShockwaveZone zone)
|
||||
{
|
||||
if (!IsServer || !IsSpawned)
|
||||
return false;
|
||||
|
||||
impactState.Value = new PlayerImpactState(
|
||||
impactState.Value.Sequence + 1,
|
||||
impulse,
|
||||
zone.CameraShakeAmplitude,
|
||||
zone.CameraShakeDuration,
|
||||
zone.CameraShakeFrequency,
|
||||
UnityEngine.Random.Range(0f, 1000f));
|
||||
return true;
|
||||
}
|
||||
|
||||
private void HandleImpactChanged(PlayerImpactState previous, PlayerImpactState current)
|
||||
{
|
||||
if (current.Sequence == 0 || current.Sequence == previous.Sequence)
|
||||
return;
|
||||
|
||||
ImpactApplied?.Invoke(current);
|
||||
if (!IsOwner)
|
||||
return;
|
||||
|
||||
movement?.AddImpulse(current.Impulse);
|
||||
cameraShake?.PlayShake(
|
||||
current.ShakeAmplitude,
|
||||
current.ShakeDuration,
|
||||
current.ShakeFrequency,
|
||||
current.ShakeSeed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6a7ddedc58d3467449dfbe83af1c9c51
|
||||
@@ -0,0 +1,57 @@
|
||||
using System.Collections;
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
|
||||
[DisallowMultipleComponent]
|
||||
[RequireComponent(typeof(NetworkRocketItem), typeof(NetworkShockwaveEmitter))]
|
||||
public sealed class RocketImpactExplosion : NetworkBehaviour
|
||||
{
|
||||
[SerializeField] private NetworkRocketItem rocket;
|
||||
[SerializeField] private NetworkShockwaveEmitter shockwave;
|
||||
[SerializeField] private bool despawnAfterExplosion = true;
|
||||
[SerializeField, Min(0f)] private float despawnDelay = 0.15f;
|
||||
|
||||
private bool exploded;
|
||||
|
||||
private void Reset() => ResolveReferences();
|
||||
private void Awake() => ResolveReferences();
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
rocket.CriticalImpact += HandleCriticalImpact;
|
||||
}
|
||||
|
||||
public override void OnNetworkDespawn()
|
||||
{
|
||||
if (rocket != null)
|
||||
rocket.CriticalImpact -= HandleCriticalImpact;
|
||||
}
|
||||
|
||||
private void HandleCriticalImpact()
|
||||
{
|
||||
if (!IsServer || exploded)
|
||||
return;
|
||||
|
||||
exploded = true;
|
||||
shockwave.EmitShockwave(rocket.LastCriticalImpactPoint);
|
||||
if (despawnAfterExplosion)
|
||||
StartCoroutine(DespawnAfterDelay());
|
||||
}
|
||||
|
||||
private IEnumerator DespawnAfterDelay()
|
||||
{
|
||||
if (despawnDelay > 0f)
|
||||
yield return new WaitForSeconds(despawnDelay);
|
||||
|
||||
if (IsServer && NetworkObject != null && NetworkObject.IsSpawned)
|
||||
NetworkObject.Despawn(true);
|
||||
}
|
||||
|
||||
private void ResolveReferences()
|
||||
{
|
||||
if (rocket == null)
|
||||
rocket = GetComponent<NetworkRocketItem>();
|
||||
if (shockwave == null)
|
||||
shockwave = GetComponent<NetworkShockwaveEmitter>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ac11f143457835f46a1a2defaa624354
|
||||
@@ -0,0 +1,68 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
[Serializable]
|
||||
public struct ShockwaveZone
|
||||
{
|
||||
[Min(0.1f)] public float Radius;
|
||||
[Min(0f)] public float PlayerImpulse;
|
||||
[Min(0f)] public float RigidbodyImpulse;
|
||||
[Range(0f, 1f)] public float UpwardBias;
|
||||
[Min(0f)] public float CameraShakeAmplitude;
|
||||
[Min(0f)] public float CameraShakeDuration;
|
||||
[Min(0.1f)] public float CameraShakeFrequency;
|
||||
}
|
||||
|
||||
[CreateAssetMenu(
|
||||
fileName = "ShockwaveProfile",
|
||||
menuName = "FORT-BO/Effects/Shockwave Profile")]
|
||||
public sealed class ShockwaveProfile : ScriptableObject
|
||||
{
|
||||
[Tooltip("Zones are evaluated from the smallest radius to the largest.")]
|
||||
[SerializeField] private ShockwaveZone[] zones = Array.Empty<ShockwaveZone>();
|
||||
|
||||
public float MaximumRadius
|
||||
{
|
||||
get
|
||||
{
|
||||
float maximum = 0f;
|
||||
foreach (ShockwaveZone zone in zones)
|
||||
maximum = Mathf.Max(maximum, zone.Radius);
|
||||
return maximum;
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryEvaluate(float distance, out ShockwaveZone result)
|
||||
{
|
||||
result = default;
|
||||
float bestRadius = float.PositiveInfinity;
|
||||
bool found = false;
|
||||
|
||||
foreach (ShockwaveZone zone in zones)
|
||||
{
|
||||
if (distance > zone.Radius || zone.Radius >= bestRadius)
|
||||
continue;
|
||||
|
||||
result = zone;
|
||||
bestRadius = zone.Radius;
|
||||
found = true;
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
for (int index = 0; index < zones.Length; index++)
|
||||
{
|
||||
ShockwaveZone zone = zones[index];
|
||||
zone.Radius = Mathf.Max(0.1f, zone.Radius);
|
||||
zone.PlayerImpulse = Mathf.Max(0f, zone.PlayerImpulse);
|
||||
zone.RigidbodyImpulse = Mathf.Max(0f, zone.RigidbodyImpulse);
|
||||
zone.CameraShakeAmplitude = Mathf.Max(0f, zone.CameraShakeAmplitude);
|
||||
zone.CameraShakeDuration = Mathf.Max(0f, zone.CameraShakeDuration);
|
||||
zone.CameraShakeFrequency = Mathf.Max(0.1f, zone.CameraShakeFrequency);
|
||||
zones[index] = zone;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 96e16f6f88696ea4c8b884f3e7cef9f7
|
||||
@@ -1,27 +1,39 @@
|
||||
using System;
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
|
||||
public sealed class NetworkButtonInteractable : NetworkInteractable
|
||||
{
|
||||
[SerializeField] private UnityEvent pressed = new();
|
||||
[SerializeField] private UnityEvent<bool> buttonStateChanged;
|
||||
|
||||
private readonly NetworkVariable<bool> isPressed = new(
|
||||
false,
|
||||
NetworkVariableReadPermission.Everyone,
|
||||
NetworkVariableWritePermission.Server);
|
||||
private readonly NetworkVariable<uint> pressSequence = new(
|
||||
0,
|
||||
NetworkVariableReadPermission.Everyone,
|
||||
NetworkVariableWritePermission.Server);
|
||||
|
||||
public bool IsPressed => isPressed.Value;
|
||||
public uint PressSequence => pressSequence.Value;
|
||||
|
||||
public event Action Pressed;
|
||||
public event Action<bool> StateChanged;
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
isPressed.OnValueChanged += HandleStateChanged;
|
||||
pressSequence.OnValueChanged += HandlePressSequenceChanged;
|
||||
ApplyState(isPressed.Value);
|
||||
}
|
||||
|
||||
public override void OnNetworkDespawn()
|
||||
{
|
||||
isPressed.OnValueChanged -= HandleStateChanged;
|
||||
pressSequence.OnValueChanged -= HandlePressSequenceChanged;
|
||||
}
|
||||
|
||||
protected override bool CanInteractOnServer(ulong senderClientId) => true;
|
||||
@@ -29,10 +41,23 @@ public sealed class NetworkButtonInteractable : NetworkInteractable
|
||||
protected override void InteractOnServer(ulong senderClientId, NetworkObject playerObject)
|
||||
{
|
||||
isPressed.Value = !isPressed.Value;
|
||||
ApplyState(isPressed.Value);
|
||||
pressSequence.Value++;
|
||||
}
|
||||
|
||||
private void HandleStateChanged(bool _, bool currentValue) => ApplyState(currentValue);
|
||||
|
||||
private void ApplyState(bool currentValue) => buttonStateChanged?.Invoke(currentValue);
|
||||
private void HandlePressSequenceChanged(uint previous, uint current)
|
||||
{
|
||||
if (current == previous)
|
||||
return;
|
||||
|
||||
Pressed?.Invoke();
|
||||
pressed?.Invoke();
|
||||
}
|
||||
|
||||
private void ApplyState(bool currentValue)
|
||||
{
|
||||
StateChanged?.Invoke(currentValue);
|
||||
buttonStateChanged?.Invoke(currentValue);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,6 +404,12 @@ public sealed class NetworkDraggableInteractable : NetworkInteractable, IWorksta
|
||||
{
|
||||
bool shouldBeKinematic = IsExternallyControlled || !IsServer;
|
||||
|
||||
if (IsExternallyControlled && !physicsBody.isKinematic)
|
||||
{
|
||||
physicsBody.linearVelocity = Vector3.zero;
|
||||
physicsBody.angularVelocity = Vector3.zero;
|
||||
}
|
||||
|
||||
if (shouldBeKinematic)
|
||||
{
|
||||
physicsBody.collisionDetectionMode = CollisionDetectionMode.ContinuousSpeculative;
|
||||
@@ -417,11 +423,6 @@ public sealed class NetworkDraggableInteractable : NetworkInteractable, IWorksta
|
||||
physicsBody.collisionDetectionMode = ResolveDynamicCollisionDetectionMode();
|
||||
}
|
||||
|
||||
if (IsExternallyControlled)
|
||||
{
|
||||
physicsBody.linearVelocity = Vector3.zero;
|
||||
physicsBody.angularVelocity = Vector3.zero;
|
||||
}
|
||||
}
|
||||
|
||||
if (HasAnyDragger)
|
||||
|
||||
@@ -37,8 +37,7 @@ public class NetworkInteractable : NetworkBehaviour, IInteractable
|
||||
if (!TryGetPlayerObject(senderClientId, out NetworkObject playerObject))
|
||||
return;
|
||||
|
||||
float maximumDistanceSquared = maximumInteractionDistance * maximumInteractionDistance;
|
||||
if ((playerObject.transform.position - transform.position).sqrMagnitude > maximumDistanceSquared)
|
||||
if (!IsWithinInteractionDistance(playerObject.transform.position))
|
||||
return;
|
||||
|
||||
if (!CanInteractOnServer(senderClientId))
|
||||
@@ -47,6 +46,28 @@ public class NetworkInteractable : NetworkBehaviour, IInteractable
|
||||
InteractOnServer(senderClientId, playerObject);
|
||||
}
|
||||
|
||||
private bool IsWithinInteractionDistance(Vector3 playerPosition)
|
||||
{
|
||||
float maximumDistanceSquared = maximumInteractionDistance * maximumInteractionDistance;
|
||||
if ((playerPosition - transform.position).sqrMagnitude <= maximumDistanceSquared)
|
||||
return true;
|
||||
|
||||
foreach (Collider interactionCollider in GetComponentsInChildren<Collider>(true))
|
||||
{
|
||||
if (interactionCollider == null || !interactionCollider.enabled ||
|
||||
!interactionCollider.gameObject.activeInHierarchy)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Vector3 closestPoint = interactionCollider.ClosestPoint(playerPosition);
|
||||
if ((playerPosition - closestPoint).sqrMagnitude <= maximumDistanceSquared)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected virtual bool CanInteractOnServer(ulong senderClientId) => false;
|
||||
protected virtual void InteractOnServer(ulong senderClientId, NetworkObject playerObject) { }
|
||||
|
||||
@@ -58,4 +79,3 @@ public class NetworkInteractable : NetworkBehaviour, IInteractable
|
||||
return playerObject != null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
@@ -44,6 +45,7 @@ public sealed class NetworkPhysicalLeverInteractable :
|
||||
NetworkVariableWritePermission.Server);
|
||||
|
||||
private bool hasSnapTarget;
|
||||
private bool maximumEndpointLatched;
|
||||
private float snapTargetValue;
|
||||
private float automaticReturnTime = -1f;
|
||||
|
||||
@@ -52,7 +54,11 @@ public sealed class NetworkPhysicalLeverInteractable :
|
||||
public bool IsControlled => controllingClientId.Value != NoController;
|
||||
public bool IsControlledByLocalPlayer =>
|
||||
NetworkManager != null && IsControlledBy(NetworkManager.LocalClientId);
|
||||
public string ReleasePrompt => "[Hold E] Pulling lever";
|
||||
public string ReleasePrompt => "[Hold LMB] Pulling lever";
|
||||
|
||||
public event Action<float> ValueChanged;
|
||||
public event Action<bool> StateChanged;
|
||||
public event Action MaximumReached;
|
||||
|
||||
public override string InteractionPrompt => IsControlledByLocalPlayer
|
||||
? ReleasePrompt
|
||||
@@ -92,6 +98,7 @@ public sealed class NetworkPhysicalLeverInteractable :
|
||||
{
|
||||
synchronizedValue.Value = InitialPositionValue;
|
||||
isActivated.Value = initialPositionIsMaximum;
|
||||
maximumEndpointLatched = initialPositionIsMaximum;
|
||||
}
|
||||
|
||||
synchronizedValue.OnValueChanged += HandleValueChanged;
|
||||
@@ -216,9 +223,24 @@ public sealed class NetworkPhysicalLeverInteractable :
|
||||
const float endpointTolerance = 0.001f;
|
||||
|
||||
if (synchronizedValue.Value >= 1f - endpointTolerance)
|
||||
{
|
||||
SetActivation(true);
|
||||
else if (synchronizedValue.Value <= endpointTolerance)
|
||||
if (!maximumEndpointLatched)
|
||||
{
|
||||
maximumEndpointLatched = true;
|
||||
MaximumReached?.Invoke();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
maximumEndpointLatched = false;
|
||||
}
|
||||
|
||||
if (synchronizedValue.Value <= endpointTolerance)
|
||||
{
|
||||
maximumEndpointLatched = false;
|
||||
SetActivation(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetActivation(bool activated)
|
||||
@@ -304,11 +326,15 @@ public sealed class NetworkPhysicalLeverInteractable :
|
||||
private void HandleValueChanged(float _, float currentValue)
|
||||
{
|
||||
ApplyVisual(currentValue);
|
||||
ValueChanged?.Invoke(currentValue);
|
||||
leverValueChanged?.Invoke(currentValue);
|
||||
}
|
||||
|
||||
private void HandleActivationChanged(bool _, bool currentState) =>
|
||||
private void HandleActivationChanged(bool _, bool currentState)
|
||||
{
|
||||
StateChanged?.Invoke(currentState);
|
||||
leverStateChanged?.Invoke(currentState);
|
||||
}
|
||||
|
||||
private void ApplyVisual(float value)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
using System;
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
|
||||
[DisallowMultipleComponent]
|
||||
public sealed class NetworkPhysicalWheelInteractable :
|
||||
NetworkInteractable,
|
||||
IPlayerInteractionSession
|
||||
{
|
||||
private const ulong NoController = ulong.MaxValue;
|
||||
|
||||
[Header("Wheel visual")]
|
||||
[SerializeField] private Transform wheelVisual;
|
||||
[SerializeField] private Vector3 neutralLocalEulerAngles;
|
||||
[SerializeField] private Vector3 localRotationAxis = Vector3.right;
|
||||
[SerializeField] private Vector3 localReferenceDirection = Vector3.up;
|
||||
[SerializeField, Range(-720f, 0f)] private float minimumAngle = -180f;
|
||||
[SerializeField, Range(0f, 720f)] private float maximumAngle = 180f;
|
||||
[SerializeField, Range(0f, 1f)] private float initialNormalizedValue = 0.5f;
|
||||
[SerializeField, Min(1f)] private float maximumRotationSpeed = 360f;
|
||||
|
||||
[Header("Control")]
|
||||
[SerializeField, Min(0.5f)] private float maximumControlDistance = 4f;
|
||||
[SerializeField] private UnityEvent<float> normalizedValueChanged;
|
||||
|
||||
private readonly NetworkVariable<ulong> controllingClientId = new(
|
||||
NoController,
|
||||
NetworkVariableReadPermission.Everyone,
|
||||
NetworkVariableWritePermission.Server);
|
||||
|
||||
private readonly NetworkVariable<float> synchronizedAngle = new(
|
||||
0f,
|
||||
NetworkVariableReadPermission.Everyone,
|
||||
NetworkVariableWritePermission.Server);
|
||||
|
||||
private float lastPointerAngle;
|
||||
private bool hasPointerAngle;
|
||||
|
||||
public float Angle => synchronizedAngle.Value;
|
||||
public float NormalizedValue => Mathf.InverseLerp(
|
||||
minimumAngle,
|
||||
maximumAngle,
|
||||
synchronizedAngle.Value);
|
||||
public bool IsControlled => controllingClientId.Value != NoController;
|
||||
public bool IsControlledByLocalPlayer =>
|
||||
NetworkManager != null && IsControlledBy(NetworkManager.LocalClientId);
|
||||
public string ReleasePrompt => "[Hold LMB] Turning wheel";
|
||||
|
||||
public override string InteractionPrompt => IsControlledByLocalPlayer
|
||||
? ReleasePrompt
|
||||
: IsControlled
|
||||
? "Wheel in use"
|
||||
: InteractionLabel;
|
||||
|
||||
public override bool IsInteractionAvailable =>
|
||||
base.IsInteractionAvailable && (!IsControlled || IsControlledByLocalPlayer);
|
||||
|
||||
public event Action<float> ValueChanged;
|
||||
|
||||
private Quaternion NeutralLocalRotation => Quaternion.Euler(neutralLocalEulerAngles);
|
||||
private Vector3 RotationAxis => localRotationAxis.sqrMagnitude > 0.0001f
|
||||
? localRotationAxis.normalized
|
||||
: Vector3.right;
|
||||
private Vector3 ReferenceDirection => localReferenceDirection.sqrMagnitude > 0.0001f
|
||||
? localReferenceDirection.normalized
|
||||
: Vector3.up;
|
||||
|
||||
private void Reset()
|
||||
{
|
||||
wheelVisual = transform;
|
||||
neutralLocalEulerAngles = wheelVisual.localEulerAngles;
|
||||
}
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
if (maximumAngle <= minimumAngle)
|
||||
maximumAngle = minimumAngle + 1f;
|
||||
}
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
PlayerInteractionSessionRegistry.Register(this);
|
||||
if (IsServer)
|
||||
{
|
||||
synchronizedAngle.Value = Mathf.Lerp(
|
||||
minimumAngle,
|
||||
maximumAngle,
|
||||
initialNormalizedValue);
|
||||
}
|
||||
|
||||
synchronizedAngle.OnValueChanged += HandleAngleChanged;
|
||||
ApplyVisual(synchronizedAngle.Value);
|
||||
InvokeValueChanged();
|
||||
}
|
||||
|
||||
public override void OnNetworkDespawn()
|
||||
{
|
||||
PlayerInteractionSessionRegistry.Unregister(this);
|
||||
synchronizedAngle.OnValueChanged -= HandleAngleChanged;
|
||||
}
|
||||
|
||||
private void FixedUpdate()
|
||||
{
|
||||
if (!IsServer || !IsSpawned || !IsControlled || wheelVisual == null)
|
||||
return;
|
||||
|
||||
if (!TryGetPlayerObject(controllingClientId.Value, out NetworkObject playerObject) ||
|
||||
!IsPlayerWithinControlDistance(playerObject))
|
||||
{
|
||||
EndControl();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TryCalculatePointerAngle(playerObject, out float pointerAngle))
|
||||
{
|
||||
hasPointerAngle = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasPointerAngle)
|
||||
{
|
||||
lastPointerAngle = pointerAngle;
|
||||
hasPointerAngle = true;
|
||||
return;
|
||||
}
|
||||
|
||||
float pointerDelta = Mathf.DeltaAngle(lastPointerAngle, pointerAngle);
|
||||
lastPointerAngle = pointerAngle;
|
||||
float maximumStep = maximumRotationSpeed * Time.fixedDeltaTime;
|
||||
float nextAngle = Mathf.Clamp(
|
||||
synchronizedAngle.Value + Mathf.Clamp(pointerDelta, -maximumStep, maximumStep),
|
||||
minimumAngle,
|
||||
maximumAngle);
|
||||
if (!Mathf.Approximately(nextAngle, synchronizedAngle.Value))
|
||||
synchronizedAngle.Value = nextAngle;
|
||||
}
|
||||
|
||||
public bool IsControlledBy(ulong clientId) =>
|
||||
controllingClientId.Value == clientId;
|
||||
|
||||
public void RequestRelease()
|
||||
{
|
||||
if (IsSpawned && IsControlledByLocalPlayer)
|
||||
RequestReleaseServerRpc();
|
||||
}
|
||||
|
||||
protected override bool CanInteractOnServer(ulong senderClientId) =>
|
||||
!IsControlled || IsControlledBy(senderClientId);
|
||||
|
||||
protected override void InteractOnServer(
|
||||
ulong senderClientId,
|
||||
NetworkObject playerObject)
|
||||
{
|
||||
if (IsControlledBy(senderClientId))
|
||||
EndControl();
|
||||
else
|
||||
BeginControl(senderClientId, playerObject);
|
||||
}
|
||||
|
||||
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
|
||||
private void RequestReleaseServerRpc(RpcParams rpcParams = default)
|
||||
{
|
||||
if (IsControlledBy(rpcParams.Receive.SenderClientId))
|
||||
EndControl();
|
||||
}
|
||||
|
||||
private void BeginControl(ulong clientId, NetworkObject playerObject)
|
||||
{
|
||||
controllingClientId.Value = clientId;
|
||||
hasPointerAngle = TryCalculatePointerAngle(playerObject, out lastPointerAngle);
|
||||
}
|
||||
|
||||
private void EndControl()
|
||||
{
|
||||
controllingClientId.Value = NoController;
|
||||
hasPointerAngle = false;
|
||||
}
|
||||
|
||||
private bool IsPlayerWithinControlDistance(NetworkObject playerObject)
|
||||
{
|
||||
float maximumDistanceSquared = maximumControlDistance * maximumControlDistance;
|
||||
return (playerObject.transform.position - wheelVisual.position).sqrMagnitude <=
|
||||
maximumDistanceSquared;
|
||||
}
|
||||
|
||||
private bool TryCalculatePointerAngle(
|
||||
NetworkObject playerObject,
|
||||
out float pointerAngle)
|
||||
{
|
||||
pointerAngle = 0f;
|
||||
FirstPersonCameraController cameraController =
|
||||
playerObject.GetComponent<FirstPersonCameraController>();
|
||||
Transform aimTransform = cameraController != null
|
||||
? cameraController.AimTransform
|
||||
: playerObject.transform;
|
||||
|
||||
Transform wheelParent = wheelVisual.parent;
|
||||
Vector3 axisWorld = wheelParent != null
|
||||
? wheelParent.TransformDirection(NeutralLocalRotation * RotationAxis)
|
||||
: NeutralLocalRotation * RotationAxis;
|
||||
Vector3 referenceWorld = wheelParent != null
|
||||
? wheelParent.TransformDirection(NeutralLocalRotation * ReferenceDirection)
|
||||
: NeutralLocalRotation * ReferenceDirection;
|
||||
|
||||
Ray aimRay = new(aimTransform.position, aimTransform.forward);
|
||||
Plane wheelPlane = new(axisWorld, wheelVisual.position);
|
||||
Vector3 targetPoint;
|
||||
if (wheelPlane.Raycast(aimRay, out float enter) && enter >= 0f)
|
||||
{
|
||||
targetPoint = aimRay.GetPoint(enter);
|
||||
}
|
||||
else
|
||||
{
|
||||
float distanceAlongRay = Mathf.Max(
|
||||
0.25f,
|
||||
Vector3.Dot(wheelVisual.position - aimRay.origin, aimRay.direction));
|
||||
targetPoint = aimRay.GetPoint(distanceAlongRay);
|
||||
}
|
||||
|
||||
Vector3 pointerDirection = Vector3.ProjectOnPlane(
|
||||
targetPoint - wheelVisual.position,
|
||||
axisWorld);
|
||||
if (pointerDirection.sqrMagnitude < 0.0001f)
|
||||
return false;
|
||||
|
||||
pointerAngle = Vector3.SignedAngle(
|
||||
referenceWorld,
|
||||
pointerDirection.normalized,
|
||||
axisWorld);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void HandleAngleChanged(float _, float current) =>
|
||||
ApplyNetworkValue(current);
|
||||
|
||||
private void ApplyNetworkValue(float angle)
|
||||
{
|
||||
ApplyVisual(angle);
|
||||
InvokeValueChanged();
|
||||
}
|
||||
|
||||
private void InvokeValueChanged()
|
||||
{
|
||||
float value = NormalizedValue;
|
||||
ValueChanged?.Invoke(value);
|
||||
normalizedValueChanged?.Invoke(value);
|
||||
}
|
||||
|
||||
private void ApplyVisual(float angle)
|
||||
{
|
||||
if (wheelVisual != null)
|
||||
{
|
||||
wheelVisual.localRotation = NeutralLocalRotation *
|
||||
Quaternion.AngleAxis(angle, RotationAxis);
|
||||
}
|
||||
}
|
||||
|
||||
[ContextMenu("Preview/Minimum")]
|
||||
private void PreviewMinimum() => ApplyVisual(minimumAngle);
|
||||
|
||||
[ContextMenu("Preview/Center")]
|
||||
private void PreviewCenter() => ApplyVisual(Mathf.Lerp(
|
||||
minimumAngle,
|
||||
maximumAngle,
|
||||
0.5f));
|
||||
|
||||
[ContextMenu("Preview/Maximum")]
|
||||
private void PreviewMaximum() => ApplyVisual(maximumAngle);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c645663bb9dcbaf43991bd38665923da
|
||||
@@ -7,11 +7,14 @@ public sealed class RocketItemDefinition : ItemDefinition
|
||||
{
|
||||
[Tooltip("Stable rocket family id used by launchers and orders.")]
|
||||
[SerializeField] private string rocketTypeId;
|
||||
[Tooltip("Combat type sent to the map when this rocket is fired.")]
|
||||
[SerializeField] private MissileType missileType = MissileType.Standard;
|
||||
[SerializeField, Min(0.1f)] private float criticalImpactSpeed = 7f;
|
||||
|
||||
public string RocketTypeId => string.IsNullOrWhiteSpace(rocketTypeId)
|
||||
? ItemId
|
||||
: rocketTypeId;
|
||||
public MissileType MissileType => missileType;
|
||||
public float CriticalImpactSpeed => criticalImpactSpeed;
|
||||
|
||||
protected override void OnValidate()
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
using UnityEngine;
|
||||
|
||||
[DisallowMultipleComponent]
|
||||
public sealed class PlayerCameraShakeController : MonoBehaviour, IPlayerSystem
|
||||
{
|
||||
private Transform cameraTransform;
|
||||
private Vector3 baseLocalPosition;
|
||||
private float amplitude;
|
||||
private float frequency;
|
||||
private float seed;
|
||||
private float endsAt;
|
||||
private float startedAt;
|
||||
private float duration;
|
||||
|
||||
public bool IsShaking => cameraTransform != null && Time.time < endsAt;
|
||||
|
||||
public void Initialize(PlayerFacade facade)
|
||||
{
|
||||
FirstPersonCameraController cameraController =
|
||||
GetComponent<FirstPersonCameraController>();
|
||||
cameraTransform = cameraController != null && cameraController.PlayerCamera != null
|
||||
? cameraController.PlayerCamera.transform
|
||||
: GetComponentInChildren<Camera>(true)?.transform;
|
||||
|
||||
if (cameraTransform != null)
|
||||
baseLocalPosition = cameraTransform.localPosition;
|
||||
}
|
||||
|
||||
public void PlayShake(
|
||||
float newAmplitude,
|
||||
float newDuration,
|
||||
float newFrequency,
|
||||
float newSeed = 0f)
|
||||
{
|
||||
if (cameraTransform == null || newAmplitude <= 0f || newDuration <= 0f)
|
||||
return;
|
||||
|
||||
float remainingStrength = IsShaking
|
||||
? amplitude * Mathf.Clamp01((endsAt - Time.time) / Mathf.Max(0.01f, duration))
|
||||
: 0f;
|
||||
amplitude = Mathf.Max(remainingStrength, newAmplitude);
|
||||
duration = newDuration;
|
||||
frequency = Mathf.Max(0.1f, newFrequency);
|
||||
seed = newSeed;
|
||||
startedAt = Time.time;
|
||||
endsAt = Time.time + newDuration;
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
if (cameraTransform == null)
|
||||
return;
|
||||
|
||||
if (!IsShaking)
|
||||
{
|
||||
cameraTransform.localPosition = baseLocalPosition;
|
||||
return;
|
||||
}
|
||||
|
||||
float normalizedTime = Mathf.Clamp01((Time.time - startedAt) / Mathf.Max(0.01f, duration));
|
||||
float strength = amplitude * (1f - normalizedTime) * (1f - normalizedTime);
|
||||
float sample = (Time.time - startedAt) * frequency;
|
||||
Vector3 noise = new(
|
||||
Mathf.PerlinNoise(seed, sample) * 2f - 1f,
|
||||
Mathf.PerlinNoise(seed + 17.31f, sample) * 2f - 1f,
|
||||
Mathf.PerlinNoise(seed + 39.77f, sample) * 2f - 1f);
|
||||
cameraTransform.localPosition = baseLocalPosition + noise * strength;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (cameraTransform != null)
|
||||
cameraTransform.localPosition = baseLocalPosition;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b12d3c124e93b5f4f9c06c7e870d4068
|
||||
@@ -13,10 +13,16 @@ public sealed class PlayerController : NetworkBehaviour, IPlayerSystem
|
||||
[SerializeField, Min(0.1f)] private float jumpHeight = 1.4f;
|
||||
[SerializeField, Min(0.1f)] private float gravity = 20f;
|
||||
|
||||
[Header("External impulses")]
|
||||
[SerializeField, Min(0.1f)] private float impulseDamping = 9f;
|
||||
|
||||
private CharacterController characterController;
|
||||
private Vector3 verticalVelocity;
|
||||
private Vector3 externalImpulseVelocity;
|
||||
private bool isInitialized;
|
||||
|
||||
public Vector3 ExternalImpulseVelocity => externalImpulseVelocity;
|
||||
|
||||
public void Initialize(PlayerFacade facade)
|
||||
{
|
||||
characterController = GetComponent<CharacterController>();
|
||||
@@ -39,6 +45,14 @@ public sealed class PlayerController : NetworkBehaviour, IPlayerSystem
|
||||
transform.Rotate(0f, yawDelta, 0f, Space.Self);
|
||||
}
|
||||
|
||||
public void AddImpulse(Vector3 velocityChange)
|
||||
{
|
||||
if (!isInitialized || !IsOwner)
|
||||
return;
|
||||
|
||||
externalImpulseVelocity += velocityChange;
|
||||
}
|
||||
|
||||
private void SimulateMovement(Vector2 moveInput, bool wantsToSprint, bool wantsToJump)
|
||||
{
|
||||
if (moveInput.sqrMagnitude > 1f)
|
||||
@@ -54,6 +68,12 @@ public sealed class PlayerController : NetworkBehaviour, IPlayerSystem
|
||||
|
||||
float speed = wantsToSprint ? sprintSpeed : walkSpeed;
|
||||
Vector3 horizontalDirection = transform.right * moveInput.x + transform.forward * moveInput.y;
|
||||
characterController.Move((horizontalDirection * speed + verticalVelocity) * Time.deltaTime);
|
||||
characterController.Move(
|
||||
(horizontalDirection * speed + verticalVelocity + externalImpulseVelocity) *
|
||||
Time.deltaTime);
|
||||
externalImpulseVelocity = Vector3.MoveTowards(
|
||||
externalImpulseVelocity,
|
||||
Vector3.zero,
|
||||
impulseDamping * Time.deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ public sealed class PlayerInteractionController : NetworkBehaviour, IPlayerSyste
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!isInitialized || !IsOwner || Keyboard.current == null)
|
||||
if (!isInitialized || !IsOwner)
|
||||
return;
|
||||
|
||||
if (PlayerInteractionSessionRegistry.TryGetControlledSession(
|
||||
@@ -31,7 +31,9 @@ public sealed class PlayerInteractionController : NetworkBehaviour, IPlayerSyste
|
||||
{
|
||||
focusedInteractable = null;
|
||||
|
||||
if (!Keyboard.current.eKey.isPressed && !activeSessionReleaseRequested)
|
||||
bool isHoldingControl = Mouse.current != null &&
|
||||
Mouse.current.leftButton.isPressed;
|
||||
if (!isHoldingControl && !activeSessionReleaseRequested)
|
||||
{
|
||||
activeSessionReleaseRequested = true;
|
||||
activeSession.RequestRelease();
|
||||
@@ -44,9 +46,14 @@ public sealed class PlayerInteractionController : NetworkBehaviour, IPlayerSyste
|
||||
activeSessionReleaseRequested = false;
|
||||
focusedInteractable = FindFocusedInteractable();
|
||||
|
||||
if (Keyboard.current.eKey.wasPressedThisFrame &&
|
||||
focusedInteractable != null &&
|
||||
focusedInteractable.IsInteractionAvailable)
|
||||
if (focusedInteractable == null || !focusedInteractable.IsInteractionAvailable)
|
||||
return;
|
||||
|
||||
bool startsHoldSession = focusedInteractable is IPlayerInteractionSession &&
|
||||
Mouse.current != null && Mouse.current.leftButton.wasPressedThisFrame;
|
||||
bool performsRegularInteraction = focusedInteractable is not IPlayerInteractionSession &&
|
||||
Keyboard.current != null && Keyboard.current.eKey.wasPressedThisFrame;
|
||||
if (startsHoldSession || performsRegularInteraction)
|
||||
{
|
||||
focusedInteractable.RequestInteraction();
|
||||
}
|
||||
@@ -73,6 +80,9 @@ public sealed class PlayerInteractionController : NetworkBehaviour, IPlayerSyste
|
||||
for (int hitIndex = 0; hitIndex < hitCount; hitIndex++)
|
||||
{
|
||||
RaycastHit hit = interactionHits[hitIndex];
|
||||
if (hit.collider != null && hit.collider.transform.IsChildOf(transform))
|
||||
continue;
|
||||
|
||||
IInteractable interactable = FindInteractable(hit.collider);
|
||||
if (interactable == null)
|
||||
{
|
||||
@@ -128,8 +138,11 @@ public sealed class PlayerInteractionController : NetworkBehaviour, IPlayerSyste
|
||||
if (focusedInteractable == null)
|
||||
return;
|
||||
|
||||
string inputPrompt = focusedInteractable is IPlayerInteractionSession
|
||||
? "[Hold LMB] "
|
||||
: "[E] ";
|
||||
string prompt = focusedInteractable.IsInteractionAvailable
|
||||
? "[E] " + focusedInteractable.InteractionPrompt
|
||||
? inputPrompt + focusedInteractable.InteractionPrompt
|
||||
: "In use";
|
||||
DrawPrompt(prompt);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,35 @@ public sealed class ItemProcessingRecipe : ScriptableObject
|
||||
(!string.IsNullOrEmpty(inputItem.ItemId) && item.ItemId == inputItem.ItemId);
|
||||
}
|
||||
|
||||
private void OnValidate() =>
|
||||
public bool TryValidateOutputPrefab(out string error)
|
||||
{
|
||||
error = string.Empty;
|
||||
if (outputItem == null || outputItem.WorldPrefab == null)
|
||||
return true;
|
||||
|
||||
NetworkItem prefabItem = outputItem.WorldPrefab.GetComponent<NetworkItem>();
|
||||
if (prefabItem == null)
|
||||
{
|
||||
error = $"Output prefab '{outputItem.WorldPrefab.name}' has no NetworkItem.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (prefabItem.Definition == outputItem)
|
||||
return true;
|
||||
|
||||
string actualOutput = prefabItem.Definition != null
|
||||
? prefabItem.Definition.name
|
||||
: "None";
|
||||
error = $"Output item '{outputItem.name}' points to prefab " +
|
||||
$"'{outputItem.WorldPrefab.name}', but that prefab uses definition " +
|
||||
$"'{actualOutput}'.";
|
||||
return false;
|
||||
}
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
recipeId = recipeId?.Trim().ToLowerInvariant();
|
||||
if (!TryValidateOutputPrefab(out string error))
|
||||
Debug.LogError($"Invalid processing recipe '{name}': {error}", this);
|
||||
}
|
||||
}
|
||||
|
||||
+8
-3
@@ -202,12 +202,16 @@ public sealed class NetworkItemProcessingWorkstation : NetworkWorkstationControl
|
||||
outputObject.Spawn();
|
||||
|
||||
currentItem = outputObject.GetComponent<NetworkItem>();
|
||||
if (currentItem == null ||
|
||||
if (currentItem == null || currentItem.Definition != recipe.OutputItem ||
|
||||
!WorkstationItemControlUtility.TryGetControl(currentItem, out _))
|
||||
{
|
||||
string actualDefinition = currentItem != null && currentItem.Definition != null
|
||||
? currentItem.Definition.name
|
||||
: "None";
|
||||
Debug.LogError(
|
||||
$"{name} cannot produce '{recipe.OutputItem.DisplayName}': " +
|
||||
"the output prefab must contain NetworkItem and a supported " +
|
||||
"the output prefab must contain NetworkItem with the same " +
|
||||
$"definition (actual: '{actualDefinition}') and a supported " +
|
||||
"Carry or Drag interaction component.",
|
||||
this);
|
||||
outputObject.Despawn(true);
|
||||
@@ -292,6 +296,7 @@ public sealed class NetworkItemProcessingWorkstation : NetworkWorkstationControl
|
||||
|
||||
private static bool IsRecipeValid(ItemProcessingRecipe recipe) =>
|
||||
recipe != null && recipe.InputItem != null &&
|
||||
recipe.OutputItem != null && recipe.OutputItem.WorldPrefab != null;
|
||||
recipe.OutputItem != null && recipe.OutputItem.WorldPrefab != null &&
|
||||
recipe.TryValidateOutputPrefab(out _);
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user