added boss (final commit)
This commit is contained in:
1 parent
ff19b0461e
commit
0a1cecfb4a
71 files changed
+1953
-144
No files matched your search
@@ -0,0 +1,23 @@
|
||||
using Code.Gameplay.Player;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Code.Gameplay
|
||||
{
|
||||
public class BossTrigger: MonoBehaviour
|
||||
{
|
||||
private bool _triggered;
|
||||
private void OnTriggerEnter2D(Collider2D other)
|
||||
{
|
||||
if (!other.CompareTag("Player") || _triggered) return;
|
||||
|
||||
Instantiate(Resources.Load<GameObject>("Prefabs/Boss"));
|
||||
_triggered = true;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (_triggered && FindFirstObjectByType<PlayerMovement>().gameObject.transform.position.x > transform.position.x + 10)
|
||||
GetComponent<Collider2D>().isTrigger = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 91d34fb33bdc44ddb5146b689aafa67f
|
||||
timeCreated: 1770457564
|
||||
@@ -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
|
||||
@@ -7,7 +7,6 @@ using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEngine.UI;
|
||||
using DG.Tweening;
|
||||
using Unity.VisualScripting;
|
||||
|
||||
namespace Code.UI
|
||||
{
|
||||
@@ -45,7 +44,9 @@ namespace Code.UI
|
||||
deathScreen.rectTransform.DOScale(1f, 0.3f)
|
||||
.SetEase(Ease.OutBack);
|
||||
|
||||
deathText.color = deathText.color.WithAlpha(0f);
|
||||
var color = deathText.color;
|
||||
color.a = 0;
|
||||
deathText.color = color;
|
||||
deathText.DOFade(1f, 0.6f)
|
||||
.OnComplete(() =>
|
||||
{
|
||||
|
||||
@@ -7,15 +7,17 @@ using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEngine.UI;
|
||||
using DG.Tweening;
|
||||
using Unity.VisualScripting;
|
||||
|
||||
namespace Code.UI
|
||||
{
|
||||
public class GameplayControllerSecond : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private Image deathScreen;
|
||||
[SerializeField] private Image winScreen;
|
||||
[SerializeField] private TMP_Text deathText;
|
||||
[SerializeField] private TMP_Text winText;
|
||||
[SerializeField] private Button exitButton;
|
||||
[SerializeField] private Button returnButton;
|
||||
[SerializeField] private PlayerHealth playerHealth;
|
||||
[SerializeField] private AudioSource audioSource;
|
||||
[SerializeField] private BoxCollider2D blockBorder;
|
||||
@@ -29,9 +31,11 @@ namespace Code.UI
|
||||
FindFirstObjectByType<PlayerMovement>().Construct(_inputService);
|
||||
|
||||
exitButton.onClick.AddListener(OnExitButtonPressed);
|
||||
returnButton.onClick.AddListener(OnReturnButtonPressed);
|
||||
playerHealth.OnDied += HandleDeath;
|
||||
|
||||
deathScreen.gameObject.SetActive(false);
|
||||
winScreen.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
private void HandleDeath()
|
||||
@@ -45,7 +49,9 @@ namespace Code.UI
|
||||
deathScreen.rectTransform.DOScale(1f, 0.3f)
|
||||
.SetEase(Ease.OutBack);
|
||||
|
||||
deathText.color = deathText.color.WithAlpha(0f);
|
||||
var color = deathText.color;
|
||||
color.a = 0;
|
||||
deathText.color = color;
|
||||
deathText.DOFade(1f, 0.6f)
|
||||
.OnComplete(() =>
|
||||
{
|
||||
@@ -54,6 +60,29 @@ namespace Code.UI
|
||||
.SetEase(Ease.InOutSine);
|
||||
});
|
||||
}
|
||||
|
||||
public void OnBossDefeated()
|
||||
{
|
||||
var clip = Resources.Load<AudioClip>("Audio/SFX/WinSFX");
|
||||
audioSource.PlayOneShot(clip);
|
||||
_inputService.DisableMap(ActionMapping.Gameplay);
|
||||
winScreen.gameObject.SetActive(true);
|
||||
|
||||
winScreen.rectTransform.localScale = Vector3.zero;
|
||||
winScreen.rectTransform.DOScale(1f, 0.3f)
|
||||
.SetEase(Ease.OutBack);
|
||||
|
||||
var color = winText.color;
|
||||
color.a = 0;
|
||||
winText.color = color;
|
||||
winText.DOFade(1f, 0.6f)
|
||||
.OnComplete(() =>
|
||||
{
|
||||
deathText.DOFade(0.8f, 0.5f)
|
||||
.SetLoops(-1, LoopType.Yoyo)
|
||||
.SetEase(Ease.InOutSine);
|
||||
});
|
||||
}
|
||||
|
||||
private void OnExitButtonPressed()
|
||||
{
|
||||
@@ -61,15 +90,18 @@ namespace Code.UI
|
||||
DOTween.Kill(deathText);
|
||||
|
||||
exitButton.onClick.RemoveAllListeners();
|
||||
returnButton.onClick.RemoveAllListeners();
|
||||
SceneManager.LoadScene("MainMenu");
|
||||
}
|
||||
|
||||
private void Update()
|
||||
|
||||
private void OnReturnButtonPressed()
|
||||
{
|
||||
if (!FindAnyObjectByType<BasicEnemy>())
|
||||
{
|
||||
blockBorder.gameObject.SetActive(false);
|
||||
}
|
||||
DOTween.Kill(winScreen.rectTransform);
|
||||
DOTween.Kill(winText);
|
||||
|
||||
exitButton.onClick.RemoveAllListeners();
|
||||
returnButton.onClick.RemoveAllListeners();
|
||||
SceneManager.LoadScene("MainMenu");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -121,7 +121,7 @@ namespace Code.UI
|
||||
parallaxRoot.localPosition = _currentParallaxOffset;
|
||||
}
|
||||
|
||||
private void OnStartGameClicked() => FadeOutAndLoad("Gameplay2");
|
||||
private void OnStartGameClicked() => FadeOutAndLoad("Gameplay1");
|
||||
|
||||
private void OnExitButtonClicked() => FadeOutAndQuit();
|
||||
|
||||
|
||||
Reference in new issue
Block a user