From a9cfdb1245e8673ef9fc4e48a532d1d7f802357c Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Sun, 12 Oct 2025 22:34:05 +0200 Subject: [PATCH] feat: make the game okish --- magisterka_2/Assets/Scripts/Bullet.cs | 18 +- magisterka_2/Assets/Scripts/EffectManager.cs | 15 +- magisterka_2/Assets/Scripts/EnemyBlueprint.cs | 88 +++++ .../Assets/Scripts/EnemyBlueprint.cs.meta | 2 + .../Assets/Scripts/EnemyController.cs | 211 +++++++++--- magisterka_2/Assets/Scripts/EnemySpawner.cs | 236 +++++++++++-- magisterka_2/Assets/Scripts/GameDirector.cs | 103 ++++++ .../Assets/Scripts/GameDirector.cs.meta | 2 + .../Assets/Scripts/GameInitializer.cs | 142 +++++++- magisterka_2/Assets/Scripts/Health.cs | 3 + .../Assets/Scripts/PlayerController.cs | 322 ++++++++++++++++-- magisterka_2/Assets/Scripts/ScoreManager.cs | 146 +++++++- 12 files changed, 1161 insertions(+), 127 deletions(-) create mode 100644 magisterka_2/Assets/Scripts/EnemyBlueprint.cs create mode 100644 magisterka_2/Assets/Scripts/EnemyBlueprint.cs.meta create mode 100644 magisterka_2/Assets/Scripts/GameDirector.cs create mode 100644 magisterka_2/Assets/Scripts/GameDirector.cs.meta diff --git a/magisterka_2/Assets/Scripts/Bullet.cs b/magisterka_2/Assets/Scripts/Bullet.cs index 2fe84ce..dbf8644 100644 --- a/magisterka_2/Assets/Scripts/Bullet.cs +++ b/magisterka_2/Assets/Scripts/Bullet.cs @@ -44,7 +44,7 @@ namespace Magisterka.BulletHell _body = gameObject.AddComponent(); _body.gravityScale = 0f; - _body.isKinematic = true; + _body.bodyType = RigidbodyType2D.Kinematic; _body.interpolation = RigidbodyInterpolation2D.Interpolate; _collider = gameObject.AddComponent(); @@ -75,6 +75,22 @@ namespace Magisterka.BulletHell private void OnTriggerEnter2D(Collider2D other) { + if (other.TryGetComponent(out Bullet otherBullet)) + { + if (otherBullet == this || otherBullet._ownerFaction == _ownerFaction) + { + return; + } + + if (otherBullet.gameObject.activeInHierarchy) + { + otherBullet.Despawn(); + } + + Despawn(); + return; + } + if (!other.TryGetComponent(out Health health)) { return; diff --git a/magisterka_2/Assets/Scripts/EffectManager.cs b/magisterka_2/Assets/Scripts/EffectManager.cs index 71a0d3c..1d10a3d 100644 --- a/magisterka_2/Assets/Scripts/EffectManager.cs +++ b/magisterka_2/Assets/Scripts/EffectManager.cs @@ -27,7 +27,6 @@ namespace Magisterka.BulletHell } Instance = this; - DontDestroyOnLoad(gameObject); } public void Initialize(Transform parent) @@ -54,6 +53,13 @@ namespace Magisterka.BulletHell var color = hitFaction == Faction.Player ? playerColor : enemyColor; var endSize = Mathf.Lerp(0.6f, 1.8f, Mathf.Clamp01(intensity + 0.2f)); PlayPulse(position, color, 0.25f, endSize, 0.18f); + + if (hitFaction == Faction.Player) + { + float boosted = Mathf.Clamp01(intensity + 0.3f); + PlayPulse(position, Color.white, 0.18f, Mathf.Lerp(1.4f, 2.8f, boosted), 0.24f); + PlayPulse(position, playerColor, 0.45f, Mathf.Lerp(1.8f, 3.4f, boosted), 0.32f); + } } public void SpawnExplosion(Vector2 position, Faction faction, float size = 2.5f) @@ -66,8 +72,6 @@ namespace Magisterka.BulletHell float duration = 0.22f + i * 0.06f; PlayPulse(position, color, start, end, duration); } - - _cameraShaker?.Shake(0.18f, 0.45f * size); } public void SpawnScreenClear(Vector2 position, float radius) @@ -85,6 +89,11 @@ namespace Magisterka.BulletHell _cameraShaker?.Shake(0.4f, 1.2f * radius); } + public void ShakeCamera(float duration, float strength) + { + _cameraShaker?.Shake(duration, strength); + } + private void Warmup(int count) { for (int i = 0; i < count; i++) diff --git a/magisterka_2/Assets/Scripts/EnemyBlueprint.cs b/magisterka_2/Assets/Scripts/EnemyBlueprint.cs new file mode 100644 index 0000000..b57635c --- /dev/null +++ b/magisterka_2/Assets/Scripts/EnemyBlueprint.cs @@ -0,0 +1,88 @@ +using UnityEngine; + +namespace Magisterka.BulletHell +{ + public enum EnemyMovementStyle + { + ZigZag, + Straight, + Sine, + Orbit + } + + public enum EnemyFireStyle + { + None, + Radial, + Stream, + Burst + } + + /// + /// Runtime configuration for enemy archetypes. + /// + public sealed class EnemyBlueprint + { + public EnemyBlueprint( + string name, + Color color, + float scale, + EnemyMovementStyle movement, + EnemyFireStyle fire, + float moveSpeed, + float horizontalAmplitude, + float horizontalFrequency, + float fireInterval, + int bulletsPerVolley, + int aimedShots, + float bulletSpeed, + float bulletDamage, + float contactDamage, + float health, + int score, + float orbitRadius, + Sprite sprite, + float centeringBias) + { + Name = name; + Color = color; + Scale = scale; + Movement = movement; + Fire = fire; + MoveSpeed = moveSpeed; + HorizontalAmplitude = horizontalAmplitude; + HorizontalFrequency = horizontalFrequency; + FireInterval = fireInterval; + BulletsPerVolley = bulletsPerVolley; + AimedShots = aimedShots; + BulletSpeed = bulletSpeed; + BulletDamage = bulletDamage; + ContactDamage = contactDamage; + Health = health; + Score = score; + OrbitRadius = orbitRadius; + Sprite = sprite; + CenteringBias = centeringBias; + } + + public string Name { get; } + public Color Color { get; } + public float Scale { get; } + public EnemyMovementStyle Movement { get; } + public EnemyFireStyle Fire { get; } + public float MoveSpeed { get; } + public float HorizontalAmplitude { get; } + public float HorizontalFrequency { get; } + public float FireInterval { get; } + public int BulletsPerVolley { get; } + public int AimedShots { get; } + public float BulletSpeed { get; } + public float BulletDamage { get; } + public float ContactDamage { get; } + public float Health { get; } + public int Score { get; } + public float OrbitRadius { get; } + public Sprite Sprite { get; } + public float CenteringBias { get; } + } +} diff --git a/magisterka_2/Assets/Scripts/EnemyBlueprint.cs.meta b/magisterka_2/Assets/Scripts/EnemyBlueprint.cs.meta new file mode 100644 index 0000000..f050eb3 --- /dev/null +++ b/magisterka_2/Assets/Scripts/EnemyBlueprint.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 3c02072540f72f170b8efc887586f480 \ No newline at end of file diff --git a/magisterka_2/Assets/Scripts/EnemyController.cs b/magisterka_2/Assets/Scripts/EnemyController.cs index f9b9b69..e2d8104 100644 --- a/magisterka_2/Assets/Scripts/EnemyController.cs +++ b/magisterka_2/Assets/Scripts/EnemyController.cs @@ -1,4 +1,3 @@ -using System.Collections; using UnityEngine; namespace Magisterka.BulletHell @@ -9,112 +8,206 @@ namespace Magisterka.BulletHell [RequireComponent(typeof(Health))] public class EnemyController : MonoBehaviour { - [SerializeField] private float moveSpeed = 3f; - [SerializeField] private float bulletSpeed = 12f; - [SerializeField] private float bulletDamage = 8f; - [SerializeField] private float fireInterval = 0.6f; - [SerializeField] private int scoreValue = 100; + [SerializeField] private float despawnPadding = 2f; private EnemySpawner _spawner; - private BulletPool _bulletPool; - private PlayerController _player; + private BulletPool _bulletPool; + private PlayerController _player; + private EnemyBlueprint _config; private Health _health; - private float _phaseSeed; - private float _zigzagWidth = 2.8f; private float _difficulty; - private Coroutine _fireRoutine; + private float _moveTime; + private float _phaseSeed; + private float _fireTimer; + private float _baseX; + private Vector2 _worldBounds; - public void Initialize(EnemySpawner spawner, BulletPool bulletPool, PlayerController player, float difficultyScale, float healthValue) + public void Initialize(EnemySpawner spawner, BulletPool bulletPool, PlayerController player, EnemyBlueprint blueprint, float difficultyScale, Vector2 worldBounds) { _spawner = spawner; _bulletPool = bulletPool; _player = player; + _config = blueprint; _difficulty = difficultyScale; _health = GetComponent(); - _health.Configure(Faction.Enemy, healthValue); + _worldBounds = worldBounds; + _health.Configure(Faction.Enemy, blueprint.Health + difficultyScale * 12f); _health.Died += HandleDeath; - _phaseSeed = Random.Range(0f, 360f); - moveSpeed += difficultyScale * 0.8f; - fireInterval = Mathf.Max(0.22f, fireInterval - difficultyScale * 0.08f); - bulletSpeed += difficultyScale * 1.4f; - bulletDamage += difficultyScale * 0.6f; - scoreValue += Mathf.RoundToInt(40f * difficultyScale); - - if (_fireRoutine != null) + var collider = GetComponent(); + if (collider != null) { - StopCoroutine(_fireRoutine); + collider.radius = 0.4f * blueprint.Scale; } - _fireRoutine = StartCoroutine(FireRoutine()); + _phaseSeed = Random.Range(0f, Mathf.PI * 2f); + _baseX = transform.position.x; + _moveTime = 0f; + _fireTimer = GetFireDelay(true); } private void Update() { - var pos = transform.position; - pos += Vector3.down * (moveSpeed * Time.deltaTime); - pos.x += Mathf.Sin(Time.time * (2f + _difficulty) + _phaseSeed) * (_zigzagWidth * Time.deltaTime); - transform.position = pos; + AdvanceMovement(); + HandleFiring(); - if (transform.position.y < -12f) + float bottomLimit = -_worldBounds.y - despawnPadding; + if (transform.position.y < bottomLimit) { _spawner?.DespawnEnemy(this); } } - private IEnumerator FireRoutine() + private void AdvanceMovement() { - yield return new WaitForSeconds(Random.Range(0.15f, fireInterval)); + _moveTime += Time.deltaTime; + var pos = transform.position; + float speed = _config.MoveSpeed + _difficulty * 0.2f; - while (true) + switch (_config.Movement) { - FirePattern(); - yield return new WaitForSeconds(fireInterval); + case EnemyMovementStyle.ZigZag: + pos += Vector3.down * (speed * Time.deltaTime); + pos.x += Mathf.Sin((_moveTime + _phaseSeed) * (1.4f + _config.HorizontalFrequency)) * (_config.HorizontalAmplitude * Time.deltaTime); + break; + case EnemyMovementStyle.Straight: + pos += Vector3.down * (speed * Time.deltaTime); + break; + case EnemyMovementStyle.Sine: + pos += Vector3.down * (speed * Time.deltaTime); + pos.x = _baseX + Mathf.Sin((_moveTime + _phaseSeed) * (1f + _config.HorizontalFrequency)) * _config.HorizontalAmplitude; + break; + case EnemyMovementStyle.Orbit: + float orbitSpeed = _config.HorizontalFrequency + 0.8f + _difficulty * 0.05f; + pos += Vector3.down * (speed * 0.6f * Time.deltaTime); + pos.x = _baseX + Mathf.Cos((_moveTime + _phaseSeed) * orbitSpeed) * _config.OrbitRadius; + pos.y += Mathf.Sin((_moveTime + _phaseSeed) * orbitSpeed) * (_config.OrbitRadius * 0.25f) * Time.deltaTime; + break; } + + if (_config.CenteringBias > 0f) + { + float lerpFactor = Mathf.Clamp01(_config.CenteringBias * Time.deltaTime); + pos.x = Mathf.Lerp(pos.x, 0f, lerpFactor); + } + + ClampWithinBounds(ref pos); + transform.position = pos; + } + + private void HandleFiring() + { + if (_config.Fire == EnemyFireStyle.None) + { + return; + } + + _fireTimer -= Time.deltaTime; + if (_fireTimer > 0f) + { + return; + } + + FirePattern(); + _fireTimer = GetFireDelay(false); } private void FirePattern() { - int radialBullets = Mathf.Clamp(Mathf.RoundToInt(16 + _difficulty * 14f), 12, 48); - float angleStep = 360f / radialBullets; - float angleOffset = Time.time * 60f + _phaseSeed; - - for (int i = 0; i < radialBullets; i++) + switch (_config.Fire) { - float angle = (angleStep * i + angleOffset) * Mathf.Deg2Rad; - var dir = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)); - _bulletPool.Spawn(transform.position, dir, bulletSpeed, bulletDamage); + case EnemyFireStyle.Radial: + FireRadial(); + break; + case EnemyFireStyle.Stream: + FireStream(); + break; + case EnemyFireStyle.Burst: + FireBurst(); + break; } + } - // Additional forward stream for dense barrages. - var forward = Vector2.down; - for (int i = -2; i <= 2; i++) + private void FireRadial() + { + int bullets = Mathf.Clamp(_config.BulletsPerVolley + Mathf.RoundToInt(_difficulty), 6, 24); + float angleStep = 360f / bullets; + float offset = (_moveTime + _phaseSeed) * 60f; + + for (int i = 0; i < bullets; i++) { - var spreadDir = Quaternion.Euler(0f, 0f, i * 8f) * forward; - _bulletPool.Spawn(transform.position, spreadDir, bulletSpeed * 1.2f, bulletDamage * 0.8f); + float angle = (angleStep * i + offset) * Mathf.Deg2Rad; + var dir = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)); + _bulletPool.Spawn(transform.position, dir, _config.BulletSpeed * 0.2f, _config.BulletDamage); + } + } + + private void FireStream() + { + var forward = Vector2.down; + int count = Mathf.Clamp(_config.BulletsPerVolley, 2, 6); + + for (int i = 0; i < count; i++) + { + float spread = (i - (count - 1) * 0.5f) * 7f; + var dir = Quaternion.Euler(0f, 0f, spread) * forward; + _bulletPool.Spawn(transform.position, dir, _config.BulletSpeed * 0.2f, _config.BulletDamage); } if (_player != null && _player.isActiveAndEnabled) { var toPlayer = ((Vector2)_player.transform.position - (Vector2)transform.position).normalized; - int aimedShots = Mathf.Clamp(3 + Mathf.RoundToInt(_difficulty), 3, 8); - for (int i = 0; i < aimedShots; i++) + _bulletPool.Spawn(transform.position, toPlayer, _config.BulletSpeed * 0.22f, _config.BulletDamage); + } + } + + private void FireBurst() + { + int volleys = Mathf.Clamp(_config.BulletsPerVolley, 1, 3); + var baseDir = Vector2.down; + + for (int v = 0; v < volleys; v++) + { + float spread = 12f + v * 4f; + for (int i = -1; i <= 1; i++) { - float sway = (i - (aimedShots - 1) * 0.5f) * 4f; - var dir = Quaternion.Euler(0f, 0f, sway) * toPlayer; - _bulletPool.Spawn(transform.position, dir, bulletSpeed * 1.4f, bulletDamage * 0.9f); + var dir = Quaternion.Euler(0f, 0f, i * spread) * baseDir; + _bulletPool.Spawn(transform.position, dir, _config.BulletSpeed * (0.18f + v * 0.02f), _config.BulletDamage); } } } + private float GetFireDelay(bool first) + { + if (_config.Fire == EnemyFireStyle.None) + { + return float.MaxValue; + } + + float baseDelay = Mathf.Max(0.3f, _config.FireInterval - _difficulty * 0.04f); + return first ? Random.Range(0.2f, baseDelay) : baseDelay; + } + private void HandleDeath(Health _) { - EffectManager.Instance?.SpawnExplosion(transform.position, Faction.Enemy, 3f + _difficulty); - ScoreManager.Instance?.AddScore(scoreValue); + EffectManager.Instance?.SpawnExplosion(transform.position, Faction.Enemy, 2.6f + _difficulty * 0.2f); + ScoreManager.Instance?.AddScore(_config.Score + Mathf.RoundToInt(_difficulty * 20f)); _spawner?.NotifyEnemyKilled(this); Destroy(gameObject); } + private void OnTriggerEnter2D(Collider2D other) + { + if (!other.TryGetComponent(out PlayerController player)) + { + return; + } + + player.ApplyDamage(Mathf.Max(10f, _config.ContactDamage)); + EffectManager.Instance?.SpawnHitEffect(player.transform.position, Faction.Player, 0.9f); + _health?.Kill(); + } + private void OnDestroy() { if (_health != null) @@ -122,5 +215,17 @@ namespace Magisterka.BulletHell _health.Died -= HandleDeath; } } + + private void ClampWithinBounds(ref Vector3 position) + { + if (_worldBounds == Vector2.zero) + { + return; + } + + position.x = Mathf.Clamp(position.x, -_worldBounds.x + 0.6f, _worldBounds.x - 0.6f); + float upperLimit = _worldBounds.y + 4f; + position.y = Mathf.Min(position.y, upperLimit); + } } } diff --git a/magisterka_2/Assets/Scripts/EnemySpawner.cs b/magisterka_2/Assets/Scripts/EnemySpawner.cs index c298e06..5fba890 100644 --- a/magisterka_2/Assets/Scripts/EnemySpawner.cs +++ b/magisterka_2/Assets/Scripts/EnemySpawner.cs @@ -11,21 +11,26 @@ namespace Magisterka.BulletHell { public static EnemySpawner Instance { get; private set; } - [SerializeField] private float totalDuration = 300f; - [SerializeField] private float spawnDelayStart = 1.2f; - [SerializeField] private float spawnDelayEnd = 0.18f; - [SerializeField] private float spawnHeight = 10f; + public float TotalDuration => totalDuration; + public bool HasActiveEnemies => _liveEnemies.Count > 0; + + [SerializeField] private float totalDuration = 90f; + [SerializeField] private float spawnDelayStart = 1.6f; + [SerializeField] private float spawnDelayEnd = 0.45f; + [SerializeField] private float spawnInset = 0.3f; [SerializeField] private float horizontalPadding = 1.2f; private readonly List _liveEnemies = new List(); private BulletPool _enemyBulletPool; private Camera _camera; - private Sprite _enemySprite; - private float _elapsed; - private Vector2 _worldBounds; - private PlayerController _player; + private Vector2 _worldBounds; + private EnemyBlueprint[] _blueprints; + private Coroutine _spawnRoutine; + private float _elapsed; + private bool _spawningActive; + private int _introIndex; public void Initialize(Camera camera, BulletPool enemyPool, PlayerController player, Vector2 worldBounds) { @@ -41,9 +46,12 @@ namespace Magisterka.BulletHell _enemyBulletPool = enemyPool; _player = player; _worldBounds = worldBounds; - _enemySprite = BuildEnemySprite(); + _blueprints = BuildBlueprints(); - StartCoroutine(SpawnRoutine()); + _spawningActive = true; + _elapsed = 0f; + _introIndex = 0; + _spawnRoutine = StartCoroutine(SpawnRoutine()); } public void NotifyEnemyKilled(EnemyController enemy) @@ -64,8 +72,9 @@ namespace Magisterka.BulletHell public void ClearScreen() { - foreach (var enemy in _liveEnemies.ToArray()) + for (int i = _liveEnemies.Count - 1; i >= 0; i--) { + var enemy = _liveEnemies[i]; if (enemy != null && enemy.TryGetComponent(out Health health)) { health.Kill(); @@ -75,17 +84,34 @@ namespace Magisterka.BulletHell _enemyBulletPool?.ClearLiveBullets(); } + public void StopSpawning() + { + _spawningActive = false; + + if (_spawnRoutine != null) + { + StopCoroutine(_spawnRoutine); + _spawnRoutine = null; + } + } + + public void StopAndClear() + { + StopSpawning(); + ClearScreen(); + } + private IEnumerator SpawnRoutine() { yield return new WaitForSeconds(1.5f); - while (_elapsed < totalDuration) + while (_spawningActive && _elapsed < totalDuration) { SpawnWave(); float progress = Mathf.Clamp01(_elapsed / totalDuration); float delay = Mathf.Lerp(spawnDelayStart, spawnDelayEnd, progress); - delay = Mathf.Max(0.08f, delay); + delay = Mathf.Max(0.4f, delay); yield return new WaitForSeconds(delay); _elapsed += delay; } @@ -93,49 +119,161 @@ namespace Magisterka.BulletHell private void SpawnWave() { - int targetCount = Mathf.Clamp(Mathf.RoundToInt(3 + _elapsed * 0.12f), 4, 18); - float difficulty = Mathf.Clamp01(_elapsed / totalDuration) * 8f; + float progression = Mathf.Clamp01(_elapsed / totalDuration); + + if (_introIndex < _blueprints.Length) + { + var introBlueprint = _blueprints[_introIndex]; + _introIndex++; + float topEdge = _camera.transform.position.y + _camera.orthographicSize; + Vector2 spawnPos = new Vector2(0f, topEdge - Mathf.Max(0.02f, spawnInset)); + SpawnEnemy(spawnPos, introBlueprint, Mathf.Lerp(0.5f, 2.5f, progression)); + return; + } + + int available = Mathf.Clamp(_introIndex, 1, _blueprints.Length); + int targetCount = Mathf.Clamp(Mathf.RoundToInt(1 + _elapsed * 0.05f), 2, 10); + float difficulty = Mathf.Lerp(1.2f, 6f, progression); for (int i = 0; i < targetCount; i++) { - Vector2 spawnPos = PickSpawnPosition(i, targetCount); - SpawnEnemy(spawnPos, difficulty); + var blueprint = _blueprints[Random.Range(0, available)]; + Vector2 spawnPos = PickSpawnPosition(i, targetCount, blueprint.CenteringBias); + SpawnEnemy(spawnPos, blueprint, difficulty); } } - private Vector2 PickSpawnPosition(int index, int total) + private Vector2 PickSpawnPosition(int index, int total, float centeringBias) { float camWidth = _worldBounds.x; float step = (camWidth * 2f - horizontalPadding * 2f) / Mathf.Max(1, total - 1); float x = -camWidth + horizontalPadding + step * index + Random.Range(-0.6f, 0.6f); - float y = _camera.orthographicSize + spawnHeight; + if (centeringBias > 0f) + { + float bias = Mathf.Clamp01(centeringBias * 0.15f); + x = Mathf.Lerp(x, 0f, bias); + } + float top = _camera.transform.position.y + _camera.orthographicSize; + float y = top - Mathf.Max(0.02f, spawnInset); return new Vector2(x, y); } - private void SpawnEnemy(Vector2 position, float difficulty) + private void SpawnEnemy(Vector2 position, EnemyBlueprint blueprint, float difficulty) { - var enemyGO = new GameObject("Enemy"); + var enemyGO = new GameObject($"Enemy_{blueprint.Name}"); enemyGO.transform.SetParent(transform, false); enemyGO.transform.position = position; - enemyGO.transform.localScale = Vector3.one * 1.6f; + enemyGO.transform.localScale = Vector3.one * blueprint.Scale; var renderer = enemyGO.AddComponent(); - renderer.sprite = _enemySprite; + renderer.sprite = blueprint.Sprite; renderer.sortingOrder = 10; - renderer.color = Color.Lerp(new Color(1f, 0.6f, 0.3f, 1f), new Color(1f, 0.1f, 0.2f, 1f), Mathf.Clamp01(difficulty / 8f)); + renderer.color = blueprint.Color; var collider = enemyGO.AddComponent(); collider.isTrigger = true; - collider.radius = 0.7f; + collider.radius = 0.45f * blueprint.Scale; - var health = enemyGO.AddComponent(); + enemyGO.AddComponent(); var controller = enemyGO.AddComponent(); - controller.Initialize(this, _enemyBulletPool, _player, difficulty, 30f + difficulty * 20f); + controller.Initialize(this, _enemyBulletPool, _player, blueprint, difficulty, _worldBounds); _liveEnemies.Add(controller); } + private EnemyBlueprint[] BuildBlueprints() + { + var blazeColor = new Color(1f, 0.55f, 0.25f, 1f); + var strikerColor = new Color(0.55f, 0.45f, 1f, 1f); + var cascadeColor = new Color(0.35f, 1f, 0.65f, 1f); + var interceptorColor = new Color(1f, 0.9f, 0.3f, 1f); + + return new[] + { + new EnemyBlueprint( + "Blazer", + blazeColor, + 1.6f, + EnemyMovementStyle.ZigZag, + EnemyFireStyle.Radial, + 2.8f, + 2.8f, + 1.1f, + 1.3f, + 12, + 3, + 13f, + 7f, + 25f, + 55f, + 140, + 0f, + GenerateSprite(EnemyShape.Diamond, blazeColor), + 1.2f), + new EnemyBlueprint( + "Striker", + strikerColor, + 1.4f, + EnemyMovementStyle.Straight, + EnemyFireStyle.Stream, + 3.3f, + 0f, + 0f, + 1f, + 4, + 2, + 18f, + 9f, + 30f, + 65f, + 160, + 0f, + GenerateSprite(EnemyShape.Triangle, strikerColor), + 1.5f), + new EnemyBlueprint( + "Cascade", + cascadeColor, + 1.7f, + EnemyMovementStyle.Sine, + EnemyFireStyle.Burst, + 2.5f, + 3.2f, + 1.2f, + 1.8f, + 2, + 0, + 12f, + 10f, + 35f, + 80f, + 200, + 0f, + GenerateSprite(EnemyShape.Disc, cascadeColor), + 1.1f), + new EnemyBlueprint( + "Interceptor", + interceptorColor, + 1.3f, + EnemyMovementStyle.Orbit, + EnemyFireStyle.None, + 5.2f, + 0f, + 1.6f, + 0f, + 0, + 0, + 0f, + 0f, + 45f, + 50f, + 180, + 1.9f, + GenerateSprite(EnemyShape.Arrow, interceptorColor), + 1.8f) + }; + } + private void OnDestroy() { if (Instance == this) @@ -144,7 +282,7 @@ namespace Magisterka.BulletHell } } - private Sprite BuildEnemySprite() + private Sprite GenerateSprite(EnemyShape shape, Color color) { const int size = 64; var texture = new Texture2D(size, size, TextureFormat.RGBA32, false) @@ -152,18 +290,42 @@ namespace Magisterka.BulletHell filterMode = FilterMode.Point }; - var center = new Vector2(size / 2f, size / 2f); var pixels = new Color[size * size]; - var body = new Color(1f, 0.35f, 0.35f, 1f); + var center = new Vector2(size * 0.5f, size * 0.5f); for (int y = 0; y < size; y++) { for (int x = 0; x < size; x++) { int index = x + y * size; - float dist = Vector2.Distance(center, new Vector2(x, y)); - float threshold = size * 0.35f + Mathf.Sin(y * 0.2f) * 2f; - pixels[index] = dist <= threshold ? body : new Color(0f, 0f, 0f, 0f); + bool fill = false; + switch (shape) + { + case EnemyShape.Disc: + fill = Vector2.Distance(center, new Vector2(x, y)) <= size * 0.32f; + break; + case EnemyShape.Triangle: + { + float ny = 1f - (y / (float)(size - 1)); + float halfWidth = ny * size * 0.35f; + fill = Mathf.Abs(x - center.x) <= halfWidth && ny > 0.1f; + break; + } + case EnemyShape.Diamond: + fill = Mathf.Abs(x - center.x) + Mathf.Abs(y - center.y) <= size * 0.32f; + break; + case EnemyShape.Arrow: + { + float ny = y / (float)(size - 1); + float half = Mathf.Lerp(size * 0.1f, size * 0.35f, ny); + bool head = ny > 0.55f && Mathf.Abs(x - center.x) <= Mathf.Lerp(size * 0.05f, size * 0.4f, Mathf.Clamp01((ny - 0.55f) / 0.45f)); + bool body = ny <= 0.55f && Mathf.Abs(x - center.x) <= half; + fill = head || body; + break; + } + } + + pixels[index] = fill ? color : new Color(0f, 0f, 0f, 0f); } } @@ -172,5 +334,13 @@ namespace Magisterka.BulletHell return Sprite.Create(texture, new Rect(0, 0, size, size), new Vector2(0.5f, 0.5f), 64f); } + + private enum EnemyShape + { + Disc, + Triangle, + Diamond, + Arrow + } } } diff --git a/magisterka_2/Assets/Scripts/GameDirector.cs b/magisterka_2/Assets/Scripts/GameDirector.cs new file mode 100644 index 0000000..17df9cf --- /dev/null +++ b/magisterka_2/Assets/Scripts/GameDirector.cs @@ -0,0 +1,103 @@ +using UnityEngine; +using UnityEngine.UI; + +namespace Magisterka.BulletHell +{ + /// + /// Oversees overall game flow: timer, victory detection, and difficulty pacing hooks. + /// + public class GameDirector : MonoBehaviour + { + public static GameDirector Instance { get; private set; } + + [SerializeField] private float totalDuration = 90f; + + private EnemySpawner _spawner; + private PlayerController _player; + private ScoreManager _scoreManager; + private Text _timerText; + + private float _timeRemaining; + private bool _timerRunning; + private bool _timerExpired; + private bool _victoryTriggered; + + public float TotalDuration => totalDuration; + + public void Initialize(EnemySpawner spawner, PlayerController player, ScoreManager scoreManager, Text timerText, float duration) + { + _spawner = spawner; + _player = player; + _scoreManager = scoreManager; + _timerText = timerText; + totalDuration = duration > 0f ? duration : totalDuration; + _timeRemaining = totalDuration; + _timerRunning = true; + _timerExpired = false; + _victoryTriggered = false; + UpdateTimerLabel(); + } + + private void Awake() + { + if (Instance != null && Instance != this) + { + Destroy(gameObject); + return; + } + + Instance = this; + } + + private void Update() + { + if (_timerRunning && !_timerExpired) + { + _timeRemaining -= Time.deltaTime; + if (_timeRemaining <= 0f) + { + _timeRemaining = 0f; + _timerExpired = true; + _timerRunning = false; + _spawner?.StopSpawning(); + } + + UpdateTimerLabel(); + } + + if (_timerExpired && !_victoryTriggered) + { + bool enemiesRemain = _spawner != null && _spawner.HasActiveEnemies; + if (!enemiesRemain) + { + TriggerVictory(); + } + } + } + + public void OnPlayerGameOver() + { + _timerRunning = false; + } + + private void TriggerVictory() + { + _victoryTriggered = true; + _scoreManager?.ShowVictory(); + _player?.HandleVictory(); + } + + private void UpdateTimerLabel() + { + if (_timerText == null) + { + return; + } + + int seconds = Mathf.CeilToInt(_timeRemaining); + int minutes = seconds / 60; + seconds %= 60; + _timerText.text = $"{minutes:00}:{seconds:00}"; + } + } +} diff --git a/magisterka_2/Assets/Scripts/GameDirector.cs.meta b/magisterka_2/Assets/Scripts/GameDirector.cs.meta new file mode 100644 index 0000000..56d3e1b --- /dev/null +++ b/magisterka_2/Assets/Scripts/GameDirector.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 40b997f70689be4e1aa9b72fabe123ea \ No newline at end of file diff --git a/magisterka_2/Assets/Scripts/GameInitializer.cs b/magisterka_2/Assets/Scripts/GameInitializer.cs index 8fb9092..fb981ae 100644 --- a/magisterka_2/Assets/Scripts/GameInitializer.cs +++ b/magisterka_2/Assets/Scripts/GameInitializer.cs @@ -11,6 +11,15 @@ namespace Magisterka.BulletHell { [SerializeField] private float cameraSize = 6.5f; + private struct HudElements + { + public Text Score; + public Image[] Lives; + public Text Status; + public Text Timer; + public Image DamageOverlay; + } + private void Awake() { BuildCamera(); @@ -49,25 +58,34 @@ namespace Magisterka.BulletHell var effectManager = new GameObject("EffectManager").AddComponent(); effectManager.Initialize(root); - var scoreManager = new GameObject("ScoreManager").AddComponent(); - var scoreText = CreateScoreUI(); - scoreManager.Initialize(scoreText); + var scoreManagerGO = new GameObject("ScoreManager"); + scoreManagerGO.transform.SetParent(root, false); + var scoreManager = scoreManagerGO.AddComponent(); var pools = new GameObject("BulletPools").transform; pools.SetParent(root, false); - var playerPool = BulletPool.Create(pools, "PlayerBullets", new Color(0.35f, 0.9f, 1f, 1f), Faction.Player, 600); - var enemyPool = BulletPool.Create(pools, "EnemyBullets", new Color(1f, 0.4f, 0.2f, 1f), Faction.Enemy, 1200); + var playerPool = BulletPool.Create(pools, "PlayerBullets", new Color(0.35f, 0.9f, 1f, 1f), Faction.Player, 400); + var enemyPool = BulletPool.Create(pools, "EnemyBullets", new Color(1f, 0.4f, 0.2f, 1f), Faction.Enemy, 900); var player = PlayerController.CreatePlayer(root, playerPool, bounds); + var hud = CreateHud(player.MaxLives); + scoreManager.Initialize(hud.Score, hud.Lives, hud.Status, hud.DamageOverlay); + scoreManager.SetLives(player.CurrentLives); + var spawnerGO = new GameObject("EnemySpawner"); spawnerGO.transform.SetParent(root, false); var spawner = spawnerGO.AddComponent(); spawner.Initialize(cam, enemyPool, player, bounds); + + var directorGO = new GameObject("GameDirector"); + directorGO.transform.SetParent(root, false); + var director = directorGO.AddComponent(); + director.Initialize(spawner, player, scoreManager, hud.Timer, 90f); } - private Text CreateScoreUI() + private HudElements CreateHud(int lives) { var canvasGO = new GameObject("Canvas"); var canvas = canvasGO.AddComponent(); @@ -83,7 +101,7 @@ namespace Magisterka.BulletHell { label.font = Font.CreateDynamicFontFromOSFont("Arial", 32); } - label.fontSize = 32; + label.fontSize = 24; label.alignment = TextAnchor.UpperLeft; label.color = Color.white; label.text = "Score: 0"; @@ -92,10 +110,116 @@ namespace Magisterka.BulletHell rect.anchorMin = new Vector2(0f, 1f); rect.anchorMax = new Vector2(0f, 1f); rect.pivot = new Vector2(0f, 1f); - rect.anchoredPosition = new Vector2(20f, -20f); + rect.anchoredPosition = new Vector2(12f, -12f); + + var livesGO = new GameObject("Lives"); + livesGO.transform.SetParent(canvasGO.transform, false); + var livesRect = livesGO.AddComponent(); + livesRect.anchorMin = new Vector2(0f, 1f); + livesRect.anchorMax = new Vector2(0f, 1f); + livesRect.pivot = new Vector2(0f, 1f); + livesRect.anchoredPosition = new Vector2(12f, -40f); + + var lifeSprite = BuildLifeSprite(); + var lifeIcons = new Image[Mathf.Max(1, lives)]; + + for (int i = 0; i < lifeIcons.Length; i++) + { + var iconGO = new GameObject($"Life_{i}"); + iconGO.transform.SetParent(livesGO.transform, false); + var icon = iconGO.AddComponent(); + icon.sprite = lifeSprite; + icon.color = new Color(0.3f + i * 0.05f, 0.85f, 1f, 0.9f); + var iconRect = icon.rectTransform; + iconRect.anchorMin = new Vector2(0f, 1f); + iconRect.anchorMax = new Vector2(0f, 1f); + iconRect.pivot = new Vector2(0f, 1f); + iconRect.anchoredPosition = new Vector2(i * 26f, 0f); + iconRect.sizeDelta = new Vector2(22f, 22f); + lifeIcons[i] = icon; + } + + var gameOverGO = new GameObject("GameOverLabel"); + gameOverGO.transform.SetParent(canvasGO.transform, false); + var gameOver = gameOverGO.AddComponent(); + gameOver.font = label.font; + gameOver.fontSize = 48; + gameOver.alignment = TextAnchor.MiddleCenter; + gameOver.color = new Color(1f, 0.55f, 0.55f, 1f); + gameOver.text = "GAME OVER"; + gameOver.enabled = false; + + var goRect = gameOver.rectTransform; + goRect.anchorMin = new Vector2(0.5f, 0.5f); + goRect.anchorMax = new Vector2(0.5f, 0.5f); + goRect.pivot = new Vector2(0.5f, 0.5f); + goRect.anchoredPosition = Vector2.zero; + + var timerGO = new GameObject("TimerLabel"); + timerGO.transform.SetParent(canvasGO.transform, false); + var timer = timerGO.AddComponent(); + timer.font = label.font; + timer.fontSize = 24; + timer.alignment = TextAnchor.UpperRight; + timer.color = Color.white; + timer.text = "05:00"; + + var timerRect = timer.rectTransform; + timerRect.anchorMin = new Vector2(1f, 1f); + timerRect.anchorMax = new Vector2(1f, 1f); + timerRect.pivot = new Vector2(1f, 1f); + timerRect.anchoredPosition = new Vector2(-12f, -12f); + + var overlayGO = new GameObject("DamageOverlay"); + overlayGO.transform.SetParent(canvasGO.transform, false); + var overlay = overlayGO.AddComponent(); + overlay.raycastTarget = false; + overlay.color = new Color(1f, 0.2f, 0.2f, 0f); + var overlayRect = overlay.rectTransform; + overlayRect.anchorMin = Vector2.zero; + overlayRect.anchorMax = Vector2.one; + overlayRect.pivot = new Vector2(0.5f, 0.5f); + overlayRect.offsetMin = Vector2.zero; + overlayRect.offsetMax = Vector2.zero; + + overlayGO.transform.SetAsLastSibling(); canvasGO.transform.SetParent(transform, false); - return label; + return new HudElements + { + Score = label, + Lives = lifeIcons, + Status = gameOver, + Timer = timer, + DamageOverlay = overlay + }; + } + + private static Sprite BuildLifeSprite() + { + const int size = 32; + var texture = new Texture2D(size, size, TextureFormat.RGBA32, false) + { + filterMode = FilterMode.Bilinear + }; + + var center = new Vector2(size / 2f, size / 2f); + var pixels = new Color[size * size]; + + for (int y = 0; y < size; y++) + { + for (int x = 0; x < size; x++) + { + int index = x + y * size; + float dist = Vector2.Distance(center, new Vector2(x, y)); + pixels[index] = dist <= size * 0.42f ? Color.white : new Color(0f, 0f, 0f, 0f); + } + } + + texture.SetPixels(pixels); + texture.Apply(); + + return Sprite.Create(texture, new Rect(0, 0, size, size), new Vector2(0.5f, 0.5f), 64f); } } } diff --git a/magisterka_2/Assets/Scripts/Health.cs b/magisterka_2/Assets/Scripts/Health.cs index caf466f..0eed5e2 100644 --- a/magisterka_2/Assets/Scripts/Health.cs +++ b/magisterka_2/Assets/Scripts/Health.cs @@ -14,6 +14,7 @@ namespace Magisterka.BulletHell private float _current; public event Action Died; + public event Action Damaged; public Faction Faction => faction; public float MaxHealth => maxHealth; @@ -40,6 +41,7 @@ namespace Magisterka.BulletHell _current -= damage; EffectManager.Instance?.SpawnHitEffect(hitPoint, faction, Mathf.Clamp01(damage / maxHealth)); + Damaged?.Invoke(damage, hitPoint); if (_current <= 0f) { @@ -60,6 +62,7 @@ namespace Magisterka.BulletHell } _current = 0f; + Damaged?.Invoke(maxHealth, transform.position); Die(); } diff --git a/magisterka_2/Assets/Scripts/PlayerController.cs b/magisterka_2/Assets/Scripts/PlayerController.cs index 0113a8d..1f10a7d 100644 --- a/magisterka_2/Assets/Scripts/PlayerController.cs +++ b/magisterka_2/Assets/Scripts/PlayerController.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Collections.Generic; using UnityEngine; namespace Magisterka.BulletHell @@ -16,30 +17,43 @@ namespace Magisterka.BulletHell [SerializeField] private float focusSpeedMultiplier = 0.6f; [Header("Offense")] - [SerializeField] private float fireRate = 0.08f; - [SerializeField] private float bulletSpeed = 26f; - [SerializeField] private float bulletDamage = 16f; - [SerializeField] private int volleyWidth = 6; + [SerializeField] private float fireRate = 0.12f; + [SerializeField] private float bulletSpeed = 22f; + [SerializeField] private float bulletDamage = 14f; + [SerializeField] private int volleyWidth = 5; - [Header("Survivability")] - [SerializeField] private int maxLives = 5; - [SerializeField] private float respawnDelay = 1.4f; + [Header("Survivability")] + [SerializeField] private int maxLives = 1; + [SerializeField] private float respawnDelay = 0.7f; [SerializeField] private float invulnerabilityDuration = 2f; + [Header("Progression")] + [SerializeField] private int baseLevelThreshold = 1500; + [Header("Controls")] [SerializeField] private KeyCode fireKey = KeyCode.Z; [SerializeField] private KeyCode modifierKey = KeyCode.LeftShift; [SerializeField] private KeyCode bombKey = KeyCode.X; - private BulletPool _playerBulletPool; + private const int MaxLevel = 12; + + private BulletPool _playerBulletPool; private Health _health; private SpriteRenderer _renderer; private Vector2 _bounds = new Vector2(8f, 4.5f); private float _fireCooldown; private bool _bombAvailable = true; - private int _remainingLives; + private int _lives; + private bool _canControl = true; + private bool _isAlive = true; private bool _isInvulnerable; + private bool _victoryAchieved; private Color _baseColor; + private Coroutine _hitFlashRoutine; + private Coroutine _levelFlashRoutine; + private int _level; + private float _baseBulletSpeed; + private int _baseVolleyWidth; public void Initialize(BulletPool bulletPool, Vector2 movementBounds) { @@ -49,12 +63,29 @@ namespace Magisterka.BulletHell _health = GetComponent(); _health.Configure(Faction.Player, 60f); _health.Died += HandleDeath; + _health.Damaged += HandleDamageTaken; _renderer = GetComponent(); _baseColor = _renderer != null ? _renderer.color : Color.cyan; - _remainingLives = Mathf.Max(0, maxLives - 1); + _baseBulletSpeed = bulletSpeed; + _baseVolleyWidth = volleyWidth; + _level = 0; + + _lives = Mathf.Max(0, maxLives); + ScoreManager.Instance?.SetLives(_lives); transform.position = new Vector3(0f, -_bounds.y + 1.5f, 0f); + _isAlive = true; + _canControl = true; + _bombAvailable = true; + _fireCooldown = 0f; + _victoryAchieved = false; + + if (ScoreManager.Instance != null) + { + ScoreManager.Instance.ScoreChanged += HandleScoreChanged; + HandleScoreChanged(ScoreManager.Instance.CurrentScore); + } } public static PlayerController CreatePlayer(Transform parent, BulletPool playerBulletPool, Vector2 movementBounds) @@ -68,30 +99,48 @@ namespace Magisterka.BulletHell renderer.color = new Color(0.25f, 0.85f, 1f, 1f); var collider = playerGO.AddComponent(); - collider.radius = 0.55f; + collider.radius = 1f / 64f; collider.isTrigger = true; - var health = playerGO.AddComponent(); + playerGO.AddComponent(); var controller = playerGO.AddComponent(); controller.Initialize(playerBulletPool, movementBounds); return controller; } + public int MaxLives => Mathf.Max(1, maxLives); + public int CurrentLives => Mathf.Max(0, _lives); + private void Update() { - HandleMovement(); - HandleShooting(); - HandleBomb(); + if (_victoryAchieved) + { + return; + } if (_fireCooldown > 0f) { _fireCooldown -= Time.deltaTime; } + + if (!_canControl) + { + return; + } + + HandleMovement(); + HandleShooting(); + HandleBomb(); } private void HandleMovement() { + if (!_canControl) + { + return; + } + float x = 0f; float y = 0f; @@ -111,6 +160,11 @@ namespace Magisterka.BulletHell private void HandleShooting() { + if (!_canControl || !_isAlive) + { + return; + } + bool wantsFire = Input.GetKey(fireKey) || Input.GetMouseButton(0); if (!wantsFire || _fireCooldown > 0f) { @@ -123,23 +177,34 @@ namespace Magisterka.BulletHell private void FireVolley() { - var origin = transform.position + Vector3.up * 0.4f; - int half = volleyWidth / 2; + var origin = transform.position + Vector3.up * 0.35f; + float speed = GetLevelAdjustedBulletSpeed(); + int count = Mathf.Clamp(_baseVolleyWidth + Mathf.Max(0, _level), 1, 17); + float mid = (count - 1) * 0.5f; + float spreadStep = Mathf.Lerp(5f, 8f, Mathf.Clamp01(_level / 12f)); - for (int i = -half; i <= half; i++) + for (int i = 0; i < count; i++) { - float spread = i * 6f; + float offsetIndex = i - mid; + float spread = offsetIndex * spreadStep; var direction = Quaternion.Euler(0f, 0f, spread) * Vector2.up; - var offset = direction * 0.25f * Mathf.Abs(i); - _playerBulletPool.Spawn(origin + (Vector3)offset, direction, bulletSpeed, bulletDamage); + var offset = new Vector3(offsetIndex * 0.18f, 0f, 0f); + _playerBulletPool.Spawn(origin + offset, direction, speed, bulletDamage); } - EffectManager.Instance?.SpawnHitEffect(origin + Vector3.up * 0.2f, Faction.Player, 0.6f); + foreach (var extra in GetExtraDirectionsForLevel()) + { + var dir = extra.normalized; + var spawnPos = transform.position + (Vector3)(dir * 0.4f); + _playerBulletPool.Spawn(spawnPos, dir, speed, bulletDamage); + } + + EffectManager.Instance?.SpawnHitEffect(origin + Vector3.up * 0.1f, Faction.Player, 0.4f + _level * 0.05f); } private void HandleBomb() { - if (!_bombAvailable) + if (!_bombAvailable || !_canControl || !_isAlive) { return; } @@ -156,6 +221,7 @@ namespace Magisterka.BulletHell private IEnumerator PerformBomb() { + _canControl = false; _isInvulnerable = true; _renderer.color = Color.white; EffectManager.Instance?.SpawnScreenClear(transform.position, 18f); @@ -186,10 +252,27 @@ namespace Magisterka.BulletHell _isInvulnerable = false; _renderer.color = _baseColor; + _canControl = true; } private void HandleDeath(Health _) { + if (!_isAlive) + { + return; + } + + _isAlive = false; + _canControl = false; + _bombAvailable = false; + if (_hitFlashRoutine != null) + { + StopCoroutine(_hitFlashRoutine); + _renderer.color = _baseColor; + _hitFlashRoutine = null; + } + _lives -= 1; + ScoreManager.Instance?.SetLives(Mathf.Max(0, _lives)); StartCoroutine(RespawnRoutine()); } @@ -199,16 +282,23 @@ namespace Magisterka.BulletHell _renderer.enabled = false; _isInvulnerable = true; - if (_remainingLives < 0) + if (_lives < 0) { + yield return new WaitForSeconds(1f); + ScoreManager.Instance?.ShowGameOver(); + EnemySpawner.Instance?.StopAndClear(); + GameDirector.Instance?.OnPlayerGameOver(); gameObject.SetActive(false); yield break; } yield return new WaitForSeconds(respawnDelay); + EnemySpawner.Instance?.ClearScreen(); transform.position = new Vector3(0f, -_bounds.y + 1.5f, 0f); _renderer.enabled = true; _health.RestoreFull(); + _fireCooldown = fireRate; + _canControl = true; float timer = invulnerabilityDuration; while (timer > 0f) @@ -229,7 +319,9 @@ namespace Magisterka.BulletHell } _isInvulnerable = false; - _remainingLives--; + _renderer.color = _baseColor; + _isAlive = true; + _canControl = true; } private void OnDestroy() @@ -237,6 +329,12 @@ namespace Magisterka.BulletHell if (_health != null) { _health.Died -= HandleDeath; + _health.Damaged -= HandleDamageTaken; + } + + if (ScoreManager.Instance != null) + { + ScoreManager.Instance.ScoreChanged -= HandleScoreChanged; } if (Instance == this) @@ -249,7 +347,7 @@ namespace Magisterka.BulletHell public void ApplyDamage(float value) { - if (_isInvulnerable) + if (_isInvulnerable || !_isAlive || _victoryAchieved) { return; } @@ -257,6 +355,178 @@ namespace Magisterka.BulletHell _health.ApplyDamage(value, transform.position); } + public void HandleVictory() + { + _victoryAchieved = true; + _canControl = false; + _isInvulnerable = true; + if (_renderer != null) + { + _renderer.color = Color.white; + } + } + + private void HandleDamageTaken(float damage, Vector2 hitPoint) + { + if (_renderer == null || _isInvulnerable || !_isAlive) + { + return; + } + + if (_hitFlashRoutine != null) + { + StopCoroutine(_hitFlashRoutine); + _renderer.color = _baseColor; + } + + _hitFlashRoutine = StartCoroutine(HitFlashRoutine()); + float strength = Mathf.Clamp01(damage / Mathf.Max(1f, _health.MaxHealth * 0.4f)); + ScoreManager.Instance?.TriggerDamageFlash(strength); + EffectManager.Instance?.ShakeCamera(0.22f, 0.65f); + } + + private IEnumerator HitFlashRoutine() + { + const float duration = 0.35f; + float elapsed = 0f; + + while (elapsed < duration) + { + elapsed += Time.deltaTime; + float t = Mathf.PingPong(elapsed * 18f, 1f); + _renderer.color = Color.Lerp(_baseColor, Color.white, t); + yield return null; + } + + _renderer.color = _baseColor; + _hitFlashRoutine = null; + } + + private float GetLevelAdjustedBulletSpeed() + { + return _baseBulletSpeed; + } + + private IEnumerable GetExtraDirectionsForLevel() + { + if (_level <= 0) + { + yield break; + } + + if (_level >= 2) + { + yield return Vector2.left; + yield return Vector2.right; + } + + if (_level >= 4) + { + yield return (Vector2.left + Vector2.up).normalized; + yield return (Vector2.right + Vector2.up).normalized; + } + + if (_level >= 6) + { + yield return Vector2.down; + } + + if (_level >= 8) + { + yield return (Vector2.left + Vector2.down * 0.7f).normalized; + yield return (Vector2.right + Vector2.down * 0.7f).normalized; + } + } + + private void HandleScoreChanged(int score) + { + int newLevel = CalculateLevelFromScore(score); + + if (newLevel > _level) + { + for (int next = _level + 1; next <= newLevel; next++) + { + _level = next; + HandleLevelUpFeedback(); + } + } + else if (newLevel < _level) + { + _level = newLevel; + } + } + + private int CalculateLevelFromScore(int score) + { + if (baseLevelThreshold <= 0) + { + return 0; + } + + int level = 0; + int increment = baseLevelThreshold; + int threshold = baseLevelThreshold; + + while (level < MaxLevel && score >= threshold) + { + level++; + increment *= 2; + threshold += increment; + } + + return level; + } + + private void HandleLevelUpFeedback() + { + EffectManager.Instance?.SpawnExplosion(transform.position, Faction.Player, 2f + _level * 0.2f); + EffectManager.Instance?.ShakeCamera(0.18f, 0.4f + _level * 0.05f); + + if (_hitFlashRoutine != null) + { + StopCoroutine(_hitFlashRoutine); + _hitFlashRoutine = null; + } + + if (_renderer != null) + { + _renderer.color = _baseColor; + } + + if (_levelFlashRoutine != null) + { + StopCoroutine(_levelFlashRoutine); + } + + _levelFlashRoutine = StartCoroutine(LevelUpFlashRoutine()); + } + + private IEnumerator LevelUpFlashRoutine() + { + float duration = 0.6f; + float elapsed = 0f; + var glowColor = Color.Lerp(_baseColor, new Color(1f, 0.95f, 0.35f, 1f), 0.85f); + + while (elapsed < duration) + { + elapsed += Time.deltaTime; + float t = Mathf.PingPong(elapsed * 6f, 1f); + if (_renderer != null) + { + _renderer.color = Color.Lerp(glowColor, _baseColor, t); + } + + yield return null; + } + + if (_renderer != null) + { + _renderer.color = _baseColor; + } + + _levelFlashRoutine = null; + } + private static Sprite BuildPlayerSprite() { const int size = 64; diff --git a/magisterka_2/Assets/Scripts/ScoreManager.cs b/magisterka_2/Assets/Scripts/ScoreManager.cs index 57e513d..3209f1f 100644 --- a/magisterka_2/Assets/Scripts/ScoreManager.cs +++ b/magisterka_2/Assets/Scripts/ScoreManager.cs @@ -1,3 +1,5 @@ +using System; +using System.Collections; using UnityEngine; using UnityEngine.UI; @@ -11,7 +13,18 @@ namespace Magisterka.BulletHell public static ScoreManager Instance { get; private set; } private Text _scoreText; + private Image[] _lifeIcons; + private Text _statusText; + private Image _damageOverlay; private int _score; + private int _queuedLives = -1; + private string _queuedStatus; + private Color _queuedStatusColor = Color.white; + private Coroutine _damageFlashRoutine; + + public event Action ScoreChanged; + + public int CurrentScore => _score; private void Awake() { @@ -22,25 +35,101 @@ namespace Magisterka.BulletHell } Instance = this; - DontDestroyOnLoad(gameObject); } - public void Initialize(Text scoreText) + public void Initialize(Text scoreText, Image[] lifeIcons, Text statusText, Image damageOverlay) { _scoreText = scoreText; + _lifeIcons = lifeIcons; + _statusText = statusText; + _damageOverlay = damageOverlay; + HideStatus(); + ResetScore(); + + if (_queuedLives >= 0) + { + ApplyLives(_queuedLives); + _queuedLives = -1; + } + + if (!string.IsNullOrEmpty(_queuedStatus)) + { + ShowStatus(_queuedStatus, _queuedStatusColor); + _queuedStatus = null; + } + + if (_damageOverlay != null) + { + var color = _damageOverlay.color; + color.a = 0f; + _damageOverlay.color = color; + } + + ScoreChanged?.Invoke(_score); } public void AddScore(int value) { _score += value; UpdateLabel(); + ScoreChanged?.Invoke(_score); } public void ResetScore() { _score = 0; UpdateLabel(); + HideStatus(); + ScoreChanged?.Invoke(_score); + } + + public void SetLives(int lives) + { + if (_lifeIcons == null || _lifeIcons.Length == 0) + { + _queuedLives = lives; + return; + } + + ApplyLives(lives); + } + + public void ShowGameOver() + { + ShowStatus("GAME OVER", new Color(1f, 0.5f, 0.5f, 1f)); + } + + public void ShowVictory() + { + ShowStatus("YOU WIN", new Color(0.6f, 1f, 0.6f, 1f)); + } + + public void TriggerDamageFlash(float strength) + { + if (_damageOverlay == null) + { + return; + } + + strength = Mathf.Clamp01(strength); + + if (_damageFlashRoutine != null) + { + StopCoroutine(_damageFlashRoutine); + } + + _damageFlashRoutine = StartCoroutine(DamageFlashRoutine(strength)); + } + + public void HideStatus() + { + if (_statusText == null) + { + return; + } + + _statusText.enabled = false; } private void UpdateLabel() @@ -52,5 +141,58 @@ namespace Magisterka.BulletHell _scoreText.text = $"Score: {_score:N0}"; } + + private void ApplyLives(int lives) + { + if (_lifeIcons == null) + { + return; + } + + for (int i = 0; i < _lifeIcons.Length; i++) + { + if (_lifeIcons[i] != null) + { + _lifeIcons[i].enabled = i < lives; + } + } + } + + private void ShowStatus(string message, Color color) + { + if (_statusText == null) + { + _queuedStatus = message; + _queuedStatusColor = color; + return; + } + + _statusText.text = message; + _statusText.color = color; + _statusText.enabled = true; + } + + private IEnumerator DamageFlashRoutine(float strength) + { + float peakAlpha = Mathf.Lerp(0.35f, 0.75f, strength); + float duration = 0.45f; + float elapsed = 0f; + + while (elapsed < duration) + { + elapsed += Time.deltaTime; + float normalized = Mathf.Clamp01(elapsed / duration); + float alpha = Mathf.Sin(normalized * Mathf.PI) * peakAlpha; + var color = _damageOverlay.color; + color.a = alpha; + _damageOverlay.color = color; + yield return null; + } + + var reset = _damageOverlay.color; + reset.a = 0f; + _damageOverlay.color = reset; + _damageFlashRoutine = null; + } } }