From 93a2f0e08c4452e32a4ec9737f82d84f3d3bcbb2 Mon Sep 17 00:00:00 2001 From: fretuks Date: Wed, 27 May 2026 00:04:52 +0200 Subject: [PATCH] implement more changes --- Assembly-CSharp.Player.csproj | 2 + Assembly-CSharp.csproj | 2 + Assets/Scenes/MainMenu.unity | 4 +- Assets/Scripts/Bullet.cs | 285 +- Assets/Scripts/CannonAreaEffect.cs | 47 + Assets/Scripts/Enemy.cs | 24 +- Assets/Scripts/EnemyHealth.cs | 19 +- Assets/Scripts/EnemySpawner.cs | 209 +- Assets/Scripts/MainMenu.cs | 249 +- Assets/Scripts/ShockwaveEffect.cs | 38 + Assets/Scripts/SpecialChoiceCard.cs | 12 +- Assets/Scripts/Tower.cs | 116 +- Assets/Scripts/UpgradeCard.cs | 62 +- Assets/Scripts/UpgradeManager.cs | 775 +++-- Logs/AssetImportWorker0-prev.log | 2912 +++++++++++++++-- Logs/AssetImportWorker0.log | 2582 +-------------- Logs/AssetImportWorker1-prev.log | 1142 ++++++- Logs/AssetImportWorker1.log | 1005 +----- Logs/AssetImportWorker2-prev.log | 630 ++-- Logs/AssetImportWorker2.log | 336 +- Logs/shadercompiler-AssetImportWorker0-0.log | 57 +- Logs/shadercompiler-AssetImportWorker1-0.log | 2 + Logs/shadercompiler-AssetImportWorker2-0.log | 4 + ...adercompiler-UnityShaderCompiler.exe-0.log | 957 ------ ProjectSettings/EditorBuildSettings.asset | 3 + UserSettings/Layouts/default-6000.dwlt | 193 +- ...mbly-CSharp.csproj.AssemblyReference.cache | Bin 321348 -> 310606 bytes 27 files changed, 5825 insertions(+), 5842 deletions(-) create mode 100644 Assets/Scripts/CannonAreaEffect.cs create mode 100644 Assets/Scripts/ShockwaveEffect.cs diff --git a/Assembly-CSharp.Player.csproj b/Assembly-CSharp.Player.csproj index 4840741..01c1f8b 100644 --- a/Assembly-CSharp.Player.csproj +++ b/Assembly-CSharp.Player.csproj @@ -53,7 +53,9 @@ + + diff --git a/Assembly-CSharp.csproj b/Assembly-CSharp.csproj index f47d18f..f0d61ca 100644 --- a/Assembly-CSharp.csproj +++ b/Assembly-CSharp.csproj @@ -50,11 +50,13 @@ + + diff --git a/Assets/Scenes/MainMenu.unity b/Assets/Scenes/MainMenu.unity index 20bf87c..43e2e8c 100644 --- a/Assets/Scenes/MainMenu.unity +++ b/Assets/Scenes/MainMenu.unity @@ -468,7 +468,7 @@ GameObject: m_Icon: {fileID: 0} m_NavMeshLayer: 0 m_StaticEditorFlags: 0 - m_IsActive: 1 + m_IsActive: 0 --- !u!224 &1038208932 RectTransform: m_ObjectHideFlags: 0 @@ -738,7 +738,7 @@ GameObject: m_Icon: {fileID: 0} m_NavMeshLayer: 0 m_StaticEditorFlags: 0 - m_IsActive: 1 + m_IsActive: 0 --- !u!224 &1406026031 RectTransform: m_ObjectHideFlags: 0 diff --git a/Assets/Scripts/Bullet.cs b/Assets/Scripts/Bullet.cs index 645dd66..8f05239 100644 --- a/Assets/Scripts/Bullet.cs +++ b/Assets/Scripts/Bullet.cs @@ -1,45 +1,98 @@ +using System.Collections; +using System.Collections.Generic; using UnityEngine; public class Bullet : MonoBehaviour { + public struct ShellStats + { + public int directDamage; + public float directHitDamageMultiplier; + public float splashRadius; + public float splashDamageMultiplier; + public float speedMultiplier; + public float knockback; + public float shrapnelEdgeDamageMultiplier; + public int fragmentCount; + public float burningGroundDuration; + public float concussiveSlowMultiplier; + public float concussiveSlowDuration; + public float craterDuration; + public float craterSlowMultiplier; + public float chainDetonationMultiplier; + public float executionDamageMultiplier; + public float finisherDamageMultiplier; + public float delayedFuseSeconds; + public float volatileSecondaryExplosionMultiplier; + } + public float speed = 5f; - private int damage = 1; - private Transform target; public GameObject shatterEffectPrefab; - private Vector2 lastKnownDirection; - private bool hasTarget = true; // Track if bullet has an active target + + ShellStats stats; + Transform target; + Vector2 lastKnownDirection; + bool hasTarget = true; + bool exploded; + + void Awake() + { + stats = new ShellStats + { + directDamage = 1, + directHitDamageMultiplier = 1f, + splashRadius = 1.15f, + splashDamageMultiplier = 0.45f, + speedMultiplier = 1f, + concussiveSlowMultiplier = 1f, + craterSlowMultiplier = 1f, + }; + } + + public void Configure(ShellStats shellStats) + { + stats = shellStats; + stats.directDamage = Mathf.Max(1, stats.directDamage); + stats.directHitDamageMultiplier = Mathf.Max(0f, stats.directHitDamageMultiplier); + stats.splashRadius = Mathf.Max(0.25f, stats.splashRadius); + stats.speedMultiplier = Mathf.Max(0.1f, stats.speedMultiplier); + if (stats.concussiveSlowMultiplier <= 0f) + stats.concussiveSlowMultiplier = 1f; + if (stats.craterSlowMultiplier <= 0f) + stats.craterSlowMultiplier = 1f; + } public void SetTarget(GameObject enemy) { if (enemy != null) { target = enemy.transform; - lastKnownDirection = (target.position - transform.position).normalized; // Store initial direction + lastKnownDirection = (target.position - transform.position).normalized; UpdateRotation(); } } public void SetDamage(int newDamage) { - damage = newDamage; + stats.directDamage = newDamage; } void Update() { + if (exploded) + return; + if (hasTarget && target != null) { - lastKnownDirection = (target.position - transform.position).normalized; // Keep updating direction + lastKnownDirection = (target.position - transform.position).normalized; } else { - hasTarget = false; // The target is gone, continue in last direction + hasTarget = false; } - // Move bullet in the last known direction UpdateRotation(); - transform.position += (Vector3)lastKnownDirection * speed * Time.deltaTime; - - // Destroy bullet after 5 seconds to prevent infinite movement + transform.position += (Vector3)lastKnownDirection * (speed * stats.speedMultiplier) * Time.deltaTime; Destroy(gameObject, 5f); } @@ -53,19 +106,217 @@ public class Bullet : MonoBehaviour void OnTriggerEnter2D(Collider2D other) { - if (other.CompareTag("Enemy")) + if (!exploded && other.CompareTag("Enemy")) + StartCoroutine(ExplodeAfterFuse(other.GetComponent())); + } + + IEnumerator ExplodeAfterFuse(EnemyHealth directTarget) + { + exploded = true; + if (stats.delayedFuseSeconds > 0f) { - other.GetComponent().TakeDamage(damage); - ShatterEffect(); - Destroy(gameObject); + GetComponent().enabled = false; + float elapsed = 0f; + while (elapsed < stats.delayedFuseSeconds) + { + elapsed += Time.deltaTime; + transform.position += (Vector3)lastKnownDirection * (speed * stats.speedMultiplier) * Time.deltaTime; + yield return null; + } } + + Explode(directTarget); + Destroy(gameObject); + } + + void Explode(EnemyHealth directTarget) + { + Vector2 center = transform.position; + NotifyBulletHit(); + ShatterEffect(); + CreateShockwave(center, stats.splashRadius); + + HashSet hitEnemies = new HashSet(); + bool directKilled = false; + + if (directTarget != null) + { + Collider2D directCollider = directTarget.GetComponent(); + if (directCollider != null) + ApplyExplosionControl(directCollider, center); + + int directDamage = CalculateDirectDamage(directTarget); + directKilled = directTarget.TakeDamage(directDamage); + hitEnemies.Add(directTarget); + } + + Collider2D[] colliders = Physics2D.OverlapCircleAll(center, stats.splashRadius); + foreach (Collider2D collider in colliders) + { + if (!collider.CompareTag("Enemy")) + continue; + + EnemyHealth enemyHealth = collider.GetComponent(); + if (enemyHealth == null || hitEnemies.Contains(enemyHealth)) + continue; + + float distance = Vector2.Distance(center, collider.transform.position); + float falloff = Mathf.Clamp01(1f - distance / stats.splashRadius); + int splashDamage = Mathf.Max(1, Mathf.RoundToInt(stats.directDamage * stats.splashDamageMultiplier * Mathf.Lerp(0.5f, 1f, falloff))); + + if (stats.shrapnelEdgeDamageMultiplier > 0f && distance >= stats.splashRadius * 0.68f) + splashDamage += Mathf.Max(1, Mathf.RoundToInt(stats.directDamage * stats.shrapnelEdgeDamageMultiplier)); + + bool killed = enemyHealth.TakeDamage(splashDamage); + hitEnemies.Add(enemyHealth); + ApplyExplosionControl(collider, center); + + if (killed) + TriggerChainDetonation(collider.transform.position); + } + + if (directKilled) + { + TriggerChainDetonation(center); + TriggerVolatileDetonation(center); + } + + SpawnFragments(center); + SpawnLingeringArea(center); + } + + int CalculateDirectDamage(EnemyHealth targetHealth) + { + float multiplier = stats.directHitDamageMultiplier; + float healthPercent = targetHealth.GetHealthPercent(); + + if (healthPercent >= 0.8f) + multiplier *= 1f + stats.executionDamageMultiplier; + if (healthPercent <= 0.3f) + multiplier *= 1f + stats.finisherDamageMultiplier; + + return Mathf.Max(1, Mathf.RoundToInt(stats.directDamage * multiplier)); + } + + void ApplyExplosionControl(Collider2D enemyCollider, Vector2 center) + { + Enemy enemy = enemyCollider.GetComponent(); + if (enemy == null) + return; + + if (stats.knockback > 0f) + enemy.ApplyKnockback(center, stats.knockback); + if (stats.concussiveSlowDuration > 0f) + enemy.ApplySlow(stats.concussiveSlowMultiplier, stats.concussiveSlowDuration); + } + + void SpawnFragments(Vector2 center) + { + if (stats.fragmentCount <= 0) + return; + + Collider2D[] colliders = Physics2D.OverlapCircleAll(center, stats.splashRadius * 1.75f); + int fragmentsLeft = stats.fragmentCount; + foreach (Collider2D collider in colliders) + { + if (fragmentsLeft <= 0) + break; + if (!collider.CompareTag("Enemy")) + continue; + + EnemyHealth enemyHealth = collider.GetComponent(); + if (enemyHealth == null) + continue; + + enemyHealth.TakeDamage(Mathf.Max(1, Mathf.RoundToInt(stats.directDamage * 0.35f))); + fragmentsLeft--; + } + } + + void SpawnLingeringArea(Vector2 center) + { + if (stats.burningGroundDuration <= 0f && stats.craterDuration <= 0f) + return; + + GameObject area = new GameObject("CannonLingeringArea", typeof(CircleCollider2D), typeof(CannonAreaEffect)); + area.transform.position = center; + + CircleCollider2D circle = area.GetComponent(); + circle.isTrigger = true; + circle.radius = stats.splashRadius * 0.72f; + + CannonAreaEffect effect = area.GetComponent(); + effect.Configure( + Mathf.Max(stats.burningGroundDuration, stats.craterDuration), + stats.burningGroundDuration > 0f ? Mathf.Max(1, Mathf.RoundToInt(stats.directDamage * 0.18f)) : 0, + stats.craterDuration > 0f ? stats.craterSlowMultiplier : 1f); + } + + void TriggerChainDetonation(Vector2 center) + { + if (stats.chainDetonationMultiplier <= 0f) + return; + + DamageSecondaryExplosion(center, stats.splashRadius * 0.65f, stats.chainDetonationMultiplier); + } + + void TriggerVolatileDetonation(Vector2 center) + { + if (stats.volatileSecondaryExplosionMultiplier <= 0f || Random.value > 0.2f) + return; + + DamageSecondaryExplosion(center, stats.splashRadius * 0.75f, stats.volatileSecondaryExplosionMultiplier); + } + + void DamageSecondaryExplosion(Vector2 center, float radius, float damageMultiplier) + { + CreateShockwave(center, radius); + Collider2D[] colliders = Physics2D.OverlapCircleAll(center, radius); + foreach (Collider2D collider in colliders) + { + if (!collider.CompareTag("Enemy")) + continue; + + EnemyHealth enemyHealth = collider.GetComponent(); + if (enemyHealth != null) + enemyHealth.TakeDamage(Mathf.Max(1, Mathf.RoundToInt(stats.directDamage * damageMultiplier))); + } + } + + void NotifyBulletHit() + { + UpgradeManager upgradeManager = FindAnyObjectByType(); + if (upgradeManager != null) + upgradeManager.EnemyHitByBullet(); } void ShatterEffect() { if (shatterEffectPrefab != null) - { Instantiate(shatterEffectPrefab, transform.position, Quaternion.identity); + } + + static void CreateShockwave(Vector2 center, float radius) + { + GameObject ring = new GameObject("CannonShockwave", typeof(LineRenderer), typeof(ShockwaveEffect)); + ring.transform.position = center; + + LineRenderer line = ring.GetComponent(); + line.useWorldSpace = false; + line.loop = true; + line.positionCount = 48; + line.startWidth = 0.035f; + line.endWidth = 0.035f; + line.material = new Material(Shader.Find("Sprites/Default")); + line.startColor = new Color(0.95f, 0.82f, 0.45f, 0.82f); + line.endColor = new Color(0.95f, 0.82f, 0.45f, 0.82f); + + for (int i = 0; i < line.positionCount; i++) + { + float angle = (float)i / line.positionCount * Mathf.PI * 2f; + line.SetPosition(i, new Vector3(Mathf.Cos(angle), Mathf.Sin(angle), 0f) * radius); } + + ring.GetComponent().Configure(0.28f); } } diff --git a/Assets/Scripts/CannonAreaEffect.cs b/Assets/Scripts/CannonAreaEffect.cs new file mode 100644 index 0000000..8623e48 --- /dev/null +++ b/Assets/Scripts/CannonAreaEffect.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using UnityEngine; + +public class CannonAreaEffect : MonoBehaviour +{ + readonly Dictionary nextDamageTick = new Dictionary(); + + float duration; + int damagePerSecond; + float slowMultiplier = 1f; + float expiresAt; + + public void Configure(float effectDuration, int burnDamagePerSecond, float craterSlowMultiplier) + { + duration = effectDuration; + damagePerSecond = burnDamagePerSecond; + slowMultiplier = Mathf.Clamp(craterSlowMultiplier, 0.1f, 1f); + expiresAt = Time.time + duration; + Destroy(gameObject, duration); + } + + void OnTriggerStay2D(Collider2D other) + { + if (!other.CompareTag("Enemy")) + return; + + Enemy enemy = other.GetComponent(); + if (enemy != null && slowMultiplier < 1f) + enemy.ApplySlow(slowMultiplier, 0.18f); + + if (damagePerSecond <= 0) + return; + + EnemyHealth health = other.GetComponent(); + if (health == null) + return; + + if (!nextDamageTick.TryGetValue(health, out float nextTick)) + nextTick = 0f; + + if (Time.time < nextTick || Time.time >= expiresAt) + return; + + health.TakeDamage(Mathf.Max(1, damagePerSecond)); + nextDamageTick[health] = Time.time + 1f; + } +} diff --git a/Assets/Scripts/Enemy.cs b/Assets/Scripts/Enemy.cs index c604800..a4b6c44 100644 --- a/Assets/Scripts/Enemy.cs +++ b/Assets/Scripts/Enemy.cs @@ -5,6 +5,8 @@ public class Enemy : MonoBehaviour public float speed = 1.5f; private Transform tower; private EnemyHealth enemyHealth; + private float slowMultiplier = 1f; + private float slowEndTime; void Start() { @@ -20,7 +22,10 @@ public class Enemy : MonoBehaviour void Update() { if (tower == null || enemyHealth == null) return; - transform.position = Vector2.MoveTowards(transform.position, tower.position, speed * Time.deltaTime); + if (Time.time >= slowEndTime) + slowMultiplier = 1f; + + transform.position = Vector2.MoveTowards(transform.position, tower.position, speed * slowMultiplier * Time.deltaTime); if (Vector2.Distance(transform.position, tower.position) < 0.5f) { int damageDealt = enemyHealth.GetCurrentHealth(); @@ -29,4 +34,19 @@ public class Enemy : MonoBehaviour Destroy(gameObject); } } -} \ No newline at end of file + + public void ApplySlow(float multiplier, float duration) + { + slowMultiplier = Mathf.Min(slowMultiplier, Mathf.Clamp(multiplier, 0.1f, 1f)); + slowEndTime = Mathf.Max(slowEndTime, Time.time + duration); + } + + public void ApplyKnockback(Vector2 source, float distance) + { + Vector2 direction = ((Vector2)transform.position - source).normalized; + if (direction.sqrMagnitude <= Mathf.Epsilon) + return; + + transform.position += (Vector3)(direction * distance); + } +} diff --git a/Assets/Scripts/EnemyHealth.cs b/Assets/Scripts/EnemyHealth.cs index 4753161..7d723c5 100644 --- a/Assets/Scripts/EnemyHealth.cs +++ b/Assets/Scripts/EnemyHealth.cs @@ -5,9 +5,13 @@ public class EnemyHealth : MonoBehaviour public int health = 1; public GameObject shatterEffectPrefab; public EnemySpawner spawner; + private int maxHealth = 1; - public void TakeDamage(int damage) + public bool TakeDamage(int damage) { + if (health <= 0) + return false; + health -= damage; if (health <= 0) { @@ -15,7 +19,10 @@ public class EnemyHealth : MonoBehaviour ShatterEffect(); NotifyUpgradeManager(); Destroy(gameObject); + return true; } + + return false; } public int GetCurrentHealth() @@ -26,6 +33,12 @@ public class EnemyHealth : MonoBehaviour public void SetHealth(int newHealth) { health = newHealth; + maxHealth = Mathf.Max(1, newHealth); + } + + public float GetHealthPercent() + { + return maxHealth <= 0 ? 0f : (float)health / maxHealth; } public void NotifySpawner() @@ -38,7 +51,7 @@ public class EnemyHealth : MonoBehaviour void NotifyUpgradeManager() { - UpgradeManager upgradeManager = FindObjectOfType(); + UpgradeManager upgradeManager = FindAnyObjectByType(); if (upgradeManager != null) { upgradeManager.EnemyKilled(); @@ -52,4 +65,4 @@ public class EnemyHealth : MonoBehaviour Instantiate(shatterEffectPrefab, transform.position, Quaternion.identity); } } -} \ No newline at end of file +} diff --git a/Assets/Scripts/EnemySpawner.cs b/Assets/Scripts/EnemySpawner.cs index 4cc7ea7..1e48faa 100644 --- a/Assets/Scripts/EnemySpawner.cs +++ b/Assets/Scripts/EnemySpawner.cs @@ -2,28 +2,33 @@ using System.Collections; using UnityEngine; using TMPro; using UnityEngine.UI; +using UnityEngine.SceneManagement; public class EnemySpawner : MonoBehaviour { + const int WavesPerStage = 15; + const string CurrentStageKey = "CurrentStage"; + public GameObject enemyPrefab; public Transform tower; public TextMeshProUGUI waveText; public Button startWaveButton; public float spawnRadius = 10f; - private int currentWave = 1; + private int currentWave = 0; + private int currentStage = 1; private bool isSpawning = false; + private bool stageComplete = false; private int enemiesRemaining = 0; private int baseEnemyCount = 5; private int enemyCountIncrease = 3; private float enemySpeed = 1.5f; private int enemyBaseHP = 1; - private float speedIncreaseRate = 0.2f; - private int hpIncreaseRate = 1; private float maxEnemySpeed = 6f; private int maxEnemyHP = 20; void Start() { + currentStage = Mathf.Max(1, PlayerPrefs.GetInt(CurrentStageKey, 1)); EnsureWaveText(); StyleStartWaveButton(); UpdateWaveText(); @@ -31,31 +36,37 @@ public class EnemySpawner : MonoBehaviour public void StartWave() { - if (!isSpawning) - { - currentWave++; - UpdateWaveText(); - enemiesRemaining = baseEnemyCount + (currentWave) * enemyCountIncrease; + if (isSpawning || stageComplete || currentWave >= WavesPerStage) + return; + + currentWave++; + isSpawning = true; + UpdateWaveText(); + enemiesRemaining = GetEnemyCountForWave(currentWave); + if (startWaveButton != null) startWaveButton.gameObject.SetActive(false); - StartCoroutine(SpawnWave()); - } + StartCoroutine(SpawnWave()); } IEnumerator SpawnWave() { - isSpawning = true; - int enemiesToSpawn = baseEnemyCount + (currentWave) * enemyCountIncrease; + int enemiesToSpawn = GetNormalEnemyCount(currentWave); for (int i = 0; i < enemiesToSpawn; i++) { - SpawnEnemy(); + SpawnEnemy(false); yield return new WaitForSeconds(1f); } + if (IsBossWave(currentWave)) + { + SpawnEnemy(true); + } + isSpawning = false; } - void SpawnEnemy() + void SpawnEnemy(bool boss) { Vector2 spawnPos = GetRandomSpawnPosition(); GameObject newEnemy = Instantiate(enemyPrefab, spawnPos, Quaternion.identity); @@ -65,19 +76,33 @@ public class EnemySpawner : MonoBehaviour { // Lower speed scale to 0.1f float waveSpeedBonus = currentWave * 0.1f; - enemyComponent.speed = Mathf.Min(maxEnemySpeed, enemySpeed + waveSpeedBonus); + float stageSpeedBonus = (currentStage - 1) * 0.08f; + enemyComponent.speed = Mathf.Min(maxEnemySpeed, enemySpeed + waveSpeedBonus + stageSpeedBonus); + if (boss) + enemyComponent.speed *= 0.72f; } EnemyHealth enemyHealth = newEnemy.GetComponent(); if (enemyHealth != null) { // For HP: baseHP + wave * 0.3 (round it) - int scaledHP = enemyBaseHP + Mathf.RoundToInt(currentWave * 0.3f); - scaledHP = Mathf.Min(maxEnemyHP, scaledHP); + int scaledHP = enemyBaseHP + Mathf.RoundToInt(currentWave * 0.3f) + Mathf.RoundToInt((currentStage - 1) * 0.5f); + if (boss) + scaledHP = Mathf.RoundToInt((scaledHP + currentStage + currentWave) * 4.5f); + scaledHP = Mathf.Min(boss ? maxEnemyHP * 4 : maxEnemyHP, scaledHP); enemyHealth.SetHealth(scaledHP); enemyHealth.spawner = this; } + + if (boss) + { + newEnemy.name = $"Boss Wave {currentWave}"; + newEnemy.transform.localScale *= 1.8f; + SpriteRenderer spriteRenderer = newEnemy.GetComponent(); + if (spriteRenderer != null) + spriteRenderer.color = new Color(0.95f, 0.32f, 0.28f, 1f); + } } Vector2 GetRandomSpawnPosition() @@ -90,29 +115,105 @@ public class EnemySpawner : MonoBehaviour { if (waveText != null) { - waveText.text = $"WAVE {currentWave}"; + int displayWave = Mathf.Clamp(isSpawning ? currentWave : currentWave + 1, 1, WavesPerStage); + waveText.text = $"Wave {displayWave}/{WavesPerStage}"; } } public void EnemyDefeated() { + if (stageComplete) + return; + enemiesRemaining--; if (enemiesRemaining <= 0) { - startWaveButton.gameObject.SetActive(true); - - // If wave is multiple of 5, show special upgrade choice - if (currentWave % 5 == 0) + if (currentWave >= WavesPerStage) { - UpgradeManager upgradeManager = FindAnyObjectByType(); - if (upgradeManager != null) - { - upgradeManager.ShowSpecialUpgradeChoice(); - } + CompleteStage(); + return; } + + UpgradeManager upgradeManager = FindAnyObjectByType(); + if (upgradeManager != null) + { + upgradeManager.ShowWaveUpgradeChoice(PrepareNextWave); + return; + } + + PrepareNextWave(); } } + void PrepareNextWave() + { + if (startWaveButton != null) + startWaveButton.gameObject.SetActive(true); + UpdateWaveText(); + } + + int GetNormalEnemyCount(int wave) + { + return baseEnemyCount + wave * enemyCountIncrease + Mathf.Max(0, currentStage - 1); + } + + int GetEnemyCountForWave(int wave) + { + return GetNormalEnemyCount(wave) + (IsBossWave(wave) ? 1 : 0); + } + + static bool IsBossWave(int wave) + { + return wave == 5 || wave == 10 || wave == 15; + } + + void CompleteStage() + { + stageComplete = true; + isSpawning = false; + int nextStage = currentStage + 1; + PlayerPrefs.SetInt(CurrentStageKey, nextStage); + PlayerPrefs.Save(); + ShowWinScreen(nextStage); + } + + void ShowWinScreen(int nextStage) + { + Time.timeScale = 0f; + + Canvas canvas = FindAnyObjectByType(); + if (canvas == null) + return; + + GameObject overlay = new GameObject("WinOverlay", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image)); + RectTransform rect = overlay.GetComponent(); + rect.SetParent(canvas.transform, false); + rect.anchorMin = Vector2.zero; + rect.anchorMax = Vector2.one; + rect.offsetMin = Vector2.zero; + rect.offsetMax = Vector2.zero; + rect.SetAsLastSibling(); + + Image background = overlay.GetComponent(); + background.color = new Color(0f, 0f, 0f, 0.78f); + background.raycastTarget = true; + + TextMeshProUGUI title = CreateOverlayText("Title", rect, "YOU WIN", 46f, FontStyles.Bold); + PlaceOverlayRect(title.rectTransform, 0f, 76f, 420f, 70f); + + TextMeshProUGUI subtitle = CreateOverlayText("Subtitle", rect, $"Stage {currentStage} complete. Stage {nextStage} unlocked.", 20f, FontStyles.Normal); + PlaceOverlayRect(subtitle.rectTransform, 0f, 22f, 520f, 42f); + + Button menuButton = CreateOverlayButton("MenuButton", rect, "MAIN MENU", ReturnToMenu); + PlaceOverlayRect(menuButton.GetComponent(), 0f, -58f, 220f, 54f); + } + + void ReturnToMenu() + { + Time.timeScale = 1f; + SceneManager.LoadScene("MainMenu"); + } + void EnsureWaveText() { if (waveText != null) @@ -198,4 +299,58 @@ public class EnemySpawner : MonoBehaviour label.raycastTarget = false; } } + + static TextMeshProUGUI CreateOverlayText(string name, Transform parent, string text, float fontSize, FontStyles style) + { + GameObject obj = new GameObject(name, typeof(RectTransform), typeof(CanvasRenderer), typeof(TextMeshProUGUI)); + RectTransform rect = obj.GetComponent(); + rect.SetParent(parent, false); + + TextMeshProUGUI label = obj.GetComponent(); + label.text = text; + label.fontSize = fontSize; + label.fontStyle = style; + label.alignment = TextAlignmentOptions.Center; + label.color = new Color(0.94f, 0.96f, 1f, 1f); + label.textWrappingMode = TextWrappingModes.Normal; + label.raycastTarget = false; + return label; + } + + static Button CreateOverlayButton(string name, Transform parent, string label, UnityEngine.Events.UnityAction onClick) + { + GameObject obj = new GameObject(name, typeof(RectTransform), typeof(CanvasRenderer), typeof(Image), typeof(Button)); + RectTransform rect = obj.GetComponent(); + rect.SetParent(parent, false); + + Image image = obj.GetComponent(); + image.color = new Color(0.18f, 0.5f, 0.74f, 1f); + + Button button = obj.GetComponent