// --------------------------------------------------------------------------------------------------------------------
//
// Copyright (c) VRMADA, All rights reserved.
//
// --------------------------------------------------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using UltimateXR.Core;
using UltimateXR.Core.Components.Singleton;
using UltimateXR.Core.Settings;
using UnityEngine;
namespace UltimateXR.Mechanics.Weapons
{
///
/// Singleton manager in charge of updating projectiles, computing hits against entities and damage done on
/// components.
///
public partial class UxrWeaponManager : UxrSingleton
{
#region Public Types & Data
///
/// Event triggered right before an is about to receive damage.
/// Setting will allow not to take the damage.
///
public event EventHandler DamageReceiving;
///
/// Event triggered right after the actor received damage.
/// Setting is not supported, since the damage was already taken.
///
public event EventHandler DamageReceived;
///
/// Event called whenever there was a projectile impact but no was involved. Mostly hits
/// against the scenario that still generate decals, FX, etc.
///
public event EventHandler NonActorImpacted;
#endregion
#region Public Methods
///
/// Updates the manager.
///
public void UpdateManager()
{
UpdateProjectiles();
}
///
/// Registers a new projectile shot so that it gets automatically update by the manager from that moment until it hits
/// something or gets destroyed.
///
/// Projectile source
/// Shot descriptor
/// World position
/// World orientation. The projectile will travel in the forward (z) direction
public void RegisterNewProjectileShot(UxrProjectileSource projectileSource, UxrShotDescriptor shotDescriptor, Vector3 position, Quaternion orientation)
{
_projectiles.Add(new ProjectileInfo(projectileSource.TryGetWeaponOwner(), projectileSource, shotDescriptor, position, orientation));
}
///
/// Applies radius damage to all elements around a source position.
///
/// The actor that was responsible for the damage or null if there wasn't any
/// Explosion world position
/// Radius
/// Damage at the very same point of the explosion
/// Damage at the distance set by
public void ApplyRadiusDamage(UxrActor actorSource, Vector3 position, float radius, float nearDamage, float farDamage)
{
foreach (KeyValuePair damageActorPair in _damageActors)
{
float distance = Vector3.Distance(damageActorPair.Value.Transform.position, position);
if (distance < radius)
{
float damage = Mathf.Lerp(nearDamage, farDamage, distance / radius);
damageActorPair.Key.ReceiveExplosion(actorSource, position, damage);
}
}
}
#endregion
#region Unity
///
/// Initializes the manager. Subscribes to actor enable/disable events.
///
protected override void Awake()
{
base.Awake();
UxrActor.GlobalEnabled += Actor_Enabled;
UxrActor.GlobalDisabled += Actor_Disabled;
}
///
/// Initializes the manager. Unsubscribes from actor enable/disable events.
///
protected override void OnDestroy()
{
base.OnDestroy();
UxrActor.GlobalEnabled -= Actor_Enabled;
UxrActor.GlobalDisabled -= Actor_Disabled;
}
#endregion
#region Event Handling Methods
///
/// Called whenever an was enabled. Create a new entry in the internal dictionary.
///
/// Actor that was enabled
private void Actor_Enabled(UxrActor actor)
{
if (_damageActors.ContainsKey(actor) == false)
{
_damageActors.Add(actor, new ActorInfo(actor));
actor.DamageReceiving += Actor_DamageReceiving;
actor.DamageReceived += Actor_DamageReceived;
}
}
///
/// Called whenever an was disabled. Remove the entry from the internal dictionary.
///
/// Actor that was disabled
private void Actor_Disabled(UxrActor actor)
{
if (_damageActors.ContainsKey(actor))
{
_damageActors.Remove(actor);
actor.DamageReceiving -= Actor_DamageReceiving;
actor.DamageReceived -= Actor_DamageReceived;
}
}
///
/// Called whenever an actor is about to receive any damage. The damage can be canceled using
/// .
///
/// Event sender
/// Event parameters
private void Actor_DamageReceiving(object sender, UxrDamageEventArgs e)
{
OnDamageReceiving(e);
}
///
/// Called right after an actor received any damage. Damage can not be canceled using
/// because it was already taken.
///
/// Event sender
/// Event parameters
private void Actor_DamageReceived(object sender, UxrDamageEventArgs e)
{
OnDamageReceived(e);
}
#endregion
#region Event Trigger Methods
///
/// Event trigger for .
///
/// Event parameters
private void OnDamageReceiving(UxrDamageEventArgs e)
{
DamageReceiving?.Invoke(this, e);
}
///
/// Event trigger for .
///
/// Event parameters
private void OnDamageReceived(UxrDamageEventArgs e)
{
if (UxrGlobalSettings.Instance.LogLevelWeapons >= UxrLogLevel.Relevant)
{
string sourceInfo = e.ActorSource != null ? $" from actor {e.ActorSource.name}." : string.Empty;
if (UxrGlobalSettings.Instance.LogLevelWeapons >= UxrLogLevel.Relevant)
{
Debug.Log($"{UxrConstants.WeaponsModule}: Actor {e.ActorTarget.name} received {e.Damage} damage of type {e.DamageType}{sourceInfo}. Is dead? {e.Dies}.");
}
}
DamageReceived?.Invoke(this, e);
}
#endregion
#region Private Methods
///
/// Updates all current projectiles.
///
private void UpdateProjectiles()
{
for (int i = 0; i < _projectiles.Count; ++i)
{
if (_projectiles[i].Projectile == null)
{
// Dynamic instance removed somewhere
_projectiles.RemoveAt(i);
i--;
continue;
}
if (_projectiles[i].FirstFrame)
{
// Render first frame where the weapon is
_projectiles[i].FirstFrame = false;
continue;
}
Vector3 oldPos = _projectiles[i].ProjectileLastPosition;
Vector3 newPos = _projectiles[i].Projectile.transform.position;
Vector3 projectileForward = _projectiles[i].Projectile.transform.forward;
if (_projectiles[i].ShotDescriptor.UseAutomaticProjectileTrajectory)
{
newPos = oldPos + (_projectiles[i].ProjectileSpeed * Time.deltaTime * projectileForward);
_projectiles[i].Projectile.transform.position = newPos;
}
_projectiles[i].ProjectileLastPosition = newPos;
_projectiles[i].ProjectileDistanceTravelled += Vector3.Distance(oldPos, newPos);
if (_projectiles[i].ProjectileDistanceTravelled >= _projectiles[i].ShotDescriptor.ProjectileMaxDistance)
{
// Max distance travelled
Destroy(_projectiles[i].Projectile);
_projectiles.RemoveAt(i);
i--;
continue;
}
if (_projectiles[i].ShotDescriptor.UseAutomaticProjectileTrajectory)
{
float rayLength = Vector3.Distance(oldPos, newPos + projectileForward * _projectiles[i].ShotDescriptor.ProjectileLength);
if (Physics.Raycast(oldPos, projectileForward, out RaycastHit raycastHit, rayLength, _projectiles[i].ShotLayerMask, QueryTriggerInteraction.Ignore))
{
UxrProjectileDeflect projectileDeflect = raycastHit.collider.GetComponentInParent();
// Has UxrProjectileDeflect component?
if (projectileDeflect != null)
{
Vector3 newForward = Vector3.Reflect(projectileForward, raycastHit.normal).normalized;
_projectiles[i].Projectile.transform.SetPositionAndRotation(raycastHit.point + newForward * 0.05f, Quaternion.LookRotation(newForward)); // + rayLength - raycastHit.distance));
_projectiles[i].ProjectileDeflectSource = projectileDeflect;
projectileDeflect.AudioDeflect?.Play(raycastHit.point);
if (projectileDeflect.DecalOnReflect != null)
{
// Generate decal?
UxrImpactDecal.CheckCreateDecal(raycastHit,
-1,
projectileDeflect.DecalOnReflect,
projectileDeflect.DecalLife,
projectileDeflect.DecalFadeoutDuration,
projectileDeflect.TwoSidedDecal,
projectileDeflect.TwoSidedDecalThickness);
}
_projectiles[i].AddShotLayerMask(projectileDeflect.CollideLayersAddOnReflect.value);
_projectiles[i].ProjectileLastPosition = _projectiles[i].Projectile.transform.position;
projectileDeflect.RaiseProjectileDeflected(new UxrDeflectEventArgs(_projectiles[i].ProjectileSource, raycastHit, newForward));
}
else
{
UxrActor targetActor = raycastHit.collider.GetComponentInParent();
if (targetActor != null)
{
// Impact with an actor.
float normalizedDistance = _projectiles[i].ProjectileDistanceTravelled / _projectiles[i].ShotDescriptor.ProjectileMaxDistance;
float damage = Mathf.Lerp(_projectiles[i].ShotDescriptor.ProjectileDamageNear, _projectiles[i].ShotDescriptor.ProjectileDamageFar, normalizedDistance);
if (_projectiles[i].ProjectileDeflectSource != null)
{
// Came from a shot deflected by a UxrProjectileDeflect
targetActor.ReceiveImpact(_projectiles[i].ProjectileDeflectSource.Owner, raycastHit, damage);
}
else
{
// Direct hit from a projectile
targetActor.ReceiveImpact(_projectiles[i].ProjectileSource.TryGetWeaponOwner(), raycastHit, damage);
}
}
else
{
// Impact with something that isn't an actor, probably the scenario.
if (_projectiles[i].ShotDescriptor.PrefabInstantiateOnImpact)
{
Vector3 instantiateForward = Vector3.Reflect(projectileForward, raycastHit.normal).normalized;
GameObject newInstance = Instantiate(_projectiles[i].ShotDescriptor.PrefabInstantiateOnImpact, raycastHit.point, Quaternion.LookRotation(instantiateForward));
if (_projectiles[i].ShotDescriptor.PrefabInstantiateOnImpactLife >= 0.0f)
{
Destroy(newInstance, _projectiles[i].ShotDescriptor.PrefabInstantiateOnImpactLife);
}
}
Rigidbody rigidbody = raycastHit.collider.GetComponentInParent();
if (rigidbody != null)
{
rigidbody.AddForceAtPosition(_projectiles[i].ProjectileSpeed * _projectiles[i].ShotDescriptor.ProjectileImpactForceMultiplier * projectileForward, raycastHit.transform.position);
}
UxrOverrideImpactDecal overrideDecal = raycastHit.collider.GetComponentInParent();
// Generate decal?
UxrImpactDecal.CheckCreateDecal(raycastHit,
_projectiles[i].ShotDescriptor.CreateDecalLayerMask,
overrideDecal != null ? overrideDecal.DecalToUse : _projectiles[i].ShotDescriptor.PrefabScenarioImpactDecal,
_projectiles[i].ShotDescriptor.PrefabScenarioImpactDecalLife,
_projectiles[i].ShotDescriptor.DecalFadeoutDuration);
NonActorImpacted?.Invoke(_projectiles[i].ProjectileSource, new UxrNonDamagingImpactEventArgs(_projectiles[i].ProjectileSource.TryGetWeaponOwner(), _projectiles[i].ProjectileSource, raycastHit));
}
Destroy(_projectiles[i].Projectile);
_projectiles.RemoveAt(i);
i--;
}
}
}
}
}
#endregion
#region Private Types & Data
private readonly Dictionary _damageActors = new Dictionary();
private readonly List _projectiles = new List();
#endregion
}
}