using System.IO; using UnityEngine; using UnityEngine.UI; using System.Collections.Generic; public class BeatmapManager : MonoBehaviour { // 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; Debug.LogWarning($"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; Debug.LogWarning($"BeatmapManager.AcceptSongData: accepted song={(song==null?"NULL":song.songName)}, difficulty={difficulty}"); } public Beatmap beatmap; // 当前谱面数据 // 音符生成器引用 public NoteSpawner noteSpawner; // teamUIController 引用,直接拖拽赋值 public teamUIController uiController; // 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("Scales the note amount to enemy HP. Example: NoteAmount * MaxMultiplier * BaseScale ≈ TotalHP")] 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; // 从 JSON 文件加载谱面数据 public void LoadBeatmap(string fileName) { string path = Application.streamingAssetsPath + "/" + fileName; if (File.Exists(path)) { string json = File.ReadAllText(path); Debug.Log("谱面 JSON 已读取: " + path); ProcessJsonAndLoad(json); } else { Debug.LogError("文件不存在:" + path); } } // 从原始 JSON 字符串加载谱面(允许任意磁盘路径先读取后传入) public void LoadBeatmapFromJsonString(string json) { if (string.IsNullOrEmpty(json)) { Debug.LogError("LoadBeatmapFromJsonString: json is null or empty"); return; } ProcessJsonAndLoad(json); } // 解析 JSON 但不启动 NoteSpawner(用于 test mode 的延迟开始) 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(); // NOTE: Do NOT modify parsed.notes here. globalDelaySeconds will be applied to audio playback instead. beatmap = parsed; Debug.Log("ParseJsonOnly: parsed beatmap " + beatmap.title); // 调用统一的敌人处理逻辑(包含 HP 计算和设置) SetupEnemiesAndHP(); return true; } // 直接传入已经解析的 Beatmap(兼容旧调用) public void LoadBeatmap(Beatmap loadedBeatmap) { beatmap = loadedBeatmap; 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"; if (File.Exists(path)) { string json = File.ReadAllText(path); ProcessJsonAndLoad(json); } else { Debug.LogError("谱面文件未找到!"); } } // 新增:从 TextAsset 解析并根据 parseOnly 决定是否只是解析或直接加载 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; } } // 新增:从 SongData 的 chart TextAsset 加载谱面 public bool LoadBeatmapFromSongData(SongData song, int difficulty, bool parseOnly = false) { if (song == null) { Debug.LogWarning("LoadBeatmapFromSongData: song is null"); return false; } TextAsset ta = song.GetChartFile(difficulty); if (ta == null) { Debug.LogWarning($"LoadBeatmapFromSongData: chart TextAsset for difficulty {difficulty} is null on song {song.songName}"); return false; } Debug.LogWarning($"LoadBeatmapFromSongData: loading chart for song {song.songName}, difficulty {difficulty}, parseOnly={parseOnly}, chartSize={(ta.text != null ? ta.text.Length : 0)}"); return LoadBeatmapFromTextAsset(ta, parseOnly); } // 在 Start() 方法中初始化 void Start() { // 被动接收上一个场景传递过来的 SongData(通过 BeatmapManager.pendingSongData) if (pendingSongData != null) { Debug.LogWarning($"BeatmapManager.Start: pendingSongData detected: {pendingSongData.songName}, difficulty={pendingDifficulty}"); // 将 pending 显示到 Inspector 字段,便于检查 assignedSongData = pendingSongData; assignedDifficulty = pendingDifficulty; // 尝试解析(只解析,不立即生成音符) bool ok = LoadBeatmapFromSongData(assignedSongData, assignedDifficulty, true); if (!ok) Debug.LogWarning("BeatmapManager.Start: LoadBeatmapFromSongData failed"); else { Debug.LogWarning("BeatmapManager.Start: chart parsed from SongData, attempting to assign audio and pause system"); // try to assign audio to GameManager.musicSource var gm = FindObjectOfType(); if (gm != null && gm.musicSource != null) { if (assignedSongData != null && assignedSongData.audioFile != null) { gm.musicSource.clip = assignedSongData.audioFile; gm.musicSource.loop = false; Debug.LogWarning("Assigned SongData.audioFile to GameManager.musicSource.clip (from SO)"); } else if (!string.IsNullOrEmpty(parsedMusicFile)) { Debug.LogWarning($"Attempting Resources.Load for audio: {parsedMusicFile}"); var ac = Resources.Load(parsedMusicFile); if (ac != null) { gm.musicSource.clip = ac; gm.musicSource.loop = false; Debug.LogWarning("Assigned audio via Resources.Load(parsedMusicFile)"); } else { Debug.LogWarning($"Resources.Load failed for '{parsedMusicFile}'"); } } else { Debug.LogWarning("No audio available on SongData and parsedMusicFile empty"); } } else { Debug.LogWarning("GameManager or its musicSource not found; audio not assigned"); } // Pause the system to show pause overlay (PauseManager will enable overlayRoot) var pauseMgr = PauseManager.Instance ?? FindObjectOfType(); if (pauseMgr != null) { pauseMgr.Pause(true); Debug.LogWarning("System paused after loading chart and audio to allow player to start"); } else { Debug.LogWarning("PauseManager not found in scene; cannot pause system automatically"); } } // clear pending so it doesn't run again pendingSongData = null; pendingDifficulty = -1; } // 如果处于测试模式,则跳过自动加载谱面 if (GameConfig.testMode) { Debug.Log("Test mode active: skipping automatic beatmap load in BeatmapManager.Start"); } else { // 原先默认加载的调试谱面已注释,改为仅在需要时手动恢复 // 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; if (parsedDifficulty == 2) return hdMultiplier; if (parsedDifficulty == 3) return inMultiplier; if (parsedDifficulty == 4) return imMultiplier; return 1f; } // 统一处理敌人 ID 提取、发送给 UI 以及 HP 计算的逻辑 private void SetupEnemiesAndHP() { if (uiController == null) { Debug.LogError("teamUIController not assigned in BeatmapManager Inspector"); return; } if (!parsedEnemyListIsEmpty && parsedColorSegments != null && parsedColorSegments.Length > 0) { List enemyIds = new List(); foreach (var segment in parsedColorSegments) { 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); } while (enemyIds.Count < 5) enemyIds.Add(0); Debug.Log($"Assigning enemySlotIds to teamUIController: {string.Join(",", enemyIds)}"); uiController.enemySlotIds = enemyIds; uiController.PopulateEnemySOsFromIds(); if (parsedNoteAmount > 0) { float currentMultiplier = GetCurrentDifficultyMultiplier(); float totalCalculatedHP = parsedNoteAmount * currentMultiplier * baseHpUnitScale; int activeEnemyCount = enemyIds.FindAll(id => id != 0).Count; if (activeEnemyCount == 0) activeEnemyCount = 1; int individualMaxHP = Mathf.RoundToInt(totalCalculatedHP / activeEnemyCount); Debug.Log($"[BeatmapManager] HP Calc -> Notes: {parsedNoteAmount}, Multiplier: {currentMultiplier}, Total: {totalCalculatedHP:F0}, ActiveEnemies: {activeEnemyCount}, Per Enemy: {individualMaxHP}"); uiController.ApplyCalculatedEnemyHP(individualMaxHP); } } else { Debug.Log("Parsed enemy list is empty, clearing UI."); uiController.enemySlotIds = new List { 0, 0, 0, 0, 0 }; uiController.PopulateEnemySOsFromIds(); } } // 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."); } CalculateNoteScores(); beatmap = parsed; Debug.Log("谱面已加载:" + beatmap.title); if (noteSpawner != null) noteSpawner.LoadBeatmap(beatmap); else Debug.LogError("NoteSpawner is null in BeatmapManager."); SetupEnemiesAndHP(); } private void CalculateNoteScores() { if (parsedNoteAmount > 0) { perNoteScore = totalChartScore / parsedNoteAmount; leftoverScore = totalChartScore % parsedNoteAmount; Debug.Log($"Calculated perNoteScore: {perNoteScore}, leftoverScore: {leftoverScore}"); } else { perNoteScore = 0; leftoverScore = 0; Debug.LogWarning("parsedNoteAmount is zero or less; perNoteScore and leftoverScore set to 0."); } } [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; } }