Files
2026-07-30 23:15:58 +08:00

2537 lines
94 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
[Tooltip("显示中位数偏移 + 校准建议的文本(可选)")]
public Text offsetSuggestion_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;
// 整体结算入场动画速度倍率。>1 更快,所有分段时长与间隔按此等比缩短,编排/节奏比例与业务不变。
// 新增字段:旧场景实例反序列化时缺此字段会取此默认值,因此无需改动 Inspector 即可生效。
[Tooltip("结算入场动画整体速度倍率(>1 更快)。等比缩短所有分段,不改变编排与业务逻辑。")]
[SerializeField] private float introTimeScale = 2f;
[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<int> uniqueHeroIds = new HashSet<int>();
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<AllyHero_SO> 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<AllyHero_SO> ResolveCurrentLineupHeroSOs()
{
List<AllyHero_SO> result = new List<AllyHero_SO>();
HashSet<int> uniqueHeroIds = new HashSet<int>();
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<AllyCombatant>();
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<GameManager>();
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;
int levelRuleScoreBonus = LevelRuleController.ConsumePendingSettlementScoreBonus();
targetIdolScore = sm.allSum_idolScore + levelRuleScoreBonus;
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";
}
UpdateOffsetSuggestion(sm);
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<loadSettlementTeamPrefab>();
}
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<GameManager>();
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<global::rewardPrefab>() : 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<NoteSpawner>();
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<Transform> rightButtons = CollectDirectChildren(buttonsRoot);
Transform hitStatusTr = FindDescendantByName(settleRoot, "hitStatus");
Transform statusTr = FindDescendantByName(hitStatusTr != null ? hitStatusTr : settleRoot, "status");
List<Transform> 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<Transform> orderedGroups = new List<Transform>();
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));
// Main settlement blocks enter together instead of waiting for each part in sequence.
bool hasMainEntryAnimations = false;
if (rightBottomRt != null)
{
StartCoroutine(AnimateAnchoredXAndFade(rightBottomRt, 1799f, 1677.75f, introMoveDuration, false));
hasMainEntryAnimations = true;
}
if (quhuiImageTr != null)
{
StartCoroutine(FlashInTransform(quhuiImageTr, introFlashCount, introFlashDuration));
hasMainEntryAnimations = true;
}
if (thisSong_backPic != null)
{
StartCoroutine(FlashInTransform(thisSong_backPic.transform, 1, introStaticElementDuration));
hasMainEntryAnimations = true;
}
if (songName_Text != null)
{
StartCoroutine(FlashInTransform(songName_Text.transform, 1, introStaticElementDuration));
hasMainEntryAnimations = true;
}
if (scoreBottomRt != null)
{
StartCoroutine(AnimateAnchoredXAndFade(scoreBottomRt, -1646.9f, 0f, introMoveDuration, true));
hasMainEntryAnimations = true;
}
if (shenstarsRt != null)
{
StartCoroutine(AnimateAnchoredXAndFade(shenstarsRt, -1602.1f, -437.5785f, introMoveDuration, true));
hasMainEntryAnimations = true;
}
if (rewardRt != null)
{
StartCoroutine(AnimateAnchoredYAndFade(rewardRt, -494.3f, 0f, introMoveDuration));
hasMainEntryAnimations = true;
}
if (hasMainEntryAnimations)
yield return WaitRealtime(Mathf.Max(introMoveDuration, introFlashDuration, introStaticElementDuration) + 0.02f);
bool hasScoreAnimations = false;
if (pmScoreSum_Text != null)
{
StartCoroutine(AnimateIntText(pmScoreSum_Text, targetPmScore, introNumberDuration));
hasScoreAnimations = true;
}
if (idolScoreSum_Text != null)
{
StartCoroutine(AnimateIntText(idolScoreSum_Text, targetIdolScore, introNumberDuration));
hasScoreAnimations = true;
}
if (accuracy_Text != null)
{
StartCoroutine(AnimateFloatPercentText(accuracy_Text, targetAccuracyPercent, introNumberDuration, "F1"));
hasScoreAnimations = true;
}
if (finalScore_Text != null)
{
StartCoroutine(AnimateIntText(finalScore_Text, targetTotalScore, introNumberDuration));
hasScoreAnimations = true;
}
if (thisLevel_currentPercentage_Text != null)
{
StartCoroutine(AnimateFloatPercentText(thisLevel_currentPercentage_Text, targetTotalPercent, introNumberDuration, "F2"));
hasScoreAnimations = true;
}
if (thisLevel_progressBar_Image != null)
{
StartCoroutine(AnimateImageFill(thisLevel_progressBar_Image, (float)targetTotalScore / Mathf.Max(1, _1000000), introBarFillDuration));
hasScoreAnimations = true;
}
if (finalLevel_img != null)
{
StartCoroutine(FlashInTransform(finalLevel_img.transform, 1, introStaticElementDuration));
hasScoreAnimations = true;
}
if (personalRecord_Text != null)
{
StartCoroutine(FlashInTransform(personalRecord_Text.transform, 1, introStaticElementDuration));
hasScoreAnimations = true;
}
if (gameNote_rate != null)
{
StartCoroutine(FlashInTransform(gameNote_rate.transform, 1, introStaticElementDuration));
hasScoreAnimations = true;
}
if (hasScoreAnimations)
yield return WaitRealtime(Mathf.Max(introNumberDuration, introBarFillDuration, introStaticElementDuration) + 0.02f);
bool hasScoreFlashAnimations = false;
if (finalScore_Text != null)
{
StartCoroutine(FlashInTransform(finalScore_Text.transform, introFlashCount, introFlashDuration * 0.9f));
hasScoreFlashAnimations = true;
}
if (thisLevel_currentPercentage_Text != null)
{
StartCoroutine(FlashInTransform(thisLevel_currentPercentage_Text.transform, introFlashCount, introFlashDuration * 0.9f));
hasScoreFlashAnimations = true;
}
if (hasScoreFlashAnimations)
yield return WaitRealtime(introFlashDuration * 0.9f + 0.02f);
// hitStatus/status groups: flash in and count up numbers together.
int noteCountForBars = Mathf.Max(1, targetPerfectCount + targetGreatCount + targetGoodCount + targetMissCount);
bool hasStatusAnimations = false;
StartCoroutine(PlayStatusGroupIn(perfectGroup, perfectHitCount_Text, targetPerfectCount, perfectHitPercent_Text, targetPerfectPercent, perfect_barFill_Image, (float)targetPerfectCount / noteCountForBars));
hasStatusAnimations = true;
StartCoroutine(PlayStatusGroupIn(greatGroup, greatHitCount_Text, targetGreatCount, greatHitPercent_Text, targetGreatPercent, great_barFill_Image, (float)targetGreatCount / noteCountForBars));
hasStatusAnimations = true;
StartCoroutine(PlayStatusGroupIn(goodGroup, goodHitCount_Text, targetGoodCount, goodHitPercent_Text, targetGoodPercent, good_barFill_Image, (float)targetGoodCount / noteCountForBars));
hasStatusAnimations = true;
StartCoroutine(PlayStatusGroupIn(missGroup, missHitCount_Text, targetMissCount, missHitPercent_Text, targetMissPercent, miss_barFill_Image, (float)targetMissCount / noteCountForBars));
hasStatusAnimations = true;
if (earlyHitCount_Text != null)
{
StartCoroutine(AnimateIntText(earlyHitCount_Text, targetEarlyCount, introNumberDuration * 0.7f));
hasStatusAnimations = true;
}
if (lateHitCount_Text != null)
{
StartCoroutine(AnimateIntText(lateHitCount_Text, targetLateCount, introNumberDuration * 0.7f));
hasStatusAnimations = true;
}
if (avgOffset_Text != null)
{
StartCoroutine(AnimateOffsetText(avgOffset_Text, targetAvgOffsetMs, introNumberDuration * 0.7f));
hasStatusAnimations = true;
}
if (hasStatusAnimations)
yield return WaitRealtime(Mathf.Max(introFlashDuration + introNumberDuration, introFlashDuration + introBarFillDuration, introNumberDuration * 0.7f) + 0.04f);
// Team, MVP and buttons enter together after the score/status area is visible.
bool hasFinalEntryAnimations = false;
if (hasAllyCardEntry)
{
StartCoroutine(settlementTeamLoader.PlayAllySlotEntryBeforeMask());
hasFinalEntryAnimations = true;
}
if (lihuiTr != null)
{
StartCoroutine(FlashInTransform(lihuiTr, introFlashCount, introFlashDuration));
hasFinalEntryAnimations = true;
}
if (shouldAnimateMvp)
{
StartCoroutine(AnimateMvpEntryAfterMask(mvpTr, mvpBaseLocalPos));
hasFinalEntryAnimations = true;
}
if (!hasAllyCardEntry && teamDisplayingRt != null)
{
StartCoroutine(AnimateAnchoredXAndFade(teamDisplayingRt, 2265f, 1490.09f, introMoveDuration, true));
hasFinalEntryAnimations = true;
}
for (int i = 0; i < rightButtons.Count; i++)
{
Transform btn = rightButtons[i];
if (btn == null) continue;
StartCoroutine(FlashInTransform(btn, introFlashCount, introFlashDuration));
hasFinalEntryAnimations = true;
}
if (hasFinalEntryAnimations)
yield return WaitRealtime(Mathf.Max(introMoveDuration, introFlashDuration, introMvpEnterDuration) + Mathf.Max(0.1f, 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<Transform> 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 (offsetSuggestion_Text != null) SetCanvasAlpha(offsetSuggestion_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);
}
}
/// <summary>
/// Documentation text normalized.
/// </summary>
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;
/// <summary>
/// Documentation text normalized.
/// </summary>
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}");
}
/// <summary>
/// Documentation text normalized.
/// </summary>
private void StartMusicTransition()
{
if (musicTransitionCoroutine != null)
{
StopCoroutine(musicTransitionCoroutine);
}
musicTransitionCoroutine = StartCoroutine(MusicTransitionRoutine());
}
/// <summary>
/// Documentation text normalized.
/// Documentation text normalized.
/// Documentation text normalized.
/// </summary>
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;
}
/// <summary>
/// Documentation text normalized.
/// </summary>
private void StartCanvasFadeIn()
{
if (canvasFadeCoroutine != null)
{
StopCoroutine(canvasFadeCoroutine);
}
canvasFadeCoroutine = StartCoroutine(CanvasFadeInRoutine());
}
/// <summary>
/// Documentation text normalized.
/// </summary>
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, ScaledDuration(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, ScaledDuration(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, ScaledDuration(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) + "%";
}
// 结算页校准建议:用中位数(抗离群点)而非平均值判断玩家系统性偏差,
// 并给出 UserGlobalDelaySeconds 的建议值(仅显示,不自动写入,保持业务逻辑等效)。
private void UpdateOffsetSuggestion(ScoreManager sm)
{
if (offsetSuggestion_Text == null) return;
if (sm == null || sm.offsetCount <= 0)
{
offsetSuggestion_Text.text = string.Empty;
return;
}
// 正数 = 偏早(Early),负数 = 偏晚(Late)。见 ScoreManager.RecordOffset。
float medianMs = sm.GetMedianOffsetMs();
float currentDelay = PlayerPrefs.GetFloat("UserGlobalDelaySeconds", 0f);
// 偏早 -> 音符应更早到达判定线 -> 减小延迟;偏晚 -> 增大延迟。
float suggestedDelay = currentDelay - medianMs / 1000f;
// 小于阈值视为已对齐,避免噪声导致反复微调。
const float alignedThresholdMs = 8f;
if (Mathf.Abs(medianMs) < alignedThresholdMs)
{
offsetSuggestion_Text.text = $"中位偏移 {medianMs:F1} ms · 时机良好";
return;
}
string dir = medianMs > 0f ? "偏早" : "偏晚";
offsetSuggestion_Text.text =
$"中位偏移 {medianMs:F1} ms{dir})· 建议全局延迟 {currentDelay * 1000f:F0} → {suggestedDelay * 1000f:F0} ms";
}
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, ScaledDuration(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, ScaledDuration(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, ScaledDuration(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, ScaledDuration(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, ScaledDuration(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);
// 注意:这里的 step 传给 WaitRealtime,由其内部统一按 introTimeScale 缩放,
// 故此处用未缩放时长,避免二次缩放(否则会被缩放两次 → duration/scale²)。
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;
}
// 结算动画整体提速:把任意时长按 introTimeScale 等比缩短。集中在此,避免逐个调用点改动。
private float ScaledDuration(float duration)
{
float scale = introTimeScale > 0.01f ? introTimeScale : 1f;
return duration / scale;
}
private IEnumerator WaitRealtime(float duration)
{
float elapsed = 0f;
float dur = Mathf.Max(0f, ScaledDuration(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<Transform> CollectDirectChildren(Transform root)
{
List<Transform> list = new List<Transform>();
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<Transform>(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<CanvasGroup>();
if (cg == null) cg = root.gameObject.AddComponent<CanvasGroup>();
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);
}
/// <summary>
/// Documentation text normalized.
/// </summary>
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;
}
}
/// <summary>
/// Documentation text normalized.
/// </summary>
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;
}
}
/// <summary>
/// Documentation text normalized.
/// Documentation text normalized.
/// </summary>
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<AllyCombatant>();
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");
}
}
}