using System.IO; using UnityEngine; using UnityEngine.UI; using System.Collections.Generic; using TMPro; public class BeatmapManager : MonoBehaviour { private static readonly bool VerboseLogs = false; public static BeatmapManager Instance { get; private set; } private void Awake() { if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; } // static holder to accept SongData passed from previous scene before this manager exists public static SongData pendingSongData = null; public static int pendingDifficulty = -1; /// /// Called by previous scene to hand over a SongData SO and difficulty to this manager after scene load. /// This stores the pair in static fields so BeatmapManager.Start can pick them up. /// public static void SetPendingSong(SongData song, int difficulty) { pendingSongData = song; pendingDifficulty = difficulty; if (VerboseLogs) Debug.Log($"BeatmapManager.SetPendingSong called: song={(song==null?"NULL":song.songName)}, difficulty={difficulty}"); } [Header("Assigned SongData (set at runtime from previous scene or via Inspector)")] public SongData assignedSongData; // visible in Inspector so you can see the SO passed from previous scene [Header("Assigned difficulty (for inspector visibility)")] public int assignedDifficulty = -1; /// /// Public API to accept a SongData at runtime (can be called by other scripts after scene load) /// Also sets inspector-exposed fields so you can visually confirm in Editor during play. /// public void AcceptSongData(SongData song, int difficulty) { assignedSongData = song; assignedDifficulty = difficulty; if (VerboseLogs) Debug.Log($"BeatmapManager.AcceptSongData: accepted song={(song==null?"NULL":song.songName)}, difficulty={difficulty}"); UpdateGameplayUI(); } public Beatmap beatmap; // Documentation text normalized. // Documentation text normalized. public NoteSpawner noteSpawner; // Documentation text normalized. public teamUIController uiController; [Header("Enemy Ball UI")] public load_enemyBall_light enemyBallLoader; [Header("UI Display")] public Text gameplay_songname; public TextMeshProUGUI gameplay_difficultyID; public TextMeshProUGUI gameplay_difficultyName; public Text modeText; public Image bgMainImage; // 场景主背景图 (UI Image) public Image startCanvas_image; public SpriteRenderer bgSpriteRenderer; // 场景 3D 背景图 (SpriteRenderer) // Extra fields from beatmap JSON (stored temporarily) [HideInInspector] public string parsedTitle; [HideInInspector] public string parsedComposer; [HideInInspector] public string parsedIllustrator; [HideInInspector] public string parsedCharter; [HideInInspector] public string parsedBeatmapId; [HideInInspector] public string parsedDifficultyName; [HideInInspector] public string parsedCreatedDate; [HideInInspector] public string parsedLastSavedTime; [HideInInspector] public string parsedMusicFile; [HideInInspector] public string parsedBackgroundFile; [HideInInspector] public int parsedDuration; // as int per request [HideInInspector] public int parsedBpm; // as int per request [HideInInspector] public int parsedDifficulty; // duplicate of beatmap.difficulty [HideInInspector] public int parsedNoteAmount; [HideInInspector] public float globalDelaySeconds = 0f; // New parsed fields [HideInInspector] public bool parsedEnemyListIsEmpty; [HideInInspector] public ColorSegment[] parsedColorSegments; [HideInInspector] public NoteStatistic[] parsedNoteStatistics; [HideInInspector] public TrackStates parsedTrackStates; // Multipliers for difficulty levels [Header("Difficulty Multipliers")] public float ezMultiplier = 1f; public float hdMultiplier = 1.5f; public float inMultiplier = 2f; public float imMultiplier = 3f; [Header("HP Calculation Settings")] [Tooltip("Documentation text normalized.")] public float baseHpUnitScale = 1f; // Total chart score and per-note score fields public int totalChartScore = 1000000; // Default total score for a chart public int perNoteScore; public int leftoverScore; // Documentation text normalized. public void LoadBeatmap(string fileName) { string path = Application.streamingAssetsPath + "/" + fileName; if (File.Exists(path)) { string json = File.ReadAllText(path); if (VerboseLogs) Debug.Log("读取 JSON 路径: " + path); ProcessJsonAndLoad(json); } else { Debug.LogError("文件未找到: " + path); } } // Documentation text normalized. public void LoadBeatmapFromJsonString(string json) { if (string.IsNullOrEmpty(json)) { Debug.LogError("LoadBeatmapFromJsonString: json is null or empty"); return; } ProcessJsonAndLoad(json); } // Documentation text normalized. public bool ParseJsonOnly(string json) { if (string.IsNullOrEmpty(json)) { Debug.LogError("ParseJsonOnly: json is empty"); return false; } // Parse main beatmap Beatmap parsed = null; try { parsed = JsonUtility.FromJson(json); } catch (System.Exception ex) { Debug.LogError($"ParseJsonOnly 解析 Beatmap 失败: {ex}"); return false; } if (parsed == null) { Debug.LogError("ParseJsonOnly: 解析 Beatmap 结果为 null"); return false; } // Parse extras BeatmapExtra extra = null; try { extra = JsonUtility.FromJson(json); } catch { extra = null; } if (extra != null) { parsedTitle = extra.title; parsedComposer = extra.composer; parsedIllustrator = extra.illustrator; parsedCharter = extra.charter; parsedBeatmapId = extra.beatmapId; parsedDifficultyName = extra.difficultyName; parsedCreatedDate = extra.createdDate; parsedLastSavedTime = extra.lastSavedTime; parsedMusicFile = extra.musicFile; parsedBackgroundFile = extra.backgroundFile; parsedDuration = extra.duration; parsedBpm = extra.bpm; parsedDifficulty = extra.difficulty; parsedNoteAmount = extra.noteAmount; globalDelaySeconds = extra.globalDelaySeconds; if (!string.IsNullOrEmpty(extra.musicFile)) parsed.musicFile = extra.musicFile; if (!string.IsNullOrEmpty(extra.backgroundFile)) parsed.backgroundFile = extra.backgroundFile; // New fields parsedEnemyListIsEmpty = extra.enemyList_isEmpty; if (!parsedEnemyListIsEmpty) { parsedColorSegments = extra.colorSegments; } parsedNoteStatistics = extra.noteStatistics; parsedTrackStates = extra.trackStates; } else { parsedTitle = parsed.title; parsedComposer = parsed.composer; parsedIllustrator = parsed.illustrator; parsedCharter = parsed.charter; parsedBeatmapId = parsed.beatmapId; parsedDifficultyName = parsed.difficultyName; parsedCreatedDate = parsed.createdDate; parsedMusicFile = parsed.musicFile; parsedBackgroundFile = parsed.backgroundFile; parsedDuration = Mathf.RoundToInt(parsed.duration); parsedBpm = Mathf.RoundToInt(parsed.bpm); parsedDifficulty = parsed.difficulty; } // Calculate per-note and leftover scores CalculateNoteScores(parsed); // NOTE: Do NOT modify parsed.notes here. globalDelaySeconds will be applied to audio playback instead. beatmap = parsed; if (VerboseLogs) Debug.Log("ParseJsonOnly: parsed beatmap " + beatmap.title); // Documentation text normalized. SetupEnemiesAndHP(); ApplyTrackScoreCaps(beatmap); ApplyTrackScoreCaps(beatmap); return true; } // Documentation text normalized. public void LoadBeatmap(Beatmap loadedBeatmap) { beatmap = loadedBeatmap; parsedNoteAmount = (beatmap != null && beatmap.notes != null) ? beatmap.notes.Length : 0; CalculateNoteScores(beatmap); if (VerboseLogs) Debug.Log("谱面加载完成,当前 Beatmap 标题: " + (beatmap != null ? beatmap.title : "null")); // No globalDelaySeconds available in this path. Call NoteSpawner directly. noteSpawner.LoadBeatmap(beatmap); ApplyTrackScoreCaps(beatmap); // Also setup enemies if loading this way SetupEnemiesAndHP(); } // Documentation text normalized. public void LoadBeatmapFromFile() { string path = Application.streamingAssetsPath + "/Emilia_demo.json"; if (File.Exists(path)) { string json = File.ReadAllText(path); ProcessJsonAndLoad(json); } else { Debug.LogError("默认谱面文件未找到"); } } // Documentation text normalized. public bool LoadBeatmapFromTextAsset(TextAsset chartAsset, bool parseOnly = false) { if (chartAsset == null) { Debug.LogWarning("LoadBeatmapFromTextAsset: chartAsset is null"); return false; } if (string.IsNullOrEmpty(chartAsset.text)) { Debug.LogWarning("LoadBeatmapFromTextAsset: chartAsset.text is null or empty"); return false; } if (parseOnly) { return ParseJsonOnly(chartAsset.text); } else { ProcessJsonAndLoad(chartAsset.text); return true; } } // Documentation text normalized. public bool LoadBeatmapFromSongData(SongData song, int difficulty, bool parseOnly = false) { if (song == null) { Debug.LogWarning("LoadBeatmapFromSongData: song is null"); return false; } TextAsset ta = song.GetResolvedChartFile(difficulty); if (ta == null) { Debug.LogWarning($"LoadBeatmapFromSongData: chart TextAsset for difficulty {difficulty} is null on song {song.songName}"); return false; } if (VerboseLogs) Debug.Log($"LoadBeatmapFromSongData: loading chart for song {song.songName}, difficulty {difficulty}, parseOnly={parseOnly}, chartSize={(ta.text != null ? ta.text.Length : 0)}"); return LoadBeatmapFromTextAsset(ta, parseOnly); } // Documentation text normalized. void Start() { // 如果 assignedSongData 为空,尝试从 SongDataHolder 获取,作为最后的兜底 if (pendingSongData == null && assignedSongData == null) { assignedSongData = SongDataHolder.SelectedSongData; if (VerboseLogs) Debug.Log($"[BeatmapManager] No pending song, fallback to SongDataHolder: {(assignedSongData != null ? assignedSongData.songName : "null")}"); } // 无论如何,在加载谱面前先同步敌人编队到 teamUIController SyncEnemyListToUI(); if (pendingSongData != null) { if (VerboseLogs) Debug.Log($"BeatmapManager.Start: pendingSongData detected: {pendingSongData.songName}, difficulty={pendingDifficulty}"); // Documentation text normalized. assignedSongData = pendingSongData; assignedDifficulty = pendingDifficulty; // Documentation text normalized. bool ok = LoadBeatmapFromSongData(assignedSongData, assignedDifficulty, true); if (!ok) Debug.LogWarning("BeatmapManager.Start: LoadBeatmapFromSongData failed"); else { if (VerboseLogs) Debug.Log("BeatmapManager.Start: chart parsed from SongData, attempting to assign audio and pause system"); // try to assign audio to GameManager.musicSource var gm = SceneObjectLookupCache.FindAny(); if (gm != null && gm.musicSource != null) { AudioClip resolvedAudio = assignedSongData != null ? assignedSongData.GetResolvedAudioFile() : null; if (resolvedAudio != null) { gm.musicSource.clip = resolvedAudio; gm.musicSource.loop = false; if (VerboseLogs) Debug.Log("Assigned resolved SongData audio to GameManager.musicSource.clip (from SO)"); } else if (!string.IsNullOrEmpty(parsedMusicFile)) { if (VerboseLogs) Debug.Log($"Attempting Resources.Load for audio: {parsedMusicFile}"); var ac = Resources.Load(parsedMusicFile); if (ac != null) { gm.musicSource.clip = ac; gm.musicSource.loop = false; if (VerboseLogs) Debug.Log("Assigned audio via Resources.Load(parsedMusicFile)"); } else { if (VerboseLogs) Debug.Log($"Resources.Load failed for '{parsedMusicFile}'"); } } else { if (VerboseLogs) Debug.Log("No audio available on SongData and parsedMusicFile empty"); } } else { if (VerboseLogs) Debug.Log("GameManager or its musicSource not found; audio not assigned"); } // Assign fullscreen image from SongData to background images if available Sprite resolvedBackground = assignedSongData != null ? assignedSongData.GetResolvedFullscreenSongPicture() : null; if (resolvedBackground != null) { Sprite bgSprite = resolvedBackground; if (bgMainImage != null) { bgMainImage.sprite = bgSprite; // ensure fully visible bgMainImage.color = new Color(bgMainImage.color.r, bgMainImage.color.g, bgMainImage.color.b, 1f); if (VerboseLogs) Debug.Log($"Assigned resolved fullscreen song picture to bgMainImage for song {assignedSongData.songName}"); } if (bgSpriteRenderer != null) { bgSpriteRenderer.sprite = bgSprite; if (VerboseLogs) Debug.Log($"Assigned resolved fullscreen song picture to bgSpriteRenderer for song {assignedSongData.songName}"); // 释放引用以节省内存(按要求:完毕后令 sprite renderer = null) bgSpriteRenderer = null; } if (startCanvas_image != null) { startCanvas_image.sprite = bgSprite; if (VerboseLogs) Debug.Log($"Assigned resolved fullscreen song picture to startCanvas_image for song {assignedSongData.songName}"); } if (bgMainImage == null && startCanvas_image == null) { if (VerboseLogs) Debug.Log("No background Image targets assigned in BeatmapManager; fullscreen sprite not applied"); } } // Pause the system to show pause overlay (PauseManager will enable overlayRoot) var pauseMgr = PauseManager.Instance ?? SceneObjectLookupCache.FindAny(); if (pauseMgr != null) { pauseMgr.Pause(true); if (VerboseLogs) Debug.Log("System paused after loading chart and audio to allow player to start"); } else { if (VerboseLogs) Debug.Log("PauseManager not found in scene; cannot pause system automatically"); } } // clear pending so it doesn't run again pendingSongData = null; pendingDifficulty = -1; } // Documentation text normalized. if (GameConfig.testMode) { if (VerboseLogs) Debug.Log("Test mode active: skipping automatic beatmap load in BeatmapManager.Start"); } else { // Documentation text normalized. // Documentation text normalized. if (VerboseLogs) Debug.Log("Default automatic demo beatmap load is commented out to prioritize SO-provided charts."); } UpdateGameplayUI(); } private void UpdateGameplayUI() { if (assignedSongData == null) return; if (gameplay_songname != null) { gameplay_songname.text = assignedSongData.songName; } if (gameplay_difficultyID != null) { // Find the ChartFileEntry matching the assignedDifficulty to get difficultyLEVEL (star rating) float diffLevel = 0; if (assignedSongData.chartFiles != null) { foreach (var entry in assignedSongData.chartFiles) { if (entry.difficulty == assignedDifficulty) { diffLevel = entry.difficultyLEVEL; break; } } } gameplay_difficultyID.text = diffLevel.ToString(); } if (gameplay_difficultyName != null) { string diffText = "Unknown"; switch (assignedDifficulty) { case 0: diffText = "Easy"; break; case 1: diffText = "Hard"; break; case 2: diffText = "Incredible"; break; case 3: diffText = "Impossible"; break; } gameplay_difficultyName.text = diffText; } if (modeText != null) { modeText.text = GameConfig.autoPlayEnabled ? "AUTOPLAYING" : string.Empty; } } // Documentation text normalized. private float GetCurrentDifficultyMultiplier() { if (parsedDifficulty == 1) return ezMultiplier; if (parsedDifficulty == 2) return hdMultiplier; if (parsedDifficulty == 3) return inMultiplier; if (parsedDifficulty == 4) return imMultiplier; return 1f; } private void SyncEnemyListToUI() { if (uiController == null) return; List enemyIds = new List(); if (assignedSongData != null && assignedSongData.chartFiles != null) { foreach (var entry in assignedSongData.chartFiles) { if (entry.difficulty == assignedDifficulty) { if (entry.enemyConfigList != null && entry.enemyConfigList.Count > 0) { foreach (var config in entry.enemyConfigList) { enemyIds.Add(config.enemyID); } } break; } } } while (enemyIds.Count < 5) enemyIds.Add(0); if (enemyIds.Count > 5) enemyIds = enemyIds.GetRange(0, 5); uiController.enemySlotIds = enemyIds; uiController.PopulateEnemySOsFromIds(); if (VerboseLogs) Debug.Log($"[BeatmapManager] SyncEnemyListToUI: {string.Join(",", enemyIds)}"); } private void SetupEnemiesAndHP() { if (uiController == null) { Debug.LogError("teamUIController not assigned in BeatmapManager Inspector"); return; } List enemyIds = new List(); List activePercentages = new List(); bool foundInDifficulty = false; if (assignedSongData != null && assignedSongData.chartFiles != null) { foreach (var entry in assignedSongData.chartFiles) { if (entry.difficulty == assignedDifficulty) { if (entry.enemyConfigList != null && entry.enemyConfigList.Count > 0) { foreach (var config in entry.enemyConfigList) { enemyIds.Add(config.enemyID); if (config.enemyID != 0) activePercentages.Add(config.hpPercentage); } foundInDifficulty = true; if (VerboseLogs) Debug.Log($"[BeatmapManager] Using enemyConfigList from ChartFileEntry (difficulty {assignedDifficulty})"); } break; } } } if (!foundInDifficulty && !parsedEnemyListIsEmpty && parsedColorSegments != null && parsedColorSegments.Length > 0) { foreach (var segment in parsedColorSegments) { if (VerboseLogs) Debug.Log($"Processing colorSegment: enemyID='{segment.enemyID}', percentage={segment.percentage}"); int enemyId; if (!int.TryParse(segment.enemyID, out enemyId)) { Debug.LogError($"Failed to parse enemyID '{segment.enemyID}' to int"); continue; } enemyIds.Add(enemyId); } if (VerboseLogs) Debug.Log($"[BeatmapManager] Using enemyList from Beatmap JSON: {string.Join(",", enemyIds)}"); } while (enemyIds.Count < 5) enemyIds.Add(0); if (enemyIds.Count > 5) enemyIds = enemyIds.GetRange(0, 5); uiController.enemySlotIds = enemyIds; uiController.PopulateEnemySOsFromIds(); if (parsedNoteAmount > 0) { float currentMultiplier = GetCurrentDifficultyMultiplier(); if (assignedSongData != null && assignedSongData.chartFiles != null) { foreach (var entry in assignedSongData.chartFiles) { if (entry.difficulty == assignedDifficulty) { if (entry.enemyTotalHP_Multiplier > 0.01f) { currentMultiplier = entry.enemyTotalHP_Multiplier; if (VerboseLogs) Debug.Log($"[BeatmapManager] Using override enemyTotalHP_Multiplier from SongData: {currentMultiplier}"); } break; } } } float totalCalculatedHP = parsedNoteAmount * currentMultiplier * baseHpUnitScale; int totalHP = Mathf.RoundToInt(totalCalculatedHP); int activeEnemyCount = enemyIds.FindAll(id => id != 0).Count; if (activeEnemyCount <= 0) activeEnemyCount = 1; List individualHPList = new List(); float sumPercentage = 0f; for (int i = 0; i < activePercentages.Count; i++) sumPercentage += activePercentages[i]; if (foundInDifficulty && activePercentages.Count == activeEnemyCount && sumPercentage > 0.1f) { float[] exact = new float[activeEnemyCount]; int[] baseHp = new int[activeEnemyCount]; float[] frac = new float[activeEnemyCount]; int allocated = 0; for (int i = 0; i < activeEnemyCount; i++) { exact[i] = totalHP * (activePercentages[i] / sumPercentage); baseHp[i] = Mathf.FloorToInt(exact[i]); frac[i] = exact[i] - baseHp[i]; allocated += baseHp[i]; } int remainder = totalHP - allocated; while (remainder > 0) { int bestIdx = 0; float bestFrac = -1f; for (int i = 0; i < activeEnemyCount; i++) { if (frac[i] > bestFrac) { bestFrac = frac[i]; bestIdx = i; } } baseHp[bestIdx] += 1; frac[bestIdx] = -1f; remainder--; } for (int i = 0; i < activeEnemyCount; i++) individualHPList.Add(baseHp[i]); if (VerboseLogs) Debug.Log($"[BeatmapManager] HP Calc (Custom Percentages) -> Total: {totalHP}, HPs: {string.Join(",", individualHPList)}"); } else { int basePer = totalHP / activeEnemyCount; int remainder = totalHP - (basePer * activeEnemyCount); for (int i = 0; i < activeEnemyCount; i++) { int hp = basePer + (i < remainder ? 1 : 0); individualHPList.Add(hp); } if (VerboseLogs) Debug.Log($"[BeatmapManager] HP Calc (Equal Dist) -> Total: {totalHP}, Active: {activeEnemyCount}, HPs: {string.Join(",", individualHPList)}"); } uiController.ApplyCalculatedEnemyHP(individualHPList); } // Initialize enemy ball list UI if (enemyBallLoader != null) { List enemyTypes = new List(); // Use uiController's populated list if available, otherwise just use IDs to look them up if possible // uiController.PopulateEnemySOsFromIds() fills uiController.recognizedEnemySOs if (uiController.recognizedEnemySOs != null) { foreach (var so in uiController.recognizedEnemySOs) { if (so != null) { enemyTypes.Add(so.enemyType); } } } enemyBallLoader.InitializeEnemyBalls(enemyTypes); } } // Parse JSON string to Beatmap and extras, then pass to NoteSpawner private void ProcessJsonAndLoad(string json) { if (string.IsNullOrEmpty(json)) { Debug.LogError("ProcessJsonAndLoad: json is empty"); return; } Beatmap parsed = null; try { parsed = JsonUtility.FromJson(json); } catch (System.Exception ex) { Debug.LogError($"解析 Beatmap 失败: {ex}"); return; } if (parsed == null) { Debug.LogError("解析 Beatmap 结果为 null"); return; } BeatmapExtra extra = null; try { extra = JsonUtility.FromJson(json); } catch { extra = null; } if (extra != null) { parsedTitle = extra.title; parsedComposer = extra.composer; parsedIllustrator = extra.illustrator; parsedCharter = extra.charter; parsedBeatmapId = extra.beatmapId; parsedDifficultyName = extra.difficultyName; parsedCreatedDate = extra.createdDate; parsedLastSavedTime = extra.lastSavedTime; parsedMusicFile = extra.musicFile; parsedBackgroundFile = extra.backgroundFile; parsedDuration = extra.duration; parsedBpm = extra.bpm; parsedDifficulty = extra.difficulty; parsedNoteAmount = extra.noteAmount; globalDelaySeconds = extra.globalDelaySeconds; if (!string.IsNullOrEmpty(extra.musicFile)) parsed.musicFile = extra.musicFile; if (!string.IsNullOrEmpty(extra.backgroundFile)) parsed.backgroundFile = extra.backgroundFile; parsedEnemyListIsEmpty = extra.enemyList_isEmpty; if (!parsedEnemyListIsEmpty) parsedColorSegments = extra.colorSegments; parsedNoteStatistics = extra.noteStatistics; parsedTrackStates = extra.trackStates; } else { parsedTitle = parsed.title; parsedComposer = parsed.composer; parsedIllustrator = parsed.illustrator; parsedCharter = parsed.charter; parsedBeatmapId = parsed.beatmapId; parsedDifficultyName = parsed.difficultyName; parsedCreatedDate = parsed.createdDate; parsedMusicFile = parsed.musicFile; parsedBackgroundFile = parsed.backgroundFile; parsedDuration = Mathf.RoundToInt(parsed.duration); parsedBpm = Mathf.RoundToInt(parsed.bpm); parsedDifficulty = parsed.difficulty; 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(parsed); beatmap = parsed; if (VerboseLogs) Debug.Log("谱面加载完成: " + beatmap.title); if (noteSpawner != null) noteSpawner.LoadBeatmap(beatmap); else Debug.LogError("NoteSpawner is null in BeatmapManager."); SetupEnemiesAndHP(); } private int GetEffectiveNoteCount(Beatmap sourceBeatmap = null) { int noteCount = 0; var src = sourceBeatmap != null ? sourceBeatmap : beatmap; if (src != null && src.notes != null) noteCount = src.notes.Length; if (noteCount <= 0) noteCount = Mathf.Max(0, parsedNoteAmount); if (noteCount > 0) parsedNoteAmount = noteCount; return noteCount; } private void CalculateNoteScores(Beatmap sourceBeatmap = null) { int noteCount = GetEffectiveNoteCount(sourceBeatmap); if (noteCount > 0) { perNoteScore = totalChartScore / noteCount; leftoverScore = totalChartScore % noteCount; if (VerboseLogs) Debug.Log($"Calculated perNoteScore: {perNoteScore}, leftoverScore: {leftoverScore}, noteCount: {noteCount}"); } else { perNoteScore = 0; leftoverScore = 0; Debug.LogWarning("parsedNoteAmount is zero or less; perNoteScore and leftoverScore set to 0."); } } 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 (JudgeManager.IsDebugEnabled) 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(); 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 (JudgeManager.IsDebugEnabled) Debug.Log($"[BeatmapManager] Calculated missing noteStatistics for {counts.Count} colors"); } } // Compute per-track note counts and apply max score caps to allies private void ApplyTrackScoreCaps(Beatmap parsed) { if (parsed == null || parsed.notes == null) return; const int trackCount = 5; var counts = new int[trackCount]; for (int i = 0; i < parsed.notes.Length; i++) { var n = parsed.notes[i]; int idx = n != null ? n.trackIndex : -1; if (idx < 0 || idx >= trackCount) continue; counts[idx]++; } int effectiveNoteCount = GetEffectiveNoteCount(parsed); int per = perNoteScore; if (per <= 0 && effectiveNoteCount > 0) per = totalChartScore / effectiveNoteCount; var ui = teamUIController.Instance; for (int i = 0; i < trackCount; i++) { int maxScore = Mathf.Max(0, per * counts[i]); var ally = ResolveAllyCombatant(i, ui); if (ally != null) { ally.SetMaxTrackScore(maxScore, true); continue; } // Fallback: update UI text directly if combatant not found string value = $"0/{maxScore}"; if (ui != null) { switch (i) { case 0: if (ui.teammate01_current_scoreText != null) ui.teammate01_current_scoreText.text = value; break; case 1: if (ui.teammate02_current_scoreText != null) ui.teammate02_current_scoreText.text = value; break; case 2: if (ui.teammate03_current_scoreText != null) ui.teammate03_current_scoreText.text = value; break; case 3: if (ui.teammate04_current_scoreText != null) ui.teammate04_current_scoreText.text = value; break; case 4: if (ui.teammate05_current_scoreText != null) ui.teammate05_current_scoreText.text = value; break; } } } } private AllyCombatant ResolveAllyCombatant(int slotIndex, teamUIController ui) { GameObject go = null; if (ui != null) go = ui.GetAllyObjectBySlot(slotIndex); if (go == null) { var byName = SceneObjectLookupCache.Find($"ally_0{slotIndex + 1}"); if (byName != null) go = byName; } if (go == null) return null; var ally = go.GetComponent(); if (ally != null) return ally; return go.GetComponentInChildren(true); } [System.Serializable] private class BeatmapExtra { public string title; public string composer; public string illustrator; public string charter; public string beatmapId; public int duration; // as int per request public int bpm; // as int per request public int difficulty; public string difficultyName; public string createdDate; public string lastSavedTime; public string musicFile; public string backgroundFile; public int noteAmount; public float globalDelaySeconds; public bool enemyList_isEmpty; public ColorSegment[] colorSegments; public NoteStatistic[] noteStatistics; public TrackStates trackStates; } [System.Serializable] public class ColorSegment { public string enemyID; public float percentage; } [System.Serializable] public class NoteStatistic { public string colorType; public int count; } [System.Serializable] public class TrackStates { public bool trackFading_active; public TrackState red; public TrackState green; public TrackState yellow; public TrackState purple; public TrackState blue; } [System.Serializable] public class TrackState { public bool isOn; public float fadeTime; } }