超大量的更新 修复很多问题,gameplay特效初步
This commit is contained in:
+267
-190
@@ -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,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:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user