934 lines
36 KiB
C#
934 lines
36 KiB
C#
using System.IO;
|
||
using UnityEngine;
|
||
using UnityEngine.UI;
|
||
using System.Collections.Generic;
|
||
using TMPro;
|
||
|
||
public class BeatmapManager : MonoBehaviour
|
||
{
|
||
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;
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
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;
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
public void AcceptSongData(SongData song, int difficulty)
|
||
{
|
||
assignedSongData = song;
|
||
assignedDifficulty = difficulty;
|
||
Debug.LogWarning($"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 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);
|
||
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<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(parsed);
|
||
|
||
// NOTE: Do NOT modify parsed.notes here. globalDelaySeconds will be applied to audio playback instead.
|
||
|
||
beatmap = parsed;
|
||
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);
|
||
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.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);
|
||
}
|
||
|
||
// Documentation text normalized.
|
||
void Start()
|
||
{
|
||
// 如果 assignedSongData 为空,尝试从 SongDataHolder 获取,作为最后的兜底
|
||
if (pendingSongData == null && assignedSongData == null)
|
||
{
|
||
assignedSongData = SongDataHolder.SelectedSongData;
|
||
Debug.Log($"[BeatmapManager] No pending song, fallback to SongDataHolder: {(assignedSongData != null ? assignedSongData.songName : "null")}");
|
||
}
|
||
|
||
// 无论如何,在加载谱面前先同步敌人编队到 teamUIController
|
||
SyncEnemyListToUI();
|
||
|
||
if (pendingSongData != null)
|
||
{
|
||
Debug.LogWarning($"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
|
||
{
|
||
Debug.LogWarning("BeatmapManager.Start: chart parsed from SongData, attempting to assign audio and pause system");
|
||
|
||
// try to assign audio to GameManager.musicSource
|
||
var gm = FindAnyObjectByType<GameManager>();
|
||
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<AudioClip>(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");
|
||
}
|
||
|
||
// Assign fullscreen image from SongData to background images if available
|
||
if (assignedSongData != null && assignedSongData.fullscreen_songPicture != null)
|
||
{
|
||
Sprite bgSprite = assignedSongData.fullscreen_songPicture;
|
||
|
||
if (bgMainImage != null)
|
||
{
|
||
bgMainImage.sprite = bgSprite;
|
||
// ensure fully visible
|
||
bgMainImage.color = new Color(bgMainImage.color.r, bgMainImage.color.g, bgMainImage.color.b, 1f);
|
||
Debug.LogWarning($"Assigned SongData.fullscreen_songPicture to bgMainImage for song {assignedSongData.songName}");
|
||
}
|
||
|
||
if (bgSpriteRenderer != null)
|
||
{
|
||
bgSpriteRenderer.sprite = bgSprite;
|
||
Debug.LogWarning($"Assigned SongData.fullscreen_songPicture to bgSpriteRenderer for song {assignedSongData.songName}");
|
||
// 释放引用以节省内存(按要求:完毕后令 sprite renderer = null)
|
||
bgSpriteRenderer = null;
|
||
}
|
||
|
||
if (startCanvas_image != null)
|
||
{
|
||
startCanvas_image.sprite = bgSprite;
|
||
Debug.LogWarning($"Assigned SongData.fullscreen_songPicture to startCanvas_image for song {assignedSongData.songName}");
|
||
}
|
||
|
||
if (bgMainImage == null && startCanvas_image == null)
|
||
{
|
||
Debug.LogWarning("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 ?? FindAnyObjectByType<PauseManager>();
|
||
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;
|
||
}
|
||
|
||
// Documentation text normalized.
|
||
if (GameConfig.testMode)
|
||
{
|
||
Debug.Log("Test mode active: skipping automatic beatmap load in BeatmapManager.Start");
|
||
}
|
||
else
|
||
{
|
||
// Documentation text normalized.
|
||
// Documentation text normalized.
|
||
Debug.LogWarning("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;
|
||
}
|
||
}
|
||
|
||
// 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<int> enemyIds = new List<int>();
|
||
|
||
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();
|
||
Debug.Log($"[BeatmapManager] SyncEnemyListToUI: {string.Join(",", enemyIds)}");
|
||
}
|
||
|
||
private void SetupEnemiesAndHP()
|
||
{
|
||
if (uiController == null)
|
||
{
|
||
Debug.LogError("teamUIController not assigned in BeatmapManager Inspector");
|
||
return;
|
||
}
|
||
|
||
List<int> enemyIds = new List<int>();
|
||
List<float> activePercentages = new List<float>();
|
||
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;
|
||
Debug.Log($"[BeatmapManager] Using enemyConfigList from ChartFileEntry (difficulty {assignedDifficulty})");
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!foundInDifficulty && !parsedEnemyListIsEmpty && parsedColorSegments != null && parsedColorSegments.Length > 0)
|
||
{
|
||
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);
|
||
}
|
||
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;
|
||
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<int> individualHPList = new List<int>();
|
||
|
||
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]);
|
||
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);
|
||
}
|
||
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<EnemyData_SO.EnemyType> enemyTypes = new List<EnemyData_SO.EnemyType>();
|
||
// 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<Beatmap>(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<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;
|
||
|
||
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;
|
||
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;
|
||
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<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 (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 = GameObject.Find($"ally_0{slotIndex + 1}");
|
||
if (byName != null) go = byName;
|
||
}
|
||
if (go == null) return null;
|
||
var ally = go.GetComponent<AllyCombatant>();
|
||
if (ally != null) return ally;
|
||
return go.GetComponentInChildren<AllyCombatant>(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; }
|
||
}
|