289 lines
9.5 KiB
C#
289 lines
9.5 KiB
C#
using System;
|
|
using UnityEngine;
|
|
using Code.Gameplay.Player;
|
|
using Code.UI;
|
|
using DG.Tweening;
|
|
using Cysharp.Threading.Tasks;
|
|
using Random = UnityEngine.Random;
|
|
|
|
namespace Code.Gameplay.Enemies
|
|
{
|
|
public class Boss : MonoBehaviour, IEnemy
|
|
{
|
|
[SerializeField] private int _maxHealth = 200;
|
|
[SerializeField] private int _damagePerHit = 25;
|
|
|
|
[SerializeField] private float _enrageThreshold = 0.5f;
|
|
|
|
[SerializeField] private float _flyUpHeight = 10f;
|
|
[SerializeField] private float _flyUpSpeed = 8f;
|
|
[SerializeField] private float _diveSpeed = 18f;
|
|
|
|
[SerializeField] private float _diveCooldown = 4f;
|
|
[SerializeField] private float _featherCooldown = 3f;
|
|
[SerializeField] private float _enragedFeatherCooldown = 1.5f;
|
|
|
|
[SerializeField] private float _sweepSpeed = 12f;
|
|
[SerializeField] private float _sweepYOffset = -1f;
|
|
[SerializeField] private int _sweepPasses = 3;
|
|
[SerializeField] private float _sweepRange = 20f;
|
|
|
|
[SerializeField] private Transform _player;
|
|
[SerializeField] private GameObject _featherPrefab;
|
|
|
|
[SerializeField] private SpriteRenderer _spriteRenderer;
|
|
|
|
[SerializeField] private AudioClip _diveSFX;
|
|
[SerializeField] private AudioClip _featherSFX;
|
|
[SerializeField] private AudioClip _sweepSFX;
|
|
[SerializeField] private AudioClip _hitSFX;
|
|
[SerializeField] private AudioClip _deathSFX;
|
|
[SerializeField] private AudioSource _audioSource;
|
|
|
|
private int _currentHealth;
|
|
private State _currentState = State.Perching;
|
|
private float _lastDiveTime;
|
|
private float _lastFeatherTime;
|
|
private bool isEnraged => (float)_currentHealth / _maxHealth <= _enrageThreshold;
|
|
private bool _isSweeping;
|
|
private Vector3 _spawnPosition;
|
|
|
|
private enum State
|
|
{
|
|
Perching,
|
|
Diving,
|
|
Returning,
|
|
Sweeping
|
|
}
|
|
|
|
private void Start()
|
|
{
|
|
_currentHealth = _maxHealth;
|
|
_spawnPosition = transform.position;
|
|
if (!_player) _player = GameObject.FindWithTag("Player").transform;
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
if (!_player) return;
|
|
|
|
if (_currentState is State.Perching or State.Returning)
|
|
{
|
|
LookAtPlayer();
|
|
}
|
|
|
|
if (_currentState != State.Perching) return;
|
|
|
|
if (isEnraged && !_isSweeping && Random.value < 0.03f)
|
|
{
|
|
SweepAttack().Forget();
|
|
return;
|
|
}
|
|
|
|
if (Time.time - _lastDiveTime > _diveCooldown && CanDive())
|
|
{
|
|
DiveAttack().Forget();
|
|
}
|
|
else if (Time.time - _lastFeatherTime > (isEnraged ? _enragedFeatherCooldown : _featherCooldown))
|
|
{
|
|
FeatherAttack();
|
|
}
|
|
}
|
|
|
|
private void LookAtPlayer()
|
|
{
|
|
if (!_player) return;
|
|
var direction = _player.position.x - transform.position.x;
|
|
transform.localScale = new Vector3(
|
|
direction >= 0 ? Mathf.Abs(transform.localScale.x) : -Mathf.Abs(transform.localScale.x),
|
|
transform.localScale.y,
|
|
transform.localScale.z
|
|
);
|
|
}
|
|
|
|
private bool CanDive() =>
|
|
_player &&
|
|
Vector2.Distance(transform.position, _player.position) > 3f &&
|
|
Mathf.Abs(transform.position.y - _player.position.y) < 8f;
|
|
|
|
private async UniTaskVoid DiveAttack()
|
|
{
|
|
_currentState = State.Diving;
|
|
|
|
if (_diveSFX && _audioSource)
|
|
_audioSource.PlayOneShot(_diveSFX);
|
|
|
|
var startPos = transform.position;
|
|
var flyUpPos = new Vector3(startPos.x, startPos.y + _flyUpHeight, startPos.z);
|
|
|
|
while (Vector3.Distance(transform.position, flyUpPos) > 0.1f)
|
|
{
|
|
transform.position = Vector3.MoveTowards(transform.position, flyUpPos, _flyUpSpeed * Time.deltaTime);
|
|
await UniTask.Yield();
|
|
}
|
|
|
|
await UniTask.Delay(TimeSpan.FromSeconds(1.2f));
|
|
|
|
if (_player)
|
|
{
|
|
var diveTarget = new Vector3(_player.position.x, startPos.y, startPos.z);
|
|
while (Vector3.Distance(transform.position, diveTarget) > 0.1f)
|
|
{
|
|
transform.position = Vector3.MoveTowards(transform.position, diveTarget, _diveSpeed * Time.deltaTime);
|
|
await UniTask.Yield();
|
|
}
|
|
|
|
if (Vector2.Distance(transform.position, _player.position) < 1.8f)
|
|
{
|
|
_player.GetComponent<PlayerHealth>()?.TakeDamage();
|
|
}
|
|
}
|
|
|
|
_currentState = State.Returning;
|
|
ReturnToPosition(_spawnPosition);
|
|
_lastDiveTime = Time.time;
|
|
_currentState = State.Perching;
|
|
}
|
|
|
|
private void FeatherAttack()
|
|
{
|
|
_lastFeatherTime = Time.time;
|
|
if (!_featherPrefab || !_player) return;
|
|
|
|
if (_featherSFX && _audioSource)
|
|
_audioSource.PlayOneShot(_featherSFX);
|
|
|
|
var facingRight = transform.localScale.x > 0;
|
|
|
|
var angles = facingRight
|
|
? new[] { 20f, 0f, -20f }
|
|
: new[] { 160f, 180f, 200f };
|
|
|
|
foreach (var angle in angles)
|
|
{
|
|
var feather = Instantiate(_featherPrefab, transform.position, Quaternion.identity);
|
|
var featherScript = feather.GetComponent<FeatherProjectile>();
|
|
if (featherScript)
|
|
{
|
|
featherScript.Launch(angle);
|
|
}
|
|
}
|
|
}
|
|
|
|
private async UniTaskVoid SweepAttack()
|
|
{
|
|
if (!_player) return;
|
|
|
|
_isSweeping = true;
|
|
_currentState = State.Sweeping;
|
|
|
|
if (_sweepSFX && _audioSource)
|
|
_audioSource.PlayOneShot(_sweepSFX);
|
|
|
|
var sweepY = _player.position.y + _sweepYOffset;
|
|
var leftBound = new Vector3(_player.position.x - _sweepRange, sweepY, transform.position.z);
|
|
var rightBound = new Vector3(_player.position.x + _sweepRange, sweepY, transform.position.z);
|
|
|
|
for (var i = 0; i < _sweepPasses; i++)
|
|
{
|
|
while (transform.position.x < rightBound.x)
|
|
{
|
|
transform.position += Vector3.right * (_sweepSpeed * Time.deltaTime);
|
|
LookAtDirection(true);
|
|
CheckPlayerCollision();
|
|
await UniTask.Yield();
|
|
}
|
|
|
|
while (transform.position.x > leftBound.x)
|
|
{
|
|
transform.position += Vector3.left * (_sweepSpeed * Time.deltaTime);
|
|
LookAtDirection(false);
|
|
CheckPlayerCollision();
|
|
await UniTask.Yield();
|
|
}
|
|
}
|
|
|
|
_isSweeping = false;
|
|
_currentState = State.Returning;
|
|
ReturnToPosition(_spawnPosition);
|
|
_currentState = State.Perching;
|
|
}
|
|
|
|
private void LookAtDirection(bool right)
|
|
{
|
|
transform.localScale = new Vector3(
|
|
right ? Mathf.Abs(transform.localScale.x) : -Mathf.Abs(transform.localScale.x),
|
|
transform.localScale.y,
|
|
transform.localScale.z
|
|
);
|
|
}
|
|
|
|
private void ReturnToPosition(Vector3 target)
|
|
{
|
|
transform.position = Vector3.MoveTowards(transform.position, target, 10f * Time.deltaTime);
|
|
}
|
|
|
|
private void CheckPlayerCollision()
|
|
{
|
|
if (_player && Vector2.Distance(transform.position, _player.position) < 1.8f)
|
|
{
|
|
_player.GetComponent<PlayerHealth>()?.TakeDamage();
|
|
}
|
|
}
|
|
|
|
public void TakeDamage()
|
|
{
|
|
_currentHealth -= _damagePerHit;
|
|
|
|
if (_hitSFX && _audioSource)
|
|
_audioSource.PlayOneShot(_hitSFX);
|
|
|
|
BlinkHit();
|
|
|
|
if (_currentHealth <= 0)
|
|
{
|
|
Die();
|
|
}
|
|
}
|
|
|
|
private void BlinkHit()
|
|
{
|
|
if (!_spriteRenderer) return;
|
|
var originalColor = _spriteRenderer.color;
|
|
var hitColor = new Color(1f, 0.3f, 0.3f, originalColor.a);
|
|
|
|
DOTween.Sequence()
|
|
.Append(_spriteRenderer.DOColor(hitColor, 0.1f))
|
|
.Append(_spriteRenderer.DOColor(originalColor, 0.1f))
|
|
.Append(_spriteRenderer.DOColor(hitColor, 0.1f))
|
|
.Append(_spriteRenderer.DOColor(originalColor, 0.1f))
|
|
.SetLink(gameObject);
|
|
}
|
|
|
|
private void Die()
|
|
{
|
|
if (_deathSFX && _audioSource)
|
|
_audioSource.PlayOneShot(_deathSFX);
|
|
|
|
enabled = false;
|
|
|
|
if (_spriteRenderer)
|
|
{
|
|
_spriteRenderer.DOFade(0f, 2f)
|
|
.OnComplete(() =>
|
|
{
|
|
var gameplayController = FindFirstObjectByType<GameplayControllerSecond>();
|
|
gameplayController?.OnBossDefeated();
|
|
Destroy(gameObject);
|
|
})
|
|
.SetLink(gameObject);
|
|
}
|
|
else
|
|
{
|
|
var gameplayController = FindFirstObjectByType<GameplayControllerSecond>();
|
|
gameplayController?.OnBossDefeated();
|
|
Destroy(gameObject);
|
|
}
|
|
}
|
|
}
|
|
} |