超大量的更新 修复很多问题,gameplay特效初步

This commit is contained in:
FloatGaming
2026-02-14 23:46:20 +08:00
parent ef8eb67259
commit 3a7a0b4669
360 changed files with 85670 additions and 4144 deletions
+267 -190
View File
@@ -5,34 +5,39 @@ using UnityEngine;
[Serializable]
public class ChartFileEntry
{
[Header("难度序列号0/1/2/3 -> ez/hd/im/in")]
public int difficulty; // 该谱面对应的难度序列号
[Header("展示给用户的难度分级:float数字")]
[Header("Inspector")]
public int difficulty;
public float difficultyLEVEL;
[Header("本难本难度敌人总生命值=音符数量×下面的倍率")]
public float enemyTotalHP_Multiplier;
[Header("ez/hd/im/in 没啥用")]
public string difficultyName; // 难度名称
[Header("谱面文件 json")]
public TextAsset chartFile; // 谱面文件
[Header("本关本难度描述 文本")]
[TextArea(3,10)]
public string difficultyDescription;
[Header("本关本难度上次游玩分数记录")]
public int lastScoreForThisDifficulty; // 上次游玩(总分/参考)
[Header("本关本难度个人分记录(谱面分)")]
public int chartPersonalRecordForThisDifficulty; // 谱面(个人)分
[Header("本关本难度偶像分记录")]
public int idolPersonalRecordForThisDifficulty; // 偶像分
[Header("本关本难度总分展示 (谱面分 + 偶像分)")]
public int totalPersonalRecordForThisDifficulty; // 展示用:谱面分 + 偶像分 (总分)
[Header("本关本难度进度")]
public float levelProgressForThisDifficulty;
[Header("本关本难度最后游玩时间")]
public DateTime lastPlayTimeForThisDifficulty;
[Header("本关本难度提供的经验值")]
public int experienceProvidedForThisDifficulty;
[Header("Inspector")]
public float enemyTotalHP_Multiplier;
[Header("Inspector")]
public string difficultyName;
[Header("Inspector")]
public TextAsset chartFile;
[Header("Inspector")]
[TextArea(3, 10)]
public string difficultyDescription;
[Header("Inspector")]
public int lastScoreForThisDifficulty;
[Header("Inspector")]
public int chartPersonalRecordForThisDifficulty;
public int idolPersonalRecordForThisDifficulty;
public int totalPersonalRecordForThisDifficulty;
[Header("Inspector")]
public float levelProgressForThisDifficulty;
[Header("Inspector")]
public DateTime lastPlayTimeForThisDifficulty;
[Header("Inspector")]
public int experienceProvidedForThisDifficulty;
}
[CreateAssetMenu(fileName = "NewSongData", menuName = "EaseOut/SongData")]
@@ -54,49 +59,58 @@ public class SongData : ScriptableObject
public float time_totalPlayingTime;
public DateTime uploadData;
[Header("各种图")]
[Header("Inspector")]
public Sprite illustration;
public Sprite backgroudPIC;
public Sprite fullscreen_songPicture;
public Sprite card_profileImage;
[Header("右侧卡片用图")]
[Header("Inspector")]
public Sprite right_pic_bottom_image;
public Sprite right_pic_top_image;
[Header("音乐")]
[Header("Inspector")]
public AudioClip audioFile;
public AudioClip audio_previewAudio;
[Header("")]
// 支持不同难度的数据
public Dictionary<int, int> difficultyNumberMap = new Dictionary<int, int>(); // 各个难度的难度数值
public Dictionary<int, int> personalRecordMap = new Dictionary<int, int>(); // 各个难度的谱面(个人)最高分
public Dictionary<int, int> idolRecordMap = new Dictionary<int, int>(); // 各个难度的偶像分
public Dictionary<int, int> chartScoreMap = new Dictionary<int, int>(); // 各个难度的谱面(上次)分数记录
public Dictionary<int, TextAsset> chartFileMap = new Dictionary<int, TextAsset>(); // 各个难度下的谱面文件
[Header("Inspector")]
public int difficultyID;
public Dictionary<int, int> difficultyNumberMap = new Dictionary<int, int>();
public Dictionary<int, int> personalRecordMap = new Dictionary<int, int>();
public Dictionary<int, int> idolRecordMap = new Dictionary<int, int>();
public Dictionary<int, int> chartScoreMap = new Dictionary<int, int>();
public Dictionary<int, TextAsset> chartFileMap = new Dictionary<int, TextAsset>();
// 用于在 Inspector 选择谱面文件
public List<ChartFileEntry> chartFiles = new List<ChartFileEntry>();
// 个人最高记录(所有难度中的最高分) - 这里表示最高的「谱面分 + 偶像分」之和(全谱面最高)
public int personalRecord;
// 当前游玩的难度
public int difficultyID;
public float current_levelProgress;
public bool _if_level_is_EASE;
public int max_comboRecord;
[Header("当前选择的关卡难度")]
[Header("Inspector")]
public int thisLevel_selectedDifficultyID;
[Header("本关卡加入时间:字符串格式yyyy-mm-dd")]
[Header("Inspector")]
public string thisLevel_addDateString;
[Header("本关卡总体简介")]
[TextArea(3,10)]
[Header("Inspector")]
[TextArea(3, 10)]
public string thisLevel_overallDescription;
// Ensure dictionaries initialized when ScriptableObject is loaded/deserialized
[Header("Enemy Configuration")]
[Tooltip("List of enemy IDs for this song (0-5 enemies)")]
public List<int> enemyList = new List<int>();
private void OnValidate()
{
if (enemyList != null && enemyList.Count > 5)
{
enemyList.RemoveRange(5, enemyList.Count - 5);
Debug.LogWarning($"[SongData] {songName}: enemyList length restricted to a maximum of 5.");
}
}
private void OnEnable()
{
if (difficultyNumberMap == null) difficultyNumberMap = new Dictionary<int, int>();
@@ -104,71 +118,16 @@ public class SongData : ScriptableObject
if (idolRecordMap == null) idolRecordMap = new Dictionary<int, int>();
if (chartScoreMap == null) chartScoreMap = new Dictionary<int, int>();
if (chartFileMap == null) chartFileMap = new Dictionary<int, TextAsset>();
if (chartFiles == null) chartFiles = new List<ChartFileEntry>();
// If chartFiles exist in inspector, ensure maps reflect them
// Commented out automatic sync — disabled per request
// if (chartFiles != null && chartFiles.Count > 0)
// {
// SyncRecordsFromChartFiles();
// SyncChartFileMap();
// }
}
/*
// 构造函数:从 JSON 数据创建 SongData
public SongData(SongDataSerializable jsonData)
{
usernameID = jsonData.usernameID;
songName = jsonData.songName;
songID = jsonData.songID;
songLength_second = jsonData.songLength_second;
artistName = jsonData.artistName;
painter = jsonData.painter;
level_creator = jsonData.level_creator;
belongsTo_whichDLC = jsonData.belongsTo_whichDLC;
bpm = jsonData.bpm;
songDuration = jsonData.songDuration;
game_enterTimes = jsonData.game_enterTimes;
time_totalPlayingTime = jsonData.time_totalPlayingTime;
uploadData = DateTime.Parse(jsonData.uploadData);
difficultyID = jsonData.difficultyID;
current_levelProgress = jsonData.current_levelProgress;
_if_level_is_EASE = jsonData._if_level_is_EASE;
max_comboRecord = jsonData.max_comboRecord;
difficultyNumberMap = jsonData.difficultyNumberMap ?? new Dictionary<int,int>();
personalRecordMap = jsonData.personalRecordMap ?? new Dictionary<int,int>();
idolRecordMap = jsonData.idolRecordMap ?? new Dictionary<int,int>();
chartScoreMap = jsonData.chartScoreMap ?? new Dictionary<int,int>();
personalRecord = jsonData.personalRecord; // 赋值最高成绩
// 确保 JSON 里的谱面文件映射到可用的 List
foreach (var kvp in jsonData.chartFileMap)
{
chartFiles.Add(new ChartFileEntry { difficulty = kvp.Key, chartFile = Resources.Load<TextAsset>("Charts/" + kvp.Value) });
}
SyncChartFileMap();
// 从 chartFiles 中同步各难度的记录到字典(如果 inspector 中有填写)
SyncRecordsFromChartFiles();
// Ensure per-difficulty chartScoreMap populated from entries if not present
foreach (var entry in chartFiles)
{
if (!chartScoreMap.ContainsKey(entry.difficulty))
chartScoreMap[entry.difficulty] = entry.lastScoreForThisDifficulty;
}
SyncChartFileMap();
}
*/
// 获取所有难度中最高的成绩(谱面分 + 偶像分 的最大和)
public int CalculateHighestPersonalRecord()
{
if (personalRecordMap == null) personalRecordMap = new Dictionary<int,int>();
if (idolRecordMap == null) idolRecordMap = new Dictionary<int,int>();
if (personalRecordMap == null) personalRecordMap = new Dictionary<int, int>();
if (idolRecordMap == null) idolRecordMap = new Dictionary<int, int>();
int highest = 0;
foreach (var kvp in personalRecordMap)
@@ -178,15 +137,149 @@ public class SongData : ScriptableObject
int i = idolRecordMap.ContainsKey(diff) ? idolRecordMap[diff] : 0;
int combined = p + i;
if (combined > highest)
{
highest = combined;
}
}
personalRecord = highest;
return highest;
}
// 更新指定难度的最高纪录 (只有当总分更高时才更新)
/// <summary>
/// 将数据保存到持久化存储,并附加 MD5 校验和防止篡改。
/// </summary>
public void SavePersistent()
{
try
{
string path = System.IO.Path.Combine(Application.persistentDataPath, $"SongData_{songID}.json");
SongDataSerializable serializable = new SongDataSerializable(this);
// 1. 序列化基础数据
string jsonWithoutHash = JsonUtility.ToJson(serializable);
// 2. 计算 MD5 (使用自定义盐值提高安全性)
serializable.dataHash = CalculateMD5(jsonWithoutHash + "bansonic2026@fudongyouxi");
// 3. 再次序列化包含 Hash 的最终数据
string finalJson = JsonUtility.ToJson(serializable, true);
System.IO.File.WriteAllText(path, finalJson);
#if UNITY_EDITOR
if (!Application.isPlaying)
{
UnityEditor.EditorUtility.SetDirty(this);
UnityEditor.AssetDatabase.SaveAssets();
}
#endif
Debug.Log($"[SongData] {songName} saved with hash validation.");
}
catch (System.Exception e)
{
Debug.LogError($"[SongData] Save failed: {e.Message}");
}
}
/// <summary>
/// 从持久化 JSON 恢复数据。
/// 校验逻辑已迁移至 DataCheck 脚本,此处仅执行基础加载。
/// </summary>
public void LoadPersistent()
{
string path = System.IO.Path.Combine(Application.persistentDataPath, $"SongData_{songID}.json");
if (System.IO.File.Exists(path))
{
try
{
string json = System.IO.File.ReadAllText(path);
// 尝试解析数据
SongDataSerializable serializable = JsonUtility.FromJson<SongDataSerializable>(json);
if (serializable != null)
{
serializable.ApplyTo(this);
// Debug.Log($"[SongData] {songName} data restored.");
}
}
catch (System.Exception e)
{
Debug.LogWarning($"[SongData] {songName} 基础加载跳过: {e.Message}");
}
}
}
/// <summary>
/// 当检测到篡改或文件损坏时,重置所有动态记录并立即重新保存文件以恢复合法性。
/// </summary>
private void ResetToZeroAndSave()
{
// 1. 重置所有动态数值字段
game_enterTimes = 0;
time_totalPlayingTime = 0;
current_levelProgress = 0;
_if_level_is_EASE = false;
max_comboRecord = 0;
personalRecord = 0;
// 2. 清空并确保初始化所有动态字典映射
if (difficultyNumberMap == null) difficultyNumberMap = new Dictionary<int, int>();
else difficultyNumberMap.Clear();
if (personalRecordMap == null) personalRecordMap = new Dictionary<int, int>();
else personalRecordMap.Clear();
if (idolRecordMap == null) idolRecordMap = new Dictionary<int, int>();
else idolRecordMap.Clear();
if (chartScoreMap == null) chartScoreMap = new Dictionary<int, int>();
else chartScoreMap.Clear();
// 3. 立即调用保存方法,生成一个新的、带有正确哈希值的合法文件
SavePersistent();
Debug.LogWarning($"[SongData] {songName} has been fully reset to zero due to data integrity failure.");
}
/// <summary>
/// 静态校验方法,可在异步线程调用,判断 JSON 内容是否合法。
/// </summary>
public static bool VerifyJsonIntegrity(string json, string salt, out SongDataSerializable result)
{
result = null;
try
{
SongDataSerializable serializable = JsonUtility.FromJson<SongDataSerializable>(json);
if (serializable == null) return false;
string savedHash = serializable.dataHash;
serializable.dataHash = "";
string currentJson = JsonUtility.ToJson(serializable);
// 使用静态 MD5 计算
string calculatedHash = StaticCalculateMD5(currentJson + salt);
result = serializable;
return savedHash == calculatedHash;
}
catch
{
return false;
}
}
public static string StaticCalculateMD5(string input)
{
using (System.Security.Cryptography.MD5 md5 = System.Security.Cryptography.MD5.Create())
{
byte[] inputBytes = System.Text.Encoding.UTF8.GetBytes(input);
byte[] hashBytes = md5.ComputeHash(inputBytes);
return System.BitConverter.ToString(hashBytes).Replace("-", "").ToLower();
}
}
private string CalculateMD5(string input)
{
return StaticCalculateMD5(input);
}
public void UpdateDifficultyRecord(int difficulty, int pmScore, int idolScore)
{
if (personalRecordMap == null) personalRecordMap = new Dictionary<int, int>();
@@ -194,43 +287,37 @@ public class SongData : ScriptableObject
int currentBestTotal = GetPersonalRecord(difficulty) + GetIdolRecord(difficulty);
int newTotal = pmScore + idolScore;
if (newTotal <= currentBestTotal)
return;
if (newTotal > currentBestTotal)
personalRecordMap[difficulty] = pmScore;
idolRecordMap[difficulty] = idolScore;
ChartFileEntry entry = chartFiles != null ? chartFiles.Find(e => e != null && e.difficulty == difficulty) : null;
if (entry != null)
{
personalRecordMap[difficulty] = pmScore;
idolRecordMap[difficulty] = idolScore;
// also update corresponding ChartFileEntry if exists
var entry = chartFiles != null ? chartFiles.Find(e => e.difficulty == difficulty) : null;
if (entry != null)
{
entry.chartPersonalRecordForThisDifficulty = pmScore;
entry.idolPersonalRecordForThisDifficulty = idolScore;
entry.totalPersonalRecordForThisDifficulty = newTotal;
}
CalculateHighestPersonalRecord();
entry.chartPersonalRecordForThisDifficulty = pmScore;
entry.idolPersonalRecordForThisDifficulty = idolScore;
entry.totalPersonalRecordForThisDifficulty = newTotal;
}
CalculateHighestPersonalRecord();
}
// 获取全难度下的最高分及其对应的难度名称
public void GetAbsoluteHighestScore(out int highestScore, out int bestDifficulty, out string bestDifficultyName)
{
highestScore = 0;
bestDifficulty = -1;
bestDifficultyName = "";
// 优先遍历 chartFiles 获取数据和名称
if (chartFiles != null && chartFiles.Count > 0)
if (chartFiles != null)
{
foreach (var entry in chartFiles)
for (int i = 0; i < chartFiles.Count; i++)
{
ChartFileEntry entry = chartFiles[i];
if (entry == null) continue;
// 重新计算总分以确保准确
int total = GetPersonalRecord(entry.difficulty) + GetIdolRecord(entry.difficulty);
// 如果字典里没数据,尝试用 entry 里的初始数据
if (total <= 0) total = entry.totalPersonalRecordForThisDifficulty;
if (total <= 0) total = entry.chartPersonalRecordForThisDifficulty + entry.idolPersonalRecordForThisDifficulty;
@@ -243,21 +330,18 @@ public class SongData : ScriptableObject
}
}
// 如果 chartFiles 为空,检查字典作为备选
if (bestDifficulty == -1 && personalRecordMap != null)
if (bestDifficulty != -1 || personalRecordMap == null)
return;
foreach (var kvp in personalRecordMap)
{
foreach (var kvp in personalRecordMap)
int diff = kvp.Key;
int total = kvp.Value + GetIdolRecord(diff);
if (total > highestScore || bestDifficulty == -1)
{
int diff = kvp.Key;
int p = kvp.Value;
int i = GetIdolRecord(diff);
int combined = p + i;
if (combined > highestScore || bestDifficulty == -1)
{
highestScore = combined;
bestDifficulty = diff;
bestDifficultyName = GetDifficultyDefaultName(diff);
}
highestScore = total;
bestDifficulty = diff;
bestDifficultyName = GetDifficultyDefaultName(diff);
}
}
}
@@ -274,17 +358,13 @@ public class SongData : ScriptableObject
}
}
// 更新指定难度的谱面(个人)最高成绩
public void UpdatePersonalRecord(int difficulty, int newScore)
{
if (personalRecordMap == null) personalRecordMap = new Dictionary<int,int>();
if (personalRecordMap == null) personalRecordMap = new Dictionary<int, int>();
if (!personalRecordMap.ContainsKey(difficulty) || newScore > personalRecordMap[difficulty])
{
personalRecordMap[difficulty] = newScore;
}
// also update corresponding ChartFileEntry if exists
var entry = chartFiles != null ? chartFiles.Find(e => e.difficulty == difficulty) : null;
ChartFileEntry entry = chartFiles != null ? chartFiles.Find(e => e != null && e.difficulty == difficulty) : null;
if (entry != null)
{
entry.chartPersonalRecordForThisDifficulty = personalRecordMap[difficulty];
@@ -295,17 +375,13 @@ public class SongData : ScriptableObject
CalculateHighestPersonalRecord();
}
// 更新指定难度的偶像分
public void UpdateIdolRecord(int difficulty, int newIdolScore)
{
if (idolRecordMap == null) idolRecordMap = new Dictionary<int,int>();
if (idolRecordMap == null) idolRecordMap = new Dictionary<int, int>();
if (!idolRecordMap.ContainsKey(difficulty) || newIdolScore > idolRecordMap[difficulty])
{
idolRecordMap[difficulty] = newIdolScore;
}
// also update corresponding ChartFileEntry if exists
var entry = chartFiles != null ? chartFiles.Find(e => e.difficulty == difficulty) : null;
ChartFileEntry entry = chartFiles != null ? chartFiles.Find(e => e != null && e.difficulty == difficulty) : null;
if (entry != null)
{
entry.idolPersonalRecordForThisDifficulty = idolRecordMap[difficulty];
@@ -316,93 +392,94 @@ public class SongData : ScriptableObject
CalculateHighestPersonalRecord();
}
// 更新或设置谱面上次分数记录(last run 总分)
public void UpdateChartScore(int difficulty, int newChartScore)
{
if (chartScoreMap == null) chartScoreMap = new Dictionary<int,int>();
if (chartScoreMap == null) chartScoreMap = new Dictionary<int, int>();
chartScoreMap[difficulty] = newChartScore;
var entry = chartFiles != null ? chartFiles.Find(e => e.difficulty == difficulty) : null;
ChartFileEntry entry = chartFiles != null ? chartFiles.Find(e => e != null && e.difficulty == difficulty) : null;
if (entry != null)
{
entry.lastScoreForThisDifficulty = newChartScore;
}
}
// 获取特定难度的谱面(个人)最高分
public int GetPersonalRecord(int difficulty)
{
if (personalRecordMap == null) return 0;
return personalRecordMap.ContainsKey(difficulty) ? personalRecordMap[difficulty] : 0;
return personalRecordMap.TryGetValue(difficulty, out int v) ? v : 0;
}
// 获取特定难度的偶像分
public int GetIdolRecord(int difficulty)
{
if (idolRecordMap == null) return 0;
return idolRecordMap.ContainsKey(difficulty) ? idolRecordMap[difficulty] : 0;
return idolRecordMap.TryGetValue(difficulty, out int v) ? v : 0;
}
// 获取特定难度的谱面(上次)分数
public int GetChartScore(int difficulty)
{
if (chartScoreMap == null) return 0;
return chartScoreMap.ContainsKey(difficulty) ? chartScoreMap[difficulty] : 0;
return chartScoreMap.TryGetValue(difficulty, out int v) ? v : 0;
}
// 获取指定难度的谱面文件
public TextAsset GetChartFile(int difficulty)
{
if (chartFiles == null) return null;
foreach (var entry in chartFiles)
if (chartFiles != null)
{
if (entry.difficulty == difficulty)
return entry.chartFile;
for (int i = 0; i < chartFiles.Count; i++)
{
ChartFileEntry entry = chartFiles[i];
if (entry != null && entry.difficulty == difficulty)
return entry.chartFile;
}
}
if (chartFileMap != null && chartFileMap.TryGetValue(difficulty, out TextAsset mapped))
return mapped;
return null;
}
// 同步 Dictionary 和 List 确保数据一致
public void SyncChartFileMap()
{
if (chartFileMap == null) chartFileMap = new Dictionary<int, TextAsset>();
chartFileMap.Clear();
if (chartFiles == null) return;
foreach (var entry in chartFiles)
for (int i = 0; i < chartFiles.Count; i++)
{
if (entry.chartFile != null)
{
ChartFileEntry entry = chartFiles[i];
if (entry != null && entry.chartFile != null)
chartFileMap[entry.difficulty] = entry.chartFile;
}
}
}
// 从 chartFiles 中读取各难度的谱面分/偶像分到 Map(用于 inspector 已填写时)
public void SyncRecordsFromChartFiles()
{
if (personalRecordMap == null) personalRecordMap = new Dictionary<int,int>();
if (idolRecordMap == null) idolRecordMap = new Dictionary<int,int>();
if (chartScoreMap == null) chartScoreMap = new Dictionary<int,int>();
if (personalRecordMap == null) personalRecordMap = new Dictionary<int, int>();
if (idolRecordMap == null) idolRecordMap = new Dictionary<int, int>();
if (chartScoreMap == null) chartScoreMap = new Dictionary<int, int>();
if (chartFiles == null) return;
foreach (var entry in chartFiles)
for (int i = 0; i < chartFiles.Count; i++)
{
ChartFileEntry entry = chartFiles[i];
if (entry == null) continue;
if (!difficultyNumberMap.ContainsKey(entry.difficulty))
difficultyNumberMap[entry.difficulty] = Mathf.RoundToInt(entry.difficultyLEVEL);
if (!personalRecordMap.ContainsKey(entry.difficulty) || entry.chartPersonalRecordForThisDifficulty > personalRecordMap[entry.difficulty])
{
personalRecordMap[entry.difficulty] = entry.chartPersonalRecordForThisDifficulty;
}
if (!idolRecordMap.ContainsKey(entry.difficulty) || entry.idolPersonalRecordForThisDifficulty > idolRecordMap[entry.difficulty])
{
idolRecordMap[entry.difficulty] = entry.idolPersonalRecordForThisDifficulty;
}
if (!chartScoreMap.ContainsKey(entry.difficulty) || entry.lastScoreForThisDifficulty > chartScoreMap[entry.difficulty])
{
chartScoreMap[entry.difficulty] = entry.lastScoreForThisDifficulty;
}
// keep per-entry convenience field in sync to combined total
int p = personalRecordMap.ContainsKey(entry.difficulty) ? personalRecordMap[entry.difficulty] : 0;
int i = idolRecordMap.ContainsKey(entry.difficulty) ? idolRecordMap[entry.difficulty] : 0;
entry.totalPersonalRecordForThisDifficulty = p + i;
int idol = idolRecordMap.ContainsKey(entry.difficulty) ? idolRecordMap[entry.difficulty] : 0;
entry.totalPersonalRecordForThisDifficulty = p + idol;
}
CalculateHighestPersonalRecord();
+1 -1
View File
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 08998297ea3a5b646991a4924cef9953
guid: 46295aa79dc737041b20f7dea3071642
folderAsset: yes
DefaultImporter:
externalObjects: {}
@@ -1,4 +1,4 @@
using UnityEngine;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
using System.Collections;
@@ -8,7 +8,7 @@ public class SongButton : MonoBehaviour
{
[Header("thisSong_so")]
public ScriptableObject thisSong_so;
[Header("显示信息")]
[Header("Inspector")]
public Text songNameText;
public Text multiNameText;
public Text joinTime_and_dlcName_Text;
@@ -19,14 +19,14 @@ public class SongButton : MonoBehaviour
public Text songIDText;
public Image buttonImage;
public Image scoreLevelImage;
[Header("头像")]
[Header("Inspector")]
public Image profileImage;
public int song_songSerialID;
[Header("select mark")]
public Image selected_boarder;
public Image fade_image;
[Header("评分阈值")]
[Header("Inspector")]
public float easePercent = 1f;
public float ssPercent = 0.95f;
public float sPercent = 0.9f;
@@ -36,7 +36,7 @@ public class SongButton : MonoBehaviour
public float dPercent = 0.5f;
public float ePercent = 0.4f;
public float fPercent = 0.3f;
[Header("评级图")]
[Header("Inspector")]
public Sprite easeSprite;
public Sprite ssSprite;
public Sprite sSprite;
@@ -91,21 +91,21 @@ public class SongButton : MonoBehaviour
if (songNameText != null) songNameText.text = sd.songName;
if (multiNameText != null)
multiNameText.text = "作曲:" + sd.artistName + " | 画师:" + sd.painter + " | 制谱:" + sd.level_creator;
multiNameText.text = "Artist: " + sd.artistName + " | Illustrator: " + sd.painter + " | Charter: " + sd.level_creator;
if (joinTime_and_dlcName_Text != null)
{
string join = string.IsNullOrEmpty(sd.thisLevel_addDateString) ? "未知" : sd.thisLevel_addDateString;
string dlc = string.IsNullOrEmpty(sd.belongsTo_whichDLC) ? "未知DLC" : sd.belongsTo_whichDLC;
joinTime_and_dlcName_Text.text = "上线时间:" + join + " | " + "所属DLC" + dlc;
string join = string.IsNullOrEmpty(sd.thisLevel_addDateString) ? "Unknown" : sd.thisLevel_addDateString;
string dlc = string.IsNullOrEmpty(sd.belongsTo_whichDLC) ? "Unknown DLC" : sd.belongsTo_whichDLC;
joinTime_and_dlcName_Text.text = "Release: " + join + " | DLC: " + dlc;
}
if (songOverallDescriptionText != null)
{
songOverallDescriptionText.text = string.IsNullOrEmpty(sd.thisLevel_overallDescription) ? "关卡简介:" : sd.thisLevel_overallDescription;
songOverallDescriptionText.text = string.IsNullOrEmpty(sd.thisLevel_overallDescription) ? "" : sd.thisLevel_overallDescription;
}
// 使用 SongData 提供的统一方法获取全难度最高分及其对应的难度名称
// Documentation text normalized.
int bestTotal = 0;
int bestDiff = -1;
string bestDiffName = "";
@@ -115,7 +115,7 @@ public class SongButton : MonoBehaviour
if (highestScoreText != null) highestScoreText.text = bestTotal.ToString();
// 在指定位置展示该最高分所属的难度名称
// Documentation text normalized.
if (highestScoreDifficultyText != null)
{
highestScoreDifficultyText.text = string.IsNullOrEmpty(bestDiffName) ? "" : "(" + bestDiffName + ")";
@@ -127,7 +127,7 @@ public class SongButton : MonoBehaviour
Sprite chosenSprite = null;
float baseScore = 2000000f;
// 如果从来没玩过(总分为0),显示未完成图
// Documentation text normalized.
if (bestTotal <= 0)
{
chosenSprite = ufSprite;
@@ -198,7 +198,7 @@ public class SongButton : MonoBehaviour
}
song_songSerialID = songID;
if (songIDText != null) songIDText.text = "歌曲识别号:" + song_songSerialID.ToString();
if (songIDText != null) songIDText.text = "Song ID: " + song_songSerialID.ToString();
}
else
{
@@ -1,8 +1,8 @@
using System.Collections.Generic;
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(fileName = "SongList", menuName = "Music/SongList")]
public class SongList : ScriptableObject
{
public List<SongData> songs = new List<SongData>(); // 存储所有歌曲的数据
public List<SongData> songs = new List<SongData>(); // Documentation text normalized.
}
@@ -1,4 +1,4 @@
using UnityEngine;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using UnityEngine.SceneManagement;
@@ -8,9 +8,9 @@ using DG.Tweening;
public class SongSelectUI : MonoBehaviour
{
public SongList songList; // 存储歌曲列表的 ScriptableObject
public GameObject songButtonPrefab; // 歌曲按钮的 Prefab (Button)
public Transform contentPanel; // 滚动视图的 Content 面板
public SongList songList; // Documentation text normalized.
public GameObject songButtonPrefab; // Documentation text normalized.
public Transform contentPanel; // Documentation text normalized.
[SerializeField] Button button_Main;
[SerializeField] string ui_Main_Scene_Name = "UI_UI";
@@ -42,6 +42,12 @@ public class SongSelectUI : MonoBehaviour
void Start()
{
// Defensive reset: prevent select-scene enter animations from freezing invisible.
if (Time.timeScale <= 0f)
{
Time.timeScale = 1f;
}
if (songList == null || songButtonPrefab == null || contentPanel == null)
{
Debug.LogError("请在 Inspector 中分配所有字段!");
@@ -73,7 +79,7 @@ public class SongSelectUI : MonoBehaviour
}
}
// 显示所有歌曲 (kept for manual use)
// Documentation text normalized.
public void DisplaySongs()
{
DisplaySongsFromList(songList != null ? songList.songs : null);
@@ -373,3 +379,4 @@ public class SongSelectUI : MonoBehaviour
// helper to avoid hardcoding scene name string in delegate - keeps serializer-friendly field
private string ui_Main_Scene_NAME() => ui_Main_Scene_Name;
}
@@ -1,11 +1,2 @@
fileFormatVersion: 2
guid: 476464d443256964eb8b3eeac58bfc2f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+6 -4
View File
@@ -1,4 +1,4 @@
using UnityEngine;
using UnityEngine;
using System.Collections.Generic;
using System.Collections;
@@ -7,8 +7,8 @@ public class SongDataLibrary : MonoBehaviour
public static SongDataLibrary Instance;
private Dictionary<int, SongData> songDataDictionary = new Dictionary<int, SongData>();
// ָ "song_songIndex" Ŀ¼µĸļ
// Assets/Resources/song_songIndex/FolderA FolderB Inspector д { "FolderA", "FolderB" }
// Documentation text normalized.
// Documentation text normalized.
public string[] subfolderNames;
[Header("Song List")]
@@ -111,6 +111,7 @@ public class SongDataLibrary : MonoBehaviour
}
if (!songDataDictionary.ContainsKey(data.songID))
{
data.LoadPersistent(); // 启动时从持久化存档恢复数据
songDataDictionary.Add(data.songID, data);
if (logLoadProgress)
{
@@ -146,6 +147,7 @@ public class SongDataLibrary : MonoBehaviour
if (data == null) continue;
if (!songDataDictionary.ContainsKey(data.songID))
{
data.LoadPersistent(); // 启动时从持久化存档恢复数据
songDataDictionary.Add(data.songID, data);
if (logLoadProgress)
{
@@ -170,7 +172,7 @@ public SongData GetSongDataByID(int songID)
{
return songDataDictionary[songID];
}
Debug.LogError("δҵ songID: " + songID);
Debug.LogError("未找到 songID: " + songID);
return null;
}
}
+3 -2
View File
@@ -17,7 +17,7 @@ public class SongDataLoader : MonoBehaviour
{
if (!Directory.Exists(jsonDirectory))
{
Debug.LogError("JSON Îļþ¼Ð²»´æÔÚ£¡");
Debug.LogError("JSON ļвڣ");
return;
}
@@ -29,7 +29,8 @@ public class SongDataLoader : MonoBehaviour
string json = File.ReadAllText(file);
SongDataSerializable jsonData = JsonUtility.FromJson<SongDataSerializable>(json);
loadedSongs.Add(jsonData);
Debug.Log($"Loaded song: {jsonData.songName}");
// 由于 SongDataSerializable 现在只存储动态分数,不再包含 songName
Debug.Log($"Loaded persistent song data from: {Path.GetFileName(file)}");
}
}
}
+10 -19
View File
@@ -1,27 +1,23 @@
using System.IO;
using System.IO;
using UnityEngine;
public class SongDataManager : MonoBehaviour
{
// JSON ļĿ¼·
private string jsonDirectory;
[Header("Settings")]
public bool autoSaveOnStart = false; // 默认关闭,仅在需要同步数据时手动开启
public bool autoSaveOnStart = false;
void Start()
{
// 获取持久化存储路径,并在该目录下创建 "SongDataJson" 文件夹
jsonDirectory = Path.Combine(Application.persistentDataPath, "SongDataJson");
jsonDirectory = Path.Combine(Application.persistentDataPath, "song_json_export");
// 如果目录不存在,则创建目录
if (!Directory.Exists(jsonDirectory))
{
Directory.CreateDirectory(jsonDirectory);
Debug.Log("创建 JSON 存储目录: " + jsonDirectory);
Debug.Log("Created song JSON directory: " + jsonDirectory);
}
// 只有开启了 autoSaveOnStart 且在编辑器模式下才自动执行
if (autoSaveOnStart)
{
StartCoroutine(SaveAllSongDataToJsonRoutine());
@@ -35,35 +31,30 @@ public class SongDataManager : MonoBehaviour
System.Collections.IEnumerator SaveAllSongDataToJsonRoutine()
{
// 从 Resources 文件夹下的 "song_songIndex" 子目录加载所有 SongData 资源
SongData[] allSongData = Resources.LoadAll<SongData>("song_songIndex");
if (allSongData.Length == 0)
{
Debug.LogWarning("未在 Resources/song_songIndex 找到任何 SongData 资源。");
Debug.LogWarning("No SongData assets found in Resources/song_songIndex.");
yield break;
}
Debug.Log($"开始异步保存 {allSongData.Length} 个歌曲数据...");
Debug.Log($"Start exporting {allSongData.Length} songs to JSON...");
// 遍历每个 SongData 资源
for (int i = 0; i < allSongData.Length; i++)
{
SongData song = allSongData[i];
// 使用 SongDataSerializable 将 SongData 转换为可序列化对象
if (song == null) continue;
SongDataSerializable jsonData = new SongDataSerializable(song);
string json = JsonUtility.ToJson(jsonData, true);
string filePath = Path.Combine(jsonDirectory, song.songID + ".json");
File.WriteAllText(filePath, json);
// 每处理 5 个文件等待一帧,防止主线程卡死
if (i % 5 == 0)
{
yield return null;
}
yield return null;
}
Debug.Log("所有 JSON 文件保存完毕。");
Debug.Log("All song JSON files exported.");
}
}
+72 -82
View File
@@ -1,107 +1,97 @@
using System;
using System;
using System.Collections.Generic;
using UnityEngine;
[Serializable]
public class SongDataSerializable
{
public string usernameID;
public string songName;
public int songID;
public int songLength_second;
public string artistName;
public string painter;
public string level_creator;
public string belongsTo_whichDLC;
public int bpm;
public float songDuration;
// --- 动态变动数据 (需要存储) ---
public int game_enterTimes;
public float time_totalPlayingTime;
public string uploadData;
public int difficultyID;
public float current_levelProgress;
public bool _if_level_is_EASE;
public int max_comboRecord;
public int personalRecord;
public string dataHash; // 防篡改校验和
public List<int> enemyList = new List<int>();
public int personalRecord; // ߷ (˷ + ż)
public Dictionary<int, int> difficultyNumberMap = new Dictionary<int, int>();
public Dictionary<int, int> personalRecordMap = new Dictionary<int, int>();
public Dictionary<int, int> idolRecordMap = new Dictionary<int, int>();
public Dictionary<int, int> chartScoreMap = new Dictionary<int, int>();
public Dictionary<int, string> chartFileMap = new Dictionary<int, string>();
public SongDataSerializable(SongData songData)
// --- 字典映射表 (JsonUtility 不支持 Dictionary,需转为 List 存储) ---
[System.Serializable]
public struct IntIntEntry
{
usernameID = songData.usernameID;
songName = songData.songName;
songID = songData.songID;
songLength_second = songData.songLength_second;
artistName = songData.artistName;
painter = songData.painter;
level_creator = songData.level_creator;
belongsTo_whichDLC = songData.belongsTo_whichDLC;
bpm = songData.bpm;
songDuration = songData.songDuration;
game_enterTimes = songData.game_enterTimes;
time_totalPlayingTime = songData.time_totalPlayingTime;
uploadData = songData.uploadData.ToString();
difficultyID = songData.difficultyID;
current_levelProgress = songData.current_levelProgress;
_if_level_is_EASE = songData._if_level_is_EASE;
max_comboRecord = songData.max_comboRecord;
difficultyNumberMap = songData.difficultyNumberMap;
personalRecordMap = songData.personalRecordMap;
idolRecordMap = songData.idolRecordMap;
chartScoreMap = songData.chartScoreMap;
personalRecord = songData.personalRecord; // ۺ߷
chartFileMap = new Dictionary<int, string>();
/*foreach (var kvp in songData.chartFileMap)
{
if (songData.chartFileMap == null) return;
chartFileMap[kvp.Key] = kvp.Value ? kvp.Value.name : "";
}*/
public int key;
public int value;
}
public void ApplyTo(SongData songData)
public List<IntIntEntry> difficultyNumberList = new List<IntIntEntry>();
public List<IntIntEntry> personalRecordList = new List<IntIntEntry>();
public List<IntIntEntry> idolRecordList = new List<IntIntEntry>();
public List<IntIntEntry> chartScoreList = new List<IntIntEntry>();
public SongDataSerializable() { }
public SongDataSerializable(SongData source)
{
songData.usernameID = usernameID;
songData.songName = songName;
songData.songID = songID;
songData.songLength_second = songLength_second;
songData.artistName = artistName;
songData.painter = painter;
songData.level_creator = level_creator;
songData.belongsTo_whichDLC = belongsTo_whichDLC;
if (source == null) return;
songData.bpm = bpm;
songData.songDuration = songDuration;
// 提取动态数据
this.game_enterTimes = source.game_enterTimes;
this.time_totalPlayingTime = source.time_totalPlayingTime;
this.current_levelProgress = source.current_levelProgress;
this._if_level_is_EASE = source._if_level_is_EASE;
this.max_comboRecord = source.max_comboRecord;
this.personalRecord = source.personalRecord;
this.enemyList = new List<int>(source.enemyList);
songData.game_enterTimes = game_enterTimes;
songData.time_totalPlayingTime = time_totalPlayingTime;
songData.uploadData = DateTime.Parse(uploadData);
// 将字典转换为 List
this.difficultyNumberList = DictToList(source.difficultyNumberMap);
this.personalRecordList = DictToList(source.personalRecordMap);
this.idolRecordList = DictToList(source.idolRecordMap);
this.chartScoreList = DictToList(source.chartScoreMap);
}
songData.difficultyID = difficultyID;
songData.current_levelProgress = current_levelProgress;
songData._if_level_is_EASE = _if_level_is_EASE;
songData.max_comboRecord = max_comboRecord;
/// <summary>
/// 将序列化的动态数据应用回 SongData SO。
/// </summary>
public void ApplyTo(SongData target)
{
if (target == null) return;
songData.difficultyNumberMap = difficultyNumberMap;
songData.personalRecordMap = personalRecordMap;
songData.idolRecordMap = idolRecordMap;
songData.chartScoreMap = chartScoreMap;
songData.personalRecord = personalRecord; // ֵۺ߷
target.game_enterTimes = this.game_enterTimes;
target.time_totalPlayingTime = this.time_totalPlayingTime;
target.current_levelProgress = this.current_levelProgress;
target._if_level_is_EASE = this._if_level_is_EASE;
target.max_comboRecord = this.max_comboRecord;
target.personalRecord = this.personalRecord;
target.enemyList = new List<int>(this.enemyList);
foreach (var kvp in chartFileMap)
// 将 List 还原回字典
ListToDict(this.difficultyNumberList, target.difficultyNumberMap);
ListToDict(this.personalRecordList, target.personalRecordMap);
ListToDict(this.idolRecordList, target.idolRecordMap);
ListToDict(this.chartScoreList, target.chartScoreMap);
}
private List<IntIntEntry> DictToList(Dictionary<int, int> dict)
{
List<IntIntEntry> list = new List<IntIntEntry>();
if (dict != null)
{
songData.chartFileMap[kvp.Key] = Resources.Load<TextAsset>("Charts/" + kvp.Value);
foreach (var kvp in dict)
{
list.Add(new IntIntEntry { key = kvp.Key, value = kvp.Value });
}
}
return list;
}
private void ListToDict(List<IntIntEntry> list, Dictionary<int, int> dict)
{
if (list == null || dict == null) return;
dict.Clear();
foreach (var entry in list)
{
dict[entry.key] = entry.value;
}
}
}
+10 -10
View File
@@ -20,7 +20,7 @@ public class SongDetailsUI : MonoBehaviour
public Text songSerialNumberText;
public Text personalRecordText;
// 新增:显示谱面(个人)分的文本
// Documentation text normalized.
public Text chartScoreText;
public Text thisSong_progressText;
@@ -30,12 +30,12 @@ public class SongDetailsUI : MonoBehaviour
public int id_of_thisSong;
[Header("查看曲绘大图")]
[Header("Inspector")]
public GameObject picDetail;
public Image profound_image;
public Text detail_informations_text;
[Header("难度选择按钮")]
[Header("Inspector")]
public Button Button_EZ;
public Button Button_HD;
public Button Button_IN;
@@ -47,12 +47,12 @@ public class SongDetailsUI : MonoBehaviour
{
if (SongDataHolder.SelectedSongData != null)
{
// 读取选中的 SongData
// Documentation text normalized.
currentSong = SongDataHolder.SelectedSongData;
this_song_SO = currentSong;
song_bgImage.sprite = currentSong.backgroudPIC;
// 更新 UI 显示
// Documentation text normalized.
songNameText.text = currentSong.songName;
artistNameText.text = currentSong.artistName;
painterNameText.text = currentSong.painter;
@@ -84,12 +84,12 @@ public class SongDetailsUI : MonoBehaviour
}
else
{
// Debug.LogError("SongDataHolder.SelectedSongData 为空!");
// Documentation text normalized.
}
picDetail.SetActive(false);
// 初始化难度按钮
// Documentation text normalized.
if (currentSong != null)
{
SetDifficultyButtons(currentSong.thisLevel_selectedDifficultyID);
@@ -128,8 +128,8 @@ public class SongDetailsUI : MonoBehaviour
difficultyDesciptionText.text = entry.difficultyDescription;
// read values from SongData (SO)
int chartPersonal = currentSong.GetPersonalRecord(difficulty); // 谱面个人分
int idol = currentSong.GetIdolRecord(difficulty); // 偶像分
int chartPersonal = currentSong.GetPersonalRecord(difficulty); // Documentation text normalized.
int idol = currentSong.GetIdolRecord(difficulty); // Documentation text normalized.
// Determine total: prefer stored total if present (>0), otherwise compute chartPersonal + idol
int total;
@@ -147,7 +147,7 @@ public class SongDetailsUI : MonoBehaviour
if (idolRecordText != null)
idolRecordText.text = idol.ToString();
// show chart personal (谱面分) if UI text provided
// Documentation text normalized.
if (chartScoreText != null)
chartScoreText.text = chartPersonal.ToString();
+59 -11
View File
@@ -1,4 +1,4 @@
using UnityEngine;
using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic;
using System.Collections;
@@ -6,23 +6,23 @@ using System.IO;
public class dlcButton : MonoBehaviour
{
[Header("׼")]
[Header("Inspector")]
public Image dlc_profileImgae;
public Text dlcName;
public Text dlcID;
public Text dlcProducer;
public Text dlcPubliushDate;
public Image dlc_nowSelectedImageBoarder;
[Header("")]
[Header("Inspector")]
public Text dlcDescription;
[Header("DLC SO ( Inspector ָ)")]
[Header("Inspector")]
public dlcData dlcDataSO;
[Header("DLC (ʱɽű)")]
[Header("Inspector")]
public List<SongData> dlcContent = new List<SongData>();
[Header("· (/ʱ)")]
[Header("Inspector")]
public string editorSearchFolder = "Assets/Resources/dlc_dlcIndex";
public string runtimeResourcesFolder = "Resources/dlc_dlcIndex";
@@ -139,6 +139,7 @@ public class dlcButton : MonoBehaviour
// fades the border in or out; if immediate, set without coroutine
private Coroutine borderFadeCoroutine = null;
private Coroutine showSongsRetryCoroutine = null;
private void SetSelectedVisual(bool selected, bool immediate = false)
{
if (dlc_nowSelectedImageBoarder == null) return;
@@ -216,15 +217,62 @@ public class dlcButton : MonoBehaviour
// mark selection first
Select();
var s = Object.FindAnyObjectByType<SongSelectUI>();
if (s != null)
if (TryDisplaySongsNow())
{
s.DisplaySongsFromList(dlcContent);
return;
}
else
if (showSongsRetryCoroutine != null)
{
Debug.LogWarning("dlcButton: SongSelectUI not found in scene to display songs");
StopCoroutine(showSongsRetryCoroutine);
}
showSongsRetryCoroutine = StartCoroutine(RetryDisplaySongsWhenUIReady());
}
private SongSelectUI FindSongSelectUI()
{
var ui = Object.FindAnyObjectByType<SongSelectUI>();
if (ui != null)
{
return ui;
}
var all = Object.FindObjectsByType<SongSelectUI>(FindObjectsInactive.Include, FindObjectsSortMode.None);
if (all != null && all.Length > 0)
{
return all[0];
}
return null;
}
private bool TryDisplaySongsNow()
{
var ui = FindSongSelectUI();
if (ui == null)
{
return false;
}
ui.DisplaySongsFromList(dlcContent);
return true;
}
private IEnumerator RetryDisplaySongsWhenUIReady()
{
const int maxRetryFrames = 8;
for (int i = 0; i < maxRetryFrames; i++)
{
yield return null;
if (TryDisplaySongsNow())
{
showSongsRetryCoroutine = null;
yield break;
}
}
showSongsRetryCoroutine = null;
Debug.LogWarning("dlcButton: SongSelectUI not found in scene to display songs");
}
// Try to find a dlcData SO by id and apply its songList to dlcContent
+11 -11
View File
@@ -1,35 +1,35 @@
using System.Collections.Generic;
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(fileName = "NewDLCData", menuName = "EaseOut/dlcData")]
public class dlcData : ScriptableObject
{
[Header("DLC֮ͼ")]
[Header("Inspector")]
public Sprite dlc_image;
[Header("DLC֮ID")]
[Header("Inspector")]
public int dlcID;
[Header("DLC")]
[Header("Inspector")]
public string dlcName;
[Header("DLCƷ")]
[Header("Inspector")]
public string dlcProducer;
[Header("DLCǷѽ")]
[Header("Inspector")]
public bool dlcIsUnlocked;
[Header("DLC")]
[Header("Inspector")]
[TextArea(3,10)]
public string dlcDescription;
[Header("DLC yyyy-mm-dd")]
[Header("Inspector")]
public string dlcPublishDate;
[Header("DLCĿScriptableObjects")]
[Header("Inspector")]
public List<SongData> songList = new List<SongData>();
[Header("DLC")]
[Tooltip("ڽŵıAudioClip")]
[Header("Inspector")]
[Tooltip("Documentation text normalized.")]
public AudioClip settlementMusic;
}
+3 -3
View File
@@ -1,4 +1,4 @@
using UnityEngine;
using UnityEngine;
using UnityEngine.UI;
using System.Collections;
using System.Collections.Generic;
@@ -12,10 +12,10 @@ public class loadDlcListPrefab : MonoBehaviour
public List<dlcData> dlcList = new List<dlcData>();
}
[Header("À¸Ä¿Áбí")]
[Header("Inspector")]
public List<Column> columns = new List<Column>();
[Header("UI Ô¤ÖÆÌå")]
[Header("Inspector")]
public GameObject dlc_scContent;
public GameObject columnPrefab;
public GameObject dlcButtonPrefab;
+514 -15
View File
@@ -1,15 +1,16 @@
using System.Collections.Generic;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
using System.Collections;
using System;
using DG.Tweening;
public class selected_songInfo : MonoBehaviour
{
public Image t_songCoverImage;
public Image btm_songCoverImage;
[Header("Ϣ")]
[Header("Inspector")]
public Text songNameText;
public Text currentDifficulty;
public Text sumScore_thisDifficulty;
@@ -18,26 +19,58 @@ public class selected_songInfo : MonoBehaviour
public Image c_DifficultyImage;
public Image c_imageMask;
[Header("ּɫ")]
[Header("Inspector")]
public Color _0to5d5;
public Color _5d5to11;
public Color _11to16d5;
public Color _16d5to22;
public Color _equal22;
[Header("ӦѶȰť")]
[Header("Inspector")]
public List<Button> difficultyButtons = new List<Button>();
[Header("ӦѶı")]
[Header("Inspector")]
public List<Text> difficultyTexts = new List<Text>();
[Header("enter detail page")]
public Button enterDetailPageButton;
[Header("quick enter game play")]
public Button quickEnter_gamePlay;
[Header("ָimageɫ")]
[Header("Inspector")]
public Sprite buttons_bgImage;
[Header("black mask")]
public Image blackMaskImage;
[Header("Quick Enter Prompt UI")]
[SerializeField] GameObject mustSelectYourIdolRoot;
[SerializeField] GameObject noChoiceRoot;
[SerializeField] GameObject choiceLessThanFiveRoot;
[SerializeField] Button promptContinueButton;
[SerializeField] Button promptConsiderButton;
[SerializeField] Button promptDoNotShowAgainButton;
[SerializeField] Button promptOkayButton;
[SerializeField] float promptFlashOnTime = 0.04f;
[SerializeField] float promptFlashOffTime = 0.03f;
[SerializeField] int promptFlashCount = 2;
[SerializeField] Color doNotShowOffButtonColor = Color.white;
[SerializeField] Color doNotShowOnButtonColor = new Color(0.227f, 0.227f, 0.227f, 1f);
[SerializeField] Color doNotShowOffTextColor = new Color(0.227f, 0.227f, 0.227f, 1f);
[SerializeField] Color doNotShowOnTextColor = Color.white;
private Coroutine _quickEnterPromptRoutine;
private bool _promptAwaitingChoice = false;
private bool _doNotShowAgainPrompt = false;
private bool _doNotShowPrefLoaded = false;
private const string QuickEnterPromptSkipPrefKey = "quickenter_team_warning_skip";
private enum QuickEnterPromptDecision
{
None = 0,
Continue = 1,
Consider = 2
}
private QuickEnterPromptDecision _promptDecision = QuickEnterPromptDecision.None;
// store the original sprites so we don't overwrite the designer's setup
private List<Sprite> originalButtonSprites = new List<Sprite>();
@@ -58,11 +91,133 @@ public class selected_songInfo : MonoBehaviour
private bool songInfo_BaseCached;
private int lastSongId = -1;
[Header("Difficulty ID Roll")]
[SerializeField] string difficultyIdName = "difficultyID";
[SerializeField] string difficultyIdGroundName = "difficultyIDGround";
[SerializeField] float difficultyIdStepDelayFast = 0.03f;
[SerializeField] float difficultyIdStepDelaySlow = 0.11f;
[SerializeField] bool difficultyIdUseUnscaledTime = true;
private readonly List<Text> difficultyIdDisplayTexts = new List<Text>();
private Coroutine difficultyIdRollCoroutine;
private int difficultyIdDisplayed = int.MinValue;
private static void LogVerbose(string message)
{
if (GameConfig.verboseLogs) Debug.LogWarning(message);
}
private void ResolveDifficultyIdDisplayTexts()
{
difficultyIdDisplayTexts.Clear();
Text[] allTexts = Resources.FindObjectsOfTypeAll<Text>();
for (int i = 0; i < allTexts.Length; i++)
{
Text txt = allTexts[i];
if (txt == null) continue;
if (txt.gameObject == null || txt.gameObject.scene != gameObject.scene) continue;
bool nameMatch = string.Equals(txt.name, difficultyIdName, StringComparison.OrdinalIgnoreCase) ||
string.Equals(txt.name, difficultyIdGroundName, StringComparison.OrdinalIgnoreCase);
if (!nameMatch) continue;
txt.raycastTarget = false;
difficultyIdDisplayTexts.Add(txt);
}
}
private void ApplyDifficultyIdDisplayValue(int value)
{
if (difficultyIdDisplayTexts.Count == 0)
ResolveDifficultyIdDisplayTexts();
string text = value.ToString();
for (int i = 0; i < difficultyIdDisplayTexts.Count; i++)
{
Text t = difficultyIdDisplayTexts[i];
if (t == null) continue;
t.text = text;
t.raycastTarget = false;
}
}
private void UpdateDifficultyIdDisplayAnimated(float difficultyLevel, bool instant)
{
int target = Mathf.RoundToInt(difficultyLevel);
if (target < 0) target = 0;
if (instant || difficultyIdDisplayed == int.MinValue)
{
difficultyIdDisplayed = target;
ApplyDifficultyIdDisplayValue(target);
return;
}
if (target == difficultyIdDisplayed)
{
ApplyDifficultyIdDisplayValue(target);
return;
}
if (difficultyIdRollCoroutine != null)
{
StopCoroutine(difficultyIdRollCoroutine);
difficultyIdRollCoroutine = null;
}
difficultyIdRollCoroutine = StartCoroutine(RollDifficultyIdDisplay(difficultyIdDisplayed, target));
}
private IEnumerator RollDifficultyIdDisplay(int from, int target)
{
int distance = Mathf.Abs(target - from);
if (distance == 0)
{
ApplyDifficultyIdDisplayValue(target);
difficultyIdDisplayed = target;
difficultyIdRollCoroutine = null;
yield break;
}
int dir = target > from ? 1 : -1;
int current = from;
for (int i = 1; i <= distance; i++)
{
current += dir;
difficultyIdDisplayed = current;
ApplyDifficultyIdDisplayValue(current);
float progress = distance <= 1 ? 1f : (float)i / distance;
float eased = progress * progress; // fast at start, slower near final value
float delay = Mathf.Lerp(difficultyIdStepDelayFast, difficultyIdStepDelaySlow, eased);
if (difficultyIdUseUnscaledTime)
yield return new WaitForSecondsRealtime(Mathf.Max(0.005f, delay));
else
yield return new WaitForSeconds(Mathf.Max(0.005f, delay));
}
difficultyIdDisplayed = target;
ApplyDifficultyIdDisplayValue(target);
difficultyIdRollCoroutine = null;
}
private int GetTeamMemberCount()
{
int id1 = PlayerPrefs.GetInt("selected_heroSlot01_heroID", 0);
int id2 = PlayerPrefs.GetInt("selected_heroSlot02_heroID", 0);
int id3 = PlayerPrefs.GetInt("selected_heroSlot03_heroID", 0);
int id4 = PlayerPrefs.GetInt("selected_heroSlot04_heroID", 0);
int id5 = PlayerPrefs.GetInt("selected_heroSlot05_heroID", 0);
int count = 0;
if (id1 != 0) count++;
if (id2 != 0) count++;
if (id3 != 0) count++;
if (id4 != 0) count++;
if (id5 != 0) count++;
return count;
}
void Awake()
{
LogVerbose("selected_songInfo.Awake called");
@@ -71,6 +226,12 @@ public class selected_songInfo : MonoBehaviour
void OnEnable()
{
LogVerbose("selected_songInfo.OnEnable called");
EnsurePromptReferences();
LoadDoNotShowAgainPreference();
SetPromptRootVisible(false);
ApplyDoNotShowAgainVisual();
ResolveDifficultyIdDisplayTexts();
// register listeners here to ensure binding even if Start wasn't called yet
if (enterDetailPageButton != null)
{
@@ -93,6 +254,8 @@ public class selected_songInfo : MonoBehaviour
{
LogVerbose("quickEnter_gamePlay is null in OnEnable");
}
BindPromptButtons();
}
void OnDisable()
@@ -106,6 +269,20 @@ public class selected_songInfo : MonoBehaviour
{
quickEnter_gamePlay.onClick.RemoveListener(OnQuickEnterClicked);
}
UnbindPromptButtons();
_promptAwaitingChoice = false;
_promptDecision = QuickEnterPromptDecision.None;
if (_quickEnterPromptRoutine != null)
{
StopCoroutine(_quickEnterPromptRoutine);
_quickEnterPromptRoutine = null;
}
if (difficultyIdRollCoroutine != null)
{
StopCoroutine(difficultyIdRollCoroutine);
difficultyIdRollCoroutine = null;
}
}
void OnDestroy()
@@ -119,6 +296,12 @@ public class selected_songInfo : MonoBehaviour
{
LogVerbose("selected_songInfo.Start called");
EnsurePromptReferences();
LoadDoNotShowAgainPreference();
SetPromptRootVisible(false);
ApplyDoNotShowAgainVisual();
ResolveDifficultyIdDisplayTexts();
// ensure black mask is transparent and inactive at start
if (blackMaskImage != null)
{
@@ -201,6 +384,7 @@ public class selected_songInfo : MonoBehaviour
{
currentDifficulty.text = difficultyLevel.ToString("0.0");
}
UpdateDifficultyIdDisplayAnimated(difficultyLevel, false);
if (c_DifficultyImage != null)
{
@@ -311,7 +495,7 @@ public class selected_songInfo : MonoBehaviour
}
if (songNameText != null)
{
songNameText.text = "δѡ";
songNameText.text = "未选择歌曲";
}
// restore original difficulty button backgrounds and text colors
@@ -319,6 +503,7 @@ public class selected_songInfo : MonoBehaviour
if (sumScore_thisDifficulty != null) sumScore_thisDifficulty.text = "0";
if (total_gameTime != null) total_gameTime.text = "0";
UpdateDifficultyIdDisplayAnimated(0f, true);
lastSongId = -1;
}
}
@@ -505,7 +690,255 @@ public class selected_songInfo : MonoBehaviour
}
}
private void LoadDoNotShowAgainPreference()
{
if (_doNotShowPrefLoaded) return;
_doNotShowAgainPrompt = PlayerPrefs.GetInt(QuickEnterPromptSkipPrefKey, 0) == 1;
_doNotShowPrefLoaded = true;
}
private void SetDoNotShowAgainPreference(bool enabled)
{
_doNotShowAgainPrompt = enabled;
_doNotShowPrefLoaded = true;
PlayerPrefs.SetInt(QuickEnterPromptSkipPrefKey, enabled ? 1 : 0);
PlayerPrefs.Save();
ApplyDoNotShowAgainVisual();
}
private void EnsurePromptReferences()
{
if (mustSelectYourIdolRoot == null)
{
mustSelectYourIdolRoot = FindSceneObjectByExactName("\uFF01mustselectyouridol");
if (mustSelectYourIdolRoot == null)
mustSelectYourIdolRoot = FindSceneObjectByContainsName("mustselectyouridol");
}
if (mustSelectYourIdolRoot != null)
{
if (noChoiceRoot == null)
noChoiceRoot = FindChildByName(mustSelectYourIdolRoot.transform, "No choice");
if (choiceLessThanFiveRoot == null)
choiceLessThanFiveRoot = FindChildByName(mustSelectYourIdolRoot.transform, "choice < 5");
if (promptContinueButton == null)
promptContinueButton = FindButtonByName(mustSelectYourIdolRoot.transform, "Continue");
if (promptConsiderButton == null)
promptConsiderButton = FindButtonByName(mustSelectYourIdolRoot.transform, "Consider");
if (promptDoNotShowAgainButton == null)
promptDoNotShowAgainButton = FindButtonByName(mustSelectYourIdolRoot.transform, "Do not show again");
if (promptOkayButton == null)
promptOkayButton = FindButtonByName(mustSelectYourIdolRoot.transform, "okay");
}
}
private void BindPromptButtons()
{
EnsurePromptReferences();
if (promptContinueButton != null)
{
promptContinueButton.onClick.RemoveListener(OnPromptContinueClicked);
promptContinueButton.onClick.AddListener(OnPromptContinueClicked);
}
if (promptConsiderButton != null)
{
promptConsiderButton.onClick.RemoveListener(OnPromptConsiderClicked);
promptConsiderButton.onClick.AddListener(OnPromptConsiderClicked);
}
if (promptDoNotShowAgainButton != null)
{
promptDoNotShowAgainButton.onClick.RemoveListener(OnPromptDoNotShowAgainClicked);
promptDoNotShowAgainButton.onClick.AddListener(OnPromptDoNotShowAgainClicked);
}
}
private void UnbindPromptButtons()
{
if (promptContinueButton != null)
promptContinueButton.onClick.RemoveListener(OnPromptContinueClicked);
if (promptConsiderButton != null)
promptConsiderButton.onClick.RemoveListener(OnPromptConsiderClicked);
if (promptDoNotShowAgainButton != null)
promptDoNotShowAgainButton.onClick.RemoveListener(OnPromptDoNotShowAgainClicked);
}
private void OnPromptContinueClicked()
{
if (!_promptAwaitingChoice) return;
_promptDecision = QuickEnterPromptDecision.Continue;
}
private void OnPromptConsiderClicked()
{
if (!_promptAwaitingChoice) return;
_promptDecision = QuickEnterPromptDecision.Consider;
}
private void OnPromptDoNotShowAgainClicked()
{
LoadDoNotShowAgainPreference();
SetDoNotShowAgainPreference(!_doNotShowAgainPrompt);
}
private void ApplyDoNotShowAgainVisual()
{
if (promptDoNotShowAgainButton == null) return;
Graphic g = promptDoNotShowAgainButton.targetGraphic;
if (g != null)
{
g.color = _doNotShowAgainPrompt ? doNotShowOnButtonColor : doNotShowOffButtonColor;
}
Text[] texts = promptDoNotShowAgainButton.GetComponentsInChildren<Text>(true);
Color textColor = _doNotShowAgainPrompt ? doNotShowOnTextColor : doNotShowOffTextColor;
for (int i = 0; i < texts.Length; i++)
{
if (texts[i] != null)
texts[i].color = textColor;
}
}
private void SetPromptRootVisible(bool visible)
{
if (mustSelectYourIdolRoot == null) return;
mustSelectYourIdolRoot.SetActive(visible);
if (!visible)
{
SetPromptCaseState(showNoChoice: false, showChoiceLessThanFive: false);
SetPromptButtonsVisible(false);
}
}
private void SetPromptCaseState(bool showNoChoice, bool showChoiceLessThanFive)
{
if (noChoiceRoot != null) noChoiceRoot.SetActive(showNoChoice);
if (choiceLessThanFiveRoot != null) choiceLessThanFiveRoot.SetActive(showChoiceLessThanFive);
}
private void SetPromptButtonsVisible(bool visible)
{
if (promptContinueButton != null) promptContinueButton.gameObject.SetActive(visible);
if (promptConsiderButton != null) promptConsiderButton.gameObject.SetActive(visible);
if (promptDoNotShowAgainButton != null) promptDoNotShowAgainButton.gameObject.SetActive(visible);
if (promptOkayButton != null) promptOkayButton.gameObject.SetActive(false);
}
private IEnumerator FlashShowObject(GameObject go)
{
if (go == null) yield break;
go.SetActive(true);
CanvasGroup cg = go.GetComponent<CanvasGroup>();
if (cg == null) cg = go.AddComponent<CanvasGroup>();
int flashes = Mathf.Max(1, promptFlashCount);
// Force high-speed flashing even if inspector values are larger.
float onTime = Mathf.Clamp(promptFlashOnTime, 0.01f, 0.05f);
float offTime = Mathf.Clamp(promptFlashOffTime, 0.01f, 0.04f);
cg.alpha = 0f;
for (int i = 0; i < flashes; i++)
{
cg.alpha = 1f;
yield return new WaitForSecondsRealtime(onTime);
if (i < flashes - 1)
{
cg.alpha = 0f;
yield return new WaitForSecondsRealtime(offTime);
}
}
cg.alpha = 1f;
}
private GameObject FindSceneObjectByExactName(string objectName)
{
if (string.IsNullOrEmpty(objectName)) return null;
Transform[] all = Resources.FindObjectsOfTypeAll<Transform>();
for (int i = 0; i < all.Length; i++)
{
Transform t = all[i];
if (t == null || !t.gameObject.scene.IsValid()) continue;
if (string.Equals(t.name, objectName, StringComparison.OrdinalIgnoreCase))
return t.gameObject;
}
return null;
}
private GameObject FindSceneObjectByContainsName(string token)
{
if (string.IsNullOrEmpty(token)) return null;
Transform[] all = Resources.FindObjectsOfTypeAll<Transform>();
for (int i = 0; i < all.Length; i++)
{
Transform t = all[i];
if (t == null || !t.gameObject.scene.IsValid()) continue;
if (t.name.IndexOf(token, StringComparison.OrdinalIgnoreCase) >= 0)
return t.gameObject;
}
return null;
}
private GameObject FindChildByName(Transform root, string childName)
{
if (root == null || string.IsNullOrEmpty(childName)) return null;
// Prefer direct children first.
for (int i = 0; i < root.childCount; i++)
{
Transform c = root.GetChild(i);
if (c != null && string.Equals(c.name, childName, StringComparison.OrdinalIgnoreCase))
return c.gameObject;
}
// Fallback: any descendant.
Transform[] all = root.GetComponentsInChildren<Transform>(true);
for (int i = 0; i < all.Length; i++)
{
Transform c = all[i];
if (c != null && string.Equals(c.name, childName, StringComparison.OrdinalIgnoreCase))
return c.gameObject;
}
return null;
}
private Button FindButtonByName(Transform root, string buttonName)
{
if (root == null || string.IsNullOrEmpty(buttonName)) return null;
Button[] btns = root.GetComponentsInChildren<Button>(true);
for (int i = 0; i < btns.Length; i++)
{
Button b = btns[i];
if (b != null && string.Equals(b.gameObject.name, buttonName, StringComparison.OrdinalIgnoreCase))
return b;
}
return null;
}
private void OnQuickEnterClicked()
{
int teamCount = GetTeamMemberCount();
bool needTeamWarning = teamCount < 5;
if (needTeamWarning)
{
LoadDoNotShowAgainPreference();
if (!_doNotShowAgainPrompt)
{
ShowQuickEnterPromptForTeam(teamCount);
return;
}
}
BeginQuickEnterInternal();
}
private void BeginQuickEnterInternal()
{
// use SongData from holder or SongButton; ensure selection exists
SongData sd = FindSongDataFromSelectedButton();
@@ -538,6 +971,72 @@ public class selected_songInfo : MonoBehaviour
StartCoroutine(QuickEnterSequence());
}
private void ShowQuickEnterPromptForTeam(int teamCount)
{
EnsurePromptReferences();
LoadDoNotShowAgainPreference();
ApplyDoNotShowAgainVisual();
if (mustSelectYourIdolRoot == null)
{
// If UI is missing, fallback to direct quick enter to avoid dead-end.
BeginQuickEnterInternal();
return;
}
if (_quickEnterPromptRoutine != null)
{
StopCoroutine(_quickEnterPromptRoutine);
_quickEnterPromptRoutine = null;
}
_quickEnterPromptRoutine = StartCoroutine(QuickEnterPromptRoutine(teamCount));
}
private IEnumerator QuickEnterPromptRoutine(int teamCount)
{
_promptAwaitingChoice = false;
_promptDecision = QuickEnterPromptDecision.None;
SetPromptRootVisible(true);
SetPromptCaseState(showNoChoice: false, showChoiceLessThanFive: false);
SetPromptButtonsVisible(false);
ApplyDoNotShowAgainVisual();
yield return FlashShowObject(mustSelectYourIdolRoot);
bool isTeamEmpty = teamCount <= 0;
if (isTeamEmpty)
{
SetPromptCaseState(showNoChoice: true, showChoiceLessThanFive: false);
yield return FlashShowObject(noChoiceRoot);
}
else
{
SetPromptCaseState(showNoChoice: false, showChoiceLessThanFive: true);
yield return FlashShowObject(choiceLessThanFiveRoot);
}
// Buttons are displayed directly without flashing.
SetPromptButtonsVisible(true);
_promptAwaitingChoice = true;
while (_promptDecision == QuickEnterPromptDecision.None)
yield return null;
_promptAwaitingChoice = false;
QuickEnterPromptDecision decision = _promptDecision;
_promptDecision = QuickEnterPromptDecision.None;
SetPromptRootVisible(false);
_quickEnterPromptRoutine = null;
if (decision == QuickEnterPromptDecision.Continue)
{
BeginQuickEnterInternal();
}
}
private IEnumerator QuickEnterSequence()
{
LogVerbose("QuickEnterSequence started: beginning fade");
@@ -598,7 +1097,7 @@ public class selected_songInfo : MonoBehaviour
private SongData FindSongDataFromSelectedButton()
{
var allButtons = Object.FindObjectsByType<SongButton>(FindObjectsInactive.Exclude, FindObjectsSortMode.InstanceID);
var allButtons = UnityEngine.Object.FindObjectsByType<SongButton>(FindObjectsInactive.Exclude, FindObjectsSortMode.InstanceID);
LogVerbose($"FindSongDataFromSelectedButton: found {allButtons.Length} SongButton instances");
foreach (var sb in allButtons)
{
@@ -694,8 +1193,8 @@ public class selected_songInfo : MonoBehaviour
}
// find managers in the new scene
var beatmapManager = Object.FindAnyObjectByType<BeatmapManager>();
var gameManager = Object.FindAnyObjectByType<GameManager>();
var beatmapManager = UnityEngine.Object.FindAnyObjectByType<BeatmapManager>();
var gameManager = UnityEngine.Object.FindAnyObjectByType<GameManager>();
LogVerbose($"selected song: {selected.songName} (id {selected.songID}), difficulty {selected.thisLevel_selectedDifficultyID}");
@@ -809,7 +1308,7 @@ public class selected_songInfo : MonoBehaviour
// Start coroutine to wait for player input (Space) to begin playback and spawning
// Use a safe host for coroutine in case this MonoBehaviour is destroyed unexpectedly
var pauseMgr = PauseManager.Instance != null ? PauseManager.Instance : Object.FindAnyObjectByType<PauseManager>();
var pauseMgr = PauseManager.Instance != null ? PauseManager.Instance : UnityEngine.Object.FindAnyObjectByType<PauseManager>();
MonoBehaviour coroutineHost = (pauseMgr as MonoBehaviour) ?? (gameManager as MonoBehaviour) ?? (beatmapManager as MonoBehaviour) ?? this;
if (coroutineHost != null)
{
@@ -859,7 +1358,7 @@ public class selected_songInfo : MonoBehaviour
}
// Prewarm particle systems via AnimationController to avoid first-play hitch
var anim = AnimationController.Global != null ? AnimationController.Global : Object.FindAnyObjectByType<AnimationController>();
var anim = AnimationController.Global != null ? AnimationController.Global : UnityEngine.Object.FindAnyObjectByType<AnimationController>();
if (anim != null)
{
LogVerbose("Starting AnimationController particle prewarm (will wait until complete)...");
@@ -869,7 +1368,7 @@ public class selected_songInfo : MonoBehaviour
}
// Unpause via PauseManager
var pauseMgr = PauseManager.Instance != null ? PauseManager.Instance : Object.FindAnyObjectByType<PauseManager>();
var pauseMgr = PauseManager.Instance != null ? PauseManager.Instance : UnityEngine.Object.FindAnyObjectByType<PauseManager>();
if (pauseMgr != null) pauseMgr.Pause(false);
// small buffer to avoid hitching immediately after unpause: wait configured delay from GameManager
@@ -929,7 +1428,7 @@ public class selected_songInfo : MonoBehaviour
Debug.LogWarning($"enterDetailPageButton assigned={(enterDetailPageButton==null?"NULL":"ASSIGNED")}");
Debug.LogWarning($"blackMaskImage assigned={(blackMaskImage==null?"NULL":"ASSIGNED")}");
Debug.LogWarning($"SongDataHolder.SelectedSongData={(SongDataHolder.SelectedSongData==null?"NULL":SongDataHolder.SelectedSongData.songName)}");
var allButtons = Object.FindObjectsByType<SongButton>(FindObjectsInactive.Exclude, FindObjectsSortMode.InstanceID);
var allButtons = UnityEngine.Object.FindObjectsByType<SongButton>(FindObjectsInactive.Exclude, FindObjectsSortMode.InstanceID);
Debug.LogWarning($"Found {allButtons.Length} SongButton instances in scene");
for (int i = 0; i < allButtons.Length; i++)
{
@@ -945,7 +1444,7 @@ public class selected_songInfo : MonoBehaviour
private void RequestPauseManagerPause()
{
if (pauseRequested) return;
var pm = PauseManager.Instance != null ? PauseManager.Instance : Object.FindAnyObjectByType<PauseManager>();
var pm = PauseManager.Instance != null ? PauseManager.Instance : UnityEngine.Object.FindAnyObjectByType<PauseManager>();
if (pm != null)
{
pm.Pause(true);