using System; using UnityEngine; using UnityEngine.UI; using System.Collections.Generic; using UnityEngine.Serialization; using UnityEngine.SceneManagement; using System.Collections; using UnityEngine.Audio; using DG.Tweening; using GameServer.Client; using Bansonic; public class settlementController : MonoBehaviour { public static event Action OnSettlementCompleted; [Header("ranking List")] public rankingList rl; [Header("Inspector")] [SerializeField] private BeatmapManager bmm; [SerializeField] private ScoreManager sm; public GameManager gm; private SongData thisSong_so; private int maxScore_sum = 2000000; [Header("1000000")] public int _1000000 = 1000000; [Header("song image")] [SerializeField] private Image thisSong_backPic; [Header("Inspector")] public Button exit_toSelectSongs; public Button replay_thisGame; public Button display_rankList; public Button share_toSocialMedia; [Header("Text And Progress")] public Text songName_Text; [Tooltip("Documentation text normalized.")] public Text thisLevel_currentPercentage_Text; public Text finalScore_Text; public Image finalLevel_img; public Text pmScoreSum_Text; public Text idolScoreSum_Text; public Text accuracy_Text; public Text personalRecord_Text; public Image thisLevel_progressBar_Image; [Header("Rank Config")] public RankConfig rankConfig; [Header("Accuracy Weights")] public float perfect_weight = 1f; public float great_weight = 0.6666667f; public float good_weight = 0.333333f; public float miss_weight = 0; [Header("Inspector")] public Text reward_playerEXP_Text; public Text reward_money_Text; public Text reward_idolEXP_bottle_Text; [Header("Inspector")] public loadSettlementTeamPrefab settlementTeamLoader; public Image mvp_hero_hd_image; [Header("Inspector")] [SerializeField] private long moneyToGive_thisLevel; [Header("mvp")] public GameObject mvp_object; public Text mvp_score; public Image mvp_heroIcon; public Text mvp_heroName; public Text legacy_text; // Documentation text normalized. [Header("Inspector")] public Text perfectHitCount_Text; public Text perfectHitPercent_Text; public Image perfect_barFill_Image; public Text greatHitCount_Text; public Text greatHitPercent_Text; public Image great_barFill_Image; public Text goodHitCount_Text; public Text goodHitPercent_Text; public Image good_barFill_Image; public Text missHitCount_Text; public Text missHitPercent_Text; public Image miss_barFill_Image; public Text gameNote_rate; [Header("Timing Statistics")] public Text earlyHitCount_Text; public Text lateHitCount_Text; public Text avgOffset_Text; [Header("Inspector")] [Tooltip("Settlement background music clip used during the settlement flow.")] public AudioSource settlementAudioSource; [Tooltip("Original gameplay music source (usually the chart song). ")] public AudioSource oriGameMusicSource; [Tooltip("Documentation text normalized.")] public AudioMixer gameMusicMixer; [Tooltip("Documentation text normalized.")] public string lowpassParamName = "inGameMusic_lowpass"; [Tooltip("Duration of lowpass transition in seconds.")] public float lowpassFadeDuration = 2f; [Tooltip("Settlement CanvasGroup used for fade-in.")] public CanvasGroup settlementCanvasGroup; [Header("Inspector")] [Tooltip("Settlement control manager object.")] public GameObject cdm; private Coroutine musicTransitionCoroutine; private Coroutine canvasFadeCoroutine; private Coroutine settlementIntroCoroutine; private bool settlementUiInitialized = false; [Header("Settlement Intro Animation")] [SerializeField] private bool enableDetailedSettlementIntro = true; [SerializeField] private int introFlashCount = 2; [SerializeField] private float introFlashDuration = 0.24f; [SerializeField] private float introMoveDuration = 0.42f; [SerializeField] private float introNumberDuration = 0.48f; [SerializeField] private float introStepGap = 0.06f; [SerializeField] private float introStaticElementDuration = 0.22f; [SerializeField] private float introBarFillDuration = 0.36f; [SerializeField] private float introMvpEnterYOffset = 120f; [SerializeField] private float introMvpEnterDuration = 0.42f; [SerializeField] private Ease introMvpEnterEase = Ease.OutCubic; [SerializeField] private bool introTweenUseUnscaledTime = true; [Header("Settle Rewards")] public GameObject rewardPrefab; public Transform rewardParent; private Coroutine settlementRewardLayoutRefreshRoutine; private int targetPmScore; private int targetIdolScore; private int targetTotalScore; private float targetAccuracyPercent; private float targetTotalPercent; private int targetPerfectCount; private int targetGreatCount; private int targetGoodCount; private int targetMissCount; private float targetPerfectPercent; private float targetGreatPercent; private float targetGoodPercent; private float targetMissPercent; private int targetEarlyCount; private int targetLateCount; private float targetAvgOffsetMs; private string targetGameNoteRateText = string.Empty; private string targetPersonalRecordText = string.Empty; private Tween mvpEntryTween; private bool skipIntroRequested; private Transform cachedIntroMvpTransform; private Vector3 cachedIntroMvpBaseLocalPos; private bool hasCachedIntroMvpBaseLocalPos; private bool settlementHistoryRecorded; private bool settlementHeroStatsRecorded; private bool roomScoreSubmitTriggered; private bool settlementRewardsGranted; private void Awake() { // Documentation text normalized. if (replay_thisGame != null) { replay_thisGame.onClick.AddListener(OnReplayButtonClicked); } if (exit_toSelectSongs != null) { exit_toSelectSongs.onClick.AddListener(OnExitButtonClicked); } if (display_rankList != null) { display_rankList.onClick.AddListener(OnDisplayRankListClicked); } // Documentation text normalized. if (cdm != null) { cdm.SetActive(false); if (JudgeManager.IsDebugEnabled) Debug.Log("[SettlementController] CDM object disabled at startup"); } else { Debug.LogWarning("[SettlementController] CDM object is not assigned"); } } private void Start() { // Documentation text normalized. if (gameMusicMixer != null && !string.IsNullOrEmpty(lowpassParamName)) { try { gameMusicMixer.SetFloat(lowpassParamName, 22000f); } catch { } } RegisterCurrentLineupDeployCount(); settlementHistoryRecorded = false; settlementHeroStatsRecorded = false; settlementRewardsGranted = false; } private void Update() { if (settlementIntroCoroutine == null) return; if (!enableDetailedSettlementIntro) return; if (Input.GetMouseButtonDown(0)) { SkipSettlementIntroAnimations(); } } private void OnDestroy() { if (replay_thisGame != null) { replay_thisGame.onClick.RemoveListener(OnReplayButtonClicked); } if (exit_toSelectSongs != null) { exit_toSelectSongs.onClick.RemoveListener(OnExitButtonClicked); } if (display_rankList != null) { display_rankList.onClick.RemoveListener(OnDisplayRankListClicked); } // Documentation text normalized. if (musicTransitionCoroutine != null) { StopCoroutine(musicTransitionCoroutine); musicTransitionCoroutine = null; } // Documentation text normalized. if (canvasFadeCoroutine != null) { StopCoroutine(canvasFadeCoroutine); canvasFadeCoroutine = null; } if (settlementIntroCoroutine != null) { StopCoroutine(settlementIntroCoroutine); settlementIntroCoroutine = null; } if (mvpEntryTween != null) { if (mvpEntryTween.active) mvpEntryTween.Kill(false); mvpEntryTween = null; } } // Start is called once before the first execution of Update after the MonoBehaviour is created public void getThisSong_info() { if (bmm != null) { thisSong_so = bmm.assignedSongData; } } private void OnDisplayRankListClicked() { if (rl == null) { Debug.LogWarning("[settlementController] rankingList reference is missing."); return; } SongData songData = thisSong_so != null ? thisSong_so : (bmm != null ? bmm.assignedSongData : null); string songId = songData != null ? songData.songID.ToString() : string.Empty; string songName = songData != null ? songData.songName : string.Empty; if (string.IsNullOrWhiteSpace(songId)) { Debug.LogWarning("[settlementController] Cannot open ranking list because song id is missing."); return; } rl.Open(songId, songName); } private void RegisterCurrentLineupDeployCount() { HashSet uniqueHeroIds = new HashSet(); AllyHero_SO[] allHeroes = null; if (teamUIController.Instance != null && teamUIController.Instance.allySlotIds != null) { for (int i = 0; i < teamUIController.Instance.allySlotIds.Count; i++) { int heroId = teamUIController.Instance.allySlotIds[i]; if (heroId > 0) { uniqueHeroIds.Add(heroId); } } } if (uniqueHeroIds.Count == 0) { for (int slotIndex = 1; slotIndex <= 5; slotIndex++) { int heroId = PlayerPrefs.GetInt("selected_heroSlot0" + slotIndex + "_heroID", 0); if (heroId > 0) { uniqueHeroIds.Add(heroId); } } } if (uniqueHeroIds.Count == 0) { return; } allHeroes = RuntimeResourcesCache.LoadAllAllyHeroes(); if (allHeroes == null || allHeroes.Length == 0) { return; } foreach (int heroId in uniqueHeroIds) { for (int i = 0; i < allHeroes.Length; i++) { AllyHero_SO hero = allHeroes[i]; if (hero == null || hero.ally_heroID != heroId) { continue; } hero.IncrementBattleDeployCount(); break; } } } private void RecordSettlementHeroStats() { if (settlementHeroStatsRecorded) { return; } settlementHeroStatsRecorded = true; List lineupHeroes = ResolveCurrentLineupHeroSOs(); for (int i = 0; i < lineupHeroes.Count; i++) { AllyHero_SO hero = lineupHeroes[i]; if (hero == null) { continue; } hero.IncrementFinishCount(); } AllyHero_SO mvpHero = ResolveMvpHeroFromTopScorer(); if (mvpHero != null) { mvpHero.IncrementMvpCount(); } } private List ResolveCurrentLineupHeroSOs() { List result = new List(); HashSet uniqueHeroIds = new HashSet(); if (teamUIController.Instance != null && teamUIController.Instance.allySlotIds != null) { for (int i = 0; i < teamUIController.Instance.allySlotIds.Count; i++) { int heroId = teamUIController.Instance.allySlotIds[i]; if (heroId > 0) { uniqueHeroIds.Add(heroId); } } } if (uniqueHeroIds.Count == 0) { for (int slotIndex = 1; slotIndex <= 5; slotIndex++) { int heroId = PlayerPrefs.GetInt("selected_heroSlot0" + slotIndex + "_heroID", 0); if (heroId > 0) { uniqueHeroIds.Add(heroId); } } } EnsureCachedAllyHeroSOs(); foreach (int heroId in uniqueHeroIds) { AllyHero_SO hero = FindAllyHeroSOById(heroId); if (hero != null) { result.Add(hero); } } return result; } private AllyHero_SO ResolveMvpHeroFromTopScorer() { int topIndex = -1; int[] scores = new int[5]; if (sm != null) { scores[0] = sm.red_idolScore_sum; scores[1] = sm.green_idolScore_sum; scores[2] = sm.yellow_idolScore_sum; scores[3] = sm.purple_idolScore_sum; scores[4] = sm.blue_idolScore_sum; } else { for (int i = 0; i < 5; i++) { GameObject go = SceneObjectLookupCache.Find($"ally_0{i + 1}"); if (go != null) { AllyCombatant ac = go.GetComponent(); scores[i] = ac != null ? ac.currentScore : 0; } else { scores[i] = 0; } } } int max = -1; for (int i = 0; i < scores.Length; i++) { if (scores[i] > max) { max = scores[i]; topIndex = i; } } if (topIndex < 0 || max <= 0) { return null; } AllyHero_SO heroSO = null; if (SkillBuilder.Instance != null) { try { heroSO = SkillBuilder.Instance.GetAllyHeroSOBySlot(topIndex); } catch { heroSO = null; } } if (heroSO == null && teamUIController.Instance != null && teamUIController.Instance.allySlotIds != null) { int allyId = -1; if (topIndex >= 0 && topIndex < teamUIController.Instance.allySlotIds.Count) { allyId = teamUIController.Instance.allySlotIds[topIndex]; } if (allyId > 0) { heroSO = FindAllyHeroSOById(allyId); } } return heroSO; } private static void EnsureCachedAllyHeroSOs() { if (_cachedAllyHeroSOs == null || _cachedAllyHeroSOs.Length == 0) { _cachedAllyHeroSOs = RuntimeResourcesCache.LoadAllAllyHeroes(); } } private static AllyHero_SO FindAllyHeroSOById(int allyId) { if (allyId <= 0) { return null; } EnsureCachedAllyHeroSOs(); if (_cachedAllyHeroSOs == null) { return null; } for (int i = 0; i < _cachedAllyHeroSOs.Length; i++) { AllyHero_SO hero = _cachedAllyHeroSOs[i]; if (hero != null && hero.ally_heroID == allyId) { return hero; } } return null; } public void startSettlement_uiUpdate() { if (settlementUiInitialized) { Debug.Log("[SettlementController] startSettlement_uiUpdate ignored: settlement already initialized."); return; } settlementUiInitialized = true; settlementHistoryRecorded = false; settlementHeroStatsRecorded = false; roomScoreSubmitTriggered = false; settlementRewardsGranted = false; OnSettlementCompleted?.Invoke(); equipSmelt.NotifySettlementCompleted(); // Documentation text normalized. InitializeSettlementCanvas(); // Documentation text normalized. if (cdm != null) { cdm.SetActive(true); Debug.Log("[SettlementController] CDM object enabled at settlement start"); } getThisSong_info(); // --- Statistics: Record total play time at settlement --- GameManager runtimeGameManager = gm != null ? gm : SceneObjectLookupCache.FindAny(); float sessionDurationForTasks = runtimeGameManager != null ? runtimeGameManager.GetPendingSessionDurationSeconds() : 0f; if (runtimeGameManager != null) runtimeGameManager.RecordTotalPlayTime(); // Documentation text normalized. PrepareSettlementMusic(); if(sm != null) { songName_Text.text = bmm.parsedTitle; if (bmm.assignedSongData != null) { thisSong_backPic.sprite = bmm.assignedSongData.GetResolvedFullscreenSongPicture(); } targetPmScore = sm.allSum_pmScore; targetIdolScore = sm.allSum_idolScore; targetTotalScore = targetPmScore + targetIdolScore; targetTotalPercent = (float)targetTotalScore / Mathf.Max(1, _1000000) * 100f; PlayerSkillService.NotifySettlementCompleted(targetIdolScore); GrantSettlementRewardsAndDisplay(); finalScore_Text.text = targetTotalScore.ToString(); pmScoreSum_Text.text = targetPmScore.ToString(); // 根据总分从 rankConfig 获取等级图标并更新 finalLevel_img if (rankConfig != null && finalLevel_img != null) { finalLevel_img.sprite = rankConfig.GetRankSprite(targetTotalScore); Debug.Log($"[settlementController] 根据总分 {targetTotalScore} 更新等级图标"); } idolScoreSum_Text.text = targetIdolScore.ToString(); thisLevel_currentPercentage_Text.text = targetTotalPercent.ToString("F2") + "%"; thisLevel_progressBar_Image.fillAmount = (float)targetTotalScore / Mathf.Max(1, _1000000); int noteCountRaw = sm.countPerfect + sm.countGreat + sm.countGood + sm.countMiss; int noteCountSum = Mathf.Max(1, noteCountRaw); targetPerfectCount = sm.countPerfect; targetGreatCount = sm.countGreat; targetGoodCount = sm.countGood; targetMissCount = sm.countMiss; targetPerfectPercent = (float)targetPerfectCount / noteCountSum * 100f; targetGreatPercent = (float)targetGreatCount / noteCountSum * 100f; targetGoodPercent = (float)targetGoodCount / noteCountSum * 100f; targetMissPercent = (float)targetMissCount / noteCountSum * 100f; targetAccuracyPercent = ((float)(targetPerfectCount * perfect_weight + targetGreatCount * great_weight + targetGoodCount * good_weight + targetMissCount * miss_weight) / noteCountSum * 100f); if (!GameConfig.autoPlayEnabled) { DailyTaskEventHub.ReportPlaySong(thisSong_so != null ? thisSong_so.songID : 0); DailyTaskEventHub.ReportTotalScore(targetTotalScore); DailyTaskEventHub.ReportSingleRunScore(targetTotalScore); if (InGamePerformanceManager.Instance != null && InGamePerformanceManager.Instance.HighestComboThisRun > 0) { DailyTaskEventHub.ReportSingleRunCombo(InGamePerformanceManager.Instance.HighestComboThisRun); } if (targetMissCount <= 0 && noteCountRaw > 0) { DailyTaskEventHub.ReportFullCombo(); } } TryRecordRecentPlayHistory(noteCountRaw); perfectHitCount_Text.text = targetPerfectCount.ToString(); greatHitCount_Text.text = targetGreatCount.ToString(); goodHitCount_Text.text = targetGoodCount.ToString(); missHitCount_Text.text = targetMissCount.ToString(); perfect_barFill_Image.fillAmount = (float)targetPerfectCount / noteCountSum; great_barFill_Image.fillAmount = (float)targetGreatCount / noteCountSum; good_barFill_Image.fillAmount = (float)targetGoodCount / noteCountSum; miss_barFill_Image.fillAmount = (float)targetMissCount / noteCountSum; gameNote_rate.text = "已完成 " + noteCountRaw.ToString() + "/" + bmm.parsedNoteAmount.ToString(); perfectHitPercent_Text.text = targetPerfectPercent.ToString("F1") + "%"; greatHitPercent_Text.text = targetGreatPercent.ToString("F1") + "%"; goodHitPercent_Text.text = targetGoodPercent.ToString("F1") + "%"; missHitPercent_Text.text = targetMissPercent.ToString("F1") + "%"; accuracy_Text.text = targetAccuracyPercent.ToString("F1") + "%"; // Timing Statistics targetEarlyCount = sm.countEarly; targetLateCount = sm.countLate; if (earlyHitCount_Text != null) earlyHitCount_Text.text = targetEarlyCount.ToString(); if (lateHitCount_Text != null) lateHitCount_Text.text = targetLateCount.ToString(); if (avgOffset_Text != null) { float avg = sm.offsetCount > 0 ? sm.totalOffsetMs / sm.offsetCount : 0f; targetAvgOffsetMs = avg; avgOffset_Text.text = targetAvgOffsetMs.ToString("F2") + " ms"; } targetGameNoteRateText = gameNote_rate != null ? gameNote_rate.text : string.Empty; TrySubmitArenaRoomScore(); } else Debug.LogError("score manager is null"); // --- Achievement System: Load and display achievements --- if (InGamePerformanceManager.Instance != null) { // Calculate total cure, damage and mana from teamUIController float totalCure = 0; float totalDamage = 0; float totalMana = 0; if (teamUIController.Instance != null) { if (teamUIController.Instance.totalHealProvided != null) { foreach (float heal in teamUIController.Instance.totalHealProvided) { totalCure += heal; } } if (teamUIController.Instance.totalDamageDealt != null) { foreach (float dmg in teamUIController.Instance.totalDamageDealt) { totalDamage += dmg; } } if (teamUIController.Instance.totalManaRestored != null) { foreach (float mana in teamUIController.Instance.totalManaRestored) { totalMana += mana; } } Debug.Log($"[SettlementController] Stats Aggregated - Cure: {totalCure}, Damage: {totalDamage}, Mana: {totalMana}"); } // Update achievement categories InGamePerformanceManager.Instance.UpdateTotalCure(totalCure); InGamePerformanceManager.Instance.UpdateTotalDamage(totalDamage); InGamePerformanceManager.Instance.UpdateTotalManaRestored(totalMana); if (sm != null) InGamePerformanceManager.Instance.UpdateTotalScore(sm.allSum_pmScore + sm.allSum_idolScore); // Load and display achievement prefabs InGamePerformanceManager.Instance.LoadArchievePrefab(); } // Persist run results into the SongData SO for this difficulty // (update personal / idol / total records and per-entry progress if improved) getThisSong_info(); // ensure thisSong_so set if (thisSong_so != null && bmm != null) { int oldPersonalRecord = thisSong_so.personalRecord; int diff = bmm.assignedDifficulty; if (diff >= 0 && !GameConfig.autoPlayEnabled) { int pm = sm != null ? sm.allSum_pmScore : 0; int idol = sm != null ? sm.allSum_idolScore : 0; thisSong_so.ApplySettlementResult(diff, pm, idol, _1000000); PlayerRksService.RefreshAndPersist(); } if (personalRecord_Text != null) { int currentRecord = thisSong_so.personalRecord; targetPersonalRecordText = currentRecord.ToString(); personalRecord_Text.text = targetPersonalRecordText; // If current total score broke the overall personal record (all difficulties) // and we are NOT in autoplay (since we didn't save the result in autoplay) if (!GameConfig.autoPlayEnabled && targetTotalScore > oldPersonalRecord) { personalRecord_Text.color = new Color(0f, 0.392f, 0f); // Dark Green #006400 } } } // Populate settlement team cards if a loader is assigned if (settlementTeamLoader == null) { // try to auto-locate loader in scene if not assigned in inspector settlementTeamLoader = SceneObjectLookupCache.FindAny(); } if (settlementTeamLoader != null) { // ensure loader has key references if (settlementTeamLoader.sm == null) settlementTeamLoader.sm = sm ?? ScoreManager.Instance; if (settlementTeamLoader.tuic == null) settlementTeamLoader.tuic = teamUIController.Instance; if (settlementTeamLoader.gm == null) settlementTeamLoader.gm = gm ?? SceneObjectLookupCache.FindAny(); try { settlementTeamLoader.PopulateSettlementCards(); } catch (System.Exception ex) { Debug.LogWarning("Failed to PopulateSettlementCards: " + ex); } } // Documentation text normalized. // Documentation text normalized. RecordSettlementHeroStats(); SetMvpHeroImageFromTopScorer(); StartMusicTransition(); // Play settlement entry animation flow. StartSettlementIntro(); } private void GrantSettlementRewardsAndDisplay() { if (settlementRewardsGranted) { return; } settlementRewardsGranted = true; if (GameConfig.autoPlayEnabled) { moneyToGive_thisLevel = 0; if (reward_money_Text != null) { reward_money_Text.text = "+0"; } if (reward_idolEXP_bottle_Text != null) { reward_idolEXP_bottle_Text.text = "+0"; } if (reward_playerEXP_Text != null) { reward_playerEXP_Text.text = "+0"; } RebuildSettlementRewardVisuals(0, 0, 0); return; } int coinReward = Mathf.CeilToInt((targetTotalScore / 10000f) * 0.75f); int materialReward = Mathf.CeilToInt((targetTotalScore / 10000f) * 0.20f); int playerExpReward = Mathf.FloorToInt(targetTotalScore / 100000f); moneyToGive_thisLevel = coinReward; Player_SO playerData = LoadDefaultPlayerSo(); if (playerData != null) { PlayerEconomyLedger.EnsureInstance().AttachPlayerData(playerData); PlayerExperienceLedger.EnsureInstance().AttachPlayerData(playerData); ExpBottleLedger.EnsureInstance().AttachPlayerData(playerData); DushMaterialLedger.EnsureInstance().AttachPlayerData(playerData); } PlayerEconomyLedger.EnsureInstance().AddCoins(coinReward); PlayerEconomyLedger.EnsureInstance().AddMaterial(materialReward); PlayerExperienceLedger.EnsureInstance().AddExperience(playerExpReward); PlayerEconomyLedger.EnsureInstance().SaveNow(); PlayerExperienceLedger.EnsureInstance().SaveNow(); if (reward_money_Text != null) { reward_money_Text.text = $"+{coinReward}"; } if (reward_idolEXP_bottle_Text != null) { reward_idolEXP_bottle_Text.text = $"+{materialReward}"; } if (reward_playerEXP_Text != null) { reward_playerEXP_Text.text = $"+{playerExpReward}"; } RebuildSettlementRewardVisuals(coinReward, materialReward, playerExpReward); } private void RebuildSettlementRewardVisuals(int coinReward, int materialReward, int playerExpReward) { if (rewardParent != null) { for (int i = rewardParent.childCount - 1; i >= 0; i--) { Destroy(rewardParent.GetChild(i).gameObject); } } SpawnSettlementReward(global::rewardPrefab.RewardVisualType.Coins, coinReward); SpawnSettlementReward(global::rewardPrefab.RewardVisualType.Material, materialReward); SpawnSettlementReward(global::rewardPrefab.RewardVisualType.PlayerExp, playerExpReward); RequestSettlementRewardLayoutRebuild(); } private void SpawnSettlementReward(global::rewardPrefab.RewardVisualType rewardType, int amount) { if (rewardPrefab == null || rewardParent == null) { return; } GameObject go = Instantiate(rewardPrefab, rewardParent); global::rewardPrefab prefab = go != null ? go.GetComponent() : null; if (prefab != null) { prefab.Bind(rewardType, amount); } UI_OrderedEntryAnimator.PlayFadeOnly(go, rewardParent != null ? rewardParent.childCount - 1 : 0, 0.2f, 0.035f, false); } private void RequestSettlementRewardLayoutRebuild() { if (rewardParent == null) { return; } if (settlementRewardLayoutRefreshRoutine != null) { StopCoroutine(settlementRewardLayoutRefreshRoutine); settlementRewardLayoutRefreshRoutine = null; } if (isActiveAndEnabled) { settlementRewardLayoutRefreshRoutine = StartCoroutine(RebuildSettlementRewardLayoutNextFrame()); return; } RebuildSettlementRewardLayoutNow(); } private IEnumerator RebuildSettlementRewardLayoutNextFrame() { yield return null; RebuildSettlementRewardLayoutNow(); settlementRewardLayoutRefreshRoutine = null; } private void RebuildSettlementRewardLayoutNow() { RectTransform rect = rewardParent as RectTransform; if (rect == null) { return; } Canvas.ForceUpdateCanvases(); LayoutRebuilder.ForceRebuildLayoutImmediate(rect); Canvas.ForceUpdateCanvases(); } private static Player_SO LoadDefaultPlayerSo() { return RuntimeResourcesCache.LoadDefaultPlayerSo(); } private void TryRecordRecentPlayHistory(int noteCountRaw) { if (GameConfig.autoPlayEnabled) { return; } if (settlementHistoryRecorded) { return; } settlementHistoryRecorded = true; var record = new RecentPlayRecord { playedAt = System.DateTime.Now.ToString("MM-dd, HH:mm"), playedAtUtc = System.DateTime.UtcNow.ToString("o"), songID = thisSong_so != null ? thisSong_so.songID : 0, songName = thisSong_so != null && !string.IsNullOrWhiteSpace(thisSong_so.songName) ? thisSong_so.songName : (bmm != null ? bmm.parsedTitle : "Unknown"), difficultyDisplay = BuildDifficultyDisplay(), accuracy = targetAccuracyPercent, srks = CalculateSongRankingScore(noteCountRaw), totalScore = targetTotalScore, chartScore = targetPmScore, idolScore = targetIdolScore, hasScoreBreakdown = true, rankIndex = CalculateRankIndex(targetTotalScore), rankTierCount = GetRankTierCount(), hasRankMarker = true, scoreReadable = true, wasEarlySettlement = IsEarlySettlement(noteCountRaw), wasAllPerfect = IsAllPerfectRun(noteCountRaw) }; RecentPlayHistoryStore.Push(record); } private string BuildDifficultyDisplay() { int difficulty = bmm != null ? bmm.assignedDifficulty : -1; string suffix = GetDifficultyShortName(difficulty); float level = 0f; if (thisSong_so != null && thisSong_so.chartFiles != null) { for (int i = 0; i < thisSong_so.chartFiles.Count; i++) { ChartFileEntry entry = thisSong_so.chartFiles[i]; if (entry == null || entry.difficulty != difficulty) { continue; } level = entry.difficultyLEVEL; break; } } string levelText = Mathf.Approximately(level, Mathf.Round(level)) ? Mathf.RoundToInt(level).ToString() : level.ToString("0.#"); if (string.IsNullOrEmpty(levelText) || levelText == "0") { return suffix; } return levelText + "_" + suffix; } private static string GetDifficultyShortName(int difficulty) { switch (difficulty) { case 0: return "EZ"; case 1: return "HD"; case 2: return "IN"; case 3: return "IM"; default: return "UN"; } } private int GetRankTierCount() { if (rankConfig == null || rankConfig.thresholds == null) { return 0; } return Mathf.Max(0, rankConfig.thresholds.Count); } private int CalculateRankIndex(int score) { if (rankConfig == null || rankConfig.thresholds == null || rankConfig.thresholds.Count == 0 || score <= 0) { return 0; } int bestLevel = 0; float highestMatchingPercent = -1f; int thresholdCount = rankConfig.thresholds.Count; for (int i = 0; i < thresholdCount; i++) { RankThreshold threshold = rankConfig.thresholds[i]; if (threshold == null) { continue; } float requiredScore = rankConfig.baseScore * threshold.thresholdPercent; if (score < requiredScore || threshold.thresholdPercent <= highestMatchingPercent) { continue; } highestMatchingPercent = threshold.thresholdPercent; bestLevel = thresholdCount - i; } return bestLevel; } private float CalculateSongRankingScore(int noteCountRaw) { int totalNotes = Mathf.Max(1, noteCountRaw); float accuracyScore = Mathf.Clamp01(targetAccuracyPercent / 100f); float scoreScore = Mathf.Clamp01((float)targetTotalScore / Mathf.Max(1, _1000000)); int highestCombo = InGamePerformanceManager.Instance != null ? InGamePerformanceManager.Instance.HighestComboThisRun : 0; float comboScore = Mathf.Clamp01((float)highestCombo / totalNotes); float judgementScore = Mathf.Clamp01( (targetPerfectCount + targetGreatCount * 0.7f + targetGoodCount * 0.35f) / totalNotes); float missPenalty = Mathf.Clamp01((float)targetMissCount / totalNotes); float srks = accuracyScore * 42f + scoreScore * 28f + comboScore * 18f + judgementScore * 12f; srks -= missPenalty * 18f; if (targetMissCount <= 0 && totalNotes > 0) { srks += 4f; } return Mathf.Clamp(srks, 0f, 100f); } private bool IsEarlySettlement(int noteCountRaw) { NoteSpawner spawner = SceneObjectLookupCache.FindAny(); if (spawner != null && spawner.IsImmediateSettlementTriggered) { return true; } int parsedNoteAmount = bmm != null ? bmm.parsedNoteAmount : 0; return parsedNoteAmount > 0 && noteCountRaw < parsedNoteAmount; } private bool IsAllPerfectRun(int noteCountRaw) { return noteCountRaw > 0 && targetPerfectCount >= noteCountRaw && targetGreatCount == 0 && targetGoodCount == 0 && targetMissCount == 0; } private void StartSettlementIntro() { skipIntroRequested = false; if (!enableDetailedSettlementIntro) { if (settlementTeamLoader != null) settlementTeamLoader.ShowSpawnedCardsInstantly(); StartCanvasFadeIn(); return; } if (settlementIntroCoroutine != null) { StopCoroutine(settlementIntroCoroutine); settlementIntroCoroutine = null; } settlementIntroCoroutine = StartCoroutine(SettlementIntroRoutine()); } private IEnumerator SettlementIntroRoutine() { if (settlementCanvasGroup == null) { StartCanvasFadeIn(); yield break; } settlementCanvasGroup.alpha = 0f; settlementCanvasGroup.interactable = false; settlementCanvasGroup.blocksRaycasts = false; Transform settleRoot = settlementCanvasGroup.transform; Vector3 settleBaseScale = settleRoot != null ? settleRoot.localScale : Vector3.one; Transform rightRoot = FindDescendantByName(settleRoot, "right"); Transform leftRoot = FindDescendantByName(settleRoot, "left"); Transform picsRoot = FindDescendantByName(leftRoot != null ? leftRoot : settleRoot, "pics"); RectTransform rightBottomRt = FindRectByName(rightRoot != null ? rightRoot : settleRoot, "right_bottom"); Transform quhuiImageTr = FindDescendantByName(picsRoot != null ? picsRoot : settleRoot, "quhuiImage"); RectTransform scoreBottomRt = FindRectByName(settleRoot, "score_bottom"); RectTransform shenstarsRt = FindRectByName(settleRoot, "shenstars"); RectTransform rewardRt = FindRectByName(settleRoot, "reward"); Transform heroDApicTr = FindDescendantByName(rightRoot != null ? rightRoot : settleRoot, "heroDApic"); Transform heroMaskTr = FindDescendantByName(heroDApicTr != null ? heroDApicTr : settleRoot, "MASK"); Transform lihuiTr = FindDescendantByName(heroMaskTr != null ? heroMaskTr : settleRoot, "lihui"); Transform teamDisplayingTr = FindDescendantByName(rightRoot != null ? rightRoot : settleRoot, "teamDisplaying"); RectTransform teamDisplayingRt = teamDisplayingTr as RectTransform; Transform mvpTr = (mvp_object != null) ? mvp_object.transform : FindDescendantByName(heroDApicTr != null ? heroDApicTr : settleRoot, "mvp"); bool hasAllyCardEntry = settlementTeamLoader != null && settlementTeamLoader.HasSpawnedCards(); bool shouldAnimateMvp = mvpTr != null && mvpTr.gameObject.activeInHierarchy; Vector3 mvpBaseLocalPos = shouldAnimateMvp ? mvpTr.localPosition : Vector3.zero; cachedIntroMvpTransform = mvpTr; hasCachedIntroMvpBaseLocalPos = shouldAnimateMvp; if (shouldAnimateMvp) cachedIntroMvpBaseLocalPos = mvpBaseLocalPos; Transform buttonsRoot = FindDescendantByName(rightRoot != null ? rightRoot : settleRoot, "buttons"); List rightButtons = CollectDirectChildren(buttonsRoot); Transform hitStatusTr = FindDescendantByName(settleRoot, "hitStatus"); Transform statusTr = FindDescendantByName(hitStatusTr != null ? hitStatusTr : settleRoot, "status"); List statusGroups = CollectDirectChildren(statusTr); Transform perfectGroup = FindGroupRoot(perfectHitCount_Text, perfectHitPercent_Text); Transform greatGroup = FindGroupRoot(greatHitCount_Text, greatHitPercent_Text); Transform goodGroup = FindGroupRoot(goodHitCount_Text, goodHitPercent_Text); Transform missGroup = FindGroupRoot(missHitCount_Text, missHitPercent_Text); List orderedGroups = new List(); if (perfectGroup != null) orderedGroups.Add(perfectGroup); if (greatGroup != null) orderedGroups.Add(greatGroup); if (goodGroup != null) orderedGroups.Add(goodGroup); if (missGroup != null) orderedGroups.Add(missGroup); if (orderedGroups.Count == 0) orderedGroups = statusGroups; if (perfectGroup == null && orderedGroups.Count > 0) perfectGroup = orderedGroups[0]; if (greatGroup == null && orderedGroups.Count > 1) greatGroup = orderedGroups[1]; if (goodGroup == null && orderedGroups.Count > 2) goodGroup = orderedGroups[2]; if (missGroup == null && orderedGroups.Count > 3) missGroup = orderedGroups[3]; // Initial hidden/setup state before entrance. if (rightBottomRt != null) SetAnchoredX(rightBottomRt, 1799f); if (quhuiImageTr != null) SetCanvasAlpha(quhuiImageTr, 0f); if (scoreBottomRt != null) { SetAnchoredX(scoreBottomRt, -1646.9f); SetCanvasAlpha(scoreBottomRt, 0f); } if (shenstarsRt != null) { SetAnchoredX(shenstarsRt, -1602.1f); SetCanvasAlpha(shenstarsRt, 0f); } if (rewardRt != null) { SetAnchoredY(rewardRt, -494.3f); SetCanvasAlpha(rewardRt, 0f); } if (lihuiTr != null) SetCanvasAlpha(lihuiTr, 0f); if (teamDisplayingRt != null) { if (hasAllyCardEntry) { SetAnchoredX(teamDisplayingRt, 1490.09f); SetCanvasAlpha(teamDisplayingRt, 1f); } else { SetAnchoredX(teamDisplayingRt, 2265f); SetCanvasAlpha(teamDisplayingRt, 0f); } } if (shouldAnimateMvp) { SetCanvasAlpha(mvpTr, 0f); mvpTr.localPosition = new Vector3(mvpBaseLocalPos.x, mvpBaseLocalPos.y + introMvpEnterYOffset, mvpBaseLocalPos.z); } for (int i = 0; i < rightButtons.Count; i++) { if (rightButtons[i] != null) SetCanvasAlpha(rightButtons[i], 0f); } for (int i = 0; i < orderedGroups.Count; i++) { if (orderedGroups[i] != null) SetCanvasAlpha(orderedGroups[i], 0f); } PrepareStaticSettlementElement(thisSong_backPic != null ? thisSong_backPic.transform : null); PrepareStaticSettlementElement(songName_Text != null ? songName_Text.transform : null); PrepareStaticSettlementElement(finalLevel_img != null ? finalLevel_img.transform : null); PrepareStaticSettlementElement(personalRecord_Text != null ? personalRecord_Text.transform : null); PrepareStaticSettlementElement(gameNote_rate != null ? gameNote_rate.transform : null); PrepareStaticSettlementElement(earlyHitCount_Text != null ? earlyHitCount_Text.transform : null); PrepareStaticSettlementElement(lateHitCount_Text != null ? lateHitCount_Text.transform : null); PrepareStaticSettlementElement(avgOffset_Text != null ? avgOffset_Text.transform : null); PrepareImageFill(thisLevel_progressBar_Image); PrepareImageFill(perfect_barFill_Image); PrepareImageFill(great_barFill_Image); PrepareImageFill(good_barFill_Image); PrepareImageFill(miss_barFill_Image); PrepareTextForCountUp(pmScoreSum_Text, false, 0); PrepareTextForCountUp(idolScoreSum_Text, false, 0); PrepareTextForCountUp(accuracy_Text, true, 0f, "F1"); PrepareTextForCountUp(finalScore_Text, false, 0); PrepareTextForCountUp(thisLevel_currentPercentage_Text, true, 0f, "F2"); PrepareTextForCountUp(perfectHitCount_Text, false, 0); PrepareTextForCountUp(greatHitCount_Text, false, 0); PrepareTextForCountUp(goodHitCount_Text, false, 0); PrepareTextForCountUp(missHitCount_Text, false, 0); PrepareTextForCountUp(perfectHitPercent_Text, true, 0f, "F1"); PrepareTextForCountUp(greatHitPercent_Text, true, 0f, "F1"); PrepareTextForCountUp(goodHitPercent_Text, true, 0f, "F1"); PrepareTextForCountUp(missHitPercent_Text, true, 0f, "F1"); // settlement: flash + scale 1.2 -> 1.0 yield return StartCoroutine(FlashCanvasGroupWithScale(settlementCanvasGroup, settleRoot, settleBaseScale * 1.2f, settleBaseScale, introFlashCount, introFlashDuration)); // right/right_bottom: X 1799 -> 1677.75 if (rightBottomRt != null) yield return StartCoroutine(AnimateAnchoredXAndFade(rightBottomRt, 1799f, 1677.75f, introMoveDuration, false)); // left/pics/quhuiImage flash if (quhuiImageTr != null) yield return StartCoroutine(FlashInTransform(quhuiImageTr, introFlashCount, introFlashDuration)); yield return StartCoroutine(FlashInTransform(thisSong_backPic != null ? thisSong_backPic.transform : null, 1, introStaticElementDuration)); yield return StartCoroutine(FlashInTransform(songName_Text != null ? songName_Text.transform : null, 1, introStaticElementDuration)); // score_bottom + shenstars move in together Coroutine scoreBottomIn = null; Coroutine shenstarsIn = null; if (scoreBottomRt != null) scoreBottomIn = StartCoroutine(AnimateAnchoredXAndFade(scoreBottomRt, -1646.9f, 0f, introMoveDuration, true)); if (shenstarsRt != null) shenstarsIn = StartCoroutine(AnimateAnchoredXAndFade(shenstarsRt, -1602.1f, -437.5785f, introMoveDuration, true)); if (scoreBottomIn != null || shenstarsIn != null) yield return WaitRealtime(introMoveDuration + 0.02f); // reward: Y -494.3 -> 0 with fade if (rewardRt != null) yield return StartCoroutine(AnimateAnchoredYAndFade(rewardRt, -494.3f, 0f, introMoveDuration)); // score numbers: pm -> idol -> accuracy yield return StartCoroutine(AnimateIntText(pmScoreSum_Text, targetPmScore, introNumberDuration)); yield return StartCoroutine(AnimateIntText(idolScoreSum_Text, targetIdolScore, introNumberDuration)); yield return StartCoroutine(AnimateFloatPercentText(accuracy_Text, targetAccuracyPercent, introNumberDuration, "F1")); // total + percent after above, and flash once completed Coroutine totalScoreIn = StartCoroutine(AnimateIntText(finalScore_Text, targetTotalScore, introNumberDuration)); Coroutine totalPercentIn = StartCoroutine(AnimateFloatPercentText(thisLevel_currentPercentage_Text, targetTotalPercent, introNumberDuration, "F2")); if (totalScoreIn != null || totalPercentIn != null) yield return WaitRealtime(introNumberDuration + 0.02f); yield return StartCoroutine(FlashInTransform(finalScore_Text != null ? finalScore_Text.transform : null, introFlashCount, introFlashDuration * 0.9f)); yield return StartCoroutine(FlashInTransform(thisLevel_currentPercentage_Text != null ? thisLevel_currentPercentage_Text.transform : null, introFlashCount, introFlashDuration * 0.9f)); yield return StartCoroutine(AnimateImageFill(thisLevel_progressBar_Image, (float)targetTotalScore / Mathf.Max(1, _1000000), introBarFillDuration)); yield return StartCoroutine(FlashInTransform(finalLevel_img != null ? finalLevel_img.transform : null, 1, introStaticElementDuration)); yield return StartCoroutine(FlashInTransform(personalRecord_Text != null ? personalRecord_Text.transform : null, 1, introStaticElementDuration)); yield return StartCoroutine(FlashInTransform(gameNote_rate != null ? gameNote_rate.transform : null, 1, introStaticElementDuration)); // hitStatus/status groups: flash in and count up numbers int noteCountForBars = Mathf.Max(1, targetPerfectCount + targetGreatCount + targetGoodCount + targetMissCount); yield return StartCoroutine(PlayStatusGroupIn(perfectGroup, perfectHitCount_Text, targetPerfectCount, perfectHitPercent_Text, targetPerfectPercent, perfect_barFill_Image, (float)targetPerfectCount / noteCountForBars)); yield return StartCoroutine(PlayStatusGroupIn(greatGroup, greatHitCount_Text, targetGreatCount, greatHitPercent_Text, targetGreatPercent, great_barFill_Image, (float)targetGreatCount / noteCountForBars)); yield return StartCoroutine(PlayStatusGroupIn(goodGroup, goodHitCount_Text, targetGoodCount, goodHitPercent_Text, targetGoodPercent, good_barFill_Image, (float)targetGoodCount / noteCountForBars)); yield return StartCoroutine(PlayStatusGroupIn(missGroup, missHitCount_Text, targetMissCount, missHitPercent_Text, targetMissPercent, miss_barFill_Image, (float)targetMissCount / noteCountForBars)); yield return StartCoroutine(AnimateIntText(earlyHitCount_Text, targetEarlyCount, introNumberDuration * 0.7f)); yield return StartCoroutine(AnimateIntText(lateHitCount_Text, targetLateCount, introNumberDuration * 0.7f)); yield return StartCoroutine(AnimateOffsetText(avgOffset_Text, targetAvgOffsetMs, introNumberDuration * 0.7f)); // ally cards enter before MASK. if (hasAllyCardEntry) yield return StartCoroutine(settlementTeamLoader.PlayAllySlotEntryBeforeMask()); // heroDApic/MASK/lihui flash if (lihuiTr != null) yield return StartCoroutine(FlashInTransform(lihuiTr, introFlashCount, introFlashDuration)); // MVP shows after MASK. if (shouldAnimateMvp) yield return StartCoroutine(AnimateMvpEntryAfterMask(mvpTr, mvpBaseLocalPos)); // Fallback for scenes still using container slide-in instead of per-slot tween. if (!hasAllyCardEntry && teamDisplayingRt != null) yield return StartCoroutine(AnimateAnchoredXAndFade(teamDisplayingRt, 2265f, 1490.09f, introMoveDuration, true)); // right/buttons: each button flashes in sequence for (int i = 0; i < rightButtons.Count; i++) { Transform btn = rightButtons[i]; if (btn == null) continue; yield return StartCoroutine(FlashInTransform(btn, introFlashCount, introFlashDuration)); yield return WaitRealtime(introStepGap); } ApplySettlementIntroFinalState(); settlementCanvasGroup.alpha = 1f; settlementCanvasGroup.interactable = true; settlementCanvasGroup.blocksRaycasts = true; hasCachedIntroMvpBaseLocalPos = false; cachedIntroMvpTransform = null; settlementIntroCoroutine = null; } private void SkipSettlementIntroAnimations() { if (skipIntroRequested) return; skipIntroRequested = true; if (settlementIntroCoroutine != null) { StopCoroutine(settlementIntroCoroutine); settlementIntroCoroutine = null; } if (mvpEntryTween != null) { if (mvpEntryTween.active) mvpEntryTween.Kill(false); mvpEntryTween = null; } if (settlementTeamLoader != null) settlementTeamLoader.CompleteAllySlotEntryImmediately(); ApplySettlementIntroFinalState(); if (settlementCanvasGroup != null) { settlementCanvasGroup.alpha = 1f; settlementCanvasGroup.interactable = true; settlementCanvasGroup.blocksRaycasts = true; } hasCachedIntroMvpBaseLocalPos = false; cachedIntroMvpTransform = null; } private void ApplySettlementIntroFinalState() { Transform settleRoot = settlementCanvasGroup != null ? settlementCanvasGroup.transform : transform; Transform rightRoot = FindDescendantByName(settleRoot, "right"); Transform leftRoot = FindDescendantByName(settleRoot, "left"); Transform picsRoot = FindDescendantByName(leftRoot != null ? leftRoot : settleRoot, "pics"); RectTransform rightBottomRt = FindRectByName(rightRoot != null ? rightRoot : settleRoot, "right_bottom"); Transform quhuiImageTr = FindDescendantByName(picsRoot != null ? picsRoot : settleRoot, "quhuiImage"); RectTransform scoreBottomRt = FindRectByName(settleRoot, "score_bottom"); RectTransform shenstarsRt = FindRectByName(settleRoot, "shenstars"); RectTransform rewardRt = FindRectByName(settleRoot, "reward"); RectTransform teamDisplayingRt = FindRectByName(rightRoot != null ? rightRoot : settleRoot, "teamDisplaying"); Transform buttonsRoot = FindDescendantByName(rightRoot != null ? rightRoot : settleRoot, "buttons"); List rightButtons = CollectDirectChildren(buttonsRoot); Transform heroDApicTr = FindDescendantByName(rightRoot != null ? rightRoot : settleRoot, "heroDApic"); Transform heroMaskTr = FindDescendantByName(heroDApicTr != null ? heroDApicTr : settleRoot, "MASK"); Transform lihuiTr = FindDescendantByName(heroMaskTr != null ? heroMaskTr : settleRoot, "lihui"); Transform mvpTr = (mvp_object != null) ? mvp_object.transform : FindDescendantByName(heroDApicTr != null ? heroDApicTr : settleRoot, "mvp"); if (rightBottomRt != null) { SetAnchoredX(rightBottomRt, 1677.75f); SetCanvasAlpha(rightBottomRt, 1f); } if (quhuiImageTr != null) SetCanvasAlpha(quhuiImageTr, 1f); if (scoreBottomRt != null) { SetAnchoredX(scoreBottomRt, 0f); SetCanvasAlpha(scoreBottomRt, 1f); } if (shenstarsRt != null) { SetAnchoredX(shenstarsRt, -437.5785f); SetCanvasAlpha(shenstarsRt, 1f); } if (rewardRt != null) { SetAnchoredY(rewardRt, 0f); SetCanvasAlpha(rewardRt, 1f); } if (lihuiTr != null) SetCanvasAlpha(lihuiTr, 1f); if (teamDisplayingRt != null) { SetAnchoredX(teamDisplayingRt, 1490.09f); SetCanvasAlpha(teamDisplayingRt, 1f); } for (int i = 0; i < rightButtons.Count; i++) { if (rightButtons[i] != null) SetCanvasAlpha(rightButtons[i], 1f); } Transform perfectGroup = FindGroupRoot(perfectHitCount_Text, perfectHitPercent_Text); Transform greatGroup = FindGroupRoot(greatHitCount_Text, greatHitPercent_Text); Transform goodGroup = FindGroupRoot(goodHitCount_Text, goodHitPercent_Text); Transform missGroup = FindGroupRoot(missHitCount_Text, missHitPercent_Text); if (perfectGroup != null) SetCanvasAlpha(perfectGroup, 1f); if (greatGroup != null) SetCanvasAlpha(greatGroup, 1f); if (goodGroup != null) SetCanvasAlpha(goodGroup, 1f); if (missGroup != null) SetCanvasAlpha(missGroup, 1f); int noteCountSum = Mathf.Max(1, targetPerfectCount + targetGreatCount + targetGoodCount + targetMissCount); if (pmScoreSum_Text != null) { pmScoreSum_Text.text = targetPmScore.ToString(); SetCanvasAlpha(pmScoreSum_Text.transform, 1f); } if (idolScoreSum_Text != null) { idolScoreSum_Text.text = targetIdolScore.ToString(); SetCanvasAlpha(idolScoreSum_Text.transform, 1f); } if (accuracy_Text != null) { accuracy_Text.text = targetAccuracyPercent.ToString("F1") + "%"; SetCanvasAlpha(accuracy_Text.transform, 1f); } if (finalScore_Text != null) { finalScore_Text.text = targetTotalScore.ToString(); SetCanvasAlpha(finalScore_Text.transform, 1f); } if (thisLevel_currentPercentage_Text != null) { thisLevel_currentPercentage_Text.text = targetTotalPercent.ToString("F2") + "%"; SetCanvasAlpha(thisLevel_currentPercentage_Text.transform, 1f); } if (perfectHitCount_Text != null) { perfectHitCount_Text.text = targetPerfectCount.ToString(); SetCanvasAlpha(perfectHitCount_Text.transform, 1f); } if (greatHitCount_Text != null) { greatHitCount_Text.text = targetGreatCount.ToString(); SetCanvasAlpha(greatHitCount_Text.transform, 1f); } if (goodHitCount_Text != null) { goodHitCount_Text.text = targetGoodCount.ToString(); SetCanvasAlpha(goodHitCount_Text.transform, 1f); } if (missHitCount_Text != null) { missHitCount_Text.text = targetMissCount.ToString(); SetCanvasAlpha(missHitCount_Text.transform, 1f); } if (perfectHitPercent_Text != null) { perfectHitPercent_Text.text = targetPerfectPercent.ToString("F1") + "%"; SetCanvasAlpha(perfectHitPercent_Text.transform, 1f); } if (greatHitPercent_Text != null) { greatHitPercent_Text.text = targetGreatPercent.ToString("F1") + "%"; SetCanvasAlpha(greatHitPercent_Text.transform, 1f); } if (goodHitPercent_Text != null) { goodHitPercent_Text.text = targetGoodPercent.ToString("F1") + "%"; SetCanvasAlpha(goodHitPercent_Text.transform, 1f); } if (missHitPercent_Text != null) { missHitPercent_Text.text = targetMissPercent.ToString("F1") + "%"; SetCanvasAlpha(missHitPercent_Text.transform, 1f); } if (thisLevel_progressBar_Image != null) thisLevel_progressBar_Image.fillAmount = (float)targetTotalScore / Mathf.Max(1, _1000000); if (perfect_barFill_Image != null) perfect_barFill_Image.fillAmount = (float)targetPerfectCount / noteCountSum; if (great_barFill_Image != null) great_barFill_Image.fillAmount = (float)targetGreatCount / noteCountSum; if (good_barFill_Image != null) good_barFill_Image.fillAmount = (float)targetGoodCount / noteCountSum; if (miss_barFill_Image != null) miss_barFill_Image.fillAmount = (float)targetMissCount / noteCountSum; if (thisSong_backPic != null) SetCanvasAlpha(thisSong_backPic.transform, 1f); if (songName_Text != null) SetCanvasAlpha(songName_Text.transform, 1f); if (finalLevel_img != null) SetCanvasAlpha(finalLevel_img.transform, 1f); if (personalRecord_Text != null) { if (!string.IsNullOrEmpty(targetPersonalRecordText)) personalRecord_Text.text = targetPersonalRecordText; SetCanvasAlpha(personalRecord_Text.transform, 1f); } if (gameNote_rate != null) { if (!string.IsNullOrEmpty(targetGameNoteRateText)) gameNote_rate.text = targetGameNoteRateText; SetCanvasAlpha(gameNote_rate.transform, 1f); } if (earlyHitCount_Text != null) { earlyHitCount_Text.text = targetEarlyCount.ToString(); SetCanvasAlpha(earlyHitCount_Text.transform, 1f); } if (lateHitCount_Text != null) { lateHitCount_Text.text = targetLateCount.ToString(); SetCanvasAlpha(lateHitCount_Text.transform, 1f); } if (avgOffset_Text != null) { avgOffset_Text.text = targetAvgOffsetMs.ToString("F2") + " ms"; SetCanvasAlpha(avgOffset_Text.transform, 1f); } if (mvpTr != null && mvpTr.gameObject.activeInHierarchy) { Vector3 targetPos; if (hasCachedIntroMvpBaseLocalPos && cachedIntroMvpTransform == mvpTr) targetPos = cachedIntroMvpBaseLocalPos; else targetPos = new Vector3(mvpTr.localPosition.x, mvpTr.localPosition.y - introMvpEnterYOffset, mvpTr.localPosition.z); mvpTr.localPosition = targetPos; SetCanvasAlpha(mvpTr, 1f); } } /// /// Documentation text normalized. /// private void InitializeSettlementCanvas() { if (settlementCanvasGroup != null) { settlementCanvasGroup.alpha = 0f; settlementCanvasGroup.interactable = false; settlementCanvasGroup.blocksRaycasts = false; Debug.Log("[SettlementController] Settlement canvas initialized to transparent state"); } else { Debug.LogWarning("[SettlementController] settlementCanvasGroup is not assigned, cannot initialize"); } } // Documentation text normalized. private static AllyHero_SO[] _cachedAllyHeroSOs; /// /// Documentation text normalized. /// private void PrepareSettlementMusic() { if (thisSong_so == null) { Debug.LogWarning("[SettlementController] thisSong_so is null, cannot prepare settlement music"); return; } if (settlementAudioSource == null) { Debug.LogWarning("[SettlementController] settlementAudioSource is not assigned, cannot prepare settlement music"); return; } dlcData foundDlc = SongDlcContentResolver.GetOwningDlc(thisSong_so); AudioClip resolvedSettlementMusic = thisSong_so.GetResolvedSettlementMusic(); if (foundDlc == null && resolvedSettlementMusic == null) { Debug.LogWarning($"[SettlementController] No DLC found containing song '{thisSong_so.songName}'"); return; } if (resolvedSettlementMusic == null) { string dlcName = foundDlc != null ? foundDlc.dlcName : thisSong_so.belongsTo_whichDLC; Debug.LogWarning($"[SettlementController] DLC '{dlcName}' does not have settlement music assigned"); return; } bool clipChanged = settlementAudioSource.clip != resolvedSettlementMusic; if (clipChanged) { settlementAudioSource.Stop(); settlementAudioSource.clip = resolvedSettlementMusic; settlementAudioSource.time = 0f; } settlementAudioSource.loop = true; settlementAudioSource.playOnAwake = false; settlementAudioSource.volume = 1f; settlementAudioSource.mute = false; string resolvedDlcName = foundDlc != null ? foundDlc.dlcName : (thisSong_so.belongsTo_whichDLC ?? "DLC"); Debug.Log($"[SettlementController] Prepared settlement music from DLC '{resolvedDlcName}': {resolvedSettlementMusic.name}"); } /// /// Documentation text normalized. /// private void StartMusicTransition() { if (musicTransitionCoroutine != null) { StopCoroutine(musicTransitionCoroutine); } musicTransitionCoroutine = StartCoroutine(MusicTransitionRoutine()); } /// /// Documentation text normalized. /// Documentation text normalized. /// Documentation text normalized. /// private IEnumerator MusicTransitionRoutine() { float elapsed = 0f; float transitionDuration = Mathf.Max(0.01f, lowpassFadeDuration); float startVolume = (oriGameMusicSource != null) ? oriGameMusicSource.volume : 1f; bool settlementMusicStarted = false; float settlementStartVolume = 0f; // Start settlement music immediately to avoid delayed playback at settlement start. if (settlementAudioSource != null && settlementAudioSource.clip != null) { try { settlementAudioSource.mute = false; if (!settlementAudioSource.isPlaying) { settlementAudioSource.volume = 0f; settlementAudioSource.Play(); settlementStartVolume = 0f; } else { settlementStartVolume = Mathf.Clamp01(settlementAudioSource.volume); } settlementMusicStarted = true; Debug.Log($"[SettlementController] Settlement music active: {settlementAudioSource.clip.name}"); } catch (System.Exception ex) { Debug.LogWarning($"[SettlementController] Failed to start settlement music immediately: {ex.Message}"); } } if (oriGameMusicSource != null) { Debug.Log($"[SettlementController] Starting game-music fade out over {transitionDuration}s"); while (elapsed < transitionDuration) { elapsed += Time.unscaledDeltaTime; float t = Mathf.Clamp01(elapsed / transitionDuration); oriGameMusicSource.volume = Mathf.Lerp(startVolume, 0f, t); if (settlementMusicStarted && settlementAudioSource != null) settlementAudioSource.volume = Mathf.Lerp(settlementStartVolume, 1f, t); yield return null; } oriGameMusicSource.volume = 0f; oriGameMusicSource.Pause(); Debug.Log("[SettlementController] Game music fade complete, game music paused."); } else { Debug.LogWarning("[SettlementController] oriGameMusicSource not assigned, skipping game music fade"); if (settlementMusicStarted && settlementAudioSource != null) settlementAudioSource.volume = 1f; } if (!settlementMusicStarted && settlementAudioSource != null && settlementAudioSource.clip != null) { try { settlementAudioSource.mute = false; settlementAudioSource.volume = 1f; settlementAudioSource.Play(); Debug.Log($"[SettlementController] Settlement music started: {settlementAudioSource.clip.name}"); } catch (System.Exception ex) { Debug.LogWarning($"[SettlementController] Failed to start settlement music: {ex.Message}"); } } musicTransitionCoroutine = null; } /// /// Documentation text normalized. /// private void StartCanvasFadeIn() { if (canvasFadeCoroutine != null) { StopCoroutine(canvasFadeCoroutine); } canvasFadeCoroutine = StartCoroutine(CanvasFadeInRoutine()); } /// /// Documentation text normalized. /// private IEnumerator CanvasFadeInRoutine() { if (settlementCanvasGroup == null) { Debug.LogWarning("[SettlementController] settlementCanvasGroup is not assigned, cannot fade in"); yield break; } float fadeDuration = 0.25f; float elapsed = 0f; float startAlpha = settlementCanvasGroup.alpha; float targetAlpha = 1f; Debug.Log($"[SettlementController] Starting CanvasGroup fade-in from {startAlpha} to {targetAlpha} over {fadeDuration}s"); while (elapsed < fadeDuration) { elapsed += Time.unscaledDeltaTime; float t = Mathf.Clamp01(elapsed / fadeDuration); settlementCanvasGroup.alpha = Mathf.Lerp(startAlpha, targetAlpha, t); yield return null; } // Documentation text normalized. settlementCanvasGroup.alpha = targetAlpha; settlementCanvasGroup.interactable = true; settlementCanvasGroup.blocksRaycasts = true; Debug.Log("[SettlementController] CanvasGroup fade-in complete"); canvasFadeCoroutine = null; } private IEnumerator PlayStatusGroupIn(Transform groupRoot, Text countText, int countTarget, Text percentText, float percentTarget) { yield return StartCoroutine(PlayStatusGroupIn(groupRoot, countText, countTarget, percentText, percentTarget, null, 0f)); } private IEnumerator PlayStatusGroupIn(Transform groupRoot, Text countText, int countTarget, Text percentText, float percentTarget, Image fillImage, float fillTarget) { if (groupRoot != null) yield return StartCoroutine(FlashInTransform(groupRoot, introFlashCount, introFlashDuration)); Coroutine c1 = null; Coroutine c2 = null; Coroutine c3 = null; if (countText != null) c1 = StartCoroutine(AnimateIntText(countText, countTarget, introNumberDuration)); if (percentText != null) c2 = StartCoroutine(AnimateFloatPercentText(percentText, percentTarget, introNumberDuration, "F1")); if (fillImage != null) c3 = StartCoroutine(AnimateImageFill(fillImage, fillTarget, introBarFillDuration)); if (c1 != null || c2 != null || c3 != null) yield return WaitRealtime(introNumberDuration + 0.02f); } private IEnumerator AnimateMvpEntryAfterMask(Transform mvpTransform, Vector3 baseLocalPosition) { if (mvpTransform == null || !mvpTransform.gameObject.activeInHierarchy) yield break; if (mvpEntryTween != null) { if (mvpEntryTween.active) mvpEntryTween.Kill(false); mvpEntryTween = null; } CanvasGroup group = GetOrAddCanvasGroup(mvpTransform); if (group != null) group.alpha = 0f; mvpTransform.localPosition = new Vector3(baseLocalPosition.x, baseLocalPosition.y + introMvpEnterYOffset, baseLocalPosition.z); float duration = Mathf.Max(0.01f, introMvpEnterDuration); Sequence seq = DOTween.Sequence().SetUpdate(introTweenUseUnscaledTime); seq.Join(mvpTransform.DOLocalMoveY(baseLocalPosition.y, duration).SetEase(introMvpEnterEase).SetUpdate(introTweenUseUnscaledTime)); if (group != null) seq.Join(group.DOFade(1f, duration).SetEase(introMvpEnterEase).SetUpdate(introTweenUseUnscaledTime)); mvpEntryTween = seq; yield return seq.WaitForCompletion(); mvpEntryTween = null; } private IEnumerator AnimateIntText(Text target, int toValue, float duration) { if (target == null) yield break; SetCanvasAlpha(target.transform, 1f); float elapsed = 0f; float dur = Mathf.Max(0.01f, duration); while (elapsed < dur) { if (skipIntroRequested) break; elapsed += Time.unscaledDeltaTime; float t = EaseOutCubic(Mathf.Clamp01(elapsed / dur)); int value = Mathf.RoundToInt(Mathf.Lerp(0f, toValue, t)); target.text = value.ToString(); yield return null; } target.text = toValue.ToString(); } private IEnumerator AnimateFloatPercentText(Text target, float toValue, float duration, string format) { if (target == null) yield break; SetCanvasAlpha(target.transform, 1f); float elapsed = 0f; float dur = Mathf.Max(0.01f, duration); while (elapsed < dur) { if (skipIntroRequested) break; elapsed += Time.unscaledDeltaTime; float t = EaseOutCubic(Mathf.Clamp01(elapsed / dur)); float value = Mathf.Lerp(0f, toValue, t); target.text = value.ToString(format) + "%"; yield return null; } target.text = toValue.ToString(format) + "%"; } private IEnumerator AnimateOffsetText(Text target, float toValue, float duration) { if (target == null) yield break; SetCanvasAlpha(target.transform, 1f); float elapsed = 0f; float dur = Mathf.Max(0.01f, duration); while (elapsed < dur) { if (skipIntroRequested) break; elapsed += Time.unscaledDeltaTime; float t = EaseOutCubic(Mathf.Clamp01(elapsed / dur)); float value = Mathf.Lerp(0f, toValue, t); target.text = value.ToString("F2") + " ms"; yield return null; } target.text = toValue.ToString("F2") + " ms"; } private IEnumerator AnimateImageFill(Image target, float toValue, float duration) { if (target == null) yield break; target.fillAmount = 0f; float elapsed = 0f; float dur = Mathf.Max(0.01f, duration); float targetValue = Mathf.Clamp01(toValue); while (elapsed < dur) { if (skipIntroRequested) break; elapsed += Time.unscaledDeltaTime; float t = EaseOutCubic(Mathf.Clamp01(elapsed / dur)); target.fillAmount = Mathf.Lerp(0f, targetValue, t); yield return null; } target.fillAmount = targetValue; } private IEnumerator AnimateAnchoredXAndFade(RectTransform rt, float fromX, float toX, float duration, bool fadeIn) { if (rt == null) yield break; SetAnchoredX(rt, fromX); if (fadeIn) SetCanvasAlpha(rt, 0f); float elapsed = 0f; float dur = Mathf.Max(0.01f, duration); while (elapsed < dur) { if (skipIntroRequested) break; elapsed += Time.unscaledDeltaTime; float t = EaseOutCubic(Mathf.Clamp01(elapsed / dur)); SetAnchoredX(rt, Mathf.LerpUnclamped(fromX, toX, t)); if (fadeIn) SetCanvasAlpha(rt, t); yield return null; } SetAnchoredX(rt, toX); if (fadeIn) SetCanvasAlpha(rt, 1f); } private IEnumerator AnimateAnchoredYAndFade(RectTransform rt, float fromY, float toY, float duration) { if (rt == null) yield break; SetAnchoredY(rt, fromY); SetCanvasAlpha(rt, 0f); float elapsed = 0f; float dur = Mathf.Max(0.01f, duration); while (elapsed < dur) { if (skipIntroRequested) break; elapsed += Time.unscaledDeltaTime; float t = EaseOutCubic(Mathf.Clamp01(elapsed / dur)); SetAnchoredY(rt, Mathf.LerpUnclamped(fromY, toY, t)); SetCanvasAlpha(rt, t); yield return null; } SetAnchoredY(rt, toY); SetCanvasAlpha(rt, 1f); } private IEnumerator FlashCanvasGroupWithScale(CanvasGroup cg, Transform scaleTarget, Vector3 fromScale, Vector3 toScale, int flashes, float duration) { if (cg == null) yield break; int count = Mathf.Max(1, flashes); float total = Mathf.Max(0.05f, duration); float flashWindow = total / Mathf.Max(1, count * 2); cg.alpha = 0f; if (scaleTarget != null) scaleTarget.localScale = fromScale; float elapsed = 0f; while (elapsed < total) { if (skipIntroRequested) break; elapsed += Time.unscaledDeltaTime; float p = Mathf.Clamp01(elapsed / total); if (scaleTarget != null) { float k = EaseOutCubic(p); scaleTarget.localScale = Vector3.LerpUnclamped(fromScale, toScale, k); } if (flashWindow > 0f) { int seg = Mathf.FloorToInt(elapsed / flashWindow); bool on = (seg % 2) == 0; cg.alpha = on ? 1f : 0f; } else { cg.alpha = 1f; } yield return null; } cg.alpha = 1f; if (scaleTarget != null) scaleTarget.localScale = toScale; } private IEnumerator FlashInTransform(Transform target, int flashes, float duration) { if (target == null) yield break; int count = Mathf.Max(1, flashes); float total = Mathf.Max(0.05f, duration); float step = total / Mathf.Max(1, count * 2); SetCanvasAlpha(target, 0f); for (int i = 0; i < count; i++) { SetCanvasAlpha(target, 1f); yield return WaitRealtime(step); if (i < count - 1) { SetCanvasAlpha(target, 0f); yield return WaitRealtime(step); } } SetCanvasAlpha(target, 1f); } private void PrepareStaticSettlementElement(Transform target) { if (target == null) return; SetCanvasAlpha(target, 0f); } private void PrepareImageFill(Image target) { if (target == null) return; target.fillAmount = 0f; } private void PrepareTextForCountUp(Text t, bool isPercent, float initialValue, string format = "F1") { if (t == null) return; SetCanvasAlpha(t.transform, 0f); t.text = isPercent ? initialValue.ToString(format) + "%" : Mathf.RoundToInt(initialValue).ToString(); } private void PrepareTextForCountUp(Text t, bool isPercent, int initialValue) { if (t == null) return; SetCanvasAlpha(t.transform, 0f); t.text = isPercent ? initialValue.ToString("F1") + "%" : initialValue.ToString(); } private static float EaseOutCubic(float t) { float inv = 1f - t; return 1f - inv * inv * inv; } private IEnumerator WaitRealtime(float duration) { float elapsed = 0f; float dur = Mathf.Max(0f, duration); while (elapsed < dur) { if (skipIntroRequested) yield break; elapsed += Time.unscaledDeltaTime; yield return null; } } private static void SetAnchoredX(RectTransform rt, float x) { if (rt == null) return; Vector2 p = rt.anchoredPosition; p.x = x; rt.anchoredPosition = p; } private static void SetAnchoredY(RectTransform rt, float y) { if (rt == null) return; Vector2 p = rt.anchoredPosition; p.y = y; rt.anchoredPosition = p; } private static List CollectDirectChildren(Transform root) { List list = new List(); if (root == null) return list; for (int i = 0; i < root.childCount; i++) { Transform c = root.GetChild(i); if (c != null) list.Add(c); } return list; } private static Transform FindDescendantByName(Transform root, string exactName) { if (root == null || string.IsNullOrEmpty(exactName)) return null; Transform[] all = root.GetComponentsInChildren(true); for (int i = 0; i < all.Length; i++) { Transform t = all[i]; if (t == null) continue; if (string.Equals(t.name, exactName, System.StringComparison.OrdinalIgnoreCase)) return t; } return null; } private static RectTransform FindRectByName(Transform root, string exactName) { Transform t = FindDescendantByName(root, exactName); return t as RectTransform; } private static Transform FindGroupRoot(Text countText, Text percentText) { Transform c = countText != null ? countText.transform : null; Transform p = percentText != null ? percentText.transform : null; if (c == null && p == null) return null; if (c == null) return p != null ? p.parent : null; if (p == null) return c.parent; Transform probe = c; while (probe != null) { if (p.IsChildOf(probe)) return probe; probe = probe.parent; } return c.parent; } private static CanvasGroup GetOrAddCanvasGroup(Transform root) { if (root == null) return null; CanvasGroup cg = root.GetComponent(); if (cg == null) cg = root.gameObject.AddComponent(); return cg; } private static void SetCanvasAlpha(Transform root, float alpha) { if (root == null) return; CanvasGroup cg = GetOrAddCanvasGroup(root); if (cg == null) return; cg.alpha = Mathf.Clamp01(alpha); } /// /// Documentation text normalized. /// private void OnReplayButtonClicked() { Debug.Log("[SettlementController] Replay button clicked - restarting current game"); // Documentation text normalized. if (bmm == null) { Debug.LogError("[SettlementController] BeatmapManager is null, cannot replay"); return; } if (bmm.assignedSongData == null) { Debug.LogError("[SettlementController] AssignedSongData is null, cannot replay"); return; } // Documentation text normalized. SongData songToReplay = bmm.assignedSongData; int difficultyToReplay = bmm.assignedDifficulty; if (difficultyToReplay < 0) { Debug.LogError("[SettlementController] AssignedDifficulty is invalid, cannot replay"); return; } Debug.Log($"[SettlementController] Reloading song: {songToReplay.songName}, difficulty: {difficultyToReplay}"); // Documentation text normalized. if (settlementAudioSource != null && settlementAudioSource.isPlaying) { settlementAudioSource.Stop(); } // Documentation text normalized. BeatmapManager.SetPendingSong(songToReplay, difficultyToReplay); // Documentation text normalized. StartCoroutine(LoadSceneAsync(SceneManager.GetActiveScene().name)); } private IEnumerator LoadSceneAsync(string sceneName) { if (gTransition.LoadScene(sceneName, LoadSceneMode.Single)) { while (gTransition.IsBusy) { yield return null; } yield break; } AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(sceneName); while (asyncLoad != null && !asyncLoad.isDone) { yield return null; } } /// /// Documentation text normalized. /// private void OnExitButtonClicked() { Debug.Log("[SettlementController] Exit button clicked - returning to song selection"); // Documentation text normalized. if (settlementAudioSource != null && settlementAudioSource.isPlaying) { settlementAudioSource.Stop(); } // Documentation text normalized. BeatmapManager.pendingSongData = null; BeatmapManager.pendingDifficulty = -1; // Ensure time scale restored try { Time.timeScale = 1f; } catch {} string targetScene = "selectYourSongFirst"; ArenaRoomService arenaRoomService = ArenaRoomService.Instance; if (arenaRoomService != null && arenaRoomService.IsInRoom) { arenaRoomService.RequestReturnToRoomUi(); targetScene = arenaRoomService.RoomSceneName; } // If GameManager and its blackMaskImage available, start coroutine on gm to fade to black then load if (gm != null && gm.blackMaskImage != null) { gm.StartCoroutine(FadeToBlackAndLoadOnGM(targetScene, 0.25f)); } else { StartCoroutine(LoadSceneAsync(targetScene)); } } private void TrySubmitArenaRoomScore() { if (roomScoreSubmitTriggered) { return; } ArenaRoomService arenaRoomService = ArenaRoomService.Instance; if (arenaRoomService == null || !arenaRoomService.IsInRoom) { return; } roomScoreSubmitTriggered = true; _ = SubmitArenaRoomScoreAsync(arenaRoomService); } private async System.Threading.Tasks.Task SubmitArenaRoomScoreAsync(ArenaRoomService arenaRoomService) { try { await arenaRoomService.SubmitScore( targetTotalScore, GetArenaGrade(targetTotalScore), targetPmScore, targetIdolScore); } catch (Exception ex) { Debug.LogWarning($"[settlementController] Arena room score submit failed: {ex.Message}"); } } private static string GetArenaGrade(long totalScore) { if (totalScore >= 960000) return "SSS"; if (totalScore >= 920000) return "SS"; if (totalScore >= 880000) return "S"; if (totalScore >= 820000) return "A"; if (totalScore >= 720000) return "B"; if (totalScore >= 600000) return "C"; if (totalScore >= 400000) return "D"; return "F"; } // Coroutine that will be started on the GameManager instance so that the gm's MonoBehaviour runs it private IEnumerator FadeToBlackAndLoadOnGM(string sceneName, float duration) { if (gTransition.LoadScene(sceneName, LoadSceneMode.Single, duration, duration)) { while (gTransition.IsBusy) { yield return null; } yield break; } if (gm == null || gm.blackMaskImage == null) { AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(sceneName); while (asyncLoad != null && !asyncLoad.isDone) yield return null; yield break; } var blackMask = gm.blackMaskImage; // ensure image active if (!blackMask.gameObject.activeSelf) blackMask.gameObject.SetActive(true); float startA = blackMask.color.a; float elapsed = 0f; // enable raycast target while fading to block input try { blackMask.raycastTarget = true; } catch {} while (elapsed < duration) { elapsed += Time.unscaledDeltaTime; float frac = Mathf.Clamp01(elapsed / Mathf.Max(0.0001f, duration)); Color c = blackMask.color; c.a = Mathf.Lerp(startA, 1f, frac); blackMask.color = c; yield return null; } // ensure fully opaque Color fc = blackMask.color; fc.a = 1f; blackMask.color = fc; // load target scene AsyncOperation asyncOp = SceneManager.LoadSceneAsync(sceneName); while (asyncOp != null && !asyncOp.isDone) { yield return null; } } /// /// Documentation text normalized. /// Documentation text normalized. /// private void SetMvpHeroImageFromTopScorer() { if (mvp_hero_hd_image == null) { Debug.LogWarning("[SettlementController] mvp_hero_hd_image is not assigned"); return; } int topIndex = -1; int[] scores = new int[5]; if (sm != null) { scores[0] = sm.red_idolScore_sum; scores[1] = sm.green_idolScore_sum; scores[2] = sm.yellow_idolScore_sum; scores[3] = sm.purple_idolScore_sum; scores[4] = sm.blue_idolScore_sum; } else { // fallback: read AllyCombatant.currentScore from scene for (int i = 0; i < 5; i++) { var go = SceneObjectLookupCache.Find($"ally_0{ i+1 }"); if (go != null) { var ac = go.GetComponent(); scores[i] = ac != null ? ac.currentScore : 0; } else scores[i] = 0; } } int max = -1; for (int i = 0; i < scores.Length; i++) { if (scores[i] > max) { max = scores[i]; topIndex = i; } } if (topIndex < 0 || max <= 0) { Debug.Log("[SettlementController] No top scorer found or scores are all zero"); // hide image and mvp object if (mvp_object != null) mvp_object.SetActive(false); if (mvp_hero_hd_image != null) { mvp_hero_hd_image.sprite = null; mvp_hero_hd_image.color = new Color(mvp_hero_hd_image.color.r, mvp_hero_hd_image.color.g, mvp_hero_hd_image.color.b, 0f); } // Activate legacy_text when no MVP conditions are met if (legacy_text != null) legacy_text.gameObject.SetActive(true); return; } // Try to resolve AllyHero_SO for the winning slot AllyHero_SO heroSO = null; // try SkillBuilder cache if (SkillBuilder.Instance != null) { try { heroSO = SkillBuilder.Instance.GetAllyHeroSOBySlot(topIndex); } catch { heroSO = null; } } // fallback: use teamUIController slot ids to find hero id -> load SO if (heroSO == null && teamUIController.Instance != null && teamUIController.Instance.allySlotIds != null) { int allyId = -1; if (topIndex >= 0 && topIndex < teamUIController.Instance.allySlotIds.Count) allyId = teamUIController.Instance.allySlotIds[topIndex]; if (allyId > 0) { if (_cachedAllyHeroSOs == null || _cachedAllyHeroSOs.Length == 0) { _cachedAllyHeroSOs = RuntimeResourcesCache.LoadAllAllyHeroes(); } foreach (var a in _cachedAllyHeroSOs) { if (a != null && a.ally_heroID == allyId) { heroSO = a; break; } } } } // final fallback: try direct Resources lookup by scanning all and picking first non-null for slot // REMOVED: This fallback causes a default character to appear when no hero is selected. /* if (heroSO == null) { if (_cachedAllyHeroSOs == null || _cachedAllyHeroSOs.Length == 0) { _cachedAllyHeroSOs = RuntimeResourcesCache.LoadAllAllyHeroes(); } if (_cachedAllyHeroSOs != null && _cachedAllyHeroSOs.Length > 0) { // attempt to match by name or just pick first heroSO = _cachedAllyHeroSOs[0] as AllyHero_SO; } } */ if (heroSO != null) { if (mvp_object != null) mvp_object.SetActive(true); if (heroSO.ally_hero_HD_image != null && mvp_hero_hd_image != null) { mvp_hero_hd_image.sprite = heroSO.ally_hero_HD_image; mvp_hero_hd_image.color = new Color(mvp_hero_hd_image.color.r, mvp_hero_hd_image.color.g, mvp_hero_hd_image.color.b, 1f); } // 更新 MVP 详情界面(名字、头像、分数) if (mvp_heroName != null) mvp_heroName.text = heroSO.ally_heroName; if (mvp_heroIcon != null) mvp_heroIcon.sprite = heroSO.ally_hero_squareProfile; if (mvp_score != null) mvp_score.text = max.ToString(); // Deactivate legacy_text when MVP is successfully displayed if (legacy_text != null) legacy_text.gameObject.SetActive(false); Debug.Log($"[SettlementController] MVP assigned from slot {topIndex+1} with score {max}: {heroSO.ally_heroName}"); } else { if (mvp_object != null) mvp_object.SetActive(false); if (mvp_hero_hd_image != null) { mvp_hero_hd_image.sprite = null; mvp_hero_hd_image.color = new Color(mvp_hero_hd_image.color.r, mvp_hero_hd_image.color.g, mvp_hero_hd_image.color.b, 0f); } // Activate legacy_text when hero SO is not found if (legacy_text != null) legacy_text.gameObject.SetActive(true); Debug.LogWarning("[SettlementController] MVP SO or HD image not found"); } } }