成就系统 巨大修改 新的ui 黑白效果 等一堆
This commit is contained in:
@@ -39,13 +39,17 @@ public class Animation_GenerateJudgementSituationPrefab : MonoBehaviour
|
||||
private Transform yellowSpawn;
|
||||
private Transform purpleSpawn;
|
||||
private Transform blueSpawn;
|
||||
|
||||
// Track last scene to detect scene changes
|
||||
private int lastSceneIndex = -1;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance == null)
|
||||
{
|
||||
Instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
// 注意:仅当第一次创建时保留,允许重新加载
|
||||
// DontDestroyOnLoad(gameObject); // ← 移除此行以允许场景重新加载时的清理
|
||||
}
|
||||
else if (Instance != this)
|
||||
{
|
||||
@@ -54,29 +58,48 @@ public class Animation_GenerateJudgementSituationPrefab : MonoBehaviour
|
||||
}
|
||||
|
||||
CacheSpawnPoints();
|
||||
lastSceneIndex = UnityEngine.SceneManagement.SceneManager.GetActiveScene().buildIndex;
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// Ensure spawn points are cached after all scene objects are initialized
|
||||
if (redSpawn == null && redEffect != null)
|
||||
{
|
||||
CacheSpawnPoints();
|
||||
}
|
||||
CacheSpawnPointsIfNeeded();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
// In case this object survives scene changes / references are assigned late.
|
||||
if (Instance == null)
|
||||
{
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
if (redSpawn == null && redEffect != null)
|
||||
// Detect scene reload and refresh cache
|
||||
int currentSceneIndex = UnityEngine.SceneManagement.SceneManager.GetActiveScene().buildIndex;
|
||||
if (currentSceneIndex != lastSceneIndex)
|
||||
{
|
||||
if (GameConfig.verboseLogs) Debug.Log($"[Animation_Generate] Scene changed detected (from {lastSceneIndex} to {currentSceneIndex}), refreshing spawn point cache");
|
||||
lastSceneIndex = currentSceneIndex;
|
||||
CacheSpawnPoints();
|
||||
}
|
||||
else
|
||||
{
|
||||
CacheSpawnPointsIfNeeded();
|
||||
}
|
||||
}
|
||||
|
||||
private void CacheSpawnPointsIfNeeded()
|
||||
{
|
||||
// Only recache if any of the references appear to be invalid (destroyed)
|
||||
if (redSpawn == null || greenSpawn == null || yellowSpawn == null || purpleSpawn == null || blueSpawn == null)
|
||||
{
|
||||
// Check if the effect GameObjects are still valid
|
||||
bool anyInvalid = (redEffect == null || redEffect.transform == null) ||
|
||||
(greenEffect == null || greenEffect.transform == null) ||
|
||||
(yellowEffect == null || yellowEffect.transform == null) ||
|
||||
(purpleEffect == null || purpleEffect.transform == null) ||
|
||||
(blueEffect == null || blueEffect.transform == null);
|
||||
|
||||
if (anyInvalid || redSpawn == null)
|
||||
{
|
||||
CacheSpawnPoints();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CacheSpawnPoints()
|
||||
@@ -217,12 +240,8 @@ public class Animation_GenerateJudgementSituationPrefab : MonoBehaviour
|
||||
{
|
||||
if (string.IsNullOrEmpty(color)) return null;
|
||||
|
||||
// If references were destroyed (Unity fake null) try to rebuild cache once.
|
||||
if (redSpawn == null && greenSpawn == null && yellowSpawn == null && purpleSpawn == null && blueSpawn == null)
|
||||
{
|
||||
if (GameConfig.verboseLogs) Debug.Log("[Animation_Generate] Rebuilding spawn point cache");
|
||||
CacheSpawnPoints();
|
||||
}
|
||||
// 在每次调用时检查是否需要重新缓存
|
||||
CacheSpawnPointsIfNeeded();
|
||||
|
||||
Transform point = null;
|
||||
switch (color.ToLower())
|
||||
|
||||
@@ -36,14 +36,19 @@ public class BeatmapManager : MonoBehaviour
|
||||
Debug.LogWarning($"BeatmapManager.AcceptSongData: accepted song={(song==null?"NULL":song.songName)}, difficulty={difficulty}");
|
||||
}
|
||||
|
||||
public Beatmap beatmap; // 当前谱面数据
|
||||
public Beatmap beatmap; // ��ǰ��������
|
||||
|
||||
// 音符生成器引用
|
||||
// ��������������
|
||||
public NoteSpawner noteSpawner;
|
||||
|
||||
// teamUIController 引用,直接拖拽赋值
|
||||
// teamUIController ���ã�ֱ����ק��ֵ
|
||||
public teamUIController uiController;
|
||||
|
||||
public Image bgSpriteImage; // ����ͼƬ����
|
||||
|
||||
public GameObject bgSpriteObject;
|
||||
public Image startCanvas_image;
|
||||
|
||||
// Extra fields from beatmap JSON (stored temporarily)
|
||||
[HideInInspector] public string parsedTitle;
|
||||
[HideInInspector] public string parsedComposer;
|
||||
@@ -77,7 +82,7 @@ public class BeatmapManager : MonoBehaviour
|
||||
public float imMultiplier = 3f;
|
||||
|
||||
[Header("HP Calculation Settings")]
|
||||
[Tooltip("Scales the note amount to enemy HP. Example: NoteAmount * MaxMultiplier * BaseScale ≈ TotalHP")]
|
||||
[Tooltip("Scales the note amount to enemy HP. Example: NoteAmount * MaxMultiplier * BaseScale �� TotalHP")]
|
||||
public float baseHpUnitScale = 1f;
|
||||
|
||||
// Total chart score and per-note score fields
|
||||
@@ -85,23 +90,23 @@ public class BeatmapManager : MonoBehaviour
|
||||
public int perNoteScore;
|
||||
public int leftoverScore;
|
||||
|
||||
// 从 JSON 文件加载谱面数据
|
||||
// �� JSON �������������
|
||||
public void LoadBeatmap(string fileName)
|
||||
{
|
||||
string path = Application.streamingAssetsPath + "/" + fileName;
|
||||
if (File.Exists(path))
|
||||
{
|
||||
string json = File.ReadAllText(path);
|
||||
Debug.Log("谱面 JSON 已读取: " + path);
|
||||
Debug.Log("���� JSON �Ѷ�ȡ: " + path);
|
||||
ProcessJsonAndLoad(json);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("文件不存在:" + path);
|
||||
Debug.LogError("�ļ������ڣ�" + path);
|
||||
}
|
||||
}
|
||||
|
||||
// 从原始 JSON 字符串加载谱面(允许任意磁盘路径先读取后传入)
|
||||
// ��ԭʼ JSON �ַ����������棨�����������·���ȶ�ȡ���룩
|
||||
public void LoadBeatmapFromJsonString(string json)
|
||||
{
|
||||
if (string.IsNullOrEmpty(json))
|
||||
@@ -112,7 +117,7 @@ public class BeatmapManager : MonoBehaviour
|
||||
ProcessJsonAndLoad(json);
|
||||
}
|
||||
|
||||
// 解析 JSON 但不启动 NoteSpawner(用于 test mode 的延迟开始)
|
||||
// ���� JSON �������� NoteSpawner������ test mode ���ӳٿ�ʼ��
|
||||
public bool ParseJsonOnly(string json)
|
||||
{
|
||||
if (string.IsNullOrEmpty(json))
|
||||
@@ -129,13 +134,13 @@ public class BeatmapManager : MonoBehaviour
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.LogError($"ParseJsonOnly 解析 Beatmap 失败: {ex}");
|
||||
Debug.LogError($"ParseJsonOnly ���� Beatmap ʧ��: {ex}");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parsed == null)
|
||||
{
|
||||
Debug.LogError("ParseJsonOnly: 解析 Beatmap 返回 null");
|
||||
Debug.LogError("ParseJsonOnly: ���� Beatmap ���� null");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -203,22 +208,22 @@ public class BeatmapManager : MonoBehaviour
|
||||
beatmap = parsed;
|
||||
Debug.Log("ParseJsonOnly: parsed beatmap " + beatmap.title);
|
||||
|
||||
// 调用统一的敌人处理逻辑(包含 HP 计算和设置)
|
||||
// ����ͳһ�ĵ��˴����������� HP ��������ã�
|
||||
SetupEnemiesAndHP();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// 直接传入已经解析的 Beatmap(兼容旧调用)
|
||||
// ֱ�Ӵ����Ѿ������� Beatmap�����ݾɵ��ã�
|
||||
public void LoadBeatmap(Beatmap loadedBeatmap)
|
||||
{
|
||||
beatmap = loadedBeatmap;
|
||||
Debug.Log("谱面已加载(来自 Beatmap 对象):" + (beatmap != null ? beatmap.title : "null"));
|
||||
Debug.Log("�����Ѽ��أ����� Beatmap ����" + (beatmap != null ? beatmap.title : "null"));
|
||||
// No globalDelaySeconds available in this path. Call NoteSpawner directly.
|
||||
noteSpawner.LoadBeatmap(beatmap);
|
||||
}
|
||||
|
||||
// 读取并加载当前谱面到音符生成器(备用)
|
||||
// ��ȡ�����ص�ǰ���浽���������������ã�
|
||||
public void LoadBeatmapFromFile()
|
||||
{
|
||||
string path = Application.streamingAssetsPath + "/Emilia_demo.json";
|
||||
@@ -229,11 +234,11 @@ public class BeatmapManager : MonoBehaviour
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("谱面文件未找到!");
|
||||
Debug.LogError("�����ļ�δ�ҵ���");
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:从 TextAsset 解析并根据 parseOnly 决定是否只是解析或直接加载
|
||||
// �������� TextAsset ���������� parseOnly �����Ƿ�ֻ�ǽ�����ֱ�Ӽ���
|
||||
public bool LoadBeatmapFromTextAsset(TextAsset chartAsset, bool parseOnly = false)
|
||||
{
|
||||
if (chartAsset == null)
|
||||
@@ -257,7 +262,7 @@ public class BeatmapManager : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:从 SongData 的 chart TextAsset 加载谱面
|
||||
// �������� SongData �� chart TextAsset ��������
|
||||
public bool LoadBeatmapFromSongData(SongData song, int difficulty, bool parseOnly = false)
|
||||
{
|
||||
if (song == null)
|
||||
@@ -275,18 +280,18 @@ public class BeatmapManager : MonoBehaviour
|
||||
return LoadBeatmapFromTextAsset(ta, parseOnly);
|
||||
}
|
||||
|
||||
// 在 Start() 方法中初始化
|
||||
// �� Start() �����г�ʼ��
|
||||
void Start()
|
||||
{
|
||||
// 被动接收上一个场景传递过来的 SongData(通过 BeatmapManager.pendingSongData)
|
||||
// ����������һ���������ݹ����� SongData��ͨ�� BeatmapManager.pendingSongData��
|
||||
if (pendingSongData != null)
|
||||
{
|
||||
Debug.LogWarning($"BeatmapManager.Start: pendingSongData detected: {pendingSongData.songName}, difficulty={pendingDifficulty}");
|
||||
// 将 pending 显示到 Inspector 字段,便于检查
|
||||
// �� pending ��ʾ�� Inspector �ֶΣ����ڼ��
|
||||
assignedSongData = pendingSongData;
|
||||
assignedDifficulty = pendingDifficulty;
|
||||
|
||||
// 尝试解析(只解析,不立即生成音符)
|
||||
// ���Խ�����ֻ����������������������
|
||||
bool ok = LoadBeatmapFromSongData(assignedSongData, assignedDifficulty, true);
|
||||
if (!ok) Debug.LogWarning("BeatmapManager.Start: LoadBeatmapFromSongData failed");
|
||||
else
|
||||
@@ -328,6 +333,41 @@ public class BeatmapManager : MonoBehaviour
|
||||
Debug.LogWarning("GameManager or its musicSource not found; audio not assigned");
|
||||
}
|
||||
|
||||
// Assign fullscreen image from SongData to bgSprite if available
|
||||
if (assignedSongData != null && assignedSongData.fullscreen_songPicture != null)
|
||||
{
|
||||
// Prefer setting a Scene GameObject's SpriteRenderer if provided
|
||||
if (bgSpriteObject != null)
|
||||
{
|
||||
var sr = bgSpriteObject.GetComponentInChildren<SpriteRenderer>();
|
||||
if (sr != null)
|
||||
{
|
||||
sr.sprite = assignedSongData.fullscreen_songPicture;
|
||||
// ensure fully visible
|
||||
sr.color = new Color(sr.color.r, sr.color.g, sr.color.b, 1f);
|
||||
if (startCanvas_image != null)
|
||||
{
|
||||
startCanvas_image.sprite = assignedSongData.fullscreen_songPicture;
|
||||
}
|
||||
sr = null;
|
||||
Debug.LogWarning($"Assigned SongData.fullscreen_songPicture to SpriteRenderer on bgSpriteObject for song {assignedSongData.songName}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("bgSpriteObject has no SpriteRenderer in children; cannot assign fullscreen sprite");
|
||||
}
|
||||
}
|
||||
else if (bgSpriteImage != null)
|
||||
{
|
||||
bgSpriteImage.sprite = assignedSongData.fullscreen_songPicture;
|
||||
Debug.LogWarning($"Assigned SongData.fullscreen_songPicture to bgSpriteImage for song {assignedSongData.songName}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("No bg target (bgSpriteObject or bgSpriteImage) assigned; fullscreen sprite not applied");
|
||||
}
|
||||
}
|
||||
|
||||
// Pause the system to show pause overlay (PauseManager will enable overlayRoot)
|
||||
var pauseMgr = PauseManager.Instance ?? FindObjectOfType<PauseManager>();
|
||||
if (pauseMgr != null)
|
||||
@@ -346,20 +386,20 @@ public class BeatmapManager : MonoBehaviour
|
||||
pendingDifficulty = -1;
|
||||
}
|
||||
|
||||
// 如果处于测试模式,则跳过自动加载谱面
|
||||
// ������ڲ���ģʽ���������Զ���������
|
||||
if (GameConfig.testMode)
|
||||
{
|
||||
Debug.Log("Test mode active: skipping automatic beatmap load in BeatmapManager.Start");
|
||||
}
|
||||
else
|
||||
{
|
||||
// 原先默认加载的调试谱面已注释,改为仅在需要时手动恢复
|
||||
// LoadBeatmap("Emilia_demo.json"); // 加载并实例化音符
|
||||
// ԭ��Ĭ�ϼ��صĵ���������ע�ͣ���Ϊ������Ҫʱ�ֶ��ָ�
|
||||
// LoadBeatmap("Emilia_demo.json"); // ���ز�ʵ��������
|
||||
Debug.LogWarning("Default automatic demo beatmap load is commented out to prioritize SO-provided charts.");
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前难度乘数
|
||||
// ��ȡ��ǰ�Ѷȳ���
|
||||
private float GetCurrentDifficultyMultiplier()
|
||||
{
|
||||
if (parsedDifficulty == 1) return ezMultiplier;
|
||||
@@ -369,7 +409,7 @@ public class BeatmapManager : MonoBehaviour
|
||||
return 1f;
|
||||
}
|
||||
|
||||
// 统一处理敌人 ID 提取、发送给 UI 以及 HP 计算的逻辑
|
||||
// ͳһ�������� ID ��ȡ������ UI �Լ� HP �������
|
||||
private void SetupEnemiesAndHP()
|
||||
{
|
||||
if (uiController == null)
|
||||
@@ -438,13 +478,13 @@ public class BeatmapManager : MonoBehaviour
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.LogError($"解析 Beatmap 失败: {ex}");
|
||||
Debug.LogError($"���� Beatmap ʧ��: {ex}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (parsed == null)
|
||||
{
|
||||
Debug.LogError("解析 Beatmap 返回 null");
|
||||
Debug.LogError("���� Beatmap ���� null");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -497,10 +537,13 @@ public class BeatmapManager : MonoBehaviour
|
||||
Debug.LogWarning("BeatmapExtra not found in JSON; populated limited fields from Beatmap.");
|
||||
}
|
||||
|
||||
// Ensure amount and color statistics are populated even if missing from JSON
|
||||
ValidateAndPopulateStatistics(parsed);
|
||||
|
||||
CalculateNoteScores();
|
||||
|
||||
beatmap = parsed;
|
||||
Debug.Log("谱面已加载:" + beatmap.title);
|
||||
Debug.Log("�����Ѽ��أ�" + beatmap.title);
|
||||
|
||||
if (noteSpawner != null) noteSpawner.LoadBeatmap(beatmap);
|
||||
else Debug.LogError("NoteSpawner is null in BeatmapManager.");
|
||||
@@ -524,6 +567,39 @@ public class BeatmapManager : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateAndPopulateStatistics(Beatmap parsed)
|
||||
{
|
||||
if (parsed == null || parsed.notes == null) return;
|
||||
|
||||
// Ensure note amount is correct
|
||||
if (parsedNoteAmount <= 0)
|
||||
{
|
||||
parsedNoteAmount = parsed.notes.Length;
|
||||
if (GameConfig.verboseLogs) Debug.Log($"[BeatmapManager] Calculated missing noteAmount: {parsedNoteAmount}");
|
||||
}
|
||||
|
||||
// Ensure note statistics (counts by color) are populated
|
||||
if (parsedNoteStatistics == null || parsedNoteStatistics.Length == 0)
|
||||
{
|
||||
var counts = new System.Collections.Generic.Dictionary<string, int>();
|
||||
foreach (var note in parsed.notes)
|
||||
{
|
||||
if (string.IsNullOrEmpty(note.color)) continue;
|
||||
string c = note.color.ToLower();
|
||||
if (!counts.ContainsKey(c)) counts[c] = 0;
|
||||
counts[c]++;
|
||||
}
|
||||
|
||||
parsedNoteStatistics = new NoteStatistic[counts.Count];
|
||||
int i = 0;
|
||||
foreach (var kvp in counts)
|
||||
{
|
||||
parsedNoteStatistics[i++] = new NoteStatistic { colorType = kvp.Key, count = kvp.Value };
|
||||
}
|
||||
if (GameConfig.verboseLogs) Debug.Log($"[BeatmapManager] Calculated missing noteStatistics for {counts.Count} colors");
|
||||
}
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
private class BeatmapExtra
|
||||
{
|
||||
|
||||
@@ -3,24 +3,41 @@ using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using System.Collections;
|
||||
using UnityEngine.Networking;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEngineInternal;
|
||||
|
||||
public class GameManager : MonoBehaviour
|
||||
{
|
||||
public BeatmapManager beatmapManager; // ������غ�������
|
||||
public NoteSpawner noteSpawner; // ����������
|
||||
public AudioSource musicSource; // ���ֲ�����
|
||||
public BeatmapManager beatmapManager; // Beatmap manager
|
||||
public NoteSpawner noteSpawner; // Note spawner
|
||||
public AudioSource musicSource; // Music audio source
|
||||
public readyLetsGo readyLetsGo;
|
||||
|
||||
[Header("paths")]
|
||||
// �����������ٴӾ�̬�����ж�ȡ
|
||||
// Paths for external test-mode resources
|
||||
public string JSONPath;
|
||||
public string audioPath;
|
||||
public string bgPicPath;
|
||||
|
||||
// Optional: sprite renderer to show background image
|
||||
[Header("canvas")]
|
||||
public GameObject startCanvas;
|
||||
public CanvasGroup cg_startCanvas;
|
||||
public float canvasFadeTime;
|
||||
private Coroutine fade_canvasGroup;
|
||||
public CanvasGroup settleCG;
|
||||
|
||||
[Header("buttons")]
|
||||
public Button startGame;
|
||||
public Button backtoSelectingPage;
|
||||
|
||||
[Header("sprite renderer")]
|
||||
public SpriteRenderer backgroundRenderer;
|
||||
|
||||
// NEW: UI Text to display banflag content from pressStart
|
||||
[Header("Debug / Test")]
|
||||
public Text banflagTextUI;
|
||||
// new: display current song name on UI when assigning background
|
||||
public Text songnameText;
|
||||
|
||||
[Header("UI Overlays")]
|
||||
public UnityEngine.UI.Image blackMaskImage; // assign in inspector: full-screen black overlay
|
||||
@@ -29,9 +46,25 @@ public class GameManager : MonoBehaviour
|
||||
[Tooltip("Delay in seconds after unpausing (press Space) before starting audio and spawning. Configure in inspector.")]
|
||||
public float playbackStartDelay = 3f;
|
||||
|
||||
[Tooltip("Delay in seconds before starting the black mask fade out. Configure in inspector.")]
|
||||
public float blackMaskFadeStartDelay = 1.5f;
|
||||
|
||||
private bool pauseSubscribed = false;
|
||||
private bool musicWasPlayingBeforePause = false;
|
||||
|
||||
// tracks whether actual music playback (the real game playback) has started
|
||||
public bool PlaybackStarted { get; private set; } = false;
|
||||
|
||||
// Flag set by UI button to request start
|
||||
private bool startRequested = false;
|
||||
// Flag to control whether Escape can trigger return (disabled once startRequested is true)
|
||||
private bool allowEscapeReturn = true;
|
||||
|
||||
// --- Statistics Tracking ---
|
||||
private float sessionStartTime;
|
||||
private SongData currentSong;
|
||||
private bool timeRecorded = false;
|
||||
|
||||
private void SubscribeToPauseManager()
|
||||
{
|
||||
if (pauseSubscribed) return;
|
||||
@@ -79,10 +112,15 @@ public class GameManager : MonoBehaviour
|
||||
{
|
||||
musicSource.Pause();
|
||||
}
|
||||
|
||||
// Also pause Time.timeScale so playback delay coroutines are also paused
|
||||
Time.timeScale = 0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Do not change mute state here. External startup flow will unmute when appropriate.
|
||||
// Resume Time.timeScale first
|
||||
Time.timeScale = 1f;
|
||||
|
||||
// resume if it was playing before pause
|
||||
if (musicWasPlayingBeforePause)
|
||||
{
|
||||
@@ -130,94 +168,194 @@ public class GameManager : MonoBehaviour
|
||||
blackMaskImage.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
private Coroutine playbackDelayCoroutine;
|
||||
|
||||
// Public helper to reliably unmute and start music playback, respecting delay.
|
||||
public void PlayMusicWithDelay(float delaySeconds)
|
||||
{
|
||||
// reset flag until actual play occurs
|
||||
PlaybackStarted = false;
|
||||
|
||||
// Stop any previous delay coroutine
|
||||
if (playbackDelayCoroutine != null)
|
||||
{
|
||||
StopCoroutine(playbackDelayCoroutine);
|
||||
playbackDelayCoroutine = null;
|
||||
}
|
||||
|
||||
if (musicSource == null) return;
|
||||
|
||||
// ✅ FIX: Force stop any existing playback before unmute
|
||||
try
|
||||
{
|
||||
// Ensure unmuted
|
||||
musicSource.Stop();
|
||||
musicSource.time = 0f;
|
||||
}
|
||||
catch { }
|
||||
|
||||
try
|
||||
{
|
||||
// ✅ FIX: Ensure unmuted BEFORE attempting to play
|
||||
musicSource.mute = false;
|
||||
Debug.Log("GameManager.PlayMusicWithDelay: musicSource unmuted");
|
||||
}
|
||||
catch { }
|
||||
|
||||
try
|
||||
{
|
||||
// If a delay is specified, stop current and schedule playback
|
||||
// Reset audio time to 0 before playing to ensure playback starts from the beginning
|
||||
musicSource.time = 0f;
|
||||
|
||||
if (delaySeconds > 0f)
|
||||
{
|
||||
// Stop any currently playing to avoid overlapping schedules
|
||||
try { musicSource.Stop(); } catch { }
|
||||
musicSource.PlayDelayed(delaySeconds);
|
||||
Debug.Log($"GameManager.PlayMusicWithDelay: PlayDelayed({delaySeconds}) called");
|
||||
// Use coroutine instead of PlayDelayed so pause state can interrupt it
|
||||
playbackDelayCoroutine = StartCoroutine(DelayAndPlayMusic(delaySeconds));
|
||||
}
|
||||
else
|
||||
{
|
||||
// If already playing but paused, try UnPause; otherwise Play
|
||||
if (musicSource.isPlaying)
|
||||
{
|
||||
// already playing - nothing to do
|
||||
Debug.Log("GameManager.PlayMusicWithDelay: musicSource already playing");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Try UnPause first (in case it was paused), otherwise Play
|
||||
try { musicSource.UnPause(); Debug.Log("GameManager.PlayMusicWithDelay: UnPause called"); }
|
||||
catch { musicSource.Play(); Debug.Log("GameManager.PlayMusicWithDelay: Play called"); }
|
||||
}
|
||||
// Immediate playback (no delay)
|
||||
musicSource.time = 0f;
|
||||
musicSource.Play();
|
||||
PlaybackStarted = true;
|
||||
Debug.Log("GameManager.PlayMusicWithDelay: Music playback started immediately (no delay)");
|
||||
}
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"PlayMusicWithDelay failed: {ex}");
|
||||
try { musicSource.Play(); } catch { }
|
||||
try
|
||||
{
|
||||
musicSource.time = 0f;
|
||||
musicSource.Play();
|
||||
PlaybackStarted = true;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator DelayAndPlayMusic(float delaySeconds)
|
||||
{
|
||||
Debug.Log($"GameManager.DelayAndPlayMusic: waiting {delaySeconds} seconds before playing (respects pause state)");
|
||||
|
||||
// Wait using unscaled real time so this delay is not blocked when Time.timeScale==0
|
||||
float elapsed = 0f;
|
||||
while (elapsed < delaySeconds)
|
||||
{
|
||||
elapsed += Time.unscaledDeltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// After delay, play the music
|
||||
if (musicSource != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
// ✅ FIX: Double-check unmute before actual playback
|
||||
musicSource.mute = false;
|
||||
musicSource.time = 0f;
|
||||
musicSource.Play();
|
||||
PlaybackStarted = true;
|
||||
Debug.Log($"GameManager.DelayAndPlayMusic: music playback started after {delaySeconds}s delay, mute={musicSource.mute}");
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"DelayAndPlayMusic failed to play: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
playbackDelayCoroutine = null;
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
Debug.Log("GameManager Start() ������");
|
||||
Debug.Log("GameManager Start() initialized");
|
||||
|
||||
// ���ؼ�����Ƿ�Ϊ��
|
||||
if (beatmapManager == null) Debug.LogError("beatmapManager δ��ֵ��");
|
||||
if (noteSpawner == null) Debug.LogError("noteSpawner δ��ֵ��");
|
||||
if (musicSource == null) Debug.LogError("musicSource δ��ֵ��");
|
||||
// --- Initialize Statistics ---
|
||||
currentSong = BeatmapManager.pendingSongData ?? SongDataHolder.SelectedSongData;
|
||||
if (currentSong != null)
|
||||
{
|
||||
currentSong.game_enterTimes++;
|
||||
sessionStartTime = Time.realtimeSinceStartup;
|
||||
Debug.Log($"[GameManager] {currentSong.songName} launch count incremented to {currentSong.game_enterTimes}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("[GameManager] No SongData found for statistics tracking");
|
||||
}
|
||||
|
||||
// Ensure start canvas and its CanvasGroup are active and visible at scene start
|
||||
if (startCanvas != null && !startCanvas.activeSelf) startCanvas.SetActive(true);
|
||||
if (cg_startCanvas != null)
|
||||
{
|
||||
cg_startCanvas.alpha = 1f;
|
||||
cg_startCanvas.interactable = true;
|
||||
cg_startCanvas.blocksRaycasts = true;
|
||||
}
|
||||
|
||||
// Try to apply assigned SongData (if any) to background and UI after scene load
|
||||
StartCoroutine(WaitAndApplyAssignedSong(2f));
|
||||
|
||||
if (beatmapManager == null) Debug.LogError("beatmapManager is null");
|
||||
if (noteSpawner == null) Debug.LogError("noteSpawner is null");
|
||||
if (musicSource == null) Debug.LogError("musicSource is null");
|
||||
|
||||
// Subscribe to NoteSpawner.AllNotesSpawned to trigger action when spawning completes
|
||||
if (noteSpawner != null)
|
||||
{
|
||||
noteSpawner.AllNotesSpawned += OnAllNotesSpawned;
|
||||
}
|
||||
|
||||
// Wire up UI buttons if assigned
|
||||
if (startGame != null)
|
||||
{
|
||||
// ensure we don't register duplicate listeners
|
||||
try { startGame.onClick.RemoveListener(RequestStart); } catch { }
|
||||
startGame.onClick.AddListener(RequestStart);
|
||||
}
|
||||
if (backtoSelectingPage != null)
|
||||
{
|
||||
try { backtoSelectingPage.onClick.RemoveListener(BackToSelectingPage); } catch { }
|
||||
backtoSelectingPage.onClick.AddListener(BackToSelectingPage);
|
||||
}
|
||||
|
||||
// preload audio fully and start muted playback to warm up audio decoding
|
||||
if (musicSource != null && musicSource.clip != null)
|
||||
{
|
||||
musicSource.mute = true;
|
||||
musicSource.loop = false;
|
||||
musicSource.playOnAwake = false;
|
||||
try
|
||||
{
|
||||
// Warm up decoding by playing briefly muted so the audio system decodes the clip.
|
||||
musicSource.mute = true;
|
||||
musicSource.Play();
|
||||
Debug.Log("GameManager: warmed up audio playback (muted)");
|
||||
// ✅ FIX: Stop immediately after warmup to avoid interference with later playback
|
||||
// Wait a frame to ensure audio system processes the Play command
|
||||
StartCoroutine(StopWarmupAfterFrame());
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.LogWarning("GameManager: failed to warm up audio: " + ex.Message);
|
||||
}
|
||||
|
||||
settleCG.alpha = 0;
|
||||
}
|
||||
|
||||
// ��� pressStart �Ѿ���ȡ�� banflag ���ݣ��������ʾ�� UI����������ˣ�
|
||||
// display banflag content if available
|
||||
if (banflagTextUI != null && !string.IsNullOrEmpty(pressStart.banflagContent))
|
||||
{
|
||||
banflagTextUI.text = pressStart.banflagContent;
|
||||
Debug.Log("Displayed banflag content from pressStart into banflagTextUI.");
|
||||
}
|
||||
|
||||
// �� test mode ��ʹ���ⲿ·��Ԥ������Դ����ͣϵͳ���ȴ����ո����
|
||||
// choose startup path
|
||||
if (GameConfig.testMode)
|
||||
{
|
||||
// �ӳٸ�ֵ���� Start() �л�ȡ��̬·������ʱ pressStart.cs �Ѿ����в�����������
|
||||
JSONPath = pressStart.testmode_beatmapJSON_path;
|
||||
audioPath = pressStart.testmode_audioFile_path;
|
||||
bgPicPath = pressStart.testmode_bgPicFile_path;
|
||||
|
||||
Debug.Log("Test mode active: using external paths to preload assets");
|
||||
|
||||
// also update banflag UI if content exists
|
||||
if (banflagTextUI != null && !string.IsNullOrEmpty(pressStart.banflagContent))
|
||||
{
|
||||
banflagTextUI.text = pressStart.banflagContent;
|
||||
@@ -241,13 +379,167 @@ public class GameManager : MonoBehaviour
|
||||
blackMaskImage.color = cc;
|
||||
blackMaskImage.gameObject.SetActive(true);
|
||||
blackMaskImage.raycastTarget = true;
|
||||
// start automatic fade from alpha=1 to 0 over 1 second after Start
|
||||
StartCoroutine(FadeOutBlackMask(1f));
|
||||
// start automatic fade from alpha=1 to 0 over 1 second after Start, with delay
|
||||
StartCoroutine(DelayedFadeOutBlackMask(blackMaskFadeStartDelay, 1f));
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper coroutine to stop audio warmup after one frame and unmute the source
|
||||
/// </summary>
|
||||
private IEnumerator StopWarmupAfterFrame()
|
||||
{
|
||||
yield return null; // wait one frame for audio system to process Play command
|
||||
|
||||
if (musicSource != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
musicSource.Stop();
|
||||
musicSource.mute = false; // ✅ Unmute immediately after warmup
|
||||
musicSource.time = 0f;
|
||||
Debug.Log("GameManager: warmup completed, audio stopped and unmuted");
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.LogWarning("GameManager: failed to stop warmup: " + ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// Detect Space or yellow note key to request start (only before gameplay starts)
|
||||
if (!startRequested)
|
||||
{
|
||||
KeyCode yellowKey = KeyBindingManager.GetKeyForColor("yellow");
|
||||
// Check Space key
|
||||
if (Input.GetKeyDown(KeyCode.Space))
|
||||
{
|
||||
RequestStart();
|
||||
}
|
||||
// Check yellow track key from key binding manager
|
||||
else if (yellowKey != KeyCode.None && Input.GetKeyDown(yellowKey))
|
||||
{
|
||||
if (yellowKey == KeyCode.Space) return;
|
||||
else RequestStart();
|
||||
}
|
||||
|
||||
// Check Escape key to trigger return to selecting page (only before gameplay starts and allowed)
|
||||
if (allowEscapeReturn && Input.GetKeyDown(KeyCode.Escape))
|
||||
{
|
||||
BackToSelectingPage();
|
||||
}
|
||||
}
|
||||
// Once startRequested is true, Escape is disabled and gameplay begins countdown
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
RecordTotalPlayTime();
|
||||
// Unsubscribe from NoteSpawner.AllNotesSpawned to avoid memory leaks
|
||||
if (noteSpawner != null)
|
||||
{
|
||||
noteSpawner.AllNotesSpawned -= OnAllNotesSpawned;
|
||||
}
|
||||
|
||||
// remove UI listeners
|
||||
if (startGame != null)
|
||||
{
|
||||
startGame.onClick.RemoveListener(RequestStart);
|
||||
}
|
||||
if (backtoSelectingPage != null)
|
||||
{
|
||||
backtoSelectingPage.onClick.RemoveListener(BackToSelectingPage);
|
||||
}
|
||||
|
||||
// stop canvas fade coroutine if running
|
||||
if (fade_canvasGroup != null)
|
||||
{
|
||||
StopCoroutine(fade_canvasGroup);
|
||||
fade_canvasGroup = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Start the fade coroutine for the start canvas group (safe to call multiple times)
|
||||
private void StartFadeStartCanvas()
|
||||
{
|
||||
if (cg_startCanvas == null || startCanvas == null) return;
|
||||
if (fade_canvasGroup != null) StopCoroutine(fade_canvasGroup);
|
||||
fade_canvasGroup = StartCoroutine(FadeStartCanvasCoroutine(canvasFadeTime));
|
||||
}
|
||||
|
||||
private IEnumerator FadeStartCanvasCoroutine(float duration)
|
||||
{
|
||||
if (cg_startCanvas == null || startCanvas == null) yield break;
|
||||
|
||||
float elapsed = 0f;
|
||||
float startA = cg_startCanvas.alpha;
|
||||
while (elapsed < duration)
|
||||
{
|
||||
elapsed += Time.unscaledDeltaTime;
|
||||
float frac = Mathf.Clamp01(elapsed / Mathf.Max(0.0001f, duration));
|
||||
cg_startCanvas.alpha = Mathf.Lerp(startA, 0f, frac);
|
||||
yield return null;
|
||||
}
|
||||
|
||||
cg_startCanvas.alpha = 0f;
|
||||
cg_startCanvas.interactable = false;
|
||||
cg_startCanvas.blocksRaycasts = false;
|
||||
startCanvas.SetActive(false);
|
||||
fade_canvasGroup = null;
|
||||
}
|
||||
|
||||
private void OnAllNotesSpawned()
|
||||
{
|
||||
// All notes have been spawned - trigger settlement decision logic
|
||||
Debug.Log("[GameManager] All notes have been spawned. Initiating settlement countdown.");
|
||||
|
||||
if (noteSpawner != null)
|
||||
{
|
||||
noteSpawner.StartSettlementRoutine();
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("[GameManager] OnAllNotesSpawned: noteSpawner is null, cannot start settlement routine");
|
||||
}
|
||||
}
|
||||
|
||||
// Called by UI button to request the start countdown
|
||||
public void RequestStart()
|
||||
{
|
||||
// Guard: prevent duplicate RequestStart execution
|
||||
if (startRequested)
|
||||
{
|
||||
Debug.LogWarning("[GameManager] RequestStart already called, ignoring duplicate request");
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Log("[GameManager] RequestStart executed");
|
||||
startRequested = true;
|
||||
allowEscapeReturn = false; // Disable Escape immediately when start is requested
|
||||
|
||||
// Play ready sequence if available (null-check to avoid NullReferenceException)
|
||||
if (readyLetsGo != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
readyLetsGo.PlaySequence();
|
||||
Debug.Log("[GameManager] readyLetsGo.PlaySequence() triggered");
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.LogError($"[GameManager] Error: {ex}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("[GameManager] readyLetsGo is null");
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator HandleTestModeStartup()
|
||||
{
|
||||
// Preload audio from audioPath (absolute)
|
||||
@@ -320,15 +612,16 @@ public class GameManager : MonoBehaviour
|
||||
musicSource.clip = clip;
|
||||
musicSource.loop = false;
|
||||
musicSource.playOnAwake = false;
|
||||
// Warm up decoding by playing briefly muted; avoid try/catch around yield (not allowed).
|
||||
// Warm up decoding by playing briefly muted so the audio system decodes the clip.
|
||||
musicSource.mute = true;
|
||||
musicSource.Play();
|
||||
// allow a few frames for audio system to start
|
||||
yield return null;
|
||||
musicSource.Stop();
|
||||
|
||||
Debug.Log("TestMode audio loaded into AudioSource");
|
||||
|
||||
// ✅ FIX: Stop immediately after warmup to avoid interference with later playback
|
||||
StartCoroutine(StopWarmupAfterFrame());
|
||||
|
||||
// After loading audio, enable overlay and pause via PauseManager
|
||||
var pm = PauseManager.Instance ?? FindObjectOfType<PauseManager>();
|
||||
if (pm != null)
|
||||
@@ -365,7 +658,7 @@ public class GameManager : MonoBehaviour
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.LogError($"��ȡ����ͼƬ�ļ�����: {ex}");
|
||||
Debug.LogError($"Failed to read background image file: {ex}");
|
||||
}
|
||||
|
||||
if (imgBytes != null)
|
||||
@@ -376,29 +669,34 @@ public class GameManager : MonoBehaviour
|
||||
Sprite s = Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), new Vector2(0.5f, 0.5f));
|
||||
backgroundRenderer.sprite = s;
|
||||
|
||||
// ���ĵ㣺���� SpriteRenderer ����Ϊ 255 (1.0f)��
|
||||
// ensure SpriteRenderer alpha is fully opaque
|
||||
Color color = backgroundRenderer.color;
|
||||
color.a = 1.0f;
|
||||
backgroundRenderer.color = color;
|
||||
|
||||
// ���ֱ�������������Ӧ�ɳ������־�������¼ assignment
|
||||
Debug.Log("Background sprite assigned (ensure your SpriteRenderer size fits scene)");
|
||||
|
||||
// Set song name text if available
|
||||
if (songnameText != null)
|
||||
{
|
||||
songnameText.text = Path.GetFileNameWithoutExtension(bgPicPath);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError($"�����ֽڴ��� Texture2D: {bgPicPath}");
|
||||
Debug.LogError($"Failed to create Texture2D from bytes: {bgPicPath}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("TestMode bgPicPath δ���á�");
|
||||
Debug.LogWarning("TestMode bgPicPath not provided");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("backgroundRenderer δ���ã������������ء�");
|
||||
Debug.LogWarning("backgroundRenderer not assigned; skipping background preload");
|
||||
}
|
||||
|
||||
// Read JSON from JSONPath and parse via BeatmapManager.ParseJsonOnly
|
||||
@@ -417,14 +715,14 @@ public class GameManager : MonoBehaviour
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.LogError($"��ȡ TestMode JSON �ļ�����: {ex}");
|
||||
Debug.LogError($"Failed to read TestMode JSON file: {ex}");
|
||||
yield break;
|
||||
}
|
||||
|
||||
bool parsed = beatmapManager.ParseJsonOnly(jsonContent);
|
||||
if (!parsed)
|
||||
{
|
||||
Debug.LogError("ParseJsonOnly ʧ�ܣ�ֹͣ��������ģʽ��");
|
||||
Debug.LogError("ParseJsonOnly failed; aborting test mode startup");
|
||||
yield break;
|
||||
}
|
||||
|
||||
@@ -433,7 +731,7 @@ public class GameManager : MonoBehaviour
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("TestMode JSONPath δ���á�");
|
||||
Debug.LogError("TestMode JSONPath not provided");
|
||||
yield break;
|
||||
}
|
||||
|
||||
@@ -443,16 +741,25 @@ public class GameManager : MonoBehaviour
|
||||
PauseManager.Instance?.Pause(true);
|
||||
UpdateStatusOnConsole("Paused: press Space to start playback");
|
||||
|
||||
// Wait for space key down (use unscaled input loop)
|
||||
while (!Input.GetKeyDown(KeyCode.Space))
|
||||
// Determine yellow track key from key binding manager
|
||||
KeyCode yellowKey = KeyBindingManager.GetKeyForColor("yellow");
|
||||
|
||||
// Wait for start request: Space, yellow key, or UI button
|
||||
while (!startRequested && !Input.GetKeyDown(KeyCode.Space) && (yellowKey == KeyCode.None || !Input.GetKeyDown(yellowKey)))
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Clear request flag
|
||||
startRequested = false;
|
||||
|
||||
// Kick off UI canvas fade-out when start requested
|
||||
StartFadeStartCanvas();
|
||||
|
||||
// Resume using PauseManager
|
||||
PauseManager.Instance?.Pause(false);
|
||||
// small buffer to avoid hitching immediately after unpause: wait configured realtime delay
|
||||
yield return new WaitForSecondsRealtime(Mathf.Max(0f, playbackStartDelay));
|
||||
// Use scaled wait so pause (Escape) will pause this delay
|
||||
yield return new WaitForSeconds(Mathf.Max(0f, playbackStartDelay));
|
||||
|
||||
// Start spawning using the parsed beatmap
|
||||
if (beatmapManager.beatmap != null)
|
||||
@@ -461,7 +768,7 @@ public class GameManager : MonoBehaviour
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("��������ʧ�ܣ�beatmap δ������Ϊ��");
|
||||
Debug.LogError("Beatmap missing after parsing");
|
||||
yield break;
|
||||
}
|
||||
|
||||
@@ -484,16 +791,25 @@ public class GameManager : MonoBehaviour
|
||||
PauseManager.Instance?.Pause(true);
|
||||
UpdateStatusOnConsole("Paused: press Space to start playback");
|
||||
|
||||
// Wait for space
|
||||
while (!Input.GetKeyDown(KeyCode.Space))
|
||||
// Determine yellow track key from key binding manager
|
||||
KeyCode yellowKey = KeyBindingManager.GetKeyForColor("yellow");
|
||||
|
||||
// Wait for start request: Space, yellow key, or UI button
|
||||
while (!startRequested && !Input.GetKeyDown(KeyCode.Space) && (yellowKey == KeyCode.None || !Input.GetKeyDown(yellowKey)))
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Clear request flag
|
||||
startRequested = false;
|
||||
|
||||
// Kick off UI canvas fade-out when start requested
|
||||
StartFadeStartCanvas();
|
||||
|
||||
// Resume
|
||||
PauseManager.Instance?.Pause(false);
|
||||
// small buffer to avoid hitching immediately after unpause: wait configured realtime delay
|
||||
yield return new WaitForSecondsRealtime(Mathf.Max(0f, playbackStartDelay));
|
||||
// Use scaled wait so pause (Escape) will pause this delay
|
||||
yield return new WaitForSeconds(Mathf.Max(0f, playbackStartDelay));
|
||||
|
||||
// Start spawning using the existing parsed beatmap
|
||||
if (beatmapManager.beatmap != null)
|
||||
@@ -542,12 +858,12 @@ public class GameManager : MonoBehaviour
|
||||
yield break;
|
||||
}
|
||||
|
||||
// ԭ�����̣��������浫����������
|
||||
// Original default demo load commented out; manual load only when needed
|
||||
string beatmapFilePath = Path.Combine(Application.streamingAssetsPath, "Emilia_demo.json");
|
||||
|
||||
if (!File.Exists(beatmapFilePath))
|
||||
{
|
||||
Debug.LogError($"�����������: {beatmapFilePath}");
|
||||
Debug.LogError($"Beatmap file missing: {beatmapFilePath}");
|
||||
yield break;
|
||||
}
|
||||
|
||||
@@ -555,55 +871,62 @@ public class GameManager : MonoBehaviour
|
||||
bool parsed = beatmapManager.ParseJsonOnly(json);
|
||||
if (!parsed)
|
||||
{
|
||||
Debug.LogError("ParseJsonOnly ʧ��");
|
||||
Debug.LogError("ParseJsonOnly failed");
|
||||
yield break;
|
||||
}
|
||||
|
||||
// ��ͣϵͳ
|
||||
// Pause system
|
||||
SubscribeToPauseManager();
|
||||
PauseManager.Instance?.Pause(true);
|
||||
UpdateStatusOnConsole("Paused: press Space to start playback");
|
||||
|
||||
// �ȴ��ո�
|
||||
while (!Input.GetKeyDown(KeyCode.Space))
|
||||
// Determine yellow track key from key binding manager
|
||||
KeyCode yellowKey2 = KeyBindingManager.GetKeyForColor("yellow");
|
||||
|
||||
// Wait for start request: Space, yellow key, or UI button
|
||||
while (!startRequested && !Input.GetKeyDown(KeyCode.Space) && (yellowKey2 == KeyCode.None || !Input.GetKeyDown(yellowKey2)))
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// �ָ�
|
||||
PauseManager.Instance?.Pause(false);
|
||||
// small buffer to avoid hitching immediately after unpause: wait configured realtime delay
|
||||
yield return new WaitForSecondsRealtime(Mathf.Max(0f, playbackStartDelay));
|
||||
// Clear request flag
|
||||
startRequested = false;
|
||||
|
||||
// ��������
|
||||
// Kick off UI canvas fade-out when start requested
|
||||
StartFadeStartCanvas();
|
||||
|
||||
// Resume
|
||||
PauseManager.Instance?.Pause(false);
|
||||
// Use scaled wait so pause (Escape) will pause this delay
|
||||
yield return new WaitForSeconds(Mathf.Max(0f, playbackStartDelay));
|
||||
|
||||
// Start spawning
|
||||
if (beatmapManager.beatmap != null)
|
||||
{
|
||||
noteSpawner.LoadBeatmap(beatmapManager.beatmap);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("beatmap ���");
|
||||
Debug.LogError("beatmap missing");
|
||||
yield break;
|
||||
}
|
||||
|
||||
// ���ű������֣��ӳٲ����� beatmapManager.globalDelaySeconds ������
|
||||
// Load and play audio
|
||||
if (!string.IsNullOrEmpty(beatmapManager.parsedMusicFile))
|
||||
{
|
||||
AudioClip musicClip = Resources.Load<AudioClip>(beatmapManager.parsedMusicFile);
|
||||
if (musicClip == null)
|
||||
{
|
||||
Debug.LogError($"�����ļ�����ʧ��: {beatmapManager.parsedMusicFile}");
|
||||
Debug.LogError($"Failed to load parsed music file: {beatmapManager.parsedMusicFile}");
|
||||
}
|
||||
else
|
||||
{
|
||||
musicSource.clip = musicClip;
|
||||
// ensure we don't accidentally autoplay when assigning clips
|
||||
musicSource.playOnAwake = false;
|
||||
musicSource.Stop();
|
||||
float delay = beatmapManager.globalDelaySeconds;
|
||||
PlayMusicWithDelay(delay);
|
||||
|
||||
// After assigning clip, enable overlay and pause
|
||||
var pm3 = PauseManager.Instance ?? FindObjectOfType<PauseManager>();
|
||||
if (pm3 != null)
|
||||
{
|
||||
@@ -614,7 +937,7 @@ public class GameManager : MonoBehaviour
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("������ musicFile Ϊ�գ�");
|
||||
Debug.LogError("parsed musicFile empty");
|
||||
}
|
||||
|
||||
UpdateStatusOnConsole("Playback started");
|
||||
@@ -624,4 +947,129 @@ public class GameManager : MonoBehaviour
|
||||
{
|
||||
Debug.Log(message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delay then fade out the black mask image. Waits for delaySeconds, then fades out over fadeDuration seconds (unscaled).
|
||||
/// </summary>
|
||||
private IEnumerator DelayedFadeOutBlackMask(float delaySeconds, float fadeDuration)
|
||||
{
|
||||
// Wait for the specified delay
|
||||
float elapsed = 0f;
|
||||
while (elapsed < delaySeconds)
|
||||
{
|
||||
elapsed += Time.unscaledDeltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// After delay, start the fade out
|
||||
yield return StartCoroutine(FadeOutBlackMask(fadeDuration));
|
||||
}
|
||||
|
||||
// Called when the back button is pressed; fades to black then loads selection scene
|
||||
private void BackToSelectingPage()
|
||||
{
|
||||
RecordTotalPlayTime();
|
||||
// restore time scale to normal so UI animations driven by unscaled or scaled time behave correctly
|
||||
try { Time.timeScale = 1f; } catch { }
|
||||
|
||||
// start coroutine to fade mask then load
|
||||
if (blackMaskImage == null)
|
||||
{
|
||||
SceneManager.LoadScene("selectYourSongFirst");
|
||||
return;
|
||||
}
|
||||
|
||||
StartCoroutine(FadeToBlackAndLoad("selectYourSongFirst", 0.25f));
|
||||
}
|
||||
|
||||
private IEnumerator FadeToBlackAndLoad(string sceneName, float duration)
|
||||
{
|
||||
RecordTotalPlayTime();
|
||||
if (blackMaskImage == null)
|
||||
{
|
||||
SceneManager.LoadScene(sceneName);
|
||||
yield break;
|
||||
}
|
||||
|
||||
// ensure image active and start from current alpha
|
||||
if (!blackMaskImage.gameObject.activeSelf) blackMaskImage.gameObject.SetActive(true);
|
||||
float startA = blackMaskImage.color.a;
|
||||
float elapsed = 0f;
|
||||
|
||||
// enable raycast target while fading to block input
|
||||
try { blackMaskImage.raycastTarget = true; } catch {}
|
||||
|
||||
while (elapsed < duration)
|
||||
{
|
||||
elapsed += Time.unscaledDeltaTime;
|
||||
float frac = Mathf.Clamp01(elapsed / Mathf.Max(0.0001f, duration));
|
||||
Color c = blackMaskImage.color;
|
||||
c.a = Mathf.Lerp(startA, 1f, frac);
|
||||
blackMaskImage.color = c;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// ensure fully opaque
|
||||
Color fc = blackMaskImage.color;
|
||||
fc.a = 1f;
|
||||
blackMaskImage.color = fc;
|
||||
|
||||
// load scene
|
||||
SceneManager.LoadScene(sceneName);
|
||||
}
|
||||
|
||||
private IEnumerator WaitAndApplyAssignedSong(float timeoutSeconds)
|
||||
{
|
||||
float waited = 0f;
|
||||
while (waited < timeoutSeconds)
|
||||
{
|
||||
if (beatmapManager != null && beatmapManager.assignedSongData != null)
|
||||
{
|
||||
ApplyAssignedSongToUI(beatmapManager.assignedSongData);
|
||||
yield break;
|
||||
}
|
||||
waited += Time.unscaledDeltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// also consider pendingSongData static as fallback
|
||||
if (BeatmapManager.pendingSongData != null)
|
||||
{
|
||||
ApplyAssignedSongToUI(BeatmapManager.pendingSongData);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyAssignedSongToUI(SongData sd)
|
||||
{
|
||||
if (sd == null) return;
|
||||
// set background sprite if present
|
||||
if (backgroundRenderer != null && sd.fullscreen_songPicture != null)
|
||||
{
|
||||
backgroundRenderer.sprite = sd.fullscreen_songPicture;
|
||||
Color color = backgroundRenderer.color;
|
||||
color.a = 1f;
|
||||
backgroundRenderer.color = color;
|
||||
}
|
||||
|
||||
// set song name text if available
|
||||
if (songnameText != null)
|
||||
{
|
||||
songnameText.text = sd.songName;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records the elapsed play time since the session started and adds it to the song's total play time.
|
||||
/// Can be called multiple times, but only records once per session.
|
||||
/// </summary>
|
||||
public void RecordTotalPlayTime()
|
||||
{
|
||||
if (timeRecorded || currentSong == null) return;
|
||||
|
||||
float elapsed = Time.realtimeSinceStartup - sessionStartTime;
|
||||
currentSong.time_totalPlayingTime += elapsed;
|
||||
timeRecorded = true;
|
||||
|
||||
Debug.Log($"[GameManager] Recorded {elapsed:F2}s to total play time for {currentSong.songName}. New total: {currentSong.time_totalPlayingTime:F2}s");
|
||||
}
|
||||
}
|
||||
@@ -228,18 +228,10 @@ public class HoldNote : BaseNote
|
||||
|
||||
if (controller != null)
|
||||
{
|
||||
controller.ConfigureTiming(time, delay, travelTime);
|
||||
|
||||
// If the activation time is already in the past at the moment of Setup,
|
||||
// the segment should have already traveled some distance. In that case
|
||||
// snap its position forward so it appears at the correct location.
|
||||
// Do this only when activation happened before now to avoid snapping
|
||||
// freshly spawned segments that haven't started moving yet.
|
||||
if (controller.ActivationTime < Time.time - 0.0001f)
|
||||
{
|
||||
// Use current transform.position as the spawn origin (NoteSpawner sets this before Setup).
|
||||
CalibratePosition(transform.position, 0f);
|
||||
}
|
||||
// Keep all hold segments moving in absolute time so they stay visually connected.
|
||||
float headActivationTime = time - travelTime;
|
||||
float baseSpawnYOffset = -speed * delay;
|
||||
controller.ConfigureAbsolutePositioning(transform.position, headActivationTime, baseSpawnYOffset);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -273,15 +265,8 @@ public class HoldNote : BaseNote
|
||||
{
|
||||
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] START auto-Miss: {noteColor}");
|
||||
|
||||
// register as missed
|
||||
JudgeManager.Instance.RegisterStartJudged(noteID, false);
|
||||
|
||||
InputManager.Instance?.ShowJudgeResult(trackIndex, "Miss");
|
||||
JudgeSoundManager.Instance?.PlayJudgeSound("Miss");
|
||||
teamUIController.Instance?.OnJudgeResult("Miss");
|
||||
|
||||
// Show judgement prefab for auto-miss as well (consistent with other notes)
|
||||
Animation_GenerateJudgementSituationPrefab.Instance?.SpawnJudgePrefab(noteColor, "Miss");
|
||||
// Register miss properly via EvaluateHoldEnd
|
||||
EvaluateHoldEnd(Time.time, true);
|
||||
|
||||
isJudged = true;
|
||||
ScheduleReturnToPool(0.2f);
|
||||
@@ -291,6 +276,7 @@ public class HoldNote : BaseNote
|
||||
{
|
||||
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] END auto-Miss: {noteColor}");
|
||||
HandleEnd(true);
|
||||
// HandleEnd/EvaluateHoldEnd will release lock as needed
|
||||
isJudged = true;
|
||||
}
|
||||
}
|
||||
@@ -411,6 +397,23 @@ public class HoldNote : BaseNote
|
||||
return;
|
||||
}
|
||||
|
||||
// NEW: If the key is still being held down when end segment enters judge zone, immediately judge
|
||||
if (Input.GetKey(keyToPress) && JudgeManager.Instance.IsStartJudged(noteID) && !isJudged)
|
||||
{
|
||||
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] End entered while key still held: {noteColor}, calculating result immediately");
|
||||
// Record release time as current time (player is still holding)
|
||||
releaseTime = Time.time;
|
||||
hasReleased = true;
|
||||
JudgeManager.Instance?.RegisterNoteReleased(noteID, true);
|
||||
// Evaluate and judge the hold end
|
||||
EvaluateHoldEnd(releaseTime, false);
|
||||
isJudged = true;
|
||||
// Terminate input - mark key as released
|
||||
isHoldActive = false;
|
||||
AnimationController.Global?.StopHoldParticles();
|
||||
return;
|
||||
}
|
||||
|
||||
autoReturnCoroutine = StartCoroutine(DelayedAutoReturnCheck());
|
||||
}
|
||||
}
|
||||
@@ -536,15 +539,19 @@ public class HoldNote : BaseNote
|
||||
if (forceMiss)
|
||||
{
|
||||
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] START forced Miss: {noteColor}");
|
||||
ScoreManager.Instance.countMiss += 1;
|
||||
JudgeManager.Instance.RegisterStartJudged(noteID, false);
|
||||
isHoldActive = false;
|
||||
|
||||
// Use EvaluateHoldEnd to centralize miss logic and statistics
|
||||
EvaluateHoldEnd(Time.time, true);
|
||||
|
||||
// Still need to show judge result and update combo for the head segment
|
||||
InputManager.Instance?.ShowJudgeResult(trackIndex, "Miss");
|
||||
JudgeSoundManager.Instance?.PlayJudgeSound("Miss");
|
||||
teamUIController.Instance?.OnJudgeResult("Miss");
|
||||
|
||||
// IMPORTANT: release lock immediately on miss
|
||||
ReleaseTrackJudgeLockSafe();
|
||||
|
||||
// Also show judgement prefab for forced miss on start
|
||||
Animation_GenerateJudgementSituationPrefab.Instance?.SpawnJudgePrefab(noteColor, "Miss");
|
||||
// Return soon
|
||||
ScheduleReturnToPool(0.2f);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -561,7 +568,11 @@ public class HoldNote : BaseNote
|
||||
if (offset <= pRange)
|
||||
{
|
||||
result = "Perfect";
|
||||
// Score statistics for Hold notes are now handled exclusively in EvaluateHoldEnd
|
||||
// to ensure they only count as 1 note in the total sum.
|
||||
if (noteData != null) noteData.judgeOffsetMs = rawOffsetMs;
|
||||
// RecordOffset removed from here to prevent double counting.
|
||||
// It will be called once in EvaluateHoldEnd.
|
||||
JudgeManager.Instance.RegisterStartJudged(noteID, true);
|
||||
isHoldActive = true;
|
||||
PlayHitAnimation();
|
||||
@@ -577,6 +588,7 @@ public class HoldNote : BaseNote
|
||||
{
|
||||
result = "Great";
|
||||
if (noteData != null) noteData.judgeOffsetMs = rawOffsetMs;
|
||||
// RecordOffset removed from here to prevent double counting
|
||||
JudgeManager.Instance.RegisterStartJudged(noteID, true);
|
||||
isHoldActive = true;
|
||||
PlayHitAnimation();
|
||||
@@ -590,6 +602,7 @@ public class HoldNote : BaseNote
|
||||
{
|
||||
result = "Good";
|
||||
if (noteData != null) noteData.judgeOffsetMs = rawOffsetMs;
|
||||
// RecordOffset removed from here to prevent double counting
|
||||
JudgeManager.Instance.RegisterStartJudged(noteID, true);
|
||||
isHoldActive = true;
|
||||
PlayHitAnimation();
|
||||
@@ -602,9 +615,13 @@ public class HoldNote : BaseNote
|
||||
else
|
||||
{
|
||||
result = "Miss";
|
||||
ScoreManager.Instance.countMiss += 1;
|
||||
// For a Miss at the start, we immediately evaluate the end as a Miss too
|
||||
// to ensure ScoreManager records exactly one Miss for this hold note.
|
||||
JudgeManager.Instance.RegisterStartJudged(noteID, false);
|
||||
isHoldActive = false;
|
||||
|
||||
EvaluateHoldEnd(Time.time, true);
|
||||
|
||||
try
|
||||
{
|
||||
bool triggered = SkillBuilder.Instance?.NotifyNoteHit(this.trackIndex, result, SkillDefinition.NoteTypeTrigger.Tap, this.noteID) ?? false;
|
||||
@@ -614,11 +631,20 @@ public class HoldNote : BaseNote
|
||||
{
|
||||
Debug.LogError($"[HoldNote] NotifyNoteHit(Start Miss) threw: {ex}");
|
||||
}
|
||||
|
||||
// IMPORTANT: release lock immediately on miss
|
||||
ReleaseTrackJudgeLockSafe();
|
||||
}
|
||||
|
||||
// show judge result and update combo/UI like short notes
|
||||
InputManager.Instance?.ShowJudgeResult(trackIndex, result);
|
||||
teamUIController.Instance?.OnJudgeResult(result);
|
||||
|
||||
// Only call OnJudgeResult on head if it's a hit, so it counts as 1 combo for the whole note.
|
||||
// If it's a Miss, EvaluateHoldEnd(..., true) already handled the OnJudgeResult("Miss").
|
||||
if (result != "Miss")
|
||||
{
|
||||
teamUIController.Instance?.OnJudgeResult(result);
|
||||
}
|
||||
|
||||
// Play judge sound only for START segment
|
||||
if (segment == NoteSegment.Start)
|
||||
@@ -636,6 +662,12 @@ public class HoldNote : BaseNote
|
||||
{
|
||||
StartCoroutine(DelayedReturn());
|
||||
}
|
||||
|
||||
// If start head was judged as Miss, ensure we return soon so it can't keep stale state
|
||||
if (segment == NoteSegment.Start && result == "Miss")
|
||||
{
|
||||
ScheduleReturnToPool(0.2f);
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator DelayedReturn()
|
||||
@@ -672,7 +704,15 @@ public class HoldNote : BaseNote
|
||||
{
|
||||
result = "Miss";
|
||||
ScoreManager.Instance.countMiss += 1;
|
||||
if (trackIndex >= 0 && trackIndex < 5) ScoreManager.Instance.trackMissCounts[trackIndex]++;
|
||||
|
||||
// Record a 0 offset for Miss to ensure total Offset Count matches note count
|
||||
ScoreManager.Instance?.RecordOffset(0);
|
||||
|
||||
if (GameConfig.verboseLogs) Debug.LogWarning($"[HoldNote] END judged as Miss: {noteColor} reason={(forceMiss ? "forced Miss" : "start not judged")}");
|
||||
|
||||
// IMPORTANT: release lock on end miss too
|
||||
ReleaseTrackJudgeLockSafe();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -687,21 +727,30 @@ public class HoldNote : BaseNote
|
||||
{
|
||||
result = "Perfect";
|
||||
ScoreManager.Instance.countPerfect += 1;
|
||||
if (trackIndex >= 0 && trackIndex < 5) ScoreManager.Instance.trackPerfectCounts[trackIndex]++;
|
||||
}
|
||||
else if (frac > 0.5f)
|
||||
{
|
||||
result = "Great";
|
||||
ScoreManager.Instance.countGreat += 1;
|
||||
if (trackIndex >= 0 && trackIndex < 5) ScoreManager.Instance.trackGreatCounts[trackIndex]++;
|
||||
}
|
||||
else
|
||||
{
|
||||
result = "Good";
|
||||
ScoreManager.Instance.countGood += 1;
|
||||
if (trackIndex >= 0 && trackIndex < 5) ScoreManager.Instance.trackGoodCounts[trackIndex]++;
|
||||
}
|
||||
|
||||
if (noteData != null && result != "Miss")
|
||||
{
|
||||
noteData.judgeOffsetMsEnd = (info.scheduledEnd - releaseTime) * 1000f;
|
||||
float offsetEnd = (info.scheduledEnd - releaseTime) * 1000f;
|
||||
noteData.judgeOffsetMsEnd = offsetEnd;
|
||||
|
||||
// Record the head offset (stored during HandleStart) as the representative timing for this note
|
||||
float headOffset = noteData.judgeOffsetMs;
|
||||
if (float.IsNaN(headOffset)) headOffset = 0;
|
||||
ScoreManager.Instance?.RecordOffset(headOffset);
|
||||
}
|
||||
|
||||
// cleanup pool
|
||||
@@ -719,31 +768,45 @@ public class HoldNote : BaseNote
|
||||
{
|
||||
result = "Perfect";
|
||||
ScoreManager.Instance.countPerfect += 1;
|
||||
if (trackIndex >= 0 && trackIndex < 5) ScoreManager.Instance.trackPerfectCounts[trackIndex]++;
|
||||
}
|
||||
else if (frac > 0.5f)
|
||||
{
|
||||
result = "Great";
|
||||
ScoreManager.Instance.countGreat += 1;
|
||||
if (trackIndex >= 0 && trackIndex < 5) ScoreManager.Instance.trackGreatCounts[trackIndex]++;
|
||||
}
|
||||
else
|
||||
{
|
||||
result = "Good";
|
||||
ScoreManager.Instance.countGood += 1;
|
||||
if (trackIndex >= 0 && trackIndex < 5) ScoreManager.Instance.trackGoodCounts[trackIndex]++;
|
||||
}
|
||||
|
||||
float rawOffsetMsEnd = (scheduledEndTime - actualReleaseTime) * 1000f;
|
||||
if (noteData != null && result != "Miss")
|
||||
{
|
||||
noteData.judgeOffsetMsEnd = rawOffsetMsEnd;
|
||||
|
||||
// Fallback: use head offset if available, otherwise 0
|
||||
float headOffset = noteData.judgeOffsetMs;
|
||||
if (float.IsNaN(headOffset)) headOffset = 0;
|
||||
ScoreManager.Instance?.RecordOffset(headOffset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] END result for {noteID}: {result} (color={noteColor})");
|
||||
|
||||
// show UI/audio and combo
|
||||
// show UI/audio
|
||||
InputManager.Instance?.ShowJudgeResult(trackIndex, result);
|
||||
teamUIController.Instance?.OnJudgeResult(result);
|
||||
|
||||
// ONLY call OnJudgeResult on end if it's a Miss, to break the combo started at the head.
|
||||
// If it's a hit, we don't call it again to avoid double-counting combo.
|
||||
if (result == "Miss")
|
||||
{
|
||||
teamUIController.Instance?.OnJudgeResult(result);
|
||||
}
|
||||
|
||||
// Ensure END always spawns judgement prefab (including Miss)
|
||||
Animation_GenerateJudgementSituationPrefab.Instance?.SpawnJudgePrefab(noteColor, result);
|
||||
@@ -830,11 +893,6 @@ public class HoldNote : BaseNote
|
||||
|
||||
private void HandleEnd(bool forceMiss = false)
|
||||
{
|
||||
if (!JudgeManager.Instance.TryResolveEnd(noteID))
|
||||
{
|
||||
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] END try-resolve failed: {noteColor}");
|
||||
return;
|
||||
}
|
||||
// record release time and register release in JudgeManager
|
||||
if (!hasReleased)
|
||||
{
|
||||
@@ -865,12 +923,9 @@ public class HoldNote : BaseNote
|
||||
}
|
||||
|
||||
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] ReturnToPool called for {noteID} segment={segment} time={Time.time:F3}");
|
||||
|
||||
// Release lock if still held
|
||||
if (TrackKeyManager.Instance != null)
|
||||
{
|
||||
TrackKeyManager.Instance.UnlockTrackForJudge(trackIndex, noteID);
|
||||
}
|
||||
|
||||
// CRITICAL: Always release lock before returning to pool, regardless of state
|
||||
ReleaseTrackJudgeLockSafe();
|
||||
|
||||
gameObject.SetActive(false);
|
||||
|
||||
@@ -884,6 +939,9 @@ public class HoldNote : BaseNote
|
||||
|
||||
public void ResetState()
|
||||
{
|
||||
// also release any stale lock from previous lifecycle
|
||||
ReleaseTrackJudgeLockSafe();
|
||||
|
||||
hasReleased = false;
|
||||
segment = NoteSegment.None;
|
||||
keyToPress = KeyCode.None;
|
||||
@@ -910,6 +968,15 @@ public class HoldNote : BaseNote
|
||||
ResetVisualScale();
|
||||
}
|
||||
|
||||
private void ReleaseTrackJudgeLockSafe()
|
||||
{
|
||||
// Defensive: any miss / pooling path should release the track lock.
|
||||
if (TrackKeyManager.Instance != null && !string.IsNullOrEmpty(noteID))
|
||||
{
|
||||
TrackKeyManager.Instance.UnlockTrackForJudge(trackIndex, noteID);
|
||||
}
|
||||
}
|
||||
|
||||
public void ApplyVisualScale(float scaleY)
|
||||
{
|
||||
float s = Mathf.Clamp(scaleY, 0.1f, 4f);
|
||||
@@ -942,12 +1009,21 @@ public class HoldNote : BaseNote
|
||||
|
||||
float activation = controller.ActivationTime;
|
||||
float s = controller.CurrentSpeed;
|
||||
float elapsed = Mathf.Max(0f, Time.time - activation);
|
||||
Vector3 expected = spawnPointPosition + Vector3.down * s * elapsed;
|
||||
float elapsed = Time.time - activation;
|
||||
Vector3 expected = spawnPointPosition;
|
||||
|
||||
if (controller.UsesAbsolutePositioning)
|
||||
{
|
||||
expected.y -= (controller.BaseSpawnYOffset + s * elapsed);
|
||||
}
|
||||
else
|
||||
{
|
||||
expected += Vector3.down * s * Mathf.Max(0f, elapsed);
|
||||
}
|
||||
|
||||
if (Vector3.Distance(transform.position, expected) > tolerance)
|
||||
{
|
||||
transform.position = expected;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,24 @@ public class HoldNoteController : MonoBehaviour
|
||||
private bool isInsideJudgeZone = false;
|
||||
public event Action<bool> OnJudgeZoneChanged;
|
||||
private bool isMoving = false;
|
||||
private float speed = 0f; // 运动速度
|
||||
private float activationTime; // 何时开始移动(Time.time 基准)
|
||||
private float speed = 0f;
|
||||
private float activationTime;
|
||||
|
||||
// Absolute positioning for hold segments (keeps segments connected under frame spikes)
|
||||
private bool useAbsolutePositioning = false;
|
||||
private Vector3 spawnPosition;
|
||||
private float baseSpawnYOffset = 0f; // can be negative to offset upward for delayed segments
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// 到达激活时间后开始移动
|
||||
if (useAbsolutePositioning)
|
||||
{
|
||||
if (!isMoving) return;
|
||||
ApplyAbsolutePosition(Time.time);
|
||||
return;
|
||||
}
|
||||
|
||||
// Start moving after activation time in legacy mode
|
||||
if (!isMoving && Time.time >= activationTime)
|
||||
{
|
||||
isMoving = true;
|
||||
@@ -24,26 +36,24 @@ public class HoldNoteController : MonoBehaviour
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置该段的“到达判定线的目标时间”(hitTime) 与分段延迟(delay),并由此推导移动开始时间。
|
||||
/// 注意:这里必须使用与 HoldNote.hitTime 一致的时间基准(NoteSpawner 传入的谱面实时点),
|
||||
/// 否则用 Time.time + delay 会在首段/暂停/开歌延迟时产生系统性错位,导致永远 Miss。
|
||||
/// Configure legacy timing (per-segment activation).
|
||||
/// </summary>
|
||||
public void ConfigureTiming(float baseHitTime, float segmentDelay, float travelTime)
|
||||
{
|
||||
// baseHitTime = startTime + noteData.time (+ globalHitDelay)
|
||||
float hitTime = baseHitTime + segmentDelay;
|
||||
activationTime = hitTime - travelTime;
|
||||
isMoving = false;
|
||||
useAbsolutePositioning = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 兼容旧接口:不推荐。旧版用 Time.time 会导致时间基准不一致。
|
||||
/// Legacy delay setter (not recommended).
|
||||
/// </summary>
|
||||
public void SetSegmentDelay(float segmentDelay)
|
||||
{
|
||||
// Fallback to old behavior if caller didn't use ConfigureTiming.
|
||||
activationTime = Time.time + segmentDelay;
|
||||
isMoving = false;
|
||||
useAbsolutePositioning = false;
|
||||
}
|
||||
|
||||
public void SetSpeed(float newSpeed)
|
||||
@@ -51,6 +61,23 @@ public class HoldNoteController : MonoBehaviour
|
||||
speed = newSpeed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configure absolute positioning for hold segments so they stay connected.
|
||||
/// spawnPoint should be the original spawn point for the lane.
|
||||
/// baseSpawnYOffset is applied to keep delayed segments offset upward (can be negative).
|
||||
/// </summary>
|
||||
public void ConfigureAbsolutePositioning(Vector3 spawnPoint, float newActivationTime, float spawnYOffset)
|
||||
{
|
||||
spawnPosition = spawnPoint;
|
||||
activationTime = newActivationTime;
|
||||
baseSpawnYOffset = spawnYOffset;
|
||||
useAbsolutePositioning = true;
|
||||
isMoving = true;
|
||||
|
||||
// Snap immediately to the expected position to avoid a 1-frame pop.
|
||||
ApplyAbsolutePosition(Time.time);
|
||||
}
|
||||
|
||||
public void StopMovement()
|
||||
{
|
||||
isMoving = false;
|
||||
@@ -79,4 +106,17 @@ public class HoldNoteController : MonoBehaviour
|
||||
|
||||
public float ActivationTime => activationTime;
|
||||
public float CurrentSpeed => speed;
|
||||
public bool UsesAbsolutePositioning => useAbsolutePositioning;
|
||||
public float BaseSpawnYOffset => baseSpawnYOffset;
|
||||
public Vector3 SpawnPosition => spawnPosition;
|
||||
|
||||
private void ApplyAbsolutePosition(float time)
|
||||
{
|
||||
float elapsed = time - activationTime;
|
||||
float travelDistance = speed * elapsed;
|
||||
|
||||
Vector3 newPos = spawnPosition;
|
||||
newPos.y -= (baseSpawnYOffset + travelDistance);
|
||||
transform.position = newPos;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,6 +93,29 @@ public class InputManager : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Force a release event for all bound keys and reset UI indicators.
|
||||
/// Useful for external "clear input" operations where the system should treat
|
||||
/// all keys as released regardless of physical state.
|
||||
/// </summary>
|
||||
public void ForceReleaseAllKeys()
|
||||
{
|
||||
string[] colors = new string[] { "red", "green", "yellow", "purple", "blue" };
|
||||
foreach (var color in colors)
|
||||
{
|
||||
KeyCode key = KeyBindingManager.GetKeyForColor(color);
|
||||
if (key == KeyCode.None) continue;
|
||||
try { OnKeyReleased?.Invoke(key); } catch { }
|
||||
|
||||
int index = GetIndexForColor(color);
|
||||
if (trackKeyTexts != null && index >= 0 && index < trackKeyTexts.Length)
|
||||
{
|
||||
var txt = trackKeyTexts[index];
|
||||
if (txt != null) txt.color = keyInactiveColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh displayed labels for key bindings (called after rebind)
|
||||
public void RefreshKeyLabels()
|
||||
{
|
||||
|
||||
@@ -12,7 +12,7 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: 088ed1ed6b6731f43ad3166781e3e931, type: 3}
|
||||
m_Name: NoteJudgeConfig
|
||||
m_EditorClassIdentifier:
|
||||
perfectRange: 0.05
|
||||
greatRange: 0.1
|
||||
goodRange: 0.15
|
||||
missRange: 0.2
|
||||
perfectRange: 0.1
|
||||
greatRange: 0.15
|
||||
goodRange: 0.2
|
||||
missRange: 0.25
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
@@ -7,6 +8,7 @@ public class JudgeManager : MonoBehaviour
|
||||
[Header("settlement go")]
|
||||
public GameObject settlement_go;
|
||||
public settlementController sc;
|
||||
public NoteSpawner ns;
|
||||
|
||||
public static JudgeManager Instance { get; private set; }
|
||||
|
||||
@@ -48,22 +50,49 @@ public class JudgeManager : MonoBehaviour
|
||||
/// </summary>
|
||||
public void OnAllNotesJudged()
|
||||
{
|
||||
// safe null checks and invocation
|
||||
if (settlement_go != null)
|
||||
if (settlement_go == null)
|
||||
{
|
||||
Debug.LogError("结算页面不存在!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Activate settlement UI so components run their lifecycle, then call update next frame
|
||||
try
|
||||
{
|
||||
settlement_go.SetActive(true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"[JudgeManager] Failed to activate settlement_go: {ex}");
|
||||
}
|
||||
|
||||
// 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());
|
||||
}
|
||||
|
||||
private IEnumerator InvokeSettlementUpdateNextFrame()
|
||||
{
|
||||
yield return null; // wait one frame to allow Awake/Start/OnEnable
|
||||
if (sc != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
sc?.startSettlement_uiUpdate();
|
||||
sc.startSettlement_uiUpdate();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"[JudgeManager] Exception calling startSettlement_uiUpdate: {ex}");
|
||||
}
|
||||
settlement_go.SetActive(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("结算页面不存在!");
|
||||
Debug.LogWarning("[JudgeManager] settlementController (sc) not found on settlement_go; cannot call startSettlement_uiUpdate().");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -134,11 +134,7 @@ public class Note : BaseNote
|
||||
if (Mathf.Abs(pressTime - hitTime) > maxWindow)
|
||||
{
|
||||
// outside allowed window and not in judge zone -> release lock and ignore the press
|
||||
if (hasLock)
|
||||
{
|
||||
TrackKeyManager.Instance?.UnlockTrackForJudge(TrackIndex, myId);
|
||||
hasLock = false;
|
||||
}
|
||||
ReleaseTrackLock(myId);
|
||||
if (GameConfig.verboseLogs) Debug.Log($"[Note] {noteColor} press rejected: outside timing window and not in judge zone");
|
||||
return;
|
||||
}
|
||||
@@ -157,26 +153,32 @@ public class Note : BaseNote
|
||||
if (GameConfig.verboseLogs) Debug.Log($"{keyToPress}: Perfect");
|
||||
judgeResult = "Perfect";
|
||||
ScoreManager.Instance.countPerfect += 1;
|
||||
if (TrackIndex >= 0 && TrackIndex < 5) ScoreManager.Instance.trackPerfectCounts[TrackIndex]++;
|
||||
if (noteData != null) noteData.judgeOffsetMs = rawOffsetMs;
|
||||
ScoreManager.Instance.RecordOffset(rawOffsetMs);
|
||||
}
|
||||
else if (timeDifference <= judgeConfig.greatRange)
|
||||
{
|
||||
if (GameConfig.verboseLogs) Debug.Log($"{keyToPress}: Great");
|
||||
judgeResult = "Great";
|
||||
ScoreManager.Instance.countGreat += 1;
|
||||
if (TrackIndex >= 0 && TrackIndex < 5) ScoreManager.Instance.trackGreatCounts[TrackIndex]++;
|
||||
if (noteData != null) noteData.judgeOffsetMs = rawOffsetMs;
|
||||
ScoreManager.Instance.RecordOffset(rawOffsetMs);
|
||||
}
|
||||
else if (timeDifference <= judgeConfig.goodRange)
|
||||
{
|
||||
if (GameConfig.verboseLogs) Debug.Log($"{keyToPress}: Good");
|
||||
judgeResult = "Good";
|
||||
ScoreManager.Instance.countGood += 1;
|
||||
if (TrackIndex >= 0 && TrackIndex < 5) ScoreManager.Instance.trackGoodCounts[TrackIndex]++;
|
||||
if (noteData != null) noteData.judgeOffsetMs = rawOffsetMs;
|
||||
ScoreManager.Instance.RecordOffset(rawOffsetMs);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (GameConfig.verboseLogs) Debug.Log($"{keyToPress}: Miss (timeout)");
|
||||
judgeResult = "Miss";
|
||||
JudgeMiss();
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -186,16 +188,21 @@ public class Note : BaseNote
|
||||
{
|
||||
if (GameConfig.verboseLogs) Debug.Log($"{keyToPress}: Perfect");
|
||||
judgeResult = "Perfect";
|
||||
ScoreManager.Instance.countPerfect += 1;
|
||||
// Add missing offset record for fallback path too if possible
|
||||
ScoreManager.Instance.RecordOffset(0);
|
||||
}
|
||||
else if (timeDifference <= 0.15f)
|
||||
{
|
||||
if (GameConfig.verboseLogs) Debug.Log($"{keyToPress}: Great");
|
||||
judgeResult = "Great";
|
||||
ScoreManager.Instance.countGreat += 1;
|
||||
ScoreManager.Instance.RecordOffset(0);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (GameConfig.verboseLogs) Debug.Log($"{keyToPress}: Miss (timeout)");
|
||||
judgeResult = "Miss";
|
||||
JudgeMiss();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,6 +282,11 @@ public class Note : BaseNote
|
||||
|
||||
isJudged = true;
|
||||
ScoreManager.Instance.countMiss += 1;
|
||||
if (TrackIndex >= 0 && TrackIndex < 5) ScoreManager.Instance.trackMissCounts[TrackIndex]++;
|
||||
|
||||
// Record a 0 offset for Miss to ensure total Offset Count matches note count
|
||||
ScoreManager.Instance?.RecordOffset(0);
|
||||
|
||||
if (GameConfig.verboseLogs) Debug.Log($"{keyToPress} Miss");
|
||||
|
||||
InputManager.Instance?.ShowJudgeResult(TrackIndex, "Miss");
|
||||
@@ -305,35 +317,36 @@ public class Note : BaseNote
|
||||
// notify global judge manager that this note has been finally judged (miss)
|
||||
JudgeManager.Instance?.NotifyNoteJudged();
|
||||
|
||||
// Remove from per-track queue and release lock immediately so next note can be judged
|
||||
string myId = gameObject.GetInstanceID().ToString();
|
||||
TrackKeyManager.Instance?.UnregisterKey(TrackIndex, myId);
|
||||
ReturnToPool();
|
||||
}
|
||||
|
||||
private void ReleaseTrackLock(string myId)
|
||||
{
|
||||
if (hasLock)
|
||||
{
|
||||
TrackKeyManager.Instance?.UnlockTrackForJudge(TrackIndex, myId);
|
||||
hasLock = false;
|
||||
}
|
||||
|
||||
ReturnToPool();
|
||||
}
|
||||
|
||||
private void ReturnToPool()
|
||||
{
|
||||
InputManager.OnKeyPressed -= HandlePress;
|
||||
|
||||
// CRITICAL: Always release lock and unregister before deactivating
|
||||
string myId = gameObject.GetInstanceID().ToString();
|
||||
|
||||
// Release lock if we still hold it
|
||||
if (hasLock)
|
||||
{
|
||||
TrackKeyManager.Instance?.UnlockTrackForJudge(TrackIndex, gameObject.GetInstanceID().ToString());
|
||||
hasLock = false;
|
||||
}
|
||||
ReleaseTrackLock(myId);
|
||||
|
||||
// Remove from per-track queue to avoid stale entries
|
||||
TrackKeyManager.Instance?.UnregisterKey(TrackIndex, myId);
|
||||
|
||||
if (controller != null)
|
||||
{
|
||||
controller.ResetState();
|
||||
}
|
||||
|
||||
// Ensure we are removed from per-track queue to avoid stale entries
|
||||
TrackKeyManager.Instance?.UnregisterKey(TrackIndex, gameObject.GetInstanceID().ToString());
|
||||
|
||||
gameObject.SetActive(false);
|
||||
NotePool.Instance.ReturnNote(gameObject, noteColor);
|
||||
}
|
||||
|
||||
@@ -93,7 +93,8 @@ public class NoteController : MonoBehaviour
|
||||
{
|
||||
isInJudgeZone = true;
|
||||
// register note by instance id so TrackKeyManager can prioritize
|
||||
TrackKeyManager.Instance.RegisterKey(linkedNote.GetTrackIndex(), linkedNote.gameObject.GetInstanceID().ToString());
|
||||
// tap notes are short notes without hold
|
||||
TrackKeyManager.Instance.RegisterKey(linkedNote.GetTrackIndex(), linkedNote.gameObject.GetInstanceID().ToString(), "tap");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,14 @@ public class NotePool : MonoBehaviour
|
||||
public GameObject[] holdNotePrefabs; // ������Ƭ��Ԥ����
|
||||
public GameObject[] holdNoteEndPrefabs; // ������β��ר�ó�����Ԥ���壨��ѡ��
|
||||
public GameObject[] startNotePrefabs; // ������startƬ��Ԥ����
|
||||
private int poolSize = 24; // ÿ�����͵������ش�С (increased)
|
||||
private int maxPoolSize = 120; // �ص�������� (increased)
|
||||
|
||||
[Header("Pool Size")]
|
||||
public int poolSize = 24;
|
||||
public int maxPoolSize = 300;
|
||||
[Tooltip("Multiplier for hold segment pool size (per color).")]
|
||||
public int holdSegmentPoolMultiplier = 8;
|
||||
[Tooltip("Multiplier for hold end pool size (per color).")]
|
||||
public int holdEndPoolMultiplier = 8;
|
||||
|
||||
// ��ɫ�������Ŀ���ӳ�䣬����ÿ�� switch
|
||||
private Dictionary<string, int> colorIndexMap;
|
||||
@@ -118,10 +124,10 @@ public class NotePool : MonoBehaviour
|
||||
private IEnumerator PrewarmCoroutine(int colorCount)
|
||||
{
|
||||
// Calculate targets
|
||||
int noteTarget = poolSize; // per color
|
||||
int startTarget = poolSize;
|
||||
int holdTarget = poolSize * 5;
|
||||
int endTarget = poolSize * 5;
|
||||
int noteTarget = Mathf.Min(poolSize, maxPoolSize); // per color
|
||||
int startTarget = Mathf.Min(poolSize, maxPoolSize);
|
||||
int holdTarget = Mathf.Min(poolSize * Mathf.Max(1, holdSegmentPoolMultiplier), maxPoolSize);
|
||||
int endTarget = Mathf.Min(poolSize * Mathf.Max(1, holdEndPoolMultiplier), maxPoolSize);
|
||||
|
||||
while (true)
|
||||
{
|
||||
@@ -237,6 +243,34 @@ public class NotePool : MonoBehaviour
|
||||
{
|
||||
if (obj == null || pool == null) return; // ��ֹ������
|
||||
|
||||
// Defensive: unregister/unlock any TrackKeyManager state referencing this object to avoid stale queues/locks
|
||||
try
|
||||
{
|
||||
// Try to determine track index from Note or HoldNote components
|
||||
int trackIdx = -1;
|
||||
var noteComp = obj.GetComponent<Note>();
|
||||
if (noteComp != null)
|
||||
{
|
||||
trackIdx = noteComp.GetTrackIndex();
|
||||
}
|
||||
else
|
||||
{
|
||||
var holdComp = obj.GetComponent<HoldNote>();
|
||||
if (holdComp != null)
|
||||
{
|
||||
trackIdx = holdComp.trackIndex;
|
||||
}
|
||||
}
|
||||
|
||||
string id = obj.GetInstanceID().ToString();
|
||||
if (TrackKeyManager.Instance != null && trackIdx >= 0)
|
||||
{
|
||||
try { TrackKeyManager.Instance.UnregisterKey(trackIdx, id); } catch { }
|
||||
try { TrackKeyManager.Instance.UnlockTrackForJudge(trackIdx, id); } catch { }
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
// �����ظ��黹������������ڳ��ڣ�ֱ�Ӻ���
|
||||
GamePlay.PoolItem pi = obj.GetComponent<GamePlay.PoolItem>();
|
||||
if (pi != null && pi.inPool) return;
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using TMPro;
|
||||
using UnityEngine.UI;
|
||||
|
||||
public class NoteSpawner : MonoBehaviour
|
||||
{
|
||||
public event Action AllNotesSpawned; // invoked when all notes have been spawned
|
||||
|
||||
public NotePool notePool; // 引用 NotePool
|
||||
public Transform[] spawnPoints; // 对应轨道的生成点
|
||||
public GameObject[] notePrefabs; // 短音符预制体
|
||||
@@ -42,6 +47,19 @@ public class NoteSpawner : MonoBehaviour
|
||||
[Tooltip("When enabled, spawn positioning will compensate each segment's position based on its own activation time so newly spawned segments appear at their expected traveled position. Default: OFF.")]
|
||||
public bool enableYOffsetCompensation = false;
|
||||
|
||||
[Header("Immediate Settlement")]
|
||||
[Tooltip("Optional. If set, this will be used to trigger settlement UI (JudgeManager.TriggerAllNotesJudged). If null, will fall back to JudgeManager.Instance.")]
|
||||
public JudgeManager judgeManager;
|
||||
|
||||
[Tooltip("Optional UI Button. When clicked, will immediately stop spawning and enter settlement.")]
|
||||
public Button immediateSettlementButton;
|
||||
|
||||
[Tooltip("Optional: the pause UI GameObject to disable when immediate settlement is triggered.")]
|
||||
public GameObject pausePanel;
|
||||
|
||||
[Tooltip("Optional: animations GameObject to disable when entering settlement. Will be restored on Start().")]
|
||||
public GameObject animations;
|
||||
|
||||
// optional: constants for runtime clamping (kept for internal use)
|
||||
private const float SpeedMultiplierMin = 0.8f;
|
||||
private const float SpeedMultiplierMax = 1.25f;
|
||||
@@ -54,8 +72,17 @@ public class NoteSpawner : MonoBehaviour
|
||||
|
||||
private static int holdNoteIdCounter = 0; // 全局唯一长音符 ID 计数器
|
||||
|
||||
// map from beatmap note index -> assigned holdNoteId (for hold notes only)
|
||||
private Dictionary<int, int> noteIndexToHoldId = new Dictionary<int, int>();
|
||||
|
||||
private const string NoteSpeedPrefKey = "noteSpeedMultiplier";
|
||||
|
||||
private Coroutine spawnCoroutine;
|
||||
private Coroutine settlementCoroutine;
|
||||
|
||||
// Guard to avoid double-trigger / deadlock
|
||||
private bool immediateSettlementTriggered = false;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
// Load saved visual speed multiplier before any spawning logic uses it
|
||||
@@ -65,6 +92,83 @@ public class NoteSpawner : MonoBehaviour
|
||||
if (GameConfig.verboseLogs) Debug.Log($"[NoteSpawner] Loaded speedMultiplier={speedMultiplier} from PlayerPrefs");
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
// 每次场景启用/加载时:清空立刻结算状态,避免进入场景即锁定
|
||||
ResetImmediateSettlementState();
|
||||
BindImmediateSettlementButton();
|
||||
|
||||
// subscribe to JudgeManager.AllNotesJudged so we disable animations on normal settlement as well
|
||||
TrySubscribeJudgeManager();
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// restore animations active state on Start
|
||||
if (animations != null)
|
||||
{
|
||||
try { animations.SetActive(true); }
|
||||
catch { }
|
||||
}
|
||||
|
||||
// ensure subscription if JudgeManager.Instance was not ready during OnEnable
|
||||
TrySubscribeJudgeManager();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
UnbindImmediateSettlementButton();
|
||||
TryUnsubscribeJudgeManager();
|
||||
}
|
||||
|
||||
private void ResetImmediateSettlementState()
|
||||
{
|
||||
immediateSettlementTriggered = false;
|
||||
// 不强制设置 isSpawning,这个状态由 LoadBeatmap 驱动;这里只清锁
|
||||
}
|
||||
|
||||
private void BindImmediateSettlementButton()
|
||||
{
|
||||
if (immediateSettlementButton == null) return;
|
||||
try { immediateSettlementButton.onClick.RemoveListener(ForceImmediateSettlement); } catch { }
|
||||
immediateSettlementButton.onClick.AddListener(ForceImmediateSettlement);
|
||||
}
|
||||
|
||||
private void UnbindImmediateSettlementButton()
|
||||
{
|
||||
if (immediateSettlementButton == null) return;
|
||||
try { immediateSettlementButton.onClick.RemoveListener(ForceImmediateSettlement); } catch { }
|
||||
}
|
||||
|
||||
private void TrySubscribeJudgeManager()
|
||||
{
|
||||
var jm = judgeManager != null ? judgeManager : JudgeManager.Instance;
|
||||
if (jm != null)
|
||||
{
|
||||
try { jm.AllNotesJudged -= OnSettlementTriggered; } catch { }
|
||||
jm.AllNotesJudged += OnSettlementTriggered;
|
||||
}
|
||||
}
|
||||
|
||||
private void TryUnsubscribeJudgeManager()
|
||||
{
|
||||
var jm = judgeManager != null ? judgeManager : JudgeManager.Instance;
|
||||
if (jm != null)
|
||||
{
|
||||
try { jm.AllNotesJudged -= OnSettlementTriggered; } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSettlementTriggered()
|
||||
{
|
||||
// Disable animations when settlement begins
|
||||
if (animations != null)
|
||||
{
|
||||
try { animations.SetActive(false); }
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
public void LoadBeatmap(Beatmap loadedBeatmap)
|
||||
{
|
||||
if (isSpawning) return;
|
||||
@@ -89,7 +193,9 @@ public class NoteSpawner : MonoBehaviour
|
||||
bpm = beatmap.bpm;
|
||||
startTime = Time.time;
|
||||
Debug.Log($"歌曲开始时间: {startTime}");
|
||||
StartCoroutine(SpawnNotes());
|
||||
|
||||
// keep reference so we can stop spawning when doing immediate settlement
|
||||
spawnCoroutine = StartCoroutine(SpawnNotes());
|
||||
}
|
||||
|
||||
private IEnumerator SpawnNotes()
|
||||
@@ -101,8 +207,14 @@ public class NoteSpawner : MonoBehaviour
|
||||
yield break;
|
||||
}
|
||||
|
||||
foreach (NoteData note in beatmap.notes)
|
||||
// iterate with index so we can map hold notes to generated ids
|
||||
for (int i = 0; i < beatmap.notes.Length; i++)
|
||||
{
|
||||
// 允许外部在运行中打断生成(例如“立刻结算”)
|
||||
if (!isSpawning)
|
||||
yield break;
|
||||
|
||||
NoteData note = beatmap.notes[i];
|
||||
// clamp speed multiplier to supported range
|
||||
float sm = Mathf.Clamp(speedMultiplier, SpeedMultiplierMin, SpeedMultiplierMax);
|
||||
|
||||
@@ -128,7 +240,9 @@ public class NoteSpawner : MonoBehaviour
|
||||
|
||||
if (note.type == "hold")
|
||||
{
|
||||
SpawnHoldNote(note);
|
||||
// create the hold note once and record its id mapping
|
||||
int hid = SpawnHoldNote(note);
|
||||
noteIndexToHoldId[i] = hid;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -137,6 +251,110 @@ public class NoteSpawner : MonoBehaviour
|
||||
}
|
||||
|
||||
isSpawning = false;
|
||||
spawnCoroutine = null;
|
||||
|
||||
// Notify subscribers that all notes have been spawned
|
||||
AllNotesSpawned?.Invoke();
|
||||
|
||||
// Settlement routine will be started by GameManager via StartSettlementRoutine() call
|
||||
// after receiving the AllNotesSpawned event
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 立刻结算:
|
||||
/// - 停止继续生成音符(停止 SpawnNotes 协程)
|
||||
/// - 取消 NoteSpawner 自己的延迟结算协程(PostSpawnSettlementRoutine)
|
||||
/// - 立刻触发结算流程(JudgeManager.TriggerAllNotesJudged)
|
||||
/// 注意:该方法不会清理场上已生成的音符,仅停止后续生成并进入结算。
|
||||
/// </summary>
|
||||
[ContextMenu("Force Immediate Settlement")]
|
||||
public void ForceImmediateSettlement()
|
||||
{
|
||||
if (immediateSettlementTriggered)
|
||||
{
|
||||
Debug.LogWarning("[NoteSpawner] ForceImmediateSettlement ignored: already triggered.");
|
||||
return;
|
||||
}
|
||||
immediateSettlementTriggered = true;
|
||||
|
||||
Debug.LogWarning("[NoteSpawner] ForceImmediateSettlement called: stopping further note spawning and triggering settlement now.");
|
||||
|
||||
// If a pause UI is assigned, disable it immediately to avoid stuck paused UI during settlement
|
||||
if (pausePanel != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
pausePanel.SetActive(false);
|
||||
Debug.Log("[NoteSpawner] pausePanel has been disabled by immediate settlement.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning("[NoteSpawner] Failed to disable pausePanel: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
// Also disable animations if assigned
|
||||
if (animations != null)
|
||||
{
|
||||
try { animations.SetActive(false); }
|
||||
catch { }
|
||||
}
|
||||
|
||||
// Restore pause manager state if present, otherwise fallback to setting timeScale
|
||||
try
|
||||
{
|
||||
var pm = PauseManager.Instance;
|
||||
if (pm != null)
|
||||
{
|
||||
pm.Pause(false);
|
||||
if (GameConfig.verboseLogs) Debug.Log("[NoteSpawner] PauseManager.Pause(false) called to resume time.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Time.timeScale = 1f;
|
||||
if (GameConfig.verboseLogs) Debug.Log("[NoteSpawner] PauseManager not found; Time.timeScale set to 1 as fallback.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
try { Time.timeScale = 1f; } catch { }
|
||||
Debug.LogWarning("[NoteSpawner] Exception while restoring time scale: " + ex.Message);
|
||||
}
|
||||
|
||||
// stop spawning loop
|
||||
isSpawning = false;
|
||||
|
||||
// cancel spawn coroutine
|
||||
if (spawnCoroutine != null)
|
||||
{
|
||||
try { StopCoroutine(spawnCoroutine); } catch { }
|
||||
spawnCoroutine = null;
|
||||
}
|
||||
|
||||
// cancel delayed settlement coroutine if any
|
||||
if (settlementCoroutine != null)
|
||||
{
|
||||
try { StopCoroutine(settlementCoroutine); } catch { }
|
||||
settlementCoroutine = null;
|
||||
}
|
||||
|
||||
// Disable animations when settlement begins
|
||||
if (animations != null)
|
||||
{
|
||||
try { animations.SetActive(false); }
|
||||
catch { }
|
||||
}
|
||||
|
||||
// trigger settlement
|
||||
var jm = judgeManager != null ? judgeManager : JudgeManager.Instance;
|
||||
if (jm != null)
|
||||
{
|
||||
jm.TriggerAllNotesJudged();
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("[NoteSpawner] ForceImmediateSettlement failed: JudgeManager reference missing.");
|
||||
}
|
||||
}
|
||||
|
||||
// 生成短音符
|
||||
@@ -217,25 +435,26 @@ public class NoteSpawner : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
public void SpawnHoldNote(NoteData noteData)
|
||||
// Modified: return generated holdNoteId so callers can map notes to ids
|
||||
public int SpawnHoldNote(NoteData noteData)
|
||||
{
|
||||
if (noteData.trackIndex < 0 || noteData.trackIndex >= spawnPoints.Length)
|
||||
{
|
||||
Debug.LogError("轨道索引超出范围!");
|
||||
return;
|
||||
return -1;
|
||||
}
|
||||
|
||||
KeyCode key = KeyBindingManager.GetKeyForColor(noteData.color);
|
||||
if (key == KeyCode.None)
|
||||
{
|
||||
Debug.LogError($"未找到颜色 {noteData.color} 对应的按键!");
|
||||
return;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(noteData.color))
|
||||
{
|
||||
Debug.LogError("[NoteSpawner] noteData.color 为空,无法生成音符!");
|
||||
return;
|
||||
return -1;
|
||||
}
|
||||
|
||||
Debug.Log($"生成时间:{Time.time}");
|
||||
@@ -271,19 +490,11 @@ public class NoteSpawner : MonoBehaviour
|
||||
if (startObj == null)
|
||||
{
|
||||
Debug.LogError("对象池返回空 hold note start!");
|
||||
return;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// compute per-segment compensation only if enabled
|
||||
float startYOffset = 0f;
|
||||
if (enableYOffsetCompensation)
|
||||
{
|
||||
float startActivation = baseHit - effectiveTravelTimeHold;
|
||||
float timeSinceActivation = Time.time - startActivation;
|
||||
startYOffset = Mathf.Max(0f, timeSinceActivation * holdSpeed);
|
||||
}
|
||||
|
||||
startObj.transform.position = spawnPoint.position + Vector3.down * startYOffset;
|
||||
// Hold segments use absolute positioning; spawn at the lane origin.
|
||||
startObj.transform.position = spawnPoint.position;
|
||||
startObj.transform.rotation = Quaternion.identity;
|
||||
|
||||
HoldNote holdNote = startObj.GetComponent<HoldNote>();
|
||||
@@ -301,7 +512,7 @@ public class NoteSpawner : MonoBehaviour
|
||||
else
|
||||
{
|
||||
Debug.LogError("长音符 start 部分缺少 HoldNote 组件!");
|
||||
return;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// 生成 Middle 片段(1 .. segmentCount-1)
|
||||
@@ -317,16 +528,7 @@ public class NoteSpawner : MonoBehaviour
|
||||
continue;
|
||||
}
|
||||
|
||||
float segYOffset = 0f;
|
||||
if (enableYOffsetCompensation)
|
||||
{
|
||||
float segHit = baseHit + segmentDelay;
|
||||
float segActivation = segHit - effectiveTravelTimeHold;
|
||||
float timeSinceActivationSeg = Time.time - segActivation;
|
||||
segYOffset = Mathf.Max(0f, timeSinceActivationSeg * holdSpeed);
|
||||
}
|
||||
|
||||
segObj.transform.position = spawnPoint.position + Vector3.down * segYOffset;
|
||||
segObj.transform.position = spawnPoint.position;
|
||||
segObj.transform.rotation = Quaternion.identity;
|
||||
|
||||
HoldNote holdSeg = segObj.GetComponent<HoldNote>();
|
||||
@@ -341,7 +543,7 @@ public class NoteSpawner : MonoBehaviour
|
||||
holdSeg.visualSpeedMultiplier = smLocalHold;
|
||||
|
||||
// Immediately calibrate position and schedule additional checks to correct any offset
|
||||
Vector3 calibSpawnPos = spawnPoint.position + Vector3.down * segYOffset;
|
||||
Vector3 calibSpawnPos = spawnPoint.position;
|
||||
holdSeg.CalibratePosition(calibSpawnPos, calibrateTolerance);
|
||||
StartCoroutine(CalibrateAfterSpawn(holdSeg, calibSpawnPos));
|
||||
}
|
||||
@@ -356,21 +558,12 @@ public class NoteSpawner : MonoBehaviour
|
||||
if (endObj == null)
|
||||
{
|
||||
Debug.LogError("对象池返回空 hold note 片段(end)!");
|
||||
return;
|
||||
return -1;
|
||||
}
|
||||
|
||||
float endDelay = segmentCount * actualSegmentInterval; // 不按 speedMultiplier 缩放,使用谱面长度保证尾段紧随中段
|
||||
|
||||
float endYOffset = 0f;
|
||||
if (enableYOffsetCompensation)
|
||||
{
|
||||
float endHit = baseHit + endDelay;
|
||||
float endActivation = endHit - effectiveTravelTimeHold;
|
||||
float timeSinceActivationEnd = Time.time - endActivation;
|
||||
endYOffset = Mathf.Max(0f, timeSinceActivationEnd * holdSpeed);
|
||||
}
|
||||
|
||||
endObj.transform.position = spawnPoint.position + Vector3.down * endYOffset;
|
||||
endObj.transform.position = spawnPoint.position;
|
||||
endObj.transform.rotation = Quaternion.identity;
|
||||
|
||||
// 为便于识别,将实例名追加后缀
|
||||
@@ -389,7 +582,7 @@ public class NoteSpawner : MonoBehaviour
|
||||
holdEnd.visualSpeedMultiplier = smLocalHold;
|
||||
|
||||
// schedule calibration for end as well to be safe
|
||||
Vector3 calibEndPos = spawnPoint.position + Vector3.down * endYOffset;
|
||||
Vector3 calibEndPos = spawnPoint.position;
|
||||
holdEnd.CalibratePosition(calibEndPos, calibrateTolerance);
|
||||
StartCoroutine(CalibrateAfterSpawn(holdEnd, calibEndPos));
|
||||
}
|
||||
@@ -397,6 +590,8 @@ public class NoteSpawner : MonoBehaviour
|
||||
{
|
||||
Debug.LogError("长音符 end 部分缺少 HoldNote 组件!");
|
||||
}
|
||||
|
||||
return holdNoteId;
|
||||
}
|
||||
|
||||
private IEnumerator CalibrateAfterSpawn(HoldNote seg, Vector3 spawnPos)
|
||||
@@ -440,6 +635,206 @@ public class NoteSpawner : MonoBehaviour
|
||||
return 10.75f / noteTravelTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Public method to start the settlement routine. Called by GameManager when all notes have been spawned.
|
||||
/// </summary>
|
||||
public void StartSettlementRoutine()
|
||||
{
|
||||
if (GameConfig.verboseLogs) Debug.Log("[NoteSpawner] StartSettlementRoutine called - beginning settlement countdown");
|
||||
// keep reference so it can be cancelled by ForceImmediateSettlement
|
||||
if (settlementCoroutine != null) StopCoroutine(settlementCoroutine);
|
||||
settlementCoroutine = StartCoroutine(PostSpawnSettlementRoutine(5));
|
||||
}
|
||||
|
||||
private IEnumerator PostSpawnSettlementRoutine(int lookback)
|
||||
{
|
||||
// Get relevant notes: last notes within 3 seconds before the last note time
|
||||
if (beatmap == null || beatmap.notes == null || beatmap.notes.Length == 0)
|
||||
yield break;
|
||||
|
||||
NoteData lastNote = beatmap.notes[beatmap.notes.Length - 1];
|
||||
float lastNoteTime = lastNote.time;
|
||||
float timeRangeStart = lastNoteTime - 3f; // 3 seconds window instead of 0.5 seconds
|
||||
|
||||
// Collect notes within the range [timeRangeStart, lastNoteTime]
|
||||
List<NoteData> relevantNotes = new List<NoteData>();
|
||||
for (int i = beatmap.notes.Length - 1; i >= 0; i--)
|
||||
{
|
||||
NoteData note = beatmap.notes[i];
|
||||
if (note.time >= timeRangeStart)
|
||||
{
|
||||
relevantNotes.Add(note);
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Reverse to process in chronological order
|
||||
relevantNotes.Reverse();
|
||||
|
||||
if (GameConfig.verboseLogs) Debug.Log($"[NoteSpawner] Found {relevantNotes.Count} relevant notes in range [{timeRangeStart:F3}, {lastNoteTime:F3}]");
|
||||
|
||||
// Analyze notes: check if any are hold notes and find the latest ending note
|
||||
bool hasHoldNotes = false;
|
||||
float maxEndTime = float.MinValue;
|
||||
NoteData maxEndNote = null;
|
||||
|
||||
foreach (var note in relevantNotes)
|
||||
{
|
||||
float noteEndTime;
|
||||
if (note.type == "hold")
|
||||
{
|
||||
hasHoldNotes = true;
|
||||
noteEndTime = note.time + note.length;
|
||||
}
|
||||
else
|
||||
{
|
||||
noteEndTime = note.time;
|
||||
}
|
||||
|
||||
if (noteEndTime > maxEndTime)
|
||||
{
|
||||
maxEndTime = noteEndTime;
|
||||
maxEndNote = note;
|
||||
}
|
||||
|
||||
if (GameConfig.verboseLogs)
|
||||
{
|
||||
Debug.Log($"[NoteSpawner] Note: type={note.type}, time={note.time:F3}, endTime={noteEndTime:F3}");
|
||||
}
|
||||
}
|
||||
|
||||
// Determine initial wait and type of max end note
|
||||
float initialWait = 0f;
|
||||
bool maxIsHold = false;
|
||||
|
||||
if (maxEndNote != null)
|
||||
{
|
||||
if (maxEndNote.type == "hold")
|
||||
{
|
||||
maxIsHold = true;
|
||||
initialWait = maxEndNote.length;
|
||||
}
|
||||
else
|
||||
{
|
||||
maxIsHold = false;
|
||||
initialWait = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
if (GameConfig.verboseLogs)
|
||||
{
|
||||
Debug.Log($"[NoteSpawner] Max end note: type={maxEndNote?.type}, time={maxEndNote?.time:F3}, maxIsHold={maxIsHold}, initialWait={initialWait:F3}");
|
||||
}
|
||||
|
||||
// Log final chosen extension method and total extension (initial wait + final buffer) as an error for visibility
|
||||
float finalBuffer = 3f; // final buffer used by settlement routine (changed to 3s)
|
||||
float totalWait = initialWait + finalBuffer;
|
||||
string methodName = maxIsHold ? "hold" : "tap";
|
||||
Debug.LogError($"[NoteSpawner] Settlement extension chosen: method={methodName}, initialWait={initialWait:F3}s, finalBuffer={finalBuffer:F3}s, totalWait={totalWait:F3}s");
|
||||
|
||||
// Clear the reference list to release memory after use
|
||||
relevantNotes.Clear();
|
||||
relevantNotes = null;
|
||||
|
||||
// Wait for initial period (hold length or 0)
|
||||
if (initialWait > 0f)
|
||||
{
|
||||
if (GameConfig.verboseLogs) Debug.Log($"[NoteSpawner] Waiting for initial hold length: {initialWait:F3}s");
|
||||
float target = Time.time + initialWait;
|
||||
while (Time.time < target)
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Final buffer: use finalBuffer variable
|
||||
if (GameConfig.verboseLogs) Debug.Log($"[NoteSpawner] Entering final {finalBuffer} s settlement buffer");
|
||||
|
||||
// Wait until 1s remaining, then clear input (clear at finalBuffer-1 seconds mark)
|
||||
float timeBeforeClear = Mathf.Max(0f, finalBuffer - 1f);
|
||||
if (timeBeforeClear > 0f)
|
||||
{
|
||||
float tBefore = Time.time + timeBeforeClear;
|
||||
while (Time.time < tBefore)
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Force clear keyboard/input state at the 1s-before-end mark
|
||||
ForceClearInputState(null);
|
||||
|
||||
// Wait the remaining 1s
|
||||
float tAfter = Time.time + 1f;
|
||||
while (Time.time < tAfter)
|
||||
yield return null;
|
||||
|
||||
// Trigger settlement
|
||||
try
|
||||
{
|
||||
if (JudgeManager.Instance != null)
|
||||
{
|
||||
JudgeManager.Instance.OnAllNotesJudged();
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("[NoteSpawner] JudgeManager.Instance is null when trying to trigger settlement");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning("[NoteSpawner] Exception while triggering settlement: " + ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void ForceClearInputState(List<NoteData> relevantNotes)
|
||||
{
|
||||
// Fire UI key release handlers: reset InputManager key indicator colors
|
||||
var im = InputManager.Instance;
|
||||
if (im != null)
|
||||
{
|
||||
var texts = im.trackKeyTexts;
|
||||
if (texts != null)
|
||||
{
|
||||
for (int ti = 0; ti < texts.Length; ti++)
|
||||
{
|
||||
if (texts[ti] != null)
|
||||
texts[ti].color = im.keyInactiveColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reset per-frame consumption and unlock any track locks
|
||||
if (TrackKeyManager.Instance != null)
|
||||
{
|
||||
TrackKeyManager.Instance.ResetConsumptionState();
|
||||
TrackKeyManager.Instance.ClearAllLocks();
|
||||
if (GameConfig.verboseLogs) Debug.Log("[NoteSpawner] TrackKeyManager consumption state reset and locks cleared");
|
||||
}
|
||||
|
||||
// Mark all relevant hold notes as released in JudgeManager
|
||||
// Only process if relevantNotes is provided (non-null)
|
||||
if (relevantNotes != null && JudgeManager.Instance != null && beatmap != null && beatmap.notes != null)
|
||||
{
|
||||
foreach (var note in relevantNotes)
|
||||
{
|
||||
if (note == null) continue;
|
||||
if (note.type == "hold")
|
||||
{
|
||||
// Find the index of this note in beatmap to get its hold ID
|
||||
for (int i = 0; i < beatmap.notes.Length; i++)
|
||||
{
|
||||
if (beatmap.notes[i] == note && noteIndexToHoldId.ContainsKey(i))
|
||||
{
|
||||
int holdId = noteIndexToHoldId[i];
|
||||
try { JudgeManager.Instance.RegisterNoteReleased(holdId.ToString(), true); }
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
//if (globalGameTime.text=="0.01")
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using System.Collections;
|
||||
|
||||
public class TrackKeyManager : MonoBehaviour
|
||||
{
|
||||
public static TrackKeyManager Instance { get; private set; }
|
||||
|
||||
// store note instance IDs (as strings) in queue order per track
|
||||
private Dictionary<int, Queue<string>> trackKeyMappings = new Dictionary<int, Queue<string>>();
|
||||
// Format: trackIndex -> Queue of (noteId, noteType) tuples
|
||||
private Dictionary<int, Queue<(string noteId, string noteType)>> trackKeyMappings =
|
||||
new Dictionary<int, Queue<(string, string)>>();
|
||||
|
||||
// Track notes currently being judged to prevent double-judging the same note
|
||||
// Format: trackIndex -> HashSet of note IDs being judged
|
||||
@@ -14,11 +17,28 @@ public class TrackKeyManager : MonoBehaviour
|
||||
|
||||
// transient per-frame consumption: ensures one physical press only affects one note per track per frame
|
||||
private Dictionary<int, int> trackConsumedFrame = new Dictionary<int, int>();
|
||||
|
||||
|
||||
// track last frame we checked for focus loss
|
||||
private int lastCheckedFrame = -1;
|
||||
private bool lastFocusedState = true;
|
||||
|
||||
// --- Anti-deadlock housekeeping ---
|
||||
// Some notes can be pooled/disabled without firing OnTriggerExit2D, leaving stale ids in the queue.
|
||||
// To guarantee the track never blocks forever, each queued id gets a TTL.
|
||||
// When TTL expires, we prune it as stale.
|
||||
private readonly Dictionary<string, float> noteIdExpiry = new Dictionary<string, float>();
|
||||
|
||||
[Header("TrackKeyManager housekeeping")]
|
||||
[Tooltip("Seconds a queued note id is allowed to stay without being removed. Prevents permanent deadlocks when OnTriggerExit2D is missed.")]
|
||||
[Range(0.5f, 10f)]
|
||||
public float queuedIdTtlSeconds = 3f;
|
||||
|
||||
[Tooltip("Maximum number of stale head entries to prune per track per call.")]
|
||||
[Range(1, 20)]
|
||||
public int pruneBatchSize = 5;
|
||||
|
||||
private Coroutine cleanupCoroutine;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance == null)
|
||||
@@ -29,14 +49,129 @@ public class TrackKeyManager : MonoBehaviour
|
||||
{
|
||||
Destroy(gameObject);
|
||||
}
|
||||
|
||||
// start periodic cleanup to defend against stale locks that can survive missed Unregister/Unlock calls
|
||||
StartCleanupRoutine();
|
||||
}
|
||||
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
StopCleanupRoutine();
|
||||
}
|
||||
|
||||
private void StartCleanupRoutine()
|
||||
{
|
||||
if (cleanupCoroutine == null)
|
||||
cleanupCoroutine = StartCoroutine(CleanupStaleLocksRoutine());
|
||||
}
|
||||
|
||||
private void StopCleanupRoutine()
|
||||
{
|
||||
if (cleanupCoroutine != null)
|
||||
{
|
||||
StopCoroutine(cleanupCoroutine);
|
||||
cleanupCoroutine = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Periodically scan locks and queued ids to remove stale entries that can cause deadlocks.
|
||||
private IEnumerator CleanupStaleLocksRoutine()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
// run once per second (unscaled so it runs during pause)
|
||||
yield return new WaitForSecondsRealtime(1f);
|
||||
|
||||
float now = Time.unscaledTime;
|
||||
|
||||
// 1) Prune head entries for all tracks (defensive)
|
||||
var trackKeys = new List<int>(trackKeyMappings.Keys);
|
||||
foreach (var ti in trackKeys)
|
||||
{
|
||||
PruneStaleHeads(ti);
|
||||
}
|
||||
|
||||
// 2) Inspect locks and release any that reference ids that are expired or not present in any queue
|
||||
var trackLockKeys = new List<int>(trackNotesBeingJudged.Keys);
|
||||
foreach (var t in trackLockKeys)
|
||||
{
|
||||
var locks = trackNotesBeingJudged[t];
|
||||
if (locks == null || locks.Count == 0) continue;
|
||||
|
||||
var toRemove = new List<string>();
|
||||
|
||||
foreach (var noteId in locks)
|
||||
{
|
||||
bool hasExpiry = noteIdExpiry.TryGetValue(noteId, out var expiry);
|
||||
bool expired = hasExpiry && now > expiry;
|
||||
|
||||
// If expiry missing or expired, consider the id stale and remove lock
|
||||
if (!hasExpiry || expired)
|
||||
{
|
||||
toRemove.Add(noteId);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Also if the queues do not contain this id anywhere, it is likely stale
|
||||
bool foundInQueues = false;
|
||||
foreach (var qpair in trackKeyMappings)
|
||||
{
|
||||
foreach (var item in qpair.Value)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(item.noteId) && item.noteId == noteId)
|
||||
{
|
||||
foundInQueues = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (foundInQueues) break;
|
||||
}
|
||||
if (!foundInQueues)
|
||||
{
|
||||
toRemove.Add(noteId);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var id in toRemove)
|
||||
{
|
||||
locks.Remove(id);
|
||||
noteIdExpiry.Remove(id);
|
||||
if (GameConfig.verboseLogs) Debug.LogWarning($"[TrackKeyManager] Cleanup: removed stale lock id={id} on track {t}");
|
||||
}
|
||||
|
||||
// If locks become empty, remove dictionary entry to keep structure clean
|
||||
if (locks.Count == 0)
|
||||
{
|
||||
trackNotesBeingJudged.Remove(t);
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Defensive: if a track has no queued ids but has consumed-frame or locks, clear them
|
||||
var allTracks = new HashSet<int>();
|
||||
foreach (var k in trackKeyMappings.Keys) allTracks.Add(k);
|
||||
foreach (var k in trackNotesBeingJudged.Keys) allTracks.Add(k);
|
||||
foreach (var k in trackConsumedFrame.Keys) allTracks.Add(k);
|
||||
|
||||
foreach (var track in allTracks)
|
||||
{
|
||||
bool hasQueue = trackKeyMappings.ContainsKey(track) && trackKeyMappings[track].Count > 0;
|
||||
bool hasLocks = trackNotesBeingJudged.ContainsKey(track) && trackNotesBeingJudged[track].Count > 0;
|
||||
if (!hasQueue && hasLocks)
|
||||
{
|
||||
// clear locks for this track
|
||||
trackNotesBeingJudged.Remove(track);
|
||||
if (GameConfig.verboseLogs) Debug.LogWarning($"[TrackKeyManager] Cleanup: cleared locks for empty track {track}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// Clear per-frame consumption dictionary when focus is regained or at frame boundaries
|
||||
int currentFrame = Time.frameCount;
|
||||
bool isFocused = Application.isFocused;
|
||||
|
||||
|
||||
// If we've moved to a new frame or focus state changed, clear consumption tracking
|
||||
if (lastCheckedFrame != currentFrame || (isFocused && !lastFocusedState))
|
||||
{
|
||||
@@ -47,20 +182,65 @@ public class TrackKeyManager : MonoBehaviour
|
||||
if (GameConfig.verboseLogs) Debug.Log("[TrackKeyManager] Focus regained, cleared track consumption");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
lastCheckedFrame = currentFrame;
|
||||
lastFocusedState = isFocused;
|
||||
}
|
||||
|
||||
private void PruneStaleHeads(int trackIndex)
|
||||
{
|
||||
if (!trackKeyMappings.TryGetValue(trackIndex, out var q) || q == null || q.Count == 0)
|
||||
return;
|
||||
|
||||
int pruned = 0;
|
||||
float now = Time.unscaledTime;
|
||||
|
||||
while (q.Count > 0 && pruned < Mathf.Max(1, pruneBatchSize))
|
||||
{
|
||||
var head = q.Peek();
|
||||
if (string.IsNullOrEmpty(head.noteId))
|
||||
{
|
||||
q.Dequeue();
|
||||
pruned++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// never prune if this id is currently locked for judgement
|
||||
if (trackNotesBeingJudged.TryGetValue(trackIndex, out var locks) && locks != null && locks.Contains(head.noteId))
|
||||
break;
|
||||
|
||||
// if an expiry exists and has passed, prune
|
||||
if (noteIdExpiry.TryGetValue(head.noteId, out var expiry) && now > expiry)
|
||||
{
|
||||
q.Dequeue();
|
||||
noteIdExpiry.Remove(head.noteId);
|
||||
pruned++;
|
||||
if (GameConfig.verboseLogs)
|
||||
Debug.LogWarning($"[TrackKeyManager] Pruned stale head on track {trackIndex}: id={head.noteId} type={head.noteType} now={now:F2} expiry={expiry:F2}");
|
||||
continue;
|
||||
}
|
||||
|
||||
// head is still within TTL
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当音符进入判定区域时,添加音符 id 到队列
|
||||
/// 当音符进入判定区域时,添加音符 id 到队列(包含note类型信息)
|
||||
/// noteType: "tap" 或 "hold"
|
||||
/// </summary>
|
||||
public void RegisterKey(int trackIndex, string noteInstanceId)
|
||||
public void RegisterKey(int trackIndex, string noteInstanceId, string noteType = "tap")
|
||||
{
|
||||
if (!trackKeyMappings.ContainsKey(trackIndex))
|
||||
trackKeyMappings[trackIndex] = new Queue<string>();
|
||||
trackKeyMappings[trackIndex] = new Queue<(string, string)>();
|
||||
|
||||
trackKeyMappings[trackIndex].Enqueue(noteInstanceId);
|
||||
trackKeyMappings[trackIndex].Enqueue((noteInstanceId, noteType));
|
||||
|
||||
// record/refresh TTL so the id can't block forever if exit/unregister is missed
|
||||
if (!string.IsNullOrEmpty(noteInstanceId))
|
||||
{
|
||||
noteIdExpiry[noteInstanceId] = Time.unscaledTime + Mathf.Max(0.25f, queuedIdTtlSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -69,24 +249,34 @@ public class TrackKeyManager : MonoBehaviour
|
||||
public void UnregisterKey(int trackIndex, string noteInstanceId)
|
||||
{
|
||||
if (!trackKeyMappings.ContainsKey(trackIndex) || trackKeyMappings[trackIndex].Count == 0)
|
||||
{
|
||||
// still clear TTL record
|
||||
if (!string.IsNullOrEmpty(noteInstanceId)) noteIdExpiry.Remove(noteInstanceId);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the head matches, dequeue. Otherwise try to remove from queue by rebuilding it.
|
||||
if (trackKeyMappings[trackIndex].Peek() == noteInstanceId)
|
||||
if (trackKeyMappings[trackIndex].Count > 0 && trackKeyMappings[trackIndex].Peek().noteId == noteInstanceId)
|
||||
{
|
||||
trackKeyMappings[trackIndex].Dequeue();
|
||||
if (!string.IsNullOrEmpty(noteInstanceId)) noteIdExpiry.Remove(noteInstanceId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise remove matching id if present (preserve order of others)
|
||||
var q = trackKeyMappings[trackIndex];
|
||||
var temp = new Queue<string>();
|
||||
var temp = new Queue<(string, string)>();
|
||||
while (q.Count > 0)
|
||||
{
|
||||
var v = q.Dequeue();
|
||||
if (v != noteInstanceId) temp.Enqueue(v);
|
||||
var item = q.Dequeue();
|
||||
if (item.noteId != noteInstanceId)
|
||||
{
|
||||
temp.Enqueue(item);
|
||||
}
|
||||
}
|
||||
trackKeyMappings[trackIndex] = temp;
|
||||
|
||||
if (!string.IsNullOrEmpty(noteInstanceId)) noteIdExpiry.Remove(noteInstanceId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -94,13 +284,29 @@ public class TrackKeyManager : MonoBehaviour
|
||||
/// </summary>
|
||||
public string GetCurrentNoteId(int trackIndex)
|
||||
{
|
||||
PruneStaleHeads(trackIndex);
|
||||
|
||||
if (trackKeyMappings.ContainsKey(trackIndex) && trackKeyMappings[trackIndex].Count > 0)
|
||||
{
|
||||
return trackKeyMappings[trackIndex].Peek();
|
||||
return trackKeyMappings[trackIndex].Peek().noteId;
|
||||
}
|
||||
return null; // 无音符
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前队头note的类型("tap" 或 "hold")
|
||||
/// </summary>
|
||||
public string GetCurrentNoteType(int trackIndex)
|
||||
{
|
||||
PruneStaleHeads(trackIndex);
|
||||
|
||||
if (trackKeyMappings.ContainsKey(trackIndex) && trackKeyMappings[trackIndex].Count > 0)
|
||||
{
|
||||
return trackKeyMappings[trackIndex].Peek().noteType;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to register a note as being judged on a track. Returns true if successful (no other note is currently being judged).
|
||||
/// This prevents multiple notes on the same track from being judged by a single key press.
|
||||
@@ -108,7 +314,10 @@ public class TrackKeyManager : MonoBehaviour
|
||||
public bool TryLockTrackForJudge(int trackIndex, string noteId)
|
||||
{
|
||||
if (trackIndex < 0) return false;
|
||||
|
||||
|
||||
// Before locking, prune any expired heads so they can't block judgement.
|
||||
PruneStaleHeads(trackIndex);
|
||||
|
||||
if (!trackNotesBeingJudged.ContainsKey(trackIndex))
|
||||
{
|
||||
trackNotesBeingJudged[trackIndex] = new HashSet<string>();
|
||||
@@ -131,11 +340,14 @@ public class TrackKeyManager : MonoBehaviour
|
||||
public void UnlockTrackForJudge(int trackIndex, string noteId)
|
||||
{
|
||||
if (trackIndex < 0) return;
|
||||
|
||||
|
||||
if (trackNotesBeingJudged.ContainsKey(trackIndex))
|
||||
{
|
||||
trackNotesBeingJudged[trackIndex].Remove(noteId);
|
||||
}
|
||||
|
||||
// also clear any expiry record for this note id (it has finished its lifecycle)
|
||||
if (!string.IsNullOrEmpty(noteId)) noteIdExpiry.Remove(noteId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -146,14 +358,14 @@ public class TrackKeyManager : MonoBehaviour
|
||||
{
|
||||
if (trackIndex < 0) return false;
|
||||
int currentFrame = Time.frameCount;
|
||||
|
||||
|
||||
// If the recorded frame is old or invalid, allow consumption
|
||||
if (!trackConsumedFrame.ContainsKey(trackIndex))
|
||||
{
|
||||
trackConsumedFrame[trackIndex] = currentFrame;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// If we're in a different frame, reset and allow
|
||||
int recordedFrame = trackConsumedFrame[trackIndex];
|
||||
if (recordedFrame != currentFrame)
|
||||
@@ -161,11 +373,11 @@ public class TrackKeyManager : MonoBehaviour
|
||||
trackConsumedFrame[trackIndex] = currentFrame;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Same frame, already consumed
|
||||
|
||||
// Same frame, already consumed - reject
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Force reset the consumption state (useful for testing or specific scenarios)
|
||||
/// </summary>
|
||||
@@ -173,4 +385,16 @@ public class TrackKeyManager : MonoBehaviour
|
||||
{
|
||||
trackConsumedFrame.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Force clear all track locks and per-frame consumption state.
|
||||
/// Used to simulate a global input release (e.g. when ending playback).
|
||||
/// </summary>
|
||||
public void ClearAllLocks()
|
||||
{
|
||||
trackNotesBeingJudged.Clear();
|
||||
trackConsumedFrame.Clear();
|
||||
noteIdExpiry.Clear();
|
||||
if (GameConfig.verboseLogs) Debug.Log("[TrackKeyManager] ClearAllLocks called: cleared locks and consumption state");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +1,58 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine.Serialization;
|
||||
using UnityEngine.SceneManagement;
|
||||
using System.Collections;
|
||||
using UnityEngine.Audio;
|
||||
using UnityEditor;
|
||||
|
||||
public class settlementController : MonoBehaviour
|
||||
{
|
||||
[Header("结算界面之爹")]
|
||||
[Header("�������֮��")]
|
||||
[SerializeField] private BeatmapManager bmm;
|
||||
[SerializeField] private ScoreManager sm;
|
||||
public GameManager gm;
|
||||
private SongData thisSong_so;
|
||||
private int maxScore_sum = 2000000;
|
||||
[SerializeField] private Image thisSong_backPic;
|
||||
[Header("跳转之按钮")]
|
||||
[Header("��ת֮��ť")]
|
||||
public Button exit_toSelectSongs;
|
||||
public Button replay_thisGame;
|
||||
public Button display_rankList;
|
||||
public Button share_toSocialMedia;
|
||||
|
||||
[Header("文本和进度条")]
|
||||
[Header("�ı��ͽ�����")]
|
||||
public Text songName_Text;
|
||||
[Tooltip("当前关卡的进度百分比")]
|
||||
[Tooltip("��ǰ�ؿ��Ľ��Ȱٷֱ�")]
|
||||
public Text thisLevel_currentPercentage_Text;
|
||||
public Text finalScore_Text;
|
||||
public Text pmScoreSum_Text;
|
||||
public Text idolScoreSum_Text;
|
||||
public Text accuracy_Text;
|
||||
|
||||
public Image thisLevel_progressBar_Image;
|
||||
|
||||
[Header("奖励内容信息")]
|
||||
[Header("ȷ���㷨")]
|
||||
public float perfect_weight = 1f;
|
||||
public float great_weight = 0.6666667f;
|
||||
public float good_weight = 0.333333f;
|
||||
public float miss_weight = 0;
|
||||
|
||||
[Header("����������Ϣ")]
|
||||
public Text reward_playerEXP_Text;
|
||||
public Text reward_money_Text;
|
||||
public Text reward_idolEXP_bottle_Text;
|
||||
|
||||
[Header("给多少钱")]
|
||||
[Header("�Ŷӽ���")]
|
||||
public loadSettlementTeamPrefab settlementTeamLoader;
|
||||
public Image mvp_hero_hd_image;
|
||||
|
||||
[Header("������Ǯ")]
|
||||
[SerializeField] private long moneyToGive_thisLevel;
|
||||
|
||||
// 得到音符统计
|
||||
[Header("击打情况统计")]
|
||||
// �õ�����ͳ��
|
||||
[Header("�������ͳ��")]
|
||||
public Text perfectHitCount_Text;
|
||||
public Text perfectHitPercent_Text;
|
||||
public Image perfect_barFill_Image;
|
||||
@@ -53,20 +69,135 @@ public class settlementController : MonoBehaviour
|
||||
public Text missHitPercent_Text;
|
||||
public Image miss_barFill_Image;
|
||||
|
||||
public Text gameNote_rate;
|
||||
|
||||
[Header("Timing Statistics")]
|
||||
public Text earlyHitCount_Text;
|
||||
public Text lateHitCount_Text;
|
||||
public Text avgOffset_Text;
|
||||
|
||||
[Header("��������ϵͳ")]
|
||||
[Tooltip("�������ֲ��������½���")]
|
||||
public AudioSource settlementAudioSource;
|
||||
|
||||
[Tooltip("��Ϸ���ֲ�������ԭ��Ϸ���֣�")]
|
||||
public AudioSource oriGameMusicSource;
|
||||
|
||||
[Tooltip("��Ƶ�����������ڵ�ͨ�˲���������")]
|
||||
public AudioMixer gameMusicMixer;
|
||||
|
||||
[Tooltip("��Ƶ�������е�ͨ�˲�����������")]
|
||||
public string lowpassParamName = "inGameMusic_lowpass";
|
||||
|
||||
[Tooltip("��ͨ�˲�����������ʱ�䣨�룩")]
|
||||
public float lowpassFadeDuration = 2f;
|
||||
|
||||
[Tooltip("������� CanvasGroup�����ڵ���Ч����")]
|
||||
public CanvasGroup settlementCanvasGroup;
|
||||
|
||||
[Header("����������")]
|
||||
[Tooltip("������")]
|
||||
public GameObject cdm;
|
||||
|
||||
private Coroutine musicTransitionCoroutine;
|
||||
private Coroutine canvasFadeCoroutine;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
// Setup button listeners
|
||||
if (replay_thisGame != null)
|
||||
{
|
||||
replay_thisGame.onClick.AddListener(OnReplayButtonClicked);
|
||||
}
|
||||
|
||||
if (exit_toSelectSongs != null)
|
||||
{
|
||||
exit_toSelectSongs.onClick.AddListener(OnExitButtonClicked);
|
||||
}
|
||||
|
||||
// ���� CDM �����ڿ�ʼʱ���ֽ���״̬��
|
||||
if (cdm != null)
|
||||
{
|
||||
cdm.SetActive(false);
|
||||
if (GameConfig.verboseLogs) Debug.Log("[SettlementController] CDM object disabled at startup");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("[SettlementController] CDM object is not assigned");
|
||||
}
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// 确保 LowPass 在开始时处于原始位置(通常是 22000Hz,即不滤波)
|
||||
if (gameMusicMixer != null && !string.IsNullOrEmpty(lowpassParamName))
|
||||
{
|
||||
try { gameMusicMixer.SetFloat(lowpassParamName, 22000f); }
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (replay_thisGame != null)
|
||||
{
|
||||
replay_thisGame.onClick.RemoveListener(OnReplayButtonClicked);
|
||||
}
|
||||
|
||||
if (exit_toSelectSongs != null)
|
||||
{
|
||||
exit_toSelectSongs.onClick.RemoveListener(OnExitButtonClicked);
|
||||
}
|
||||
|
||||
// ֹͣ���ֹ���Э��
|
||||
if (musicTransitionCoroutine != null)
|
||||
{
|
||||
StopCoroutine(musicTransitionCoroutine);
|
||||
musicTransitionCoroutine = null;
|
||||
}
|
||||
|
||||
// ֹͣ CanvasGroup ����Э��
|
||||
if (canvasFadeCoroutine != null)
|
||||
{
|
||||
StopCoroutine(canvasFadeCoroutine);
|
||||
canvasFadeCoroutine = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Start is called once before the first execution of Update after the MonoBehaviour is created
|
||||
void getThisSong_info()
|
||||
public void getThisSong_info()
|
||||
{
|
||||
thisSong_so = bmm.assignedSongData;
|
||||
if (bmm != null)
|
||||
{
|
||||
thisSong_so = bmm.assignedSongData;
|
||||
}
|
||||
}
|
||||
|
||||
public void startSettlement_uiUpdate()
|
||||
{
|
||||
// --- �������ڽ������̿�ʼʱ���� CanvasGroup Ϊ�� ---
|
||||
InitializeSettlementCanvas();
|
||||
|
||||
// --- ���������ý��������� ---
|
||||
if (cdm != null)
|
||||
{
|
||||
cdm.SetActive(true);
|
||||
Debug.Log("[SettlementController] CDM object enabled at settlement start");
|
||||
}
|
||||
|
||||
getThisSong_info();
|
||||
if(!sm)
|
||||
|
||||
// --- Statistics: Record total play time at settlement ---
|
||||
if (gm != null) gm.RecordTotalPlayTime();
|
||||
else { var activeGM = FindObjectOfType<GameManager>(); if (activeGM != null) activeGM.RecordTotalPlayTime(); }
|
||||
|
||||
// ���������֣���UI����֮ǰ��
|
||||
PrepareSettlementMusic();
|
||||
|
||||
if(sm != null)
|
||||
{
|
||||
songName_Text.text = bmm.parsedTitle;
|
||||
thisSong_backPic.sprite = bmm.assignedSongData.illustration;
|
||||
thisSong_backPic.sprite = bmm.assignedSongData.fullscreen_songPicture;
|
||||
|
||||
finalScore_Text.text = (sm.allSum_pmScore + sm.allSum_idolScore).ToString();
|
||||
pmScoreSum_Text.text = sm.allSum_pmScore.ToString();
|
||||
@@ -86,11 +217,584 @@ public class settlementController : MonoBehaviour
|
||||
good_barFill_Image.fillAmount = (float)sm.countGood / noteCountSum;
|
||||
miss_barFill_Image.fillAmount = (float)sm.countMiss / noteCountSum;
|
||||
|
||||
gameNote_rate.text = "已完成: " + noteCountSum.ToString() + "/" + bmm.parsedNoteAmount.ToString();
|
||||
|
||||
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") + "%";
|
||||
|
||||
accuracy_Text.text = ((float)(sm.countPerfect * perfect_weight + sm.countGreat * great_weight + sm.countGood * good_weight + sm.countMiss * miss_weight) / noteCountSum * 100).ToString("F3") + "%";
|
||||
|
||||
// Timing Statistics
|
||||
if (earlyHitCount_Text != null) earlyHitCount_Text.text = sm.countEarly.ToString();
|
||||
if (lateHitCount_Text != null) lateHitCount_Text.text = sm.countLate.ToString();
|
||||
if (avgOffset_Text != null)
|
||||
{
|
||||
float avg = sm.offsetCount > 0 ? sm.totalOffsetMs / sm.offsetCount : 0f;
|
||||
avgOffset_Text.text = avg.ToString("F2") + " ms";
|
||||
}
|
||||
}
|
||||
else Debug.LogError("score manager is null");
|
||||
|
||||
// --- Achievement System: Load and display achievements ---
|
||||
if (InGamePerformanceManager.Instance != null)
|
||||
{
|
||||
// Calculate total cure, damage and mana from teamUIController
|
||||
float totalCure = 0;
|
||||
float totalDamage = 0;
|
||||
float totalMana = 0;
|
||||
if (teamUIController.Instance != null)
|
||||
{
|
||||
if (teamUIController.Instance.totalHealProvided != null)
|
||||
{
|
||||
foreach (float heal in teamUIController.Instance.totalHealProvided)
|
||||
{
|
||||
totalCure += heal;
|
||||
}
|
||||
}
|
||||
|
||||
if (teamUIController.Instance.totalDamageDealt != null)
|
||||
{
|
||||
foreach (float dmg in teamUIController.Instance.totalDamageDealt)
|
||||
{
|
||||
totalDamage += dmg;
|
||||
}
|
||||
}
|
||||
|
||||
if (teamUIController.Instance.totalManaRestored != null)
|
||||
{
|
||||
foreach (float mana in teamUIController.Instance.totalManaRestored)
|
||||
{
|
||||
totalMana += mana;
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Log($"[SettlementController] Stats Aggregated - Cure: {totalCure}, Damage: {totalDamage}, Mana: {totalMana}");
|
||||
}
|
||||
|
||||
// Update achievement categories
|
||||
InGamePerformanceManager.Instance.UpdateTotalCure(totalCure);
|
||||
InGamePerformanceManager.Instance.UpdateTotalDamage(totalDamage);
|
||||
InGamePerformanceManager.Instance.UpdateTotalManaRestored(totalMana);
|
||||
|
||||
// Load and display achievement prefabs
|
||||
InGamePerformanceManager.Instance.LoadArchievePrefab();
|
||||
}
|
||||
|
||||
// Persist run results into the SongData SO for this difficulty
|
||||
// (update personal / idol / total records and per-entry progress if improved)
|
||||
getThisSong_info(); // ensure thisSong_so set
|
||||
if (thisSong_so != null && bmm != null)
|
||||
{
|
||||
int diff = bmm.assignedDifficulty;
|
||||
if (diff >= 0)
|
||||
{
|
||||
int pm = sm != null ? sm.allSum_pmScore : 0;
|
||||
int idol = sm != null ? sm.allSum_idolScore : 0;
|
||||
int total = pm + idol;
|
||||
|
||||
// compare and update per-difficulty personal/idol records
|
||||
int prevPm = thisSong_so.GetPersonalRecord(diff);
|
||||
int prevIdol = thisSong_so.GetIdolRecord(diff);
|
||||
|
||||
if (pm > prevPm)
|
||||
{
|
||||
thisSong_so.UpdatePersonalRecord(diff, pm);
|
||||
}
|
||||
if (idol > prevIdol)
|
||||
{
|
||||
thisSong_so.UpdateIdolRecord(diff, idol);
|
||||
}
|
||||
|
||||
// update last-run total (chart score map / entry.lastScoreForThisDifficulty)
|
||||
thisSong_so.UpdateChartScore(diff, total);
|
||||
|
||||
// update entry convenience fields (total and progress) only if improved
|
||||
if (thisSong_so.chartFiles != null)
|
||||
{
|
||||
var entry = thisSong_so.chartFiles.Find(e => e != null && e.difficulty == diff);
|
||||
if (entry != null)
|
||||
{
|
||||
entry.totalPersonalRecordForThisDifficulty = thisSong_so.GetPersonalRecord(diff) + thisSong_so.GetIdolRecord(diff);
|
||||
// compute progress as 0..1
|
||||
float newProgress = Mathf.Clamp01((float)entry.totalPersonalRecordForThisDifficulty / (float)maxScore_sum);
|
||||
if (newProgress > entry.levelProgressForThisDifficulty)
|
||||
{
|
||||
entry.levelProgressForThisDifficulty = newProgress;
|
||||
}
|
||||
|
||||
// if this is the currently selected difficulty for this song, update SongData.current_levelProgress
|
||||
if (thisSong_so.thisLevel_selectedDifficultyID == diff)
|
||||
{
|
||||
thisSong_so.current_levelProgress = Mathf.Max(thisSong_so.current_levelProgress, entry.levelProgressForThisDifficulty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// refresh overall personalRecord cached value
|
||||
thisSong_so.CalculateHighestPersonalRecord();
|
||||
}
|
||||
}
|
||||
|
||||
// Populate settlement team cards if a loader is assigned
|
||||
if (settlementTeamLoader == null)
|
||||
{
|
||||
// try to auto-locate loader in scene if not assigned in inspector
|
||||
settlementTeamLoader = FindObjectOfType<loadSettlementTeamPrefab>();
|
||||
}
|
||||
|
||||
if (settlementTeamLoader != null)
|
||||
{
|
||||
// ensure loader has key references
|
||||
if (settlementTeamLoader.sm == null) settlementTeamLoader.sm = sm ?? ScoreManager.Instance;
|
||||
if (settlementTeamLoader.tuic == null) settlementTeamLoader.tuic = teamUIController.Instance;
|
||||
if (settlementTeamLoader.gm == null) settlementTeamLoader.gm = gm ?? FindObjectOfType<GameManager>();
|
||||
try { settlementTeamLoader.PopulateSettlementCards(); }
|
||||
catch (System.Exception ex) { Debug.LogWarning("Failed to PopulateSettlementCards: " + ex); }
|
||||
}
|
||||
|
||||
// ��ʼ���ֹ��ɣ��ڽ���UI��ʾ��
|
||||
// ��ѡ�� MVP ������ͼƬ
|
||||
SetMvpHeroImageFromTopScorer();
|
||||
StartMusicTransition();
|
||||
|
||||
// --- �ģ�������ĩβ��ʼ CanvasGroup ���� ---
|
||||
StartCanvasFadeIn();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ��ʼ��������� CanvasGroup Ϊ��״̬
|
||||
/// </summary>
|
||||
private void InitializeSettlementCanvas()
|
||||
{
|
||||
if (settlementCanvasGroup != null)
|
||||
{
|
||||
settlementCanvasGroup.alpha = 0f;
|
||||
settlementCanvasGroup.interactable = false;
|
||||
settlementCanvasGroup.blocksRaycasts = false;
|
||||
Debug.Log("[SettlementController] Settlement canvas initialized to transparent state");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("[SettlementController] settlementCanvasGroup is not assigned, cannot initialize");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ������dlcData�з����Ұ�����ǰ������DLC��������������
|
||||
/// </summary>
|
||||
private void PrepareSettlementMusic()
|
||||
{
|
||||
// --- �ģ��Ƴ� CanvasGroup ���ã������ڽ�����ĩβ�ŵ��� ---
|
||||
|
||||
if (thisSong_so == null)
|
||||
{
|
||||
Debug.LogWarning("[SettlementController] thisSong_so is null, cannot prepare settlement music");
|
||||
return;
|
||||
}
|
||||
|
||||
if (settlementAudioSource == null)
|
||||
{
|
||||
Debug.LogWarning("[SettlementController] settlementAudioSource is not assigned, cannot prepare settlement music");
|
||||
return;
|
||||
}
|
||||
|
||||
// �������� dlcData ScriptableObjects
|
||||
dlcData[] allDlcs = Resources.LoadAll<dlcData>("");
|
||||
dlcData foundDlc = null;
|
||||
|
||||
// ��������DLC�����Ұ�����ǰ������DLC
|
||||
foreach (var dlc in allDlcs)
|
||||
{
|
||||
if (dlc == null || dlc.songList == null) continue;
|
||||
|
||||
if (dlc.songList.Contains(thisSong_so))
|
||||
{
|
||||
foundDlc = dlc;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (foundDlc == null)
|
||||
{
|
||||
Debug.LogWarning($"[SettlementController] No DLC found containing song '{thisSong_so.songName}'");
|
||||
return;
|
||||
}
|
||||
|
||||
if (foundDlc.settlementMusic == null)
|
||||
{
|
||||
Debug.LogWarning($"[SettlementController] DLC '{foundDlc.dlcName}' does not have settlement music assigned");
|
||||
return;
|
||||
}
|
||||
|
||||
// ���ý������ֵ�AudioSource
|
||||
settlementAudioSource.clip = foundDlc.settlementMusic;
|
||||
settlementAudioSource.loop = true;
|
||||
settlementAudioSource.playOnAwake = false;
|
||||
|
||||
// ��������ͣ���ȴ�����
|
||||
settlementAudioSource.volume = 1f;
|
||||
settlementAudioSource.mute = true;
|
||||
settlementAudioSource.Stop();
|
||||
|
||||
Debug.Log($"[SettlementController] Prepared settlement music from DLC '{foundDlc.dlcName}': {foundDlc.settlementMusic.name}");
|
||||
|
||||
// --- �ģ��Ƴ� CanvasGroup ������ã������ڽ�����ĩβ�ŵ��� ---
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ��ʼ���ֹ��ɣ�������Ϸ���֣�ͨ����ͨ�˲�������Ȼ�Ž�������
|
||||
/// </summary>
|
||||
private void StartMusicTransition()
|
||||
{
|
||||
if (musicTransitionCoroutine != null)
|
||||
{
|
||||
StopCoroutine(musicTransitionCoroutine);
|
||||
}
|
||||
musicTransitionCoroutine = StartCoroutine(MusicTransitionRoutine());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ���ֹ���Э�̣�
|
||||
/// 1. ����Ϸ���ֵĵ�ͨ�˲�����22000Hz����0Hz��ͬʱ��������0
|
||||
/// 2. ������ֹͣԭ���ֲ���ʼ���Ž�������
|
||||
/// </summary>
|
||||
private IEnumerator MusicTransitionRoutine()
|
||||
{
|
||||
float elapsed = 0f;
|
||||
float startLowpass = 22000f;
|
||||
float endLowpass = 0f;
|
||||
|
||||
// --- ��������¼��ʼ���� ---
|
||||
float startVolume = (oriGameMusicSource != null) ? oriGameMusicSource.volume : 1f;
|
||||
|
||||
// ���ԭ��Ϸ����Դ�ͻ����������ڣ�����е�ͨ�˲�����������������
|
||||
if (oriGameMusicSource != null && gameMusicMixer != null && !string.IsNullOrEmpty(lowpassParamName))
|
||||
{
|
||||
Debug.Log($"[SettlementController] Starting lowpass and volume fade over {lowpassFadeDuration}s");
|
||||
|
||||
while (elapsed < lowpassFadeDuration)
|
||||
{
|
||||
elapsed += Time.unscaledDeltaTime;
|
||||
float t = Mathf.Clamp01(elapsed / Mathf.Max(0.0001f, lowpassFadeDuration));
|
||||
|
||||
// 1. ���õ�ͨ�˲���
|
||||
float currentLowpass = Mathf.Lerp(startLowpass, endLowpass, t);
|
||||
try { gameMusicMixer.SetFloat(lowpassParamName, currentLowpass); }
|
||||
catch (System.Exception ex) { Debug.LogWarning($"Mixer error: {ex.Message}"); }
|
||||
|
||||
// 2. --- �ģ�ƽ���������� ---
|
||||
oriGameMusicSource.volume = Mathf.Lerp(startVolume, 0f, t);
|
||||
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// ȷ������״̬
|
||||
try { gameMusicMixer.SetFloat(lowpassParamName, endLowpass); } catch { }
|
||||
|
||||
// --- �ģ��������㲢��ͣ/ֹͣ ---
|
||||
oriGameMusicSource.volume = 0f;
|
||||
oriGameMusicSource.Pause(); // ����ʹ�� .Stop()
|
||||
|
||||
Debug.Log("[SettlementController] Lowpass and volume fade complete, game music paused.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("[SettlementController] oriGameMusicSource or gameMusicMixer not assigned, skipping fade");
|
||||
}
|
||||
|
||||
// ������ɺ�ʼ���Ž�������
|
||||
if (settlementAudioSource != null && settlementAudioSource.clip != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
settlementAudioSource.mute = false;
|
||||
settlementAudioSource.Play();
|
||||
Debug.Log($"[SettlementController] Settlement music started: {settlementAudioSource.clip.name}");
|
||||
|
||||
// --- 一旦结算音乐开始播放,立刻将 LowPass 设置回原位确保音频正常 ---
|
||||
if (gameMusicMixer != null && !string.IsNullOrEmpty(lowpassParamName))
|
||||
{
|
||||
gameMusicMixer.SetFloat(lowpassParamName, 22000f);
|
||||
Debug.Log("[SettlementController] Reset lowpass to 22000Hz for normal audio quality.");
|
||||
}
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"[SettlementController] Failed to start settlement music: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
musicTransitionCoroutine = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ��ʼ CanvasGroup ����Ч��
|
||||
/// </summary>
|
||||
private void StartCanvasFadeIn()
|
||||
{
|
||||
if (canvasFadeCoroutine != null)
|
||||
{
|
||||
StopCoroutine(canvasFadeCoroutine);
|
||||
}
|
||||
canvasFadeCoroutine = StartCoroutine(CanvasFadeInRoutine());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CanvasGroup ����Э�̣�0.25���ڴ� alpha=0 ���뵽 alpha=1
|
||||
/// </summary>
|
||||
private IEnumerator CanvasFadeInRoutine()
|
||||
{
|
||||
if (settlementCanvasGroup == null)
|
||||
{
|
||||
Debug.LogWarning("[SettlementController] settlementCanvasGroup is not assigned, cannot fade in");
|
||||
yield break;
|
||||
}
|
||||
|
||||
float fadeDuration = 0.25f;
|
||||
float elapsed = 0f;
|
||||
float startAlpha = settlementCanvasGroup.alpha;
|
||||
float targetAlpha = 1f;
|
||||
|
||||
Debug.Log($"[SettlementController] Starting CanvasGroup fade-in from {startAlpha} to {targetAlpha} over {fadeDuration}s");
|
||||
|
||||
while (elapsed < fadeDuration)
|
||||
{
|
||||
elapsed += Time.unscaledDeltaTime;
|
||||
float t = Mathf.Clamp01(elapsed / fadeDuration);
|
||||
settlementCanvasGroup.alpha = Mathf.Lerp(startAlpha, targetAlpha, t);
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// ȷ������״̬
|
||||
settlementCanvasGroup.alpha = targetAlpha;
|
||||
settlementCanvasGroup.interactable = true;
|
||||
settlementCanvasGroup.blocksRaycasts = true;
|
||||
|
||||
Debug.Log("[SettlementController] CanvasGroup fade-in complete");
|
||||
|
||||
canvasFadeCoroutine = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ����������ǰ��Ϸ - ���¼������桢���ֺͳ�ʼ��������Ϸ״̬
|
||||
/// </summary>
|
||||
private void OnReplayButtonClicked()
|
||||
{
|
||||
Debug.Log("[SettlementController] Replay button clicked - restarting current game");
|
||||
|
||||
// ��֤��Ҫ������
|
||||
if (bmm == null)
|
||||
{
|
||||
Debug.LogError("[SettlementController] BeatmapManager is null, cannot replay");
|
||||
return;
|
||||
}
|
||||
|
||||
if (bmm.assignedSongData == null)
|
||||
{
|
||||
Debug.LogError("[SettlementController] AssignedSongData is null, cannot replay");
|
||||
return;
|
||||
}
|
||||
|
||||
// ���浱ǰ�ؿ���Ϣ���������¼��أ�
|
||||
SongData songToReplay = bmm.assignedSongData;
|
||||
int difficultyToReplay = bmm.assignedDifficulty;
|
||||
|
||||
if (difficultyToReplay < 0)
|
||||
{
|
||||
Debug.LogError("[SettlementController] AssignedDifficulty is invalid, cannot replay");
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Log($"[SettlementController] Reloading song: {songToReplay.songName}, difficulty: {difficultyToReplay}");
|
||||
|
||||
// ֹͣ��������
|
||||
if (settlementAudioSource != null && settlementAudioSource.isPlaying)
|
||||
{
|
||||
settlementAudioSource.Stop();
|
||||
}
|
||||
|
||||
// ���� BeatmapManager Ϊ��һ������������
|
||||
BeatmapManager.SetPendingSong(songToReplay, difficultyToReplay);
|
||||
|
||||
// ���¼��ص�ǰ������GamePlay_gamePlay��
|
||||
SceneManager.LoadScene(SceneManager.GetActiveScene().name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ����ѡ�����
|
||||
/// </summary>
|
||||
private void OnExitButtonClicked()
|
||||
{
|
||||
Debug.Log("[SettlementController] Exit button clicked - returning to song selection");
|
||||
|
||||
// ֹͣ��������
|
||||
if (settlementAudioSource != null && settlementAudioSource.isPlaying)
|
||||
{
|
||||
settlementAudioSource.Stop();
|
||||
}
|
||||
|
||||
// ����κδ�����������
|
||||
BeatmapManager.pendingSongData = null;
|
||||
BeatmapManager.pendingDifficulty = -1;
|
||||
|
||||
// Ensure time scale restored
|
||||
try { Time.timeScale = 1f; } catch {}
|
||||
|
||||
// If GameManager and its blackMaskImage available, start coroutine on gm to fade to black then load
|
||||
if (gm != null && gm.blackMaskImage != null)
|
||||
{
|
||||
gm.StartCoroutine(FadeToBlackAndLoadOnGM("selectYourSongFirst", 0.25f));
|
||||
}
|
||||
else
|
||||
{
|
||||
SceneManager.LoadScene("selectYourSongFirst");
|
||||
}
|
||||
}
|
||||
|
||||
// Coroutine that will be started on the GameManager instance so that the gm's MonoBehaviour runs it
|
||||
private IEnumerator FadeToBlackAndLoadOnGM(string sceneName, float duration)
|
||||
{
|
||||
if (gm == null || gm.blackMaskImage == null)
|
||||
{
|
||||
SceneManager.LoadScene(sceneName);
|
||||
yield break;
|
||||
}
|
||||
|
||||
var blackMask = gm.blackMaskImage;
|
||||
|
||||
// ensure image active
|
||||
if (!blackMask.gameObject.activeSelf) blackMask.gameObject.SetActive(true);
|
||||
float startA = blackMask.color.a;
|
||||
float elapsed = 0f;
|
||||
|
||||
// enable raycast target while fading to block input
|
||||
try { blackMask.raycastTarget = true; } catch {}
|
||||
|
||||
while (elapsed < duration)
|
||||
{
|
||||
elapsed += Time.unscaledDeltaTime;
|
||||
float frac = Mathf.Clamp01(elapsed / Mathf.Max(0.0001f, duration));
|
||||
Color c = blackMask.color;
|
||||
c.a = Mathf.Lerp(startA, 1f, frac);
|
||||
blackMask.color = c;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// ensure fully opaque
|
||||
Color fc = blackMask.color;
|
||||
fc.a = 1f;
|
||||
blackMask.color = fc;
|
||||
|
||||
// load target scene
|
||||
SceneManager.LoadScene(sceneName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ���ݸ���ɫ�������ѡ����߷ֵĽ�ɫ�������� AllyHero_SO �е� ally_hero_HD_image ��ֵ�� mvp_hero_hd_image
|
||||
/// ����ʹ�� ScoreManager �� per-track pm sums��������������˵����� AllyCombatant.currentScore
|
||||
/// </summary>
|
||||
private void SetMvpHeroImageFromTopScorer()
|
||||
{
|
||||
if (mvp_hero_hd_image == null)
|
||||
{
|
||||
Debug.LogWarning("[SettlementController] mvp_hero_hd_image is not assigned");
|
||||
return;
|
||||
}
|
||||
|
||||
int topIndex = -1;
|
||||
int[] scores = new int[5];
|
||||
|
||||
if (sm != null)
|
||||
{
|
||||
scores[0] = sm.red_pmScore_sum;
|
||||
scores[1] = sm.green_pmScore_sum;
|
||||
scores[2] = sm.yellow_pmScore_sum;
|
||||
scores[3] = sm.purple_pmScore_sum;
|
||||
scores[4] = sm.blue_pmScore_sum;
|
||||
}
|
||||
else
|
||||
{
|
||||
// fallback: read AllyCombatant.currentScore from scene
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
var go = GameObject.Find($"ally_0{ i+1 }");
|
||||
if (go != null)
|
||||
{
|
||||
var ac = go.GetComponent<AllyCombatant>();
|
||||
scores[i] = ac != null ? ac.currentScore : 0;
|
||||
}
|
||||
else scores[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
int max = -1;
|
||||
for (int i = 0; i < scores.Length; i++)
|
||||
{
|
||||
if (scores[i] > max)
|
||||
{
|
||||
max = scores[i];
|
||||
topIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (topIndex < 0 || max <= 0)
|
||||
{
|
||||
Debug.Log("[SettlementController] No top scorer found or scores are all zero");
|
||||
// hide image
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to resolve AllyHero_SO for the winning slot
|
||||
AllyHero_SO heroSO = null;
|
||||
// try SkillBuilder cache
|
||||
if (SkillBuilder.Instance != null)
|
||||
{
|
||||
try { heroSO = SkillBuilder.Instance.GetAllyHeroSOBySlot(topIndex); }
|
||||
catch { heroSO = null; }
|
||||
}
|
||||
|
||||
// fallback: use teamUIController slot ids to find hero id -> load SO
|
||||
if (heroSO == null && teamUIController.Instance != null && teamUIController.Instance.allySlotIds != null)
|
||||
{
|
||||
int allyId = -1;
|
||||
if (topIndex >= 0 && topIndex < teamUIController.Instance.allySlotIds.Count)
|
||||
allyId = teamUIController.Instance.allySlotIds[topIndex];
|
||||
if (allyId > 0)
|
||||
{
|
||||
var arr = Resources.LoadAll<AllyHero_SO>("");
|
||||
foreach (var a in arr)
|
||||
{
|
||||
if (a != null && a.ally_heroID == allyId) { heroSO = a; break; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// final fallback: try direct Resources lookup by scanning all and picking first non-null for slot
|
||||
if (heroSO == null)
|
||||
{
|
||||
var arr = Resources.LoadAll<AllyHero_SO>("");
|
||||
if (arr != null && arr.Length > 0)
|
||||
{
|
||||
// attempt to match by name or just pick first
|
||||
heroSO = arr[0] as AllyHero_SO;
|
||||
}
|
||||
}
|
||||
|
||||
if (heroSO != null && heroSO.ally_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);
|
||||
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);
|
||||
Debug.LogWarning("[SettlementController] MVP SO or HD image not found");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user