using UnityEngine; using UnityEngine.UI; using TMPro; using System.Collections.Generic; using System.Collections; #if UNITY_EDITOR using UnityEditor; #endif public class teamUIController : MonoBehaviour { public static teamUIController Instance { get; private set; } // --- 新增:队伍槽 ID 列表(从上到下 5 个友军) --- [Header("Runtime ally configuration")] [Tooltip("IDs for the 5 ally slots (top to bottom). These are used by PopulateAllySOsFromIds to resolve SOs into currentAllySOs.")] public List allySlotIds = new List { 0, 0, 0, 0, 0 }; // --- 新增:敌人队列槽 ID 列表(按出场顺序) --- [Header("Runtime enemy configuration")] [Tooltip("IDs for enemies to appear in sequence. All enemies share a single GameObject and will be initialized from these SOs in order.")] public List enemySlotIds = new List(); // --- 新增:解析后的当前五个 SO 引用(按顺序) --- private TeamCharacterDataInfo[] currentAllySOs = new TeamCharacterDataInfo[5]; // 新增:解析后的当前敌人 SO 列表(按出场顺序) private EnemyData_SO[] currentEnemySOs = new EnemyData_SO[0]; // 新增:仅包含已解析到的、有效的敌人 SO(不包含 id==0 或未找到的项) private EnemyData_SO[] recognizedEnemySOs = new EnemyData_SO[0]; // previous active flags for detecting external changes private bool[] prevAllyActive = new bool[5]; // 两个路径字段:一个用于编辑器查找(可以填写 Assets/Resources/so/ally 或磁盘绝对路径), // 一个用于运行时代码查找(Resources 相对路径,例如 so/ally) [Header("SO folder paths")] [Tooltip("Editor-only: project path or absolute disk path to the folder that contains SOs. Example: Assets/Resources/so/ally")] public string editorSOFolderPath = "Assets/Resources/so/ally"; [Tooltip("Runtime: Resources subfolder path (no 'Resources/' prefix). Example: so/ally")] public string runtimeResourcesFolderPath = "so/ally"; // 新增:敌人 SO 路径(Editor / Runtime) [Tooltip("Editor-only: project path or absolute disk path to the folder that contains enemy SOs. Example: Assets/Resources/so/enemies")] public string editorEnemySOFolderPath = "Assets/Resources/so/enemies"; [Tooltip("Runtime: Resources subfolder path (no 'Resources/' prefix) for enemy SOs. Example: so/enemies")] public string runtimeEnemyResourcesFolderPath = "so/enemies"; // Optional per-ally object to toggle together with isAllyX_active. If null falls back to objectFather_allyXX. [Header("Ally runtime objects (optional)")] public GameObject ally01_object; public GameObject ally02_object; public GameObject ally03_object; public GameObject ally04_object; public GameObject ally05_object; // 兼容旧字段(可选) [Header("Editor folder load (Editor only)")] [Tooltip("Deprecated: kept for compatibility. Use editorSOFolderPath or runtimeResourcesFolderPath instead.")] public string selectedProjectFolderPath = string.Empty; /// /// 在运行/编辑器中调用:根据 allySlotIds(5 个 int)在指定文件夹中搜索对应的 TeamCharacterDataInfo 资产。 /// Editor: 使用 editorSOFolderPath / selectedProjectFolderPath;Runtime: 使用 runtimeResourcesFolderPath / selectedProjectFolderPath。 /// public void PopulateAllySOsFromIds() { if (allySlotIds == null || allySlotIds.Count < 5) { Debug.LogWarning("allySlotIds 未正确设置,自动填充为 5 个占位 ID"); allySlotIds = new List { 0, 0, 0, 0, 0 }; } #if UNITY_EDITOR // Choose editor folder path preferentially from editorSOFolderPath, fallback to selectedProjectFolderPath string folderToUse = !string.IsNullOrEmpty(editorSOFolderPath) ? editorSOFolderPath : selectedProjectFolderPath; // Ensure folderToUse points inside the project Assets folder when possible string projectRelative = null; if (!string.IsNullOrEmpty(folderToUse)) { string projectPath = folderToUse.Replace("\\", "/"); if (projectPath.StartsWith(Application.dataPath)) { projectRelative = "Assets" + projectPath.Substring(Application.dataPath.Length); } else if (projectPath.StartsWith("Assets/")) { projectRelative = projectPath; } else { Debug.LogWarning("editorSOFolderPath 必须位于项目的 Assets 文件夹下以便进行查询(建议填写 Assets/Resources/so/ally)。回退到全局列表查找。路径也可填写为磁盘绝对路径。"); } } // Collect TeamCharacterDataInfo and AllyHero_SO assets under the folder (or project-wide) List discovered = new List(); if (!string.IsNullOrEmpty(projectRelative)) { string[] guids = AssetDatabase.FindAssets("t:TeamCharacterDataInfo", new[] { projectRelative }); foreach (var g in guids) { string assetPath = AssetDatabase.GUIDToAssetPath(g); var so = AssetDatabase.LoadAssetAtPath(assetPath); if (so != null) discovered.Add(so); } string[] guids2 = AssetDatabase.FindAssets("t:AllyHero_SO", new[] { projectRelative }); foreach (var g in guids2) { string assetPath = AssetDatabase.GUIDToAssetPath(g); var so = AssetDatabase.LoadAssetAtPath(assetPath); if (so != null) discovered.Add(so); } } else { // no valid folder chosen -> search entire project string[] guids = AssetDatabase.FindAssets("t:TeamCharacterDataInfo"); foreach (var g in guids) { string assetPath = AssetDatabase.GUIDToAssetPath(g); var so = AssetDatabase.LoadAssetAtPath(assetPath); if (so != null) discovered.Add(so); } string[] guids2 = AssetDatabase.FindAssets("t:AllyHero_SO"); foreach (var g in guids2) { string assetPath = AssetDatabase.GUIDToAssetPath(g); var so = AssetDatabase.LoadAssetAtPath(assetPath); if (so != null) discovered.Add(so); } } TeamCharacterList globalList = null; if (teamSettingPanel.Instance != null) globalList = teamSettingPanel.Instance.teamCharacterList; for (int i = 0; i < 5; i++) { int id = allySlotIds[i]; TeamCharacterDataInfo found = null; // 如果 slot ID 为 0 -> 关闭对应 ally UI 并跳过 if (id == 0) { currentAllySOs[i] = null; switch (i) { case 0: isAlly01_active = false; break; case 1: isAlly02_active = false; break; case 2: isAlly03_active = false; break; case 3: isAlly04_active = false; break; case 4: isAlly05_active = false; break; } ToggleAllyObject(i, false); Debug.Log($"[teamUIController] slot {i+1} id=0 -> disabled"); continue; } // 只要 asset 的路径或名称包含 id 即可引用(Editor) for (int j = 0; j < discovered.Count; j++) { var obj = discovered[j]; if (obj == null) continue; string path = AssetDatabase.GetAssetPath(obj); if (!string.IsNullOrEmpty(path) && path.Contains(id.ToString())) { // if it's already TeamCharacterDataInfo, use it if (obj is TeamCharacterDataInfo tc) { found = tc; Debug.Log($"[teamUIController] (Editor) matched asset by path: {path} for id={id} slot={i+1}"); break; } // if it's AllyHero_SO, map to a runtime TeamCharacterDataInfo instance if (obj is AllyHero_SO ah) { var mapped = ScriptableObject.CreateInstance(); mapped.CharacterID = ah.ally_heroID; mapped.CharacterName = ah.ally_heroName; mapped.CharacterCardSprite = ah.ally_heroImage; mapped.CharacterTeamSprite = ah.ally_heroProfile; mapped.name = ah.name; found = mapped; Debug.Log($"[teamUIController] (Editor) matched AllyHero_SO by path: {path} for id={id} slot={i+1}"); break; } } } // 回退到全局 teamCharacterList if (found == null && globalList != null && globalList.characters != null) { for (int j = 0; j < globalList.characters.Count; j++) { var so = globalList.characters[j]; if (so == null) continue; // globalList 中也以 name 包含 id 为准 if (!string.IsNullOrEmpty(so.name) && so.name.Contains(id.ToString())) { found = so; break; } } } currentAllySOs[i] = found; if (found == null) { Debug.LogWarning($"[teamUIController] 未找到 id={id} 对应的 SO (slot {i + 1})"); // disable UI slot when not found switch (i) { case 0: isAlly01_active = false; break; case 1: isAlly02_active = false; break; case 2: isAlly03_active = false; break; case 3: isAlly04_active = false; break; case 4: isAlly05_active = false; break; } ToggleAllyObject(i, false); } else { Debug.Log($"[teamUIController] (Editor) slot {i + 1} resolved to SO: {found.name} (id={found.CharacterID})"); // 确保对应 UI slot 被开启 switch (i) { case 0: isAlly01_active = true; break; case 1: isAlly02_active = true; break; case 2: isAlly03_active = true; break; case 3: isAlly04_active = true; break; case 4: isAlly05_active = true; break; } ToggleAllyObject(i, true); DebugCharacterSO(found, i+1, true); // Also attempt to set the teammate character image from an underlying AllyHero_SO if available try { // first try to find AllyHero_SO by the same id var ahList = Resources.LoadAll(""); AllyHero_SO matched = null; foreach (var a in ahList) if (a != null && a.ally_heroID == id) { matched = a; break; } if (matched != null) { var sprite = matched.ally_heroProfile != null ? matched.ally_heroProfile : matched.ally_heroImage; SetTeammateCharacterImage(i, sprite); } else { // fallback: if current TeamCharacterDataInfo has a TeamSprite, use that var tc = found as TeamCharacterDataInfo; if (tc != null && tc.CharacterTeamSprite != null) SetTeammateCharacterImage(i, tc.CharacterTeamSprite); } } catch { } } } #else // Runtime: prefer runtimeResourcesFolderPath, fallback to selectedProjectFolderPath string resourcesPath = ResolveResourcesRelativePath(!string.IsNullOrEmpty(runtimeResourcesFolderPath) ? runtimeResourcesFolderPath : selectedProjectFolderPath); List runtimeDiscovered = new List(); if (!string.IsNullOrEmpty(resourcesPath)) { resourcesPath = resourcesPath.Trim('/'); try { var arr = Resources.LoadAll(resourcesPath); if (arr != null && arr.Length > 0) runtimeDiscovered.AddRange(arr); // If nothing loaded from the specific folder, fallback to loading all and filter by name later if (runtimeDiscovered.Count == 0) { var arrAll = Resources.LoadAll(""); if (arrAll != null && arr.Length > 0) { runtimeDiscovered.AddRange(arrAll); Debug.LogWarning($"[teamUIController] (Runtime) Resources.LoadAll returned 0 in '{resourcesPath}', fell back to loading all ScriptableObjects ({arrAll.Length})"); } } Debug.Log($"[teamUIcontroller] (Runtime) Loaded {runtimeDiscovered.Count} objects from Resources/{resourcesPath}"); } catch (System.Exception ex) { Debug.LogWarning($"[teamUIController] (Runtime) Resources.LoadAll failed for '{resourcesPath}': {ex.Message}"); var arrAll = Resources.LoadAll(""); if (arrAll != null && arr.Length > 0) runtimeDiscovered.AddRange(arrAll); } } else { var arr = Resources.LoadAll(""); if (arr != null && arr.Length > 0) runtimeDiscovered.AddRange(arr); Debug.Log($"[teamUIController] (Runtime) Loaded {arr?.Length ?? 0} objects from Resources (project-wide)"); } TeamCharacterList globalList = null; if (teamSettingPanel.Instance != null) globalList = teamSettingPanel.Instance.teamCharacterList; for (int i = 0; i < 5; i++) { int id = allySlotIds[i]; TeamCharacterDataInfo found = null; // 如果 slot ID 为 0 -> 关闭对应 ally UI 并跳过 if (id == 0) { currentAllySOs[i] = null; switch (i) { case 0: isAlly01_active = false; break; case 1: isAlly02_active = false; break; case 2: isAlly03_active = false; break; case 3: isAlly04_active = false; break; case 4: isAlly05_active = false; break; } // also toggle associated object ToggleAllyObject(i, false); Debug.Log($"[teamUIController] (Runtime) slot {i+1} id=0 -> disabled"); continue; } // 只要资源名包含 id 即可引用(Runtime)。支持 TeamCharacterDataInfo 与 AllyHero_SO string runtimeSourceInfo = string.Empty; for (int j = 0; j < runtimeDiscovered.Count; j++) { var obj = runtimeDiscovered[j]; if (obj == null) continue; // name match first if (!string.IsNullOrEmpty(obj.name) && obj.name.Contains(id.ToString())) { if (obj is TeamCharacterDataInfo tc) { found = tc; break; } if (obj is AllyHero_SO ah) { var mapped = ScriptableObject.CreateInstance(); mapped.CharacterID = ah.ally_heroID; mapped.CharacterName = ah.ally_heroName; mapped.CharacterCardSprite = ah.ally_heroImage; mapped.CharacterTeamSprite = ah.ally_heroProfile; found = mapped; break; } } // fallback: if object is TeamCharacterDataInfo, check its CharacterID if (obj is TeamCharacterDataInfo tco && tco.CharacterID == id) { found = tco; break; } // fallback: AllyHero_SO CharacterID equivalent if (obj is AllyHero_SO ah2 && ah2.ally_heroID == id) { var mapped2 = ScriptableObject.CreateInstance(); mapped2.CharacterID = ah2.ally_heroID; mapped2.CharacterName = ah2.ally_heroName; mapped2.CharacterCardSprite = ah2.ally_heroImage; mapped2.CharacterTeamSprite = ah2.ally_heroProfile; found = mapped2; break; } } // 回退到全局 teamCharacterList if (found == null && globalList != null && globalList.characters != null) { for (int j = 0; j < globalList.characters.Count; j++) { var so = globalList.characters[j]; if (so == null) continue; if (!string.IsNullOrEmpty(so.name) && so.name.Contains(id.ToString())) { found = so; break; } if (so.CharacterID == id) { found = so; break; } } } // final fallback: try loading by resource path/id if resourcesPath provided if (found == null && !string.IsNullOrEmpty(resourcesPath)) { try { var byId = Resources.Load($"{resourcesPath}/{id}"); if (byId != null) found = byId; if (found != null) runtimeSourceInfo = $"Resources.Load: {resourcesPath}/{id}"; } catch { } } currentAllySOs[i] = found; if (found == null) { Debug.LogWarning($"[teamUIController] (Runtime) 未找到 id={id} 对应的 SO (slot {i + 1})"); switch (i) { case 0: isAlly01_active = false; break; case 1: isAlly02_active = false; break; case 2: isAlly03_active = false; break; case 3: isAlly04_active = false; break; case 4: isAlly05_active = false; break; } ToggleAllyObject(i, false); } else { // try to get a readable source for the matched object if (string.IsNullOrEmpty(runtimeSourceInfo)) { runtimeSourceInfo = found.name + " (mapped)"; } Debug.Log($"[teamUIcontroller] (Runtime) slot {i + 1} resolved to SO: {found.name} (id={found.CharacterID}) Source={runtimeSourceInfo}"); switch (i) { case 0: isAlly01_active = true; break; case 1: isAlly02_active = true; break; case 2: isAlly03_active = true; break; case 3: isAlly04_active = true; break; case 4: isAlly05_active = true; break; } ToggleAllyObject(i, true); DebugCharacterSO(found, i+1, false); // attempt to set teammate image from underlying AllyHero_SO if possible try { var ahList = Resources.LoadAll(""); AllyHero_SO matched = null; foreach (var a in ahList) if (a != null && a.ally_heroID == id) { matched = a; break; } if (matched != null) { var sprite = matched.ally_heroProfile != null ? matched.ally_heroProfile : matched.ally_heroImage; SetTeammateCharacterImage(i, sprite); } else { var tc = found as TeamCharacterDataInfo; if (tc != null && tc.CharacterTeamSprite != null) SetTeammateCharacterImage(i, tc.CharacterTeamSprite); } } catch { } } } #endif } // Populate enemy SOs from enemySlotIds using editor/runtime paths similar to allies public void PopulateEnemySOsFromIds() { if (enemySlotIds == null || enemySlotIds.Count == 0) { currentEnemySOs = new EnemyData_SO[0]; // update recognized list and UI FilterRecognizedEnemies(); UpdateEnemyListText(); return; } #if UNITY_EDITOR string folderToUse = !string.IsNullOrEmpty(editorEnemySOFolderPath) ? editorEnemySOFolderPath : selectedProjectFolderPath; string projectRelative = null; if (!string.IsNullOrEmpty(folderToUse)) { string projectPath = folderToUse.Replace("\\", "/"); if (projectPath.StartsWith(Application.dataPath)) projectRelative = "Assets" + projectPath.Substring(Application.dataPath.Length); else if (projectPath.StartsWith("Assets/")) projectRelative = projectPath; else Debug.LogWarning("editorEnemySOFolderPath should be inside project Assets for editor search. Falling back to project-wide search."); } List results = new List(); List discovered = new List(); if (!string.IsNullOrEmpty(projectRelative)) { string[] guids = AssetDatabase.FindAssets("t:EnemyData_SO", new[] { projectRelative }); foreach (var g in guids) { var path = AssetDatabase.GUIDToAssetPath(g); var so = AssetDatabase.LoadAssetAtPath(path); if (so != null) discovered.Add(so); } } else { string[] guids = AssetDatabase.FindAssets("t:EnemyData_SO"); foreach (var g in guids) { var path = AssetDatabase.GUIDToAssetPath(g); var so = AssetDatabase.LoadAssetAtPath(path); if (so != null) discovered.Add(so); } } // For each id in enemySlotIds try to resolve an EnemyData_SO; if id==0 or not found push null (skip) foreach (var id in enemySlotIds) { if (id == 0) { results.Add(null); continue; } EnemyData_SO found = null; for (int j = 0; j < discovered.Count; j++) { var obj = discovered[j] as EnemyData_SO; if (obj == null) continue; string path = AssetDatabase.GetAssetPath(obj); if (!string.IsNullOrEmpty(path) && path.Contains(id.ToString())) { found = obj; break; } if (obj.enemyID == id) { found = obj; break; } } results.Add(found); } currentEnemySOs = results.ToArray(); #else string resourcesPath = ResolveResourcesRelativePath(!string.IsNullOrEmpty(runtimeEnemyResourcesFolderPath) ? runtimeEnemyResourcesFolderPath : selectedProjectFolderPath); List runtimeDiscovered = new List(); if (!string.IsNullOrEmpty(resourcesPath)) { resourcesPath = resourcesPath.Trim('/'); try { var arr = Resources.LoadAll(resourcesPath); if (arr != null && arr.Length > 0) runtimeDiscovered.AddRange(arr); if (runtimeDiscovered.Count == 0) { var arrAll = Resources.LoadAll(""); if (arrAll != null && arrAll.Length > 0) runtimeDiscovered.AddRange(arrAll); } } catch { var arrAll = Resources.LoadAll(""); if (arrAll != null && arrAll.Length > 0) runtimeDiscovered.AddRange(arrAll); } } else { var arr = Resources.LoadAll(""); if (arr != null && arr.Length > 0) runtimeDiscovered.AddRange(arr); } List results = new List(); foreach (var id in enemySlotIds) { if (id == 0) { results.Add(null); continue; } EnemyData_SO found = null; foreach (var o in runtimeDiscovered) { if (o == null) continue; if (o is EnemyData_SO e) { if (!string.IsNullOrEmpty(e.name) && e.name.Contains(id.ToString())) { found = e; break; } if (e.enemyID == id) { found = e; break; } } } // final attempt: Resources.Load by path if (found == null && !string.IsNullOrEmpty(resourcesPath)) { try { var byId = Resources.Load($"{resourcesPath}/{id}"); if (byId != null) found = byId; } catch { } } results.Add(found); } currentEnemySOs = results.ToArray(); #endif // build recognized list and update UI FilterRecognizedEnemies(); UpdateEnemyListText(); } // Build recognizedEnemySOs from currentEnemySOs by filtering out null entries (id==0 or unresolved) private void FilterRecognizedEnemies() { var list = new List(); if (currentEnemySOs != null) { foreach (var so in currentEnemySOs) { if (so != null) { list.Add(so); } } } recognizedEnemySOs = list.ToArray(); // update enemyCounterMax to reflect actual number of recognized enemies enemyCounterMax = recognizedEnemySOs != null ? recognizedEnemySOs.Length : 0; // clamp current index if (enemyCurrentCount < 0) enemyCurrentCount = 0; if (enemyCurrentCount > enemyCounterMax) enemyCurrentCount = enemyCounterMax; } // 返回解析到的敌人 SO 列表(副本) public EnemyData_SO[] GetCurrentEnemySOs() { return (EnemyData_SO[])currentEnemySOs.Clone(); } // Debug helper to print key fields of a TeamCharacterDataInfo private void DebugCharacterSO(TeamCharacterDataInfo so, int slotIndex, bool isEditor) { if (so == null) return; string mode = isEditor ? "Editor" : "Runtime"; string spriteCard = so.CharacterCardSprite != null ? so.CharacterCardSprite.name : "(null)"; string spriteTeam = so.CharacterTeamSprite != null ? so.CharacterTeamSprite.name : "(null)"; Debug.Log($"[teamUIController] ({mode}) Slot {slotIndex} -> CharacterID={so.CharacterID}, Name={so.CharacterName}, Skill={so.CharacterSkillName}, MaxHealth={so.CharacterMaxHealth}, CardSprite={spriteCard}, TeamSprite={spriteTeam}"); } /// /// 返回当前解析到的 SO 列表(数组副本) /// public TeamCharacterDataInfo[] GetCurrentAllySOs() { return (TeamCharacterDataInfo[])currentAllySOs.Clone(); } [Header("基本文本")] [Tooltip("title")] public TextMeshProUGUI songNametitle; public TextMeshProUGUI difficultyName; public TextMeshProUGUI difficultyID; public TextMeshProUGUI constructionName; [Tooltip("连击计数")] public Text comboCounter; [Tooltip("敌人出场数量")] public Text enemyCounter; [SerializeField] private int enemyCounterMax; // 本局游戏敌人最大数 [SerializeField] private int enemyCurrentCount; // 现在的敌人顺序 [Tooltip("当前总分")] public TextMeshProUGUI currentTotalScore; // new: runtime enemy instance and UI sync fields private EnemyCombatant enemyCombatantInstance; private Coroutine enemyFadeHealthCoroutine; private Coroutine enemyFadeManaCoroutine; private int prevEnemyHP = -1; private int prevEnemyMana = -1; [Header("Combo判定最低要求")] public ComboJudgeType comboJudgeType = ComboJudgeType.Perfect; private int combo = 0; public enum ComboJudgeType { Perfect, Great, Good, // Miss } [Header("teammate01")] [Tooltip("父物体-控制整体显隐")] public GameObject objectFather_ally01; [Tooltip("开关")] public bool isAlly01_active = true; [Tooltip("角色图")] public Image teammate01_characterImage; [Tooltip("血条")] public Image teammate01_healthImage; [Tooltip("虚血条")] public Image teammate01_fadehealthImage; [Tooltip("法力条")] public Image teammate01_manaImage; [Tooltip("虚法力条")] public Image teammate01_fadeManaImage; [Tooltip("名字文本")] public TextMeshProUGUI teammate01_nameText; [Tooltip("当前生命值/最大生命值")] public TextMeshProUGUI teammate01_healthRate; [Tooltip("当前分数/轨道分数上限")] public TextMeshProUGUI teammate01_current_scoreText; // rate : now score / max score [Tooltip("角色唯一识别码")] [SerializeField] private int ally01_id; [Tooltip("受击闪红")] public Image teammate01_hurtRedImage; [Tooltip("当前生命值")] [SerializeField] private int teammate01_currentHP; [Tooltip("最大生命值")] [SerializeField] private int teammate01_maxHP; [Tooltip("当前法力值")] [SerializeField] private int teammate01_currentMana; [Tooltip("最大法力值")] [SerializeField] private int teammate01_maxMana; [Header("teammate02")] [Tooltip("父物体-控制整体显隐")] public GameObject objectFather_ally02; [Tooltip("开关")] public bool isAlly02_active = true; [Tooltip("角色图")] public Image teammate02_characterImage; [Tooltip("血条")] public Image teammate02_healthImage; [Tooltip("虚血条")] public Image teammate02_fadehealthImage; [Tooltip("法力条")] public Image teammate02_manaImage; [Tooltip("虚法力条")] public Image teammate02_fadeManaImage; [Tooltip("名字文本")] public TextMeshProUGUI teammate02_nameText; [Tooltip("当前生命值/最大生命值")] public TextMeshProUGUI teammate02_healthRate; [Tooltip("当前分数/轨道分数上限")] public TextMeshProUGUI teammate02_current_scoreText; // rate : now score / max score [Tooltip("角色唯一识别码")] [SerializeField] private int ally02_id; [Tooltip("受击闪红")] public Image teammate02_hurtRedImage; [Tooltip("当前生命值")] [SerializeField] private int teammate02_currentHP; [Tooltip("最大生命值")] [SerializeField] private int teammate02_maxHP; [Tooltip("当前法力值")] [SerializeField] private int teammate02_currentMana; [Tooltip("最大法力值")] [SerializeField] private int teammate02_maxMana; [Header("teammate03")] [Tooltip("父物体-控制整体显隐")] public GameObject objectFather_ally03; [Tooltip("开关")] public bool isAlly03_active = true; [Tooltip("角色图")] public Image teammate03_characterImage; [Tooltip("血条")] public Image teammate03_healthImage; [Tooltip("虚血条")] public Image teammate03_fadehealthImage; [Tooltip("法力条")] public Image teammate03_manaImage; [Tooltip("虚法力条")] public Image teammate03_fadeManaImage; [Tooltip("名字文本")] public TextMeshProUGUI teammate03_nameText; [Tooltip("当前生命值/最大生命值")] public TextMeshProUGUI teammate03_healthRate; [Tooltip("当前分数/轨道分数上限")] public TextMeshProUGUI teammate03_current_scoreText; // rate : now score / max score [Tooltip("角色唯一识别码")] [SerializeField] private int ally03_id; [Tooltip("受击闪红")] public Image teammate03_hurtRedImage; [Tooltip("当前生命值")] [SerializeField] private int teammate03_currentHP; [Tooltip("最大生命值")] [SerializeField] private int teammate03_maxHP; [Tooltip("当前法力值")] [SerializeField] private int teammate03_currentMana; [Tooltip("最大法力值")] [SerializeField] private int teammate03_maxMana; [Header("teammate04")] [Tooltip("父物体-控制整体显隐")] public GameObject objectFather_ally04; [Tooltip("开关")] public bool isAlly04_active = true; [Tooltip("角色图")] public Image teammate04_characterImage; [Tooltip("血条")] public Image teammate04_healthImage; [Tooltip("虚血条")] public Image teammate04_fadehealthImage; [Tooltip("法力条")] public Image teammate04_manaImage; [Tooltip("虚法力条")] public Image teammate04_fadeManaImage; [Tooltip("名字文本")] public TextMeshProUGUI teammate04_nameText; [Tooltip("当前生命值/最大生命值")] public TextMeshProUGUI teammate04_healthRate; [Tooltip("当前分数/轨道分数上限")] public TextMeshProUGUI teammate04_current_scoreText; // rate : now score / max score [Tooltip("角色唯一识别码")] [SerializeField] private int ally04_id; [Tooltip("受击闪红")] public Image teammate04_hurtRedImage; [Tooltip("当前生命值")] [SerializeField] private int teammate04_currentHP; [Tooltip("最大生命值")] [SerializeField] private int teammate04_maxHP; [Tooltip("当前法力值")] [SerializeField] private int teammate04_currentMana; [Tooltip("最大法力值")] [SerializeField] private int teammate04_maxMana; [Header("teammate05")] [Tooltip("父物体-控制整体显隐")] public GameObject objectFather_ally05; [Tooltip("开关")] public bool isAlly05_active = true; [Tooltip("角色图")] public Image teammate05_characterImage; [Tooltip("血条")] public Image teammate05_healthImage; [Tooltip("虚血条")] public Image teammate05_fadehealthImage; [Tooltip("法力条")] public Image teammate05_manaImage; [Tooltip("虚法力条")] public Image teammate05_fadeManaImage; [Tooltip("名字文本")] public TextMeshProUGUI teammate05_nameText; [Tooltip("当前生命值/最大生命值")] public TextMeshProUGUI teammate05_healthRate; [Tooltip("当前分数/轨道分数上限")] public TextMeshProUGUI teammate05_current_scoreText; // rate : now score / max score [Tooltip("角色唯一识别码")] [SerializeField] private int ally05_id; [Tooltip("受击闪红")] public Image teammate05_hurtRedImage; [Tooltip("当前生命值")] [SerializeField] private int teammate05_currentHP; [Tooltip("最大生命值")] [SerializeField] private int teammate05_maxHP; [Tooltip("当前法力值")] [SerializeField] private int teammate05_currentMana; [Tooltip("最大法力值")] [SerializeField] private int teammate05_maxMana; [Header("currentEnemy")] [Tooltip("父物体-控制整体显隐")] public GameObject objectFather_enemy; [Tooltip("开关")] public bool isEnemy_active = true; [Tooltip("角色图")] public Image currentEnemy_characterImage; [Tooltip("血条")] public Image currentEnemy_healthImage; [Tooltip("虚血条")] public Image currentEnemy_fadehealthImage; [Tooltip("法力条")] public Image currentEnemy_manaImage; [Tooltip("虚法力条")] public Image currentEnemy_fademanaImage; [Tooltip("共计血条")] public Image allEnemy_totalHealthImage; [Tooltip("共计虚血条")] public Image allEnemy_totalFadehealthImage; [Tooltip("名字文本")] public Text currentEnemy_nameText; [Tooltip("类型文本")] public Text currentEnemy_typeText; [Tooltip("当前敌人生命值/最大生命值")] public TextMeshProUGUI currentEnemy_healthRate; [Tooltip("所有敌人生命值/最大生命值总计")] public TextMeshProUGUI totalEnemy_healthRate; [Tooltip("当前敌人法力值/最大法力值")] public TextMeshProUGUI currentEnemy_manaRate; [Tooltip("当前分数/轨道分数上限")] public TextMeshProUGUI currentEnemy_current_scoreText; // rate : now score / max score [Tooltip("敌人唯一识别码")] [SerializeField] private int enemy_id; [Tooltip("受击闪红")] public Image currentEnemy_hurtRedImage; [Tooltip("敌人排队")] public TextMeshProUGUI enemyList_rateText; // Start is called once before the first execution of Update after the MonoBehaviour is created void Start() { PopulateAllySOsFromIds(); // initialize previous flags so Update can detect changes prevAllyActive[0] = isAlly01_active; prevAllyActive[1] = isAlly02_active; prevAllyActive[2] = isAlly03_active; prevAllyActive[3] = isAlly04_active; prevAllyActive[4] = isAlly05_active; // Ensure score text references are resolved so ScoreManager and AllyCombatant can write to them ResolveScoreTextReferences(); // Initialize enemies list and spawn first enemy if any PopulateEnemySOsFromIds(); // set enemyCounterMax based on recognized (resolved non-null) enemies so zeros/unresolved don't count enemyCounterMax = recognizedEnemySOs != null ? recognizedEnemySOs.Length : 0; enemyCurrentCount = 0; InitializeEnemyInstance(); if (recognizedEnemySOs != null && recognizedEnemySOs.Length > 0) { SpawnNextEnemy(); } } // Update is called once per frame void Update() { // detect external changes to isAllyX_active and toggle objects accordingly bool[] current = new bool[] { isAlly01_active, isAlly02_active, isAlly03_active, isAlly04_active, isAlly05_active }; for (int i = 0; i < 5; i++) { if (prevAllyActive[i] != current[i]) { ToggleAllyObject(i, current[i]); prevAllyActive[i] = current[i]; } } // Sync enemy UI with EnemyCombatant values if (enemyCombatantInstance != null) { int hp = enemyCombatantInstance.currentHP; int mana = enemyCombatantInstance.currentMana; if (prevEnemyHP != hp) { UpdateEnemyHealthVisuals(prevEnemyHP, hp, true); prevEnemyHP = hp; } if (prevEnemyMana != mana) { UpdateEnemyManaVisuals(prevEnemyMana, mana, true); prevEnemyMana = mana; } } } private void FixedUpdate() { } private void Awake() { if (Instance == null) Instance = this; else Destroy(gameObject); } /// /// 由判定系统调用,传入判定结果(如"Perfect"、"Great"、"Good"、"Miss") /// public void OnJudgeResult(string result) { if (IsCombo(result)) { combo++; } else { combo = 0; } if (comboCounter != null) comboCounter.text = combo.ToString(); } /// /// 判断当前判定结果是否达到combo要求 /// private bool IsCombo(string result) { switch (comboJudgeType) { case ComboJudgeType.Perfect: return result == "Perfect"; case ComboJudgeType.Great: return result == "Perfect" || result == "Great"; case ComboJudgeType.Good: return result == "Perfect" || result == "Great" || result == "Good"; default: return false; } } // helper to toggle ally object by slot index private void ToggleAllyObject(int slotIndex, bool active) { GameObject target = null; switch (slotIndex) { case 0: target = ally01_object ?? objectFather_ally01; break; case 1: target = ally02_object ?? objectFather_ally02; break; case 2: target = ally03_object ?? objectFather_ally03; break; case 3: target = ally04_object ?? objectFather_ally04; break; case 4: target = ally05_object ?? objectFather_ally05; break; } if (target != null) target.SetActive(active); } // Try to ensure the teammateXX_current_scoreText fields are assigned; if null, search the corresponding parent for TMP/Text private void ResolveScoreTextReferences() { void TryResolve(ref TextMeshProUGUI field, GameObject parent, string slotName) { if (field != null) return; if (parent != null) { var tmp = parent.GetComponentInChildren(true); if (tmp != null) { field = tmp; Debug.Log($"[teamUIController] Resolved {slotName} TMP from parent {parent.name} -> {tmp.gameObject.name}"); return; } var legacy = parent.GetComponentInChildren(true); if (legacy != null) { // if only legacy Text exists, try to create a TMP component to avoid type mismatch var go = legacy.gameObject; var created = go.GetComponent() ?? go.AddComponent(); created.text = legacy.text; field = created; Debug.LogWarning($"[teamUIController] Found legacy Text for {slotName} under {parent.name}. Added/used TMP component on {go.name} and copied text."); return; } } Debug.LogWarning($"[teamUIController] Could not resolve {slotName} TMP (parent '{parent?.name}')"); } TryResolve(ref teammate01_current_scoreText, objectFather_ally01, "teammate01_current_scoreText"); TryResolve(ref teammate02_current_scoreText, objectFather_ally02, "teammate02_current_scoreText"); TryResolve(ref teammate03_current_scoreText, objectFather_ally03, "teammate03_current_scoreText"); TryResolve(ref teammate04_current_scoreText, objectFather_ally04, "teammate04_current_scoreText"); TryResolve(ref teammate05_current_scoreText, objectFather_ally05, "teammate05_current_scoreText"); // total score: if assigned field is null, try to find any TMP in scene named "currentTotalScore" or under this object if (currentTotalScore == null) { var tmp = GetComponentInChildren(true); if (tmp != null) { currentTotalScore = tmp; Debug.Log($"[teamUIController] Resolved currentTotalScore from child {tmp.gameObject.name}"); } } } /// /// 返回相邻的友军槽索引(左右),边侧只有一侧 /// public int[] GetAdjacentAllyIndices(int slotIndex) { var list = new List(); if (slotIndex - 1 >= 0) list.Add(slotIndex - 1); if (slotIndex + 1 <= 4) list.Add(slotIndex + 1); return list.ToArray(); } /// /// 返回指定槽位对应的友军 GameObject(优先返回 allyX_object,否则返回 objectFather_allyXX),可能为 null /// public GameObject GetAllyObjectBySlot(int slotIndex) { switch (slotIndex) { case 0: return ally01_object ?? objectFather_ally01; case 1: return ally02_object ?? objectFather_ally02; case 2: return ally03_object ?? objectFather_ally03; case 3: return ally04_object ?? objectFather_ally04; case 4: return ally05_object ?? objectFather_ally05; default: return null; } } // helper to set teammate character image based on slot index private void SetTeammateCharacterImage(int slotIndex, Sprite sprite) { if (sprite == null) return; switch (slotIndex) { case 0: if (teammate01_characterImage != null) teammate01_characterImage.sprite = sprite; break; case 1: if (teammate02_characterImage != null) teammate02_characterImage.sprite = sprite; break; case 2: if (teammate03_characterImage != null) teammate03_characterImage.sprite = sprite; break; case 3: if (teammate04_characterImage != null) teammate04_characterImage.sprite = sprite; break; case 4: if (teammate05_characterImage != null) teammate05_characterImage.sprite = sprite; break; } } // ------------------- Enemy spawn & UI sync helpers ------------------- private void InitializeEnemyInstance() { // Try to find an existing enemy object in scene enemyCombatantInstance = FindFirstObjectByType(); if (enemyCombatantInstance == null) { var go = new GameObject("thisEnemy"); enemyCombatantInstance = go.AddComponent(); } // attach listeners if (enemyCombatantInstance != null) { enemyCombatantInstance.OnEnemyDied -= OnEnemyDiedHandler; enemyCombatantInstance.OnEnemyRevived -= OnEnemyRevivedHandler; enemyCombatantInstance.OnEnemyDied += OnEnemyDiedHandler; enemyCombatantInstance.OnEnemyRevived += OnEnemyRevivedHandler; // initialize previous tracked values prevEnemyHP = enemyCombatantInstance.currentHP; prevEnemyMana = enemyCombatantInstance.currentMana; } } private void SpawnNextEnemy() { // handle case: no configured/recognized enemies -> show empty state but keep UI visible if (recognizedEnemySOs == null || recognizedEnemySOs.Length == 0) { if (objectFather_enemy != null) objectFather_enemy.SetActive(true); if (currentEnemy_nameText != null) currentEnemy_nameText.text = "(No Enemy)"; if (currentEnemy_characterImage != null) currentEnemy_characterImage.sprite = null; if (currentEnemy_healthImage != null) currentEnemy_healthImage.fillAmount = 0f; if (currentEnemy_fadehealthImage != null) currentEnemy_fadehealthImage.fillAmount = 0f; if (currentEnemy_healthRate != null) currentEnemy_healthRate.text = "0/0"; if (currentEnemy_manaImage != null) currentEnemy_manaImage.fillAmount = 0f; if (currentEnemy_fademanaImage != null) currentEnemy_fademanaImage.fillAmount = 0f; if (enemyCounter != null) enemyCounter.text = "0/0"; if (currentEnemy_typeText != null) currentEnemy_typeText.text = "(普通)"; UpdateEnemyListText(); return; } if (enemyCurrentCount < 0) enemyCurrentCount = 0; if (enemyCurrentCount >= recognizedEnemySOs.Length) { // All enemies processed: show empty/finished state but keep UI visible if (objectFather_enemy != null) objectFather_enemy.SetActive(true); if (currentEnemy_nameText != null) currentEnemy_nameText.text = "(All Defeated)"; if (currentEnemy_characterImage != null) currentEnemy_characterImage.sprite = null; if (currentEnemy_healthImage != null) currentEnemy_healthImage.fillAmount = 0f; if (currentEnemy_fadehealthImage != null) currentEnemy_fadehealthImage.fillAmount = 0f; if (currentEnemy_healthRate != null) currentEnemy_healthRate.text = "0/0"; if (currentEnemy_manaImage != null) currentEnemy_manaImage.fillAmount = 0f; if (currentEnemy_fademanaImage != null) currentEnemy_fademanaImage.fillAmount = 0f; if (enemyCounter != null) enemyCounter.text = "0/" + enemyCounterMax.ToString(); if (currentEnemy_typeText != null) currentEnemy_typeText.text = "(普通)"; UpdateEnemyListText(); return; } var so = recognizedEnemySOs[enemyCurrentCount]; if (so == null) { Debug.LogWarning($"[teamUIController] SpawnNextEnemy: SO is null at index {enemyCurrentCount}"); // skip null slots (treated as empty) and advance enemyCurrentCount++; SpawnNextEnemy(); return; } InitializeEnemyInstance(); enemyCombatantInstance.InitializeFromSO(so); // set UI visuals (name / sprites) if (currentEnemy_nameText != null) currentEnemy_nameText.text = so.enemyName ?? so.name; if (currentEnemy_characterImage != null) { var sprite = so.enemy_Profile != null ? so.enemy_Profile : so.enemy_Image; if (sprite != null) currentEnemy_characterImage.sprite = sprite; else currentEnemy_characterImage.sprite = null; } // set type text: show for Simple/Elite/Boss/Legend if (currentEnemy_typeText != null) { switch (so.enemyType) { case EnemyData_SO.EnemyType.Elite: currentEnemy_typeText.text = "(精英)"; break; case EnemyData_SO.EnemyType.Boss: currentEnemy_typeText.text = "(首领)"; break; case EnemyData_SO.EnemyType.Legend: currentEnemy_typeText.text = "(史诗)"; break; default: currentEnemy_typeText.text = "(普通)"; break; } } // ensure UI parent is visible if (objectFather_enemy != null) objectFather_enemy.SetActive(true); // reset previous values so Update picks up initial state prevEnemyHP = enemyCombatantInstance.currentHP; prevEnemyMana = enemyCombatantInstance.currentMana; UpdateEnemyUIImmediate(); // update enemy counter text if (enemyCounter != null) { enemyCounter.text = $"{Mathf.Clamp(enemyCurrentCount+1,1,999)}/{enemyCounterMax}"; } // refresh the enemy list display UpdateEnemyListText(); } // Build a readable representation of the enemy queue and write to enemyList_rateText private void UpdateEnemyListText() { if (enemyList_rateText == null) return; int total = recognizedEnemySOs != null ? recognizedEnemySOs.Length : 0; int displayIndex = 0; if (total == 0) displayIndex = 0; else displayIndex = Mathf.Clamp(enemyCurrentCount + 1, 1, total); // show as "current/total" (e.g. 1/3). When no enemies, show 0/0 enemyList_rateText.text = total == 0 ? "0/0" : $"{displayIndex}/{total}"; } private void OnEnemyDiedHandler(EnemyCombatant e) { // advance to next enemy after death enemyCurrentCount++; SpawnNextEnemy(); } private void OnEnemyRevivedHandler(EnemyCombatant e) { // refresh UI UpdateEnemyUIImmediate(); } private void UpdateEnemyUIImmediate() { if (enemyCombatantInstance == null) return; // health if (currentEnemy_healthImage != null) currentEnemy_healthImage.fillAmount = enemyCombatantInstance.maxHP > 0 ? (float)enemyCombatantInstance.currentHP / enemyCombatantInstance.maxHP : 0f; if (currentEnemy_fadehealthImage != null) currentEnemy_fadehealthImage.fillAmount = currentEnemy_healthImage != null ? currentEnemy_healthImage.fillAmount : (enemyCombatantInstance.maxHP > 0 ? (float)enemyCombatantInstance.currentHP / enemyCombatantInstance.maxHP : 0f); if (currentEnemy_healthRate != null) currentEnemy_healthRate.text = $"{enemyCombatantInstance.currentHP}/{enemyCombatantInstance.maxHP}"; // mana visuals (new) if (currentEnemy_manaImage != null) currentEnemy_manaImage.fillAmount = enemyCombatantInstance.maxMana > 0 ? (float)enemyCombatantInstance.currentMana / enemyCombatantInstance.maxMana : 0f; if (currentEnemy_fademanaImage != null) currentEnemy_fademanaImage.fillAmount = currentEnemy_manaImage != null ? currentEnemy_manaImage.fillAmount : (enemyCombatantInstance.maxMana > 0 ? (float)enemyCombatantInstance.currentMana / enemyCombatantInstance.maxMana : 0f); if (currentEnemy_manaRate != null) currentEnemy_manaRate.text = enemyCombatantInstance.maxMana > 0 ? $"{enemyCombatantInstance.currentMana}/{enemyCombatantInstance.maxMana}" : "0/0"; if (allEnemy_totalHealthImage != null) { // optional: aggregate total health across remaining enemies int totalCur = 0, totalMax = 0; if (currentEnemySOs != null) { for (int i = enemyCurrentCount; i < currentEnemySOs.Length; i++) { var so = currentEnemySOs[i]; if (so == null) continue; totalMax += Mathf.Max(1, so.enemy_maxHP); // use max for not-yet-spawned enemies if (i == enemyCurrentCount) totalCur += enemyCombatantInstance.currentHP; else totalCur += so.enemy_maxHP; } } if (totalMax > 0) { allEnemy_totalHealthImage.fillAmount = (float)totalCur / totalMax; if (totalEnemy_healthRate != null) totalEnemy_healthRate.text = $"{totalCur}/{totalMax}"; } } } private void UpdateEnemyHealthVisuals(int oldHP, int newHP, bool animateFade) { if (enemyCombatantInstance == null) return; if (currentEnemy_healthImage != null) { float newFill = enemyCombatantInstance.maxHP > 0 ? (float)newHP / enemyCombatantInstance.maxHP : 0f; currentEnemy_healthImage.fillAmount = Mathf.MoveTowards(currentEnemy_healthImage.fillAmount, newFill, 1f); } if (currentEnemy_healthRate != null) currentEnemy_healthRate.text = $"{newHP}/{enemyCombatantInstance.maxHP}"; if (enemyFadeHealthCoroutine != null) StopCoroutine(enemyFadeHealthCoroutine); if (currentEnemy_fadehealthImage != null) { float target = currentEnemy_healthImage != null ? currentEnemy_healthImage.fillAmount : (enemyCombatantInstance.maxHP > 0 ? (float)newHP / enemyCombatantInstance.maxHP : 0f); if (animateFade && Application.isPlaying) enemyFadeHealthCoroutine = StartCoroutine(EnemyFadeHealthCoroutine(target)); else currentEnemy_fadehealthImage.fillAmount = target; } // hurt flash if hp decreased if (oldHP >= 0 && newHP < oldHP && currentEnemy_hurtRedImage != null && Application.isPlaying) { StopCoroutine("EnemyHurtFlash"); StartCoroutine(EnemyHurtFlash()); } } private void UpdateEnemyManaVisuals(int oldMana, int newMana, bool animateFade) { if (enemyCombatantInstance == null) return; if (currentEnemy_manaRate != null) currentEnemy_manaRate.text = enemyCombatantInstance.maxMana > 0 ? $"{newMana}/{enemyCombatantInstance.maxMana}" : "0/0"; if (enemyFadeManaCoroutine != null) StopCoroutine(enemyFadeManaCoroutine); if (currentEnemy_fademanaImage != null) { float target = currentEnemy_manaImage != null ? currentEnemy_manaImage.fillAmount : (enemyCombatantInstance.maxMana > 0 ? (float)newMana / enemyCombatantInstance.maxMana : 0f); if (animateFade && Application.isPlaying) enemyFadeManaCoroutine = StartCoroutine(EnemyFadeManaCoroutine(target)); else if (currentEnemy_fademanaImage != null) currentEnemy_fademanaImage.fillAmount = target; } // update main mana bar immediately if (currentEnemy_manaImage != null) { currentEnemy_manaImage.fillAmount = enemyCombatantInstance.maxMana > 0 ? (float)newMana / enemyCombatantInstance.maxMana : 0f; } } private IEnumerator EnemyFadeHealthCoroutine(float targetFill) { if (currentEnemy_fadehealthImage == null) yield break; float start = currentEnemy_fadehealthImage.fillAmount; float duration = 0.6f; float t = 0f; while (t < duration) { t += Time.deltaTime; currentEnemy_fadehealthImage.fillAmount = Mathf.Lerp(start, targetFill, t / duration); yield return null; } currentEnemy_fadehealthImage.fillAmount = targetFill; } private IEnumerator EnemyFadeManaCoroutine(float targetFill) { if (currentEnemy_fademanaImage == null) yield break; float start = currentEnemy_fademanaImage.fillAmount; float duration = 0.4f; float t = 0f; while (t < duration) { t += Time.deltaTime; currentEnemy_fademanaImage.fillAmount = Mathf.Lerp(start, targetFill, t / duration); yield return null; } currentEnemy_fademanaImage.fillAmount = targetFill; } private IEnumerator EnemyHurtFlash() { if (currentEnemy_hurtRedImage == null) yield break; Color c = currentEnemy_hurtRedImage.color; c.a = 1f; currentEnemy_hurtRedImage.color = c; yield return new WaitForSeconds(0.2f); float dur = 0.4f; float t = 0f; while (t < dur) { t += Time.deltaTime; c.a = Mathf.Lerp(1f, 0f, t / dur); currentEnemy_hurtRedImage.color = c; yield return null; } c.a = 0f; currentEnemy_hurtRedImage.color = c; } }