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() { 0, 0, 0, 0, 0 }; // --- 新增:解析后的当前五个 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 && arrAll.Length > 0) // Fixed: changed arr.Length to arrAll.Length { 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 && arrAll.Length > 0) // Fixed: changed arr.Length to arrAll.Length 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 } // ResolveResourcesRelativePath helper method is missing, assuming it exists externally or is part of a base class / extension. // Since the user is providing the full class, I will assume it's either defined outside this snippet or is implicitly handled, // but the code within #if UNITY_EDITOR is what matters for the file content. // For completeness, if it's not defined, the runtime block would throw an error, but as I cannot add it, I must assume it's defined elsewhere. // A simplified placeholder for ResolveResourcesRelativePath for compilation (assuming it strips Resources/ and leading/trailing slashes): private string ResolveResourcesRelativePath(string path) { if (string.IsNullOrEmpty(path)) return string.Empty; path = path.Replace("\\", "/"); path = path.Replace("Assets/Resources/", ""); path = path.Replace("Resources/", ""); return path.Trim('/').ToLower(); } // Populate enemy SOs from enemySlotIds using editor/runtime paths similar to allies public void PopulateEnemySOsFromIds() { Debug.Log($"PopulateEnemySOsFromIds called, enemySlotIds: {string.Join(",", enemySlotIds)}"); 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); // Fixed: arr.Length to arrAll.Length } } catch { var arrAll = Resources.LoadAll(""); if (arrAll != null && arrAll.Length > 0) runtimeDiscovered.AddRange(arrAll); } // Fixed: arr.Length to arrAll.Length } 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 (!string.IsNullOrEmpty(e.name) && e.name.StartsWith(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(); // Calculate total max HP for all recognized enemies // Note: totalMaxHP is a class member and is intended to be the fixed maximum HP for the entire encounter. RecalculateTotalMaxHP(); if (recognizedEnemySOs.Length > 0) { enemyCurrentCount = 0; SpawnNextEnemy(); } // If enemies were populated and we have some, start spawning from the beginning if (recognizedEnemySOs.Length > 0) { enemyCurrentCount = 0; SpawnNextEnemy(); } } // 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; [Header("谱面总分")] public TextMeshProUGUI allSum_pmScore; [Header("爱豆总分")] public TextMeshProUGUI allSum_idolScore; // 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; // For total enemy health bar private int totalMaxHP = 0; // 保留现有 totalMaxHP private Coroutine totalFadeHealthCoroutine; // 移除 private float prevTotalHealthFill = 1f; [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("pmScore")] public TextMeshProUGUI red_pmScore_sum; [Header("idolScore")] public TextMeshProUGUI red_idolScore_sum; [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("pmScore")] public TextMeshProUGUI green_pmScore_sum; [Header("idolScore")] public TextMeshProUGUI green_idolScore_sum; [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("pmScore")] public TextMeshProUGUI yellow_pmScore_sum; [Header("idolScore")] public TextMeshProUGUI yellow_idolScore_sum; [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("pmScore")] public TextMeshProUGUI purple_pmScore_sum; [Header("idolScore")] public TextMeshProUGUI purple_idolScore_sum; [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("pmScore")] public TextMeshProUGUI blue_pmScore_sum; [Header("idolScore")] public TextMeshProUGUI blue_idolScore_sum; [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() { // Load ally slot IDs from PlayerPrefs allySlotIds[0] = PlayerPrefs.GetInt("selected_heroSlot01_heroID", 0); allySlotIds[1] = PlayerPrefs.GetInt("selected_heroSlot02_heroID", 0); allySlotIds[2] = PlayerPrefs.GetInt("selected_heroSlot03_heroID", 0); allySlotIds[3] = PlayerPrefs.GetInt("selected_heroSlot04_heroID", 0); allySlotIds[4] = PlayerPrefs.GetInt("selected_heroSlot05_heroID", 0); 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); // 当当前敌人的 HP 变化时,也需要更新总血条 UpdateAllEnemyTotalHealthUIOnHpChange(hp); 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 // Try to ensure the teammateXX_current_scoreText fields and pm sum fields are assigned; // if null, search the corresponding parent, a scene object by name, or convert legacy Text -> TMP. private void ResolveScoreTextReferences() { TextMeshProUGUI FindOrConvertTMP(GameObject candidate, string sceneNameHint = null) { if (candidate != null) { // Try TMP first var tmp = candidate.GetComponentInChildren(true); if (tmp != null) return tmp; // Legacy Text fallback: copy text to a new TMP component on the same GameObject var legacy = candidate.GetComponentInChildren(true); if (legacy != null) { var go = legacy.gameObject; var added = go.GetComponent() ?? go.AddComponent(); added.text = legacy.text; // Optionally disable legacy renderer to avoid duplicate rendering legacy.enabled = false; Debug.LogWarning($"[teamUIController] Converted legacy Text to TMP on {go.name} (parent {candidate.name})"); return added; } } // If candidate not found or no components under it, try direct scene object by name if (!string.IsNullOrEmpty(sceneNameHint)) { var goByName = GameObject.Find(sceneNameHint); if (goByName != null) { var tmp2 = goByName.GetComponent(); if (tmp2 != null) return tmp2; var legacy2 = goByName.GetComponent(); if (legacy2 != null) { var created = goByName.GetComponent() ?? goByName.AddComponent(); created.text = legacy2.text; legacy2.enabled = false; Debug.LogWarning($"[teamUIController] Converted legacy Text to TMP on {goByName.name}"); return created; } // also try children var childTmp = goByName.GetComponentInChildren(true); if (childTmp != null) return childTmp; var childLegacy = goByName.GetComponentInChildren(true); if (childLegacy != null) { var go = childLegacy.gameObject; var created = go.GetComponent() ?? go.AddComponent(); created.text = childLegacy.text; childLegacy.enabled = false; Debug.LogWarning($"[teamUIController] Converted legacy Text to TMP on {go.name} (child of {goByName.name})"); return created; } } } // Best-effort fallback: find any TMP in scene that matches hint substring if (!string.IsNullOrEmpty(sceneNameHint)) { var all = Resources.FindObjectsOfTypeAll(); var lowHint = sceneNameHint.ToLower(); foreach (var t in all) { if (t == null || t.gameObject == null) continue; if (t.gameObject.name.ToLower().Contains(lowHint) || t.gameObject.name.ToLower().Contains("pm") && t.gameObject.name.ToLower().Contains(lowHint)) return t; } } return null; } // Helper: try resolve TMP under parent, then scene by name void TryResolveScore(ref TextMeshProUGUI field, GameObject parent, string sceneName) { if (field != null) return; // 1) under parent field = FindOrConvertTMP(parent, null); if (field != null) { Debug.Log($"[teamUIController] Resolved score TMP from parent {parent?.name} -> {field.gameObject.name}"); return; } // 2) by scene object name field = FindOrConvertTMP(null, sceneName); if (field != null) { Debug.Log($"[teamUIController] Resolved score TMP by scene name '{sceneName}' -> {field.gameObject.name}"); return; } Debug.LogWarning($"[teamUIController] Could not resolve {sceneName} TMP (parent '{parent?.name}')"); } // Resolve per-track current score TMPs (these are usually set in inspector; fallback to parent lookup) TryResolveScore(ref teammate01_current_scoreText, objectFather_ally01, "teammate01_current_scoreText"); TryResolveScore(ref teammate02_current_scoreText, objectFather_ally02, "teammate02_current_scoreText"); TryResolveScore(ref teammate03_current_scoreText, objectFather_ally03, "teammate03_current_scoreText"); TryResolveScore(ref teammate04_current_scoreText, objectFather_ally04, "teammate04_current_scoreText"); TryResolveScore(ref teammate05_current_scoreText, objectFather_ally05, "teammate05_current_scoreText"); // Resolve pm score TMPs and aggregate sum (recommend these fields be TextMeshProUGUI) TryResolveScore(ref red_pmScore_sum, null, "red_pmScore_sum"); TryResolveScore(ref green_pmScore_sum, null, "green_pmScore_sum"); TryResolveScore(ref yellow_pmScore_sum, null, "yellow_pmScore_sum"); TryResolveScore(ref purple_pmScore_sum, null, "purple_pmScore_sum"); TryResolveScore(ref blue_pmScore_sum, null, "blue_pmScore_sum"); // allSum_pmScore may have been declared as TextMeshProUGUI; if it's legacy Text, convert similarly // If you declared allSum_pmScore as TextMeshProUGUI field, do: TryResolveScore(ref allSum_pmScore, null, "allSum_pmScore"); // total score: if assigned field is null, try to find TMP child under this object or by name "currentTotalScore" if (currentTotalScore == null) { var tmp = GetComponentInChildren(true); if (tmp != null) { currentTotalScore = tmp; Debug.Log($"[teamUIController] Resolved currentTotalScore from child {tmp.gameObject.name}"); } else { var byName = GameObject.Find("currentTotalScore"); if (byName != null) { var t = byName.GetComponent() ?? byName.GetComponent()?.gameObject.AddComponent(); if (t != null) currentTotalScore = t; } } } } /// /// 返回相邻的友军槽索引(左右),边侧只有一侧 /// 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(); // 刷新总血条,在没有敌人时显示 0/TotalMax UpdateAllEnemyTotalHealthUIImmediate(); 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(); // 刷新总血条,在所有敌人死亡时显示 0/TotalMax UpdateAllEnemyTotalHealthUIImmediate(); 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(); } // teamUIController.cs - 【新增】 // ========================================================= // 供 BeatmapManager 调用的方法:设置单个敌人 HP // ========================================================= /// /// 将计算得到的单个敌人最大生命值写入到所有已识别的敌人 Scriptable Object 中。 /// public void ApplyCalculatedEnemyHP(int individualMaxHP) { // 确保 recognizedEnemySOs 数组不为空 if (recognizedEnemySOs != null && recognizedEnemySOs.Length > 0) { Debug.Log($"[teamUIController] Applying calculated HP: {individualMaxHP} to {recognizedEnemySOs.Length} recognized enemies."); // 1. 修改 SO 数据:遍历所有已识别的敌人 SO,并设置其最大生命值 foreach (var so in recognizedEnemySOs) { // 假设 EnemyData_SO 包含 public int enemy_maxHP; 字段 if (so != null) { so.enemy_maxHP = individualMaxHP; } } // 2. 重新计算总血量并更新总血条 RecalculateTotalMaxHP(); // 3. 强制刷新当前生成的敌人实例 // 因为 Populate 已经在 Apply 之前生成了旧数据的敌人,所以需要重置 if (enemyCombatantInstance != null && enemyCurrentCount >= 0 && enemyCurrentCount < recognizedEnemySOs.Length) { var currentSO = recognizedEnemySOs[enemyCurrentCount]; if (currentSO != null) { Debug.Log($"[teamUIController] Re-initializing current enemy instance with new HP: {individualMaxHP}"); enemyCombatantInstance.InitializeFromSO(currentSO); // 重新使用新的 SO 数据初始化实例 UpdateEnemyUIImmediate(); // 立即刷新 UI } } } } // ========================================================= // 【新增】总生命值重新计算方法(封装原有的累加逻辑) // ========================================================= /// /// 遍历所有已识别敌人的 SO,累加它们的生命值,更新 totalMaxHP,并立即刷新总血条。 /// private void RecalculateTotalMaxHP() { totalMaxHP = 0; if (recognizedEnemySOs != null) { foreach (var so in recognizedEnemySOs) { if (so != null) totalMaxHP += so.enemy_maxHP; } } Debug.Log($"[teamUIController] Total Max HP has been recalculated to: {totalMaxHP}"); // 刷新总血条上限显示,防止出现延迟 UpdateAllEnemyTotalHealthUIImmediate(); } // 新增:在当前敌人 HP 变化时,更新总血条 UI private void UpdateAllEnemyTotalHealthUIOnHpChange(int newCurrentHP) { if (allEnemy_totalHealthImage == null) return; // 重新计算总当前血量(currentEnemySOs 保持不变,但 active enemy 的 current HP 改变了) int totalCur = 0; if (currentEnemySOs != null) { for (int i = enemyCurrentCount; i < currentEnemySOs.Length; i++) { var so = currentEnemySOs[i]; if (so == null) continue; // 仅更新当前活跃敌人的血量,后续敌人保持 Max HP if (i == enemyCurrentCount) totalCur += newCurrentHP; else totalCur += so.enemy_maxHP; } } float newFill = totalMaxHP > 0 ? (float)totalCur / totalMaxHP : 0f; // 1. 更新实血条 allEnemy_totalHealthImage.fillAmount = newFill; if (totalEnemy_healthRate != null) totalEnemy_healthRate.text = $"{totalCur}/{totalMaxHP}"; // 2. Animate fade bar (only when health decreases) if (allEnemy_totalFadehealthImage != null) { float currentFadeFill = allEnemy_totalFadehealthImage.fillAmount; if (currentFadeFill > newFill) { // Health decreased: stop previous coroutine and start new fall animation if (totalFadeHealthCoroutine != null) StopCoroutine(totalFadeHealthCoroutine); if (Application.isPlaying) totalFadeHealthCoroutine = StartCoroutine(TotalFadeHealthCoroutine(newFill)); else allEnemy_totalFadehealthImage.fillAmount = newFill; } else if (currentFadeFill < newFill) { // Health increased (healing): update fade bar immediately if (totalFadeHealthCoroutine != null) StopCoroutine(totalFadeHealthCoroutine); allEnemy_totalFadehealthImage.fillAmount = newFill; } // If currentFadeFill == newFill, do nothing. } } // 新增:用于在 SpawnNextEnemy/No Enemy 时立即更新总血条(不使用动画逻辑) private void UpdateAllEnemyTotalHealthUIImmediate() { if (allEnemy_totalHealthImage == null) return; int totalCur = 0; if (currentEnemySOs != null) { for (int i = enemyCurrentCount; i < currentEnemySOs.Length; i++) { var so = currentEnemySOs[i]; if (so == null) continue; // 仅更新当前活跃敌人的血量,后续敌人保持 Max HP if (i == enemyCurrentCount) { // 使用 Combatant 实例的当前 HP if (enemyCombatantInstance != null) totalCur += enemyCombatantInstance.currentHP; else totalCur += so.enemy_maxHP; // Fallback } else { totalCur += so.enemy_maxHP; } } } float newFill = totalMaxHP > 0 ? (float)totalCur / totalMaxHP : 0f; // 立即更新实血条和虚血条 allEnemy_totalHealthImage.fillAmount = newFill; if (allEnemy_totalFadehealthImage != null) allEnemy_totalFadehealthImage.fillAmount = newFill; if (totalEnemy_healthRate != null) totalEnemy_healthRate.text = $"{totalCur}/{totalMaxHP}"; // 停止可能正在运行的虚血条动画 if (totalFadeHealthCoroutine != null) StopCoroutine(totalFadeHealthCoroutine); } 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"; // Aggregate total health logic (modified to call the helper method for animation control) UpdateAllEnemyTotalHealthUIImmediate(); } 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; } private IEnumerator TotalFadeHealthCoroutine(float targetFill) { if (allEnemy_totalFadehealthImage == null) yield break; float start = allEnemy_totalFadehealthImage.fillAmount; float duration = 0.6f; float t = 0f; while (t < duration) { t += Time.deltaTime; allEnemy_totalFadehealthImage.fillAmount = Mathf.Lerp(start, targetFill, t / duration); yield return null; } allEnemy_totalFadehealthImage.fillAmount = targetFill; } }