diff --git a/Assembly-CSharp.Player.csproj b/Assembly-CSharp.Player.csproj index 01c1f8b..f9bcd21 100644 --- a/Assembly-CSharp.Player.csproj +++ b/Assembly-CSharp.Player.csproj @@ -54,6 +54,8 @@ + + diff --git a/Assembly-CSharp.csproj b/Assembly-CSharp.csproj index f0d61ca..b548ae9 100644 --- a/Assembly-CSharp.csproj +++ b/Assembly-CSharp.csproj @@ -55,6 +55,7 @@ + @@ -92,6 +93,7 @@ + diff --git a/Assets/Scripts/Bullet.cs b/Assets/Scripts/Bullet.cs index 8f05239..7435e52 100644 --- a/Assets/Scripts/Bullet.cs +++ b/Assets/Scripts/Bullet.cs @@ -143,7 +143,7 @@ public class Bullet : MonoBehaviour { Collider2D directCollider = directTarget.GetComponent(); if (directCollider != null) - ApplyExplosionControl(directCollider, center); + ApplyExplosionControl(directCollider, center - lastKnownDirection * 0.35f); int directDamage = CalculateDirectDamage(directTarget); directKilled = directTarget.TakeDamage(directDamage); @@ -228,6 +228,7 @@ public class Bullet : MonoBehaviour if (enemyHealth == null) continue; + CreateFragmentVisual(center, collider.transform.position); enemyHealth.TakeDamage(Mathf.Max(1, Mathf.RoundToInt(stats.directDamage * 0.35f))); fragmentsLeft--; } @@ -249,7 +250,8 @@ public class Bullet : MonoBehaviour 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); + stats.craterDuration > 0f ? stats.craterSlowMultiplier : 1f, + stats.splashRadius * 0.72f); } void TriggerChainDetonation(Vector2 center) @@ -319,4 +321,23 @@ public class Bullet : MonoBehaviour ring.GetComponent().Configure(0.28f); } + + static void CreateFragmentVisual(Vector2 start, Vector2 end) + { + GameObject fragment = new GameObject("ShrapnelFragment", typeof(LineRenderer), typeof(ShockwaveEffect)); + fragment.transform.position = start; + + LineRenderer line = fragment.GetComponent(); + line.useWorldSpace = true; + line.positionCount = 2; + line.SetPosition(0, start); + line.SetPosition(1, end); + line.startWidth = 0.045f; + line.endWidth = 0.012f; + line.material = new Material(Shader.Find("Sprites/Default")); + line.startColor = new Color(0.82f, 0.84f, 0.88f, 0.95f); + line.endColor = new Color(0.82f, 0.84f, 0.88f, 0.1f); + + fragment.GetComponent().Configure(0.18f); + } } diff --git a/Assets/Scripts/CannonAreaEffect.cs b/Assets/Scripts/CannonAreaEffect.cs index 8623e48..8228c60 100644 --- a/Assets/Scripts/CannonAreaEffect.cs +++ b/Assets/Scripts/CannonAreaEffect.cs @@ -9,16 +9,29 @@ public class CannonAreaEffect : MonoBehaviour int damagePerSecond; float slowMultiplier = 1f; float expiresAt; + SpriteRenderer visual; - public void Configure(float effectDuration, int burnDamagePerSecond, float craterSlowMultiplier) + public void Configure(float effectDuration, int burnDamagePerSecond, float craterSlowMultiplier, float radius) { duration = effectDuration; damagePerSecond = burnDamagePerSecond; slowMultiplier = Mathf.Clamp(craterSlowMultiplier, 0.1f, 1f); expiresAt = Time.time + duration; + CreateVisual(radius); Destroy(gameObject, duration); } + void Update() + { + if (visual == null || duration <= 0f) + return; + + float remaining = Mathf.Clamp01((expiresAt - Time.time) / duration); + Color color = visual.color; + color.a = Mathf.Lerp(0f, color.a, remaining); + visual.color = color; + } + void OnTriggerStay2D(Collider2D other) { if (!other.CompareTag("Enemy")) @@ -44,4 +57,44 @@ public class CannonAreaEffect : MonoBehaviour health.TakeDamage(Mathf.Max(1, damagePerSecond)); nextDamageTick[health] = Time.time + 1f; } + + void CreateVisual(float radius) + { + GameObject visualObject = new GameObject("Visual", typeof(SpriteRenderer)); + visualObject.transform.SetParent(transform, false); + visualObject.transform.localScale = Vector3.one * radius * 2f; + + visual = visualObject.GetComponent(); + visual.sprite = CreateCircleSprite(); + visual.sortingOrder = -1; + + if (damagePerSecond > 0 && slowMultiplier < 1f) + visual.color = new Color(0.95f, 0.33f, 0.12f, 0.34f); + else if (damagePerSecond > 0) + visual.color = new Color(1f, 0.32f, 0.08f, 0.34f); + else + visual.color = new Color(0.34f, 0.24f, 0.16f, 0.38f); + } + + static Sprite CreateCircleSprite() + { + const int size = 64; + Texture2D texture = new Texture2D(size, size, TextureFormat.RGBA32, false); + texture.filterMode = FilterMode.Bilinear; + Vector2 center = new Vector2((size - 1) * 0.5f, (size - 1) * 0.5f); + float radius = size * 0.46f; + + for (int y = 0; y < size; y++) + { + for (int x = 0; x < size; x++) + { + float distance = Vector2.Distance(new Vector2(x, y), center); + float alpha = Mathf.Clamp01((radius - distance) / 5f); + texture.SetPixel(x, y, new Color(1f, 1f, 1f, alpha)); + } + } + + texture.Apply(); + return Sprite.Create(texture, new Rect(0f, 0f, size, size), new Vector2(0.5f, 0.5f), size); + } } diff --git a/Assets/Scripts/Enemy.cs b/Assets/Scripts/Enemy.cs index a4b6c44..d02a49d 100644 --- a/Assets/Scripts/Enemy.cs +++ b/Assets/Scripts/Enemy.cs @@ -7,6 +7,8 @@ public class Enemy : MonoBehaviour private EnemyHealth enemyHealth; private float slowMultiplier = 1f; private float slowEndTime; + private Vector2 knockbackVelocity; + private float knockbackEndTime; void Start() { @@ -25,12 +27,21 @@ public class Enemy : MonoBehaviour if (Time.time >= slowEndTime) slowMultiplier = 1f; + if (Time.time < knockbackEndTime && knockbackVelocity.sqrMagnitude > 0.01f) + { + transform.position += (Vector3)(knockbackVelocity * Time.deltaTime); + knockbackVelocity = Vector2.Lerp(knockbackVelocity, Vector2.zero, 8f * Time.deltaTime); + return; + } + 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(); tower.GetComponent().TakeDamage(damageDealt); enemyHealth.NotifySpawner(); + if (enemyHealth.isBoss && enemyHealth.spawner != null) + enemyHealth.spawner.HideBossHealth(enemyHealth); Destroy(gameObject); } } @@ -47,6 +58,8 @@ public class Enemy : MonoBehaviour if (direction.sqrMagnitude <= Mathf.Epsilon) return; - transform.position += (Vector3)(direction * distance); + transform.position += (Vector3)(direction * distance * 0.45f); + knockbackVelocity = direction * Mathf.Max(2.5f, distance * 12f); + knockbackEndTime = Time.time + 0.22f; } } diff --git a/Assets/Scripts/EnemyHealth.cs b/Assets/Scripts/EnemyHealth.cs index 7d723c5..7541495 100644 --- a/Assets/Scripts/EnemyHealth.cs +++ b/Assets/Scripts/EnemyHealth.cs @@ -1,3 +1,4 @@ +using TMPro; using UnityEngine; public class EnemyHealth : MonoBehaviour @@ -5,6 +6,7 @@ public class EnemyHealth : MonoBehaviour public int health = 1; public GameObject shatterEffectPrefab; public EnemySpawner spawner; + public bool isBoss; private int maxHealth = 1; public bool TakeDamage(int damage) @@ -12,9 +14,16 @@ public class EnemyHealth : MonoBehaviour if (health <= 0) return false; + ShowDamageNumber(damage); health -= damage; + if (isBoss && spawner != null) + spawner.UpdateBossHealth(this, health, maxHealth); + if (health <= 0) { + if (isBoss && spawner != null) + spawner.HideBossHealth(this); + NotifySpawner(); ShatterEffect(); NotifyUpgradeManager(); @@ -34,6 +43,8 @@ public class EnemyHealth : MonoBehaviour { health = newHealth; maxHealth = Mathf.Max(1, newHealth); + if (isBoss && spawner != null) + spawner.UpdateBossHealth(this, health, maxHealth); } public float GetHealthPercent() @@ -45,6 +56,8 @@ public class EnemyHealth : MonoBehaviour { if (spawner != null) { + if (isBoss) + spawner.HideBossHealth(this); spawner.EnemyDefeated(); } } @@ -65,4 +78,18 @@ public class EnemyHealth : MonoBehaviour Instantiate(shatterEffectPrefab, transform.position, Quaternion.identity); } } + + void ShowDamageNumber(int damage) + { + GameObject textObject = new GameObject("EnemyDamageText", typeof(TextMeshPro)); + textObject.transform.position = transform.position + new Vector3(Random.Range(-0.18f, 0.18f), 0.72f, 0f); + + TextMeshPro text = textObject.GetComponent(); + text.text = damage.ToString(); + text.fontSize = 3.1f; + text.alignment = TextAlignmentOptions.Center; + text.color = Color.white; + text.sortingOrder = 50; + textObject.AddComponent().Configure(0.75f, new Vector3(0f, 0.58f, 0f)); + } } diff --git a/Assets/Scripts/EnemySpawner.cs b/Assets/Scripts/EnemySpawner.cs index 1e48faa..a4dbf4f 100644 --- a/Assets/Scripts/EnemySpawner.cs +++ b/Assets/Scripts/EnemySpawner.cs @@ -21,10 +21,14 @@ public class EnemySpawner : MonoBehaviour private int enemiesRemaining = 0; private int baseEnemyCount = 5; private int enemyCountIncrease = 3; - private float enemySpeed = 1.5f; + private float enemySpeed = 1.16f; private int enemyBaseHP = 1; private float maxEnemySpeed = 6f; private int maxEnemyHP = 20; + private RectTransform bossHealthBar; + private Image bossHealthFill; + private TextMeshProUGUI bossHealthText; + private EnemyHealth activeBoss; void Start() { @@ -74,9 +78,8 @@ public class EnemySpawner : MonoBehaviour Enemy enemyComponent = newEnemy.GetComponent(); if (enemyComponent != null) { - // Lower speed scale to 0.1f - float waveSpeedBonus = currentWave * 0.1f; - float stageSpeedBonus = (currentStage - 1) * 0.08f; + float waveSpeedBonus = currentWave * 0.028f; + float stageSpeedBonus = (currentStage - 1) * 0.025f; enemyComponent.speed = Mathf.Min(maxEnemySpeed, enemySpeed + waveSpeedBonus + stageSpeedBonus); if (boss) enemyComponent.speed *= 0.72f; @@ -85,14 +88,14 @@ public class EnemySpawner : MonoBehaviour EnemyHealth enemyHealth = newEnemy.GetComponent(); if (enemyHealth != null) { - // For HP: baseHP + wave * 0.3 (round it) - int scaledHP = enemyBaseHP + Mathf.RoundToInt(currentWave * 0.3f) + Mathf.RoundToInt((currentStage - 1) * 0.5f); + int scaledHP = enemyBaseHP + Mathf.FloorToInt(currentWave * 0.14f) + Mathf.FloorToInt((currentStage - 1) * 0.25f); if (boss) - scaledHP = Mathf.RoundToInt((scaledHP + currentStage + currentWave) * 4.5f); + scaledHP = Mathf.RoundToInt((scaledHP + currentStage + currentWave * 0.48f) * 3.05f); scaledHP = Mathf.Min(boss ? maxEnemyHP * 4 : maxEnemyHP, scaledHP); enemyHealth.SetHealth(scaledHP); enemyHealth.spawner = this; + enemyHealth.isBoss = boss; } if (boss) @@ -102,6 +105,44 @@ public class EnemySpawner : MonoBehaviour SpriteRenderer spriteRenderer = newEnemy.GetComponent(); if (spriteRenderer != null) spriteRenderer.color = new Color(0.95f, 0.32f, 0.28f, 1f); + + activeBoss = enemyHealth; + EnsureBossHealthBar(); + UpdateBossHealth(enemyHealth, enemyHealth.GetCurrentHealth(), enemyHealth.GetCurrentHealth()); + } + else + { + ApplyEnemyVariant(newEnemy, enemyComponent, enemyHealth); + } + } + + void ApplyEnemyVariant(GameObject enemyObject, Enemy enemyComponent, EnemyHealth enemyHealth) + { + if (enemyComponent == null || enemyHealth == null) + return; + + float waveFactor = Mathf.Clamp01(currentWave / 15f); + float fastChance = Mathf.Lerp(0.08f, 0.24f, waveFactor); + float tankChance = Mathf.Lerp(0.05f, 0.2f, waveFactor); + float roll = Random.value; + + SpriteRenderer spriteRenderer = enemyObject.GetComponent(); + if (roll < fastChance) + { + enemyObject.name = "Fast Enemy"; + enemyComponent.speed *= 1.65f; + enemyObject.transform.localScale *= 0.82f; + if (spriteRenderer != null) + spriteRenderer.color = new Color(0.35f, 0.78f, 1f, 1f); + } + else if (roll < fastChance + tankChance) + { + enemyObject.name = "Tank Enemy"; + enemyComponent.speed *= 0.62f; + enemyObject.transform.localScale *= 1.32f; + enemyHealth.SetHealth(Mathf.Max(2, Mathf.RoundToInt(enemyHealth.GetCurrentHealth() * 2.35f))); + if (spriteRenderer != null) + spriteRenderer.color = new Color(0.78f, 0.56f, 0.28f, 1f); } } @@ -137,7 +178,7 @@ public class EnemySpawner : MonoBehaviour UpgradeManager upgradeManager = FindAnyObjectByType(); if (upgradeManager != null) { - upgradeManager.ShowWaveUpgradeChoice(PrepareNextWave); + StartCoroutine(ShowUpgradeChoiceAfterIntermission(upgradeManager, IsBossWave(currentWave))); return; } @@ -152,6 +193,15 @@ public class EnemySpawner : MonoBehaviour UpdateWaveText(); } + IEnumerator ShowUpgradeChoiceAfterIntermission(UpgradeManager upgradeManager, bool bossWave) + { + yield return new WaitForSeconds(1f); + if (upgradeManager != null) + upgradeManager.ShowWaveUpgradeChoice(PrepareNextWave, bossWave); + else + PrepareNextWave(); + } + int GetNormalEnemyCount(int wave) { return baseEnemyCount + wave * enemyCountIncrease + Mathf.Max(0, currentStage - 1); @@ -171,12 +221,47 @@ public class EnemySpawner : MonoBehaviour { stageComplete = true; isSpawning = false; + HideBossHealth(activeBoss); int nextStage = currentStage + 1; PlayerPrefs.SetInt(CurrentStageKey, nextStage); PlayerPrefs.Save(); ShowWinScreen(nextStage); } + public void UpdateBossHealth(EnemyHealth boss, int currentHealth, int maxHealth) + { + if (boss == null) + return; + + activeBoss = boss; + EnsureBossHealthBar(); + if (bossHealthBar != null) + bossHealthBar.gameObject.SetActive(true); + + float percent = maxHealth <= 0 ? 0f : Mathf.Clamp01((float)Mathf.Max(0, currentHealth) / maxHealth); + if (bossHealthFill != null) + { + bossHealthFill.fillAmount = 1f; + bossHealthFill.color = Color.Lerp(new Color(0.55f, 0.05f, 0.06f, 1f), new Color(0.95f, 0.2f, 0.18f, 1f), percent); + RectTransform fillRect = bossHealthFill.rectTransform; + fillRect.anchorMax = new Vector2(percent, fillRect.anchorMax.y); + fillRect.offsetMax = Vector2.zero; + } + + if (bossHealthText != null) + bossHealthText.text = $"BOSS {Mathf.Max(0, currentHealth)}/{maxHealth}"; + } + + public void HideBossHealth(EnemyHealth boss) + { + if (boss != null && activeBoss != null && boss != activeBoss) + return; + + activeBoss = null; + if (bossHealthBar != null) + bossHealthBar.gameObject.SetActive(false); + } + void ShowWinScreen(int nextStage) { Time.timeScale = 0f; @@ -214,6 +299,66 @@ public class EnemySpawner : MonoBehaviour SceneManager.LoadScene("MainMenu"); } + void EnsureBossHealthBar() + { + if (bossHealthBar != null) + return; + + Canvas canvas = FindAnyObjectByType(); + if (canvas == null) + return; + + Transform existing = canvas.transform.Find("BossHealthBar"); + bossHealthBar = existing != null ? existing.GetComponent() : CreateRect("BossHealthBar", canvas.transform); + bossHealthBar.anchorMin = new Vector2(0.5f, 1f); + bossHealthBar.anchorMax = new Vector2(0.5f, 1f); + bossHealthBar.pivot = new Vector2(0.5f, 1f); + bossHealthBar.anchoredPosition = new Vector2(0f, -126f); + bossHealthBar.sizeDelta = new Vector2(360f, 48f); + bossHealthBar.gameObject.SetActive(false); + + Image panel = bossHealthBar.GetComponent(); + if (panel == null) + panel = bossHealthBar.gameObject.AddComponent(); + panel.color = new Color(0.05f, 0.03f, 0.035f, 0.88f); + + RectTransform track = bossHealthBar.Find("Track")?.GetComponent(); + if (track == null) + { + track = CreateRect("Track", bossHealthBar); + track.anchorMin = new Vector2(0f, 0f); + track.anchorMax = new Vector2(1f, 0f); + track.pivot = new Vector2(0.5f, 0f); + track.anchoredPosition = new Vector2(0f, 8f); + track.sizeDelta = new Vector2(-24f, 16f); + Image trackImage = track.gameObject.AddComponent(); + trackImage.color = new Color(0.16f, 0.08f, 0.08f, 1f); + } + + bossHealthFill = track.Find("Fill")?.GetComponent(); + if (bossHealthFill == null) + { + RectTransform fill = CreateRect("Fill", track); + fill.anchorMin = Vector2.zero; + fill.anchorMax = Vector2.one; + fill.offsetMin = Vector2.zero; + fill.offsetMax = Vector2.zero; + bossHealthFill = fill.gameObject.AddComponent(); + } + bossHealthFill.type = Image.Type.Simple; + + bossHealthText = bossHealthBar.Find("Text")?.GetComponent(); + if (bossHealthText == null) + { + bossHealthText = CreateOverlayText("Text", bossHealthBar, "BOSS", 15f, FontStyles.Bold); + bossHealthText.rectTransform.anchorMin = new Vector2(0f, 1f); + bossHealthText.rectTransform.anchorMax = new Vector2(1f, 1f); + bossHealthText.rectTransform.pivot = new Vector2(0.5f, 1f); + bossHealthText.rectTransform.anchoredPosition = new Vector2(0f, -4f); + bossHealthText.rectTransform.sizeDelta = new Vector2(-16f, 20f); + } + } + void EnsureWaveText() { if (waveText != null) @@ -345,6 +490,14 @@ public class EnemySpawner : MonoBehaviour return button; } + static RectTransform CreateRect(string name, Transform parent) + { + GameObject obj = new GameObject(name, typeof(RectTransform)); + RectTransform rect = obj.GetComponent(); + rect.SetParent(parent, false); + return rect; + } + static void PlaceOverlayRect(RectTransform rect, float x, float y, float width, float height) { rect.anchorMin = new Vector2(0.5f, 0.5f); diff --git a/Assets/Scripts/FloatingWorldText.cs b/Assets/Scripts/FloatingWorldText.cs new file mode 100644 index 0000000..6922080 --- /dev/null +++ b/Assets/Scripts/FloatingWorldText.cs @@ -0,0 +1,39 @@ +using TMPro; +using UnityEngine; + +public class FloatingWorldText : MonoBehaviour +{ + TextMeshPro text; + Vector3 startPosition; + Vector3 offset = new Vector3(0f, 0.6f, 0f); + Color startColor = Color.white; + float duration = 0.75f; + float elapsed; + + public void Configure(float fadeDuration, Vector3 moveOffset) + { + duration = Mathf.Max(0.05f, fadeDuration); + offset = moveOffset; + } + + void Awake() + { + text = GetComponent(); + startPosition = transform.position; + if (text != null) + startColor = text.color; + } + + void Update() + { + elapsed += Time.deltaTime; + float t = Mathf.Clamp01(elapsed / duration); + transform.position = Vector3.Lerp(startPosition, startPosition + offset, t); + + if (text != null) + text.color = new Color(startColor.r, startColor.g, startColor.b, 1f - t); + + if (elapsed >= duration) + Destroy(gameObject); + } +} diff --git a/Assets/Scripts/RarityPulse.cs b/Assets/Scripts/RarityPulse.cs new file mode 100644 index 0000000..e19eea4 --- /dev/null +++ b/Assets/Scripts/RarityPulse.cs @@ -0,0 +1,33 @@ +using UnityEngine; +using UnityEngine.UI; + +public class RarityPulse : MonoBehaviour +{ + Outline outline; + Color baseColor = Color.white; + + public void Configure(Outline targetOutline, Color color) + { + outline = targetOutline; + baseColor = color; + } + + void Awake() + { + if (outline == null) + outline = GetComponent(); + } + + void Update() + { + if (outline == null) + return; + + float pulse = 0.55f + Mathf.Sin(Time.unscaledTime * 4.5f) * 0.25f; + Color color = baseColor; + color.a = Mathf.Clamp01(pulse); + outline.effectColor = color; + float distance = 2.5f + Mathf.Sin(Time.unscaledTime * 4.5f) * 0.8f; + outline.effectDistance = new Vector2(distance, -distance); + } +} diff --git a/Assets/Scripts/Tower.cs b/Assets/Scripts/Tower.cs index ab3d940..648b432 100644 --- a/Assets/Scripts/Tower.cs +++ b/Assets/Scripts/Tower.cs @@ -38,6 +38,8 @@ public class Tower : MonoBehaviour private int shotsFired; private float cooldownUntil; private bool cannonBaselineApplied; + private SpriteRenderer rangeCircle; + private Camera mainCamera; public int extraBullets = 0; // For "Shoot +1 bullet" public bool homingEnabled = false; public bool ricochetEnabled = false; @@ -51,10 +53,15 @@ public class Tower : MonoBehaviour void Start() { ApplyCannonBaseline(); + EnsureRangeCircle(); + FitCameraToRange(); } void Update() { + UpdateRangeCircle(); + FitCameraToRange(); + if (Time.time < cooldownUntil) return; @@ -65,6 +72,69 @@ public class Tower : MonoBehaviour } } + void EnsureRangeCircle() + { + if (rangeCircle != null) + return; + + GameObject rangeObject = new GameObject("TowerRangeCircle", typeof(SpriteRenderer)); + rangeObject.transform.position = transform.position; + rangeCircle = rangeObject.GetComponent(); + rangeCircle.sprite = CreateCircleSprite(); + rangeCircle.color = new Color(0.72f, 0.74f, 0.78f, 0.16f); + rangeCircle.sortingOrder = -10; + UpdateRangeCircle(); + } + + void UpdateRangeCircle() + { + if (rangeCircle == null) + return; + + rangeCircle.transform.position = transform.position; + rangeCircle.transform.localScale = Vector3.one * (attackRange * 2.08f); + } + + void FitCameraToRange() + { + if (mainCamera == null) + mainCamera = Camera.main; + if (mainCamera == null || !mainCamera.orthographic) + return; + + float padding = 0.45f; + float verticalSize = attackRange + padding; + float horizontalSize = (attackRange + padding) / Mathf.Max(0.1f, mainCamera.aspect); + mainCamera.orthographicSize = Mathf.Max(5.5f, verticalSize, horizontalSize); + Vector3 cameraPosition = transform.position; + cameraPosition.z = mainCamera.transform.position.z; + mainCamera.transform.position = cameraPosition; + } + + static Sprite CreateCircleSprite() + { + const int size = 128; + Texture2D texture = new Texture2D(size, size, TextureFormat.RGBA32, false); + texture.filterMode = FilterMode.Bilinear; + Vector2 center = new Vector2((size - 1) * 0.5f, (size - 1) * 0.5f); + float radius = size * 0.47f; + + for (int y = 0; y < size; y++) + { + for (int x = 0; x < size; x++) + { + float distance = Vector2.Distance(new Vector2(x, y), center); + float fillAlpha = distance <= radius ? 0.75f : 0f; + float edge = Mathf.Clamp01(1f - Mathf.Abs(distance - radius) / 2.2f); + float alpha = Mathf.Max(fillAlpha * 0.18f, edge); + texture.SetPixel(x, y, new Color(1f, 1f, 1f, alpha)); + } + } + + texture.Apply(); + return Sprite.Create(texture, new Rect(0f, 0f, size, size), new Vector2(0.5f, 0.5f), size); + } + void ApplyCannonBaseline() { if (cannonBaselineApplied) diff --git a/Assets/Scripts/TowerHealth.cs b/Assets/Scripts/TowerHealth.cs index 9429d53..31a7566 100644 --- a/Assets/Scripts/TowerHealth.cs +++ b/Assets/Scripts/TowerHealth.cs @@ -1,6 +1,7 @@ using UnityEngine; using TMPro; using UnityEngine.UI; +using UnityEngine.SceneManagement; using System.Collections; public class TowerHealth : MonoBehaviour @@ -15,6 +16,7 @@ public class TowerHealth : MonoBehaviour public Color lowHealthColor = new Color(0.95f, 0.22f, 0.18f); private int currentHealth; + private bool isDead; private SpriteRenderer spriteRenderer; void Start() @@ -32,6 +34,9 @@ public class TowerHealth : MonoBehaviour public void TakeDamage(int damage) { + if (isDead) + return; + currentHealth -= damage; if (currentHealth < 0) currentHealth = 0; ShowFloatingDamage(damage); @@ -39,7 +44,8 @@ public class TowerHealth : MonoBehaviour if (currentHealth <= 0) { - Debug.Log("Tower Destroyed! Game Over."); + isDead = true; + ShowStageLostScreen(); } } @@ -49,8 +55,12 @@ public class TowerHealth : MonoBehaviour if (healthFill != null) { - healthFill.fillAmount = healthPercentage; + healthFill.fillAmount = 1f; healthFill.color = Color.Lerp(lowHealthColor, highHealthColor, healthPercentage); + + RectTransform fillRect = healthFill.rectTransform; + fillRect.anchorMax = new Vector2(healthPercentage, fillRect.anchorMax.y); + fillRect.offsetMax = Vector2.zero; } if (healthText != null) @@ -152,6 +162,56 @@ public class TowerHealth : MonoBehaviour healthFill.fillOrigin = (int)Image.OriginHorizontal.Left; } } + + healthFill.type = Image.Type.Simple; + } + + void ShowStageLostScreen() + { + Time.timeScale = 0f; + + Canvas canvas = FindAnyObjectByType(); + if (canvas == null) + return; + + GameObject overlay = new GameObject("StageLostOverlay", 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.8f); + background.raycastTarget = true; + + TextMeshProUGUI title = CreateText("Title", rect, "STAGE LOST", 44f); + title.alignment = TextAlignmentOptions.Center; + PlaceOverlayRect(title.rectTransform, 0f, 64f, 420f, 68f); + + TextMeshProUGUI subtitle = CreateText("Subtitle", rect, "Wave progress and upgrades were lost.", 19f); + subtitle.fontStyle = FontStyles.Normal; + subtitle.alignment = TextAlignmentOptions.Center; + PlaceOverlayRect(subtitle.rectTransform, 0f, 12f, 520f, 42f); + + Button menuButton = CreateOverlayButton("MenuButton", rect, "MAIN MENU", ReturnToMenu); + PlaceOverlayRect(menuButton.GetComponent(), 0f, -64f, 220f, 54f); + + StartCoroutine(ReturnToMenuAfterDelay()); + } + + IEnumerator ReturnToMenuAfterDelay() + { + yield return new WaitForSecondsRealtime(2.25f); + ReturnToMenu(); + } + + void ReturnToMenu() + { + Time.timeScale = 1f; + SceneManager.LoadScene("MainMenu"); } static RectTransform CreateRect(string name, Transform parent) @@ -177,6 +237,44 @@ public class TowerHealth : MonoBehaviour 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