修了很多bug和增加功能,优化不少问题
This commit is contained in:
@@ -283,12 +283,17 @@ public class GameManager : MonoBehaviour
|
||||
|
||||
// --- Initialize Statistics ---
|
||||
currentSong = BeatmapManager.pendingSongData ?? SongDataHolder.SelectedSongData;
|
||||
if (currentSong != null)
|
||||
if (currentSong != null && !GameConfig.autoPlayEnabled)
|
||||
{
|
||||
currentSong.game_enterTimes++;
|
||||
sessionStartTime = Time.realtimeSinceStartup;
|
||||
Debug.Log($"[GameManager] {currentSong.songName} launch count incremented to {currentSong.game_enterTimes}");
|
||||
}
|
||||
else if (currentSong != null && GameConfig.autoPlayEnabled)
|
||||
{
|
||||
sessionStartTime = Time.realtimeSinceStartup;
|
||||
Debug.Log($"[GameManager] {currentSong.songName} autoplay enabled, skipping launch count increment");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("[GameManager] No SongData found for statistics tracking");
|
||||
@@ -1166,7 +1171,7 @@ public class GameManager : MonoBehaviour
|
||||
/// </summary>
|
||||
public void RecordTotalPlayTime()
|
||||
{
|
||||
if (timeRecorded || currentSong == null) return;
|
||||
if (timeRecorded || currentSong == null || GameConfig.autoPlayEnabled) return;
|
||||
|
||||
float elapsed = Time.realtimeSinceStartup - sessionStartTime;
|
||||
currentSong.time_totalPlayingTime += elapsed;
|
||||
|
||||
@@ -0,0 +1,969 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEngine.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Gameplay start-time track entrance animation controller.
|
||||
/// Triggered by readyLetsGo when 3-2-1-Go starts.
|
||||
/// Uses unscaled time so it can run while Time.timeScale == 0.
|
||||
/// </summary>
|
||||
public class GameplayTrackStartIntroAnimator : MonoBehaviour
|
||||
{
|
||||
[Header("Scope")]
|
||||
[SerializeField] private string requiredSceneName = "gamePlay_gamePlay";
|
||||
[SerializeField] private bool playOnlyOncePerScene = true;
|
||||
|
||||
[Header("Timing")]
|
||||
[SerializeField] private float stepStagger = 0.06f;
|
||||
[SerializeField] private float moveDuration = 0.30f;
|
||||
[SerializeField] private float flashDuration = 0.26f;
|
||||
[SerializeField] private int flashTimes = 2;
|
||||
[SerializeField] private float phaseGap = 0.06f;
|
||||
[SerializeField] private float laneRectFlashDuration = 0.18f;
|
||||
[SerializeField] private float allyFlashDuration = 0.16f;
|
||||
[SerializeField] private float hudFlashDuration = 0.14f;
|
||||
[SerializeField] private bool keepHiddenBeforeStart = true;
|
||||
|
||||
[Header("Preset Values")]
|
||||
[SerializeField] private float keyDownStartY = -30f;
|
||||
[SerializeField] private float keyDownTargetY = 5.3f;
|
||||
[SerializeField] private float keyDownStartRotY = -30f;
|
||||
[SerializeField] private float keyDownTargetRotY = 0f;
|
||||
[SerializeField] private float gameObjectStartY = -10f;
|
||||
[SerializeField] private float gameObjectTargetY = -4.523f;
|
||||
[SerializeField] private float allyStartRotY = 130.8f;
|
||||
[SerializeField] private float allyTargetRotY = 0f;
|
||||
|
||||
[Header("Ally Turn")]
|
||||
[Tooltip("Use pseudo Y-turn on UI cards to avoid color/mask artifacts caused by true Y rotation on complex UI roots.")]
|
||||
[SerializeField] private bool allyUsePseudoYTurnForUI = true;
|
||||
[SerializeField] private float allyPseudoStartScaleX = 0.68f;
|
||||
|
||||
private bool playedOnce;
|
||||
private bool refsCached;
|
||||
private bool preStartPrepared;
|
||||
private Coroutine playRoutine;
|
||||
|
||||
private Transform keyDown;
|
||||
private Transform colorLineStatic;
|
||||
private Transform enemyRoot;
|
||||
private Transform progressLine;
|
||||
private Transform score;
|
||||
private Transform pause;
|
||||
private Transform bgReplace;
|
||||
private Transform totalEnemyTop;
|
||||
private Transform totalEnemyFade;
|
||||
private Transform enemyImg;
|
||||
private Transform enemySpineSystem;
|
||||
private Transform enemyListBall;
|
||||
private Transform enemyStatics;
|
||||
|
||||
private readonly List<Transform> keyDownRects = new List<Transform>();
|
||||
private readonly List<Transform> gameObjectRects = new List<Transform>();
|
||||
private readonly List<Transform> colorRects = new List<Transform>();
|
||||
private readonly List<Transform> allies = new List<Transform>();
|
||||
|
||||
private readonly Dictionary<Graphic, float> baseGraphicAlpha = new Dictionary<Graphic, float>();
|
||||
private readonly Dictionary<SpriteRenderer, float> baseSpriteAlpha = new Dictionary<SpriteRenderer, float>();
|
||||
private readonly Dictionary<CanvasGroup, float> baseCanvasGroupAlpha = new Dictionary<CanvasGroup, float>();
|
||||
private readonly Dictionary<Transform, Vector3> baseLocalScale = new Dictionary<Transform, Vector3>();
|
||||
private readonly Dictionary<Transform, CanvasGroup> rootCanvasGroups = new Dictionary<Transform, CanvasGroup>();
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (!IsInRequiredScene()) return;
|
||||
CacheRefsIfNeeded();
|
||||
PreparePreStartState();
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (!IsInRequiredScene()) return;
|
||||
StartCoroutine(PreparePreStartStateNextFrame());
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
if (!keepHiddenBeforeStart) return;
|
||||
if (!IsInRequiredScene()) return;
|
||||
if (playedOnce || playRoutine != null) return;
|
||||
|
||||
CacheRefsIfNeeded();
|
||||
// Keep hidden state stable before player presses start.
|
||||
PreparePreStartState(force: true);
|
||||
}
|
||||
|
||||
public void PlayIfNeeded()
|
||||
{
|
||||
if (!IsInRequiredScene()) return;
|
||||
if (playRoutine != null) return;
|
||||
if (playOnlyOncePerScene && playedOnce) return;
|
||||
|
||||
CacheRefsIfNeeded();
|
||||
playRoutine = StartCoroutine(PlayRoutine());
|
||||
}
|
||||
|
||||
private IEnumerator PreparePreStartStateNextFrame()
|
||||
{
|
||||
// Re-apply after one frame so late UI initializers don't bring these elements visible before start.
|
||||
yield return null;
|
||||
if (playRoutine == null && !playedOnce)
|
||||
{
|
||||
PreparePreStartState(force: true);
|
||||
}
|
||||
}
|
||||
|
||||
public void ResetPlayState()
|
||||
{
|
||||
playedOnce = false;
|
||||
if (playRoutine != null)
|
||||
{
|
||||
StopCoroutine(playRoutine);
|
||||
playRoutine = null;
|
||||
}
|
||||
|
||||
CacheRefsIfNeeded();
|
||||
PreparePreStartState(force: true);
|
||||
}
|
||||
|
||||
private bool IsInRequiredScene()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(requiredSceneName)) return true;
|
||||
Scene active = SceneManager.GetActiveScene();
|
||||
return string.Equals(active.name, requiredSceneName, System.StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private void CacheRefsIfNeeded()
|
||||
{
|
||||
if (refsCached) return;
|
||||
|
||||
keyDown = FindSceneTransformByName("KEY_down");
|
||||
colorLineStatic = FindSceneTransformByName("colorLineStatic");
|
||||
enemyRoot = FindSceneTransformByName("enemyObject_toWiggle");
|
||||
progressLine = FindSceneTransformByName("progressLine");
|
||||
score = FindSceneTransformByName("score");
|
||||
pause = FindPauseInScene();
|
||||
bgReplace = FindSceneTransformByName("bg_replace");
|
||||
totalEnemyTop = FindSceneTransformByName("TOTALenemy_healthbar_top");
|
||||
totalEnemyFade = FindSceneTransformByName("TOTALenemy_healthbar_fade");
|
||||
enemyImg = FindSceneTransformByNameUnder("img", enemyRoot);
|
||||
enemySpineSystem = FindSceneTransformByNameUnder("spineSystem", enemyImg != null ? enemyImg : enemyRoot);
|
||||
enemyListBall = FindSceneTransformByNameUnder("enemyListBall", enemyRoot);
|
||||
enemyStatics = FindSceneTransformByNameUnder("statics", enemyRoot);
|
||||
|
||||
keyDownRects.Clear();
|
||||
if (keyDown != null)
|
||||
keyDownRects.AddRange(CollectDescendantsByNameContains(keyDown, "sdw 3"));
|
||||
keyDownRects.Sort((a, b) => a.localPosition.x.CompareTo(b.localPosition.x));
|
||||
FilterLaneRects(keyDownRects, keyDownTargetY, 5);
|
||||
|
||||
gameObjectRects.Clear();
|
||||
List<Transform> byNamedRoots = CollectSdw3UnderNamedRoots("GameObject", keyDown);
|
||||
if (byNamedRoots.Count > 0)
|
||||
gameObjectRects.AddRange(byNamedRoots);
|
||||
else
|
||||
gameObjectRects.AddRange(CollectGameObjectSdw3Excluding(keyDown));
|
||||
gameObjectRects.Sort((a, b) => a.localPosition.x.CompareTo(b.localPosition.x));
|
||||
FilterLaneRects(gameObjectRects, gameObjectTargetY, 5);
|
||||
|
||||
colorRects.Clear();
|
||||
colorRects.AddRange(CollectColorRects(colorLineStatic));
|
||||
colorRects.Sort((a, b) => a.GetSiblingIndex().CompareTo(b.GetSiblingIndex()));
|
||||
|
||||
allies.Clear();
|
||||
for (int i = 1; i <= 5; i++)
|
||||
{
|
||||
Transform ally = FindSceneTransformByName($"ally_0{i}");
|
||||
if (ally != null) allies.Add(ally);
|
||||
}
|
||||
allies.Sort((a, b) => a.name.CompareTo(b.name));
|
||||
|
||||
refsCached = true;
|
||||
}
|
||||
|
||||
private void PreparePreStartState(bool force = false)
|
||||
{
|
||||
if (preStartPrepared && !force) return;
|
||||
|
||||
if (bgReplace != null) SetAlphaNormalized(bgReplace, 0f, true);
|
||||
if (colorLineStatic != null) SetAlphaNormalized(colorLineStatic, 0f, true);
|
||||
if (enemyRoot != null) SetAlphaNormalized(enemyRoot, 0f, true);
|
||||
if (enemyListBall != null) SetAlphaNormalized(enemyListBall, 0f, true);
|
||||
if (enemyStatics != null) SetAlphaNormalized(enemyStatics, 0f, true);
|
||||
if (enemySpineSystem != null) enemySpineSystem.gameObject.SetActive(false);
|
||||
if (progressLine != null) SetAlphaNormalized(progressLine, 0f, true);
|
||||
if (score != null) SetAlphaNormalized(score, 0f, true);
|
||||
if (pause != null) SetAlphaNormalized(pause, 0f, true);
|
||||
|
||||
for (int i = 0; i < colorRects.Count; i++)
|
||||
{
|
||||
if (colorRects[i] == null) continue;
|
||||
SetAlphaNormalized(colorRects[i], 0f, true);
|
||||
}
|
||||
|
||||
for (int i = 0; i < keyDownRects.Count; i++)
|
||||
{
|
||||
Transform t = keyDownRects[i];
|
||||
if (t == null) continue;
|
||||
SetAlphaNormalized(t, 0f, true);
|
||||
SetLocalY(t, keyDownStartY);
|
||||
SetLocalRotY(t, keyDownStartRotY);
|
||||
}
|
||||
|
||||
for (int i = 0; i < gameObjectRects.Count; i++)
|
||||
{
|
||||
Transform t = gameObjectRects[i];
|
||||
if (t == null) continue;
|
||||
SetAlphaNormalized(t, 0f, true);
|
||||
SetLocalY(t, gameObjectStartY);
|
||||
}
|
||||
|
||||
for (int i = 0; i < allies.Count; i++)
|
||||
{
|
||||
Transform ally = allies[i];
|
||||
if (ally == null) continue;
|
||||
SetRootCanvasAlpha(ally, 0f);
|
||||
if (allyUsePseudoYTurnForUI)
|
||||
{
|
||||
SetPseudoTurnScaleX(ally, allyPseudoStartScaleX);
|
||||
SetLocalRotY(ally, allyTargetRotY);
|
||||
}
|
||||
else
|
||||
{
|
||||
SetLocalRotY(ally, allyStartRotY);
|
||||
}
|
||||
}
|
||||
|
||||
if (totalEnemyTop != null) SetScaleY(totalEnemyTop, 0f);
|
||||
if (totalEnemyFade != null) SetScaleY(totalEnemyFade, 0f);
|
||||
|
||||
preStartPrepared = true;
|
||||
}
|
||||
|
||||
private IEnumerator PlayRoutine()
|
||||
{
|
||||
PreparePreStartState();
|
||||
|
||||
// 1) bg_replace flash in, then KEY_down sdw3 move/rotate in sequence
|
||||
if (bgReplace != null)
|
||||
{
|
||||
yield return StartCoroutine(FlashIn(bgReplace, flashTimes, flashDuration, true));
|
||||
}
|
||||
|
||||
yield return StartCoroutine(AnimateRectsYAndRotY(
|
||||
keyDownRects,
|
||||
keyDownStartY,
|
||||
keyDownTargetY,
|
||||
keyDownStartRotY,
|
||||
keyDownTargetRotY,
|
||||
moveDuration,
|
||||
stepStagger));
|
||||
|
||||
yield return WaitRealtime(phaseGap);
|
||||
|
||||
// 2) GameObject sdw3 move in sequence
|
||||
yield return StartCoroutine(AnimateRectsYOnlyWithFlash(
|
||||
gameObjectRects,
|
||||
gameObjectStartY,
|
||||
gameObjectTargetY,
|
||||
moveDuration,
|
||||
stepStagger,
|
||||
laneRectFlashDuration));
|
||||
|
||||
// 3) colorLineStatic enters
|
||||
// 4) enemy enters (parallel)
|
||||
// 5) progressLine enters (parallel)
|
||||
// 6) allies then score/pause (parallel)
|
||||
Coroutine colorStep = StartCoroutine(FlashInSequence(colorRects, flashTimes, flashDuration, stepStagger));
|
||||
Coroutine enemyStep = StartCoroutine(PlayEnemyStep(enemyRoot, enemyListBall, enemyStatics, enemySpineSystem, totalEnemyTop, totalEnemyFade));
|
||||
Coroutine progressStep = StartCoroutine(FlashIn(progressLine, flashTimes, flashDuration, true));
|
||||
Coroutine alliesStep = StartCoroutine(PlayAlliesAndHudStep(allies, score, pause));
|
||||
|
||||
if (colorStep != null) yield return colorStep;
|
||||
if (enemyStep != null) yield return enemyStep;
|
||||
if (progressStep != null) yield return progressStep;
|
||||
if (alliesStep != null) yield return alliesStep;
|
||||
|
||||
playedOnce = true;
|
||||
playRoutine = null;
|
||||
}
|
||||
|
||||
private IEnumerator PlayEnemyStep(
|
||||
Transform enemyRootTr,
|
||||
Transform enemyListBallTr,
|
||||
Transform enemyStaticsTr,
|
||||
Transform enemySpineSystemTr,
|
||||
Transform totalEnemyTopTr,
|
||||
Transform totalEnemyFadeTr)
|
||||
{
|
||||
if (enemyRootTr != null)
|
||||
yield return StartCoroutine(FlashIn(enemyRootTr, flashTimes, flashDuration, true));
|
||||
|
||||
if (enemyListBallTr != null)
|
||||
yield return StartCoroutine(FlashIn(enemyListBallTr, flashTimes, flashDuration, true));
|
||||
|
||||
if (enemyStaticsTr != null)
|
||||
yield return StartCoroutine(FlashIn(enemyStaticsTr, flashTimes, flashDuration, true));
|
||||
|
||||
if (enemySpineSystemTr != null)
|
||||
yield return StartCoroutine(FlashInByActiveToggle(enemySpineSystemTr, flashTimes, flashDuration));
|
||||
|
||||
if (totalEnemyTopTr != null) SetScaleY(totalEnemyTopTr, 0f);
|
||||
if (totalEnemyFadeTr != null) SetScaleY(totalEnemyFadeTr, 0f);
|
||||
|
||||
float elapsed = 0f;
|
||||
float dur = Mathf.Max(0.01f, moveDuration);
|
||||
while (elapsed < dur)
|
||||
{
|
||||
elapsed += Time.unscaledDeltaTime;
|
||||
float t = EaseOutCubic(Mathf.Clamp01(elapsed / dur));
|
||||
if (totalEnemyTopTr != null) SetScaleY(totalEnemyTopTr, Mathf.LerpUnclamped(0f, 1f, t));
|
||||
if (totalEnemyFadeTr != null) SetScaleY(totalEnemyFadeTr, Mathf.LerpUnclamped(0f, 1f, t));
|
||||
yield return null;
|
||||
}
|
||||
|
||||
if (totalEnemyTopTr != null) SetScaleY(totalEnemyTopTr, 1f);
|
||||
if (totalEnemyFadeTr != null) SetScaleY(totalEnemyFadeTr, 1f);
|
||||
}
|
||||
|
||||
private IEnumerator PlayAlliesAndHudStep(List<Transform> allyList, Transform scoreTr, Transform pauseTr)
|
||||
{
|
||||
for (int i = 0; i < allyList.Count; i++)
|
||||
{
|
||||
Transform ally = allyList[i];
|
||||
if (ally == null) continue;
|
||||
|
||||
yield return StartCoroutine(FlashInCanvasGroup(ally, flashTimes, allyFlashDuration));
|
||||
|
||||
if (allyUsePseudoYTurnForUI)
|
||||
yield return StartCoroutine(AnimatePseudoTurnScaleX(ally, allyPseudoStartScaleX, 1f, moveDuration));
|
||||
else
|
||||
yield return StartCoroutine(AnimateRotY(ally, allyStartRotY, allyTargetRotY, moveDuration));
|
||||
|
||||
if (i < allyList.Count - 1 && stepStagger > 0f)
|
||||
yield return WaitRealtime(stepStagger);
|
||||
}
|
||||
|
||||
if (scoreTr != null)
|
||||
yield return StartCoroutine(FlashIn(scoreTr, flashTimes, hudFlashDuration, true));
|
||||
|
||||
if (pauseTr != null)
|
||||
yield return StartCoroutine(FlashIn(pauseTr, flashTimes, hudFlashDuration, true));
|
||||
}
|
||||
|
||||
private IEnumerator FlashInSequence(List<Transform> list, int flashes, float duration, float stagger)
|
||||
{
|
||||
if (list == null || list.Count == 0) yield break;
|
||||
for (int i = 0; i < list.Count; i++)
|
||||
{
|
||||
Transform t = list[i];
|
||||
if (t == null) continue;
|
||||
yield return StartCoroutine(FlashIn(t, flashes, duration, true));
|
||||
if (i < list.Count - 1 && stagger > 0f)
|
||||
yield return WaitRealtime(stagger);
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator AnimateRectsYAndRotY(List<Transform> list, float fromY, float toY, float fromRotY, float toRotY, float duration, float stagger)
|
||||
{
|
||||
if (list == null || list.Count == 0) yield break;
|
||||
|
||||
for (int i = 0; i < list.Count; i++)
|
||||
{
|
||||
Transform t = list[i];
|
||||
if (t == null) continue;
|
||||
|
||||
SetAlphaNormalized(t, 1f, true);
|
||||
SetLocalY(t, fromY);
|
||||
SetLocalRotY(t, fromRotY);
|
||||
|
||||
StartCoroutine(AnimateYAndRotY(t, fromY, toY, fromRotY, toRotY, duration));
|
||||
if (i < list.Count - 1 && stagger > 0f)
|
||||
yield return WaitRealtime(stagger);
|
||||
}
|
||||
|
||||
yield return WaitRealtime(duration);
|
||||
}
|
||||
|
||||
private IEnumerator AnimateRectsYOnly(List<Transform> list, float fromY, float toY, float duration, float stagger)
|
||||
{
|
||||
if (list == null || list.Count == 0) yield break;
|
||||
|
||||
for (int i = 0; i < list.Count; i++)
|
||||
{
|
||||
Transform t = list[i];
|
||||
if (t == null) continue;
|
||||
|
||||
SetAlphaNormalized(t, 1f, true);
|
||||
SetLocalY(t, fromY);
|
||||
StartCoroutine(AnimateYOnly(t, fromY, toY, duration));
|
||||
if (i < list.Count - 1 && stagger > 0f)
|
||||
yield return WaitRealtime(stagger);
|
||||
}
|
||||
|
||||
yield return WaitRealtime(duration);
|
||||
}
|
||||
|
||||
private IEnumerator AnimateRectsYOnlyWithFlash(List<Transform> list, float fromY, float toY, float duration, float stagger, float flashDur)
|
||||
{
|
||||
if (list == null || list.Count == 0) yield break;
|
||||
|
||||
for (int i = 0; i < list.Count; i++)
|
||||
{
|
||||
Transform t = list[i];
|
||||
if (t == null) continue;
|
||||
|
||||
SetAlphaNormalized(t, 0f, true);
|
||||
SetLocalY(t, fromY);
|
||||
StartCoroutine(FlashIn(t, flashTimes, flashDur, true));
|
||||
StartCoroutine(AnimateYOnly(t, fromY, toY, duration));
|
||||
if (i < list.Count - 1 && stagger > 0f)
|
||||
yield return WaitRealtime(stagger);
|
||||
}
|
||||
|
||||
yield return WaitRealtime(Mathf.Max(duration, flashDur));
|
||||
}
|
||||
|
||||
private IEnumerator AnimateYAndRotY(Transform t, float fromY, float toY, float fromRotY, float toRotY, float duration)
|
||||
{
|
||||
if (t == null) yield break;
|
||||
float elapsed = 0f;
|
||||
float dur = Mathf.Max(0.01f, duration);
|
||||
float fromRot = NormalizeEulerY(fromRotY);
|
||||
float toRot = NormalizeEulerY(toRotY);
|
||||
|
||||
while (elapsed < dur)
|
||||
{
|
||||
elapsed += Time.unscaledDeltaTime;
|
||||
float k = EaseOutCubic(Mathf.Clamp01(elapsed / dur));
|
||||
SetLocalY(t, Mathf.LerpUnclamped(fromY, toY, k));
|
||||
SetLocalRotY(t, Mathf.LerpAngle(fromRot, toRot, k));
|
||||
yield return null;
|
||||
}
|
||||
|
||||
SetLocalY(t, toY);
|
||||
SetLocalRotY(t, toRotY);
|
||||
}
|
||||
|
||||
private IEnumerator AnimateYOnly(Transform t, float fromY, float toY, float duration)
|
||||
{
|
||||
if (t == null) yield break;
|
||||
float elapsed = 0f;
|
||||
float dur = Mathf.Max(0.01f, duration);
|
||||
while (elapsed < dur)
|
||||
{
|
||||
elapsed += Time.unscaledDeltaTime;
|
||||
float k = EaseOutCubic(Mathf.Clamp01(elapsed / dur));
|
||||
SetLocalY(t, Mathf.LerpUnclamped(fromY, toY, k));
|
||||
yield return null;
|
||||
}
|
||||
|
||||
SetLocalY(t, toY);
|
||||
}
|
||||
|
||||
private IEnumerator AnimateRotY(Transform t, float fromY, float toY, float duration)
|
||||
{
|
||||
if (t == null) yield break;
|
||||
float elapsed = 0f;
|
||||
float dur = Mathf.Max(0.01f, duration);
|
||||
float from = NormalizeEulerY(fromY);
|
||||
float to = NormalizeEulerY(toY);
|
||||
|
||||
SetLocalRotY(t, from);
|
||||
|
||||
while (elapsed < dur)
|
||||
{
|
||||
elapsed += Time.unscaledDeltaTime;
|
||||
float k = EaseOutCubic(Mathf.Clamp01(elapsed / dur));
|
||||
SetLocalRotY(t, Mathf.LerpAngle(from, to, k));
|
||||
yield return null;
|
||||
}
|
||||
|
||||
SetLocalRotY(t, to);
|
||||
}
|
||||
|
||||
private IEnumerator AnimatePseudoTurnScaleX(Transform t, float fromScaleX, float toScaleX, float duration)
|
||||
{
|
||||
if (t == null) yield break;
|
||||
|
||||
float elapsed = 0f;
|
||||
float dur = Mathf.Max(0.01f, duration);
|
||||
SetPseudoTurnScaleX(t, fromScaleX);
|
||||
|
||||
while (elapsed < dur)
|
||||
{
|
||||
elapsed += Time.unscaledDeltaTime;
|
||||
float k = EaseOutCubic(Mathf.Clamp01(elapsed / dur));
|
||||
float sx = Mathf.LerpUnclamped(fromScaleX, toScaleX, k);
|
||||
SetPseudoTurnScaleX(t, sx);
|
||||
yield return null;
|
||||
}
|
||||
|
||||
SetPseudoTurnScaleX(t, toScaleX);
|
||||
}
|
||||
|
||||
private IEnumerator FlashIn(Transform target, int flashes, float duration, bool includeChildren)
|
||||
{
|
||||
if (target == null) yield break;
|
||||
|
||||
int count = Mathf.Max(1, flashes);
|
||||
float total = Mathf.Max(0.04f, duration);
|
||||
float unit = total / (count * 2f);
|
||||
|
||||
SetAlphaNormalized(target, 0f, includeChildren);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
SetAlphaNormalized(target, 1f, includeChildren);
|
||||
yield return WaitRealtime(unit);
|
||||
|
||||
if (i < count - 1)
|
||||
{
|
||||
SetAlphaNormalized(target, 0f, includeChildren);
|
||||
yield return WaitRealtime(unit);
|
||||
}
|
||||
}
|
||||
|
||||
SetAlphaNormalized(target, 1f, includeChildren);
|
||||
}
|
||||
|
||||
private IEnumerator FlashInByActiveToggle(Transform target, int flashes, float duration)
|
||||
{
|
||||
if (target == null) yield break;
|
||||
int count = Mathf.Max(1, flashes);
|
||||
float total = Mathf.Max(0.04f, duration);
|
||||
float unit = total / (count * 2f);
|
||||
|
||||
if (target.gameObject.activeSelf)
|
||||
target.gameObject.SetActive(false);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
target.gameObject.SetActive(true);
|
||||
yield return WaitRealtime(unit);
|
||||
|
||||
if (i < count - 1)
|
||||
{
|
||||
target.gameObject.SetActive(false);
|
||||
yield return WaitRealtime(unit);
|
||||
}
|
||||
}
|
||||
|
||||
target.gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
private static IEnumerator WaitRealtime(float duration)
|
||||
{
|
||||
float elapsed = 0f;
|
||||
while (elapsed < duration)
|
||||
{
|
||||
elapsed += Time.unscaledDeltaTime;
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static float EaseOutCubic(float t)
|
||||
{
|
||||
float inv = 1f - t;
|
||||
return 1f - inv * inv * inv;
|
||||
}
|
||||
|
||||
private static float NormalizeEulerY(float y)
|
||||
{
|
||||
float v = y % 360f;
|
||||
if (v < 0f) v += 360f;
|
||||
return v;
|
||||
}
|
||||
|
||||
private void SetPseudoTurnScaleX(Transform t, float scaleX)
|
||||
{
|
||||
if (t == null) return;
|
||||
Vector3 baseScale = GetBaseLocalScale(t);
|
||||
Vector3 s = t.localScale;
|
||||
s.x = Mathf.Max(0.01f, baseScale.x * scaleX);
|
||||
s.y = baseScale.y;
|
||||
s.z = baseScale.z;
|
||||
t.localScale = s;
|
||||
}
|
||||
|
||||
private Vector3 GetBaseLocalScale(Transform t)
|
||||
{
|
||||
if (t == null) return Vector3.one;
|
||||
if (!baseLocalScale.TryGetValue(t, out Vector3 s))
|
||||
{
|
||||
s = t.localScale;
|
||||
baseLocalScale[t] = s;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
private static void SetScaleY(Transform t, float y)
|
||||
{
|
||||
if (t == null) return;
|
||||
Vector3 s = t.localScale;
|
||||
s.y = y;
|
||||
t.localScale = s;
|
||||
}
|
||||
|
||||
private static void SetLocalY(Transform t, float y)
|
||||
{
|
||||
if (t == null) return;
|
||||
Vector3 p = t.localPosition;
|
||||
p.y = y;
|
||||
t.localPosition = p;
|
||||
}
|
||||
|
||||
private static void SetLocalRotY(Transform t, float y)
|
||||
{
|
||||
if (t == null) return;
|
||||
Vector3 e = t.localEulerAngles;
|
||||
e.y = NormalizeEulerY(y);
|
||||
t.localEulerAngles = e;
|
||||
}
|
||||
|
||||
private void SetAlphaNormalized(Transform root, float normalized, bool includeChildren)
|
||||
{
|
||||
if (root == null) return;
|
||||
normalized = Mathf.Clamp01(normalized);
|
||||
|
||||
if (includeChildren)
|
||||
{
|
||||
CanvasGroup[] cgs = root.GetComponentsInChildren<CanvasGroup>(true);
|
||||
for (int i = 0; i < cgs.Length; i++)
|
||||
{
|
||||
CanvasGroup cg = cgs[i];
|
||||
if (cg == null) continue;
|
||||
float baseA = GetBaseAlpha(cg);
|
||||
cg.alpha = baseA * normalized;
|
||||
}
|
||||
|
||||
Graphic[] graphics = root.GetComponentsInChildren<Graphic>(true);
|
||||
for (int i = 0; i < graphics.Length; i++)
|
||||
{
|
||||
Graphic g = graphics[i];
|
||||
if (g == null) continue;
|
||||
float baseA = GetBaseAlpha(g);
|
||||
Color c = g.color;
|
||||
c.a = baseA * normalized;
|
||||
g.color = c;
|
||||
}
|
||||
|
||||
SpriteRenderer[] sprites = root.GetComponentsInChildren<SpriteRenderer>(true);
|
||||
for (int i = 0; i < sprites.Length; i++)
|
||||
{
|
||||
SpriteRenderer sr = sprites[i];
|
||||
if (sr == null) continue;
|
||||
float baseA = GetBaseAlpha(sr);
|
||||
Color c = sr.color;
|
||||
c.a = baseA * normalized;
|
||||
sr.color = c;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
CanvasGroup cg = root.GetComponent<CanvasGroup>();
|
||||
if (cg != null)
|
||||
{
|
||||
float baseA = GetBaseAlpha(cg);
|
||||
cg.alpha = baseA * normalized;
|
||||
}
|
||||
|
||||
Graphic g = root.GetComponent<Graphic>();
|
||||
if (g != null)
|
||||
{
|
||||
float baseA = GetBaseAlpha(g);
|
||||
Color c = g.color;
|
||||
c.a = baseA * normalized;
|
||||
g.color = c;
|
||||
}
|
||||
|
||||
SpriteRenderer sr = root.GetComponent<SpriteRenderer>();
|
||||
if (sr != null)
|
||||
{
|
||||
float baseA = GetBaseAlpha(sr);
|
||||
Color c = sr.color;
|
||||
c.a = baseA * normalized;
|
||||
sr.color = c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private float GetBaseAlpha(CanvasGroup cg)
|
||||
{
|
||||
if (!baseCanvasGroupAlpha.TryGetValue(cg, out float a))
|
||||
{
|
||||
a = cg.alpha;
|
||||
if (a <= 0.001f) a = 1f;
|
||||
baseCanvasGroupAlpha[cg] = a;
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
private CanvasGroup GetOrAddRootCanvasGroup(Transform root)
|
||||
{
|
||||
if (root == null) return null;
|
||||
if (!rootCanvasGroups.TryGetValue(root, out CanvasGroup cg) || cg == null)
|
||||
{
|
||||
cg = root.GetComponent<CanvasGroup>();
|
||||
if (cg == null) cg = root.gameObject.AddComponent<CanvasGroup>();
|
||||
rootCanvasGroups[root] = cg;
|
||||
}
|
||||
return cg;
|
||||
}
|
||||
|
||||
private void SetRootCanvasAlpha(Transform root, float normalized)
|
||||
{
|
||||
if (root == null) return;
|
||||
CanvasGroup cg = GetOrAddRootCanvasGroup(root);
|
||||
if (cg == null) return;
|
||||
float n = Mathf.Clamp01(normalized);
|
||||
// Use direct alpha for root group to avoid touching child graphic colors.
|
||||
cg.alpha = n;
|
||||
}
|
||||
|
||||
private IEnumerator FlashInCanvasGroup(Transform root, int flashes, float duration)
|
||||
{
|
||||
if (root == null) yield break;
|
||||
int count = Mathf.Max(1, flashes);
|
||||
float total = Mathf.Max(0.04f, duration);
|
||||
float unit = total / (count * 2f);
|
||||
|
||||
SetRootCanvasAlpha(root, 0f);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
SetRootCanvasAlpha(root, 1f);
|
||||
yield return WaitRealtime(unit);
|
||||
if (i < count - 1)
|
||||
{
|
||||
SetRootCanvasAlpha(root, 0f);
|
||||
yield return WaitRealtime(unit);
|
||||
}
|
||||
}
|
||||
SetRootCanvasAlpha(root, 1f);
|
||||
}
|
||||
|
||||
private float GetBaseAlpha(Graphic g)
|
||||
{
|
||||
if (!baseGraphicAlpha.TryGetValue(g, out float a))
|
||||
{
|
||||
a = g.color.a;
|
||||
if (a <= 0.001f) a = 1f;
|
||||
baseGraphicAlpha[g] = a;
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
private float GetBaseAlpha(SpriteRenderer sr)
|
||||
{
|
||||
if (!baseSpriteAlpha.TryGetValue(sr, out float a))
|
||||
{
|
||||
a = sr.color.a;
|
||||
if (a <= 0.001f) a = 1f;
|
||||
baseSpriteAlpha[sr] = a;
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
private List<Transform> CollectColorRects(Transform colorLineStaticRoot)
|
||||
{
|
||||
List<Transform> list = new List<Transform>();
|
||||
if (colorLineStaticRoot == null) return list;
|
||||
|
||||
List<Transform> byEnglish = CollectDescendantsByNameContains(colorLineStaticRoot, "Rectangle 6");
|
||||
if (byEnglish.Count > 0) return byEnglish;
|
||||
|
||||
for (int i = 0; i < colorLineStaticRoot.childCount; i++)
|
||||
{
|
||||
Transform child = colorLineStaticRoot.GetChild(i);
|
||||
if (child == null) continue;
|
||||
if (child.GetComponentInChildren<Graphic>(true) != null || child.GetComponentInChildren<SpriteRenderer>(true) != null)
|
||||
{
|
||||
list.Add(child);
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private List<Transform> CollectGameObjectSdw3Excluding(Transform keyDownRoot)
|
||||
{
|
||||
List<Transform> list = new List<Transform>();
|
||||
HashSet<int> seen = new HashSet<int>();
|
||||
Transform[] all = GetAllSceneTransforms();
|
||||
for (int i = 0; i < all.Length; i++)
|
||||
{
|
||||
Transform t = all[i];
|
||||
if (t == null) continue;
|
||||
if (t.name == null || t.name.IndexOf("sdw 3", System.StringComparison.OrdinalIgnoreCase) < 0) continue;
|
||||
if (keyDownRoot != null && t.IsChildOf(keyDownRoot)) continue;
|
||||
if (!seen.Add(t.GetInstanceID())) continue;
|
||||
list.Add(t);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private List<Transform> CollectSdw3UnderNamedRoots(string rootName, Transform excludeRoot)
|
||||
{
|
||||
List<Transform> list = new List<Transform>();
|
||||
if (string.IsNullOrEmpty(rootName)) return list;
|
||||
|
||||
HashSet<int> seen = new HashSet<int>();
|
||||
Transform[] all = GetAllSceneTransforms();
|
||||
for (int i = 0; i < all.Length; i++)
|
||||
{
|
||||
Transform root = all[i];
|
||||
if (root == null) continue;
|
||||
if (!string.Equals(root.name, rootName, System.StringComparison.OrdinalIgnoreCase)) continue;
|
||||
if (excludeRoot != null && root.IsChildOf(excludeRoot)) continue;
|
||||
|
||||
List<Transform> descendants = CollectDescendantsByNameContains(root, "sdw 3");
|
||||
for (int j = 0; j < descendants.Count; j++)
|
||||
{
|
||||
Transform t = descendants[j];
|
||||
if (t == null) continue;
|
||||
if (!seen.Add(t.GetInstanceID())) continue;
|
||||
list.Add(t);
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private static void FilterLaneRects(List<Transform> list, float targetY, int expectedCount)
|
||||
{
|
||||
if (list == null) return;
|
||||
list.RemoveAll(t => t == null);
|
||||
if (list.Count == 0) return;
|
||||
|
||||
const float yTolerance = 2.5f;
|
||||
List<Transform> nearTarget = new List<Transform>();
|
||||
for (int i = 0; i < list.Count; i++)
|
||||
{
|
||||
Transform t = list[i];
|
||||
if (Mathf.Abs(t.localPosition.y - targetY) <= yTolerance)
|
||||
nearTarget.Add(t);
|
||||
}
|
||||
|
||||
List<Transform> selected = (nearTarget.Count >= expectedCount) ? nearTarget : new List<Transform>(list);
|
||||
|
||||
selected.Sort((a, b) =>
|
||||
{
|
||||
float da = Mathf.Abs(a.localPosition.y - targetY);
|
||||
float db = Mathf.Abs(b.localPosition.y - targetY);
|
||||
int yCmp = da.CompareTo(db);
|
||||
if (yCmp != 0) return yCmp;
|
||||
return a.localPosition.x.CompareTo(b.localPosition.x);
|
||||
});
|
||||
|
||||
if (expectedCount > 0 && selected.Count > expectedCount)
|
||||
selected = selected.GetRange(0, expectedCount);
|
||||
|
||||
selected.Sort((a, b) => a.localPosition.x.CompareTo(b.localPosition.x));
|
||||
|
||||
list.Clear();
|
||||
list.AddRange(selected);
|
||||
}
|
||||
|
||||
private static List<Transform> CollectDescendantsByNameContains(Transform root, string keyword)
|
||||
{
|
||||
List<Transform> list = new List<Transform>();
|
||||
if (root == null || string.IsNullOrEmpty(keyword)) return list;
|
||||
|
||||
Transform[] all = root.GetComponentsInChildren<Transform>(true);
|
||||
for (int i = 0; i < all.Length; i++)
|
||||
{
|
||||
Transform child = all[i];
|
||||
if (child == null || child == root) continue;
|
||||
if (child.name != null && child.name.IndexOf(keyword, System.StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
list.Add(child);
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private Transform FindPauseInScene()
|
||||
{
|
||||
Transform[] all = GetAllSceneTransforms();
|
||||
Transform fallback = null;
|
||||
|
||||
for (int i = 0; i < all.Length; i++)
|
||||
{
|
||||
Transform t = all[i];
|
||||
if (t == null || !string.Equals(t.name, "Pause", System.StringComparison.OrdinalIgnoreCase)) continue;
|
||||
if (fallback == null) fallback = t;
|
||||
if (t.GetComponent<Button>() != null) return t;
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
||||
private Transform FindSceneTransformByName(string exactName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(exactName)) return null;
|
||||
|
||||
Transform[] all = GetAllSceneTransforms();
|
||||
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 Transform FindSceneTransformByNameUnder(string exactName, Transform parent)
|
||||
{
|
||||
if (string.IsNullOrEmpty(exactName)) return null;
|
||||
|
||||
if (parent != null)
|
||||
{
|
||||
Transform[] all = parent.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 FindSceneTransformByName(exactName);
|
||||
}
|
||||
|
||||
private Transform[] GetAllSceneTransforms()
|
||||
{
|
||||
Transform[] all = Resources.FindObjectsOfTypeAll<Transform>();
|
||||
List<Transform> filtered = new List<Transform>(all.Length);
|
||||
Scene active = SceneManager.GetActiveScene();
|
||||
|
||||
for (int i = 0; i < all.Length; i++)
|
||||
{
|
||||
Transform t = all[i];
|
||||
if (t == null) continue;
|
||||
if (!t.gameObject.scene.IsValid()) continue;
|
||||
if (t.gameObject.scene != active) continue;
|
||||
filtered.Add(t);
|
||||
}
|
||||
|
||||
return filtered.ToArray();
|
||||
}
|
||||
|
||||
private Transform[] GetRootTransformsOfActiveScene()
|
||||
{
|
||||
Scene active = SceneManager.GetActiveScene();
|
||||
if (!active.IsValid() || !active.isLoaded) return new Transform[0];
|
||||
|
||||
GameObject[] roots = active.GetRootGameObjects();
|
||||
Transform[] result = new Transform[roots.Length];
|
||||
for (int i = 0; i < roots.Length; i++)
|
||||
result[i] = roots[i] != null ? roots[i].transform : null;
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b083b3896bb872146832768cab3dbb0c
|
||||
@@ -126,6 +126,44 @@ public class HoldNote : BaseNote
|
||||
return Mathf.FloorToInt(baseScore * mult);
|
||||
}
|
||||
|
||||
private static bool TryRewriteNonMissJudgeToPerfect(int trackIndex, ref string judgeResult)
|
||||
{
|
||||
if (judgeResult != "Great" && judgeResult != "Good") return false;
|
||||
var ally = GetAllyForTrackCached(trackIndex);
|
||||
if (ally == null || ally.IsDead) return false;
|
||||
if (!ally.IsNonMissToPerfectRewriteActive()) return false;
|
||||
|
||||
judgeResult = "Perfect";
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void AdjustCountsAfterRewriteToPerfect(int trackIndex, string originalJudge)
|
||||
{
|
||||
var sm = ScoreManager.Instance;
|
||||
if (sm == null) return;
|
||||
|
||||
if (originalJudge == "Great")
|
||||
{
|
||||
sm.countGreat = Mathf.Max(0, sm.countGreat - 1);
|
||||
if (trackIndex >= 0 && trackIndex < sm.trackGreatCounts.Length)
|
||||
sm.trackGreatCounts[trackIndex] = Mathf.Max(0, sm.trackGreatCounts[trackIndex] - 1);
|
||||
}
|
||||
else if (originalJudge == "Good")
|
||||
{
|
||||
sm.countGood = Mathf.Max(0, sm.countGood - 1);
|
||||
if (trackIndex >= 0 && trackIndex < sm.trackGoodCounts.Length)
|
||||
sm.trackGoodCounts[trackIndex] = Mathf.Max(0, sm.trackGoodCounts[trackIndex] - 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
sm.countPerfect += 1;
|
||||
if (trackIndex >= 0 && trackIndex < sm.trackPerfectCounts.Length)
|
||||
sm.trackPerfectCounts[trackIndex] += 1;
|
||||
}
|
||||
|
||||
private JudgeManager cachedJudgeManager;
|
||||
private TrackKeyManager cachedTrackKeyManager;
|
||||
|
||||
@@ -897,6 +935,12 @@ public class HoldNote : BaseNote
|
||||
ReleaseTrackJudgeLockSafe();
|
||||
}
|
||||
|
||||
// Lucky Chance: rewrite non-Miss judge to Perfect during active duration.
|
||||
if (result != "Miss")
|
||||
{
|
||||
TryRewriteNonMissJudgeToPerfect(trackIndex, ref result);
|
||||
}
|
||||
|
||||
// show judge result and update combo/UI like short notes
|
||||
InputManager.Instance?.ShowJudgeResult(trackIndex, result);
|
||||
|
||||
@@ -1057,6 +1101,12 @@ public class HoldNote : BaseNote
|
||||
}
|
||||
}
|
||||
|
||||
string originalEndJudge = result;
|
||||
if (TryRewriteNonMissJudgeToPerfect(trackIndex, ref result))
|
||||
{
|
||||
AdjustCountsAfterRewriteToPerfect(trackIndex, originalEndJudge);
|
||||
}
|
||||
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log($"[HoldNote] END result for {noteID}: {result} (color={noteColor})");
|
||||
|
||||
// show UI/audio
|
||||
|
||||
@@ -10,6 +10,9 @@ public class JudgeManager : MonoBehaviour
|
||||
public settlementController sc;
|
||||
public NoteSpawner ns;
|
||||
|
||||
[Header("Settlement Delay")]
|
||||
[SerializeField] private float settlementEnterDelaySeconds = 1.5f;
|
||||
|
||||
private bool _cachedIsDebugEnabled;
|
||||
private void UpdateDebugCache()
|
||||
{
|
||||
@@ -84,7 +87,20 @@ public class JudgeManager : MonoBehaviour
|
||||
return;
|
||||
}
|
||||
|
||||
// Activate settlement UI so components run their lifecycle, then call update next frame
|
||||
// Enter settlement after a real-time delay to leave a short idle window after chart end.
|
||||
StartCoroutine(InvokeSettlementUpdateNextFrame());
|
||||
}
|
||||
|
||||
private IEnumerator InvokeSettlementUpdateNextFrame()
|
||||
{
|
||||
// Keep settlement lead-in fixed at 1.5s for consistent pacing.
|
||||
float delay = 1.5f;
|
||||
if (delay > 0f)
|
||||
{
|
||||
yield return new WaitForSecondsRealtime(delay);
|
||||
}
|
||||
|
||||
// Activate settlement UI so components run their lifecycle.
|
||||
try
|
||||
{
|
||||
settlement_go.SetActive(true);
|
||||
@@ -94,19 +110,15 @@ public class JudgeManager : MonoBehaviour
|
||||
if (IsDebugEnabled) Debug.LogWarning($"[JudgeManager] Failed to activate settlement_go: {ex}");
|
||||
}
|
||||
|
||||
// Try to resolve settlementController reference if missing
|
||||
// Try to resolve settlementController reference if missing.
|
||||
if (sc == null && settlement_go != null)
|
||||
{
|
||||
sc = settlement_go.GetComponent<settlementController>() ?? settlement_go.GetComponentInChildren<settlementController>(true);
|
||||
}
|
||||
|
||||
// Invoke the settlement update on next frame to ensure UI initialization finished
|
||||
StartCoroutine(InvokeSettlementUpdateNextFrame());
|
||||
}
|
||||
// Wait one frame to allow Awake/Start/OnEnable.
|
||||
yield return null;
|
||||
|
||||
private IEnumerator InvokeSettlementUpdateNextFrame()
|
||||
{
|
||||
yield return null; // wait one frame to allow Awake/Start/OnEnable
|
||||
if (sc != null)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -68,6 +68,44 @@ public class Note : BaseNote
|
||||
return Mathf.FloorToInt(baseScore * mult);
|
||||
}
|
||||
|
||||
private static bool TryRewriteNonMissJudgeToPerfect(int trackIndex, ref string judgeResult)
|
||||
{
|
||||
if (judgeResult != "Great" && judgeResult != "Good") return false;
|
||||
var ally = GetAllyForTrackCached(trackIndex);
|
||||
if (ally == null || ally.IsDead) return false;
|
||||
if (!ally.IsNonMissToPerfectRewriteActive()) return false;
|
||||
|
||||
judgeResult = "Perfect";
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void AdjustCountsAfterRewriteToPerfect(int trackIndex, string originalJudge)
|
||||
{
|
||||
var sm = ScoreManager.Instance;
|
||||
if (sm == null) return;
|
||||
|
||||
if (originalJudge == "Great")
|
||||
{
|
||||
sm.countGreat = Mathf.Max(0, sm.countGreat - 1);
|
||||
if (trackIndex >= 0 && trackIndex < sm.trackGreatCounts.Length)
|
||||
sm.trackGreatCounts[trackIndex] = Mathf.Max(0, sm.trackGreatCounts[trackIndex] - 1);
|
||||
}
|
||||
else if (originalJudge == "Good")
|
||||
{
|
||||
sm.countGood = Mathf.Max(0, sm.countGood - 1);
|
||||
if (trackIndex >= 0 && trackIndex < sm.trackGoodCounts.Length)
|
||||
sm.trackGoodCounts[trackIndex] = Mathf.Max(0, sm.trackGoodCounts[trackIndex] - 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
sm.countPerfect += 1;
|
||||
if (trackIndex >= 0 && trackIndex < sm.trackPerfectCounts.Length)
|
||||
sm.trackPerfectCounts[trackIndex] += 1;
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
anim = GetComponent<AnimationController>();
|
||||
@@ -326,6 +364,12 @@ public class Note : BaseNote
|
||||
|
||||
if (!string.IsNullOrEmpty(judgeResult))
|
||||
{
|
||||
string originalJudge = judgeResult;
|
||||
if (TryRewriteNonMissJudgeToPerfect(TrackIndex, ref judgeResult))
|
||||
{
|
||||
AdjustCountsAfterRewriteToPerfect(TrackIndex, originalJudge);
|
||||
}
|
||||
|
||||
InputManager.Instance?.ShowJudgeResult(TrackIndex, judgeResult);
|
||||
JudgeSoundManager.Instance?.PlayJudgeSound(judgeResult);
|
||||
teamUIController.Instance?.OnJudgeResult(judgeResult);
|
||||
|
||||
@@ -21,6 +21,7 @@ public class NoteSpawner : MonoBehaviour
|
||||
public GameObject[] holdNoteEndPrefabs;
|
||||
|
||||
public float spawnOffset = 0f; // Documentation text normalized.
|
||||
public float static_value_add_to_spawnoffset = 0f;
|
||||
|
||||
[Header("Global timing adjustments")]
|
||||
[Tooltip("Global additional realtime offset (seconds) added to hit times for all notes. Use to test input latency or adjust judgement timing. Default 0. Can be negative to make notes arrive earlier.")]
|
||||
@@ -124,6 +125,12 @@ public class NoteSpawner : MonoBehaviour
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// 读取 PlayerPrefs 中的延迟偏移值(秒)并应用
|
||||
const string DELAY_PREFS_KEY = "UserGlobalDelaySeconds";
|
||||
float savedDelay = PlayerPrefs.GetFloat(DELAY_PREFS_KEY, 0f);
|
||||
spawnOffset = savedDelay + static_value_add_to_spawnoffset;
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log($"[NoteSpawner] Applied spawnOffset={spawnOffset} (Saved={savedDelay} + Static={static_value_add_to_spawnoffset})");
|
||||
|
||||
// restore animations active state on Start
|
||||
if (animations != null)
|
||||
{
|
||||
|
||||
@@ -36,6 +36,8 @@ public class GfxController : MonoBehaviour
|
||||
public GameObject explosionFX;
|
||||
public GameObject k_o_fx;
|
||||
public GameObject koFatherObject;
|
||||
public float koFxScale = 1f;
|
||||
public int koFxSortingOrder = 31;
|
||||
|
||||
[Header("Explosion Effect Settings")]
|
||||
[Tooltip("当单次伤害超过敌人最大生命值的百分比时触发爆炸 (0.2 = 20%)")]
|
||||
@@ -117,8 +119,6 @@ public class GfxController : MonoBehaviour
|
||||
public float allySkillRandomOffsetY = 0f;
|
||||
[Tooltip("友军技能弹道到达时的随机偏移范围 - Z轴")]
|
||||
public float allySkillRandomOffsetZ = 0f;
|
||||
|
||||
public float koFxScale = 1f;
|
||||
|
||||
[Header("Enemy Hurt Shake Settings")]
|
||||
public Vector3 hitShakeAmplitude = new Vector3(10f, 10f, 0f);
|
||||
@@ -153,6 +153,37 @@ public class GfxController : MonoBehaviour
|
||||
/// <param name="damageInfo">可选:传入伤害信息以判断是否触发爆炸特效 (damage, maxHealth)</param>
|
||||
public void PlayHitFX(GameObject target, GameObject parent = null, bool isEnemy = false, Vector3? customWorldPos = null, float? scaleOverride = null, (float damage, float maxHealth)? damageInfo = null)
|
||||
{
|
||||
// --- Check if the target ally slot is active before playing VFX ---
|
||||
if (!isEnemy && teamUIController.Instance != null)
|
||||
{
|
||||
int slot = -1;
|
||||
var ally = target.GetComponent<AllyCombatant>();
|
||||
if (ally != null) slot = ally.slotIndex;
|
||||
else
|
||||
{
|
||||
string n = target.name.ToLower();
|
||||
if (n.Contains("ally_01")) slot = 0;
|
||||
else if (n.Contains("ally_02")) slot = 1;
|
||||
else if (n.Contains("ally_03")) slot = 2;
|
||||
else if (n.Contains("ally_04")) slot = 3;
|
||||
else if (n.Contains("ally_05")) slot = 4;
|
||||
}
|
||||
|
||||
if (slot != -1)
|
||||
{
|
||||
bool isActive = false;
|
||||
switch (slot)
|
||||
{
|
||||
case 0: isActive = teamUIController.Instance.isAlly01_active; break;
|
||||
case 1: isActive = teamUIController.Instance.isAlly02_active; break;
|
||||
case 2: isActive = teamUIController.Instance.isAlly03_active; break;
|
||||
case 3: isActive = teamUIController.Instance.isAlly04_active; break;
|
||||
case 4: isActive = teamUIController.Instance.isAlly05_active; break;
|
||||
}
|
||||
if (!isActive) return; // Skip VFX for inactive ally
|
||||
}
|
||||
}
|
||||
|
||||
// Determine which FX prefab to use (Hit or Explosion)
|
||||
GameObject fxPrefab = hitFX;
|
||||
bool isExplosion = false;
|
||||
@@ -390,6 +421,42 @@ public class GfxController : MonoBehaviour
|
||||
{
|
||||
if (source == null || target == null) return;
|
||||
|
||||
// --- Check if the source or target ally slot is active before playing VFX ---
|
||||
if (teamUIController.Instance != null)
|
||||
{
|
||||
// Check source
|
||||
var sourceAlly = source.GetComponent<AllyCombatant>();
|
||||
if (sourceAlly != null)
|
||||
{
|
||||
bool isActive = false;
|
||||
switch (sourceAlly.slotIndex)
|
||||
{
|
||||
case 0: isActive = teamUIController.Instance.isAlly01_active; break;
|
||||
case 1: isActive = teamUIController.Instance.isAlly02_active; break;
|
||||
case 2: isActive = teamUIController.Instance.isAlly03_active; break;
|
||||
case 3: isActive = teamUIController.Instance.isAlly04_active; break;
|
||||
case 4: isActive = teamUIController.Instance.isAlly05_active; break;
|
||||
}
|
||||
if (!isActive) { onComplete?.Invoke(target.transform.position); return; }
|
||||
}
|
||||
|
||||
// Check target
|
||||
var targetAlly = target.GetComponent<AllyCombatant>();
|
||||
if (targetAlly != null)
|
||||
{
|
||||
bool isActive = false;
|
||||
switch (targetAlly.slotIndex)
|
||||
{
|
||||
case 0: isActive = teamUIController.Instance.isAlly01_active; break;
|
||||
case 1: isActive = teamUIController.Instance.isAlly02_active; break;
|
||||
case 2: isActive = teamUIController.Instance.isAlly03_active; break;
|
||||
case 3: isActive = teamUIController.Instance.isAlly04_active; break;
|
||||
case 4: isActive = teamUIController.Instance.isAlly05_active; break;
|
||||
}
|
||||
if (!isActive) { onComplete?.Invoke(target.transform.position); return; }
|
||||
}
|
||||
}
|
||||
|
||||
GameObject prefab = allySkillProjectilePrefab != null ? allySkillProjectilePrefab : projectileFX;
|
||||
if (prefab == null)
|
||||
{
|
||||
@@ -747,6 +814,18 @@ public class GfxController : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
// 应用层级 Order In Layer
|
||||
Renderer[] renderers = fx.GetComponentsInChildren<Renderer>(true);
|
||||
foreach (var r in renderers)
|
||||
{
|
||||
r.sortingOrder = koFxSortingOrder;
|
||||
}
|
||||
Canvas[] canvases = fx.GetComponentsInChildren<Canvas>(true);
|
||||
foreach (var c in canvases)
|
||||
{
|
||||
c.sortingOrder = koFxSortingOrder;
|
||||
}
|
||||
|
||||
// 动画播放完后销毁
|
||||
Destroy(fx, 3.0f);
|
||||
}
|
||||
@@ -763,6 +842,25 @@ public class GfxController : MonoBehaviour
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- Check if the target ally slot is active before playing VFX ---
|
||||
if (teamUIController.Instance != null)
|
||||
{
|
||||
bool isActive = false;
|
||||
switch (allySlotIndex)
|
||||
{
|
||||
case 0: isActive = teamUIController.Instance.isAlly01_active; break;
|
||||
case 1: isActive = teamUIController.Instance.isAlly02_active; break;
|
||||
case 2: isActive = teamUIController.Instance.isAlly03_active; break;
|
||||
case 3: isActive = teamUIController.Instance.isAlly04_active; break;
|
||||
case 4: isActive = teamUIController.Instance.isAlly05_active; break;
|
||||
}
|
||||
if (!isActive)
|
||||
{
|
||||
// If inactive, skip VFX and return false so SkillBuilder applies immediate (but blocked) damage
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
GameObject targetAllyObj = GetAllyObject(allySlotIndex);
|
||||
if (targetAllyObj == null)
|
||||
{
|
||||
|
||||
@@ -18,7 +18,8 @@ public enum PlayerBudeffIconType
|
||||
ot_defend_down,
|
||||
ot_vulnerability,
|
||||
dmg_redirect_toSelf,
|
||||
dmg_redirect_toAdjacent
|
||||
dmg_redirect_toAdjacent,
|
||||
ot_luckyChance
|
||||
}
|
||||
|
||||
public class PlayerBudeffPrefab : MonoBehaviour
|
||||
@@ -43,6 +44,7 @@ public class PlayerBudeffPrefab : MonoBehaviour
|
||||
public Sprite ot_vulnerability;
|
||||
public Sprite dmg_redirect_toSelf;
|
||||
public Sprite dmg_redirect_toAdjacent;
|
||||
public Sprite ot_luckyChance;
|
||||
|
||||
[Header("num colors")]
|
||||
public Color ot_maxHP_up_color;
|
||||
@@ -60,6 +62,7 @@ public class PlayerBudeffPrefab : MonoBehaviour
|
||||
public Color ot_vulnerability_color;
|
||||
public Color dmg_redirect_toSelf_color;
|
||||
public Color dmg_redirect_toAdjacent_color;
|
||||
public Color ot_luckyChance_color;
|
||||
|
||||
public void Apply(PlayerBudeffIconType type, string numText)
|
||||
{
|
||||
@@ -96,6 +99,7 @@ public class PlayerBudeffPrefab : MonoBehaviour
|
||||
case PlayerBudeffIconType.ot_vulnerability: return ot_vulnerability;
|
||||
case PlayerBudeffIconType.dmg_redirect_toSelf: return dmg_redirect_toSelf;
|
||||
case PlayerBudeffIconType.dmg_redirect_toAdjacent: return dmg_redirect_toAdjacent;
|
||||
case PlayerBudeffIconType.ot_luckyChance: return ot_luckyChance;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
@@ -119,6 +123,7 @@ public class PlayerBudeffPrefab : MonoBehaviour
|
||||
case PlayerBudeffIconType.ot_vulnerability: return ot_vulnerability_color;
|
||||
case PlayerBudeffIconType.dmg_redirect_toSelf: return dmg_redirect_toSelf_color;
|
||||
case PlayerBudeffIconType.dmg_redirect_toAdjacent: return dmg_redirect_toAdjacent_color;
|
||||
case PlayerBudeffIconType.ot_luckyChance: return ot_luckyChance_color;
|
||||
default: return Color.white;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,6 +278,7 @@ MonoBehaviour:
|
||||
ot_vulnerability: {fileID: 21300000, guid: ba83152a6fcffb24899119ba44cae403, type: 3}
|
||||
dmg_redirect_toSelf: {fileID: 21300000, guid: d11b1c841cccade4c9f1c254d671ae15, type: 3}
|
||||
dmg_redirect_toAdjacent: {fileID: 21300000, guid: 54492507a5422564aa5db7c7ef96311e, type: 3}
|
||||
ot_luckyChance: {fileID: 2978455473066875312, guid: f2e1a192758189c49b8ef48c7a471107, type: 3}
|
||||
ot_maxHP_up_color: {r: 0.19215688, g: 0.8117648, b: 0.65882355, a: 0}
|
||||
ot_maxHP_down_color: {r: 0.10588236, g: 0.47058827, b: 0.3921569, a: 0}
|
||||
ot_maxMana_up_color: {r: 0.2784314, g: 0.75294125, b: 1, a: 0}
|
||||
@@ -293,3 +294,4 @@ MonoBehaviour:
|
||||
ot_vulnerability_color: {r: 0.18867922, g: 0.18867922, b: 0.18867922, a: 0}
|
||||
dmg_redirect_toSelf_color: {r: 0.82745105, g: 0.3647059, b: 0.23529413, a: 0}
|
||||
dmg_redirect_toAdjacent_color: {r: 0.8196079, g: 0.41960788, b: 0.19607845, a: 0}
|
||||
ot_luckyChance_color: {r: 0.8679245, g: 0.6304734, b: 0.8679245, a: 1}
|
||||
|
||||
+333
-21
@@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
[DefaultExecutionOrder(-1000)]
|
||||
public class iBudeffPrefabController : MonoBehaviour
|
||||
@@ -17,6 +19,13 @@ public class iBudeffPrefabController : MonoBehaviour
|
||||
public float apprFloatDistance = 40f;
|
||||
public float apprFadeDuration = 0.6f;
|
||||
|
||||
[Header("buff icon flash animation")]
|
||||
public bool enableBuffIconFlashAnimation = true;
|
||||
[Range(1, 8)] public int buffIconEntryFlashCount = 3;
|
||||
[Range(0.02f, 0.6f)] public float buffIconEntryFlashDuration = 0.18f;
|
||||
[Range(1, 8)] public int buffIconExitFlashCount = 3;
|
||||
[Range(0.02f, 0.6f)] public float buffIconExitFlashDuration = 0.16f;
|
||||
|
||||
[Header("place to put")]
|
||||
public GameObject enemy_icon_put;
|
||||
public GameObject ally01_put;
|
||||
@@ -42,7 +51,8 @@ public class iBudeffPrefabController : MonoBehaviour
|
||||
DamageResistance,
|
||||
Attack,
|
||||
RedirectToSelf,
|
||||
RedirectToAdjacent
|
||||
RedirectToAdjacent,
|
||||
LuckyChance
|
||||
}
|
||||
|
||||
private class Entry
|
||||
@@ -51,6 +61,7 @@ public class iBudeffPrefabController : MonoBehaviour
|
||||
public Group group;
|
||||
public float value;
|
||||
public float startTime;
|
||||
public float duration;
|
||||
}
|
||||
|
||||
private class SlotState
|
||||
@@ -82,6 +93,8 @@ public class iBudeffPrefabController : MonoBehaviour
|
||||
|
||||
private readonly Dictionary<int, SlotState> _slots = new Dictionary<int, SlotState>(8);
|
||||
private readonly Dictionary<int, EnemyState> _enemies = new Dictionary<int, EnemyState>(8);
|
||||
private readonly Dictionary<int, Coroutine> _iconEntryCoroutines = new Dictionary<int, Coroutine>(32);
|
||||
private readonly Dictionary<int, Coroutine> _iconExitCoroutines = new Dictionary<int, Coroutine>(32);
|
||||
private bool _warnedMissingApprPrefab;
|
||||
private bool _warnedMissingApprParents;
|
||||
private PlayerBudeffPrefab _spriteSource;
|
||||
@@ -193,17 +206,159 @@ public class iBudeffPrefabController : MonoBehaviour
|
||||
|
||||
var group = GetGroupForIconType(type);
|
||||
string id = Guid.NewGuid().ToString();
|
||||
RegisterEntry(ally, group, id, value, Time.time);
|
||||
RegisterEntry(ally, group, id, value, Time.time, duration);
|
||||
return id;
|
||||
}
|
||||
|
||||
public void RefreshAllyNow(AllyCombatant ally)
|
||||
{
|
||||
if (ally == null) return;
|
||||
if (!Application.isPlaying) return;
|
||||
var s = GetOrCreateSlot(ally.slotIndex);
|
||||
s.ally = ally;
|
||||
if (!s.baselineReady) RegisterBaseline(ally);
|
||||
else RefreshSlot(ally.slotIndex);
|
||||
}
|
||||
|
||||
public void UnregisterTimedEffect(int slotIndex, string id)
|
||||
{
|
||||
if (string.IsNullOrEmpty(id)) return;
|
||||
UnregisterEntry(slotIndex, id);
|
||||
if (!UnregisterEntry(slotIndex, id))
|
||||
{
|
||||
// Entry may have been transferred to another ally slot; fallback to global search.
|
||||
UnregisterEntryAnySlot(id);
|
||||
}
|
||||
}
|
||||
|
||||
private void RegisterEntry(AllyCombatant ally, Group group, string id, float value, float startTime)
|
||||
public bool TryGetLatestAllyBuffTimestamp(AllyCombatant ally, out float latestStartTime)
|
||||
{
|
||||
latestStartTime = float.NegativeInfinity;
|
||||
if (ally == null) return false;
|
||||
if (!_slots.TryGetValue(ally.slotIndex, out var s) || s == null) return false;
|
||||
|
||||
bool found = false;
|
||||
foreach (var kv in s.entriesByGroup)
|
||||
{
|
||||
if (!IsTransferableGroupForRedirect(kv.Key)) continue;
|
||||
var list = kv.Value;
|
||||
if (list == null) continue;
|
||||
for (int i = 0; i < list.Count; i++)
|
||||
{
|
||||
var e = list[i];
|
||||
if (e == null) continue;
|
||||
if (!found || e.startTime > latestStartTime)
|
||||
{
|
||||
latestStartTime = e.startTime;
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
// Count currently-held transferable buff groups for target selection rules.
|
||||
// This mirrors what players perceive in budeff icon groups.
|
||||
public bool TryGetTransferableAllyBuffGroupCount(AllyCombatant ally, out int count)
|
||||
{
|
||||
count = 0;
|
||||
if (ally == null) return false;
|
||||
if (!_slots.TryGetValue(ally.slotIndex, out var s) || s == null) return false;
|
||||
|
||||
foreach (var kv in s.entriesByGroup)
|
||||
{
|
||||
if (!IsTransferableGroupForRedirect(kv.Key)) continue;
|
||||
var list = kv.Value;
|
||||
if (list != null && list.Count > 0) count++;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryTransferLatestAllyBuff(AllyCombatant from, AllyCombatant to, out PlayerBudeffIconType iconType, out float value, out float startTime, out float duration)
|
||||
{
|
||||
iconType = PlayerBudeffIconType.ot_scoreEfficiency_up;
|
||||
value = 0f;
|
||||
startTime = 0f;
|
||||
duration = 0f;
|
||||
|
||||
if (from == null || to == null || from == to) return false;
|
||||
if (!_slots.TryGetValue(from.slotIndex, out var fromState) || fromState == null) return false;
|
||||
|
||||
Entry bestEntry = null;
|
||||
Group bestGroup = Group.ScoreEfficiency;
|
||||
|
||||
foreach (var kv in fromState.entriesByGroup)
|
||||
{
|
||||
if (!IsTransferableGroupForRedirect(kv.Key)) continue;
|
||||
var list = kv.Value;
|
||||
if (list == null) continue;
|
||||
for (int i = 0; i < list.Count; i++)
|
||||
{
|
||||
var e = list[i];
|
||||
if (e == null) continue;
|
||||
if (bestEntry == null || e.startTime > bestEntry.startTime)
|
||||
{
|
||||
bestEntry = e;
|
||||
bestGroup = kv.Key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bestEntry == null) return false;
|
||||
|
||||
// Remove from source
|
||||
if (fromState.entriesByGroup.TryGetValue(bestGroup, out var srcList) && srcList != null)
|
||||
{
|
||||
srcList.Remove(bestEntry);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(bestEntry.id))
|
||||
{
|
||||
fromState.groupByEntryId.Remove(bestEntry.id);
|
||||
}
|
||||
|
||||
// Add to destination with same entry id so later unregistration still works.
|
||||
var toState = GetOrCreateSlot(to.slotIndex);
|
||||
toState.ally = to;
|
||||
if (!toState.baselineReady) RegisterBaseline(to);
|
||||
if (!toState.entriesByGroup.TryGetValue(bestGroup, out var dstList) || dstList == null)
|
||||
{
|
||||
dstList = new List<Entry>(4);
|
||||
toState.entriesByGroup[bestGroup] = dstList;
|
||||
}
|
||||
dstList.Add(bestEntry);
|
||||
if (!string.IsNullOrEmpty(bestEntry.id))
|
||||
{
|
||||
toState.groupByEntryId[bestEntry.id] = bestGroup;
|
||||
}
|
||||
|
||||
iconType = GetRepresentativeIconType(bestGroup, bestEntry.value);
|
||||
value = bestEntry.value;
|
||||
startTime = bestEntry.startTime;
|
||||
duration = bestEntry.duration;
|
||||
|
||||
RefreshSlot(from.slotIndex);
|
||||
RefreshSlot(to.slotIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsTransferableGroupForRedirect(Group group)
|
||||
{
|
||||
switch (group)
|
||||
{
|
||||
case Group.MaxHP:
|
||||
case Group.MaxMana:
|
||||
case Group.ScoreEfficiency:
|
||||
case Group.DamageResistance:
|
||||
case Group.Attack:
|
||||
case Group.RedirectToSelf:
|
||||
case Group.RedirectToAdjacent:
|
||||
return true;
|
||||
default:
|
||||
// Exclude unsupported/non-transfer payload groups only.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void RegisterEntry(AllyCombatant ally, Group group, string id, float value, float startTime, float duration = 0f)
|
||||
{
|
||||
var s = GetOrCreateSlot(ally.slotIndex);
|
||||
s.ally = ally;
|
||||
@@ -215,17 +370,17 @@ public class iBudeffPrefabController : MonoBehaviour
|
||||
s.entriesByGroup[group] = list;
|
||||
}
|
||||
|
||||
var e = new Entry { id = id, group = group, value = value, startTime = startTime };
|
||||
var e = new Entry { id = id, group = group, value = value, startTime = startTime, duration = duration };
|
||||
list.Add(e);
|
||||
s.groupByEntryId[id] = group;
|
||||
|
||||
RefreshSlot(ally.slotIndex);
|
||||
}
|
||||
|
||||
private void UnregisterEntry(int slotIndex, string id)
|
||||
private bool UnregisterEntry(int slotIndex, string id)
|
||||
{
|
||||
if (!_slots.TryGetValue(slotIndex, out var s)) return;
|
||||
if (!s.groupByEntryId.TryGetValue(id, out var group)) return;
|
||||
if (!_slots.TryGetValue(slotIndex, out var s)) return false;
|
||||
if (!s.groupByEntryId.TryGetValue(id, out var group)) return false;
|
||||
if (s.entriesByGroup.TryGetValue(group, out var list))
|
||||
{
|
||||
for (int i = list.Count - 1; i >= 0; i--)
|
||||
@@ -239,6 +394,17 @@ public class iBudeffPrefabController : MonoBehaviour
|
||||
}
|
||||
s.groupByEntryId.Remove(id);
|
||||
RefreshSlot(slotIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool UnregisterEntryAnySlot(string id)
|
||||
{
|
||||
if (string.IsNullOrEmpty(id)) return false;
|
||||
foreach (var kv in _slots)
|
||||
{
|
||||
if (UnregisterEntry(kv.Key, id)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void RefreshSlot(int slotIndex)
|
||||
@@ -341,6 +507,12 @@ public class iBudeffPrefabController : MonoBehaviour
|
||||
display.Add((Group.RedirectToAdjacent, PlayerBudeffIconType.dmg_redirect_toAdjacent, "1", GetOrderKey(s, Group.RedirectToAdjacent)));
|
||||
}
|
||||
|
||||
bool hasLuckyChanceByState = s.ally != null && s.ally.HasNonMissToPerfectRewriteForUI();
|
||||
if (HasAny(s, Group.LuckyChance) || hasLuckyChanceByState)
|
||||
{
|
||||
display.Add((Group.LuckyChance, PlayerBudeffIconType.ot_luckyChance, " ", GetOrderKey(s, Group.LuckyChance)));
|
||||
}
|
||||
|
||||
display.Sort((a, b) => a.orderKey.CompareTo(b.orderKey));
|
||||
|
||||
var shouldShow = new HashSet<Group>();
|
||||
@@ -354,7 +526,7 @@ public class iBudeffPrefabController : MonoBehaviour
|
||||
{
|
||||
var go = s.iconByGroup[g];
|
||||
s.iconByGroup.Remove(g);
|
||||
if (go != null) Destroy(go);
|
||||
if (go != null) PlayBuffIconExitAndDestroy(go);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -365,6 +537,7 @@ public class iBudeffPrefabController : MonoBehaviour
|
||||
{
|
||||
go = Instantiate(budeff_prefab, parent.transform, false);
|
||||
s.iconByGroup[item.group] = go;
|
||||
PlayBuffIconEntry(go);
|
||||
}
|
||||
go.transform.SetSiblingIndex(i);
|
||||
var comp = go.GetComponent<PlayerBudeffPrefab>();
|
||||
@@ -472,6 +645,11 @@ public class iBudeffPrefabController : MonoBehaviour
|
||||
display.Add((Group.RedirectToAdjacent, PlayerBudeffIconType.dmg_redirect_toAdjacent, "1", GetOrderKeyEnemy(s, Group.RedirectToAdjacent)));
|
||||
}
|
||||
|
||||
if (HasAnyEnemy(s, Group.LuckyChance))
|
||||
{
|
||||
display.Add((Group.LuckyChance, PlayerBudeffIconType.ot_luckyChance, string.Empty, GetOrderKeyEnemy(s, Group.LuckyChance)));
|
||||
}
|
||||
|
||||
if (hasEntries)
|
||||
{
|
||||
if (display.Count == 0)
|
||||
@@ -497,7 +675,7 @@ public class iBudeffPrefabController : MonoBehaviour
|
||||
{
|
||||
var go = s.iconByGroup[g];
|
||||
s.iconByGroup.Remove(g);
|
||||
if (go != null) Destroy(go);
|
||||
if (go != null) PlayBuffIconExitAndDestroy(go);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -508,6 +686,7 @@ public class iBudeffPrefabController : MonoBehaviour
|
||||
{
|
||||
go = Instantiate(budeff_prefab, parent.transform, false);
|
||||
s.iconByGroup[item.group] = go;
|
||||
PlayBuffIconEntry(go);
|
||||
}
|
||||
go.transform.SetSiblingIndex(i);
|
||||
var comp = go.GetComponent<PlayerBudeffPrefab>();
|
||||
@@ -709,7 +888,7 @@ public class iBudeffPrefabController : MonoBehaviour
|
||||
{
|
||||
var g = existingGroups[i];
|
||||
var go = s.iconByGroup[g];
|
||||
if (go != null) Destroy(go);
|
||||
if (go != null) PlayBuffIconExitAndDestroy(go);
|
||||
}
|
||||
s.iconByGroup.Clear();
|
||||
s.entriesByGroup.Clear();
|
||||
@@ -740,12 +919,14 @@ public class iBudeffPrefabController : MonoBehaviour
|
||||
{
|
||||
if (s == null || enemy == null) return;
|
||||
var so = enemy.sourceData;
|
||||
if (so != null && !ShouldUseRuntimeBaseline(enemy, so))
|
||||
int diffID = BeatmapManager.Instance != null ? BeatmapManager.Instance.assignedDifficulty : 0;
|
||||
if (so != null && !ShouldUseRuntimeBaseline(enemy, so, diffID))
|
||||
{
|
||||
s.baseMaxHP = Mathf.Max(1, so.enemy_maxHP);
|
||||
s.baseMaxMana = Mathf.Max(0, so.GetEffectiveMaxMana());
|
||||
s.baseAttack = Mathf.Max(0, so.enemy_baseAttack);
|
||||
s.baseDamageResistance = ClampDamageResistance(so.enemy_damageResistance);
|
||||
var stats = so.GetStatsByDifficultyID(diffID);
|
||||
s.baseMaxHP = Mathf.Max(1, stats.enemy_maxHP);
|
||||
s.baseMaxMana = Mathf.Max(0, stats.enemy_maxMana);
|
||||
s.baseAttack = Mathf.Max(0, stats.enemy_baseAttack);
|
||||
s.baseDamageResistance = ClampDamageResistance(stats.enemy_damageResistance);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -756,13 +937,14 @@ public class iBudeffPrefabController : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ShouldUseRuntimeBaseline(EnemyCombatant enemy, EnemyData_SO so)
|
||||
private static bool ShouldUseRuntimeBaseline(EnemyCombatant enemy, EnemyData_SO so, int difficultyID)
|
||||
{
|
||||
if (enemy == null || so == null) return true;
|
||||
int soMaxHP = Mathf.Max(1, so.enemy_maxHP);
|
||||
int soMaxMana = Mathf.Max(0, so.GetEffectiveMaxMana());
|
||||
int soAttack = Mathf.Max(0, so.enemy_baseAttack);
|
||||
float soResist = ClampDamageResistance(so.enemy_damageResistance);
|
||||
var stats = so.GetStatsByDifficultyID(difficultyID);
|
||||
int soMaxHP = Mathf.Max(1, stats.enemy_maxHP);
|
||||
int soMaxMana = Mathf.Max(0, stats.enemy_maxMana);
|
||||
int soAttack = Mathf.Max(0, stats.enemy_baseAttack);
|
||||
float soResist = ClampDamageResistance(stats.enemy_damageResistance);
|
||||
if (enemy.maxHP != soMaxHP) return true;
|
||||
if (enemy.maxMana != soMaxMana) return true;
|
||||
if (enemy.attack != soAttack) return true;
|
||||
@@ -844,11 +1026,43 @@ public class iBudeffPrefabController : MonoBehaviour
|
||||
return Group.RedirectToSelf;
|
||||
case PlayerBudeffIconType.dmg_redirect_toAdjacent:
|
||||
return Group.RedirectToAdjacent;
|
||||
case PlayerBudeffIconType.ot_luckyChance:
|
||||
return Group.LuckyChance;
|
||||
default:
|
||||
return Group.ScoreEfficiency;
|
||||
}
|
||||
}
|
||||
|
||||
private static PlayerBudeffIconType GetRepresentativeIconType(Group group, float value)
|
||||
{
|
||||
switch (group)
|
||||
{
|
||||
case Group.MaxHP:
|
||||
return value >= 0f ? PlayerBudeffIconType.ot_maxHP_up : PlayerBudeffIconType.ot_maxHP_down;
|
||||
case Group.MaxMana:
|
||||
return value >= 0f ? PlayerBudeffIconType.ot_maxMana_up : PlayerBudeffIconType.ot_maxMana_down;
|
||||
case Group.Bleeding:
|
||||
return PlayerBudeffIconType.ot_bleeding;
|
||||
case Group.DeepHurt:
|
||||
return PlayerBudeffIconType.ot_deephurt;
|
||||
case Group.ScoreEfficiency:
|
||||
return value >= 0f ? PlayerBudeffIconType.ot_scoreEfficiency_up : PlayerBudeffIconType.ot_scoreEfficiency_down;
|
||||
case Group.DamageResistance:
|
||||
if (value < 0f) return PlayerBudeffIconType.ot_defend_down;
|
||||
return PlayerBudeffIconType.ot_defend_up;
|
||||
case Group.Attack:
|
||||
return value >= 0f ? PlayerBudeffIconType.ot_atk_up : PlayerBudeffIconType.ot_atk_down;
|
||||
case Group.RedirectToSelf:
|
||||
return PlayerBudeffIconType.dmg_redirect_toSelf;
|
||||
case Group.RedirectToAdjacent:
|
||||
return PlayerBudeffIconType.dmg_redirect_toAdjacent;
|
||||
case Group.LuckyChance:
|
||||
return PlayerBudeffIconType.ot_luckyChance;
|
||||
default:
|
||||
return PlayerBudeffIconType.ot_scoreEfficiency_up;
|
||||
}
|
||||
}
|
||||
|
||||
private void TryAutoBindPuts()
|
||||
{
|
||||
var ui = teamUIController.Instance != null ? teamUIController.Instance : FindAnyObjectByType<teamUIController>();
|
||||
@@ -967,6 +1181,8 @@ public class iBudeffPrefabController : MonoBehaviour
|
||||
return source.dmg_redirect_toSelf;
|
||||
case EffectType.RedirectSelfDamageToAdjacent:
|
||||
return source.dmg_redirect_toAdjacent;
|
||||
case EffectType.RewriteNonMissToPerfect:
|
||||
return source.ot_luckyChance;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -1006,4 +1222,100 @@ public class iBudeffPrefabController : MonoBehaviour
|
||||
_warnedMissingApprParents = true;
|
||||
Debug.LogWarning("[iBudeffPrefabController] appr_to_put objects are not assigned (no appr popup will be shown).");
|
||||
}
|
||||
|
||||
private static CanvasGroup EnsureCanvasGroup(GameObject go)
|
||||
{
|
||||
if (go == null) return null;
|
||||
var cg = go.GetComponent<CanvasGroup>();
|
||||
if (cg == null) cg = go.AddComponent<CanvasGroup>();
|
||||
return cg;
|
||||
}
|
||||
|
||||
private void PlayBuffIconEntry(GameObject go)
|
||||
{
|
||||
if (go == null) return;
|
||||
|
||||
int id = go.GetInstanceID();
|
||||
if (_iconExitCoroutines.TryGetValue(id, out var exitCo) && exitCo != null)
|
||||
{
|
||||
StopCoroutine(exitCo);
|
||||
_iconExitCoroutines.Remove(id);
|
||||
}
|
||||
|
||||
var cg = EnsureCanvasGroup(go);
|
||||
if (!enableBuffIconFlashAnimation || cg == null)
|
||||
{
|
||||
if (cg != null) cg.alpha = 1f;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_iconEntryCoroutines.TryGetValue(id, out var running) && running != null)
|
||||
StopCoroutine(running);
|
||||
|
||||
_iconEntryCoroutines[id] = StartCoroutine(BuffIconEntryFlashCoroutine(go, cg, id));
|
||||
}
|
||||
|
||||
private void PlayBuffIconExitAndDestroy(GameObject go)
|
||||
{
|
||||
if (go == null) return;
|
||||
|
||||
int id = go.GetInstanceID();
|
||||
if (_iconEntryCoroutines.TryGetValue(id, out var entryCo) && entryCo != null)
|
||||
{
|
||||
StopCoroutine(entryCo);
|
||||
_iconEntryCoroutines.Remove(id);
|
||||
}
|
||||
|
||||
var cg = EnsureCanvasGroup(go);
|
||||
if (!enableBuffIconFlashAnimation || cg == null)
|
||||
{
|
||||
Destroy(go);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_iconExitCoroutines.TryGetValue(id, out var running) && running != null)
|
||||
StopCoroutine(running);
|
||||
|
||||
_iconExitCoroutines[id] = StartCoroutine(BuffIconExitFlashCoroutine(go, cg, id));
|
||||
}
|
||||
|
||||
private IEnumerator BuffIconEntryFlashCoroutine(GameObject go, CanvasGroup cg, int id)
|
||||
{
|
||||
if (go == null || cg == null) yield break;
|
||||
|
||||
int toggles = Mathf.Max(2, buffIconEntryFlashCount * 2);
|
||||
float step = Mathf.Max(0.005f, buffIconEntryFlashDuration / toggles);
|
||||
|
||||
cg.alpha = 0f;
|
||||
for (int i = 0; i < toggles; i++)
|
||||
{
|
||||
if (go == null || cg == null) yield break;
|
||||
cg.alpha = (i % 2 == 0) ? 1f : 0.08f;
|
||||
yield return new WaitForSeconds(step);
|
||||
}
|
||||
|
||||
if (go != null && cg != null) cg.alpha = 1f;
|
||||
_iconEntryCoroutines.Remove(id);
|
||||
}
|
||||
|
||||
private IEnumerator BuffIconExitFlashCoroutine(GameObject go, CanvasGroup cg, int id)
|
||||
{
|
||||
if (go == null || cg == null) yield break;
|
||||
|
||||
int toggles = Mathf.Max(2, buffIconExitFlashCount * 2);
|
||||
float step = Mathf.Max(0.005f, buffIconExitFlashDuration / toggles);
|
||||
|
||||
cg.alpha = 1f;
|
||||
for (int i = 0; i < toggles; i++)
|
||||
{
|
||||
if (go == null || cg == null) yield break;
|
||||
cg.alpha = (i % 2 == 0) ? 0.08f : 1f;
|
||||
yield return new WaitForSeconds(step);
|
||||
}
|
||||
|
||||
if (go != null && cg != null) cg.alpha = 0f;
|
||||
_iconExitCoroutines.Remove(id);
|
||||
_iconEntryCoroutines.Remove(id);
|
||||
if (go != null) Destroy(go);
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,156 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f2e1a192758189c49b8ef48c7a471107
|
||||
TextureImporter:
|
||||
internalIDToNameTable:
|
||||
- first:
|
||||
213: 2978455473066875312
|
||||
second: "\u5E78\u8FD0\u5148\u673A_0"
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 0
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 1
|
||||
wrapV: 1
|
||||
wrapW: 1
|
||||
nPOTScale: 0
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 2
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 1
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 8
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
swizzle: 50462976
|
||||
cookieLightType: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 4
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: Android
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites:
|
||||
- serializedVersion: 2
|
||||
name: "\u5E78\u8FD0\u5148\u673A_0"
|
||||
rect:
|
||||
serializedVersion: 2
|
||||
x: 0
|
||||
y: 0
|
||||
width: 200
|
||||
height: 200
|
||||
alignment: 0
|
||||
pivot: {x: 0, y: 0}
|
||||
border: {x: 0, y: 0, z: 0, w: 0}
|
||||
customData:
|
||||
outline: []
|
||||
physicsShape: []
|
||||
tessellationDetail: -1
|
||||
bones: []
|
||||
spriteID: 0b91873e879955920800000000000000
|
||||
internalID: 2978455473066875312
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
outline: []
|
||||
customData:
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spriteCustomMetadata:
|
||||
entries: []
|
||||
nameFileIdTable:
|
||||
"\u5E78\u8FD0\u5148\u673A_0": 2978455473066875312
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+5
-2
@@ -144,9 +144,9 @@ public class iNumberPrefabController : MonoBehaviour
|
||||
WarnMissingPrefabOnce();
|
||||
return;
|
||||
}
|
||||
if (parent == null)
|
||||
if (parent == null || !parent.gameObject.activeInHierarchy)
|
||||
{
|
||||
WarnMissingParentsOnce();
|
||||
// If the parent is inactive (e.g., ally slot not active), skip spawning to avoid coroutine errors
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -158,6 +158,9 @@ public class iNumberPrefabController : MonoBehaviour
|
||||
if (prefab.activeSelf) prefab.SetActive(false);
|
||||
}
|
||||
|
||||
// Ensure the newly created object is active before playing animation
|
||||
if (!go.activeInHierarchy) go.SetActive(true);
|
||||
|
||||
var view = go.GetComponent<playerInstantNumbersPrefab>();
|
||||
if (view != null) view.Play(type, signedValue);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ using UnityEngine.Serialization;
|
||||
using UnityEngine.SceneManagement;
|
||||
using System.Collections;
|
||||
using UnityEngine.Audio;
|
||||
using UnityEditor;
|
||||
|
||||
public class settlementController : MonoBehaviour
|
||||
{
|
||||
@@ -30,12 +29,17 @@ public class settlementController : MonoBehaviour
|
||||
[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;
|
||||
@@ -54,6 +58,12 @@ public class settlementController : MonoBehaviour
|
||||
[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;
|
||||
|
||||
// Documentation text normalized.
|
||||
[Header("Inspector")]
|
||||
public Text perfectHitCount_Text;
|
||||
@@ -104,8 +114,32 @@ public class settlementController : MonoBehaviour
|
||||
|
||||
private Coroutine musicTransitionCoroutine;
|
||||
private Coroutine canvasFadeCoroutine;
|
||||
private Coroutine settlementIntroCoroutine;
|
||||
private bool settlementUiInitialized = false;
|
||||
|
||||
[Header("Settlement Intro Animation")]
|
||||
[SerializeField] private bool enableDetailedSettlementIntro = true;
|
||||
[SerializeField] private int introFlashCount = 2;
|
||||
[SerializeField] private float introFlashDuration = 0.24f;
|
||||
[SerializeField] private float introMoveDuration = 0.42f;
|
||||
[SerializeField] private float introNumberDuration = 0.48f;
|
||||
[SerializeField] private float introStepGap = 0.06f;
|
||||
|
||||
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 void Awake()
|
||||
{
|
||||
// Documentation text normalized.
|
||||
@@ -166,6 +200,12 @@ public class settlementController : MonoBehaviour
|
||||
StopCoroutine(canvasFadeCoroutine);
|
||||
canvasFadeCoroutine = null;
|
||||
}
|
||||
|
||||
if (settlementIntroCoroutine != null)
|
||||
{
|
||||
StopCoroutine(settlementIntroCoroutine);
|
||||
settlementIntroCoroutine = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Start is called once before the first execution of Update after the MonoBehaviour is created
|
||||
@@ -210,32 +250,62 @@ public class settlementController : MonoBehaviour
|
||||
songName_Text.text = bmm.parsedTitle;
|
||||
thisSong_backPic.sprite = bmm.assignedSongData.fullscreen_songPicture;
|
||||
|
||||
finalScore_Text.text = (sm.allSum_pmScore + sm.allSum_idolScore).ToString();
|
||||
pmScoreSum_Text.text = sm.allSum_pmScore.ToString();
|
||||
idolScoreSum_Text.text = sm.allSum_idolScore.ToString();
|
||||
thisLevel_currentPercentage_Text.text = ((float)(sm.allSum_pmScore + sm.allSum_idolScore) / _1000000 * 100).ToString("F2") + "%";
|
||||
thisLevel_progressBar_Image.fillAmount = (float)(sm.allSum_pmScore + sm.allSum_idolScore) / _1000000;
|
||||
targetPmScore = sm.allSum_pmScore;
|
||||
targetIdolScore = sm.allSum_idolScore;
|
||||
targetTotalScore = targetPmScore + targetIdolScore;
|
||||
targetTotalPercent = (float)targetTotalScore / Mathf.Max(1, _1000000) * 100f;
|
||||
|
||||
int noteCountSum = sm.countPerfect + sm.countGreat + sm.countGood + sm.countMiss;
|
||||
finalScore_Text.text = targetTotalScore.ToString();
|
||||
pmScoreSum_Text.text = targetPmScore.ToString();
|
||||
|
||||
perfectHitCount_Text.text = sm.countPerfect.ToString();
|
||||
greatHitCount_Text.text = sm.countGreat.ToString();
|
||||
goodHitCount_Text.text = sm.countGood.ToString();
|
||||
missHitCount_Text.text = sm.countMiss.ToString();
|
||||
// 根据总分从 rankConfig 获取等级图标并更新 finalLevel_img
|
||||
if (rankConfig != null && finalLevel_img != null)
|
||||
{
|
||||
finalLevel_img.sprite = rankConfig.GetRankSprite(targetTotalScore);
|
||||
Debug.Log($"[settlementController] 根据总分 {targetTotalScore} 更新等级图标");
|
||||
}
|
||||
|
||||
perfect_barFill_Image.fillAmount = (float)sm.countPerfect / noteCountSum;
|
||||
great_barFill_Image.fillAmount = (float)sm.countGreat / noteCountSum;
|
||||
good_barFill_Image.fillAmount = (float)sm.countGood / noteCountSum;
|
||||
miss_barFill_Image.fillAmount = (float)sm.countMiss / noteCountSum;
|
||||
idolScoreSum_Text.text = targetIdolScore.ToString();
|
||||
thisLevel_currentPercentage_Text.text = targetTotalPercent.ToString("F2") + "%";
|
||||
thisLevel_progressBar_Image.fillAmount = (float)targetTotalScore / Mathf.Max(1, _1000000);
|
||||
|
||||
gameNote_rate.text = "已完成 " + noteCountSum.ToString() + "/" + bmm.parsedNoteAmount.ToString();
|
||||
int noteCountRaw = sm.countPerfect + sm.countGreat + sm.countGood + sm.countMiss;
|
||||
int noteCountSum = Mathf.Max(1, noteCountRaw);
|
||||
|
||||
perfectHitPercent_Text.text = ((float)sm.countPerfect / noteCountSum * 100).ToString("F1") + "%";
|
||||
greatHitPercent_Text.text = ((float)sm.countGreat / noteCountSum * 100).ToString("F1") + "%";
|
||||
goodHitPercent_Text.text = ((float)sm.countGood / noteCountSum * 100).ToString("F1") + "%";
|
||||
missHitPercent_Text.text = ((float)sm.countMiss / noteCountSum * 100).ToString("F1") + "%";
|
||||
targetPerfectCount = sm.countPerfect;
|
||||
targetGreatCount = sm.countGreat;
|
||||
targetGoodCount = sm.countGood;
|
||||
targetMissCount = sm.countMiss;
|
||||
|
||||
accuracy_Text.text = ((float)(sm.countPerfect * perfect_weight + sm.countGreat * great_weight + sm.countGood * good_weight + sm.countMiss * miss_weight) / noteCountSum * 100).ToString("F3") + "%";
|
||||
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);
|
||||
|
||||
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("F3") + "%";
|
||||
|
||||
// Timing Statistics
|
||||
if (earlyHitCount_Text != null) earlyHitCount_Text.text = sm.countEarly.ToString();
|
||||
@@ -249,7 +319,7 @@ public class settlementController : MonoBehaviour
|
||||
else Debug.LogError("score manager is null");
|
||||
|
||||
// --- Achievement System: Load and display achievements ---
|
||||
if (InGamePerformanceManager.Instance != null)
|
||||
if (InGamePerformanceManager.Instance != null && !GameConfig.autoPlayEnabled)
|
||||
{
|
||||
// Calculate total cure, damage and mana from teamUIController
|
||||
float totalCure = 0;
|
||||
@@ -299,13 +369,27 @@ public class settlementController : MonoBehaviour
|
||||
getThisSong_info(); // ensure thisSong_so set
|
||||
if (thisSong_so != null && bmm != null)
|
||||
{
|
||||
int oldPersonalRecord = thisSong_so.personalRecord;
|
||||
int diff = bmm.assignedDifficulty;
|
||||
if (diff >= 0)
|
||||
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);
|
||||
}
|
||||
|
||||
if (personalRecord_Text != null)
|
||||
{
|
||||
int currentRecord = thisSong_so.personalRecord;
|
||||
personalRecord_Text.text = currentRecord.ToString();
|
||||
|
||||
// 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
|
||||
@@ -330,8 +414,177 @@ public class settlementController : MonoBehaviour
|
||||
SetMvpHeroImageFromTopScorer();
|
||||
StartMusicTransition();
|
||||
|
||||
// Documentation text normalized.
|
||||
StartCanvasFadeIn();
|
||||
// Play settlement entry animation flow.
|
||||
StartSettlementIntro();
|
||||
}
|
||||
|
||||
private void StartSettlementIntro()
|
||||
{
|
||||
if (!enableDetailedSettlementIntro)
|
||||
{
|
||||
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");
|
||||
RectTransform teamDisplayingRt = FindRectByName(rightRoot != null ? rightRoot : settleRoot, "teamDisplaying");
|
||||
|
||||
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) { SetAnchoredX(teamDisplayingRt, 2265f); SetCanvasAlpha(teamDisplayingRt, 0f); }
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
PrepareTextForCountUp(pmScoreSum_Text, false, 0);
|
||||
PrepareTextForCountUp(idolScoreSum_Text, false, 0);
|
||||
PrepareTextForCountUp(accuracy_Text, true, 0f, "F3");
|
||||
PrepareTextForCountUp(finalScore_Text, false, 0);
|
||||
PrepareTextForCountUp(thisLevel_currentPercentage_Text, true, 0f, "F2");
|
||||
PrepareTextForCountUp(perfectHitCount_Text, false, 0);
|
||||
PrepareTextForCountUp(greatHitCount_Text, false, 0);
|
||||
PrepareTextForCountUp(goodHitCount_Text, false, 0);
|
||||
PrepareTextForCountUp(missHitCount_Text, false, 0);
|
||||
PrepareTextForCountUp(perfectHitPercent_Text, true, 0f, "F1");
|
||||
PrepareTextForCountUp(greatHitPercent_Text, true, 0f, "F1");
|
||||
PrepareTextForCountUp(goodHitPercent_Text, true, 0f, "F1");
|
||||
PrepareTextForCountUp(missHitPercent_Text, true, 0f, "F1");
|
||||
|
||||
// settlement: flash + scale 1.2 -> 1.0
|
||||
yield return StartCoroutine(FlashCanvasGroupWithScale(settlementCanvasGroup, settleRoot, settleBaseScale * 1.2f, settleBaseScale, introFlashCount, introFlashDuration));
|
||||
|
||||
// right/right_bottom: X 1799 -> 1677.75
|
||||
if (rightBottomRt != null)
|
||||
yield return StartCoroutine(AnimateAnchoredXAndFade(rightBottomRt, 1799f, 1677.75f, introMoveDuration, false));
|
||||
|
||||
// left/pics/quhuiImage flash
|
||||
if (quhuiImageTr != null)
|
||||
yield return StartCoroutine(FlashInTransform(quhuiImageTr, introFlashCount, introFlashDuration));
|
||||
|
||||
// score_bottom + shenstars move in together
|
||||
Coroutine scoreBottomIn = null;
|
||||
Coroutine shenstarsIn = null;
|
||||
if (scoreBottomRt != null)
|
||||
scoreBottomIn = StartCoroutine(AnimateAnchoredXAndFade(scoreBottomRt, -1646.9f, -432.4f, introMoveDuration, true));
|
||||
if (shenstarsRt != null)
|
||||
shenstarsIn = StartCoroutine(AnimateAnchoredXAndFade(shenstarsRt, -1602.1f, -437.5785f, introMoveDuration, true));
|
||||
if (scoreBottomIn != null || shenstarsIn != null)
|
||||
yield return WaitRealtime(introMoveDuration + 0.02f);
|
||||
|
||||
// reward: Y -494.3 -> 0 with fade
|
||||
if (rewardRt != null)
|
||||
yield return StartCoroutine(AnimateAnchoredYAndFade(rewardRt, -494.3f, 0f, introMoveDuration));
|
||||
|
||||
// score numbers: pm -> idol -> accuracy
|
||||
yield return StartCoroutine(AnimateIntText(pmScoreSum_Text, targetPmScore, introNumberDuration));
|
||||
yield return StartCoroutine(AnimateIntText(idolScoreSum_Text, targetIdolScore, introNumberDuration));
|
||||
yield return StartCoroutine(AnimateFloatPercentText(accuracy_Text, targetAccuracyPercent, introNumberDuration, "F3"));
|
||||
|
||||
// total + percent after above, and flash once completed
|
||||
Coroutine totalScoreIn = StartCoroutine(AnimateIntText(finalScore_Text, targetTotalScore, introNumberDuration));
|
||||
Coroutine totalPercentIn = StartCoroutine(AnimateFloatPercentText(thisLevel_currentPercentage_Text, targetTotalPercent, introNumberDuration, "F2"));
|
||||
if (totalScoreIn != null || totalPercentIn != null)
|
||||
yield return WaitRealtime(introNumberDuration + 0.02f);
|
||||
yield return StartCoroutine(FlashInTransform(finalScore_Text != null ? finalScore_Text.transform : null, introFlashCount, introFlashDuration * 0.9f));
|
||||
yield return StartCoroutine(FlashInTransform(thisLevel_currentPercentage_Text != null ? thisLevel_currentPercentage_Text.transform : null, introFlashCount, introFlashDuration * 0.9f));
|
||||
|
||||
// hitStatus/status groups: flash in and count up numbers
|
||||
yield return StartCoroutine(PlayStatusGroupIn(perfectGroup, perfectHitCount_Text, targetPerfectCount, perfectHitPercent_Text, targetPerfectPercent));
|
||||
yield return StartCoroutine(PlayStatusGroupIn(greatGroup, greatHitCount_Text, targetGreatCount, greatHitPercent_Text, targetGreatPercent));
|
||||
yield return StartCoroutine(PlayStatusGroupIn(goodGroup, goodHitCount_Text, targetGoodCount, goodHitPercent_Text, targetGoodPercent));
|
||||
yield return StartCoroutine(PlayStatusGroupIn(missGroup, missHitCount_Text, targetMissCount, missHitPercent_Text, targetMissPercent));
|
||||
|
||||
// heroDApic/MASK/lihui flash + teamDisplaying move/fade in
|
||||
if (lihuiTr != null)
|
||||
yield return StartCoroutine(FlashInTransform(lihuiTr, introFlashCount, introFlashDuration));
|
||||
if (teamDisplayingRt != null)
|
||||
yield return StartCoroutine(AnimateAnchoredXAndFade(teamDisplayingRt, 2265f, 1490.09f, introMoveDuration, true));
|
||||
|
||||
// right/buttons: each button flashes in sequence
|
||||
for (int i = 0; i < rightButtons.Count; i++)
|
||||
{
|
||||
Transform btn = rightButtons[i];
|
||||
if (btn == null) continue;
|
||||
yield return StartCoroutine(FlashInTransform(btn, introFlashCount, introFlashDuration));
|
||||
yield return WaitRealtime(introStepGap);
|
||||
}
|
||||
|
||||
settlementCanvasGroup.alpha = 1f;
|
||||
settlementCanvasGroup.interactable = true;
|
||||
settlementCanvasGroup.blocksRaycasts = true;
|
||||
settlementIntroCoroutine = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -556,6 +809,280 @@ public class settlementController : MonoBehaviour
|
||||
canvasFadeCoroutine = null;
|
||||
}
|
||||
|
||||
private IEnumerator PlayStatusGroupIn(Transform groupRoot, Text countText, int countTarget, Text percentText, float percentTarget)
|
||||
{
|
||||
if (groupRoot != null)
|
||||
yield return StartCoroutine(FlashInTransform(groupRoot, introFlashCount, introFlashDuration));
|
||||
|
||||
Coroutine c1 = null;
|
||||
Coroutine c2 = null;
|
||||
if (countText != null) c1 = StartCoroutine(AnimateIntText(countText, countTarget, introNumberDuration));
|
||||
if (percentText != null) c2 = StartCoroutine(AnimateFloatPercentText(percentText, percentTarget, introNumberDuration, "F1"));
|
||||
|
||||
if (c1 != null || c2 != null)
|
||||
yield return WaitRealtime(introNumberDuration + 0.02f);
|
||||
}
|
||||
|
||||
private IEnumerator AnimateIntText(Text target, int toValue, float duration)
|
||||
{
|
||||
if (target == null) yield break;
|
||||
SetCanvasAlpha(target.transform, 1f);
|
||||
|
||||
float elapsed = 0f;
|
||||
float dur = Mathf.Max(0.01f, duration);
|
||||
|
||||
while (elapsed < dur)
|
||||
{
|
||||
elapsed += Time.unscaledDeltaTime;
|
||||
float t = EaseOutCubic(Mathf.Clamp01(elapsed / dur));
|
||||
int value = Mathf.RoundToInt(Mathf.Lerp(0f, toValue, t));
|
||||
target.text = value.ToString();
|
||||
yield return null;
|
||||
}
|
||||
|
||||
target.text = toValue.ToString();
|
||||
}
|
||||
|
||||
private IEnumerator AnimateFloatPercentText(Text target, float toValue, float duration, string format)
|
||||
{
|
||||
if (target == null) yield break;
|
||||
SetCanvasAlpha(target.transform, 1f);
|
||||
|
||||
float elapsed = 0f;
|
||||
float dur = Mathf.Max(0.01f, duration);
|
||||
|
||||
while (elapsed < dur)
|
||||
{
|
||||
elapsed += Time.unscaledDeltaTime;
|
||||
float t = EaseOutCubic(Mathf.Clamp01(elapsed / dur));
|
||||
float value = Mathf.Lerp(0f, toValue, t);
|
||||
target.text = value.ToString(format) + "%";
|
||||
yield return null;
|
||||
}
|
||||
|
||||
target.text = toValue.ToString(format) + "%";
|
||||
}
|
||||
|
||||
private IEnumerator AnimateAnchoredXAndFade(RectTransform rt, float fromX, float toX, float duration, bool fadeIn)
|
||||
{
|
||||
if (rt == null) yield break;
|
||||
|
||||
SetAnchoredX(rt, fromX);
|
||||
if (fadeIn) SetCanvasAlpha(rt, 0f);
|
||||
|
||||
float elapsed = 0f;
|
||||
float dur = Mathf.Max(0.01f, duration);
|
||||
while (elapsed < dur)
|
||||
{
|
||||
elapsed += Time.unscaledDeltaTime;
|
||||
float t = EaseOutCubic(Mathf.Clamp01(elapsed / dur));
|
||||
SetAnchoredX(rt, Mathf.LerpUnclamped(fromX, toX, t));
|
||||
if (fadeIn) SetCanvasAlpha(rt, t);
|
||||
yield return null;
|
||||
}
|
||||
|
||||
SetAnchoredX(rt, toX);
|
||||
if (fadeIn) SetCanvasAlpha(rt, 1f);
|
||||
}
|
||||
|
||||
private IEnumerator AnimateAnchoredYAndFade(RectTransform rt, float fromY, float toY, float duration)
|
||||
{
|
||||
if (rt == null) yield break;
|
||||
|
||||
SetAnchoredY(rt, fromY);
|
||||
SetCanvasAlpha(rt, 0f);
|
||||
|
||||
float elapsed = 0f;
|
||||
float dur = Mathf.Max(0.01f, duration);
|
||||
while (elapsed < dur)
|
||||
{
|
||||
elapsed += Time.unscaledDeltaTime;
|
||||
float t = EaseOutCubic(Mathf.Clamp01(elapsed / dur));
|
||||
SetAnchoredY(rt, Mathf.LerpUnclamped(fromY, toY, t));
|
||||
SetCanvasAlpha(rt, t);
|
||||
yield return null;
|
||||
}
|
||||
|
||||
SetAnchoredY(rt, toY);
|
||||
SetCanvasAlpha(rt, 1f);
|
||||
}
|
||||
|
||||
private IEnumerator FlashCanvasGroupWithScale(CanvasGroup cg, Transform scaleTarget, Vector3 fromScale, Vector3 toScale, int flashes, float duration)
|
||||
{
|
||||
if (cg == null) yield break;
|
||||
int count = Mathf.Max(1, flashes);
|
||||
float total = Mathf.Max(0.05f, duration);
|
||||
float flashWindow = total / Mathf.Max(1, count * 2);
|
||||
|
||||
cg.alpha = 0f;
|
||||
if (scaleTarget != null) scaleTarget.localScale = fromScale;
|
||||
|
||||
float elapsed = 0f;
|
||||
while (elapsed < total)
|
||||
{
|
||||
elapsed += Time.unscaledDeltaTime;
|
||||
float p = Mathf.Clamp01(elapsed / total);
|
||||
|
||||
if (scaleTarget != null)
|
||||
{
|
||||
float k = EaseOutCubic(p);
|
||||
scaleTarget.localScale = Vector3.LerpUnclamped(fromScale, toScale, k);
|
||||
}
|
||||
|
||||
if (flashWindow > 0f)
|
||||
{
|
||||
int seg = Mathf.FloorToInt(elapsed / flashWindow);
|
||||
bool on = (seg % 2) == 0;
|
||||
cg.alpha = on ? 1f : 0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
cg.alpha = 1f;
|
||||
}
|
||||
|
||||
yield return null;
|
||||
}
|
||||
|
||||
cg.alpha = 1f;
|
||||
if (scaleTarget != null) scaleTarget.localScale = toScale;
|
||||
}
|
||||
|
||||
private IEnumerator FlashInTransform(Transform target, int flashes, float duration)
|
||||
{
|
||||
if (target == null) yield break;
|
||||
int count = Mathf.Max(1, flashes);
|
||||
float total = Mathf.Max(0.05f, duration);
|
||||
float step = total / Mathf.Max(1, count * 2);
|
||||
|
||||
SetCanvasAlpha(target, 0f);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
SetCanvasAlpha(target, 1f);
|
||||
yield return WaitRealtime(step);
|
||||
|
||||
if (i < count - 1)
|
||||
{
|
||||
SetCanvasAlpha(target, 0f);
|
||||
yield return WaitRealtime(step);
|
||||
}
|
||||
}
|
||||
|
||||
SetCanvasAlpha(target, 1f);
|
||||
}
|
||||
|
||||
private void PrepareTextForCountUp(Text t, bool isPercent, float initialValue, string format = "F1")
|
||||
{
|
||||
if (t == null) return;
|
||||
SetCanvasAlpha(t.transform, 0f);
|
||||
t.text = isPercent ? initialValue.ToString(format) + "%" : Mathf.RoundToInt(initialValue).ToString();
|
||||
}
|
||||
|
||||
private void PrepareTextForCountUp(Text t, bool isPercent, int initialValue)
|
||||
{
|
||||
if (t == null) return;
|
||||
SetCanvasAlpha(t.transform, 0f);
|
||||
t.text = isPercent ? initialValue.ToString("F1") + "%" : initialValue.ToString();
|
||||
}
|
||||
|
||||
private static float EaseOutCubic(float t)
|
||||
{
|
||||
float inv = 1f - t;
|
||||
return 1f - inv * inv * inv;
|
||||
}
|
||||
|
||||
private static IEnumerator WaitRealtime(float duration)
|
||||
{
|
||||
float elapsed = 0f;
|
||||
float dur = Mathf.Max(0f, duration);
|
||||
while (elapsed < dur)
|
||||
{
|
||||
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>
|
||||
@@ -783,17 +1310,34 @@ public class settlementController : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
if (heroSO != null && heroSO.ally_hero_HD_image != null)
|
||||
if (heroSO != 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);
|
||||
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();
|
||||
|
||||
Debug.Log($"[SettlementController] MVP assigned from slot {topIndex+1} with score {max}: {heroSO.ally_heroName}");
|
||||
}
|
||||
else
|
||||
{
|
||||
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);
|
||||
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);
|
||||
}
|
||||
Debug.LogWarning("[SettlementController] MVP SO or HD image not found");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user