using System.Collections.Generic;
using UnityEngine;
// Sci-Fi Ship Controller. Copyright (c) 2018-2023 SCSM Pty Ltd. All rights reserved.
namespace SciFiShipController
{
///
/// This module can be used to trigger a DestructModule when the health of the object reaches 0.
/// For a simplified solution, see SSCTargetable.
/// IMPORTANT: If you want a ship, a ship’s damage regions, or a surface turret to take damage,
/// use the features included in the Ship Control Module or the Surface Turret Module. This
/// module is to be used with regular gameobjects that don’t include those components.
/// Examples could include buildings or destructible props.
///
[AddComponentMenu("Sci-Fi Ship Controller/Object Components/Destructive Object Module")]
[DisallowMultipleComponent]
[RequireComponent(typeof(SciFiShipController.DamageReceiver))]
[HelpURL("http://scsmmedia.com/ssc-documentation")]
public class DestructibleObjectModule : MonoBehaviour
{
#region Public Variables
public bool initialiseOnStart = false;
///
/// How much health the object has initially
///
public float startingHealth = 100f;
///
/// Whether this object uses shielding. Up until a point, shielding protects the object from damage
///
public bool useShielding = false;
///
/// Damage below this value will not affect the shield or the object's health while the shield is still active
/// (i.e. until the shield has absorbed damage more than or equal to the shieldingAmount value from damage events above the
/// damage threshold). Only relevant if useShielding is enabled.
///
public float shieldingDamageThreshold = 10f;
///
/// How much damage the shield can absorb before it ceases to protect the object from damage. Only relevant if
/// useShielding is enabled.
///
public float shieldingAmount = 100f;
///
/// When useShielding is true, this is the rate per second that a shield will recharge (default = 0)
///
[Range(0f, 100f)] public float shieldingRechargeRate = 0f;
///
/// When useShielding is true, and shieldingRechargeRate is greater than 0, this is the delay, in seconds,
/// between when damage occurs to a shield and it begins to recharge.
///
[Range(0f, 300f)] public float shieldingRechargeDelay = 10f;
///
/// The sound or particle FX used when object is destroyed
/// If you modify this, call ReinitialiseEffects().
///
public EffectsModule destructionEffectsObject;
///
/// The offset in the forward direction, from the objects gameobject, that the destruction effect is instantiated.
///
public Vector3 destructionEffectsOffset = Vector3.zero;
///
/// This is used when you want pre-build fragments of the object to explode out from the object position when it is destroyed.
/// If you modify this, call ReinitialiseDestructObjects().
///
public DestructModule destructObject = null;
///
/// The offset in the forward direction, from the objects gameobject, that the destruct module is instantiated.
///
public Vector3 destructObjectOffset = Vector3.zero;
///
/// The relative size of the blip on the radar mini-map
/// Must be between 1 and 5 inclusive.
///
[Range(1, 5)] public byte radarBlipSize = 1;
#endregion
#region Public Properties
///
/// The current health value of this object.
///
public float Health
{
get { return health; }
set
{
// Update the health value
health = value < 0f ? 0f : value;
if (isInitialised && health == 0f)
{
DestructObject();
}
}
}
///
/// [READONLY]
/// Normalised (0.0 – 1.0) value of the health of the object.
///
public float HealthNormalised
{
get
{
float _healthN = startingHealth == 0f ? 0f : health / startingHealth;
if (_healthN > 1f) { return 1f; }
else if (_healthN < 0f) { return 0f; }
else { return _healthN; }
}
}
///
/// [READONLY] Has the module been initialised?
///
public bool IsInitialised { get { return isInitialised; } }
///
/// [READONLY] The number used by the SSCRadar system to identify this object at a point in time.
/// This should not be stored across frames and is updated as required by the system.
///
public int RadarId { get { return radarItemIndex; } }
///
/// The current health value of this object's shield.
/// When a shield is destroyed, its value is set to -0.01.
///
public float ShieldHealth
{
get { return shieldHealth; }
set
{
// Update the health value
shieldHealth = value;
}
}
///
/// [READONLY]
/// Normalised (0.0 – 1.0) value of the shield for this object. If useShielding is false, it will
/// always return 0.
///
public float ShieldNormalised
{
get
{
float _shieldN = !useShielding || shieldingAmount == 0f ? 0f : shieldHealth / shieldingAmount;
if (_shieldN > 1f) { return 1f; }
else if (_shieldN < 0f) { return 0f; }
else { return _shieldN; }
}
}
#endregion
#region Private or Internal variables
private bool isInitialised = false;
private DamageReceiver damageReceiver = null;
private float health;
private float shieldHealth;
///
/// Whether damage type multipliers are used when calculating damage from projectiles.
///
[SerializeField] private bool useDamageMultipliers = false;
///
/// The array of damage multipliers for this object.
///
[SerializeField] private float[] damageMultipliersArray;
///
/// [INTERNAL USE ONLY] Instead, call EnableRadar() or DisableRadar().
///
[SerializeField] private bool isRadarEnabled = false;
///
/// The faction or alliance the object belongs to. This can be used to identify if a object is friend or foe. Neutral = 0.
///
[SerializeField] private int factionId = 0;
///
/// Although normally representing a squadron of ships, this can be used on a gameobjects to group it with other things in your scene
///
[SerializeField] private int squadronId = -1;
///
/// This identifies the destructionEffectsObject instance that may have been instantiated.
///
[System.NonSerialized] internal SSCEffectItemKey destructionEffectItemKey;
///
/// The ID number for this damage region's destruct prefab (as assigned by the SSCManager in the scene).
/// This is the index in the SSCManager destructTemplateList.
/// [INTERNAL USE ONLY]
///
[System.NonSerialized] internal int destructObjectPrefabID;
///
/// Flag for whether the destruct object has been activated.
/// [INTERNAL USE ONLY]
///
[System.NonSerialized] internal bool isDestructObjectActivated;
///
/// This identifies the destruct object instance that may have been instantiated.
///
[System.NonSerialized] internal SSCDestructItemKey destructItemKey;
///
/// [INTERNAL USE ONLY]
/// Flag for when the object destroy "event" has been actioned after health reaches 0
///
[System.NonSerialized] internal bool isDestroyed = false;
[System.NonSerialized] private SSCManager sscManager = null;
// Radar variables
[System.NonSerialized] private SSCRadar sscRadar = null;
[System.NonSerialized] internal int radarItemIndex = -1;
//[System.NonSerialized] internal SSCRadarPacket sscRadarPacket;
///
/// [INTERNAL USE ONLY]
///
[System.NonSerialized] internal float shieldRechargeDelayTimer;
///
/// The ID number for this object's destruction effects object prefab (as assigned by the Ship Controller Manager in the scene).
/// This is the index in the SSCManager effectsObjectTemplatesList.
/// [INTERNAL USE ONLY]
///
[System.NonSerialized] public int effectsObjectPrefabID;
///
/// Flag for whether the destruction effects object has been instantiated.
/// [INTERNAL USE ONLY]
///
[System.NonSerialized] public bool isDestructionEffectsObjectInstantiated;
#endregion
#region Initialise methods
// Start is called before the first frame update
void Start()
{
if (initialiseOnStart) { Initialise(); }
}
#endregion
#region Update Methods
private void Update()
{
if (isInitialised)
{
CheckShieldRecharge();
}
}
#endregion
#region Private Methods
///
/// Add or remove this gameobject from radar.
///
///
private void EnableOrDisableRadar(bool isEnabled)
{
if (isEnabled)
{
if (isInitialised && radarItemIndex == -1)
{
// Create as a RadarItemType.GameObject
if (sscRadar == null) { sscRadar = SSCRadar.GetOrCreateRadar(); }
if (sscRadar != null) { radarItemIndex = sscRadar.EnableRadar(gameObject, transform.position, factionId, squadronId, 0, radarBlipSize); }
isRadarEnabled = radarItemIndex >= 0;
}
}
else
{
if (isInitialised && isRadarEnabled && radarItemIndex >= 0)
{
sscRadar.DisableRadar(radarItemIndex);
}
isRadarEnabled = false;
}
}
///
/// Recharge the shield if required
///
private void CheckShieldRecharge()
{
// Is shield recharging enabled?
if (health > 0 && useShielding && shieldingRechargeRate > 0)
{
// Can the shield be recharged?
if (shieldingRechargeDelay > shieldRechargeDelayTimer)
{
shieldRechargeDelayTimer += Time.deltaTime;
}
else
{
shieldHealth += shieldingRechargeRate * Time.deltaTime;
if (shieldHealth > shieldingAmount) { shieldHealth = shieldingAmount; }
}
}
}
#endregion
#region Public API Methods
///
/// Apply damage to the object. If Use Damage Multipliers is enabled, you can optionally pass in the DamageTye.
///
///
///
public void ApplyDamage (float damageAmount, ProjectileModule.DamageType damageType = ProjectileModule.DamageType.Default)
{
float actualDamage = damageAmount;
bool objectDamaged = true;
// Modify damage dealt based on relevant damage multipliers
if (useDamageMultipliers)
{
actualDamage *= GetDamageMultiplier(damageType);
}
// Determine whether shielding is active for this object
if (useShielding && shieldHealth > 0f)
{
// Set the shielding to active
objectDamaged = false;
// Only do damage to the shield if the damage amount is above the shielding threshold
if (actualDamage >= shieldingDamageThreshold)
{
shieldHealth -= actualDamage;
shieldRechargeDelayTimer = 0f;
// If this damage destroys the shielding entirely...
if (shieldHealth <= 0f)
{
// Get the residual damage value
actualDamage = -shieldHealth;
// Set the shielding to inactive
objectDamaged = true;
shieldHealth = -0.01f;
}
}
}
if (objectDamaged)
{
// Reduce health of object itself
Health -= actualDamage;
}
}
///
/// This routine is called by our damage receiver when a projectile or beam hits our object
///
///
public void ApplyDamage (CallbackOnObjectHitParameters callbackOnObjectHitParameters)
{
if (callbackOnObjectHitParameters.hitInfo.transform != null)
{
ProjectileModule projectile = callbackOnObjectHitParameters.projectilePrefab;
if (projectile != null)
{
ApplyDamage(projectile.damageAmount, projectile.damageType);
}
// Should have been a beam weapon that fired at the object
else
{
BeamModule beam = callbackOnObjectHitParameters.beamPrefab;
if (beam != null)
{
ApplyDamage(callbackOnObjectHitParameters.damageAmount, beam.damageType);
}
else
{
// if we don't need to know what hit the object, simply reduce the health
ApplyDamage(callbackOnObjectHitParameters.damageAmount);
}
}
}
}
///
/// Add health to the object. If isAffectShield is true, and the health reaches the maximum
/// configured, excess health will be applied to the shield.
/// To incur damage use the ApplyDamage(..).
///
///
///
public void AddHealth (float healthAmount, bool isAffectShield)
{
if (healthAmount > 0f)
{
if (health < 0f) { health = healthAmount; }
else { health += healthAmount; }
if (health > startingHealth)
{
if (isAffectShield && useShielding)
{
float newShieldHealth = 0f;
// When shielding is -ve (e.g. -0.01 when it has been used up) set the shielding amount rather than adding it
if (shieldHealth < 0f) { newShieldHealth = health - startingHealth; }
else { newShieldHealth = shieldHealth + health - startingHealth; }
// Cap shielding to maximum permitted.
if (newShieldHealth > shieldingAmount) { newShieldHealth = shieldingAmount; }
shieldHealth = newShieldHealth;
}
// Cap health to maximum permitted
health = startingHealth;
}
}
}
///
/// Disable radar for this object. If you want to change the visibility to other radar consumers,
/// consider calling SetRadarVisibility(..) rather than disabling the radar and (later)
/// calling EnableRadar() again. This will be automatically called by DestructObject().
///
public void DisableRadar()
{
EnableOrDisableRadar(false);
}
// Enable radar for this object. It will be visible on radar to others in the scene.
public void EnableRadar()
{
EnableOrDisableRadar(true);
}
///
/// Returns the damage multiplier for damageType.
///
///
///
public float GetDamageMultiplier (ProjectileModule.DamageType damageType)
{
switch ((int)damageType)
{
// Hardcoded int values for performance
// Default = 0
case 0: return 1f;
// Type A = 100
case 100: return damageMultipliersArray[0];
// Type B = 105
case 105: return damageMultipliersArray[1];
// Type C = 110
case 110: return damageMultipliersArray[2];
// Type D = 115
case 115: return damageMultipliersArray[3];
// Type E = 120
case 120: return damageMultipliersArray[4];
// Type F = 125
case 125: return damageMultipliersArray[5];
// Default case
default: return 1f;
}
}
///
/// Reinitialises variables required for Destruct Module.
/// Call after modifying the destructObject.
///
public void ReinitialiseDestructObjects()
{
if (sscManager == null) { sscManager = SSCManager.GetOrCreateManager(); }
if (sscManager != null)
{
// Initialise destruct modules
sscManager.UpdateDestructObjects(this);
}
#if UNITY_EDITOR
else
{
Debug.LogWarning("DestructibleObjectModule.ReinitialiseDestructObjects Warning: could not find SSCManager to update effects.");
}
#endif
}
///
/// Reinitialises variables required for effects of the Destructible Object Module.
/// Call after modifying any effect data for this module.
///
public void ReinitialiseEffects()
{
if (sscManager == null) { sscManager = SSCManager.GetOrCreateManager(); }
if (sscManager != null)
{
// Initialise effects objects
sscManager.UpdateEffects(this);
}
#if UNITY_EDITOR
else
{
Debug.LogWarning("DestructibleObjectModule.ReinitialiseEffects Warning: could not find SSCManager to update effects.");
}
#endif
}
///
/// Reset the health of the object back to initial values
///
public void ResetHealth()
{
// Reset health value for the object
health = startingHealth;
// Reset shield health value for the object
shieldHealth = shieldingAmount;
shieldRechargeDelayTimer = 0f;
isDestructObjectActivated = false;
isDestroyed = false;
}
///
/// Sets the damage multiplier for damageType to damageMultiplier.
///
///
///
public void SetDamageMultiplier (ProjectileModule.DamageType damageType, float damageMultiplier)
{
switch ((int)damageType)
{
// Hardcoded int values for performance
// Type A = 100
case 100: damageMultipliersArray[0] = damageMultiplier; break;
// Type B = 105
case 105: damageMultipliersArray[1] = damageMultiplier; break;
// Type C = 110
case 110: damageMultipliersArray[2] = damageMultiplier; break;
// Type D = 115
case 115: damageMultipliersArray[3] = damageMultiplier; break;
// Type E = 120
case 120: damageMultipliersArray[4] = damageMultiplier; break;
// Type F = 125
case 125: damageMultipliersArray[5] = damageMultiplier; break;
}
}
///
/// If radar is enabled for this object, set its visibility to radar.
///
///
public void SetRadarVisibility (bool isVisible)
{
if (isInitialised && isRadarEnabled && radarItemIndex >= 0)
{
sscRadar.SetVisibility(radarItemIndex, isVisible);
}
}
///
/// Set the Squadron Id for the object. If radar is enabled, this will also update the radar.
///
///
public void SetSquadronId (int newSquadronId)
{
if (squadronId != newSquadronId)
{
squadronId = newSquadronId;
// Do we need to update the radar item?
if (isInitialised && isRadarEnabled && radarItemIndex >= 0)
{
sscRadar.SetSquardronId(radarItemIndex, squadronId);
}
}
}
///
/// Set the Faction Id for the object. If radar is enabled, this will also update the radar.
///
///
public void SetFactionId (int newFactionId)
{
if (factionId != newFactionId)
{
factionId = newFactionId;
// Do we need to update the radar item?
if (isInitialised && isRadarEnabled && radarItemIndex >= 0)
{
sscRadar.SetFactionId(radarItemIndex, factionId);
}
}
}
///
/// Verify that the damage multiplier array is correctly sized
///
public void VerifyMultiplierArray()
{
// Check that the damage multipliers array exists
if (damageMultipliersArray == null)
{
damageMultipliersArray = new float[] { 1f, 1f, 1f, 1f, 1f, 1f };
}
else
{
// Check that the damage multipliers array is of the correct length
int damageMultipliersArrayLength = damageMultipliersArray.Length;
if (damageMultipliersArrayLength != 6)
{
// If it is not the correct length, resize it
// Convert the array into a list
List tempDamageMultipliersList = new List();
tempDamageMultipliersList.AddRange(damageMultipliersArray);
if (damageMultipliersArrayLength > 6)
{
// If we have too many items in the array, remove some
for (int i = damageMultipliersArrayLength; i > 6; i--) { tempDamageMultipliersList.RemoveAt(i - 1); }
}
else
{
// If we don't have enough items in the array, add some
for (int i = damageMultipliersArrayLength; i < 6; i++) { tempDamageMultipliersList.Add(1f); }
}
// Convert the list back into an array
damageMultipliersArray = tempDamageMultipliersList.ToArray();
}
}
}
#endregion
#region Public Virtual and Protected API Methods
///
/// Initialises the DestructibleObjectModule. If you wish to override this in a child (inherited)
/// class you almost always will want to call the base method first.
/// public override void Initialise()
/// {
/// base.Initialise();
/// // Do stuff here
/// }
///
public virtual void Initialise()
{
if (!isInitialised)
{
damageReceiver = GetComponent();
if (damageReceiver == null) { damageReceiver = gameObject.AddComponent(); }
if (damageReceiver == null)
{
#if UNITY_EDITOR
Debug.LogWarning("ERROR: DestructibleOjectModule.Initialise() - could not find or add DamageReceiver component to " + name);
#endif
}
else
{
VerifyMultiplierArray();
ResetHealth();
// Get a reference to the Ship Controller Manager instance
sscManager = SSCManager.GetOrCreateManager();
// Not sure if we need these or just the ReinitialiseEffects()
effectsObjectPrefabID = -1;
isDestructionEffectsObjectInstantiated = false;
destructionEffectItemKey = new SSCEffectItemKey(-1, -1, 0);
// Not sure if we need these or just the ReinitialiseDestructObjects()
destructObjectPrefabID = -1;
isDestructObjectActivated = false;
destructItemKey = new SSCDestructItemKey(-1, -1, 0);
// Initialise effects objects
ReinitialiseEffects();
// Initialise destruct modules
ReinitialiseDestructObjects();
// Get notified when the object is hit by a projectile or beam
damageReceiver.callbackOnHit = ApplyDamage;
isInitialised = true;
}
if (isInitialised)
{
EnableOrDisableRadar(isRadarEnabled);
}
}
}
///
/// Destroys the object.
/// If you wish to override this in a child (inherited) class you almost
/// always will want to call the base after doing your actions.
/// public override void DestructObject()
/// {
/// // Do stuff here
/// base.DestructObject();
/// }
///
public virtual void DestructObject()
{
if (isInitialised)
{
if (isRadarEnabled) { EnableOrDisableRadar(false); }
#region Instantiate the destruction effects prefab
if (destructionEffectsObject != null)
{
InstantiateEffectsObjectParameters ieParms = new InstantiateEffectsObjectParameters
{
effectsObjectPrefabID = effectsObjectPrefabID,
position = transform.position + (transform.rotation * destructionEffectsOffset),
rotation = transform.rotation
};
sscManager.InstantiateEffectsObject(ref ieParms);
}
#endregion
#region Instantiate the destruct module prefab
if (!isDestructObjectActivated && destructObjectPrefabID >= 0 && destructObject != null)
{
// Turn off all colliders. As we are going to destroy the gameobject,
// we can simply deactivate it.
gameObject.SetActive(false);
// Instantiate the region destruct prefab
InstantiateDestructParameters dstParms = new InstantiateDestructParameters
{
destructPrefabID = destructObjectPrefabID,
position = transform.position + (transform.rotation * destructObjectOffset),
rotation = transform.rotation,
explosionPowerFactor = 1f,
explosionRadiusFactor = 1f
};
// Keep track of the DestructModule instance that was instantiated for this object
// In our case, we don't really need this as we're going to destroy this script. However,
// it may be useful if we want to do something else in the future.
if (sscManager.InstantiateDestruct(ref dstParms) != null)
{
destructItemKey = new SSCDestructItemKey(destructObjectPrefabID, dstParms.destructPoolListIndex, dstParms.destructSequenceNumber);
}
isDestructObjectActivated = true;
}
#endregion
// Destroy the original object
Destroy(gameObject);
}
}
///
/// Destroys the object after a delay period in seconds
///
///
public virtual void DestructObjectDelayed (float delayDuration)
{
if (delayDuration > 0f)
{
Invoke("DestructObject", delayDuration);
}
else { DestructObject(); }
}
#endregion
}
}