fix multiple issues

This commit is contained in:
2026-05-27 07:07:03 +02:00
parent 93a2f0e08c
commit 432dd57d07
12 changed files with 680 additions and 36 deletions
+2
View File
@@ -54,6 +54,8 @@
<Compile Include="Assets\TextMesh Pro\Examples &amp; Extras\Scripts\TMPro_InstructionOverlay.cs" />
<Compile Include="Assets\Scripts\Enemy.cs" />
<Compile Include="Assets\Scripts\ShockwaveEffect.cs" />
<Compile Include="Assets\Scripts\RarityPulse.cs" />
<Compile Include="Assets\Scripts\FloatingWorldText.cs" />
<Compile Include="Assets\TextMesh Pro\Examples &amp; Extras\Scripts\VertexShakeB.cs" />
<Compile Include="Assets\Scripts\CannonAreaEffect.cs" />
<Compile Include="Assets\TextMesh Pro\Examples &amp; Extras\Scripts\VertexJitter.cs" />
+2
View File
@@ -55,6 +55,7 @@
<Compile Include="Assets\TextMesh Pro\Examples &amp; Extras\Scripts\TMP_ExampleScript_01.cs" />
<Compile Include="Assets\TextMesh Pro\Examples &amp; Extras\Scripts\TMPro_InstructionOverlay.cs" />
<Compile Include="Assets\Scripts\Enemy.cs" />
<Compile Include="Assets\Scripts\RarityPulse.cs" />
<Compile Include="Assets\TextMesh Pro\Examples &amp; Extras\Scripts\VertexShakeB.cs" />
<Compile Include="Assets\Scripts\CannonAreaEffect.cs" />
<Compile Include="Assets\TextMesh Pro\Examples &amp; Extras\Scripts\VertexJitter.cs" />
@@ -92,6 +93,7 @@
<Compile Include="Assets\TextMesh Pro\Examples &amp; Extras\Scripts\TextConsoleSimulator.cs" />
<Compile Include="Assets\TextMesh Pro\Examples &amp; Extras\Scripts\TeleType.cs" />
<Compile Include="Assets\TextMesh Pro\Examples &amp; Extras\Scripts\TMP_TextEventCheck.cs" />
<Compile Include="Assets\Scripts\FloatingWorldText.cs" />
<Compile Include="Assets\TextMesh Pro\Examples &amp; Extras\Scripts\TextMeshProFloatingText.cs" />
<None Include="Assets\TextMesh Pro\Shaders\TMPro.cginc" />
<None Include="Assets\TextMesh Pro\Shaders\TMP_SDF-Mobile Overlay.shader" />
+23 -2
View File
@@ -143,7 +143,7 @@ public class Bullet : MonoBehaviour
{
Collider2D directCollider = directTarget.GetComponent<Collider2D>();
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<ShockwaveEffect>().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<LineRenderer>();
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<ShockwaveEffect>().Configure(0.18f);
}
}
+54 -1
View File
@@ -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<SpriteRenderer>();
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);
}
}
+14 -1
View File
@@ -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<TowerHealth>().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;
}
}
+27
View File
@@ -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<TextMeshPro>();
text.text = damage.ToString();
text.fontSize = 3.1f;
text.alignment = TextAlignmentOptions.Center;
text.color = Color.white;
text.sortingOrder = 50;
textObject.AddComponent<FloatingWorldText>().Configure(0.75f, new Vector3(0f, 0.58f, 0f));
}
}
+161 -8
View File
@@ -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<Enemy>();
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<EnemyHealth>();
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<SpriteRenderer>();
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<SpriteRenderer>();
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<UpgradeManager>();
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<Canvas>();
if (canvas == null)
return;
Transform existing = canvas.transform.Find("BossHealthBar");
bossHealthBar = existing != null ? existing.GetComponent<RectTransform>() : 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<Image>();
if (panel == null)
panel = bossHealthBar.gameObject.AddComponent<Image>();
panel.color = new Color(0.05f, 0.03f, 0.035f, 0.88f);
RectTransform track = bossHealthBar.Find("Track")?.GetComponent<RectTransform>();
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<Image>();
trackImage.color = new Color(0.16f, 0.08f, 0.08f, 1f);
}
bossHealthFill = track.Find("Fill")?.GetComponent<Image>();
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<Image>();
}
bossHealthFill.type = Image.Type.Simple;
bossHealthText = bossHealthBar.Find("Text")?.GetComponent<TextMeshProUGUI>();
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<RectTransform>();
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);
+39
View File
@@ -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<TextMeshPro>();
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);
}
}
+33
View File
@@ -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<Outline>();
}
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);
}
}
+70
View File
@@ -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<SpriteRenderer>();
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)
+100 -2
View File
@@ -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<Canvas>();
if (canvas == null)
return;
GameObject overlay = new GameObject("StageLostOverlay", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
RectTransform rect = overlay.GetComponent<RectTransform>();
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<Image>();
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<RectTransform>(), 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<RectTransform>();
rect.SetParent(parent, false);
Image image = obj.GetComponent<Image>();
image.color = new Color(0.18f, 0.5f, 0.74f, 1f);
Button button = obj.GetComponent<Button>();
ColorBlock colors = button.colors;
colors.normalColor = image.color;
colors.highlightedColor = new Color(0.25f, 0.62f, 0.88f, 1f);
colors.pressedColor = new Color(0.12f, 0.36f, 0.56f, 1f);
colors.selectedColor = colors.highlightedColor;
colors.disabledColor = new Color(0.16f, 0.18f, 0.22f, 1f);
colors.colorMultiplier = 1f;
button.colors = colors;
button.onClick.AddListener(onClick);
TextMeshProUGUI text = CreateText("Text", rect, label, 18f);
text.alignment = TextAlignmentOptions.Center;
text.rectTransform.anchorMin = Vector2.zero;
text.rectTransform.anchorMax = Vector2.one;
text.rectTransform.offsetMin = Vector2.zero;
text.rectTransform.offsetMax = Vector2.zero;
return button;
}
static void PlaceOverlayRect(RectTransform rect, float x, float y, float width, float height)
{
rect.anchorMin = new Vector2(0.5f, 0.5f);
rect.anchorMax = new Vector2(0.5f, 0.5f);
rect.pivot = new Vector2(0.5f, 0.5f);
rect.anchoredPosition = new Vector2(x, y);
rect.sizeDelta = new Vector2(width, height);
}
void ShowFloatingDamage(int damage)
{
if (floatingTextPrefab != null)
+155 -22
View File
@@ -41,9 +41,12 @@ public class UpgradeManager : MonoBehaviour
readonly Dictionary<UpgradeCard.UpgradeType, int> upgradeCosts = new Dictionary<UpgradeCard.UpgradeType, int>();
GameObject choiceOverlay;
RectTransform currentCardRow;
bool currentGuaranteeEpicOrHigher;
Action currentChoiceComplete;
int bonusBitsPerKill;
float hitBitChance;
int rerollCost = 1;
static readonly CardDefinition[] CannonCards =
{
@@ -99,7 +102,7 @@ public class UpgradeManager : MonoBehaviour
}
}
public void ShowWaveUpgradeChoice(Action onChoiceComplete)
public void ShowWaveUpgradeChoice(Action onChoiceComplete, bool guaranteeEpicOrHigher = false)
{
if (choiceOverlay != null)
return;
@@ -107,8 +110,10 @@ public class UpgradeManager : MonoBehaviour
InitializeUpgrades();
EnsureBitsText();
currentChoiceComplete = onChoiceComplete;
currentGuaranteeEpicOrHigher = guaranteeEpicOrHigher;
rerollCost = 1;
List<UpgradeOffer> offers = BuildOffers();
List<UpgradeOffer> offers = BuildOffers(guaranteeEpicOrHigher);
if (offers.Count == 0)
{
CompleteChoice();
@@ -117,23 +122,9 @@ public class UpgradeManager : MonoBehaviour
Time.timeScale = 0f;
choiceOverlay = CreateOverlay(offers[0].isFree);
RectTransform row = CreateRect("UpgradeChoices", choiceOverlay.transform);
row.anchorMin = new Vector2(0.5f, 0.5f);
row.anchorMax = new Vector2(0.5f, 0.5f);
row.pivot = new Vector2(0.5f, 0.5f);
row.anchoredPosition = new Vector2(0f, -12f);
row.sizeDelta = new Vector2(700f, 212f);
HorizontalLayoutGroup layout = row.gameObject.AddComponent<HorizontalLayoutGroup>();
layout.spacing = 16f;
layout.childAlignment = TextAnchor.MiddleCenter;
layout.childControlWidth = true;
layout.childControlHeight = true;
layout.childForceExpandWidth = false;
layout.childForceExpandHeight = false;
foreach (UpgradeOffer offer in offers)
CreateChoiceCard(offer, row);
currentCardRow = CreateChoiceRow(choiceOverlay.transform);
PopulateChoiceCards(offers);
CreateRerollButton(choiceOverlay.transform);
}
public void ShowSpecialUpgradeChoice()
@@ -160,7 +151,7 @@ public class UpgradeManager : MonoBehaviour
}
}
List<UpgradeOffer> BuildOffers()
List<UpgradeOffer> BuildOffers(bool guaranteeEpicOrHigher)
{
List<UpgradeOffer> paidOffers = new List<UpgradeOffer>();
foreach (CardDefinition card in CannonCards)
@@ -182,7 +173,7 @@ public class UpgradeManager : MonoBehaviour
}
if (paidOffers.Count > 0)
return PickRandomWeighted(paidOffers, 3);
return PickRandomWeighted(paidOffers, 3, guaranteeEpicOrHigher);
return new List<UpgradeOffer>
{
@@ -207,11 +198,28 @@ public class UpgradeManager : MonoBehaviour
};
}
static List<UpgradeOffer> PickRandomWeighted(List<UpgradeOffer> source, int count)
static List<UpgradeOffer> PickRandomWeighted(List<UpgradeOffer> source, int count, bool guaranteeEpicOrHigher)
{
List<UpgradeOffer> pool = new List<UpgradeOffer>(source);
List<UpgradeOffer> result = new List<UpgradeOffer>();
if (guaranteeEpicOrHigher)
{
List<UpgradeOffer> highRarity = new List<UpgradeOffer>();
foreach (UpgradeOffer offer in pool)
{
if (IsEpicOrHigher(offer.rarity))
highRarity.Add(offer);
}
if (highRarity.Count > 0)
{
UpgradeOffer guaranteed = highRarity[UnityEngine.Random.Range(0, highRarity.Count)];
result.Add(guaranteed);
pool.Remove(guaranteed);
}
}
while (pool.Count > 0 && result.Count < count)
{
float totalWeight = 0f;
@@ -237,6 +245,11 @@ public class UpgradeManager : MonoBehaviour
return result;
}
static bool IsEpicOrHigher(string rarity)
{
return rarity == "Epic" || rarity == "Legendary";
}
static float GetRarityWeight(string rarity)
{
switch (rarity)
@@ -265,6 +278,12 @@ public class UpgradeManager : MonoBehaviour
Image image = cardObject.GetComponent<Image>();
image.color = offer.isFree ? new Color(0.11f, 0.17f, 0.13f, 0.98f) : GetRarityColor(offer.rarity);
Outline outline = cardObject.AddComponent<Outline>();
outline.effectDistance = new Vector2(3f, -3f);
outline.effectColor = GetRarityBorderColor(offer.rarity);
RarityPulse pulse = cardObject.AddComponent<RarityPulse>();
pulse.Configure(outline, GetRarityBorderColor(offer.rarity));
Button button = cardObject.GetComponent<Button>();
ColorBlock colors = button.colors;
colors.normalColor = image.color;
@@ -288,6 +307,77 @@ public class UpgradeManager : MonoBehaviour
PlaceCardText(footer.rectTransform, 12f, 12f, -12f, 28f, false);
}
RectTransform CreateChoiceRow(Transform parent)
{
RectTransform row = CreateRect("UpgradeChoices", parent);
row.anchorMin = new Vector2(0.5f, 0.5f);
row.anchorMax = new Vector2(0.5f, 0.5f);
row.pivot = new Vector2(0.5f, 0.5f);
row.anchoredPosition = new Vector2(0f, -12f);
row.sizeDelta = new Vector2(700f, 212f);
HorizontalLayoutGroup layout = row.gameObject.AddComponent<HorizontalLayoutGroup>();
layout.spacing = 16f;
layout.childAlignment = TextAnchor.MiddleCenter;
layout.childControlWidth = true;
layout.childControlHeight = true;
layout.childForceExpandWidth = false;
layout.childForceExpandHeight = false;
return row;
}
void PopulateChoiceCards(List<UpgradeOffer> offers)
{
if (currentCardRow == null)
return;
for (int i = currentCardRow.childCount - 1; i >= 0; i--)
Destroy(currentCardRow.GetChild(i).gameObject);
foreach (UpgradeOffer offer in offers)
CreateChoiceCard(offer, currentCardRow);
}
void CreateRerollButton(Transform parent)
{
Button rerollButton = CreateButton("RerollButton", parent, GetRerollLabel(), RerollOffers);
RectTransform rect = rerollButton.GetComponent<RectTransform>();
rect.anchorMin = new Vector2(0.5f, 0.5f);
rect.anchorMax = new Vector2(0.5f, 0.5f);
rect.pivot = new Vector2(0.5f, 0.5f);
rect.anchoredPosition = new Vector2(0f, -156f);
rect.sizeDelta = new Vector2(190f, 42f);
rerollButton.interactable = bits >= rerollCost;
}
void RerollOffers()
{
if (bits < rerollCost || currentCardRow == null)
return;
bits -= rerollCost;
rerollCost++;
UpdateBitsText();
PopulateChoiceCards(BuildOffers(currentGuaranteeEpicOrHigher));
Transform rerollTransform = choiceOverlay != null ? choiceOverlay.transform.Find("RerollButton") : null;
if (rerollTransform != null)
{
TextMeshProUGUI label = rerollTransform.GetComponentInChildren<TextMeshProUGUI>();
if (label != null)
label.text = GetRerollLabel();
Button button = rerollTransform.GetComponent<Button>();
if (button != null)
button.interactable = bits >= rerollCost;
}
}
string GetRerollLabel()
{
return $"REROLL ({rerollCost} BIT{(rerollCost == 1 ? "" : "S")})";
}
static Color GetRarityColor(string rarity)
{
switch (rarity)
@@ -301,6 +391,19 @@ public class UpgradeManager : MonoBehaviour
}
}
static Color GetRarityBorderColor(string rarity)
{
switch (rarity)
{
case "Common": return new Color(0.62f, 0.64f, 0.66f, 0.85f);
case "Uncommon": return new Color(0.22f, 0.9f, 0.34f, 0.9f);
case "Rare": return new Color(0.22f, 0.48f, 1f, 0.92f);
case "Epic": return new Color(0.68f, 0.25f, 1f, 0.95f);
case "Legendary": return new Color(1f, 0.55f, 0.08f, 0.98f);
default: return new Color(0.62f, 0.64f, 0.66f, 0.85f);
}
}
void SelectOffer(UpgradeOffer offer)
{
if (offer.isFree)
@@ -439,6 +542,8 @@ public class UpgradeManager : MonoBehaviour
Destroy(choiceOverlay);
choiceOverlay = null;
currentCardRow = null;
rerollCost = 1;
Time.timeScale = 1f;
Action callback = currentChoiceComplete;
@@ -588,6 +693,34 @@ public class UpgradeManager : MonoBehaviour
return rect;
}
static Button CreateButton(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<RectTransform>();
rect.SetParent(parent, false);
Image image = obj.GetComponent<Image>();
image.color = new Color(0.18f, 0.5f, 0.74f, 1f);
Button button = obj.GetComponent<Button>();
ColorBlock colors = button.colors;
colors.normalColor = image.color;
colors.highlightedColor = new Color(0.25f, 0.62f, 0.88f, 1f);
colors.pressedColor = new Color(0.12f, 0.36f, 0.56f, 1f);
colors.selectedColor = colors.highlightedColor;
colors.disabledColor = new Color(0.16f, 0.18f, 0.22f, 0.82f);
colors.colorMultiplier = 1f;
button.colors = colors;
button.onClick.AddListener(onClick);
TextMeshProUGUI text = CreateText("Text", rect, label, 16f, FontStyles.Bold, TextAlignmentOptions.Center);
text.rectTransform.anchorMin = Vector2.zero;
text.rectTransform.anchorMax = Vector2.one;
text.rectTransform.offsetMin = Vector2.zero;
text.rectTransform.offsetMax = Vector2.zero;
return button;
}
static TextMeshProUGUI CreateText(string name, Transform parent, string text, float fontSize, FontStyles style, TextAlignmentOptions alignment)
{
GameObject obj = new GameObject(name, typeof(RectTransform), typeof(CanvasRenderer), typeof(TextMeshProUGUI));