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