using System.Collections; using System.Collections.Generic; using UnityEngine; namespace Magisterka.BulletHell { /// /// Handles user input, shooting, screen-clearing bomb, and respawn logic. /// [RequireComponent(typeof(Health))] public class PlayerController : MonoBehaviour { public static PlayerController Instance { get; private set; } [Header("Movement")] [SerializeField] private float moveSpeed = 12f; [SerializeField] private float focusSpeedMultiplier = 0.6f; [Header("Offense")] [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 = 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; [SerializeField] private KeyCode invincibilityToggleKey = KeyCode.I; [SerializeField] private KeyCode inactiveModeToggleKey = KeyCode.P; 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 _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; // Testing modes private bool _permanentInvincibility; private bool _inactiveMode; public void Initialize(BulletPool bulletPool, Vector2 movementBounds) { Instance = this; _playerBulletPool = bulletPool; _bounds = movementBounds; _health = GetComponent(); _health.Configure(Faction.Player, 60f); _health.Died += HandleDeath; _health.Damaged += HandleDamageTaken; _renderer = GetComponent(); _baseColor = _renderer != null ? _renderer.color : Color.cyan; _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; // Check command-line arguments for test modes CheckCommandLineArgs(); if (ScoreManager.Instance != null) { ScoreManager.Instance.ScoreChanged += HandleScoreChanged; HandleScoreChanged(ScoreManager.Instance.CurrentScore); } } /// /// Check command-line arguments for automated test modes. /// Usage: ./Final.x86_64 --invincible --stationary /// private void CheckCommandLineArgs() { string[] args = System.Environment.GetCommandLineArgs(); foreach (string arg in args) { if (arg == "--invincible" || arg == "-i") { _permanentInvincibility = true; _isInvulnerable = true; Debug.Log("[TEST] Command-line: Invincibility ENABLED"); if (_renderer != null) { _renderer.color = Color.yellow; } } else if (arg == "--stationary" || arg == "-s") { _inactiveMode = true; Debug.Log("[TEST] Command-line: Stationary mode ENABLED"); } } } public static PlayerController CreatePlayer(Transform parent, BulletPool playerBulletPool, Vector2 movementBounds) { var playerGO = new GameObject("Player"); playerGO.transform.SetParent(parent, false); var renderer = playerGO.AddComponent(); renderer.sprite = BuildPlayerSprite(); renderer.sortingOrder = 20; renderer.color = new Color(0.25f, 0.85f, 1f, 1f); var collider = playerGO.AddComponent(); collider.radius = 1f / 64f; collider.isTrigger = true; 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() { if (_victoryAchieved) { return; } // Toggle invincibility with I key if (Input.GetKeyDown(invincibilityToggleKey)) { _permanentInvincibility = !_permanentInvincibility; Debug.Log($"[TEST] Invincibility: {(_permanentInvincibility ? "ON" : "OFF")}"); if (_permanentInvincibility) { _isInvulnerable = true; if (_renderer != null) { _renderer.color = Color.yellow; } } else { _isInvulnerable = false; if (_renderer != null) { _renderer.color = _baseColor; } } } // Toggle inactive mode with P key if (Input.GetKeyDown(inactiveModeToggleKey)) { _inactiveMode = !_inactiveMode; Debug.Log($"[TEST] Inactive Mode: {(_inactiveMode ? "ON (stationary)" : "OFF (active)")}"); } if (_fireCooldown > 0f) { _fireCooldown -= Time.deltaTime; } if (!_canControl) { return; } // Skip movement and shooting in inactive mode if (!_inactiveMode) { HandleMovement(); HandleShooting(); } HandleBomb(); } private void HandleMovement() { if (!_canControl) { return; } float x = 0f; float y = 0f; if (Input.GetKey(KeyCode.A) || Input.GetKey(KeyCode.LeftArrow)) x -= 1f; if (Input.GetKey(KeyCode.D) || Input.GetKey(KeyCode.RightArrow)) x += 1f; if (Input.GetKey(KeyCode.W) || Input.GetKey(KeyCode.UpArrow)) y += 1f; if (Input.GetKey(KeyCode.S) || Input.GetKey(KeyCode.DownArrow)) y -= 1f; var direction = new Vector2(x, y).normalized; float speed = moveSpeed * (Input.GetKey(modifierKey) ? focusSpeedMultiplier : 1f); var delta = direction * (speed * Time.deltaTime); var next = transform.position + (Vector3)delta; next.x = Mathf.Clamp(next.x, -_bounds.x + 0.5f, _bounds.x - 0.5f); next.y = Mathf.Clamp(next.y, -_bounds.y + 0.5f, _bounds.y - 0.5f); transform.position = next; } private void HandleShooting() { if (!_canControl || !_isAlive) { return; } bool wantsFire = Input.GetKey(fireKey) || Input.GetMouseButton(0); if (!wantsFire || _fireCooldown > 0f) { return; } FireVolley(); _fireCooldown = fireRate; } private void FireVolley() { 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 = 0; i < count; i++) { float offsetIndex = i - mid; float spread = offsetIndex * spreadStep; var direction = Quaternion.Euler(0f, 0f, spread) * Vector2.up; var offset = new Vector3(offsetIndex * 0.18f, 0f, 0f); _playerBulletPool.Spawn(origin + offset, direction, speed, bulletDamage); } 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 || !_canControl || !_isAlive) { return; } bool trigger = Input.GetKeyDown(bombKey) || Input.GetKeyDown(KeyCode.Space) || Input.GetMouseButtonDown(1); if (!trigger) { return; } _bombAvailable = false; StartCoroutine(PerformBomb()); } private IEnumerator PerformBomb() { _canControl = false; _isInvulnerable = true; _renderer.color = Color.white; EffectManager.Instance?.SpawnScreenClear(transform.position, 18f); EnemySpawner.Instance?.ClearScreen(); // Hyper dash upwards and back for dramatic flair. Vector3 start = transform.position; Vector3 apex = new Vector3(0f, Mathf.Min(_bounds.y - 0.6f, start.y + 4.5f), 0f); float travel = 0.4f; float elapsed = 0f; while (elapsed < travel) { elapsed += Time.deltaTime; float t = elapsed / travel; transform.position = Vector3.Lerp(start, apex, t); yield return null; } elapsed = 0f; while (elapsed < travel) { elapsed += Time.deltaTime; float t = elapsed / travel; transform.position = Vector3.Lerp(apex, start, t); yield return null; } // Only disable invulnerability if permanent mode is off if (!_permanentInvincibility) { _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()); } private IEnumerator RespawnRoutine() { EffectManager.Instance?.SpawnExplosion(transform.position, Faction.Player, 4f); _renderer.enabled = false; _isInvulnerable = true; 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) { timer -= Time.deltaTime; float pulse = Mathf.PingPong(Time.time * 10f, 1f); if (_renderer != null) { _renderer.color = Color.Lerp(_baseColor, Color.white, pulse); } yield return null; } if (_renderer != null) { _renderer.color = _baseColor; } // Only disable invulnerability if permanent mode is off if (!_permanentInvincibility) { _isInvulnerable = false; _renderer.color = _baseColor; } else { _renderer.color = Color.yellow; } _isAlive = true; _canControl = true; } private void OnDestroy() { if (_health != null) { _health.Died -= HandleDeath; _health.Damaged -= HandleDamageTaken; } if (ScoreManager.Instance != null) { ScoreManager.Instance.ScoreChanged -= HandleScoreChanged; } if (Instance == this) { Instance = null; } } public bool IsInvulnerable() => _isInvulnerable; public void ApplyDamage(float value) { if (_isInvulnerable || !_isAlive || _victoryAchieved) { return; } _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; var texture = new Texture2D(size, size, TextureFormat.RGBA32, false) { filterMode = FilterMode.Point }; var center = new Vector2(size / 2f, size / 5f * 4f); var pixels = new Color[size * size]; var body = new Color(0.2f, 0.8f, 1f, 1f); 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 * 0.9f)); pixels[index] = dist < size * 0.35f ? body : 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.2f), 64f); } } }