518 lines
19 KiB
C#
518 lines
19 KiB
C#
using System.IO;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
using System.Collections.Generic;
|
|
|
|
public class BeatmapManager : MonoBehaviour
|
|
|
|
{
|
|
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
|
|
public float ezMultiplier = 1f;
|
|
public float hdMultiplier = 1.5f;
|
|
public float inMultiplier = 2f;
|
|
public float imMultiplier = 3f;
|
|
|
|
// 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<Beatmap>(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<BeatmapExtra>(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);
|
|
|
|
// Process colorSegments if not empty (for both test and normal mode)
|
|
if (!parsedEnemyListIsEmpty && parsedColorSegments != null && parsedColorSegments.Length > 0)
|
|
{
|
|
// Determine multiplier based on difficulty
|
|
float multiplier = ezMultiplier;
|
|
if (parsedDifficulty == 1) multiplier = hdMultiplier;
|
|
else if (parsedDifficulty == 2) multiplier = inMultiplier;
|
|
else if (parsedDifficulty == 3) multiplier = imMultiplier;
|
|
|
|
// Calculate total HP
|
|
float totalHP = parsedNoteAmount * multiplier;
|
|
|
|
// Set enemy slot IDs and calculate HP for each enemy
|
|
List<int> enemyIds = new List<int>();
|
|
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;
|
|
}
|
|
Debug.LogError($"Parsed enemyId: {enemyId}");
|
|
enemyIds.Add(enemyId);
|
|
|
|
// Calculate individual enemy HP
|
|
int enemyHP = Mathf.RoundToInt(totalHP * segment.percentage);
|
|
|
|
// Find and update the corresponding EnemyData_SO
|
|
EnemyData_SO[] allEnemies = Resources.LoadAll<EnemyData_SO>("");
|
|
foreach (var so in allEnemies)
|
|
{
|
|
if (so != null && so.enemyID == enemyId)
|
|
{
|
|
so.enemy_maxHP = enemyHP;
|
|
Debug.Log(string.Format("Set enemy {0} maxHP to {1}", enemyId, enemyHP));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update teamUIController
|
|
if (uiController != null)
|
|
{
|
|
Debug.Log($"enemyIds before padding: {string.Join(",", enemyIds)}");
|
|
// Ensure enemySlotIds has exactly 5 elements, padding with 0 if necessary
|
|
while (enemyIds.Count < 5)
|
|
{
|
|
enemyIds.Add(0);
|
|
}
|
|
Debug.Log($"enemyIds after padding: {string.Join(",", enemyIds)}");
|
|
uiController.enemySlotIds = enemyIds;
|
|
Debug.LogError($"Assigned enemySlotIds to teamUIController: {string.Join(",", enemyIds)}");
|
|
uiController.PopulateEnemySOsFromIds();
|
|
}
|
|
else
|
|
{
|
|
Debug.LogError("teamUIController not assigned in the Inspector");
|
|
}
|
|
}
|
|
|
|
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("谱面文件未找到!");
|
|
}
|
|
}
|
|
|
|
// 在 Start() 方法中初始化
|
|
void Start()
|
|
{
|
|
// 如果处于测试模式,则跳过自动加载谱面
|
|
if (GameConfig.testMode)
|
|
{
|
|
Debug.Log("Test mode active: skipping automatic beatmap load in BeatmapManager.Start");
|
|
}
|
|
else
|
|
{
|
|
// 加载默认谱面
|
|
LoadBeatmap("Emilia_demo.json"); // 加载并实例化音符
|
|
}
|
|
|
|
}
|
|
|
|
// Helper class used to parse extra fields from JSON that Beatmap doesn't define
|
|
[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;
|
|
|
|
// New fields
|
|
public bool enemyList_isEmpty;
|
|
public ColorSegment[] colorSegments;
|
|
public NoteStatistic[] noteStatistics;
|
|
public TrackStates trackStates;
|
|
}
|
|
|
|
// New serializable classes for the extra fields
|
|
[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;
|
|
}
|
|
|
|
// Parse JSON string to Beatmap and extras, then pass to NoteSpawner
|
|
private void ProcessJsonAndLoad(string json)
|
|
{
|
|
// Debug.LogError("ProcessJsonAndLoad called");
|
|
if (string.IsNullOrEmpty(json))
|
|
{
|
|
Debug.LogError("ProcessJsonAndLoad: json is empty");
|
|
return;
|
|
}
|
|
|
|
// Parse main beatmap
|
|
Beatmap parsed = null;
|
|
try
|
|
{
|
|
parsed = JsonUtility.FromJson<Beatmap>(json);
|
|
}
|
|
catch (System.Exception ex)
|
|
{
|
|
Debug.LogError($"解析 Beatmap 失败: {ex}");
|
|
return;
|
|
}
|
|
|
|
if (parsed == null)
|
|
{
|
|
Debug.LogError("解析 Beatmap 返回 null");
|
|
return;
|
|
}
|
|
|
|
// Parse extras using BeatmapExtra which contains the requested fields and types
|
|
BeatmapExtra extra = null;
|
|
try
|
|
{
|
|
extra = JsonUtility.FromJson<BeatmapExtra>(json);
|
|
}
|
|
catch { extra = null; }
|
|
|
|
if (extra != null)
|
|
{
|
|
// store into manager fields
|
|
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 JSON contains absolute paths for music/background, copy into parsed object for later use
|
|
if (!string.IsNullOrEmpty(extra.musicFile)) parsed.musicFile = extra.musicFile;
|
|
if (!string.IsNullOrEmpty(extra.backgroundFile)) parsed.backgroundFile = extra.backgroundFile;
|
|
|
|
Debug.Log($"Beatmap extras parsed: title={parsedTitle}, composer={parsedComposer}, duration={parsedDuration}, bpm={parsedBpm}, difficulty={parsedDifficulty}, noteAmount={parsedNoteAmount}, globalDelaySeconds={globalDelaySeconds}");
|
|
|
|
// New fields
|
|
parsedEnemyListIsEmpty = extra.enemyList_isEmpty;
|
|
if (!parsedEnemyListIsEmpty)
|
|
{
|
|
parsedColorSegments = extra.colorSegments;
|
|
}
|
|
parsedNoteStatistics = extra.noteStatistics;
|
|
parsedTrackStates = extra.trackStates;
|
|
}
|
|
else
|
|
{
|
|
// Fallback to populate from parsed Beatmap where possible
|
|
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;
|
|
|
|
// convert types
|
|
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.");
|
|
}
|
|
|
|
// Calculate per-note and leftover scores
|
|
CalculateNoteScores();
|
|
|
|
// NOTE: Do NOT modify parsed.notes here. globalDelaySeconds will be applied to audio playback instead.
|
|
|
|
// assign and hand off to spawner
|
|
beatmap = parsed;
|
|
Debug.Log("谱面已加载:" + beatmap.title);
|
|
noteSpawner.LoadBeatmap(beatmap);
|
|
|
|
// Process colorSegments if not empty
|
|
if (!parsedEnemyListIsEmpty && parsedColorSegments != null && parsedColorSegments.Length > 0)
|
|
{
|
|
// Determine multiplier based on difficulty
|
|
float multiplier = ezMultiplier;
|
|
if (parsedDifficulty == 1) multiplier = hdMultiplier;
|
|
else if (parsedDifficulty == 2) multiplier = inMultiplier;
|
|
else if (parsedDifficulty == 3) multiplier = imMultiplier;
|
|
|
|
// Calculate total HP
|
|
float totalHP = parsedNoteAmount * multiplier;
|
|
|
|
// Set enemy slot IDs and calculate HP for each enemy
|
|
List<int> enemyIds = new List<int>();
|
|
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;
|
|
}
|
|
Debug.LogError($"Parsed enemyId: {enemyId}");
|
|
enemyIds.Add(enemyId);
|
|
|
|
// Calculate individual enemy HP
|
|
int enemyHP = Mathf.RoundToInt(totalHP * segment.percentage);
|
|
|
|
// Find and update the corresponding EnemyData_SO
|
|
EnemyData_SO[] allEnemies = Resources.LoadAll<EnemyData_SO>("");
|
|
foreach (var so in allEnemies)
|
|
{
|
|
if (so != null && so.enemyID == enemyId)
|
|
{
|
|
so.enemy_maxHP = enemyHP;
|
|
Debug.Log(string.Format("Set enemy {0} maxHP to {1}", enemyId, enemyHP));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update teamUIController
|
|
if (uiController != null)
|
|
{
|
|
Debug.Log($"enemyIds before padding: {string.Join(",", enemyIds)}");
|
|
// Ensure enemySlotIds has exactly 5 elements, padding with 0 if necessary
|
|
while (enemyIds.Count < 5)
|
|
{
|
|
enemyIds.Add(0);
|
|
}
|
|
Debug.Log($"enemyIds after padding: {string.Join(",", enemyIds)}");
|
|
uiController.enemySlotIds = enemyIds;
|
|
Debug.LogError($"Assigned enemySlotIds to teamUIController: {string.Join(",", enemyIds)}");
|
|
uiController.PopulateEnemySOsFromIds();
|
|
}
|
|
else
|
|
{
|
|
Debug.LogError("teamUIController not assigned in the Inspector");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Calculate per-note and leftover scores based on parsedNoteAmount
|
|
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.");
|
|
}
|
|
}
|
|
}
|