589 lines
23 KiB
C#

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace BulletHellTemplate
{
/// <summary>
/// Represents the data for a skill.
/// </summary>
[CreateAssetMenu(fileName = "NewSkillData", menuName = "Skill Data", order = 51)]
public partial class SkillData : ScriptableObject
{
// -------------------------
// 1) SKILL DETAILS
// -------------------------
[Header("Skill Details")]
[Tooltip("Icon representing the skill.")]
public Sprite icon;
[Tooltip("Icon representing the evolved skill.")]
public Sprite iconEvolved;
[Tooltip("Frame used when the skill is evolved.")]
public Sprite frameEvolved;
[Tooltip("Name of the skill.")]
public string skillName;
public NameTranslatedByLanguage[] skillNameTranslated;
[Tooltip("Description of the skill.")]
public string skillDescription;
public DescriptionTranslatedByLanguage[] skillDescriptionTranslated;
[Tooltip("Type of damage dealt by the skill.")]
public CharacterTypeData damageType;
[Tooltip("Cooldown time between skill uses.")]
public int cooldown;
[Tooltip("Delay before the skill is launched.")]
public float delayToLaunch = 0.3f;
[Tooltip("Set a value above zero if you want the character to stop moving for a period of time.")]
public float delayToMove = 0;
[Tooltip("Check if you want the character to be able to turn while standing still.")]
public bool canRotateWhileStopped;
[Tooltip("Time to wait before allowing auto-attack after using this skill.")]
public float autoAttackDelay = 1.0f;
[Tooltip("Does it cost mana to use an ability?")]
public float manaCost = 0;
public AimType AimMode = AimType.InputDirection;
[Tooltip("Default DamageEntity prefab.")]
public DamageEntity damageEntityPrefab;
// -------------------------
// 2) EFFECTS
// -------------------------
[Header("Effects")]
[Tooltip("Effect to spawn when the skill is activated.")]
public GameObject spawnEffect;
[Tooltip("Duration of the spawn effect.")]
public float effectDuration;
[Tooltip("Audio clip to play when the skill is activated.")]
public AudioClip spawnAudio;
public bool playSpawnAudioEaShot;
[Tooltip("Audio clip to play when the skill is in action.")]
public AudioClip skillAudio;
public bool playSkillAudioEaShot;
[Tooltip("Delay before playing the skill audio.")]
public float playSkillAudioAfter = 0.15f;
// -------------------------
// 3) SKILL SETTINGS
// -------------------------
[Header("Skill Settings")]
[Tooltip("Type of range indicator to display.")]
public RangeIndicatorType rangeIndicatorType = RangeIndicatorType.None;
[Tooltip("Settings for the radial range indicator.")]
public RadialIndicatorSettings radialIndicatorSettings;
[Tooltip("Settings for the radial area-of-effect (AoE) indicator.")]
public RadialAoEIndicatorSettings radialAoEIndicatorSettings;
[Tooltip("Settings for the arrow range indicator.")]
public ArrowIndicatorSettings arrowIndicatorSettings;
[Tooltip("Settings for the cone range indicator.")]
public ConeIndicatorSettings coneIndicatorSettings;
[Tooltip("Defines the launch method for the skill (e.g., Projectile, Targeted AoE, Targeted Air Strike).")]
public LaunchType launchType = LaunchType.Projectile;
[Tooltip("Height used for air strike skills.")]
public float airStrikeHeight;
[Tooltip("Should the character rotate towards the enemy?")]
public bool isRotateToEnemy;
[Tooltip("Duration of the rotation.")]
public float rotateDuration;
// -------------------------
// 4) DESTROY SETTINGS
// -------------------------
[Header("Destroy Settings")]
[Tooltip("Destroy the DamageEntity when hitting the first enemy.")]
public bool destroyOnFirstHit;
[Tooltip("Change the size of the axes of the DamageEntity prefab.")]
public DamageEntitySizeChange sizeChangeConfig;
[Tooltip("Does the skill have lifesteal?")]
public bool isHpLeech;
[Header("Explosion on Destroy Settings")]
[Tooltip("When destroyed (by lifetime or hitting an enemy), instantiate an explosion DamageEntity.")]
public bool explodeOnDestroy;
[Tooltip("DamageEntity prefab for the explosion.")]
public DamageEntity explodeDamageEntity;
[Tooltip("Skill level settings for the explosion.")]
public SkillLevel explodeEntitySettings;
// -------------------------
// 5) MULTI-SHOT SETTINGS
// -------------------------
[Header("Multi-Shot Settings")]
[Tooltip("Is this a Multi-Shot skill?")]
public bool isMultiShot;
[Tooltip("Number of shots.")]
public int shots;
[Tooltip("Angle between shots.")]
public float angle = 30;
[Tooltip("Delay between each shot.")]
public float delay = 0;
// -------------------------
// 6) ADVANCED MULTI-SHOT SETTINGS
// -------------------------
[Header("Advanced Multi-Shot Settings")]
[Tooltip("Enable advanced multi-shot, using waves and angles.")]
public bool isAdvancedMultiShot;
public bool playAnimationEaShot = false;
[Tooltip("List of shot waves for normal state.")]
public List<ShotWave> shotWaves;
[Tooltip("List of shot waves for evolved state.")]
public List<ShotWave> shotWavesEvolved;
// -------------------------
// 7) ORBITAL SETTINGS
// -------------------------
[Header("Orbital Settings")]
[Tooltip("Is this an Orbital skill?")]
public bool isOrbital;
[Tooltip("Distance before starting to orbit.")]
public float orbitalDistance = 5f;
// -------------------------
// 8) RICOCHET SETTINGS
// -------------------------
[Header("Ricochet Settings")]
[Tooltip("Does the skill ricochet off surfaces?")]
public bool isRicochet;
// -------------------------
// 9) BOOMERANG SETTINGS
// -------------------------
[Header("Boomerang Settings")]
[Tooltip("Is this a Boomerang skill?")]
public bool isBoomerangSkill;
[Tooltip("Maximum distance before returning.")]
public float maxDistance;
// -------------------------
// 10) DASH SETTINGS
// -------------------------
[Header("Dash Settings")]
[Tooltip("Is this a Dash skill?")]
public bool isDash;
[Tooltip("If true, the dash will move in the reverse direction.")]
public bool isReverseDash;
[Tooltip("Speed of the dash.")]
public float dashSpeed;
[Tooltip("Duration of the dash.")]
public float dashDuration;
/// <summary>
/// Advanced dash settings for multiple dash waves and modes.
/// </summary>
[Tooltip("Advanced dash configuration.")]
public AdvancedDashSettings advancedDashSettings = new AdvancedDashSettings();
// -------------------------
// 11) MELEE SETTINGS
// -------------------------
[Header("Melee Settings")]
[Tooltip("Is this a Melee skill?")]
public bool isMelee;
// -------------------------
// 12) SLOW EFFECT SETTINGS
// -------------------------
[Header("Slow Effect Settings")]
[Tooltip("Does the skill apply a slow effect?")]
public bool applySlow;
[Tooltip("Percentage to slow the target.")]
public float slowPercent;
[Tooltip("Duration of the slow effect.")]
public float slowDuration;
// -------------------------
// 13) KNOCKBACK EFFECT SETTINGS
// -------------------------
[Header("Knockback Effect Settings")]
[Tooltip("Does the skill apply knockback?")]
public bool applyKnockback;
[Tooltip("Distance of the knockback.")]
public float knockbackDistance;
[Tooltip("Duration of the knockback effect.")]
public float knockbackDuration;
// -------------------------
// 14) STUN EFFECT SETTINGS
// -------------------------
[Header("Stun Effect Settings")]
[Tooltip("Does the skill apply stun?")]
public bool applyStun;
[Tooltip("Duration of the stun effect.")]
public float stunDuration;
// -------------------------
// 15) DAMAGE OVER TIME (DOT) SETTINGS
// -------------------------
[Header("Damage Over Time (DOT) Settings")]
[Tooltip("Does the skill apply damage over time?")]
public bool applyDOT;
[Tooltip("Total DOT damage.")]
public int dotAmount;
[Tooltip("Duration of the DOT effect.")]
public float dotDuration;
// -------------------------
// 16) BUFFS AND DEBUFFS
// -------------------------
[Header("Self-buffs/Self-debuffs")]
[Header("Buffs")]
[Tooltip("Does this skill heal the user?")]
public bool receiveHeal;
[Tooltip("Heal amount if receiveHeal is true.")]
public float healAmount;
[Tooltip("Is this a Shield skill?")]
public bool receiveShield;
[Tooltip("Amount of shield provided.")]
public int shieldAmount;
[Tooltip("Duration of the shield.")]
public float shieldDuration;
[Tooltip("Does this skill grant a movement speed buff?")]
public bool receiveMoveSpeed;
[Tooltip("Movement speed increase amount.")]
public float moveSpeedAmount;
[Tooltip("Duration of the movement speed buff.")]
public float moveSpeedDuration;
[Tooltip("Does this skill grant an attack speed buff?")]
public bool receiveAttackSpeed;
[Tooltip("Attack speed increase amount.")]
public float attackSpeedAmount;
[Tooltip("Duration of the attack speed buff.")]
public float attackSpeedDuration;
[Tooltip("Does this skill grant a defense buff?")]
public bool receiveDefense;
[Tooltip("Defense increase amount.")]
public float defenseAmount;
[Tooltip("Duration of the defense buff.")]
public float defenseDuration;
[Tooltip("Does this skill grant a damage buff?")]
public bool receiveDamage;
[Tooltip("Damage increase amount.")]
public float damageAmount;
[Tooltip("Duration of the damage buff.")]
public float damageDuration;
[Tooltip("Does the skill grant invincibility?")]
public bool isInvincible;
[Tooltip("Duration of invincibility.")]
public float invincibleDuration = 0;
[Header("Debuff")]
[Tooltip("Does this skill apply a slow to the user?")]
public bool receiveSlow;
[Tooltip("Amount of self-slow if receiveSlow is true (0 = no slow, 1 = 100% slow).")]
[Range(0f, 1f)]
public float receiveSlowAmount;
[Tooltip("Duration of the self-slow.")]
public float receiveSlowDuration;
// -------------------------
// 17) EVOLVE CHANGES
// -------------------------
[Tooltip("Evolved DamageEntity prefab.")]
public DamageEntity evolvedDamageEntityPrefab;
[Tooltip("Do evolve changes affect Multi-Shot settings?")]
public bool evolveChanges;
[Tooltip("Number of shots when evolved.")]
public int shotsEvolved;
[Tooltip("Angle between shots when evolved.")]
public float angleEvolved = 30;
[Tooltip("Delay between shots when evolved.")]
public float delayEvolved = 0;
[Header("Skill Levels")]
[Tooltip("Levels of the skill.")]
public List<SkillLevel> skillLevels;
[Header("Evolved Skill Settings")]
[Tooltip("Stat required for skill evolution.")]
public StatPerkData requireStatForEvolve;
/// <summary>
/// Launches damage from a character.
/// </summary>
/// <param name="attackTransform">Transform from which the damage is launched.</param>
/// <param name="direction">Direction to launch the damage.</param>
/// <param name="attacker">Character who is attacking.</param>
public void LaunchDamage(Transform attackTransform, Vector3 direction, CharacterEntity attacker)
{
int currentLevel = GameplayManager.Singleton.GetBaseSkillLevel(this);
int skillLevelIndex = Mathf.Clamp(currentLevel, 0, skillLevels.Count - 1);
SkillLevel levelData = skillLevels[skillLevelIndex];
GameplayManager.Singleton.StartCoroutine(
LaunchDamageInternal(attackTransform, direction, levelData, attacker)
);
}
/// <summary>
/// Launches the damage entity (projectile) in the specified direction, handling multi-shot, orbital skills, etc.
/// </summary>
/// <param name="attackTransform">The transform from which to launch the damage entity.</param>
/// <param name="direction">The direction in which to launch the damage entity.</param>
/// <param name="levelData">The current skill level data.</param>
/// <param name="attacker">The character entity that is attacking.</param>
private IEnumerator LaunchDamageInternal(Transform attackTransform, Vector3 direction, SkillLevel levelData, CharacterEntity attacker)
{
bool useEvolvedSettings = levelData.isEvolved;
int finalShots = useEvolvedSettings && evolveChanges ? shotsEvolved : shots;
float finalAngle = useEvolvedSettings && evolveChanges ? angleEvolved : angle;
float finalDelay = useEvolvedSettings && evolveChanges ? delayEvolved : delay;
List<ShotWave> finalShotWaves = (useEvolvedSettings && evolveChanges) ? shotWavesEvolved : shotWaves;
DamageEntity damageEntityPrefabToUse = useEvolvedSettings ? evolvedDamageEntityPrefab : damageEntityPrefab;
if (damageEntityPrefabToUse == null) { Debug.LogWarning("DamageEntity prefab not assigned."); yield break; }
if (attackTransform == null) { Debug.LogWarning("Attack transform is null."); yield break; }
if (isOrbital)
{
GameObject orbitManagerObj = new GameObject("OrbitManager");
OrbitManager orbitManager = orbitManagerObj.AddComponent<OrbitManager>();
orbitManager.InitializeOrbital(this, levelData, attacker, finalShots, damageEntityPrefabToUse, direction.normalized * levelData.speed);
yield break;
}
if (isAdvancedMultiShot)
{
if (finalShotWaves == null || finalShotWaves.Count == 0) { Debug.LogWarning("No shot waves configured for advanced multi-shot."); yield break; }
foreach (ShotWave wave in finalShotWaves)
{
if (wave.shotAngles == null || wave.shotAngles.Count == 0) { Debug.LogWarning("Shot wave has no angles defined."); continue; }
Vector3 waveBaseDirection = GetUpdatedDirection(attacker, direction);
List<float> generatedAngles = GenerateShotAngles(wave.shotCount, wave.shotAngles.Count > 0 ? wave.shotAngles[0] : 0f, wave.useInitialAngle, wave.initialAngle, wave.shotAngles);
foreach (float angleVal in generatedAngles)
{
Quaternion rotation = Quaternion.AngleAxis(angleVal, Vector3.up);
Vector3 shotDirection = rotation * waveBaseDirection;
SpawnDamageEntity(attackTransform, shotDirection, damageEntityPrefabToUse, levelData, attacker);
}
if (wave.delayBeforeNextWave > 0) yield return new WaitForSeconds(wave.delayBeforeNextWave);
}
}
else if (isMultiShot)
{
if (finalShots <= 0) finalShots = 1;
float totalSpreadAngle = finalAngle;
int numberOfShots = finalShots;
float angleStep = numberOfShots > 1 ? totalSpreadAngle / (numberOfShots - 1) : 0f;
float startingAngle = -totalSpreadAngle / 2f;
for (int i = 0; i < numberOfShots; i++)
{
Vector3 currentDirection = GetUpdatedDirection(attacker, direction);
float currentAngle = startingAngle + (angleStep * i);
Quaternion rotation = Quaternion.AngleAxis(currentAngle, Vector3.up);
Vector3 shotDirection = rotation * currentDirection;
SpawnDamageEntity(attackTransform, shotDirection, damageEntityPrefabToUse, levelData, attacker);
if (finalDelay > 0) yield return new WaitForSeconds(finalDelay);
}
}
else
{
Vector3 currentDirection = GetUpdatedDirection(attacker, direction);
SpawnDamageEntity(attackTransform, currentDirection, damageEntityPrefabToUse, levelData, attacker);
}
}
/// <summary>
/// Recalculates the current direction based on the attacker's forward direction.
/// </summary>
private Vector3 GetUpdatedDirection(CharacterEntity attacker, Vector3 originalDirection)
{
if (attacker != null && attacker.characterModelTransform != null)
{
return attacker.characterModelTransform.forward.normalized;
}
return originalDirection.normalized;
}
/// <summary>
/// Generates shot angles based on wave data or a single baseAngle approach.
/// This version supports passing an entire list of angles if needed.
/// </summary>
/// <param name="shotCount">Number of shots in the wave.</param>
/// <param name="baseAngle">Base angle for shot increments.</param>
/// <param name="useInitialAngle">If true, the first shot uses initialAngle.</param>
/// <param name="initialAngle">The angle used if useInitialAngle is true.</param>
/// <param name="shotAnglesList">The raw angles from the wave (optional).</param>
/// <returns>List of angles for each shot in the wave.</returns>
private List<float> GenerateShotAngles(int shotCount, float baseAngle, bool useInitialAngle, float initialAngle, List<float> shotAnglesList)
{
List<float> angles = new List<float>();
if (shotCount <= 0) return angles;
if (shotAnglesList != null && shotAnglesList.Count > 1)
{
for (int i = 0; i < shotCount; i++)
angles.Add(i < shotAnglesList.Count ? shotAnglesList[i] : shotAnglesList[^1]); // repeat last
}
else
{
if (shotCount == 1)
{
angles.Add(useInitialAngle ? initialAngle : baseAngle);
}
else
{
if (baseAngle != 0f)
{
if (useInitialAngle)
{
angles.Add(initialAngle);
for (int i = 1; i < shotCount; i++)
{
float angleOffset = ((i + 1) / 2) * baseAngle;
angles.Add(i % 2 == 1 ? initialAngle + angleOffset : initialAngle - angleOffset);
}
}
else
{
angles.Add(0f);
for (int i = 1; i < shotCount; i++)
{
float angleOffset = ((i + 1) / 2) * baseAngle;
angles.Add(i % 2 == 1 ? angleOffset : -angleOffset);
}
}
}
else
{
for (int i = 0; i < shotCount; i++) angles.Add(0f);
}
}
}
return angles;
}
/// <summary>
/// Spawns a damage entity in the given direction.
/// </summary>
private void SpawnDamageEntity(
Transform attackTransform,
Vector3 direction,
DamageEntity damageEntityPrefab,
SkillLevel levelData,
CharacterEntity attacker
)
{
if (playSkillAudioEaShot && skillAudio != null)
{
AudioManager.Singleton.PlayAudio(skillAudio, "vfx");
}
if (playAnimationEaShot)
{
attacker.PlayTriggerAnimation(attacker.GetSkillIndex(this));
}
if (playSpawnAudioEaShot && spawnAudio != null)
{
AudioManager.Singleton.PlayAudio(spawnAudio, "vfx");
}
if (isMelee && attacker.characterModelTransform != null)
{
DamageEntity meleeDamage = Instantiate(
damageEntityPrefab,
attacker.characterModelTransform.position,
Quaternion.identity,
attacker.characterModelTransform
);
meleeDamage.transform.localPosition = Vector3.zero;
meleeDamage.transform.localRotation = Quaternion.identity;
meleeDamage.SetSkill(
this,
levelData.baseDamage,
levelData.attackerDamageRate,
levelData.canCauseCriticalDamage,
attacker,
levelData.lifeTime,
Vector3.zero
);
if (sizeChangeConfig.enableSizeChange)
{
meleeDamage.SetSizeChange(sizeChangeConfig);
}
}
else
{
DamageEntity projectile = Instantiate(
damageEntityPrefab,
attackTransform.position,
Quaternion.LookRotation(direction)
);
projectile.SetSkill(
this,
levelData.baseDamage,
levelData.attackerDamageRate,
levelData.canCauseCriticalDamage,
attacker,
levelData.lifeTime,
direction.normalized * levelData.speed
);
if (sizeChangeConfig.enableSizeChange)
{
projectile.SetSizeChange(sizeChangeConfig);
}
}
}
}
}