using System.Collections; using System.Collections.Generic; using UnityEngine; public interface IFighterCallback { void OnFighterDeath(Fighter fighter); } // Generic subclass for any Fighter on the field, whether it's an Enemy or the AntiPlayer. // Contains stats management such as health, attacks, damage, death, etc. public abstract class Fighter : MonoBehaviour, IRoundCallback { [SerializeField] int baseHealth = 100; [SerializeField, Tooltip("Attack every x seconds")] float baseAttackSpeed = 1f; [SerializeField] int baseAttackDamage = 10; [SerializeField] int baseArmor = 1; [SerializeField, Tooltip("Being hurt may cancel the current Attack animation, aborting the current attack. If this Fighter is hurt within x seconds of the last attack, attack again after attackRetryDelayAfterBeingHurt seconds")] float attackRetryGracePeriod = 0.8f; [SerializeField, Tooltip("If an attack is being retried, how long to wait until to actually attack again. Gives the Hrt animation time to play out.")] float attackRetryDelayAfterBeingHurt = 0.25f; [SerializeField] List<AudioClip> attackBarks; [SerializeField] List<AudioClip> hurtBarks; [SerializeField] List<AudioClip> deathBarks; public List<IFighterCallback> callbacks = new List<IFighterCallback>(); protected int maxHealth = 100; // Dynamically valculated based on upgrades protected int currentHealth = 100; protected float currentHealthPercentage { get => (float) currentHealth / maxHealth; } protected Animator animator; protected new Rigidbody2D rigidbody; protected SpriteRenderer spriteRenderer; protected HealthBarController healthBarController; protected DamageIndicatorCanvas damageIndicator; protected BaseCameraController cameraController; protected bool alive { get => currentHealth > 0; } protected bool roundRunning = false; protected FighterTypes fighterType; protected string opponentTag; protected Vector3 initalScale; protected Vector3 initalPosition; protected int initialLayer; protected Sprite initialSprite; protected abstract bool CanAttack(); protected abstract void Attack(); float timeSinceLastAttack = float.PositiveInfinity; protected virtual void Awake() { animator = GetComponent<Animator>(); rigidbody = GetComponent<Rigidbody2D>(); spriteRenderer = GetComponent<SpriteRenderer>(); healthBarController = GetComponentInChildren<HealthBarController>(); damageIndicator = GetComponentInChildren<DamageIndicatorCanvas>(); cameraController = Camera.main.GetComponent<BaseCameraController>(); fighterType = FighterTypes.ENEMY; initalPosition = transform.position; initalScale = transform.localScale; initialLayer = gameObject.layer; initialSprite = spriteRenderer.sprite; OnRoundEnd(false); } protected virtual void Start() { RoundController.instance.roundCallbacks.Add(this); } protected virtual void Update() { if (alive && roundRunning) { if (CanAttack()) { timeSinceLastAttack += Time.deltaTime; var timeUntilNextAttack = baseAttackSpeed - timeSinceLastAttack; if (timeUntilNextAttack <= 0) { cameraController.PlayRandomGlobalAudioClip(attackBarks); Attack(); timeSinceLastAttack = float.IsInfinity(timeUntilNextAttack) ? 0 : -timeUntilNextAttack; // To account for overshoot } } else { timeSinceLastAttack = float.PositiveInfinity; } } } public virtual int DealDamage(int dmg) { if (!alive || !roundRunning) { return 0; } var actualDamage = Mathf.Max(0, Mathf.RoundToInt(dmg - baseArmor * GetStats().armorMultiplier)); currentHealth = Mathf.Max(currentHealth - actualDamage, 0); if (currentHealth == 0) { animator.SetTrigger("Death"); callbacks.ForEach(c => c.OnFighterDeath(this)); // Disable own collision so that other Fighters can run over our dead body gameObject.layer = LayerMask.NameToLayer("Ground-Only Collision"); healthBarController.SetHealthBarEnabled(false); cameraController.PlayRandomGlobalAudioClip(deathBarks); } else { animator.SetTrigger("Hurt"); healthBarController.ShowHealth(currentHealthPercentage); if (timeSinceLastAttack - attackRetryGracePeriod <= 0f) { // Fighter was hit during the grace period, possibly interrupting the Attack animation. // Attack again after another grace period to allow the Hurt animation to play out. timeSinceLastAttack = baseAttackSpeed - attackRetryDelayAfterBeingHurt; } cameraController.PlayRandomGlobalAudioClip(hurtBarks); } damageIndicator.FlashText(string.Format("-{0}", actualDamage)); return actualDamage; } protected FighterStats GetStats() { return StatsManager.instance.fighterStats[fighterType]; } void OnTriggerEnter2D(Collider2D other) { #if false Debug.Log(string.Format("'{0}' (tag '{1}') collided with '{2}' (tag '{3}') in parent '{4}' (tag '{5}')", name, tag, other.name, other.tag, other.transform.parent ? other.transform.parent.name : null, other.transform.parent ? other.transform.parent.tag : null)); #endif if (roundRunning && other.CompareTag(opponentTag)) { var damageToDeal = Mathf.RoundToInt(baseAttackDamage * GetStats().damageMultiplier); other.GetComponent<Fighter>().DealDamage(damageToDeal); } } public virtual void OnRoundStart() { roundRunning = true; animator.enabled = true; rigidbody.bodyType = RigidbodyType2D.Dynamic; maxHealth = currentHealth = Mathf.RoundToInt(baseHealth * GetStats().healthMultiplier); animator.Play("Idle"); healthBarController.Reset(); initalPosition = transform.position; } public virtual void OnRoundEnd(bool won) { roundRunning = false; animator.enabled = false; timeSinceLastAttack = float.PositiveInfinity; rigidbody.velocity = Vector2.zero; rigidbody.bodyType = RigidbodyType2D.Static; transform.position = initalPosition; transform.localScale = initalScale; gameObject.layer = initialLayer; spriteRenderer.sprite = initialSprite; callbacks.Clear(); healthBarController.SetHealthBarEnabled(false); } protected virtual void OnDestroy() { RoundController.instance.roundCallbacks.Remove(this); } }