using System; using System.Collections.Generic; using Bansonic; using UnityEngine; using UnityEngine.UI; public class idolUpgrade : MonoBehaviour { [Serializable] private sealed class PendingDebtMaterialEntry { public int materialKind; public int amount; } [Serializable] private sealed class PendingDebtState { public int pendingCoins; public List materials = new List(); } private sealed class AggregatedMaterialRequirement { public growthMaterialSO material; public DushMaterialKind materialKind; public expBottlesSO bottle; public ExpBottleKind bottleKind; public int requiredAmount; public bool isUniversalOption; public bool isBottleRequirement; } [Header("Config")] public upgradeConfig config; [Header("Runtime")] public AllyHero_SO currentHero; [Header("objects")] public GameObject materialPrefab; public Transform materialContainer; [Header("the Button")] public Button yesdoButton; public Text yesdoButtonText; public Text infoButton; public Text upgradeText; private readonly List spawnedMaterials = new List(); private readonly Dictionary spawnedBottleBindings = new Dictionary(); private expBottlesSO selectedBottle; private DushMaterialKind? selectedBreakthroughMaterialKind; private bool isShowingBreakthrough; private string lastOperationSuccessMessage; private storeItemSO[] cachedStoreItems; private static readonly Color UpgradeWarningColor = new Color32(120, 36, 36, 255); private static readonly Color UpgradeMaxColor = new Color32(205, 170, 64, 255); private void Awake() { BindButtons(); } private void OnEnable() { BindButtons(); BindLedgerEvents(); RefreshView(); } private void OnDisable() { UnbindLedgerEvents(); } public void SetHero(AllyHero_SO hero) { bool heroChanged = currentHero == null || hero == null || currentHero.ally_heroID != hero.ally_heroID; currentHero = hero; if (heroChanged) { selectedBottle = null; selectedBreakthroughMaterialKind = null; } RefreshView(); } private void BindLedgerEvents() { PlayerEconomyLedger.EnsureInstance().OnCoinsChanged -= HandleExternalResourceChanged; PlayerEconomyLedger.EnsureInstance().OnCoinsChanged += HandleExternalResourceChanged; PlayerEconomyLedger.EnsureInstance().OnMaterialChanged -= HandleExternalResourceChanged; PlayerEconomyLedger.EnsureInstance().OnMaterialChanged += HandleExternalResourceChanged; ExpBottleLedger.EnsureInstance().OnBottleCountChanged -= HandleBottleLedgerChanged; ExpBottleLedger.EnsureInstance().OnBottleCountChanged += HandleBottleLedgerChanged; ExpBottleLedger.EnsureInstance().OnLedgerReloaded -= HandleExternalLedgerReloaded; ExpBottleLedger.EnsureInstance().OnLedgerReloaded += HandleExternalLedgerReloaded; DushMaterialLedger.EnsureInstance().OnMaterialCountChanged -= HandleMaterialLedgerChanged; DushMaterialLedger.EnsureInstance().OnMaterialCountChanged += HandleMaterialLedgerChanged; AllyHeroDeployLedger.EnsureInstance().OnHeroGrowthChanged -= HandleHeroGrowthLedgerChanged; AllyHeroDeployLedger.EnsureInstance().OnHeroGrowthChanged += HandleHeroGrowthLedgerChanged; } private void UnbindLedgerEvents() { if (PlayerEconomyLedger.Instance != null) { PlayerEconomyLedger.Instance.OnCoinsChanged -= HandleExternalResourceChanged; PlayerEconomyLedger.Instance.OnMaterialChanged -= HandleExternalResourceChanged; } if (ExpBottleLedger.Instance != null) { ExpBottleLedger.Instance.OnBottleCountChanged -= HandleBottleLedgerChanged; ExpBottleLedger.Instance.OnLedgerReloaded -= HandleExternalLedgerReloaded; } if (DushMaterialLedger.Instance != null) { DushMaterialLedger.Instance.OnMaterialCountChanged -= HandleMaterialLedgerChanged; } if (AllyHeroDeployLedger.Instance != null) { AllyHeroDeployLedger.Instance.OnHeroGrowthChanged -= HandleHeroGrowthLedgerChanged; } } private void HandleExternalResourceChanged(int _) { RefreshViewIfActive(); } private void HandleBottleLedgerChanged(ExpBottleKind _, int __) { RefreshViewIfActive(); } private void HandleExternalLedgerReloaded() { RefreshViewIfActive(); } private void HandleMaterialLedgerChanged(DushMaterialKind _, int __) { RefreshViewIfActive(); } private void HandleHeroGrowthLedgerChanged(int heroId) { if (currentHero == null || heroId <= 0 || currentHero.ally_heroID != heroId) { return; } RefreshViewIfActive(); } private void RefreshViewIfActive() { if (!isActiveAndEnabled) { return; } RefreshView(); } public HeroGrowthTier GetCurrentTier() { return AllyHeroGrowthService.GetUnlockedTier(currentHero); } public upgradeConfig.TierRule GetCurrentRule() { if (config == null || currentHero == null) { return null; } HeroGrowthTier currentTier = GetCurrentTier(); if (currentTier >= HeroGrowthTier.S) { return null; } return config.GetRule(currentTier); } public List GetCurrentLinkedBottles() { upgradeConfig.TierRule rule = GetCurrentRule(); return rule != null ? rule.linkedExpBottles : null; } public List GetCurrentBreakthroughCosts() { upgradeConfig.TierRule rule = GetCurrentRule(); return rule != null ? rule.breakthroughCosts : null; } public int GetCurrentBreakthroughCoinCost() { upgradeConfig.TierRule rule = GetCurrentRule(); return rule != null ? Mathf.Max(0, rule.requiredCoins) : 0; } public upgradeConfig.SpecialBottleRule GetSpecialBottleRule(expBottlesSO bottle) { return config != null ? config.GetSpecialBottleRule(bottle) : null; } public void RefreshView() { ClearMaterialItems(); SetUpgradeTextVisible(false, string.Empty, UpgradeWarningColor); if (currentHero == null || config == null) { SetInfoText(string.Empty); SetConfirmInteractable(false); SetConfirmButtonText("不可用"); return; } if (GetCurrentTier() == HeroGrowthTier.S) { selectedBottle = null; selectedBreakthroughMaterialKind = null; isShowingBreakthrough = false; SetInfoText("已达最高阶"); SetConfirmInteractable(false); SetConfirmButtonText("已满级"); SetUpgradeTextVisible(true, "满级角色已不可培养", UpgradeMaxColor); return; } isShowingBreakthrough = ShouldShowBreakthrough(); if (isShowingBreakthrough) { List requirements = BuildEffectiveBreakthroughRequirements(); bool requiresFullSubmission = HasPendingDebtRequirements(); bool bundledSelection = ShouldUseBundledBreakthroughSelection(requirements, requiresFullSubmission); BuildBreakthroughMaterialList(); string prefix = requiresFullSubmission ? "须全部提交," : string.Empty; if (bundledSelection && selectedBreakthroughMaterialKind.HasValue && selectedBreakthroughMaterialKind.Value != DushMaterialKind.Material78024) { prefix += "所选两项材料为一组,"; } SetInfoText(string.Format("{0}突破需消耗 {1} 算力", prefix, GetEffectiveBreakthroughCoinCost())); SetConfirmInteractable(true); SetConfirmButtonText("突破"); return; } if (!BuildExpBottleList()) { selectedBottle = null; SetInfoText(string.Empty); SetConfirmInteractable(false); SetConfirmButtonText("不可用"); SetUpgradeTextVisible(true, "未拥有相关材料", UpgradeWarningColor); return; } UpdateTrainingInfoAndSelectionState(); } private void BindButtons() { if (yesdoButton != null) { yesdoButton.onClick.RemoveListener(HandleYesDoClicked); yesdoButton.onClick.AddListener(HandleYesDoClicked); } } private void HandleYesDoClicked() { if (currentHero == null) { gNotice.warning.display("未选择偶像"); return; } if (isShowingBreakthrough) { string failureReason; if (!TryBreakthroughCurrentHero(out failureReason)) { gNotice.warning.display(failureReason); RefreshView(); return; } gNotice.recommendation.display("突破成功"); RefreshView(); return; } if (selectedBottle == null) { gNotice.warning.display("请选择经验瓶"); return; } string bottleName = GetDisplayName(selectedBottle); string failure; int consumedCount; int grantedExp; string successMessage; if (!TryUseSelectedBottle(selectedBottle, out failure, out consumedCount, out grantedExp, out successMessage)) { gNotice.warning.display(failure); RefreshView(); return; } gNotice.recommendation.display(!string.IsNullOrWhiteSpace(successMessage) ? successMessage : string.Format("给{0}增加了{1}经验", currentHero.ally_heroName, grantedExp)); RefreshView(); SetInfoText(bottleName); } private bool BuildExpBottleList() { List validBottles = GetDisplayableBottles(); if (validBottles.Count == 0) { return false; } if (selectedBottle == null || !validBottles.Contains(selectedBottle)) { selectedBottle = validBottles[0]; } for (int i = 0; i < validBottles.Count; i++) { expBottlesSO bottle = validBottles[i]; materialPrefab item = CreateMaterialItem(); if (item == null) { continue; } item.Bind( bottle.expBottleSprite, GetDisplayName(bottle), Mathf.Max(0, ExpBottleLedger.EnsureInstance().GetCount(bottle.bottleKind)).ToString(), bottle.itemRarity, bottle == selectedBottle, true, () => SelectBottle(bottle)); spawnedBottleBindings[item] = bottle; } return true; } private List GetDisplayableBottles() { List result = new List(); List source = GetCurrentLinkedBottles() ?? new List(); for (int i = 0; i < source.Count; i++) { expBottlesSO bottle = source[i]; if (bottle == null || ExpBottleLedger.EnsureInstance().GetCount(bottle.bottleKind) <= 0) { continue; } result.Add(bottle); } return result; } private void BuildBreakthroughMaterialList() { List requirements = BuildEffectiveBreakthroughRequirements(); bool requiresFullSubmission = HasPendingDebtRequirements(); bool bundledSelection = ShouldUseBundledBreakthroughSelection(requirements, requiresFullSubmission); if (requirements.Count == 0) { selectedBreakthroughMaterialKind = null; return; } bool selectionValid = false; for (int i = 0; i < requirements.Count; i++) { if (selectedBreakthroughMaterialKind.HasValue && requirements[i].materialKind == selectedBreakthroughMaterialKind.Value) { selectionValid = true; break; } } if (!selectionValid) { selectedBreakthroughMaterialKind = GetDefaultBreakthroughSelection(requirements, bundledSelection); } for (int i = 0; i < requirements.Count; i++) { AggregatedMaterialRequirement requirement = requirements[i]; int owned = requirement.isBottleRequirement ? ExpBottleLedger.EnsureInstance().GetCount(requirement.bottleKind) : DushMaterialLedger.EnsureInstance().GetCount(requirement.materialKind); bool enough = owned >= requirement.requiredAmount; string ownedText = enough ? owned.ToString() : string.Format("{0}", owned); string amountText = string.Format("{0}/{1}", ownedText, requirement.requiredAmount); materialPrefab item = CreateMaterialItem(); if (item == null) { continue; } item.Bind( requirement.isBottleRequirement ? (requirement.bottle != null ? requirement.bottle.expBottleSprite : null) : (requirement.material != null ? requirement.material.growthMaterialSprite : null), requirement.isBottleRequirement ? GetDisplayName(requirement.bottle, requirement.bottleKind) : GetDisplayName(requirement.material, requirement.materialKind), amountText, requirement.isBottleRequirement ? (requirement.bottle != null ? requirement.bottle.itemRarity : ItemRarity.None) : (requirement.material != null ? requirement.material.itemRarity : ItemRarity.None), ShouldHighlightBreakthroughRequirement(requirement, requiresFullSubmission, bundledSelection), !requiresFullSubmission, () => SelectBreakthroughMaterial(requirement.materialKind)); } } private void UpdateTrainingInfoAndSelectionState() { if (selectedBottle == null) { SetInfoText("请选择经验瓶"); SetConfirmInteractable(false); SetConfirmButtonText("选择"); return; } SetInfoText(GetDisplayName(selectedBottle)); SetConfirmInteractable(true); SetConfirmButtonText("使用"); } private void SelectBottle(expBottlesSO bottle) { selectedBottle = bottle; for (int i = 0; i < spawnedMaterials.Count; i++) { materialPrefab item = spawnedMaterials[i]; if (item == null) { continue; } expBottlesSO boundBottle; bool isSelected = bottle != null && spawnedBottleBindings.TryGetValue(item, out boundBottle) && boundBottle == bottle; item.SetSelected(isSelected); } UpdateTrainingInfoAndSelectionState(); } private void SelectBreakthroughMaterial(DushMaterialKind materialKind) { selectedBreakthroughMaterialKind = materialKind; RefreshView(); } private bool TryUseSelectedBottle(expBottlesSO bottle, out string failureReason, out int consumedCount, out int grantedExp, out string successMessage) { consumedCount = 0; grantedExp = 0; failureReason = null; successMessage = null; lastOperationSuccessMessage = null; if (bottle == null) { failureReason = "未选择经验瓶"; return false; } if (bottle.bottleType == expBottlesSO.BottleType.RainAll) { bool success = TryUseRainAllBottle(bottle, out failureReason, out consumedCount, out grantedExp); successMessage = lastOperationSuccessMessage; return success; } upgradeConfig.SpecialBottleRule specialRule = GetSpecialBottleRule(bottle); if (specialRule != null) { bool success = TryUseSpecialBottleRule(bottle, specialRule, out failureReason, out consumedCount, out grantedExp); successMessage = lastOperationSuccessMessage; return success; } bool normalSuccess = TryUseNormalBottle(bottle, out failureReason, out consumedCount, out grantedExp); successMessage = lastOperationSuccessMessage; return normalSuccess; } private bool TryUseNormalBottle(expBottlesSO bottle, out string failureReason, out int consumedCount, out int grantedExp) { consumedCount = 0; grantedExp = 0; failureReason = null; if (bottle == null || currentHero == null) { failureReason = "经验瓶配置缺失"; return false; } HeroGrowthTier currentTier = GetCurrentTier(); if (currentTier >= HeroGrowthTier.S) { failureReason = "当前偶像已达最高阶"; return false; } if (!CanBottleServeTier(bottle.serviceLevel, currentTier)) { failureReason = "该经验瓶不可用于当前偶像等级"; return false; } if (!ExpBottleLedger.EnsureInstance().HasEnough(bottle.bottleKind, 1)) { failureReason = "经验瓶库存不足"; return false; } int expToGrant = ResolveBottleGrantExp(bottle, currentHero); if (expToGrant <= 0) { failureReason = "当前偶像无法从该经验瓶获得经验"; return false; } grantedExp = ApplyDirectExpToHero(currentHero, expToGrant); if (grantedExp <= 0) { failureReason = "当前偶像无法从该经验瓶获得经验"; return false; } if (!ExpBottleLedger.EnsureInstance().TryConsume(bottle.bottleKind, 1)) { failureReason = "经验瓶库存不足"; return false; } consumedCount = 1; lastOperationSuccessMessage = string.Format("给{0}增加了{1}经验", currentHero.ally_heroName, grantedExp); return true; } private bool TryUseRainAllBottle(expBottlesSO bottle, out string failureReason, out int consumedCount, out int grantedExp) { consumedCount = 0; grantedExp = 0; failureReason = null; if (bottle == null || currentHero == null) { failureReason = "经验瓶配置缺失"; return false; } if (!ExpBottleLedger.EnsureInstance().HasEnough(bottle.bottleKind, 1)) { failureReason = "经验瓶库存不足"; return false; } int targetCount = Mathf.Max(1, bottle.rainAllTargetCount); int expPerHero = Mathf.Max(0, bottle.rainAllGrantedExpPerHero); if (expPerHero <= 0) { failureReason = "雨露均沾经验瓶配置不完整"; return false; } List targets = BuildRainAllTargets(targetCount); if (targets.Count == 0) { failureReason = "没有可被该经验瓶作用的偶像"; return false; } int totalGrantedExp = 0; List recipientSummaries = new List(); List secondaryHeroNames = new List(); List secondaryGrantedValues = new List(); for (int i = 0; i < targets.Count; i++) { int grantedToHero = ApplyDirectExpToHero(targets[i], expPerHero); totalGrantedExp += grantedToHero; if (grantedToHero > 0) { recipientSummaries.Add(string.Format("{0}+{1}", targets[i].ally_heroName, grantedToHero)); if (i > 0) { secondaryHeroNames.Add(targets[i].ally_heroName); secondaryGrantedValues.Add(grantedToHero); } } } if (totalGrantedExp <= 0) { failureReason = "没有可被该经验瓶作用的偶像"; return false; } if (!ExpBottleLedger.EnsureInstance().TryConsume(bottle.bottleKind, 1)) { failureReason = "经验瓶库存不足"; return false; } consumedCount = 1; grantedExp = totalGrantedExp; lastOperationSuccessMessage = BuildRainAllSuccessMessage(recipientSummaries, secondaryHeroNames, secondaryGrantedValues); return true; } private List BuildRainAllTargets(int targetCount) { List result = new List(); if (currentHero == null || !CanReceiveTrainingExp(currentHero)) { return result; } result.Add(currentHero); if (targetCount <= 1) { return result; } AllyHero_SO[] heroes = RuntimeResourcesCache.LoadAllAllyHeroes(); List candidates = new List(); for (int i = 0; i < heroes.Length; i++) { AllyHero_SO hero = heroes[i]; if (hero == null || hero.ally_heroID <= 0 || hero.ally_heroID == currentHero.ally_heroID || !hero.isUnlocked) { continue; } if (!CanReceiveTrainingExp(hero)) { continue; } candidates.Add(hero); } candidates.Sort((left, right) => GetRemainingProgressExp(left).CompareTo(GetRemainingProgressExp(right))); for (int i = 0; i < candidates.Count && result.Count < targetCount; i++) { result.Add(candidates[i]); } return result; } private bool CanReceiveTrainingExp(AllyHero_SO hero) { if (hero == null || hero.ally_heroID <= 0) { return false; } HeroGrowthTier tier = AllyHeroGrowthService.GetUnlockedTier(hero); if (tier >= HeroGrowthTier.S) { return false; } List levels = GetSortedLevels(hero); if (levels.Count < 2) { return false; } int currentExp = AllyHeroDeployLedger.EnsureInstance().GetCurrentExp(hero.ally_heroID); int currentCap = GetTierCap(levels, (int)tier); if (currentExp < currentCap) { return true; } return AllyHeroDeployLedger.EnsureInstance().IsAutoBreakthroughEnabled(hero.ally_heroID); } private int GetRemainingProgressExp(AllyHero_SO hero) { if (hero == null) { return int.MaxValue; } HeroGrowthTier tier = AllyHeroGrowthService.GetUnlockedTier(hero); List levels = GetSortedLevels(hero); if (levels.Count < 2 || tier >= HeroGrowthTier.S) { return int.MaxValue; } int currentExp = AllyHeroDeployLedger.EnsureInstance().GetCurrentExp(hero.ally_heroID); int currentCap = GetTierCap(levels, (int)tier); if (currentExp >= currentCap && AllyHeroDeployLedger.EnsureInstance().IsAutoBreakthroughEnabled(hero.ally_heroID)) { return 0; } return Mathf.Max(0, currentCap - currentExp); } private int ResolveBottleGrantExp(expBottlesSO bottle, AllyHero_SO hero) { if (bottle == null || hero == null) { return 0; } List levels = GetSortedLevels(hero); if (levels.Count == 0) { return 0; } int tierIndex = Mathf.Clamp(AllyHeroDeployLedger.EnsureInstance().GetUnlockedTierIndex(hero.ally_heroID), 0, levels.Count - 1); int currentExp = AllyHeroDeployLedger.EnsureInstance().GetCurrentExp(hero.ally_heroID); int currentCap = GetTierCap(levels, tierIndex); switch (bottle.expApplyMode) { case expBottlesSO.ExpApplyMode.FillCurrentLevel: return Mathf.Max(0, currentCap - currentExp); case expBottlesSO.ExpApplyMode.FillAllLevels: return Mathf.Max(0, levels[levels.Count - 1].requiredEXP - currentExp); default: return Mathf.Max(0, bottle.grantedExp); } } private int ApplyDirectExpToHero(AllyHero_SO hero, int expAmount) { if (hero == null || hero.ally_heroID <= 0 || expAmount <= 0) { return 0; } List levels = GetSortedLevels(hero); if (levels.Count == 0) { return 0; } int currentExp = AllyHeroDeployLedger.EnsureInstance().GetCurrentExp(hero.ally_heroID); int tierIndex = Mathf.Clamp(AllyHeroDeployLedger.EnsureInstance().GetUnlockedTierIndex(hero.ally_heroID), 0, levels.Count - 1); bool autoBreakthrough = AllyHeroDeployLedger.EnsureInstance().IsAutoBreakthroughEnabled(hero.ally_heroID); int remaining = expAmount; int granted = 0; while (remaining > 0) { if (tierIndex >= (int)HeroGrowthTier.S) { break; } int currentCap = GetTierCap(levels, tierIndex); if (!autoBreakthrough) { if (currentExp >= currentCap) { break; } int add = Mathf.Min(remaining, Mathf.Max(0, currentCap - currentExp)); currentExp += add; granted += add; break; } if (currentExp >= currentCap) { tierIndex = Mathf.Clamp(tierIndex + 1, 0, (int)HeroGrowthTier.S); continue; } int delta = Mathf.Min(remaining, Mathf.Max(0, currentCap - currentExp)); currentExp += delta; granted += delta; remaining -= delta; if (currentExp >= currentCap) { tierIndex = Mathf.Clamp(tierIndex + 1, 0, (int)HeroGrowthTier.S); } } AllyHeroDeployLedger.EnsureInstance().SetUnlockedTierIndex(hero, tierIndex); AllyHeroDeployLedger.EnsureInstance().SetCurrentExp(hero, currentExp); return granted; } private bool TryUseSpecialBottleRule(expBottlesSO bottle, upgradeConfig.SpecialBottleRule rule, out string failureReason, out int consumedCount, out int grantedExp) { consumedCount = 0; grantedExp = 0; failureReason = null; if (bottle == null || rule == null) { failureReason = "经验瓶配置缺失"; return false; } if (currentHero == null) { failureReason = "未选择偶像"; return false; } HeroGrowthTier currentTier = GetCurrentTier(); if (currentTier >= HeroGrowthTier.S) { failureReason = "当前偶像已达最高阶"; return false; } if (!rule.allowAnyCurrentTier && (currentTier < rule.minimumCurrentTier || currentTier > rule.maximumCurrentTier)) { failureReason = "当前偶像无法使用该经验瓶"; return false; } if (!ExpBottleLedger.EnsureInstance().HasEnough(bottle.bottleKind, 1)) { failureReason = "经验瓶库存不足"; return false; } List sortedLevels = GetSortedLevels(currentHero); if (sortedLevels.Count < 2) { failureReason = "偶像等级数据不完整"; return false; } int previousExp = AllyHeroDeployLedger.EnsureInstance().GetCurrentExp(currentHero.ally_heroID); int currentTierIndex = Mathf.Clamp((int)currentTier, 0, sortedLevels.Count - 1); switch (rule.effect) { case upgradeConfig.SpecialBottleEffect.FillToNextTierCap: { int currentCap = GetTierCap(sortedLevels, currentTierIndex); if (previousExp >= currentCap && !AllyHeroDeployLedger.EnsureInstance().IsAutoBreakthroughEnabled(currentHero.ally_heroID)) { failureReason = "当前等级已满,请先突破"; return false; } int needed = Mathf.Max(0, currentCap - previousExp); grantedExp = needed > 0 ? ApplyDirectExpToHero(currentHero, needed) : 0; if (grantedExp <= 0) { failureReason = "当前偶像无法从该经验瓶获得经验"; return false; } if (!ExpBottleLedger.EnsureInstance().TryConsume(bottle.bottleKind, 1)) { failureReason = "经验瓶库存不足"; return false; } consumedCount = 1; lastOperationSuccessMessage = string.Format("给{0}增加了{1}经验", currentHero.ally_heroName, grantedExp); return true; } case upgradeConfig.SpecialBottleEffect.ReachTargetTierMaxWithoutSkippedBreakthroughPayment: { HeroGrowthTier targetTier = rule.targetTier; int targetTierIndex = Mathf.Clamp((int)targetTier, 0, (int)HeroGrowthTier.A); if (currentTierIndex > targetTierIndex) { failureReason = "当前偶像已超过该经验瓶作用范围"; return false; } int targetCap = GetTierCap(sortedLevels, targetTierIndex); if (currentTierIndex == targetTierIndex && previousExp >= targetCap) { failureReason = "当前偶像已达到该培养上限"; return false; } PendingDebtState debtState = LoadPendingDebt(currentHero.ally_heroID); for (int tier = currentTierIndex; tier < targetTierIndex; tier++) { upgradeConfig.TierRule tierRule = config.GetRule((HeroGrowthTier)tier); if (tierRule == null) { continue; } debtState.pendingCoins += Mathf.Max(0, tierRule.requiredCoins); if (tierRule.breakthroughCosts != null) { for (int i = 0; i < tierRule.breakthroughCosts.Count; i++) { upgradeConfig.GrowthMaterialCost cost = tierRule.breakthroughCosts[i]; if (cost == null || cost.material == null || cost.amount <= 0) { continue; } AddDebtMaterial(debtState, cost.material.materialKind, cost.amount); } } } if (!ExpBottleLedger.EnsureInstance().TryConsume(bottle.bottleKind, 1)) { failureReason = "经验瓶库存不足"; return false; } SavePendingDebt(currentHero.ally_heroID, debtState); AllyHeroDeployLedger.EnsureInstance().SetUnlockedTierIndex(currentHero, targetTierIndex); AllyHeroDeployLedger.EnsureInstance().SetCurrentExp(currentHero, targetCap); consumedCount = 1; grantedExp = Mathf.Max(0, targetCap - previousExp); lastOperationSuccessMessage = string.Format("已将{0}提升至{1}阶满经验", currentHero.ally_heroName, targetTier.ToString()); return true; } default: failureReason = "未支持的特殊经验瓶效果"; return false; } } private bool TryBreakthroughCurrentHero(out string failureReason) { failureReason = null; if (currentHero == null) { failureReason = "未选择偶像"; return false; } if (!ShouldShowBreakthrough()) { failureReason = "当前等级经验未满,无法突破"; return false; } if (selectedBreakthroughMaterialKind == DushMaterialKind.Material78024) { return TryUseUniversalBreakthroughMaterial(out failureReason); } upgradeConfig.TierRule rule = GetCurrentRule(); if (rule == null || !rule.canBreakthrough) { failureReason = "当前偶像无法突破"; return false; } int requiredCoins = GetEffectiveBreakthroughCoinCost(); if (!PlayerEconomyLedger.EnsureInstance().HasEnoughCoins(requiredCoins)) { failureReason = "算力不足"; return false; } List requirements = BuildEffectiveBreakthroughRequirements(false); for (int i = 0; i < requirements.Count; i++) { AggregatedMaterialRequirement requirement = requirements[i]; bool enough = requirement.isBottleRequirement ? ExpBottleLedger.EnsureInstance().GetCount(requirement.bottleKind) >= requirement.requiredAmount : DushMaterialLedger.EnsureInstance().GetCount(requirement.materialKind) >= requirement.requiredAmount; if (!enough) { failureReason = "突破材料不足"; return false; } } if (!PlayerEconomyLedger.EnsureInstance().TrySpendCoins(requiredCoins)) { failureReason = "算力不足"; return false; } for (int i = 0; i < requirements.Count; i++) { AggregatedMaterialRequirement requirement = requirements[i]; if (requirement.isBottleRequirement) { ExpBottleLedger.EnsureInstance().TryConsume(requirement.bottleKind, requirement.requiredAmount); } else { DushMaterialLedger.EnsureInstance().TryConsume(requirement.materialKind, requirement.requiredAmount); } } int nextTierIndex = Mathf.Clamp((int)GetCurrentTier() + 1, 0, (int)HeroGrowthTier.S); AllyHeroDeployLedger.EnsureInstance().SetUnlockedTierIndex(currentHero, nextTierIndex); AllyHeroDeployLedger.EnsureInstance().SetCurrentExp(currentHero, AllyHeroDeployLedger.EnsureInstance().GetCurrentExp(currentHero.ally_heroID)); ClearPendingDebt(currentHero.ally_heroID); return true; } private bool TryUseUniversalBreakthroughMaterial(out string failureReason) { failureReason = null; if (currentHero == null) { failureReason = "未选择偶像"; return false; } growthMaterialSO universal = FindGrowthMaterialDefinition(DushMaterialKind.Material78024); if (universal == null) { failureReason = "未找到通用突破材料定义"; return false; } if (DushMaterialLedger.EnsureInstance().GetCount(DushMaterialKind.Material78024) <= 0 || !DushMaterialLedger.EnsureInstance().TryConsume(DushMaterialKind.Material78024, 1)) { failureReason = "通用突破材料不足"; return false; } int nextTierIndex = Mathf.Clamp((int)GetCurrentTier() + 1, 0, (int)HeroGrowthTier.S); AllyHeroDeployLedger.EnsureInstance().SetAutoBreakthroughEnabled(currentHero, true); AllyHeroDeployLedger.EnsureInstance().SetUnlockedTierIndex(currentHero, nextTierIndex); AllyHeroDeployLedger.EnsureInstance().SetCurrentExp(currentHero, AllyHeroDeployLedger.EnsureInstance().GetCurrentExp(currentHero.ally_heroID)); ClearPendingDebt(currentHero.ally_heroID); return true; } private bool ShouldShowBreakthrough() { if (currentHero == null || GetCurrentTier() >= HeroGrowthTier.S) { return false; } if (AllyHeroDeployLedger.EnsureInstance().IsAutoBreakthroughEnabled(currentHero.ally_heroID)) { return false; } List sortedLevels = GetSortedLevels(currentHero); if (sortedLevels.Count < 2) { return false; } int currentExp = AllyHeroDeployLedger.EnsureInstance().GetCurrentExp(currentHero.ally_heroID); int currentCap = GetTierCap(sortedLevels, (int)GetCurrentTier()); return currentExp >= currentCap; } private int GetEffectiveBreakthroughCoinCost() { int coinCost = GetCurrentBreakthroughCoinCost(); if (currentHero == null) { return coinCost; } PendingDebtState debtState = LoadPendingDebt(currentHero.ally_heroID); return Mathf.Max(0, coinCost + debtState.pendingCoins); } private DushMaterialKind? GetDefaultBreakthroughSelection(List requirements, bool bundledSelection) { if (requirements == null || requirements.Count == 0) { return null; } if (!bundledSelection) { return requirements[0].materialKind; } for (int i = 0; i < requirements.Count; i++) { if (!requirements[i].isUniversalOption) { return requirements[i].materialKind; } } return requirements[0].materialKind; } private bool ShouldUseBundledBreakthroughSelection(List requirements, bool requiresFullSubmission) { if (requiresFullSubmission || currentHero == null || GetCurrentTier() != HeroGrowthTier.A || requirements == null || requirements.Count == 0) { return false; } bool hasBottleRequirement = false; bool hasNormalMaterialRequirement = false; bool hasUniversalOption = false; for (int i = 0; i < requirements.Count; i++) { AggregatedMaterialRequirement requirement = requirements[i]; if (requirement == null) { continue; } if (requirement.isUniversalOption) { hasUniversalOption = true; continue; } if (requirement.isBottleRequirement) { hasBottleRequirement = true; } else { hasNormalMaterialRequirement = true; } } return hasBottleRequirement && hasNormalMaterialRequirement && hasUniversalOption; } private bool ShouldHighlightBreakthroughRequirement(AggregatedMaterialRequirement requirement, bool requiresFullSubmission, bool bundledSelection) { if (requirement == null) { return false; } if (requiresFullSubmission) { return true; } if (!selectedBreakthroughMaterialKind.HasValue) { return false; } if (!bundledSelection) { return requirement.materialKind == selectedBreakthroughMaterialKind.Value; } if (selectedBreakthroughMaterialKind.Value == DushMaterialKind.Material78024) { return requirement.materialKind == DushMaterialKind.Material78024; } return !requirement.isUniversalOption; } private List BuildEffectiveBreakthroughRequirements() { return BuildEffectiveBreakthroughRequirements(!HasPendingDebtRequirements()); } private List BuildEffectiveBreakthroughRequirements(bool includeUniversalOption) { Dictionary byKind = new Dictionary(); List orderedKinds = new List(); List directCosts = GetCurrentBreakthroughCosts(); if (directCosts != null) { for (int i = 0; i < directCosts.Count; i++) { upgradeConfig.GrowthMaterialCost cost = directCosts[i]; if (cost == null || cost.material == null || cost.amount <= 0) { continue; } AppendRequirement(byKind, orderedKinds, cost.material.materialKind, cost.material, cost.amount, false); } } upgradeConfig.TierRule currentRule = GetCurrentRule(); if (currentRule != null && currentRule.breakthroughBottleCosts != null) { for (int i = 0; i < currentRule.breakthroughBottleCosts.Count; i++) { upgradeConfig.BreakthroughBottleCost bottleCost = currentRule.breakthroughBottleCosts[i]; if (bottleCost == null || bottleCost.bottle == null || bottleCost.amount <= 0) { continue; } AppendBottleRequirement(byKind, orderedKinds, bottleCost.bottle.bottleKind, bottleCost.bottle, bottleCost.amount); } } if (currentHero != null) { PendingDebtState debtState = LoadPendingDebt(currentHero.ally_heroID); if (debtState.materials != null) { for (int i = 0; i < debtState.materials.Count; i++) { PendingDebtMaterialEntry debt = debtState.materials[i]; if (debt == null || debt.amount <= 0) { continue; } AppendRequirement(byKind, orderedKinds, (DushMaterialKind)debt.materialKind, FindGrowthMaterialDefinition((DushMaterialKind)debt.materialKind), debt.amount, false); } } } if (includeUniversalOption) { growthMaterialSO universal = FindGrowthMaterialDefinition(DushMaterialKind.Material78024); if (universal != null) { AppendRequirement(byKind, orderedKinds, DushMaterialKind.Material78024, universal, 1, true); } } List orderedRequirements = new List(orderedKinds.Count); for (int i = 0; i < orderedKinds.Count; i++) { AggregatedMaterialRequirement requirement; if (byKind.TryGetValue(orderedKinds[i], out requirement) && requirement != null) { orderedRequirements.Add(requirement); } } return orderedRequirements; } private materialPrefab CreateMaterialItem() { if (materialPrefab == null || materialContainer == null) { return null; } GameObject instance = Instantiate(materialPrefab, materialContainer); UI_OrderedEntryAnimator.PlaySingle(instance, materialContainer.childCount - 1, 0.16f, 0.016f, -10f, 0.97f, true); materialPrefab item = instance.GetComponent(); if (item != null) { spawnedMaterials.Add(item); } return item; } private void ClearMaterialItems() { for (int i = materialContainer != null ? materialContainer.childCount - 1 : -1; i >= 0; i--) { Destroy(materialContainer.GetChild(i).gameObject); } spawnedMaterials.Clear(); spawnedBottleBindings.Clear(); } private string GetDisplayName(expBottlesSO bottle) { return GetDisplayName(bottle, bottle != null ? bottle.bottleKind : default); } private string GetDisplayName(expBottlesSO bottle, ExpBottleKind fallbackKind) { if (bottle != null) { storeItemSO storeItem = FindStoreItemForBottle(bottle); if (storeItem != null && !string.IsNullOrWhiteSpace(storeItem.itemName)) { return storeItem.itemName.Trim(); } if (!string.IsNullOrWhiteSpace(bottle.expBottleName)) { return bottle.expBottleName.Trim(); } } return fallbackKind.ToString().Trim(); } private string GetDisplayName(growthMaterialSO material, DushMaterialKind fallbackKind) { if (material != null) { storeItemSO storeItem = FindStoreItemForGrowthMaterial(material); if (storeItem != null && !string.IsNullOrWhiteSpace(storeItem.itemName)) { return storeItem.itemName.Trim(); } if (!string.IsNullOrWhiteSpace(material.growthMaterialName)) { return material.growthMaterialName.Trim(); } } return fallbackKind.ToString().Trim(); } private storeItemSO FindStoreItemForBottle(expBottlesSO bottle) { if (bottle == null) { return null; } storeItemSO[] storeItems = GetStoreItems(); for (int i = 0; i < storeItems.Length; i++) { storeItemSO storeItem = storeItems[i]; if (storeItem != null && storeItem.associatedExpBottle == bottle) { return storeItem; } } return null; } private storeItemSO FindStoreItemForGrowthMaterial(growthMaterialSO material) { if (material == null) { return null; } storeItemSO[] storeItems = GetStoreItems(); for (int i = 0; i < storeItems.Length; i++) { storeItemSO storeItem = storeItems[i]; if (storeItem != null && storeItem.associatedGrowthMaterial == material) { return storeItem; } } return null; } private storeItemSO[] GetStoreItems() { if (cachedStoreItems == null) { cachedStoreItems = RuntimeResourcesCache.LoadAllStoreItems() ?? Array.Empty(); } return cachedStoreItems; } private void SetInfoText(string content) { if (infoButton != null) { infoButton.text = content ?? string.Empty; } } private void SetConfirmButtonText(string content) { Text target = yesdoButtonText; if (target == null && yesdoButton != null) { target = yesdoButton.GetComponentInChildren(true); } if (target != null) { target.text = content ?? string.Empty; } } private void SetConfirmInteractable(bool interactable) { if (yesdoButton != null) { yesdoButton.interactable = interactable; } } private void SetUpgradeTextVisible(bool visible, string content, Color color) { if (upgradeText == null) { return; } upgradeText.gameObject.SetActive(visible); if (!visible) { return; } upgradeText.text = content ?? string.Empty; upgradeText.color = color; } private static List GetSortedLevels(AllyHero_SO hero) { List sorted = new List(); if (hero == null || hero.levelStats == null) { return sorted; } for (int i = 0; i < hero.levelStats.Count; i++) { if (hero.levelStats[i] != null) { sorted.Add(hero.levelStats[i]); } } sorted.Sort((left, right) => left.requiredEXP.CompareTo(right.requiredEXP)); return sorted; } private static int GetTierCap(List sortedLevels, int tierIndex) { if (sortedLevels == null || sortedLevels.Count == 0) { return 0; } int clampedIndex = Mathf.Clamp(tierIndex, 0, sortedLevels.Count - 1); if (clampedIndex >= sortedLevels.Count - 1) { return Mathf.Max(0, sortedLevels[clampedIndex].requiredEXP); } return Mathf.Max(sortedLevels[clampedIndex].requiredEXP, sortedLevels[clampedIndex + 1].requiredEXP); } private static bool CanBottleServeTier(expBottlesSO.ServiceLevel serviceLevel, HeroGrowthTier tier) { return serviceLevel == expBottlesSO.ServiceLevel.Any || (int)serviceLevel - 1 == (int)tier; } private static void AppendRequirement(Dictionary byKind, List orderedKinds, DushMaterialKind kind, growthMaterialSO material, int amount, bool isUniversalOption) { if (amount <= 0) { return; } AggregatedMaterialRequirement existing; if (!byKind.TryGetValue(kind, out existing)) { existing = new AggregatedMaterialRequirement { material = material, materialKind = kind, requiredAmount = 0, isUniversalOption = isUniversalOption }; byKind[kind] = existing; if (orderedKinds != null) { orderedKinds.Add(kind); } } else if (existing.material == null && material != null) { existing.material = material; } existing.requiredAmount = Mathf.Max(existing.requiredAmount, amount); existing.isUniversalOption |= isUniversalOption; } private static void AppendBottleRequirement(Dictionary byKind, List orderedKinds, ExpBottleKind kind, expBottlesSO bottle, int amount) { if (amount <= 0) { return; } DushMaterialKind surrogateKind = (DushMaterialKind)(100000 + (int)kind); AggregatedMaterialRequirement existing; if (!byKind.TryGetValue(surrogateKind, out existing)) { existing = new AggregatedMaterialRequirement { bottle = bottle, bottleKind = kind, requiredAmount = 0, isUniversalOption = false, isBottleRequirement = true }; byKind[surrogateKind] = existing; if (orderedKinds != null) { orderedKinds.Add(surrogateKind); } } else if (existing.bottle == null && bottle != null) { existing.bottle = bottle; } existing.requiredAmount = Mathf.Max(existing.requiredAmount, amount); } private bool HasPendingDebtRequirements() { if (currentHero == null) { return false; } PendingDebtState debtState = LoadPendingDebt(currentHero.ally_heroID); if (debtState == null) { return false; } if (debtState.pendingCoins > 0) { return true; } if (debtState.materials == null) { return false; } for (int i = 0; i < debtState.materials.Count; i++) { PendingDebtMaterialEntry entry = debtState.materials[i]; if (entry != null && entry.amount > 0) { return true; } } return false; } private static void AddDebtMaterial(PendingDebtState debtState, DushMaterialKind kind, int amount) { if (debtState == null || amount <= 0) { return; } if (debtState.materials == null) { debtState.materials = new List(); } for (int i = 0; i < debtState.materials.Count; i++) { PendingDebtMaterialEntry entry = debtState.materials[i]; if (entry != null && entry.materialKind == (int)kind) { entry.amount += amount; return; } } debtState.materials.Add(new PendingDebtMaterialEntry { materialKind = (int)kind, amount = amount }); } private string BuildRainAllSuccessMessage(List recipientSummaries, List secondaryHeroNames, List secondaryGrantedValues) { if (recipientSummaries == null || recipientSummaries.Count == 0) { return string.Format("给{0}增加了经验", currentHero != null ? currentHero.ally_heroName : "当前偶像"); } if (recipientSummaries.Count == 1) { return string.Format("雨露均沾生效:{0}", recipientSummaries[0]); } string primary = recipientSummaries[0]; if (secondaryHeroNames != null && secondaryHeroNames.Count > 0 && secondaryGrantedValues != null && secondaryGrantedValues.Count == secondaryHeroNames.Count) { bool sameGrant = true; int granted = secondaryGrantedValues[0]; for (int i = 1; i < secondaryGrantedValues.Count; i++) { if (secondaryGrantedValues[i] != granted) { sameGrant = false; break; } } if (sameGrant) { return string.Format("雨露均沾生效:{0};同时为{1}提供{2}经验", primary, string.Join(",", secondaryHeroNames.ToArray()), granted); } } List others = recipientSummaries.GetRange(1, recipientSummaries.Count - 1); return string.Format("雨露均沾生效:{0};同时为{1}提供经验", primary, string.Join(",", others.ToArray())); } private static string GetPendingDebtKey(int heroId) { return "idol_upgrade_pending_debt_" + heroId; } private static PendingDebtState LoadPendingDebt(int heroId) { if (heroId <= 0) { return new PendingDebtState(); } string raw = PlayerPrefs.GetString(GetPendingDebtKey(heroId), string.Empty); if (string.IsNullOrWhiteSpace(raw)) { return new PendingDebtState(); } try { PendingDebtState state = JsonUtility.FromJson(raw); return state ?? new PendingDebtState(); } catch { return new PendingDebtState(); } } private static void SavePendingDebt(int heroId, PendingDebtState state) { if (heroId <= 0) { return; } if (state == null || (state.pendingCoins <= 0 && (state.materials == null || state.materials.Count == 0))) { ClearPendingDebt(heroId); return; } PlayerPrefs.SetString(GetPendingDebtKey(heroId), JsonUtility.ToJson(state)); PlayerPrefs.Save(); } private static void ClearPendingDebt(int heroId) { if (heroId <= 0) { return; } PlayerPrefs.DeleteKey(GetPendingDebtKey(heroId)); PlayerPrefs.Save(); } private static growthMaterialSO FindGrowthMaterialDefinition(DushMaterialKind materialKind) { growthMaterialSO[] definitions = Resources.FindObjectsOfTypeAll(); for (int i = 0; i < definitions.Length; i++) { if (definitions[i] != null && definitions[i].materialKind == materialKind) { return definitions[i]; } } return null; } }