added boss (final commit)

This commit is contained in:
MrTorex committed 2026-02-07 13:34:37 +03:00
1 parent ff19b0461e
commit 0a1cecfb4a
71 files changed
+1953 -144

No files matched your search

+289
View File
@@ -0,0 +1,289 @@
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);
}
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 1f7e2017ad2d4e8a9dfcb2a92ae14eec
timeCreated: 1770453614
@@ -0,0 +1,41 @@
using Code.Gameplay.Player;
using UnityEngine;
namespace Code.Gameplay.Enemies
{
public class FeatherProjectile : MonoBehaviour
{
[SerializeField] private float _speed = 6f;
[SerializeField] private float _lifetime = 3f;
[SerializeField] private LayerMask _playerLayer;
private Vector2 _direction;
private float _timer;
public void Launch(float angle)
{
var rad = angle * Mathf.Deg2Rad;
_direction = new Vector2(Mathf.Cos(rad), Mathf.Sin(rad));
transform.rotation = Quaternion.Euler(0, 0, angle);
}
private void Update()
{
transform.Translate(_direction * (_speed * Time.deltaTime), Space.World);
var hit = Physics2D.OverlapCircle(transform.position, 0.4f, _playerLayer);
if (hit && hit.CompareTag("Player"))
{
hit.GetComponent<PlayerHealth>()?.TakeDamage();
Destroy(gameObject);
}
_timer += Time.deltaTime;
if (_timer > _lifetime)
{
Destroy(gameObject);
}
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: dcf6d88618b142b6b38c021a1b66054d
timeCreated: 1770454095