using System.Linq; using SanAndreasUnity.Behaviours.Audio; using SanAndreasUnity.Net; using SanAndreasUnity.Utilities; using UnityEngine; namespace SanAndreasUnity.Behaviours.Vehicles { public partial class Vehicle { public Damageable Damageable { get; private set; } public float Health { get; set; } = 1000; public float MaxHealth { get; set; } = 1000; public bool IsUnderFlame { get; private set; } = false; public bool IsUnderSmoke { get; private set; } = false; bool m_alreadyExploded = false; public bool ExplodedThisFrame => m_alreadyExploded; public float TimeWhenBecameUnderFlame { get; private set; } = float.NegativeInfinity; public float TimeSinceBecameUnderFlame => Time.time - this.TimeWhenBecameUnderFlame; GameObject m_smokeGameObject; GameObject m_flameGameObject; public static AudioClip ExplosionSound { get; private set; } void Awake_Damage() { } void SetupDamagable() { this.Damageable = this.HighDetailMeshesParent.gameObject.AddComponent(); this.Damageable.OnDamageEvent.AddListener(() => this.OnDamaged()); } void OnDamaged() { if (!NetStatus.IsServer) return; var damageInfo = this.Damageable.LastDamageInfo; if (this.Health <= 0) return; this.Health -= damageInfo.amount; if (this.Health <= 0) { this.Explode(); } } void Update_Damage() { bool shouldBeUnderSmoke = this.MaxHealth * 0.33f >= this.Health; if (shouldBeUnderSmoke != this.IsUnderSmoke) { // smoke status changed this.IsUnderSmoke = shouldBeUnderSmoke; // update vfx this.UpdateSmokeVfx(); } bool shouldBeUnderFlame = this.MaxHealth * 0.1f >= this.Health; if (shouldBeUnderFlame != this.IsUnderFlame) { // flame status changed this.IsUnderFlame = shouldBeUnderFlame; if (this.IsUnderFlame) this.TimeWhenBecameUnderFlame = Time.time; // update vfx this.UpdateFlameVfx(); } if (this.IsUnderFlame && Time.time - this.TimeWhenBecameUnderFlame >= 5) { // enough time passed since vehicle flamed - explode it if (NetStatus.IsServer) { this.Explode(); } } } void UpdateSmokeVfx() { if (this.IsUnderSmoke) { if (null == m_smokeGameObject) { Transform parent = this.EngineTransform != null ? this.EngineTransform : this.transform; m_smokeGameObject = Object.Instantiate( VehicleManager.Instance.smokePrefab, parent.position, parent.rotation, parent); } } else { if (null != m_smokeGameObject) { Object.Destroy(m_smokeGameObject); m_smokeGameObject = null; } } } void UpdateFlameVfx() { if (this.IsUnderFlame) { if (null == m_flameGameObject) { Transform parent = this.EngineTransform != null ? this.EngineTransform : this.transform; m_flameGameObject = Object.Instantiate( VehicleManager.Instance.flamePrefab, parent.position, parent.rotation, parent); } } else { if (null != m_flameGameObject) { Object.Destroy(m_flameGameObject); m_flameGameObject = null; } } } public void Explode() { F.RunExceptionSafe(() => this.ExplodeInternal()); } private void ExplodeInternal() { if (m_alreadyExploded) return; m_alreadyExploded = true; // destroy this game object before doing anything else Object.Destroy(this.gameObject); // detach vehicle parts string[] startingNames = new string[] { "door_", "wheel_", "bonnet_", "boot_", "windscreen_", "exhaust_" }; foreach (var frame in _frames) { if (!frame.gameObject.activeInHierarchy) continue; if (!startingNames.Any(n => frame.gameObject.name.StartsWith(n))) continue; DetachFrameDuringExplosion(frame, VehicleManager.Instance.explosionLeftoverPartsMass, null); } // chassis need to be handled after all other objects are detached, because chassis can sometimes // have other objects as children Frame chassisFrame = _frames.FirstOrDefault(f => f.Name == "chassis"); if (null == chassisFrame) { Debug.LogError($"Chassis object not found on vehicle {this.DescriptionForLogging}"); } else { DetachFrameDuringExplosion(chassisFrame, this.HandlingData.Mass * 0.8f, null); } // inflict damage to nearby objects Damageable.InflictDamageToObjectsInArea( this.transform.position, VehicleManager.Instance.explosionDamageRadius, Mathf.Pow(this.HandlingData.Mass, VehicleManager.Instance.explosionMassToDamageExponent), VehicleManager.Instance.explosionDamageOverDistanceCurve, DamageType.Explosion); // create explosion - this includes effects, physics force, sound GameObject explosionGo = Object.Instantiate(VehicleManager.Instance.explosionPrefab, this.transform.position, this.transform.rotation); if (NetStatus.IsServer) NetManager.Spawn(explosionGo); // modify strength of explosion based on vehicle mass float forceFactor = Mathf.Sqrt(this.HandlingData.Mass) / Mathf.Sqrt(1500f); var physicsForce = explosionGo.GetComponentOrThrow(); physicsForce.explosionForce *= forceFactor; physicsForce.upwardsModifier *= forceFactor; // assign explosion sound F.RunExceptionSafe(() => AssignExplosionSound(explosionGo)); // kill all peds inside foreach (Ped ped in this.Seats .Select(s => s.OccupyingPed) .Where(p => p != null) .ToList()) { ped.Kill(); } } public void DetachFrameDuringExplosion(string frameName, float mass, GameObject parentGo) { Frame frame = this.Frames.FirstOrDefault(f => f.gameObject.name == frameName); if (null == frame) { Debug.LogError($"Failed to find frame by name: {frameName}"); } else { DetachFrameDuringExplosion(frame, mass, parentGo); } } void DetachFrameDuringExplosion(Frame frame, float mass, GameObject parentGo) { DetachFrameFromTransformDuringExplosion(this.transform, frame, mass, parentGo, this.NetIdentity.netId, this.Definition.Id, this.Colors); } public static void DetachFrameFromTransformDuringExplosion(Transform tr, Frame frame, float mass, GameObject parentGo, uint vehicleNetId, int vehicleModelId, int[] vehicleColors) { if (! tr.IsParentOf(frame.transform)) // already detached ? return; var meshFilter = frame.GetComponentInChildren(); if (null == meshFilter) return; if (!meshFilter.gameObject.activeInHierarchy) return; if (null == parentGo) parentGo = Object.Instantiate(VehicleManager.Instance.explosionLeftoverPartPrefab, meshFilter.transform.position, meshFilter.transform.rotation); parentGo.name = "vehicle_part_" + meshFilter.gameObject.name; meshFilter.transform.SetParent(parentGo.transform, true); meshFilter.gameObject.layer = UnityEngine.LayerMask.NameToLayer("Default"); var meshCollider = meshFilter.gameObject.GetOrAddComponent(); meshCollider.cookingOptions = MeshColliderCookingOptions.None; meshCollider.convex = true; meshCollider.sharedMesh = meshFilter.sharedMesh; var rigidBody = meshFilter.gameObject.GetOrAddComponent(); rigidBody.mass = mass; rigidBody.drag = 0.05f; rigidBody.maxDepenetrationVelocity = VehicleManager.Instance.explosionLeftoverPartsMaxDepenetrationVelocity; rigidBody.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic; if (NetStatus.IsServer) { Object.Destroy(parentGo, VehicleManager.Instance.explosionLeftoverPartsLifetime * Random.Range(0.8f, 1.2f)); var netScript = parentGo.GetComponentOrThrow(); netScript.InitializeOnServer(vehicleNetId, vehicleModelId, vehicleColors, frame.gameObject.name, mass, rigidBody); NetManager.Spawn(parentGo); } } public static void AssignExplosionSound(GameObject explosionGo) { if (null == ExplosionSound) ExplosionSound = AudioManager.CreateAudioClipFromSfx("GENRL", 45, 1); var audioSource = explosionGo.GetComponentOrThrow(); audioSource.clip = ExplosionSound; audioSource.Play(); } void OnDrawGizmosSelected() { // draw sphere indicating explosion damage radius Gizmos.color = Color.red; Gizmos.DrawWireSphere(this.transform.position, VehicleManager.Instance.explosionDamageRadius); } } }