using MegaKoop.Game.Networking; using Steamworks; using UnityEngine; namespace MegaKoop.Game.Vfx { [DefaultExecutionOrder(-50)] public class ProjectileImpactVfxService : MonoBehaviour { private const float MinLifetime = 0.5f; private static ProjectileImpactVfxService instance; private SteamCoopNetworkManager networkManager; private bool isRegistered; private ProjectileImpactVfxSettings settings; public static ProjectileImpactVfxService Instance { get { if (instance == null) { instance = FindObjectOfType(); if (instance == null) { var go = new GameObject(nameof(ProjectileImpactVfxService)); instance = go.AddComponent(); } } return instance; } } [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] private static void AutoCreate() { _ = Instance; } public static void ReportImpact(ProjectileImpactKind kind, Vector3 position, Vector3 normal) { Instance.InternalReportImpact(kind, position, normal); } private void Awake() { if (instance != null && instance != this) { Destroy(gameObject); return; } instance = this; DontDestroyOnLoad(gameObject); LoadSettings(); } private void OnEnable() { TryBindNetworkManager(); } private void Update() { TryBindNetworkManager(); } private void OnDisable() { UnregisterHandler(); } private void OnDestroy() { if (instance == this) { instance = null; } UnregisterHandler(); } private void LoadSettings() { if (settings != null) { return; } settings = ProjectileImpactVfxSettings.LoadFromResources(); if (settings == null) { Debug.LogWarning($"[ProjectileImpactVfxService] Could not locate Resources/{ProjectileImpactVfxSettings.ResourcePath}. Impact particles will be skipped."); } } private void TryBindNetworkManager() { var current = SteamCoopNetworkManager.Instance; if (current != networkManager) { UnregisterHandler(); networkManager = current; } if (networkManager != null && !isRegistered) { networkManager.RegisterHandler(NetworkMessageType.ProjectileImpact, HandleImpactMessage); isRegistered = true; } } private void UnregisterHandler() { if (isRegistered && networkManager != null) { networkManager.UnregisterHandler(NetworkMessageType.ProjectileImpact, HandleImpactMessage); } isRegistered = false; } private void HandleImpactMessage(NetworkMessage message) { ProjectileImpactMessage impact = ProjectileImpactMessage.Deserialize(message.Payload); SpawnImpact(impact.Kind, impact.Position, impact.Normal); } private void InternalReportImpact(ProjectileImpactKind kind, Vector3 position, Vector3 normal) { SpawnImpact(kind, position, normal); TryBindNetworkManager(); if (networkManager == null || !networkManager.IsHost) { return; } var impactMessage = new ProjectileImpactMessage(kind, position, normal); byte[] payload = ProjectileImpactMessage.Serialize(impactMessage); networkManager.SendToAll(NetworkMessageType.ProjectileImpact, payload, EP2PSend.k_EP2PSendUnreliableNoDelay); } private void SpawnImpact(ProjectileImpactKind kind, Vector3 position, Vector3 normal) { LoadSettings(); if (settings == null) { return; } GameObject prefab = kind switch { ProjectileImpactKind.Hero => settings.HeroImpactPrefab, ProjectileImpactKind.Enemy => settings.EnemyImpactPrefab, _ => null }; if (prefab == null) { return; } Quaternion rotation = normal.sqrMagnitude > 0.001f ? Quaternion.LookRotation(normal) : Quaternion.identity; GameObject instance = Instantiate(prefab, position, rotation); float lifetime = EstimateLifetime(instance); if (lifetime > 0f) { Destroy(instance, lifetime); } } private float EstimateLifetime(GameObject effectInstance) { float fallback = settings != null ? settings.FallbackLifetime : 4f; ParticleSystem[] particleSystems = effectInstance.GetComponentsInChildren(); if (particleSystems == null || particleSystems.Length == 0) { return Mathf.Max(MinLifetime, fallback); } float maxLifetime = fallback; foreach (ParticleSystem system in particleSystems) { var main = system.main; if (main.loop) { maxLifetime = Mathf.Max(maxLifetime, fallback); continue; } float candidate = main.duration + main.startLifetime.constantMax; if (candidate <= 0f) { candidate = fallback; } maxLifetime = Mathf.Max(maxLifetime, candidate); } return Mathf.Max(MinLifetime, maxLifetime); } } }