using System; using System.Collections.Generic; using TMPro; using UnityEngine; using UnityEngine.UI; public class UpgradeManager : MonoBehaviour { enum FreeUpgradeType { KillBonus, HitBonus } struct CardDefinition { public UpgradeCard.UpgradeType type; public string title; public string description; public string rarity; public int maxStacks; public int baseCost; } 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; readonly Dictionary upgradeLevels = new Dictionary(); readonly Dictionary upgradeCosts = new Dictionary(); GameObject choiceOverlay; RectTransform currentCardRow; bool currentGuaranteeEpicOrHigher; Action currentChoiceComplete; int bonusBitsPerKill; float hitBitChance; int rerollCost = 1; static readonly CardDefinition[] CannonCards = { 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() { InitializeUpgrades(); EnsureBitsText(); UpdateBitsText(); } public void EnemyKilled() { bits += 1 + bonusBitsPerKill; UpdateBitsText(); } public void EnemyHitByBullet() { if (hitBitChance <= 0f) return; if (UnityEngine.Random.value <= hitBitChance) { bits++; UpdateBitsText(); } } public void ShowWaveUpgradeChoice(Action onChoiceComplete, bool guaranteeEpicOrHigher = false) { if (choiceOverlay != null) return; InitializeUpgrades(); EnsureBitsText(); currentChoiceComplete = onChoiceComplete; currentGuaranteeEpicOrHigher = guaranteeEpicOrHigher; rerollCost = 1; List offers = BuildOffers(guaranteeEpicOrHigher); if (offers.Count == 0) { CompleteChoice(); return; } Time.timeScale = 0f; choiceOverlay = CreateOverlay(offers[0].isFree); currentCardRow = CreateChoiceRow(choiceOverlay.transform); PopulateChoiceCards(offers); CreateRerollButton(choiceOverlay.transform); } public void ShowSpecialUpgradeChoice() { ShowWaveUpgradeChoice(null); } public void TryUpgrade(UpgradeCard.UpgradeType type) { if (!upgradeCosts.ContainsKey(type) || bits < upgradeCosts[type] || IsAtMaxStacks(type)) return; 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 BuildOffers(bool guaranteeEpicOrHigher) { List paidOffers = new List(); 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, guaranteeEpicOrHigher); return new List { 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 PickRandomWeighted(List source, int count, bool guaranteeEpicOrHigher) { List pool = new List(source); List result = new List(); if (guaranteeEpicOrHigher) { List highRarity = new List(); foreach (UpgradeOffer offer in pool) { if (IsEpicOrHigher(offer.rarity)) highRarity.Add(offer); } if (highRarity.Count > 0) { UpgradeOffer guaranteed = highRarity[UnityEngine.Random.Range(0, highRarity.Count)]; result.Add(guaranteed); pool.Remove(guaranteed); } } while (pool.Count > 0 && result.Count < count) { float totalWeight = 0f; 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 bool IsEpicOrHigher(string rarity) { return rarity == "Epic" || rarity == "Legendary"; } 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(); cardRect.SetParent(parent, false); LayoutElement layout = cardObject.AddComponent(); layout.preferredWidth = 212f; layout.preferredHeight = 198f; layout.minWidth = layout.preferredWidth; layout.minHeight = layout.preferredHeight; Image image = cardObject.GetComponent(); image.color = offer.isFree ? new Color(0.11f, 0.17f, 0.13f, 0.98f) : GetRarityColor(offer.rarity); Outline outline = cardObject.AddComponent(); outline.effectDistance = new Vector2(3f, -3f); outline.effectColor = GetRarityBorderColor(offer.rarity); RarityPulse pulse = cardObject.AddComponent(); pulse.Configure(outline, GetRarityBorderColor(offer.rarity)); Button button = cardObject.GetComponent