implement more changes

This commit is contained in:
2026-05-27 00:04:52 +02:00
parent e460a69443
commit 93a2f0e08c
27 changed files with 5825 additions and 5842 deletions
+268 -17
View File
@@ -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<EnemyHealth>()));
}
IEnumerator ExplodeAfterFuse(EnemyHealth directTarget)
{
exploded = true;
if (stats.delayedFuseSeconds > 0f)
{
other.GetComponent<EnemyHealth>().TakeDamage(damage);
ShatterEffect();
Destroy(gameObject);
GetComponent<Collider2D>().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<EnemyHealth> hitEnemies = new HashSet<EnemyHealth>();
bool directKilled = false;
if (directTarget != null)
{
Collider2D directCollider = directTarget.GetComponent<Collider2D>();
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<EnemyHealth>();
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<Enemy>();
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<EnemyHealth>();
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<CircleCollider2D>();
circle.isTrigger = true;
circle.radius = stats.splashRadius * 0.72f;
CannonAreaEffect effect = area.GetComponent<CannonAreaEffect>();
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<EnemyHealth>();
if (enemyHealth != null)
enemyHealth.TakeDamage(Mathf.Max(1, Mathf.RoundToInt(stats.directDamage * damageMultiplier)));
}
}
void NotifyBulletHit()
{
UpgradeManager upgradeManager = FindAnyObjectByType<UpgradeManager>();
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<LineRenderer>();
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<ShockwaveEffect>().Configure(0.28f);
}
}
+47
View File
@@ -0,0 +1,47 @@
using System.Collections.Generic;
using UnityEngine;
public class CannonAreaEffect : MonoBehaviour
{
readonly Dictionary<EnemyHealth, float> nextDamageTick = new Dictionary<EnemyHealth, float>();
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<Enemy>();
if (enemy != null && slowMultiplier < 1f)
enemy.ApplySlow(slowMultiplier, 0.18f);
if (damagePerSecond <= 0)
return;
EnemyHealth health = other.GetComponent<EnemyHealth>();
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;
}
}
+22 -2
View File
@@ -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);
}
}
}
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);
}
}
+16 -3
View File
@@ -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 upgradeManager = FindAnyObjectByType<UpgradeManager>();
if (upgradeManager != null)
{
upgradeManager.EnemyKilled();
@@ -52,4 +65,4 @@ public class EnemyHealth : MonoBehaviour
Instantiate(shatterEffectPrefab, transform.position, Quaternion.identity);
}
}
}
}
+182 -27
View File
@@ -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<EnemyHealth>();
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<SpriteRenderer>();
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<UpgradeManager>();
if (upgradeManager != null)
{
upgradeManager.ShowSpecialUpgradeChoice();
}
CompleteStage();
return;
}
UpgradeManager upgradeManager = FindAnyObjectByType<UpgradeManager>();
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<Canvas>();
if (canvas == null)
return;
GameObject overlay = new GameObject("WinOverlay", 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.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<RectTransform>(), 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<RectTransform>();
rect.SetParent(parent, false);
TextMeshProUGUI label = obj.GetComponent<TextMeshProUGUI>();
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<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 = CreateOverlayText("Text", rect, label, 18f, FontStyles.Bold);
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);
}
}
+247 -2
View File
@@ -1,11 +1,35 @@
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
using TMPro;
public class MainMenu : MonoBehaviour
{
const string CurrentStageKey = "CurrentStage";
const string SelectedTowerKey = "SelectedTower";
const string CannonTowerId = "cannon";
Canvas canvas;
RectTransform contentRoot;
TextMeshProUGUI homeTabLabel;
TextMeshProUGUI towersTabLabel;
void Start()
{
Time.timeScale = 1f;
if (!PlayerPrefs.HasKey(CurrentStageKey))
PlayerPrefs.SetInt(CurrentStageKey, 1);
if (!PlayerPrefs.HasKey(SelectedTowerKey))
PlayerPrefs.SetString(SelectedTowerKey, CannonTowerId);
BuildMenu();
ShowHome();
}
public void StartGame()
{
Debug.Log("Game Start!");
PlayerPrefs.SetString(SelectedTowerKey, CannonTowerId);
PlayerPrefs.Save();
SceneManager.LoadScene("SampleScene");
}
@@ -14,4 +38,225 @@ public class MainMenu : MonoBehaviour
Debug.Log("Game Quit!");
Application.Quit();
}
}
void BuildMenu()
{
Canvas[] existingCanvases = FindObjectsByType<Canvas>();
canvas = null;
foreach (Canvas existingCanvas in existingCanvases)
{
if (existingCanvas.GetComponent<MainMenu>() != null)
{
canvas = existingCanvas;
continue;
}
Destroy(existingCanvas.gameObject);
}
if (canvas == null)
{
GameObject canvasObject = new GameObject("MainMenuCanvas", typeof(RectTransform), typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster));
canvas = canvasObject.GetComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
CanvasScaler scaler = canvasObject.GetComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(900f, 600f);
scaler.matchWidthOrHeight = 0.5f;
}
for (int i = canvas.transform.childCount - 1; i >= 0; i--)
Destroy(canvas.transform.GetChild(i).gameObject);
RectTransform root = CreatePanel("MainMenuRoot", canvas.transform, new Color(0.035f, 0.04f, 0.052f, 1f));
root.anchorMin = Vector2.zero;
root.anchorMax = Vector2.one;
root.offsetMin = Vector2.zero;
root.offsetMax = Vector2.zero;
TextMeshProUGUI title = CreateText("Title", root, "THE TOWER", 42f, FontStyles.Bold, TextAlignmentOptions.Center);
RectTransform titleRect = title.rectTransform;
titleRect.anchorMin = new Vector2(0f, 1f);
titleRect.anchorMax = new Vector2(1f, 1f);
titleRect.pivot = new Vector2(0.5f, 1f);
titleRect.anchoredPosition = new Vector2(0f, -30f);
titleRect.sizeDelta = new Vector2(0f, 58f);
RectTransform nav = CreateRect("Nav", root);
nav.anchorMin = new Vector2(0f, 0f);
nav.anchorMax = new Vector2(1f, 0f);
nav.pivot = new Vector2(0.5f, 0f);
nav.anchoredPosition = Vector2.zero;
nav.sizeDelta = new Vector2(0f, 58f);
HorizontalLayoutGroup navLayout = nav.gameObject.AddComponent<HorizontalLayoutGroup>();
navLayout.spacing = 0f;
navLayout.childAlignment = TextAnchor.MiddleCenter;
navLayout.childControlWidth = true;
navLayout.childControlHeight = true;
navLayout.childForceExpandWidth = true;
navLayout.childForceExpandHeight = true;
Button homeButton = CreateButton("HomeButton", nav, "HOME", () => ShowHome());
homeTabLabel = homeButton.GetComponentInChildren<TextMeshProUGUI>();
Button towersButton = CreateButton("TowersButton", nav, "TOWERS", () => ShowTowers());
towersTabLabel = towersButton.GetComponentInChildren<TextMeshProUGUI>();
contentRoot = CreatePanel("Content", root, new Color(0.075f, 0.084f, 0.105f, 0.98f));
contentRoot.anchorMin = new Vector2(0.5f, 0.5f);
contentRoot.anchorMax = new Vector2(0.5f, 0.5f);
contentRoot.pivot = new Vector2(0.5f, 0.5f);
contentRoot.anchoredPosition = new Vector2(0f, -12f);
contentRoot.sizeDelta = new Vector2(640f, 350f);
Button quitButton = CreateButton("QuitButton", root, "QUIT", QuitGame);
RectTransform quitRect = quitButton.GetComponent<RectTransform>();
quitRect.anchorMin = new Vector2(1f, 1f);
quitRect.anchorMax = new Vector2(1f, 1f);
quitRect.pivot = new Vector2(1f, 1f);
quitRect.anchoredPosition = new Vector2(-20f, -20f);
quitRect.sizeDelta = new Vector2(112f, 40f);
}
void ShowHome()
{
SetTabState(true);
ClearContent();
int stage = Mathf.Max(1, PlayerPrefs.GetInt(CurrentStageKey, 1));
TextMeshProUGUI heading = CreateText("Heading", contentRoot, "HOME", 26f, FontStyles.Bold, TextAlignmentOptions.Center);
PlaceTop(heading.rectTransform, -34f, 36f);
TextMeshProUGUI stageText = CreateText("Stage", contentRoot, $"STAGE {stage}", 42f, FontStyles.Bold, TextAlignmentOptions.Center);
PlaceCenter(stageText.rectTransform, 0f, 76f, 320f, 60f);
TextMeshProUGUI detail = CreateText("Detail", contentRoot, "15 waves. Boss waves at 5, 10 and 15.", 17f, FontStyles.Normal, TextAlignmentOptions.Center);
PlaceCenter(detail.rectTransform, 0f, 8f, 460f, 34f);
Button playButton = CreateButton("PlayButton", contentRoot, "PLAY", StartGame);
RectTransform playRect = playButton.GetComponent<RectTransform>();
PlaceCenter(playRect, 0f, -104f, 240f, 58f);
}
void ShowTowers()
{
SetTabState(false);
ClearContent();
TextMeshProUGUI heading = CreateText("Heading", contentRoot, "TOWERS", 26f, FontStyles.Bold, TextAlignmentOptions.Center);
PlaceTop(heading.rectTransform, -34f, 36f);
RectTransform card = CreatePanel("CannonTowerCard", contentRoot, new Color(0.105f, 0.12f, 0.15f, 1f));
PlaceCenter(card, 0f, -10f, 420f, 198f);
TextMeshProUGUI name = CreateText("Name", card, "CANNON TOWER", 24f, FontStyles.Bold, TextAlignmentOptions.Center);
PlaceTop(name.rectTransform, -18f, 34f);
TextMeshProUGUI description = CreateText("Description", card, "Single-target bullets with Fire Rate, Range and Damage upgrades.", 16f, FontStyles.Normal, TextAlignmentOptions.Center);
PlaceCenter(description.rectTransform, 0f, 18f, 320f, 54f);
Button equipButton = CreateButton("EquipButton", card, "EQUIPPED", () =>
{
PlayerPrefs.SetString(SelectedTowerKey, CannonTowerId);
PlayerPrefs.Save();
ShowTowers();
});
RectTransform equipRect = equipButton.GetComponent<RectTransform>();
PlaceCenter(equipRect, 0f, -54f, 170f, 42f);
equipButton.interactable = false;
}
void SetTabState(bool homeActive)
{
if (homeTabLabel != null)
homeTabLabel.color = homeActive ? Color.white : new Color(0.68f, 0.73f, 0.8f, 1f);
if (towersTabLabel != null)
towersTabLabel.color = homeActive ? new Color(0.68f, 0.73f, 0.8f, 1f) : Color.white;
}
void ClearContent()
{
for (int i = contentRoot.childCount - 1; i >= 0; i--)
Destroy(contentRoot.GetChild(i).gameObject);
}
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, 1f);
colors.colorMultiplier = 1f;
button.colors = colors;
button.onClick.AddListener(onClick);
TextMeshProUGUI text = CreateText("Text", rect, label, 18f, 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 RectTransform CreatePanel(string name, Transform parent, Color color)
{
GameObject obj = new GameObject(name, typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
RectTransform rect = obj.GetComponent<RectTransform>();
rect.SetParent(parent, false);
obj.GetComponent<Image>().color = color;
return rect;
}
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 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));
RectTransform rect = obj.GetComponent<RectTransform>();
rect.SetParent(parent, false);
TextMeshProUGUI label = obj.GetComponent<TextMeshProUGUI>();
label.text = text;
label.fontSize = fontSize;
label.fontStyle = style;
label.alignment = alignment;
label.color = new Color(0.91f, 0.94f, 0.98f, 1f);
label.textWrappingMode = TextWrappingModes.Normal;
label.raycastTarget = false;
return label;
}
static void PlaceTop(RectTransform rect, float y, float height)
{
rect.anchorMin = new Vector2(0f, 1f);
rect.anchorMax = new Vector2(1f, 1f);
rect.pivot = new Vector2(0.5f, 1f);
rect.anchoredPosition = new Vector2(0f, y);
rect.sizeDelta = new Vector2(-40f, height);
}
static void PlaceCenter(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);
}
}
+38
View File
@@ -0,0 +1,38 @@
using UnityEngine;
public class ShockwaveEffect : MonoBehaviour
{
LineRenderer line;
float duration = 0.28f;
float elapsed;
Vector3 startScale;
public void Configure(float effectDuration)
{
duration = Mathf.Max(0.05f, effectDuration);
}
void Awake()
{
line = GetComponent<LineRenderer>();
startScale = transform.localScale * 0.35f;
transform.localScale = startScale;
}
void Update()
{
elapsed += Time.deltaTime;
float t = Mathf.Clamp01(elapsed / duration);
transform.localScale = Vector3.Lerp(startScale, Vector3.one, t);
if (line != null)
{
Color color = Color.Lerp(new Color(0.95f, 0.82f, 0.45f, 0.8f), new Color(0.95f, 0.82f, 0.45f, 0f), t);
line.startColor = color;
line.endColor = color;
}
if (elapsed >= duration)
Destroy(gameObject);
}
}
+5 -7
View File
@@ -6,20 +6,18 @@ public class SpecialChoiceCard : MonoBehaviour
void OnMouseDown()
{
// Identify which UpgradeType this card has
UpgradeCard card = GetComponent<UpgradeCard>();
if (card != null)
if (card != null && upgradeManager != null)
{
upgradeManager.UnlockSpecialUpgrade(card.upgradeType);
upgradeManager.TryUpgrade(card.upgradeType);
}
// Destroy all other special cards
SpecialChoiceCard[] allChoices = FindObjectsOfType<SpecialChoiceCard>();
SpecialChoiceCard[] allChoices = FindObjectsByType<SpecialChoiceCard>();
foreach (var choice in allChoices)
{
Destroy(choice.gameObject);
}
Time.timeScale = 1f; // Resume game
Time.timeScale = 1f;
}
}
}
+113 -3
View File
@@ -8,15 +8,56 @@ public class Tower : MonoBehaviour
public int damage = 1;
public float fireRate = 1f;
public float attackRange = 5f;
public float splashRadius = 1.15f;
public float splashDamageMultiplier = 0.45f;
public float shellDamageMultiplier = 1f;
public float projectileSpeedMultiplier = 1f;
public float explosionKnockback = 0.18f;
public float shrapnelEdgeDamageMultiplier;
public int fragmentCount;
public float burningGroundDuration;
public float concussiveSlowMultiplier = 1f;
public float concussiveSlowDuration;
public float craterDuration;
public float craterSlowMultiplier = 1f;
public float chainDetonationMultiplier;
public int siegeShellStacks;
public float directHitDamageMultiplier = 1f;
public float doubleChargeChance;
public int siegePlatformStacks;
public float executionDamageMultiplier;
public float finisherDamageMultiplier;
public bool smartTargeting;
public float delayedFuseSeconds;
public float delayedFuseRadiusMultiplier = 1f;
public float volatileSecondaryExplosionMultiplier;
public bool glassCannonDesign;
public bool apocalypseRound;
private float nextFireTime;
private GameObject currentTarget;
private int shotsFired;
private float cooldownUntil;
private bool cannonBaselineApplied;
public int extraBullets = 0; // For "Shoot +1 bullet"
public bool homingEnabled = false;
public bool ricochetEnabled = false;
public bool pierceEnabled = false;
void Awake()
{
ApplyCannonBaseline();
}
void Start()
{
ApplyCannonBaseline();
}
void Update()
{
if (Time.time < cooldownUntil)
return;
if (Time.time >= nextFireTime)
{
RetargetAndFire();
@@ -24,6 +65,15 @@ public class Tower : MonoBehaviour
}
}
void ApplyCannonBaseline()
{
if (cannonBaselineApplied)
return;
fireRate *= 1.1f;
cannonBaselineApplied = true;
}
void RetargetAndFire()
{
currentTarget = GetNearestEnemy();
@@ -34,10 +84,70 @@ public class Tower : MonoBehaviour
}
void FireAtTarget(GameObject target)
{
shotsFired++;
FireShell(target, BuildShellStats());
if (Random.value < doubleChargeChance)
FireShell(target, BuildShellStats());
if (glassCannonDesign && shotsFired % 10 == 0)
cooldownUntil = Time.time + 1.75f;
}
void FireShell(GameObject target, Bullet.ShellStats stats)
{
GameObject bullet = Instantiate(bulletPrefab, transform.position, Quaternion.identity);
bullet.GetComponent<Bullet>().SetDamage(damage);
bullet.GetComponent<Bullet>().SetTarget(target);
Bullet shell = bullet.GetComponent<Bullet>();
shell.Configure(stats);
shell.SetTarget(target);
}
Bullet.ShellStats BuildShellStats()
{
bool siegeShot = siegeShellStacks > 0 && shotsFired % Mathf.Max(2, 6 - siegeShellStacks) == 0;
bool apocalypseShot = apocalypseRound && shotsFired % 20 == 0;
float damageBonus = shellDamageMultiplier;
if (siegePlatformStacks > 0 && Time.time >= cooldownUntil)
damageBonus *= 1f + 0.35f * siegePlatformStacks;
Bullet.ShellStats stats = new Bullet.ShellStats
{
directDamage = Mathf.Max(1, Mathf.RoundToInt(damage * damageBonus)),
directHitDamageMultiplier = directHitDamageMultiplier,
splashRadius = splashRadius * delayedFuseRadiusMultiplier,
splashDamageMultiplier = splashDamageMultiplier,
speedMultiplier = projectileSpeedMultiplier,
knockback = explosionKnockback,
shrapnelEdgeDamageMultiplier = shrapnelEdgeDamageMultiplier,
fragmentCount = fragmentCount,
burningGroundDuration = burningGroundDuration,
concussiveSlowMultiplier = concussiveSlowMultiplier,
concussiveSlowDuration = concussiveSlowDuration,
craterDuration = craterDuration,
craterSlowMultiplier = craterSlowMultiplier,
chainDetonationMultiplier = chainDetonationMultiplier,
executionDamageMultiplier = executionDamageMultiplier,
finisherDamageMultiplier = finisherDamageMultiplier,
delayedFuseSeconds = delayedFuseSeconds,
volatileSecondaryExplosionMultiplier = volatileSecondaryExplosionMultiplier,
};
if (siegeShot)
{
stats.directDamage = Mathf.RoundToInt(stats.directDamage * 1.8f);
stats.splashRadius *= 1.6f;
}
if (apocalypseShot)
{
stats.directDamage = Mathf.RoundToInt(stats.directDamage * 4f);
stats.splashRadius *= 3.2f;
stats.splashDamageMultiplier = Mathf.Max(stats.splashDamageMultiplier, 0.8f);
}
return stats;
}
GameObject GetNearestEnemy()
@@ -60,4 +170,4 @@ public class Tower : MonoBehaviour
{
RetargetAndFire();
}
}
}
+50 -12
View File
@@ -6,12 +6,31 @@ public class UpgradeCard : MonoBehaviour
{
public enum UpgradeType
{
FireRate = 0,
Range = 1,
Damage = 2,
MultiShot= 3,
Homing = 4,
Ricochet = 5,
ReinforcedPowder = 0,
ExpandedPayload = 1,
AutoLoader = 2,
HeavyCaliber = 3,
ShockwaveShells = 4,
ShrapnelRounds = 5,
FragmentBurst = 6,
BurningDebris = 7,
ConcussiveImpact = 8,
CraterMaker = 9,
ChainDetonation = 10,
SiegeShells = 11,
ArmorPiercingCore = 12,
DoubleCharge = 13,
SiegePlatform = 14,
ExecutionShell = 15,
FinisherPayload = 16,
StabilizedBarrel = 17,
SmartTargeting = 18,
RangefinderOptics = 19,
DelayedFuse = 20,
OverloadedChamber = 21,
VolatileMunitions = 22,
GlassCannonDesign = 23,
ApocalypseRound = 24,
}
@@ -79,12 +98,31 @@ public class UpgradeCard : MonoBehaviour
{
switch (type)
{
case UpgradeType.FireRate: return "FIRE RATE";
case UpgradeType.Range: return "RANGE";
case UpgradeType.Damage: return "DAMAGE";
case UpgradeType.MultiShot: return "MULTI SHOT";
case UpgradeType.Homing: return "HOMING";
case UpgradeType.Ricochet: return "RICOCHET";
case UpgradeType.ReinforcedPowder: return "REINFORCED POWDER";
case UpgradeType.ExpandedPayload: return "EXPANDED PAYLOAD";
case UpgradeType.AutoLoader: return "AUTO-LOADER";
case UpgradeType.HeavyCaliber: return "HEAVY CALIBER";
case UpgradeType.ShockwaveShells: return "SHOCKWAVE SHELLS";
case UpgradeType.ShrapnelRounds: return "SHRAPNEL ROUNDS";
case UpgradeType.FragmentBurst: return "FRAGMENT BURST";
case UpgradeType.BurningDebris: return "BURNING DEBRIS";
case UpgradeType.ConcussiveImpact: return "CONCUSSIVE IMPACT";
case UpgradeType.CraterMaker: return "CRATER MAKER";
case UpgradeType.ChainDetonation: return "CHAIN DETONATION";
case UpgradeType.SiegeShells: return "SIEGE SHELLS";
case UpgradeType.ArmorPiercingCore: return "ARMOR-PIERCING CORE";
case UpgradeType.DoubleCharge: return "DOUBLE CHARGE";
case UpgradeType.SiegePlatform: return "SIEGE PLATFORM";
case UpgradeType.ExecutionShell: return "EXECUTION SHELL";
case UpgradeType.FinisherPayload: return "FINISHER PAYLOAD";
case UpgradeType.StabilizedBarrel: return "STABILIZED BARREL";
case UpgradeType.SmartTargeting: return "SMART TARGETING";
case UpgradeType.RangefinderOptics: return "RANGEFINDER OPTICS";
case UpgradeType.DelayedFuse: return "DELAYED FUSE";
case UpgradeType.OverloadedChamber: return "OVERLOADED CHAMBER";
case UpgradeType.VolatileMunitions: return "VOLATILE MUNITIONS";
case UpgradeType.GlassCannonDesign: return "GLASS CANNON DESIGN";
case UpgradeType.ApocalypseRound: return "APOCALYPSE ROUND";
default: return type.ToString().ToUpperInvariant();
}
}
+467 -308
View File
@@ -6,160 +6,491 @@ using UnityEngine.UI;
public class UpgradeManager : MonoBehaviour
{
public GameObject[] normalUpgradePrefabs;
public GameObject[] specialUpgradePrefabs;
enum FreeUpgradeType
{
KillBonus,
HitBonus
}
[HideInInspector] public List<UpgradeCard.UpgradeType> unlockedUpgrades = new List<UpgradeCard.UpgradeType>();
struct CardDefinition
{
public UpgradeCard.UpgradeType type;
public string title;
public string description;
public string rarity;
public int maxStacks;
public int baseCost;
}
public Transform specialUpgradeSpawnParent;
public float specialUpgradeSpacing = 2f;
struct UpgradeOffer
{
public UpgradeCard.UpgradeType paidType;
public FreeUpgradeType freeType;
public bool isFree;
public string title;
public string description;
public string footer;
public string rarity;
}
public Tower tower;
public int bits = 5;
public TextMeshProUGUI bitsText;
private readonly Dictionary<UpgradeCard.UpgradeType, int> upgradeLevels = new Dictionary<UpgradeCard.UpgradeType, int>();
private readonly Dictionary<UpgradeCard.UpgradeType, int> upgradeCosts = new Dictionary<UpgradeCard.UpgradeType, int>();
private readonly Dictionary<UpgradeCard.UpgradeType, UpgradeCard> activeCards = new Dictionary<UpgradeCard.UpgradeType, UpgradeCard>();
readonly Dictionary<UpgradeCard.UpgradeType, int> upgradeLevels = new Dictionary<UpgradeCard.UpgradeType, int>();
readonly Dictionary<UpgradeCard.UpgradeType, int> upgradeCosts = new Dictionary<UpgradeCard.UpgradeType, int>();
private RectTransform upgradeDock;
private RectTransform upgradeCardRow;
private GameObject specialOverlay;
GameObject choiceOverlay;
Action currentChoiceComplete;
int bonusBitsPerKill;
float hitBitChance;
private static readonly UpgradeCard.UpgradeType[] NormalUpgradeTypes =
static readonly CardDefinition[] CannonCards =
{
UpgradeCard.UpgradeType.FireRate,
UpgradeCard.UpgradeType.Range,
UpgradeCard.UpgradeType.Damage
};
private static readonly UpgradeCard.UpgradeType[] SpecialUpgradeTypes =
{
UpgradeCard.UpgradeType.MultiShot,
UpgradeCard.UpgradeType.Homing,
UpgradeCard.UpgradeType.Ricochet
Card(UpgradeCard.UpgradeType.ReinforcedPowder, "REINFORCED POWDER", "Shell Damage +15%.", "Common", 8, 3),
Card(UpgradeCard.UpgradeType.ExpandedPayload, "EXPANDED PAYLOAD", "Blast Radius +12%.", "Common", 6, 3),
Card(UpgradeCard.UpgradeType.AutoLoader, "AUTO-LOADER", "Reload Speed +10%.", "Common", 8, 3),
Card(UpgradeCard.UpgradeType.HeavyCaliber, "HEAVY CALIBER", "Shells gain +25% damage but Reload Speed -8%.", "Uncommon", 5, 5),
Card(UpgradeCard.UpgradeType.ShockwaveShells, "SHOCKWAVE SHELLS", "Explosion knockback increased slightly.", "Uncommon", 4, 5),
Card(UpgradeCard.UpgradeType.ShrapnelRounds, "SHRAPNEL ROUNDS", "Explosion edges deal shrapnel damage equal to 35% shell damage.", "Rare", 5, 7),
Card(UpgradeCard.UpgradeType.FragmentBurst, "FRAGMENT BURST", "Explosions release 3 metal fragments.", "Rare", 3, 7),
Card(UpgradeCard.UpgradeType.BurningDebris, "BURNING DEBRIS", "Explosions leave burning ground for 2 seconds.", "Rare", 4, 7),
Card(UpgradeCard.UpgradeType.ConcussiveImpact, "CONCUSSIVE IMPACT", "Explosion hits slow enemies by 20% for 1 second.", "Rare", 3, 7),
Card(UpgradeCard.UpgradeType.CraterMaker, "CRATER MAKER", "Direct hits create a lingering slowing crater.", "Epic", 2, 10),
Card(UpgradeCard.UpgradeType.ChainDetonation, "CHAIN DETONATION", "Enemies killed by explosions detonate for 20% shell damage.", "Epic", 3, 10),
Card(UpgradeCard.UpgradeType.SiegeShells, "SIEGE SHELLS", "Every 5th shot fires a massive shell. Stacks reduce required shots.", "Epic", 3, 10),
Card(UpgradeCard.UpgradeType.ArmorPiercingCore, "ARMOR-PIERCING CORE", "Direct-hit damage pierces defenses, increasing direct damage.", "Rare", 4, 7),
Card(UpgradeCard.UpgradeType.DoubleCharge, "DOUBLE CHARGE", "Every reload has a 20% chance to instantly load a second shell.", "Rare", 3, 7),
Card(UpgradeCard.UpgradeType.SiegePlatform, "SIEGE PLATFORM", "Stable firing increases damage by 35%.", "Epic", 2, 10),
Card(UpgradeCard.UpgradeType.ExecutionShell, "EXECUTION SHELL", "Shells deal bonus damage to enemies above 80% health.", "Uncommon", 4, 5),
Card(UpgradeCard.UpgradeType.FinisherPayload, "FINISHER PAYLOAD", "Shells deal bonus damage to enemies below 30% health.", "Uncommon", 4, 5),
Card(UpgradeCard.UpgradeType.StabilizedBarrel, "STABILIZED BARREL", "Projectile speed +30%.", "Common", 4, 3),
Card(UpgradeCard.UpgradeType.SmartTargeting, "SMART TARGETING", "Prioritizes enemies near the center tower.", "Rare", 1, 7),
Card(UpgradeCard.UpgradeType.RangefinderOptics, "RANGEFINDER OPTICS", "Tower range +18%.", "Common", 5, 3),
Card(UpgradeCard.UpgradeType.DelayedFuse, "DELAYED FUSE", "Shells explode slightly later for deeper penetration and radius.", "Rare", 2, 7),
Card(UpgradeCard.UpgradeType.OverloadedChamber, "OVERLOADED CHAMBER", "Damage +40%, Reload Speed -15%.", "Rare", 3, 7),
Card(UpgradeCard.UpgradeType.VolatileMunitions, "VOLATILE MUNITIONS", "Critical kills create secondary explosions.", "Epic", 2, 10),
Card(UpgradeCard.UpgradeType.GlassCannonDesign, "GLASS CANNON DESIGN", "Massive damage, but pauses after every 10 shots to cool down.", "Legendary", 1, 14),
Card(UpgradeCard.UpgradeType.ApocalypseRound, "APOCALYPSE ROUND", "Every 20th shot fires an enormous extreme-damage shell.", "Legendary", 1, 14),
};
void Start()
{
EnsureUpgradeDock();
InitializeUpgrades();
EnsureBitsText();
UpdateBitsText();
}
public void EnemyKilled()
{
bits++;
bits += 1 + bonusBitsPerKill;
UpdateBitsText();
UpdateUpgradeCards();
}
void InitializeUpgrades()
public void EnemyHitByBullet()
{
foreach (UpgradeCard.UpgradeType type in Enum.GetValues(typeof(UpgradeCard.UpgradeType)))
{
if (!upgradeLevels.ContainsKey(type))
upgradeLevels[type] = 0;
if (!upgradeCosts.ContainsKey(type))
upgradeCosts[type] = GetBaseCost(type);
}
foreach (UpgradeCard.UpgradeType type in NormalUpgradeTypes)
{
SpawnUpgradeCard(type, 0f);
}
}
void SpawnUpgradeCard(UpgradeCard.UpgradeType upgradeType, float xPos)
{
if (activeCards.ContainsKey(upgradeType))
if (hitBitChance <= 0f)
return;
EnsureUpgradeDock();
int currentLevel = upgradeLevels[upgradeType] + 1;
GameObject cardObject = CreateUpgradeCardObject(upgradeType, upgradeCardRow, false);
UpgradeCard upgradeCard = cardObject.GetComponent<UpgradeCard>();
upgradeCard.upgradeType = upgradeType;
upgradeCard.upgradeLevel = currentLevel;
upgradeCard.upgradeCost = upgradeCosts[upgradeType];
upgradeCard.upgradeManager = this;
upgradeCard.UpdateUpgradeUI();
Button button = cardObject.GetComponent<Button>();
button.onClick.AddListener(() => TryUpgrade(upgradeType));
activeCards[upgradeType] = upgradeCard;
UpdateCardAffordability(upgradeCard);
if (UnityEngine.Random.value <= hitBitChance)
{
bits++;
UpdateBitsText();
}
}
public int GetBaseCost(UpgradeCard.UpgradeType type)
public void ShowWaveUpgradeChoice(Action onChoiceComplete)
{
switch (type)
if (choiceOverlay != null)
return;
InitializeUpgrades();
EnsureBitsText();
currentChoiceComplete = onChoiceComplete;
List<UpgradeOffer> offers = BuildOffers();
if (offers.Count == 0)
{
case UpgradeCard.UpgradeType.FireRate: return 3;
case UpgradeCard.UpgradeType.Range: return 2;
case UpgradeCard.UpgradeType.Damage: return 5;
default: return 1;
CompleteChoice();
return;
}
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);
}
public void ShowSpecialUpgradeChoice()
{
ShowWaveUpgradeChoice(null);
}
public void TryUpgrade(UpgradeCard.UpgradeType type)
{
if (!upgradeCosts.ContainsKey(type) || bits < upgradeCosts[type])
if (!upgradeCosts.ContainsKey(type) || bits < upgradeCosts[type] || IsAtMaxStacks(type))
return;
bits -= upgradeCosts[type];
BuyPaidUpgrade(type);
}
void InitializeUpgrades()
{
foreach (CardDefinition card in CannonCards)
{
if (!upgradeLevels.ContainsKey(card.type))
upgradeLevels[card.type] = 0;
if (!upgradeCosts.ContainsKey(card.type))
upgradeCosts[card.type] = card.baseCost;
}
}
List<UpgradeOffer> BuildOffers()
{
List<UpgradeOffer> paidOffers = new List<UpgradeOffer>();
foreach (CardDefinition card in CannonCards)
{
int cost = upgradeCosts[card.type];
if (bits < cost || IsAtMaxStacks(card.type))
continue;
int nextLevel = upgradeLevels[card.type] + 1;
paidOffers.Add(new UpgradeOffer
{
paidType = card.type,
isFree = false,
title = card.maxStacks == 1 ? card.title : $"{card.title} {ToRoman(nextLevel)}",
description = card.description,
footer = $"Cost: {cost} Bits",
rarity = card.rarity
});
}
if (paidOffers.Count > 0)
return PickRandomWeighted(paidOffers, 3);
return new List<UpgradeOffer>
{
new UpgradeOffer
{
freeType = FreeUpgradeType.KillBonus,
isFree = true,
title = $"SALVAGE BONUS {ToRoman(bonusBitsPerKill + 1)}",
description = "Enemies killed give +1 extra bit from now on.",
footer = "Free",
rarity = "Supply"
},
new UpgradeOffer
{
freeType = FreeUpgradeType.HitBonus,
isFree = true,
title = $"SCRAP ROUNDS {ToRoman(Mathf.RoundToInt(hitBitChance / 0.4f) + 1)}",
description = "Bullet hits gain a +40% chance to give 1 bit.",
footer = "Free",
rarity = "Supply"
}
};
}
static List<UpgradeOffer> PickRandomWeighted(List<UpgradeOffer> source, int count)
{
List<UpgradeOffer> pool = new List<UpgradeOffer>(source);
List<UpgradeOffer> result = new List<UpgradeOffer>();
while (pool.Count > 0 && result.Count < count)
{
float totalWeight = 0f;
foreach (UpgradeOffer offer in pool)
totalWeight += GetRarityWeight(offer.rarity);
float roll = UnityEngine.Random.value * totalWeight;
int pickedIndex = 0;
for (int i = 0; i < pool.Count; i++)
{
roll -= GetRarityWeight(pool[i].rarity);
if (roll <= 0f)
{
pickedIndex = i;
break;
}
}
result.Add(pool[pickedIndex]);
pool.RemoveAt(pickedIndex);
}
return result;
}
static float GetRarityWeight(string rarity)
{
switch (rarity)
{
case "Common": return 10f;
case "Uncommon": return 6f;
case "Rare": return 3.5f;
case "Epic": return 1.5f;
case "Legendary": return 0.55f;
default: return 1f;
}
}
void CreateChoiceCard(UpgradeOffer offer, Transform parent)
{
GameObject cardObject = new GameObject(offer.title + "Card", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image), typeof(Button));
RectTransform cardRect = cardObject.GetComponent<RectTransform>();
cardRect.SetParent(parent, false);
LayoutElement layout = cardObject.AddComponent<LayoutElement>();
layout.preferredWidth = 212f;
layout.preferredHeight = 198f;
layout.minWidth = layout.preferredWidth;
layout.minHeight = layout.preferredHeight;
Image image = cardObject.GetComponent<Image>();
image.color = offer.isFree ? new Color(0.11f, 0.17f, 0.13f, 0.98f) : GetRarityColor(offer.rarity);
Button button = cardObject.GetComponent<Button>();
ColorBlock colors = button.colors;
colors.normalColor = image.color;
colors.highlightedColor = image.color + new Color(0.04f, 0.05f, 0.06f, 0f);
colors.pressedColor = new Color(0.06f, 0.28f, 0.36f, 1f);
colors.selectedColor = colors.highlightedColor;
colors.disabledColor = new Color(0.08f, 0.08f, 0.09f, 0.78f);
colors.colorMultiplier = 1f;
button.colors = colors;
button.onClick.AddListener(() => SelectOffer(offer));
TextMeshProUGUI rarity = CreateText("Rarity", cardRect, offer.rarity.ToUpperInvariant(), 11f, FontStyles.Bold, TextAlignmentOptions.Center);
TextMeshProUGUI title = CreateText("Title", cardRect, offer.title, 16f, FontStyles.Bold, TextAlignmentOptions.Center);
TextMeshProUGUI description = CreateText("Description", cardRect, offer.description, 12f, FontStyles.Normal, TextAlignmentOptions.TopLeft);
TextMeshProUGUI footer = CreateText("Footer", cardRect, offer.footer, 14f, FontStyles.Bold, TextAlignmentOptions.Center);
footer.color = offer.isFree ? new Color(0.56f, 0.95f, 0.58f, 1f) : new Color(0.98f, 0.87f, 0.32f, 1f);
PlaceCardText(rarity.rectTransform, 12f, -8f, -12f, 18f, true);
PlaceCardText(title.rectTransform, 12f, -30f, -12f, 46f, true);
PlaceCardText(description.rectTransform, 16f, -82f, -16f, 72f, true);
PlaceCardText(footer.rectTransform, 12f, 12f, -12f, 28f, false);
}
static Color GetRarityColor(string rarity)
{
switch (rarity)
{
case "Common": return new Color(0.1f, 0.12f, 0.14f, 0.98f);
case "Uncommon": return new Color(0.09f, 0.16f, 0.12f, 0.98f);
case "Rare": return new Color(0.08f, 0.13f, 0.2f, 0.98f);
case "Epic": return new Color(0.15f, 0.1f, 0.19f, 0.98f);
case "Legendary": return new Color(0.2f, 0.15f, 0.07f, 0.98f);
default: return new Color(0.1f, 0.11f, 0.14f, 0.98f);
}
}
void SelectOffer(UpgradeOffer offer)
{
if (offer.isFree)
ApplyFreeUpgrade(offer.freeType);
else
BuyPaidUpgrade(offer.paidType);
CompleteChoice();
}
void BuyPaidUpgrade(UpgradeCard.UpgradeType type)
{
int cost = upgradeCosts[type];
if (bits < cost || IsAtMaxStacks(type))
return;
bits -= cost;
upgradeLevels[type]++;
upgradeCosts[type] += Mathf.Max(1, Mathf.RoundToInt(upgradeCosts[type] * 0.5f));
ApplyUpgrade(type);
upgradeCosts[type] += Mathf.Max(1, Mathf.RoundToInt(cost * 0.45f));
ApplyTowerUpgrade(type);
UpdateBitsText();
}
void ApplyUpgrade(UpgradeCard.UpgradeType type)
void ApplyTowerUpgrade(UpgradeCard.UpgradeType type)
{
if (tower == null)
tower = FindAnyObjectByType<Tower>();
if (tower == null)
return;
switch (type)
{
case UpgradeCard.UpgradeType.ReinforcedPowder:
tower.shellDamageMultiplier *= 1.15f;
break;
case UpgradeCard.UpgradeType.ExpandedPayload:
tower.splashRadius *= 1.12f;
break;
case UpgradeCard.UpgradeType.AutoLoader:
tower.fireRate *= 0.9f;
break;
case UpgradeCard.UpgradeType.HeavyCaliber:
tower.shellDamageMultiplier *= 1.25f;
tower.fireRate *= 1.08f;
break;
case UpgradeCard.UpgradeType.ShockwaveShells:
tower.explosionKnockback += 0.12f;
break;
case UpgradeCard.UpgradeType.ShrapnelRounds:
tower.shrapnelEdgeDamageMultiplier += 0.35f;
break;
case UpgradeCard.UpgradeType.FragmentBurst:
tower.fragmentCount += 3;
break;
case UpgradeCard.UpgradeType.BurningDebris:
tower.burningGroundDuration += 2f;
break;
case UpgradeCard.UpgradeType.ConcussiveImpact:
tower.concussiveSlowMultiplier = Mathf.Min(tower.concussiveSlowMultiplier, Mathf.Max(0.35f, 0.8f - 0.06f * upgradeLevels[type]));
tower.concussiveSlowDuration += 1f;
break;
case UpgradeCard.UpgradeType.CraterMaker:
tower.craterDuration += 2.5f;
tower.craterSlowMultiplier = Mathf.Min(tower.craterSlowMultiplier, 0.7f);
break;
case UpgradeCard.UpgradeType.ChainDetonation:
tower.chainDetonationMultiplier += 0.2f;
break;
case UpgradeCard.UpgradeType.SiegeShells:
tower.siegeShellStacks++;
break;
case UpgradeCard.UpgradeType.ArmorPiercingCore:
tower.directHitDamageMultiplier *= 1.25f;
break;
case UpgradeCard.UpgradeType.DoubleCharge:
tower.doubleChargeChance += 0.2f;
break;
case UpgradeCard.UpgradeType.SiegePlatform:
tower.siegePlatformStacks++;
break;
case UpgradeCard.UpgradeType.ExecutionShell:
tower.executionDamageMultiplier += 0.25f;
break;
case UpgradeCard.UpgradeType.FinisherPayload:
tower.finisherDamageMultiplier += 0.25f;
break;
case UpgradeCard.UpgradeType.StabilizedBarrel:
tower.projectileSpeedMultiplier *= 1.3f;
break;
case UpgradeCard.UpgradeType.SmartTargeting:
tower.smartTargeting = true;
break;
case UpgradeCard.UpgradeType.RangefinderOptics:
tower.attackRange *= 1.18f;
break;
case UpgradeCard.UpgradeType.DelayedFuse:
tower.delayedFuseSeconds += 0.08f;
tower.delayedFuseRadiusMultiplier *= 1.16f;
break;
case UpgradeCard.UpgradeType.OverloadedChamber:
tower.shellDamageMultiplier *= 1.4f;
tower.fireRate *= 1.15f;
break;
case UpgradeCard.UpgradeType.VolatileMunitions:
tower.volatileSecondaryExplosionMultiplier += 0.35f;
break;
case UpgradeCard.UpgradeType.GlassCannonDesign:
tower.glassCannonDesign = true;
tower.shellDamageMultiplier *= 2.25f;
break;
case UpgradeCard.UpgradeType.ApocalypseRound:
tower.apocalypseRound = true;
break;
}
}
void ApplyFreeUpgrade(FreeUpgradeType type)
{
switch (type)
{
case UpgradeCard.UpgradeType.FireRate:
tower.fireRate *= 0.9f;
case FreeUpgradeType.KillBonus:
bonusBitsPerKill++;
break;
case UpgradeCard.UpgradeType.Range:
tower.attackRange += 2.5f;
break;
case UpgradeCard.UpgradeType.Damage:
tower.damage += 1;
break;
case UpgradeCard.UpgradeType.Ricochet:
tower.ricochetEnabled = true;
break;
case UpgradeCard.UpgradeType.Homing:
tower.homingEnabled = true;
break;
case UpgradeCard.UpgradeType.MultiShot:
tower.extraBullets += 1;
case FreeUpgradeType.HitBonus:
hitBitChance += 0.4f;
break;
}
UpdateUpgradeCards();
UpdateBitsText();
}
void UpdateUpgradeCards()
void CompleteChoice()
{
foreach (UpgradeCard card in activeCards.Values)
if (choiceOverlay != null)
Destroy(choiceOverlay);
choiceOverlay = null;
Time.timeScale = 1f;
Action callback = currentChoiceComplete;
currentChoiceComplete = null;
callback?.Invoke();
}
bool IsAtMaxStacks(UpgradeCard.UpgradeType type)
{
CardDefinition card = GetCard(type);
return card.maxStacks > 0 && upgradeLevels.TryGetValue(type, out int level) && level >= card.maxStacks;
}
void EnsureBitsText()
{
if (bitsText != null)
{
card.upgradeLevel = upgradeLevels[card.upgradeType] + 1;
card.upgradeCost = upgradeCosts[card.upgradeType];
card.UpdateUpgradeUI();
UpdateCardAffordability(card);
StyleBitsText();
return;
}
Canvas canvas = FindAnyObjectByType<Canvas>();
if (canvas == null)
{
GameObject canvasObject = new GameObject("GameUI", typeof(RectTransform), typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster));
canvas = canvasObject.GetComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
CanvasScaler scaler = canvasObject.GetComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(800f, 600f);
scaler.matchWidthOrHeight = 0.5f;
}
Transform existing = canvas.transform.Find("BitsCounter");
if (existing != null)
bitsText = existing.GetComponent<TextMeshProUGUI>();
if (bitsText == null)
{
GameObject textObject = new GameObject("BitsCounter", typeof(RectTransform), typeof(CanvasRenderer), typeof(TextMeshProUGUI));
RectTransform rect = textObject.GetComponent<RectTransform>();
rect.SetParent(canvas.transform, false);
bitsText = textObject.GetComponent<TextMeshProUGUI>();
}
StyleBitsText();
}
void UpdateBitsText()
{
if (bitsText == null)
return;
StyleBitsText();
EnsureBitsText();
bitsText.text = "BITS " + bits;
}
@@ -172,7 +503,7 @@ public class UpgradeManager : MonoBehaviour
rect.anchorMax = new Vector2(0f, 1f);
rect.pivot = new Vector2(0f, 1f);
rect.anchoredPosition = new Vector2(24f, -24f);
rect.sizeDelta = new Vector2(150f, 42f);
rect.sizeDelta = new Vector2(170f, 42f);
}
bitsText.alignment = TextAlignmentOptions.Center;
@@ -188,180 +519,10 @@ public class UpgradeManager : MonoBehaviour
outline.effectDistance = new Vector2(1.5f, -1.5f);
}
public void ShowSpecialUpgradeChoice()
GameObject CreateOverlay(bool freeFallback)
{
EnsureUpgradeDock();
List<UpgradeCard.UpgradeType> available = new List<UpgradeCard.UpgradeType>();
foreach (UpgradeCard.UpgradeType type in SpecialUpgradeTypes)
{
if (!unlockedUpgrades.Contains(type))
available.Add(type);
}
if (available.Count == 0)
return;
for (int i = 0; i < available.Count; i++)
{
int rand = UnityEngine.Random.Range(i, available.Count);
UpgradeCard.UpgradeType temp = available[i];
available[i] = available[rand];
available[rand] = temp;
}
Time.timeScale = 0f;
specialOverlay = CreateOverlay();
RectTransform row = CreateRect("SpecialUpgradeRow", specialOverlay.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.sizeDelta = new Vector2(590f, 190f);
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;
int numToShow = Mathf.Min(3, available.Count);
for (int i = 0; i < numToShow; i++)
{
UpgradeCard.UpgradeType type = available[i];
GameObject cardObject = CreateUpgradeCardObject(type, row, true);
UpgradeCard card = cardObject.GetComponent<UpgradeCard>();
card.upgradeType = type;
card.upgradeLevel = 1;
card.upgradeCost = 0;
card.upgradeManager = this;
card.UpdateUpgradeUI();
card.upgradeCostText.text = "UNLOCK";
Button button = cardObject.GetComponent<Button>();
button.onClick.AddListener(() => SelectSpecialUpgrade(type));
}
}
public void UnlockSpecialUpgrade(UpgradeCard.UpgradeType type)
{
if (!upgradeLevels.ContainsKey(type))
{
upgradeLevels[type] = 0;
upgradeCosts[type] = GetBaseCost(type);
}
if (!unlockedUpgrades.Contains(type))
unlockedUpgrades.Add(type);
SpawnUpgradeCard(type, 0f);
}
void SelectSpecialUpgrade(UpgradeCard.UpgradeType type)
{
UnlockSpecialUpgrade(type);
if (specialOverlay != null)
Destroy(specialOverlay);
specialOverlay = null;
Time.timeScale = 1f;
}
void EnsureUpgradeDock()
{
if (upgradeDock != null)
return;
Canvas canvas = FindAnyObjectByType<Canvas>();
if (canvas == null)
{
GameObject canvasObject = new GameObject("GameUI", typeof(RectTransform), typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster));
canvas = canvasObject.GetComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
CanvasScaler scaler = canvasObject.GetComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(800f, 600f);
scaler.matchWidthOrHeight = 0.5f;
}
Transform existingDock = canvas.transform.Find("UpgradeDock");
upgradeDock = existingDock != null ? existingDock.GetComponent<RectTransform>() : CreateRect("UpgradeDock", canvas.transform);
upgradeDock.anchorMin = new Vector2(0f, 0f);
upgradeDock.anchorMax = new Vector2(1f, 0f);
upgradeDock.pivot = new Vector2(0.5f, 0f);
upgradeDock.anchoredPosition = Vector2.zero;
upgradeDock.sizeDelta = new Vector2(0f, 150f);
upgradeDock.SetAsLastSibling();
Image dockImage = upgradeDock.GetComponent<Image>();
if (dockImage == null)
dockImage = upgradeDock.gameObject.AddComponent<Image>();
dockImage.color = new Color(0.03f, 0.035f, 0.045f, 0.94f);
dockImage.raycastTarget = true;
Transform existingRow = upgradeDock.Find("Cards");
upgradeCardRow = existingRow != null ? existingRow.GetComponent<RectTransform>() : CreateRect("Cards", upgradeDock);
upgradeCardRow.anchorMin = new Vector2(0.5f, 0.5f);
upgradeCardRow.anchorMax = new Vector2(0.5f, 0.5f);
upgradeCardRow.pivot = new Vector2(0.5f, 0.5f);
upgradeCardRow.anchoredPosition = new Vector2(0f, 6f);
upgradeCardRow.sizeDelta = new Vector2(720f, 126f);
HorizontalLayoutGroup layout = upgradeCardRow.GetComponent<HorizontalLayoutGroup>();
if (layout == null)
layout = upgradeCardRow.gameObject.AddComponent<HorizontalLayoutGroup>();
layout.spacing = 12f;
layout.childAlignment = TextAnchor.MiddleCenter;
layout.childControlWidth = true;
layout.childControlHeight = true;
layout.childForceExpandWidth = false;
layout.childForceExpandHeight = false;
}
GameObject CreateUpgradeCardObject(UpgradeCard.UpgradeType type, Transform parent, bool specialChoice)
{
GameObject cardObject = new GameObject(type + "Card", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image), typeof(Button), typeof(UpgradeCard));
RectTransform cardRect = cardObject.GetComponent<RectTransform>();
cardRect.SetParent(parent, false);
LayoutElement layout = cardObject.AddComponent<LayoutElement>();
layout.preferredWidth = specialChoice ? 180f : 150f;
layout.preferredHeight = specialChoice ? 180f : 116f;
layout.minWidth = layout.preferredWidth;
layout.minHeight = layout.preferredHeight;
Image image = cardObject.GetComponent<Image>();
image.color = specialChoice ? new Color(0.11f, 0.12f, 0.16f, 0.98f) : new Color(0.1f, 0.11f, 0.14f, 0.98f);
Button button = cardObject.GetComponent<Button>();
ColorBlock colors = button.colors;
colors.normalColor = image.color;
colors.highlightedColor = new Color(0.16f, 0.19f, 0.25f, 1f);
colors.pressedColor = new Color(0.06f, 0.28f, 0.36f, 1f);
colors.selectedColor = colors.highlightedColor;
colors.disabledColor = new Color(0.08f, 0.08f, 0.09f, 0.78f);
colors.colorMultiplier = 1f;
button.colors = colors;
UpgradeCard card = cardObject.GetComponent<UpgradeCard>();
card.upgradeText = CreateText("UpgradeText", cardRect, GetDisplayName(type), specialChoice ? 20f : 17f, FontStyles.Bold, TextAlignmentOptions.Center);
card.upgradeDescription = CreateText("UpgradeDescription", cardRect, GetDescription(type), specialChoice ? 13f : 11f, FontStyles.Normal, TextAlignmentOptions.TopLeft);
card.upgradeCostText = CreateText("UpgradeCost", cardRect, "", specialChoice ? 12f : 11f, FontStyles.Bold, TextAlignmentOptions.Center);
PlaceCardText(card.upgradeText.rectTransform, 10f, -8f, -10f, 28f, true);
PlaceCardText(card.upgradeDescription.rectTransform, 12f, -40f, -12f, 44f, true);
PlaceCardText(card.upgradeCostText.rectTransform, 10f, 8f, -10f, 22f, false);
return cardObject;
}
GameObject CreateOverlay()
{
Canvas canvas = upgradeDock.GetComponentInParent<Canvas>();
GameObject overlay = new GameObject("SpecialUpgradeOverlay", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
Canvas canvas = bitsText.GetComponentInParent<Canvas>();
GameObject overlay = new GameObject("WaveUpgradeOverlay", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
RectTransform rect = overlay.GetComponent<RectTransform>();
rect.SetParent(canvas.transform, false);
rect.anchorMin = Vector2.zero;
@@ -374,29 +535,49 @@ public class UpgradeManager : MonoBehaviour
image.color = new Color(0f, 0f, 0f, 0.74f);
image.raycastTarget = true;
TextMeshProUGUI title = CreateText("Title", rect, "CHOOSE UPGRADE", 28f, FontStyles.Bold, TextAlignmentOptions.Center);
string titleText = freeFallback ? "LOW BITS: CHOOSE SUPPLY BOOST" : "CHOOSE CANNON UPGRADE";
TextMeshProUGUI title = CreateText("Title", rect, titleText, 28f, FontStyles.Bold, TextAlignmentOptions.Center);
RectTransform titleRect = title.rectTransform;
titleRect.anchorMin = new Vector2(0.5f, 0.5f);
titleRect.anchorMax = new Vector2(0.5f, 0.5f);
titleRect.pivot = new Vector2(0.5f, 0.5f);
titleRect.anchoredPosition = new Vector2(0f, 128f);
titleRect.sizeDelta = new Vector2(420f, 42f);
titleRect.anchoredPosition = new Vector2(0f, 138f);
titleRect.sizeDelta = new Vector2(640f, 42f);
TextMeshProUGUI bitsLabel = CreateText("Bits", rect, "BITS " + bits, 18f, FontStyles.Bold, TextAlignmentOptions.Center);
bitsLabel.color = new Color(0.98f, 0.87f, 0.32f, 1f);
RectTransform bitsRect = bitsLabel.rectTransform;
bitsRect.anchorMin = new Vector2(0.5f, 0.5f);
bitsRect.anchorMax = new Vector2(0.5f, 0.5f);
bitsRect.pivot = new Vector2(0.5f, 0.5f);
bitsRect.anchoredPosition = new Vector2(0f, 104f);
bitsRect.sizeDelta = new Vector2(220f, 30f);
return overlay;
}
void UpdateCardAffordability(UpgradeCard card)
static CardDefinition Card(UpgradeCard.UpgradeType type, string title, string description, string rarity, int maxStacks, int baseCost)
{
Button button = card.GetComponent<Button>();
if (button != null)
button.interactable = bits >= card.upgradeCost;
if (card.upgradeCostText != null)
return new CardDefinition
{
card.upgradeCostText.color = bits >= card.upgradeCost
? new Color(0.98f, 0.87f, 0.32f, 1f)
: new Color(0.95f, 0.34f, 0.32f, 1f);
type = type,
title = title,
description = description,
rarity = rarity,
maxStacks = maxStacks,
baseCost = baseCost
};
}
static CardDefinition GetCard(UpgradeCard.UpgradeType type)
{
foreach (CardDefinition card in CannonCards)
{
if (card.type == type)
return card;
}
return CannonCards[0];
}
static RectTransform CreateRect(string name, Transform parent)
@@ -433,31 +614,9 @@ public class UpgradeManager : MonoBehaviour
rect.offsetMax = new Vector2(right, fromTop ? top : top + height);
}
static string GetDisplayName(UpgradeCard.UpgradeType type)
static string ToRoman(int number)
{
switch (type)
{
case UpgradeCard.UpgradeType.FireRate: return "FIRE RATE";
case UpgradeCard.UpgradeType.Range: return "RANGE";
case UpgradeCard.UpgradeType.Damage: return "DAMAGE";
case UpgradeCard.UpgradeType.MultiShot: return "MULTI SHOT";
case UpgradeCard.UpgradeType.Homing: return "HOMING";
case UpgradeCard.UpgradeType.Ricochet: return "RICOCHET";
default: return type.ToString().ToUpperInvariant();
}
}
static string GetDescription(UpgradeCard.UpgradeType type)
{
switch (type)
{
case UpgradeCard.UpgradeType.FireRate: return "Shoot 10% faster.";
case UpgradeCard.UpgradeType.Range: return "Increase targeting range.";
case UpgradeCard.UpgradeType.Damage: return "Add 1 damage per bullet.";
case UpgradeCard.UpgradeType.MultiShot: return "Fire one extra bullet.";
case UpgradeCard.UpgradeType.Homing: return "Bullets track targets.";
case UpgradeCard.UpgradeType.Ricochet: return "Shots can bounce onward.";
default: return "";
}
string[] numerals = { "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X" };
return number < 1 || number > 10 ? number.ToString() : numerals[number - 1];
}
}