implement more changes
This commit is contained in:
@@ -468,7 +468,7 @@ GameObject:
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
m_IsActive: 0
|
||||
--- !u!224 &1038208932
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -738,7 +738,7 @@ GameObject:
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
m_IsActive: 0
|
||||
--- !u!224 &1406026031
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
|
||||
+268
-17
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user