2537 lines
94 KiB
C#
2537 lines
94 KiB
C#
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");
|
||
}
|
||
}
|
||
}
|